Merge remote-tracking branch 'ab9rf/tailor-v2' into develop

Conflicts: docs/changelog.txt
develop
lethosor 2021-09-01 23:34:26 -04:00
commit 0d14a2ccef
No known key found for this signature in database
GPG Key ID: 76A269552F4F58C1
2 changed files with 466 additions and 314 deletions

@ -48,6 +48,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `orders`: new ``sort`` command. sorts orders according to their repeat frequency. this prevents daily orders from blocking other orders for simlar items from ever getting completed.
- `tiletypes-here`, `tiletypes-here-point`: add ``--cursor`` and ``--quiet`` options to support non-interactive use cases
- `quickfort`: Dreamfort blueprint set improvements: extensive revision based on playtesting and feedback. includes updated ``onMapLoad_dreamfort.init`` settings file, enhanced automation orders, and premade profession definitions. see full changelog at https://github.com/DFHack/dfhack/pull/1921 and https://github.com/DFHack/dfhack/pull/1925
- `tailor`: allow user to specify which materials to be used, and in what order
## API
- The ``Items`` module ``moveTo*`` and ``remove`` functions now handle projectiles

@ -41,44 +41,92 @@ 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 = (
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"
" tailor materials ...\n"
" for example: tailor materials silk cloth yarn leather\n"
" Set allowed material list to the specified list.\n"
" The example sets the list to silk, cloth, yarn, leather, in that order, which is the default.\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"
);
);
class Tailor {
// ARMOR, SHOES, HELM, GLOVES, PANTS
// ARMOR, SHOES, HELM, GLOVES, PANTS
// ah, if only STL had a bimap
// ah, if only STL had a bimap
private:
static map<df::job_type, df::item_type> jobTypeMap = {
const 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 = {
const 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}
};
{ 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;
}
// operator< is required to use this as a std::map key
bool operator<(const MatType& m) const
{
return name < m.name;
}
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) {};
};
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);
std::list<MatType> all_materials = { M_SILK, M_CLOTH, M_YARN, M_LEATHER };
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
@ -87,24 +135,27 @@ void do_scan(color_ostream& out)
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;
std::map<MatType, int> supply;
#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
color_ostream* out;
available.empty();
needed.empty();
queued.empty();
orders.empty();
std::list<MatType> material_order = { M_SILK, M_CLOTH, M_YARN, M_LEATHER };
std::map<MatType, int> reserves;
int silk = 0, yarn = 0, cloth = 0, leather = 0;
int default_reserve = 10;
// scan for useable clothing
void reset()
{
available.clear();
needed.clear();
queued.clear();
sizes.clear();
orders.clear();
supply.clear();
}
void scan_clothing()
{
for (auto i : world->items.other[df::items_other_id::ANY_GENERIC37]) // GENERIC37 is "clothing"
{
if (i->flags.whole & bad_flags.whole)
@ -118,9 +169,10 @@ void do_scan(color_ostream& out)
available[make_pair(t, size)] += 1;
}
}
// scan for clothing raw materials
void scan_materials()
{
for (auto i : world->items.other[df::items_other_id::CLOTH])
{
if (i->flags.whole & bad_flags.whole)
@ -133,11 +185,11 @@ void do_scan(color_ostream& out)
if (mat.material)
{
if (mat.material->flags.is_set(df::material_flags::SILK))
silk += ss;
supply[M_SILK] += ss;
else if (mat.material->flags.is_set(df::material_flags::THREAD_PLANT))
cloth += ss;
supply[M_CLOTH] += ss;
else if (mat.material->flags.is_set(df::material_flags::YARN))
yarn += ss;
supply[M_YARN] += ss;
}
}
@ -145,13 +197,14 @@ void do_scan(color_ostream& out)
{
if (i->flags.whole & bad_flags.whole)
continue;
leather += i->getStackSize();
supply[M_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
out->print("tailor: available silk %d yarn %d cloth %d leather %d\n", supply[M_SILK], supply[M_YARN], supply[M_CLOTH], supply[M_LEATHER]);
}
void scan_replacements()
{
for (auto u : world->units.active)
{
if (!Units::isOwnCiv(u) ||
@ -161,10 +214,10 @@ void do_scan(color_ostream& out)
continue; // skip units we don't control
set <df::item_type> wearing;
wearing.empty();
wearing.clear();
deque<df::item*> worn;
worn.empty();
worn.clear();
for (auto inv : u->inventory)
{
@ -188,10 +241,7 @@ void do_scan(color_ostream& out)
for (auto w : worn)
{
auto ty = w->getType();
auto oo = itemTypeMap.find(ty);
if (oo == itemTypeMap.end())
continue;
df::job_type o = oo->second;
auto o = itemTypeMap.at(ty);
int size = world->raws.creatures.all[w->getMakerRace()]->adultsize;
std::string description;
@ -203,9 +253,9 @@ void do_scan(color_ostream& out)
{
bool confiscated = Items::setOwner(w, NULL);
out.print(
"%s %s from %s.\n",
(confiscated ? "Confiscated" : "Could not confiscate"),
out->print(
"tailor: %s %s from %s.\n",
(confiscated ? "confiscated" : "could not confiscate"),
description.c_str(),
Translation::TranslateName(&u->name, false).c_str()
);
@ -219,18 +269,21 @@ void do_scan(color_ostream& out)
}
else
{
// out.print("%s worn by %s needs replacement\n",
// description.c_str(),
// Translation::TranslateName(&u->name, false).c_str()
// );
// 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;
}
}
}
}
void create_orders()
{
auto entity = world->entities.all[ui->civ_id];
for (auto a : needed)
for (auto& a : needed)
{
df::item_type ty = a.first.first;
int size = a.first.second;
@ -251,11 +304,11 @@ void do_scan(color_ostream& out)
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::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;
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;
default: break;
}
if (isClothing)
@ -265,11 +318,13 @@ void do_scan(color_ostream& out)
}
}
orders[make_tuple(itemTypeMap[ty], sub, size)] += count;
const df::job_type j = itemTypeMap.at(ty);
orders[make_tuple(j, sub, size)] += count;
}
}
// scan orders
void scan_existing_orders()
{
for (auto o : world->manager_orders)
{
auto f = jobTypeMap.find(o->job_type);
@ -286,9 +341,13 @@ void do_scan(color_ostream& out)
orders[make_tuple(o->job_type, sub, size)] -= o->amount_left;
}
// place orders
}
void place_orders()
{
auto entity = world->entities.all[ui->civ_id];
for (auto o : orders)
for (auto& o : orders)
{
df::job_type ty;
int sub;
@ -304,10 +363,11 @@ void do_scan(color_ostream& out)
string name_s, name_p;
switch (ty) {
case df::job_type::MakeArmor:
v = entity->resources.armor_type;
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:
@ -338,95 +398,132 @@ void do_scan(color_ostream& out)
break;
}
bool can_make = false;
for (auto vv : v)
{
if (vv == sub)
{
can_make = true;
break;
}
}
bool can_make = std::find(v.begin(), v.end(), sub) != v.end();
if (!can_make)
{
out.print("Cannot make %s, skipped\n", name_p.c_str());
continue; // this civilization does not know how to make this item, so sorry
out->print("tailor: civilization cannot make %s, skipped\n", name_p.c_str());
continue;
}
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;
default: break;
}
for (auto& m : material_order)
{
if (count <= 0)
break;
df::job_material_category mat;
auto r = reserves.find(m);
int res = (r == reserves.end()) ? default_reserve : r->second;
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;
if (supply[m] > res && fl->is_set(m.armor_flag)) {
int c = count;
if (supply[m] < count + res)
c = supply[m] - res;
supply[m] -= c;
auto order = new df::manager_order();
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->amount_left = c;
order->amount_total = c;
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;
order->material_category = m.job_material;
world->manager_orders.push_back(order);
out.print("Added order #%d for %d %s %s (sized for %s)\n",
out->print("tailor: added order #%d for %d %s %s, sized for %s\n",
order->id,
count,
c,
bitfield_to_string(order->material_category).c_str(),
(count > 1) ? name_p.c_str() : name_s.c_str(),
(c > 1) ? name_p.c_str() : name_s.c_str(),
world->raws.creatures.all[order->hist_figure_id]->name[1].c_str()
);
count -= c;
}
}
}
}
}
}
#define DELTA_TICKS 600
public:
void do_scan(color_ostream& o)
{
out = &o;
DFhackCExport command_result plugin_onupdate(color_ostream &out)
{
if (!enabled)
return CR_OK;
reset();
if (!Maps::IsValid())
return CR_OK;
// scan for useable clothing
if (DFHack::World::ReadPauseState())
return CR_OK;
scan_clothing();
// scan for clothing raw materials
scan_materials();
// scan for units who need replacement clothing
scan_replacements();
// create new orders
create_orders();
// scan existing orders and subtract
scan_existing_orders();
// place orders
place_orders();
}
public:
command_result set_materials(color_ostream& out, vector<string>& parameters)
{
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())
{
out.print("tailor: material %s not recognized\n", m->c_str());
return CR_WRONG_USAGE;
}
else {
newmat.push_back(*mm);
}
}
material_order = newmat;
out.print("tailor: material list set to %s\n", get_material_list().c_str());
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
}
public:
std::string get_material_list()
{
std::string s;
for (const auto& m : material_order)
{
if (!s.empty()) s += ", ";
s += m.name;
}
return s;
}
public:
void process(color_ostream& out)
{
bool found = false;
for (df::job_list_link* link = &world->jobs.list; link != NULL; link = link->next)
@ -443,48 +540,97 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out)
{
do_scan(out);
}
}
};
static std::unique_ptr<Tailor> tailor_instance;
#define DELTA_TICKS 600
DFhackCExport command_result plugin_onupdate(color_ostream& out)
{
if (!enabled || !tailor_instance)
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;
{
CoreSuspender suspend;
tailor_instance->process(out);
}
return CR_OK;
}
static command_result tailor_cmd(color_ostream &out, vector <string> & parameters) {
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")
if (parameters.size() == 1 && parameters[0] == "enable" || parameters[0] == "on" || parameters[0] == "1")
{
desired = true;
}
else if (parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0")
else if (parameters.size() == 1 && parameters[0] == "disable" || parameters[0] == "off" || parameters[0] == "0")
{
desired = false;
}
else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?")
else if (parameters.size() == 1 && 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")
else if (parameters.size() == 1 && parameters[0] == "test")
{
do_scan(out);
if (tailor_instance)
{
tailor_instance->do_scan(out);
return CR_OK;
}
else if (parameters[0] != "status")
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;
}
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
return CR_WRONG_USAGE;
{
out.print("%s: not instantiated\n", plugin_name);
}
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)
DFhackCExport command_result plugin_onstatechange(color_ostream& out, state_change_event event)
{
return CR_OK;
}
@ -495,8 +641,10 @@ DFhackCExport command_result plugin_enable(color_ostream& out, bool enable)
return CR_OK;
}
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands)
DFhackCExport command_result plugin_init(color_ostream& out, std::vector <PluginCommand>& commands)
{
tailor_instance = std::move(dts::make_unique<Tailor>());
if (AUTOENABLE) {
enabled = true;
}
@ -505,6 +653,9 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
DFhackCExport command_result plugin_shutdown(color_ostream& out)
{
tailor_instance.release();
return plugin_enable(out, false);
}