#include #include #include #include #include using namespace std; #include "Core.h" #include "Console.h" #include "Export.h" #include "PluginManager.h" #include "modules/MapCache.h" #include "MiscUtils.h" #include "DataDefs.h" #include "df/world.h" #include "df/world_data.h" #include "df/world_region_details.h" #include "df/world_region_feature.h" #include "df/world_geo_biome.h" #include "df/world_geo_layer.h" #include "df/world_underground_region.h" #include "df/feature_init.h" #include "df/region_map_entry.h" #include "df/inclusion_type.h" #include "df/viewscreen_choose_start_sitest.h" #include "df/plant.h" using namespace df::enums; using namespace DFHack; using namespace MapExtras; using df::global::world; command_result cmd_3dveins(color_ostream &out, vector & parameters); DFHACK_PLUGIN("3dveins"); DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) { commands.push_back(PluginCommand( "3dveins", "Rewrites the veins to make them extend in 3D space.", cmd_3dveins, false, " Run this after embark to change all veins on the map to a shape\n" " that consistently spans Z levels. The operation preserves the\n" " mineral counts reported by prospect.\n" )); return CR_OK; } DFhackCExport command_result plugin_shutdown ( color_ostream &out ) { return CR_OK; } /* * Data structures. */ template class BlockGrid { df::coord dim; std::vector buf; public: BlockGrid(df::coord size) : dim(size) { buf.resize(dim.x * dim.y * dim.z); } BlockGrid(df::coord2d size, int zdepth = 1) : dim(df::coord(size.x, size.y, zdepth)) { buf.resize(dim.x * dim.y * dim.z); } const df::coord &size() { return dim; } void resize_depth(int16_t zdepth) { dim.z = zdepth; buf.resize(dim.x * dim.y * dim.z); } void shift_depth(int16_t to_add) { dim.z += to_add; buf.insert(buf.begin(), dim.x*dim.y*to_add, T()); } T &operator() (int x, int y, int z = 0) { return buf[x + dim.x*(y + dim.y*z)]; } T &operator() (df::coord pos) { return buf[pos.x + dim.x*(pos.y + dim.y*pos.z)]; } T &operator() (df::coord2d pos, int z = 0) { return buf[pos.x + dim.x*(pos.y + dim.y*z)]; } }; enum SpecialMatCodes { // Tile not mapped to an actual tile SMC_NO_MAPPING = -1, // Tile not assigned to a vein yet SMC_LAYER = -2 }; struct GeoLayer; struct GeoColumn; struct GeoBiome; typedef std::pair t_veinkey; /* Representation of a block in geolayer coordinate space. * That is, it represents a block that would have happened * if geological layers weren't shifted in Z direction to * conform to terrain height. Computing veins in this system * of coordinates should make them nicely follow the layers. */ struct GeoBlock { GeoLayer *layer; GeoColumn *column; df::coord pos; df::tile_bitmask unmined; int16_t material[16][16]; uint8_t veintype[16][16]; float weight[16][16]; GeoBlock(GeoLayer *parent, df::coord pos) : layer(parent), pos(pos) { memset(material, -1, sizeof(material)); } }; struct GeoColumn { // Original Z level values for each layer int16_t min_level[16][16][BiomeInfo::MAX_LAYERS]; int16_t max_level[16][16][BiomeInfo::MAX_LAYERS]; int8_t top_layer[16][16]; int8_t bottom_layer[16][16]; int8_t top_solid_z[16][16]; GeoColumn() { memset(min_level, -1, sizeof(min_level)); memset(max_level, -1, sizeof(max_level)); memset(top_layer, -1, sizeof(top_layer)); memset(bottom_layer, -1, sizeof(bottom_layer)); memset(top_solid_z, -1, sizeof(top_solid_z)); } }; struct GeoLayer { GeoBiome *biome; int index; df::world_geo_layer *info; int thickness, z_bias; int16_t material; bool is_soil; // World-global origin coordinates in blocks df::coord world_pos; df::coord2d size; BlockGrid blocks; std::vector block_list; int tiles, unmined_tiles, mineral_tiles; std::map mineral_count; GeoLayer(GeoBiome *parent, int index, df::world_geo_layer *info); ~GeoLayer() { for (size_t i = 0; i < block_list.size(); i++) delete block_list[i]; } GeoBlock *blockAt(df::coord pos) { assert(pos.z >= 0); if (pos.z >= blocks.size().z) blocks.resize_depth(pos.z+1); GeoBlock *&blk = blocks(pos); if (!blk) { blk = new GeoBlock(this, pos); block_list.push_back(blk); } return blk; } GeoBlock *getBlockAt(df::coord pos) { if (pos.z < 0 || pos.z >= blocks.size().z) return NULL; return blocks(pos); } void setZBias(int bias) { if (bias <= z_bias) return; blocks.shift_depth(bias - z_bias); z_bias = bias; } void print_mineral_stats(color_ostream &out) { for (auto it = mineral_count.begin(); it != mineral_count.end(); ++it) out << " " << MaterialInfo(0,it->first.first).getToken() << " " << ENUM_KEY_STR(inclusion_type,it->first.second) << ": \t\t" << it->second << " (" << (float(it->second)/unmined_tiles) << ")" << std::endl; out.print(" Total tiles: %d (%d unmined)\n", tiles, unmined_tiles); } }; struct GeoBiome { const BiomeInfo &info; df::coord2d world_pos; df::coord2d size; BlockGrid columns; std::vector layers; GeoBiome(const BiomeInfo &biome, df::coord2d base, df::coord2d mapsize) : info(biome), world_pos(base), size(mapsize), columns(mapsize) { } ~GeoBiome() { for (size_t i = 0; i < layers.size(); i++) delete layers[i]; } bool init_layers(); void print_mineral_stats(color_ostream &out) { out.print("Geological biome %d:\n", info.geo_index); for (size_t i = 0; i < layers.size(); i++) if (layers[i]) { out << " Layer " << i << std::endl; layers[i]->print_mineral_stats(out); } } }; GeoLayer::GeoLayer(GeoBiome *parent, int index, df::world_geo_layer *info) : biome(parent), index(index), info(info), world_pos(parent->world_pos.x, parent->world_pos.y, -info->top_height), size(parent->size), blocks(parent->size) { thickness = info->top_height - info->bottom_height + 1; z_bias = 0; tiles = unmined_tiles = mineral_tiles = 0; material = info->mat_index; is_soil = isSoilInorganic(material); } struct VeinGenerator { color_ostream &out; MapCache map; df::coord2d size; df::coord2d base; std::map biomes; std::vector biome_by_idx; VeinGenerator(color_ostream &out) : out(out) {} ~VeinGenerator() { for (auto it = biomes.begin(); it != biomes.end(); ++it) delete it->second; } GeoLayer *mapLayer(Block *pb, df::coord2d tile); bool init_biomes(); bool scan_tiles(); bool scan_layer_depth(Block *b, df::coord2d column, int z); bool adjust_layer_depth(df::coord2d column); bool scan_block_tiles(Block *b, df::coord2d column, int z); void write_tiles(); void write_block_tiles(Block *b, df::coord2d column, int z); void print_mineral_stats() { for (auto it = biomes.begin(); it != biomes.end(); ++it) it->second->print_mineral_stats(out); } }; /* * General initialization */ bool VeinGenerator::init_biomes() { biome_by_idx.resize(map.getBiomeCount()); size = df::coord2d(map.maxBlockX()+1, map.maxBlockY()+1); base = df::coord2d(world->map.region_x*3, world->map.region_y*3); for (size_t i = 0; i < biome_by_idx.size(); i++) { const BiomeInfo &info = map.getBiomeByIndex(i); if (info.geo_index < 0 || !info.geobiome) { out.printerr("Biome %d is not defined.\n", i); return false; } GeoBiome *&biome = biomes[info.geo_index]; if (!biome) { biome = new GeoBiome(info, base, size); if (!biome->init_layers()) return false; } biome_by_idx[i] = biome; } return true; } bool GeoBiome::init_layers() { auto &info_layers = info.geobiome->layers; layers.resize(info_layers.size()); for (size_t i = 0; i < layers.size(); i++) { layers[i] = new GeoLayer(this, i, info_layers[i]); } return true; } /* * Initial statistics collection scan. It has to: * * 1) Find out how the layers are shifted in Z direction for each tile column. * 2) Record which tiles are available, which are unmined, and how much minerals are there. */ GeoLayer *VeinGenerator::mapLayer(Block *pb, df::coord2d tile) { int idx = pb->biomeIndexAt(tile); GeoBiome *biome = biome_by_idx.at(idx); int lidx = pb->layerIndexAt(tile); if (unsigned(lidx) >= biome->layers.size()) return NULL; return biome->layers[lidx]; } bool VeinGenerator::scan_tiles() { for (int x = 0; x < size.x; x++) { for (int y = 0; y < size.y; y++) { df::coord2d column(x,y); // First find where layers start and end for (int z = map.maxZ(); z >= 0; z--) { Block *b = map.BlockAt(df::coord(x,y,z)); if (!b || !b->is_valid()) continue; if (!scan_layer_depth(b, column, z)) return false; } if (!adjust_layer_depth(column)) return false; // Collect tile data for (int z = map.maxZ(); z >= 0; z--) { Block *b = map.BlockAt(df::coord(x,y,z)); if (!b || !b->is_valid()) continue; if (!scan_block_tiles(b, column, z)) return false; map.discardBlock(b); } // Discard this column of parsed blocks map.trash(); } } return true; } bool VeinGenerator::scan_layer_depth(Block *b, df::coord2d column, int z) { for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { df::coord2d tile(x,y); GeoLayer *layer = mapLayer(b, tile); if (!layer) continue; int idx = layer->index; auto &col_info = layer->biome->columns(column); auto &max_level = col_info.max_level[x][y]; auto &min_level = col_info.min_level[x][y]; auto &top = col_info.top_layer[x][y]; auto &top_solid = col_info.top_solid_z[x][y]; auto &bottom = col_info.bottom_layer[x][y]; if (top_solid < 0 && isWallTerrain(b->baseTiletypeAt(tile))) top_solid = z; if (max_level[idx] < 0) { // Do not start the layer stack in open air. // Those tiles can be very weird. if (bottom < 0 && isOpenTerrain(b->baseTiletypeAt(tile))) continue; max_level[idx] = min_level[idx] = z; if (top < 0 || idx < top) top = idx; bottom = std::max(idx, bottom); } else { if (z != min_level[idx]-1 && min_level[idx] <= top_solid) { out.printerr( "Discontinuous layer %d at (%d,%d,%d).\n", layer->index, x+column.x*16, y+column.y*16, z ); return false; } min_level[idx] = z; } } } return true; } bool VeinGenerator::adjust_layer_depth(df::coord2d column) { for (auto it = biomes.begin(); it != biomes.end(); ++it) { GeoBiome *biome = it->second; auto &col_info = biome->columns(column); for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { auto &max_level = col_info.max_level[x][y]; auto &min_level = col_info.min_level[x][y]; auto top_solid = col_info.top_solid_z[x][y]; int min_defined = col_info.top_layer[x][y]; int max_defined = col_info.bottom_layer[x][y]; if (max_defined < 0) continue; // Verify assumptions for (int i = min_defined; i < max_defined; i++) { if (max_level[i+1] < 0 && min_level[i] > top_solid) max_level[i+1] = min_level[i+1] = min_level[i]; if (max_level[i+1] > top_solid) continue; if (max_level[i+1] != min_level[i]-1) { out.printerr( "Gap or overlap with next layer %d at (%d,%d,%d-%d).\n", i+1, x+column.x*16, y+column.y*16, max_level[i+1], min_level[i] ); return false; } } for (int i = min_defined; i < max_defined; i++) { auto layer = biome->layers[i]; int size = max_level[i]-min_level[i]+1; if (size == layer->thickness) continue; // Adjust the top layers so that the bottom of the layer maps to the correct place if (max_level[i] >= top_solid) { max_level[i] += layer->thickness - size; if (size > layer->thickness) layer->setZBias(size - layer->thickness); continue; } out.printerr( "Layer height change in layer %d at (%d,%d,%d): %d instead of %d.\n", i, x+column.x*16, y+column.y*16, max_level[i], size, biome->layers[i]->thickness ); return false; } } } } return true; } bool VeinGenerator::scan_block_tiles(Block *b, df::coord2d column, int z) { for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { df::coord2d tile(x,y); GeoLayer *layer = mapLayer(b, tile); if (!layer) continue; auto tt = b->baseTiletypeAt(tile); bool wall = isWallTerrain(tt); bool matches = false; switch (tileMaterial(tt)) { case tiletype_material::MINERAL: matches = true; // Count minerals if (wall) { layer->mineral_tiles++; layer->mineral_count[t_veinkey(b->veinMaterialAt(tile),b->veinTypeAt(tile))]++; } break; case tiletype_material::SOIL: matches = layer->is_soil; break; case tiletype_material::STONE: matches = !layer->is_soil; break; default:; } // This tile should be overwritten if (matches) { auto &col_info = layer->biome->columns(column); int max_level = col_info.max_level[x][y][layer->index] + layer->z_bias; GeoBlock *block = layer->blockAt(df::coord(column, max_level-z)); block->material[x][y] = SMC_LAYER; layer->tiles++; if (wall) { layer->unmined_tiles++; block->unmined.setassignment(x,y,true); } } } } return true; } /* * Vein writing pass. Without running generation it just deletes all veins. */ void VeinGenerator::write_tiles() { for (int x = 0; x < size.x; x++) { for (int y = 0; y < size.y; y++) { df::coord2d column(x,y); for (int z = map.maxZ(); z >= 0; z--) { Block *b = map.BlockAt(df::coord(x,y,z)); if (!b || !b->is_valid()) continue; write_block_tiles(b, column, z); b->Write(); map.discardBlock(b); } map.trash(); } } } void VeinGenerator::write_block_tiles(Block *b, df::coord2d column, int z) { for (int x = 0; x < 16; x++) { for (int y = 0; y < 16; y++) { df::coord2d tile(x,y); GeoLayer *layer = mapLayer(b, tile); if (!layer) continue; auto &col_info = layer->biome->columns(column); int max_level = col_info.max_level[x][y][layer->index] + layer->z_bias; GeoBlock *block = layer->getBlockAt(df::coord(column, max_level-z)); auto tt = b->baseTiletypeAt(tile); if (block && block->material[x][y] != SMC_NO_MAPPING) { bool ok; int mat = block->material[x][y]; df::inclusion_type vein = inclusion_type::CLUSTER; if (mat < 0) { if (layer->is_soil) ok = b->setSoilAt(tile, tt, true); else ok = b->setStoneAt(tile, tt, layer->material, vein, true, true); } else { vein = (df::inclusion_type)block->veintype[x][y]; ok = b->setStoneAt(tile, tt, mat, vein, true, true); } if (!ok) { out.printerr( "Couldn't write %d vein at (%d,%d,%d)\n", mat, x+column.x*16, y+column.y*16, z ); } } else { // Otherwise just kill latent veins if they happen to be there if (tileMaterial(tt) != tiletype_material::MINERAL) b->setVeinMaterialAt(tile, -1); } } } } command_result cmd_3dveins(color_ostream &con, vector & parameters) { if (!parameters.empty()) return CR_WRONG_USAGE; CoreSuspender suspend; if (!Maps::IsValid()) { con.printerr("Map is not available!\n"); return CR_FAILURE; } VeinGenerator generator(con); con.print("Collecting statistics...\n"); if (!generator.init_biomes()) return CR_FAILURE; if (!generator.scan_tiles()) return CR_FAILURE; generator.print_mineral_stats(); con.print("Writing tiles...\n"); generator.write_tiles(); return CR_OK; }