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": [
{
"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"
}

@ -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_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,160 @@
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
-- --------------
-- 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