From 93c9a41a3a0fdf1ac3c177b9c158459a6010701a Mon Sep 17 00:00:00 2001 From: lethosor Date: Sat, 13 Jun 2015 17:13:09 -0400 Subject: [PATCH] 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 --- CMakeLists.txt | 2 - dfhack-config/dwarfmonitor.json | 20 ++- library/lua/json.lua | 2 +- plugins/CMakeLists.txt | 2 +- plugins/dwarfmonitor.cpp | 243 +++++++++++++++++--------------- plugins/lua/dwarfmonitor.lua | 157 +++++++++++++++++++++ 6 files changed, 308 insertions(+), 118 deletions(-) create mode 100644 plugins/lua/dwarfmonitor.lua diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a93db52b..59f0eb1f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -174,8 +174,6 @@ IF(BUILD_LIBRARY) install(DIRECTORY images DESTINATION ${DFHACK_USERDOC_DESTINATION}) endif() -install(DIRECTORY dfhack-config DESTINATION .) - #build the plugins IF(BUILD_PLUGINS) add_subdirectory (plugins) diff --git a/dfhack-config/dwarfmonitor.json b/dfhack-config/dwarfmonitor.json index 2a96eb4c7..3fd365e74 100644 --- a/dfhack-config/dwarfmonitor.json +++ b/dfhack-config/dwarfmonitor.json @@ -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" + } + ] } diff --git a/library/lua/json.lua b/library/lua/json.lua index 2f1bf8238..bcb86537c 100644 --- a/library/lua/json.lua +++ b/library/lua/json.lua @@ -36,7 +36,7 @@ function decode_file(path, ...) if not f then error('Could not open ' .. path) end - local raw = f:read('*all') + local contents = f:read('*all') f:close() return decode(contents, ...) end diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 090effbb2..f5a82e916 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -112,7 +112,7 @@ if (BUILD_SUPPORTED) DFHACK_PLUGIN(digFlood digFlood.cpp) add_subdirectory(diggingInvaders) 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(eventful eventful.cpp LINK_LIBRARIES lua) DFHACK_PLUGIN(fastdwarf fastdwarf.cpp) diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index 5d6ff4b7c..798d4e36d 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -10,6 +10,8 @@ #include "df/misc_trait_type.h" #include "df/unit_misc_trait.h" +#include "LuaTools.h" +#include "LuaWrapper.h" #include "modules/Gui.h" #include "modules/Units.h" #include "modules/Translation.h" @@ -96,6 +98,8 @@ static color_value monitor_colors[] = 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; if (stress >= 500000) return 0; @@ -150,6 +154,113 @@ static void move_cursor(df::coord &pos) 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 + 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_UNKNOWN -2 #define JOB_MILITARY -3 @@ -1699,97 +1810,7 @@ struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest if (Maps::IsValid()) { - if (monitor_misery) - { - 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"); - } + dm_lua::call("render_all"); } } }; @@ -1811,17 +1832,17 @@ static bool set_monitoring_mode(const string &mode, const bool &state) if (!monitor_jobs) reset(); } - else if (mode == "misery" || mode == "all") + if (mode == "misery" || mode == "all") { mode_recognized = true; monitor_misery = state; } - else if (mode == "date" || mode == "all") + if (mode == "date" || mode == "all") { mode_recognized = true; monitor_date = state; } - else if (mode == "weather" || mode == "all") + if (mode == "weather" || mode == "all") { mode_recognized = true; monitor_weather = state; @@ -1830,11 +1851,15 @@ static bool set_monitoring_mode(const string &mode, const bool &state) return mode_recognized; } -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +static bool load_config() { - if (!gps) - return CR_FAILURE; + return dm_lua::call("load_config"); +} +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + if (enable) + load_config(); if (is_enabled != 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; } -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(infile)), (std::istreambuf_iterator())); - if (!o.parse(contents)) - out.printerr("dwarfmonitor: invalid JSON\n"); - } - dwarfmonitor_config.date_format = o.get("date_format", "y-m-d"); -} - static command_result dwarfmonitor_cmd(color_ostream &out, vector & parameters) { bool show_help = false; @@ -1913,7 +1925,7 @@ static command_result dwarfmonitor_cmd(color_ostream &out, vector & par } else if (cmd == 'r' || cmd == 'R') { - load_config(out); + load_config(); } else { @@ -1929,7 +1941,6 @@ static command_result dwarfmonitor_cmd(color_ostream &out, vector & par DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - load_config(out); activity_labels[JOB_IDLE] = "Idle"; activity_labels[JOB_MILITARY] = "Military Duty"; activity_labels[JOB_LEISURE] = "Leisure"; @@ -1962,12 +1973,18 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector = 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