diff --git a/src/muqsit/vanillagenerator/Loader.php b/src/muqsit/vanillagenerator/Loader.php index 527f691..b427425 100644 --- a/src/muqsit/vanillagenerator/Loader.php +++ b/src/muqsit/vanillagenerator/Loader.php @@ -6,6 +6,8 @@ use muqsit\vanillagenerator\generator\nether\NetherGenerator; use muqsit\vanillagenerator\generator\overworld\OverworldGenerator; +use muqsit\vanillagenerator\generator\test\StoneWaterTestGenerator; +use muqsit\vanillagenerator\generator\test\TestGenerator; use pocketmine\plugin\PluginBase; use pocketmine\world\generator\GeneratorManager; @@ -15,5 +17,7 @@ public function onLoad() : void{ $generator_manager = GeneratorManager::getInstance(); $generator_manager->addGenerator(NetherGenerator::class, "vanilla_nether", fn() => null); $generator_manager->addGenerator(OverworldGenerator::class, "vanilla_overworld", fn() => null); + $generator_manager->addGenerator(TestGenerator::class, "test", fn() => null); + $generator_manager->addGenerator(StoneWaterTestGenerator::class, "lol", fn() => null); } } diff --git a/src/muqsit/vanillagenerator/generator/VanillaBiomeGrid.php b/src/muqsit/vanillagenerator/generator/VanillaBiomeGrid.php index 78cdeae..4002588 100644 --- a/src/muqsit/vanillagenerator/generator/VanillaBiomeGrid.php +++ b/src/muqsit/vanillagenerator/generator/VanillaBiomeGrid.php @@ -12,13 +12,36 @@ class VanillaBiomeGrid implements BiomeGrid{ /** @var int[] */ public array $biomes = []; + + /** @var int[] */ + private array $biomes_3d = []; - public function getBiome(int $x, int $z) : ?int{ + public function getBiome(int $x, int $z): ?int { // upcasting is very important to get extended biomes return array_key_exists($hash = $x | $z << Chunk::COORD_BIT_SIZE, $this->biomes) ? $this->biomes[$hash] & 0xFF : null; } - public function setBiome(int $x, int $z, int $biome_id) : void{ + public function getBiome3D(int $x, int $y, int $z): ?int { + // For 3D biomes, we use a different hash that includes Y coordinate + // We sample biomes every 4 blocks in Y (like 1.18+) to save memory + $sample_y = $y >> 2; // Sample every 4 blocks in Y direction + $hash = ($x) | ($z << 4) | ($sample_y << 8); + + if(array_key_exists($hash, $this->biomes_3d)) { + return $this->biomes_3d[$hash] & 0xFF; + } + + // Fall back to surface biome if no 3D biome is set + return $this->getBiome($x, $z); + } + + public function setBiome(int $x, int $z, int $biome_id): void { $this->biomes[$x | $z << Chunk::COORD_BIT_SIZE] = $biome_id; } + + public function setBiome3D(int $x, int $y, int $z, int $biome_id): void { + $sample_y = $y >> 2; + $hash = ($x) | ($z << 4) | ($sample_y << 8); + $this->biomes_3d[$hash] = $biome_id; + } } \ No newline at end of file diff --git a/src/muqsit/vanillagenerator/generator/VanillaGenerator.php b/src/muqsit/vanillagenerator/generator/VanillaGenerator.php index 4459abf..2035661 100644 --- a/src/muqsit/vanillagenerator/generator/VanillaGenerator.php +++ b/src/muqsit/vanillagenerator/generator/VanillaGenerator.php @@ -12,7 +12,6 @@ use pocketmine\world\ChunkManager; use pocketmine\world\format\Chunk; use pocketmine\world\generator\Generator; -use pocketmine\world\World; /** * @template T of WorldOctaves @@ -97,7 +96,11 @@ public function populateChunk(ChunkManager $world, int $chunk_x, int $chunk_z) : } } - public function getMaxY() : int{ - return World::Y_MAX; + public function getMaxY(): int { + return 319; + } + + public function getMinY(): int { + return -64; } } \ No newline at end of file diff --git a/src/muqsit/vanillagenerator/generator/biomegrid/BiomeGrid.php b/src/muqsit/vanillagenerator/generator/biomegrid/BiomeGrid.php index 74f756e..29874e8 100644 --- a/src/muqsit/vanillagenerator/generator/biomegrid/BiomeGrid.php +++ b/src/muqsit/vanillagenerator/generator/biomegrid/BiomeGrid.php @@ -7,7 +7,7 @@ interface BiomeGrid{ /** - * Get biome at x, z within chunk being generated + * Get biome at x, z within chunk being generated (2D surface biome) * * @param int $x - 0-15 * @param int $z - 0-15 @@ -16,11 +16,31 @@ interface BiomeGrid{ public function getBiome(int $x, int $z) : ?int; /** - * Set biome at x, z within chunk being generated + * Get biome at x, y, z within chunk being generated (3D biome support for 1.18+) + * + * @param int $x - 0-15 + * @param int $y - world min to world max + * @param int $z - 0-15 + * @return int|null + */ + public function getBiome3D(int $x, int $y, int $z) : ?int; + + /** + * Set biome at x, z within chunk being generated (2D surface biome) * * @param int $x - 0-15 * @param int $z - 0-15 * @param int $biome_id */ public function setBiome(int $x, int $z, int $biome_id) : void; + + /** + * Set biome at x, y, z within chunk being generated (3D biome support for 1.18+) + * + * @param int $x - 0-15 + * @param int $y - world min to world max + * @param int $z - 0-15 + * @param int $biome_id + */ + public function setBiome3D(int $x, int $y, int $z, int $biome_id) : void; } \ No newline at end of file diff --git a/src/muqsit/vanillagenerator/generator/biomegrid/ErosionMapLayer.php b/src/muqsit/vanillagenerator/generator/biomegrid/ErosionMapLayer.php index 253487b..11d2425 100644 --- a/src/muqsit/vanillagenerator/generator/biomegrid/ErosionMapLayer.php +++ b/src/muqsit/vanillagenerator/generator/biomegrid/ErosionMapLayer.php @@ -42,8 +42,8 @@ public function generateValues(int $x, int $z, int $size_x, int $size_z) : array $this->setCoordsSeed($x + $j, $z + $i); $final_values[$j + $i * $size_x] = match(true){ - $center_val !== 0 && ($upper_left_val === 0 || $upper_right_val === 0 || $lower_left_val === 0 || $lower_right_val === 0) => $this->nextInt(5) === 0 ? 0 : $center_val, - $center_val === 0 && ($upper_left_val !== 0 || $upper_right_val !== 0 || $lower_left_val !== 0 || $lower_right_val !== 0) => $this->nextInt(3) === 0 ? $upper_left_val : 0, + $center_val !== 0 && ($upper_left_val === 0 || $upper_right_val === 0 || $lower_left_val === 0 || $lower_right_val === 0) => $this->nextInt(8) === 0 ? 0 : $center_val, + $center_val === 0 && ($upper_left_val !== 0 || $upper_right_val !== 0 || $lower_left_val !== 0 || $lower_right_val !== 0) => $this->nextInt(2) === 0 ? $upper_left_val : 0, default => $center_val }; } diff --git a/src/muqsit/vanillagenerator/generator/biomegrid/NoiseMapLayer.php b/src/muqsit/vanillagenerator/generator/biomegrid/NoiseMapLayer.php index 842de03..73cab38 100644 --- a/src/muqsit/vanillagenerator/generator/biomegrid/NoiseMapLayer.php +++ b/src/muqsit/vanillagenerator/generator/biomegrid/NoiseMapLayer.php @@ -13,32 +13,22 @@ class NoiseMapLayer extends MapLayer{ public function __construct(int $seed){ parent::__construct($seed); - $this->noise_gen = new SimplexOctaveGenerator(new Random($seed), 2); + $this->noise_gen = new SimplexOctaveGenerator(new Random($seed), 3); } public function generateValues(int $x, int $z, int $size_x, int $size_z) : array{ $values = []; for($i = 0; $i < $size_z; ++$i){ for($j = 0; $j < $size_x; ++$j){ - $noise = $this->noise_gen->octaveNoise($x + $j, $z + $i, 0, 0.175, 0.8, true) * 4.0; + $noise = $this->noise_gen->octaveNoise($x + $j, $z + $i, 0, 0.2, 0.75, true) * 3.5; $val = 0; - if($noise >= 0.05){ - $val = $noise <= 0.2 ? 3 : 2; + if($noise >= -0.1){ + $val = $noise <= 0.15 ? 3 : 2; }else{ $this->setCoordsSeed($x + $j, $z + $i); - $val = $this->nextInt(2) === 0 ? 3 : 0; + $val = $this->nextInt(10) < 3 ? 0 : 3; } $values[$j + $i * $size_x] = $val; - //$values[$j + $i * $size_x] = - // $noise >= -0.5 - // ? (float) $noise >= 0.57 - // ? 2 - // : $noise <= 0.2 - // ? 3 - // : 2 - // : $this->nextInt(2) === 0 - // ? 3 - // : 0; } } return $values; diff --git a/src/muqsit/vanillagenerator/generator/cave/CaveGenerator.php b/src/muqsit/vanillagenerator/generator/cave/CaveGenerator.php new file mode 100644 index 0000000..a4a1f6b --- /dev/null +++ b/src/muqsit/vanillagenerator/generator/cave/CaveGenerator.php @@ -0,0 +1,397 @@ +seed = $seed; + $this->random = new Random($seed); + + // Low-frequency field to steer tunnels (Perlin worms) + $this->flowNoise = SimplexOctaveGenerator::fromRandomAndOctaves(new Random($seed + 11), 3, 1, 0, 0); + $this->flowNoise->setScale(1.0 / 96.0); + } + + /** + * Carve caves in the specified chunk + * @param ChunkManager $world the affected world + * @param int $chunkX chunk X coordinate + * @param int $chunkZ chunk Z coordinate + */ + public function carveDirectly(ChunkManager $world, int $chunkX, int $chunkZ): void { + $chunk = $world->getChunk($chunkX, $chunkZ); + if ($chunk === null) return; + + $this->generateRegionalCaves($world, $chunkX, $chunkZ); + } + + /** + * Generate caves considering a larger region for natural flow + * @param ChunkManager $world the affected world + * @param int $chunkX chunk X coordinate + * @param int $chunkZ chunk Z coordinate + */ + private function generateRegionalCaves(ChunkManager $world, int $chunkX, int $chunkZ): void { + for ($regionX = $chunkX - 1; $regionX <= $chunkX + 1; $regionX++) { + for ($regionZ = $chunkZ - 1; $regionZ <= $chunkZ + 1; $regionZ++) { + // Include world seed so cave layout differs between worlds + $this->random->setSeed(($regionX * 341873128712 + $regionZ * 132897987541) ^ $this->seed); + + $caveAttempts = 6 + $this->random->nextBoundedInt(8); + + for ($i = 0; $i < $caveAttempts; $i++) { + if ($this->random->nextFloat() < self::CAVE_FREQUENCY) { + $regionBaseX = $regionX << 4; + $regionBaseZ = $regionZ << 4; + + $startX = $regionBaseX + $this->random->nextBoundedInt(16); + $startY = self::MIN_CAVE_Y + $this->random->nextBoundedInt(self::MAX_CAVE_Y - self::MIN_CAVE_Y); + $startZ = $regionBaseZ + $this->random->nextBoundedInt(16); + $this->generateCaveSystem($world, $startX, $startY, $startZ, $chunkX, $chunkZ); + } + } + + if ($this->random->nextFloat() < self::CAVERN_FREQUENCY) { + $regionBaseX = $regionX << 4; + $regionBaseZ = $regionZ << 4; + + $centerX = $regionBaseX + $this->random->nextBoundedInt(16); + $centerY = self::MIN_CAVE_Y + $this->random->nextBoundedInt(self::MAX_CAVE_Y - self::MIN_CAVE_Y); + $centerZ = $regionBaseZ + $this->random->nextBoundedInt(16); + $this->generateNaturalCavern($world, $centerX, $centerY, $centerZ, $chunkX, $chunkZ); + } + + } + } + } + + /** + * Generate SpaghettiCaves: Perlin-worm style long, curvy tunnels with occasional branches and chambers. + * @param ChunkManager $world the affected world + * @param int $startX starting X coordinate + * @param int $startY starting Y coordinate + * @param int $startZ starting Z coordinate + * @param int $targetChunkX chunk X coordinate to limit carving to + * @param int $targetChunkZ chunk Z coordinate to limit carving to + * @return int number of blocks carved + */ + private function generateCaveSystem(ChunkManager $world, int $startX, int $startY, int $startZ, int $targetChunkX, int $targetChunkZ): int { + $length = self::TUNNEL_LENGTH_MIN + $this->random->nextBoundedInt(self::TUNNEL_LENGTH_MAX - self::TUNNEL_LENGTH_MIN); + + $x = (float)$startX; + $y = (float)$startY; + $z = (float)$startZ; + + // Initialize direction as a 3D unit vector + $yaw = $this->random->nextFloat() * M_PI * 2.0; + $pitch = ($this->random->nextFloat() - 0.5) * 0.25; + + // Perlin-worm + $yawStrength = 0.35; + $pitchStrength = 0.22; + $maxPitch = 0.46; + $speed = 0.95; + + $carvedBlocks = 0; + + for ($i = 0; $i < $length; $i++) { + $progress = $i / (float)$length; + $baseRadius = 1.9 + $this->random->nextFloat() * 1.9; // thicker tunnels + + // Mild size variation to keep shape organic but not jagged + $sizeVariation = sin($progress * M_PI * 2.4) * 0.35 + ($this->random->nextFloat() - 0.5) * 0.4; + $radius = max(1.1, $baseRadius + $sizeVariation); + + $carvedBlocks += $this->carveSphere($world, (int)round($x), (int)round($y), (int)round($z), $radius, $targetChunkX, $targetChunkZ); + + // Perlin-worm heading updates from flow field + $noiseYaw = $this->flowNoise->noise($x, $y, $z, 2.0, 0.5, true); // [-1,1] + $noisePitch = $this->flowNoise->noise($x + 1000.0, $y + 1000.0, $z + 1000.0, 2.0, 0.5, true); // decorrelated + $yaw += $noiseYaw * $yawStrength; + $pitch += $noisePitch * $pitchStrength; + $pitch = max(-$maxPitch, min($maxPitch, $pitch)); + + $x += cos($yaw) * cos($pitch) * $speed; + $y += sin($pitch) * $speed; + $z += sin($yaw) * cos($pitch) * $speed; + + // Keep within reasonable Y bounds by reflecting direction softly + if ($y < self::MIN_CAVE_Y + 2) { + $y = (float)(self::MIN_CAVE_Y + 2); + $pitch = abs($pitch); // go up + } elseif ($y > self::MAX_CAVE_Y - 2) { + $y = (float)(self::MAX_CAVE_Y - 2); + $pitch = -abs($pitch); // go down + } + + // Less frequent branches, but longer + if ($this->random->nextFloat() < 0.03 && $i > 30) { + $branchLength = 100 + $this->random->nextBoundedInt(140); + $carvedBlocks += $this->generateBranch($world, $x, $y, $z, $branchLength, $targetChunkX, $targetChunkZ); + } + + // Chambers occur rarely to keep the "worm" feel + if ($this->random->nextFloat() < 0.006 && $i > 40) { + $carvedBlocks += $this->generateSmallChamber($world, $x, $y, $z, $targetChunkX, $targetChunkZ); + } + } + + return $carvedBlocks; + } + + /** + * Generate natural branch tunnels + * @param ChunkManager $world the affected world + * @param float $startX starting X coordinate + * @param float $startY starting Y coordinate + * @param float $startZ starting Z coordinate + * @param int $length length of the branch + * @param int $targetChunkX chunk X coordinate to limit carving to + * @param int $targetChunkZ chunk Z coordinate to limit carving to + * @return int number of blocks carved + */ + private function generateBranch(ChunkManager $world, float $startX, float $startY, float $startZ, int $length, int $targetChunkX, int $targetChunkZ): int { + $x = $startX; + $y = $startY; + $z = $startZ; + + $yaw = $this->random->nextFloat() * M_PI * 2.0; + $pitch = ($this->random->nextFloat() - 0.5) * 0.3; + + $carvedBlocks = 0; + + for ($i = 0; $i < $length; $i++) { + $progress = $i / (float)$length; + $radius = 2.1 - $progress * 0.55 + ($this->random->nextFloat() - 0.5) * 0.38; + $radius = max(0.9, $radius); + + $carvedBlocks += $this->carveSphere($world, (int)round($x), (int)round($y), (int)round($z), $radius, $targetChunkX, $targetChunkZ); + + // steer by flow field (weaker than main tunnels) + $noiseYaw = $this->flowNoise->noise($x, $y, $z, 2.0, 0.5, true); + $noisePitch = $this->flowNoise->noise($x + 1000.0, $y + 1000.0, $z + 1000.0, 2.0, 0.5, true); + $yaw += $noiseYaw * 0.22; + $pitch += $noisePitch * 0.15; + $pitch = max(-0.35, min(0.35, $pitch)); + + $moveSpeed = 0.7 + $this->random->nextFloat() * 0.35; + + $x += cos($yaw) * cos($pitch) * $moveSpeed; + $y += sin($pitch) * $moveSpeed; + $z += sin($yaw) * cos($pitch) * $moveSpeed; + } + + return $carvedBlocks; + } + + /** + * Generate a small chamber by overlapping several spheres + * @param ChunkManager $world the affected world + * @param float $centerX center X coordinate + * @param float $centerY center Y coordinate + * @param float $centerZ center Z coordinate + * @param int $targetChunkX chunk X coordinate to limit carving to + * @param int $targetChunkZ chunk Z coordinate to limit carving to + * @return int number of blocks carved + */ + private function generateSmallChamber(ChunkManager $world, float $centerX, float $centerY, float $centerZ, int $targetChunkX, int $targetChunkZ): int { + $size = 4 + $this->random->nextBoundedInt(6); + $carvedBlocks = 0; + + $sphereCount = 2 + $this->random->nextBoundedInt(3); + + for ($i = 0; $i < $sphereCount; $i++) { + $offsetX = $centerX + ($this->random->nextFloat() - 0.5) * $size * 0.5; + $offsetY = $centerY + ($this->random->nextFloat() - 0.5) * $size * 0.3; + $offsetZ = $centerZ + ($this->random->nextFloat() - 0.5) * $size * 0.5; + + $radius = 2.5 + $this->random->nextFloat() * 2.0; + + $carvedBlocks += $this->carveSphere($world, (int)round($offsetX), (int)round($offsetY), (int)round($offsetZ), $radius, $targetChunkX, $targetChunkZ); + } + + return $carvedBlocks; + } + + /** + * Generate natural large caverns + * @param ChunkManager $world the affected world + * @param int $centerX center X coordinate + * @param int $centerY center Y coordinate + * @param int $centerZ center Z coordinate + * @param int $targetChunkX chunk X coordinate to limit carving to + * @param int $targetChunkZ chunk Z coordinate to limit carving to + * @return int number of blocks carved + */ + private function generateNaturalCavern(ChunkManager $world, int $centerX, int $centerY, int $centerZ, int $targetChunkX, int $targetChunkZ): int { + $sizeX = self::CAVERN_SIZE_MIN + $this->random->nextBoundedInt(self::CAVERN_SIZE_MAX - self::CAVERN_SIZE_MIN); + $sizeY = (int)($sizeX * 0.6) + $this->random->nextBoundedInt((int)($sizeX * 0.3)); + $sizeZ = self::CAVERN_SIZE_MIN + $this->random->nextBoundedInt(self::CAVERN_SIZE_MAX - self::CAVERN_SIZE_MIN); + + $carvedBlocks = 0; + + $sphereCount = 4 + $this->random->nextBoundedInt(7); // 4-10 spheres + + for ($i = 0; $i < $sphereCount; $i++) { + $angle = ($i / (float)$sphereCount) * M_PI * 2.0 + ($this->random->nextFloat() - 0.5) * 1.0; + $distance = $this->random->nextFloat() * $sizeX * 0.4; + + $offsetX = $centerX + cos($angle) * $distance; + $offsetY = $centerY + ($this->random->nextFloat() - 0.5) * $sizeY * 0.8; + $offsetZ = $centerZ + sin($angle) * $distance; + + $radius = 3.0 + $this->random->nextFloat() * 4.0; + + $carvedBlocks += $this->carveSphere($world, (int)round($offsetX), (int)round($offsetY), (int)round($offsetZ), $radius, $targetChunkX, $targetChunkZ); + } + + $tunnelCount = 2 + $this->random->nextBoundedInt(4); + for ($i = 0; $i < $tunnelCount; $i++) { + // Longer tunnels extending from caverns + $tunnelLength = 60 + $this->random->nextBoundedInt(120); + $yaw = $this->random->nextFloat() * M_PI * 2.0; + $pitch = ($this->random->nextFloat() - 0.5) * 0.4; + + $x = (float)$centerX; + $y = (float)$centerY; + $z = (float)$centerZ; + + for ($j = 0; $j < $tunnelLength; $j++) { + $progress = $j / (float)$tunnelLength; + $radius = 2.8 - $progress * 1.0; + $radius = max(1.0, $radius); + + $carvedBlocks += $this->carveSphere($world, (int)round($x), (int)round($y), (int)round($z), $radius, $targetChunkX, $targetChunkZ); + + $yaw += ($this->random->nextFloat() - 0.5) * 0.15; + $pitch += ($this->random->nextFloat() - 0.5) * 0.08; + + $x += cos($yaw) * cos($pitch) * 0.9; + $y += sin($pitch) * 0.9; + $z += sin($yaw) * cos($pitch) * 0.9; + } + } + + return $carvedBlocks; + } + + /** + * Carve out a sphere of given radius at the specified coordinates within the target chunk + * @param ChunkManager $world the affected world + * @param int $centerX center X coordinate + * @param int $centerY center Y coordinate + * @param int $centerZ center Z coordinate + * @param float $radius radius of the sphere + * @param int $targetChunkX chunk X coordinate to limit carving to + * @param int $targetChunkZ chunk Z coordinate to limit carving to + * @return int number of blocks carved + */ + private function carveSphere(ChunkManager $world, int $centerX, int $centerY, int $centerZ, float $radius, int $targetChunkX, int $targetChunkZ): int { + $chunk = $world->getChunk($targetChunkX, $targetChunkZ); + if ($chunk === null) return 0; + + $chunkBaseX = $targetChunkX << 4; + $chunkBaseZ = $targetChunkZ << 4; + + $carvedBlocks = 0; + $radiusSquared = $radius * $radius; + + $airId = VanillaBlocks::AIR()->getStateId(); + $stoneId = VanillaBlocks::STONE()->getStateId(); + $deepslateId = VanillaBlocks::DEEPSLATE()->getStateId(); + + $minX = max(0, $centerX - (int)ceil($radius) - $chunkBaseX); + $maxX = min(15, $centerX + (int)ceil($radius) - $chunkBaseX); + $minY = max(self::MIN_CAVE_Y, $centerY - (int)ceil($radius)); + $maxY = min(self::MAX_CAVE_Y, $centerY + (int)ceil($radius)); + $minZ = max(0, $centerZ - (int)ceil($radius) - $chunkBaseZ); + $maxZ = min(15, $centerZ + (int)ceil($radius) - $chunkBaseZ); + + for ($x = $minX; $x <= $maxX; $x++) { + for ($y = $minY; $y <= $maxY; $y++) { + for ($z = $minZ; $z <= $maxZ; $z++) { + $worldX = $chunkBaseX + $x; + $worldY = $y; + $worldZ = $chunkBaseZ + $z; + + $dx = $worldX - $centerX; + $dy = $worldY - $centerY; + $dz = $worldZ - $centerZ; + $distanceSquared = $dx * $dx + $dy * $dy + $dz * $dz; + + if ($distanceSquared <= $radiusSquared) { + $block = $chunk->getBlockStateId($x, $y, $z); + if ($block === $stoneId || $block === $deepslateId) { + $chunk->setBlockStateId($x, $y, $z, $airId); + $carvedBlocks++; + } + } + } + } + } + + return $carvedBlocks; + } + + /** + * Apply aquifers such as lava pools at low depths + * @param ChunkManager $world the affected world + * @param int $chunkX chunk X coordinate + * @param int $chunkZ chunk Z coordinate + */ + public function applyAquifers(ChunkManager $world, int $chunkX, int $chunkZ): void { + $chunk = $world->getChunk($chunkX, $chunkZ); + if ($chunk === null) return; + + // Include world seed in aquifer-related randomness as well + $this->random->setSeed(($chunkX * 871236847 + $chunkZ * 321487613) ^ $this->seed); + + $this->applyLavaPools($chunk); + } + + /** + * Fill air pockets below a certain Y level with lava + * @param Chunk $chunk the affected chunk + */ + private function applyLavaPools(Chunk $chunk): void { + $airId = VanillaBlocks::AIR()->getStateId(); + $lavaId = VanillaBlocks::LAVA()->getStateId(); + + for ($x = 0; $x < 16; $x++) { + for ($z = 0; $z < 16; $z++) { + for ($y = self::LAVA_LEVEL; $y >= self::LAVA_MIN_Y; $y--) { + if ($chunk->getBlockStateId($x, $y, $z) === $airId) { + $chunk->setBlockStateId($x, $y, $z, $lavaId); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/muqsit/vanillagenerator/generator/ground/GroundGenerator.php b/src/muqsit/vanillagenerator/generator/ground/GroundGenerator.php index 0a1a693..d451bea 100644 --- a/src/muqsit/vanillagenerator/generator/ground/GroundGenerator.php +++ b/src/muqsit/vanillagenerator/generator/ground/GroundGenerator.php @@ -67,6 +67,7 @@ public function generateTerrainColumn(ChunkManager $world, Random $random, int $ $block_state_registry = RuntimeBlockStateRegistry::getInstance(); $air = VanillaBlocks::AIR()->getStateId(); $stone = VanillaBlocks::STONE()->getStateId(); + $deepslate = VanillaBlocks::DEEPSLATE()->getStateId(); $sandstone = VanillaBlocks::SANDSTONE()->getStateId(); $gravel = VanillaBlocks::GRAVEL()->getStateId(); $bedrock = VanillaBlocks::BEDROCK()->getStateId(); @@ -77,8 +78,33 @@ public function generateTerrainColumn(ChunkManager $world, Random $random, int $ $block_x = $x & Chunk::COORD_MASK; $block_z = $z & Chunk::COORD_MASK; - for($y = 255; $y >= 0; --$y){ - if($y <= $random->nextBoundedInt($this->bedrock_roughness)){ + // Use actual world bounds but validate subchunk access + $world_max_y = $world->getMaxY(); + $world_min_y = $world->getMinY(); + + $rng = $random; + $pickStoneId = static function(int $y) use ($stone, $deepslate, $rng) : int{ + if($y <= 0){ + return $deepslate; + } + if($y > 8){ + return $stone; + } + $p = (8 - $y) / 8.0; + $p = $p * $p; + $rand01 = $rng->nextFloat(); + return ($rand01 < $p) ? $deepslate : $stone; + }; + + for($y = $world_max_y; $y >= $world_min_y; --$y){ + // Validate subchunk index before accessing chunk + $subchunk_index = $y >> 4; + if($subchunk_index < -4 || $subchunk_index > 19) { + continue; // Skip invalid subchunk indices + } + + // Place bedrock at the bottom of the world + if($y <= $world_min_y + $random->nextBoundedInt($this->bedrock_roughness)){ $chunk->setBlockStateId($block_x, $y, $block_z, $bedrock); }else{ $mat = $block_state_registry->fromStateId($chunk->getBlockStateId($block_x, $y, $block_z)); @@ -98,15 +124,24 @@ public function generateTerrainColumn(ChunkManager $world, Random $random, int $ $chunk->setBlockStateId($block_x, $y, $block_z, $top_mat); }elseif($y < $sea_level - 8 - $surface_height){ $top_mat = $air; - $ground_mat = $stone; + $ground_mat = $pickStoneId($y, $chunk_x, $chunk_z); $ground_mat_id = BlockTypeIds::STONE; $chunk->setBlockStateId($block_x, $y, $block_z, $gravel); }else{ - $chunk->setBlockStateId($block_x, $y, $block_z, $ground_mat); + // If the current ground material is stone, swap to deepslate when appropriate + $mat_to_set = $ground_mat; + if($ground_mat_id === BlockTypeIds::STONE){ + $mat_to_set = $pickStoneId($y, $chunk_x, $chunk_z); + } + $chunk->setBlockStateId($block_x, $y, $block_z, $mat_to_set); } }elseif($deep > 0){ --$deep; - $chunk->setBlockStateId($block_x, $y, $block_z, $ground_mat); + $mat_to_set = $ground_mat; + if($ground_mat_id === BlockTypeIds::STONE){ + $mat_to_set = $pickStoneId($y, $chunk_x, $chunk_z); + } + $chunk->setBlockStateId($block_x, $y, $block_z, $mat_to_set); if($deep === 0 && $ground_mat_id === BlockTypeIds::SAND){ $deep = $random->nextBoundedInt(4) + max(0, $y - $sea_level - 1); diff --git a/src/muqsit/vanillagenerator/generator/ground/MesaGroundGenerator.php b/src/muqsit/vanillagenerator/generator/ground/MesaGroundGenerator.php index 3d5bdf8..f785293 100644 --- a/src/muqsit/vanillagenerator/generator/ground/MesaGroundGenerator.php +++ b/src/muqsit/vanillagenerator/generator/ground/MesaGroundGenerator.php @@ -83,11 +83,22 @@ public function generateTerrainColumn(ChunkManager $world, Random $random, int $ $grass = VanillaBlocks::GRASS(); $coarse_dirt = VanillaBlocks::DIRT()->setDirtType(DirtType::COARSE); - for($y = 255; $y >= 0; --$y){ + // Use actual world bounds but validate subchunk access + $world_max_y = $world->getMaxY(); + $world_min_y = $world->getMinY(); + + for($y = $world_max_y; $y >= $world_min_y; --$y){ + // Validate subchunk index before accessing world + $subchunk_index = $y >> 4; + if($subchunk_index < -4 || $subchunk_index > 19) { + continue; // Skip invalid subchunk indices + } + if($y < (int) $bryce_canyon_height && $world->getBlockAt($x, $y, $z)->getTypeId() === BlockTypeIds::AIR){ $world->setBlockAt($x, $y, $z, VanillaBlocks::STONE()); } - if($y <= $random->nextBoundedInt(5)){ + // Place bedrock at the bottom of the world + if($y <= $world_min_y + $random->nextBoundedInt(5)){ $world->setBlockAt($x, $y, $z, VanillaBlocks::BEDROCK()); }else{ $mat_id = $world->getBlockAt($x, $y, $z)->getTypeId(); diff --git a/src/muqsit/vanillagenerator/generator/object/OreVein.php b/src/muqsit/vanillagenerator/generator/object/OreVein.php index 18445b7..3321846 100644 --- a/src/muqsit/vanillagenerator/generator/object/OreVein.php +++ b/src/muqsit/vanillagenerator/generator/object/OreVein.php @@ -5,6 +5,7 @@ namespace muqsit\vanillagenerator\generator\object; use pocketmine\block\Block; +use pocketmine\block\BlockTypeIds; use pocketmine\utils\Random; use pocketmine\world\ChunkManager; @@ -80,9 +81,13 @@ public function generate(ChunkManager $world, Random $random, int $source_x, int } for($z = $min_z; $z <= $max_z; ++$z){ $squared_normalized_z = self::normalizedSquaredCoordinate($origin_z, $radius_h, $z); - if($squared_normalized_x + $squared_normalized_y + $squared_normalized_z < 1 && $world->getBlockAt($x, $y, $z)->getTypeId() === $this->target_type){ - $world->setBlockAt($x, $y, $z, $this->type); - $succeeded = true; + if($squared_normalized_x + $squared_normalized_y + $squared_normalized_z < 1){ + $under_id = $world->getBlockAt($x, $y, $z)->getTypeId(); + // Replace only the configured target type (STONE for stone ores, DEEPSLATE for deepslate ores) + if($under_id === $this->target_type){ + $world->setBlockAt($x, $y, $z, $this->type); + $succeeded = true; + } } } } diff --git a/src/muqsit/vanillagenerator/generator/object/TerrainObject.php b/src/muqsit/vanillagenerator/generator/object/TerrainObject.php index 8ba92a0..40f2169 100644 --- a/src/muqsit/vanillagenerator/generator/object/TerrainObject.php +++ b/src/muqsit/vanillagenerator/generator/object/TerrainObject.php @@ -25,8 +25,9 @@ abstract class TerrainObject{ public static function killWeakBlocksAbove(ChunkManager $world, int $x, int $y, int $z) : bool{ $cur_y = $y + 1; $changed = false; + $max_y = $world->getMaxY(); - while($cur_y < World::Y_MAX){ + while($cur_y < $max_y){ // Use < instead of <= to prevent accessing Y=320 $block = $world->getBlockAt($x, $cur_y, $z); if(!($block instanceof Flowable)){ break; diff --git a/src/muqsit/vanillagenerator/generator/overworld/OverworldGenerator.php b/src/muqsit/vanillagenerator/generator/overworld/OverworldGenerator.php index f847353..f06e278 100644 --- a/src/muqsit/vanillagenerator/generator/overworld/OverworldGenerator.php +++ b/src/muqsit/vanillagenerator/generator/overworld/OverworldGenerator.php @@ -19,17 +19,21 @@ use muqsit\vanillagenerator\generator\noise\glowstone\SimplexOctaveGenerator; use muqsit\vanillagenerator\generator\overworld\biome\BiomeHeightManager; use muqsit\vanillagenerator\generator\overworld\biome\BiomeIds; +use muqsit\vanillagenerator\generator\overworld\biome\Biome3DGenerator; use muqsit\vanillagenerator\generator\overworld\populator\OverworldPopulator; use muqsit\vanillagenerator\generator\overworld\populator\SnowPopulator; use muqsit\vanillagenerator\generator\utils\preset\SimpleGeneratorPreset; use muqsit\vanillagenerator\generator\utils\WorldOctaves; use muqsit\vanillagenerator\generator\VanillaBiomeGrid; use muqsit\vanillagenerator\generator\VanillaGenerator; +use muqsit\vanillagenerator\generator\cave\CaveGenerator; use pocketmine\block\VanillaBlocks; use pocketmine\utils\Random; use pocketmine\world\ChunkManager; use pocketmine\world\format\Chunk; use function array_key_exists; +use function max; +use function min; /** * @extends VanillaGenerator> @@ -62,6 +66,7 @@ private static function densityHash(int $i, int $j, int $k) : int{ } public static function init() : void{ + // Biome-specific ground generators self::setBiomeSpecificGround(new SandyGroundGenerator(), BiomeIds::BEACH, BiomeIds::COLD_BEACH, BiomeIds::DESERT, BiomeIds::DESERT_HILLS, BiomeIds::DESERT_MUTATED); self::setBiomeSpecificGround(new RockyGroundGenerator(), BiomeIds::STONE_BEACH); self::setBiomeSpecificGround(new SnowyGroundGenerator(), BiomeIds::ICE_PLAINS_SPIKES); @@ -103,7 +108,7 @@ protected static function setBiomeSpecificGround(GroundGenerator $gen, int ...$b protected const DETAIL_NOISE_SCALE_Y = 160.0; protected const DETAIL_NOISE_SCALE_Z = 80.0; protected const SURFACE_SCALE = 0.0625; - protected const BASE_SIZE = 8.5; + protected const BASE_SIZE = 16.0; protected const STRETCH_Y = 12.0; protected const BIOME_HEIGHT_OFFSET = 0.0; protected const BIOME_HEIGHT_WEIGHT = 1.0; @@ -115,6 +120,7 @@ protected static function setBiomeSpecificGround(GroundGenerator $gen, int ...$b private GroundGenerator $ground_gen; private string $type = WorldType::NORMAL; + private ?CaveGenerator $cave_generator = null; public function __construct(int $seed, string $preset_string){ $preset = SimpleGeneratorPreset::parse($preset_string); @@ -125,13 +131,10 @@ public function __construct(int $seed, string $preset_string){ $preset ); $this->ground_gen = new GroundGenerator(); + $this->cave_generator = new CaveGenerator($seed); $this->addPopulators(new OverworldPopulator(), new SnowPopulator()); } - public function getGroundGenerator() : GroundGenerator{ - return $this->ground_gen; - } - protected function generateChunkData(ChunkManager $world, int $chunk_x, int $chunk_z, VanillaBiomeGrid $grid) : void{ $this->generateRawTerrain($world, $chunk_x, $chunk_z); @@ -150,19 +153,40 @@ protected function generateChunkData(ChunkManager $world, int $chunk_x, int $chu $min_y = $world->getMinY(); $max_y = $world->getMaxY(); + + // Generate 3D biomes for this chunk + Biome3DGenerator::generate3DBiomes($grid, $this->random, $chunk_x, $chunk_z, $min_y, $max_y); + for($x = 0; $x < $size_x; ++$x){ for($z = 0; $z < $size_z; ++$z){ - $id = $grid->getBiome($x, $z); + $surface_biome_id = $grid->getBiome($x, $z); + + // Set biomes for all Y levels using 3D biome data for($y = $min_y; $y < $max_y; ++$y){ - $chunk->setBiomeId($x, $y, $z, $id); + $biome_3d = $grid->getBiome3D($x, $y, $z); + if($biome_3d !== null){ + $chunk->setBiomeId($x, $y, $z, $biome_3d); + }else{ + // Fallback to surface biome if no 3D biome is set + $chunk->setBiomeId($x, $y, $z, $surface_biome_id); + } } - if($id !== null && array_key_exists($id, self::$GROUND_MAP)){ - self::$GROUND_MAP[$id]->generateTerrainColumn($world, $this->random, $cx + $x, $cz + $z, $id, $surface_noise[$x | $z << Chunk::COORD_BIT_SIZE]); + + // Surface terrain (top/ground materials) + if($surface_biome_id !== null && array_key_exists($surface_biome_id, self::$GROUND_MAP)){ + self::$GROUND_MAP[$surface_biome_id]->generateTerrainColumn($world, $this->random, $cx + $x, $cz + $z, $surface_biome_id, $surface_noise[$x | $z << Chunk::COORD_BIT_SIZE]); }else{ - $this->ground_gen->generateTerrainColumn($world, $this->random, $cx + $x, $cz + $z, $id, $surface_noise[$x | $z << Chunk::COORD_BIT_SIZE]); + $this->ground_gen->generateTerrainColumn($world, $this->random, $cx + $x, $cz + $z, $surface_biome_id, $surface_noise[$x | $z << Chunk::COORD_BIT_SIZE]); } } } + + // Caves: carve after base stone terrain and biome assignment (legacy) + if($this->cave_generator !== null){ + $this->cave_generator->carveDirectly($world, $chunk_x, $chunk_z); + $this->cave_generator->applyAquifers($world, $chunk_x, $chunk_z); + } + } protected function createWorldOctaves() : WorldOctaves{ @@ -172,17 +196,17 @@ protected function createWorldOctaves() : WorldOctaves{ $height->x_scale = self::HEIGHT_NOISE_SCALE_X; $height->z_scale = self::HEIGHT_NOISE_SCALE_Z; - $roughness = PerlinOctaveGenerator::fromRandomAndOctaves($seed, 16, 5, 33, 5); + $roughness = PerlinOctaveGenerator::fromRandomAndOctaves($seed, 16, 5, 48, 5); $roughness->x_scale = self::COORDINATE_SCALE; $roughness->y_scale = self::HEIGHT_SCALE; $roughness->z_scale = self::COORDINATE_SCALE; - $roughness2 = PerlinOctaveGenerator::fromRandomAndOctaves($seed, 16, 5, 33, 5); + $roughness2 = PerlinOctaveGenerator::fromRandomAndOctaves($seed, 16, 5, 48, 5); $roughness2->x_scale = self::COORDINATE_SCALE; $roughness2->y_scale = self::HEIGHT_SCALE; $roughness2->z_scale = self::COORDINATE_SCALE; - $detail = PerlinOctaveGenerator::fromRandomAndOctaves($seed, 8, 5, 33, 5); + $detail = PerlinOctaveGenerator::fromRandomAndOctaves($seed, 8, 5, 48, 5); $detail->x_scale = self::COORDINATE_SCALE / self::DETAIL_NOISE_SCALE_X; $detail->y_scale = self::HEIGHT_SCALE / self::DETAIL_NOISE_SCALE_Y; $detail->z_scale = self::COORDINATE_SCALE / self::DETAIL_NOISE_SCALE_Z; @@ -193,6 +217,9 @@ protected function createWorldOctaves() : WorldOctaves{ return new WorldOctaves($height, $roughness, $roughness2, $detail, $surface); } + /** + * Generates raw terrain for a chunk at chunk coordinates x,z (does not accept block coords). + */ protected function generateRawTerrain(ChunkManager $world, int $chunk_x, int $chunk_z) : void{ $density = $this->generateTerrainDensity($chunk_x, $chunk_z); @@ -210,13 +237,28 @@ protected function generateRawTerrain(ChunkManager $world, int $chunk_x, int $ch $still_water = VanillaBlocks::WATER()->getStillForm()->getStateId(); $water = VanillaBlocks::WATER()->getFlowingForm()->getStateId(); $stone = VanillaBlocks::STONE()->getStateId(); + $deepslate = VanillaBlocks::DEEPSLATE()->getStateId(); + + $rng = $this->random; + $pickStoneId = static function(int $y) use ($stone, $deepslate, $rng) : int{ + if($y <= 0){ + return $deepslate; + } + if($y > 8){ + return $stone; + } + $p = (8 - $y) / 8.0; + $p = $p * $p; + $rand01 = $rng->nextFloat(); + return ($rand01 < $p) ? $deepslate : $stone; + }; /** @var Chunk $chunk */ $chunk = $world->getChunk($chunk_x, $chunk_z); for($i = 0; $i < 5 - 1; ++$i){ for($j = 0; $j < 5 - 1; ++$j){ - for($k = 0; $k < 33 - 1; ++$k){ + for($k = 0; $k < 48 - 1; ++$k){ // 2x2 grid $d1 = $density[self::densityHash($i, $j, $k)]; $d2 = $density[self::densityHash($i + 1, $j, $k)]; @@ -231,13 +273,29 @@ protected function generateRawTerrain(ChunkManager $world, int $chunk_x, int $ch $d9 = $d1; $d10 = $d3; - $y_pos = $l + ($k << 3); + $y_pos = $l + ($k << 3) - 64; + + // Clamp Y position to valid range as safety measure + $y_pos = max(-64, min(319, $y_pos)); + $y_block_pos = $y_pos & 0xf; - $sub_chunk = $chunk->getSubChunk($y_pos >> Chunk::COORD_BIT_SIZE); + // Calculate subchunk index: Y=-64 maps to subchunk -4, Y=319 maps to subchunk 19 + $subchunk_index = $y_pos >> Chunk::COORD_BIT_SIZE; + + // Final safety check for subchunk index + if($subchunk_index < -4 || $subchunk_index > 19){ + continue; + } + + $sub_chunk = $chunk->getSubChunk($subchunk_index); for($m = 0; $m < 4; ++$m){ $dens = $d9; for($n = 0; $n < 4; ++$n){ + $block_x = $m + ($i << 2); + $block_z = $n + ($j << 2); + $abs_x = ($chunk_x << 4) + $block_x; + $abs_z = ($chunk_z << 4) + $block_z; // any density higher than density offset is ground, any density // lower or equal to the density offset is air // (or water if under the sea level). @@ -251,12 +309,12 @@ protected function generateRawTerrain(ChunkManager $world, int $chunk_x, int $ch if($afill === 1 || $afill === 10 || $afill === 13 || $afill === 16){ $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $water); }elseif($afill === 2 || $afill === 9 || $afill === 12 || $afill === 15){ - $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $stone); + $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $pickStoneId($y_pos, $abs_x, $abs_z)); } if(($dens > $density_offset && $fill > -1) || ($dens <= $density_offset && $fill < 0)){ if($afill === 0 || $afill === 3 || $afill === 6 || $afill === 9 || $afill === 12){ - $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $stone); + $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $pickStoneId($y_pos, $abs_x, $abs_z)); }elseif($afill === 2 || $afill === 7 || $afill === 10 || $afill === 16){ $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $still_water); } @@ -264,7 +322,7 @@ protected function generateRawTerrain(ChunkManager $world, int $chunk_x, int $ch if($afill === 0 || $afill === 3 || $afill === 7 || $afill === 10 || $afill === 13){ $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $still_water); }elseif($afill === 1 || $afill === 6 || $afill === 9 || $afill === 15){ - $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $stone); + $sub_chunk->setBlockStateId($m + ($i << 2), $y_block_pos, $n + ($j << 2), $pickStoneId($y_pos, $abs_x, $abs_z)); } } @@ -291,6 +349,10 @@ protected function generateRawTerrain(ChunkManager $world, int $chunk_x, int $ch } /** + * Generates terrain density for a chunk at chunk coordinates x,z (does not accept block coords). + * The returned array contains 5x5x33 = 825 density values, sampled at every 4 blocks on x and z + * axis, and every 8 blocks on y axis. The values are used later to generate raw terrain by + * re-scaling it to 16x16x256 (or other height if world height is different) using linear interpolation. * @param int $x * @param int $z * @return float[] @@ -385,10 +447,11 @@ protected function generateTerrainDensity(int $x, int $z) : array{ } $noise_h = ($noise_h * 0.2 + $avg_height_base) * self::BASE_SIZE / 8.0 * 4.0 + self::BASE_SIZE; - for($k = 0; $k < 33; ++$k){ + + for($k = 0; $k < 48; ++$k){ // density should be lower and lower as we climb up, this gets a height value to // subtract from the noise. - $nh = ($k - $noise_h) * self::STRETCH_Y * 128.0 / 256.0 / $avg_height_scale; + $nh = ($k - $noise_h) * self::STRETCH_Y * 128.0 / 384.0 / $avg_height_scale; if($nh < 0.0){ $nh *= 4.0; } @@ -400,9 +463,17 @@ protected function generateTerrainDensity(int $x, int $z) : array{ // linear interpolation $dens = $noise_d < 0 ? $noise_r : ($noise_d > 1 ? $noise_r_2 : $noise_r + ($noise_r_2 - $noise_r) * $noise_d); $dens -= $nh; + + $targetSeabedY = 40; + $bandMidY = ($k << 3) + 3 - 64; + if($bandMidY < $targetSeabedY){ + $bias = ($targetSeabedY - $bandMidY) * 0.09; + $dens += $bias; + } ++$index; - if($k > 29){ - $lowering = ($k - 29) / 3.0; + // Allow mountains up to Y=255 (k=39 => y 248..255), start tapering above that + if($k >= 40){ + $lowering = ($k - 40) / 9.0; // Spread over 9 levels instead of 3 // linear interpolation $dens = $dens * (1.0 - $lowering) + -10.0 * $lowering; } diff --git a/src/muqsit/vanillagenerator/generator/overworld/biome/Biome3DGenerator.php b/src/muqsit/vanillagenerator/generator/overworld/biome/Biome3DGenerator.php new file mode 100644 index 0000000..bdeb9fa --- /dev/null +++ b/src/muqsit/vanillagenerator/generator/overworld/biome/Biome3DGenerator.php @@ -0,0 +1,106 @@ +getBiome($x, $z); + if($surface_biome === null) continue; + + // Generate biomes for different depth layers + for($y = $world_min_y; $y <= $world_max_y; $y += 4) { + $depth_biome = self::getBiomeForDepth($surface_biome, $y, $random, $chunk_x * 16 + $x, $chunk_z * 16 + $z); + + // Set biome for 4x4x4 region + for($dx = 0; $dx < 4 && $x + $dx < 16; $dx++) { + for($dz = 0; $dz < 4 && $z + $dz < 16; $dz++) { + for($dy = 0; $dy < 4 && $y + $dy <= $world_max_y; $dy++) { + $biome_grid->setBiome3D($x + $dx, $y + $dy, $z + $dz, $depth_biome); + } + } + } + } + } + } + } + + /** + * Determine appropriate biome based on surface biome and Y level + * + * @param int $surface_biome + * @param int $y + * @param Random $random + * @param int $world_x + * @param int $world_z + * @return int + */ + private static function getBiomeForDepth(int $surface_biome, int $y, Random $random, int $world_x, int $world_z): int { + // Underground biome generation based on depth and surface biome + // IMPORTANT: Use deterministic, position-based randomness so results don't depend on PRNG state/order. + $rand01 = static function(float $chance) use ($random, $world_x, $world_z, $y): bool { + $seed = method_exists($random, 'getSeed') ? (string)$random->getSeed() : '0'; + // crc32 gives a stable 32-bit unsigned int; normalize to [0,1) + $h = crc32($seed . '|b3d|' . $world_x . '|' . $world_z . '|' . $y); + $val = $h / 4294967296.0; // 2^32 + return $val < $chance; + }; + + // Deep Dark biome (Y -64 to -10, rare) + if($y >= -64 && $y <= -10){ + // 5% chance for Deep Dark in deep areas, higher chance near Y -52 + $deep_dark_chance = ($y <= -40 && $y >= -60) ? 0.08 : 0.02; + if($rand01($deep_dark_chance)){ + return BiomeIds::DEEP_DARK; + } + } + + // Lush Caves (Y -20 to Y 40, forest and jungle areas) + if($y >= -20 && $y <= 40){ + $is_lush_surface = in_array($surface_biome, [ + BiomeIds::FOREST, BiomeIds::BIRCH_FOREST, BiomeIds::FLOWER_FOREST, + BiomeIds::JUNGLE, BiomeIds::JUNGLE_HILLS, BiomeIds::ROOFED_FOREST + ], true); + + if($is_lush_surface && $rand01(0.15)){ + return BiomeIds::LUSH_CAVES; + } + } + + // Dripstone Caves (Y -60 to Y 20, desert and mesa areas primarily) + if($y >= -60 && $y <= 20){ + $is_dry_surface = in_array($surface_biome, [ + BiomeIds::DESERT, BiomeIds::DESERT_HILLS, BiomeIds::MESA, + BiomeIds::MESA_PLATEAU, BiomeIds::MESA_PLATEAU_STONE, BiomeIds::SAVANNA + ], true); + + if($is_dry_surface && $rand01(0.2)){ + return BiomeIds::DRIPSTONE_CAVES; + } + + // Lower chance in other biomes + if($rand01(0.05)){ + return BiomeIds::DRIPSTONE_CAVES; + } + } + return $surface_biome; + } +} \ No newline at end of file diff --git a/src/muqsit/vanillagenerator/generator/overworld/biome/BiomeHeightManager.php b/src/muqsit/vanillagenerator/generator/overworld/biome/BiomeHeightManager.php index 678b600..c000b4b 100644 --- a/src/muqsit/vanillagenerator/generator/overworld/biome/BiomeHeightManager.php +++ b/src/muqsit/vanillagenerator/generator/overworld/biome/BiomeHeightManager.php @@ -56,6 +56,17 @@ public static function init() : void{ self::register(new BiomeHeight(0.1, 0.4), BiomeIds::FLOWER_FOREST); self::register(new BiomeHeight(0.4125, 1.325), BiomeIds::SAVANNA_MUTATED); self::register(new BiomeHeight(1.1, 1.3125), BiomeIds::SAVANNA_PLATEAU_MUTATED); + + self::register(new BiomeHeight(-1.0, 0.1), BiomeIds::DEEP_DARK); + self::register(new BiomeHeight(-0.7, 0.4), BiomeIds::DRIPSTONE_CAVES); + self::register(new BiomeHeight(-0.5, 0.4), BiomeIds::LUSH_CAVES); + self::register(new BiomeHeight(-0.2, 0.1), BiomeIds::MANGROVE_SWAMP); + self::register(new BiomeHeight(0.2, 0.8), BiomeIds::MEADOW); + self::register(new BiomeHeight(0.25, 0.8), BiomeIds::GROVE); + self::register(new BiomeHeight(1.0, 0.3), BiomeIds::SNOWY_SLOPES); + self::register(new BiomeHeight(1.4, 0.8), BiomeIds::FROZEN_PEAKS); + self::register(new BiomeHeight(1.4, 0.8), BiomeIds::JAGGED_PEAKS); + self::register(new BiomeHeight(1.4, 0.8), BiomeIds::STONY_PEAKS); } public static function register(BiomeHeight $height, int ...$biomes) : void{ diff --git a/src/muqsit/vanillagenerator/generator/overworld/biome/BiomeIds.php b/src/muqsit/vanillagenerator/generator/overworld/biome/BiomeIds.php index f3eb00e..7ecbe93 100644 --- a/src/muqsit/vanillagenerator/generator/overworld/biome/BiomeIds.php +++ b/src/muqsit/vanillagenerator/generator/overworld/biome/BiomeIds.php @@ -69,4 +69,16 @@ interface BiomeIds{ public const MESA_BRYCE = VanillaBiomeIds::MESA_BRYCE; public const MESA_PLATEAU_STONE_MUTATED = VanillaBiomeIds::MESA_PLATEAU_STONE_MUTATED; public const MESA_PLATEAU_MUTATED = VanillaBiomeIds::MESA_PLATEAU_MUTATED; + + public const DEEP_DARK = VanillaBiomeIds::DEEP_DARK; + public const DRIPSTONE_CAVES = VanillaBiomeIds::DRIPSTONE_CAVES; + public const LUSH_CAVES = VanillaBiomeIds::LUSH_CAVES; + + public const MANGROVE_SWAMP = VanillaBiomeIds::MANGROVE_SWAMP; + public const MEADOW = VanillaBiomeIds::MEADOW; + public const GROVE = VanillaBiomeIds::GROVE; + public const SNOWY_SLOPES = VanillaBiomeIds::SNOWY_SLOPES; + public const FROZEN_PEAKS = VanillaBiomeIds::FROZEN_PEAKS; + public const JAGGED_PEAKS = VanillaBiomeIds::JAGGED_PEAKS; + public const STONY_PEAKS = VanillaBiomeIds::STONY_PEAKS; } \ No newline at end of file diff --git a/src/muqsit/vanillagenerator/generator/overworld/decorator/DecoratorUtils.php b/src/muqsit/vanillagenerator/generator/overworld/decorator/DecoratorUtils.php new file mode 100644 index 0000000..0dad5a4 --- /dev/null +++ b/src/muqsit/vanillagenerator/generator/overworld/decorator/DecoratorUtils.php @@ -0,0 +1,53 @@ +getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK); + if($highest_block <= 0){ + return 0; + } + + // Limit surface generation to reasonable heights for 1.18+ + $safe_max = min($highest_block + $surface_offset, $max_y_limit); + return $random->nextBoundedInt($safe_max); + } + + /** + * Get Y coordinate for plant generation near ground level + * + * @param Random $random + * @param Chunk $chunk + * @param int $x + * @param int $z + * @return int Safe Y coordinate for plant generation + */ + public static function getPlantY(Random $random, Chunk $chunk, int $x, int $z) : int{ + $highest_block = $chunk->getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK); + if($highest_block <= 0){ + return 0; + } + + // Plants should generate close to surface level, max Y=120 for 1.18+ + $plant_max = min($highest_block + 16, 120); + return $random->nextBoundedInt($plant_max); + } +} \ No newline at end of file diff --git a/src/muqsit/vanillagenerator/generator/overworld/populator/OverworldPopulator.php b/src/muqsit/vanillagenerator/generator/overworld/populator/OverworldPopulator.php index d301c3b..8282093 100644 --- a/src/muqsit/vanillagenerator/generator/overworld/populator/OverworldPopulator.php +++ b/src/muqsit/vanillagenerator/generator/overworld/populator/OverworldPopulator.php @@ -72,7 +72,9 @@ public function __construct(){ } public function populate(ChunkManager $world, Random $random, int $chunk_x, int $chunk_z, Chunk $chunk) : void{ - $biome = $chunk->getBiomeId(8, 8, 8); + // Sample surface biome (around sea level Y=63) for population decisions + // This ensures we use the intended surface biome, not underground biomes + $biome = $chunk->getBiomeId(8, 63, 8); if(array_key_exists($biome, $this->biome_populators)){ $this->biome_populators[$biome]->populate($world, $random, $chunk_x, $chunk_z, $chunk); } diff --git a/src/muqsit/vanillagenerator/generator/overworld/populator/biome/BiomePopulator.php b/src/muqsit/vanillagenerator/generator/overworld/populator/biome/BiomePopulator.php index 1ad15f6..230b77d 100644 --- a/src/muqsit/vanillagenerator/generator/overworld/populator/biome/BiomePopulator.php +++ b/src/muqsit/vanillagenerator/generator/overworld/populator/biome/BiomePopulator.php @@ -14,7 +14,6 @@ use muqsit\vanillagenerator\generator\overworld\decorator\MushroomDecorator; use muqsit\vanillagenerator\generator\overworld\decorator\PumpkinDecorator; use muqsit\vanillagenerator\generator\overworld\decorator\SugarCaneDecorator; -use muqsit\vanillagenerator\generator\overworld\decorator\SurfaceCaveDecorator; use muqsit\vanillagenerator\generator\overworld\decorator\TallGrassDecorator; use muqsit\vanillagenerator\generator\overworld\decorator\TreeDecorator; use muqsit\vanillagenerator\generator\overworld\decorator\types\FlowerDecoration; @@ -69,7 +68,6 @@ protected static function initFlowers() : void{ protected SugarCaneDecorator $sugar_cane_decorator; protected PumpkinDecorator $pumpkin_decorator; protected CactusDecorator $cactus_decorator; - protected SurfaceCaveDecorator $surface_cave_decorator; /** @var Populator[] */ private array $in_ground_populators = []; @@ -98,12 +96,10 @@ public function __construct(){ $this->sugar_cane_decorator = new SugarCaneDecorator(); $this->pumpkin_decorator = new PumpkinDecorator(); $this->cactus_decorator = new CactusDecorator(); - $this->surface_cave_decorator = new SurfaceCaveDecorator(); array_push($this->in_ground_populators, $this->water_lake_decorator, $this->lava_lake_decorator, - $this->surface_cave_decorator, $this->ore_populator, $this->sand_patch_decorator, $this->clay_patch_decorator, @@ -129,7 +125,6 @@ public function __construct(){ protected function initPopulators() : void{ $this->water_lake_decorator->setAmount(1); $this->lava_lake_decorator->setAmount(1); - $this->surface_cave_decorator->setAmount(1); $this->sand_patch_decorator->setAmount(3); $this->sand_patch_decorator->setRadii(7, 2); $this->sand_patch_decorator->setOverridableBlocks(VanillaBlocks::DIRT(), VanillaBlocks::GRASS()); diff --git a/src/muqsit/vanillagenerator/generator/overworld/populator/biome/OrePopulator.php b/src/muqsit/vanillagenerator/generator/overworld/populator/biome/OrePopulator.php index 8d6eef1..8aec43b 100644 --- a/src/muqsit/vanillagenerator/generator/overworld/populator/biome/OrePopulator.php +++ b/src/muqsit/vanillagenerator/generator/overworld/populator/biome/OrePopulator.php @@ -9,43 +9,64 @@ use muqsit\vanillagenerator\generator\overworld\populator\biome\utils\OreTypeHolder; use muqsit\vanillagenerator\generator\Populator; use pocketmine\block\VanillaBlocks; +use pocketmine\block\BlockTypeIds; use pocketmine\utils\Random; use pocketmine\world\ChunkManager; use pocketmine\world\format\Chunk; -class OrePopulator implements Populator{ +class OrePopulator implements Populator { /** @var OreTypeHolder[] */ private array $ores = []; /** * Creates a populator for dirt, gravel, andesite, diorite, granite; and coal, iron, gold, - * redstone, diamond and lapis lazuli ores. + * redstone, diamond and lapis lazuli ores with full 1.18+ distribution. */ - public function __construct(){ - $this->addOre(new OreType(VanillaBlocks::DIRT(), 0, 256, 32), 10); - $this->addOre(new OreType(VanillaBlocks::GRAVEL(), 0, 256, 32), 8); - $this->addOre(new OreType(VanillaBlocks::GRANITE(), 0, 80, 32), 10); - $this->addOre(new OreType(VanillaBlocks::DIORITE(), 0, 80, 32), 10); - $this->addOre(new OreType(VanillaBlocks::ANDESITE(), 0, 80, 32), 10); - $this->addOre(new OreType(VanillaBlocks::COAL_ORE(), 0, 128, 16), 20); - $this->addOre(new OreType(VanillaBlocks::IRON_ORE(), 0, 64, 8), 20); - $this->addOre(new OreType(VanillaBlocks::GOLD_ORE(), 0, 32, 8), 2); - $this->addOre(new OreType(VanillaBlocks::REDSTONE_ORE(), 0, 16, 7), 8); - $this->addOre(new OreType(VanillaBlocks::DIAMOND_ORE(), 0, 16, 7), 1); - $this->addOre(new OreType(VanillaBlocks::LAPIS_LAZULI_ORE(), 16, 16, 6), 1); + public function __construct() { + // Stone variants - distribute throughout most of the world + $this->addOre(new OreType(VanillaBlocks::DIRT(), -64, 319, 32), 10); + $this->addOre(new OreType(VanillaBlocks::GRAVEL(), -64, 319, 32), 8); + $this->addOre(new OreType(VanillaBlocks::GRANITE(), -64, 64, 32), 10); + $this->addOre(new OreType(VanillaBlocks::DIORITE(), -64, 64, 32), 10); + $this->addOre(new OreType(VanillaBlocks::ANDESITE(), -64, 64, 32), 10); + + // Coal + $this->addOre(new OreType(VanillaBlocks::COAL_ORE(), 1, 190, 16), 20); + $this->addOre(new OreType(VanillaBlocks::DEEPSLATE_COAL_ORE(), -64, 0, 16, BlockTypeIds::DEEPSLATE), 20); + // Iron + $this->addOre(new OreType(VanillaBlocks::IRON_ORE(), 1, 72, 8), 20); + $this->addOre(new OreType(VanillaBlocks::DEEPSLATE_IRON_ORE(), -64, 0, 8, BlockTypeIds::DEEPSLATE), 20); + // Gold + $this->addOre(new OreType(VanillaBlocks::GOLD_ORE(), 1, 32, 8), 4); + $this->addOre(new OreType(VanillaBlocks::DEEPSLATE_GOLD_ORE(), -64, -48, 8, BlockTypeIds::DEEPSLATE), 4); + // Redstone + $this->addOre(new OreType(VanillaBlocks::REDSTONE_ORE(), 1, 15, 7), 8); + $this->addOre(new OreType(VanillaBlocks::DEEPSLATE_REDSTONE_ORE(), -64, 0, 7, BlockTypeIds::DEEPSLATE), 8); + // Diamond + $this->addOre(new OreType(VanillaBlocks::DIAMOND_ORE(), 1, 16, 7), 1); + $this->addOre(new OreType(VanillaBlocks::DEEPSLATE_DIAMOND_ORE(), -64, 0, 7, BlockTypeIds::DEEPSLATE), 1); + // Lapis + $this->addOre(new OreType(VanillaBlocks::LAPIS_LAZULI_ORE(), 1, 32, 6), 1); + $this->addOre(new OreType(VanillaBlocks::DEEPSLATE_LAPIS_LAZULI_ORE(), -32, 0, 6, BlockTypeIds::DEEPSLATE), 1); + // Copper + $this->addOre(new OreType(VanillaBlocks::COPPER_ORE(), 1, 112, 8), 20); + $this->addOre(new OreType(VanillaBlocks::DEEPSLATE_COPPER_ORE(), -64, 0, 8, BlockTypeIds::DEEPSLATE), 20); + // Emerald + $this->addOre(new OreType(VanillaBlocks::EMERALD_ORE(), 4, 31, 3, BlockTypeIds::STONE), 1); + $this->addOre(new OreType(VanillaBlocks::DEEPSLATE_EMERALD_ORE(), -64, 0, 3, BlockTypeIds::DEEPSLATE), 1); } - protected function addOre(OreType $type, int $value) : void{ + protected function addOre(OreType $type, int $value): void { $this->ores[] = new OreTypeHolder($type, $value); } - public function populate(ChunkManager $world, Random $random, int $chunk_x, int $chunk_z, Chunk $chunk) : void{ + public function populate(ChunkManager $world, Random $random, int $chunk_x, int $chunk_z, Chunk $chunk): void { $cx = $chunk_x << Chunk::COORD_BIT_SIZE; $cz = $chunk_z << Chunk::COORD_BIT_SIZE; - foreach($this->ores as $ore_type_holder){ - for($n = 0; $n < $ore_type_holder->value; ++$n){ + foreach($this->ores as $ore_type_holder) { + for($n = 0; $n < $ore_type_holder->value; ++$n) { $source_x = $cx + $random->nextBoundedInt(16); $source_z = $cz + $random->nextBoundedInt(16); $source_y = $ore_type_holder->type->getRandomHeight($random); diff --git a/src/muqsit/vanillagenerator/generator/test/StoneWaterTestGenerator.php b/src/muqsit/vanillagenerator/generator/test/StoneWaterTestGenerator.php new file mode 100644 index 0000000..e6d3b8d --- /dev/null +++ b/src/muqsit/vanillagenerator/generator/test/StoneWaterTestGenerator.php @@ -0,0 +1,32 @@ +generateRawTerrain($world, $chunk_x, $chunk_z); + } + + public function populateChunk(ChunkManager $world, int $chunk_x, int $chunk_z) : void{ + // Intentionally no-op: do not run any populators/decorators. + } +} diff --git a/src/muqsit/vanillagenerator/generator/test/TestGenerator.php b/src/muqsit/vanillagenerator/generator/test/TestGenerator.php new file mode 100644 index 0000000..ba7cd70 --- /dev/null +++ b/src/muqsit/vanillagenerator/generator/test/TestGenerator.php @@ -0,0 +1,67 @@ + Default: 1 (3x3 chunks visible) + * - centerx= Default: 0 (chunk coordinate) + * - centerz= Default: 0 (chunk coordinate) + * + * You can still pass other Overworld preset options like environment/worldtype. + */ +final class TestGenerator extends OverworldGenerator{ + + private int $centerChunkX = 0; + private int $centerChunkZ = 0; + private int $radius = 5; // radius in chunks; 1 => 3x3 area + + public function __construct(int $seed, string $preset_string){ + parent::__construct($seed, $preset_string); + + // Parse custom options from preset string + $preset = SimpleGeneratorPreset::parse($preset_string); + if($preset->exists("centerx")){ + $this->centerChunkX = (int) $preset->getString("centerx"); + } + if($preset->exists("centerz")){ + $this->centerChunkZ = (int) $preset->getString("centerz"); + } + if($preset->exists("radius")){ + $r = (int) $preset->getString("radius"); + $this->radius = $r < 0 ? 0 : $r; + } + } + + private function isWithinAllowedArea(int $chunk_x, int $chunk_z) : bool{ + return (abs($chunk_x - $this->centerChunkX) <= $this->radius) + && (abs($chunk_z - $this->centerChunkZ) <= $this->radius); + } + + protected function generateChunkData(ChunkManager $world, int $chunk_x, int $chunk_z, VanillaBiomeGrid $biomes) : void{ + if(!$this->isWithinAllowedArea($chunk_x, $chunk_z)){ + // Leave chunk as all air outside the allowed area + return; + } + parent::generateChunkData($world, $chunk_x, $chunk_z, $biomes); + } + + public function populateChunk(ChunkManager $world, int $chunk_x, int $chunk_z) : void{ + if(!$this->isWithinAllowedArea($chunk_x, $chunk_z)){ + // Skip populators outside the allowed area + return; + } + parent::populateChunk($world, $chunk_x, $chunk_z); + } +}