migrate dwarfmonitor widgets to overlay v2

develop
myk002 2022-11-02 12:40:18 -07:00
parent 63410d63c7
commit 2cf6767589
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
7 changed files with 201 additions and 467 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"
}
]
} }

@ -0,0 +1,12 @@
{
"pos": {
"x": 2,
"y": 2
},
"provider": "dwarfmonitor",
"class": "Widget_cursor",
"viewscreens": [
"dungeonmode",
"dwarfmode"
]
}

@ -0,0 +1,11 @@
{
"pos": {
"x": -16,
"y": 1
},
"provider": "dwarfmonitor",
"class": "Widget_date",
"viewscreens": [
"dwarfmode"
]
}

@ -0,0 +1,11 @@
{
"pos": {
"x": -2,
"y": -1
},
"provider": "dwarfmonitor",
"class": "Widget_misery",
"viewscreens": [
"dwarfmode"
]
}

@ -0,0 +1,11 @@
{
"pos": {
"x": 15,
"y": -1
},
"provider": "dwarfmonitor",
"class": "Widget_weather",
"viewscreens": [
"dwarfmode"
]
}

@ -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;
}
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;
}
} }
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)
{ return CR_OK;
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;
}
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; CoreSuspender guard;
} if(Maps::IsValid())
else Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
{
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;
}
} }
else if (cmd == 'p' || cmd == 'P') {
if (show_help) CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self);
}
else
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;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out)
{
dm_lua::cleanup();
return CR_OK; 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) { if (event == SC_MAP_LOADED)
case SC_MAP_LOADED:
reset(); reset();
break;
default:
break;
}
return CR_OK; return CR_OK;
} }

@ -1,175 +1,160 @@
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)) -- Widget_weather
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
Widget_weather = defclass(Widget_weather, Widget) Widget_weather = defclass(Widget_weather, overlay.OverlayWidget)
function Widget_weather:update() function Widget_weather:init()
self.counts = get_weather_counts() self.rain = false
self.snow = false
end end
function Widget_weather:get_width() function Widget_weather:overlay_onupdate()
if self.counts.rain > 0 then local rain, snow = false, false
if self.counts.snow > 0 then local cw = df.global.current_weather
return 9 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 end
return 4
elseif self.counts.snow > 0 then
return 4
end 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 end
function Widget_weather:render_body(p) function Widget_weather:onRenderBody(dc)
if self.counts.rain > 0 then if self.rain then dc:string('Rain', COLOR_LIGHTBLUE):advance(1) end
p:string('Rain', COLOR_LIGHTBLUE):advance(1) if self.snow then dc:string('Snow', COLOR_WHITE) end
end
if self.counts.snow > 0 then
p:string('Snow', COLOR_WHITE)
end
end end
Widget_date = defclass(Widget_date, Widget) -- -----------
Widget_date.ATTRS = { -- Widget_date
output = '' -- -----------
}
function Widget_date:update() local function get_date_format()
if not self.opts.format then local ok, config = pcall(json.decode_file, DWARFMONITOR_CONFIG_FILE)
self.opts.format = 'Y-M-D' if not ok or not config.date_format then
return 'Y-M-D'
end 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 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 Widget_date:onRenderBody(dc)
p:string(self.output, COLOR_GREY) dc:string(self.datestr, COLOR_GREY)
end end
Widget_misery = defclass(Widget_misery, Widget) -- -------------
-- Widget_misery
-- -------------
Widget_misery = defclass(Widget_misery, overlay.OverlayWidget)
function Widget_misery:update() function Widget_misery:init()
self.data = get_misery_data() self.colors = getStressCategoryColors()
self.stress_category_counts = {}
end end
function Widget_misery:get_width() function Widget_misery:overlay_onupdate()
local w = 2 + 6 local counts, num_colors = {}, #self.colors
for k, v in pairs(self.data) do for _,unit in ipairs(df.global.world.units.active) do
w = w + #tostring(v.value) local stress_category = math.min(num_colors,
dfhack.units.getStressCategory(unit))
counts[stress_category] = (counts[stress_category] or 0) + 1
end end
return w
end
function Widget_misery:render_body(p) local width = 2 + num_colors - 1 -- 'H:' plus the slashes
p:string("H:", COLOR_WHITE) for i=1,num_colors do
for i = 0, 6 do width = width + #tostring(counts[i] or 0)
local v = self.data[i]
p:string(tostring(v.value), v.color)
if not v.last then
p:string("/", COLOR_WHITE)
end
end end
end
Widget_cursor = defclass(Widget_cursor, Widget) self.stress_category_counts = counts
self.frame.w = width
end
function Widget_cursor:update() function Widget_misery: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 -- Widget_cursor
end -- -------------
function Widget_cursor:render_body(p) Widget_cursor = defclass(Widget_cursor, overlay.OverlayWidget)
p:string(self.output)
end
function render_all() function Widget_cursor: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
return _ENV return _ENV