diff --git a/NEWS b/NEWS index 135cb2f42..fbc40ce43 100644 --- a/NEWS +++ b/NEWS @@ -27,6 +27,13 @@ DFHack future - digfort: improved csv parsing, add start() comment handling - exterminate: allow specifying a caste (exterminate gob:male) - createitem: in adventure mode it now defaults to the controlled unit as maker. + - autotrade: adds "(Un)mark All" options to both panes of trade screen. + - mousequery: several usability improvements. + - mousequery: show live overlay (in menu area) of what's on the tile under the mouse cursor. + - search: workshop profile search added. + - dwarfmonitor: add screen to summarise preferences of fortress dwarfs. + - getplants: add autochop function to automate woodcutting. + - stocks: added more filtering and display options. Siege engine plugin: - engine quality and distance to target now affect accuracy diff --git a/Readme.rst b/Readme.rst index 6987f1333..fc9478f5d 100644 --- a/Readme.rst +++ b/Readme.rst @@ -1938,6 +1938,29 @@ another savegame you can use the command list_export: autobutcher.bat +autochop +--------- +Automatically manage tree cutting designation to keep available logs withing given +quotas. + +Open the dashboard by running: +:: + + getplants autochop + +The plugin must be activated (with 'a') before it can be used. You can then set logging quotas +and restrict designations to specific burrows (with 'Enter') if desired. The plugin's activity +cycle runs once every in game day. + +If you add +:: + + enable getplants + +to your dfhack.init there will be a hotkey to open the dashboard from the chop designation +menu. + + autolabor --------- Automatically manage dwarf labors. diff --git a/plugins/automaterial.cpp b/plugins/automaterial.cpp index 825e69b01..b8021aaf2 100644 --- a/plugins/automaterial.cpp +++ b/plugins/automaterial.cpp @@ -735,7 +735,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest 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 (input->count(interface_key::SELECT) || input->count(interface_key::SELECT_ALL)) { if (get_last_moved_material().matches(material)) last_used_moved = false; //Keep selected material on top @@ -749,7 +749,7 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest 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)); + populate_box_materials(gen_material, ((input->count(interface_key::SELECT_ALL) && ui_build_selector->is_grouped) ? -1 : 1)); input->clear(); // Let the apply_box_selection routine allocate the construction input->insert(interface_key::LEAVESCREEN); @@ -1162,6 +1162,15 @@ struct jobutils_hook : public df::viewscreen_dwarfmodest case SELECT_SECOND: OutputString(COLOR_GREEN, x, y, "Choose second corner", true, left_margin); + + int32_t curr_x, curr_y, curr_z; + Gui::getCursorCoords(curr_x, curr_y, curr_z); + int dX = abs(box_first.x - curr_x) + 1; + int dY = abs(box_first.y - curr_y) + 1; + stringstream label; + label << "Selection: " << dX << "x" << dY; + OutputString(COLOR_WHITE, x, ++y, label.str(), true, left_margin); + int cx = box_first.x; int cy = box_first.y; OutputString(COLOR_BROWN, cx, cy, "X"); diff --git a/plugins/autotrade.cpp b/plugins/autotrade.cpp index 0d11af9f0..e31fccdbc 100644 --- a/plugins/autotrade.cpp +++ b/plugins/autotrade.cpp @@ -6,6 +6,7 @@ #include "df/world_raws.h" #include "df/building_def.h" #include "df/viewscreen_dwarfmodest.h" +#include "df/viewscreen_tradegoodsst.h" #include "df/building_stockpilest.h" #include "modules/Items.h" #include "df/building_tradedepotst.h" @@ -14,10 +15,8 @@ #include "df/job_item_ref.h" #include "modules/Job.h" #include "df/ui.h" -#include "df/caravan_state.h" #include "df/mandate.h" #include "modules/Maps.h" -#include "modules/World.h" using df::global::world; using df::global::cursor; @@ -25,127 +24,9 @@ using df::global::ui; using df::building_stockpilest; DFHACK_PLUGIN("autotrade"); -#define PLUGIN_VERSION 0.2 - - -/* - * Stockpile Access - */ - -static building_stockpilest *get_selected_stockpile() -{ - if (!Gui::dwarfmode_hotkey(Core::getTopViewscreen()) || - ui->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; - } -}; +#define PLUGIN_VERSION 0.4 +static const string PERSISTENCE_KEY = "autotrade/stockpiles"; /* * Depot Access @@ -316,15 +197,10 @@ static bool is_valid_item(df::item *item) return true; } -static void mark_all_in_stockpiles(vector &stockpiles, bool announce) +static void mark_all_in_stockpiles(vector &stockpiles) { 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]; @@ -389,8 +265,6 @@ static void mark_all_in_stockpiles(vector &stockpiles, bool annou 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) { @@ -419,10 +293,10 @@ public: void add(df::building_stockpilest *sp) { - auto pile = StockpileInfo(sp); + auto pile = PersistentStockpileInfo(sp, PERSISTENCE_KEY); if (pile.isValid()) { - monitored_stockpiles.push_back(StockpileInfo(sp)); + monitored_stockpiles.push_back(pile); monitored_stockpiles.back().save(); } } @@ -456,20 +330,20 @@ public: ++it; } - mark_all_in_stockpiles(monitored_stockpiles, false); + mark_all_in_stockpiles(monitored_stockpiles); } void reset() { monitored_stockpiles.clear(); std::vector items; - DFHack::World::GetPersistentData(&items, "autotrade/stockpiles"); + DFHack::World::GetPersistentData(&items, PERSISTENCE_KEY); for (auto i = items.begin(); i != items.end(); i++) { - auto pile = StockpileInfo(*i); + auto pile = PersistentStockpileInfo(*i, PERSISTENCE_KEY); if (pile.load()) - monitored_stockpiles.push_back(StockpileInfo(pile)); + monitored_stockpiles.push_back(pile); else pile.remove(); } @@ -477,7 +351,7 @@ public: private: - vector monitored_stockpiles; + vector monitored_stockpiles; }; static StockpileMonitor monitor; @@ -519,18 +393,7 @@ struct trade_hook : public df::viewscreen_dwarfmodest 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 (input->count(interface_key::CUSTOM_SHIFT_T)) { if (monitor.isMonitored(sp)) monitor.remove(sp); @@ -558,18 +421,81 @@ struct trade_hook : public df::viewscreen_dwarfmodest 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); + int y = 24; + OutputToggleString(x, y, "Auto trade", "Shift-T", monitor.isMonitored(sp), true, left_margin); } }; IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, feed); IMPLEMENT_VMETHOD_INTERPOSE(trade_hook, render); +struct tradeview_hook : public df::viewscreen_tradegoodsst +{ + typedef df::viewscreen_tradegoodsst interpose_base; + + bool handleInput(set *input) + { + if (input->count(interface_key::CUSTOM_M)) + { + for (int i = 0; i < trader_selected.size(); i++) + { + trader_selected[i] = 1; + } + } + else if (input->count(interface_key::CUSTOM_U)) + { + for (int i = 0; i < trader_selected.size(); i++) + { + trader_selected[i] = 0; + } + } + else if (input->count(interface_key::CUSTOM_SHIFT_M)) + { + for (int i = 0; i < broker_selected.size(); i++) + { + broker_selected[i] = 1; + } + } + else if (input->count(interface_key::CUSTOM_SHIFT_U)) + { + for (int i = 0; i < broker_selected.size(); i++) + { + broker_selected[i] = 0; + } + } + else + { + return false; + } + + return true; + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!handleInput(input)) + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + int x = 2; + int y = 27; + OutputHotkeyString(x, y, "Mark all", "m", true, 2); + OutputHotkeyString(x, y, "Unmark all", "u"); + + x = 42; + y = 27; + OutputHotkeyString(x, y, "Mark all", "Shift-m", true, 42); + OutputHotkeyString(x, y, "Unmark all", "Shift-u"); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(tradeview_hook, feed); +IMPLEMENT_VMETHOD_INTERPOSE(tradeview_hook, render); + + static command_result autotrade_cmd(color_ostream &out, vector & parameters) { if (!parameters.empty()) @@ -612,7 +538,9 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) monitor.reset(); if (!INTERPOSE_HOOK(trade_hook, feed).apply(enable) || - !INTERPOSE_HOOK(trade_hook, render).apply(enable)) + !INTERPOSE_HOOK(trade_hook, render).apply(enable) || + !INTERPOSE_HOOK(tradeview_hook, feed).apply(enable) || + !INTERPOSE_HOOK(tradeview_hook, render).apply(enable)) return CR_FAILURE; is_enabled = enable; diff --git a/plugins/buildingplan.cpp b/plugins/buildingplan.cpp index 6bc1c5525..8b228b49e 100644 --- a/plugins/buildingplan.cpp +++ b/plugins/buildingplan.cpp @@ -38,7 +38,7 @@ using df::global::ui_build_selector; using df::global::world; DFHACK_PLUGIN("buildingplan"); -#define PLUGIN_VERSION 0.9 +#define PLUGIN_VERSION 0.12 struct MaterialDescriptor { @@ -58,16 +58,6 @@ struct MaterialDescriptor } }; -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; @@ -78,6 +68,7 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out ) #define SIDEBAR_WIDTH 30 static bool show_debugging = false; +static bool show_help = false; static void debug(const string &msg) { @@ -943,7 +934,7 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest if (isInPlannedBuildingPlacementMode()) { auto type = ui_build_selector->building_type; - if (input->count(interface_key::CUSTOM_P)) + if (input->count(interface_key::CUSTOM_SHIFT_P)) { planmode_enabled[type] = !planmode_enabled[type]; if (!planmode_enabled[type]) @@ -954,6 +945,14 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest } return true; } + else if (input->count(interface_key::CUSTOM_P) || + input->count(interface_key::CUSTOM_F) || + input->count(interface_key::CUSTOM_Q) || + input->count(interface_key::CUSTOM_D) || + input->count(interface_key::CUSTOM_N)) + { + show_help = true; + } if (is_planmode_enabled(type)) { @@ -983,7 +982,7 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest return true; } - else if (input->count(interface_key::CUSTOM_F)) + else if (input->count(interface_key::CUSTOM_SHIFT_F)) { if (!planner.inQuickFortMode()) { @@ -994,15 +993,15 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest planner.disableQuickfortMode(); } } - else if (input->count(interface_key::CUSTOM_M)) + else if (input->count(interface_key::CUSTOM_SHIFT_M)) { Screen::show(new ViewscreenChooseMaterial(planner.getDefaultItemFilterForType(type))); } - else if (input->count(interface_key::CUSTOM_Q)) + else if (input->count(interface_key::CUSTOM_SHIFT_Q)) { planner.cycleDefaultQuality(type); } - else if (input->count(interface_key::CUSTOM_D)) + else if (input->count(interface_key::CUSTOM_SHIFT_D)) { planner.getDefaultItemFilterForType(type)->decorated_only = !planner.getDefaultItemFilterForType(type)->decorated_only; @@ -1080,22 +1079,25 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest { int y = 23; - OutputToggleString(x, y, "Planning Mode", "p", is_planmode_enabled(type), true, left_margin); + if (show_help) + { + OutputString(COLOR_BROWN, x, y, "Note: "); + OutputString(COLOR_WHITE, x, y, "Use Shift-Keys here", true, left_margin); + } + OutputToggleString(x, y, "Planning Mode", "P", is_planmode_enabled(type), true, left_margin); if (is_planmode_enabled(type)) { - OutputToggleString(x, y, "Quickfort Mode", "f", planner.inQuickFortMode(), true, left_margin); + OutputToggleString(x, y, "Quickfort Mode", "F", planner.inQuickFortMode(), true, left_margin); auto filter = planner.getDefaultItemFilterForType(type); - OutputHotkeyString(x, y, "Min Quality: ", "q"); + 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); + OutputToggleString(x, y, "Decorated Only: ", "D", filter->decorated_only, true, left_margin); - OutputHotkeyString(x, y, "Material Filter:", "m", true, left_margin); + OutputHotkeyString(x, y, "Material Filter:", "M", true, left_margin); auto filter_descriptions = filter->getMaterialFilterAsVector(); for (auto it = filter_descriptions.begin(); it != filter_descriptions.end(); ++it) OutputString(COLOR_BROWN, x, y, " *" + *it, true, left_margin); @@ -1131,6 +1133,7 @@ struct buildingplan_hook : public df::viewscreen_dwarfmodest else { planner.in_dummmy_screen = false; + show_help = false; } } }; diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index 2776227c5..be271750a 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -17,6 +17,29 @@ #include "modules/Maps.h" #include "df/activity_event.h" #include "df/activity_entry.h" +#include "df/unit_preference.h" +#include "df/unit_soul.h" +#include "df/item_type.h" + +#include "df/itemdef_weaponst.h" +#include "df/itemdef_trapcompst.h" +#include "df/itemdef_toyst.h" +#include "df/itemdef_toolst.h" +#include "df/itemdef_instrumentst.h" +#include "df/itemdef_armorst.h" +#include "df/itemdef_ammost.h" +#include "df/itemdef_siegeammost.h" +#include "df/itemdef_glovesst.h" +#include "df/itemdef_shoesst.h" +#include "df/itemdef_shieldst.h" +#include "df/itemdef_helmst.h" +#include "df/itemdef_pantsst.h" +#include "df/itemdef_foodst.h" +#include "df/trapcomp_flags.h" +#include "df/creature_raw.h" +#include "df/world_raws.h" +#include "df/descriptor_shape.h" +#include "df/descriptor_color.h" using std::deque; @@ -25,7 +48,7 @@ using df::global::ui; typedef int16_t activity_type; -#define PLUGIN_VERSION 0.5 +#define PLUGIN_VERSION 0.8 #define DAY_TICKS 1200 #define DELTA_TICKS 100 @@ -48,6 +71,36 @@ static map> work_history; static int misery[] = { 0, 0, 0, 0, 0, 0, 0 }; static bool misery_upto_date = false; +static color_value monitor_colors[] = +{ + COLOR_LIGHTRED, + COLOR_RED, + COLOR_YELLOW, + COLOR_WHITE, + COLOR_CYAN, + COLOR_LIGHTBLUE, + COLOR_LIGHTGREEN +}; + +static int get_happiness_cat(df::unit *unit) +{ + int happy = unit->status.happiness; + if (happy == 0) // miserable + return 0; + else if (happy <= 25) // very unhappy + return 1; + else if (happy <= 50) // unhappy + return 2; + else if (happy <= 75) // fine + return 3; + else if (happy <= 125) // quite content + return 4; + else if (happy <= 150) // happy + return 5; + else // ecstatic + return 6; +} + static int get_max_history() { return ticks_per_day * max_history_days; @@ -129,6 +182,7 @@ static string getActivityLabel(const activity_type activity) return label; } + class ViewscreenDwarfStats : public dfhack_viewscreen { public: @@ -981,6 +1035,520 @@ private: } }; + +struct preference_map +{ + df::unit_preference pref; + vector dwarves; + string label; + + string getItemLabel() + { + df::world_raws::T_itemdefs &defs = df::global::world->raws.itemdefs; + label = ENUM_ATTR_STR(item_type, caption, pref.item_type); + switch (pref.item_type) + { + case (df::item_type::WEAPON): + label = defs.weapons[pref.item_subtype]->name_plural; + break; + case (df::item_type::TRAPCOMP): + label = defs.trapcomps[pref.item_subtype]->name_plural; + break; + case (df::item_type::TOY): + label = defs.toys[pref.item_subtype]->name_plural; + break; + case (df::item_type::TOOL): + label = defs.tools[pref.item_subtype]->name_plural; + break; + case (df::item_type::INSTRUMENT): + label = defs.instruments[pref.item_subtype]->name_plural; + break; + case (df::item_type::ARMOR): + label = defs.armor[pref.item_subtype]->name_plural; + break; + case (df::item_type::AMMO): + label = defs.ammo[pref.item_subtype]->name_plural; + break; + case (df::item_type::SIEGEAMMO): + label = defs.siege_ammo[pref.item_subtype]->name_plural; + break; + case (df::item_type::GLOVES): + label = defs.gloves[pref.item_subtype]->name_plural; + break; + case (df::item_type::SHOES): + label = defs.shoes[pref.item_subtype]->name_plural; + break; + case (df::item_type::SHIELD): + label = defs.shields[pref.item_subtype]->name_plural; + break; + case (df::item_type::HELM): + label = defs.helms[pref.item_subtype]->name_plural; + break; + case (df::item_type::PANTS): + label = defs.pants[pref.item_subtype]->name_plural; + break; + case (df::item_type::FOOD): + label = defs.food[pref.item_subtype]->name; + break; + + default: + break; + } + + return label; + } + + void makeLabel() + { + label = ""; + + typedef df::unit_preference::T_type T_type; + df::world_raws &raws = world->raws; + switch (pref.type) + { + case (T_type::LikeCreature): + { + label = "Creature :"; + auto creature = df::creature_raw::find(pref.creature_id); + if (creature) + label += creature->name[1]; + break; + } + + case (T_type::HateCreature): + { + label = "Hates :"; + auto creature = df::creature_raw::find(pref.creature_id); + if (creature) + label += creature->name[1]; + break; + } + + case (T_type::LikeItem): + label = "Item :" + getItemLabel(); + break; + + case (T_type::LikeFood): + { + label = "Food :"; + if (pref.matindex < 0 || pref.item_type == item_type::MEAT) + { + auto index = (pref.item_type == item_type::FISH) ? pref.mattype : pref.matindex; + if (index > 0) + { + auto creature = df::creature_raw::find(index); + if (creature) + label += creature->name[0]; + } + else + { + label += "Invalid"; + } + + break; + } + } + + case (T_type::LikeMaterial): + { + if (label.length() == 0) + label += "Material :"; + MaterialInfo matinfo(pref.mattype, pref.matindex); + if (pref.type == T_type::LikeFood && pref.item_type == item_type::PLANT) + { + label += matinfo.material->prefix; + } + else + label += matinfo.toString(); + + break; + } + + case (T_type::LikePlant): + { + df::plant_raw *p = raws.plants.all[pref.plant_id]; + label += "Plant :" + p->name_plural; + break; + } + + case (T_type::LikeShape): + label += "Shape :" + raws.language.shapes[pref.shape_id]->name_plural; + break; + + case (T_type::LikeTree): + { + df::plant_raw *p = raws.plants.all[pref.plant_id]; + label += "Tree :" + p->name_plural; + break; + } + + case (T_type::LikeColor): + label += "Color :" + raws.language.colors[pref.color_id]->name; + break; + } + } +}; + + +class ViewscreenPreferences : public dfhack_viewscreen +{ +public: + ViewscreenPreferences() + { + preferences_column.multiselect = false; + preferences_column.auto_select = true; + preferences_column.setTitle("Preference"); + preferences_column.bottom_margin = 3; + preferences_column.search_margin = 35; + + dwarf_column.multiselect = false; + dwarf_column.auto_select = true; + dwarf_column.allow_null = true; + dwarf_column.setTitle("Units with Preference"); + dwarf_column.bottom_margin = 3; + dwarf_column.search_margin = 35; + + populatePreferencesColumn(); + } + + void populatePreferencesColumn() + { + selected_column = 0; + + auto last_selected_index = preferences_column.highlighted_index; + preferences_column.clear(); + preference_totals.clear(); + + 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)) + continue; + + if (!unit->status.current_soul) + continue; + + for (auto it = unit->status.current_soul->preferences.begin(); + it != unit->status.current_soul->preferences.end(); + it++) + { + auto pref = *it; + if (!pref->active) + continue; + bool foundInStore = false; + for (size_t pref_index = 0; pref_index < preferences_store.size(); pref_index++) + { + if (isMatchingPreference(preferences_store[pref_index].pref, *pref)) + { + foundInStore = true; + preferences_store[pref_index].dwarves.push_back(unit); + } + } + + if (!foundInStore) + { + size_t pref_index = preferences_store.size(); + preferences_store.resize(pref_index + 1); + preferences_store[pref_index].pref = *pref; + preferences_store[pref_index].dwarves.push_back(unit); + } + } + } + + for (size_t i = 0; i < preferences_store.size(); i++) + { + preference_totals[i] = preferences_store[i].dwarves.size(); + } + + vector> rev_vec(preference_totals.begin(), preference_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 pref_index = rev_it->first; + preferences_store[pref_index].makeLabel(); + + string label = pad_string(int_to_string(rev_it->second), 3); + label += " "; + label += preferences_store[pref_index].label; + ListEntry elem(label, pref_index, "", getItemColor(preferences_store[pref_index].pref.type)); + preferences_column.add(elem); + } + + dwarf_column.left_margin = preferences_column.fixWidth() + 2; + preferences_column.filterDisplay(); + preferences_column.setHighlight(last_selected_index); + populateDwarfColumn(); + } + + bool isMatchingPreference(df::unit_preference &lhs, df::unit_preference &rhs) + { + if (lhs.type != rhs.type) + return false; + + typedef df::unit_preference::T_type T_type; + switch (lhs.type) + { + case (T_type::LikeCreature): + if (lhs.creature_id != rhs.creature_id) + return false; + break; + + case (T_type::HateCreature): + if (lhs.creature_id != rhs.creature_id) + return false; + break; + + case (T_type::LikeFood): + if (lhs.item_type != rhs.item_type) + return false; + if (lhs.mattype != rhs.mattype || lhs.matindex != rhs.matindex) + return false; + break; + + case (T_type::LikeItem): + if (lhs.item_type != rhs.item_type || lhs.item_subtype != rhs.item_subtype) + return false; + break; + + case (T_type::LikeMaterial): + if (lhs.mattype != rhs.mattype || lhs.matindex != rhs.matindex) + return false; + break; + + case (T_type::LikePlant): + if (lhs.plant_id != rhs.plant_id) + return false; + break; + + case (T_type::LikeShape): + if (lhs.shape_id != rhs.shape_id) + return false; + break; + + case (T_type::LikeTree): + if (lhs.item_type != rhs.item_type) + return false; + break; + + case (T_type::LikeColor): + if (lhs.color_id != rhs.color_id) + return false; + break; + + default: + return false; + } + + return true; + } + + UIColor getItemColor(const df::unit_preference::T_type &type) const + { + typedef df::unit_preference::T_type T_type; + switch (type) + { + case (T_type::LikeCreature): + return COLOR_WHITE; + + case (T_type::HateCreature): + return COLOR_LIGHTRED; + + case (T_type::LikeFood): + return COLOR_GREEN; + + case (T_type::LikeItem): + return COLOR_YELLOW; + + case (T_type::LikeMaterial): + return COLOR_CYAN; + + case (T_type::LikePlant): + return COLOR_BROWN; + + case (T_type::LikeShape): + return COLOR_BLUE; + + case (T_type::LikeTree): + return COLOR_BROWN; + + case (T_type::LikeColor): + return COLOR_BLUE; + + default: + return false; + } + + return true; + } + + void populateDwarfColumn() + { + dwarf_column.clear(); + if (preferences_column.getDisplayListSize() > 0) + { + auto selected_preference = preferences_column.getFirstSelectedElem(); + for (auto dfit = preferences_store[selected_preference].dwarves.begin(); + dfit != preferences_store[selected_preference].dwarves.end(); + dfit++) + { + string label = getUnitName(*dfit); + auto happy = get_happiness_cat(*dfit); + UIColor color = monitor_colors[happy]; + switch (happy) + { + case 0: + label += " (miserable)"; + break; + + case 1: + label += " (very unhappy)"; + break; + + case 2: + label += " (unhappy)"; + break; + + case 3: + label += " (fine)"; + break; + + case 4: + label += " (quite content)"; + break; + + case 5: + label += " (happy)"; + break; + + case 6: + label += " (ecstatic)"; + break; + } + + ListEntry elem(label, *dfit, "", color); + dwarf_column.add(elem); + } + } + + dwarf_column.clearSearch(); + dwarf_column.setHighlight(0); + } + + void feed(set *input) + { + bool key_processed = false; + switch (selected_column) + { + case 0: + key_processed = preferences_column.feed(input); + break; + case 1: + key_processed = dwarf_column.feed(input); + break; + } + + if (key_processed) + { + if (selected_column == 0 && preferences_column.feed_changed_highlight) + { + populateDwarfColumn(); + } + + return; + } + + if (input->count(interface_key::LEAVESCREEN)) + { + input->clear(); + Screen::dismiss(this); + return; + } + else if (input->count(interface_key::CUSTOM_SHIFT_Z)) + { + df::unit *selected_unit = (selected_column == 1) ? dwarf_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 (preferences_column.setHighlightByMouse()) + { + selected_column = 0; + populateDwarfColumn(); + } + else if (dwarf_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 Preferences "); + + preferences_column.display(selected_column == 0); + dwarf_column.display(selected_column == 1); + + int32_t y = gps->dimy - 3; + int32_t x = 2; + OutputHotkeyString(x, y, "Leave", "Esc"); + + x += 2; + OutputHotkeyString(x, y, "Zoom Unit", "Shift-Z"); + } + + std::string getFocusString() { return "dwarfmonitor_preferences"; } + +private: + ListColumn preferences_column; + ListColumn dwarf_column; + int selected_column; + + map preference_totals; + + vector preferences_store; + + void validateColumn() + { + set_to_limit(selected_column, 1); + } + + void resize(int32_t x, int32_t y) + { + dfhack_viewscreen::resize(x, y); + preferences_column.resize(); + dwarf_column.resize(); + } +}; + + static void open_stats_srceen() { Screen::show(new ViewscreenFortStats()); @@ -1045,21 +1613,7 @@ static void update_dwarf_stats(bool is_paused) 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]++; + misery[get_happiness_cat(unit)]++; } if (!monitor_jobs || is_paused) @@ -1094,6 +1648,7 @@ static void update_dwarf_stats(bool is_paused) } } + DFhackCExport command_result plugin_onupdate (color_ostream &out) { if (!monitor_jobs && !monitor_misery) @@ -1125,17 +1680,6 @@ DFhackCExport command_result plugin_onupdate (color_ostream &out) 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; @@ -1265,6 +1809,11 @@ static command_result dwarfmonitor_cmd(color_ostream &out, vector & par if(Maps::IsValid()) Screen::show(new ViewscreenFortStats()); } + else if (cmd == 'p' || cmd == 'P') + { + if(Maps::IsValid()) + Screen::show(new ViewscreenPreferences()); + } else { show_help = true; @@ -1307,7 +1856,9 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector \n" " as above\n\n" "dwarfmonitor stats\n" - " Show statistics summary\n\n" + " Show statistics summary\n" + "dwarfmonitor prefs\n" + " Show dwarf preferences summary\n\n" )); return CR_OK; diff --git a/plugins/getplants.cpp b/plugins/getplants.cpp index eaa8077f2..5a5337f45 100644 --- a/plugins/getplants.cpp +++ b/plugins/getplants.cpp @@ -1,17 +1,32 @@ // (un)designate matching plants for gathering/cutting +#include "uicommon.h" + #include "Core.h" #include "Console.h" #include "Export.h" #include "PluginManager.h" - #include "DataDefs.h" #include "TileTypes.h" + #include "df/world.h" #include "df/map_block.h" #include "df/tile_dig_designation.h" #include "df/plant_raw.h" #include "df/plant.h" +#include "df/ui.h" +#include "df/burrow.h" +#include "df/item_flags.h" +#include "df/item.h" +#include "df/items_other_id.h" +#include "df/viewscreen_dwarfmodest.h" + +#include "modules/Screen.h" +#include "modules/Maps.h" +#include "modules/Burrows.h" +#include "modules/World.h" +#include "modules/MapCache.h" +#include "modules/Gui.h" #include @@ -22,6 +37,601 @@ using namespace DFHack; using namespace df::enums; using df::global::world; +using df::global::ui; + +#define PLUGIN_VERSION 0.3 +DFHACK_PLUGIN("getplants"); + + +static bool autochop_enabled = false; +static int min_logs, max_logs; +static bool wait_for_threshold; + +static PersistentDataItem config_autochop; + +struct WatchedBurrow +{ + int32_t id; + df::burrow *burrow; + + WatchedBurrow(df::burrow *burrow) : burrow(burrow) + { + id = burrow->id; + } +}; + +class WatchedBurrows +{ +public: + string getSerialisedIds() + { + validate(); + stringstream burrow_ids; + bool append_started = false; + for (auto it = burrows.begin(); it != burrows.end(); it++) + { + if (append_started) + burrow_ids << " "; + burrow_ids << it->id; + append_started = true; + } + + return burrow_ids.str(); + } + + void clear() + { + burrows.clear(); + } + + void add(const int32_t id) + { + if (!isValidBurrow(id)) + return; + + WatchedBurrow wb(getBurrow(id)); + burrows.push_back(wb); + } + + void add(const string burrow_ids) + { + istringstream iss(burrow_ids); + int id; + while (iss >> id) + { + add(id); + } + } + + bool isValidPos(const df::coord &plant_pos) + { + validate(); + if (!burrows.size()) + return true; + + for (auto it = burrows.begin(); it != burrows.end(); it++) + { + df::burrow *burrow = it->burrow; + if (Burrows::isAssignedTile(burrow, plant_pos)) + return true; + } + + return false; + } + + bool isBurrowWatched(const df::burrow *burrow) + { + validate(); + for (auto it = burrows.begin(); it != burrows.end(); it++) + { + if (it->burrow == burrow) + return true; + } + + return false; + } + +private: + static bool isValidBurrow(const int32_t id) + { + return getBurrow(id); + } + + static df::burrow *getBurrow(const int32_t id) + { + return df::burrow::find(id); + } + + void validate() + { + for (auto it = burrows.begin(); it != burrows.end();) + { + if (!isValidBurrow(it->id)) + it = burrows.erase(it); + else + ++it; + } + } + + vector burrows; +}; + +static WatchedBurrows watchedBurrows; + +static void save_config() +{ + config_autochop.val() = watchedBurrows.getSerialisedIds(); + config_autochop.ival(0) = autochop_enabled; + config_autochop.ival(1) = min_logs; + config_autochop.ival(2) = max_logs; + config_autochop.ival(3) = wait_for_threshold; +} + +static void initialize() +{ + watchedBurrows.clear(); + autochop_enabled = false; + min_logs = 80; + max_logs = 100; + wait_for_threshold = false; + + config_autochop = World::GetPersistentData("autochop/config"); + if (config_autochop.isValid()) + { + watchedBurrows.add(config_autochop.val()); + autochop_enabled = config_autochop.ival(0); + min_logs = config_autochop.ival(1); + max_logs = config_autochop.ival(2); + wait_for_threshold = config_autochop.ival(3); + } + else + { + config_autochop = World::AddPersistentData("autochop/config"); + if (config_autochop.isValid()) + save_config(); + } +} + +static int do_chop_designation(bool chop, bool count_only) +{ + int count = 0; + for (size_t i = 0; i < world->map.map_blocks.size(); i++) + { + df::map_block *cur = world->map.map_blocks[i]; + for (size_t j = 0; j < cur->plants.size(); j++) + { + const df::plant *plant = cur->plants[j]; + int x = plant->pos.x % 16; + int y = plant->pos.y % 16; + + if (plant->flags.bits.is_shrub) + continue; + if (cur->designation[x][y].bits.hidden) + continue; + + df::tiletype_shape shape = tileShape(cur->tiletype[x][y]); + if (shape != tiletype_shape::TREE) + continue; + + if (!count_only && !watchedBurrows.isValidPos(plant->pos)) + continue; + + bool dirty = false; + if (chop && cur->designation[x][y].bits.dig == tile_dig_designation::No) + { + if (count_only) + { + ++count; + } + else + { + cur->designation[x][y].bits.dig = tile_dig_designation::Default; + dirty = true; + } + } + + if (!chop && cur->designation[x][y].bits.dig == tile_dig_designation::Default) + { + if (count_only) + { + ++count; + } + else + { + cur->designation[x][y].bits.dig = tile_dig_designation::No; + dirty = true; + } + } + + if (dirty) + { + cur->flags.bits.designated = true; + ++count; + } + } + } + + return count; +} + +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; + } + } + + return true; +} + +static int get_log_count() +{ + std::vector &items = world->items.other[items_other_id::IN_PLAY]; + + // Pre-compute 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 valid_count = 0; + for (size_t i = 0; i < items.size(); i++) + { + df::item *item = items[i]; + + if (item->getType() != item_type::WOOD) + continue; + + if (item->flags.whole & bad_flags.whole) + continue; + + if (!is_valid_item(item)) + continue; + + ++valid_count; + } + + return valid_count; +} + +static void set_threshold_check(bool state) +{ + wait_for_threshold = state; + save_config(); +} + +static void do_autochop() +{ + int log_count = get_log_count(); + if (wait_for_threshold) + { + if (log_count < min_logs) + { + set_threshold_check(false); + do_chop_designation(true, false); + } + } + else + { + if (log_count >= max_logs) + { + set_threshold_check(true); + do_chop_designation(false, false); + } + else + { + do_chop_designation(true, false); + } + } +} + +class ViewscreenAutochop : public dfhack_viewscreen +{ +public: + ViewscreenAutochop() + { + burrows_column.multiselect = true; + burrows_column.setTitle("Burrows"); + burrows_column.bottom_margin = 3; + burrows_column.allow_search = false; + burrows_column.text_clip_at = 30; + + populateBurrowsColumn(); + message.clear(); + } + + void populateBurrowsColumn() + { + selected_column = 0; + + auto last_selected_index = burrows_column.highlighted_index; + burrows_column.clear(); + + for (auto iter = ui->burrows.list.begin(); iter != ui->burrows.list.end(); iter++) + { + df::burrow* burrow = *iter; + auto elem = ListEntry(burrow->name, burrow); + elem.selected = watchedBurrows.isBurrowWatched(burrow); + burrows_column.add(elem); + } + + burrows_column.fixWidth(); + burrows_column.filterDisplay(); + + current_log_count = get_log_count(); + marked_tree_count = do_chop_designation(false, true); + } + + void change_min_logs(int delta) + { + if (!autochop_enabled) + return; + + min_logs += delta; + if (min_logs < 0) + min_logs = 0; + if (min_logs > max_logs) + max_logs = min_logs; + } + + void change_max_logs(int delta) + { + if (!autochop_enabled) + return; + + max_logs += delta; + if (max_logs < min_logs) + min_logs = max_logs; + } + + void feed(set *input) + { + bool key_processed = false; + message.clear(); + switch (selected_column) + { + case 0: + key_processed = burrows_column.feed(input); + break; + } + + if (key_processed) + { + if (input->count(interface_key::SELECT)) + updateAutochopBurrows(); + return; + } + + if (input->count(interface_key::LEAVESCREEN)) + { + save_config(); + input->clear(); + Screen::dismiss(this); + if (autochop_enabled) + do_autochop(); + return; + } + else if (input->count(interface_key::CUSTOM_A)) + { + autochop_enabled = !autochop_enabled; + } + else if (input->count(interface_key::CUSTOM_D)) + { + int count = do_chop_designation(true, false); + message = "Trees marked for chop: " + int_to_string(count); + marked_tree_count = do_chop_designation(false, true); + } + else if (input->count(interface_key::CUSTOM_U)) + { + int count = do_chop_designation(false, false); + message = "Trees unmarked: " + int_to_string(count); + marked_tree_count = do_chop_designation(false, true); + } + else if (input->count(interface_key::CUSTOM_H)) + { + change_min_logs(-1); + } + else if (input->count(interface_key::CUSTOM_SHIFT_H)) + { + change_min_logs(-10); + } + else if (input->count(interface_key::CUSTOM_J)) + { + change_min_logs(1); + } + else if (input->count(interface_key::CUSTOM_SHIFT_J)) + { + change_min_logs(10); + } + else if (input->count(interface_key::CUSTOM_K)) + { + change_max_logs(-1); + } + else if (input->count(interface_key::CUSTOM_SHIFT_K)) + { + change_max_logs(-10); + } + else if (input->count(interface_key::CUSTOM_L)) + { + change_max_logs(1); + } + else if (input->count(interface_key::CUSTOM_SHIFT_L)) + { + change_max_logs(10); + } + else if (enabler->tracking_on && enabler->mouse_lbut) + { + if (burrows_column.setHighlightByMouse()) + { + selected_column = 0; + } + + enabler->mouse_lbut = enabler->mouse_rbut = 0; + } + } + + void render() + { + if (Screen::isDismissed(this)) + return; + + dfhack_viewscreen::render(); + + Screen::clear(); + Screen::drawBorder(" Autochop "); + + burrows_column.display(selected_column == 0); + + int32_t y = gps->dimy - 3; + int32_t x = 2; + OutputHotkeyString(x, y, "Leave", "Esc"); + x += 3; + OutputString(COLOR_YELLOW, x, y, message); + + y = 3; + int32_t left_margin = burrows_column.getMaxItemWidth() + 3; + x = left_margin; + if (burrows_column.getSelectedElems().size() > 0) + { + OutputString(COLOR_GREEN, x, y, "Will chop in selected burrows", true, left_margin); + } + else + { + OutputString(COLOR_YELLOW, x, y, "Will chop from whole map", true, left_margin); + OutputString(COLOR_YELLOW, x, y, "Select from left to chop in specific burrows", true, left_margin); + } + + ++y; + OutputToggleString(x, y, "Autochop", "a", autochop_enabled, true, left_margin); + OutputHotkeyString(x, y, "Designate Now", "d", true, left_margin); + OutputHotkeyString(x, y, "Undesignate Now", "u", true, left_margin); + OutputHotkeyString(x, y, "Toggle Burrow", "Enter", true, left_margin); + if (autochop_enabled) + { + OutputLabelString(x, y, "Min Logs", "hjHJ", int_to_string(min_logs), true, left_margin); + OutputLabelString(x, y, "Max Logs", "klKL", int_to_string(max_logs), true, left_margin); + } + + ++y; + OutputString(COLOR_BROWN, x, y, "Current Counts", true, left_margin); + OutputString(COLOR_WHITE, x, y, "Current Logs: "); + OutputString(COLOR_GREEN, x, y, int_to_string(current_log_count), true, left_margin); + OutputString(COLOR_WHITE, x, y, "Marked Trees: "); + OutputString(COLOR_GREEN, x, y, int_to_string(marked_tree_count), true, left_margin); + } + + std::string getFocusString() { return "autochop"; } + + void updateAutochopBurrows() + { + watchedBurrows.clear(); + vector v = burrows_column.getSelectedElems(); + for_each_(v, [] (df::burrow *b) { watchedBurrows.add(b->id); }); + } + +private: + ListColumn burrows_column; + int selected_column; + int current_log_count; + int marked_tree_count; + MapExtras::MapCache mcache; + string message; + + void validateColumn() + { + set_to_limit(selected_column, 0); + } + + void resize(int32_t x, int32_t y) + { + dfhack_viewscreen::resize(x, y); + burrows_column.resize(); + } +}; + +struct autochop_hook : public df::viewscreen_dwarfmodest +{ + typedef df::viewscreen_dwarfmodest interpose_base; + + bool isInDesignationMenu() + { + using namespace df::enums::ui_sidebar_mode; + return (ui->main.mode == DesignateChopTrees); + } + + void sendKey(const df::interface_key &key) + { + set tmp; + tmp.insert(key); + INTERPOSE_NEXT(feed)(&tmp); + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (isInDesignationMenu() && input->count(interface_key::CUSTOM_C)) + { + sendKey(interface_key::LEAVESCREEN); + Screen::show(new ViewscreenAutochop()); + } + else + { + INTERPOSE_NEXT(feed)(input); + } + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + auto dims = Gui::getDwarfmodeViewDims(); + if (dims.menu_x1 <= 0) + return; + + df::ui_sidebar_mode d = ui->main.mode; + if (!isInDesignationMenu()) + return; + + int left_margin = dims.menu_x1 + 1; + int x = left_margin; + int y = 26; + OutputHotkeyString(x, y, "Autochop Dashboard", "c"); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE_PRIO(autochop_hook, feed, 100); +IMPLEMENT_VMETHOD_INTERPOSE_PRIO(autochop_hook, render, 100); + command_result df_getplants (color_ostream &out, vector & parameters) { @@ -45,6 +655,17 @@ command_result df_getplants (color_ostream &out, vector & parameters) exclude = true; else if(parameters[i] == "-a") all = true; + else if(parameters[i] == "debug") + { + save_config(); + } + else if(parameters[i] == "autochop") + { + if(Maps::IsValid()) + Screen::show(new ViewscreenAutochop()); + + return CR_OK; + } else plantNames.insert(parameters[i]); } @@ -148,7 +769,48 @@ command_result df_getplants (color_ostream &out, vector & parameters) return CR_OK; } -DFHACK_PLUGIN("getplants"); +DFhackCExport command_result plugin_onupdate (color_ostream &out) +{ + if (!autochop_enabled) + return CR_OK; + + 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 < 1200) // Check every day + return CR_OK; + + last_frame_count = world->frame_counter; + + do_autochop(); + + return CR_OK; +} + +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + if (!gps) + return CR_FAILURE; + + if (enable != is_enabled) + { + if (!INTERPOSE_HOOK(autochop_hook, feed).apply(enable) || + !INTERPOSE_HOOK(autochop_hook, render).apply(enable)) + return CR_FAILURE; + + is_enabled = enable; + initialize(); + } + + return CR_OK; +} DFhackCExport command_result plugin_init ( color_ostream &out, vector &commands) { @@ -164,8 +826,11 @@ DFhackCExport command_result plugin_init ( color_ostream &out, vector - -#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 "df/items_other_id.h" +#include "df/ui_build_selector.h" +#include "df/ui_sidebar_menus.h" #include "modules/Gui.h" -#include "modules/Screen.h" - - -using std::set; -using std::string; -using std::ostringstream; +#include "modules/World.h" +#include "modules/Maps.h" +#include "modules/Buildings.h" +#include "modules/Items.h" +#include "modules/Units.h" +#include "modules/Translation.h" -using namespace DFHack; -using namespace df::enums; +#include "uicommon.h" +#include "TileTypes.h" +#include "DataFuncs.h" -using df::global::enabler; -using df::global::gps; using df::global::world; using df::global::ui; +using namespace df::enums::ui_sidebar_mode; + +DFHACK_PLUGIN("mousequery"); + +#define PLUGIN_VERSION 0.17 + +static int32_t last_clicked_x, last_clicked_y, last_clicked_z; +static int32_t last_pos_x, last_pos_y, last_pos_z; +static df::coord last_move_pos; +static size_t max_list_size = 300000; // Avoid iterating over huge lists -static int32_t last_x, last_y, last_z; -static size_t max_list_size = 100000; // Avoid iterating over huge lists +static bool plugin_enabled = true; +static bool rbutton_enabled = true; +static bool tracking_enabled = false; +static bool active_scrolling = false; +static bool box_designation_enabled = false; +static bool live_view = true; +static bool skip_tracking_once = false; +static bool mouse_moved = false; + +static int scroll_delay = 100; + +static df::coord get_mouse_pos(int32_t &mx, int32_t &my) +{ + df::coord pos; + pos.x = -30000; + + if (!enabler->tracking_on) + return pos; + + if (!Gui::getMousePos(mx, my)) + return pos; + + int32_t vx, vy, vz; + if (!Gui::getViewCoords(vx, vy, vz)) + return pos; + + pos.x = vx + mx - 1; + pos.y = vy + my - 1; + pos.z = vz; + + return pos; +} + +static bool is_valid_pos(const df::coord pos) +{ + auto designation = Maps::getTileDesignation(pos); + if (!designation) + return false; + + if (designation->bits.hidden) + return false; // Items in parts of the map not yet revealed + + return true; +} + +static vector get_units_at(const df::coord pos, bool only_one) +{ + vector list; + + auto count = world->units.active.size(); + if (count > max_list_size) + return list; + + df::unit_flags1 bad_flags; + bad_flags.whole = 0; + bad_flags.bits.dead = true; + bad_flags.bits.hidden_ambusher = true; + bad_flags.bits.hidden_in_ambush = true; + + for (size_t i = 0; i < count; i++) + { + df::unit *unit = world->units.active[i]; + + if(unit->pos.x == pos.x && unit->pos.y == pos.y && unit->pos.z == pos.z && + !(unit->flags1.whole & bad_flags.whole) && + unit->profession != profession::THIEF && unit->profession != profession::MASTER_THIEF) + { + list.push_back(unit); + if (only_one) + break; + } + } + + return list; +} + +static vector get_items_at(const df::coord pos, bool only_one) +{ + vector list; + auto count = world->items.other[items_other_id::IN_PLAY].size(); + if (count > max_list_size) + return list; + + df::item_flags bad_flags; + bad_flags.whole = 0; + bad_flags.bits.in_building = true; + bad_flags.bits.garbage_collect = true; + bad_flags.bits.removed = true; + bad_flags.bits.dead_dwarf = true; + bad_flags.bits.murder = true; + bad_flags.bits.construction = true; + bad_flags.bits.in_inventory = true; + bad_flags.bits.in_chest = true; + + for (size_t i = 0; i < count; i++) + { + df::item *item = world->items.other[items_other_id::IN_PLAY][i]; + if (item->flags.whole & bad_flags.whole) + continue; + + if (pos.z == item->pos.z && pos.x == item->pos.x && pos.y == item->pos.y) + list.push_back(item); + } + + return list; +} + +static df::interface_key get_default_query_mode(const df::coord pos) +{ + if (!is_valid_pos(pos)) + return df::interface_key::D_LOOK; + + bool fallback_to_building_query = false; + + // Check for unit under cursor + auto ulist = get_units_at(pos, true); + if (ulist.size() > 0) + return df::interface_key::D_VIEWUNIT; + + // Check for building under cursor + auto bld = Buildings::findAtTile(pos); + if (bld) + { + df::building_type type = bld->getType(); + + if (type == building_type::Stockpile) + { + fallback_to_building_query = true; + } + else + { + // For containers use item view, for 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; + } + } + + // Check for items under cursor + auto ilist = get_items_at(pos, true); + if (ilist.size() > 0) + return df::interface_key::D_LOOK; + + return (fallback_to_building_query) ? df::interface_key::D_BUILDJOB : df::interface_key::D_LOOK; +} struct mousequery_hook : public df::viewscreen_dwarfmodest { typedef df::viewscreen_dwarfmodest interpose_base; - void send_key(const df::interface_key &key) + void sendKey(const df::interface_key &key) { set tmp; tmp.insert(key); - //INTERPOSE_NEXT(feed)(&tmp); - this->feed(&tmp); + INTERPOSE_NEXT(feed)(&tmp); + } + + bool isInDesignationMenu() + { + switch (ui->main.mode) + { + case DesignateMine: + case DesignateRemoveRamps: + case DesignateUpStair: + case DesignateDownStair: + case DesignateUpDownStair: + case DesignateUpRamp: + case DesignateChannel: + case DesignateGatherPlants: + case DesignateRemoveDesignation: + case DesignateSmooth: + case DesignateCarveTrack: + case DesignateEngrave: + case DesignateCarveFortification: + case DesignateChopTrees: + case DesignateToggleEngravings: + case DesignateRemoveConstruction: + case DesignateTrafficHigh: + case DesignateTrafficNormal: + case DesignateTrafficLow: + case DesignateTrafficRestricted: + return true; + + case Burrows: + return ui->burrows.in_define_mode; + }; + + return false; } - df::interface_key get_default_query_mode(const int32_t &x, const int32_t &y, const int32_t &z) + bool isInTrackableMode() { - bool fallback_to_building_query = false; + if (isInDesignationMenu()) + return box_designation_enabled; - // Check for unit under cursor - size_t count = world->units.all.size(); - if (count <= max_list_size) + switch (ui->main.mode) { - for(size_t i = 0; i < count; i++) - { - df::unit *unit = world->units.all[i]; + case DesignateItemsClaim: + case DesignateItemsForbid: + case DesignateItemsMelt: + case DesignateItemsUnmelt: + case DesignateItemsDump: + case DesignateItemsUndump: + case DesignateItemsHide: + case DesignateItemsUnhide: + case DesignateTrafficHigh: + case DesignateTrafficNormal: + case DesignateTrafficLow: + case DesignateTrafficRestricted: + case Stockpiles: + case Squads: + case NotesPoints: + case NotesRoutes: + case Zones: + return true; + + case Build: + return inBuildPlacement(); + + case QueryBuilding: + case BuildingItems: + case ViewUnits: + case LookAround: + return !enabler->mouse_lbut; + + default: + return false; + }; + } - if(unit->pos.x == x && unit->pos.y == y && unit->pos.z == z) - return df::interface_key::D_VIEWUNIT; - } - } - else + bool isInAreaSelectionMode() + { + bool selectableMode = + isInDesignationMenu() || + ui->main.mode == Stockpiles || + ui->main.mode == Zones; + + if (selectableMode) { - fallback_to_building_query = true; + int32_t x, y, z; + return Gui::getDesignationCoords(x, y, z); } - // Check for building under cursor - count = world->buildings.all.size(); - if (count <= max_list_size) + return false; + } + + bool handleMouse(const set *input) + { + int32_t mx, my; + auto mpos = get_mouse_pos(mx, my); + if (mpos.x == -30000) + return false; + + auto dims = Gui::getDwarfmodeViewDims(); + if (enabler->mouse_lbut) { - for(size_t i = 0; i < count; i++) - { - df::building *bld = world->buildings.all[i]; + bool cursor_still_here = (last_clicked_x == mpos.x && last_clicked_y == mpos.y && last_clicked_z == mpos.z); + last_clicked_x = mpos.x; + last_clicked_y = mpos.y; + last_clicked_z = mpos.z; - if (z == bld->z && - x >= bld->x1 && x <= bld->x2 && - y >= bld->y1 && y <= bld->y2) - { - df::building_type type = bld->getType(); + df::interface_key key = interface_key::NONE; + bool designationMode = false; + bool skipRefresh = false; - if (type == building_type::Stockpile) + if (isInTrackableMode()) + { + designationMode = true; + key = df::interface_key::SELECT; + } + else + { + 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 Build: + if (df::global::ui_build_selector) { - fallback_to_building_query = true; - break; // Check for items in stockpile first + if (df::global::ui_build_selector->stage < 2) + { + designationMode = true; + key = df::interface_key::SELECT; + } + else + { + designationMode = true; + skipRefresh = true; + key = df::interface_key::SELECT_ALL; + } } + break; + + case Default: + break; - // 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; + default: + return false; } } - } - else - { - fallback_to_building_query = true; - } + enabler->mouse_lbut = 0; - // Check for items under cursor - count = world->items.all.size(); - if (count <= max_list_size) - { - for(size_t i = 0; i < count; i++) + // Can't check limits earlier as we must be sure we are in query or default mode + // (so we can clear the button down flag) + int right_bound = (dims.menu_x1 > 0) ? dims.menu_x1 - 2 : gps->dimx - 2; + if (mx < 1 || mx > right_bound || my < 1 || my > gps->dimy - 2) + return false; + + if (ui->main.mode == df::ui_sidebar_mode::Zones || + ui->main.mode == df::ui_sidebar_mode::Stockpiles) { - 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) + int32_t x, y, z; + if (Gui::getDesignationCoords(x, y, z)) { - return df::interface_key::D_LOOK; + auto dX = abs(x - mpos.x); + if (dX > 30) + return false; + + auto dY = abs(y - mpos.y); + if (dY > 30) + return false; } } + + if (!designationMode) + { + while (ui->main.mode != Default) + { + sendKey(df::interface_key::LEAVESCREEN); + } + + if (key == interface_key::NONE) + key = get_default_query_mode(mpos); + + sendKey(key); + } + + if (!skipRefresh) + { + // Force UI refresh + moveCursor(mpos, true); + } + + if (designationMode) + sendKey(key); + + return true; + } + else if (rbutton_enabled && enabler->mouse_rbut) + { + if (isInDesignationMenu() && !box_designation_enabled) + return false; + + // Escape out of query mode + enabler->mouse_rbut_down = 0; + enabler->mouse_rbut = 0; + + using namespace df::enums::ui_sidebar_mode; + if ((ui->main.mode == QueryBuilding || ui->main.mode == BuildingItems || + ui->main.mode == ViewUnits || ui->main.mode == LookAround) || + (isInTrackableMode() && tracking_enabled)) + { + sendKey(df::interface_key::LEAVESCREEN); + } + else + { + int scroll_trigger_x = dims.menu_x1 / 3; + int scroll_trigger_y = gps->dimy / 3; + if (mx < scroll_trigger_x) + sendKey(interface_key::CURSOR_LEFT_FAST); + + if (mx > ((dims.menu_x1 > 0) ? dims.menu_x1 : gps->dimx) - scroll_trigger_x) + sendKey(interface_key::CURSOR_RIGHT_FAST); + + if (my < scroll_trigger_y) + sendKey(interface_key::CURSOR_UP_FAST); + + if (my > gps->dimy - scroll_trigger_y) + sendKey(interface_key::CURSOR_DOWN_FAST); + } + } + else if (input->count(interface_key::CUSTOM_M) && isInDesignationMenu()) + { + box_designation_enabled = !box_designation_enabled; } else { - fallback_to_building_query = true; + if (input->count(interface_key::CURSOR_UP) || + input->count(interface_key::CURSOR_DOWN) || + input->count(interface_key::CURSOR_LEFT) || + input->count(interface_key::CURSOR_RIGHT) || + input->count(interface_key::CURSOR_UPLEFT) || + input->count(interface_key::CURSOR_UPRIGHT) || + input->count(interface_key::CURSOR_DOWNLEFT) || + input->count(interface_key::CURSOR_DOWNRIGHT) || + input->count(interface_key::CURSOR_UP_FAST) || + input->count(interface_key::CURSOR_DOWN_FAST) || + input->count(interface_key::CURSOR_LEFT_FAST) || + input->count(interface_key::CURSOR_RIGHT_FAST) || + input->count(interface_key::CURSOR_UPLEFT_FAST) || + input->count(interface_key::CURSOR_UPRIGHT_FAST) || + input->count(interface_key::CURSOR_DOWNLEFT_FAST) || + input->count(interface_key::CURSOR_DOWNRIGHT_FAST) || + input->count(interface_key::CURSOR_UP_Z) || + input->count(interface_key::CURSOR_DOWN_Z) || + input->count(interface_key::CURSOR_UP_Z_AUX) || + input->count(interface_key::CURSOR_DOWN_Z_AUX)) + { + mouse_moved = false; + if (shouldTrack()) + skip_tracking_once = true; + } } - return (fallback_to_building_query) ? df::interface_key::D_BUILDJOB : df::interface_key::D_LOOK; + return false; } - bool handle_mouse(const set *input) + void moveCursor(df::coord &mpos, bool forced) { - int32_t cx, cy, vz; - if (enabler->tracking_on) + bool should_skip_tracking = skip_tracking_once; + skip_tracking_once = false; + if (!forced) + { + if (mpos.x == last_pos_x && mpos.y == last_pos_y && mpos.z == last_pos_z) + return; + } + + last_pos_x = mpos.x; + last_pos_y = mpos.y; + last_pos_z = mpos.z; + + if (!forced && should_skip_tracking) { - if (enabler->mouse_lbut) + return; + } + + int32_t x, y, z; + Gui::getCursorCoords(x, y, z); + if (mpos.x == x && mpos.y == y && mpos.z == z) + return; + + Gui::setCursorCoords(mpos.x, mpos.y, mpos.z); + sendKey(interface_key::CURSOR_DOWN_Z); + sendKey(interface_key::CURSOR_UP_Z); + } + + bool inBuildPlacement() + { + return df::global::ui_build_selector && + df::global::ui_build_selector->building_type != -1 && + df::global::ui_build_selector->stage == 1; + } + + bool shouldTrack() + { + if (!tracking_enabled) + return false; + + return isInTrackableMode(); + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!plugin_enabled || !handleMouse(input)) + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + if (!plugin_enabled) + return; + + static decltype(enabler->clock) last_t = 0; + + auto dims = Gui::getDwarfmodeViewDims(); + auto right_margin = (dims.menu_x1 > 0) ? dims.menu_x1 : gps->dimx; + + int32_t mx, my; + auto mpos = get_mouse_pos(mx, my); + bool mpos_valid = mpos.x != -30000 && mpos.y != -30000 && mpos.z != -30000; + if (mx < 1 || mx > right_margin - 2 || my < 1 || my > gps->dimy - 2) + mpos_valid = false; + + if (mpos_valid) + { + if (mpos.x != last_move_pos.x || mpos.y != last_move_pos.y || mpos.z != last_move_pos.z) { - 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; + mouse_moved = true; + last_move_pos = mpos; + } + } - 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; - } + int left_margin = dims.menu_x1 + 1; + int look_width = dims.menu_x2 - dims.menu_x1 - 1; + int disp_x = left_margin; - enabler->mouse_lbut = 0; + if (isInDesignationMenu()) + { + int x = left_margin; + int y = 24; + OutputString(COLOR_BROWN, x, y, "DFHack MouseQuery", true, left_margin); + OutputToggleString(x, y, "Box Select", "m", box_designation_enabled, true, left_margin); + } - // 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 + //Display selection dimensions + bool showing_dimensions = false; + if (isInAreaSelectionMode()) + { + showing_dimensions = true; + int32_t x, y, z; + Gui::getDesignationCoords(x, y, z); + coord32_t curr_pos; - if (mx < 1 || mx > w || my < 1 || my > gps->dimy - 2) - return false; + if (!tracking_enabled && mouse_moved && mpos_valid && + (!isInDesignationMenu() || box_designation_enabled)) + { + curr_pos = mpos; + } + else + { + Gui::getCursorCoords(curr_pos.x, curr_pos.y, curr_pos.z); + } + auto dX = abs(x - curr_pos.x) + 1; + auto dY = abs(y - curr_pos.y) + 1; + auto dZ = abs(z - curr_pos.z) + 1; + + int disp_y = gps->dimy - 3; + stringstream label; + label << "Selection: " << dX << "x" << dY << "x" << dZ; + OutputString(COLOR_WHITE, disp_x, disp_y, label.str()); + } + else + { + mouse_moved = false; + } - while (ui->main.mode != Default) - { - send_key(df::interface_key::LEAVESCREEN); - } + if (!mpos_valid) + return; - if (key == interface_key::NONE) - key = get_default_query_mode(cx, cy, vz); + int scroll_buffer = 6; + auto delta_t = enabler->clock - last_t; + if (active_scrolling && !isInTrackableMode() && delta_t > scroll_delay) + { + last_t = enabler->clock; + if (mx < scroll_buffer) + { + sendKey(interface_key::CURSOR_LEFT); + return; + } - send_key(key); + if (mx > right_margin - scroll_buffer) + { + sendKey(interface_key::CURSOR_RIGHT); + return; + } - // 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; + if (my < scroll_buffer) + { + sendKey(interface_key::CURSOR_UP); + return; + } - return true; - } - } + if (my > gps->dimy - scroll_buffer) + { + sendKey(interface_key::CURSOR_DOWN); + return; } - else if (enabler->mouse_rbut) + } + + if (!live_view && !isInTrackableMode() && !DFHack::World::ReadPauseState()) + return; + + if (!tracking_enabled && isInTrackableMode()) + { + UIColor color = COLOR_GREEN; + int32_t x, y, z; + if (Gui::getDesignationCoords(x, y, z)) { - // 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) + color = COLOR_WHITE; + if (ui->main.mode == df::ui_sidebar_mode::Zones || + ui->main.mode == df::ui_sidebar_mode::Stockpiles) { - while (ui->main.mode != Default) - { - enabler->mouse_rbut = 0; - send_key(df::interface_key::LEAVESCREEN); - } + auto dX = abs(x - mpos.x); + if (dX > 30) + color = COLOR_RED; + + auto dY = abs(y - mpos.y); + if (dY > 30) + color = COLOR_RED; } } + + OutputString(color, mx, my, "X"); + return; + } + + if (shouldTrack()) + { + if (delta_t <= scroll_delay && (mx < scroll_buffer || + mx > dims.menu_x1 - scroll_buffer || + my < scroll_buffer || + my > gps->dimy - scroll_buffer)) + { + return; + } + + last_t = enabler->clock; + moveCursor(mpos, false); } - return false; + if (dims.menu_x1 <= 0) + return; // No menu displayed + + if (!is_valid_pos(mpos) || isInTrackableMode()) + return; + + if (showing_dimensions) + return; + + // Display live query + auto ulist = get_units_at(mpos, false); + auto bld = Buildings::findAtTile(mpos); + auto ilist = get_items_at(mpos, false); + + int look_list = ulist.size() + ((bld) ? 1 : 0) + ilist.size() + 1; + set_to_limit(look_list, 8); + int disp_y = gps->dimy - look_list - 2; + + int c = 0; + for (auto it = ulist.begin(); it != ulist.end() && c < 8; it++, c++) + { + string label; + auto name = Units::getVisibleName(*it); + if (name->has_name) + label = Translation::TranslateName(name, false); + if (label.length() > 0) + label += ", "; + + label += Units::getProfessionName(*it); // Check animal type too + label = pad_string(label, look_width, false, true); + + OutputString(COLOR_WHITE, disp_x, disp_y, label, true, left_margin); + } + + for (auto it = ilist.begin(); it != ilist.end() && c < 8; it++, c++) + { + auto label = Items::getDescription(*it, 0, false); + label = pad_string(label, look_width, false, true); + OutputString(COLOR_YELLOW, disp_x, disp_y, label, true, left_margin); + } + + if (c > 7) + return; + + if (bld) + { + string label; + bld->getName(&label); + label = pad_string(label, look_width, false, true); + OutputString(COLOR_CYAN, disp_x, disp_y, label, true, left_margin); + } + + if (c > 7) + return; + + auto tt = Maps::getTileType(mpos); + OutputString(COLOR_BLUE, disp_x, disp_y, tileName(*tt), true, left_margin); } +}; - DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) +IMPLEMENT_VMETHOD_INTERPOSE_PRIO(mousequery_hook, feed, 100); +IMPLEMENT_VMETHOD_INTERPOSE_PRIO(mousequery_hook, render, 100); + +static command_result mousequery_cmd(color_ostream &out, vector & parameters) +{ + bool show_help = false; + if (parameters.size() < 1) { - if (!handle_mouse(input)) - INTERPOSE_NEXT(feed)(input); + show_help = true; + } + else + { + auto cmd = toLower(parameters[0]); + auto state = (parameters.size() == 2) ? toLower(parameters[1]) : "-1"; + if (cmd[0] == 'v') + { + out << "MouseQuery" << endl << "Version: " << PLUGIN_VERSION << endl; + } + else if (cmd[0] == 'p') + { + plugin_enabled = (state == "enable"); + } + else if (cmd[0] == 'r') + { + rbutton_enabled = (state == "enable"); + } + else if (cmd[0] == 't') + { + tracking_enabled = (state == "enable"); + if (!tracking_enabled) + active_scrolling = false; + } + else if (cmd[0] == 'e') + { + active_scrolling = (state == "enable"); + if (active_scrolling) + tracking_enabled = true; + } + else if (cmd[0] == 'l') + { + live_view = (state == "enable"); + } + else if (cmd[0] == 'd') + { + auto l = atoi(state.c_str()); + if (l > 0 || state == "0") + scroll_delay = l; + else + out << "Current delay: " << scroll_delay << endl; + } + else + { + show_help = true; + } } -}; -IMPLEMENT_VMETHOD_INTERPOSE(mousequery_hook, feed); + if (show_help) + return CR_WRONG_USAGE; -DFHACK_PLUGIN("mousequery"); + return CR_OK; +} DFHACK_PLUGIN_IS_ENABLED(is_enabled); DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable) @@ -248,9 +796,12 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable) if (is_enabled != enable) { - last_x = last_y = last_z = -1; + last_clicked_x = last_clicked_y = last_clicked_z = -1; + last_pos_x = last_pos_y = last_pos_z = -1; + last_move_pos.x = last_move_pos.y = last_move_pos.z = -1; - if (!INTERPOSE_HOOK(mousequery_hook, feed).apply(enable)) + if (!INTERPOSE_HOOK(mousequery_hook, feed).apply(enable) || + !INTERPOSE_HOOK(mousequery_hook, render).apply(enable)) return CR_FAILURE; is_enabled = enable; @@ -261,7 +812,19 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable) DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - last_x = last_y = last_z = -1; + commands.push_back( + PluginCommand( + "mousequery", "Add mouse functionality to Dwarf Fortress", + mousequery_cmd, false, + "mousequery [plugin|rbutton|track|edge|live] [enabled|disabled]\n" + " plugin: enable/disable the entire plugin\n" + " rbutton: enable/disable right mouse button\n" + " track: enable/disable moving cursor in build and designation mode\n" + " edge: enable/disable active edge scrolling (when on, will also enable tracking)\n" + " live: enable/disable query view when unpaused\n\n" + "mousequery delay \n" + " Set delay when edge scrolling in tracking mode. Omit amount to display current setting.\n" + )); return CR_OK; } @@ -270,7 +833,9 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan { switch (event) { case SC_MAP_LOADED: - last_x = last_y = last_z = -1; + last_clicked_x = last_clicked_y = last_clicked_z = -1; + last_pos_x = last_pos_y = last_pos_z = -1; + last_move_pos.x = last_move_pos.y = last_move_pos.z = -1; break; default: break; diff --git a/plugins/search.cpp b/plugins/search.cpp index 0f519414b..e0213bf06 100644 --- a/plugins/search.cpp +++ b/plugins/search.cpp @@ -11,6 +11,7 @@ #include "df/viewscreen_layer_stockpilest.h" #include "df/viewscreen_layer_militaryst.h" #include "df/viewscreen_layer_noblelistst.h" +#include "df/viewscreen_layer_workshop_profilest.h" #include "df/viewscreen_tradegoodsst.h" #include "df/viewscreen_unitlistst.h" #include "df/viewscreen_buildinglistst.h" @@ -423,7 +424,7 @@ protected: virtual bool can_init(S *screen) { auto list = getLayerList(screen); - if (!is_list_valid(screen) || !list->active) + if (!is_list_valid(screen) || !list || !list->active) return false; return true; @@ -699,8 +700,8 @@ template V generic_search_hook ::module; #define IMPLEMENT_HOOKS_PRIO(screen, module, prio) \ typedef generic_search_hook module##_hook; \ - template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, feed, 100); \ - template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, render, 100) + template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, feed, prio); \ + template<> IMPLEMENT_VMETHOD_INTERPOSE_PRIO(module##_hook, render, prio) // // END: Generic Search functionality @@ -920,7 +921,7 @@ private: }; -IMPLEMENT_HOOKS(df::viewscreen_storesst, stocks_search); +IMPLEMENT_HOOKS_PRIO(df::viewscreen_storesst, stocks_search, 100); // // END: Stocks screen search @@ -1048,7 +1049,9 @@ private: { // Block the keys if were searching if (!search_string.empty()) + { input->clear(); + } return false; } @@ -1081,6 +1084,9 @@ public: { make_text_dim(2, 37, 22); make_text_dim(42, gps->dimx-2, 22); + int32_t x = 2; + int32_t y = gps->dimy - 3; + OutputString(COLOR_YELLOW, x, y, "Note: Clear search to trade"); } } @@ -1120,6 +1126,9 @@ public: { make_text_dim(2, 37, 22); make_text_dim(42, gps->dimx-2, 22); + int32_t x = 42; + int32_t y = gps->dimy - 3; + OutputString(COLOR_YELLOW, x, y, "Note: Clear search to trade"); } } @@ -1432,6 +1441,36 @@ IMPLEMENT_HOOKS(df::viewscreen_layer_noblelistst, nobles_search); // END: Nobles search list // +// +// START: Workshop profiles search list +// +typedef layered_search profiles_search_base; +class profiles_search : public profiles_search_base +{ +public: + + string get_element_description(df::unit *element) const + { + return get_unit_description(element); + } + + void render() const + { + print_search_option(2, 23); + } + + vector *get_primary_list() + { + return &viewscreen->workers; + } +}; + +IMPLEMENT_HOOKS(df::viewscreen_layer_workshop_profilest, profiles_search); + +// +// END: Workshop profiles search list +// + // // START: Job list search @@ -1621,6 +1660,7 @@ DFHACK_PLUGIN_IS_ENABLED(is_enabled); HOOK_ACTION(pets_search_hook) \ HOOK_ACTION(military_search_hook) \ HOOK_ACTION(nobles_search_hook) \ + HOOK_ACTION(profiles_search_hook) \ HOOK_ACTION(annoucnement_search_hook) \ HOOK_ACTION(joblist_search_hook) \ HOOK_ACTION(burrow_search_hook) \ diff --git a/plugins/stocks.cpp b/plugins/stocks.cpp index 7873595ff..3b9c0317d 100644 --- a/plugins/stocks.cpp +++ b/plugins/stocks.cpp @@ -8,6 +8,7 @@ #include "df/item.h" #include "df/viewscreen_dwarfmodest.h" +#include "df/viewscreen_storesst.h" #include "df/items_other_id.h" #include "df/job.h" #include "df/unit.h" @@ -23,11 +24,14 @@ #include "modules/World.h" #include "modules/Screen.h" #include "modules/Maps.h" +#include "modules/Units.h" +#include "df/building_cagest.h" +#include "df/ui_advmode.h" using df::global::world; DFHACK_PLUGIN("stocks"); -#define PLUGIN_VERSION 0.2 +#define PLUGIN_VERSION 0.12 DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { @@ -61,36 +65,23 @@ static string get_quality_name(const df::item_quality quality) 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) +static df::item *get_container_of(df::unit *unit) { - item = (container) ? container : get_container_of(item); - auto job = get_item_job(item); - if (!job) - return false; - - return job->job_type == job_type::BringItemToDepot; + auto ref = Units::getGeneralRef(unit, general_ref_type::CONTAINED_IN_ITEM); + return (ref) ? ref->getItem() : nullptr; } + +/* + * Trade Info + */ + static bool check_mandates(df::item *item) { for (auto it = world->mandates.begin(); it != world->mandates.end(); it++) @@ -173,12 +164,6 @@ static bool can_trade_item_and_container(df::item *item) return true; } -static bool is_in_inventory(df::item *item) -{ - item = get_container_of(item); - return item->flags.bits.in_inventory; -} - class TradeDepotInfo { @@ -188,7 +173,7 @@ public: reset(); } - void prepareTradeVarables() + void prepareTradeVariables() { reset(); for(auto bld_it = world->buildings.all.begin(); bld_it != world->buildings.all.end(); bld_it++) @@ -199,43 +184,47 @@ public: depot = bld; id = depot->id; - trade_possible = caravansAvailable(); + trade_possible = can_trade(); break; } } - bool assignItem(df::item *item) + bool assignItem(vector &entries) { - item = get_container_of(item); - if (!can_trade_item_and_container(item)) - return false; + for (auto it = entries.begin(); it != entries.end(); it++) + { + auto item = *it; + item = get_container_of(item); + if (!can_trade_item_and_container(item)) + return false; - auto href = df::allocate(); - if (!href) - return false; + auto href = df::allocate(); + if (!href) + return false; - auto job = new df::job(); + auto job = new df::job(); - df::coord tpos(depot->centerx, depot->centery, depot->z); - job->pos = tpos; + df::coord tpos(depot->centerx, depot->centery, depot->z); + job->pos = tpos; - job->job_type = job_type::BringItemToDepot; + 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 <-> 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); + // 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); + // add to job list + Job::linkIntoWorld(job); + } return true; } @@ -269,27 +258,46 @@ private: return true; } +}; - bool caravansAvailable() - { - if (df::global::ui->caravans.size() == 0) - return false; +static TradeDepotInfo depot_info; - 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; - } -}; +/* + * Item manipulation + */ -static TradeDepotInfo depot_info; +static map items_in_cages; +static df::job *get_item_job(df::item *item) +{ + auto ref = Items::getSpecificRef(item, specific_ref_type::JOB); + if (ref && ref->job) + return ref->job; + + return nullptr; +} + +static bool is_marked_for_trade(df::item *item, df::item *container = nullptr) +{ + item = (container) ? container : get_container_of(item); + auto job = get_item_job(item); + if (!job) + return false; + + return job->job_type == job_type::BringItemToDepot; +} + +static bool is_in_inventory(df::item *item) +{ + item = get_container_of(item); + return item->flags.bits.in_inventory; +} + +static bool is_item_in_cage_cache(df::item *item) +{ + return items_in_cages.find(item) != items_in_cages.end(); +} static string get_keywords(df::item *item) { @@ -301,9 +309,6 @@ static string get_keywords(df::item *item) if (item->flags.bits.rotten) keywords += "rotten "; - if (item->flags.bits.foreign) - keywords += "foreign "; - if (item->flags.bits.owned) keywords += "owned "; @@ -319,6 +324,9 @@ static string get_keywords(df::item *item) if (item->flags.bits.melt) keywords += "melt "; + if (is_item_in_cage_cache(item)) + keywords += "caged "; + if (is_in_inventory(item)) keywords += "inventory "; @@ -331,11 +339,178 @@ static string get_keywords(df::item *item) return keywords; } +static string get_item_label(df::item *item, bool trim = false) +{ + auto label = Items::getDescription(item, 0, false); + if (trim && item->getType() == item_type::BIN) + { + auto pos = label.find("<#"); + if (pos != string::npos) + { + label = label.substr(0, pos-1); + } + } + + auto wear = item->getWear(); + if (wear > 0) + { + string wearX; + switch (wear) + { + case 1: + wearX = "x"; + break; + + case 2: + wearX = "X"; + break; + + case 3: + wearX = "xX"; + break; + + default: + wearX = "XX"; + break; + + } + + label = wearX + label + wearX; + } + + label = pad_string(label, MAX_NAME, false, true); + + return label; +} + +struct item_grouped_entry +{ + std::vector entries; + + string getLabel(bool grouped) const + { + if (entries.size() == 0) + return ""; + + return get_item_label(entries[0], grouped); + } + + string getKeywords() const + { + return get_keywords(entries[0]); + } + + df::item *getFirstItem() const + { + if (entries.size() == 0) + return nullptr; + + return entries[0]; + } + + bool canMelt() const + { + // TODO: Fix melting + return false; + + df::item *item = getFirstItem(); + if (!item) + return false; + + return can_melt(item); + } + + bool isSetToMelt() const + { + df::item *item = getFirstItem(); + if (!item) + return false; + + return is_set_to_melt(item); + } + + bool contains(df::item *item) const + { + return std::find(entries.begin(), entries.end(), item) != entries.end(); + } + + void setFlags(const df::item_flags flags, const bool state) + { + for (auto it = entries.begin(); it != entries.end(); it++) + { + if (state) + (*it)->flags.whole |= flags.whole; + else + (*it)->flags.whole &= ~flags.whole; + } + } + + bool isSingleItem() + { + return entries.size() == 1; + } +}; + + +struct extra_filters +{ + bool hide_trade_marked, hide_in_inventory, hide_in_cages; + + extra_filters() + { + reset(); + } + + void reset() + { + hide_in_inventory = false; + hide_trade_marked = false; + } +}; + +static bool cages_populated = false; +static vector cages; + +static void find_cages() +{ + if (cages_populated) + return; + + for (size_t b=0; b < world->buildings.all.size(); b++) + { + df::building* building = world->buildings.all[b]; + if (building->getType() == building_type::Cage) + { + cages.push_back(static_cast(building)); + } + } + + cages_populated = true; +} + +static df::building_cagest *is_in_cage(df::unit *unit) +{ + find_cages(); + for (auto it = cages.begin(); it != cages.end(); it++) + { + auto cage = *it; + for (size_t c = 0; c < cage->assigned_units.size(); c++) + { + if(cage->assigned_units[c] == unit->id) + return cage; + } + } + + return nullptr; +} + + template class StockListColumn : public ListColumn { - virtual void display_extras(const T &item, int32_t &x, int32_t &y) const + virtual void display_extras(const T &item_group, int32_t &x, int32_t &y) const { + auto item = item_group->getFirstItem(); if (item->flags.bits.in_job) OutputString(COLOR_LIGHTBLUE, x, y, "J"); else @@ -346,11 +521,6 @@ class StockListColumn : public ListColumn 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 @@ -365,12 +535,12 @@ class StockListColumn : public ListColumn 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 @@ -381,6 +551,11 @@ class StockListColumn : public ListColumn else OutputString(COLOR_LIGHTBLUE, x, y, " "); + if (is_item_in_cage_cache(item)) + OutputString(COLOR_LIGHTRED, x, y, "C"); + else + OutputString(COLOR_LIGHTBLUE, x, y, " "); + if (depot_info.canTrade()) { if (is_marked_for_trade(item)) @@ -428,21 +603,6 @@ class StockListColumn : public ListColumn } }; -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 { @@ -450,10 +610,10 @@ public: static df::item_flags hide_flags; static extra_filters extra_hide_flags; - ViewscreenStocks() + ViewscreenStocks(df::building_stockpilest *sp = NULL) : sp(sp) { + is_grouped = true; selected_column = 0; - items_column.setTitle("Item"); items_column.multiselect = false; items_column.auto_select = true; items_column.allow_search = true; @@ -468,7 +628,6 @@ public: 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; @@ -479,6 +638,11 @@ public: min_quality = item_quality::Ordinary; max_quality = item_quality::Artifact; min_wear = 0; + cages.clear(); + items_in_cages.clear(); + cages_populated = false; + + last_selected_item = nullptr; populateItems(); @@ -512,12 +676,7 @@ public: 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)) + if (input->count(interface_key::CUSTOM_CTRL_J)) { hide_flags.bits.in_job = !hide_flags.bits.in_job; populateItems(); @@ -557,6 +716,11 @@ public: extra_hide_flags.hide_in_inventory = !extra_hide_flags.hide_in_inventory; populateItems(); } + else if (input->count(interface_key::CUSTOM_CTRL_C)) + { + extra_hide_flags.hide_in_cages = !extra_hide_flags.hide_in_cages; + populateItems(); + } else if (input->count(interface_key::CUSTOM_CTRL_T)) { extra_hide_flags.hide_trade_marked = !extra_hide_flags.hide_trade_marked; @@ -577,6 +741,12 @@ public: setAllFlags(false); populateItems(); } + else if (input->count(interface_key::CHANGETAB)) + { + is_grouped = !is_grouped; + populateItems(); + items_column.centerSelection(); + } else if (input->count(interface_key::SECONDSCROLL_UP)) { if (min_quality > item_quality::Ordinary) @@ -621,18 +791,23 @@ public: else if (input->count(interface_key::CUSTOM_SHIFT_Z)) { input->clear(); - auto item = items_column.getFirstSelectedElem(); - if (!item) + auto item_group = items_column.getFirstSelectedElem(); + if (!item_group) return; + + if (is_grouped && !item_group->isSingleItem()) + return; + + auto item = item_group->getFirstItem(); auto pos = getRealPos(item); - if (!pos) + if (!isRealPos(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); + move_cursor(pos); } else if (input->count(interface_key::CUSTOM_SHIFT_A)) { @@ -642,35 +817,33 @@ public: { df::item_flags flags; flags.bits.dump = true; - applyFlag(flags); + toggleFlag(flags); populateItems(); } else if (input->count(interface_key::CUSTOM_SHIFT_F)) { df::item_flags flags; flags.bits.forbid = true; - applyFlag(flags); + toggleFlag(flags); + populateItems(); + } + else if (input->count(interface_key::CUSTOM_SHIFT_M)) + { + //TODO: Fix melting + return; + + toggleMelt(); populateItems(); } else if (input->count(interface_key::CUSTOM_SHIFT_T)) { - if (apply_to_all) + if (depot_info.canTrade()) { - auto &list = items_column.getDisplayList(); - for (auto iter = list.begin(); iter != list.end(); iter++) + auto selected = getSelectedItems(); + for (auto it = selected.begin(); it != selected.end(); it++) { - auto item = (*iter)->elem; - if (item) - depot_info.assignItem(item); + depot_info.assignItem((*it)->entries); } - - populateItems(); - } - else - { - auto item = items_column.getFirstSelectedElem(); - if (item && depot_info.assignItem(item)) - populateItems(); } } @@ -731,21 +904,22 @@ public: 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); + 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, "Caged", "C", !extra_hide_flags.hide_in_cages, true, left_margin, COLOR_LIGHTRED); OutputFilterString(x, y, "Trade", "T", !extra_hide_flags.hide_trade_marked, true, left_margin, COLOR_LIGHTGREEN); OutputFilterString(x, y, "No Flags", "N", !hide_unflagged, true, left_margin, COLOR_GREY); ++y; OutputHotkeyString(x, y, "Clear All", "Shift-C", true, left_margin); OutputHotkeyString(x, y, "Enable All", "Shift-E", true, left_margin); + OutputHotkeyString(x, y, "Toggle Grouping", "TAB", true, left_margin); ++y; OutputHotkeyString(x, y, "Min Qual: ", "-+"); OutputString(COLOR_BROWN, x, y, get_quality_name(min_quality), true, left_margin); @@ -765,21 +939,43 @@ public: 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); + + //TODO: Fix melting + //OutputHotkeyString(x, y, "Melt", "Shift-M", true, left_margin); + + if (depot_info.canTrade()) + OutputHotkeyString(x, y, "Mark for Trade", "Shift-T", true, left_margin); + + y = gps->dimy - 6; + OutputString(COLOR_LIGHTRED, x, y, "Flag names can also", true, left_margin); + OutputString(COLOR_LIGHTRED, x, y, "be searched for", true, left_margin); } std::string getFocusString() { return "stocks_view"; } private: - StockListColumn items_column; + StockListColumn items_column; int selected_column; bool apply_to_all, hide_unflagged; df::item_flags checked_flags; df::item_quality min_quality, max_quality; int16_t min_wear; + bool is_grouped; + std::list grouped_items_store; + df::item *last_selected_item; + string last_selected_hash; + int last_display_offset; + df::building_stockpilest *sp; + + static bool isRealPos(const df::coord pos) + { + return pos.x != -30000; + } - static df::coord *getRealPos(df::item *item) + static df::coord getRealPos(df::item *item) { + df::coord pos; + pos.x = -30000; item = get_container_of(item); if (item->flags.bits.in_inventory) { @@ -789,61 +985,144 @@ private: if (ref && ref->job) { if (ref->job->job_type == job_type::Eat || ref->job->job_type == job_type::Drink) - return nullptr; + return pos; auto unit = Job::getWorker(ref->job); if (unit) - return &unit->pos; + return unit->pos; } - return nullptr; + return pos; } else { auto unit = Items::getHolderUnit(item); if (unit) - return &unit->pos; + { + if (!Units::isCitizen(unit)) + { + auto cage_item = get_container_of(unit); + if (cage_item) + { + items_in_cages[item] = true; + return cage_item->pos; + } + + auto cage_building = is_in_cage(unit); + if (cage_building) + { + items_in_cages[item] = true; + pos.x = cage_building->centerx; + pos.y = cage_building->centery; + pos.z = cage_building->z; + } + + return pos; + } + + return unit->pos; + } - return nullptr; + return pos; } } - return &item->pos; + return item->pos; } - void applyFlag(const df::item_flags flags) + void toggleMelt() { - if (apply_to_all) + //TODO: Fix melting + return; + + int set_to_melt = -1; + auto selected = getSelectedItems(); + vector items; + for (auto it = selected.begin(); it != selected.end(); it++) { - int state_to_apply = -1; - for (auto iter = items_column.getDisplayList().begin(); iter != items_column.getDisplayList().end(); iter++) + auto item_group = *it; + + if (set_to_melt == -1) + set_to_melt = (item_group->isSetToMelt()) ? 0 : 1; + + if (set_to_melt) { - auto item = (*iter)->elem; - if (item) + if (!item_group->canMelt() || item_group->isSetToMelt()) + continue; + } + else if (!item_group->isSetToMelt()) + { + continue; + } + + items.insert(items.end(), item_group->entries.begin(), item_group->entries.end()); + } + + auto &melting_items = world->items.other[items_other_id::ANY_MELT_DESIGNATED]; + for (auto it = items.begin(); it != items.end(); it++) + { + auto item = *it; + if (set_to_melt) + { + insert_into_vector(melting_items, &df::item::id, item); + item->flags.bits.melt = true; + } + else + { + for (auto mit = melting_items.begin(); mit != melting_items.end(); mit++) { - // 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; + if (item != *mit) + continue; + + melting_items.erase(mit); + item->flags.bits.melt = false; + break; } } } + } + + void toggleFlag(const df::item_flags flags) + { + int state_to_apply = -1; + auto selected = getSelectedItems(); + for (auto it = selected.begin(); it != selected.end(); it++) + { + auto grouped_entry = (*it); + auto item = grouped_entry->getFirstItem(); + if (state_to_apply == -1) + state_to_apply = (item->flags.whole & flags.whole) ? 0 : 1; + + grouped_entry->setFlags(flags.whole, state_to_apply); + } + } + + vector getSelectedItems() + { + vector result; + if (apply_to_all) + { + for (auto it = items_column.getDisplayList().begin(); it != items_column.getDisplayList().end(); it++) + { + auto item_group = (*it)->elem; + if (!item_group) + continue; + result.push_back(item_group); + } + } else { - auto item = items_column.getFirstSelectedElem(); - if (item) - item->flags.whole ^= flags.whole; + auto item_group = items_column.getFirstSelectedElem(); + if (item_group) + result.push_back(item_group); } + + return result; } void setAllFlags(bool state) { hide_flags.bits.in_job = state; hide_flags.bits.rotten = state; - hide_flags.bits.foreign = state; hide_flags.bits.owned = state; hide_flags.bits.forbid = state; hide_flags.bits.dump = state; @@ -853,10 +1132,13 @@ private: hide_unflagged = state; extra_hide_flags.hide_trade_marked = state; extra_hide_flags.hide_in_inventory = state; + extra_hide_flags.hide_in_cages = state; } void populateItems() { + items_column.setTitle((is_grouped) ? "Item (count)" : "Item"); + preserveLastSelected(); items_column.clear(); df::item_flags bad_flags; @@ -865,15 +1147,20 @@ private: 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(); + depot_info.prepareTradeVariables(); - std::vector &items = world->items.other[items_other_id::IN_PLAY]; + std::vector &items = world->items.other[items_other_id::IN_PLAY]; + std::map grouped_items; + grouped_items_store.clear(); + item_grouped_entry *next_selected_group = nullptr; + StockpileInfo spInfo; + if (sp) + spInfo = StockpileInfo(sp); for (size_t i = 0; i < items.size(); i++) { @@ -887,13 +1174,10 @@ private: continue; auto pos = getRealPos(item); - if (!pos) + if (!isRealPos(pos)) continue; - if (pos->x == -30000) - continue; - - auto designation = Maps::getTileDesignation(*pos); + auto designation = Maps::getTileDesignation(pos); if (!designation) continue; @@ -904,11 +1188,15 @@ private: if (extra_hide_flags.hide_trade_marked && trade_marked) continue; + bool caged = is_item_in_cage_cache(item); + if (extra_hide_flags.hide_in_cages && caged) + continue; + if (extra_hide_flags.hide_in_inventory && container->flags.bits.in_inventory) continue; if (hide_unflagged && (!(item->flags.whole & checked_flags.whole) && - !trade_marked && !container->flags.bits.in_inventory)) + !trade_marked && !caged && !container->flags.bits.in_inventory)) { continue; } @@ -921,40 +1209,89 @@ private: if (wear < min_wear) continue; - auto label = Items::getDescription(item, 0, false); - if (wear > 0) + if (spInfo.isValid() && !spInfo.inStockpile(item)) + continue; + + if (is_grouped) { - string wearX; - switch (wear) + auto hash = getItemHash(item); + if (grouped_items.find(hash) == grouped_items.end()) { - case 1: - wearX = "x"; - break; - - case 2: - wearX = "X"; - break; - - case 3: - wearX = "xX"; - break; + grouped_items_store.push_back(item_grouped_entry()); + grouped_items[hash] = &grouped_items_store.back(); + } + grouped_items[hash]->entries.push_back(item); + if (last_selected_item && + !next_selected_group && + hash == last_selected_hash) + { + next_selected_group = grouped_items[hash]; + } + } + else + { + grouped_items_store.push_back(item_grouped_entry()); + auto item_group = &grouped_items_store.back(); + item_group->entries.push_back(item); - default: - wearX = "XX"; - break; + auto label = get_item_label(item); + auto entry = ListEntry(label, item_group, item_group->getKeywords()); + items_column.add(entry); + if (last_selected_item && + !next_selected_group && + item == last_selected_item) + { + next_selected_group = item_group; } + } + } - label = wearX + label + wearX; + if (is_grouped) + { + for (auto groups_iter = grouped_items.begin(); groups_iter != grouped_items.end(); groups_iter++) + { + auto item_group = groups_iter->second; + stringstream label; + label << item_group->getLabel(is_grouped); + if (!item_group->isSingleItem()) + label << " (" << item_group->entries.size() << ")"; + auto entry = ListEntry(label.str(), item_group, item_group->getKeywords()); + items_column.add(entry); } + } - label = pad_string(label, MAX_NAME, false, true); + items_column.fixWidth(); + items_column.filterDisplay(); - auto entry = ListEntry(label, item, get_keywords(item)); - items_column.add(entry); + if (next_selected_group) + { + items_column.selectItem(next_selected_group); + items_column.display_start_offset = last_display_offset; } + } + + string getItemHash(df::item *item) + { + auto label = get_item_label(item, true); + auto quality = static_cast(item->getQuality()); + auto quality_enum = static_cast(quality); + auto quality_string = ENUM_KEY_STR(item_quality, quality_enum); + auto hash = label + quality_string + int_to_string(item->flags.whole & checked_flags.whole) + " " + + int_to_string(item->hasImprovements()); - items_column.filterDisplay(); + return hash; + } + + void preserveLastSelected() + { + last_selected_item = nullptr; + auto selected_entry = items_column.getFirstSelectedElem(); + if (!selected_entry) + return; + last_selected_item = selected_entry->getFirstItem(); + last_selected_hash = (is_grouped && last_selected_item) ? getItemHash(last_selected_item) : ""; + last_display_offset = items_column.display_start_offset; } void validateColumn() @@ -973,6 +1310,102 @@ private: df::item_flags ViewscreenStocks::hide_flags; extra_filters ViewscreenStocks::extra_hide_flags; +struct stocks_hook : public df::viewscreen_storesst +{ + typedef df::viewscreen_storesst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (input->count(interface_key::CUSTOM_E)) + { + Screen::dismiss(this); + Screen::dismiss(Gui::getCurViewscreen(true)); + Screen::show(new ViewscreenStocks()); + return; + } + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + auto dim = Screen::getWindowSize(); + int x = 40; + int y = dim.y - 2; + OutputHotkeyString(x, y, "Enhanced View", "e"); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(stocks_hook, feed); +IMPLEMENT_VMETHOD_INTERPOSE(stocks_hook, render); + + +struct stocks_stockpile_hook : public df::viewscreen_dwarfmodest +{ + typedef df::viewscreen_dwarfmodest interpose_base; + + bool handleInput(set *input) + { + df::building_stockpilest *sp = get_selected_stockpile(); + if (!sp) + return false; + + if (input->count(interface_key::CUSTOM_I)) + { + Screen::show(new ViewscreenStocks(sp)); + return true; + } + + return false; + } + + DEFINE_VMETHOD_INTERPOSE(void, feed, (set *input)) + { + if (!handleInput(input)) + INTERPOSE_NEXT(feed)(input); + } + + DEFINE_VMETHOD_INTERPOSE(void, render, ()) + { + INTERPOSE_NEXT(render)(); + + df::building_stockpilest *sp = get_selected_stockpile(); + if (!sp) + return; + + auto dims = Gui::getDwarfmodeViewDims(); + int left_margin = dims.menu_x1 + 1; + int x = left_margin; + int y = 23; + + OutputHotkeyString(x, y, "Show Inventory", "i", true, left_margin); + } +}; + +IMPLEMENT_VMETHOD_INTERPOSE(stocks_stockpile_hook, feed); +IMPLEMENT_VMETHOD_INTERPOSE(stocks_stockpile_hook, render); + + +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + if (!gps) + return CR_FAILURE; + + if (enable != is_enabled) + { + if (!INTERPOSE_HOOK(stocks_hook, feed).apply(enable) || + !INTERPOSE_HOOK(stocks_hook, render).apply(enable) || + !INTERPOSE_HOOK(stocks_stockpile_hook, feed).apply(enable) || + !INTERPOSE_HOOK(stocks_stockpile_hook, render).apply(enable)) + return CR_FAILURE; + + is_enabled = enable; + } + + return CR_OK; +} static command_result stocks_cmd(color_ostream &out, vector & parameters) { @@ -993,12 +1426,8 @@ static command_result stocks_cmd(color_ostream &out, vector & parameter return CR_WRONG_USAGE; } - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +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", @@ -1009,7 +1438,6 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector +#include +#include +#include #include #include #include @@ -10,9 +13,17 @@ #include #include +#include "modules/Items.h" #include "modules/Screen.h" +#include "modules/World.h" +#include "df/building_stockpilest.h" +#include "df/caravan_state.h" +#include "df/dfhack_material_category.h" #include "df/enabler.h" +#include "df/item_quality.h" +#include "df/ui.h" +#include "df/world.h" using namespace std; using std::string; @@ -32,11 +43,34 @@ using df::global::gps; #define nullptr 0L #endif -#define COLOR_TITLE COLOR_BLUE +#define COLOR_TITLE COLOR_BROWN #define COLOR_UNSELECTED COLOR_GREY #define COLOR_SELECTED COLOR_WHITE #define COLOR_HIGHLIGHTED COLOR_GREEN +struct coord32_t +{ + int32_t x, y, z; + + coord32_t() + { + x = -30000; + y = -30000; + z = -30000; + } + + coord32_t(df::coord& other) + { + x = other.x; + y = other.y; + z = other.z; + } + + df::coord get_coord16() const + { + return df::coord(x, y, z); + } +}; template static void for_each_(vector &v, Fn func) @@ -80,6 +114,17 @@ void OutputHotkeyString(int &x, int &y, const char *text, const char *hotkey, bo OutputString(text_color, x, y, display, newline, left_margin); } +void OutputLabelString(int &x, int &y, const char *text, const char *hotkey, const string &label, 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); + display.append(": "); + OutputString(text_color, x, y, display); + OutputString(hotkey_color, x, y, label, 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) { @@ -93,9 +138,9 @@ void OutputToggleString(int &x, int &y, const char *text, const char *hotkey, bo OutputHotkeyString(x, y, text, hotkey); OutputString(COLOR_WHITE, x, y, ": "); if (state) - OutputString(COLOR_GREEN, x, y, "Enabled", newline, left_margin); + OutputString(COLOR_GREEN, x, y, "On", newline, left_margin); else - OutputString(COLOR_GREY, x, y, "Disabled", newline, left_margin); + OutputString(COLOR_GREY, x, y, "Off", newline, left_margin); } const int ascii_to_enum_offset = interface_key::STRING_A048 - '0'; @@ -113,6 +158,22 @@ static void set_to_limit(int &value, const int maximum, const int min = 0) value = maximum; } +// trim from start +static inline std::string <rim(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), std::not1(std::ptr_fun(std::isspace)))); + return s; +} + +// trim from end +static inline std::string &rtrim(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), std::not1(std::ptr_fun(std::isspace))).base(), s.end()); + return s; +} + +// trim from both ends +static inline std::string &trim(std::string &s) { + return ltrim(rtrim(s)); +} inline void paint_text(const UIColor color, const int &x, const int &y, const std::string &text, const UIColor background = 0) { @@ -145,6 +206,215 @@ static string pad_string(string text, const int size, const bool front = true, c } +/* + * Utility Functions + */ + +static df::building_stockpilest *get_selected_stockpile() +{ + if (!Gui::dwarfmode_hotkey(Core::getTopViewscreen()) || + df::global::ui->main.mode != ui_sidebar_mode::QueryBuilding) + { + return nullptr; + } + + return virtual_cast(df::global::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; +} + +static bool is_metal_item(df::item *item) +{ + MaterialInfo mat(item); + return (mat.getCraftClass() == craft_material_class::Metal); +} + +bool is_set_to_melt(df::item* item) +{ + return item->flags.bits.melt; +} + +// Copied from Kelly Martin's code +bool can_melt(df::item* item) +{ + + 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(in_job); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(artifact); F(melt); +#undef F + + if (item->flags.whole & bad_flags.whole) + return false; + + df::item_type t = item->getType(); + + if (t == df::enums::item_type::BOX || t == df::enums::item_type::BAR) + return false; + + if (!is_metal_item(item)) return false; + + for (auto g = item->general_refs.begin(); g != item->general_refs.end(); g++) + { + switch ((*g)->getType()) + { + case general_ref_type::CONTAINS_ITEM: + case general_ref_type::UNIT_HOLDER: + case general_ref_type::CONTAINS_UNIT: + return false; + case general_ref_type::CONTAINED_IN_ITEM: + { + df::item* c = (*g)->getItem(); + for (auto gg = c->general_refs.begin(); gg != c->general_refs.end(); gg++) + { + if ((*gg)->getType() == general_ref_type::UNIT_HOLDER) + return false; + } + } + break; + } + } + + if (item->getQuality() >= item_quality::Masterful) + return false; + + return true; +} + + +/* + * Stockpile Access + */ + +class StockpileInfo { +public: + StockpileInfo() : id(0), sp(nullptr) + { + } + + StockpileInfo(df::building_stockpilest *sp_) : sp(sp_) + { + readBuilding(); + } + + 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() + { + if (!id) + return false; + + auto found = df::building::find(id); + return found && found == sp && found->getType() == building_type::Stockpile; + } + + int32_t getId() + { + return id; + } + + bool matches(df::building_stockpilest* sp) + { + return this->sp == sp; + } + +protected: + int32_t id; + df::building_stockpilest* sp; + + void readBuilding() + { + if (!sp) + return; + + 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; + } + +private: + int x1, x2, y1, y2, z; +}; + + +class PersistentStockpileInfo : public StockpileInfo { +public: + PersistentStockpileInfo(df::building_stockpilest *sp, string persistence_key) : + StockpileInfo(sp), persistence_key(persistence_key) + { + } + + PersistentStockpileInfo(PersistentDataItem &config, string persistence_key) : + config(config), persistence_key(persistence_key) + { + id = config.ival(1); + } + + 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; + } + + void save() + { + config = DFHack::World::AddPersistentData(persistence_key); + config.ival(1) = id; + } + + void remove() + { + DFHack::World::DeletePersistentData(config); + } + +private: + PersistentDataItem config; + string persistence_key; +}; + + + /* * List classes */ @@ -155,9 +425,10 @@ public: T elem; string text, keywords; bool selected; + UIColor color; - ListEntry(const string text, const T elem, const string keywords = "") : - elem(elem), text(text), selected(false), keywords(keywords) + ListEntry(const string text, const T elem, const string keywords = "", const UIColor color = COLOR_UNSELECTED) : + elem(elem), text(text), selected(false), keywords(keywords), color(color) { } }; @@ -173,7 +444,6 @@ public: bool multiselect; bool allow_null; bool auto_select; - bool force_sort; bool allow_search; bool feed_changed_highlight; @@ -188,7 +458,6 @@ public: multiselect = false; allow_null = true; auto_select = false; - force_sort = false; allow_search = true; feed_changed_highlight = false; } @@ -198,6 +467,8 @@ public: list.clear(); display_list.clear(); display_start_offset = 0; + if (highlighted_index != -1) + highlighted_index = 0; max_item_width = title.length(); resize(); } @@ -231,6 +502,11 @@ public: it->text = pad_string(it->text, max_item_width, false); } + return getMaxItemWidth(); + } + + int getMaxItemWidth() + { return left_margin + max_item_width; } @@ -245,7 +521,7 @@ public: 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 fg_color = (display_list[i]->selected) ? COLOR_SELECTED : display_list[i]->color; UIColor bg_color = (is_selected_column && i == highlighted_index) ? COLOR_HIGHLIGHTED : COLOR_BLACK; string item_label = display_list[i]->text; @@ -324,6 +600,15 @@ public: } } + void centerSelection() + { + if (display_list.size() == 0) + return; + display_start_offset = highlighted_index - (display_max_rows / 2); + validateDisplayOffset(); + validateHighlight(); + } + void validateHighlight() { set_to_limit(highlighted_index, display_list.size() - 1); @@ -347,10 +632,15 @@ public: 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)); + validateDisplayOffset(); validateHighlight(); } + void validateDisplayOffset() + { + set_to_limit(display_start_offset, max(0, (int)(display_list.size())-display_max_rows)); + } + void setHighlight(const int index) { if (!initHighlightChange()) @@ -378,6 +668,9 @@ public: void toggleHighlighted() { + if (display_list.size() == 0) + return; + if (auto_select) return; @@ -505,6 +798,7 @@ public: // Standard character search_string += last_token - ascii_to_enum_offset; filterDisplay(); + centerSelection(); } else if (last_token == interface_key::STRING_A000) { @@ -513,6 +807,7 @@ public: { search_string.erase(search_string.length()-1); filterDisplay(); + centerSelection(); } } else @@ -547,7 +842,7 @@ public: return false; } - void sort() + void sort(bool force_sort = false) { if (force_sort || list.size() < 100) std::sort(list.begin(), list.end(), sort_fn);