Reimplement dwarfmonitor widgets in Lua (and improve customizability)

Widget positions and a few other options (e.g. date formats) can be
specified in dfhack-config/dwarfmonitor.json on a per-instance basis.

Related changes:
* Fixed an issue loading JSON files from Lua
* JSON files in dfhack-config (only dwarfmonitor.json currently) are
  no longer copied into the DF directory when building DFHack. This
  keeps developers' personal settings intact, but will require
  copying over changes made to DFHack's copies manually.
* Fixed incorrect config path in dwarfmonitor help
develop
lethosor 2015-06-13 17:13:09 -04:00
parent 06cf6530fc
commit 93c9a41a3a
6 changed files with 308 additions and 118 deletions

@ -174,8 +174,6 @@ IF(BUILD_LIBRARY)
install(DIRECTORY images DESTINATION ${DFHACK_USERDOC_DESTINATION}) install(DIRECTORY images DESTINATION ${DFHACK_USERDOC_DESTINATION})
endif() endif()
install(DIRECTORY dfhack-config DESTINATION .)
#build the plugins #build the plugins
IF(BUILD_PLUGINS) IF(BUILD_PLUGINS)
add_subdirectory (plugins) add_subdirectory (plugins)

@ -1,3 +1,21 @@
{ {
"date_format": "y-m-d" "widgets": [
{
"type": "weather",
"x": 1,
"y": -1
},
{
"type": "date",
"x": -30,
"y": 0,
"format": "Y-M-D"
},
{
"type": "misery",
"x": -2,
"y": -1,
"anchor": "right"
}
]
} }

@ -36,7 +36,7 @@ function decode_file(path, ...)
if not f then if not f then
error('Could not open ' .. path) error('Could not open ' .. path)
end end
local raw = f:read('*all') local contents = f:read('*all')
f:close() f:close()
return decode(contents, ...) return decode(contents, ...)
end end

