2068 lines
63 KiB
C++
2068 lines
63 KiB
C++
#include "uicommon.h"
|
|
#include "listcolumn.h"
|
|
|
|
#include "DataDefs.h"
|
|
|
|
#include "df/job.h"
|
|
#include "df/ui.h"
|
|
#include "df/unit.h"
|
|
#include "df/viewscreen_dwarfmodest.h"
|
|
#include "df/world.h"
|
|
#include "df/misc_trait_type.h"
|
|
#include "df/unit_misc_trait.h"
|
|
|
|
#include "LuaTools.h"
|
|
#include "LuaWrapper.h"
|
|
#include "modules/Gui.h"
|
|
#include "modules/Units.h"
|
|
#include "modules/Translation.h"
|
|
#include "modules/World.h"
|
|
#include "modules/Maps.h"
|
|
|
|
#include "df/activity_entry.h"
|
|
#include "df/activity_event.h"
|
|
#include "df/creature_raw.h"
|
|
#include "df/dance_form.h"
|
|
#include "df/descriptor_color.h"
|
|
#include "df/descriptor_shape.h"
|
|
#include "df/item_type.h"
|
|
#include "df/itemdef_ammost.h"
|
|
#include "df/itemdef_armorst.h"
|
|
#include "df/itemdef_foodst.h"
|
|
#include "df/itemdef_glovesst.h"
|
|
#include "df/itemdef_helmst.h"
|
|
#include "df/itemdef_instrumentst.h"
|
|
#include "df/itemdef_pantsst.h"
|
|
#include "df/itemdef_shieldst.h"
|
|
#include "df/itemdef_shoesst.h"
|
|
#include "df/itemdef_siegeammost.h"
|
|
#include "df/itemdef_toolst.h"
|
|
#include "df/itemdef_toyst.h"
|
|
#include "df/itemdef_trapcompst.h"
|
|
#include "df/itemdef_weaponst.h"
|
|
#include "df/musical_form.h"
|
|
#include "df/poetic_form.h"
|
|
#include "df/trapcomp_flags.h"
|
|
#include "df/unit_preference.h"
|
|
#include "df/unit_soul.h"
|
|
#include "df/viewscreen_unitst.h"
|
|
#include "df/world_raws.h"
|
|
|
|
using std::deque;
|
|
|
|
DFHACK_PLUGIN("dwarfmonitor");
|
|
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
|
|
REQUIRE_GLOBAL(current_weather);
|
|
REQUIRE_GLOBAL(world);
|
|
REQUIRE_GLOBAL(ui);
|
|
|
|
typedef int16_t activity_type;
|
|
|
|
#define PLUGIN_VERSION 0.9
|
|
#define DAY_TICKS 1200
|
|
#define DELTA_TICKS 100
|
|
|
|
const int min_window = 28;
|
|
const int max_history_days = 3 * min_window;
|
|
const int ticks_per_day = DAY_TICKS / DELTA_TICKS;
|
|
|
|
template <typename T1, typename T2>
|
|
struct less_second {
|
|
typedef pair<T1, T2> type;
|
|
bool operator ()(type const& a, type const& b) const {
|
|
return a.second > b.second;
|
|
}
|
|
};
|
|
|
|
struct dwarfmonitor_configst {
|
|
std::string date_format;
|
|
};
|
|
static dwarfmonitor_configst dwarfmonitor_config;
|
|
|
|
static bool monitor_jobs = false;
|
|
static bool monitor_misery = true;
|
|
static bool monitor_date = true;
|
|
static bool monitor_weather = true;
|
|
static map<df::unit *, deque<activity_type>> 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)
|
|
{
|
|
if (!unit || !unit->status.current_soul)
|
|
return 3;
|
|
int stress = unit->status.current_soul->personality.stress_level;
|
|
if (stress >= 500000)
|
|
return 0;
|
|
else if (stress >= 250000)
|
|
return 1;
|
|
else if (stress >= 100000)
|
|
return 2;
|
|
else if (stress >= 60000)
|
|
return 3;
|
|
else if (stress >= 30000)
|
|
return 4;
|
|
else if (stress >= 0)
|
|
return 5;
|
|
else
|
|
return 6;
|
|
}
|
|
|
|
static int get_max_history()
|
|
{
|
|
return ticks_per_day * max_history_days;
|
|
}
|
|
|
|
static int getPercentage(const int n, const int d)
|
|
{
|
|
return static_cast<int>(
|
|
static_cast<float>(n) / static_cast<float>(d) * 100.0);
|
|
}
|
|
|
|
static string getUnitName(df::unit * unit)
|
|
{
|
|
string label = "";
|
|
auto name = Units::getVisibleName(unit);
|
|
if (name->has_name)
|
|
label = Translation::TranslateName(name, false);
|
|
|
|
return label;
|
|
}
|
|
|
|
template<typename T>
|
|
static string getFormName(int32_t id, const string &default_ = "?") {
|
|
T *form = T::find(id);
|
|
if (form)
|
|
return Translation::TranslateName(&form->name);
|
|
return default_;
|
|
}
|
|
|
|
static void send_key(const df::interface_key &key)
|
|
{
|
|
set< df::interface_key > keys;
|
|
keys.insert(key);
|
|
Gui::getCurViewscreen(true)->feed(&keys);
|
|
}
|
|
|
|
static void move_cursor(df::coord &pos)
|
|
{
|
|
Gui::setCursorCoords(pos.x, pos.y, pos.z);
|
|
Gui::refreshSidebar();
|
|
}
|
|
|
|
static void open_stats_screen();
|
|
|
|
namespace dm_lua {
|
|
static color_ostream_proxy *out;
|
|
static lua_State *state;
|
|
typedef int(*initializer)(lua_State*);
|
|
int no_args (lua_State *L) { return 0; }
|
|
void cleanup()
|
|
{
|
|
if (out)
|
|
{
|
|
delete out;
|
|
out = NULL;
|
|
}
|
|
lua_close(state);
|
|
}
|
|
bool init_call (const char *func)
|
|
{
|
|
if (!out)
|
|
out = new color_ostream_proxy(Core::getInstance().getConsole());
|
|
return Lua::PushModulePublic(*out, state, "plugins.dwarfmonitor", func);
|
|
}
|
|
bool safe_call (int nargs)
|
|
{
|
|
return Lua::SafeCall(*out, state, nargs, 0);
|
|
}
|
|
|
|
bool call (const char *func, initializer init = no_args)
|
|
{
|
|
Lua::StackUnwinder top(state);
|
|
if (!init_call(func))
|
|
return false;
|
|
int nargs = init(state);
|
|
return safe_call(nargs);
|
|
}
|
|
|
|
namespace api {
|
|
int monitor_state (lua_State *L)
|
|
{
|
|
std::string type = luaL_checkstring(L, 1);
|
|
if (type == "weather")
|
|
lua_pushboolean(L, monitor_weather);
|
|
else if (type == "misery")
|
|
lua_pushboolean(L, monitor_misery);
|
|
else if (type == "date")
|
|
lua_pushboolean(L, monitor_date);
|
|
else
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
int get_weather_counts (lua_State *L)
|
|
{
|
|
#define WEATHER_TYPES WTYPE(clear, None); WTYPE(rain, Rain); WTYPE(snow, Snow);
|
|
#define WTYPE(type, name) int type = 0;
|
|
WEATHER_TYPES
|
|
#undef WTYPE
|
|
int i, j;
|
|
for (i = 0; i < 5; ++i)
|
|
{
|
|
for (j = 0; j < 5; ++j)
|
|
{
|
|
switch ((*current_weather)[i][j])
|
|
{
|
|
#define WTYPE(type, name) case weather_type::name: type++; break;
|
|
WEATHER_TYPES
|
|
#undef WTYPE
|
|
}
|
|
}
|
|
}
|
|
lua_newtable(L);
|
|
#define WTYPE(type, name) Lua::TableInsert(L, #type, type);
|
|
WEATHER_TYPES
|
|
#undef WTYPE
|
|
#undef WEATHER_TYPES
|
|
return 1;
|
|
}
|
|
int get_misery_data (lua_State *L)
|
|
{
|
|
lua_newtable(L);
|
|
for (int i = 0; i < 7; i++)
|
|
{
|
|
Lua::Push(L, i);
|
|
lua_newtable(L);
|
|
Lua::TableInsert(L, "value", misery[i]);
|
|
Lua::TableInsert(L, "color", monitor_colors[i]);
|
|
Lua::TableInsert(L, "last", (i == 6));
|
|
lua_settable(L, -3);
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
#define DM_LUA_FUNC(name) { #name, df::wrap_function(dm_lua::api::name, true) }
|
|
#define DM_LUA_CMD(name) { #name, dm_lua::api::name }
|
|
DFHACK_PLUGIN_LUA_COMMANDS {
|
|
DM_LUA_CMD(monitor_state),
|
|
DM_LUA_CMD(get_weather_counts),
|
|
DM_LUA_CMD(get_misery_data),
|
|
DFHACK_LUA_END
|
|
};
|
|
|
|
#define JOB_IDLE -1
|
|
#define JOB_UNKNOWN -2
|
|
#define JOB_MILITARY -3
|
|
#define JOB_LEISURE -4
|
|
#define JOB_UNPRODUCTIVE -5
|
|
#define JOB_DESIGNATE -6
|
|
#define JOB_STORE_ITEM -7
|
|
#define JOB_MANUFACTURE -8
|
|
#define JOB_DETAILING -9
|
|
#define JOB_HUNTING -10
|
|
#define JOB_MEDICAL -14
|
|
#define JOB_COLLECT -15
|
|
#define JOB_CONSTRUCTION -16
|
|
#define JOB_AGRICULTURE -17
|
|
#define JOB_FOOD_PROD -18
|
|
#define JOB_MECHANICAL -19
|
|
#define JOB_ANIMALS -20
|
|
#define JOB_PRODUCTIVE -21
|
|
|
|
static map<activity_type, string> activity_labels;
|
|
|
|
static string getActivityLabel(const activity_type activity)
|
|
{
|
|
string label;
|
|
|
|
if (activity_labels.find(activity) != activity_labels.end())
|
|
{
|
|
label = activity_labels[activity];
|
|
}
|
|
else
|
|
{
|
|
string raw_label = enum_item_key_str(static_cast<df::job_type>(activity));
|
|
for (auto c = raw_label.begin(); c != raw_label.end(); c++)
|
|
{
|
|
if (label.length() > 0 && *c >= 'A' && *c <= 'Z')
|
|
label += ' ';
|
|
|
|
label += *c;
|
|
}
|
|
}
|
|
|
|
return label;
|
|
}
|
|
|
|
|
|
class ViewscreenDwarfStats : public dfhack_viewscreen
|
|
{
|
|
public:
|
|
ViewscreenDwarfStats(df::unit *starting_selection) : selected_column(0)
|
|
{
|
|
dwarves_column.multiselect = false;
|
|
dwarves_column.auto_select = true;
|
|
dwarves_column.setTitle("Dwarves");
|
|
|
|
dwarf_activity_column.multiselect = false;
|
|
dwarf_activity_column.auto_select = true;
|
|
dwarf_activity_column.setTitle("Dwarf Activity");
|
|
|
|
window_days = min_window;
|
|
|
|
populateDwarfColumn(starting_selection);
|
|
}
|
|
|
|
void populateDwarfColumn(df::unit *starting_selection = NULL)
|
|
{
|
|
selected_column = 0;
|
|
|
|
auto last_selected_index = dwarf_activity_column.highlighted_index;
|
|
dwarves_column.clear();
|
|
dwarf_activity_values.clear();
|
|
|
|
for (auto it = work_history.begin(); it != work_history.end();)
|
|
{
|
|
auto unit = it->first;
|
|
if (Units::isDead(unit))
|
|
{
|
|
work_history.erase(it++);
|
|
continue;
|
|
}
|
|
|
|
deque<activity_type> *work_list = &it->second;
|
|
++it;
|
|
|
|
size_t dwarf_total = 0;
|
|
dwarf_activity_values[unit] = map<activity_type, size_t>();
|
|
size_t count = window_days * ticks_per_day;
|
|
for (auto entry = work_list->rbegin(); entry != work_list->rend() && count > 0; entry++, count--)
|
|
{
|
|
if (*entry == JOB_UNKNOWN || *entry == job_type::DrinkBlood)
|
|
continue;
|
|
|
|
++dwarf_total;
|
|
addDwarfActivity(unit, *entry);
|
|
}
|
|
|
|
auto &values = dwarf_activity_values[unit];
|
|
for (auto it = values.begin(); it != values.end(); ++it)
|
|
it->second = getPercentage(it->second, dwarf_total);
|
|
|
|
dwarves_column.add(getUnitName(unit), unit);
|
|
}
|
|
|
|
dwarf_activity_column.left_margin = dwarves_column.fixWidth() + 2;
|
|
dwarves_column.filterDisplay();
|
|
if (starting_selection)
|
|
dwarves_column.selectItem(starting_selection);
|
|
else
|
|
dwarves_column.setHighlight(last_selected_index);
|
|
|
|
populateActivityColumn();
|
|
}
|
|
|
|
void populateActivityColumn()
|
|
{
|
|
dwarf_activity_column.clear();
|
|
if (dwarves_column.getDisplayedListSize() == 0)
|
|
return;
|
|
|
|
auto unit = dwarves_column.getFirstSelectedElem();
|
|
if (dwarf_activity_values.find(unit) == dwarf_activity_values.end())
|
|
return;
|
|
|
|
auto dwarf_activities = &dwarf_activity_values[unit];
|
|
if (dwarf_activities)
|
|
{
|
|
vector<pair<activity_type, size_t>> rev_vec(dwarf_activities->begin(), dwarf_activities->end());
|
|
sort(rev_vec.begin(), rev_vec.end(), less_second<activity_type, size_t>());
|
|
|
|
for (auto it = rev_vec.begin(); it != rev_vec.end(); ++it)
|
|
dwarf_activity_column.add(getActivityItem(it->first, it->second), it->first);
|
|
}
|
|
|
|
dwarf_activity_column.fixWidth();
|
|
dwarf_activity_column.clearSearch();
|
|
dwarf_activity_column.setHighlight(0);
|
|
}
|
|
|
|
void addDwarfActivity(df::unit *unit, const activity_type &activity)
|
|
{
|
|
if (dwarf_activity_values[unit].find(activity) == dwarf_activity_values[unit].end())
|
|
dwarf_activity_values[unit][activity] = 0;
|
|
|
|
dwarf_activity_values[unit][activity]++;
|
|
}
|
|
|
|
string getActivityItem(activity_type activity, size_t value)
|
|
{
|
|
return pad_string(int_to_string(value), 3) + " " + getActivityLabel(activity);
|
|
}
|
|
|
|
void feed(set<df::interface_key> *input)
|
|
{
|
|
bool key_processed = false;
|
|
switch (selected_column)
|
|
{
|
|
case 0:
|
|
key_processed = dwarves_column.feed(input);
|
|
break;
|
|
case 1:
|
|
key_processed = dwarf_activity_column.feed(input);
|
|
break;
|
|
}
|
|
|
|
if (key_processed)
|
|
{
|
|
if (selected_column == 0 && dwarves_column.feed_changed_highlight)
|
|
populateActivityColumn();
|
|
|
|
return;
|
|
}
|
|
|
|
if (input->count(interface_key::LEAVESCREEN))
|
|
{
|
|
input->clear();
|
|
Screen::dismiss(this);
|
|
return;
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_D))
|
|
{
|
|
Screen::dismiss(this);
|
|
open_stats_screen();
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_Z))
|
|
{
|
|
df::unit *selected_unit = (selected_column == 0) ? dwarves_column.getFirstSelectedElem() : nullptr;
|
|
if (selected_unit)
|
|
{
|
|
input->clear();
|
|
Screen::dismiss(this);
|
|
Gui::resetDwarfmodeView(true);
|
|
send_key(interface_key::D_VIEWUNIT);
|
|
move_cursor(selected_unit->pos);
|
|
}
|
|
}
|
|
else if (input->count(interface_key::SECONDSCROLL_PAGEDOWN))
|
|
{
|
|
window_days += min_window;
|
|
if (window_days > max_history_days)
|
|
window_days = min_window;
|
|
|
|
populateDwarfColumn();
|
|
}
|
|
else if (input->count(interface_key::CURSOR_LEFT))
|
|
{
|
|
--selected_column;
|
|
validateColumn();
|
|
}
|
|
else if (input->count(interface_key::CURSOR_RIGHT))
|
|
{
|
|
++selected_column;
|
|
validateColumn();
|
|
}
|
|
else if (enabler->tracking_on && enabler->mouse_lbut)
|
|
{
|
|
if (dwarves_column.setHighlightByMouse())
|
|
{
|
|
selected_column = 0;
|
|
populateActivityColumn();
|
|
}
|
|
else if (dwarf_activity_column.setHighlightByMouse())
|
|
selected_column = 1;
|
|
|
|
enabler->mouse_lbut = enabler->mouse_rbut = 0;
|
|
}
|
|
}
|
|
|
|
void render()
|
|
{
|
|
using namespace df::enums::interface_key;
|
|
|
|
if (Screen::isDismissed(this))
|
|
return;
|
|
|
|
dfhack_viewscreen::render();
|
|
|
|
Screen::clear();
|
|
Screen::drawBorder(" Dwarf Activity ");
|
|
|
|
dwarves_column.display(selected_column == 0);
|
|
dwarf_activity_column.display(selected_column == 1);
|
|
|
|
int32_t y = gps->dimy - 4;
|
|
int32_t x = 2;
|
|
OutputHotkeyString(x, y, "Leave", LEAVESCREEN);
|
|
|
|
x += 13;
|
|
string window_label = "Window Months: " + int_to_string(window_days / min_window);
|
|
OutputHotkeyString(x, y, window_label.c_str(), SECONDSCROLL_PAGEDOWN);
|
|
|
|
++y;
|
|
x = 2;
|
|
OutputHotkeyString(x, y, "Fort Stats", CUSTOM_SHIFT_D);
|
|
|
|
x += 3;
|
|
OutputHotkeyString(x, y, "Zoom Unit", CUSTOM_SHIFT_Z);
|
|
}
|
|
|
|
std::string getFocusString() { return "dwarfmonitor_dwarfstats"; }
|
|
|
|
private:
|
|
ListColumn<df::unit *> dwarves_column;
|
|
ListColumn<activity_type> dwarf_activity_column;
|
|
int selected_column;
|
|
size_t window_days;
|
|
|
|
map<df::unit *, map<activity_type, size_t>> dwarf_activity_values;
|
|
|
|
void validateColumn()
|
|
{
|
|
set_to_limit(selected_column, 1);
|
|
}
|
|
|
|
void resize(int32_t x, int32_t y)
|
|
{
|
|
dfhack_viewscreen::resize(x, y);
|
|
dwarves_column.resize();
|
|
dwarf_activity_column.resize();
|
|
}
|
|
};
|
|
|
|
|
|
class ViewscreenFortStats : public dfhack_viewscreen
|
|
{
|
|
public:
|
|
ViewscreenFortStats()
|
|
{
|
|
fort_activity_column.multiselect = false;
|
|
fort_activity_column.auto_select = true;
|
|
fort_activity_column.setTitle("Fort Activities");
|
|
fort_activity_column.bottom_margin = 4;
|
|
|
|
dwarf_activity_column.multiselect = false;
|
|
dwarf_activity_column.auto_select = true;
|
|
dwarf_activity_column.setTitle("Units on Activity");
|
|
dwarf_activity_column.bottom_margin = 4;
|
|
dwarf_activity_column.text_clip_at = 25;
|
|
|
|
category_breakdown_column.setTitle("Category Breakdown");
|
|
category_breakdown_column.bottom_margin = 4;
|
|
|
|
window_days = min_window;
|
|
|
|
populateFortColumn();
|
|
}
|
|
|
|
void populateFortColumn()
|
|
{
|
|
selected_column = 0;
|
|
fort_activity_count = 0;
|
|
|
|
auto last_selected_index = fort_activity_column.highlighted_index;
|
|
fort_activity_column.clear();
|
|
fort_activity_totals.clear();
|
|
dwarf_activity_values.clear();
|
|
category_breakdown.clear();
|
|
|
|
for (auto it = work_history.begin(); it != work_history.end();)
|
|
{
|
|
auto unit = it->first;
|
|
if (Units::isDead(unit))
|
|
{
|
|
work_history.erase(it++);
|
|
continue;
|
|
}
|
|
|
|
deque<activity_type> *work_list = &it->second;
|
|
++it;
|
|
|
|
size_t count = window_days * ticks_per_day;
|
|
for (auto entry = work_list->rbegin(); entry != work_list->rend() && count > 0; entry++, count--)
|
|
{
|
|
if (*entry == JOB_UNKNOWN)
|
|
continue;
|
|
|
|
++fort_activity_count;
|
|
|
|
auto real_activity = *entry;
|
|
if (real_activity < 0)
|
|
{
|
|
addFortActivity(real_activity);
|
|
}
|
|
else
|
|
{
|
|
auto activity = static_cast<df::job_type>(real_activity);
|
|
|
|
switch (activity)
|
|
{
|
|
case job_type::Eat:
|
|
case job_type::Drink:
|
|
case job_type::Drink2:
|
|
case job_type::Sleep:
|
|
case job_type::AttendParty:
|
|
case job_type::Rest:
|
|
case job_type::CleanSelf:
|
|
case job_type::DrinkBlood:
|
|
real_activity = JOB_LEISURE;
|
|
break;
|
|
|
|
case job_type::Kidnap:
|
|
case job_type::StartingFistFight:
|
|
case job_type::SeekInfant:
|
|
case job_type::SeekArtifact:
|
|
case job_type::GoShopping:
|
|
case job_type::GoShopping2:
|
|
case job_type::RecoverPet:
|
|
case job_type::CauseTrouble:
|
|
case job_type::ReportCrime:
|
|
case job_type::BeatCriminal:
|
|
case job_type::ExecuteCriminal:
|
|
real_activity = JOB_UNPRODUCTIVE;
|
|
break;
|
|
|
|
case job_type::CarveUpwardStaircase:
|
|
case job_type::CarveDownwardStaircase:
|
|
case job_type::CarveUpDownStaircase:
|
|
case job_type::CarveRamp:
|
|
case job_type::DigChannel:
|
|
case job_type::Dig:
|
|
case job_type::CarveTrack:
|
|
case job_type::CarveFortification:
|
|
real_activity = JOB_DESIGNATE;
|
|
break;
|
|
|
|
case job_type::StoreOwnedItem:
|
|
case job_type::PlaceItemInTomb:
|
|
case job_type::StoreItemInStockpile:
|
|
case job_type::StoreItemInBag:
|
|
case job_type::StoreItemInHospital:
|
|
case job_type::StoreWeapon:
|
|
case job_type::StoreArmor:
|
|
case job_type::StoreItemInBarrel:
|
|
case job_type::StoreItemInBin:
|
|
case job_type::BringItemToDepot:
|
|
case job_type::BringItemToShop:
|
|
case job_type::GetProvisions:
|
|
case job_type::FillWaterskin:
|
|
case job_type::FillWaterskin2:
|
|
case job_type::CheckChest:
|
|
case job_type::PickupEquipment:
|
|
case job_type::DumpItem:
|
|
case job_type::PushTrackVehicle:
|
|
case job_type::PlaceTrackVehicle:
|
|
case job_type::StoreItemInVehicle:
|
|
real_activity = JOB_STORE_ITEM;
|
|
break;
|
|
|
|
case job_type::ConstructDoor:
|
|
case job_type::ConstructFloodgate:
|
|
case job_type::ConstructBed:
|
|
case job_type::ConstructThrone:
|
|
case job_type::ConstructCoffin:
|
|
case job_type::ConstructTable:
|
|
case job_type::ConstructChest:
|
|
case job_type::ConstructBin:
|
|
case job_type::ConstructArmorStand:
|
|
case job_type::ConstructWeaponRack:
|
|
case job_type::ConstructCabinet:
|
|
case job_type::ConstructStatue:
|
|
case job_type::ConstructBlocks:
|
|
case job_type::MakeRawGlass:
|
|
case job_type::MakeCrafts:
|
|
case job_type::MintCoins:
|
|
case job_type::CutGems:
|
|
case job_type::CutGlass:
|
|
case job_type::EncrustWithGems:
|
|
case job_type::EncrustWithGlass:
|
|
case job_type::SmeltOre:
|
|
case job_type::MeltMetalObject:
|
|
case job_type::ExtractMetalStrands:
|
|
case job_type::MakeWeapon:
|
|
case job_type::ForgeAnvil:
|
|
case job_type::ConstructCatapultParts:
|
|
case job_type::ConstructBallistaParts:
|
|
case job_type::MakeArmor:
|
|
case job_type::MakeHelm:
|
|
case job_type::MakePants:
|
|
case job_type::StudWith:
|
|
case job_type::ProcessPlantsVial:
|
|
case job_type::ProcessPlantsBarrel:
|
|
case job_type::WeaveCloth:
|
|
case job_type::MakeGloves:
|
|
case job_type::MakeShoes:
|
|
case job_type::MakeShield:
|
|
case job_type::MakeCage:
|
|
case job_type::MakeChain:
|
|
case job_type::MakeFlask:
|
|
case job_type::MakeGoblet:
|
|
case job_type::MakeToy:
|
|
case job_type::MakeAnimalTrap:
|
|
case job_type::MakeBarrel:
|
|
case job_type::MakeBucket:
|
|
case job_type::MakeWindow:
|
|
case job_type::MakeTotem:
|
|
case job_type::MakeAmmo:
|
|
case job_type::DecorateWith:
|
|
case job_type::MakeBackpack:
|
|
case job_type::MakeQuiver:
|
|
case job_type::MakeBallistaArrowHead:
|
|
case job_type::AssembleSiegeAmmo:
|
|
case job_type::ConstructMechanisms:
|
|
case job_type::MakeTrapComponent:
|
|
case job_type::ExtractFromPlants:
|
|
case job_type::ExtractFromRawFish:
|
|
case job_type::ExtractFromLandAnimal:
|
|
case job_type::MakeCharcoal:
|
|
case job_type::MakeAsh:
|
|
case job_type::MakeLye:
|
|
case job_type::MakePotashFromLye:
|
|
case job_type::MakePotashFromAsh:
|
|
case job_type::DyeThread:
|
|
case job_type::DyeCloth:
|
|
case job_type::SewImage:
|
|
case job_type::MakePipeSection:
|
|
case job_type::ConstructHatchCover:
|
|
case job_type::ConstructGrate:
|
|
case job_type::ConstructQuern:
|
|
case job_type::ConstructMillstone:
|
|
case job_type::ConstructSplint:
|
|
case job_type::ConstructCrutch:
|
|
case job_type::ConstructTractionBench:
|
|
case job_type::CustomReaction:
|
|
case job_type::ConstructSlab:
|
|
case job_type::EngraveSlab:
|
|
case job_type::SpinThread:
|
|
case job_type::MakeTool:
|
|
real_activity = JOB_MANUFACTURE;
|
|
break;
|
|
|
|
case job_type::DetailFloor:
|
|
case job_type::DetailWall:
|
|
real_activity = JOB_DETAILING;
|
|
break;
|
|
|
|
case job_type::Hunt:
|
|
case job_type::ReturnKill:
|
|
case job_type::HuntVermin:
|
|
case job_type::GatherPlants:
|
|
case job_type::Fish:
|
|
case job_type::CatchLiveFish:
|
|
case job_type::BaitTrap:
|
|
case job_type::InstallColonyInHive:
|
|
real_activity = JOB_HUNTING;
|
|
break;
|
|
|
|
case job_type::RemoveConstruction:
|
|
case job_type::DestroyBuilding:
|
|
case job_type::RemoveStairs:
|
|
case job_type::ConstructBuilding:
|
|
real_activity = JOB_CONSTRUCTION;
|
|
break;
|
|
|
|
case job_type::FellTree:
|
|
case job_type::CollectWebs:
|
|
case job_type::CollectSand:
|
|
case job_type::DrainAquarium:
|
|
case job_type::FillAquarium:
|
|
case job_type::FillPond:
|
|
case job_type::CollectClay:
|
|
real_activity = JOB_COLLECT;
|
|
break;
|
|
|
|
case job_type::TrainHuntingAnimal:
|
|
case job_type::TrainWarAnimal:
|
|
case job_type::CatchLiveLandAnimal:
|
|
case job_type::TameVermin:
|
|
case job_type::TameAnimal:
|
|
case job_type::ChainAnimal:
|
|
case job_type::UnchainAnimal:
|
|
case job_type::UnchainPet:
|
|
case job_type::ReleaseLargeCreature:
|
|
case job_type::ReleasePet:
|
|
case job_type::ReleaseSmallCreature:
|
|
case job_type::HandleSmallCreature:
|
|
case job_type::HandleLargeCreature:
|
|
case job_type::CageLargeCreature:
|
|
case job_type::CageSmallCreature:
|
|
case job_type::PitLargeAnimal:
|
|
case job_type::PitSmallAnimal:
|
|
case job_type::SlaughterAnimal:
|
|
case job_type::ShearCreature:
|
|
case job_type::PenLargeAnimal:
|
|
case job_type::PenSmallAnimal:
|
|
case job_type::TrainAnimal:
|
|
real_activity = JOB_ANIMALS;
|
|
break;
|
|
|
|
case job_type::PlantSeeds:
|
|
case job_type::HarvestPlants:
|
|
case job_type::FertilizeField:
|
|
real_activity = JOB_AGRICULTURE;
|
|
break;
|
|
|
|
case job_type::ButcherAnimal:
|
|
case job_type::PrepareRawFish:
|
|
case job_type::MillPlants:
|
|
case job_type::MilkCreature:
|
|
case job_type::MakeCheese:
|
|
case job_type::PrepareMeal:
|
|
case job_type::ProcessPlants:
|
|
case job_type::CollectHiveProducts:
|
|
real_activity = JOB_FOOD_PROD;
|
|
break;
|
|
|
|
case job_type::LoadCatapult:
|
|
case job_type::LoadBallista:
|
|
case job_type::FireCatapult:
|
|
case job_type::FireBallista:
|
|
real_activity = JOB_MILITARY;
|
|
break;
|
|
|
|
case job_type::LoadCageTrap:
|
|
case job_type::LoadStoneTrap:
|
|
case job_type::LoadWeaponTrap:
|
|
case job_type::CleanTrap:
|
|
case job_type::LinkBuildingToTrigger:
|
|
case job_type::PullLever:
|
|
real_activity = JOB_MECHANICAL;
|
|
break;
|
|
|
|
case job_type::RecoverWounded:
|
|
case job_type::DiagnosePatient:
|
|
case job_type::ImmobilizeBreak:
|
|
case job_type::DressWound:
|
|
case job_type::CleanPatient:
|
|
case job_type::Surgery:
|
|
case job_type::Suture:
|
|
case job_type::SetBone:
|
|
case job_type::PlaceInTraction:
|
|
case job_type::GiveWater:
|
|
case job_type::GiveFood:
|
|
case job_type::GiveWater2:
|
|
case job_type::GiveFood2:
|
|
case job_type::BringCrutch:
|
|
case job_type::ApplyCast:
|
|
real_activity = JOB_MEDICAL;
|
|
break;
|
|
|
|
case job_type::OperatePump:
|
|
case job_type::ManageWorkOrders:
|
|
case job_type::UpdateStockpileRecords:
|
|
case job_type::TradeAtDepot:
|
|
real_activity = JOB_PRODUCTIVE;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
addFortActivity(real_activity);
|
|
addCategoryActivity(real_activity, *entry);
|
|
}
|
|
|
|
if (dwarf_activity_values.find(real_activity) == dwarf_activity_values.end())
|
|
dwarf_activity_values[real_activity] = map<df::unit *, size_t>();
|
|
|
|
map<df::unit *, size_t> &activity_for_dwarf = dwarf_activity_values[real_activity];
|
|
if (activity_for_dwarf.find(unit) == activity_for_dwarf.end())
|
|
activity_for_dwarf[unit] = 0;
|
|
|
|
++activity_for_dwarf[unit];
|
|
}
|
|
}
|
|
|
|
vector<pair<activity_type, size_t>> rev_vec(fort_activity_totals.begin(), fort_activity_totals.end());
|
|
sort(rev_vec.begin(), rev_vec.end(), less_second<activity_type, size_t>());
|
|
|
|
for (auto rev_it = rev_vec.begin(); rev_it != rev_vec.end(); rev_it++)
|
|
{
|
|
auto activity = rev_it->first;
|
|
addToFortAverageColumn(activity);
|
|
|
|
for (auto it = dwarf_activity_values[activity].begin(); it != dwarf_activity_values[activity].end(); it++)
|
|
{
|
|
auto avg = getPercentage(it->second, getFortActivityCount(activity));
|
|
dwarf_activity_values[activity][it->first] = avg;
|
|
}
|
|
}
|
|
|
|
for (auto cat_it = category_breakdown.begin(); cat_it != category_breakdown.end(); cat_it++)
|
|
{
|
|
auto cat_total = fort_activity_totals[cat_it->first];
|
|
for (auto val_it = cat_it->second.begin(); val_it != cat_it->second.end(); val_it++)
|
|
{
|
|
category_breakdown[cat_it->first][val_it->first] = getPercentage(val_it->second, cat_total);
|
|
}
|
|
}
|
|
|
|
dwarf_activity_column.left_margin = fort_activity_column.fixWidth() + 2;
|
|
fort_activity_column.filterDisplay();
|
|
fort_activity_column.setHighlight(last_selected_index);
|
|
populateDwarfColumn();
|
|
populateCategoryBreakdownColumn();
|
|
}
|
|
|
|
void populateDwarfColumn()
|
|
{
|
|
dwarf_activity_column.clear();
|
|
if (fort_activity_column.getDisplayListSize() > 0)
|
|
{
|
|
activity_type selected_activity = fort_activity_column.getFirstSelectedElem();
|
|
auto dwarf_activities = &dwarf_activity_values[selected_activity];
|
|
if (dwarf_activities)
|
|
{
|
|
vector<pair<df::unit *, size_t>> rev_vec(dwarf_activities->begin(), dwarf_activities->end());
|
|
sort(rev_vec.begin(), rev_vec.end(), less_second<df::unit *, size_t>());
|
|
|
|
for (auto it = rev_vec.begin(); it != rev_vec.end(); ++it)
|
|
dwarf_activity_column.add(getDwarfAverage(it->first, it->second), it->first);
|
|
}
|
|
}
|
|
|
|
category_breakdown_column.left_margin = dwarf_activity_column.fixWidth() + 2;
|
|
dwarf_activity_column.clearSearch();
|
|
dwarf_activity_column.setHighlight(0);
|
|
}
|
|
|
|
void populateCategoryBreakdownColumn()
|
|
{
|
|
category_breakdown_column.clear();
|
|
if (fort_activity_column.getDisplayListSize() == 0)
|
|
return;
|
|
|
|
auto selected_activity = fort_activity_column.getFirstSelectedElem();
|
|
auto category_activities = &category_breakdown[selected_activity];
|
|
if (category_activities)
|
|
{
|
|
vector<pair<activity_type, size_t>> rev_vec(category_activities->begin(), category_activities->end());
|
|
sort(rev_vec.begin(), rev_vec.end(), less_second<activity_type, size_t>());
|
|
|
|
for (auto it = rev_vec.begin(); it != rev_vec.end(); ++it)
|
|
category_breakdown_column.add(getBreakdownAverage(it->first, it->second), it->first);
|
|
}
|
|
|
|
category_breakdown_column.fixWidth();
|
|
category_breakdown_column.clearSearch();
|
|
category_breakdown_column.setHighlight(0);
|
|
}
|
|
|
|
void addToFortAverageColumn(activity_type &type)
|
|
{
|
|
if (getFortActivityCount(type))
|
|
fort_activity_column.add(getFortAverage(type), type);
|
|
}
|
|
|
|
string getFortAverage(const activity_type &activity)
|
|
{
|
|
auto average = getPercentage(getFortActivityCount(activity), fort_activity_count);
|
|
auto label = getActivityLabel(activity);
|
|
auto result = pad_string(int_to_string(average), 3) + " " + label;
|
|
|
|
return result;
|
|
}
|
|
|
|
string getDwarfAverage(df::unit *unit, const size_t value)
|
|
{
|
|
auto label = getUnitName(unit);
|
|
auto result = pad_string(int_to_string(value), 3) + " " + label;
|
|
|
|
return result;
|
|
}
|
|
|
|
string getBreakdownAverage(activity_type activity, const size_t value)
|
|
{
|
|
auto label = getActivityLabel(activity);
|
|
auto result = pad_string(int_to_string(value), 3) + " " + label;
|
|
|
|
return result;
|
|
}
|
|
|
|
size_t getFortActivityCount(const activity_type activity)
|
|
{
|
|
if (fort_activity_totals.find(activity) == fort_activity_totals.end())
|
|
return 0;
|
|
|
|
return fort_activity_totals[activity];
|
|
}
|
|
|
|
void addFortActivity(const activity_type activity)
|
|
{
|
|
if (fort_activity_totals.find(activity) == fort_activity_totals.end())
|
|
fort_activity_totals[activity] = 0;
|
|
|
|
fort_activity_totals[activity]++;
|
|
}
|
|
|
|
void addCategoryActivity(const int category, const activity_type activity)
|
|
{
|
|
if (category_breakdown.find(category) == category_breakdown.end())
|
|
category_breakdown[category] = map<activity_type, size_t>();
|
|
|
|
if (category_breakdown[category].find(activity) == category_breakdown[category].end())
|
|
category_breakdown[category][activity] = 0;
|
|
|
|
category_breakdown[category][activity]++;
|
|
}
|
|
|
|
void feed(set<df::interface_key> *input)
|
|
{
|
|
bool key_processed = false;
|
|
switch (selected_column)
|
|
{
|
|
case 0:
|
|
key_processed = fort_activity_column.feed(input);
|
|
break;
|
|
case 1:
|
|
key_processed = dwarf_activity_column.feed(input);
|
|
break;
|
|
}
|
|
|
|
if (key_processed)
|
|
{
|
|
if (selected_column == 0 && fort_activity_column.feed_changed_highlight)
|
|
{
|
|
populateDwarfColumn();
|
|
populateCategoryBreakdownColumn();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (input->count(interface_key::LEAVESCREEN))
|
|
{
|
|
input->clear();
|
|
Screen::dismiss(this);
|
|
return;
|
|
}
|
|
else if (input->count(interface_key::SECONDSCROLL_PAGEDOWN))
|
|
{
|
|
window_days += min_window;
|
|
if (window_days > max_history_days)
|
|
window_days = min_window;
|
|
|
|
populateFortColumn();
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_D))
|
|
{
|
|
df::unit *selected_unit = (selected_column == 1) ? dwarf_activity_column.getFirstSelectedElem() : nullptr;
|
|
Screen::dismiss(this);
|
|
Screen::show(new ViewscreenDwarfStats(selected_unit), plugin_self);
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_Z))
|
|
{
|
|
df::unit *selected_unit = (selected_column == 1) ? dwarf_activity_column.getFirstSelectedElem() : nullptr;
|
|
if (selected_unit)
|
|
{
|
|
input->clear();
|
|
Screen::dismiss(this);
|
|
Gui::resetDwarfmodeView(true);
|
|
send_key(interface_key::D_VIEWUNIT);
|
|
move_cursor(selected_unit->pos);
|
|
}
|
|
}
|
|
else if (input->count(interface_key::CURSOR_LEFT))
|
|
{
|
|
--selected_column;
|
|
validateColumn();
|
|
}
|
|
else if (input->count(interface_key::CURSOR_RIGHT))
|
|
{
|
|
++selected_column;
|
|
validateColumn();
|
|
}
|
|
else if (enabler->tracking_on && enabler->mouse_lbut)
|
|
{
|
|
if (fort_activity_column.setHighlightByMouse())
|
|
{
|
|
selected_column = 0;
|
|
populateDwarfColumn();
|
|
}
|
|
else if (dwarf_activity_column.setHighlightByMouse())
|
|
selected_column = 1;
|
|
|
|
enabler->mouse_lbut = enabler->mouse_rbut = 0;
|
|
}
|
|
}
|
|
|
|
void render()
|
|
{
|
|
using namespace df::enums::interface_key;
|
|
|
|
if (Screen::isDismissed(this))
|
|
return;
|
|
|
|
dfhack_viewscreen::render();
|
|
|
|
Screen::clear();
|
|
Screen::drawBorder(" Fortress Efficiency ");
|
|
|
|
fort_activity_column.display(selected_column == 0);
|
|
dwarf_activity_column.display(selected_column == 1);
|
|
category_breakdown_column.display(false);
|
|
|
|
int32_t y = gps->dimy - 4;
|
|
int32_t x = 2;
|
|
OutputHotkeyString(x, y, "Leave", LEAVESCREEN);
|
|
|
|
x += 13;
|
|
string window_label = "Window Months: " + int_to_string(window_days / min_window);
|
|
OutputHotkeyString(x, y, window_label.c_str(), SECONDSCROLL_PAGEDOWN);
|
|
|
|
++y;
|
|
x = 2;
|
|
OutputHotkeyString(x, y, "Dwarf Stats", CUSTOM_SHIFT_D);
|
|
|
|
x += 3;
|
|
OutputHotkeyString(x, y, "Zoom Unit", CUSTOM_SHIFT_Z);
|
|
}
|
|
|
|
std::string getFocusString() { return "dwarfmonitor_fortstats"; }
|
|
|
|
private:
|
|
ListColumn<activity_type> fort_activity_column, category_breakdown_column;
|
|
ListColumn<df::unit *> dwarf_activity_column;
|
|
int selected_column;
|
|
|
|
map<activity_type, size_t> fort_activity_totals;
|
|
map<int, map<activity_type, size_t>> category_breakdown;
|
|
map<activity_type, map<df::unit *, size_t>> dwarf_activity_values;
|
|
size_t fort_activity_count;
|
|
size_t window_days;
|
|
|
|
vector<activity_type> listed_activities;
|
|
|
|
void validateColumn()
|
|
{
|
|
set_to_limit(selected_column, 1);
|
|
}
|
|
|
|
void resize(int32_t x, int32_t y)
|
|
{
|
|
dfhack_viewscreen::resize(x, y);
|
|
fort_activity_column.resize();
|
|
dwarf_activity_column.resize();
|
|
}
|
|
};
|
|
|
|
|
|
struct preference_map
|
|
{
|
|
df::unit_preference pref;
|
|
vector<df::unit *> dwarves;
|
|
string label;
|
|
|
|
string getItemLabel()
|
|
{
|
|
df::world_raws::T_itemdefs &defs = 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:
|
|
label = ENUM_ATTR_STR(item_type, caption, pref.item_type);
|
|
if (label.size())
|
|
{
|
|
if (label[label.size() - 1] == 's')
|
|
label += "es";
|
|
else
|
|
label += "s";
|
|
}
|
|
else
|
|
{
|
|
label = "UNKNOWN";
|
|
}
|
|
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 :" + Units::getRaceNamePluralById(pref.creature_id);
|
|
break;
|
|
}
|
|
|
|
case (T_type::HateCreature):
|
|
{
|
|
label = "Hates :" + Units::getRaceNamePluralById(pref.creature_id);
|
|
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.descriptors.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.descriptors.colors[pref.color_id]->name;
|
|
break;
|
|
|
|
case (T_type::LikePoeticForm):
|
|
label += "Poetry :" + getFormName<df::poetic_form>(pref.poetic_form_id);
|
|
break;
|
|
|
|
case (T_type::LikeMusicalForm):
|
|
label += "Music :" + getFormName<df::musical_form>(pref.musical_form_id);
|
|
break;
|
|
|
|
case (T_type::LikeDanceForm):
|
|
label += "Dance :" + getFormName<df::dance_form>(pref.dance_form_id);
|
|
break;
|
|
|
|
default:
|
|
label += string("UNKNOWN ") + ENUM_KEY_STR(unit_preference::T_type, pref.type);
|
|
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 = 50;
|
|
|
|
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 = 50;
|
|
|
|
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<pair<size_t, size_t>> rev_vec(preference_totals.begin(), preference_totals.end());
|
|
sort(rev_vec.begin(), rev_vec.end(), less_second<size_t, size_t>());
|
|
|
|
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<size_t> 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;
|
|
|
|
case (T_type::LikePoeticForm):
|
|
return lhs.poetic_form_id == rhs.poetic_form_id;
|
|
break;
|
|
|
|
case (T_type::LikeMusicalForm):
|
|
return lhs.musical_form_id == rhs.musical_form_id;
|
|
break;
|
|
|
|
case (T_type::LikeDanceForm):
|
|
return lhs.dance_form_id == rhs.dance_form_id;
|
|
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;
|
|
|
|
case (T_type::LikePoeticForm):
|
|
case (T_type::LikeMusicalForm):
|
|
case (T_type::LikeDanceForm):
|
|
return COLOR_LIGHTCYAN;
|
|
|
|
default:
|
|
return COLOR_LIGHTMAGENTA;
|
|
}
|
|
|
|
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<df::unit *> elem(label, *dfit, "", color);
|
|
dwarf_column.add(elem);
|
|
}
|
|
}
|
|
|
|
dwarf_column.clearSearch();
|
|
dwarf_column.setHighlight(0);
|
|
}
|
|
|
|
df::unit *getSelectedUnit() override
|
|
{
|
|
return (selected_column == 1) ? dwarf_column.getFirstSelectedElem() : nullptr;
|
|
}
|
|
|
|
void feed(set<df::interface_key> *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_V))
|
|
{
|
|
df::unit *unit = getSelectedUnit();
|
|
if (unit)
|
|
{
|
|
auto unitscr = df::allocate<df::viewscreen_unitst>();
|
|
unitscr->unit = unit;
|
|
Screen::show(unitscr);
|
|
}
|
|
}
|
|
else if (input->count(interface_key::CUSTOM_SHIFT_Z))
|
|
{
|
|
df::unit *selected_unit = getSelectedUnit();
|
|
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()
|
|
{
|
|
using namespace df::enums::interface_key;
|
|
|
|
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", LEAVESCREEN);
|
|
|
|
x += 2;
|
|
OutputHotkeyString(x, y, "View Unit", CUSTOM_SHIFT_V, false, 0,
|
|
getSelectedUnit() ? COLOR_WHITE : COLOR_DARKGREY);
|
|
|
|
x += 2;
|
|
OutputHotkeyString(x, y, "Zoom Unit", CUSTOM_SHIFT_Z, false, 0,
|
|
getSelectedUnit() ? COLOR_WHITE : COLOR_DARKGREY);
|
|
}
|
|
|
|
std::string getFocusString() { return "dwarfmonitor_preferences"; }
|
|
|
|
private:
|
|
ListColumn<size_t> preferences_column;
|
|
ListColumn<df::unit *> dwarf_column;
|
|
int selected_column;
|
|
|
|
map<size_t, size_t> preference_totals;
|
|
|
|
vector<preference_map> 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_screen()
|
|
{
|
|
Screen::show(new ViewscreenFortStats(), plugin_self);
|
|
}
|
|
|
|
static void add_work_history(df::unit *unit, activity_type type)
|
|
{
|
|
if (work_history.find(unit) == work_history.end())
|
|
{
|
|
auto max_history = get_max_history();
|
|
for (int i = 0; i < max_history; i++)
|
|
work_history[unit].push_back(JOB_UNKNOWN);
|
|
}
|
|
|
|
work_history[unit].push_back(type);
|
|
work_history[unit].pop_front();
|
|
}
|
|
|
|
static bool is_at_leisure(df::unit *unit)
|
|
{
|
|
for (auto p = unit->status.misc_traits.begin(); p < unit->status.misc_traits.end(); p++)
|
|
{
|
|
if ((*p)->id == misc_trait_type::Migrant || (*p)->id == misc_trait_type::OnBreak)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void reset()
|
|
{
|
|
work_history.clear();
|
|
|
|
for (int i = 0; i < 7; i++)
|
|
misery[i] = 0;
|
|
|
|
misery_upto_date = false;
|
|
}
|
|
|
|
static void update_dwarf_stats(bool is_paused)
|
|
{
|
|
if (monitor_misery)
|
|
{
|
|
for (int i = 0; i < 7; i++)
|
|
misery[i] = 0;
|
|
}
|
|
|
|
for (auto iter = world->units.active.begin(); iter != world->units.active.end(); iter++)
|
|
{
|
|
df::unit* unit = *iter;
|
|
if (!Units::isCitizen(unit))
|
|
continue;
|
|
|
|
if (DFHack::Units::isDead(unit))
|
|
{
|
|
auto it = work_history.find(unit);
|
|
if (it != work_history.end())
|
|
work_history.erase(it);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (monitor_misery)
|
|
{
|
|
misery[get_happiness_cat(unit)]++;
|
|
}
|
|
|
|
if (!monitor_jobs || is_paused)
|
|
continue;
|
|
|
|
if (Units::isBaby(unit) ||
|
|
Units::isChild(unit) ||
|
|
unit->profession == profession::DRUNK)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (ENUM_ATTR(profession, military, unit->profession))
|
|
{
|
|
add_work_history(unit, JOB_MILITARY);
|
|
continue;
|
|
}
|
|
|
|
if (!unit->job.current_job)
|
|
{
|
|
add_work_history(unit, JOB_IDLE);
|
|
continue;
|
|
}
|
|
|
|
if (is_at_leisure(unit))
|
|
{
|
|
add_work_history(unit, JOB_LEISURE);
|
|
continue;
|
|
}
|
|
|
|
add_work_history(unit, unit->job.current_job->job_type);
|
|
}
|
|
}
|
|
|
|
|
|
DFhackCExport command_result plugin_onupdate (color_ostream &out)
|
|
{
|
|
if (!monitor_jobs && !monitor_misery)
|
|
return CR_OK;
|
|
|
|
if(!Maps::IsValid())
|
|
return CR_OK;
|
|
|
|
static decltype(world->frame_counter) last_frame_count = 0;
|
|
|
|
bool is_paused = DFHack::World::ReadPauseState();
|
|
if (is_paused)
|
|
{
|
|
if (monitor_misery && !misery_upto_date)
|
|
misery_upto_date = true;
|
|
else
|
|
return CR_OK;
|
|
}
|
|
else
|
|
{
|
|
if (world->frame_counter - last_frame_count < DELTA_TICKS)
|
|
return CR_OK;
|
|
|
|
last_frame_count = world->frame_counter;
|
|
}
|
|
|
|
update_dwarf_stats(is_paused);
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest
|
|
{
|
|
typedef df::viewscreen_dwarfmodest interpose_base;
|
|
|
|
DEFINE_VMETHOD_INTERPOSE(void, render, ())
|
|
{
|
|
INTERPOSE_NEXT(render)();
|
|
|
|
CoreSuspendClaimer suspend;
|
|
if (Maps::IsValid())
|
|
{
|
|
dm_lua::call("render_all");
|
|
}
|
|
}
|
|
};
|
|
|
|
IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, render);
|
|
|
|
static bool set_monitoring_mode(const string &mode, const bool &state)
|
|
{
|
|
bool mode_recognized = false;
|
|
|
|
if (!is_enabled)
|
|
return false;
|
|
|
|
if (mode == "work" || mode == "all")
|
|
{
|
|
mode_recognized = true;
|
|
monitor_jobs = state;
|
|
if (!monitor_jobs)
|
|
reset();
|
|
}
|
|
if (mode == "misery" || mode == "all")
|
|
{
|
|
mode_recognized = true;
|
|
monitor_misery = state;
|
|
}
|
|
if (mode == "date" || mode == "all")
|
|
{
|
|
mode_recognized = true;
|
|
monitor_date = state;
|
|
}
|
|
if (mode == "weather" || mode == "all")
|
|
{
|
|
mode_recognized = true;
|
|
monitor_weather = state;
|
|
}
|
|
|
|
return mode_recognized;
|
|
}
|
|
|
|
static bool load_config()
|
|
{
|
|
return dm_lua::call("load_config");
|
|
}
|
|
|
|
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
|
|
{
|
|
if (enable)
|
|
load_config();
|
|
if (is_enabled != enable)
|
|
{
|
|
if (!INTERPOSE_HOOK(dwarf_monitor_hook, render).apply(enable))
|
|
return CR_FAILURE;
|
|
|
|
reset();
|
|
is_enabled = enable;
|
|
}
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & parameters)
|
|
{
|
|
bool show_help = false;
|
|
if (parameters.empty())
|
|
{
|
|
show_help = true;
|
|
}
|
|
else
|
|
{
|
|
auto cmd = parameters[0][0];
|
|
string mode;
|
|
|
|
if (parameters.size() > 1)
|
|
mode = toLower(parameters[1]);
|
|
|
|
if (cmd == 'v' || cmd == 'V')
|
|
{
|
|
out << "DwarfMonitor" << endl << "Version: " << PLUGIN_VERSION << endl;
|
|
}
|
|
else if ((cmd == 'e' || cmd == 'E') && !mode.empty())
|
|
{
|
|
if (!is_enabled)
|
|
plugin_enable(out, true);
|
|
|
|
if (set_monitoring_mode(mode, true))
|
|
{
|
|
out << "Monitoring enabled: " << mode << endl;
|
|
}
|
|
else
|
|
{
|
|
show_help = true;
|
|
}
|
|
}
|
|
else if ((cmd == 'd' || cmd == 'D') && !mode.empty())
|
|
{
|
|
if (set_monitoring_mode(mode, false))
|
|
out << "Monitoring disabled: " << mode << endl;
|
|
else
|
|
show_help = true;
|
|
}
|
|
else if (cmd == 's' || cmd == 'S')
|
|
{
|
|
if(Maps::IsValid())
|
|
Screen::show(new ViewscreenFortStats(), plugin_self);
|
|
}
|
|
else if (cmd == 'p' || cmd == 'P')
|
|
{
|
|
if(Maps::IsValid())
|
|
Screen::show(new ViewscreenPreferences(), plugin_self);
|
|
}
|
|
else if (cmd == 'r' || cmd == 'R')
|
|
{
|
|
load_config();
|
|
}
|
|
else
|
|
{
|
|
show_help = true;
|
|
}
|
|
}
|
|
|
|
if (show_help)
|
|
return CR_WRONG_USAGE;
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands)
|
|
{
|
|
activity_labels[JOB_IDLE] = "Idle";
|
|
activity_labels[JOB_MILITARY] = "Military Duty";
|
|
activity_labels[JOB_LEISURE] = "Leisure";
|
|
activity_labels[JOB_UNPRODUCTIVE] = "Unproductive";
|
|
activity_labels[JOB_DESIGNATE] = "Mining";
|
|
activity_labels[JOB_STORE_ITEM] = "Store/Fetch Item";
|
|
activity_labels[JOB_MANUFACTURE] = "Manufacturing";
|
|
activity_labels[JOB_DETAILING] = "Detailing";
|
|
activity_labels[JOB_HUNTING] = "Hunting/Gathering";
|
|
activity_labels[JOB_MEDICAL] = "Medical";
|
|
activity_labels[JOB_COLLECT] = "Collect Materials";
|
|
activity_labels[JOB_CONSTRUCTION] = "Construction";
|
|
activity_labels[JOB_AGRICULTURE] = "Agriculture";
|
|
activity_labels[JOB_FOOD_PROD] = "Food/Drink Production";
|
|
activity_labels[JOB_MECHANICAL] = "Mechanics";
|
|
activity_labels[JOB_ANIMALS] = "Animal Handling";
|
|
activity_labels[JOB_PRODUCTIVE] = "Other Productive";
|
|
|
|
commands.push_back(
|
|
PluginCommand(
|
|
"dwarfmonitor", "Records dwarf activity to measure fort efficiency",
|
|
dwarfmonitor_cmd, false,
|
|
"dwarfmonitor enable <mode>\n"
|
|
" Start monitoring <mode>\n"
|
|
" <mode> can be \"work\", \"misery\", \"weather\", or \"all\"\n"
|
|
"dwarfmonitor disable <mode>\n"
|
|
" <mode> as above\n\n"
|
|
"dwarfmonitor stats\n"
|
|
" Show statistics summary\n"
|
|
"dwarfmonitor prefs\n"
|
|
" Show dwarf preferences summary\n\n"
|
|
"dwarfmonitor reload\n"
|
|
" Reload configuration file (dfhack-config/dwarfmonitor.json)\n"
|
|
));
|
|
|
|
dm_lua::state = Lua::Open(out);
|
|
if (dm_lua::state == NULL)
|
|
return CR_FAILURE;
|
|
|
|
return CR_OK;
|
|
}
|
|
|
|
DFhackCExport command_result plugin_shutdown(color_ostream &out)
|
|
{
|
|
dm_lua::cleanup();
|
|
return CR_OK;
|
|
}
|
|
|
|
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
|
|
{
|
|
switch (event) {
|
|
case SC_MAP_LOADED:
|
|
reset();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return CR_OK;
|
|
}
|