#include "Core.h" #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" #include "TileTypes.h" #include "modules/Burrows.h" #include "modules/EventManager.h" #include "modules/Job.h" #include "modules/Persistence.h" #include "modules/World.h" #include "df/block_burrow.h" #include "df/burrow.h" #include "df/map_block.h" #include "df/plotinfost.h" #include "df/tile_designation.h" #include "df/unit.h" #include "df/world.h" using std::vector; using std::string; using namespace DFHack; DFHACK_PLUGIN("burrow"); DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(plotinfo); REQUIRE_GLOBAL(window_z); REQUIRE_GLOBAL(world); // logging levels can be dynamically controlled with the `debugfilter` command. namespace DFHack { // for configuration-related logging DBG_DECLARE(burrow, status, DebugCategory::LINFO); // for logging during the periodic scan DBG_DECLARE(burrow, event, DebugCategory::LINFO); } static std::unordered_map active_dig_jobs; static command_result do_command(color_ostream &out, vector ¶meters); static void init_diggers(color_ostream& out); static void jobStartedHandler(color_ostream& out, void* ptr); static void jobCompletedHandler(color_ostream& out, void* ptr); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { DEBUG(status, out).print("initializing %s\n", plugin_name); commands.push_back( PluginCommand("burrow", "Quickly adjust burrow tiles and units.", do_command)); return CR_OK; } static void reset() { active_dig_jobs.clear(); } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (enable != is_enabled) { is_enabled = enable; DEBUG(status, out).print("%s from the API\n", is_enabled ? "enabled" : "disabled"); reset(); if (enable) { init_diggers(out); EventManager::registerListener(EventManager::EventType::JOB_STARTED, EventManager::EventHandler(jobStartedHandler, 0), plugin_self); EventManager::registerListener(EventManager::EventType::JOB_COMPLETED, EventManager::EventHandler(jobCompletedHandler, 0), plugin_self); } else { EventManager::unregisterAll(plugin_self); } } else { DEBUG(status, out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { DEBUG(status, out).print("shutting down %s\n", plugin_name); reset(); return CR_OK; } DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { if (event == DFHack::SC_WORLD_UNLOADED) reset(); return CR_OK; } static bool call_burrow_lua(color_ostream *out, const char *fn_name, int nargs = 0, int nres = 0, Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { DEBUG(status).print("calling %s lua function: '%s'\n", plugin_name, fn_name); CoreSuspender guard; auto L = Lua::Core::State; Lua::StackUnwinder top(L); if (!out) out = &Core::getInstance().getConsole(); return Lua::CallLuaModuleFunction(*out, L, "plugins.burrow", fn_name, nargs, nres, std::forward(args_lambda), std::forward(res_lambda)); } static command_result do_command(color_ostream &out, vector ¶meters) { CoreSuspender suspend; if (!Core::getInstance().isWorldLoaded()) { out.printerr("Cannot run %s without a loaded world.\n", plugin_name); return CR_FAILURE; } bool show_help = false; if (!call_burrow_lua(&out, "parse_commandline", parameters.size(), 1, [&](lua_State *L) { for (const string ¶m : parameters) Lua::Push(L, param); }, [&](lua_State *L) { show_help = !lua_toboolean(L, -1); })) { return CR_FAILURE; } return show_help ? CR_WRONG_USAGE : CR_OK; } ///////////////////////////////////////////////////// // listener logic // static void init_diggers(color_ostream& out) { if (!Core::getInstance().isWorldLoaded()) { DEBUG(status, out).print("world not yet loaded; not scanning jobs\n"); return; } std::vector pvec; int start_id = 0; if (Job::listNewlyCreated(&pvec, &start_id)) { for (auto job : pvec) { if (Job::getWorker(job)) jobStartedHandler(out, job); } } } static void jobStartedHandler(color_ostream& out, void* ptr) { DEBUG(event, out).print("entering jobStartedHandler\n"); df::job *job = (df::job *)ptr; auto type = ENUM_ATTR(job_type, type, job->job_type); if (type != job_type_class::Digging) return; const df::coord &pos = job->pos; DEBUG(event, out).print("dig job started: id=%d, pos=(%d,%d,%d), type=%s\n", job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type).c_str()); df::tiletype *tt = Maps::getTileType(pos); if (tt) active_dig_jobs[pos] = *tt; } static void add_walls_to_burrow(color_ostream &out, df::burrow* b, const df::coord & pos1, const df::coord & pos2) { for (int z = pos1.z; z <= pos2.z; z++) { for (int y = pos1.y; y <= pos2.y; y++) { for (int x = pos1.x; x <= pos2.x; x++) { df::coord pos(x,y,z); df::tiletype *tt = Maps::getTileType(pos); if (tt && isWallTerrain(*tt)) Burrows::setAssignedTile(b, pos, true); } } } } static void expand_burrows(color_ostream &out, const df::coord & pos, df::tiletype prev_tt, df::tiletype tt) { if (!isWalkable(tt) && tileShape(tt) != tiletype_shape::RAMP_TOP) return; bool changed = false; for (auto b : plotinfo->burrows.list) { if (!b->name.ends_with('+') || !Burrows::isAssignedTile(b, pos)) continue; if (!isWalkable(prev_tt)) { changed = true; add_walls_to_burrow(out, b, pos+df::coord(-1,-1,0), pos+df::coord(1,1,0)); if (isWalkableUp(tt)) Burrows::setAssignedTile(b, pos+df::coord(0,0,1), true); if (tileShape(tt) == tiletype_shape::RAMP) add_walls_to_burrow(out, b, pos+df::coord(-1,-1,1), pos+df::coord(1,1,1)); } if (LowPassable(tt) && !LowPassable(prev_tt)) { changed = true; Burrows::setAssignedTile(b, pos-df::coord(0,0,1), true); if (tileShape(tt) == tiletype_shape::RAMP_TOP) add_walls_to_burrow(out, b, pos+df::coord(-1,-1,-1), pos+df::coord(1,1,-1)); } } if (changed) Job::checkDesignationsNow(); } static void jobCompletedHandler(color_ostream& out, void* ptr) { DEBUG(event, out).print("entering jobCompletedHandler\n"); df::job *job = (df::job *)ptr; auto type = ENUM_ATTR(job_type, type, job->job_type); if (type != job_type_class::Digging) return; const df::coord &pos = job->pos; DEBUG(event, out).print("dig job completed: id=%d, pos=(%d,%d,%d), type=%s\n", job->id, pos.x, pos.y, pos.z, ENUM_KEY_STR(job_type, job->job_type).c_str()); df::tiletype prev_tt = active_dig_jobs[pos]; df::tiletype *tt = Maps::getTileType(pos); if (tt && *tt && *tt != prev_tt) expand_burrows(out, pos, prev_tt, *tt); active_dig_jobs.erase(pos); } ///////////////////////////////////////////////////// // Lua API // static void get_bool_field(lua_State *L, int idx, const char *name, bool *dest) { lua_getfield(L, idx, name); if (lua_isnil(L, -1)) { lua_pop(L, 1); return; } *dest = lua_toboolean(L, -1); lua_pop(L, 1); } static void get_opts(lua_State *L, int idx, bool &zlevel) { if (lua_gettop(L) < idx) return; get_bool_field(L, idx, "zlevel", &zlevel); } static bool get_int_field(lua_State *L, int idx, const char *name, int16_t *dest) { lua_getfield(L, idx, name); if (lua_isnil(L, -1)) { lua_pop(L, 1); return false; } *dest = lua_tointeger(L, -1); lua_pop(L, 1); return true; } static bool get_bounds(lua_State *L, int idx, df::coord &pos1, df::coord &pos2) { return get_int_field(L, idx, "x1", &pos1.x) && get_int_field(L, idx, "y1", &pos1.y) && get_int_field(L, idx, "z1", &pos1.z) && get_int_field(L, idx, "x2", &pos2.x) && get_int_field(L, idx, "y2", &pos2.y) && get_int_field(L, idx, "z2", &pos2.z); } static df::burrow* get_burrow(lua_State *L, int idx) { df::burrow *burrow = NULL; if (lua_isuserdata(L, idx)) burrow = Lua::GetDFObject(L, idx); else if (lua_isstring(L, idx)) burrow = Burrows::findByName(luaL_checkstring(L, idx), true); else if (lua_isinteger(L, idx)) burrow = df::burrow::find(luaL_checkinteger(L, idx)); return burrow; } static void copyTiles(df::burrow *target, df::burrow *source, bool enable) { CHECK_NULL_POINTER(target); CHECK_NULL_POINTER(source); if (source == target) { if (!enable) Burrows::clearTiles(target); return; } vector pvec; Burrows::listBlocks(&pvec, source); for (auto block : pvec) { auto smask = Burrows::getBlockMask(source, block); if (!smask) continue; auto tmask = Burrows::getBlockMask(target, block, enable); if (!tmask) continue; if (enable) { for (int j = 0; j < 16; j++) tmask->tile_bitmask[j] |= smask->tile_bitmask[j]; } else { for (int j = 0; j < 16; j++) tmask->tile_bitmask[j] &= ~smask->tile_bitmask[j]; if (!tmask->has_assignments()) Burrows::deleteBlockMask(target, block, tmask); } } } static void setTilesByDesignation(df::burrow *target, df::tile_designation d_mask, df::tile_designation d_value, bool enable) { CHECK_NULL_POINTER(target); auto &blocks = world->map.map_blocks; for (auto block : blocks) { df::block_burrow *mask = NULL; for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { if ((block->designation[x][y].whole & d_mask.whole) != d_value.whole) continue; if (!mask) mask = Burrows::getBlockMask(target, block, enable); if (!mask) goto next_block; mask->setassignment(x, y, enable); } } if (mask && !enable && !mask->has_assignments()) Burrows::deleteBlockMask(target, block, mask); next_block:; } } static bool setTilesByKeyword(df::burrow *target, std::string name, bool enable) { CHECK_NULL_POINTER(target); df::tile_designation mask; df::tile_designation value; if (name == "ABOVE_GROUND") mask.bits.subterranean = true; else if (name == "SUBTERRANEAN") mask.bits.subterranean = value.bits.subterranean = true; else if (name == "LIGHT") mask.bits.light = value.bits.light = true; else if (name == "DARK") mask.bits.light = true; else if (name == "OUTSIDE") mask.bits.outside = value.bits.outside = true; else if (name == "INSIDE") mask.bits.outside = true; else if (name == "HIDDEN") mask.bits.hidden = value.bits.hidden = true; else if (name == "REVEALED") mask.bits.hidden = true; else return false; setTilesByDesignation(target, mask, value, enable); return true; } static void copyUnits(df::burrow *target, df::burrow *source, bool enable) { CHECK_NULL_POINTER(target); CHECK_NULL_POINTER(source); if (source == target) { if (!enable) Burrows::clearUnits(target); return; } for (size_t i = 0; i < source->units.size(); i++) { if (auto unit = df::unit::find(source->units[i])) Burrows::setAssignedUnit(target, unit, enable); } } static int burrow_tiles_clear(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_tiles_clear\n"); lua_pushnil(L); // first key while (lua_next(L, 1)) { df::burrow * burrow = get_burrow(L, -1); if (burrow) Burrows::clearTiles(burrow); lua_pop(L, 1); // remove value, leave key } return 0; } static void tiles_set_add_remove(lua_State *L, bool do_set, bool enable) { df::burrow *target = get_burrow(L, 1); if (!target) { luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); return; } if (do_set) Burrows::clearTiles(target); lua_pushnil(L); // first key while (lua_next(L, 2)) { if (!lua_isstring(L, -1) || !setTilesByKeyword(target, luaL_checkstring(L, -1), enable)) { if (auto burrow = get_burrow(L, -1)) copyTiles(target, burrow, enable); } lua_pop(L, 1); // remove value, leave key } } static int burrow_tiles_set(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_tiles_set\n"); tiles_set_add_remove(L, true, true); return 0; } static int burrow_tiles_add(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_tiles_add\n"); tiles_set_add_remove(L, false, true); return 0; } static int burrow_tiles_remove(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_tiles_remove\n"); tiles_set_add_remove(L, false, false); return 0; } static void box_fill(lua_State *L, bool enable) { df::burrow *burrow = get_burrow(L, 1); if (!burrow) { luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); return; } df::coord pos_start, pos_end; if (!get_bounds(L, 2, pos_start, pos_end)) { luaL_argerror(L, 2, "invalid box bounds"); return; } for (int32_t z = pos_start.z; z <= pos_end.z; ++z) { for (int32_t y = pos_start.y; y <= pos_end.y; ++y) { for (int32_t x = pos_start.x; x <= pos_end.x; ++x) { df::coord pos(x, y, z); Burrows::setAssignedTile(burrow, pos, enable); } } } } static int burrow_tiles_box_add(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_tiles_box_add\n"); box_fill(L, true); return 0; } static int burrow_tiles_box_remove(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_tiles_box_remove\n"); box_fill(L, false); return 0; } // ramp tops inherit walkability group of the tile below static uint16_t get_walk_group(const df::coord & pos) { df::tile_designation *des = Maps::getTileDesignation(pos); if (!des || des->bits.hidden) return 0; uint16_t walk = Maps::getWalkableGroup(pos); if (walk) return walk; if (auto tt = Maps::getTileType(pos)) { if (tileShape(*tt) == df::tiletype_shape::RAMP_TOP) { df::coord pos_below(pos); --pos_below.z; walk = Maps::getWalkableGroup(pos_below); } } return walk; } static bool is_tree(const df::tiletype *tt) { return tileMaterial(*tt) == tiletype_material::TREE; } static bool is_tree_trunk(const df::tiletype *tt) { return is_tree(tt) && tileShape(*tt)== tiletype_shape::WALL; } // if the outside tile flag is set // or it's light and it's a tree // or there are just outside or light tree tiles above it static bool is_outside(const df::coord & pos, const df::tile_designation *des) { if (!des) return false; if (des->bits.outside) return true; if (!des->bits.light) return false; df::coord pos_above = pos + df::coord(0, 0, 1); df::tile_designation *des_above = Maps::getTileDesignation(pos_above); df::tiletype *tt_above = Maps::getTileType(pos_above); if (!des_above || !tt_above || (!is_tree(tt_above) && !LowPassable(*tt_above))) return false; return is_outside(pos_above, des_above); } static void flood_fill(lua_State *L, bool enable) { df::coord start_pos; bool zlevel = false; df::burrow *burrow = get_burrow(L, 1); if (!burrow) { luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); return; } Lua::CheckDFAssign(L, &start_pos, 2); get_opts(L, 3, zlevel); df::tile_designation *start_des = Maps::getTileDesignation(start_pos); if (!start_des) { luaL_argerror(L, 2, "invalid starting coordinates"); return; } bool start_outside = is_outside(start_pos, start_des); bool start_hidden = start_des->bits.hidden; uint16_t start_walk = Maps::getWalkableGroup(start_pos); DEBUG(status).print("starting pos: (%d,%d,%d); outside: %d; hidden: %d\n", start_pos.x, start_pos.y, start_pos.z, start_outside, start_hidden); std::stack flood; flood.emplace(start_pos); while(!flood.empty()) { const df::coord pos = flood.top(); flood.pop(); TRACE(status).print("pos: (%d,%d,%d)\n", pos.x, pos.y, pos.z); df::tile_designation *des = Maps::getTileDesignation(pos); if (!des || is_outside(pos, des) != start_outside || des->bits.hidden != start_hidden) { continue; } uint16_t walk = get_walk_group(pos); if (!start_walk && walk) continue; if (pos != start_pos && enable == Burrows::isAssignedTile(burrow, pos)) continue; Burrows::setAssignedTile(burrow, pos, enable); // only go one tile outside of a walkability group (trees don't count) df::tiletype *tt = Maps::getTileType(pos); if (start_walk && start_walk != walk && tt && !is_tree_trunk(tt)) continue; flood.emplace(pos.x-1, pos.y-1, pos.z); flood.emplace(pos.x, pos.y-1, pos.z); flood.emplace(pos.x+1, pos.y-1, pos.z); flood.emplace(pos.x-1, pos.y, pos.z); flood.emplace(pos.x+1, pos.y, pos.z); flood.emplace(pos.x-1, pos.y+1, pos.z); flood.emplace(pos.x, pos.y+1, pos.z); flood.emplace(pos.x+1, pos.y+1, pos.z); if (!zlevel) { df::coord pos_above = pos + df::coord(0, 0, 1); df::tiletype *tt_above = Maps::getTileType(pos_above); if (tt_above) { uint16_t walk_above = get_walk_group(pos_above); if (start_walk == walk_above) flood.emplace(pos_above); } df::coord pos_below = pos + df::coord(0, 0, -1); df::tiletype *tt_below = Maps::getTileType(pos_below); if (tt_below) { uint16_t walk_below = get_walk_group(pos_below); if (start_walk == walk_below) flood.emplace(pos_below); } } } } static int burrow_tiles_flood_add(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_tiles_flood_add\n"); flood_fill(L, true); return 0; } static int burrow_tiles_flood_remove(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_tiles_flood_remove\n"); flood_fill(L, false); return 0; } static int burrow_units_clear(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_units_clear\n"); int32_t count = 0; lua_pushnil(L); // first key while (lua_next(L, 1)) { df::burrow * burrow = get_burrow(L, -1); if (burrow) { count += burrow->units.size(); Burrows::clearUnits(burrow); } lua_pop(L, 1); // remove value, leave key } Lua::Push(L, count); return 1; } static void units_set_add_remove(lua_State *L, bool do_set, bool enable) { df::burrow *target = get_burrow(L, 1); if (!target) { luaL_argerror(L, 1, "invalid burrow specifier or burrow not found"); return; } if (do_set) Burrows::clearUnits(target); lua_pushnil(L); // first key while (lua_next(L, 2)) { if (auto burrow = get_burrow(L, -1)) copyUnits(target, burrow, enable); lua_pop(L, 1); // remove value, leave key } } static int burrow_units_set(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_units_set\n"); units_set_add_remove(L, true, true); return 0; } static int burrow_units_add(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_units_add\n"); units_set_add_remove(L, false, true); return 0; } static int burrow_units_remove(lua_State *L) { color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); DEBUG(status,*out).print("entering burrow_units_remove\n"); units_set_add_remove(L, false, false); return 0; } DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(burrow_tiles_clear), DFHACK_LUA_COMMAND(burrow_tiles_set), DFHACK_LUA_COMMAND(burrow_tiles_add), DFHACK_LUA_COMMAND(burrow_tiles_remove), DFHACK_LUA_COMMAND(burrow_tiles_box_add), DFHACK_LUA_COMMAND(burrow_tiles_box_remove), DFHACK_LUA_COMMAND(burrow_tiles_flood_add), DFHACK_LUA_COMMAND(burrow_tiles_flood_remove), DFHACK_LUA_COMMAND(burrow_units_clear), DFHACK_LUA_COMMAND(burrow_units_set), DFHACK_LUA_COMMAND(burrow_units_add), DFHACK_LUA_COMMAND(burrow_units_remove), DFHACK_LUA_END };