Add "tailor" plugin, to provide clothing management

develop
Kelly Kinkade 2018-08-25 11:59:28 -05:00
parent e56cb2a25a
commit d3b335105c
2 changed files with 524 additions and 0 deletions

@ -19,6 +19,7 @@ DFHACK_PLUGIN(rprobe rprobe.cpp)
DFHACK_PLUGIN(stepBetween stepBetween.cpp)
DFHACK_PLUGIN(stockcheck stockcheck.cpp)
DFHACK_PLUGIN(stripcaged stripcaged.cpp)
DFHACK_PLUGIN(tailor tailor.cpp)
DFHACK_PLUGIN(tilesieve tilesieve.cpp)
DFHACK_PLUGIN(zoom zoom.cpp)

@ -0,0 +1,523 @@
/*
* 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 "PluginManager.h"
#include "df/creature_raw.h"
#include "df/global_objects.h"
#include "df/historical_entity.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/ui.h"
#include "df/world.h"
#include "modules/Maps.h"
#include "modules/Units.h"
#include "modules/Translation.h"
#include "modules/World.h"
using namespace DFHack;
using namespace std;
using df::global::world;
using df::global::ui;
DFHACK_PLUGIN("tailor");
#define AUTOENABLE false
DFHACK_PLUGIN_IS_ENABLED(enabled);
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(ui);
const char *tagline = "Allow the bookkeeper to queue jobs to keep dwarfs in adequate clothing.";
const char *usage = (
" tailor enable\n"
" Enable the plugin.\n"
" tailor disable\n"
" Disable the plugin.\n"
" tailor use LEATHER,CLOTH,YARN,SILK\n"
" Use these materials, in this order, when requesting additional clothing\n"
" tailor status\n"
" Display plugin status\n"
"\n"
"Whenever the bookkeeper updates stockpile records, the plugin will scan every unit in the fort, \n"
"count up the number that are worn, and then order enough more made to replace all worn items.\n"
"If there are enough replacement items in inventory to replace all worn items, the units wearing them\n"
"will have the worn items confiscated (in the same manner as the _cleanowned_ plugin) so that they'll\n"
"reeequip with replacement items.\n"
);
// ARMOR, SHOES, HELM, GLOVES, PANTS
// ah, if only STL had a bimap
static 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 }
};
static 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}
};
void do_scan(color_ostream& out)
{
map<pair<df::item_type, int>, int> available; // key is item type & size
map<pair<df::item_type, int>, int> needed; // same
map<pair<df::item_type, int>, int> queued; // same
map<int, int> sizes; // this maps body size to races
map<tuple<df::job_type, int, int>, int> orders; // key is item type, item subtype, size
df::item_flags bad_flags;
bad_flags.whole = 0;
#define F(x) bad_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);
#undef F
available.empty();
needed.empty();
queued.empty();
orders.empty();
int silk = 0, yarn = 0, cloth = 0, leather = 0;
// scan for useable clothing
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)
continue;
if (i->getWear() >= 1)
continue;
df::item_type t = i->getType();
int size = world->raws.creatures.all[i->getMakerRace()]->adultsize;
available[make_pair(t, size)] += 1;
}
// scan for clothing raw materials
for (auto i : world->items.other[df::items_other_id::CLOTH])
{
if (i->flags.whole & bad_flags.whole)
continue;
if (!i->hasImprovements()) // only count dyed
continue;
MaterialInfo mat(i);
int ss = i->getStackSize();
if (mat.material)
{
if (mat.material->flags.is_set(df::material_flags::SILK))
silk += ss;
else if (mat.material->flags.is_set(df::material_flags::THREAD_PLANT))
cloth += ss;
else if (mat.material->flags.is_set(df::material_flags::YARN))
yarn += ss;
}
}
for (auto i : world->items.other[df::items_other_id::SKIN_TANNED])
{
if (i->flags.whole & bad_flags.whole)
continue;
leather += i->getStackSize();
}
out.print("available: silk %d yarn %d cloth %d leather %d\n", silk, yarn, cloth, leather);
// scan for units who need replacement clothing
for (auto u : world->units.active)
{
if (!Units::isOwnCiv(u) ||
!Units::isOwnGroup(u) ||
!Units::isActive(u) ||
Units::isBaby(u))
continue; // skip units we don't control
set <df::item_type> wearing;
wearing.empty();
deque<df::item*> worn;
worn.empty();
for (auto inv : u->inventory)
{
if (inv->mode != df::unit_inventory_item::Worn)
continue;
if (inv->item->getWear() > 0)
worn.push_back(inv->item);
else
wearing.insert(inv->item->getType());
}
int size = world->raws.creatures.all[u->race]->adultsize;
sizes[size] = u->race;
for (auto ty : set<df::item_type>{ df::item_type::ARMOR, df::item_type::PANTS, df::item_type::SHOES })
{
if (wearing.count(ty) == 0)
needed[make_pair(ty, size)] += 1;
}
for (auto w : worn)
{
auto ty = w->getType();
auto oo = itemTypeMap.find(ty);
if (oo == itemTypeMap.end())
continue;
df::job_type o = oo->second;
int size = world->raws.creatures.all[w->getMakerRace()]->adultsize;
std::string description;
w->getItemDescription(&description, 0);
if (available[make_pair(ty, size)] > 0)
{
if (w->flags.bits.owned)
{
bool confiscated = Items::setOwner(w, NULL);
out.print(
"%s %s from %s.\n",
(confiscated ? "Confiscated" : "Could not confiscate"),
description.c_str(),
Translation::TranslateName(&u->name, false).c_str()
);
}
if (wearing.count(ty) == 0)
available[make_pair(ty, size)] -= 1;
if (w->getWear() > 1)
w->flags.bits.dump = true;
}
else
{
// out.print("%s worn by %s needs replacement\n",
// description.c_str(),
// Translation::TranslateName(&u->name, false).c_str()
// );
orders[make_tuple(o, w->getSubtype(), size)] += 1;
}
}
}
auto entity = world->entities.all[ui->civ_id];
for (auto a : needed)
{
df::item_type ty = a.first.first;
int size = a.first.second;
int count = a.second;
int sub = 0;
vector<int16_t> v;
switch (ty) {
case df::item_type::ARMOR: v = entity->resources.armor_type; break;
case df::item_type::GLOVES: v = entity->resources.gloves_type; break;
case df::item_type::HELM: v = entity->resources.helm_type; break;
case df::item_type::PANTS: v = entity->resources.pants_type; break;
case df::item_type::SHOES: v = entity->resources.shoes_type; break;
}
for (auto vv : v) {
bool isClothing = false;
switch (ty) {
case df::item_type::ARMOR: isClothing = world->raws.itemdefs.armor[vv] ->armorlevel == 0; break;
case df::item_type::GLOVES: isClothing = world->raws.itemdefs.gloves[vv]->armorlevel == 0; break;
case df::item_type::HELM: isClothing = world->raws.itemdefs.helms[vv] ->armorlevel == 0; break;
case df::item_type::PANTS: isClothing = world->raws.itemdefs.pants[vv] ->armorlevel == 0; break;
case df::item_type::SHOES: isClothing = world->raws.itemdefs.shoes[vv] ->armorlevel == 0; break;
}
if (isClothing)
{
sub = vv;
break;
}
}
orders[make_tuple(itemTypeMap[ty], sub, size)] += count;
}
// scan orders
for (auto o : world->manager_orders)
{
auto f = jobTypeMap.find(o->job_type);
if (f == jobTypeMap.end())
continue;
auto sub = o->item_subtype;
int race = o->hist_figure_id;
if (race == -1)
continue; // -1 means that the race of the worker will determine the size made; we must ignore these jobs
int size = world->raws.creatures.all[race]->adultsize;
orders[make_tuple(o->job_type, sub, size)] -= o->amount_left;
}
// place orders
for (auto o : orders)
{
df::job_type ty;
int sub;
int size;
tie(ty, sub, size) = o.first;
int count = o.second;
if (count > 0)
{
vector<int16_t> v;
BitArray<df::armor_general_flags>* fl;
string name_s, name_p;
switch (ty) {
case df::job_type::MakeArmor:
name_s = world->raws.itemdefs.armor[sub]->name;
name_p = world->raws.itemdefs.armor[sub]->name_plural;
v = entity->resources.armor_type;
fl = &world->raws.itemdefs.armor[sub]->props.flags;
break;
case df::job_type::MakeGloves:
name_s = world->raws.itemdefs.gloves[sub]->name;
name_p = world->raws.itemdefs.gloves[sub]->name_plural;
v = entity->resources.gloves_type;
fl = &world->raws.itemdefs.gloves[sub]->props.flags;
break;
case df::job_type::MakeHelm:
name_s = world->raws.itemdefs.helms[sub]->name;
name_p = world->raws.itemdefs.helms[sub]->name_plural;
v = entity->resources.helm_type;
fl = &world->raws.itemdefs.helms[sub]->props.flags;
break;
case df::job_type::MakePants:
name_s = world->raws.itemdefs.pants[sub]->name;
name_p = world->raws.itemdefs.pants[sub]->name_plural;
v = entity->resources.pants_type;
fl = &world->raws.itemdefs.pants[sub]->props.flags;
break;
case df::job_type::MakeShoes:
name_s = world->raws.itemdefs.shoes[sub]->name;
name_p = world->raws.itemdefs.shoes[sub]->name_plural;
v = entity->resources.shoes_type;
fl = &world->raws.itemdefs.shoes[sub]->props.flags;
break;
}
bool can_make = false;
for (auto vv : v)
{
if (vv == sub)
{
can_make = true;
break;
}
}
if (!can_make)
{
out.print("Cannot make %s, skipped\n", name_p);
continue; // this civilization does not know how to make this item, so sorry
}
switch (ty) {
case df::item_type::ARMOR: break;
case df::item_type::GLOVES: break;
case df::item_type::HELM: break;
case df::item_type::PANTS: break;
case df::item_type::SHOES: break;
}
df::job_material_category mat;
if (silk > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) {
mat.whole = df::job_material_category::mask_silk;
silk -= count;
}
else if (cloth > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) {
mat.whole = df::job_material_category::mask_cloth;
cloth -= count;
}
else if (yarn > count + 10 && fl->is_set(df::armor_general_flags::SOFT)) {
mat.whole = df::job_material_category::mask_yarn;
yarn -= count;
}
else if (leather > count + 10 && fl->is_set(df::armor_general_flags::LEATHER)) {
mat.whole = df::job_material_category::mask_leather;
leather -= count;
}
else // not enough appropriate material available
continue;
auto order = new df::manager_order();
order->job_type = ty;
order->item_type = df::item_type::NONE;
order->item_subtype = sub;
order->mat_type = -1;
order->mat_index = -1;
order->amount_left = count;
order->amount_total = count;
order->status.bits.validated = false;
order->status.bits.active = false;
order->id = world->manager_order_next_id++;
order->hist_figure_id = sizes[size];
order->material_category = mat;
world->manager_orders.push_back(order);
out.print("Added order #%d for %d %s %s (sized for %s)\n",
order->id,
count,
bitfield_to_string(order->material_category),
(count > 1) ? name_p : name_s,
world->raws.creatures.all[order->hist_figure_id]->name[1]
);
}
}
}
#define DELTA_TICKS 600
DFhackCExport command_result plugin_onupdate(color_ostream &out)
{
if (!enabled)
return CR_OK;
if (!Maps::IsValid())
return CR_OK;
if (DFHack::World::ReadPauseState())
return CR_OK;
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
bool found = false;
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;
}
}
if (found)
{
do_scan(out);
}
return CR_OK;
}
static bool tailor_set_materials(string parameter)
{
return false;
}
static command_result tailor_cmd(color_ostream &out, vector <string> & parameters) {
bool desired = enabled;
if (parameters.size() == 1)
{
if (parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1")
{
desired = true;
}
else if (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0")
{
desired = false;
}
else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?")
{
out.print("%s: %s\nUsage:\n%s", plugin_name, tagline, usage);
return CR_OK;
}
else if (parameters[0] == "test")
{
do_scan(out);
return CR_OK;
}
else if (parameters[0] != "status")
{
return CR_WRONG_USAGE;
}
}
else if (parameters.size() == 2 && parameters[1] == "use")
{
return (
tailor_set_materials(parameters[2]) ?
CR_OK : CR_WRONG_USAGE
);
}
out.print("Tailor is %s %s.\n", (desired == enabled)? "currently": "now", desired? "enabled": "disabled");
enabled = desired;
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream& out, bool enable)
{
enabled = enable;
return CR_OK;
}
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands)
{
if (AUTOENABLE) {
enabled = true;
}
commands.push_back(PluginCommand(plugin_name, tagline, tailor_cmd, false, usage));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
return plugin_enable(out, false);
}