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": [ "date_format": "Y-M-D"
{
"type": "weather",
"x": 22,
"y": -1
},
{
"type": "date",
"x": -30,
"y": 0,
"format": "Y-M-D"
},
{
"type": "misery",
"x": -2,
"y": -1,
"anchor": "right"
}
]
} }

@ -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`: 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`: 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 - `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`: 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. - `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 - `digtype`: new ``-z`` option for digtype to restrict designations to the current z-level and down

@ -2,7 +2,7 @@ dwarfmonitor
============ ============
.. dfhack-tool:: .. dfhack-tool::
:summary: Measure fort happiness and efficiency. :summary: Report on dwarf preferences and efficiency.
:tags: fort inspection jobs units :tags: fort inspection jobs units
It can also show heads-up display widgets with live fort statistics. It can also show heads-up display widgets with live fort statistics.
@ -11,84 +11,39 @@ Usage
----- -----
``enable dwarfmonitor`` ``enable dwarfmonitor``
Enable the plugin. Enable tracking of job efficiency for display on the ``dwarfmonitor stats``
``dwarfmonitor enable <mode>`` screen.
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.
``dwarfmonitor stats`` ``dwarfmonitor stats``
Show statistics summary. Show statistics and efficiency summary.
``dwarfmonitor prefs`` ``dwarfmonitor prefs``
Show summary of dwarf preferences. Show a summary of preferences for dwarves in your fort.
``dwarfmonitor reload``
Reload the widget configuration file (``dfhack-config/dwarfmonitor.json``).
Widget configuration Widget configuration
-------------------- --------------------
The following types of widgets (defined in The following widgets are registered for display on the main fortress mode
:file:`hack/lua/plugins/dwarfmonitor.lua`) can be displayed on the main fortress screen with the `overlay` framework:
mode screen:
``misery`` ``dwarfmonitor.cursor``
Show overall happiness levels of all dwarves. Show the current keyboard and mouse cursor positions.
``date`` ``dwarfmonitor.date``
Show the in-game 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). 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 They can be enabled or disable via the `overlay` command.
* ``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
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 .. _ISO8601: https://en.wikipedia.org/wiki/ISO_8601
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.

@ -52,7 +52,6 @@ using std::deque;
DFHACK_PLUGIN("dwarfmonitor"); DFHACK_PLUGIN("dwarfmonitor");
DFHACK_PLUGIN_IS_ENABLED(is_enabled); DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(current_weather);
REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(ui); 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 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[] = static color_value monitor_colors[] =
{ {
COLOR_LIGHTRED, COLOR_LIGHTRED,
@ -151,102 +138,18 @@ static void move_cursor(df::coord &pos)
static void open_stats_screen(); static void open_stats_screen();
namespace dm_lua { static int getStressCategoryColors(lua_State *L) {
static color_ostream_proxy *out; const size_t n = sizeof(monitor_colors)/sizeof(color_value);
static lua_State *state; lua_createtable(L, n, 0);
typedef int(*initializer)(lua_State*); for (size_t i = 0; i < n; ++i) {
int no_args (lua_State *L) { return 0; } Lua::Push(L, monitor_colors[i]);
void cleanup() lua_rawseti(L, -2, i+1);
{
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; 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 { DFHACK_PLUGIN_LUA_COMMANDS {
DM_LUA_CMD(monitor_state), DFHACK_LUA_COMMAND(getStressCategoryColors),
DM_LUA_CMD(get_weather_counts),
DM_LUA_CMD(get_misery_data),
DFHACK_LUA_END DFHACK_LUA_END
}; };
@ -1648,8 +1551,7 @@ public:
return (selected_column == 1) ? dwarf_column.getFirstSelectedElem() : nullptr; 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; bool key_processed = false;
switch (selected_column) switch (selected_column)
{ {
@ -1723,8 +1625,7 @@ public:
} }
} }
void render() void render() override {
{
using namespace df::enums::interface_key; using namespace df::enums::interface_key;
if (Screen::isDismissed(this)) if (Screen::isDismissed(this))
@ -1751,7 +1652,7 @@ public:
getSelectedUnit() ? COLOR_WHITE : COLOR_DARKGREY); getSelectedUnit() ? COLOR_WHITE : COLOR_DARKGREY);
} }
std::string getFocusString() { return "dwarfmonitor_preferences"; } std::string getFocusString() override { return "dwarfmonitor_preferences"; }
private: private:
ListColumn<size_t> preferences_column; ListColumn<size_t> preferences_column;
@ -1762,13 +1663,11 @@ private:
vector<preference_map> preferences_store; vector<preference_map> preferences_store;
void validateColumn() void validateColumn() {
{
set_to_limit(selected_column, 1); 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); dfhack_viewscreen::resize(x, y);
preferences_column.resize(); preferences_column.resize();
dwarf_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); Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
} }
static void add_work_history(df::unit *unit, activity_type type) static void add_work_history(df::unit *unit, activity_type type) {
{ if (work_history.find(unit) == work_history.end()) {
if (work_history.find(unit) == work_history.end())
{
auto max_history = get_max_history(); auto max_history = get_max_history();
for (int i = 0; i < max_history; i++) for (int i = 0; i < max_history; i++)
work_history[unit].push_back(JOB_UNKNOWN); 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(); 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)) if (Units::getMiscTrait(unit, misc_trait_type::Migrant))
return true; return true;
@ -1805,32 +1700,17 @@ static bool is_at_leisure(df::unit *unit)
return false; return false;
} }
static void reset() static void reset() {
{
work_history.clear(); 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) static void update_dwarf_stats(bool is_paused)
{ {
if (monitor_misery) for (auto unit : world->units.active) {
{
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)) if (!Units::isCitizen(unit))
continue; continue;
if (!DFHack::Units::isActive(unit)) if (!DFHack::Units::isActive(unit)) {
{
auto it = work_history.find(unit); auto it = work_history.find(unit);
if (it != work_history.end()) if (it != work_history.end())
work_history.erase(it); work_history.erase(it);
@ -1838,35 +1718,25 @@ static void update_dwarf_stats(bool is_paused)
continue; continue;
} }
if (monitor_misery) if (is_paused)
{
misery[get_happiness_cat(unit)]++;
}
if (!monitor_jobs || is_paused)
continue; continue;
if (Units::isBaby(unit) || if (Units::isBaby(unit) ||
Units::isChild(unit) || Units::isChild(unit) ||
unit->profession == profession::DRUNK) unit->profession == profession::DRUNK)
{
continue; continue;
}
if (ENUM_ATTR(profession, military, unit->profession)) if (ENUM_ATTR(profession, military, unit->profession)) {
{
add_work_history(unit, JOB_MILITARY); add_work_history(unit, JOB_MILITARY);
continue; continue;
} }
if (!unit->job.current_job) if (!unit->job.current_job) {
{
add_work_history(unit, JOB_IDLE); add_work_history(unit, JOB_IDLE);
continue; continue;
} }
if (is_at_leisure(unit)) if (is_at_leisure(unit)) {
{
add_work_history(unit, JOB_LEISURE); add_work_history(unit, JOB_LEISURE);
continue; continue;
} }
@ -1876,107 +1746,21 @@ static void update_dwarf_stats(bool is_paused)
} }
DFhackCExport command_result plugin_onupdate (color_ostream &out) DFhackCExport command_result plugin_onupdate (color_ostream &out) {
{ if (!is_enabled | !Maps::IsValid())
if (!monitor_jobs && !monitor_misery)
return CR_OK;
if(!Maps::IsValid())
return CR_OK; return CR_OK;
bool is_paused = DFHack::World::ReadPauseState(); bool is_paused = DFHack::World::ReadPauseState();
if (is_paused) if (!is_paused && world->frame_counter % DELTA_TICKS != 0)
{
if (monitor_misery && !misery_upto_date)
misery_upto_date = true;
else
return CR_OK; return CR_OK;
}
else
{
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
}
update_dwarf_stats(is_paused); update_dwarf_stats(is_paused);
return CR_OK; return CR_OK;
} }
struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest DFhackCExport command_result plugin_enable(color_ostream &, bool enable) {
{ if (is_enabled != enable) {
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;
reset(); reset();
is_enabled = enable; is_enabled = enable;
} }
@ -1984,76 +1768,28 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
return CR_OK; return CR_OK;
} }
static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & parameters) static command_result dwarfmonitor_cmd(color_ostream &, vector <string> & parameters) {
{
bool show_help = false;
if (parameters.empty()) if (parameters.empty())
{ return CR_WRONG_USAGE;
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)) auto cmd = parameters[0][0];
{ if (cmd == 's' || cmd == 'S') {
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; CoreSuspender guard;
if(Maps::IsValid()) if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self); Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
} }
else if (cmd == 'p' || cmd == 'P') else if (cmd == 'p' || cmd == 'P') {
{
CoreSuspender guard; CoreSuspender guard;
if(Maps::IsValid()) if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self); Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self);
} }
else if (cmd == 'r' || cmd == 'R')
{
CoreSuspender guard;
load_config();
}
else else
{
show_help = true;
}
}
if (show_help)
return CR_WRONG_USAGE; return CR_WRONG_USAGE;
return CR_OK; 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_IDLE] = "Idle";
activity_labels[JOB_MILITARY] = "Military Duty"; 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.", "Measure fort happiness and efficiency.",
dwarfmonitor_cmd)); dwarfmonitor_cmd));
dm_lua::state=Lua::Core::State;
if (dm_lua::state == NULL)
return CR_FAILURE;
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_shutdown(color_ostream &out) DFhackCExport command_result plugin_onstatechange(color_ostream &, state_change_event event)
{ {
dm_lua::cleanup(); if (event == SC_MAP_LOADED)
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
reset(); reset();
break;
default:
break;
}
return CR_OK; return CR_OK;
} }

@ -1,175 +1,184 @@
local _ENV = mkmodule('plugins.dwarfmonitor') local _ENV = mkmodule('plugins.dwarfmonitor')
local gps = df.global.gps local json = require('json')
local gui = require 'gui' local guidm = require('gui.dwarfmode')
local overlay = require('plugins.overlay')
config = {} local DWARFMONITOR_CONFIG_FILE = 'dfhack-config/dwarfmonitor.json'
widgets = {}
function dmerror(desc) -- ------------- --
qerror('dwarfmonitor: ' .. tostring(desc)) -- WeatherWidget --
end -- ------------- --
WeatherWidget = defclass(WeatherWidget, overlay.OverlayWidget)
WeatherWidget.ATTRS{
default_pos={x=15,y=-1},
viewscreens={'dungeonmode', 'dwarfmode'},
}
Widget = defclass(Widget) function WeatherWidget:init()
function Widget:init(opts) self.rain = false
self.opts = opts self.snow = false
end end
function Widget:get_pos()
local x = self.opts.x >= 0 and self.opts.x or gps.dimx + self.opts.x function WeatherWidget:overlay_onupdate()
local y = self.opts.y >= 0 and self.opts.y or gps.dimy + self.opts.y local rain, snow = false, false
if self.opts.anchor == 'right' then local cw = df.global.current_weather
x = x - (self:get_width() or 0) + 1 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 end
return x, y
end
function Widget:render()
if monitor_state(self.opts.type) == false then
return
end end
self:update() self.frame.w = (rain and 4 or 0) + (snow and 4 or 0) +
local x, y = self:get_pos() ((snow and rain) and 1 or 0)
local p = gui.Painter.new_xy(x, y, gps.dimx - 1, y) self.rain, self.snow = rain, snow
self:render_body(p)
end end
function Widget:update() end
function Widget:get_width() end
function Widget:render_body() end
Widget_weather = defclass(Widget_weather, Widget)
function Widget_weather:update() function WeatherWidget:onRenderBody(dc)
self.counts = get_weather_counts() if self.rain then dc:string('Rain', COLOR_LIGHTBLUE):advance(1) end
if self.snow then dc:string('Snow', COLOR_WHITE) end
end end
function Widget_weather:get_width() -- ---------- --
if self.counts.rain > 0 then -- DateWidget --
if self.counts.snow > 0 then -- ---------- --
return 9
end
return 4
elseif self.counts.snow > 0 then
return 4
end
return 0
end
function Widget_weather:render_body(p) local function get_date_format()
if self.counts.rain > 0 then local ok, config = pcall(json.decode_file, DWARFMONITOR_CONFIG_FILE)
p:string('Rain', COLOR_LIGHTBLUE):advance(1) if not ok or not config.date_format then
end return 'Y-M-D'
if self.counts.snow > 0 then
p:string('Snow', COLOR_WHITE)
end end
return config.date_format
end end
Widget_date = defclass(Widget_date, Widget) DateWidget = defclass(DateWidget, overlay.OverlayWidget)
Widget_date.ATTRS = { DateWidget.ATTRS{
output = '' default_pos={x=-16,y=1},
viewscreens={'dungeonmode', 'dwarfmode'},
} }
function Widget_date:update() function DateWidget:init()
if not self.opts.format then self.datestr = ''
self.opts.format = 'Y-M-D' self.fmt = get_date_format()
end end
function DateWidget:overlay_onupdate()
local year = dfhack.world.ReadCurrentYear() local year = dfhack.world.ReadCurrentYear()
local month = dfhack.world.ReadCurrentMonth() + 1 local month = dfhack.world.ReadCurrentMonth() + 1
local day = dfhack.world.ReadCurrentDay() local day = dfhack.world.ReadCurrentDay()
self.output = 'Date:'
for i = 1, #self.opts.format do local fmt = self.fmt
local c = self.opts.format:sub(i, i) local datestr = 'Date:'
for i=1,#fmt do
local c = fmt:sub(i, i)
if c == 'y' or c == 'Y' then if c == 'y' or c == 'Y' then
self.output = self.output .. year datestr = datestr .. year
elseif c == 'm' or c == 'M' then elseif c == 'm' or c == 'M' then
if c == 'M' and month < 10 then if c == 'M' and month < 10 then
self.output = self.output .. '0' datestr = datestr .. '0'
end end
self.output = self.output .. month datestr = datestr .. month
elseif c == 'd' or c == 'D' then elseif c == 'd' or c == 'D' then
if c == 'D' and day < 10 then if c == 'D' and day < 10 then
self.output = self.output .. '0' datestr = datestr .. '0'
end end
self.output = self.output .. day datestr = datestr .. day
else else
self.output = self.output .. c datestr = datestr .. c
end end
end end
end
function Widget_date:get_width() self.frame.w = #datestr
return #self.output self.datestr = datestr
end end
function Widget_date:render_body(p) function DateWidget:onRenderBody(dc)
p:string(self.output, COLOR_GREY) dc:string(self.datestr, COLOR_GREY)
end end
Widget_misery = defclass(Widget_misery, Widget) -- ------------ --
-- MiseryWidget --
-- ------------ --
function Widget_misery:update() MiseryWidget = defclass(MiseryWidget, overlay.OverlayWidget)
self.data = get_misery_data() MiseryWidget.ATTRS{
end default_pos={x=-2,y=-1},
viewscreens={'dwarfmode'},
}
function Widget_misery:get_width() function MiseryWidget:init()
local w = 2 + 6 self.colors = getStressCategoryColors()
for k, v in pairs(self.data) do self.stress_category_counts = {}
w = w + #tostring(v.value)
end
return w
end end
function Widget_misery:render_body(p) function MiseryWidget:overlay_onupdate()
p:string("H:", COLOR_WHITE) local counts, num_colors = {}, #self.colors
for i = 0, 6 do for _,unit in ipairs(df.global.world.units.active) do
local v = self.data[i] local stress_category = math.min(num_colors,
p:string(tostring(v.value), v.color) dfhack.units.getStressCategory(unit))
if not v.last then counts[stress_category] = (counts[stress_category] or 0) + 1
p:string("/", COLOR_WHITE)
end 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
end
Widget_cursor = defclass(Widget_cursor, Widget) self.stress_category_counts = counts
self.frame.w = width
end
function Widget_cursor:update() function MiseryWidget:onRenderBody(dc)
if gps.mouse_x == -1 and not self.opts.show_invalid then dc:string('H:', COLOR_WHITE)
self.output = '' local counts = self.stress_category_counts
return 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 end
self.output = (self.opts.format or '(x,y)'):gsub('[xX]', gps.mouse_x):gsub('[yY]', gps.mouse_y)
end end
function Widget_cursor:get_width() -- ------------ --
return #self.output -- CursorWidget --
end -- ------------ --
function Widget_cursor:render_body(p) CursorWidget = defclass(CursorWidget, overlay.OverlayWidget)
p:string(self.output) CursorWidget.ATTRS{
end default_pos={x=2,y=-4},
viewscreens={'dungeonmode', 'dwarfmode'},
}
function render_all() function CursorWidget:onRenderBody(dc)
for _, w in pairs(widgets) do local screenx, screeny = dfhack.screen.getMousePos()
w:render() local mouse_map = dfhack.gui.getMousePos()
end local keyboard_map = guidm.getCursorPos()
end
function load_config() local text = {}
config = require('json').decode_file('dfhack-config/dwarfmonitor.json') table.insert(text, ('mouse UI grid (%d,%d)'):format(screenx, screeny))
if not config.widgets then if mouse_map then
dmerror('No widgets enabled') table.insert(text, ('mouse map coord (%d,%d,%d)')
:format(mouse_map.x, mouse_map.y, mouse_map.z))
end end
if type(config.widgets) ~= 'table' then if keyboard_map then
dmerror('"widgets" is not a table') table.insert(text, ('kbd cursor coord (%d,%d,%d)')
:format(keyboard_map.x, keyboard_map.y, keyboard_map.z))
end end
widgets = {} local width = 0
for _, opts in pairs(config.widgets) do for i,line in ipairs(text) do
if type(opts) ~= 'table' then dmerror('"widgets" is not an array') end dc:seek(0, i-1):string(line)
if not opts.type then dmerror('Widget missing type field') end width = math.max(width, #line)
local cls = _ENV['Widget_' .. opts.type]
if not cls then
dmerror('Invalid widget type: ' .. opts.type)
end
table.insert(widgets, cls(opts))
end end
self.frame.w = width
self.frame.h = #text
end end
-- register our widgets with the overlay
OVERLAY_WIDGETS = {
cursor=CursorWidget,
date=DateWidget,
misery=MiseryWidget,
weather=WeatherWidget,
}
return _ENV return _ENV