Merge pull request #2737 from johncosker/merge-autoslab
Implement autoslab engraving featuredevelop
commit
a0b3656c45
@ -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.
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue