diff --git a/NEWS b/NEWS index 961708dc3..2b6470787 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,7 @@ DFHack future - 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. Siege engine plugin: - engine quality and distance to target now affect accuracy 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;