update tailor, persist state, use best practices

develop
Myk Taylor 2023-02-06 18:38:16 -08:00
parent 5113823d8c
commit 5c84d18001
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
4 changed files with 350 additions and 263 deletions

@ -5,16 +5,15 @@ tailor
:summary: Automatically keep your dwarves in fresh clothing.
:tags: fort auto workorders
Whenever the bookkeeper updates stockpile records, this plugin will scan the
fort. If there are fresh cloths available, dwarves who are wearing tattered
clothing will have their rags confiscated (in the same manner as the
`cleanowned` tool) so that they'll reequip with replacement clothes.
Once a day, this plugin will scan the clothing situation in the fort. If there
are fresh cloths available, dwarves who are wearing tattered clothing will have
their rags confiscated (in the same manner as the `cleanowned` tool) so that
they'll reequip with replacement clothes.
If there are not enough clothes available, manager orders will be generated
to manufacture some more. ``tailor`` will intelligently create orders using
raw materials that you have on hand in the fort. For example, if you have
lots of silk, but no cloth, then ``tailor`` will order only silk clothing to
be made.
If there are not enough clothes available, manager orders will be generated to
manufacture some more. ``tailor`` will intelligently create orders using raw
materials that you have on hand in the fort. For example, if you have lots of
silk, but no cloth, then ``tailor`` will order only silk clothing to be made.
Usage
-----
@ -22,7 +21,8 @@ Usage
::
enable tailor
tailor status
tailor [status]
tailor now
tailor materials <material> [<material> ...]
By default, ``tailor`` will prefer using materials in this order::
@ -32,12 +32,16 @@ By default, ``tailor`` will prefer using materials in this order::
but you can use the ``tailor materials`` command to restrict which materials
are used, and in what order.
Example
-------
Examples
--------
``enable tailor``
Start replacing tattered clothes with default settings.
``tailor now``
Run a scan and order cycle right now, regardless of whether the plugin is
enabled.
``tailor materials silk cloth yarn``
Restrict the materials used for automatically manufacturing clothing to
silk, cloth, and yarn, preferred in that order. This saves leather for

@ -159,7 +159,7 @@ dfhack_plugin(showmood showmood.cpp)
#add_subdirectory(stockpiles)
#dfhack_plugin(stocks stocks.cpp)
#dfhack_plugin(strangemood strangemood.cpp)
dfhack_plugin(tailor tailor.cpp)
dfhack_plugin(tailor tailor.cpp LINK_LIBRARIES lua)
dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua)
#dfhack_plugin(title-folder title-folder.cpp)
#dfhack_plugin(title-version title-version.cpp)

@ -0,0 +1,56 @@
local _ENV = mkmodule('plugins.tailor')
local argparse = require('argparse')
local utils = require('utils')
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 status()
print(('tailor is %s'):format(enabled and "enabled" or "disabled"))
print('materials preference order:')
for _,name in ipairs(tailor_getMaterialPreferences()) do
print((' %s'):format(name))
end
end
function setMaterials(names)
local idxs = utils.invert(names)
tailor_setMaterialPreferences(
idxs.silk or -1,
idxs.cloth or -1,
idxs.yarn or -1,
idxs.leather or -1)
end
function parse_commandline(...)
local args, opts = {...}, {}
local positionals = process_args(opts, args)
if opts.help then
return false
end
local command = table.remove(positionals, 1)
if not command or command == 'status' then
status()
elseif command == 'now' then
tailor_doCycle()
elseif command == 'materials' then
setMaterials(positionals)
else
return false
end
return true
end
return _ENV

