Merge pull request #2737 from johncosker/merge-autoslab

Implement autoslab engraving feature
develop
Myk 2023-01-29 19:46:41 -08:00 committed by GitHub
commit a0b3656c45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 268 additions and 0 deletions

@ -34,6 +34,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
# Future # Future
## New Plugins ## New Plugins
- `autoslab`: Automatically create work orders to engrave slabs for ghostly dwarves.
## Fixes ## 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. -@ 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.

@ -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 <orders>`, 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.

@ -134,6 +134,7 @@ dfhack_plugin(misery misery.cpp)
#dfhack_plugin(mode mode.cpp) #dfhack_plugin(mode mode.cpp)
#dfhack_plugin(mousequery mousequery.cpp) #dfhack_plugin(mousequery mousequery.cpp)
dfhack_plugin(nestboxes nestboxes.cpp) dfhack_plugin(nestboxes nestboxes.cpp)
dfhack_plugin(autoslab autoslab.cpp)
dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua) dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static lua)
dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua)
dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua)

@ -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<PluginCommand> &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<df::item *> 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<int32_t, int32_t> 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<df::item *> 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<df::unit *> 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);
}