From 0096f7c88279e71af9e6ac23149de7534692a9e3 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 31 Jul 2022 23:42:59 -0700 Subject: [PATCH 01/10] split autonestbox out from zone --- plugins/CMakeLists.txt | 1 + plugins/autonestbox.cpp | 418 ++++++++++++++++++++++++++++++++++++ plugins/lua/autonestbox.lua | 51 +++++ plugins/zone.cpp | 234 +------------------- 4 files changed, 472 insertions(+), 232 deletions(-) create mode 100644 plugins/autonestbox.cpp create mode 100644 plugins/lua/autonestbox.lua diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4a4b74eaa..6ef5b59db 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -92,6 +92,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(autolabor autolabor.cpp) dfhack_plugin(automaterial automaterial.cpp LINK_LIBRARIES lua) dfhack_plugin(automelt automelt.cpp) + dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) dfhack_plugin(autotrade autotrade.cpp) dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua) dfhack_plugin(burrows burrows.cpp LINK_LIBRARIES lua) diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp new file mode 100644 index 000000000..6c878806c --- /dev/null +++ b/plugins/autonestbox.cpp @@ -0,0 +1,418 @@ +// - full automation of handling mini-pastures over nestboxes: +// go through all pens, check if they are empty and placed over a nestbox +// find female tame egg-layer who is not assigned to another pen and assign it to nestbox pasture +// maybe check for minimum age? it's not that useful to fill nestboxes with freshly hatched birds +// state and sleep setting is saved the first time autonestbox is started (to avoid writing stuff if the plugin is never used) + +#include "df/building_cagest.h" +#include "df/building_civzonest.h" +#include "df/building_nest_boxst.h" +#include "df/general_ref_building_civzone_assignedst.h" +#include "df/world.h" + +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "modules/Buildings.h" +#include "modules/Gui.h" +#include "modules/Maps.h" +#include "modules/Persistence.h" +#include "modules/Units.h" +#include "modules/World.h" + +using std::string; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("autonestbox"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + +static const string autonestbox_help = + "Assigns unpastured female egg-layers to nestbox zones.\n" + "Requires that you create pen/pasture zones above nestboxes.\n" + "If the pen is bigger than 1x1 the nestbox must be in the top left corner.\n" + "Only 1 unit will be assigned per pen, regardless of the size.\n" + "The age of the units is currently not checked, most birds grow up quite fast.\n" + "When called without options autonestbox will instantly run once.\n" + "Usage:\n" + "\n" + "enable autonestbox\n" + " Start checking for unpastured egg-layers and assigning them to nestbox zones.\n" + "autonestbox now\n" + " Run a scan and assignment cycle right now. Does not require that the plugin is enabled.\n" + "autonestbox ticks \n" + " Change the number of ticks between scan and assignment cycles when the plugin is enabled.\n" + " The default is 6000 (about 8 days)\n"; + +namespace DFHack { + DBG_DECLARE(autonestbox, status); + DBG_DECLARE(autonestbox, cycle); +} + +static const string CONFIG_KEY = "autonestbox/config"; +static PersistentDataItem config; +enum ConfigValues { + IS_ENABLED = 0, + CYCLE_TICKS = 1, +}; +static int get_config_val(int index) { + if (!config.isValid()) + return -1; + return config.ival(index); +} +static bool set_config_val(int index, int value) { + if (!config.isValid()) + return false; + config.ival(index) = value; + return true; +} + +static bool did_complain = false; // avoids message spam +static size_t cycle_counter = 0; // how many ticks since the last cycle + +struct autonestbox_options { + // whether to display help + bool help = false; + + // whether to run a cycle right now + bool now = false; + + // how many ticks to wait between automatic cycles, -1 means unset + int32_t ticks = -1; + + static struct_identity _identity; +}; +static const struct_field_info autonestbox_options_fields[] = { + { struct_field_info::PRIMITIVE, "help", offsetof(autonestbox_options, help), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "now", offsetof(autonestbox_options, now), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "ticks", offsetof(autonestbox_options, ticks), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::END } +}; +struct_identity autonestbox_options::_identity(sizeof(autonestbox_options), &df::allocator_fn, NULL, "autonestbox_options", NULL, autonestbox_options_fields); + +static command_result df_autonestbox(color_ostream &out, vector ¶meters); +static void autonestbox_cycle(color_ostream &out); + +static void init_autonestbox(color_ostream &out) { + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) + config = World::AddPersistentData(CONFIG_KEY); + + if (get_config_val(IS_ENABLED) == -1) { + set_config_val(IS_ENABLED, 0); + set_config_val(CYCLE_TICKS, 6000); + } + + if (is_enabled) + set_config_val(IS_ENABLED, 1); + else + is_enabled = (get_config_val(IS_ENABLED) == 1); + did_complain = false; +} + +static void cleanup_autonestbox(color_ostream &out) { + is_enabled = false; +} + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + commands.push_back(PluginCommand( + "autonestbox", + "Auto-assign egg-laying female pets to nestbox zones.", + df_autonestbox, + false, + autonestbox_help.c_str())); + + init_autonestbox(out); + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Maps::IsValid()) { + out.printerr("Cannot run autonestbox without a loaded map.\n"); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + if (is_enabled) + init_autonestbox(out); + else + cleanup_autonestbox(out); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + cleanup_autonestbox(out); + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + switch (event) { + case DFHack::SC_MAP_LOADED: + init_autonestbox(out); + break; + case DFHack::SC_MAP_UNLOADED: + cleanup_autonestbox(out); + break; + default: + break; + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && ++cycle_counter >= (size_t)get_config_val(CYCLE_TICKS)) + autonestbox_cycle(out); + return CR_OK; +} + +static bool get_options(color_ostream &out, + autonestbox_options &opts, + const vector ¶meters) +{ + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, parameters.size() + 2) || + !Lua::PushModulePublic( + out, L, "plugins.autonestbox", "parse_commandline")) { + out.printerr("Failed to load autonestbox Lua code\n"); + return false; + } + + Lua::Push(L, &opts); + for (const string ¶m : parameters) + Lua::Push(L, param); + + if (!Lua::SafeCall(out, L, parameters.size() + 1, 0)) + return false; + + return true; +} + +static command_result df_autonestbox(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!Maps::IsValid()) { + out.printerr("Cannot run autonestbox without a loaded map.\n"); + return CR_FAILURE; + } + + autonestbox_options opts; + if (!get_options(out, opts, parameters) || opts.help) + return CR_WRONG_USAGE; + + if (opts.ticks > -1) { + set_config_val(CYCLE_TICKS, opts.ticks); + INFO(status,out).print("New cycle timer: %d ticks.\n", opts.ticks); + } + else if (opts.now) { + autonestbox_cycle(out); + } + return CR_OK; +} + +///////////////////////////////////////////////////// +// autonestbox cycle logic +// + +static bool isEmptyPasture(df::building *building) { + if (!Buildings::isPenPasture(building)) + return false; + df::building_civzonest *civ = (df::building_civzonest *)building; + return (civ->assigned_units.size() == 0); +} + +static bool isFreeNestboxAtPos(int32_t x, int32_t y, int32_t z) { + for (auto building : world->buildings.all) { + if (building->getType() == df::building_type::NestBox + && building->x1 == x + && building->y1 == y + && building->z == z) { + df::building_nest_boxst *nestbox = (df::building_nest_boxst *)building; + if (nestbox->claimed_by == -1 && nestbox->contained_items.size() == 1) { + return true; + } + } + } + return false; +} + +static df::building* findFreeNestboxZone() { + for (auto building : world->buildings.all) { + if (isEmptyPasture(building) && + Buildings::isActive(building) && + isFreeNestboxAtPos(building->x1, building->y1, building->z)) { + return building; + } + } + return NULL; +} + +static bool isInBuiltCage(df::unit *unit) { + for (auto building : world->buildings.all) { + if (building->getType() == df::building_type::Cage) { + df::building_cagest* cage = (df::building_cagest *)building; + for (auto unitid : cage->assigned_units) { + if (unitid == unit->id) + return true; + } + } + } + return false; +} + +// check if assigned to pen, pit, (built) cage or chain +// note: BUILDING_CAGED is not set for animals (maybe it's used for dwarves who get caged as sentence) +// animals in cages (no matter if built or on stockpile) get the ref CONTAINED_IN_ITEM instead +// removing them from cages on stockpiles is no problem even without clearing the ref +// and usually it will be desired behavior to do so. +static bool isAssigned(df::unit *unit) { + for (auto ref : unit->general_refs) { + auto rtype = ref->getType(); + if(rtype == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED + || rtype == df::general_ref_type::BUILDING_CAGED + || rtype == df::general_ref_type::BUILDING_CHAIN + || (rtype == df::general_ref_type::CONTAINED_IN_ITEM && isInBuiltCage(unit))) { + return true; + } + } + return false; +} + +static bool isFreeEgglayer(df::unit *unit) +{ + return Units::isActive(unit) && !Units::isUndead(unit) + && Units::isFemale(unit) + && Units::isTame(unit) + && Units::isOwnCiv(unit) + && Units::isEggLayer(unit) + && !isAssigned(unit) + && !Units::isGrazer(unit) // exclude grazing birds because they're messy + && !Units::isMerchant(unit) // don't steal merchant mounts + && !Units::isForest(unit); // don't steal birds from traders, they hate that +} + +static df::unit * findFreeEgglayer() { + for (auto unit : world->units.all) { + if (isFreeEgglayer(unit)) + return unit; + } + return NULL; +} + +static df::general_ref_building_civzone_assignedst * createCivzoneRef() { + static bool vt_initialized = false; + + // after having run successfully for the first time it's safe to simply create the object + if (vt_initialized) { + return (df::general_ref_building_civzone_assignedst *) + df::general_ref_building_civzone_assignedst::_identity.instantiate(); + } + + // being called for the first time, need to initialize the vtable + for (auto creature : world->units.all) { + for (auto ref : creature->general_refs) { + if (ref->getType() == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED) { + if (strict_virtual_cast(ref)) { + vt_initialized = true; + // !! calling new() doesn't work, need _identity.instantiate() instead !! + return (df::general_ref_building_civzone_assignedst *) + df::general_ref_building_civzone_assignedst::_identity.instantiate(); + } + } + } + } + return NULL; +} + +static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building *building) { + // try to get a fresh civzone ref + df::general_ref_building_civzone_assignedst *ref = createCivzoneRef(); + if (!ref) { + ERR(cycle,out).print("Could not find a clonable activity zone reference!" + " You need to pen/pasture/pit at least one creature" + " before autonestbox can function.\n"); + return false; + } + + ref->building_id = building->id; + unit->general_refs.push_back(ref); + + df::building_civzonest *civz = (df::building_civzonest *)building; + civz->assigned_units.push_back(unit->id); + + INFO(cycle,out).print("Unit %d (%s) assigned to nestbox zone %d (%s)\n", + unit->id, Units::getRaceName(unit).c_str(), + building->id, building->name.c_str()); + + return true; +} + +static size_t countFreeEgglayers() { + size_t count = 0; + for (auto unit : world->units.all) { + if (isFreeEgglayer(unit)) + ++count; + } + return count; +} + +static size_t assign_nestboxes(color_ostream &out) { + size_t processed = 0; + df::building *free_building = NULL; + df::unit *free_unit = NULL; + do { + free_building = findFreeNestboxZone(); + free_unit = findFreeEgglayer(); + if (free_building && free_unit) { + if (!assignUnitToZone(out, free_unit, free_building)) { + DEBUG(cycle,out).print("Failed to assign unit to building.\n"); + return processed; + } + ++processed; + } + } while (free_unit && free_building); + + if (free_unit && !free_building) { + static size_t old_count = 0; + size_t freeEgglayers = countFreeEgglayers(); + // avoid spamming the same message + if (old_count != freeEgglayers) + did_complain = false; + old_count = freeEgglayers; + if (!did_complain) { + stringstream ss; + ss << freeEgglayers; + string announce = "Not enough free nestbox zones found! You need " + ss.str() + " more."; + Gui::showAnnouncement(announce, 6, true); + out << announce << endl; + did_complain = true; + } + } + return processed; +} + +static void autonestbox_cycle(color_ostream &out) { + // mark that we have recently run + cycle_counter = 0; + + size_t processed = assign_nestboxes(out); + if (processed > 0) { + stringstream ss; + ss << processed << " nestboxes were assigned."; + string announce = ss.str(); + Gui::showAnnouncement(announce, 2, false); + out << announce << endl; + // can complain again + // (might lead to spamming the same message twice, but catches the case + // where for example 2 new egglayers hatched right after 2 zones were created and assigned) + did_complain = false; + } +} diff --git a/plugins/lua/autonestbox.lua b/plugins/lua/autonestbox.lua new file mode 100644 index 000000000..f7140b0b7 --- /dev/null +++ b/plugins/lua/autonestbox.lua @@ -0,0 +1,51 @@ +local _ENV = mkmodule('plugins.autonestbox') + +local argparse = require('argparse') + +local function is_int(val) + return val and val == math.floor(val) +end + +local function is_positive_int(val) + return is_int(val) and val > 0 +end + +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 + +function parse_commandline(opts, ...) + local positionals = process_args(opts, {...}) + + if opts.help then return end + + local in_ticks = false + for _,arg in ipairs(positionals) do + if in_ticks then + arg = tonumber(arg) + if not is_positive_int(arg) then + qerror('number of ticks must be a positive integer: ' .. arg) + else + opts.ticks = arg + end + in_ticks = false + elseif arg == 'ticks' then + in_ticks = true + elseif arg == 'now' then + opts.now = true + end + end + + if in_ticks then + qerror('missing number of ticks') + end +end + +return _ENV diff --git a/plugins/zone.cpp b/plugins/zone.cpp index 385d79728..6353bfd35 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -112,7 +112,6 @@ REQUIRE_GLOBAL(ui_menu_width); using namespace DFHack::Gui; command_result df_zone (color_ostream &out, vector & parameters); -command_result df_autonestbox (color_ostream &out, vector & parameters); command_result df_autobutcher(color_ostream &out, vector & parameters); DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable); @@ -201,19 +200,6 @@ const string zone_help_examples = " well, unless you have a mod with egg-laying male elves who give milk...\n"; -const string autonestbox_help = - "Assigns unpastured female egg-layers to nestbox zones.\n" - "Requires that you create pen/pasture zones above nestboxes.\n" - "If the pen is bigger than 1x1 the nestbox must be in the top left corner.\n" - "Only 1 unit will be assigned per pen, regardless of the size.\n" - "The age of the units is currently not checked, most birds grow up quite fast.\n" - "When called without options autonestbox will instantly run once.\n" - "Options:\n" - " start - run every X frames (df simulation ticks)\n" - " default: X=6000 (~60 seconds at 100fps)\n" - " stop - stop running automatically\n" - " sleep X - change timer to sleep X frames between runs.\n"; - const string autobutcher_help = "Assigns your lifestock for slaughter once it reaches a specific count. Requires\n" "that you add the target race(s) to a watch list. Only tame units will be\n" @@ -277,27 +263,19 @@ command_result init_autobutcher(color_ostream &out); command_result cleanup_autobutcher(color_ostream &out); command_result start_autobutcher(color_ostream &out); -command_result init_autonestbox(color_ostream &out); -command_result cleanup_autonestbox(color_ostream &out); -command_result start_autonestbox(color_ostream &out); - /////////////// -// stuff for autonestbox and autobutcher +// stuff for autobutcher // should be moved to own plugin once the tool methods it shares with the zone plugin are moved to Unit.h / Building.h command_result autoNestbox( color_ostream &out, bool verbose ); command_result autoButcher( color_ostream &out, bool verbose ); -static bool enable_autonestbox = false; static bool enable_autobutcher = false; static bool enable_autobutcher_autowatch = false; -static size_t sleep_autonestbox = 6000; static size_t sleep_autobutcher = 6000; -static bool autonestbox_did_complain = false; // avoids message spam static PersistentDataItem config_autobutcher; -static PersistentDataItem config_autonestbox; DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { @@ -306,14 +284,11 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan case DFHack::SC_MAP_LOADED: // initialize from the world just loaded init_autobutcher(out); - init_autonestbox(out); break; case DFHack::SC_MAP_UNLOADED: - enable_autonestbox = false; enable_autobutcher = false; // cleanup cleanup_autobutcher(out); - cleanup_autonestbox(out); break; default: break; @@ -323,18 +298,8 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan DFhackCExport command_result plugin_onupdate ( color_ostream &out ) { - static size_t ticks_autonestbox = 0; static size_t ticks_autobutcher = 0; - if(enable_autonestbox) - { - if(++ticks_autonestbox >= sleep_autonestbox) - { - ticks_autonestbox = 0; - autoNestbox(out, false); - } - } - if(enable_autobutcher) { if(++ticks_autobutcher >= sleep_autobutcher) @@ -2242,138 +2207,6 @@ command_result df_zone (color_ostream &out, vector & parameters) return CR_OK; } -//////////////////// -// autonestbox stuff - -command_result df_autonestbox(color_ostream &out, vector & parameters) -{ - CoreSuspender suspend; - - bool verbose = false; - - for (size_t i = 0; i < parameters.size(); i++) - { - string & p = parameters[i]; - - if (p == "help" || p == "?") - { - out << autonestbox_help << endl; - return CR_OK; - } - if (p == "start") - { - autonestbox_did_complain = false; - start_autonestbox(out); - return autoNestbox(out, verbose); - } - if (p == "stop") - { - enable_autonestbox = false; - if(config_autonestbox.isValid()) - config_autonestbox.ival(0) = 0; - out << "Autonestbox stopped." << endl; - return CR_OK; - } - else if(p == "verbose") - { - verbose = true; - } - else if(p == "sleep") - { - if(i == parameters.size()-1) - { - out.printerr("No duration specified!\n"); - return CR_WRONG_USAGE; - } - else - { - size_t ticks = 0; - stringstream ss(parameters[i+1]); - i++; - ss >> ticks; - if(ticks <= 0) - { - out.printerr("Invalid duration specified (must be > 0)!\n"); - return CR_WRONG_USAGE; - } - sleep_autonestbox = ticks; - if(config_autonestbox.isValid()) - config_autonestbox.ival(1) = sleep_autonestbox; - out << "New sleep timer for autonestbox: " << ticks << " ticks." << endl; - return CR_OK; - } - } - else - { - out << "Unknown command: " << p << endl; - return CR_WRONG_USAGE; - } - } - return autoNestbox(out, verbose); -} - -command_result autoNestbox( color_ostream &out, bool verbose = false ) -{ - bool stop = false; - size_t processed = 0; - - if (!Maps::IsValid()) - { - out.printerr("Map is not available!\n"); - enable_autonestbox = false; - return CR_FAILURE; - } - - do - { - df::building * free_building = findFreeNestboxZone(); - df::unit * free_unit = findFreeEgglayer(); - if(free_building && free_unit) - { - command_result result = assignUnitToBuilding(out, free_unit, free_building, verbose); - if(result != CR_OK) - return result; - processed ++; - } - else - { - stop = true; - if(free_unit && !free_building) - { - static size_t old_count = 0; - size_t freeEgglayers = countFreeEgglayers(); - // avoid spamming the same message - if(old_count != freeEgglayers) - autonestbox_did_complain = false; - old_count = freeEgglayers; - if(!autonestbox_did_complain) - { - stringstream ss; - ss << freeEgglayers; - string announce = "Not enough free nestbox zones found! You need " + ss.str() + " more."; - Gui::showAnnouncement(announce, 6, true); - out << announce << endl; - autonestbox_did_complain = true; - } - } - } - } while (!stop); - if(processed > 0) - { - stringstream ss; - ss << processed; - string announce; - announce = ss.str() + " nestboxes were assigned."; - Gui::showAnnouncement(announce, 2, false); - out << announce << endl; - // can complain again - // (might lead to spamming the same message twice, but catches the case - // where for example 2 new egglayers hatched right after 2 zones were created and assigned) - autonestbox_did_complain = false; - } - return CR_OK; -} - //////////////////// // autobutcher stuff @@ -3129,7 +2962,7 @@ command_result autoButcher( color_ostream &out, bool verbose = false ) } //////////////////////////////////////////////////// -// autobutcher and autonestbox start/init/cleanup +// autobutcher start/init/cleanup command_result start_autobutcher(color_ostream &out) { @@ -3227,62 +3060,6 @@ command_result cleanup_autobutcher(color_ostream &out) return CR_OK; } -command_result start_autonestbox(color_ostream &out) -{ - plugin_enable(out, true); - enable_autonestbox = true; - - if (!config_autonestbox.isValid()) - { - config_autonestbox = World::AddPersistentData("autonestbox/config"); - - if (!config_autonestbox.isValid()) - { - out << "Cannot enable autonestbox without a world!" << endl; - return CR_OK; - } - - config_autonestbox.ival(1) = sleep_autonestbox; - } - - config_autonestbox.ival(0) = enable_autonestbox; - - out << "Starting autonestbox." << endl; - init_autonestbox(out); - return CR_OK; -} - -command_result init_autonestbox(color_ostream &out) -{ - cleanup_autonestbox(out); - - config_autonestbox = World::GetPersistentData("autonestbox/config"); - if(config_autonestbox.isValid()) - { - if (config_autonestbox.ival(0) == -1) - { - config_autonestbox.ival(0) = enable_autonestbox; - config_autonestbox.ival(1) = sleep_autonestbox; - out << "Autonestbox's persistent config object was invalid!" << endl; - } - else - { - enable_autonestbox = config_autonestbox.ival(0); - sleep_autonestbox = config_autonestbox.ival(1); - } - } - if (enable_autonestbox) - plugin_enable(out, true); - return CR_OK; -} - -command_result cleanup_autonestbox(color_ostream &out) -{ - // nothing to cleanup currently - // (future version of autonestbox could store info about cages for useless male kids) - return CR_OK; -} - // abuse WatchedRace struct for counting stocks (since it sorts by gender and age) // calling method must delete pointer! WatchedRace * checkRaceStocksTotal(int race) @@ -4121,24 +3898,17 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector Date: Tue, 2 Aug 2022 01:07:13 -0700 Subject: [PATCH 02/10] split autobutcher out from zone --- plugins/CMakeLists.txt | 3 +- plugins/autobutcher.cpp | 1166 +++++++++++++++++++++ plugins/lua/autobutcher.lua | 89 ++ plugins/lua/zone.lua | 12 - plugins/zone.cpp | 1965 ++++------------------------------- 5 files changed, 1437 insertions(+), 1798 deletions(-) create mode 100644 plugins/autobutcher.cpp create mode 100644 plugins/lua/autobutcher.lua delete mode 100644 plugins/lua/zone.lua diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 6ef5b59db..338db4ab9 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -83,6 +83,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(3dveins 3dveins.cpp) dfhack_plugin(add-spatter add-spatter.cpp) # dfhack_plugin(advtools advtools.cpp) + dfhack_plugin(autobutcher autobutcher.cpp LINK_LIBRARIES lua) dfhack_plugin(autochop autochop.cpp) dfhack_plugin(autoclothing autoclothing.cpp) dfhack_plugin(autodump autodump.cpp) @@ -178,7 +179,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(workflow workflow.cpp LINK_LIBRARIES lua) dfhack_plugin(workNow workNow.cpp) dfhack_plugin(xlsxreader xlsxreader.cpp LINK_LIBRARIES lua xlsxio_read_STATIC zip expat) - dfhack_plugin(zone zone.cpp LINK_LIBRARIES lua) + dfhack_plugin(zone zone.cpp) # If you are adding a plugin that you do not intend to commit to the DFHack repo, # see instructions for adding "external" plugins at the end of this file. diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp new file mode 100644 index 000000000..0d7a9c632 --- /dev/null +++ b/plugins/autobutcher.cpp @@ -0,0 +1,1166 @@ +// - full automation of marking live-stock for slaughtering +// races can be added to a watchlist and it can be set how many male/female kids/adults are left alive +// adding to the watchlist can be automated as well. +// config for autobutcher (state and sleep setting) is saved the first time autobutcher is started +// config for watchlist entries is saved when they are created or modified + +#include +#include + +#include "df/building_cagest.h" +#include "df/creature_raw.h" +#include "df/world.h" + +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" + +#include "modules/Gui.h" +#include "modules/Maps.h" +#include "modules/Persistence.h" +#include "modules/Units.h" +#include "modules/World.h" + +using std::string; +using std::unordered_map; +using std::unordered_set; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("autobutcher"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(world); + +const string autobutcher_help = + "Assigns your lifestock for slaughter once it reaches a specific count. Requires\n" + "that you add the target race(s) to a watch list. Only tame units will be\n" + "processed. Named units will be completely ignored (you can give animals\n" + "nicknames with the tool 'rename unit' to protect them from getting slaughtered\n" + "automatically. Trained war or hunting pets will be ignored.\n" + "Once you have too much adults, the oldest will be butchered first.\n" + "Once you have too much kids, the youngest will be butchered first.\n" + "If you don't set a target count the following default will be used:\n" + "1 male kid, 5 female kids, 1 male adult, 5 female adults.\n" + "Options:\n" + " start - run every X frames (df simulation ticks)\n" + " default: X=6000 (~60 seconds at 100fps)\n" + " stop - stop running automatically\n" + " sleep X - change timer to sleep X frames between runs.\n" + " watch R - start watching race(s)\n" + " R = valid race RAW id (ALPACA, BIRD_TURKEY, etc)\n" + " or a list of RAW ids seperated by spaces\n" + " or the keyword 'all' which affects your whole current watchlist.\n" + " unwatch R - stop watching race(s)\n" + " the current target settings will be remembered\n" + " forget R - unwatch race(s) and forget target settings for it/them\n" + " autowatch - automatically adds all new races (animals you buy\n" + " from merchants, tame yourself or get from migrants)\n" + " to the watch list using default target count\n" + " noautowatch - stop auto-adding new races to the watch list\n" + " list - print status and watchlist\n" + " list_export - print status and watchlist in batchfile format\n" + " can be used to copy settings into another savegame\n" + " usage: 'dfhack-run autobutcher list_export > xyz.bat' \n" + " target fk mk fa ma R\n" + " - set target count for specified race:\n" + " fk = number of female kids\n" + " mk = number of male kids\n" + " fa = number of female adults\n" + " ma = number of female adults\n" + " R = 'all' sets count for all races on the current watchlist\n" + " including the races which are currenly set to 'unwatched'\n" + " and sets the new default for future watch commands\n" + " R = 'new' sets the new default for future watch commands\n" + " without changing your current watchlist\n" + " example - print some usage examples\n"; + +const string autobutcher_help_example = + "Examples:\n" + " autobutcher target 4 3 2 1 ALPACA BIRD_TURKEY\n" + " autobutcher watch ALPACA BIRD_TURKEY\n" + " autobutcher start\n" + " This means you want to have max 7 kids (4 female, 3 male) and max 3 adults\n" + " (2 female, 1 male) of the races alpaca and turkey. Once the kids grow up the\n" + " oldest adults will get slaughtered. Excess kids will get slaughtered starting\n" + " the the youngest to allow that the older ones grow into adults.\n" + " autobutcher target 0 0 0 0 new\n" + " autobutcher autowatch\n" + " autobutcher start\n" + " This tells autobutcher to automatically put all new races onto the watchlist\n" + " and mark unnamed tame units for slaughter as soon as they arrive in your\n" + " fortress. Settings already made for some races will be left untouched.\n"; + +namespace DFHack { + DBG_DECLARE(autobutcher, status); + DBG_DECLARE(autobutcher, cycle); +} + +static const string CONFIG_KEY = "autobutcher/config"; +static const string WATCHLIST_CONFIG_KEY_PREFIX = "autobutcher/watchlist/"; + +static PersistentDataItem config; +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_CYCLE_TICKS = 1, + CONFIG_AUTOWATCH = 2, + CONFIG_DEFAULT_FK = 3, + CONFIG_DEFAULT_MK = 4, + CONFIG_DEFAULT_FA = 5, + CONFIG_DEFAULT_MA = 6, +}; +static int get_config_val(int index) { + if (!config.isValid()) + return -1; + return config.ival(index); +} +static bool get_config_bool(int index) { + return get_config_val(index) == 1; +} +static void set_config_val(int index, int value) { + if (config.isValid()) + config.ival(index) = value; +} +static void set_config_bool(int index, bool value) { + set_config_val(index, value ? 1 : 0); +} + +static unordered_map race_to_id; +static size_t cycle_counter = 0; // how many ticks since the last cycle + +static size_t DEFAULT_CYCLE_TICKS = 6000; + +struct autobutcher_options { + // whether to display help + bool help = false; + + // the command to run. + string command; + + // the set of (unverified) races that the command should affect, and whether + // "all" or "new" was specified as the race + vector races; + bool races_all = false; + bool races_new = false; + + // params for the "target" command + int32_t fk = -1; + int32_t mk = -1; + int32_t fa = -1; + int32_t ma = -1; + + // how many ticks to wait between automatic cycles, -1 means unset + int32_t ticks = -1; + + static struct_identity _identity; + + // non-virtual destructor so offsetof() still works for the fields + ~autobutcher_options() { + for (auto str : races) + delete str; + } +}; +static const struct_field_info autobutcher_options_fields[] = { + { struct_field_info::PRIMITIVE, "help", offsetof(autobutcher_options, help), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "command", offsetof(autobutcher_options, command), df::identity_traits::get(), 0, 0 }, + { struct_field_info::STL_VECTOR_PTR, "races", offsetof(autobutcher_options, races), df::identity_traits::get(), 0, 0 }, + { struct_field_info::PRIMITIVE, "races_all", offsetof(autobutcher_options, races_all), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "races_new", offsetof(autobutcher_options, races_new), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "fk", offsetof(autobutcher_options, fk), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "mk", offsetof(autobutcher_options, mk), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "fa", offsetof(autobutcher_options, fa), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "ma", offsetof(autobutcher_options, ma), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "ticks", offsetof(autobutcher_options, ticks), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::END } +}; +struct_identity autobutcher_options::_identity(sizeof(autobutcher_options), &df::allocator_fn, NULL, "autobutcher_options", NULL, autobutcher_options_fields); + +static void init_autobutcher(color_ostream &out); +static void cleanup_autobutcher(color_ostream &out); +static command_result df_autobutcher(color_ostream &out, vector ¶meters); +static void autobutcher_cycle(color_ostream &out); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + commands.push_back(PluginCommand( + "autobutcher", + "Automatically butcher excess livestock.", + df_autobutcher, + false, + autobutcher_help.c_str())); + + init_autobutcher(out); + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (!Maps::IsValid()) { + out.printerr("Cannot run autobutcher without a loaded map.\n"); + return CR_FAILURE; + } + + if (enable != is_enabled) { + is_enabled = enable; + if (is_enabled) + init_autobutcher(out); + else + cleanup_autobutcher(out); + } + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + cleanup_autobutcher(out); + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + switch (event) { + case DFHack::SC_MAP_LOADED: + init_autobutcher(out); + break; + case DFHack::SC_MAP_UNLOADED: + cleanup_autobutcher(out); + break; + default: + break; + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && ++cycle_counter >= (size_t)get_config_val(CONFIG_CYCLE_TICKS)) + autobutcher_cycle(out); + return CR_OK; +} + +///////////////////////////////////////////////////// +// autobutcher config logic +// + +static void doMarkForSlaughter(df::unit *unit) { + unit->flags2.bits.slaughter = 1; +} + +// getUnitAge() returns 0 if born in current year, therefore the look at birth_time in that case +// (assuming that the value from there indicates in which tick of the current year the unit was born) +static bool compareUnitAgesYounger(df::unit *i, df::unit *j) { + int32_t age_i = (int32_t)Units::getAge(i, true); + int32_t age_j = (int32_t)Units::getAge(j, true); + if (age_i == 0 && age_j == 0) { + age_i = i->birth_time; + age_j = j->birth_time; + } + return age_i < age_j; +} + +static bool compareUnitAgesOlder(df::unit* i, df::unit* j) { + int32_t age_i = (int32_t)Units::getAge(i, true); + int32_t age_j = (int32_t)Units::getAge(j, true); + if(age_i == 0 && age_j == 0) { + age_i = i->birth_time; + age_j = j->birth_time; + } + return age_i > age_j; +} + +enum unit_ptr_index { + fk_index = 0, + mk_index = 1, + fa_index = 2, + ma_index = 3 +}; + +struct WatchedRace { +public: + PersistentDataItem rconfig; + + int raceId; + bool isWatched; // if true, autobutcher will process this race + + // target amounts + unsigned fk; // max female kids + unsigned mk; // max male kids + unsigned fa; // max female adults + unsigned ma; // max male adults + + // amounts of protected (not butcherable) units + unsigned fk_prot; + unsigned fa_prot; + unsigned mk_prot; + unsigned ma_prot; + + // butcherable units + vector unit_ptr[4]; + + // priority butcherable units + vector prot_ptr[4]; + + WatchedRace(color_ostream &out, int id, bool watch, unsigned _fk, unsigned _mk, unsigned _fa, unsigned _ma) { + raceId = id; + isWatched = watch; + fk = _fk; + mk = _mk; + fa = _fa; + ma = _ma; + fk_prot = fa_prot = mk_prot = ma_prot = 0; + + DEBUG(status,out).print("creating new WatchedRace: id=%d, watched=%s, fk=%u, mk=%u, fa=%u, ma=%u\n", + id, watch ? "true" : "false", fk, mk, fa, ma); + } + + WatchedRace(color_ostream &out, const PersistentDataItem &p) + : WatchedRace(out, p.ival(0), p.ival(1), p.ival(2), p.ival(3), p.ival(4), p.ival(5)) { + rconfig = p; + } + + ~WatchedRace() { + ClearUnits(); + } + + void UpdateConfig(color_ostream &out) { + if(!rconfig.isValid()) { + string keyname = WATCHLIST_CONFIG_KEY_PREFIX + Units::getRaceNameById(raceId); + rconfig = World::GetPersistentData(keyname, NULL); + } + if(rconfig.isValid()) { + rconfig.ival(0) = raceId; + rconfig.ival(1) = isWatched; + rconfig.ival(2) = fk; + rconfig.ival(3) = mk; + rconfig.ival(4) = fa; + rconfig.ival(5) = ma; + } + else { + ERR(status,out).print("could not create persistent key for race: %s", + Units::getRaceNameById(raceId).c_str()); + } + } + + void RemoveConfig(color_ostream &out) { + if(!rconfig.isValid()) + return; + World::DeletePersistentData(rconfig); + } + + void SortUnitsByAge() { + sort(unit_ptr[fk_index].begin(), unit_ptr[fk_index].end(), compareUnitAgesOlder); + sort(unit_ptr[mk_index].begin(), unit_ptr[mk_index].end(), compareUnitAgesOlder); + sort(unit_ptr[fa_index].begin(), unit_ptr[fa_index].end(), compareUnitAgesYounger); + sort(unit_ptr[ma_index].begin(), unit_ptr[ma_index].end(), compareUnitAgesYounger); + sort(prot_ptr[fk_index].begin(), prot_ptr[fk_index].end(), compareUnitAgesOlder); + sort(prot_ptr[mk_index].begin(), prot_ptr[mk_index].end(), compareUnitAgesOlder); + sort(prot_ptr[fa_index].begin(), prot_ptr[fa_index].end(), compareUnitAgesYounger); + sort(prot_ptr[ma_index].begin(), prot_ptr[ma_index].end(), compareUnitAgesYounger); + } + + void PushUnit(df::unit *unit) { + if(Units::isFemale(unit)) { + if(Units::isBaby(unit) || Units::isChild(unit)) + unit_ptr[fk_index].push_back(unit); + else + unit_ptr[fa_index].push_back(unit); + } + else //treat sex n/a like it was male + { + if(Units::isBaby(unit) || Units::isChild(unit)) + unit_ptr[mk_index].push_back(unit); + else + unit_ptr[ma_index].push_back(unit); + } + } + + void PushPriorityUnit(df::unit *unit) { + if(Units::isFemale(unit)) { + if(Units::isBaby(unit) || Units::isChild(unit)) + prot_ptr[fk_index].push_back(unit); + else + prot_ptr[fa_index].push_back(unit); + } + else { + if(Units::isBaby(unit) || Units::isChild(unit)) + prot_ptr[mk_index].push_back(unit); + else + prot_ptr[ma_index].push_back(unit); + } + } + + void PushProtectedUnit(df::unit *unit) { + if(Units::isFemale(unit)) { + if(Units::isBaby(unit) || Units::isChild(unit)) + fk_prot++; + else + fa_prot++; + } + else { //treat sex n/a like it was male + if(Units::isBaby(unit) || Units::isChild(unit)) + mk_prot++; + else + ma_prot++; + } + } + + void ClearUnits() { + fk_prot = fa_prot = mk_prot = ma_prot = 0; + for (size_t i = 0; i < 4; i++) { + unit_ptr[i].clear(); + prot_ptr[i].clear(); + } + } + + int ProcessUnits(vector& unit_ptr, vector& unit_pri_ptr, unsigned prot, unsigned goal) { + int subcount = 0; + while (unit_pri_ptr.size() && (unit_ptr.size() + unit_pri_ptr.size() + prot > goal)) { + df::unit *unit = unit_pri_ptr.back(); + doMarkForSlaughter(unit); + unit_pri_ptr.pop_back(); + subcount++; + } + while (unit_ptr.size() && (unit_ptr.size() + prot > goal)) { + df::unit *unit = unit_ptr.back(); + doMarkForSlaughter(unit); + unit_ptr.pop_back(); + subcount++; + } + return subcount; + } + + int ProcessUnits() { + SortUnitsByAge(); + int slaughter_count = 0; + slaughter_count += ProcessUnits(unit_ptr[fk_index], prot_ptr[fk_index], fk_prot, fk); + slaughter_count += ProcessUnits(unit_ptr[mk_index], prot_ptr[mk_index], mk_prot, mk); + slaughter_count += ProcessUnits(unit_ptr[fa_index], prot_ptr[fa_index], fa_prot, fa); + slaughter_count += ProcessUnits(unit_ptr[ma_index], prot_ptr[ma_index], ma_prot, ma); + ClearUnits(); + return slaughter_count; + } +}; + +// vector of races handled by autobutcher +// the name is a bit misleading since entries can be set to 'unwatched' +// to ignore them for a while but still keep the target count settings +static unordered_map watched_races; + +static void init_autobutcher(color_ostream &out) { + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) + config = World::AddPersistentData(CONFIG_KEY); + + if (get_config_val(CONFIG_IS_ENABLED) == -1) { + set_config_bool(CONFIG_IS_ENABLED, false); + set_config_val(CONFIG_CYCLE_TICKS, DEFAULT_CYCLE_TICKS); + set_config_bool(CONFIG_AUTOWATCH, false); + set_config_val(CONFIG_DEFAULT_FK, 5); + set_config_val(CONFIG_DEFAULT_MK, 1); + set_config_val(CONFIG_DEFAULT_FA, 5); + set_config_val(CONFIG_DEFAULT_MA, 1); + } + + if (is_enabled) + set_config_bool(CONFIG_IS_ENABLED, true); + else + is_enabled = (get_config_val(CONFIG_IS_ENABLED) == 1); + + if (!config.isValid()) + return; + + if (!race_to_id.size()) { + const size_t num_races = world->raws.creatures.all.size(); + for(size_t i = 0; i < num_races; ++i) + race_to_id.emplace(Units::getRaceNameById(i), i); + } + + std::vector watchlist; + World::GetPersistentData(&watchlist, WATCHLIST_CONFIG_KEY_PREFIX, true); + for (auto & p : watchlist) { + DEBUG(status,out).print("Reading from save: %s\n", p.key().c_str()); + WatchedRace *w = new WatchedRace(out, p); + watched_races.emplace(w->raceId, w); + } +} + +static void cleanup_autobutcher(color_ostream &out) { + is_enabled = false; + race_to_id.clear(); + for (auto w : watched_races) + delete w.second; + watched_races.clear(); +} + +static bool get_options(color_ostream &out, + autobutcher_options &opts, + const vector ¶meters) +{ + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, parameters.size() + 2) || + !Lua::PushModulePublic( + out, L, "plugins.autobutcher", "parse_commandline")) { + out.printerr("Failed to load autobutcher Lua code\n"); + return false; + } + + Lua::Push(L, &opts); + for (const string ¶m : parameters) + Lua::Push(L, param); + + if (!Lua::SafeCall(out, L, parameters.size() + 1, 0)) + return false; + + return true; +} + +static void autobutcher_export(color_ostream &out); +static void autobutcher_status(color_ostream &out); +static void autobutcher_target(color_ostream &out, const autobutcher_options &opts); +static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_options &opts); + +static command_result df_autobutcher(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!Maps::IsValid()) { + out.printerr("Cannot run autobutcher without a loaded map.\n"); + return CR_FAILURE; + } + + autobutcher_options opts; + if (!get_options(out, opts, parameters) || opts.help) + return CR_WRONG_USAGE; + + if (opts.command == "now") { + autobutcher_cycle(out); + } + else if (opts.command == "autowatch") { + set_config_bool(CONFIG_AUTOWATCH, true); + } + else if (opts.command == "noautowatch") { + set_config_bool(CONFIG_AUTOWATCH, false); + } + else if (opts.command == "list_export") { + autobutcher_export(out); + } + else if (opts.command == "target") { + autobutcher_target(out, opts); + } + else if (opts.command == "watch" || + opts.command == "unwatch" || + opts.command == "forget") { + autobutcher_modify_watchlist(out, opts); + } + else if (opts.command == "ticks") { + set_config_val(CONFIG_CYCLE_TICKS, opts.ticks); + INFO(status,out).print("New cycle timer: %d ticks.\n", opts.ticks); + } + else { + autobutcher_status(out); + } + + return CR_OK; +} + +// helper for sorting the watchlist alphabetically +static bool compareRaceNames(WatchedRace* i, WatchedRace* j) { + string name_i = Units::getRaceNamePluralById(i->raceId); + string name_j = Units::getRaceNamePluralById(j->raceId); + + return name_i < name_j; +} + +// sort watchlist alphabetically +static vector getSortedWatchList() { + vector list; + for (auto w : watched_races) { + list.push_back(w.second); + } + sort(list.begin(), list.end(), compareRaceNames); + return list; +} + +static void autobutcher_export(color_ostream &out) { + out << "enable autobutcher" << endl; + out << "autobutcher ticks " << get_config_val(CONFIG_CYCLE_TICKS) << endl; + out << "autobutcher " << (get_config_bool(CONFIG_AUTOWATCH) ? "" : "no") + << "autowatch" << endl; + out << "autobutcher target" + << " " << get_config_val(CONFIG_DEFAULT_FK) + << " " << get_config_val(CONFIG_DEFAULT_MK) + << " " << get_config_val(CONFIG_DEFAULT_FA) + << " " << get_config_val(CONFIG_DEFAULT_MA) + << " new" << endl; + + for (auto w : getSortedWatchList()) { + df::creature_raw *raw = world->raws.creatures.all[w->raceId]; + string name = raw->creature_id; + out << "autobutcher target" + << " " << w->fk + << " " << w->mk + << " " << w->fa + << " " << w->ma + << " " << name << endl; + if (w->isWatched) + out << "autobutcher watch " << name << endl; + } +} + +static void autobutcher_status(color_ostream &out) { + out << "autobutcher is " << (is_enabled ? "" : "not ") << "enabled\n"; + if (is_enabled) + out << " running every " << get_config_val(CONFIG_CYCLE_TICKS) << " game ticks\n"; + out << " " << (get_config_bool(CONFIG_AUTOWATCH) ? "" : "not ") << "autowatching for new races\n"; + + out << "\ndefault setting for new races:" + << " fk=" << get_config_val(CONFIG_DEFAULT_FK) + << " mk=" << get_config_val(CONFIG_DEFAULT_MK) + << " fa=" << get_config_val(CONFIG_DEFAULT_FA) + << " ma=" << get_config_val(CONFIG_DEFAULT_MA) + << endl << endl; + + if (!watched_races.size()) { + out << "not currently watching any races. to find out how to add some, run:\n help autobutcher" << endl; + return; + } + + out << "monitoring races: " << endl; + for (auto w : getSortedWatchList()) { + df::creature_raw *raw = world->raws.creatures.all[w->raceId]; + out << " " << Units::getRaceNamePluralById(w->raceId) << " \t"; + out << "(" << raw->creature_id; + out << " fk=" << w->fk + << " mk=" << w->mk + << " fa=" << w->fa + << " ma=" << w->ma; + if (!w->isWatched) + out << "; autobutchering is paused"; + out << ")" << endl; + } +} + +static void autobutcher_target(color_ostream &out, const autobutcher_options &opts) { + if (opts.races_new) { + DEBUG(status,out).print("setting targets for new races\n"); + set_config_val(CONFIG_DEFAULT_FK, opts.fk); + set_config_val(CONFIG_DEFAULT_MK, opts.mk); + set_config_val(CONFIG_DEFAULT_FA, opts.fa); + set_config_val(CONFIG_DEFAULT_MA, opts.ma); + } + + if (opts.races_all) { + DEBUG(status,out).print("setting targets for all races on watchlist\n"); + for (auto w : watched_races) { + w.second->fk = opts.fk; + w.second->mk = opts.mk; + w.second->fa = opts.fa; + w.second->ma = opts.ma; + w.second->UpdateConfig(out); + } + } + + for (auto race : opts.races) { + if (!race_to_id.count(*race)) { + out.printerr("race not found: '%s'", race->c_str()); + continue; + } + int id = race_to_id[*race]; + WatchedRace *w; + if (!watched_races.count(id)) { + DEBUG(status,out).print("adding new targets for %s\n", race->c_str()); + w = new WatchedRace(out, id, true, opts.fk, opts.mk, opts.fa, opts.ma); + watched_races.emplace(id, w); + } else { + w = watched_races[id]; + w->fk = opts.fk; + w->mk = opts.mk; + w->fa = opts.fa; + w->ma = opts.ma; + } + w->UpdateConfig(out); + } +} + +static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_options &opts) { + unordered_set ids; + + if (opts.races_all) { + DEBUG(status,out).print("modifying all races on watchlist: %s\n", + opts.command.c_str()); + for (auto w : watched_races) + ids.emplace(w.first); + } + + for (auto race : opts.races) { + if (!race_to_id.count(*race)) { + out.printerr("race not found: '%s'", race->c_str()); + continue; + } + ids.emplace(race_to_id[*race]); + } + + for (int id : ids) { + if (opts.command == "watch") + watched_races[id]->isWatched = true; + else if (opts.command == "unwatch") + watched_races[id]->isWatched = false; + else if (opts.command == "forget") { + watched_races[id]->RemoveConfig(out); + watched_races.erase(id); + continue; + } + watched_races[id]->UpdateConfig(out); + } +} + +///////////////////////////////////////////////////// +// autobutcher cycle logic +// + +// check if contained in item (e.g. animals in cages) +static bool isContainedInItem(df::unit *unit) { + for (auto gref : unit->general_refs) { + if (gref->getType() == df::general_ref_type::CONTAINED_IN_ITEM) { + return true; + } + } + return false; +} + +// found a unit with weird position values on one of my maps (negative and in the thousands) +// it didn't appear in the animal stocks screen, but looked completely fine otherwise (alive, tame, own, etc) +// maybe a rare bug, but better avoid assigning such units to zones or slaughter etc. +static bool hasValidMapPos(df::unit *unit) { + return unit->pos.x >= 0 && unit->pos.y >= 0 && unit->pos.z >= 0 + && unit->pos.x < world->map.x_count + && unit->pos.y < world->map.y_count + && unit->pos.z < world->map.z_count; +} + +// built cage defined as room (supposed to detect zoo cages) +static bool isInBuiltCageRoom(df::unit *unit) { + for (auto building : world->buildings.all) { + // !!! building->isRoom() returns true if the building can be made a room but currently isn't + // !!! except for coffins/tombs which always return false + // !!! using the bool is_room however gives the correct state/value + if (!building->is_room || building->getType() != df::building_type::Cage) + continue; + + df::building_cagest* cage = (df::building_cagest*)building; + for (auto cu : cage->assigned_units) + if (cu == unit->id) return true; + } + return false; +} + +static void autobutcher_cycle(color_ostream &out) { + DEBUG(cycle,out).print("running autobutcher_cycle\n"); + + // check if there is anything to watch before walking through units vector + if (!get_config_bool(CONFIG_AUTOWATCH)) { + bool watching = false; + for (auto w : watched_races) { + if (w.second->isWatched) { + watching = true; + break; + } + } + if (!watching) + return; + } + + for (auto unit : world->units.all) { + // this check is now divided into two steps, squeezed autowatch into the middle + // first one ignores completely inappropriate units (dead, undead, not belonging to the fort, ...) + // then let autowatch add units to the watchlist which will probably start breeding (owned pets, war animals, ...) + // then process units counting those which can't be butchered (war animals, named pets, ...) + // so that they are treated as "own stock" as well and count towards the target quota + if ( !Units::isActive(unit) + || Units::isUndead(unit) + || Units::isMarkedForSlaughter(unit) + || Units::isMerchant(unit) // ignore merchants' draft animals + || Units::isForest(unit) // ignore merchants' caged animals + || !Units::isOwnCiv(unit) + || !Units::isTame(unit) + ) + continue; + + // found a bugged unit which had invalid coordinates but was not in a cage. + // marking it for slaughter didn't seem to have negative effects, but you never know... + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + WatchedRace *w; + if (watched_races.count(unit->race)) { + w = watched_races[unit->race]; + } + else if (!get_config_bool(CONFIG_AUTOWATCH)) { + continue; + } + else { + w = new WatchedRace(out, unit->race, true, get_config_val(CONFIG_DEFAULT_FK), + get_config_val(CONFIG_DEFAULT_MK), get_config_val(CONFIG_DEFAULT_FA), + get_config_val(CONFIG_DEFAULT_MA)); + w->UpdateConfig(out); + watched_races.emplace(unit->race, w); + + string announce = "New race added to autobutcher watchlist: " + Units::getRaceNamePluralById(unit->race); + Gui::showAnnouncement(announce, 2, false); + } + + if (w->isWatched) { + // don't butcher protected units, but count them as stock as well + // this way they count towards target quota, so if you order that you want 1 female adult cat + // and have 2 cats, one of them being a pet, the other gets butchered + if( Units::isWar(unit) // ignore war dogs etc + || Units::isHunter(unit) // ignore hunting dogs etc + // ignore creatures in built cages which are defined as rooms to leave zoos alone + // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || Units::isAvailableForAdoption(unit) + || unit->name.has_name) + w->PushProtectedUnit(unit); + else if ( Units::isGay(unit) + || Units::isGelded(unit)) + w->PushPriorityUnit(unit); + else + w->PushUnit(unit); + } + } + + int slaughter_count = 0; + for (auto w : watched_races) { + int slaughter_subcount = w.second->ProcessUnits(); + slaughter_count += slaughter_subcount; + if (slaughter_subcount) { + stringstream ss; + ss << slaughter_subcount; + string announce = Units::getRaceNamePluralById(w.first) + " marked for slaughter: " + ss.str(); + Gui::showAnnouncement(announce, 2, false); + } + } +} + +///////////////////////////////////// +// API functions to control autobutcher with a lua script + +// abuse WatchedRace struct for counting stocks (since it sorts by gender and age) +// calling method must delete pointer! +static WatchedRace * checkRaceStocksTotal(color_ostream &out, int race) { + WatchedRace * w = new WatchedRace(out, race, true, 0, 0, 0, 0); + for (auto unit : world->units.all) { + if (unit->race != race) + continue; + + if ( !Units::isActive(unit) + || Units::isUndead(unit) + || Units::isMerchant(unit) // ignore merchants' draft animals + || Units::isForest(unit) // ignore merchants' caged animals + || !Units::isOwnCiv(unit) + ) + continue; + + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + w->PushUnit(unit); + } + return w; +} + +WatchedRace * checkRaceStocksProtected(color_ostream &out, int race) { + WatchedRace * w = new WatchedRace(out, race, true, 0, 0, 0, 0); + for (auto unit : world->units.all) { + if (unit->race != race) + continue; + + if ( !Units::isActive(unit) + || Units::isUndead(unit) + || Units::isMerchant(unit) // ignore merchants' draft animals + || Units::isForest(unit) // ignore merchants' caged animals + || !Units::isOwnCiv(unit) + ) + continue; + + // found a bugged unit which had invalid coordinates but was not in a cage. + // marking it for slaughter didn't seem to have negative effects, but you never know... + if (!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + if ( !Units::isTame(unit) + || Units::isWar(unit) // ignore war dogs etc + || Units::isHunter(unit) // ignore hunting dogs etc + // ignore creatures in built cages which are defined as rooms to leave zoos alone + // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || Units::isAvailableForAdoption(unit) + || unit->name.has_name ) + w->PushUnit(unit); + } + return w; +} + +WatchedRace * checkRaceStocksButcherable(color_ostream &out, int race) { + WatchedRace * w = new WatchedRace(out, race, true, 0, 0, 0, 0); + for (auto unit : world->units.all) { + if (unit->race != race) + continue; + + if ( !Units::isActive(unit) + || Units::isUndead(unit) + || Units::isMerchant(unit) // ignore merchants' draft animals + || Units::isForest(unit) // ignore merchants' caged animals + || !Units::isOwnCiv(unit) + || !Units::isTame(unit) + || Units::isWar(unit) // ignore war dogs etc + || Units::isHunter(unit) // ignore hunting dogs etc + // ignore creatures in built cages which are defined as rooms to leave zoos alone + // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || Units::isAvailableForAdoption(unit) + || unit->name.has_name + ) + continue; + + if (!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + w->PushUnit(unit); + } + return w; +} + +WatchedRace * checkRaceStocksButcherFlag(color_ostream &out, int race) { + WatchedRace * w = new WatchedRace(out, race, true, 0, 0, 0, 0); + for (auto unit : world->units.all) { + if(unit->race != race) + continue; + + if ( !Units::isActive(unit) + || Units::isUndead(unit) + || Units::isMerchant(unit) // ignore merchants' draft animals + || Units::isForest(unit) // ignore merchants' caged animals + || !Units::isOwnCiv(unit) + ) + continue; + + if (!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + if (Units::isMarkedForSlaughter(unit)) + w->PushUnit(unit); + } + return w; +} + +static bool autowatch_isEnabled() { + return get_config_bool(CONFIG_AUTOWATCH); +} + +static unsigned autobutcher_getSleep(color_ostream &out) { + return get_config_val(CONFIG_CYCLE_TICKS); +} + +static void autobutcher_setSleep(color_ostream &out, unsigned ticks) { + + set_config_val(CONFIG_CYCLE_TICKS, ticks); +} + +static void autowatch_setEnabled(color_ostream &out, bool enable) { + DEBUG(status,out).print("auto-adding to watchlist %s\n", enable ? "started" : "stopped"); + set_config_bool(CONFIG_AUTOWATCH, enable); +} + +// set all data for a watchlist race in one go +// if race is not already on watchlist it will be added +// params: (id, fk, mk, fa, ma, watched) +static void autobutcher_setWatchListRace(color_ostream &out, unsigned id, unsigned fk, unsigned mk, unsigned fa, unsigned ma, bool watched) { + if (watched_races.count(id)) { + DEBUG(status,out).print("updating watchlist entry\n"); + WatchedRace * w = watched_races[id]; + w->fk = fk; + w->mk = mk; + w->fa = fa; + w->ma = ma; + w->isWatched = watched; + w->UpdateConfig(out); + return; + } + + DEBUG(status,out).print("creating new watchlist entry\n"); + WatchedRace * w = new WatchedRace(out, id, watched, fk, mk, fa, ma); + w->UpdateConfig(out); + watched_races.emplace(id, w); + + string announce; + announce = "New race added to autobutcher watchlist: " + Units::getRaceNamePluralById(id); + Gui::showAnnouncement(announce, 2, false); +} + +// remove entry from watchlist +static void autobutcher_removeFromWatchList(color_ostream &out, unsigned id) { + if (watched_races.count(id)) { + DEBUG(status,out).print("removing watchlist entry\n"); + WatchedRace * w = watched_races[id]; + w->RemoveConfig(out); + watched_races.erase(id); + } +} + +// set default target values for new races +static void autobutcher_setDefaultTargetNew(color_ostream &out, unsigned fk, unsigned mk, unsigned fa, unsigned ma) { + set_config_val(CONFIG_DEFAULT_FK, fk); + set_config_val(CONFIG_DEFAULT_MK, mk); + set_config_val(CONFIG_DEFAULT_FA, fa); + set_config_val(CONFIG_DEFAULT_MA, ma); +} + +// set default target values for ALL races (update watchlist and set new default) +static void autobutcher_setDefaultTargetAll(color_ostream &out, unsigned fk, unsigned mk, unsigned fa, unsigned ma) { + for (auto w : watched_races) { + w.second->fk = fk; + w.second->mk = mk; + w.second->fa = fa; + w.second->ma = ma; + w.second->UpdateConfig(out); + } + autobutcher_setDefaultTargetNew(out, fk, mk, fa, ma); +} + +static void autobutcher_butcherRace(color_ostream &out, int id) { + for (auto unit : world->units.all) { + if(unit->race != id) + continue; + + if( !Units::isActive(unit) + || Units::isUndead(unit) + || Units::isMerchant(unit) // ignore merchants' draught animals + || Units::isForest(unit) // ignore merchants' caged animals + || !Units::isOwnCiv(unit) + || !Units::isTame(unit) + || Units::isWar(unit) // ignore war dogs etc + || Units::isHunter(unit) // ignore hunting dogs etc + // ignore creatures in built cages which are defined as rooms to leave zoos alone + // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || Units::isAvailableForAdoption(unit) + || unit->name.has_name + ) + continue; + + // found a bugged unit which had invalid coordinates but was not in a cage. + // marking it for slaughter didn't seem to have negative effects, but you never know... + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + doMarkForSlaughter(unit); + } +} + +// remove butcher flag for all units of a given race +static void autobutcher_unbutcherRace(color_ostream &out, int id) { + for (auto unit : world->units.all) { + if(unit->race != id) + continue; + + if( !Units::isActive(unit) + || Units::isUndead(unit) + || !Units::isMarkedForSlaughter(unit) + ) + continue; + + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + unit->flags2.bits.slaughter = 0; + } +} + +// push autobutcher settings on lua stack +static int autobutcher_getSettings(lua_State *L) { + lua_newtable(L); + int ctable = lua_gettop(L); + Lua::SetField(L, get_config_bool(CONFIG_IS_ENABLED), ctable, "enable_autobutcher"); + Lua::SetField(L, get_config_bool(CONFIG_AUTOWATCH), ctable, "enable_autowatch"); + Lua::SetField(L, get_config_val(CONFIG_DEFAULT_FK), ctable, "fk"); + Lua::SetField(L, get_config_val(CONFIG_DEFAULT_MK), ctable, "mk"); + Lua::SetField(L, get_config_val(CONFIG_DEFAULT_FA), ctable, "fa"); + Lua::SetField(L, get_config_val(CONFIG_DEFAULT_MA), ctable, "ma"); + Lua::SetField(L, get_config_val(CONFIG_CYCLE_TICKS), ctable, "sleep"); + return 1; +} + +// push the watchlist vector as nested table on the lua stack +static int autobutcher_getWatchList(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + + lua_newtable(L); + int entry_index = 0; + for (auto wr : watched_races) { + lua_newtable(L); + int ctable = lua_gettop(L); + + WatchedRace * w = wr.second; + int id = w->raceId; + Lua::SetField(L, id, ctable, "id"); + Lua::SetField(L, w->isWatched, ctable, "watched"); + Lua::SetField(L, Units::getRaceNamePluralById(id), ctable, "name"); + Lua::SetField(L, w->fk, ctable, "fk"); + Lua::SetField(L, w->mk, ctable, "mk"); + Lua::SetField(L, w->fa, ctable, "fa"); + Lua::SetField(L, w->ma, ctable, "ma"); + + WatchedRace *tally = checkRaceStocksTotal(*out, id); + Lua::SetField(L, tally->unit_ptr[fk_index].size(), ctable, "fk_total"); + Lua::SetField(L, tally->unit_ptr[mk_index].size(), ctable, "mk_total"); + Lua::SetField(L, tally->unit_ptr[fa_index].size(), ctable, "fa_total"); + Lua::SetField(L, tally->unit_ptr[ma_index].size(), ctable, "ma_total"); + delete tally; + + tally = checkRaceStocksProtected(*out, id); + Lua::SetField(L, tally->unit_ptr[fk_index].size(), ctable, "fk_protected"); + Lua::SetField(L, tally->unit_ptr[mk_index].size(), ctable, "mk_protected"); + Lua::SetField(L, tally->unit_ptr[fa_index].size(), ctable, "fa_protected"); + Lua::SetField(L, tally->unit_ptr[ma_index].size(), ctable, "ma_protected"); + delete tally; + + tally = checkRaceStocksButcherable(*out, id); + Lua::SetField(L, tally->unit_ptr[fk_index].size(), ctable, "fk_butcherable"); + Lua::SetField(L, tally->unit_ptr[mk_index].size(), ctable, "mk_butcherable"); + Lua::SetField(L, tally->unit_ptr[fa_index].size(), ctable, "fa_butcherable"); + Lua::SetField(L, tally->unit_ptr[ma_index].size(), ctable, "ma_butcherable"); + delete tally; + + tally = checkRaceStocksButcherFlag(*out, id); + Lua::SetField(L, tally->unit_ptr[fk_index].size(), ctable, "fk_butcherflag"); + Lua::SetField(L, tally->unit_ptr[mk_index].size(), ctable, "mk_butcherflag"); + Lua::SetField(L, tally->unit_ptr[fa_index].size(), ctable, "fa_butcherflag"); + Lua::SetField(L, tally->unit_ptr[ma_index].size(), ctable, "ma_butcherflag"); + delete tally; + + lua_rawseti(L, -2, ++entry_index); + } + + return 1; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(autowatch_isEnabled), + DFHACK_LUA_FUNCTION(autowatch_setEnabled), + DFHACK_LUA_FUNCTION(autobutcher_getSleep), + DFHACK_LUA_FUNCTION(autobutcher_setSleep), + DFHACK_LUA_FUNCTION(autobutcher_setWatchListRace), + DFHACK_LUA_FUNCTION(autobutcher_setDefaultTargetNew), + DFHACK_LUA_FUNCTION(autobutcher_setDefaultTargetAll), + DFHACK_LUA_FUNCTION(autobutcher_butcherRace), + DFHACK_LUA_FUNCTION(autobutcher_unbutcherRace), + DFHACK_LUA_FUNCTION(autobutcher_removeFromWatchList), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(autobutcher_getSettings), + DFHACK_LUA_COMMAND(autobutcher_getWatchList), + DFHACK_LUA_END +}; diff --git a/plugins/lua/autobutcher.lua b/plugins/lua/autobutcher.lua new file mode 100644 index 000000000..ab334896c --- /dev/null +++ b/plugins/lua/autobutcher.lua @@ -0,0 +1,89 @@ +local _ENV = mkmodule('plugins.autobutcher') + +--[[ + + Native functions: + + * autobutcher_isEnabled() + * autowatch_isEnabled() + +--]] + +local argparse = require('argparse') + +local function is_int(val) + return val and val == math.floor(val) +end + +local function is_positive_int(val) + return is_int(val) and val > 0 +end + +local function check_nonnegative_int(str) + local val = tonumber(str) + if is_positive_int(val) or val == 0 then return val end + qerror('expecting a non-negative integer, but got: '..tostring(str)) +end + +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 process_races(opts, races, start_idx) + if #races < start_idx then + qerror('missing list of races (or "all" or "new" keywords)') + end + for i=start_idx,#races do + local race = races[i] + if race == 'all' then + opts.races_all = true + elseif race == 'new' then + opts.races_new = true + else + local str = df.new('string') + str.value = race + opts.races:insert('#', str) + end + end +end + +function parse_commandline(opts, ...) + local positionals = process_args(opts, {...}) + + local command = positionals[1] + if command then opts.command = command end + + if opts.help or not command or command == 'now' or + command == 'autowatch' or command == 'noautowatch' or + command == 'list' or command == 'list_export' then + return + end + + if command == 'watch' or command == 'unwatch' or command == 'forget' then + process_races(opts, positionals, 2) + elseif command == 'target' then + opts.fk = check_nonnegative_int(positionals[2]) + opts.mk = check_nonnegative_int(positionals[3]) + opts.fa = check_nonnegative_int(positionals[4]) + opts.ma = check_nonnegative_int(positionals[5]) + process_races(opts, positionals, 6) + elseif command == 'ticks' then + local ticks = tonumber(positionals[2]) + if not is_positive_int(arg) then + qerror('number of ticks must be a positive integer: ' .. ticks) + else + opts.ticks = ticks + end + else + qerror(('unrecognized command: "%s"'):format(command)) + end +end + +return _ENV diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua deleted file mode 100644 index 75c9feec8..000000000 --- a/plugins/lua/zone.lua +++ /dev/null @@ -1,12 +0,0 @@ -local _ENV = mkmodule('plugins.zone') - ---[[ - - Native functions: - - * autobutcher_isEnabled() - * autowatch_isEnabled() - ---]] - -return _ENV diff --git a/plugins/zone.cpp b/plugins/zone.cpp index 6353bfd35..60f1f2b61 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -14,107 +14,59 @@ // - mass-assign creatures using filters // - unassign single creature under cursor from current zone // - pitting own dwarves :) -// - full automation of handling mini-pastures over nestboxes: -// go through all pens, check if they are empty and placed over a nestbox -// find female tame egg-layer who is not assigned to another pen and assign it to nestbox pasture -// maybe check for minimum age? it's not that useful to fill nestboxes with freshly hatched birds -// state and sleep setting is saved the first time autonestbox is started (to avoid writing stuff if the plugin is never used) -// - full automation of marking live-stock for slaughtering -// races can be added to a watchlist and it can be set how many male/female kids/adults are left alive -// adding to the watchlist can be automated as well. -// config for autobutcher (state and sleep setting) is saved the first time autobutcher is started -// config for watchlist entries is saved when they are created or modified - -#include -#include -#include -#include -#include -#include -#include + #include -#include -#include #include -#include -#include -#include - -#include "Core.h" -#include "Console.h" -#include "Export.h" -#include "PluginManager.h" -#include "MiscUtils.h" -#include "uicommon.h" - -#include "LuaTools.h" -#include "DataFuncs.h" - -#include "modules/Units.h" -#include "modules/Maps.h" -#include "modules/Gui.h" -#include "modules/Materials.h" -#include "modules/MapCache.h" -#include "modules/Buildings.h" -#include "modules/World.h" -#include "modules/Screen.h" -#include "MiscUtils.h" -#include +#include +#include -#include "df/ui.h" -#include "df/world.h" -#include "df/world_raws.h" -#include "df/building_def.h" -#include "df/building_civzonest.h" #include "df/building_cagest.h" #include "df/building_chainst.h" -#include "df/building_nest_boxst.h" +#include "df/building_civzonest.h" #include "df/general_ref_building_civzone_assignedst.h" -#include -#include +#include "df/ui.h" +#include "df/unit.h" #include "df/unit_relationship_type.h" -#include "df/unit_soul.h" -#include "df/unit_wound.h" #include "df/viewscreen_dwarfmodest.h" +#include "df/world.h" + +#include "PluginManager.h" +#include "uicommon.h" +#include "VTableInterpose.h" + +#include "modules/Buildings.h" +#include "modules/Gui.h" +#include "modules/Units.h" #include "modules/Translation.h" +using std::function; using std::make_pair; +using std::ostringstream; +using std::pair; +using std::runtime_error; using std::string; using std::unordered_map; using std::unordered_set; using std::vector; using namespace DFHack; -using namespace DFHack::Units; -using namespace DFHack::Buildings; -using namespace df::enums; DFHACK_PLUGIN("zone"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(cursor); -REQUIRE_GLOBAL(ui); -REQUIRE_GLOBAL(ui_build_selector); REQUIRE_GLOBAL(gps); -REQUIRE_GLOBAL(cur_year); -REQUIRE_GLOBAL(cur_year_tick); - +REQUIRE_GLOBAL(ui); REQUIRE_GLOBAL(ui_building_item_cursor); REQUIRE_GLOBAL(ui_building_assign_type); REQUIRE_GLOBAL(ui_building_assign_is_marked); REQUIRE_GLOBAL(ui_building_assign_units); REQUIRE_GLOBAL(ui_building_assign_items); REQUIRE_GLOBAL(ui_building_in_assign); - REQUIRE_GLOBAL(ui_menu_width); +REQUIRE_GLOBAL(world); -using namespace DFHack::Gui; - -command_result df_zone (color_ostream &out, vector & parameters); -command_result df_autobutcher(color_ostream &out, vector & parameters); - -DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable); +static command_result df_zone (color_ostream &out, vector & parameters); const string zone_help = "Allows easier management of pens/pastures, pits and cages.\n" @@ -200,134 +152,20 @@ const string zone_help_examples = " well, unless you have a mod with egg-laying male elves who give milk...\n"; -const string autobutcher_help = - "Assigns your lifestock for slaughter once it reaches a specific count. Requires\n" - "that you add the target race(s) to a watch list. Only tame units will be\n" - "processed. Named units will be completely ignored (you can give animals\n" - "nicknames with the tool 'rename unit' to protect them from getting slaughtered\n" - "automatically. Trained war or hunting pets will be ignored.\n" - "Once you have too much adults, the oldest will be butchered first.\n" - "Once you have too much kids, the youngest will be butchered first.\n" - "If you don't set a target count the following default will be used:\n" - "1 male kid, 5 female kids, 1 male adult, 5 female adults.\n" - "Options:\n" - " start - run every X frames (df simulation ticks)\n" - " default: X=6000 (~60 seconds at 100fps)\n" - " stop - stop running automatically\n" - " sleep X - change timer to sleep X frames between runs.\n" - " watch R - start watching race(s)\n" - " R = valid race RAW id (ALPACA, BIRD_TURKEY, etc)\n" - " or a list of RAW ids seperated by spaces\n" - " or the keyword 'all' which affects your whole current watchlist.\n" - " unwatch R - stop watching race(s)\n" - " the current target settings will be remembered\n" - " forget R - unwatch race(s) and forget target settings for it/them\n" - " autowatch - automatically adds all new races (animals you buy\n" - " from merchants, tame yourself or get from migrants)\n" - " to the watch list using default target count\n" - " noautowatch - stop auto-adding new races to the watch list\n" - " list - print status and watchlist\n" - " list_export - print status and watchlist in batchfile format\n" - " can be used to copy settings into another savegame\n" - " usage: 'dfhack-run autobutcher list_export > xyz.bat' \n" - " target fk mk fa ma R\n" - " - set target count for specified race:\n" - " fk = number of female kids\n" - " mk = number of male kids\n" - " fa = number of female adults\n" - " ma = number of female adults\n" - " R = 'all' sets count for all races on the current watchlist\n" - " including the races which are currenly set to 'unwatched'\n" - " and sets the new default for future watch commands\n" - " R = 'new' sets the new default for future watch commands\n" - " without changing your current watchlist\n" - " example - print some usage examples\n"; - -const string autobutcher_help_example = - "Examples:\n" - " autobutcher target 4 3 2 1 ALPACA BIRD_TURKEY\n" - " autobutcher watch ALPACA BIRD_TURKEY\n" - " autobutcher start\n" - " This means you want to have max 7 kids (4 female, 3 male) and max 3 adults\n" - " (2 female, 1 male) of the races alpaca and turkey. Once the kids grow up the\n" - " oldest adults will get slaughtered. Excess kids will get slaughtered starting\n" - " the the youngest to allow that the older ones grow into adults.\n" - " autobutcher target 0 0 0 0 new\n" - " autobutcher autowatch\n" - " autobutcher start\n" - " This tells autobutcher to automatically put all new races onto the watchlist\n" - " and mark unnamed tame units for slaughter as soon as they arrive in your\n" - " fortress. Settings already made for some races will be left untouched.\n"; - -command_result init_autobutcher(color_ostream &out); -command_result cleanup_autobutcher(color_ostream &out); -command_result start_autobutcher(color_ostream &out); - - -/////////////// -// stuff for autobutcher -// should be moved to own plugin once the tool methods it shares with the zone plugin are moved to Unit.h / Building.h - -command_result autoNestbox( color_ostream &out, bool verbose ); -command_result autoButcher( color_ostream &out, bool verbose ); - -static bool enable_autobutcher = false; -static bool enable_autobutcher_autowatch = false; -static size_t sleep_autobutcher = 6000; - -static PersistentDataItem config_autobutcher; - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) - { - case DFHack::SC_MAP_LOADED: - // initialize from the world just loaded - init_autobutcher(out); - break; - case DFHack::SC_MAP_UNLOADED: - enable_autobutcher = false; - // cleanup - cleanup_autobutcher(out); - break; - default: - break; - } - return CR_OK; -} - -DFhackCExport command_result plugin_onupdate ( color_ostream &out ) -{ - static size_t ticks_autobutcher = 0; - - if(enable_autobutcher) - { - if(++ticks_autobutcher >= sleep_autobutcher) - { - ticks_autobutcher= 0; - autoButcher(out, false); - } - } - - return CR_OK; -} - - /////////////// // Various small tool functions // probably many of these should be moved to Unit.h and Building.h sometime later... -df::general_ref_building_civzone_assignedst * createCivzoneRef(); -bool unassignUnitFromBuilding(df::unit* unit); -command_result assignUnitToZone(color_ostream& out, df::unit* unit, df::building* building, bool verbose); -void unitInfo(color_ostream & out, df::unit* creature, bool verbose); -void zoneInfo(color_ostream & out, df::building* building, bool verbose); -void cageInfo(color_ostream & out, df::building* building, bool verbose); -void chainInfo(color_ostream & out, df::building* building, bool verbose); -bool isBuiltCageAtPos(df::coord pos); -bool isInBuiltCageRoom(df::unit*); - -void doMarkForSlaughter(df::unit* unit) +// static df::general_ref_building_civzone_assignedst * createCivzoneRef(); +// static bool unassignUnitFromBuilding(df::unit* unit); +// static command_result assignUnitToZone(color_ostream& out, df::unit* unit, df::building* building, bool verbose); +// static void unitInfo(color_ostream & out, df::unit* creature, bool verbose); +// static void zoneInfo(color_ostream & out, df::building* building, bool verbose); +// static void cageInfo(color_ostream & out, df::building* building, bool verbose); +// static void chainInfo(color_ostream & out, df::building* building, bool verbose); +static bool isInBuiltCageRoom(df::unit*); + +static void doMarkForSlaughter(df::unit* unit) { unit->flags2.bits.slaughter = 1; } @@ -335,7 +173,7 @@ void doMarkForSlaughter(df::unit* unit) // found a unit with weird position values on one of my maps (negative and in the thousands) // it didn't appear in the animal stocks screen, but looked completely fine otherwise (alive, tame, own, etc) // maybe a rare bug, but better avoid assigning such units to zones or slaughter etc. -bool hasValidMapPos(df::unit* unit) +static bool hasValidMapPos(df::unit* unit) { if( unit->pos.x >=0 && unit->pos.y >= 0 && unit->pos.z >= 0 && unit->pos.x < world->map.x_count @@ -347,7 +185,7 @@ bool hasValidMapPos(df::unit* unit) } // dump some unit info -void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false) +static void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false) { out.print("Unit %d ", unit->id); //race %d, civ %d,", creature->race, creature->civ_id if(unit->name.has_name) @@ -364,11 +202,11 @@ void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false) out << ", "; } - if(isAdult(unit)) + if(Units::isAdult(unit)) out << "adult"; - else if(isBaby(unit)) + else if(Units::isBaby(unit)) out << "baby"; - else if(isChild(unit)) + else if(Units::isChild(unit)) out << "child"; out << " "; // sometimes empty even in vanilla RAWS, sometimes contains full race name (e.g. baby alpaca) @@ -376,7 +214,7 @@ void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false) //out << getRaceBabyName(unit); //out << getRaceChildName(unit); - out << getRaceName(unit) << " ("; + out << Units::getRaceName(unit) << " ("; switch(unit->sex) { case 0: @@ -391,26 +229,26 @@ void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false) break; } out << ")"; - out << ", age: " << getAge(unit, true); + out << ", age: " << Units::getAge(unit, true); - if(isTame(unit)) + if(Units::isTame(unit)) out << ", tame"; - if(isOwnCiv(unit)) + if(Units::isOwnCiv(unit)) out << ", owned"; - if(isWar(unit)) + if(Units::isWar(unit)) out << ", war"; - if(isHunter(unit)) + if(Units::isHunter(unit)) out << ", hunter"; - if(isMerchant(unit)) + if(Units::isMerchant(unit)) out << ", merchant"; - if(isForest(unit)) + if(Units::isForest(unit)) out << ", forest"; - if(isEggLayer(unit)) + if(Units::isEggLayer(unit)) out << ", egglayer"; - if(isGrazer(unit)) + if(Units::isGrazer(unit)) out << ", grazer"; - if(isMilkable(unit)) + if(Units::isMilkable(unit)) out << ", milkable"; if(unit->flags2.bits.slaughter) out << ", slaughter"; @@ -418,7 +256,7 @@ void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false) if(verbose) { out << ". Pos: ("<pos.x << "/"<< unit->pos.y << "/" << unit->pos.z << ") " << endl; - out << "index in units vector: " << findIndexById(unit->id) << endl; + out << "index in units vector: " << Units::findIndexById(unit->id) << endl; } out << endl; @@ -461,17 +299,17 @@ void unitInfo(color_ostream & out, df::unit* unit, bool verbose = false) } } -bool isCage(df::building * building) +static bool isCage(df::building * building) { - return building && (building->getType() == building_type::Cage); + return building && (building->getType() == df::building_type::Cage); } -bool isChain(df::building * building) +static bool isChain(df::building * building) { - return building && (building->getType() == building_type::Chain); + return building && (building->getType() == df::building_type::Chain); } -df::general_ref_building_civzone_assignedst * createCivzoneRef() +static df::general_ref_building_civzone_assignedst * createCivzoneRef() { static bool vt_initialized = false; df::general_ref_building_civzone_assignedst* newref = NULL; @@ -510,14 +348,14 @@ df::general_ref_building_civzone_assignedst * createCivzoneRef() return newref; } -bool isInBuiltCage(df::unit* unit); +static bool isInBuiltCage(df::unit* unit); // check if assigned to pen, pit, (built) cage or chain // note: BUILDING_CAGED is not set for animals (maybe it's used for dwarves who get caged as sentence) // animals in cages (no matter if built or on stockpile) get the ref CONTAINED_IN_ITEM instead // removing them from cages on stockpiles is no problem even without clearing the ref // and usually it will be desired behavior to do so. -bool isAssigned(df::unit* unit) +static bool isAssigned(df::unit* unit) { bool assigned = false; for (size_t r=0; r < unit->general_refs.size(); r++) @@ -537,7 +375,7 @@ bool isAssigned(df::unit* unit) return assigned; } -bool isAssignedToZone(df::unit* unit) +static bool isAssignedToZone(df::unit* unit) { bool assigned = false; for (size_t r=0; r < unit->general_refs.size(); r++) @@ -553,26 +391,8 @@ bool isAssignedToZone(df::unit* unit) return assigned; } -// check if assigned to a chain or built cage -// (need to check if the ref needs to be removed, until then touching them is forbidden) -bool isChained(df::unit* unit) -{ - bool contained = false; - for (size_t r=0; r < unit->general_refs.size(); r++) - { - df::general_ref * ref = unit->general_refs[r]; - auto rtype = ref->getType(); - if(rtype == df::general_ref_type::BUILDING_CHAIN) - { - contained = true; - break; - } - } - return contained; -} - // check if contained in item (e.g. animals in cages) -bool isContainedInItem(df::unit* unit) +static bool isContainedInItem(df::unit* unit) { bool contained = false; for (size_t r=0; r < unit->general_refs.size(); r++) @@ -588,13 +408,13 @@ bool isContainedInItem(df::unit* unit) return contained; } -bool isInBuiltCage(df::unit* unit) +static bool isInBuiltCage(df::unit* unit) { bool caged = false; for (size_t b=0; b < world->buildings.all.size(); b++) { df::building* building = world->buildings.all[b]; - if( building->getType() == building_type::Cage) + if( building->getType() == df::building_type::Cage) { df::building_cagest* cage = (df::building_cagest*) building; for(size_t c=0; cassigned_units.size(); c++) @@ -613,7 +433,7 @@ bool isInBuiltCage(df::unit* unit) } // built cage defined as room (supposed to detect zoo cages) -bool isInBuiltCageRoom(df::unit* unit) +static bool isInBuiltCageRoom(df::unit* unit) { bool caged_room = false; for (size_t b=0; b < world->buildings.all.size(); b++) @@ -626,7 +446,7 @@ bool isInBuiltCageRoom(df::unit* unit) if(!building->is_room) continue; - if(building->getType() == building_type::Cage) + if(building->getType() == df::building_type::Cage) { df::building_cagest* cage = (df::building_cagest*) building; for(size_t c=0; cassigned_units.size(); c++) @@ -644,35 +464,13 @@ bool isInBuiltCageRoom(df::unit* unit) return caged_room; } -// check a map position for a built cage -// animals in cages are CONTAINED_IN_ITEM, no matter if they are on a stockpile or inside a built cage -// if they are on animal stockpiles they should count as unassigned to allow pasturing them -// if they are inside built cages they should be ignored in case the cage is a zoo or linked to a lever or whatever -bool isBuiltCageAtPos(df::coord pos) -{ - bool cage = false; - for (size_t b=0; b < world->buildings.all.size(); b++) - { - df::building* building = world->buildings.all[b]; - if( building->getType() == building_type::Cage - && building->x1 == pos.x - && building->y1 == pos.y - && building->z == pos.z ) - { - cage = true; - break; - } - } - return cage; -} - -df::building * getBuiltCageAtPos(df::coord pos) +static df::building * getBuiltCageAtPos(df::coord pos) { df::building* cage = NULL; for (size_t b=0; b < world->buildings.all.size(); b++) { df::building* building = world->buildings.all[b]; - if( building->getType() == building_type::Cage + if( building->getType() == df::building_type::Cage && building->x1 == pos.x && building->y1 == pos.y && building->z == pos.z ) @@ -688,120 +486,12 @@ df::building * getBuiltCageAtPos(df::coord pos) return cage; } -bool isNestboxAtPos(int32_t x, int32_t y, int32_t z) -{ - bool found = false; - for (size_t b=0; b < world->buildings.all.size(); b++) - { - df::building* building = world->buildings.all[b]; - if( building->getType() == building_type::NestBox - && building->x1 == x - && building->y1 == y - && building->z == z ) - { - found = true; - break; - } - } - return found; -} - -bool isFreeNestboxAtPos(int32_t x, int32_t y, int32_t z) -{ - bool found = false; - for (size_t b=0; b < world->buildings.all.size(); b++) - { - df::building* building = world->buildings.all[b]; - if( building->getType() == building_type::NestBox - && building->x1 == x - && building->y1 == y - && building->z == z ) - { - df::building_nest_boxst* nestbox = (df::building_nest_boxst*) building; - if(nestbox->claimed_by == -1 && nestbox->contained_items.size() == 1) - { - found = true; - break; - } - } - } - return found; -} - -bool isEmptyPasture(df::building* building) -{ - if(!isPenPasture(building)) - return false; - df::building_civzonest * civ = (df::building_civzonest *) building; - if(civ->assigned_units.size() == 0) - return true; - else - return false; -} - -df::building* findFreeNestboxZone() -{ - df::building * free_building = NULL; - for (size_t b=0; b < world->buildings.all.size(); b++) - { - df::building* building = world->buildings.all[b]; - if( isEmptyPasture(building) && - isActive(building) && - isFreeNestboxAtPos(building->x1, building->y1, building->z)) - { - free_building = building; - break; - } - } - return free_building; -} - -bool isFreeEgglayer(df::unit * unit) -{ - return isActive(unit) && !isUndead(unit) - && isFemale(unit) - && isTame(unit) - && isOwnCiv(unit) - && isEggLayer(unit) - && !isAssigned(unit) - && !isGrazer(unit) // exclude grazing birds because they're messy - && !isMerchant(unit) // don't steal merchant mounts - && !isForest(unit); // don't steal birds from traders, they hate that -} - -df::unit * findFreeEgglayer() -{ - df::unit* free_unit = NULL; - for (size_t i=0; i < world->units.all.size(); i++) - { - df::unit* unit = world->units.all[i]; - if(isFreeEgglayer(unit)) - { - free_unit = unit; - break; - } - } - return free_unit; -} - -size_t countFreeEgglayers() -{ - size_t count = 0; - for (size_t i=0; i < world->units.all.size(); i++) - { - df::unit* unit = world->units.all[i]; - if(isFreeEgglayer(unit)) - count ++; - } - return count; -} - // check if unit is already assigned to a zone, remove that ref from unit and old zone // check if unit is already assigned to a cage, remove that ref from the cage // returns false if no cage or pasture information was found // helps as workaround for http://www.bay12games.com/dwarves/mantisbt/view.php?id=4475 by the way // (pastured animals assigned to chains will get hauled back and forth because the pasture ref is not deleted) -bool unassignUnitFromBuilding(df::unit* unit) +static bool unassignUnitFromBuilding(df::unit* unit) { bool success = false; for (std::size_t idx = 0; idx < unit->general_refs.size(); idx++) @@ -885,10 +575,10 @@ bool unassignUnitFromBuilding(df::unit* unit) } // assign to pen or pit -command_result assignUnitToZone(color_ostream& out, df::unit* unit, df::building* building, bool verbose = false) +static command_result assignUnitToZone(color_ostream& out, df::unit* unit, df::building* building, bool verbose = false) { // building must be a pen/pasture or pit - if(!isPenPasture(building) && !isPitPond(building)) + if(!Buildings::isPenPasture(building) && !Buildings::isPitPond(building)) { out << "Invalid building type. This is not a pen/pasture or pit/pond." << endl; return CR_WRONG_USAGE; @@ -926,18 +616,18 @@ command_result assignUnitToZone(color_ostream& out, df::unit* unit, df::building civz->assigned_units.push_back(unit->id); out << "Unit " << unit->id - << "(" << getRaceName(unit) << ")" + << "(" << Units::getRaceName(unit) << ")" << " assigned to zone " << building->id; - if(isPitPond(building)) + if(Buildings::isPitPond(building)) out << " (pit/pond)."; - if(isPenPasture(building)) + if(Buildings::isPenPasture(building)) out << " (pen/pasture)."; out << endl; return CR_OK; } -command_result assignUnitToCage(color_ostream& out, df::unit* unit, df::building* building, bool verbose) +static command_result assignUnitToCage(color_ostream& out, df::unit* unit, df::building* building, bool verbose) { // building must be a pen/pasture or pit if(!isCage(building)) @@ -967,24 +657,24 @@ command_result assignUnitToCage(color_ostream& out, df::unit* unit, df::building civz->assigned_units.push_back(unit->id); out << "Unit " << unit->id - << "(" << getRaceName(unit) << ")" + << "(" << Units::getRaceName(unit) << ")" << " assigned to cage " << building->id; out << endl; return CR_OK; } -command_result assignUnitToChain(color_ostream& out, df::unit* unit, df::building* building, bool verbose) +static command_result assignUnitToChain(color_ostream& out, df::unit* unit, df::building* building, bool verbose) { out << "sorry. assigning to chains is not possible yet." << endl; return CR_WRONG_USAGE; } -command_result assignUnitToBuilding(color_ostream& out, df::unit* unit, df::building* building, bool verbose) +static command_result assignUnitToBuilding(color_ostream& out, df::unit* unit, df::building* building, bool verbose) { command_result result = CR_WRONG_USAGE; - if(isActivityZone(building)) + if(Buildings::isActivityZone(building)) result = assignUnitToZone(out, unit, building, verbose); else if(isCage(building)) result = assignUnitToCage(out, unit, building, verbose); @@ -996,9 +686,9 @@ command_result assignUnitToBuilding(color_ostream& out, df::unit* unit, df::buil return result; } -command_result assignUnitsToCagezone(color_ostream& out, vector units, df::building* building, bool verbose) +static command_result assignUnitsToCagezone(color_ostream& out, vector units, df::building* building, bool verbose) { - if(!isPenPasture(building)) + if(!Buildings::isPenPasture(building)) { out << "A cage zone needs to be a pen/pasture containing at least one cage!" << endl; return CR_WRONG_USAGE; @@ -1055,10 +745,10 @@ command_result assignUnitsToCagezone(color_ostream& out, vector units return CR_OK; } -command_result nickUnitsInZone(color_ostream& out, df::building* building, string nick) +static command_result nickUnitsInZone(color_ostream& out, df::building* building, string nick) { // building must be a pen/pasture or pit - if(!isPenPasture(building) && !isPitPond(building)) + if(!Buildings::isPenPasture(building) && !Buildings::isPitPond(building)) { out << "Invalid building type. This is not a pen/pasture or pit/pond." << endl; return CR_WRONG_USAGE; @@ -1075,7 +765,7 @@ command_result nickUnitsInZone(color_ostream& out, df::building* building, strin return CR_OK; } -command_result nickUnitsInCage(color_ostream& out, df::building* building, string nick) +static command_result nickUnitsInCage(color_ostream& out, df::building* building, string nick) { // building must be a pen/pasture or pit if(!isCage(building)) @@ -1095,7 +785,7 @@ command_result nickUnitsInCage(color_ostream& out, df::building* building, strin return CR_OK; } -command_result nickUnitsInChain(color_ostream& out, df::building* building, string nick) +static command_result nickUnitsInChain(color_ostream& out, df::building* building, string nick) { out << "sorry. nicknaming chained units is not possible yet." << endl; return CR_WRONG_USAGE; @@ -1103,11 +793,11 @@ command_result nickUnitsInChain(color_ostream& out, df::building* building, stri // give all units inside a pasture or cage the same nickname // (usage example: protect them from being autobutchered) -command_result nickUnitsInBuilding(color_ostream& out, df::building* building, string nick) +static command_result nickUnitsInBuilding(color_ostream& out, df::building* building, string nick) { command_result result = CR_WRONG_USAGE; - if(isActivityZone(building)) + if(Buildings::isActivityZone(building)) result = nickUnitsInZone(out, building, nick); else if(isCage(building)) result = nickUnitsInCage(out, building, nick); @@ -1120,9 +810,9 @@ command_result nickUnitsInBuilding(color_ostream& out, df::building* building, s } // dump some zone info -void zoneInfo(color_ostream & out, df::building* building, bool verbose) +static void zoneInfo(color_ostream & out, df::building* building, bool verbose) { - if(!isActivityZone(building)) + if(!Buildings::isActivityZone(building)) return; string name; @@ -1138,7 +828,7 @@ void zoneInfo(color_ostream & out, df::building* building, bool verbose) out.print("\n"); df::building_civzonest * civ = (df::building_civzonest *) building; - if(isActive(civ)) + if(Buildings::isActive(civ)) out << "active"; else out << "not active"; @@ -1180,7 +870,7 @@ void zoneInfo(color_ostream & out, df::building* building, bool verbose) } // dump some cage info -void cageInfo(color_ostream & out, df::building* building, bool verbose) +static void cageInfo(color_ostream & out, df::building* building, bool verbose) { if(!isCage(building)) return; @@ -1220,7 +910,7 @@ void cageInfo(color_ostream & out, df::building* building, bool verbose) } // dump some chain/restraint info -void chainInfo(color_ostream & out, df::building* building, bool list_refs = false) +static void chainInfo(color_ostream & out, df::building* building, bool list_refs = false) { if(!isChain(building)) return; @@ -1247,7 +937,7 @@ void chainInfo(color_ostream & out, df::building* building, bool list_refs = fal } } -df::building* getAssignableBuildingAtCursor(color_ostream& out) +static df::building* getAssignableBuildingAtCursor(color_ostream& out) { // set building at cursor position to be new target building if (cursor->x == -30000) @@ -1267,7 +957,7 @@ df::building* getAssignableBuildingAtCursor(color_ostream& out) } else { - auto zone_at_tile = findPenPitAt(Gui::getCursorPos()); + auto zone_at_tile = Buildings::findPenPitAt(Gui::getCursorPos()); if(!zone_at_tile) { out << "No pen/pasture, pit, or cage under cursor!" << endl; @@ -1284,38 +974,38 @@ df::building* getAssignableBuildingAtCursor(color_ostream& out) // ZONE FILTERS (as in, filters used by 'zone') // Maps parameter names to filters. -unordered_map> zone_filters; +static unordered_map> zone_filters; static struct zone_filters_init { zone_filters_init() { zone_filters["caged"] = isContainedInItem; - zone_filters["egglayer"] = isEggLayer; - zone_filters["female"] = isFemale; - zone_filters["grazer"] = isGrazer; - zone_filters["hunting"] = isHunter; - zone_filters["male"] = isMale; - zone_filters["milkable"] = isMilkable; - zone_filters["naked"] = isNaked; - zone_filters["own"] = isOwnCiv; - zone_filters["tamable"] = isTamable; - zone_filters["tame"] = isTame; + zone_filters["egglayer"] = Units::isEggLayer; + zone_filters["female"] = Units::isFemale; + zone_filters["grazer"] = Units::isGrazer; + zone_filters["hunting"] = Units::isHunter; + zone_filters["male"] = Units::isMale; + zone_filters["milkable"] = Units::isMilkable; + zone_filters["naked"] = Units::isNaked; + zone_filters["own"] = Units::isOwnCiv; + zone_filters["tamable"] = Units::isTamable; + zone_filters["tame"] = Units::isTame; zone_filters["trainablewar"] = [](df::unit *unit) -> bool { - return !isWar(unit) && !isHunter(unit) && isTrainableWar(unit); + return !Units::isWar(unit) && !Units::isHunter(unit) && Units::isTrainableWar(unit); }; zone_filters["trainablehunt"] = [](df::unit *unit) -> bool { - return !isWar(unit) && !isHunter(unit) && isTrainableHunting(unit); + return !Units::isWar(unit) && !Units::isHunter(unit) && Units::isTrainableHunting(unit); }; - zone_filters["trained"] = isTrained; + zone_filters["trained"] = Units::isTrained; // backwards compatibility zone_filters["unassigned"] = [](df::unit *unit) -> bool { return !isAssigned(unit); }; - zone_filters["war"] = isWar; + zone_filters["war"] = Units::isWar; }} zone_filters_init_; // Extra annotations / descriptions for parameter names. -unordered_map zone_filter_notes; +static unordered_map zone_filter_notes; static struct zone_filter_notes_init { zone_filter_notes_init() { zone_filter_notes["caged"] = "caged (ignores built cages)"; zone_filter_notes["hunting"] = "trained hunting creature"; @@ -1326,7 +1016,7 @@ static struct zone_filter_notes_init { zone_filter_notes_init() { zone_filter_notes["war"] = "trained war creature"; }} zone_filter_notes_init_; -pair> createRaceFilter(vector &filter_args) +static pair> createRaceFilter(vector &filter_args) { // guaranteed to exist. string race = filter_args[0]; @@ -1334,12 +1024,12 @@ pair> createRaceFilter(vector &filter_ return make_pair( "race of " + race, [race](df::unit *unit) -> bool { - return getRaceName(unit) == race; + return Units::getRaceName(unit) == race; } ); } -pair> createAgeFilter(vector &filter_args) +static pair> createAgeFilter(vector &filter_args) { int target_age; stringstream ss(filter_args[0]); @@ -1360,12 +1050,12 @@ pair> createAgeFilter(vector &filter_a return make_pair( "age of exactly " + int_to_string(target_age), [target_age](df::unit *unit) -> bool { - return getAge(unit, true) == target_age; + return Units::getAge(unit, true) == target_age; } ); } -pair> createMinAgeFilter(vector &filter_args) +static pair> createMinAgeFilter(vector &filter_args) { double min_age; stringstream ss(filter_args[0]); @@ -1386,12 +1076,12 @@ pair> createMinAgeFilter(vector &filte return make_pair( "minimum age of " + int_to_string(min_age), [min_age](df::unit *unit) -> bool { - return getAge(unit, true) >= min_age; + return Units::getAge(unit, true) >= min_age; } ); } -pair> createMaxAgeFilter(vector &filter_args) +static pair> createMaxAgeFilter(vector &filter_args) { double max_age; stringstream ss(filter_args[0]); @@ -1412,7 +1102,7 @@ pair> createMaxAgeFilter(vector &filte return make_pair( "maximum age of " + int_to_string(max_age), [max_age](df::unit *unit) -> bool { - return getAge(unit, true) <= max_age; + return Units::getAge(unit, true) <= max_age; } ); } @@ -1428,7 +1118,7 @@ pair> createMaxAgeFilter(vector &filte // Constructor functions are permitted to throw strings, which will be caught and printed. // Result filter functions are not permitted to throw std::exceptions. // Result filter functions should not store references -unordered_map>(vector&)>>> zone_param_filters; static struct zone_param_filters_init { zone_param_filters_init() { zone_param_filters["race"] = make_pair(1, createRaceFilter); @@ -1437,7 +1127,7 @@ static struct zone_param_filters_init { zone_param_filters_init() { zone_param_filters["maxage"] = make_pair(1, createMaxAgeFilter); }} zone_param_filters_init_; -command_result df_zone (color_ostream &out, vector & parameters) +static command_result df_zone (color_ostream &out, vector & parameters) { CoreSuspender suspend; @@ -1526,7 +1216,7 @@ command_result df_zone (color_ostream &out, vector & parameters) else if(p0 == "unassign") { // if there's a unit selected... - df::unit *unit = getSelectedUnit(out, true); + df::unit *unit = Gui::getSelectedUnit(out, true); if (unit) { // remove assignment reference from unit and old zone if(unassignUnitFromBuilding(unit)) @@ -1865,7 +1555,7 @@ command_result df_zone (color_ostream &out, vector & parameters) active_filters.push_back([](df::unit *unit) { - return !isMerchant(unit) && !isForest(unit); + return !Units::isMerchant(unit) && !Units::isForest(unit); } ); } else { @@ -1875,7 +1565,7 @@ command_result df_zone (color_ostream &out, vector & parameters) active_filters.push_back([](df::unit *unit) { - return isMerchant(unit) || isForest(unit); + return Units::isMerchant(unit) || Units::isForest(unit); } ); } @@ -1926,7 +1616,7 @@ command_result df_zone (color_ostream &out, vector & parameters) { // filter for units in the building unordered_set assigned_unit_ids; - if(isActivityZone(target_building)) + if(Buildings::isActivityZone(target_building)) { df::building_civzonest *civz = (df::building_civzonest *) target_building; auto &assigned_units_vec = civz->assigned_units; @@ -1984,7 +1674,7 @@ command_result df_zone (color_ostream &out, vector & parameters) if(!race_filter_set && (building_assign || cagezone_assign || unit_slaughter)) { - string own_race_name = getRaceNameById(ui->race_id); + string own_race_name = Units::getRaceNameById(ui->race_id); out.color(COLOR_BROWN); out << "Default filter for " << parameters[0] << ": 'not (race " << own_race_name << " or own civilization)'; use 'race " @@ -1994,7 +1684,7 @@ command_result df_zone (color_ostream &out, vector & parameters) active_filters.push_back([](df::unit *unit) { - return !isOwnRace(unit) || !isOwnCiv(unit); + return !Units::isOwnRace(unit) || !Units::isOwnCiv(unit); } ); } @@ -2022,7 +1712,7 @@ command_result df_zone (color_ostream &out, vector & parameters) active_filters.push_back([](df::unit *unit) { - return !isMerchant(unit) && !isForest(unit); + return !Units::isMerchant(unit) && !Units::isForest(unit); } ); } @@ -2058,7 +1748,7 @@ command_result df_zone (color_ostream &out, vector & parameters) df::unit *unit = *unit_it; // ignore inactive and undead units - if (!isActive(unit) || isUndead(unit)) { + if (!Units::isActive(unit) || Units::isUndead(unit)) { continue; } @@ -2130,10 +1820,10 @@ command_result df_zone (color_ostream &out, vector & parameters) if (removed) { out << "Unit " << unit->id - << "(" << getRaceName(unit) << ")" + << "(" << Units::getRaceName(unit) << ")" << " unassigned from"; - if (isActivityZone(target_building)) + if (Buildings::isActivityZone(target_building)) { out << " zone "; } @@ -2179,7 +1869,7 @@ command_result df_zone (color_ostream &out, vector & parameters) else { // must have unit selected - df::unit *unit = getSelectedUnit(out, true); + df::unit *unit = Gui::getSelectedUnit(out, true); if (!unit) { out.color(COLOR_RED); out << "Error: no unit selected!" << endl; @@ -2207,1361 +1897,72 @@ command_result df_zone (color_ostream &out, vector & parameters) return CR_OK; } -//////////////////// -// autobutcher stuff - -// getUnitAge() returns 0 if born in current year, therefore the look at birth_time in that case -// (assuming that the value from there indicates in which tick of the current year the unit was born) -bool compareUnitAgesYounger(df::unit* i, df::unit* j) -{ - int32_t age_i = (int32_t) getAge(i, true); - int32_t age_j = (int32_t) getAge(j, true); - if(age_i == 0 && age_j == 0) - { - age_i = i->birth_time; - age_j = j->birth_time; - } - return (age_i < age_j); -} -bool compareUnitAgesOlder(df::unit* i, df::unit* j) -{ - int32_t age_i = (int32_t) getAge(i, true); - int32_t age_j = (int32_t) getAge(j, true); - if(age_i == 0 && age_j == 0) - { - age_i = i->birth_time; - age_j = j->birth_time; - } - return (age_i > age_j); -} - - - -//enum WatchedRaceSubtypes -//{ -// femaleKid=0, -// maleKid, -// femaleAdult, -// maleAdult -//}; - -enum unit_ptr_index -{ - fk_index = 0, - mk_index = 1, - fa_index = 2, - ma_index = 3 -}; +//START zone filters -struct WatchedRace +class zone_filter { public: - PersistentDataItem rconfig; - - bool isWatched; // if true, autobutcher will process this race - int raceId; - - // target amounts - unsigned fk; // max female kids - unsigned mk; // max male kids - unsigned fa; // max female adults - unsigned ma; // max male adults - - // amounts of protected (not butcherable) units - unsigned fk_prot; - unsigned fa_prot; - unsigned mk_prot; - unsigned ma_prot; - - // butcherable units - vector unit_ptr[4]; - - // priority butcherable units - vector prot_ptr[4]; - - WatchedRace(bool watch, int id, unsigned _fk, unsigned _mk, unsigned _fa, unsigned _ma) - { - isWatched = watch; - raceId = id; - fk = _fk; - mk = _mk; - fa = _fa; - ma = _ma; - fk_prot = fa_prot = mk_prot = ma_prot = 0; - } - - ~WatchedRace() + zone_filter() { - ClearUnits(); + initialized = false; } - void UpdateConfig(color_ostream & out) + void initialize(const df::ui_sidebar_mode &mode) { - if(!rconfig.isValid()) - { - string keyname = "autobutcher/watchlist/" + getRaceNameById(raceId); - rconfig = World::GetPersistentData(keyname, NULL); - } - if(rconfig.isValid()) - { - rconfig.ival(0) = raceId; - rconfig.ival(1) = isWatched; - rconfig.ival(2) = fk; - rconfig.ival(3) = mk; - rconfig.ival(4) = fa; - rconfig.ival(5) = ma; - } - else + if (!initialized) { - // this should never happen - string keyname = "autobutcher/watchlist/" + getRaceNameById(raceId); - out << "Something failed, could not find/create config key " << keyname << "!" << endl; - } - } + this->mode = mode; + saved_ui_building_assign_type.clear(); + saved_ui_building_assign_units.clear(); + saved_ui_building_assign_items.clear(); + saved_ui_building_assign_is_marked.clear(); + saved_indexes.clear(); - void RemoveConfig(color_ostream & out) - { - if(!rconfig.isValid()) - return; - World::DeletePersistentData(rconfig); - } + for (size_t i = 0; i < ui_building_assign_units->size(); i++) + { + saved_ui_building_assign_type.push_back(ui_building_assign_type->at(i)); + saved_ui_building_assign_units.push_back(ui_building_assign_units->at(i)); + saved_ui_building_assign_items.push_back(ui_building_assign_items->at(i)); + saved_ui_building_assign_is_marked.push_back(ui_building_assign_is_marked->at(i)); + } - void SortUnitsByAge() - { - sort(unit_ptr[fk_index].begin(), unit_ptr[fk_index].end(), compareUnitAgesOlder); - sort(unit_ptr[mk_index].begin(), unit_ptr[mk_index].end(), compareUnitAgesOlder); - sort(unit_ptr[fa_index].begin(), unit_ptr[fa_index].end(), compareUnitAgesYounger); - sort(unit_ptr[ma_index].begin(), unit_ptr[ma_index].end(), compareUnitAgesYounger); - sort(prot_ptr[fk_index].begin(), prot_ptr[fk_index].end(), compareUnitAgesOlder); - sort(prot_ptr[mk_index].begin(), prot_ptr[mk_index].end(), compareUnitAgesOlder); - sort(prot_ptr[fa_index].begin(), prot_ptr[fa_index].end(), compareUnitAgesYounger); - sort(prot_ptr[ma_index].begin(), prot_ptr[ma_index].end(), compareUnitAgesYounger); - } + search_string.clear(); + show_non_grazers = show_pastured = show_noncaged = show_male = show_female = show_other_zones = true; + entry_mode = false; - void PushUnit(df::unit * unit) - { - if(isFemale(unit)) - { - if(isBaby(unit) || isChild(unit)) - unit_ptr[fk_index].push_back(unit); - else - unit_ptr[fa_index].push_back(unit); - } - else //treat sex n/a like it was male - { - if(isBaby(unit) || isChild(unit)) - unit_ptr[mk_index].push_back(unit); - else - unit_ptr[ma_index].push_back(unit); + initialized = true; } } - void PushPriorityUnit(df::unit * unit) + void deinitialize() { - if(isFemale(unit)) - { - if(isBaby(unit) || isChild(unit)) - prot_ptr[fk_index].push_back(unit); - else - prot_ptr[fa_index].push_back(unit); - } - else - { - if(isBaby(unit) || isChild(unit)) - prot_ptr[mk_index].push_back(unit); - else - prot_ptr[ma_index].push_back(unit); - } + initialized = false; } - void PushProtectedUnit(df::unit * unit) + void apply_filters() { - if(isFemale(unit)) - { - if(isBaby(unit) || isChild(unit)) - fk_prot++; - else - fa_prot++; - } - else //treat sex n/a like it was male + if (saved_indexes.size() > 0) { - if(isBaby(unit) || isChild(unit)) - mk_prot++; - else - ma_prot++; - } - } + bool list_has_been_sorted = (ui_building_assign_units->size() == reference_list.size() + && *ui_building_assign_units != reference_list); - void ClearUnits() - { - fk_prot = fa_prot = mk_prot = ma_prot = 0; - for (size_t i = 0; i < 4; i++) - { - unit_ptr[i].clear(); - prot_ptr[i].clear(); - } - } + for (size_t i = 0; i < saved_indexes.size(); i++) + { + int adjusted_item_index = i; + if (list_has_been_sorted) + { + for (size_t j = 0; j < ui_building_assign_units->size(); j++) + { + if (ui_building_assign_units->at(j) == reference_list[i]) + { + adjusted_item_index = j; + break; + } + } + } - int ProcessUnits(vector& unit_ptr, vector& unit_pri_ptr, unsigned prot, unsigned goal) - { - int subcount = 0; - while(unit_pri_ptr.size() && (unit_ptr.size() + unit_pri_ptr.size() + prot > goal) ) - { - df::unit* unit = unit_pri_ptr.back(); - doMarkForSlaughter(unit); - unit_pri_ptr.pop_back(); - subcount++; - } - while(unit_ptr.size() && (unit_ptr.size() + prot > goal) ) - { - df::unit* unit = unit_ptr.back(); - doMarkForSlaughter(unit); - unit_ptr.pop_back(); - subcount++; - } - return subcount; - } - - int ProcessUnits() - { - SortUnitsByAge(); - int slaughter_count = 0; - slaughter_count += ProcessUnits(unit_ptr[fk_index], prot_ptr[fk_index], fk_prot, fk); - slaughter_count += ProcessUnits(unit_ptr[mk_index], prot_ptr[mk_index], mk_prot, mk); - slaughter_count += ProcessUnits(unit_ptr[fa_index], prot_ptr[fa_index], fa_prot, fa); - slaughter_count += ProcessUnits(unit_ptr[ma_index], prot_ptr[ma_index], ma_prot, ma); - ClearUnits(); - return slaughter_count; - } -}; -// vector of races handled by autobutcher -// the name is a bit misleading since entries can be set to 'unwatched' -// to ignore them for a while but still keep the target count settings -std::vector watched_races; - -// helper for sorting the watchlist alphabetically -bool compareRaceNames(WatchedRace* i, WatchedRace* j) -{ - string name_i = getRaceNamePluralById(i->raceId); - string name_j = getRaceNamePluralById(j->raceId); - - return (name_i < name_j); -} - -static void autobutcher_sortWatchList(color_ostream &out); - -// default target values for autobutcher -static unsigned default_fk = 5; -static unsigned default_mk = 1; -static unsigned default_fa = 5; -static unsigned default_ma = 1; - -command_result df_autobutcher(color_ostream &out, vector & parameters) -{ - CoreSuspender suspend; - - bool verbose = false; - bool watch_race = false; - bool unwatch_race = false; - bool forget_race = false; - bool list_watched = false; - bool list_export = false; - bool change_target = false; - vector target_racenames; - vector target_raceids; - - unsigned target_fk = default_fk; - unsigned target_mk = default_mk; - unsigned target_fa = default_fa; - unsigned target_ma = default_ma; - - if(!parameters.size()) - { - out << "You must specify a command!" << endl; - out << autobutcher_help << endl; - return CR_OK; - } - - // parse main command - string & p = parameters[0]; - if (p == "help" || p == "?") - { - out << autobutcher_help << endl; - return CR_OK; - } - if (p == "example") - { - out << autobutcher_help_example << endl; - return CR_OK; - } - else if (p == "start") - { - plugin_enable(out, true); - enable_autobutcher = true; - start_autobutcher(out); - return autoButcher(out, verbose); - } - else if (p == "stop") - { - enable_autobutcher = false; - if(config_autobutcher.isValid()) - config_autobutcher.ival(0) = enable_autobutcher; - out << "Autobutcher stopped." << endl; - return CR_OK; - } - else if(p == "sleep") - { - parameters.erase(parameters.begin()); - if(!parameters.size()) - { - out.printerr("No duration specified!\n"); - return CR_WRONG_USAGE; - } - else - { - size_t ticks = 0; - stringstream ss(parameters.back()); - ss >> ticks; - if(ticks <= 0) - { - out.printerr("Invalid duration specified (must be > 0)!\n"); - return CR_WRONG_USAGE; - } - sleep_autobutcher = ticks; - if(config_autobutcher.isValid()) - config_autobutcher.ival(1) = sleep_autobutcher; - out << "New sleep timer for autobutcher: " << ticks << " ticks." << endl; - return CR_OK; - } - } - else if(p == "watch") - { - parameters.erase(parameters.begin()); - watch_race = true; - out << "Start watching race(s): "; // << endl; - } - else if(p == "unwatch") - { - parameters.erase(parameters.begin()); - unwatch_race = true; - out << "Stop watching race(s): "; // << endl; - } - else if(p == "forget") - { - parameters.erase(parameters.begin()); - forget_race = true; - out << "Removing race(s) from watchlist: "; // << endl; - } - else if(p == "target") - { - // needs at least 5 more parameters: - // fk mk fa ma R (can have more than 1 R) - if(parameters.size() < 6) - { - out.printerr("Not enough parameters!\n"); - return CR_WRONG_USAGE; - } - else - { - stringstream fk(parameters[1]); - stringstream mk(parameters[2]); - stringstream fa(parameters[3]); - stringstream ma(parameters[4]); - fk >> target_fk; - mk >> target_mk; - fa >> target_fa; - ma >> target_ma; - parameters.erase(parameters.begin(), parameters.begin()+5); - change_target = true; - out << "Setting new target count for race(s): "; // << endl; - } - } - else if(p == "autowatch") - { - out << "Auto-adding to watchlist started." << endl; - enable_autobutcher_autowatch = true; - if(config_autobutcher.isValid()) - config_autobutcher.ival(2) = enable_autobutcher_autowatch; - return CR_OK; - } - else if(p == "noautowatch") - { - out << "Auto-adding to watchlist stopped." << endl; - enable_autobutcher_autowatch = false; - if(config_autobutcher.isValid()) - config_autobutcher.ival(2) = enable_autobutcher_autowatch; - return CR_OK; - } - else if(p == "list") - { - list_watched = true; - } - else if(p == "list_export") - { - list_export = true; - } - else - { - out << "Unknown command: " << p << endl; - return CR_WRONG_USAGE; - } - - if(list_watched) - { - out << "Autobutcher status: "; - - if(enable_autobutcher) - out << "enabled,"; - else - out << "not enabled,"; - - if (enable_autobutcher_autowatch) - out << " autowatch,"; - else - out << " noautowatch,"; - - out << " sleep: " << sleep_autobutcher << endl; - - out << "Default setting for new races:" - << " fk=" << default_fk - << " mk=" << default_mk - << " fa=" << default_fa - << " ma=" << default_ma - << endl; - - if(!watched_races.size()) - { - out << "The autobutcher race list is empty." << endl; - return CR_OK; - } - - out << "Races on autobutcher list: " << endl; - for(size_t i=0; iraws.creatures.all[w->raceId]; - string name = raw->creature_id; - if(w->isWatched) - out << "watched: "; - else - out << "not watched: "; - out << name - << " fk=" << w->fk - << " mk=" << w->mk - << " fa=" << w->fa - << " ma=" << w->ma - << endl; - } - return CR_OK; - } - - if(list_export) - { - // force creation of config - out << "autobutcher start" << endl; - - if(!enable_autobutcher) - out << "autobutcher stop" << endl; - - if (enable_autobutcher_autowatch) - out << "autobutcher autowatch" << endl; - - out << "autobutcher sleep " << sleep_autobutcher << endl; - out << "autobutcher target" - << " " << default_fk - << " " << default_mk - << " " << default_fa - << " " << default_ma - << " new" << endl; - - for(size_t i=0; iraws.creatures.all[w->raceId]; - string name = raw->creature_id; - - out << "autobutcher target" - << " " << w->fk - << " " << w->mk - << " " << w->fa - << " " << w->ma - << " " << name << endl; - - if(w->isWatched) - out << "autobutcher watch " << name << endl; - } - return CR_OK; - } - - // parse rest of parameters for commands followed by a list of races - if( watch_race - || unwatch_race - || forget_race - || change_target ) - { - if(!parameters.size()) - { - out.printerr("No race(s) specified!\n"); - return CR_WRONG_USAGE; - } - while(parameters.size()) - { - string tr = parameters.back(); - target_racenames.push_back(tr); - parameters.pop_back(); - out << tr << " "; - } - out << endl; - } - - if(change_target && target_racenames.size() && target_racenames[0] == "all") - { - out << "Setting target count for all races on watchlist." << endl; - for(size_t i=0; ifk = target_fk; - w->mk = target_mk; - w->fa = target_fa; - w->ma = target_ma; - w->UpdateConfig(out); - } - } - - if(target_racenames.size() && (target_racenames[0] == "all" || target_racenames[0] == "new")) - { - if(change_target) - { - out << "Setting target count for the future." << endl; - default_fk = target_fk; - default_mk = target_mk; - default_fa = target_fa; - default_ma = target_ma; - if(config_autobutcher.isValid()) - { - config_autobutcher.ival(3) = default_fk; - config_autobutcher.ival(4) = default_mk; - config_autobutcher.ival(5) = default_fa; - config_autobutcher.ival(6) = default_ma; - } - return CR_OK; - } - else if(target_racenames[0] == "new") - { - out << "The only valid usage of 'new' is in combination when setting a target count!" << endl; - - // hm, maybe instead of complaining start/stop autowatch instead? and get rid of the autowatch option? - if(unwatch_race) - out << "'unwatch new' makes no sense! Use 'noautowatch' instead." << endl; - else if(forget_race) - out << "'forget new' makes no sense, 'forget' is only for existing watchlist entries! Use 'noautowatch' instead." << endl; - else if(watch_race) - out << "'watch new' makes no sense! Use 'autowatch' instead." << endl; - return CR_WRONG_USAGE; - } - } - - if(target_racenames.size() && target_racenames[0] == "all") - { - // fill with race ids from watchlist - for(size_t i=0; iraceId); - } - } - else - { - // map race names from parameter list to ids - size_t num_races = world->raws.creatures.all.size(); - while(target_racenames.size()) - { - bool found_race = false; - for(size_t i=0; iraceId == target_raceids.back()) - { - if(unwatch_race) - { - w->isWatched=false; - w->UpdateConfig(out); - } - else if(forget_race) - { - w->RemoveConfig(out); - watched_races.erase(watched_races.begin()+i); - } - else if(watch_race) - { - w->isWatched = true; - w->UpdateConfig(out); - } - else if(change_target) - { - w->fk = target_fk; - w->mk = target_mk; - w->fa = target_fa; - w->ma = target_ma; - w->UpdateConfig(out); - } - entry_found = true; - break; - } - } - if(!entry_found && (watch_race||change_target)) - { - WatchedRace * w = new WatchedRace(watch_race, target_raceids.back(), target_fk, target_mk, target_fa, target_ma); - w->UpdateConfig(out); - watched_races.push_back(w); - autobutcher_sortWatchList(out); - } - target_raceids.pop_back(); - } - - return CR_OK; -} - -// check watched_races vector for a race id, return -1 if nothing found -// calling method needs to check itself if the race is currently being watched or ignored -int getWatchedIndex(int id) -{ - for(size_t i=0; iraceId == id) // && w->isWatched) - return i; - } - return -1; -} - -command_result autoButcher( color_ostream &out, bool verbose = false ) -{ - // don't run if not supposed to - if(!Maps::IsValid()) - return CR_OK; - - // check if there is anything to watch before walking through units vector - if(!enable_autobutcher_autowatch) - { - bool watching = false; - for(size_t i=0; iisWatched) - { - watching = true; - break; - } - } - if(!watching) - return CR_OK; - } - - for(size_t i=0; iunits.all.size(); i++) - { - df::unit * unit = world->units.all[i]; - - // this check is now divided into two steps, squeezed autowatch into the middle - // first one ignores completely inappropriate units (dead, undead, not belonging to the fort, ...) - // then let autowatch add units to the watchlist which will probably start breeding (owned pets, war animals, ...) - // then process units counting those which can't be butchered (war animals, named pets, ...) - // so that they are treated as "own stock" as well and count towards the target quota - if( !isActive(unit) - || isUndead(unit) - || isMarkedForSlaughter(unit) - || isMerchant(unit) // ignore merchants' draught animals - || isForest(unit) // ignore merchants' caged animals - || !isOwnCiv(unit) - || !isTame(unit) - ) - continue; - - // found a bugged unit which had invalid coordinates but was not in a cage. - // marking it for slaughter didn't seem to have negative effects, but you never know... - if(!isContainedInItem(unit) && !hasValidMapPos(unit)) - continue; - - WatchedRace * w = NULL; - int watched_index = getWatchedIndex(unit->race); - if(watched_index != -1) - { - w = watched_races[watched_index]; - } - else if(enable_autobutcher_autowatch) - { - w = new WatchedRace(true, unit->race, default_fk, default_mk, default_fa, default_ma); - w->UpdateConfig(out); - watched_races.push_back(w); - - string announce; - announce = "New race added to autobutcher watchlist: " + getRaceNamePluralById(w->raceId); - Gui::showAnnouncement(announce, 2, false); - autobutcher_sortWatchList(out); - } - - if(w && w->isWatched) - { - // don't butcher protected units, but count them as stock as well - // this way they count towards target quota, so if you order that you want 1 female adult cat - // and have 2 cats, one of them being a pet, the other gets butchered - if( isWar(unit) // ignore war dogs etc - || isHunter(unit) // ignore hunting dogs etc - // ignore creatures in built cages which are defined as rooms to leave zoos alone - // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() - || isAvailableForAdoption(unit) - || unit->name.has_name ) - w->PushProtectedUnit(unit); - else if ( isGay(unit) - || isGelded(unit)) - w->PushPriorityUnit(unit); - else - w->PushUnit(unit); - } - } - - int slaughter_count = 0; - for(size_t i=0; iProcessUnits(); - slaughter_count += slaughter_subcount; - if(slaughter_subcount) - { - stringstream ss; - ss << slaughter_subcount; - string announce; - announce = getRaceNamePluralById(w->raceId) + " marked for slaughter: " + ss.str(); - Gui::showAnnouncement(announce, 2, false); - } - } - - return CR_OK; -} - -//////////////////////////////////////////////////// -// autobutcher start/init/cleanup - -command_result start_autobutcher(color_ostream &out) -{ - plugin_enable(out, true); - enable_autobutcher = true; - - if (!config_autobutcher.isValid()) - { - config_autobutcher = World::AddPersistentData("autobutcher/config"); - - if (!config_autobutcher.isValid()) - { - out << "Cannot enable autobutcher without a world!" << endl; - return CR_OK; - } - - config_autobutcher.ival(1) = sleep_autobutcher; - config_autobutcher.ival(2) = enable_autobutcher_autowatch; - config_autobutcher.ival(3) = default_fk; - config_autobutcher.ival(4) = default_mk; - config_autobutcher.ival(5) = default_fa; - config_autobutcher.ival(6) = default_ma; - } - - config_autobutcher.ival(0) = enable_autobutcher; - - out << "Starting autobutcher." << endl; - init_autobutcher(out); - return CR_OK; -} - -command_result init_autobutcher(color_ostream &out) -{ - cleanup_autobutcher(out); - - config_autobutcher = World::GetPersistentData("autobutcher/config"); - if(config_autobutcher.isValid()) - { - if (config_autobutcher.ival(0) == -1) - { - config_autobutcher.ival(0) = enable_autobutcher; - config_autobutcher.ival(1) = sleep_autobutcher; - config_autobutcher.ival(2) = enable_autobutcher_autowatch; - config_autobutcher.ival(3) = default_fk; - config_autobutcher.ival(4) = default_mk; - config_autobutcher.ival(5) = default_fa; - config_autobutcher.ival(6) = default_ma; - out << "Autobutcher's persistent config object was invalid!" << endl; - } - else - { - enable_autobutcher = config_autobutcher.ival(0); - sleep_autobutcher = config_autobutcher.ival(1); - enable_autobutcher_autowatch = config_autobutcher.ival(2); - default_fk = config_autobutcher.ival(3); - default_mk = config_autobutcher.ival(4); - default_fa = config_autobutcher.ival(5); - default_ma = config_autobutcher.ival(6); - } - } - - if(!enable_autobutcher) - return CR_OK; - - plugin_enable(out, true); - // read watchlist from save - - std::vector items; - World::GetPersistentData(&items, "autobutcher/watchlist/", true); - for (auto p = items.begin(); p != items.end(); p++) - { - string key = p->key(); - out << "Reading from save: " << key << endl; - //out << " raceid: " << p->ival(0) << endl; - //out << " watched: " << p->ival(1) << endl; - //out << " fk: " << p->ival(2) << endl; - //out << " mk: " << p->ival(3) << endl; - //out << " fa: " << p->ival(4) << endl; - //out << " ma: " << p->ival(5) << endl; - WatchedRace * w = new WatchedRace(p->ival(1), p->ival(0), p->ival(2), p->ival(3),p->ival(4),p->ival(5)); - w->rconfig = *p; - watched_races.push_back(w); - } - autobutcher_sortWatchList(out); - return CR_OK; -} - -command_result cleanup_autobutcher(color_ostream &out) -{ - for(size_t i=0; iunits.all.size(); i++) - { - df::unit * unit = world->units.all[i]; - - if(unit->race != race) - continue; - - if( !isActive(unit) - || isUndead(unit) - || isMerchant(unit) // ignore merchants' draught animals - || isForest(unit) // ignore merchants' caged animals - || !isOwnCiv(unit) - ) - continue; - - // found a bugged unit which had invalid coordinates but was not in a cage. - // marking it for slaughter didn't seem to have negative effects, but you never know... - if(!isContainedInItem(unit) && !hasValidMapPos(unit)) - continue; - - w->PushUnit(unit); - } - return w; -} - -WatchedRace * checkRaceStocksProtected(int race) -{ - WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma); - - for(size_t i=0; iunits.all.size(); i++) - { - df::unit * unit = world->units.all[i]; - - if(unit->race != race) - continue; - - if( !isActive(unit) - || isUndead(unit) - || isMerchant(unit) // ignore merchants' draught animals - || isForest(unit) // ignore merchants' caged animals - || !isOwnCiv(unit) - ) - continue; - - // found a bugged unit which had invalid coordinates but was not in a cage. - // marking it for slaughter didn't seem to have negative effects, but you never know... - if(!isContainedInItem(unit) && !hasValidMapPos(unit)) - continue; - - if( !isTame(unit) - || isWar(unit) // ignore war dogs etc - || isHunter(unit) // ignore hunting dogs etc - // ignore creatures in built cages which are defined as rooms to leave zoos alone - // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() - || isAvailableForAdoption(unit) - || unit->name.has_name ) - w->PushUnit(unit); - } - return w; -} - -WatchedRace * checkRaceStocksButcherable(int race) -{ - WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma); - - for(size_t i=0; iunits.all.size(); i++) - { - df::unit * unit = world->units.all[i]; - - if(unit->race != race) - continue; - - if( !isActive(unit) - || isUndead(unit) - || isMerchant(unit) // ignore merchants' draught animals - || isForest(unit) // ignore merchants' caged animals - || !isOwnCiv(unit) - || !isTame(unit) - || isWar(unit) // ignore war dogs etc - || isHunter(unit) // ignore hunting dogs etc - // ignore creatures in built cages which are defined as rooms to leave zoos alone - // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() - || isAvailableForAdoption(unit) - || unit->name.has_name - ) - continue; - - // found a bugged unit which had invalid coordinates but was not in a cage. - // marking it for slaughter didn't seem to have negative effects, but you never know... - if(!isContainedInItem(unit) && !hasValidMapPos(unit)) - continue; - - w->PushUnit(unit); - } - return w; -} - -WatchedRace * checkRaceStocksButcherFlag(int race) -{ - WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma); - - for(size_t i=0; iunits.all.size(); i++) - { - df::unit * unit = world->units.all[i]; - - if(unit->race != race) - continue; - - if( !isActive(unit) - || isUndead(unit) - || isMerchant(unit) // ignore merchants' draught animals - || isForest(unit) // ignore merchants' caged animals - || !isOwnCiv(unit) - ) - continue; - - // found a bugged unit which had invalid coordinates but was not in a cage. - // marking it for slaughter didn't seem to have negative effects, but you never know... - if(!isContainedInItem(unit) && !hasValidMapPos(unit)) - continue; - - if(isMarkedForSlaughter(unit)) - w->PushUnit(unit); - } - return w; -} - -void butcherRace(int race) -{ - for(size_t i=0; iunits.all.size(); i++) - { - df::unit * unit = world->units.all[i]; - - if(unit->race != race) - continue; - - if( !isActive(unit) - || isUndead(unit) - || isMerchant(unit) // ignore merchants' draught animals - || isForest(unit) // ignore merchants' caged animals - || !isOwnCiv(unit) - || !isTame(unit) - || isWar(unit) // ignore war dogs etc - || isHunter(unit) // ignore hunting dogs etc - // ignore creatures in built cages which are defined as rooms to leave zoos alone - // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() - || isAvailableForAdoption(unit) - || unit->name.has_name - ) - continue; - - // found a bugged unit which had invalid coordinates but was not in a cage. - // marking it for slaughter didn't seem to have negative effects, but you never know... - if(!isContainedInItem(unit) && !hasValidMapPos(unit)) - continue; - - doMarkForSlaughter(unit); - } -} - -// remove butcher flag for all units of a given race -void unbutcherRace(int race) -{ - for(size_t i=0; iunits.all.size(); i++) - { - df::unit * unit = world->units.all[i]; - - if(unit->race != race) - continue; - - if( !isActive(unit) - || isUndead(unit) - || !isMarkedForSlaughter(unit) - ) - continue; - - if(!isContainedInItem(unit) && !hasValidMapPos(unit)) - continue; - - unit->flags2.bits.slaughter = 0; - } -} - - -///////////////////////////////////// -// API functions to control autobutcher with a lua script - -static bool autobutcher_isEnabled() { return enable_autobutcher; } -static bool autowatch_isEnabled() { return enable_autobutcher_autowatch; } - -static unsigned autobutcher_getSleep(color_ostream &out) -{ - return sleep_autobutcher; -} - -static void autobutcher_setSleep(color_ostream &out, unsigned ticks) -{ - sleep_autobutcher = ticks; - if(config_autobutcher.isValid()) - config_autobutcher.ival(1) = sleep_autobutcher; -} - -static void autobutcher_setEnabled(color_ostream &out, bool enable) -{ - if(enable) - { - enable_autobutcher = true; - start_autobutcher(out); - autoButcher(out, false); - } - else - { - enable_autobutcher = false; - if(config_autobutcher.isValid()) - config_autobutcher.ival(0) = enable_autobutcher; - out << "Autobutcher stopped." << endl; - } - - if (enable) - plugin_enable(out, true); -} - -static void autowatch_setEnabled(color_ostream &out, bool enable) -{ - if(enable) - { - out << "Auto-adding to watchlist started." << endl; - enable_autobutcher_autowatch = true; - if(config_autobutcher.isValid()) - config_autobutcher.ival(2) = enable_autobutcher_autowatch; - } - else - { - out << "Auto-adding to watchlist stopped." << endl; - enable_autobutcher_autowatch = false; - if(config_autobutcher.isValid()) - config_autobutcher.ival(2) = enable_autobutcher_autowatch; - } -} - -// set all data for a watchlist race in one go -// if race is not already on watchlist it will be added -// params: (id, fk, mk, fa, ma, watched) -static void autobutcher_setWatchListRace(color_ostream &out, unsigned id, unsigned fk, unsigned mk, unsigned fa, unsigned ma, bool watched) -{ - int watched_index = getWatchedIndex(id); - if(watched_index != -1) - { - out << "updating watchlist entry" << endl; - WatchedRace * w = watched_races[watched_index]; - w->fk = fk; - w->mk = mk; - w->fa = fa; - w->ma = ma; - w->isWatched = watched; - w->UpdateConfig(out); - } - else - { - out << "creating new watchlist entry" << endl; - WatchedRace * w = new WatchedRace(watched, id, fk, mk, fa, ma); //default_fk, default_mk, default_fa, default_ma); - w->UpdateConfig(out); - watched_races.push_back(w); - - string announce; - announce = "New race added to autobutcher watchlist: " + getRaceNamePluralById(w->raceId); - Gui::showAnnouncement(announce, 2, false); - autobutcher_sortWatchList(out); - } -} - -// remove entry from watchlist -static void autobutcher_removeFromWatchList(color_ostream &out, unsigned id) -{ - int watched_index = getWatchedIndex(id); - if(watched_index != -1) - { - out << "updating watchlist entry" << endl; - WatchedRace * w = watched_races[watched_index]; - w->RemoveConfig(out); - watched_races.erase(watched_races.begin() + watched_index); - } -} - -// sort watchlist alphabetically -static void autobutcher_sortWatchList(color_ostream &out) -{ - sort(watched_races.begin(), watched_races.end(), compareRaceNames); -} - -// set default target values for new races -static void autobutcher_setDefaultTargetNew(color_ostream &out, unsigned fk, unsigned mk, unsigned fa, unsigned ma) -{ - default_fk = fk; - default_mk = mk; - default_fa = fa; - default_ma = ma; - if(config_autobutcher.isValid()) - { - config_autobutcher.ival(3) = default_fk; - config_autobutcher.ival(4) = default_mk; - config_autobutcher.ival(5) = default_fa; - config_autobutcher.ival(6) = default_ma; - } -} - -// set default target values for ALL races (update watchlist and set new default) -static void autobutcher_setDefaultTargetAll(color_ostream &out, unsigned fk, unsigned mk, unsigned fa, unsigned ma) -{ - for(unsigned i=0; ifk = fk; - w->mk = mk; - w->fa = fa; - w->ma = ma; - w->UpdateConfig(out); - } - autobutcher_setDefaultTargetNew(out, fk, mk, fa, ma); -} - -static void autobutcher_butcherRace(color_ostream &out, unsigned id) -{ - butcherRace(id); -} - -static void autobutcher_unbutcherRace(color_ostream &out, unsigned id) -{ - unbutcherRace(id); -} - -// push autobutcher settings on lua stack -static int autobutcher_getSettings(lua_State *L) -{ - lua_newtable(L); - int ctable = lua_gettop(L); - Lua::SetField(L, enable_autobutcher, ctable, "enable_autobutcher"); - Lua::SetField(L, enable_autobutcher_autowatch, ctable, "enable_autowatch"); - Lua::SetField(L, default_fk, ctable, "fk"); - Lua::SetField(L, default_mk, ctable, "mk"); - Lua::SetField(L, default_fa, ctable, "fa"); - Lua::SetField(L, default_ma, ctable, "ma"); - Lua::SetField(L, sleep_autobutcher, ctable, "sleep"); - return 1; -} - -// push the watchlist vector as nested table on the lua stack -static int autobutcher_getWatchList(lua_State *L) -{ - lua_newtable(L); - - for(size_t i=0; iraceId, ctable, "id"); - Lua::SetField(L, w->isWatched, ctable, "watched"); - Lua::SetField(L, getRaceNamePluralById(w->raceId), ctable, "name"); - Lua::SetField(L, w->fk, ctable, "fk"); - Lua::SetField(L, w->mk, ctable, "mk"); - Lua::SetField(L, w->fa, ctable, "fa"); - Lua::SetField(L, w->ma, ctable, "ma"); - - int id = w->raceId; - - w = checkRaceStocksTotal(id); - Lua::SetField(L, w->unit_ptr[fk_index].size(), ctable, "fk_total"); - Lua::SetField(L, w->unit_ptr[mk_index].size(), ctable, "mk_total"); - Lua::SetField(L, w->unit_ptr[fa_index].size(), ctable, "fa_total"); - Lua::SetField(L, w->unit_ptr[ma_index].size(), ctable, "ma_total"); - delete w; - - w = checkRaceStocksProtected(id); - Lua::SetField(L, w->unit_ptr[fk_index].size(), ctable, "fk_protected"); - Lua::SetField(L, w->unit_ptr[mk_index].size(), ctable, "mk_protected"); - Lua::SetField(L, w->unit_ptr[fa_index].size(), ctable, "fa_protected"); - Lua::SetField(L, w->unit_ptr[ma_index].size(), ctable, "ma_protected"); - delete w; - - w = checkRaceStocksButcherable(id); - Lua::SetField(L, w->unit_ptr[fk_index].size(), ctable, "fk_butcherable"); - Lua::SetField(L, w->unit_ptr[mk_index].size(), ctable, "mk_butcherable"); - Lua::SetField(L, w->unit_ptr[fa_index].size(), ctable, "fa_butcherable"); - Lua::SetField(L, w->unit_ptr[ma_index].size(), ctable, "ma_butcherable"); - delete w; - - w = checkRaceStocksButcherFlag(id); - Lua::SetField(L, w->unit_ptr[fk_index].size(), ctable, "fk_butcherflag"); - Lua::SetField(L, w->unit_ptr[mk_index].size(), ctable, "mk_butcherflag"); - Lua::SetField(L, w->unit_ptr[fa_index].size(), ctable, "fa_butcherflag"); - Lua::SetField(L, w->unit_ptr[ma_index].size(), ctable, "ma_butcherflag"); - delete w; - - lua_rawseti(L, -2, i+1); - } - - return 1; -} - -DFHACK_PLUGIN_LUA_FUNCTIONS { - DFHACK_LUA_FUNCTION(autobutcher_isEnabled), - DFHACK_LUA_FUNCTION(autowatch_isEnabled), - DFHACK_LUA_FUNCTION(autobutcher_setEnabled), - DFHACK_LUA_FUNCTION(autowatch_setEnabled), - DFHACK_LUA_FUNCTION(autobutcher_getSleep), - DFHACK_LUA_FUNCTION(autobutcher_setSleep), - DFHACK_LUA_FUNCTION(autobutcher_setWatchListRace), - DFHACK_LUA_FUNCTION(autobutcher_setDefaultTargetNew), - DFHACK_LUA_FUNCTION(autobutcher_setDefaultTargetAll), - DFHACK_LUA_FUNCTION(autobutcher_butcherRace), - DFHACK_LUA_FUNCTION(autobutcher_unbutcherRace), - DFHACK_LUA_FUNCTION(autobutcher_removeFromWatchList), - DFHACK_LUA_FUNCTION(autobutcher_sortWatchList), - DFHACK_LUA_END -}; - -DFHACK_PLUGIN_LUA_COMMANDS { - DFHACK_LUA_COMMAND(autobutcher_getSettings), - DFHACK_LUA_COMMAND(autobutcher_getWatchList), - DFHACK_LUA_END -}; - -// end lua API - - - -//START zone filters - -class zone_filter -{ -public: - zone_filter() - { - initialized = false; - } - - void initialize(const df::ui_sidebar_mode &mode) - { - if (!initialized) - { - this->mode = mode; - saved_ui_building_assign_type.clear(); - saved_ui_building_assign_units.clear(); - saved_ui_building_assign_items.clear(); - saved_ui_building_assign_is_marked.clear(); - saved_indexes.clear(); - - for (size_t i = 0; i < ui_building_assign_units->size(); i++) - { - saved_ui_building_assign_type.push_back(ui_building_assign_type->at(i)); - saved_ui_building_assign_units.push_back(ui_building_assign_units->at(i)); - saved_ui_building_assign_items.push_back(ui_building_assign_items->at(i)); - saved_ui_building_assign_is_marked.push_back(ui_building_assign_is_marked->at(i)); - } - - search_string.clear(); - show_non_grazers = show_pastured = show_noncaged = show_male = show_female = show_other_zones = true; - entry_mode = false; - - initialized = true; - } - } - - void deinitialize() - { - initialized = false; - } - - void apply_filters() - { - if (saved_indexes.size() > 0) - { - bool list_has_been_sorted = (ui_building_assign_units->size() == reference_list.size() - && *ui_building_assign_units != reference_list); - - for (size_t i = 0; i < saved_indexes.size(); i++) - { - int adjusted_item_index = i; - if (list_has_been_sorted) - { - for (size_t j = 0; j < ui_building_assign_units->size(); j++) - { - if (ui_building_assign_units->at(j) == reference_list[i]) - { - adjusted_item_index = j; - break; - } - } - } - - saved_ui_building_assign_is_marked[saved_indexes[i]] = ui_building_assign_is_marked->at(adjusted_item_index); - } + saved_ui_building_assign_is_marked[saved_indexes[i]] = ui_building_assign_is_marked->at(adjusted_item_index); + } } string search_string_l = toLower(search_string); @@ -3578,7 +1979,7 @@ public: if (!curr_unit) continue; - if (!show_non_grazers && !isGrazer(curr_unit)) + if (!show_non_grazers && !Units::isGrazer(curr_unit)) continue; if (!show_pastured && isAssignedToZone(curr_unit)) @@ -3594,10 +1995,10 @@ public: continue; } - if (!show_male && isMale(curr_unit)) + if (!show_male && Units::isMale(curr_unit)) continue; - if (!show_female && isFemale(curr_unit)) + if (!show_female && Units::isFemale(curr_unit)) continue; if (!search_string_l.empty()) @@ -3894,21 +2295,15 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable) DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand( - "zone", "manage activity zones.", - df_zone, false, - zone_help.c_str() - )); - commands.push_back(PluginCommand( - "autobutcher", "auto-assign lifestock for butchering.", - df_autobutcher, false, - autobutcher_help.c_str() - )); - init_autobutcher(out); + "zone", + "manage activity zones.", + df_zone, + false, + zone_help.c_str())); return CR_OK; } DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { - cleanup_autobutcher(out); return CR_OK; } From fe2212db96244c056e00a374bba5269aad3e49fc Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 2 Aug 2022 01:07:36 -0700 Subject: [PATCH 03/10] output status when run without params --- plugins/autonestbox.cpp | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index 6c878806c..b1932b269 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -41,12 +41,14 @@ static const string autonestbox_help = "Usage:\n" "\n" "enable autonestbox\n" - " Start checking for unpastured egg-layers and assigning them to nestbox zones.\n" + " Start checking for unpastured egg-layers and assigning them to nestbox zones.\n" + "autonestbox\n" + " Print current status." "autonestbox now\n" - " Run a scan and assignment cycle right now. Does not require that the plugin is enabled.\n" + " Run a scan and assignment cycle right now. Does not require that the plugin is enabled.\n" "autonestbox ticks \n" - " Change the number of ticks between scan and assignment cycles when the plugin is enabled.\n" - " The default is 6000 (about 8 days)\n"; + " Change the number of ticks between scan and assignment cycles when the plugin is enabled.\n" + " The default is 6000 (about 8 days)\n"; namespace DFHack { DBG_DECLARE(autonestbox, status); @@ -56,8 +58,8 @@ namespace DFHack { static const string CONFIG_KEY = "autonestbox/config"; static PersistentDataItem config; enum ConfigValues { - IS_ENABLED = 0, - CYCLE_TICKS = 1, + CONFIG_IS_ENABLED = 0, + CONFIG_CYCLE_TICKS = 1, }; static int get_config_val(int index) { if (!config.isValid()) @@ -103,15 +105,15 @@ static void init_autonestbox(color_ostream &out) { if (!config.isValid()) config = World::AddPersistentData(CONFIG_KEY); - if (get_config_val(IS_ENABLED) == -1) { - set_config_val(IS_ENABLED, 0); - set_config_val(CYCLE_TICKS, 6000); + if (get_config_val(CONFIG_IS_ENABLED) == -1) { + set_config_val(CONFIG_IS_ENABLED, 0); + set_config_val(CONFIG_CYCLE_TICKS, 6000); } if (is_enabled) - set_config_val(IS_ENABLED, 1); + set_config_val(CONFIG_IS_ENABLED, 1); else - is_enabled = (get_config_val(IS_ENABLED) == 1); + is_enabled = (get_config_val(CONFIG_IS_ENABLED) == 1); did_complain = false; } @@ -167,7 +169,8 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (is_enabled && ++cycle_counter >= (size_t)get_config_val(CYCLE_TICKS)) + if (is_enabled && ++cycle_counter >= + (size_t)get_config_val(CONFIG_CYCLE_TICKS)) autonestbox_cycle(out); return CR_OK; } @@ -209,12 +212,15 @@ static command_result df_autonestbox(color_ostream &out, vector ¶met return CR_WRONG_USAGE; if (opts.ticks > -1) { - set_config_val(CYCLE_TICKS, opts.ticks); + set_config_val(CONFIG_CYCLE_TICKS, opts.ticks); INFO(status,out).print("New cycle timer: %d ticks.\n", opts.ticks); } else if (opts.now) { autonestbox_cycle(out); } + else { + out << "autonestbox is " << (is_enabled ? "" : "not ") << "running\n"; + } return CR_OK; } From 3983b4d75b41c31510c42ae833825514b730232a Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 2 Aug 2022 01:54:21 -0700 Subject: [PATCH 04/10] update docs --- plugins/autobutcher.cpp | 121 +++++++++++++++++++++--------------- plugins/autonestbox.cpp | 1 - plugins/lua/autobutcher.lua | 9 --- 3 files changed, 72 insertions(+), 59 deletions(-) diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 0d7a9c632..69799207a 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -34,63 +34,86 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(world); const string autobutcher_help = - "Assigns your lifestock for slaughter once it reaches a specific count. Requires\n" + "Automatically butcher excess livestock. This plugin monitors how many pets\n" + "you have of each gender and age and assigns excess lifestock for slaughter\n" + "once they reach a specific count. Requires\n" "that you add the target race(s) to a watch list. Only tame units will be\n" "processed. Named units will be completely ignored (you can give animals\n" "nicknames with the tool 'rename unit' to protect them from getting slaughtered\n" - "automatically. Trained war or hunting pets will be ignored.\n" - "Once you have too much adults, the oldest will be butchered first.\n" - "Once you have too much kids, the youngest will be butchered first.\n" + "automatically). Trained war or hunting pets will be ignored.\n" + "Once you have too many adults, the oldest will be butchered first.\n" + "Once you have too many kids, the youngest will be butchered first.\n" "If you don't set a target count the following default will be used:\n" "1 male kid, 5 female kids, 1 male adult, 5 female adults.\n" - "Options:\n" - " start - run every X frames (df simulation ticks)\n" - " default: X=6000 (~60 seconds at 100fps)\n" - " stop - stop running automatically\n" - " sleep X - change timer to sleep X frames between runs.\n" - " watch R - start watching race(s)\n" - " R = valid race RAW id (ALPACA, BIRD_TURKEY, etc)\n" - " or a list of RAW ids seperated by spaces\n" - " or the keyword 'all' which affects your whole current watchlist.\n" - " unwatch R - stop watching race(s)\n" - " the current target settings will be remembered\n" - " forget R - unwatch race(s) and forget target settings for it/them\n" - " autowatch - automatically adds all new races (animals you buy\n" - " from merchants, tame yourself or get from migrants)\n" - " to the watch list using default target count\n" - " noautowatch - stop auto-adding new races to the watch list\n" - " list - print status and watchlist\n" - " list_export - print status and watchlist in batchfile format\n" - " can be used to copy settings into another savegame\n" - " usage: 'dfhack-run autobutcher list_export > xyz.bat' \n" - " target fk mk fa ma R\n" - " - set target count for specified race:\n" - " fk = number of female kids\n" - " mk = number of male kids\n" - " fa = number of female adults\n" - " ma = number of female adults\n" - " R = 'all' sets count for all races on the current watchlist\n" - " including the races which are currenly set to 'unwatched'\n" - " and sets the new default for future watch commands\n" - " R = 'new' sets the new default for future watch commands\n" - " without changing your current watchlist\n" - " example - print some usage examples\n"; - -const string autobutcher_help_example = + "\n" + "Usage:\n" + "\n" + "enable autobutcher\n" + " Start processing livestock according to the configuration. Note that\n" + " no races are watched by default. You have to add the ones you want to\n" + " monitor (or use autowatch)\n" + "autobutcher autowatch\n" + " Automatically add all new races (animals you buy\n" + " from merchants, tame yourself, or get from migrants)\n" + " to the watch list using the default target counts.\n" + "autobutcher noautowatch\n" + " Stop auto-adding new races to the watch list.\n" + "autobutcher target all|new| [ ...] \n" + " Set target counts for the specified races:\n" + " fk = number of female kids\n" + " mk = number of male kids\n" + " fa = number of female adults\n" + " ma = number of female adults\n" + " If you specify 'all', then this command will set the counts for all races\n" + " on your current watchlist (including the races which are currenly set to\n" + " 'unwatched') and sets the new default for future watch commands. If you\n" + " specify 'new', then this command just sets the new default counts for\n" + " future watch commands without changing your current watchlist. Otherwise,\n" + " all space separated races listed will be modified (or added to the watchlist\n" + " if they aren't there already).\n" + "autobutcher ticks \n" + " Change the number of ticks between scanning cycles when the plugin is\n" + " enabled. By default, a cycle happens every 6000 ticks (about 8 game days).\n" + "autobutcher watch all| [ ...]\n" + " Start watching the listed races. If they aren't already in your watchlist, then\n" + " they will be added with the default target counts. If you specify the keyword 'all',\n" + " then all races in your watchlist that are currently marked as unwatched will become\n" + " watched.\n" + "autobutcher unwatch all| [ ...]\n" + " Stop watching the specified race(s) (or all races on your watchlist if 'all' is\n" + " given). The current target settings will be remembered.\n" + "autobutcher forget all| [ ...]\n" + " Unwatch the specified race(s) (or all races on your watchlist if 'all' is given)\n" + " and forget target settings for it/them.\n" + "autobutcher [list]\n" + " Print status and current settings, including the watchlist.\n" + "autobutcher list_export\n" + " Print commands required to set the current settings in another fort.\n" + " Useful to run form dfhack-run like: 'dfhack-run autobutcher list_export > autobutcher.script'\n" + "\n" + "To see a list of all races, run this command:\n" + "\n" + " devel/query --table df.global.world.raws.creatures.all --search ^creature_id --maxdepth 1'\n" + "\n" + "Though not all the races listed there are tameable/butcherable\n" + "\n" "Examples:\n" - " autobutcher target 4 3 2 1 ALPACA BIRD_TURKEY\n" - " autobutcher watch ALPACA BIRD_TURKEY\n" - " autobutcher start\n" - " This means you want to have max 7 kids (4 female, 3 male) and max 3 adults\n" - " (2 female, 1 male) of the races alpaca and turkey. Once the kids grow up the\n" + "\n" + "autobutcher target 4 3 2 1 BIRD_TURKEY\n" + " This means you want to have at most 7 kids (4 female, 3 male) and at most 3 adults\n" + " (2 female, 1 male) for turkeys. Once the kids grow up, the\n" " oldest adults will get slaughtered. Excess kids will get slaughtered starting\n" " the the youngest to allow that the older ones grow into adults.\n" - " autobutcher target 0 0 0 0 new\n" - " autobutcher autowatch\n" - " autobutcher start\n" - " This tells autobutcher to automatically put all new races onto the watchlist\n" - " and mark unnamed tame units for slaughter as soon as they arrive in your\n" - " fortress. Settings already made for some races will be left untouched.\n"; + "autobutcher target 2 2 2 2 DOG\n" + "autobutcher target 1 1 2 2 CAT\n" + "autobutcher target 50 50 14 2 BIRD_GOOSE\n" + "autobutcher target 2 2 4 2 ALPACA SHEEP LLAMA\n" + "autobutcher target 5 5 6 2 PIG\n" + "autobutcher target 0 0 0 0 new\n" + "autobutcher autowatch\n" + " Configure useful limits for dogs, cats, geese (for eggs, leather, and bones), alpacas, sheep,\n" + " and llamas (for wool), and pigs (for milk and meat). All other unnamed tame units will be marked\n" + " for slaughter as soon as they arrive in your fortress.\n"; namespace DFHack { DBG_DECLARE(autobutcher, status); diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index b1932b269..f61f9d635 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -37,7 +37,6 @@ static const string autonestbox_help = "If the pen is bigger than 1x1 the nestbox must be in the top left corner.\n" "Only 1 unit will be assigned per pen, regardless of the size.\n" "The age of the units is currently not checked, most birds grow up quite fast.\n" - "When called without options autonestbox will instantly run once.\n" "Usage:\n" "\n" "enable autonestbox\n" diff --git a/plugins/lua/autobutcher.lua b/plugins/lua/autobutcher.lua index ab334896c..f357f8fb1 100644 --- a/plugins/lua/autobutcher.lua +++ b/plugins/lua/autobutcher.lua @@ -1,14 +1,5 @@ local _ENV = mkmodule('plugins.autobutcher') ---[[ - - Native functions: - - * autobutcher_isEnabled() - * autowatch_isEnabled() - ---]] - local argparse = require('argparse') local function is_int(val) From db81538f63c3a4a6c334d084dccd311077e50885 Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 2 Aug 2022 02:00:15 -0700 Subject: [PATCH 05/10] update changelog --- docs/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index ce61d31d3..efaab734d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -34,6 +34,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future ## New Plugins +- `autonestbox`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autonestbox start`` to `enable autonestbox`. +- `autobutcher`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autobutcher start`` to `enable autobutcher`. ## New Tweaks From 4acb59cb64666f0ce3c57c56b3f3de667a433a15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Aug 2022 09:01:43 +0000 Subject: [PATCH 06/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/autonestbox.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index f61f9d635..10c0e3cd8 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -100,7 +100,7 @@ static void autonestbox_cycle(color_ostream &out); static void init_autonestbox(color_ostream &out) { config = World::GetPersistentData(CONFIG_KEY); - + if (!config.isValid()) config = World::AddPersistentData(CONFIG_KEY); From 9595e2152da4e9502b9926894b7af9540041d13e Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 2 Aug 2022 02:02:23 -0700 Subject: [PATCH 07/10] update changelog (fix typo) --- docs/changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index efaab734d..2733cd539 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -34,8 +34,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future ## New Plugins -- `autonestbox`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autonestbox start`` to `enable autonestbox`. -- `autobutcher`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autobutcher start`` to `enable autobutcher`. +- `autonestbox`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autonestbox start`` to ``enable autonestbox``. +- `autobutcher`: split off from `zone` into its own plugin. Note that to enable, the command has changed from ``autobutcher start`` to ``enable autobutcher``. ## New Tweaks From f98015ae5544b526aab2799796894bd1535f42c4 Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 2 Aug 2022 10:42:54 -0700 Subject: [PATCH 08/10] ensure we run every N ticks, not frames add more debug messages fix watching/unwatching/forgetting races that aren't in the watchlist --- data/examples/init/onMapLoad_dreamfort.init | 10 ++- plugins/autobutcher.cpp | 67 +++++++++++++++------ plugins/autonestbox.cpp | 14 +++-- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/data/examples/init/onMapLoad_dreamfort.init b/data/examples/init/onMapLoad_dreamfort.init index c75620846..05d32134f 100644 --- a/data/examples/init/onMapLoad_dreamfort.init +++ b/data/examples/init/onMapLoad_dreamfort.init @@ -37,17 +37,14 @@ enable automelt # creates manager orders to produce replacements for worn clothing enable tailor -tailor enable # auto-assigns nesting birds to nestbox zones and protects fertile eggs from # being cooked/eaten -enable zone nestboxes -autonestbox start +enable zone autonestbox nestboxes # manages seed stocks enable seedwatch seedwatch all 30 -seedwatch start # ensures important tasks get assigned to workers. # otherwise these job types can get ignored in busy forts. @@ -65,6 +62,7 @@ prioritize -a --reaction-name=TAN_A_HIDE CustomReaction # feel free to change this to "target 0 0 0 0" if you don't expect to want to raise # any animals not listed here -- you can always change it anytime during the game # later if you change your mind. +on-new-fortress enable autobutcher on-new-fortress autobutcher target 2 2 2 2 new # dogs and cats. You should raise the limits for dogs if you will be training them # for hunting or war. @@ -82,5 +80,5 @@ on-new-fortress autobutcher target 2 2 4 2 ALPACA SHEEP LLAMA on-new-fortress autobutcher target 5 5 6 2 PIG # butcher all unprofitable animals on-new-fortress autobutcher target 0 0 0 0 HORSE YAK DONKEY WATER_BUFFALO GOAT CAVY BIRD_DUCK BIRD_GUINEAFOWL -# start it up! -on-new-fortress autobutcher start; autobutcher watch all; autobutcher autowatch +# watch for new animals +on-new-fortress autobutcher autowatch diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 69799207a..0b0d4ffdb 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -150,7 +150,7 @@ static void set_config_bool(int index, bool value) { } static unordered_map race_to_id; -static size_t cycle_counter = 0; // how many ticks since the last cycle +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle static size_t DEFAULT_CYCLE_TICKS = 6000; @@ -252,7 +252,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (is_enabled && ++cycle_counter >= (size_t)get_config_val(CONFIG_CYCLE_TICKS)) + if (is_enabled && world->frame_counter - cycle_timestamp >= get_config_val(CONFIG_CYCLE_TICKS)) autobutcher_cycle(out); return CR_OK; } @@ -663,7 +663,8 @@ static void autobutcher_status(color_ostream &out) { static void autobutcher_target(color_ostream &out, const autobutcher_options &opts) { if (opts.races_new) { - DEBUG(status,out).print("setting targets for new races\n"); + DEBUG(status,out).print("setting targets for new races to fk=%u, mk=%u, fa=%u, ma=%u\n", + opts.fk, opts.mk, opts.fa, opts.ma); set_config_val(CONFIG_DEFAULT_FK, opts.fk); set_config_val(CONFIG_DEFAULT_MK, opts.mk); set_config_val(CONFIG_DEFAULT_FA, opts.fa); @@ -671,7 +672,8 @@ static void autobutcher_target(color_ostream &out, const autobutcher_options &op } if (opts.races_all) { - DEBUG(status,out).print("setting targets for all races on watchlist\n"); + DEBUG(status,out).print("setting targets for all races on watchlist to fk=%u, mk=%u, fa=%u, ma=%u\n", + opts.fk, opts.mk, opts.fa, opts.ma); for (auto w : watched_races) { w.second->fk = opts.fk; w.second->mk = opts.mk; @@ -689,7 +691,6 @@ static void autobutcher_target(color_ostream &out, const autobutcher_options &op int id = race_to_id[*race]; WatchedRace *w; if (!watched_races.count(id)) { - DEBUG(status,out).print("adding new targets for %s\n", race->c_str()); w = new WatchedRace(out, id, true, opts.fk, opts.mk, opts.fa, opts.ma); watched_races.emplace(id, w); } else { @@ -707,8 +708,6 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o unordered_set ids; if (opts.races_all) { - DEBUG(status,out).print("modifying all races on watchlist: %s\n", - opts.command.c_str()); for (auto w : watched_races) ids.emplace(w.first); } @@ -722,13 +721,41 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o } for (int id : ids) { - if (opts.command == "watch") - watched_races[id]->isWatched = true; - else if (opts.command == "unwatch") - watched_races[id]->isWatched = false; + if (opts.command == "watch") { + if (!watched_races.count(id)) { + watched_races.emplace(id, + new WatchedRace(out, id, true, + get_config_val(CONFIG_DEFAULT_FK), + get_config_val(CONFIG_DEFAULT_MK), + get_config_val(CONFIG_DEFAULT_FA), + get_config_val(CONFIG_DEFAULT_MA))); + } + else if (!watched_races[id]->isWatched) { + DEBUG(status,out).print("watching: %s\n", opts.command.c_str()); + watched_races[id]->isWatched = true; + } + } + else if (opts.command == "unwatch") { + if (!watched_races.count(id)) { + watched_races.emplace(id, + new WatchedRace(out, id, false, + get_config_val(CONFIG_DEFAULT_FK), + get_config_val(CONFIG_DEFAULT_MK), + get_config_val(CONFIG_DEFAULT_FA), + get_config_val(CONFIG_DEFAULT_MA))); + } + else if (watched_races[id]->isWatched) { + DEBUG(status,out).print("unwatching: %s\n", opts.command.c_str()); + watched_races[id]->isWatched = false; + } + } else if (opts.command == "forget") { - watched_races[id]->RemoveConfig(out); - watched_races.erase(id); + if (watched_races.count(id)) { + DEBUG(status,out).print("forgetting: %s\n", opts.command.c_str()); + watched_races[id]->RemoveConfig(out); + delete watched_races[id]; + watched_races.erase(id); + } continue; } watched_races[id]->UpdateConfig(out); @@ -776,7 +803,10 @@ static bool isInBuiltCageRoom(df::unit *unit) { } static void autobutcher_cycle(color_ostream &out) { - DEBUG(cycle,out).print("running autobutcher_cycle\n"); + // mark that we have recently run + cycle_timestamp = world->frame_counter; + + DEBUG(cycle,out).print("running autobutcher cycle\n"); // check if there is anything to watch before walking through units vector if (!get_config_bool(CONFIG_AUTOWATCH)) { @@ -850,14 +880,13 @@ static void autobutcher_cycle(color_ostream &out) { } } - int slaughter_count = 0; for (auto w : watched_races) { - int slaughter_subcount = w.second->ProcessUnits(); - slaughter_count += slaughter_subcount; - if (slaughter_subcount) { + int slaughter_count = w.second->ProcessUnits(); + if (slaughter_count) { stringstream ss; - ss << slaughter_subcount; + ss << slaughter_count; string announce = Units::getRaceNamePluralById(w.first) + " marked for slaughter: " + ss.str(); + DEBUG(cycle,out).print("%s\n", announce.c_str()); Gui::showAnnouncement(announce, 2, false); } } diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index 10c0e3cd8..668938c2c 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -73,7 +73,7 @@ static bool set_config_val(int index, int value) { } static bool did_complain = false; // avoids message spam -static size_t cycle_counter = 0; // how many ticks since the last cycle +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle struct autonestbox_options { // whether to display help @@ -168,8 +168,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (is_enabled && ++cycle_counter >= - (size_t)get_config_val(CONFIG_CYCLE_TICKS)) + if (is_enabled && world->frame_counter - cycle_timestamp >= get_config_val(CONFIG_CYCLE_TICKS)) autonestbox_cycle(out); return CR_OK; } @@ -342,7 +341,7 @@ static bool assignUnitToZone(color_ostream &out, df::unit *unit, df::building *b df::general_ref_building_civzone_assignedst *ref = createCivzoneRef(); if (!ref) { ERR(cycle,out).print("Could not find a clonable activity zone reference!" - " You need to pen/pasture/pit at least one creature" + " You need to manually pen/pasture/pit at least one creature" " before autonestbox can function.\n"); return false; } @@ -381,6 +380,8 @@ static size_t assign_nestboxes(color_ostream &out) { DEBUG(cycle,out).print("Failed to assign unit to building.\n"); return processed; } + DEBUG(cycle,out).print("assigned unit %d to zone %d\n", + free_unit->id, free_building->id); ++processed; } } while (free_unit && free_building); @@ -406,13 +407,16 @@ static size_t assign_nestboxes(color_ostream &out) { static void autonestbox_cycle(color_ostream &out) { // mark that we have recently run - cycle_counter = 0; + cycle_timestamp = world->frame_counter; + + DEBUG(cycle,out).print("running autonestbox cycle\n"); size_t processed = assign_nestboxes(out); if (processed > 0) { stringstream ss; ss << processed << " nestboxes were assigned."; string announce = ss.str(); + DEBUG(cycle,out).print("%s\n", announce.c_str()); Gui::showAnnouncement(announce, 2, false); out << announce << endl; // can complain again From 1dec977476c606ed048289f1d7e8f851d6a16795 Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 2 Aug 2022 11:07:36 -0700 Subject: [PATCH 09/10] clean up and add logging to state persistence --- plugins/autonestbox.cpp | 43 +++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index 668938c2c..ea8d7a50d 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -65,11 +65,15 @@ static int get_config_val(int index) { return -1; return config.ival(index); } -static bool set_config_val(int index, int value) { - if (!config.isValid()) - return false; - config.ival(index) = value; - return true; +static bool get_config_bool(int index) { + return get_config_val(index) == 1; +} +static void set_config_val(int index, int value) { + if (config.isValid()) + config.ival(index) = value; +} +static void set_config_bool(int index, bool value) { + set_config_val(index, value ? 1 : 0); } static bool did_complain = false; // avoids message spam @@ -101,23 +105,24 @@ static void autonestbox_cycle(color_ostream &out); static void init_autonestbox(color_ostream &out) { config = World::GetPersistentData(CONFIG_KEY); - if (!config.isValid()) + if (!config.isValid()) { + DEBUG(status,out).print("no config found in this save; initializing\n"); config = World::AddPersistentData(CONFIG_KEY); - - if (get_config_val(CONFIG_IS_ENABLED) == -1) { - set_config_val(CONFIG_IS_ENABLED, 0); + set_config_bool(CONFIG_IS_ENABLED, false); set_config_val(CONFIG_CYCLE_TICKS, 6000); } - if (is_enabled) - set_config_val(CONFIG_IS_ENABLED, 1); - else - is_enabled = (get_config_val(CONFIG_IS_ENABLED) == 1); + is_enabled = get_config_bool(CONFIG_IS_ENABLED); + DEBUG(status,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); did_complain = false; } static void cleanup_autonestbox(color_ostream &out) { - is_enabled = false; + if (is_enabled) { + DEBUG(status,out).print("disabling (not persisting)\n"); + is_enabled = false; + } } DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { @@ -140,10 +145,10 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable != is_enabled) { is_enabled = enable; - if (is_enabled) - init_autonestbox(out); - else - cleanup_autonestbox(out); + DEBUG(status,out).print("%s from the API, persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); + init_autonestbox(out); } return CR_OK; } @@ -217,7 +222,7 @@ static command_result df_autonestbox(color_ostream &out, vector ¶met autonestbox_cycle(out); } else { - out << "autonestbox is " << (is_enabled ? "" : "not ") << "running\n"; + out << "autonestbox is " << (is_enabled ? "" : "not ") << "running" << endl; } return CR_OK; } From 1695919411635922a2aece78c8134bc91d7db6c3 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 3 Aug 2022 22:40:55 -0700 Subject: [PATCH 10/10] apply canonical class 3 plugin structure --- plugins/autobutcher.cpp | 301 +++++++++++++++++++++------------------- plugins/autonestbox.cpp | 134 +++++++++--------- 2 files changed, 221 insertions(+), 214 deletions(-) diff --git a/plugins/autobutcher.cpp b/plugins/autobutcher.cpp index 0b0d4ffdb..ee93d30e5 100644 --- a/plugins/autobutcher.cpp +++ b/plugins/autobutcher.cpp @@ -1,16 +1,19 @@ -// - full automation of marking live-stock for slaughtering -// races can be added to a watchlist and it can be set how many male/female kids/adults are left alive -// adding to the watchlist can be automated as well. -// config for autobutcher (state and sleep setting) is saved the first time autobutcher is started -// config for watchlist entries is saved when they are created or modified +// full automation of marking live-stock for slaughtering +// races can be added to a watchlist and it can be set how many male/female kids/adults are left alive +// adding to the watchlist can be automated as well. +// config for autobutcher (state and sleep setting) is saved the first time autobutcher is started +// config for watchlist entries is saved when they are created or modified +#include #include #include +#include #include "df/building_cagest.h" #include "df/creature_raw.h" #include "df/world.h" +#include "Core.h" #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -33,6 +36,56 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(world); +// logging levels can be dynamically controlled with the `debugfilter` command. +namespace DFHack { + // for configuration-related logging + DBG_DECLARE(autobutcher, status, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(autobutcher, cycle, DebugCategory::LINFO); +} + +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static const string WATCHLIST_CONFIG_KEY_PREFIX = string(plugin_name) + "/watchlist/"; +static PersistentDataItem config; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, + CONFIG_CYCLE_TICKS = 1, + CONFIG_AUTOWATCH = 2, + CONFIG_DEFAULT_FK = 3, + CONFIG_DEFAULT_MK = 4, + CONFIG_DEFAULT_FA = 5, + CONFIG_DEFAULT_MA = 6, +}; +static int get_config_val(int index) { + if (!config.isValid()) + return -1; + return config.ival(index); +} +static bool get_config_bool(int index) { + return get_config_val(index) == 1; +} +static void set_config_val(int index, int value) { + if (config.isValid()) + config.ival(index) = value; +} +static void set_config_bool(int index, bool value) { + set_config_val(index, value ? 1 : 0); +} + +struct WatchedRace; +// vector of races handled by autobutcher +// the name is a bit misleading since entries can be set to 'unwatched' +// to ignore them for a while but still keep the target count settings +static unordered_map watched_races; +static unordered_map race_to_id; +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static void init_autobutcher(color_ostream &out); +static void cleanup_autobutcher(color_ostream &out); +static command_result df_autobutcher(color_ostream &out, vector ¶meters); +static void autobutcher_cycle(color_ostream &out); + const string autobutcher_help = "Automatically butcher excess livestock. This plugin monitors how many pets\n" "you have of each gender and age and assigns excess lifestock for slaughter\n" @@ -115,44 +168,90 @@ const string autobutcher_help = " and llamas (for wool), and pigs (for milk and meat). All other unnamed tame units will be marked\n" " for slaughter as soon as they arrive in your fortress.\n"; -namespace DFHack { - DBG_DECLARE(autobutcher, status); - DBG_DECLARE(autobutcher, cycle); +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + commands.push_back(PluginCommand( + plugin_name, + "Automatically butcher excess livestock.", + df_autobutcher, + false, + autobutcher_help.c_str())); + return CR_OK; } -static const string CONFIG_KEY = "autobutcher/config"; -static const string WATCHLIST_CONFIG_KEY_PREFIX = "autobutcher/watchlist/"; +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; + } -static PersistentDataItem config; -enum ConfigValues { - CONFIG_IS_ENABLED = 0, - CONFIG_CYCLE_TICKS = 1, - CONFIG_AUTOWATCH = 2, - CONFIG_DEFAULT_FK = 3, - CONFIG_DEFAULT_MK = 4, - CONFIG_DEFAULT_FA = 5, - CONFIG_DEFAULT_MA = 6, -}; -static int get_config_val(int index) { - if (!config.isValid()) - return -1; - return config.ival(index); + if (enable != is_enabled) { + is_enabled = enable; + DEBUG(status,out).print("%s from the API; persisting\n", + is_enabled ? "enabled" : "disabled"); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); + } else { + DEBUG(status,out).print("%s from the API, but already %s; no action\n", + is_enabled ? "enabled" : "disabled", + is_enabled ? "enabled" : "disabled"); + } + return CR_OK; } -static bool get_config_bool(int index) { - return get_config_val(index) == 1; + +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(status,out).print("shutting down %s\n", plugin_name); + cleanup_autobutcher(out); + return CR_OK; } -static void set_config_val(int index, int value) { - if (config.isValid()) - config.ival(index) = value; + +DFhackCExport command_result plugin_load_data (color_ostream &out) { + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(status,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); + set_config_val(CONFIG_CYCLE_TICKS, 6000); + set_config_bool(CONFIG_AUTOWATCH, false); + set_config_val(CONFIG_DEFAULT_FK, 5); + set_config_val(CONFIG_DEFAULT_MK, 1); + set_config_val(CONFIG_DEFAULT_FA, 5); + set_config_val(CONFIG_DEFAULT_MA, 1); + } + + // we have to copy our enabled flag into the global plugin variable, but + // all the other state we can directly read/modify from the persistent + // data structure. + is_enabled = get_config_bool(CONFIG_IS_ENABLED); + DEBUG(status,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); + + // load the persisted watchlist + init_autobutcher(out); + + return CR_OK; } -static void set_config_bool(int index, bool value) { - set_config_val(index, value ? 1 : 0); + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(status,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + } + cleanup_autobutcher(out); + } + return CR_OK; } -static unordered_map race_to_id; -static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= get_config_val(CONFIG_CYCLE_TICKS)) + autobutcher_cycle(out); + return CR_OK; +} -static size_t DEFAULT_CYCLE_TICKS = 6000; +///////////////////////////////////////////////////// +// autobutcher config logic +// struct autobutcher_options { // whether to display help @@ -199,68 +298,30 @@ static const struct_field_info autobutcher_options_fields[] = { }; struct_identity autobutcher_options::_identity(sizeof(autobutcher_options), &df::allocator_fn, NULL, "autobutcher_options", NULL, autobutcher_options_fields); -static void init_autobutcher(color_ostream &out); -static void cleanup_autobutcher(color_ostream &out); -static command_result df_autobutcher(color_ostream &out, vector ¶meters); -static void autobutcher_cycle(color_ostream &out); - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - commands.push_back(PluginCommand( - "autobutcher", - "Automatically butcher excess livestock.", - df_autobutcher, - false, - autobutcher_help.c_str())); - - init_autobutcher(out); - return CR_OK; -} - -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (!Maps::IsValid()) { - out.printerr("Cannot run autobutcher without a loaded map.\n"); - return CR_FAILURE; - } +static bool get_options(color_ostream &out, + autobutcher_options &opts, + const vector ¶meters) +{ + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); - if (enable != is_enabled) { - is_enabled = enable; - if (is_enabled) - init_autobutcher(out); - else - cleanup_autobutcher(out); + if (!lua_checkstack(L, parameters.size() + 2) || + !Lua::PushModulePublic( + out, L, "plugins.autobutcher", "parse_commandline")) { + out.printerr("Failed to load autobutcher Lua code\n"); + return false; } - return CR_OK; -} -DFhackCExport command_result plugin_shutdown (color_ostream &out) { - cleanup_autobutcher(out); - return CR_OK; -} + Lua::Push(L, &opts); + for (const string ¶m : parameters) + Lua::Push(L, param); -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - switch (event) { - case DFHack::SC_MAP_LOADED: - init_autobutcher(out); - break; - case DFHack::SC_MAP_UNLOADED: - cleanup_autobutcher(out); - break; - default: - break; - } - return CR_OK; -} + if (!Lua::SafeCall(out, L, parameters.size() + 1, 0)) + return false; -DFhackCExport command_result plugin_onupdate(color_ostream &out) { - if (is_enabled && world->frame_counter - cycle_timestamp >= get_config_val(CONFIG_CYCLE_TICKS)) - autobutcher_cycle(out); - return CR_OK; + return true; } -///////////////////////////////////////////////////// -// autobutcher config logic -// - static void doMarkForSlaughter(df::unit *unit) { unit->flags2.bits.slaughter = 1; } @@ -460,35 +521,7 @@ public: } }; -// vector of races handled by autobutcher -// the name is a bit misleading since entries can be set to 'unwatched' -// to ignore them for a while but still keep the target count settings -static unordered_map watched_races; - static void init_autobutcher(color_ostream &out) { - config = World::GetPersistentData(CONFIG_KEY); - - if (!config.isValid()) - config = World::AddPersistentData(CONFIG_KEY); - - if (get_config_val(CONFIG_IS_ENABLED) == -1) { - set_config_bool(CONFIG_IS_ENABLED, false); - set_config_val(CONFIG_CYCLE_TICKS, DEFAULT_CYCLE_TICKS); - set_config_bool(CONFIG_AUTOWATCH, false); - set_config_val(CONFIG_DEFAULT_FK, 5); - set_config_val(CONFIG_DEFAULT_MK, 1); - set_config_val(CONFIG_DEFAULT_FA, 5); - set_config_val(CONFIG_DEFAULT_MA, 1); - } - - if (is_enabled) - set_config_bool(CONFIG_IS_ENABLED, true); - else - is_enabled = (get_config_val(CONFIG_IS_ENABLED) == 1); - - if (!config.isValid()) - return; - if (!race_to_id.size()) { const size_t num_races = world->raws.creatures.all.size(); for(size_t i = 0; i < num_races; ++i) @@ -505,37 +538,13 @@ static void init_autobutcher(color_ostream &out) { } static void cleanup_autobutcher(color_ostream &out) { - is_enabled = false; + DEBUG(status,out).print("cleaning %s state\n", plugin_name); race_to_id.clear(); for (auto w : watched_races) delete w.second; watched_races.clear(); } -static bool get_options(color_ostream &out, - autobutcher_options &opts, - const vector ¶meters) -{ - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, parameters.size() + 2) || - !Lua::PushModulePublic( - out, L, "plugins.autobutcher", "parse_commandline")) { - out.printerr("Failed to load autobutcher Lua code\n"); - return false; - } - - Lua::Push(L, &opts); - for (const string ¶m : parameters) - Lua::Push(L, param); - - if (!Lua::SafeCall(out, L, parameters.size() + 1, 0)) - return false; - - return true; -} - static void autobutcher_export(color_ostream &out); static void autobutcher_status(color_ostream &out); static void autobutcher_target(color_ostream &out, const autobutcher_options &opts); @@ -544,8 +553,8 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o static command_result df_autobutcher(color_ostream &out, vector ¶meters) { CoreSuspender suspend; - if (!Maps::IsValid()) { - out.printerr("Cannot run autobutcher without a loaded map.\n"); + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); return CR_FAILURE; } @@ -763,7 +772,7 @@ static void autobutcher_modify_watchlist(color_ostream &out, const autobutcher_o } ///////////////////////////////////////////////////// -// autobutcher cycle logic +// cycle logic // // check if contained in item (e.g. animals in cages) @@ -806,7 +815,7 @@ static void autobutcher_cycle(color_ostream &out) { // mark that we have recently run cycle_timestamp = world->frame_counter; - DEBUG(cycle,out).print("running autobutcher cycle\n"); + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); // check if there is anything to watch before walking through units vector if (!get_config_bool(CONFIG_AUTOWATCH)) { diff --git a/plugins/autonestbox.cpp b/plugins/autonestbox.cpp index ea8d7a50d..c47f4425f 100644 --- a/plugins/autonestbox.cpp +++ b/plugins/autonestbox.cpp @@ -4,6 +4,9 @@ // maybe check for minimum age? it's not that useful to fill nestboxes with freshly hatched birds // state and sleep setting is saved the first time autonestbox is started (to avoid writing stuff if the plugin is never used) +#include +#include + #include "df/building_cagest.h" #include "df/building_civzonest.h" #include "df/building_nest_boxst.h" @@ -16,7 +19,6 @@ #include "modules/Buildings.h" #include "modules/Gui.h" -#include "modules/Maps.h" #include "modules/Persistence.h" #include "modules/Units.h" #include "modules/World.h" @@ -50,11 +52,13 @@ static const string autonestbox_help = " The default is 6000 (about 8 days)\n"; namespace DFHack { - DBG_DECLARE(autonestbox, status); - DBG_DECLARE(autonestbox, cycle); + // for configuration-related logging + DBG_DECLARE(autonestbox, status, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(autonestbox, cycle, DebugCategory::LINFO); } -static const string CONFIG_KEY = "autonestbox/config"; +static const string CONFIG_KEY = string(plugin_name) + "/config"; static PersistentDataItem config; enum ConfigValues { CONFIG_IS_ENABLED = 0, @@ -79,95 +83,65 @@ static void set_config_bool(int index, bool value) { static bool did_complain = false; // avoids message spam static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle -struct autonestbox_options { - // whether to display help - bool help = false; - - // whether to run a cycle right now - bool now = false; - - // how many ticks to wait between automatic cycles, -1 means unset - int32_t ticks = -1; - - static struct_identity _identity; -}; -static const struct_field_info autonestbox_options_fields[] = { - { struct_field_info::PRIMITIVE, "help", offsetof(autonestbox_options, help), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "now", offsetof(autonestbox_options, now), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::PRIMITIVE, "ticks", offsetof(autonestbox_options, ticks), &df::identity_traits::identity, 0, 0 }, - { struct_field_info::END } -}; -struct_identity autonestbox_options::_identity(sizeof(autonestbox_options), &df::allocator_fn, NULL, "autonestbox_options", NULL, autonestbox_options_fields); - static command_result df_autonestbox(color_ostream &out, vector ¶meters); static void autonestbox_cycle(color_ostream &out); -static void init_autonestbox(color_ostream &out) { - config = World::GetPersistentData(CONFIG_KEY); - - if (!config.isValid()) { - DEBUG(status,out).print("no config found in this save; initializing\n"); - config = World::AddPersistentData(CONFIG_KEY); - set_config_bool(CONFIG_IS_ENABLED, false); - set_config_val(CONFIG_CYCLE_TICKS, 6000); - } - - is_enabled = get_config_bool(CONFIG_IS_ENABLED); - DEBUG(status,out).print("loading persisted enabled state: %s\n", - is_enabled ? "true" : "false"); - did_complain = false; -} - -static void cleanup_autonestbox(color_ostream &out) { - if (is_enabled) { - DEBUG(status,out).print("disabling (not persisting)\n"); - is_enabled = false; - } -} - DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand( - "autonestbox", + plugin_name, "Auto-assign egg-laying female pets to nestbox zones.", df_autonestbox, false, autonestbox_help.c_str())); - - init_autonestbox(out); return CR_OK; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - if (!Maps::IsValid()) { - out.printerr("Cannot run autonestbox without a loaded map.\n"); + 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(status,out).print("%s from the API, persisting\n", + DEBUG(status,out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled"); set_config_bool(CONFIG_IS_ENABLED, is_enabled); - init_autonestbox(out); + } else { + DEBUG(status,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) { - cleanup_autonestbox(out); +DFhackCExport command_result plugin_load_data (color_ostream &out) { + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(status,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(CONFIG_IS_ENABLED, is_enabled); + set_config_val(CONFIG_CYCLE_TICKS, 6000); + } + + // we have to copy our enabled flag into the global plugin variable, but + // all the other state we can directly read/modify from the persistent + // data structure. + is_enabled = get_config_bool(CONFIG_IS_ENABLED); + DEBUG(status,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); + did_complain = false; return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - switch (event) { - case DFHack::SC_MAP_LOADED: - init_autonestbox(out); - break; - case DFHack::SC_MAP_UNLOADED: - cleanup_autonestbox(out); - break; - default: - break; + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(status,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; + } } return CR_OK; } @@ -178,6 +152,30 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { return CR_OK; } +///////////////////////////////////////////////////// +// configuration interface +// + +struct autonestbox_options { + // whether to display help + bool help = false; + + // whether to run a cycle right now + bool now = false; + + // how many ticks to wait between automatic cycles, -1 means unset + int32_t ticks = -1; + + static struct_identity _identity; +}; +static const struct_field_info autonestbox_options_fields[] = { + { struct_field_info::PRIMITIVE, "help", offsetof(autonestbox_options, help), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "now", offsetof(autonestbox_options, now), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "ticks", offsetof(autonestbox_options, ticks), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::END } +}; +struct_identity autonestbox_options::_identity(sizeof(autonestbox_options), &df::allocator_fn, NULL, "autonestbox_options", NULL, autonestbox_options_fields); + static bool get_options(color_ostream &out, autonestbox_options &opts, const vector ¶meters) @@ -205,8 +203,8 @@ static bool get_options(color_ostream &out, static command_result df_autonestbox(color_ostream &out, vector ¶meters) { CoreSuspender suspend; - if (!Maps::IsValid()) { - out.printerr("Cannot run autonestbox without a loaded map.\n"); + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); return CR_FAILURE; } @@ -228,7 +226,7 @@ static command_result df_autonestbox(color_ostream &out, vector ¶met } ///////////////////////////////////////////////////// -// autonestbox cycle logic +// cycle logic // static bool isEmptyPasture(df::building *building) {