summaryrefslogtreecommitdiff
path: root/plugins/manipulator.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/manipulator.cpp')
-rw-r--r--plugins/manipulator.cpp1194
1 files changed, 1194 insertions, 0 deletions
diff --git a/plugins/manipulator.cpp b/plugins/manipulator.cpp
new file mode 100644
index 00000000..953cf2ba
--- /dev/null
+++ b/plugins/manipulator.cpp
@@ -0,0 +1,1194 @@
+// Dwarf Manipulator - a Therapist-style labor editor
+
+#include "Core.h"
+#include <Console.h>
+#include <Export.h>
+#include <PluginManager.h>
+#include <MiscUtils.h>
+#include <modules/Screen.h>
+#include <modules/Translation.h>
+#include <modules/Units.h>
+#include <vector>
+#include <string>
+#include <set>
+#include <algorithm>
+
+#include <VTableInterpose.h>
+#include "df/world.h"
+#include "df/ui.h"
+#include "df/graphic.h"
+#include "df/enabler.h"
+#include "df/viewscreen_unitlistst.h"
+#include "df/interface_key.h"
+#include "df/unit.h"
+#include "df/unit_soul.h"
+#include "df/unit_skill.h"
+#include "df/creature_graphics_role.h"
+#include "df/creature_raw.h"
+#include "df/caste_raw.h"
+
+using std::set;
+using std::vector;
+using std::string;
+
+using namespace DFHack;
+using namespace df::enums;
+
+using df::global::world;
+using df::global::ui;
+using df::global::gps;
+using df::global::enabler;
+
+struct SkillLevel
+{
+ const char *name;
+ int points;
+ char abbrev;
+};
+
+#define NUM_SKILL_LEVELS (sizeof(skill_levels) / sizeof(SkillLevel))
+
+// The various skill rankings. Zero skill is hardcoded to "Not" and '-'.
+const SkillLevel skill_levels[] = {
+ {"Dabbling", 500, '0'},
+ {"Novice", 600, '1'},
+ {"Adequate", 700, '2'},
+ {"Competent", 800, '3'},
+ {"Skilled", 900, '4'},
+ {"Proficient", 1000, '5'},
+ {"Talented", 1100, '6'},
+ {"Adept", 1200, '7'},
+ {"Expert", 1300, '8'},
+ {"Professional",1400, '9'},
+ {"Accomplished",1500, 'A'},
+ {"Great", 1600, 'B'},
+ {"Master", 1700, 'C'},
+ {"High Master", 1800, 'D'},
+ {"Grand Master",1900, 'E'},
+ {"Legendary", 2000, 'U'},
+ {"Legendary+1", 2100, 'V'},
+ {"Legendary+2", 2200, 'W'},
+ {"Legendary+3", 2300, 'X'},
+ {"Legendary+4", 2400, 'Y'},
+ {"Legendary+5", 0, 'Z'}
+};
+
+struct SkillColumn
+{
+ int group; // for navigation and mass toggling
+ int8_t color; // for column headers
+ df::profession profession; // to display graphical tiles
+ df::unit_labor labor; // toggled when pressing Enter
+ df::job_skill skill; // displayed rating
+ char label[3]; // column header
+ bool special; // specified labor is mutually exclusive with all other special labors
+};
+
+#define NUM_COLUMNS (sizeof(columns) / sizeof(SkillColumn))
+
+// All of the skill/labor columns we want to display.
+const SkillColumn columns[] = {
+// Mining
+ {0, 7, profession::MINER, unit_labor::MINE, job_skill::MINING, "Mi", true},
+// Woodworking
+ {1, 14, profession::CARPENTER, unit_labor::CARPENTER, job_skill::CARPENTRY, "Ca"},
+ {1, 14, profession::BOWYER, unit_labor::BOWYER, job_skill::BOWYER, "Bw"},
+ {1, 14, profession::WOODCUTTER, unit_labor::CUTWOOD, job_skill::WOODCUTTING, "WC", true},
+// Stoneworking
+ {2, 15, profession::MASON, unit_labor::MASON, job_skill::MASONRY, "Ma"},
+ {2, 15, profession::ENGRAVER, unit_labor::DETAIL, job_skill::DETAILSTONE, "En"},
+// Hunting/Related
+ {3, 2, profession::ANIMAL_TRAINER, unit_labor::ANIMALTRAIN, job_skill::ANIMALTRAIN, "Tn"},
+ {3, 2, profession::ANIMAL_CARETAKER, unit_labor::ANIMALCARE, job_skill::ANIMALCARE, "Ca"},
+ {3, 2, profession::HUNTER, unit_labor::HUNT, job_skill::SNEAK, "Hu", true},
+ {3, 2, profession::TRAPPER, unit_labor::TRAPPER, job_skill::TRAPPING, "Tr"},
+ {3, 2, profession::ANIMAL_DISSECTOR, unit_labor::DISSECT_VERMIN, job_skill::DISSECT_VERMIN, "Di"},
+// Healthcare
+ {4, 5, profession::DIAGNOSER, unit_labor::DIAGNOSE, job_skill::DIAGNOSE, "Di"},
+ {4, 5, profession::SURGEON, unit_labor::SURGERY, job_skill::SURGERY, "Su"},
+ {4, 5, profession::BONE_SETTER, unit_labor::BONE_SETTING, job_skill::SET_BONE, "Bo"},
+ {4, 5, profession::SUTURER, unit_labor::SUTURING, job_skill::SUTURE, "St"},
+ {4, 5, profession::DOCTOR, unit_labor::DRESSING_WOUNDS, job_skill::DRESS_WOUNDS, "Dr"},
+ {4, 5, profession::NONE, unit_labor::FEED_WATER_CIVILIANS, job_skill::NONE, "Fd"},
+ {4, 5, profession::NONE, unit_labor::RECOVER_WOUNDED, job_skill::NONE, "Re"},
+// Farming/Related
+ {5, 6, profession::BUTCHER, unit_labor::BUTCHER, job_skill::BUTCHER, "Bu"},
+ {5, 6, profession::TANNER, unit_labor::TANNER, job_skill::TANNER, "Ta"},
+ {5, 6, profession::PLANTER, unit_labor::PLANT, job_skill::PLANT, "Gr"},
+ {5, 6, profession::DYER, unit_labor::DYER, job_skill::DYER, "Dy"},
+ {5, 6, profession::SOAP_MAKER, unit_labor::SOAP_MAKER, job_skill::SOAP_MAKING, "So"},
+ {5, 6, profession::WOOD_BURNER, unit_labor::BURN_WOOD, job_skill::WOOD_BURNING, "WB"},
+ {5, 6, profession::POTASH_MAKER, unit_labor::POTASH_MAKING, job_skill::POTASH_MAKING, "Po"},
+ {5, 6, profession::LYE_MAKER, unit_labor::LYE_MAKING, job_skill::LYE_MAKING, "Ly"},
+ {5, 6, profession::MILLER, unit_labor::MILLER, job_skill::MILLING, "Ml"},
+ {5, 6, profession::BREWER, unit_labor::BREWER, job_skill::BREWING, "Br"},
+ {5, 6, profession::HERBALIST, unit_labor::HERBALIST, job_skill::HERBALISM, "He"},
+ {5, 6, profession::THRESHER, unit_labor::PROCESS_PLANT, job_skill::PROCESSPLANTS, "Th"},
+ {5, 6, profession::CHEESE_MAKER, unit_labor::MAKE_CHEESE, job_skill::CHEESEMAKING, "Ch"},
+ {5, 6, profession::MILKER, unit_labor::MILK, job_skill::MILK, "Mk"},
+ {5, 6, profession::SHEARER, unit_labor::SHEARER, job_skill::SHEARING, "Sh"},
+ {5, 6, profession::SPINNER, unit_labor::SPINNER, job_skill::SPINNING, "Sp"},
+ {5, 6, profession::COOK, unit_labor::COOK, job_skill::COOK, "Co"},
+ {5, 6, profession::PRESSER, unit_labor::PRESSING, job_skill::PRESSING, "Pr"},
+ {5, 6, profession::BEEKEEPER, unit_labor::BEEKEEPING, job_skill::BEEKEEPING, "Be"},
+// Fishing/Related
+ {6, 1, profession::FISHERMAN, unit_labor::FISH, job_skill::FISH, "Fi"},
+ {6, 1, profession::FISH_CLEANER, unit_labor::CLEAN_FISH, job_skill::PROCESSFISH, "Cl"},
+ {6, 1, profession::FISH_DISSECTOR, unit_labor::DISSECT_FISH, job_skill::DISSECT_FISH, "Di"},
+// Metalsmithing
+ {7, 8, profession::FURNACE_OPERATOR, unit_labor::SMELT, job_skill::SMELT, "Fu"},
+ {7, 8, profession::WEAPONSMITH, unit_labor::FORGE_WEAPON, job_skill::FORGE_WEAPON, "We"},
+ {7, 8, profession::ARMORER, unit_labor::FORGE_ARMOR, job_skill::FORGE_ARMOR, "Ar"},
+ {7, 8, profession::BLACKSMITH, unit_labor::FORGE_FURNITURE, job_skill::FORGE_FURNITURE, "Bl"},
+ {7, 8, profession::METALCRAFTER, unit_labor::METAL_CRAFT, job_skill::METALCRAFT, "Cr"},
+// Jewelry
+ {8, 10, profession::GEM_CUTTER, unit_labor::CUT_GEM, job_skill::CUTGEM, "Cu"},
+ {8, 10, profession::GEM_SETTER, unit_labor::ENCRUST_GEM, job_skill::ENCRUSTGEM, "Se"},
+// Crafts
+ {9, 9, profession::LEATHERWORKER, unit_labor::LEATHER, job_skill::LEATHERWORK, "Le"},
+ {9, 9, profession::WOODCRAFTER, unit_labor::WOOD_CRAFT, job_skill::WOODCRAFT, "Wo"},
+ {9, 9, profession::STONECRAFTER, unit_labor::STONE_CRAFT, job_skill::STONECRAFT, "St"},
+ {9, 9, profession::BONE_CARVER, unit_labor::BONE_CARVE, job_skill::BONECARVE, "Bo"},
+ {9, 9, profession::GLASSMAKER, unit_labor::GLASSMAKER, job_skill::GLASSMAKER, "Gl"},
+ {9, 9, profession::WEAVER, unit_labor::WEAVER, job_skill::WEAVING, "We"},
+ {9, 9, profession::CLOTHIER, unit_labor::CLOTHESMAKER, job_skill::CLOTHESMAKING, "Cl"},
+ {9, 9, profession::STRAND_EXTRACTOR, unit_labor::EXTRACT_STRAND, job_skill::EXTRACT_STRAND, "Ad"},
+ {9, 9, profession::POTTER, unit_labor::POTTERY, job_skill::POTTERY, "Po"},
+ {9, 9, profession::GLAZER, unit_labor::GLAZING, job_skill::GLAZING, "Gl"},
+ {9, 9, profession::WAX_WORKER, unit_labor::WAX_WORKING, job_skill::WAX_WORKING, "Wx"},
+// Engineering
+ {10, 12, profession::SIEGE_ENGINEER, unit_labor::SIEGECRAFT, job_skill::SIEGECRAFT, "En"},
+ {10, 12, profession::SIEGE_OPERATOR, unit_labor::SIEGEOPERATE, job_skill::SIEGEOPERATE, "Op"},
+ {10, 12, profession::MECHANIC, unit_labor::MECHANIC, job_skill::MECHANICS, "Me"},
+ {10, 12, profession::PUMP_OPERATOR, unit_labor::OPERATE_PUMP, job_skill::OPERATE_PUMP, "Pu"},
+// Hauling
+ {11, 3, profession::NONE, unit_labor::HAUL_STONE, job_skill::NONE, "St"},
+ {11, 3, profession::NONE, unit_labor::HAUL_WOOD, job_skill::NONE, "Wo"},
+ {11, 3, profession::NONE, unit_labor::HAUL_ITEM, job_skill::NONE, "It"},
+ {11, 3, profession::NONE, unit_labor::HAUL_BODY, job_skill::NONE, "Bu"},
+ {11, 3, profession::NONE, unit_labor::HAUL_FOOD, job_skill::NONE, "Fo"},
+ {11, 3, profession::NONE, unit_labor::HAUL_REFUSE, job_skill::NONE, "Re"},
+ {11, 3, profession::NONE, unit_labor::HAUL_FURNITURE, job_skill::NONE, "Fu"},
+ {11, 3, profession::NONE, unit_labor::HAUL_ANIMAL, job_skill::NONE, "An"},
+ {11, 3, profession::NONE, unit_labor::PUSH_HAUL_VEHICLE, job_skill::NONE, "Ve"},
+// Other Jobs
+ {12, 4, profession::ARCHITECT, unit_labor::ARCHITECT, job_skill::DESIGNBUILDING, "Ar"},
+ {12, 4, profession::ALCHEMIST, unit_labor::ALCHEMIST, job_skill::ALCHEMY, "Al"},
+ {12, 4, profession::NONE, unit_labor::CLEAN, job_skill::NONE, "Cl"},
+// Military - Weapons
+ {13, 7, profession::WRESTLER, unit_labor::NONE, job_skill::WRESTLING, "Wr"},
+ {13, 7, profession::AXEMAN, unit_labor::NONE, job_skill::AXE, "Ax"},
+ {13, 7, profession::SWORDSMAN, unit_labor::NONE, job_skill::SWORD, "Sw"},
+ {13, 7, profession::MACEMAN, unit_labor::NONE, job_skill::MACE, "Mc"},
+ {13, 7, profession::HAMMERMAN, unit_labor::NONE, job_skill::HAMMER, "Ha"},
+ {13, 7, profession::SPEARMAN, unit_labor::NONE, job_skill::SPEAR, "Sp"},
+ {13, 7, profession::CROSSBOWMAN, unit_labor::NONE, job_skill::CROSSBOW, "Cb"},
+ {13, 7, profession::THIEF, unit_labor::NONE, job_skill::DAGGER, "Kn"},
+ {13, 7, profession::BOWMAN, unit_labor::NONE, job_skill::BOW, "Bo"},
+ {13, 7, profession::BLOWGUNMAN, unit_labor::NONE, job_skill::BLOWGUN, "Bl"},
+ {13, 7, profession::PIKEMAN, unit_labor::NONE, job_skill::PIKE, "Pk"},
+ {13, 7, profession::LASHER, unit_labor::NONE, job_skill::WHIP, "La"},
+// Military - Other Combat
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::BITE, "Bi"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::GRASP_STRIKE, "St"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::STANCE_STRIKE, "Ki"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::MISC_WEAPON, "Mi"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::MELEE_COMBAT, "Fg"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::RANGED_COMBAT, "Ac"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::ARMOR, "Ar"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::SHIELD, "Sh"},
+ {14, 15, profession::NONE, unit_labor::NONE, job_skill::DODGING, "Do"},
+// Military - Misc
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::LEADERSHIP, "Ld"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::TEACHING, "Te"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::KNOWLEDGE_ACQUISITION, "St"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::DISCIPLINE, "Di"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::CONCENTRATION, "Co"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::SITUATIONAL_AWARENESS, "Ob"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::COORDINATION, "Cr"},
+ {15, 8, profession::NONE, unit_labor::NONE, job_skill::BALANCE, "Ba"},
+// Social
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::PERSUASION, "Pe"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::NEGOTIATION, "Ne"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::JUDGING_INTENT, "Ju"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::LYING, "Li"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::INTIMIDATION, "In"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::CONVERSATION, "Cn"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::COMEDY, "Cm"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::FLATTERY, "Fl"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::CONSOLE, "Cs"},
+ {16, 3, profession::NONE, unit_labor::NONE, job_skill::PACIFY, "Pc"},
+// Noble
+ {17, 5, profession::TRADER, unit_labor::NONE, job_skill::APPRAISAL, "Ap"},
+ {17, 5, profession::ADMINISTRATOR, unit_labor::NONE, job_skill::ORGANIZATION, "Or"},
+ {17, 5, profession::CLERK, unit_labor::NONE, job_skill::RECORD_KEEPING, "RK"},
+// Miscellaneous
+ {18, 3, profession::NONE, unit_labor::NONE, job_skill::THROW, "Th"},
+ {18, 3, profession::NONE, unit_labor::NONE, job_skill::CRUTCH_WALK, "CW"},
+ {18, 3, profession::NONE, unit_labor::NONE, job_skill::SWIMMING, "Sw"},
+ {18, 3, profession::NONE, unit_labor::NONE, job_skill::KNAPPING, "Kn"},
+
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::WRITING, "Wr"},
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::PROSE, "Pr"},
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::POETRY, "Po"},
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::READING, "Rd"},
+ {19, 6, profession::NONE, unit_labor::NONE, job_skill::SPEAKING, "Sp"},
+
+ {20, 5, profession::NONE, unit_labor::NONE, job_skill::MILITARY_TACTICS, "MT"},
+ {20, 5, profession::NONE, unit_labor::NONE, job_skill::TRACKING, "Tr"},
+ {20, 5, profession::NONE, unit_labor::NONE, job_skill::MAGIC_NATURE, "Dr"},
+};
+
+struct UnitInfo
+{
+ df::unit *unit;
+ bool allowEdit;
+ string name;
+ string transname;
+ string profession;
+ int8_t color;
+ int active_index;
+};
+
+enum altsort_mode {
+ ALTSORT_NAME,
+ ALTSORT_PROFESSION,
+ ALTSORT_HAPPINESS,
+ ALTSORT_ARRIVAL,
+ ALTSORT_MAX
+};
+
+bool descending;
+df::job_skill sort_skill;
+df::unit_labor sort_labor;
+
+bool sortByName (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (descending)
+ return (d1->name > d2->name);
+ else
+ return (d1->name < d2->name);
+}
+
+bool sortByProfession (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (descending)
+ return (d1->profession > d2->profession);
+ else
+ return (d1->profession < d2->profession);
+}
+
+bool sortByHappiness (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (descending)
+ return (d1->unit->status.happiness > d2->unit->status.happiness);
+ else
+ return (d1->unit->status.happiness < d2->unit->status.happiness);
+}
+
+bool sortByArrival (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (descending)
+ return (d1->active_index > d2->active_index);
+ else
+ return (d1->active_index < d2->active_index);
+}
+
+bool sortBySkill (const UnitInfo *d1, const UnitInfo *d2)
+{
+ if (sort_skill != job_skill::NONE)
+ {
+ df::unit_skill *s1 = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(d1->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill);
+ df::unit_skill *s2 = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(d2->unit->status.current_soul->skills, &df::unit_skill::id, sort_skill);
+ int l1 = s1 ? s1->rating : 0;
+ int l2 = s2 ? s2->rating : 0;
+ int e1 = s1 ? s1->experience : 0;
+ int e2 = s2 ? s2->experience : 0;
+ if (descending)
+ {
+ if (l1 != l2)
+ return l1 > l2;
+ if (e1 != e2)
+ return e1 > e2;
+ }
+ else
+ {
+ if (l1 != l2)
+ return l1 < l2;
+ if (e1 != e2)
+ return e1 < e2;
+ }
+ }
+ if (sort_labor != unit_labor::NONE)
+ {
+ if (descending)
+ return d1->unit->status.labors[sort_labor] > d2->unit->status.labors[sort_labor];
+ else
+ return d1->unit->status.labors[sort_labor] < d2->unit->status.labors[sort_labor];
+ }
+ return sortByName(d1, d2);
+}
+
+enum display_columns {
+ DISP_COLUMN_HAPPINESS,
+ DISP_COLUMN_NAME,
+ DISP_COLUMN_PROFESSION,
+ DISP_COLUMN_LABORS,
+ DISP_COLUMN_MAX,
+};
+
+class viewscreen_unitlaborsst : public dfhack_viewscreen {
+public:
+ void feed(set<df::interface_key> *events);
+
+ void logic() {
+ dfhack_viewscreen::logic();
+ if (do_refresh_names)
+ refreshNames();
+ }
+
+ void render();
+ void resize(int w, int h) { calcSize(); }
+
+ void help() { }
+
+ std::string getFocusString() { return "unitlabors"; }
+
+ df::unit *getSelectedUnit();
+
+ viewscreen_unitlaborsst(vector<df::unit*> &src, int cursor_pos);
+ ~viewscreen_unitlaborsst() { };
+
+protected:
+ vector<UnitInfo *> units;
+ altsort_mode altsort;
+
+ bool do_refresh_names;
+ int first_row, sel_row, num_rows;
+ int first_column, sel_column;
+
+ int col_widths[DISP_COLUMN_MAX];
+ int col_offsets[DISP_COLUMN_MAX];
+
+ void refreshNames();
+ void calcSize ();
+};
+
+viewscreen_unitlaborsst::viewscreen_unitlaborsst(vector<df::unit*> &src, int cursor_pos)
+{
+ std::map<df::unit*,int> active_idx;
+ auto &active = world->units.active;
+ for (size_t i = 0; i < active.size(); i++)
+ active_idx[active[i]] = i;
+
+ for (size_t i = 0; i < src.size(); i++)
+ {
+ UnitInfo *cur = new UnitInfo;
+ df::unit *unit = src[i];
+ cur->unit = unit;
+ cur->allowEdit = true;
+ cur->active_index = active_idx[unit];
+
+ if (unit->race != ui->race_id)
+ cur->allowEdit = false;
+
+ if (unit->civ_id != ui->civ_id)
+ cur->allowEdit = false;
+
+ if (unit->flags1.bits.dead)
+ cur->allowEdit = false;
+
+ if (!ENUM_ATTR(profession, can_assign_labor, unit->profession))
+ cur->allowEdit = false;
+
+ cur->color = Units::getProfessionColor(unit);
+
+ units.push_back(cur);
+ }
+ altsort = ALTSORT_NAME;
+ first_column = sel_column = 0;
+
+ refreshNames();
+
+ first_row = 0;
+ sel_row = cursor_pos;
+ calcSize();
+
+ // recalculate first_row to roughly match the original layout
+ first_row = 0;
+ while (first_row < sel_row - num_rows + 1)
+ first_row += num_rows + 2;
+ // make sure the selection stays visible
+ if (first_row > sel_row)
+ first_row = sel_row - num_rows + 1;
+ // don't scroll beyond the end
+ if (first_row > units.size() - num_rows)
+ first_row = units.size() - num_rows;
+}
+
+void viewscreen_unitlaborsst::refreshNames()
+{
+ do_refresh_names = false;
+
+ for (size_t i = 0; i < units.size(); i++)
+ {
+ UnitInfo *cur = units[i];
+ df::unit *unit = cur->unit;
+
+ cur->name = Translation::TranslateName(&unit->name, false);
+ cur->transname = Translation::TranslateName(&unit->name, true);
+ cur->profession = Units::getProfessionName(unit);
+ }
+ calcSize();
+}
+
+void viewscreen_unitlaborsst::calcSize()
+{
+ num_rows = gps->dimy - 10;
+ if (num_rows > units.size())
+ num_rows = units.size();
+
+ int num_columns = gps->dimx - DISP_COLUMN_MAX - 1;
+
+ // min/max width of columns
+ int col_minwidth[DISP_COLUMN_MAX];
+ int col_maxwidth[DISP_COLUMN_MAX];
+ col_minwidth[DISP_COLUMN_HAPPINESS] = 4;
+ col_maxwidth[DISP_COLUMN_HAPPINESS] = 4;
+ col_minwidth[DISP_COLUMN_NAME] = 0;
+ col_maxwidth[DISP_COLUMN_NAME] = 0;
+ col_minwidth[DISP_COLUMN_PROFESSION] = 0;
+ col_maxwidth[DISP_COLUMN_PROFESSION] = 0;
+ col_minwidth[DISP_COLUMN_LABORS] = num_columns*3/5; // 60%
+ col_maxwidth[DISP_COLUMN_LABORS] = NUM_COLUMNS;
+
+ // get max_name/max_prof from strings length
+ for (size_t i = 0; i < units.size(); i++)
+ {
+ if (col_maxwidth[DISP_COLUMN_NAME] < units[i]->name.size())
+ col_maxwidth[DISP_COLUMN_NAME] = units[i]->name.size();
+ if (col_maxwidth[DISP_COLUMN_PROFESSION] < units[i]->profession.size())
+ col_maxwidth[DISP_COLUMN_PROFESSION] = units[i]->profession.size();
+ }
+
+ // check how much room we have
+ int width_min = 0, width_max = 0;
+ for (int i = 0; i < DISP_COLUMN_MAX; i++)
+ {
+ width_min += col_minwidth[i];
+ width_max += col_maxwidth[i];
+ }
+
+ if (width_max <= num_columns)
+ {
+ // lots of space, distribute leftover (except last column)
+ int col_margin = (num_columns - width_max) / (DISP_COLUMN_MAX-1);
+ int col_margin_r = (num_columns - width_max) % (DISP_COLUMN_MAX-1);
+ for (int i = DISP_COLUMN_MAX-1; i>=0; i--)
+ {
+ col_widths[i] = col_maxwidth[i];
+
+ if (i < DISP_COLUMN_MAX-1)
+ {
+ col_widths[i] += col_margin;
+
+ if (col_margin_r)
+ {
+ col_margin_r--;
+ col_widths[i]++;
+ }
+ }
+ }
+ }
+ else if (width_min <= num_columns)
+ {
+ // constrained, give between min and max to every column
+ int space = num_columns - width_min;
+ // max size columns not yet seen may consume
+ int next_consume_max = width_max - width_min;
+
+ for (int i = 0; i < DISP_COLUMN_MAX; i++)
+ {
+ // divide evenly remaining space
+ int col_margin = space / (DISP_COLUMN_MAX-i);
+
+ // take more if the columns after us cannot
+ next_consume_max -= (col_maxwidth[i]-col_minwidth[i]);
+ if (col_margin < space-next_consume_max)
+ col_margin = space - next_consume_max;
+
+ // no more than maxwidth
+ if (col_margin > col_maxwidth[i] - col_minwidth[i])
+ col_margin = col_maxwidth[i] - col_minwidth[i];
+
+ col_widths[i] = col_minwidth[i] + col_margin;
+
+ space -= col_margin;
+ }
+ }
+ else
+ {
+ // should not happen, min screen is 80x25
+ int space = num_columns;
+ for (int i = 0; i < DISP_COLUMN_MAX; i++)
+ {
+ col_widths[i] = space / (DISP_COLUMN_MAX-i);
+ space -= col_widths[i];
+ }
+ }
+
+ for (int i = 0; i < DISP_COLUMN_MAX; i++)
+ {
+ if (i == 0)
+ col_offsets[i] = 1;
+ else
+ col_offsets[i] = col_offsets[i - 1] + col_widths[i - 1] + 1;
+ }
+
+ // don't adjust scroll position immediately after the window opened
+ if (units.size() == 0)
+ return;
+
+ // if the window grows vertically, scroll upward to eliminate blank rows from the bottom
+ if (first_row > units.size() - num_rows)
+ first_row = units.size() - num_rows;
+
+ // if it shrinks vertically, scroll downward to keep the cursor visible
+ if (first_row < sel_row - num_rows + 1)
+ first_row = sel_row - num_rows + 1;
+
+ // if the window grows horizontally, scroll to the left to eliminate blank columns from the right
+ if (first_column > NUM_COLUMNS - col_widths[DISP_COLUMN_LABORS])
+ first_column = NUM_COLUMNS - col_widths[DISP_COLUMN_LABORS];
+
+ // if it shrinks horizontally, scroll to the right to keep the cursor visible
+ if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1)
+ first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1;
+}
+
+void viewscreen_unitlaborsst::feed(set<df::interface_key> *events)
+{
+ bool leave_all = events->count(interface_key::LEAVESCREEN_ALL);
+ if (leave_all || events->count(interface_key::LEAVESCREEN))
+ {
+ events->clear();
+ Screen::dismiss(this);
+ if (leave_all)
+ {
+ events->insert(interface_key::LEAVESCREEN);
+ parent->feed(events);
+ events->clear();
+ }
+ return;
+ }
+
+ if (!units.size())
+ return;
+
+ if (do_refresh_names)
+ refreshNames();
+
+ int old_sel_row = sel_row;
+
+ if (events->count(interface_key::CURSOR_UP) || events->count(interface_key::CURSOR_UPLEFT) || events->count(interface_key::CURSOR_UPRIGHT))
+ sel_row--;
+ if (events->count(interface_key::CURSOR_UP_FAST) || events->count(interface_key::CURSOR_UPLEFT_FAST) || events->count(interface_key::CURSOR_UPRIGHT_FAST))
+ sel_row -= 10;
+ if (events->count(interface_key::CURSOR_DOWN) || events->count(interface_key::CURSOR_DOWNLEFT) || events->count(interface_key::CURSOR_DOWNRIGHT))
+ sel_row++;
+ if (events->count(interface_key::CURSOR_DOWN_FAST) || events->count(interface_key::CURSOR_DOWNLEFT_FAST) || events->count(interface_key::CURSOR_DOWNRIGHT_FAST))
+ sel_row += 10;
+
+ if ((sel_row > 0) && events->count(interface_key::CURSOR_UP_Z_AUX))
+ {
+ sel_row = 0;
+ }
+ if ((sel_row < units.size()-1) && events->count(interface_key::CURSOR_DOWN_Z_AUX))
+ {
+ sel_row = units.size()-1;
+ }
+
+ if (sel_row < 0)
+ {
+ if (old_sel_row == 0 && events->count(interface_key::CURSOR_UP))
+ sel_row = units.size() - 1;
+ else
+ sel_row = 0;
+ }
+
+ if (sel_row > units.size() - 1)
+ {
+ if (old_sel_row == units.size()-1 && events->count(interface_key::CURSOR_DOWN))
+ sel_row = 0;
+ else
+ sel_row = units.size() - 1;
+ }
+
+ if (sel_row < first_row)
+ first_row = sel_row;
+ if (first_row < sel_row - num_rows + 1)
+ first_row = sel_row - num_rows + 1;
+
+ if (events->count(interface_key::CURSOR_LEFT) || events->count(interface_key::CURSOR_UPLEFT) || events->count(interface_key::CURSOR_DOWNLEFT))
+ sel_column--;
+ if (events->count(interface_key::CURSOR_LEFT_FAST) || events->count(interface_key::CURSOR_UPLEFT_FAST) || events->count(interface_key::CURSOR_DOWNLEFT_FAST))
+ sel_column -= 10;
+ if (events->count(interface_key::CURSOR_RIGHT) || events->count(interface_key::CURSOR_UPRIGHT) || events->count(interface_key::CURSOR_DOWNRIGHT))
+ sel_column++;
+ if (events->count(interface_key::CURSOR_RIGHT_FAST) || events->count(interface_key::CURSOR_UPRIGHT_FAST) || events->count(interface_key::CURSOR_DOWNRIGHT_FAST))
+ sel_column += 10;
+
+ if ((sel_column != 0) && events->count(interface_key::CURSOR_UP_Z))
+ {
+ // go to beginning of current column group; if already at the beginning, go to the beginning of the previous one
+ sel_column--;
+ int cur = columns[sel_column].group;
+ while ((sel_column > 0) && columns[sel_column - 1].group == cur)
+ sel_column--;
+ }
+ if ((sel_column != NUM_COLUMNS - 1) && events->count(interface_key::CURSOR_DOWN_Z))
+ {
+ // go to beginning of next group
+ int cur = columns[sel_column].group;
+ int next = sel_column+1;
+ while ((next < NUM_COLUMNS) && (columns[next].group == cur))
+ next++;
+ if ((next < NUM_COLUMNS) && (columns[next].group != cur))
+ sel_column = next;
+ }
+
+ if (sel_column < 0)
+ sel_column = 0;
+ if (sel_column > NUM_COLUMNS - 1)
+ sel_column = NUM_COLUMNS - 1;
+
+ if (events->count(interface_key::CURSOR_DOWN_Z) || events->count(interface_key::CURSOR_UP_Z))
+ {
+ // when moving by group, ensure the whole group is shown onscreen
+ int endgroup_column = sel_column;
+ while ((endgroup_column < NUM_COLUMNS-1) && columns[endgroup_column+1].group == columns[sel_column].group)
+ endgroup_column++;
+
+ if (first_column < endgroup_column - col_widths[DISP_COLUMN_LABORS] + 1)
+ first_column = endgroup_column - col_widths[DISP_COLUMN_LABORS] + 1;
+ }
+
+ if (sel_column < first_column)
+ first_column = sel_column;
+ if (first_column < sel_column - col_widths[DISP_COLUMN_LABORS] + 1)
+ first_column = sel_column - col_widths[DISP_COLUMN_LABORS] + 1;
+
+ int input_row = sel_row;
+ int input_column = sel_column;
+ int input_sort = altsort;
+
+ // Translate mouse input to appropriate keyboard input
+ if (enabler->tracking_on && gps->mouse_x != -1 && gps->mouse_y != -1)
+ {
+ int click_header = DISP_COLUMN_MAX; // group ID of the column header clicked
+ int click_body = DISP_COLUMN_MAX; // group ID of the column body clicked
+
+ int click_unit = -1; // Index into units[] (-1 if out of range)
+ int click_labor = -1; // Index into columns[] (-1 if out of range)
+
+ for (int i = 0; i < DISP_COLUMN_MAX; i++)
+ {
+ if ((gps->mouse_x >= col_offsets[i]) &&
+ (gps->mouse_x < col_offsets[i] + col_widths[i]))
+ {
+ if ((gps->mouse_y >= 1) && (gps->mouse_y <= 2))
+ click_header = i;
+ if ((gps->mouse_y >= 4) && (gps->mouse_y <= 4 + num_rows))
+ click_body = i;
+ }
+ }
+
+ if ((gps->mouse_x >= col_offsets[DISP_COLUMN_LABORS]) &&
+ (gps->mouse_x < col_offsets[DISP_COLUMN_LABORS] + col_widths[DISP_COLUMN_LABORS]))
+ click_labor = gps->mouse_x - col_offsets[DISP_COLUMN_LABORS] + first_column;
+ if ((gps->mouse_y >= 4) && (gps->mouse_y <= 4 + num_rows))
+ click_unit = gps->mouse_y - 4 + first_row;
+
+ switch (click_header)
+ {
+ case DISP_COLUMN_HAPPINESS:
+ if (enabler->mouse_lbut || enabler->mouse_rbut)
+ {
+ input_sort = ALTSORT_HAPPINESS;
+ if (enabler->mouse_lbut)
+ events->insert(interface_key::SECONDSCROLL_PAGEUP);
+ if (enabler->mouse_rbut)
+ events->insert(interface_key::SECONDSCROLL_PAGEDOWN);
+ }
+ break;
+
+ case DISP_COLUMN_NAME:
+ if (enabler->mouse_lbut || enabler->mouse_rbut)
+ {
+ input_sort = ALTSORT_NAME;
+ if (enabler->mouse_lbut)
+ events->insert(interface_key::SECONDSCROLL_PAGEDOWN);
+ if (enabler->mouse_rbut)
+ events->insert(interface_key::SECONDSCROLL_PAGEUP);
+ }
+ break;
+
+ case DISP_COLUMN_PROFESSION:
+ if (enabler->mouse_lbut || enabler->mouse_rbut)
+ {
+ input_sort = ALTSORT_PROFESSION;
+ if (enabler->mouse_lbut)
+ events->insert(interface_key::SECONDSCROLL_PAGEDOWN);
+ if (enabler->mouse_rbut)
+ events->insert(interface_key::SECONDSCROLL_PAGEUP);
+ }
+ break;
+
+ case DISP_COLUMN_LABORS:
+ if (enabler->mouse_lbut || enabler->mouse_rbut)
+ {
+ input_column = click_labor;
+ if (enabler->mouse_lbut)
+ events->insert(interface_key::SECONDSCROLL_UP);
+ if (enabler->mouse_rbut)
+ events->insert(interface_key::SECONDSCROLL_DOWN);
+ }
+ break;
+ }
+
+ switch (click_body)
+ {
+ case DISP_COLUMN_HAPPINESS:
+ // do nothing
+ break;
+
+ case DISP_COLUMN_NAME:
+ case DISP_COLUMN_PROFESSION:
+ // left-click to view, right-click to zoom
+ if (enabler->mouse_lbut)
+ {
+ input_row = click_unit;
+ events->insert(interface_key::UNITJOB_VIEW);
+ }
+ if (enabler->mouse_rbut)
+ {
+ input_row = click_unit;
+ events->insert(interface_key::UNITJOB_ZOOM_CRE);
+ }
+ break;
+
+ case DISP_COLUMN_LABORS:
+ // left-click to toggle, right-click to just highlight
+ if (enabler->mouse_lbut || enabler->mouse_rbut)
+ {
+ if (enabler->mouse_lbut)
+ {
+ input_row = click_unit;
+ input_column = click_labor;
+ events->insert(interface_key::SELECT);
+ }
+ if (enabler->mouse_rbut)
+ {
+ sel_row = click_unit;
+ sel_column = click_labor;
+ }
+ }
+ break;
+ }
+ enabler->mouse_lbut = enabler->mouse_rbut = 0;
+ }
+
+ UnitInfo *cur = units[input_row];
+ if (events->count(interface_key::SELECT) && (cur->allowEdit) && (columns[input_column].labor != unit_labor::NONE))
+ {
+ df::unit *unit = cur->unit;
+ const SkillColumn &col = columns[input_column];
+ bool newstatus = !unit->status.labors[col.labor];
+ if (col.special)
+ {
+ if (newstatus)
+ {
+ for (int i = 0; i < NUM_COLUMNS; i++)
+ {
+ if ((columns[i].labor != unit_labor::NONE) && columns[i].special)
+ unit->status.labors[columns[i].labor] = false;
+ }
+ }
+ unit->military.pickup_flags.bits.update = true;
+ }
+ unit->status.labors[col.labor] = newstatus;
+ }
+ if (events->count(interface_key::SELECT_ALL) && (cur->allowEdit))
+ {
+ df::unit *unit = cur->unit;
+ const SkillColumn &col = columns[input_column];
+ bool newstatus = !unit->status.labors[col.labor];
+ for (int i = 0; i < NUM_COLUMNS; i++)
+ {
+ if (columns[i].group != col.group)
+ continue;
+ if (columns[i].special)
+ {
+ if (newstatus)
+ {
+ for (int j = 0; j < NUM_COLUMNS; j++)
+ {
+ if ((columns[j].labor != unit_labor::NONE) && columns[j].special)
+ unit->status.labors[columns[j].labor] = false;
+ }
+ }
+ unit->military.pickup_flags.bits.update = true;
+ }
+ unit->status.labors[columns[i].labor] = newstatus;
+ }
+ }
+
+ if (events->count(interface_key::SECONDSCROLL_UP) || events->count(interface_key::SECONDSCROLL_DOWN))
+ {
+ descending = events->count(interface_key::SECONDSCROLL_UP);
+ sort_skill = columns[input_column].skill;
+ sort_labor = columns[input_column].labor;
+ std::sort(units.begin(), units.end(), sortBySkill);
+ }
+
+ if (events->count(interface_key::SECONDSCROLL_PAGEUP) || events->count(interface_key::SECONDSCROLL_PAGEDOWN))
+ {
+ descending = events->count(interface_key::SECONDSCROLL_PAGEUP);
+ switch (input_sort)
+ {
+ case ALTSORT_NAME:
+ std::sort(units.begin(), units.end(), sortByName);
+ break;
+ case ALTSORT_PROFESSION:
+ std::sort(units.begin(), units.end(), sortByProfession);
+ break;
+ case ALTSORT_HAPPINESS:
+ std::sort(units.begin(), units.end(), sortByHappiness);
+ break;
+ case ALTSORT_ARRIVAL:
+ std::sort(units.begin(), units.end(), sortByArrival);
+ break;
+ }
+ }
+ if (events->count(interface_key::CHANGETAB))
+ {
+ switch (altsort)
+ {
+ case ALTSORT_NAME:
+ altsort = ALTSORT_PROFESSION;
+ break;
+ case ALTSORT_PROFESSION:
+ altsort = ALTSORT_HAPPINESS;
+ break;
+ case ALTSORT_HAPPINESS:
+ altsort = ALTSORT_ARRIVAL;
+ break;
+ case ALTSORT_ARRIVAL:
+ altsort = ALTSORT_NAME;
+ break;
+ }
+ }
+
+ if (VIRTUAL_CAST_VAR(unitlist, df::viewscreen_unitlistst, parent))
+ {
+ if (events->count(interface_key::UNITJOB_VIEW) || events->count(interface_key::UNITJOB_ZOOM_CRE))
+ {
+ for (int i = 0; i < unitlist->units[unitlist->page].size(); i++)
+ {
+ if (unitlist->units[unitlist->page][i] == units[input_row]->unit)
+ {
+ unitlist->cursor_pos[unitlist->page] = i;
+ unitlist->feed(events);
+ if (Screen::isDismissed(unitlist))
+ Screen::dismiss(this);
+ else
+ do_refresh_names = true;
+ break;
+ }
+ }
+ }
+ }
+}
+
+void OutputString(int8_t color, int &x, int y, const std::string &text)
+{
+ Screen::paintString(Screen::Pen(' ', color, 0), x, y, text);
+ x += text.length();
+}
+void viewscreen_unitlaborsst::render()
+{
+ if (Screen::isDismissed(this))
+ return;
+
+ dfhack_viewscreen::render();
+
+ Screen::clear();
+ Screen::drawBorder(" Dwarf Manipulator - Manage Labors ");
+
+
+ Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_HAPPINESS], 2, "Hap.");
+ Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_NAME], 2, "Name");
+ Screen::paintString(Screen::Pen(' ', 7, 0), col_offsets[DISP_COLUMN_PROFESSION], 2, "Profession");
+
+ for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
+ {
+ int col_offset = col + first_column;
+ if (col_offset >= NUM_COLUMNS)
+ break;
+
+ int8_t fg = columns[col_offset].color;
+ int8_t bg = 0;
+
+ if (col_offset == sel_column)
+ {
+ fg = 0;
+ bg = 7;
+ }
+
+ Screen::paintTile(Screen::Pen(columns[col_offset].label[0], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 1);
+ Screen::paintTile(Screen::Pen(columns[col_offset].label[1], fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 2);
+ df::profession profession = columns[col_offset].profession;
+ if ((profession != profession::NONE) && (ui->race_id != -1))
+ {
+ auto graphics = world->raws.creatures.all[ui->race_id]->graphics;
+ Screen::paintTile(
+ Screen::Pen(' ', fg, 0,
+ graphics.profession_add_color[creature_graphics_role::DEFAULT][profession],
+ graphics.profession_texpos[creature_graphics_role::DEFAULT][profession]),
+ col_offsets[DISP_COLUMN_LABORS] + col, 3);
+ }
+ }
+
+ for (int row = 0; row < num_rows; row++)
+ {
+ int row_offset = row + first_row;
+ if (row_offset >= units.size())
+ break;
+
+ UnitInfo *cur = units[row_offset];
+ df::unit *unit = cur->unit;
+ int8_t fg = 15, bg = 0;
+
+ int happy = cur->unit->status.happiness;
+ string happiness = stl_sprintf("%4i", happy);
+ if (happy == 0) // miserable
+ fg = 13; // 5:1
+ else if (happy <= 25) // very unhappy
+ fg = 12; // 4:1
+ else if (happy <= 50) // unhappy
+ fg = 4; // 4:0
+ else if (happy < 75) // fine
+ fg = 14; // 6:1
+ else if (happy < 125) // quite content
+ fg = 6; // 6:0
+ else if (happy < 150) // happy
+ fg = 2; // 2:0
+ else // ecstatic
+ fg = 10; // 2:1
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_HAPPINESS], 4 + row, happiness);
+
+ fg = 15;
+ if (row_offset == sel_row)
+ {
+ fg = 0;
+ bg = 7;
+ }
+
+ string name = cur->name;
+ name.resize(col_widths[DISP_COLUMN_NAME]);
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_NAME], 4 + row, name);
+
+ string profession = cur->profession;
+ profession.resize(col_widths[DISP_COLUMN_PROFESSION]);
+ fg = cur->color;
+ bg = 0;
+
+ Screen::paintString(Screen::Pen(' ', fg, bg), col_offsets[DISP_COLUMN_PROFESSION], 4 + row, profession);
+
+ // Print unit's skills and labor assignments
+ for (int col = 0; col < col_widths[DISP_COLUMN_LABORS]; col++)
+ {
+ int col_offset = col + first_column;
+ fg = 15;
+ bg = 0;
+ uint8_t c = 0xFA;
+ if ((col_offset == sel_column) && (row_offset == sel_row))
+ fg = 9;
+ if (columns[col_offset].skill != job_skill::NONE)
+ {
+ df::unit_skill *skill = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(unit->status.current_soul->skills, &df::unit_skill::id, columns[col_offset].skill);
+ if ((skill != NULL) && (skill->rating || skill->experience))
+ {
+ int level = skill->rating;
+ if (level > NUM_SKILL_LEVELS - 1)
+ level = NUM_SKILL_LEVELS - 1;
+ c = skill_levels[level].abbrev;
+ }
+ else
+ c = '-';
+ }
+ if (columns[col_offset].labor != unit_labor::NONE)
+ {
+ if (unit->status.labors[columns[col_offset].labor])
+ {
+ bg = 7;
+ if (columns[col_offset].skill == job_skill::NONE)
+ c = 0xF9;
+ }
+ }
+ else
+ bg = 4;
+ Screen::paintTile(Screen::Pen(c, fg, bg), col_offsets[DISP_COLUMN_LABORS] + col, 4 + row);
+ }
+ }
+
+ UnitInfo *cur = units[sel_row];
+ bool canToggle = false;
+ if (cur != NULL)
+ {
+ df::unit *unit = cur->unit;
+ int x = 1;
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->transname);
+ x += cur->transname.length();
+
+ if (cur->transname.length())
+ {
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ", ");
+ x += 2;
+ }
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, cur->profession);
+ x += cur->profession.length();
+ Screen::paintString(Screen::Pen(' ', 15, 0), x, 3 + num_rows + 2, ": ");
+ x += 2;
+
+ string str;
+ if (columns[sel_column].skill == job_skill::NONE)
+ {
+ str = ENUM_ATTR_STR(unit_labor, caption, columns[sel_column].labor);
+ if (unit->status.labors[columns[sel_column].labor])
+ str += " Enabled";
+ else
+ str += " Not Enabled";
+ }
+ else
+ {
+ df::unit_skill *skill = binsearch_in_vector<df::unit_skill,df::enum_field<df::job_skill,int16_t>>(unit->status.current_soul->skills, &df::unit_skill::id, columns[sel_column].skill);
+ if (skill)
+ {
+ int level = skill->rating;
+ if (level > NUM_SKILL_LEVELS - 1)
+ level = NUM_SKILL_LEVELS - 1;
+ str = stl_sprintf("%s %s", skill_levels[level].name, ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill));
+ if (level != NUM_SKILL_LEVELS - 1)
+ str += stl_sprintf(" (%d/%d)", skill->experience, skill_levels[level].points);
+ }
+ else
+ str = stl_sprintf("Not %s (0/500)", ENUM_ATTR_STR(job_skill, caption_noun, columns[sel_column].skill));
+ }
+ Screen::paintString(Screen::Pen(' ', 9, 0), x, 3 + num_rows + 2, str);
+
+ canToggle = (cur->allowEdit) && (columns[sel_column].labor != unit_labor::NONE);
+ }
+
+ int x = 2;
+ OutputString(10, x, gps->dimy - 3, "Enter"); // SELECT key
+ OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle labor, ");
+
+ OutputString(10, x, gps->dimy - 3, "Shift+Enter"); // SELECT_ALL key
+ OutputString(canToggle ? 15 : 8, x, gps->dimy - 3, ": Toggle Group, ");
+
+ OutputString(10, x, gps->dimy - 3, "v"); // UNITJOB_VIEW key
+ OutputString(15, x, gps->dimy - 3, ": ViewCre, ");
+
+ OutputString(10, x, gps->dimy - 3, "c"); // UNITJOB_ZOOM_CRE key
+ OutputString(15, x, gps->dimy - 3, ": Zoom-Cre");
+
+ x = 2;
+ OutputString(10, x, gps->dimy - 2, "Esc"); // LEAVESCREEN key
+ OutputString(15, x, gps->dimy - 2, ": Done, ");
+
+ OutputString(10, x, gps->dimy - 2, "+"); // SECONDSCROLL_DOWN key
+ OutputString(10, x, gps->dimy - 2, "-"); // SECONDSCROLL_UP key
+ OutputString(15, x, gps->dimy - 2, ": Sort by Skill, ");
+
+ OutputString(10, x, gps->dimy - 2, "*"); // SECONDSCROLL_PAGEDOWN key
+ OutputString(10, x, gps->dimy - 2, "/"); // SECONDSCROLL_PAGEUP key
+ OutputString(15, x, gps->dimy - 2, ": Sort by (");
+ OutputString(10, x, gps->dimy - 2, "Tab"); // CHANGETAB key
+ OutputString(15, x, gps->dimy - 2, ") ");
+ switch (altsort)
+ {
+ case ALTSORT_NAME:
+ OutputString(15, x, gps->dimy - 2, "Name");
+ break;
+ case ALTSORT_PROFESSION:
+ OutputString(15, x, gps->dimy - 2, "Profession");
+ break;
+ case ALTSORT_HAPPINESS:
+ OutputString(15, x, gps->dimy - 2, "Happiness");
+ break;
+ case ALTSORT_ARRIVAL:
+ OutputString(15, x, gps->dimy - 2, "Arrival");
+ break;
+ default:
+ OutputString(15, x, gps->dimy - 2, "Unknown");
+ break;
+ }
+}
+
+df::unit *viewscreen_unitlaborsst::getSelectedUnit()
+{
+ // This query might be from the rename plugin
+ do_refresh_names = true;
+
+ return units[sel_row]->unit;
+}
+
+struct unitlist_hook : df::viewscreen_unitlistst
+{
+ typedef df::viewscreen_unitlistst interpose_base;
+
+ DEFINE_VMETHOD_INTERPOSE(void, feed, (set<df::interface_key> *input))
+ {
+ if (input->count(interface_key::UNITVIEW_PRF_PROF))
+ {
+ if (units[page].size())
+ {
+ Screen::show(new viewscreen_unitlaborsst(units[page], cursor_pos[page]));
+ return;
+ }
+ }
+ INTERPOSE_NEXT(feed)(input);
+ }
+
+ DEFINE_VMETHOD_INTERPOSE(void, render, ())
+ {
+ INTERPOSE_NEXT(render)();
+
+ if (units[page].size())
+ {
+ int x = 2;
+ OutputString(12, x, gps->dimy - 2, "l"); // UNITVIEW_PRF_PROF key
+ OutputString(15, x, gps->dimy - 2, ": Manage labors (DFHack)");
+ }
+ }
+};
+
+IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, feed);
+IMPLEMENT_VMETHOD_INTERPOSE(unitlist_hook, render);
+
+DFHACK_PLUGIN("manipulator");
+
+DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
+{
+ if (!gps || !INTERPOSE_HOOK(unitlist_hook, feed).apply() || !INTERPOSE_HOOK(unitlist_hook, render).apply())
+ out.printerr("Could not insert Dwarf Manipulator hooks!\n");
+ return CR_OK;
+}
+
+DFhackCExport command_result plugin_shutdown ( color_ostream &out )
+{
+ INTERPOSE_HOOK(unitlist_hook, feed).remove();
+ INTERPOSE_HOOK(unitlist_hook, render).remove();
+ return CR_OK;
+}