From 89ed9950c7eb6ec4e01307ac06d8a1e3f7d5fc1c Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 24 Apr 2022 22:32:41 -0700 Subject: [PATCH 001/161] Update Gui.h Added parseReportString, which parses a string using '&' as a control character (&r as newline, && as &) and cuts to a certain length w/o splitting words. Added autoDFAnnouncement, which takes a report_init and a string, and handles them like DF does. Added variants to log unprinted announcements and to build the report_init from arguments. Added pauseRecenter, which recenters on an xyz coord (item style, not unit) and optionally pauses, while respecting pause_zoom_no_interface_ms. Added variant that takes a pos. Added recenterViewscreen, which recenters on an xyz coord using a report zoom style (item, unit, generic. revealInDwarfmodeMap calls "unit" style "center". Generic style ignores coords and just enforces valid view bounds.) Added variants that take pos or use current cursor coords. --- library/include/modules/Gui.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 455032fea..098bb8dd3 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -36,6 +36,8 @@ distribution. #include "df/ui.h" #include "df/announcement_type.h" #include "df/announcement_flags.h" +#include "df/report_init.h" +#include "df/report_zoom_type.h" #include "df/unit_report_type.h" #include "modules/GuiHooks.h" @@ -113,6 +115,7 @@ namespace DFHack // Low-level API that gives full control over announcements and reports DFHACK_EXPORT void writeToGamelog(std::string message); + DFHACK_EXPORT bool parseReportString(std::vector &out, const std::string &str, size_t line_length = 73); DFHACK_EXPORT int makeAnnouncement(df::announcement_type type, df::announcement_flags mode, df::coord pos, std::string message, int color = 7, bool bright = true); DFHACK_EXPORT bool addCombatReport(df::unit *unit, df::unit_report_type slot, int report_index); DFHACK_EXPORT bool addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index); @@ -124,6 +127,11 @@ namespace DFHack // Show an announcement with effects determined by announcements.txt DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL); + + // Process an announcement exactly like DF would, which might result in no announcement + DFHACK_EXPORT int autoDFAnnouncement(df::report_init r, std::string message); + DFHACK_EXPORT int autoDFAnnouncement(df::report_init r, std::string message, bool log_failures); + DFHACK_EXPORT int autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool sparring = false, bool log_failures = false); /* * Cursor and window coords @@ -131,6 +139,13 @@ namespace DFHack DFHACK_EXPORT df::coord getViewportPos(); DFHACK_EXPORT df::coord getCursorPos(); + // Recenter the viewscreen, based on DF code for announcements and scrolling + DFHACK_EXPORT void pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause); + DFHACK_EXPORT inline void pauseRecenter(df::coord pos, bool pause) { return pauseRecenter(pos.x, pos.y, pos.z, pause); } + DFHACK_EXPORT void recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom = df::enums::report_zoom_type::Item); + DFHACK_EXPORT inline void recenterViewscreen(df::coord pos, df::report_zoom_type zoom = df::enums::report_zoom_type::Item) { recenterViewscreen(pos.x, pos.y, pos.z, zoom); }; + DFHACK_EXPORT inline void recenterViewscreen(df::report_zoom_type zoom = df::enums::report_zoom_type::Item) { recenterViewscreen(getCursorPos(), zoom); }; + static const int AREA_MAP_WIDTH = 23; static const int MENU_WIDTH = 30; From c7be54dac0f234b6337007f27f4b44ef1927bddd Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 24 Apr 2022 22:45:26 -0700 Subject: [PATCH 002/161] Update Gui.cpp Add reverse engineered functions: parseReportString, autoDFAnnouncement, recenterViewscreen, and pauseRecenter. Add versions of autoDFAnnouncement that don't take a report_init struct and that log unprinted announcements. Add utility functions: recent_report, recent_report_any, delete_old_reports, and check_repeat_report. --- library/modules/Gui.cpp | 431 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 431 insertions(+) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 963b2ecd6..e6b865df0 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -44,6 +44,7 @@ using namespace DFHack; #include "modules/Job.h" #include "modules/Screen.h" #include "modules/Maps.h" +#include "modules/Units.h" #include "DataDefs.h" @@ -70,6 +71,7 @@ using namespace DFHack; #include "df/plant.h" #include "df/popup_message.h" #include "df/report.h" +#include "df/report_zoom_type.h" #include "df/route_stockpile_link.h" #include "df/stop_depart_condition.h" #include "df/ui_advmode.h" @@ -1360,6 +1362,137 @@ DFHACK_EXPORT void Gui::writeToGamelog(std::string message) fseed.close(); } +bool Gui::parseReportString(std::vector &out, const std::string &str, size_t line_length) +{ // out vector will contain strings cut to line_length, avoiding cutting up words + // Reverse-engineered from DF announcement code, fixes applied + + if (str.empty() || line_length == 0) + return false; + out.clear(); + + bool ignore_space = false; + string current_line = ""; + size_t iter = 0; + do + { + if (ignore_space) + { + if (str[iter] == ' ') + continue; + ignore_space = false; + } + + if (str[iter] == '&') // escape character + { + iter++; // ignore the '&' itself + if (iter >= str.length()) + break; + + if (str[iter] == 'r') // "&r" starts new line + { + if (!current_line.empty()) + { + out.push_back(string(current_line)); + current_line = ""; + } + out.push_back(" "); + continue; // don't add 'r' to current_line + } + else if (str[iter] != '&') + { // not "&&", don't add character to current_line + continue; + } + } + + current_line += str[iter]; + if (current_line.length() > line_length) + { + size_t i = current_line.length(); // start of current word + size_t j; // end of previous word + while (--i > 0 && current_line[i] != ' '); // find start of current word + + if (i == 0) + { // need to push at least one char + j = i = line_length; // last char ends up on next line + } + else + { + j = i; + while (j > 1 && current_line[j - 1] == ' ') + j--; // consume excess spaces at the split point + } + out.push_back(current_line.substr(0, j)); // push string before j + + if (current_line[i] == ' ') + i++; // don't keep this space + current_line.erase(0, i); // current_line now starts at last word or is empty + ignore_space = current_line.empty(); // ignore leading spaces on new line + } + } while (++iter < str.length()); + + if (!current_line.empty()) + out.push_back(current_line); + + return true; +} + +namespace +{ // Utility functions for reports + bool recent_report(df::unit *unit, df::unit_report_type slot) + { + if (unit && !unit->reports.log[slot].empty() && + *df::global::cur_year == unit->reports.last_year[slot] && + (*df::global::cur_year_tick - unit->reports.last_year_tick[slot]) <= 500) + { + return true; + } + return false; + } + + bool recent_report_any(df::unit *unit) + { + FOR_ENUM_ITEMS(unit_report_type, slot) + { + if (recent_report(unit, slot)) + return true; + } + return false; + } + + void delete_old_reports() + { + auto &reports = world->status.reports; + while (reports.size() > 3000) + { + if (reports[0] != NULL) + { + if (reports[0]->flags.bits.announcement) + erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); + delete reports[0]; + } + reports.erase(reports.begin()); + } + } + + int32_t check_repeat_report(vector &results) + { + if (*gamemode == game_mode::DWARF && !results.empty() && world->status.reports.size() >= results.size()) + { + auto &reports = world->status.reports; + size_t base = reports.size() - results.size(); // index where a repeat would start + size_t offset = 0; + while (reports[base + offset]->text == results[offset] && ++offset < results.size()); // match each report + + if (offset == results.size()) // all lines matched + { + reports[base]->duration = 100; + return ++(reports[base]->repeat_count); + } + } + return 0; + } +} + DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announcement_flags flags, df::coord pos, std::string message, int color, bool bright) { using df::global::world; @@ -1574,6 +1707,239 @@ void Gui::showAutoAnnouncement( addCombatReportAuto(unit2, flags, id); } +int Gui::autoDFAnnouncement(df::report_init r, string message) +{ // Reverse-engineered from DF announcement code + + if (!world->unk_26a9a8) // TODO: world->show_announcements + return 1; + + df::announcement_flags a_flags; + if (is_valid_enum_item(r.type)) + a_flags = df::global::d_init->announcements.flags[r.type]; + else + return 2; + + if (message.empty()) + { + Core::printerr("Empty announcement %u\n", r.type); // DF would print this to errorlog.txt + return 3; + } + + // Check if the announcement will actually be announced + if (*gamemode == game_mode::ADVENTURE) + { + if (r.pos.x != -30000 && + r.type != announcement_type::CREATURE_SOUND && + r.type != announcement_type::REGULAR_CONVERSATION && + r.type != announcement_type::CONFLICT_CONVERSATION && + r.type != announcement_type::MECHANISM_SOUND) + { // If not sound, make sure we can see pos + if ((world->units.active.empty() || (r.unit1 != world->units.active[0] && r.unit2 != world->units.active[0])) && + ((Maps::getTileDesignation(r.pos)->whole & 0x10) == 0x0)) // Adventure mode uses this bit to determine current visibility + { + return 4; + } + } + } + else + { // Dwarf mode (or arena?) + if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2))) + return 5; + + if (!a_flags.bits.D_DISPLAY) + { + if (a_flags.bits.UNIT_COMBAT_REPORT) + { + if (r.unit1 == NULL && r.unit2 == NULL) + return 6; + } + else + { + if (!a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) + return 7; + if (!recent_report_any(r.unit1) && !recent_report_any(r.unit2)) + return 8; + } + } + } + + if (a_flags.bits.PAUSE || a_flags.bits.RECENTER) + pauseRecenter((a_flags.bits.RECENTER ? r.pos : df::coord()), a_flags.bits.PAUSE); // Does nothing outside dwarf mode + + if (a_flags.bits.DO_MEGA && (*gamemode != game_mode::ADVENTURE || world->units.active.empty() || world->units.active[0]->counters.unconscious <= 0)) + showPopupAnnouncement(message, r.color, r.bright); + + vector results; + size_t line_length = (r.speaker_id == -1) ? (init->display.grid_x - 7) : (init->display.grid_x - 10); + parseReportString(results, message, line_length); + + if (results.empty()) + return 9; + + // Check for repeat report + int32_t repeat_count = check_repeat_report(results); + if (repeat_count > 0) + { + if (a_flags.bits.D_DISPLAY) + { + world->status.display_timer = r.display_timer; + Gui::writeToGamelog("x" + (repeat_count + 1)); + } + return 0; + } + + bool success = false; // only print to gamelog if report was used + size_t new_report_index = world->status.reports.size(); + for (size_t i = 0; i < results.size(); i++) + { // Generate report entries for each line + auto new_report = new df::report(); + new_report->type = r.type; + new_report->text = results[i]; + new_report->color = r.color; + new_report->bright = r.bright; + new_report->flags.whole = 0x0; + new_report->zoom_type = r.zoom_type; + new_report->pos = r.pos; + new_report->zoom_type2 = r.zoom_type2; + new_report->pos2 = r.pos2; + new_report->id = world->status.next_report_id++; + new_report->year = *df::global::cur_year; + new_report->time = *df::global::cur_year_tick; + new_report->unk_v40_1 = r.unk_v40_1; + new_report->unk_v40_2 = r.unk_v40_2; + new_report->speaker_id = r.speaker_id; + world->status.reports.push_back(new_report); + + if (i > 0) + new_report->flags.bits.continuation = true; + + if (*gamemode == game_mode::ADVENTURE && !world->units.active.empty() && world->units.active[0]->counters.unconscious > 0) + new_report->flags.bits.unconscious = true; + + if ((*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && a_flags.bits.D_DISPLAY)) + { + insert_into_vector(world->status.announcements, &df::report::id, new_report); + new_report->flags.bits.announcement = true; + world->status.display_timer = r.display_timer; + success = true; + } + } + + if (*gamemode == game_mode::DWARF) + { + if (a_flags.bits.UNIT_COMBAT_REPORT) + { + if (r.unit1 != NULL) + { + if (r.flags.bits.sparring) // TODO: flags.sparring is inverted + success |= addCombatReport(r.unit1, unit_report_type::Combat, new_report_index); + else if (r.unit1->job.current_job != NULL && r.unit1->job.current_job->job_type == job_type::Hunt) + success |= addCombatReport(r.unit1, unit_report_type::Hunting, new_report_index); + else + success |= addCombatReport(r.unit1, unit_report_type::Sparring, new_report_index); + } + + if (r.unit2 != NULL) + { + if (r.flags.bits.sparring) // TODO: flags.sparring is inverted + success |= addCombatReport(r.unit2, unit_report_type::Combat, new_report_index); + else if (r.unit2->job.current_job != NULL && r.unit2->job.current_job->job_type == job_type::Hunt) + success |= addCombatReport(r.unit2, unit_report_type::Hunting, new_report_index); + else + success |= addCombatReport(r.unit2, unit_report_type::Sparring, new_report_index); + } + } + + if (a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) + { + FOR_ENUM_ITEMS(unit_report_type, slot) + { + if (recent_report(r.unit1, slot)) + success |= addCombatReport(r.unit1, slot, new_report_index); + if (recent_report(r.unit2, slot)) + success |= addCombatReport(r.unit2, slot, new_report_index); + } + } + } + + delete_old_reports(); + + if (/*debug_gamelog &&*/ success) + Gui::writeToGamelog(message); + else if (success) + return 10; + else + return 11; + + return 0; +} + +int Gui::autoDFAnnouncement(df::report_init r, string message, bool log_failures) +{ // Prints info about failed announcements to DFHack console if log_failures is true + int rv = autoDFAnnouncement(r, message); + + if (log_failures) + { + switch (rv) + { + case 0: + break; // success + case 1: + Core::print("Skipped an announcement because world->show_announcements is false:\n%s\n", message.c_str()); + break; + case 2: + Core::printerr("Invalid announcement type!\n"); + break; + case 3: + break; // empty announcement, already handled + case 4: + Core::print("An adventure announcement occured, but nobody heard:\n%s\n", message.c_str()); + break; + case 5: + Core::print("An announcement occured, but nobody heard:\n%s\n", message.c_str()); + break; + case 6: + Core::print("Skipped a UNIT_COMBAT_REPORT because it has no units:\n%s\n", message.c_str()); + break; + case 7: + Core::print("Skipped an announcement not enabled for this game mode:\n%s\n", message.c_str()); + break; + case 8: + Core::print("Skipped an announcement because there's no active report:\n%s\n", message.c_str()); + break; + case 9: + Core::print("Skipped an announcement because it was empty after parsing:\n%s\n", message.c_str()); + break; + case 10: + Core::print("Report added but skipped printing to gamelog.txt because debug_gamelog is false.\n"); + break; + case 11: + Core::print("Report added but didn't qualify to be displayed anywhere:\n%s\n", message.c_str()); + break; + default: + Core::printerr("autoDFAnnouncement: Unexpected return value!\n"); + } + } + return rv; +} + +int Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color, bool bright, df::unit *unit1, df::unit *unit2, bool sparring, bool log_failures) +{ + auto r = df::report_init(); + r.type = type; + r.color = color; + r.bright = bright; + r.pos = pos; + r.unit1 = unit1; + r.unit2 = unit2; + r.flags.bits.sparring = !sparring; // TODO: inverted + + if (Maps::isValidTilePos(pos)) + r.zoom_type = report_zoom_type::Unit; + + return autoDFAnnouncement(r, message, log_failures); +} + df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed) { if (!gview) @@ -1624,6 +1990,71 @@ df::coord Gui::getCursorPos() return df::coord(cursor->x, cursor->y, cursor->z); } +void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom) +{ + // Reverse-engineered from DF announcement code, also used when scrolling + + auto dims = getDwarfmodeViewDims(); + int32_t w = dims.map_x2 - dims.map_x1 + 1; + int32_t h = dims.map_y2 - dims.map_y1 + 1; + int32_t new_win_x, new_win_y, new_win_z; + getViewCoords(new_win_x, new_win_y, new_win_z); + + if (zoom != report_zoom_type::Generic && x != -30000) + { + if (zoom == report_zoom_type::Unit) + { + new_win_x = x - w / 2; + new_win_y = y - h / 2; + } + else // report_zoom_type::Item + { + if (new_win_x > (x - 5)) + new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; + if (new_win_y > (y - 5)) + new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; + if (new_win_x < (x + 5 - w)) + new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; + if (new_win_y < (y + 5 - h)) + new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; + } + + if (new_win_z != z) + ui_sidebar_menus->minimap.need_scan = true; + new_win_z = z; + } + + *df::global::window_x = clip_range(new_win_x, 0, (world->map.x_count - w)); + *df::global::window_y = clip_range(new_win_y, 0, (world->map.y_count - h)); + *df::global::window_z = clip_range(new_win_z, 0, (world->map.z_count - 1)); + + return; +} + +void Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause) +{ + // Reverse-engineered from DF announcement code + + if (*gamemode != game_mode::DWARF) + return; + + resetDwarfmodeView(pause); + if (x != -30000) + { + recenterViewscreen(x, y, z, report_zoom_type::Item); + ui_sidebar_menus->minimap.need_render = true; + ui_sidebar_menus->minimap.need_scan = true; + } + + if (init->input.pause_zoom_no_interface_ms > 0) + { + gview->shutdown_interface_tickcount = Core::getInstance().p->getTickCount(); + gview->shutdown_interface_for_ms = init->input.pause_zoom_no_interface_ms; + } + + return; +} + Gui::DwarfmodeDims getDwarfmodeViewDims_default() { Gui::DwarfmodeDims dims; From 861a0ee85ecf75cb1d35a0ee0dd2e3b2e528bc51 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 24 Apr 2022 22:52:31 -0700 Subject: [PATCH 003/161] Update LuaApi.cpp Wrappers for autoDFAnnouncement, pauseRecenter, and recenterViewscreen. --- library/LuaApi.cpp | 122 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 101b645fd..fcdd96cbf 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -101,6 +101,8 @@ distribution. #include "df/specific_ref.h" #include "df/specific_ref_type.h" #include "df/vermin.h" +#include "df/report_init.h" +#include "df/report_zoom_type.h" #include #include @@ -1505,7 +1507,127 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = { { NULL, NULL } }; +static int gui_autoDFAnnouncement(lua_State *state) +{ + int rv; + df::report_init *r = Lua::GetDFObject(state, 1); + + if (r) + { + std::string message = luaL_checkstring(state, 2); + + if (lua_gettop(state) >= 3) + rv = Gui::autoDFAnnouncement(*r, message, lua_toboolean(state, 3)); + else + rv = Gui::autoDFAnnouncement(*r, message); + } + else + { + df::coord pos; + int color; + bool bright, sparring, log_failures; + df::unit *unit1, *unit2; + + auto type = (df::announcement_type)lua_tointeger(state, 1); + Lua::CheckDFAssign(state, &pos, 2); + std::string message = luaL_checkstring(state, 3); + + switch (lua_gettop(state)) + { + default: + case 9: + log_failures = lua_toboolean(state, 9); + case 8: + sparring = lua_toboolean(state, 8); + case 7: + unit2 = Lua::CheckDFObject(state, 7); + case 6: + unit1 = Lua::CheckDFObject(state, 6); + case 5: + bright = lua_toboolean(state, 5); + case 4: + color = lua_tointeger(state, 4); + case 3: + break; + } + + switch (lua_gettop(state)) + { // Use the defaults in Gui.h + default: + case 9: + rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, sparring, log_failures); + break; + case 8: + rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, sparring); + break; + case 7: + rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2); + break; + case 6: + rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1); + break; + case 5: + rv = Gui::autoDFAnnouncement(type, pos, message, color, bright); + break; + case 4: + rv = Gui::autoDFAnnouncement(type, pos, message, color); + break; + case 3: + rv = Gui::autoDFAnnouncement(type, pos, message); + } + } + + lua_pushinteger(state, rv); + return 1; +} + +static int gui_pauseRecenter(lua_State *state) +{ + if (lua_gettop(state) == 2) + { + df::coord p; + Lua::CheckDFAssign(state, &p, 1); + Gui::pauseRecenter(p, lua_toboolean(state, 2)); + } + else + Gui::pauseRecenter(CheckCoordXYZ(state, 1, false), lua_toboolean(state, 4)); + + return 1; +} + +static int gui_recenterViewscreen(lua_State *state) +{ + df::coord p; + switch (lua_gettop(state)) + { + default: + case 4: + Gui::recenterViewscreen(CheckCoordXYZ(state, 1, false), (df::report_zoom_type)lua_tointeger(state, 4)); + break; + case 3: + Gui::recenterViewscreen(CheckCoordXYZ(state, 1, false)); + break; + case 2: + Lua::CheckDFAssign(state, &p, 1); + Gui::recenterViewscreen(p, (df::report_zoom_type)lua_tointeger(state, 2)); + break; + case 1: + if (lua_type(state, 1) == LUA_TNUMBER) + Gui::recenterViewscreen((df::report_zoom_type)lua_tointeger(state, 1)); + else + Gui::recenterViewscreen(CheckCoordXYZ(state, 1, true)); + break; + case 0: + Gui::recenterViewscreen(); + } + + return 1; +} + static const luaL_Reg dfhack_gui_funcs[] = { + { "autoDFAnnouncement", gui_autoDFAnnouncement }, + { "pauseRecenter", gui_pauseRecenter }, + { "recenterViewscreen", gui_recenterViewscreen }, { "getDwarfmodeViewDims", gui_getDwarfmodeViewDims }, { NULL, NULL } }; From c66448015eeb28e40d4b8c76474c6642d0a39b85 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 25 Apr 2022 00:02:37 -0700 Subject: [PATCH 004/161] Update Lua API.rst Document autoDFAnnouncement, pauseRecenter, and recenterViewscreen. --- docs/Lua API.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index a0764df99..946af7afe 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1004,6 +1004,22 @@ Fortress mode Resets the fortress mode sidebar menus and cursors to their default state. If ``pause`` is true, also pauses the game. +* ``dfhack.gui.pauseRecenter(pos[,pause]) +* ``dfhack.gui.pauseRecenter(x,y,z[,pause]) + + Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``, + and respects RECENTER_INTERFACE_SHUTDOWN_MS in DF's init.txt. + +* ``dfhack.gui.recenterViewscreen(pos[,zoom]) +* ``dfhack.gui.recenterViewscreen(x,y,z[,zoom]) +* ``dfhack.gui.recenterViewscreen([zoom]) + + Recenter the view on a position using a specific zoom type. If no position is + given, recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` + (0 = Generic, 1 = Item, 2 = Unit), where Generic skips recentering and + enforces valid view bounds (the same as x = -30000.) + Default zoom type is Item. + * ``dfhack.gui.revealInDwarfmodeMap(pos)`` Centers the view on the given position, which can be a ``df.coord`` instance @@ -1073,6 +1089,14 @@ Announcements Uses the type to look up options from announcements.txt, and calls the above operations accordingly. The units are used to call ``addCombatReportAuto``. +* ``dfhack.gui.autoDFAnnouncement(report,text[,log_failures]) +* ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures]) + + Takes a ``df.report_init`` and a string and processes them just like DF does. + Sometimes this means the announcement won't occur. Set ``log_failures`` to ``true`` to + log the reason why to the dfhack console (e.g., unrevealed map or wrong gamemode.) + Can also be built from parameters instead of a ``report_init``. + Other ~~~~~ From 709adda9682b9e91f9b0a36ad6bc691639ef6519 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 25 Apr 2022 00:06:53 -0700 Subject: [PATCH 005/161] Update Lua API.rst Fix formatting --- docs/Lua API.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 946af7afe..d4aa284a9 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1004,15 +1004,15 @@ Fortress mode Resets the fortress mode sidebar menus and cursors to their default state. If ``pause`` is true, also pauses the game. -* ``dfhack.gui.pauseRecenter(pos[,pause]) -* ``dfhack.gui.pauseRecenter(x,y,z[,pause]) +* ``dfhack.gui.pauseRecenter(pos[,pause])`` +* ``dfhack.gui.pauseRecenter(x,y,z[,pause])`` Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``, and respects RECENTER_INTERFACE_SHUTDOWN_MS in DF's init.txt. -* ``dfhack.gui.recenterViewscreen(pos[,zoom]) -* ``dfhack.gui.recenterViewscreen(x,y,z[,zoom]) -* ``dfhack.gui.recenterViewscreen([zoom]) +* ``dfhack.gui.recenterViewscreen(pos[,zoom])`` +* ``dfhack.gui.recenterViewscreen(x,y,z[,zoom])`` +* ``dfhack.gui.recenterViewscreen([zoom])`` Recenter the view on a position using a specific zoom type. If no position is given, recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` @@ -1089,8 +1089,8 @@ Announcements Uses the type to look up options from announcements.txt, and calls the above operations accordingly. The units are used to call ``addCombatReportAuto``. -* ``dfhack.gui.autoDFAnnouncement(report,text[,log_failures]) -* ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures]) +* ``dfhack.gui.autoDFAnnouncement(report,text[,log_failures])`` +* ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures])`` Takes a ``df.report_init`` and a string and processes them just like DF does. Sometimes this means the announcement won't occur. Set ``log_failures`` to ``true`` to From ad9a08f1e73ab5afd5a7703460a0fa7065f37ac9 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 25 Apr 2022 00:10:26 -0700 Subject: [PATCH 006/161] Update Lua API.rst Fix formatting (again) --- docs/Lua API.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index d4aa284a9..33d6b0a44 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1005,14 +1005,14 @@ Fortress mode ``pause`` is true, also pauses the game. * ``dfhack.gui.pauseRecenter(pos[,pause])`` -* ``dfhack.gui.pauseRecenter(x,y,z[,pause])`` + ``dfhack.gui.pauseRecenter(x,y,z[,pause])`` Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``, and respects RECENTER_INTERFACE_SHUTDOWN_MS in DF's init.txt. * ``dfhack.gui.recenterViewscreen(pos[,zoom])`` -* ``dfhack.gui.recenterViewscreen(x,y,z[,zoom])`` -* ``dfhack.gui.recenterViewscreen([zoom])`` + ``dfhack.gui.recenterViewscreen(x,y,z[,zoom])`` + ``dfhack.gui.recenterViewscreen([zoom])`` Recenter the view on a position using a specific zoom type. If no position is given, recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` @@ -1090,7 +1090,7 @@ Announcements operations accordingly. The units are used to call ``addCombatReportAuto``. * ``dfhack.gui.autoDFAnnouncement(report,text[,log_failures])`` -* ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures])`` + ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures])`` Takes a ``df.report_init`` and a string and processes them just like DF does. Sometimes this means the announcement won't occur. Set ``log_failures`` to ``true`` to From a47bf1533ed5307b023d75fc2343becc4ff665e8 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 25 Apr 2022 00:17:31 -0700 Subject: [PATCH 007/161] Update changelog.txt --- docs/changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 1a3bf64b4..cae8a7871 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Documentation - Add more examples to the plugin skeleton files so they are more informative for a newbie - Lua API.rst added: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` +- Lua API.rst added: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` - Update download link and installation instructions for Visual C++ 2015 build tools on Windows - Updated information regarding obtaining a compatible Windows build environment - `confirm`: Correct the command name in the plugin help text @@ -70,11 +71,13 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API - Added functions reverse-engineered from ambushing unit code: ``Units::isHidden``, ``Units::isFortControlled``, ``Units::getOuterContainerRef``, ``Items::getOuterContainerRef`` +- Added functions reverse-engineered from announcement code: ``Gui::parseReportString``, ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` ## Lua - ``widgets.FilteredList`` now allows all punctuation to be typed into the filter and can match search keys that start with punctuation. - ``widgets.ListBox``: minimum height of dialog is now calculated correctly when there are no items in the list (e.g. when a filter doesn't match anything) - Lua wrappers for functions reverse-engineered from ambushing unit code: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` +- Lua wrappers for functions reverse-engineered from announcement code: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` - ``dwarfmode.MenuOverlay``: if ``sidebar_mode`` attribute is set, automatically manage entering a specific sidebar mode on show and restoring the previous sidebar mode on dismiss - ``dwarfmode.enterSidebarMode()``: passing ``df.ui_sidebar_mode.DesignateMine`` now always results in you entering ``DesignateMine`` mode and not ``DesignateChopTrees``, even when you looking at the surface where the default designation mode is ``DesignateChopTrees`` - New string class function: ``string:escape_pattern()`` escapes regex special characters within a string From b56d9520e9ac34855a1dea1b4196ff7e07bb3404 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 25 Apr 2022 00:39:05 -0700 Subject: [PATCH 008/161] Fix trailing whitespace --- library/LuaApi.cpp | 2 +- library/modules/Gui.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index fcdd96cbf..16bef7fc7 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1507,7 +1507,7 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = { { NULL, NULL } }; -static int gui_autoDFAnnouncement(lua_State *state) +static int gui_autoDFAnnouncement(lua_State *state) { int rv; df::report_init *r = Lua::GetDFObject(state, 1); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index e6b865df0..ceea6251d 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1991,7 +1991,7 @@ df::coord Gui::getCursorPos() } void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom) -{ +{ // Reverse-engineered from DF announcement code, also used when scrolling auto dims = getDwarfmodeViewDims(); @@ -2032,7 +2032,7 @@ void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_ty } void Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause) -{ +{ // Reverse-engineered from DF announcement code if (*gamemode != game_mode::DWARF) From c89baa5e338c16ba3ea2cc4539e93d5b0d2db742 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 28 Apr 2022 22:40:36 -0700 Subject: [PATCH 009/161] Update Lua API.rst --- docs/Lua API.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 33d6b0a44..bad0481bb 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1008,7 +1008,7 @@ Fortress mode ``dfhack.gui.pauseRecenter(x,y,z[,pause])`` Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``, - and respects RECENTER_INTERFACE_SHUTDOWN_MS in DF's init.txt. + and respects RECENTER_INTERFACE_SHUTDOWN_MS (the delay before input is recognized when a recenter occurs) in DF's init.txt. * ``dfhack.gui.recenterViewscreen(pos[,zoom])`` ``dfhack.gui.recenterViewscreen(x,y,z[,zoom])`` @@ -1016,8 +1016,8 @@ Fortress mode Recenter the view on a position using a specific zoom type. If no position is given, recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` - (0 = Generic, 1 = Item, 2 = Unit), where Generic skips recentering and - enforces valid view bounds (the same as x = -30000.) + (0 = Generic, 1 = Item, 2 = Unit), where ``Generic`` skips recentering and + enforces valid view bounds (the same as x = -30000,) ``Item`` brings the position onscreen, and ``Unit`` centers the screen on the position. Default zoom type is Item. * ``dfhack.gui.revealInDwarfmodeMap(pos)`` @@ -1090,12 +1090,12 @@ Announcements operations accordingly. The units are used to call ``addCombatReportAuto``. * ``dfhack.gui.autoDFAnnouncement(report,text[,log_failures])`` - ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures])`` + ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,not_sparring,log_failures])`` - Takes a ``df.report_init`` and a string and processes them just like DF does. + Takes a ``df.report_init`` (see: https://github.com/DFHack/df-structures/blob/master/df.announcements.xml#L451) and a string and processes them just like DF does. Sometimes this means the announcement won't occur. Set ``log_failures`` to ``true`` to log the reason why to the dfhack console (e.g., unrevealed map or wrong gamemode.) - Can also be built from parameters instead of a ``report_init``. + Can also be built from parameters instead of a ``report_init``. Setting ``not_sparring`` to ``false`` means it will be added to sparring logs (if applicable) rather than hunting or combat. Other ~~~~~ From f565de88e81a60ec1eb87621430cf9a8a899912b Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 1 May 2022 22:53:53 -0700 Subject: [PATCH 010/161] Fix stuff (#4) * Update Lua API.rst * Update Gui.h * Update Gui.cpp * Update LuaApi.cpp --- docs/Lua API.rst | 24 ++++++++++++------------ library/LuaApi.cpp | 8 ++++---- library/include/modules/Gui.h | 2 +- library/modules/Gui.cpp | 8 ++++---- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index bad0481bb..11c1d41e3 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1007,18 +1007,17 @@ Fortress mode * ``dfhack.gui.pauseRecenter(pos[,pause])`` ``dfhack.gui.pauseRecenter(x,y,z[,pause])`` - Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``, - and respects RECENTER_INTERFACE_SHUTDOWN_MS (the delay before input is recognized when a recenter occurs) in DF's init.txt. + Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``, and respects + RECENTER_INTERFACE_SHUTDOWN_MS (the delay before input is recognized when a recenter occurs) in DF's init.txt. * ``dfhack.gui.recenterViewscreen(pos[,zoom])`` ``dfhack.gui.recenterViewscreen(x,y,z[,zoom])`` ``dfhack.gui.recenterViewscreen([zoom])`` - Recenter the view on a position using a specific zoom type. If no position is - given, recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` - (0 = Generic, 1 = Item, 2 = Unit), where ``Generic`` skips recentering and - enforces valid view bounds (the same as x = -30000,) ``Item`` brings the position onscreen, and ``Unit`` centers the screen on the position. - Default zoom type is Item. + Recenter the view on a position using a specific zoom type. If no position is given, + recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` (0 = Generic, 1 = Item, 2 = Unit), + where ``Generic`` skips recentering and enforces valid view bounds (the same as x = -30000,) ``Item`` brings + the position onscreen without centering, and ``Unit`` centers the screen on the position. Default zoom type is Item. * ``dfhack.gui.revealInDwarfmodeMap(pos)`` @@ -1090,12 +1089,13 @@ Announcements operations accordingly. The units are used to call ``addCombatReportAuto``. * ``dfhack.gui.autoDFAnnouncement(report,text[,log_failures])`` - ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,not_sparring,log_failures])`` + ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures])`` - Takes a ``df.report_init`` (see: https://github.com/DFHack/df-structures/blob/master/df.announcements.xml#L451) and a string and processes them just like DF does. - Sometimes this means the announcement won't occur. Set ``log_failures`` to ``true`` to - log the reason why to the dfhack console (e.g., unrevealed map or wrong gamemode.) - Can also be built from parameters instead of a ``report_init``. Setting ``not_sparring`` to ``false`` means it will be added to sparring logs (if applicable) rather than hunting or combat. + Takes a ``df.report_init`` (see: https://github.com/DFHack/df-structures/blob/master/df.announcements.xml#L451) + and a string and processes them just like DF does. Sometimes this means the announcement won't occur. + Set ``log_failures`` to ``true`` to log the reason why to the dfhack console (e.g., unrevealed map or wrong gamemode.) + Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to ``true`` means the report + will be added to sparring logs (if applicable) rather than hunting or combat. Other ~~~~~ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 16bef7fc7..d9ebd7f84 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1525,7 +1525,7 @@ static int gui_autoDFAnnouncement(lua_State *state) { df::coord pos; int color; - bool bright, sparring, log_failures; + bool bright, is_sparring, log_failures; df::unit *unit1, *unit2; auto type = (df::announcement_type)lua_tointeger(state, 1); @@ -1538,7 +1538,7 @@ static int gui_autoDFAnnouncement(lua_State *state) case 9: log_failures = lua_toboolean(state, 9); case 8: - sparring = lua_toboolean(state, 8); + is_sparring = lua_toboolean(state, 8); case 7: unit2 = Lua::CheckDFObject(state, 7); case 6: @@ -1555,10 +1555,10 @@ static int gui_autoDFAnnouncement(lua_State *state) { // Use the defaults in Gui.h default: case 9: - rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, sparring, log_failures); + rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, is_sparring, log_failures); break; case 8: - rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, sparring); + rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, is_sparring); break; case 7: rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2); diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 098bb8dd3..07672aaec 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -131,7 +131,7 @@ namespace DFHack // Process an announcement exactly like DF would, which might result in no announcement DFHACK_EXPORT int autoDFAnnouncement(df::report_init r, std::string message); DFHACK_EXPORT int autoDFAnnouncement(df::report_init r, std::string message, bool log_failures); - DFHACK_EXPORT int autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool sparring = false, bool log_failures = false); + DFHACK_EXPORT int autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false, bool log_failures = false); /* * Cursor and window coords diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index ceea6251d..746a13b6a 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1831,7 +1831,7 @@ int Gui::autoDFAnnouncement(df::report_init r, string message) { if (r.unit1 != NULL) { - if (r.flags.bits.sparring) // TODO: flags.sparring is inverted + if (r.flags.bits.hostile_combat) success |= addCombatReport(r.unit1, unit_report_type::Combat, new_report_index); else if (r.unit1->job.current_job != NULL && r.unit1->job.current_job->job_type == job_type::Hunt) success |= addCombatReport(r.unit1, unit_report_type::Hunting, new_report_index); @@ -1841,7 +1841,7 @@ int Gui::autoDFAnnouncement(df::report_init r, string message) if (r.unit2 != NULL) { - if (r.flags.bits.sparring) // TODO: flags.sparring is inverted + if (r.flags.bits.hostile_combat) success |= addCombatReport(r.unit2, unit_report_type::Combat, new_report_index); else if (r.unit2->job.current_job != NULL && r.unit2->job.current_job->job_type == job_type::Hunt) success |= addCombatReport(r.unit2, unit_report_type::Hunting, new_report_index); @@ -1923,7 +1923,7 @@ int Gui::autoDFAnnouncement(df::report_init r, string message, bool log_failures return rv; } -int Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color, bool bright, df::unit *unit1, df::unit *unit2, bool sparring, bool log_failures) +int Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color, bool bright, df::unit *unit1, df::unit *unit2, bool is_sparring, bool log_failures) { auto r = df::report_init(); r.type = type; @@ -1932,7 +1932,7 @@ int Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::stri r.pos = pos; r.unit1 = unit1; r.unit2 = unit2; - r.flags.bits.sparring = !sparring; // TODO: inverted + r.flags.bits.hostile_combat = !is_sparring; if (Maps::isValidTilePos(pos)) r.zoom_type = report_zoom_type::Unit; From 038b6f0d86636a69522a554919c945644804cb0d Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 1 May 2022 23:07:52 -0700 Subject: [PATCH 011/161] Fix changelog conflicts --- docs/changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index cae8a7871..6502f9573 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -38,7 +38,12 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Removed +## New Tweaks +- `tweak` partial-items: displays percentages on partially-consumed items such as hospital cloth + ## Fixes +- `cxxrandom`: fixed exception when calling ``bool_distribution`` +- `cxxrandom`: fixed id order for ShuffleSequence, but adds code to detect which parameter is which so each id is used correctly. 16000 limit before things get weird (previous was 16 bits) - `autofarm` removed restriction on only planting 'discovered' plants - `luasocket` (and others): return correct status code when closing socket connections @@ -58,8 +63,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `blueprint`: ``track`` phase renamed to ``carve``. carved fortifications and (optionally) engravings are now captured in blueprints - `tweak` stable-cursor: Keep the cursor stable even when the viewport moves a small amount - `cursecheck`: Added a new parameter, ``ids``, to print creature and race IDs of the cursed creature. +- Include recently-added tweaks in example dfhack.init file, clean up dreamfort onMapLoad.init file ## Documentation +- `cxxrandom`: added usage examples - Add more examples to the plugin skeleton files so they are more informative for a newbie - Lua API.rst added: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` - Lua API.rst added: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` @@ -68,6 +75,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `confirm`: Correct the command name in the plugin help text - ``Quickfort Blueprint Editing Guide``: added screenshots to the Dreamfort case study and overall clarified text - Document DFHack `lua-string` (``startswith``, ``endswith``, ``split``, ``trim``, ``wrap``, and ``escape_pattern``). +- Added a Rust client library to the `remote interface docs ` ## API - Added functions reverse-engineered from ambushing unit code: ``Units::isHidden``, ``Units::isFortControlled``, ``Units::getOuterContainerRef``, ``Items::getOuterContainerRef`` @@ -78,7 +86,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``widgets.ListBox``: minimum height of dialog is now calculated correctly when there are no items in the list (e.g. when a filter doesn't match anything) - Lua wrappers for functions reverse-engineered from ambushing unit code: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` - Lua wrappers for functions reverse-engineered from announcement code: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` +- Added `custom-raw-tokens` utility to Lua library for reading tokens added to raws by mods. - ``dwarfmode.MenuOverlay``: if ``sidebar_mode`` attribute is set, automatically manage entering a specific sidebar mode on show and restoring the previous sidebar mode on dismiss +- ``dwarfmode.MenuOverlay``: new class function ``renderMapOverlay`` to assist with drawing tiles over the visible map - ``dwarfmode.enterSidebarMode()``: passing ``df.ui_sidebar_mode.DesignateMine`` now always results in you entering ``DesignateMine`` mode and not ``DesignateChopTrees``, even when you looking at the surface where the default designation mode is ``DesignateChopTrees`` - New string class function: ``string:escape_pattern()`` escapes regex special characters within a string - ``widgets.Panel``: if ``autoarrange_subviews`` is set, ``Panel``\s will now automatically lay out widgets vertically according to their current height. This allows you to have widgets dynamically change height or become visible/hidden and you don't have to worry about recalculating frame layouts @@ -90,6 +100,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``widgets.ToggleHotkeyLabel``: new ``CycleHotkeyLabel`` subclass that toggles between ``On`` and ``Off`` states - ``safe_index`` now properly handles lua sparse tables that are indexed by numbers - ``widgets``: unset values in ``frame_inset``-table default to ``0`` +- ``dialogs``: ``show*`` functions now return a reference to the created dialog +- ``ensure_key``: new global function for retrieving or dynamically creating Lua table mappings # 0.47.05-r4 From 5be0fe2a44290aa1ac3a2a0a0f96334d18ef0cda Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 May 2022 23:52:47 -0700 Subject: [PATCH 012/161] Fix issues, update changelog and docs Initialize variables in LuaApi.cpp, solve changelog conflicts, hyperlink and escape char info in docs --- data/blueprints/library/dreamfort.csv | 66 ++++++++++++++----- docs/Lua API.rst | 5 +- docs/changelog.txt | 95 +++++++++++++-------------- library/LuaApi.cpp | 6 +- scripts | 2 +- 5 files changed, 103 insertions(+), 71 deletions(-) diff --git a/data/blueprints/library/dreamfort.csv b/data/blueprints/library/dreamfort.csv index c58ce9111..07aeb09a0 100644 --- a/data/blueprints/library/dreamfort.csv +++ b/data/blueprints/library/dreamfort.csv @@ -28,7 +28,7 @@ "" "Dreamfort works best at an embark site that is flat and has at least one soil layer. New players should avoid embarks with aquifers if they are not prepared to deal with them. Bring picks for mining, an axe for woodcutting, and an anvil for a forge. Bring a few blocks to speed up initial workshop construction as well. That's all you really need, but see the example embark profile in the online spreadsheets for a more complete setup." "" -"Other DFHack scripts and plugins also work very well with Dreamfort, such as autofarm, automelt, autonestbox, burial, prioritize, seedwatch, tailor, and, of course, buildingplan. An init file that configures all these plugins is distributed with DFHack as hack/examples/init/onMapLoad_dreamfort.init." +"Other DFHack commands also work very well with Dreamfort, such as autofarm, autonestbox, prioritize, seedwatch, tailor, and, of course, buildingplan. An init file that gets everything configured for you is distributed with DFHack as hack/examples/init/onMapLoad_dreamfort.init." Put that file in your Dwarf Fortress directory -- the same directory that has dfhack.init. "" "Also copy the files in hack/examples/orders/ to dfhack-config/orders/ and the files in hack/examples/professions/ to professions/. We'll be using these files later. See https://docs.dfhack.org/en/stable/docs/guides/examples-guide.html for more information, including suggestions on how many dwarves of each profession you are likely to need at each stage of fort maturity." @@ -46,7 +46,7 @@ interactively." "# The dreamfort.csv distributed with DFHack is generated with the following command: for fname in dreamfort*.xlsx; do xlsx2csv -a -p '' ""$fname""; done | sed 's/,*$//'" #notes label(checklist) command checklist -"Here is the recommended order for Dreamfort commands. You can copy/paste the command lines directly into the DFHack terminal, or, if you prefer, you can run the blueprints in the UI with gui/quickfort. See the level walkthroughs for context and details. Also remember to read the messages the blueprints print out after you run them so you don't miss any important manual steps." +"Here is the recommended order for Dreamfort commands. You can copy/paste the command lines directly into the DFHack terminal, or, if you prefer, you can run the blueprints in the UI with gui/quickfort. See the walkthroughs (the ""help"" blueprints) for context and details. Also remember to read the messages the blueprints print out after you run them so you don't miss any important manual steps." "" -- Preparation (before you embark!) -- Copy hack/examples/init/onMapLoad_dreamfort.init to your DF directory @@ -67,7 +67,7 @@ quickfort run library/dreamfort.csv -n /surface1,# Run when you find your center quickfort run library/dreamfort.csv -n /dig_all,"# Run when you find a suitable rock layer for the industry level. It designates digging for industry, services, guildhall, suites, and apartments all in one go. This list does not include the farming level, which we'll dig in the uppermost soil layer a bit later. Note that it is more efficient for your miners if you designate your digging before they dig the central stairs past that level since the stairs are dug at a low priority. This keeps your miners focused on one level at a time. If you need to designate your levels individually due to caverns interrupting the sequence or just because it is your preference, run the level-specific dig blueprints (i.e. /industry1, /services1, /guildhall1, /suites1, and 5 levels of /apartments1) instead of running /dig_all." "" -- Core fort (should finish at about the third migration wave) -- -quickfort run library/dreamfort.csv -n /surface2,# Run after initial trees are cleared. +quickfort run library/dreamfort.csv -n /surface2,"# Run after initial trees are cleared. If you are deconstructing the wagon now, wait until after the wagon is deconstructed so the jobs that depend on wagon contents (e.g. blocks) don't get canceled later." quickfort run library/dreamfort.csv -n /farming1,# Run when channels are dug and the additional designated trees are cleared. quickfort run library/dreamfort.csv -n /farming2,# Run when the farming level has been dug out. quickfort run library/dreamfort.csv -n /surface3,# Run right after /farming2. @@ -78,7 +78,7 @@ quickfort run library/dreamfort.csv -n /surface4,"# Run after the walls and floo "quickfort run,orders library/dreamfort.csv -n /services2",# Run when the services levels have been dug out. Feel free to remove the orders for the ropes if you already brought them with you. orders import basic,"# Run after the first migration wave, so you have dorfs to do all the basic tasks. Note that this is the ""orders"" plugin, not the ""quickfort orders"" command." "quickfort run,orders library/dreamfort.csv -n /surface5","# Run when all marked trees on the surface are chopped down and walls and floors have been constructed, including the roof section over the future barracks." -prioritize ConstructBuilding,# Run when you see the bridges ready to be built so the masons come and actually build them. +prioritize ConstructBuilding,# Run when you see the bridges ready to be built so the busy masons come and build them. "quickfort run,orders library/dreamfort.csv -n /surface6",# Run when at least the beehives and weapon rack are constructed and you have linked all levers to their respective bridges. "quickfort run,orders library/dreamfort.csv -n /surface7",# Run after the surface walls are completed and any marked trees are chopped down. "" @@ -94,6 +94,7 @@ orders import furnace,# Automated production of basic furnace-related items. Don "quickfort run,orders library/dreamfort.csv -n /apartments3",# Run when all beds have been constructed on the first apartments level. "quickfort run,orders library/dreamfort.csv -n /services3","# Run after the dining table and chair, weapon rack, and archery targets have been constructed. Also wait until after you complete /surface7, though, because surface defenses are more important than a grand dining hall." "quickfort run,orders library/dreamfort.csv -n /guildhall2",# Run when the guildhall level has been dug out. +"quickfort run,orders library/dreamfort.csv -n /guildhall3",# Optionally run after /guildhall2 to build default furnishings. "quickfort run,orders library/dreamfort.csv -n /farming4",# Run once you have a cache of potash. orders import military,# Automated production of military equipment. Turn on automelt in the meltables piles on the industry level to automatically upgrade all metal military equipment to masterwork quality. These orders are optional if you are not using a military. orders import smelting,# Automated production of all types of metal bars. @@ -104,7 +105,7 @@ orders import glassstock,# Maintains a small stock of all types of glass furnitu -- Repeat for each remaining apartments level as needed -- "quickfort run,orders library/dreamfort.csv -n /apartments2",# Run when the apartment level has been dug out. "quickfort run,orders library/dreamfort.csv -n /apartments3",# Run when all beds have been constructed. -burial -pets,# Run once the coffins are placed to set them to allow for burial. This is handled for you if you are using the provided onMapLoad_dreamfort.init file. +burial -pets,# Run once the coffins are placed to set them to allow for burial. See this checklist online at https://docs.google.com/spreadsheets/d/13PVZ2h3Mm3x_G1OXQvwKd7oIR2lK4A1Ahf6Om1kFigw/edit#gid=1459509569 #notes label(setup_help) @@ -113,14 +114,14 @@ Makes common initial adjustments to in-game settings. "The /setup blueprint is intended to be run once at the start of the game, before anything else is changed. Players are welcome to make any further customizations after this blueprint is run. Please be sure to run the /setup blueprint before making any other changes, though, so your settings are not overwritten!" "" The following settings are changed: -"- The manager, chief medical dwarf, broker, and bookkeeper noble roles are assigned to the first suggested dwarf. This is likely to be the expedition leader, but the game could suggest others if they have relevant skills. Bookkeeping is also set to the highest precision." +"- The manager, chief medical dwarf, broker, and bookkeeper noble roles are assigned to the first suggested dwarf. This is likely to be the expedition leader, but the game could suggest others if they have relevant skills. Bookkeeping is set to the highest precision." "" - Standing orders are set to: - only farmers harvest - gather refuse from outside (incl. vermin) - - no autoloom (we'll be managing cloth production with automated orders) + - no autoloom (so the hospital always has thread -- we'll be managing cloth production with automated orders) "" -"- A burrow named ""Inside"" is created (it's up to the player to define the area). It is intended for use in getting your civilians to safety during sieges. An alert named ""Siege"" is also created and associated with the ""Inside"" burrow." +"- A burrow named ""Inside"" is created, but it's up to the player to define the area as the fort expands. It is intended for use in getting your civilians to safety during sieges. A military alert named ""Siege"" is also created and associated with the ""Inside"" burrow." "" - Military uniforms get the following modifications: - all default uniforms set to replace clothing @@ -144,7 +145,7 @@ starthotkeys: H{sethotkey fkey={F2} name=Farming}{sethotkey fkey={F3} name=Indus "" "#config label(setup) message(Please set the zoom targets of the hotkeys (the 'H' menu) according to where you actually end up digging the levels. As you build your fort, expand the ""Inside"" burrow to include new civilian-safe areas. -Optionally, add a leather cloak to your military uniforms to enhance the protection of the uniforms. +Optionally, add a leather cloak to your military uniforms to enhance their protection rating. Nothing in Dreamfort depends on these settings staying as they are. Feel free to change any setting to your personal preference.) assign nobles, set standing orders, create burrows, make adjustments to military uniforms, and set hotkey names" {startnobles}{startorders}{startburrows}{startmilitary}{starthotkeys} "#meta label(dig_all) start(central stairs on industry level) dig industry, services, guildhall, suites, and apartments levels. does not include farming." @@ -532,7 +533,7 @@ Feel free to assign an unimportant animal to the pasture in the main entranceway -"#place label(surface_place_start) start(19; 19) hidden() message(if you haven't already, now is a good time to deconstruct the wagon) starting stockpiles" +#place label(surface_place_start) start(19; 19) hidden() starting stockpiles @@ -974,7 +975,7 @@ You might also want to set the ""trade goods quantum"" stockpile to Auto Trade i ,,,`,,`,nocontainers,crafts,,,,,,,`,,,,"{givename name=""inner main gate""}",,,,`,,,,,,,,,`,,` ,,,`,,`,"{givename name=""trade goods""}",,,,,,,,,,,,,,,,,,,,,,,,,`,,` ,,,`,,`,{forbidmasterworkfinishedgoods}{forbidartifactfinishedgoods},,,,,,,,"{givename name=""trade depo gate""}",,,,,,,,"{givename name=""barracks gate""}",,,,,,,,,`,,` -,,,`,,`,,"{quantumstopfromnorth name=""Trade Goods Dumper""}",,,,,,,,,,,,,,,,,"{quantumstop name=""Prisoner quantum"" move={Up 5} move_back={Down 5} route_enable={prisoner_route_enable}}{givename name=""prisoner dumper""}",,,,,,,`,,` +,,,`,,`,,"{quantumstopfromnorth name=""Trade Goods quantum""}",,,,,,,,,,,,,,,,,"{quantumstop name=""Prisoner quantum"" move={Up 5} move_back={Down 5} route_enable={prisoner_route_enable}}{givename name=""prisoner dumper""}",,,,,,,`,,` ,,,`,,`,,"{quantum name=""trade goods quantum""}",,,,,,,`,,,,,,,,`,,"{quantum name=""prisoner quantum"" quantum_enable={enableanimals}}",,,,,,,`,,` ,,,`,,`,`,`,`,`,`,`,`,`,`,,,,,,,,`,`,`,`,`,`,`,`,`,`,,` ,,,`,,"{givename name=""left outer gate""}",,,,,,,,,"{givename name=""left inner gate""}",,,,,,,,"{givename name=""right inner gate""}",,,,,,,,,"{givename name=""right outer gate""}",,` @@ -2554,14 +2555,14 @@ query_jail/services_query_jail Screenshot: https://drive.google.com/file/d/17jHiCKeZm6FSS-CI4V0r0GJZh09nzcO_ "" Features: -"- Big rooms, optionally pre-furnished. Double-thick walls to ensure engravings add value to the ""correct"" side. Fill with furniture and assign as needed." +"- Big rooms, optionally pre-furnished. Double-thick walls to ensure engravings add value to the ""correct"" side. Declare locations as needed." "" Guildhall Walkthrough: 1) Dig out the rooms with /guildhall1. "" -"2) Once the area is dug out, add doors and a few statues with /guildhall2. Run ""quickfort orders"" for /guildhall2." +"2) Once the area is dug out, pre-create the zones and add doors and a few statues with /guildhall2. Run ""quickfort orders"" for /guildhall2." "" -"3) Furnish individual rooms manually, or alternately get default furnishings for a variety of room types by running /guildhall3. Declare appropriate locations as you need guildhalls, libraries, and temples. If you need more rooms, you can dig another /guildhall1 in an unused z-level." +"3) Furnish individual rooms manually, or get default furnishings for a variety of room types by running /guildhall3. Declare appropriate locations from the pre-created zones as you need guildhalls, libraries, and temples. If you need more rooms, you can dig another /guildhall1 in an unused z-level." "#dig label(guildhall1) start(15; 15; central stairs) message(Once the area is dug out, continue with /guildhall2.)" @@ -2592,7 +2593,10 @@ Guildhall Walkthrough: ,,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d,,,d,d,d,d,d,d,d -"#build label(guildhall2) start(15; 15; central stairs) message(Remember to enqueue manager orders for this blueprint. +#meta label(guildhall2) +doors/guildhall_doors +zones/guildhall_zones +"#build label(guildhall_doors) start(15; 15; central stairs) hidden() message(Remember to enqueue manager orders for this blueprint. Smooth/engrave tiles, furnish rooms, and declare locations as required.) build doors" @@ -2623,6 +2627,36 @@ Smooth/engrave tiles, furnish rooms, and declare locations as required.) build d ,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +#zone label(guildhall_zones) start(15; 15; central stairs) hidden() designate zones + +,m(9x9),,,,,,,,,m(9x9),,,,,,,,,m(9x9) +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,~,~,`,`,`,`,`,`,`,~,~,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,,,,,,~,,,,,,~,,~,,,,,,~ +,m(9x9),,,,,,~,,,,,,`,~,`,,,,m(9x9),,~ +,,`,`,`,`,`,`,`,,,,,~,,~,,,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,~,`,~,`,,,,`,~,`,~,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,~,,`,,`,,`,,~,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,~,`,~,`,,,,`,~,`,~,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,,,~,,~,,,,,`,`,`,`,`,`,` +,,,,,,,~,,,,,,`,~,`,,,,,,~ +,m(9x9),,,,,,~,,,m(9x9),,,~,,~,,,,m(9x9),,~ +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,~,~,`,`,`,`,`,`,`,~,~,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` +,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,`,,,`,`,`,`,`,`,` + + "#build label(guildhall3) start(15; 15; central stairs) message(Remember to enqueue manager orders for this blueprint.) furnish 4 guildhalls, 3 temples, and a library" @@ -2676,7 +2710,7 @@ Apartments Walkthrough: "" "3) Once the beds are built, configure the rooms and build the remaining furniture with /apartments3. Run ""quickfort orders"" for /apartments3." "" -"4) Once the coffins are all in place, run ""burial -pets"" to set them all to accept burials. This is handled for you if you're using the onMapLoad_dreamfort.init file included with DFHack." +"4) Once the coffins are all in place, run ""burial -pets"" to set them all to accept burials." "#dig label(suites1) start(18; 18; central ramp) message(Once the area is dug out, run /suites2) noble suites" ,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d diff --git a/docs/Lua API.rst b/docs/Lua API.rst index caaab0040..c13897e07 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1091,11 +1091,12 @@ Announcements * ``dfhack.gui.autoDFAnnouncement(report,text[,log_failures])`` ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures])`` - Takes a ``df.report_init`` (see: https://github.com/DFHack/df-structures/blob/master/df.announcements.xml#L451) + Takes a ``df.report_init`` (see: `structure definition `_) and a string and processes them just like DF does. Sometimes this means the announcement won't occur. Set ``log_failures`` to ``true`` to log the reason why to the dfhack console (e.g., unrevealed map or wrong gamemode.) Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to ``true`` means the report - will be added to sparring logs (if applicable) rather than hunting or combat. + will be added to sparring logs (if applicable) rather than hunting or combat. Text uses ``&`` as an escape character, with ``&r`` being a newline, + ``&&`` being just ``&``, and any other combination causing neither character to display. Other ~~~~~ diff --git a/docs/changelog.txt b/docs/changelog.txt index 6502f9573..6e60e38c9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -34,74 +34,71 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future ## New Plugins -- `spectate`: automates the following of dwarves more often than not based on job zlevel activity levels, sometimes randomly though. - -## Removed +- `spectate`: "spectator mode" -- automatically follows dwarves doing things in your fort ## New Tweaks -- `tweak` partial-items: displays percentages on partially-consumed items such as hospital cloth +- `tweak`: ``partial-items`` displays percentage remaining for partially-consumed items such as hospital cloth ## Fixes +- `autofarm`: removed restriction on only planting "discovered" plants - `cxxrandom`: fixed exception when calling ``bool_distribution`` -- `cxxrandom`: fixed id order for ShuffleSequence, but adds code to detect which parameter is which so each id is used correctly. 16000 limit before things get weird (previous was 16 bits) -- `autofarm` removed restriction on only planting 'discovered' plants -- `luasocket` (and others): return correct status code when closing socket connections +- `luasocket`: return correct status code when closing socket connections so clients can know when to retry ## Misc Improvements +- `autochop`: only designate the amount of trees required to reach ``max_logs`` +- `autochop`: preferably designate larger trees over smaller ones +- `blueprint`: ``track`` phase renamed to ``carve`` +- `blueprint`: carved fortifications and (optionally) engravings are now captured in generated blueprints +- `cursecheck`: new option, ``--ids`` prints creature and race IDs of the cursed creature +- `debug`: DFHack log messages now have configurable headers (e.g. timestamp, origin plugin name, etc.) via the ``debugfilter`` command of the `debug` plugin +- `debug`: script execution log messages (e.g. "Loading script: dfhack_extras.init" can now be controlled with the ``debugfilter`` command. To hide the messages, add this line to your ``dfhack.init`` file: ``debugfilter set Warning core script`` +- `dfhack-examples-guide`: add mugs to ``basic`` manager orders +- `dfhack-examples-guide`: ``onMapLoad_dreamfort.init`` remove "cheaty" commands and new tweaks that are now in the default ``dfhack.init-example`` file +- ``dfhack.init-example``: recently-added tweaks added to example ``dfhack.init`` file - `dig-now`: handle fortification carving - `EventManager`: add new event type ``JOB_STARTED``, triggered when a job first gains a worker -- `EventManager`: add new event type ``NEW_UNIT_ACTIVE``, triggered when a new unit appears on the active list -- `EventManager`: now each registered handler for an event can have its own frequency instead of all handlers using the lowest frequency of all handlers -- `stocks`: allow search terms to match the full item label, even when the label is truncated for length -- `dfhack-examples-guide`: add mugs to ``basic`` manager orders +- `EventManager`: add new event type ``UNIT_NEW_ACTIVE``, triggered when a new unit appears on the active list - `gui/create-item`: Added "(chain)" annotation text for armours with the [CHAIN_METAL_TEXT] flag set -- DFHack log messages now have configurable headers (e.g. timestamp, origin plugin name, etc.) via the ``debugfilter`` command of the `debug` plugin -- Script execution log messages (e.g. "Loading script: dfhack_extras.init" can now be controlled with the ``debugfilter`` command. To hide the messages, add this line to your ``dfhack.init`` file: ``debugfilter set Warning core script`` -- `manipulator`: Tweak colors to make the cursor easier to locate -- `autochop`: only designate the amount of trees required to reach ``max_logs`` -- `autochop`: preferably designate larger trees over smaller ones -- `blueprint`: ``track`` phase renamed to ``carve``. carved fortifications and (optionally) engravings are now captured in blueprints -- `tweak` stable-cursor: Keep the cursor stable even when the viewport moves a small amount -- `cursecheck`: Added a new parameter, ``ids``, to print creature and race IDs of the cursed creature. -- Include recently-added tweaks in example dfhack.init file, clean up dreamfort onMapLoad.init file +- `manipulator`: tweak colors to make the cursor easier to locate +- `stocks`: allow search terms to match the full item label, even when the label is truncated for length +- `tweak`: ``stable-cursor`` now keeps the cursor stable even when the viewport moves a small amount ## Documentation +- add more examples to the plugin example skeleton files so they are more informative for a newbie +- `confirm`: correct the command name in the plugin help text - `cxxrandom`: added usage examples -- Add more examples to the plugin skeleton files so they are more informative for a newbie -- Lua API.rst added: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` -- Lua API.rst added: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` -- Update download link and installation instructions for Visual C++ 2015 build tools on Windows -- Updated information regarding obtaining a compatible Windows build environment -- `confirm`: Correct the command name in the plugin help text -- ``Quickfort Blueprint Editing Guide``: added screenshots to the Dreamfort case study and overall clarified text -- Document DFHack `lua-string` (``startswith``, ``endswith``, ``split``, ``trim``, ``wrap``, and ``escape_pattern``). -- Added a Rust client library to the `remote interface docs ` +- ``Lua API.rst``: added ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)``, ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` +- `lua-string`: document DFHack string extensions (``startswith()``, ``endswith()``, ``split()``, ``trim()``, ``wrap()``, and ``escape_pattern()``) +- `quickfort-blueprint-guide`: added screenshots to the Dreamfort case study and overall clarified text +- `remote-client-libs`: add new Rust client library +- update download link and installation instructions for Visual C++ 2015 build tools on Windows +- update information regarding obtaining a compatible Windows build environment ## API -- Added functions reverse-engineered from ambushing unit code: ``Units::isHidden``, ``Units::isFortControlled``, ``Units::getOuterContainerRef``, ``Items::getOuterContainerRef`` -- Added functions reverse-engineered from announcement code: ``Gui::parseReportString``, ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` +- add functions reverse-engineered from ambushing unit code: ``Units::isHidden()``, ``Units::isFortControlled()``, ``Units::getOuterContainerRef()``, ``Items::getOuterContainerRef()`` +- add functions reverse-engineered from announcement code: ``Gui::parseReportString``, ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` ## Lua -- ``widgets.FilteredList`` now allows all punctuation to be typed into the filter and can match search keys that start with punctuation. -- ``widgets.ListBox``: minimum height of dialog is now calculated correctly when there are no items in the list (e.g. when a filter doesn't match anything) -- Lua wrappers for functions reverse-engineered from ambushing unit code: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` -- Lua wrappers for functions reverse-engineered from announcement code: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` -- Added `custom-raw-tokens` utility to Lua library for reading tokens added to raws by mods. -- ``dwarfmode.MenuOverlay``: if ``sidebar_mode`` attribute is set, automatically manage entering a specific sidebar mode on show and restoring the previous sidebar mode on dismiss -- ``dwarfmode.MenuOverlay``: new class function ``renderMapOverlay`` to assist with drawing tiles over the visible map -- ``dwarfmode.enterSidebarMode()``: passing ``df.ui_sidebar_mode.DesignateMine`` now always results in you entering ``DesignateMine`` mode and not ``DesignateChopTrees``, even when you looking at the surface where the default designation mode is ``DesignateChopTrees`` -- New string class function: ``string:escape_pattern()`` escapes regex special characters within a string -- ``widgets.Panel``: if ``autoarrange_subviews`` is set, ``Panel``\s will now automatically lay out widgets vertically according to their current height. This allows you to have widgets dynamically change height or become visible/hidden and you don't have to worry about recalculating frame layouts -- ``widgets.ResizingPanel``: new ``Panel`` subclass that automatically recalculates it's own frame height based on the size, position, and visibility of its subviews -- ``widgets.WrappedLabel``: new ``Label`` subclass that provides autowrapping of text -- ``widgets.TooltipLabel``: new ``WrappedLabel`` subclass that provides tooltip-like behavior -- ``widgets.HotkeyLabel``: new ``Label`` subclass that displays and reacts to hotkeys -- ``widgets.CycleHotkeyLabel``: new ``Label`` subclass that allows users to cycle through a list of options by pressing a hotkey -- ``widgets.ToggleHotkeyLabel``: new ``CycleHotkeyLabel`` subclass that toggles between ``On`` and ``Off`` states -- ``safe_index`` now properly handles lua sparse tables that are indexed by numbers -- ``widgets``: unset values in ``frame_inset``-table default to ``0`` +- `custom-raw-tokens`: library for accessing tokens added to raws by mods +- ``dfhack.gui``: Lua wrappers for functions reverse-engineered from announcement code: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` +- ``dfhack.units``: Lua wrappers for functions reverse-engineered from ambushing unit code: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` - ``dialogs``: ``show*`` functions now return a reference to the created dialog +- ``dwarfmode.enterSidebarMode()``: passing ``df.ui_sidebar_mode.DesignateMine`` now always results in you entering ``DesignateMine`` mode and not ``DesignateChopTrees``, even when you looking at the surface (where the default designation mode is ``DesignateChopTrees``) +- ``dwarfmode.MenuOverlay``: if ``sidebar_mode`` attribute is set, automatically manage entering a specific sidebar mode on show and restoring the previous sidebar mode on dismiss +- ``dwarfmode.MenuOverlay``: new class function ``renderMapOverlay`` to assist with painting tiles over the visible map - ``ensure_key``: new global function for retrieving or dynamically creating Lua table mappings +- ``safe_index``: now properly handles lua sparse tables that are indexed by numbers +- ``string``: new function ``escape_pattern()`` escapes regex special characters within a string +- ``widgets``: unset values in ``frame_inset`` table default to ``0`` +- ``widgets``: ``FilteredList`` class now allows all punctuation to be typed into the filter and can match search keys that start with punctuation +- ``widgets``: minimum height of ``ListBox`` dialog is now calculated correctly when there are no items in the list (e.g. when a filter doesn't match anything) +- ``widgets``: if ``autoarrange_subviews`` is set, ``Panel``\s will now automatically lay out widgets vertically according to their current height. This allows you to have widgets dynamically change height or become visible/hidden and you don't have to worry about recalculating frame layouts +- ``widgets``: new class ``ResizingPanel`` (subclass of ``Panel``) automatically recalculates its own frame height based on the size, position, and visibility of its subviews +- ``widgets``: new class ``HotkeyLabel`` (subclass of ``Label``) that displays and reacts to hotkeys +- ``widgets``: new class ``CycleHotkeyLabel`` (subclass of ``Label``) allows users to cycle through a list of options by pressing a hotkey +- ``widgets``: new class ``ToggleHotkeyLabel`` (subclass of ``CycleHotkeyLabel``) toggles between ``On`` and ``Off`` states +- ``widgets``: new class ``WrappedLabel`` (subclass of ``Label``) provides autowrapping of text +- ``widgets``: new class ``TooltipLabel`` (subclass of ``WrappedLabel``) provides tooltip-like behavior # 0.47.05-r4 diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index d9ebd7f84..6954e3996 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1524,9 +1524,9 @@ static int gui_autoDFAnnouncement(lua_State *state) else { df::coord pos; - int color; - bool bright, is_sparring, log_failures; - df::unit *unit1, *unit2; + int color = 0; //initialize these to prevent warning + bool bright = false, is_sparring = false, log_failures = false; + df::unit *unit1 = NULL, *unit2 = NULL; auto type = (df::announcement_type)lua_tointeger(state, 1); Lua::CheckDFAssign(state, &pos, 2); diff --git a/scripts b/scripts index 74f03c0e4..714ef87ae 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 74f03c0e4a5a7818b7a1cbc3576ce2c3d30d3696 +Subproject commit 714ef87aef713af94874c6441e7810b9cf9ef312 From 74499ad64ad02bc5d7ef5cc584445eadf260a5c6 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 3 May 2022 00:09:34 -0700 Subject: [PATCH 013/161] Use to_string on integer --- library/modules/Gui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 746a13b6a..ce43df71f 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1783,7 +1783,7 @@ int Gui::autoDFAnnouncement(df::report_init r, string message) if (a_flags.bits.D_DISPLAY) { world->status.display_timer = r.display_timer; - Gui::writeToGamelog("x" + (repeat_count + 1)); + Gui::writeToGamelog("x" + to_string(repeat_count + 1)); } return 0; } From 018452af8284748320ec122796623176fa412498 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 5 May 2022 22:57:04 -0700 Subject: [PATCH 014/161] Update changelog.txt --- docs/changelog.txt | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 6e60e38c9..57fe888b6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,6 +33,25 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future +## New Plugins + +## New Tweaks + +## Fixes + +## Misc Improvements + +## Documentation +- ``Lua API.rst``: added ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` + +## API +- add functions reverse-engineered from announcement code: ``Gui::parseReportString``, ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` + +## Lua +- ``dfhack.gui``: Lua wrappers for functions reverse-engineered from announcement code: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` + +# 0.47.05-r5 + ## New Plugins - `spectate`: "spectator mode" -- automatically follows dwarves doing things in your fort @@ -67,7 +86,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - add more examples to the plugin example skeleton files so they are more informative for a newbie - `confirm`: correct the command name in the plugin help text - `cxxrandom`: added usage examples -- ``Lua API.rst``: added ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)``, ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` +- ``Lua API.rst``: added ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` - `lua-string`: document DFHack string extensions (``startswith()``, ``endswith()``, ``split()``, ``trim()``, ``wrap()``, and ``escape_pattern()``) - `quickfort-blueprint-guide`: added screenshots to the Dreamfort case study and overall clarified text - `remote-client-libs`: add new Rust client library @@ -76,11 +95,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API - add functions reverse-engineered from ambushing unit code: ``Units::isHidden()``, ``Units::isFortControlled()``, ``Units::getOuterContainerRef()``, ``Items::getOuterContainerRef()`` -- add functions reverse-engineered from announcement code: ``Gui::parseReportString``, ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` ## Lua - `custom-raw-tokens`: library for accessing tokens added to raws by mods -- ``dfhack.gui``: Lua wrappers for functions reverse-engineered from announcement code: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` - ``dfhack.units``: Lua wrappers for functions reverse-engineered from ambushing unit code: ``isHidden(unit)``, ``isFortControlled(unit)``, ``getOuterContainerRef(unit)``, ``getOuterContainerRef(item)`` - ``dialogs``: ``show*`` functions now return a reference to the created dialog - ``dwarfmode.enterSidebarMode()``: passing ``df.ui_sidebar_mode.DesignateMine`` now always results in you entering ``DesignateMine`` mode and not ``DesignateChopTrees``, even when you looking at the surface (where the default designation mode is ``DesignateChopTrees``) From ce34ac8f33e56b89b90da76384825673a5f1dc42 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 16 May 2022 18:41:47 -0700 Subject: [PATCH 015/161] Use debugfilter; remove redundant changelog entries --- .github/workflows/build.yml | 4 +- .pre-commit-config.yaml | 1 + ci/script-docs.py | 4 +- ci/update-submodules.manifest | 2 + data/examples/orders/basic.json | 131 +++++++++++++++++++------------- depends/CMakeLists.txt | 8 +- depends/jsoncpp-sub | 2 +- depends/libexpat | 2 +- depends/libzip | 2 +- depends/lua/src/lapi.c | 2 +- depends/luacov | 2 +- depends/xlsxio | 2 +- docs/Authors.rst | 1 + docs/Lua API.rst | 11 ++- docs/Removed.rst | 8 ++ docs/changelog.txt | 3 +- library/CMakeLists.txt | 4 +- library/Debug.cpp | 2 +- library/LuaApi.cpp | 17 +---- library/MiscUtils.cpp | 58 ++++++++++---- library/include/Debug.h | 2 +- library/include/MiscUtils.h | 3 +- library/include/modules/Gui.h | 8 +- library/modules/Gui.cpp | 126 ++++++++++++++---------------- library/xml | 2 +- plugins/CMakeLists.txt | 6 +- plugins/autolabor.cpp | 2 +- plugins/command-prompt.cpp | 109 ++++++++++++++------------ plugins/isoworld | 2 +- scripts | 2 +- 30 files changed, 295 insertions(+), 233 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8506ae9b..07febaa63 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: - name: Download DF run: | sh ci/download-df.sh - - name: Build DFHack + - name: Configure DFHack env: CC: gcc-${{ matrix.gcc }} CXX: g++-${{ matrix.gcc }} @@ -85,6 +85,8 @@ jobs: -DBUILD_STONESENSE:BOOL=${{ matrix.plugins == 'all' }} \ -DBUILD_SUPPORTED:BOOL=1 \ -DCMAKE_INSTALL_PREFIX="$DF_FOLDER" + - name: Build DFHack + run: | ninja -C build-ci install - name: Run tests id: run_tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26d135ca2..336bda270 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ ci: autofix_prs: false + autoupdate_schedule: monthly repos: # shared across repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/ci/script-docs.py b/ci/script-docs.py index 71d7f37b2..7ef287f8a 100755 --- a/ci/script-docs.py +++ b/ci/script-docs.py @@ -81,11 +81,11 @@ def check_file(fname): def main(): """Check that all DFHack scripts include documentation""" err = 0 - exclude = set(['internal', 'test']) + exclude = {'.git', 'internal', 'test'} for root, dirs, files in os.walk(SCRIPT_PATH, topdown=True): dirs[:] = [d for d in dirs if d not in exclude] for f in files: - if f[-3:] in {'.rb', 'lua'}: + if f.split('.')[-1] in {'rb', 'lua'}: err += check_file(join(root, f)) return err diff --git a/ci/update-submodules.manifest b/ci/update-submodules.manifest index 4bd383018..e97cae6f3 100644 --- a/ci/update-submodules.manifest +++ b/ci/update-submodules.manifest @@ -1,7 +1,9 @@ library/xml master scripts master plugins/stonesense master +plugins/isoworld dfhack depends/libzip dfhack depends/libexpat dfhack depends/xlsxio dfhack depends/luacov dfhack +depends/jsoncpp-sub dfhack diff --git a/data/examples/orders/basic.json b/data/examples/orders/basic.json index fb4668e81..e8dee1680 100644 --- a/data/examples/orders/basic.json +++ b/data/examples/orders/basic.json @@ -1,12 +1,34 @@ [ { - "amount_left" : 1, - "amount_total" : 1, - "frequency" : "Daily", + "amount_left" : 150, + "amount_total" : 150, + "frequency" : "Monthly", "id" : 0, "is_active" : false, "is_validated" : false, "item_conditions" : + [ + { + "condition" : "LessThan", + "flags" : + [ + "unrotten" + ], + "item_type" : "FOOD", + "value" : 400 + } + ], + "job" : "PrepareMeal", + "meal_ingredients" : 2 + }, + { + "amount_left" : 10, + "amount_total" : 10, + "frequency" : "Daily", + "id" : 1, + "is_active" : false, + "is_validated" : false, + "item_conditions" : [ { "condition" : "AtLeast", @@ -25,7 +47,7 @@ "unrotten", "cookable" ], - "value" : 15 + "value" : 500 }, { "condition" : "AtMost", @@ -35,6 +57,15 @@ ], "item_type" : "FOOD", "value" : 3500 + }, + { + "condition" : "AtLeast", + "flags" : + [ + "unrotten" + ], + "item_type" : "FOOD", + "value" : 400 } ], "job" : "PrepareMeal", @@ -44,7 +75,7 @@ "amount_left" : 2, "amount_total" : 2, "frequency" : "Daily", - "id" : 1, + "id" : 2, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -81,7 +112,7 @@ "amount_left" : 2, "amount_total" : 2, "frequency" : "Daily", - "id" : 2, + "id" : 3, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -118,7 +149,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 3, + "id" : 4, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -139,7 +170,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 4, + "id" : 5, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -174,7 +205,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 5, + "id" : 6, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -206,7 +237,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 6, + "id" : 7, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -238,7 +269,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 7, + "id" : 8, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -260,7 +291,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 8, + "id" : 9, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -293,7 +324,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 9, + "id" : 10, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -324,7 +355,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 10, + "id" : 11, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -357,7 +388,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 11, + "id" : 12, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -397,7 +428,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 12, + "id" : 13, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -423,7 +454,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 13, + "id" : 14, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -458,7 +489,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 14, + "id" : 15, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -493,7 +524,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 15, + "id" : 16, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -528,7 +559,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 16, + "id" : 17, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -559,7 +590,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 17, + "id" : 18, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -589,7 +620,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 18, + "id" : 19, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -613,7 +644,6 @@ ], "item_subtype" : "ITEM_TOOL_LARGE_POT", "item_type" : "TOOL", - "material" : "INORGANIC", "value" : 25 } ], @@ -625,7 +655,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 19, + "id" : 20, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -649,7 +679,6 @@ ], "item_subtype" : "ITEM_TOOL_JUG", "item_type" : "TOOL", - "material" : "INORGANIC", "value" : 10 } ], @@ -661,7 +690,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 20, + "id" : 21, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -690,7 +719,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 21, + "id" : 22, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -718,7 +747,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 22, + "id" : 23, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -746,7 +775,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 23, + "id" : 24, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -776,7 +805,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 24, + "id" : 25, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -806,7 +835,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 25, + "id" : 26, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -835,7 +864,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 26, + "id" : 27, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -866,7 +895,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 27, + "id" : 28, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -902,7 +931,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 28, + "id" : 29, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -938,7 +967,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 29, + "id" : 30, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -974,7 +1003,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 30, + "id" : 31, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -993,7 +1022,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 31, + "id" : 32, "is_active" : false, "is_validated" : true, "item_conditions" : @@ -1019,7 +1048,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 32, + "id" : 33, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1041,7 +1070,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 33, + "id" : 34, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1067,7 +1096,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 34, + "id" : 35, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1093,7 +1122,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 35, + "id" : 36, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1119,7 +1148,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 36, + "id" : 37, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1152,7 +1181,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 37, + "id" : 38, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1191,7 +1220,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 38, + "id" : 39, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1225,7 +1254,7 @@ "amount_left" : 4, "amount_total" : 4, "frequency" : "Daily", - "id" : 39, + "id" : 40, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1248,7 +1277,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 40, + "id" : 41, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1272,7 +1301,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 41, + "id" : 42, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1306,7 +1335,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 42, + "id" : 43, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1335,7 +1364,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 43, + "id" : 44, "is_active" : false, "is_validated" : false, "item_conditions" : @@ -1364,7 +1393,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 44, + "id" : 45, "is_active" : false, "is_validated" : true, "item_conditions" : @@ -1395,7 +1424,7 @@ "amount_left" : 1, "amount_total" : 1, "frequency" : "Daily", - "id" : 45, + "id" : 46, "is_active" : false, "is_validated" : false, "item_conditions" : diff --git a/depends/CMakeLists.txt b/depends/CMakeLists.txt index 0fcb4ca3c..405a9555e 100644 --- a/depends/CMakeLists.txt +++ b/depends/CMakeLists.txt @@ -12,10 +12,11 @@ endif() add_subdirectory(tthread) option(JSONCPP_WITH_TESTS "Compile and (for jsoncpp_check) run JsonCpp test executables" OFF) option(JSONCPP_WITH_POST_BUILD_UNITTEST "Automatically run unit-tests as a post build step" OFF) +option(JSONCPP_BUILD_SHARED_LIBS "Build jsoncpp_lib as a shared library." OFF) +option(JSONCPP_BUILD_OBJECT_LIBS "Build jsoncpp_lib as a object library." OFF) +option(JSONCPP_WITH_CMAKE_PACKAGE "Generate and install cmake package files" OFF) + add_subdirectory(jsoncpp-sub EXCLUDE_FROM_ALL) -if(UNIX) - set_target_properties(jsoncpp_lib_static PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations") -endif() # build clsocket static and only as a dependency. Setting those options here overrides its own default settings. option(CLSOCKET_SHARED "Build clsocket lib as shared." OFF) option(CLSOCKET_DEP_ONLY "Build for use inside other CMake projects as dependency." ON) @@ -37,6 +38,7 @@ if(UNIX) set_target_properties(expat PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized") endif() +set(CMAKE_REQUIRED_QUIET ON) set(LIBZIP_BUILD_DOC OFF CACHE BOOL "") set(LIBZIP_BUILD_EXAMPLES OFF CACHE BOOL "") set(LIBZIP_BUILD_REGRESS OFF CACHE BOOL "") diff --git a/depends/jsoncpp-sub b/depends/jsoncpp-sub index ddabf50f7..ba5eac541 160000 --- a/depends/jsoncpp-sub +++ b/depends/jsoncpp-sub @@ -1 +1 @@ -Subproject commit ddabf50f72cf369bf652a95c4d9fe31a1865a781 +Subproject commit ba5eac54136064af94ab4a923ac110d7534d4f83 diff --git a/depends/libexpat b/depends/libexpat index 3c0f2e86c..3e877cbb3 160000 --- a/depends/libexpat +++ b/depends/libexpat @@ -1 +1 @@ -Subproject commit 3c0f2e86ce4e7a3a3b30e765087d02a68bba7e6f +Subproject commit 3e877cbb3c9bc8f22946053e70490d2e5431f4d5 diff --git a/depends/libzip b/depends/libzip index da0d18ae5..081249cce 160000 --- a/depends/libzip +++ b/depends/libzip @@ -1 +1 @@ -Subproject commit da0d18ae59ef2699013316b703cdc93809414c93 +Subproject commit 081249cceb59adc857a72d67e60c32047680f787 diff --git a/depends/lua/src/lapi.c b/depends/lua/src/lapi.c index 711895b39..aa01148ab 100644 --- a/depends/lua/src/lapi.c +++ b/depends/lua/src/lapi.c @@ -395,7 +395,7 @@ LUA_API size_t lua_rawlen (lua_State *L, int idx) { case LUA_TSHRSTR: return tsvalue(o)->shrlen; case LUA_TLNGSTR: return tsvalue(o)->u.lnglen; case LUA_TUSERDATA: return uvalue(o)->len; - case LUA_TTABLE: return luaH_getn(hvalue(o)); + case LUA_TTABLE: return size_t(luaH_getn(hvalue(o))); default: return 0; } } diff --git a/depends/luacov b/depends/luacov index 87d6ae018..99d068278 160000 --- a/depends/luacov +++ b/depends/luacov @@ -1 +1 @@ -Subproject commit 87d6ae018cb8d288d854f632e9d8d959d75d7db4 +Subproject commit 99d06827848583232dd77afb34cd7ab589567086 diff --git a/depends/xlsxio b/depends/xlsxio index 4056226fe..ab8fd7f3e 160000 --- a/depends/xlsxio +++ b/depends/xlsxio @@ -1 +1 @@ -Subproject commit 4056226fe0df6bff4593ee2353cca07c2b7f327e +Subproject commit ab8fd7f3e9df457e8bc1b5cb31b78d57df0ac5a4 diff --git a/docs/Authors.rst b/docs/Authors.rst index 38fe19862..afb0a3f14 100644 --- a/docs/Authors.rst +++ b/docs/Authors.rst @@ -32,6 +32,7 @@ billw2012 billw2012 BrickViking brickviking brndd brndd burneddi Caldfir caldfir +Cameron Ewell Ozzatron Carter Bray Qartar Chris Dombroski cdombroski Chris Parsons chrismdp diff --git a/docs/Lua API.rst b/docs/Lua API.rst index c13897e07..7e1f45231 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1015,7 +1015,7 @@ Fortress mode ``dfhack.gui.recenterViewscreen([zoom])`` Recenter the view on a position using a specific zoom type. If no position is given, - recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` (0 = Generic, 1 = Item, 2 = Unit), + recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` (see: `enum definition `_), where ``Generic`` skips recentering and enforces valid view bounds (the same as x = -30000,) ``Item`` brings the position onscreen without centering, and ``Unit`` centers the screen on the position. Default zoom type is Item. @@ -1088,15 +1088,14 @@ Announcements Uses the type to look up options from announcements.txt, and calls the above operations accordingly. The units are used to call ``addCombatReportAuto``. -* ``dfhack.gui.autoDFAnnouncement(report,text[,log_failures])`` - ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring,log_failures])`` +* ``dfhack.gui.autoDFAnnouncement(report,text)`` + ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring])`` Takes a ``df.report_init`` (see: `structure definition `_) and a string and processes them just like DF does. Sometimes this means the announcement won't occur. - Set ``log_failures`` to ``true`` to log the reason why to the dfhack console (e.g., unrevealed map or wrong gamemode.) Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to ``true`` means the report - will be added to sparring logs (if applicable) rather than hunting or combat. Text uses ``&`` as an escape character, with ``&r`` being a newline, - ``&&`` being just ``&``, and any other combination causing neither character to display. + will be added to sparring logs (if applicable) rather than hunting or combat. Text is parsed using ``&`` as an escape character, with ``&r`` + being a newline, ``&&`` being just ``&``, and any other combination causing neither character to display. Other ~~~~~ diff --git a/docs/Removed.rst b/docs/Removed.rst index 5d68b2133..54e83a6fc 100644 --- a/docs/Removed.rst +++ b/docs/Removed.rst @@ -10,6 +10,14 @@ work (e.g. links from the `changelog`). :local: :depth: 1 +.. _devel/unforbidall: + +devel/unforbidall +================= + +Replaced by the `unforbid` script. Run ``unforbid all --quiet`` to match the +behavior of the original ``devel/unforbidall`` script. + .. _digfort: digfort diff --git a/docs/changelog.txt b/docs/changelog.txt index 57fe888b6..dacf382fd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -40,15 +40,14 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Fixes ## Misc Improvements +- `dfhack-examples-guide`: refine food preparation orders and fix conditions for making jugs and pots in the ``basic`` manager orders ## Documentation -- ``Lua API.rst``: added ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` ## API - add functions reverse-engineered from announcement code: ``Gui::parseReportString``, ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` ## Lua -- ``dfhack.gui``: Lua wrappers for functions reverse-engineered from announcement code: ``autoDFAnnouncement``, ``pauseRecenter``, ``recenterViewscreen`` # 0.47.05-r5 diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 4ec165308..9e7bf8590 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -415,10 +415,10 @@ if(APPLE) set_target_properties(dfhack PROPERTIES SOVERSION 1.0.0) endif() -target_link_libraries(dfhack protobuf-lite clsocket lua jsoncpp_lib_static dfhack-version ${PROJECT_LIBS}) +target_link_libraries(dfhack protobuf-lite clsocket lua jsoncpp_static dfhack-version ${PROJECT_LIBS}) set_target_properties(dfhack PROPERTIES INTERFACE_LINK_LIBRARIES "") -target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_lib_static) +target_link_libraries(dfhack-client protobuf-lite clsocket jsoncpp_static) target_link_libraries(dfhack-run dfhack-client) if(APPLE) diff --git a/library/Debug.cpp b/library/Debug.cpp index 7ac981d30..9b13af168 100644 --- a/library/Debug.cpp +++ b/library/Debug.cpp @@ -198,7 +198,7 @@ DebugCategory::cstring_ref DebugCategory::plugin() const noexcept //! standards only provide runtime checks if an atomic type is lock free struct failIfEnumAtomicIsNotLockFree { failIfEnumAtomicIsNotLockFree() { - std::atomic test; + std::atomic test(DebugCategory::LINFO); if (test.is_lock_free()) return; std::cerr << __FILE__ << ':' << __LINE__ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 6954e3996..5881e1b1f 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1509,23 +1509,19 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = { static int gui_autoDFAnnouncement(lua_State *state) { - int rv; + bool rv; df::report_init *r = Lua::GetDFObject(state, 1); if (r) { std::string message = luaL_checkstring(state, 2); - - if (lua_gettop(state) >= 3) - rv = Gui::autoDFAnnouncement(*r, message, lua_toboolean(state, 3)); - else - rv = Gui::autoDFAnnouncement(*r, message); + rv = Gui::autoDFAnnouncement(*r, message); } else { df::coord pos; int color = 0; //initialize these to prevent warning - bool bright = false, is_sparring = false, log_failures = false; + bool bright = false, is_sparring = false; df::unit *unit1 = NULL, *unit2 = NULL; auto type = (df::announcement_type)lua_tointeger(state, 1); @@ -1535,8 +1531,6 @@ static int gui_autoDFAnnouncement(lua_State *state) switch (lua_gettop(state)) { default: - case 9: - log_failures = lua_toboolean(state, 9); case 8: is_sparring = lua_toboolean(state, 8); case 7: @@ -1554,9 +1548,6 @@ static int gui_autoDFAnnouncement(lua_State *state) switch (lua_gettop(state)) { // Use the defaults in Gui.h default: - case 9: - rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, is_sparring, log_failures); - break; case 8: rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, is_sparring); break; @@ -1577,7 +1568,7 @@ static int gui_autoDFAnnouncement(lua_State *state) } } - lua_pushinteger(state, rv); + lua_pushboolean(state, rv); return 1; } diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index 56af85afe..6ab63d5eb 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -168,32 +168,58 @@ std::string to_search_normalized(const std::string &str) return result; } -bool word_wrap(std::vector *out, const std::string &str, size_t line_length) + +bool word_wrap(std::vector *out, const std::string &str, + size_t line_length, bool collapse_whitespace) { - out->clear(); - std::istringstream input(str); - std::string out_line; - std::string word; - if (input >> word) + if (line_length == 0) + line_length = SIZE_MAX; + + std::string line; + size_t break_pos = 0; + + for (auto &c : str) { - out_line += word; - // size_t remaining = line_length - std::min(line_length, word.length()); - while (input >> word) + if (c == '\n') + { + out->push_back(line); + line.clear(); + break_pos = 0; + continue; + } + + if (isspace(c)) { - if (out_line.length() + word.length() + 1 <= line_length) + if (break_pos == line.length() && collapse_whitespace) + continue; + + line.push_back(collapse_whitespace ? ' ' : c); + break_pos = line.length(); + } + else { + line.push_back(c); + } + + if (line.length() > line_length) + { + if (break_pos > 0) { - out_line += ' '; - out_line += word; + // Break before last space, and skip that space + out->push_back(line.substr(0, break_pos - 1)); } else { - out->push_back(out_line); - out_line = word; + // Single word is too long, just break it + out->push_back(line.substr(0, line_length)); + break_pos = line_length; } + line = line.substr(break_pos); + break_pos = 0; } - if (out_line.length()) - out->push_back(out_line); } + if (line.length()) + out->push_back(line); + return true; } diff --git a/library/include/Debug.h b/library/include/Debug.h index 63811a51e..4cad178dc 100644 --- a/library/include/Debug.h +++ b/library/include/Debug.h @@ -326,7 +326,7 @@ public: DFHack::DebugCategory::LDEBUG, ## __VA_ARGS__) /*! - * Open a line for error level debug output if enabled + * Open a line for info level debug output if enabled * * Important debug messages when some rarely changed state changes. Example * would be when a debug category filtering level changes. diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 7a5f050a2..764b11413 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -391,7 +391,8 @@ DFHACK_EXPORT std::string to_search_normalized(const std::string &str); DFHACK_EXPORT bool word_wrap(std::vector *out, const std::string &str, - size_t line_length = 80); + size_t line_length = 80, + bool collapse_whitespace = false); inline bool bits_match(unsigned required, unsigned ok, unsigned mask) { diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 07672aaec..9b78e4984 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -129,10 +129,10 @@ namespace DFHack DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL); // Process an announcement exactly like DF would, which might result in no announcement - DFHACK_EXPORT int autoDFAnnouncement(df::report_init r, std::string message); - DFHACK_EXPORT int autoDFAnnouncement(df::report_init r, std::string message, bool log_failures); - DFHACK_EXPORT int autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false, bool log_failures = false); - + DFHACK_EXPORT bool autoDFAnnouncement(df::report_init r, std::string message); + DFHACK_EXPORT bool autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, + df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false); + /* * Cursor and window coords */ diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index ce43df71f..0c59b0db2 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -37,6 +37,7 @@ using namespace std; #include "Error.h" #include "ModuleFactory.h" #include "Core.h" +#include "Debug.h" #include "PluginManager.h" #include "MiscUtils.h" using namespace DFHack; @@ -112,6 +113,11 @@ using namespace DFHack; #include "df/viewscreen_workshop_profilest.h" #include "df/world.h" +namespace DFHack +{ + DBG_DECLARE(core, gui, DebugCategory::LINFO); +} + using namespace df::enums; using df::global::gamemode; @@ -1707,22 +1713,28 @@ void Gui::showAutoAnnouncement( addCombatReportAuto(unit2, flags, id); } -int Gui::autoDFAnnouncement(df::report_init r, string message) +bool Gui::autoDFAnnouncement(df::report_init r, string message) { // Reverse-engineered from DF announcement code - - if (!world->unk_26a9a8) // TODO: world->show_announcements - return 1; + + if (!world->allow_announcements) + { + DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n%s\n", message.c_str()); + return false; + } df::announcement_flags a_flags; if (is_valid_enum_item(r.type)) a_flags = df::global::d_init->announcements.flags[r.type]; else - return 2; + { + WARN(gui).print("Invalid announcement type:\n%s\n", message.c_str()); + return false; + } if (message.empty()) { Core::printerr("Empty announcement %u\n", r.type); // DF would print this to errorlog.txt - return 3; + return false; } // Check if the announcement will actually be announced @@ -1737,28 +1749,41 @@ int Gui::autoDFAnnouncement(df::report_init r, string message) if ((world->units.active.empty() || (r.unit1 != world->units.active[0] && r.unit2 != world->units.active[0])) && ((Maps::getTileDesignation(r.pos)->whole & 0x10) == 0x0)) // Adventure mode uses this bit to determine current visibility { - return 4; + DEBUG(gui).print("Adventure mode announcement not heard:\n%s\n", message.c_str()); + return false; } } } else { // Dwarf mode (or arena?) if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2))) - return 5; + { + DEBUG(gui).print("Dwarf mode announcement not heard:\n%s\n", message.c_str()); + return false; + } if (!a_flags.bits.D_DISPLAY) { if (a_flags.bits.UNIT_COMBAT_REPORT) { if (r.unit1 == NULL && r.unit2 == NULL) - return 6; + { + DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT because it has no units:\n%s\n", message.c_str()); + return false; + } } else { if (!a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) - return 7; - if (!recent_report_any(r.unit1) && !recent_report_any(r.unit2)) - return 8; + { + DEBUG(gui).print("Skipped announcement not enabled for this game mode:\n%s\n", message.c_str()); + return false; + } + else if (!recent_report_any(r.unit1) && !recent_report_any(r.unit2)) + { + DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT_ALL_ACTIVE because there's no active report:\n%s\n", message.c_str()); + return false; + } } } } @@ -1774,7 +1799,10 @@ int Gui::autoDFAnnouncement(df::report_init r, string message) parseReportString(results, message, line_length); if (results.empty()) - return 9; + { + DEBUG(gui).print("Skipped announcement because it was empty after parsing:\n%s\n", message.c_str()); + return false; + } // Check for repeat report int32_t repeat_count = check_repeat_report(results); @@ -1785,7 +1813,8 @@ int Gui::autoDFAnnouncement(df::report_init r, string message) world->status.display_timer = r.display_timer; Gui::writeToGamelog("x" + to_string(repeat_count + 1)); } - return 0; + DEBUG(gui).print("Announcement succeeded as repeat:\n%s\n", message.c_str()); + return true; } bool success = false; // only print to gamelog if report was used @@ -1864,66 +1893,25 @@ int Gui::autoDFAnnouncement(df::report_init r, string message) delete_old_reports(); - if (/*debug_gamelog &&*/ success) + if (/*debug_gamelog &&*/ success) // TODO: Add debug_gamelog to globals? + { + DEBUG(gui).print("Announcement succeeded and printed to gamelog.txt:\n%s\n", message.c_str()); Gui::writeToGamelog(message); - else if (success) - return 10; + } + /*else if (success) + { + DEBUG(gui).print("Announcement succeeded but skipped printing to gamelog.txt because debug_gamelog is false:\n%s\n", message.c_str()); + }*/ else - return 11; - - return 0; -} - -int Gui::autoDFAnnouncement(df::report_init r, string message, bool log_failures) -{ // Prints info about failed announcements to DFHack console if log_failures is true - int rv = autoDFAnnouncement(r, message); - - if (log_failures) { - switch (rv) - { - case 0: - break; // success - case 1: - Core::print("Skipped an announcement because world->show_announcements is false:\n%s\n", message.c_str()); - break; - case 2: - Core::printerr("Invalid announcement type!\n"); - break; - case 3: - break; // empty announcement, already handled - case 4: - Core::print("An adventure announcement occured, but nobody heard:\n%s\n", message.c_str()); - break; - case 5: - Core::print("An announcement occured, but nobody heard:\n%s\n", message.c_str()); - break; - case 6: - Core::print("Skipped a UNIT_COMBAT_REPORT because it has no units:\n%s\n", message.c_str()); - break; - case 7: - Core::print("Skipped an announcement not enabled for this game mode:\n%s\n", message.c_str()); - break; - case 8: - Core::print("Skipped an announcement because there's no active report:\n%s\n", message.c_str()); - break; - case 9: - Core::print("Skipped an announcement because it was empty after parsing:\n%s\n", message.c_str()); - break; - case 10: - Core::print("Report added but skipped printing to gamelog.txt because debug_gamelog is false.\n"); - break; - case 11: - Core::print("Report added but didn't qualify to be displayed anywhere:\n%s\n", message.c_str()); - break; - default: - Core::printerr("autoDFAnnouncement: Unexpected return value!\n"); - } + DEBUG(gui).print("Announcement succeeded internally but didn't qualify to be displayed anywhere:\n%s\n", message.c_str()); } - return rv; + + return true; } -int Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color, bool bright, df::unit *unit1, df::unit *unit2, bool is_sparring, bool log_failures) +bool Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color, + bool bright, df::unit *unit1, df::unit *unit2, bool is_sparring) { auto r = df::report_init(); r.type = type; @@ -1937,7 +1925,7 @@ int Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::stri if (Maps::isValidTilePos(pos)) r.zoom_type = report_zoom_type::Unit; - return autoDFAnnouncement(r, message, log_failures); + return autoDFAnnouncement(r, message); } df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed) diff --git a/library/xml b/library/xml index 7ec5d8699..59075f42b 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7ec5d86996269df5e01d64ea5bae67d0c29afd77 +Subproject commit 59075f42bbc77c354b5f815c5c1cce5bf48e76a5 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index b8cd25860..ed9404b8e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -87,7 +87,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(autoclothing autoclothing.cpp) dfhack_plugin(autodump autodump.cpp) dfhack_plugin(autofarm autofarm.cpp) - dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_lib_static) + dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(autohauler autohauler.cpp) dfhack_plugin(autolabor autolabor.cpp) dfhack_plugin(automaterial automaterial.cpp LINK_LIBRARIES lua) @@ -109,7 +109,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(cursecheck cursecheck.cpp) dfhack_plugin(cxxrandom cxxrandom.cpp LINK_LIBRARIES lua) dfhack_plugin(deramp deramp.cpp) - dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_lib_static) + dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(dig dig.cpp) dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua) dfhack_plugin(digFlood digFlood.cpp) @@ -143,7 +143,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(mode mode.cpp) dfhack_plugin(mousequery mousequery.cpp) dfhack_plugin(nestboxes nestboxes.cpp) - dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_lib_static) + dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(petcapRemover petcapRemover.cpp) dfhack_plugin(plants plants.cpp) diff --git a/plugins/autolabor.cpp b/plugins/autolabor.cpp index d6921f1ac..ba116c1c5 100644 --- a/plugins/autolabor.cpp +++ b/plugins/autolabor.cpp @@ -873,7 +873,7 @@ static void assign_labor(unit_labor::unit_labor labor, } int pool = labor_infos[labor].talent_pool(); - if (pool < 200 && candidates.size() > 1 && abs(pool) < candidates.size()) + if (pool < 200 && candidates.size() > 1 && size_t(abs(pool)) < candidates.size()) { // Sort in descending order std::sort(candidates.begin(), candidates.end(), [&](const int lhs, const int rhs) -> bool { diff --git a/plugins/command-prompt.cpp b/plugins/command-prompt.cpp index a987ea829..ba2fe0e87 100644 --- a/plugins/command-prompt.cpp +++ b/plugins/command-prompt.cpp @@ -1,23 +1,25 @@ -//command-prompt a one line command entry at the top of the screen for quick commands +// command-prompt: A one-line command entry at the top of the screen for quick commands #include "Core.h" +#include #include #include +#include #include -#include -#include #include +#include -#include #include +#include +#include #include #include +#include "df/enabler.h" +#include "df/graphic.h" #include "df/interface_key.h" #include "df/ui.h" -#include "df/graphic.h" -#include "df/enabler.h" using namespace DFHack; using namespace df::enums; @@ -36,8 +38,10 @@ class prompt_ostream:public buffered_color_ostream protected: void flush_proxy(); public: - prompt_ostream(viewscreen_commandpromptst* parent):parent_(parent){} - bool empty(){return buffer.empty();} + prompt_ostream(viewscreen_commandpromptst* parent) + : parent_(parent) + {} + bool empty() { return buffer.empty(); } }; class viewscreen_commandpromptst : public dfhack_viewscreen { public: @@ -48,7 +52,7 @@ public: } void render(); - void help() { } + void help() {} int8_t movies_okay() { return 0; } df::unit* getSelectedUnit() { return Gui::getAnyUnit(parent); } @@ -57,10 +61,11 @@ public: df::plant* getSelectedPlant() { return Gui::getAnyPlant(parent); } std::string getFocusString() { return "commandprompt"; } - viewscreen_commandpromptst(std::string entry):submitted(false), is_response(false) + viewscreen_commandpromptst(std::string entry) + : submitted(false), is_response(false) { - show_fps=gps->display_frames; - gps->display_frames=0; + show_fps = gps->display_frames; + gps->display_frames = 0; cursor_pos = entry.size(); frame = 0; history_idx = command_history.size(); @@ -76,7 +81,7 @@ public: } ~viewscreen_commandpromptst() { - gps->display_frames=show_fps; + gps->display_frames = show_fps; } void add_response(color_value v, std::string s) @@ -125,7 +130,7 @@ public: } protected: - std::list > responses; + std::list > responses; int cursor_pos; int history_idx; bool submitted; @@ -138,8 +143,8 @@ void prompt_ostream::flush_proxy() { if (buffer.empty()) return; - for(auto it=buffer.begin();it!=buffer.end();it++) - parent_->add_response(it->first,it->second); + for(auto it = buffer.begin(); it != buffer.end(); it++) + parent_->add_response(it->first, it->second); buffer.clear(); } void viewscreen_commandpromptst::render() @@ -154,25 +159,31 @@ void viewscreen_commandpromptst::render() auto dim = Screen::getWindowSize(); parent->render(); - if(is_response) + if (is_response) { - auto it=responses.begin(); - for(int i=0;isecond; - Screen::paintString(Screen::Pen(' ',it->first,0),0,i,cur_line.substr(0,cur_line.size()-1)); + std::vector lines; + word_wrap(&lines, response.second, dim.x); + for (auto &line : lines) + { + Screen::fillRect(Screen::Pen(' ', 7, 0), 0, y, dim.x, y); + Screen::paintString(Screen::Pen(' ', response.first, 0), 0, y, line); + if (++y >= dim.y) + return; + } } } else { std::string entry = get_entry(); - Screen::fillRect(Screen::Pen(' ', 7, 0),0,0,dim.x,0); - Screen::paintString(Screen::Pen(' ', 7, 0), 0, 0,"[DFHack]#"); + Screen::fillRect(Screen::Pen(' ', 7, 0), 0, 0, dim.x, 0); + Screen::paintString(Screen::Pen(' ', 7, 0), 0, 0, "[DFHack]#"); std::string cursor = (frame < enabler->gfps / 2) ? "_" : " "; - if(cursor_pos < (dim.x - 10)) + if (cursor_pos < dim.x - 10) { - Screen::paintString(Screen::Pen(' ', 7, 0), 10,0 , entry); + Screen::paintString(Screen::Pen(' ', 7, 0), 10, 0, entry); if (int16_t(entry.size()) > dim.x - 10) Screen::paintTile(Screen::Pen('\032', 7, 0), dim.x - 1, 0); if (cursor != " ") @@ -191,12 +202,12 @@ void viewscreen_commandpromptst::render() void viewscreen_commandpromptst::submit() { CoreSuspendClaimer suspend; - if(is_response) + if (is_response) { Screen::dismiss(this); return; } - if(submitted) + if (submitted) return; submitted = true; prompt_ostream out(this); @@ -204,11 +215,11 @@ void viewscreen_commandpromptst::submit() Screen::Hide hide_guard(this, Screen::Hide::RESTORE_AT_TOP); Core::getInstance().runCommand(out, get_entry()); } - if(out.empty() && responses.empty()) + if (out.empty() && responses.empty()) Screen::dismiss(this); else { - is_response=true; + is_response = true; } } void viewscreen_commandpromptst::feed(std::set *events) @@ -240,14 +251,14 @@ void viewscreen_commandpromptst::feed(std::set *events) for (auto it = events->begin(); it != events->end(); ++it) { auto key = *it; - if (key==interface_key::STRING_A000) //delete? + if (key == interface_key::STRING_A000) //delete? { - if(entry.size() && cursor_pos > 0) + if (entry.size() && cursor_pos > 0) { entry.erase(cursor_pos - 1, 1); cursor_pos--; } - if(size_t(cursor_pos) > entry.size()) + if (size_t(cursor_pos) > entry.size()) cursor_pos = entry.size(); continue; } @@ -261,34 +272,34 @@ void viewscreen_commandpromptst::feed(std::set *events) } } // Prevent number keys from moving cursor - if(events->count(interface_key::CURSOR_RIGHT)) + if (events->count(interface_key::CURSOR_RIGHT)) { cursor_pos++; if (size_t(cursor_pos) > entry.size()) cursor_pos = entry.size(); } - else if(events->count(interface_key::CURSOR_LEFT)) + else if (events->count(interface_key::CURSOR_LEFT)) { cursor_pos--; if (cursor_pos < 0) cursor_pos = 0; } - else if(events->count(interface_key::CURSOR_RIGHT_FAST)) + else if (events->count(interface_key::CURSOR_RIGHT_FAST)) { forward_word(); } - else if(events->count(interface_key::CURSOR_LEFT_FAST)) + else if (events->count(interface_key::CURSOR_LEFT_FAST)) { back_word(); } - else if(events->count(interface_key::CUSTOM_CTRL_A)) + else if (events->count(interface_key::CUSTOM_CTRL_A)) { cursor_pos = 0; } - else if(events->count(interface_key::CUSTOM_CTRL_E)) + else if (events->count(interface_key::CUSTOM_CTRL_E)) { cursor_pos = entry.size(); } - else if(events->count(interface_key::CURSOR_UP)) + else if (events->count(interface_key::CURSOR_UP)) { history_idx--; if (history_idx < 0) @@ -296,7 +307,7 @@ void viewscreen_commandpromptst::feed(std::set *events) entry = get_entry(); cursor_pos = entry.size(); } - else if(events->count(interface_key::CURSOR_DOWN)) + else if (events->count(interface_key::CURSOR_DOWN)) { if (size_t(history_idx) < command_history.size() - 1) { @@ -321,8 +332,8 @@ command_result show_prompt(color_ostream &out, std::vector & param return CR_OK; } std::string params; - for(size_t i=0;i(params), plugin_self); return CR_OK; } @@ -330,21 +341,23 @@ bool hotkey_allow_all(df::viewscreen *top) { return true; } -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand( - "command-prompt","Shows a command prompt on window.",show_prompt,hotkey_allow_all, - "command-prompt [entry] - shows a cmd prompt in df window. Entry is used for default prefix (e.g. ':lua')" + "command-prompt", "Shows a command prompt on window.", + show_prompt, hotkey_allow_all, + "command-prompt [entry] - shows a cmd prompt in df window." + " Entry is used for default prefix (e.g. ':lua')" )); return CR_OK; } -DFhackCExport command_result plugin_onstatechange (color_ostream &out, state_change_event e) +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event e) { return CR_OK; } -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +DFhackCExport command_result plugin_shutdown(color_ostream &out) { return CR_OK; } diff --git a/plugins/isoworld b/plugins/isoworld index e3c49ab01..3630c816d 160000 --- a/plugins/isoworld +++ b/plugins/isoworld @@ -1 +1 @@ -Subproject commit e3c49ab017da2dcbeaadccd10e56d07d8f03b4ca +Subproject commit 3630c816df6962b4594d46d1ae75974c36c11629 diff --git a/scripts b/scripts index b808050d4..e062237ee 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit b808050d4a3885aa0e250e726708b2b28fe28260 +Subproject commit e062237ee03a22d0d8b88b725ba3712e649f1bf6 From 4b21e7afb4ecb0b90cd4fd3a7c716ec2cfeb5e23 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 24 May 2022 03:52:33 -0700 Subject: [PATCH 016/161] Remove parseReportString from API (now utility fn) Implementations using `word_wrap()` are commented out pending changes to that function. --- docs/Lua API.rst | 11 +- docs/Removed.rst | 21 ++++ docs/changelog.txt | 7 +- library/include/modules/Gui.h | 3 +- library/lua/gui/widgets.lua | 39 ++++--- library/modules/Gui.cpp | 197 ++++++++++++++++++++++------------ library/xml | 2 +- plugins/confirm.cpp | 2 + plugins/lua/confirm.lua | 8 ++ scripts | 2 +- test/library/gui/widgets.lua | 18 ++++ 11 files changed, 224 insertions(+), 86 deletions(-) create mode 100644 test/library/gui/widgets.lua diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 7e1f45231..5a5737b93 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1095,7 +1095,7 @@ Announcements and a string and processes them just like DF does. Sometimes this means the announcement won't occur. Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to ``true`` means the report will be added to sparring logs (if applicable) rather than hunting or combat. Text is parsed using ``&`` as an escape character, with ``&r`` - being a newline, ``&&`` being just ``&``, and any other combination causing neither character to display. + adding a blank line (equivalent to ``\n \n``,) ``&&`` being just ``&``, and any other combination causing neither character to display. Other ~~~~~ @@ -3856,6 +3856,7 @@ Subclass of Widget; implements a simple edit field. Attributes: +:label_text: The optional text label displayed before the editable text. :text: The current contents of the field. :text_pen: The pen to draw the text with. :on_char: Input validation callback; used as ``on_char(new_char,text)``. @@ -3863,6 +3864,8 @@ Attributes: :on_change: Change notification callback; used as ``on_change(new_text,old_text)``. :on_submit: Enter key callback; if set the field will handle the key and call ``on_submit(text)``. :key: If specified, the field is disabled until this key is pressed. Must be given as a string. +:key_sep: If specified, will be used to customize how the activation key is + displayed. See ``token.key_sep`` in the ``Label`` documentation below. Label class ----------- @@ -3931,8 +3934,8 @@ containing newlines, or a table with the following possible fields: * ``token.key_sep = '...'`` Specifies the separator to place between the keybinding label produced - by ``token.key``, and the main text of the token. If the separator is - '()', the token is formatted as ``text..' ('..binding..')'``. Otherwise + by ``token.key``, and the main text of the token. If the separator starts with + '()', the token is formatted as ``text..' ('..binding..sep:sub(2)``. Otherwise it is simply ``binding..sep..text``. * ``token.enabled``, ``token.disabled`` @@ -4023,6 +4026,8 @@ a hotkey. It has the following attributes: :key: The hotkey keycode to display, e.g. ``'CUSTOM_A'``. +:key_sep: If specified, will be used to customize how the activation key is + displayed. See ``token.key_sep`` in the ``Label`` documentation. :label: The string (or a function that returns a string) to display after the hotkey. :on_activate: If specified, it is the callback that will be called whenever diff --git a/docs/Removed.rst b/docs/Removed.rst index 54e83a6fc..e94b04cf8 100644 --- a/docs/Removed.rst +++ b/docs/Removed.rst @@ -18,6 +18,27 @@ devel/unforbidall Replaced by the `unforbid` script. Run ``unforbid all --quiet`` to match the behavior of the original ``devel/unforbidall`` script. +.. _deteriorateclothes: + +deteriorateclothes +================== +Replaced by the new combined `deteriorate` script. Run +``deteriorate --types=clothes``. + +.. _deterioratecorpses: + +deterioratecorpses +================== +Replaced by the new combined `deteriorate` script. Run +``deteriorate --types=corpses``. + +.. _deterioratefood: + +deterioratefood +=============== +Replaced by the new combined `deteriorate` script. Run +``deteriorate --types=food``. + .. _digfort: digfort diff --git a/docs/changelog.txt b/docs/changelog.txt index dacf382fd..34b89d93e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -38,16 +38,21 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## New Tweaks ## Fixes +- ``widgets.CycleHotkeyLabel``: allow initial option values to be specified as an index instead of an option value ## Misc Improvements +- `confirm`: added a confirmation dialog for removing manager orders - `dfhack-examples-guide`: refine food preparation orders and fix conditions for making jugs and pots in the ``basic`` manager orders ## Documentation ## API -- add functions reverse-engineered from announcement code: ``Gui::parseReportString``, ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` +- add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` ## Lua +- ``widgets.HotkeyLabel``: the ``key_sep`` string is now configurable +- ``widgets.EditField``: the ``key_sep`` string is now configurable +- ``widgets.EditField``: can now display an optional string label in addition to the activation key # 0.47.05-r5 diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 9b78e4984..c7f857a1d 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -115,7 +115,6 @@ namespace DFHack // Low-level API that gives full control over announcements and reports DFHACK_EXPORT void writeToGamelog(std::string message); - DFHACK_EXPORT bool parseReportString(std::vector &out, const std::string &str, size_t line_length = 73); DFHACK_EXPORT int makeAnnouncement(df::announcement_type type, df::announcement_flags mode, df::coord pos, std::string message, int color = 7, bool bright = true); DFHACK_EXPORT bool addCombatReport(df::unit *unit, df::unit_report_type slot, int report_index); DFHACK_EXPORT bool addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index); @@ -131,7 +130,7 @@ namespace DFHack // Process an announcement exactly like DF would, which might result in no announcement DFHACK_EXPORT bool autoDFAnnouncement(df::report_init r, std::string message); DFHACK_EXPORT bool autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, - df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false); + df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false); /* * Cursor and window coords diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index b8782bd04..21e03370b 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -178,14 +178,27 @@ end EditField = defclass(EditField, Widget) EditField.ATTRS{ + label_text = DEFAULT_NIL, text = '', text_pen = DEFAULT_NIL, on_char = DEFAULT_NIL, on_change = DEFAULT_NIL, on_submit = DEFAULT_NIL, key = DEFAULT_NIL, + key_sep = DEFAULT_NIL, } +function EditField:init() + self:addviews{HotkeyLabel{frame={t=0,l=0}, + key=self.key, + key_sep=self.key_sep, + label=self.label_text}} +end + +function EditField:postUpdateLayout() + self.text_offset = self.subviews[1]:getTextWidth() +end + function EditField:onRenderBody(dc) dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0) @@ -194,16 +207,11 @@ function EditField:onRenderBody(dc) cursor = ' ' end local txt = self.text .. cursor - local dx = dc.x - if self.key then - dc:key_string(self.key, '') - end - dx = dc.x - dx - local max_width = dc.width - dx + local max_width = dc.width - self.text_offset if #txt > max_width then txt = string.char(27)..string.sub(txt, #txt-max_width+2) end - dc:string(txt) + dc:advance(self.text_offset):string(txt) end function EditField:onInput(keys) @@ -359,12 +367,13 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled) x = x + #keystr - if sep == '()' then + if sep:startswith('()') then if dc then dc:string(text) - dc:string(' ('):string(keystr,keypen):string(')') + dc:string(' ('):string(keystr,keypen) + dc:string(sep:sub(2)) end - x = x + 3 + x = x + 1 + #sep else if dc then dc:string(keystr,keypen):string(sep):string(text) @@ -605,13 +614,14 @@ HotkeyLabel = defclass(HotkeyLabel, Label) HotkeyLabel.ATTRS{ key=DEFAULT_NIL, + key_sep=': ', label=DEFAULT_NIL, on_activate=DEFAULT_NIL, } function HotkeyLabel:init() - self:setText{{key=self.key, key_sep=': ', text=self.label, - on_activate=self.on_activate}} + self:setText{{key=self.key, key_sep=self.key_sep, text=self.label, + on_activate=self.on_activate}} end ---------------------- @@ -637,6 +647,11 @@ function CycleHotkeyLabel:init() break end end + if not self.option_idx then + if self.options[self.initial_option] then + self.option_idx = self.initial_option + end + end if not self.option_idx then error(('cannot find option with value or index: "%s"') :format(self.initial_option)) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 0c59b0db2..828c2632f 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1368,82 +1368,150 @@ DFHACK_EXPORT void Gui::writeToGamelog(std::string message) fseed.close(); } -bool Gui::parseReportString(std::vector &out, const std::string &str, size_t line_length) -{ // out vector will contain strings cut to line_length, avoiding cutting up words - // Reverse-engineered from DF announcement code, fixes applied +namespace +{ // Utility functions for reports + /*bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) + { + if (str.empty() || line_length == 0) + return false; - if (str.empty() || line_length == 0) - return false; - out.clear(); + string parsed = ""; + size_t i = 0; - bool ignore_space = false; - string current_line = ""; - size_t iter = 0; - do - { - if (ignore_space) + while (i < str.length()) { - if (str[iter] == ' ') - continue; - ignore_space = false; + if (str[i] == '&') // escape character + { + i++; // ignore the '&' itself + if (i >= str.length()) + break; + + if (str[i] == 'r') // "&r" adds a blank line + parsed += "\n \n"; // DF adds a line with a space for some reason + else if (str[i] == '&') // "&&" is '&' + parsed += "&"; + // else next char is ignored + } + else + { + parsed += str[i]; + } + i++; } - if (str[iter] == '&') // escape character - { - iter++; // ignore the '&' itself - if (iter >= str.length()) - break; + return word_wrap(out, parsed, line_length, true); + }*/ + /*bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) + { + if (str.empty() || line_length == 0) + return false; + + string parsed = ""; + size_t i = 0; - if (str[iter] == 'r') // "&r" starts new line + while (i < str.length()) + { + if (str[i] == '&') // escape character { - if (!current_line.empty()) + i++; // ignore the '&' itself + if (i >= str.length()) + break; + + if (str[i] == 'r') // "&r" adds a blank line { - out.push_back(string(current_line)); - current_line = ""; + word_wrap(out, parsed, line_length, true); + out->push_back(" "); // DF adds a line with a space for some reason + parsed = ""; } - out.push_back(" "); - continue; // don't add 'r' to current_line + else if (str[i] == '&') // "&&" is '&' + parsed += "&"; + // else next char is ignored } - else if (str[iter] != '&') - { // not "&&", don't add character to current_line - continue; + else + { + parsed += str[i]; } + i++; } - current_line += str[iter]; - if (current_line.length() > line_length) - { - size_t i = current_line.length(); // start of current word - size_t j; // end of previous word - while (--i > 0 && current_line[i] != ' '); // find start of current word + if (parsed != "") + word_wrap(out, parsed, line_length, true); + + return true; + }*/ + bool parseReportString(std::vector *out, const std::string &str, size_t line_length) + { // out vector will contain strings cut to line_length, avoiding cutting up words + // Reverse-engineered from DF announcement code, fixes applied + + if (str.empty() || line_length == 0) + return false; - if (i == 0) - { // need to push at least one char - j = i = line_length; // last char ends up on next line + bool ignore_space = false; + string current_line = ""; + size_t iter = 0; + do + { + if (ignore_space) + { + if (str[iter] == ' ') + continue; + ignore_space = false; } - else + + if (str[iter] == '&') // escape character { - j = i; - while (j > 1 && current_line[j - 1] == ' ') - j--; // consume excess spaces at the split point + iter++; // ignore the '&' itself + if (iter >= str.length()) + break; + + if (str[iter] == 'r') // "&r" adds a blank line + { + if (!current_line.empty()) + { + out->push_back(string(current_line)); + current_line = ""; + } + out->push_back(" "); // DF adds a line with a space for some reason + continue; // don't add 'r' to current_line + } + else if (str[iter] != '&') + { // not "&&", don't add character to current_line + continue; + } } - out.push_back(current_line.substr(0, j)); // push string before j - if (current_line[i] == ' ') - i++; // don't keep this space - current_line.erase(0, i); // current_line now starts at last word or is empty - ignore_space = current_line.empty(); // ignore leading spaces on new line - } - } while (++iter < str.length()); + current_line += str[iter]; + if (current_line.length() > line_length) + { + size_t i = current_line.length(); // start of current word + size_t j; // end of previous word + while (--i > 0 && current_line[i] != ' '); // find start of current word - if (!current_line.empty()) - out.push_back(current_line); + if (i == 0) + { // need to push at least one char + j = i = line_length; // last char ends up on next line + } + else + { + j = i; + while (j > 1 && current_line[j - 1] == ' ') + j--; // consume excess spaces at the split point + } + out->push_back(current_line.substr(0, j)); // push string before j - return true; -} + if (current_line[i] == ' ') + i++; // don't keep this space + current_line.erase(0, i); // current_line now starts at last word or is empty + ignore_space = current_line.empty(); // ignore leading spaces on new line + } + } while (++iter < str.length()); + + if (!current_line.empty()) + out->push_back(current_line); + + return true; + } -namespace -{ // Utility functions for reports bool recent_report(df::unit *unit, df::unit_report_type slot) { if (unit && !unit->reports.log[slot].empty() && @@ -1755,7 +1823,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) } } else - { // Dwarf mode (or arena?) + { // Dwarf mode if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2))) { DEBUG(gui).print("Dwarf mode announcement not heard:\n%s\n", message.c_str()); @@ -1796,16 +1864,16 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) vector results; size_t line_length = (r.speaker_id == -1) ? (init->display.grid_x - 7) : (init->display.grid_x - 10); - parseReportString(results, message, line_length); + parseReportString(&results, message, line_length); - if (results.empty()) + if (results.empty()) // DF doesn't do this check { DEBUG(gui).print("Skipped announcement because it was empty after parsing:\n%s\n", message.c_str()); return false; } // Check for repeat report - int32_t repeat_count = check_repeat_report(results); + int32_t repeat_count = check_repeat_report(results); // Does nothing outside dwarf mode if (repeat_count > 0) { if (a_flags.bits.D_DISPLAY) @@ -1818,7 +1886,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) } bool success = false; // only print to gamelog if report was used - size_t new_report_index = world->status.reports.size(); + size_t new_report_index = world->status.reports.size(); // we need this for addCombatReport for (size_t i = 0; i < results.size(); i++) { // Generate report entries for each line auto new_report = new df::report(); @@ -1854,7 +1922,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) } } - if (*gamemode == game_mode::DWARF) + if (*gamemode == game_mode::DWARF) // DF does this inside the previous loop, but we're using addCombatReport instead { if (a_flags.bits.UNIT_COMBAT_REPORT) { @@ -1902,7 +1970,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) { DEBUG(gui).print("Announcement succeeded but skipped printing to gamelog.txt because debug_gamelog is false:\n%s\n", message.c_str()); }*/ - else + else // not sure if this can actually happen; our results.empty() check handles the one edge case I can think of that would get this far { DEBUG(gui).print("Announcement succeeded internally but didn't qualify to be displayed anywhere:\n%s\n", message.c_str()); } @@ -1911,7 +1979,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) } bool Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color, - bool bright, df::unit *unit1, df::unit *unit2, bool is_sparring) + bool bright, df::unit *unit1, df::unit *unit2, bool is_sparring) { auto r = df::report_init(); r.type = type; @@ -1922,9 +1990,6 @@ bool Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::str r.unit2 = unit2; r.flags.bits.hostile_combat = !is_sparring; - if (Maps::isValidTilePos(pos)) - r.zoom_type = report_zoom_type::Unit; - return autoDFAnnouncement(r, message); } diff --git a/library/xml b/library/xml index 59075f42b..a59495e8f 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 59075f42bbc77c354b5f815c5c1cce5bf48e76a5 +Subproject commit a59495e8f72115909772e6df20a7b9dec272f14c diff --git a/plugins/confirm.cpp b/plugins/confirm.cpp index a57ec45dd..6a2798cb4 100644 --- a/plugins/confirm.cpp +++ b/plugins/confirm.cpp @@ -18,6 +18,7 @@ #include "df/general_ref.h" #include "df/general_ref_contained_in_itemst.h" #include "df/viewscreen_dwarfmodest.h" +#include "df/viewscreen_jobmanagementst.h" #include "df/viewscreen_justicest.h" #include "df/viewscreen_layer_militaryst.h" #include "df/viewscreen_locationsst.h" @@ -481,6 +482,7 @@ DEFINE_CONFIRMATION(note_delete, viewscreen_dwarfmodest); DEFINE_CONFIRMATION(route_delete, viewscreen_dwarfmodest); DEFINE_CONFIRMATION(location_retire, viewscreen_locationsst); DEFINE_CONFIRMATION(convict, viewscreen_justicest); +DEFINE_CONFIRMATION(order_remove, viewscreen_jobmanagementst); DFhackCExport command_result plugin_init (color_ostream &out, vector &commands) { diff --git a/plugins/lua/confirm.lua b/plugins/lua/confirm.lua index 13b28962a..e36a4fb86 100644 --- a/plugins/lua/confirm.lua +++ b/plugins/lua/confirm.lua @@ -219,6 +219,14 @@ function convict.get_message() "This action is irreversible." end +order_remove = defconf('order-remove') +function order_remove.intercept_key(key) + return key == keys.MANAGER_REMOVE and + not screen.in_max_workshops +end +order_remove.title = "Remove manager order" +order_remove.message = "Are you sure you want to remove this order?" + -- End of confirmation definitions function check() diff --git a/scripts b/scripts index e062237ee..05d46b32a 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e062237ee03a22d0d8b88b725ba3712e649f1bf6 +Subproject commit 05d46b32a3aff4f5f98534fdccfbf9ae88dd31a3 diff --git a/test/library/gui/widgets.lua b/test/library/gui/widgets.lua new file mode 100644 index 000000000..1eed30e4f --- /dev/null +++ b/test/library/gui/widgets.lua @@ -0,0 +1,18 @@ +local widgets = require('gui.widgets') + +function test.togglehotkeylabel() + local toggle = widgets.ToggleHotkeyLabel{} + expect.true_(toggle:getOptionValue()) + toggle:cycle() + expect.false_(toggle:getOptionValue()) + toggle:cycle() + expect.true_(toggle:getOptionValue()) +end + +function test.togglehotkeylabel_default_value() + local toggle = widgets.ToggleHotkeyLabel{initial_option=2} + expect.false_(toggle:getOptionValue()) + + toggle = widgets.ToggleHotkeyLabel{initial_option=false} + expect.false_(toggle:getOptionValue()) +end From 2b294318060c0251831ccc42d01f678eca04f988 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 28 May 2022 12:35:49 -0700 Subject: [PATCH 017/161] More fixes * Use word_wrap() * add_proper_report utility fn; have addCombatReportAuto use this * Update Lua API.rst * Update Gui.cpp --- README.md | 6 +- ci/test.lua | 12 +- depends/libexpat | 2 +- docs/Lua API.rst | 26 ++-- library/lua/gui/dialogs.lua | 8 +- library/lua/gui/widgets.lua | 12 +- library/lua/test_util/mock.lua | 52 ++++++- library/modules/Gui.cpp | 218 +++++++---------------------- library/xml | 2 +- scripts | 2 +- test/library/gui/widgets.Label.lua | 94 +++++++++++++ test/library/test_util/mock.lua | 37 ++++- 12 files changed, 273 insertions(+), 198 deletions(-) create mode 100644 test/library/gui/widgets.Label.lua diff --git a/README.md b/README.md index 15f13dbc9..fa6c343e1 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ DFHack is a Dwarf Fortress memory access library, distributed with scripts and plugins implementing a wide variety of useful functions and tools. -The full documentation [is available online here](https://dfhack.readthedocs.org), -from the README.html page in the DFHack distribution, or as raw text in the `./docs` folder. +The full documentation [is available online here](https://dfhack.readthedocs.org). +It is also accessible via the README.html page in the DFHack distribution or as raw text in the `./docs` folder. If you're an end-user, modder, or interested in contributing to DFHack - go read those docs. -If that's unclear, or you need more help checkout our [support page](https://docs.dfhack.org/en/latest/docs/Support.html) for up-to-date options. +If the docs are unclear or you need more help, please check out our [support page](https://docs.dfhack.org/en/latest/docs/Support.html) for ways to contact the DFHack developers. diff --git a/ci/test.lua b/ci/test.lua index 9a4a33ef2..aca78e851 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -240,7 +240,7 @@ end -- output doesn't trigger its own dfhack.printerr usage detection (see -- detect_printerr below) local orig_printerr = dfhack.printerr -local function wrap_expect(func, private) +local function wrap_expect(func, private, path) return function(...) private.checks = private.checks + 1 local ret = {func(...)} @@ -269,7 +269,7 @@ local function wrap_expect(func, private) end -- Skip any frames corresponding to C calls, or Lua functions defined in another file -- these could include pcall(), with_finalize(), etc. - if info.what == 'Lua' and info.short_src == caller_src then + if info.what == 'Lua' and (info.short_src == caller_src or info.short_src == path) then orig_printerr((' at %s:%d'):format(info.short_src, info.currentline)) end frame = frame + 1 @@ -278,7 +278,7 @@ local function wrap_expect(func, private) end end -local function build_test_env() +local function build_test_env(path) local env = { test = utils.OrderedTable(), -- config values can be overridden in the test file to define @@ -309,7 +309,7 @@ local function build_test_env() checks_ok = 0, } for name, func in pairs(expect) do - env.expect[name] = wrap_expect(func, private) + env.expect[name] = wrap_expect(func, private, path) end setmetatable(env, {__index = _G}) return env, private @@ -345,9 +345,9 @@ local function finish_tests(done_command) end local function load_tests(file, tests) - local short_filename = file:sub((file:find('test') or -4)+5, -1) + local short_filename = file:sub((file:find('test') or -4) + 5, -1) print('Loading file: ' .. short_filename) - local env, env_private = build_test_env() + local env, env_private = build_test_env(file) local code, err = loadfile(file, 't', env) if not code then dfhack.printerr('Failed to load file: ' .. tostring(err)) diff --git a/depends/libexpat b/depends/libexpat index 3e877cbb3..6ac8628a3 160000 --- a/depends/libexpat +++ b/depends/libexpat @@ -1 +1 @@ -Subproject commit 3e877cbb3c9bc8f22946053e70490d2e5431f4d5 +Subproject commit 6ac8628a3c7a1677b27fb007db96f665b684a183 diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 5a5737b93..8ec347bef 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1007,17 +1007,19 @@ Fortress mode * ``dfhack.gui.pauseRecenter(pos[,pause])`` ``dfhack.gui.pauseRecenter(x,y,z[,pause])`` - Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``, and respects - RECENTER_INTERFACE_SHUTDOWN_MS (the delay before input is recognized when a recenter occurs) in DF's init.txt. + Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``. Respects + ``RECENTER_INTERFACE_SHUTDOWN_MS`` in DF's ``init.txt`` (the delay before input is recognized when a recenter occurs.) * ``dfhack.gui.recenterViewscreen(pos[,zoom])`` ``dfhack.gui.recenterViewscreen(x,y,z[,zoom])`` ``dfhack.gui.recenterViewscreen([zoom])`` Recenter the view on a position using a specific zoom type. If no position is given, - recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` (see: `enum definition `_), - where ``Generic`` skips recentering and enforces valid view bounds (the same as x = -30000,) ``Item`` brings - the position onscreen without centering, and ``Unit`` centers the screen on the position. Default zoom type is Item. + recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` + (see: `enum definition `_), + where ``df.report_zoom_type.Generic`` skips recentering and enforces valid view bounds (the same as x = -30000,) + ``df.report_zoom_type.Item`` brings the position onscreen without centering, and + ``df.report_zoom_type.Unit`` centers the screen on the position. Default zoom type is ``df.report_zoom_type.Item``. * ``dfhack.gui.revealInDwarfmodeMap(pos)`` @@ -1091,11 +1093,15 @@ Announcements * ``dfhack.gui.autoDFAnnouncement(report,text)`` ``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring])`` - Takes a ``df.report_init`` (see: `structure definition `_) - and a string and processes them just like DF does. Sometimes this means the announcement won't occur. - Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to ``true`` means the report - will be added to sparring logs (if applicable) rather than hunting or combat. Text is parsed using ``&`` as an escape character, with ``&r`` - adding a blank line (equivalent to ``\n \n``,) ``&&`` being just ``&``, and any other combination causing neither character to display. + Takes a ``df.report_init`` (see: `structure definition `_) + and a string and processes them just like DF does. Can also be built from parameters instead of a ``report_init``. + Setting ``is_sparring`` to *true* means the report will be added to sparring logs (if applicable) rather than hunting or combat. + + The announcement will not display if units are involved and the player can't see them (or hear, for adventure mode sound announcement types.) + Text is parsed using ``&`` as an escape character, with ``&r`` adding a blank line (equivalent to ``\n \n``,) + ``&&`` being just ``&``, and any other combination causing neither character to display. + + If you want a guaranteed announcement without parsing, use ``dfhack.gui.showAutoAnnouncement`` instead. Other ~~~~~ diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index 51f346bbd..952e8f734 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -36,7 +36,13 @@ end function MessageBox:getWantedFrameSize() local label = self.subviews.label local width = math.max(self.frame_width or 0, 20, #(self.frame_title or '') + 4) - return math.max(width, label:getTextWidth()), label:getTextHeight() + local text_area_width = label:getTextWidth() + if label.frame_inset then + -- account for scroll icons + text_area_width = text_area_width + (label.frame_inset.l or 0) + text_area_width = text_area_width + (label.frame_inset.r or 0) + end + return math.max(width, text_area_width), label:getTextHeight() end function MessageBox:onRenderFrame(dc,rect) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 21e03370b..c6e84d7a1 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -478,8 +478,16 @@ function Label:render_scroll_icons(dc, x, y1, y2) end end -function Label:postComputeFrame() - self:update_scroll_inset() +function Label:computeFrame(parent_rect) + local frame_rect,body_rect = Label.super.computeFrame(self, parent_rect) + + self.frame_rect = frame_rect + self.frame_body = parent_rect:viewport(body_rect or frame_rect) + + self:update_scroll_inset() -- frame_body is now set + + -- recalc with updated frame_inset + return Label.super.computeFrame(self, parent_rect) end function Label:preUpdateLayout() diff --git a/library/lua/test_util/mock.lua b/library/lua/test_util/mock.lua index c60646b77..8d253cc10 100644 --- a/library/lua/test_util/mock.lua +++ b/library/lua/test_util/mock.lua @@ -32,12 +32,17 @@ function _patch_impl(patches_raw, callback, restore_only) end --[[ + +Replaces `table[key]` with `value`, calls `callback()`, then restores the +original value of `table[key]`. + Usage: patch(table, key, value, callback) patch({ {table, key, value}, {table2, key2, value2}, }, callback) + ]] function mock.patch(...) local args = {...} @@ -57,12 +62,18 @@ function mock.patch(...) end --[[ + +Restores the original value of `table[key]` after calling `callback()`. + +Equivalent to: patch(table, key, table[key], callback) + Usage: restore(table, key, callback) restore({ {table, key}, {table2, key2}, }, callback) + ]] function mock.restore(...) local args = {...} @@ -81,9 +92,19 @@ function mock.restore(...) return _patch_impl(patches, callback, true) end -function mock.func(...) +--[[ + +Returns a callable object that tracks the arguments it is called with, then +passes those arguments to `callback()`. + +The returned object has the following properties: +- `call_count`: the number of times the object has been called +- `call_args`: a table of function arguments (shallow-copied) corresponding + to each time the object was called + +]] +function mock.observe_func(callback) local f = { - return_values = {...}, call_count = 0, call_args = {}, } @@ -101,11 +122,36 @@ function mock.func(...) end end table.insert(self.call_args, args) - return table.unpack(self.return_values) + return callback(...) end, }) return f end +--[[ + +Returns a callable object similar to `mock.observe_func()`, but which when +called, only returns the given `return_value`(s) with no additional side effects. + +Intended for use by `patch()`. + +Usage: + func(return_value [, return_value2 ...]) + +See `observe_func()` for a description of the return value. + +The return value also has an additional `return_values` field, which is a table +of values returned when the object is called. This can be modified. + +]] +function mock.func(...) + local f + f = mock.observe_func(function() + return table.unpack(f.return_values) + end) + f.return_values = {...} + return f +end + return mock diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 828c2632f..0dd50414e 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1370,46 +1370,15 @@ DFHACK_EXPORT void Gui::writeToGamelog(std::string message) namespace { // Utility functions for reports - /*bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) - { + bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) + { // parse a string into output strings like DF does for reports if (str.empty() || line_length == 0) return false; - string parsed = ""; + string parsed; size_t i = 0; - while (i < str.length()) - { - if (str[i] == '&') // escape character - { - i++; // ignore the '&' itself - if (i >= str.length()) - break; - - if (str[i] == 'r') // "&r" adds a blank line - parsed += "\n \n"; // DF adds a line with a space for some reason - else if (str[i] == '&') // "&&" is '&' - parsed += "&"; - // else next char is ignored - } - else - { - parsed += str[i]; - } - i++; - } - - return word_wrap(out, parsed, line_length, true); - }*/ - /*bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) - { - if (str.empty() || line_length == 0) - return false; - - string parsed = ""; - size_t i = 0; - - while (i < str.length()) + do { if (str[i] == '&') // escape character { @@ -1419,95 +1388,21 @@ namespace if (str[i] == 'r') // "&r" adds a blank line { - word_wrap(out, parsed, line_length, true); + word_wrap(out, parsed, line_length, false, true); out->push_back(" "); // DF adds a line with a space for some reason - parsed = ""; + parsed.clear(); } else if (str[i] == '&') // "&&" is '&' - parsed += "&"; + parsed.push_back('&'); // else next char is ignored } else - { - parsed += str[i]; - } - i++; + parsed.push_back(str[i]); } + while (++i < str.length()); - if (parsed != "") - word_wrap(out, parsed, line_length, true); - - return true; - }*/ - bool parseReportString(std::vector *out, const std::string &str, size_t line_length) - { // out vector will contain strings cut to line_length, avoiding cutting up words - // Reverse-engineered from DF announcement code, fixes applied - - if (str.empty() || line_length == 0) - return false; - - bool ignore_space = false; - string current_line = ""; - size_t iter = 0; - do - { - if (ignore_space) - { - if (str[iter] == ' ') - continue; - ignore_space = false; - } - - if (str[iter] == '&') // escape character - { - iter++; // ignore the '&' itself - if (iter >= str.length()) - break; - - if (str[iter] == 'r') // "&r" adds a blank line - { - if (!current_line.empty()) - { - out->push_back(string(current_line)); - current_line = ""; - } - out->push_back(" "); // DF adds a line with a space for some reason - continue; // don't add 'r' to current_line - } - else if (str[iter] != '&') - { // not "&&", don't add character to current_line - continue; - } - } - - current_line += str[iter]; - if (current_line.length() > line_length) - { - size_t i = current_line.length(); // start of current word - size_t j; // end of previous word - while (--i > 0 && current_line[i] != ' '); // find start of current word - - if (i == 0) - { // need to push at least one char - j = i = line_length; // last char ends up on next line - } - else - { - j = i; - while (j > 1 && current_line[j - 1] == ' ') - j--; // consume excess spaces at the split point - } - out->push_back(current_line.substr(0, j)); // push string before j - - if (current_line[i] == ' ') - i++; // don't keep this space - current_line.erase(0, i); // current_line now starts at last word or is empty - ignore_space = current_line.empty(); // ignore leading spaces on new line - } - } while (++iter < str.length()); - - if (!current_line.empty()) - out->push_back(current_line); + if (parsed.length()) + word_wrap(out, parsed, line_length, false, true); return true; } @@ -1549,7 +1444,7 @@ namespace } int32_t check_repeat_report(vector &results) - { + { // returns the new repeat count, else 0 if (*gamemode == game_mode::DWARF && !results.empty() && world->status.reports.size() >= results.size()) { auto &reports = world->status.reports; @@ -1697,6 +1592,19 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, int report_ return true; } +namespace +{ // An additional utility function for reports + bool add_proper_report(df::unit *unit, bool is_sparring, int report_index) + { + if (is_sparring) + return addCombatReport(unit, unit_report_type::Sparring, report_index); + else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt) + return addCombatReport(unit, unit_report_type::Hunting, report_index); + else + return addCombatReport(unit, unit_report_type::Combat, report_index); + } +} + bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index) { using df::global::world; @@ -1710,14 +1618,7 @@ bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int r bool ok = false; if (mode.bits.UNIT_COMBAT_REPORT) - { - if (unit->flags2.bits.sparring) - ok |= addCombatReport(unit, unit_report_type::Sparring, report_index); - else if (unit->job.current_job && unit->job.current_job->job_type == job_type::Hunt) - ok |= addCombatReport(unit, unit_report_type::Hunting, report_index); - else - ok |= addCombatReport(unit, unit_report_type::Combat, report_index); - } + ok |= add_proper_report(unit, unit->flags2.bits.sparring, report_index); if (mode.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) { @@ -1783,28 +1684,24 @@ void Gui::showAutoAnnouncement( bool Gui::autoDFAnnouncement(df::report_init r, string message) { // Reverse-engineered from DF announcement code - if (!world->allow_announcements) { DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n%s\n", message.c_str()); return false; - } - - df::announcement_flags a_flags; - if (is_valid_enum_item(r.type)) - a_flags = df::global::d_init->announcements.flags[r.type]; - else + } + else if (!is_valid_enum_item(r.type)) { WARN(gui).print("Invalid announcement type:\n%s\n", message.c_str()); return false; } - - if (message.empty()) + else if (message.empty()) { Core::printerr("Empty announcement %u\n", r.type); // DF would print this to errorlog.txt return false; } + df::announcement_flags a_flags = df::global::d_init->announcements.flags[r.type]; + // Check if the announcement will actually be announced if (*gamemode == game_mode::ADVENTURE) { @@ -1814,11 +1711,13 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) r.type != announcement_type::CONFLICT_CONVERSATION && r.type != announcement_type::MECHANISM_SOUND) { // If not sound, make sure we can see pos - if ((world->units.active.empty() || (r.unit1 != world->units.active[0] && r.unit2 != world->units.active[0])) && - ((Maps::getTileDesignation(r.pos)->whole & 0x10) == 0x0)) // Adventure mode uses this bit to determine current visibility - { - DEBUG(gui).print("Adventure mode announcement not heard:\n%s\n", message.c_str()); - return false; + if (world->units.active.empty() || (r.unit1 != world->units.active[0] && r.unit2 != world->units.active[0])) + { // Adventure mode reuses a dwarf mode digging designation bit to determine current visibility + if (!Maps::isValidTilePos(r.pos) || (Maps::getTileDesignation(r.pos)->whole & 0x10) == 0x0) + { + DEBUG(gui).print("Adventure mode announcement not detected:\n%s\n", message.c_str()); + return false; + } } } } @@ -1826,7 +1725,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) { // Dwarf mode if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2))) { - DEBUG(gui).print("Dwarf mode announcement not heard:\n%s\n", message.c_str()); + DEBUG(gui).print("Dwarf mode announcement not detected:\n%s\n", message.c_str()); return false; } @@ -1927,24 +1826,10 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) if (a_flags.bits.UNIT_COMBAT_REPORT) { if (r.unit1 != NULL) - { - if (r.flags.bits.hostile_combat) - success |= addCombatReport(r.unit1, unit_report_type::Combat, new_report_index); - else if (r.unit1->job.current_job != NULL && r.unit1->job.current_job->job_type == job_type::Hunt) - success |= addCombatReport(r.unit1, unit_report_type::Hunting, new_report_index); - else - success |= addCombatReport(r.unit1, unit_report_type::Sparring, new_report_index); - } + success |= add_proper_report(r.unit1, !r.flags.bits.hostile_combat, new_report_index); if (r.unit2 != NULL) - { - if (r.flags.bits.hostile_combat) - success |= addCombatReport(r.unit2, unit_report_type::Combat, new_report_index); - else if (r.unit2->job.current_job != NULL && r.unit2->job.current_job->job_type == job_type::Hunt) - success |= addCombatReport(r.unit2, unit_report_type::Hunting, new_report_index); - else - success |= addCombatReport(r.unit2, unit_report_type::Sparring, new_report_index); - } + success |= add_proper_report(r.unit2, !r.flags.bits.hostile_combat, new_report_index); } if (a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE) @@ -1953,6 +1838,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) { if (recent_report(r.unit1, slot)) success |= addCombatReport(r.unit1, slot, new_report_index); + if (recent_report(r.unit2, slot)) success |= addCombatReport(r.unit2, slot, new_report_index); } @@ -2044,9 +1930,7 @@ df::coord Gui::getCursorPos() } void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom) -{ - // Reverse-engineered from DF announcement code, also used when scrolling - +{ // Reverse-engineered from DF announcement code, also used when scrolling auto dims = getDwarfmodeViewDims(); int32_t w = dims.map_x2 - dims.map_x1 + 1; int32_t h = dims.map_y2 - dims.map_y1 + 1; @@ -2062,7 +1946,7 @@ void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_ty } else // report_zoom_type::Item { - if (new_win_x > (x - 5)) + if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; if (new_win_y > (y - 5)) new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; @@ -2072,32 +1956,28 @@ void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_ty new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; } - if (new_win_z != z) - ui_sidebar_menus->minimap.need_scan = true; new_win_z = z; } *df::global::window_x = clip_range(new_win_x, 0, (world->map.x_count - w)); *df::global::window_y = clip_range(new_win_y, 0, (world->map.y_count - h)); *df::global::window_z = clip_range(new_win_z, 0, (world->map.z_count - 1)); + + ui_sidebar_menus->minimap.need_render = true; + ui_sidebar_menus->minimap.need_scan = true; return; } void Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause) -{ - // Reverse-engineered from DF announcement code - +{ // Reverse-engineered from DF announcement code if (*gamemode != game_mode::DWARF) return; resetDwarfmodeView(pause); + if (x != -30000) - { recenterViewscreen(x, y, z, report_zoom_type::Item); - ui_sidebar_menus->minimap.need_render = true; - ui_sidebar_menus->minimap.need_scan = true; - } if (init->input.pause_zoom_no_interface_ms > 0) { diff --git a/library/xml b/library/xml index a59495e8f..1dfe6c5ab 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit a59495e8f72115909772e6df20a7b9dec272f14c +Subproject commit 1dfe6c5ab9887507cdcdebdd9390352fe0bba2dd diff --git a/scripts b/scripts index 05d46b32a..741c84ada 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 05d46b32a3aff4f5f98534fdccfbf9ae88dd31a3 +Subproject commit 741c84ada2ec7fdd0083744afab294d9a1b6e370 diff --git a/test/library/gui/widgets.Label.lua b/test/library/gui/widgets.Label.lua new file mode 100644 index 000000000..49a75a235 --- /dev/null +++ b/test/library/gui/widgets.Label.lua @@ -0,0 +1,94 @@ +-- test -dhack/scripts/devel/tests -twidgets.Label + +local gui = require('gui') +local widgets = require('gui.widgets') + +local xtest = {} -- use to temporarily disable tests (change `function xtest.somename` to `function xxtest.somename`) +local wait = function(n) + delay(n or 30) -- enable for debugging the tests +end + +local fs = defclass(fs, gui.FramedScreen) +fs.ATTRS = { + frame_style = gui.GREY_LINE_FRAME, + frame_title = 'TestFramedScreen', + frame_width = 10, + frame_height = 10, + frame_inset = 0, + focus_path = 'test-framed-screen', +} + +function test.Label_correct_frame_body_with_scroll_icons() + local t = {} + for i = 1, 12 do + t[#t+1] = tostring(i) + t[#t+1] = NEWLINE + end + + function fs:init(args) + self:addviews{ + widgets.Label{ + view_id = 'text', + frame_inset = 0, + text = t, + --show_scroll_icons = 'right', + }, + } + end + + local o = fs{} + --o:show() + --wait() + expect.eq(o.subviews.text.frame_body.width, 9, "Label's frame_body.x2 and .width should be one smaller because of show_scroll_icons.") + --o:dismiss() +end + +function test.Label_correct_frame_body_with_few_text_lines() + local t = {} + for i = 1, 10 do + t[#t+1] = tostring(i) + t[#t+1] = NEWLINE + end + + function fs:init(args) + self:addviews{ + widgets.Label{ + view_id = 'text', + frame_inset = 0, + text = t, + --show_scroll_icons = 'right', + }, + } + end + + local o = fs{} + --o:show() + --wait() + expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scroll_icons = false.") + --o:dismiss() +end + +function test.Label_correct_frame_body_without_show_scroll_icons() + local t = {} + for i = 1, 12 do + t[#t+1] = tostring(i) + t[#t+1] = NEWLINE + end + + function fs:init(args) + self:addviews{ + widgets.Label{ + view_id = 'text', + frame_inset = 0, + text = t, + show_scroll_icons = false, + }, + } + end + + local o = fs{} + --o:show() + --wait() + expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scroll_icons = false.") + --o:dismiss() +end diff --git a/test/library/test_util/mock.lua b/test/library/test_util/mock.lua index 32aed28e1..1031a496a 100644 --- a/test/library/test_util/mock.lua +++ b/test/library/test_util/mock.lua @@ -208,9 +208,44 @@ function test.func_call_return_value() end function test.func_call_return_multiple_values() - local f = mock.func(7,5,{imatable='snarfsnarf'}) + local f = mock.func(7, 5, {imatable='snarfsnarf'}) local a, b, c = f() expect.eq(7, a) expect.eq(5, b) expect.table_eq({imatable='snarfsnarf'}, c) end + +function test.observe_func() + -- basic end-to-end test for common cases; + -- most edge cases are covered by mock.func() tests + local counter = 0 + local function target() + counter = counter + 1 + return counter + end + local observer = mock.observe_func(target) + + expect.eq(observer(), 1) + expect.eq(counter, 1) + expect.eq(observer.call_count, 1) + expect.table_eq(observer.call_args, {{}}) + + expect.eq(observer('x', 'y'), 2) + expect.eq(counter, 2) + expect.eq(observer.call_count, 2) + expect.table_eq(observer.call_args, {{}, {'x', 'y'}}) +end + +function test.observe_func_error() + local function target() + error('asdf') + end + local observer = mock.observe_func(target) + + expect.error_match('asdf', function() + observer('x') + end) + -- make sure the call was still tracked + expect.eq(observer.call_count, 1) + expect.table_eq(observer.call_args, {{'x'}}) +end From e613085b0eec7ccc8b8a511b07e1dbf01ed81b25 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 28 May 2022 12:39:49 -0700 Subject: [PATCH 018/161] remove whitespace --- library/modules/Gui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 0dd50414e..b931c22af 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1688,7 +1688,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) { DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n%s\n", message.c_str()); return false; - } + } else if (!is_valid_enum_item(r.type)) { WARN(gui).print("Invalid announcement type:\n%s\n", message.c_str()); From 249ed2888fcb582c91e4e21e57409a9b448de1b6 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 28 May 2022 13:28:10 -0700 Subject: [PATCH 019/161] Fix scope issue --- library/modules/Gui.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index b931c22af..35c6ca8a5 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1388,7 +1388,7 @@ namespace if (str[i] == 'r') // "&r" adds a blank line { - word_wrap(out, parsed, line_length, false, true); + word_wrap(out, parsed, line_length, false/*, true*/); out->push_back(" "); // DF adds a line with a space for some reason parsed.clear(); } @@ -1402,7 +1402,7 @@ namespace while (++i < str.length()); if (parsed.length()) - word_wrap(out, parsed, line_length, false, true); + word_wrap(out, parsed, line_length, false/*, true*/); return true; } @@ -1597,11 +1597,11 @@ namespace bool add_proper_report(df::unit *unit, bool is_sparring, int report_index) { if (is_sparring) - return addCombatReport(unit, unit_report_type::Sparring, report_index); + return Gui::addCombatReport(unit, unit_report_type::Sparring, report_index); else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt) - return addCombatReport(unit, unit_report_type::Hunting, report_index); + return Gui::addCombatReport(unit, unit_report_type::Hunting, report_index); else - return addCombatReport(unit, unit_report_type::Combat, report_index); + return Gui::addCombatReport(unit, unit_report_type::Combat, report_index); } } From 1c3ea000e1c26a88a3e27a5f5f951dc70bcb4295 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 28 May 2022 15:56:49 -0700 Subject: [PATCH 020/161] Trim trailing whitespace --- docs/Lua API.rst | 6 +++--- library/include/modules/Gui.h | 4 ++-- library/modules/Gui.cpp | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 8ec347bef..efd963c03 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1020,7 +1020,7 @@ Fortress mode where ``df.report_zoom_type.Generic`` skips recentering and enforces valid view bounds (the same as x = -30000,) ``df.report_zoom_type.Item`` brings the position onscreen without centering, and ``df.report_zoom_type.Unit`` centers the screen on the position. Default zoom type is ``df.report_zoom_type.Item``. - + * ``dfhack.gui.revealInDwarfmodeMap(pos)`` Centers the view on the given position, which can be a ``df.coord`` instance @@ -1096,11 +1096,11 @@ Announcements Takes a ``df.report_init`` (see: `structure definition `_) and a string and processes them just like DF does. Can also be built from parameters instead of a ``report_init``. Setting ``is_sparring`` to *true* means the report will be added to sparring logs (if applicable) rather than hunting or combat. - + The announcement will not display if units are involved and the player can't see them (or hear, for adventure mode sound announcement types.) Text is parsed using ``&`` as an escape character, with ``&r`` adding a blank line (equivalent to ``\n \n``,) ``&&`` being just ``&``, and any other combination causing neither character to display. - + If you want a guaranteed announcement without parsing, use ``dfhack.gui.showAutoAnnouncement`` instead. Other diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index c7f857a1d..0d2f1c210 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -126,12 +126,12 @@ namespace DFHack // Show an announcement with effects determined by announcements.txt DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL); - + // Process an announcement exactly like DF would, which might result in no announcement DFHACK_EXPORT bool autoDFAnnouncement(df::report_init r, std::string message); DFHACK_EXPORT bool autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false); - + /* * Cursor and window coords */ diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 35c6ca8a5..90e6b41b1 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1962,7 +1962,7 @@ void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_ty *df::global::window_x = clip_range(new_win_x, 0, (world->map.x_count - w)); *df::global::window_y = clip_range(new_win_y, 0, (world->map.y_count - h)); *df::global::window_z = clip_range(new_win_z, 0, (world->map.z_count - 1)); - + ui_sidebar_menus->minimap.need_render = true; ui_sidebar_menus->minimap.need_scan = true; @@ -1975,7 +1975,7 @@ void Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause) return; resetDwarfmodeView(pause); - + if (x != -30000) recenterViewscreen(x, y, z, report_zoom_type::Item); From 0ff0d272b5936584b980642c44d44a3f1273d59e Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 30 May 2022 13:51:24 -0700 Subject: [PATCH 021/161] use static instead of anon namespace; suggested changes --- library/include/modules/Gui.h | 2 +- library/modules/Gui.cpp | 151 ++++++++++++++++------------------ 2 files changed, 73 insertions(+), 80 deletions(-) diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index 0d2f1c210..bfe165179 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -140,7 +140,7 @@ namespace DFHack // Recenter the viewscreen, based on DF code for announcements and scrolling DFHACK_EXPORT void pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause); - DFHACK_EXPORT inline void pauseRecenter(df::coord pos, bool pause) { return pauseRecenter(pos.x, pos.y, pos.z, pause); } + DFHACK_EXPORT inline void pauseRecenter(df::coord pos, bool pause) { pauseRecenter(pos.x, pos.y, pos.z, pause); } DFHACK_EXPORT void recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom = df::enums::report_zoom_type::Item); DFHACK_EXPORT inline void recenterViewscreen(df::coord pos, df::report_zoom_type zoom = df::enums::report_zoom_type::Item) { recenterViewscreen(pos.x, pos.y, pos.z, zoom); }; DFHACK_EXPORT inline void recenterViewscreen(df::report_zoom_type zoom = df::enums::report_zoom_type::Item) { recenterViewscreen(getCursorPos(), zoom); }; diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 90e6b41b1..65a9fbcb5 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1368,99 +1368,94 @@ DFHACK_EXPORT void Gui::writeToGamelog(std::string message) fseed.close(); } -namespace -{ // Utility functions for reports - bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) - { // parse a string into output strings like DF does for reports - if (str.empty() || line_length == 0) - return false; +// Utility functions for reports +static bool parseReportString(std::vector *out, const std::string &str, size_t line_length = 73) +{ // parse a string into output strings like DF does for reports + if (str.empty() || line_length == 0) + return false; - string parsed; - size_t i = 0; + string parsed; + size_t i = 0; - do + do + { + if (str[i] == '&') // escape character { - if (str[i] == '&') // escape character - { - i++; // ignore the '&' itself - if (i >= str.length()) - break; + i++; // ignore the '&' itself + if (i >= str.length()) + break; - if (str[i] == 'r') // "&r" adds a blank line - { - word_wrap(out, parsed, line_length, false/*, true*/); - out->push_back(" "); // DF adds a line with a space for some reason - parsed.clear(); - } - else if (str[i] == '&') // "&&" is '&' - parsed.push_back('&'); - // else next char is ignored + if (str[i] == 'r') // "&r" adds a blank line + { + word_wrap(out, parsed, line_length/*, WSMODE_TRIM_LEADING*/); + out->push_back(" "); // DF adds a line with a space for some reason + parsed.clear(); } - else - parsed.push_back(str[i]); + else if (str[i] == '&') // "&&" is '&' + parsed.push_back('&'); + // else next char is ignored } - while (++i < str.length()); + else + parsed.push_back(str[i]); + } + while (++i < str.length()); - if (parsed.length()) - word_wrap(out, parsed, line_length, false/*, true*/); + if (parsed.length()) + word_wrap(out, parsed, line_length/*, WSMODE_TRIM_LEADING*/); - return true; - } + return true; +} - bool recent_report(df::unit *unit, df::unit_report_type slot) +static bool recent_report(df::unit *unit, df::unit_report_type slot) +{ + return unit && !unit->reports.log[slot].empty() && + *df::global::cur_year == unit->reports.last_year[slot] && + (*df::global::cur_year_tick - unit->reports.last_year_tick[slot]) <= 500; +} + +static bool recent_report_any(df::unit *unit) +{ + FOR_ENUM_ITEMS(unit_report_type, slot) { - if (unit && !unit->reports.log[slot].empty() && - *df::global::cur_year == unit->reports.last_year[slot] && - (*df::global::cur_year_tick - unit->reports.last_year_tick[slot]) <= 500) - { + if (recent_report(unit, slot)) return true; - } - return false; } + return false; +} - bool recent_report_any(df::unit *unit) +static void delete_old_reports() +{ + auto &reports = world->status.reports; + while (reports.size() > 3000) { - FOR_ENUM_ITEMS(unit_report_type, slot) + if (reports[0] != NULL) { - if (recent_report(unit, slot)) - return true; + if (reports[0]->flags.bits.announcement) + erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); + delete reports[0]; } - return false; + reports.erase(reports.begin()); } +} - void delete_old_reports() +static int32_t check_repeat_report(vector &results) +{ // returns the new repeat count, else 0 + if (*gamemode == game_mode::DWARF && !results.empty() && world->status.reports.size() >= results.size()) { auto &reports = world->status.reports; - while (reports.size() > 3000) - { - if (reports[0] != NULL) - { - if (reports[0]->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); - delete reports[0]; - } - reports.erase(reports.begin()); - } - } + size_t base = reports.size() - results.size(); // index where a repeat would start + size_t offset = 0; + while (reports[base + offset]->text == results[offset] && ++offset < results.size()); // match each report - int32_t check_repeat_report(vector &results) - { // returns the new repeat count, else 0 - if (*gamemode == game_mode::DWARF && !results.empty() && world->status.reports.size() >= results.size()) + if (offset == results.size()) // all lines matched { - auto &reports = world->status.reports; - size_t base = reports.size() - results.size(); // index where a repeat would start - size_t offset = 0; - while (reports[base + offset]->text == results[offset] && ++offset < results.size()); // match each report - - if (offset == results.size()) // all lines matched - { - reports[base]->duration = 100; - return ++(reports[base]->repeat_count); - } + reports[base]->duration = 100; + return ++(reports[base]->repeat_count); } - return 0; } + return 0; } +// End of utility functions for reports DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announcement_flags flags, df::coord pos, std::string message, int color, bool bright) { @@ -1592,17 +1587,15 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, int report_ return true; } -namespace -{ // An additional utility function for reports - bool add_proper_report(df::unit *unit, bool is_sparring, int report_index) - { - if (is_sparring) - return Gui::addCombatReport(unit, unit_report_type::Sparring, report_index); - else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt) - return Gui::addCombatReport(unit, unit_report_type::Hunting, report_index); - else - return Gui::addCombatReport(unit, unit_report_type::Combat, report_index); - } +// An additional utility function for reports +static bool add_proper_report(df::unit *unit, bool is_sparring, int report_index) +{ + if (is_sparring) + return Gui::addCombatReport(unit, unit_report_type::Sparring, report_index); + else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt) + return Gui::addCombatReport(unit, unit_report_type::Hunting, report_index); + else + return Gui::addCombatReport(unit, unit_report_type::Combat, report_index); } bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index) From 574728ac5ca1b1edff55f36defe180f77979ceec Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 30 May 2022 15:04:43 -0700 Subject: [PATCH 022/161] Move add_proper_report up with other utility fns --- library/modules/Gui.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 65a9fbcb5..16fd0057a 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1455,6 +1455,16 @@ static int32_t check_repeat_report(vector &results) } return 0; } + +static bool add_proper_report(df::unit *unit, bool is_sparring, int report_index) +{ + if (is_sparring) + return Gui::addCombatReport(unit, unit_report_type::Sparring, report_index); + else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt) + return Gui::addCombatReport(unit, unit_report_type::Hunting, report_index); + else + return Gui::addCombatReport(unit, unit_report_type::Combat, report_index); +} // End of utility functions for reports DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announcement_flags flags, df::coord pos, std::string message, int color, bool bright) @@ -1587,17 +1597,6 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, int report_ return true; } -// An additional utility function for reports -static bool add_proper_report(df::unit *unit, bool is_sparring, int report_index) -{ - if (is_sparring) - return Gui::addCombatReport(unit, unit_report_type::Sparring, report_index); - else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt) - return Gui::addCombatReport(unit, unit_report_type::Hunting, report_index); - else - return Gui::addCombatReport(unit, unit_report_type::Combat, report_index); -} - bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index) { using df::global::world; From b0b601cf0fe55e6a4d93ee2f18db4392194e50dd Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 4 Jun 2022 11:59:04 -0700 Subject: [PATCH 023/161] Remove recenterViewscreen, update revealInDwarfmodeMap --- docs/Lua API.rst | 24 +++---- docs/changelog.txt | 4 +- library/LuaApi.cpp | 92 +++++++++++++----------- library/include/modules/Gui.h | 12 ++-- library/modules/Gui.cpp | 129 +++++++++++++--------------------- 5 files changed, 116 insertions(+), 145 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index efd963c03..803b84768 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1007,31 +1007,23 @@ Fortress mode * ``dfhack.gui.pauseRecenter(pos[,pause])`` ``dfhack.gui.pauseRecenter(x,y,z[,pause])`` - Same as ``resetDwarfmodeView``, but also recenter if ``x`` isn't ``-30000``. Respects + Same as ``resetDwarfmodeView``, but also recenter if position is valid. If ``pause`` is false, skip pausing. Respects ``RECENTER_INTERFACE_SHUTDOWN_MS`` in DF's ``init.txt`` (the delay before input is recognized when a recenter occurs.) -* ``dfhack.gui.recenterViewscreen(pos[,zoom])`` - ``dfhack.gui.recenterViewscreen(x,y,z[,zoom])`` - ``dfhack.gui.recenterViewscreen([zoom])`` +* ``dfhack.gui.revealInDwarfmodeMap(pos[,center])`` + ``dfhack.gui.revealInDwarfmodeMap(x,y,z[,center])`` - Recenter the view on a position using a specific zoom type. If no position is given, - recenter on ``df.global.cursor``. Zoom types are ``df.report_zoom_type`` - (see: `enum definition `_), - where ``df.report_zoom_type.Generic`` skips recentering and enforces valid view bounds (the same as x = -30000,) - ``df.report_zoom_type.Item`` brings the position onscreen without centering, and - ``df.report_zoom_type.Unit`` centers the screen on the position. Default zoom type is ``df.report_zoom_type.Item``. - -* ``dfhack.gui.revealInDwarfmodeMap(pos)`` - - Centers the view on the given position, which can be a ``df.coord`` instance - or a table assignable to a ``df.coord`` (see `lua-api-table-assignment`), + Centers the view on the given coordinates. If ``center`` is true, make sure the + position is in the exact center of the view, else just bring it on screen. + + ``pos`` can be a ``df.coord`` instance or a table assignable to a ``df.coord`` (see `lua-api-table-assignment`), e.g.:: {x = 5, y = 7, z = 11} getSelectedUnit().pos copyall(df.global.cursor) - Returns false if unsuccessful. + If the position is invalid, the function will simply ensure the current window position is clamped between valid values. * ``dfhack.gui.refreshSidebar()`` diff --git a/docs/changelog.txt b/docs/changelog.txt index 34b89d93e..fc184a35e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -45,9 +45,11 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `dfhack-examples-guide`: refine food preparation orders and fix conditions for making jugs and pots in the ``basic`` manager orders ## Documentation +- ``dfhack.gui.revealInDwarfmodeMap``: document ``center`` bool for lua API ## API -- add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``, ``Gui::recenterViewscreen`` +- add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter`` +- ``Gui::revealInDwarfmodeMap``: Now enforce valid view bounds when pos invalid, add variant accepting x, y, z ## Lua - ``widgets.HotkeyLabel``: the ``key_sep`` string is now configurable diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 5881e1b1f..b4e4fe3cd 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1457,26 +1457,6 @@ static const LuaWrapper::FunctionReg dfhack_module[] = { /***** Gui module *****/ -static int gui_getDwarfmodeViewDims(lua_State *state) -{ - auto dims = Gui::getDwarfmodeViewDims(); - lua_newtable(state); - Lua::TableInsert(state, "map_x1", dims.map_x1); - Lua::TableInsert(state, "map_x2", dims.map_x2); - Lua::TableInsert(state, "menu_x1", dims.menu_x1); - Lua::TableInsert(state, "menu_x2", dims.menu_x2); - Lua::TableInsert(state, "area_x1", dims.area_x1); - Lua::TableInsert(state, "area_x2", dims.area_x2); - Lua::TableInsert(state, "y1", dims.y1); - Lua::TableInsert(state, "y2", dims.y2); - Lua::TableInsert(state, "map_y1", dims.map_y1); - Lua::TableInsert(state, "map_y2", dims.map_y2); - Lua::TableInsert(state, "menu_on", dims.menu_on); - Lua::TableInsert(state, "area_on", dims.area_on); - Lua::TableInsert(state, "menu_forced", dims.menu_forced); - return 1; -} - static const LuaWrapper::FunctionReg dfhack_gui_module[] = { WRAPM(Gui, getCurViewscreen), WRAPM(Gui, getFocusString), @@ -1500,7 +1480,6 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = { WRAPM(Gui, showPopupAnnouncement), WRAPM(Gui, showAutoAnnouncement), WRAPM(Gui, resetDwarfmodeView), - WRAPM(Gui, revealInDwarfmodeMap), WRAPM(Gui, refreshSidebar), WRAPM(Gui, inRenameBuilding), WRAPM(Gui, getDepthAt), @@ -1520,7 +1499,7 @@ static int gui_autoDFAnnouncement(lua_State *state) else { df::coord pos; - int color = 0; //initialize these to prevent warning + int color = 0; // initialize these to prevent warning bool bright = false, is_sparring = false; df::unit *unit1 = NULL, *unit2 = NULL; @@ -1572,54 +1551,85 @@ static int gui_autoDFAnnouncement(lua_State *state) return 1; } +static int gui_getDwarfmodeViewDims(lua_State *state) +{ + auto dims = Gui::getDwarfmodeViewDims(); + lua_newtable(state); + Lua::TableInsert(state, "map_x1", dims.map_x1); + Lua::TableInsert(state, "map_x2", dims.map_x2); + Lua::TableInsert(state, "menu_x1", dims.menu_x1); + Lua::TableInsert(state, "menu_x2", dims.menu_x2); + Lua::TableInsert(state, "area_x1", dims.area_x1); + Lua::TableInsert(state, "area_x2", dims.area_x2); + Lua::TableInsert(state, "y1", dims.y1); + Lua::TableInsert(state, "y2", dims.y2); + Lua::TableInsert(state, "map_y1", dims.map_y1); + Lua::TableInsert(state, "map_y2", dims.map_y2); + Lua::TableInsert(state, "menu_on", dims.menu_on); + Lua::TableInsert(state, "area_on", dims.area_on); + Lua::TableInsert(state, "menu_forced", dims.menu_forced); + return 1; +} + static int gui_pauseRecenter(lua_State *state) { - if (lua_gettop(state) == 2) + bool rv; + df::coord p; + + switch (lua_gettop(state)) { - df::coord p; - Lua::CheckDFAssign(state, &p, 1); - Gui::pauseRecenter(p, lua_toboolean(state, 2)); + default: + case 4: + rv = Gui::pauseRecenter(CheckCoordXYZ(state, 1, false), lua_toboolean(state, 4)); + break; + case 3: + rv = Gui::pauseRecenter(CheckCoordXYZ(state, 1, false)); + break; + case 2: + Lua::CheckDFAssign(state, &p, 1); + rv = Gui::pauseRecenter(p, lua_toboolean(state, 2)); + break; + case 1: + Lua::CheckDFAssign(state, &p, 1); + rv = Gui::pauseRecenter(p); } - else - Gui::pauseRecenter(CheckCoordXYZ(state, 1, false), lua_toboolean(state, 4)); + lua_pushboolean(state, rv); return 1; } -static int gui_recenterViewscreen(lua_State *state) +static int gui_revealInDwarfmodeMap(lua_State *state) { + bool rv; df::coord p; + switch (lua_gettop(state)) { default: case 4: - Gui::recenterViewscreen(CheckCoordXYZ(state, 1, false), (df::report_zoom_type)lua_tointeger(state, 4)); + rv = Gui::revealInDwarfmodeMap(CheckCoordXYZ(state, 1, false), lua_toboolean(state, 4)); break; case 3: - Gui::recenterViewscreen(CheckCoordXYZ(state, 1, false)); + rv = Gui::revealInDwarfmodeMap(CheckCoordXYZ(state, 1, false)); break; case 2: Lua::CheckDFAssign(state, &p, 1); - Gui::recenterViewscreen(p, (df::report_zoom_type)lua_tointeger(state, 2)); + rv = Gui::revealInDwarfmodeMap(p, lua_toboolean(state, 2)); break; case 1: - if (lua_type(state, 1) == LUA_TNUMBER) - Gui::recenterViewscreen((df::report_zoom_type)lua_tointeger(state, 1)); - else - Gui::recenterViewscreen(CheckCoordXYZ(state, 1, true)); - break; - case 0: - Gui::recenterViewscreen(); + Lua::CheckDFAssign(state, &p, 1); + rv = Gui::revealInDwarfmodeMap(p); } + lua_pushboolean(state, rv); return 1; } static const luaL_Reg dfhack_gui_funcs[] = { { "autoDFAnnouncement", gui_autoDFAnnouncement }, - { "pauseRecenter", gui_pauseRecenter }, - { "recenterViewscreen", gui_recenterViewscreen }, { "getDwarfmodeViewDims", gui_getDwarfmodeViewDims }, + { "pauseRecenter", gui_pauseRecenter }, + { "revealInDwarfmodeMap", gui_revealInDwarfmodeMap }, { NULL, NULL } }; diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index bfe165179..c44d33f4e 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -138,13 +138,6 @@ namespace DFHack DFHACK_EXPORT df::coord getViewportPos(); DFHACK_EXPORT df::coord getCursorPos(); - // Recenter the viewscreen, based on DF code for announcements and scrolling - DFHACK_EXPORT void pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause); - DFHACK_EXPORT inline void pauseRecenter(df::coord pos, bool pause) { pauseRecenter(pos.x, pos.y, pos.z, pause); } - DFHACK_EXPORT void recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom = df::enums::report_zoom_type::Item); - DFHACK_EXPORT inline void recenterViewscreen(df::coord pos, df::report_zoom_type zoom = df::enums::report_zoom_type::Item) { recenterViewscreen(pos.x, pos.y, pos.z, zoom); }; - DFHACK_EXPORT inline void recenterViewscreen(df::report_zoom_type zoom = df::enums::report_zoom_type::Item) { recenterViewscreen(getCursorPos(), zoom); }; - static const int AREA_MAP_WIDTH = 23; static const int MENU_WIDTH = 30; @@ -161,7 +154,10 @@ namespace DFHack DFHACK_EXPORT DwarfmodeDims getDwarfmodeViewDims(); DFHACK_EXPORT void resetDwarfmodeView(bool pause = false); - DFHACK_EXPORT bool revealInDwarfmodeMap(df::coord pos, bool center = false); + DFHACK_EXPORT bool revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center = false); + DFHACK_EXPORT inline bool revealInDwarfmodeMap(df::coord pos, bool center = false) { return revealInDwarfmodeMap(pos.x, pos.y, pos.z, center); }; + DFHACK_EXPORT bool pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause = true); + DFHACK_EXPORT inline bool pauseRecenter(df::coord pos, bool pause = true) { return pauseRecenter(pos.x, pos.y, pos.z, pause); }; DFHACK_EXPORT bool refreshSidebar(); DFHACK_EXPORT bool inRenameBuilding(); diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 16fd0057a..fa4535238 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1387,7 +1387,7 @@ static bool parseReportString(std::vector *out, const std::string & if (str[i] == 'r') // "&r" adds a blank line { - word_wrap(out, parsed, line_length/*, WSMODE_TRIM_LEADING*/); + word_wrap(out, parsed, line_length, WSMODE_TRIM_LEADING); out->push_back(" "); // DF adds a line with a space for some reason parsed.clear(); } @@ -1401,7 +1401,7 @@ static bool parseReportString(std::vector *out, const std::string & while (++i < str.length()); if (parsed.length()) - word_wrap(out, parsed, line_length/*, WSMODE_TRIM_LEADING*/); + word_wrap(out, parsed, line_length, WSMODE_TRIM_LEADING); return true; } @@ -1921,65 +1921,6 @@ df::coord Gui::getCursorPos() return df::coord(cursor->x, cursor->y, cursor->z); } -void Gui::recenterViewscreen(int32_t x, int32_t y, int32_t z, df::report_zoom_type zoom) -{ // Reverse-engineered from DF announcement code, also used when scrolling - auto dims = getDwarfmodeViewDims(); - int32_t w = dims.map_x2 - dims.map_x1 + 1; - int32_t h = dims.map_y2 - dims.map_y1 + 1; - int32_t new_win_x, new_win_y, new_win_z; - getViewCoords(new_win_x, new_win_y, new_win_z); - - if (zoom != report_zoom_type::Generic && x != -30000) - { - if (zoom == report_zoom_type::Unit) - { - new_win_x = x - w / 2; - new_win_y = y - h / 2; - } - else // report_zoom_type::Item - { - if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" - new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; - if (new_win_y > (y - 5)) - new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; - if (new_win_x < (x + 5 - w)) - new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; - if (new_win_y < (y + 5 - h)) - new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; - } - - new_win_z = z; - } - - *df::global::window_x = clip_range(new_win_x, 0, (world->map.x_count - w)); - *df::global::window_y = clip_range(new_win_y, 0, (world->map.y_count - h)); - *df::global::window_z = clip_range(new_win_z, 0, (world->map.z_count - 1)); - - ui_sidebar_menus->minimap.need_render = true; - ui_sidebar_menus->minimap.need_scan = true; - - return; -} - -void Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause) -{ // Reverse-engineered from DF announcement code - if (*gamemode != game_mode::DWARF) - return; - - resetDwarfmodeView(pause); - - if (x != -30000) - recenterViewscreen(x, y, z, report_zoom_type::Item); - - if (init->input.pause_zoom_no_interface_ms > 0) - { - gview->shutdown_interface_tickcount = Core::getInstance().p->getTickCount(); - gview->shutdown_interface_for_ms = init->input.pause_zoom_no_interface_ms; - } - - return; -} - Gui::DwarfmodeDims getDwarfmodeViewDims_default() { Gui::DwarfmodeDims dims; @@ -2059,38 +2000,68 @@ void Gui::resetDwarfmodeView(bool pause) *df::global::pause_state = true; } -bool Gui::revealInDwarfmodeMap(df::coord pos, bool center) -{ +bool Gui::revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center) +{ // Reverse-engineered from DF announcement and scrolling code using df::global::window_x; using df::global::window_y; using df::global::window_z; if (!window_x || !window_y || !window_z || !world) return false; - if (!Maps::isValidTilePos(pos)) - return false; auto dims = getDwarfmodeViewDims(); - int w = dims.map_x2 - dims.map_x1 + 1; - int h = dims.map_y2 - dims.map_y1 + 1; - - *window_z = pos.z; + int32_t w = dims.map_x2 - dims.map_x1 + 1; + int32_t h = dims.map_y2 - dims.map_y1 + 1; + int32_t new_win_x, new_win_y, new_win_z; + getViewCoords(new_win_x, new_win_y, new_win_z); - if (center) + if (Maps::isValidTilePos(x, y, z)) { - *window_x = pos.x - w/2; - *window_y = pos.y - h/2; + if (center) + { + new_win_x = x - w / 2; + new_win_y = y - h / 2; + } + else // just bring it on screen + { + if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;" + new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10; + if (new_win_y > (y - 5)) + new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10; + if (new_win_x < (x + 5 - w)) + new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10; + if (new_win_y < (y + 5 - h)) + new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10; + } + + new_win_z = z; } - else + + *window_x = clip_range(new_win_x, 0, (world->map.x_count - w)); + *window_y = clip_range(new_win_y, 0, (world->map.y_count - h)); + *window_z = clip_range(new_win_z, 0, (world->map.z_count - 1)); + ui_sidebar_menus->minimap.need_render = true; + ui_sidebar_menus->minimap.need_scan = true; + + return true; +} + +bool Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause) +{ // Reverse-engineered from DF announcement code + if (*gamemode != game_mode::DWARF) + return false; + + resetDwarfmodeView(pause); + + if (Maps::isValidTilePos(x, y, z)) + revealInDwarfmodeMap(x, y, z, false); + + if (init->input.pause_zoom_no_interface_ms > 0) { - while (*window_x + w < pos.x+5) *window_x += 10; - while (*window_y + h < pos.y+5) *window_y += 10; - while (*window_x + 5 > pos.x) *window_x -= 10; - while (*window_y + 5 > pos.y) *window_y -= 10; + gview->shutdown_interface_tickcount = Core::getInstance().p->getTickCount(); + gview->shutdown_interface_for_ms = init->input.pause_zoom_no_interface_ms; } - *window_x = std::max(0, std::min(*window_x, world->map.x_count-w)); - *window_y = std::max(0, std::min(*window_y, world->map.y_count-h)); return true; } From f993c23d755c473535beffaea0634b1d70fb81dd Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 4 Jun 2022 12:22:19 -0700 Subject: [PATCH 024/161] fix whitespace --- docs/Lua API.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index c292eb560..211636abb 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1015,7 +1015,7 @@ Fortress mode Centers the view on the given coordinates. If ``center`` is true, make sure the position is in the exact center of the view, else just bring it on screen. - + ``pos`` can be a ``df.coord`` instance or a table assignable to a ``df.coord`` (see `lua-api-table-assignment`), e.g.:: From 16b5cade001d703e0cb714b64f97622cf0478fad Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 4 Jun 2022 15:23:57 -0700 Subject: [PATCH 025/161] Add constants, update old fns Add MAX_REPORTS_SIZE, RECENT_REPORT_TICKS Remove redundant "using df::global::world" inside fns Update `makeAnnouncement`: Use `word_wrap`, `pauseRecenter`, and utility fn `delete_old_reports` Handle repeat announcements Insert sorted into ``world->status.announcements`` Update `addCombatReportAuto`: Use utility fn `recent_report` Update `showPopupAnnouncement`: Delete old popups at end of fn Update `getDwarfmodeViewDims_default`: Check for ui_sidebar_mode::Default and ArenaWeather --- library/modules/Gui.cpp | 130 +++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 67 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index fa4535238..1959c54a1 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -113,6 +113,9 @@ using namespace DFHack; #include "df/viewscreen_workshop_profilest.h" #include "df/world.h" +const size_t MAX_REPORTS_SIZE = 3000; +const int32_t RECENT_REPORT_TICKS = 500; + namespace DFHack { DBG_DECLARE(core, gui, DebugCategory::LINFO); @@ -1410,7 +1413,7 @@ static bool recent_report(df::unit *unit, df::unit_report_type slot) { return unit && !unit->reports.log[slot].empty() && *df::global::cur_year == unit->reports.last_year[slot] && - (*df::global::cur_year_tick - unit->reports.last_year_tick[slot]) <= 500; + (*df::global::cur_year_tick - unit->reports.last_year_tick[slot]) <= RECENT_REPORT_TICKS; } static bool recent_report_any(df::unit *unit) @@ -1426,7 +1429,7 @@ static bool recent_report_any(df::unit *unit) static void delete_old_reports() { auto &reports = world->status.reports; - while (reports.size() > 3000) + while (reports.size() > MAX_REPORTS_SIZE) { if (reports[0] != NULL) { @@ -1457,7 +1460,7 @@ static int32_t check_repeat_report(vector &results) } static bool add_proper_report(df::unit *unit, bool is_sparring, int report_index) -{ +{ // add report to proper category based on is_sparring and unit current job if (is_sparring) return Gui::addCombatReport(unit, unit_report_type::Sparring, report_index); else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt) @@ -1469,96 +1472,84 @@ static bool add_proper_report(df::unit *unit, bool is_sparring, int report_index DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announcement_flags flags, df::coord pos, std::string message, int color, bool bright) { - using df::global::world; - using df::global::cur_year; - using df::global::cur_year_tick; - - if (message.empty()) - return -1; - - int year = 0, year_time = 0; - - if (cur_year && cur_year_tick) + if (gamemode == NULL || cur_year == NULL || cur_year_tick == NULL) { - year = *cur_year; - year_time = *cur_year_tick; + return -1; } - else if (!world->status.reports.empty()) + else if (message.empty()) { - // Fallback: copy from the last report - df::report *last = world->status.reports.back(); - year = last->year; - year_time = last->time; + Core::printerr("Empty announcement %u\n", r.type); // DF would print this to errorlog.txt + return -1; } // Apply the requested effects - writeToGamelog(message); - if (flags.bits.DO_MEGA || flags.bits.PAUSE || flags.bits.RECENTER) - { - resetDwarfmodeView(flags.bits.DO_MEGA || flags.bits.PAUSE); + if (flags.bits.PAUSE || flags.bits.RECENTER) + pauseRecenter((flags.bits.RECENTER ? pos : df::coord()), flags.bits.PAUSE); - if (flags.bits.RECENTER && pos.isValid()) - revealInDwarfmodeMap(pos, true); + bool adv_unconscious = (*gamemode == game_mode::ADVENTURE && !world->units.active.empty() && world->units.active[0]->counters.unconscious > 0); - if (flags.bits.DO_MEGA) - showPopupAnnouncement(message, color, bright); - } + if (flags.bits.DO_MEGA && !adv_unconscious) + showPopupAnnouncement(message, color, bright); - bool display = false; + vector results; + word_wrap(&results, message, init->display.grid_x - 7); - if (gamemode == NULL) - display = flags.bits.A_DISPLAY || flags.bits.D_DISPLAY; - else if (*gamemode == game_mode::ADVENTURE) - display = flags.bits.A_DISPLAY; - else - display = flags.bits.D_DISPLAY; + // Check for repeat report + int32_t repeat_count = check_repeat_report(results); // Does nothing outside dwarf mode + if (repeat_count > 0) + { + if (flags.bits.D_DISPLAY) + { + world->status.display_timer = 2000; + Gui::writeToGamelog("x" + to_string(repeat_count + 1)); + } + return -1; + } + + // Not a repeat, write the message to gamelog.txt + writeToGamelog(message); // Generate the report objects int report_idx = world->status.reports.size(); bool continued = false; + bool display = ((*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && a_flags.bits.D_DISPLAY)); - while (!message.empty()) + for (size_t i = 0; i < results.size(); i++) { - df::report *new_rep = new df::report(); - + auto new_rep = new df::report(); new_rep->type = type; new_rep->pos = pos; new_rep->color = color; new_rep->bright = bright; - new_rep->year = year; - new_rep->time = year_time; + new_rep->year = *df::global::cur_year; + new_rep->time = *df::global::cur_year_tick; new_rep->flags.bits.continuation = continued; - - int size = std::min(message.size(), (size_t)73); - new_rep->text = message.substr(0, size); - message = message.substr(size); - continued = true; - // Add the object to the lists + new_rep->text = results[i]; new_rep->id = world->status.next_report_id++; - world->status.reports.push_back(new_rep); + if (adv_unconscious) + new_rep->flags.bits.unconscious = true; + if (display) { + insert_into_vector(world->status.announcements, &df::report::id, new_rep); new_rep->flags.bits.announcement = true; - world->status.announcements.push_back(new_rep); world->status.display_timer = 2000; } } + delete_old_reports(); return report_idx; } - bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, int report_index) { - using df::global::world; - CHECK_INVALID_ARGUMENT(is_valid_enum_item(slot)); auto &vec = world->status.reports; @@ -1599,8 +1590,6 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, int report_ bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index) { - using df::global::world; - auto &vec = world->status.reports; auto report = vector_get(vec, report_index); @@ -1616,12 +1605,8 @@ bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int r { FOR_ENUM_ITEMS(unit_report_type, slot) { - if (!unit->reports.log[slot].empty() && - unit->reports.last_year[slot] == report->year && - (report->time - unit->reports.last_year_tick[slot]) <= 500) - { + if (recent_report(unit, slot)) ok |= addCombatReport(unit, slot, report_index); - } } } @@ -1647,13 +1632,20 @@ void Gui::showZoomAnnouncement( void Gui::showPopupAnnouncement(std::string message, int color, bool bright) { - using df::global::world; - df::popup_message *popup = new df::popup_message(); popup->text = message; popup->color = color; popup->bright = bright; - world->status.popups.push_back(popup); + + auto &popups = world->status.popups; + popups.push_back(popup); + + while (popups.size() > MAX_REPORTS_SIZE) + { // Delete old popups + if (popups[0] != NULL) + delete popups[0]; + popups.erase(popups.begin()); + } } void Gui::showAutoAnnouncement( @@ -1750,7 +1742,9 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) if (a_flags.bits.PAUSE || a_flags.bits.RECENTER) pauseRecenter((a_flags.bits.RECENTER ? r.pos : df::coord()), a_flags.bits.PAUSE); // Does nothing outside dwarf mode - if (a_flags.bits.DO_MEGA && (*gamemode != game_mode::ADVENTURE || world->units.active.empty() || world->units.active[0]->counters.unconscious <= 0)) + bool adv_unconscious = (*gamemode == game_mode::ADVENTURE && !world->units.active.empty() && world->units.active[0]->counters.unconscious > 0); + + if (a_flags.bits.DO_MEGA && !adv_unconscious) showPopupAnnouncement(message, r.color, r.bright); vector results; @@ -1776,8 +1770,10 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) return true; } - bool success = false; // only print to gamelog if report was used size_t new_report_index = world->status.reports.size(); // we need this for addCombatReport + bool success = false; // only print to gamelog if report was used + bool display = ((*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && a_flags.bits.D_DISPLAY)); + for (size_t i = 0; i < results.size(); i++) { // Generate report entries for each line auto new_report = new df::report(); @@ -1801,10 +1797,10 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) if (i > 0) new_report->flags.bits.continuation = true; - if (*gamemode == game_mode::ADVENTURE && !world->units.active.empty() && world->units.active[0]->counters.unconscious > 0) + if (adv_unconscious) new_report->flags.bits.unconscious = true; - if ((*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && a_flags.bits.D_DISPLAY)) + if (display) { insert_into_vector(world->status.announcements, &df::report::id, new_report); new_report->flags.bits.announcement = true; @@ -1940,7 +1936,7 @@ Gui::DwarfmodeDims getDwarfmodeViewDims_default() int menu_pos = (ui_menu_width ? (*ui_menu_width)[0] : 2); int area_pos = (ui_menu_width ? (*ui_menu_width)[1] : 3); - if (ui && ui->main.mode && menu_pos >= area_pos) + if (ui && ui->main.mode != ui_sidebar_mode::Default && ui->main.mode != ui_sidebar_mode::ArenaWeather && menu_pos >= area_pos) { dims.menu_forced = true; menu_pos = area_pos-1; From ae4446610b5a00f0c91f18286519f362f5fefc79 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 4 Jun 2022 15:36:50 -0700 Subject: [PATCH 026/161] Update Gui.cpp --- library/modules/Gui.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 1959c54a1..30577c5f8 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1472,6 +1472,9 @@ static bool add_proper_report(df::unit *unit, bool is_sparring, int report_index DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announcement_flags flags, df::coord pos, std::string message, int color, bool bright) { + using df::global::cur_year; + using df::global::cur_year_tick; + if (gamemode == NULL || cur_year == NULL || cur_year_tick == NULL) { return -1; @@ -1523,8 +1526,8 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce new_rep->color = color; new_rep->bright = bright; - new_rep->year = *df::global::cur_year; - new_rep->time = *df::global::cur_year_tick; + new_rep->year = *cur_year; + new_rep->time = *cur_year_tick; new_rep->flags.bits.continuation = continued; continued = true; From ce36abce47d5285f9e03faf159b223e9863cd716 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 4 Jun 2022 15:46:02 -0700 Subject: [PATCH 027/161] Fixes --- library/modules/Gui.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 30577c5f8..84049045e 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1481,7 +1481,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce } else if (message.empty()) { - Core::printerr("Empty announcement %u\n", r.type); // DF would print this to errorlog.txt + Core::printerr("Empty announcement %u\n", type); // DF would print this to errorlog.txt return -1; } @@ -1516,7 +1516,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce // Generate the report objects int report_idx = world->status.reports.size(); bool continued = false; - bool display = ((*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && a_flags.bits.D_DISPLAY)); + bool display = ((*gamemode == game_mode::ADVENTURE && flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && flags.bits.D_DISPLAY)); for (size_t i = 0; i < results.size(); i++) { From ad1a3408e69e01125a6d80160e9c7a179ef56f4a Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 6 Jun 2022 01:41:17 -0700 Subject: [PATCH 028/161] Hopefully fix submodules --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 1dfe6c5ab..a24581cc5 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 1dfe6c5ab9887507cdcdebdd9390352fe0bba2dd +Subproject commit a24581cc5318bdbc6227f368f67bc03a9082c19c diff --git a/scripts b/scripts index 741c84ada..52e21c5e0 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 741c84ada2ec7fdd0083744afab294d9a1b6e370 +Subproject commit 52e21c5e0c78e17acdad23f94efffb043290d5b5 From 5d08e5ae676f4a9273447b3a61d5a614bfddf848 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 6 Jun 2022 01:56:11 -0700 Subject: [PATCH 029/161] More constants; remove extra "using" statements --- library/modules/Gui.cpp | 51 +++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 84049045e..8bc5a7473 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -113,8 +113,10 @@ using namespace DFHack; #include "df/viewscreen_workshop_profilest.h" #include "df/world.h" -const size_t MAX_REPORTS_SIZE = 3000; -const int32_t RECENT_REPORT_TICKS = 500; +const size_t MAX_REPORTS_SIZE = 3000; // DF clears old reports to maintain this vector size +const int32_t RECENT_REPORT_TICKS = 500; // used by UNIT_COMBAT_REPORT_ALL_ACTIVE +const int32_t ANNOUNCE_LINE_DURATION = 100; // time to display each line in announcement bar; 3.3 sec at 30 GFPS +const int16_t ANNOUNCE_DISPLAY_TIME = 2000; // DF uses this value for most announcements; 66.6 sec at 30 GFPS namespace DFHack { @@ -679,8 +681,6 @@ bool Gui::cursor_hotkey(df::viewscreen *top) bool Gui::workshop_job_hotkey(df::viewscreen *top) { using namespace ui_sidebar_mode; - using df::global::ui; - using df::global::world; using df::global::ui_workshop_in_add; using df::global::ui_workshop_job_cursor; @@ -717,7 +717,6 @@ bool Gui::workshop_job_hotkey(df::viewscreen *top) bool Gui::build_selector_hotkey(df::viewscreen *top) { using namespace ui_sidebar_mode; - using df::global::ui; using df::global::ui_build_selector; if (!dwarfmode_hotkey(top)) @@ -744,8 +743,6 @@ bool Gui::build_selector_hotkey(df::viewscreen *top) bool Gui::view_unit_hotkey(df::viewscreen *top) { - using df::global::ui; - using df::global::world; using df::global::ui_selected_unit; if (!dwarfmode_hotkey(top)) @@ -772,7 +769,6 @@ bool Gui::unit_inventory_hotkey(df::viewscreen *top) df::job *Gui::getSelectedWorkshopJob(color_ostream &out, bool quiet) { - using df::global::world; using df::global::ui_workshop_job_cursor; if (!workshop_job_hotkey(Core::getTopViewscreen())) { @@ -840,8 +836,6 @@ df::job *Gui::getSelectedJob(color_ostream &out, bool quiet) df::unit *Gui::getAnyUnit(df::viewscreen *top) { using namespace ui_sidebar_mode; - using df::global::ui; - using df::global::world; using df::global::ui_look_cursor; using df::global::ui_look_list; using df::global::ui_selected_unit; @@ -1122,13 +1116,10 @@ df::unit *Gui::getSelectedUnit(color_ostream &out, bool quiet) df::item *Gui::getAnyItem(df::viewscreen *top) { using namespace ui_sidebar_mode; - using df::global::ui; - using df::global::world; using df::global::ui_look_cursor; using df::global::ui_look_list; using df::global::ui_unit_view_mode; using df::global::ui_building_item_cursor; - using df::global::ui_sidebar_menus; if (VIRTUAL_CAST_VAR(screen, df::viewscreen_textviewerst, top)) { @@ -1256,11 +1247,8 @@ df::item *Gui::getSelectedItem(color_ostream &out, bool quiet) df::building *Gui::getAnyBuilding(df::viewscreen *top) { using namespace ui_sidebar_mode; - using df::global::ui; using df::global::ui_look_list; using df::global::ui_look_cursor; - using df::global::world; - using df::global::ui_sidebar_menus; if (VIRTUAL_CAST_VAR(screen, df::viewscreen_buildinglistst, top)) return vector_get(screen->buildings, screen->cursor); @@ -1323,8 +1311,6 @@ df::building *Gui::getSelectedBuilding(color_ostream &out, bool quiet) df::plant *Gui::getAnyPlant(df::viewscreen *top) { using df::global::cursor; - using df::global::ui; - using df::global::world; if (auto dfscreen = dfhack_viewscreen::try_cast(top)) return dfscreen->getSelectedPlant(); @@ -1452,7 +1438,7 @@ static int32_t check_repeat_report(vector &results) if (offset == results.size()) // all lines matched { - reports[base]->duration = 100; + reports[base]->duration = ANNOUNCE_LINE_DURATION; // display the last line again return ++(reports[base]->repeat_count); } } @@ -1504,7 +1490,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce { if (flags.bits.D_DISPLAY) { - world->status.display_timer = 2000; + world->status.display_timer = ANNOUNCE_DISPLAY_TIME; Gui::writeToGamelog("x" + to_string(repeat_count + 1)); } return -1; @@ -1543,7 +1529,7 @@ DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announce { insert_into_vector(world->status.announcements, &df::report::id, new_rep); new_rep->flags.bits.announcement = true; - world->status.display_timer = 2000; + world->status.display_timer = ANNOUNCE_DISPLAY_TIME; } } @@ -1761,7 +1747,7 @@ bool Gui::autoDFAnnouncement(df::report_init r, string message) } // Check for repeat report - int32_t repeat_count = check_repeat_report(results); // Does nothing outside dwarf mode + int32_t repeat_count = check_repeat_report(results); // always returns 0 outside dwarf mode if (repeat_count > 0) { if (a_flags.bits.D_DISPLAY) @@ -1863,6 +1849,7 @@ bool Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::str r.color = color; r.bright = bright; r.pos = pos; + r.display_timer = ANNOUNCE_DISPLAY_TIME; r.unit1 = unit1; r.unit2 = unit2; r.flags.bits.hostile_combat = !is_sparring; @@ -2135,17 +2122,17 @@ bool Gui::setCursorCoords (const int32_t x, const int32_t y, const int32_t z) bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z) { - x = df::global::selection_rect->start_x; - y = df::global::selection_rect->start_y; - z = df::global::selection_rect->start_z; + x = selection_rect->start_x; + y = selection_rect->start_y; + z = selection_rect->start_z; return (x == -30000) ? false : true; } bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t z) { - df::global::selection_rect->start_x = x; - df::global::selection_rect->start_y = y; - df::global::selection_rect->start_z = z; + selection_rect->start_x = x; + selection_rect->start_y = y; + selection_rect->start_z = z; return true; } @@ -2189,14 +2176,14 @@ bool Gui::getWindowSize (int32_t &width, int32_t &height) bool Gui::getMenuWidth(uint8_t &menu_width, uint8_t &area_map_width) { - menu_width = (*df::global::ui_menu_width)[0]; - area_map_width = (*df::global::ui_menu_width)[1]; + menu_width = (*ui_menu_width)[0]; + area_map_width = (*ui_menu_width)[1]; return true; } bool Gui::setMenuWidth(const uint8_t menu_width, const uint8_t area_map_width) { - (*df::global::ui_menu_width)[0] = menu_width; - (*df::global::ui_menu_width)[1] = area_map_width; + (*ui_menu_width)[0] = menu_width; + (*ui_menu_width)[1] = area_map_width; return true; } From a7267e3c4ecfddae81447e5748d3ba4ed88b4b70 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 7 Jun 2022 04:21:48 -0700 Subject: [PATCH 030/161] Optimize report deletion --- library/modules/Gui.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 8bc5a7473..82da223ea 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -1415,15 +1415,19 @@ static bool recent_report_any(df::unit *unit) static void delete_old_reports() { auto &reports = world->status.reports; - while (reports.size() > MAX_REPORTS_SIZE) + if (reports.size() > MAX_REPORTS_SIZE) { - if (reports[0] != NULL) + size_t excess = reports.size() - MAX_REPORTS_SIZE; + for (size_t i = 0; i < excess; i++) { - if (reports[0]->flags.bits.announcement) - erase_from_vector(world->status.announcements, &df::report::id, reports[0]->id); - delete reports[0]; + if (reports[i] != NULL) + { // report destructor + if (reports[i]->flags.bits.announcement) + erase_from_vector(world->status.announcements, &df::report::id, reports[i]->id); + delete reports[i]; + } } - reports.erase(reports.begin()); + reports.erase(reports.begin(), reports.begin() + excess); } } From beee445f6fac666f768a3e9547d1b0a9a9d0485a Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 11:44:28 -0800 Subject: [PATCH 031/161] Extends Units module Adds unit check functions for - animals - demons - titans - megabeasts - semimegabeasts - night creatures --- docs/changelog.txt | 1 + library/include/modules/Units.h | 6 ++++++ library/modules/Units.cpp | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index b90248c91..7836c5e14 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -72,6 +72,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API - ``Lua::PushInterfaceKeys()``: transforms viewscreen ``feed()`` keys into something that can be interpreted by lua-based widgets - ``Lua::Push()``: now handles maps with otherwise supported keys and values +- Units module: added ``isAnimal()``, ``isDemon()``, ``isTitan()``, ``isMegabeast()``, ``isSemiMegabeast()``, and ``isNightCreature()`` - Constructions module: added ``insert()`` to insert constructions into the game's sorted list. - MiscUtils: moved the following string transformation functions from ``uicommon.h``: ``int_to_string``, ``ltrim``, ``rtrim``, and ``trim`` diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 4dfa9f937..c6323f862 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -141,6 +141,7 @@ DFHACK_EXPORT std::string getRaceChildName(df::unit* unit); DFHACK_EXPORT bool isBaby(df::unit* unit); DFHACK_EXPORT bool isChild(df::unit* unit); DFHACK_EXPORT bool isAdult(df::unit* unit); +DFHACK_EXPORT bool isAnimal(df::unit* unit); DFHACK_EXPORT bool isEggLayer(df::unit* unit); DFHACK_EXPORT bool isGrazer(df::unit* unit); DFHACK_EXPORT bool isMilkable(df::unit* unit); @@ -163,6 +164,11 @@ DFHACK_EXPORT bool isActive(df::unit *unit); DFHACK_EXPORT bool isKilled(df::unit *unit); DFHACK_EXPORT bool isGelded(df::unit* unit); DFHACK_EXPORT bool isDomesticated(df::unit* unit); +DFHACK_EXPORT bool isDemon(df::unit* unit); +DFHACK_EXPORT bool isTitan(df::unit* unit); +DFHACK_EXPORT bool isMegabeast(df::unit* unit); +DFHACK_EXPORT bool isSemiMegabeast(df::unit* unit); +DFHACK_EXPORT bool isNightCreature(df::unit* unit); DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); DFHACK_EXPORT int getKillCount(df::unit *unit); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 32b9f28e5..9111b54d4 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -770,6 +770,12 @@ bool Units::isAdult(df::unit* unit) return !isBaby(unit) && !isChild(unit); } +bool Units::isAnimal(df::unit* unit) +{ + CHECK_NULL_POINTER(unit) + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NATURAL_ANIMAL); +} + bool Units::isEggLayer(df::unit* unit) { CHECK_NULL_POINTER(unit); @@ -1824,6 +1830,37 @@ bool Units::isDomesticated(df::unit* unit) return tame; } +bool Units::isDemon(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::DEMON); +} + +bool Units::isTitan(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::TITAN); +} + +bool Units::isMegabeast(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::MEGABEAST); +} + +bool Units::isSemiMegabeast(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::SEMIMEGABEAST); +} + +bool Units::isNightCreature(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NIGHT_CREATURE); +} + + // 50000 and up is level 0, 25000 and up is level 1, etc. const vector Units::stress_cutoffs {50000, 25000, 10000, -10000, -25000, -50000, -100000}; From c0ffcc2f79ee6bff9ddf303c3cfbf20e1aac8a14 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 12:37:41 -0800 Subject: [PATCH 032/161] Updates Units::isDemon() --- library/modules/Units.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 9111b54d4..be8c1ec1e 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1833,7 +1833,9 @@ bool Units::isDomesticated(df::unit* unit) bool Units::isDemon(df::unit* unit) { CHECK_NULL_POINTER(unit); - return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::DEMON); + using namespace df::enums::caste_raw_flags; + const auto &cf = unit->enemy.caste_flags; + return cf.is_set(DEMON) || cf.is_set(UNIQUE_DEMON); } bool Units::isTitan(df::unit* unit) From 80824f5b75f0b626afe7ae3180690059afb6da6c Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 14:41:45 -0800 Subject: [PATCH 033/161] Extends Units module --- docs/changelog.txt | 16 +++++++++- library/include/modules/Units.h | 10 +++++-- library/modules/Units.cpp | 52 ++++++++++++++++++++++++++++----- 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7836c5e14..a5a7f84a2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -72,7 +72,21 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API - ``Lua::PushInterfaceKeys()``: transforms viewscreen ``feed()`` keys into something that can be interpreted by lua-based widgets - ``Lua::Push()``: now handles maps with otherwise supported keys and values -- Units module: added ``isAnimal()``, ``isDemon()``, ``isTitan()``, ``isMegabeast()``, ``isSemiMegabeast()``, and ``isNightCreature()`` +- Units module: added new checks + - ``isAnimal()`` + - ``isVisiting()`` any visiting unit (diplomat, merchant, visitor) + - ``isVisitor()`` ie. not merchants or diplomats + - ``isInvader()`` + - ``isDemon()`` returns true for unique/regular demons + - ``isTitan()`` + - ``isMegabeast()`` + - ``isGreatDanger()`` returns true if unit is a demon, titan, or megabeast + - ``isSemiMegabeast()`` + - ``isNightCreature()`` + - ``isDanger()`` returns true if is a 'GreatDanger', semi-megabeast, night creature, undead, or invader +- Units module: modifies existing checks + - ``isUndead(df::unit* unit)`` => ``isUndead(df::unit* unit, bool ignore_vamps = true)`` isUndead used to always ignore vamps, now it does it by default and includes them when false is passed + - ``isCitizen(df::unit* unit)`` => ``isCitizen(df::unit* unit, bool ignore_sanity = false)`` isCitizen used to always check sanity, now it does it by default and ignores sanity when true is passed - Constructions module: added ``insert()`` to insert constructions into the game's sorted list. - MiscUtils: moved the following string transformation functions from ``uicommon.h``: ``int_to_string``, ``ltrim``, ``rtrim``, and ``trim`` diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index c6323f862..d25ee8d0f 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -115,7 +115,7 @@ DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_t DFHACK_EXPORT bool isDead(df::unit *unit); DFHACK_EXPORT bool isAlive(df::unit *unit); DFHACK_EXPORT bool isSane(df::unit *unit); -DFHACK_EXPORT bool isCitizen(df::unit *unit); +DFHACK_EXPORT bool isCitizen(df::unit *unit, bool ignore_sanity = false); DFHACK_EXPORT bool isFortControlled(df::unit *unit); DFHACK_EXPORT bool isDwarf(df::unit *unit); DFHACK_EXPORT bool isWar(df::unit* unit); @@ -138,6 +138,7 @@ DFHACK_EXPORT std::string getRaceBabyName(df::unit* unit); DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceChildName(df::unit* unit); +DFHACK_EXPORT bool isInvader(df::unit* unit); DFHACK_EXPORT bool isBaby(df::unit* unit); DFHACK_EXPORT bool isChild(df::unit* unit); DFHACK_EXPORT bool isAdult(df::unit* unit); @@ -150,15 +151,17 @@ DFHACK_EXPORT bool isTrainableHunting(df::unit* unit); DFHACK_EXPORT bool isTamable(df::unit* unit); DFHACK_EXPORT bool isMale(df::unit* unit); DFHACK_EXPORT bool isFemale(df::unit* unit); +DFHACK_EXPORT bool isVisiting(df::unit* unit); DFHACK_EXPORT bool isMerchant(df::unit* unit); DFHACK_EXPORT bool isDiplomat(df::unit* unit); +DFHACK_EXPORT bool isVisitor(df::unit* unit); DFHACK_EXPORT bool isForest(df::unit* unit); DFHACK_EXPORT bool isMarkedForSlaughter(df::unit* unit); DFHACK_EXPORT bool isTame(df::unit* unit); DFHACK_EXPORT bool isTrained(df::unit* unit); DFHACK_EXPORT bool isGay(df::unit* unit); DFHACK_EXPORT bool isNaked(df::unit* unit); -DFHACK_EXPORT bool isUndead(df::unit* unit); +DFHACK_EXPORT bool isUndead(df::unit* unit, bool ignore_vamps = true); DFHACK_EXPORT bool isGhost(df::unit *unit); DFHACK_EXPORT bool isActive(df::unit *unit); DFHACK_EXPORT bool isKilled(df::unit *unit); @@ -167,8 +170,11 @@ DFHACK_EXPORT bool isDomesticated(df::unit* unit); DFHACK_EXPORT bool isDemon(df::unit* unit); DFHACK_EXPORT bool isTitan(df::unit* unit); DFHACK_EXPORT bool isMegabeast(df::unit* unit); +DFHACK_EXPORT bool isGreatDanger(df::unit* unit); DFHACK_EXPORT bool isSemiMegabeast(df::unit* unit); DFHACK_EXPORT bool isNightCreature(df::unit* unit); +DFHACK_EXPORT bool isDanger(df::unit* unit); + DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); DFHACK_EXPORT int getKillCount(df::unit *unit); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index be8c1ec1e..0bb6235f9 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -499,7 +499,7 @@ bool Units::isSane(df::unit *unit) return true; } -bool Units::isCitizen(df::unit *unit) +bool Units::isCitizen(df::unit *unit, bool ignore_sanity) { CHECK_NULL_POINTER(unit); @@ -519,7 +519,7 @@ bool Units::isCitizen(df::unit *unit) unit->flags2.bits.resident) return false; - if (!isSane(unit)) + if (!ignore_sanity && !isSane(unit)) return false; return isOwnGroup(unit); @@ -752,6 +752,15 @@ string Units::getRaceChildName(df::unit* unit) return getRaceChildNameById(unit->race); } +bool Units::isInvader(df::unit* unit) { + CHECK_NULL_POINTER(unit); + + return !isOwnGroup(unit) && + (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader); +} + bool Units::isBaby(df::unit* unit) { CHECK_NULL_POINTER(unit); @@ -1666,6 +1675,15 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) return entry->events[entry->events.size() - 1]; } +bool Units::isVisiting(df::unit* unit) { + CHECK_NULL_POINTER(unit); + + return unit->flags1.bits.merchant || + unit->flags1.bits.diplomat || + unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited; +} + bool Units::isMerchant(df::unit* unit) { CHECK_NULL_POINTER(unit); @@ -1680,6 +1698,12 @@ bool Units::isDiplomat(df::unit* unit) return unit->flags1.bits.diplomat == 1; } +bool Units::isVisitor(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->flags2.bits.visitor || unit->flags2.bits.visitor_uninvited; +} + bool Units::isForest(df::unit* unit) { CHECK_NULL_POINTER(unit); @@ -1763,13 +1787,13 @@ bool Units::isNaked(df::unit* unit) return (unit->inventory.empty()); } -bool Units::isUndead(df::unit* unit) +bool Units::isUndead(df::unit* unit, bool ignore_vamps) { CHECK_NULL_POINTER(unit); - // ignore vampires, they should be treated like normal dwarves - return (unit->flags3.bits.ghostly || - ( (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE || unit->curse.add_tags1.bits.NOT_LIVING) - && !unit->curse.add_tags1.bits.BLOODSUCKER )); + + const auto &cb = unit->curse.add_tags1.bits; + return unit->flags3.bits.ghostly || + ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (!ignore_vamps || !cb.BLOODSUCKER)); } bool Units::isGhost(df::unit *unit) @@ -1850,6 +1874,12 @@ bool Units::isMegabeast(df::unit* unit) return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::MEGABEAST); } +bool Units::isGreatDanger(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return isDemon(unit) || isTitan(unit) || isMegabeast(unit); +} + bool Units::isSemiMegabeast(df::unit* unit) { CHECK_NULL_POINTER(unit); @@ -1862,6 +1892,14 @@ bool Units::isNightCreature(df::unit* unit) return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NIGHT_CREATURE); } +bool Units::isDanger(df::unit* unit) { + CHECK_NULL_POINTER(unit); + return isInvader(unit) || + isUndead(unit, false) || + isSemiMegabeast(unit) || + isNightCreature(unit) || + isGreatDanger(unit); +} // 50000 and up is level 0, 25000 and up is level 1, etc. const vector Units::stress_cutoffs {50000, 25000, 10000, -10000, -25000, -50000, -100000}; From 34de030ba9c9c78a10fdeb19e479aff4ba4e6972 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 22:03:39 -0800 Subject: [PATCH 034/161] Units module - Updates `Lua API.rst` - Only adds the most important additions (complicated enough to need explaining) - Adds new functions to LuaApi.cpp - Revises isUndead to accommodate `dfhack.units.isUndead(u)` => `Units::isUndead(u, false)` instead of taking the default value --- docs/Lua API.rst | 29 +++++++++++++++++++++++++++-- library/LuaApi.cpp | 11 +++++++++++ library/include/modules/Units.h | 2 +- library/modules/Units.cpp | 6 +++--- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 5b6a30d93..6b8290d12 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1287,10 +1287,35 @@ Units module The unit is of the correct race of the fortress. -* ``dfhack.units.isCitizen(unit)`` +* ``dfhack.units.isCitizen(unit[,ignore_sanity])`` The unit is an alive sane citizen of the fortress; wraps the - same checks the game uses to decide game-over by extinction. + same checks the game uses to decide game-over by extinction + (except for the sanity check). + +* ``dfhack.units.isInvader(unit)`` + + The unit is an active invader or marauder. + +* ``dfhack.units.isVisiting(unit)`` + + The unit is either a merchant, diplomat, or plain visitor. + +* ``dfhack.units.isVisitor(unit)`` + + The unit is strictly a visitor. Merchants and diplomats do not count here. + +* ``dfhack.units.isUndead(unit[,include_vamps])`` + + The unit is undead, but not a vampire. + +* ``dfhack.units.isGreatDanger(unit)`` + + The unit is of Great Danger. This include demons, titans, and megabeasts. + +* ``dfhack.units.isDanger(unit)`` + + The unit is dangerous, and probably hostile. This includes Great Dangers, semi-megabeasts, night creatures, undead, and invaders. * ``dfhack.units.isFortControlled(unit)`` diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 9f713244d..f99d65c65 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1615,9 +1615,11 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getRaceBabyNameById), WRAPM(Units, getRaceChildName), WRAPM(Units, getRaceChildNameById), + WRAPM(Units, isInvader), WRAPM(Units, isBaby), WRAPM(Units, isChild), WRAPM(Units, isAdult), + WRAPM(Units, isAnimal), WRAPM(Units, isEggLayer), WRAPM(Units, isGrazer), WRAPM(Units, isMilkable), @@ -1626,8 +1628,10 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, isTamable), WRAPM(Units, isMale), WRAPM(Units, isFemale), + WRAPM(Units, isVisiting), WRAPM(Units, isMerchant), WRAPM(Units, isDiplomat), + WRAPM(Units, isVisitor), WRAPM(Units, isForest), WRAPM(Units, isMarkedForSlaughter), WRAPM(Units, isTame), @@ -1640,6 +1644,13 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, isKilled), WRAPM(Units, isGelded), WRAPM(Units, isDomesticated), + WRAPM(Units, isDemon), + WRAPM(Units, isTitan), + WRAPM(Units, isMegabeast), + WRAPM(Units, isGreatDanger), + WRAPM(Units, isSemiMegabeast), + WRAPM(Units, isNightCreature), + WRAPM(Units, isDanger), WRAPM(Units, getMainSocialActivity), WRAPM(Units, getMainSocialEvent), WRAPM(Units, getStressCategory), diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index d25ee8d0f..84fcf5bfc 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -161,7 +161,7 @@ DFHACK_EXPORT bool isTame(df::unit* unit); DFHACK_EXPORT bool isTrained(df::unit* unit); DFHACK_EXPORT bool isGay(df::unit* unit); DFHACK_EXPORT bool isNaked(df::unit* unit); -DFHACK_EXPORT bool isUndead(df::unit* unit, bool ignore_vamps = true); +DFHACK_EXPORT bool isUndead(df::unit* unit, bool include_vamps = false); DFHACK_EXPORT bool isGhost(df::unit *unit); DFHACK_EXPORT bool isActive(df::unit *unit); DFHACK_EXPORT bool isKilled(df::unit *unit); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 0bb6235f9..b0aad49fb 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -1787,13 +1787,13 @@ bool Units::isNaked(df::unit* unit) return (unit->inventory.empty()); } -bool Units::isUndead(df::unit* unit, bool ignore_vamps) +bool Units::isUndead(df::unit* unit, bool include_vamps) { CHECK_NULL_POINTER(unit); const auto &cb = unit->curse.add_tags1.bits; return unit->flags3.bits.ghostly || - ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (!ignore_vamps || !cb.BLOODSUCKER)); + ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (include_vamps || !cb.BLOODSUCKER)); } bool Units::isGhost(df::unit *unit) @@ -1895,7 +1895,7 @@ bool Units::isNightCreature(df::unit* unit) bool Units::isDanger(df::unit* unit) { CHECK_NULL_POINTER(unit); return isInvader(unit) || - isUndead(unit, false) || + isUndead(unit, true) || isSemiMegabeast(unit) || isNightCreature(unit) || isGreatDanger(unit); From b73cef3d78c6bbb93fec234f13c3c55d38d2e3d5 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sat, 12 Nov 2022 12:28:53 -0800 Subject: [PATCH 035/161] Update docs/Lua API.rst Co-authored-by: Myk --- docs/Lua API.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 6b8290d12..607dd424f 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1307,7 +1307,8 @@ Units module * ``dfhack.units.isUndead(unit[,include_vamps])`` - The unit is undead, but not a vampire. + The unit is undead. Pass ``true`` as the optional second parameter to + count vampires as undead. * ``dfhack.units.isGreatDanger(unit)`` From ebd450af0eaebfc1c4fb66f6a0d1b2d43b6e4c31 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sat, 12 Nov 2022 14:42:41 -0800 Subject: [PATCH 036/161] Adds isUnitInBox to Units module --- docs/Lua API.rst | 4 ++++ docs/changelog.txt | 1 + library/LuaApi.cpp | 3 +++ library/include/modules/Units.h | 3 +++ library/modules/Units.cpp | 31 +++++++++++++++++++------------ 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 6b8290d12..33e133162 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1192,6 +1192,10 @@ Units module Returns true *x,y,z* of the unit, or *nil* if invalid; may be not equal to unit.pos if caged. +* ``dfhack.units.isUnitInBox(unit,x1,y1,z1,x2,y2,z2)`` + + Returns true if the unit is within the specified coordinates. + * ``dfhack.units.getUnitsInBox(x1,y1,z1,x2,y2,z2[,filter])`` Returns a table of all units within the specified coordinates. If the ``filter`` diff --git a/docs/changelog.txt b/docs/changelog.txt index a5a7f84a2..0497f2f64 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -73,6 +73,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``Lua::PushInterfaceKeys()``: transforms viewscreen ``feed()`` keys into something that can be interpreted by lua-based widgets - ``Lua::Push()``: now handles maps with otherwise supported keys and values - Units module: added new checks + - ``isUnitInBox()`` - ``isAnimal()`` - ``isVisiting()`` any visiting unit (diplomat, merchant, visitor) - ``isVisitor()`` ie. not merchants or diplomats diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index f99d65c65..b5a1cd754 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1559,6 +1559,7 @@ static const luaL_Reg dfhack_job_funcs[] = { /***** Units module *****/ static const LuaWrapper::FunctionReg dfhack_units_module[] = { + WRAPM(Units, isUnitInBox), WRAPM(Units, teleport), WRAPM(Units, getGeneralRef), WRAPM(Units, getSpecificRef), @@ -1716,6 +1717,8 @@ static int units_getUnitsInBox(lua_State *state) { luaL_checktype(state, 7, LUA_TFUNCTION); units.erase(std::remove_if(units.begin(), units.end(), [&state](df::unit *unit) -> bool { + // todo: merging this filter into the base function would be welcomed by plugins + // (it would also be faster, and less obfuscated than this [ie. erase(remove_if)]) lua_dup(state); // copy function Lua::PushDFObject(state, unit); lua_call(state, 1, 1); diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 84fcf5bfc..c001b6a5a 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -73,6 +73,9 @@ static const int MAX_COLORS = 15; // found. Call repeatedly do get all units in a specified box (uses tile coords) DFHACK_EXPORT int32_t getNumUnits(); DFHACK_EXPORT df::unit *getUnit(const int32_t index); +DFHACK_EXPORT bool isUnitInBox(df::unit* u, + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2); DFHACK_EXPORT bool getUnitsInBox(std::vector &units, int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index b0aad49fb..5dc41da02 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -98,6 +98,23 @@ df::unit *Units::getUnit (const int32_t index) return vector_get(world->units.all, index); } +bool Units::isUnitInBox(df::unit* u, + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2) { + + if (x1 > x2) swap(x1, x2); + if (y1 > y2) swap(y1, y2); + if (z1 > z2) swap(z1, z2); + if (u->pos.x >= x1 && u->pos.x <= x2) { + if (u->pos.y >= y1 && u->pos.y <= y2) { + if (u->pos.z >= z1 && u->pos.z <= z2) { + return true; + } + } + } + return false; +} + // returns index of creature actually read or -1 if no creature can be found bool Units::getUnitsInBox (std::vector &units, int16_t x1, int16_t y1, int16_t z1, @@ -106,22 +123,12 @@ bool Units::getUnitsInBox (std::vector &units, if (!world) return false; - if (x1 > x2) swap(x1, x2); - if (y1 > y2) swap(y1, y2); - if (z1 > z2) swap(z1, z2); - units.clear(); for (df::unit *u : world->units.all) { - if (u->pos.x >= x1 && u->pos.x <= x2) + if (isUnitInBox(u, x1, y1, z1, x2, y2, z2)) { - if (u->pos.y >= y1 && u->pos.y <= y2) - { - if (u->pos.z >= z1 && u->pos.z <= z2) - { - units.push_back(u); - } - } + units.push_back(u); } } return true; From d112649886abfafa799f0f396c1f6ad2f60e331f Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sat, 12 Nov 2022 16:40:20 -0800 Subject: [PATCH 037/161] Organizes and documents Units::is*(unit) functions --- docs/Lua API.rst | 295 +++++-- library/include/modules/Units.h | 132 +-- library/modules/Units.cpp | 1434 ++++++++++++++++--------------- 3 files changed, 1012 insertions(+), 849 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 33e133162..66631f483 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1188,150 +1188,301 @@ Job module Units module ------------ -* ``dfhack.units.getPosition(unit)`` +* ``dfhack.units.isUnitInBox(unit,x1,y1,z1,x2,y2,z2)`` - Returns true *x,y,z* of the unit, or *nil* if invalid; may be not equal to unit.pos if caged. + The unit is within the specified coordinates. -* ``dfhack.units.isUnitInBox(unit,x1,y1,z1,x2,y2,z2)`` +* ``dfhack.units.isActive(unit)`` - Returns true if the unit is within the specified coordinates. + The unit is active (alive and on the map). -* ``dfhack.units.getUnitsInBox(x1,y1,z1,x2,y2,z2[,filter])`` +* ``dfhack.units.isVisible(unit)`` - Returns a table of all units within the specified coordinates. If the ``filter`` - argument is given, only units where ``filter(unit)`` returns true will be included. - Note that ``pos2xyz()`` cannot currently be used to convert coordinate objects to - the arguments required by this function. + The unit is visible on the map. -* ``dfhack.units.teleport(unit, pos)`` +* ``dfhack.units.isCitizen(unit[,ignore_sanity])`` - Moves the specified unit and any riders to the target coordinates, setting - tile occupancy flags appropriately. Returns true if successful. + The unit is an alive sane citizen of the fortress; wraps the + same checks the game uses to decide game-over by extinction, + with an additional sanity check. You can identify citizens, + regardless of their sanity, by passing ``true`` as the optional + second parameter. -* ``dfhack.units.getGeneralRef(unit, type)`` +* ``dfhack.units.isFortControlled(unit)`` - Searches for a general_ref with the given type. + Similar to ``dfhack.units.isCitizen(unit)``, but is based on checks + for units hidden in ambush, and includes tame animals. Returns *false* + if not in fort mode. -* ``dfhack.units.getSpecificRef(unit, type)`` +* ``dfhack.units.isOwnCiv(unit)`` - Searches for a specific_ref with the given type. + The unit belongs to the player's civilization. -* ``dfhack.units.getContainer(unit)`` +* ``dfhack.units.isOwnGroup(unit)`` - Returns the container (cage) item or *nil*. + The unit belongs to the player's group. -* ``dfhack.units.setNickname(unit,nick)`` +* ``dfhack.units.isOwnRace(unit)`` - Sets the unit's nickname properly. + The unit belongs to the player's race. -* ``dfhack.units.getOuterContainerRef(unit)`` +* ``dfhack.units.isAlive(unit)`` - Returns a table (in the style of a ``specific_ref`` struct) of the outermost object that contains the unit (or one of the unit itself.) - The ``type`` field contains a ``specific_ref_type`` of ``UNIT``, ``ITEM_GENERAL``, or ``VERMIN_EVENT``. - The ``object`` field contains a pointer to a unit, item, or vermin, respectively. + The unit isn't dead or undead. -* ``dfhack.units.getVisibleName(unit)`` +* ``dfhack.units.isDead(unit)`` - Returns the language_name object visible in game, accounting for false identities. + The unit is completely dead and passive, or a ghost. Equivalent to + ``dfhack.units.isKilled(unit) or dfhack.units.isGhost(unit)``. -* ``dfhack.units.getIdentity(unit)`` +* ``dfhack.units.isKilled(unit)`` - Returns the false identity of the unit if it has one, or *nil*. + The unit has been killed. -* ``dfhack.units.getNemesis(unit)`` +* ``dfhack.units.isSane(unit)`` - Returns the nemesis record of the unit if it has one, or *nil*. + The unit is capable of rational action, i.e. not dead, insane, zombie, or active werewolf. + +* ``dfhack.units.isCrazed`` + + The unit is berserk and will attack all other creatures except members of its own species + that are also crazed. (can be modified by curses) + +* ``dfhack.units.isGhost(unit)`` + + The unit is a ghost. + +* ``dfhack.units.isHidden(unit)`` + + The unit is hidden to the player, accounting for sneaking. Works for any game mode. * ``dfhack.units.isHidingCurse(unit)`` - Checks if the unit hides improved attributes from its curse. + The unit is hiding a curse. -* ``dfhack.units.getPhysicalAttrValue(unit, attr_type)`` -* ``dfhack.units.getMentalAttrValue(unit, attr_type)`` - Computes the effective attribute value, including curse effect. +* ``dfhack.units.isMale(unit)`` -* ``dfhack.units.isCrazed(unit)`` -* ``dfhack.units.isOpposedToLife(unit)`` -* ``dfhack.units.hasExtravision(unit)`` -* ``dfhack.units.isBloodsucker(unit)`` + The unit is male. - Simple checks of caste attributes that can be modified by curses. +* ``dfhack.units.isFemale(unit)`` -* ``dfhack.units.getMiscTrait(unit, type[, create])`` + The unit is female. - Finds (or creates if requested) a misc trait object with the given id. +* ``dfhack.units.isBaby(unit)`` -* ``dfhack.units.isActive(unit)`` + The unit is a baby. - The unit is active (alive and on the map). +* ``dfhack.units.isChild(unit)`` -* ``dfhack.units.isAlive(unit)`` + The unit is a child. - The unit isn't dead or undead. +* ``dfhack.units.isAdult(unit)`` -* ``dfhack.units.isDead(unit)`` + The unit is an adult. - The unit is completely dead and passive, or a ghost. Equivalent to - ``dfhack.units.isKilled(unit) or dfhack.units.isGhost(unit)``. +* ``dfhack.units.isGay(unit)`` -* ``dfhack.units.isKilled(unit)`` + The unit is gay. - The unit has been killed. +* ``dfhack.units.isNake(unit)`` -* ``dfhack.units.isGhost(unit)`` + The unit is naked. - The unit is a ghost. +* ``dfhack.units.isVisiting(unit)`` -* ``dfhack.units.isSane(unit)`` + The unit is visiting. eg. Merchants, Diplomatics, travelers. + + +* ``dfhack.units.isTrainableHunting(unit)`` + + The unit is trainable for hunting. + +* ``dfhack.units.isTrainableWar(unit)`` + + The unit is trainable for war. + +* ``dfhack.units.isTrained(unit)`` + + The unit is trained. + +* ``dfhack.units.isHunter(unit)`` + + The unit is a trained hunter. + +* ``dfhack.units.isWar(unit)`` + + The unit is trained for war. + +* ``dfhack.units.isTame(unit)`` + + The unit is tame. + +* ``dfhack.units.isTamable(unit)`` + + The unit is tamable. + +* ``dfhack.units.isDomesticated(unit)`` + + The unit is domesticated. + +* ``dfhack.units.isMarkedForSlaughter(unit)`` + + The unit is marked for slaughter. + +* ``dfhack.units.isGelded(unit)`` + + The unit is gelded. + +* ``dfhack.units.isEggLayer(unit)`` + + The unit is an egg layer. + +* ``dfhack.units.isGrazer(unit)`` + + The unit is a grazer. + +* ``dfhack.units.isMilkable(unit)`` + + The unit is milkable. + +* ``dfhack.units.isForest(unit)`` + + The unit is of the forest. + +* ``dfhack.units.isMischievous(unit)`` + + The unit is mischievous. + +* ``dfhack.units.isAvailableForAdoption(unit)`` + + The unit is available for adoption. + + +* ``dfhack.units.isOpposedToLife(unit)`` +* ``dfhack.units.hasExtravision(unit)`` +* ``dfhack.units.isBloodsucker(unit)`` + + Simple checks of caste attributes that can be modified by curses. - The unit is capable of rational action, i.e. not dead, insane, zombie, or active werewolf. * ``dfhack.units.isDwarf(unit)`` - The unit is of the correct race of the fortress. + The unit is of the correct race for the fortress. -* ``dfhack.units.isCitizen(unit[,ignore_sanity])`` +* ``dfhack.units.isAnimal(unit)`` - The unit is an alive sane citizen of the fortress; wraps the - same checks the game uses to decide game-over by extinction - (except for the sanity check). + The unit is an animal. -* ``dfhack.units.isInvader(unit)`` +* ``dfhack.units.isMerchant(unit)`` - The unit is an active invader or marauder. + The unit is a merchant. -* ``dfhack.units.isVisiting(unit)`` +* ``dfhack.units.isDiplomat(unit)`` - The unit is either a merchant, diplomat, or plain visitor. + The unit is a diplomat. * ``dfhack.units.isVisitor(unit)`` - The unit is strictly a visitor. Merchants and diplomats do not count here. + The unit is a regular visitor with no special purpose (eg. merchant). + +* ``dfhack.units.isInvader(unit)`` + + The unit is an active invader or marauder. * ``dfhack.units.isUndead(unit[,include_vamps])`` The unit is undead, but not a vampire. +* ``dfhack.units.isNightCreature(unit)`` + + The unit is undead, but not a vampire. + +* ``dfhack.units.isSemiMegabeast(unit)`` + + The unit is undead, but not a vampire. + +* ``dfhack.units.isMegabeast(unit)`` + + The unit is a megabeast. + +* ``dfhack.units.isTitan(unit)`` + + The unit is a titan. + +* ``dfhack.units.isDemon(unit)`` + + The unit is a demon. + +* ``dfhack.units.isDanger(unit)`` + + The unit is dangerous, and probably hostile. This includes + Great Dangers (see below), semi-megabeasts, night creatures, + undead, invaders, and crazed units. + * ``dfhack.units.isGreatDanger(unit)`` The unit is of Great Danger. This include demons, titans, and megabeasts. -* ``dfhack.units.isDanger(unit)`` - The unit is dangerous, and probably hostile. This includes Great Dangers, semi-megabeasts, night creatures, undead, and invaders. +* ``dfhack.units.getPosition(unit)`` -* ``dfhack.units.isFortControlled(unit)`` + Returns true *x,y,z* of the unit, or *nil* if invalid; may be not equal to unit.pos if caged. - Similar to ``dfhack.units.isCitizen(unit)``, but is based on checks for units hidden in ambush, and includes tame animals. Returns *false* if not in fort mode. +* ``dfhack.units.getUnitsInBox(x1,y1,z1,x2,y2,z2[,filter])`` -* ``dfhack.units.isVisible(unit)`` + Returns a table of all units within the specified coordinates. If the ``filter`` + argument is given, only units where ``filter(unit)`` returns true will be included. + Note that ``pos2xyz()`` cannot currently be used to convert coordinate objects to + the arguments required by this function. - The unit is visible on the map. +* ``dfhack.units.teleport(unit, pos)`` -* ``dfhack.units.isHidden(unit)`` + Moves the specified unit and any riders to the target coordinates, setting + tile occupancy flags appropriately. Returns true if successful. - The unit is hidden to the player, accounting for sneaking. Works for any game mode. +* ``dfhack.units.getGeneralRef(unit, type)`` + + Searches for a general_ref with the given type. + +* ``dfhack.units.getSpecificRef(unit, type)`` + + Searches for a specific_ref with the given type. + +* ``dfhack.units.getContainer(unit)`` + + Returns the container (cage) item or *nil*. + +* ``dfhack.units.setNickname(unit,nick)`` + + Sets the unit's nickname properly. + +* ``dfhack.units.getOuterContainerRef(unit)`` + + Returns a table (in the style of a ``specific_ref`` struct) of the outermost object that contains the unit (or one of the unit itself.) + The ``type`` field contains a ``specific_ref_type`` of ``UNIT``, ``ITEM_GENERAL``, or ``VERMIN_EVENT``. + The ``object`` field contains a pointer to a unit, item, or vermin, respectively. + +* ``dfhack.units.getVisibleName(unit)`` + + Returns the language_name object visible in game, accounting for false identities. + +* ``dfhack.units.getIdentity(unit)`` + + Returns the false identity of the unit if it has one, or *nil*. + +* ``dfhack.units.getNemesis(unit)`` + + Returns the nemesis record of the unit if it has one, or *nil*. + +* ``dfhack.units.getPhysicalAttrValue(unit, attr_type)`` +* ``dfhack.units.getMentalAttrValue(unit, attr_type)`` + + Computes the effective attribute value, including curse effect. + +* ``dfhack.units.getMiscTrait(unit, type[, create])`` + + Finds (or creates if requested) a misc trait object with the given id. * ``dfhack.units.getAge(unit[,true_age])`` diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index c001b6a5a..aa5edcf8c 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -68,14 +68,78 @@ static const int MAX_COLORS = 15; * The Units module - allows reading all non-vermin units and their properties */ +DFHACK_EXPORT bool isUnitInBox(df::unit* u, +int16_t x1, int16_t y1, int16_t z1, +int16_t x2, int16_t y2, int16_t z2); + +DFHACK_EXPORT bool isActive(df::unit *unit); +DFHACK_EXPORT bool isVisible(df::unit* unit); +DFHACK_EXPORT bool isCitizen(df::unit *unit, bool ignore_sanity = false); +DFHACK_EXPORT bool isFortControlled(df::unit *unit); +DFHACK_EXPORT bool isOwnCiv(df::unit* unit); +DFHACK_EXPORT bool isOwnGroup(df::unit* unit); +DFHACK_EXPORT bool isOwnRace(df::unit* unit); + +DFHACK_EXPORT bool isAlive(df::unit *unit); +DFHACK_EXPORT bool isDead(df::unit *unit); +DFHACK_EXPORT bool isKilled(df::unit *unit); +DFHACK_EXPORT bool isSane(df::unit *unit); +DFHACK_EXPORT bool isCrazed(df::unit *unit); +DFHACK_EXPORT bool isGhost(df::unit *unit); +/// is unit hidden to the player? accounts for ambushing +DFHACK_EXPORT bool isHidden(df::unit *unit); +DFHACK_EXPORT bool isHidingCurse(df::unit *unit); + +DFHACK_EXPORT bool isMale(df::unit* unit); +DFHACK_EXPORT bool isFemale(df::unit* unit); +DFHACK_EXPORT bool isBaby(df::unit* unit); +DFHACK_EXPORT bool isChild(df::unit* unit); +DFHACK_EXPORT bool isAdult(df::unit* unit); +DFHACK_EXPORT bool isGay(df::unit* unit); +DFHACK_EXPORT bool isNaked(df::unit* unit); +DFHACK_EXPORT bool isVisiting(df::unit* unit); + +DFHACK_EXPORT bool isTrainableHunting(df::unit* unit); +DFHACK_EXPORT bool isTrainableWar(df::unit* unit); +DFHACK_EXPORT bool isTrained(df::unit* unit); +DFHACK_EXPORT bool isHunter(df::unit* unit); +DFHACK_EXPORT bool isWar(df::unit* unit); +DFHACK_EXPORT bool isTame(df::unit* unit); +DFHACK_EXPORT bool isTamable(df::unit* unit); +DFHACK_EXPORT bool isDomesticated(df::unit* unit); +DFHACK_EXPORT bool isMarkedForSlaughter(df::unit* unit); +DFHACK_EXPORT bool isGelded(df::unit* unit); +DFHACK_EXPORT bool isEggLayer(df::unit* unit); +DFHACK_EXPORT bool isGrazer(df::unit* unit); +DFHACK_EXPORT bool isMilkable(df::unit* unit); +DFHACK_EXPORT bool isForest(df::unit* unit); +DFHACK_EXPORT bool isMischievous(df::unit *unit); +DFHACK_EXPORT bool isAvailableForAdoption(df::unit* unit); + +DFHACK_EXPORT bool hasExtravision(df::unit *unit); +DFHACK_EXPORT bool isOpposedToLife(df::unit *unit); +DFHACK_EXPORT bool isBloodsucker(df::unit *unit); + +DFHACK_EXPORT bool isDwarf(df::unit *unit); +DFHACK_EXPORT bool isAnimal(df::unit* unit); +DFHACK_EXPORT bool isMerchant(df::unit* unit); +DFHACK_EXPORT bool isDiplomat(df::unit* unit); +DFHACK_EXPORT bool isVisitor(df::unit* unit); +DFHACK_EXPORT bool isInvader(df::unit* unit); +DFHACK_EXPORT bool isUndead(df::unit* unit, bool include_vamps = false); +DFHACK_EXPORT bool isNightCreature(df::unit* unit); +DFHACK_EXPORT bool isSemiMegabeast(df::unit* unit); +DFHACK_EXPORT bool isMegabeast(df::unit* unit); +DFHACK_EXPORT bool isTitan(df::unit* unit); +DFHACK_EXPORT bool isDemon(df::unit* unit); +DFHACK_EXPORT bool isDanger(df::unit* unit); +DFHACK_EXPORT bool isGreatDanger(df::unit* unit); + /* Read Functions */ // Read units in a box, starting with index. Returns -1 if no more units // found. Call repeatedly do get all units in a specified box (uses tile coords) DFHACK_EXPORT int32_t getNumUnits(); DFHACK_EXPORT df::unit *getUnit(const int32_t index); -DFHACK_EXPORT bool isUnitInBox(df::unit* u, - int16_t x1, int16_t y1, int16_t z1, - int16_t x2, int16_t y2, int16_t z2); DFHACK_EXPORT bool getUnitsInBox(std::vector &units, int16_t x1, int16_t y1, int16_t z1, int16_t x2, int16_t y2, int16_t z2); @@ -102,34 +166,11 @@ DFHACK_EXPORT df::language_name *getVisibleName(df::unit *unit); DFHACK_EXPORT df::identity *getIdentity(df::unit *unit); DFHACK_EXPORT df::nemesis_record *getNemesis(df::unit *unit); -DFHACK_EXPORT bool isHidingCurse(df::unit *unit); DFHACK_EXPORT int getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr); DFHACK_EXPORT int getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr); DFHACK_EXPORT bool casteFlagSet(int race, int caste, df::caste_raw_flags flag); -DFHACK_EXPORT bool isCrazed(df::unit *unit); -DFHACK_EXPORT bool isOpposedToLife(df::unit *unit); -DFHACK_EXPORT bool hasExtravision(df::unit *unit); -DFHACK_EXPORT bool isBloodsucker(df::unit *unit); -DFHACK_EXPORT bool isMischievous(df::unit *unit); - -DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false); - -DFHACK_EXPORT bool isDead(df::unit *unit); -DFHACK_EXPORT bool isAlive(df::unit *unit); -DFHACK_EXPORT bool isSane(df::unit *unit); -DFHACK_EXPORT bool isCitizen(df::unit *unit, bool ignore_sanity = false); -DFHACK_EXPORT bool isFortControlled(df::unit *unit); -DFHACK_EXPORT bool isDwarf(df::unit *unit); -DFHACK_EXPORT bool isWar(df::unit* unit); -DFHACK_EXPORT bool isHunter(df::unit* unit); -DFHACK_EXPORT bool isAvailableForAdoption(df::unit* unit); -DFHACK_EXPORT bool isOwnCiv(df::unit* unit); -DFHACK_EXPORT bool isOwnGroup(df::unit* unit); -DFHACK_EXPORT bool isOwnRace(df::unit* unit); -DFHACK_EXPORT bool isVisible(df::unit* unit); -/// is unit hidden to the player? accounts for ambushing -DFHACK_EXPORT bool isHidden(df::unit *unit); + DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false); DFHACK_EXPORT std::string getRaceNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceName(df::unit* unit); @@ -141,43 +182,6 @@ DFHACK_EXPORT std::string getRaceBabyName(df::unit* unit); DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceChildName(df::unit* unit); -DFHACK_EXPORT bool isInvader(df::unit* unit); -DFHACK_EXPORT bool isBaby(df::unit* unit); -DFHACK_EXPORT bool isChild(df::unit* unit); -DFHACK_EXPORT bool isAdult(df::unit* unit); -DFHACK_EXPORT bool isAnimal(df::unit* unit); -DFHACK_EXPORT bool isEggLayer(df::unit* unit); -DFHACK_EXPORT bool isGrazer(df::unit* unit); -DFHACK_EXPORT bool isMilkable(df::unit* unit); -DFHACK_EXPORT bool isTrainableWar(df::unit* unit); -DFHACK_EXPORT bool isTrainableHunting(df::unit* unit); -DFHACK_EXPORT bool isTamable(df::unit* unit); -DFHACK_EXPORT bool isMale(df::unit* unit); -DFHACK_EXPORT bool isFemale(df::unit* unit); -DFHACK_EXPORT bool isVisiting(df::unit* unit); -DFHACK_EXPORT bool isMerchant(df::unit* unit); -DFHACK_EXPORT bool isDiplomat(df::unit* unit); -DFHACK_EXPORT bool isVisitor(df::unit* unit); -DFHACK_EXPORT bool isForest(df::unit* unit); -DFHACK_EXPORT bool isMarkedForSlaughter(df::unit* unit); -DFHACK_EXPORT bool isTame(df::unit* unit); -DFHACK_EXPORT bool isTrained(df::unit* unit); -DFHACK_EXPORT bool isGay(df::unit* unit); -DFHACK_EXPORT bool isNaked(df::unit* unit); -DFHACK_EXPORT bool isUndead(df::unit* unit, bool include_vamps = false); -DFHACK_EXPORT bool isGhost(df::unit *unit); -DFHACK_EXPORT bool isActive(df::unit *unit); -DFHACK_EXPORT bool isKilled(df::unit *unit); -DFHACK_EXPORT bool isGelded(df::unit* unit); -DFHACK_EXPORT bool isDomesticated(df::unit* unit); -DFHACK_EXPORT bool isDemon(df::unit* unit); -DFHACK_EXPORT bool isTitan(df::unit* unit); -DFHACK_EXPORT bool isMegabeast(df::unit* unit); -DFHACK_EXPORT bool isGreatDanger(df::unit* unit); -DFHACK_EXPORT bool isSemiMegabeast(df::unit* unit); -DFHACK_EXPORT bool isNightCreature(df::unit* unit); -DFHACK_EXPORT bool isDanger(df::unit* unit); - DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false); DFHACK_EXPORT int getKillCount(df::unit *unit); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 5dc41da02..0395044ce 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -88,19 +88,9 @@ using df::global::ui; using df::global::gamemode; using df::global::gametype; -int32_t Units::getNumUnits() -{ - return world->units.all.size(); -} - -df::unit *Units::getUnit (const int32_t index) -{ - return vector_get(world->units.all, index); -} - bool Units::isUnitInBox(df::unit* u, - int16_t x1, int16_t y1, int16_t z1, - int16_t x2, int16_t y2, int16_t z2) { + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2) { if (x1 > x2) swap(x1, x2); if (y1 > y2) swap(y1, y2); @@ -115,218 +105,215 @@ bool Units::isUnitInBox(df::unit* u, return false; } -// returns index of creature actually read or -1 if no creature can be found -bool Units::getUnitsInBox (std::vector &units, - int16_t x1, int16_t y1, int16_t z1, - int16_t x2, int16_t y2, int16_t z2) +bool Units::isActive(df::unit *unit) { - if (!world) - return false; + CHECK_NULL_POINTER(unit); - units.clear(); - for (df::unit *u : world->units.all) - { - if (isUnitInBox(u, x1, y1, z1, x2, y2, z2)) - { - units.push_back(u); - } - } - return true; + return !unit->flags1.bits.inactive; } -int32_t Units::findIndexById(int32_t creature_id) +bool Units::isVisible(df::unit* unit) { - return df::unit::binsearch_index(world->units.all, creature_id); + CHECK_NULL_POINTER(unit); + return Maps::isTileVisible(unit->pos); } -df::coord Units::getPosition(df::unit *unit) +bool Units::isCitizen(df::unit *unit, bool ignore_sanity) { CHECK_NULL_POINTER(unit); - if (unit->flags1.bits.caged) - { - auto cage = getContainer(unit); - if (cage) - return Items::getPosition(cage); - } + // Copied from the conditions used to decide game over, + // except that the game appears to let melancholy/raving + // dwarves count as citizens. - return unit->pos; -} + if (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader || + unit->flags1.bits.forest || + unit->flags1.bits.merchant || + unit->flags1.bits.diplomat || + unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited || + unit->flags2.bits.underworld || + unit->flags2.bits.resident) + return false; -bool Units::teleport(df::unit *unit, df::coord target_pos) -{ - // make sure source and dest map blocks are valid - auto old_occ = Maps::getTileOccupancy(unit->pos); - auto new_occ = Maps::getTileOccupancy(target_pos); - if (!old_occ || !new_occ) + if (!ignore_sanity && !isSane(unit)) return false; - // clear appropriate occupancy flags at old tile - if (unit->flags1.bits.on_ground) - // this is potentially wrong, but the game will recompute this as needed - old_occ->bits.unit_grounded = 0; - else - old_occ->bits.unit = 0; + return isOwnGroup(unit); +} - // if there's already somebody standing at the destination, then force the - // unit to lay down - if (new_occ->bits.unit) - unit->flags1.bits.on_ground = 1; +bool Units::isFortControlled(df::unit *unit) +{ // Reverse-engineered from ambushing unit code + CHECK_NULL_POINTER(unit); - // set appropriate occupancy flags at new tile - if (unit->flags1.bits.on_ground) - new_occ->bits.unit_grounded = 1; - else - new_occ->bits.unit = 1; + if (*gamemode != game_mode::DWARF) + return false; - // move unit to destination - unit->pos = target_pos; - unit->idle_area = target_pos; + if (unit->mood == mood_type::Berserk || + Units::isCrazed(unit) || + Units::isOpposedToLife(unit) || + unit->enemy.undead || + unit->flags3.bits.ghostly) + return false; - // move unit's riders (including babies) to destination - if (unit->flags1.bits.ridden) - { - for (size_t j = 0; j < world->units.other[units_other_id::ANY_RIDER].size(); j++) - { - df::unit *rider = world->units.other[units_other_id::ANY_RIDER][j]; - if (rider->relationship_ids[df::unit_relationship_type::RiderMount] == unit->id) - rider->pos = unit->pos; - } - } + if (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader || + unit->flags1.bits.forest || + unit->flags1.bits.merchant || + unit->flags1.bits.diplomat) + return false; - return true; + if (unit->flags1.bits.tame) + return true; + + if (unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited || + unit->flags2.bits.underworld || + unit->flags2.bits.resident) + return false; + + return unit->civ_id != -1 && unit->civ_id == ui->civ_id; } -df::general_ref *Units::getGeneralRef(df::unit *unit, df::general_ref_type type) +// check if creature belongs to the player's civilization +// (don't try to pasture/slaughter random untame animals) +bool Units::isOwnCiv(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return findRef(unit->general_refs, type); + return unit->civ_id == ui->civ_id; } -df::specific_ref *Units::getSpecificRef(df::unit *unit, df::specific_ref_type type) +// check if creature belongs to the player's group +bool Units::isOwnGroup(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return findRef(unit->specific_refs, type); + auto histfig = df::historical_figure::find(unit->hist_figure_id); + if (!histfig) + return false; + for (size_t i = 0; i < histfig->entity_links.size(); i++) + { + auto link = histfig->entity_links[i]; + if (link->entity_id == ui->group_id && link->getType() == df::histfig_entity_link_type::MEMBER) + return true; + } + return false; } -df::item *Units::getContainer(df::unit *unit) +// check if creature belongs to the player's race +// (in combination with check for civ helps to filter out own dwarves) +bool Units::isOwnRace(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return findItemRef(unit->general_refs, general_ref_type::CONTAINED_IN_ITEM); + return unit->race == ui->race_id; } -void Units::getOuterContainerRef(df::specific_ref &spec_ref, df::unit *unit, bool init_ref) + +bool Units::isAlive(df::unit *unit) { CHECK_NULL_POINTER(unit); - // Reverse-engineered from ambushing unit code - - if (init_ref) - { - spec_ref.type = specific_ref_type::UNIT; - spec_ref.data.unit = unit; - } - if (unit->flags1.bits.caged) - { - df::item *cage = getContainer(unit); - if (cage) - return Items::getOuterContainerRef(spec_ref, cage); - } - return; + return !unit->flags2.bits.killed && + !unit->flags3.bits.ghostly && + !unit->curse.add_tags1.bits.NOT_LIVING; } -static df::identity *getFigureIdentity(df::historical_figure *figure) +bool Units::isDead(df::unit *unit) { - if (figure && figure->info && figure->info->reputation) - return df::identity::find(figure->info->reputation->cur_identity); + CHECK_NULL_POINTER(unit); - return NULL; + return unit->flags2.bits.killed || + unit->flags3.bits.ghostly; } -df::identity *Units::getIdentity(df::unit *unit) +bool Units::isKilled(df::unit *unit) { CHECK_NULL_POINTER(unit); - df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); - - return getFigureIdentity(figure); + return unit->flags2.bits.killed; } -void Units::setNickname(df::unit *unit, std::string nick) +bool Units::isSane(df::unit *unit) { CHECK_NULL_POINTER(unit); - // There are >=3 copies of the name, and the one - // in the unit is not the authoritative one. - // This is the reason why military units often - // lose nicknames set from Dwarf Therapist. - Translation::setNickname(&unit->name, nick); + if (isDead(unit) || + isOpposedToLife(unit) || + unit->enemy.undead) + return false; - if (unit->status.current_soul) - Translation::setNickname(&unit->status.current_soul->name, nick); + if (unit->enemy.normal_race == unit->enemy.were_race && isCrazed(unit)) + return false; - df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); - if (figure) + switch (unit->mood) { - Translation::setNickname(&figure->name, nick); - - if (auto identity = getFigureIdentity(figure)) - { - df::historical_figure *id_hfig = NULL; - - switch (identity->type) { - case df::identity_type::None: - case df::identity_type::HidingCurse: - case df::identity_type::FalseIdentity: - case df::identity_type::InfiltrationIdentity: - case df::identity_type::Identity: - break; // We want the nickname to end up in the identity - - case df::identity_type::Impersonating: - case df::identity_type::TrueName: - id_hfig = df::historical_figure::find(identity->histfig_id); - break; - } - - if (id_hfig) - { - Translation::setNickname(&id_hfig->name, nick); - } - else - Translation::setNickname(&identity->name, nick); - } + case mood_type::Melancholy: + case mood_type::Raving: + case mood_type::Berserk: + return false; + default: + break; } + + return true; } -df::language_name *Units::getVisibleName(df::unit *unit) +bool Units::isCrazed(df::unit *unit) { CHECK_NULL_POINTER(unit); + if (unit->flags3.bits.scuttle) + return false; + if (unit->curse.rem_tags1.bits.CRAZED) + return false; + if (unit->curse.add_tags1.bits.CRAZED) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED); +} - // as of 0.44.11, identity names take precedence over associated histfig names - if (auto identity = getIdentity(unit)) - return &identity->name; +bool Units::isGhost(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); - return &unit->name; + return unit->flags3.bits.ghostly; } -df::nemesis_record *Units::getNemesis(df::unit *unit) +bool Units::isHidden(df::unit *unit) { - if (!unit) - return NULL; + CHECK_NULL_POINTER(unit); + // Reverse-engineered from ambushing unit code - for (unsigned i = 0; i < unit->general_refs.size(); i++) + if (*df::global::debug_showambush) + return false; + + if (*gamemode == game_mode::ADVENTURE) { - df::nemesis_record *rv = unit->general_refs[i]->getNemesis(); - if (rv && rv->unit == unit) - return rv; + if (unit == world->units.active[0]) + return false; + else if (unit->flags1.bits.hidden_in_ambush) + return true; + } + else + { + if (*gametype == game_type::DWARF_ARENA) + return false; + else if (unit->flags1.bits.hidden_in_ambush && !isFortControlled(unit)) + return true; } - return NULL; -} + if (unit->flags1.bits.caged) + { + auto spec_ref = getOuterContainerRef(unit); + if (spec_ref.type == specific_ref_type::UNIT) + return isHidden(spec_ref.data.unit); + } + if (*gamemode == game_mode::ADVENTURE || isFortControlled(unit)) + return false; + else + return !Maps::isTileVisible(Units::getPosition(unit)); +} bool Units::isHidingCurse(df::unit *unit) { @@ -340,254 +327,241 @@ bool Units::isHidingCurse(df::unit *unit) return false; } -int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr) -{ - auto &aobj = unit->body.physical_attrs[attr]; - int value = std::max(0, aobj.value - aobj.soft_demotion); - - if (auto mod = unit->curse.attr_change) - { - int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; - - if (isHidingCurse(unit)) - value = std::min(value, mvalue); - else - value = mvalue; - } - return std::max(0, value); +bool Units::isMale(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->sex == 1; } -int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) +bool Units::isFemale(df::unit* unit) { - auto soul = unit->status.current_soul; - if (!soul) return 0; - - auto &aobj = soul->mental_attrs[attr]; - int value = std::max(0, aobj.value - aobj.soft_demotion); - - if (auto mod = unit->curse.attr_change) - { - int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; - - if (isHidingCurse(unit)) - value = std::min(value, mvalue); - else - value = mvalue; - } - - return std::max(0, value); + CHECK_NULL_POINTER(unit); + return unit->sex == 0; } -bool Units::casteFlagSet(int race, int caste, df::caste_raw_flags flag) +bool Units::isBaby(df::unit* unit) { - auto creature = df::creature_raw::find(race); - if (!creature) - return false; - - auto craw = vector_get(creature->caste, caste); - if (!craw) - return false; - - return craw->flags.is_set(flag); + CHECK_NULL_POINTER(unit); + return unit->profession == df::profession::BABY; } -bool Units::isCrazed(df::unit *unit) +bool Units::isChild(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->flags3.bits.scuttle) - return false; - if (unit->curse.rem_tags1.bits.CRAZED) - return false; - if (unit->curse.add_tags1.bits.CRAZED) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED); + return unit->profession == df::profession::CHILD; } -bool Units::isOpposedToLife(df::unit *unit) +bool Units::isAdult(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE) - return false; - if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE); + return !isBaby(unit) && !isChild(unit); } -bool Units::hasExtravision(df::unit *unit) +bool Units::isGay(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.EXTRAVISION) + if (!unit->status.current_soul) return false; - if (unit->curse.add_tags1.bits.EXTRAVISION) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION); + df::orientation_flags orientation = unit->status.current_soul->orientation_flags; + return (!Units::isFemale(unit) || !(orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male))) + && (!Units::isMale(unit) || !(orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female))); } -bool Units::isBloodsucker(df::unit *unit) +bool Units::isNaked(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.BLOODSUCKER) - return false; - if (unit->curse.add_tags1.bits.BLOODSUCKER) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER); + // TODO(kazimuth): is this correct? + return (unit->inventory.empty()); } -bool Units::isMischievous(df::unit *unit) +bool Units::isVisiting(df::unit* unit) { + CHECK_NULL_POINTER(unit); + + return unit->flags1.bits.merchant || + unit->flags1.bits.diplomat || + unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited; +} + + +bool Units::isTrainableHunting(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.MISCHIEVOUS) - return false; - if (unit->curse.add_tags1.bits.MISCHIEVOUS) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::TRAINABLE_HUNTING); } -df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create) +bool Units::isTrainableWar(df::unit* unit) { CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::TRAINABLE_WAR); +} - auto &vec = unit->status.misc_traits; - for (size_t i = 0; i < vec.size(); i++) - if (vec[i]->id == type) - return vec[i]; +bool Units::isTrained(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + // case a: trained for war/hunting (those don't have a training level, strangely) + if(Units::isWar(unit) || Units::isHunter(unit)) + return true; - if (create) + // case b: tamed and trained wild creature, gets a training level + bool trained = false; + switch (unit->training_level) { - auto obj = new df::unit_misc_trait(); - obj->id = type; - vec.push_back(obj); - return obj; + case df::animal_training_level::Trained: + case df::animal_training_level::WellTrained: + case df::animal_training_level::SkilfullyTrained: + case df::animal_training_level::ExpertlyTrained: + case df::animal_training_level::ExceptionallyTrained: + case df::animal_training_level::MasterfullyTrained: + //case df::animal_training_level::Domesticated: + trained = true; + break; + default: + break; } + return trained; +} - return NULL; +// check for profession "hunting creature" +bool Units::isHunter(df::unit* unit) +{ + CHECK_NULL_POINTER(unit) + return unit->profession == df::profession::TRAINED_HUNTER + || unit->profession2 == df::profession::TRAINED_HUNTER; } -bool Units::isDead(df::unit *unit) +// check for profession "war creature" +bool Units::isWar(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return unit->flags2.bits.killed || - unit->flags3.bits.ghostly; + return unit->profession == df::profession::TRAINED_WAR + || unit->profession2 == df::profession::TRAINED_WAR; } -bool Units::isAlive(df::unit *unit) +bool Units::isTame(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return !unit->flags2.bits.killed && - !unit->flags3.bits.ghostly && - !unit->curse.add_tags1.bits.NOT_LIVING; + bool tame = false; + if(unit->flags1.bits.tame) + { + switch (unit->training_level) + { + case df::animal_training_level::SemiWild: //?? + case df::animal_training_level::Trained: + case df::animal_training_level::WellTrained: + case df::animal_training_level::SkilfullyTrained: + case df::animal_training_level::ExpertlyTrained: + case df::animal_training_level::ExceptionallyTrained: + case df::animal_training_level::MasterfullyTrained: + case df::animal_training_level::Domesticated: + tame=true; + break; + case df::animal_training_level::Unk8: //?? + case df::animal_training_level::WildUntamed: + default: + tame=false; + break; + } + } + return tame; } -bool Units::isSane(df::unit *unit) +bool Units::isTamable(df::unit* unit) { CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::PET) + || caste->flags.is_set(caste_raw_flags::PET_EXOTIC); +} - if (isDead(unit) || - isOpposedToLife(unit) || - unit->enemy.undead) - return false; - - if (unit->enemy.normal_race == unit->enemy.were_race && isCrazed(unit)) - return false; - - switch (unit->mood) +// check if creature is domesticated +// seems to be the only way to really tell if it's completely safe to autonestbox it (training can revert) +bool Units::isDomesticated(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + bool tame = false; + if(unit->flags1.bits.tame) { - case mood_type::Melancholy: - case mood_type::Raving: - case mood_type::Berserk: - return false; - default: - break; + switch (unit->training_level) + { + case df::animal_training_level::Domesticated: + tame=true; + break; + default: + tame=false; + break; + } } - - return true; + return tame; } -bool Units::isCitizen(df::unit *unit, bool ignore_sanity) +bool Units::isMarkedForSlaughter(df::unit* unit) { CHECK_NULL_POINTER(unit); + return unit->flags2.bits.slaughter == 1; +} - // Copied from the conditions used to decide game over, - // except that the game appears to let melancholy/raving - // dwarves count as citizens. - - if (unit->flags1.bits.marauder || - unit->flags1.bits.invader_origin || - unit->flags1.bits.active_invader || - unit->flags1.bits.forest || - unit->flags1.bits.merchant || - unit->flags1.bits.diplomat || - unit->flags2.bits.visitor || - unit->flags2.bits.visitor_uninvited || - unit->flags2.bits.underworld || - unit->flags2.bits.resident) - return false; - - if (!ignore_sanity && !isSane(unit)) - return false; - - return isOwnGroup(unit); +bool Units::isGelded(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto wounds = unit->body.wounds; + for(auto wound = wounds.begin(); wound != wounds.end(); ++wound) + { + auto parts = (*wound)->parts; + for (auto part = parts.begin(); part != parts.end(); ++part) + { + if ((*part)->flags2.bits.gelded) + return true; + } + } + return false; } -bool Units::isFortControlled(df::unit *unit) -{ // Reverse-engineered from ambushing unit code +bool Units::isEggLayer(df::unit* unit) +{ CHECK_NULL_POINTER(unit); - - if (*gamemode != game_mode::DWARF) - return false; - - if (unit->mood == mood_type::Berserk || - Units::isCrazed(unit) || - Units::isOpposedToLife(unit) || - unit->enemy.undead || - unit->flags3.bits.ghostly) - return false; - - if (unit->flags1.bits.marauder || - unit->flags1.bits.invader_origin || - unit->flags1.bits.active_invader || - unit->flags1.bits.forest || - unit->flags1.bits.merchant || - unit->flags1.bits.diplomat) - return false; - - if (unit->flags1.bits.tame) - return true; - - if (unit->flags2.bits.visitor || - unit->flags2.bits.visitor_uninvited || - unit->flags2.bits.underworld || - unit->flags2.bits.resident) - return false; - - return unit->civ_id != -1 && unit->civ_id == ui->civ_id; + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::LAYS_EGGS) + || caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS); } -bool Units::isDwarf(df::unit *unit) +bool Units::isGrazer(df::unit* unit) { CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::GRAZER); +} - return unit->race == ui->race_id || - unit->enemy.normal_race == ui->race_id; +bool Units::isMilkable(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::MILKABLE); } -// check for profession "war creature" -bool Units::isWar(df::unit* unit) +bool Units::isForest(df::unit* unit) { CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::TRAINED_WAR - || unit->profession2 == df::profession::TRAINED_WAR; + return unit->flags1.bits.forest == 1; } -// check for profession "hunting creature" -bool Units::isHunter(df::unit* unit) +bool Units::isMischievous(df::unit *unit) { - CHECK_NULL_POINTER(unit) - return unit->profession == df::profession::TRAINED_HUNTER - || unit->profession2 == df::profession::TRAINED_HUNTER; + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.MISCHIEVOUS) + return false; + if (unit->curse.add_tags1.bits.MISCHIEVOUS) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS); } // check if unit is marked as available for adoption @@ -609,154 +583,70 @@ bool Units::isAvailableForAdoption(df::unit* unit) return false; } -// check if creature belongs to the player's civilization -// (don't try to pasture/slaughter random untame animals) -bool Units::isOwnCiv(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->civ_id == ui->civ_id; -} -// check if creature belongs to the player's group -bool Units::isOwnGroup(df::unit* unit) +bool Units::hasExtravision(df::unit *unit) { CHECK_NULL_POINTER(unit); - auto histfig = df::historical_figure::find(unit->hist_figure_id); - if (!histfig) + if (unit->curse.rem_tags1.bits.EXTRAVISION) return false; - for (size_t i = 0; i < histfig->entity_links.size(); i++) - { - auto link = histfig->entity_links[i]; - if (link->entity_id == ui->group_id && link->getType() == df::histfig_entity_link_type::MEMBER) - return true; - } - return false; -} - -// check if creature belongs to the player's race -// (in combination with check for civ helps to filter out own dwarves) -bool Units::isOwnRace(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->race == ui->race_id; -} - -bool Units::isVisible(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return Maps::isTileVisible(unit->pos); + if (unit->curse.add_tags1.bits.EXTRAVISION) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION); } -bool Units::isHidden(df::unit *unit) +bool Units::isOpposedToLife(df::unit *unit) { CHECK_NULL_POINTER(unit); - // Reverse-engineered from ambushing unit code - - if (*df::global::debug_showambush) - return false; - - if (*gamemode == game_mode::ADVENTURE) - { - if (unit == world->units.active[0]) - return false; - else if (unit->flags1.bits.hidden_in_ambush) - return true; - } - else - { - if (*gametype == game_type::DWARF_ARENA) - return false; - else if (unit->flags1.bits.hidden_in_ambush && !isFortControlled(unit)) - return true; - } - - if (unit->flags1.bits.caged) - { - auto spec_ref = getOuterContainerRef(unit); - if (spec_ref.type == specific_ref_type::UNIT) - return isHidden(spec_ref.data.unit); - } - - if (*gamemode == game_mode::ADVENTURE || isFortControlled(unit)) + if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE) return false; - else - return !Maps::isTileVisible(Units::getPosition(unit)); + if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE); } -// get race name by id or unit pointer -string Units::getRaceNameById(int32_t id) -{ - df::creature_raw *raw = world->raws.creatures.all[id]; - if (raw) - return raw->creature_id; - return ""; -} -string Units::getRaceName(df::unit* unit) +bool Units::isBloodsucker(df::unit *unit) { CHECK_NULL_POINTER(unit); - return getRaceNameById(unit->race); + if (unit->curse.rem_tags1.bits.BLOODSUCKER) + return false; + if (unit->curse.add_tags1.bits.BLOODSUCKER) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER); } -void df_unit_get_physical_description(df::unit* unit, string* out_str) -{ - static auto* const fn = - reinterpret_cast( - Core::getInstance().vinfo->getAddress("unit_get_physical_description")); - if (fn) - fn(unit, out_str); - else - *out_str = ""; -} -string Units::getPhysicalDescription(df::unit* unit) +bool Units::isDwarf(df::unit *unit) { CHECK_NULL_POINTER(unit); - string str; - df_unit_get_physical_description(unit, &str); - return str; + + return unit->race == ui->race_id || + unit->enemy.normal_race == ui->race_id; } -// get plural of race name (used for display in autobutcher UI and for sorting the watchlist) -string Units::getRaceNamePluralById(int32_t id) +bool Units::isAnimal(df::unit* unit) { - df::creature_raw *raw = world->raws.creatures.all[id]; - if (raw) - return raw->name[1]; // second field is plural of race name - return ""; + CHECK_NULL_POINTER(unit) + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NATURAL_ANIMAL); } -string Units::getRaceNamePlural(df::unit* unit) +bool Units::isMerchant(df::unit* unit) { CHECK_NULL_POINTER(unit); - return getRaceNamePluralById(unit->race); -} -string Units::getRaceBabyNameById(int32_t id) -{ - df::creature_raw *raw = world->raws.creatures.all[id]; - if (raw) - return raw->general_baby_name[0]; - return ""; + return unit->flags1.bits.merchant == 1; } -string Units::getRaceBabyName(df::unit* unit) +bool Units::isDiplomat(df::unit* unit) { CHECK_NULL_POINTER(unit); - return getRaceBabyNameById(unit->race); -} -string Units::getRaceChildNameById(int32_t id) -{ - df::creature_raw *raw = world->raws.creatures.all[id]; - if (raw) - return raw->general_child_name[0]; - return ""; + return unit->flags1.bits.diplomat == 1; } -string Units::getRaceChildName(df::unit* unit) +bool Units::isVisitor(df::unit* unit) { CHECK_NULL_POINTER(unit); - return getRaceChildNameById(unit->race); + return unit->flags2.bits.visitor || unit->flags2.bits.visitor_uninvited; } bool Units::isInvader(df::unit* unit) { @@ -768,90 +658,434 @@ bool Units::isInvader(df::unit* unit) { unit->flags1.bits.active_invader); } -bool Units::isBaby(df::unit* unit) +bool Units::isUndead(df::unit* unit, bool include_vamps) { CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::BABY; + + const auto &cb = unit->curse.add_tags1.bits; + return unit->flags3.bits.ghostly || + ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (include_vamps || !cb.BLOODSUCKER)); } -bool Units::isChild(df::unit* unit) +bool Units::isNightCreature(df::unit* unit) { CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::CHILD; + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NIGHT_CREATURE); } -bool Units::isAdult(df::unit* unit) +bool Units::isSemiMegabeast(df::unit* unit) { CHECK_NULL_POINTER(unit); - return !isBaby(unit) && !isChild(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::SEMIMEGABEAST); } -bool Units::isAnimal(df::unit* unit) +bool Units::isMegabeast(df::unit* unit) { - CHECK_NULL_POINTER(unit) - return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NATURAL_ANIMAL); + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::MEGABEAST); } -bool Units::isEggLayer(df::unit* unit) +bool Units::isTitan(df::unit* unit) { CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::LAYS_EGGS) - || caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::TITAN); } -bool Units::isGrazer(df::unit* unit) +bool Units::isDemon(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + using namespace df::enums::caste_raw_flags; + const auto &cf = unit->enemy.caste_flags; + return cf.is_set(DEMON) || cf.is_set(UNIQUE_DEMON); +} + +bool Units::isDanger(df::unit* unit) { + CHECK_NULL_POINTER(unit); + return isCrazed(unit) || + isInvader(unit) || + isUndead(unit, true) || + isSemiMegabeast(unit) || + isNightCreature(unit) || + isGreatDanger(unit); +} + +bool Units::isGreatDanger(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return isDemon(unit) || isTitan(unit) || isMegabeast(unit); +} + + + +int32_t Units::getNumUnits() +{ + return world->units.all.size(); +} + +df::unit *Units::getUnit (const int32_t index) +{ + return vector_get(world->units.all, index); +} + +// returns index of creature actually read or -1 if no creature can be found +bool Units::getUnitsInBox (std::vector &units, + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2) +{ + if (!world) + return false; + + units.clear(); + for (df::unit *u : world->units.all) + { + if (isUnitInBox(u, x1, y1, z1, x2, y2, z2)) + { + units.push_back(u); + } + } + return true; +} + +int32_t Units::findIndexById(int32_t creature_id) +{ + return df::unit::binsearch_index(world->units.all, creature_id); +} + +df::coord Units::getPosition(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + if (unit->flags1.bits.caged) + { + auto cage = getContainer(unit); + if (cage) + return Items::getPosition(cage); + } + + return unit->pos; +} + +bool Units::teleport(df::unit *unit, df::coord target_pos) +{ + // make sure source and dest map blocks are valid + auto old_occ = Maps::getTileOccupancy(unit->pos); + auto new_occ = Maps::getTileOccupancy(target_pos); + if (!old_occ || !new_occ) + return false; + + // clear appropriate occupancy flags at old tile + if (unit->flags1.bits.on_ground) + // this is potentially wrong, but the game will recompute this as needed + old_occ->bits.unit_grounded = 0; + else + old_occ->bits.unit = 0; + + // if there's already somebody standing at the destination, then force the + // unit to lay down + if (new_occ->bits.unit) + unit->flags1.bits.on_ground = 1; + + // set appropriate occupancy flags at new tile + if (unit->flags1.bits.on_ground) + new_occ->bits.unit_grounded = 1; + else + new_occ->bits.unit = 1; + + // move unit to destination + unit->pos = target_pos; + unit->idle_area = target_pos; + + // move unit's riders (including babies) to destination + if (unit->flags1.bits.ridden) + { + for (size_t j = 0; j < world->units.other[units_other_id::ANY_RIDER].size(); j++) + { + df::unit *rider = world->units.other[units_other_id::ANY_RIDER][j]; + if (rider->relationship_ids[df::unit_relationship_type::RiderMount] == unit->id) + rider->pos = unit->pos; + } + } + + return true; +} + +df::general_ref *Units::getGeneralRef(df::unit *unit, df::general_ref_type type) +{ + CHECK_NULL_POINTER(unit); + + return findRef(unit->general_refs, type); +} + +df::specific_ref *Units::getSpecificRef(df::unit *unit, df::specific_ref_type type) +{ + CHECK_NULL_POINTER(unit); + + return findRef(unit->specific_refs, type); +} + +df::item *Units::getContainer(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + return findItemRef(unit->general_refs, general_ref_type::CONTAINED_IN_ITEM); +} + +void Units::getOuterContainerRef(df::specific_ref &spec_ref, df::unit *unit, bool init_ref) +{ + CHECK_NULL_POINTER(unit); + // Reverse-engineered from ambushing unit code + + if (init_ref) + { + spec_ref.type = specific_ref_type::UNIT; + spec_ref.data.unit = unit; + } + + if (unit->flags1.bits.caged) + { + df::item *cage = getContainer(unit); + if (cage) + return Items::getOuterContainerRef(spec_ref, cage); + } + return; +} + +static df::identity *getFigureIdentity(df::historical_figure *figure) +{ + if (figure && figure->info && figure->info->reputation) + return df::identity::find(figure->info->reputation->cur_identity); + + return NULL; +} + +df::identity *Units::getIdentity(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); + + return getFigureIdentity(figure); +} + +void Units::setNickname(df::unit *unit, std::string nick) +{ + CHECK_NULL_POINTER(unit); + + // There are >=3 copies of the name, and the one + // in the unit is not the authoritative one. + // This is the reason why military units often + // lose nicknames set from Dwarf Therapist. + Translation::setNickname(&unit->name, nick); + + if (unit->status.current_soul) + Translation::setNickname(&unit->status.current_soul->name, nick); + + df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); + if (figure) + { + Translation::setNickname(&figure->name, nick); + + if (auto identity = getFigureIdentity(figure)) + { + df::historical_figure *id_hfig = NULL; + + switch (identity->type) { + case df::identity_type::None: + case df::identity_type::HidingCurse: + case df::identity_type::FalseIdentity: + case df::identity_type::InfiltrationIdentity: + case df::identity_type::Identity: + break; // We want the nickname to end up in the identity + + case df::identity_type::Impersonating: + case df::identity_type::TrueName: + id_hfig = df::historical_figure::find(identity->histfig_id); + break; + } + + if (id_hfig) + { + Translation::setNickname(&id_hfig->name, nick); + } + else + Translation::setNickname(&identity->name, nick); + } + } +} + +df::language_name *Units::getVisibleName(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + // as of 0.44.11, identity names take precedence over associated histfig names + if (auto identity = getIdentity(unit)) + return &identity->name; + + return &unit->name; +} + +df::nemesis_record *Units::getNemesis(df::unit *unit) +{ + if (!unit) + return NULL; + + for (unsigned i = 0; i < unit->general_refs.size(); i++) + { + df::nemesis_record *rv = unit->general_refs[i]->getNemesis(); + if (rv && rv->unit == unit) + return rv; + } + + return NULL; +} + + +int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr) +{ + auto &aobj = unit->body.physical_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + +int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) +{ + auto soul = unit->status.current_soul; + if (!soul) return 0; + + auto &aobj = soul->mental_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + +bool Units::casteFlagSet(int race, int caste, df::caste_raw_flags flag) +{ + auto creature = df::creature_raw::find(race); + if (!creature) + return false; + + auto craw = vector_get(creature->caste, caste); + if (!craw) + return false; + + return craw->flags.is_set(flag); +} + +df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create) +{ + CHECK_NULL_POINTER(unit); + + auto &vec = unit->status.misc_traits; + for (size_t i = 0; i < vec.size(); i++) + if (vec[i]->id == type) + return vec[i]; + + if (create) + { + auto obj = new df::unit_misc_trait(); + obj->id = type; + vec.push_back(obj); + return obj; + } + + return NULL; +} + +// get race name by id or unit pointer +string Units::getRaceNameById(int32_t id) +{ + df::creature_raw *raw = world->raws.creatures.all[id]; + if (raw) + return raw->creature_id; + return ""; +} +string Units::getRaceName(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return getRaceNameById(unit->race); +} + +void df_unit_get_physical_description(df::unit* unit, string* out_str) +{ + static auto* const fn = + reinterpret_cast( + Core::getInstance().vinfo->getAddress("unit_get_physical_description")); + if (fn) + fn(unit, out_str); + else + *out_str = ""; +} + +string Units::getPhysicalDescription(df::unit* unit) { CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::GRAZER); + string str; + df_unit_get_physical_description(unit, &str); + return str; } -bool Units::isMilkable(df::unit* unit) +// get plural of race name (used for display in autobutcher UI and for sorting the watchlist) +string Units::getRaceNamePluralById(int32_t id) { - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::MILKABLE); + df::creature_raw *raw = world->raws.creatures.all[id]; + if (raw) + return raw->name[1]; // second field is plural of race name + return ""; } -bool Units::isTrainableWar(df::unit* unit) +string Units::getRaceNamePlural(df::unit* unit) { CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::TRAINABLE_WAR); + return getRaceNamePluralById(unit->race); } -bool Units::isTrainableHunting(df::unit* unit) +string Units::getRaceBabyNameById(int32_t id) { - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::TRAINABLE_HUNTING); + df::creature_raw *raw = world->raws.creatures.all[id]; + if (raw) + return raw->general_baby_name[0]; + return ""; } -bool Units::isTamable(df::unit* unit) +string Units::getRaceBabyName(df::unit* unit) { CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::PET) - || caste->flags.is_set(caste_raw_flags::PET_EXOTIC); + return getRaceBabyNameById(unit->race); } -bool Units::isMale(df::unit* unit) +string Units::getRaceChildNameById(int32_t id) { - CHECK_NULL_POINTER(unit); - return unit->sex == 1; + df::creature_raw *raw = world->raws.creatures.all[id]; + if (raw) + return raw->general_child_name[0]; + return ""; } -bool Units::isFemale(df::unit* unit) +string Units::getRaceChildName(df::unit* unit) { CHECK_NULL_POINTER(unit); - return unit->sex == 0; + return getRaceChildNameById(unit->race); } @@ -1682,232 +1916,6 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) return entry->events[entry->events.size() - 1]; } -bool Units::isVisiting(df::unit* unit) { - CHECK_NULL_POINTER(unit); - - return unit->flags1.bits.merchant || - unit->flags1.bits.diplomat || - unit->flags2.bits.visitor || - unit->flags2.bits.visitor_uninvited; -} - -bool Units::isMerchant(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags1.bits.merchant == 1; -} - -bool Units::isDiplomat(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags1.bits.diplomat == 1; -} - -bool Units::isVisitor(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->flags2.bits.visitor || unit->flags2.bits.visitor_uninvited; -} - -bool Units::isForest(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->flags1.bits.forest == 1; -} - -bool Units::isMarkedForSlaughter(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->flags2.bits.slaughter == 1; -} - -bool Units::isTame(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - bool tame = false; - if(unit->flags1.bits.tame) - { - switch (unit->training_level) - { - case df::animal_training_level::SemiWild: //?? - case df::animal_training_level::Trained: - case df::animal_training_level::WellTrained: - case df::animal_training_level::SkilfullyTrained: - case df::animal_training_level::ExpertlyTrained: - case df::animal_training_level::ExceptionallyTrained: - case df::animal_training_level::MasterfullyTrained: - case df::animal_training_level::Domesticated: - tame=true; - break; - case df::animal_training_level::Unk8: //?? - case df::animal_training_level::WildUntamed: - default: - tame=false; - break; - } - } - return tame; -} - -bool Units::isTrained(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - // case a: trained for war/hunting (those don't have a training level, strangely) - if(Units::isWar(unit) || Units::isHunter(unit)) - return true; - - // case b: tamed and trained wild creature, gets a training level - bool trained = false; - switch (unit->training_level) - { - case df::animal_training_level::Trained: - case df::animal_training_level::WellTrained: - case df::animal_training_level::SkilfullyTrained: - case df::animal_training_level::ExpertlyTrained: - case df::animal_training_level::ExceptionallyTrained: - case df::animal_training_level::MasterfullyTrained: - //case df::animal_training_level::Domesticated: - trained = true; - break; - default: - break; - } - return trained; -} - -bool Units::isGay(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - if (!unit->status.current_soul) - return false; - df::orientation_flags orientation = unit->status.current_soul->orientation_flags; - return (!Units::isFemale(unit) || !(orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male))) - && (!Units::isMale(unit) || !(orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female))); -} - -bool Units::isNaked(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - // TODO(kazimuth): is this correct? - return (unit->inventory.empty()); -} - -bool Units::isUndead(df::unit* unit, bool include_vamps) -{ - CHECK_NULL_POINTER(unit); - - const auto &cb = unit->curse.add_tags1.bits; - return unit->flags3.bits.ghostly || - ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (include_vamps || !cb.BLOODSUCKER)); -} - -bool Units::isGhost(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags3.bits.ghostly; -} - -bool Units::isActive(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return !unit->flags1.bits.inactive; -} - -bool Units::isKilled(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags2.bits.killed; -} - -bool Units::isGelded(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - auto wounds = unit->body.wounds; - for(auto wound = wounds.begin(); wound != wounds.end(); ++wound) - { - auto parts = (*wound)->parts; - for (auto part = parts.begin(); part != parts.end(); ++part) - { - if ((*part)->flags2.bits.gelded) - return true; - } - } - return false; -} - -// check if creature is domesticated -// seems to be the only way to really tell if it's completely safe to autonestbox it (training can revert) -bool Units::isDomesticated(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - bool tame = false; - if(unit->flags1.bits.tame) - { - switch (unit->training_level) - { - case df::animal_training_level::Domesticated: - tame=true; - break; - default: - tame=false; - break; - } - } - return tame; -} - -bool Units::isDemon(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - using namespace df::enums::caste_raw_flags; - const auto &cf = unit->enemy.caste_flags; - return cf.is_set(DEMON) || cf.is_set(UNIQUE_DEMON); -} - -bool Units::isTitan(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::TITAN); -} - -bool Units::isMegabeast(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::MEGABEAST); -} - -bool Units::isGreatDanger(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return isDemon(unit) || isTitan(unit) || isMegabeast(unit); -} - -bool Units::isSemiMegabeast(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::SEMIMEGABEAST); -} - -bool Units::isNightCreature(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NIGHT_CREATURE); -} - -bool Units::isDanger(df::unit* unit) { - CHECK_NULL_POINTER(unit); - return isInvader(unit) || - isUndead(unit, true) || - isSemiMegabeast(unit) || - isNightCreature(unit) || - isGreatDanger(unit); -} - // 50000 and up is level 0, 25000 and up is level 1, etc. const vector Units::stress_cutoffs {50000, 25000, 10000, -10000, -25000, -50000, -100000}; From 7e1bdb53026d7869471e80566fc79283914346e8 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sat, 12 Nov 2022 17:05:11 -0800 Subject: [PATCH 038/161] Matches Units.h ordering in LuaApi.cpp --- library/LuaApi.cpp | 112 ++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index b5a1cd754..ed59786a6 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1560,6 +1560,62 @@ static const luaL_Reg dfhack_job_funcs[] = { static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, isUnitInBox), + WRAPM(Units, isActive), + WRAPM(Units, isVisible), + WRAPM(Units, isCitizen), + WRAPM(Units, isFortControlled), + WRAPM(Units, isOwnCiv), + WRAPM(Units, isOwnGroup), + WRAPM(Units, isOwnRace), + WRAPM(Units, isAlive), + WRAPM(Units, isDead), + WRAPM(Units, isKilled), + WRAPM(Units, isSane), + WRAPM(Units, isCrazed), + WRAPM(Units, isGhost), + WRAPM(Units, isHidden), + WRAPM(Units, isHidingCurse), + WRAPM(Units, isMale), + WRAPM(Units, isFemale), + WRAPM(Units, isBaby), + WRAPM(Units, isChild), + WRAPM(Units, isAdult), + WRAPM(Units, isGay), + WRAPM(Units, isNaked), + WRAPM(Units, isVisiting), + WRAPM(Units, isTrainableHunting), + WRAPM(Units, isTrainableWar), + WRAPM(Units, isTrained), + WRAPM(Units, isHunter), + WRAPM(Units, isWar), + WRAPM(Units, isTame), + WRAPM(Units, isTamable), + WRAPM(Units, isDomesticated), + WRAPM(Units, isMarkedForSlaughter), + WRAPM(Units, isGelded), + WRAPM(Units, isEggLayer), + WRAPM(Units, isGrazer), + WRAPM(Units, isMilkable), + WRAPM(Units, isForest), + WRAPM(Units, isMischievous), + WRAPM(Units, isAvailableForAdoption), + WRAPM(Units, hasExtravision), + WRAPM(Units, isOpposedToLife), + WRAPM(Units, isBloodsucker), + WRAPM(Units, isDwarf), + WRAPM(Units, isAnimal), + WRAPM(Units, isMerchant), + WRAPM(Units, isDiplomat), + WRAPM(Units, isVisitor), + WRAPM(Units, isInvader), + WRAPM(Units, isUndead), + WRAPM(Units, isNightCreature), + WRAPM(Units, isSemiMegabeast), + WRAPM(Units, isMegabeast), + WRAPM(Units, isTitan), + WRAPM(Units, isDemon), + WRAPM(Units, isDanger), + WRAPM(Units, isGreatDanger), WRAPM(Units, teleport), WRAPM(Units, getGeneralRef), WRAPM(Units, getSpecificRef), @@ -1568,23 +1624,9 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getVisibleName), WRAPM(Units, getIdentity), WRAPM(Units, getNemesis), - WRAPM(Units, isHidingCurse), WRAPM(Units, getPhysicalAttrValue), WRAPM(Units, getMentalAttrValue), - WRAPM(Units, isCrazed), - WRAPM(Units, isOpposedToLife), - WRAPM(Units, hasExtravision), - WRAPM(Units, isBloodsucker), - WRAPM(Units, isMischievous), WRAPM(Units, getMiscTrait), - WRAPM(Units, isDead), - WRAPM(Units, isAlive), - WRAPM(Units, isSane), - WRAPM(Units, isDwarf), - WRAPM(Units, isCitizen), - WRAPM(Units, isFortControlled), - WRAPM(Units, isVisible), - WRAPM(Units, isHidden), WRAPM(Units, getAge), WRAPM(Units, getKillCount), WRAPM(Units, getNominalSkill), @@ -1602,12 +1644,6 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getGoalName), WRAPM(Units, isGoalAchieved), WRAPM(Units, getSquadName), - WRAPM(Units, isWar), - WRAPM(Units, isHunter), - WRAPM(Units, isAvailableForAdoption), - WRAPM(Units, isOwnCiv), - WRAPM(Units, isOwnGroup), - WRAPM(Units, isOwnRace), WRAPM(Units, getPhysicalDescription), WRAPM(Units, getRaceName), WRAPM(Units, getRaceNamePlural), @@ -1616,42 +1652,6 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getRaceBabyNameById), WRAPM(Units, getRaceChildName), WRAPM(Units, getRaceChildNameById), - WRAPM(Units, isInvader), - WRAPM(Units, isBaby), - WRAPM(Units, isChild), - WRAPM(Units, isAdult), - WRAPM(Units, isAnimal), - WRAPM(Units, isEggLayer), - WRAPM(Units, isGrazer), - WRAPM(Units, isMilkable), - WRAPM(Units, isTrainableWar), - WRAPM(Units, isTrainableHunting), - WRAPM(Units, isTamable), - WRAPM(Units, isMale), - WRAPM(Units, isFemale), - WRAPM(Units, isVisiting), - WRAPM(Units, isMerchant), - WRAPM(Units, isDiplomat), - WRAPM(Units, isVisitor), - WRAPM(Units, isForest), - WRAPM(Units, isMarkedForSlaughter), - WRAPM(Units, isTame), - WRAPM(Units, isTrained), - WRAPM(Units, isGay), - WRAPM(Units, isNaked), - WRAPM(Units, isUndead), - WRAPM(Units, isGhost), - WRAPM(Units, isActive), - WRAPM(Units, isKilled), - WRAPM(Units, isGelded), - WRAPM(Units, isDomesticated), - WRAPM(Units, isDemon), - WRAPM(Units, isTitan), - WRAPM(Units, isMegabeast), - WRAPM(Units, isGreatDanger), - WRAPM(Units, isSemiMegabeast), - WRAPM(Units, isNightCreature), - WRAPM(Units, isDanger), WRAPM(Units, getMainSocialActivity), WRAPM(Units, getMainSocialEvent), WRAPM(Units, getStressCategory), From 441ff8a820912170ab526ededb8f60a8d0f7a773 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sat, 12 Nov 2022 17:55:17 -0800 Subject: [PATCH 039/161] Fixes Lua API.rst mistakes --- docs/Lua API.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 059f28d37..fc750f077 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1397,11 +1397,11 @@ Units module * ``dfhack.units.isNightCreature(unit)`` - The unit is undead, but not a vampire. + The unit is a Night Creature. * ``dfhack.units.isSemiMegabeast(unit)`` - The unit is undead, but not a vampire. + The unit is a semi-megabeast. * ``dfhack.units.isMegabeast(unit)`` From 52ce09804b35ae096c4dd542dd45a75366bfd0a5 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sat, 12 Nov 2022 19:07:16 -0800 Subject: [PATCH 040/161] Apply suggestions from code review --- docs/Lua API.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index fc750f077..1af3335df 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1285,7 +1285,7 @@ Units module The unit is gay. -* ``dfhack.units.isNake(unit)`` +* ``dfhack.units.isNaked(unit)`` The unit is naked. From 78021ec672bddf599b6a9567c02f64fbc45ddd74 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 13 Nov 2022 11:35:01 -0800 Subject: [PATCH 041/161] Adds indentation --- library/include/modules/Units.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index aa5edcf8c..9cfe7d3c5 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -69,8 +69,8 @@ static const int MAX_COLORS = 15; */ DFHACK_EXPORT bool isUnitInBox(df::unit* u, -int16_t x1, int16_t y1, int16_t z1, -int16_t x2, int16_t y2, int16_t z2); + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2); DFHACK_EXPORT bool isActive(df::unit *unit); DFHACK_EXPORT bool isVisible(df::unit* unit); From c164263af9f1ac2a7c142b74f2ce3ec2465ff74b Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 13 Nov 2022 11:35:20 -0800 Subject: [PATCH 042/161] Groups sections of functions in lua api docs --- docs/Lua API.rst | 65 +++--------------------------------------------- 1 file changed, 4 insertions(+), 61 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 1af3335df..b2d976ec2 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -1262,32 +1262,14 @@ Units module * ``dfhack.units.isMale(unit)`` - - The unit is male. - * ``dfhack.units.isFemale(unit)`` - - The unit is female. - * ``dfhack.units.isBaby(unit)`` - - The unit is a baby. - * ``dfhack.units.isChild(unit)`` - - The unit is a child. - * ``dfhack.units.isAdult(unit)`` - - The unit is an adult. - * ``dfhack.units.isGay(unit)`` - - The unit is gay. - * ``dfhack.units.isNaked(unit)`` - The unit is naked. + Simple unit property checks * ``dfhack.units.isVisiting(unit)`` @@ -1315,36 +1297,15 @@ Units module The unit is trained for war. * ``dfhack.units.isTame(unit)`` - - The unit is tame. - * ``dfhack.units.isTamable(unit)`` - - The unit is tamable. - * ``dfhack.units.isDomesticated(unit)`` - - The unit is domesticated. - * ``dfhack.units.isMarkedForSlaughter(unit)`` - - The unit is marked for slaughter. - * ``dfhack.units.isGelded(unit)`` - - The unit is gelded. - * ``dfhack.units.isEggLayer(unit)`` - - The unit is an egg layer. - * ``dfhack.units.isGrazer(unit)`` - - The unit is a grazer. - * ``dfhack.units.isMilkable(unit)`` - The unit is milkable. + Simple unit property checks. * ``dfhack.units.isForest(unit)`` @@ -1371,16 +1332,10 @@ Units module The unit is of the correct race for the fortress. * ``dfhack.units.isAnimal(unit)`` - - The unit is an animal. - * ``dfhack.units.isMerchant(unit)`` - - The unit is a merchant. - * ``dfhack.units.isDiplomat(unit)`` - The unit is a diplomat. + Simple unit type checks. * ``dfhack.units.isVisitor(unit)`` @@ -1396,24 +1351,12 @@ Units module count vampires as undead. * ``dfhack.units.isNightCreature(unit)`` - - The unit is a Night Creature. - * ``dfhack.units.isSemiMegabeast(unit)`` - - The unit is a semi-megabeast. - * ``dfhack.units.isMegabeast(unit)`` - - The unit is a megabeast. - * ``dfhack.units.isTitan(unit)`` - - The unit is a titan. - * ``dfhack.units.isDemon(unit)`` - The unit is a demon. + Simple enemy type checks. * ``dfhack.units.isDanger(unit)`` From aa5c6515e001afd77df3547b1f7ea6028ff28291 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 13 Nov 2022 11:39:20 -0800 Subject: [PATCH 043/161] Removes rogue indent --- library/include/modules/Units.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 9cfe7d3c5..be630b802 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -170,7 +170,7 @@ DFHACK_EXPORT int getPhysicalAttrValue(df::unit *unit, df::physical_attribute_ty DFHACK_EXPORT int getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr); DFHACK_EXPORT bool casteFlagSet(int race, int caste, df::caste_raw_flags flag); - DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false); +DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false); DFHACK_EXPORT std::string getRaceNameById(int32_t race_id); DFHACK_EXPORT std::string getRaceName(df::unit* unit); From d7fba5c720e47c0c9469dab144c8b94faabc5a1e Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 19 Oct 2022 13:04:16 -0700 Subject: [PATCH 044/161] add stub lua layer for overlay --- plugins/CMakeLists.txt | 2 +- plugins/lua/overlay.lua | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 plugins/lua/overlay.lua diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index c9438e04e..7f22a47b3 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -144,7 +144,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(mousequery mousequery.cpp) dfhack_plugin(nestboxes nestboxes.cpp) dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static) - dfhack_plugin(overlay overlay.cpp) + dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(petcapRemover petcapRemover.cpp) dfhack_plugin(plants plants.cpp) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua new file mode 100644 index 000000000..b0cb2f0c2 --- /dev/null +++ b/plugins/lua/overlay.lua @@ -0,0 +1,3 @@ +local _ENV = mkmodule('plugins.overlay') + +return _ENV From bd318b7a215cf9c46b2657715b4c6244ae104ea1 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 31 Oct 2022 12:22:00 -0700 Subject: [PATCH 045/161] implement basic overlay event logic --- plugins/lua/overlay.lua | 80 +++++++++++++++++++++++++++++++++++++++++ plugins/overlay.cpp | 71 +++++++++++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index b0cb2f0c2..90852f9fe 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -1,3 +1,83 @@ local _ENV = mkmodule('plugins.overlay') +local widgets = require('gui.widgets') + +local widget_db = {} -- map of widget name to state +local active_hotspot_widgets = {} -- map of widget names to the db entry +local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db +local active_triggered_screen = nil + +function reload() + widget_db = {} + active_hotspot_widgets = {} + active_viewscreen_widgets = {} + active_triggered_screen = nil +end + +-- reduces the next call by a small random amount to introduce jitter into the +-- widget processing timings +local function do_update(db_entry, now_ms, vs) + if db_entry.next_update_ms > now_ms then return end + local w = db_entry.widget + local freq_ms = w.overlay_onupdate_max_freq_seconds * 1000 + local jitter = math.rand(0, freq_ms // 8) -- up to ~12% jitter + db_entry.next_update_ms = now_ms + freq_ms - jitter + if w:overlay_onupdate(vs) then + active_triggered_screen = w:overlay_trigger() + if active_triggered_screen then return true end + end +end + +function update_hotspot_widgets() + if active_triggered_screen then + if active_triggered_screen:isActive() then return end + active_triggered_screen = nil + end + local now_ms = dfhack.getTickCount() + for _,db_entry in pairs(active_hotspot_widgets) do + if do_update(db_entry, now_ms) then return end + end +end + +function update_viewscreen_widgets(vs_name, vs) + local vs_widgets = active_viewscreen_widgets[vs_name] + if not vs_widgets then return end + local now_ms = dfhack.getTickCount() + for _,db_entry in pairs(vs_widgets) do + if do_update(db_entry, now_ms, vs) then return end + end +end + +function feed_viewscreen_widgets(vs_name, keys) + local vs_widgets = active_viewscreen_widgets[vs_name] + if not vs_widgets then return false end + for _,db_entry in pairs(vs_widgets) do + if db_entry.widget:onInput(keys) then return true end + end + return false +end + +function render_viewscreen_widgets(vs_name) + local vs_widgets = active_viewscreen_widgets[vs_name] + if not vs_widgets then return false end + local dc = Painter.new() + for _,db_entry in pairs(vs_widgets) do + db_entry.widget:render(dc) + end +end + +-- called when the DF window is resized +function reposition_widgets() + local w, h = dscreen.getWindowSize() + local vr = ViewRect{rect=mkdims_wh(0, 0, w, h)} + for _,db_entry in pairs(widget_db) do + db_entry.widget:updateLayout(vr) + end +end + +OverlayWidget = defclass(OverlayWidget, widgets.Widget) +OverlayWidget.ATTRS{ + overlay_onupdate_max_freq_seconds=5, +} + return _ENV diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp index 0c63e53e8..ad7a6ab53 100644 --- a/plugins/overlay.cpp +++ b/plugins/overlay.cpp @@ -88,6 +88,8 @@ #include "PluginManager.h" #include "VTableInterpose.h" +#include "modules/Screen.h" + using namespace DFHack; DFHACK_PLUGIN("overlay"); @@ -98,18 +100,76 @@ namespace DFHack { DBG_DECLARE(overlay, event, DebugCategory::LINFO); } +static df::coord2d screenSize; + +template +static void call_overlay_lua(const char *fn_name, int nargs, int nres, + FA && args_lambda, + FR && res_lambda) { + DEBUG(event).print("calling overlay lua function: '%s'\n", fn_name); + + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + color_ostream &out = Core::getInstance().getConsole(); + + if (!lua_checkstack(L, 1 + nargs) || + !Lua::PushModulePublic( + out, L, "plugins.overlay", fn_name)) { + out.printerr("Failed to load overlay Lua code\n"); + return; + } + + std::forward(args_lambda)(L); + + if (!Lua::SafeCall(out, L, nargs, nres)) + out.printerr("Failed Lua call to '%s'\n", fn_name); + + std::forward(res_lambda)(L); +} + +static auto DEFAULT_LAMBDA = [](lua_State *){}; +template +static void call_overlay_lua(const char *fn_name, int nargs, int nres, + FA && args_lambda) { + call_overlay_lua(fn_name, nargs, nres, args_lambda, DEFAULT_LAMBDA); +} + +static void call_overlay_lua(const char *fn_name) { + call_overlay_lua(fn_name, 0, 0, DEFAULT_LAMBDA, DEFAULT_LAMBDA); +} + template struct viewscreen_overlay : T { typedef T interpose_base; DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { INTERPOSE_NEXT(logic)(); + call_overlay_lua("update_viewscreen_widgets", 2, 0, [&](lua_State *L) { + Lua::Push(L, T::_identity.getName()); + Lua::Push(L, this); + }); } DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { - INTERPOSE_NEXT(feed)(input); + bool input_is_handled = false; + call_overlay_lua("feed_viewscreen_widgets", 2, 1, + [&](lua_State *L) { + Lua::Push(L, T::_identity.getName()); + Lua::PushInterfaceKeys(L, *input); + }, [&](lua_State *L) { + input_is_handled = lua_toboolean(L, -1); + }); + if (!input_is_handled) + INTERPOSE_NEXT(feed)(input); } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { INTERPOSE_NEXT(render)(); + call_overlay_lua("render_viewscreen_widgets", 2, 0, [&](lua_State *L) { + Lua::Push(L, T::_identity.getName()); + Lua::Push(L, this); + }); } }; @@ -320,6 +380,9 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector Date: Mon, 31 Oct 2022 13:32:45 -0700 Subject: [PATCH 046/161] load overlay widget configuration --- plugins/lua/overlay.lua | 88 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 90852f9fe..1313ba380 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -1,17 +1,105 @@ local _ENV = mkmodule('plugins.overlay') +local json = require('json') +local utils = require('utils') local widgets = require('gui.widgets') +local WIDGETS_ENABLED_FILE = 'dfhack-config/overlay/widgets.json' +local WIDGETS_STATE_DIR = 'dfhack-config/overlay/widgets/' + local widget_db = {} -- map of widget name to state local active_hotspot_widgets = {} -- map of widget names to the db entry local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db local active_triggered_screen = nil +local function instantiate_widget(name, config) + local provider = config.provider + local ok, provider_env = pcall(require, provider) + if not ok then + ok, provider_env = pcall(require, 'plugins.'..provider) + end + if not ok then + ok, provider_env = pcall(reqscript, provider) + end + if not ok then + dfhack.printerr( + ('error loading overlay widget "%s": could not find provider' .. + ' environment "%s"') + :format(name, provider)) + return nil + end + + local classname = config.class + if not provider_env[classname] then + dfhack.printerr( + ('error loading overlay widget "%s": could not find class "%s"') + :format(name, classname)) + return nil + end + + local frame = {} + local pos = utils.assign({x=-1, y=20}, config.pos or {}) + if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end + if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end + + return provider_env[classname]{frame=frame} +end + +local function normalize_list(element_or_list) + if type(element_or_list) == 'table' then return element_or_list end + return {element_or_list} +end + +-- allow "short form" to be specified, but use "long form" +local function normalize_viewscreen_name(vs_name) + if vs_name:match('viewscreen_.*st') then return vs_name end + return 'viewscreen_' .. vs_name .. 'st' +end + +local function load_widget(name, config, enabled) + local widget = instantiate_widget(name, config) + if not widget then return end + local db_entry = { + widget=widget, + next_update_ms=widget.overlay_onupdate and 0 or math.huge, + } + widget_db[name] = db_entry + if not enabled then return end + if config.hotspot then + active_hotspot_widgets[name] = db_entry + end + for vs_name in ipairs(normalize_list(config.viewscreens)) do + vs_name = normalize_viewscreen_name(vs_name) + ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry + end +end + +local function load_config(fname) + local ok, config = pcall(json.decode_file, fname) + return ok and config or {} +end + function reload() widget_db = {} active_hotspot_widgets = {} active_viewscreen_widgets = {} active_triggered_screen = nil + + local enabled_map = load_config(WIDGETS_ENABLED_FILE) + for _,fname in ipairs(dfhack.filesystem.listdir(WIDGETS_STATE_DIR)) do + local _,_,name = fname:find('^(.*)%.json$') + if not name then goto continue end + local widget_config = load_config(WIDGETS_STATE_DIR..fname) + if not widget_config.provider or not widget_config.class then + dfhack.printerr( + ('error loading overlay widget "%s": "provider" and' .. + ' "class" must be specified in %s%s') + :format(name, WIDGETS_STATE_DIR, fname)) + goto continue + end + load_widget(name, widget_config, not not enabled_map[name]) + ::continue:: + end end -- reduces the next call by a small random amount to introduce jitter into the From 6e6e174c312ef4665688ffaeed7b91fb68a28f2c Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 2 Nov 2022 12:33:06 -0700 Subject: [PATCH 047/161] implement CLI interface --- plugins/lua/overlay.lua | 249 ++++++++++++++++++++++++++++++++++------ plugins/overlay.cpp | 67 +++++------ 2 files changed, 245 insertions(+), 71 deletions(-) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 1313ba380..cb499eaa9 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -1,5 +1,6 @@ local _ENV = mkmodule('plugins.overlay') +local gui = require('gui') local json = require('json') local utils = require('utils') local widgets = require('gui.widgets') @@ -8,10 +9,179 @@ local WIDGETS_ENABLED_FILE = 'dfhack-config/overlay/widgets.json' local WIDGETS_STATE_DIR = 'dfhack-config/overlay/widgets/' local widget_db = {} -- map of widget name to state +local widget_index = {} -- list of widget names local active_hotspot_widgets = {} -- map of widget names to the db entry local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db local active_triggered_screen = nil +local function load_config(path) + local ok, config = safecall(json.decode_file, path) + return ok and config or {} +end + +local function save_config(data, path) + if not safecall(json.encode_file, data, path) then + dfhack.printerr(('failed to save overlay config file: "%s"') + :format(path)) + end +end + +local function normalize_list(element_or_list) + if type(element_or_list) == 'table' then return element_or_list end + return {element_or_list} +end + +-- allow "short form" to be specified, but use "long form" +local function normalize_viewscreen_name(vs_name) + if vs_name:match('viewscreen_.*st') then return vs_name end + return 'viewscreen_' .. vs_name .. 'st' +end + +local function is_empty(tbl) + for _ in pairs(tbl) do + return false + end + return true +end + +local function get_name(name_or_number) + local num = tonumber(name_or_number) + if num and widget_index[num] then + return widget_index[num] + end + return tostring(name_or_number) +end + +local function do_by_names_or_numbers(args, fn) + for _,name_or_number in ipairs(normalize_list(args)) do + local name = get_name(name_or_number) + local db_entry = widget_db[name] + if not db_entry then + dfhack.printerr(('widget not found: "%s"'):format(name)) + else + fn(name, db_entry) + end + end +end + +local function save_enabled() + local enabled_map = {} + for name,db_entry in pairs(widget_db) do + enabled_map[name] = db_entry.enabled + end + save_config(enabled_map, WIDGETS_ENABLED_FILE) +end + +local function do_enable(args) + do_by_names_or_numbers(args, function(name, db_entry) + db_entry.enabled = true + if db_entry.config.hotspot then + active_hotspot_widgets[name] = db_entry + end + for _,vs_name in ipairs(normalize_list(db_entry.config.viewscreens)) do + vs_name = normalize_viewscreen_name(vs_name) + ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry + end + end) + save_enabled() +end + +local function do_disable(args) + do_by_names_or_numbers(args, function(name, db_entry) + db_entry.enabled = false + if db_entry.config.hotspot then + active_hotspot_widgets[name] = nil + end + for _,vs_name in ipairs(normalize_list(db_entry.config.viewscreens)) do + vs_name = normalize_viewscreen_name(vs_name) + ensure_key(active_viewscreen_widgets, vs_name)[name] = nil + if is_empty(active_viewscreen_widgets[vs_name]) then + active_viewscreen_widgets[vs_name] = nil + end + end + end) + save_enabled() +end + +local function do_list(args) + local filter = args and #args > 0 + for i,name in ipairs(widget_index) do + if filter then + local passes = false + for _,str in ipairs(args) do + if name:find(str) then + passes = true + break + end + end + if not passes then goto continue end + end + local enabled = widget_db[name].enabled + dfhack.color(enabled and COLOR_YELLOW or COLOR_LIGHTGREEN) + dfhack.print(enabled and '[enabled] ' or '[disabled]') + dfhack.color() + print((' %d) %s%s'):format(i, name, + widget_db[name].widget.overlay_trigger and ' (can trigger)' or '')) + ::continue:: + end +end + +local function make_frame(config, old_frame) + local old_frame, frame = old_frame or {}, {} + frame.w, frame.h = old_frame.w, old_frame.h + local pos = utils.assign({x=-1, y=20}, config.pos or {}) + -- if someone accidentally uses 1-based instead of 0-based indexing, fix it + if pos.x == 0 then pos.x = 1 end + if pos.y == 0 then pos.y = 1 end + if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end + if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end + return frame +end + +local function get_screen_rect() + local w, h = dfhack.screen.getWindowSize() + return gui.ViewRect{rect=gui.mkdims_wh(0, 0, w, h)} +end + +local function do_reposition(args) + local name_or_number, x, y = table.unpack(args) + local name = get_name(name_or_number) + local db_entry = widget_db[name] + local config = db_entry.config + config.pos.x, config.pos.y = tonumber(x), tonumber(y) + db_entry.widget.frame = make_frame(config, db_entry.widget.frame) + db_entry.widget:updateLayout(get_screen_rect()) + save_config(config, WIDGETS_STATE_DIR .. name .. '.json') +end + +local function do_trigger(args) + local target = args[1] + do_by_names_or_numbers(target, function(name, db_entry) + if db_entry.widget.overlay_trigger then + db_entry.widget:overlay_trigger() + end + end) +end + +local command_fns = { + enable=do_enable, + disable=do_disable, + list=do_list, + reload=function() reload() end, + reposition=do_reposition, + trigger=do_trigger, +} + +local HELP_ARGS = utils.invert{'help', '--help', '-h'} + +function overlay_command(args) + local command = table.remove(args, 1) or 'help' + if HELP_ARGS[command] or not command_fns[command] then return false end + + command_fns[command](args) + return true +end + local function instantiate_widget(name, config) local provider = config.provider local ok, provider_env = pcall(require, provider) @@ -37,50 +207,25 @@ local function instantiate_widget(name, config) return nil end - local frame = {} - local pos = utils.assign({x=-1, y=20}, config.pos or {}) - if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end - if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end - - return provider_env[classname]{frame=frame} -end - -local function normalize_list(element_or_list) - if type(element_or_list) == 'table' then return element_or_list end - return {element_or_list} -end - --- allow "short form" to be specified, but use "long form" -local function normalize_viewscreen_name(vs_name) - if vs_name:match('viewscreen_.*st') then return vs_name end - return 'viewscreen_' .. vs_name .. 'st' + return provider_env[classname]{frame=make_frame(config)} end local function load_widget(name, config, enabled) local widget = instantiate_widget(name, config) if not widget then return end local db_entry = { + enabled=enabled, + config=config, widget=widget, next_update_ms=widget.overlay_onupdate and 0 or math.huge, } widget_db[name] = db_entry - if not enabled then return end - if config.hotspot then - active_hotspot_widgets[name] = db_entry - end - for vs_name in ipairs(normalize_list(config.viewscreens)) do - vs_name = normalize_viewscreen_name(vs_name) - ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry - end -end - -local function load_config(fname) - local ok, config = pcall(json.decode_file, fname) - return ok and config or {} + if enabled then do_enable(name) end end function reload() widget_db = {} + widget_index = {} active_hotspot_widgets = {} active_viewscreen_widgets = {} active_triggered_screen = nil @@ -100,6 +245,23 @@ function reload() load_widget(name, widget_config, not not enabled_map[name]) ::continue:: end + + for name in pairs(widget_db) do + table.insert(widget_index, name) + end + table.sort(widget_index) + + reposition_widgets() +end + +local function detect_frame_change(widget, fn) + local frame = widget.frame + local w, h = frame.w, frame.h + local ret = fn() + if w ~= frame.w or h ~= frame.h then + widget:updateLayout() + end + return ret end -- reduces the next call by a small random amount to introduce jitter into the @@ -108,9 +270,10 @@ local function do_update(db_entry, now_ms, vs) if db_entry.next_update_ms > now_ms then return end local w = db_entry.widget local freq_ms = w.overlay_onupdate_max_freq_seconds * 1000 - local jitter = math.rand(0, freq_ms // 8) -- up to ~12% jitter + local jitter = math.random(0, freq_ms // 8) -- up to ~12% jitter db_entry.next_update_ms = now_ms + freq_ms - jitter - if w:overlay_onupdate(vs) then + if detect_frame_change(w, + function() return w:overlay_onupdate(vs) end) then active_triggered_screen = w:overlay_trigger() if active_triggered_screen then return true end end @@ -140,7 +303,11 @@ function feed_viewscreen_widgets(vs_name, keys) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return false end for _,db_entry in pairs(vs_widgets) do - if db_entry.widget:onInput(keys) then return true end + local widget = db_entry.widget + if detect_frame_change(widget, + function() return widget:onInput(keys) end) then + return true + end end return false end @@ -148,18 +315,18 @@ end function render_viewscreen_widgets(vs_name) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return false end - local dc = Painter.new() + local dc = gui.Painter.new() for _,db_entry in pairs(vs_widgets) do - db_entry.widget:render(dc) + local widget = db_entry.widget + detect_frame_change(widget, function() widget:render(dc) end) end end -- called when the DF window is resized function reposition_widgets() - local w, h = dscreen.getWindowSize() - local vr = ViewRect{rect=mkdims_wh(0, 0, w, h)} + local sr = get_screen_rect() for _,db_entry in pairs(widget_db) do - db_entry.widget:updateLayout(vr) + db_entry.widget:updateLayout(sr) end end @@ -168,4 +335,10 @@ OverlayWidget.ATTRS{ overlay_onupdate_max_freq_seconds=5, } +function OverlayWidget:preinit(info) + info.frame = info.frame or {} + info.frame.w = info.frame.w or 5 + info.frame.h = info.frame.h or 1 +end + return _ENV diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp index ad7a6ab53..c6cc0ef20 100644 --- a/plugins/overlay.cpp +++ b/plugins/overlay.cpp @@ -103,9 +103,8 @@ namespace DFHack { static df::coord2d screenSize; template -static void call_overlay_lua(const char *fn_name, int nargs, int nres, - FA && args_lambda, - FR && res_lambda) { +static void call_overlay_lua(color_ostream *out, const char *fn_name, int nargs, + int nres, FA && args_lambda, FR && res_lambda) { DEBUG(event).print("calling overlay lua function: '%s'\n", fn_name); CoreSuspender guard; @@ -113,32 +112,33 @@ static void call_overlay_lua(const char *fn_name, int nargs, int nres, auto L = Lua::Core::State; Lua::StackUnwinder top(L); - color_ostream &out = Core::getInstance().getConsole(); + if (!out) + out = &Core::getInstance().getConsole(); if (!lua_checkstack(L, 1 + nargs) || !Lua::PushModulePublic( - out, L, "plugins.overlay", fn_name)) { - out.printerr("Failed to load overlay Lua code\n"); + *out, L, "plugins.overlay", fn_name)) { + out->printerr("Failed to load overlay Lua code\n"); return; } std::forward(args_lambda)(L); - if (!Lua::SafeCall(out, L, nargs, nres)) - out.printerr("Failed Lua call to '%s'\n", fn_name); + if (!Lua::SafeCall(*out, L, nargs, nres)) + out->printerr("Failed Lua call to '%s'\n", fn_name); std::forward(res_lambda)(L); } static auto DEFAULT_LAMBDA = [](lua_State *){}; template -static void call_overlay_lua(const char *fn_name, int nargs, int nres, - FA && args_lambda) { - call_overlay_lua(fn_name, nargs, nres, args_lambda, DEFAULT_LAMBDA); +static void call_overlay_lua(color_ostream *out, const char *fn_name, int nargs, + int nres, FA && args_lambda) { + call_overlay_lua(out, fn_name, nargs, nres, args_lambda, DEFAULT_LAMBDA); } -static void call_overlay_lua(const char *fn_name) { - call_overlay_lua(fn_name, 0, 0, DEFAULT_LAMBDA, DEFAULT_LAMBDA); +static void call_overlay_lua(color_ostream *out, const char *fn_name) { + call_overlay_lua(out, fn_name, 0, 0, DEFAULT_LAMBDA, DEFAULT_LAMBDA); } template @@ -147,14 +147,15 @@ struct viewscreen_overlay : T { DEFINE_VMETHOD_INTERPOSE(void, logic, ()) { INTERPOSE_NEXT(logic)(); - call_overlay_lua("update_viewscreen_widgets", 2, 0, [&](lua_State *L) { - Lua::Push(L, T::_identity.getName()); - Lua::Push(L, this); - }); + call_overlay_lua(NULL, "update_viewscreen_widgets", 2, 0, + [&](lua_State *L) { + Lua::Push(L, T::_identity.getName()); + Lua::Push(L, this); + }); } DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set *input)) { bool input_is_handled = false; - call_overlay_lua("feed_viewscreen_widgets", 2, 1, + call_overlay_lua(NULL, "feed_viewscreen_widgets", 2, 1, [&](lua_State *L) { Lua::Push(L, T::_identity.getName()); Lua::PushInterfaceKeys(L, *input); @@ -166,10 +167,11 @@ struct viewscreen_overlay : T { } DEFINE_VMETHOD_INTERPOSE(void, render, ()) { INTERPOSE_NEXT(render)(); - call_overlay_lua("render_viewscreen_widgets", 2, 0, [&](lua_State *L) { - Lua::Push(L, T::_identity.getName()); - Lua::Push(L, this); - }); + call_overlay_lua(NULL, "render_viewscreen_widgets", 2, 0, + [&](lua_State *L) { + Lua::Push(L, T::_identity.getName()); + Lua::Push(L, this); + }); } }; @@ -362,15 +364,14 @@ DFhackCExport command_result plugin_enable(color_ostream &, bool enable) { #undef INTERPOSE_HOOKS_FAILED static command_result overlay_cmd(color_ostream &out, std::vector & parameters) { - if (DBG_NAME(control).isEnabled(DebugCategory::LDEBUG)) { - DEBUG(control).print("interpreting command with %zu parameters:\n", - parameters.size()); - for (auto ¶m : parameters) { - DEBUG(control).print(" %s\n", param.c_str()); - } - } + bool show_help = false; + call_overlay_lua(&out, "overlay_command", 1, 1, [&](lua_State *L) { + Lua::PushVector(L, parameters); + }, [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + }); - return CR_OK; + return show_help ? CR_WRONG_USAGE : CR_OK; } DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { @@ -381,7 +382,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector Date: Wed, 2 Nov 2022 12:39:13 -0700 Subject: [PATCH 048/161] add stub default widget enabled config --- dfhack-config/overlay/widgets.json | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 dfhack-config/overlay/widgets.json diff --git a/dfhack-config/overlay/widgets.json b/dfhack-config/overlay/widgets.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/dfhack-config/overlay/widgets.json @@ -0,0 +1,2 @@ +{ +} From 94c6bc8063b8d69c9ebd9014aaade9a8b7dc788a Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 2 Nov 2022 13:50:52 -0700 Subject: [PATCH 049/161] refactor generic lua function caller to Lua ns --- library/LuaTools.cpp | 23 +++++++++++++++++++++++ library/include/LuaTools.h | 15 +++++++++++++++ plugins/overlay.cpp | 34 +++++++--------------------------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 3cd6a1f7d..8d317c076 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -820,6 +820,29 @@ bool DFHack::Lua::SafeCall(color_ostream &out, lua_State *L, int nargs, int nres return ok; } +bool DFHack::Lua::CallLuaModuleFunction(color_ostream &out, lua_State *L, + const char *module_name, const char *fn_name, + int nargs, int nres, LuaLambda && args_lambda, LuaLambda && res_lambda, + bool perr){ + if (!lua_checkstack(L, 1 + nargs) || + !Lua::PushModulePublic(out, L, module_name, fn_name)) { + if (perr) + out.printerr("Failed to load %s Lua code\n", module_name); + return false; + } + + std::forward(args_lambda)(L); + + if (!Lua::SafeCall(out, L, nargs, nres, perr)) { + if (perr) + out.printerr("Failed Lua call to '%s.%s'\n", module_name, fn_name); + return false; + } + + std::forward(res_lambda)(L); + return true; +} + // Copied from lcorolib.c, with error handling modifications static int resume_helper(lua_State *L, lua_State *co, int narg, int nres) { diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index e4245f09a..9e1901f03 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -24,6 +24,7 @@ distribution. #pragma once +#include #include #include #include @@ -218,6 +219,20 @@ namespace DFHack {namespace Lua { */ DFHACK_EXPORT bool SafeCall(color_ostream &out, lua_State *state, int nargs, int nres, bool perr = true); + /** + * Load named module and function and invoke it via SafeCall. Returns true + * on success. If an error is signalled, and perr is true, it is printed and + * popped from the stack. + */ + typedef std::function LuaLambda; + static auto DEFAULT_LUA_LAMBDA = [](lua_State *){}; + DFHACK_EXPORT bool CallLuaModuleFunction(color_ostream &out, + lua_State *state, const char *module_name, const char *fn_name, + int nargs = 0, int nres = 0, + LuaLambda && args_lambda = DEFAULT_LUA_LAMBDA, + LuaLambda && res_lambda = DEFAULT_LUA_LAMBDA, + bool perr = true); + /** * Pops a function from the top of the stack, and pushes a new coroutine. */ diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp index c6cc0ef20..509fa5597 100644 --- a/plugins/overlay.cpp +++ b/plugins/overlay.cpp @@ -102,9 +102,10 @@ namespace DFHack { static df::coord2d screenSize; -template -static void call_overlay_lua(color_ostream *out, const char *fn_name, int nargs, - int nres, FA && args_lambda, FR && res_lambda) { +static void call_overlay_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { DEBUG(event).print("calling overlay lua function: '%s'\n", fn_name); CoreSuspender guard; @@ -115,30 +116,9 @@ static void call_overlay_lua(color_ostream *out, const char *fn_name, int nargs, if (!out) out = &Core::getInstance().getConsole(); - if (!lua_checkstack(L, 1 + nargs) || - !Lua::PushModulePublic( - *out, L, "plugins.overlay", fn_name)) { - out->printerr("Failed to load overlay Lua code\n"); - return; - } - - std::forward(args_lambda)(L); - - if (!Lua::SafeCall(*out, L, nargs, nres)) - out->printerr("Failed Lua call to '%s'\n", fn_name); - - std::forward(res_lambda)(L); -} - -static auto DEFAULT_LAMBDA = [](lua_State *){}; -template -static void call_overlay_lua(color_ostream *out, const char *fn_name, int nargs, - int nres, FA && args_lambda) { - call_overlay_lua(out, fn_name, nargs, nres, args_lambda, DEFAULT_LAMBDA); -} - -static void call_overlay_lua(color_ostream *out, const char *fn_name) { - call_overlay_lua(out, fn_name, 0, 0, DEFAULT_LAMBDA, DEFAULT_LAMBDA); + Lua::CallLuaModuleFunction(*out, L, "plugins.overlay", fn_name, nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); } template From 1ebf58ff8352c45429fd2d64d04734c01946e77b Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 4 Nov 2022 12:57:05 -0700 Subject: [PATCH 050/161] rewrite conf management according to discord convo move non-user-editable state into the widget config remove registry json files all user-editable state is now in a single overlay.json file --- dfhack-config/overlay/widgets.json | 2 - plugins/lua/overlay.lua | 332 +++++++++++++++++------------ 2 files changed, 199 insertions(+), 135 deletions(-) delete mode 100644 dfhack-config/overlay/widgets.json diff --git a/dfhack-config/overlay/widgets.json b/dfhack-config/overlay/widgets.json deleted file mode 100644 index 2c63c0851..000000000 --- a/dfhack-config/overlay/widgets.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index cb499eaa9..e23caa3e7 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -5,27 +5,56 @@ local json = require('json') local utils = require('utils') local widgets = require('gui.widgets') -local WIDGETS_ENABLED_FILE = 'dfhack-config/overlay/widgets.json' -local WIDGETS_STATE_DIR = 'dfhack-config/overlay/widgets/' +local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json' +local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS' -local widget_db = {} -- map of widget name to state -local widget_index = {} -- list of widget names +local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2 + +-- ---------------- -- +-- state and config -- +-- ---------------- -- + +local active_triggered_screen = nil -- if non-nil, hotspots will not get updates +local widget_db = {} -- map of widget name to ephermeral state +local widget_index = {} -- ordered list of widget names +local overlay_config = {} -- map of widget name to persisted state local active_hotspot_widgets = {} -- map of widget names to the db entry local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db -local active_triggered_screen = nil -local function load_config(path) - local ok, config = safecall(json.decode_file, path) - return ok and config or {} +local function reset() + if active_triggered_screen then + active_triggered_screen:dismiss() + end + active_triggered_screen = nil + + widget_db = {} + widget_index = {} + + local ok, config = pcall(json.decode_file, OVERLAY_CONFIG_FILE) + overlay_config = ok and config or {} + + active_hotspot_widgets = {} + active_viewscreen_widgets = {} end -local function save_config(data, path) - if not safecall(json.encode_file, data, path) then +local function save_config() + if not safecall(json.encode_file, overlay_config, OVERLAY_CONFIG_FILE) then dfhack.printerr(('failed to save overlay config file: "%s"') :format(path)) end end +local function triggered_screen_has_lock() + if not active_triggered_screen then return false end + if active_triggered_screen:isActive() then return true end + active_triggered_screen = nil + return false +end + +-- ----------- -- +-- utility fns -- +-- ----------- -- + local function normalize_list(element_or_list) if type(element_or_list) == 'table' then return element_or_list end return {element_or_list} @@ -44,6 +73,32 @@ local function is_empty(tbl) return true end +local function sanitize_pos(pos) + local x = math.floor(tonumber(pos.x) or DEFAULT_X_POS) + local y = math.floor(tonumber(pos.y) or DEFAULT_Y_POS) + -- if someone accidentally uses 1-based instead of 0-based indexing, fix it + if x == 0 then x = 1 end + if y == 0 then y = 1 end + return {x=x, y=y} +end + +local function make_frame(pos, old_frame) + old_frame = old_frame or {} + local frame = {w=old_frame.w, h=old_frame.h} + if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end + if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end + return frame +end + +local function get_screen_rect() + local w, h = dfhack.screen.getWindowSize() + return gui.ViewRect{rect=gui.mkdims_wh(0, 0, w, h)} +end + +-- ------------- -- +-- CLI functions -- +-- ------------- -- + local function get_name(name_or_number) local num = tonumber(name_or_number) if num and widget_index[num] then @@ -64,47 +119,46 @@ local function do_by_names_or_numbers(args, fn) end end -local function save_enabled() - local enabled_map = {} - for name,db_entry in pairs(widget_db) do - enabled_map[name] = db_entry.enabled - end - save_config(enabled_map, WIDGETS_ENABLED_FILE) -end - -local function do_enable(args) +local function do_enable(args, quiet, skip_save) do_by_names_or_numbers(args, function(name, db_entry) - db_entry.enabled = true - if db_entry.config.hotspot then + overlay_config[name].enabled = true + if db_entry.widget.hotspot then active_hotspot_widgets[name] = db_entry end - for _,vs_name in ipairs(normalize_list(db_entry.config.viewscreens)) do + for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do vs_name = normalize_viewscreen_name(vs_name) ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry end + if not quiet then + print(('enabled widget %s'):format(name)) + end end) - save_enabled() + if not skip_save then + save_config() + end end local function do_disable(args) do_by_names_or_numbers(args, function(name, db_entry) - db_entry.enabled = false - if db_entry.config.hotspot then + overlay_config[name].enabled = false + if db_entry.widget.hotspot then active_hotspot_widgets[name] = nil end - for _,vs_name in ipairs(normalize_list(db_entry.config.viewscreens)) do + for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do vs_name = normalize_viewscreen_name(vs_name) ensure_key(active_viewscreen_widgets, vs_name)[name] = nil if is_empty(active_viewscreen_widgets[vs_name]) then active_viewscreen_widgets[vs_name] = nil end end + print(('disabled widget %s'):format(name)) end) - save_enabled() + save_config() end local function do_list(args) local filter = args and #args > 0 + local num_filtered = 0 for i,name in ipairs(widget_index) do if filter then local passes = false @@ -114,51 +168,125 @@ local function do_list(args) break end end - if not passes then goto continue end + if not passes then + num_filtered = num_filtered + 1 + goto continue + end end - local enabled = widget_db[name].enabled + local db_entry = widget_db[name] + local enabled = overlay_config[name].enabled dfhack.color(enabled and COLOR_YELLOW or COLOR_LIGHTGREEN) dfhack.print(enabled and '[enabled] ' or '[disabled]') dfhack.color() print((' %d) %s%s'):format(i, name, - widget_db[name].widget.overlay_trigger and ' (can trigger)' or '')) + db_entry.widget.overlay_trigger and ' (can trigger)' or '')) ::continue:: end + if num_filtered > 0 then + print(('(%d widgets filtered out)'):format(num_filtered)) + end end -local function make_frame(config, old_frame) - local old_frame, frame = old_frame or {}, {} - frame.w, frame.h = old_frame.w, old_frame.h - local pos = utils.assign({x=-1, y=20}, config.pos or {}) - -- if someone accidentally uses 1-based instead of 0-based indexing, fix it - if pos.x == 0 then pos.x = 1 end - if pos.y == 0 then pos.y = 1 end - if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end - if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end - return frame +local function load_widget(name, widget_class) + local widget = widget_class{name=name} + widget_db[name] = { + widget=widget, + next_update_ms=widget.overlay_onupdate and 0 or math.huge, + } + if not overlay_config[name] then overlay_config[name] = {} end + local config = overlay_config[name] + if not config.pos then + config.pos = sanitize_pos(widget.default_pos) + end + widget.frame = make_frame(config.pos, widget.frame) + if config.enabled then + do_enable(name, true, true) + else + config.enabled = false + end end -local function get_screen_rect() - local w, h = dfhack.screen.getWindowSize() - return gui.ViewRect{rect=gui.mkdims_wh(0, 0, w, h)} +local function load_widgets(env_prefix, provider, env_fn) + local env_name = env_prefix .. provider + local ok, provider_env = pcall(env_fn, env_name) + if not ok or not provider_env[OVERLAY_WIDGETS_VAR] then return end + local overlay_widgets = provider_env[OVERLAY_WIDGETS_VAR] + if type(overlay_widgets) ~= 'table' then + dfhack.printerr( + ('error loading overlay widgets from "%s": %s map is malformed') + :format(env_name, OVERLAY_WIDGETS_VAR)) + return + end + for widget_name,widget_class in pairs(overlay_widgets) do + local name = provider .. '.' .. widget_name + if not safecall(load_widget, name, widget_class) then + dfhack.printerr(('error loading overlay widget "%s"'):format(name)) + end + end +end + +-- also called directly from cpp on init +function reload() + reset() + + for _,plugin in ipairs(dfhack.internal.listPlugins()) do + load_widgets('plugins.', plugin, require) + end + for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do + local files = dfhack.filesystem.listdir_recursive( + script_path, nil, false) + if not files then goto skip_path end + for _,f in ipairs(files) do + if not f.isdir and + f.path:endswith('.lua') and + not f.path:startswith('test/') and + not f.path:startswith('internal/') then + local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua' + load_widgets('', script_name, reqscript) + end + end + ::skip_path:: + end + + for name in pairs(widget_db) do + table.insert(widget_index, name) + end + table.sort(widget_index) + + reposition_widgets() +end + +local function do_reload() + reload() + print('reloaded overlay configuration') end local function do_reposition(args) local name_or_number, x, y = table.unpack(args) local name = get_name(name_or_number) - local db_entry = widget_db[name] - local config = db_entry.config - config.pos.x, config.pos.y = tonumber(x), tonumber(y) - db_entry.widget.frame = make_frame(config, db_entry.widget.frame) - db_entry.widget:updateLayout(get_screen_rect()) - save_config(config, WIDGETS_STATE_DIR .. name .. '.json') + -- TODO: check existence of widget, validate numbers, warn if offscreen + local pos = sanitize_pos{x=tonumber(x), y=tonumber(y)} + overlay_config[name].pos = pos + local widget = widget_db[name].widget + widget.frame = make_frame(pos, widget.frame) + widget:updateLayout(get_screen_rect()) + save_config() + print(('repositioned widget %s to x=%d, y=%d'):format(name, pos.x, pos.y)) end +-- note that the widget does not have to be enabled to be triggered local function do_trigger(args) + if triggered_screen_has_lock() then + dfhack.printerr( + 'cannot trigger widget; another widget is already active') + return + end local target = args[1] do_by_names_or_numbers(target, function(name, db_entry) - if db_entry.widget.overlay_trigger then - db_entry.widget:overlay_trigger() + local widget = db_entry.widget + if widget.overlay_trigger then + widget:overlay_trigger() + print(('triggered widget %s'):format(name)) end end) end @@ -167,7 +295,7 @@ local command_fns = { enable=do_enable, disable=do_disable, list=do_list, - reload=function() reload() end, + reload=do_reload, reposition=do_reposition, trigger=do_trigger, } @@ -177,82 +305,13 @@ local HELP_ARGS = utils.invert{'help', '--help', '-h'} function overlay_command(args) local command = table.remove(args, 1) or 'help' if HELP_ARGS[command] or not command_fns[command] then return false end - command_fns[command](args) return true end -local function instantiate_widget(name, config) - local provider = config.provider - local ok, provider_env = pcall(require, provider) - if not ok then - ok, provider_env = pcall(require, 'plugins.'..provider) - end - if not ok then - ok, provider_env = pcall(reqscript, provider) - end - if not ok then - dfhack.printerr( - ('error loading overlay widget "%s": could not find provider' .. - ' environment "%s"') - :format(name, provider)) - return nil - end - - local classname = config.class - if not provider_env[classname] then - dfhack.printerr( - ('error loading overlay widget "%s": could not find class "%s"') - :format(name, classname)) - return nil - end - - return provider_env[classname]{frame=make_frame(config)} -end - -local function load_widget(name, config, enabled) - local widget = instantiate_widget(name, config) - if not widget then return end - local db_entry = { - enabled=enabled, - config=config, - widget=widget, - next_update_ms=widget.overlay_onupdate and 0 or math.huge, - } - widget_db[name] = db_entry - if enabled then do_enable(name) end -end - -function reload() - widget_db = {} - widget_index = {} - active_hotspot_widgets = {} - active_viewscreen_widgets = {} - active_triggered_screen = nil - - local enabled_map = load_config(WIDGETS_ENABLED_FILE) - for _,fname in ipairs(dfhack.filesystem.listdir(WIDGETS_STATE_DIR)) do - local _,_,name = fname:find('^(.*)%.json$') - if not name then goto continue end - local widget_config = load_config(WIDGETS_STATE_DIR..fname) - if not widget_config.provider or not widget_config.class then - dfhack.printerr( - ('error loading overlay widget "%s": "provider" and' .. - ' "class" must be specified in %s%s') - :format(name, WIDGETS_STATE_DIR, fname)) - goto continue - end - load_widget(name, widget_config, not not enabled_map[name]) - ::continue:: - end - - for name in pairs(widget_db) do - table.insert(widget_index, name) - end - table.sort(widget_index) - - reposition_widgets() -end +-- ---------------- -- +-- event management -- +-- ---------------- -- local function detect_frame_change(widget, fn) local frame = widget.frame @@ -280,10 +339,7 @@ local function do_update(db_entry, now_ms, vs) end function update_hotspot_widgets() - if active_triggered_screen then - if active_triggered_screen:isActive() then return end - active_triggered_screen = nil - end + if triggered_screen_has_lock() then return end local now_ms = dfhack.getTickCount() for _,db_entry in pairs(active_hotspot_widgets) do if do_update(db_entry, now_ms) then return end @@ -330,15 +386,25 @@ function reposition_widgets() end end +-- ------------------------------------------------- -- +-- OverlayWidget (base class of all overlay widgets) -- +-- ------------------------------------------------- -- + OverlayWidget = defclass(OverlayWidget, widgets.Widget) OverlayWidget.ATTRS{ - overlay_onupdate_max_freq_seconds=5, + name=DEFAULT_NIL, -- this is set by the framework to the widget name + default_pos={x=DEFAULT_X_POS, y=DEFAULT_Y_POS}, -- initial widget screen pos, 1-based + hotspot=false, -- whether to call overlay_onupdate for all screens + viewscreens={}, -- override with list of viewscrens to interpose + overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate } -function OverlayWidget:preinit(info) - info.frame = info.frame or {} - info.frame.w = info.frame.w or 5 - info.frame.h = info.frame.h or 1 +-- set defaults for frame. the widget is expected to keep these up to date as +-- display contents change. +function OverlayWidget:init() + self.frame = self.frame or {} + self.frame.w = self.frame.w or 5 + self.frame.h = self.frame.h or 1 end return _ENV From 6e0a583e8d933044707b6711bc06b07a91d44c6e Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 15:29:13 -0800 Subject: [PATCH 051/161] enable overlay from init scripts; self-enable bad loading other plugin modules during plugin_init results in those other plugin modules not getting initialized properly. specifically, their DFHACK_PLUGIN_LUA_FUNCTIONS and commands don't get added to their lua module namespaces. --- data/init/dfhack.tools.init | 1 + plugins/overlay.cpp | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/data/init/dfhack.tools.init b/data/init/dfhack.tools.init index aab17ebbe..0887f80ba 100644 --- a/data/init/dfhack.tools.init +++ b/data/init/dfhack.tools.init @@ -89,6 +89,7 @@ enable automaterial # Other interface improvement tools enable \ + overlay \ confirm \ dwarfmonitor \ mousequery \ diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp index 509fa5597..cb19df892 100644 --- a/plugins/overlay.cpp +++ b/plugins/overlay.cpp @@ -249,10 +249,15 @@ IMPLEMENT_HOOKS(workshop_profile) !INTERPOSE_HOOK(screen##_overlay, feed).apply(enable) || \ !INTERPOSE_HOOK(screen##_overlay, render).apply(enable) -DFhackCExport command_result plugin_enable(color_ostream &, bool enable) { +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (is_enabled == enable) return CR_OK; + if (enable) { + screenSize = Screen::getWindowSize(); + call_overlay_lua(&out, "reload"); + } + DEBUG(control).print("%sing interpose hooks\n", enable ? "enabl" : "disabl"); if (INTERPOSE_HOOKS_FAILED(adopt_region) || @@ -361,10 +366,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector Date: Wed, 9 Nov 2022 12:15:51 -0800 Subject: [PATCH 052/161] record explicitly triggered widget screens --- plugins/lua/overlay.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index e23caa3e7..69a32242b 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -285,7 +285,7 @@ local function do_trigger(args) do_by_names_or_numbers(target, function(name, db_entry) local widget = db_entry.widget if widget.overlay_trigger then - widget:overlay_trigger() + active_triggered_screen = widget:overlay_trigger() print(('triggered widget %s'):format(name)) end end) From 9bdc995f2098d11bbc3ef67e610ddaa499b3b35f Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 9 Nov 2022 14:08:38 -0800 Subject: [PATCH 053/161] write docs and make implementation match the docs added ability to enable/disable all added showing current widget configuration added moving widget back to default coordinates --- docs/plugins/overlay.rst | 65 +++++++++++++++++++---- plugins/lua/overlay.lua | 108 +++++++++++++++++++++++++++++---------- 2 files changed, 138 insertions(+), 35 deletions(-) diff --git a/docs/plugins/overlay.rst b/docs/plugins/overlay.rst index 9416fba31..8a63c8f20 100644 --- a/docs/plugins/overlay.rst +++ b/docs/plugins/overlay.rst @@ -2,19 +2,66 @@ overlay ======= .. dfhack-tool:: - :summary: Provide an on-screen clickable DFHack launcher button. + :summary: Manage on-screen overlay widgets. :tags: dfhack interface -This tool places a small button in the lower left corner of the screen that you -can click to run DFHack commands with `gui/launcher`. - -If you would rather always run `gui/launcher` with the hotkeys, or just don't -want the DFHack button on-screen, just disable the plugin with -``disable overlay``. +The overlay framework manages the on-screen widgets that other tools (including +3rd party plugins and scripts) can register for display. If you are a developer +who wants to write an overlay widget, please see the `overlay-widget-guide`. Usage ----- -:: +``enable overlay`` + Display enabled widgets. +``overlay enable|disable all| [ ...]`` + Enable/disable all or specified widgets. Widgets can be specified by either + their name or their number, as returned by ``overlay list``. +``overlay list`` + Show a list of all the widgets that are registered with the overlay + framework. +``overlay position [default| ]`` + Display configuration information for the given widget or change the + position where it is rendered. See the `Widget position`_ section below for + details. +``overlay trigger `` + Intended to be used by keybindings for manually triggering a widget. For + example, you could use an ``overlay trigger`` keybinding to show a menu that + normally appears when you hover the mouse over a screen hotspot. + +Examples +-------- + +``overlay enable all`` + Enable all widgets. Note that they will only be displayed on the screens + that they are associated with. You can see which screens a widget will be + displayed on, along with whether the widget is a hotspot, by calling + ``overlay position``. +``overlay position hotkeys.menu`` + Show the current configuration of the `hotkeys` menu widget. +``overlay position dwarfmonitor.cursor -2 -3`` + Display the `dwarfmonitor` cursor position reporting widget in the lower + right corner of the screen, 2 tiles from the left and 3 tiles from the + bottom. +``overlay position dwarfmonitor.cursor default`` + Reset the `dwarfmonitor` cursor position to its default. +``overlay trigger hotkeys.menu`` + Trigger the `hotkeys` menu widget so that it shows its popup menu. This is + what is run when you hit :kbd:`Ctrl`:kbd:`Shift`:kbd:`C`. + +Widget position +--------------- + +Widgets can be positioned at any (``x``, ``y``) position on the screen, and can +be specified relative to any edge. Coordinates are 1-based, which means that +``1`` is the far left column (for ``x``) or the top row (for ``y``). Negative +numbers are measured from the right of the screen to the right edge of the +widget or from the bottom of the screen to the bottom of the widget, +respectively. + +For easy reference, the corners can be found at the following coordinates: - enable overlay +:(1, 1): top left corner +:(-1, 1): top right corner +:(1, -1): lower left corner +:(-1, -1): lower right corner diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 69a32242b..5327d22f1 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -14,6 +14,7 @@ local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2 -- state and config -- -- ---------------- -- +local active_triggered_widget = nil local active_triggered_screen = nil -- if non-nil, hotspots will not get updates local widget_db = {} -- map of widget name to ephermeral state local widget_index = {} -- ordered list of widget names @@ -25,6 +26,7 @@ local function reset() if active_triggered_screen then active_triggered_screen:dismiss() end + active_triggered_widget = nil active_triggered_screen = nil widget_db = {} @@ -47,6 +49,7 @@ end local function triggered_screen_has_lock() if not active_triggered_screen then return false end if active_triggered_screen:isActive() then return true end + active_triggered_widget = nil active_triggered_screen = nil return false end @@ -108,7 +111,12 @@ local function get_name(name_or_number) end local function do_by_names_or_numbers(args, fn) - for _,name_or_number in ipairs(normalize_list(args)) do + local arglist = normalize_list(args) + if #arglist == 0 then + dfhack.printerr('please specify a widget name or list number') + return + end + for _,name_or_number in ipairs(arglist) do local name = get_name(name_or_number) local db_entry = widget_db[name] if not db_entry then @@ -120,7 +128,7 @@ local function do_by_names_or_numbers(args, fn) end local function do_enable(args, quiet, skip_save) - do_by_names_or_numbers(args, function(name, db_entry) + local enable_fn = function(name, db_entry) overlay_config[name].enabled = true if db_entry.widget.hotspot then active_hotspot_widgets[name] = db_entry @@ -132,14 +140,23 @@ local function do_enable(args, quiet, skip_save) if not quiet then print(('enabled widget %s'):format(name)) end - end) + end + if args[1] == 'all' then + for name,db_entry in pairs(widget_db) do + if not overlay_config[name].enabled then + enable_fn(name, db_entry) + end + end + else + do_by_names_or_numbers(args, enable_fn) + end if not skip_save then save_config() end end local function do_disable(args) - do_by_names_or_numbers(args, function(name, db_entry) + local disable_fn = function(name, db_entry) overlay_config[name].enabled = false if db_entry.widget.hotspot then active_hotspot_widgets[name] = nil @@ -152,7 +169,16 @@ local function do_disable(args) end end print(('disabled widget %s'):format(name)) - end) + end + if args[1] == 'all' then + for name,db_entry in pairs(widget_db) do + if overlay_config[name].enabled then + disable_fn(name, db_entry) + end + end + else + do_by_names_or_numbers(args, disable_fn) + end save_config() end @@ -175,11 +201,10 @@ local function do_list(args) end local db_entry = widget_db[name] local enabled = overlay_config[name].enabled - dfhack.color(enabled and COLOR_YELLOW or COLOR_LIGHTGREEN) + dfhack.color(enabled and COLOR_LIGHTGREEN or COLOR_YELLOW) dfhack.print(enabled and '[enabled] ' or '[disabled]') dfhack.color() - print((' %d) %s%s'):format(i, name, - db_entry.widget.overlay_trigger and ' (can trigger)' or '')) + print((' %d) %s'):format(i, name)) ::continue:: end if num_filtered > 0 then @@ -225,7 +250,7 @@ local function load_widgets(env_prefix, provider, env_fn) end end --- also called directly from cpp on init +-- called directly from cpp on init function reload() reset() @@ -256,18 +281,44 @@ function reload() reposition_widgets() end -local function do_reload() - reload() - print('reloaded overlay configuration') +local function dump_widget_config(name, widget) + local pos = overlay_config[name].pos + print(('widget %s is positioned at x=%d, y=%d'):format(name, pos.x, pos.y)) + if #widget.viewscreens > 0 then + print(' it will be attached to the following viewscreens:') + for _,vs in ipairs(widget.viewscreens) do + print((' %s'):format(vs)) + end + end + if widget.hotspot then + print(' on all screens it will act as a hotspot') + end end -local function do_reposition(args) +local function do_position(args) local name_or_number, x, y = table.unpack(args) local name = get_name(name_or_number) - -- TODO: check existence of widget, validate numbers, warn if offscreen - local pos = sanitize_pos{x=tonumber(x), y=tonumber(y)} - overlay_config[name].pos = pos + if not widget_db[name] then + if not name_or_number then + dfhack.printerr('please specify a widget name or list number') + else + dfhack.printerr(('widget not found: "%s"'):format(name)) + end + return + end local widget = widget_db[name].widget + local pos + if x == 'default' then + pos = sanitize_pos(widget.default_pos) + else + x, y = tonumber(x), tonumber(y) + if not x or not y then + dump_widget_config(name, widget) + return + end + pos = sanitize_pos{x=x, y=y} + end + overlay_config[name].pos = pos widget.frame = make_frame(pos, widget.frame) widget:updateLayout(get_screen_rect()) save_config() @@ -277,8 +328,8 @@ end -- note that the widget does not have to be enabled to be triggered local function do_trigger(args) if triggered_screen_has_lock() then - dfhack.printerr( - 'cannot trigger widget; another widget is already active') + dfhack.printerr(('cannot trigger widget; widget "%s" is already active') + :format(active_triggered_widget)) return end local target = args[1] @@ -286,6 +337,9 @@ local function do_trigger(args) local widget = db_entry.widget if widget.overlay_trigger then active_triggered_screen = widget:overlay_trigger() + if active_triggered_screen then + active_triggered_widget = name + end print(('triggered widget %s'):format(name)) end end) @@ -295,8 +349,7 @@ local command_fns = { enable=do_enable, disable=do_disable, list=do_list, - reload=do_reload, - reposition=do_reposition, + position=do_position, trigger=do_trigger, } @@ -325,7 +378,7 @@ end -- reduces the next call by a small random amount to introduce jitter into the -- widget processing timings -local function do_update(db_entry, now_ms, vs) +local function do_update(name, db_entry, now_ms, vs) if db_entry.next_update_ms > now_ms then return end local w = db_entry.widget local freq_ms = w.overlay_onupdate_max_freq_seconds * 1000 @@ -334,15 +387,18 @@ local function do_update(db_entry, now_ms, vs) if detect_frame_change(w, function() return w:overlay_onupdate(vs) end) then active_triggered_screen = w:overlay_trigger() - if active_triggered_screen then return true end + if active_triggered_screen then + active_triggered_widget = name + return true + end end end function update_hotspot_widgets() if triggered_screen_has_lock() then return end local now_ms = dfhack.getTickCount() - for _,db_entry in pairs(active_hotspot_widgets) do - if do_update(db_entry, now_ms) then return end + for name,db_entry in pairs(active_hotspot_widgets) do + if do_update(name, db_entry, now_ms) then return end end end @@ -350,8 +406,8 @@ function update_viewscreen_widgets(vs_name, vs) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return end local now_ms = dfhack.getTickCount() - for _,db_entry in pairs(vs_widgets) do - if do_update(db_entry, now_ms, vs) then return end + for name,db_entry in pairs(vs_widgets) do + if do_update(name, db_entry, now_ms, vs) then return end end end From 56cf7e945cff9090f630fda0c128360db634b396 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 9 Nov 2022 14:37:17 -0800 Subject: [PATCH 054/161] use anywhere hotkey so the overlay can be invoked --- plugins/overlay.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp index cb19df892..c2a04ac8b 100644 --- a/plugins/overlay.cpp +++ b/plugins/overlay.cpp @@ -88,6 +88,7 @@ #include "PluginManager.h" #include "VTableInterpose.h" +#include "modules/Gui.h" #include "modules/Screen.h" using namespace DFHack; @@ -364,7 +365,8 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector Date: Wed, 9 Nov 2022 15:48:24 -0800 Subject: [PATCH 055/161] bounds check overlay_onupdate_max_freq_seconds --- plugins/lua/overlay.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 5327d22f1..0403d9b0b 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -455,9 +455,15 @@ OverlayWidget.ATTRS{ overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate } --- set defaults for frame. the widget is expected to keep these up to date as --- display contents change. function OverlayWidget:init() + if self.overlay_onupdate_max_freq_seconds < 0 then + error(('overlay_onupdate_max_freq_seconds must be >= 0: %s') + :format(tostring(self.overlay_onupdate_max_freq_seconds))) + end + + -- set defaults for frame. the widget is expected to keep these up to date + -- if display contents change so the widget position can shift if the frame + -- is relative to the right or bottom edges. self.frame = self.frame or {} self.frame.w = self.frame.w or 5 self.frame.h = self.frame.h or 1 From e992e302a72a64101b334ae32efa089421ac13c9 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 09:46:53 -0800 Subject: [PATCH 056/161] add messy first draft of the dev guide --- docs/guides/overlay-widget-guide.rst | 194 +++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/guides/overlay-widget-guide.rst diff --git a/docs/guides/overlay-widget-guide.rst b/docs/guides/overlay-widget-guide.rst new file mode 100644 index 000000000..a55d89e44 --- /dev/null +++ b/docs/guides/overlay-widget-guide.rst @@ -0,0 +1,194 @@ +.. _overlay-widget-guide: + +.. highlight:: lua + +DFHack overlay widget dev guide +=============================== + +This guide walks you through how to build overlay widgets and register them with +the `overlay` framework for injection into Dwarf Fortress viewscreens. + +Why would I want to create an overlay widget? +--------------------------------------------- + +There are both C++ and Lua APIs for creating viewscreens and drawing to the +screen. If you need very specific low-level control, those APIs might be the +right choice for you. Here are some reasons you might want to implement an +overlay widget instead: + +1. You can draw directly to an existing viewscreen instead of creating an + entirely new screen on the viewscreen stack. This allows the original + viewscreen to continue processing uninterrupted and keybindings bound to + that viewscreen will continue to function. This was previously only + achievable by C++ plugins. +1. Your widget will be listed along with other widgets, making it more + discoverable for players who don't already have it enabled. +1. You don't have to manage the C++ interposing logic yourself and can focus on + the business logic, writing purely in Lua if desired. +1. You get the state of whether your widget is enabled and its (configurable) + position managed for you for free. + +In general, if you are writing a plugin or script and have anything you'd like +to add to an existing screen (including overlaying map tiles), an overlay widget +is probably your easiest path to get it done. If your plugin or script doesn't +otherwise need to be enabled to function, using the overlay allows you to avoid +writing any of the enable management code that would normally be required for +you to show info in the UI. + +What is an overlay widget? +-------------------------- + +Overlay widgets are Lua classes that inherit from ``overlay.OverlayWidget`` +(which itself inherits from ``widgets.Widget``). The regular ``onInput(keys)``, +``onRenderFrame(dc, frame_rect)``, and ``onRenderBody(dc)`` functions work as +normal, and they are called when the viewscreen that the widget is associated +with does its usual input and render processing. + +Overlay widgets can contain other Widgets, just like you're building a regular +UI element. + +There are a few extra capabilities that overlay widgets have that take them +beyond your everyday ``Widget``: + +bool = overlay_onupdate(viewscreen) if defined, will be called on every viewscreen logic() execution, but no more frequently than what is specified in the overlay_onupdate_max_freq_seconds class attribute. Widgets that need to update their state according to game changes can do it here. The viewscreen parameter is the viewscreen that this widget is attached to at the moment. For hotspot widgets, viewscreen will be nil. Returns whether overlay should subsequently call the widget's overlay_trigger() function. +screen = overlay_trigger() if defined, will be called when the overlay_onupdate callback returns true or when the player uses the CLI (or a keybinding calling the CLI) to trigger the widget. must return either nil or the Screen object that the widget code has allocated, shown, and now owns. Overlay widgets will receive no callbacks until the returned screen is dismissed. Unbound hotspot widgets must allocate a Screen if they want to react to the onInput() feed or be rendered. The widgets owned by the overlay must not be attached to that new screen, but the returned screen can instantiate and configure new views. +overlay_onupdate() will always get called for hotspots. Un-hotspotted widgets bound to particular viewscreens only get callbacks called when the relevant functions of the viewscreen are called (that is, the widget will be rendered when that viewscreen's render() function is run; the widget will get its onInput(keys) function called when the viewscreen's feed() function is run; the overlay_onupdate(viewscreen) function is called when that viewscren's logic() function is run). + +How do I register a widget with the overlay framework? +------------------------------------------------------ + +Anywhere in your code after the widget classes are declared, define a table +named ``OVERLAY_WIDGETS``. The keys are the display names for your widgets and +the values are the widget classes. For example, the `dwarfmonitor` widgets are +declared like this:: + + OVERLAY_WIDGETS = { + cursor=CursorWidget, + date=DateWidget, + misery=MiseryWidget, + weather=WeatherWidget, + } + +When the `overlay` plugin is enabled, it scans all plugins and scripts for +this table and registers the widgets on your behalf. The widget is enabled if it +was enabled the last time the `overlay` plugin was loaded and the widget's +position is restored according to the state saved in the +:file:`dfhack-config/overlay.json` file. + +Widget example 1: adding text, hotkeys, or functionality to a DF screen +----------------------------------------------------------------------- + + +Widget example 2: highlighting artifacts on the live game map +------------------------------------------------------------- + + +Widget example 3: corner hotspot +-------------------------------- + + +Here is a fully functional widget that displays a message on the screen:: + +local overlay = require('plugins.overlay') + +MessageWidget = defclass(MessageWidget, overlay.OverlayWidget) +MessageWidget.ATTRS{ + default_pos={x=-16,y=4}, -- default position near the upper right corner + viewscreens={'dungeonmode', 'dwarfmode'}, -- only display on main maps +} + +function MessageWidget:init() + self.message = '' + + self:addviews{widgets.} + + self:overlay_onupdate() +end + +function MessageWidget:overlay_onupdate() + -- getMessage() can be implemented elsewhere in the lua file or even + -- in a host plugin (e.g. exported with DFHACK_PLUGIN_LUA_COMMANDS) + local message = getMessage() + + self.frame.w = #message + self.message = message +end + +-- onRenderBody will be called whenever the associated viewscreen is +-- visible, even if it is not currently the top viewscreen +function MessageWidget:onRenderBody(dc) + dc:string(self.message, COLOR_GREY) +end + +function MessageWidget:onInput(keys) + +end + +-- register our widgets with the overlay +OVERLAY_WIDGETS = { + cursor=CursorWidget, + date=DateWidget, + misery=MiseryWidget, + weather=WeatherWidget, +} + + + + +Widget lifecycle +---------------- + +Overlay will instantiate and own the widgets. The instantiated widgets must not be added as subviews to any other View. + + + +The overlay widget can modify self.frame.w and self.frame.h at any time (in init() or in any of the callbacks) to indicate a new size. + + +Widget state +------------ +whether the widget is enabled +the screen position of the widget (relative to any edge) + + +Widget architecture +------------------- + +bool = overlay_onupdate(viewscreen) if defined, will be called on every viewscreen logic() execution, but no more frequently than what is specified in the overlay_onupdate_max_freq_seconds class attribute. Widgets that need to update their state according to game changes can do it here. The viewscreen parameter is the viewscreen that this widget is attached to at the moment. For hotspot widgets, viewscreen will be nil. Returns whether overlay should subsequently call the widget's overlay_trigger() function. +screen = overlay_trigger() if defined, will be called when the overlay_onupdate callback returns true or when the player uses the CLI (or a keybinding calling the CLI) to trigger the widget. must return either nil or the Screen object that the widget code has allocated, shown, and now owns. Overlay widgets will receive no callbacks until the returned screen is dismissed. Unbound hotspot widgets must allocate a Screen if they want to react to the onInput() feed or be rendered. The widgets owned by the overlay must not be attached to that new screen, but the returned screen can instantiate and configure new views. +overlay_onupdate() will always get called for hotspots. Un-hotspotted widgets bound to particular viewscreens only get callbacks called when the relevant functions of the viewscreen are called (that is, the widget will be rendered when that viewscreen's render() function is run; the widget will get its onInput(keys) function called when the viewscreen's feed() function is run; the overlay_onupdate(viewscreen) function is called when that viewscren's logic() function is run). + + + +Widget attributes +----------------- + +Your widget must inherit from ``overlay.OverlayWidget``, which defines the +following class properties: + +* ``name`` + This will be filled in with the display name of your widget, in case you + have multiple widgets with the same implementation but different + configurations. +* ``default_pos`` (default: ``{x=-2, y=-2}``) + Override this attribute with your desired default widget position. See + the `overlay` docs for information on what positive and negative numbers + mean for the position. +* ``viewscreens`` (default: ``{}``) + The list of viewscreens that this widget should be associated with. When + one of these viewscreens is on top, your widget's callback functions for + update, input, and render will be interposed into the viewscreen's call + path. +* ``hotspot`` (default: ``false``) + If set to ``true``, your widget's ``overlay_onupdate`` function will be + called whenever the `overlay` plugin's ``plugin_onupdate()`` function is + called (which corresponds to one call to the current top viewscreen's + ``logic()`` function). This is in addition to any calls to + ``overlay_onupdate`` initiated from associated interposed viewscreens. +* ``overlay_onupdate_max_freq_seconds`` (default: ``5``) + This throttles how often a widget's ``overlay_onupdate`` function can be + called. Set this to the largest amount of time (in seconds) that your + widget can take to react to changes in information and not annoy the player. + Set to 0 to be called at the maximum rate. Be aware that running more often + than you really need to will impact game FPS, especially if your widget is + bound to the main map screen. From 19289bf3c8969081ee8e2d56641bac575db53d00 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 10:36:58 -0800 Subject: [PATCH 057/161] clean up docs and code --- docs/Lua API.rst | 2 + docs/guides/index.rst | 1 + docs/guides/overlay-dev-guide.rst | 302 +++++++++++++++++++++++++++ docs/guides/overlay-widget-guide.rst | 194 ----------------- docs/plugins/overlay.rst | 6 +- plugins/lua/overlay.lua | 67 +++--- 6 files changed, 348 insertions(+), 224 deletions(-) create mode 100644 docs/guides/overlay-dev-guide.rst delete mode 100644 docs/guides/overlay-widget-guide.rst diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 5b6a30d93..cfebdd792 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -3898,6 +3898,8 @@ gui.widgets This module implements some basic widgets based on the View infrastructure. +.. _widget: + Widget class ------------ diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 96c5688d2..a3d9a0482 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -8,6 +8,7 @@ These pages are detailed guides covering DFHack tools. :maxdepth: 1 /docs/guides/examples-guide + /docs/guides/overlay-dev-guide /docs/guides/modding-guide /docs/guides/quickfort-library-guide /docs/guides/quickfort-user-guide diff --git a/docs/guides/overlay-dev-guide.rst b/docs/guides/overlay-dev-guide.rst new file mode 100644 index 000000000..8c84f27c7 --- /dev/null +++ b/docs/guides/overlay-dev-guide.rst @@ -0,0 +1,302 @@ +.. _overlay-dev-guide: + +DFHack overlay dev guide +========================= + +.. highlight:: lua + +This guide walks you through how to build overlay widgets and register them with +the `overlay` framework for injection into Dwarf Fortress viewscreens. + +Why would I want to create an overlay widget? +--------------------------------------------- + +There are both C++ and Lua APIs for creating viewscreens and drawing to the +screen. If you need very specific low-level control, those APIs might be the +right choice for you. However, here are some reasons you might want to implement +an overlay widget instead: + +1. You can draw directly to an existing viewscreen instead of creating an + entirely new screen on the viewscreen stack. This allows the original + viewscreen to continue processing uninterrupted and keybindings bound to + that viewscreen will continue to function. This was previously only + achievable by C++ plugins. + +1. You'll get a free UI for enabling/disabling your widget and repositioning it + on the screen. Widget state is saved for you and is automatically restored + when the game is restarted. + +1. You don't have to manage the C++ interposing logic yourself and can focus on + the business logic, writing purely in Lua if desired. + +In general, if you are writing a plugin or script and have anything you'd like +to add to an existing screen (including live updates of map tiles while the game +is unpaused), an overlay widget is probably your easiest path to get it done. If +your plugin or script doesn't otherwise need to be enabled to function, using +the overlay allows you to avoid writing any of the enable management code that +would normally be required for you to show info in the UI. + +Overlay widget API +------------------ + +Overlay widgets are Lua classes that inherit from ``overlay.OverlayWidget`` +(which itself inherits from `widgets.Widget `). The regular +``onInput(keys)``, ``onRenderFrame(dc, frame_rect)``, and ``onRenderBody(dc)`` +functions work as normal, and they are called when the viewscreen that the +widget is associated with does its usual input and render processing. The widget +gets first dibs on input processing. If a widget returns ``true`` from its +``onInput()`` function, the viewscreen will not receive the input. + +Overlay widgets can contain other Widgets and be as simple or complex as you +need them to be, just like you're building a regular UI element. + +There are a few extra capabilities that overlay widgets have that take them +beyond your everyday ``Widget``: + +- If an ``overlay_onupdate(viewscreen)`` function is defined, it will be called + just after the associated viewscreen's ``logic()`` function is called (i.e. + a "tick" or a (non-graphical) "frame"). For hotspot widgets, this function + will also get called after the top viewscreen's ``logic()`` function is + called, regardless of whether the widget is associated with that viewscreen. + If this function returns ``true``, then the widget's ``overlay_trigger()`` + function is immediately called. Note that the ``viewscreen`` parameter will + be ``nil`` for hotspot widgets that are not also associated with the current + viewscreen. +- If an ``overlay_trigger()`` function is defined, will be called when the + widget's ``overlay_onupdate`` callback returns true or when the player uses + the CLI (or a keybinding calling the CLI) to trigger the widget. The + function must return either ``nil`` or the ``gui.Screen`` object that the + widget code has allocated, shown, and now owns. Hotspot widgets will receive + no callbacks from unassociated viewscreens until the returned screen is + dismissed. Unbound hotspot widgets **must** allocate a Screen with this + function if they want to react to the ``onInput()`` feed or be rendered. The + widgets owned by the overlay framework must not be attached to that new + screen, but the returned screen can instantiate and configure any new views + that it wants to. + +If the widget can take up a variable amount of space on the screen, and you want +the widget to adjust its position according to the size of its contents, you can +modify ``self.frame.w`` and ``self.frame.h`` at any time -- in ``init()`` or in +any of the callbacks -- to indicate a new size. The overlay framework will +detect the size change and adjust the widget position and layout. + +If you don't need to dynamically resize, just set ``self.frame.w`` and +``self.frame.h`` once in ``init()``. + +Widget attributes +***************** + +The ``overlay.OverlayWidget`` superclass defines the following class attributes: + +- ``name`` + This will be filled in with the display name of your widget, in case you + have multiple widgets with the same implementation but different + configurations. +- ``default_pos`` (default: ``{x=-2, y=-2}``) + Override this attribute with your desired default widget position. See + the `overlay` docs for information on what positive and negative numbers + mean for the position. Players can change the widget position at any time + via the `overlay position ` command, so don't assume that your + widget will always be at the default position. +- ``viewscreens`` (default: ``{}``) + The list of viewscreens that this widget should be associated with. When + one of these viewscreens is on top of the viewscreen stack, your widget's + callback functions for update, input, and render will be interposed into the + viewscreen's call path. The name of the viewscreen is the name of the DFHack + class that represents the viewscreen, minus the ``viewscreen_`` prefix and + ``st`` suffix. For example, the fort mode main map viewscreen would be + ``dwarfmode`` and the adventure mode map viewscreen would be + ``dungeonmode``. If there is only one viewscreen that this widget is + associated with, it can be specified as a string instead of a list of + strings with a single element. +- ``hotspot`` (default: ``false``) + If set to ``true``, your widget's ``overlay_onupdate`` function will be + called whenever the `overlay` plugin's ``plugin_onupdate()`` function is + called (which corresponds to one call per call to the current top + viewscreen's ``logic()`` function). This call to ``overlay_onupdate`` is in + addition to any calls initiated from associated interposed viewscreens and + will come after calls from associated viewscreens. +- ``overlay_onupdate_max_freq_seconds`` (default: ``5``) + This throttles how often a widget's ``overlay_onupdate`` function can be + called (from any source). Set this to the largest amount of time (in + seconds) that your widget can take to react to changes in information and + not annoy the player. Set to 0 to be called at the maximum rate. Be aware + that running more often than you really need to will impact game FPS, + especially if your widget can run while the game is unpaused. + +Registering a widget with the overlay framework +*********************************************** + +Anywhere in your code after the widget classes are declared, define a table +named ``OVERLAY_WIDGETS``. The keys are the display names for your widgets and +the values are the widget classes. For example, the `dwarfmonitor` widgets are +declared like this:: + + OVERLAY_WIDGETS = { + cursor=CursorWidget, + date=DateWidget, + misery=MiseryWidget, + weather=WeatherWidget, + } + +When the `overlay` plugin is enabled, it scans all plugins and scripts for +this table and registers the widgets on your behalf. The widget is enabled if it +was enabled the last time the `overlay` plugin was loaded and the widget's +position is restored according to the state saved in the +:file:`dfhack-config/overlay.json` file. + +The overlay framework will instantiate widgets from the named classes and own +the resulting objects. The instantiated widgets must not be added as subviews to +any other View, including the Screen views that can be returned from the +``overlay_trigger()`` function. + +Widget example 1: adding text to a DF screen +-------------------------------------------- + +This is a simple widget that displays a message at its position. The message +text is retrieved from the host script or plugin every ~20 seconds or when +the :kbd:`Alt`:kbd:`Z` hotkey is hit:: + + local overlay = require('plugins.overlay') + local widgets = require('gui.widgets') + + MessageWidget = defclass(MessageWidget, overlay.OverlayWidget) + MessageWidget.ATTRS{ + default_pos={x=5,y=-2}, + viewscreens={'dwarfmode', 'dungeonmode'}, + overlay_onupdate_max_freq_seconds=20, + } + + function MessageWidget:init() + self.label = widgets.Label{text=''} + self:addviews{self.label} + end + + function MessageWidget:overlay_onupdate() + local text = getImportantMessage() -- defined in the host script/plugin + self.label:setText(text) + self.frame.w = #text + end + + function MessageWidget:onInput(keys) + if keys.CUSTOM_ALT_Z then + self:overlay_onupdate() + return true + end + end + + OVERLAY_WIDGETS = {message=MessageWidget} + +Widget example 2: highlighting artifacts on the live game map +------------------------------------------------------------- + +This widget is not rendered at its "position" at all, but instead monitors the +map and overlays information about where artifacts are located. Scanning for +which artifacts are visible on the map can slow, so that is only done every 10 +seconds to avoid slowing down the entire game on every frame. + +:: + + local overlay = require('plugins.overlay') + local widgets = require('gui.widgets') + + ArtifactRadarWidget = defclass(ArtifactRadarWidget, overlay.OverlayWidget) + ArtifactRadarWidget.ATTRS{ + viewscreens={'dwarfmode', 'dungeonmode'}, + overlay_onupdate_max_freq_seconds=10, + } + + function ArtifactRadarWidget:overlay_onupdate() + self.visible_artifacts_coords = getVisibleArtifactCoords() + end + + function ArtifactRadarWidget:onRenderFrame() + for _,pos in ipairs(self.visible_artifacts_coords) do + -- highlight tile at given coordinates + end + end + + OVERLAY_WIDGETS = {radar=ArtifactRadarWidget} + +Widget example 3: corner hotspot +-------------------------------- + +This hotspot reacts to mouseover events and launches a screen that can react to +input events. The hotspot area is a 2x2 block near the lower right corner of the +screen (by default, but the player can move it wherever). + +:: + + local overlay = require('plugins.overlay') + local widgets = require('gui.widgets') + + HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget) + HotspotMenuWidget.ATTRS{ + default_pos={x=-3,y=-3}, + frame={w=2, h=2}, + hotspot=true, + viewscreens='dwarfmode', + overlay_onupdate_max_freq_seconds=0, -- check for mouseover every tick + } + + function HotspotMenuWidget:init() + -- note this label only gets rendered on the associated viewscreen + -- (dwarfmode), but the hotspot is active on all screens + self:addviews{widgets.Label{text={'!!', NEWLINE, '!!'}}} + self.mouseover = false + end + + function HotspotMenuWidget:overlay_onupdate() + local hasMouse = self:getMousePos() + if hasMouse and not self.mouseover then -- only trigger on mouse entry + self.mouseover = true + return true + end + self.mouseover = hasMouse + end + + function HotspotMenuWidget:overlay_trigger() + return MenuScreen{hotspot_frame=self.frame}:show() + end + + OVERLAY_WIDGETS = {menu=HotspotMenuWidget} + + MenuScreen = defclass(MenuScreen, gui.Screen) + MenuScreen.ATTRS{ + focus_path='hotspot/menu', + hotspot_frame=DEFAULT_NIL, + } + + function MenuScreen:init() + self.mouseover = false + + -- derrive the menu frame from the hotspot frame so it + -- can appear in a nearby location + local frame = copyall(self.hotspot_frame) + -- ... + + self:addviews{ + widgets.ResizingPanel{ + autoarrange_subviews=true, + frame=frame, + frame_style=gui.GREY_LINE_FRAME, + frame_background=gui.CLEAR_PEN, + subviews={ + -- ... + }, + }, + }, + } + end + + function MenuScreen:onInput(keys) + if keys.LEAVESCREEN then + self:dismiss() + return true + end + return self:inputToSubviews(keys) + end + + function MenuScreen:onRenderFrame(dc, rect) + self:renderParent() + end diff --git a/docs/guides/overlay-widget-guide.rst b/docs/guides/overlay-widget-guide.rst deleted file mode 100644 index a55d89e44..000000000 --- a/docs/guides/overlay-widget-guide.rst +++ /dev/null @@ -1,194 +0,0 @@ -.. _overlay-widget-guide: - -.. highlight:: lua - -DFHack overlay widget dev guide -=============================== - -This guide walks you through how to build overlay widgets and register them with -the `overlay` framework for injection into Dwarf Fortress viewscreens. - -Why would I want to create an overlay widget? ---------------------------------------------- - -There are both C++ and Lua APIs for creating viewscreens and drawing to the -screen. If you need very specific low-level control, those APIs might be the -right choice for you. Here are some reasons you might want to implement an -overlay widget instead: - -1. You can draw directly to an existing viewscreen instead of creating an - entirely new screen on the viewscreen stack. This allows the original - viewscreen to continue processing uninterrupted and keybindings bound to - that viewscreen will continue to function. This was previously only - achievable by C++ plugins. -1. Your widget will be listed along with other widgets, making it more - discoverable for players who don't already have it enabled. -1. You don't have to manage the C++ interposing logic yourself and can focus on - the business logic, writing purely in Lua if desired. -1. You get the state of whether your widget is enabled and its (configurable) - position managed for you for free. - -In general, if you are writing a plugin or script and have anything you'd like -to add to an existing screen (including overlaying map tiles), an overlay widget -is probably your easiest path to get it done. If your plugin or script doesn't -otherwise need to be enabled to function, using the overlay allows you to avoid -writing any of the enable management code that would normally be required for -you to show info in the UI. - -What is an overlay widget? --------------------------- - -Overlay widgets are Lua classes that inherit from ``overlay.OverlayWidget`` -(which itself inherits from ``widgets.Widget``). The regular ``onInput(keys)``, -``onRenderFrame(dc, frame_rect)``, and ``onRenderBody(dc)`` functions work as -normal, and they are called when the viewscreen that the widget is associated -with does its usual input and render processing. - -Overlay widgets can contain other Widgets, just like you're building a regular -UI element. - -There are a few extra capabilities that overlay widgets have that take them -beyond your everyday ``Widget``: - -bool = overlay_onupdate(viewscreen) if defined, will be called on every viewscreen logic() execution, but no more frequently than what is specified in the overlay_onupdate_max_freq_seconds class attribute. Widgets that need to update their state according to game changes can do it here. The viewscreen parameter is the viewscreen that this widget is attached to at the moment. For hotspot widgets, viewscreen will be nil. Returns whether overlay should subsequently call the widget's overlay_trigger() function. -screen = overlay_trigger() if defined, will be called when the overlay_onupdate callback returns true or when the player uses the CLI (or a keybinding calling the CLI) to trigger the widget. must return either nil or the Screen object that the widget code has allocated, shown, and now owns. Overlay widgets will receive no callbacks until the returned screen is dismissed. Unbound hotspot widgets must allocate a Screen if they want to react to the onInput() feed or be rendered. The widgets owned by the overlay must not be attached to that new screen, but the returned screen can instantiate and configure new views. -overlay_onupdate() will always get called for hotspots. Un-hotspotted widgets bound to particular viewscreens only get callbacks called when the relevant functions of the viewscreen are called (that is, the widget will be rendered when that viewscreen's render() function is run; the widget will get its onInput(keys) function called when the viewscreen's feed() function is run; the overlay_onupdate(viewscreen) function is called when that viewscren's logic() function is run). - -How do I register a widget with the overlay framework? ------------------------------------------------------- - -Anywhere in your code after the widget classes are declared, define a table -named ``OVERLAY_WIDGETS``. The keys are the display names for your widgets and -the values are the widget classes. For example, the `dwarfmonitor` widgets are -declared like this:: - - OVERLAY_WIDGETS = { - cursor=CursorWidget, - date=DateWidget, - misery=MiseryWidget, - weather=WeatherWidget, - } - -When the `overlay` plugin is enabled, it scans all plugins and scripts for -this table and registers the widgets on your behalf. The widget is enabled if it -was enabled the last time the `overlay` plugin was loaded and the widget's -position is restored according to the state saved in the -:file:`dfhack-config/overlay.json` file. - -Widget example 1: adding text, hotkeys, or functionality to a DF screen ------------------------------------------------------------------------ - - -Widget example 2: highlighting artifacts on the live game map -------------------------------------------------------------- - - -Widget example 3: corner hotspot --------------------------------- - - -Here is a fully functional widget that displays a message on the screen:: - -local overlay = require('plugins.overlay') - -MessageWidget = defclass(MessageWidget, overlay.OverlayWidget) -MessageWidget.ATTRS{ - default_pos={x=-16,y=4}, -- default position near the upper right corner - viewscreens={'dungeonmode', 'dwarfmode'}, -- only display on main maps -} - -function MessageWidget:init() - self.message = '' - - self:addviews{widgets.} - - self:overlay_onupdate() -end - -function MessageWidget:overlay_onupdate() - -- getMessage() can be implemented elsewhere in the lua file or even - -- in a host plugin (e.g. exported with DFHACK_PLUGIN_LUA_COMMANDS) - local message = getMessage() - - self.frame.w = #message - self.message = message -end - --- onRenderBody will be called whenever the associated viewscreen is --- visible, even if it is not currently the top viewscreen -function MessageWidget:onRenderBody(dc) - dc:string(self.message, COLOR_GREY) -end - -function MessageWidget:onInput(keys) - -end - --- register our widgets with the overlay -OVERLAY_WIDGETS = { - cursor=CursorWidget, - date=DateWidget, - misery=MiseryWidget, - weather=WeatherWidget, -} - - - - -Widget lifecycle ----------------- - -Overlay will instantiate and own the widgets. The instantiated widgets must not be added as subviews to any other View. - - - -The overlay widget can modify self.frame.w and self.frame.h at any time (in init() or in any of the callbacks) to indicate a new size. - - -Widget state ------------- -whether the widget is enabled -the screen position of the widget (relative to any edge) - - -Widget architecture -------------------- - -bool = overlay_onupdate(viewscreen) if defined, will be called on every viewscreen logic() execution, but no more frequently than what is specified in the overlay_onupdate_max_freq_seconds class attribute. Widgets that need to update their state according to game changes can do it here. The viewscreen parameter is the viewscreen that this widget is attached to at the moment. For hotspot widgets, viewscreen will be nil. Returns whether overlay should subsequently call the widget's overlay_trigger() function. -screen = overlay_trigger() if defined, will be called when the overlay_onupdate callback returns true or when the player uses the CLI (or a keybinding calling the CLI) to trigger the widget. must return either nil or the Screen object that the widget code has allocated, shown, and now owns. Overlay widgets will receive no callbacks until the returned screen is dismissed. Unbound hotspot widgets must allocate a Screen if they want to react to the onInput() feed or be rendered. The widgets owned by the overlay must not be attached to that new screen, but the returned screen can instantiate and configure new views. -overlay_onupdate() will always get called for hotspots. Un-hotspotted widgets bound to particular viewscreens only get callbacks called when the relevant functions of the viewscreen are called (that is, the widget will be rendered when that viewscreen's render() function is run; the widget will get its onInput(keys) function called when the viewscreen's feed() function is run; the overlay_onupdate(viewscreen) function is called when that viewscren's logic() function is run). - - - -Widget attributes ------------------ - -Your widget must inherit from ``overlay.OverlayWidget``, which defines the -following class properties: - -* ``name`` - This will be filled in with the display name of your widget, in case you - have multiple widgets with the same implementation but different - configurations. -* ``default_pos`` (default: ``{x=-2, y=-2}``) - Override this attribute with your desired default widget position. See - the `overlay` docs for information on what positive and negative numbers - mean for the position. -* ``viewscreens`` (default: ``{}``) - The list of viewscreens that this widget should be associated with. When - one of these viewscreens is on top, your widget's callback functions for - update, input, and render will be interposed into the viewscreen's call - path. -* ``hotspot`` (default: ``false``) - If set to ``true``, your widget's ``overlay_onupdate`` function will be - called whenever the `overlay` plugin's ``plugin_onupdate()`` function is - called (which corresponds to one call to the current top viewscreen's - ``logic()`` function). This is in addition to any calls to - ``overlay_onupdate`` initiated from associated interposed viewscreens. -* ``overlay_onupdate_max_freq_seconds`` (default: ``5``) - This throttles how often a widget's ``overlay_onupdate`` function can be - called. Set this to the largest amount of time (in seconds) that your - widget can take to react to changes in information and not annoy the player. - Set to 0 to be called at the maximum rate. Be aware that running more often - than you really need to will impact game FPS, especially if your widget is - bound to the main map screen. diff --git a/docs/plugins/overlay.rst b/docs/plugins/overlay.rst index 8a63c8f20..4d2a1be7f 100644 --- a/docs/plugins/overlay.rst +++ b/docs/plugins/overlay.rst @@ -7,7 +7,7 @@ overlay The overlay framework manages the on-screen widgets that other tools (including 3rd party plugins and scripts) can register for display. If you are a developer -who wants to write an overlay widget, please see the `overlay-widget-guide`. +who wants to write an overlay widget, please see the `overlay-dev-guide`. Usage ----- @@ -17,9 +17,9 @@ Usage ``overlay enable|disable all| [ ...]`` Enable/disable all or specified widgets. Widgets can be specified by either their name or their number, as returned by ``overlay list``. -``overlay list`` +``overlay list []`` Show a list of all the widgets that are registered with the overlay - framework. + framework, optionally filtered by the given filter string. ``overlay position [default| ]`` Display configuration information for the given widget or change the position where it is rendered. See the `Widget position`_ section below for diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua index 0403d9b0b..ad20c2605 100644 --- a/plugins/lua/overlay.lua +++ b/plugins/lua/overlay.lua @@ -63,12 +63,19 @@ local function normalize_list(element_or_list) return {element_or_list} end --- allow "short form" to be specified, but use "long form" +-- normalize "short form" viewscreen names to "long form" local function normalize_viewscreen_name(vs_name) if vs_name:match('viewscreen_.*st') then return vs_name end return 'viewscreen_' .. vs_name .. 'st' end +-- reduce "long form" viewscreen names to "short form" +local function simplify_viewscreen_name(vs_name) + _,_,short_name = vs_name:find('^viewscreen_(.*)st$') + if short_name then return short_name end + return vs_name +end + local function is_empty(tbl) for _ in pairs(tbl) do return false @@ -79,7 +86,7 @@ end local function sanitize_pos(pos) local x = math.floor(tonumber(pos.x) or DEFAULT_X_POS) local y = math.floor(tonumber(pos.y) or DEFAULT_Y_POS) - -- if someone accidentally uses 1-based instead of 0-based indexing, fix it + -- if someone accidentally uses 0-based instead of 1-based indexing, fix it if x == 0 then x = 1 end if y == 0 then y = 1 end return {x=x, y=y} @@ -220,9 +227,7 @@ local function load_widget(name, widget_class) } if not overlay_config[name] then overlay_config[name] = {} end local config = overlay_config[name] - if not config.pos then - config.pos = sanitize_pos(widget.default_pos) - end + config.pos = sanitize_pos(config.pos or widget.default_pos) widget.frame = make_frame(config.pos, widget.frame) if config.enabled then do_enable(name, true, true) @@ -250,7 +255,7 @@ local function load_widgets(env_prefix, provider, env_fn) end end --- called directly from cpp on init +-- called directly from cpp on plugin enable function reload() reset() @@ -284,14 +289,15 @@ end local function dump_widget_config(name, widget) local pos = overlay_config[name].pos print(('widget %s is positioned at x=%d, y=%d'):format(name, pos.x, pos.y)) - if #widget.viewscreens > 0 then + local viewscreens = normalize_list(widget.viewscreens) + if #viewscreens > 0 then print(' it will be attached to the following viewscreens:') - for _,vs in ipairs(widget.viewscreens) do - print((' %s'):format(vs)) + for _,vs in ipairs(viewscreens) do + print((' %s'):format(simplify_viewscreen_name(vs))) end end if widget.hotspot then - print(' on all screens it will act as a hotspot') + print(' it will act as a hotspot on all screens') end end @@ -332,8 +338,7 @@ local function do_trigger(args) :format(active_triggered_widget)) return end - local target = args[1] - do_by_names_or_numbers(target, function(name, db_entry) + do_by_names_or_numbers(args[1], function(name, db_entry) local widget = db_entry.widget if widget.overlay_trigger then active_triggered_screen = widget:overlay_trigger() @@ -376,16 +381,23 @@ local function detect_frame_change(widget, fn) return ret end +local function get_next_onupdate_timestamp(now_ms, widget) + local freq_s = widget.overlay_onupdate_max_freq_seconds + if freq_s == 0 then + return now_ms + end + local freq_ms = math.floor(freq_s * 1000) + local jitter = math.random(0, freq_ms // 8) -- up to ~12% jitter + return now_ms + freq_ms - jitter +end + -- reduces the next call by a small random amount to introduce jitter into the -- widget processing timings local function do_update(name, db_entry, now_ms, vs) if db_entry.next_update_ms > now_ms then return end local w = db_entry.widget - local freq_ms = w.overlay_onupdate_max_freq_seconds * 1000 - local jitter = math.random(0, freq_ms // 8) -- up to ~12% jitter - db_entry.next_update_ms = now_ms + freq_ms - jitter - if detect_frame_change(w, - function() return w:overlay_onupdate(vs) end) then + db_entry.next_update_ms = get_next_onupdate_timestamp(now_ms, w) + if detect_frame_change(w, function() return w:overlay_onupdate(vs) end) then active_triggered_screen = w:overlay_trigger() if active_triggered_screen then active_triggered_widget = name @@ -402,6 +414,8 @@ function update_hotspot_widgets() end end +-- not subject to trigger lock since these widgets are already filtered by +-- viewscreen function update_viewscreen_widgets(vs_name, vs) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return end @@ -415,9 +429,8 @@ function feed_viewscreen_widgets(vs_name, keys) local vs_widgets = active_viewscreen_widgets[vs_name] if not vs_widgets then return false end for _,db_entry in pairs(vs_widgets) do - local widget = db_entry.widget - if detect_frame_change(widget, - function() return widget:onInput(keys) end) then + local w = db_entry.widget + if detect_frame_change(w, function() return w:onInput(keys) end) then return true end end @@ -429,8 +442,8 @@ function render_viewscreen_widgets(vs_name) if not vs_widgets then return false end local dc = gui.Painter.new() for _,db_entry in pairs(vs_widgets) do - local widget = db_entry.widget - detect_frame_change(widget, function() widget:render(dc) end) + local w = db_entry.widget + detect_frame_change(w, function() w:render(dc) end) end end @@ -449,9 +462,9 @@ end OverlayWidget = defclass(OverlayWidget, widgets.Widget) OverlayWidget.ATTRS{ name=DEFAULT_NIL, -- this is set by the framework to the widget name - default_pos={x=DEFAULT_X_POS, y=DEFAULT_Y_POS}, -- initial widget screen pos, 1-based - hotspot=false, -- whether to call overlay_onupdate for all screens - viewscreens={}, -- override with list of viewscrens to interpose + default_pos={x=DEFAULT_X_POS, y=DEFAULT_Y_POS}, -- 1-based widget screen pos + hotspot=false, -- whether to call overlay_onupdate on all screens + viewscreens={}, -- override with associated viewscreen or list of viewscrens overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate } @@ -462,8 +475,8 @@ function OverlayWidget:init() end -- set defaults for frame. the widget is expected to keep these up to date - -- if display contents change so the widget position can shift if the frame - -- is relative to the right or bottom edges. + -- when display contents change so the widget position can shift if the + -- frame is relative to the right or bottom edges. self.frame = self.frame or {} self.frame.w = self.frame.w or 5 self.frame.h = self.frame.h or 1 From 14ab11cb9d8bf1b4578211424c363952b4cb5a29 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 17:31:47 -0800 Subject: [PATCH 058/161] update changelog --- docs/changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 9eb067d50..4465a0f12 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -34,6 +34,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future ## New Plugins +- `overlay`: plugin is transformed from a single line of text that runs `gui/launcher` on click to a fully-featured overlay injection framework. See `overlay-dev-guide` for details. ## Fixes - `automaterial`: fix the cursor jumping up a z level when clicking quickly after box select @@ -58,7 +59,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - UX: List widgets now have mouse-interactive scrollbars - UX: You can now hold down the mouse button on a scrollbar to make it scroll multiple times. - UX: You can now drag the scrollbar to scroll to a specific spot -- `overlay`: reduce the size of the "DFHack Launcher" button - Constructions module: ``findAtTile`` now uses a binary search intead of a linear search. - `spectate`: new ``auto-unpause`` option for auto-dismissal of announcement pause events (e.g. sieges). - `spectate`: new ``auto-disengage`` option for auto-disengagement of plugin through player interaction whilst unpaused. @@ -69,6 +69,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Documentation - `spectate`: improved documentation of features and functionality +- `overlay-dev-guide`: documentation and guide for injecting functionality into DF viewscreens from Lua scripts and creating overlay widgets ## API - ``Gui::anywhere_hotkey``: for plugin commands bound to keybindings that can be invoked on any screen From 2cf676758967a63db2845fb9f31f73a8412e5475 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 2 Nov 2022 12:40:18 -0700 Subject: [PATCH 059/161] migrate dwarfmonitor widgets to overlay v2 --- dfhack-config/dwarfmonitor.json | 20 +- .../overlay/widgets/dwarfmonitor_cursor.json | 12 + .../overlay/widgets/dwarfmonitor_date.json | 11 + .../overlay/widgets/dwarfmonitor_misery.json | 11 + .../overlay/widgets/dwarfmonitor_weather.json | 11 + plugins/dwarfmonitor.cpp | 376 +++--------------- plugins/lua/dwarfmonitor.lua | 227 +++++------ 7 files changed, 201 insertions(+), 467 deletions(-) create mode 100644 dfhack-config/overlay/widgets/dwarfmonitor_cursor.json create mode 100644 dfhack-config/overlay/widgets/dwarfmonitor_date.json create mode 100644 dfhack-config/overlay/widgets/dwarfmonitor_misery.json create mode 100644 dfhack-config/overlay/widgets/dwarfmonitor_weather.json diff --git a/dfhack-config/dwarfmonitor.json b/dfhack-config/dwarfmonitor.json index 007dad020..9bd3b1f76 100644 --- a/dfhack-config/dwarfmonitor.json +++ b/dfhack-config/dwarfmonitor.json @@ -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" } diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json b/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json new file mode 100644 index 000000000..cc2db582b --- /dev/null +++ b/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json @@ -0,0 +1,12 @@ +{ + "pos": { + "x": 2, + "y": 2 + }, + "provider": "dwarfmonitor", + "class": "Widget_cursor", + "viewscreens": [ + "dungeonmode", + "dwarfmode" + ] +} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_date.json b/dfhack-config/overlay/widgets/dwarfmonitor_date.json new file mode 100644 index 000000000..6c868d419 --- /dev/null +++ b/dfhack-config/overlay/widgets/dwarfmonitor_date.json @@ -0,0 +1,11 @@ +{ + "pos": { + "x": -16, + "y": 1 + }, + "provider": "dwarfmonitor", + "class": "Widget_date", + "viewscreens": [ + "dwarfmode" + ] +} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_misery.json b/dfhack-config/overlay/widgets/dwarfmonitor_misery.json new file mode 100644 index 000000000..5ef741823 --- /dev/null +++ b/dfhack-config/overlay/widgets/dwarfmonitor_misery.json @@ -0,0 +1,11 @@ +{ + "pos": { + "x": -2, + "y": -1 + }, + "provider": "dwarfmonitor", + "class": "Widget_misery", + "viewscreens": [ + "dwarfmode" + ] +} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_weather.json b/dfhack-config/overlay/widgets/dwarfmonitor_weather.json new file mode 100644 index 000000000..8a6130af0 --- /dev/null +++ b/dfhack-config/overlay/widgets/dwarfmonitor_weather.json @@ -0,0 +1,11 @@ +{ + "pos": { + "x": 15, + "y": -1 + }, + "provider": "dwarfmonitor", + "class": "Widget_weather", + "viewscreens": [ + "dwarfmode" + ] +} diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index 1a453e0d5..4355104a7 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -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> 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 *input) - { + void feed(set *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 preferences_column; @@ -1762,13 +1663,11 @@ private: vector 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(), 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 & parameters) -{ - bool show_help = false; +static command_result dwarfmonitor_cmd(color_ostream &, vector & 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(), plugin_self); - } - else if (cmd == 'p' || cmd == 'P') - { - CoreSuspender guard; - if(Maps::IsValid()) - Screen::show(dts::make_unique(), 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(), plugin_self); } - - if (show_help) + else if (cmd == 'p' || cmd == 'P') { + CoreSuspender guard; + if(Maps::IsValid()) + Screen::show(dts::make_unique(), plugin_self); + } + else return CR_WRONG_USAGE; return CR_OK; } -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) +DFhackCExport command_result plugin_init(color_ostream &, std::vector &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 = 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 From 9ce9d2001c7214be18197e4c4183326c50b73b82 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 4 Nov 2022 10:29:09 -0700 Subject: [PATCH 060/161] adapt dwarfmonitor widgets to new API --- .../overlay/widgets/dwarfmonitor_cursor.json | 12 --- .../overlay/widgets/dwarfmonitor_date.json | 11 --- .../overlay/widgets/dwarfmonitor_misery.json | 11 --- .../overlay/widgets/dwarfmonitor_weather.json | 11 --- plugins/lua/dwarfmonitor.lua | 76 ++++++++++++------- 5 files changed, 50 insertions(+), 71 deletions(-) delete mode 100644 dfhack-config/overlay/widgets/dwarfmonitor_cursor.json delete mode 100644 dfhack-config/overlay/widgets/dwarfmonitor_date.json delete mode 100644 dfhack-config/overlay/widgets/dwarfmonitor_misery.json delete mode 100644 dfhack-config/overlay/widgets/dwarfmonitor_weather.json diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json b/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json deleted file mode 100644 index cc2db582b..000000000 --- a/dfhack-config/overlay/widgets/dwarfmonitor_cursor.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "pos": { - "x": 2, - "y": 2 - }, - "provider": "dwarfmonitor", - "class": "Widget_cursor", - "viewscreens": [ - "dungeonmode", - "dwarfmode" - ] -} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_date.json b/dfhack-config/overlay/widgets/dwarfmonitor_date.json deleted file mode 100644 index 6c868d419..000000000 --- a/dfhack-config/overlay/widgets/dwarfmonitor_date.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "pos": { - "x": -16, - "y": 1 - }, - "provider": "dwarfmonitor", - "class": "Widget_date", - "viewscreens": [ - "dwarfmode" - ] -} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_misery.json b/dfhack-config/overlay/widgets/dwarfmonitor_misery.json deleted file mode 100644 index 5ef741823..000000000 --- a/dfhack-config/overlay/widgets/dwarfmonitor_misery.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "pos": { - "x": -2, - "y": -1 - }, - "provider": "dwarfmonitor", - "class": "Widget_misery", - "viewscreens": [ - "dwarfmode" - ] -} diff --git a/dfhack-config/overlay/widgets/dwarfmonitor_weather.json b/dfhack-config/overlay/widgets/dwarfmonitor_weather.json deleted file mode 100644 index 8a6130af0..000000000 --- a/dfhack-config/overlay/widgets/dwarfmonitor_weather.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "pos": { - "x": 15, - "y": -1 - }, - "provider": "dwarfmonitor", - "class": "Widget_weather", - "viewscreens": [ - "dwarfmode" - ] -} diff --git a/plugins/lua/dwarfmonitor.lua b/plugins/lua/dwarfmonitor.lua index 27d43cd5b..866aa609a 100644 --- a/plugins/lua/dwarfmonitor.lua +++ b/plugins/lua/dwarfmonitor.lua @@ -6,18 +6,22 @@ local overlay = require('plugins.overlay') local DWARFMONITOR_CONFIG_FILE = 'dfhack-config/dwarfmonitor.json' --- -------------- --- Widget_weather --- -------------- +-- ------------- -- +-- WeatherWidget -- +-- ------------- -- -Widget_weather = defclass(Widget_weather, overlay.OverlayWidget) +WeatherWidget = defclass(WeatherWidget, overlay.OverlayWidget) +WeatherWidget.ATTRS{ + default_pos={x=15,y=-1}, + viewscreens={'dungeonmode', 'dwarfmode'}, +} -function Widget_weather:init() +function WeatherWidget:init() self.rain = false self.snow = false end -function Widget_weather:overlay_onupdate() +function WeatherWidget:overlay_onupdate() local rain, snow = false, false local cw = df.global.current_weather for i=0,4 do @@ -32,14 +36,14 @@ function Widget_weather:overlay_onupdate() self.rain, self.snow = rain, snow end -function Widget_weather:onRenderBody(dc) +function WeatherWidget: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 --- ----------- +-- ---------- -- +-- DateWidget -- +-- ---------- -- local function get_date_format() local ok, config = pcall(json.decode_file, DWARFMONITOR_CONFIG_FILE) @@ -49,14 +53,18 @@ local function get_date_format() return config.date_format end -Widget_date = defclass(Widget_date, overlay.OverlayWidget) +DateWidget = defclass(DateWidget, overlay.OverlayWidget) +DateWidget.ATTRS{ + default_pos={x=-16,y=1}, + viewscreens={'dungeonmode', 'dwarfmode'}, +} -function Widget_date:init() +function DateWidget:init() self.datestr = '' self.fmt = get_date_format() end -function Widget_date:overlay_onupdate() +function DateWidget:overlay_onupdate() local year = dfhack.world.ReadCurrentYear() local month = dfhack.world.ReadCurrentMonth() + 1 local day = dfhack.world.ReadCurrentDay() @@ -86,22 +94,26 @@ function Widget_date:overlay_onupdate() self.datestr = datestr end -function Widget_date:onRenderBody(dc) +function DateWidget:onRenderBody(dc) dc:string(self.datestr, COLOR_GREY) end --- ------------- --- Widget_misery --- ------------- +-- ------------ -- +-- MiseryWidget -- +-- ------------ -- -Widget_misery = defclass(Widget_misery, overlay.OverlayWidget) +MiseryWidget = defclass(MiseryWidget, overlay.OverlayWidget) +MiseryWidget.ATTRS{ + default_pos={x=-2,y=-1}, + viewscreens={'dwarfmode'}, +} -function Widget_misery:init() +function MiseryWidget:init() self.colors = getStressCategoryColors() self.stress_category_counts = {} end -function Widget_misery:overlay_onupdate() +function MiseryWidget: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, @@ -118,7 +130,7 @@ function Widget_misery:overlay_onupdate() self.frame.w = width end -function Widget_misery:onRenderBody(dc) +function MiseryWidget:onRenderBody(dc) dc:string('H:', COLOR_WHITE) local counts = self.stress_category_counts for i,color in ipairs(self.colors) do @@ -127,13 +139,17 @@ function Widget_misery:onRenderBody(dc) end end --- ------------- --- Widget_cursor --- ------------- +-- ------------ -- +-- CursorWidget -- +-- ------------ -- -Widget_cursor = defclass(Widget_cursor, overlay.OverlayWidget) +CursorWidget = defclass(CursorWidget, overlay.OverlayWidget) +CursorWidget.ATTRS{ + default_pos={x=2,y=-4}, + viewscreens={'dungeonmode', 'dwarfmode'}, +} -function Widget_cursor:onRenderBody(dc) +function CursorWidget:onRenderBody(dc) local screenx, screeny = dfhack.screen.getMousePos() local mouse_map = dfhack.gui.getMousePos() local keyboard_map = guidm.getCursorPos() @@ -157,4 +173,12 @@ function Widget_cursor:onRenderBody(dc) self.frame.h = #text end +-- register our widgets with the overlay +OVERLAY_WIDGETS = { + cursor=CursorWidget, + date=DateWidget, + misery=MiseryWidget, + weather=WeatherWidget, +} + return _ENV From 722f70437ccf59aa68f5c1a608f4e6213d42d2d7 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 17:42:18 -0800 Subject: [PATCH 061/161] update dwarfmonitor docs --- docs/plugins/dwarfmonitor.rst | 91 +++++++++-------------------------- 1 file changed, 23 insertions(+), 68 deletions(-) diff --git a/docs/plugins/dwarfmonitor.rst b/docs/plugins/dwarfmonitor.rst index 79472ae58..75e261171 100644 --- a/docs/plugins/dwarfmonitor.rst +++ b/docs/plugins/dwarfmonitor.rst @@ -2,7 +2,7 @@ dwarfmonitor ============ .. dfhack-tool:: - :summary: Measure fort happiness and efficiency. + :summary: Report on dwarf preferences and efficiency. :tags: fort inspection jobs units It can also show heads-up display widgets with live fort statistics. @@ -11,84 +11,39 @@ Usage ----- ``enable dwarfmonitor`` - Enable the plugin. -``dwarfmonitor enable `` - 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 `` - Stop monitoring ``mode`` and disable corresponding widgets. + Enable tracking of job efficiency for display on the ``dwarfmonitor stats`` + screen. ``dwarfmonitor stats`` - Show statistics summary. + Show statistics and efficiency summary. ``dwarfmonitor prefs`` - Show summary of dwarf preferences. -``dwarfmonitor reload`` - Reload the widget configuration file (``dfhack-config/dwarfmonitor.json``). + Show a summary of preferences for dwarves in your fort. Widget configuration -------------------- -The following types of widgets (defined in -:file:`hack/lua/plugins/dwarfmonitor.lua`) can be displayed on the main fortress -mode screen: +The following widgets are registered for display on the main fortress mode +screen with the `overlay` framework: -``misery`` - Show overall happiness levels of all dwarves. -``date`` +``dwarfmonitor.cursor`` + Show the current keyboard and mouse cursor positions. +``dwarfmonitor.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). -``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 - * ``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 +They can be enabled or disable via the `overlay` command. - 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 - 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. +.. _ISO8601: https://en.wikipedia.org/wiki/ISO_8601 From 52e850917e9b44e0f723c3ccf2de2969afe86dcc Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 17:43:35 -0800 Subject: [PATCH 062/161] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4465a0f12..9c6940cd0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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`: 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 +- `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`: 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 From e3498f275fca456fb8d33f27b6ed809cb2bebb22 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 14 Nov 2022 15:23:30 -0800 Subject: [PATCH 063/161] fix dwarfmonitor weather widget --- plugins/lua/dwarfmonitor.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lua/dwarfmonitor.lua b/plugins/lua/dwarfmonitor.lua index 866aa609a..e8c07cac6 100644 --- a/plugins/lua/dwarfmonitor.lua +++ b/plugins/lua/dwarfmonitor.lua @@ -27,8 +27,8 @@ function WeatherWidget:overlay_onupdate() 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 + if weather == df.weather_type.Rain then rain = true end + if weather == df.weather_type.Snow then snow = true end end end self.frame.w = (rain and 4 or 0) + (snow and 4 or 0) + From 6e005d4a8d869b40a464be69a09ad52ccc709972 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 4 Nov 2022 17:42:38 -0700 Subject: [PATCH 064/161] implement basic logic for hotspot menu --- plugins/hotkeys.cpp | 12 ++++ plugins/lua/hotkeys.lua | 150 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 plugins/lua/hotkeys.lua diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index e92bc61fe..6637c9931 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -349,6 +349,18 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete return CR_OK; } +static int getHotkeys(lua_State *L) { + find_active_keybindings(Gui::getCurViewscreen(true)); + Lua::PushVector(L, sorted_keys); + Lua::Push(L, current_bindings); + return 2; +} + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getHotkeys), + DFHACK_LUA_END +}; + DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua new file mode 100644 index 000000000..562dfd719 --- /dev/null +++ b/plugins/lua/hotkeys.lua @@ -0,0 +1,150 @@ +local _ENV = mkmodule('plugins.hotkeys') + +local gui = require('gui') +local helpdb = require('helpdb') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +-- ----------------- -- +-- HotspotMenuWidget -- +-- ----------------- -- + +HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget) +HotspotMenuWidget.ATTRS{ + default_pos={x=1,y=2}, + hotspot=true, + viewscreens={'dwarfmode'}, + overlay_onupdate_max_freq_seconds=0, + frame={w=2, h=1} +} + +function HotspotMenuWidget:init() + self:addviews{widgets.Label{text='!!'}} + self.mouseover = false +end + +function HotspotMenuWidget:overlay_onupdate() + local hasMouse = self:getMousePos() + if hasMouse and not self.mouseover then + self.mouseover = true + return true + end + self.mouseover = hasMouse +end + +function HotspotMenuWidget:overlay_trigger() + local hotkeys, bindings = getHotkeys() + return MenuScreen{ + hotspot_frame=self.frame, + hotkeys=hotkeys, + bindings=bindings}:show() +end + +-- register the menu hotspot with the overlay +OVERLAY_WIDGETS = {menu=HotspotMenuWidget} + +-- ---------- -- +-- MenuScreen -- +-- ---------- -- + +local ARROW = string.char(26) +local MENU_WIDTH = 40 +local MENU_HEIGHT = 10 + +MenuScreen = defclass(MenuScreen, gui.Screen) +MenuScreen.ATTRS{ + focus_path='hotkeys/menu', + hotspot_frame=DEFAULT_NIL, + hotkeys=DEFAULT_NIL, + bindings=DEFAULT_NIL, +} + +function MenuScreen:init() + self.mouseover = false + + local list_frame = copyall(self.hotspot_frame) + list_frame.w = MENU_WIDTH + list_frame.h = MENU_HEIGHT + + local help_frame = {w=MENU_WIDTH, l=list_frame.l, r=list_frame.r} + if list_frame.t then + help_frame.t = list_frame.t + MENU_HEIGHT + 1 + else + help_frame.b = list_frame.b + MENU_HEIGHT + 1 + end + + local bindings = self.bindings + local choices = {} + for _,hotkey in ipairs(self.hotkeys) do + local command = bindings[hotkey] + local choice_text = command .. (' (%s)'):format(hotkey) + local choice = { + icon=ARROW, text=choice_text, command=command} + table.insert(choices, list_frame.b and 1 or #choices + 1, choice) + end + + self:addviews{ + widgets.List{ + view_id='list', + frame=list_frame, + choices=choices, + icon_width=2, + on_select=self:callback('onSelect'), + on_submit=self:callback('onSubmit'), + on_submit2=self:callback('onSubmit2'), + }, + widgets.WrappedLabel{ + view_id='help', + frame=help_frame, + text_to_wrap='', + scroll_keys={}, + }, + } +end + +function MenuScreen:onSelect(_, choice) + if not choice then return end + local help = self.subviews.help + local first_word = choice.command:trim():split(' +')[1] + if not help or #first_word == 0 then return end + help.text_to_wrap = helpdb.get_entry_short_help(first_word) + help:updateLayout() +end + +function MenuScreen:onSubmit(_, choice) + self:dismiss() + dfhack.run_command(choice.command) +end + +function MenuScreen:onSubmit2(_, choice) + self:dismiss() + dfhack.run_script('gui/launcher', choice.command) +end + +function MenuScreen:onInput(keys) + if keys.LEAVESCREEN then + self:dismiss() + return true + end + return self:inputToSubviews(keys) +end + +function MenuScreen:onRenderBody(dc) + self:renderParent() + local list = self.subviews.list + local idx = list:getIdxUnderMouse() + if idx and idx ~= self.last_mouse_idx then + -- focus follows mouse, but if cursor keys were used to change the + -- selection, don't override the selection until the mouse moves to + -- another item + list:setSelected(idx) + self.mouseover = true + self.last_mouse_idx = idx + elseif not idx and self.mouseover then + -- once the mouse has entered the list area, leaving it again should + -- close the menu screen + self:dismiss() + end +end + +return _ENV From ae2d9008ef37e3d057493c569c1993c87351f41c Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 6 Nov 2022 16:42:13 -0800 Subject: [PATCH 065/161] add frames around menu panels --- plugins/lua/hotkeys.lua | 55 +++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 562dfd719..e83cf9586 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -48,8 +48,8 @@ OVERLAY_WIDGETS = {menu=HotspotMenuWidget} -- ---------- -- local ARROW = string.char(26) -local MENU_WIDTH = 40 -local MENU_HEIGHT = 10 +local MENU_WIDTH = 42 +local MENU_HEIGHT = 12 MenuScreen = defclass(MenuScreen, gui.Screen) MenuScreen.ATTRS{ @@ -84,31 +84,45 @@ function MenuScreen:init() end self:addviews{ - widgets.List{ - view_id='list', + widgets.ResizingPanel{ + autoarrange_subviews=true, frame=list_frame, - choices=choices, - icon_width=2, - on_select=self:callback('onSelect'), - on_submit=self:callback('onSubmit'), - on_submit2=self:callback('onSubmit2'), + frame_style=gui.GREY_LINE_FRAME, + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.List{ + view_id='list', + choices=choices, + icon_width=2, + on_select=self:callback('onSelect'), + on_submit=self:callback('onSubmit'), + on_submit2=self:callback('onSubmit2'), + }, + }, }, - widgets.WrappedLabel{ - view_id='help', + widgets.ResizingPanel{ + view_id='help_panel', + autoarrange_subviews=true, frame=help_frame, - text_to_wrap='', - scroll_keys={}, + frame_style=gui.GREY_LINE_FRAME, + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.WrappedLabel{ + view_id='help', + text_to_wrap='', + scroll_keys={}, + }, + }, }, } end function MenuScreen:onSelect(_, choice) - if not choice then return end - local help = self.subviews.help + if not choice or #self.subviews == 0 then return end local first_word = choice.command:trim():split(' +')[1] - if not help or #first_word == 0 then return end - help.text_to_wrap = helpdb.get_entry_short_help(first_word) - help:updateLayout() + self.subviews.help.text_to_wrap = helpdb.is_entry(first_word) and + helpdb.get_entry_short_help(first_word) or 'Command not found' + self.subviews.help_panel:updateLayout() end function MenuScreen:onSubmit(_, choice) @@ -129,8 +143,11 @@ function MenuScreen:onInput(keys) return self:inputToSubviews(keys) end -function MenuScreen:onRenderBody(dc) +function MenuScreen:onRenderFrame(dc, rect) self:renderParent() +end + +function MenuScreen:onRenderBody(dc) local list = self.subviews.list local idx = list:getIdxUnderMouse() if idx and idx ~= self.last_mouse_idx then From fb7b55fb111d0a2acf7ad13bbd5cbaad0ca12c19 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 6 Nov 2022 16:47:45 -0800 Subject: [PATCH 066/161] open gui/launcher with the command on right arrow --- plugins/lua/hotkeys.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index e83cf9586..921a1fd9b 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -126,11 +126,13 @@ function MenuScreen:onSelect(_, choice) end function MenuScreen:onSubmit(_, choice) + if not choice then return end self:dismiss() dfhack.run_command(choice.command) end function MenuScreen:onSubmit2(_, choice) + if not choice then return end self:dismiss() dfhack.run_script('gui/launcher', choice.command) end @@ -139,6 +141,9 @@ function MenuScreen:onInput(keys) if keys.LEAVESCREEN then self:dismiss() return true + elseif keys.STANDARDSCROLL_RIGHT then + self:onSubmit2(self.subviews.list:getSelected()) + return true end return self:inputToSubviews(keys) end From d8c86fd0b1fb1f20a3101e5ac67e58769f06d388 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 6 Nov 2022 16:54:05 -0800 Subject: [PATCH 067/161] allow commands with hotkey guards to work --- plugins/lua/hotkeys.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 921a1fd9b..1cd0e74c4 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -127,8 +127,8 @@ end function MenuScreen:onSubmit(_, choice) if not choice then return end + dfhack.screen.hideGuard(self, dfhack.run_command, choice.command) self:dismiss() - dfhack.run_command(choice.command) end function MenuScreen:onSubmit2(_, choice) From c630a71c73076f7d2ffc827814f5d1191979aed2 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 6 Nov 2022 16:58:44 -0800 Subject: [PATCH 068/161] click on arrow to launch gui/launcher with command --- plugins/lua/hotkeys.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 1cd0e74c4..1750d5470 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -144,6 +144,13 @@ function MenuScreen:onInput(keys) elseif keys.STANDARDSCROLL_RIGHT then self:onSubmit2(self.subviews.list:getSelected()) return true + elseif keys._MOUSE_L then + local list = self.subviews.list + local x = list:getMousePos() + if x == 0 then -- clicked on icon + self:onSubmit2(list:getSelected()) + return true + end end return self:inputToSubviews(keys) end From 5d29da31b0a349f6da584f27cd23c72efd315192 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 09:13:27 -0800 Subject: [PATCH 069/161] rework hotkeys plugin to support the widget --- plugins/hotkeys.cpp | 293 +++++--------------------------------------- 1 file changed, 31 insertions(+), 262 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 6637c9931..70f6a2f98 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -1,24 +1,27 @@ -#include "uicommon.h" -#include "listcolumn.h" +#include +#include +#include -#include "df/viewscreen_dwarfmodest.h" -#include "df/ui.h" - -#include "modules/Maps.h" -#include "modules/World.h" #include "modules/Gui.h" +#include "modules/Screen.h" #include "LuaTools.h" #include "PluginManager.h" DFHACK_PLUGIN("hotkeys"); -#define PLUGIN_VERSION 0.1 + +using std::map; +using std::string; +using std::vector; + +using namespace DFHack; + +static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; static map current_bindings; static vector sorted_keys; -static bool show_usage = false; -static bool can_invoke(string cmdline, df::viewscreen *screen) +static bool can_invoke(const string &cmdline, df::viewscreen *screen) { vector cmd_parts; split_string(&cmd_parts, cmdline, " "); @@ -28,14 +31,14 @@ static bool can_invoke(string cmdline, df::viewscreen *screen) return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen); } -static void add_binding_if_valid(string sym, string cmdline, df::viewscreen *screen) +static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen) { if (!can_invoke(cmdline, screen)) return; current_bindings[sym] = cmdline; sorted_keys.push_back(sym); - string keyspec = sym + "@dfhack/viewscreen_hotkeys"; + string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; Core::getInstance().AddKeyBinding(keyspec, "hotkeys invoke " + int_to_string(sorted_keys.size() - 1)); } @@ -101,252 +104,39 @@ static void find_active_keybindings(df::viewscreen *screen) static bool close_hotkeys_screen() { auto screen = Core::getTopViewscreen(); - if (Gui::getFocusString(screen) != "dfhack/viewscreen_hotkeys") + if (Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING) return false; Screen::dismiss(Core::getTopViewscreen()); - for_each_(sorted_keys, [] (const string &sym) - { Core::getInstance().ClearKeyBindings(sym + "@dfhack/viewscreen_hotkeys"); }); + std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym){ + Core::getInstance().ClearKeyBindings(sym + "@" + MENU_SCREEN_FOCUS_STRING); + }); sorted_keys.clear(); return true; } - -static void invoke_command(const size_t index) +static bool invoke_command(const size_t index) { if (sorted_keys.size() <= index) - return; + return false; auto cmd = current_bindings[sorted_keys[index]]; - if (close_hotkeys_screen()) - { + if (close_hotkeys_screen()) { Core::getInstance().setHotkeyCmd(cmd); + return true; } + return false; } -static std::string get_help(const std::string &command, bool full_help) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 2) || - !Lua::PushModulePublic(out, L, "helpdb", - full_help ? "get_entry_long_help" : "get_entry_short_help")) - return "Help text unavailable."; - - Lua::Push(L, command); - - if (!Lua::SafeCall(out, L, 1, 1)) - return "Help text unavailable."; - - const char *s = lua_tostring(L, -1); - if (!s) - return "Help text unavailable."; - - return s; -} - -class ViewscreenHotkeys : public dfhack_viewscreen -{ -public: - ViewscreenHotkeys(df::viewscreen *top_screen) : top_screen(top_screen) - { - hotkeys_column.multiselect = false; - hotkeys_column.auto_select = true; - hotkeys_column.setTitle("Key Binding"); - hotkeys_column.bottom_margin = 4; - hotkeys_column.allow_search = false; - - focus = Gui::getFocusString(top_screen); - populateColumns(); - } - - void populateColumns() - { - hotkeys_column.clear(); - - size_t max_key_length = 0; - for_each_(sorted_keys, [&] (const string &sym) - { if (sym.length() > max_key_length) { max_key_length = sym.length(); } }); - int padding = max_key_length + 2; - - for (size_t i = 0; i < sorted_keys.size(); i++) - { - string text = pad_string(sorted_keys[i], padding, false); - text += current_bindings[sorted_keys[i]]; - hotkeys_column.add(text, i+1); - - } - - help_start = hotkeys_column.fixWidth() + 2; - hotkeys_column.filterDisplay(); - } - - void feed(set *input) - { - if (hotkeys_column.feed(input)) - return; - - if (input->count(interface_key::LEAVESCREEN)) - { - close_hotkeys_screen(); - } - else if (input->count(interface_key::SELECT)) - { - invoke_command(hotkeys_column.highlighted_index); - } - else if (input->count(interface_key::CUSTOM_U)) - { - show_usage = !show_usage; - } - } - - void render() - { - if (Screen::isDismissed(this)) - return; - - dfhack_viewscreen::render(); - - Screen::clear(); - Screen::drawBorder(" Hotkeys "); - - hotkeys_column.display(true); - - int32_t y = gps->dimy - 3; - int32_t x = 2; - OutputHotkeyString(x, y, "Leave", "Esc"); - - x += 3; - OutputHotkeyString(x, y, "Invoke", "Enter or Hotkey"); - - x += 3; - OutputToggleString(x, y, "Show Usage", "u", show_usage); - - y = gps->dimy - 4; - x = 2; - OutputHotkeyString(x, y, focus.c_str(), "Context", false, help_start, COLOR_WHITE, COLOR_BROWN); - - if (sorted_keys.size() == 0) - return; - - y = 2; - x = help_start; - - auto width = gps->dimx - help_start - 2; - vector parts; - Core::cheap_tokenise(current_bindings[sorted_keys[hotkeys_column.highlighted_index]], parts); - if(parts.size() == 0) - return; - - string first = parts[0]; - parts.erase(parts.begin()); - - if (first[0] == '#') - return; - - OutputString(COLOR_BROWN, x, y, "Help", true, help_start); - string help_text = get_help(first, show_usage); - vector lines; - split_string(&lines, help_text, "\n"); - for (auto it = lines.begin(); it != lines.end() && y < gps->dimy - 4; it++) - { - auto wrapped_lines = wrapString(*it, width); - for (auto wit = wrapped_lines.begin(); wit != wrapped_lines.end() && y < gps->dimy - 4; wit++) - { - OutputString(COLOR_WHITE, x, y, *wit, true, help_start); - } - } - } - - virtual std::string getFocusString() - { - return "viewscreen_hotkeys"; - } - -private: - ListColumn hotkeys_column; - df::viewscreen *top_screen; - string focus; - - int32_t help_start; - - void resize(int32_t x, int32_t y) - { - dfhack_viewscreen::resize(x, y); - hotkeys_column.resize(); - } - - static vector wrapString(string str, int width) - { - vector result; - string excess; - if (int(str.length()) > width) - { - auto cut_space = str.rfind(' ', width-1); - int excess_start; - if (cut_space == string::npos) - { - cut_space = width-1; - excess_start = cut_space; - } - else - { - excess_start = cut_space + 1; - } - - string line = str.substr(0, cut_space); - excess = str.substr(excess_start); - result.push_back(line); - auto excess_lines = wrapString(excess, width); - result.insert(result.end(), excess_lines.begin(), excess_lines.end()); - } - else - { - result.push_back(str); - } - - return result; - } -}; - - static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { - if (parameters.empty()) - { - if (Maps::IsValid()) - { - auto top_screen = Core::getTopViewscreen(); - if (Gui::getFocusString(top_screen) != "dfhack/viewscreen_hotkeys") - { - find_active_keybindings(top_screen); - Screen::show(dts::make_unique(top_screen), plugin_self); - } - } - } - else - { - auto cmd = parameters[0][0]; - if (cmd == 'v') - { - out << "Hotkeys" << endl << "Version: " << PLUGIN_VERSION << endl; - } - else if (cmd == 'i') - { - int index; - stringstream index_raw(parameters[1]); - index_raw >> index; - invoke_command(index); - } - else - { - return CR_WRONG_USAGE; - } - } + if (parameters.size() != 2 || parameters[0] != "invoke") + return CR_WRONG_USAGE; - return CR_OK; + int index; + std::stringstream index_raw(parameters[1]); + index_raw >> index; + return invoke_command(index) ? CR_OK : CR_WRONG_USAGE; } static int getHotkeys(lua_State *L) { @@ -364,32 +154,11 @@ DFHACK_PLUGIN_LUA_COMMANDS { DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { - if (!gps) - out.printerr("Could not insert hotkeys hooks!\n"); - commands.push_back( PluginCommand( "hotkeys", - "Show all dfhack keybindings in current context.", + "Invoke hotkeys from the interactive menu.", hotkeys_cmd)); return CR_OK; } - -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - return CR_OK; -} - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - sorted_keys.clear(); - break; - default: - break; - } - - return CR_OK; -} From 66d3409a655b38fa31462762742ac8c9a2be5806 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 10:01:49 -0800 Subject: [PATCH 070/161] solve concurrency issues --- plugins/hotkeys.cpp | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 70f6a2f98..930ffa362 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -5,11 +5,16 @@ #include "modules/Gui.h" #include "modules/Screen.h" +#include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" DFHACK_PLUGIN("hotkeys"); +namespace DFHack { + DBG_DECLARE(hotkeys, log, DebugCategory::LINFO); +} + using std::map; using std::string; using std::vector; @@ -39,7 +44,9 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v current_bindings[sym] = cmdline; sorted_keys.push_back(sym); string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; - Core::getInstance().AddKeyBinding(keyspec, "hotkeys invoke " + int_to_string(sorted_keys.size() - 1)); + string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1); + DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str()); + Core::getInstance().AddKeyBinding(keyspec, binding); } static void find_active_keybindings(df::viewscreen *screen) @@ -101,31 +108,28 @@ static void find_active_keybindings(df::viewscreen *screen) } } -static bool close_hotkeys_screen() +static bool invoke_command(color_ostream &out, const size_t index) { auto screen = Core::getTopViewscreen(); - if (Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING) + if (sorted_keys.size() <= index || + Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING) return false; - Screen::dismiss(Core::getTopViewscreen()); + auto cmd = current_bindings[sorted_keys[index]]; + DEBUG(log).print("invoking command: '%s'\n", cmd.c_str()); + + { + Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP); + Core::getInstance().runCommand(out, cmd); + } + + Screen::dismiss(screen); std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym){ Core::getInstance().ClearKeyBindings(sym + "@" + MENU_SCREEN_FOCUS_STRING); }); sorted_keys.clear(); - return true; -} - -static bool invoke_command(const size_t index) -{ - if (sorted_keys.size() <= index) - return false; - auto cmd = current_bindings[sorted_keys[index]]; - if (close_hotkeys_screen()) { - Core::getInstance().setHotkeyCmd(cmd); - return true; - } - return false; + return true; } static command_result hotkeys_cmd(color_ostream &out, vector & parameters) @@ -133,10 +137,12 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete if (parameters.size() != 2 || parameters[0] != "invoke") return CR_WRONG_USAGE; + CoreSuspender guard; + int index; std::stringstream index_raw(parameters[1]); index_raw >> index; - return invoke_command(index) ? CR_OK : CR_WRONG_USAGE; + return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE; } static int getHotkeys(lua_State *L) { From 234919ffe108ae0acd0c3cfe63fb6f53c3bae0b3 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 11:36:15 -0800 Subject: [PATCH 071/161] replace hotkeys keybinding with menu keybinding --- data/init/dfhack.keybindings.init | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index fd02635e2..697641ab1 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -12,9 +12,8 @@ keybinding add ` gui/launcher keybinding add Ctrl-Shift-D gui/launcher -# show all current key bindings -keybinding add Ctrl-F1 hotkeys -keybinding add Alt-F1 hotkeys +# show hotkey popup menu +keybinding add Ctrl-Shift-C "overlay trigger hotkeys.menu" # on-screen keyboard keybinding add Ctrl-Shift-K gui/cp437-table From 2b73d6e8e93b1c3b067b0afe45378a3f690c51d6 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 12:18:08 -0800 Subject: [PATCH 072/161] allow hotkeys to be invoked as a hotkey also ensure keybindings are always cleaned up --- plugins/hotkeys.cpp | 24 ++++++++++++++++++------ plugins/lua/hotkeys.lua | 4 ++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 930ffa362..e275c637a 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -124,11 +124,6 @@ static bool invoke_command(color_ostream &out, const size_t index) } Screen::dismiss(screen); - std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym){ - Core::getInstance().ClearKeyBindings(sym + "@" + MENU_SCREEN_FOCUS_STRING); - }); - sorted_keys.clear(); - return true; } @@ -152,11 +147,27 @@ static int getHotkeys(lua_State *L) { return 2; } +static int cleanupHotkeys(lua_State *) { + std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { + string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; + DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); + Core::getInstance().ClearKeyBindings(keyspec); + }); + sorted_keys.clear(); + current_bindings.clear(); + return 0; +} + DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(getHotkeys), + DFHACK_LUA_COMMAND(cleanupHotkeys), DFHACK_LUA_END }; +// allow "hotkeys" to be invoked as a hotkey from any screen +static bool hotkeys_anywhere(df::viewscreen *) { + return true; +} DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { @@ -164,7 +175,8 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector Date: Mon, 7 Nov 2022 13:58:39 -0800 Subject: [PATCH 073/161] use a more natural ordering for modifier keys --- plugins/hotkeys.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index e275c637a..3cc2490b7 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -71,16 +71,16 @@ static void find_active_keybindings(df::viewscreen *screen) auto current_focus = Gui::getFocusString(screen); for (int shifted = 0; shifted < 2; shifted++) { - for (int ctrl = 0; ctrl < 2; ctrl++) + for (int alt = 0; alt < 2; alt++) { - for (int alt = 0; alt < 2; alt++) + for (int ctrl = 0; ctrl < 2; ctrl++) { for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { string sym; - if (shifted) sym += "Shift-"; if (ctrl) sym += "Ctrl-"; if (alt) sym += "Alt-"; + if (shifted) sym += "Shift-"; sym += *it; auto list = Core::getInstance().ListKeyBindings(sym); From 1fc30493c0eae4b3c7943d2a35b393ec65e9df4a Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 7 Nov 2022 13:59:10 -0800 Subject: [PATCH 074/161] right align hotkeys for list items and combine hotkeys for identical commands and don't hide the menu until the mouse has left the frame and start the widget one tile closer to the edge so the mouse is already on the list instead of on the frame --- plugins/lua/hotkeys.lua | 98 +++++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 95ccfbe1f..bd88fdd89 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -11,7 +11,7 @@ local widgets = require('gui.widgets') HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget) HotspotMenuWidget.ATTRS{ - default_pos={x=1,y=2}, + default_pos={x=1,y=3}, hotspot=true, viewscreens={'dwarfmode'}, overlay_onupdate_max_freq_seconds=0, @@ -48,8 +48,8 @@ OVERLAY_WIDGETS = {menu=HotspotMenuWidget} -- ---------- -- local ARROW = string.char(26) -local MENU_WIDTH = 42 -local MENU_HEIGHT = 12 +local MAX_LIST_WIDTH = 45 +local MAX_LIST_HEIGHT = 15 MenuScreen = defclass(MenuScreen, gui.Screen) MenuScreen.ATTRS{ @@ -59,32 +59,90 @@ MenuScreen.ATTRS{ bindings=DEFAULT_NIL, } +-- get a map from the binding string to a list of hotkey strings that all +-- point to that binding +local function get_bindings_to_hotkeys(hotkeys, bindings) + local bindings_to_hotkeys = {} + for _,hotkey in ipairs(hotkeys) do + local binding = bindings[hotkey] + table.insert(ensure_key(bindings_to_hotkeys, binding), hotkey) + end + return bindings_to_hotkeys +end + +-- number of non-text tiles: icon, space, space between cmd and hk, scrollbar +local LIST_BUFFER = 2 + 1 + 1 + +local function get_choices(hotkeys, bindings, is_inverted) + local choices, max_width, seen = {}, 0, {} + local bindings_to_hotkeys = get_bindings_to_hotkeys(hotkeys, bindings) + + -- build list choices + for _,hotkey in ipairs(hotkeys) do + local command = bindings[hotkey] + if seen[command] then goto continue end + seen[command] = true + local hk_width, tokens = 0, {} + for _,hk in ipairs(bindings_to_hotkeys[command]) do + if hk_width ~= 0 then + table.insert(tokens, ', ') + hk_width = hk_width + 2 + end + table.insert(tokens, {text=hk, pen=COLOR_LIGHTGREEN}) + hk_width = hk_width + #hk + end + local command_str = command + if hk_width + #command + LIST_BUFFER > MAX_LIST_WIDTH then + local max_command_len = MAX_LIST_WIDTH - hk_width - LIST_BUFFER + command_str = command:sub(1, max_command_len - 3) .. '...' + end + table.insert(tokens, 1, {text=command_str}) + local choice = {icon=ARROW, command=command, text=tokens, + hk_width=hk_width} + max_width = math.max(max_width, hk_width + #command_str + LIST_BUFFER) + table.insert(choices, is_inverted and 1 or #choices + 1, choice) + ::continue:: + end + + -- adjust width of command fields so the hotkey tokens are right justified + for _,choice in ipairs(choices) do + local command_token = choice.text[1] + command_token.width = max_width - choice.hk_width - 3 + end + + return choices, max_width +end + function MenuScreen:init() self.mouseover = false - local list_frame = copyall(self.hotspot_frame) - list_frame.w = MENU_WIDTH - list_frame.h = MENU_HEIGHT + local choices,list_width = get_choices(self.hotkeys, self.bindings, + self.hotspot_frame.b) - local help_frame = {w=MENU_WIDTH, l=list_frame.l, r=list_frame.r} + local list_frame = copyall(self.hotspot_frame) + list_frame.w = list_width + 2 + list_frame.h = math.min(#choices, MAX_LIST_HEIGHT) + 2 if list_frame.t then - help_frame.t = list_frame.t + MENU_HEIGHT + 1 + list_frame.t = math.max(0, list_frame.t - 1) else - help_frame.b = list_frame.b + MENU_HEIGHT + 1 + list_frame.b = math.max(0, list_frame.b - 1) + end + if list_frame.l then + list_frame.l = math.max(0, list_frame.l - 1) + else + list_frame.r = math.max(0, list_frame.r - 1) end - local bindings = self.bindings - local choices = {} - for _,hotkey in ipairs(self.hotkeys) do - local command = bindings[hotkey] - local choice_text = command .. (' (%s)'):format(hotkey) - local choice = { - icon=ARROW, text=choice_text, command=command} - table.insert(choices, list_frame.b and 1 or #choices + 1, choice) + local help_frame = {w=list_frame.w, l=list_frame.l, r=list_frame.r} + if list_frame.t then + help_frame.t = list_frame.t + list_frame.h + 1 + else + help_frame.b = list_frame.b + list_frame.h + 1 end self:addviews{ widgets.ResizingPanel{ + view_id='list_panel', autoarrange_subviews=true, frame=list_frame, frame_style=gui.GREY_LINE_FRAME, @@ -164,6 +222,7 @@ function MenuScreen:onRenderFrame(dc, rect) end function MenuScreen:onRenderBody(dc) + local panel = self.subviews.list_panel local list = self.subviews.list local idx = list:getIdxUnderMouse() if idx and idx ~= self.last_mouse_idx then @@ -173,8 +232,9 @@ function MenuScreen:onRenderBody(dc) list:setSelected(idx) self.mouseover = true self.last_mouse_idx = idx - elseif not idx and self.mouseover then - -- once the mouse has entered the list area, leaving it again should + elseif not panel:getMousePos(gui.ViewRect{rect=panel.frame_rect}) + and self.mouseover then + -- once the mouse has entered the list area, leaving the frame should -- close the menu screen self:dismiss() end From de20603080a0c7ba66e4599c2e8c20263f2071e6 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 9 Nov 2022 12:16:44 -0800 Subject: [PATCH 075/161] implement CLI commands --- plugins/hotkeys.cpp | 143 +++++++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 62 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 3cc2490b7..de475ea46 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -23,11 +23,12 @@ using namespace DFHack; static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; +static bool valid = false; // whether the following two vars contain valid data +static string current_focus; static map current_bindings; static vector sorted_keys; -static bool can_invoke(const string &cmdline, df::viewscreen *screen) -{ +static bool can_invoke(const string &cmdline, df::viewscreen *screen) { vector cmd_parts; split_string(&cmd_parts, cmdline, " "); if (toLower(cmd_parts[0]) == "hotkeys") @@ -36,8 +37,21 @@ static bool can_invoke(const string &cmdline, df::viewscreen *screen) return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen); } -static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen) -{ +static int cleanupHotkeys(lua_State *) { + DEBUG(log).print("cleaning up old stub keybindings for %s\n", current_focus.c_str()); + std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { + string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; + DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); + Core::getInstance().ClearKeyBindings(keyspec); + }); + valid = false; + current_focus = ""; + sorted_keys.clear(); + current_bindings.clear(); + return 0; +} + +static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen) { if (!can_invoke(cmdline, screen)) return; @@ -49,34 +63,28 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v Core::getInstance().AddKeyBinding(keyspec, binding); } -static void find_active_keybindings(df::viewscreen *screen) -{ - current_bindings.clear(); - sorted_keys.clear(); +static void find_active_keybindings(df::viewscreen *screen) { + DEBUG(log).print("scanning for active keybindings\n"); + if (valid) + cleanupHotkeys(NULL); vector valid_keys; - for (char c = 'A'; c <= 'Z'; c++) - { + for (char c = 'A'; c <= 'Z'; c++) { valid_keys.push_back(string(&c, 1)); } - for (int i = 1; i < 10; i++) - { + for (int i = 1; i < 10; i++) { valid_keys.push_back("F" + int_to_string(i)); } valid_keys.push_back("`"); - auto current_focus = Gui::getFocusString(screen); - for (int shifted = 0; shifted < 2; shifted++) - { - for (int alt = 0; alt < 2; alt++) - { - for (int ctrl = 0; ctrl < 2; ctrl++) - { - for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) - { + current_focus = Gui::getFocusString(screen); + for (int shifted = 0; shifted < 2; shifted++) { + for (int alt = 0; alt < 2; alt++) { + for (int ctrl = 0; ctrl < 2; ctrl++) { + for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) { string sym; if (ctrl) sym += "Ctrl-"; if (alt) sym += "Alt-"; @@ -84,19 +92,15 @@ static void find_active_keybindings(df::viewscreen *screen) sym += *it; auto list = Core::getInstance().ListKeyBindings(sym); - for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) - { - if (invoke_cmd->find(":") == string::npos) - { + for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { + if (invoke_cmd->find(":") == string::npos) { add_binding_if_valid(sym, *invoke_cmd, screen); } - else - { + else { vector tokens; split_string(&tokens, *invoke_cmd, ":"); string focus = tokens[0].substr(1); - if (prefix_matches(focus, current_focus)) - { + if (prefix_matches(focus, current_focus)) { auto cmdline = trim(tokens[1]); add_binding_if_valid(sym, cmdline, screen); } @@ -106,10 +110,40 @@ static void find_active_keybindings(df::viewscreen *screen) } } } + + valid = true; +} + +static int getHotkeys(lua_State *L) { + find_active_keybindings(Gui::getCurViewscreen(true)); + Lua::PushVector(L, sorted_keys); + Lua::Push(L, current_bindings); + return 2; +} + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(getHotkeys), + DFHACK_LUA_COMMAND(cleanupHotkeys), + DFHACK_LUA_END +}; + +static void list(color_ostream &out) { + DEBUG(log).print("listing active hotkeys\n"); + bool was_valid = valid; + if (!valid) + find_active_keybindings(Gui::getCurViewscreen(true)); + + out.print("Valid keybindings for the current screen (%s)\n", + current_focus.c_str()); + std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) { + out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str()); + }); + + if (!was_valid) + cleanupHotkeys(NULL); } -static bool invoke_command(color_ostream &out, const size_t index) -{ +static bool invoke_command(color_ostream &out, const size_t index) { auto screen = Core::getTopViewscreen(); if (sorted_keys.size() <= index || Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING) @@ -127,50 +161,35 @@ static bool invoke_command(color_ostream &out, const size_t index) return true; } -static command_result hotkeys_cmd(color_ostream &out, vector & parameters) -{ +static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { + if (!parameters.size()) { + static const string invokeOverlayCmd = "overlay trigger hotkeys.menu"; + DEBUG(log).print("invoking command: '%s'\n", invokeOverlayCmd.c_str()); + return Core::getInstance().runCommand(out, invokeOverlayCmd); + } + + if (parameters[0] == "list") { + list(out); + return CR_OK; + } + if (parameters.size() != 2 || parameters[0] != "invoke") return CR_WRONG_USAGE; CoreSuspender guard; - int index; - std::stringstream index_raw(parameters[1]); - index_raw >> index; + int index = string_to_int(parameters[1], -1); + if (index < 0) + return CR_WRONG_USAGE; return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE; } -static int getHotkeys(lua_State *L) { - find_active_keybindings(Gui::getCurViewscreen(true)); - Lua::PushVector(L, sorted_keys); - Lua::Push(L, current_bindings); - return 2; -} - -static int cleanupHotkeys(lua_State *) { - std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) { - string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; - DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str()); - Core::getInstance().ClearKeyBindings(keyspec); - }); - sorted_keys.clear(); - current_bindings.clear(); - return 0; -} - -DFHACK_PLUGIN_LUA_COMMANDS { - DFHACK_LUA_COMMAND(getHotkeys), - DFHACK_LUA_COMMAND(cleanupHotkeys), - DFHACK_LUA_END -}; - // allow "hotkeys" to be invoked as a hotkey from any screen static bool hotkeys_anywhere(df::viewscreen *) { return true; } -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back( PluginCommand( "hotkeys", From a2efc41fef3fa9ed4c99619286b889a2a1cf654b Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 9 Nov 2022 14:36:23 -0800 Subject: [PATCH 076/161] use new anywhere hotkey and filter out own hotkey --- plugins/hotkeys.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index de475ea46..72f2e0111 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -21,6 +21,7 @@ using std::vector; using namespace DFHack; +static const string INVOKE_MENU_COMMAND = "overlay trigger hotkeys.menu"; static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; static bool valid = false; // whether the following two vars contain valid data @@ -55,6 +56,11 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v if (!can_invoke(cmdline, screen)) return; + if (cmdline == INVOKE_MENU_COMMAND) { + DEBUG(log).print("filtering out hotkey menu keybinding\n"); + return; + } + current_bindings[sym] = cmdline; sorted_keys.push_back(sym); string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING; @@ -163,9 +169,8 @@ static bool invoke_command(color_ostream &out, const size_t index) { static command_result hotkeys_cmd(color_ostream &out, vector & parameters) { if (!parameters.size()) { - static const string invokeOverlayCmd = "overlay trigger hotkeys.menu"; - DEBUG(log).print("invoking command: '%s'\n", invokeOverlayCmd.c_str()); - return Core::getInstance().runCommand(out, invokeOverlayCmd); + DEBUG(log).print("invoking command: '%s'\n", INVOKE_MENU_COMMAND.c_str()); + return Core::getInstance().runCommand(out, INVOKE_MENU_COMMAND ); } if (parameters[0] == "list") { @@ -184,18 +189,13 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE; } -// allow "hotkeys" to be invoked as a hotkey from any screen -static bool hotkeys_anywhere(df::viewscreen *) { - return true; -} - DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back( PluginCommand( "hotkeys", "Invoke hotkeys from the interactive menu.", hotkeys_cmd, - hotkeys_anywhere)); + Gui::anywhere_hotkey)); return CR_OK; } From 47d7c477b39edb30a414841d110391618bc811d4 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 9 Nov 2022 17:49:55 -0800 Subject: [PATCH 077/161] show menu hotkey for list but not on the menu --- plugins/hotkeys.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 72f2e0111..77db827bd 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -52,11 +52,11 @@ static int cleanupHotkeys(lua_State *) { return 0; } -static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen) { +static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen, bool filtermenu) { if (!can_invoke(cmdline, screen)) return; - if (cmdline == INVOKE_MENU_COMMAND) { + if (filtermenu && cmdline == INVOKE_MENU_COMMAND) { DEBUG(log).print("filtering out hotkey menu keybinding\n"); return; } @@ -69,7 +69,7 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v Core::getInstance().AddKeyBinding(keyspec, binding); } -static void find_active_keybindings(df::viewscreen *screen) { +static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) { DEBUG(log).print("scanning for active keybindings\n"); if (valid) cleanupHotkeys(NULL); @@ -100,7 +100,7 @@ static void find_active_keybindings(df::viewscreen *screen) { auto list = Core::getInstance().ListKeyBindings(sym); for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { if (invoke_cmd->find(":") == string::npos) { - add_binding_if_valid(sym, *invoke_cmd, screen); + add_binding_if_valid(sym, *invoke_cmd, screen, filtermenu); } else { vector tokens; @@ -108,7 +108,7 @@ static void find_active_keybindings(df::viewscreen *screen) { string focus = tokens[0].substr(1); if (prefix_matches(focus, current_focus)) { auto cmdline = trim(tokens[1]); - add_binding_if_valid(sym, cmdline, screen); + add_binding_if_valid(sym, cmdline, screen, filtermenu); } } } @@ -121,7 +121,7 @@ static void find_active_keybindings(df::viewscreen *screen) { } static int getHotkeys(lua_State *L) { - find_active_keybindings(Gui::getCurViewscreen(true)); + find_active_keybindings(Gui::getCurViewscreen(true), true); Lua::PushVector(L, sorted_keys); Lua::Push(L, current_bindings); return 2; @@ -137,7 +137,7 @@ static void list(color_ostream &out) { DEBUG(log).print("listing active hotkeys\n"); bool was_valid = valid; if (!valid) - find_active_keybindings(Gui::getCurViewscreen(true)); + find_active_keybindings(Gui::getCurViewscreen(true), false); out.print("Valid keybindings for the current screen (%s)\n", current_focus.c_str()); From aecc190b74f492794bff39113db62c67a22aa211 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 18:04:42 -0800 Subject: [PATCH 078/161] update hotkeys docs --- docs/images/hotkeys.png | Bin 32376 -> 0 bytes docs/plugins/hotkeys.rst | 22 ++++++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) delete mode 100644 docs/images/hotkeys.png diff --git a/docs/images/hotkeys.png b/docs/images/hotkeys.png deleted file mode 100644 index 524ce9a52f45de4e5dbdc7e0a9e0cf708ee928b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32376 zcmXVX1ytPL)AqKsNRbvP6nA%bC=_>F+zKr2?pxfU?cxr_Ve!SKxI=MY+}#~Me*gEo zIXO9*OeV?Pu`{_(go=_h`Uip!00018Rz^Y%06@?M0N#L+-@Z~-L|8$uhj$iYiedmj zRUGQ$_xG>oWTrA|iU5E&4FK>X7y!6`CH>e10NmIBfPG^CKp+_az;jG*QWbi=fM_Bw zEdhA>Z_92iguGHv9A$J|004}x|MoY14n<}FfS(J%6@!_$}ZGXvc z#?Yr6wyo=Ic`#=&4eq@fc*N(0Kg&pkPRM-J{bijCT2xRiTBn1 zbV!pR>+)G=g%`XKylrvp=#uVFj378p!oFyBFymYFB&K&FW9uks^P*c7H=}2Xa5VqV zh{xNJ$Nvk@`N_WN^70;+uiG0yJz2|S+*#_8)4}HoL7|J=4?>Md(;VA5!e5rR?ukkG zCxCtTzv8?um0vDJUrJw|jPzKNCW-5tX7~LtX&u0eiy}KNA`kahe=_s|RUH<7GZxHA zTZ&D|1d*LDOJh$CYA{Q> z&Og1PxVFo$P%QR;a=P)}3g{)L4lHiWe93Cv4-d$x_eRwV%w1^Of_mKi3>P`T z2Ol%H-`CyQc|0eHdY%NwZcY!@&qu^P?e?{85Vsrt_L&9W8)V|d+LXZJ?snt6cLd=% zhbvctFJ4F{adFfrqH7x@ugZI#6?v8gZwG{x`*rd9PsWsLwLNfGP&c`+b-o9?y1HIT z-Bl+&W&G_~1V8ceG<2syl8kVtjb_rdOGjhRwh>zFF5uz5`o-_ELPjAU~A4Wm2U!e5R{4bsfnT4?-r7Uqc{$ z{D&lRo1^@+sq8&J^gL?w0za?-?G07?c5a9?B^y0nc|32^%zCiAOG$}HnK${!4soO? z?^~+Vt{_^n^G=9i3ayjc;Hecioh!Y};5SX6d!G;!AI_vLF~T(1u^?_)0!637bCK?b zQa&^BDq}1lKCm`fJANg`K<$QS1;y?GQ42xWq5GuHp8%B(&W;)YG(T$?UAkev!HI+N zqvC#hK{0v6EXNmf2gQTV@~=d{VoXDo)Tgt_gFg+6nY&Pje__ zQIp*4?pXfIE-4{@r`xsMUN^TWg=4LPZe5atCUL3P{&f86jg!YMu6MYV(c26U`4I3Iue-!B=gvp{aN{lsUPoMgHHN5_BQl&Fn3C^ERa$#3Q=rm4@wbbxI(&8VC3E+(=@u zuFxl&gF08&JK3rkdJax*?g>8kLbv3-g=s?R*~0agZM?^jMT`Lf3xk1u(h?tAwdcHg zQm@Tn$ZP!#nbVF5MCJR4T|!ka{CSA z!L5Cxz}m2eyqNiSOibKFxwnygnC8Fokf`xLDJvi!GiW=KePKy`?w4V_$SM&4#srG~ z{LPME<9JC`YV`z*B;8WLbeH^IzCII&SBnxw23z{Kk;}G{6I-Cggy8qufFh>NTI zF|X&fp7Q>(jF6Ojwlp4uncPHseZ2x^|2>yX#-(j#o;|s9K9(?V61{uC(gh>fpT5DXEynh(Pryb0hWf zY%z^*_){(_<>iVdK=z3yF+!X`)0vJvyaRy*PI5kT451D2Pr9!TVbSE+TvS0E*}2`8 znug%gk%XiFTXCFDCS9J6r)B_8b^eOl_&FDzLiNx<`ED_{l1Br}LmG)N@rUfFc~}p# zjWM&H%ljFgNn#s#sG?q|7R*5c2``e>Z}j5N^TT|W|2J=9*lI_J$w5>JyFtma+YU9w zxv7nQi{omO(#>#=QQy1XntHVT^e7XyhtKc7Kekt6X8ypfg^dl}uC$^tWXMCSNz$Uc zymHad$SbQ81BAmqG5H)Cb9^9b7aeD_S5=*eS9OGaTKY%f=}X=K`ykYoENG$osS^)C z*GU}Ssrtlg$#fXmsf#sD)6nbKZ}p?p4T8_>fYBa~1&z&#WGxwSPn<#}gpPYHGYun< zK~tJ#hjHWn(qVf+sZ(b1slYDyd_?=B2%3LKe-B$X$Rai^u?uyt*&#W($YO5mo_5-W z5n~_Hbj7$OMh|va2w&i5op#fr6U)0vK8=TwgbTY}YoH7C%}K|sK92^by*mNKTX{c=Q3N89B&A{If9 zA_L(&?Y={JNB)ld%^QGTY10PS*Yu;apupI<=)tFoZD#qw%HNT;wh-1~6IY$u(|n$y z*1QQ{zl-lLlI3)B!^VX~rNG+eO4Mi_{rE_pQN%+x5}Dr9?RR^tsgP z?q7_50@Nmff~)$!X#}tL8R8{9j#T(rO$0V`f?|g{1W#Hnr7avYjyRURodos?!lho=eRz@^4IJGsn}{%p$HDekAhoo}d0qo6>EcL4LwH1ZFP&_~_T1)Q>^5EPz; zDCjv&TzhsoZkZt)p;K!IUl8>L{l^a-yXjnYV3krijy#SGT%U9J#8Du&Rumsy1tdLGJ+de5%TDLfm{qO7t)+tWH)y zA0HWFz-BF0BHTOeej(eJZf0_w%f6@qwoDidiZ8Pgx(3p@Z4Jl8lkugXj7%xSC@{Oy zl40kc(|!~_m4R#MlHBAG27Q^%exE9&9&};=-uy3oBCI?_)<%%t!nJ%S>*3O7;|W4} z-kyURI%(AcS(gY&Dfu>kpE?ps4@X7lR3Gv{~!l_iFmuWp#Z!8qeb3@U#CS~JfM^UY|rSPb1TqJ%? zGxFzdL$0Iv1(g27MSZHEH{+FpJY}89hhjOtblN|%&G^zI3u@XZeTlebQ9x~Bc^K;( z!3e7f-%p%!{;R`tgN>ulr(vQk`$UeWDP{MGr6E8u^y8tn+4gHthPeRnd|^K{jr321 zI($}QmbB5ywbl2@#t3)ur7o$J$Nv7R!f(F5^$;e(i?PUJs+$8XI5jP1Ndl4O#(54u z2H1Qw(lKQ2!b})?^d`#-YnBud(8})xqcKRpSxxhoP~Dq`0R5yyd&PowoatX+z)n(v@30Qf2UE)R-T) z@XgT5V{caR%`x%HInVvz%N;uS4rQJ?je}5%VRSY@>VIu|=jY&c=J$exId1W&ymB_J+@y^vRwpD>bMO5cIY4PJ zSWB1bq=dl0do5(KKbro%trPI7X8 zr-3qC+wOX_0CQutIrc(k#t$V>)lS^HU%`PdaV+sSOV19?`p(L)OSL}iT24M1oi@$-P4s*cFD4ii_+1%WQPf5QbCk!d4nk~Wros5w zi|iguHv6V+Ppx2kymBFT+o(z*|I1N7>+m<-s0YQE={qYQMP&_4CG!mjy>q+Lj?rG8 zRN#9{yrVT@;)!mIIYd*B_S(;OQxp!a8c&ay^?!e4wx|D-Y%$bYT==lcXA(UtAh_IJ z2J!JUyjnV5;5VQ6>1*K$(ru1?CcMvfV+v`cJ8U0K2PG~m#^}kH`E+eCKx06>-?n{4 znQu%A>l-jOEeZzb`x@1ig}@v2E#A2m`j{6pjy|gqG$cyYkJfHIH3%<9D`&#k=%W~q zU$0>LU!WeMGT&Vr=+c{q_CsiT`O%`{R32|Nqh^sn_g5`!Rhz_F2C2b6Q1Bmgk<{ z$?ckjpV>3pY_h~b1tM*!n{RV0ovrY}CiVgU2&96-zSL6BrZBX)Jz!oP_lhZy{f&xU z=ZR$+oiO#*KzY`*Nr}SfcGQ&v_Dq!hY6PQTl8#;RYbD1+0%S-`TTYv)OSc^DLa?XZ z|8cP{mu_JIV>r+x&bRHgL9fpsLQJ<$)~PPI*XyLb3A-9hb)bcXPT?u@jXq$RD75I@ zsF!l*K;Z)eeV;|DlKJ#p><*4Ap%;?)D|OJOJg)>2BF`Ud$Az>u4GRq%q(v_N`&AXZ z>Id^E^XFb3cpMdL{<~04{gdSLw|!k>wabz2dlhTWZmqg*rEq1M!b3cWOJ}zd)#o2F z5B%+XHYpUn?hSUeGBS+sVj^}=>w+@5?cJvL7MN(5s^|UE?mCI_SeM3i6Is{LD~beA zjT1gL)da>hDW+?9|5YoOrJw*eTuSamPJGU1P0Uz4gPeF%VEX+yj{7a7YfXJ+IdRC; z{rLQHDb@6lpOk#!IhB=j;xp?29`eb2<~Mpm^B$9jKam3qB-zxnA+&eji=&9NYff9i@s;sM}4{Q%MgD zjE)FVqua8COWlh!c?dlek5*_d`$pB!X^6w|wXr zACe+V?R5TDYNnog!m($FojVz|tjrz;bc#ttozJ^vsPD?AObI(4fCaT3O&=ZYOdUI` zCu9gFMOhg@nndgp52mhHMO_Pt_CJ?nJIfKDX4%;#tBK!-hD%i~weL9P z+0z7fwl9JRqbQH@Nd!32o9c7RuVdeeHKgJngx!bD?&=-SFProV*SFfP^jv&nXOzfG z9!Uwg$oOuKImXnvo?gmW1uIC$xd=&1fG4$S3mDbeqf5Q^jFLe18{2~%bZ!bpWuW2L z+;4PM!uxdRaGx{QAYmT&GPUz>$ba`C^KPBAoZ-0>gS zh`8;zd0wo?^fRol$#Wa4o@tbQI&)IKlk>h}1V7Wi>_(ADyKeEmY>oYMF}m}tMyjcr z@-{8S{AJ+XX#w8;=sTt?dJy(Jqt3eZamT%{Q20dpq=mm2(gV~Zx>!5QYj!xYDpOqVrg~D<(09=tABa3j@-O+E15TnOw$jFyS@_|+ij01?!%--w<%vPl}T@RcOKDN|{Kh~T{p(c2g!ExY4VqpMz4FQF8AY`-P- zw#Z%KtQ#i}j>$in5|;-?cg1nE1%2SN8{eUit(y+Mz@w6cg>?D^Ag2M|_!uEJ2Djne zzDwK#qv+%M%FDf$D62q5)~jtkuJ^GU47`2R7CAn4c)D1*kF2=gYc{&Sudw!Q_1&@X zz2jYeswVY3LpA-T{E|59dz8_B4Xb!u2@l_$G`bZwdScgtKVh$2e>COjc{v$sJD^Vr zWT;`ouc>0QBDxCE$bjq;n3>Xmwl+P@iZf!ms)Fg`yqU+CSY47331G<|#-k+|m1}?& zJP9ay8kB6!yv5ujX(=Im6^7@n?3A3;bxa8Ask1;EMtgIJbmOFp7&SS~oo^NcOkG3A zs}*;2XLK%{znb>_Buna6*6=lpJjT?l74e0!V8|!@;t~nT{(1BBd7&9?^u@&^`kV%& z_4J!juN_n;rF1-}(Z*dlnM_E;&S)Dbd^QR&WM_L-W90v&qdeiVBf2lL#Y3lviRHnyc2T;3#VVTdScYD zfeCSsF@AqTNk*kipKPS9A4Qw&VFK&1i(MK6GSE9>4MlFaK2Y(`Z&jc=dK1&Pe`-N2 zLmclo(LskK+^Ay3_cGV|(1B!-qE5yjQPPTMgI#wqeA;?tJmh zK3_7=bT}fh-BmrVi76=qXwYVjZJyvbc`uubQ7!{@nf)=6yBtbPeefk(oz$^BY4Nq> z^yK{JVLxV%j|?67_z$-r@kaAu|1f>2O=ZlX11J^*YT7 zyQpt_tngoZIVsINFcoez;jtkKH7mJ>-?YA*>%A;z52#9T1)EL0wvv2VrIQ?ikZu=X zWio$=l&qb!F$E-^t%ww<9UhVk1(vRHM-?I`pHMR%B5;iWt^P-E{t$3zK0`Q|u^7BE9Pl z0dcmYvfeZWYEu|miEgNIcegb`NF5usmKg)p(E_9=7kQM4KvhZSn#{ID4EPN>fZKAZ zB@%O*fDNC#^xstSYdoV(M9k8SRNZ*tKqn8*7K06b`f1fO`ZWu&^f0xG3jPiDXVA7J z!Mw1{*nKPR^8n8Godp{sZXIVQPGEwr^Y-t;HiFcb1TI6oln~9|=CP6ezUyygw1@U1fW)}1R<6esO7{A<^-sq!E~0z#b_-TI z7AZA5;>UYDgdB0dU{g|ce-<(cQx4MLf1U^LS{a#tP=eYgpSjm%YSJXH(;@g&0cL+-U4LWTvz6@&eXC6=w`mV1qLYQD-qvS_lPJxd`^m#G|gr>ek(`HAc zDpiXk{i3G!>q0KJG%)%DIMXV_2>(!QhC^hEW7KZ#zjA-gIc*6~kigln2u zv<8N1Kw=na+p{!j%gflyorPhG5o|-6O0j0eJ*8sVYtKr)P*H3!_2KDVCAXx z#d~YbX1ILE4)-J-JF#=@w-1})Nc#(1)O!~E>_-6v#(Z<9yYPue*21XhT=74}gYiurs%ggFG5(8aJgk>SreVv4Ff{Lh>wOTGeA9Wsq6R+U24A8CVm)HIW>nvB2x6 zRLm7I9lgQlU`;-l$kF{z)Q(%Bo5fp3Qn81hf+8Y+PXpMa+g6@vRU>B=FZFJSV!!BF zkT~4De$~4SMQl(dbU|OHqc&9pi#yzfMc2kgbqOF>jq1~d^-$B^j@&U$L3BuWhV`)5&{WrD#uTp1ph3$VRdqpM%J_$J9OILbU124 zKPAhWwo?4&iYeI-xD|wWBf#6rFx2u9Wi_T1Gz2X;IY$_Z8H$TOVQMW*%BuGW`j45g zMqGqs5#!V}W)a`!hU@<0A5-!iMzG{@^e8FFlQ0s3(He|r$5h(5l}ep4d2-d8bL*;y z;(V{(rdiHyEVI?gu{MAwi3N3dHh!t4;+}FI8BhJyZ-;t0MeOyJHnu;ZJhlM-(x|G! zt$oD#k7}>!7xF|=RST!!jJ*wMSqZl+UxTW1<8(?RGgNP8uva0uY_@a$TgDKMH7!;H zbJRiNM1q4;a8X}`?oa{Q&$ZK`vnlDq)QHI$Bb#o+3^!MAl?rHQl_qQ2krMhMO1QWl zh@L2|f9ft9Ms4(LF{o+mWy|1lBPp1sz{rjcj3}!K=2o=ZdJuj5hd}CI^tq9bjIUXF zB8WWBj}=GRb2yB4=Y&k)bS@{@rY$3+jLj!@Fz3|HBWA?B1{IH03RQ=0sFG<&MRPo` z1UEW3D%WxJ&O{R@bH8t7@Ugdy{e8`Ej)-4QcAjPEzE?ftx4-D+qx-t`2U@%ts`A+c zCVO!lS{sP%?ta@)Dgn`@<5N;#P460IiTV|sctTU(bEAuWL$)U8a%MvpNwnWu@19GS zUHYbKLZkX&SfWICo=XrEwWwLN;LKxOP?El%y1cN_W_A9OiWworPuhTsqM-~b3Dgep z8FM(AnzS3i6xSK#IMy2Irq)Pu8MMGB`!?BoISc zc;`R7jX}_=t_Ar+r5aL-5$Waa!7PR*YK&qLs%UWj9UFD?4@QNDHlKXGwUL7iVk3 zi19y?Ac8?)J{wf$;oLAG|n~1!g`D6WD{aznA6U+8&mHIi?4Z= zE)}kTxrF70+Ike#T;qutDGysxsT5;2IH>S!xhnzg;=-|u1^SV+D-*aF)=nydb)000 zX4r|2)gVP#LD>~`$3Ui<9()}wjA4oyw`e;apIStGIup|Jm&8MX%!ZL>tokR^y+@%r8#XPbs z8#swf(`H1mi&k-sgnla=NhzqYH6MP-Q?jd9?Ve)M9;SO3w%ojya|X`QasrF3&2c?< zW16z=?3{Qzd%Z(H8?Y^h2dFbbNMw$7H8>~##w$5bYjY?|zrmf>UQ*uab`r+Xob~Yg z6&{(Y3zRNMDcNHXTVdESGZ&L%=Fh9}POZlmESd;}<4LM|?tIuumx$3G&c(f~X}qZM z){TghO1=N$B43?CU(BO4ful&yvW=MSu9;Icp35!$r>Y5-?ias%~*MLW)Iu4 zAp>coc}bBbnohWJeiId_S|%-TcwM>Q}bsRsw~m}GEJf-wg4wwtSe<4dKmtn4^thFa2= zpZ;77H&kKk3YU^G`Xoh%Ez$9eP9%aNpa*z+^b-JPS74`tz8d*%~O5=wurF}Z;=moZU|Xg2D|(-$PEVYU0azyaB@p1DD& zmS0^4vIKuJqKi_Hx+-lhMVXx2VYa+!i_POWy--D3Fhv@n+5;NFB=)a-eCc=t4%nMN zAmVsBqDU)V46euZMwb$uoQy?tQsZ5&0fY zlq<)RT4}c3HZw-Pwd3o;pTKwutmK0ct0SaT9CG`HOH(1dc=*Ao}Rflu^JiOHR z8iIq2p!G~XVqQn=fIWMZ@K zJ%JM^FcI!-gi&8uvo()zeV{{Yp}!(bBuy>v&pOStt^;JYEHdYi|B1t;z8A-JqM@S$ z9O}tM18K7iIqM{{2bOaI-D5MbRSQelwZh-=DD`4etCt^APj`c?dxp5n+r2fTe)TwZ zSpq7a@-=l}2Yp5E=WI~h(1p>jkYh9_D&&r2i0W6u3m{C%4OIBA-21jnDed)dhz%-#oj;2E)VVT09lB~P<|OmvIXwa4h+@*MiLJWxKHxz;iNE_XB5bzMzxzQt_F zd{$jBHEF*kO)hnW6f5R9%w6tuQ@Q^V+~npuB7Ri__tSMK$NIjn^f{cT4nCr}Zt!n? z$h>KvLY2MHGecP-|da`x+EUiObI|+wq-M> zm0hO0v_EK7`_?%K&Aai|(mdCZ;x^F%O?hFEt4%Al25iZ!4{X~QlZlee_a?RN4a@H$ zuUrFZ%uAg?ulad6vIrIRq!rJ6lEb<~g{A9vpry965X3r9*N9^&(sj)DU81>+n@Q0h zS712{*lu`!ZxPfe&#Zi`qf9eHqpEU53nl)eppa|A6hFddpUFjcF^;wqyB0vTq%Rso z6J_O>c;SO=V{lh_v@esSxyWmGN6BMBT@qgHlL&iDa3TGEfB5=y;_VLwsu8W+i^|UV zNeyqb(mpyMM7OXIuc})$-+!{pAeGoer+FlVahQgv6}h*pGk3|7#fE{88_%Ojt~dcF zwnim*ZjGLsA?+7R3Z(^mp}4Pe$;pppUq}Wrf;v!BU3JDxZh+!L9-*B4k^UI8R#k{# zUzE+KUw|7=cfL_w=$nXPWQpunqH4K@Pc*I-{k`?!h5(H~mdSEtp3oavOFd<-D#hrK zZ|ot^%FexHaV2WtfrAb6hljJ^>8qKkQ@I+Kl24t3;{EgLwvj)|rCZHuG$&cOr~l5K z6Au|$m{MDm@Cr6{kcrl`6_3rJo)C%zl;S)@PPT=v(o$TUxf>=@vqwc%2Je>0vD|#Y zRSQ+op&>Zxuw@De(r^)oNH$^8nm%AhhkK3(&vXkEFyK@QzuWN=QzJ=@PYE&M>5j*s zk*fK!g@2X$*QlCrLz2(&G6*<%NHP=HKhKO-L8^DNzRUN8luD@sHA+=ejV3+iw72MiNx6ReXx^hLk-q0$++ zNCg%`Wmx3cPD)33iIA$wJ*z(8dI?`bItn3;ISO^s`hyj+XTZN3j}& z%EC#Vg~uxiN*p)C2P>{pakt%Ugk_5=nq@SSeJgRA5VW0){HYn)X~k&DjFcoVmr|S} zdcrDn(Yf~Bd{%2DBle28{CcGO?u*W$Rz=OZ1?+nAwp+j4OmA4^uYstx;Er`ZZ}q12 zRC!cdGc8hXLg^BCU?V*=h{j1OwN)E^a|E4%@oGE<|0o2D7f=71%3ATOB;1JScWiyq z@*kz*WjT2o9l6MyBEMWAK?TkU?k?lZP+jJ`Uup9(US*ncpJfw$uwV(h$xU9qdT->G zq6${m{?+?(c#UCc?@a!5=apdv%QI5z^$Dy*7!JC$fOKlVnaaQf$U%nqG+FKQ@uHFH z5eMf~R}z~zVFN_yg8>YUnxWOvIrk+RvwtEa>lq=+dL!agu5<{d!UWP2-AFO=0*GDm01^+;JS+l*1P(a6o$p&Z+Im1)4%5__1qSV z=CyK^stIih3a{j@RoggpI3djMqo*`4hA)$sr3KVo7PP9}CsVlw5Yp&HCQxfF@uR{< zZ7G}bN2w*IGJXkXylZno9kiD)gQ5ha`*<`Nt=!Y-DK=YeX^nANx_=`|cJHkHF1fp8 zq~PT#S2yM*z12umtnOQo@k=o6dxcJ|0wgn4NdpZov2$_XGu6k~#0drB#ColN6LK6o z5G6w+ED5EkSWHWo``*)c+~wAk66Z5mZrFT%(SH7Nne<>;qr;LkUH^4+KI@?jd@=Mt zc!k5lcM{-y*q|Z~gj?%I$!Z`^7sI``ATthh`Yw8lX!ZC8)~1-}%31tVD&bFDk%7## zx7Lixo0D4Mq$X5eRiI?7#8lWGg?jHX2)ti8!|oO zFXRe`VW2~74aLfNAw;%{IVAngLOeM&FJIW{kS@w9j(l7>k$wt3q~ssjiFTy8K>}T{ zdF3+%h+GIyuE&KW?j%HkDH8AZdPX(&tXu2r+X8bP5G#j?Xl?l?R{A4YEq!v{C zo8Wy+4c`33VQyU;iL7`(G3=*#@KnKp`}$HcezRKe2zDe-gcaB1x`xhDBQw&0{mD$P zohaw!dVMu)LS3>{TmjIBhB>D})F$MW&=cP4n z3Nb{7OPwe`(guv~fDGVU9IPl9^y%F0HaKTQ-(nosxuCE=UYRnX|YzXs4zagz@HD)LGyj@ zi_PsB_+l{-0O{DEluLbSd;1*49OO3u3tcqL_ge-~bjE2~DYGf40|qxTU{3U&WxFji zOJi52#10~se7tlAxJY>M^Kd;07&;k?9nkgx++XATfqRa z2ilZ>1OP1LF}zRG(=7OQS}qI~fl#y@40mzu9@%XSo;nl z)@Q+G-$26;Q&Adv1<}0@#~~-rwa&-Y({gE18tT*(QX(QE!_yM$5|9qZFHFoTL}35_ z7=QRdg77|KV4QegO6Ls+T?7Sn$zl91My!^jO-(^mpnjci0AP-B8!7DKt|P;PL?y$- zTcjo^=$o=xK?z7^uXtn%?G<bj>%hv?0P)60=V&UdZKKD)ycK)`0 z@JX=0dIoixeN|1DUOmEJ{=64%qq~Fwt-m*@0KjM{164_S&sDJLfr6k&oxWhi8g1KL zgh|(rcYo;_WqkKuy|sX{E5bZH!n6J?h_~l&UFpd8?*G2??=z;wPmK$iB@3eMxu_?m#8KUYRd?LM zk3N3w#VE8>tAnPb^G(E-VvhC+E84q9Qg1?TN!|ed((iSYF~~BgQ7jn_Hp>WbdMdAc zpZgSw4j3&XTDrR-eiex^O*Ts{OU0<5Mw=Sp{;lQ>!us(m!{57K5wCAKT0j&J7bmBo zN&Et&Q|Rm-?!yf+ooS98G2ZdtoFqLpe*eaJT`D5o@`DKXf*)TD^%xP%M6HA$%c5h#?OZk|sm25ShvYc|s zDIh|zh8_iY49;3@XPRNdcAMQ4#yVaHf0EyUZ~M&ShmF#A06?KasZ0hR?ypWf35fy; zDhdi`moSJPh)=>D4dCuZ`ojYMHQtH6B~b0J&6eWd?dGSw@Dh_SH#4VLPZtF7dzQw% z;y`_0KX5hrkA$4Y%mf?r*{I;oQb&iFZ(o0l2mAqA?|OF*qG;O5^tnf+%&4!ke7rq6 zuC z8wZ&+E8E#Bg5&4`N1mB+{w(Vdnl-$a$wBZGtJ6iYu`G0RZEXoy1m6y)b~HD0nGPyCZb3$=_2K$je@kg z7R2oAy9$Rk1ysTt2eB@kFEtx0>6Y!2+uiqi{SA8?`+^p;^T*oY>yH-A&qj?ej1Mk; ztk&U5bGv+&)&gO2 zPU39}uJkdB`#cjAL7H&()`mT=^trKTcR^mA< z)0M0y=k+G2#l??qZ4vt6d2gCIH!EsHO%LMspo)uor9Wfp1RGvV@2#RX47>~r;!44W znwcH~nQmT_{)?9aDaT{Px_xi>^3z;~Ili#^^H5-$luxK&bE0xW==ZL@kZ2|T7I0wF z9!m6V?NyfA*;#8W9$10w?*^SyW!huo;2+$UmoY-|mNRsK?l8?hD=b zjUZR>F_Vqnkdjx@s5sfi{qd;5Ue4tFvC_iI)I!t66Ml;l4^}*2DH0hQ;-&kEHA0TV z^jutwtEiX-H)?C#xluoHNR7KFbaIwa<)>-qeX_7is_rRMR-Y2sev16gDc_xDII$tq zft*~BoN}UhaUdfh(qi(uZ)S|QYScZhOSZm!ySS15$v6Gr&5s)A14>i1ocureg_M1$ z1%LC@LaWi%dSiNG{`?{2#hrB^3dsgFn(`;d#RgF_+VQ!qrbokWrlJQ@{au$OM3wvh zaN9Kr3dIE-*}6{D|MW18h(x^k-_t}L@Q$2tdRM~**}Y6ukfPuJy@E;hrSr5tYirEo zrkejtg^jvfIhTy!>RR77AdU?UUL$$9HDz?*v)K@A)*aV#S3ndWuzzrZyMQ#G8F~^G zH9$$>i*KfBxuXp^fdBH>V&-+_OjUIXWe_QD&9T{xd6W;xB zp&(^vJ9+Zz&_Oh|p}Ab-ZMi&Y*={y;bGt)cr0B(f^Y~n@+%MqWI_rdTEH${tlf(~l z#1Bz>Uld^b{p#;T_-aJ0BK#RZTy3%Qo>~FGw^w03Ecm*7!>rp}LQ3SdJ#Et4U5u(OG^aVx5(G>FuEnq-VB{xziYUEN})bQ9&F7W z#m=@O9tm;R+VC`bcufQVKI-J~`37T#aPHAL3Uks8_i1sQ7zqVexrhXn8c)GBp9yZ4 z2f~cVE=OYp2PuIjblsOdz)(Q6r`&7520CE7a!- zg;4a572AX)cGP!h%^W+&MCS6Xo=|=*J2iYXe?Gv&J3buK7W%qlc2zIp1V>t&pK3@h zjL1})K8{MRfDSn}oX?^akpm!uMJyZorvy5zI-^ZF= zc7yEul6_4Gh2eY0OYhI)_s4I_L*4s)p69*idCqyBeF}cqodXN!z?TStH*q2Dk?k9~ z2a5UB819U?uQ=>8BNsO9wo;D?njHg*CT~d?RkV$#%X0y90TedN zK~AD4leBe@1g#&pE9OvKJ(MG*ny$v9>iO@}caX;0uPCPnn>g{R+l`iq9e-t2^3naU zcWvZ7)Ws8>t=yA10^u;cepo^+))~73@{V_5p?xRT=mVDDQT$k|?#}1Dn`=Zo%-w*R z3he++=BBf*=3msbdZV%;m1`U#PQ%{?%G=Z#biAW;f7iKc*wM(70vZxz)$RTBz6C|{ zq}y$VZ4S^{5+-AK=3z*;fc&d`pO?i4VbOx@);R(jq;*uWHen zf_j-=I++6=JeHHS#-lm#x&u$a84a6St`o;z9H2oEINcKCEPT>j@c1dujnKq(k8Ijv zc$inOVf(6$k7DmaX^+`ZQcE&nWd=e3Pz{X$`#S!&;cY<>NJxkpv?E7W#3$@!qg;hD3J=K!l5GSWBOMt`&1fS$hSm)x`61laB9ULazR>kfb)QDRlKc z(U~7+6;cp9)~4O$O7^K~RmzFl=QLWy%n?yMy}_2t#zckpsYh7a?Pem$Ml83t-fYSD zE=2V0v<4PU|1kSKpL9Y3yaZsIP+#9$ls@Zn4jj^~|6>yrnFxor;wa8%Xaj~sgZ6`d zR|LIDY6FH65LD4nA8&u=3;F|ONLGbMy@Wz_6Qs_s@lk$XVnmonRX@5~kB3Q6QoMwa z>u+ggEj17z4dWm3DhIw;1>Iex<$qtF;BX$AZRfE=1*HSdVC7^gsu>$37Fy(Dg zi$n7nIe0Ihg)djrO*8o<0?6DVvQR`I=+6Zr>|8jWmxxAw^ypovAAVKZLC7|cBJ7(U z!>;no$p*`*8JPO_IN_+Vx&)46Y5*Lh2a2ODOU)sdN(@Z{;(p}dCCXO=hkXmB>7|YS z7L+p{{xdzhK5}*rW1gF-uNqnzj36_^u6>K*#Q>3tb7?ZI%$j-vfDRKl|8cfby$)uh z%7B!HBqoF>V#A+h3xzeih8*mwKRK}zudfkg!~rBUA8!dCEc;|XmYiDl$y!-jj|F6| z0~XOyv`GxpaI!9Ao3hw-xnNf;KE@< z7@FXjvY7HgU^qebed9*2?e>FBis^Hg!RJf@wK(8EU@-Sqz4xrdY6`@HCVsIqbf_5a zuW@r+-PtD0a1ininAil53Vvr?AGp1NV>)ia{{)>ZQk+)FoHa#kjun~KZor`i{@;7K zuTqmOgjnAZ!|$Oy=|@9}i)B4)4m(FS-%0LqFdY-Xgp}5fsqY5hCRt3N(AJa0D3lJF z1{CeyF~@30v!n7=eHAwGC4RU2U`&$FQaBVZ7G1Kc1Gl3#fm+OSP3gx_tQu6bNM^nL zz`ntahW1+2HJCg5f)%YB{}^5HgLKmbUbgt;c%|SB_vH>}vC1c1DIu?Nr+dsKEa5m2 zDZYzGVuOf(3H0z-?0&1p%`lI>e;Zs%T+uaG!vX&`Hi!`6mcVghf&L;_gkXc#mcVV)raDE30R#$kf>z zH`!480I61T6K_x3sE~_;Iux{6znaFGD`qlq_dGZ$f4^S3bPu=*0M2xKVl2Sb31Ppx z#7U$MgY{S|%c-eh^JIn7dM-vBs*t}`oybZ|fWV1hFwauSx2lPd5XzQ#Hy3NeMU-a5 zvf705_r*SwZEv0#VN0@ib3juefGk7J5C{;5m72o{6-Wz41~g6KrVVSeD20e$XI|5D zb15*}rLnUF+jP_1*AFGCbXWwfsTLE=+LLp0-3+R_cvE6*Z<% zleL@gY0Yb4eUoN?=27&`mbe-Xnc{)~wU}mtJKK2Ohd_e~COje-JMiEc)>WlcltpNS zvLHHms-Ih9M7F*CELN;06Wc!BjFa>o(t2(A+yl$$(>}zm*;!zFOL{yNR}ix4kdHYt zQ(qE3VhVkSe}`lqqF}gDdL#IXX+;#$c*B{HH?d|afO??#|m=?{E|1o8Ar zkj+UnqDu>Kk})@)pB|FsZNMKGnVqtMD|n|(n$`HwIr$Q$F1_Ak@O_#=Hp@ZB0t!*_ zCG~#N?HqPNJOrofSy@@kvCQvXupE|%2Q{wzC$v&tuMDF^f_xluKxa<=PUg!dQ|xcoq37{Go5!I;Kl&T`tmW{Oc;RJD2%!|Vmk*w0_dp(|NL3RDheQ8d})NW7t^XwtB zIbNSb&vunnzeXm)F(xDF!fV-MrZzGieJ5Gl7ss@|+k1HHpWj~>7O!Yl+$g{OB!}UA z+D#}CytP>wd z`0bvDAdw{ynN0EQdtua|(ofg8Sj=;>8B=Z9jR*bY#oKK;b`){^m4O|JwQjY^KUNhd zy><5afr$j8^mYmcP&BP+312HMR*gGX#Fa5fZ#ii~c58mV?j?z5w8>Gn%8k5uo~(}! zUs}|qZCG$(qZF2#&d*KQD{Eu+8*ESTaK_{AveJN313AURhEA|BSoER+;TQ?yW>1nQ zJ$!#AVpQ@(&I)%^a%gjBsSf?iHyiXu8b8nLGdD89Amdm5^#OsC1YbpH_^$+@?+pHf zcD0+!(UQt+#SjZg@FC=^@_PO5xbr>7t51Nt{JN}AnOVHW)kQ3*zXiMy09uW?I?72| zHHBU7`7pEzL@}~4{5*80x~nsBsp5USDbHB`bW2SC5;*xjI{6R}LqmlEL1JZbX6w2D zwGv6D#UygQ&EO1yC*5bilm7+)z>)W65s5Os=QTW%Ha_X`LBx@xqGYAp$lq=#5OVE5 zXEI1c6@} z(r~Xpm$_~I>xdj*UkxV_OrBF7Iij+ZU}QAJSC>NVB~fb`l$qkSC&Te+Da*LQ-*?r* z>9bCQvmS$=)QkdG|I^0Y!Xul7=i{gjAofA<;x^VE)FIb~xaqD16lLDRz_o79`c8;= z@ss_uY%gIAcT4}qG~-?)lJoD6+%Emil1SdIa-mWV%Qr&xGjV=p4X4|+HDj*b#h8QN zxplc3C-xtY7kB!{UDbU~@eEi}^I&TwYL^=fZs637>%zt+!Ye&!R^OHs6OP8xCeyEK zv>aAOzVTl6dUxNnQbfC`=DcGoHNs=|_(bR#Mos^wS}*{msc#nxWY+?P>i58rmWmsN zx>rW@&;VIe!%YpB2{DWA$0;RKxVaol4g`!zX`14B{KevIqbl46pFc-Zl^lPl)CtZm z+#W5TEnNKB1}klN(xVcevkr9Q-zKD28h+b!{o|23Wlh z-X&VUXVnZuo&ahL0WWF#PNw>Oy)*lB=)8k3!M4UYwrBan`3T?hKRu0$W9KtQfID@z zxj&KzFBS<1L_d(=4u`XY4g-n<5AK|;pC{2YtT_j??Wdk!DYg@63r`HClq1ek%vu9t zQotrkbs&*-c27o@EzGUVYk<~i-P(AS2#}Efm;@7!v{}d^fCg)v>cV`8Do_qwEoMDQ zlc;qZk0Y}H)@R=+X_S=0GBbf5YsTEVFwZ+S0(LZW56s+n@i3@NV3`c8f~52T9bRAh z7U~P&{?tRf8qT!;aT|}=vpl;m#tfjSM1`TWyM7Ny0KF6nBL6W+BOF9@613oxopOdd z9D@gK?&F;qFBmQAv9C;E>J`TEuGe5;iFhH(C)5ey2%ocRm}UHK-w@sW@ur;sGJi2k#9f@6jFf(fZ+qnD6!x62hc?rqhaxnymDq z$wcgiRM*uMunbkvlo>JH_a6loZtBg@324=)f5JubtZ=0ACIg^Hj^6zKtCZsI&heYZ zr`N;`t^&Yb2)%;(nW3dE^y2a*PT)k&aY=0*AgPoEhwQXq&BmZ7Li7NzYB*~b$8H>a zUw&#s7SMc2geUr4g|u{h9AvVy^^4)uz9|~B_dF1E&p zr8k0Vh(ylkmzhTgLQ}j%gxMXx%xssN=!<{?!d9%CuPqTw`qsQvgbsLdnpc;lcoKdo0IE*7{vRqXQ<70hdZ=zpb=;ga(ht~tx<(a%#hpa!-W>aTUPZEIn>Dw|0r`OieS%nH!IZh#3l!1Vh&lV*OyPv`Rp z{f50;J7phnrv~y-{+yYJg9fhH- zo4@wTL;}9=Gh-Qo92@fKz2vtOk!Xp8SaJKrE z;6Yf?rmR0875bna58r?Z1{*00@3UYB!?N=L#}|!})pLpUC6|sPvycDN)Z!U#bm_qe$i>WTx= zHs*-Fem_q_)kmHHn70S&#FJz(#Jt+=%6)lfItWf zb}w{bB~54!eEPi8wR{B%i*Clvm5*+ZH!x;r{ZuZ;TV5|xx|`uZwrB?D+e8S*!f#8N z@5(aAubFVoOeZ8@u%O`am(_p+F4Ns^Z(O?s;8+9Z5X_HSSoba*opwbjAV2Pn2jfZP zBO|6HSuAgr0mCn$=zL)zY%yaQ&Y3L{?1u|b^vyRIPNLrMlt-7TA#{j5yL4`y2@4%z z9lqI`mVRf6@G#xreGT+qvlj(4_T+S)uGWa7|3rykzPfXL=X%g?;W$UCnotl$0o>e}8|dWL5fmt!<&RAJJBj zFcY{=mCO^b)UzDL2OtffApv~+*oe4ncI9oAJ(&DVZ#Hl`)oy!%lX>2@0cdUcyL>%3 z)|#&S&RDUHsvO-b?>@gBAg!SjOQQO;i~Q+jzD)vLO=JkZ5E`18vJcIosmnt`T-PqF zu@?byt)<9*iz}8B;->cUY(j!y1Og#TZ--z1MLn=k1}E>Vmysb=FkyXS-iC!M6bcw` zA$iCP)0AMG1wlk~ks=}W7Ku#gMC|`@kUj!(JIm_4w&_8+xpu^Pc5iv{Vu)azgPekb zf}BtCSbzyAYZ8Y*Bq5Mr%WgnYG=*tE0xBGf?Jm1vjr!kcaz2K;CO%jte_aeva6CW% z%9Qs*Pus%xH^r@=)r^6Evt8oVL?E?)m1w{AZooHgv64T{SiTPcukZehdxJzJqMnr+ z@P1JJyRibvLk=3=pH2l76=d7O{Q-5w+V|l5o5VcZwq-XhUYfsnY4O|pNRF{!ne99= zBsXb%UKk&&%d2wX!v?&AH<+owbEX;Axz;t+UMzXC;Oo}8@A0}KkS+9Brlx!>jlR6D zA>3K8G@f4W5s;n)e72ZyTZ133Xx%fiyBVzL(9C&{LKuI&sewe6WpI|t6nCd=84VsEtrRfrfZn_;_^6?!ZRtcHMIKz zqK&(rxes;84rtOkV5j&0)KQ@j8fbU&rx*L+!n>5_u7Sp8Fg#=duihiNc(_AKr&Y%A zpywb^R0bz-2MjX*`>xucmFM4(NJ#-8#!C&N6M~U`gI_G`$9A=&AlM2pBV40ViNNJc zvb}#@!UvGb%Wkr|bT<~MJZSKvo6bm@{}H<__bn|fQ-z!!PAm(AXz(}8m~MT4fiaLL zW@eayYEg#b^UI&Dba{E$Dg$dlEo{Sp1pp>v)Df?f19>uJ1^?~r>XrN;aeO`(@Np@6>3L!9&DWVhC(nj1 z(b)Hnh85h4i21t#d}5H>(!^r_O^IB(hi=19CU_rwNi3_{>D3k^52`Fy%lom zLn|1T(iuiBhMT=BH?uKxwPv*Xb)i@{)kFo$14ka>?YTk%itfRMYsO4Q!JLR`+%N1(GQLN0#>lKmr(k z(v7H#$!Hm|XHZ1m%g%&E6c(#CaKS7DCs(NjqJG8u2*ZZlYQo7a5ZYw_BbZxqtx%^& ztA;l_1}>?RorqCuzyu66k*AY`d~uZuC5yKv{wqNmx@75MgJp$7To&6S z9)_~UT9aIEn#NMvYs!?i^$5V^`R7ug2DyEs=#+)Qp7PQTh(CkM23tJ12qiTIHe2c_ z1GeIxnXHaN#>G+cqCPfQkevu-kFycjNlU+UzG~m}I{Ss^96eO?r|TCS*mT!Cv86{L z{hjmD$|IpympNd3%W0$17GNr{y(aI)Fap0Ys`~?FyqZ)&e`${NB_}1%1_|4h&3kyn zHPu+yma~2_A#FA&(O~|Mz5Bu#jGxq`DXCCt_C+0eJi7UJZUjU5@~a{fmeo$*2iLzW zQjW^vLA-F6hXeIezBrI)pTtfNUFfS?VtlrK?>iJm2B+fG|oxaV?nM`b*lRtzxc43B6E;F!u&ZcEsfwy8rShl<)0V(rXxG z3EyKhDwqN9U?YGq-4xQl>9#9^V5f2Wh~^kGAgY1KiMg|1$gvkvQNg;{H4Cl#QmdVG zBvG2z!nt6bQoa|=MsWCu{Q509+M4L+YD*}FsNyy=w0VoJrkE^{TnT)(s3SW;jKe{0 zPwU2oT!&xY^i1QwB>M(Hcd3LvxMMyX$c=5~2Dfl?cPd=~EzyU5DB0xM9pE)U0c_yT z4PLmEc_$o1C%k5+HE$FWCd5|Jw4h7rV8*n3kF2Zti!lm`bf4`Ax+KPv5D@%CvuN2Y zNkN;W<=K7cd zc!chxFS&igCX8dT!=0Ts{~r5SGah-w`bJ1;s2!vu@dqEy^Y(ZAu&+!QmNPxU;mlf1L$TC!C6Ucxpjt^cq=zs)Eq+(MMHh-VIvW$%093P(s zXgshP+_DuAbt4GevXH@GMU$TJA_`q|dS}w1_mZa}aJ#kpcQNw6N zoZDYSQ#Y8&ERxcS$d?1$*GoYj6ErTiBXvndCf`vE1OEuSeFKTT9hF^O zAtEwfmvz5;$zK8TNQQxevlvv!V`LqY+7m^f110mxvwZ^qC2mwyr&Za~AZrp9ohU_0XPYl+aXte06k2o{@)l5k(NAF1(Qhhv82QIe{3dpq-2>ZwNS~K!ap8 zG4EXz@_5$ojp@!m_n9o8&ix4&3B+cJ+j5LKSaF=BIv=x&;{`Kh%#9xice#~5f$7A} z>(9@I#Qs&|xcKjwxXo&Ka-4mEFn-Ip}lg79&)0^)(WK~1d9Uop~=U&qs9`+`@wVD)N z)F@Nx)SD#XC3ix>gO@2=inf#({^Z-q$S{#dOxv#%wD{f8aPAgxnbCcb-(NCEj(a#v ze$gJDqtAZtE|&m zrj(;CT7<~>+Wba(Ru6*Z_=nySWQ^mblz z%)Vq1T0iQ}sYb-&Z=`-D=BW&R_4)E;Gg;cVkc=2gF_w-4tOd|@s!xo=^=?=1N0sO* zF>`W;>0jcTJ5S7(vl=iv9avn=X2&*J63*#uOD%lr8UW%@mYm8rLq3aCjTT9cl8j=> zYh_XuPEUYGrh1etR?UD}k3U96h}##DOz2Ro>7On|})i zcEkBB+iTWCaknb%ZI)A~1lZHXwQVy!vxFy-9G$+T;uAlN?28)LHxH`tD(VYQB_j^=LHYVgeH94Hx0HBas^ZPq9JSoN3USb5e3fYPEP~ z;)H{|{o`}8ulPb)J<&atb~ssK5oX%RiN0~!iPs$!+TA}VB`~!u9kum=grAqx@2{0T zmF_G3*EDDvc<#s_cW@HKG1i~nC$>4~So2-#v)6CJl||@oR}FFetV8~6UGrl|{?>J)?M`RXEeoeDp4m`& zuS_p5wgSvd@~$o3_L7J>T>72jdyESLwjK!R;HyyrtSjX= z8E?EK095S^>lCLa8?qxYbU@dcd_}#fiQnBUNoMtT&PJowEgR}cRI$e_bGhNA9EYy6 zQG2s62_uIp=fznU(Z2ccH^v>)A)AgD&F#!&;#S=Mo@+l>1zkmitQy^?~7iW^qQ7b(MS(tTbYM z{6PhQ$@JZ26h&K_Q%|gaSDau4}&sQLq*r) z8~RolO$~>5XAX)}dF-@PJK4OWaq@RYCr$R7-sznArQF9#te>yfy)fe2>8x(n-2N0m z&}2e&CG=6)BmYN5hd1v;%|+A`y}*1WvEjRNB75Cel8poipx6+=v_G4DQhCt(Vtqi^ zY^UAV;~xWkPzFBv>e584yC(-;LZNkmG7#in^Mj->%3T+-=UJ2}{-3=~03pIx#h`OL zm+t*FuNBF!`(}tmvFv%ms4fiycNQW*jpF9g{t!>C#YaK6GWDPKPAj*xL5sdG-T(AY zhg$2m|Lk)=4k9s9x`J3HK-*`gc6f!ChXg08PFJz8)OQD>3ZLMf+i#05uFmTXhl)ve zLsv7n60yiclFLW;pI3lF(MP2&DNzDpf87wYis~sAKYE%oI6FJB;o+fT`;135N3(td z=@+1?G8oDWB%dmg_l0AJv9Su25*N0@`yqihpJj-8t$>lv#id`P%gWj!7<2(=67#cDt-{_A{9S@Hwzg-2 z16W0EH?ZA%uOz1u&?{rx=17Jm&C^(!6N#{N^&!j4>sgL2IMWLi4cB+C8+ zD>Im>JSrFpwaUxN3z=;F?;aab%^Bme+37Zr?HJ1jYD;U*A^JZ*!skHQ#FyWa6Gv8> z*0@9j)%Y<m86bofVu8Vqy6~Jo>@V1Ven<4?&5k%SkpD5xjad_%Zatc~es5R#JT6 z&#l3+knp${`z!rTQQ#=>-C`~?7JX+NPBtl@$4X|`>A zA~F%4l#*vCnkNbi2;YqE83}v7vw;%)(|BOKQSP(#-iXIzwrVnAvU;3HxFdILvv0w( zvNR9(X-elra!|ag?}n#WiSN~Cv)DlmJzV1P!vh1SCYNb;?dfib1+B z)cNrBoml8$S1}X*A~FfRtC&B{TaOSz4-`*zYFj273!i$Y7Y(RDsq9M*3t84NlV{u% z5t)|BcWckxP7caP+9n;{rZR*o@659nWt8ss*oPIyW!Flbk~@59eEQ>Q{KItS=bQLm ztidX+9eyZxxmJg~?#2_p#&e*K+1GTB4d{9?$Z!IJ=0S^p*= z!>bSPC?7a{Y`E9L&A>-siacsaP`G6P?AM>>(|NPNc@0By^1LR)ak{8>0$&TN_}>L! z*5TR2Q=Q$8hZ6rzGolJJXC0;|2kx}_j&RNnWTr5|<;`C(cFP)BJ7h-L5SnO`4_6;ljGWIOY(+*mPEl zRWz%+>O(!q2m6uJj|LPwSy>Z~$?h?9S_tO#)4UGFe3jEsN{hj}5f3^t#2yO*Jd&2Fr-F-F#nh0uTU8ojblHe%P_Kd4({sI_*;O+=Tt$VFi zJa^S3(LPZ?Hl9xkG-ae#V=#>aNOnOV&C>Synck=DhmKfrjvUWDegbp!PRawpg5N$96Q><4EciGb=v^GcFC)nuI{Ft_v&EumfAR!*!i;9r}Xs8rq+kM%RBzDjSsU)q1U2h z{^`+^WM#X4u=A*#^#Q8eEZsPV_aQeAcS6?**yM`_xulrp60&+U=w%M8Ar()2xxo|8 z{cJG{W>B;b!FtU^XY4R1Ig4>Xq1~T^pl>jy{vrQxlp&io3k=N8w}b@zjX{3k^+{M0 zZ*k5q-S<-Z;VMlHI0F&dACkXxW6qYChwc@d=NZRT)h+WNR(8H@7N!2~O7;IS_uJ?w z=(j9FPiV0+>QjJxzG9!5M!3I zh<5_$&*Vpxvw*KevL)!N)1Mu?qWEctJAXtA;gqS^yF@KtAm8N82v{am*zT^GVTO$1&8fwDh20-@K%hAPpDy3kI;5p* zujMUQOBV+t7mHQh9eO(N9paJexkn4WtJQhS8vDbA5mO~@8-~XcK@hmWPXnBS?L>+1 z^iM|?8qnejSJnZBaHz9M?AQ3v@l|*Cuib4yJjn(71W|rHC+V!2N-Zfx;>jH&jX_#` zR%!c^#wjY1qq0IM-bEmk9MT4opdx)K|2~a3BO~|k6b7ktwBbf z9yfFIbl?xOngR&lQ^pXRFdb^;tST-@$@8JST2|EQ31@z8+^&f}i9}HYZ(MSF293j& zcso9o1Nm6n=40=h$P|uO!)bK3_h8y=hzAH=Z0FdVx1;LSk{~r7z2h+%ia$w2xE|_c zpfIm~eF$jAT<`v)nTz5nde_E(GW)FExn4Ezq}sLTds3*5^-3%vrG4!a^iv;-ki-H! z@Z#o?h%Kv*3tkgaPDqS?Y?yxqxmr9F1ToZmUr+h zh1^s)U%$_t*_^-V~4YED$ zCw!KOq!+vG%r+wAc|%7a{{}mj-dPX9EeEr+$MijJc4X5J6N`_p$UP2hE42P@5x?a% zGE?FEQ1v>-v$jXdcl$BAa*>;qRw&H3hR6GS*xMXavXn;=?X3*u3VshkAqpm$$D@Pc zl7>>Td-2;R89>Je3z}`$*c+9TSL-S&Yyv!F|NWq*+wMIQo4>tVLgQZXv|8_#XREnpod4}dpD2UuQ4w!wkf)fNaO)^zW`;!;vkQG#p`d{h#zcC^ zI&+f;9=)IWI9l@K>uIsskn}_lsRYNuXOU$+jS-cHqKiu63&yW0Lf-=|7!Qi3jBn^g zHfmtASuvc6cJt3z*>AB~3om-MyKd&C**y|kiRf_eW9l!sQ&wsiCj4>gw>f*|ctm$= z+?{k+f~B+h>F|-!ATR$Ex7U$eLfkeVbOjl_GAFnlOGzFgO^MBg1Z@-%7Bt9L@KY_j z+9}O}TH0Q}&x01SqLD2Jca|-*>^OyE4S7>AD%Yt>eY`$%p`H5~QR3WJuUo@;X_<2X zZ+k0;vi)3*U*D03j7Lp#hRF|lOBJctD|6{LU&aKy>+;p)=3bJ#rd0G1^zWhOmB^HH(!O=39xS60nSd#v-&WlSS-*BrxAzm`M+`!vL3ie|E@!_#WXBM*ohey$3K(U%9 z%-FOOaodRc&RE%v6%1{(w`lF>%2m^`G2ykjRps9gSTHlrC?J({C_`NalR;q_CgMgd8naW!M<%>`9>b2*2`*_v-=4SNlLHlS4fhKJj$= zb{exy{(%i?;ja4kt~qNwy%u4xcVFI7e1l-|1#IJ@WTQq?1T!3it%O@wbh;o~S%iGw+T2cJ$J7 zL|GNVrs{B1`ZC6|j4@rGXoKxfvMH+6hAS0TcrA01F*^8;iL)+IL0LJv?+40t~Pjj3Ie!WI7L^099y)_Fc}HS9aple{uUZ66a>d~DBc z+gk`GB3h6aj{^UDK5H?cknLk!NP|giW2lH5#)JWy=7fWEa{nq&t|I*IoXZxj4Yz~y z%I8RqTXm~xkv{LA^@d?G&rG#DK8}+YO+x`k0Go$`frpj3hqbt+n>Fx@K#*Sm!ov^Y5#rb77ZDc_ t6&HHQ#m_I!&u`{MWcdG?;OJ~+i}3ybp8(=&T*3Pqlod3fMRI1Z{vVCj0&D;P diff --git a/docs/plugins/hotkeys.rst b/docs/plugins/hotkeys.rst index 6780efca1..6b2ef3f0c 100644 --- a/docs/plugins/hotkeys.rst +++ b/docs/plugins/hotkeys.rst @@ -2,7 +2,7 @@ hotkeys ======= .. dfhack-tool:: - :summary: Show all dfhack keybindings for the current context. + :summary: Show all DFHack keybindings for the current context. :tags: dfhack The command opens an in-game screen showing which DFHack keybindings are active @@ -11,8 +11,22 @@ in the current context. See also `hotkey-notes`. Usage ----- -:: +``hotkeys`` + Show the list of keybindings for the current context in an in-game menu. +``hotkeys list`` + List the keybindings to the console. - hotkeys +Menu overlay widget +------------------- -.. image:: ../images/hotkeys.png +The in-game hotkeys menu is registered with the `overlay` framework and can be +enabled as a hotspot in the upper-left corner of the screen. You can bring up +the menu by hovering the mouse cursor over the hotspot and can select a command +to run from the list by clicking on it with the mouse or by using the keyboard +to select a command with the arrow keys and hitting :kbd:`Enter`. + +A short description of the command will appear in a nearby textbox. If you'd +like to see the full help text for the command or edit the command before +running, you can open it for editing in `gui/launcher` by right clicking on the +command, left clicking on the arrow to the left of the command, or by pressing +the right arrow key while the command is selected. From bdf201c6705619dfc0bfe0769ef119167e7ef726 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 18:05:07 -0800 Subject: [PATCH 079/161] ensure keybinding shows up in hotkeys command not overlay --- data/init/dfhack.keybindings.init | 2 +- plugins/hotkeys.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index 697641ab1..330260b06 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -13,7 +13,7 @@ keybinding add ` gui/launcher keybinding add Ctrl-Shift-D gui/launcher # show hotkey popup menu -keybinding add Ctrl-Shift-C "overlay trigger hotkeys.menu" +keybinding add Ctrl-Shift-C hotkeys # on-screen keyboard keybinding add Ctrl-Shift-K gui/cp437-table diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 77db827bd..8f980cb2b 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -22,6 +22,7 @@ using std::vector; using namespace DFHack; static const string INVOKE_MENU_COMMAND = "overlay trigger hotkeys.menu"; +static const string INVOKE_HOTKEYS_COMMAND = "hotkeys"; static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu"; static bool valid = false; // whether the following two vars contain valid data @@ -32,8 +33,6 @@ static vector sorted_keys; static bool can_invoke(const string &cmdline, df::viewscreen *screen) { vector cmd_parts; split_string(&cmd_parts, cmdline, " "); - if (toLower(cmd_parts[0]) == "hotkeys") - return false; return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen); } @@ -56,7 +55,8 @@ static void add_binding_if_valid(const string &sym, const string &cmdline, df::v if (!can_invoke(cmdline, screen)) return; - if (filtermenu && cmdline == INVOKE_MENU_COMMAND) { + if (filtermenu && (cmdline == INVOKE_MENU_COMMAND || + cmdline == INVOKE_HOTKEYS_COMMAND)) { DEBUG(log).print("filtering out hotkey menu keybinding\n"); return; } @@ -178,6 +178,7 @@ static command_result hotkeys_cmd(color_ostream &out, vector & paramete return CR_OK; } + // internal command -- intentionally undocumented if (parameters.size() != 2 || parameters[0] != "invoke") return CR_WRONG_USAGE; From 2093287bf009bfc120b00f55a370ae539d368afe Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 18:05:26 -0800 Subject: [PATCH 080/161] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 4465a0f12..e3a5bb5ba 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `blueprint`: when splitting output files, number them so they sort into the order you should apply them in - `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. +- `hotkeys`: hotkey screen has been transformed into an interactive `overlay` widget that you can bring up by moving the mouse cursor over the hotspot (in the upper left corner of the screen by default) - `digtype`: new ``-z`` option for digtype to restrict designations to the current z-level and down - UX: List widgets now have mouse-interactive scrollbars - UX: You can now hold down the mouse button on a scrollbar to make it scroll multiple times. From 6635b6489ba7f156c75ffa80b8ff301f707a2228 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sat, 12 Nov 2022 09:57:32 -0800 Subject: [PATCH 081/161] handle commands like ':lua ' --- plugins/hotkeys.cpp | 4 +++- plugins/lua/hotkeys.lua | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/hotkeys.cpp b/plugins/hotkeys.cpp index 8f980cb2b..85c304d82 100644 --- a/plugins/hotkeys.cpp +++ b/plugins/hotkeys.cpp @@ -99,7 +99,9 @@ static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) { auto list = Core::getInstance().ListKeyBindings(sym); for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) { - if (invoke_cmd->find(":") == string::npos) { + string::size_type colon_pos = invoke_cmd->find(":"); + // colons at location 0 are for commands like ":lua" + if (colon_pos == string::npos || colon_pos == 0) { add_binding_if_valid(sym, *invoke_cmd, screen, filtermenu); } else { diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index bd88fdd89..7a1a39115 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -182,6 +182,7 @@ end function MenuScreen:onSelect(_, choice) if not choice or #self.subviews == 0 then return end local first_word = choice.command:trim():split(' +')[1] + if first_word:startswith(':') then first_word = first_word:sub(2) end self.subviews.help.text_to_wrap = helpdb.is_entry(first_word) and helpdb.get_entry_short_help(first_word) or 'Command not found' self.subviews.help_panel:updateLayout() From d786989450ff35e91d30cc395bf4be5fc8cac1b1 Mon Sep 17 00:00:00 2001 From: lethosor Date: Mon, 14 Nov 2022 22:10:05 -0500 Subject: [PATCH 082/161] Revert "Allowing whitespace-only lines." This makes lint.py's behavior match pre-commit, which is useful because lint.py errors show up inline on GitHub PRs. This reverts commit ebb3dc48bf3cb3e460cd4eef1645425b8565f6b4 --- ci/lint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/lint.py b/ci/lint.py index b2fb8e647..f2c01cd9c 100755 --- a/ci/lint.py +++ b/ci/lint.py @@ -97,7 +97,7 @@ class TrailingWhitespaceLinter(Linter): msg = 'Contains trailing whitespace' def check_line(self, line): line = line.replace('\r', '').replace('\n', '') - return not line.strip() or line == line.rstrip('\t ') + return line == line.rstrip('\t ') def fix_line(self, line): return line.rstrip('\t ') From 89e579239fa26828230819faad3866fd64f0721b Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 14 Nov 2022 15:25:04 -0800 Subject: [PATCH 083/161] update dreamfort help, add qsp for training bolts --- data/blueprints/library/dreamfort.csv | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/data/blueprints/library/dreamfort.csv b/data/blueprints/library/dreamfort.csv index e26650943..c5566920b 100644 --- a/data/blueprints/library/dreamfort.csv +++ b/data/blueprints/library/dreamfort.csv @@ -314,7 +314,7 @@ Here are some tips and procedures for handling seiges -- including how to clean "" "After a siege, you can use the caged prisoners to safely train your military. Here's how:" "" -"- Once the prisoners are hauled to the ""prisoner quantum"" stockpile, run ""stripcaged all"" in the DFHack console." +"- Once the prisoners are hauled to the ""prisoner quantum"" stockpile, run ""unforbid all"" and ""stripcaged all"" in the DFHack console (or GUI launcher)." "" "- After all the prisoners' items have been confiscated, bring your military dwarves to the barracks (if they aren't already there)." "" @@ -2292,7 +2292,7 @@ query_jail/services_query_jail ,`,`,`,`,`,`,`,`,`,`,`,,`,A,A,A,`,A,A,A,`,,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,r,`,`,`,`,,,,` ,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,d,`,`,` -,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,` +,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,trackstopN,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,,`,,`,,b ,`,`,`,`,`,`,`,`,`,`,`,,,,,d,,d,,,,,`,,`,,`,,` ,`,`,`,`,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,t,`,`,`,`,R @@ -2327,9 +2327,9 @@ query_jail/services_query_jail ,`,`,`,`,`,`,`,`,`,`,`,,,,,,`,,,,,,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,,,` -,`,`,`,`,`,`,`,`,`,`,`,,`,z(7x3),,,`,`,`,`,`,`,`,`,` +,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,c,`,`,`,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,` -,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,,`,,`,,` +,`,`,`,`,`,`,`,`,`,`,`,,`,z(7x1),,,`,`,`,`,`,,`,,`,,`,,` ,`,`,`,`,`,`,`,`,`,`,`,,,,,`,,`,,,,,`,,`,,`,,` ,`,`,`,`,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,`,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` @@ -2409,7 +2409,7 @@ query_jail/services_query_jail ,,,,,,,,,,,,,,,`,`,`,`,` ,,,,,,,,,,,,,,,`,`,`,`,` -#query label(services_query_stockpiles) start(18; 18) hidden() message(Configure the training ammo stockpile to take from the metalworker quantum on the industry level.) configure stockpiles +"#query label(services_query_stockpiles) start(18; 18) hidden() message(Configure the training ammo stockpile to take from the metalworker quantum on the industry level. Assign a minecart to the training ammo quantum dump with ""assign-minecarts all"") configure stockpiles" ,`,`,`,,`,`,`,,`,`,`,,`,`,`,,`,,`,`,` ,`,`,`,,`,`,`,,`,`,`,,`,`,`,`,`,`,`,`,` @@ -2421,9 +2421,9 @@ query_jail/services_query_jail ,`,`,`,`,`,`,`,`,`,`,`,,,,,,`,,,,,,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,,,` -,`,`,`,`,`,`,`,`,`,`,`,,`,nocontainers,{bolts}{forbidmetalbolts}{forbidartifactammo},"{givename name=""training bolts""}",`,`,`,`,`,`,`,`,` -,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,` -,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,,`,,`,,` +,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,"{quantum name=""training quantum""}",`,`,`,`,`,`,`,` +,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,"{quantumstopfromsouth name=""Training quantum""}{givename name=""training dumper""}",`,`,`,` +,`,`,`,`,`,`,`,`,`,`,`,,`,nocontainers,{bolts}{forbidmetalbolts}{forbidartifactammo},"{givename name=""training bolts""}",`,`,`,`,`,,`,,`,,`,,` ,`,`,`,`,`,`,`,`,`,`,`,,,,,`,,`,,,,,`,,`,,`,,` ,`,`,`,`,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,`,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` From 8f7624fb76e1fcb1cfdf64da1797c756e9a9415d Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 14 Nov 2022 16:35:41 -0800 Subject: [PATCH 084/161] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f89849bf7..2839d9c74 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,6 +68,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `spectate`: new ``tick-threshold``, option for specifying the change interval (maximum follow time when focus-jobs is enabled) - `spectate`: added persistent configuration of the plugin settings - `gui/cp437-table`: new global keybinding for the clickable on-screen keyboard for players with keyboard layouts that prevent them from using certain keys: Ctrl-Shift-K +- `quickfort-library-guide`: dreamfort blueprint improvements: added a quantum stockpile for training bolts ## Documentation - `spectate`: improved documentation of features and functionality From 511250afbe88240aa6033fd49e42396439a592ce Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 14:48:14 -0800 Subject: [PATCH 085/161] reorder keybindings, no definition changes --- data/init/dfhack.keybindings.init | 114 ++++++++++++++---------------- 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index 330260b06..e526848f3 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -5,7 +5,7 @@ # dfhack-config/init/dfhack.init ################### -# Global bindings # +# global bindings # ################### # the GUI command launcher (two bindings since some keyboards don't have `) @@ -15,18 +15,27 @@ keybinding add Ctrl-Shift-D gui/launcher # show hotkey popup menu keybinding add Ctrl-Shift-C hotkeys +# a dfhack prompt in df. Sublime text like. +keybinding add Ctrl-Shift-P command-prompt + # on-screen keyboard keybinding add Ctrl-Shift-K gui/cp437-table -############################## -# Generic dwarfmode bindings # -############################## +# an in-game init file editor +keybinding add Alt-S@title gui/settings-manager +keybinding add Alt-S@dwarfmode/Default gui/settings-manager + + +###################### +# dwarfmode bindings # +###################### + +# quicksave, only in main dwarfmode screen and menu page +keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave # toggle the display of water level as 1-7 tiles keybinding add Ctrl-W twaterlvl -# with cursor: - # designate the whole vein for digging keybinding add Ctrl-V digv keybinding add Ctrl-Shift-V "digv x" @@ -34,43 +43,29 @@ keybinding add Ctrl-Shift-V "digv x" # clean the selected tile of blood etc keybinding add Ctrl-C spotclean -# destroy items designated for dump in the selected tile -keybinding add Ctrl-Shift-K autodump-destroy-here - -# set the zone or cage under the cursor as the default -keybinding add Alt-Shift-I@dwarfmode/Zones "zone set" - -# with an item selected: - # destroy the selected item keybinding add Ctrl-K autodump-destroy-item -# scripts: - -# quicksave, only in main dwarfmode screen and menu page -keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave +# destroy items designated for dump in the selected tile +keybinding add Ctrl-Shift-K autodump-destroy-here # apply blueprints to the map (Alt-F for compatibility with LNP Quickfort) keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort keybinding add Alt-F@dwarfmode gui/quickfort -# gui/rename script - rename units and buildings -keybinding add Ctrl-Shift-N gui/rename -keybinding add Ctrl-Shift-T "gui/rename unit-profession" - -# a dfhack prompt in df. Sublime text like. -keybinding add Ctrl-Shift-P command-prompt - # show information collected by dwarfmonitor keybinding add Alt-M@dwarfmode/Default "dwarfmonitor prefs" keybinding add Ctrl-F@dwarfmode/Default "dwarfmonitor stats" -# export a Dwarf's preferences screen in BBCode to post to a forum -keybinding add Ctrl-Shift-F@dwarfmode forum-dwarves +# set the zone or cage under the cursor as the default +keybinding add Alt-Shift-I@dwarfmode/Zones "zone set" -# an in-game init file editor -keybinding add Alt-S@title gui/settings-manager -keybinding add Alt-S@dwarfmode/Default gui/settings-manager +# Stocks plugin +keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show" + +# open an overview window summarising some stocks (dfstatus) +keybinding add Ctrl-Shift-I@dwarfmode/Default gui/dfstatus +keybinding add Ctrl-Shift-I@dfhack/lua/dfstatus gui/dfstatus # change quantity of manager orders keybinding add Alt-Q@jobmanagement/Main gui/manager-quantity @@ -78,7 +73,7 @@ keybinding add Alt-Q@jobmanagement/Main gui/manager-quantity # re-check manager orders keybinding add Alt-R@jobmanagement/Main workorder-recheck -# workorder detail configuration +# set workorder item details (on workorder details screen press D again) keybinding add D@workquota_details gui/workorder-details # view combat reports for the selected unit/corpse/spatter @@ -87,39 +82,11 @@ keybinding add Ctrl-Shift-R view-unit-reports # view extra unit information keybinding add Alt-I@dwarfmode/ViewUnits|unitlist gui/unit-info-viewer -# set workorder item details (on workorder details screen press D again) -keybinding add D@workquota_details gui/workorder-details - # boost priority of jobs related to the selected entity keybinding add Alt-N do-job-now -############################## -# Generic adv mode bindings # -############################## - -keybinding add Ctrl-B@dungeonmode adv-bodyswap -keybinding add Ctrl-Shift-B@dungeonmode "adv-bodyswap force" -keybinding add Shift-O@dungeonmode gui/companion-order -keybinding add Ctrl-T@dungeonmode gui/advfort -keybinding add Ctrl-A@dungeonmode/ConversationSpeak adv-rumors - -############################## -# Generic legends bindings # -############################## - -# export all information, or just the detailed maps (doesn't handle site maps) -keybinding add Ctrl-A@legends "exportlegends all" - -############################# -# Context-specific bindings # -############################# - -# Stocks plugin -keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show" - -# open an overview window summarising some stocks (dfstatus) -keybinding add Ctrl-Shift-I@dwarfmode/Default "gui/dfstatus" -keybinding add Ctrl-Shift-I@dfhack/lua/dfstatus "gui/dfstatus" +# export a Dwarf's preferences screen in BBCode to post to a forum +keybinding add Ctrl-Shift-F@dwarfmode forum-dwarves # q->stockpile - copy & paste stockpiles keybinding add Alt-P copystock @@ -182,7 +149,30 @@ keybinding add Alt-W@overallstatus "gui/workflow status" keybinding add Alt-W@dfhack/lua/status_overlay "gui/workflow status" # autobutcher front-end -keybinding add Shift-B@pet/List/Unit "gui/autobutcher" +keybinding add Shift-B@pet/List/Unit gui/autobutcher # view pathable tiles from active cursor keybinding add Alt-Shift-P@dwarfmode/LookAround gui/pathable + +# gui/rename script - rename units and buildings +keybinding add Ctrl-Shift-N gui/rename +keybinding add Ctrl-Shift-T "gui/rename unit-profession" + + +##################### +# adv mode bindings # +##################### + +keybinding add Ctrl-A@dungeonmode/ConversationSpeak adv-rumors +keybinding add Ctrl-B@dungeonmode adv-bodyswap +keybinding add Ctrl-Shift-B@dungeonmode "adv-bodyswap force" +keybinding add Shift-O@dungeonmode gui/companion-order +keybinding add Ctrl-T@dungeonmode gui/advfort + + +######################### +# legends mode bindings # +######################### + +# export all information, or just the detailed maps (doesn't handle site maps) +keybinding add Ctrl-A@legends "exportlegends all" From 4f5cb196a74560630aa8af7185285b2944f082e6 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 11 Nov 2022 14:54:45 -0800 Subject: [PATCH 086/161] scope keybindings closer to their area of use so they don't clutter the hotkeys list unnecessarily the better solution for many of these commands is hotkey guards, but we don't yet support hotkey guards for scripts --- data/init/dfhack.keybindings.init | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index e526848f3..f8a35f874 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -22,8 +22,7 @@ keybinding add Ctrl-Shift-P command-prompt keybinding add Ctrl-Shift-K gui/cp437-table # an in-game init file editor -keybinding add Alt-S@title gui/settings-manager -keybinding add Alt-S@dwarfmode/Default gui/settings-manager +keybinding add Alt-S@title|dwarfmode/Default|dungeonmode gui/settings-manager ###################### @@ -34,20 +33,20 @@ keybinding add Alt-S@dwarfmode/Default gui/settings-manager keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave # toggle the display of water level as 1-7 tiles -keybinding add Ctrl-W twaterlvl +keybinding add Ctrl-W@dwarfmode|dungeonmode twaterlvl # designate the whole vein for digging -keybinding add Ctrl-V digv -keybinding add Ctrl-Shift-V "digv x" +keybinding add Ctrl-V@dwarfmode digv +keybinding add Ctrl-Shift-V@dwarfmode "digv x" # clean the selected tile of blood etc keybinding add Ctrl-C spotclean # destroy the selected item -keybinding add Ctrl-K autodump-destroy-item +keybinding add Ctrl-K@dwarfmode autodump-destroy-item # destroy items designated for dump in the selected tile -keybinding add Ctrl-Shift-K autodump-destroy-here +keybinding add Ctrl-Shift-K@dwarfmode autodump-destroy-here # apply blueprints to the map (Alt-F for compatibility with LNP Quickfort) keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort @@ -64,8 +63,7 @@ keybinding add Alt-Shift-I@dwarfmode/Zones "zone set" keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show" # open an overview window summarising some stocks (dfstatus) -keybinding add Ctrl-Shift-I@dwarfmode/Default gui/dfstatus -keybinding add Ctrl-Shift-I@dfhack/lua/dfstatus gui/dfstatus +keybinding add Ctrl-Shift-I@dwarfmode/Default|dfhack/lua/dfstatus gui/dfstatus # change quantity of manager orders keybinding add Alt-Q@jobmanagement/Main gui/manager-quantity @@ -77,19 +75,19 @@ keybinding add Alt-R@jobmanagement/Main workorder-recheck keybinding add D@workquota_details gui/workorder-details # view combat reports for the selected unit/corpse/spatter -keybinding add Ctrl-Shift-R view-unit-reports +keybinding add Ctrl-Shift-R@dwarfmode|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|workshop_profile|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit|buildinglist|workshop_profile view-unit-reports # view extra unit information keybinding add Alt-I@dwarfmode/ViewUnits|unitlist gui/unit-info-viewer # boost priority of jobs related to the selected entity -keybinding add Alt-N do-job-now +keybinding add Alt-N@dwarfmode|job|joblist|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit|buildinglist|textviewer|item|layer_assigntrade|tradegoods|store|assign_display_item|treasurelist do-job-now # export a Dwarf's preferences screen in BBCode to post to a forum -keybinding add Ctrl-Shift-F@dwarfmode forum-dwarves +keybinding add Ctrl-Shift-F@textviewer forum-dwarves # q->stockpile - copy & paste stockpiles -keybinding add Alt-P copystock +keybinding add Alt-P@dwarfmode/QueryBuilding/Some/Stockpile copystock # q->stockpile - load and save stockpile settings out of game keybinding add Alt-L@dwarfmode/QueryBuilding/Some/Stockpile "gui/stockpiles -load" @@ -155,8 +153,8 @@ keybinding add Shift-B@pet/List/Unit gui/autobutcher keybinding add Alt-Shift-P@dwarfmode/LookAround gui/pathable # gui/rename script - rename units and buildings -keybinding add Ctrl-Shift-N gui/rename -keybinding add Ctrl-Shift-T "gui/rename unit-profession" +keybinding add Ctrl-Shift-N@dwarfmode|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|workshop_profile|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit|buildinglist gui/rename +keybinding add Ctrl-Shift-T@dwarfmode|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|workshop_profile|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit "gui/rename unit-profession" ##################### From 983ae025173ed3786ae2e037ca3981da38bca192 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sat, 12 Nov 2022 10:29:09 -0800 Subject: [PATCH 087/161] update keybinding docs --- docs/builtins/keybinding.rst | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index 6d1565509..c9665a048 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -7,10 +7,10 @@ keybinding Like any other command, it can be used at any time from the console, but bindings are not remembered between runs of the game unless re-created in -`dfhack.init`. +:file:`dfhack-config/init/dfhack.init`. -Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or -\` (the key below the :kbd:`Esc` key. +Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or ` +(the key below the :kbd:`Esc` key on most keyboards). Usage ----- @@ -21,16 +21,17 @@ Usage List bindings active for the key combination. ``keybinding clear [...]`` Remove bindings for the specified keys. -``keybinding add "cmdline" ["cmdline"...]`` +``keybinding add "" ["" ...]`` Add bindings for the specified key. -``keybinding set "cmdline" ["cmdline"...]`` +``keybinding set "" ["" ...]`` Clear, and then add bindings for the specified key. The ```` parameter above has the following **case-sensitive** syntax:: [Ctrl-][Alt-][Shift-]KEY[@context[|context...]] -where the ``KEY`` part can be any recognized key and [] denote optional parts. +where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote +optional parts. When multiple commands are bound to the same key combination, DFHack selects the first applicable one. Later ``add`` commands, and earlier entries within one @@ -49,13 +50,18 @@ Multiple contexts can be specified by separating them with a pipe (``|``) - for example, ``@foo|bar|baz/foo`` would match anything under ``@foo``, ``@bar``, or ``@baz/foo``. -Interactive commands like `liquids` cannot be used as hotkeys. +Commands like `liquids` or `tiletypes` cannot be used as hotkeys since they +require the console for interactive input. Examples -------- -``keybinding add Alt-F1 hotkeys`` - Bind Alt-F1 to run the `hotkeys` command on any screen at any time. +``keybinding add Ctrl-Shift-C hotkeys`` + Bind Ctrl-Shift-C to run the `hotkeys` command on any screen at any time. ``keybinding add Alt-F@dwarfmode gui/quickfort`` Bind Alt-F to run `gui/quickfort`, but only when on a screen that shows the main map. +``keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show"`` + Bind Ctrl-Shift-Z to run `stocks show `, but only when on the main + map in the default mode (that is, no special mode, like cursor look, is + enabled). From 822a1c83590ac0eaa64cec79bcf61168aa0868ad Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 15 Nov 2022 07:17:03 +0000 Subject: [PATCH 088/161] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index ea78ed8bf..7513311fc 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ea78ed8bf70c3e75b8fba90cdc61cab34788899e +Subproject commit 7513311fc4f490bd7510fe79f6834731ea1581c8 diff --git a/scripts b/scripts index 020f2466b..08329f4da 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 020f2466bc4462e59c1c16c036881907cad9718e +Subproject commit 08329f4da18610eb2eaa7d038bf51ce7e3c91183 From d1919933767f68b8da11ad57954ef621a6ed997c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 15 Nov 2022 13:13:33 -0600 Subject: [PATCH 089/161] autolabor/autohauler: add labor entries for 241-243 add missing entries for jobs 241, 242, and 243 this was fixed in labormanager in PR #1566 (see #1561) but was never addressed in autolabor or autohauler should close #1994 --- plugins/autohauler.cpp | 3 +++ plugins/autolabor.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/plugins/autohauler.cpp b/plugins/autohauler.cpp index b52e9cfb5..5b6d573a6 100644 --- a/plugins/autohauler.cpp +++ b/plugins/autohauler.cpp @@ -407,6 +407,9 @@ static const dwarf_state dwarf_states[] = { BUSY /* MakeBracelet */, BUSY /* MakeGem */, BUSY /* PutItemOnDisplay */, + OTHER /* unk_fake_no_job */, + OTHER /* InterrogateSubject */, + OTHER /* unk_fake_no_activity */, }; // Mode assigned to labors. Either it's a hauling job, or it's not. diff --git a/plugins/autolabor.cpp b/plugins/autolabor.cpp index 15c6903b4..97bd11a11 100644 --- a/plugins/autolabor.cpp +++ b/plugins/autolabor.cpp @@ -375,6 +375,9 @@ static const dwarf_state dwarf_states[] = { BUSY /* MakeBracelet */, BUSY /* MakeGem */, BUSY /* PutItemOnDisplay */, + OTHER /* unk_fake_no_job */, + OTHER /* InterrogateSubject */, + OTHER /* unk_fake_no_activity */, }; struct labor_info From eeee75273346681cda77c51231d87481ecffb956 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 15 Nov 2022 13:21:45 -0600 Subject: [PATCH 090/161] Update changelog.txt add changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2839d9c74..995aceaa6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -42,6 +42,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `mousequery`: fix the cursor jumping up z levels sometimes when using TWBT - `tiletypes`: no longer resets dig priority to the default when updating other properties of a tile - `automaterial`: fix rendering errors with box boundary markers +- `autolabor` & `autohauler`: properly handle jobs 241, 242, and 243 - Core: fix the segmentation fault with the REPORT event in EventManager - Core: fix the new JOB_STARTED event only sending each event once, to the first handler listed From 899fa3d940dc6442346221604632b8df56092862 Mon Sep 17 00:00:00 2001 From: lethosor Date: Tue, 15 Nov 2022 15:06:40 -0500 Subject: [PATCH 091/161] Fix quickfort docs link in data/blueprints/README.md --- data/blueprints/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/blueprints/README.md b/data/blueprints/README.md index 2facb3f4e..5d0e41a24 100644 --- a/data/blueprints/README.md +++ b/data/blueprints/README.md @@ -1,5 +1,5 @@ This folder contains blueprints that can be applied by the `quickfort` script. For more information, see: -* [Quickfort command reference](https://docs.dfhack.org/en/stable/docs/_auto/base.html#quickfort) +* [Quickfort command reference](https://docs.dfhack.org/en/stable/docs/tools/quickfort.html) * [Quickfort blueprint guide](https://docs.dfhack.org/en/stable/docs/guides/quickfort-user-guide.html) * [Quickfort library guide](https://docs.dfhack.org/en/stable/docs/guides/quickfort-library-guide.html) From 99f919c3dc438e58235a3459e30ca50e7f52bfad Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 15 Nov 2022 20:00:27 -0600 Subject: [PATCH 092/161] autofarm: insert missing output flushes insert calls to std::flush as appropriate fixes #2365 --- docs/changelog.txt | 1 + plugins/autofarm.cpp | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 995aceaa6..7ac0a412d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -43,6 +43,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `tiletypes`: no longer resets dig priority to the default when updating other properties of a tile - `automaterial`: fix rendering errors with box boundary markers - `autolabor` & `autohauler`: properly handle jobs 241, 242, and 243 +- `autofarm`: add missing output flushes - Core: fix the segmentation fault with the REPORT event in EventManager - Core: fix the new JOB_STARTED event only sending each event once, to the first handler listed diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index 1a02b6a06..2f8762425 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.cpp @@ -317,6 +317,8 @@ public: { set_farms(out, plants[ff.first], ff.second); } + + out << std::flush; } void status(color_ostream& out) @@ -336,6 +338,8 @@ public: out << plant->id << " limit " << getThreshold(th.first) << " current 0" << '\n'; } out << "Default: " << defaultThreshold << '\n'; + + out << std::flush; } }; @@ -409,7 +413,7 @@ static command_result setThresholds(color_ostream& out, std::vector } if (!ok) { - out << "Cannot find plant with id " << id << '\n'; + out << "Cannot find plant with id " << id << '\n' << std::flush; return CR_WRONG_USAGE; } } From 1c0a4182b773e5349a8697393382663596a0159f Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 16 Nov 2022 08:53:02 -0800 Subject: [PATCH 093/161] add a default overlay config with standard widgets --- dfhack-config/overlay.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 dfhack-config/overlay.json diff --git a/dfhack-config/overlay.json b/dfhack-config/overlay.json new file mode 100644 index 000000000..8ad7af0a1 --- /dev/null +++ b/dfhack-config/overlay.json @@ -0,0 +1,14 @@ +{ + "dwarfmonitor.date": { + "enabled": true + }, + "dwarfmonitor.misery": { + "enabled": true + }, + "dwarfmonitor.weather": { + "enabled": true + }, + "hotkeys.menu": { + "enabled": true + } +} From 1cc9a4d8329a869deb23c5cd211659c25b38d930 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 16 Nov 2022 12:31:24 -0800 Subject: [PATCH 094/161] make leggings instead of crafts out of shells they have a greater average trade price, and "shleggings" is hilarious. --- data/orders/basic.json | 3 ++- docs/plugins/orders.rst | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/data/orders/basic.json b/data/orders/basic.json index 4c785e7ab..b3fae8dcd 100644 --- a/data/orders/basic.json +++ b/data/orders/basic.json @@ -1004,7 +1004,8 @@ "value" : 2 } ], - "job" : "MakeCrafts", + "item_subtype" : "ITEM_PANTS_LEGGINGS", + "job" : "MakePants", "material_category" : [ "shell" diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 298800c96..bc15fd175 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -55,7 +55,7 @@ This collection of orders handles basic fort necessities: - thread/cloth/dye - pots/jugs/buckets/mugs - bags of leather, cloth, silk, and yarn -- crafts and totems from otherwise unusable by-products +- crafts, totems, and shleggings from otherwise unusable by-products - mechanisms/cages - splints/crutches - lye/soap @@ -66,6 +66,8 @@ This collection of orders handles basic fort necessities: You should import it as soon as you have enough dwarves to perform the tasks. Right after the first migration wave is usually a good time. +Armok's note: shleggings? Yes, `shleggings `__. + :source:`library/furnace ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 7b6cd148236813c46f236d245f1b0c6291288c23 Mon Sep 17 00:00:00 2001 From: myk002 Date: Wed, 16 Nov 2022 12:33:24 -0800 Subject: [PATCH 095/161] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 7ac0a412d..8413276f3 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - UX: You can now hold down the mouse button on a scrollbar to make it scroll multiple times. - UX: You can now drag the scrollbar to scroll to a specific spot - Constructions module: ``findAtTile`` now uses a binary search intead of a linear search. +- `orders`: replace shell craft orders with orders for shell leggings. they have a slightly higher trade price, and "shleggings" is hilarious. - `spectate`: new ``auto-unpause`` option for auto-dismissal of announcement pause events (e.g. sieges). - `spectate`: new ``auto-disengage`` option for auto-disengagement of plugin through player interaction whilst unpaused. - `spectate`: new ``focus-jobs`` option for following a dwarf after their job has finished (when disabled). From acd561ec9cdba6c01d9cdae6ed032a5915bb7dbc Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Fri, 18 Nov 2022 20:59:44 +0000 Subject: [PATCH 096/161] Auto-update submodules library/xml: master scripts: master --- library/xml | 2 +- scripts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/xml b/library/xml index 7513311fc..4c5697dcb 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit 7513311fc4f490bd7510fe79f6834731ea1581c8 +Subproject commit 4c5697dcb060d645849327410b8ecce6880053d4 diff --git a/scripts b/scripts index 08329f4da..1748c0a8b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 08329f4da18610eb2eaa7d038bf51ce7e3c91183 +Subproject commit 1748c0a8b1f19091eb675334f7c38be95cb7a2b8 From a16aca0f037083bc42856fbcc20d08b94145d8ce Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 18 Nov 2022 15:09:50 -0600 Subject: [PATCH 097/161] up MSVC warning level to /W3 /WX This makes MSVC warn at a level comparable to what we use on gcc for Linux builds --- CMakeLists.txt | 4 ++++ docs/changelog.txt | 3 +++ 2 files changed, 7 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e6f5c835..7c2cb36fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) if(MSVC) + # increase warning level + add_definitions("/WX") + add_definitions("/W3") + # disable C4819 code-page warning add_definitions("/wd4819") diff --git a/docs/changelog.txt b/docs/changelog.txt index 8413276f3..70531c8a6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -111,6 +111,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``gui.Screen.show()`` now returns ``self`` as a convenience - ``gui.View.getMousePos()`` now takes an optional ``ViewRect`` parameter in case the caller wants to get the mouse pos relative to a rect that is not the frame_body (such as the frame_rect) +## Build configuration +- MSVC warning level upped to /W3, and /WX added to make warnings cause compilations to fail. + # 0.47.05-r7 ## New Plugins From b6ffaebedaf1a32adfcfafd851dfeee162cb5029 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 18 Nov 2022 15:29:02 -0600 Subject: [PATCH 098/161] refine comments --- CMakeLists.txt | 2 +- docs/changelog.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c2cb36fc..c03037973 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) if(MSVC) - # increase warning level + # increase warning level and treat warnings as errors add_definitions("/WX") add_definitions("/W3") diff --git a/docs/changelog.txt b/docs/changelog.txt index 70531c8a6..83aca94e8 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -111,7 +111,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``gui.Screen.show()`` now returns ``self`` as a convenience - ``gui.View.getMousePos()`` now takes an optional ``ViewRect`` parameter in case the caller wants to get the mouse pos relative to a rect that is not the frame_body (such as the frame_rect) -## Build configuration +## Internal - MSVC warning level upped to /W3, and /WX added to make warnings cause compilations to fail. # 0.47.05-r7 From 0d5b80204f46f8a190085cd7d18a35410010f4cf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 18 Nov 2022 15:35:08 -0600 Subject: [PATCH 099/161] `Internals` not `Internal` --- docs/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 83aca94e8..8c5e4c98a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -111,7 +111,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``gui.Screen.show()`` now returns ``self`` as a convenience - ``gui.View.getMousePos()`` now takes an optional ``ViewRect`` parameter in case the caller wants to get the mouse pos relative to a rect that is not the frame_body (such as the frame_rect) -## Internal +## Internals - MSVC warning level upped to /W3, and /WX added to make warnings cause compilations to fail. # 0.47.05-r7 From 24b237ae25dd10cbefd31fc27743a5ef8766e03f Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 18 Nov 2022 17:35:50 -0800 Subject: [PATCH 100/161] Update changelog.txt --- docs/changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 0003c875e..d0bfa789f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -80,6 +80,8 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## API - ``Gui::anywhere_hotkey``: for plugin commands bound to keybindings that can be invoked on any screen +- add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter`` +- ``Gui::revealInDwarfmodeMap``: Now enforce valid view bounds when pos invalid, add variant accepting x, y, z - ``Lua::PushInterfaceKeys()``: transforms viewscreen ``feed()`` keys into something that can be interpreted by lua-based widgets - ``Lua::Push()``: now handles maps with otherwise supported keys and values - Units module: added new checks @@ -203,8 +205,6 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `quickfort`: `Dreamfort ` blueprint set improvements: new design for the services level, including were-bitten hospital recovery rooms and an appropriately-themed interrogation room next to the jail! Also fits better in a 1x1 embark for minimalist players. ## API -- add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter`` -- ``Gui::revealInDwarfmodeMap``: Now enforce valid view bounds when pos invalid, add variant accepting x, y, z - ``word_wrap``: argument ``bool collapse_whitespace`` converted to enum ``word_wrap_whitespace_mode mode``, with valid modes ``WSMODE_KEEP_ALL``, ``WSMODE_COLLAPSE_ALL``, and ``WSMODE_TRIM_LEADING``. ## Lua From ac5a1d35ae19d4e7c3b4003842d17d1151e44110 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 18 Nov 2022 17:39:49 -0800 Subject: [PATCH 101/161] remove duplicate function definition --- library/LuaApi.cpp | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 51a58d948..36d132124 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -1554,26 +1554,6 @@ static int gui_autoDFAnnouncement(lua_State *state) return 1; } -static int gui_getDwarfmodeViewDims(lua_State *state) -{ - auto dims = Gui::getDwarfmodeViewDims(); - lua_newtable(state); - Lua::TableInsert(state, "map_x1", dims.map_x1); - Lua::TableInsert(state, "map_x2", dims.map_x2); - Lua::TableInsert(state, "menu_x1", dims.menu_x1); - Lua::TableInsert(state, "menu_x2", dims.menu_x2); - Lua::TableInsert(state, "area_x1", dims.area_x1); - Lua::TableInsert(state, "area_x2", dims.area_x2); - Lua::TableInsert(state, "y1", dims.y1); - Lua::TableInsert(state, "y2", dims.y2); - Lua::TableInsert(state, "map_y1", dims.map_y1); - Lua::TableInsert(state, "map_y2", dims.map_y2); - Lua::TableInsert(state, "menu_on", dims.menu_on); - Lua::TableInsert(state, "area_on", dims.area_on); - Lua::TableInsert(state, "menu_forced", dims.menu_forced); - return 1; -} - static int gui_pauseRecenter(lua_State *state) { bool rv; From aa78c626f90cc83c45b6191af09d22e12141d27b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 20 Nov 2022 01:58:08 -0600 Subject: [PATCH 102/161] collect autolabor plugins into one directory these plugins all share code, moving them into a common directory will make refactoring the code shared between them more straightforward --- plugins/CMakeLists.txt | 4 +--- plugins/autolabor/CMakeLists.txt | 16 ++++++++++++++++ plugins/{ => autolabor}/autohauler.cpp | 0 plugins/{ => autolabor}/autolabor.cpp | 0 .../joblabormapper.cpp | 0 .../joblabormapper.h | 0 .../labormanager.cpp | 0 .../{labormanager => autolabor}/labormanager.h | 0 plugins/labormanager/CMakeLists.txt | 17 ----------------- 9 files changed, 17 insertions(+), 20 deletions(-) create mode 100644 plugins/autolabor/CMakeLists.txt rename plugins/{ => autolabor}/autohauler.cpp (100%) rename plugins/{ => autolabor}/autolabor.cpp (100%) rename plugins/{labormanager => autolabor}/joblabormapper.cpp (100%) rename plugins/{labormanager => autolabor}/joblabormapper.h (100%) rename plugins/{labormanager => autolabor}/labormanager.cpp (100%) rename plugins/{labormanager => autolabor}/labormanager.h (100%) delete mode 100644 plugins/labormanager/CMakeLists.txt diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7f22a47b3..9038d5d79 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -88,8 +88,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(autodump autodump.cpp) dfhack_plugin(autofarm autofarm.cpp) dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static) - dfhack_plugin(autohauler autohauler.cpp) - dfhack_plugin(autolabor autolabor.cpp) + add_subdirectory(autolabor) dfhack_plugin(automaterial automaterial.cpp LINK_LIBRARIES lua) dfhack_plugin(automelt automelt.cpp) dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua) @@ -133,7 +132,6 @@ if(BUILD_SUPPORTED) dfhack_plugin(infiniteSky infiniteSky.cpp) dfhack_plugin(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote) dfhack_plugin(jobutils jobutils.cpp) - add_subdirectory(labormanager) dfhack_plugin(lair lair.cpp) dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua) dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread) diff --git a/plugins/autolabor/CMakeLists.txt b/plugins/autolabor/CMakeLists.txt new file mode 100644 index 000000000..73ae5e3c5 --- /dev/null +++ b/plugins/autolabor/CMakeLists.txt @@ -0,0 +1,16 @@ +project(autolahor) +# A list of source files +set(COMMON_SRCS +) +# A list of headers +set(COMMON_HDRS +) +set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) + +# mash them together (headers are marked as headers and nothing will try to compile them) +list(APPEND COMMON_SRCS ${COMMON_HDRS}) + +dfhack_plugin(labormanager labormanager.cpp joblabormapper.cpp ${COMMON_SRCS}) + +dfhack_plugin(autohauler autohauler.cpp ${COMMON_SRCS}) +dfhack_plugin(autolabor autolabor.cpp ${COMMON_SRCS}) diff --git a/plugins/autohauler.cpp b/plugins/autolabor/autohauler.cpp similarity index 100% rename from plugins/autohauler.cpp rename to plugins/autolabor/autohauler.cpp diff --git a/plugins/autolabor.cpp b/plugins/autolabor/autolabor.cpp similarity index 100% rename from plugins/autolabor.cpp rename to plugins/autolabor/autolabor.cpp diff --git a/plugins/labormanager/joblabormapper.cpp b/plugins/autolabor/joblabormapper.cpp similarity index 100% rename from plugins/labormanager/joblabormapper.cpp rename to plugins/autolabor/joblabormapper.cpp diff --git a/plugins/labormanager/joblabormapper.h b/plugins/autolabor/joblabormapper.h similarity index 100% rename from plugins/labormanager/joblabormapper.h rename to plugins/autolabor/joblabormapper.h diff --git a/plugins/labormanager/labormanager.cpp b/plugins/autolabor/labormanager.cpp similarity index 100% rename from plugins/labormanager/labormanager.cpp rename to plugins/autolabor/labormanager.cpp diff --git a/plugins/labormanager/labormanager.h b/plugins/autolabor/labormanager.h similarity index 100% rename from plugins/labormanager/labormanager.h rename to plugins/autolabor/labormanager.h diff --git a/plugins/labormanager/CMakeLists.txt b/plugins/labormanager/CMakeLists.txt deleted file mode 100644 index 787028452..000000000 --- a/plugins/labormanager/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -project(labormanager) -# A list of source files -set(PROJECT_SRCS - labormanager.cpp - joblabormapper.cpp -) -# A list of headers -set(PROJECT_HDRS - labormanager.h - joblabormapper.h -) -set_source_files_properties(${PROJECT_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) - -# mash them together (headers are marked as headers and nothing will try to compile them) -list(APPEND PROJECT_SRCS ${PROJECT_HDRS}) - -dfhack_plugin(labormanager ${PROJECT_SRCS}) From 2453b34194a9360873858b3c266d09ef6ed129bf Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 20 Nov 2022 10:09:52 -0600 Subject: [PATCH 103/161] refactor common labor state map table eliminates code duplication --- plugins/autolabor/CMakeLists.txt | 2 +- plugins/autolabor/autohauler.cpp | 287 +--------------------------- plugins/autolabor/autolabor.cpp | 279 +-------------------------- plugins/autolabor/labormanager.cpp | 276 +-------------------------- plugins/autolabor/laborstatemap.h | 294 +++++++++++++++++++++++++++++ 5 files changed, 302 insertions(+), 836 deletions(-) create mode 100644 plugins/autolabor/laborstatemap.h diff --git a/plugins/autolabor/CMakeLists.txt b/plugins/autolabor/CMakeLists.txt index 73ae5e3c5..96f3026b4 100644 --- a/plugins/autolabor/CMakeLists.txt +++ b/plugins/autolabor/CMakeLists.txt @@ -3,7 +3,7 @@ project(autolahor) set(COMMON_SRCS ) # A list of headers -set(COMMON_HDRS +set(COMMON_HDRS laborstatemap.h ) set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/plugins/autolabor/autohauler.cpp b/plugins/autolabor/autohauler.cpp index 5b6d573a6..691dc2e8e 100644 --- a/plugins/autolabor/autohauler.cpp +++ b/plugins/autolabor/autohauler.cpp @@ -43,6 +43,8 @@ #include "modules/Items.h" #include "modules/Units.h" +#include "laborstatemap.h" + // Not sure what this does, but may have to figure it out later #define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) @@ -127,290 +129,7 @@ static void setOptionEnabled(ConfigFlags flag, bool on) } // This is a vector of states and number of dwarves in that state -static std::vector state_count(5); - -// Employment status of dwarves -enum dwarf_state { - // Ready for a new task - IDLE, - - // Busy with a useful task - BUSY, - - // In the military, can't work - MILITARY, - - // Baby or Child, can't work - CHILD, - - // Doing something that precludes working, may be busy for a while - OTHER -}; - -// I presume this is the number of states in the following enumeration. -static const int NUM_STATE = 5; - -// This is a list of strings to be associated with aforementioned dwarf_state -// struct -static const char *state_names[] = { - "IDLE", - "BUSY", - "MILITARY", - "CHILD", - "OTHER" -}; - -// List of possible activites of a dwarf that will be further narrowed to states -// IDLE - Specifically waiting to be assigned a task (No Job) -// BUSY - Performing a toggleable labor, or a support action for that labor. -// OTHER - Doing something else - -static const dwarf_state dwarf_states[] = { - BUSY /* CarveFortification */, - BUSY /* DetailWall */, - BUSY /* DetailFloor */, - BUSY /* Dig */, - BUSY /* CarveUpwardStaircase */, - BUSY /* CarveDownwardStaircase */, - BUSY /* CarveUpDownStaircase */, - BUSY /* CarveRamp */, - BUSY /* DigChannel */, - BUSY /* FellTree */, - BUSY /* GatherPlants */, - BUSY /* RemoveConstruction */, - BUSY /* CollectWebs */, - BUSY /* BringItemToDepot */, - BUSY /* BringItemToShop */, - OTHER /* Eat */, - OTHER /* GetProvisions */, - OTHER /* Drink */, - OTHER /* Drink2 */, - OTHER /* FillWaterskin */, - OTHER /* FillWaterskin2 */, - OTHER /* Sleep */, - BUSY /* CollectSand */, - BUSY /* Fish */, - BUSY /* Hunt */, - BUSY /* HuntVermin */, - OTHER /* Kidnap */, - OTHER /* BeatCriminal */, - OTHER /* StartingFistFight */, - OTHER /* CollectTaxes */, - OTHER /* GuardTaxCollector */, - BUSY /* CatchLiveLandAnimal */, - BUSY /* CatchLiveFish */, - OTHER /* ReturnKill */, - OTHER /* CheckChest */, - OTHER /* StoreOwnedItem */, - BUSY /* PlaceItemInTomb */, - BUSY /* StoreItemInStockpile */, - BUSY /* StoreItemInBag */, - BUSY /* StoreItemInHospital */, - BUSY /* StoreItemInChest */, - BUSY /* StoreItemInCabinet */, - BUSY /* StoreWeapon */, - BUSY /* StoreArmor */, - BUSY /* StoreItemInBarrel */, - BUSY /* StoreItemInBin */, - OTHER /* SeekArtifact */, - OTHER /* SeekInfant */, - OTHER /* AttendParty */, - OTHER /* GoShopping */, - OTHER /* GoShopping2 */, - OTHER /* Clean */, - OTHER /* Rest */, - BUSY /* PickupEquipment */, - BUSY /* DumpItem */, - OTHER /* StrangeMoodCrafter */, - OTHER /* StrangeMoodJeweller */, - OTHER /* StrangeMoodForge */, - OTHER /* StrangeMoodMagmaForge */, - OTHER /* StrangeMoodBrooding */, - OTHER /* StrangeMoodFell */, - OTHER /* StrangeMoodCarpenter */, - OTHER /* StrangeMoodMason */, - OTHER /* StrangeMoodBowyer */, - OTHER /* StrangeMoodTanner */, - OTHER /* StrangeMoodWeaver */, - OTHER /* StrangeMoodGlassmaker */, - OTHER /* StrangeMoodMechanics */, - BUSY /* ConstructBuilding */, - BUSY /* ConstructDoor */, - BUSY /* ConstructFloodgate */, - BUSY /* ConstructBed */, - BUSY /* ConstructThrone */, - BUSY /* ConstructCoffin */, - BUSY /* ConstructTable */, - BUSY /* ConstructChest */, - BUSY /* ConstructBin */, - BUSY /* ConstructArmorStand */, - BUSY /* ConstructWeaponRack */, - BUSY /* ConstructCabinet */, - BUSY /* ConstructStatue */, - BUSY /* ConstructBlocks */, - BUSY /* MakeRawGlass */, - BUSY /* MakeCrafts */, - BUSY /* MintCoins */, - BUSY /* CutGems */, - BUSY /* CutGlass */, - BUSY /* EncrustWithGems */, - BUSY /* EncrustWithGlass */, - BUSY /* DestroyBuilding */, - BUSY /* SmeltOre */, - BUSY /* MeltMetalObject */, - BUSY /* ExtractMetalStrands */, - BUSY /* PlantSeeds */, - BUSY /* HarvestPlants */, - BUSY /* TrainHuntingAnimal */, - BUSY /* TrainWarAnimal */, - BUSY /* MakeWeapon */, - BUSY /* ForgeAnvil */, - BUSY /* ConstructCatapultParts */, - BUSY /* ConstructBallistaParts */, - BUSY /* MakeArmor */, - BUSY /* MakeHelm */, - BUSY /* MakePants */, - BUSY /* StudWith */, - BUSY /* ButcherAnimal */, - BUSY /* PrepareRawFish */, - BUSY /* MillPlants */, - BUSY /* BaitTrap */, - BUSY /* MilkCreature */, - BUSY /* MakeCheese */, - BUSY /* ProcessPlants */, - BUSY /* ProcessPlantsBag */, - BUSY /* ProcessPlantsVial */, - BUSY /* ProcessPlantsBarrel */, - BUSY /* PrepareMeal */, - BUSY /* WeaveCloth */, - BUSY /* MakeGloves */, - BUSY /* MakeShoes */, - BUSY /* MakeShield */, - BUSY /* MakeCage */, - BUSY /* MakeChain */, - BUSY /* MakeFlask */, - BUSY /* MakeGoblet */, - BUSY /* MakeInstrument */, - BUSY /* MakeToy */, - BUSY /* MakeAnimalTrap */, - BUSY /* MakeBarrel */, - BUSY /* MakeBucket */, - BUSY /* MakeWindow */, - BUSY /* MakeTotem */, - BUSY /* MakeAmmo */, - BUSY /* DecorateWith */, - BUSY /* MakeBackpack */, - BUSY /* MakeQuiver */, - BUSY /* MakeBallistaArrowHead */, - BUSY /* AssembleSiegeAmmo */, - BUSY /* LoadCatapult */, - BUSY /* LoadBallista */, - BUSY /* FireCatapult */, - BUSY /* FireBallista */, - BUSY /* ConstructMechanisms */, - BUSY /* MakeTrapComponent */, - BUSY /* LoadCageTrap */, - BUSY /* LoadStoneTrap */, - BUSY /* LoadWeaponTrap */, - BUSY /* CleanTrap */, - OTHER /* CastSpell */, - BUSY /* LinkBuildingToTrigger */, - BUSY /* PullLever */, - BUSY /* BrewDrink */, - BUSY /* ExtractFromPlants */, - BUSY /* ExtractFromRawFish */, - BUSY /* ExtractFromLandAnimal */, - BUSY /* TameVermin */, - BUSY /* TameAnimal */, - BUSY /* ChainAnimal */, - BUSY /* UnchainAnimal */, - BUSY /* UnchainPet */, - BUSY /* ReleaseLargeCreature */, - BUSY /* ReleasePet */, - BUSY /* ReleaseSmallCreature */, - BUSY /* HandleSmallCreature */, - BUSY /* HandleLargeCreature */, - BUSY /* CageLargeCreature */, - BUSY /* CageSmallCreature */, - BUSY /* RecoverWounded */, - BUSY /* DiagnosePatient */, - BUSY /* ImmobilizeBreak */, - BUSY /* DressWound */, - BUSY /* CleanPatient */, - BUSY /* Surgery */, - BUSY /* Suture */, - BUSY /* SetBone */, - BUSY /* PlaceInTraction */, - BUSY /* DrainAquarium */, - BUSY /* FillAquarium */, - BUSY /* FillPond */, - BUSY /* GiveWater */, - BUSY /* GiveFood */, - BUSY /* GiveWater2 */, - BUSY /* GiveFood2 */, - BUSY /* RecoverPet */, - BUSY /* PitLargeAnimal */, - BUSY /* PitSmallAnimal */, - BUSY /* SlaughterAnimal */, - BUSY /* MakeCharcoal */, - BUSY /* MakeAsh */, - BUSY /* MakeLye */, - BUSY /* MakePotashFromLye */, - BUSY /* FertilizeField */, - BUSY /* MakePotashFromAsh */, - BUSY /* DyeThread */, - BUSY /* DyeCloth */, - BUSY /* SewImage */, - BUSY /* MakePipeSection */, - BUSY /* OperatePump */, - OTHER /* ManageWorkOrders */, - OTHER /* UpdateStockpileRecords */, - OTHER /* TradeAtDepot */, - BUSY /* ConstructHatchCover */, - BUSY /* ConstructGrate */, - BUSY /* RemoveStairs */, - BUSY /* ConstructQuern */, - BUSY /* ConstructMillstone */, - BUSY /* ConstructSplint */, - BUSY /* ConstructCrutch */, - BUSY /* ConstructTractionBench */, - OTHER /* CleanSelf */, - BUSY /* BringCrutch */, - BUSY /* ApplyCast */, - BUSY /* CustomReaction */, - BUSY /* ConstructSlab */, - BUSY /* EngraveSlab */, - BUSY /* ShearCreature */, - BUSY /* SpinThread */, - BUSY /* PenLargeAnimal */, - BUSY /* PenSmallAnimal */, - BUSY /* MakeTool */, - BUSY /* CollectClay */, - BUSY /* InstallColonyInHive */, - BUSY /* CollectHiveProducts */, - OTHER /* CauseTrouble */, - OTHER /* DrinkBlood */, - OTHER /* ReportCrime */, - OTHER /* ExecuteCriminal */, - BUSY /* TrainAnimal */, - BUSY /* CarveTrack */, - BUSY /* PushTrackVehicle */, - BUSY /* PlaceTrackVehicle */, - BUSY /* StoreItemInVehicle */, - BUSY /* GeldAnimal */, - BUSY /* MakeFigurine */, - BUSY /* MakeAmulet */, - BUSY /* MakeScepter */, - BUSY /* MakeCrown */, - BUSY /* MakeRing */, - BUSY /* MakeEarring */, - BUSY /* MakeBracelet */, - BUSY /* MakeGem */, - BUSY /* PutItemOnDisplay */, - OTHER /* unk_fake_no_job */, - OTHER /* InterrogateSubject */, - OTHER /* unk_fake_no_activity */, -}; +static std::vector state_count(NUM_STATE); // Mode assigned to labors. Either it's a hauling job, or it's not. enum labor_mode { diff --git a/plugins/autolabor/autolabor.cpp b/plugins/autolabor/autolabor.cpp index 97bd11a11..198a588cc 100644 --- a/plugins/autolabor/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -40,6 +40,8 @@ #include "modules/Items.h" #include "modules/Units.h" +#include "laborstatemap.h" + using std::string; using std::endl; using std::vector; @@ -102,283 +104,6 @@ enum labor_mode { AUTOMATIC, }; -enum dwarf_state { - // Ready for a new task - IDLE, - - // Busy with a useful task - BUSY, - - // Busy with a useful task that requires a tool - EXCLUSIVE, - - // In the military, can't work - MILITARY, - - // Child or noble, can't work - CHILD, - - // Doing something that precludes working, may be busy for a while - OTHER -}; - -const int NUM_STATE = 6; - -static const char *state_names[] = { - "IDLE", - "BUSY", - "EXCLUSIVE", - "MILITARY", - "CHILD", - "OTHER", -}; - -static const dwarf_state dwarf_states[] = { - BUSY /* CarveFortification */, - BUSY /* DetailWall */, - BUSY /* DetailFloor */, - EXCLUSIVE /* Dig */, - EXCLUSIVE /* CarveUpwardStaircase */, - EXCLUSIVE /* CarveDownwardStaircase */, - EXCLUSIVE /* CarveUpDownStaircase */, - EXCLUSIVE /* CarveRamp */, - EXCLUSIVE /* DigChannel */, - EXCLUSIVE /* FellTree */, - BUSY /* GatherPlants */, - BUSY /* RemoveConstruction */, - BUSY /* CollectWebs */, - BUSY /* BringItemToDepot */, - BUSY /* BringItemToShop */, - OTHER /* Eat */, - OTHER /* GetProvisions */, - OTHER /* Drink */, - OTHER /* Drink2 */, - OTHER /* FillWaterskin */, - OTHER /* FillWaterskin2 */, - OTHER /* Sleep */, - BUSY /* CollectSand */, - BUSY /* Fish */, - EXCLUSIVE /* Hunt */, - OTHER /* HuntVermin */, - BUSY /* Kidnap */, - BUSY /* BeatCriminal */, - BUSY /* StartingFistFight */, - BUSY /* CollectTaxes */, - BUSY /* GuardTaxCollector */, - BUSY /* CatchLiveLandAnimal */, - BUSY /* CatchLiveFish */, - BUSY /* ReturnKill */, - BUSY /* CheckChest */, - BUSY /* StoreOwnedItem */, - BUSY /* PlaceItemInTomb */, - BUSY /* StoreItemInStockpile */, - BUSY /* StoreItemInBag */, - BUSY /* StoreItemInHospital */, - BUSY /* StoreItemInChest */, - BUSY /* StoreItemInCabinet */, - BUSY /* StoreWeapon */, - BUSY /* StoreArmor */, - BUSY /* StoreItemInBarrel */, - BUSY /* StoreItemInBin */, - BUSY /* SeekArtifact */, - BUSY /* SeekInfant */, - OTHER /* AttendParty */, - OTHER /* GoShopping */, - OTHER /* GoShopping2 */, - BUSY /* Clean */, - OTHER /* Rest */, - EXCLUSIVE /* PickupEquipment */, - BUSY /* DumpItem */, - OTHER /* StrangeMoodCrafter */, - OTHER /* StrangeMoodJeweller */, - OTHER /* StrangeMoodForge */, - OTHER /* StrangeMoodMagmaForge */, - OTHER /* StrangeMoodBrooding */, - OTHER /* StrangeMoodFell */, - OTHER /* StrangeMoodCarpenter */, - OTHER /* StrangeMoodMason */, - OTHER /* StrangeMoodBowyer */, - OTHER /* StrangeMoodTanner */, - OTHER /* StrangeMoodWeaver */, - OTHER /* StrangeMoodGlassmaker */, - OTHER /* StrangeMoodMechanics */, - BUSY /* ConstructBuilding */, - BUSY /* ConstructDoor */, - BUSY /* ConstructFloodgate */, - BUSY /* ConstructBed */, - BUSY /* ConstructThrone */, - BUSY /* ConstructCoffin */, - BUSY /* ConstructTable */, - BUSY /* ConstructChest */, - BUSY /* ConstructBin */, - BUSY /* ConstructArmorStand */, - BUSY /* ConstructWeaponRack */, - BUSY /* ConstructCabinet */, - BUSY /* ConstructStatue */, - BUSY /* ConstructBlocks */, - BUSY /* MakeRawGlass */, - BUSY /* MakeCrafts */, - BUSY /* MintCoins */, - BUSY /* CutGems */, - BUSY /* CutGlass */, - BUSY /* EncrustWithGems */, - BUSY /* EncrustWithGlass */, - BUSY /* DestroyBuilding */, - BUSY /* SmeltOre */, - BUSY /* MeltMetalObject */, - BUSY /* ExtractMetalStrands */, - BUSY /* PlantSeeds */, - BUSY /* HarvestPlants */, - BUSY /* TrainHuntingAnimal */, - BUSY /* TrainWarAnimal */, - BUSY /* MakeWeapon */, - BUSY /* ForgeAnvil */, - BUSY /* ConstructCatapultParts */, - BUSY /* ConstructBallistaParts */, - BUSY /* MakeArmor */, - BUSY /* MakeHelm */, - BUSY /* MakePants */, - BUSY /* StudWith */, - BUSY /* ButcherAnimal */, - BUSY /* PrepareRawFish */, - BUSY /* MillPlants */, - BUSY /* BaitTrap */, - BUSY /* MilkCreature */, - BUSY /* MakeCheese */, - BUSY /* ProcessPlants */, - BUSY /* ProcessPlantsBag */, - BUSY /* ProcessPlantsVial */, - BUSY /* ProcessPlantsBarrel */, - BUSY /* PrepareMeal */, - BUSY /* WeaveCloth */, - BUSY /* MakeGloves */, - BUSY /* MakeShoes */, - BUSY /* MakeShield */, - BUSY /* MakeCage */, - BUSY /* MakeChain */, - BUSY /* MakeFlask */, - BUSY /* MakeGoblet */, - BUSY /* MakeInstrument */, - BUSY /* MakeToy */, - BUSY /* MakeAnimalTrap */, - BUSY /* MakeBarrel */, - BUSY /* MakeBucket */, - BUSY /* MakeWindow */, - BUSY /* MakeTotem */, - BUSY /* MakeAmmo */, - BUSY /* DecorateWith */, - BUSY /* MakeBackpack */, - BUSY /* MakeQuiver */, - BUSY /* MakeBallistaArrowHead */, - BUSY /* AssembleSiegeAmmo */, - BUSY /* LoadCatapult */, - BUSY /* LoadBallista */, - BUSY /* FireCatapult */, - BUSY /* FireBallista */, - BUSY /* ConstructMechanisms */, - BUSY /* MakeTrapComponent */, - BUSY /* LoadCageTrap */, - BUSY /* LoadStoneTrap */, - BUSY /* LoadWeaponTrap */, - BUSY /* CleanTrap */, - BUSY /* CastSpell */, - BUSY /* LinkBuildingToTrigger */, - BUSY /* PullLever */, - BUSY /* BrewDrink */, - BUSY /* ExtractFromPlants */, - BUSY /* ExtractFromRawFish */, - BUSY /* ExtractFromLandAnimal */, - BUSY /* TameVermin */, - BUSY /* TameAnimal */, - BUSY /* ChainAnimal */, - BUSY /* UnchainAnimal */, - BUSY /* UnchainPet */, - BUSY /* ReleaseLargeCreature */, - BUSY /* ReleasePet */, - BUSY /* ReleaseSmallCreature */, - BUSY /* HandleSmallCreature */, - BUSY /* HandleLargeCreature */, - BUSY /* CageLargeCreature */, - BUSY /* CageSmallCreature */, - BUSY /* RecoverWounded */, - BUSY /* DiagnosePatient */, - BUSY /* ImmobilizeBreak */, - BUSY /* DressWound */, - BUSY /* CleanPatient */, - BUSY /* Surgery */, - BUSY /* Suture */, - BUSY /* SetBone */, - BUSY /* PlaceInTraction */, - BUSY /* DrainAquarium */, - BUSY /* FillAquarium */, - BUSY /* FillPond */, - BUSY /* GiveWater */, - BUSY /* GiveFood */, - BUSY /* GiveWater2 */, - BUSY /* GiveFood2 */, - BUSY /* RecoverPet */, - BUSY /* PitLargeAnimal */, - BUSY /* PitSmallAnimal */, - BUSY /* SlaughterAnimal */, - BUSY /* MakeCharcoal */, - BUSY /* MakeAsh */, - BUSY /* MakeLye */, - BUSY /* MakePotashFromLye */, - BUSY /* FertilizeField */, - BUSY /* MakePotashFromAsh */, - BUSY /* DyeThread */, - BUSY /* DyeCloth */, - BUSY /* SewImage */, - BUSY /* MakePipeSection */, - BUSY /* OperatePump */, - OTHER /* ManageWorkOrders */, - OTHER /* UpdateStockpileRecords */, - OTHER /* TradeAtDepot */, - BUSY /* ConstructHatchCover */, - BUSY /* ConstructGrate */, - BUSY /* RemoveStairs */, - BUSY /* ConstructQuern */, - BUSY /* ConstructMillstone */, - BUSY /* ConstructSplint */, - BUSY /* ConstructCrutch */, - BUSY /* ConstructTractionBench */, - BUSY /* CleanSelf */, - BUSY /* BringCrutch */, - BUSY /* ApplyCast */, - BUSY /* CustomReaction */, - BUSY /* ConstructSlab */, - BUSY /* EngraveSlab */, - BUSY /* ShearCreature */, - BUSY /* SpinThread */, - BUSY /* PenLargeAnimal */, - BUSY /* PenSmallAnimal */, - BUSY /* MakeTool */, - BUSY /* CollectClay */, - BUSY /* InstallColonyInHive */, - BUSY /* CollectHiveProducts */, - OTHER /* CauseTrouble */, - OTHER /* DrinkBlood */, - OTHER /* ReportCrime */, - OTHER /* ExecuteCriminal */, - BUSY /* TrainAnimal */, - BUSY /* CarveTrack */, - BUSY /* PushTrackVehicle */, - BUSY /* PlaceTrackVehicle */, - BUSY /* StoreItemInVehicle */, - BUSY /* GeldAnimal */, - BUSY /* MakeFigurine */, - BUSY /* MakeAmulet */, - BUSY /* MakeScepter */, - BUSY /* MakeCrown */, - BUSY /* MakeRing */, - BUSY /* MakeEarring */, - BUSY /* MakeBracelet */, - BUSY /* MakeGem */, - BUSY /* PutItemOnDisplay */, - OTHER /* unk_fake_no_job */, - OTHER /* InterrogateSubject */, - OTHER /* unk_fake_no_activity */, -}; struct labor_info { diff --git a/plugins/autolabor/labormanager.cpp b/plugins/autolabor/labormanager.cpp index f508c9797..44817e405 100644 --- a/plugins/autolabor/labormanager.cpp +++ b/plugins/autolabor/labormanager.cpp @@ -75,6 +75,8 @@ #include "labormanager.h" #include "joblabormapper.h" +#include "laborstatemap.h" + using namespace std; using std::string; using std::endl; @@ -116,280 +118,6 @@ DFHACK_PLUGIN("labormanager"); static void generate_labor_to_skill_map(); -enum dwarf_state { - // Ready for a new task - IDLE, - - // Busy with a useful task - BUSY, - - // In the military, can't work - MILITARY, - - // Child or noble, can't work - CHILD, - - // Doing something that precludes working, may be busy for a while - OTHER -}; - -const int NUM_STATE = 5; - -static const char *state_names[] = { - "IDLE", - "BUSY", - "MILITARY", - "CHILD", - "OTHER", -}; - -static const dwarf_state dwarf_states[] = { - BUSY /* CarveFortification */, - BUSY /* DetailWall */, - BUSY /* DetailFloor */, - BUSY /* Dig */, - BUSY /* CarveUpwardStaircase */, - BUSY /* CarveDownwardStaircase */, - BUSY /* CarveUpDownStaircase */, - BUSY /* CarveRamp */, - BUSY /* DigChannel */, - BUSY /* FellTree */, - BUSY /* GatherPlants */, - BUSY /* RemoveConstruction */, - BUSY /* CollectWebs */, - BUSY /* BringItemToDepot */, - BUSY /* BringItemToShop */, - OTHER /* Eat */, - OTHER /* GetProvisions */, - OTHER /* Drink */, - OTHER /* Drink2 */, - OTHER /* FillWaterskin */, - OTHER /* FillWaterskin2 */, - OTHER /* Sleep */, - BUSY /* CollectSand */, - BUSY /* Fish */, - BUSY /* Hunt */, - OTHER /* HuntVermin */, - BUSY /* Kidnap */, - BUSY /* BeatCriminal */, - BUSY /* StartingFistFight */, - BUSY /* CollectTaxes */, - BUSY /* GuardTaxCollector */, - BUSY /* CatchLiveLandAnimal */, - BUSY /* CatchLiveFish */, - BUSY /* ReturnKill */, - BUSY /* CheckChest */, - BUSY /* StoreOwnedItem */, - BUSY /* PlaceItemInTomb */, - BUSY /* StoreItemInStockpile */, - BUSY /* StoreItemInBag */, - BUSY /* StoreItemInHospital */, - BUSY /* StoreItemInChest */, - BUSY /* StoreItemInCabinet */, - BUSY /* StoreWeapon */, - BUSY /* StoreArmor */, - BUSY /* StoreItemInBarrel */, - BUSY /* StoreItemInBin */, - BUSY /* SeekArtifact */, - BUSY /* SeekInfant */, - OTHER /* AttendParty */, - OTHER /* GoShopping */, - OTHER /* GoShopping2 */, - BUSY /* Clean */, - OTHER /* Rest */, - OTHER /* PickupEquipment */, - BUSY /* DumpItem */, - OTHER /* StrangeMoodCrafter */, - OTHER /* StrangeMoodJeweller */, - OTHER /* StrangeMoodForge */, - OTHER /* StrangeMoodMagmaForge */, - OTHER /* StrangeMoodBrooding */, - OTHER /* StrangeMoodFell */, - OTHER /* StrangeMoodCarpenter */, - OTHER /* StrangeMoodMason */, - OTHER /* StrangeMoodBowyer */, - OTHER /* StrangeMoodTanner */, - OTHER /* StrangeMoodWeaver */, - OTHER /* StrangeMoodGlassmaker */, - OTHER /* StrangeMoodMechanics */, - BUSY /* ConstructBuilding */, - BUSY /* ConstructDoor */, - BUSY /* ConstructFloodgate */, - BUSY /* ConstructBed */, - BUSY /* ConstructThrone */, - BUSY /* ConstructCoffin */, - BUSY /* ConstructTable */, - BUSY /* ConstructChest */, - BUSY /* ConstructBin */, - BUSY /* ConstructArmorStand */, - BUSY /* ConstructWeaponRack */, - BUSY /* ConstructCabinet */, - BUSY /* ConstructStatue */, - BUSY /* ConstructBlocks */, - BUSY /* MakeRawGlass */, - BUSY /* MakeCrafts */, - BUSY /* MintCoins */, - BUSY /* CutGems */, - BUSY /* CutGlass */, - BUSY /* EncrustWithGems */, - BUSY /* EncrustWithGlass */, - BUSY /* DestroyBuilding */, - BUSY /* SmeltOre */, - BUSY /* MeltMetalObject */, - BUSY /* ExtractMetalStrands */, - BUSY /* PlantSeeds */, - BUSY /* HarvestPlants */, - BUSY /* TrainHuntingAnimal */, - BUSY /* TrainWarAnimal */, - BUSY /* MakeWeapon */, - BUSY /* ForgeAnvil */, - BUSY /* ConstructCatapultParts */, - BUSY /* ConstructBallistaParts */, - BUSY /* MakeArmor */, - BUSY /* MakeHelm */, - BUSY /* MakePants */, - BUSY /* StudWith */, - BUSY /* ButcherAnimal */, - BUSY /* PrepareRawFish */, - BUSY /* MillPlants */, - BUSY /* BaitTrap */, - BUSY /* MilkCreature */, - BUSY /* MakeCheese */, - BUSY /* ProcessPlants */, - BUSY /* ProcessPlantsBag */, - BUSY /* ProcessPlantsVial */, - BUSY /* ProcessPlantsBarrel */, - BUSY /* PrepareMeal */, - BUSY /* WeaveCloth */, - BUSY /* MakeGloves */, - BUSY /* MakeShoes */, - BUSY /* MakeShield */, - BUSY /* MakeCage */, - BUSY /* MakeChain */, - BUSY /* MakeFlask */, - BUSY /* MakeGoblet */, - BUSY /* MakeInstrument */, - BUSY /* MakeToy */, - BUSY /* MakeAnimalTrap */, - BUSY /* MakeBarrel */, - BUSY /* MakeBucket */, - BUSY /* MakeWindow */, - BUSY /* MakeTotem */, - BUSY /* MakeAmmo */, - BUSY /* DecorateWith */, - BUSY /* MakeBackpack */, - BUSY /* MakeQuiver */, - BUSY /* MakeBallistaArrowHead */, - BUSY /* AssembleSiegeAmmo */, - BUSY /* LoadCatapult */, - BUSY /* LoadBallista */, - BUSY /* FireCatapult */, - BUSY /* FireBallista */, - BUSY /* ConstructMechanisms */, - BUSY /* MakeTrapComponent */, - BUSY /* LoadCageTrap */, - BUSY /* LoadStoneTrap */, - BUSY /* LoadWeaponTrap */, - BUSY /* CleanTrap */, - BUSY /* CastSpell */, - BUSY /* LinkBuildingToTrigger */, - BUSY /* PullLever */, - BUSY /* BrewDrink */, - BUSY /* ExtractFromPlants */, - BUSY /* ExtractFromRawFish */, - BUSY /* ExtractFromLandAnimal */, - BUSY /* TameVermin */, - BUSY /* TameAnimal */, - BUSY /* ChainAnimal */, - BUSY /* UnchainAnimal */, - BUSY /* UnchainPet */, - BUSY /* ReleaseLargeCreature */, - BUSY /* ReleasePet */, - BUSY /* ReleaseSmallCreature */, - BUSY /* HandleSmallCreature */, - BUSY /* HandleLargeCreature */, - BUSY /* CageLargeCreature */, - BUSY /* CageSmallCreature */, - BUSY /* RecoverWounded */, - BUSY /* DiagnosePatient */, - BUSY /* ImmobilizeBreak */, - BUSY /* DressWound */, - BUSY /* CleanPatient */, - BUSY /* Surgery */, - BUSY /* Suture */, - BUSY /* SetBone */, - BUSY /* PlaceInTraction */, - BUSY /* DrainAquarium */, - BUSY /* FillAquarium */, - BUSY /* FillPond */, - BUSY /* GiveWater */, - BUSY /* GiveFood */, - BUSY /* GiveWater2 */, - BUSY /* GiveFood2 */, - BUSY /* RecoverPet */, - BUSY /* PitLargeAnimal */, - BUSY /* PitSmallAnimal */, - BUSY /* SlaughterAnimal */, - BUSY /* MakeCharcoal */, - BUSY /* MakeAsh */, - BUSY /* MakeLye */, - BUSY /* MakePotashFromLye */, - BUSY /* FertilizeField */, - BUSY /* MakePotashFromAsh */, - BUSY /* DyeThread */, - BUSY /* DyeCloth */, - BUSY /* SewImage */, - BUSY /* MakePipeSection */, - BUSY /* OperatePump */, - OTHER /* ManageWorkOrders */, - OTHER /* UpdateStockpileRecords */, - OTHER /* TradeAtDepot */, - BUSY /* ConstructHatchCover */, - BUSY /* ConstructGrate */, - BUSY /* RemoveStairs */, - BUSY /* ConstructQuern */, - BUSY /* ConstructMillstone */, - BUSY /* ConstructSplint */, - BUSY /* ConstructCrutch */, - BUSY /* ConstructTractionBench */, - BUSY /* CleanSelf */, - BUSY /* BringCrutch */, - BUSY /* ApplyCast */, - BUSY /* CustomReaction */, - BUSY /* ConstructSlab */, - BUSY /* EngraveSlab */, - BUSY /* ShearCreature */, - BUSY /* SpinThread */, - BUSY /* PenLargeAnimal */, - BUSY /* PenSmallAnimal */, - BUSY /* MakeTool */, - BUSY /* CollectClay */, - BUSY /* InstallColonyInHive */, - BUSY /* CollectHiveProducts */, - OTHER /* CauseTrouble */, - OTHER /* DrinkBlood */, - OTHER /* ReportCrime */, - OTHER /* ExecuteCriminal */, - BUSY /* TrainAnimal */, - BUSY /* CarveTrack */, - BUSY /* PushTrackVehicle */, - BUSY /* PlaceTrackVehicle */, - BUSY /* StoreItemInVehicle */, - BUSY /* GeldAnimal */, - BUSY /* MakeFigurine */, - BUSY /* MakeAmulet */, - BUSY /* MakeScepter */, - BUSY /* MakeCrown */, - BUSY /* MakeRing */, - BUSY /* MakeEarring */, - BUSY /* MakeBracelet */, - BUSY /* MakeGem */, - BUSY /* PutItemOnDisplay */, - OTHER /* unk_fake_no_job */, - OTHER /* InterrogateSubject */, - OTHER /* unk_fake_no_activity */, -}; - struct labor_info { PersistentDataItem config; diff --git a/plugins/autolabor/laborstatemap.h b/plugins/autolabor/laborstatemap.h new file mode 100644 index 000000000..0e9770829 --- /dev/null +++ b/plugins/autolabor/laborstatemap.h @@ -0,0 +1,294 @@ +#pragma once + +#include + +#include "df/job.h" +#include "df/job_type.h" +#include "df/unit_labor.h" + +using namespace DFHack; +using namespace df::enums; + + const enum dwarf_state : const int { + // Ready for a new task + IDLE=0, + + // Busy with a useful task + BUSY, + + // Busy with a useful task that requires a tool + EXCLUSIVE, + + // In the military, can't work + MILITARY, + + // Child or noble, can't work + CHILD, + + // Doing something that precludes working, may be busy for a while + OTHER +}; + +const int NUM_STATE = 6; + +char const* state_names[] { + "IDLE", + "BUSY", + "EXCLUSIVE", + "MILITARY", + "CHILD", + "OTHER", +}; + +const dwarf_state dwarf_states[] = { + dwarf_state::BUSY /* CarveFortification */, + dwarf_state::BUSY /* DetailWall */, + dwarf_state::BUSY /* DetailFloor */, + dwarf_state::EXCLUSIVE /* Dig */, + dwarf_state::EXCLUSIVE /* CarveUpwardStaircase */, + dwarf_state::EXCLUSIVE /* CarveDownwardStaircase */, + dwarf_state::EXCLUSIVE /* CarveUpDownStaircase */, + dwarf_state::EXCLUSIVE /* CarveRamp */, + dwarf_state::EXCLUSIVE /* DigChannel */, + dwarf_state::EXCLUSIVE /* FellTree */, + dwarf_state::BUSY /* GatherPlants */, + dwarf_state::BUSY /* RemoveConstruction */, + dwarf_state::BUSY /* CollectWebs */, + dwarf_state::BUSY /* BringItemToDepot */, + dwarf_state::BUSY /* BringItemToShop */, + dwarf_state::OTHER /* Eat */, + dwarf_state::OTHER /* GetProvisions */, + dwarf_state::OTHER /* Drink */, + dwarf_state::OTHER /* Drink2 */, + dwarf_state::OTHER /* FillWaterskin */, + dwarf_state::OTHER /* FillWaterskin2 */, + dwarf_state::OTHER /* Sleep */, + dwarf_state::BUSY /* CollectSand */, + dwarf_state::BUSY /* Fish */, + dwarf_state::EXCLUSIVE /* Hunt */, + dwarf_state::OTHER /* HuntVermin */, + dwarf_state::BUSY /* Kidnap */, + dwarf_state::BUSY /* BeatCriminal */, + dwarf_state::BUSY /* StartingFistFight */, + dwarf_state::BUSY /* CollectTaxes */, + dwarf_state::BUSY /* GuardTaxCollector */, + dwarf_state::BUSY /* CatchLiveLandAnimal */, + dwarf_state::BUSY /* CatchLiveFish */, + dwarf_state::BUSY /* ReturnKill */, + dwarf_state::BUSY /* CheckChest */, + dwarf_state::BUSY /* StoreOwnedItem */, + dwarf_state::BUSY /* PlaceItemInTomb */, + dwarf_state::BUSY /* StoreItemInStockpile */, + dwarf_state::BUSY /* StoreItemInBag */, + dwarf_state::BUSY /* StoreItemInHospital */, + dwarf_state::BUSY /* StoreItemInChest */, + dwarf_state::BUSY /* StoreItemInCabinet */, + dwarf_state::BUSY /* StoreWeapon */, + dwarf_state::BUSY /* StoreArmor */, + dwarf_state::BUSY /* StoreItemInBarrel */, + dwarf_state::BUSY /* StoreItemInBin */, + dwarf_state::BUSY /* SeekArtifact */, + dwarf_state::BUSY /* SeekInfant */, + dwarf_state::OTHER /* AttendParty */, + dwarf_state::OTHER /* GoShopping */, + dwarf_state::OTHER /* GoShopping2 */, + dwarf_state::BUSY /* Clean */, + dwarf_state::OTHER /* Rest */, + dwarf_state::EXCLUSIVE /* PickupEquipment */, + dwarf_state::BUSY /* DumpItem */, + dwarf_state::OTHER /* StrangeMoodCrafter */, + dwarf_state::OTHER /* StrangeMoodJeweller */, + dwarf_state::OTHER /* StrangeMoodForge */, + dwarf_state::OTHER /* StrangeMoodMagmaForge */, + dwarf_state::OTHER /* StrangeMoodBrooding */, + dwarf_state::OTHER /* StrangeMoodFell */, + dwarf_state::OTHER /* StrangeMoodCarpenter */, + dwarf_state::OTHER /* StrangeMoodMason */, + dwarf_state::OTHER /* StrangeMoodBowyer */, + dwarf_state::OTHER /* StrangeMoodTanner */, + dwarf_state::OTHER /* StrangeMoodWeaver */, + dwarf_state::OTHER /* StrangeMoodGlassmaker */, + dwarf_state::OTHER /* StrangeMoodMechanics */, + dwarf_state::BUSY /* ConstructBuilding */, + dwarf_state::BUSY /* ConstructDoor */, + dwarf_state::BUSY /* ConstructFloodgate */, + dwarf_state::BUSY /* ConstructBed */, + dwarf_state::BUSY /* ConstructThrone */, + dwarf_state::BUSY /* ConstructCoffin */, + dwarf_state::BUSY /* ConstructTable */, + dwarf_state::BUSY /* ConstructChest */, + dwarf_state::BUSY /* ConstructBin */, + dwarf_state::BUSY /* ConstructArmorStand */, + dwarf_state::BUSY /* ConstructWeaponRack */, + dwarf_state::BUSY /* ConstructCabinet */, + dwarf_state::BUSY /* ConstructStatue */, + dwarf_state::BUSY /* ConstructBlocks */, + dwarf_state::BUSY /* MakeRawGlass */, + dwarf_state::BUSY /* MakeCrafts */, + dwarf_state::BUSY /* MintCoins */, + dwarf_state::BUSY /* CutGems */, + dwarf_state::BUSY /* CutGlass */, + dwarf_state::BUSY /* EncrustWithGems */, + dwarf_state::BUSY /* EncrustWithGlass */, + dwarf_state::BUSY /* DestroyBuilding */, + dwarf_state::BUSY /* SmeltOre */, + dwarf_state::BUSY /* MeltMetalObject */, + dwarf_state::BUSY /* ExtractMetalStrands */, + dwarf_state::BUSY /* PlantSeeds */, + dwarf_state::BUSY /* HarvestPlants */, + dwarf_state::BUSY /* TrainHuntingAnimal */, + dwarf_state::BUSY /* TrainWarAnimal */, + dwarf_state::BUSY /* MakeWeapon */, + dwarf_state::BUSY /* ForgeAnvil */, + dwarf_state::BUSY /* ConstructCatapultParts */, + dwarf_state::BUSY /* ConstructBallistaParts */, + dwarf_state::BUSY /* MakeArmor */, + dwarf_state::BUSY /* MakeHelm */, + dwarf_state::BUSY /* MakePants */, + dwarf_state::BUSY /* StudWith */, + dwarf_state::BUSY /* ButcherAnimal */, + dwarf_state::BUSY /* PrepareRawFish */, + dwarf_state::BUSY /* MillPlants */, + dwarf_state::BUSY /* BaitTrap */, + dwarf_state::BUSY /* MilkCreature */, + dwarf_state::BUSY /* MakeCheese */, + dwarf_state::BUSY /* ProcessPlants */, + dwarf_state::BUSY /* ProcessPlantsBag */, + dwarf_state::BUSY /* ProcessPlantsVial */, + dwarf_state::BUSY /* ProcessPlantsBarrel */, + dwarf_state::BUSY /* PrepareMeal */, + dwarf_state::BUSY /* WeaveCloth */, + dwarf_state::BUSY /* MakeGloves */, + dwarf_state::BUSY /* MakeShoes */, + dwarf_state::BUSY /* MakeShield */, + dwarf_state::BUSY /* MakeCage */, + dwarf_state::BUSY /* MakeChain */, + dwarf_state::BUSY /* MakeFlask */, + dwarf_state::BUSY /* MakeGoblet */, + dwarf_state::BUSY /* MakeInstrument */, + dwarf_state::BUSY /* MakeToy */, + dwarf_state::BUSY /* MakeAnimalTrap */, + dwarf_state::BUSY /* MakeBarrel */, + dwarf_state::BUSY /* MakeBucket */, + dwarf_state::BUSY /* MakeWindow */, + dwarf_state::BUSY /* MakeTotem */, + dwarf_state::BUSY /* MakeAmmo */, + dwarf_state::BUSY /* DecorateWith */, + dwarf_state::BUSY /* MakeBackpack */, + dwarf_state::BUSY /* MakeQuiver */, + dwarf_state::BUSY /* MakeBallistaArrowHead */, + dwarf_state::BUSY /* AssembleSiegeAmmo */, + dwarf_state::BUSY /* LoadCatapult */, + dwarf_state::BUSY /* LoadBallista */, + dwarf_state::BUSY /* FireCatapult */, + dwarf_state::BUSY /* FireBallista */, + dwarf_state::BUSY /* ConstructMechanisms */, + dwarf_state::BUSY /* MakeTrapComponent */, + dwarf_state::BUSY /* LoadCageTrap */, + dwarf_state::BUSY /* LoadStoneTrap */, + dwarf_state::BUSY /* LoadWeaponTrap */, + dwarf_state::BUSY /* CleanTrap */, + dwarf_state::BUSY /* CastSpell */, + dwarf_state::BUSY /* LinkBuildingToTrigger */, + dwarf_state::BUSY /* PullLever */, + dwarf_state::BUSY /* BrewDrink */, + dwarf_state::BUSY /* ExtractFromPlants */, + dwarf_state::BUSY /* ExtractFromRawFish */, + dwarf_state::BUSY /* ExtractFromLandAnimal */, + dwarf_state::BUSY /* TameVermin */, + dwarf_state::BUSY /* TameAnimal */, + dwarf_state::BUSY /* ChainAnimal */, + dwarf_state::BUSY /* UnchainAnimal */, + dwarf_state::BUSY /* UnchainPet */, + dwarf_state::BUSY /* ReleaseLargeCreature */, + dwarf_state::BUSY /* ReleasePet */, + dwarf_state::BUSY /* ReleaseSmallCreature */, + dwarf_state::BUSY /* HandleSmallCreature */, + dwarf_state::BUSY /* HandleLargeCreature */, + dwarf_state::BUSY /* CageLargeCreature */, + dwarf_state::BUSY /* CageSmallCreature */, + dwarf_state::BUSY /* RecoverWounded */, + dwarf_state::BUSY /* DiagnosePatient */, + dwarf_state::BUSY /* ImmobilizeBreak */, + dwarf_state::BUSY /* DressWound */, + dwarf_state::BUSY /* CleanPatient */, + dwarf_state::BUSY /* Surgery */, + dwarf_state::BUSY /* Suture */, + dwarf_state::BUSY /* SetBone */, + dwarf_state::BUSY /* PlaceInTraction */, + dwarf_state::BUSY /* DrainAquarium */, + dwarf_state::BUSY /* FillAquarium */, + dwarf_state::BUSY /* FillPond */, + dwarf_state::BUSY /* GiveWater */, + dwarf_state::BUSY /* GiveFood */, + dwarf_state::BUSY /* GiveWater2 */, + dwarf_state::BUSY /* GiveFood2 */, + dwarf_state::BUSY /* RecoverPet */, + dwarf_state::BUSY /* PitLargeAnimal */, + dwarf_state::BUSY /* PitSmallAnimal */, + dwarf_state::BUSY /* SlaughterAnimal */, + dwarf_state::BUSY /* MakeCharcoal */, + dwarf_state::BUSY /* MakeAsh */, + dwarf_state::BUSY /* MakeLye */, + dwarf_state::BUSY /* MakePotashFromLye */, + dwarf_state::BUSY /* FertilizeField */, + dwarf_state::BUSY /* MakePotashFromAsh */, + dwarf_state::BUSY /* DyeThread */, + dwarf_state::BUSY /* DyeCloth */, + dwarf_state::BUSY /* SewImage */, + dwarf_state::BUSY /* MakePipeSection */, + dwarf_state::BUSY /* OperatePump */, + dwarf_state::OTHER /* ManageWorkOrders */, + dwarf_state::OTHER /* UpdateStockpileRecords */, + dwarf_state::OTHER /* TradeAtDepot */, + dwarf_state::BUSY /* ConstructHatchCover */, + dwarf_state::BUSY /* ConstructGrate */, + dwarf_state::BUSY /* RemoveStairs */, + dwarf_state::BUSY /* ConstructQuern */, + dwarf_state::BUSY /* ConstructMillstone */, + dwarf_state::BUSY /* ConstructSplint */, + dwarf_state::BUSY /* ConstructCrutch */, + dwarf_state::BUSY /* ConstructTractionBench */, + dwarf_state::BUSY /* CleanSelf */, + dwarf_state::BUSY /* BringCrutch */, + dwarf_state::BUSY /* ApplyCast */, + dwarf_state::BUSY /* CustomReaction */, + dwarf_state::BUSY /* ConstructSlab */, + dwarf_state::BUSY /* EngraveSlab */, + dwarf_state::BUSY /* ShearCreature */, + dwarf_state::BUSY /* SpinThread */, + dwarf_state::BUSY /* PenLargeAnimal */, + dwarf_state::BUSY /* PenSmallAnimal */, + dwarf_state::BUSY /* MakeTool */, + dwarf_state::BUSY /* CollectClay */, + dwarf_state::BUSY /* InstallColonyInHive */, + dwarf_state::BUSY /* CollectHiveProducts */, + dwarf_state::OTHER /* CauseTrouble */, + dwarf_state::OTHER /* DrinkBlood */, + dwarf_state::OTHER /* ReportCrime */, + dwarf_state::OTHER /* ExecuteCriminal */, + dwarf_state::BUSY /* TrainAnimal */, + dwarf_state::BUSY /* CarveTrack */, + dwarf_state::BUSY /* PushTrackVehicle */, + dwarf_state::BUSY /* PlaceTrackVehicle */, + dwarf_state::BUSY /* StoreItemInVehicle */, + dwarf_state::BUSY /* GeldAnimal */, + dwarf_state::BUSY /* MakeFigurine */, + dwarf_state::BUSY /* MakeAmulet */, + dwarf_state::BUSY /* MakeScepter */, + dwarf_state::BUSY /* MakeCrown */, + dwarf_state::BUSY /* MakeRing */, + dwarf_state::BUSY /* MakeEarring */, + dwarf_state::BUSY /* MakeBracelet */, + dwarf_state::BUSY /* MakeGem */, + dwarf_state::BUSY /* PutItemOnDisplay */, + dwarf_state::OTHER /* unk_fake_no_job */, + dwarf_state::OTHER /* InterrogateSubject */, + dwarf_state::OTHER /* unk_fake_no_activity */, +}; + +#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0])) + +const int dwarf_state_count = ARRAY_COUNT(dwarf_states); + +#undef ARRAY_COUNT From 2bc2dd8f5b0f90b0c66c631a35f12fae0b16857f Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 20 Nov 2022 10:21:32 -0600 Subject: [PATCH 104/161] remove unnecessary 'const' wouldn't it be nice if there was a single C++ standard that all compilers adhered to? --- plugins/autolabor/laborstatemap.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/autolabor/laborstatemap.h b/plugins/autolabor/laborstatemap.h index 0e9770829..789685930 100644 --- a/plugins/autolabor/laborstatemap.h +++ b/plugins/autolabor/laborstatemap.h @@ -9,7 +9,7 @@ using namespace DFHack; using namespace df::enums; - const enum dwarf_state : const int { + enum dwarf_state : int { // Ready for a new task IDLE=0, From ff447d46bd2fdca95ea2fa3cdf55344d20bb331e Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 20 Nov 2022 09:45:23 -0800 Subject: [PATCH 105/161] don't overcount units for misery widget addendum to #2366 --- plugins/lua/dwarfmonitor.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/lua/dwarfmonitor.lua b/plugins/lua/dwarfmonitor.lua index e8c07cac6..5358e4289 100644 --- a/plugins/lua/dwarfmonitor.lua +++ b/plugins/lua/dwarfmonitor.lua @@ -116,9 +116,11 @@ end function MiseryWidget:overlay_onupdate() local counts, num_colors = {}, #self.colors for _,unit in ipairs(df.global.world.units.active) do + if not dfhack.units.isCitizen(unit, true) then goto continue end local stress_category = math.min(num_colors, dfhack.units.getStressCategory(unit)) counts[stress_category] = (counts[stress_category] or 0) + 1 + ::continue:: end local width = 2 + num_colors - 1 -- 'H:' plus the slashes From 5967e5c9e04e2824268051bbd0242f19b0244f71 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 20 Nov 2022 16:03:08 -0800 Subject: [PATCH 106/161] fix up cleanowned status message print unit id instead of raw pointer and display "wear level" instead of just "wear" --- plugins/cleanowned.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp index 387dfdd92..86ef0a2e1 100644 --- a/plugins/cleanowned.cpp +++ b/plugins/cleanowned.cpp @@ -147,8 +147,8 @@ command_result df_cleanowned (color_ostream &out, vector & parameters) std::string description; item->getItemDescription(&description, 0); out.print( - "%p %s (wear %d)", - item, + "[%d] %s (wear level %d)", + item->id, description.c_str(), item->getWear() ); From d2c26acb47659986c83e7f2ea1d18dce571d163c Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 20 Nov 2022 07:50:59 -0800 Subject: [PATCH 107/161] create robots.txt for HTML docs --- conf.py | 4 ++++ robots.txt | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 robots.txt diff --git a/conf.py b/conf.py index 661b0daea..60c3be579 100644 --- a/conf.py +++ b/conf.py @@ -256,6 +256,10 @@ html_favicon = 'docs/styles/dfhack-icon.ico' # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['docs/styles'] +# A list of paths that contain extra files not directly related to the +# documentation. +html_extra_path = ['robots.txt'] + # Custom sidebar templates, maps document names to template names. html_sidebars = { '**': [ diff --git a/robots.txt b/robots.txt new file mode 100644 index 000000000..15dc7919e --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +User-agent: * + +Allow: /en/stable/ + +Sitemap: https://docs.dfhack.org/sitemap.xml From 0190cfb117546b8075860251bdff819c2167c9a9 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 20 Nov 2022 17:27:14 -0800 Subject: [PATCH 108/161] ensure foo.init runs before foo.*.init --- library/Core.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index 083a6223f..836772c04 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2000,15 +2000,21 @@ void getFilesWithPrefixAndSuffix(const std::string& folder, const std::string& p } size_t loadScriptFiles(Core* core, color_ostream& out, const vector& prefix, const std::string& folder) { - vector scriptFiles; + static const string suffix = ".init"; + vector scriptFiles; for ( size_t a = 0; a < prefix.size(); a++ ) { getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles); } - std::sort(scriptFiles.begin(), scriptFiles.end()); + std::sort(scriptFiles.begin(), scriptFiles.end(), + [&](const string &a, const string &b) { + string a_base = a.substr(0, a.size() - suffix.size()); + string b_base = b.substr(0, b.size() - suffix.size()); + return a_base < b_base; + }); size_t result = 0; for ( size_t a = 0; a < scriptFiles.size(); a++ ) { result++; - std::string path = ""; + string path = ""; if (folder != ".") path = folder + "/"; core->loadScriptFile(out, path + scriptFiles[a], false); From 086ce64787d4558e5ff5cf6f14e7caadb77dc2b2 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 20 Nov 2022 17:28:59 -0800 Subject: [PATCH 109/161] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index d0bfa789f..206d64879 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -46,6 +46,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `autofarm`: add missing output flushes - Core: fix the segmentation fault with the REPORT event in EventManager - Core: fix the new JOB_STARTED event only sending each event once, to the first handler listed +- Core: ensure ``foo.init`` always runs before ``foo.*.init`` (e.g. ``dfhack.init`` should always run before ``dfhack.something.init``) ## Misc Improvements - `blueprint`: new ``--smooth`` option for recording all smoothed floors and walls instead of just the ones that require smoothing for later carving From 22414f26fa0035b85e5946010eb3eb01a4e5cf16 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 00:12:35 -0700 Subject: [PATCH 110/161] Implements plugin: channel-safely v0.1 --- docs/plugins/channel-safely.rst | 62 +++ plugins/CMakeLists.txt | 1 + plugins/channel-safely/CMakeLists.txt | 10 + plugins/channel-safely/channel-groups.cpp | 253 +++++++++ plugins/channel-safely/channel-jobs.cpp | 46 ++ plugins/channel-safely/channel-manager.cpp | 99 ++++ .../channel-safely/channel-safely-plugin.cpp | 509 ++++++++++++++++++ .../channel-safely/include/channel-groups.h | 49 ++ plugins/channel-safely/include/channel-jobs.h | 29 + .../channel-safely/include/channel-manager.h | 39 ++ plugins/channel-safely/include/inlines.h | 149 +++++ plugins/channel-safely/include/plugin.h | 23 + 12 files changed, 1269 insertions(+) create mode 100644 docs/plugins/channel-safely.rst create mode 100644 plugins/channel-safely/CMakeLists.txt create mode 100644 plugins/channel-safely/channel-groups.cpp create mode 100644 plugins/channel-safely/channel-jobs.cpp create mode 100644 plugins/channel-safely/channel-manager.cpp create mode 100644 plugins/channel-safely/channel-safely-plugin.cpp create mode 100644 plugins/channel-safely/include/channel-groups.h create mode 100644 plugins/channel-safely/include/channel-jobs.h create mode 100644 plugins/channel-safely/include/channel-manager.h create mode 100644 plugins/channel-safely/include/inlines.h create mode 100644 plugins/channel-safely/include/plugin.h diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst new file mode 100644 index 000000000..6010f4925 --- /dev/null +++ b/docs/plugins/channel-safely.rst @@ -0,0 +1,62 @@ +channel-safely +============== + +.. dfhack-tool:: + :summary: Auto-manage channel designations to keep dwarves safe + :tags: fort auto + +Multi-level channel projects can be dangerous, and managing the safety of your +dwarves throughout the completion of such projects can be difficult and time +consuming. This plugin keeps your dwarves safe (while channeling) so you don't +have to. Now you can focus on designing your dwarven cities with the deep chasms +they were meant to have. + +Usage +----- + +:: + enable channel-safely + channel-safely set + channel-safely enable|disable + channel-safely run once + +When enabled the map will be scanned for channel designations which will be grouped +together based on adjacency and z-level. These groups will then be analyzed for safety +and designations deemed unsafe will be put into :wiki:`Marker Mode `. +Each time a channel designation is completed its group status is checked, and if the group +is complete pending groups below are made active again. + +Examples +-------- + +``channel-safely`` + The plugin reports its configured status. + +``channel-safely run once`` + Runs the safety procedures once. You can use this if you prefer initiating scans manually. + +``channel-safely disable require-vision`` + Allows the plugin to read all tiles, including the ones your dwarves know nothing about. + +``channel-safely enable monitor-active`` + Enables monitoring active channel digging jobs. Meaning that if another unit it present + or the tile below becomes open space the job will be paused or canceled (respectively). + +``channel-safely set ignore-threshold 3`` + Configures the plugin to ignore designations equal to or above priority 3 designations. + +Features +-------- +:monitor-active: Toggle whether to monitor the conditions of active digs. (default: disabled) +:require-vision: Toggle whether the dwarves need vision of a tile before channeling to it can be deemed unsafe. (default: enabled) +:insta-dig: Toggle whether to use insta-digging on unreachable designations. (default: disabled) + +Settings +-------- +:refresh-freq: The rate at which full refreshes are performed. + This can be expensive if you're undertaking many mega projects. (default:600, twice a day) +:monitor-freq: The rate at which active jobs are monitored. + todo: this can have a massive impact? (default:10) +:ignore-threshold: Sets the priority threshold below which designations are processed. You can set to 1 or 0 to + effectively disable the scanning. (default: 7) +:fall-threshold: Sets the fall threshold beyond which is considered unsafe. (default: 1) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9038d5d79..c3cd88479 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -100,6 +100,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(changeitem changeitem.cpp) dfhack_plugin(changelayer changelayer.cpp) dfhack_plugin(changevein changevein.cpp) + add_subdirectory(channel-safely) dfhack_plugin(cleanconst cleanconst.cpp) dfhack_plugin(cleaners cleaners.cpp) dfhack_plugin(cleanowned cleanowned.cpp) diff --git a/plugins/channel-safely/CMakeLists.txt b/plugins/channel-safely/CMakeLists.txt new file mode 100644 index 000000000..d660d2262 --- /dev/null +++ b/plugins/channel-safely/CMakeLists.txt @@ -0,0 +1,10 @@ +project(channel-safely) + +include_directories(include) +SET(SOURCES + channel-jobs.cpp + channel-groups.cpp + channel-manager.cpp + channel-safely-plugin.cpp) + +dfhack_plugin(${PROJECT_NAME} ${SOURCES} LINK_LIBRARIES lua) diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp new file mode 100644 index 000000000..7ee779df8 --- /dev/null +++ b/plugins/channel-safely/channel-groups.cpp @@ -0,0 +1,253 @@ +#include +#include +#include +#include + +#include + +// scans the map for channel designations +void ChannelGroups::scan_map() { + static std::default_random_engine RNG(0); + static std::bernoulli_distribution optimizing(0.3333); + DEBUG(groups).print(" scan_map()\n"); + // foreach block + for (int32_t z = mapz - 1; z >= 0; --z) { + for (int32_t by = 0; by < mapy; ++by) { + for (int32_t bx = 0; bx < mapx; ++bx) { + // the block + if (df::map_block* block = Maps::getBlock(bx, by, z)) { + // skip this block? + if (!block->flags.bits.designated && optimizing(RNG)) { + // todo: add remainder of block width onto bx + TRACE(groups).print(" skipping this block, it has no designations\n"); + continue; + } + // foreach tile + for (int16_t lx = 0; lx < 16; ++lx) { + for (int16_t ly = 0; ly < 16; ++ly) { + // the tile, check if it has a channel designation + if (is_dig_designation(block->designation[lx][ly])) { + for (df::block_square_event* event: block->block_events) { + if (auto evT = virtual_cast(event)) { + // we want to let the user keep some designations free of being managed + TRACE(groups).print(" tile designation priority: %d\n", evT->priority[lx][ly]); + if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { + df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z); + TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); + add(map_pos); + } + } + } + } + } + } + } + } + } + } + INFO(groups).print("scan_map() exits\n"); +} + +// scans a single tile for channel designations +void ChannelGroups::scan_one(const df::coord &map_pos) { + df::map_block* block = Maps::getTileBlock(map_pos); + int16_t lx = map_pos.x % 16; + int16_t ly = map_pos.y % 16; + if (is_dig_designation(block->designation[lx][ly])) { + for (df::block_square_event* event: block->block_events) { + if (auto evT = virtual_cast(event)) { + // we want to let the user keep some designations free of being managed + if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { + TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); + add(map_pos); + } + } + } + } +} + +// adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found +void ChannelGroups::add(const df::coord &map_pos) { + // if we've already added this, we don't need to do it again + if (groups_map.count(map_pos)) { + return; + } + /* We need to add map_pos to an existing group if possible... + * So what we do is we look at neighbours to see if they belong to one or more existing groups + * If there is more than one group, we'll be merging them + */ + df::coord neighbors[8]; + get_neighbours(map_pos, neighbors); + Group* group = nullptr; + int group_index = -1; + + DEBUG(groups).print(" add(" COORD ")\n", COORDARGS(map_pos)); + // and so we begin iterating the neighbours + for (auto &neighbour: neighbors) { + // go to the next neighbour if this one doesn't have a group + if (!groups_map.count(neighbour)) { + TRACE(groups).print(" -> neighbour is not designated\n"); + continue; + } + // get the group, since at least one exists... then merge any additional into that one + if (!group){ + TRACE(groups).print(" -> group* has no valid state yet\n"); + group_index = groups_map.find(neighbour)->second; + group = &groups.at(group_index); + } else { + TRACE(groups).print(" -> group* has an existing state\n"); + + // we don't do anything if the found group is the same as the existing group + auto index2 = groups_map.find(neighbour)->second; + if (group_index != index2) { + // we already have group "prime" if you will, so we're going to merge the new find into prime + Group &group2 = groups.at(index2); + // merge + TRACE(groups).print(" -> merging two groups. group 1 size: %zu. group 2 size: %zu\n", group->size(), + group2.size()); + for (auto pos2: group2) { + group->emplace(pos2); + groups_map[pos2] = group_index; + } + group2.clear(); + free_spots.emplace(index2); + TRACE(groups).print(" merged size: %zu\n", group->size()); + } + } + } + // if we haven't found at least one group by now we need to create/get one + if (!group) { + TRACE(groups).print(" -> no merging took place\n"); + // first we check if we can re-use a group that's been freed + if (!free_spots.empty()) { + TRACE(groups).print(" -> use recycled old group\n"); + // first element in a set is always the lowest value, so we re-use from the front of the vector + group_index = *free_spots.begin(); + group = &groups[group_index]; + free_spots.erase(free_spots.begin()); + } else { + TRACE(groups).print(" -> brand new group\n"); + // we create a brand-new group to use + group_index = groups.size(); + groups.push_back(Group()); + group = &groups[group_index]; + } + } + // puts the "add" in "ChannelGroups::add" + group->emplace(map_pos); + DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size()); +// ERR(groups).print("\n\n\nDEBUG MAPPINGS:\n"); +// debug_map(); +// DEBUG(groups).flush(); + + // we may have performed a merge, so we update all the `coord -> group index` mappings + for (auto &wpos: *group) { + groups_map[wpos] = group_index; + } + DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size()); +// ERR(groups).print("\n\n\nDEBUG MAPPINGS:\n"); +// debug_map(); +// DEBUG(groups).flush(); +} + +// builds groupings of adjacent channel designations +void ChannelGroups::build() { + clear(); + // iterate over each job, finding channel jobs + jobs.load_channel_jobs(); + // transpose channel jobs to + for (auto &pos : jobs) { + add(pos); + } + scan_map(); +} + +// clears out the containers for unloading maps or disabling the plugin +void ChannelGroups::clear() { + debug_map(); + WARN(groups).print(" <- clearing groups\n"); + free_spots.clear(); + groups_map.clear(); + for(size_t i = 0; i < groups.size(); ++i) { + groups[i].clear(); + free_spots.emplace(i); + } +} + +// erases map_pos from its group, and deletes mappings IFF the group is empty +void ChannelGroups::remove(const df::coord &map_pos) { + // we don't need to do anything if the position isn't in a group (granted, that should never be the case) + INFO(groups).print(" remove()\n"); + if (groups_map.count(map_pos)) { + INFO(groups).print(" -> found group\n"); + // get the group, and map_pos' block* + int group_index = groups_map.find(map_pos)->second; + Group &group = groups[group_index]; + // erase map_pos from the group + INFO(groups).print(" -> erase(" COORD ")\n", COORDARGS(map_pos)); + group.erase(map_pos); + groups_map.erase(map_pos); + // clean up if the group is empty + if (group.empty()) { + WARN(groups).print(" -> group is empty\n"); + // erase `coord -> group group_index` mappings + for (auto iter = groups_map.begin(); iter != groups_map.end();) { + if (group_index == iter->second) { + iter = groups_map.erase(iter); + continue; + } + ++iter; + } + // flag the `groups` group_index as available + free_spots.insert(group_index); + } + } + INFO(groups).print(" remove() exits\n"); +} + +// finds a group corresponding to a map position if one exists +Groups::const_iterator ChannelGroups::find(const df::coord &map_pos) const { + const auto iter = groups_map.find(map_pos); + if (iter != groups_map.end()) { + return groups.begin() + iter->second; + } + return groups.end(); +} + +// returns an iterator to the first element stored +Groups::const_iterator ChannelGroups::begin() const { + return groups.begin(); +} + +// returns an iterator to after the last element stored +Groups::const_iterator ChannelGroups::end() const { + return groups.end(); +} + +// returns a count of 0 or 1 depending on whether map_pos is mapped to a group +size_t ChannelGroups::count(const df::coord &map_pos) const { + return groups_map.count(map_pos); +} + +// prints debug info about the groups stored, and their members +void ChannelGroups::debug_groups() { +// int idx = 0; +// TRACE(groups).print(" debugging group data\n"); +// for (auto &group : groups) { +// TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size()); +// for (auto &pos : group) { +// TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); +// } +// idx++; +// } +} + +// prints debug info group mappings +void ChannelGroups::debug_map() { +// INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); +// for (auto &pair : groups_map) { +// DEBUG(groups).print(" map[" COORD "] = %d\n",COORDARGS(pair.first), pair.second); +// } +} + + diff --git a/plugins/channel-safely/channel-jobs.cpp b/plugins/channel-safely/channel-jobs.cpp new file mode 100644 index 000000000..7a1c2f4be --- /dev/null +++ b/plugins/channel-safely/channel-jobs.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include + +// iterates the DF job list and adds channel jobs to the `jobs` container +void ChannelJobs::load_channel_jobs() { + jobs.clear(); + df::job_list_link* node = df::global::world->jobs.list.next; + while (node) { + df::job* job = node->item; + node = node->next; + if (is_dig_job(job)) { + jobs.emplace(job->pos); + } + } +} + +// clears the container +void ChannelJobs::clear() { + jobs.clear(); +} + +// finds and erases a job corresponding to a map position, then returns the iterator following the element removed +std::set::iterator ChannelJobs::erase(const df::coord &map_pos) { + auto iter = jobs.find(map_pos); + if (iter != jobs.end()) { + return jobs.erase(iter); + } + return iter; +} + +// finds a job corresponding to a map position if one exists +std::set::const_iterator ChannelJobs::find(const df::coord &map_pos) const { + return jobs.find(map_pos); +} + +// returns an iterator to the first element stored +std::set::const_iterator ChannelJobs::begin() const { + return jobs.begin(); +} + +// returns an iterator to after the last element stored +std::set::const_iterator ChannelJobs::end() const { + return jobs.end(); +} diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp new file mode 100644 index 000000000..79bc6a7aa --- /dev/null +++ b/plugins/channel-safely/channel-manager.cpp @@ -0,0 +1,99 @@ +#include +#include + +#include + +/** +blocks[48][96][135]: +blocks[48][96][135].default_liquid.hidden: false +blocks[48][96][135].designation[10][0].hidden: false + * */ + +// sets mark flags as necessary, for all designations +void ChannelManager::manage_all() { + INFO(manager).print("manage_all()\n"); + // make sure we've got a fort map to analyze + if (World::isFortressMode() && Maps::IsValid()) { + // iterate the groups we built/updated + for (const auto &group: groups) { + manage_group(group, true, has_any_groups_above(groups, group)); + } + } +} + +void ChannelManager::manage_group(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) { + INFO(manager).print("manage_group(" COORD ")\n ", COORDARGS(map_pos)); + if (!groups.count(map_pos)) { + groups.scan_one(map_pos); + } + auto iter = groups.find(map_pos); + if (iter != groups.end()) { + manage_group(*iter, set_marker_mode, marker_mode); + } + INFO(manager).print("manage_group() is done\n"); +} + +void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool marker_mode) { + INFO(manager).print("manage_group()\n"); + if (!set_marker_mode) { + if (has_any_groups_above(groups, group)) { + marker_mode = true; + } else { + marker_mode = false; + } + } + for (auto &designation: group) { + manage_one(group, designation, true, marker_mode); + } + INFO(manager).print("manage_group() is done\n"); +} + +bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode, bool marker_mode) { + if (Maps::isValidTilePos(map_pos)) { + INFO(manager).print("manage_one(" COORD ")\n", COORDARGS(map_pos)); + df::map_block* block = Maps::getTileBlock(map_pos); + // we calculate the position inside the block* + df::coord local(map_pos); + local.x = local.x % 16; + local.y = local.y % 16; + df::tile_occupancy &tile_occupancy = block->occupancy[Coord(local)]; + // ensure that we aren't on the top-most layers + if (map_pos.z < mapz - 3) { + // do we already know whether to set marker mode? + if (set_marker_mode) { + DEBUG(manager).print(" -> marker_mode\n"); + tile_occupancy.bits.dig_marked = marker_mode; + jobs.erase(map_pos); + return true; + } else { + // next search for the designation priority + for (df::block_square_event* event: block->block_events) { + if (auto evT = virtual_cast(event)) { + // we want to let the user keep some designations free of being managed + if (evT->priority[Coord(local)] < 1000 * config.ignore_threshold) { + DEBUG(manager).print(" if(has_groups_above())\n"); + // check that the group has no incomplete groups directly above it + if (has_group_above(groups, map_pos)) { + DEBUG(manager).print(" has_groups_above: setting marker mode\n"); + tile_occupancy.bits.dig_marked = true; + jobs.erase(map_pos); + WARN(manager).print(" <- manage_one() exits normally\n"); + return true; + } + } + } + } + } + } else { + // if we are though, it should be totally safe to dig + tile_occupancy.bits.dig_marked = false; + } + WARN(manager).print(" <- manage_one() exits normally\n"); + } + return false; +} + +void ChannelManager::mark_done(const df::coord &map_pos) { + groups.remove(map_pos); + jobs.erase(map_pos); +} diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp new file mode 100644 index 000000000..e6cdf231b --- /dev/null +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -0,0 +1,509 @@ +/* Prevent channeling down into known open space. +Author: Josh Cooper +Created: Aug. 4 2020 +Updated: Nov. 1 2022 + + Enable plugin: + -> build groups + -> manage designations + + Unpause event: + -> build groups + -> manage designations + + Manage Designation(s): + -> for each group in groups: + -> for each designation in this group: + -> + + + Job started event: + -> validate job type (channel) + -> check pathing: + -> Can: add job/worker to tracking + -> Can: set tile to restricted + -> Cannot: remove worker + -> Cannot: insta-dig & delete job + -> Cannot: set designation to Marker Mode (no insta-digging) + + OnUpdate: + -> check worker location: + -> CanFall: check if a fall would be safe: + -> Safe: do nothing + -> Unsafe: remove worker + -> Unsafe: insta-dig & delete job (presumes the job is only accessible from directly on the tile) + -> Unsafe: set designation to Marker Mode (no insta-digging) + -> check tile occupancy: + -> HasUnit: check if a fall would be safe: + -> Safe: do nothing, let them fall + -> Unsafe: remove worker for 1 tick (test if this "pauses" or cancels the job) + -> Unsafe: Add feature to teleport unit? + + Job completed event: + -> validate job type (channel) + -> verify completion: + -> IsOpenSpace: mark done + -> IsOpenSpace: manage tile below + -> NotOpenSpace: check for designation + -> HasDesignation: do nothing + -> NoDesignation: mark done (erases from group) + -> NoDesignation: manage tile below +*/ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +// Debugging +namespace DFHack { + DBG_DECLARE(channelsafely, monitor, DebugCategory::LINFO); + DBG_DECLARE(channelsafely, manager, DebugCategory::LINFO); + DBG_DECLARE(channelsafely, groups, DebugCategory::LINFO); + DBG_DECLARE(channelsafely, jobs, DebugCategory::LINFO); +} + +DFHACK_PLUGIN("channel-safely"); +DFHACK_PLUGIN_IS_ENABLED(enabled); +REQUIRE_GLOBAL(world); + +namespace EM = EventManager; +using namespace DFHack; +using namespace EM::EventType; + +int32_t mapx, mapy, mapz; +Configuration config; +PersistentDataItem pconfig; +const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; +//std::unordered_set active_jobs; + +#include + +enum ConfigurationData { + MONITOR, + VISION, + INSTADIG, + IGNORE_THRESH, + FALL_THRESH, + REFRESH_RATE, + MONITOR_RATE +}; + +inline void saveConfig() { + if (pconfig.isValid()) { + pconfig.ival(MONITOR) = config.monitor_active; + pconfig.ival(VISION) = config.require_vision; + pconfig.ival(INSTADIG) = config.insta_dig; + pconfig.ival(REFRESH_RATE) = config.refresh_freq; + pconfig.ival(MONITOR_RATE) = config.monitor_freq; + pconfig.ival(IGNORE_THRESH) = config.ignore_threshold; + pconfig.ival(FALL_THRESH) = config.fall_threshold; + } +} + +// executes dig designations for the specified tile coordinates +inline bool dig_now(color_ostream &out, const df::coord &map_pos) { + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, 2) || + !Lua::PushModulePublic(out, L, "plugins.dig-now", "dig_now_tile")) + return false; + + Lua::Push(L, map_pos); + + if (!Lua::SafeCall(out, L, 1, 1)) + return false; + + return lua_toboolean(L, -1); + +} + +namespace CSP { + std::unordered_map active_workers; + std::unordered_map last_safe; + std::unordered_set dignow_queue; + + void UnpauseEvent(){ + INFO(monitor).print("UnpauseEvent()\n"); + ChannelManager::Get().build_groups(); + INFO(monitor).print("after building groups\n"); + ChannelManager::Get().debug(); + ChannelManager::Get().manage_all(); + INFO(monitor).print("UnpauseEvent() exits\n"); + ChannelManager::Get().debug(); + } + + void JobStartedEvent(color_ostream &out, void* p) { + if (config.monitor_active) { + if (enabled && World::isFortressMode() && Maps::IsValid()) { + INFO(monitor).print("JobStartedEvent()\n"); + auto job = (df::job*) p; + // validate job type + if (is_dig_job(job)) { + DEBUG(monitor).print(" valid channel job:\n"); + df::unit* worker = Job::getWorker(job); + // there is a valid worker (living citizen) on the job? right.. + if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { + DEBUG(monitor).print(" valid worker:\n"); + df::coord local(job->pos); + local.x = local.x % 16; + local.y = local.y % 16; + // check pathing exists to job + if (Maps::canWalkBetween(worker->pos, job->pos)) { + DEBUG(monitor).print(" can path from (" COORD ") to (" COORD ")\n", + COORDARGS(worker->pos), COORDARGS(job->pos)); + // track workers on jobs + active_workers.emplace(job->id, Units::findIndexById(Job::getWorker(job)->id)); + // set tile to restricted + TRACE(monitor).print(" setting job tile to restricted\n"); + Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; + } else { + DEBUG(monitor).print(" no path exists to job:\n"); + // if we can't get there, then we should remove the worker and cancel the job (restore tile designation) + Job::removeWorker(job); + cancel_job(job); + if (!config.insta_dig) { + TRACE(monitor).print(" setting marker mode for (" COORD ")\n", COORDARGS(job->pos)); + // set to marker mode + auto occupancy = Maps::getTileOccupancy(job->pos); + if (!occupancy) { + WARN(monitor).print(" Could not acquire tile occupancy*\n"); + return; + } + occupancy->bits.dig_marked = true; + // prevent algorithm from re-enabling designation + df::map_block* block = Maps::getTileBlock(job->pos); + if (!block) { + WARN(monitor).print(" Could not acquire block*\n"); + return; + } + for (auto &be: block->block_events) { ; + if (auto bsedp = virtual_cast(be)) { + TRACE(monitor).print(" re-setting priority\n"); + bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; + } + } + } else { + TRACE(monitor).print(" deleting job, and queuing insta-dig)\n"); + // queue digging the job instantly + dignow_queue.emplace(job->pos); + } + } + } + } + INFO(monitor).print(" <- JobStartedEvent() exits normally\n"); + } + } + } + + void JobCompletedEvent(color_ostream &out, void* job_ptr) { + if (config.monitor_active) { + INFO(monitor).print("JobCompletedEvent()\n"); + if (enabled && World::isFortressMode() && Maps::IsValid()) { + auto job = (df::job*) job_ptr; + // we only care if the job is a channeling one + if (is_dig_job(job)) { + // untrack job/worker + active_workers.erase(job->id); + // check job outcome + df::coord local(job->pos); + auto block = Maps::getTileBlock(local); + local.x = local.x % 16; + local.y = local.y % 16; + // verify completion + if (isOpenTerrain(block->tiletype[local.x][local.y]) + || block->designation[local.x][local.y].bits.dig != df::enums::tile_dig_designation::Channel) { + // the job can be considered done + df::coord below(job->pos); + below.z--; + WARN(monitor).print(" -> Marking tile done and managing the group below.\n"); + // mark done and manage below + Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Normal; + ChannelManager::Get().mark_done(job->pos); + ChannelManager::Get().manage_group(below); + ChannelManager::Get().debug(); + Job::removeJob(job); + } + } + } + INFO(monitor).print("JobCompletedEvent() exits\n"); + } + } + + void OnUpdate(color_ostream &out) { + if (enabled && World::isFortressMode() && Maps::IsValid() && !World::ReadPauseState()) { + static int32_t last_monitor_tick = df::global::world->frame_counter; + static int32_t last_refresh_tick = df::global::world->frame_counter; + int32_t tick = df::global::world->frame_counter; + if (tick - last_refresh_tick >= config.refresh_freq) { + last_refresh_tick = tick; + TRACE(monitor).print("OnUpdate()\n"); + UnpauseEvent(); + } + if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) { + last_monitor_tick = tick; + TRACE(monitor).print("OnUpdate()\n"); + for (df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next) { + df::job* job = link->item; + if (job) { + auto iter = active_workers.find(job->id); + TRACE(monitor).print(" -> check for job in tracking\n"); + if (iter != active_workers.end()) { + df::unit* unit = df::global::world->units.active[iter->second]; + TRACE(monitor).print(" -> compare positions of worker and job\n"); + // check if fall is possible + if (unit->pos == job->pos) { + // can fall, is safe? + TRACE(monitor).print(" equal -> check if safe fall\n"); + if (!is_safe_fall(job->pos)) { + // unsafe + Job::removeWorker(job); + if (config.insta_dig) { + TRACE(monitor).print(" -> insta-dig\n"); + // delete the job + Job::removeJob(job); + // queue digging the job instantly + dignow_queue.emplace(job->pos); + // worker is currently in the air + Units::teleport(unit, last_safe[unit->id]); + last_safe.erase(unit->id); + } else { + TRACE(monitor).print(" -> set marker mode\n"); + // set to marker mode + Maps::getTileOccupancy(job->pos)->bits.dig_marked = true; + // prevent algorithm from re-enabling designation + for (auto &be: Maps::getBlock(job->pos)->block_events) { ; + if (auto bsedp = virtual_cast( + be)) { + df::coord local(job->pos); + local.x = local.x % 16; + local.y = local.y % 16; + bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; + break; + } + } + } + } + } else { + TRACE(monitor).print(" -> save safe position\n"); + // worker is perfectly safe right now + last_safe[unit->id] = unit->pos; + } + } + } + } + TRACE(monitor).print(" -> evaluate dignow queue\n"); + for (const df::coord &pos: dignow_queue) { + if (!has_unit(Maps::getTileOccupancy(pos))) { + dig_now(out, pos); + } else { + // todo: teleport? + //Units::teleport() + } + } + TRACE(monitor).print("OnUpdate() exits\n"); + } + } + } +} + +command_result channel_safely(color_ostream &out, std::vector ¶meters); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + commands.push_back(PluginCommand("channel-safely", + "Automatically manage channel designations.", + channel_safely, + false)); + DBG_NAME(monitor).allowed(DFHack::DebugCategory::LERROR); + DBG_NAME(manager).allowed(DFHack::DebugCategory::LERROR); + DBG_NAME(groups).allowed(DFHack::DebugCategory::LERROR); + DBG_NAME(jobs).allowed(DFHack::DebugCategory::LERROR); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream &out) { + EM::unregisterAll(plugin_self); + return CR_OK; +} + +DFhackCExport command_result plugin_load_data (color_ostream &out) { + pconfig = World::GetPersistentData(CONFIG_KEY); + + if (!pconfig.isValid()) { + pconfig = World::AddPersistentData(CONFIG_KEY); + saveConfig(); + } else { + config.monitor_active = pconfig.ival(MONITOR); + config.require_vision = pconfig.ival(VISION); + config.insta_dig = pconfig.ival(INSTADIG); + config.refresh_freq = pconfig.ival(REFRESH_RATE); + config.monitor_freq = pconfig.ival(MONITOR_RATE); + config.ignore_threshold = pconfig.ival(IGNORE_THRESH); + config.fall_threshold = pconfig.ival(FALL_THRESH); + } + return DFHack::CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { + if (enable && !enabled) { + // register events to check jobs / update tracking + EM::EventHandler jobStartHandler(CSP::JobStartedEvent, 0); + EM::EventHandler jobCompletionHandler(CSP::JobCompletedEvent, 0); + EM::registerListener(EventType::JOB_STARTED, jobStartHandler, plugin_self); + EM::registerListener(EventType::JOB_COMPLETED, jobCompletionHandler, plugin_self); + // manage designations to start off (first time building groups [very important]) + out.print("channel-safely: enabled!\n"); + CSP::UnpauseEvent(); + } else if (!enable) { + // don't need the groups if the plugin isn't going to be enabled + EM::unregisterAll(plugin_self); + out.print("channel-safely: disabled!\n"); + } + enabled = enable; + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (enabled && World::isFortressMode() && Maps::IsValid()) { + switch (event) { + case SC_MAP_LOADED: + // cache the map size + Maps::getSize(mapx, mapy, mapz); + case SC_UNPAUSED: + // manage all designations on unpause + CSP::UnpauseEvent(); + default: + break; + } + } + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_event event) { + CSP::OnUpdate(out); + return DFHack::CR_OK; +} + +command_result channel_safely(color_ostream &out, std::vector ¶meters) { + if (!parameters.empty()) { + if (parameters.size() >= 2 && parameters.size() <= 3) { + if (parameters[0] == "run" && parameters[1] == "once") { + CSP::UnpauseEvent(); + return DFHack::CR_OK; + } + bool state = false; + bool set = false; + if (parameters[0] == "enable") { + state = true; + } else if (parameters[0] == "disable") { + state = false; + } else if (parameters[0] == "set") { + set = true; + } else { + return DFHack::CR_WRONG_USAGE; + } + try { + if (parameters[1] == "debug") { + auto level = std::abs(std::stol(parameters[2])); + config.debug = true; + switch (level) { + case 1: + DBG_NAME(manager).allowed(DFHack::DebugCategory::LDEBUG); + DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO); + DBG_NAME(groups).allowed(DFHack::DebugCategory::LINFO); + DBG_NAME(jobs).allowed(DFHack::DebugCategory::LINFO); + break; + case 2: + DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO); + DBG_NAME(groups).allowed(DFHack::DebugCategory::LDEBUG); + DBG_NAME(jobs).allowed(DFHack::DebugCategory::LDEBUG); + break; + case 3: + DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO); + DBG_NAME(groups).allowed(DFHack::DebugCategory::LDEBUG); + DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE); + break; + case 4: + DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO); + DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE); + break; + case 5: + DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(monitor).allowed(DFHack::DebugCategory::LDEBUG); + DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE); + break; + case 6: + DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(monitor).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE); + DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE); + break; + case 0: + default: + DBG_NAME(monitor).allowed(DFHack::DebugCategory::LERROR); + DBG_NAME(manager).allowed(DFHack::DebugCategory::LERROR); + DBG_NAME(groups).allowed(DFHack::DebugCategory::LERROR); + DBG_NAME(jobs).allowed(DFHack::DebugCategory::LERROR); + } + } else if(parameters[1] == "monitor-active"){ + config.monitor_active = state; + } else if (parameters[1] == "require-vision") { + config.require_vision = state; + } else if (parameters[1] == "insta-dig") { + config.insta_dig = state; + } else if (parameters[1] == "refresh-freq" && set && parameters.size() == 3) { + config.refresh_freq = std::abs(std::stol(parameters[2])); + } else if (parameters[1] == "monitor-freq" && set && parameters.size() == 3) { + config.monitor_freq = std::abs(std::stol(parameters[2])); + } else if (parameters[1] == "ignore-threshold" && set && parameters.size() == 3) { + config.ignore_threshold = std::abs(std::stol(parameters[2])); + } else if (parameters[1] == "fall-threshold" && set && parameters.size() == 3) { + uint8_t t = std::abs(std::stol(parameters[2])); + if (t > 0) { + config.fall_threshold = t; + } else { + out.printerr("fall-threshold must have a value greater than 0 or the plugin does a lot of nothing.\n"); + return DFHack::CR_FAILURE; + } + } else { + return DFHack::CR_WRONG_USAGE; + } + } catch (const std::exception &e) { + out.printerr("%s\n", e.what()); + return DFHack::CR_FAILURE; + } + } + } else { + out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED."); + out.print("monitor-active: %s\n", config.monitor_active ? "on." : "off."); + out.print("require-vision: %s\n", config.require_vision ? "on." : "off."); + out.print("insta-dig: %s\n", config.insta_dig ? "on." : "off."); + out.print("refresh-freq: %" PRIi32 "\n", config.refresh_freq); + out.print("monitor-freq: %" PRIi32 "\n", config.monitor_freq); + out.print("ignore-threshold: %" PRIu8 "\n", config.ignore_threshold); + out.print("fall-threshold: %" PRIu8 "\n", config.fall_threshold); + } + saveConfig(); + return DFHack::CR_OK; +} + + diff --git a/plugins/channel-safely/include/channel-groups.h b/plugins/channel-safely/include/channel-groups.h new file mode 100644 index 000000000..d39780df3 --- /dev/null +++ b/plugins/channel-safely/include/channel-groups.h @@ -0,0 +1,49 @@ +#pragma once +#include "plugin.h" +#include "channel-jobs.h" + +#include +#include + +#include +#include +#include + +using namespace DFHack; + +using Group = std::set; +using Groups = std::vector; + +/* Used to build groups of adjacent channel designations/jobs + * groups_map: maps coordinates to a group index in `groups` + * groups: list of Groups + * Group: used to track designations which are connected through adjacency to one another (a group cannot span Z) + * Note: a designation plan may become unsafe if the jobs aren't completed in a specific order; + * the easiest way to programmatically ensure safety is to.. + * lock overlapping groups directly adjacent across Z until the above groups are complete, or no longer overlap + * groups may no longer overlap if the adjacent designations are completed, but requires a rebuild of groups + * jobs: list of coordinates with channel jobs associated to them + */ +class ChannelGroups { +private: + using GroupsMap = std::map; + GroupsMap groups_map; + Groups groups; + ChannelJobs &jobs; + std::set free_spots; +protected: + void scan_map(); + void add(const df::coord &map_pos); +public: + explicit ChannelGroups(ChannelJobs &jobs) : jobs(jobs) { groups.reserve(200); } + void scan_one(const df::coord &map_pos); + void build(); + void clear(); + void remove(const df::coord &map_pos); + Groups::const_iterator find(const df::coord &map_pos) const; + Groups::const_iterator begin() const; + Groups::const_iterator end() const; + size_t count(const df::coord &map_pos) const; + void debug_groups(); + void debug_map(); +}; diff --git a/plugins/channel-safely/include/channel-jobs.h b/plugins/channel-safely/include/channel-jobs.h new file mode 100644 index 000000000..d0aaead7c --- /dev/null +++ b/plugins/channel-safely/include/channel-jobs.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include + +using namespace DFHack; + +/* Used to read/store/iterate channel digging jobs + * jobs: list of coordinates with channel jobs associated to them + * load_channel_jobs: iterates world->jobs.list to find channel jobs and adds them into the `jobs` map + * clear: empties the container + * erase: finds a job corresponding to a coord, removes the mapping in jobs, and calls Job::removeJob, then returns an iterator following the element removed + * find: returns an iterator to a job if one exists for a map coordinate + * begin: returns jobs.begin() + * end: returns jobs.end() + */ +class ChannelJobs { +private: + friend class ChannelGroup; + using Jobs = std::set; // job* will exist until it is complete, and likely beyond + Jobs jobs; +public: + void load_channel_jobs(); + void clear(); + Jobs::iterator erase(const df::coord &map_pos); + Jobs::const_iterator find(const df::coord &map_pos) const; + Jobs::const_iterator begin() const; + Jobs::const_iterator end() const; +}; diff --git a/plugins/channel-safely/include/channel-manager.h b/plugins/channel-safely/include/channel-manager.h new file mode 100644 index 000000000..2e33c5c46 --- /dev/null +++ b/plugins/channel-safely/include/channel-manager.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include +#include +#include "channel-groups.h" +#include "plugin.h" + +using namespace DFHack; + +// Uses GroupData to detect an unsafe work environment +class ChannelManager { +private: + ChannelJobs jobs; + ChannelManager()= default; +protected: +public: + ChannelGroups groups = ChannelGroups(jobs); + + static ChannelManager& Get(){ + static ChannelManager instance; + return instance; + } + + void build_groups() { groups.build(); debug(); } + void manage_all(); + void manage_group(const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false); + void manage_group(const Group &group, bool set_marker_mode = false, bool marker_mode = false); + bool manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false); + void mark_done(const df::coord &map_pos); + void debug() { + if (config.debug) { + groups.debug_groups(); + groups.debug_map(); + //std::terminate(); + } + } +}; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h new file mode 100644 index 000000000..72042327a --- /dev/null +++ b/plugins/channel-safely/include/inlines.h @@ -0,0 +1,149 @@ +#pragma once +#include "plugin.h" +#include "channel-manager.h" + +#include +#include +#include + +#include +#include + +#define Coord(id) id.x][id.y +#define COORD "%" PRIi16 " %" PRIi16 " %" PRIi16 +#define COORDARGS(id) id.x, id.y, id.z + +namespace CSP { + extern std::unordered_set dignow_queue; +} + +inline bool is_dig_job(const df::job* job) { + return job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel; +} + +inline bool is_dig_designation(const df::tile_designation &designation) { + return designation.bits.dig != df::tile_dig_designation::No; +} + +inline bool has_unit(const df::tile_occupancy* occupancy) { + return occupancy->bits.unit || occupancy->bits.unit_grounded; +} + +inline bool is_safe_fall(const df::coord &map_pos) { + df::coord below(map_pos); + for (uint8_t zi = 0; zi < config.fall_threshold; ++zi) { + below.z--; + if (config.require_vision && Maps::getTileDesignation(below)->bits.hidden) { + return true; //we require vision, and we can't see below.. so we gotta assume it's safe + } + df::tiletype type = *Maps::getTileType(below); + if (!isOpenTerrain(type)) { + return true; + } + } + return false; +} + +inline bool is_safe_to_dig_down(const df::coord &map_pos) { + df::coord pos(map_pos); + + for (uint8_t zi = 0; zi <= config.fall_threshold; ++zi) { + // assume safe if we can't see and need vision + if (config.require_vision && Maps::getTileDesignation(pos)->bits.hidden) { + return true; + } + df::tiletype type = *Maps::getTileType(pos); + if (zi == 0 && isOpenTerrain(type)) { + // the starting tile is open space, that's obviously not safe + return false; + } else if (!isOpenTerrain(type)) { + // a tile after the first one is not open space + return true; + } + pos.z--; + } + return false; +} + +inline bool is_group_occupied(const ChannelGroups &groups, const Group &group) { + // return true if any tile in the group is occupied by a unit + return std::any_of(group.begin(), group.end(), [](const Group::key_type &pos){ + return has_unit(Maps::getTileOccupancy(pos)); + }); +} + +inline bool has_group_above(const ChannelGroups &groups, const df::coord &map_pos) { + df::coord above(map_pos); + above.z++; + if (groups.count(above)) { + return true; + } + return false; +} + +inline bool has_any_groups_above(const ChannelGroups &groups, const Group &group) { + // for each designation in the group + for (auto &pos : group) { + df::coord above(pos); + above.z++; + if (groups.count(above)) { + return true; + } + } + // if there are no incomplete groups above this group, then this group is ready + return false; +} + +inline void cancel_job(df::job* job) { + if (job != nullptr) { + df::coord &pos = job->pos; + df::map_block* job_block = Maps::getTileBlock(pos); + uint16_t x, y; + x = pos.x % 16; + y = pos.y % 16; + df::tile_designation &designation = job_block->designation[x][y]; + switch (job->job_type) { + case job_type::Dig: + designation.bits.dig = df::tile_dig_designation::Default; + break; + case job_type::CarveUpwardStaircase: + designation.bits.dig = df::tile_dig_designation::UpStair; + break; + case job_type::CarveDownwardStaircase: + designation.bits.dig = df::tile_dig_designation::DownStair; + break; + case job_type::CarveUpDownStaircase: + designation.bits.dig = df::tile_dig_designation::UpDownStair; + break; + case job_type::CarveRamp: + designation.bits.dig = df::tile_dig_designation::Ramp; + break; + case job_type::DigChannel: + designation.bits.dig = df::tile_dig_designation::Channel; + break; + default: + designation.bits.dig = df::tile_dig_designation::No; + break; + } + Job::removeJob(job); + } +} + +inline void get_neighbours(const df::coord &map_pos, df::coord(&neighbours)[8]) { + neighbours[0] = map_pos; + neighbours[1] = map_pos; + neighbours[2] = map_pos; + neighbours[3] = map_pos; + neighbours[4] = map_pos; + neighbours[5] = map_pos; + neighbours[6] = map_pos; + neighbours[7] = map_pos; + neighbours[0].x--; neighbours[0].y--; + neighbours[1].y--; + neighbours[2].x++; neighbours[2].y--; + neighbours[3].x--; + neighbours[4].x++; + neighbours[5].x--; neighbours[5].y++; + neighbours[6].y++; + neighbours[7].x++; neighbours[7].y++; +} diff --git a/plugins/channel-safely/include/plugin.h b/plugins/channel-safely/include/plugin.h new file mode 100644 index 000000000..71e4665c7 --- /dev/null +++ b/plugins/channel-safely/include/plugin.h @@ -0,0 +1,23 @@ +#pragma once +#include + +namespace DFHack { + DBG_EXTERN(channelsafely, monitor); + DBG_EXTERN(channelsafely, manager); + DBG_EXTERN(channelsafely, groups); + DBG_EXTERN(channelsafely, jobs); +} + +struct Configuration { + bool debug = false; + bool monitor_active = false; + bool require_vision = true; + bool insta_dig = false; + int32_t refresh_freq = 600; + int32_t monitor_freq = 10; + uint8_t ignore_threshold = 7; + uint8_t fall_threshold = 1; +}; + +extern Configuration config; +extern int32_t mapx, mapy, mapz; From bd6c748d00d99202ccf9844c3f8459561a265973 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 11:53:46 -0800 Subject: [PATCH 111/161] Implements plugin: channel-safely v0.2 --- plugins/channel-safely/channel-groups.cpp | 156 +++++++++--------- plugins/channel-safely/channel-manager.cpp | 6 +- .../channel-safely/channel-safely-plugin.cpp | 20 ++- .../channel-safely/include/channel-groups.h | 5 +- .../channel-safely/include/channel-manager.h | 7 +- 5 files changed, 104 insertions(+), 90 deletions(-) diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 7ee779df8..41928c556 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -5,67 +5,6 @@ #include -// scans the map for channel designations -void ChannelGroups::scan_map() { - static std::default_random_engine RNG(0); - static std::bernoulli_distribution optimizing(0.3333); - DEBUG(groups).print(" scan_map()\n"); - // foreach block - for (int32_t z = mapz - 1; z >= 0; --z) { - for (int32_t by = 0; by < mapy; ++by) { - for (int32_t bx = 0; bx < mapx; ++bx) { - // the block - if (df::map_block* block = Maps::getBlock(bx, by, z)) { - // skip this block? - if (!block->flags.bits.designated && optimizing(RNG)) { - // todo: add remainder of block width onto bx - TRACE(groups).print(" skipping this block, it has no designations\n"); - continue; - } - // foreach tile - for (int16_t lx = 0; lx < 16; ++lx) { - for (int16_t ly = 0; ly < 16; ++ly) { - // the tile, check if it has a channel designation - if (is_dig_designation(block->designation[lx][ly])) { - for (df::block_square_event* event: block->block_events) { - if (auto evT = virtual_cast(event)) { - // we want to let the user keep some designations free of being managed - TRACE(groups).print(" tile designation priority: %d\n", evT->priority[lx][ly]); - if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { - df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z); - TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); - add(map_pos); - } - } - } - } - } - } - } - } - } - } - INFO(groups).print("scan_map() exits\n"); -} - -// scans a single tile for channel designations -void ChannelGroups::scan_one(const df::coord &map_pos) { - df::map_block* block = Maps::getTileBlock(map_pos); - int16_t lx = map_pos.x % 16; - int16_t ly = map_pos.y % 16; - if (is_dig_designation(block->designation[lx][ly])) { - for (df::block_square_event* event: block->block_events) { - if (auto evT = virtual_cast(event)) { - // we want to let the user keep some designations free of being managed - if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { - TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); - add(map_pos); - } - } - } - } -} - // adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found void ChannelGroups::add(const df::coord &map_pos) { // if we've already added this, we don't need to do it again @@ -150,16 +89,79 @@ void ChannelGroups::add(const df::coord &map_pos) { // DEBUG(groups).flush(); } +// scans a single tile for channel designations +void ChannelGroups::scan_one(const df::coord &map_pos) { + df::map_block* block = Maps::getTileBlock(map_pos); + int16_t lx = map_pos.x % 16; + int16_t ly = map_pos.y % 16; + if (is_dig_designation(block->designation[lx][ly])) { + for (df::block_square_event* event: block->block_events) { + if (auto evT = virtual_cast(event)) { + // we want to let the user keep some designations free of being managed + if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { + TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); + add(map_pos); + } + } + } + } else if (isOpenTerrain(block->tiletype[lx][ly])) { + remove(map_pos); + } +} + // builds groupings of adjacent channel designations -void ChannelGroups::build() { - clear(); +void ChannelGroups::scan() { // iterate over each job, finding channel jobs jobs.load_channel_jobs(); // transpose channel jobs to for (auto &pos : jobs) { add(pos); } - scan_map(); + DEBUG(groups).print(" scan()\n"); + // foreach block + for (int32_t z = mapz - 1; z >= 0; --z) { + for (int32_t by = 0; by < mapy; ++by) { + for (int32_t bx = 0; bx < mapx; ++bx) { + // the block + if (df::map_block* block = Maps::getBlock(bx, by, z)) { + // skip this block? + if (!block->flags.bits.designated && !group_blocks.count(block)) { + continue; + } + // foreach tile + bool empty_group = true; + for (int16_t lx = 0; lx < 16; ++lx) { + for (int16_t ly = 0; ly < 16; ++ly) { + // the tile, check if it has a channel designation + df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z); + if (is_dig_designation(block->designation[lx][ly])) { + for (df::block_square_event* event: block->block_events) { + if (auto evT = virtual_cast(event)) { + // we want to let the user keep some designations free of being managed + TRACE(groups).print(" tile designation priority: %d\n", evT->priority[lx][ly]); + if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { + if (empty_group) { + group_blocks.emplace(block); + } + TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); + add(map_pos); + empty_group = false; + } + } + } + } else if (isOpenTerrain(block->tiletype[lx][ly])) { + remove(map_pos); + } + } + } + if (empty_group) { + group_blocks.erase(block); + } + } + } + } + } + INFO(groups).print("scan() exits\n"); } // clears out the containers for unloading maps or disabling the plugin @@ -231,23 +233,23 @@ size_t ChannelGroups::count(const df::coord &map_pos) const { // prints debug info about the groups stored, and their members void ChannelGroups::debug_groups() { -// int idx = 0; -// TRACE(groups).print(" debugging group data\n"); -// for (auto &group : groups) { -// TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size()); -// for (auto &pos : group) { -// TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); -// } -// idx++; -// } + int idx = 0; + TRACE(groups).print(" debugging group data\n"); + for (auto &group : groups) { + TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size()); + for (auto &pos : group) { + TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + } + idx++; + } } // prints debug info group mappings void ChannelGroups::debug_map() { -// INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); -// for (auto &pair : groups_map) { -// DEBUG(groups).print(" map[" COORD "] = %d\n",COORDARGS(pair.first), pair.second); -// } + INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); + for (auto &pair : groups_map) { + DEBUG(groups).print(" map[" COORD "] = %d\n",COORDARGS(pair.first), pair.second); + } } diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 79bc6a7aa..28d757772 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -10,8 +10,8 @@ blocks[48][96][135].designation[10][0].hidden: false * */ // sets mark flags as necessary, for all designations -void ChannelManager::manage_all() { - INFO(manager).print("manage_all()\n"); +void ChannelManager::manage_groups() { + INFO(manager).print("manage_groups()\n"); // make sure we've got a fort map to analyze if (World::isFortressMode() && Maps::IsValid()) { // iterate the groups we built/updated @@ -95,5 +95,5 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo void ChannelManager::mark_done(const df::coord &map_pos) { groups.remove(map_pos); - jobs.erase(map_pos); + jobs.erase(map_pos); //redundant (repopulated on each build) } diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index e6cdf231b..b473598ae 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -140,11 +140,9 @@ namespace CSP { void UnpauseEvent(){ INFO(monitor).print("UnpauseEvent()\n"); ChannelManager::Get().build_groups(); - INFO(monitor).print("after building groups\n"); + ChannelManager::Get().manage_groups(); ChannelManager::Get().debug(); - ChannelManager::Get().manage_all(); INFO(monitor).print("UnpauseEvent() exits\n"); - ChannelManager::Get().debug(); } void JobStartedEvent(color_ostream &out, void* p) { @@ -387,10 +385,22 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan // manage all designations on unpause CSP::UnpauseEvent(); default: - break; + return DFHack::CR_OK; } } - return CR_OK; + switch (event) { + case SC_WORLD_LOADED: + case SC_WORLD_UNLOADED: + case SC_MAP_UNLOADED: + // destroy any old group data + out.print("channel-safely: unloading data!\n"); + ChannelManager::Get().destroy_groups(); + case SC_MAP_LOADED: + // cache the map size + Maps::getSize(mapx, mapy, mapz); + default: + return DFHack::CR_OK; + } } DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_event event) { diff --git a/plugins/channel-safely/include/channel-groups.h b/plugins/channel-safely/include/channel-groups.h index d39780df3..abdbc56fc 100644 --- a/plugins/channel-safely/include/channel-groups.h +++ b/plugins/channel-safely/include/channel-groups.h @@ -26,18 +26,19 @@ using Groups = std::vector; */ class ChannelGroups { private: + using GroupBlocks = std::set; using GroupsMap = std::map; + GroupBlocks group_blocks; GroupsMap groups_map; Groups groups; ChannelJobs &jobs; std::set free_spots; protected: - void scan_map(); void add(const df::coord &map_pos); public: explicit ChannelGroups(ChannelJobs &jobs) : jobs(jobs) { groups.reserve(200); } void scan_one(const df::coord &map_pos); - void build(); + void scan(); void clear(); void remove(const df::coord &map_pos); Groups::const_iterator find(const df::coord &map_pos) const; diff --git a/plugins/channel-safely/include/channel-manager.h b/plugins/channel-safely/include/channel-manager.h index 2e33c5c46..79c1e3770 100644 --- a/plugins/channel-safely/include/channel-manager.h +++ b/plugins/channel-safely/include/channel-manager.h @@ -23,17 +23,18 @@ public: return instance; } - void build_groups() { groups.build(); debug(); } - void manage_all(); + void build_groups() { groups.scan(); debug(); } + void destroy_groups() { groups.clear(); debug(); } + void manage_groups(); void manage_group(const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false); void manage_group(const Group &group, bool set_marker_mode = false, bool marker_mode = false); bool manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false); void mark_done(const df::coord &map_pos); void debug() { + DEBUG(groups).print(" DEBUGGING GROUPS:\n"); if (config.debug) { groups.debug_groups(); groups.debug_map(); - //std::terminate(); } } }; From 3a6205d19e2f218b33dae5f4f0fea16154575678 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 11:55:45 -0800 Subject: [PATCH 112/161] Removes extra EOF lines --- plugins/channel-safely/channel-groups.cpp | 2 -- plugins/channel-safely/channel-safely-plugin.cpp | 2 -- 2 files changed, 4 deletions(-) diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 41928c556..638ba395b 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -251,5 +251,3 @@ void ChannelGroups::debug_map() { DEBUG(groups).print(" map[" COORD "] = %d\n",COORDARGS(pair.first), pair.second); } } - - diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index b473598ae..a5f5be583 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -515,5 +515,3 @@ command_result channel_safely(color_ostream &out, std::vector ¶ saveConfig(); return DFHack::CR_OK; } - - From b7ee01108e1a5ae5e984f4f6d4d51103f520d7db Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 12:04:01 -0800 Subject: [PATCH 113/161] Implements plugin: channel-safely v0.3 --- .../channel-safely/channel-safely-plugin.cpp | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index a5f5be583..58b92f4a6 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -209,33 +209,30 @@ namespace CSP { } void JobCompletedEvent(color_ostream &out, void* job_ptr) { - if (config.monitor_active) { + if (enabled && World::isFortressMode() && Maps::IsValid()) { INFO(monitor).print("JobCompletedEvent()\n"); - if (enabled && World::isFortressMode() && Maps::IsValid()) { - auto job = (df::job*) job_ptr; - // we only care if the job is a channeling one - if (is_dig_job(job)) { - // untrack job/worker - active_workers.erase(job->id); - // check job outcome - df::coord local(job->pos); - auto block = Maps::getTileBlock(local); - local.x = local.x % 16; - local.y = local.y % 16; - // verify completion - if (isOpenTerrain(block->tiletype[local.x][local.y]) - || block->designation[local.x][local.y].bits.dig != df::enums::tile_dig_designation::Channel) { - // the job can be considered done - df::coord below(job->pos); - below.z--; - WARN(monitor).print(" -> Marking tile done and managing the group below.\n"); - // mark done and manage below - Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Normal; - ChannelManager::Get().mark_done(job->pos); - ChannelManager::Get().manage_group(below); - ChannelManager::Get().debug(); - Job::removeJob(job); - } + auto job = (df::job*) job_ptr; + // we only care if the job is a channeling one + if (is_dig_job(job)) { + // untrack job/worker + active_workers.erase(job->id); + // check job outcome + df::coord local(job->pos); + auto block = Maps::getTileBlock(local); + local.x = local.x % 16; + local.y = local.y % 16; + // verify completion + if (isOpenTerrain(block->tiletype[local.x][local.y]) + || block->designation[local.x][local.y].bits.dig != df::enums::tile_dig_designation::Channel) { + // the job can be considered done + df::coord below(job->pos); + below.z--; + WARN(monitor).print(" -> Marking tile done and managing the group below.\n"); + // mark done and manage below + Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Normal; + ChannelManager::Get().mark_done(job->pos); + ChannelManager::Get().manage_group(below); + ChannelManager::Get().debug(); } } INFO(monitor).print("JobCompletedEvent() exits\n"); From a8dcfeead9dddfb8a90f2533c0ac2c7ce406bc63 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 12:09:38 -0800 Subject: [PATCH 114/161] Implements plugin: channel-safely v0.3.1 --- .../channel-safely/channel-safely-plugin.cpp | 104 +++++++++--------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 58b92f4a6..333d4452a 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -146,65 +146,63 @@ namespace CSP { } void JobStartedEvent(color_ostream &out, void* p) { - if (config.monitor_active) { - if (enabled && World::isFortressMode() && Maps::IsValid()) { - INFO(monitor).print("JobStartedEvent()\n"); - auto job = (df::job*) p; - // validate job type - if (is_dig_job(job)) { - DEBUG(monitor).print(" valid channel job:\n"); - df::unit* worker = Job::getWorker(job); - // there is a valid worker (living citizen) on the job? right.. - if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { - DEBUG(monitor).print(" valid worker:\n"); - df::coord local(job->pos); - local.x = local.x % 16; - local.y = local.y % 16; - // check pathing exists to job - if (Maps::canWalkBetween(worker->pos, job->pos)) { - DEBUG(monitor).print(" can path from (" COORD ") to (" COORD ")\n", - COORDARGS(worker->pos), COORDARGS(job->pos)); - // track workers on jobs - active_workers.emplace(job->id, Units::findIndexById(Job::getWorker(job)->id)); - // set tile to restricted - TRACE(monitor).print(" setting job tile to restricted\n"); - Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; - } else { - DEBUG(monitor).print(" no path exists to job:\n"); - // if we can't get there, then we should remove the worker and cancel the job (restore tile designation) - Job::removeWorker(job); - cancel_job(job); - if (!config.insta_dig) { - TRACE(monitor).print(" setting marker mode for (" COORD ")\n", COORDARGS(job->pos)); - // set to marker mode - auto occupancy = Maps::getTileOccupancy(job->pos); - if (!occupancy) { - WARN(monitor).print(" Could not acquire tile occupancy*\n"); - return; - } - occupancy->bits.dig_marked = true; - // prevent algorithm from re-enabling designation - df::map_block* block = Maps::getTileBlock(job->pos); - if (!block) { - WARN(monitor).print(" Could not acquire block*\n"); - return; - } - for (auto &be: block->block_events) { ; - if (auto bsedp = virtual_cast(be)) { - TRACE(monitor).print(" re-setting priority\n"); - bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; - } + if (enabled && World::isFortressMode() && Maps::IsValid()) { + INFO(monitor).print("JobStartedEvent()\n"); + auto job = (df::job*) p; + // validate job type + if (is_dig_job(job)) { + DEBUG(monitor).print(" valid channel job:\n"); + df::unit* worker = Job::getWorker(job); + // there is a valid worker (living citizen) on the job? right.. + if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { + DEBUG(monitor).print(" valid worker:\n"); + df::coord local(job->pos); + local.x = local.x % 16; + local.y = local.y % 16; + // check pathing exists to job + if (Maps::canWalkBetween(worker->pos, job->pos)) { + DEBUG(monitor).print(" can path from (" COORD ") to (" COORD ")\n", + COORDARGS(worker->pos), COORDARGS(job->pos)); + // track workers on jobs + active_workers.emplace(job->id, Units::findIndexById(Job::getWorker(job)->id)); + // set tile to restricted + TRACE(monitor).print(" setting job tile to restricted\n"); + Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; + } else { + DEBUG(monitor).print(" no path exists to job:\n"); + // if we can't get there, then we should remove the worker and cancel the job (restore tile designation) + Job::removeWorker(job); + cancel_job(job); + if (!config.insta_dig) { + TRACE(monitor).print(" setting marker mode for (" COORD ")\n", COORDARGS(job->pos)); + // set to marker mode + auto occupancy = Maps::getTileOccupancy(job->pos); + if (!occupancy) { + WARN(monitor).print(" Could not acquire tile occupancy*\n"); + return; + } + occupancy->bits.dig_marked = true; + // prevent algorithm from re-enabling designation + df::map_block* block = Maps::getTileBlock(job->pos); + if (!block) { + WARN(monitor).print(" Could not acquire block*\n"); + return; + } + for (auto &be: block->block_events) { ; + if (auto bsedp = virtual_cast(be)) { + TRACE(monitor).print(" re-setting priority\n"); + bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; } - } else { - TRACE(monitor).print(" deleting job, and queuing insta-dig)\n"); - // queue digging the job instantly - dignow_queue.emplace(job->pos); } + } else { + TRACE(monitor).print(" deleting job, and queuing insta-dig)\n"); + // queue digging the job instantly + dignow_queue.emplace(job->pos); } } } - INFO(monitor).print(" <- JobStartedEvent() exits normally\n"); } + INFO(monitor).print(" <- JobStartedEvent() exits normally\n"); } } From c2d346fc841dfa8b6affdb4c683ed15241b410cc Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 13:16:27 -0800 Subject: [PATCH 115/161] Implements plugin: channel-safely v0.4 --- docs/plugins/channel-safely.rst | 5 +- plugins/channel-safely/channel-groups.cpp | 32 ++++++++- plugins/channel-safely/channel-manager.cpp | 18 +++-- .../channel-safely/channel-safely-plugin.cpp | 33 ++++----- plugins/channel-safely/include/inlines.h | 67 ++++++++++++------- plugins/channel-safely/include/plugin.h | 4 +- 6 files changed, 105 insertions(+), 54 deletions(-) diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 6010f4925..842d57353 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -55,8 +55,7 @@ Settings -------- :refresh-freq: The rate at which full refreshes are performed. This can be expensive if you're undertaking many mega projects. (default:600, twice a day) -:monitor-freq: The rate at which active jobs are monitored. - todo: this can have a massive impact? (default:10) +:monitor-freq: The rate at which active jobs are monitored. (default:1) :ignore-threshold: Sets the priority threshold below which designations are processed. You can set to 1 or 0 to - effectively disable the scanning. (default: 7) + effectively disable the scanning. (default: 5) :fall-threshold: Sets the fall threshold beyond which is considered unsafe. (default: 1) diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 638ba395b..7e5cccf20 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -5,6 +5,22 @@ #include +template +void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { + for (const auto &a : c1) { + bool matched = false; + for (const auto &b : c2) { + if (a == b) { + matched = true; + break; + } + } + if (!matched) { + c3.emplace(a); + } + } +} + // adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found void ChannelGroups::add(const df::coord &map_pos) { // if we've already added this, we don't need to do it again @@ -111,12 +127,24 @@ void ChannelGroups::scan_one(const df::coord &map_pos) { // builds groupings of adjacent channel designations void ChannelGroups::scan() { - // iterate over each job, finding channel jobs + // save current jobs, then clear and load the current jobs + std::set last_jobs; + for (auto &pos : jobs) { + last_jobs.emplace(pos); + } jobs.load_channel_jobs(); // transpose channel jobs to - for (auto &pos : jobs) { + std::set new_jobs; + std::set gone_jobs; + set_difference(last_jobs, jobs, gone_jobs); + set_difference(jobs, last_jobs, new_jobs); + for (auto &pos : new_jobs) { add(pos); } + for (auto &pos : gone_jobs){ + remove(pos); + } + DEBUG(groups).print(" scan()\n"); // foreach block for (int32_t z = mapz - 1; z >= 0; --z) { diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 28d757772..86ce5a726 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -62,9 +62,18 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo // do we already know whether to set marker mode? if (set_marker_mode) { DEBUG(manager).print(" -> marker_mode\n"); - tile_occupancy.bits.dig_marked = marker_mode; - jobs.erase(map_pos); - return true; + // if enabling marker mode, just do it + if (marker_mode) { + tile_occupancy.bits.dig_marked = marker_mode; + return true; + } + // if activating designation, check if it is safe to dig or not a channel designation + if (!is_channel_designation(block->designation[Coord(local)]) || is_safe_to_dig_down(map_pos)) { + tile_occupancy.bits.dig_marked = marker_mode; + return marker_mode; + } + return false; + } else { // next search for the designation priority for (df::block_square_event* event: block->block_events) { @@ -73,7 +82,7 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo if (evT->priority[Coord(local)] < 1000 * config.ignore_threshold) { DEBUG(manager).print(" if(has_groups_above())\n"); // check that the group has no incomplete groups directly above it - if (has_group_above(groups, map_pos)) { + if (has_group_above(groups, map_pos) || !is_safe_to_dig_down(map_pos)) { DEBUG(manager).print(" has_groups_above: setting marker mode\n"); tile_occupancy.bits.dig_marked = true; jobs.erase(map_pos); @@ -95,5 +104,4 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo void ChannelManager::mark_done(const df::coord &map_pos) { groups.remove(map_pos); - jobs.erase(map_pos); //redundant (repopulated on each build) } diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 333d4452a..a63228e27 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -1,7 +1,7 @@ /* Prevent channeling down into known open space. Author: Josh Cooper Created: Aug. 4 2020 -Updated: Nov. 1 2022 +Updated: Nov. 6 2022 Enable plugin: -> build groups @@ -13,9 +13,9 @@ Updated: Nov. 1 2022 Manage Designation(s): -> for each group in groups: - -> for each designation in this group: - -> - + -> does any tile in group have a group above + -> Yes: set entire group to marker mode + -> No: activate entire group (still checks is_safe_to_dig_down before activating each designation) Job started event: -> validate job type (channel) @@ -150,7 +150,7 @@ namespace CSP { INFO(monitor).print("JobStartedEvent()\n"); auto job = (df::job*) p; // validate job type - if (is_dig_job(job)) { + if (is_channel_job(job)) { DEBUG(monitor).print(" valid channel job:\n"); df::unit* worker = Job::getWorker(job); // there is a valid worker (living citizen) on the job? right.. @@ -160,7 +160,7 @@ namespace CSP { local.x = local.x % 16; local.y = local.y % 16; // check pathing exists to job - if (Maps::canWalkBetween(worker->pos, job->pos)) { + if (can_reach_designation(worker->pos, job->pos)) { DEBUG(monitor).print(" can path from (" COORD ") to (" COORD ")\n", COORDARGS(worker->pos), COORDARGS(job->pos)); // track workers on jobs @@ -246,6 +246,17 @@ namespace CSP { last_refresh_tick = tick; TRACE(monitor).print("OnUpdate()\n"); UnpauseEvent(); + + TRACE(monitor).print(" -> evaluate dignow queue\n"); + for (const df::coord &pos: dignow_queue) { + if (!has_unit(Maps::getTileOccupancy(pos))) { + dig_now(out, pos); + } else { + // todo: teleport? + //Units::teleport() + } + } + TRACE(monitor).print("OnUpdate() exits\n"); } if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) { last_monitor_tick = tick; @@ -299,16 +310,6 @@ namespace CSP { } } } - TRACE(monitor).print(" -> evaluate dignow queue\n"); - for (const df::coord &pos: dignow_queue) { - if (!has_unit(Maps::getTileOccupancy(pos))) { - dig_now(out, pos); - } else { - // todo: teleport? - //Units::teleport() - } - } - TRACE(monitor).print("OnUpdate() exits\n"); } } } diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index 72042327a..e8689d245 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -17,16 +17,39 @@ namespace CSP { extern std::unordered_set dignow_queue; } +inline void get_neighbours(const df::coord &map_pos, df::coord(&neighbours)[8]) { + neighbours[0] = map_pos; + neighbours[1] = map_pos; + neighbours[2] = map_pos; + neighbours[3] = map_pos; + neighbours[4] = map_pos; + neighbours[5] = map_pos; + neighbours[6] = map_pos; + neighbours[7] = map_pos; + neighbours[0].x--; neighbours[0].y--; + neighbours[1].y--; + neighbours[2].x++; neighbours[2].y--; + neighbours[3].x--; + neighbours[4].x++; + neighbours[5].x--; neighbours[5].y++; + neighbours[6].y++; + neighbours[7].x++; neighbours[7].y++; +} + inline bool is_dig_job(const df::job* job) { return job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel; } +inline bool is_channel_job(const df::job* job) { + return job->job_type == df::job_type::DigChannel; +} + inline bool is_dig_designation(const df::tile_designation &designation) { return designation.bits.dig != df::tile_dig_designation::No; } -inline bool has_unit(const df::tile_occupancy* occupancy) { - return occupancy->bits.unit || occupancy->bits.unit_grounded; +inline bool is_channel_designation(const df::tile_designation &designation) { + return designation.bits.dig != df::tile_dig_designation::Channel; } inline bool is_safe_fall(const df::coord &map_pos) { @@ -65,11 +88,22 @@ inline bool is_safe_to_dig_down(const df::coord &map_pos) { return false; } -inline bool is_group_occupied(const ChannelGroups &groups, const Group &group) { - // return true if any tile in the group is occupied by a unit - return std::any_of(group.begin(), group.end(), [](const Group::key_type &pos){ - return has_unit(Maps::getTileOccupancy(pos)); - }); +inline bool can_reach_designation(const df::coord &start, const df::coord &end) { + if (!Maps::canWalkBetween(start,end)) { + df::coord neighbours[8]; + get_neighbours(end, neighbours); + for (auto &pos : neighbours) { + if (Maps::canWalkBetween(start, pos)) { + return true; + } + } + return false; + } + return true; +} + +inline bool has_unit(const df::tile_occupancy* occupancy) { + return occupancy->bits.unit || occupancy->bits.unit_grounded; } inline bool has_group_above(const ChannelGroups &groups, const df::coord &map_pos) { @@ -128,22 +162,3 @@ inline void cancel_job(df::job* job) { Job::removeJob(job); } } - -inline void get_neighbours(const df::coord &map_pos, df::coord(&neighbours)[8]) { - neighbours[0] = map_pos; - neighbours[1] = map_pos; - neighbours[2] = map_pos; - neighbours[3] = map_pos; - neighbours[4] = map_pos; - neighbours[5] = map_pos; - neighbours[6] = map_pos; - neighbours[7] = map_pos; - neighbours[0].x--; neighbours[0].y--; - neighbours[1].y--; - neighbours[2].x++; neighbours[2].y--; - neighbours[3].x--; - neighbours[4].x++; - neighbours[5].x--; neighbours[5].y++; - neighbours[6].y++; - neighbours[7].x++; neighbours[7].y++; -} diff --git a/plugins/channel-safely/include/plugin.h b/plugins/channel-safely/include/plugin.h index 71e4665c7..d165d3f18 100644 --- a/plugins/channel-safely/include/plugin.h +++ b/plugins/channel-safely/include/plugin.h @@ -14,8 +14,8 @@ struct Configuration { bool require_vision = true; bool insta_dig = false; int32_t refresh_freq = 600; - int32_t monitor_freq = 10; - uint8_t ignore_threshold = 7; + int32_t monitor_freq = 1; + uint8_t ignore_threshold = 5; uint8_t fall_threshold = 1; }; From e5dbaac85d83bad166948fb0446cf59a6e6a7a20 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 15:59:30 -0800 Subject: [PATCH 116/161] Implements plugin: channel-safely v0.5 --- plugins/channel-safely/channel-groups.cpp | 29 ++++++++------- plugins/channel-safely/channel-manager.cpp | 14 ++++++-- .../channel-safely/channel-safely-plugin.cpp | 30 +++++++++------- plugins/channel-safely/include/channel-jobs.h | 1 + plugins/channel-safely/include/inlines.h | 4 +++ plugins/channel-safely/include/tile-cache.h | 35 +++++++++++++++++++ 6 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 plugins/channel-safely/include/tile-cache.h diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 7e5cccf20..48f4a9ee5 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -91,18 +92,12 @@ void ChannelGroups::add(const df::coord &map_pos) { // puts the "add" in "ChannelGroups::add" group->emplace(map_pos); DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size()); -// ERR(groups).print("\n\n\nDEBUG MAPPINGS:\n"); -// debug_map(); -// DEBUG(groups).flush(); // we may have performed a merge, so we update all the `coord -> group index` mappings for (auto &wpos: *group) { groups_map[wpos] = group_index; } DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size()); -// ERR(groups).print("\n\n\nDEBUG MAPPINGS:\n"); -// debug_map(); -// DEBUG(groups).flush(); } // scans a single tile for channel designations @@ -120,7 +115,8 @@ void ChannelGroups::scan_one(const df::coord &map_pos) { } } } - } else if (isOpenTerrain(block->tiletype[lx][ly])) { + } else if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) { + TileCache::Get().uncache(map_pos); remove(map_pos); } } @@ -145,6 +141,9 @@ void ChannelGroups::scan() { remove(pos); } + static std::default_random_engine RNG(0); + static std::bernoulli_distribution optimizing(0.75); // fixing OpenSpace as designated + DEBUG(groups).print(" scan()\n"); // foreach block for (int32_t z = mapz - 1; z >= 0; --z) { @@ -153,7 +152,7 @@ void ChannelGroups::scan() { // the block if (df::map_block* block = Maps::getBlock(bx, by, z)) { // skip this block? - if (!block->flags.bits.designated && !group_blocks.count(block)) { + if (!block->flags.bits.designated && !group_blocks.count(block) && optimizing(RNG)) { continue; } // foreach tile @@ -162,7 +161,13 @@ void ChannelGroups::scan() { for (int16_t ly = 0; ly < 16; ++ly) { // the tile, check if it has a channel designation df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z); - if (is_dig_designation(block->designation[lx][ly])) { + if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) { + remove(map_pos); + if (jobs.count(map_pos)) { + jobs.erase(map_pos); + } + block->designation[lx][ly].bits.dig = df::tile_dig_designation::No; + } else if (is_dig_designation(block->designation[lx][ly])) { for (df::block_square_event* event: block->block_events) { if (auto evT = virtual_cast(event)) { // we want to let the user keep some designations free of being managed @@ -170,18 +175,17 @@ void ChannelGroups::scan() { if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) { if (empty_group) { group_blocks.emplace(block); + empty_group = false; } TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); add(map_pos); - empty_group = false; } } } - } else if (isOpenTerrain(block->tiletype[lx][ly])) { - remove(map_pos); } } } + // erase the block if we didn't find anything iterating through it if (empty_group) { group_blocks.erase(block); } @@ -196,6 +200,7 @@ void ChannelGroups::scan() { void ChannelGroups::clear() { debug_map(); WARN(groups).print(" <- clearing groups\n"); + group_blocks.clear(); free_spots.clear(); groups_map.clear(); for(size_t i = 0; i < groups.size(); ++i) { diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 86ce5a726..7d923e9b2 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -69,8 +70,11 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo } // if activating designation, check if it is safe to dig or not a channel designation if (!is_channel_designation(block->designation[Coord(local)]) || is_safe_to_dig_down(map_pos)) { - tile_occupancy.bits.dig_marked = marker_mode; - return marker_mode; + if (!block->flags.bits.designated) { + block->flags.bits.designated = true; + } + tile_occupancy.bits.dig_marked = false; + TileCache::Get().cache(map_pos, block->tiletype[Coord(local)]); } return false; @@ -85,7 +89,9 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo if (has_group_above(groups, map_pos) || !is_safe_to_dig_down(map_pos)) { DEBUG(manager).print(" has_groups_above: setting marker mode\n"); tile_occupancy.bits.dig_marked = true; - jobs.erase(map_pos); + if (jobs.count(map_pos)) { + jobs.erase(map_pos); + } WARN(manager).print(" <- manage_one() exits normally\n"); return true; } @@ -104,4 +110,6 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo void ChannelManager::mark_done(const df::coord &map_pos) { groups.remove(map_pos); + jobs.erase(map_pos); + TileCache::Get().uncache(map_pos); } diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index a63228e27..8c22ede44 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -53,6 +53,7 @@ Updated: Nov. 6 2022 #include #include #include +#include #include #include @@ -211,23 +212,23 @@ namespace CSP { INFO(monitor).print("JobCompletedEvent()\n"); auto job = (df::job*) job_ptr; // we only care if the job is a channeling one - if (is_dig_job(job)) { + if (ChannelManager::Get().groups.count(job->pos)) { // untrack job/worker active_workers.erase(job->id); // check job outcome + auto block = Maps::getTileBlock(job->pos); df::coord local(job->pos); - auto block = Maps::getTileBlock(local); local.x = local.x % 16; local.y = local.y % 16; + const auto &type = block->tiletype[Coord(local)]; // verify completion - if (isOpenTerrain(block->tiletype[local.x][local.y]) - || block->designation[local.x][local.y].bits.dig != df::enums::tile_dig_designation::Channel) { + if (TileCache::Get().hasChanged(job->pos, type)) { // the job can be considered done df::coord below(job->pos); below.z--; WARN(monitor).print(" -> Marking tile done and managing the group below.\n"); // mark done and manage below - Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Normal; + block->designation[Coord(local)].bits.traffic = df::tile_traffic::Normal; ChannelManager::Get().mark_done(job->pos); ChannelManager::Get().manage_group(below); ChannelManager::Get().debug(); @@ -247,16 +248,18 @@ namespace CSP { TRACE(monitor).print("OnUpdate()\n"); UnpauseEvent(); - TRACE(monitor).print(" -> evaluate dignow queue\n"); - for (const df::coord &pos: dignow_queue) { - if (!has_unit(Maps::getTileOccupancy(pos))) { - dig_now(out, pos); - } else { - // todo: teleport? - //Units::teleport() + if (config.insta_dig) { + TRACE(monitor).print(" -> evaluate dignow queue\n"); + for (const df::coord &pos: dignow_queue) { + if (!has_unit(Maps::getTileOccupancy(pos))) { + out.print("channel-safely: insta-dig: Digging now!\n"); + dig_now(out, pos); + } else { + // todo: teleport? + //Units::teleport() + } } } - TRACE(monitor).print("OnUpdate() exits\n"); } if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) { last_monitor_tick = tick; @@ -311,6 +314,7 @@ namespace CSP { } } } + TRACE(monitor).print("OnUpdate() exits\n"); } } } diff --git a/plugins/channel-safely/include/channel-jobs.h b/plugins/channel-safely/include/channel-jobs.h index d0aaead7c..0290baa19 100644 --- a/plugins/channel-safely/include/channel-jobs.h +++ b/plugins/channel-safely/include/channel-jobs.h @@ -22,6 +22,7 @@ private: public: void load_channel_jobs(); void clear(); + int count(const df::coord &map_pos) const { return jobs.count(map_pos); } Jobs::iterator erase(const df::coord &map_pos); Jobs::const_iterator find(const df::coord &map_pos) const; Jobs::const_iterator begin() const; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index e8689d245..dca17460e 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -44,6 +44,10 @@ inline bool is_channel_job(const df::job* job) { return job->job_type == df::job_type::DigChannel; } +inline bool is_group_job(const ChannelGroups &groups, const df::job* job) { + return groups.count(job->pos); +} + inline bool is_dig_designation(const df::tile_designation &designation) { return designation.bits.dig != df::tile_dig_designation::No; } diff --git a/plugins/channel-safely/include/tile-cache.h b/plugins/channel-safely/include/tile-cache.h new file mode 100644 index 000000000..ddd37c34a --- /dev/null +++ b/plugins/channel-safely/include/tile-cache.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include + +class TileCache { +private: + TileCache() = default; + std::map locations; +public: + static TileCache& Get() { + static TileCache instance; + return instance; + } + + void cache(const df::coord &pos, df::tiletype type) { + locations.emplace(pos, type); + } + + void uncache(const df::coord &pos) { + locations.erase(pos); + } + + bool hasChanged(const df::coord &pos, const df::tiletype &type) { + if (locations.count(pos)) { + if (type != locations.find(pos)->second){ + return true; + } + } + return false; + } +}; From 9db0d809fadea8617b87f4ca860a35029fb957dd Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 16:18:39 -0800 Subject: [PATCH 117/161] Implements plugin: channel-safely v0.5.1 --- .../channel-safely/channel-safely-plugin.cpp | 106 +++++++++--------- plugins/channel-safely/include/inlines.h | 2 +- 2 files changed, 55 insertions(+), 53 deletions(-) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 8c22ede44..3796bddc1 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -148,68 +148,70 @@ namespace CSP { void JobStartedEvent(color_ostream &out, void* p) { if (enabled && World::isFortressMode() && Maps::IsValid()) { - INFO(monitor).print("JobStartedEvent()\n"); - auto job = (df::job*) p; - // validate job type - if (is_channel_job(job)) { - DEBUG(monitor).print(" valid channel job:\n"); - df::unit* worker = Job::getWorker(job); - // there is a valid worker (living citizen) on the job? right.. - if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { - DEBUG(monitor).print(" valid worker:\n"); - df::coord local(job->pos); - local.x = local.x % 16; - local.y = local.y % 16; - // check pathing exists to job - if (can_reach_designation(worker->pos, job->pos)) { - DEBUG(monitor).print(" can path from (" COORD ") to (" COORD ")\n", - COORDARGS(worker->pos), COORDARGS(job->pos)); - // track workers on jobs - active_workers.emplace(job->id, Units::findIndexById(Job::getWorker(job)->id)); - // set tile to restricted - TRACE(monitor).print(" setting job tile to restricted\n"); - Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; - } else { - DEBUG(monitor).print(" no path exists to job:\n"); - // if we can't get there, then we should remove the worker and cancel the job (restore tile designation) - Job::removeWorker(job); - cancel_job(job); - if (!config.insta_dig) { - TRACE(monitor).print(" setting marker mode for (" COORD ")\n", COORDARGS(job->pos)); - // set to marker mode - auto occupancy = Maps::getTileOccupancy(job->pos); - if (!occupancy) { - WARN(monitor).print(" Could not acquire tile occupancy*\n"); - return; - } - occupancy->bits.dig_marked = true; - // prevent algorithm from re-enabling designation - df::map_block* block = Maps::getTileBlock(job->pos); - if (!block) { - WARN(monitor).print(" Could not acquire block*\n"); - return; - } - for (auto &be: block->block_events) { ; - if (auto bsedp = virtual_cast(be)) { - TRACE(monitor).print(" re-setting priority\n"); - bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; + if (config.monitor_active) { + INFO(monitor).print("JobStartedEvent()\n"); + auto job = (df::job*) p; + // validate job type + if (is_channel_job(job)) { + DEBUG(monitor).print(" valid channel job:\n"); + df::unit* worker = Job::getWorker(job); + // there is a valid worker (living citizen) on the job? right.. + if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { + DEBUG(monitor).print(" valid worker:\n"); + df::coord local(job->pos); + local.x = local.x % 16; + local.y = local.y % 16; + // check pathing exists to job + if (can_reach_designation(worker->pos, job->pos)) { + DEBUG(monitor).print(" can path from (" COORD ") to (" COORD ")\n", + COORDARGS(worker->pos), COORDARGS(job->pos)); + // track workers on jobs + active_workers.emplace(job->id, Units::findIndexById(Job::getWorker(job)->id)); + // set tile to restricted + TRACE(monitor).print(" setting job tile to restricted\n"); + Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; + } else { + DEBUG(monitor).print(" no path exists to job:\n"); + // if we can't get there, then we should remove the worker and cancel the job (restore tile designation) + Job::removeWorker(job); + cancel_job(job); + if (!config.insta_dig) { + TRACE(monitor).print(" setting marker mode for (" COORD ")\n", COORDARGS(job->pos)); + // set to marker mode + auto occupancy = Maps::getTileOccupancy(job->pos); + if (!occupancy) { + WARN(monitor).print(" Could not acquire tile occupancy*\n"); + return; + } + occupancy->bits.dig_marked = true; + // prevent algorithm from re-enabling designation + df::map_block* block = Maps::getTileBlock(job->pos); + if (!block) { + WARN(monitor).print(" Could not acquire block*\n"); + return; } + for (auto &be: block->block_events) { ; + if (auto bsedp = virtual_cast(be)) { + TRACE(monitor).print(" re-setting priority\n"); + bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; + } + } + } else { + TRACE(monitor).print(" deleting job, and queuing insta-dig)\n"); + // queue digging the job instantly + dignow_queue.emplace(job->pos); } - } else { - TRACE(monitor).print(" deleting job, and queuing insta-dig)\n"); - // queue digging the job instantly - dignow_queue.emplace(job->pos); } } } + INFO(monitor).print(" <- JobStartedEvent() exits normally\n"); } - INFO(monitor).print(" <- JobStartedEvent() exits normally\n"); } } void JobCompletedEvent(color_ostream &out, void* job_ptr) { if (enabled && World::isFortressMode() && Maps::IsValid()) { - INFO(monitor).print("JobCompletedEvent()\n"); + INFO(jobs).print("JobCompletedEvent()\n"); auto job = (df::job*) job_ptr; // we only care if the job is a channeling one if (ChannelManager::Get().groups.count(job->pos)) { @@ -226,7 +228,7 @@ namespace CSP { // the job can be considered done df::coord below(job->pos); below.z--; - WARN(monitor).print(" -> Marking tile done and managing the group below.\n"); + WARN(jobs).print(" -> Marking tile done and managing the group below.\n"); // mark done and manage below block->designation[Coord(local)].bits.traffic = df::tile_traffic::Normal; ChannelManager::Get().mark_done(job->pos); diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index dca17460e..928cc10a8 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -97,7 +97,7 @@ inline bool can_reach_designation(const df::coord &start, const df::coord &end) df::coord neighbours[8]; get_neighbours(end, neighbours); for (auto &pos : neighbours) { - if (Maps::canWalkBetween(start, pos)) { + if (Maps::isValidTilePos(pos) && Maps::canWalkBetween(start, pos)) { return true; } } From 84ffeef0925fb1dbbd1d614b166ebb5aef93177b Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 6 Nov 2022 17:02:11 -0800 Subject: [PATCH 118/161] Implements plugin: channel-safely v0.5.2 --- .../channel-safely/channel-safely-plugin.cpp | 108 ++++++------------ plugins/channel-safely/include/inlines.h | 16 +-- 2 files changed, 45 insertions(+), 79 deletions(-) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 3796bddc1..37bee4282 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -71,10 +71,10 @@ Updated: Nov. 6 2022 // Debugging namespace DFHack { - DBG_DECLARE(channelsafely, monitor, DebugCategory::LINFO); - DBG_DECLARE(channelsafely, manager, DebugCategory::LINFO); - DBG_DECLARE(channelsafely, groups, DebugCategory::LINFO); - DBG_DECLARE(channelsafely, jobs, DebugCategory::LINFO); + DBG_DECLARE(channelsafely, monitor, DebugCategory::LERROR); + DBG_DECLARE(channelsafely, manager, DebugCategory::LERROR); + DBG_DECLARE(channelsafely, groups, DebugCategory::LERROR); + DBG_DECLARE(channelsafely, jobs, DebugCategory::LERROR); } DFHACK_PLUGIN("channel-safely"); @@ -146,73 +146,34 @@ namespace CSP { INFO(monitor).print("UnpauseEvent() exits\n"); } - void JobStartedEvent(color_ostream &out, void* p) { + void JobStartedEvent(color_ostream &out, void* j) { if (enabled && World::isFortressMode() && Maps::IsValid()) { - if (config.monitor_active) { - INFO(monitor).print("JobStartedEvent()\n"); - auto job = (df::job*) p; - // validate job type - if (is_channel_job(job)) { - DEBUG(monitor).print(" valid channel job:\n"); - df::unit* worker = Job::getWorker(job); - // there is a valid worker (living citizen) on the job? right.. - if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { - DEBUG(monitor).print(" valid worker:\n"); - df::coord local(job->pos); - local.x = local.x % 16; - local.y = local.y % 16; - // check pathing exists to job - if (can_reach_designation(worker->pos, job->pos)) { - DEBUG(monitor).print(" can path from (" COORD ") to (" COORD ")\n", - COORDARGS(worker->pos), COORDARGS(job->pos)); - // track workers on jobs - active_workers.emplace(job->id, Units::findIndexById(Job::getWorker(job)->id)); - // set tile to restricted - TRACE(monitor).print(" setting job tile to restricted\n"); - Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; - } else { - DEBUG(monitor).print(" no path exists to job:\n"); - // if we can't get there, then we should remove the worker and cancel the job (restore tile designation) - Job::removeWorker(job); - cancel_job(job); - if (!config.insta_dig) { - TRACE(monitor).print(" setting marker mode for (" COORD ")\n", COORDARGS(job->pos)); - // set to marker mode - auto occupancy = Maps::getTileOccupancy(job->pos); - if (!occupancy) { - WARN(monitor).print(" Could not acquire tile occupancy*\n"); - return; - } - occupancy->bits.dig_marked = true; - // prevent algorithm from re-enabling designation - df::map_block* block = Maps::getTileBlock(job->pos); - if (!block) { - WARN(monitor).print(" Could not acquire block*\n"); - return; - } - for (auto &be: block->block_events) { ; - if (auto bsedp = virtual_cast(be)) { - TRACE(monitor).print(" re-setting priority\n"); - bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; - } - } - } else { - TRACE(monitor).print(" deleting job, and queuing insta-dig)\n"); - // queue digging the job instantly - dignow_queue.emplace(job->pos); - } - } + INFO(jobs).print("JobStartedEvent()\n"); + auto job = (df::job*) j; + // validate job type + if (is_channel_job(job)) { + DEBUG(jobs).print(" valid channel job:\n"); + df::unit* worker = Job::getWorker(job); + // there is a valid worker (living citizen) on the job? right.. + if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { + DEBUG(jobs).print(" valid worker:\n"); + // track workers on jobs + if (config.monitor_active) { + active_workers.emplace(job->id, Units::findIndexById(worker->id)); } + // set tile to restricted + TRACE(jobs).print(" setting job tile to restricted\n"); + Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; } - INFO(monitor).print(" <- JobStartedEvent() exits normally\n"); } + INFO(jobs).print(" <- JobStartedEvent() exits normally\n"); } } - void JobCompletedEvent(color_ostream &out, void* job_ptr) { + void JobCompletedEvent(color_ostream &out, void* j) { if (enabled && World::isFortressMode() && Maps::IsValid()) { INFO(jobs).print("JobCompletedEvent()\n"); - auto job = (df::job*) job_ptr; + auto job = (df::job*) j; // we only care if the job is a channeling one if (ChannelManager::Get().groups.count(job->pos)) { // untrack job/worker @@ -222,9 +183,8 @@ namespace CSP { df::coord local(job->pos); local.x = local.x % 16; local.y = local.y % 16; - const auto &type = block->tiletype[Coord(local)]; // verify completion - if (TileCache::Get().hasChanged(job->pos, type)) { + if (TileCache::Get().hasChanged(job->pos, block->tiletype[Coord(local)])) { // the job can be considered done df::coord below(job->pos); below.z--; @@ -236,10 +196,15 @@ namespace CSP { ChannelManager::Get().debug(); } } - INFO(monitor).print("JobCompletedEvent() exits\n"); + INFO(jobs).print("JobCompletedEvent() exits\n"); } } + void NewReportEvent(color_ostream &out, void* r) { + int32_t report_id = (int32_t)(intptr_t(r)); + out.print("%d\n", report_id); + } + void OnUpdate(color_ostream &out) { if (enabled && World::isFortressMode() && Maps::IsValid() && !World::ReadPauseState()) { static int32_t last_monitor_tick = df::global::world->frame_counter; @@ -247,7 +212,7 @@ namespace CSP { int32_t tick = df::global::world->frame_counter; if (tick - last_refresh_tick >= config.refresh_freq) { last_refresh_tick = tick; - TRACE(monitor).print("OnUpdate()\n"); + TRACE(monitor).print("OnUpdate() refreshing now\n"); UnpauseEvent(); if (config.insta_dig) { @@ -261,11 +226,12 @@ namespace CSP { //Units::teleport() } } + TRACE(monitor).print("OnUpdate() refresh done\n"); } } if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) { last_monitor_tick = tick; - TRACE(monitor).print("OnUpdate()\n"); + TRACE(monitor).print("OnUpdate() monitoring now\n"); for (df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next) { df::job* job = link->item; if (job) { @@ -315,8 +281,8 @@ namespace CSP { } } } + TRACE(monitor).print("OnUpdate() monitoring done\n"); } - TRACE(monitor).print("OnUpdate() exits\n"); } } } @@ -328,10 +294,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector Date: Sun, 6 Nov 2022 18:07:22 -0800 Subject: [PATCH 119/161] Implements plugin: channel-safely v0.5.3 --- plugins/channel-safely/channel-manager.cpp | 25 +++++++------------ .../channel-safely/channel-safely-plugin.cpp | 11 ++++++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 7d923e9b2..9f5c5d4b4 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -80,23 +80,16 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo } else { // next search for the designation priority - for (df::block_square_event* event: block->block_events) { - if (auto evT = virtual_cast(event)) { - // we want to let the user keep some designations free of being managed - if (evT->priority[Coord(local)] < 1000 * config.ignore_threshold) { - DEBUG(manager).print(" if(has_groups_above())\n"); - // check that the group has no incomplete groups directly above it - if (has_group_above(groups, map_pos) || !is_safe_to_dig_down(map_pos)) { - DEBUG(manager).print(" has_groups_above: setting marker mode\n"); - tile_occupancy.bits.dig_marked = true; - if (jobs.count(map_pos)) { - jobs.erase(map_pos); - } - WARN(manager).print(" <- manage_one() exits normally\n"); - return true; - } - } + DEBUG(manager).print(" if(has_groups_above())\n"); + // check that the group has no incomplete groups directly above it + if (has_group_above(groups, map_pos) || !is_safe_to_dig_down(map_pos)) { + DEBUG(manager).print(" has_groups_above: setting marker mode\n"); + tile_occupancy.bits.dig_marked = true; + if (jobs.count(map_pos)) { + jobs.erase(map_pos); } + WARN(manager).print(" <- manage_one() exits normally\n"); + return true; } } } else { diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 37bee4282..fa6864b8c 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -202,7 +202,14 @@ namespace CSP { void NewReportEvent(color_ostream &out, void* r) { int32_t report_id = (int32_t)(intptr_t(r)); - out.print("%d\n", report_id); + if (df::global::world) { + auto &reports = df::global::world->status.reports; + size_t idx = df::report::binsearch_index(reports, report_id); + if (idx >= 0 && idx < reports.size()){ + auto report = reports[report_id]; + out.print("%d\n%s\n", report_id, report->text.c_str()); + } + } } void OnUpdate(color_ostream &out) { @@ -326,9 +333,9 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { EM::EventHandler jobStartHandler(CSP::JobStartedEvent, 0); EM::EventHandler jobCompletionHandler(CSP::JobCompletedEvent, 0); EM::EventHandler reportHandler(CSP::NewReportEvent, 0); + EM::registerListener(EventType::REPORT, reportHandler, plugin_self); EM::registerListener(EventType::JOB_STARTED, jobStartHandler, plugin_self); EM::registerListener(EventType::JOB_COMPLETED, jobCompletionHandler, plugin_self); - EM::registerListener(EventType::REPORT, reportHandler, plugin_self); // manage designations to start off (first time building groups [very important]) out.print("channel-safely: enabled!\n"); CSP::UnpauseEvent(); From 92537bc4596392188863b15b38e1119bc575467b Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 7 Nov 2022 14:54:39 -0800 Subject: [PATCH 120/161] Implements plugin: channel-safely v0.6 --- docs/plugins/channel-safely.rst | 6 +- plugins/channel-safely/channel-groups.cpp | 16 +- .../channel-safely/channel-safely-plugin.cpp | 341 ++++++++++++------ plugins/channel-safely/include/inlines.h | 32 ++ plugins/channel-safely/include/plugin.h | 1 + 5 files changed, 275 insertions(+), 121 deletions(-) diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 842d57353..3dd051edb 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -47,9 +47,11 @@ Examples Features -------- -:monitor-active: Toggle whether to monitor the conditions of active digs. (default: disabled) :require-vision: Toggle whether the dwarves need vision of a tile before channeling to it can be deemed unsafe. (default: enabled) -:insta-dig: Toggle whether to use insta-digging on unreachable designations. (default: disabled) +:monitor: Toggle whether to monitor the conditions of active digs. (default: disabled) +:resurrect: Toggle whether to resurrect dwarves killed on the job. (default: disabled) +:insta-dig: Toggle whether to use insta-digging on unreachable designations. + Runs on the refresh cycles. (default: disabled) Settings -------- diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 48f4a9ee5..1a7f81a13 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -6,21 +6,7 @@ #include -template -void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { - for (const auto &a : c1) { - bool matched = false; - for (const auto &b : c2) { - if (a == b) { - matched = true; - break; - } - } - if (!matched) { - c3.emplace(a); - } - } -} + // adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found void ChannelGroups::add(const df::coord &map_pos) { diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index fa6864b8c..c74eecc34 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -60,17 +60,19 @@ Updated: Nov. 6 2022 #include #include #include +#include +#include +#include +#include +#include #include #include #include -#include -#include -#include -#include // Debugging namespace DFHack { + DBG_DECLARE(channelsafely, plugin, DebugCategory::LINFO); DBG_DECLARE(channelsafely, monitor, DebugCategory::LERROR); DBG_DECLARE(channelsafely, manager, DebugCategory::LERROR); DBG_DECLARE(channelsafely, groups, DebugCategory::LERROR); @@ -87,33 +89,25 @@ using namespace EM::EventType; int32_t mapx, mapy, mapz; Configuration config; -PersistentDataItem pconfig; -const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; +PersistentDataItem psetting; +PersistentDataItem pfeature; +const std::string FCONFIG_KEY = std::string(plugin_name) + "/feature"; +const std::string SCONFIG_KEY = std::string(plugin_name) + "/setting"; //std::unordered_set active_jobs; -#include - -enum ConfigurationData { - MONITOR, +enum FeatureConfigData { VISION, - INSTADIG, - IGNORE_THRESH, - FALL_THRESH, - REFRESH_RATE, - MONITOR_RATE + MONITOR, + RESURRECT, + INSTADIG }; -inline void saveConfig() { - if (pconfig.isValid()) { - pconfig.ival(MONITOR) = config.monitor_active; - pconfig.ival(VISION) = config.require_vision; - pconfig.ival(INSTADIG) = config.insta_dig; - pconfig.ival(REFRESH_RATE) = config.refresh_freq; - pconfig.ival(MONITOR_RATE) = config.monitor_freq; - pconfig.ival(IGNORE_THRESH) = config.ignore_threshold; - pconfig.ival(FALL_THRESH) = config.fall_threshold; - } -} +enum SettingConfigData { + REFRESH_RATE, + MONITOR_RATE, + IGNORE_THRESH, + FALL_THRESH +}; // executes dig designations for the specified tile coordinates inline bool dig_now(color_ostream &out, const df::coord &map_pos) { @@ -133,11 +127,65 @@ inline bool dig_now(color_ostream &out, const df::coord &map_pos) { } +// fully heals the unit specified, resurrecting if need be +inline void resurrect(color_ostream &out, const int32_t &unit) { + std::vector params{"-r", "--unit", std::to_string(unit)}; + Core::getInstance().runCommand(out,"full-heal", params); +} + namespace CSP { - std::unordered_map active_workers; + std::unordered_set endangered_workers; + std::unordered_map job_ids; + std::unordered_map active_jobs; + std::unordered_map active_workers; + std::unordered_map last_safe; std::unordered_set dignow_queue; + void SaveSettings() { + if (pfeature.isValid() && psetting.isValid()) { + try { + pfeature.ival(MONITOR) = config.monitor_active; + pfeature.ival(VISION) = config.require_vision; + pfeature.ival(INSTADIG) = config.insta_dig; + pfeature.ival(RESURRECT) = config.resurrect; + + psetting.ival(REFRESH_RATE) = config.refresh_freq; + psetting.ival(MONITOR_RATE) = config.monitor_freq; + psetting.ival(IGNORE_THRESH) = config.ignore_threshold; + psetting.ival(FALL_THRESH) = config.fall_threshold; + } catch (std::exception &e) { + ERR(plugin).print("%s\n", e.what()); + } + } + } + + void LoadSettings() { + pfeature = World::GetPersistentData(FCONFIG_KEY); + psetting = World::GetPersistentData(SCONFIG_KEY); + + if (!pfeature.isValid() || !psetting.isValid()) { + pfeature = World::AddPersistentData(FCONFIG_KEY); + psetting = World::AddPersistentData(SCONFIG_KEY); + SaveSettings(); + } else { + try { + config.monitor_active = pfeature.ival(MONITOR); + config.require_vision = pfeature.ival(VISION); + config.insta_dig = pfeature.ival(INSTADIG); + config.resurrect = pfeature.ival(RESURRECT); + + config.ignore_threshold = psetting.ival(IGNORE_THRESH); + config.fall_threshold = psetting.ival(FALL_THRESH); + config.refresh_freq = psetting.ival(REFRESH_RATE); + config.monitor_freq = psetting.ival(MONITOR_RATE); + } catch (std::exception &e) { + ERR(plugin).print("%s\n", e.what()); + } + } + active_workers.clear(); + } + void UnpauseEvent(){ INFO(monitor).print("UnpauseEvent()\n"); ChannelManager::Get().build_groups(); @@ -158,8 +206,10 @@ namespace CSP { if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { DEBUG(jobs).print(" valid worker:\n"); // track workers on jobs - if (config.monitor_active) { - active_workers.emplace(job->id, Units::findIndexById(worker->id)); + if (config.monitor_active || config.resurrect) { + job_ids.emplace(job, job->id); + active_jobs.emplace(job->id, job); + active_workers[job->id] = worker; } // set tile to restricted TRACE(jobs).print(" setting job tile to restricted\n"); @@ -176,8 +226,6 @@ namespace CSP { auto job = (df::job*) j; // we only care if the job is a channeling one if (ChannelManager::Get().groups.count(job->pos)) { - // untrack job/worker - active_workers.erase(job->id); // check job outcome auto block = Maps::getTileBlock(job->pos); df::coord local(job->pos); @@ -188,12 +236,22 @@ namespace CSP { // the job can be considered done df::coord below(job->pos); below.z--; - WARN(jobs).print(" -> Marking tile done and managing the group below.\n"); + WARN(jobs).print(" -> (" COORD ") is marked done, managing group below.\n", COORDARGS(job->pos)); // mark done and manage below block->designation[Coord(local)].bits.traffic = df::tile_traffic::Normal; ChannelManager::Get().mark_done(job->pos); ChannelManager::Get().manage_group(below); ChannelManager::Get().debug(); + } else { + ERR(jobs).print(" -> (" COORD ") is not done but the job \"completed\".\n", COORDARGS(job->pos)); + endangered_workers.emplace(active_workers[job->id]); + } + // clean up + if (!config.resurrect) { + auto jp = active_jobs[job->id]; + job_ids.erase(jp); + active_workers.erase(job->id); + active_jobs.erase(job->id); } } INFO(jobs).print("JobCompletedEvent() exits\n"); @@ -201,22 +259,34 @@ namespace CSP { } void NewReportEvent(color_ostream &out, void* r) { - int32_t report_id = (int32_t)(intptr_t(r)); + auto report_id = (int32_t)(intptr_t(r)); if (df::global::world) { - auto &reports = df::global::world->status.reports; - size_t idx = df::report::binsearch_index(reports, report_id); - if (idx >= 0 && idx < reports.size()){ - auto report = reports[report_id]; - out.print("%d\n%s\n", report_id, report->text.c_str()); + std::vector &reports = df::global::world->status.reports; + size_t idx = -1; + idx = df::report::binsearch_index(reports, report_id); + df::report* report = reports.at(idx); + switch (report->type) { + case announcement_type::CAVE_COLLAPSE: + for (auto p : active_workers) { + endangered_workers.emplace(p.second); + } + case announcement_type::CANCEL_JOB: + out.print("%d, pos: " COORD "\n%s\n", report_id, COORDARGS(report->pos), report->text.c_str()); + default: + break; } } } void OnUpdate(color_ostream &out) { if (enabled && World::isFortressMode() && Maps::IsValid() && !World::ReadPauseState()) { + static int32_t last_tick = df::global::world->frame_counter; static int32_t last_monitor_tick = df::global::world->frame_counter; static int32_t last_refresh_tick = df::global::world->frame_counter; + static int32_t last_resurrect_tick = df::global::world->frame_counter; int32_t tick = df::global::world->frame_counter; + + // Refreshing the group data with full scanning if (tick - last_refresh_tick >= config.refresh_freq) { last_refresh_tick = tick; TRACE(monitor).print("OnUpdate() refreshing now\n"); @@ -236,60 +306,112 @@ namespace CSP { TRACE(monitor).print("OnUpdate() refresh done\n"); } } + + // Clean up stale df::job* + if ((config.monitor_active || config.resurrect) && tick - last_tick >= 1) { + last_tick = tick; + // make note of valid jobs + std::unordered_map valid_jobs; + for (df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next) { + df::job* job = link->item; + if (job && active_jobs.count(job->id)) { + valid_jobs.emplace(job->id, job); + } + } + + // erase the active jobs that aren't valid + std::unordered_set erase; + map_value_difference(active_jobs, valid_jobs, erase); + for (auto j : erase) { + auto id = job_ids[j]; + job_ids.erase(j); + active_jobs.erase(id); + active_workers.erase(id); + } + } + + // Monitoring Active and Resurrecting Dead if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) { last_monitor_tick = tick; TRACE(monitor).print("OnUpdate() monitoring now\n"); - for (df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next) { - df::job* job = link->item; - if (job) { - auto iter = active_workers.find(job->id); - TRACE(monitor).print(" -> check for job in tracking\n"); - if (iter != active_workers.end()) { - df::unit* unit = df::global::world->units.active[iter->second]; - TRACE(monitor).print(" -> compare positions of worker and job\n"); - // check if fall is possible - if (unit->pos == job->pos) { - // can fall, is safe? - TRACE(monitor).print(" equal -> check if safe fall\n"); - if (!is_safe_fall(job->pos)) { - // unsafe - Job::removeWorker(job); - if (config.insta_dig) { - TRACE(monitor).print(" -> insta-dig\n"); - // delete the job - Job::removeJob(job); - // queue digging the job instantly - dignow_queue.emplace(job->pos); - // worker is currently in the air - Units::teleport(unit, last_safe[unit->id]); - last_safe.erase(unit->id); - } else { - TRACE(monitor).print(" -> set marker mode\n"); - // set to marker mode - Maps::getTileOccupancy(job->pos)->bits.dig_marked = true; - // prevent algorithm from re-enabling designation - for (auto &be: Maps::getBlock(job->pos)->block_events) { ; - if (auto bsedp = virtual_cast( - be)) { - df::coord local(job->pos); - local.x = local.x % 16; - local.y = local.y % 16; - bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; - break; - } - } + + // iterate active jobs + for (auto pair: active_jobs) { + df::job* job = pair.second; + df::unit* unit = active_workers[job->id]; + if (!unit) continue; + TRACE(monitor).print(" -> check for job in tracking\n"); + if (Units::isAlive(unit)) { + if (!config.monitor_active) continue; + TRACE(monitor).print(" -> compare positions of worker and job\n"); + + // save position + if (unit->pos != job->pos && isFloorTerrain(*Maps::getTileType(unit->pos))) { + // worker is perfectly safe right now + last_safe[unit->id] = unit->pos; + TRACE(monitor).print(" -> save safe position\n"); + continue; + } + + // check for fall safety + if (unit->pos == job->pos && !is_safe_fall(job->pos)) { + // unsafe + WARN(monitor).print(" -> unsafe job\n"); + Job::removeWorker(job); + + // decide to insta-dig or marker mode + if (config.insta_dig) { + // delete the job + Job::removeJob(job); + // queue digging the job instantly + dignow_queue.emplace(job->pos); + DEBUG(monitor).print(" -> insta-dig\n"); + } else if (Maps::isValidTilePos(job->pos)) { + // set marker mode + Maps::getTileOccupancy(job->pos)->bits.dig_marked = true; + + // prevent algorithm from re-enabling designation + for (auto &be: Maps::getBlock(job->pos)->block_events) { ; + if (auto bsedp = virtual_cast( + be)) { + df::coord local(job->pos); + local.x = local.x % 16; + local.y = local.y % 16; + bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1; + break; } } - } else { - TRACE(monitor).print(" -> save safe position\n"); - // worker is perfectly safe right now - last_safe[unit->id] = unit->pos; + DEBUG(monitor).print(" -> set marker mode\n"); } } } } TRACE(monitor).print("OnUpdate() monitoring done\n"); } + + if (config.resurrect && tick - last_resurrect_tick >= 1) { + last_resurrect_tick = tick; + static std::unordered_map age; + + // clean up any "endangered" workers that have been tracked 100 ticks or more + for (auto iter = age.begin(); iter != age.end();) { + if (tick - iter->second >= 1200) { //keep watch 1 day + iter = age.erase(iter); + continue; + } + ++iter; + } + + // resurrect any dead units + for (auto unit : endangered_workers) { + age.emplace(unit, tick); + if (!Units::isAlive(unit)) { + resurrect(out, unit->id); + Units::teleport(unit, last_safe[unit->id]); + WARN(plugin).print(">RESURRECTING<\n"); + } + } + } } } } @@ -310,19 +432,10 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) { } DFhackCExport command_result plugin_load_data (color_ostream &out) { - pconfig = World::GetPersistentData(CONFIG_KEY); - - if (!pconfig.isValid()) { - pconfig = World::AddPersistentData(CONFIG_KEY); - saveConfig(); - } else { - config.monitor_active = pconfig.ival(MONITOR); - config.require_vision = pconfig.ival(VISION); - config.insta_dig = pconfig.ival(INSTADIG); - config.refresh_freq = pconfig.ival(REFRESH_RATE); - config.monitor_freq = pconfig.ival(MONITOR_RATE); - config.ignore_threshold = pconfig.ival(IGNORE_THRESH); - config.fall_threshold = pconfig.ival(FALL_THRESH); + CSP::LoadSettings(); + if (enabled) { + std::vector params; + channel_safely(out, params); } return DFHack::CR_OK; } @@ -352,6 +465,7 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan if (enabled && World::isFortressMode() && Maps::IsValid()) { switch (event) { case SC_MAP_LOADED: + CSP::active_workers.clear(); // cache the map size Maps::getSize(mapx, mapy, mapz); case SC_UNPAUSED: @@ -447,12 +561,28 @@ command_result channel_safely(color_ostream &out, std::vector ¶ DBG_NAME(groups).allowed(DFHack::DebugCategory::LERROR); DBG_NAME(jobs).allowed(DFHack::DebugCategory::LERROR); } - } else if(parameters[1] == "monitor-active"){ - config.monitor_active = state; + } else if(parameters[1] == "monitor"){ + if (state != config.monitor_active) { + config.monitor_active = state; + // if this is a fresh start + if (state && !config.resurrect) { + // we need a fresh start + CSP::active_workers.clear(); + } + } } else if (parameters[1] == "require-vision") { config.require_vision = state; } else if (parameters[1] == "insta-dig") { config.insta_dig = state; + } else if (parameters[1] == "resurrect") { + if (state != config.resurrect) { + config.resurrect = state; + // if this is a fresh start + if (state && !config.monitor_active) { + // we need a fresh start + CSP::active_workers.clear(); + } + } } else if (parameters[1] == "refresh-freq" && set && parameters.size() == 3) { config.refresh_freq = std::abs(std::stol(parameters[2])); } else if (parameters[1] == "monitor-freq" && set && parameters.size() == 3) { @@ -477,14 +607,17 @@ command_result channel_safely(color_ostream &out, std::vector ¶ } } else { out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED."); - out.print("monitor-active: %s\n", config.monitor_active ? "on." : "off."); - out.print("require-vision: %s\n", config.require_vision ? "on." : "off."); - out.print("insta-dig: %s\n", config.insta_dig ? "on." : "off."); - out.print("refresh-freq: %" PRIi32 "\n", config.refresh_freq); - out.print("monitor-freq: %" PRIi32 "\n", config.monitor_freq); - out.print("ignore-threshold: %" PRIu8 "\n", config.ignore_threshold); - out.print("fall-threshold: %" PRIu8 "\n", config.fall_threshold); + out.print(" FEATURES:\n"); + out.print(" %-20s\t%s\n", "monitor-active: ", config.monitor_active ? "on." : "off."); + out.print(" %-20s\t%s\n", "require-vision: ", config.require_vision ? "on." : "off."); + out.print(" %-20s\t%s\n", "insta-dig: ", config.insta_dig ? "on." : "off."); + out.print(" %-20s\t%s\n", "resurrect: ", config.resurrect ? "on." : "off."); + out.print(" SETTINGS:\n"); + out.print(" %-20s\t%" PRIi32 "\n", "refresh-freq: ", config.refresh_freq); + out.print(" %-20s\t%" PRIi32 "\n", "monitor-freq: ", config.monitor_freq); + out.print(" %-20s\t%" PRIu8 "\n", "ignore-threshold: ", config.ignore_threshold); + out.print(" %-20s\t%" PRIu8 "\n", "fall-threshold: ", config.fall_threshold); } - saveConfig(); + CSP::SaveSettings(); return DFHack::CR_OK; } diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index 242bcb652..e22210f7a 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -168,3 +168,35 @@ inline void cancel_job(df::job* job) { Job::removeJob(job); } } + +template +void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { + for (const auto &a : c1) { + bool matched = false; + for (const auto &b : c2) { + if (a == b) { + matched = true; + break; + } + } + if (!matched) { + c3.emplace(a); + } + } +} + +template +void map_value_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { + for (const auto &a : c1) { + bool matched = false; + for (const auto &b : c2) { + if (a.second == b.second) { + matched = true; + break; + } + } + if (!matched) { + c3.emplace(a.second); + } + } +} diff --git a/plugins/channel-safely/include/plugin.h b/plugins/channel-safely/include/plugin.h index d165d3f18..2451033c5 100644 --- a/plugins/channel-safely/include/plugin.h +++ b/plugins/channel-safely/include/plugin.h @@ -13,6 +13,7 @@ struct Configuration { bool monitor_active = false; bool require_vision = true; bool insta_dig = false; + bool resurrect = false; int32_t refresh_freq = 600; int32_t monitor_freq = 1; uint8_t ignore_threshold = 5; From a9f00219272653d8bebd289cf1df0781d46493b7 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 8 Nov 2022 11:38:30 -0800 Subject: [PATCH 121/161] Implements plugin: channel-safely v0.6.1 --- .../channel-safely/channel-safely-plugin.cpp | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index c74eecc34..8c1700b69 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -243,8 +243,13 @@ namespace CSP { ChannelManager::Get().manage_group(below); ChannelManager::Get().debug(); } else { - ERR(jobs).print(" -> (" COORD ") is not done but the job \"completed\".\n", COORDARGS(job->pos)); + // the tile is unchanged + ERR(jobs).print(" -> (" COORD ") stopped working but (" COORD ") doesn't appear done.\n",COORDARGS(worker->pos), COORDARGS(job->pos)); + df::unit* worker = active_workers[job->id]; endangered_workers.emplace(active_workers[job->id]); + if (config.insta_dig) { + dignow_queue.emplace(job->pos); + } } // clean up if (!config.resurrect) { @@ -266,12 +271,18 @@ namespace CSP { idx = df::report::binsearch_index(reports, report_id); df::report* report = reports.at(idx); switch (report->type) { + case announcement_type::CANCEL_JOB: + out.print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), COORDARGS(report->pos2), report->text.c_str()); + if (report->text.find("Dangerous") != std::string::npos) { + dignow_queue.emplace(report->pos); + break; + } else if (!report->flags.bits.unconscious) { + break; + } case announcement_type::CAVE_COLLAPSE: for (auto p : active_workers) { endangered_workers.emplace(p.second); } - case announcement_type::CANCEL_JOB: - out.print("%d, pos: " COORD "\n%s\n", report_id, COORDARGS(report->pos), report->text.c_str()); default: break; } @@ -294,14 +305,17 @@ namespace CSP { if (config.insta_dig) { TRACE(monitor).print(" -> evaluate dignow queue\n"); - for (const df::coord &pos: dignow_queue) { - if (!has_unit(Maps::getTileOccupancy(pos))) { - out.print("channel-safely: insta-dig: Digging now!\n"); - dig_now(out, pos); + for (auto iter = dignow_queue.begin(); iter != dignow_queue.end();) { + if (!has_unit(Maps::getTileOccupancy(*iter))) { + dig_now(out, *iter); + iter = dignow_queue.erase(iter); + WARN(plugin).print(">INSTA-DIGGING<\n"); + continue; } else { // todo: teleport? //Units::teleport() } + ++iter; } TRACE(monitor).print("OnUpdate() refresh done\n"); } @@ -389,6 +403,7 @@ namespace CSP { TRACE(monitor).print("OnUpdate() monitoring done\n"); } + // Resurrect Dead Workers if (config.resurrect && tick - last_resurrect_tick >= 1) { last_resurrect_tick = tick; static std::unordered_map age; From 8847ed23b4c0ff4f1b7cc9e3c6a3d96f4a37f13d Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 8 Nov 2022 11:46:01 -0800 Subject: [PATCH 122/161] Implements plugin: channel-safely v0.6.1.1 --- plugins/channel-safely/channel-safely-plugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 8c1700b69..bfb99b463 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -244,9 +244,9 @@ namespace CSP { ChannelManager::Get().debug(); } else { // the tile is unchanged - ERR(jobs).print(" -> (" COORD ") stopped working but (" COORD ") doesn't appear done.\n",COORDARGS(worker->pos), COORDARGS(job->pos)); df::unit* worker = active_workers[job->id]; endangered_workers.emplace(active_workers[job->id]); + ERR(jobs).print(" -> (" COORD ") stopped working but (" COORD ") doesn't appear done.\n",COORDARGS(worker->pos), COORDARGS(job->pos)); if (config.insta_dig) { dignow_queue.emplace(job->pos); } From 3cb186a62fba3505a174bc438833dd17ec337f8c Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 8 Nov 2022 20:01:30 -0800 Subject: [PATCH 123/161] Implements plugin: channel-safely v0.6.1.2 --- plugins/channel-safely/channel-safely-plugin.cpp | 10 ++++++---- plugins/channel-safely/include/channel-manager.h | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index bfb99b463..1224413f1 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -199,13 +199,15 @@ namespace CSP { INFO(jobs).print("JobStartedEvent()\n"); auto job = (df::job*) j; // validate job type - if (is_channel_job(job)) { - DEBUG(jobs).print(" valid channel job:\n"); + if (ChannelManager::Get().exists(job->pos)) { + WARN(jobs).print(" valid channel job:\n"); df::unit* worker = Job::getWorker(job); // there is a valid worker (living citizen) on the job? right.. if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) { DEBUG(jobs).print(" valid worker:\n"); // track workers on jobs + df::coord &pos = job->pos; + WARN(jobs).print(" -> Starting job at (" COORD ")\n", COORDARGS(pos)); if (config.monitor_active || config.resurrect) { job_ids.emplace(job, job->id); active_jobs.emplace(job->id, job); @@ -225,7 +227,7 @@ namespace CSP { INFO(jobs).print("JobCompletedEvent()\n"); auto job = (df::job*) j; // we only care if the job is a channeling one - if (ChannelManager::Get().groups.count(job->pos)) { + if (ChannelManager::Get().exists(job->pos)) { // check job outcome auto block = Maps::getTileBlock(job->pos); df::coord local(job->pos); @@ -246,7 +248,7 @@ namespace CSP { // the tile is unchanged df::unit* worker = active_workers[job->id]; endangered_workers.emplace(active_workers[job->id]); - ERR(jobs).print(" -> (" COORD ") stopped working but (" COORD ") doesn't appear done.\n",COORDARGS(worker->pos), COORDARGS(job->pos)); + ERR(jobs).print("() -> job at (" COORD ") is done, but (" COORD ") doesn't appear done.\n",COORDARGS(worker->pos), COORDARGS(job->pos)); if (config.insta_dig) { dignow_queue.emplace(job->pos); } diff --git a/plugins/channel-safely/include/channel-manager.h b/plugins/channel-safely/include/channel-manager.h index 79c1e3770..d36e98ca3 100644 --- a/plugins/channel-safely/include/channel-manager.h +++ b/plugins/channel-safely/include/channel-manager.h @@ -30,6 +30,7 @@ public: void manage_group(const Group &group, bool set_marker_mode = false, bool marker_mode = false); bool manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false); void mark_done(const df::coord &map_pos); + bool exists(const df::coord &map_pos) const { return groups.count(map_pos); } void debug() { DEBUG(groups).print(" DEBUGGING GROUPS:\n"); if (config.debug) { From c4e55f1cc9f1138c6125cd8e4a71b6d434fb336c Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 10 Nov 2022 11:06:05 -0800 Subject: [PATCH 124/161] Implements plugin: channel-safely v0.6.1.3 --- plugins/channel-safely/channel-safely-plugin.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 1224413f1..acb0ba15a 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -413,6 +413,7 @@ namespace CSP { // clean up any "endangered" workers that have been tracked 100 ticks or more for (auto iter = age.begin(); iter != age.end();) { if (tick - iter->second >= 1200) { //keep watch 1 day + endangered_workers.erase(iter->first); iter = age.erase(iter); continue; } From 6c68e72295c4af7706c9c86a0d17a19677e8b118 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 10 Nov 2022 15:59:48 -0800 Subject: [PATCH 125/161] Implements plugin: channel-safely v1.0a --- docs/plugins/channel-safely.rst | 3 +- plugins/channel-safely/channel-manager.cpp | 2 + .../channel-safely/channel-safely-plugin.cpp | 206 ++++++++++++------ 3 files changed, 141 insertions(+), 70 deletions(-) diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 3dd051edb..703600f43 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -49,7 +49,8 @@ Features -------- :require-vision: Toggle whether the dwarves need vision of a tile before channeling to it can be deemed unsafe. (default: enabled) :monitor: Toggle whether to monitor the conditions of active digs. (default: disabled) -:resurrect: Toggle whether to resurrect dwarves killed on the job. (default: disabled) +:resurrect: Toggle whether to resurrect units involved in cave-ins, and if monitor is enabled + units who die while digging. (default: disabled) :insta-dig: Toggle whether to use insta-digging on unreachable designations. Runs on the refresh cycles. (default: disabled) diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 9f5c5d4b4..27d4c5153 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -2,6 +2,7 @@ #include #include +#include //hash function for df::coord #include /** @@ -104,5 +105,6 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo void ChannelManager::mark_done(const df::coord &map_pos) { groups.remove(map_pos); jobs.erase(map_pos); + CSP::dignow_queue.erase(map_pos); TileCache::Get().uncache(map_pos); } diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index acb0ba15a..b6abbf2d2 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -109,6 +109,33 @@ enum SettingConfigData { FALL_THRESH }; +// dig-now.cpp +df::coord simulate_fall(const df::coord &pos) { + df::coord resting_pos(pos); + + while (Maps::ensureTileBlock(resting_pos)) { + df::tiletype tt = *Maps::getTileType(resting_pos); + df::tiletype_shape_basic basic_shape = tileShapeBasic(tileShape(tt)); + if (isWalkable(tt) && basic_shape != df::tiletype_shape_basic::Open) + break; + --resting_pos.z; + } + + return resting_pos; +} + +df::coord simulate_area_fall(const df::coord &pos) { + df::coord neighbours[8]{}; + get_neighbours(pos, neighbours); + df::coord lowest = simulate_fall(pos); + for (auto p : neighbours) { + if (p.z < lowest.z) { + lowest = p; + } + } + return lowest; +} + // executes dig designations for the specified tile coordinates inline bool dig_now(color_ostream &out, const df::coord &map_pos) { auto L = Lua::Core::State; @@ -134,14 +161,24 @@ inline void resurrect(color_ostream &out, const int32_t &unit) { } namespace CSP { - std::unordered_set endangered_workers; - std::unordered_map job_ids; + std::unordered_map endangered_units; + std::unordered_map job_id_map; std::unordered_map active_jobs; std::unordered_map active_workers; std::unordered_map last_safe; std::unordered_set dignow_queue; + void ClearData() { + ChannelManager::Get().destroy_groups(); + dignow_queue.clear(); + last_safe.clear(); + endangered_units.clear(); + active_workers.clear(); + active_jobs.clear(); + job_id_map.clear(); + } + void SaveSettings() { if (pfeature.isValid() && psetting.isValid()) { try { @@ -209,9 +246,14 @@ namespace CSP { df::coord &pos = job->pos; WARN(jobs).print(" -> Starting job at (" COORD ")\n", COORDARGS(pos)); if (config.monitor_active || config.resurrect) { - job_ids.emplace(job, job->id); + job_id_map.emplace(job, job->id); active_jobs.emplace(job->id, job); active_workers[job->id] = worker; + if (config.resurrect) { + // this is the only place we can be 100% sure of "safety" + // (excluding deadly enemies that will have arrived) + last_safe[worker->id] = worker->pos; + } } // set tile to restricted TRACE(jobs).print(" setting job tile to restricted\n"); @@ -244,28 +286,27 @@ namespace CSP { ChannelManager::Get().mark_done(job->pos); ChannelManager::Get().manage_group(below); ChannelManager::Get().debug(); - } else { - // the tile is unchanged - df::unit* worker = active_workers[job->id]; - endangered_workers.emplace(active_workers[job->id]); - ERR(jobs).print("() -> job at (" COORD ") is done, but (" COORD ") doesn't appear done.\n",COORDARGS(worker->pos), COORDARGS(job->pos)); - if (config.insta_dig) { - dignow_queue.emplace(job->pos); + if (config.resurrect) { + // this is the only place we can be 100% sure of "safety" + // (excluding deadly enemies that will have arrived) + if (active_workers.count(job->id)) { + df::unit* worker = active_workers[job->id]; + last_safe[worker->id] = worker->pos; + } } } // clean up - if (!config.resurrect) { - auto jp = active_jobs[job->id]; - job_ids.erase(jp); - active_workers.erase(job->id); - active_jobs.erase(job->id); - } + auto jp = active_jobs[job->id]; + job_id_map.erase(jp); + active_workers.erase(job->id); + active_jobs.erase(job->id); } INFO(jobs).print("JobCompletedEvent() exits\n"); } } void NewReportEvent(color_ostream &out, void* r) { + int32_t tick = df::global::world->frame_counter; auto report_id = (int32_t)(intptr_t(r)); if (df::global::world) { std::vector &reports = df::global::world->status.reports; @@ -274,17 +315,46 @@ namespace CSP { df::report* report = reports.at(idx); switch (report->type) { case announcement_type::CANCEL_JOB: - out.print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), COORDARGS(report->pos2), report->text.c_str()); - if (report->text.find("Dangerous") != std::string::npos) { - dignow_queue.emplace(report->pos); - break; - } else if (!report->flags.bits.unconscious) { - break; + if (config.insta_dig) { + if (report->text.find("cancels Dig") != std::string::npos) { + dignow_queue.emplace(report->pos); + } else if (report->text.find("path") != std::string::npos) { + dignow_queue.emplace(report->pos); + } + DEBUG(plugin).print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), + COORDARGS(report->pos2), report->text.c_str()); } + break; case announcement_type::CAVE_COLLAPSE: - for (auto p : active_workers) { - endangered_workers.emplace(p.second); + if (config.resurrect) { + DEBUG(plugin).print("CAVE IN\n%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos), + COORDARGS(report->pos2), report->text.c_str()); + + df::coord below = report->pos; + below.z -= 1; + below = simulate_area_fall(below); + df::coord areaMin{report->pos}; + df::coord areaMax{areaMin}; + areaMin.x -= 15; + areaMin.y -= 15; + areaMax.x += 15; + areaMax.y += 15; + areaMin.z = below.z; + areaMax.z += 1; + std::vector units; + Units::getUnitsInBox(units, COORDARGS(areaMin), COORDARGS(areaMax)); + for (auto unit: units) { + endangered_units[unit] = tick; + DEBUG(plugin).print(" [id %d] was near a cave in.\n", unit->id); + } + for (auto unit : world->units.all) { + if (last_safe.count(unit->id)) { + endangered_units[unit] = tick; + DEBUG(plugin).print(" [id %d] is/was a worker, we'll track them too.\n", unit->id); + } + } } + break; default: break; } @@ -292,6 +362,9 @@ namespace CSP { } void OnUpdate(color_ostream &out) { + static auto print_res_msg = [](df::unit* unit) { + WARN(plugin).print("Channel-Safely: Resurrecting..\n [id: %d]\n", unit->id); + }; if (enabled && World::isFortressMode() && Maps::IsValid() && !World::ReadPauseState()) { static int32_t last_tick = df::global::world->frame_counter; static int32_t last_monitor_tick = df::global::world->frame_counter; @@ -303,24 +376,16 @@ namespace CSP { if (tick - last_refresh_tick >= config.refresh_freq) { last_refresh_tick = tick; TRACE(monitor).print("OnUpdate() refreshing now\n"); - UnpauseEvent(); - if (config.insta_dig) { TRACE(monitor).print(" -> evaluate dignow queue\n"); for (auto iter = dignow_queue.begin(); iter != dignow_queue.end();) { - if (!has_unit(Maps::getTileOccupancy(*iter))) { - dig_now(out, *iter); - iter = dignow_queue.erase(iter); - WARN(plugin).print(">INSTA-DIGGING<\n"); - continue; - } else { - // todo: teleport? - //Units::teleport() - } - ++iter; + dig_now(out, *iter); // teleports units to the bottom of a simulated fall + iter = dignow_queue.erase(iter); + DEBUG(plugin).print(">INSTA-DIGGING<\n"); } - TRACE(monitor).print("OnUpdate() refresh done\n"); } + UnpauseEvent(); + TRACE(monitor).print("OnUpdate() refresh done\n"); } // Clean up stale df::job* @@ -339,8 +404,8 @@ namespace CSP { std::unordered_set erase; map_value_difference(active_jobs, valid_jobs, erase); for (auto j : erase) { - auto id = job_ids[j]; - job_ids.erase(j); + auto id = job_id_map[j]; + job_id_map.erase(j); active_jobs.erase(id); active_workers.erase(id); } @@ -356,6 +421,7 @@ namespace CSP { df::job* job = pair.second; df::unit* unit = active_workers[job->id]; if (!unit) continue; + if (!Maps::isValidTilePos(job->pos)) continue; TRACE(monitor).print(" -> check for job in tracking\n"); if (Units::isAlive(unit)) { if (!config.monitor_active) continue; @@ -363,9 +429,7 @@ namespace CSP { // save position if (unit->pos != job->pos && isFloorTerrain(*Maps::getTileType(unit->pos))) { - // worker is perfectly safe right now - last_safe[unit->id] = unit->pos; - TRACE(monitor).print(" -> save safe position\n"); + // worker is probably safe right now continue; } @@ -382,7 +446,9 @@ namespace CSP { // queue digging the job instantly dignow_queue.emplace(job->pos); DEBUG(monitor).print(" -> insta-dig\n"); - } else if (Maps::isValidTilePos(job->pos)) { + } else if (config.resurrect) { + endangered_units.emplace(unit, tick); + } else { // set marker mode Maps::getTileOccupancy(job->pos)->bits.dig_marked = true; @@ -400,6 +466,13 @@ namespace CSP { DEBUG(monitor).print(" -> set marker mode\n"); } } + } else if (config.resurrect) { + resurrect(out, unit->id); + if (last_safe.count(unit->id)) { + df::coord lowest = simulate_fall(last_safe[unit->id]); + Units::teleport(unit, lowest); + } + print_res_msg(unit); } } TRACE(monitor).print("OnUpdate() monitoring done\n"); @@ -408,25 +481,27 @@ namespace CSP { // Resurrect Dead Workers if (config.resurrect && tick - last_resurrect_tick >= 1) { last_resurrect_tick = tick; - static std::unordered_map age; // clean up any "endangered" workers that have been tracked 100 ticks or more - for (auto iter = age.begin(); iter != age.end();) { + for (auto iter = endangered_units.begin(); iter != endangered_units.end();) { if (tick - iter->second >= 1200) { //keep watch 1 day - endangered_workers.erase(iter->first); - iter = age.erase(iter); + DEBUG(plugin).print("It has been one day since [id %d]'s last incident.\n", iter->first->id); + iter = endangered_units.erase(iter); continue; } ++iter; } // resurrect any dead units - for (auto unit : endangered_workers) { - age.emplace(unit, tick); + for (auto pair : endangered_units) { + auto unit = pair.first; if (!Units::isAlive(unit)) { resurrect(out, unit->id); - Units::teleport(unit, last_safe[unit->id]); - WARN(plugin).print(">RESURRECTING<\n"); + if (last_safe.count(unit->id)) { + df::coord lowest = simulate_fall(last_safe[unit->id]); + Units::teleport(unit, lowest); + } + print_res_msg(unit); } } } @@ -480,32 +555,25 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { - if (enabled && World::isFortressMode() && Maps::IsValid()) { - switch (event) { - case SC_MAP_LOADED: - CSP::active_workers.clear(); - // cache the map size - Maps::getSize(mapx, mapy, mapz); - case SC_UNPAUSED: + switch (event) { + case SC_UNPAUSED: + if (enabled && World::isFortressMode() && Maps::IsValid()) { // manage all designations on unpause CSP::UnpauseEvent(); - default: - return DFHack::CR_OK; - } - } - switch (event) { - case SC_WORLD_LOADED: - case SC_WORLD_UNLOADED: - case SC_MAP_UNLOADED: - // destroy any old group data - out.print("channel-safely: unloading data!\n"); - ChannelManager::Get().destroy_groups(); + } + break; case SC_MAP_LOADED: // cache the map size Maps::getSize(mapx, mapy, mapz); + case SC_WORLD_LOADED: + case SC_WORLD_UNLOADED: + case SC_MAP_UNLOADED: + CSP::ClearData(); + break; default: return DFHack::CR_OK; } + return DFHack::CR_OK; } DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_event event) { From 9959ef1b36a5d839d3a71c062c71de39957b1a10 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 18 Nov 2022 10:47:39 -0800 Subject: [PATCH 126/161] Implements plugin: channel-safely v1.0.1a --- docs/plugins/channel-safely.rst | 6 ++++-- plugins/channel-safely/channel-safely-plugin.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 703600f43..1fce8043d 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -18,7 +18,7 @@ Usage enable channel-safely channel-safely set channel-safely enable|disable - channel-safely run once + channel-safely runonce When enabled the map will be scanned for channel designations which will be grouped together based on adjacency and z-level. These groups will then be analyzed for safety @@ -32,7 +32,7 @@ Examples ``channel-safely`` The plugin reports its configured status. -``channel-safely run once`` +``channel-safely runonce`` Runs the safety procedures once. You can use this if you prefer initiating scans manually. ``channel-safely disable require-vision`` @@ -47,6 +47,7 @@ Examples Features -------- + :require-vision: Toggle whether the dwarves need vision of a tile before channeling to it can be deemed unsafe. (default: enabled) :monitor: Toggle whether to monitor the conditions of active digs. (default: disabled) :resurrect: Toggle whether to resurrect units involved in cave-ins, and if monitor is enabled @@ -56,6 +57,7 @@ Features Settings -------- + :refresh-freq: The rate at which full refreshes are performed. This can be expensive if you're undertaking many mega projects. (default:600, twice a day) :monitor-freq: The rate at which active jobs are monitored. (default:1) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index b6abbf2d2..d4107bc56 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -584,7 +584,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_ev command_result channel_safely(color_ostream &out, std::vector ¶meters) { if (!parameters.empty()) { if (parameters.size() >= 2 && parameters.size() <= 3) { - if (parameters[0] == "run" && parameters[1] == "once") { + if (parameters[0] == "runonce") { CSP::UnpauseEvent(); return DFHack::CR_OK; } From ec6cd8d53a7da8ca2c4035575d4356bd34429942 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 8 Nov 2022 11:42:12 -0800 Subject: [PATCH 127/161] Implements plugin: spectate v0.5 Fixes spectate not starting with the first job Updates spectate.cpp - refactors features/settings to under a `Configuration` struct with a global variable `config` - refactors existing `config` => `pconfig` - moves plugin logic, mostly, to namespace SP (spectate plugin) - utilizes debugging log macros - updates status format - refactors status print code into a separate function --- docs/plugins/spectate.rst | 2 +- plugins/spectate/spectate.cpp | 465 ++++++++++++++++++---------------- 2 files changed, 251 insertions(+), 216 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 1ca3efeb8..7e287c8be 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -45,4 +45,4 @@ Features Settings -------- :tick-threshold: Set the plugin's tick interval for changing the followed dwarf. - Acts as a maximum follow time when used with focus-jobs enabled. (default: 50) + Acts as a maximum follow time when used with focus-jobs enabled. (default: 1000) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 5f720806c..2e056cf9c 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -4,11 +4,13 @@ #include "pause.h" -#include "Core.h" -#include +#include +#include #include #include #include + +#include #include #include #include @@ -25,6 +27,11 @@ #include #include +// Debugging +namespace DFHack { + DBG_DECLARE(spectate, plugin, DebugCategory::LINFO); +} + DFHACK_PLUGIN("spectate"); DFHACK_PLUGIN_IS_ENABLED(enabled); REQUIRE_GLOBAL(world); @@ -36,14 +43,14 @@ using namespace DFHack; using namespace Pausing; using namespace df::enums; -void onTick(color_ostream& out, void* tick); -void onJobStart(color_ostream &out, void* job); -void onJobCompletion(color_ostream &out, void* job); +struct Configuration { + bool debug = false; + bool jobs_focus = false; + bool unpause = false; + bool disengage = false; + int32_t tick_threshold = 1000; +} config; -uint64_t tick_threshold = 1000; -bool focus_jobs_enabled = false; -bool disengage_enabled = false; -bool unpause_enabled = false; Pausing::AnnouncementLock* pause_lock = nullptr; bool lock_collision = false; bool announcements_disabled = false; @@ -57,9 +64,6 @@ std::set job_tracker; std::map freq; std::default_random_engine RNG; -void enable_auto_unpause(color_ostream &out, bool state); - - #define base 0.99 static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; @@ -70,18 +74,225 @@ enum ConfigData { TICK_THRESHOLD }; -static PersistentDataItem config; -inline void saveConfig() { - if (config.isValid()) { - config.ival(UNPAUSE) = unpause_enabled; - config.ival(DISENGAGE) = disengage_enabled; - config.ival(JOB_FOCUS) = focus_jobs_enabled; - config.ival(TICK_THRESHOLD) = tick_threshold; - } -} +static PersistentDataItem pconfig; +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); command_result spectate (color_ostream &out, std::vector & parameters); +namespace SP { + + void PrintStatus(color_ostream &out) { + out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); + out.print(" FEATURES:\n"); + out.print(" %-20s\t%s\n", "focus-jobs: ", config.jobs_focus ? "on." : "off."); + out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off."); + out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off."); + out.print(" SETTINGS:\n"); + out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); + } + + void SetUnpauseState(bool state) { + // we don't need to do any of this yet if the plugin isn't enabled + if (enabled) { + // todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings] + // lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete + // The onupdate function above ensure the procedure properly completes, thus we only care about + // state reversal here ergo `enabled != state` + if (lock_collision && config.unpause != state) { + WARN(plugin).print("Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own.\n"); + // if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings, + // therefore nothing to revert and the lock won't even be engaged (nothing to unlock) + lock_collision = false; + config.unpause = state; + if (config.unpause) { + // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock + pause_lock->lock(); + } + return; + } + // update the announcement settings if we can + if (state) { + if (World::SaveAnnouncementSettings()) { + World::DisableAnnouncementPausing(); + announcements_disabled = true; + pause_lock->lock(); + } else { + WARN(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); + lock_collision = true; + } + } else { + pause_lock->unlock(); + if (announcements_disabled) { + if (!World::RestoreAnnouncementSettings()) { + // this in theory shouldn't happen, if others use the lock like we do in spectate + WARN(plugin).print("Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own.\n"); + lock_collision = true; + } else { + announcements_disabled = false; + } + } + } + if (lock_collision) { + ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); + WARN(plugin).print( + " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" + " The action you were attempting will complete when the following lock or locks lift.\n"); + pause_lock->reportLocks(Core::getInstance().getConsole()); + } + } + config.unpause = state; + } + + void SaveSettings() { + if (pconfig.isValid()) { + pconfig.ival(UNPAUSE) = config.unpause; + pconfig.ival(DISENGAGE) = config.disengage; + pconfig.ival(JOB_FOCUS) = config.jobs_focus; + pconfig.ival(TICK_THRESHOLD) = config.tick_threshold; + } + } + + void LoadSettings() { + pconfig = World::GetPersistentData(CONFIG_KEY); + + if (!pconfig.isValid()) { + pconfig = World::AddPersistentData(CONFIG_KEY); + SaveSettings(); + } else { + config.unpause = pconfig.ival(UNPAUSE); + config.disengage = pconfig.ival(DISENGAGE); + config.jobs_focus = pconfig.ival(JOB_FOCUS); + config.tick_threshold = pconfig.ival(TICK_THRESHOLD); + pause_lock->unlock(); + SetUnpauseState(config.unpause); + } + } + + void Enable(color_ostream &out, bool enable) { + + } + + void onUpdate(color_ostream &out) { + // keeps announcement pause settings locked + World::Update(); // from pause.h + if (lock_collision) { + if (config.unpause) { + // player asked for auto-unpause enabled + World::SaveAnnouncementSettings(); + if (World::DisableAnnouncementPausing()) { + // now that we've got what we want, we can lock it down + lock_collision = false; + } + } else { + if (World::RestoreAnnouncementSettings()) { + lock_collision = false; + } + } + } + int failsafe = 0; + while (config.unpause && !world->status.popups.empty() && ++failsafe <= 10) { + // dismiss announcement popup(s) + Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT); + if (World::ReadPauseState()) { + // WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is + World::SetPauseState(false); + } + } + if (failsafe >= 10) { + out.printerr("spectate encountered a problem dismissing a popup!\n"); + } + if (config.disengage && !World::ReadPauseState()) { + if (our_dorf && our_dorf->id != df::global::ui->follow_unit) { + plugin_enable(out, false); + } + } + } + + // every tick check whether to decide to follow a dwarf + void TickHandler(color_ostream& out, void* ptr) { + int32_t tick = df::global::world->frame_counter; + if (our_dorf) { + if (!Units::isAlive(our_dorf)) { + following_dwarf = false; + df::global::ui->follow_unit = -1; + } + } + if (!following_dwarf || (config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) { + std::vector dwarves; + for (auto unit: df::global::world->units.active) { + if (!Units::isCitizen(unit)) { + continue; + } + dwarves.push_back(unit); + } + std::uniform_int_distribution follow_any(0, dwarves.size() - 1); + // if you're looking at a warning about a local address escaping, it means the unit* from dwarves (which aren't local) + our_dorf = dwarves[follow_any(RNG)]; + df::global::ui->follow_unit = our_dorf->id; + job_watched = our_dorf->job.current_job; + following_dwarf = true; + if (config.jobs_focus && !job_watched) { + timestamp = tick; + } + } + // todo: refactor event manager to respect tick listeners + namespace EM = EventManager; + EM::EventHandler ticking(TickHandler, config.tick_threshold); + EM::registerTick(ticking, config.tick_threshold, plugin_self); + } + + // every new worked job needs to be considered + void JobStartEvent(color_ostream& out, void* job_ptr) { + // todo: detect mood jobs + int32_t tick = df::global::world->frame_counter; + auto job = (df::job*) job_ptr; + // don't forget about it + int zcount = ++freq[job->pos.z]; + job_tracker.emplace(job->id); + // if we're not doing anything~ then let's pick something + if ((config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) { + timestamp = tick; + following_dwarf = true; + // todo: allow the user to configure b, and also revise the math + const double b = base; + double p = b * ((double) zcount / job_tracker.size()); + std::bernoulli_distribution follow_job(p); + if (!job->flags.bits.special && follow_job(RNG)) { + job_watched = job; + if (df::unit* unit = Job::getWorker(job)) { + our_dorf = unit; + df::global::ui->follow_unit = unit->id; + } + } else { + timestamp = tick; + std::vector nonworkers; + for (auto unit: df::global::world->units.active) { + if (!Units::isCitizen(unit) || unit->job.current_job) { + continue; + } + nonworkers.push_back(unit); + } + std::uniform_int_distribution<> follow_drunk(0, nonworkers.size() - 1); + df::global::ui->follow_unit = nonworkers[follow_drunk(RNG)]->id; + } + } + } + + // every job completed can be forgotten about + void JobCompletedEvent(color_ostream &out, void* job_ptr) { + auto job = (df::job*) job_ptr; + // forget about it + freq[job->pos.z]--; + freq[job->pos.z] = freq[job->pos.z] < 0 ? 0 : freq[job->pos.z]; + // the job doesn't exist, so we definitely need to get rid of that + job_tracker.erase(job->id); + // the event manager clones jobs and returns those clones for completed jobs. So the pointers won't match without a refactor of EM passing clones to both events + if (job_watched && job_watched->id == job->id) { + job_watched = nullptr; + } + } +}; + DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand("spectate", "Automated spectator mode.", @@ -97,19 +308,8 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { } DFhackCExport command_result plugin_load_data (color_ostream &out) { - config = World::GetPersistentData(CONFIG_KEY); - - if (!config.isValid()) { - config = World::AddPersistentData(CONFIG_KEY); - saveConfig(); - } else { - unpause_enabled = config.ival(UNPAUSE); - disengage_enabled = config.ival(DISENGAGE); - focus_jobs_enabled = config.ival(JOB_FOCUS); - tick_threshold = config.ival(TICK_THRESHOLD); - pause_lock->unlock(); - enable_auto_unpause(out, unpause_enabled); - } + SP::LoadSettings(); + SP::PrintStatus(out); return DFHack::CR_OK; } @@ -118,22 +318,23 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable && !enabled) { out.print("Spectate mode enabled!\n"); using namespace EM::EventType; - EM::EventHandler ticking(onTick, 15); - EM::EventHandler start(onJobStart, 0); - EM::EventHandler complete(onJobCompletion, 0); - EM::registerListener(EventType::TICK, ticking, plugin_self); + EM::EventHandler ticking(SP::TickHandler, config.tick_threshold); + EM::EventHandler start(SP::JobStartEvent, 0); + EM::EventHandler complete(SP::JobCompletedEvent, 0); + //EM::registerListener(EventType::TICK, ticking, plugin_self); + EM::registerTick(ticking, config.tick_threshold, plugin_self); EM::registerListener(EventType::JOB_STARTED, start, plugin_self); EM::registerListener(EventType::JOB_COMPLETED, complete, plugin_self); enabled = true; // enable_auto_unpause won't do anything without this set now - enable_auto_unpause(out, unpause_enabled); + SP::SetUnpauseState(config.unpause); } else if (!enable && enabled) { // warp 8, engage! out.print("Spectate mode disabled!\n"); EM::unregisterAll(plugin_self); // we need to retain whether auto-unpause is enabled, but we also need to disable its effect - bool temp = unpause_enabled; - enable_auto_unpause(out, false); - unpause_enabled = temp; + bool temp = config.unpause; + SP::SetUnpauseState(false); + config.unpause = temp; job_tracker.clear(); freq.clear(); } @@ -158,93 +359,10 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - // keeps announcement pause settings locked - World::Update(); // from pause.h - if (lock_collision) { - if (unpause_enabled) { - // player asked for auto-unpause enabled - World::SaveAnnouncementSettings(); - if (World::DisableAnnouncementPausing()) { - // now that we've got what we want, we can lock it down - lock_collision = false; - } - } else { - if (World::RestoreAnnouncementSettings()) { - lock_collision = false; - } - } - } - int failsafe = 0; - while (unpause_enabled && !world->status.popups.empty() && ++failsafe <= 10) { - // dismiss announcement popup(s) - Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT); - if (World::ReadPauseState()) { - // WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is - World::SetPauseState(false); - } - } - if (failsafe >= 10) { - out.printerr("spectate encountered a problem dismissing a popup!\n"); - } - if (disengage_enabled && !World::ReadPauseState()) { - if (our_dorf && our_dorf->id != df::global::ui->follow_unit) { - plugin_enable(out, false); - } - } + SP::onUpdate(out); return DFHack::CR_OK; } -void enable_auto_unpause(color_ostream &out, bool state) { - // we don't need to do any of this yet if the plugin isn't enabled - if (enabled) { - // todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings] - // lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete - // The onupdate function above ensure the procedure properly completes, thus we only care about - // state reversal here ergo `enabled != state` - if (lock_collision && unpause_enabled != state) { - out.print("handling collision\n"); - // if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings, - // therefore nothing to revert and the lock won't even be engaged (nothing to unlock) - lock_collision = false; - unpause_enabled = state; - if (unpause_enabled) { - // a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock - pause_lock->lock(); - } - return; - } - // update the announcement settings if we can - if (state) { - if (World::SaveAnnouncementSettings()) { - World::DisableAnnouncementPausing(); - announcements_disabled = true; - pause_lock->lock(); - } else { - out.printerr("lock collision enabling auto-unpause\n"); - lock_collision = true; - } - } else { - pause_lock->unlock(); - if (announcements_disabled) { - if (!World::RestoreAnnouncementSettings()) { - // this in theory shouldn't happen, if others use the lock like we do in spectate - out.printerr("lock collision disabling auto-unpause\n"); - lock_collision = true; - } else { - announcements_disabled = false; - } - } - } - if (lock_collision) { - out.printerr( - "auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" - "The action you were attempting will complete when the following lock or locks lift.\n"); - pause_lock->reportLocks(out); - } - } - unpause_enabled = state; -} - command_result spectate (color_ostream &out, std::vector & parameters) { if (!parameters.empty()) { if (parameters.size() >= 2 && parameters.size() <= 3) { @@ -260,14 +378,14 @@ command_result spectate (color_ostream &out, std::vector & paramet return DFHack::CR_WRONG_USAGE; } if(parameters[1] == "auto-unpause"){ - enable_auto_unpause(out, state); + SP::SetUnpauseState(state); } else if (parameters[1] == "auto-disengage") { - disengage_enabled = state; + config.disengage = state; } else if (parameters[1] == "focus-jobs") { - focus_jobs_enabled = state; + config.jobs_focus = state; } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { try { - tick_threshold = std::abs(std::stol(parameters[2])); + config.tick_threshold = std::abs(std::stol(parameters[2])); } catch (const std::exception &e) { out.printerr("%s\n", e.what()); } @@ -276,91 +394,8 @@ command_result spectate (color_ostream &out, std::vector & paramet } } } else { - out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); - out.print("tick-threshold: %" PRIu64 "\n", tick_threshold); - out.print("focus-jobs: %s\n", focus_jobs_enabled ? "on." : "off."); - out.print("auto-unpause: %s\n", unpause_enabled ? "on." : "off."); - out.print("auto-disengage: %s\n", disengage_enabled ? "on." : "off."); + SP::PrintStatus(out); } - saveConfig(); + SP::SaveSettings(); return DFHack::CR_OK; } - -// every tick check whether to decide to follow a dwarf -void onTick(color_ostream& out, void* ptr) { - int32_t tick = df::global::world->frame_counter; - if (our_dorf) { - if (!Units::isAlive(our_dorf)) { - following_dwarf = false; - df::global::ui->follow_unit = -1; - } - } - if (!following_dwarf || (focus_jobs_enabled && !job_watched) || (tick - timestamp) > (int32_t) tick_threshold) { - std::vector dwarves; - for (auto unit: df::global::world->units.active) { - if (!Units::isCitizen(unit)) { - continue; - } - dwarves.push_back(unit); - } - std::uniform_int_distribution follow_any(0, dwarves.size() - 1); - // if you're looking at a warning about a local address escaping, it means the unit* from dwarves (which aren't local) - our_dorf = dwarves[follow_any(RNG)]; - df::global::ui->follow_unit = our_dorf->id; - job_watched = our_dorf->job.current_job; - following_dwarf = true; - if (!job_watched) { - timestamp = tick; - } - } -} - -// every new worked job needs to be considered -void onJobStart(color_ostream& out, void* job_ptr) { - // todo: detect mood jobs - int32_t tick = df::global::world->frame_counter; - auto job = (df::job*) job_ptr; - // don't forget about it - int zcount = ++freq[job->pos.z]; - job_tracker.emplace(job->id); - // if we're not doing anything~ then let's pick something - if ((focus_jobs_enabled && !job_watched) || (tick - timestamp) > (int32_t) tick_threshold) { - following_dwarf = true; - // todo: allow the user to configure b, and also revise the math - const double b = base; - double p = b * ((double) zcount / job_tracker.size()); - std::bernoulli_distribution follow_job(p); - if (!job->flags.bits.special && follow_job(RNG)) { - job_watched = job; - if (df::unit* unit = Job::getWorker(job)) { - our_dorf = unit; - df::global::ui->follow_unit = unit->id; - } - } else { - timestamp = tick; - std::vector nonworkers; - for (auto unit: df::global::world->units.active) { - if (!Units::isCitizen(unit) || unit->job.current_job) { - continue; - } - nonworkers.push_back(unit); - } - std::uniform_int_distribution<> follow_drunk(0, nonworkers.size() - 1); - df::global::ui->follow_unit = nonworkers[follow_drunk(RNG)]->id; - } - } -} - -// every job completed can be forgotten about -void onJobCompletion(color_ostream &out, void* job_ptr) { - auto job = (df::job*) job_ptr; - // forget about it - freq[job->pos.z]--; - freq[job->pos.z] = freq[job->pos.z] < 0 ? 0 : freq[job->pos.z]; - // the job doesn't exist, so we definitely need to get rid of that - job_tracker.erase(job->id); - // the event manager clones jobs and returns those clones for completed jobs. So the pointers won't match without a refactor of EM passing clones to both events - if (job_watched && job_watched->id == job->id) { - job_watched = nullptr; - } -} From 40cbe4fe88c8430ab446d740613c73d255239295 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 15:49:24 -0800 Subject: [PATCH 128/161] Implements plugin: spectate v1.0a --- docs/changelog.txt | 4 +- docs/plugins/spectate.rst | 4 +- plugins/spectate/spectate.cpp | 284 +++++++++++++++++++--------------- 3 files changed, 168 insertions(+), 124 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 206d64879..1546c6c14 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -68,8 +68,10 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `orders`: replace shell craft orders with orders for shell leggings. they have a slightly higher trade price, and "shleggings" is hilarious. - `spectate`: new ``auto-unpause`` option for auto-dismissal of announcement pause events (e.g. sieges). - `spectate`: new ``auto-disengage`` option for auto-disengagement of plugin through player interaction whilst unpaused. -- `spectate`: new ``focus-jobs`` option for following a dwarf after their job has finished (when disabled). - `spectate`: new ``tick-threshold``, option for specifying the change interval (maximum follow time when focus-jobs is enabled) +- `spectate`: new ``animals``, option for sometimes following animals +- `spectate`: new ``hostiles``, option for sometimes following hostiles +- `spectate`: new ``visiting``, option for sometimes following visiting merchants, diplomats or plain visitors - `spectate`: added persistent configuration of the plugin settings - `gui/cp437-table`: new global keybinding for the clickable on-screen keyboard for players with keyboard layouts that prevent them from using certain keys: Ctrl-Shift-K - `quickfort-library-guide`: dreamfort blueprint improvements: added a quantum stockpile for training bolts diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 7e287c8be..8bfff4f03 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -38,9 +38,11 @@ Examples Features -------- -:focus-jobs: Toggle whether the plugin should always be following a job. (default: disabled) :auto-unpause: Toggle auto-dismissal of game pause events. (default: disabled) :auto-disengage: Toggle auto-disengagement of plugin through player intervention while unpaused. (default: disabled) +:animals: Toggle whether to sometimes follow animals. (default: disabled) +:hostiles: Toggle whether to sometimes follow hostiles (eg. undead, titan, invader, etc.) (default: disabled) +:visiting: Toggle whether to sometimes follow visiting units (eg. diplomat) Settings -------- diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 2e056cf9c..e308ddd19 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -10,9 +10,10 @@ #include #include -#include -#include #include +#include +#include +#include #include #include #include @@ -21,11 +22,13 @@ #include #include #include +#include #include #include #include #include +#include // Debugging namespace DFHack { @@ -45,9 +48,12 @@ using namespace df::enums; struct Configuration { bool debug = false; - bool jobs_focus = false; + bool unpause = false; bool disengage = false; + bool animals = false; + bool hostiles = true; + bool visitors = false; int32_t tick_threshold = 1000; } config; @@ -55,40 +61,43 @@ Pausing::AnnouncementLock* pause_lock = nullptr; bool lock_collision = false; bool announcements_disabled = false; -bool following_dwarf = false; -df::unit* our_dorf = nullptr; -df::job* job_watched = nullptr; -int32_t timestamp = -1; - -std::set job_tracker; -std::map freq; -std::default_random_engine RNG; - #define base 0.99 static const std::string CONFIG_KEY = std::string(plugin_name) + "/config"; enum ConfigData { UNPAUSE, DISENGAGE, - JOB_FOCUS, - TICK_THRESHOLD + TICK_THRESHOLD, + ANIMALS, + HOSTILES, + VISITORS + }; static PersistentDataItem pconfig; DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); command_result spectate (color_ostream &out, std::vector & parameters); +#define COORDARGS(id) id.x, (id).y, id.z namespace SP { + bool following_dwarf = false; + df::unit* our_dorf = nullptr; + int32_t timestamp = -1; + std::default_random_engine RNG; void PrintStatus(color_ostream &out) { out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "focus-jobs: ", config.jobs_focus ? "on." : "off."); out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off."); out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off."); + out.print(" %-20s\t%s\n", "animals: ", config.animals ? "on." : "off."); + out.print(" %-20s\t%s\n", "hostiles: ", config.hostiles ? "on." : "off."); + out.print(" %-20s\t%s\n", "visiting: ", config.visitors ? "on." : "off."); out.print(" SETTINGS:\n"); out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold); + if (following_dwarf) + out.print(" %-21s\t%s[id: %d]\n","FOLLOWING:", our_dorf ? our_dorf->name.first_name.c_str() : "nullptr", df::global::ui->follow_unit); } void SetUnpauseState(bool state) { @@ -135,8 +144,8 @@ namespace SP { if (lock_collision) { ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n"); WARN(plugin).print( - " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" - " The action you were attempting will complete when the following lock or locks lift.\n"); + " auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n" + " The action you were attempting will complete when the following lock or locks lift.\n"); pause_lock->reportLocks(Core::getInstance().getConsole()); } } @@ -147,8 +156,10 @@ namespace SP { if (pconfig.isValid()) { pconfig.ival(UNPAUSE) = config.unpause; pconfig.ival(DISENGAGE) = config.disengage; - pconfig.ival(JOB_FOCUS) = config.jobs_focus; pconfig.ival(TICK_THRESHOLD) = config.tick_threshold; + pconfig.ival(ANIMALS) = config.animals; + pconfig.ival(HOSTILES) = config.hostiles; + pconfig.ival(VISITORS) = config.visitors; } } @@ -161,20 +172,128 @@ namespace SP { } else { config.unpause = pconfig.ival(UNPAUSE); config.disengage = pconfig.ival(DISENGAGE); - config.jobs_focus = pconfig.ival(JOB_FOCUS); config.tick_threshold = pconfig.ival(TICK_THRESHOLD); + config.animals = pconfig.ival(ANIMALS); + config.hostiles = pconfig.ival(HOSTILES); + config.visitors = pconfig.ival(VISITORS); pause_lock->unlock(); SetUnpauseState(config.unpause); } } - void Enable(color_ostream &out, bool enable) { - + bool FollowADwarf() { + if (enabled && !World::ReadPauseState()) { + df::coord viewMin = Gui::getViewportPos(); + df::coord viewMax{viewMin}; + const auto &dims = Gui::getDwarfmodeViewDims().map().second; + viewMax.x += dims.x - 1; + viewMax.y += dims.y - 1; + viewMax.z = viewMin.z; + std::vector units; + static auto add_if = [&](std::function check) { + for (auto unit : world->units.active) { + if (check(unit)) { + units.push_back(unit); + } + } + }; + static auto valid = [](df::unit* unit) { + if (Units::isAnimal(unit)) { + return config.animals; + } + if (Units::isVisiting(unit)) { + return config.visitors; + } + if (Units::isDanger(unit)) { + return config.hostiles; + } + return true; + }; + /// RANGE 1 (in view) + // grab all valid units + add_if(valid); + // keep only those in the box + Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); + int32_t inview_idx2 = units.size()-1; + bool range1_exists = inview_idx2 >= 0; + int32_t inview_idx1 = range1_exists ? 0 : -1; + + /// RANGE 2 (citizens) + add_if([](df::unit* unit) { + return valid(unit) && Units::isCitizen(unit, true); + }); + int32_t cit_idx2 = units.size()-1; + bool range2_exists = cit_idx2 > inview_idx2; + int32_t cit_idx1 = range2_exists ? inview_idx2+1 : cit_idx2; + + /// RANGE 3 (any valid) + add_if(valid); + int32_t all_idx2 = units.size()-1; + bool range3_exists = all_idx2 > cit_idx2; + int32_t all_idx1 = range3_exists ? cit_idx2+1 : all_idx2; + + + if (!units.empty()) { + std::vector i; + std::vector w; + if (!range1_exists && !range2_exists && !range3_exists) { + return false; + } + if (range1_exists) { + if (inview_idx1 == inview_idx2) { + i.push_back(0); + w.push_back(17); + } else { + i.push_back(inview_idx1); + i.push_back(inview_idx2); + w.push_back(inview_idx2 + 1); + w.push_back(inview_idx2 + 1); + } + } + if (range2_exists) { + if (cit_idx1 == cit_idx2) { + i.push_back(cit_idx1); + w.push_back(7); + } else { + i.push_back(cit_idx1); + i.push_back(cit_idx2); + w.push_back(7); + w.push_back(7); + } + } + if (range3_exists) { + if (all_idx1 == all_idx2) { + i.push_back(all_idx1); + w.push_back(1); + } else { + i.push_back(all_idx1); + i.push_back(all_idx2); + w.push_back(1); + w.push_back(1); + } + } + std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); + // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) + size_t idx = follow_any(RNG); + our_dorf = units[idx]; + df::global::ui->follow_unit = our_dorf->id; + timestamp = df::global::world->frame_counter; + return true; + } else { + WARN(plugin).print("units vector is empty!\n"); + } + } + return false; } void onUpdate(color_ostream &out) { + if (!World::isFortressMode() || !Maps::IsValid()) + return; + // keeps announcement pause settings locked World::Update(); // from pause.h + + // Plugin Management if (lock_collision) { if (config.unpause) { // player asked for auto-unpause enabled @@ -201,95 +320,24 @@ namespace SP { if (failsafe >= 10) { out.printerr("spectate encountered a problem dismissing a popup!\n"); } - if (config.disengage && !World::ReadPauseState()) { - if (our_dorf && our_dorf->id != df::global::ui->follow_unit) { - plugin_enable(out, false); - } - } - } - // every tick check whether to decide to follow a dwarf - void TickHandler(color_ostream& out, void* ptr) { - int32_t tick = df::global::world->frame_counter; - if (our_dorf) { - if (!Units::isAlive(our_dorf)) { + // plugin logic + static int32_t last_tick = -1; + int32_t tick = world->frame_counter; + if (!World::ReadPauseState() && tick - last_tick >= 1) { + last_tick = tick; + // validate follow state + if (!following_dwarf || !our_dorf || df::global::ui->follow_unit < 0) { + // we're not following anyone following_dwarf = false; - df::global::ui->follow_unit = -1; - } - } - if (!following_dwarf || (config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) { - std::vector dwarves; - for (auto unit: df::global::world->units.active) { - if (!Units::isCitizen(unit)) { - continue; + if (!config.disengage) { + // try to + following_dwarf = FollowADwarf(); + } else if (!World::ReadPauseState()) { + plugin_enable(out, false); } - dwarves.push_back(unit); - } - std::uniform_int_distribution follow_any(0, dwarves.size() - 1); - // if you're looking at a warning about a local address escaping, it means the unit* from dwarves (which aren't local) - our_dorf = dwarves[follow_any(RNG)]; - df::global::ui->follow_unit = our_dorf->id; - job_watched = our_dorf->job.current_job; - following_dwarf = true; - if (config.jobs_focus && !job_watched) { - timestamp = tick; } } - // todo: refactor event manager to respect tick listeners - namespace EM = EventManager; - EM::EventHandler ticking(TickHandler, config.tick_threshold); - EM::registerTick(ticking, config.tick_threshold, plugin_self); - } - - // every new worked job needs to be considered - void JobStartEvent(color_ostream& out, void* job_ptr) { - // todo: detect mood jobs - int32_t tick = df::global::world->frame_counter; - auto job = (df::job*) job_ptr; - // don't forget about it - int zcount = ++freq[job->pos.z]; - job_tracker.emplace(job->id); - // if we're not doing anything~ then let's pick something - if ((config.jobs_focus && !job_watched) || timestamp == -1 || (tick - timestamp) > config.tick_threshold) { - timestamp = tick; - following_dwarf = true; - // todo: allow the user to configure b, and also revise the math - const double b = base; - double p = b * ((double) zcount / job_tracker.size()); - std::bernoulli_distribution follow_job(p); - if (!job->flags.bits.special && follow_job(RNG)) { - job_watched = job; - if (df::unit* unit = Job::getWorker(job)) { - our_dorf = unit; - df::global::ui->follow_unit = unit->id; - } - } else { - timestamp = tick; - std::vector nonworkers; - for (auto unit: df::global::world->units.active) { - if (!Units::isCitizen(unit) || unit->job.current_job) { - continue; - } - nonworkers.push_back(unit); - } - std::uniform_int_distribution<> follow_drunk(0, nonworkers.size() - 1); - df::global::ui->follow_unit = nonworkers[follow_drunk(RNG)]->id; - } - } - } - - // every job completed can be forgotten about - void JobCompletedEvent(color_ostream &out, void* job_ptr) { - auto job = (df::job*) job_ptr; - // forget about it - freq[job->pos.z]--; - freq[job->pos.z] = freq[job->pos.z] < 0 ? 0 : freq[job->pos.z]; - // the job doesn't exist, so we definitely need to get rid of that - job_tracker.erase(job->id); - // the event manager clones jobs and returns those clones for completed jobs. So the pointers won't match without a refactor of EM passing clones to both events - if (job_watched && job_watched->id == job->id) { - job_watched = nullptr; - } } }; @@ -309,34 +357,23 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) { DFhackCExport command_result plugin_load_data (color_ostream &out) { SP::LoadSettings(); + SP::following_dwarf = SP::FollowADwarf(); SP::PrintStatus(out); return DFHack::CR_OK; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { - namespace EM = EventManager; if (enable && !enabled) { out.print("Spectate mode enabled!\n"); - using namespace EM::EventType; - EM::EventHandler ticking(SP::TickHandler, config.tick_threshold); - EM::EventHandler start(SP::JobStartEvent, 0); - EM::EventHandler complete(SP::JobCompletedEvent, 0); - //EM::registerListener(EventType::TICK, ticking, plugin_self); - EM::registerTick(ticking, config.tick_threshold, plugin_self); - EM::registerListener(EventType::JOB_STARTED, start, plugin_self); - EM::registerListener(EventType::JOB_COMPLETED, complete, plugin_self); enabled = true; // enable_auto_unpause won't do anything without this set now SP::SetUnpauseState(config.unpause); } else if (!enable && enabled) { // warp 8, engage! out.print("Spectate mode disabled!\n"); - EM::unregisterAll(plugin_self); // we need to retain whether auto-unpause is enabled, but we also need to disable its effect bool temp = config.unpause; SP::SetUnpauseState(false); config.unpause = temp; - job_tracker.clear(); - freq.clear(); } enabled = enable; return DFHack::CR_OK; @@ -348,9 +385,8 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan case SC_MAP_UNLOADED: case SC_BEGIN_UNLOAD: case SC_WORLD_UNLOADED: - our_dorf = nullptr; - job_watched = nullptr; - following_dwarf = false; + SP::our_dorf = nullptr; + SP::following_dwarf = false; default: break; } @@ -381,8 +417,12 @@ command_result spectate (color_ostream &out, std::vector & paramet SP::SetUnpauseState(state); } else if (parameters[1] == "auto-disengage") { config.disengage = state; - } else if (parameters[1] == "focus-jobs") { - config.jobs_focus = state; + } else if (parameters[1] == "animals") { + config.animals = state; + } else if (parameters[1] == "hostiles") { + config.hostiles = state; + } else if (parameters[1] == "visiting") { + config.visitors = state; } else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) { try { config.tick_threshold = std::abs(std::stol(parameters[2])); From b99e948b8aac74772f6963261d63e4cdafabf651 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 9 Nov 2022 21:10:18 -0800 Subject: [PATCH 129/161] Implements plugin: spectate v1.0.1a --- docs/plugins/spectate.rst | 3 +- plugins/spectate/spectate.cpp | 122 ++++++++++++++++++++-------------- 2 files changed, 74 insertions(+), 51 deletions(-) diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 8bfff4f03..f54d68142 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -46,5 +46,4 @@ Features Settings -------- -:tick-threshold: Set the plugin's tick interval for changing the followed dwarf. - Acts as a maximum follow time when used with focus-jobs enabled. (default: 1000) +:tick-threshold: Set the plugin's tick interval for changing the followed dwarf. (default: 1000) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index e308ddd19..9ad4a1f4e 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -78,7 +79,7 @@ static PersistentDataItem pconfig; DFhackCExport command_result plugin_enable(color_ostream &out, bool enable); command_result spectate (color_ostream &out, std::vector & parameters); -#define COORDARGS(id) id.x, (id).y, id.z +#define COORDARGS(id) id.x, id.y, id.z namespace SP { bool following_dwarf = false; @@ -209,68 +210,91 @@ namespace SP { } return true; }; + static auto calc_extra_weight = [](size_t idx, double r1, double r2) { + switch(idx) { + case 0: + return r2; + case 1: + return (r2-r1)/1.3; + case 2: + return (r2-r1)/2; + default: + return 0.0; + } + }; + /// Collecting our choice pool + /////////////////////////////// + std::array ranges{}; + std::array range_exists{}; + static auto build_range = [&](size_t idx){ + size_t first = idx * 2; + size_t second = idx * 2 + 1; + size_t previous = first - 1; + // first we get the end of the range + ranges[second] = units.size() - 1; + // then we calculate whether this indicates there is a range or not + if (first != 0) { + range_exists[idx] = ranges[second] > ranges[previous]; + } else { + range_exists[idx] = ranges[second] >= 0; + } + // lastly we set the start of the range + ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); + }; + + /// RANGE 0 (in view + working) + // grab valid working units + add_if([](df::unit* unit) { + return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; + }); + // keep only those in the box + Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); + build_range(0); + /// RANGE 1 (in view) - // grab all valid units add_if(valid); - // keep only those in the box Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); - int32_t inview_idx2 = units.size()-1; - bool range1_exists = inview_idx2 >= 0; - int32_t inview_idx1 = range1_exists ? 0 : -1; + build_range(1); - /// RANGE 2 (citizens) + /// RANGE 2 (working citizens) + add_if([](df::unit* unit) { + return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; + }); + build_range(2); + + /// RANGE 3 (citizens) add_if([](df::unit* unit) { return valid(unit) && Units::isCitizen(unit, true); }); - int32_t cit_idx2 = units.size()-1; - bool range2_exists = cit_idx2 > inview_idx2; - int32_t cit_idx1 = range2_exists ? inview_idx2+1 : cit_idx2; + build_range(3); - /// RANGE 3 (any valid) + /// RANGE 4 (any valid) add_if(valid); - int32_t all_idx2 = units.size()-1; - bool range3_exists = all_idx2 > cit_idx2; - int32_t all_idx1 = range3_exists ? cit_idx2+1 : all_idx2; - + build_range(4); + // selecting from our choice pool if (!units.empty()) { + std::array bw{23,17,13,7,1}; // probability weights for each range std::vector i; std::vector w; - if (!range1_exists && !range2_exists && !range3_exists) { - return false; - } - if (range1_exists) { - if (inview_idx1 == inview_idx2) { - i.push_back(0); - w.push_back(17); - } else { - i.push_back(inview_idx1); - i.push_back(inview_idx2); - w.push_back(inview_idx2 + 1); - w.push_back(inview_idx2 + 1); + bool at_least_one = false; + // in one word, elegance + for(size_t idx = 0; idx < range_exists.size(); ++idx) { + if (range_exists[idx]) { + at_least_one = true; + const auto &r1 = ranges[idx*2]; + const auto &r2 = ranges[idx*2+1]; + double extra = calc_extra_weight(idx, r1, r2); + i.push_back(r1); + w.push_back(bw[idx] + extra); + if (r1 != r2) { + i.push_back(r2); + w.push_back(bw[idx] + extra); + } } } - if (range2_exists) { - if (cit_idx1 == cit_idx2) { - i.push_back(cit_idx1); - w.push_back(7); - } else { - i.push_back(cit_idx1); - i.push_back(cit_idx2); - w.push_back(7); - w.push_back(7); - } - } - if (range3_exists) { - if (all_idx1 == all_idx2) { - i.push_back(all_idx1); - w.push_back(1); - } else { - i.push_back(all_idx1); - i.push_back(all_idx2); - w.push_back(1); - w.push_back(1); - } + if (!at_least_one) { + return false; } std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) @@ -327,7 +351,7 @@ namespace SP { if (!World::ReadPauseState() && tick - last_tick >= 1) { last_tick = tick; // validate follow state - if (!following_dwarf || !our_dorf || df::global::ui->follow_unit < 0) { + if (!following_dwarf || !our_dorf || df::global::ui->follow_unit < 0 || tick - timestamp >= config.tick_threshold) { // we're not following anyone following_dwarf = false; if (!config.disengage) { From 4a0abd19151ffa52885c19f7b41b54c732d6bb3c Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 17 Nov 2022 11:17:13 -0800 Subject: [PATCH 130/161] Implements plugin: spectate v1.0.2b --- plugins/spectate/spectate.cpp | 41 ++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 9ad4a1f4e..411316775 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -1,12 +1,12 @@ // // Created by josh on 7/28/21. +// Last updated: 11//10/22 // #include "pause.h" #include #include -#include #include #include @@ -24,8 +24,6 @@ #include #include -#include -#include #include #include #include @@ -87,6 +85,16 @@ namespace SP { int32_t timestamp = -1; std::default_random_engine RNG; + void DebugUnitVector(std::vector units) { + for (auto unit : units) { + INFO(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", + unit->id, + Units::isAnimal(unit), + Units::isDanger(unit), + Units::isVisiting(unit)); + } + } + void PrintStatus(color_ostream &out) { out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED."); out.print(" FEATURES:\n"); @@ -232,28 +240,30 @@ namespace SP { size_t previous = first - 1; // first we get the end of the range ranges[second] = units.size() - 1; - // then we calculate whether this indicates there is a range or not - if (first != 0) { - range_exists[idx] = ranges[second] > ranges[previous]; - } else { + // then we calculate whether the range exists, and set the first index appropriately + if (idx == 0) { range_exists[idx] = ranges[second] >= 0; + ranges[first] = 0; + } else { + range_exists[idx] = ranges[second] > ranges[previous]; + ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); } - // lastly we set the start of the range - ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0); }; /// RANGE 0 (in view + working) // grab valid working units - add_if([](df::unit* unit) { - return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job; + add_if([&](df::unit* unit) { + return valid(unit) && + Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)) && + Units::isCitizen(unit, true) && + unit->job.current_job; }); - // keep only those in the box - Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); build_range(0); /// RANGE 1 (in view) - add_if(valid); - Units::getUnitsInBox(units, COORDARGS(viewMin), COORDARGS(viewMax)); + add_if([&](df::unit* unit) { + return valid(unit) && Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)); + }); build_range(1); /// RANGE 2 (working citizens) @@ -296,6 +306,7 @@ namespace SP { if (!at_least_one) { return false; } + //DebugUnitVector(units); std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) size_t idx = follow_any(RNG); From 5352649b884b42538f3b290c762884f982a43b4c Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 21 Nov 2022 12:13:11 -0800 Subject: [PATCH 131/161] Implements plugin: spectate v1.0.3b test --- plugins/spectate/spectate.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 411316775..f6a5ad8af 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -46,8 +46,6 @@ using namespace Pausing; using namespace df::enums; struct Configuration { - bool debug = false; - bool unpause = false; bool disengage = false; bool animals = false; @@ -86,12 +84,14 @@ namespace SP { std::default_random_engine RNG; void DebugUnitVector(std::vector units) { - for (auto unit : units) { - INFO(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", - unit->id, - Units::isAnimal(unit), - Units::isDanger(unit), - Units::isVisiting(unit)); + if (debug_plugin.isEnabled(DFHack::DebugCategory::LDEBUG)) { + for (auto unit: units) { + DEBUG(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n", + unit->id, + Units::isAnimal(unit), + Units::isDanger(unit), + Units::isVisiting(unit)); + } } } @@ -306,7 +306,7 @@ namespace SP { if (!at_least_one) { return false; } - //DebugUnitVector(units); + DebugUnitVector(units); std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin()); // if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local) size_t idx = follow_any(RNG); From 8a0999ffdc564b3d45e2628a8df9e37b9e7824ca Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 21 Nov 2022 12:39:26 -0800 Subject: [PATCH 132/161] Implements plugin: channel-safely v1.1a --- docs/plugins/channel-safely.rst | 15 ++- plugins/channel-safely/CMakeLists.txt | 1 - plugins/channel-safely/channel-groups.cpp | 40 +++++--- plugins/channel-safely/channel-jobs.cpp | 46 ---------- plugins/channel-safely/channel-manager.cpp | 5 - .../channel-safely/channel-safely-plugin.cpp | 91 +++++-------------- .../channel-safely/include/channel-groups.h | 11 ++- plugins/channel-safely/include/channel-jobs.h | 31 +++++-- .../channel-safely/include/channel-manager.h | 6 +- plugins/channel-safely/include/inlines.h | 20 ++-- plugins/channel-safely/include/plugin.h | 1 - plugins/channel-safely/include/tile-cache.h | 12 +-- 12 files changed, 105 insertions(+), 174 deletions(-) delete mode 100644 plugins/channel-safely/channel-jobs.cpp diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 1fce8043d..4106aee04 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -2,12 +2,12 @@ channel-safely ============== .. dfhack-tool:: - :summary: Auto-manage channel designations to keep dwarves safe + :summary: Auto-manage channel designations to keep dwarves safe. :tags: fort auto Multi-level channel projects can be dangerous, and managing the safety of your dwarves throughout the completion of such projects can be difficult and time -consuming. This plugin keeps your dwarves safe (while channeling) so you don't +consuming. This plugin keeps your dwarves safe (at least while channeling) so you don't have to. Now you can focus on designing your dwarven cities with the deep chasms they were meant to have. @@ -18,7 +18,7 @@ Usage enable channel-safely channel-safely set channel-safely enable|disable - channel-safely runonce + channel-safely When enabled the map will be scanned for channel designations which will be grouped together based on adjacency and z-level. These groups will then be analyzed for safety @@ -38,13 +38,20 @@ Examples ``channel-safely disable require-vision`` Allows the plugin to read all tiles, including the ones your dwarves know nothing about. -``channel-safely enable monitor-active`` +``channel-safely enable monitor`` Enables monitoring active channel digging jobs. Meaning that if another unit it present or the tile below becomes open space the job will be paused or canceled (respectively). ``channel-safely set ignore-threshold 3`` Configures the plugin to ignore designations equal to or above priority 3 designations. +Commands +-------- + +:runonce: Run the safety procedures once to set the marker mode of designations. +:rebuild: Rebuild the designation group data. Intended for to be used in the event + the marker mode isn't being set correctly (mostly for debugging). + Features -------- diff --git a/plugins/channel-safely/CMakeLists.txt b/plugins/channel-safely/CMakeLists.txt index d660d2262..36c7307e4 100644 --- a/plugins/channel-safely/CMakeLists.txt +++ b/plugins/channel-safely/CMakeLists.txt @@ -2,7 +2,6 @@ project(channel-safely) include_directories(include) SET(SOURCES - channel-jobs.cpp channel-groups.cpp channel-manager.cpp channel-safely-plugin.cpp) diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 1a7f81a13..52f7e6c40 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -6,7 +6,18 @@ #include - +// iterates the DF job list and adds channel jobs to the `jobs` container +void ChannelJobs::load_channel_jobs() { + locations.clear(); + df::job_list_link* node = df::global::world->jobs.list.next; + while (node) { + df::job* job = node->item; + node = node->next; + if (is_dig_job(job)) { + locations.emplace(job->pos); + } + } +} // adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found void ChannelGroups::add(const df::coord &map_pos) { @@ -148,6 +159,7 @@ void ChannelGroups::scan() { // the tile, check if it has a channel designation df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z); if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) { + TileCache::Get().uncache(map_pos); remove(map_pos); if (jobs.count(map_pos)) { jobs.erase(map_pos); @@ -165,6 +177,8 @@ void ChannelGroups::scan() { } TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); add(map_pos); + } else if (groups_map.count(map_pos)) { + remove(map_pos); } } } @@ -252,21 +266,25 @@ size_t ChannelGroups::count(const df::coord &map_pos) const { // prints debug info about the groups stored, and their members void ChannelGroups::debug_groups() { - int idx = 0; - TRACE(groups).print(" debugging group data\n"); - for (auto &group : groups) { - TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size()); - for (auto &pos : group) { - TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) { + int idx = 0; + TRACE(groups).print(" debugging group data\n"); + for (auto &group: groups) { + TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size()); + for (auto &pos: group) { + TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); + } + idx++; } - idx++; } } // prints debug info group mappings void ChannelGroups::debug_map() { - INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); - for (auto &pair : groups_map) { - DEBUG(groups).print(" map[" COORD "] = %d\n",COORDARGS(pair.first), pair.second); + if (DFHack::debug_groups.isEnabled(DebugCategory::LDEBUG)) { + INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); + for (auto &pair: groups_map) { + DEBUG(groups).print(" map[" COORD "] = %d\n", COORDARGS(pair.first), pair.second); + } } } diff --git a/plugins/channel-safely/channel-jobs.cpp b/plugins/channel-safely/channel-jobs.cpp deleted file mode 100644 index 7a1c2f4be..000000000 --- a/plugins/channel-safely/channel-jobs.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include -#include -#include -#include - -// iterates the DF job list and adds channel jobs to the `jobs` container -void ChannelJobs::load_channel_jobs() { - jobs.clear(); - df::job_list_link* node = df::global::world->jobs.list.next; - while (node) { - df::job* job = node->item; - node = node->next; - if (is_dig_job(job)) { - jobs.emplace(job->pos); - } - } -} - -// clears the container -void ChannelJobs::clear() { - jobs.clear(); -} - -// finds and erases a job corresponding to a map position, then returns the iterator following the element removed -std::set::iterator ChannelJobs::erase(const df::coord &map_pos) { - auto iter = jobs.find(map_pos); - if (iter != jobs.end()) { - return jobs.erase(iter); - } - return iter; -} - -// finds a job corresponding to a map position if one exists -std::set::const_iterator ChannelJobs::find(const df::coord &map_pos) const { - return jobs.find(map_pos); -} - -// returns an iterator to the first element stored -std::set::const_iterator ChannelJobs::begin() const { - return jobs.begin(); -} - -// returns an iterator to after the last element stored -std::set::const_iterator ChannelJobs::end() const { - return jobs.end(); -} diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp index 27d4c5153..e905f2cfb 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -5,11 +5,6 @@ #include //hash function for df::coord #include -/** -blocks[48][96][135]: -blocks[48][96][135].default_liquid.hidden: false -blocks[48][96][135].designation[10][0].hidden: false - * */ // sets mark flags as necessary, for all designations void ChannelManager::manage_groups() { diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index d4107bc56..e5c9e2760 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -93,7 +93,6 @@ PersistentDataItem psetting; PersistentDataItem pfeature; const std::string FCONFIG_KEY = std::string(plugin_name) + "/feature"; const std::string SCONFIG_KEY = std::string(plugin_name) + "/setting"; -//std::unordered_set active_jobs; enum FeatureConfigData { VISION, @@ -138,20 +137,22 @@ df::coord simulate_area_fall(const df::coord &pos) { // executes dig designations for the specified tile coordinates inline bool dig_now(color_ostream &out, const df::coord &map_pos) { - auto L = Lua::Core::State; - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 2) || - !Lua::PushModulePublic(out, L, "plugins.dig-now", "dig_now_tile")) - return false; - - Lua::Push(L, map_pos); - - if (!Lua::SafeCall(out, L, 1, 1)) - return false; - - return lua_toboolean(L, -1); - + bool ret = false; + + lua_State* state = Lua::Core::State; + static const char* module_name = "plugins.dig-now"; + static const char* fn_name = "dig_now_tile"; + // the stack layout isn't likely to change, ever + static auto args_lambda = [&map_pos](lua_State* L) { + Lua::Push(L, map_pos); + }; + static auto res_lambda = [&ret](lua_State* L) { + ret = lua_toboolean(L, -1); + }; + + Lua::StackUnwinder top(state); + Lua::CallLuaModuleFunction(out, state, module_name, fn_name, 1, 1, args_lambda, res_lambda); + return ret; } // fully heals the unit specified, resurrecting if need be @@ -583,11 +584,14 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_ev command_result channel_safely(color_ostream &out, std::vector ¶meters) { if (!parameters.empty()) { + if (parameters[0] == "runonce") { + CSP::UnpauseEvent(); + return DFHack::CR_OK; + } else if (parameters[0] == "rebuild") { + ChannelManager::Get().destroy_groups(); + ChannelManager::Get().build_groups(); + } if (parameters.size() >= 2 && parameters.size() <= 3) { - if (parameters[0] == "runonce") { - CSP::UnpauseEvent(); - return DFHack::CR_OK; - } bool state = false; bool set = false; if (parameters[0] == "enable") { @@ -600,54 +604,7 @@ command_result channel_safely(color_ostream &out, std::vector ¶ return DFHack::CR_WRONG_USAGE; } try { - if (parameters[1] == "debug") { - auto level = std::abs(std::stol(parameters[2])); - config.debug = true; - switch (level) { - case 1: - DBG_NAME(manager).allowed(DFHack::DebugCategory::LDEBUG); - DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO); - DBG_NAME(groups).allowed(DFHack::DebugCategory::LINFO); - DBG_NAME(jobs).allowed(DFHack::DebugCategory::LINFO); - break; - case 2: - DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO); - DBG_NAME(groups).allowed(DFHack::DebugCategory::LDEBUG); - DBG_NAME(jobs).allowed(DFHack::DebugCategory::LDEBUG); - break; - case 3: - DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO); - DBG_NAME(groups).allowed(DFHack::DebugCategory::LDEBUG); - DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE); - break; - case 4: - DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO); - DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE); - break; - case 5: - DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(monitor).allowed(DFHack::DebugCategory::LDEBUG); - DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE); - break; - case 6: - DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(monitor).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE); - DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE); - break; - case 0: - default: - DBG_NAME(monitor).allowed(DFHack::DebugCategory::LERROR); - DBG_NAME(manager).allowed(DFHack::DebugCategory::LERROR); - DBG_NAME(groups).allowed(DFHack::DebugCategory::LERROR); - DBG_NAME(jobs).allowed(DFHack::DebugCategory::LERROR); - } - } else if(parameters[1] == "monitor"){ + if(parameters[1] == "monitor"){ if (state != config.monitor_active) { config.monitor_active = state; // if this is a fresh start diff --git a/plugins/channel-safely/include/channel-groups.h b/plugins/channel-safely/include/channel-groups.h index abdbc56fc..7547e2564 100644 --- a/plugins/channel-safely/include/channel-groups.h +++ b/plugins/channel-safely/include/channel-groups.h @@ -4,14 +4,15 @@ #include #include +#include //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway) #include -#include -#include +#include +#include using namespace DFHack; -using Group = std::set; +using Group = std::unordered_set; using Groups = std::vector; /* Used to build groups of adjacent channel designations/jobs @@ -26,8 +27,8 @@ using Groups = std::vector; */ class ChannelGroups { private: - using GroupBlocks = std::set; - using GroupsMap = std::map; + using GroupBlocks = std::unordered_set; + using GroupsMap = std::unordered_map; GroupBlocks group_blocks; GroupsMap groups_map; Groups groups; diff --git a/plugins/channel-safely/include/channel-jobs.h b/plugins/channel-safely/include/channel-jobs.h index 0290baa19..3be704aeb 100644 --- a/plugins/channel-safely/include/channel-jobs.h +++ b/plugins/channel-safely/include/channel-jobs.h @@ -1,7 +1,11 @@ #pragma once #include #include -#include +#include //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway) +#include +#include + +#include using namespace DFHack; @@ -17,14 +21,23 @@ using namespace DFHack; class ChannelJobs { private: friend class ChannelGroup; - using Jobs = std::set; // job* will exist until it is complete, and likely beyond - Jobs jobs; + + using Jobs = std::unordered_set; // job* will exist until it is complete, and likely beyond + Jobs locations; public: void load_channel_jobs(); - void clear(); - int count(const df::coord &map_pos) const { return jobs.count(map_pos); } - Jobs::iterator erase(const df::coord &map_pos); - Jobs::const_iterator find(const df::coord &map_pos) const; - Jobs::const_iterator begin() const; - Jobs::const_iterator end() const; + void clear() { + locations.clear(); + } + int count(const df::coord &map_pos) const { return locations.count(map_pos); } + Jobs::iterator erase(const df::coord &map_pos) { + auto iter = locations.find(map_pos); + if (iter != locations.end()) { + return locations.erase(iter); + } + return iter; + } + Jobs::const_iterator find(const df::coord &map_pos) const { return locations.find(map_pos); } + Jobs::const_iterator begin() const { return locations.begin(); } + Jobs::const_iterator end() const { return locations.end(); } }; diff --git a/plugins/channel-safely/include/channel-manager.h b/plugins/channel-safely/include/channel-manager.h index d36e98ca3..0cd3abfac 100644 --- a/plugins/channel-safely/include/channel-manager.h +++ b/plugins/channel-safely/include/channel-manager.h @@ -33,9 +33,7 @@ public: bool exists(const df::coord &map_pos) const { return groups.count(map_pos); } void debug() { DEBUG(groups).print(" DEBUGGING GROUPS:\n"); - if (config.debug) { - groups.debug_groups(); - groups.debug_map(); - } + groups.debug_groups(); + groups.debug_map(); } }; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index e22210f7a..8bd1de44d 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -64,7 +64,7 @@ inline bool is_safe_fall(const df::coord &map_pos) { return true; //we require vision, and we can't see below.. so we gotta assume it's safe } df::tiletype type = *Maps::getTileType(below); - if (!isOpenTerrain(type)) { + if (!DFHack::isOpenTerrain(type)) { return true; } } @@ -80,10 +80,10 @@ inline bool is_safe_to_dig_down(const df::coord &map_pos) { return true; } df::tiletype type = *Maps::getTileType(pos); - if (zi == 0 && isOpenTerrain(type)) { + if (zi == 0 && DFHack::isOpenTerrain(type)) { // the starting tile is open space, that's obviously not safe return false; - } else if (!isOpenTerrain(type)) { + } else if (!DFHack::isOpenTerrain(type)) { // a tile after the first one is not open space return true; } @@ -142,7 +142,9 @@ inline void cancel_job(df::job* job) { x = pos.x % 16; y = pos.y % 16; df::tile_designation &designation = job_block->designation[x][y]; - switch (job->job_type) { + auto type = job->job_type; + Job::removeJob(job); + switch (type) { case job_type::Dig: designation.bits.dig = df::tile_dig_designation::Default; break; @@ -165,21 +167,13 @@ inline void cancel_job(df::job* job) { designation.bits.dig = df::tile_dig_designation::No; break; } - Job::removeJob(job); } } template void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { for (const auto &a : c1) { - bool matched = false; - for (const auto &b : c2) { - if (a == b) { - matched = true; - break; - } - } - if (!matched) { + if (!c2.count(a)) { c3.emplace(a); } } diff --git a/plugins/channel-safely/include/plugin.h b/plugins/channel-safely/include/plugin.h index 2451033c5..23b2f8441 100644 --- a/plugins/channel-safely/include/plugin.h +++ b/plugins/channel-safely/include/plugin.h @@ -9,7 +9,6 @@ namespace DFHack { } struct Configuration { - bool debug = false; bool monitor_active = false; bool require_vision = true; bool insta_dig = false; diff --git a/plugins/channel-safely/include/tile-cache.h b/plugins/channel-safely/include/tile-cache.h index ddd37c34a..10e91cd46 100644 --- a/plugins/channel-safely/include/tile-cache.h +++ b/plugins/channel-safely/include/tile-cache.h @@ -3,13 +3,14 @@ #include #include #include +#include //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway) -#include +#include class TileCache { private: TileCache() = default; - std::map locations; + std::unordered_map locations; public: static TileCache& Get() { static TileCache instance; @@ -25,11 +26,6 @@ public: } bool hasChanged(const df::coord &pos, const df::tiletype &type) { - if (locations.count(pos)) { - if (type != locations.find(pos)->second){ - return true; - } - } - return false; + return locations.count(pos) && type != locations[pos]; } }; From cb338e425745f8b617d41bfe8e613b27e44f64db Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 21 Nov 2022 15:27:40 -0800 Subject: [PATCH 133/161] remove resume plugin all functionality has been migrated to unsuspend --- data/init/dfhack.tools.init | 1 - dfhack-config/overlay.json | 3 + docs/Removed.rst | 8 + docs/changelog.txt | 3 + docs/plugins/resume.rst | 23 --- plugins/CMakeLists.txt | 1 - plugins/resume.cpp | 325 ------------------------------------ 7 files changed, 14 insertions(+), 350 deletions(-) delete mode 100644 docs/plugins/resume.rst delete mode 100644 plugins/resume.cpp diff --git a/data/init/dfhack.tools.init b/data/init/dfhack.tools.init index 0887f80ba..70da533b3 100644 --- a/data/init/dfhack.tools.init +++ b/data/init/dfhack.tools.init @@ -98,7 +98,6 @@ enable \ automelt \ autotrade \ buildingplan \ - resume \ trackstop \ zone \ stocks \ diff --git a/dfhack-config/overlay.json b/dfhack-config/overlay.json index 8ad7af0a1..8b90fc4a4 100644 --- a/dfhack-config/overlay.json +++ b/dfhack-config/overlay.json @@ -10,5 +10,8 @@ }, "hotkeys.menu": { "enabled": true + }, + "unsuspend.overlay": { + "enabled": true } } diff --git a/docs/Removed.rst b/docs/Removed.rst index f9bf1c62e..a5fa29a1d 100644 --- a/docs/Removed.rst +++ b/docs/Removed.rst @@ -118,6 +118,14 @@ Tool that warned the user when the ``dfhack.init`` file did not exist. Now that ``dfhack.init`` is autogenerated in ``dfhack-config/init``, this warning is no longer necessary. +.. _resume: + +resume +====== + +Allowed you to resume suspended jobs and displayed an overlay indicating +suspended building construction jobs. Replaced by `unsuspend` script. + .. _warn-stuck-trees: warn-stuck-trees diff --git a/docs/changelog.txt b/docs/changelog.txt index 206d64879..a10665c7b 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -118,6 +118,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Internals - MSVC warning level upped to /W3, and /WX added to make warnings cause compilations to fail. +## Removed +- `resume`: functionality (including suspended building overlay) has moved to `unsuspend` + # 0.47.05-r7 ## New Plugins diff --git a/docs/plugins/resume.rst b/docs/plugins/resume.rst deleted file mode 100644 index af3fe161d..000000000 --- a/docs/plugins/resume.rst +++ /dev/null @@ -1,23 +0,0 @@ -resume -====== - -.. dfhack-tool:: - :summary: Color planned buildings based on their suspend status. - :tags: fort productivity interface jobs - :no-command: - -.. dfhack-command:: resume - :summary: Resume all suspended building jobs. - -When enabled, this plugin will display a colored 'X' over suspended buildings. -When run as a command, it can resume all suspended building jobs, allowing you -to quickly recover if a bunch of jobs were suspended due to the workers getting -scared off by wildlife or items temporarily blocking building sites. - -Usage ------ - -:: - - enable resume - resume all diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9038d5d79..dd9b22fb1 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -153,7 +153,6 @@ if(BUILD_SUPPORTED) add_subdirectory(remotefortressreader) dfhack_plugin(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename) add_subdirectory(rendermax) - dfhack_plugin(resume resume.cpp LINK_LIBRARIES lua) dfhack_plugin(reveal reveal.cpp LINK_LIBRARIES lua) dfhack_plugin(search search.cpp) dfhack_plugin(seedwatch seedwatch.cpp) diff --git a/plugins/resume.cpp b/plugins/resume.cpp deleted file mode 100644 index 21a4b521e..000000000 --- a/plugins/resume.cpp +++ /dev/null @@ -1,325 +0,0 @@ -#include -#include -#include - -#include "Core.h" -#include -#include -#include -#include - - -// DF data structure definition headers -#include "DataDefs.h" -#include "LuaTools.h" -#include "MiscUtils.h" -#include "Types.h" -#include "df/viewscreen_dwarfmodest.h" -#include "df/world.h" -#include "df/building_constructionst.h" -#include "df/building.h" -#include "df/job.h" -#include "df/job_item.h" - -#include "modules/Gui.h" -#include "modules/Screen.h" -#include "modules/Buildings.h" -#include "modules/Maps.h" - -#include "modules/World.h" - -#include "uicommon.h" - -using std::map; -using std::string; -using std::vector; - -using namespace DFHack; -using namespace df::enums; - -DFHACK_PLUGIN("resume"); -#define PLUGIN_VERSION 0.2 - -REQUIRE_GLOBAL(gps); -REQUIRE_GLOBAL(process_jobs); -REQUIRE_GLOBAL(ui); -REQUIRE_GLOBAL(world); - -#ifndef HAVE_NULLPTR -#define nullptr 0L -#endif - -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - return CR_OK; -} - -df::job *get_suspended_job(df::building *bld) -{ - if (bld->getBuildStage() != 0) - return nullptr; - - if (bld->jobs.size() == 0) - return nullptr; - - auto job = bld->jobs[0]; - if (job->flags.bits.suspend) - return job; - - return nullptr; -} - -struct SuspendedBuilding -{ - df::building *bld; - df::coord pos; - bool was_resumed; - bool is_planned; - - SuspendedBuilding(df::building *bld_) : bld(bld_), was_resumed(false), is_planned(false) - { - pos = df::coord(bld->centerx, bld->centery, bld->z); - } - - bool isValid() - { - return bld && Buildings::findAtTile(pos) == bld && get_suspended_job(bld); - } -}; - -static bool is_planned_building(df::building *bld) -{ - auto L = Lua::Core::State; - color_ostream_proxy out(Core::getInstance().getConsole()); - Lua::StackUnwinder top(L); - - if (!lua_checkstack(L, 2) || - !Lua::PushModulePublic( - out, L, "plugins.buildingplan", "isPlannedBuilding")) - return false; - - Lua::Push(L, bld); - - if (!Lua::SafeCall(out, L, 1, 1)) - return false; - - return lua_toboolean(L, -1); -} - -DFHACK_PLUGIN_IS_ENABLED(enabled); -static bool buildings_scanned = false; -static vector suspended_buildings, resumed_buildings; - -void scan_for_suspended_buildings() -{ - if (buildings_scanned) - return; - - for (auto b = world->buildings.all.begin(); b != world->buildings.all.end(); b++) - { - auto bld = *b; - auto job = get_suspended_job(bld); - if (job) - { - SuspendedBuilding sb(bld); - sb.is_planned = is_planned_building(bld); - - auto it = resumed_buildings.begin(); - - for (; it != resumed_buildings.end(); ++it) - if (it->bld == bld) break; - - sb.was_resumed = it != resumed_buildings.end(); - - suspended_buildings.push_back(sb); - } - } - - buildings_scanned = true; -} - -void show_suspended_buildings() -{ - int32_t vx, vy, vz; - if (!Gui::getViewCoords(vx, vy, vz)) - return; - - auto dims = Gui::getDwarfmodeViewDims(); - int left_margin = vx + dims.map_x2; - int bottom_margin = vy + dims.map_y2 - 1; - - for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end();) - { - if (!sb->isValid()) - { - sb = suspended_buildings.erase(sb); - continue; - } - - if (sb->bld->z == vz && sb->bld->centerx >= vx && sb->bld->centerx <= left_margin && - sb->bld->centery >= vy && sb->bld->centery <= bottom_margin) - { - int x = sb->bld->centerx - vx + 1; - int y = sb->bld->centery - vy + 1; - auto color = COLOR_YELLOW; - if (sb->is_planned) - color = COLOR_GREEN; - else if (sb->was_resumed) - color = COLOR_RED; - - OutputString(color, x, y, "X", false, 0, 0, true /* map */); - } - - sb++; - } -} - -void clear_scanned() -{ - buildings_scanned = false; - suspended_buildings.clear(); -} - -void resume_suspended_buildings(color_ostream &out) -{ - out << "Resuming all buildings." << endl; - - for (auto isb = resumed_buildings.begin(); isb != resumed_buildings.end();) - { - if (isb->isValid()) - { - isb++; - continue; - } - - isb = resumed_buildings.erase(isb); - } - - scan_for_suspended_buildings(); - for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end(); sb++) - { - if (sb->is_planned) - continue; - - resumed_buildings.push_back(*sb); - sb->bld->jobs[0]->flags.bits.suspend = false; - } - - clear_scanned(); - - out << resumed_buildings.size() << " buildings resumed" << endl; -} - - -//START Viewscreen Hook -struct resume_hook : public df::viewscreen_dwarfmodest -{ - //START UI Methods - typedef df::viewscreen_dwarfmodest interpose_base; - - DEFINE_VMETHOD_INTERPOSE(void, render, ()) - { - INTERPOSE_NEXT(render)(); - - if (enabled && DFHack::World::ReadPauseState() && ui->main.mode == ui_sidebar_mode::Default) - { - if (*process_jobs) - { - // something just created some buildings. rescan. - clear_scanned(); - } - scan_for_suspended_buildings(); - show_suspended_buildings(); - } - else - { - clear_scanned(); - } - } -}; - -IMPLEMENT_VMETHOD_INTERPOSE(resume_hook, render); - -DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable) -{ - if (!gps) - return CR_FAILURE; - - if (enabled != enable) - { - clear_scanned(); - - if (!INTERPOSE_HOOK(resume_hook, render).apply(enable)) - return CR_FAILURE; - - enabled = enable; - } - - return CR_OK; -} - -static command_result resume_cmd(color_ostream &out, vector & parameters) -{ - bool show_help = false; - if (parameters.empty()) - { - show_help = true; - } - else - { - auto cmd = parameters[0][0]; - if (cmd == 'v') - { - out << "Resume" << endl << "Version: " << PLUGIN_VERSION << endl; - } - else if (cmd == 's') - { - plugin_enable(out, true); - out << "Overlay enabled" << endl; - } - else if (cmd == 'h') - { - plugin_enable(out, false); - out << "Overlay disabled" << endl; - } - else if (cmd == 'a') - { - resume_suspended_buildings(out); - } - else - { - show_help = true; - } - } - - if (show_help) - return CR_WRONG_USAGE; - - return CR_OK; -} - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - commands.push_back( - PluginCommand( - "resume", - "Mark suspended constructions on the map and easily resume them.", - resume_cmd)); - - return CR_OK; -} - - -DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) -{ - switch (event) { - case SC_MAP_LOADED: - suspended_buildings.clear(); - resumed_buildings.clear(); - break; - default: - break; - } - - return CR_OK; -} From e2218d042945a870934f832ad5707b81db175bcb Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 21 Nov 2022 17:35:14 -0800 Subject: [PATCH 134/161] make mouse button event behavior conform to docs before, when a mouse button was held down, we'd send a single _MOUSE_L and _MOUSE_L_DOWN event and that's it. now we properly send a single _MOUSE_L_DOWN event and _MOUSE_L events for as long as the button is held down. similar for the right mouse button --- library/LuaTools.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 8d317c076..5b080ce43 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -156,21 +156,21 @@ void DFHack::Lua::PushInterfaceKeys(lua_State *L, if (df::global::enabler) { if (df::global::enabler->mouse_lbut_down) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_L"); + lua_setfield(L, -2, "_MOUSE_L_DOWN"); } if (df::global::enabler->mouse_rbut_down) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_R"); + lua_setfield(L, -2, "_MOUSE_R_DOWN"); } if (df::global::enabler->mouse_lbut) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_L_DOWN"); - df::global::enabler->mouse_lbut = 0; + lua_setfield(L, -2, "_MOUSE_L"); + df::global::enabler->mouse_lbut_down = 0; } if (df::global::enabler->mouse_rbut) { lua_pushboolean(L, true); - lua_setfield(L, -2, "_MOUSE_R_DOWN"); - df::global::enabler->mouse_rbut = 0; + lua_setfield(L, -2, "_MOUSE_R"); + df::global::enabler->mouse_rbut_down = 0; } } } From 24dc879888ea6c7ec9fd38f216006416a159bce1 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 21 Nov 2022 17:36:46 -0800 Subject: [PATCH 135/161] adapt library code to newly correct mouse events --- library/lua/gui/widgets.lua | 10 +++++----- plugins/lua/hotkeys.lua | 2 +- test/library/gui/widgets.EditField.lua | 6 +++--- test/library/gui/widgets.lua | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 2fab12f70..84b625d52 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -334,7 +334,7 @@ function EditField:onInput(keys) elseif keys._MOUSE_L then local mouse_x, mouse_y = self:getMousePos() if mouse_x then - self:setCursor(self.start_pos + mouse_x) + self:setCursor(self.start_pos + mouse_x - self.text_offset) return true end elseif keys._STRING then @@ -496,7 +496,7 @@ function Scrollbar:onRenderBody(dc) if self.is_dragging then scrollbar_do_drag(self) end - if df.global.enabler.mouse_lbut_down == 0 then + if df.global.enabler.mouse_lbut == 0 then self.last_scroll_ms = 0 self.is_dragging = false self.scroll_spec = nil @@ -928,7 +928,7 @@ end function HotkeyLabel:onInput(keys) if HotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L and self:getMousePos() then + elseif keys._MOUSE_L_DOWN and self:getMousePos() then self.on_activate() return true end @@ -1009,7 +1009,7 @@ end function CycleHotkeyLabel:onInput(keys) if CycleHotkeyLabel.super.onInput(self, keys) then return true - elseif keys._MOUSE_L and self:getMousePos() then + elseif keys._MOUSE_L_DOWN and self:getMousePos() then self:cycle() return true end @@ -1274,7 +1274,7 @@ function List:onInput(keys) elseif self.on_submit2 and keys.SEC_SELECT then self:submit2() return true - elseif keys._MOUSE_L then + elseif keys._MOUSE_L_DOWN then local idx = self:getIdxUnderMouse() if idx then self:setSelected(idx) diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua index 7a1a39115..e3ad26e68 100644 --- a/plugins/lua/hotkeys.lua +++ b/plugins/lua/hotkeys.lua @@ -207,7 +207,7 @@ function MenuScreen:onInput(keys) elseif keys.STANDARDSCROLL_RIGHT then self:onSubmit2(self.subviews.list:getSelected()) return true - elseif keys._MOUSE_L then + elseif keys._MOUSE_L_DOWN then local list = self.subviews.list local x = list:getMousePos() if x == 0 then -- clicked on icon diff --git a/test/library/gui/widgets.EditField.lua b/test/library/gui/widgets.EditField.lua index 23558987b..ba98f6dee 100644 --- a/test/library/gui/widgets.EditField.lua +++ b/test/library/gui/widgets.EditField.lua @@ -40,17 +40,17 @@ function test.editfield_click() expect.eq(5, e.cursor) mock.patch(e, 'getMousePos', mock.func(0), function() - e:onInput{_MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} expect.eq(1, e.cursor) end) mock.patch(e, 'getMousePos', mock.func(20), function() - e:onInput{_MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} expect.eq(5, e.cursor, 'should only seek to end of text') end) mock.patch(e, 'getMousePos', mock.func(2), function() - e:onInput{_MOUSE_L=true} + e:onInput{_MOUSE_L_DOWN=true} expect.eq(3, e.cursor) end) end diff --git a/test/library/gui/widgets.lua b/test/library/gui/widgets.lua index 95dbd34f1..51622e691 100644 --- a/test/library/gui/widgets.lua +++ b/test/library/gui/widgets.lua @@ -5,7 +5,7 @@ function test.hotkeylabel_click() local l = widgets.HotkeyLabel{key='SELECT', on_activate=func} mock.patch(l, 'getMousePos', mock.func(0), function() - l:onInput{_MOUSE_L=true} + l:onInput{_MOUSE_L_DOWN=true} expect.eq(1, func.call_count) end) end @@ -31,7 +31,7 @@ function test.togglehotkeylabel_click() local l = widgets.ToggleHotkeyLabel{} expect.true_(l:getOptionValue()) mock.patch(l, 'getMousePos', mock.func(0), function() - l:onInput{_MOUSE_L=true} + l:onInput{_MOUSE_L_DOWN=true} expect.false_(l:getOptionValue()) end) end From db516d94744bca02bdd545617f0f21b47591c0c9 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 21 Nov 2022 17:38:29 -0800 Subject: [PATCH 136/161] update changelog --- docs/changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 206d64879..9233d0551 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -114,6 +114,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - ``widgets.EditField`` now allows other widgets to process characters that the ``on_char`` callback rejects. - ``gui.Screen.show()`` now returns ``self`` as a convenience - ``gui.View.getMousePos()`` now takes an optional ``ViewRect`` parameter in case the caller wants to get the mouse pos relative to a rect that is not the frame_body (such as the frame_rect) +- Lua mouse events now conform to documented behavior in `lua-api` -- ``_MOUSE_L_DOWN`` will be sent exactly once per mouse click and ``_MOUSE_L`` will be sent repeatedly as long as the button is held down. Similarly for right mouse button events. ## Internals - MSVC warning level upped to /W3, and /WX added to make warnings cause compilations to fail. From c9cf5ecca83505ad1694b3c92f5ad8cdc03c8c8e Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 21 Nov 2022 17:51:04 -0800 Subject: [PATCH 137/161] we kept editfield as MOUSE_L for click and drag --- library/lua/gui/widgets.lua | 2 +- test/library/gui/widgets.EditField.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 84b625d52..e8093345f 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -334,7 +334,7 @@ function EditField:onInput(keys) elseif keys._MOUSE_L then local mouse_x, mouse_y = self:getMousePos() if mouse_x then - self:setCursor(self.start_pos + mouse_x - self.text_offset) + self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) return true end elseif keys._STRING then diff --git a/test/library/gui/widgets.EditField.lua b/test/library/gui/widgets.EditField.lua index ba98f6dee..23558987b 100644 --- a/test/library/gui/widgets.EditField.lua +++ b/test/library/gui/widgets.EditField.lua @@ -40,17 +40,17 @@ function test.editfield_click() expect.eq(5, e.cursor) mock.patch(e, 'getMousePos', mock.func(0), function() - e:onInput{_MOUSE_L_DOWN=true} + e:onInput{_MOUSE_L=true} expect.eq(1, e.cursor) end) mock.patch(e, 'getMousePos', mock.func(20), function() - e:onInput{_MOUSE_L_DOWN=true} + e:onInput{_MOUSE_L=true} expect.eq(5, e.cursor, 'should only seek to end of text') end) mock.patch(e, 'getMousePos', mock.func(2), function() - e:onInput{_MOUSE_L_DOWN=true} + e:onInput{_MOUSE_L=true} expect.eq(3, e.cursor) end) end From 807ce39251e90b3c16d6a783a4f5b7475ea7a1eb Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Tue, 22 Nov 2022 07:17:58 +0000 Subject: [PATCH 138/161] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 1748c0a8b..e48c4e2c0 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 1748c0a8b1f19091eb675334f7c38be95cb7a2b8 +Subproject commit e48c4e2c0cdb871cecd9feb7b1f7c154970666b2 From c6b15b1ccb77e3e821aa31c0577d7a462d9cada6 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 22 Nov 2022 11:03:28 -0800 Subject: [PATCH 139/161] Implements plugin: channel-safely v1.2a --- plugins/channel-safely/channel-safely-plugin.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index e5c9e2760..d291c0efc 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -128,8 +128,9 @@ df::coord simulate_area_fall(const df::coord &pos) { get_neighbours(pos, neighbours); df::coord lowest = simulate_fall(pos); for (auto p : neighbours) { - if (p.z < lowest.z) { - lowest = p; + auto nlow = simulate_fall(p); + if (nlow.z < lowest.z) { + lowest = nlow; } } return lowest; @@ -225,6 +226,7 @@ namespace CSP { } void UnpauseEvent(){ + CoreSuspender suspend; // we need exclusive access to df memory and this call stack doesn't already have a lock INFO(monitor).print("UnpauseEvent()\n"); ChannelManager::Get().build_groups(); ChannelManager::Get().manage_groups(); From 86ec1c17adceca353c0968d1827d4f87e112a50e Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 22 Nov 2022 13:28:27 -0800 Subject: [PATCH 140/161] Fixes doc formatting --- docs/plugins/channel-safely.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 4106aee04..4a9902216 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -15,6 +15,7 @@ Usage ----- :: + enable channel-safely channel-safely set channel-safely enable|disable From a9ea68b26aa601003bf400c33594eb538e2ff7a3 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Wed, 23 Nov 2022 07:15:32 +0000 Subject: [PATCH 141/161] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index e48c4e2c0..4e3100a1b 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit e48c4e2c0cdb871cecd9feb7b1f7c154970666b2 +Subproject commit 4e3100a1b7bf1e54d26408733feaf1a0e861d45e From 6cdb192181694072422e5564f1479f7032512e4f Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 23 Nov 2022 10:37:33 -0800 Subject: [PATCH 142/161] Documents persistence of settings --- docs/plugins/channel-safely.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 4a9902216..812936d33 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -27,6 +27,8 @@ and designations deemed unsafe will be put into :wiki:`Marker Mode Date: Wed, 23 Nov 2022 19:33:21 +0000 Subject: [PATCH 143/161] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 4e3100a1b..727e4921c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 4e3100a1b7bf1e54d26408733feaf1a0e861d45e +Subproject commit 727e4921c00e260d7c8d1112daf77115ce3960ee From 92a53bbef6f6f69b64a551e825df93644192f738 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 23 Nov 2022 11:39:04 -0800 Subject: [PATCH 144/161] Update docs/plugins/channel-safely.rst --- docs/plugins/channel-safely.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index 812936d33..1b72c93df 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -27,7 +27,7 @@ and designations deemed unsafe will be put into :wiki:`Marker Mode Date: Wed, 20 Apr 2022 20:28:13 -0400 Subject: [PATCH 145/161] unit tests: Add CTest support, and a trivial first unit test If BUILD_TESTS=ON: - Adds a 'test' target for ninja - Adds a library/MiscUtils.test unit test executable --- .gitignore | 2 ++ CMakeLists.txt | 10 +++++++- library/CMakeLists.txt | 10 ++++++++ library/MiscUtils.test.cpp | 47 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 library/MiscUtils.test.cpp diff --git a/.gitignore b/.gitignore index e91dcab3b..9b78fa31f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ build/Makefile build/CMakeCache.txt build/cmake_install.cmake build/CMakeFiles +build/CTestTestfile.cmake +build/DartConfiguration.tcl build/data build/docs build/lua diff --git a/CMakeLists.txt b/CMakeLists.txt index c03037973..e36385c43 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -527,14 +527,22 @@ if(BUILD_DOCS) install(FILES "README.html" DESTINATION "${DFHACK_DATA_DESTINATION}") endif() -option(BUILD_TESTS "Include tests (currently just installs Lua tests into the scripts folder)" OFF) +option(BUILD_TESTS "Build 'test' target, and install integration tests in hack/scripts/test" OFF) if(BUILD_TESTS) + include(CTest) + # Install Lua tests into the scripts folder if(EXISTS "${dfhack_SOURCE_DIR}/test/scripts") message(SEND_ERROR "test/scripts must not exist in the dfhack repo since it would conflict with the tests installed from the scripts repo.") endif() install(DIRECTORY ${dfhack_SOURCE_DIR}/test DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) +else() + add_custom_target(test + COMMENT "Nothing to do: CMake option BUILD_TESTS is OFF" + # Portable NOOP; need to put something here or the comment isn't displayed + COMMAND cd + ) endif() # Packaging with CPack! diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index f969ade92..640d17978 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -82,6 +82,16 @@ set(MAIN_SOURCES RemoteTools.cpp ) +if(BUILD_TESTS) + # TODO Make a function or macro for this + add_executable( + MiscUtils.test + MiscUtils.test.cpp + MiscUtils.cpp) + add_test(NAME MiscUtils.test + COMMAND MiscUtils.test) +endif() + if(WIN32) set(CONSOLE_SOURCES Console-windows.cpp) else() diff --git a/library/MiscUtils.test.cpp b/library/MiscUtils.test.cpp new file mode 100644 index 000000000..f3360b121 --- /dev/null +++ b/library/MiscUtils.test.cpp @@ -0,0 +1,47 @@ +#include "Internal.h" +#include "MiscUtils.h" + +#include +#include +using namespace std; + +int compare_result(const vector &expect, const vector &result) +{ + if (result == expect) + { + cout << "ok\n"; + return 0; + } + else { + cout << "not ok\n"; + auto e = expect.begin(); + auto r = result.begin(); + cout << "# " << setw(20) << left << "Expected" << " " << left << "Got\n"; + while (e < expect.end() || r < result.end()) + { + cout + << "# " + << setw(20) << left << ":" + (e < expect.end() ? *e++ : "") + ":" + << " " + << setw(20) << left << ":" + (r < result.end() ? *r++ : "") + ":" + << "\n"; + } + return 1; + } +} + +int main() +{ + int fails = 0; +#define TEST_WORDWRAP(label, expect, args) \ + { \ + vector result; \ + cout << label << "... "; \ + word_wrap args; \ + fails += compare_result(expect, result); \ + } + + TEST_WORDWRAP("Short line, no wrap", vector({"12345"}), (&result, "12345", 15)); + + return fails == 0 ? 0 : 1; +} From face558dd060161de5e8a9f6301e4876d572a087 Mon Sep 17 00:00:00 2001 From: Tim Siegel Date: Thu, 21 Apr 2022 10:42:19 -0400 Subject: [PATCH 146/161] unit testing: Link test executables against libdfhack Note: Hard-coded "SDL" here is wrong, but requires some refactoring in top-level CMakeLists.txt to fix. --- library/CMakeLists.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 640d17978..dd9582b5b 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -84,12 +84,10 @@ set(MAIN_SOURCES if(BUILD_TESTS) # TODO Make a function or macro for this - add_executable( - MiscUtils.test - MiscUtils.test.cpp - MiscUtils.cpp) - add_test(NAME MiscUtils.test - COMMAND MiscUtils.test) + add_executable(MiscUtils.test MiscUtils.test.cpp) + # FIXME Hard-coded "SDL" works on Linux, but probably isn't portable + target_link_libraries(MiscUtils.test dfhack SDL) + add_test(NAME MiscUtils.test COMMAND MiscUtils.test) endif() if(WIN32) From 8e18d610f5f38df97dad4247b608c4abe320c7b8 Mon Sep 17 00:00:00 2001 From: Tim Siegel Date: Thu, 21 Apr 2022 18:33:53 -0400 Subject: [PATCH 147/161] cmake: Add SDL dep for Linux dfhack; deprecate BUILD_TESTS On Linux, libdfhack.so depends on libSDL.so, but that was not marked inside CMake. As it's only used via LD_PRELOAD, there was no problem. But when linking unit tests against it, this becomes necessary. It may be wise to add a find_package(SDL) to provide the user with more control, but just a hard-coded "SDL" should work for most installs. The CTest module creates a BUILD_TESTING option, which clashes (thematically, not in code) with the existing BUILD_TESTS option. Resolve it thus: - Deprecate BUILD_TESTS; it still works, but is marked as an advanced option so it doesn't show in the CMake UI by default. - Add a new BUILD_TEST_SCRIPTS that does what BUILD_TESTS used to do, but is a "dependent" option so it goes away if BUILD_TESTING=OFF. The up-shot is that, by default, the C++ unit tests will be built (BUILD_TESTING=ON) and the Lua integration tests are not installed (BUILD_TEST_SCRIPTS=OFF). --- CMakeLists.txt | 32 +++++++++++++++++++++++--------- library/CMakeLists.txt | 12 +++++++++--- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e36385c43..caf53c2a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -527,16 +527,30 @@ if(BUILD_DOCS) install(FILES "README.html" DESTINATION "${DFHACK_DATA_DESTINATION}") endif() -option(BUILD_TESTS "Build 'test' target, and install integration tests in hack/scripts/test" OFF) -if(BUILD_TESTS) - include(CTest) - # Install Lua tests into the scripts folder - if(EXISTS "${dfhack_SOURCE_DIR}/test/scripts") - message(SEND_ERROR "test/scripts must not exist in the dfhack repo since it would conflict with the tests installed from the scripts repo.") +# Testing with CTest +include(CTest) + +include(CMakeDependentOption) +cmake_dependent_option( + BUILD_TEST_SCRIPTS "Install integration tests in hack/scripts/test" OFF + "BUILD_TESTING" OFF) +mark_as_advanced(FORCE BUILD_TESTS) + +# Handle deprecated BUILD_TESTS option +option(BUILD_TESTS "Deprecated option; please use BUILD_TEST_SCRIPTS=ON" OFF) +if(BUILD_TESTING AND BUILD_TESTS) + set(BUILD_TEST_SCRIPTS ON FORCE) +endif() + +if(BUILD_TESTING) + if(BUILD_TEST_SCRIPTS) + if(EXISTS "${dfhack_SOURCE_DIR}/test/scripts") + message(SEND_ERROR "test/scripts must not exist in the dfhack repo since it would conflict with the tests installed from the scripts repo.") + endif() + install(DIRECTORY ${dfhack_SOURCE_DIR}/test + DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) + install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) endif() - install(DIRECTORY ${dfhack_SOURCE_DIR}/test - DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) - install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) else() add_custom_target(test COMMENT "Nothing to do: CMake option BUILD_TESTS is OFF" diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index dd9582b5b..a62eb2fe0 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -82,12 +82,16 @@ set(MAIN_SOURCES RemoteTools.cpp ) -if(BUILD_TESTS) +if(BUILD_TESTING) # TODO Make a function or macro for this add_executable(MiscUtils.test MiscUtils.test.cpp) - # FIXME Hard-coded "SDL" works on Linux, but probably isn't portable - target_link_libraries(MiscUtils.test dfhack SDL) + target_link_libraries(MiscUtils.test dfhack) add_test(NAME MiscUtils.test COMMAND MiscUtils.test) + + # How to get `test` to ensure everything is up to date before running + # tests? This add_dependencies() fails with: + # Cannot add target-level dependencies to non-existent target "test". + #add_dependencies(test MiscUtils.test) endif() if(WIN32) @@ -400,6 +404,8 @@ if(APPLE) target_link_libraries(dfhack ncurses) set_target_properties(dfhack PROPERTIES VERSION 1.0.0) set_target_properties(dfhack PROPERTIES SOVERSION 1.0.0) +elseif(UNIX) + target_link_libraries(dfhack SDL) endif() target_link_libraries(dfhack protobuf-lite clsocket lua jsoncpp_static dfhack-version ${PROJECT_LIBS}) From 268719ed1f4d9a1fdf1731853bfd787639805e06 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 14:48:00 -0800 Subject: [PATCH 148/161] Integrates googletest --- .gitmodules | 3 ++ CMakeLists.txt | 65 +++++++++++++++++++------------------- depends/CMakeLists.txt | 3 ++ depends/googletest | 1 + library/CMakeLists.txt | 14 +++++--- library/MiscUtils.test.cpp | 57 +++++++++++---------------------- library/test.cpp | 6 ++++ 7 files changed, 74 insertions(+), 75 deletions(-) create mode 160000 depends/googletest create mode 100644 library/test.cpp diff --git a/.gitmodules b/.gitmodules index 9c5ac2d51..ee26c1926 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "depends/luacov"] path = depends/luacov url = ../../DFHack/luacov.git +[submodule "depends/googletest"] + path = depends/googletest + url = https://github.com/google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt index caf53c2a0..40dc64b8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -426,6 +426,39 @@ if(NOT GIT_FOUND) message(SEND_ERROR "could not find git") endif() +# Testing with CTest +include(CTest) + +include(CMakeDependentOption) +cmake_dependent_option( + BUILD_TEST_SCRIPTS "Install integration tests in hack/scripts/test" OFF + "BUILD_TESTING" OFF) +mark_as_advanced(FORCE BUILD_TESTS) + +# Handle deprecated BUILD_TESTS option +option(BUILD_TESTS "Deprecated option; please use BUILD_TEST_SCRIPTS=ON" OFF) +if(BUILD_TESTING AND BUILD_TESTS) + set(BUILD_TEST_SCRIPTS ON FORCE) +endif() + +if(BUILD_TESTING) + include_directories(depends/googletest/googletest/include) + if(BUILD_TEST_SCRIPTS) + if(EXISTS "${dfhack_SOURCE_DIR}/test/scripts") + message(SEND_ERROR "test/scripts must not exist in the dfhack repo since it would conflict with the tests installed from the scripts repo.") + endif() + install(DIRECTORY ${dfhack_SOURCE_DIR}/test + DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) + install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) + endif() +else() + add_custom_target(test + COMMENT "Nothing to do: CMake option BUILD_TESTS is OFF" + # Portable NOOP; need to put something here or the comment isn't displayed + COMMAND cd + ) +endif() + # build the lib itself if(BUILD_LIBRARY) add_subdirectory(library) @@ -527,38 +560,6 @@ if(BUILD_DOCS) install(FILES "README.html" DESTINATION "${DFHACK_DATA_DESTINATION}") endif() -# Testing with CTest -include(CTest) - -include(CMakeDependentOption) -cmake_dependent_option( - BUILD_TEST_SCRIPTS "Install integration tests in hack/scripts/test" OFF - "BUILD_TESTING" OFF) -mark_as_advanced(FORCE BUILD_TESTS) - -# Handle deprecated BUILD_TESTS option -option(BUILD_TESTS "Deprecated option; please use BUILD_TEST_SCRIPTS=ON" OFF) -if(BUILD_TESTING AND BUILD_TESTS) - set(BUILD_TEST_SCRIPTS ON FORCE) -endif() - -if(BUILD_TESTING) - if(BUILD_TEST_SCRIPTS) - if(EXISTS "${dfhack_SOURCE_DIR}/test/scripts") - message(SEND_ERROR "test/scripts must not exist in the dfhack repo since it would conflict with the tests installed from the scripts repo.") - endif() - install(DIRECTORY ${dfhack_SOURCE_DIR}/test - DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) - install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) - endif() -else() - add_custom_target(test - COMMENT "Nothing to do: CMake option BUILD_TESTS is OFF" - # Portable NOOP; need to put something here or the comment isn't displayed - COMMAND cd - ) -endif() - # Packaging with CPack! set(DFHACK_PACKAGE_SUFFIX "") if(UNIX) diff --git a/depends/CMakeLists.txt b/depends/CMakeLists.txt index 405a9555e..1d43fbf86 100644 --- a/depends/CMakeLists.txt +++ b/depends/CMakeLists.txt @@ -3,6 +3,9 @@ add_subdirectory(lodepng) add_subdirectory(lua) add_subdirectory(md5) add_subdirectory(protobuf) +if(BUILD_TESTING) + add_subdirectory(googletest) +endif() # Don't build tinyxml if it's being externally linked against. if(NOT TinyXML_FOUND) diff --git a/depends/googletest b/depends/googletest new file mode 160000 index 000000000..15460959c --- /dev/null +++ b/depends/googletest @@ -0,0 +1 @@ +Subproject commit 15460959cbbfa20e66ef0b5ab497367e47fc0a04 diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index a62eb2fe0..3c85d5861 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -82,11 +82,17 @@ set(MAIN_SOURCES RemoteTools.cpp ) +macro(dfhack_test name files) + message("dfhack_test files: ${files}") + add_executable(${name} test.cpp ${files}) + target_link_libraries(${name} dfhack gtest) + add_test(NAME ${name} COMMAND ${name}) +endmacro() + if(BUILD_TESTING) - # TODO Make a function or macro for this - add_executable(MiscUtils.test MiscUtils.test.cpp) - target_link_libraries(MiscUtils.test dfhack) - add_test(NAME MiscUtils.test COMMAND MiscUtils.test) + file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *.test.cpp) + dfhack_test(test-all ${TEST_SOURCES}) + dfhack_test(test-MiscUtils MiscUtils.test.cpp) # How to get `test` to ensure everything is up to date before running # tests? This add_dependencies() fails with: diff --git a/library/MiscUtils.test.cpp b/library/MiscUtils.test.cpp index f3360b121..e0cabe234 100644 --- a/library/MiscUtils.test.cpp +++ b/library/MiscUtils.test.cpp @@ -1,47 +1,26 @@ -#include "Internal.h" -#include "MiscUtils.h" +#include "MiscUtils.h" +#include #include #include -using namespace std; -int compare_result(const vector &expect, const vector &result) -{ - if (result == expect) - { - cout << "ok\n"; - return 0; - } - else { - cout << "not ok\n"; - auto e = expect.begin(); - auto r = result.begin(); - cout << "# " << setw(20) << left << "Expected" << " " << left << "Got\n"; - while (e < expect.end() || r < result.end()) - { - cout - << "# " - << setw(20) << left << ":" + (e < expect.end() ? *e++ : "") + ":" - << " " - << setw(20) << left << ":" + (r < result.end() ? *r++ : "") + ":" - << "\n"; - } - return 1; - } -} +TEST(MiscUtils, wordwrap) { + std::vector result; -int main() -{ - int fails = 0; -#define TEST_WORDWRAP(label, expect, args) \ - { \ - vector result; \ - cout << label << "... "; \ - word_wrap args; \ - fails += compare_result(expect, result); \ - } + std::cout << "MiscUtils.wordwrap: 0 wrap" << "... "; + word_wrap(&result, "123", 3); + ASSERT_EQ(result.size(), 1); + std::cout << "ok\n"; - TEST_WORDWRAP("Short line, no wrap", vector({"12345"}), (&result, "12345", 15)); + result.clear(); + std::cout << "MiscUtils.wordwrap: 1 wrap" << "... "; + word_wrap(&result, "12345", 3); + ASSERT_EQ(result.size(), 2); + std::cout << "ok\n"; - return fails == 0 ? 0 : 1; + result.clear(); + std::cout << "MiscUtils.wordwrap: 2 wrap" << "... "; + word_wrap(&result, "1234567", 3); + ASSERT_EQ(result.size(), 3); + std::cout << "ok\n"; } diff --git a/library/test.cpp b/library/test.cpp new file mode 100644 index 000000000..2dc3787b7 --- /dev/null +++ b/library/test.cpp @@ -0,0 +1,6 @@ +#include + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file From e1f9a95d4f5b9b5492494359ed0911c73dd15aa3 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 14:54:18 -0800 Subject: [PATCH 149/161] Rolls back googletest to release-1.8.1 --- depends/googletest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/googletest b/depends/googletest index 15460959c..2fe3bd994 160000 --- a/depends/googletest +++ b/depends/googletest @@ -1 +1 @@ -Subproject commit 15460959cbbfa20e66ef0b5ab497367e47fc0a04 +Subproject commit 2fe3bd994b3189899d93f1d5a881e725e046fdc2 From 32b030e348be03aa1576cc2f64446e328b75f5cd Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 15:54:08 -0800 Subject: [PATCH 150/161] Adds -Wno-maybe-uninitialized to gtest target --- depends/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/depends/CMakeLists.txt b/depends/CMakeLists.txt index 1d43fbf86..b7c47d6ca 100644 --- a/depends/CMakeLists.txt +++ b/depends/CMakeLists.txt @@ -4,7 +4,8 @@ add_subdirectory(lua) add_subdirectory(md5) add_subdirectory(protobuf) if(BUILD_TESTING) - add_subdirectory(googletest) + add_subdirectory(googletest EXCLUDE_FROM_ALL) + set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized") endif() # Don't build tinyxml if it's being externally linked against. From 9fdb2f7e475f1ff2d34e98360a7691e4aa564f2a Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 16:01:16 -0800 Subject: [PATCH 151/161] Adds -Wno-sign-compare to gtest target --- depends/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/CMakeLists.txt b/depends/CMakeLists.txt index b7c47d6ca..13a8cc466 100644 --- a/depends/CMakeLists.txt +++ b/depends/CMakeLists.txt @@ -5,7 +5,7 @@ add_subdirectory(md5) add_subdirectory(protobuf) if(BUILD_TESTING) add_subdirectory(googletest EXCLUDE_FROM_ALL) - set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized") + set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized -Wno-sign-compare") endif() # Don't build tinyxml if it's being externally linked against. From de91fa7f28813b8ada2ebab8d12a155f6510402a Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 16:12:51 -0800 Subject: [PATCH 152/161] Adds -Wno-sign-compare to test targets --- library/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 3c85d5861..f8a60cfc0 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -87,6 +87,7 @@ macro(dfhack_test name files) add_executable(${name} test.cpp ${files}) target_link_libraries(${name} dfhack gtest) add_test(NAME ${name} COMMAND ${name}) + set_target_properties(${name} PROPERTIES COMPILE_FLAGS "-Wno-sign-compare") endmacro() if(BUILD_TESTING) From 3e2d0f2ec2a762fd3794d60847ded42ab5ba5b21 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 16:41:06 -0800 Subject: [PATCH 153/161] Updates GithubActions yml file to use new BUILD_TESTING variable --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83f3490da..d9af9a20e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,7 +83,7 @@ jobs: -B build-ci \ -G Ninja \ -DDFHACK_BUILD_ARCH=64 \ - -DBUILD_TESTS:BOOL=ON \ + -DBUILD_TESTING:BOOL=ON \ -DBUILD_DEV_PLUGINS:BOOL=${{ matrix.plugins == 'all' }} \ -DBUILD_SIZECHECK:BOOL=${{ matrix.plugins == 'all' }} \ -DBUILD_SKELETON:BOOL=${{ matrix.plugins == 'all' }} \ From 79551f7ef00efd9506f94ed1390dba476a9fc091 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 17:17:29 -0800 Subject: [PATCH 154/161] Updates CMake TESTING vars --- CMakeLists.txt | 48 ++++++++++++++++++++++++------------------ data/CMakeLists.txt | 2 +- depends/CMakeLists.txt | 2 +- library/CMakeLists.txt | 2 +- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 40dc64b8f..c78c45bec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -414,6 +414,15 @@ else() set(DFHACK_TINYXML "dfhack-tinyxml") endif() +if(BUILD_TESTING) + message("BUILD TESTS: Core, Scripts") + set(BUILD_SCRIPT_TESTS ON FORCE) + set(BUILD_CORE_TESTS ON FORCE) +endif() + +if(BUILD_CORE_TESTS) + include_directories(depends/googletest/googletest/include) +endif() include_directories(depends/lodepng) include_directories(depends/tthread) include_directories(${ZLIB_INCLUDE_DIRS}) @@ -421,44 +430,41 @@ include_directories(depends/clsocket/src) include_directories(depends/xlsxio/include) add_subdirectory(depends) -find_package(Git REQUIRED) -if(NOT GIT_FOUND) - message(SEND_ERROR "could not find git") -endif() # Testing with CTest -include(CTest) +if(BUILD_TESTING OR BUILD_CORE_TESTS) + message("Including CTest") + include(CTest) +endif() include(CMakeDependentOption) cmake_dependent_option( - BUILD_TEST_SCRIPTS "Install integration tests in hack/scripts/test" OFF + BUILD_SCRIPT_TESTS "Install integration tests in hack/scripts/test" OFF "BUILD_TESTING" OFF) mark_as_advanced(FORCE BUILD_TESTS) - # Handle deprecated BUILD_TESTS option -option(BUILD_TESTS "Deprecated option; please use BUILD_TEST_SCRIPTS=ON" OFF) -if(BUILD_TESTING AND BUILD_TESTS) - set(BUILD_TEST_SCRIPTS ON FORCE) -endif() +option(BUILD_TESTS "Deprecated option; please use BUILD_SCRIPT_TESTS=ON" OFF) -if(BUILD_TESTING) - include_directories(depends/googletest/googletest/include) - if(BUILD_TEST_SCRIPTS) - if(EXISTS "${dfhack_SOURCE_DIR}/test/scripts") - message(SEND_ERROR "test/scripts must not exist in the dfhack repo since it would conflict with the tests installed from the scripts repo.") - endif() - install(DIRECTORY ${dfhack_SOURCE_DIR}/test - DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) - install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) +if(BUILD_TESTING OR BUILD_SCRIPT_TESTS) + if(EXISTS "${dfhack_SOURCE_DIR}/test/scripts") + message(SEND_ERROR "test/scripts must not exist in the dfhack repo since it would conflict with the tests installed from the scripts repo.") endif() + install(DIRECTORY ${dfhack_SOURCE_DIR}/test + DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) + install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) else() add_custom_target(test - COMMENT "Nothing to do: CMake option BUILD_TESTS is OFF" + COMMENT "Nothing to do: CMake option BUILD_TESTING is OFF" # Portable NOOP; need to put something here or the comment isn't displayed COMMAND cd ) endif() +find_package(Git REQUIRED) +if(NOT GIT_FOUND) + message(SEND_ERROR "could not find git") +endif() + # build the lib itself if(BUILD_LIBRARY) add_subdirectory(library) diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 412ffa347..8cf6bb2ca 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -21,7 +21,7 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/ FILES_MATCHING PATTERN "*" PATTERN blueprints/library/test EXCLUDE) -if(BUILD_TESTS) +if(BUILD_SCRIPT_TESTS) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/library/test/ DESTINATION blueprints/library/test ) diff --git a/depends/CMakeLists.txt b/depends/CMakeLists.txt index 13a8cc466..f5e77ad8a 100644 --- a/depends/CMakeLists.txt +++ b/depends/CMakeLists.txt @@ -3,7 +3,7 @@ add_subdirectory(lodepng) add_subdirectory(lua) add_subdirectory(md5) add_subdirectory(protobuf) -if(BUILD_TESTING) +if(BUILD_CORE_TESTS) add_subdirectory(googletest EXCLUDE_FROM_ALL) set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized -Wno-sign-compare") endif() diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index f8a60cfc0..035fcda73 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -90,7 +90,7 @@ macro(dfhack_test name files) set_target_properties(${name} PROPERTIES COMPILE_FLAGS "-Wno-sign-compare") endmacro() -if(BUILD_TESTING) +if(BUILD_CORE_TESTS) file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *.test.cpp) dfhack_test(test-all ${TEST_SOURCES}) dfhack_test(test-MiscUtils MiscUtils.test.cpp) From 69d988332c129fc5719a95160924dffb5e61ab9b Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 17:36:45 -0800 Subject: [PATCH 155/161] Updates build.yml to call test-all --- .github/workflows/build.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9af9a20e..984a7622e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,12 +100,15 @@ jobs: id: run_tests run: | export TERM=dumb - status=0 - script -qe -c "python ci/run-tests.py --headless --keep-status \"$DF_FOLDER\"" || status=$((status + 1)) - python ci/check-rpc.py "$DF_FOLDER/dfhack-rpc.txt" || status=$((status + 2)) - mkdir -p artifacts - cp "$DF_FOLDER"/test*.json "$DF_FOLDER"/*.log artifacts || status=$((status + 4)) - exit $status + if build-ci/library/test-all; then + status=0 + script -qe -c "python ci/run-tests.py --headless --keep-status \"$DF_FOLDER\"" || status=$((status + 1)) + python ci/check-rpc.py "$DF_FOLDER/dfhack-rpc.txt" || status=$((status + 2)) + mkdir -p artifacts + cp "$DF_FOLDER"/test*.json "$DF_FOLDER"/*.log artifacts || status=$((status + 4)) + exit $status + fi + exit 1 - name: Upload test artifacts uses: actions/upload-artifact@v1 if: (success() || failure()) && steps.run_tests.outcome != 'skipped' From 25f87306b427c39e14591e19754d56633e940b92 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 18:46:46 -0800 Subject: [PATCH 156/161] Removes separated unit test executables --- CMakeLists.txt | 19 +++++++++++++------ library/CMakeLists.txt | 17 +---------------- library/tests/CMakeLists.txt | 7 +++++++ library/{ => tests}/MiscUtils.test.cpp | 7 ------- library/{ => tests}/test.cpp | 2 +- 5 files changed, 22 insertions(+), 30 deletions(-) create mode 100644 library/tests/CMakeLists.txt rename library/{ => tests}/MiscUtils.test.cpp (59%) rename library/{ => tests}/test.cpp (98%) diff --git a/CMakeLists.txt b/CMakeLists.txt index c78c45bec..29103d495 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -394,11 +394,6 @@ else() endif() endif() -find_package(ZLIB REQUIRED) -include_directories(depends/protobuf) -include_directories(depends/lua/include) -include_directories(depends/md5) - # Support linking against external tinyxml # If we find an external tinyxml, set the DFHACK_TINYXML variable to "tinyxml" # Otherwise, set it to "dfhack-tinyxml" @@ -420,9 +415,15 @@ if(BUILD_TESTING) set(BUILD_CORE_TESTS ON FORCE) endif() +find_package(ZLIB REQUIRED) + if(BUILD_CORE_TESTS) include_directories(depends/googletest/googletest/include) endif() + +include_directories(depends/protobuf) +include_directories(depends/lua/include) +include_directories(depends/md5) include_directories(depends/lodepng) include_directories(depends/tthread) include_directories(${ZLIB_INCLUDE_DIRS}) @@ -433,7 +434,13 @@ add_subdirectory(depends) # Testing with CTest if(BUILD_TESTING OR BUILD_CORE_TESTS) - message("Including CTest") + macro(dfhack_test name files) + message("dfhack_test(${name}, ${files})") + add_executable(${name} ${files}) + target_link_libraries(${name} dfhack gtest) + add_test(NAME ${name} COMMAND ${name}) + set_target_properties(${name} PROPERTIES COMPILE_FLAGS "-Wno-sign-compare") + endmacro() include(CTest) endif() diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index 035fcda73..5e7488027 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -82,23 +82,8 @@ set(MAIN_SOURCES RemoteTools.cpp ) -macro(dfhack_test name files) - message("dfhack_test files: ${files}") - add_executable(${name} test.cpp ${files}) - target_link_libraries(${name} dfhack gtest) - add_test(NAME ${name} COMMAND ${name}) - set_target_properties(${name} PROPERTIES COMPILE_FLAGS "-Wno-sign-compare") -endmacro() - if(BUILD_CORE_TESTS) - file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *.test.cpp) - dfhack_test(test-all ${TEST_SOURCES}) - dfhack_test(test-MiscUtils MiscUtils.test.cpp) - - # How to get `test` to ensure everything is up to date before running - # tests? This add_dependencies() fails with: - # Cannot add target-level dependencies to non-existent target "test". - #add_dependencies(test MiscUtils.test) + add_subdirectory(tests) endif() if(WIN32) diff --git a/library/tests/CMakeLists.txt b/library/tests/CMakeLists.txt new file mode 100644 index 000000000..3e5a2b3ca --- /dev/null +++ b/library/tests/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *test.cpp) +dfhack_test(test-library "${TEST_SOURCES}") + +# How to get `test` to ensure everything is up to date before running +# tests? This add_dependencies() fails with: +# Cannot add target-level dependencies to non-existent target "test". +#add_dependencies(test MiscUtils.test) diff --git a/library/MiscUtils.test.cpp b/library/tests/MiscUtils.test.cpp similarity index 59% rename from library/MiscUtils.test.cpp rename to library/tests/MiscUtils.test.cpp index e0cabe234..033b226a7 100644 --- a/library/MiscUtils.test.cpp +++ b/library/tests/MiscUtils.test.cpp @@ -1,26 +1,19 @@ #include "MiscUtils.h" #include -#include #include TEST(MiscUtils, wordwrap) { std::vector result; - std::cout << "MiscUtils.wordwrap: 0 wrap" << "... "; word_wrap(&result, "123", 3); ASSERT_EQ(result.size(), 1); - std::cout << "ok\n"; result.clear(); - std::cout << "MiscUtils.wordwrap: 1 wrap" << "... "; word_wrap(&result, "12345", 3); ASSERT_EQ(result.size(), 2); - std::cout << "ok\n"; result.clear(); - std::cout << "MiscUtils.wordwrap: 2 wrap" << "... "; word_wrap(&result, "1234567", 3); ASSERT_EQ(result.size(), 3); - std::cout << "ok\n"; } diff --git a/library/test.cpp b/library/tests/test.cpp similarity index 98% rename from library/test.cpp rename to library/tests/test.cpp index 2dc3787b7..76f841f1b 100644 --- a/library/test.cpp +++ b/library/tests/test.cpp @@ -3,4 +3,4 @@ int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); -} \ No newline at end of file +} From a716b2796eb4598451dba3e0ea6ce3151dea3e59 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 18:49:52 -0800 Subject: [PATCH 157/161] Updates test binary name in build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 984a7622e..53bfca76e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,7 +100,7 @@ jobs: id: run_tests run: | export TERM=dumb - if build-ci/library/test-all; then + if build-ci/library/tests/test-library; then status=0 script -qe -c "python ci/run-tests.py --headless --keep-status \"$DF_FOLDER\"" || status=$((status + 1)) python ci/check-rpc.py "$DF_FOLDER/dfhack-rpc.txt" || status=$((status + 2)) From cd7fe8a213b20ac8a05ce631fadd078a16f90a1d Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 18:54:16 -0800 Subject: [PATCH 158/161] Update .gitmodules Co-authored-by: Alan --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index ee26c1926..c349de288 100644 --- a/.gitmodules +++ b/.gitmodules @@ -30,4 +30,4 @@ url = ../../DFHack/luacov.git [submodule "depends/googletest"] path = depends/googletest - url = https://github.com/google/googletest.git + url = ../../google/googletest.git From d03f93c0d7b57d17ee5929a052a5255d3acaf95b Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 11 Nov 2022 19:01:10 -0800 Subject: [PATCH 159/161] Adds a second test stage --- .github/workflows/build.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53bfca76e..1779e486e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -96,19 +96,20 @@ jobs: run: | ninja -C build-ci install ccache --show-stats - - name: Run tests - id: run_tests + - name: Run unit tests + id: run_tests1 + run: | + exit build-ci/library/tests/test-library + - name: Run lua tests + id: run_tests2 run: | export TERM=dumb - if build-ci/library/tests/test-library; then - status=0 - script -qe -c "python ci/run-tests.py --headless --keep-status \"$DF_FOLDER\"" || status=$((status + 1)) - python ci/check-rpc.py "$DF_FOLDER/dfhack-rpc.txt" || status=$((status + 2)) - mkdir -p artifacts - cp "$DF_FOLDER"/test*.json "$DF_FOLDER"/*.log artifacts || status=$((status + 4)) - exit $status - fi - exit 1 + status=0 + script -qe -c "python ci/run-tests.py --headless --keep-status \"$DF_FOLDER\"" || status=$((status + 1)) + python ci/check-rpc.py "$DF_FOLDER/dfhack-rpc.txt" || status=$((status + 2)) + mkdir -p artifacts + cp "$DF_FOLDER"/test*.json "$DF_FOLDER"/*.log artifacts || status=$((status + 4)) + exit $status - name: Upload test artifacts uses: actions/upload-artifact@v1 if: (success() || failure()) && steps.run_tests.outcome != 'skipped' From b11b1c3d5f4b9ea57874ca2b5006355f58679abe Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Sun, 13 Nov 2022 12:43:21 -0800 Subject: [PATCH 160/161] Updates build.yml & moves TEST variable setup --- .github/workflows/build.yml | 5 ++++- CMakeLists.txt | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1779e486e..f8631bc4e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,7 +99,10 @@ jobs: - name: Run unit tests id: run_tests1 run: | - exit build-ci/library/tests/test-library + if build-ci/library/tests/test-library; then + exit 0 + fi + exit 1 - name: Run lua tests id: run_tests2 run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 29103d495..a1965fe3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -394,6 +394,12 @@ else() endif() endif() +if(BUILD_TESTING) + message("BUILD TESTS: Core, Scripts") + set(BUILD_SCRIPT_TESTS ON FORCE) + set(BUILD_CORE_TESTS ON FORCE) +endif() + # Support linking against external tinyxml # If we find an external tinyxml, set the DFHACK_TINYXML variable to "tinyxml" # Otherwise, set it to "dfhack-tinyxml" @@ -409,12 +415,6 @@ else() set(DFHACK_TINYXML "dfhack-tinyxml") endif() -if(BUILD_TESTING) - message("BUILD TESTS: Core, Scripts") - set(BUILD_SCRIPT_TESTS ON FORCE) - set(BUILD_CORE_TESTS ON FORCE) -endif() - find_package(ZLIB REQUIRED) if(BUILD_CORE_TESTS) From 72ad7a1b0199b8a9664cc987d33a732876395847 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Fri, 25 Nov 2022 09:42:59 -0800 Subject: [PATCH 161/161] Adds googletest/include for test targets only --- CMakeLists.txt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a1965fe3b..5a94274c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -417,10 +417,6 @@ endif() find_package(ZLIB REQUIRED) -if(BUILD_CORE_TESTS) - include_directories(depends/googletest/googletest/include) -endif() - include_directories(depends/protobuf) include_directories(depends/lua/include) include_directories(depends/md5) @@ -437,9 +433,10 @@ if(BUILD_TESTING OR BUILD_CORE_TESTS) macro(dfhack_test name files) message("dfhack_test(${name}, ${files})") add_executable(${name} ${files}) + target_include_directories(${name} PUBLIC depends/googletest/googletest/include) target_link_libraries(${name} dfhack gtest) - add_test(NAME ${name} COMMAND ${name}) set_target_properties(${name} PROPERTIES COMPILE_FLAGS "-Wno-sign-compare") + add_test(NAME ${name} COMMAND ${name}) endmacro() include(CTest) endif()