#include "uicommon.h" #include "listcolumn.h" #include // DF data structure definition headers #include "DataDefs.h" #include "Types.h" #include "df/item.h" #include "df/viewscreen_dwarfmodest.h" #include "df/viewscreen_storesst.h" #include "df/items_other_id.h" #include "df/job.h" #include "df/unit.h" #include "df/world.h" #include "df/item_quality.h" #include "df/caravan_state.h" #include "df/mandate.h" #include "df/general_ref_building_holderst.h" #include "modules/Gui.h" #include "modules/Items.h" #include "modules/Job.h" #include "modules/World.h" #include "modules/Screen.h" #include "modules/Maps.h" #include "modules/Units.h" #include "df/building_cagest.h" #include "df/ui_advmode.h" DFHACK_PLUGIN("stocks"); #define PLUGIN_VERSION 0.13 REQUIRE_GLOBAL(world); DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { return CR_OK; } #define MAX_NAME 30 #define SIDEBAR_WIDTH 30 /* * Utility */ static string get_quality_name(const df::item_quality quality) { if (gps->dimx - SIDEBAR_WIDTH < 60) return int_to_string(quality); else return ENUM_KEY_STR(item_quality, quality); } static df::item *get_container_of(df::item *item) { auto container = Items::getContainer(item); return (container) ? container : item; } static df::item *get_container_of(df::unit *unit) { auto ref = Units::getGeneralRef(unit, general_ref_type::CONTAINED_IN_ITEM); return (ref) ? ref->getItem() : nullptr; } /* * Trade Info */ class TradeDepotInfo { public: TradeDepotInfo() { reset(); } void prepareTradeVariables() { reset(); for(auto bld_it = world->buildings.all.begin(); bld_it != world->buildings.all.end(); bld_it++) { auto bld = *bld_it; if (!isUsableDepot(bld)) continue; depot = bld; id = depot->id; trade_possible = can_trade(); break; } } bool assignItem(vector &entries) { for (auto it = entries.begin(); it != entries.end(); it++) { auto item = *it; item = get_container_of(item); if (!Items::canTradeWithContents(item)) return false; auto href = df::allocate(); if (!href) return false; auto job = new df::job(); df::coord tpos(depot->centerx, depot->centery, depot->z); job->pos = tpos; job->job_type = job_type::BringItemToDepot; // job <-> item link if (!Job::attachJobItem(job, item, df::job_item_ref::Hauled)) { delete job; delete href; return false; } // job <-> building link href->building_id = id; depot->jobs.push_back(job); job->general_refs.push_back(href); // add to job list Job::linkIntoWorld(job); } return true; } void reset() { depot = 0; trade_possible = false; } bool canTrade() { return trade_possible; } private: int32_t id; df::building *depot; bool trade_possible; bool isUsableDepot(df::building* bld) { if (bld->getType() != building_type::TradeDepot) return false; if (bld->getBuildStage() < bld->getMaxBuildStage()) return false; if (bld->jobs.size() == 1 && bld->jobs[0]->job_type == job_type::DestroyBuilding) return false; return true; } }; static TradeDepotInfo depot_info; /* * Item manipulation */ static map items_in_cages; static df::job *get_item_job(df::item *item) { auto ref = Items::getSpecificRef(item, specific_ref_type::JOB); if (ref && ref->data.job) return ref->data.job; return nullptr; } static bool is_marked_for_trade(df::item *item, df::item *container = nullptr) { item = (container) ? container : get_container_of(item); auto job = get_item_job(item); if (!job) return false; return job->job_type == job_type::BringItemToDepot; } static bool is_in_inventory(df::item *item) { item = get_container_of(item); return item->flags.bits.in_inventory; } static bool is_item_in_cage_cache(df::item *item) { return items_in_cages.find(item) != items_in_cages.end(); } static string get_item_label(df::item *item, bool truncate, bool trim) { auto label = Items::getBookTitle(item); if (label == "") { label = Items::getDescription(item, 0, false); } if (trim && item->getType() == item_type::BIN) { auto pos = label.find("<#"); if (pos != string::npos) { label = label.substr(0, pos-1); } } auto wear = item->getWear(); if (wear > 0) { string wearX; switch (wear) { case 1: wearX = "x"; break; case 2: wearX = "X"; break; case 3: wearX = "xX"; break; default: wearX = "XX"; break; } label = wearX + label + wearX; } label = pad_string(label, MAX_NAME, false, truncate); return label; } static string get_keywords(df::item *item) { string keywords; if (item->flags.bits.in_job) keywords += "job "; if (item->flags.bits.rotten) keywords += "rotten "; if (item->flags.bits.owned) keywords += "owned "; if (item->flags.bits.forbid) keywords += "forbid "; if (item->flags.bits.dump) keywords += "dump "; if (item->flags.bits.on_fire) keywords += "fire "; if (item->flags.bits.melt) keywords += "melt "; if (is_item_in_cage_cache(item)) keywords += "caged "; if (is_in_inventory(item)) keywords += "inventory "; if (depot_info.canTrade()) { if (is_marked_for_trade(item)) keywords += "trade "; } return keywords + get_item_label(item, false, false); } struct item_grouped_entry { std::vector entries; string getLabel(bool grouped) const { if (entries.size() == 0) return ""; return get_item_label(entries[0], true, grouped); } string getKeywords() const { return get_keywords(entries[0]); } df::item *getFirstItem() const { if (entries.size() == 0) return nullptr; return entries[0]; } bool canMelt() const { df::item *item = getFirstItem(); if (!item) return false; return can_melt(item); } bool isSetToMelt() const { df::item *item = getFirstItem(); if (!item) return false; return is_set_to_melt(item); } bool contains(df::item *item) const { return std::find(entries.begin(), entries.end(), item) != entries.end(); } void setFlags(const df::item_flags flags, const bool state) { for (auto it = entries.begin(); it != entries.end(); it++) { if (state) (*it)->flags.whole |= flags.whole; else (*it)->flags.whole &= ~flags.whole; } } bool isSingleItem() { return entries.size() == 1; } }; struct extra_filters { bool hide_trade_marked, hide_in_inventory, hide_in_cages; extra_filters() { reset(); } void reset() { hide_in_inventory = false; hide_trade_marked = false; } }; static bool cages_populated = false; static vector cages; static void find_cages() { if (cages_populated) return; for (size_t b=0; b < world->buildings.all.size(); b++) { df::building* building = world->buildings.all[b]; if (building->getType() == building_type::Cage) { cages.push_back(static_cast(building)); } } cages_populated = true; } static df::building_cagest *is_in_cage(df::unit *unit) { find_cages(); for (auto it = cages.begin(); it != cages.end(); it++) { auto cage = *it; for (size_t c = 0; c < cage->assigned_units.size(); c++) { if(cage->assigned_units[c] == unit->id) return cage; } } return nullptr; } template class StockListColumn : public ListColumn { virtual void display_extras(const T &item_group, int32_t &x, int32_t &y) const { auto item = item_group->getFirstItem(); if (item->flags.bits.in_job) OutputString(COLOR_LIGHTBLUE, x, y, "J"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (item->flags.bits.rotten) OutputString(COLOR_CYAN, x, y, "X"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (item->flags.bits.owned) OutputString(COLOR_GREEN, x, y, "O"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (item->flags.bits.forbid) OutputString(COLOR_RED, x, y, "F"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (item->flags.bits.dump) OutputString(COLOR_LIGHTMAGENTA, x, y, "D"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (item->flags.bits.on_fire) OutputString(COLOR_LIGHTRED, x, y, "R"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (item->flags.bits.melt) OutputString(COLOR_BLUE, x, y, "M"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (is_in_inventory(item)) OutputString(COLOR_WHITE, x, y, "I"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (is_item_in_cage_cache(item)) OutputString(COLOR_LIGHTRED, x, y, "C"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); if (depot_info.canTrade()) { if (is_marked_for_trade(item)) OutputString(COLOR_LIGHTGREEN, x, y, "T"); else OutputString(COLOR_LIGHTBLUE, x, y, " "); } if (item->isImproved()) OutputString(COLOR_BLUE, x, y, "* "); else OutputString(COLOR_LIGHTBLUE, x, y, " "); auto quality = static_cast(item->getQuality()); if (quality > item_quality::Ordinary) { auto color = COLOR_BROWN; switch(quality) { case item_quality::FinelyCrafted: color = COLOR_CYAN; break; case item_quality::Superior: color = COLOR_LIGHTBLUE; break; case item_quality::Exceptional: color = COLOR_GREEN; break; case item_quality::Masterful: color = COLOR_LIGHTGREEN; break; case item_quality::Artifact: color = COLOR_BLUE; break; default: break; } OutputString(color, x, y, get_quality_name(quality)); } } virtual bool validSearchInput (unsigned char c) { switch (c) { case '(': case ')': return true; break; default: break; } string &search_string = ListColumn::search_string; if (c == '^' && !search_string.size()) return true; else if (c == '$' && search_string.size()) { if (search_string == "^") return false; if (search_string[search_string.size() - 1] != '$') return true; } return ListColumn::validSearchInput(c); } std::string getRawSearch(const std::string s) { string raw_search = s; if (raw_search.size() && raw_search[0] == '^') raw_search.erase(0, 1); if (raw_search.size() && raw_search[raw_search.size() - 1] == '$') raw_search.erase(raw_search.size() - 1, 1); return toLower(raw_search); } virtual void tokenizeSearch (vector *dest, const string search) { string raw_search = getRawSearch(search); ListColumn::tokenizeSearch(dest, raw_search); } virtual bool showEntry (const ListEntry *entry, const vector &search_tokens) { string &search_string = ListColumn::search_string; if (!search_string.size()) return true; bool match_start = false, match_end = false; string raw_search = getRawSearch(search_string); if (search_string.size() && search_string[0] == '^') match_start = true; if (search_string.size() && search_string[search_string.size() - 1] == '$') match_end = true; if (!ListColumn::showEntry(entry, search_tokens)) return false; string item_name = toLower(Items::getBookTitle(entry->elem->entries[0])); if (item_name == "") { item_name = toLower(Items::getDescription(entry->elem->entries[0], 0, false)); } if ((match_start || match_end) && raw_search.size() > item_name.size()) return false; if (match_start && item_name.compare(0, raw_search.size(), raw_search) != 0) return false; if (match_end && item_name.compare(item_name.size() - raw_search.size(), raw_search.size(), raw_search) != 0) return false; return true; } }; class search_help : public dfhack_viewscreen { public: void feed (std::set *input) { if (input->count(interface_key::HELP)) return; if (Screen::isDismissed(this)) return; Screen::dismiss(this); if (!input->count(interface_key::LEAVESCREEN) && !input->count(interface_key::SELECT)) parent->feed(input); } void render() { static std::string text = "\7 Flag names can be\n" " searched for - e.g. job,\n" " inventory, dump, forbid\n" "\n" "\7 Use ^ to match the start\n" " of a name, and/or $ to\n" " match the end of a name"; if (Screen::isDismissed(this)) return; parent->render(); int left_margin = gps->dimx - SIDEBAR_WIDTH; int x = left_margin, y = 2; Screen::fillRect(Screen::Pen(' ', 0, 0), left_margin - 1, 1, gps->dimx - 2, gps->dimy - 4); Screen::fillRect(Screen::Pen(' ', 0, 0), left_margin - 1, 1, left_margin - 1, gps->dimy - 2); OutputString(COLOR_WHITE, x, y, "Search help", true, left_margin); ++y; vector lines; split_string(&lines, text, "\n"); for (auto line = lines.begin(); line != lines.end(); ++line) OutputString(COLOR_WHITE, x, y, line->c_str(), true, left_margin); } std::string getFocusString() { return "stocks_view/search_help"; } }; class ViewscreenStocks : public dfhack_viewscreen { public: static df::item_flags hide_flags; static extra_filters extra_hide_flags; ViewscreenStocks(df::building_stockpilest *sp = NULL) : sp(sp) { is_grouped = true; selected_column = 0; items_column.multiselect = false; items_column.auto_select = true; items_column.allow_search = true; items_column.left_margin = 2; items_column.bottom_margin = 1; items_column.search_margin = gps->dimx - SIDEBAR_WIDTH; items_column.changeHighlight(0); apply_to_all = false; hide_unflagged = false; checked_flags.bits.in_job = true; checked_flags.bits.rotten = true; checked_flags.bits.owned = true; checked_flags.bits.forbid = true; checked_flags.bits.dump = true; checked_flags.bits.on_fire = true; checked_flags.bits.melt = true; checked_flags.bits.on_fire = true; min_quality = item_quality::Ordinary; max_quality = item_quality::Artifact; min_wear = 0; cages.clear(); items_in_cages.clear(); cages_populated = false; last_selected_item = nullptr; populateItems(); items_column.selectDefaultEntry(); } static void reset() { hide_flags.whole = 0; extra_hide_flags.reset(); depot_info.reset(); } void feed(set *input) { if (input->count(interface_key::LEAVESCREEN)) { input->clear(); Screen::dismiss(this); return; } else if (input->count(interface_key::HELP)) { Screen::show(dts::make_unique(), plugin_self); } bool key_processed = false; switch (selected_column) { case 0: key_processed = items_column.feed(input); break; } if (key_processed) return; if (input->count(interface_key::CUSTOM_CTRL_J)) { hide_flags.bits.in_job = !hide_flags.bits.in_job; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_X)) { hide_flags.bits.rotten = !hide_flags.bits.rotten; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_O)) { hide_flags.bits.owned = !hide_flags.bits.owned; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_F)) { hide_flags.bits.forbid = !hide_flags.bits.forbid; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_D)) { hide_flags.bits.dump = !hide_flags.bits.dump; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_E)) { hide_flags.bits.on_fire = !hide_flags.bits.on_fire; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_M)) { hide_flags.bits.melt = !hide_flags.bits.melt; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_I)) { extra_hide_flags.hide_in_inventory = !extra_hide_flags.hide_in_inventory; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_C)) { extra_hide_flags.hide_in_cages = !extra_hide_flags.hide_in_cages; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_T)) { extra_hide_flags.hide_trade_marked = !extra_hide_flags.hide_trade_marked; populateItems(); } else if (input->count(interface_key::CUSTOM_CTRL_N)) { hide_unflagged = !hide_unflagged; populateItems(); } else if (input->count(interface_key::CUSTOM_SHIFT_C)) { setAllFlags(true); populateItems(); } else if (input->count(interface_key::CUSTOM_SHIFT_E)) { setAllFlags(false); populateItems(); } else if (input->count(interface_key::CHANGETAB)) { is_grouped = !is_grouped; populateItems(); items_column.centerSelection(); } else if (input->count(interface_key::SECONDSCROLL_UP)) { if (min_quality > item_quality::Ordinary) { min_quality = static_cast(static_cast(min_quality) - 1); populateItems(); } } else if (input->count(interface_key::SECONDSCROLL_DOWN)) { if (min_quality < max_quality && min_quality < item_quality::Artifact) { min_quality = static_cast(static_cast(min_quality) + 1); populateItems(); } } else if (input->count(interface_key::SECONDSCROLL_PAGEUP)) { if (max_quality > min_quality && max_quality > item_quality::Ordinary) { max_quality = static_cast(static_cast(max_quality) - 1); populateItems(); } } else if (input->count(interface_key::SECONDSCROLL_PAGEDOWN)) { if (max_quality < item_quality::Artifact) { max_quality = static_cast(static_cast(max_quality) + 1); populateItems(); } } else if (input->count(interface_key::CUSTOM_SHIFT_W)) { ++min_wear; if (min_wear > 3) min_wear = 0; populateItems(); } else if (input->count(interface_key::CUSTOM_SHIFT_Z)) { input->clear(); auto item_group = items_column.getFirstSelectedElem(); if (!item_group) return; if (is_grouped && !item_group->isSingleItem()) return; auto item = item_group->getFirstItem(); auto pos = getRealPos(item); if (!isRealPos(pos)) return; Screen::dismiss(this); auto vs = Gui::getCurViewscreen(true); while (vs && !virtual_cast(vs)) { Screen::dismiss(vs); vs = vs->parent; } // Could be clever here, if item is in a container, to look inside the container. // But that's different for built containers vs bags/pots in stockpiles. send_key(interface_key::D_LOOK); move_cursor(pos); } else if (input->count(interface_key::CUSTOM_SHIFT_A)) { apply_to_all = !apply_to_all; } else if (input->count(interface_key::CUSTOM_SHIFT_D)) { df::item_flags flags; flags.bits.dump = true; toggleFlag(flags); populateItems(); } else if (input->count(interface_key::CUSTOM_SHIFT_F)) { df::item_flags flags; flags.bits.forbid = true; toggleFlag(flags); populateItems(); } else if (input->count(interface_key::CUSTOM_SHIFT_M)) { toggleMelt(); populateItems(); } else if (input->count(interface_key::CUSTOM_SHIFT_T)) { if (depot_info.canTrade()) { auto selected = getSelectedItems(); for (auto it = selected.begin(); it != selected.end(); it++) { depot_info.assignItem((*it)->entries); } } } else if (input->count(interface_key::CURSOR_LEFT)) { --selected_column; validateColumn(); } else if (input->count(interface_key::CURSOR_RIGHT)) { selected_column++; validateColumn(); } else if (enabler->tracking_on && enabler->mouse_lbut) { if (items_column.setHighlightByMouse()) selected_column = 0; enabler->mouse_lbut = enabler->mouse_rbut = 0; } } void move_cursor(const df::coord &pos) { Gui::setCursorCoords(pos.x, pos.y, pos.z); Gui::refreshSidebar(); } void send_key(const df::interface_key &key) { set< df::interface_key > keys; keys.insert(key); Gui::getCurViewscreen(true)->feed(&keys); } void render() { if (Screen::isDismissed(this)) return; dfhack_viewscreen::render(); Screen::clear(); Screen::drawBorder(" Stocks "); items_column.display(selected_column == 0); int32_t y = 1; auto left_margin = gps->dimx - SIDEBAR_WIDTH; int32_t x = left_margin - 2; Screen::Pen border('\xDB', 8); for (; y < gps->dimy - 1; y++) { paintTile(border, x, y); } y = 2; x = left_margin; OutputString(COLOR_BROWN, x, y, "Filters ", false, left_margin); OutputString(COLOR_LIGHTRED, x, y, "(Ctrl+Key toggles)", true, left_margin); OutputFilterString(x, y, "In Job ", "J", !hide_flags.bits.in_job, false, left_margin, COLOR_LIGHTBLUE); OutputFilterString(x, y, "Rotten", "X", !hide_flags.bits.rotten, true, left_margin, COLOR_CYAN); OutputFilterString(x, y, "Owned ", "O", !hide_flags.bits.owned, false, left_margin, COLOR_GREEN); OutputFilterString(x, y, "Forbidden", "F", !hide_flags.bits.forbid, true, left_margin, COLOR_RED); OutputFilterString(x, y, "Dump ", "D", !hide_flags.bits.dump, false, left_margin, COLOR_LIGHTMAGENTA); OutputFilterString(x, y, "On Fire", "E", !hide_flags.bits.on_fire, true, left_margin, COLOR_LIGHTRED); OutputFilterString(x, y, "Melt ", "M", !hide_flags.bits.melt, false, left_margin, COLOR_BLUE); OutputFilterString(x, y, "In Inventory", "I", !extra_hide_flags.hide_in_inventory, true, left_margin, COLOR_WHITE); OutputFilterString(x, y, "Caged ", "C", !extra_hide_flags.hide_in_cages, false, left_margin, COLOR_LIGHTRED); OutputFilterString(x, y, "Trade", "T", !extra_hide_flags.hide_trade_marked, true, left_margin, COLOR_LIGHTGREEN); OutputFilterString(x, y, "No Flags", "N", !hide_unflagged, true, left_margin, COLOR_GREY); if (gps->dimy > 26) ++y; OutputHotkeyString(x, y, "Clear All", "Shift-C", true, left_margin); OutputHotkeyString(x, y, "Enable All", "Shift-E", true, left_margin); OutputHotkeyString(x, y, "Toggle Grouping", interface_key::CHANGETAB, true, left_margin); ++y; OutputHotkeyString(x, y, "Min Qual: ", "-+"); OutputString(COLOR_BROWN, x, y, get_quality_name(min_quality), true, left_margin); OutputHotkeyString(x, y, "Max Qual: ", "/*"); OutputString(COLOR_BROWN, x, y, get_quality_name(max_quality), true, left_margin); OutputHotkeyString(x, y, "Min Wear: ", "Shift-W"); OutputString(COLOR_BROWN, x, y, int_to_string(min_wear), true, left_margin); if (gps->dimy > 27) ++y; OutputString(COLOR_BROWN, x, y, "Actions ("); OutputString(COLOR_LIGHTGREEN, x, y, int_to_string(items_column.getDisplayedListSize())); OutputString(COLOR_BROWN, x, y, " Items)", true, left_margin); OutputHotkeyString(x, y, "Zoom ", "Shift-Z", false, left_margin); OutputHotkeyString(x, y, "Dump", "-D", true, left_margin); OutputHotkeyString(x, y, "Forbid ", "Shift-F", false, left_margin); OutputHotkeyString(x, y, "Melt", "-M", true, left_margin); OutputHotkeyString(x, y, "Mark for Trade", "Shift-T", true, left_margin, depot_info.canTrade() ? COLOR_WHITE : COLOR_DARKGREY); OutputHotkeyString(x, y, "Apply to: ", "Shift-A"); OutputString(COLOR_BROWN, x, y, (apply_to_all) ? "Listed" : "Selected", true, left_margin); y = gps->dimy - 4; OutputHotkeyString(x, y, "Search help", interface_key::HELP, true, left_margin); } std::string getFocusString() { return "stocks_view"; } df::item *getSelectedItem() override { if (is_grouped) return nullptr; vector items = getSelectedItems(); if (items.size() != 1) return nullptr; if (items[0]->entries.size() != 1) return nullptr; return items[0]->entries[0]; } private: StockListColumn items_column; int selected_column; bool apply_to_all, hide_unflagged; df::item_flags checked_flags; df::item_quality min_quality, max_quality; int16_t min_wear; bool is_grouped; std::list grouped_items_store; df::item *last_selected_item; string last_selected_hash; int last_display_offset; df::building_stockpilest *sp; static bool isRealPos(const df::coord pos) { return pos.x != -30000; } static df::coord getRealPos(df::item *item) { df::coord pos; pos.x = -30000; item = get_container_of(item); if (item->flags.bits.in_inventory) { if (item->flags.bits.in_job) { auto ref = Items::getSpecificRef(item, specific_ref_type::JOB); if (ref && ref->data.job) { if (ref->data.job->job_type == job_type::Eat || ref->data.job->job_type == job_type::Drink) return pos; auto unit = Job::getWorker(ref->data.job); if (unit) return unit->pos; } return pos; } else { auto unit = Items::getHolderUnit(item); if (unit) { if (!Units::isCitizen(unit)) { auto cage_item = get_container_of(unit); if (cage_item) { items_in_cages[item] = true; return cage_item->pos; } auto cage_building = is_in_cage(unit); if (cage_building) { items_in_cages[item] = true; pos.x = cage_building->centerx; pos.y = cage_building->centery; pos.z = cage_building->z; } return pos; } return unit->pos; } return pos; } } return item->pos; } void toggleMelt() { int set_to_melt = -1; auto selected = getSelectedItems(); vector items; for (auto it = selected.begin(); it != selected.end(); it++) { auto item_group = *it; if (set_to_melt == -1) set_to_melt = (item_group->isSetToMelt()) ? 0 : 1; if (set_to_melt) { if (!item_group->canMelt() || item_group->isSetToMelt()) continue; } else if (!item_group->isSetToMelt()) { continue; } items.insert(items.end(), item_group->entries.begin(), item_group->entries.end()); } auto &melting_items = world->items.other[items_other_id::ANY_MELT_DESIGNATED]; for (auto it = items.begin(); it != items.end(); it++) { auto item = *it; if (set_to_melt) { insert_into_vector(melting_items, &df::item::id, item); item->flags.bits.melt = true; } else { for (auto mit = melting_items.begin(); mit != melting_items.end(); mit++) { if (item != *mit) continue; melting_items.erase(mit); item->flags.bits.melt = false; break; } } } } void toggleFlag(const df::item_flags flags) { int state_to_apply = -1; auto selected = getSelectedItems(); for (auto it = selected.begin(); it != selected.end(); it++) { auto grouped_entry = (*it); auto item = grouped_entry->getFirstItem(); if (state_to_apply == -1) state_to_apply = (item->flags.whole & flags.whole) ? 0 : 1; grouped_entry->setFlags(flags, state_to_apply); } } vector getSelectedItems() { vector result; if (apply_to_all) { for (auto it = items_column.getDisplayList().begin(); it != items_column.getDisplayList().end(); it++) { auto item_group = (*it)->elem; if (!item_group) continue; result.push_back(item_group); } } else { auto item_group = items_column.getFirstSelectedElem(); if (item_group) result.push_back(item_group); } return result; } void setAllFlags(bool state) { hide_flags.bits.in_job = state; hide_flags.bits.rotten = state; hide_flags.bits.owned = state; hide_flags.bits.forbid = state; hide_flags.bits.dump = state; hide_flags.bits.on_fire = state; hide_flags.bits.melt = state; hide_flags.bits.on_fire = state; hide_unflagged = state; extra_hide_flags.hide_trade_marked = state; extra_hide_flags.hide_in_inventory = state; extra_hide_flags.hide_in_cages = state; } void populateItems() { items_column.setTitle((is_grouped) ? "Item (count)" : "Item"); preserveLastSelected(); items_column.clear(); df::item_flags bad_flags; bad_flags.whole = 0; bad_flags.bits.hostile = true; bad_flags.bits.trader = true; bad_flags.bits.in_building = true; bad_flags.bits.garbage_collect = true; bad_flags.bits.removed = true; bad_flags.bits.dead_dwarf = true; bad_flags.bits.murder = true; bad_flags.bits.construction = true; depot_info.prepareTradeVariables(); std::vector &items = world->items.other[items_other_id::IN_PLAY]; std::map grouped_items; grouped_items_store.clear(); item_grouped_entry *next_selected_group = nullptr; StockpileInfo spInfo; if (sp) spInfo = StockpileInfo(sp); for (size_t i = 0; i < items.size(); i++) { df::item *item = items[i]; if (item->flags.whole & bad_flags.whole || item->flags.whole & hide_flags.whole) continue; auto container = get_container_of(item); if (container->flags.whole & bad_flags.whole) continue; auto pos = getRealPos(item); if (!isRealPos(pos)) continue; auto designation = Maps::getTileDesignation(pos); if (!designation) continue; if (designation->bits.hidden) continue; // Items in parts of the map not yet revealed bool trade_marked = is_marked_for_trade(item, container); if (extra_hide_flags.hide_trade_marked && trade_marked) continue; bool caged = is_item_in_cage_cache(item); if (extra_hide_flags.hide_in_cages && caged) continue; if (extra_hide_flags.hide_in_inventory && container->flags.bits.in_inventory) continue; if (hide_unflagged && (!(item->flags.whole & checked_flags.whole) && !trade_marked && !caged && !container->flags.bits.in_inventory)) { continue; } auto quality = static_cast(item->getQuality()); if (quality < min_quality || quality > max_quality) continue; auto wear = item->getWear(); if (wear < min_wear) continue; if (spInfo.isValid() && !spInfo.inStockpile(item)) continue; if (is_grouped) { auto hash = getItemHash(item); if (grouped_items.find(hash) == grouped_items.end()) { grouped_items_store.push_back(item_grouped_entry()); grouped_items[hash] = &grouped_items_store.back(); } grouped_items[hash]->entries.push_back(item); if (last_selected_item && !next_selected_group && hash == last_selected_hash) { next_selected_group = grouped_items[hash]; } } else { grouped_items_store.push_back(item_grouped_entry()); auto item_group = &grouped_items_store.back(); item_group->entries.push_back(item); auto label = get_item_label(item, true, false); auto entry = ListEntry(label, item_group, item_group->getKeywords()); items_column.add(entry); if (last_selected_item && !next_selected_group && item == last_selected_item) { next_selected_group = item_group; } } } if (is_grouped) { for (auto groups_iter = grouped_items.begin(); groups_iter != grouped_items.end(); groups_iter++) { auto item_group = groups_iter->second; stringstream label; label << item_group->getLabel(is_grouped); if (!item_group->isSingleItem()) label << " (" << item_group->entries.size() << ")"; auto entry = ListEntry(label.str(), item_group, item_group->getKeywords()); items_column.add(entry); } } items_column.fixWidth(); items_column.filterDisplay(); if (next_selected_group) { items_column.selectItem(next_selected_group); items_column.display_start_offset = last_display_offset; } } string getItemHash(df::item *item) { auto label = get_item_label(item, false, true); auto quality = static_cast(item->getQuality()); auto quality_enum = static_cast(quality); auto quality_string = ENUM_KEY_STR(item_quality, quality_enum); auto hash = label + quality_string + int_to_string(item->flags.whole & checked_flags.whole) + " " + int_to_string(item->hasImprovements()); return hash; } void preserveLastSelected() { last_selected_item = nullptr; auto selected_entry = items_column.getFirstSelectedElem(); if (!selected_entry) return; last_selected_item = selected_entry->getFirstItem(); last_selected_hash = (is_grouped && last_selected_item) ? getItemHash(last_selected_item) : ""; last_display_offset = items_column.display_start_offset; } void validateColumn() { set_to_limit(selected_column, 0); } void resize(int32_t x, int32_t y) { dfhack_viewscreen::resize(x, y); items_column.resize(); items_column.search_margin = gps->dimx - SIDEBAR_WIDTH; } }; df::item_flags ViewscreenStocks::hide_flags; extra_filters ViewscreenStocks::extra_hide_flags; struct stocks_hook : public df::viewscreen_storesst { typedef df::viewscreen_storesst interpose_base; DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) { if (input->count(interface_key::CUSTOM_E)) { Screen::dismiss(this); Screen::show(dts::make_unique(), plugin_self); return; } INTERPOSE_NEXT(feed)(input); } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { INTERPOSE_NEXT(render)(); auto dim = Screen::getWindowSize(); int x = 40; int y = dim.y - 2; OutputHotkeyString(x, y, "Enhanced View", "e", false, 0, COLOR_WHITE, COLOR_LIGHTRED); } }; IMPLEMENT_VMETHOD_INTERPOSE(stocks_hook, feed); IMPLEMENT_VMETHOD_INTERPOSE(stocks_hook, render); struct stocks_stockpile_hook : public df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; bool handleInput(set *input) { if (Gui::inRenameBuilding()) return false; df::building_stockpilest *sp = get_selected_stockpile(); if (!sp) return false; if (input->count(interface_key::CUSTOM_I)) { Screen::show(dts::make_unique(sp), plugin_self); return true; } return false; } DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) { if (!handleInput(input)) INTERPOSE_NEXT(feed)(input); } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { INTERPOSE_NEXT(render)(); df::building_stockpilest *sp = get_selected_stockpile(); if (!sp) return; auto dims = Gui::getDwarfmodeViewDims(); int left_margin = dims.menu_x1 + 1; int x = left_margin; int y = dims.y2 - 4; int links = 0; links += sp->links.give_to_pile.size(); links += sp->links.take_from_pile.size(); links += sp->links.give_to_workshop.size(); links += sp->links.take_from_workshop.size(); if (links + 12 >= y) y = 3; OutputHotkeyString(x, y, "Show Inventory", "i", true, left_margin, COLOR_WHITE, COLOR_LIGHTRED); } }; IMPLEMENT_VMETHOD_INTERPOSE(stocks_stockpile_hook, feed); IMPLEMENT_VMETHOD_INTERPOSE(stocks_stockpile_hook, render); DFHACK_PLUGIN_IS_ENABLED(is_enabled); DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!gps) return CR_FAILURE; if (enable != is_enabled) { if (!INTERPOSE_HOOK(stocks_hook, feed).apply(enable) || !INTERPOSE_HOOK(stocks_hook, render).apply(enable) || !INTERPOSE_HOOK(stocks_stockpile_hook, feed).apply(enable) || !INTERPOSE_HOOK(stocks_stockpile_hook, render).apply(enable)) return CR_FAILURE; is_enabled = enable; } return CR_OK; } static command_result stocks_cmd(color_ostream &out, vector & parameters) { if (!parameters.empty()) { if (toLower(parameters[0])[0] == 'v') { out << "Stocks plugin" << endl << "Version: " << PLUGIN_VERSION << endl; return CR_OK; } else if (toLower(parameters[0])[0] == 's') { Screen::show(dts::make_unique(), plugin_self); return CR_OK; } } return CR_WRONG_USAGE; } DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand( "stocks", "An improved stocks management screen.", stocks_cmd)); ViewscreenStocks::reset(); return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { case SC_MAP_LOADED: ViewscreenStocks::reset(); break; default: break; } return CR_OK; }