diff --git a/plugins/blueprint.cpp b/plugins/blueprint.cpp index 4077b1190..63e721bde 100644 --- a/plugins/blueprint.cpp +++ b/plugins/blueprint.cpp @@ -35,6 +35,7 @@ using std::endl; using std::ofstream; using std::pair; +using std::map; using std::string; using std::vector; using namespace DFHack; @@ -50,6 +51,10 @@ struct blueprint_options { // coordinates are set to -30000) df::coord start; + // output file format. this could be an enum if we set up the boilerplate + // for it. + string format; + // file splitting strategy. this could be an enum if we set up the // boilerplate for it. string split_strategy; @@ -77,6 +82,7 @@ struct blueprint_options { static const struct_field_info blueprint_options_fields[] = { { struct_field_info::PRIMITIVE, "help", offsetof(blueprint_options, help), &df::identity_traits::identity, 0, 0 }, { struct_field_info::SUBSTRUCT, "start", offsetof(blueprint_options, start), &df::coord::_identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "format", offsetof(blueprint_options, format), df::identity_traits::get(), 0, 0 }, { struct_field_info::PRIMITIVE, "split_strategy", offsetof(blueprint_options, split_strategy), df::identity_traits::get(), 0, 0 }, { struct_field_info::PRIMITIVE, "width", offsetof(blueprint_options, width), &df::identity_traits::identity, 0, 0 }, { struct_field_info::PRIMITIVE, "height", offsetof(blueprint_options, height), &df::identity_traits::identity, 0, 0 }, @@ -93,517 +99,498 @@ struct_identity blueprint_options::_identity(sizeof(blueprint_options), &df::all command_result blueprint(color_ostream &, vector &); -DFhackCExport command_result plugin_init(color_ostream &, vector &commands) -{ +DFhackCExport command_result plugin_init(color_ostream &, vector &commands) { commands.push_back(PluginCommand("blueprint", "Record the structure of a live game map in a quickfort blueprint file", blueprint, false)); return CR_OK; } -DFhackCExport command_result plugin_shutdown(color_ostream &) -{ +DFhackCExport command_result plugin_shutdown(color_ostream &) { return CR_OK; } -static pair get_building_size(df::building* b) -{ +struct tile_context { + bool pretty = false; + df::building* b = NULL; +}; + +static pair get_building_size(df::building *b) { return pair(b->x2 - b->x1 + 1, b->y2 - b->y1 + 1); } -static 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) +static const char * if_pretty(const tile_context &ctx, const char *c) { + return ctx.pretty ? c : ""; +} + +static void get_tile_dig(const df::coord &pos, const tile_context &, + string &str) { + df::tiletype *tt = Maps::getTileType(pos); + switch (tileShape(tt ? *tt : tiletype::Void)) { case tiletype_shape::EMPTY: case tiletype_shape::RAMP_TOP: - return 'h'; + str = "h"; break; case tiletype_shape::FLOOR: case tiletype_shape::BOULDER: case tiletype_shape::PEBBLES: case tiletype_shape::BROOK_TOP: - return 'd'; + str = "d"; break; case tiletype_shape::FORTIFICATION: - return 'F'; + str = "F"; break; case tiletype_shape::STAIR_UP: - return 'u'; + str = "u"; break; case tiletype_shape::STAIR_DOWN: - return 'j'; + str = "j"; break; case tiletype_shape::STAIR_UPDOWN: - return 'i'; + str = "i"; break; case tiletype_shape::RAMP: - return 'r'; + str = "r"; break; + case tiletype_shape::WALL: default: - return ' '; + break; } } -static 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()) +static void do_block_building(const tile_context &ctx, string &str, string s, + bool at_target_pos, bool *add_size = NULL) { + if(!at_target_pos) { + str = if_pretty(ctx, "`"); + return; + } + str = s; + if (add_size) + *add_size = true; +} + +static string get_bridge_str(df::building *b) { + df::building_bridgest *bridge = virtual_cast(b); + if (!bridge) + return "g"; + + switch(bridge->direction) { + case df::building_bridgest::T_direction::Retracting: return "gs"; + case df::building_bridgest::T_direction::Left: return "ga"; + case df::building_bridgest::T_direction::Right: return "gd"; + case df::building_bridgest::T_direction::Up: return "gw"; + case df::building_bridgest::T_direction::Down: return "gx"; + default: + return "g"; + } +} + +static string get_siege_str(df::building *b) { + df::building_siegeenginest *se = + virtual_cast(b); + return !se || se->type == df::siegeengine_type::Catapult ? "ic" : "ib"; +} + +static string get_workshop_str(df::building *b) { + df::building_workshopst *ws = virtual_cast(b); + if (!ws) + return "~"; + + switch (ws->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: + default: + return "~"; + } +} + +static string get_furnace_str(df::building *b) { + df::building_furnacest *furnace = virtual_cast(b); + if (!furnace) + return "~"; + + switch (furnace->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: + default: + return "~"; + } +} + +static string get_construction_str(df::building *b) { + df::building_constructionst *cons = + virtual_cast(b); + if (!cons) + return "~"; + + switch (cons->type) { + 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 construction_type::NONE: + default: + return "~"; + } +} + +static string get_trap_str(df::building *b) { + df::building_trapst *trap = virtual_cast(b); + if (!trap) + return "~"; + + switch (trap->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: + { + std::ostringstream buf; + buf << "CS"; + if (trap->use_dump) { + if (trap->dump_x_shift == 0) { + buf << "d"; + if (trap->dump_y_shift > 0) + buf << "d"; + } else { + buf << "ddd"; + if (trap->dump_x_shift < 0) + buf << "d"; + } + } + + // each case falls through and is additive + switch (trap->friction) { + case 10: buf << "a"; + case 50: buf << "a"; + case 500: buf << "a"; + case 10000: buf << "a"; + } + return buf.str(); + } + default: + return "~"; + } +} + +static string get_screw_pump_str(df::building *b) { + df::building_screw_pumpst *sp = virtual_cast(b); + if (!sp) + return "~"; + + switch (sp->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"; + default: + return "~"; + } +} + +static string get_water_wheel_str(df::building *b) { + df::building_water_wheelst *ww = + virtual_cast(b); + if (!ww) + return "~"; + + return ww->is_vertical ? "Mw" : "Mws"; +} + +static string get_axle_str(df::building *b) { + df::building_axle_horizontalst *ah = + virtual_cast(b); + if (!ah) + return "~"; + + return ah->is_vertical ? "Mhs" : "Mh"; +} + +static string get_roller_str(df::building *b) { + df::building_rollersst *r = virtual_cast(b); + if (!r) + return "~"; + + switch (r->direction) { + case screw_pump_direction::FromNorth: return "Mr"; + case screw_pump_direction::FromEast: return "Mrs"; + case screw_pump_direction::FromSouth: return "Mrss"; + case screw_pump_direction::FromWest: return "Mrsss"; + default: + return "~"; + } +} + +static string get_expansion_str(df::building *b) { + pair size = get_building_size(b); + std::ostringstream s; + s << "(" << size.first << "x" << size.second << ")"; + return s.str(); +} + +static void get_tile_build(const df::coord &pos, const tile_context &ctx, + string &str) { + if (!ctx.b || ctx.b->getType() == building_type::Stockpile) { + return; + } + + bool at_nw_corner = static_cast(pos.x) == ctx.b->x1 + && static_cast(pos.y) == ctx.b->y1; + bool at_se_corner = static_cast(pos.x) == ctx.b->x2 + && static_cast(pos.y) == ctx.b->y2; + bool at_center = static_cast(pos.x) == ctx.b->centerx + && static_cast(pos.y) == ctx.b->centery; + bool add_size = false; + + switch(ctx.b->getType()) { case building_type::Armorstand: - return "a"; + str = "a"; break; case building_type::Bed: - return "b"; + str = "b"; break; case building_type::Chair: - return "c"; + str = "c"; break; case building_type::Door: - return "d"; + str = "d"; break; case building_type::Floodgate: - return "x"; + str = "x"; break; case building_type::Cabinet: - return "f"; + str = "f"; break; case building_type::Box: - return "h"; - //case building_type::Kennel is missing + str = "h"; break; + //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(); + do_block_building(ctx, str, "p", at_nw_corner, &add_size); break; case building_type::Weaponrack: - return "r"; + str = "r"; break; case building_type::Statue: - return "s"; + str = "s"; break; case building_type::Table: - return "t"; + str = "t"; break; case building_type::RoadPaved: - if(! at_nw_corner) - return "`"; - out << "o(" << size.first << "x" << size.second << ")"; - return out.str(); + do_block_building(ctx, str, "o", at_nw_corner, &add_size); break; case building_type::RoadDirt: - if(! at_nw_corner) - return "`"; - out << "O(" << size.first << "x" << size.second << ")"; - return out.str(); + do_block_building(ctx, str, "O", at_nw_corner, &add_size); break; 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(); + do_block_building(ctx, str, get_bridge_str(ctx.b), at_nw_corner, + &add_size); + break; case building_type::Well: - return "l"; + str = "l"; break; case building_type::SiegeEngine: - if (! at_center) - return "`"; - return ((df::building_siegeenginest*) b)->type == df::siegeengine_type::Ballista ? "ib" : "ic"; + do_block_building(ctx, str, get_siege_str(ctx.b), at_center); + break; 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 "`"; - } + do_block_building(ctx, str, get_workshop_str(ctx.b), at_center); + break; 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 "`"; - } + do_block_building(ctx, str, get_furnace_str(ctx.b), at_center); + break; case building_type::WindowGlass: - return "y"; + str = "y"; break; case building_type::WindowGem: - return "Y"; + str = "Y"; break; 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"; - } + str = get_construction_str(ctx.b); break; case building_type::Shop: - if (! at_center) - return "`"; - return "z"; + do_block_building(ctx, str, "z", at_center); + break; case building_type::AnimalTrap: - return "m"; + str = "m"; break; case building_type::Chain: - return "v"; + str = "v"; break; case building_type::Cage: - return "j"; + str = "j"; break; case building_type::TradeDepot: - if (! at_center) - return "`"; - return "D"; + do_block_building(ctx, str, "D", at_center); break; 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(); - } + str = get_trap_str(ctx.b); break; 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"; - } + do_block_building(ctx, str, get_screw_pump_str(ctx.b), at_se_corner); + break; 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"; + do_block_building(ctx, str, get_water_wheel_str(ctx.b), at_center); + break; case building_type::Windmill: - if (! at_center) - return "`"; - return "Mm"; + do_block_building(ctx, str, "Mm", at_center); break; case building_type::GearAssembly: - return "Mg"; + str = "Mg"; break; 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(); + do_block_building(ctx, str, get_axle_str(ctx.b), at_nw_corner, + &add_size); + break; case building_type::AxleVertical: - return "Mv"; + str = "Mv"; break; 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(); + do_block_building(ctx, str, get_roller_str(ctx.b), at_nw_corner, + &add_size); + break; case building_type::Support: - return "S"; + str = "S"; break; case building_type::ArcheryTarget: - return "A"; + str = "A"; break; case building_type::TractionBench: - return "R"; + str = "R"; break; case building_type::Hatch: - return "H"; + str = "H"; break; case building_type::Slab: //how to mine alt key?!? //alt+s - return " "; + str = "~"; break; case building_type::NestBox: - return "N"; + str = "N"; break; case building_type::Hive: //alt+h - return " "; + str = "~"; break; case building_type::GrateWall: - return "W"; + str = "W"; break; case building_type::GrateFloor: - return "G"; + str = "G"; break; case building_type::BarsVertical: - return "B"; + str = "B"; break; case building_type::BarsFloor: //alt+b - return " "; + str = "~"; break; default: - return " "; + str = if_pretty(ctx, "~"); + break; } + + if (add_size) + str.append(get_expansion_str(ctx.b)); } -static 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(); +static void get_tile_place(const df::coord &pos, const tile_context &ctx, + string &str) { + if (!ctx.b || ctx.b->getType() != building_type::Stockpile) + return; + + if (ctx.b->x1 != static_cast(pos.x) + || ctx.b->y1 != static_cast(pos.y)) { + str = if_pretty(ctx, "`"); + return; + } + + df::building_stockpilest* sp = + virtual_cast(ctx.b); + if (!sp) { + str = "~"; + return; + } + 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 "`"; + case df::stockpile_group_set::mask_animals: str = "a"; break; + case df::stockpile_group_set::mask_food: str = "f"; break; + case df::stockpile_group_set::mask_furniture: str = "u"; break; + case df::stockpile_group_set::mask_corpses: str = "y"; break; + case df::stockpile_group_set::mask_refuse: str = "r"; break; + case df::stockpile_group_set::mask_wood: str = "w"; break; + case df::stockpile_group_set::mask_stone: str = "s"; break; + case df::stockpile_group_set::mask_gems: str = "e"; break; + case df::stockpile_group_set::mask_bars_blocks: str = "b"; break; + case df::stockpile_group_set::mask_cloth: str = "h"; break; + case df::stockpile_group_set::mask_leather: str = "l"; break; + case df::stockpile_group_set::mask_ammo: str = "z"; break; + case df::stockpile_group_set::mask_coins: str = "n"; break; + case df::stockpile_group_set::mask_finished_goods: str = "g"; break; + case df::stockpile_group_set::mask_weapons: str = "p"; break; + case df::stockpile_group_set::mask_armor: str = "d"; break; + default: // multiple stockpile types + str = "~"; + return; } - out << "("<< size.first << "x" << size.second << ")"; - return out.str(); + + str.append(get_expansion_str(ctx.b)); } -static string get_tile_query(df::building* b) -{ - if (b && b->is_room) - return "r+"; - return " "; +static void get_tile_query(const df::coord &, const tile_context &ctx, + string &str) { + if (ctx.b && ctx.b->is_room) + str = "r+"; +} + +static bool create_output_dir(color_ostream &out, + const blueprint_options &opts) { + string basename = "blueprints/" + opts.name; + size_t last_slash = basename.find_last_of("/"); + string parent_path = basename.substr(0, last_slash); + + // create output directory if it doesn't already exist + if (!Filesystem::mkdir_recursive(parent_path)) { + out.printerr("could not create output directory: '%s'\n", + parent_path.c_str()); + return false; + } + return true; } static bool get_filename(string &fname, color_ostream &out, blueprint_options opts, // copy because we can't const - const string &phase) -{ + const string &phase) { auto L = Lua::Core::State; Lua::StackUnwinder top(L); if (!lua_checkstack(L, 3) || !Lua::PushModulePublic( - out, L, "plugins.blueprint", "get_filename")) - { + out, L, "plugins.blueprint", "get_filename")) { out.printerr("Failed to load blueprint Lua code\n"); return false; } @@ -611,15 +598,13 @@ static bool get_filename(string &fname, Lua::Push(L, &opts); Lua::Push(L, phase); - if (!Lua::SafeCall(out, L, 2, 1)) - { + if (!Lua::SafeCall(out, L, 2, 1)) { out.printerr("Failed Lua call to get_filename\n"); return false; } const char *s = lua_tostring(L, -1); - if (!s) - { + if (!s) { out.printerr("Failed to retrieve filename from get_filename\n"); return false; } @@ -628,8 +613,80 @@ static bool get_filename(string &fname, return true; } -static string get_modeline(const string &phase) -{ +typedef map bp_row; +typedef map bp_area; +typedef map bp_volume; + +static const bp_area NEW_AREA; +static const bp_row NEW_ROW; + +typedef void (get_tile_fn)(const df::coord &pos, const tile_context &ctx, + string &str); +typedef void (init_ctx_fn)(const df::coord &pos, tile_context &ctx); + +struct blueprint_processor { + bp_volume mapdata; + string phase; + get_tile_fn *get_tile; + init_ctx_fn *init_ctx; + blueprint_processor(const string &phase, get_tile_fn *get_tile, + init_ctx_fn *init_ctx = NULL) + : phase(phase), get_tile(get_tile), init_ctx(init_ctx) { } +}; + +static void write_minimal(ofstream &ofile, const blueprint_options &opts, + const bp_volume &mapdata) { + if (mapdata.begin() == mapdata.end()) + return; + + const string z_key = opts.depth > 0 ? "#<" : "#>"; + + int16_t zprev = 0; + for (auto area : mapdata) { + for ( ; zprev < area.first; ++zprev) + ofile << z_key << endl; + int16_t yprev = 0; + for (auto row : area.second) { + for ( ; yprev < row.first; ++yprev) + ofile << endl; + int16_t xprev = 0; + for (auto tile : row.second) { + for ( ; xprev < tile.first; ++xprev) + ofile << ","; + ofile << tile.second; + } + } + ofile << endl; + } +} + +static void write_pretty(ofstream &ofile, const blueprint_options &opts, + const bp_volume &mapdata) { + const string z_key = opts.depth > 0 ? "#<" : "#>"; + + int16_t absdepth = abs(opts.depth); + for (int16_t z = 0; z < absdepth; ++z) { + const bp_area *area = NULL; + if (mapdata.count(z)) + area = &mapdata.at(z); + for (int16_t y = 0; y < opts.height; ++y) { + const bp_row *row = NULL; + if (area && area->count(y)) + row = &area->at(y); + for (int16_t x = 0; x < opts.width; ++x) { + const string *tile = NULL; + if (row && row->count(x)) + tile = &row->at(x); + ofile << (tile ? *tile : " ") << ","; + } + ofile << "#" << endl; + } + if (z < absdepth - 1) + ofile << z_key << endl; + } +} + +static string get_modeline(const string &phase) { std::ostringstream modeline; modeline << "#" << phase << " label(" << phase << ")"; @@ -639,110 +696,95 @@ static string get_modeline(const string &phase) static bool write_blueprint(color_ostream &out, std::map &output_files, const blueprint_options &opts, - const string &phase, - const std::ostringstream &stream) -{ + const blueprint_processor &processor, + bool pretty) { string fname; - if (!get_filename(fname, out, opts, phase)) + if (!get_filename(fname, out, opts, processor.phase)) return false; if (!output_files.count(fname)) output_files[fname] = new ofstream(fname, ofstream::trunc); ofstream &ofile = *output_files[fname]; - ofile << get_modeline(phase) << endl; - ofile << stream.str(); + ofile << get_modeline(processor.phase) << endl; + + if (pretty) + write_pretty(ofile, opts, processor.mapdata); + else + write_minimal(ofile, opts, processor.mapdata); + return true; } +void ensure_building(const df::coord &pos, tile_context &ctx) { + if (ctx.b) + return; + ctx.b = Buildings::findAtTile(pos); +} + static bool do_transform(color_ostream &out, - const DFCoord &start, const DFCoord &end, + const df::coord &start, const df::coord &end, const blueprint_options &opts, - vector &filenames) -{ - std::ostringstream dig, build, place, query; + vector &filenames) { + vector processors; - string basename = "blueprints/" + opts.name; - size_t last_slash = basename.find_last_of("/"); - string parent_path = basename.substr(0, last_slash); + if (opts.auto_phase || opts.dig) + processors.push_back(blueprint_processor("dig", get_tile_dig)); + if (opts.auto_phase || opts.build) + processors.push_back(blueprint_processor("build", get_tile_build, + ensure_building)); + if (opts.auto_phase || opts.place) + processors.push_back(blueprint_processor("place", get_tile_place, + ensure_building)); + if (opts.auto_phase || opts.query) + processors.push_back(blueprint_processor("query", get_tile_query, + ensure_building)); - // create output directory if it doesn't already exist - std::error_code ec; - if (!Filesystem::mkdir_recursive(parent_path)) - { - out.printerr("could not create output directory: '%s'\n", - parent_path.c_str()); + if (processors.empty()) { + out.printerr("no phases requested! nothing to do!\n"); return false; } + if (!create_output_dir(out, opts)) + return false; + + const bool pretty = opts.format != "minimal"; const int32_t z_inc = start.z < end.z ? 1 : -1; - const string z_key = start.z < end.z ? "#<" : "#>"; - for (int32_t z = start.z; z != end.z; z += z_inc) - { - for (int32_t y = start.y; y < end.y; y++) - { - for (int32_t x = start.x; x < end.x; x++) - { - df::building* b = Buildings::findAtTile(DFCoord(x, y, z)); - if (opts.auto_phase || opts.query) - query << get_tile_query(b) << ','; - if (opts.auto_phase || opts.place) - place << get_tile_place(x, y, b) << ','; - if (opts.auto_phase || opts.build) - build << get_tile_build(x, y, b) << ','; - if (opts.auto_phase || opts.dig) - dig << get_tile_dig(x, y, z) << ','; + for (int32_t z = start.z; z != end.z; z += z_inc) { + for (int32_t y = start.y; y < end.y; y++) { + for (int32_t x = start.x; x < end.x; x++) { + df::coord pos(x, y, z); + tile_context ctx; + ctx.pretty = pretty; + for (blueprint_processor &processor : processors) { + if (processor.init_ctx) + processor.init_ctx(pos, ctx); + string tile_str; + processor.get_tile(pos, ctx, tile_str); + if (!tile_str.empty()) { + // ensure our z-index is in the order we want to write + auto area = processor.mapdata.emplace(abs(z - start.z), + NEW_AREA); + auto row = area.first->second.emplace(y - start.y, + NEW_ROW); + row.first->second[x - start.x] = tile_str; + } + } } - if (opts.auto_phase || opts.query) - query << "#" << endl; - if (opts.auto_phase || opts.place) - place << "#" << endl; - if (opts.auto_phase || opts.build) - build << "#" << endl; - if (opts.auto_phase || opts.dig) - dig << "#" << endl; - } - if (z != end.z - z_inc) - { - if (opts.auto_phase || opts.query) - query << z_key << endl; - if (opts.auto_phase || opts.place) - place << z_key << endl; - if (opts.auto_phase || opts.build) - build << z_key << endl; - if (opts.auto_phase || opts.dig) - dig << z_key << endl; } } std::map output_files; - string fname; - if (opts.auto_phase || opts.dig) - { - if (!write_blueprint(out, output_files, opts, "dig", dig)) - return false; - } - if (opts.auto_phase || opts.build) - { - if (!write_blueprint(out, output_files, opts, "build", build)) - return false; - } - if (opts.auto_phase || opts.place) - { - if (!write_blueprint(out, output_files, opts, "place", place)) - return false; - } - if (opts.auto_phase || opts.query) - { - if (!write_blueprint(out, output_files, opts, "query", query)) + for (blueprint_processor &processor : processors) { + if (!write_blueprint(out, output_files, opts, processor, pretty)) return false; } - for (auto &it : output_files) - { + for (auto &it : output_files) { filenames.push_back(it.first); it.second->close(); delete(it.second); } + output_files.clear(); return true; } @@ -756,8 +798,7 @@ static bool get_options(color_ostream &out, if (!lua_checkstack(L, parameters.size() + 2) || !Lua::PushModulePublic( - out, L, "plugins.blueprint", "parse_commandline")) - { + out, L, "plugins.blueprint", "parse_commandline")) { out.printerr("Failed to load blueprint Lua code\n"); return false; } @@ -773,8 +814,7 @@ static bool get_options(color_ostream &out, return true; } -static void print_help(color_ostream &out) -{ +static void print_help(color_ostream &out) { auto L = Lua::Core::State; Lua::StackUnwinder top(L); @@ -790,16 +830,13 @@ static void print_help(color_ostream &out) // names of the files that were generated static bool do_blueprint(color_ostream &out, const vector ¶meters, - vector &files) -{ + vector &files) { CoreSuspender suspend; - if (parameters.size() >= 1 && parameters[0] == "gui") - { + if (parameters.size() >= 1 && parameters[0] == "gui") { std::ostringstream command; command << "gui/blueprint"; - for (size_t i = 1; i < parameters.size(); ++i) - { + for (size_t i = 1; i < parameters.size(); ++i) { command << " " << parameters[i]; } string command_str = command.str(); @@ -810,31 +847,26 @@ static bool do_blueprint(color_ostream &out, } blueprint_options options; - if (!get_options(out, options, parameters) || options.help) - { + if (!get_options(out, options, parameters) || options.help) { print_help(out); return options.help; } - if (!Maps::IsValid()) - { + if (!Maps::IsValid()) { out.printerr("Map is not available!\n"); return false; } // start coordinates can come from either the commandline or the map cursor - DFCoord start(options.start); - if (start.x == -30000) - { - if (!Gui::getCursorCoords(start)) - { + df::coord start(options.start); + if (start.x == -30000) { + if (!Gui::getCursorCoords(start)) { out.printerr("Can't get cursor coords! Make sure you specify the" " --cursor parameter or have an active cursor in DF.\n"); return false; } } - if (!Maps::isValidTilePos(start)) - { + if (!Maps::isValidTilePos(start)) { out.printerr("Invalid start position: %d,%d,%d\n", start.x, start.y, start.z); return false; @@ -842,8 +874,8 @@ static bool do_blueprint(color_ostream &out, // end coords are one beyond the last processed coordinate. note that // options.depth can be negative. - DFCoord end(start.x + options.width, start.y + options.height, - start.z + options.depth); + df::coord end(start.x + options.width, start.y + options.height, + start.z + options.depth); // crop end coordinate to map bounds. we've already verified that start is // a valid coordinate, and width, height, and depth are non-zero, so our @@ -862,13 +894,11 @@ static bool do_blueprint(color_ostream &out, } // entrypoint when called from Lua. returns the names of the generated files -static int run(lua_State *L) -{ +static int run(lua_State *L) { int argc = lua_gettop(L); vector argv; - for (int i = 1; i <= argc; ++i) - { + for (int i = 1; i <= argc; ++i) { const char *s = lua_tostring(L, i); if (s == NULL) luaL_error(L, "all parameters must be strings"); @@ -879,8 +909,7 @@ static int run(lua_State *L) color_ostream *out = Lua::GetOutput(L); if (!out) out = &Core::getInstance().getConsole(); - if (do_blueprint(*out, argv, files)) - { + if (do_blueprint(*out, argv, files)) { Lua::PushVector(L, files); return 1; } @@ -888,11 +917,9 @@ static int run(lua_State *L) return 0; } -command_result blueprint(color_ostream &out, vector ¶meters) -{ +command_result blueprint(color_ostream &out, vector ¶meters) { vector files; - if (do_blueprint(out, parameters, files)) - { + if (do_blueprint(out, parameters, files)) { out.print("Generated blueprint file(s):\n"); for (string &fname : files) out.print(" %s\n", fname.c_str()); diff --git a/plugins/lua/blueprint.lua b/plugins/lua/blueprint.lua index 2708781ed..405b9de20 100644 --- a/plugins/lua/blueprint.lua +++ b/plugins/lua/blueprint.lua @@ -42,6 +42,12 @@ local valid_phase_list = { } valid_phases = utils.invert(valid_phase_list) +local valid_formats_list = { + 'minimal', + 'pretty', +} +valid_formats = utils.invert(valid_formats_list) + local valid_split_strategies_list = { 'none', 'phase', @@ -56,13 +62,20 @@ local function parse_cursor(opts, arg) utils.assign(opts.start, cursor) end -local function parse_split_strategy(opts, strategy) - if not valid_split_strategies[strategy] then - qerror(('unknown split strategy: "%s"; expected one of: %s') - :format(strategy, - table.concat(valid_split_strategies_list, ', '))) +local function parse_enum(opts, valid, name, val) + if not valid[val] then + qerror(('unknown %s: "%s"; expected one of: %s') + :format(name, val, table.concat(valid, ', '))) end - opts.split_strategy = strategy + opts[name] = val +end + +local function parse_split_strategy(opts, strategy) + parse_enum(opts, valid_split_strategies, 'split_strategy', strategy) +end + +local function parse_format(opts, file_format) + parse_enum(opts, valid_formats, 'format', file_format) end local function parse_positionals(opts, args, start_argidx) @@ -102,9 +115,15 @@ local function process_args(opts, args) return end + -- set defaults + opts.format = valid_formats_list[1] + opts.split_strategy = valid_split_strategies_list[1] + local positionals = argparse.processArgsGetopt(args, { {'c', 'cursor', hasArg=true, handler=function(optarg) parse_cursor(opts, optarg) end}, + {'f', 'format', hasArg=true, + handler=function(optarg) parse_format(opts, optarg) end}, {'h', 'help', handler=function() opts.help = true end}, {'t', 'splitby', hasArg=true, handler=function(optarg) parse_split_strategy(opts, optarg) end}, @@ -114,7 +133,6 @@ local function process_args(opts, args) return end - opts.split_strategy = opts.split_strategy or valid_split_strategies_list[1] return positionals end diff --git a/test/quickfort/ecosystem.lua b/test/quickfort/ecosystem.lua index ecde71a00..56f6471fb 100644 --- a/test/quickfort/ecosystem.lua +++ b/test/quickfort/ecosystem.lua @@ -194,7 +194,7 @@ local function run_blueprint(basename, set, pos) tostring(set.spec.height), tostring(-set.spec.depth), output_dir..basename, get_cursor_arg(pos), - '-tphase'} + '-tphase', '-fpretty'} for _,mode_name in pairs(mode_names) do if set.modes[mode_name] then table.insert(blueprint_args, mode_name) end end @@ -249,7 +249,7 @@ function test.end_to_end() end -- run dig-now to dig out designated tiles - dfhack.run_command('dig-now') + dfhack.run_command('dig-now', '--clean') -- quickfort run remaining blueprints for _,mode_name in pairs(mode_names) do