#include "Debug.h" #include "MemAccess.h" #include "PluginManager.h" #include "TileTypes.h" #include "modules/EventManager.h" #include "modules/Gui.h" #include "modules/Job.h" #include "modules/Maps.h" #include "modules/Screen.h" #include "modules/Textures.h" #include "df/block_square_event_designation_priorityst.h" #include "df/init.h" #include "df/job_list_link.h" #include "df/map_block.h" #include "df/tile_designation.h" #include "df/world.h" #include using namespace DFHack; DFHACK_PLUGIN("pathable"); REQUIRE_GLOBAL(init); REQUIRE_GLOBAL(window_x); REQUIRE_GLOBAL(window_y); REQUIRE_GLOBAL(window_z); REQUIRE_GLOBAL(world); namespace DFHack { DBG_DECLARE(pathable, log, DebugCategory::LINFO); } static std::vector textures; DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { textures = Textures::loadTileset("hack/data/art/pathable.png", 32, 32); return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { return CR_OK; } static void paintScreenPathable(df::coord target, bool show_hidden = false) { DEBUG(log).print("entering paintScreen\n"); bool use_graphics = Screen::inGraphicsMode(); int selected_tile_texpos = 0; Screen::findGraphicsTile("CURSORS", 4, 3, &selected_tile_texpos); long pathable_tile_texpos = init->load_bar_texpos[1]; long unpathable_tile_texpos = init->load_bar_texpos[4]; long on_off_texpos = Textures::getTexposByHandle(textures[0]); if (on_off_texpos > 0) { pathable_tile_texpos = on_off_texpos; unpathable_tile_texpos = Textures::getTexposByHandle(textures[1]); } auto dims = Gui::getDwarfmodeViewDims().map(); for (int y = dims.first.y; y <= dims.second.y; ++y) { for (int x = dims.first.x; x <= dims.second.x; ++x) { df::coord map_pos(*window_x + x, *window_y + y, *window_z); if (!Maps::isValidTilePos(map_pos)) continue; // don't overwrite the target tile if (!use_graphics && map_pos == target) { TRACE(log).print("skipping target tile\n"); continue; } if (!show_hidden && !Maps::isTileVisible(map_pos)) { TRACE(log).print("skipping hidden tile\n"); continue; } DEBUG(log).print("scanning map tile at offset %d, %d\n", x, y); Screen::Pen cur_tile = Screen::readTile(x, y, true); DEBUG(log).print("tile data: ch=%d, fg=%d, bg=%d, bold=%s\n", cur_tile.ch, cur_tile.fg, cur_tile.bg, cur_tile.bold ? "true" : "false"); DEBUG(log).print("tile data: tile=%d, tile_mode=%d, tile_fg=%d, tile_bg=%d\n", cur_tile.tile, cur_tile.tile_mode, cur_tile.tile_fg, cur_tile.tile_bg); if (!cur_tile.valid()) { DEBUG(log).print("cannot read tile at offset %d, %d\n", x, y); continue; } bool can_walk = Maps::canWalkBetween(target, map_pos); DEBUG(log).print("tile is %swalkable at offset %d, %d\n", can_walk ? "" : "not ", x, y); if (use_graphics) { if (map_pos == target) { cur_tile.tile = selected_tile_texpos; } else{ cur_tile.tile = can_walk ? pathable_tile_texpos : unpathable_tile_texpos; } } else { int color = can_walk ? COLOR_GREEN : COLOR_RED; if (cur_tile.fg && cur_tile.ch != ' ') { cur_tile.fg = color; cur_tile.bg = 0; } else { cur_tile.fg = 0; cur_tile.bg = color; } cur_tile.bold = false; if (cur_tile.tile) cur_tile.tile_mode = Screen::Pen::CharColor; } Screen::paintTile(cur_tile, x, y, true); } } } static bool is_warm(const df::coord &pos) { auto block = Maps::getTileBlock(pos); if (!block) return false; return block->temperature_1[pos.x&15][pos.y&15] >= 10075; } static bool is_rough_wall(int16_t x, int16_t y, int16_t z) { df::tiletype *tt = Maps::getTileType(x, y, z); if (!tt) return false; return tileShape(*tt) == df::tiletype_shape::WALL && tileSpecial(*tt) != df::tiletype_special::SMOOTH; } static bool will_leak(int16_t x, int16_t y, int16_t z) { auto des = Maps::getTileDesignation(x, y, z); if (!des) return false; if (des->bits.liquid_type == df::tile_liquid::Water && des->bits.flow_size >= 1) return true; if (des->bits.water_table && is_rough_wall(x, y, z)) return true; return false; } static bool is_damp(const df::coord &pos) { return will_leak(pos.x-1, pos.y-1, pos.z) || will_leak(pos.x, pos.y-1, pos.z) || will_leak(pos.x+1, pos.y-1, pos.z) || will_leak(pos.x-1, pos.y, pos.z) || will_leak(pos.x+1, pos.y, pos.z) || will_leak(pos.x-1, pos.y+1, pos.z) || will_leak(pos.x, pos.y+1, pos.z) || will_leak(pos.x+1, pos.y+1, pos.z); will_leak(pos.x, pos.y+1, pos.z+1); } static void paintScreenWarmDamp(bool show_hidden = false) { DEBUG(log).print("entering paintScreenDampWarm\n"); if (Screen::inGraphicsMode()) return; auto dims = Gui::getDwarfmodeViewDims().map(); for (int y = dims.first.y; y <= dims.second.y; ++y) { for (int x = dims.first.x; x <= dims.second.x; ++x) { df::coord map_pos(*window_x + x, *window_y + y, *window_z); if (!Maps::isValidTilePos(map_pos)) continue; if (!show_hidden && !Maps::isTileVisible(map_pos)) { TRACE(log).print("skipping hidden tile\n"); continue; } TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", map_pos.x, map_pos.y, map_pos.z, x, y); Screen::Pen cur_tile = Screen::readTile(x, y, true); if (!cur_tile.valid()) { DEBUG(log).print("cannot read tile at offset %d, %d\n", x, y); continue; } int color = is_warm(map_pos) ? COLOR_RED : is_damp(map_pos) ? COLOR_BLUE : COLOR_BLACK; if (color == COLOR_BLACK) { TRACE(log).print("skipping non-warm, non-damp tile\n"); continue; } // this will also change the color of the cursor or any selection box that overlaps // the tile. this is intentional since it makes the UI more readable if (cur_tile.fg && cur_tile.ch != ' ') { cur_tile.fg = color; cur_tile.bg = 0; } else { cur_tile.fg = 0; cur_tile.bg = color; } cur_tile.bold = false; if (cur_tile.tile) cur_tile.tile_mode = Screen::Pen::CharColor; Screen::paintTile(cur_tile, x, y, true); } } } struct designation{ df::coord pos; df::tile_designation td; df::tile_occupancy to; designation() = default; designation(const df::coord &c, const df::tile_designation &td, const df::tile_occupancy &to) : pos(c), td(td), to(to) {} bool operator==(const designation &rhs) const { return pos == rhs.pos; } bool operator!=(const designation &rhs) const { return !(rhs == *this); } }; namespace std { template<> struct hash { std::size_t operator()(const designation &c) const { std::hash hash_coord; return hash_coord(c.pos); } }; } class Designations { private: std::unordered_map designations; public: Designations() { df::job_list_link *link = world->jobs.list.next; for (; link; link = link->next) { df::job *job = link->item; if(!job || !Maps::isValidTilePos(job->pos)) continue; df::tile_designation td; df::tile_occupancy to; bool keep_if_taken = false; switch (job->job_type) { case df::job_type::SmoothWall: case df::job_type::SmoothFloor: keep_if_taken = true; // fallthrough case df::job_type::CarveFortification: td.bits.smooth = 1; break; case df::job_type::DetailWall: case df::job_type::DetailFloor: td.bits.smooth = 2; break; case job_type::CarveTrack: to.bits.carve_track_north = (job->item_category.whole >> 18) & 1; to.bits.carve_track_south = (job->item_category.whole >> 19) & 1; to.bits.carve_track_west = (job->item_category.whole >> 20) & 1; to.bits.carve_track_east = (job->item_category.whole >> 21) & 1; break; default: continue; } if (keep_if_taken || !Job::getWorker(job)) designations.emplace(job->pos, designation(job->pos, td, to)); } } // get from job; if no job, then fall back to querying map designation get(const df::coord &pos) const { if (designations.count(pos)) { return designations.at(pos); } auto pdes = Maps::getTileDesignation(pos); auto pocc = Maps::getTileOccupancy(pos); if (!pdes || !pocc) return {}; return designation(pos, *pdes, *pocc); } }; static bool is_designated_for_smoothing(const designation &designation) { return designation.td.bits.smooth == 1; } static bool is_designated_for_engraving(const designation &designation) { return designation.td.bits.smooth == 2; } static bool is_designated_for_track_carving(const designation &designation) { const df::tile_occupancy &occ = designation.to; return occ.bits.carve_track_east || occ.bits.carve_track_north || occ.bits.carve_track_south || occ.bits.carve_track_west; } static char get_track_char(const designation &designation) { const df::tile_occupancy &occ = designation.to; if (occ.bits.carve_track_east && occ.bits.carve_track_north && occ.bits.carve_track_south && occ.bits.carve_track_west) return (char)0xCE; // NSEW if (occ.bits.carve_track_east && occ.bits.carve_track_north && occ.bits.carve_track_south) return (char)0xCC; // NSE if (occ.bits.carve_track_east && occ.bits.carve_track_north && occ.bits.carve_track_west) return (char)0xCA; // NEW if (occ.bits.carve_track_east && occ.bits.carve_track_south && occ.bits.carve_track_west) return (char)0xCB; // SEW if (occ.bits.carve_track_north && occ.bits.carve_track_south && occ.bits.carve_track_west) return (char)0xB9; // NSW if (occ.bits.carve_track_north && occ.bits.carve_track_south) return (char)0xBA; // NS if (occ.bits.carve_track_east && occ.bits.carve_track_west) return (char)0xCD; // EW if (occ.bits.carve_track_east && occ.bits.carve_track_north) return (char)0xC8; // NE if (occ.bits.carve_track_north && occ.bits.carve_track_west) return (char)0xBC; // NW if (occ.bits.carve_track_east && occ.bits.carve_track_south) return (char)0xC9; // SE if (occ.bits.carve_track_south && occ.bits.carve_track_west) return (char)0xBB; // SW if (occ.bits.carve_track_north) return (char)0xD0; // N if (occ.bits.carve_track_south) return (char)0xD2; // S if (occ.bits.carve_track_east) return (char)0xC6; // E if (occ.bits.carve_track_west) return (char)0xB5; // W return (char)0xC5; // single line cross; should never happen } static bool is_smooth_wall(const df::coord &pos) { df::tiletype *tt = Maps::getTileType(pos); return tt && tileSpecial(*tt) == df::tiletype_special::SMOOTH && tileShape(*tt) == df::tiletype_shape::WALL; } static bool blink(int delay) { return (Core::getInstance().p->getTickCount()/delay) % 2 == 0; } static char get_tile_char(const df::coord &pos, char desig_char, bool draw_priority) { if (!draw_priority) return desig_char; std::vector priorities; Maps::SortBlockEvents(Maps::getTileBlock(pos), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &priorities); if (priorities.empty()) return desig_char; switch (priorities[0]->priority[pos.x % 16][pos.y % 16] / 1000) { case 1: return '1'; case 2: return '2'; case 3: return '3'; case 4: return '4'; case 5: return '5'; case 6: return '6'; case 7: return '7'; default: return '4'; } } static void paintScreenCarve() { DEBUG(log).print("entering paintScreenCarve\n"); if (Screen::inGraphicsMode() || blink(500)) return; Designations designations; bool draw_priority = blink(1000); auto dims = Gui::getDwarfmodeViewDims().map(); for (int y = dims.first.y; y <= dims.second.y; ++y) { for (int x = dims.first.x; x <= dims.second.x; ++x) { df::coord map_pos(*window_x + x, *window_y + y, *window_z); if (!Maps::isValidTilePos(map_pos)) continue; if (!Maps::isTileVisible(map_pos)) { TRACE(log).print("skipping hidden tile\n"); continue; } TRACE(log).print("scanning map tile at (%d, %d, %d) screen offset (%d, %d)\n", map_pos.x, map_pos.y, map_pos.z, x, y); Screen::Pen cur_tile; cur_tile.fg = COLOR_DARKGREY; auto des = designations.get(map_pos); if (is_designated_for_smoothing(des)) { if (is_smooth_wall(map_pos)) cur_tile.ch = get_tile_char(map_pos, (char)206, draw_priority); // hash, indicating a fortification designation else cur_tile.ch = get_tile_char(map_pos, (char)219, draw_priority); // solid block, indicating a smoothing designation } else if (is_designated_for_engraving(des)) { cur_tile.ch = get_tile_char(map_pos, (char)10, draw_priority); // solid block with a circle on it } else if (is_designated_for_track_carving(des)) { cur_tile.ch = get_tile_char(map_pos, get_track_char(des), draw_priority); // directional track } else { TRACE(log).print("skipping tile with no carving designation\n"); continue; } Screen::paintTile(cur_tile, x, y, true); } } } DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_LUA_FUNCTION(paintScreenPathable), DFHACK_LUA_FUNCTION(paintScreenWarmDamp), DFHACK_LUA_FUNCTION(paintScreenCarve), DFHACK_LUA_END };