/** * Translates a region of tiles specified by the cursor and arguments/prompts * into a series of blueprint files suitable for replay via quickfort. * * Written by cdombroski. */ #include #include #include #include #include "LuaTools.h" #include "TileTypes.h" #include "modules/Buildings.h" #include "modules/Filesystem.h" #include "modules/Gui.h" #include "df/building_axle_horizontalst.h" #include "df/building_bridgest.h" #include "df/building_constructionst.h" #include "df/building_furnacest.h" #include "df/building_rollersst.h" #include "df/building_screw_pumpst.h" #include "df/building_siegeenginest.h" #include "df/building_trapst.h" #include "df/building_water_wheelst.h" #include "df/building_workshopst.h" using std::string; using std::endl; using std::vector; using std::ofstream; using std::swap; using std::find; using std::pair; using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("blueprint"); enum phase {DIG=1, BUILD=2, PLACE=4, QUERY=8}; command_result blueprint(color_ostream &out, vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { commands.push_back(PluginCommand("blueprint", "Record the structure of a live game map in a quickfort blueprint", blueprint, false)); return CR_OK; } DFhackCExport command_result plugin_shutdown(color_ostream &out) { return CR_OK; } command_result help(color_ostream &out) { out << "blueprint width height depth name [dig] [build] [place] [query]" << endl << " width, height, depth: area to translate in tiles" << endl << " name: base name for blueprint files" << endl << " dig: generate blueprints for digging" << endl << " build: generate blueprints for building" << endl << " place: generate blueprints for stockpiles" << endl << " query: generate blueprints for querying (room designations)" << endl << " defaults to generating all blueprints" << endl << endl << "blueprint translates a portion of your fortress into blueprints suitable for" << endl << " digfort/fortplan/quickfort. Blueprints are created in the \"blueprints\"" << endl << " subdirectory of the DF folder with names following a \"name-phase.csv\" pattern." << endl << " Translation starts at the current cursor location and includes all tiles in the" << endl << " range specified." << endl; return CR_OK; } pair get_building_size(df::building* b) { return pair(b->x2 - b->x1 + 1, b->y2 - b->y1 + 1); } char get_tile_dig(int32_t x, int32_t y, int32_t z) { df::tiletype *tt = Maps::getTileType(x, y , z); df::tiletype_shape ts = tileShape(tt ? *tt : tiletype::Void); switch (ts) { case tiletype_shape::EMPTY: case tiletype_shape::RAMP_TOP: return 'h'; case tiletype_shape::FLOOR: case tiletype_shape::BOULDER: case tiletype_shape::PEBBLES: case tiletype_shape::BROOK_TOP: return 'd'; case tiletype_shape::FORTIFICATION: return 'F'; case tiletype_shape::STAIR_UP: return 'u'; case tiletype_shape::STAIR_DOWN: return 'j'; case tiletype_shape::STAIR_UPDOWN: return 'i'; case tiletype_shape::RAMP: return 'r'; default: return ' '; } } string get_tile_build(uint32_t x, uint32_t y, df::building* b) { if (! b) return " "; bool at_nw_corner = int32_t(x) == b->x1 && int32_t(y) == b->y1; bool at_se_corner = int32_t(x) == b->x2 && int32_t(y) == b->y2; bool at_center = int32_t(x) == b->centerx && int32_t(y) == b->centery; pair size = get_building_size(b); stringstream out;// = stringstream(); switch(b->getType()) { case building_type::Armorstand: return "a"; case building_type::Bed: return "b"; case building_type::Chair: return "c"; case building_type::Door: return "d"; case building_type::Floodgate: return "x"; case building_type::Cabinet: return "f"; case building_type::Box: return "h"; //case building_type::Kennel is missing case building_type::FarmPlot: if(!at_nw_corner) return "`"; out << "p(" << size.first << "x" << size.second << ")"; return out.str(); case building_type::Weaponrack: return "r"; case building_type::Statue: return "s"; case building_type::Table: return "t"; case building_type::RoadPaved: if(! at_nw_corner) return "`"; out << "o(" << size.first << "x" << size.second << ")"; return out.str(); case building_type::RoadDirt: if(! at_nw_corner) return "`"; out << "O(" << size.first << "x" << size.second << ")"; return out.str(); case building_type::Bridge: if(! at_nw_corner) return "`"; switch(((df::building_bridgest*) b)->direction) { case df::building_bridgest::T_direction::Down: out << "gx"; break; case df::building_bridgest::T_direction::Left: out << "ga"; break; case df::building_bridgest::T_direction::Up: out << "gw"; break; case df::building_bridgest::T_direction::Right: out << "gd"; break; case df::building_bridgest::T_direction::Retracting: out << "gs"; break; } out << "(" << size.first << "x" << size.second << ")"; return out.str(); case building_type::Well: return "l"; case building_type::SiegeEngine: if (! at_center) return "`"; return ((df::building_siegeenginest*) b)->type == df::siegeengine_type::Ballista ? "ib" : "ic"; case building_type::Workshop: if (! at_center) return "`"; switch (((df::building_workshopst*) b)->type) { case workshop_type::Leatherworks: return "we"; case workshop_type::Quern: return "wq"; case workshop_type::Millstone: return "wM"; case workshop_type::Loom: return "wo"; case workshop_type::Clothiers: return "wk"; case workshop_type::Bowyers: return "wb"; case workshop_type::Carpenters: return "wc"; case workshop_type::MetalsmithsForge: return "wf"; case workshop_type::MagmaForge: return "wv"; case workshop_type::Jewelers: return "wj"; case workshop_type::Masons: return "wm"; case workshop_type::Butchers: return "wu"; case workshop_type::Tanners: return "wn"; case workshop_type::Craftsdwarfs: return "wr"; case workshop_type::Siege: return "ws"; case workshop_type::Mechanics: return "wt"; case workshop_type::Still: return "wl"; case workshop_type::Farmers: return "ww"; case workshop_type::Kitchen: return "wz"; case workshop_type::Fishery: return "wh"; case workshop_type::Ashery: return "wy"; case workshop_type::Dyers: return "wd"; case workshop_type::Kennels: return "k"; case workshop_type::Custom: case workshop_type::Tool: //can't do anything with custom workshop return "`"; } case building_type::Furnace: if (! at_center) return "`"; switch (((df::building_furnacest*) b)->type) { case furnace_type::WoodFurnace: return "ew"; case furnace_type::Smelter: return "es"; case furnace_type::GlassFurnace: return "eg"; case furnace_type::Kiln: return "ek"; case furnace_type::MagmaSmelter: return "el"; case furnace_type::MagmaGlassFurnace: return "ea"; case furnace_type::MagmaKiln: return "en"; case furnace_type::Custom: //can't do anything with custom furnace return "`"; } case building_type::WindowGlass: return "y"; case building_type::WindowGem: return "Y"; case building_type::Construction: switch (((df::building_constructionst*) b)->type) { case construction_type::NONE: return "`"; case construction_type::Fortification: return "CF"; case construction_type::Wall: return "CW"; case construction_type::Floor: return "Cf"; case construction_type::UpStair: return "Cu"; case construction_type::DownStair: return "Cj"; case construction_type::UpDownStair: return "Cx"; case construction_type::Ramp: return "Cr"; case construction_type::TrackN: return "trackN"; case construction_type::TrackS: return "trackS"; case construction_type::TrackE: return "trackE"; case construction_type::TrackW: return "trackW"; case construction_type::TrackNS: return "trackNS"; case construction_type::TrackNE: return "trackNE"; case construction_type::TrackNW: return "trackNW"; case construction_type::TrackSE: return "trackSE"; case construction_type::TrackSW: return "trackSW"; case construction_type::TrackEW: return "trackEW"; case construction_type::TrackNSE: return "trackNSE"; case construction_type::TrackNSW: return "trackNSW"; case construction_type::TrackNEW: return "trackNEW"; case construction_type::TrackSEW: return "trackSEW"; case construction_type::TrackNSEW: return "trackNSEW"; case construction_type::TrackRampN: return "trackrampN"; case construction_type::TrackRampS: return "trackrampS"; case construction_type::TrackRampE: return "trackrampE"; case construction_type::TrackRampW: return "trackrampW"; case construction_type::TrackRampNS: return "trackrampNS"; case construction_type::TrackRampNE: return "trackrampNE"; case construction_type::TrackRampNW: return "trackrampNW"; case construction_type::TrackRampSE: return "trackrampSE"; case construction_type::TrackRampSW: return "trackrampSW"; case construction_type::TrackRampEW: return "trackrampEW"; case construction_type::TrackRampNSE: return "trackrampNSE"; case construction_type::TrackRampNSW: return "trackrampNSW"; case construction_type::TrackRampNEW: return "trackrampNEW"; case construction_type::TrackRampSEW: return "trackrampSEW"; case construction_type::TrackRampNSEW: return "trackrampNSEW"; } case building_type::Shop: if (! at_center) return "`"; return "z"; case building_type::AnimalTrap: return "m"; case building_type::Chain: return "v"; case building_type::Cage: return "j"; case building_type::TradeDepot: if (! at_center) return "`"; return "D"; case building_type::Trap: switch (((df::building_trapst*) b)->trap_type) { case trap_type::StoneFallTrap: return "Ts"; case trap_type::WeaponTrap: return "Tw"; case trap_type::Lever: return "Tl"; case trap_type::PressurePlate: return "Tp"; case trap_type::CageTrap: return "Tc"; case trap_type::TrackStop: df::building_trapst* ts = (df::building_trapst*) b; out << "CS"; if (ts->use_dump) { if (ts->dump_x_shift == 0) { if (ts->dump_y_shift > 0) out << "dd"; else out << "d"; } else { if (ts->dump_x_shift > 0) out << "ddd"; else out << "dddd"; } } switch (ts->friction) { case 10: out << "a"; case 50: out << "a"; case 500: out << "a"; case 10000: out << "a"; } return out.str(); } case building_type::ScrewPump: if (! at_se_corner) //screw pumps anchor at bottom/right return "`"; switch (((df::building_screw_pumpst*) b)->direction) { case screw_pump_direction::FromNorth: return "Msu"; case screw_pump_direction::FromEast: return "Msk"; case screw_pump_direction::FromSouth: return "Msm"; case screw_pump_direction::FromWest: return "Msh"; } case building_type::WaterWheel: if (! at_center) return "`"; //s swaps orientation which defaults to vertical return ((df::building_water_wheelst*) b)->is_vertical ? "Mw" : "Mws"; case building_type::Windmill: if (! at_center) return "`"; return "Mm"; case building_type::GearAssembly: return "Mg"; case building_type::AxleHorizontal: if (! at_nw_corner) //a guess based on how constructions work return "`"; //same as water wheel but reversed out << "Mh" << (((df::building_axle_horizontalst*) b)->is_vertical ? "s" : "") << "(" << size.first << "x" << size.second << ")"; return out.str(); case building_type::AxleVertical: return "Mv"; case building_type::Rollers: if (! at_nw_corner) return "`"; out << "Mr"; switch (((df::building_rollersst*) b)->direction) { case screw_pump_direction::FromNorth: break; case screw_pump_direction::FromEast: out << "s"; case screw_pump_direction::FromSouth: out << "s"; case screw_pump_direction::FromWest: out << "s"; } out << "(" << size.first << "x" << size.second << ")"; return out.str(); case building_type::Support: return "S"; case building_type::ArcheryTarget: return "A"; case building_type::TractionBench: return "R"; case building_type::Hatch: return "H"; case building_type::Slab: //how to mine alt key?!? //alt+s return " "; case building_type::NestBox: return "N"; case building_type::Hive: //alt+h return " "; case building_type::GrateWall: return "W"; case building_type::GrateFloor: return "G"; case building_type::BarsVertical: return "B"; case building_type::BarsFloor: //alt+b return " "; default: return " "; } } string get_tile_place(uint32_t x, uint32_t y, df::building* b) { if (! b || b->getType() != building_type::Stockpile) return " "; if (b->x1 != int32_t(x) || b->y1 != int32_t(y)) return "`"; pair size = get_building_size(b); df::building_stockpilest* sp = (df::building_stockpilest*) b; stringstream out;// = stringstream(); switch (sp->settings.flags.whole) { case df::stockpile_group_set::mask_animals: out << "a"; break; case df::stockpile_group_set::mask_food: out << "f"; break; case df::stockpile_group_set::mask_furniture: out << "u"; break; case df::stockpile_group_set::mask_corpses: out << "y"; break; case df::stockpile_group_set::mask_refuse: out << "r"; break; case df::stockpile_group_set::mask_wood: out << "w"; break; case df::stockpile_group_set::mask_stone: out << "s"; break; case df::stockpile_group_set::mask_gems: out << "e"; break; case df::stockpile_group_set::mask_bars_blocks: out << "b"; break; case df::stockpile_group_set::mask_cloth: out << "h"; break; case df::stockpile_group_set::mask_leather: out << "l"; break; case df::stockpile_group_set::mask_ammo: out << "z"; break; case df::stockpile_group_set::mask_coins: out << "n"; break; case df::stockpile_group_set::mask_finished_goods: out << "g"; break; case df::stockpile_group_set::mask_weapons: out << "p"; break; case df::stockpile_group_set::mask_armor: out << "d"; break; default: //multiple stockpile type return "`"; } out << "("<< size.first << "x" << size.second << ")"; return out.str(); } string get_tile_query(df::building* b) { if (b && b->is_room) return "r+"; return " "; } void init_stream(ofstream &out, std::string basename, std::string target) { std::ostringstream out_path; out_path << basename << "-" << target << ".csv"; out.open(out_path.str(), ofstream::trunc); out << "#" << target << endl; } command_result do_transform(DFCoord start, DFCoord end, string name, uint32_t phases, std::ostringstream &err) { ofstream dig, build, place, query; std::string basename = "blueprints/" + name; #ifdef _WIN32 // normalize to forward slashes std::replace(basename.begin(), basename.end(), '\\', '/'); #endif size_t last_slash = basename.find_last_of("/"); std::string parent_path = basename.substr(0, last_slash); // create output directory if it doesn't already exist std::error_code ec; if (!Filesystem::mkdir_recursive(parent_path)) { err << "could not create output directory: '" << parent_path << "'"; return CR_FAILURE; } if (phases & QUERY) { //query = ofstream((name + "-query.csv").c_str(), ofstream::trunc); init_stream(query, basename, "query"); } if (phases & PLACE) { //place = ofstream(name + "-place.csv", ofstream::trunc); init_stream(place, basename, "place"); } if (phases & BUILD) { //build = ofstream(name + "-build.csv", ofstream::trunc); init_stream(build, basename, "build"); } if (phases & DIG) { //dig = ofstream(name + "-dig.csv", ofstream::trunc); init_stream(dig, basename, "dig"); } if (start.x > end.x) { swap(start.x, end.x); start.x++; end.x++; } if (start.y > end.y) { swap(start.y, end.y); start.y++; end.y++; } if (start.z > end.z) { swap(start.z, end.z); start.z++; end.z++; } for (int32_t z = start.z; z < end.z; z++) { for (int32_t y = start.y; y < end.y; y++) { for (int32_t x = start.x; x < end.x; x++) { df::building* b = DFHack::Buildings::findAtTile(DFCoord(x, y, z)); if (phases & QUERY) query << get_tile_query(b) << ','; if (phases & PLACE) place << get_tile_place(x, y, b) << ','; if (phases & BUILD) build << get_tile_build(x, y, b) << ','; if (phases & DIG) dig << get_tile_dig(x, y, z) << ','; } if (phases & QUERY) query << "#" << endl; if (phases & PLACE) place << "#" << endl; if (phases & BUILD) build << "#" << endl; if (phases & DIG) dig << "#" << endl; } if (z < end.z - 1) { if (phases & QUERY) query << "#<" << endl; if (phases & PLACE) place << "#<" << endl; if (phases & BUILD) build << "#<" << endl; if (phases & DIG) dig << "#<" << endl; } } if (phases & QUERY) query.close(); if (phases & PLACE) place.close(); if (phases & BUILD) build.close(); if (phases & DIG) dig.close(); return CR_OK; } bool cmd_option_exists(vector& parameters, const string& option) { return find(parameters.begin(), parameters.end(), option) != parameters.end(); } command_result blueprint(color_ostream &out, vector ¶meters) { if (parameters.size() < 4 || parameters.size() > 8) return help(out); CoreSuspender suspend; if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); return CR_FAILURE; } int32_t x, y, z; if (!Gui::getCursorCoords(x, y, z)) { out.printerr("Can't get cursor coords! Make sure you have an active cursor in DF.\n"); return CR_FAILURE; } DFCoord start (x, y, z); DFCoord end (x + stoi(parameters[0]), y + stoi(parameters[1]), z + stoi(parameters[2])); uint32_t option = 0; if (parameters.size() == 4) { option = DIG | BUILD | PLACE | QUERY; } else { if (cmd_option_exists(parameters, "dig")) option |= DIG; if (cmd_option_exists(parameters, "build")) option |= BUILD; if (cmd_option_exists(parameters, "place")) option |= PLACE; if (cmd_option_exists(parameters, "query")) option |= QUERY; } std::ostringstream err; DFHack::command_result result = do_transform(start, end, parameters[3], option, err); if (result != CR_OK) out.printerr("%s\n", err.str().c_str()); return result; } static int create(lua_State *L, uint32_t options) { df::coord start, end; lua_settop(L, 3); Lua::CheckDFAssign(L, &start, 1); if (!start.isValid()) luaL_argerror(L, 1, "invalid start position"); Lua::CheckDFAssign(L, &end, 2); if (!end.isValid()) luaL_argerror(L, 2, "invalid end position"); string filename(lua_tostring(L, 3)); std::ostringstream err; DFHack::command_result result = do_transform(start, end, filename, options, err); if (result != CR_OK) luaL_error(L, "%s", err.str().c_str()); lua_pushboolean(L, result); return 1; } static int dig(lua_State *L) { return create(L, DIG); } static int build(lua_State *L) { return create(L, BUILD); } static int place(lua_State *L) { return create(L, PLACE); } static int query(lua_State *L) { return create(L, QUERY); } DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(dig), DFHACK_LUA_COMMAND(build), DFHACK_LUA_COMMAND(place), DFHACK_LUA_COMMAND(query), DFHACK_LUA_END };