PocketMine-MP 5.36.1 git-eaa7c4834c8fe2f379d24e7f0ee6cc63cfb18ccc
Loading...
Searching...
No Matches
Normal.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\world\generator\normal;
25
34use pocketmine\world\format\PalettedBlockArray;
46use function ceil;
47use function floor;
48use function fmod;
49use function is_int;
50use function max;
51use function min;
52
53class Normal extends Generator{
54
55 private int $waterHeight = 62;
57 private array $populators = [];
59 private array $generationPopulators = [];
60 private Simplex $noiseBase;
61 private BiomeSelector $selector;
62 private Gaussian $gaussian;
63
64 private const NOISE_SAMPLING_RATE_Y = 8;
65
69 public function __construct(int $seed, string $preset){
70 parent::__construct($seed, $preset);
71
72 $this->gaussian = new Gaussian(2);
73
74 $this->noiseBase = new Simplex($this->random, 4, 1 / 4, 1 / 32);
75 $this->random->setSeed($this->seed);
76
77 $this->selector = new class($this->random) extends BiomeSelector{
78 protected function lookup(float $temperature, float $rainfall) : int{
79 if($rainfall < 0.25){
80 if($temperature < 0.7){
81 return BiomeIds::OCEAN;
82 }elseif($temperature < 0.85){
83 return BiomeIds::RIVER;
84 }else{
85 return BiomeIds::SWAMPLAND;
86 }
87 }elseif($rainfall < 0.60){
88 if($temperature < 0.25){
89 return BiomeIds::ICE_PLAINS;
90 }elseif($temperature < 0.75){
91 return BiomeIds::PLAINS;
92 }else{
93 return BiomeIds::DESERT;
94 }
95 }elseif($rainfall < 0.80){
96 if($temperature < 0.25){
97 return BiomeIds::TAIGA;
98 }elseif($temperature < 0.75){
99 return BiomeIds::FOREST;
100 }else{
101 return BiomeIds::BIRCH_FOREST;
102 }
103 }else{
104 if($temperature < 0.20){
105 return BiomeIds::EXTREME_HILLS;
106 }elseif($temperature < 0.40){
107 return BiomeIds::EXTREME_HILLS_EDGE;
108 }else{
109 return BiomeIds::RIVER;
110 }
111 }
112 }
113 };
114
115 $this->selector->recalculate();
116
117 $cover = new GroundCover();
118 $this->generationPopulators[] = $cover;
119
120 $ores = new Ore();
121 $stone = VanillaBlocks::STONE();
122 $ores->setOreTypes([
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)
131 ]);
132 $this->populators[] = $ores;
133 }
134
135 private function pickBiome(int $x, int $z) : Biome{
136 $hash = $x * 2345803 ^ $z * 9236449 ^ $this->seed;
137 $hash *= $hash + 223;
138 //the above operations may result in a float. This probably wasn't intended, but we need to stick with it to
139 //avoid cliff edges in existing user worlds.
140 //We need to mod this so it doesn't generate an error in PHP 8.5 when we cast it back to an int.
141 $hash = (int) fmod($hash, 2.0 ** 63);
142 $xNoise = $hash >> 20 & 3;
143 $zNoise = $hash >> 22 & 3;
144 if($xNoise === 3){
145 $xNoise = 1;
146 }
147 if($zNoise === 3){
148 $zNoise = 1;
149 }
150
151 return $this->selector->pickBiome($x + $xNoise - 1, $z + $zNoise - 1);
152 }
153
154 public function generateChunk(ChunkManager $world, int $chunkX, int $chunkZ) : void{
155 $this->random->setSeed(0xdeadbeef ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed);
156
157 //TODO: why don't we just create and set the chunk here directly?
158 $chunk = $world->getChunk($chunkX, $chunkZ) ?? throw new \InvalidArgumentException("Chunk $chunkX $chunkZ does not yet exist");
159
160 $bedrock = VanillaBlocks::BEDROCK()->getStateId();
161 $stillWater = VanillaBlocks::WATER()->getStateId();
162 $stone = VanillaBlocks::STONE()->getStateId();
163
164 $baseX = $chunkX * Chunk::EDGE_LENGTH;
165 $baseZ = $chunkZ * Chunk::EDGE_LENGTH;
166
167 [$biomeArray, $minNoiseHeights, $maxNoiseHeights] = $this->generateBiomes($baseX, $baseZ);
168
169 $lowestNoiseBlock = (int) floor(min($minNoiseHeights));
170 $highestNoiseBlock = (int) ceil(max($maxNoiseHeights));
171
172 //getFastNoise3D expects the inputs to be aligned with the sampling rate, otherwise the samples will be taken
173 //from different coordinates than we originally used when we first implemented this generator
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;
176
177 //we only need to generate noise for the blocks which could be affected
178 //outside these bounds we'll just flood-fill blocks to save time
179 $noise = $this->noiseBase->getFastNoise3D(
180 xSize: Chunk::EDGE_LENGTH,
181 ySize: $noiseMax - $noiseMin,
182 zSize: Chunk::EDGE_LENGTH,
183 xSamplingRate: 4,
184 ySamplingRate: self::NOISE_SAMPLING_RATE_Y,
185 zSamplingRate: 4,
186 x: $chunkX * Chunk::EDGE_LENGTH,
187 y: $noiseMin,
188 z: $chunkZ * Chunk::EDGE_LENGTH
189 );
190
191 $minNoiseSubChunk = (int) floor($noiseMin / SubChunk::EDGE_LENGTH);
192 foreach($chunk->getSubChunks() as $y => $subChunk){
193 if($y >= 0 && $y < $minNoiseSubChunk){
194 //Everything above 0 and below noiseMin is always solid stone, which can be flood-filled instead of
195 //setting the blocks one at a time - this is vastly faster
196 $fillId = $stone;
197 }else{
198 $fillId = Block::EMPTY_STATE_ID;
199 }
200 $chunk->setSubChunk($y, new SubChunk(Block::EMPTY_STATE_ID, new PalettedBlockArray($fillId), null, clone $biomeArray));
201 }
202
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);
206
207 $columnIndex = World::chunkHash($x, $z);
208 $minSum = $minNoiseHeights[$columnIndex];
209 $maxSum = $maxNoiseHeights[$columnIndex];
210 $maxBlockY = max($maxSum, $this->waterHeight);
211 $smoothHeight = ($maxSum - $minSum) / 2;
212
213 //Everything below minSum is always solid stone - we already flood-filled the subchunks below though, so
214 //we only need to fill the gap in the column here
215 for($y = $minNoiseSubChunk * SubChunk::EDGE_LENGTH; $y < $minSum; $y++){
216 $chunk->setBlockStateId($x, $y, $z, $stone);
217 }
218 for($y = (int) floor($minSum); $y <= $maxBlockY; ++$y){
219 //noiseValue would anyway be <= 0 above maxSum because the smoothing term is >= 1
220 $noiseValue = $y > $noiseMax ?
221 -1 :
222 $noise[$x][$z][$y - $noiseMin] - 1 / $smoothHeight * ($y - $smoothHeight - $minSum);
223
224 if($noiseValue > 0){
225 $chunk->setBlockStateId($x, $y, $z, $stone);
226 }elseif($y <= $this->waterHeight){
227 $chunk->setBlockStateId($x, $y, $z, $stillWater);
228 }
229 }
230 }
231 }
232
233 foreach($this->generationPopulators as $populator){
234 $populator->populate($world, $chunkX, $chunkZ, $this->random);
235 }
236 }
237
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);
242 }
243
244 $chunk = $world->getChunk($chunkX, $chunkZ);
245 $biome = BiomeRegistry::getInstance()->getBiome($chunk->getBiomeId(7, 7, 7));
246 $biome->populateChunk($world, $chunkX, $chunkZ, $this->random);
247 }
248
253 private function generateBiomes(int $baseX, int $baseZ) : array{
254 $biomeCache = [];
255
256 $biomeArray = new PalettedBlockArray(BiomeIds::OCEAN);
257
258 $uniform = null;
259 $padding = $this->gaussian->smoothSize;
260 $start = -$padding;
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;
266
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,
272 default => false
273 };
274
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);
278 }
279 }
280 }
281 }
282
283 if($uniform === false){
284 [$minHeights, $maxHeights] = $this->gaussianSmoothElevation($start, $end, $biomeCache);
285 }else{
286 //If all the biomes in the blurred area are the same, we can save some performance by skipping blurring
287 //With the current generator as of 2025-10-23, blurring can be skipped in two-thirds of chunks
288 if(!is_int($uniform)){
289 throw new AssumptionFailedError();
290 }
291 $biome = BiomeRegistry::getInstance()->getBiome($uniform);
293 $minHeights = [];
295 $maxHeights = [];
296
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;
304 }
305 }
306 }
307
308 return [$biomeArray, $minHeights, $maxHeights];
309 }
310
318 private function gaussianSmoothElevation(int $start, int $end, array $biomeCache) : array{
319 $minHeightsX = [];
320 $maxHeightsX = [];
321 //blur along the X axis first
322 for($x = 0; $x < Chunk::EDGE_LENGTH; $x++){
323 //while we don't need to smooth the padding corners, we do need to make sure that the contributions of
324 //those corners are included in Z padding, otherwise we can get artifacts at chunk corners
325 for($z = $start; $z < $end; $z++){
326 $columnIndex = World::chunkHash($x, $z);
327
328 $minSum = 0;
329 $maxSum = 0;
330
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)];
334
335 $minSum += ($adjacent->getMinElevation() - 1) * $weight;
336 $maxSum += $adjacent->getMaxElevation() * $weight;
337 }
338
339 $minHeightsX[$columnIndex] = $minSum / $this->gaussian->weightSum1D;
340 $maxHeightsX[$columnIndex] = $maxSum / $this->gaussian->weightSum1D;
341 }
342 }
343
345 $minHeights = [];
347 $maxHeights = [];
348
349 //then the Z axis, using the blurred values from the previous loop
350 for($x = 0; $x < Chunk::EDGE_LENGTH; $x++){
351 for($z = 0; $z < Chunk::EDGE_LENGTH; $z++){
352 $columnIndex = World::chunkHash($x, $z);
353
354 $minSum = 0;
355 $maxSum = 0;
356
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];
362
363 $minSum += $minElevation * $weight;
364 $maxSum += $maxElevation * $weight;
365 }
366
367 $minHeights[$columnIndex] = $minSum / $this->gaussian->weightSum1D;
368 $maxHeights[$columnIndex] = $maxSum / $this->gaussian->weightSum1D;
369 }
370 }
371
372 return [$minHeights, $maxHeights];
373 }
374}
static chunkHash(int $x, int $z)
Definition World.php:384
__construct(int $seed, string $preset)
Definition Normal.php:69