Merge pull request #2366 from myk002/myk_overlay_dwarfmonitor

[dwarfmonitor] migrate widgets to overlay v2
develop
Myk 2022-11-14 16:50:18 -08:00 committed by GitHub
commit 3cf14610f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 203 additions and 534 deletions

@ -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"
}

@ -53,6 +53,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `blueprint`: generate meta blueprints to reduce the number of blueprints you have to apply
- `blueprint`: support splitting the output file into phases grouped by when they can be applied
- `blueprint`: when splitting output files, number them so they sort into the order you should apply them in
- `dwarfmonitor`: widgets have been ported to the overlay framework and can be enabled and configured via the overlay command
- `ls`: indent tag listings and wrap them in the right column for better readability
- `ls`: new ``--exclude`` option for hiding matched scripts from the output. this can be especially useful for modders who don't want their mod scripts to be included in ``ls`` output.
- `digtype`: new ``-z`` option for digtype to restrict designations to the current z-level and down

@ -2,7 +2,7 @@ dwarfmonitor
============
.. dfhack-tool::
:summary: Measure fort happiness and efficiency.
:summary: Report on dwarf preferences and efficiency.
:tags: fort inspection jobs units
It can also show heads-up display widgets with live fort statistics.
@ -11,84 +11,39 @@ Usage
-----
``enable dwarfmonitor``
Enable the plugin.
``dwarfmonitor enable <mode>``
Start tracking a specific facet of fortress life. The ``mode`` can be
"work", "misery", "date", "weather", or "all". This will show the
corresponding on-screen widgets, if applicable.
``dwarfmonitor disable <mode>``
Stop monitoring ``mode`` and disable corresponding widgets.
Enable tracking of job efficiency for display on the ``dwarfmonitor stats``
screen.
``dwarfmonitor stats``
Show statistics summary.
Show statistics and efficiency summary.
``dwarfmonitor prefs``
Show summary of dwarf preferences.
``dwarfmonitor reload``
Reload the widget configuration file (``dfhack-config/dwarfmonitor.json``).
Show a summary of preferences for dwarves in your fort.
Widget configuration
--------------------
The following types of widgets (defined in
:file:`hack/lua/plugins/dwarfmonitor.lua`) can be displayed on the main fortress
mode screen:
The following widgets are registered for display on the main fortress mode
screen with the `overlay` framework:
``misery``
Show overall happiness levels of all dwarves.
``date``
``dwarfmonitor.cursor``
Show the current keyboard and mouse cursor positions.
``dwarfmonitor.date``
Show the in-game date.
``weather``
``dwarfmonitor.misery``
Show overall happiness levels of all dwarves.
``dwarfmonitor.weather``
Show current weather (e.g. rain/snow).
``cursor``
Show the current mouse cursor position.
The file :file:`dfhack-config/dwarfmonitor.json` can be edited to control the
positions and settings of all widgets. This file should contain a JSON object
with the key ``widgets`` containing an array of objects:
.. code-block:: lua
{
"widgets": [
{
"type": "widget type (weather, misery, etc.)",
"x": X coordinate,
"y": Y coordinate
<...additional options...>
}
]
}
X and Y coordinates begin at zero (in the upper left corner of the screen).
Negative coordinates will be treated as distances from the lower right corner,
beginning at 1 - e.g. an x coordinate of 0 is the leftmost column, while an x
coordinate of -1 is the rightmost column.
By default, the x and y coordinates given correspond to the leftmost tile of
the widget. Including an ``anchor`` option set to ``right`` will cause the
rightmost tile of the widget to be located at this position instead.
Some widgets support additional options:
* ``date`` widget:
* ``format``: specifies the format of the date. The following characters
are replaced (all others, such as punctuation, are not modified)
* ``Y`` or ``y``: The current year
* ``M``: The current month, zero-padded if necessary
* ``m``: The current month, *not* zero-padded
* ``D``: The current day, zero-padded if necessary
* ``d``: The current day, *not* zero-padded
They can be enabled or disable via the `overlay` command.
The default date format is ``Y-M-D``, per the ISO8601_ standard.
The :file:`dfhack-config/dwarfmonitor.json` file can be edited to specify the
format for the ``dwarfmonitor.date`` widget:
.. _ISO8601: https://en.wikipedia.org/wiki/ISO_8601
* ``Y`` or ``y``: The current year
* ``M``: The current month, zero-padded if necessary
* ``m``: The current month, *not* zero-padded
* ``D``: The current day, zero-padded if necessary
* ``d``: The current day, *not* zero-padded
* ``cursor`` widget:
The default date format is ``Y-M-D``, per the ISO8601_ standard.
* ``format``: Specifies the format. ``X``, ``x``, ``Y``, and ``y`` are
replaced with the corresponding cursor coordinates, while all other
characters are unmodified.
* ``show_invalid``: If set to ``true``, the mouse coordinates will both be
displayed as ``-1`` when the cursor is outside of the DF window; otherwise,
nothing will be displayed.
.. _ISO8601: https://en.wikipedia.org/wiki/ISO_8601

