55 private int $waterHeight = 62;
57 private array $populators = [];
59 private array $generationPopulators = [];
64 private const NOISE_SAMPLING_RATE_Y = 8;
70 parent::__construct($seed, $preset);
74 $this->noiseBase =
new Simplex($this->random, 4, 1 / 4, 1 / 32);
75 $this->random->setSeed($this->seed);
77 $this->selector =
new class($this->random) extends
BiomeSelector{
78 protected function lookup(
float $temperature,
float $rainfall) :
int{
80 if($temperature < 0.7){
81 return BiomeIds::OCEAN;
82 }elseif($temperature < 0.85){
83 return BiomeIds::RIVER;
85 return BiomeIds::SWAMPLAND;
87 }elseif($rainfall < 0.60){
88 if($temperature < 0.25){
89 return BiomeIds::ICE_PLAINS;
90 }elseif($temperature < 0.75){
91 return BiomeIds::PLAINS;
93 return BiomeIds::DESERT;
95 }elseif($rainfall < 0.80){
96 if($temperature < 0.25){
97 return BiomeIds::TAIGA;
98 }elseif($temperature < 0.75){
99 return BiomeIds::FOREST;
101 return BiomeIds::BIRCH_FOREST;
104 if($temperature < 0.20){
105 return BiomeIds::EXTREME_HILLS;
106 }elseif($temperature < 0.40){
107 return BiomeIds::EXTREME_HILLS_EDGE;
109 return BiomeIds::RIVER;
115 $this->selector->recalculate();
118 $this->generationPopulators[] = $cover;
121 $stone = VanillaBlocks::STONE();
123 new OreType(VanillaBlocks::COAL_ORE(), $stone, 20, 16, 0, 128),
124 new OreType(VanillaBlocks::IRON_ORE(), $stone, 20, 8, 0, 64),
125 new OreType(VanillaBlocks::REDSTONE_ORE(), $stone, 8, 7, 0, 16),
126 new OreType(VanillaBlocks::LAPIS_LAZULI_ORE(), $stone, 1, 6, 0, 32),
127 new OreType(VanillaBlocks::GOLD_ORE(), $stone, 2, 8, 0, 32),
128 new OreType(VanillaBlocks::DIAMOND_ORE(), $stone, 1, 7, 0, 16),
129 new OreType(VanillaBlocks::DIRT(), $stone, 20, 32, 0, 128),
130 new OreType(VanillaBlocks::GRAVEL(), $stone, 10, 16, 0, 128)
132 $this->populators[] = $ores;
135 private function pickBiome(
int $x,
int $z) :
Biome{
136 $hash = $x * 2345803 ^ $z * 9236449 ^ $this->seed;
137 $hash *= $hash + 223;
141 $hash = (int) fmod($hash, 2.0 ** 63);
142 $xNoise = $hash >> 20 & 3;
143 $zNoise = $hash >> 22 & 3;
151 return $this->selector->pickBiome($x + $xNoise - 1, $z + $zNoise - 1);
154 public function generateChunk(ChunkManager $world,
int $chunkX,
int $chunkZ) : void{
155 $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed);
158 $chunk = $world->getChunk($chunkX, $chunkZ) ??
throw new \InvalidArgumentException(
"Chunk $chunkX $chunkZ does not yet exist");
160 $bedrock = VanillaBlocks::BEDROCK()->getStateId();
161 $stillWater = VanillaBlocks::WATER()->getStateId();
162 $stone = VanillaBlocks::STONE()->getStateId();
164 $baseX = $chunkX * Chunk::EDGE_LENGTH;
165 $baseZ = $chunkZ * Chunk::EDGE_LENGTH;
167 [$biomeArray, $minNoiseHeights, $maxNoiseHeights] = $this->generateBiomes($baseX, $baseZ);
169 $lowestNoiseBlock = (int) floor(min($minNoiseHeights));
170 $highestNoiseBlock = (int) ceil(max($maxNoiseHeights));
174 $noiseMin = (int) floor($lowestNoiseBlock / self::NOISE_SAMPLING_RATE_Y) * self::NOISE_SAMPLING_RATE_Y;
175 $noiseMax = (int) ceil($highestNoiseBlock / self::NOISE_SAMPLING_RATE_Y) * self::NOISE_SAMPLING_RATE_Y;
179 $noise = $this->noiseBase->getFastNoise3D(
180 xSize: Chunk::EDGE_LENGTH,
181 ySize: $noiseMax - $noiseMin,
182 zSize: Chunk::EDGE_LENGTH,
184 ySamplingRate: self::NOISE_SAMPLING_RATE_Y,
186 x: $chunkX * Chunk::EDGE_LENGTH,
188 z: $chunkZ * Chunk::EDGE_LENGTH
191 $minNoiseSubChunk = (int) floor($noiseMin / SubChunk::EDGE_LENGTH);
192 foreach($chunk->getSubChunks() as $y => $subChunk){
193 if($y >= 0 && $y < $minNoiseSubChunk){
198 $fillId = Block::EMPTY_STATE_ID;
200 $chunk->setSubChunk($y,
new SubChunk(Block::EMPTY_STATE_ID,
new PalettedBlockArray($fillId),
null, clone $biomeArray));
203 for($x = 0; $x < Chunk::EDGE_LENGTH; ++$x){
204 for($z = 0; $z < Chunk::EDGE_LENGTH; ++$z){
205 $chunk->setBlockStateId($x, 0, $z, $bedrock);
208 $minSum = $minNoiseHeights[$columnIndex];
209 $maxSum = $maxNoiseHeights[$columnIndex];
210 $maxBlockY = max($maxSum, $this->waterHeight);
211 $smoothHeight = ($maxSum - $minSum) / 2;
215 for($y = $minNoiseSubChunk * SubChunk::EDGE_LENGTH; $y < $minSum; $y++){
216 $chunk->setBlockStateId($x, $y, $z, $stone);
218 for($y = (
int) floor($minSum); $y <= $maxBlockY; ++$y){
220 $noiseValue = $y > $noiseMax ?
222 $noise[$x][$z][$y - $noiseMin] - 1 / $smoothHeight * ($y - $smoothHeight - $minSum);
225 $chunk->setBlockStateId($x, $y, $z, $stone);
226 }elseif($y <= $this->waterHeight){
227 $chunk->setBlockStateId($x, $y, $z, $stillWater);
233 foreach($this->generationPopulators as $populator){
234 $populator->populate($world, $chunkX, $chunkZ, $this->random);
238 public function populateChunk(ChunkManager $world,
int $chunkX,
int $chunkZ) : void{
239 $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed);
240 foreach($this->populators as $populator){
241 $populator->populate($world, $chunkX, $chunkZ, $this->random);
244 $chunk = $world->getChunk($chunkX, $chunkZ);
245 $biome = BiomeRegistry::getInstance()->getBiome($chunk->getBiomeId(7, 7, 7));
246 $biome->populateChunk($world, $chunkX, $chunkZ, $this->random);
253 private function generateBiomes(
int $baseX,
int $baseZ) : array{
256 $biomeArray =
new PalettedBlockArray(BiomeIds::OCEAN);
259 $padding = $this->gaussian->smoothSize;
261 $end = Chunk::EDGE_LENGTH + $padding;
262 for($x = $start; $x < $end; ++$x){
263 $absoluteX = $baseX + $x;
264 for($z = $start; $z < $end; ++$z){
265 $absoluteZ = $baseZ + $z;
267 $columnIndex = World::chunkHash($x, $z);
268 $biome = $biomeCache[$columnIndex] = $this->pickBiome($absoluteX, $absoluteZ);
269 $biomeId = $biome->getId();
270 $uniform = match ($uniform) {
271 null, $biomeId => $biomeId,
275 if($x >= 0 && $x < Chunk::EDGE_LENGTH && $z >= 0 && $z < Chunk::EDGE_LENGTH){
276 for($y = 0; $y < 16; $y++){
277 $biomeArray->set($x, $y, $z, $biomeId);
283 if($uniform ===
false){
284 [$minHeights, $maxHeights] = $this->gaussianSmoothElevation($start, $end, $biomeCache);
288 if(!is_int($uniform)){
289 throw new AssumptionFailedError();
291 $biome = BiomeRegistry::getInstance()->getBiome($uniform);
297 $minElevation = $biome->getMinElevation() - 1;
298 $maxElevation = $biome->getMaxElevation();
299 for($x = 0; $x < Chunk::EDGE_LENGTH; $x++){
300 for($z = 0; $z < Chunk::EDGE_LENGTH; $z++){
301 $columnIndex = World::chunkHash($x, $z);
302 $minHeights[$columnIndex] = $minElevation;
303 $maxHeights[$columnIndex] = $maxElevation;
308 return [$biomeArray, $minHeights, $maxHeights];
318 private function gaussianSmoothElevation(
int $start,
int $end, array $biomeCache) : array{
322 for($x = 0; $x < Chunk::EDGE_LENGTH; $x++){
325 for($z = $start; $z < $end; $z++){
326 $columnIndex = World::chunkHash($x, $z);
331 for($sx = -$this->gaussian->smoothSize; $sx <= $this->gaussian->smoothSize; ++$sx){
332 $weight = $this->gaussian->kernel1D[$sx + $this->gaussian->smoothSize];
333 $adjacent = $biomeCache[World::chunkHash($x + $sx, $z)];
335 $minSum += ($adjacent->getMinElevation() - 1) * $weight;
336 $maxSum += $adjacent->getMaxElevation() * $weight;
339 $minHeightsX[$columnIndex] = $minSum / $this->gaussian->weightSum1D;
340 $maxHeightsX[$columnIndex] = $maxSum / $this->gaussian->weightSum1D;
350 for($x = 0; $x < Chunk::EDGE_LENGTH; $x++){
351 for($z = 0; $z < Chunk::EDGE_LENGTH; $z++){
352 $columnIndex = World::chunkHash($x, $z);
357 for($sx = -$this->gaussian->smoothSize; $sx <= $this->gaussian->smoothSize; ++$sx){
358 $weight = $this->gaussian->kernel1D[$sx + $this->gaussian->smoothSize];
359 $adjacentIndex = World::chunkHash($x, $z + $sx);
360 $minElevation = $minHeightsX[$adjacentIndex];
361 $maxElevation = $maxHeightsX[$adjacentIndex];
363 $minSum += $minElevation * $weight;
364 $maxSum += $maxElevation * $weight;
367 $minHeights[$columnIndex] = $minSum / $this->gaussian->weightSum1D;
368 $maxHeights[$columnIndex] = $maxSum / $this->gaussian->weightSum1D;
372 return [$minHeights, $maxHeights];