diff --git a/docs/changelog.txt b/docs/changelog.txt index 1174508e2..73ff17e4f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -34,6 +34,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future ## New Plugins +- `autoslab`: Automatically create work orders to engrave slabs for ghostly dwarves. ## Fixes -@ DF screens can no longer get "stuck" on transitions when DFHack tool windows are visible. Instead, those DF screens are force-paused while DFHack windows are visible so the player can close them first and not corrupt the screen sequence. The "force pause" indicator will appear on these DFHack windows to indicate what is happening. diff --git a/docs/plugins/autoslab.rst b/docs/plugins/autoslab.rst new file mode 100644 index 000000000..a947f7bbe --- /dev/null +++ b/docs/plugins/autoslab.rst @@ -0,0 +1,23 @@ +autoslab +======== + +.. dfhack-tool:: + :summary: Automatically engrave slabs for ghostly citizens. + :tags: fort auto workorders + :no-command: + +Automatically queue orders to engrave slabs of existing ghosts. Will only queue +an order if there is no existing slab with that unit's memorial engraved and +there is not already an existing work order to engrave a slab for that unit. +Make sure you have spare slabs on hand for engraving! If you run +`orders import library/rockstock `, you'll be sure to always have +some slabs in stock. + +Usage +----- + +``enable autoslab`` + Enables the plugin and starts checking for ghosts that need memorializing. + +``disable autoslab`` + Disables the plugin. diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 284d1788e..41b3ad542 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -134,6 +134,7 @@ dfhack_plugin(misery misery.cpp) #dfhack_plugin(mode mode.cpp) #dfhack_plugin(mousequery mousequery.cpp) dfhack_plugin(nestboxes nestboxes.cpp) +dfhack_plugin(autoslab autoslab.cpp) dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua) dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) diff --git a/plugins/autoslab.cpp b/plugins/autoslab.cpp new file mode 100644 index 000000000..51c92b0f7 --- /dev/null +++ b/plugins/autoslab.cpp @@ -0,0 +1,243 @@ +/* Simple plugin to check for ghosts and automatically queue jobs to engrave slabs for them. + * + * Enhancement idea: Queue up a ConstructSlab job, then link the engrave slab job to it. Avoids need to have slabs in stockpiles + * Would require argument parsing, specifying materials + * Enhancement idea: Automatically place the slab. This seems like a tricky problem but maybe solveable with named zones? + * Might be made obsolete by people just using buildingplan to pre-place plans for slab? + * Enhancement idea: Optionally enable autoengraving for pets. + * Enhancement idea: Try to get ahead of ghosts by autoengraving for dead dwarves with no remains, or dwarves + * whose remains are unreachable. + */ + +#include "Core.h" +#include "Debug.h" +#include "PluginManager.h" + +#include "modules/Persistence.h" +#include "modules/Translation.h" +#include "modules/World.h" + +#include "df/historical_figure.h" +#include "df/item.h" +#include "df/manager_order.h" +#include "df/plotinfost.h" +#include "df/unit.h" +#include "df/world.h" + +using namespace DFHack; + +DFHACK_PLUGIN("autoslab"); +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(autoslab, status, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(autoslab, cycle, DebugCategory::LINFO); +} + +static const auto CONFIG_KEY = std::string(plugin_name) + "/config"; +static PersistentDataItem config; +enum ConfigValues +{ + CONFIG_IS_ENABLED = 0, +}; +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 int32_t cycle_timestamp = 0; // world->frame_counter at last cycle + +static void do_cycle(color_ostream &out); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) +{ + DEBUG(status, out).print("initializing %s\n", plugin_name); + + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + if (!Core::getInstance().isWorldLoaded()) + { + out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + if (enable != is_enabled) + { + is_enabled = enable; + DEBUG(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; +} + +DFhackCExport command_result plugin_shutdown(color_ostream &out) +{ + DEBUG(status, out).print("shutting down %s\n", plugin_name); + + return CR_OK; +} + +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); + } + + // 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"); + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + if (event == DFHack::SC_WORLD_UNLOADED) + { + if (is_enabled) + { + DEBUG(status, out).print("world unloaded; disabling %s\n", plugin_name); + is_enabled = false; + } + } + return CR_OK; +} + +static const int32_t CYCLE_TICKS = 1200; + +DFhackCExport command_result plugin_onupdate(color_ostream &out) +{ + CoreSuspender suspend; + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) + do_cycle(out); + return CR_OK; +} + +// Name functions taken from manipulator.cpp +static std::string get_first_name(df::unit *unit) +{ + return Translation::capitalize(unit->name.first_name); +} + +static std::string get_last_name(df::unit *unit) +{ + df::language_name name = unit->name; + std::string ret = ""; + for (int i = 0; i < 2; i++) + { + if (name.words[i] >= 0) + ret += *world->raws.language.translations[name.language]->words[name.words[i]]; + } + return Translation::capitalize(ret); +} + +// Couldn't figure out any other way to do this besides look for the dwarf name in +// the slab item description. +// Ideally, we could get the historical figure id from the slab but I didn't +// see anything like that in the item struct. This seems to work based on testing. +// Confirmed nicknames don't show up in engraved slab names, so this should probably work okay +bool engravedSlabItemExists(df::unit *unit, std::vector slabs) +{ + for (auto slab : slabs) + { + std::string desc = ""; + slab->getItemDescription(&desc, 0); + auto fullName = get_first_name(unit) + " " + get_last_name(unit); + if (desc.find(fullName) != std::string::npos) + return true; + } + + return false; +} + +// Queue up a single order to engrave the slab for the given unit +static void createSlabJob(df::unit *unit) +{ + auto next_id = world->manager_order_next_id++; + auto order = new df::manager_order(); + + order->id = next_id; + order->job_type = df::job_type::EngraveSlab; + order->hist_figure_id = unit->hist_figure_id; + order->amount_left = 1; + order->amount_total = 1; + world->manager_orders.push_back(order); +} + +static void checkslabs(color_ostream &out) +{ + // Get existing orders for slab engraving as map hist_figure_id -> order ID + std::map histToJob; + for (auto order : world->manager_orders) + { + if (order->job_type == df::job_type::EngraveSlab) + histToJob[order->hist_figure_id] = order->id; + } + + // Get list of engraved slab items on map + std::vector engravedSlabs; + std::copy_if(world->items.all.begin(), world->items.all.end(), + std::back_inserter(engravedSlabs), + [](df::item *item) + { return item->getType() == df::item_type::SLAB && item->getSlabEngravingType() == df::slab_engraving_type::Memorial; }); + + // Build list of ghosts + std::vector ghosts; + std::copy_if(world->units.all.begin(), world->units.all.end(), + std::back_inserter(ghosts), + [](const df::unit *unit) + { return unit->flags3.bits.ghostly; }); + + for (auto ghost : ghosts) + { + // Only create a job is the map has no existing jobs for that historical figure or no existing engraved slabs + if (histToJob.count(ghost->hist_figure_id) == 0 && !engravedSlabItemExists(ghost, engravedSlabs)) + { + createSlabJob(ghost); + auto fullName = get_first_name(ghost) + " " + get_last_name(ghost); + out.print("Added slab order for ghost %s\n", fullName.c_str()); + } + } +} + +static void do_cycle(color_ostream &out) +{ + // mark that we have recently run + cycle_timestamp = world->frame_counter; + + checkslabs(out); +}