From 92537bc4596392188863b15b38e1119bc575467b Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 7 Nov 2022 14:54:39 -0800 Subject: [PATCH] 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;