diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 98258682e..345dfa578 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -38,6 +38,31 @@ #include "df/inorganic_raw.h" #include "df/builtin_mats.h" +#include "df/viewscreen_dwarfmodest.h" +#include "df/itemdef_weaponst.h" +#include "df/itemdef_trapcompst.h" +#include "df/itemdef_toyst.h" +#include "df/itemdef_toolst.h" +#include "df/itemdef_instrumentst.h" +#include "df/itemdef_armorst.h" +#include "df/itemdef_ammost.h" +#include "df/itemdef_siegeammost.h" +#include "df/itemdef_glovesst.h" +#include "df/itemdef_shoesst.h" +#include "df/itemdef_shieldst.h" +#include "df/itemdef_helmst.h" +#include "df/itemdef_pantsst.h" +#include "df/itemdef_foodst.h" +#include "df/trapcomp_flags.h" + +#include +#include +#include +#include +#include "df/creature_raw.h" +using df::global::gps; +using std::deque; + using std::vector; using std::string; using std::endl; @@ -47,6 +72,7 @@ using namespace df::enums; using df::global::world; using df::global::ui; using df::global::ui_workshop_job_cursor; +using df::global::ui_workshop_in_add; using df::global::job_next_id; /* Plugin registration */ @@ -58,100 +84,28 @@ static void cleanup_state(color_ostream &out); DFHACK_PLUGIN("workflow"); -DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) -{ - if (!world || !ui) - return CR_FAILURE; - if (ui_workshop_job_cursor && job_next_id) { - commands.push_back( - PluginCommand( - "workflow", "Manage control of repeat jobs.", - workflow_cmd, false, - " workflow enable [option...]\n" - " workflow disable [option...]\n" - " If no options are specified, enables or disables the plugin.\n" - " Otherwise, enables or disables any of the following options:\n" - " - drybuckets: Automatically empty abandoned water buckets.\n" - " - auto-melt: Resume melt jobs when there are objects to melt.\n" - " workflow jobs\n" - " List workflow-controlled jobs (if in a workshop, filtered by it).\n" - " workflow list\n" - " List active constraints, and their job counts.\n" - " workflow list-commands\n" - " List workflow commands that re-create existing constraints.\n" - " workflow count [cnt-gap]\n" - " workflow amount [cnt-gap]\n" - " Set a constraint. The first form counts each stack as only 1 item.\n" - " workflow unlimit \n" - " Delete a constraint.\n" - " workflow unlimit-all\n" - " Delete all constraints.\n" - "Function:\n" - " - When the plugin is enabled, it protects all repeat jobs from removal.\n" - " If they do disappear due to any cause, they are immediately re-added\n" - " to their workshop and suspended.\n" - " - In addition, when any constraints on item amounts are set, repeat jobs\n" - " that produce that kind of item are automatically suspended and resumed\n" - " as the item amount goes above or below the limit. The gap specifies how\n" - " much below the limit the amount has to drop before jobs are resumed;\n" - " this is intended to reduce the frequency of jobs being toggled.\n" - "Constraint examples:\n" - " workflow amount AMMO:ITEM_AMMO_BOLTS/METAL 1000 100\n" - " workflow amount AMMO:ITEM_AMMO_BOLTS/WOOD,BONE 200 50\n" - " Keep metal bolts within 900-1000, and wood/bone within 150-200.\n" - " workflow count FOOD 120 30\n" - " workflow count DRINK 120 30\n" - " Keep the number of prepared food & drink stacks between 90 and 120\n" - " workflow count BIN 30\n" - " workflow count BARREL 30\n" - " workflow count BOX/CLOTH,SILK,YARN 30\n" - " Make sure there are always 25-30 empty bins/barrels/bags.\n" - " workflow count BAR//COAL 20\n" - " workflow count BAR//COPPER 30\n" - " Make sure there are always 15-20 coal and 25-30 copper bars.\n" - " workflow count CRAFTS//GOLD 20\n" - " Produce 15-20 gold crafts.\n" - " workflow count POWDER_MISC/SAND 20\n" - " workflow count BOULDER/CLAY 20\n" - " Collect 15-20 sand bags and clay boulders.\n" - " workflow amount POWDER_MISC//MUSHROOM_CUP_DIMPLE:MILL 100 20\n" - " Make sure there are always 80-100 units of dimple dye.\n" - " In order for this to work, you have to set the material of\n" - " the PLANT input on the Mill Plants job to MUSHROOM_CUP_DIMPLE\n" - " using the 'job item-material' command.\n" - ) - ); +void OutputString(int8_t color, int &x, int &y, const std::string &text, bool newline = false, int left_margin = 0) +{ + Screen::paintString(Screen::Pen(' ', color, 0), x, y, text); + if (newline) + { + ++y; + x = left_margin; } - - init_state(out); - - return CR_OK; + else + x += text.length(); } -DFhackCExport command_result plugin_shutdown (color_ostream &out) +void OutputHotkeyString(int &x, int &y, const char *text, const char *hotkey, bool newline = false, int left_margin = 0) { - cleanup_state(out); - - return CR_OK; + OutputString(10, x, y, hotkey); + string display(": "); + display.append(text); + OutputString(15, x, y, display, newline, left_margin); } -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - cleanup_state(out); - init_state(out); - break; - case SC_MAP_UNLOADED: - cleanup_state(out); - break; - default: - break; - } - return CR_OK; -} /****************************** * JOB STATE TRACKING STRUCTS * @@ -272,6 +226,8 @@ int ProtectedJob::cur_tick_idx = 0; typedef std::map, bool> TMaterialCache; +static int max_history_days = 14; + struct ItemConstraint { PersistentDataItem config; @@ -293,6 +249,8 @@ struct ItemConstraint { TMaterialCache material_cache; + deque history; + public: ItemConstraint() : is_craft(false), weight(0), min_quality(item_quality::Ordinary),item_amount(0), @@ -327,6 +285,14 @@ public: int size = goalByCount() ? item_count : item_amount; request_resume = (size <= goalCount()-goalGap()); request_suspend = (size >= goalCount()); + + if (max_history_days > 0) + { + history.push_back(size); + if (history.size() > max_history_days * 2) + history.pop_front(); + } + } }; @@ -547,6 +513,15 @@ static bool recover_job(color_ostream &out, ProtectedJob *pj) return true; } +static ProtectedJob *add_known_job(df::job *job) +{ + ProtectedJob *pj = new ProtectedJob(job); + assert(pj->holder); + known_jobs[pj->id] = pj; + + return pj; +} + static void check_lost_jobs(color_ostream &out, int ticks) { ProtectedJob::cur_tick_idx++; @@ -567,9 +542,7 @@ static void check_lost_jobs(color_ostream &out, int ticks) } else if (job->flags.bits.repeat && isSupportedJob(job)) { - pj = new ProtectedJob(job); - assert(pj->holder); - known_jobs[pj->id] = pj; + add_known_job(job); } } @@ -605,6 +578,23 @@ static void recover_jobs(color_ostream &out) static void process_constraints(color_ostream &out); +#define ITEMDEF_VECTORS \ + ITEM(WEAPON, weapons, itemdef_weaponst) \ + ITEM(TRAPCOMP, trapcomps, itemdef_trapcompst) \ + ITEM(TOY, toys, itemdef_toyst) \ + ITEM(TOOL, tools, itemdef_toolst) \ + ITEM(INSTRUMENT, instruments, itemdef_instrumentst) \ + ITEM(ARMOR, armor, itemdef_armorst) \ + ITEM(AMMO, ammo, itemdef_ammost) \ + ITEM(SIEGEAMMO, siege_ammo, itemdef_siegeammost) \ + ITEM(GLOVES, gloves, itemdef_glovesst) \ + ITEM(SHOES, shoes, itemdef_shoesst) \ + ITEM(SHIELD, shields, itemdef_shieldst) \ + ITEM(HELM, helms, itemdef_helmst) \ + ITEM(PANTS, pants, itemdef_pantsst) \ + ITEM(FOOD, food, itemdef_foodst) + +static bool first_update_done = false; DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (!enabled) @@ -612,7 +602,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) // Every 5 frames check the jobs for disappearance static unsigned cnt = 0; - if ((++cnt % 5) != 0) + if ((++cnt % 5) != 0 && !first_update_done) return CR_OK; check_lost_jobs(out, world->frame_counter - last_tick_frame_count); @@ -622,28 +612,55 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) static unsigned last_rlen = 0; bool check_time = (world->frame_counter - last_frame_count) >= DAY_TICKS/2; - if (pending_recover.size() != last_rlen || check_time) + if (pending_recover.size() != last_rlen || check_time || !first_update_done) { recover_jobs(out); last_rlen = pending_recover.size(); // If the half-day passed, proceed to update - if (check_time) + if (check_time || !first_update_done) { last_frame_count = world->frame_counter; update_job_data(out); process_constraints(out); + first_update_done = true; } } + return CR_OK; } + /****************************** * ITEM COUNT CONSTRAINT * ******************************/ +static ItemConstraint * create_new_constraint(bool is_craft, ItemTypeInfo item, MaterialInfo material, + df::dfhack_material_category mat_mask, item_quality::item_quality minqual, + int weight, PersistentDataItem * cfg, const std::string & str) +{ + ItemConstraint *nct = new ItemConstraint; + nct->is_craft = is_craft; + nct->item = item; + nct->material = material; + nct->mat_mask = mat_mask; + nct->min_quality = minqual; + nct->weight = weight; + + if (cfg) + nct->config = *cfg; + else + { + nct->config = Core::getInstance().getWorld()->AddPersistentData("workflow/constraints"); + nct->init(str); + } + + constraints.push_back(nct); + return nct; +} + static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg) { std::vector tokens; @@ -717,23 +734,8 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str return ct; } - ItemConstraint *nct = new ItemConstraint; - nct->is_craft = is_craft; - nct->item = item; - nct->material = material; - nct->mat_mask = mat_mask; - nct->min_quality = minqual; - nct->weight = weight; - - if (cfg) - nct->config = *cfg; - else - { - nct->config = Core::getInstance().getWorld()->AddPersistentData("workflow/constraints"); - nct->init(str); - } + ItemConstraint *nct = create_new_constraint(is_craft, item, material, mat_mask, minqual, weight, cfg, str); - constraints.push_back(nct); return nct; } @@ -766,10 +768,11 @@ static bool isCraftItem(df::item_type type) static void link_job_constraint(ProtectedJob *pj, df::item_type itype, int16_t isubtype, df::dfhack_material_category mat_mask, int16_t mat_type, int32_t mat_index, - bool is_craft = false) + bool is_craft = false, bool create_constraint = false) { MaterialInfo mat(mat_type, mat_index); + bool constraint_found = false; for (size_t i = 0; i < constraints.size(); i++) { ItemConstraint *ct = constraints[i]; @@ -807,10 +810,30 @@ static void link_job_constraint(ProtectedJob *pj, df::item_type itype, int16_t i ct->jobs.push_back(pj); pj->constraints.push_back(ct); + constraint_found = true; if (!ct->is_active && pj->isResumed()) ct->is_active = true; } + + if (create_constraint && !constraint_found) + { + ItemTypeInfo item; + item.type = itype; + item.subtype = isubtype; + + int weight = 0; + if (item.subtype >= 0) + weight += 10000; + if (mat_mask.whole != 0) + weight += 100; + if (mat.type >= 0) + weight += (mat.index >= 0 ? 5000 : 1000); + + ItemConstraint *nct = create_new_constraint(is_craft, item, mat, mat_mask, item_quality::Ordinary, weight, NULL, string("test")); + nct->jobs.push_back(pj); + pj->constraints.push_back(nct); + } } static void compute_custom_job(ProtectedJob *pj, df::job *job) @@ -919,7 +942,7 @@ static void guess_job_material(df::job *job, MaterialInfo &mat, df::dfhack_mater } } -static void compute_job_outputs(color_ostream &out, ProtectedJob *pj) +static void compute_job_outputs(color_ostream &out, ProtectedJob *pj, bool create_constraint = false) { using namespace df::enums::job_type; @@ -1012,7 +1035,7 @@ static void compute_job_outputs(color_ostream &out, ProtectedJob *pj) break; } - link_job_constraint(pj, itype, isubtype, mat_mask, mat.type, mat.index); + link_job_constraint(pj, itype, isubtype, mat_mask, mat.type, mat.index, false, create_constraint); } static void map_job_constraints(color_ostream &out) @@ -1129,7 +1152,7 @@ static void map_job_items(color_ostream &out) df::item_flags bad_flags; bad_flags.whole = 0; -#define F(x) bad_flags.bits.x = true; +#define F(left_margin) bad_flags.bits.left_margin = true; F(dump); F(forbid); F(garbage_collect); F(hostile); F(on_fire); F(rotten); F(trader); F(in_building); F(construction); F(artifact1); @@ -1237,7 +1260,9 @@ static void map_job_items(color_ostream &out) } for (size_t i = 0; i < constraints.size(); i++) + { constraints[i]->computeRequest(); + } } /****************************** @@ -1673,3 +1698,1170 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet else return CR_WRONG_USAGE; } + +/****************************** + * Inventory Monitor * + ******************************/ +#define INV_MONITOR_COL_COUNT 3 +#define MAX_ITEM_NAME 15 +#define MAX_MASK 10 +#define MAX_MATERIAL 20 +#define SIDEBAR_WIDTH 30 +#define COLOR_TITLE COLOR_BLUE +#define COLOR_UNSELECTED COLOR_GREY +#define COLOR_SELECTED COLOR_WHITE +#define COLOR_HIGHLIGHTED COLOR_GREEN + +namespace wf_ui +{ + /* + * Utility Functions + */ + typedef int8_t UIColor; + + const int ascii_to_enum_offset = interface_key::STRING_A048 - '0'; + + inline string int_to_string(const int n) + { + return static_cast( &(ostringstream() << n) )->str(); + } + + void set_to_limit(int &value, const int maximum, const int min = 0) + { + if (value < min) + value = min; + else if (value > maximum) + value = maximum; + } + + inline void paint_text(const UIColor color, const int &x, const int &y, const std::string &text, const UIColor background = 0) + { + Screen::paintString(Screen::Pen(' ', color, background), x, y, text); + } + + string get_constraint_material(ItemConstraint *cv) + { + string text; + if (!cv->material.isNone()) + { + text.append(cv->material.toString()); + text.append(" "); + } + + text.append(bitfield_to_string(cv->mat_mask)); + + return text; + } + + string pad_string(string text, const int size, const bool front = true) + { + if (text.length() >= size) + return text; + + string aligned(size - text.length(), ' '); + if (front) + { + aligned.append(text); + return aligned; + } + else + { + text.append(aligned); + return text; + } + } + + /* + * Adjustment Dialog + */ + + class AdjustmentScreen + { + public: + int32_t x, y, left_margin; + + AdjustmentScreen(); + void reset(); + bool feed(set *input, ItemConstraint *cv, ProtectedJob *pj = NULL); + void render(ItemConstraint *cv); + + protected: + int32_t adjustment_ui_display_start; + + virtual void onConstraintChanged() {} + + private: + bool edit_limit, edit_gap; + string edit_string; + + }; + + AdjustmentScreen::AdjustmentScreen() + { + reset(); + } + + void AdjustmentScreen::reset() + { + edit_gap = false; + edit_limit = false; + adjustment_ui_display_start = 24; + } + + bool AdjustmentScreen::feed(set *input, ItemConstraint *cv, ProtectedJob *pj /* = NULL */) + { + if ((edit_limit || edit_gap)) + { + df::interface_key last_token = *input->rbegin(); + if (last_token == interface_key::STRING_A000) + { + // Backspace + if (edit_string.length() > 0) + { + edit_string.erase(edit_string.length()-1); + } + + return true; + } + + if (edit_string.length() >= 6) + return true; + + if (last_token >= interface_key::STRING_A048 && last_token <= interface_key::STRING_A057) + { + // Numeric character + edit_string += last_token - ascii_to_enum_offset; + } + else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN)) + { + if (input->count(interface_key::SELECT) && edit_string.length() > 0) + { + if (edit_limit) + cv->setGoalCount(atoi(edit_string.c_str())); + else + cv->setGoalGap(atoi(edit_string.c_str())); + + onConstraintChanged(); + } + edit_string.clear(); + edit_limit = false; + edit_gap = false; + } + else if (last_token == interface_key::STRING_A000) + { + // Backspace + if (edit_string.length() > 0) + { + edit_string.erase(edit_string.length()-1); + } + } + + return true; + } + else if (input->count(interface_key::CUSTOM_L)) + { + edit_string = int_to_string(cv->goalCount()); + edit_limit = true; + } + else if (input->count(interface_key::CUSTOM_G)) + { + edit_string = int_to_string(cv->goalGap()); + edit_gap = true; + } + else if (input->count(interface_key::CUSTOM_M)) + { + cv->setGoalByCount(!cv->goalByCount()); + onConstraintChanged(); + } + else if (input->count(interface_key::CUSTOM_T)) + { + if (cv) + { + // Remove tracking + if (pj) + { + for (vector::iterator it = pj->constraints.begin(); it < pj->constraints.end(); it++) + delete_constraint(*it); + + forget_job(color_ostream_proxy(Core::getInstance().getConsole()), pj); + } + } + else + { + // Add tracking + return false; + } + } + else + { + return false; + } + + return true; + } + + void AdjustmentScreen::render(ItemConstraint *cv) + { + left_margin = gps->dimx - 30; + x = left_margin; + y = adjustment_ui_display_start; + if (cv != NULL) + { + string text; + text.reserve(20); + + text.append(get_constraint_material(cv)); + if (!text.empty()) + text.append(" "); + text.append(cv->item.toString()); + + OutputString(COLOR_GREEN, x, y, text, true, left_margin); + + text.clear(); + text.append("Available: "); + text.append(int_to_string((cv->goalByCount()) ? cv->item_count : cv->item_amount)); + text.append(" "); + text.append((cv->goalByCount()) ? "Stacks" : "Items"); + + OutputString(15, x, y, text, true, left_margin); + + text.clear(); + text.append("In use : "); + text.append(int_to_string(cv->item_inuse)); + OutputString(15, x, y, text, true, left_margin); + + y += 2; + x = left_margin; + OutputHotkeyString(x, y, "Disable Tracking", "t", true, left_margin); + + text.clear(); + text.append("Limit: "); + text.append((edit_limit) ? edit_string : int_to_string(cv->goalCount())); + OutputHotkeyString(x, y, text.c_str(), "l"); + if (edit_limit) + OutputString(10, x, y, "_"); + + ++y; + x = left_margin; + text.clear(); + text.append("Gap: "); + text.append((edit_gap) ? edit_string : int_to_string(cv->goalGap())); + OutputHotkeyString(x, y, text.c_str(), "g"); + int pad = (int) (11 - text.length()); + if (edit_gap) + { + OutputString(10, x, y, "_"); + --pad; + } + + x += max(0, pad); + OutputHotkeyString(x, y, "Toggle Mode", "m"); + } + else + OutputHotkeyString(x, y, "Enable Tracking", "t", true, left_margin); + } + + + /* + * List classes + */ + template + class ListEntry + { + public: + T elem; + string text; + bool selected; + + ListEntry(string text, T elem) + { + this->text = text; + this->elem = elem; + selected = false; + } + }; + + template + class ListColumn + { + public: + string title; + vector< ListEntry > list; + int highlighted_index; + int display_max_rows; + int display_start_offset; + bool multiselect; + bool allow_null; + + ListColumn() + { + highlighted_index = 0; + display_max_rows = gps->dimy - 4; + display_start_offset = 0; + multiselect = false; + allow_null = true; + } + + void clear() + { + list.clear(); + } + + void add(ListEntry &entry) + { + list.push_back(entry); + } + + void add(string &text, T &elem) + { + list.push_back(ListEntry(text, elem)); + } + + virtual void display_extras(const T &elem) const {} + + void display(const int left_margin, const bool is_selected_column) const + { + int y = 2; + paint_text(COLOR_TITLE, left_margin, y, title); + + int last_index_able_to_display = display_start_offset + display_max_rows; + for (int i = display_start_offset; i < list.size() && i < last_index_able_to_display; i++) + { + ++y; + UIColor fg_color = (list[i].selected) ? COLOR_SELECTED : COLOR_UNSELECTED; + UIColor bg_color = (is_selected_column && i == highlighted_index) ? COLOR_HIGHLIGHTED : COLOR_BLACK; + paint_text(fg_color, left_margin, y, list[i].text, bg_color); + display_extras(list[i].elem); + } + } + + void changeHighlight(const int highlight_change, const int offset_shift = 0) + { + highlighted_index += highlight_change + offset_shift * display_max_rows; + set_to_limit(highlighted_index, list.size() - 1); + + display_start_offset += offset_shift * display_max_rows; + set_to_limit(display_start_offset, max(0, (int)(list.size())-display_max_rows)); + + + if (highlighted_index < display_start_offset) + display_start_offset = highlighted_index; + else if (highlighted_index >= display_start_offset + display_max_rows) + display_start_offset = highlighted_index - display_max_rows + 1; + } + + void toggleHighlighted() + { + ListEntry *entry = &list[highlighted_index]; + if (!multiselect || !allow_null) + { + int selected_count = 0; + for (size_t i = 0; i < list.size(); i++) + { + if (!multiselect && i != highlighted_index && !entry->selected) + list[i].selected = false; + if (!allow_null && list[i].selected) + selected_count++; + } + + if (!allow_null && entry->selected && selected_count == 1) + return; + } + + entry->selected = !entry->selected; + } + + vector getSelectedElems() + { + vector results; + for (vector< ListEntry >::iterator it = list.begin(); it != list.end(); it++) + { + if ((*it).selected) + results.push_back((*it).elem); + } + + return results; + } + }; + + + class viewscreenChooseMaterial : public dfhack_viewscreen + { + public: + viewscreenChooseMaterial(ItemConstraint *cv = NULL); + void feed(set *input); + void render(); + + std::string getFocusString() { return "wfchoosemat"; } + + private: + ItemConstraint *cv; + + ListColumn items_column; + ListColumn masks_column; + ListColumn materials_column; + vector< ListEntry > all_masks; + + int selected_column; + + void populateItems(); + void populateMasks(const bool set_defaults = false); + void populateMaterials(const bool set_defaults = false); + + bool addMaterialEntry(df::dfhack_material_category &selected_category, + MaterialInfo &material, string name, const bool set_defaults); + + + void changeHighlight(const int highlight_change, const int offset_shift = 0); + void changeColumn(const int amount); + void toggleHighlighted(); + }; + + viewscreenChooseMaterial::viewscreenChooseMaterial(ItemConstraint *cv /*= NULL*/) + { + this->cv = cv; + selected_column = 0; + items_column.title = "Item"; + items_column.allow_null = false; + masks_column.title = "Type"; + masks_column.multiselect = true; + materials_column.title = "Material"; + + populateItems(); + items_column.list[0].selected = true; + + vector raw_masks; + df::dfhack_material_category full_mat_mask, curr_mat_mask; + full_mat_mask.whole = -1; + curr_mat_mask.whole = 1; + bitfield_to_string(&raw_masks, full_mat_mask); + for (int i = 0; i < raw_masks.size(); i++) + { + if (raw_masks[i][0] == '?') + break; + + all_masks.push_back(ListEntry(pad_string(raw_masks[i], MAX_MASK, false), curr_mat_mask)); + curr_mat_mask.whole <<= 1; + } + populateMasks(cv != NULL); + populateMaterials(cv != NULL); + } + + void viewscreenChooseMaterial::populateItems() + { + items_column.clear(); + if (cv != NULL) + { + items_column.add(cv->item.toString(), cv->item); + } + } + + void viewscreenChooseMaterial::populateMasks(const bool set_defaults /*= false */) + { + masks_column.clear(); + + for (vector< ListEntry >::iterator it = all_masks.begin(); it != all_masks.end(); it++) + { + auto entry = *it; + if (set_defaults) + { + if (cv->mat_mask.whole & entry.elem.whole) + entry.selected = true; + } + masks_column.add(entry); + } + } + + void viewscreenChooseMaterial::populateMaterials(const bool set_defaults /*= false */) + { + materials_column.clear(); + df::dfhack_material_category selected_category; + vector selected_materials = masks_column.getSelectedElems(); + if (selected_materials.size() == 1) + selected_category = selected_materials[0]; + else if (selected_materials.size() > 1) + return; + + df::world_raws &raws = world->raws; + for (int i = 1; i < DFHack::MaterialInfo::NUM_BUILTIN; i++) + { + auto obj = raws.mat_table.builtin[i]; + if (obj) + { + MaterialInfo material; + material.decode(i, -1); + addMaterialEntry(selected_category, material, material.toString(), set_defaults); + } + } + + for (size_t i = 0; i < raws.inorganics.size(); i++) + { + df::inorganic_raw *p = raws.inorganics[i]; + MaterialInfo material; + material.decode(0, i); + addMaterialEntry(selected_category, material, material.toString(), set_defaults); + } + + for (size_t i = 0; i < raws.plants.all.size(); i++) + { + df::plant_raw *p = raws.plants.all[i]; + string basename = p->name; + + MaterialInfo material; + material.decode(p->material_defs.type_basic_mat, p->material_defs.idx_basic_mat); + if (!selected_category.whole || material.matches(selected_category)) + { + ListEntry entry(pad_string(basename+" (all)", MAX_MATERIAL, false), material); + if (set_defaults) + { + if (cv->material.matches(material)) + entry.selected = true; + } + materials_column.add(entry); + + for (size_t j = 0; p->material.size() > 1 && j < p->material.size(); j++) + { + MaterialInfo material; + material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i); + if (addMaterialEntry(selected_category, material, basename+" (" + p->material[j]->id + "): " + material.toString(), set_defaults)) + entry.selected = false; + } + } + } + + for (size_t i = 0; i < raws.creatures.all.size(); i++) + { + df::creature_raw *p = raws.creatures.all[i]; + string basename = p->name[0]; + + for (size_t j = 0; j < p->material.size(); j++) + { + MaterialInfo material; + material.decode(DFHack::MaterialInfo::CREATURE_BASE+j, i); + addMaterialEntry(selected_category, material, basename+" (" + p->material[j]->id + "): " + material.toString(), set_defaults); + } + } + } + + + bool viewscreenChooseMaterial::addMaterialEntry(df::dfhack_material_category &selected_category, MaterialInfo &material, + string name, const bool set_defaults) + { + bool selected = false; + if (!selected_category.whole || material.matches(selected_category)) + { + ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); + if (set_defaults) + { + if (cv->material.matches(material)) + { + entry.selected = true; + selected = true; + } + } + materials_column.add(entry); + } + + return selected; + } + + void viewscreenChooseMaterial::feed(set *input) + { + if (input->count(interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + else if (input->count(interface_key::CURSOR_UP)) + { + changeHighlight(-1); + } + else if (input->count(interface_key::CURSOR_DOWN)) + { + changeHighlight(1); + } + else if (input->count(interface_key::CURSOR_LEFT)) + { + changeColumn(-1); + } + else if (input->count(interface_key::CURSOR_RIGHT)) + { + changeColumn(1); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEUP)) + { + changeHighlight(0, -1); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEDOWN)) + { + changeHighlight(0, 1); + } + else if (input->count(interface_key::SELECT)) + { + toggleHighlighted(); + } + } + + void viewscreenChooseMaterial::render() + { + if (Screen::isDismissed(this)) + return; + + dfhack_viewscreen::render(); + + Screen::clear(); + Screen::drawBorder(" Workflow Material "); + + int x = 2; + items_column.display(x, selected_column == 0); + + x += MAX_ITEM_NAME + 1; + masks_column.display(x, selected_column == 1); + + x += MAX_MASK + 1; + materials_column.display(x, selected_column == 2); + + } + + void viewscreenChooseMaterial::changeHighlight(const int highlight_change, const int offset_shift /* = 0 */) + { + switch (selected_column) + { + case 0: + items_column.changeHighlight(highlight_change, offset_shift); + break; + case 1: + masks_column.changeHighlight(highlight_change, offset_shift); + break; + case 2: + materials_column.changeHighlight(highlight_change, offset_shift); + break; + } + } + + void viewscreenChooseMaterial::changeColumn(const int amount) + { + selected_column += amount; + set_to_limit(selected_column, 2); + } + + void viewscreenChooseMaterial::toggleHighlighted() + { + switch (selected_column) + { + case 0: + items_column.toggleHighlighted(); + break; + case 1: + masks_column.toggleHighlighted(); + populateMaterials(false); + break; + case 2: + materials_column.toggleHighlighted(); + break; + } + } + + + /* + * Inventory Monitor + */ + class viewscreenInventoryMonitor : public dfhack_viewscreen, public AdjustmentScreen + { + private: + struct TableRow + { + string texts[INV_MONITOR_COL_COUNT]; + int8_t colors[INV_MONITOR_COL_COUNT]; + + vector< pair > history_plot; + int32_t limit_y, gap_y; + + ItemConstraint *cv; + }; + + + public: + const static int column_widths[]; + const static string column_titles[]; + const static int title_row; + + viewscreenInventoryMonitor(); + + void feed(set *input); + + void render(); + + std::string getFocusString() { return "invmonitor"; } + + virtual void resize(int32_t x, int32_t y) + { + dfhack_viewscreen::resize(x, y); + init(); + } + + virtual void onConstraintChanged(); + + private: + vector rows; + + int bottom_controls_row; + int table_max_rows; + int table_start_offset; + int selected_row; + + int32_t divider_x; + int chart_width, chart_height; + int32_t axis_y_end, axis_y_start, axis_x_start, axis_x_end; + + void init(); + + void changeSelection(const int amount); + + static bool compareConstraints(TableRow const& a, TableRow const& b) + { + return a.texts[0].compare(b.texts[0]) < 0; + } + }; + + const int viewscreenInventoryMonitor::title_row = 2; + + const int viewscreenInventoryMonitor::column_widths[] = + {MAX_ITEM_NAME, 20, 21}; + + const string viewscreenInventoryMonitor::column_titles[] = + {"Item", "Material", "Stock / Limit"}; + + + viewscreenInventoryMonitor::viewscreenInventoryMonitor() + { + adjustment_ui_display_start = 2; + selected_row = 0; + chart_width = SIDEBAR_WIDTH - 2; + + init(); + } + + void viewscreenInventoryMonitor::init() + { + bottom_controls_row = gps->dimy - 2; + table_max_rows = bottom_controls_row - 4; + table_start_offset = 0; + + divider_x = gps->dimx - SIDEBAR_WIDTH - 2; + chart_height = min(SIDEBAR_WIDTH, gps->dimy - 20); + axis_y_end = gps->dimy - 3; + axis_y_start = axis_y_end - chart_height; + axis_x_start = divider_x + 2; + axis_x_end = axis_x_start + chart_width - 1; + + rows.clear(); + for (vector::iterator it = constraints.begin(); it < constraints.end(); it++) + { + TableRow row; + row.cv = *it; + + row.texts[0] = row.cv->item.toString(); + row.colors[0] = COLOR_UNSELECTED; + + row.texts[1] = get_constraint_material(row.cv); + row.colors[1] = COLOR_UNSELECTED; + + string text; + text.append(int_to_string((row.cv->goalByCount()) ? row.cv->item_count : row.cv->item_amount)); + text.append(" "); + text.append((row.cv->goalByCount()) ? "S " : "I "); + text = pad_string(text, 9); + text.append(int_to_string(row.cv->goalCount())); + row.texts[2] = text; + row.colors[2] = COLOR_UNSELECTED; + + if (max_history_days > 0) + { + row.history_plot.clear(); + int max_val = *max_element(row.cv->history.begin(), row.cv->history.end()); + max_val = max(max_val, row.cv->goalCount()); + float scale_y = (float) chart_height / (float) max_val; + float scale_x = (float) chart_width / (float) (max_history_days * 2); + + row.limit_y = axis_y_end - (int) (scale_y * (float) row.cv->goalCount()); + row.gap_y = axis_y_end - (int) (scale_y * (float) (row.cv->goalCount() - row.cv->goalGap())); + + for (size_t i = 0; i < row.cv->history.size(); i++) + { + pair point(axis_x_start + (int) (scale_x * (float) i), + axis_y_end - (int) (scale_y * (float) row.cv->history[i])); + + row.history_plot.push_back(point); + } + } + + rows.push_back(row); + } + + sort(rows.begin(), rows.end(), &viewscreenInventoryMonitor::compareConstraints); + changeSelection(0); + } + + void viewscreenInventoryMonitor::onConstraintChanged() + { + init(); + } + + void viewscreenInventoryMonitor::render() + { + if (Screen::isDismissed(this)) + return; + + dfhack_viewscreen::render(); + + Screen::clear(); + Screen::drawBorder(" Inventory Monitor "); + + Screen::Pen border('\xDB', 8); + for (int32_t y = 1; y < gps->dimy - 1; y++) + { + paintTile(border, divider_x, y); + } + + int32_t x = 2; + int32_t y = title_row; + + + for (int column = 0; column < INV_MONITOR_COL_COUNT; column++) + { + paint_text(COLOR_TITLE, x, y, column_titles[column]); + x += column_widths[column]; + } + + int last_row_able_to_display = table_start_offset + table_max_rows; + for (int row = table_start_offset; row < last_row_able_to_display && row < rows.size(); row++) + { + x = 2; + ++y; + for (int column = 0; column < INV_MONITOR_COL_COUNT; column++) + { + int8_t color = rows[row].colors[column]; + if (column < 2 && row == selected_row) + color = COLOR_SELECTED; + paint_text(color, x, y, rows[row].texts[column]); + x += column_widths[column]; + } + } + + y = bottom_controls_row; + x = 2; + OutputHotkeyString(x, y, "Edit", "e"); + + if (rows.size() > 0) + { + TableRow row = rows[selected_row]; + AdjustmentScreen::render(row.cv); + + if (max_history_days > 0) + { + Screen::Pen y_axis('\xB3', COLOR_BROWN); + x = axis_x_start; + for (y = axis_y_start; y <= axis_y_end; y++) + { + paintTile(y_axis, x, y); + } + + Screen::Pen up_arrow('\xCF', COLOR_BROWN); + paintTile(up_arrow, axis_x_start, axis_y_start-1); + + Screen::Pen x_axis('\xC4', COLOR_BROWN); + y = axis_y_end; + for (x = axis_x_start; x <= axis_x_end; x++) + { + paintTile(x_axis, x, y); + paint_text(COLOR_LIGHTGREEN, x, row.limit_y, "-"); + paint_text(COLOR_GREEN, x, row.gap_y, "-"); + } + + Screen::Pen right_arrow('\xAF', COLOR_BROWN); + paintTile(right_arrow, axis_x_end+1, axis_y_end); + + Screen::Pen zero_axis('\x9E', COLOR_BROWN); + paintTile(zero_axis, axis_x_start, axis_y_end); + + for (size_t i = 0; i < row.history_plot.size(); i++) + { + int x = row.history_plot[i].first; + int y = row.history_plot[i].second; + paint_text(COLOR_CYAN, x, y, "*"); + } + } + } + + + } + + void viewscreenInventoryMonitor::feed(set *input) + { + if (rows.size() > 0 && AdjustmentScreen::feed(input, rows[selected_row].cv)) + return; + + if (input->count(interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + else if (input->count(interface_key::CURSOR_UP)) + { + changeSelection(-1); + } + else if (input->count(interface_key::CURSOR_DOWN)) + { + changeSelection(1); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEUP)) + { + table_start_offset = max(table_start_offset-table_max_rows, 0); + changeSelection(-table_max_rows); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEDOWN)) + { + table_start_offset = min(table_start_offset+table_max_rows, (int)(rows.size())-table_max_rows); + changeSelection(table_max_rows); + } + else if (input->count(interface_key::CUSTOM_E)) + { + Screen::show(new viewscreenChooseMaterial(rows[selected_row].cv)); + } + } + + void viewscreenInventoryMonitor::changeSelection(int amount) + { + selected_row += amount; + set_to_limit(selected_row, rows.size() - 1); + + if (selected_row < table_start_offset) + table_start_offset = selected_row; + else if (selected_row >= table_start_offset + table_max_rows) + table_start_offset = selected_row - table_max_rows + 1; + } + + /****************************** + * Hook for workshop view * + ******************************/ + struct wf_workshop_hook : public df::viewscreen_dwarfmodest + { + typedef df::viewscreen_dwarfmodest interpose_base; + + static color_ostream_proxy console_out; + static df::job *last_job; + static df::job *job; + static AdjustmentScreen dialog; + + bool checkJobSelection() + { + if (!enabled) + return NULL; + + if (!first_update_done) + { + plugin_onupdate(console_out); + } + + job = Gui::getSelectedWorkshopJob(console_out, true); + if (job != last_job) + { + dialog.reset(); + last_job = job; + } + + + return job != NULL; + } + + bool handleInput(set *input) + { + bool key_processed = true; + if (checkJobSelection()) + { + ProtectedJob *pj = get_known(job->id); + if (!pj) + return false; + + ItemConstraint *cv = NULL; + if (pj->constraints.size() > 0) + cv = pj->constraints[0]; + + if (!dialog.feed(input, cv, pj)) + { + if (input->count(interface_key::CUSTOM_T)) + { + if (!cv) + { + // Add tracking + job->flags.bits.repeat = true; + compute_job_outputs(console_out, pj, true); + cv = pj->constraints[0]; + cv->setGoalByCount(false); + cv->setGoalCount(10); + cv->setGoalGap(1); + } + } + else if (input->count(interface_key::CUSTOM_I)) + { + Screen::show(new viewscreenInventoryMonitor()); + } + else + key_processed = false; + } + } + else + key_processed = false; + + return key_processed; + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!handleInput(input)) + INTERPOSE_NEXT(feed)(input); + else + input->clear(); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + if (checkJobSelection()) + { + ProtectedJob *pj = get_known(job->id); + if (!pj) + { + pj = add_known_job(job); + compute_job_outputs(console_out, pj); + } + + ItemConstraint *cv = NULL; + if (pj->constraints.size() > 0) + { + cv = pj->constraints[0]; + } + dialog.render(cv); + ++dialog.y; + OutputHotkeyString(dialog.left_margin, dialog.y, "Inventory Monitor", "i"); + } + } + }; + + color_ostream_proxy wf_workshop_hook::console_out(Core::getInstance().getConsole()); + df::job *wf_workshop_hook::job = NULL; + df::job *wf_workshop_hook::last_job = NULL; + AdjustmentScreen wf_workshop_hook::dialog; + + + IMPLEMENT_VMETHOD_INTERPOSE(wf_workshop_hook, feed); + IMPLEMENT_VMETHOD_INTERPOSE(wf_workshop_hook, render); +} + +#undef INV_MONITOR_COL_COUNT +#undef MAX_ITEM_NAME + +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) +{ + if (!world || !ui) + return CR_FAILURE; + + if (!gps || !INTERPOSE_HOOK(wf_ui::wf_workshop_hook, feed).apply() || !INTERPOSE_HOOK(wf_ui::wf_workshop_hook, render).apply()) + out.printerr("Could not insert Workflow hooks!\n"); + + if (ui_workshop_job_cursor && job_next_id) { + commands.push_back( + PluginCommand( + "workflow", "Manage control of repeat jobs.", + workflow_cmd, false, + " workflow enable [option...]\n" + " workflow disable [option...]\n" + " If no options are specified, enables or disables the plugin.\n" + " Otherwise, enables or disables any of the following options:\n" + " - drybuckets: Automatically empty abandoned water buckets.\n" + " - auto-melt: Resume melt jobs when there are objects to melt.\n" + " workflow jobs\n" + " List workflow-controlled jobs (if in a workshop, filtered by it).\n" + " workflow list\n" + " List active constraints, and their job counts.\n" + " workflow list-commands\n" + " List workflow commands that re-create existing constraints.\n" + " workflow count [cnt-gap]\n" + " workflow amount [cnt-gap]\n" + " Set a constraint. The first form counts each stack as only 1 item.\n" + " workflow unlimit \n" + " Delete a constraint.\n" + " workflow unlimit-all\n" + " Delete all constraints.\n" + "Function:\n" + " - When the plugin is enabled, it protects all repeat jobs from removal.\n" + " If they do disappear due to any cause, they are immediately re-added\n" + " to their workshop and suspended.\n" + " - In addition, when any constraints on item amounts are set, repeat jobs\n" + " that produce that kind of item are automatically suspended and resumed\n" + " as the item amount goes above or below the limit. The gap specifies how\n" + " much below the limit the amount has to drop before jobs are resumed;\n" + " this is intended to reduce the frequency of jobs being toggled.\n" + "Constraint examples:\n" + " workflow amount AMMO:ITEM_AMMO_BOLTS/METAL 1000 100\n" + " workflow amount AMMO:ITEM_AMMO_BOLTS/WOOD,BONE 200 50\n" + " Keep metal bolts within 900-1000, and wood/bone within 150-200.\n" + " workflow count FOOD 120 30\n" + " workflow count DRINK 120 30\n" + " Keep the number of prepared food & drink stacks between 90 and 120\n" + " workflow count BIN 30\n" + " workflow count BARREL 30\n" + " workflow count BOX/CLOTH,SILK,YARN 30\n" + " Make sure there are always 25-30 empty bins/barrels/bags.\n" + " workflow count BAR//COAL 20\n" + " workflow count BAR//COPPER 30\n" + " Make sure there are always 15-20 coal and 25-30 copper bars.\n" + " workflow count CRAFTS//GOLD 20\n" + " Produce 15-20 gold crafts.\n" + " workflow count POWDER_MISC/SAND 20\n" + " workflow count BOULDER/CLAY 20\n" + " Collect 15-20 sand bags and clay boulders.\n" + " workflow amount POWDER_MISC//MUSHROOM_CUP_DIMPLE:MILL 100 20\n" + " Make sure there are always 80-100 units of dimple dye.\n" + " In order for this to work, you have to set the material of\n" + " the PLANT input on the Mill Plants job to MUSHROOM_CUP_DIMPLE\n" + " using the 'job item-material' command.\n" + ) + ); + } + + init_state(out); + + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown (color_ostream &out) +{ + INTERPOSE_HOOK(wf_ui::wf_workshop_hook, feed).remove(); + INTERPOSE_HOOK(wf_ui::wf_workshop_hook, render).remove(); + + cleanup_state(out); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + cleanup_state(out); + init_state(out); + break; + case SC_MAP_UNLOADED: + cleanup_state(out); + break; + default: + break; + } + + return CR_OK; +} +