@ -1,136 +1,161 @@
/*
* Tailor plugin. Automatically manages keeping your dorfs clothed.
* For best effect, place "tailor enable" in your dfhack.init configuration,
* or set AUTOENABLE to true.
*/
#include "Core.h"
#include "DataDefs.h"
#include "Debug.h"
#include "PluginManager.h"
#include <string>
#include <unordered_map>
#include <vector>
#include "df/creature_raw.h"
#include "df/global_objects.h"
#include "df/historical_entity.h"
#include "df/item.h"
#include "df/item_flags.h"
#include "df/itemdef_armorst.h"
#include "df/itemdef_glovesst.h"
#include "df/itemdef_helmst.h"
#include "df/itemdef_pantsst.h"
#include "df/itemdef_shoesst.h"
#include "df/items_other_id.h"
#include "df/job.h"
#include "df/job_type.h"
#include "df/manager_order.h"
#include "df/plotinfost.h"
#include "df/world.h"
#include "modules/Maps.h"
#include "modules/Units.h"
#include "Core.h"
#include "Debug.h"
#include "LuaTools.h"
#include "PluginManager.h"
#include "modules/Materials.h"
#include "modules/Persistence.h"
#include "modules/Translation.h"
#include "modules/Units.h"
#include "modules/World.h"
using namespace DFHack;
using std::string;
using std::vector;
using df::global::world;
using df::global::plotinfo;
using namespace DFHack;
DFHACK_PLUGIN("tailor");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
#define AUTOENABLE false
DFHACK_PLUGIN_IS_ENABLED(enabled);
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(plotinfo);
REQUIRE_GLOBAL(standing_orders_use_dyed_cloth);
REQUIRE_GLOBAL(world);
namespace DFHack {
DBG_DECLARE(tailor, cycle, DebugCategory::LINFO);
DBG_DECLARE(tailor, config, DebugCategory::LINFO);
}
class Tailor {
// ARMOR, SHOES, HELM, GLOVES, PANTS
static const string CONFIG_KEY = string(plugin_name) + "/config";
static PersistentDataItem config;
// ah, if only STL had a bimap
enum ConfigValues {
CONFIG_IS_ENABLED = 0,
CONFIG_SILK_IDX = 1,
CONFIG_CLOTH_IDX = 2,
CONFIG_YARN_IDX = 3,
CONFIG_LEATHER_IDX = 4,
};
private:
static int get_config_val(PersistentDataItem &c, int index) {
if (!c.isValid())
return -1;
return c.ival(index);
}
static bool get_config_bool(PersistentDataItem &c, int index) {
return get_config_val(c, index) == 1;
}
static void set_config_val(PersistentDataItem &c, int index, int value) {
if (c.isValid())
c.ival(index) = value;
}
static void set_config_bool(PersistentDataItem &c, int index, bool value) {
set_config_val(c, index, value ? 1 : 0);
}
const std::map<df::job_type, df::item_type> jobTypeMap = {
{ df::job_type::MakeArmor, df::item_type::ARMOR },
{ df::job_type::MakePants, df::item_type::PANTS },
{ df::job_type::MakeHelm, df::item_type::HELM },
{ df::job_type::MakeGloves, df::item_type::GLOVES },
{ df::job_type::MakeShoes, df::item_type::SHOES }
};
const std::map<df::item_type, df::job_type> itemTypeMap = {
{ df::item_type::ARMOR, df::job_type::MakeArmor },
{ df::item_type::PANTS, df::job_type::MakePants },
{ df::item_type::HELM, df::job_type::MakeHelm },
{ df::item_type::GLOVES, df::job_type::MakeGloves },
{ df::item_type::SHOES, df::job_type::MakeShoes }
};
#define F(x) df::item_flags::mask_##x
const df::item_flags bad_flags = {
(
F(dump) | F(forbid) | F(garbage_collect) |
F(hostile) | F(on_fire) | F(rotten) | F(trader) |
F(in_building) | F(construction) | F(owned)
)
#undef F
};
class MatType {
public:
std::string name;
df::job_material_category job_material;
df::armor_general_flags armor_flag;
bool operator==(const MatType& m) const
{
return name == m.name;
}
static const int32_t CYCLE_TICKS = 1200; // one day
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
// operator< is required to use this as a std::map key
bool operator<(const MatType& m) const
{
return name < m.name;
}
// ah, if only STL had a bimap
static const std::map<df::job_type, df::item_type> jobTypeMap = {
{ df::job_type::MakeArmor, df::item_type::ARMOR },
{ df::job_type::MakePants, df::item_type::PANTS },
{ df::job_type::MakeHelm, df::item_type::HELM },
{ df::job_type::MakeGloves, df::item_type::GLOVES },
{ df::job_type::MakeShoes, df::item_type::SHOES }
};
MatType(std::string& n, df::job_material_category jm, df::armor_general_flags af)
: name(n), job_material(jm), armor_flag(af) {};
MatType(const char* n, df::job_material_category jm, df::armor_general_flags af)
: name(std::string(n)), job_material(jm), armor_flag(af) {};
static const std::map<df::item_type, df::job_type> itemTypeMap = {
{ df::item_type::ARMOR, df::job_type::MakeArmor },
{ df::item_type::PANTS, df::job_type::MakePants },
{ df::item_type::HELM, df::job_type::MakeHelm },
{ df::item_type::GLOVES, df::job_type::MakeGloves },
{ df::item_type::SHOES, df::job_type::MakeShoes }
};
class MatType {
public:
const std::string name;
const df::job_material_category job_material;
const df::armor_general_flags armor_flag;
bool operator==(const MatType& m) const {
return name == m.name;
}
};
// operator< is required to use this as a std::map key
bool operator<(const MatType& m) const {
return name < m.name;
}
const MatType
M_SILK = MatType("silk", df::job_material_category::mask_silk, df::armor_general_flags::SOFT),
M_CLOTH = MatType("cloth", df::job_material_category::mask_cloth, df::armor_general_flags::SOFT),
M_YARN = MatType("yarn", df::job_material_category::mask_yarn, df::armor_general_flags::SOFT),
M_LEATHER = MatType("leather", df::job_material_category::mask_leather, df::armor_general_flags::LEATHER);
MatType(std::string& n, df::job_material_category jm, df::armor_general_flags af)
: name(n), job_material(jm), armor_flag(af) {};
MatType(const char* n, df::job_material_category jm, df::armor_general_flags af)
: name(std::string(n)), job_material(jm), armor_flag(af) {};
};
std::list<MatType> all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER };
static const MatType
M_SILK = MatType("silk", df::job_material_category::mask_silk, df::armor_general_flags::SOFT),
M_CLOTH = MatType("cloth", df::job_material_category::mask_cloth, df::armor_general_flags::SOFT),
M_YARN = MatType("yarn", df::job_material_category::mask_yarn, df::armor_general_flags::SOFT),
M_LEATHER = MatType("leather", df::job_material_category::mask_leather, df::armor_general_flags::LEATHER);
static const std::list<MatType> all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER };
static std::list<MatType> material_order = all_materials;
static struct BadFlags {
uint32_t whole;
BadFlags() {
df::item_flags flags;
#define F(x) flags.bits.x = true;
F(dump); F(forbid); F(garbage_collect);
F(hostile); F(on_fire); F(rotten); F(trader);
F(in_building); F(construction); F(owned);
F(in_chest); F(removed); F(encased);
F(spider_web);
#undef F
whole = flags.whole;
}
} badFlags;
class Tailor {
private:
std::map<std::pair<df::item_type, int>, int> available; // key is item type & size
std::map<std::pair<df::item_type, int>, int> needed; // same
std::map<std::pair<df::item_type, int>, int> queued; // same
std::map<int, int> sizes; // this maps body size to races
std::map<std::tuple<df::job_type, int, int>, int> orders; // key is item type, item subtype, size
std::map<MatType, int> supply;
color_ostream* out;
std::list<MatType> material_order = { M_SILK, M_CLOTH, M_YARN, M_LEATHER };
std::map<MatType, int> reserves;
int default_reserve = 10;
public:
void reset()
{
available.clear();
@ -145,9 +170,7 @@ private:
{
for (auto i : world->items.other[df::items_other_id::ANY_GENERIC37]) // GENERIC37 is "clothing"
{
if (i->flags.whole & bad_flags.whole)
continue;
if (i->flags.bits.owned)
if (i->flags.whole & badFlags.whole)
continue;
if (i->getWear() >= 1)
continue;
@ -164,7 +187,7 @@ private:
for (auto i : world->items.other[df::items_other_id::CLOTH])
{
if (i->flags.whole & bad_flags.whole)
if (i->flags.whole & badFlags.whole)
continue;
if (require_dyed && !i->hasImprovements())
@ -197,7 +220,7 @@ private:
for (auto i : world->items.other[df::items_other_id::SKIN_TANNED])
{
if (i->flags.whole & bad_flags.whole)
if (i->flags.whole & badFlags.whole)
continue;
supply[M_LEATHER] += i->getStackSize();
}
@ -369,8 +392,9 @@ private:
}
void place_orders()
int place_orders()
{
int ordered = 0;
auto entity = world->entities.all[plotinfo->civ_id];
for (auto& o : orders)
@ -477,6 +501,7 @@ private:
);
count -= c;
ordered += c;
}
else
{
@ -486,215 +511,217 @@ private:
}
}
}
return ordered;
}
};
public:
void do_scan(color_ostream& o)
{
out = &o;
reset();
// scan for useable clothing
static std::unique_ptr<Tailor> tailor_instance;
scan_clothing();
static command_result do_command(color_ostream &out, vector<string> &parameters);
static int do_cycle(color_ostream &out);
// scan for clothing raw materials
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
DEBUG(config,out).print("initializing %s\n", plugin_name);
scan_materials();
tailor_instance = dts::make_unique<Tailor>();
// scan for units who need replacement clothing
// provide a configuration interface for the plugin
commands.push_back(PluginCommand(
plugin_name,
"Automatically keep your dwarves in fresh clothing.",
do_command));
scan_replacements();
return CR_OK;
}
// create new orders
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;
}
create_orders();
if (enable != is_enabled) {
is_enabled = enable;
DEBUG(config,out).print("%s from the API; persisting\n",
is_enabled ? "enabled" : "disabled");
set_config_bool(config, CONFIG_IS_ENABLED, is_enabled);
if (enable)
do_cycle(out);
} else {
DEBUG(config,out).print("%s from the API, but already %s; no action\n",
is_enabled ? "enabled" : "disabled",
is_enabled ? "enabled" : "disabled");
}
return CR_OK;
}
// scan existing orders and subtract
DFhackCExport command_result plugin_shutdown (color_ostream &out) {
DEBUG(config,out).print("shutting down %s\n", plugin_name);
scan_existing_orders();
tailor_instance.release();
// place orders
return CR_OK;
}
place_orders();
static void set_material_order() {
material_order.clear();
for (int i = 0; i < all_materials.size(); ++i) {
if (i == get_config_val(config, CONFIG_SILK_IDX))
material_order.push_back(M_SILK);
else if (i == get_config_val(config, CONFIG_CLOTH_IDX))
material_order.push_back(M_CLOTH);
else if (i == get_config_val(config, CONFIG_YARN_IDX))
material_order.push_back(M_YARN);
else if (i == get_config_val(config, CONFIG_LEATHER_IDX))
material_order.push_back(M_LEATHER);
}
if (!material_order.size())
std::copy(all_materials.begin(), all_materials.end(), std::back_inserter(material_order));
}
public:
command_result set_materials(color_ostream& out, std::vector<std::string>& parameters)
{
std::list<MatType> newmat;
newmat.clear();
for (auto m = parameters.begin() + 1; m != parameters.end(); m++)
{
auto nameMatch = [m](MatType& m1) { return *m == m1.name; };
auto mm = std::find_if(all_materials.begin(), all_materials.end(), nameMatch);
if (mm == all_materials.end())
{
WARN(config,out).print("tailor: material %s not recognized\n", m->c_str());
return CR_WRONG_USAGE;
}
else {
newmat.push_back(*mm);
}
}
material_order = newmat;
INFO(config,out).print("tailor: material list set to %s\n", get_material_list().c_str());
DFhackCExport command_result plugin_load_data (color_ostream &out) {
cycle_timestamp = 0;
config = World::GetPersistentData(CONFIG_KEY);
return CR_OK;
if (!config.isValid()) {
DEBUG(config,out).print("no config found in this save; initializing\n");
config = World::AddPersistentData(CONFIG_KEY);
set_config_bool(config, CONFIG_IS_ENABLED, is_enabled);
}
public:
std::string get_material_list()
{
std::string s;
for (const auto& m : material_order)
{
if (!s.empty()) s += ", ";
s += m.name;
}
return s;
}
is_enabled = get_config_bool(config, CONFIG_IS_ENABLED);
DEBUG(config,out).print("loading persisted enabled state: %s\n",
is_enabled ? "true" : "false");
set_material_order();
public:
void process(color_ostream& out)
{
bool found = false;
return CR_OK;
}
for (df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next)
{
if (link->item == NULL) continue;
if (link->item->job_type == df::enums::job_type::UpdateStockpileRecords)
{
found = true;
break;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (event == DFHack::SC_WORLD_UNLOADED) {
if (is_enabled) {
DEBUG(config,out).print("world unloaded; disabling %s\n",
plugin_name);
is_enabled = false;
}
}
return CR_OK;
}
if (found)
{
do_scan(out);
}
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) {
int ordered = do_cycle(out);
if (0 < ordered)
out.print("tailor: ordered %d items of clothing\n", ordered);
}
};
return CR_OK;
}
static std::unique_ptr<Tailor> tailor_instance;
static bool call_tailor_lua(color_ostream *out, const char *fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
DEBUG(config).print("calling tailor lua function: '%s'\n", fn_name);
#define DELTA_TICKS 50
CoreSuspender guard;
DFhackCExport command_result plugin_onupdate(color_ostream& out)
{
if (!enabled || !tailor_instance)
return CR_OK;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!Maps::IsValid())
return CR_OK;
if (!out)
out = &Core::getInstance().getConsole();
if (DFHack::World::ReadPauseState())
return CR_OK;
return Lua::CallLuaModuleFunction(*out, L, "plugins.tailor", fn_name,
nargs, nres,
std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
static command_result do_command(color_ostream &out, vector<string> &parameters) {
CoreSuspender suspend;
{
CoreSuspender suspend;
tailor_instance->process(out);
if (!Core::getInstance().isWorldLoaded()) {
out.printerr("Cannot run %s without a loaded world.\n", plugin_name);
return CR_FAILURE;
}
return CR_OK;
bool show_help = false;
if (!call_tailor_lua(&out, "parse_commandline", parameters.size(), 1,
[&](lua_State *L) {
for (const string &param : parameters)
Lua::Push(L, param);
},
[&](lua_State *L) {
show_help = !lua_toboolean(L, -1);
})) {
return CR_FAILURE;
}
return show_help ? CR_WRONG_USAGE : CR_OK;
}
static command_result tailor_cmd(color_ostream& out, std::vector <std::string>& parameters) {
bool desired = enabled;
if (parameters.size() == 1 && (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1"))
{
desired = true;
}
else if (parameters.size() == 1 && (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0"))
{
desired = false;
}
else if (parameters.size() == 1 && (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?"))
{
return CR_WRONG_USAGE;
}
else if (parameters.size() == 1 && parameters[0] == "test")
{
if (tailor_instance)
{
tailor_instance->do_scan(out);
return CR_OK;
}
else
{
out.print("%s: not instantiated\n", plugin_name);
return CR_FAILURE;
}
}
else if (parameters.size() > 1 && parameters[0] == "materials")
{
if (tailor_instance)
{
return tailor_instance->set_materials(out, parameters);
}
else
{
out.print("%s: not instantiated\n", plugin_name);
return CR_FAILURE;
}
}
else if (parameters.size() == 1 && parameters[0] != "status")
{
return CR_WRONG_USAGE;
}
/////////////////////////////////////////////////////
// cycle logic
//
out.print("Tailor is %s %s.\n", (desired == enabled) ? "currently" : "now", desired ? "enabled" : "disabled");
if (tailor_instance)
{
out.print("Material list is: %s\n", tailor_instance->get_material_list().c_str());
}
else
{
out.print("%s: not instantiated\n", plugin_name);
}
static int do_cycle(color_ostream &out) {
// mark that we have recently run
cycle_timestamp = world->frame_counter;
enabled = desired;
DEBUG(cycle,out).print("running %s cycle\n", plugin_name);
return CR_OK;
tailor_instance->reset();
tailor_instance->scan_clothing();
tailor_instance->scan_materials();
tailor_instance->scan_replacements();
tailor_instance->create_orders();
tailor_instance->scan_existing_orders();
return tailor_instance->place_orders();
}
/////////////////////////////////////////////////////
// Lua API
//
DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event)
{
return CR_OK;
static void tailor_doCycle(color_ostream &out) {
DEBUG(config,out).print("entering tailor_doCycle\n");
out.print("ordered %d items of clothing\n", do_cycle(out));
}
DFhackCExport command_result plugin_enable(color_ostream& out, bool enable)
{
enabled = enable;
return CR_OK;
}
// remember, these are ONE-based indices from Lua
static void tailor_setMaterialPreferences(color_ostream &out, int32_t silkIdx,
int32_t clothIdx, int32_t yarnIdx, int32_t leatherIdx) {
DEBUG(config,out).print("entering tailor_setMaterialPreferences\n");
DFhackCExport command_result plugin_init(color_ostream& out, std::vector <PluginCommand>& commands)
{
tailor_instance = std::move(dts::make_unique<Tailor>());
// it doesn't really matter if these are invalid. set_material_order will do
// the right thing.
set_config_val(config, CONFIG_SILK_IDX, silkIdx);
set_config_val(config, CONFIG_CLOTH_IDX, clothIdx);
set_config_val(config, CONFIG_YARN_IDX, yarnIdx);
set_config_val(config, CONFIG_LEATHER_IDX, leatherIdx);
if (AUTOENABLE) {
enabled = true;
}
set_material_order();
}
commands.push_back(PluginCommand(
plugin_name,
"Automatically keep your dwarves in fresh clothing.",
tailor_cmd));
return CR_OK;
static int tailor_getMaterialPreferences(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
DEBUG(config,*out).print("entering tailor_getMaterialPreferences\n");
vector<string> names;
for (const auto& m : material_order)
names.emplace_back(m.name);
Lua::PushVector(L, names);
return 1;
}
DFhackCExport command_result plugin_shutdown(color_ostream& out)
{
tailor_instance.release();
DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(tailor_doCycle),
DFHACK_LUA_FUNCTION(tailor_setMaterialPreferences),
DFHACK_LUA_END
};
return plugin_enable(out, false);
}
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(tailor_getMaterialPreferences),
DFHACK_LUA_END
};