diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst index e87a36455..3acbe66cd 100644 --- a/docs/plugins/channel-safely.rst +++ b/docs/plugins/channel-safely.rst @@ -52,18 +52,17 @@ 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). +:rebuild: Rebuild the designation group data. You should also read Troubleshooting. 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) +:risk-averse: Toggles whether to use cave-in prevention. Designations are activated in stages + and their priorities along edges are modified. (default: enabled) +:monitoring: 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 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) Settings -------- @@ -74,3 +73,13 @@ Settings :ignore-threshold: Sets the priority threshold below which designations are processed. You can set to 1 or 0 to effectively disable the scanning. (default: 5) :fall-threshold: Sets the fall threshold beyond which is considered unsafe. (default: 1) + +Troubleshooting +--------------- + +If designations aren't switching correctly, try putting the designations into marker mode. +Then press . (next) or resume. If you're debugging code you'll want these:: + + debugfilter set Info channel manager + debugfilter set Debug channel plugin + debugfilter set Trace channel group diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 95b1d9ba8..284d1788e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -93,7 +93,7 @@ dfhack_plugin(buildingplan buildingplan.cpp LINK_LIBRARIES lua) #dfhack_plugin(changeitem changeitem.cpp) dfhack_plugin(changelayer changelayer.cpp) dfhack_plugin(changevein changevein.cpp) -#add_subdirectory(channel-safely) +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/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp index 8f7f4b117..5338eb6ed 100644 --- a/plugins/channel-safely/channel-groups.cpp +++ b/plugins/channel-safely/channel-groups.cpp @@ -13,12 +13,63 @@ void ChannelJobs::load_channel_jobs() { while (node) { df::job* job = node->item; node = node->next; - if (is_dig_job(job)) { + if (is_channel_job(job)) { locations.emplace(job->pos); + jobs.emplace(job->pos, job); } } } +bool ChannelJobs::has_cavein_conditions(const df::coord &map_pos) { + auto p = map_pos; + auto ttype = *Maps::getTileType(p); + if (!DFHack::isOpenTerrain(ttype)) { + // check shared neighbour for cave-in conditions + df::coord neighbours[4]; + get_connected_neighbours(map_pos, neighbours); + int connectedness = 4; + for (auto n: neighbours) { + if (active.count(n) || DFHack::isOpenTerrain(*Maps::getTileType(n))) { + connectedness--; + } + } + if (!connectedness) { + // do what? + p.z--; + ttype = *Maps::getTileType(p); + if (DFHack::isOpenTerrain(ttype) || DFHack::isFloorTerrain(ttype)) { + return true; + } + } + } + return false; +} + +bool ChannelJobs::possible_cavein(const df::coord &job_pos) { + for (auto iter : active) { + if (iter == job_pos) continue; + if (calc_distance(job_pos, iter) <= 2) { + // find neighbours + df::coord n1[8]; + df::coord n2[8]; + get_neighbours(job_pos, n1); + get_neighbours(iter, n2); + // find shared neighbours + for (int i = 0; i < 7; ++i) { + for (int j = i + 1; j < 8; ++j) { + if (n1[i] == n2[j]) { + if (has_cavein_conditions(n1[i])) { + WARN(jobs).print("Channel-Safely::jobs: Cave-in conditions detected at (" COORD ")\n", COORDARGS(n1[i])); + return true; + } + } + } + } + } + } + return false; +} + // 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 @@ -82,7 +133,7 @@ void ChannelGroups::add(const df::coord &map_pos) { TRACE(groups).print(" -> brand new group\n"); // we create a brand-new group to use group_index = groups.size(); - groups.push_back(Group()); + groups.emplace_back(); group = &groups[group_index]; } } @@ -137,6 +188,7 @@ void ChannelGroups::scan(bool full_scan) { std::set gone_jobs; set_difference(last_jobs, jobs, gone_jobs); set_difference(jobs, last_jobs, new_jobs); + INFO(groups).print("gone jobs: %" PRIu64 "\nnew jobs: %" PRIu64 "\n",gone_jobs.size(), new_jobs.size()); for (auto &pos : new_jobs) { add(pos); } @@ -155,6 +207,7 @@ void ChannelGroups::scan(bool full_scan) { if (!full_scan && !block->flags.bits.designated) { continue; } + df::map_block* block_above = Maps::getBlock(bx, by, z+1); // foreach tile bool empty_group = true; for (int16_t lx = 0; lx < 16; ++lx) { @@ -168,7 +221,14 @@ void ChannelGroups::scan(bool full_scan) { jobs.erase(map_pos); } block->designation[lx][ly].bits.dig = df::tile_dig_designation::No; - } else if (is_dig_designation(block->designation[lx][ly]) || block->occupancy[lx][ly].bits.dig_marked) { + } else if (is_dig_designation(block->designation[lx][ly]) || block->occupancy[lx][ly].bits.dig_marked ) { + // We have a dig designated, or marked. Some of these will not need intervention. + if (block_above && + !is_channel_designation(block->designation[lx][ly]) && + !is_channel_designation(block_above->designation[lx][ly])) { + // if this tile isn't a channel designation, and doesn't have a channel designation above it.. we can skip it + continue; + } 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 @@ -203,6 +263,7 @@ void ChannelGroups::scan(bool full_scan) { void ChannelGroups::clear() { debug_map(); WARN(groups).print(" <- clearing groups\n"); + jobs.clear(); group_blocks.clear(); free_spots.clear(); groups_map.clear(); @@ -269,13 +330,13 @@ size_t ChannelGroups::count(const df::coord &map_pos) const { // prints debug info about the groups stored, and their members void ChannelGroups::debug_groups() { - if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) { + if (DFHack::debug_groups.isEnabled(DebugCategory::LDEBUG)) { int idx = 0; - TRACE(groups).print(" debugging group data\n"); + DEBUG(groups).print(" debugging group data\n"); for (auto &group: groups) { - TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size()); + DEBUG(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); + DEBUG(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); } idx++; } @@ -284,10 +345,10 @@ void ChannelGroups::debug_groups() { // prints debug info group mappings void ChannelGroups::debug_map() { - if (DFHack::debug_groups.isEnabled(DebugCategory::LDEBUG)) { + if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) { 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); + TRACE(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 e905f2cfb..c22bd8a8e 100644 --- a/plugins/channel-safely/channel-manager.cpp +++ b/plugins/channel-safely/channel-manager.cpp @@ -5,6 +5,25 @@ #include //hash function for df::coord #include +df::unit* find_dwarf(const df::coord &map_pos) { + df::unit* nearest = nullptr; + uint32_t distance; + for (auto unit : df::global::world->units.active) { + if (!nearest) { + nearest = unit; + distance = calc_distance(unit->pos, map_pos); + } else if (unit->status.labors[df::unit_labor::MINE]) { + uint32_t d = calc_distance(unit->pos, map_pos); + if (d < distance) { + nearest = unit; + distance = d; + } else if (Maps::canWalkBetween(unit->pos, map_pos)) { + return unit; + } + } + } + return nearest; +} // sets mark flags as necessary, for all designations void ChannelManager::manage_groups() { @@ -30,24 +49,110 @@ void ChannelManager::manage_group(const df::coord &map_pos, bool set_marker_mode INFO(manager).print("manage_group() is done\n"); } -void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool marker_mode) { +void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool marker_mode) const { INFO(manager).print("manage_group()\n"); if (!set_marker_mode) { - if (has_any_groups_above(groups, group)) { - marker_mode = true; - } else { - marker_mode = false; + marker_mode = has_any_groups_above(groups, group); + } + // cavein prevention + bool cavein_possible = false; + uint8_t least_access = 100; + std::unordered_map cavein_candidates; + if (!marker_mode) { + /* To prevent cave-ins we're looking at accessibility of tiles with open space below them + * If it has space below, it has somewhere to fall + * Accessibility tells us how close to a cave-in a tile is, low values are at risk of cave-ins + * To count access, we find a random miner dwarf and count how many tile neighbours they can path to + * */ + // find a dwarf to path from + df::coord miner_pos = find_dwarf(*group.begin())->pos; + + // Analyze designations + for (const auto &pos: group) { + df::coord below(pos); + below.z--; + const auto below_ttype = *Maps::getTileType(below); + // we can skip designations already queued for insta-digging + if (CSP::dignow_queue.count(pos)) continue; + if (DFHack::isOpenTerrain(below_ttype) || DFHack::isFloorTerrain(below_ttype)) { + // the tile below is not solid earth + // we're counting accessibility then dealing with 0 access + DEBUG(manager).print("analysis: cave-in condition found\n"); + INFO(manager).print("(%d) checking accessibility of (" COORD ") from (" COORD ")\n", cavein_possible,COORDARGS(pos),COORDARGS(miner_pos)); + auto access = count_accessibility(miner_pos, pos); + if (access == 0) { + TRACE(groups).print(" has 0 access\n"); + if (config.insta_dig) { + manage_one(pos, true, false); + dig_now(DFHack::Core::getInstance().getConsole(), pos); + } else { + // todo: engage dig management, swap channel designations for dig + } + } else { + WARN(manager).print(" has %d access\n", access); + cavein_possible = config.riskaverse; + cavein_candidates.emplace(pos, access); + least_access = min(access, least_access); + } + } else if (config.insta_dig && isEntombed(miner_pos, pos)) { + manage_one(pos, true, false); + dig_now(DFHack::Core::getInstance().getConsole(), pos); + } } + DEBUG(manager).print("cavein possible(%d)\n" + "%zu candidates\n" + "least access %d\n", cavein_possible, cavein_candidates.size(), least_access); + } + // managing designations + if (!group.empty()) { + DEBUG(manager).print("managing group #%d\n", groups.debugGIndex(*group.begin())); } - for (auto &designation: group) { - manage_one(group, designation, true, marker_mode); + for (auto &pos: group) { + // if no cave-in is possible [or we don't check for], we'll just execute normally and move on + if (!cavein_possible) { + TRACE(manager).print("cave-in evaluated false\n"); + assert(manage_one(pos, true, marker_mode)); + continue; + } + // cavein is only possible if marker_mode is false + // we want to dig the cavein candidates first, the least accessible ones specifically + const static uint8_t MAX = 84; //arbitrary number that indicates the value has changed + const static uint8_t OFFSET = 2; //value has been tweaked to avoid cave-ins whilst activating as many designations as possible + if (CSP::dignow_queue.count(pos) || (cavein_candidates.count(pos) && + least_access < MAX && cavein_candidates[pos] <= least_access+OFFSET)) { + + TRACE(manager).print("cave-in evaluated true and either of dignow or (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET); + df::coord local(pos); + local.x %= 16; + local.y %= 16; + auto block = Maps::ensureTileBlock(pos); + // if we don't find the priority in block_events, it probably means bad things + 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 + auto b = max(0, cavein_candidates[pos] - least_access); + auto v = 1000 + (b * 1700); + DEBUG(manager).print("(" COORD ") 1000+1000(%d) -> %d {least-access: %d}\n",COORDARGS(pos), b, v, least_access); + evT->priority[Coord(local)] = v; + } + } + assert(manage_one(pos, true, false)); + continue; + } + // cavein possible, but we failed to meet the criteria for activation + if (cavein_candidates.count(pos)) { + DEBUG(manager).print("cave-in evaluated true and no dignow and (%d > %d)\n", cavein_candidates[pos], least_access+OFFSET); + } else { + DEBUG(manager).print("cave-in evaluated true and no dignow and pos is not a candidate\n"); + } + assert(manage_one(pos, true, true)); } 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) { +bool ChannelManager::manage_one(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) const { if (Maps::isValidTilePos(map_pos)) { - INFO(manager).print("manage_one(" COORD ")\n", COORDARGS(map_pos)); + TRACE(manager).print("manage_one((" COORD "), %d, %d)\n", COORDARGS(map_pos), set_marker_mode, marker_mode); df::map_block* block = Maps::getTileBlock(map_pos); // we calculate the position inside the block* df::coord local(map_pos); @@ -57,42 +162,28 @@ bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bo // 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"); - // 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)) { - 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; - - } else { - // next search for the designation priority - 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; + if (!marker_mode) { + // marker_mode is set to true if it is unsafe to dig + marker_mode = (!set_marker_mode && + (has_group_above(groups, map_pos) || !is_safe_to_dig_down(map_pos))) || + (set_marker_mode && + is_channel_designation(block->designation[Coord(local)]) && !is_safe_to_dig_down(map_pos)); + } + if (marker_mode) { + if (jobs.count(map_pos)) { + cancel_job(map_pos); } + } else if (!block->flags.bits.designated) { + block->flags.bits.designated = true; } + tile_occupancy.bits.dig_marked = marker_mode; + TRACE(manager).print("marker mode %s\n", marker_mode ? "ENABLED" : "DISABLED"); } 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"); + TRACE(manager).print(" <- manage_one() exits normally\n"); + return true; } return false; } diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp index 8af90c335..adb668468 100644 --- a/plugins/channel-safely/channel-safely-plugin.cpp +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -1,7 +1,10 @@ /* Prevent channeling down into known open space. Author: Josh Cooper Created: Aug. 4 2020 -Updated: Nov. 28 2022 +Updated: Dec. 8 2022 +*/ +/* +This skeletal logic has not been kept up-to-date since ~v0.5 Enable plugin: -> build groups @@ -56,8 +59,6 @@ Updated: Nov. 28 2022 #include #include -#include -#include #include #include #include @@ -98,7 +99,8 @@ enum FeatureConfigData { VISION, MONITOR, RESURRECT, - INSTADIG + INSTADIG, + RISKAVERSE }; enum SettingConfigData { @@ -136,38 +138,13 @@ df::coord simulate_area_fall(const df::coord &pos) { return lowest; } -// executes dig designations for the specified tile coordinates -inline bool dig_now(color_ostream &out, const df::coord &map_pos) { - 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 -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 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; @@ -184,10 +161,11 @@ namespace CSP { void SaveSettings() { if (pfeature.isValid() && psetting.isValid()) { try { - pfeature.ival(MONITOR) = config.monitor_active; + pfeature.ival(MONITOR) = config.monitoring; pfeature.ival(VISION) = config.require_vision; - pfeature.ival(INSTADIG) = config.insta_dig; + pfeature.ival(INSTADIG) = false; //config.insta_dig; pfeature.ival(RESURRECT) = config.resurrect; + pfeature.ival(RISKAVERSE) = config.riskaverse; psetting.ival(REFRESH_RATE) = config.refresh_freq; psetting.ival(MONITOR_RATE) = config.monitor_freq; @@ -209,10 +187,11 @@ namespace CSP { SaveSettings(); } else { try { - config.monitor_active = pfeature.ival(MONITOR); + config.monitoring = pfeature.ival(MONITOR); config.require_vision = pfeature.ival(VISION); - config.insta_dig = pfeature.ival(INSTADIG); + config.insta_dig = false; //pfeature.ival(INSTADIG); config.resurrect = pfeature.ival(RESURRECT); + config.riskaverse = pfeature.ival(RISKAVERSE); config.ignore_threshold = psetting.ival(IGNORE_THRESH); config.fall_threshold = psetting.ival(FALL_THRESH); @@ -227,28 +206,36 @@ namespace CSP { void UnpauseEvent(bool full_scan = false){ CoreSuspender suspend; // we need exclusive access to df memory and this call stack doesn't already have a lock - INFO(monitor).print("UnpauseEvent()\n"); + DEBUG(plugin).print("UnpauseEvent()\n"); ChannelManager::Get().build_groups(full_scan); ChannelManager::Get().manage_groups(); - ChannelManager::Get().debug(); - INFO(monitor).print("UnpauseEvent() exits\n"); + DEBUG(plugin).print("UnpauseEvent() exits\n"); } void JobStartedEvent(color_ostream &out, void* j) { if (enabled && World::isFortressMode() && Maps::IsValid()) { - INFO(jobs).print("JobStartedEvent()\n"); + TRACE(jobs).print("JobStartedEvent()\n"); auto job = (df::job*) j; // validate job type if (ChannelManager::Get().exists(job->pos)) { - WARN(jobs).print(" valid channel job:\n"); + 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)) { + ChannelManager::Get().jobs.mark_active(job->pos); + if (config.riskaverse) { + if (ChannelManager::Get().jobs.possible_cavein(job->pos)) { + cancel_job(job); + ChannelManager::Get().manage_one(job->pos, true, true); + } else { + ChannelManager::Get().manage_group(job->pos, true, false); + } + } 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) { + TRACE(jobs).print(" -> Starting job at (" COORD ")\n", COORDARGS(pos)); + if (config.monitoring || config.resurrect) { job_id_map.emplace(job, job->id); active_jobs.emplace(job->id, job); active_workers[job->id] = worker; @@ -263,16 +250,17 @@ namespace CSP { Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; } } - INFO(jobs).print(" <- JobStartedEvent() exits normally\n"); + TRACE(jobs).print(" <- JobStartedEvent() exits normally\n"); } } void JobCompletedEvent(color_ostream &out, void* j) { if (enabled && World::isFortressMode() && Maps::IsValid()) { - INFO(jobs).print("JobCompletedEvent()\n"); + TRACE(jobs).print("JobCompletedEvent()\n"); auto job = (df::job*) j; // we only care if the job is a channeling one if (ChannelManager::Get().exists(job->pos)) { + ChannelManager::Get().manage_group(job->pos, true, false); // check job outcome auto block = Maps::getTileBlock(job->pos); df::coord local(job->pos); @@ -283,15 +271,14 @@ namespace CSP { // the job can be considered done df::coord below(job->pos); below.z--; - WARN(jobs).print(" -> (" COORD ") is marked done, managing group below.\n", COORDARGS(job->pos)); - // mark done and manage below + DEBUG(jobs).print(" -> (" COORD ") is marked done, managing group below.\n", COORDARGS(job->pos)); + // mark done and manage below (and the rest of the group, if there were cavein candidates) block->designation[Coord(local)].bits.traffic = df::tile_traffic::Normal; ChannelManager::Get().mark_done(job->pos); ChannelManager::Get().manage_group(below); - ChannelManager::Get().debug(); if (config.resurrect) { // this is the only place we can be 100% sure of "safety" - // (excluding deadly enemies that will have arrived) + // (excluding deadly enemies that may have arrived, and future digging) if (active_workers.count(job->id)) { df::unit* worker = active_workers[job->id]; last_safe[worker->id] = worker->pos; @@ -304,7 +291,7 @@ namespace CSP { active_workers.erase(job->id); active_jobs.erase(job->id); } - INFO(jobs).print("JobCompletedEvent() exits\n"); + TRACE(jobs).print("JobCompletedEvent() exits\n"); } } @@ -319,9 +306,9 @@ namespace CSP { switch (report->type) { case announcement_type::CANCEL_JOB: 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) { + if (report->text.find("cancels Dig") != std::string::npos || + 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), @@ -365,9 +352,7 @@ 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); - }; + CoreSuspender suspend; 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; @@ -382,17 +367,18 @@ namespace CSP { if (config.insta_dig) { TRACE(monitor).print(" -> evaluate dignow queue\n"); for (auto iter = dignow_queue.begin(); iter != dignow_queue.end();) { - dig_now(out, *iter); // teleports units to the bottom of a simulated fall + auto map_pos = *iter; + dig_now(out, map_pos); // teleports units to the bottom of a simulated fall + ChannelManager::Get().mark_done(map_pos); iter = dignow_queue.erase(iter); - DEBUG(plugin).print(">INSTA-DIGGING<\n"); } } - UnpauseEvent(); + UnpauseEvent(false); TRACE(monitor).print("OnUpdate() refresh done\n"); } // Clean up stale df::job* - if ((config.monitor_active || config.resurrect) && tick - last_tick >= 1) { + if ((config.monitoring || config.resurrect) && tick - last_tick >= 1) { last_tick = tick; // make note of valid jobs std::unordered_map valid_jobs; @@ -415,7 +401,7 @@ namespace CSP { } // Monitoring Active and Resurrecting Dead - if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) { + if (config.monitoring && tick - last_monitor_tick >= config.monitor_freq) { last_monitor_tick = tick; TRACE(monitor).print("OnUpdate() monitoring now\n"); @@ -427,15 +413,9 @@ namespace CSP { if (!Maps::isValidTilePos(job->pos)) continue; TRACE(monitor).print(" -> check for job in tracking\n"); if (Units::isAlive(unit)) { - if (!config.monitor_active) continue; + if (!config.monitoring) 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 probably safe right now - continue; - } - // check for fall safety if (unit->pos == job->pos && !is_safe_fall(job->pos)) { // unsafe @@ -456,7 +436,7 @@ namespace CSP { Maps::getTileOccupancy(job->pos)->bits.dig_marked = true; // prevent algorithm from re-enabling designation - for (auto &be: Maps::getBlock(job->pos)->block_events) { ; + for (auto &be: Maps::getBlock(job->pos)->block_events) { if (auto bsedp = virtual_cast( be)) { df::coord local(job->pos); @@ -475,7 +455,6 @@ namespace CSP { df::coord lowest = simulate_fall(last_safe[unit->id]); Units::teleport(unit, lowest); } - print_res_msg(unit); } } TRACE(monitor).print("OnUpdate() monitoring done\n"); @@ -504,7 +483,6 @@ namespace CSP { df::coord lowest = simulate_fall(last_safe[unit->id]); Units::teleport(unit, lowest); } - print_res_msg(unit); } } } @@ -547,7 +525,7 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { 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(); + CSP::UnpauseEvent(true); } else if (!enable) { // don't need the groups if the plugin isn't going to be enabled EM::unregisterAll(plugin_self); @@ -562,11 +540,10 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan case SC_UNPAUSED: if (enabled && World::isFortressMode() && Maps::IsValid()) { // manage all designations on unpause - CSP::UnpauseEvent(); + CSP::UnpauseEvent(true); } break; case SC_MAP_LOADED: - case SC_WORLD_LOADED: // cache the map size Maps::getSize(mapx, mapy, mapz); CSP::ClearData(); @@ -609,24 +586,27 @@ command_result channel_safely(color_ostream &out, std::vector ¶ return DFHack::CR_WRONG_USAGE; } try { - if(parameters[1] == "monitor"){ - if (state != config.monitor_active) { - config.monitor_active = state; + if(parameters[1] == "monitoring"){ + if (state != config.monitoring) { + config.monitoring = state; // if this is a fresh start if (state && !config.resurrect) { // we need a fresh start CSP::active_workers.clear(); } } + } else if (parameters[1] == "risk-averse") { + config.riskaverse = state; } else if (parameters[1] == "require-vision") { config.require_vision = state; } else if (parameters[1] == "insta-dig") { - config.insta_dig = state; + //config.insta_dig = state; + config.insta_dig = false; } else if (parameters[1] == "resurrect") { if (state != config.resurrect) { config.resurrect = state; // if this is a fresh start - if (state && !config.monitor_active) { + if (state && !config.monitoring) { // we need a fresh start CSP::active_workers.clear(); } @@ -656,9 +636,10 @@ command_result channel_safely(color_ostream &out, std::vector ¶ } else { out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED."); out.print(" FEATURES:\n"); - out.print(" %-20s\t%s\n", "monitor-active: ", config.monitor_active ? "on." : "off."); + out.print(" %-20s\t%s\n", "risk-averse: ", config.riskaverse ? "on." : "off."); + out.print(" %-20s\t%s\n", "monitoring: ", config.monitoring ? "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", "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); diff --git a/plugins/channel-safely/include/channel-groups.h b/plugins/channel-safely/include/channel-groups.h index 8e3224c98..ef3f5d082 100644 --- a/plugins/channel-safely/include/channel-groups.h +++ b/plugins/channel-safely/include/channel-groups.h @@ -12,7 +12,7 @@ using namespace DFHack; -using Group = std::unordered_set; +using Group = std::set; using Groups = std::vector; /* Used to build groups of adjacent channel designations/jobs @@ -37,6 +37,12 @@ private: protected: void add(const df::coord &map_pos); public: + int debugGIndex(const df::coord &map_pos) const { + if (groups_map.count(map_pos)) { + return groups_map.find(map_pos)->second; + } + return -1; + } explicit ChannelGroups(ChannelJobs &jobs) : jobs(jobs) { groups.reserve(200); } void scan_one(const df::coord &map_pos); void scan(bool full_scan = false); diff --git a/plugins/channel-safely/include/channel-jobs.h b/plugins/channel-safely/include/channel-jobs.h index 3be704aeb..2e6c6a567 100644 --- a/plugins/channel-safely/include/channel-jobs.h +++ b/plugins/channel-safely/include/channel-jobs.h @@ -6,6 +6,7 @@ #include #include +#include using namespace DFHack; @@ -20,23 +21,32 @@ using namespace DFHack; */ class ChannelJobs { private: - friend class ChannelGroup; - using Jobs = std::unordered_set; // job* will exist until it is complete, and likely beyond + std::unordered_map jobs; Jobs locations; + Jobs active; +protected: + bool has_cavein_conditions(const df::coord &map_pos); public: void load_channel_jobs(); + bool possible_cavein(const df::coord &job_pos); + void mark_active(const df::coord &map_pos) { active.emplace(map_pos); } void clear() { + active.clear(); locations.clear(); + jobs.clear(); } int count(const df::coord &map_pos) const { return locations.count(map_pos); } Jobs::iterator erase(const df::coord &map_pos) { + active.erase(map_pos); + jobs.erase(map_pos); auto iter = locations.find(map_pos); if (iter != locations.end()) { return locations.erase(iter); } return iter; } + df::job* find_job(const df::coord &map_pos) const { return jobs.count(map_pos) ? jobs.find(map_pos)->second : nullptr; } 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 323ce8468..582916e06 100644 --- a/plugins/channel-safely/include/channel-manager.h +++ b/plugins/channel-safely/include/channel-manager.h @@ -12,11 +12,11 @@ using namespace DFHack; // Uses GroupData to detect an unsafe work environment class ChannelManager { private: - ChannelJobs jobs; ChannelManager()= default; protected: public: ChannelGroups groups = ChannelGroups(jobs); + ChannelJobs jobs; static ChannelManager& Get(){ static ChannelManager instance; @@ -27,13 +27,13 @@ public: 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 manage_group(const Group &group, bool set_marker_mode = false, bool marker_mode = false) const; + bool manage_one(const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false) const; 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"); - groups.debug_groups(); groups.debug_map(); + groups.debug_groups(); } }; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h index a8686d235..172275778 100644 --- a/plugins/channel-safely/include/inlines.h +++ b/plugins/channel-safely/include/inlines.h @@ -2,21 +2,42 @@ #include "plugin.h" #include "channel-manager.h" +#include +#include +#include #include #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 +#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 uint32_t calc_distance(df::coord p1, df::coord p2) { + // calculate chebyshev (chessboard) distance + uint32_t distance = abs(p2.z - p1.z); + distance += max(abs(p2.x - p1.x), abs(p2.y - p1.y)); + return distance; +} + +inline void get_connected_neighbours(const df::coord &map_pos, df::coord(&neighbours)[4]) { + neighbours[0] = map_pos; + neighbours[1] = map_pos; + neighbours[2] = map_pos; + neighbours[3] = map_pos; + neighbours[0].y--; + neighbours[1].x--; + neighbours[2].x++; + neighbours[3].y++; +} + inline void get_neighbours(const df::coord &map_pos, df::coord(&neighbours)[8]) { neighbours[0] = map_pos; neighbours[1] = map_pos; @@ -36,6 +57,36 @@ inline void get_neighbours(const df::coord &map_pos, df::coord(&neighbours)[8]) neighbours[7].x++; neighbours[7].y++; } +inline uint8_t count_accessibility(const df::coord &unit_pos, const df::coord &map_pos) { + df::coord neighbours[8]; + df::coord connections[4]; + get_neighbours(map_pos, neighbours); + get_connected_neighbours(map_pos, connections); + uint8_t accessibility = Maps::canWalkBetween(unit_pos, map_pos) ? 1 : 0; + for (auto n: neighbours) { + if (Maps::canWalkBetween(unit_pos, n)) { + accessibility++; + } + } + for (auto n : connections) { + if (Maps::canWalkBetween(unit_pos, n)) { + accessibility++; + } + } + return accessibility; +} + +inline bool isEntombed(const df::coord &unit_pos, const df::coord &map_pos) { + if (Maps::canWalkBetween(unit_pos, map_pos)) { + return false; + } + df::coord neighbours[8]; + get_neighbours(map_pos, neighbours); + return std::all_of(neighbours+0, neighbours+8, [&unit_pos](df::coord n) { + return !Maps::canWalkBetween(unit_pos, n); + }); +} + inline bool is_dig_job(const df::job* job) { return job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel; } @@ -92,22 +143,6 @@ inline bool is_safe_to_dig_down(const df::coord &map_pos) { return false; } -inline bool can_reach_designation(const df::coord &start, const df::coord &end) { - if (start != end) { - if (!Maps::canWalkBetween(start, end)) { - df::coord neighbours[8]; - get_neighbours(end, neighbours); - for (auto &pos: neighbours) { - if (Maps::isValidTilePos(pos) && 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; } @@ -135,15 +170,20 @@ inline bool has_any_groups_above(const ChannelGroups &groups, const Group &group } inline void cancel_job(df::job* job) { - if (job != nullptr) { - df::coord &pos = job->pos; + if (job) { + const 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]; auto type = job->job_type; + ChannelManager::Get().jobs.erase(pos); + Job::removeWorker(job); + Job::removePostings(job, true); Job::removeJob(job); + job_block->flags.bits.designated = true; + job_block->occupancy[x][y].bits.dig_marked = true; switch (type) { case job_type::Dig: designation.bits.dig = df::tile_dig_designation::Default; @@ -170,6 +210,96 @@ inline void cancel_job(df::job* job) { } } +inline void cancel_job(const df::coord &map_pos) { + cancel_job(ChannelManager::Get().jobs.find_job(map_pos)); + ChannelManager::Get().jobs.erase(map_pos); +} + +// executes dig designations for the specified tile coordinates +inline bool dig_now(color_ostream &out, const df::coord &map_pos) { + static std::default_random_engine rng; + std::uniform_int_distribution<> dist(0,5); + out.color(color_value::COLOR_YELLOW); + out.print("channel-safely: insta-dig: digging (" COORD ")<\n", COORDARGS(map_pos)); + + df::coord below(map_pos); + below.z--; + auto ttype_below = *Maps::getTileType(below); + if (isOpenTerrain(ttype_below) || isFloorTerrain(ttype_below)) { + *Maps::getTileType(map_pos) = tiletype::OpenSpace; + } else { + auto ttype_p = Maps::getTileType(map_pos); + if (isSoilMaterial(*ttype_p)) { + switch(dist(rng)) { + case 0: + *ttype_p = tiletype::SoilFloor1; + break; + case 1: + *ttype_p = tiletype::SoilFloor2; + break; + case 2: + *ttype_p = tiletype::SoilFloor3; + break; + case 3: + *ttype_p = tiletype::SoilFloor4; + break; + default: + *ttype_p = tiletype::SoilFloor1; + break; + } + } else if (isStoneMaterial(*ttype_p)) { + switch(dist(rng)) { + case 0: + *ttype_p = tiletype::FeatureFloor1; + break; + case 1: + *ttype_p = tiletype::FeatureFloor2; + break; + case 2: + *ttype_p = tiletype::FeatureFloor3; + break; + case 3: + *ttype_p = tiletype::FeatureFloor4; + break; + default: + *ttype_p = tiletype::MineralFloor1; + break; + } + } else { + out.print("Unknown type\n"); + return false; + } + } + + return true; + /* + 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 +inline void resurrect(color_ostream &out, const int32_t &unit) { + out.color(DFHack::COLOR_RED); + out.print("channel-safely: resurrecting [id: %d]\n", unit); + std::vector params{"-r", "--unit", std::to_string(unit)}; + Core::getInstance().runCommand(out,"full-heal", params); +} + template void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { for (const auto &a : c1) { diff --git a/plugins/channel-safely/include/plugin.h b/plugins/channel-safely/include/plugin.h index 23b2f8441..336d944ab 100644 --- a/plugins/channel-safely/include/plugin.h +++ b/plugins/channel-safely/include/plugin.h @@ -9,7 +9,8 @@ namespace DFHack { } struct Configuration { - bool monitor_active = false; + bool riskaverse = true; + bool monitoring = false; bool require_vision = true; bool insta_dig = false; bool resurrect = false;