Implements plugin: channel-safely v0.1

develop
Josh Cooper 2022-11-06 00:12:35 -07:00
parent edfaf5f9f2
commit 22414f26fa
12 changed files with 1269 additions and 0 deletions

@ -0,0 +1,62 @@
channel-safely
==============
.. dfhack-tool::
:summary: Auto-manage channel designations to keep dwarves safe
:tags: fort auto
Multi-level channel projects can be dangerous, and managing the safety of your
dwarves throughout the completion of such projects can be difficult and time
consuming. This plugin keeps your dwarves safe (while channeling) so you don't
have to. Now you can focus on designing your dwarven cities with the deep chasms
they were meant to have.
Usage
-----
::
enable channel-safely
channel-safely set <setting> <value>
channel-safely enable|disable <feature>
channel-safely run once
When enabled the map will be scanned for channel designations which will be grouped
together based on adjacency and z-level. These groups will then be analyzed for safety
and designations deemed unsafe will be put into :wiki:`Marker Mode <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.
Examples
--------
``channel-safely``
The plugin reports its configured status.
``channel-safely run once``
Runs the safety procedures once. You can use this if you prefer initiating scans manually.
``channel-safely disable require-vision``
Allows the plugin to read all tiles, including the ones your dwarves know nothing about.
``channel-safely enable monitor-active``
Enables monitoring active channel digging jobs. Meaning that if another unit it present
or the tile below becomes open space the job will be paused or canceled (respectively).
``channel-safely set ignore-threshold 3``
Configures the plugin to ignore designations equal to or above priority 3 designations.
Features
--------
:monitor-active: Toggle whether to monitor the conditions of active digs. (default: disabled)
:require-vision: Toggle whether the dwarves need vision of a tile before channeling to it can be deemed unsafe. (default: enabled)
:insta-dig: Toggle whether to use insta-digging on unreachable designations. (default: disabled)
Settings
--------
:refresh-freq: The rate at which full refreshes are performed.
This can be expensive if you're undertaking many mega projects. (default:600, twice a day)
:monitor-freq: The rate at which active jobs are monitored.
todo: this can have a massive impact? (default:10)
:ignore-threshold: Sets the priority threshold below which designations are processed. You can set to 1 or 0 to
effectively disable the scanning. (default: 7)
:fall-threshold: Sets the fall threshold beyond which is considered unsafe. (default: 1)

@ -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,10 @@
project(channel-safely)
include_directories(include)
SET(SOURCES
channel-jobs.cpp
channel-groups.cpp
channel-manager.cpp
channel-safely-plugin.cpp)
dfhack_plugin(${PROJECT_NAME} ${SOURCES} LINK_LIBRARIES lua)

@ -0,0 +1,253 @@
#include <channel-groups.h>
#include <inlines.h>
#include <modules/Maps.h>
#include <df/block_square_event_designation_priorityst.h>
#include <random>
// scans the map for channel designations
void ChannelGroups::scan_map() {
static std::default_random_engine RNG(0);
static std::bernoulli_distribution optimizing(0.3333);
DEBUG(groups).print(" scan_map()\n");
// foreach block
for (int32_t z = mapz - 1; z >= 0; --z) {
for (int32_t by = 0; by < mapy; ++by) {
for (int32_t bx = 0; bx < mapx; ++bx) {
// the block
if (df::map_block* block = Maps::getBlock(bx, by, z)) {
// skip this block?
if (!block->flags.bits.designated && optimizing(RNG)) {
// todo: add remainder of block width onto bx
TRACE(groups).print(" skipping this block, it has no designations\n");
continue;
}
// foreach tile
for (int16_t lx = 0; lx < 16; ++lx) {
for (int16_t ly = 0; ly < 16; ++ly) {
// the tile, check if it has a channel designation
if (is_dig_designation(block->designation[lx][ly])) {
for (df::block_square_event* event: block->block_events) {
if (auto evT = virtual_cast<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) {
df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z);
TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos));
add(map_pos);
}
}
}
}
}
}
}
}
}
}
INFO(groups).print("scan_map() exits\n");
}
// scans a single tile for channel designations
void ChannelGroups::scan_one(const df::coord &map_pos) {
df::map_block* block = Maps::getTileBlock(map_pos);
int16_t lx = map_pos.x % 16;
int16_t ly = map_pos.y % 16;
if (is_dig_designation(block->designation[lx][ly])) {
for (df::block_square_event* event: block->block_events) {
if (auto evT = virtual_cast<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);
}
}
}
}
}
// adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found
void ChannelGroups::add(const df::coord &map_pos) {
// if we've already added this, we don't need to do it again
if (groups_map.count(map_pos)) {
return;
}
/* We need to add map_pos to an existing group if possible...
* So what we do is we look at neighbours to see if they belong to one or more existing groups
* If there is more than one group, we'll be merging them
*/
df::coord neighbors[8];
get_neighbours(map_pos, neighbors);
Group* group = nullptr;
int group_index = -1;
DEBUG(groups).print(" add(" COORD ")\n", COORDARGS(map_pos));
// and so we begin iterating the neighbours
for (auto &neighbour: neighbors) {
// go to the next neighbour if this one doesn't have a group
if (!groups_map.count(neighbour)) {
TRACE(groups).print(" -> neighbour is not designated\n");
continue;
}
// get the group, since at least one exists... then merge any additional into that one
if (!group){
TRACE(groups).print(" -> group* has no valid state yet\n");
group_index = groups_map.find(neighbour)->second;
group = &groups.at(group_index);
} else {
TRACE(groups).print(" -> group* has an existing state\n");
// we don't do anything if the found group is the same as the existing group
auto index2 = groups_map.find(neighbour)->second;
if (group_index != index2) {
// we already have group "prime" if you will, so we're going to merge the new find into prime
Group &group2 = groups.at(index2);
// merge
TRACE(groups).print(" -> merging two groups. group 1 size: %zu. group 2 size: %zu\n", group->size(),
group2.size());
for (auto pos2: group2) {
group->emplace(pos2);
groups_map[pos2] = group_index;
}
group2.clear();
free_spots.emplace(index2);
TRACE(groups).print(" merged size: %zu\n", group->size());
}
}
}
// if we haven't found at least one group by now we need to create/get one
if (!group) {
TRACE(groups).print(" -> no merging took place\n");
// first we check if we can re-use a group that's been freed
if (!free_spots.empty()) {
TRACE(groups).print(" -> use recycled old group\n");
// first element in a set is always the lowest value, so we re-use from the front of the vector
group_index = *free_spots.begin();
group = &groups[group_index];
free_spots.erase(free_spots.begin());
} else {
TRACE(groups).print(" -> brand new group\n");
// we create a brand-new group to use
group_index = groups.size();
groups.push_back(Group());
group = &groups[group_index];
}
}
// puts the "add" in "ChannelGroups::add"
group->emplace(map_pos);
DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size());
// ERR(groups).print("\n\n\nDEBUG MAPPINGS:\n");
// debug_map();
// DEBUG(groups).flush();
// we may have performed a merge, so we update all the `coord -> group index` mappings
for (auto &wpos: *group) {
groups_map[wpos] = group_index;
}
DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size());
// ERR(groups).print("\n\n\nDEBUG MAPPINGS:\n");
// debug_map();
// DEBUG(groups).flush();
}
// builds groupings of adjacent channel designations
void ChannelGroups::build() {
clear();
// iterate over each job, finding channel jobs
jobs.load_channel_jobs();
// transpose channel jobs to
for (auto &pos : jobs) {
add(pos);
}
scan_map();
}
// clears out the containers for unloading maps or disabling the plugin
void ChannelGroups::clear() {
debug_map();
WARN(groups).print(" <- clearing groups\n");
free_spots.clear();
groups_map.clear();
for(size_t i = 0; i < groups.size(); ++i) {
groups[i].clear();
free_spots.emplace(i);
}
}
// erases map_pos from its group, and deletes mappings IFF the group is empty
void ChannelGroups::remove(const df::coord &map_pos) {
// we don't need to do anything if the position isn't in a group (granted, that should never be the case)
INFO(groups).print(" remove()\n");
if (groups_map.count(map_pos)) {
INFO(groups).print(" -> found group\n");
// get the group, and map_pos' block*
int group_index = groups_map.find(map_pos)->second;
Group &group = groups[group_index];
// erase map_pos from the group
INFO(groups).print(" -> erase(" COORD ")\n", COORDARGS(map_pos));
group.erase(map_pos);
groups_map.erase(map_pos);
// clean up if the group is empty
if (group.empty()) {
WARN(groups).print(" -> group is empty\n");
// erase `coord -> group group_index` mappings
for (auto iter = groups_map.begin(); iter != groups_map.end();) {
if (group_index == iter->second) {
iter = groups_map.erase(iter);
continue;
}
++iter;
}
// flag the `groups` group_index as available
free_spots.insert(group_index);
}
}
INFO(groups).print(" remove() exits\n");
}
// finds a group corresponding to a map position if one exists
Groups::const_iterator ChannelGroups::find(const df::coord &map_pos) const {
const auto iter = groups_map.find(map_pos);
if (iter != groups_map.end()) {
return groups.begin() + iter->second;
}
return groups.end();
}
// returns an iterator to the first element stored
Groups::const_iterator ChannelGroups::begin() const {
return groups.begin();
}
// returns an iterator to after the last element stored
Groups::const_iterator ChannelGroups::end() const {
return groups.end();
}
// returns a count of 0 or 1 depending on whether map_pos is mapped to a group
size_t ChannelGroups::count(const df::coord &map_pos) const {
return groups_map.count(map_pos);
}
// prints debug info about the groups stored, and their members
void ChannelGroups::debug_groups() {
// int idx = 0;
// TRACE(groups).print(" debugging group data\n");
// for (auto &group : groups) {
// TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size());
// for (auto &pos : group) {
// TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z);
// }
// idx++;
// }
}
// prints debug info group mappings
void ChannelGroups::debug_map() {
// INFO(groups).print("Group Mappings: %zu\n", groups_map.size());
// for (auto &pair : groups_map) {
// DEBUG(groups).print(" map[" COORD "] = %d\n",COORDARGS(pair.first), pair.second);
// }
}

@ -0,0 +1,46 @@
#include <channel-jobs.h>
#include <inlines.h>
#include <df/world.h>
#include <df/job.h>
// iterates the DF job list and adds channel jobs to the `jobs` container
void ChannelJobs::load_channel_jobs() {
jobs.clear();
df::job_list_link* node = df::global::world->jobs.list.next;
while (node) {
df::job* job = node->item;
node = node->next;
if (is_dig_job(job)) {
jobs.emplace(job->pos);
}
}
}
// clears the container
void ChannelJobs::clear() {
jobs.clear();
}
// finds and erases a job corresponding to a map position, then returns the iterator following the element removed
std::set<df::coord>::iterator ChannelJobs::erase(const df::coord &map_pos) {
auto iter = jobs.find(map_pos);
if (iter != jobs.end()) {
return jobs.erase(iter);
}
return iter;
}
// finds a job corresponding to a map position if one exists
std::set<df::coord>::const_iterator ChannelJobs::find(const df::coord &map_pos) const {
return jobs.find(map_pos);
}
// returns an iterator to the first element stored
std::set<df::coord>::const_iterator ChannelJobs::begin() const {
return jobs.begin();
}
// returns an iterator to after the last element stored
std::set<df::coord>::const_iterator ChannelJobs::end() const {
return jobs.end();
}

@ -0,0 +1,99 @@
#include <channel-manager.h>
#include <inlines.h>
#include <df/block_square_event_designation_priorityst.h>
/**
blocks[48][96][135]: <map_block: 0x7fff8e587200>
blocks[48][96][135].default_liquid.hidden: false
blocks[48][96][135].designation[10][0].hidden: false
* */
// sets mark flags as necessary, for all designations
void ChannelManager::manage_all() {
INFO(manager).print("manage_all()\n");
// make sure we've got a fort map to analyze
if (World::isFortressMode() && Maps::IsValid()) {
// iterate the groups we built/updated
for (const auto &group: groups) {
manage_group(group, true, has_any_groups_above(groups, group));
}
}
}
void ChannelManager::manage_group(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) {
INFO(manager).print("manage_group(" COORD ")\n ", COORDARGS(map_pos));
if (!groups.count(map_pos)) {
groups.scan_one(map_pos);
}
auto iter = groups.find(map_pos);
if (iter != groups.end()) {
manage_group(*iter, set_marker_mode, marker_mode);
}
INFO(manager).print("manage_group() is done\n");
}
void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool marker_mode) {
INFO(manager).print("manage_group()\n");
if (!set_marker_mode) {
if (has_any_groups_above(groups, group)) {
marker_mode = true;
} else {
marker_mode = false;
}
}
for (auto &designation: group) {
manage_one(group, designation, true, marker_mode);
}
INFO(manager).print("manage_group() is done\n");
}
bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode, bool marker_mode) {
if (Maps::isValidTilePos(map_pos)) {
INFO(manager).print("manage_one(" COORD ")\n", COORDARGS(map_pos));
df::map_block* block = Maps::getTileBlock(map_pos);
// we calculate the position inside the block*
df::coord local(map_pos);
local.x = local.x % 16;
local.y = local.y % 16;
df::tile_occupancy &tile_occupancy = block->occupancy[Coord(local)];
// ensure that we aren't on the top-most layers
if (map_pos.z < mapz - 3) {
// do we already know whether to set marker mode?
if (set_marker_mode) {
DEBUG(manager).print(" -> marker_mode\n");
tile_occupancy.bits.dig_marked = marker_mode;
jobs.erase(map_pos);
return true;
} else {
// next search for the designation priority
for (df::block_square_event* event: block->block_events) {
if (auto evT = virtual_cast<df::block_square_event_designation_priorityst>(event)) {
// we want to let the user keep some designations free of being managed
if (evT->priority[Coord(local)] < 1000 * config.ignore_threshold) {
DEBUG(manager).print(" if(has_groups_above())\n");
// check that the group has no incomplete groups directly above it
if (has_group_above(groups, map_pos)) {
DEBUG(manager).print(" has_groups_above: setting marker mode\n");
tile_occupancy.bits.dig_marked = true;
jobs.erase(map_pos);
WARN(manager).print(" <- manage_one() exits normally\n");
return true;
}
}
}
}
}
} else {
// if we are though, it should be totally safe to dig
tile_occupancy.bits.dig_marked = false;
}
WARN(manager).print(" <- manage_one() exits normally\n");
}
return false;
}
void ChannelManager::mark_done(const df::coord &map_pos) {
groups.remove(map_pos);
jobs.erase(map_pos);
}

@ -0,0 +1,509 @@
/* Prevent channeling down into known open space.
Author: Josh Cooper
Created: Aug. 4 2020
Updated: Nov. 1 2022
Enable plugin:
-> build groups
-> manage designations
Unpause event:
-> build groups
-> manage designations
Manage Designation(s):
-> for each group in groups:
-> for each designation in this group:
->
Job started event:
-> validate job type (channel)
-> check pathing:
-> Can: add job/worker to tracking
-> Can: set tile to restricted
-> Cannot: remove worker
-> Cannot: insta-dig & delete job
-> Cannot: set designation to Marker Mode (no insta-digging)
OnUpdate:
-> check worker location:
-> CanFall: check if a fall would be safe:
-> Safe: do nothing
-> Unsafe: remove worker
-> Unsafe: insta-dig & delete job (presumes the job is only accessible from directly on the tile)
-> Unsafe: set designation to Marker Mode (no insta-digging)
-> check tile occupancy:
-> HasUnit: check if a fall would be safe:
-> Safe: do nothing, let them fall
-> Unsafe: remove worker for 1 tick (test if this "pauses" or cancels the job)
-> Unsafe: Add feature to teleport unit?
Job completed event:
-> validate job type (channel)
-> verify completion:
-> IsOpenSpace: mark done
-> IsOpenSpace: manage tile below
-> NotOpenSpace: check for designation
-> HasDesignation: do nothing
-> NoDesignation: mark done (erases from group)
-> NoDesignation: manage tile below
*/
#include <plugin.h>
#include <inlines.h>
#include <channel-manager.h>
#include <Debug.h>
#include <LuaTools.h>
#include <LuaWrapper.h>
#include <PluginManager.h>
#include <modules/EventManager.h>
#include <cinttypes>
#include <unordered_map>
#include <unordered_set>
#include <modules/Units.h>
#include <df/report.h>
#include <df/tile_traffic.h>
#include <df/world.h>
// Debugging
namespace DFHack {
DBG_DECLARE(channelsafely, monitor, DebugCategory::LINFO);
DBG_DECLARE(channelsafely, manager, DebugCategory::LINFO);
DBG_DECLARE(channelsafely, groups, DebugCategory::LINFO);
DBG_DECLARE(channelsafely, jobs, DebugCategory::LINFO);
}
DFHACK_PLUGIN("channel-safely");
DFHACK_PLUGIN_IS_ENABLED(enabled);
REQUIRE_GLOBAL(world);
namespace EM = EventManager;
using namespace DFHack;
using namespace EM::EventType;
int32_t mapx, mapy, mapz;
Configuration config;
PersistentDataItem pconfig;
const std::string CONFIG_KEY = std::string(plugin_name) + "/config";
//std::unordered_set<int32_t> active_jobs;
#include <df/block_square_event_designation_priorityst.h>
enum ConfigurationData {
MONITOR,
VISION,
INSTADIG,
IGNORE_THRESH,
FALL_THRESH,
REFRESH_RATE,
MONITOR_RATE
};
inline void saveConfig() {
if (pconfig.isValid()) {
pconfig.ival(MONITOR) = config.monitor_active;
pconfig.ival(VISION) = config.require_vision;
pconfig.ival(INSTADIG) = config.insta_dig;
pconfig.ival(REFRESH_RATE) = config.refresh_freq;
pconfig.ival(MONITOR_RATE) = config.monitor_freq;
pconfig.ival(IGNORE_THRESH) = config.ignore_threshold;
pconfig.ival(FALL_THRESH) = config.fall_threshold;
}
}
// executes dig designations for the specified tile coordinates
inline bool dig_now(color_ostream &out, const df::coord &map_pos) {
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 2) ||
!Lua::PushModulePublic(out, L, "plugins.dig-now", "dig_now_tile"))
return false;
Lua::Push(L, map_pos);
if (!Lua::SafeCall(out, L, 1, 1))
return false;
return lua_toboolean(L, -1);
}
namespace CSP {
std::unordered_map<int32_t, int32_t> active_workers;
std::unordered_map<int32_t, df::coord> last_safe;
std::unordered_set<df::coord> dignow_queue;
void UnpauseEvent(){
INFO(monitor).print("UnpauseEvent()\n");
ChannelManager::Get().build_groups();
INFO(monitor).print("after building groups\n");
ChannelManager::Get().debug();
ChannelManager::Get().manage_all();
INFO(monitor).print("UnpauseEvent() exits\n");
ChannelManager::Get().debug();
}
void JobStartedEvent(color_ostream &out, void* p) {
if (config.monitor_active) {
if (enabled && World::isFortressMode() && Maps::IsValid()) {
INFO(monitor).print("JobStartedEvent()\n");
auto job = (df::job*) p;
// validate job type
if (is_dig_job(job)) {
DEBUG(monitor).print(" valid channel job:\n");
df::unit* worker = Job::getWorker(job);
// there is a valid worker (living citizen) on the job? right..
if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) {
DEBUG(monitor).print(" valid worker:\n");
df::coord local(job->pos);
local.x = local.x % 16;
local.y = local.y % 16;
// check pathing exists to job
if (Maps::canWalkBetween(worker->pos, job->pos)) {
DEBUG(monitor).print(" can path from (" COORD ") to (" COORD ")\n",
COORDARGS(worker->pos), COORDARGS(job->pos));
// track workers on jobs
active_workers.emplace(job->id, Units::findIndexById(Job::getWorker(job)->id));
// set tile to restricted
TRACE(monitor).print(" setting job tile to restricted\n");
Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted;
} else {
DEBUG(monitor).print(" no path exists to job:\n");
// if we can't get there, then we should remove the worker and cancel the job (restore tile designation)
Job::removeWorker(job);
cancel_job(job);
if (!config.insta_dig) {
TRACE(monitor).print(" setting marker mode for (" COORD ")\n", COORDARGS(job->pos));
// set to marker mode
auto occupancy = Maps::getTileOccupancy(job->pos);
if (!occupancy) {
WARN(monitor).print(" <X> Could not acquire tile occupancy*\n");
return;
}
occupancy->bits.dig_marked = true;
// prevent algorithm from re-enabling designation
df::map_block* block = Maps::getTileBlock(job->pos);
if (!block) {
WARN(monitor).print(" <X> Could not acquire block*\n");
return;
}
for (auto &be: block->block_events) { ;
if (auto bsedp = virtual_cast<df::block_square_event_designation_priorityst>(be)) {
TRACE(monitor).print(" re-setting priority\n");
bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1;
}
}
} else {
TRACE(monitor).print(" deleting job, and queuing insta-dig)\n");
// queue digging the job instantly
dignow_queue.emplace(job->pos);
}
}
}
}
INFO(monitor).print(" <- JobStartedEvent() exits normally\n");
}
}
}
void JobCompletedEvent(color_ostream &out, void* job_ptr) {
if (config.monitor_active) {
INFO(monitor).print("JobCompletedEvent()\n");
if (enabled && World::isFortressMode() && Maps::IsValid()) {
auto job = (df::job*) job_ptr;
// we only care if the job is a channeling one
if (is_dig_job(job)) {
// untrack job/worker
active_workers.erase(job->id);
// check job outcome
df::coord local(job->pos);
auto block = Maps::getTileBlock(local);
local.x = local.x % 16;
local.y = local.y % 16;
// verify completion
if (isOpenTerrain(block->tiletype[local.x][local.y])
|| block->designation[local.x][local.y].bits.dig != df::enums::tile_dig_designation::Channel) {
// the job can be considered done
df::coord below(job->pos);
below.z--;
WARN(monitor).print(" -> Marking tile done and managing the group below.\n");
// mark done and manage below
Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Normal;
ChannelManager::Get().mark_done(job->pos);
ChannelManager::Get().manage_group(below);
ChannelManager::Get().debug();
Job::removeJob(job);
}
}
}
INFO(monitor).print("JobCompletedEvent() exits\n");
}
}
void OnUpdate(color_ostream &out) {
if (enabled && World::isFortressMode() && Maps::IsValid() && !World::ReadPauseState()) {
static int32_t last_monitor_tick = df::global::world->frame_counter;
static int32_t last_refresh_tick = df::global::world->frame_counter;
int32_t tick = df::global::world->frame_counter;
if (tick - last_refresh_tick >= config.refresh_freq) {
last_refresh_tick = tick;
TRACE(monitor).print("OnUpdate()\n");
UnpauseEvent();
}
if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) {
last_monitor_tick = tick;
TRACE(monitor).print("OnUpdate()\n");
for (df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next) {
df::job* job = link->item;
if (job) {
auto iter = active_workers.find(job->id);
TRACE(monitor).print(" -> check for job in tracking\n");
if (iter != active_workers.end()) {
df::unit* unit = df::global::world->units.active[iter->second];
TRACE(monitor).print(" -> compare positions of worker and job\n");
// check if fall is possible
if (unit->pos == job->pos) {
// can fall, is safe?
TRACE(monitor).print(" equal -> check if safe fall\n");
if (!is_safe_fall(job->pos)) {
// unsafe
Job::removeWorker(job);
if (config.insta_dig) {
TRACE(monitor).print(" -> insta-dig\n");
// delete the job
Job::removeJob(job);
// queue digging the job instantly
dignow_queue.emplace(job->pos);
// worker is currently in the air
Units::teleport(unit, last_safe[unit->id]);
last_safe.erase(unit->id);
} else {
TRACE(monitor).print(" -> set marker mode\n");
// set to marker mode
Maps::getTileOccupancy(job->pos)->bits.dig_marked = true;
// prevent algorithm from re-enabling designation
for (auto &be: Maps::getBlock(job->pos)->block_events) { ;
if (auto bsedp = virtual_cast<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;
}
}
}
}
} else {
TRACE(monitor).print(" -> save safe position\n");
// worker is perfectly safe right now
last_safe[unit->id] = unit->pos;
}
}
}
}
TRACE(monitor).print(" -> evaluate dignow queue\n");
for (const df::coord &pos: dignow_queue) {
if (!has_unit(Maps::getTileOccupancy(pos))) {
dig_now(out, pos);
} else {
// todo: teleport?
//Units::teleport()
}
}
TRACE(monitor).print("OnUpdate() exits\n");
}
}
}
}
command_result channel_safely(color_ostream &out, std::vector<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));
DBG_NAME(monitor).allowed(DFHack::DebugCategory::LERROR);
DBG_NAME(manager).allowed(DFHack::DebugCategory::LERROR);
DBG_NAME(groups).allowed(DFHack::DebugCategory::LERROR);
DBG_NAME(jobs).allowed(DFHack::DebugCategory::LERROR);
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
EM::unregisterAll(plugin_self);
return CR_OK;
}
DFhackCExport command_result plugin_load_data (color_ostream &out) {
pconfig = World::GetPersistentData(CONFIG_KEY);
if (!pconfig.isValid()) {
pconfig = World::AddPersistentData(CONFIG_KEY);
saveConfig();
} else {
config.monitor_active = pconfig.ival(MONITOR);
config.require_vision = pconfig.ival(VISION);
config.insta_dig = pconfig.ival(INSTADIG);
config.refresh_freq = pconfig.ival(REFRESH_RATE);
config.monitor_freq = pconfig.ival(MONITOR_RATE);
config.ignore_threshold = pconfig.ival(IGNORE_THRESH);
config.fall_threshold = pconfig.ival(FALL_THRESH);
}
return DFHack::CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
if (enable && !enabled) {
// register events to check jobs / update tracking
EM::EventHandler jobStartHandler(CSP::JobStartedEvent, 0);
EM::EventHandler jobCompletionHandler(CSP::JobCompletedEvent, 0);
EM::registerListener(EventType::JOB_STARTED, jobStartHandler, plugin_self);
EM::registerListener(EventType::JOB_COMPLETED, jobCompletionHandler, plugin_self);
// manage designations to start off (first time building groups [very important])
out.print("channel-safely: enabled!\n");
CSP::UnpauseEvent();
} else if (!enable) {
// don't need the groups if the plugin isn't going to be enabled
EM::unregisterAll(plugin_self);
out.print("channel-safely: disabled!\n");
}
enabled = enable;
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (enabled && World::isFortressMode() && Maps::IsValid()) {
switch (event) {
case SC_MAP_LOADED:
// cache the map size
Maps::getSize(mapx, mapy, mapz);
case SC_UNPAUSED:
// manage all designations on unpause
CSP::UnpauseEvent();
default:
break;
}
}
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_event event) {
CSP::OnUpdate(out);
return DFHack::CR_OK;
}
command_result channel_safely(color_ostream &out, std::vector<std::string> &parameters) {
if (!parameters.empty()) {
if (parameters.size() >= 2 && parameters.size() <= 3) {
if (parameters[0] == "run" && parameters[1] == "once") {
CSP::UnpauseEvent();
return DFHack::CR_OK;
}
bool state = false;
bool set = false;
if (parameters[0] == "enable") {
state = true;
} else if (parameters[0] == "disable") {
state = false;
} else if (parameters[0] == "set") {
set = true;
} else {
return DFHack::CR_WRONG_USAGE;
}
try {
if (parameters[1] == "debug") {
auto level = std::abs(std::stol(parameters[2]));
config.debug = true;
switch (level) {
case 1:
DBG_NAME(manager).allowed(DFHack::DebugCategory::LDEBUG);
DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO);
DBG_NAME(groups).allowed(DFHack::DebugCategory::LINFO);
DBG_NAME(jobs).allowed(DFHack::DebugCategory::LINFO);
break;
case 2:
DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO);
DBG_NAME(groups).allowed(DFHack::DebugCategory::LDEBUG);
DBG_NAME(jobs).allowed(DFHack::DebugCategory::LDEBUG);
break;
case 3:
DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO);
DBG_NAME(groups).allowed(DFHack::DebugCategory::LDEBUG);
DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE);
break;
case 4:
DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(monitor).allowed(DFHack::DebugCategory::LINFO);
DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE);
break;
case 5:
DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(monitor).allowed(DFHack::DebugCategory::LDEBUG);
DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE);
break;
case 6:
DBG_NAME(manager).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(monitor).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(groups).allowed(DFHack::DebugCategory::LTRACE);
DBG_NAME(jobs).allowed(DFHack::DebugCategory::LTRACE);
break;
case 0:
default:
DBG_NAME(monitor).allowed(DFHack::DebugCategory::LERROR);
DBG_NAME(manager).allowed(DFHack::DebugCategory::LERROR);
DBG_NAME(groups).allowed(DFHack::DebugCategory::LERROR);
DBG_NAME(jobs).allowed(DFHack::DebugCategory::LERROR);
}
} else if(parameters[1] == "monitor-active"){
config.monitor_active = state;
} else if (parameters[1] == "require-vision") {
config.require_vision = state;
} else if (parameters[1] == "insta-dig") {
config.insta_dig = state;
} else if (parameters[1] == "refresh-freq" && set && parameters.size() == 3) {
config.refresh_freq = std::abs(std::stol(parameters[2]));
} else if (parameters[1] == "monitor-freq" && set && parameters.size() == 3) {
config.monitor_freq = std::abs(std::stol(parameters[2]));
} else if (parameters[1] == "ignore-threshold" && set && parameters.size() == 3) {
config.ignore_threshold = std::abs(std::stol(parameters[2]));
} else if (parameters[1] == "fall-threshold" && set && parameters.size() == 3) {
uint8_t t = std::abs(std::stol(parameters[2]));
if (t > 0) {
config.fall_threshold = t;
} else {
out.printerr("fall-threshold must have a value greater than 0 or the plugin does a lot of nothing.\n");
return DFHack::CR_FAILURE;
}
} else {
return DFHack::CR_WRONG_USAGE;
}
} catch (const std::exception &e) {
out.printerr("%s\n", e.what());
return DFHack::CR_FAILURE;
}
}
} else {
out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED.");
out.print("monitor-active: %s\n", config.monitor_active ? "on." : "off.");
out.print("require-vision: %s\n", config.require_vision ? "on." : "off.");
out.print("insta-dig: %s\n", config.insta_dig ? "on." : "off.");
out.print("refresh-freq: %" PRIi32 "\n", config.refresh_freq);
out.print("monitor-freq: %" PRIi32 "\n", config.monitor_freq);
out.print("ignore-threshold: %" PRIu8 "\n", config.ignore_threshold);
out.print("fall-threshold: %" PRIu8 "\n", config.fall_threshold);
}
saveConfig();
return DFHack::CR_OK;
}

@ -0,0 +1,49 @@
#pragma once
#include "plugin.h"
#include "channel-jobs.h"
#include <df/map_block.h>
#include <df/coord.h>
#include <vector>
#include <map>
#include <set>
using namespace DFHack;
using Group = std::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 GroupsMap = std::map<df::coord, int>;
GroupsMap groups_map;
Groups groups;
ChannelJobs &jobs;
std::set<int> free_spots;
protected:
void scan_map();
void add(const df::coord &map_pos);
public:
explicit ChannelGroups(ChannelJobs &jobs) : jobs(jobs) { groups.reserve(200); }
void scan_one(const df::coord &map_pos);
void build();
void clear();
void remove(const df::coord &map_pos);
Groups::const_iterator find(const df::coord &map_pos) const;
Groups::const_iterator begin() const;
Groups::const_iterator end() const;
size_t count(const df::coord &map_pos) const;
void debug_groups();
void debug_map();
};

@ -0,0 +1,29 @@
#pragma once
#include <PluginManager.h>
#include <modules/Job.h>
#include <map>
using namespace DFHack;
/* Used to read/store/iterate channel digging jobs
* jobs: list of coordinates with channel jobs associated to them
* load_channel_jobs: iterates world->jobs.list to find channel jobs and adds them into the `jobs` map
* clear: empties the container
* erase: finds a job corresponding to a coord, removes the mapping in jobs, and calls Job::removeJob, then returns an iterator following the element removed
* find: returns an iterator to a job if one exists for a map coordinate
* begin: returns jobs.begin()
* end: returns jobs.end()
*/
class ChannelJobs {
private:
friend class ChannelGroup;
using Jobs = std::set<df::coord>; // job* will exist until it is complete, and likely beyond
Jobs jobs;
public:
void load_channel_jobs();
void clear();
Jobs::iterator erase(const df::coord &map_pos);
Jobs::const_iterator find(const df::coord &map_pos) const;
Jobs::const_iterator begin() const;
Jobs::const_iterator end() const;
};

@ -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.build(); debug(); }
void manage_all();
void manage_group(const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false);
void manage_group(const Group &group, bool set_marker_mode = false, bool marker_mode = false);
bool manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false);
void mark_done(const df::coord &map_pos);
void debug() {
if (config.debug) {
groups.debug_groups();
groups.debug_map();
//std::terminate();
}
}
};

@ -0,0 +1,149 @@
#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 bool is_dig_job(const df::job* job) {
return job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel;
}
inline bool is_dig_designation(const df::tile_designation &designation) {
return designation.bits.dig != df::tile_dig_designation::No;
}
inline bool has_unit(const df::tile_occupancy* occupancy) {
return occupancy->bits.unit || occupancy->bits.unit_grounded;
}
inline bool is_safe_fall(const df::coord &map_pos) {
df::coord below(map_pos);
for (uint8_t zi = 0; zi < config.fall_threshold; ++zi) {
below.z--;
if (config.require_vision && Maps::getTileDesignation(below)->bits.hidden) {
return true; //we require vision, and we can't see below.. so we gotta assume it's safe
}
df::tiletype type = *Maps::getTileType(below);
if (!isOpenTerrain(type)) {
return true;
}
}
return false;
}
inline bool is_safe_to_dig_down(const df::coord &map_pos) {
df::coord pos(map_pos);
for (uint8_t zi = 0; zi <= config.fall_threshold; ++zi) {
// assume safe if we can't see and need vision
if (config.require_vision && Maps::getTileDesignation(pos)->bits.hidden) {
return true;
}
df::tiletype type = *Maps::getTileType(pos);
if (zi == 0 && isOpenTerrain(type)) {
// the starting tile is open space, that's obviously not safe
return false;
} else if (!isOpenTerrain(type)) {
// a tile after the first one is not open space
return true;
}
pos.z--;
}
return false;
}
inline bool is_group_occupied(const ChannelGroups &groups, const Group &group) {
// return true if any tile in the group is occupied by a unit
return std::any_of(group.begin(), group.end(), [](const Group::key_type &pos){
return has_unit(Maps::getTileOccupancy(pos));
});
}
inline bool has_group_above(const ChannelGroups &groups, const df::coord &map_pos) {
df::coord above(map_pos);
above.z++;
if (groups.count(above)) {
return true;
}
return false;
}
inline bool has_any_groups_above(const ChannelGroups &groups, const Group &group) {
// for each designation in the group
for (auto &pos : group) {
df::coord above(pos);
above.z++;
if (groups.count(above)) {
return true;
}
}
// if there are no incomplete groups above this group, then this group is ready
return false;
}
inline void cancel_job(df::job* job) {
if (job != nullptr) {
df::coord &pos = job->pos;
df::map_block* job_block = Maps::getTileBlock(pos);
uint16_t x, y;
x = pos.x % 16;
y = pos.y % 16;
df::tile_designation &designation = job_block->designation[x][y];
switch (job->job_type) {
case job_type::Dig:
designation.bits.dig = df::tile_dig_designation::Default;
break;
case job_type::CarveUpwardStaircase:
designation.bits.dig = df::tile_dig_designation::UpStair;
break;
case job_type::CarveDownwardStaircase:
designation.bits.dig = df::tile_dig_designation::DownStair;
break;
case job_type::CarveUpDownStaircase:
designation.bits.dig = df::tile_dig_designation::UpDownStair;
break;
case job_type::CarveRamp:
designation.bits.dig = df::tile_dig_designation::Ramp;
break;
case job_type::DigChannel:
designation.bits.dig = df::tile_dig_designation::Channel;
break;
default:
designation.bits.dig = df::tile_dig_designation::No;
break;
}
Job::removeJob(job);
}
}
inline void get_neighbours(const df::coord &map_pos, df::coord(&neighbours)[8]) {
neighbours[0] = map_pos;
neighbours[1] = map_pos;
neighbours[2] = map_pos;
neighbours[3] = map_pos;
neighbours[4] = map_pos;
neighbours[5] = map_pos;
neighbours[6] = map_pos;
neighbours[7] = map_pos;
neighbours[0].x--; neighbours[0].y--;
neighbours[1].y--;
neighbours[2].x++; neighbours[2].y--;
neighbours[3].x--;
neighbours[4].x++;
neighbours[5].x--; neighbours[5].y++;
neighbours[6].y++;
neighbours[7].x++; neighbours[7].y++;
}

@ -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 debug = false;
bool monitor_active = false;
bool require_vision = true;
bool insta_dig = false;
int32_t refresh_freq = 600;
int32_t monitor_freq = 10;
uint8_t ignore_threshold = 7;
uint8_t fall_threshold = 1;
};
extern Configuration config;
extern int32_t mapx, mapy, mapz;