#include "uicommon.h" #include // DF data structure definition headers #include "DataDefs.h" #include "Types.h" #include "df/build_req_choice_genst.h" #include "df/build_req_choice_specst.h" #include "df/item.h" #include "df/ui.h" #include "df/ui_build_selector.h" #include "df/viewscreen_dwarfmodest.h" #include "df/items_other_id.h" #include "df/job.h" #include "df/world.h" #include "df/building_constructionst.h" #include "df/building_design.h" #include "modules/Gui.h" #include "modules/Buildings.h" #include "modules/Maps.h" #include "modules/Items.h" #include "TileTypes.h" #include "df/job_item.h" #include "df/dfhack_material_category.h" #include "df/general_ref_building_holderst.h" #include "modules/Job.h" #include "df/building_design.h" #include "df/buildings_other_id.h" #include "modules/World.h" #include "df/building.h" #include "df/building_doorst.h" using df::global::ui; using df::global::ui_build_selector; using df::global::world; DFHACK_PLUGIN("buildingplan"); #define PLUGIN_VERSION 0.12 struct MaterialDescriptor { df::item_type item_type; int16_t item_subtype; int16_t type; int32_t index; bool valid; bool matches(const MaterialDescriptor &a) const { return a.valid && valid && a.type == type && a.index == index && a.item_type == item_type && a.item_subtype == item_subtype; } }; DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { return CR_OK; } #define MAX_MASK 10 #define MAX_MATERIAL 21 #define SIDEBAR_WIDTH 30 static bool show_debugging = false; static bool show_help = false; static void debug(const string &msg) { if (!show_debugging) return; color_ostream_proxy out(Core::getInstance().getConsole()); out << "DEBUG (buildingplan): " << msg << endl; } /* * Material Choice Screen */ static string material_to_string_fn(MaterialInfo m) { return m.toString(); } struct ItemFilter { df::dfhack_material_category mat_mask; vector materials; df::item_quality min_quality; bool decorated_only; ItemFilter() : min_quality(item_quality::Ordinary), decorated_only(false), valid(true) { } bool matchesMask(MaterialInfo &mat) { return (mat_mask.whole) ? mat.matches(mat_mask) : true; } bool matches(const df::dfhack_material_category mask) const { return mask.whole & mat_mask.whole; } bool matches(MaterialInfo &material) const { for (auto it = materials.begin(); it != materials.end(); ++it) if (material.matches(*it)) return true; return false; } bool matches(df::item *item) { if (item->getQuality() < min_quality) return false; if (decorated_only && !item->hasImprovements()) return false; auto imattype = item->getActualMaterial(); auto imatindex = item->getActualMaterialIndex(); auto item_mat = MaterialInfo(imattype, imatindex); return (materials.size() == 0) ? matchesMask(item_mat) : matches(item_mat); } vector getMaterialFilterAsVector() { vector descriptions; transform_(materials, descriptions, material_to_string_fn); if (descriptions.size() == 0) bitfield_to_string(&descriptions, mat_mask); if (descriptions.size() == 0) descriptions.push_back("any"); return descriptions; } string getMaterialFilterAsSerial() { string str; str.append(bitfield_to_string(mat_mask, ",")); str.append("/"); if (materials.size() > 0) { for (size_t i = 0; i < materials.size(); i++) str.append(materials[i].getToken() + ","); if (str[str.size()-1] == ',') str.resize(str.size () - 1); } return str; } bool parseSerializedMaterialTokens(string str) { valid = false; vector tokens; split_string(&tokens, str, "/"); if (tokens.size() > 0 && !tokens[0].empty()) { if (!parseJobMaterialCategory(&mat_mask, tokens[0])) return false; } if (tokens.size() > 1 && !tokens[1].empty()) { vector mat_names; split_string(&mat_names, tokens[1], ","); for (auto m = mat_names.begin(); m != mat_names.end(); m++) { MaterialInfo material; if (!material.find(*m) || !material.isValid()) return false; materials.push_back(material); } } valid = true; return true; } string getMinQuality() { return ENUM_KEY_STR(item_quality, min_quality); } bool isValid() { return valid; } void clear() { mat_mask.whole = 0; materials.clear(); } private: bool valid; }; static MaterialInfo &material_info_identity_fn(MaterialInfo &m) { return m; } class ViewscreenChooseMaterial : public dfhack_viewscreen { public: ViewscreenChooseMaterial(ItemFilter *filter) { selected_column = 0; masks_column.setTitle("Type"); masks_column.multiselect = true; masks_column.allow_search = false; masks_column.left_margin = 2; materials_column.left_margin = MAX_MASK + 3; materials_column.setTitle("Material"); materials_column.multiselect = true; this->filter = filter; masks_column.changeHighlight(0); populateMasks(); populateMaterials(); masks_column.selectDefaultEntry(); materials_column.selectDefaultEntry(); materials_column.changeHighlight(0); } void feed(set *input) { bool key_processed = false; switch (selected_column) { case 0: key_processed = masks_column.feed(input); if (input->count(interface_key::SELECT)) populateMaterials(); // Redo materials lists based on category selection break; case 1: key_processed = materials_column.feed(input); break; } if (key_processed) return; if (input->count(interface_key::LEAVESCREEN)) { input->clear(); Screen::dismiss(this); return; } if (input->count(interface_key::CUSTOM_SHIFT_C)) { filter->clear(); masks_column.clearSelection(); materials_column.clearSelection(); populateMaterials(); } else if (input->count(interface_key::SEC_SELECT)) { // Convert list selections to material filters filter->mat_mask.whole = 0; filter->materials.clear(); // Category masks auto masks = masks_column.getSelectedElems(); for (auto it = masks.begin(); it != masks.end(); ++it) filter->mat_mask.whole |= it->whole; // Specific materials auto materials = materials_column.getSelectedElems(); transform_(materials, filter->materials, material_info_identity_fn); Screen::dismiss(this); } 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 (masks_column.setHighlightByMouse()) selected_column = 0; else if (materials_column.setHighlightByMouse()) selected_column = 1; enabler->mouse_lbut = enabler->mouse_rbut = 0; } } void render() { if (Screen::isDismissed(this)) return; dfhack_viewscreen::render(); Screen::clear(); Screen::drawBorder(" Building Material "); masks_column.display(selected_column == 0); materials_column.display(selected_column == 1); int32_t y = gps->dimy - 3; int32_t x = 2; OutputHotkeyString(x, y, "Toggle", "Enter"); x += 3; OutputHotkeyString(x, y, "Save", "Shift-Enter"); x += 3; OutputHotkeyString(x, y, "Clear", "C"); x += 3; OutputHotkeyString(x, y, "Cancel", "Esc"); } std::string getFocusString() { return "buildingplan_choosemat"; } private: ListColumn masks_column; ListColumn materials_column; int selected_column; ItemFilter *filter; df::building_type btype; void addMaskEntry(df::dfhack_material_category &mask, const string &text) { auto entry = ListEntry(pad_string(text, MAX_MASK, false), mask); if (filter->matches(mask)) entry.selected = true; masks_column.add(entry); } void populateMasks() { masks_column.clear(); df::dfhack_material_category mask; mask.whole = 0; mask.bits.stone = true; addMaskEntry(mask, "Stone"); mask.whole = 0; mask.bits.wood = true; addMaskEntry(mask, "Wood"); mask.whole = 0; mask.bits.metal = true; addMaskEntry(mask, "Metal"); mask.whole = 0; mask.bits.soap = true; addMaskEntry(mask, "Soap"); masks_column.filterDisplay(); } void populateMaterials() { materials_column.clear(); df::dfhack_material_category selected_category; vector selected_masks = masks_column.getSelectedElems(); if (selected_masks.size() == 1) selected_category = selected_masks[0]; else if (selected_masks.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()); } } 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()); } decltype(selected_category) wood_flag; wood_flag.bits.wood = true; if (!selected_category.whole || selected_category.bits.wood) { for (size_t i = 0; i < raws.plants.all.size(); i++) { df::plant_raw *p = raws.plants.all[i]; for (size_t j = 0; p->material.size() > 1 && j < p->material.size(); j++) { auto t = p->material[j]; if (p->material[j]->id != "WOOD") continue; MaterialInfo material; material.decode(DFHack::MaterialInfo::PLANT_BASE+j, i); auto name = material.toString(); ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); if (filter->matches(material)) entry.selected = true; materials_column.add(entry); } } } materials_column.sort(); } void addMaterialEntry(df::dfhack_material_category &selected_category, MaterialInfo &material, string name) { if (!selected_category.whole || material.matches(selected_category)) { ListEntry entry(pad_string(name, MAX_MATERIAL, false), material); if (filter->matches(material)) entry.selected = true; materials_column.add(entry); } } void validateColumn() { set_to_limit(selected_column, 1); } void resize(int32_t x, int32_t y) { dfhack_viewscreen::resize(x, y); masks_column.resize(); materials_column.resize(); } }; static void delete_item_fn(df::job_item *x) { delete x; } // START Planning class PlannedBuilding { public: PlannedBuilding(df::building *building, ItemFilter *filter) { this->building = building; this->filter = *filter; pos = df::coord(building->centerx, building->centery, building->z); config = DFHack::World::AddPersistentData("buildingplan/constraints"); config.val() = filter->getMaterialFilterAsSerial(); config.ival(1) = building->id; config.ival(2) = filter->min_quality + 1; config.ival(3) = static_cast(filter->decorated_only) + 1; } PlannedBuilding(PersistentDataItem &config, color_ostream &out) { this->config = config; if (!filter.parseSerializedMaterialTokens(config.val())) { out.printerr("Buildingplan: Cannot parse filter: %s\nDiscarding.", config.val().c_str()); return; } building = df::building::find(config.ival(1)); if (!building) return; pos = df::coord(building->centerx, building->centery, building->z); filter.min_quality = static_cast(config.ival(2) - 1); filter.decorated_only = config.ival(3) - 1; } df::building_type getType() { return building->getType(); } bool assignClosestItem(vector *items_vector) { decltype(items_vector->begin()) closest_item; int32_t closest_distance = -1; for (auto item_iter = items_vector->begin(); item_iter != items_vector->end(); item_iter++) { auto item = *item_iter; if (!filter.matches(item)) continue; auto pos = item->pos; auto distance = abs(pos.x - building->centerx) + abs(pos.y - building->centery) + abs(pos.z - building->z) * 50; if (closest_distance > -1 && distance >= closest_distance) continue; closest_distance = distance; closest_item = item_iter; } if (closest_distance > -1 && assignItem(*closest_item)) { debug("Item assigned"); items_vector->erase(closest_item); remove(); return true; } return false; } bool assignItem(df::item *item) { auto ref = df::allocate(); if (!ref) { Core::printerr("Could not allocate general_ref_building_holderst\n"); return false; } ref->building_id = building->id; if (building->jobs.size() != 1) return false; auto job = building->jobs[0]; for_each_(job->job_items, delete_item_fn); job->job_items.clear(); job->flags.bits.suspend = false; bool rough = false; Job::attachJobItem(job, item, df::job_item_ref::Hauled); if (item->getType() == item_type::BOULDER) rough = true; building->mat_type = item->getMaterial(); building->mat_index = item->getMaterialIndex(); job->mat_type = building->mat_type; job->mat_index = building->mat_index; if (building->needsDesign()) { auto act = (df::building_actual *) building; act->design = new df::building_design(); act->design->flags.bits.rough = rough; } return true; } bool isValid() { bool valid = filter.isValid() && building && Buildings::findAtTile(pos) == building && building->getBuildStage() == 0; if (!valid) remove(); return valid; } bool isCurrentlySelectedBuilding() { return isValid() && (building == world->selected_building); } ItemFilter *getFilter() { return &filter; } void remove() { DFHack::World::DeletePersistentData(config); } private: df::building *building; PersistentDataItem config; df::coord pos; ItemFilter filter; }; static map planmode_enabled, saved_planmodes; static void enable_quickfort_fn(pair& pair) { pair.second = true; } class Planner { public: bool in_dummmy_screen; Planner() : quickfort_mode(false), in_dummmy_screen(false) { } bool isPlanableBuilding(const df::building_type type) const { return item_for_building_type.find(type) != item_for_building_type.end(); } void reset(color_ostream &out) { planned_buildings.clear(); std::vector items; DFHack::World::GetPersistentData(&items, "buildingplan/constraints"); for (auto i = items.begin(); i != items.end(); i++) { PlannedBuilding pb(*i, out); if (pb.isValid()) planned_buildings.push_back(pb); } } void initialize() { vector item_names; typedef df::enum_traits item_types; int size = item_types::last_item_value - item_types::first_item_value+1; for (size_t i = 1; i < size; i++) { is_relevant_item_type[(df::item_type) (i-1)] = false; string item_name = toLower(item_types::key_table[i]); string item_name_clean; for (auto c = item_name.begin(); c != item_name.end(); c++) { if (*c == '_') continue; item_name_clean += *c; } item_names.push_back(item_name_clean); } typedef df::enum_traits building_types; size = building_types::last_item_value - building_types::first_item_value+1; for (size_t i = 1; i < size; i++) { auto building_type = (df::building_type) (i-1); if (building_type == building_type::Weapon || building_type == building_type::Floodgate) continue; string building_name = toLower(building_types::key_table[i]); for (size_t j = 0; j < item_names.size(); j++) { if (building_name == item_names[j]) { auto btype = (df::building_type) (i-1); auto itype = (df::item_type) j; item_for_building_type[btype] = itype; default_item_filters[btype] = ItemFilter(); available_item_vectors[itype] = vector(); is_relevant_item_type[itype] = true; if (planmode_enabled.find(btype) == planmode_enabled.end()) { planmode_enabled[btype] = false; } } } } } void addPlannedBuilding(df::building *bld) { PlannedBuilding pb(bld, &default_item_filters[bld->getType()]); planned_buildings.push_back(pb); } void doCycle() { debug("Running Cycle"); if (planned_buildings.size() == 0) return; debug("Planned count: " + int_to_string(planned_buildings.size())); gather_available_items(); for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end();) { if (building_iter->isValid()) { if (show_debugging) debug(string("Trying to allocate ") + enum_item_key_str(building_iter->getType())); auto required_item_type = item_for_building_type[building_iter->getType()]; auto items_vector = &available_item_vectors[required_item_type]; if (items_vector->size() == 0 || !building_iter->assignClosestItem(items_vector)) { debug("Unable to allocate an item"); ++building_iter; continue; } } debug("Removing building plan"); building_iter = planned_buildings.erase(building_iter); } } bool allocatePlannedBuilding(df::building_type type) { coord32_t cursor; if (!Gui::getCursorCoords(cursor.x, cursor.y, cursor.z)) return false; auto newinst = Buildings::allocInstance(cursor.get_coord16(), type); if (!newinst) return false; df::job_item *filter = new df::job_item(); filter->item_type = item_type::NONE; filter->mat_index = 0; filter->flags2.bits.building_material = true; std::vector filters; filters.push_back(filter); if (!Buildings::constructWithFilters(newinst, filters)) { delete newinst; return false; } for (auto iter = newinst->jobs.begin(); iter != newinst->jobs.end(); iter++) { (*iter)->flags.bits.suspend = true; } if (type == building_type::Door) { auto door = virtual_cast(newinst); if (door) door->door_flags.bits.pet_passable = true; } addPlannedBuilding(newinst); return true; } PlannedBuilding *getSelectedPlannedBuilding() { for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end(); building_iter++) { if (building_iter->isCurrentlySelectedBuilding()) { return &(*building_iter); } } return nullptr; } void removeSelectedPlannedBuilding() { getSelectedPlannedBuilding()->remove(); } ItemFilter *getDefaultItemFilterForType(df::building_type type) { return &default_item_filters[type]; } void cycleDefaultQuality(df::building_type type) { auto quality = &getDefaultItemFilterForType(type)->min_quality; *quality = static_cast(*quality + 1); if (*quality == item_quality::Artifact) (*quality) = item_quality::Ordinary; } void enableQuickfortMode() { saved_planmodes = planmode_enabled; for_each_(planmode_enabled, enable_quickfort_fn); quickfort_mode = true; } void disableQuickfortMode() { planmode_enabled = saved_planmodes; quickfort_mode = false; } bool inQuickFortMode() { return quickfort_mode; } private: map item_for_building_type; map default_item_filters; map> available_item_vectors; map is_relevant_item_type; //Needed for fast check when looping over all items bool quickfort_mode; vector planned_buildings; void gather_available_items() { debug("Gather available items"); for (auto iter = available_item_vectors.begin(); iter != available_item_vectors.end(); iter++) { iter->second.clear(); } // Precompute a bitmask with the bad flags 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(artifact); #undef F std::vector &items = world->items.other[items_other_id::IN_PLAY]; for (size_t i = 0; i < items.size(); i++) { df::item *item = items[i]; if (item->flags.whole & bad_flags.whole) continue; df::item_type itype = item->getType(); if (!is_relevant_item_type[itype]) continue; if (itype == item_type::BOX && item->isBag()) continue; //Skip bags if (item->flags.bits.artifact) continue; if (item->flags.bits.in_job || item->isAssignedToStockpile() || item->flags.bits.owned || item->flags.bits.in_chest) { continue; } available_item_vectors[itype].push_back(item); } } }; static Planner planner; static bool is_planmode_enabled(df::building_type type) { if (planmode_enabled.find(type) == planmode_enabled.end()) { return false; } return planmode_enabled[type]; } #define DAY_TICKS 1200 DFhackCExport command_result plugin_onupdate(color_ostream &out) { static decltype(world->frame_counter) last_frame_count = 0; if ((world->frame_counter - last_frame_count) >= DAY_TICKS/2) { last_frame_count = world->frame_counter; planner.doCycle(); } return CR_OK; } //START Viewscreen Hook struct buildingplan_hook : public df::viewscreen_dwarfmodest { //START UI Methods typedef df::viewscreen_dwarfmodest interpose_base; void send_key(const df::interface_key &key) { set< df::interface_key > keys; keys.insert(key); this->feed(&keys); } bool isInPlannedBuildingQueryMode() { return (ui->main.mode == df::ui_sidebar_mode::QueryBuilding || ui->main.mode == df::ui_sidebar_mode::BuildingItems) && planner.getSelectedPlannedBuilding(); } bool isInPlannedBuildingPlacementMode() { return ui->main.mode == ui_sidebar_mode::Build && ui_build_selector && ui_build_selector->stage < 2 && planner.isPlanableBuilding(ui_build_selector->building_type); } bool handleInput(set *input) { if (isInPlannedBuildingPlacementMode()) { auto type = ui_build_selector->building_type; if (input->count(interface_key::CUSTOM_SHIFT_P)) { planmode_enabled[type] = !planmode_enabled[type]; if (!planmode_enabled[type]) { send_key(interface_key::CURSOR_DOWN_Z); send_key(interface_key::CURSOR_UP_Z); planner.in_dummmy_screen = false; } return true; } else if (input->count(interface_key::CUSTOM_P) || input->count(interface_key::CUSTOM_F) || input->count(interface_key::CUSTOM_Q) || input->count(interface_key::CUSTOM_D) || input->count(interface_key::CUSTOM_N)) { show_help = true; } if (is_planmode_enabled(type)) { if (planner.inQuickFortMode() && planner.in_dummmy_screen) { if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT) || input->count(interface_key::LEAVESCREEN)) { planner.in_dummmy_screen = false; send_key(interface_key::LEAVESCREEN); } return true; } if (input->count(interface_key::SELECT)) { if (ui_build_selector->errors.size() == 0 && planner.allocatePlannedBuilding(type)) { send_key(interface_key::CURSOR_DOWN_Z); send_key(interface_key::CURSOR_UP_Z); if (planner.inQuickFortMode()) { planner.in_dummmy_screen = true; } } return true; } else if (input->count(interface_key::CUSTOM_SHIFT_F)) { if (!planner.inQuickFortMode()) { planner.enableQuickfortMode(); } else { planner.disableQuickfortMode(); } } else if (input->count(interface_key::CUSTOM_SHIFT_M)) { Screen::show(new ViewscreenChooseMaterial(planner.getDefaultItemFilterForType(type))); } else if (input->count(interface_key::CUSTOM_SHIFT_Q)) { planner.cycleDefaultQuality(type); } else if (input->count(interface_key::CUSTOM_SHIFT_D)) { planner.getDefaultItemFilterForType(type)->decorated_only = !planner.getDefaultItemFilterForType(type)->decorated_only; } } } else if (isInPlannedBuildingQueryMode()) { if (input->count(interface_key::SUSPENDBUILDING)) { return true; // Don't unsuspend planned buildings } else if (input->count(interface_key::DESTROYBUILDING)) { planner.removeSelectedPlannedBuilding(); // Remove persistent data } } return false; } DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) { if (!handleInput(input)) INTERPOSE_NEXT(feed)(input); } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { bool plannable = isInPlannedBuildingPlacementMode(); if (plannable && is_planmode_enabled(ui_build_selector->building_type)) { if (ui_build_selector->stage < 1) { // No materials but turn on cursor ui_build_selector->stage = 1; } for (auto iter = ui_build_selector->errors.begin(); iter != ui_build_selector->errors.end();) { //FIXME Hide bags if (((*iter)->find("Needs") != string::npos && **iter != "Needs adjacent wall") || (*iter)->find("No access") != string::npos) { iter = ui_build_selector->errors.erase(iter); } else { ++iter; } } } INTERPOSE_NEXT(render)(); auto dims = Gui::getDwarfmodeViewDims(); int left_margin = dims.menu_x1 + 1; int x = left_margin; auto type = ui_build_selector->building_type; if (plannable) { if (planner.inQuickFortMode() && planner.in_dummmy_screen) { Screen::Pen pen(' ',COLOR_BLACK); int y = dims.y1 + 1; Screen::fillRect(pen, x, y, dims.menu_x2, y + 20); ++y; OutputString(COLOR_BROWN, x, y, "Quickfort Placeholder", true, left_margin); OutputString(COLOR_WHITE, x, y, "Enter, Shift-Enter or Esc", true, left_margin); } else { int y = 23; if (show_help) { OutputString(COLOR_BROWN, x, y, "Note: "); OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin); } OutputToggleString(x, y, "Planning Mode", "P", is_planmode_enabled(type), true, left_margin); if (is_planmode_enabled(type)) { OutputToggleString(x, y, "Quickfort Mode", "F", planner.inQuickFortMode(), true, left_margin); auto filter = planner.getDefaultItemFilterForType(type); OutputHotkeyString(x, y, "Min Quality: ", "Q"); OutputString(COLOR_BROWN, x, y, filter->getMinQuality(), true, left_margin); OutputToggleString(x, y, "Decorated Only: ", "D", filter->decorated_only, true, left_margin); OutputHotkeyString(x, y, "Material Filter:", "M", true, left_margin); auto filter_descriptions = filter->getMaterialFilterAsVector(); for (auto it = filter_descriptions.begin(); it != filter_descriptions.end(); ++it) OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin); } else { planner.in_dummmy_screen = false; } } } else if (isInPlannedBuildingQueryMode()) { planner.in_dummmy_screen = false; // Hide suspend toggle option int y = 20; Screen::Pen pen(' ', COLOR_BLACK); Screen::fillRect(pen, x, y, dims.menu_x2, y); auto filter = planner.getSelectedPlannedBuilding()->getFilter(); y = 24; OutputString(COLOR_BROWN, x, y, "Planned Building Filter:", true, left_margin); OutputString(COLOR_BLUE, x, y, filter->getMinQuality(), true, left_margin); if (filter->decorated_only) OutputString(COLOR_BLUE, x, y, "Decorated Only", true, left_margin); OutputString(COLOR_BROWN, x, y, "Materials:", true, left_margin); auto filters = filter->getMaterialFilterAsVector(); for (auto it = filters.begin(); it != filters.end(); ++it) OutputString(COLOR_BLUE, x, y, "*" + *it, true, left_margin); } else { planner.in_dummmy_screen = false; show_help = false; } } }; IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_hook, feed); IMPLEMENT_VMETHOD_INTERPOSE(buildingplan_hook, render); static command_result buildingplan_cmd(color_ostream &out, vector & parameters) { if (!parameters.empty()) { if (parameters.size() == 1 && toLower(parameters[0])[0] == 'v') { out << "Building Plan" << endl << "Version: " << PLUGIN_VERSION << endl; } else if (parameters.size() == 2 && toLower(parameters[0]) == "debug") { show_debugging = (toLower(parameters[1]) == "on"); out << "Debugging " << ((show_debugging) ? "enabled" : "disabled") << endl; } } return CR_OK; } 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) { planner.reset(out); if (!INTERPOSE_HOOK(buildingplan_hook, feed).apply(enable) || !INTERPOSE_HOOK(buildingplan_hook, render).apply(enable)) return CR_FAILURE; is_enabled = enable; } return CR_OK; } DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { commands.push_back( PluginCommand( "buildingplan", "Place furniture before it's built", buildingplan_cmd, false, "Run 'buildingplan debug [on|off]' to toggle debugging, or 'buildingplan version' to query the plugin version.")); planner.initialize(); return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { switch (event) { case SC_MAP_LOADED: planner.reset(out); break; default: break; } return CR_OK; }