Implements plugin: channel-safely v1.1a

develop
Josh Cooper 2022-11-21 12:39:26 -08:00
parent 9959ef1b36
commit 8a0999ffdc
12 changed files with 105 additions and 174 deletions

@ -2,12 +2,12 @@ channel-safely
============== ==============
.. dfhack-tool:: .. dfhack-tool::
:summary: Auto-manage channel designations to keep dwarves safe :summary: Auto-manage channel designations to keep dwarves safe.
:tags: fort auto :tags: fort auto
Multi-level channel projects can be dangerous, and managing the safety of your 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 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 consuming. This plugin keeps your dwarves safe (at least while channeling) so you don't
have to. Now you can focus on designing your dwarven cities with the deep chasms have to. Now you can focus on designing your dwarven cities with the deep chasms
they were meant to have. they were meant to have.
@ -18,7 +18,7 @@ Usage
enable channel-safely enable channel-safely
channel-safely set <setting> <value> channel-safely set <setting> <value>
channel-safely enable|disable <feature> channel-safely enable|disable <feature>
channel-safely runonce channel-safely <command>
When enabled the map will be scanned for channel designations which will be grouped 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 together based on adjacency and z-level. These groups will then be analyzed for safety
@ -38,13 +38,20 @@ Examples
``channel-safely disable require-vision`` ``channel-safely disable require-vision``
Allows the plugin to read all tiles, including the ones your dwarves know nothing about. Allows the plugin to read all tiles, including the ones your dwarves know nothing about.
``channel-safely enable monitor-active`` ``channel-safely enable monitor``
Enables monitoring active channel digging jobs. Meaning that if another unit it present 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). or the tile below becomes open space the job will be paused or canceled (respectively).
``channel-safely set ignore-threshold 3`` ``channel-safely set ignore-threshold 3``
Configures the plugin to ignore designations equal to or above priority 3 designations. Configures the plugin to ignore designations equal to or above priority 3 designations.
Commands
--------
:runonce: Run the safety procedures once to set the marker mode of designations.
:rebuild: Rebuild the designation group data. Intended for to be used in the event
the marker mode isn't being set correctly (mostly for debugging).
Features Features
-------- --------

@ -2,7 +2,6 @@ project(channel-safely)
include_directories(include) include_directories(include)
SET(SOURCES SET(SOURCES
channel-jobs.cpp
channel-groups.cpp channel-groups.cpp
channel-manager.cpp channel-manager.cpp
channel-safely-plugin.cpp) channel-safely-plugin.cpp)

@ -6,7 +6,18 @@
#include <random> #include <random>
// iterates the DF job list and adds channel jobs to the `jobs` container
void ChannelJobs::load_channel_jobs() {
locations.clear();
df::job_list_link* node = df::global::world->jobs.list.next;
while (node) {
df::job* job = node->item;
node = node->next;
if (is_dig_job(job)) {
locations.emplace(job->pos);
}
}
}
// adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found // 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) { void ChannelGroups::add(const df::coord &map_pos) {
@ -148,6 +159,7 @@ void ChannelGroups::scan() {
// the tile, check if it has a channel designation // the tile, check if it has a channel designation
df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z); df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z);
if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) { if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) {
TileCache::Get().uncache(map_pos);
remove(map_pos); remove(map_pos);
if (jobs.count(map_pos)) { if (jobs.count(map_pos)) {
jobs.erase(map_pos); jobs.erase(map_pos);
@ -165,6 +177,8 @@ void ChannelGroups::scan() {
} }
TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos)); TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos));
add(map_pos); add(map_pos);
} else if (groups_map.count(map_pos)) {
remove(map_pos);
} }
} }
} }
@ -252,21 +266,25 @@ size_t ChannelGroups::count(const df::coord &map_pos) const {
// prints debug info about the groups stored, and their members // prints debug info about the groups stored, and their members
void ChannelGroups::debug_groups() { void ChannelGroups::debug_groups() {
if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) {
int idx = 0; int idx = 0;
TRACE(groups).print(" debugging group data\n"); TRACE(groups).print(" debugging group data\n");
for (auto &group : groups) { for (auto &group: groups) {
TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size()); TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size());
for (auto &pos : group) { for (auto &pos: group) {
TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z); TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z);
} }
idx++; idx++;
} }
}
} }
// prints debug info group mappings // prints debug info group mappings
void ChannelGroups::debug_map() { void ChannelGroups::debug_map() {
if (DFHack::debug_groups.isEnabled(DebugCategory::LDEBUG)) {
INFO(groups).print("Group Mappings: %zu\n", groups_map.size()); INFO(groups).print("Group Mappings: %zu\n", groups_map.size());
for (auto &pair : groups_map) { for (auto &pair: groups_map) {
DEBUG(groups).print(" map[" COORD "] = %d\n",COORDARGS(pair.first), pair.second); DEBUG(groups).print(" map[" COORD "] = %d\n", COORDARGS(pair.first), pair.second);
}
} }
} }

