506 lines
16 KiB
C++
506 lines
16 KiB
C++
|
/*
|
||
|
* 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 status\n"
|
||
|
" Display plugin status\n"
|
||
|
"\n"
|
||
|
"Whenever the bookkeeper updates stockpile records, this 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 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
|
||
|
return 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);
|
||
|
}
|