Implement generation of 3D veins that match the existing mineral counts.

Vein distributions may need some improvement.
develop
Alexander Gavrilov 2013-10-10 12:50:52 +04:00
parent 86d0022c58
commit 5bcae49249
3 changed files with 781 additions and 9 deletions

@ -28,7 +28,8 @@ DFHack future
- mousequery: Look and poke at the map elements with the mouse. - mousequery: Look and poke at the map elements with the mouse.
- autotrade: Automatically send items in marked stockpiles to trade depot, when trading is possible. - autotrade: Automatically send items in marked stockpiles to trade depot, when trading is possible.
- stocks: An improved stocks display screen. - stocks: An improved stocks display screen.
- 3dveins: Reshapes all veins on the map in a way that flows between Z levels.
DFHack v0.34.11-r3 DFHack v0.34.11-r3
Internals: Internals:

@ -361,6 +361,24 @@ Usage:
Map modification Map modification
================ ================
3dveins
-------
Removes all existing veins from the map and generates new ones using
3D Perlin noise, in order to produce a layout that smoothly flows between
Z levels. The vein distribution is based on the world seed, so running
the command for the second time should produce no change. It is best to
run it just once immediately after embark.
This command is intended as only a cosmetic change, so it takes
care to exactly preserve the mineral counts reported by ``prospect all``.
The amounts of different layer stone may slightly change in some cases
if vein mass shifts between Z layers.
This command is very unlikely to work on maps generated before version 0.34.08.
Note that there is no undo option other than restoring from backup.
changelayer changelayer
----------- -----------
Changes material of the geology layer under cursor to the specified inorganic Changes material of the geology layer under cursor to the specified inorganic

