diff --git a/dfhack-config/dwarfmonitor.json b/dfhack-config/dwarfmonitor.json index 007dad020..9bd3b1f76 100644 --- a/dfhack-config/dwarfmonitor.json +++ b/dfhack-config/dwarfmonitor.json @@ -1,21 +1,3 @@ { - "widgets": [ - { - "type": "weather", - "x": 22, - "y": -1 - }, - { - "type": "date", - "x": -30, - "y": 0, - "format": "Y-M-D" - }, - { - "type": "misery", - "x": -2, - "y": -1, - "anchor": "right" - } - ] + "date_format": "Y-M-D" } diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json b/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json new file mode 100644 index 000000000..cc2db582b --- /dev/null +++ b/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json @@ -0,0 +1,12 @@ +{ + "pos": { + "x": 2, + "y": 2 + }, + "provider": "dwarfmonitor", + "class": "Widget_cursor", + "viewscreens": [ + "dungeonmode", + "dwarfmode" + ] +} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_date.json b/dfhack-config/overlay/widgets/dwarfmonitor_date.json new file mode 100644 index 000000000..6c868d419 --- /dev/null +++ b/dfhack-config/overlay/widgets/dwarfmonitor_date.json @@ -0,0 +1,11 @@ +{ + "pos": { + "x": -16, + "y": 1 + }, + "provider": "dwarfmonitor", + "class": "Widget_date", + "viewscreens": [ + "dwarfmode" + ] +} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_misery.json b/dfhack-config/overlay/widgets/dwarfmonitor_misery.json new file mode 100644 index 000000000..5ef741823 --- /dev/null +++ b/dfhack-config/overlay/widgets/dwarfmonitor_misery.json @@ -0,0 +1,11 @@ +{ + "pos": { + "x": -2, + "y": -1 + }, + "provider": "dwarfmonitor", + "class": "Widget_misery", + "viewscreens": [ + "dwarfmode" + ] +} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_weather.json b/dfhack-config/overlay/widgets/dwarfmonitor_weather.json new file mode 100644 index 000000000..8a6130af0 --- /dev/null +++ b/dfhack-config/overlay/widgets/dwarfmonitor_weather.json @@ -0,0 +1,11 @@ +{ + "pos": { + "x": 15, + "y": -1 + }, + "provider": "dwarfmonitor", + "class": "Widget_weather", + "viewscreens": [ + "dwarfmode" + ] +} diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index 1a453e0d5..4355104a7 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -52,7 +52,6 @@ using std::deque; DFHACK_PLUGIN("dwarfmonitor"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); -REQUIRE_GLOBAL(current_weather); REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(ui); @@ -74,20 +73,8 @@ struct less_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> 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, @@ -151,102 +138,18 @@ static void move_cursor(df::coord &pos) 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; - } - } - 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; - } +static int getStressCategoryColors(lua_State *L) { + const size_t n = sizeof(monitor_colors)/sizeof(color_value); + lua_createtable(L, n, 0); + for (size_t i = 0; i < n; ++i) { + Lua::Push(L, monitor_colors[i]); + lua_rawseti(L, -2, i+1); } + 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_COMMAND(getStressCategoryColors), DFHACK_LUA_END }; @@ -1648,8 +1551,7 @@ public: return (selected_column == 1) ? dwarf_column.getFirstSelectedElem() : nullptr; } - void feed(set *input) - { + void feed(set *input) override { bool key_processed = false; switch (selected_column) { @@ -1723,8 +1625,7 @@ public: } } - void render() - { + void render() override { using namespace df::enums::interface_key; if (Screen::isDismissed(this)) @@ -1751,7 +1652,7 @@ public: getSelectedUnit() ? COLOR_WHITE : COLOR_DARKGREY); } - std::string getFocusString() { return "dwarfmonitor_preferences"; } + std::string getFocusString() override { return "dwarfmonitor_preferences"; } private: ListColumn preferences_column; @@ -1762,13 +1663,11 @@ private: vector preferences_store; - void validateColumn() - { + void validateColumn() { set_to_limit(selected_column, 1); } - void resize(int32_t x, int32_t y) - { + void resize(int32_t x, int32_t y) override { dfhack_viewscreen::resize(x, y); preferences_column.resize(); dwarf_column.resize(); @@ -1776,15 +1675,12 @@ private: }; -static void open_stats_screen() -{ +static void open_stats_screen() { Screen::show(dts::make_unique(), plugin_self); } -static void add_work_history(df::unit *unit, activity_type type) -{ - if (work_history.find(unit) == work_history.end()) - { +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); @@ -1794,8 +1690,7 @@ static void add_work_history(df::unit *unit, activity_type type) work_history[unit].pop_front(); } -static bool is_at_leisure(df::unit *unit) -{ +static bool is_at_leisure(df::unit *unit) { if (Units::getMiscTrait(unit, misc_trait_type::Migrant)) return true; @@ -1805,32 +1700,17 @@ static bool is_at_leisure(df::unit *unit) return false; } -static void reset() -{ +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; + for (auto unit : world->units.active) { if (!Units::isCitizen(unit)) continue; - if (!DFHack::Units::isActive(unit)) - { + if (!DFHack::Units::isActive(unit)) { auto it = work_history.find(unit); if (it != work_history.end()) work_history.erase(it); @@ -1838,35 +1718,25 @@ static void update_dwarf_stats(bool is_paused) continue; } - if (monitor_misery) - { - misery[get_happiness_cat(unit)]++; - } - - if (!monitor_jobs || is_paused) + if (is_paused) continue; if (Units::isBaby(unit) || - Units::isChild(unit) || - unit->profession == profession::DRUNK) - { + Units::isChild(unit) || + unit->profession == profession::DRUNK) continue; - } - if (ENUM_ATTR(profession, military, unit->profession)) - { + if (ENUM_ATTR(profession, military, unit->profession)) { add_work_history(unit, JOB_MILITARY); continue; } - if (!unit->job.current_job) - { + if (!unit->job.current_job) { add_work_history(unit, JOB_IDLE); continue; } - if (is_at_leisure(unit)) - { + if (is_at_leisure(unit)) { add_work_history(unit, JOB_LEISURE); continue; } @@ -1876,107 +1746,21 @@ static void update_dwarf_stats(bool is_paused) } -DFhackCExport command_result plugin_onupdate (color_ostream &out) -{ - if (!monitor_jobs && !monitor_misery) - return CR_OK; - - if(!Maps::IsValid()) +DFhackCExport command_result plugin_onupdate (color_ostream &out) { + if (!is_enabled | !Maps::IsValid()) return CR_OK; 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 % DELTA_TICKS != 0) - return CR_OK; - } + if (!is_paused && world->frame_counter % DELTA_TICKS != 0) + return CR_OK; 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; - /* - NOTE: although we are not touching DF directly but there might be - code running that uses these values. So this could use another mutex - or just suspend the core while we edit our values. - */ - CoreSuspender guard; - - 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) - { - CoreSuspender guard; - load_config(); - } - if (is_enabled != enable) - { - if (!INTERPOSE_HOOK(dwarf_monitor_hook, render).apply(enable)) - return CR_FAILURE; - +DFhackCExport command_result plugin_enable(color_ostream &, bool enable) { + if (is_enabled != enable) { reset(); is_enabled = enable; } @@ -1984,76 +1768,28 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) return CR_OK; } -static command_result dwarfmonitor_cmd(color_ostream &out, vector & parameters) -{ - bool show_help = false; +static command_result dwarfmonitor_cmd(color_ostream &, vector & parameters) { 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); + return CR_WRONG_USAGE; - 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') - { - CoreSuspender guard; - if(Maps::IsValid()) - Screen::show(dts::make_unique(), plugin_self); - } - else if (cmd == 'p' || cmd == 'P') - { - CoreSuspender guard; - if(Maps::IsValid()) - Screen::show(dts::make_unique(), plugin_self); - } - else if (cmd == 'r' || cmd == 'R') - { - CoreSuspender guard; - load_config(); - } - else - { - show_help = true; - } + auto cmd = parameters[0][0]; + if (cmd == 's' || cmd == 'S') { + CoreSuspender guard; + if(Maps::IsValid()) + Screen::show(dts::make_unique(), plugin_self); } - - if (show_help) + else if (cmd == 'p' || cmd == 'P') { + CoreSuspender guard; + if(Maps::IsValid()) + Screen::show(dts::make_unique(), plugin_self); + } + else return CR_WRONG_USAGE; return CR_OK; } -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) +DFhackCExport command_result plugin_init(color_ostream &, std::vector &commands) { activity_labels[JOB_IDLE] = "Idle"; activity_labels[JOB_MILITARY] = "Military Duty"; @@ -2079,27 +1815,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector = 0 and self.opts.x or gps.dimx + self.opts.x - local y = self.opts.y >= 0 and self.opts.y or gps.dimy + self.opts.y - if self.opts.anchor == 'right' then - x = x - (self:get_width() or 0) + 1 - end - return x, y -end -function Widget:render() - if monitor_state(self.opts.type) == false then - return - end - self:update() - local x, y = self:get_pos() - local p = gui.Painter.new_xy(x, y, gps.dimx - 1, y) - self:render_body(p) -end -function Widget:update() end -function Widget:get_width() end -function Widget:render_body() end +-- -------------- +-- Widget_weather +-- -------------- -Widget_weather = defclass(Widget_weather, Widget) +Widget_weather = defclass(Widget_weather, overlay.OverlayWidget) -function Widget_weather:update() - self.counts = get_weather_counts() +function Widget_weather:init() + self.rain = false + self.snow = false end -function Widget_weather:get_width() - if self.counts.rain > 0 then - if self.counts.snow > 0 then - return 9 +function Widget_weather:overlay_onupdate() + local rain, snow = false, false + local cw = df.global.current_weather + for i=0,4 do + for j=0,4 do + weather = cw[i][j] + if weather == df.weather_type.Rain then self.rain = true end + if weather == df.weather_type.Snow then self.snow = true end end - return 4 - elseif self.counts.snow > 0 then - return 4 end - return 0 + self.frame.w = (rain and 4 or 0) + (snow and 4 or 0) + + ((snow and rain) and 1 or 0) + self.rain, self.snow = rain, snow end -function Widget_weather:render_body(p) - if self.counts.rain > 0 then - p:string('Rain', COLOR_LIGHTBLUE):advance(1) - end - if self.counts.snow > 0 then - p:string('Snow', COLOR_WHITE) - end +function Widget_weather:onRenderBody(dc) + if self.rain then dc:string('Rain', COLOR_LIGHTBLUE):advance(1) end + if self.snow then dc:string('Snow', COLOR_WHITE) end end -Widget_date = defclass(Widget_date, Widget) -Widget_date.ATTRS = { - output = '' -} +-- ----------- +-- Widget_date +-- ----------- -function Widget_date:update() - if not self.opts.format then - self.opts.format = 'Y-M-D' +local function get_date_format() + local ok, config = pcall(json.decode_file, DWARFMONITOR_CONFIG_FILE) + if not ok or not config.date_format then + return 'Y-M-D' end + return config.date_format +end + +Widget_date = defclass(Widget_date, overlay.OverlayWidget) + +function Widget_date:init() + self.datestr = '' + self.fmt = get_date_format() +end + +function Widget_date:overlay_onupdate() local year = dfhack.world.ReadCurrentYear() local month = dfhack.world.ReadCurrentMonth() + 1 local day = dfhack.world.ReadCurrentDay() - self.output = 'Date:' - for i = 1, #self.opts.format do - local c = self.opts.format:sub(i, i) + + local fmt = self.fmt + local datestr = 'Date:' + for i=1,#fmt do + local c = fmt:sub(i, i) if c == 'y' or c == 'Y' then - self.output = self.output .. year + datestr = datestr .. year elseif c == 'm' or c == 'M' then if c == 'M' and month < 10 then - self.output = self.output .. '0' + datestr = datestr .. '0' end - self.output = self.output .. month + datestr = datestr .. month elseif c == 'd' or c == 'D' then if c == 'D' and day < 10 then - self.output = self.output .. '0' + datestr = datestr .. '0' end - self.output = self.output .. day + datestr = datestr .. day else - self.output = self.output .. c + datestr = datestr .. c end end -end -function Widget_date:get_width() - return #self.output + self.frame.w = #datestr + self.datestr = datestr end -function Widget_date:render_body(p) - p:string(self.output, COLOR_GREY) +function Widget_date:onRenderBody(dc) + dc:string(self.datestr, COLOR_GREY) end -Widget_misery = defclass(Widget_misery, Widget) +-- ------------- +-- Widget_misery +-- ------------- + +Widget_misery = defclass(Widget_misery, overlay.OverlayWidget) -function Widget_misery:update() - self.data = get_misery_data() +function Widget_misery:init() + self.colors = getStressCategoryColors() + self.stress_category_counts = {} end -function Widget_misery:get_width() - local w = 2 + 6 - for k, v in pairs(self.data) do - w = w + #tostring(v.value) +function Widget_misery:overlay_onupdate() + local counts, num_colors = {}, #self.colors + for _,unit in ipairs(df.global.world.units.active) do + local stress_category = math.min(num_colors, + dfhack.units.getStressCategory(unit)) + counts[stress_category] = (counts[stress_category] or 0) + 1 end - return w -end -function Widget_misery:render_body(p) - p:string("H:", COLOR_WHITE) - for i = 0, 6 do - local v = self.data[i] - p:string(tostring(v.value), v.color) - if not v.last then - p:string("/", COLOR_WHITE) - end + local width = 2 + num_colors - 1 -- 'H:' plus the slashes + for i=1,num_colors do + width = width + #tostring(counts[i] or 0) end -end -Widget_cursor = defclass(Widget_cursor, Widget) + self.stress_category_counts = counts + self.frame.w = width +end -function Widget_cursor:update() - if gps.mouse_x == -1 and not self.opts.show_invalid then - self.output = '' - return +function Widget_misery:onRenderBody(dc) + dc:string('H:', COLOR_WHITE) + local counts = self.stress_category_counts + for i,color in ipairs(self.colors) do + dc:string(tostring(counts[i] or 0), color) + if i < #self.colors then dc:string('/', COLOR_WHITE) end end - self.output = (self.opts.format or '(x,y)'):gsub('[xX]', gps.mouse_x):gsub('[yY]', gps.mouse_y) end -function Widget_cursor:get_width() - return #self.output -end +-- ------------- +-- Widget_cursor +-- ------------- -function Widget_cursor:render_body(p) - p:string(self.output) -end +Widget_cursor = defclass(Widget_cursor, overlay.OverlayWidget) -function render_all() - for _, w in pairs(widgets) do - w:render() - end -end +function Widget_cursor:onRenderBody(dc) + local screenx, screeny = dfhack.screen.getMousePos() + local mouse_map = dfhack.gui.getMousePos() + local keyboard_map = guidm.getCursorPos() -function load_config() - config = require('json').decode_file('dfhack-config/dwarfmonitor.json') - if not config.widgets then - dmerror('No widgets enabled') + local text = {} + table.insert(text, ('mouse UI grid (%d,%d)'):format(screenx, screeny)) + if mouse_map then + table.insert(text, ('mouse map coord (%d,%d,%d)') + :format(mouse_map.x, mouse_map.y, mouse_map.z)) end - if type(config.widgets) ~= 'table' then - dmerror('"widgets" is not a table') + if keyboard_map then + table.insert(text, ('kbd cursor coord (%d,%d,%d)') + :format(keyboard_map.x, keyboard_map.y, keyboard_map.z)) end - widgets = {} - for _, opts in pairs(config.widgets) do - if type(opts) ~= 'table' then dmerror('"widgets" is not an array') end - if not opts.type then dmerror('Widget missing type field') end - local cls = _ENV['Widget_' .. opts.type] - if not cls then - dmerror('Invalid widget type: ' .. opts.type) - end - table.insert(widgets, cls(opts)) + local width = 0 + for i,line in ipairs(text) do + dc:seek(0, i-1):string(line) + width = math.max(width, #line) end + self.frame.w = width + self.frame.h = #text end return _ENV