@ -112,7 +112,7 @@ if (BUILD_SUPPORTED)
DFHACK_PLUGIN(digFlood digFlood.cpp) DFHACK_PLUGIN(digFlood digFlood.cpp)
add_subdirectory(diggingInvaders) add_subdirectory(diggingInvaders)
DFHACK_PLUGIN(drybuckets drybuckets.cpp) DFHACK_PLUGIN(drybuckets drybuckets.cpp)
DFHACK_PLUGIN(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES jsonxx) DFHACK_PLUGIN(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES jsonxx lua)
DFHACK_PLUGIN(embark-tools embark-tools.cpp) DFHACK_PLUGIN(embark-tools embark-tools.cpp)
DFHACK_PLUGIN(eventful eventful.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(eventful eventful.cpp LINK_LIBRARIES lua)
DFHACK_PLUGIN(fastdwarf fastdwarf.cpp) DFHACK_PLUGIN(fastdwarf fastdwarf.cpp)

@ -10,6 +10,8 @@
#include "df/misc_trait_type.h" #include "df/misc_trait_type.h"
#include "df/unit_misc_trait.h" #include "df/unit_misc_trait.h"
#include "LuaTools.h"
#include "LuaWrapper.h"
#include "modules/Gui.h" #include "modules/Gui.h"
#include "modules/Units.h" #include "modules/Units.h"
#include "modules/Translation.h" #include "modules/Translation.h"
@ -96,6 +98,8 @@ static color_value monitor_colors[] =
static int get_happiness_cat(df::unit *unit) static int get_happiness_cat(df::unit *unit)
{ {
if (!unit || !unit->status.current_soul)
return 3;
int stress = unit->status.current_soul->personality.stress_level; int stress = unit->status.current_soul->personality.stress_level;
if (stress >= 500000) if (stress >= 500000)
return 0; return 0;
@ -150,6 +154,113 @@ static void move_cursor(df::coord &pos)
static void open_stats_srceen(); static void open_stats_srceen();
namespace dm_lua {
static color_ostream_proxy *out;
typedef int(*initializer)(lua_State*);
int no_args (lua_State *L) { return 0; }
void cleanup()
{
if (out)
{
delete out;
out = NULL;
}
}
bool init_call (lua_State *L, const char *func)
{
if (!out)
out = new color_ostream_proxy(Core::getInstance().getConsole());
return Lua::PushModulePublic(*out, L, "plugins.dwarfmonitor", func);
}
bool safe_call (lua_State *L, int nargs)
{
return Lua::SafeCall(*out, L, nargs, 0);
}
bool call (const char *func, initializer init = no_args)
{
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!init_call(L, func))
return false;
int nargs = init(L);
return safe_call(L, nargs);
}
template <typename KeyType, typename ValueType>
void table_set (lua_State *L, KeyType k, ValueType v)
{
Lua::Push(L, k);
Lua::Push(L, v);
lua_settable(L, -3);
}
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) dm_lua::table_set(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);
dm_lua::table_set(L, "value", misery[i]);
dm_lua::table_set(L, "color", monitor_colors[i]);
dm_lua::table_set(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 {
DM_LUA_CMD(monitor_state),
DM_LUA_CMD(get_weather_counts),
DM_LUA_CMD(get_misery_data),
DFHACK_LUA_END
};
#define JOB_IDLE -1 #define JOB_IDLE -1
#define JOB_UNKNOWN -2 #define JOB_UNKNOWN -2
#define JOB_MILITARY -3 #define JOB_MILITARY -3
@ -1699,97 +1810,7 @@ struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest
if (Maps::IsValid()) if (Maps::IsValid())
{ {
if (monitor_misery) dm_lua::call("render_all");
{
string entries[7];
size_t length = 9;
for (int i = 0; i < 7; i++)
{
entries[i] = int_to_string(misery[i]);
length += entries[i].length();
}
int x = gps->dimx - length;
int y = gps->dimy - 1;
OutputString(COLOR_WHITE, x, y, "H:");
for (int i = 0; i < 7; i++)
{
OutputString(monitor_colors[i], x, y, entries[i]);
if (i < 6)
OutputString(COLOR_WHITE, x, y, "/");
}
}
if (monitor_date)
{
int x = gps->dimx - 30;
int y = 0;
ostringstream date_str;
auto month = World::ReadCurrentMonth() + 1;
auto day = World::ReadCurrentDay();
date_str << "Date:";
for (size_t i = 0; i < dwarfmonitor_config.date_format.size(); i++)
{
char c = dwarfmonitor_config.date_format[i];
switch (c)
{
case 'Y':
case 'y':
date_str << World::ReadCurrentYear();
break;
case 'M':
case 'm':
date_str << ((month < 10) ? "0" : "") << month;
break;
case 'D':
case 'd':
date_str << ((day < 10) ? "0" : "") << day;
break;
default:
date_str << c;
break;
}
}
OutputString(COLOR_GREY, x, y, date_str.str());
}
if (monitor_weather)
{
int x = 1;
int y = gps->dimy - 1;
bool rain = false,
snow = false;
if (current_weather)
{
int i, j;
for (i = 0; i < 5; ++i)
{
for (j = 0; j < 5; ++j)
{
switch ((*current_weather)[i][j])
{
case weather_type::Rain:
rain = true;
break;
case weather_type::Snow:
snow = true;
break;
default:
break;
}
}
}
}
if (rain)
{
OutputString(COLOR_LIGHTBLUE, x, y, "Rain");
++x;
}
if (snow)
OutputString(COLOR_WHITE, x, y, "Snow");
}
} }
} }
}; };
@ -1811,17 +1832,17 @@ static bool set_monitoring_mode(const string &mode, const bool &state)
if (!monitor_jobs) if (!monitor_jobs)
reset(); reset();
} }
else if (mode == "misery" || mode == "all") if (mode == "misery" || mode == "all")
{ {
mode_recognized = true; mode_recognized = true;
monitor_misery = state; monitor_misery = state;
} }
else if (mode == "date" || mode == "all") if (mode == "date" || mode == "all")
{ {
mode_recognized = true; mode_recognized = true;
monitor_date = state; monitor_date = state;
} }
else if (mode == "weather" || mode == "all") if (mode == "weather" || mode == "all")
{ {
mode_recognized = true; mode_recognized = true;
monitor_weather = state; monitor_weather = state;
@ -1830,11 +1851,15 @@ static bool set_monitoring_mode(const string &mode, const bool &state)
return mode_recognized; return mode_recognized;
} }
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) static bool load_config()
{ {
if (!gps) return dm_lua::call("load_config");
return CR_FAILURE; }
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
if (enable)
load_config();
if (is_enabled != enable) if (is_enabled != enable)
{ {
if (!INTERPOSE_HOOK(dwarf_monitor_hook, feed).apply(enable) || if (!INTERPOSE_HOOK(dwarf_monitor_hook, feed).apply(enable) ||
@ -1848,19 +1873,6 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
return CR_OK; return CR_OK;
} }
static bool load_config (color_ostream &out)
{
jsonxx::Object o;
std::ifstream infile("dfhack-config/dwarfmonitor.json");
if (infile.good())
{
std::string contents((std::istreambuf_iterator<char>(infile)), (std::istreambuf_iterator<char>()));
if (!o.parse(contents))
out.printerr("dwarfmonitor: invalid JSON\n");
}
dwarfmonitor_config.date_format = o.get<jsonxx::String>("date_format", "y-m-d");
}
static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & parameters) static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & parameters)
{ {
bool show_help = false; bool show_help = false;
@ -1913,7 +1925,7 @@ static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & par
} }
else if (cmd == 'r' || cmd == 'R') else if (cmd == 'r' || cmd == 'R')
{ {
load_config(out); load_config();
} }
else else
{ {
@ -1929,7 +1941,6 @@ static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & par
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands)
{ {
load_config(out);
activity_labels[JOB_IDLE] = "Idle"; activity_labels[JOB_IDLE] = "Idle";
activity_labels[JOB_MILITARY] = "Military Duty"; activity_labels[JOB_MILITARY] = "Military Duty";
activity_labels[JOB_LEISURE] = "Leisure"; activity_labels[JOB_LEISURE] = "Leisure";
@ -1962,12 +1973,18 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
"dwarfmonitor prefs\n" "dwarfmonitor prefs\n"
" Show dwarf preferences summary\n\n" " Show dwarf preferences summary\n\n"
"dwarfmonitor reload\n" "dwarfmonitor reload\n"
" Reload configuration file (hack/config/dwarfmonitor.json)\n" " Reload configuration file (dfhack-config/dwarfmonitor.json)\n"
)); ));
return CR_OK; 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 &out, state_change_event event)
{ {
switch (event) { switch (event) {

@ -0,0 +1,157 @@
local _ENV = mkmodule('plugins.dwarfmonitor')
local gps = df.global.gps
local gui = require 'gui'
config = {}
widgets = {}
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 not monitor_state(self.opts.type) 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)
function Widget_weather:update()
self.counts = get_weather_counts()
end
function Widget_weather:get_width()
if self.counts.rain > 0 then
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)
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
end
Widget_date = defclass(Widget_date, Widget)
Widget_date.ATTRS = {
output = ''
}
function Widget_date:update()
if not self.opts.format then
self.opts.format = 'Y-M-D'
end
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)
if c == 'y' or c == 'Y' then
self.output = self.output .. year
elseif c == 'm' or c == 'M' then
if c == 'M' and month < 10 then
self.output = self.output .. '0'
end
self.output = self.output .. month
elseif c == 'd' or c == 'D' then
if c == 'D' and day < 10 then
self.output = self.output .. '0'
end
self.output = self.output .. day
else
self.output = self.output .. c
end
end
end
function Widget_date:get_width()
return #self.output
end
function Widget_date:render_body(p)
p:string(self.output, COLOR_GREY)
end
Widget_misery = defclass(Widget_misery, Widget)
function Widget_misery:update()
self.data = get_misery_data()
end
function Widget_misery:get_width()
local w = 2 + 6
for k, v in pairs(self.data) do
w = w + #tostring(v.value)
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
end
end
function render_all()
for _, w in pairs(widgets) do
w:render()
end
end
function load_config()
config = require('json').decode_file('dfhack-config/dwarfmonitor.json')
if not config.widgets then
dmerror('No widgets enabled')
end
if type(config.widgets) ~= 'table' then
dmerror('"widgets" is not a table')
end
widgets = {}
for _, opts in pairs(config.widgets) do
if type(opts) ~= 'table' then qerror('"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))
end
end
return _ENV