@ -1,46 +0,0 @@
#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();
}

@ -5,11 +5,6 @@
#include <modules/EventManager.h> //hash function for df::coord #include <modules/EventManager.h> //hash function for df::coord
#include <df/block_square_event_designation_priorityst.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 // sets mark flags as necessary, for all designations
void ChannelManager::manage_groups() { void ChannelManager::manage_groups() {

@ -93,7 +93,6 @@ PersistentDataItem psetting;
PersistentDataItem pfeature; PersistentDataItem pfeature;
const std::string FCONFIG_KEY = std::string(plugin_name) + "/feature"; const std::string FCONFIG_KEY = std::string(plugin_name) + "/feature";
const std::string SCONFIG_KEY = std::string(plugin_name) + "/setting"; const std::string SCONFIG_KEY = std::string(plugin_name) + "/setting";
//std::unordered_set<int32_t> active_jobs;
enum FeatureConfigData { enum FeatureConfigData {
VISION, VISION,
@ -138,20 +137,22 @@ df::coord simulate_area_fall(const df::coord &pos) {
// executes dig designations for the specified tile coordinates // executes dig designations for the specified tile coordinates
inline bool dig_now(color_ostream &out, const df::coord &map_pos) { inline bool dig_now(color_ostream &out, const df::coord &map_pos) {
auto L = Lua::Core::State; bool ret = false;
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 2) ||
!Lua::PushModulePublic(out, L, "plugins.dig-now", "dig_now_tile"))
return false;
lua_State* state = Lua::Core::State;
static const char* module_name = "plugins.dig-now";
static const char* fn_name = "dig_now_tile";
// the stack layout isn't likely to change, ever
static auto args_lambda = [&map_pos](lua_State* L) {
Lua::Push(L, map_pos); Lua::Push(L, map_pos);
};
static auto res_lambda = [&ret](lua_State* L) {
ret = lua_toboolean(L, -1);
};
if (!Lua::SafeCall(out, L, 1, 1)) Lua::StackUnwinder top(state);
return false; Lua::CallLuaModuleFunction(out, state, module_name, fn_name, 1, 1, args_lambda, res_lambda);
return ret;
return lua_toboolean(L, -1);
} }
// fully heals the unit specified, resurrecting if need be // fully heals the unit specified, resurrecting if need be
@ -583,11 +584,14 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_ev
command_result channel_safely(color_ostream &out, std::vector<std::string> &parameters) { command_result channel_safely(color_ostream &out, std::vector<std::string> &parameters) {
if (!parameters.empty()) { if (!parameters.empty()) {
if (parameters.size() >= 2 && parameters.size() <= 3) {
if (parameters[0] == "runonce") { if (parameters[0] == "runonce") {
CSP::UnpauseEvent(); CSP::UnpauseEvent();
return DFHack::CR_OK; return DFHack::CR_OK;
} else if (parameters[0] == "rebuild") {
ChannelManager::Get().destroy_groups();
ChannelManager::Get().build_groups();
} }
if (parameters.size() >= 2 && parameters.size() <= 3) {
bool state = false; bool state = false;
bool set = false; bool set = false;
if (parameters[0] == "enable") { if (parameters[0] == "enable") {
@ -600,54 +604,7 @@ command_result channel_safely(color_ostream &out, std::vector<std::string> &para
return DFHack::CR_WRONG_USAGE; return DFHack::CR_WRONG_USAGE;
} }
try { try {
if (parameters[1] == "debug") { if(parameters[1] == "monitor"){
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"){
if (state != config.monitor_active) { if (state != config.monitor_active) {
config.monitor_active = state; config.monitor_active = state;
// if this is a fresh start // if this is a fresh start

@ -4,14 +4,15 @@
#include <df/map_block.h> #include <df/map_block.h>
#include <df/coord.h> #include <df/coord.h>
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <vector> #include <vector>
#include <map> #include <unordered_map>
#include <set> #include <unordered_set>
using namespace DFHack; using namespace DFHack;
using Group = std::set<df::coord>; using Group = std::unordered_set<df::coord>;
using Groups = std::vector<Group>; using Groups = std::vector<Group>;
/* Used to build groups of adjacent channel designations/jobs /* Used to build groups of adjacent channel designations/jobs
@ -26,8 +27,8 @@ using Groups = std::vector<Group>;
*/ */
class ChannelGroups { class ChannelGroups {
private: private:
using GroupBlocks = std::set<df::map_block*>; using GroupBlocks = std::unordered_set<df::map_block*>;
using GroupsMap = std::map<df::coord, int>; using GroupsMap = std::unordered_map<df::coord, int>;
GroupBlocks group_blocks; GroupBlocks group_blocks;
GroupsMap groups_map; GroupsMap groups_map;
Groups groups; Groups groups;

@ -1,7 +1,11 @@
#pragma once #pragma once
#include <PluginManager.h> #include <PluginManager.h>
#include <modules/Job.h> #include <modules/Job.h>
#include <map> #include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <df/world.h>
#include <df/job.h>
#include <unordered_set>
using namespace DFHack; using namespace DFHack;
@ -17,14 +21,23 @@ using namespace DFHack;
class ChannelJobs { class ChannelJobs {
private: private:
friend class ChannelGroup; friend class ChannelGroup;
using Jobs = std::set<df::coord>; // job* will exist until it is complete, and likely beyond
Jobs jobs; using Jobs = std::unordered_set<df::coord>; // job* will exist until it is complete, and likely beyond
Jobs locations;
public: public:
void load_channel_jobs(); void load_channel_jobs();
void clear(); void clear() {
int count(const df::coord &map_pos) const { return jobs.count(map_pos); } locations.clear();
Jobs::iterator erase(const df::coord &map_pos); }
Jobs::const_iterator find(const df::coord &map_pos) const; int count(const df::coord &map_pos) const { return locations.count(map_pos); }
Jobs::const_iterator begin() const; Jobs::iterator erase(const df::coord &map_pos) {
Jobs::const_iterator end() const; auto iter = locations.find(map_pos);
if (iter != locations.end()) {
return locations.erase(iter);
}
return iter;
}
Jobs::const_iterator find(const df::coord &map_pos) const { return locations.find(map_pos); }
Jobs::const_iterator begin() const { return locations.begin(); }
Jobs::const_iterator end() const { return locations.end(); }
}; };

@ -33,9 +33,7 @@ public:
bool exists(const df::coord &map_pos) const { return groups.count(map_pos); } bool exists(const df::coord &map_pos) const { return groups.count(map_pos); }
void debug() { void debug() {
DEBUG(groups).print(" DEBUGGING GROUPS:\n"); DEBUG(groups).print(" DEBUGGING GROUPS:\n");
if (config.debug) {
groups.debug_groups(); groups.debug_groups();
groups.debug_map(); groups.debug_map();
} }
}
}; };

@ -64,7 +64,7 @@ inline bool is_safe_fall(const df::coord &map_pos) {
return true; //we require vision, and we can't see below.. so we gotta assume it's safe return true; //we require vision, and we can't see below.. so we gotta assume it's safe
} }
df::tiletype type = *Maps::getTileType(below); df::tiletype type = *Maps::getTileType(below);
if (!isOpenTerrain(type)) { if (!DFHack::isOpenTerrain(type)) {
return true; return true;
} }
} }
@ -80,10 +80,10 @@ inline bool is_safe_to_dig_down(const df::coord &map_pos) {
return true; return true;
} }
df::tiletype type = *Maps::getTileType(pos); df::tiletype type = *Maps::getTileType(pos);
if (zi == 0 && isOpenTerrain(type)) { if (zi == 0 && DFHack::isOpenTerrain(type)) {
// the starting tile is open space, that's obviously not safe // the starting tile is open space, that's obviously not safe
return false; return false;
} else if (!isOpenTerrain(type)) { } else if (!DFHack::isOpenTerrain(type)) {
// a tile after the first one is not open space // a tile after the first one is not open space
return true; return true;
} }
@ -142,7 +142,9 @@ inline void cancel_job(df::job* job) {
x = pos.x % 16; x = pos.x % 16;
y = pos.y % 16; y = pos.y % 16;
df::tile_designation &designation = job_block->designation[x][y]; df::tile_designation &designation = job_block->designation[x][y];
switch (job->job_type) { auto type = job->job_type;
Job::removeJob(job);
switch (type) {
case job_type::Dig: case job_type::Dig:
designation.bits.dig = df::tile_dig_designation::Default; designation.bits.dig = df::tile_dig_designation::Default;
break; break;
@ -165,21 +167,13 @@ inline void cancel_job(df::job* job) {
designation.bits.dig = df::tile_dig_designation::No; designation.bits.dig = df::tile_dig_designation::No;
break; break;
} }
Job::removeJob(job);
} }
} }
template<class Ctr1, class Ctr2, class Ctr3> template<class Ctr1, class Ctr2, class Ctr3>
void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) {
for (const auto &a : c1) { for (const auto &a : c1) {
bool matched = false; if (!c2.count(a)) {
for (const auto &b : c2) {
if (a == b) {
matched = true;
break;
}
}
if (!matched) {
c3.emplace(a); c3.emplace(a);
} }
} }

@ -9,7 +9,6 @@ namespace DFHack {
} }
struct Configuration { struct Configuration {
bool debug = false;
bool monitor_active = false; bool monitor_active = false;
bool require_vision = true; bool require_vision = true;
bool insta_dig = false; bool insta_dig = false;

@ -3,13 +3,14 @@
#include <modules/Maps.h> #include <modules/Maps.h>
#include <df/coord.h> #include <df/coord.h>
#include <df/tiletype.h> #include <df/tiletype.h>
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <map> #include <unordered_map>
class TileCache { class TileCache {
private: private:
TileCache() = default; TileCache() = default;
std::map<df::coord, df::tiletype> locations; std::unordered_map<df::coord, df::tiletype> locations;
public: public:
static TileCache& Get() { static TileCache& Get() {
static TileCache instance; static TileCache instance;
@ -25,11 +26,6 @@ public:
} }
bool hasChanged(const df::coord &pos, const df::tiletype &type) { bool hasChanged(const df::coord &pos, const df::tiletype &type) {
if (locations.count(pos)) { return locations.count(pos) && type != locations[pos];
if (type != locations.find(pos)->second){
return true;
}
}
return false;
} }
}; };