Merge pull request #2111 from cppcooper/channel-safely

Channel safely
develop
Myk 2022-11-23 12:41:50 -08:00 committed by GitHub
commit 344ed4312b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1532 additions and 0 deletions

@ -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 <setting> <value>
channel-safely enable|disable <feature>
channel-safely <command>
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 <Designations_menu#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)

@ -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)

@ -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)

@ -0,0 +1,290 @@
#include <channel-groups.h>
#include <tile-cache.h>
#include <inlines.h>
#include <modules/Maps.h>
#include <df/block_square_event_designation_priorityst.h>
#include <random>
// 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<df::block_square_event_designation_priorityst>(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<df::coord> last_jobs;
for (auto &pos : jobs) {
last_jobs.emplace(pos);
}
jobs.load_channel_jobs();
// transpose channel jobs to
std::set<df::coord> new_jobs;
std::set<df::coord> 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<df::block_square_event_designation_priorityst>(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);
}
}
}

@ -0,0 +1,105 @@
#include <channel-manager.h>
#include <tile-cache.h>
#include <inlines.h>
#include <modules/EventManager.h> //hash function for df::coord
#include <df/block_square_event_designation_priorityst.h>
// 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);
}

@ -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 <plugin.h>
#include <inlines.h>
#include <channel-manager.h>
#include <tile-cache.h>
#include <Debug.h>
#include <LuaTools.h>
#include <LuaWrapper.h>
#include <PluginManager.h>
#include <modules/EventManager.h>
#include <modules/Units.h>
#include <df/world.h>
#include <df/report.h>
#include <df/tile_traffic.h>
#include <df/block_square_event_designation_priorityst.h>
#include <cinttypes>
#include <unordered_map>
#include <unordered_set>
// 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<std::string> params{"-r", "--unit", std::to_string(unit)};
Core::getInstance().runCommand(out,"full-heal", params);
}
namespace CSP {
std::unordered_map<df::unit*, int32_t> endangered_units;
std::unordered_map<df::job*, int32_t> job_id_map;
std::unordered_map<int32_t, df::job*> active_jobs;
std::unordered_map<int32_t, df::unit*> active_workers;
std::unordered_map<int32_t, df::coord> last_safe;
std::unordered_set<df::coord> 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<df::report*> &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<df::unit*> 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<int32_t, df::job*> 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<df::job*> 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<df::block_square_event_designation_priorityst>(
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<std::string> &parameters);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector<PluginCommand> &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<std::string> 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<std::string> &parameters) {
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;
}

@ -0,0 +1,51 @@
#pragma once
#include "plugin.h"
#include "channel-jobs.h"
#include <df/map_block.h>
#include <df/coord.h>
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <vector>
#include <unordered_map>
#include <unordered_set>
using namespace DFHack;
using Group = std::unordered_set<df::coord>;
using Groups = std::vector<Group>;
/* 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<df::map_block*>;
using GroupsMap = std::unordered_map<df::coord, int>;
GroupBlocks group_blocks;
GroupsMap groups_map;
Groups groups;
ChannelJobs &jobs;
std::set<int> 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();
};

@ -0,0 +1,43 @@
#pragma once
#include <PluginManager.h>
#include <modules/Job.h>
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <df/world.h>
#include <df/job.h>
#include <unordered_set>
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<df::coord>; // 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(); }
};

@ -0,0 +1,39 @@
#pragma once
#include <PluginManager.h>
#include <modules/World.h>
#include <modules/Maps.h>
#include <modules/Job.h>
#include <df/map_block.h>
#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();
}
};

@ -0,0 +1,196 @@
#pragma once
#include "plugin.h"
#include "channel-manager.h"
#include <modules/Maps.h>
#include <df/job.h>
#include <TileTypes.h>
#include <cinttypes>
#include <unordered_set>
#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<df::coord> 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<class Ctr1, class Ctr2, class Ctr3>
void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) {
for (const auto &a : c1) {
if (!c2.count(a)) {
c3.emplace(a);
}
}
}
template<class Ctr1, class Ctr2, class Ctr3>
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);
}
}
}

@ -0,0 +1,23 @@
#pragma once
#include <Debug.h>
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;

@ -0,0 +1,31 @@
#pragma once
#include <modules/Maps.h>
#include <df/coord.h>
#include <df/tiletype.h>
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <unordered_map>
class TileCache {
private:
TileCache() = default;
std::unordered_map<df::coord, df::tiletype> 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];
}
};