@ -4,12 +4,12 @@
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
using namespace std;
#include "Core.h" #include "Core.h"
#include "Console.h" #include "Console.h"
#include "Export.h" #include "Export.h"
#include "PluginManager.h" #include "PluginManager.h"
#include "modules/MapCache.h" #include "modules/MapCache.h"
#include "modules/Random.h"
#include "MiscUtils.h" #include "MiscUtils.h"
@ -27,14 +27,23 @@ using namespace std;
#include "df/viewscreen_choose_start_sitest.h" #include "df/viewscreen_choose_start_sitest.h"
#include "df/plant.h" #include "df/plant.h"
#ifdef LINUX_BUILD
#include <tr1/memory>
using std::tr1::shared_ptr;
#else
#include <memory>
using std::shared_ptr;
#endif
using namespace df::enums; using namespace df::enums;
using namespace DFHack; using namespace DFHack;
using namespace MapExtras; using namespace MapExtras;
using namespace DFHack::Random;
using df::global::world; using df::global::world;
command_result cmd_3dveins(color_ostream &out, vector <string> & parameters); command_result cmd_3dveins(color_ostream &out, std::vector <std::string> & parameters);
DFHACK_PLUGIN("3dveins"); DFHACK_PLUGIN("3dveins");
@ -55,6 +64,145 @@ DFhackCExport command_result plugin_shutdown ( color_ostream &out )
return CR_OK; return CR_OK;
} }
/*
* Vein density fields
*/
typedef std::pair<int,df::inclusion_type> t_veinkey;
struct NoiseFunction
{
typedef shared_ptr<NoiseFunction> Ptr;
typedef std::pair<float,float> t_range;
virtual ~NoiseFunction() {};
/*
* Veins are placed by clipping the computed value
* against a floating threshold, with values above
* the threshold causing placement of a vein tile.
*/
virtual float eval(float x, float y, float z) = 0;
virtual t_range range() = 0;
virtual void displace(float &x, float &y, float &z) = 0;
};
inline float apow(float a, float b) { return powf(fabsf(a), b); }
struct Distribution : NoiseFunction
{
float bx, by, bz;
Distribution(MersenneRNG &rng, float scale)
{
bx = rng.drandom() * scale;
by = rng.drandom() * scale;
bz = rng.drandom() * scale;
}
void displace(float &x, float &y, float &z) {
x += bx; y += by; z += bz;
}
};
struct DistributionVein : Distribution
{
PerlinNoise3D<float> density1, density2;
PerlinNoise3D<float> strand1a, strand1b;
DistributionVein(MersenneRNG &rng) : Distribution(rng, 96) {
density1.init(rng);
density2.init(rng);
strand1a.init(rng);
strand1b.init(rng);
}
float eval(float x, float y, float z) {
return 0.1f * density1(x/96, y/96, z/48)
+ 0.2f * density2(x/48, y/48, z/24)
- apow( strand1a(x/24,y/24,z/12)
+0.6f*strand1b(x/16,y/16,z/8), 0.6f);
}
t_range range() { return t_range(-0.3f-1.33f,0.3f); }
};
struct DistributionCluster : Distribution
{
PerlinNoise3D<float> density1, density2, shape;
DistributionCluster(MersenneRNG &rng) : Distribution(rng, 96) {
density1.init(rng);
density2.init(rng);
shape.init(rng);
}
float eval(float x, float y, float z) {
return 0.2f * density1(x/96, y/96, z/32)
+ 0.6f * density2(x/48, y/48, z/16)
+ shape(x/24, y/24, z/8);
}
t_range range() { return t_range(-1.8f,1.8f); }
};
struct DistributionClusterSmall : Distribution
{
PerlinNoise3D<float> density1, density2, shape;
DistributionClusterSmall(MersenneRNG &rng) : Distribution(rng, 96) {
density1.init(rng);
density2.init(rng);
shape.init(rng);
}
float eval(float x, float y, float z) {
const float scale = 1.0f/4.3f;
return 0.06f * density1(x/96, y/96, z/48)
+ 0.12f * density2(x/24, y/24, z/12)
+ apow(shape(x*scale, y*scale, z*scale), 0.1f);
}
t_range range() { return t_range(-0.18f,1.18f); }
};
struct DistributionClusterOne : Distribution
{
PerlinNoise3D<float> density1, density2, shape;
DistributionClusterOne(MersenneRNG &rng) : Distribution(rng, 96) {
density1.init(rng);
density2.init(rng);
shape.init(rng);
}
float eval(float x, float y, float z) {
return 0.05f * density1(x/96, y/96, z/48)
+ 0.1f * density2(x/48, y/48, z/24)
+ shape(x-bx, y-by, z-bz);
}
t_range range() { return t_range(-1.15f,1.15f); }
};
static NoiseFunction *makeVeinDistribution(t_veinkey vein, MersenneRNG &rng)
{
using namespace df::enums::inclusion_type;
switch (vein.second)
{
case VEIN:
return new DistributionVein(rng);
case CLUSTER_SMALL:
return new DistributionClusterSmall(rng);
case CLUSTER_ONE:
return new DistributionClusterOne(rng);
case CLUSTER:
default:
return new DistributionCluster(rng);
}
}
/* /*
* Data structures. * Data structures.
*/ */
@ -107,8 +255,6 @@ struct GeoLayer;
struct GeoColumn; struct GeoColumn;
struct GeoBiome; struct GeoBiome;
typedef std::pair<int,df::inclusion_type> t_veinkey;
/* Representation of a block in geolayer coordinate space. /* Representation of a block in geolayer coordinate space.
* That is, it represents a block that would have happened * That is, it represents a block that would have happened
* if geological layers weren't shifted in Z direction to * if geological layers weren't shifted in Z direction to
@ -121,6 +267,9 @@ struct GeoBlock
GeoColumn *column; GeoColumn *column;
df::coord pos; df::coord pos;
uint16_t arena_mask, arena_unmined;
int16_t arena_material;
df::tile_bitmask unmined; df::tile_bitmask unmined;
int16_t material[16][16]; int16_t material[16][16];
uint8_t veintype[16][16]; uint8_t veintype[16][16];
@ -129,6 +278,73 @@ struct GeoBlock
GeoBlock(GeoLayer *parent, df::coord pos) : layer(parent), pos(pos) { GeoBlock(GeoLayer *parent, df::coord pos) : layer(parent), pos(pos) {
memset(material, -1, sizeof(material)); memset(material, -1, sizeof(material));
} }
bool prepare_arena(int16_t env_material, NoiseFunction::Ptr fn);
int measure_placement(float threshold);
void place_tiles(float threshold, int16_t new_material, df::inclusion_type itype);
};
/*
* A set of layers that have the same vein type, distribution
* and approximate location, and thus should be placed using
* one binsearch pass for more uniform appearance.
*/
struct VeinExtent
{
typedef shared_ptr<VeinExtent> Ptr;
typedef std::vector<Ptr> PVec;
t_veinkey vein;
int probability, num_tiles;
Ptr parent;
int parent_depth;
bool placed;
int placed_tiles;
NoiseFunction::Ptr distribution;
int num_unmined, num_layer, min_z, max_z;
std::vector<GeoLayer*> layers;
VeinExtent(t_veinkey vein) : vein(vein) {
probability = num_tiles = placed_tiles = 0;
num_unmined = num_layer = 0;
min_z = max_z = 0;
parent_depth = 0;
placed = false;
}
float density() { return float(num_tiles) / num_unmined; }
void set_parent(Ptr pp) {
if (parent)
parent->add_tiles(-num_tiles);
parent = pp;
if (parent)
parent->add_tiles(num_tiles);
parent_depth = (pp ? pp->parent_depth+1 : 0);
}
int parent_mat() {
return parent ? parent->vein.first : SMC_LAYER;
}
void add_tiles(int tiles) {
num_tiles += tiles;
if (parent)
parent->add_tiles(tiles);
}
bool is_similar(Ptr ext2) {
return probability == ext2->probability &&
parent_mat() == ext2->parent_mat();
}
void link(GeoLayer *layer);
void merge_into(VeinExtent::Ptr ext2);
void place_tiles();
}; };
struct GeoColumn struct GeoColumn
@ -164,12 +380,16 @@ struct GeoLayer
// World-global origin coordinates in blocks // World-global origin coordinates in blocks
df::coord world_pos; df::coord world_pos;
int min_z() { return world_pos.z - z_bias; }
int max_z() { return world_pos.z + thickness - 1; }
df::coord2d size; df::coord2d size;
BlockGrid<GeoBlock*> blocks; BlockGrid<GeoBlock*> blocks;
std::vector<GeoBlock*> block_list; std::vector<GeoBlock*> block_list;
int tiles, unmined_tiles, mineral_tiles; int tiles, unmined_tiles, mineral_tiles;
std::map<t_veinkey,int> mineral_count; std::map<t_veinkey,int> mineral_count;
std::map<t_veinkey, VeinExtent::Ptr> veins;
GeoLayer(GeoBiome *parent, int index, df::world_geo_layer *info); GeoLayer(GeoBiome *parent, int index, df::world_geo_layer *info);
@ -216,6 +436,8 @@ struct GeoLayer
out.print(" Total tiles: %d (%d unmined)\n", tiles, unmined_tiles); out.print(" Total tiles: %d (%d unmined)\n", tiles, unmined_tiles);
} }
bool form_veins(color_ostream &out);
}; };
struct GeoBiome struct GeoBiome
@ -267,6 +489,8 @@ GeoLayer::GeoLayer(GeoBiome *parent, int index, df::world_geo_layer *info)
is_soil = isSoilInorganic(material); is_soil = isSoilInorganic(material);
} }
const int NUM_INCLUSIONS = 1+(int)ENUM_LAST_ITEM(inclusion_type);
struct VeinGenerator struct VeinGenerator
{ {
color_ostream &out; color_ostream &out;
@ -278,6 +502,14 @@ struct VeinGenerator
std::map<int, GeoBiome*> biomes; std::map<int, GeoBiome*> biomes;
std::vector<GeoBiome*> biome_by_idx; std::vector<GeoBiome*> biome_by_idx;
struct VSeeds {
uint32_t seeds[NUM_INCLUSIONS];
NoiseFunction::Ptr funcs[NUM_INCLUSIONS];
};
std::vector<VSeeds> seeds;
std::map<t_veinkey, VeinExtent::PVec> veins;
VeinGenerator(color_ostream &out) : out(out) {} VeinGenerator(color_ostream &out) : out(out) {}
~VeinGenerator() { ~VeinGenerator() {
@ -297,6 +529,14 @@ struct VeinGenerator
void write_tiles(); void write_tiles();
void write_block_tiles(Block *b, df::coord2d column, int z); void write_block_tiles(Block *b, df::coord2d column, int z);
bool form_veins();
bool place_orphan(t_veinkey vein, int size, GeoLayer *from);
void init_seeds();
NoiseFunction::Ptr get_noise(t_veinkey vein);
bool place_veins(bool verbose);
void print_mineral_stats() void print_mineral_stats()
{ {
for (auto it = biomes.begin(); it != biomes.end(); ++it) for (auto it = biomes.begin(); it != biomes.end(); ++it)
@ -687,10 +927,518 @@ void VeinGenerator::write_block_tiles(Block *b, df::coord2d column, int z)
} }
} }
command_result cmd_3dveins(color_ostream &con, vector <string> & parameters) /*
* Vein placement code
*/
bool GeoBlock::prepare_arena(int16_t basemat, NoiseFunction::Ptr fn)
{
arena_mask = arena_unmined = 0;
arena_material = basemat;
df::coord origin = pos + layer->world_pos;
float x0 = float(origin.x)*16 + 0.5f, y0 = float(origin.y)*16 + 0.5f;
float z = origin.z - layer->z_bias + 0.5f;
fn->displace(x0, y0, z);
for (int x = 0; x < 16; x++)
{
for (int y = 0; y < 16; y++)
{
if (material[x][y] != arena_material)
continue;
weight[x][y] = fn->eval(x0+x, y0+y, z);
arena_mask |= (1<<x);
if (unmined.getassignment(x,y))
arena_unmined |= (1<<x);
}
}
return arena_mask != 0;
}
int GeoBlock::measure_placement(float threshold)
{
if (!arena_unmined)
return 0;
int count = 0;
for (int x = 0; x < 16; x++)
{
if ((arena_unmined & (1<<x)) == 0)
continue;
for (int y = 0; y < 16; y++)
{
if (material[x][y] != arena_material || weight[x][y] < threshold)
continue;
if (unmined.getassignment(x,y))
count++;
}
}
return count;
}
void GeoBlock::place_tiles(float threshold, int16_t new_material, df::inclusion_type itype)
{
for (int x = 0; x < 16; x++)
{
if ((arena_mask & (1<<x)) == 0)
continue;
for (int y = 0; y < 16; y++)
{
if (material[x][y] != arena_material || weight[x][y] < threshold)
continue;
material[x][y] = new_material;
veintype[x][y] = itype;
}
}
}
static int measure(const std::vector<GeoBlock*> &arena, float threshold)
{
int count = 0;
for (size_t i = 0; i < arena.size(); i++)
count += arena[i]->measure_placement(threshold);
return count;
}
void VeinExtent::link(GeoLayer *layer)
{
layers.push_back(layer);
num_unmined += layer->unmined_tiles;
num_layer += layer->tiles;
if (layers.size() == 1)
{
min_z = layer->min_z();
max_z = layer->max_z();
}
else
{
min_z = std::min(min_z, layer->min_z());
max_z = std::max(max_z, layer->max_z());
}
}
void VeinExtent::merge_into(Ptr target)
{
assert(target->vein == vein);
target->num_tiles += num_tiles;
for (size_t i = 0; i < layers.size(); i++)
{
target->link(layers[i]);
layers[i]->veins[vein] = target;
}
num_tiles = 0;
layers.clear();
}
void VeinExtent::place_tiles()
{
std::vector<GeoBlock*> arena;
int env_material = parent_mat();
for (size_t i = 0; i < layers.size(); i++)
{
auto layer = layers[i];
for (auto bit = layer->block_list.begin(); bit != layer->block_list.end(); ++bit)
{
if ((*bit)->prepare_arena(env_material, distribution))
arena.push_back(*bit);
}
}
// Binary search to meet the required number
auto range = distribution->range();
float mid;
for (int i = 0; i < 32; i++) // iteration limit
{
mid = (range.first + range.second) / 2;
int count = placed_tiles = measure(arena, mid);
if (count == num_tiles)
break;
else if (count > num_tiles)
range.first = mid;
else
range.second = mid;
}
// Write the tiles out
for (size_t i = 0; i < arena.size(); i++)
arena[i]->place_tiles(mid, vein.first, vein.second);
placed = true;
}
bool GeoLayer::form_veins(color_ostream &out)
{
std::vector<VeinExtent::Ptr> refs;
// Defunct layers cannot have veins
if (tiles <= 0)
return true;
for (size_t i = 0; i < info->vein_mat.size(); i++)
{
int parent_id = info->vein_nested_in[i];
if (parent_id >= (int)refs.size())
{
out.printerr("Forward vein reference in biome %d.\n", biome->info.geo_index);
return false;
}
t_veinkey key(info->vein_mat[i], info->vein_type[i]);
VeinExtent::Ptr &vptr = veins[key];
if (vptr)
{
int tgt_pmat = parent_id < 0 ? SMC_LAYER : info->vein_mat[parent_id];
int cur_pmat = vptr->parent_mat();
if (parent_id < 0)
vptr->set_parent(VeinExtent::Ptr());
if (cur_pmat != tgt_pmat)
{
std::string ctx = "be anywhere";
if (vptr->parent)
ctx = "only be in "+MaterialInfo(0,vptr->parent_mat()).getToken();
out.printerr(
"Duplicate vein %s %s in biome %d layer %d - will %s.\n",
MaterialInfo(0,key.first).getToken().c_str(),
ENUM_KEY_STR(inclusion_type, key.second).c_str(),
biome->info.geo_index, index, ctx.c_str()
);
}
vptr->probability = std::max<int>(vptr->probability, info->vein_unk_38[i]);
}
else
{
vptr = VeinExtent::Ptr(new VeinExtent(key));
vptr->probability = info->vein_unk_38[i];
if (parent_id >= 0)
vptr->set_parent(refs[parent_id]);
vptr->add_tiles(mineral_count[key]);
mineral_count.erase(key);
vptr->link(this);
refs.push_back(vptr);
}
}
return true;
}
bool VeinGenerator::place_orphan(t_veinkey key, int size, GeoLayer *from)
{ {
if (!parameters.empty()) std::map<int, VeinExtent::PVec> best;
return CR_WRONG_USAGE;
for (auto it = biomes.begin(); it != biomes.end(); ++it)
{
auto &layers = it->second->layers;
for (size_t i = 0; i < layers.size(); i++)
{
if (layers[i]->mineral_tiles >= layers[i]->unmined_tiles)
continue;
VeinExtent::Ptr vein = map_find(layers[i]->veins, key);
if (!vein)
continue;
int dist = std::min(
layers[i]->min_z() - from->max_z(),
from->min_z() - layers[i]->max_z()
);
best[std::max(0, dist)].push_back(vein);
}
}
if (best.empty())
{
out.printerr(
"Could not place orphaned vein %s %s anywhere.\n",
MaterialInfo(0,key.first).getToken().c_str(),
ENUM_KEY_STR(inclusion_type, key.second).c_str()
);
return false;
}
for (auto it = best.begin(); size > 0 && it != best.end(); ++it)
{
auto &vec = it->second;
int free = 0;
for (size_t i = 0; i < vec.size(); i++)
{
auto layer = vec[i]->layers[0];
free += layer->unmined_tiles - layer->mineral_tiles;
}
float coeff = float(size)/free;
for (size_t i = 0; i < vec.size(); i++)
{
auto layer = vec[i]->layers[0];
int cfree = std::max(0, layer->unmined_tiles - layer->mineral_tiles);
int cnt = std::min(size, std::min(cfree, int(ceilf(cfree*coeff))));
vec[i]->add_tiles(cnt);
layer->mineral_tiles += cnt;
size -= cnt;
}
}
if (size > 0)
{
out.printerr(
"Could not place all of orphaned vein %s %s: %d left.\n",
MaterialInfo(0,key.first).getToken().c_str(),
ENUM_KEY_STR(inclusion_type, key.second).c_str(),
size
);
}
return true;
}
bool VeinGenerator::form_veins()
{
// Form veins in layers
for (auto it = biomes.begin(); it != biomes.end(); ++it)
{
auto &layers = it->second->layers;
for (size_t i = 0; i < layers.size(); i++)
if (layers[i] && !layers[i]->form_veins(out))
return false;
}
// Place orphaned minerals
for (auto it = biomes.begin(); it != biomes.end(); ++it)
{
auto &layers = it->second->layers;
for (size_t i = 0; i < layers.size(); i++)
{
auto &mins = layers[i]->mineral_count;
for (auto mit = mins.begin(); mit != mins.end(); ++mit)
{
if (mit->second <= 0) continue;
if (!place_orphan(mit->first, mit->second, layers[i]))
return false;
}
}
}
// Join adjacent extents with the same density
for (auto it = biomes.begin(); it != biomes.end(); ++it)
{
auto &layers = it->second->layers;
for (size_t i = 0; i < layers.size(); i++)
{
auto &mins = layers[i]->veins;
for (auto mit = mins.begin(); mit != mins.end(); ++mit)
{
auto cur = mit->second;
if (!cur) continue;
// Merge in this biome
if (i > 0)
{
if (auto prev = map_find(layers[i-1]->veins, mit->first))
{
if (cur->is_similar(prev))
{
cur->merge_into(prev);
continue;
}
}
}
// Merge across biomes
auto &vec = veins[cur->vein];
for (size_t j = 0; j < vec.size(); j++)
{
if (vec[j]->min_z <= cur->max_z &&
vec[j]->max_z >= cur->min_z &&
cur->is_similar(vec[j]))
{
cur->merge_into(vec[j]);
cur.reset();
break;
}
}
if (cur)
vec.push_back(cur);
}
}
}
return true;
}
void VeinGenerator::init_seeds()
{
MersenneRNG rng;
std::string seed = world->worldgen.worldgen_parms.seed;
seed.resize((seed.size()+3)&~3);
rng.init((uint32_t*)seed.data(), seed.size()/4, 10);
seeds.resize(world->raws.inorganics.size());
for (size_t i = 0; i < seeds.size(); i++)
{
for (int j = 0; j < NUM_INCLUSIONS; j++)
seeds[i].seeds[j] = rng.random();
}
}
NoiseFunction::Ptr VeinGenerator::get_noise(t_veinkey vein)
{
auto &seed = seeds[vein.first];
auto &func = seed.funcs[vein.second];
if (!func)
{
MersenneRNG rng;
rng.init(seed.seeds[vein.second], 10);
func = NoiseFunction::Ptr(makeVeinDistribution(vein, rng));
}
return func;
}
static bool vein_cmp(const VeinExtent::Ptr &a, const VeinExtent::Ptr &b)
{
return (a->parent_depth < b->parent_depth)
|| (a->parent_depth == b->parent_depth && a->density() < b->density());
}
bool VeinGenerator::place_veins(bool verbose)
{
VeinExtent::PVec queue;
init_seeds();
// Compute the placement queue
for (auto it = veins.begin(); it != veins.end(); ++it)
{
auto &vec = it->second;
for (size_t i = 0; i < vec.size(); i++)
{
auto key = vec[i]->vein;
if (vec[i]->num_tiles <= 0)
continue;
if (!isStoneInorganic(key.first))
{
out.printerr(
"Invalid vein material: %s\n",
MaterialInfo(0, key.first).getToken().c_str()
);
return false;
}
if (!is_valid_enum_item(key.second))
{
out.printerr("Invalid vein type: %d\n", key.second);
return false;
}
vec[i]->distribution = get_noise(key);
queue.push_back(vec[i]);
}
}
sort(queue.begin(), queue.end(), vein_cmp);
// Place tiles
out.print("Processing... ", queue.size());
for (size_t j = 0; j < queue.size(); j++)
{
if (queue[j]->parent && !queue[j]->parent->placed)
{
out.printerr(
"\nParent vein not placed for %s %s.\n",
MaterialInfo(0,queue[j]->vein.first).getToken().c_str(),
ENUM_KEY_STR(inclusion_type, queue[j]->vein.second).c_str()
);
return false;
}
if (verbose)
{
if (j > 0)
out.print("done.");
out.print(
"\nVein layer %d of %d: %s %s (%.2f%%)... ",
j+1, queue.size(),
MaterialInfo(0,queue[j]->vein.first).getToken().c_str(),
ENUM_KEY_STR(inclusion_type, queue[j]->vein.second).c_str(),
queue[j]->density() * 100
);
}
else
{
out.print("\rVein layer %d of %d... ", j+1, queue.size());
out.flush();
}
queue[j]->place_tiles();
}
out.print("done.\n");
return true;
}
command_result cmd_3dveins(color_ostream &con, std::vector<std::string> & parameters)
{
bool verbose = false;
for (size_t i = 0; i < parameters.size(); i++)
{
if (parameters[i] == "verbose")
verbose = true;
else
return CR_WRONG_USAGE;
}
CoreSuspender suspend; CoreSuspender suspend;
@ -709,7 +1457,12 @@ command_result cmd_3dveins(color_ostream &con, vector <string> & parameters)
if (!generator.scan_tiles()) if (!generator.scan_tiles())
return CR_FAILURE; return CR_FAILURE;
generator.print_mineral_stats(); con.print("Generating veins...\n");
if (!generator.form_veins())
return CR_FAILURE;
if (!generator.place_veins(verbose))
return CR_FAILURE;
con.print("Writing tiles...\n"); con.print("Writing tiles...\n");