@ -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<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,
@ -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<df::interface_key> *input)
{
void feed(set<df::interface_key> *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<size_t> preferences_column;
@ -1762,13 +1663,11 @@ private:
vector<preference_map> 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<ViewscreenFortStats>(), 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 <string> & parameters)
{
bool show_help = false;
static command_result dwarfmonitor_cmd(color_ostream &, vector <string> & 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<ViewscreenFortStats>(), plugin_self);
}
else if (cmd == 'p' || cmd == 'P')
{
CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenPreferences>(), 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<ViewscreenFortStats>(), plugin_self);
}
if (show_help)
else if (cmd == 'p' || cmd == 'P') {
CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self);
}
else
return CR_WRONG_USAGE;
return CR_OK;
}
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands)
DFhackCExport command_result plugin_init(color_ostream &, std::vector <PluginCommand> &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 <Plugin
"Measure fort happiness and efficiency.",
dwarfmonitor_cmd));
dm_lua::state=Lua::Core::State;
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)
DFhackCExport command_result plugin_onstatechange(color_ostream &, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
if (event == SC_MAP_LOADED)
reset();
break;
default:
break;
}
return CR_OK;
}

@ -1,175 +1,184 @@
local _ENV = mkmodule('plugins.dwarfmonitor')
local gps = df.global.gps
local gui = require 'gui'
local json = require('json')
local guidm = require('gui.dwarfmode')
local overlay = require('plugins.overlay')
config = {}
widgets = {}
local DWARFMONITOR_CONFIG_FILE = 'dfhack-config/dwarfmonitor.json'
function dmerror(desc)
qerror('dwarfmonitor: ' .. tostring(desc))
end
Widget = defclass(Widget)
function Widget:init(opts)
self.opts = opts
end
function Widget:get_pos()
local x = self.opts.x >= 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
-- ------------- --
-- WeatherWidget --
-- ------------- --
Widget_weather = defclass(Widget_weather, Widget)
WeatherWidget = defclass(WeatherWidget, overlay.OverlayWidget)
WeatherWidget.ATTRS{
default_pos={x=15,y=-1},
viewscreens={'dungeonmode', 'dwarfmode'},
}
function Widget_weather:update()
self.counts = get_weather_counts()
function WeatherWidget: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 WeatherWidget: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 rain = true end
if weather == df.weather_type.Snow then 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)
function WeatherWidget: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
-- ---------- --
-- DateWidget --
-- ---------- --
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, Widget)
Widget_date.ATTRS = {
output = ''
DateWidget = defclass(DateWidget, overlay.OverlayWidget)
DateWidget.ATTRS{
default_pos={x=-16,y=1},
viewscreens={'dungeonmode', 'dwarfmode'},
}
function Widget_date:update()
if not self.opts.format then
self.opts.format = 'Y-M-D'
end
function DateWidget:init()
self.datestr = ''
self.fmt = get_date_format()
end
function DateWidget: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 DateWidget:onRenderBody(dc)
dc:string(self.datestr, COLOR_GREY)
end
Widget_misery = defclass(Widget_misery, Widget)
-- ------------ --
-- MiseryWidget --
-- ------------ --
MiseryWidget = defclass(MiseryWidget, overlay.OverlayWidget)
MiseryWidget.ATTRS{
default_pos={x=-2,y=-1},
viewscreens={'dwarfmode'},
}
function Widget_misery:update()
self.data = get_misery_data()
function MiseryWidget: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 MiseryWidget: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 MiseryWidget: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
-- ------------ --
-- CursorWidget --
-- ------------ --
function Widget_cursor:render_body(p)
p:string(self.output)
end
CursorWidget = defclass(CursorWidget, overlay.OverlayWidget)
CursorWidget.ATTRS{
default_pos={x=2,y=-4},
viewscreens={'dungeonmode', 'dwarfmode'},
}
function render_all()
for _, w in pairs(widgets) do
w:render()
end
end
function CursorWidget: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
-- register our widgets with the overlay
OVERLAY_WIDGETS = {
cursor=CursorWidget,
date=DateWidget,
misery=MiseryWidget,
weather=WeatherWidget,
}
return _ENV