dfhack/plugins/blueprint.cpp

771 lines
23 KiB
C++

/**
* 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 <algorithm>
#include <sstream>
#include <Console.h>
#include <PluginManager.h>
#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 <string> &parameters);
DFhackCExport command_result plugin_init(color_ostream &out, vector<PluginCommand> &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<uint32_t, uint32_t> get_building_size(df::building* b)
{
return pair<uint32_t, uint32_t>(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<uint32_t, uint32_t> 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<uint32_t, uint32_t> 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<string>& parameters, const string& option)
{
return find(parameters.begin(), parameters.end(), option) != parameters.end();
}
command_result blueprint(color_ostream &out, vector<string> &parameters)
{
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
};