diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 34fedd77e..2f3c82f4f 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -129,6 +129,7 @@ if (BUILD_SUPPORTED) # not yet. busy with other crud again... #DFHACK_PLUGIN(versionosd versionosd.cpp) DFHACK_PLUGIN(misery misery.cpp) + DFHACK_PLUGIN(buildingplan buildingplan.cpp) endif() diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp new file mode 100644 index 000000000..8be1c682f --- /dev/null +++ b/plugins/buildingplan.cpp @@ -0,0 +1,1291 @@ +// Auto Material Select + +#include +#include +#include +#include + +#include "Core.h" +#include +#include +#include +#include + + +// DF data structure definition headers +#include "DataDefs.h" +#include "MiscUtils.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/enabler.h" +#include "df/building_design.h" + +#include "modules/Gui.h" +#include "modules/Screen.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" + +using std::map; +using std::string; +using std::vector; + +using namespace DFHack; +using namespace df::enums; + +using df::global::gps; +using df::global::ui; +using df::global::ui_build_selector; +using df::global::world; +using df::global::enabler; + +DFHACK_PLUGIN("buildingplan"); +#define PLUGIN_VERSION 0.2 + +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; + } +}; + +static command_result automaterial_cmd(color_ostream &out, vector & parameters) +{ + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + return CR_OK; +} + +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; + } + else + x += text.length(); +} + +void OutputHotkeyString(int &x, int &y, const char *text, const char *hotkey, bool newline = false, int left_margin = 0, int8_t color = COLOR_WHITE) +{ + OutputString(10, x, y, hotkey); + string display(": "); + display.append(text); + OutputString(color, x, y, display, newline, left_margin); +} + +void OutputToggleString(int &x, int &y, const char *text, const char *hotkey, bool state, bool newline = true, int left_margin = 0, int8_t color = COLOR_WHITE) +{ + OutputHotkeyString(x, y, text, hotkey); + OutputString(COLOR_WHITE, x, y, ": "); + if (state) + OutputString(COLOR_GREEN, x, y, "Enabled", newline, left_margin); + else + OutputString(COLOR_GREY, x, y, "Disabled", newline, left_margin); +} + +struct coord32_t +{ + int32_t x, y, z; + + df::coord get_coord16() const + { + return df::coord(x, y, z); + } +}; + +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(); +} + +static 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); +} + +static string pad_string(string text, const int size, const bool front = true, const bool trim = false) +{ + if (text.length() > size) + { + if (trim && size > 10) + { + text = text.substr(0, size-3); + text.append("..."); + } + return text; + } + + string aligned(size - text.length(), ' '); + if (front) + { + aligned.append(text); + return aligned; + } + else + { + text.append(aligned); + return text; + } +} + + +#define MAX_MASK 10 +#define MAX_MATERIAL 21 + +#define SIDEBAR_WIDTH 30 +#define COLOR_TITLE COLOR_BLUE +#define COLOR_UNSELECTED COLOR_GREY +#define COLOR_SELECTED COLOR_WHITE +#define COLOR_HIGHLIGHTED COLOR_GREEN + + +/* + * 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; + int highlighted_index; + int display_start_offset; + int32_t bottom_margin, search_margin, left_margin; + bool search_entry_mode; + bool multiselect; + bool allow_null; + bool auto_select; + bool force_sort; + + ListColumn() + { + clear(); + left_margin = 2; + bottom_margin = 3; + search_margin = 38; + highlighted_index = 0; + multiselect = false; + allow_null = true; + auto_select = false; + search_entry_mode = false; + force_sort = false; + } + + void clear() + { + list.clear(); + display_list.clear(); + display_start_offset = 0; + max_item_width = 0; + resize(); + } + + void resize() + { + display_max_rows = gps->dimy - 4 - bottom_margin; + } + + void add(ListEntry &entry) + { + list.push_back(entry); + if (entry.text.length() > max_item_width) + max_item_width = entry.text.length(); + } + + void add(const string &text, T &elem) + { + list.push_back(ListEntry(text, elem)); + if (text.length() > max_item_width) + max_item_width = text.length(); + } + + virtual void display_extras(const T &elem, int32_t &x, int32_t &y) const {} + + void display(const bool is_selected_column) const + { + int32_t 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 < display_list.size() && i < last_index_able_to_display; i++) + { + ++y; + UIColor fg_color = (display_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, display_list[i]->text, bg_color); + int x = left_margin + display_list[i]->text.length() + 1; + display_extras(display_list[i]->elem, x, y); + } + + if (is_selected_column) + { + y = gps->dimy - bottom_margin; + int32_t x = search_margin; + OutputHotkeyString(x, y, "Search" ,"S"); + if (!search_string.empty() || search_entry_mode) + { + OutputString(COLOR_WHITE, x, y, ": "); + OutputString(COLOR_WHITE, x, y, search_string); + if (search_entry_mode) + OutputString(COLOR_LIGHTGREEN, x, y, "_"); + } + } + } + + void filter_display() + { + ListEntry *prev_selected = (getDisplayListSize() > 0) ? display_list[highlighted_index] : NULL; + display_list.clear(); + for (size_t i = 0; i < list.size(); i++) + { + if (search_string.empty() || list[i].text.find(search_string) != string::npos) + { + ListEntry *entry = &list[i]; + display_list.push_back(entry); + if (entry == prev_selected) + highlighted_index = display_list.size() - 1; + } + } + changeHighlight(0); + } + + void selectDefaultEntry() + { + for (size_t i = 0; i < display_list.size(); i++) + { + if (display_list[i]->selected) + { + highlighted_index = i; + break; + } + } + } + + void validateHighlight() + { + set_to_limit(highlighted_index, display_list.size() - 1); + + 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; + + if (auto_select || (!allow_null && list.size() == 1)) + display_list[highlighted_index]->selected = true; + } + + void changeHighlight(const int highlight_change, const int offset_shift = 0) + { + if (!initHighlightChange()) + return; + + highlighted_index += highlight_change + offset_shift * display_max_rows; + + display_start_offset += offset_shift * display_max_rows; + set_to_limit(display_start_offset, max(0, (int)(display_list.size())-display_max_rows)); + validateHighlight(); + } + + void setHighlight(const int index) + { + if (!initHighlightChange()) + return; + + highlighted_index = index; + validateHighlight(); + } + + bool initHighlightChange() + { + if (display_list.size() == 0) + return false; + + if (auto_select && !multiselect) + { + for (typename vector< ListEntry >::iterator it = list.begin(); it != list.end(); it++) + { + it->selected = false; + } + } + + return true; + } + + void toggleHighlighted() + { + if (auto_select) + return; + + ListEntry *entry = display_list[highlighted_index]; + if (!multiselect || !allow_null) + { + int selected_count = 0; + for (size_t i = 0; i < list.size(); i++) + { + if (!multiselect && !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(bool only_one = false) + { + vector results; + for (typename vector< ListEntry >::iterator it = list.begin(); it != list.end(); it++) + { + if ((*it).selected) + { + results.push_back(&(*it).elem); + if (only_one) + break; + } + } + + return results; + } + + T* getFirstSelectedElem() + { + vector results = getSelectedElems(true); + if (results.size() == 0) + return NULL; + else + return results[0]; + } + + size_t getDisplayListSize() + { + return display_list.size(); + } + + size_t getBaseListSize() + { + return list.size(); + } + + bool feed(set *input) + { + if (input->count(interface_key::CURSOR_UP)) + { + search_entry_mode = false; + changeHighlight(-1); + } + else if (input->count(interface_key::CURSOR_DOWN)) + { + search_entry_mode = false; + changeHighlight(1); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEUP)) + { + search_entry_mode = false; + changeHighlight(0, -1); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEDOWN)) + { + search_entry_mode = false; + changeHighlight(0, 1); + } + else if (search_entry_mode) + { + // Search query typing mode + + df::interface_key last_token = *input->rbegin(); + if (last_token >= interface_key::STRING_A032 && last_token <= interface_key::STRING_A126) + { + // Standard character + search_string += last_token - ascii_to_enum_offset; + filter_display(); + } + else if (last_token == interface_key::STRING_A000) + { + // Backspace + if (search_string.length() > 0) + { + search_string.erase(search_string.length()-1); + filter_display(); + } + } + else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN)) + { + // ENTER or ESC: leave typing mode + search_entry_mode = false; + } + else if (input->count(interface_key::CURSOR_LEFT) || input->count(interface_key::CURSOR_RIGHT)) + { + // Arrow key pressed. Leave entry mode and allow screen to process key + search_entry_mode = false; + return false; + } + + return true; + } + + // Not in search query typing mode + else if (input->count(interface_key::SELECT) && !auto_select) + { + toggleHighlighted(); + } + else if (input->count(interface_key::CUSTOM_S)) + { + search_entry_mode = true; + } + else if (input->count(interface_key::CUSTOM_SHIFT_S)) + { + search_string.clear(); + filter_display(); + } + else if (enabler->tracking_on && gps->mouse_x != -1 && gps->mouse_y != -1 && enabler->mouse_lbut) + { + return setHighlightByMouse(); + } + else + return false; + + return true; + } + + bool setHighlightByMouse() + { + if (gps->mouse_y >= 3 && gps->mouse_y < display_max_rows + 3 && + gps->mouse_x >= left_margin && gps->mouse_x < left_margin + max_item_width) + { + int new_index = display_start_offset + gps->mouse_y - 3; + if (new_index < display_list.size()) + setHighlight(new_index); + + enabler->mouse_lbut = enabler->mouse_rbut = 0; + + return true; + } + + return false; + } + + static bool compareText(ListEntry const& a, ListEntry const& b) + { + return a.text.compare(b.text) < 0; + } + + void doSort(bool (*function)(ListEntry const&, ListEntry const&)) + { + if (force_sort || list.size() < 100) + std::sort(list.begin(), list.end(), function); + + filter_display(); + } + + virtual void sort() + { + doSort(&compareText); + } + + + private: + vector< ListEntry > list; + vector< ListEntry* > display_list; + string search_string; + int display_max_rows; + int max_item_width; +}; + + +/* + * Material Choice Screen + */ +class ViewscreenChooseMaterial : public dfhack_viewscreen +{ +public: + static bool reset_list; + + ViewscreenChooseMaterial(); + void feed(set *input); + void render(); + + std::string getFocusString() { return "buildingplan_choosemat"; } + +private: + ListColumn masks_column; + ListColumn materials_column; + vector< ListEntry > all_masks; + int selected_column; + + df::building_type btype; + + 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); + + virtual void resize(int32_t x, int32_t y); + + void validateColumn(); +}; + +bool ViewscreenChooseMaterial::reset_list = false; + + +ViewscreenChooseMaterial::ViewscreenChooseMaterial() +{ + selected_column = 0; + masks_column.title = "Type"; + masks_column.multiselect = true; + masks_column.left_margin = 2; + materials_column.left_margin = MAX_MASK + 3; + materials_column.title = "Material"; + + masks_column.changeHighlight(0); + + 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(); + populateMaterials(); + + masks_column.selectDefaultEntry(); + materials_column.selectDefaultEntry(); + materials_column.changeHighlight(0); +} + +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) + { + //TODO + //if (cv->mat_mask.whole & entry.elem.whole) + //entry.selected = true; + } + masks_column.add(entry); + } + masks_column.sort(); +} + +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); + } + + materials_column.sort(); +} + +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; + }*/ + //TODO + } + materials_column.add(entry); + } + + return selected; +} + +void ViewscreenChooseMaterial::feed(set *input) +{ + bool key_processed; + switch (selected_column) + { + case 0: + key_processed = masks_column.feed(input); + if (input->count(interface_key::SELECT)) + populateMaterials(false); + 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; + } + else if (input->count(interface_key::SEC_SELECT)) + { + df::dfhack_material_category mat_mask; + vector selected_masks = masks_column.getSelectedElems(); + for (vector::iterator it = selected_masks.begin(); it != selected_masks.end(); it++) + { + mat_mask.whole |= (*it)->whole; + } + + MaterialInfo *selected_material = materials_column.getFirstSelectedElem(); + MaterialInfo material = (selected_material) ? *selected_material : MaterialInfo(); + + //TODO + + reset_list = true; + 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 ViewscreenChooseMaterial::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, "Save", "Shift-Enter"); + x += 3; + OutputHotkeyString(x, y, "Cancel", "Esc"); +} + +void ViewscreenChooseMaterial::validateColumn() +{ + set_to_limit(selected_column, 1); +} + +void ViewscreenChooseMaterial::resize(int32_t x, int32_t y) +{ + dfhack_viewscreen::resize(x, y); + masks_column.resize(); + materials_column.resize(); +} + + +// START Planning +class PlannedBuilding +{ +public: + PlannedBuilding(df::building *building) : min_quality(item_quality::Ordinary) + { + this->building = building; + pos = df::coord(building->centerx, building->centery, building->z); + } + + 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 pos = (*item_iter)->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)) + { + items_vector->erase(closest_item); + 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; + + auto job = building->jobs[0]; + delete job->job_items[0]; + 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() + { + return building && Buildings::findAtTile(pos) == building; + } + + bool isCurrentlySelectedBuilding() + { + return building == world->selected_building; + } + +private: + df::building *building; + PersistentDataItem config; + df::coord pos; + + vector materials; + df::dfhack_material_category mat_mask; + item_quality::item_quality min_quality; +}; + + +class Planner +{ +public: + bool isPlanableBuilding(const df::building_type type) const + { + return item_for_building_type.find(type) != item_for_building_type.end(); + } + + void reset() + { + planned_buildings.clear(); + for(auto iter = world->buildings.all.begin(); iter != world->buildings.all.end(); iter++) + { + auto bld = *iter; + if (isPlanableBuilding(bld->getType())) + { + if (bld->jobs.size() != 1) + continue; + + auto job = bld->jobs[0]; + if (!job->flags.bits.suspend) + continue; + + if (job->job_items.size() != 1) + continue; + + if (job->job_items[0]->item_type != item_type::NONE) + continue; + + addPlannedBuilding(bld); + } + } + } + + 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]) + { + item_for_building_type[(df::building_type) (i-1)] = (df::item_type) j; + available_item_vectors[(df::item_type) j] = vector(); + is_relevant_item_type[(df::item_type) j] = true; + } + } + } + } + + void addPlannedBuilding(df::building *bld) + { + PlannedBuilding pb(bld); + planned_buildings.push_back(pb); + } + + void doCycle() + { + if (planned_buildings.size() == 0) + return; + + gather_available_items(); + for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end();) + { + if (building_iter->isValid()) + { + 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)) + { + ++building_iter; + continue; + } + } + + 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; + } + + addPlannedBuilding(newinst); + + return true; + } + + bool canUnsuspendSelectedBuilding() + { + for (auto building_iter = planned_buildings.begin(); building_iter != planned_buildings.end(); building_iter++) + { + if (building_iter->isValid() && building_iter->isCurrentlySelectedBuilding()) + { + return false; + } + } + + return true; + } + +private: + map item_for_building_type; + map> available_item_vectors; + map is_relevant_item_type; //Needed for fast check when loopin over all items + + vector planned_buildings; + + void 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(artifact1); +#undef F + + std::vector &items = world->items.other[items_other_id::ANY_FREE]; + + 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.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 map planmode_enabled; +static bool is_planmode_enabled(df::building_type type) +{ + if (planmode_enabled.find(type) == planmode_enabled.end()) + { + planmode_enabled[type] = 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.canUnsuspendSelectedBuilding(); + } + + 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 handle_input(set *input) + { + if (isInPlannedBuildingPlacementMode()) + { + auto type = ui_build_selector->building_type; + if (input->count(interface_key::CUSTOM_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); + } + return true; + } + + if (is_planmode_enabled(type)) + { + 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); + } + + return true; + } + } + } + else if (isInPlannedBuildingQueryMode() && + input->count(interface_key::SUSPENDBUILDING)) + { + return true; // Don't unsuspend planned buildings + } + + return false; + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!handle_input(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; + if (plannable) + { + int y = 23; + + OutputToggleString(x, y, "Planning Mode", "p", is_planmode_enabled(ui_build_selector->building_type), true, left_margin); + + if (is_planmode_enabled(ui_build_selector->building_type)) + { + //OutputHotkeyString(x, y, "Material Filter", "m", true, left_margin); + } + } + else if (isInPlannedBuildingQueryMode()) + { + // Hide suspend toggle option + int y = 20; + Screen::Pen pen(' ', COLOR_BLACK); + Screen::fillRect(pen, x, y, dims.menu_x2, y); + } + } +}; + +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()) + { + out << "Building Plan" << endl << "Version: " << PLUGIN_VERSION << endl; + } + + return CR_OK; +} + + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +{ + if (!gps || !INTERPOSE_HOOK(buildingplan_hook, feed).apply() || !INTERPOSE_HOOK(buildingplan_hook, render).apply()) + out.printerr("Could not insert buildingplan hooks!\n"); + + commands.push_back( + PluginCommand( + "buildingplan", "Place furniture before it's built", + buildingplan_cmd, false, "")); + + 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(); + break; + default: + break; + } + + return CR_OK; +} \ No newline at end of file