From 92981b2fee19fd04e2a49533ffa1f778bff6ca72 Mon Sep 17 00:00:00 2001 From: paramat Date: Thu, 16 Oct 2014 12:45:55 +0100 Subject: Add mgv5. New noise code, uses biome API. Eased 3d noise for terrain, caves, blobs --- src/CMakeLists.txt | 1 + src/emerge.cpp | 2 + src/mapgen_v5.cpp | 496 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/mapgen_v5.h | 115 +++++++++++++ src/noise.cpp | 2 +- src/noise.h | 2 + 6 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 src/mapgen_v5.cpp create mode 100644 src/mapgen_v5.h (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5de5834d3..eeceb6358 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -368,6 +368,7 @@ set(common_SRCS mapblock.cpp mapgen.cpp mapgen_singlenode.cpp + mapgen_v5.cpp mapgen_v6.cpp mapgen_v7.cpp mapnode.cpp diff --git a/src/emerge.cpp b/src/emerge.cpp index 9cbcd2574..7427f6f4b 100644 --- a/src/emerge.cpp +++ b/src/emerge.cpp @@ -42,6 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "mg_biome.h" #include "mg_decoration.h" #include "mg_ore.h" +#include "mapgen_v5.h" #include "mapgen_v6.h" #include "mapgen_v7.h" #include "mapgen_singlenode.h" @@ -82,6 +83,7 @@ public: EmergeManager::EmergeManager(IGameDef *gamedef) { //register built-in mapgens + registerMapgen("v5", new MapgenFactoryV5()); registerMapgen("v6", new MapgenFactoryV6()); registerMapgen("v7", new MapgenFactoryV7()); registerMapgen("singlenode", new MapgenFactorySinglenode()); diff --git a/src/mapgen_v5.cpp b/src/mapgen_v5.cpp new file mode 100644 index 000000000..152537eed --- /dev/null +++ b/src/mapgen_v5.cpp @@ -0,0 +1,496 @@ +/* +Minetest +Copyright (C) 2010-2013 kwolekr, Ryan Kwolek + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + + +#include "mapgen.h" +#include "voxel.h" +#include "noise.h" +#include "mapblock.h" +#include "mapnode.h" +#include "map.h" +#include "content_sao.h" +#include "nodedef.h" +#include "voxelalgorithms.h" +#include "profiler.h" +#include "settings.h" // For g_settings +#include "main.h" // For g_profiler +#include "emerge.h" +#include "dungeongen.h" +#include "cavegen.h" +#include "treegen.h" +#include "mg_biome.h" +#include "mg_ore.h" +#include "mg_decoration.h" +#include "mapgen_v5.h" +#include "util/directiontables.h" + + +FlagDesc flagdesc_mapgen_v5[] = { + //{"blobs", MGV5_BLOBS}, + {NULL, 0} +}; + + +MapgenV5::MapgenV5(int mapgenid, MapgenParams *params, EmergeManager *emerge_) { + this->generating = false; + this->id = mapgenid; + this->emerge = emerge_; + this->bmgr = emerge->biomedef; + + this->seed = (int)params->seed; + this->water_level = params->water_level; + this->flags = params->flags; + this->gennotify = emerge->gennotify; + + this->csize = v3s16(1, 1, 1) * params->chunksize * MAP_BLOCKSIZE; + + // amount of elements to skip for the next index + // for noise/height/biome maps (not vmanip) + this->ystride = csize.X; + this->zstride = csize.X * csize.Y; + + this->biomemap = new u8[csize.X * csize.Z]; + this->heightmap = new s16[csize.X * csize.Z]; + + MapgenV5Params *sp = (MapgenV5Params *)params->sparams; + + // Terrain noise + noise_filler_depth = new Noise(&sp->np_filler_depth, seed, csize.X, csize.Z); + noise_factor = new Noise(&sp->np_factor, seed, csize.X, csize.Z); + noise_height = new Noise(&sp->np_height, seed, csize.X, csize.Z); + + // 3D terrain noise + noise_cave1 = new Noise(&sp->np_cave1, seed, csize.X, csize.Y, csize.Z); + noise_cave2 = new Noise(&sp->np_cave2, seed, csize.X, csize.Y, csize.Z); + noise_ground = new Noise(&sp->np_ground, seed, csize.X, csize.Y, csize.Z); + noise_crumble = new Noise(&sp->np_crumble, seed, csize.X, csize.Y, csize.Z); + noise_wetness = new Noise(&sp->np_wetness, seed, csize.X, csize.Y, csize.Z); + + // Biome noise + noise_heat = new Noise(bmgr->np_heat, seed, csize.X, csize.Z); + noise_humidity = new Noise(bmgr->np_humidity, seed, csize.X, csize.Z); + + //// Resolve nodes to be used + INodeDefManager *ndef = emerge->ndef; + + c_stone = ndef->getId("mapgen_stone"); + c_dirt = ndef->getId("mapgen_dirt"); + c_dirt_with_grass = ndef->getId("mapgen_dirt_with_grass"); + c_sand = ndef->getId("mapgen_sand"); + c_water_source = ndef->getId("mapgen_water_source"); + c_lava_source = ndef->getId("mapgen_lava_source"); + c_gravel = ndef->getId("mapgen_gravel"); + c_cobble = ndef->getId("mapgen_cobble"); + c_ice = ndef->getId("default:ice"); + c_mossycobble = ndef->getId("mapgen_mossycobble"); + c_sandbrick = ndef->getId("mapgen_sandstonebrick"); + c_stair_cobble = ndef->getId("mapgen_stair_cobble"); + c_stair_sandstone = ndef->getId("mapgen_stair_sandstone"); + if (c_ice == CONTENT_IGNORE) + c_ice = CONTENT_AIR; + if (c_mossycobble == CONTENT_IGNORE) + c_mossycobble = c_cobble; + if (c_sandbrick == CONTENT_IGNORE) + c_sandbrick = c_desert_stone; + if (c_stair_cobble == CONTENT_IGNORE) + c_stair_cobble = c_cobble; + if (c_stair_sandstone == CONTENT_IGNORE) + c_stair_sandstone = c_sandbrick; +} + + +MapgenV5::~MapgenV5() { + delete noise_filler_depth; + delete noise_factor; + delete noise_height; + delete noise_cave1; + delete noise_cave2; + delete noise_ground; + delete noise_crumble; + delete noise_wetness; + + delete noise_heat; + delete noise_humidity; + + delete[] heightmap; + delete[] biomemap; +} + + +MapgenV5Params::MapgenV5Params() { + //spflags = MGV5_BLOBS; + spflags = 0; + + np_filler_depth = NoiseParams(0, 1, v3f(150, 150, 150), 261, 4, 0.7); + np_factor = NoiseParams(0, 1, v3f(250, 250, 250), 920381, 3, 0.45); + np_height = NoiseParams(0, 10, v3f(250, 250, 250), 84174, 4, 0.5); + np_cave1 = NoiseParams(0, 7.5, v3f(50, 50, 50), 52534, 4, 0.5); + np_cave2 = NoiseParams(0, 7.5, v3f(50, 50, 50), 10325, 4, 0.5); + np_ground = NoiseParams(0, 40, v3f(80, 80, 80), 983240, 4, 0.55); + np_crumble = NoiseParams(0, 1, v3f(20, 20, 20), 34413, 3, 1.3); + np_wetness = NoiseParams(0, 1, v3f(40, 40, 40), 32474, 4, 1.1); +} + + +// Scaling the output of the noise function affects the overdrive of the +// contour function, which affects the shape of the output considerably. + +//#define CAVE_NOISE_SCALE 12.0 < original default +//#define CAVE_NOISE_SCALE 10.0 +//#define CAVE_NOISE_SCALE 7.5 < current default to compensate for new eased 3d noise +//#define CAVE_NOISE_SCALE 5.0 +//#define CAVE_NOISE_SCALE 1.0 + +//#define CAVE_NOISE_THRESHOLD (2.5/CAVE_NOISE_SCALE) +//#define CAVE_NOISE_THRESHOLD (1.5/CAVE_NOISE_SCALE) < original and current default + + +void MapgenV5Params::readParams(Settings *settings) { + settings->getFlagStrNoEx("mgv5_spflags", spflags, flagdesc_mapgen_v5); + + settings->getNoiseParams("mgv5_np_filler_depth", np_filler_depth); + settings->getNoiseParams("mgv5_np_factor", np_factor); + settings->getNoiseParams("mgv5_np_height", np_height); + settings->getNoiseParams("mgv5_np_cave1", np_cave1); + settings->getNoiseParams("mgv5_np_cave2", np_cave2); + settings->getNoiseParams("mgv5_np_ground", np_ground); + settings->getNoiseParams("mgv5_np_crumble", np_crumble); + settings->getNoiseParams("mgv5_np_wetness", np_wetness); +} + + +void MapgenV5Params::writeParams(Settings *settings) { + settings->setFlagStr("mgv5_spflags", spflags, flagdesc_mapgen_v5, (u32)-1); + + settings->setNoiseParams("mgv5_np_filler_depth", np_filler_depth); + settings->setNoiseParams("mgv5_np_factor", np_factor); + settings->setNoiseParams("mgv5_np_height", np_height); + settings->setNoiseParams("mgv5_np_cave1", np_cave1); + settings->setNoiseParams("mgv5_np_cave2", np_cave2); + settings->setNoiseParams("mgv5_np_ground", np_ground); + settings->setNoiseParams("mgv5_np_crumble", np_crumble); + settings->setNoiseParams("mgv5_np_wetness", np_wetness); +} + + +void MapgenV5::makeChunk(BlockMakeData *data) { + assert(data->vmanip); + assert(data->nodedef); + assert(data->blockpos_requested.X >= data->blockpos_min.X && + data->blockpos_requested.Y >= data->blockpos_min.Y && + data->blockpos_requested.Z >= data->blockpos_min.Z); + assert(data->blockpos_requested.X <= data->blockpos_max.X && + data->blockpos_requested.Y <= data->blockpos_max.Y && + data->blockpos_requested.Z <= data->blockpos_max.Z); + + generating = true; + vm = data->vmanip; + ndef = data->nodedef; + //TimeTaker t("makeChunk"); + + v3s16 blockpos_min = data->blockpos_min; + v3s16 blockpos_max = data->blockpos_max; + node_min = blockpos_min * MAP_BLOCKSIZE; + node_max = (blockpos_max + v3s16(1, 1, 1)) * MAP_BLOCKSIZE - v3s16(1, 1, 1); + full_node_min = (blockpos_min - 1) * MAP_BLOCKSIZE; + full_node_max = (blockpos_max + 2) * MAP_BLOCKSIZE - v3s16(1, 1, 1); + + // Create a block-specific seed + blockseed = emerge->getBlockSeed(full_node_min); //////use getBlockSeed2()! + + // Make some noise + calculateNoise(); + + // Generate base terrain + generateBaseTerrain(); + updateHeightmap(node_min, node_max); + + // Generate underground dirt, sand, gravel and lava blobs + //if (spflags & MGV5_BLOBS) { + generateBlobs(); + //} + + // Calculate biomes + BiomeNoiseInput binput; + binput.mapsize = v2s16(csize.X, csize.Z); + binput.heat_map = noise_heat->result; + binput.humidity_map = noise_humidity->result; + binput.height_map = heightmap; + bmgr->calcBiomes(&binput, biomemap); + + // Actually place the biome-specific nodes + generateBiomes(); + + // Generate dungeons and desert temples + if (flags & MG_DUNGEONS) { + DungeonGen dgen(this, NULL); + dgen.generate(blockseed, full_node_min, full_node_max); + } + + // Generate the registered decorations + for (size_t i = 0; i != emerge->decorations.size(); i++) { + Decoration *deco = emerge->decorations[i]; + deco->placeDeco(this, blockseed + i, node_min, node_max); + } + + // Generate the registered ores + for (unsigned int i = 0; i != emerge->ores.size(); i++) { + Ore *ore = emerge->ores[i]; + ore->placeOre(this, blockseed + i, node_min, node_max); + } + + // Sprinkle some dust on top after everything else was generated + dustTopNodes(); + + //printf("makeChunk: %dms\n", t.stop()); + + // Add top and bottom side of water to transforming_liquid queue + updateLiquid(&data->transforming_liquid, full_node_min, full_node_max); + + // Calculate lighting + if (flags & MG_LIGHT) + calcLighting(node_min - v3s16(1, 0, 1) * MAP_BLOCKSIZE, + node_max + v3s16(1, 0, 1) * MAP_BLOCKSIZE); + + this->generating = false; +} + + +void MapgenV5::calculateNoise() { + //TimeTaker t("calculateNoise", NULL, PRECISION_MICRO); + int x = node_min.X; + int y = node_min.Y; + int z = node_min.Z; + + noise_filler_depth->perlinMap2D(x, z); + noise_factor->perlinMap2D(x, z); + noise_height->perlinMap2D(x, z); + noise_height->transformNoiseMap(); + + noise_cave1->perlinMap3D(x, y, z, true); + noise_cave1->transformNoiseMap(); + noise_cave2->perlinMap3D(x, y, z, true); + noise_cave2->transformNoiseMap(); + noise_ground->perlinMap3D(x, y, z, true); + noise_ground->transformNoiseMap(); + + //if (spflags & MGV5_BLOBS) { + noise_crumble->perlinMap3D(x, y, z, true); + noise_wetness->perlinMap3D(x, y, z, false); + //} + + noise_heat->perlinMap2D(x, z); + noise_humidity->perlinMap2D(x, z); + + //printf("calculateNoise: %dus\n", t.stop()); +} + + +//bool is_cave(u32 index) { +// double d1 = contour(noise_cave1->result[index]); +// double d2 = contour(noise_cave2->result[index]); +// return d1*d2 > CAVE_NOISE_THRESHOLD; +//} + + +//bool val_is_ground(v3s16 p, u32 index, u32 index2d) { +// double f = 0.55 + noise_factor->result[index2d]; +// if(f < 0.01) +// f = 0.01; +// else if(f >= 1.0) +// f *= 1.6; +// double h = WATER_LEVEL + 10 * noise_height->result[index2d]; +// return (noise_ground->result[index] * f > (double)p.Y - h); +//} + + +// Make base ground level +void MapgenV5::generateBaseTerrain() { + u32 index = 0; + u32 index2d = 0; + + for(s16 z=node_min.Z; z<=node_max.Z; z++) { + for(s16 y=node_min.Y; y<=node_max.Y; y++) { + u32 i = vm->m_area.index(node_min.X, y, z); + for(s16 x=node_min.X; x<=node_max.X; x++, i++, index++, index2d++) { + if(vm->m_data[i].getContent() != CONTENT_IGNORE) + continue; + + float f = 0.55 + noise_factor->result[index2d]; + if(f < 0.01) + f = 0.01; + else if(f >= 1.0) + f *= 1.6; + float h = water_level + noise_height->result[index2d]; + float d1 = contour(noise_cave1->result[index]); + float d2 = contour(noise_cave2->result[index]); + if(noise_ground->result[index] * f < y - h) { + if(y <= water_level) + vm->m_data[i] = MapNode(c_water_source); + else + vm->m_data[i] = MapNode(CONTENT_AIR); + } else if(d1*d2 > 0.2) { + vm->m_data[i] = MapNode(CONTENT_AIR); + } else { + vm->m_data[i] = MapNode(c_stone); + } + } + index2d = index2d - ystride; + } + index2d = index2d + ystride; + } +} + + +// Add mud and sand and others underground (in place of stone) +void MapgenV5::generateBlobs() { + u32 index = 0; + + for(s16 z=node_min.Z; z<=node_max.Z; z++) { + for(s16 y=node_min.Y; y<=node_max.Y; y++) { + u32 i = vm->m_area.index(node_min.X, y, z); + for(s16 x=node_min.X; x<=node_max.X; x++, i++, index++) { + content_t c = vm->m_data[i].getContent(); + if(c != c_stone) + continue; + + if(noise_crumble->result[index] > 1.3) { + if(noise_wetness->result[index] > 0.0) + vm->m_data[i] = MapNode(c_dirt); + else + vm->m_data[i] = MapNode(c_sand); + } else if(noise_crumble->result[index] > 0.7) { + if(noise_wetness->result[index] < -0.6) + vm->m_data[i] = MapNode(c_gravel); + } else if(noise_crumble->result[index] < -3.5 + + MYMIN(0.1 * + sqrt((float)MYMAX(0, -y)), 1.5)) { + vm->m_data[i] = MapNode(c_lava_source); + } + } + } + } +} + + +void MapgenV5::generateBiomes() { + if (node_max.Y < water_level) + return; + + MapNode n_air(CONTENT_AIR); + MapNode n_stone(c_stone); + MapNode n_water(c_water_source); + + v3s16 em = vm->m_area.getExtent(); + u32 index = 0; + + for (s16 z = node_min.Z; z <= node_max.Z; z++) + for (s16 x = node_min.X; x <= node_max.X; x++, index++) { + Biome *biome = bmgr->biomes[biomemap[index]]; + s16 dfiller = biome->depth_filler + noise_filler_depth->result[index]; + s16 y0_top = biome->depth_top; + s16 y0_filler = biome->depth_filler + biome->depth_top + dfiller; + + s16 nplaced = 0; + u32 i = vm->m_area.index(x, node_max.Y, z); + + content_t c_above = vm->m_data[i + em.X].getContent(); + bool have_air = c_above == CONTENT_AIR; + + for (s16 y = node_max.Y; y >= node_min.Y; y--) { + content_t c = vm->m_data[i].getContent(); + if ((c == c_stone || c == c_dirt_with_grass + || c == c_dirt + || c == c_sand + || c == c_lava_source + || c == c_gravel) && have_air) { + content_t c_below = vm->m_data[i - em.X].getContent(); + + if (c_below != CONTENT_AIR) { + if (nplaced < y0_top) { + // A hack to prevent dirt_with_grass from being + // placed below water. TODO: fix later + content_t c_place = ((y < water_level) && + (biome->c_top == + c_dirt_with_grass)) ? + c_dirt : biome->c_top; + + vm->m_data[i] = MapNode(c_place); + nplaced++; + } else if (nplaced < y0_filler && nplaced >= y0_top) { + vm->m_data[i] = MapNode(biome->c_filler); + nplaced++; + } else { + have_air = false; + nplaced = 0; + } + } + } else if (c == c_water_source) { + have_air = true; + nplaced = 0; + vm->m_data[i] = MapNode(biome->c_water); + } else if (c == CONTENT_AIR) { + have_air = true; + nplaced = 0; + } + + vm->m_area.add_y(em, i, -1); + } + } +} + +void MapgenV5::dustTopNodes() { + v3s16 em = vm->m_area.getExtent(); + u32 index = 0; + + if (water_level > node_max.Y) + return; + + for (s16 z = node_min.Z; z <= node_max.Z; z++) + for (s16 x = node_min.X; x <= node_max.X; x++, index++) { + Biome *biome = bmgr->biomes[biomemap[index]]; + + if (biome->c_dust == CONTENT_IGNORE) + continue; + + s16 y = node_max.Y; + u32 vi = vm->m_area.index(x, y, z); + for (; y >= node_min.Y; y--) { + if (vm->m_data[vi].getContent() != CONTENT_AIR) + break; + + vm->m_area.add_y(em, vi, -1); + } + + content_t c = vm->m_data[vi].getContent(); + if (c == biome->c_water && biome->c_dust_water != CONTENT_IGNORE) { + if (y < node_min.Y) + continue; + + vm->m_data[vi] = MapNode(biome->c_dust_water); + } else if (!ndef->get(c).buildable_to && c != CONTENT_IGNORE) { + if (y == node_max.Y) + continue; + + vm->m_area.add_y(em, vi, 1); + vm->m_data[vi] = MapNode(biome->c_dust); + } + } +} + diff --git a/src/mapgen_v5.h b/src/mapgen_v5.h new file mode 100644 index 000000000..c38113fec --- /dev/null +++ b/src/mapgen_v5.h @@ -0,0 +1,115 @@ +/* +Minetest +Copyright (C) 2010-2013 kwolekr, Ryan Kwolek + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#ifndef MAPGEN_V5_HEADER +#define MAPGEN_V5_HEADER + +#include "mapgen.h" + +/////////////////// Mapgen V5 flags +//#define MGV5_BLOBS 0x01 + +extern FlagDesc flagdesc_mapgen_v5[]; + + +struct MapgenV5Params : public MapgenSpecificParams { + u32 spflags; + NoiseParams np_filler_depth; + NoiseParams np_factor; + NoiseParams np_height; + NoiseParams np_cave1; + NoiseParams np_cave2; + NoiseParams np_ground; + NoiseParams np_crumble; + NoiseParams np_wetness; + + MapgenV5Params(); + ~MapgenV5Params() {} + + void readParams(Settings *settings); + void writeParams(Settings *settings); +}; + + +class MapgenV5 : public Mapgen { +public: + EmergeManager *emerge; + BiomeDefManager *bmgr; + + int ystride; + int zstride; + u32 flags; + u32 spflags; + + u32 blockseed; + v3s16 node_min; + v3s16 node_max; + v3s16 full_node_min; + v3s16 full_node_max; + + Noise *noise_filler_depth; + Noise *noise_factor; + Noise *noise_height; + Noise *noise_cave1; + Noise *noise_cave2; + Noise *noise_ground; + Noise *noise_crumble; + Noise *noise_wetness; + Noise *noise_heat; + Noise *noise_humidity; + + content_t c_stone; + content_t c_dirt; + content_t c_dirt_with_grass; + content_t c_sand; + content_t c_water_source; + content_t c_lava_source; + content_t c_ice; + content_t c_gravel; + content_t c_cobble; + content_t c_desert_sand; + content_t c_desert_stone; + content_t c_mossycobble; + content_t c_sandbrick; + content_t c_stair_cobble; + content_t c_stair_sandstone; + + MapgenV5(int mapgenid, MapgenParams *params, EmergeManager *emerge_); + ~MapgenV5(); + + virtual void makeChunk(BlockMakeData *data); + void calculateNoise(); + void generateBaseTerrain(); + void generateBlobs(); + void generateBiomes(); + void dustTopNodes(); +}; + + +struct MapgenFactoryV5 : public MapgenFactory { + Mapgen *createMapgen(int mgid, MapgenParams *params, EmergeManager *emerge) { + return new MapgenV5(mgid, params, emerge); + }; + + MapgenSpecificParams *createMapgenParams() { + return new MapgenV5Params(); + }; +}; + +#endif diff --git a/src/noise.cpp b/src/noise.cpp index ba2fe5421..c30e1570d 100644 --- a/src/noise.cpp +++ b/src/noise.cpp @@ -306,7 +306,6 @@ float noise3d_perlin_abs(float x, float y, float z, int seed, } -// -1->0, 0->1, 1->0 float contour(float v) { v = fabs(v); @@ -653,3 +652,4 @@ void Noise::transformNoiseMap() i++; } } + diff --git a/src/noise.h b/src/noise.h index 34dcb7374..aa489b2c0 100644 --- a/src/noise.h +++ b/src/noise.h @@ -152,6 +152,8 @@ inline float easeCurve(float t) { return t * t * t * (t * (6.f * t - 15.f) + 10.f); } +float contour(float v); + #define NoisePerlin2D(np, x, y, s) \ ((np)->offset + (np)->scale * noise2d_perlin( \ (float)(x) / (np)->spread.X, \ -- cgit v1.2.3