diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst new file mode 100644 index 000000000..1b72c93df --- /dev/null +++ b/docs/plugins/channel-safely.rst @@ -0,0 +1,76 @@ +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 (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. + +Usage +----- + +:: + + enable channel-safely + channel-safely set + channel-safely enable|disable + 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 +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. + +Features and settings once set will persist until you change them, even if you save and reload your game. + +Examples +-------- + +``channel-safely`` + The plugin reports its configured status. + +``channel-safely runonce`` + 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`` + 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 +-------- + +: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 + 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 +-------- + +: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) +: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) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index dd9b22fb1..7ed7a46ed 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..36c7307e4 --- /dev/null +++ b/plugins/channel-safely/CMakeLists.txt @@ -0,0 +1,9 @@ +project(channel-safely) + +include_directories(include) +SET(SOURCES + 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..52f7e6c40 --- /dev/null +++ b/plugins/channel-safely/channel-groups.cpp @@ -0,0 +1,290 @@ +#include +#include +#include +#include +#include + +#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) { + // 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()); + + // 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()); +} + +// 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 (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) { + TileCache::Get().uncache(map_pos); + remove(map_pos); + } +} + +// builds groupings of adjacent channel designations +void ChannelGroups::scan() { + // 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 + 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); + } + + 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) { + 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) && optimizing(RNG)) { + 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 (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); + } + 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 + 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); + empty_group = false; + } + TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); + add(map_pos); + } else if (groups_map.count(map_pos)) { + remove(map_pos); + } + } + } + } + } + } + // erase the block if we didn't find anything iterating through it + if (empty_group) { + group_blocks.erase(block); + } + } + } + } + } + INFO(groups).print("scan() exits\n"); +} + +// clears out the containers for unloading maps or disabling the plugin +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) { + 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() { + 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++; + } + } +} + +// prints debug info group mappings +void ChannelGroups::debug_map() { + 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-manager.cpp b/plugins/channel-safely/channel-manager.cpp new file mode 100644 index 000000000..e905f2cfb --- /dev/null +++ b/plugins/channel-safely/channel-manager.cpp @@ -0,0 +1,105 @@ +#include +#include +#include + +#include //hash function for df::coord +#include + + +// sets mark flags as necessary, for all designations +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 + 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"); + // 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; + } + } + } 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); + 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 new file mode 100644 index 000000000..d291c0efc --- /dev/null +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -0,0 +1,668 @@ +/* Prevent channeling down into known open space. +Author: Josh Cooper +Created: Aug. 4 2020 +Updated: Nov. 6 2022 + + Enable plugin: + -> build groups + -> manage designations + + Unpause event: + -> build groups + -> manage designations + + Manage Designation(s): + -> for each group in groups: + -> 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) + -> 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 +#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); + DBG_DECLARE(channelsafely, jobs, DebugCategory::LERROR); +} + +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 psetting; +PersistentDataItem pfeature; +const std::string FCONFIG_KEY = std::string(plugin_name) + "/feature"; +const std::string SCONFIG_KEY = std::string(plugin_name) + "/setting"; + +enum FeatureConfigData { + VISION, + MONITOR, + RESURRECT, + INSTADIG +}; + +enum SettingConfigData { + REFRESH_RATE, + MONITOR_RATE, + IGNORE_THRESH, + 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) { + auto nlow = simulate_fall(p); + if (nlow.z < lowest.z) { + lowest = nlow; + } + } + 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; + + 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 { + 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(){ + 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(); + ChannelManager::Get().debug(); + INFO(monitor).print("UnpauseEvent() exits\n"); + } + + void JobStartedEvent(color_ostream &out, void* j) { + if (enabled && World::isFortressMode() && Maps::IsValid()) { + INFO(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"); + 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_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"); + Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted; + } + } + INFO(jobs).print(" <- JobStartedEvent() exits normally\n"); + } + } + + void JobCompletedEvent(color_ostream &out, void* j) { + if (enabled && World::isFortressMode() && Maps::IsValid()) { + INFO(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)) { + // check job outcome + auto block = Maps::getTileBlock(job->pos); + df::coord local(job->pos); + local.x = local.x % 16; + local.y = local.y % 16; + // verify completion + if (TileCache::Get().hasChanged(job->pos, block->tiletype[Coord(local)])) { + // 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 + 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) + if (active_workers.count(job->id)) { + df::unit* worker = active_workers[job->id]; + last_safe[worker->id] = worker->pos; + } + } + } + // clean up + 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; + size_t idx = -1; + idx = df::report::binsearch_index(reports, report_id); + df::report* report = reports.at(idx); + 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) { + 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: + 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; + } + } + } + + 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; + 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"); + 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 + iter = dignow_queue.erase(iter); + DEBUG(plugin).print(">INSTA-DIGGING<\n"); + } + } + UnpauseEvent(); + 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_id_map[j]; + job_id_map.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"); + + // iterate active jobs + for (auto pair: active_jobs) { + 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; + 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 + 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 (config.resurrect) { + endangered_units.emplace(unit, tick); + } else { + // 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; + } + } + 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"); + } + + // Resurrect Dead Workers + if (config.resurrect && tick - last_resurrect_tick >= 1) { + last_resurrect_tick = tick; + + // clean up any "endangered" workers that have been tracked 100 ticks or more + for (auto iter = endangered_units.begin(); iter != endangered_units.end();) { + if (tick - iter->second >= 1200) { //keep watch 1 day + 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 pair : endangered_units) { + auto unit = pair.first; + if (!Units::isAlive(unit)) { + 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); + } + } + } + } + } +} + +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)); + 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) { + CSP::LoadSettings(); + if (enabled) { + std::vector params; + channel_safely(out, params); + } + 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::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); + // 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) { + switch (event) { + case SC_UNPAUSED: + if (enabled && World::isFortressMode() && Maps::IsValid()) { + // manage all designations on unpause + CSP::UnpauseEvent(); + } + 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) { + CSP::OnUpdate(out); + return DFHack::CR_OK; +} + +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) { + 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] == "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) { + 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(" 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); + } + CSP::SaveSettings(); + 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..7547e2564 --- /dev/null +++ b/plugins/channel-safely/include/channel-groups.h @@ -0,0 +1,51 @@ +#pragma once +#include "plugin.h" +#include "channel-jobs.h" + +#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; + +using Group = std::unordered_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 GroupBlocks = std::unordered_set; + using GroupsMap = std::unordered_map; + GroupBlocks group_blocks; + GroupsMap groups_map; + Groups groups; + ChannelJobs &jobs; + std::set free_spots; +protected: + 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 scan(); + 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..3be704aeb --- /dev/null +++ b/plugins/channel-safely/include/channel-jobs.h @@ -0,0 +1,43 @@ +#pragma once +#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; + +/* 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::unordered_set; // job* will exist until it is complete, and likely beyond + Jobs locations; +public: + void load_channel_jobs(); + 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 new file mode 100644 index 000000000..0cd3abfac --- /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.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); + 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(); + } +}; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h new file mode 100644 index 000000000..8bd1de44d --- /dev/null +++ b/plugins/channel-safely/include/inlines.h @@ -0,0 +1,196 @@ +#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 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_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; +} + +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) { + 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 (!DFHack::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 && DFHack::isOpenTerrain(type)) { + // the starting tile is open space, that's obviously not safe + return false; + } else if (!DFHack::isOpenTerrain(type)) { + // a tile after the first one is not open space + return true; + } + pos.z--; + } + 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; +} + +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]; + auto type = job->job_type; + Job::removeJob(job); + switch (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; + } + } +} + +template +void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { + for (const auto &a : c1) { + if (!c2.count(a)) { + 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 new file mode 100644 index 000000000..23b2f8441 --- /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 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; + uint8_t fall_threshold = 1; +}; + +extern Configuration config; +extern int32_t mapx, mapy, mapz; diff --git a/plugins/channel-safely/include/tile-cache.h b/plugins/channel-safely/include/tile-cache.h new file mode 100644 index 000000000..10e91cd46 --- /dev/null +++ b/plugins/channel-safely/include/tile-cache.h @@ -0,0 +1,31 @@ +#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 + +class TileCache { +private: + TileCache() = default; + std::unordered_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) { + return locations.count(pos) && type != locations[pos]; + } +};