Implements plugin: channel-safely v0.1
parent
edfaf5f9f2
commit
22414f26fa
@ -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)
|
@ -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> ¶meters);
|
||||||
|
|
||||||
|
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> ¶meters) {
|
||||||
|
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;
|
Loading…
Reference in New Issue