diff --git a/CMakeLists.txt b/CMakeLists.txt index 95b80f50c..c7c84e859 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,7 +109,8 @@ IF(UNIX) SET(CMAKE_C_FLAGS "-fvisibility=hidden -m32 -march=i686 -mtune=generic") ELSEIF(MSVC) # for msvc, tell it to always use 8-byte pointers to member functions to avoid confusion - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg /vmm /MP") + SET(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /Od") ENDIF() # use shared libraries for protobuf diff --git a/NEWS b/NEWS index 4813ea265..a054d69a9 100644 --- a/NEWS +++ b/NEWS @@ -12,7 +12,14 @@ DFHack future - autoSyndrome: disable by default - ruby: add df.dfhack_run "somecommand" - magmasource: rename to source, allow water/magma sources/drains - + New plugins: + - buildingplan: Place furniture before it's built + - resume: A plugin to help display and resume suspended constructions conveniently + - dwarfmonitor: Records dwarf activity to measure fort efficiency + - mousequery: Look and poke at the map elements with the mouse. + - autotrade: Automatically send items in marked stockpiles to trade depot, when trading is possible. + - stocks: An improved stocks display screen. + DFHack v0.34.11-r3 Internals: diff --git a/dfhack.init-example b/dfhack.init-example index 2dd380a01..22d2c2972 100644 --- a/dfhack.init-example +++ b/dfhack.init-example @@ -42,6 +42,13 @@ keybinding add Ctrl-Shift-B "adv-bodyswap force" # Context-specific bindings # ############################# +# Stocks plugin +keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show" + +# Workflow +keybinding add Ctrl-W@dwarfmode/QueryBuilding/Some "gui/workflow" +keybinding add Ctrl-I "gui/workflow status" + # q->stockpile; p - copy & paste stockpiles keybinding add Alt-P copystock @@ -93,6 +100,9 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job keybinding add Alt-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow keybinding add Alt-W@overallstatus "gui/workflow status" +# autobutcher front-end +keybinding add Shift-B@pet/List/Unit "gui/autobutcher" + # assign weapon racks to squads so that they can be used keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index c2edc6b4a..638fed3fd 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -123,7 +123,7 @@ if (BUILD_SUPPORTED) DFHACK_PLUGIN(tweak tweak.cpp) DFHACK_PLUGIN(feature feature.cpp) DFHACK_PLUGIN(lair lair.cpp) - DFHACK_PLUGIN(zone zone.cpp) + DFHACK_PLUGIN(zone zone.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(catsplosion catsplosion.cpp) DFHACK_PLUGIN(regrass regrass.cpp) DFHACK_PLUGIN(forceequip forceequip.cpp) @@ -148,7 +148,13 @@ if (BUILD_SUPPORTED) DFHACK_PLUGIN(trueTransformation trueTransformation.cpp) DFHACK_PLUGIN(infiniteSky infiniteSky.cpp) DFHACK_PLUGIN(createitem createitem.cpp) - DFHACK_PLUGIN(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote) + DFHACK_PLUGIN(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote) + DFHACK_PLUGIN(buildingplan buildingplan.cpp) + DFHACK_PLUGIN(resume resume.cpp) + DFHACK_PLUGIN(dwarfmonitor dwarfmonitor.cpp) + DFHACK_PLUGIN(mousequery mousequery.cpp) + DFHACK_PLUGIN(autotrade autotrade.cpp) + DFHACK_PLUGIN(stocks stocks.cpp) endif() diff --git a/plugins/automaterial.cpp b/plugins/automaterial.cpp index 6f613cf0e..3c38df215 100644 --- a/plugins/automaterial.cpp +++ b/plugins/automaterial.cpp @@ -21,9 +21,20 @@ #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 "modules/Gui.h" #include "modules/Screen.h" +#include "modules/Items.h" +#include "modules/Constructions.h" +#include "modules/Buildings.h" +#include "modules/Maps.h" + +#include "TileTypes.h" +#include "df/job_item.h" using std::map; using std::string; @@ -55,14 +66,6 @@ struct MaterialDescriptor } }; -static map last_used_material; -static map last_moved_material; -static map< int16_t, vector > preferred_materials; -static map< int16_t, df::interface_key > hotkeys; -static bool last_used_moved = false; -static bool auto_choose_materials = true; -static bool auto_choose_attempted = true; -static bool revert_to_last_used_type = false; static command_result automaterial_cmd(color_ostream &out, vector & parameters) { @@ -74,6 +77,67 @@ 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); +} + +static string int_to_string(int i) +{ + return static_cast( &(ostringstream() << i))->str(); +} + +//START UI Functions +struct coord32_t +{ + int32_t x, y, z; +}; + +static enum t_box_select_mode {SELECT_FIRST, SELECT_SECOND, SELECT_MATERIALS, AUTOSELECT_MATERIALS} box_select_mode = SELECT_FIRST; +static coord32_t box_first, box_second; +static bool box_select_enabled = false; +static bool show_box_selection = true; +static bool hollow_selection = false; +static deque box_select_materials; + +#define SELECTION_IGNORE_TICKS 10 +static int ignore_selection = SELECTION_IGNORE_TICKS; + +static map last_used_material; +static map last_moved_material; +static map< int16_t, vector > preferred_materials; +static map< int16_t, df::interface_key > hotkeys; +static bool last_used_moved = false; +static bool auto_choose_materials = true; +static bool auto_choose_attempted = true; +static bool revert_to_last_used_type = false; +static bool allow_future_placement = false; + static inline bool in_material_choice_stage() { return Gui::build_selector_hotkey(Core::getTopViewscreen()) && @@ -218,12 +282,367 @@ static bool check_autoselect(MaterialDescriptor &material, bool toggle) } } +static void cancel_box_selection() +{ + if (box_select_mode == SELECT_FIRST) + return; + + box_select_mode = SELECT_FIRST; + box_select_materials.clear(); + if (!show_box_selection) + Gui::setDesignationCoords(-1, -1, -1); +} +//END UI Functions + + +//START Building and Verification +struct building_site +{ + df::coord pos; + bool in_open_air; + + building_site(df::coord pos, bool in_open_air) + { + this->pos = pos; + this->in_open_air = in_open_air; + } + + building_site() {} +}; + +static deque valid_building_sites; +static deque open_air_sites; +static building_site anchor; + +static bool is_orthogonal_to_pending_construction(building_site &site) +{ + for (deque::iterator it = valid_building_sites.begin(); it != valid_building_sites.end(); it++) + { + if ((it->pos.x == site.pos.x && abs(it->pos.y - site.pos.y) == 1) || (it->pos.y == site.pos.y && abs(it->pos.x - site.pos.x) == 1)) + { + site.in_open_air = true; + return true; + } + } + + return false; +} + +static df::building_constructionst *get_construction_on_tile(const df::coord &pos) +{ + auto current = Buildings::findAtTile(pos); + if (current) + return strict_virtual_cast(current); + + return NULL; +} + +static df::tiletype *read_tile_shapes(const df::coord &pos, df::tiletype_shape &shape, df::tiletype_shape_basic &shape_basic) +{ + if (!Maps::isValidTilePos(pos)) + return NULL; + + auto ttype = Maps::getTileType(pos); + + if (!ttype) + return NULL; + + shape = tileShape(*ttype); + shape_basic = tileShapeBasic(shape); + + return ttype; +} + +static bool is_valid_building_site(building_site &site, bool orthogonal_check, bool check_placed_constructions, bool in_future_placement_mode) +{ + df::tiletype_shape shape; + df::tiletype_shape_basic shape_basic; + + auto ttype = read_tile_shapes(site.pos, shape, shape_basic); + if (!ttype) + return false; + + if (shape_basic == tiletype_shape_basic::Open) + { + if (orthogonal_check) + { + // Check if this is a valid tile to have a construction placed orthogonally to it + if (!in_future_placement_mode) + return false; + + df::building_constructionst *cons = get_construction_on_tile(site.pos); + if (cons && cons == construction_type::Floor) + { + site.in_open_air = true; + return true; + } + + return false; + } + + // Stairs can be placed in open space, if they can connect to other stairs + df::tiletype_shape shape_s; + df::tiletype_shape_basic shape_basic_s; + + if (ui_build_selector->building_subtype == construction_type::DownStair || + ui_build_selector->building_subtype == construction_type::UpDownStair) + { + df::coord below(site.pos.x, site.pos.y, site.pos.z - 1); + auto ttype_s = read_tile_shapes(below, shape_s, shape_basic_s); + if (ttype_s) + { + if (shape_s == tiletype_shape::STAIR_UP || shape_s == tiletype_shape::STAIR_UPDOWN) + return true; + } + } + + if (ui_build_selector->building_subtype == construction_type::UpStair || + ui_build_selector->building_subtype == construction_type::UpDownStair) + { + df::coord above(site.pos.x, site.pos.y, site.pos.z + 1); + auto ttype_s = read_tile_shapes(above, shape_s, shape_basic_s); + if (ttype_s) + { + if (shape_s == tiletype_shape::STAIR_DOWN || shape_s == tiletype_shape::STAIR_UPDOWN) + return true; + } + } + + // Check if there is a valid tile orthogonally adjacent + bool valid_orthogonal_tile_found = false; + df::coord orthagonal_pos; + orthagonal_pos.z = site.pos.z; + for (orthagonal_pos.x = site.pos.x-1; orthagonal_pos.x <= site.pos.x+1 && !valid_orthogonal_tile_found; orthagonal_pos.x++) + { + for (orthagonal_pos.y = site.pos.y-1; orthagonal_pos.y <= site.pos.y+1; orthagonal_pos.y++) + { + if ((site.pos.x == orthagonal_pos.x) == (site.pos.y == orthagonal_pos.y)) + continue; + + building_site orthogonal_site(orthagonal_pos, false); + if (is_valid_building_site(orthogonal_site, true, check_placed_constructions, in_future_placement_mode)) + { + valid_orthogonal_tile_found = true; + if (orthogonal_site.in_open_air) + site.in_open_air = true; + break; + } + + } + } + + if (!(valid_orthogonal_tile_found || (check_placed_constructions && is_orthogonal_to_pending_construction(site)))) + { + site.in_open_air = true; + return false; + } + } + else if (orthogonal_check) + { + if (shape != tiletype_shape::RAMP && + shape_basic != tiletype_shape_basic::Floor && + shape_basic != tiletype_shape_basic::Stair) + return false; + } + else + { + auto material = tileMaterial(*ttype); + if (shape == tiletype_shape::RAMP) + { + if (material == tiletype_material::CONSTRUCTION) + return false; + } + else + { + if (shape_basic != tiletype_shape_basic::Floor) + return false; + + if (material == tiletype_material::CONSTRUCTION) + { + // Can build on top of a wall, but not on a constructed floor + df::coord pos_below = site.pos; + pos_below.z--; + if (!Maps::isValidTilePos(pos_below)) + return false; + + auto ttype = Maps::getTileType(pos_below); + if (!ttype) + return false; + + auto shape = tileShape(*ttype); + auto shapeBasic = tileShapeBasic(shape); + if (tileShapeBasic(shape) != tiletype_shape_basic::Wall) + return false; + } + + if (shape == tiletype_shape::TREE) + return false; + + if (material == tiletype_material::FIRE || + material == tiletype_material::POOL || + material == tiletype_material::BROOK || + material == tiletype_material::RIVER || + material == tiletype_material::MAGMA || + material == tiletype_material::DRIFTWOOD || + material == tiletype_material::CAMPFIRE + ) + + return false; + } + } + + if (orthogonal_check) + return true; + + auto designation = Maps::getTileDesignation(site.pos); + if (designation->bits.flow_size > 2) + return false; + + auto current = Buildings::findAtTile(site.pos); + if (current) + return false; + + df::coord2d size(1,1); + return Buildings::checkFreeTiles(site.pos, size, NULL, false, false); +} + + +static bool find_anchor_in_spiral(const df::coord &start) +{ + bool found = false; + + for (anchor.pos.z = start.z; anchor.pos.z > start.z - 4; anchor.pos.z--) + { + int x, y, dx, dy; + x = y = dx = 0; + dy = -1; + const int side = 11; + const int maxI = side*side; + for (int i = 0; i < maxI; i++) + { + if (-side/2 < x && x <= side/2 && -side/2 < y && y <= side/2) + { + anchor.pos.x = start.x + x; + anchor.pos.y = start.y + y; + if (is_valid_building_site(anchor, false, false, false)) + { + found = true; + break; + } + } + + if ((x == y) || ((x < 0) && (x == -y)) || ((x > 0) && (x == 1-y))) + { + int tmp = dx; + dx = -dy; + dy = tmp; + } + + x += dx; + y += dy; + } + + if (found) + break; + } + + return found; +} + +static bool find_valid_building_sites(bool in_future_placement_mode) +{ + valid_building_sites.clear(); + open_air_sites.clear(); + + int xD = (box_second.x > box_first.x) ? 1 : -1; + int yD = (box_second.y > box_first.y) ? 1 : -1; + for (int32_t xB = box_first.x; (xD > 0) ? (xB <= box_second.x) : (xB >= box_second.x); xB += xD) + { + for (int32_t yB = box_first.y; (yD > 0) ? (yB <= box_second.y) : (yB >= box_second.y); yB += yD) + { + if (hollow_selection && !(xB == box_first.x || xB == box_second.x || yB == box_first.y || yB == box_second.y)) + continue; + + building_site site(df::coord(xB, yB, box_second.z), false); + if (is_valid_building_site(site, false, true, in_future_placement_mode)) + valid_building_sites.push_back(site); + else if (site.in_open_air) + { + if (in_future_placement_mode) + valid_building_sites.push_back(site); + else + open_air_sites.push_back(site); + } + } + } + + size_t last_open_air_count = 0; + while (valid_building_sites.size() > 0 && open_air_sites.size() != last_open_air_count) + { + last_open_air_count = open_air_sites.size(); + deque current_open_air_list = open_air_sites; + open_air_sites.clear(); + for (deque::iterator it = current_open_air_list.begin(); it != current_open_air_list.end(); it++) + { + if (is_orthogonal_to_pending_construction(*it)) + valid_building_sites.push_back(*it); + else + open_air_sites.push_back(*it); + } + + } + + return valid_building_sites.size() > 0; +} + +static bool designate_new_construction(df::coord &pos, df::construction_type &type, df::item *item) +{ + auto newinst = Buildings::allocInstance(pos, building_type::Construction, type); + if (!newinst) + return false; + + vector items; + items.push_back(item); + Maps::ensureTileBlock(pos); + + if (!Buildings::constructWithItems(newinst, items)) + { + delete newinst; + return false; + } + + return true; +} +//END Building and Verification + + +//START Viewscreen Hook struct jobutils_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 select_material_at_index(size_t i) + { + ui_build_selector->sel_index = i; + std::set< df::interface_key > keys; + keys.insert(df::interface_key::SELECT_ALL); + this->feed(&keys); + return !in_material_choice_stage(); + } + bool choose_materials() { + if (!auto_choose_materials || get_curr_constr_prefs().size() == 0) + return false; + size_t size = ui_build_selector->choices.size(); for (size_t i = 0; i < size; i++) { @@ -231,31 +650,109 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest size_t j; if (is_material_in_autoselect(j, material)) { - ui_build_selector->sel_index = i; - std::set< df::interface_key > keys; - keys.insert(df::interface_key::SELECT_ALL); - this->feed(&keys); - if (!in_material_choice_stage()) - return true; + return select_material_at_index(i); } } return false; } - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + void draw_box_selection() { + if (!box_select_enabled) + return; + + df::coord vport = Gui::getViewportPos(); + + //Even if selection drawing is disabled, paint a green cursor as we can place box selection anywhere + + if (box_select_mode == SELECT_FIRST || (!show_box_selection && box_select_mode == SELECT_SECOND)) + { + int32_t x, y, z; + + if (!Gui::getCursorCoords(x, y, z)) + return; + + x = x - vport.x + 1; + y = y - vport.y + 1; + OutputString(COLOR_GREEN, x, y, "X"); + } + else if (show_box_selection && box_select_mode == SELECT_SECOND) + { + if (!Gui::getCursorCoords(box_second.x, box_second.y, box_second.z)) + return; + + int32_t xD = (box_second.x > box_first.x) ? 1 : -1; + int32_t yD = (box_second.y > box_first.y) ? 1 : -1; + for (int32_t xB = box_first.x; (xD > 0) ? (xB <= box_second.x) : (xB >= box_second.x); xB += xD) + { + for (int32_t yB = box_first.y; (yD > 0) ? (yB <= box_second.y) : (yB >= box_second.y); yB += yD) + { + if (hollow_selection && !(xB == box_first.x || xB == box_second.x || yB == box_first.y || yB == box_second.y)) + continue; + + int8_t color = (xB == box_second.x && yB == box_second.y) ? COLOR_GREEN : COLOR_BROWN; + + int32_t x = xB - vport.x + 1; + int32_t y = yB - vport.y + 1; + OutputString(color, x, y, "X"); + } + } + } + else if (show_box_selection && box_select_mode == SELECT_MATERIALS) + { + for (deque::iterator it = valid_building_sites.begin(); it != valid_building_sites.end(); it++) + { + int32_t x = it->pos.x - vport.x + 1; + int32_t y = it->pos.y - vport.y + 1; + OutputString(COLOR_GREEN, x, y, "X"); + } + } + } + + void reset_existing_selection() + { + for (int i = 0; i < 10; i++) + { + send_key(df::interface_key::BUILDING_DIM_Y_DOWN); + send_key(df::interface_key::BUILDING_DIM_X_DOWN); + } + } + + void handle_input(set *input) + { + if (ui_build_selector->building_subtype >= 7) + return; + if (in_material_choice_stage()) { + if (input->count(interface_key::LEAVESCREEN)) + { + box_select_mode = SELECT_FIRST; + } + MaterialDescriptor material = get_material_in_list(ui_build_selector->sel_index); if (material.valid) { if (input->count(interface_key::SELECT) || input->count(interface_key::SEC_SELECT)) { if (get_last_moved_material().matches(material)) - last_used_moved = false; + last_used_moved = false; //Keep selected material on top set_last_used_material(material); + + if (box_select_enabled) + { + auto curr_index = ui_build_selector->sel_index; + vector gen_material; + gen_material.push_back(get_material_in_list(curr_index)); + box_select_materials.clear(); + // Populate material list with selected material + populate_box_materials(gen_material, ((input->count(interface_key::SEC_SELECT) && ui_build_selector->is_grouped) ? -1 : 1)); + + input->clear(); // Let the apply_box_selection routine allocate the construction + input->insert(interface_key::LEAVESCREEN); + } } else if (input->count(interface_key::CUSTOM_A)) { @@ -274,39 +771,324 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest { revert_to_last_used_type = !revert_to_last_used_type; } + else if (input->count(interface_key::CUSTOM_B)) + { + reset_existing_selection(); + box_select_enabled = !box_select_enabled; + if (!box_select_enabled) + cancel_box_selection(); + + return; + } + else if (input->count(interface_key::CUSTOM_O)) + { + allow_future_placement = !allow_future_placement; + } + else if (input->count(interface_key::LEAVESCREEN)) + { + switch (box_select_mode) + { + case SELECT_FIRST: + case SELECT_SECOND: + cancel_box_selection(); + + default: + break; + } + } + else if (box_select_enabled) + { + if (input->count(interface_key::SELECT)) + { + switch (box_select_mode) + { + case SELECT_FIRST: + if (!Gui::getCursorCoords(box_first.x, box_first.y, box_first.z)) + { + cancel_box_selection(); + return; + } + box_select_mode = SELECT_SECOND; + if (!show_box_selection) + Gui::setDesignationCoords(box_first.x, box_first.y, box_first.z); + input->clear(); + return; + + case SELECT_SECOND: + if (!Gui::getCursorCoords(box_second.x, box_second.y, box_second.z)) + { + cancel_box_selection(); + return; + } + cancel_box_selection(); + input->clear(); + apply_box_selection(true); + return; + + default: + break; + } + } + else if (input->count(interface_key::CUSTOM_X)) + { + show_box_selection = !show_box_selection; + if (box_select_mode == SELECT_SECOND) + { + if (show_box_selection) + { + Gui::setDesignationCoords(-1, -1, -1); + } + else + { + Gui::setDesignationCoords(box_first.x, box_first.y, box_first.z); + } + } + } + else if (input->count(interface_key::CUSTOM_H)) + { + hollow_selection = !hollow_selection; + } + else if (input->count(interface_key::BUILDING_DIM_Y_UP) || + input->count(interface_key::BUILDING_DIM_Y_DOWN) || + input->count(interface_key::BUILDING_DIM_X_UP) || + input->count(interface_key::BUILDING_DIM_X_DOWN)) + { + input->clear(); + return; + } + } + } + } + //END UI Methods + + //START Building Application + bool populate_box_materials(vector &gen_materials, int32_t count = -1) + { + bool result = false; + + if (gen_materials.size() == 0) + return result; + + if (ui_build_selector->is_grouped) + send_key(interface_key::BUILDING_EXPAND_CONTRACT); + + size_t size = ui_build_selector->choices.size(); + vector::iterator gen_material; + for (size_t i = 0; i < size; i++) + { + if (VIRTUAL_CAST_VAR(spec, df::build_req_choice_specst, ui_build_selector->choices[i])) + { + for (gen_material = gen_materials.begin(); gen_material != gen_materials.end(); gen_material++) + { + if (gen_material->item_type == spec->candidate->getType() && + gen_material->item_subtype == spec->candidate->getSubtype() && + gen_material->type == spec->candidate->getActualMaterial() && + gen_material->index == spec->candidate->getActualMaterialIndex()) + { + box_select_materials.push_back(spec->candidate); + if (count > -1) + return true; // Right now we only support 1 or all materials + + result = true; + break; + } + } + } + } + send_key(interface_key::BUILDING_EXPAND_CONTRACT); + + return result; + } + + void move_cursor(df::coord &pos) + { + Gui::setCursorCoords(pos.x, pos.y, pos.z); + send_key(interface_key::CURSOR_DOWN_Z); + send_key(interface_key::CURSOR_UP_Z); + } + + void move_cursor(coord32_t &pos) + { + df::coord c((int16_t) pos.x, (int16_t) pos.y, (int16_t) pos.z); + move_cursor(c); + } + + void apply_box_selection(bool new_start) + { + static bool saved_revert_setting = false; + static bool auto_select_applied = false; + + box_select_mode = SELECT_MATERIALS; + if (new_start) + { + bool ok_to_continue = false; + bool in_future_placement_mode = false; + if (!find_valid_building_sites(false)) + { + if (allow_future_placement) + { + in_future_placement_mode = find_valid_building_sites(true); + } + } + else + { + ok_to_continue = true; + } + + if (in_future_placement_mode) + { + ok_to_continue = find_anchor_in_spiral(valid_building_sites[0].pos); + } + else if (ok_to_continue) + { + // First valid site is guaranteed to be anchored, either on a tile or against a valid orthogonal tile + // Use it as an anchor point to generate materials list + anchor = valid_building_sites.front(); + valid_building_sites.pop_front(); + valid_building_sites.push_back(anchor); + } + + if (!ok_to_continue) + { + cancel_box_selection(); + hollow_selection = false; + return; + } + + saved_revert_setting = revert_to_last_used_type; + revert_to_last_used_type = true; + auto_select_applied = false; + box_select_materials.clear(); + } + while (valid_building_sites.size() > 0) + { + building_site site = valid_building_sites.front(); + valid_building_sites.pop_front(); + if (box_select_materials.size() > 0) + { + df::construction_type type = (df::construction_type) ui_build_selector->building_subtype; + df::item *item = NULL; + while (box_select_materials.size() > 0) + { + item = box_select_materials.front(); + if (!item->flags.bits.in_job) + break; + box_select_materials.pop_front(); + item = NULL; + } + + if (item != NULL) + { + if (designate_new_construction(site.pos, type, item)) + { + box_select_materials.pop_front(); + box_select_mode = AUTOSELECT_MATERIALS; + send_key(interface_key::LEAVESCREEN); //Must do this to register items in use + send_key(hotkeys[type]); + box_select_mode = SELECT_MATERIALS; + } + continue; + } + } + + // Generate material list using regular construction placement routine + + if (site.in_open_air) + { + // Cannot invoke material selection on an unconnected tile, use anchor instead + move_cursor(anchor.pos); + send_key(df::interface_key::SELECT); + } + + move_cursor(site.pos); + + if (!site.in_open_air) + send_key(df::interface_key::SELECT); + + if (in_material_choice_stage()) + { + valid_building_sites.push_front(site); //Redo current tile with whatever gets selected + if (!auto_select_applied) + { + // See if any auto select materials are available + auto_select_applied = true; + if (auto_choose_materials && populate_box_materials(preferred_materials[ui_build_selector->building_subtype])) + { + continue; + } + } + + last_used_moved = false; + return; // No auto select materials left, ask user + } + } + + // Allocation done, reset + move_cursor(box_second); + + revert_to_last_used_type = saved_revert_setting; + if (!revert_to_last_used_type) + { + send_key(df::interface_key::LEAVESCREEN); + } + + cancel_box_selection(); + hollow_selection = false; + ignore_selection = 0; + } + //END Building Application + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (ignore_selection < SELECTION_IGNORE_TICKS) + { + //FIXME: Sometimes there's an extra ENTER key left over after box selection + ignore_selection = SELECTION_IGNORE_TICKS; + return; + } + + if (box_select_mode != AUTOSELECT_MATERIALS) + handle_input(input); + int16_t last_used_constr_subtype = (in_material_choice_stage()) ? ui_build_selector->building_subtype : -1; INTERPOSE_NEXT(feed)(input); if (revert_to_last_used_type && last_used_constr_subtype >= 0 && - !in_material_choice_stage() && + in_type_choice_stage() && hotkeys.find(last_used_constr_subtype) != hotkeys.end()) { - interface_key_set keys; - keys.insert(hotkeys[last_used_constr_subtype]); - INTERPOSE_NEXT(feed)(&keys); + input->clear(); + input->insert(hotkeys[last_used_constr_subtype]); + INTERPOSE_NEXT(feed)(input); + + if (box_select_mode == SELECT_MATERIALS) + { + apply_box_selection(false); + } } } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { + if (ignore_selection < SELECTION_IGNORE_TICKS) + { + ++ignore_selection; + } + if (in_material_choice_stage()) { if (!last_used_moved && ui_build_selector->is_grouped) { - if (auto_choose_materials && get_curr_constr_prefs().size() > 0) + last_used_moved = true; + if (!box_select_enabled && choose_materials()) { - last_used_moved = true; - if (choose_materials()) - { - return; - } + return; } else { - last_used_moved = true; move_material_to_top(get_last_used_material()); } } @@ -322,47 +1104,84 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest INTERPOSE_NEXT(render)(); + draw_box_selection(); + + if (in_type_choice_stage()) + { + cancel_box_selection(); + return; + } + + auto dims = Gui::getDwarfmodeViewDims(); + int left_margin = dims.menu_x1 + 1; + int x = left_margin; + int y = 25; if (in_material_choice_stage()) { MaterialDescriptor material = get_material_in_list(ui_build_selector->sel_index); if (material.valid) { - string title = "Disabled"; - if (check_autoselect(material, false)) + OutputToggleString(x, y, "Autoselect", "a", check_autoselect(material, false), true, left_margin); + + if (box_select_mode == SELECT_MATERIALS) { - title = "Enabled"; + ++y; + OutputString(COLOR_BROWN, x, y, "Construction:", true, left_margin); + OutputString(COLOR_WHITE, x, y, int_to_string(valid_building_sites.size() + 1) + " tiles to fill", true, left_margin); } - - auto dims = Gui::getDwarfmodeViewDims(); - Screen::Painter dc(dims.menu()); - - dc.seek(1,24).key_pen(COLOR_LIGHTRED).pen(COLOR_WHITE); - dc.key(interface_key::CUSTOM_A).string(": Autoselect "+title); } } - else if (in_placement_stage() && ui_build_selector->building_subtype < construction_type::TrackN) + else if (in_placement_stage() && ui_build_selector->building_subtype < 7) { - string autoselect_toggle = (auto_choose_materials) ? "Disable" : "Enable"; - string revert_toggle = (revert_to_last_used_type) ? "Disable" : "Enable"; + OutputString(COLOR_BROWN, x, y, "DFHack Options", true, left_margin); + OutputToggleString(x, y, "Auto Mat-select", "a", auto_choose_materials, true, left_margin); + OutputToggleString(x, y, "Reselect Type", "t", revert_to_last_used_type, true, left_margin); - auto dims = Gui::getDwarfmodeViewDims(); - Screen::Painter dc(dims.menu()); + ++y; + OutputToggleString(x, y, "Box Select", "b", box_select_enabled, true, left_margin); + if (box_select_enabled) + { + OutputToggleString(x, y, "Show Box Mask", "x", show_box_selection, true, left_margin); + OutputHotkeyString(x, y, (hollow_selection) ? "Make Solid" : "Make Hollow", "h", true, left_margin); + OutputToggleString(x, y, "Open Placement", "o", allow_future_placement, true, left_margin); + } + ++y; + if (box_select_enabled) + { + Screen::Pen pen(' ',COLOR_BLACK); + y = dims.y1 + 2; + Screen::fillRect(pen, x, y, dims.menu_x2, y + 17); - dc.seek(1,23).key_pen(COLOR_LIGHTRED).pen(COLOR_WHITE); - dc.key(interface_key::CUSTOM_A).string(": "+autoselect_toggle+" Auto Mat-Select").newline(1); - dc.key(interface_key::CUSTOM_T).string(": "+revert_toggle+" Auto Type-Select"); + y += 2; + switch (box_select_mode) + { + case SELECT_FIRST: + OutputString(COLOR_BROWN, x, y, "Choose first corner", true, left_margin); + break; + + case SELECT_SECOND: + OutputString(COLOR_GREEN, x, y, "Choose second corner", true, left_margin); + int cx = box_first.x; + int cy = box_first.y; + OutputString(COLOR_BROWN, cx, cy, "X"); + } + + OutputString(COLOR_BROWN, x, ++y, "Ignore Building Restrictions", true, left_margin); + } } } }; +//END Viewscreen Hook + +color_ostream_proxy console_out(Core::getInstance().getConsole()); + IMPLEMENT_VMETHOD_INTERPOSE(jobutils_hook, feed); IMPLEMENT_VMETHOD_INTERPOSE(jobutils_hook, render); DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - if (!gps || !ui_build_selector || - !INTERPOSE_HOOK(jobutils_hook, feed).apply() || - !INTERPOSE_HOOK(jobutils_hook, render).apply()) + if (!gps || !INTERPOSE_HOOK(jobutils_hook, feed).apply() || !INTERPOSE_HOOK(jobutils_hook, render).apply()) out.printerr("Could not insert jobutils hooks!\n"); hotkeys[construction_type::Wall] = df::interface_key::HOTKEY_BUILDING_CONSTRUCTION_WALL; @@ -375,4 +1194,4 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector main.mode != ui_sidebar_mode::QueryBuilding) + { + return nullptr; + } + + return virtual_cast(world->selected_building); +} + +static bool can_trade() +{ + if (df::global::ui->caravans.size() == 0) + return false; + + for (auto it = df::global::ui->caravans.begin(); it != df::global::ui->caravans.end(); it++) + { + auto caravan = *it; + auto trade_state = caravan->trade_state; + auto time_remaining = caravan->time_remaining; + if ((trade_state != 1 && trade_state != 2) || time_remaining == 0) + return false; + } + + return true; +} + +class StockpileInfo { +public: + + StockpileInfo(df::building_stockpilest *sp_) : sp(sp_) + { + readBuilding(); + } + + StockpileInfo(PersistentDataItem &config) + { + this->config = config; + id = config.ival(1); + } + + bool inStockpile(df::item *i) + { + df::item *container = Items::getContainer(i); + if (container) + return inStockpile(container); + + if (i->pos.z != z) return false; + if (i->pos.x < x1 || i->pos.x >= x2 || + i->pos.y < y1 || i->pos.y >= y2) return false; + int e = (i->pos.x - x1) + (i->pos.y - y1) * sp->room.width; + return sp->room.extents[e] == 1; + } + + bool isValid() + { + auto found = df::building::find(id); + return found && found == sp && found->getType() == building_type::Stockpile; + } + + bool load() + { + auto found = df::building::find(id); + if (!found || found->getType() != building_type::Stockpile) + return false; + + sp = virtual_cast(found); + if (!sp) + return false; + + readBuilding(); + + return true; + } + + int32_t getId() + { + return id; + } + + bool matches(df::building_stockpilest* sp) + { + return this->sp == sp; + } + + void save() + { + config = DFHack::World::AddPersistentData("autotrade/stockpiles"); + config.ival(1) = id; + } + + void remove() + { + DFHack::World::DeletePersistentData(config); + } + +private: + PersistentDataItem config; + df::building_stockpilest* sp; + int x1, x2, y1, y2, z; + int32_t id; + + void readBuilding() + { + id = sp->id; + z = sp->z; + x1 = sp->room.x; + x2 = sp->room.x + sp->room.width; + y1 = sp->room.y; + y2 = sp->room.y + sp->room.height; + } +}; + + +/* + * Depot Access + */ + +class TradeDepotInfo +{ +public: + TradeDepotInfo() : depot(0) + { + + } + + bool findDepot() + { + if (isValid()) + return true; + + 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; + break; + } + + return depot; + } + + bool assignItem(df::item *item) + { + 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; + } + +private: + int32_t id; + df::building *depot; + + 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; + } + + bool isValid() + { + if (!depot) + return false; + + auto found = df::building::find(id); + return found && found == depot && isUsableDepot(found); + } + +}; + +static TradeDepotInfo depot_info; + + +/* + * Item Manipulation + */ + +static bool check_mandates(df::item *item) +{ + for (auto it = world->mandates.begin(); it != world->mandates.end(); it++) + { + auto mandate = *it; + + if (mandate->mode != 0) + continue; + + if (item->getType() != mandate->item_type || + (mandate->item_subtype != -1 && item->getSubtype() != mandate->item_subtype)) + continue; + + if (mandate->mat_type != -1 && item->getMaterial() != mandate->mat_type) + continue; + + if (mandate->mat_index != -1 && item->getMaterialIndex() != mandate->mat_index) + continue; + + return false; + } + + return true; +} + +static bool is_valid_item(df::item *item) +{ + for (size_t i = 0; i < item->general_refs.size(); i++) + { + df::general_ref *ref = item->general_refs[i]; + + switch (ref->getType()) + { + case general_ref_type::CONTAINED_IN_ITEM: + return false; + + case general_ref_type::UNIT_HOLDER: + return false; + + case general_ref_type::BUILDING_HOLDER: + return false; + + default: + break; + } + } + + for (size_t i = 0; i < item->specific_refs.size(); i++) + { + df::specific_ref *ref = item->specific_refs[i]; + + if (ref->type == specific_ref_type::JOB) + { + // Ignore any items assigned to a job + return false; + } + } + + if (!check_mandates(item)) + return false; + + return true; +} + +static void mark_all_in_stockpiles(vector &stockpiles, bool announce) +{ + if (!depot_info.findDepot()) + { + if (announce) + Gui::showAnnouncement("Cannot trade, no valid depot available", COLOR_RED, true); + + return; + } + + std::vector &items = world->items.other[items_other_id::IN_PLAY]; + + + // 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); + F(spider_web); F(owned); F(in_job); +#undef F + + size_t marked_count = 0; + size_t error_count = 0; + for (size_t i = 0; i < items.size(); i++) + { + df::item *item = items[i]; + if (item->flags.whole & bad_flags.whole) + continue; + + if (!is_valid_item(item)) + continue; + + for (auto it = stockpiles.begin(); it != stockpiles.end(); it++) + { + if (!it->inStockpile(item)) + continue; + + // In case of container, check contained items for mandates + bool mandates_ok = true; + vector contained_items; + Items::getContainedItems(item, &contained_items); + for (auto cit = contained_items.begin(); cit != contained_items.end(); cit++) + { + if (!check_mandates(*cit)) + { + mandates_ok = false; + break; + } + } + + if (!mandates_ok) + continue; + + if (depot_info.assignItem(item)) + { + ++marked_count; + } + else + { + if (++error_count < 5) + { + Gui::showZoomAnnouncement(df::announcement_type::CANCEL_JOB, item->pos, + "Cannot trade item from stockpile " + int_to_string(it->getId()), COLOR_RED, true); + } + } + } + } + + if (marked_count) + Gui::showAnnouncement("Marked " + int_to_string(marked_count) + " items for trade", COLOR_GREEN, false); + else if (announce) + Gui::showAnnouncement("No more items to mark", COLOR_RED, true); + + if (error_count >= 5) + { + Gui::showAnnouncement(int_to_string(error_count) + " items were not marked", COLOR_RED, true); + } +} + + +/* + * Stockpile Monitoring + */ + +class StockpileMonitor +{ +public: + bool isMonitored(df::building_stockpilest *sp) + { + for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++) + { + if (it->matches(sp)) + return true; + } + + return false; + } + + void add(df::building_stockpilest *sp) + { + auto pile = StockpileInfo(sp); + if (pile.isValid()) + { + monitored_stockpiles.push_back(StockpileInfo(sp)); + monitored_stockpiles.back().save(); + } + } + + void remove(df::building_stockpilest *sp) + { + for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end(); it++) + { + if (it->matches(sp)) + { + it->remove(); + monitored_stockpiles.erase(it); + break; + } + } + } + + void doCycle() + { + if (!can_trade()) + return; + + for (auto it = monitored_stockpiles.begin(); it != monitored_stockpiles.end();) + { + if (!it->isValid()) + { + it = monitored_stockpiles.erase(it); + continue; + } + + ++it; + } + + mark_all_in_stockpiles(monitored_stockpiles, false); + } + + void reset() + { + monitored_stockpiles.clear(); + std::vector items; + DFHack::World::GetPersistentData(&items, "autotrade/stockpiles"); + + for (auto i = items.begin(); i != items.end(); i++) + { + auto pile = StockpileInfo(*i); + if (pile.load()) + monitored_stockpiles.push_back(StockpileInfo(pile)); + else + pile.remove(); + } + } + + +private: + vector monitored_stockpiles; +}; + +static StockpileMonitor monitor; + +#define DELTA_TICKS 600 + +DFhackCExport command_result plugin_onupdate ( color_ostream &out ) +{ + if(!Maps::IsValid()) + return CR_OK; + + static decltype(world->frame_counter) last_frame_count = 0; + + if (DFHack::World::ReadPauseState()) + return CR_OK; + + if (world->frame_counter - last_frame_count < DELTA_TICKS) + return CR_OK; + + last_frame_count = world->frame_counter; + + monitor.doCycle(); + + return CR_OK; +} + + +/* + * Interface + */ + +struct trade_hook : public df::viewscreen_dwarfmodest +{ + typedef df::viewscreen_dwarfmodest interpose_base; + + bool handleInput(set *input) + { + building_stockpilest *sp = get_selected_stockpile(); + if (!sp) + return false; + + if (input->count(interface_key::CUSTOM_M)) + { + if (!can_trade()) + return false; + + vector wrapper; + wrapper.push_back(StockpileInfo(sp)); + mark_all_in_stockpiles(wrapper, true); + + return true; + } + else if (input->count(interface_key::CUSTOM_U)) + { + if (monitor.isMonitored(sp)) + monitor.remove(sp); + else + monitor.add(sp); + } + + return false; + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!handleInput(input)) + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + 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 = 23; + + if (can_trade()) + OutputHotkeyString(x, y, "Mark all for trade", "m", true, left_margin); + + OutputToggleString(x, y, "Auto trade", "u", monitor.isMonitored(sp), true, left_margin); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, feed); +IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, render); + +static command_result autotrade_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; + } + } + + return CR_OK; +} + + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) + { + case DFHack::SC_MAP_LOADED: + depot_info.reset(); + monitor.reset(); + break; + case DFHack::SC_MAP_UNLOADED: + break; + default: + break; + } + return CR_OK; +} + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +{ + if (!gps || !INTERPOSE_HOOK(trade_hook, feed).apply() || !INTERPOSE_HOOK(trade_hook, render).apply()) + out.printerr("Could not insert autotrade hooks!\n"); + + commands.push_back( + PluginCommand( + "autotrade", "Automatically send items in marked stockpiles to trade depot, when trading is possible.", + autotrade_cmd, false, "")); + + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + return CR_OK; +} diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp new file mode 100644 index 000000000..810314bc1 --- /dev/null +++ b/plugins/buildingplan.cpp @@ -0,0 +1,1183 @@ +#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.9 + +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; + } +}; + +struct coord32_t +{ + int32_t x, y, z; + + df::coord get_coord16() const + { + return df::coord(x, y, z); + } +}; + +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 void debug(const string &msg) +{ + if (!show_debugging) + return; + + color_ostream_proxy out(Core::getInstance().getConsole()); + out << "DEBUG (buildingplan): " << msg << endl; +} + +/* + * Material Choice Screen + */ + +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 + { + return any_of(materials.begin(), materials.end(), + [&] (const MaterialInfo &m) { return material.matches(m); }); + } + + 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, + [] (MaterialInfo m) { return m.toString(); }); + + 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_each_(materials, + [&] (MaterialInfo &m) { str.append(m.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; +}; + + +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_each_(masks, + [&] (df::dfhack_material_category &m) { filter->mat_mask.whole |= m.whole; }); + + // Specific materials + auto materials = materials_column.getSelectedElems(); + transform_(materials, filter->materials, + [] (MaterialInfo &m) { return m; }); + + 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(); + } +}; + + +// 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, [] (df::job_item *x) { delete x; }); + 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; + +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, + [] (pair& pair) { pair.second = true; } ); + + 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_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; + } + + 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_F)) + { + if (!planner.inQuickFortMode()) + { + planner.enableQuickfortMode(); + } + else + { + planner.disableQuickfortMode(); + } + } + else if (input->count(interface_key::CUSTOM_M)) + { + Screen::show(new ViewscreenChooseMaterial(planner.getDefaultItemFilterForType(type))); + } + else if (input->count(interface_key::CUSTOM_Q)) + { + planner.cycleDefaultQuality(type); + } + else if (input->count(interface_key::CUSTOM_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; + + 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); + + OutputHotkeyString(x, y, "Decorated Only: ", "d"); + OutputString(COLOR_BROWN, x, y, + (filter->decorated_only) ? "Yes" : "No", true, left_margin); + + OutputHotkeyString(x, y, "Material Filter:", "m", true, left_margin); + auto filter_descriptions = filter->getMaterialFilterAsVector(); + for_each_(filter_descriptions, + [&](string d) { OutputString(COLOR_BROWN, x, y, " *" + d, 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_each_(filters, + [&](string d) { OutputString(COLOR_BLUE, x, y, "*" + d, true, left_margin); }); + } + else + { + planner.in_dummmy_screen = 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; +} + + +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(out); + break; + default: + break; + } + + return CR_OK; +} diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp new file mode 100644 index 000000000..6ff62253a --- /dev/null +++ b/plugins/dwarfmonitor.cpp @@ -0,0 +1,1307 @@ +#include "uicommon.h" + +#include "DataDefs.h" + +#include "df/job.h" +#include "df/ui.h" +#include "df/unit.h" +#include "df/viewscreen_dwarfmodest.h" +#include "df/world.h" +#include "df/misc_trait_type.h" +#include "df/unit_misc_trait.h" + +#include "modules/Gui.h" +#include "modules/Units.h" +#include "modules/Translation.h" +#include "modules/World.h" +#include "modules/Maps.h" +#include "df/activity_event.h" +#include "df/activity_entry.h" + +using std::deque; + +using df::global::world; +using df::global::ui; + +typedef int16_t activity_type; + +#define PLUGIN_VERSION 0.5 +#define DAY_TICKS 1200 +#define DELTA_TICKS 100 + +const int min_window = 28; +const int max_history_days = 3 * min_window; +const int ticks_per_day = DAY_TICKS / DELTA_TICKS; + +template +struct less_second { + typedef pair type; + bool operator ()(type const& a, type const& b) const { + return a.second > b.second; + } +}; + +static bool monitor_jobs = false; +static bool monitor_misery = true; +static map> work_history; + +static int misery[] = { 0, 0, 0, 0, 0, 0, 0 }; +static bool misery_upto_date = false; + +static int get_max_history() +{ + return ticks_per_day * max_history_days; +} + +static int getPercentage(const int n, const int d) +{ + return static_cast( + static_cast(n) / static_cast(d) * 100.0); +} + +static string getUnitName(df::unit * unit) +{ + string label = ""; + auto name = Units::getVisibleName(unit); + if (name->has_name) + label = Translation::TranslateName(name, false); + + return label; +} + +static void send_key(const df::interface_key &key) +{ + set< df::interface_key > keys; + keys.insert(key); + Gui::getCurViewscreen(true)->feed(&keys); +} + +static void move_cursor(df::coord &pos) +{ + Gui::setCursorCoords(pos.x, pos.y, pos.z); + send_key(interface_key::CURSOR_DOWN_Z); + send_key(interface_key::CURSOR_UP_Z); +} + +static void open_stats_srceen(); + +#define JOB_IDLE -1 +#define JOB_UNKNOWN -2 +#define JOB_MILITARY -3 +#define JOB_LEISURE -4 +#define JOB_UNPRODUCTIVE -5 +#define JOB_DESIGNATE -6 +#define JOB_STORE_ITEM -7 +#define JOB_MANUFACTURE -8 +#define JOB_DETAILING -9 +#define JOB_HUNTING -10 +#define JOB_MEDICAL -14 +#define JOB_COLLECT -15 +#define JOB_CONSTRUCTION -16 +#define JOB_AGRICULTURE -17 +#define JOB_FOOD_PROD -18 +#define JOB_MECHANICAL -19 +#define JOB_ANIMALS -20 +#define JOB_PRODUCTIVE -21 + +static map activity_labels; + +static string getActivityLabel(const activity_type activity) +{ + string label; + + if (activity_labels.find(activity) != activity_labels.end()) + { + label = activity_labels[activity]; + } + else + { + string raw_label = enum_item_key_str(static_cast(activity)); + for (auto c = raw_label.begin(); c != raw_label.end(); c++) + { + if (label.length() > 0 && *c >= 'A' && *c <= 'Z') + label += ' '; + + label += *c; + } + } + + return label; +} + +class ViewscreenDwarfStats : public dfhack_viewscreen +{ +public: + ViewscreenDwarfStats(df::unit *starting_selection) : selected_column(0) + { + dwarves_column.multiselect = false; + dwarves_column.auto_select = true; + dwarves_column.setTitle("Dwarves"); + + dwarf_activity_column.multiselect = false; + dwarf_activity_column.auto_select = true; + dwarf_activity_column.setTitle("Dwarf Activity"); + + window_days = min_window; + + populateDwarfColumn(starting_selection); + } + + void populateDwarfColumn(df::unit *starting_selection = NULL) + { + selected_column = 0; + + auto last_selected_index = dwarf_activity_column.highlighted_index; + dwarves_column.clear(); + dwarf_activity_values.clear(); + + for (auto it = work_history.begin(); it != work_history.end();) + { + auto unit = it->first; + if (Units::isDead(unit)) + { + work_history.erase(it++); + continue; + } + + deque *work_list = &it->second; + ++it; + + size_t dwarf_total = 0; + dwarf_activity_values[unit] = map(); + size_t count = window_days * ticks_per_day; + for (auto entry = work_list->rbegin(); entry != work_list->rend() && count > 0; entry++, count--) + { + if (*entry == JOB_UNKNOWN || *entry == job_type::DrinkBlood) + continue; + + ++dwarf_total; + addDwarfActivity(unit, *entry); + } + + for_each_(dwarf_activity_values[unit], + [&] (const pair &x) + { dwarf_activity_values[unit][x.first] = getPercentage(x.second, dwarf_total); } ); + + dwarves_column.add(getUnitName(unit), unit); + } + + dwarf_activity_column.left_margin = dwarves_column.fixWidth() + 2; + dwarves_column.filterDisplay(); + if (starting_selection) + dwarves_column.selectItem(starting_selection); + else + dwarves_column.setHighlight(last_selected_index); + + populateActivityColumn(); + } + + void populateActivityColumn() + { + dwarf_activity_column.clear(); + if (dwarves_column.getDisplayedListSize() == 0) + return; + + auto unit = dwarves_column.getFirstSelectedElem(); + if (dwarf_activity_values.find(unit) == dwarf_activity_values.end()) + return; + + auto dwarf_activities = &dwarf_activity_values[unit]; + if (dwarf_activities) + { + vector> rev_vec(dwarf_activities->begin(), dwarf_activities->end()); + sort(rev_vec.begin(), rev_vec.end(), less_second()); + + for_each_(rev_vec, + [&] (pair x) + { dwarf_activity_column.add(getActivityItem(x.first, x.second), x.first); }); + } + + dwarf_activity_column.fixWidth(); + dwarf_activity_column.clearSearch(); + dwarf_activity_column.setHighlight(0); + } + + void addDwarfActivity(df::unit *unit, const activity_type &activity) + { + if (dwarf_activity_values[unit].find(activity) == dwarf_activity_values[unit].end()) + dwarf_activity_values[unit][activity] = 0; + + dwarf_activity_values[unit][activity]++; + } + + string getActivityItem(activity_type activity, size_t value) + { + return pad_string(int_to_string(value), 3) + " " + getActivityLabel(activity); + } + + void feed(set *input) + { + bool key_processed = false; + switch (selected_column) + { + case 0: + key_processed = dwarves_column.feed(input); + break; + case 1: + key_processed = dwarf_activity_column.feed(input); + break; + } + + if (key_processed) + { + if (selected_column == 0 && dwarves_column.feed_changed_highlight) + populateActivityColumn(); + + return; + } + + if (input->count(interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + else if (input->count(interface_key::CUSTOM_SHIFT_D)) + { + Screen::dismiss(this); + open_stats_srceen(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_Z)) + { + df::unit *selected_unit = (selected_column == 0) ? dwarves_column.getFirstSelectedElem() : nullptr; + if (selected_unit) + { + input->clear(); + Screen::dismiss(this); + Gui::resetDwarfmodeView(true); + send_key(interface_key::D_VIEWUNIT); + move_cursor(selected_unit->pos); + } + } + else if (input->count(interface_key::SECONDSCROLL_PAGEDOWN)) + { + window_days += min_window; + if (window_days > max_history_days) + window_days = min_window; + + populateDwarfColumn(); + } + 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 (dwarves_column.setHighlightByMouse()) + { + selected_column = 0; + populateActivityColumn(); + } + else if (dwarf_activity_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(" Dwarf Activity "); + + dwarves_column.display(selected_column == 0); + dwarf_activity_column.display(selected_column == 1); + + int32_t y = gps->dimy - 4; + int32_t x = 2; + OutputHotkeyString(x, y, "Leave", "Esc"); + + x += 13; + string window_label = "Window Months: " + int_to_string(window_days / min_window); + OutputHotkeyString(x, y, window_label.c_str(), "*"); + + ++y; + x = 2; + OutputHotkeyString(x, y, "Fort Stats", "Shift-D"); + + x += 3; + OutputHotkeyString(x, y, "Zoom Unit", "Shift-Z"); + } + + std::string getFocusString() { return "dwarfmonitor_dwarfstats"; } + +private: + ListColumn dwarves_column; + ListColumn dwarf_activity_column; + int selected_column; + size_t window_days; + + map> dwarf_activity_values; + + void validateColumn() + { + set_to_limit(selected_column, 1); + } + + void resize(int32_t x, int32_t y) + { + dfhack_viewscreen::resize(x, y); + dwarves_column.resize(); + dwarf_activity_column.resize(); + } +}; + + +class ViewscreenFortStats : public dfhack_viewscreen +{ +public: + ViewscreenFortStats() + { + fort_activity_column.multiselect = false; + fort_activity_column.auto_select = true; + fort_activity_column.setTitle("Fort Activities"); + fort_activity_column.bottom_margin = 4; + + dwarf_activity_column.multiselect = false; + dwarf_activity_column.auto_select = true; + dwarf_activity_column.setTitle("Units on Activity"); + dwarf_activity_column.bottom_margin = 4; + dwarf_activity_column.text_clip_at = 25; + + category_breakdown_column.setTitle("Category Breakdown"); + category_breakdown_column.bottom_margin = 4; + + window_days = min_window; + + populateFortColumn(); + } + + void populateFortColumn() + { + selected_column = 0; + fort_activity_count = 0; + + auto last_selected_index = fort_activity_column.highlighted_index; + fort_activity_column.clear(); + fort_activity_totals.clear(); + dwarf_activity_values.clear(); + category_breakdown.clear(); + + for (auto it = work_history.begin(); it != work_history.end();) + { + auto unit = it->first; + if (Units::isDead(unit)) + { + work_history.erase(it++); + continue; + } + + deque *work_list = &it->second; + ++it; + + size_t count = window_days * ticks_per_day; + for (auto entry = work_list->rbegin(); entry != work_list->rend() && count > 0; entry++, count--) + { + if (*entry == JOB_UNKNOWN) + continue; + + ++fort_activity_count; + + auto real_activity = *entry; + if (real_activity < 0) + { + addFortActivity(real_activity); + } + else + { + auto activity = static_cast(real_activity); + + switch (activity) + { + case job_type::Eat: + case job_type::Drink: + case job_type::Drink2: + case job_type::Sleep: + case job_type::AttendParty: + case job_type::Rest: + case job_type::CleanSelf: + case job_type::DrinkBlood: + real_activity = JOB_LEISURE; + break; + + case job_type::Kidnap: + case job_type::StartingFistFight: + case job_type::SeekInfant: + case job_type::SeekArtifact: + case job_type::GoShopping: + case job_type::GoShopping2: + case job_type::RecoverPet: + case job_type::CauseTrouble: + case job_type::ReportCrime: + case job_type::BeatCriminal: + case job_type::ExecuteCriminal: + real_activity = JOB_UNPRODUCTIVE; + break; + + case job_type::CarveUpwardStaircase: + case job_type::CarveDownwardStaircase: + case job_type::CarveUpDownStaircase: + case job_type::CarveRamp: + case job_type::DigChannel: + case job_type::Dig: + case job_type::CarveTrack: + case job_type::CarveFortification: + real_activity = JOB_DESIGNATE; + break; + + case job_type::StoreOwnedItem: + case job_type::PlaceItemInTomb: + case job_type::StoreItemInStockpile: + case job_type::StoreItemInBag: + case job_type::StoreItemInHospital: + case job_type::StoreItemInChest: + case job_type::StoreItemInCabinet: + case job_type::StoreWeapon: + case job_type::StoreArmor: + case job_type::StoreItemInBarrel: + case job_type::StoreItemInBin: + case job_type::BringItemToDepot: + case job_type::BringItemToShop: + case job_type::GetProvisions: + case job_type::FillWaterskin: + case job_type::FillWaterskin2: + case job_type::CheckChest: + case job_type::PickupEquipment: + case job_type::DumpItem: + case job_type::PushTrackVehicle: + case job_type::PlaceTrackVehicle: + case job_type::StoreItemInVehicle: + real_activity = JOB_STORE_ITEM; + break; + + case job_type::ConstructDoor: + case job_type::ConstructFloodgate: + case job_type::ConstructBed: + case job_type::ConstructThrone: + case job_type::ConstructCoffin: + case job_type::ConstructTable: + case job_type::ConstructChest: + case job_type::ConstructBin: + case job_type::ConstructArmorStand: + case job_type::ConstructWeaponRack: + case job_type::ConstructCabinet: + case job_type::ConstructStatue: + case job_type::ConstructBlocks: + case job_type::MakeRawGlass: + case job_type::MakeCrafts: + case job_type::MintCoins: + case job_type::CutGems: + case job_type::CutGlass: + case job_type::EncrustWithGems: + case job_type::EncrustWithGlass: + case job_type::SmeltOre: + case job_type::MeltMetalObject: + case job_type::ExtractMetalStrands: + case job_type::MakeWeapon: + case job_type::ForgeAnvil: + case job_type::ConstructCatapultParts: + case job_type::ConstructBallistaParts: + case job_type::MakeArmor: + case job_type::MakeHelm: + case job_type::MakePants: + case job_type::StudWith: + case job_type::ProcessPlantsBag: + case job_type::ProcessPlantsVial: + case job_type::ProcessPlantsBarrel: + case job_type::WeaveCloth: + case job_type::MakeGloves: + case job_type::MakeShoes: + case job_type::MakeShield: + case job_type::MakeCage: + case job_type::MakeChain: + case job_type::MakeFlask: + case job_type::MakeGoblet: + case job_type::MakeInstrument: + case job_type::MakeToy: + case job_type::MakeAnimalTrap: + case job_type::MakeBarrel: + case job_type::MakeBucket: + case job_type::MakeWindow: + case job_type::MakeTotem: + case job_type::MakeAmmo: + case job_type::DecorateWith: + case job_type::MakeBackpack: + case job_type::MakeQuiver: + case job_type::MakeBallistaArrowHead: + case job_type::AssembleSiegeAmmo: + case job_type::ConstructMechanisms: + case job_type::MakeTrapComponent: + case job_type::ExtractFromPlants: + case job_type::ExtractFromRawFish: + case job_type::ExtractFromLandAnimal: + case job_type::MakeCharcoal: + case job_type::MakeAsh: + case job_type::MakeLye: + case job_type::MakePotashFromLye: + case job_type::MakePotashFromAsh: + case job_type::DyeThread: + case job_type::DyeCloth: + case job_type::SewImage: + case job_type::MakePipeSection: + case job_type::ConstructHatchCover: + case job_type::ConstructGrate: + case job_type::ConstructQuern: + case job_type::ConstructMillstone: + case job_type::ConstructSplint: + case job_type::ConstructCrutch: + case job_type::ConstructTractionBench: + case job_type::CustomReaction: + case job_type::ConstructSlab: + case job_type::EngraveSlab: + case job_type::SpinThread: + case job_type::MakeTool: + real_activity = JOB_MANUFACTURE; + break; + + case job_type::DetailFloor: + case job_type::DetailWall: + real_activity = JOB_DETAILING; + break; + + case job_type::Hunt: + case job_type::ReturnKill: + case job_type::HuntVermin: + case job_type::GatherPlants: + case job_type::Fish: + case job_type::CatchLiveFish: + case job_type::BaitTrap: + case job_type::InstallColonyInHive: + real_activity = JOB_HUNTING; + break; + + case job_type::RemoveConstruction: + case job_type::DestroyBuilding: + case job_type::RemoveStairs: + case job_type::ConstructBuilding: + real_activity = JOB_CONSTRUCTION; + break; + + case job_type::FellTree: + case job_type::CollectWebs: + case job_type::CollectSand: + case job_type::DrainAquarium: + case job_type::FillAquarium: + case job_type::FillPond: + case job_type::CollectClay: + real_activity = JOB_COLLECT; + break; + + case job_type::TrainHuntingAnimal: + case job_type::TrainWarAnimal: + case job_type::CatchLiveLandAnimal: + case job_type::TameVermin: + case job_type::TameAnimal: + case job_type::ChainAnimal: + case job_type::UnchainAnimal: + case job_type::UnchainPet: + case job_type::ReleaseLargeCreature: + case job_type::ReleasePet: + case job_type::ReleaseSmallCreature: + case job_type::HandleSmallCreature: + case job_type::HandleLargeCreature: + case job_type::CageLargeCreature: + case job_type::CageSmallCreature: + case job_type::PitLargeAnimal: + case job_type::PitSmallAnimal: + case job_type::SlaughterAnimal: + case job_type::ShearCreature: + case job_type::PenLargeAnimal: + case job_type::PenSmallAnimal: + case job_type::TrainAnimal: + real_activity = JOB_ANIMALS; + break; + + case job_type::PlantSeeds: + case job_type::HarvestPlants: + case job_type::FertilizeField: + real_activity = JOB_AGRICULTURE; + break; + + case job_type::ButcherAnimal: + case job_type::PrepareRawFish: + case job_type::MillPlants: + case job_type::MilkCreature: + case job_type::MakeCheese: + case job_type::PrepareMeal: + case job_type::ProcessPlants: + case job_type::BrewDrink: + case job_type::CollectHiveProducts: + real_activity = JOB_FOOD_PROD; + break; + + case job_type::LoadCatapult: + case job_type::LoadBallista: + case job_type::FireCatapult: + case job_type::FireBallista: + real_activity = JOB_MILITARY; + break; + + case job_type::LoadCageTrap: + case job_type::LoadStoneTrap: + case job_type::LoadWeaponTrap: + case job_type::CleanTrap: + case job_type::LinkBuildingToTrigger: + case job_type::PullLever: + real_activity = JOB_MECHANICAL; + break; + + case job_type::RecoverWounded: + case job_type::DiagnosePatient: + case job_type::ImmobilizeBreak: + case job_type::DressWound: + case job_type::CleanPatient: + case job_type::Surgery: + case job_type::Suture: + case job_type::SetBone: + case job_type::PlaceInTraction: + case job_type::GiveWater: + case job_type::GiveFood: + case job_type::GiveWater2: + case job_type::GiveFood2: + case job_type::BringCrutch: + case job_type::ApplyCast: + real_activity = JOB_MEDICAL; + break; + + case job_type::OperatePump: + case job_type::ManageWorkOrders: + case job_type::UpdateStockpileRecords: + case job_type::TradeAtDepot: + real_activity = JOB_PRODUCTIVE; + break; + + default: + break; + } + + addFortActivity(real_activity); + addCategoryActivity(real_activity, *entry); + } + + if (dwarf_activity_values.find(real_activity) == dwarf_activity_values.end()) + dwarf_activity_values[real_activity] = map(); + + map &activity_for_dwarf = dwarf_activity_values[real_activity]; + if (activity_for_dwarf.find(unit) == activity_for_dwarf.end()) + activity_for_dwarf[unit] = 0; + + ++activity_for_dwarf[unit]; + } + } + + vector> rev_vec(fort_activity_totals.begin(), fort_activity_totals.end()); + sort(rev_vec.begin(), rev_vec.end(), less_second()); + + for (auto rev_it = rev_vec.begin(); rev_it != rev_vec.end(); rev_it++) + { + auto activity = rev_it->first; + addToFortAverageColumn(activity); + + for (auto it = dwarf_activity_values[activity].begin(); it != dwarf_activity_values[activity].end(); it++) + { + auto avg = getPercentage(it->second, getFortActivityCount(activity)); + dwarf_activity_values[activity][it->first] = avg; + } + } + + for (auto cat_it = category_breakdown.begin(); cat_it != category_breakdown.end(); cat_it++) + { + auto cat_total = fort_activity_totals[cat_it->first]; + for (auto val_it = cat_it->second.begin(); val_it != cat_it->second.end(); val_it++) + { + category_breakdown[cat_it->first][val_it->first] = getPercentage(val_it->second, cat_total); + } + } + + dwarf_activity_column.left_margin = fort_activity_column.fixWidth() + 2; + fort_activity_column.filterDisplay(); + fort_activity_column.setHighlight(last_selected_index); + populateDwarfColumn(); + populateCategoryBreakdownColumn(); + } + + void populateDwarfColumn() + { + dwarf_activity_column.clear(); + if (fort_activity_column.getDisplayListSize() > 0) + { + activity_type selected_activity = fort_activity_column.getFirstSelectedElem(); + auto dwarf_activities = &dwarf_activity_values[selected_activity]; + if (dwarf_activities) + { + vector> rev_vec(dwarf_activities->begin(), dwarf_activities->end()); + sort(rev_vec.begin(), rev_vec.end(), less_second()); + + for_each_(rev_vec, + [&] (pair x) + { dwarf_activity_column.add(getDwarfAverage(x.first, x.second), x.first); }); + } + } + + category_breakdown_column.left_margin = dwarf_activity_column.fixWidth() + 2; + dwarf_activity_column.clearSearch(); + dwarf_activity_column.setHighlight(0); + } + + void populateCategoryBreakdownColumn() + { + category_breakdown_column.clear(); + if (fort_activity_column.getDisplayListSize() == 0) + return; + + auto selected_activity = fort_activity_column.getFirstSelectedElem(); + auto category_activities = &category_breakdown[selected_activity]; + if (category_activities) + { + vector> rev_vec(category_activities->begin(), category_activities->end()); + sort(rev_vec.begin(), rev_vec.end(), less_second()); + + for_each_(rev_vec, + [&] (pair x) + { category_breakdown_column.add(getBreakdownAverage(x.first, x.second), x.first); }); + } + + category_breakdown_column.fixWidth(); + category_breakdown_column.clearSearch(); + category_breakdown_column.setHighlight(0); + } + + void addToFortAverageColumn(activity_type &type) + { + if (getFortActivityCount(type)) + fort_activity_column.add(getFortAverage(type), type); + } + + string getFortAverage(const activity_type &activity) + { + auto average = getPercentage(getFortActivityCount(activity), fort_activity_count); + auto label = getActivityLabel(activity); + auto result = pad_string(int_to_string(average), 3) + " " + label; + + return result; + } + + string getDwarfAverage(df::unit *unit, const size_t value) + { + auto label = getUnitName(unit); + auto result = pad_string(int_to_string(value), 3) + " " + label; + + return result; + } + + string getBreakdownAverage(activity_type activity, const size_t value) + { + auto label = getActivityLabel(activity); + auto result = pad_string(int_to_string(value), 3) + " " + label; + + return result; + } + + size_t getFortActivityCount(const activity_type activity) + { + if (fort_activity_totals.find(activity) == fort_activity_totals.end()) + return 0; + + return fort_activity_totals[activity]; + } + + void addFortActivity(const activity_type activity) + { + if (fort_activity_totals.find(activity) == fort_activity_totals.end()) + fort_activity_totals[activity] = 0; + + fort_activity_totals[activity]++; + } + + void addCategoryActivity(const int category, const activity_type activity) + { + if (category_breakdown.find(category) == category_breakdown.end()) + category_breakdown[category] = map(); + + if (category_breakdown[category].find(activity) == category_breakdown[category].end()) + category_breakdown[category][activity] = 0; + + category_breakdown[category][activity]++; + } + + void feed(set *input) + { + bool key_processed = false; + switch (selected_column) + { + case 0: + key_processed = fort_activity_column.feed(input); + break; + case 1: + key_processed = dwarf_activity_column.feed(input); + break; + } + + if (key_processed) + { + if (selected_column == 0 && fort_activity_column.feed_changed_highlight) + { + populateDwarfColumn(); + populateCategoryBreakdownColumn(); + } + + return; + } + + if (input->count(interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + else if (input->count(interface_key::SECONDSCROLL_PAGEDOWN)) + { + window_days += min_window; + if (window_days > max_history_days) + window_days = min_window; + + populateFortColumn(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_D)) + { + df::unit *selected_unit = (selected_column == 1) ? dwarf_activity_column.getFirstSelectedElem() : nullptr; + Screen::dismiss(this); + Screen::show(new ViewscreenDwarfStats(selected_unit)); + } + else if (input->count(interface_key::CUSTOM_SHIFT_Z)) + { + df::unit *selected_unit = (selected_column == 1) ? dwarf_activity_column.getFirstSelectedElem() : nullptr; + if (selected_unit) + { + input->clear(); + Screen::dismiss(this); + Gui::resetDwarfmodeView(true); + send_key(interface_key::D_VIEWUNIT); + move_cursor(selected_unit->pos); + } + } + 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 (fort_activity_column.setHighlightByMouse()) + { + selected_column = 0; + populateDwarfColumn(); + } + else if (dwarf_activity_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(" Fortress Efficiency "); + + fort_activity_column.display(selected_column == 0); + dwarf_activity_column.display(selected_column == 1); + category_breakdown_column.display(false); + + int32_t y = gps->dimy - 4; + int32_t x = 2; + OutputHotkeyString(x, y, "Leave", "Esc"); + + x += 13; + string window_label = "Window Months: " + int_to_string(window_days / min_window); + OutputHotkeyString(x, y, window_label.c_str(), "*"); + + ++y; + x = 2; + OutputHotkeyString(x, y, "Dwarf Stats", "Shift-D"); + + x += 3; + OutputHotkeyString(x, y, "Zoom Unit", "Shift-Z"); + } + + std::string getFocusString() { return "dwarfmonitor_fortstats"; } + +private: + ListColumn fort_activity_column, category_breakdown_column; + ListColumn dwarf_activity_column; + int selected_column; + + map fort_activity_totals; + map> category_breakdown; + map> dwarf_activity_values; + size_t fort_activity_count; + size_t window_days; + + vector listed_activities; + + void validateColumn() + { + set_to_limit(selected_column, 1); + } + + void resize(int32_t x, int32_t y) + { + dfhack_viewscreen::resize(x, y); + fort_activity_column.resize(); + dwarf_activity_column.resize(); + } +}; + +static void open_stats_srceen() +{ + Screen::show(new ViewscreenFortStats()); +} + +static void add_work_history(df::unit *unit, activity_type type) +{ + if (work_history.find(unit) == work_history.end()) + { + auto max_history = get_max_history(); + for (int i = 0; i < max_history; i++) + work_history[unit].push_back(JOB_UNKNOWN); + } + + work_history[unit].push_back(type); + work_history[unit].pop_front(); +} + +static bool is_at_leisure(df::unit *unit) +{ + for (auto p = unit->status.misc_traits.begin(); p < unit->status.misc_traits.end(); p++) + { + if ((*p)->id == misc_trait_type::Migrant || (*p)->id == misc_trait_type::OnBreak) + return true; + } + + return false; +} + +static void reset() +{ + work_history.clear(); + + for (int i = 0; i < 7; i++) + misery[i] = 0; + + misery_upto_date = false; +} + +static void update_dwarf_stats(bool is_paused) +{ + if (monitor_misery) + { + for (int i = 0; i < 7; i++) + misery[i] = 0; + } + + for (auto iter = world->units.active.begin(); iter != world->units.active.end(); iter++) + { + df::unit* unit = *iter; + if (!Units::isCitizen(unit)) + continue; + + if (DFHack::Units::isDead(unit)) + { + auto it = work_history.find(unit); + if (it != work_history.end()) + work_history.erase(it); + + continue; + } + + if (monitor_misery) + { + int happy = unit->status.happiness; + if (happy == 0) // miserable + misery[0]++; + else if (happy <= 25) // very unhappy + misery[1]++; + else if (happy <= 50) // unhappy + misery[2]++; + else if (happy <= 75) // fine + misery[3]++; + else if (happy <= 125) // quite content + misery[4]++; + else if (happy <= 150) // happy + misery[5]++; + else // ecstatic + misery[6]++; + } + + if (!monitor_jobs || is_paused) + continue; + + if (unit->profession == profession::BABY || + unit->profession == profession::CHILD || + unit->profession == profession::DRUNK) + { + continue; + } + + if (ENUM_ATTR(profession, military, unit->profession)) + { + add_work_history(unit, JOB_MILITARY); + continue; + } + + if (!unit->job.current_job) + { + add_work_history(unit, JOB_IDLE); + continue; + } + + if (is_at_leisure(unit)) + { + add_work_history(unit, JOB_LEISURE); + continue; + } + + add_work_history(unit, unit->job.current_job->job_type); + } +} + +DFhackCExport command_result plugin_onupdate (color_ostream &out) +{ + if (!monitor_jobs && !monitor_misery) + return CR_OK; + + if(!Maps::IsValid()) + return CR_OK; + + static decltype(world->frame_counter) last_frame_count = 0; + + bool is_paused = DFHack::World::ReadPauseState(); + if (is_paused) + { + if (monitor_misery && !misery_upto_date) + misery_upto_date = true; + else + return CR_OK; + } + else + { + if (world->frame_counter - last_frame_count < DELTA_TICKS) + return CR_OK; + + last_frame_count = world->frame_counter; + } + + update_dwarf_stats(is_paused); + + return CR_OK; +} + +static color_value monitor_colors[] = +{ + COLOR_LIGHTRED, + COLOR_RED, + COLOR_YELLOW, + COLOR_WHITE, + COLOR_CYAN, + COLOR_LIGHTBLUE, + COLOR_LIGHTGREEN +}; + +struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest +{ + typedef df::viewscreen_dwarfmodest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + if (monitor_misery && Maps::IsValid()) + { + string entries[7]; + size_t length = 9; + for (int i = 0; i < 7; i++) + { + entries[i] = int_to_string(misery[i]); + length += entries[i].length(); + } + + int x = gps->dimx - length; + int y = gps->dimy - 1; + OutputString(COLOR_WHITE, x, y, "H:"); + for (int i = 0; i < 7; i++) + { + OutputString(monitor_colors[i], x, y, entries[i]); + if (i < 6) + OutputString(COLOR_WHITE, x, y, "/"); + } + } + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, feed); +IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, render); + +DFHACK_PLUGIN("dwarfmonitor"); + +static bool set_monitoring_mode(const string &mode, const bool &state) +{ + bool mode_recognized = false; + + if (mode == "work" || mode == "all") + { + mode_recognized = true; + monitor_jobs = state; + if (!monitor_jobs) + reset(); + } + + if (mode == "misery" || mode == "all") + { + mode_recognized = true; + monitor_misery = state; + } + + return mode_recognized; +} + +static command_result dwarfmonitor_cmd(color_ostream &out, vector & parameters) +{ + bool show_help = false; + if (parameters.empty()) + { + show_help = true; + } + else + { + auto cmd = parameters[0][0]; + string mode; + + if (parameters.size() > 1) + mode = toLower(parameters[1]); + + if (cmd == 'v' || cmd == 'V') + { + out << "DwarfMonitor" << endl << "Version: " << PLUGIN_VERSION << endl; + } + else if ((cmd == 'e' || cmd == 'E') && !mode.empty()) + { + if (set_monitoring_mode(mode, true)) + { + out << "Monitoring enabled: " << mode << endl; + } + else + { + show_help = true; + } + } + else if ((cmd == 'd' || cmd == 'D') && !mode.empty()) + { + if (set_monitoring_mode(mode, false)) + out << "Monitoring disabled: " << mode << endl; + else + show_help = true; + } + else if (cmd == 's' || cmd == 'S') + { + if(Maps::IsValid()) + Screen::show(new ViewscreenFortStats()); + } + else + { + show_help = true; + } + } + + if (show_help) + return CR_WRONG_USAGE; + + return CR_OK; +} + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) +{ + if (!gps || !INTERPOSE_HOOK(dwarf_monitor_hook, feed).apply() || !INTERPOSE_HOOK(dwarf_monitor_hook, render).apply()) + out.printerr("Could not insert dwarfmonitor hooks!\n"); + + activity_labels[JOB_IDLE] = "Idle"; + activity_labels[JOB_MILITARY] = "Military Duty"; + activity_labels[JOB_LEISURE] = "Leisure"; + activity_labels[JOB_UNPRODUCTIVE] = "Unproductive"; + activity_labels[JOB_DESIGNATE] = "Mining"; + activity_labels[JOB_STORE_ITEM] = "Store/Fetch Item"; + activity_labels[JOB_MANUFACTURE] = "Manufacturing"; + activity_labels[JOB_DETAILING] = "Detailing"; + activity_labels[JOB_HUNTING] = "Hunting/Gathering"; + activity_labels[JOB_MEDICAL] = "Medical"; + activity_labels[JOB_COLLECT] = "Collect Materials"; + activity_labels[JOB_CONSTRUCTION] = "Construction"; + activity_labels[JOB_AGRICULTURE] = "Agriculture"; + activity_labels[JOB_FOOD_PROD] = "Food/Drink Production"; + activity_labels[JOB_MECHANICAL] = "Mechanics"; + activity_labels[JOB_ANIMALS] = "Animal Handling"; + activity_labels[JOB_PRODUCTIVE] = "Other Productive"; + + commands.push_back( + PluginCommand( + "dwarfmonitor", "Records dwarf activity to measure fort efficiency", + dwarfmonitor_cmd, false, + "dwarfmonitor enable \n" + " Start monitoring \n" + " can be \"work\", \"misery\", or \"all\"\n" + "dwarfmonitor disable \n" + " as above\n\n" + "dwarfmonitor stats\n" + " Show statistics summary\n\n" + )); + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + reset(); + break; + default: + break; + } + return CR_OK; +} diff --git a/plugins/lua/zone.lua b/plugins/lua/zone.lua new file mode 100644 index 000000000..f6a136690 --- /dev/null +++ b/plugins/lua/zone.lua @@ -0,0 +1,12 @@ +local _ENV = mkmodule('plugins.zone') + +--[[ + + Native functions: + + * autobutcher_isEnabled() + * autowatch_isEnabled() + +--]] + +return _ENV \ No newline at end of file diff --git a/plugins/mousequery.cpp b/plugins/mousequery.cpp new file mode 100644 index 000000000..dfb89f6f4 --- /dev/null +++ b/plugins/mousequery.cpp @@ -0,0 +1,263 @@ +#include + +#include "Core.h" +#include +#include +#include +#include + +#include "DataDefs.h" + +#include "df/building.h" +#include "df/enabler.h" +#include "df/item.h" +#include "df/ui.h" +#include "df/unit.h" +#include "df/viewscreen_dwarfmodest.h" +#include "df/world.h" + +#include "modules/Gui.h" +#include "modules/Screen.h" + + +using std::set; +using std::string; +using std::ostringstream; + +using namespace DFHack; +using namespace df::enums; + +using df::global::enabler; +using df::global::gps; +using df::global::world; +using df::global::ui; + + +static int32_t last_x, last_y, last_z; +static size_t max_list_size = 100000; // Avoid iterating over huge lists + +struct mousequery_hook : public df::viewscreen_dwarfmodest +{ + typedef df::viewscreen_dwarfmodest interpose_base; + + void send_key(const df::interface_key &key) + { + set tmp; + tmp.insert(key); + //INTERPOSE_NEXT(feed)(&tmp); + this->feed(&tmp); + } + + df::interface_key get_default_query_mode(const int32_t &x, const int32_t &y, const int32_t &z) + { + bool fallback_to_building_query = false; + + // Check for unit under cursor + size_t count = world->units.all.size(); + if (count <= max_list_size) + { + for(size_t i = 0; i < count; i++) + { + df::unit *unit = world->units.all[i]; + + if(unit->pos.x == x && unit->pos.y == y && unit->pos.z == z) + return df::interface_key::D_VIEWUNIT; + } + } + else + { + fallback_to_building_query = true; + } + + // Check for building under cursor + count = world->buildings.all.size(); + if (count <= max_list_size) + { + for(size_t i = 0; i < count; i++) + { + df::building *bld = world->buildings.all[i]; + + if (z == bld->z && + x >= bld->x1 && x <= bld->x2 && + y >= bld->y1 && y <= bld->y2) + { + df::building_type type = bld->getType(); + + if (type == building_type::Stockpile) + { + fallback_to_building_query = true; + break; // Check for items in stockpile first + } + + // For containers use item view, fir everything else, query view + return (type == building_type::Box || type == building_type::Cabinet || + type == building_type::Weaponrack || type == building_type::Armorstand) + ? df::interface_key::D_BUILDITEM : df::interface_key::D_BUILDJOB; + } + } + } + else + { + fallback_to_building_query = true; + } + + + // Check for items under cursor + count = world->items.all.size(); + if (count <= max_list_size) + { + for(size_t i = 0; i < count; i++) + { + df::item *item = world->items.all[i]; + if (z == item->pos.z && x == item->pos.x && y == item->pos.y && + !item->flags.bits.in_building && !item->flags.bits.hidden && + !item->flags.bits.in_job && !item->flags.bits.in_chest && + !item->flags.bits.in_inventory) + { + return df::interface_key::D_LOOK; + } + } + } + else + { + fallback_to_building_query = true; + } + + return (fallback_to_building_query) ? df::interface_key::D_BUILDJOB : df::interface_key::D_LOOK; + } + + bool handle_mouse(const set *input) + { + int32_t cx, cy, vz; + if (enabler->tracking_on) + { + if (enabler->mouse_lbut) + { + int32_t mx, my; + if (Gui::getMousePos(mx, my)) + { + int32_t vx, vy; + if (Gui::getViewCoords(vx, vy, vz)) + { + cx = vx + mx - 1; + cy = vy + my - 1; + + using namespace df::enums::ui_sidebar_mode; + df::interface_key key = interface_key::NONE; + bool cursor_still_here = (last_x == cx && last_y == cy && last_z == vz); + switch(ui->main.mode) + { + case QueryBuilding: + if (cursor_still_here) + key = df::interface_key::D_BUILDITEM; + break; + + case BuildingItems: + if (cursor_still_here) + key = df::interface_key::D_VIEWUNIT; + break; + + case ViewUnits: + if (cursor_still_here) + key = df::interface_key::D_LOOK; + break; + + case LookAround: + if (cursor_still_here) + key = df::interface_key::D_BUILDJOB; + break; + + case Default: + break; + + default: + return false; + } + + enabler->mouse_lbut = 0; + + // Can't check limits earlier as we must be sure we are in query or default mode we can clear the button flag + // Otherwise the feed gets stuck in a loop + uint8_t menu_width, area_map_width; + Gui::getMenuWidth(menu_width, area_map_width); + int32_t w = gps->dimx; + if (menu_width == 1) w -= 57; //Menu is open doubly wide + else if (menu_width == 2 && area_map_width == 3) w -= 33; //Just the menu is open + else if (menu_width == 2 && area_map_width == 2) w -= 26; //Just the area map is open + + if (mx < 1 || mx > w || my < 1 || my > gps->dimy - 2) + return false; + + while (ui->main.mode != Default) + { + send_key(df::interface_key::LEAVESCREEN); + } + + if (key == interface_key::NONE) + key = get_default_query_mode(cx, cy, vz); + + send_key(key); + + // Force UI refresh + Gui::setCursorCoords(cx, cy, vz); + send_key(interface_key::CURSOR_DOWN_Z); + send_key(interface_key::CURSOR_UP_Z); + last_x = cx; + last_y = cy; + last_z = vz; + + return true; + } + } + } + else if (enabler->mouse_rbut) + { + // Escape out of query mode + using namespace df::enums::ui_sidebar_mode; + if (ui->main.mode == QueryBuilding || ui->main.mode == BuildingItems || + ui->main.mode == ViewUnits || ui->main.mode == LookAround) + { + while (ui->main.mode != Default) + { + enabler->mouse_rbut = 0; + send_key(df::interface_key::LEAVESCREEN); + } + } + } + } + + return false; + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!handle_mouse(input)) + INTERPOSE_NEXT(feed)(input); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(mousequery_hook, feed); + +DFHACK_PLUGIN("mousequery"); + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +{ + if (!gps || !INTERPOSE_HOOK(mousequery_hook, feed).apply()) + out.printerr("Could not insert mousequery hooks!\n"); + + last_x = last_y = last_z = -1; + + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + last_x = last_y = last_z = -1; + break; + default: + break; + } + return CR_OK; +} diff --git a/plugins/resume.cpp b/plugins/resume.cpp new file mode 100644 index 000000000..e697a133f --- /dev/null +++ b/plugins/resume.cpp @@ -0,0 +1,315 @@ +#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/viewscreen_dwarfmodest.h" +#include "df/world.h" +#include "df/building_constructionst.h" +#include "df/building.h" +#include "df/job.h" +#include "df/job_item.h" + +#include "modules/Gui.h" +#include "modules/Screen.h" +#include "modules/Buildings.h" +#include "modules/Maps.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::world; + +DFHACK_PLUGIN("resume"); +#define PLUGIN_VERSION 0.2 + +#ifndef HAVE_NULLPTR +#define nullptr 0L +#endif + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + return CR_OK; +} + +template +static void for_each_(vector &v, Fn func) +{ + for_each(v.begin(), v.end(), func); +} + +template +static void transform_(vector &src, vector &dst, Fn func) +{ + transform(src.begin(), src.end(), back_inserter(dst), func); +} + +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(); +} + +df::job *get_suspended_job(df::building *bld) +{ + if (bld->getBuildStage() != 0) + return nullptr; + + if (bld->jobs.size() == 0) + return nullptr; + + auto job = bld->jobs[0]; + if (job->flags.bits.suspend) + return job; + + return nullptr; +} + +struct SuspendedBuilding +{ + df::building *bld; + df::coord pos; + bool was_resumed; + bool is_planned; + + SuspendedBuilding(df::building *bld_) : bld(bld_), was_resumed(false), is_planned(false) + { + pos = df::coord(bld->centerx, bld->centery, bld->z); + } + + bool isValid() + { + return bld && Buildings::findAtTile(pos) == bld && get_suspended_job(bld); + } +}; + +static bool enabled = false; +static bool buildings_scanned = false; +static vector suspended_buildings, resumed_buildings; + +void scan_for_suspended_buildings() +{ + if (buildings_scanned) + return; + + for (auto b = world->buildings.all.begin(); b != world->buildings.all.end(); b++) + { + auto bld = *b; + auto job = get_suspended_job(bld); + if (job) + { + SuspendedBuilding sb(bld); + sb.is_planned = job->job_items.size() == 1 && job->job_items[0]->item_type == item_type::NONE; + + auto it = find_if(resumed_buildings.begin(), resumed_buildings.end(), + [&] (SuspendedBuilding &rsb) { return rsb.bld == bld; }); + + sb.was_resumed = it != resumed_buildings.end(); + + suspended_buildings.push_back(sb); + } + } + + buildings_scanned = true; +} + +void show_suspended_buildings() +{ + int32_t vx, vy, vz; + if (!Gui::getViewCoords(vx, vy, vz)) + return; + + auto dims = Gui::getDwarfmodeViewDims(); + int left_margin = vx + dims.map_x2; + int bottom_margin = vy + dims.y2; + + for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end();) + { + if (!sb->isValid()) + { + sb = suspended_buildings.erase(sb); + continue; + } + + if (sb->bld->z == vz && sb->bld->centerx >= vx && sb->bld->centerx <= left_margin && + sb->bld->centery >= vy && sb->bld->centery <= bottom_margin) + { + int x = sb->bld->centerx - vx + 1; + int y = sb->bld->centery - vy + 1; + auto color = COLOR_YELLOW; + if (sb->is_planned) + color = COLOR_GREEN; + else if (sb->was_resumed) + color = COLOR_RED; + + OutputString(color, x, y, "X"); + } + + sb++; + } +} + +void clear_scanned() +{ + buildings_scanned = false; + suspended_buildings.clear(); +} + +void resume_suspended_buildings(color_ostream &out) +{ + out << "Resuming all buildings." << endl; + + for (auto isb = resumed_buildings.begin(); isb != resumed_buildings.end();) + { + if (isb->isValid()) + { + isb++; + continue; + } + + isb = resumed_buildings.erase(isb); + } + + scan_for_suspended_buildings(); + for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end(); sb++) + { + if (sb->is_planned) + continue; + + resumed_buildings.push_back(*sb); + sb->bld->jobs[0]->flags.bits.suspend = false; + } + + clear_scanned(); + + out << resumed_buildings.size() << " buildings resumed" << endl; +} + + +//START Viewscreen Hook +struct resume_hook : public df::viewscreen_dwarfmodest +{ + //START UI Methods + typedef df::viewscreen_dwarfmodest interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + if (enabled && DFHack::World::ReadPauseState() && ui->main.mode == ui_sidebar_mode::Default) + { + scan_for_suspended_buildings(); + show_suspended_buildings(); + } + else + { + clear_scanned(); + } + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(resume_hook, render); + + +static command_result resume_cmd(color_ostream &out, vector & parameters) +{ + bool show_help = false; + if (parameters.empty()) + { + show_help = true; + } + else + { + auto cmd = parameters[0][0]; + if (cmd == 'v') + { + out << "Resume" << endl << "Version: " << PLUGIN_VERSION << endl; + } + else if (cmd == 's') + { + enabled = true; + out << "Overlay enabled" << endl; + } + else if (cmd == 'h') + { + enabled = false; + out << "Overlay disabled" << endl; + } + else if (cmd == 'a') + { + resume_suspended_buildings(out); + } + else + { + show_help = true; + } + } + + if (show_help) + return CR_WRONG_USAGE; + + return CR_OK; +} + + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +{ + if (!gps || !INTERPOSE_HOOK(resume_hook, render).apply()) + out.printerr("Could not insert resume hooks!\n"); + + commands.push_back( + PluginCommand( + "resume", "A plugin to help display and resume suspended constructions conveniently", + resume_cmd, false, + "resume show\n" + " Show overlay when paused:\n" + " Yellow: Suspended construction\n" + " Red: Suspended after resume attempt, possibly stuck\n" + " Green: Planned building waiting for materials\n" + "resume hide\n" + " Hide overlay\n" + "resume all\n" + " Resume all suspended building constructions\n" + )); + + return CR_OK; +} + + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) +{ + switch (event) { + case SC_MAP_LOADED: + suspended_buildings.clear(); + resumed_buildings.clear(); + break; + default: + break; + } + + return CR_OK; +} diff --git a/plugins/stocks.cpp b/plugins/stocks.cpp new file mode 100644 index 000000000..10594fe9e --- /dev/null +++ b/plugins/stocks.cpp @@ -0,0 +1,1024 @@ +#include "uicommon.h" + +#include + +// DF data structure definition headers +#include "DataDefs.h" +#include "Types.h" + +#include "df/item.h" +#include "df/viewscreen_dwarfmodest.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" + +using df::global::world; + +DFHACK_PLUGIN("stocks"); +#define PLUGIN_VERSION 0.2 + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + return CR_OK; +} + +#define MAX_NAME 30 +#define SIDEBAR_WIDTH 30 + +static bool show_debugging = false; + +static void debug(const string &msg) +{ + if (!show_debugging) + return; + + color_ostream_proxy out(Core::getInstance().getConsole()); + out << "DEBUG (stocks): " << msg << endl; +} + + +/* + * 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); +} + + +/* + * Trade + */ + +static df::job *get_item_job(df::item *item) +{ + auto ref = Items::getSpecificRef(item, specific_ref_type::JOB); + if (ref && ref->job) + return ref->job; + + return nullptr; +} + +static df::item *get_container_of(df::item *item) +{ + auto container = Items::getContainer(item); + return (container) ? container : item; +} + +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 check_mandates(df::item *item) +{ + for (auto it = world->mandates.begin(); it != world->mandates.end(); it++) + { + auto mandate = *it; + + if (mandate->mode != 0) + continue; + + if (item->getType() != mandate->item_type || + (mandate->item_subtype != -1 && item->getSubtype() != mandate->item_subtype)) + continue; + + if (mandate->mat_type != -1 && item->getMaterial() != mandate->mat_type) + continue; + + if (mandate->mat_index != -1 && item->getMaterialIndex() != mandate->mat_index) + continue; + + return false; + } + + return true; +} + +static bool can_trade_item(df::item *item) +{ + if (item->flags.bits.owned || item->flags.bits.artifact || item->flags.bits.spider_web || item->flags.bits.in_job) + return false; + + for (size_t i = 0; i < item->general_refs.size(); i++) + { + df::general_ref *ref = item->general_refs[i]; + + switch (ref->getType()) + { + case general_ref_type::UNIT_HOLDER: + return false; + + case general_ref_type::BUILDING_HOLDER: + return false; + + default: + break; + } + } + + for (size_t i = 0; i < item->specific_refs.size(); i++) + { + df::specific_ref *ref = item->specific_refs[i]; + + if (ref->type == specific_ref_type::JOB) + { + // Ignore any items assigned to a job + return false; + } + } + + return check_mandates(item); +} + +static bool can_trade_item_and_container(df::item *item) +{ + item = get_container_of(item); + + if (item->flags.bits.in_inventory) + return false; + + if (!can_trade_item(item)) + return false; + + vector contained_items; + Items::getContainedItems(item, &contained_items); + for (auto cit = contained_items.begin(); cit != contained_items.end(); cit++) + { + if (!can_trade_item(*cit)) + return false; + } + + return true; +} + +static bool is_in_inventory(df::item *item) +{ + item = get_container_of(item); + return item->flags.bits.in_inventory; +} + + +class TradeDepotInfo +{ +public: + TradeDepotInfo() + { + reset(); + } + + void prepareTradeVarables() + { + 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 = caravansAvailable(); + break; + } + } + + bool assignItem(df::item *item) + { + item = get_container_of(item); + if (!can_trade_item_and_container(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; + } + + bool caravansAvailable() + { + if (df::global::ui->caravans.size() == 0) + return false; + + for (auto it = df::global::ui->caravans.begin(); it != df::global::ui->caravans.end(); it++) + { + auto caravan = *it; + auto trade_state = caravan->trade_state; + auto time_remaining = caravan->time_remaining; + if ((trade_state != 1 && trade_state != 2) || time_remaining == 0) + return false; + } + + return true; + } +}; + +static TradeDepotInfo depot_info; + + +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.foreign) + keywords += "foreign "; + + 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_in_inventory(item)) + keywords += "inventory "; + + if (depot_info.canTrade()) + { + if (is_marked_for_trade(item)) + keywords += "trade "; + } + + return keywords; +} + +template +class StockListColumn : public ListColumn +{ + virtual void display_extras(const T &item, int32_t &x, int32_t &y) const + { + 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.foreign) + OutputString(COLOR_BROWN, x, y, "G"); + 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 (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)); + } + } +}; + +struct extra_filters +{ + bool hide_trade_marked, hide_in_inventory; + + extra_filters() + { + reset(); + } + + void reset() + { + hide_in_inventory = false; + hide_trade_marked = false; + } +}; + +class ViewscreenStocks : public dfhack_viewscreen +{ +public: + static df::item_flags hide_flags; + static extra_filters extra_hide_flags; + + ViewscreenStocks() + { + selected_column = 0; + items_column.setTitle("Item"); + 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.foreign = 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; + + populateItems(); + + items_column.selectDefaultEntry(); + } + + static void reset() + { + hide_flags.whole = 0; + extra_hide_flags.reset(); + depot_info.reset(); + } + + void feed(set *input) + { + 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::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + + if (input->count(interface_key::CUSTOM_CTRL_G)) + { + hide_flags.bits.foreign = !hide_flags.bits.foreign; + populateItems(); + } + else 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_R)) + { + 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_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::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 = items_column.getFirstSelectedElem(); + if (!item) + return; + auto pos = getRealPos(item); + if (!pos) + return; + + Screen::dismiss(this); + // 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; + applyFlag(flags); + populateItems(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_F)) + { + df::item_flags flags; + flags.bits.forbid = true; + applyFlag(flags); + populateItems(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_T)) + { + if (apply_to_all) + { + auto &list = items_column.getDisplayList(); + for (auto iter = list.begin(); iter != list.end(); iter++) + { + auto item = (*iter)->elem; + if (item) + depot_info.assignItem(item); + } + + populateItems(); + } + else + { + auto item = items_column.getFirstSelectedElem(); + if (item && depot_info.assignItem(item)) + populateItems(); + } + } + + 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); + send_key(interface_key::CURSOR_DOWN_Z); + send_key(interface_key::CURSOR_UP_Z); + } + + 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", true, left_margin); + OutputString(COLOR_LIGHTRED, x, y, "Press Ctrl-Hotkey to Toggle", true, left_margin); + OutputFilterString(x, y, "In Job", "J", !hide_flags.bits.in_job, true, left_margin, COLOR_LIGHTBLUE); + OutputFilterString(x, y, "Rotten", "X", !hide_flags.bits.rotten, true, left_margin, COLOR_CYAN); + OutputFilterString(x, y, "Foreign Made", "G", !hide_flags.bits.foreign, true, left_margin, COLOR_BROWN); + OutputFilterString(x, y, "Owned", "O", !hide_flags.bits.owned, true, 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, true, left_margin, COLOR_LIGHTMAGENTA); + OutputFilterString(x, y, "On Fire", "R", !hide_flags.bits.on_fire, true, left_margin, COLOR_LIGHTRED); + OutputFilterString(x, y, "Melt", "M", !hide_flags.bits.melt, true, left_margin, COLOR_BLUE); + OutputFilterString(x, y, "In Inventory", "I", !extra_hide_flags.hide_in_inventory, true, left_margin, COLOR_WHITE); + 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); + ++y; + OutputHotkeyString(x, y, "Clear All", "Shift-C", true, left_margin); + OutputHotkeyString(x, y, "Enable All", "Shift-E", 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); + + ++y; + OutputHotkeyString(x, y, "Min Wear: ", "Shift-W"); + OutputString(COLOR_BROWN, x, y, int_to_string(min_wear), true, left_margin); + + ++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", true, left_margin); + OutputHotkeyString(x, y, "Apply to: ", "Shift-A"); + OutputString(COLOR_BROWN, x, y, (apply_to_all) ? "Listed" : "Selected", true, left_margin); + OutputHotkeyString(x, y, "Dump", "Shift-D", true, left_margin); + OutputHotkeyString(x, y, "Forbid", "Shift-F", true, left_margin); + OutputHotkeyString(x, y, "Mark for Trade", "Shift-T", true, left_margin); + } + + std::string getFocusString() { return "stocks_view"; } + +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; + + static df::coord *getRealPos(df::item *item) + { + 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->job) + { + if (ref->job->job_type == job_type::Eat || ref->job->job_type == job_type::Drink) + return nullptr; + + auto unit = Job::getWorker(ref->job); + if (unit) + return &unit->pos; + } + return nullptr; + } + else + { + auto unit = Items::getHolderUnit(item); + if (unit) + return &unit->pos; + + return nullptr; + } + } + + return &item->pos; + } + + void applyFlag(const df::item_flags flags) + { + if (apply_to_all) + { + int state_to_apply = -1; + for (auto iter = items_column.getDisplayList().begin(); iter != items_column.getDisplayList().end(); iter++) + { + auto item = (*iter)->elem; + if (item) + { + // Set all flags based on state of first item in list + if (state_to_apply == -1) + state_to_apply = (item->flags.whole & flags.whole) ? 0 : 1; + + if (state_to_apply) + item->flags.whole |= flags.whole; + else + item->flags.whole &= ~flags.whole; + } + } + } + else + { + auto item = items_column.getFirstSelectedElem(); + if (item) + item->flags.whole ^= flags.whole; + } + } + + void setAllFlags(bool state) + { + hide_flags.bits.in_job = state; + hide_flags.bits.rotten = state; + hide_flags.bits.foreign = 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; + } + + void populateItems() + { + 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.hostile = true; + bad_flags.bits.removed = true; + bad_flags.bits.dead_dwarf = true; + bad_flags.bits.murder = true; + bad_flags.bits.construction = true; + + depot_info.prepareTradeVarables(); + + 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 || 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 (!pos) + continue; + + if (pos->x == -30000) + 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; + + if (extra_hide_flags.hide_in_inventory && container->flags.bits.in_inventory) + continue; + + if (hide_unflagged && (!(item->flags.whole & checked_flags.whole) && + !trade_marked && !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; + + auto label = Items::getDescription(item, 0, false); + 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, true); + + auto entry = ListEntry(label, item, get_keywords(item)); + items_column.add(entry); + } + + items_column.filterDisplay(); + } + + 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; + + +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(new ViewscreenStocks()); + return CR_OK; + } + } + + return CR_WRONG_USAGE; +} + + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +{ + if (!gps) + out.printerr("Could not insert stocks plugin hooks!\n"); + + commands.push_back( + PluginCommand( + "stocks", "An improved stocks display screen", + stocks_cmd, false, "")); + + 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; +} diff --git a/plugins/uicommon.h b/plugins/uicommon.h new file mode 100644 index 000000000..147556ff1 --- /dev/null +++ b/plugins/uicommon.h @@ -0,0 +1,580 @@ +#include +#include +#include +#include + +#include "Core.h" +#include "MiscUtils.h" +#include +#include +#include +#include + +#include "modules/Screen.h" + +#include "df/enabler.h" + + +using std::string; +using std::vector; +using std::map; +using std::ostringstream; +using std::set; + +using namespace DFHack; +using namespace df::enums; + +using df::global::enabler; +using df::global::gps; + + +#ifndef HAVE_NULLPTR +#define nullptr 0L +#endif + +#define COLOR_TITLE COLOR_BLUE +#define COLOR_UNSELECTED COLOR_GREY +#define COLOR_SELECTED COLOR_WHITE +#define COLOR_HIGHLIGHTED COLOR_GREEN + + +template +static void for_each_(vector &v, Fn func) +{ + for_each(v.begin(), v.end(), func); +} + +template +static void for_each_(map &v, Fn func) +{ + for_each(v.begin(), v.end(), func); +} + +template +static void transform_(vector &src, vector &dst, Fn func) +{ + transform(src.begin(), src.end(), back_inserter(dst), func); +} + +typedef int8_t UIColor; + +void OutputString(UIColor color, int &x, int &y, const std::string &text, + bool newline = false, int left_margin = 0, const UIColor bg_color = 0) +{ + Screen::paintString(Screen::Pen(' ', color, bg_color), 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 text_color = COLOR_WHITE, int8_t hotkey_color = COLOR_LIGHTGREEN) +{ + OutputString(hotkey_color, x, y, hotkey); + string display(": "); + display.append(text); + OutputString(text_color, x, y, display, newline, left_margin); +} + +void OutputFilterString(int &x, int &y, const char *text, const char *hotkey, bool state, bool newline = false, + int left_margin = 0, int8_t hotkey_color = COLOR_LIGHTGREEN) +{ + OutputString(hotkey_color, x, y, hotkey); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((state) ? COLOR_WHITE : COLOR_GREY, x, y, text, 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); +} + +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; + } +} + + +/* + * List classes + */ +template +class ListEntry +{ +public: + T elem; + string text, keywords; + bool selected; + + ListEntry(const string text, const T elem, const string keywords = "") : + elem(elem), text(text), selected(false), keywords(keywords) + { + } +}; + +template +class ListColumn +{ +public: + int highlighted_index; + int display_start_offset; + unsigned short text_clip_at; + int32_t bottom_margin, search_margin, left_margin; + bool multiselect; + bool allow_null; + bool auto_select; + bool force_sort; + bool allow_search; + bool feed_changed_highlight; + + ListColumn() + { + bottom_margin = 3; + clear(); + left_margin = 2; + search_margin = 63; + highlighted_index = 0; + text_clip_at = 0; + multiselect = false; + allow_null = true; + auto_select = false; + force_sort = false; + allow_search = true; + feed_changed_highlight = false; + } + + void clear() + { + list.clear(); + display_list.clear(); + display_start_offset = 0; + max_item_width = title.length(); + 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, const T &elem) + { + list.push_back(ListEntry(text, elem)); + if (text.length() > max_item_width) + max_item_width = text.length(); + } + + int fixWidth() + { + if (text_clip_at > 0 && max_item_width > text_clip_at) + max_item_width = text_clip_at; + + for (auto it = list.begin(); it != list.end(); it++) + { + it->text = pad_string(it->text, max_item_width, false); + } + + return left_margin + max_item_width; + } + + 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; + + string item_label = display_list[i]->text; + if (text_clip_at > 0 && item_label.length() > text_clip_at) + item_label.resize(text_clip_at); + + paint_text(fg_color, left_margin, y, item_label, bg_color); + int x = left_margin + display_list[i]->text.length() + 1; + display_extras(display_list[i]->elem, x, y); + } + + if (is_selected_column && allow_search) + { + y = gps->dimy - 3; + int32_t x = search_margin; + OutputHotkeyString(x, y, "Search" ,"S"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString(COLOR_WHITE, x, y, search_string); + OutputString(COLOR_LIGHTGREEN, x, y, "_"); + } + } + + void filterDisplay() + { + ListEntry *prev_selected = (getDisplayListSize() > 0) ? display_list[highlighted_index] : NULL; + display_list.clear(); + + search_string = toLower(search_string); + vector search_tokens; + if (!search_string.empty()) + split_string(&search_tokens, search_string, " "); + + for (size_t i = 0; i < list.size(); i++) + { + ListEntry *entry = &list[i]; + + bool include_item = true; + if (!search_string.empty()) + { + string item_string = toLower(list[i].text); + for (auto si = search_tokens.begin(); si != search_tokens.end(); si++) + { + if (!si->empty() && item_string.find(*si) == string::npos && + list[i].keywords.find(*si) == string::npos) + { + include_item = false; + break; + } + } + } + + if (include_item) + { + display_list.push_back(entry); + if (entry == prev_selected) + highlighted_index = display_list.size() - 1; + } + else if (auto_select) + { + entry->selected = false; + } + } + changeHighlight(0); + feed_changed_highlight = true; + } + + 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; + + feed_changed_highlight = 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 (auto 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 (auto 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 nullptr; + else + return results[0]; + } + + void clearSelection() + { + for_each_(list, [] (ListEntry &e) { e.selected = false; }); + } + + void selectItem(const T elem) + { + int i = 0; + for (; i < display_list.size(); i++) + { + if (display_list[i]->elem == elem) + { + setHighlight(i); + break; + } + } + } + + void clearSearch() + { + search_string.clear(); + filterDisplay(); + } + + size_t getDisplayListSize() + { + return display_list.size(); + } + + vector*> &getDisplayList() + { + return display_list; + } + + size_t getBaseListSize() + { + return list.size(); + } + + bool feed(set *input) + { + feed_changed_highlight = false; + 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::STANDARDSCROLL_PAGEUP)) + { + changeHighlight(0, -1); + } + else if (input->count(interface_key::STANDARDSCROLL_PAGEDOWN)) + { + changeHighlight(0, 1); + } + else if (input->count(interface_key::SELECT) && !auto_select) + { + toggleHighlighted(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_S)) + { + clearSearch(); + } + else if (enabler->tracking_on && gps->mouse_x != -1 && gps->mouse_y != -1 && enabler->mouse_lbut) + { + return setHighlightByMouse(); + } + else if (allow_search) + { + // Search query typing mode always on + df::interface_key last_token = *input->rbegin(); + if ((last_token >= interface_key::STRING_A096 && last_token <= interface_key::STRING_A123) || + last_token == interface_key::STRING_A032) + { + // Standard character + search_string += last_token - ascii_to_enum_offset; + filterDisplay(); + } + else if (last_token == interface_key::STRING_A000) + { + // Backspace + if (search_string.length() > 0) + { + search_string.erase(search_string.length()-1); + filterDisplay(); + } + } + else + { + return false; + } + + return true; + } + 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; + } + + void sort() + { + if (force_sort || list.size() < 100) + std::sort(list.begin(), list.end(), + [] (ListEntry const& a, ListEntry const& b) { return a.text.compare(b.text) < 0; }); + + filterDisplay(); + } + + void setTitle(const string t) + { + title = t; + if (title.length() > max_item_width) + max_item_width = title.length(); + } + + size_t getDisplayedListSize() + { + return display_list.size(); + } + +private: + vector> list; + vector*> display_list; + string search_string; + string title; + int display_max_rows; + int max_item_width; +}; + + diff --git a/plugins/zone.cpp b/plugins/zone.cpp index dff37be0f..5649da252 100644 --- a/plugins/zone.cpp +++ b/plugins/zone.cpp @@ -40,6 +40,11 @@ using namespace std; #include "Console.h" #include "Export.h" #include "PluginManager.h" +#include "MiscUtils.h" + +#include "LuaTools.h" +#include "DataFuncs.h" + #include "modules/Units.h" #include "modules/Maps.h" #include "modules/Gui.h" @@ -47,9 +52,11 @@ using namespace std; #include "modules/MapCache.h" #include "modules/Buildings.h" #include "modules/World.h" +#include "modules/Screen.h" #include "MiscUtils.h" +#include -#include +#include "df/ui.h" #include "df/world.h" #include "df/world_raws.h" #include "df/building_def.h" @@ -60,6 +67,8 @@ using namespace std; #include "df/general_ref_building_civzone_assignedst.h" #include #include +#include "df/viewscreen_dwarfmodest.h" +#include "modules/Translation.h" using std::vector; using std::string; @@ -68,6 +77,8 @@ using namespace df::enums; using df::global::world; using df::global::cursor; using df::global::ui; +using df::global::ui_build_selector; +using df::global::gps; using namespace DFHack::Gui; @@ -238,34 +249,6 @@ command_result init_autonestbox(color_ostream &out); command_result cleanup_autonestbox(color_ostream &out); command_result start_autonestbox(color_ostream &out); -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - commands.push_back(PluginCommand( - "zone", "manage activity zones.", - df_zone, false, - zone_help.c_str() - )); - commands.push_back(PluginCommand( - "autonestbox", "auto-assign nestbox zones.", - df_autonestbox, false, - autonestbox_help.c_str() - )); - commands.push_back(PluginCommand( - "autobutcher", "auto-assign lifestock for butchering.", - df_autobutcher, false, - autobutcher_help.c_str() - )); - init_autobutcher(out); - init_autonestbox(out); - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - cleanup_autobutcher(out); - cleanup_autonestbox(out); - return CR_OK; -} /////////////// // stuff for autonestbox and autobutcher @@ -506,6 +489,24 @@ bool isHunter(df::unit* unit) return false; } +// check if unit is marked as available for adoption +bool isAvailableForAdoption(df::unit* unit) +{ + auto refs = unit->specific_refs; + for(int i=0; itype; + if( reftype == df::specific_ref_type::PETINFO_PET ) + { + //df::pet_info* pet = ref->pet; + return true; + } + } + + return false; +} + // check if creature belongs to the player's civilization // (don't try to pasture/slaughter random untame animals) bool isOwnCiv(df::unit* unit) @@ -520,6 +521,8 @@ bool isOwnRace(df::unit* unit) return unit->race == ui->race_id; } +// get race name by id or unit pointer +// todo: rename these two functions to "getRaceToken" since the output is more of a token string getRaceName(int32_t id) { df::creature_raw *raw = df::global::world->raws.creatures.all[id]; @@ -530,6 +533,15 @@ string getRaceName(df::unit* unit) df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race]; return raw->creature_id; } + +// get plural of race name (used for display in autobutcher UI and for sorting the watchlist) +string getRaceNamePlural(int32_t id) +{ + //WatchedRace * w = watched_races[idx]; + df::creature_raw *raw = df::global::world->raws.creatures.all[id]; + return raw->name[1]; // second field is plural of race name +} + string getRaceBabyName(df::unit* unit) { df::creature_raw *raw = df::global::world->raws.creatures.all[unit->race]; @@ -1024,6 +1036,22 @@ bool isAssigned(df::unit* unit) return assigned; } +bool isAssignedToZone(df::unit* unit) +{ + bool assigned = false; + for (size_t r=0; r < unit->general_refs.size(); r++) + { + df::general_ref * ref = unit->general_refs[r]; + auto rtype = ref->getType(); + if(rtype == df::general_ref_type::BUILDING_CIVZONE_ASSIGNED) + { + assigned = true; + break; + } + } + return assigned; +} + // check if assigned to a chain or built cage // (need to check if the ref needs to be removed, until then touching them is forbidden) bool isChained(df::unit* unit) @@ -2722,6 +2750,8 @@ bool compareUnitAgesOlder(df::unit* i, df::unit* j) return (age_i > age_j); } + + //enum WatchedRaceSubtypes //{ // femaleKid=0, @@ -2737,11 +2767,19 @@ public: bool isWatched; // if true, autobutcher will process this race int raceId; + + // target amounts int fk; // max female kids int mk; // max male kids int fa; // max female adults int ma; // max male adults + // amounts of protected (not butcherable) units + int fk_prot; + int fa_prot; + int mk_prot; + int ma_prot; + // bah, this should better be an array of 4 vectors // that way there's no need for the 4 ugly process methods vector fk_ptr; @@ -2757,6 +2795,7 @@ public: mk = _mk; fa = _fa; ma = _ma; + fk_prot = fa_prot = mk_prot = ma_prot = 0; } ~WatchedRace() @@ -2821,8 +2860,27 @@ public: } } + void PushProtectedUnit(df::unit * unit) + { + if(isFemale(unit)) + { + if(isBaby(unit) || isChild(unit)) + fk_prot++; + else + fa_prot++; + } + else //treat sex n/a like it was male + { + if(isBaby(unit) || isChild(unit)) + mk_prot++; + else + ma_prot++; + } + } + void ClearUnits() { + fk_prot = fa_prot = mk_prot = ma_prot = 0; fk_ptr.clear(); mk_ptr.clear(); fa_ptr.clear(); @@ -2832,7 +2890,7 @@ public: int ProcessUnits_fk() { int subcount = 0; - while(fk_ptr.size() > fk) + while(fk_ptr.size() && (fk_ptr.size() + fk_prot > fk) ) { df::unit* unit = fk_ptr.back(); doMarkForSlaughter(unit); @@ -2845,7 +2903,7 @@ public: int ProcessUnits_mk() { int subcount = 0; - while(mk_ptr.size() > mk) + while(mk_ptr.size() && (mk_ptr.size() + mk_prot > mk) ) { df::unit* unit = mk_ptr.back(); doMarkForSlaughter(unit); @@ -2858,7 +2916,7 @@ public: int ProcessUnits_fa() { int subcount = 0; - while(fa_ptr.size() > fa) + while(fa_ptr.size() && (fa_ptr.size() + fa_prot > fa) ) { df::unit* unit = fa_ptr.back(); doMarkForSlaughter(unit); @@ -2871,7 +2929,7 @@ public: int ProcessUnits_ma() { int subcount = 0; - while(ma_ptr.size() > ma) + while(ma_ptr.size() && (ma_ptr.size() + ma_prot > ma) ) { df::unit* unit = ma_ptr.back(); doMarkForSlaughter(unit); @@ -2898,6 +2956,17 @@ public: // to ignore them for a while but still keep the target count settings std::vector watched_races; +// helper for sorting the watchlist alphabetically +bool compareRaceNames(WatchedRace* i, WatchedRace* j) +{ + string name_i = getRaceNamePlural(i->raceId); + string name_j = getRaceNamePlural(j->raceId); + + return (name_i < name_j); +} + +static void autobutcher_sortWatchList(color_ostream &out); + // default target values for autobutcher static int default_fk = 5; static int default_mk = 1; @@ -3288,6 +3357,7 @@ command_result df_autobutcher(color_ostream &out, vector & parameters) WatchedRace * w = new WatchedRace(watch_race, target_raceids.back(), target_fk, target_mk, target_fa, target_ma); w->UpdateConfig(out); watched_races.push_back(w); + autobutcher_sortWatchList(out); } target_raceids.pop_back(); } @@ -3334,6 +3404,12 @@ command_result autoButcher( color_ostream &out, bool verbose = false ) for(size_t i=0; iunits.all.size(); i++) { df::unit * unit = world->units.all[i]; + + // this check is now divided into two steps, squeezed autowatch into the middle + // first one ignores completely inappropriate units (dead, undead, not belonging to the fort, ...) + // then let autowatch add units to the watchlist which will probably start breeding (owned pets, war animals, ...) + // then process units counting those which can't be butchered (war animals, named pets, ...) + // so that they are treated as "own stock" as well and count towards the target quota if( isDead(unit) || isUndead(unit) || isMarkedForSlaughter(unit) @@ -3341,12 +3417,6 @@ command_result autoButcher( color_ostream &out, bool verbose = false ) || isForest(unit) // ignore merchants' caged animals || !isOwnCiv(unit) || !isTame(unit) - || isWar(unit) // ignore war dogs etc - || isHunter(unit) // ignore hunting dogs etc - // ignore creatures in built cages which are defined as rooms to leave zoos alone - // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) - || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() - || unit->name.has_name ) continue; @@ -3355,24 +3425,39 @@ command_result autoButcher( color_ostream &out, bool verbose = false ) if(!isContainedInItem(unit) && !hasValidMapPos(unit)) continue; + WatchedRace * w = NULL; int watched_index = getWatchedIndex(unit->race); if(watched_index != -1) { - WatchedRace * w = watched_races[watched_index]; - if(w->isWatched) - w->PushUnit(unit); + w = watched_races[watched_index]; } else if(enable_autobutcher_autowatch) { - WatchedRace * w = new WatchedRace(true, unit->race, default_fk, default_mk, default_fa, default_ma); + w = new WatchedRace(true, unit->race, default_fk, default_mk, default_fa, default_ma); w->UpdateConfig(out); watched_races.push_back(w); - w->PushUnit(unit); string announce; - announce = "New race added to autobutcher watchlist: " + getRaceName(w->raceId); + announce = "New race added to autobutcher watchlist: " + getRaceNamePlural(w->raceId); Gui::showAnnouncement(announce, 2, false); - //out << announce << endl; + autobutcher_sortWatchList(out); + } + + if(w && w->isWatched) + { + // don't butcher protected units, but count them as stock as well + // this way they count towards target quota, so if you order that you want 1 female adult cat + // and have 2 cats, one of them being a pet, the other gets butchered + if( isWar(unit) // ignore war dogs etc + || isHunter(unit) // ignore hunting dogs etc + // ignore creatures in built cages which are defined as rooms to leave zoos alone + // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || isAvailableForAdoption(unit) + || unit->name.has_name ) + w->PushProtectedUnit(unit); + else + w->PushUnit(unit); } } @@ -3387,12 +3472,10 @@ command_result autoButcher( color_ostream &out, bool verbose = false ) stringstream ss; ss << slaughter_subcount; string announce; - announce = getRaceName(w->raceId) + " marked for slaughter: " + ss.str(); + announce = getRaceNamePlural(w->raceId) + " marked for slaughter: " + ss.str(); Gui::showAnnouncement(announce, 2, false); - //out << announce << endl; } } - //out << slaughter_count << " units total marked for slaughter." << endl; return CR_OK; } @@ -3476,11 +3559,11 @@ command_result init_autobutcher(color_ostream &out) //out << " mk: " << p->ival(3) << endl; //out << " fa: " << p->ival(4) << endl; //out << " ma: " << p->ival(5) << endl; - WatchedRace * w = new WatchedRace(p->ival(1), p->ival(0), p->ival(2), p->ival(3),p->ival(4),p->ival(5)); w->rconfig = *p; watched_races.push_back(w); } + autobutcher_sortWatchList(out); return CR_OK; } @@ -3546,3 +3629,861 @@ command_result cleanup_autonestbox(color_ostream &out) // (future version of autonestbox could store info about cages for useless male kids) return CR_OK; } + +// abuse WatchedRace struct for counting stocks (since it sorts by gender and age) +// calling method must delete pointer! +WatchedRace * checkRaceStocksTotal(int race) +{ + WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma); + + for(size_t i=0; iunits.all.size(); i++) + { + df::unit * unit = world->units.all[i]; + + if(unit->race != race) + continue; + + if( isDead(unit) + || isUndead(unit) + || isMerchant(unit) // ignore merchants' draught animals + || isForest(unit) // ignore merchants' caged animals + || !isOwnCiv(unit) + ) + continue; + + // found a bugged unit which had invalid coordinates but was not in a cage. + // marking it for slaughter didn't seem to have negative effects, but you never know... + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + w->PushUnit(unit); + } + return w; +} + +WatchedRace * checkRaceStocksProtected(int race) +{ + WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma); + + for(size_t i=0; iunits.all.size(); i++) + { + df::unit * unit = world->units.all[i]; + + if(unit->race != race) + continue; + + if( isDead(unit) + || isUndead(unit) + || isMerchant(unit) // ignore merchants' draught animals + || isForest(unit) // ignore merchants' caged animals + || !isOwnCiv(unit) + ) + continue; + + // found a bugged unit which had invalid coordinates but was not in a cage. + // marking it for slaughter didn't seem to have negative effects, but you never know... + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + if( !isTame(unit) + || isWar(unit) // ignore war dogs etc + || isHunter(unit) // ignore hunting dogs etc + // ignore creatures in built cages which are defined as rooms to leave zoos alone + // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || isAvailableForAdoption(unit) + || unit->name.has_name ) + w->PushUnit(unit); + } + return w; +} + +WatchedRace * checkRaceStocksButcherable(int race) +{ + WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma); + + for(size_t i=0; iunits.all.size(); i++) + { + df::unit * unit = world->units.all[i]; + + if(unit->race != race) + continue; + + if( isDead(unit) + || isUndead(unit) + || isMerchant(unit) // ignore merchants' draught animals + || isForest(unit) // ignore merchants' caged animals + || !isOwnCiv(unit) + || !isTame(unit) + || isWar(unit) // ignore war dogs etc + || isHunter(unit) // ignore hunting dogs etc + // ignore creatures in built cages which are defined as rooms to leave zoos alone + // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || isAvailableForAdoption(unit) + || unit->name.has_name + ) + continue; + + // found a bugged unit which had invalid coordinates but was not in a cage. + // marking it for slaughter didn't seem to have negative effects, but you never know... + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + w->PushUnit(unit); + } + return w; +} + +WatchedRace * checkRaceStocksButcherFlag(int race) +{ + WatchedRace * w = new WatchedRace(true, race, default_fk, default_mk, default_fa, default_ma); + + for(size_t i=0; iunits.all.size(); i++) + { + df::unit * unit = world->units.all[i]; + + if(unit->race != race) + continue; + + if( isDead(unit) + || isUndead(unit) + || isMerchant(unit) // ignore merchants' draught animals + || isForest(unit) // ignore merchants' caged animals + || !isOwnCiv(unit) + ) + continue; + + // found a bugged unit which had invalid coordinates but was not in a cage. + // marking it for slaughter didn't seem to have negative effects, but you never know... + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + if(isMarkedForSlaughter(unit)) + w->PushUnit(unit); + } + return w; +} + +void butcherRace(int race) +{ + for(size_t i=0; iunits.all.size(); i++) + { + df::unit * unit = world->units.all[i]; + + if(unit->race != race) + continue; + + if( isDead(unit) + || isUndead(unit) + || isMerchant(unit) // ignore merchants' draught animals + || isForest(unit) // ignore merchants' caged animals + || !isOwnCiv(unit) + || !isTame(unit) + || isWar(unit) // ignore war dogs etc + || isHunter(unit) // ignore hunting dogs etc + // ignore creatures in built cages which are defined as rooms to leave zoos alone + // (TODO: better solution would be to allow some kind of slaughter cages which you can place near the butcher) + || (isContainedInItem(unit) && isInBuiltCageRoom(unit)) // !!! see comments in isBuiltCageRoom() + || isAvailableForAdoption(unit) + || unit->name.has_name + ) + continue; + + // found a bugged unit which had invalid coordinates but was not in a cage. + // marking it for slaughter didn't seem to have negative effects, but you never know... + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + unit->flags2.bits.slaughter = true; + } +} + +// remove butcher flag for all units of a given race +void unbutcherRace(int race) +{ + for(size_t i=0; iunits.all.size(); i++) + { + df::unit * unit = world->units.all[i]; + + if(unit->race != race) + continue; + + if( isDead(unit) + || isUndead(unit) + || !isMarkedForSlaughter(unit) + ) + continue; + + if(!isContainedInItem(unit) && !hasValidMapPos(unit)) + continue; + + unit->flags2.bits.slaughter = false; + } +} + + +///////////////////////////////////// +// API functions to control autobutcher with a lua script + +static bool autobutcher_isEnabled() { return enable_autobutcher; } +static bool autowatch_isEnabled() { return enable_autobutcher_autowatch; } + +static size_t autobutcher_getSleep(color_ostream &out) +{ + return sleep_autobutcher; +} + +static void autobutcher_setSleep(color_ostream &out, size_t ticks) +{ + sleep_autobutcher = ticks; + if(config_autobutcher.isValid()) + config_autobutcher.ival(1) = sleep_autobutcher; +} + +static void autobutcher_setEnabled(color_ostream &out, bool enable) +{ + if(enable) + { + enable_autobutcher = true; + start_autobutcher(out); + autoButcher(out, false); + } + else + { + enable_autobutcher = false; + if(config_autobutcher.isValid()) + config_autobutcher.ival(0) = enable_autobutcher; + out << "Autobutcher stopped." << endl; + } +} + +static void autowatch_setEnabled(color_ostream &out, bool enable) +{ + if(enable) + { + out << "Auto-adding to watchlist started." << endl; + enable_autobutcher_autowatch = true; + if(config_autobutcher.isValid()) + config_autobutcher.ival(2) = enable_autobutcher_autowatch; + } + else + { + out << "Auto-adding to watchlist stopped." << endl; + enable_autobutcher_autowatch = false; + if(config_autobutcher.isValid()) + config_autobutcher.ival(2) = enable_autobutcher_autowatch; + } +} + +// set all data for a watchlist race in one go +// if race is not already on watchlist it will be added +// params: (id, fk, mk, fa, ma, watched) +static void autobutcher_setWatchListRace(color_ostream &out, size_t id, size_t fk, size_t mk, size_t fa, size_t ma, bool watched) +{ + int watched_index = getWatchedIndex(id); + if(watched_index != -1) + { + out << "updating watchlist entry" << endl; + WatchedRace * w = watched_races[watched_index]; + w->fk = fk; + w->mk = mk; + w->fa = fa; + w->ma = ma; + w->isWatched = watched; + w->UpdateConfig(out); + } + else + { + out << "creating new watchlist entry" << endl; + WatchedRace * w = new WatchedRace(watched, id, fk, mk, fa, ma); //default_fk, default_mk, default_fa, default_ma); + w->UpdateConfig(out); + watched_races.push_back(w); + + string announce; + announce = "New race added to autobutcher watchlist: " + getRaceNamePlural(w->raceId); + Gui::showAnnouncement(announce, 2, false); + autobutcher_sortWatchList(out); + } +} + +// remove entry from watchlist +static void autobutcher_removeFromWatchList(color_ostream &out, size_t id) +{ + int watched_index = getWatchedIndex(id); + if(watched_index != -1) + { + out << "updating watchlist entry" << endl; + WatchedRace * w = watched_races[watched_index]; + w->RemoveConfig(out); + watched_races.erase(watched_races.begin() + watched_index); + } +} + +// sort watchlist alphabetically +static void autobutcher_sortWatchList(color_ostream &out) +{ + sort(watched_races.begin(), watched_races.end(), compareRaceNames); +} + +// set default target values for new races +static void autobutcher_setDefaultTargetNew(color_ostream &out, size_t fk, size_t mk, size_t fa, size_t ma) +{ + default_fk = fk; + default_mk = mk; + default_fa = fa; + default_ma = ma; + if(config_autobutcher.isValid()) + { + config_autobutcher.ival(3) = default_fk; + config_autobutcher.ival(4) = default_mk; + config_autobutcher.ival(5) = default_fa; + config_autobutcher.ival(6) = default_ma; + } +} + +// set default target values for ALL races (update watchlist and set new default) +static void autobutcher_setDefaultTargetAll(color_ostream &out, size_t fk, size_t mk, size_t fa, size_t ma) +{ + for(size_t i=0; ifk = fk; + w->mk = mk; + w->fa = fa; + w->ma = ma; + w->UpdateConfig(out); + } + autobutcher_setDefaultTargetNew(out, fk, mk, fa, ma); +} + +static void autobutcher_butcherRace(color_ostream &out, size_t id) +{ + butcherRace(id); +} + +static void autobutcher_unbutcherRace(color_ostream &out, size_t id) +{ + unbutcherRace(id); +} + +// push autobutcher settings on lua stack +static int autobutcher_getSettings(lua_State *L) +{ + color_ostream &out = *Lua::GetOutput(L); + lua_newtable(L); + int ctable = lua_gettop(L); + Lua::SetField(L, enable_autobutcher, ctable, "enable_autobutcher"); + Lua::SetField(L, enable_autobutcher_autowatch, ctable, "enable_autowatch"); + Lua::SetField(L, default_fk, ctable, "fk"); + Lua::SetField(L, default_mk, ctable, "mk"); + Lua::SetField(L, default_fa, ctable, "fa"); + Lua::SetField(L, default_ma, ctable, "ma"); + Lua::SetField(L, sleep_autobutcher, ctable, "sleep"); + return 1; +} + +// push the watchlist vector as nested table on the lua stack +static int autobutcher_getWatchList(lua_State *L) +{ + color_ostream &out = *Lua::GetOutput(L); + lua_newtable(L); + + for(size_t i=0; iraceId, ctable, "id"); + Lua::SetField(L, w->isWatched, ctable, "watched"); + Lua::SetField(L, getRaceNamePlural(w->raceId), ctable, "name"); + Lua::SetField(L, w->fk, ctable, "fk"); + Lua::SetField(L, w->mk, ctable, "mk"); + Lua::SetField(L, w->fa, ctable, "fa"); + Lua::SetField(L, w->ma, ctable, "ma"); + + int id = w->raceId; + + w = checkRaceStocksTotal(id); + Lua::SetField(L, w->fk_ptr.size(), ctable, "fk_total"); + Lua::SetField(L, w->mk_ptr.size(), ctable, "mk_total"); + Lua::SetField(L, w->fa_ptr.size(), ctable, "fa_total"); + Lua::SetField(L, w->ma_ptr.size(), ctable, "ma_total"); + delete w; + + w = checkRaceStocksProtected(id); + Lua::SetField(L, w->fk_ptr.size(), ctable, "fk_protected"); + Lua::SetField(L, w->mk_ptr.size(), ctable, "mk_protected"); + Lua::SetField(L, w->fa_ptr.size(), ctable, "fa_protected"); + Lua::SetField(L, w->ma_ptr.size(), ctable, "ma_protected"); + delete w; + + w = checkRaceStocksButcherable(id); + Lua::SetField(L, w->fk_ptr.size(), ctable, "fk_butcherable"); + Lua::SetField(L, w->mk_ptr.size(), ctable, "mk_butcherable"); + Lua::SetField(L, w->fa_ptr.size(), ctable, "fa_butcherable"); + Lua::SetField(L, w->ma_ptr.size(), ctable, "ma_butcherable"); + delete w; + + w = checkRaceStocksButcherFlag(id); + Lua::SetField(L, w->fk_ptr.size(), ctable, "fk_butcherflag"); + Lua::SetField(L, w->mk_ptr.size(), ctable, "mk_butcherflag"); + Lua::SetField(L, w->fa_ptr.size(), ctable, "fa_butcherflag"); + Lua::SetField(L, w->ma_ptr.size(), ctable, "ma_butcherflag"); + delete w; + + lua_rawseti(L, -2, i+1); + } + + return 1; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(autobutcher_isEnabled), + DFHACK_LUA_FUNCTION(autowatch_isEnabled), + DFHACK_LUA_FUNCTION(autobutcher_setEnabled), + DFHACK_LUA_FUNCTION(autowatch_setEnabled), + DFHACK_LUA_FUNCTION(autobutcher_getSleep), + DFHACK_LUA_FUNCTION(autobutcher_setSleep), + DFHACK_LUA_FUNCTION(autobutcher_setWatchListRace), + DFHACK_LUA_FUNCTION(autobutcher_setDefaultTargetNew), + DFHACK_LUA_FUNCTION(autobutcher_setDefaultTargetAll), + DFHACK_LUA_FUNCTION(autobutcher_butcherRace), + DFHACK_LUA_FUNCTION(autobutcher_unbutcherRace), + DFHACK_LUA_FUNCTION(autobutcher_removeFromWatchList), + DFHACK_LUA_FUNCTION(autobutcher_sortWatchList), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(autobutcher_getSettings), + DFHACK_LUA_COMMAND(autobutcher_getWatchList), + DFHACK_LUA_END +}; + +// end lua API + + + +//START zone filters +using df::global::ui_building_item_cursor; +using df::global::ui_building_assign_type; +using df::global::ui_building_assign_is_marked; +using df::global::ui_building_assign_units; +using df::global::ui_building_assign_items; + +using df::global::ui_building_in_assign; + +static const int ascii_to_enum_offset = interface_key::STRING_A048 - '0'; + +void OutputString(int8_t color, int &x, int y, const std::string &text) +{ + Screen::paintString(Screen::Pen(' ', color, 0), x, y, text); + x += text.length(); +} + +class zone_filter +{ +public: + zone_filter() + { + initialized = false; + } + + void initialize(const df::ui_sidebar_mode &mode) + { + if (!initialized) + { + this->mode = mode; + saved_ui_building_assign_type.clear(); + saved_ui_building_assign_units.clear(); + saved_ui_building_assign_items.clear(); + saved_ui_building_assign_is_marked.clear(); + saved_indexes.clear(); + + for (size_t i = 0; i < ui_building_assign_units->size(); i++) + { + saved_ui_building_assign_type.push_back(ui_building_assign_type->at(i)); + saved_ui_building_assign_units.push_back(ui_building_assign_units->at(i)); + saved_ui_building_assign_items.push_back(ui_building_assign_items->at(i)); + saved_ui_building_assign_is_marked.push_back(ui_building_assign_is_marked->at(i)); + } + + search_string.clear(); + show_non_grazers = show_pastured = show_noncaged = show_male = show_female = show_other_zones = true; + entry_mode = false; + + initialized = true; + } + } + + void deinitialize() + { + initialized = false; + } + + void apply_filters() + { + if (saved_indexes.size() > 0) + { + bool list_has_been_sorted = (ui_building_assign_units->size() == reference_list.size() + && *ui_building_assign_units != reference_list); + + for (size_t i = 0; i < saved_indexes.size(); i++) + { + int adjusted_item_index = i; + if (list_has_been_sorted) + { + for (int j = 0; j < ui_building_assign_units->size(); j++) + { + if (ui_building_assign_units->at(j) == reference_list[i]) + { + adjusted_item_index = j; + break; + } + } + } + + saved_ui_building_assign_is_marked[saved_indexes[i]] = ui_building_assign_is_marked->at(adjusted_item_index); + } + } + + string search_string_l = toLower(search_string); + saved_indexes.clear(); + ui_building_assign_type->clear(); + ui_building_assign_is_marked->clear(); + ui_building_assign_units->clear(); + ui_building_assign_items->clear(); + + for (size_t i = 0; i < saved_ui_building_assign_units.size(); i++) + { + df::unit *curr_unit = saved_ui_building_assign_units[i]; + + if (!curr_unit) + continue; + + if (!show_non_grazers && !isGrazer(curr_unit)) + continue; + + if (!show_pastured && isAssignedToZone(curr_unit)) + continue; + + if (!show_noncaged) + { + // must be in a container + if(!isContainedInItem(curr_unit)) + continue; + // but exclude built cages (zoos, traps, ...) to avoid "accidental" pitting of creatures you'd prefer to keep + if (isInBuiltCage(curr_unit)) + continue; + } + + if (!show_male && isMale(curr_unit)) + continue; + + if (!show_female && isFemale(curr_unit)) + continue; + + if (!search_string_l.empty()) + { + string desc = Translation::TranslateName( + Units::getVisibleName(curr_unit), false); + + desc += Units::getProfessionName(curr_unit); + desc = toLower(desc); + + if (desc.find(search_string_l) == string::npos) + continue; + } + + ui_building_assign_type->push_back(saved_ui_building_assign_type[i]); + ui_building_assign_units->push_back(curr_unit); + ui_building_assign_items->push_back(saved_ui_building_assign_items[i]); + ui_building_assign_is_marked->push_back(saved_ui_building_assign_is_marked[i]); + + saved_indexes.push_back(i); // Used to map filtered indexes back to original, if needed + } + + reference_list = *ui_building_assign_units; + *ui_building_item_cursor = 0; + } + + bool handle_input(const set *input) + { + if (!initialized) + return false; + + bool key_processed = true; + + if (entry_mode) + { + // Query typing mode + + if (input->count(interface_key::SECONDSCROLL_UP) || input->count(interface_key::SECONDSCROLL_DOWN) || + input->count(interface_key::SECONDSCROLL_PAGEUP) || input->count(interface_key::SECONDSCROLL_PAGEDOWN)) + { + // Arrow key pressed. Leave entry mode and allow screen to process key + entry_mode = false; + return false; + } + + 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; + apply_filters(); + } + else if (last_token == interface_key::STRING_A000) + { + // Backspace + if (search_string.length() > 0) + { + search_string.erase(search_string.length()-1); + apply_filters(); + } + } + else if (input->count(interface_key::SELECT) || input->count(interface_key::LEAVESCREEN)) + { + // ENTER or ESC: leave typing mode + entry_mode = false; + } + } + // Not in query typing mode + else if (input->count(interface_key::CUSTOM_SHIFT_G) && + (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::QueryBuilding)) + { + show_non_grazers = !show_non_grazers; + apply_filters(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_C) && + (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding)) + { + show_noncaged = !show_noncaged; + apply_filters(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_P) && + (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding)) + { + show_pastured = !show_pastured; + apply_filters(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_M) && + (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding)) + { + show_male = !show_male; + apply_filters(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_F) && + (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::ZonesPitInfo || mode == ui_sidebar_mode::QueryBuilding)) + { + show_female = !show_female; + apply_filters(); + } + else if (input->count(interface_key::CUSTOM_S)) + { + // Hotkey pressed, enter typing mode + entry_mode = true; + } + else if (input->count(interface_key::CUSTOM_SHIFT_S)) + { + // Shift + Hotkey pressed, clear query + search_string.clear(); + apply_filters(); + } + else + { + // Not a key for us, pass it on to the screen + key_processed = false; + } + + return key_processed || entry_mode; // Only pass unrecognized keys down if not in typing mode + } + + void do_render() + { + if (!initialized) + return; + + int left_margin = gps->dimx - 30; + int8_t a = *df::global::ui_menu_width; + int8_t b = *df::global::ui_area_map_width; + if ((a == 1 && b > 1) || (a == 2 && b == 2)) + left_margin -= 24; + + int x = left_margin; + int y = 24; + + OutputString(COLOR_BROWN, x, y, "DFHack Filtering"); + x = left_margin; + ++y; + OutputString(COLOR_LIGHTGREEN, x, y, "s"); + OutputString(COLOR_WHITE, x, y, ": Search"); + if (!search_string.empty() || entry_mode) + { + OutputString(COLOR_WHITE, x, y, ": "); + if (!search_string.empty()) + OutputString(COLOR_WHITE, x, y, search_string); + if (entry_mode) + OutputString(COLOR_LIGHTGREEN, x, y, "_"); + } + + if (mode == ui_sidebar_mode::ZonesPenInfo || mode == ui_sidebar_mode::QueryBuilding) + { + x = left_margin; + y += 2; + OutputString(COLOR_LIGHTGREEN, x, y, "G"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_non_grazers) ? COLOR_WHITE : COLOR_GREY, x, y, "Non-Grazing"); + + x = left_margin; + ++y; + OutputString(COLOR_LIGHTGREEN, x, y, "C"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_noncaged) ? COLOR_WHITE : COLOR_GREY, x, y, "Not Caged"); + + x = left_margin; + ++y; + OutputString(COLOR_LIGHTGREEN, x, y, "P"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_pastured) ? COLOR_WHITE : COLOR_GREY, x, y, "Currently Pastured"); + + x = left_margin; + ++y; + OutputString(COLOR_LIGHTGREEN, x, y, "F"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_female) ? COLOR_WHITE : COLOR_GREY, x, y, "Female"); + + x = left_margin; + ++y; + OutputString(COLOR_LIGHTGREEN, x, y, "M"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_male) ? COLOR_WHITE : COLOR_GREY, x, y, "Male"); + } + + // pits don't have grazer filter because it seems pointless + if (mode == ui_sidebar_mode::ZonesPitInfo) + { + x = left_margin; + y += 2; + OutputString(COLOR_LIGHTGREEN, x, y, "C"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_noncaged) ? COLOR_WHITE : COLOR_GREY, x, y, "Not Caged"); + + x = left_margin; + ++y; + OutputString(COLOR_LIGHTGREEN, x, y, "P"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_pastured) ? COLOR_WHITE : COLOR_GREY, x, y, "Currently Pastured"); + + x = left_margin; + ++y; + OutputString(COLOR_LIGHTGREEN, x, y, "F"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_female) ? COLOR_WHITE : COLOR_GREY, x, y, "Female"); + + x = left_margin; + ++y; + OutputString(COLOR_LIGHTGREEN, x, y, "M"); + OutputString(COLOR_WHITE, x, y, ": "); + OutputString((show_male) ? COLOR_WHITE : COLOR_GREY, x, y, "Male"); + } + } + +private: + df::ui_sidebar_mode mode; + string search_string; + bool initialized; + bool entry_mode; + bool show_non_grazers, show_pastured, show_noncaged, show_male, show_female, show_other_zones; + + std::vector saved_ui_building_assign_type; + std::vector saved_ui_building_assign_units, reference_list; + std::vector saved_ui_building_assign_items; + std::vector saved_ui_building_assign_is_marked; + + vector saved_indexes; + +}; + +struct zone_hook : public df::viewscreen_dwarfmodest +{ + typedef df::viewscreen_dwarfmodest interpose_base; + static zone_filter filter; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!filter.handle_input(input)) + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + if ( ( (ui->main.mode == ui_sidebar_mode::ZonesPenInfo || ui->main.mode == ui_sidebar_mode::ZonesPitInfo) && + ui_building_assign_type && ui_building_assign_units && + ui_building_assign_is_marked && ui_building_assign_items && + ui_building_assign_type->size() == ui_building_assign_units->size() && + ui_building_item_cursor) + // allow mode QueryBuilding, but only for cages (bedrooms will crash DF with this code, chains don't work either etc) + || + ( ui->main.mode == ui_sidebar_mode::QueryBuilding && + ui_building_in_assign && *ui_building_in_assign && + ui_building_assign_type && ui_building_assign_units && + ui_building_assign_type->size() == ui_building_assign_units->size() && + ui_building_item_cursor && + world->selected_building && isCage(world->selected_building) ) + ) + { + if (vector_get(*ui_building_assign_units, *ui_building_item_cursor)) + filter.initialize(ui->main.mode); + } + else + { + filter.deinitialize(); + } + + INTERPOSE_NEXT(render)(); + + filter.do_render(); + + } +}; + +zone_filter zone_hook::filter; + +IMPLEMENT_VMETHOD_INTERPOSE(zone_hook, feed); +IMPLEMENT_VMETHOD_INTERPOSE(zone_hook, render); +//END zone filters + + +DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +{ + if (!gps || !INTERPOSE_HOOK(zone_hook, feed).apply() || !INTERPOSE_HOOK(zone_hook, render).apply()) + out.printerr("Could not insert jobutils hooks!\n"); + + commands.push_back(PluginCommand( + "zone", "manage activity zones.", + df_zone, false, + zone_help.c_str() + )); + commands.push_back(PluginCommand( + "autonestbox", "auto-assign nestbox zones.", + df_autonestbox, false, + autonestbox_help.c_str() + )); + commands.push_back(PluginCommand( + "autobutcher", "auto-assign lifestock for butchering.", + df_autobutcher, false, + autobutcher_help.c_str() + )); + init_autobutcher(out); + init_autonestbox(out); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + cleanup_autobutcher(out); + cleanup_autonestbox(out); + return CR_OK; +} diff --git a/scripts/gui/autobutcher.lua b/scripts/gui/autobutcher.lua new file mode 100644 index 000000000..013406f33 --- /dev/null +++ b/scripts/gui/autobutcher.lua @@ -0,0 +1,656 @@ +-- A GUI front-end for the autobutcher plugin. + +local gui = require 'gui' +local utils = require 'utils' +local widgets = require 'gui.widgets' +local dlg = require 'gui.dialogs' + +local plugin = require 'plugins.zone' + +WatchList = defclass(WatchList, gui.FramedScreen) + +WatchList.ATTRS { + frame_title = 'Autobutcher Watchlist', + frame_inset = 0, -- cover full DF window + frame_background = COLOR_BLACK, + frame_style = gui.BOUNDARY_FRAME, +} + +-- width of the race name column in the UI +local racewidth = 25 + +function nextAutowatchState() + if(plugin.autowatch_isEnabled()) then + return 'Stop ' + end + return 'Start' +end + +function nextAutobutcherState() + if(plugin.autobutcher_isEnabled()) then + return 'Stop ' + end + return 'Start' +end + +function getSleepTimer() + return plugin.autobutcher_getSleep() +end + +function setSleepTimer(ticks) + plugin.autobutcher_setSleep(ticks) +end + +function WatchList:init(args) + local colwidth = 7 + self:addviews{ + widgets.Panel{ + frame = { l = 0, r = 0 }, + frame_inset = 1, + subviews = { + widgets.Label{ + frame = { l = 0, t = 0 }, + text_pen = COLOR_CYAN, + text = { + { text = 'Race', width = racewidth }, ' ', + { text = 'female', width = colwidth }, ' ', + { text = ' male', width = colwidth }, ' ', + { text = 'Female', width = colwidth }, ' ', + { text = ' Male', width = colwidth }, ' ', + { text = 'watch? ' }, + { text = ' butchering' }, + NEWLINE, + { text = '', width = racewidth }, ' ', + { text = ' kids', width = colwidth }, ' ', + { text = ' kids', width = colwidth }, ' ', + { text = 'adults', width = colwidth }, ' ', + { text = 'adults', width = colwidth }, ' ', + { text = ' ' }, + { text = ' ordered' }, + } + }, + widgets.List{ + view_id = 'list', + frame = { t = 3, b = 5 }, + not_found_label = 'Watchlist is empty.', + edit_pen = COLOR_LIGHTCYAN, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + cursor_pen = { fg = COLOR_WHITE, bg = COLOR_GREEN }, + --on_select = self:callback('onSelectEntry'), + }, + widgets.Label{ + view_id = 'bottom_ui', + frame = { b = 0, h = 1 }, + text = 'filled by updateBottom()' + } + } + }, + } + + self:initListChoices() + self:updateBottom() +end + +-- change the viewmode for stock data displayed in left section of columns +local viewmodes = { 'total stock', 'protected stock', 'butcherable', 'butchering ordered' } +local viewmode = 1 +function WatchList:onToggleView() + if viewmode < #viewmodes then + viewmode = viewmode + 1 + else + viewmode = 1 + end + self:initListChoices() + self:updateBottom() +end + +-- update the bottom part of the UI (after sleep timer changed etc) +function WatchList:updateBottom() + self.subviews.bottom_ui:setText( + { + { key = 'CUSTOM_SHIFT_V', text = ': View in colums shows: '..viewmodes[viewmode]..' / target max', + on_activate = self:callback('onToggleView') }, NEWLINE, + { key = 'CUSTOM_F', text = ': f kids', + on_activate = self:callback('onEditFK') }, ', ', + { key = 'CUSTOM_M', text = ': m kids', + on_activate = self:callback('onEditMK') }, ', ', + { key = 'CUSTOM_SHIFT_F', text = ': f adults', + on_activate = self:callback('onEditFA') }, ', ', + { key = 'CUSTOM_SHIFT_M', text = ': m adults', + on_activate = self:callback('onEditMA') }, '. ', + { key = 'CUSTOM_W', text = ': Toggle watch', + on_activate = self:callback('onToggleWatching') }, '. ', + { key = 'CUSTOM_X', text = ': Delete', + on_activate = self:callback('onDeleteEntry') }, '. ', NEWLINE, + --{ key = 'CUSTOM_A', text = ': Add race', + -- on_activate = self:callback('onAddRace') }, ', ', + { key = 'CUSTOM_SHIFT_R', text = ': Set whole row', + on_activate = self:callback('onSetRow') }, '. ', + { key = 'CUSTOM_B', text = ': Remove butcher orders', + on_activate = self:callback('onUnbutcherRace') }, '. ', + { key = 'CUSTOM_SHIFT_B', text = ': Butcher race', + on_activate = self:callback('onButcherRace') }, '. ', NEWLINE, + { key = 'CUSTOM_SHIFT_A', text = ': '..nextAutobutcherState()..' Autobutcher', + on_activate = self:callback('onToggleAutobutcher') }, '. ', + { key = 'CUSTOM_SHIFT_W', text = ': '..nextAutowatchState()..' Autowatch', + on_activate = self:callback('onToggleAutowatch') }, '. ', + { key = 'CUSTOM_SHIFT_S', text = ': Sleep ('..getSleepTimer()..' ticks)', + on_activate = self:callback('onEditSleepTimer') }, '. ', + }) +end + +function stringify(number) + -- cap displayed number to 3 digits + -- after population of 50 per race is reached pets stop breeding anyways + -- so probably this could safely be reduced to 99 + local max = 999 + if number > max then number = max end + return tostring(number) +end + +function WatchList:initListChoices() + + local choices = {} + + -- first two rows are for "edit all races" and "edit new races" + local settings = plugin.autobutcher_getSettings() + local fk = stringify(settings.fk) + local fa = stringify(settings.fa) + local mk = stringify(settings.mk) + local ma = stringify(settings.ma) + + local watched = '' + + local colwidth = 7 + + table.insert (choices, { + text = { + { text = '!! ALL RACES PLUS NEW', width = racewidth, pad_char = ' ' }, --' ', + { text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ', + { text = fk, width = 3, rjustify = false, pad_char = ' ' }, ' ', + { text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ', + { text = mk, width = 3, rjustify = false, pad_char = ' ' }, ' ', + { text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ', + { text = fa, width = 3, rjustify = false, pad_char = ' ' }, ' ', + { text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ', + { text = ma, width = 3, rjustify = false, pad_char = ' ' }, ' ', + { text = watched, width = 6, rjustify = true } + } + }) + + table.insert (choices, { + text = { + { text = '!! ONLY NEW RACES', width = racewidth, pad_char = ' ' }, --' ', + { text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ', + { text = fk, width = 3, rjustify = false, pad_char = ' ' }, ' ', + { text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ', + { text = mk, width = 3, rjustify = false, pad_char = ' ' }, ' ', + { text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ', + { text = fa, width = 3, rjustify = false, pad_char = ' ' }, ' ', + { text = ' ', width = 3, rjustify = true, pad_char = ' ' }, ' ', + { text = ma, width = 3, rjustify = false, pad_char = ' ' }, ' ', + { text = watched, width = 6, rjustify = true } + } + }) + + local watchlist = plugin.autobutcher_getWatchList() + + for i,entry in ipairs(watchlist) do + fk = stringify(entry.fk) + fa = stringify(entry.fa) + mk = stringify(entry.mk) + ma = stringify(entry.ma) + if viewmode == 1 then + fkc = stringify(entry.fk_total) + fac = stringify(entry.fa_total) + mkc = stringify(entry.mk_total) + mac = stringify(entry.ma_total) + end + if viewmode == 2 then + fkc = stringify(entry.fk_protected) + fac = stringify(entry.fa_protected) + mkc = stringify(entry.mk_protected) + mac = stringify(entry.ma_protected) + end + if viewmode == 3 then + fkc = stringify(entry.fk_butcherable) + fac = stringify(entry.fa_butcherable) + mkc = stringify(entry.mk_butcherable) + mac = stringify(entry.ma_butcherable) + end + if viewmode == 4 then + fkc = stringify(entry.fk_butcherflag) + fac = stringify(entry.fa_butcherflag) + mkc = stringify(entry.mk_butcherflag) + mac = stringify(entry.ma_butcherflag) + end + local butcher_ordered = entry.fk_butcherflag + entry.fa_butcherflag + entry.mk_butcherflag + entry.ma_butcherflag + local bo = ' ' + if butcher_ordered > 0 then bo = stringify(butcher_ordered) end + + local watched = 'no' + if entry.watched then watched = 'yes' end + + local racestr = entry.name + + -- highlight entries where the target quota can't be met because too many are protected + bad_pen = COLOR_LIGHTRED + good_pen = NONE -- this is stupid, but it works. sue me + fk_pen = good_pen + fa_pen = good_pen + mk_pen = good_pen + ma_pen = good_pen + if entry.fk_protected > entry.fk then fk_pen = bad_pen end + if entry.fa_protected > entry.fa then fa_pen = bad_pen end + if entry.mk_protected > entry.mk then mk_pen = bad_pen end + if entry.ma_protected > entry.ma then ma_pen = bad_pen end + + table.insert (choices, { + text = { + { text = racestr, width = racewidth, pad_char = ' ' }, --' ', + { text = fkc, width = 3, rjustify = true, pad_char = ' ' }, '/', + { text = fk, width = 3, rjustify = false, pad_char = ' ', pen = fk_pen }, ' ', + { text = mkc, width = 3, rjustify = true, pad_char = ' ' }, '/', + { text = mk, width = 3, rjustify = false, pad_char = ' ', pen = mk_pen }, ' ', + { text = fac, width = 3, rjustify = true, pad_char = ' ' }, '/', + { text = fa, width = 3, rjustify = false, pad_char = ' ', pen = fa_pen }, ' ', + { text = mac, width = 3, rjustify = true, pad_char = ' ' }, '/', + { text = ma, width = 3, rjustify = false, pad_char = ' ', pen = ma_pen }, ' ', + { text = watched, width = 6, rjustify = true, pad_char = ' ' }, ' ', + { text = bo, width = 8, rjustify = true, pad_char = ' ' } + }, + obj = entry, + }) + end + + local list = self.subviews.list + list:setChoices(choices) +end + +function WatchList:onInput(keys) + if keys.LEAVESCREEN then + self:dismiss() + else + WatchList.super.onInput(self, keys) + end +end + +-- check the user input for target population values +function WatchList:checkUserInput(count, text) + if count == nil then + dlg.showMessage('Invalid Number', 'This is not a number: '..text..NEWLINE..'(for zero enter a 0)', COLOR_LIGHTRED) + return false + end + if count < 0 then + dlg.showMessage('Invalid Number', 'Negative numbers make no sense!', COLOR_LIGHTRED) + return false + end + return true +end + +-- check the user input for sleep timer +function WatchList:checkUserInputSleep(count, text) + if count == nil then + dlg.showMessage('Invalid Number', 'This is not a number: '..text..NEWLINE..'(for zero enter a 0)', COLOR_LIGHTRED) + return false + end + if count < 1000 then + dlg.showMessage('Invalid Number', + 'Minimum allowed timer value is 1000!'..NEWLINE..'Too low values could decrease performance'..NEWLINE..'and are not necessary!', + COLOR_LIGHTRED) + return false + end + return true +end + +function WatchList:onEditFK() + local selidx,selobj = self.subviews.list:getSelected() + local settings = plugin.autobutcher_getSettings() + local fk = settings.fk + local mk = settings.mk + local fa = settings.fa + local ma = settings.ma + local race = 'ALL RACES PLUS NEW' + local id = -1 + local watched = false + + if selidx == 2 then + race = 'ONLY NEW RACES' + end + + if selidx > 2 then + local entry = selobj.obj + fk = entry.fk + mk = entry.mk + fa = entry.fa + ma = entry.ma + race = entry.name + id = entry.id + watched = entry.watched + end + + dlg.showInputPrompt( + 'Race: '..race, + 'Enter desired maximum of female kids:', + COLOR_WHITE, + ' '..fk, + function(text) + local count = tonumber(text) + if self:checkUserInput(count, text) then + fk = count + if selidx == 1 then + plugin.autobutcher_setDefaultTargetAll( fk, mk, fa, ma ) + end + if selidx == 2 then + plugin.autobutcher_setDefaultTargetNew( fk, mk, fa, ma ) + end + if selidx > 2 then + plugin.autobutcher_setWatchListRace(id, fk, mk, fa, ma, watched) + end + self:initListChoices() + end + end + ) +end + +function WatchList:onEditMK() + local selidx,selobj = self.subviews.list:getSelected() + local settings = plugin.autobutcher_getSettings() + local fk = settings.fk + local mk = settings.mk + local fa = settings.fa + local ma = settings.ma + local race = 'ALL RACES PLUS NEW' + local id = -1 + local watched = false + + if selidx == 2 then + race = 'ONLY NEW RACES' + end + + if selidx > 2 then + local entry = selobj.obj + fk = entry.fk + mk = entry.mk + fa = entry.fa + ma = entry.ma + race = entry.name + id = entry.id + watched = entry.watched + end + + dlg.showInputPrompt( + 'Race: '..race, + 'Enter desired maximum of male kids:', + COLOR_WHITE, + ' '..mk, + function(text) + local count = tonumber(text) + if self:checkUserInput(count, text) then + mk = count + if selidx == 1 then + plugin.autobutcher_setDefaultTargetAll( fk, mk, fa, ma ) + end + if selidx == 2 then + plugin.autobutcher_setDefaultTargetNew( fk, mk, fa, ma ) + end + if selidx > 2 then + plugin.autobutcher_setWatchListRace(id, fk, mk, fa, ma, watched) + end + self:initListChoices() + end + end + ) +end + +function WatchList:onEditFA() + local selidx,selobj = self.subviews.list:getSelected() + local settings = plugin.autobutcher_getSettings() + local fk = settings.fk + local mk = settings.mk + local fa = settings.fa + local ma = settings.ma + local race = 'ALL RACES PLUS NEW' + local id = -1 + local watched = false + + if selidx == 2 then + race = 'ONLY NEW RACES' + end + + if selidx > 2 then + local entry = selobj.obj + fk = entry.fk + mk = entry.mk + fa = entry.fa + ma = entry.ma + race = entry.name + id = entry.id + watched = entry.watched + end + + dlg.showInputPrompt( + 'Race: '..race, + 'Enter desired maximum of female adults:', + COLOR_WHITE, + ' '..fa, + function(text) + local count = tonumber(text) + if self:checkUserInput(count, text) then + fa = count + if selidx == 1 then + plugin.autobutcher_setDefaultTargetAll( fk, mk, fa, ma ) + end + if selidx == 2 then + plugin.autobutcher_setDefaultTargetNew( fk, mk, fa, ma ) + end + if selidx > 2 then + plugin.autobutcher_setWatchListRace(id, fk, mk, fa, ma, watched) + end + self:initListChoices() + end + end + ) +end + +function WatchList:onEditMA() + local selidx,selobj = self.subviews.list:getSelected() + local settings = plugin.autobutcher_getSettings() + local fk = settings.fk + local mk = settings.mk + local fa = settings.fa + local ma = settings.ma + local race = 'ALL RACES PLUS NEW' + local id = -1 + local watched = false + + if selidx == 2 then + race = 'ONLY NEW RACES' + end + + if selidx > 2 then + local entry = selobj.obj + fk = entry.fk + mk = entry.mk + fa = entry.fa + ma = entry.ma + race = entry.name + id = entry.id + watched = entry.watched + end + + dlg.showInputPrompt( + 'Race: '..race, + 'Enter desired maximum of male adults:', + COLOR_WHITE, + ' '..ma, + function(text) + local count = tonumber(text) + if self:checkUserInput(count, text) then + ma = count + if selidx == 1 then + plugin.autobutcher_setDefaultTargetAll( fk, mk, fa, ma ) + end + if selidx == 2 then + plugin.autobutcher_setDefaultTargetNew( fk, mk, fa, ma ) + end + if selidx > 2 then + plugin.autobutcher_setWatchListRace(id, fk, mk, fa, ma, watched) + end + self:initListChoices() + end + end + ) +end + +function WatchList:onEditSleepTimer() + local sleep = getSleepTimer() + dlg.showInputPrompt( + 'Edit Sleep Timer', + 'Enter new sleep timer in ticks:'..NEWLINE..'(1 ingame day equals 1200 ticks)', + COLOR_WHITE, + ' '..sleep, + function(text) + local count = tonumber(text) + if self:checkUserInputSleep(count, text) then + sleep = count + setSleepTimer(sleep) + self:updateBottom() + end + end + ) +end + +function WatchList:onToggleWatching() + local selidx,selobj = self.subviews.list:getSelected() + if selidx > 2 then + local entry = selobj.obj + plugin.autobutcher_setWatchListRace(entry.id, entry.fk, entry.mk, entry.fa, entry.ma, not entry.watched) + end + self:initListChoices() +end + +function WatchList:onDeleteEntry() + local selidx,selobj = self.subviews.list:getSelected() + if(selidx < 3 or selobj == nil) then + return + end + dlg.showYesNoPrompt( + 'Delete from Watchlist', + 'Really delete the selected entry?'..NEWLINE..'(you could just toggle watch instead)', + COLOR_YELLOW, + function() + plugin.autobutcher_removeFromWatchList(selobj.obj.id) + self:initListChoices() + end + ) +end + +function WatchList:onAddRace() + print('onAddRace - not implemented yet') +end + +function WatchList:onUnbutcherRace() + local selidx,selobj = self.subviews.list:getSelected() + if selidx < 3 then dlg.showMessage('Error', 'Select a specific race.', COLOR_LIGHTRED) end + if selidx > 2 then + local entry = selobj.obj + local race = entry.name + plugin.autobutcher_unbutcherRace(entry.id) + self:initListChoices() + self:updateBottom() + end +end + +function WatchList:onButcherRace() + local selidx,selobj = self.subviews.list:getSelected() + if selidx < 3 then dlg.showMessage('Error', 'Select a specific race.', COLOR_LIGHTRED) end + if selidx > 2 then + local entry = selobj.obj + local race = entry.name + plugin.autobutcher_butcherRace(entry.id) + self:initListChoices() + self:updateBottom() + end +end + +-- set whole row (fk, mk, fa, ma) to one value +function WatchList:onSetRow() + local selidx,selobj = self.subviews.list:getSelected() + local race = 'ALL RACES PLUS NEW' + local id = -1 + local watched = false + + if selidx == 2 then + race = 'ONLY NEW RACES' + end + + local watchindex = selidx - 3 + if selidx > 2 then + local entry = selobj.obj + race = entry.name + id = entry.id + watched = entry.watched + end + + dlg.showInputPrompt( + 'Set whole row for '..race, + 'Enter desired maximum for all subtypes:', + COLOR_WHITE, + ' ', + function(text) + local count = tonumber(text) + if self:checkUserInput(count, text) then + if selidx == 1 then + plugin.autobutcher_setDefaultTargetAll( count, count, count, count ) + end + if selidx == 2 then + plugin.autobutcher_setDefaultTargetNew( count, count, count, count ) + end + if selidx > 2 then + plugin.autobutcher_setWatchListRace(id, count, count, count, count, watched) + end + self:initListChoices() + end + end + ) +end + +function WatchList:onToggleAutobutcher() + if(plugin.autobutcher_isEnabled()) then + plugin.autobutcher_setEnabled(false) + plugin.autobutcher_sortWatchList() + else + plugin.autobutcher_setEnabled(true) + end + self:initListChoices() + self:updateBottom() +end + +function WatchList:onToggleAutowatch() + if(plugin.autowatch_isEnabled()) then + plugin.autowatch_setEnabled(false) + else + plugin.autowatch_setEnabled(true) + end + self:initListChoices() + self:updateBottom() +end + +if not dfhack.isMapLoaded() then + qerror('Map is not loaded.') +end + +if string.match(dfhack.gui.getCurFocus(), '^dfhack/lua') then + qerror("This script must not be called while other lua gui stuff is running.") +end + +-- maybe this is too strict, there is not really a reason why it can only be called from the status screen +-- (other than the hotkey might overlap with other scripts) +if (not string.match(dfhack.gui.getCurFocus(), '^overallstatus') and not string.match(dfhack.gui.getCurFocus(), '^pet/List/Unit')) then + qerror("This script must either be called from the overall status screen or the animal list screen.") +end + + +local screen = WatchList{ } +screen:show()