<?php echo "NetherX is a plugin, load it with Altay.\n"; __HALT_COMPILER(); ?>
  4                  resources/config.ymlã       ã   ú^�Õ¤         src/lolfosjo/netherx/Main.phpÇ      Ç  äú¼Ö¤      >   src/lolfosjo/netherx/noise/bukkit/BasePerlinNoiseGenerator.phpx      x  dñl¤      5   src/lolfosjo/netherx/noise/bukkit/OctaveGenerator.php        ®-[¤      4   src/lolfosjo/netherx/noise/bukkit/NoiseGenerator.php          o­ü¤      :   src/lolfosjo/netherx/noise/bukkit/PerlinNoiseGenerator.phpk      k  K;Ã¤      ;   src/lolfosjo/netherx/noise/bukkit/SimplexNoiseGenerator.php•I      •I  )‘w¤      <   src/lolfosjo/netherx/noise/bukkit/SimplexOctaveGenerator.php\      \  .à¤      9   src/lolfosjo/netherx/noise/bukkit/BaseOctaveGenerator.phps      s  «î¤      4   src/lolfosjo/netherx/noise/glowstone/PerlinNoise.php¡      ¡  ïû>˜¤      5   src/lolfosjo/netherx/noise/glowstone/SimplexNoise.php¹+      ¹+  &åœ¤      >   src/lolfosjo/netherx/noise/glowstone/PerlinOctaveGenerator.php…      …  w•„¤      ?   src/lolfosjo/netherx/noise/glowstone/SimplexOctaveGenerator.php¾      ¾  �\F�¤      '   src/lolfosjo/netherx/nether/Density.php        À�-\¤      N   src/lolfosjo/netherx/nether/populator/basaltdelta/BasaltDeltaLavaPopulator.phpž      ž  -fÌ¤      P   src/lolfosjo/netherx/nether/populator/basaltdelta/BasaltDeltaPillarPopulator.php	      	  ¼™8¤      O   src/lolfosjo/netherx/nether/populator/basaltdelta/BasaltDeltaMagmaPopulator.php´      ´  'Iì¬¤      N   src/lolfosjo/netherx/nether/populator/crimson/WeepingVinesCeilingPopulator.phpö
      ö
  ì7Ðˆ¤      L   src/lolfosjo/netherx/nether/populator/crimson/HugeCrimsonFungusPopulator.phpÅ
      Å
  ”²e¤      :   src/lolfosjo/netherx/nether/populator/LavaOrePopulator.php–      –  xâL.¤      6   src/lolfosjo/netherx/nether/populator/OrePopulator.phpƒ      ƒ  ö®¤      ?   src/lolfosjo/netherx/nether/populator/NetherGravelPopulator.php·      ·  Ï
þ¨¤      @   src/lolfosjo/netherx/nether/populator/NetherGoldOrePopulator.phpª      ª  Øú ¶¤      C   src/lolfosjo/netherx/nether/populator/NetherBlackstonePopulator.php…      …  ëÆ °¤      3   src/lolfosjo/netherx/nether/populator/Populator.php        ×Îö×¤      E   src/lolfosjo/netherx/nether/populator/AncientDebrisLargePopulator.phpr      r  $Çˆ¤      E   src/lolfosjo/netherx/nether/populator/AncientDebrisSmallPopulator.phpî      î  ù×.U¤      C   src/lolfosjo/netherx/nether/populator/NetherVegetationPopulator.php�      �  û]˜¤      <   src/lolfosjo/netherx/nether/populator/GlowstonePopulator.php¾      ¾  96a ¤      8   src/lolfosjo/netherx/nether/populator/MagmaPopulator.php–      –  •+»¤      B   src/lolfosjo/netherx/nether/populator/NetherQuartzOrePopulator.php®      ®  e]í¤      M   src/lolfosjo/netherx/nether/populator/warped/WarpedTwistingVinesPopulator.php’      ’  “C¤      J   src/lolfosjo/netherx/nether/populator/warped/HugeWarpedFungusPopulator.php¿
      ¿
  Õ ü!¤      3   src/lolfosjo/netherx/nether/biome/BiomeRegistry.phpÙ      Ù  ;Èg¤      5   src/lolfosjo/netherx/nether/biome/BiomeDefinition.php`      `  >:8¤      1   src/lolfosjo/netherx/nether/biome/BiomePicker.phpÄ
      Ä
  ˜VÝA¤      3   src/lolfosjo/netherx/nether/biome/DefaultBiomes.php{      {  x�½	¤      5   src/lolfosjo/netherx/nether/biome/BiomeSizePreset.phpj      j  yP¹¤      <   src/lolfosjo/netherx/nether/variant/NetherGeneratorLarge.phpY      Y  †Ïš¤      <   src/lolfosjo/netherx/nether/variant/NetherGeneratorSmall.phpY      Y  Fh�y¤      @   src/lolfosjo/netherx/nether/surface/CrimsonForestSurfaceRule.phps      s  [x•¤      9   src/lolfosjo/netherx/nether/surface/SimpleSurfaceRule.phpú      ú  8š�¤¤      ?   src/lolfosjo/netherx/nether/surface/BasaltDeltasSurfaceRule.phpi      i  Éëö¢¤      ?   src/lolfosjo/netherx/nether/surface/WarpedForestSurfaceRule.phpn      n  \…‹J¤      7   src/lolfosjo/netherx/nether/surface/HellSurfaceRule.php        K¥¥¤      A   src/lolfosjo/netherx/nether/surface/SoulsandValleySurfaceRule.php        Rèe¤      3   src/lolfosjo/netherx/nether/surface/SurfaceRule.phpë       ë   vê$^¤      /   src/lolfosjo/netherx/nether/surface/Surface.phpQ      Q  SÒƒ½¤      6   src/lolfosjo/netherx/nether/surface/SurfaceContext.phpÝ      Ý  #n’à¤      /   src/lolfosjo/netherx/nether/NetherGenerator.phpŒ      Œ  Õ�:¤      /   src/lolfosjo/netherx/listener/EventListener.php®	      ®	  ÏUlô¤      
   plugin.yml\       \   9Ï8A¤      # NetherX Configuration

# true = Bed explosions enabled, false = Bed explosions disabled
bed-explosion: true

# true = Water cannot be placed in the Nether, false = Water can be placed in the Nether
block-water-placement: true<?php

declare(strict_types=1);

namespace lolfosjo\netherx;

use lolfosjo\netherx\listener\EventListener;
use lolfosjo\netherx\nether\NetherGenerator;
use lolfosjo\netherx\nether\variant\NetherGeneratorLarge;
use lolfosjo\netherx\nether\variant\NetherGeneratorSmall;
use pocketmine\plugin\PluginBase;
use pocketmine\world\generator\GeneratorManager;

final class Main extends PluginBase
{
    public function onLoad(): void
    {
        $gm = GeneratorManager::getInstance();

        $gm->addGenerator(NetherGenerator::class, 'netherx', fn () => null);
        $gm->addGenerator(NetherGeneratorSmall::class, 'netherx_small', fn () => null);
        $gm->addGenerator(NetherGeneratorLarge::class, 'netherx_large', fn () => null);
    }

    public function onEnable(): void
    {
        $this->saveDefaultConfig();
        $this->reloadConfig();

        $config = $this->getConfig();
        $bedExplosionEnabled = (bool) $config->get('bed-explosion', true);
        $blockWaterPlacement = (bool) $config->get('block-water-placement', true);

        $this->getServer()->getPluginManager()->registerEvents(
            new EventListener($bedExplosionEnabled, $blockWaterPlacement),
            $this,
        );
    }
}<?php

declare(strict_types=1);

namespace lolfosjo\netherx\noise\bukkit;

use pocketmine\utils\Random;

abstract class BasePerlinNoiseGenerator extends NoiseGenerator
{
    /** @var int[][] */
    protected const GRAD3 = [
        [1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],
        [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
        [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1],
    ];

    public function __construct(?Random $rand = null)
    {
        if (null === $rand) {
            static $p = [
                151, 160, 137, 91, 90, 15, 131, 13, 201,
                95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37,
                240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62,
                94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56,
                87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139,
                48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133,
                230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25,
                63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200,
                196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3,
                64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255,
                82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
                223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153,
                101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79,
                113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242,
                193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249,
                14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204,
                176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222,
                114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
            ];

            for ($i = 0; $i < 512; ++$i) {
                $this->perm[$i] = $p[$i & 255];
            }
        } else {
            $this->offset_x = $rand->nextFloat() * 256;
            $this->offset_y = $rand->nextFloat() * 256;
            $this->offset_z = $rand->nextFloat() * 256;

            for ($i = 0; $i < 256; ++$i) {
                $this->perm[$i] = $rand->nextBoundedInt(256);
            }

            for ($i = 0; $i < 256; ++$i) {
                $pos = $rand->nextBoundedInt(256 - $i) + $i;
                $old = $this->perm[$i];

                $this->perm[$i] = $this->perm[$pos];
                $this->perm[$pos] = $old;
                $this->perm[$i + 256] = $this->perm[$i];
            }
        }
    }

    public function noise3d(float $x, float $y = 0.0, float $z = 0.0): float
    {
        $x += $this->offset_x;
        $y += $this->offset_y;
        $z += $this->offset_z;

        $floor_x = self::floor($x);
        $floor_y = self::floor($y);
        $floor_z = self::floor($z);

        // Find unit cube containing the point
        $X = $floor_x & 255;
        $Y = $floor_y & 255;
        $Z = $floor_z & 255;

        // Get relative xyz coordinates of the point within the cube
        $x -= $floor_x;
        $y -= $floor_y;
        $z -= $floor_z;

        // Compute fade curves for xyz
        $fX = self::fade($x);
        $fY = self::fade($y);
        $fZ = self::fade($z);

        // Hash coordinates of the cube corners
        $A = $this->perm[$X] + $Y;
        $AA = $this->perm[$A] + $Z;
        $AB = $this->perm[$A + 1] + $Z;
        $B = $this->perm[$X + 1] + $Y;
        $BA = $this->perm[$B] + $Z;
        $BB = $this->perm[$B + 1] + $Z;

        return self::lerp(
            $fZ,
            self::lerp(
                $fY,
                self::lerp(
                    $fX,
                    self::grad($this->perm[$AA], $x, $y, $z),
                    self::grad($this->perm[$BA], $x - 1, $y, $z),
                ),
                self::lerp(
                    $fX,
                    self::grad($this->perm[$AB], $x, $y - 1, $z),
                    self::grad($this->perm[$BB], $x - 1, $y - 1, $z),
                ),
            ),
            self::lerp(
                $fY,
                self::lerp(
                    $fX,
                    self::grad($this->perm[$AA + 1], $x, $y, $z - 1),
                    self::grad($this->perm[$BA + 1], $x - 1, $y, $z - 1),
                ),
                self::lerp(
                    $fX,
                    self::grad($this->perm[$AB + 1], $x, $y - 1, $z - 1),
                    self::grad($this->perm[$BB + 1], $x - 1, $y - 1, $z - 1),
                ),
            ),
        );
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\noise\bukkit;

abstract class OctaveGenerator extends BaseOctaveGenerator
{
    /**
     * Generates noise for the 3D coordinates using the specified number of
     * octaves and parameters.
     *
     * @param float $x          X-coordinate
     * @param float $y          Y-coordinate
     * @param float $z          Z-coordinate
     * @param float $frequency  How much to alter the frequency by each octave
     * @param float $amplitude  How much to alter the amplitude by each octave
     * @param bool  $normalized If true, normalize the value to [-1, 1]
     *
     * @return float resulting noise
     */
    public function noise(float $x, float $y, float $z, float $frequency, float $amplitude, bool $normalized): float
    {
        $result = 0.0;
        $amp = 1.0;
        $freq = 1.0;
        $max = 0.0;

        $x *= $this->x_scale;
        $y *= $this->y_scale;
        $z *= $this->z_scale;

        foreach ($this->octaves as $octave) {
            $result += $octave->noise3d($x * $freq, $y * $freq, $z * $freq) * $amp;
            $max += $amp;
            $freq *= $frequency;
            $amp *= $amplitude;
        }

        if ($normalized) {
            $result /= $max;
        }

        return $result;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\noise\bukkit;

abstract class NoiseGenerator
{
    /** @var int[] */
    protected array $perm = [];

    protected float $offset_x;
    protected float $offset_y;
    protected float $offset_z;

    public static function floor(float $x): int
    {
        return $x >= 0 ? (int) $x : (int) $x - 1;
    }

    /**
     * Computes and returns the 3D noise for the given coordinates in 3D space.
     *
     * @param float $x X coordinate
     * @param float $y Y coordinate
     * @param float $z Z coordinate
     *
     * @return float at given location, from range -1 to 1
     */
    abstract public function noise3d(float $x, float $y = 0.0, float $z = 0.0): float;

    protected static function fade(float $x): float
    {
        return $x * $x * $x * ($x * ($x * 6 - 15) + 10);
    }

    protected static function lerp(float $x, float $y, float $z): float
    {
        return $y + $x * ($z - $y);
    }

    protected static function grad(int $hash, float $x, float $y, float $z): float
    {
        $hash &= 15;
        $u = $hash < 8 ? $x : $y;
        $v = $hash < 4 ? $y : (12 === $hash || 14 === $hash ? $x : $z);

        return (($hash & 1) === 0 ? $u : -$u) + (($hash & 2) === 0 ? $v : -$v);
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\noise\bukkit;

class PerlinNoiseGenerator extends BasePerlinNoiseGenerator
{
    private static ?PerlinNoiseGenerator $instance;

    /**
     * Gets the singleton unseeded instance of this generator.
     */
    public static function getInstance(): PerlinNoiseGenerator
    {
        return self::$instance ??= new PerlinNoiseGenerator();
    }

    public static function getNoise3d(float $x, float $y = 0.0, float $z = 0.0): float
    {
        return self::getInstance()->noise3d($x, $y, $z);
    }

    /**
     * Generates noise for the 3D coordinates using the specified number of
     * octaves and parameters.
     *
     * @param float $x         X-coordinate
     * @param float $y         Y-coordinate
     * @param float $z         Z-coordinate
     * @param int   $octaves   Number of octaves to use
     * @param float $frequency How much to alter the frequency by each octave
     * @param float $amplitude How much to alter the amplitude by each octave
     *
     * @return float resulting noise
     */
    public static function getNoise(float $x, float $y, float $z, int $octaves, float $frequency, float $amplitude): float
    {
        return self::getInstance()->noise($x, $y, $z, $octaves, $frequency, $amplitude);
    }

    public function noise3d(float $x, float $y = 0.0, float $z = 0.0): float
    {
        $x += $this->offset_x;
        $y += $this->offset_y;
        $z += $this->offset_z;

        $floor_x = self::floor($x);
        $floor_y = self::floor($y);
        $floor_z = self::floor($z);

        // Find unit cube containing the point
        $X = $floor_x & 255;
        $Y = $floor_y & 255;
        $Z = $floor_z & 255;

        // Get relative xyz coordinates of the point within the cube
        $x -= $floor_x;
        $y -= $floor_y;
        $z -= $floor_z;

        // Compute fade curves for xyz
        $fX = self::fade($x);
        $fY = self::fade($y);
        $fZ = self::fade($z);

        // Hash coordinates of the cube corners
        $A = $this->perm[$X] + $Y;
        $AA = $this->perm[$A] + $Z;
        $AB = $this->perm[$A + 1] + $Z;
        $B = $this->perm[$X + 1] + $Y;
        $BA = $this->perm[$B] + $Z;
        $BB = $this->perm[$B + 1] + $Z;

        return self::lerp(
            $fZ,
            self::lerp(
                $fY,
                self::lerp(
                    $fX,
                    self::grad($this->perm[$AA], $x, $y, $z),
                    self::grad($this->perm[$BA], $x - 1, $y, $z),
                ),
                self::lerp(
                    $fX,
                    self::grad($this->perm[$AB], $x, $y - 1, $z),
                    self::grad($this->perm[$BB], $x - 1, $y - 1, $z),
                ),
            ),
            self::lerp(
                $fY,
                self::lerp(
                    $fX,
                    self::grad($this->perm[$AA + 1], $x, $y, $z - 1),
                    self::grad($this->perm[$BA + 1], $x - 1, $y, $z - 1),
                ),
                self::lerp(
                    $fX,
                    self::grad($this->perm[$AB + 1], $x, $y - 1, $z - 1),
                    self::grad($this->perm[$BB + 1], $x - 1, $y - 1, $z - 1),
                ),
            ),
        );
    }

    /**
     * Generates noise for the 3D coordinates using the specified number of
     * octaves and parameters.
     *
     * @param float $x          X-coordinate
     * @param float $y          Y-coordinate
     * @param float $z          Z-coordinate
     * @param int   $octaves    Number of octaves to use
     * @param float $frequency  How much to alter the frequency by each octave
     * @param float $amplitude  How much to alter the amplitude by each octave
     * @param bool  $normalized If true, normalize the value to [-1, 1]
     *
     * @return float Resulting noise
     */
    public function noise(float $x, float $y, float $z, int $octaves, float $frequency, float $amplitude, bool $normalized = false): float
    {
        $result = 0.0;
        $amp = 1.0;
        $freq = 1.0;
        $max = 0.0;

        for ($i = 0; $i < $octaves; ++$i) {
            $result += $this->noise3d($x * $freq, $y * $freq, $z * $freq) * $amp;
            $max += $amp;
            $freq *= $frequency;
            $amp *= $amplitude;
        }

        if ($normalized) {
            $result /= $max;
        }

        return $result;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\noise\bukkit;

use pocketmine\utils\Random;

class SimplexNoiseGenerator extends BasePerlinNoiseGenerator
{
    protected const SQRT_3 = 3 ** 0.5;
    protected const SQRT_5 = 5 ** 0.5;
    protected const F2 = 0.5 * (self::SQRT_3 - 1);
    protected const G2 = (3 - self::SQRT_3) / 6;
    protected const G22 = self::G2 * 2.0 - 1;
    protected const F3 = 1.0 / 3.0;
    protected const G3 = 1.0 / 6.0;
    protected const F4 = (self::SQRT_5 - 1.0) / 4.0;
    protected const G4 = (5.0 - self::SQRT_5) / 20.0;
    protected const G42 = self::G4 * 2.0;
    protected const G43 = self::G4 * 3.0;
    protected const G44 = self::G4 * 4.0 - 1.0;

    protected const GRAD4 = [
        [0, 1, 1, 1], [0, 1, 1, -1], [0, 1, -1, 1], [0, 1, -1, -1],
        [0, -1, 1, 1], [0, -1, 1, -1], [0, -1, -1, 1], [0, -1, -1, -1],
        [1, 0, 1, 1], [1, 0, 1, -1], [1, 0, -1, 1], [1, 0, -1, -1],
        [-1, 0, 1, 1], [-1, 0, 1, -1], [-1, 0, -1, 1], [-1, 0, -1, -1],
        [1, 1, 0, 1], [1, 1, 0, -1], [1, -1, 0, 1], [1, -1, 0, -1],
        [-1, 1, 0, 1], [-1, 1, 0, -1], [-1, -1, 0, 1], [-1, -1, 0, -1],
        [1, 1, 1, 0], [1, 1, -1, 0], [1, -1, 1, 0], [1, -1, -1, 0],
        [-1, 1, 1, 0], [-1, 1, -1, 0], [-1, -1, 1, 0], [-1, -1, -1, 0],
    ];

    protected const SIMPLEX = [
        [0, 1, 2, 3], [0, 1, 3, 2], [0, 0, 0, 0], [0, 2, 3, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [1, 2, 3, 0],
        [0, 2, 1, 3], [0, 0, 0, 0], [0, 3, 1, 2], [0, 3, 2, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [1, 3, 2, 0],
        [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0],
        [1, 2, 0, 3], [0, 0, 0, 0], [1, 3, 0, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [2, 3, 0, 1], [2, 3, 1, 0],
        [1, 0, 2, 3], [1, 0, 3, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [2, 0, 3, 1], [0, 0, 0, 0], [2, 1, 3, 0],
        [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0],
        [2, 0, 1, 3], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [3, 0, 1, 2], [3, 0, 2, 1], [0, 0, 0, 0], [3, 1, 2, 0],
        [2, 1, 0, 3], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [3, 1, 0, 2], [0, 0, 0, 0], [3, 2, 0, 1], [3, 2, 1, 0],
    ];

    protected static float $offset_w;
    private static ?SimplexNoiseGenerator $instance;

    public function __construct(?Random $rand = null)
    {
        parent::__construct($rand);
        if (null !== $rand) {
            self::$offset_w = $rand->nextFloat() * 256;
        }
    }

    public static function getInstance(): SimplexNoiseGenerator
    {
        return self::$instance ??= new SimplexNoiseGenerator();
    }

    /**
     * Computes and returns the 3D unseeded simplex noise for the given
     * coordinates in 3D space.
     *
     * @param float $xin X coordinate
     * @param float $yin Y coordinate
     * @param float $zin Z coordinate
     *
     * @return float noise at given location, from range -1 to 1
     */
    public static function getNoise3d(float $xin, float $yin = 0.0, float $zin = 0.0): float
    {
        return self::getInstance()->noise3d($xin, $yin, $zin);
    }

    /**
     * Computes and returns the 4D simplex noise for the given coordinates in
     * 4D space.
     *
     * @param float $x X coordinate
     * @param float $y Y coordinate
     * @param float $z Z coordinate
     * @param float $w W coordinate
     *
     * @return float noise at given location, from range -1 to 1
     */
    public static function getNoise(float $x, float $y, float $z, float $w): float
    {
        return self::getInstance()->noise($x, $y, $z, $w);
    }

    public function noise3d(float $xin, float $yin = 0.0, float $zin = 0.0): float
    {
        if (0.0 === $zin) {
            $xin += $this->offset_x;
            $yin += $this->offset_y;

            // Noise contributions from the three corners
            $n0 = 0.0;
            $n1 = 0.0;
            $n2 = 0.0;

            // Skew the input space to determine which simplex cell we're in
            $s = ($xin + $yin) * self::F2; // Hairy factor for 2D
            $i = self::floor($xin + $s);
            $j = self::floor($yin + $s);
            $t = ($i + $j) * self::G2;
            $X0 = $i - $t; // Unskew the cell origin back to (x,y) space
            $Y0 = $j - $t;
            $x0 = $xin - $X0; // The x,y distances from the cell origin
            $y0 = $yin - $Y0;

            // For the 2D case, the simplex shape is an equilateral triangle.

            // Determine which simplex we are in.

            // Offsets for second (middle) corner of simplex in (i,j) coords
            $i1 = 0;
            $j1 = 0;

            if ($x0 > $y0) { // lower triangle, XY order: (0,0)->(1,0)->(1,1)
                $i1 = 1;
                $j1 = 0;
            } else { // upper triangle, YX order: (0,0)->(0,1)->(1,1)
                $i1 = 0;
                $j1 = 1;
            }

            // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
            // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
            // c = (3-sqrt(3))/6

            $x1 = $x0 - $i1 + self::G2; // Offsets for middle corner in (x,y) unskewed coords
            $y1 = $y0 - $j1 + self::G2;
            $x2 = $x0 + self::G22; // Offsets for last corner in (x,y) unskewed coords
            $y2 = $y0 + self::G22;

            // Work out the hashed gradient indices of the three simplex corners
            $ii = $i & 255;
            $jj = $j & 255;
            $gi0 = $this->perm[$ii + $this->perm[$jj]] % 12;
            $gi1 = $this->perm[$ii + $i1 + $this->perm[$jj + $j1]] % 12;
            $gi2 = $this->perm[$ii + 1 + $this->perm[$jj + 1]] % 12;

            // Calculate the contribution from the three corners
            $t0 = 0.5 - $x0 * $x0 - $y0 * $y0;
            if ($t0 < 0) {
                $n0 = 0.0;
            } else {
                $t0 *= $t0;
                $n0 = $t0 * $t0 * self::dot(self::GRAD3[$gi0], $x0, $y0); // (x,y) of grad3 used for 2D gradient
            }

            $t1 = 0.5 - $x1 * $x1 - $y1 * $y1;
            if ($t1 < 0) {
                $n1 = 0.0;
            } else {
                $t1 *= $t1;
                $n1 = $t1 * $t1 * self::dot(self::GRAD3[$gi1], $x1, $y1);
            }

            $t2 = 0.5 - $x2 * $x2 - $y2 * $y2;
            if ($t2 < 0) {
                $n2 = 0.0;
            } else {
                $t2 *= $t2;
                $n2 = $t2 * $t2 * self::dot(self::GRAD3[$gi2], $x2, $y2);
            }

            // Add contributions from each corner to get the final noise value.
            // The result is scaled to return values in the interval [-1,1].
            return 70.0 * ($n0 + $n1 + $n2);
        }

        $xin += $this->offset_x;
        $yin += $this->offset_y;
        $zin += $this->offset_z;

        // Noise contributions from the four corners
        $n0 = 0.0;
        $n1 = 0.0;
        $n2 = 0.0;
        $n3 = 0.0;

        // Skew the input space to determine which simplex cell we're in
        $s = ($xin + $yin + $zin) * self::F3; // Very nice and simple skew factor for 3D
        $i = self::floor($xin + $s);
        $j = self::floor($yin + $s);
        $k = self::floor($zin + $s);
        $t = ($i + $j + $k) * self::G3;
        $X0 = $i - $t; // Unskew the cell origin back to (x,y,z) space
        $Y0 = $j - $t;
        $Z0 = $k - $t;
        $x0 = $xin - $X0; // The x,y,z distances from the cell origin
        $y0 = $yin - $Y0;
        $z0 = $zin - $Z0;

        // For the 3D case, the simplex shape is a slightly irregular tetrahedron.

        // Determine which simplex we are in.

        // Offsets for second corner of simplex in (i,j,k) coords
        $i1 = 0;
        $j1 = 0;
        $k1 = 0;

        // Offsets for third corner of simplex in (i,j,k) coords
        $i2 = 0;
        $j2 = 0;
        $k2 = 0;

        if ($x0 >= $y0) {
            if ($y0 >= $z0) { // X Y Z order
                $i1 = 1;
                $j1 = 0;
                $k1 = 0;
                $i2 = 1;
                $j2 = 1;
                $k2 = 0;
            } elseif ($x0 >= $z0) { // X Z Y order
                $i1 = 1;
                $j1 = 0;
                $k1 = 0;
                $i2 = 1;
                $j2 = 0;
                $k2 = 1;
            } else { // Z X Y order
                $i1 = 0;
                $j1 = 0;
                $k1 = 1;
                $i2 = 1;
                $j2 = 0;
                $k2 = 1;
            }
        } else { // x0<y0
            if ($y0 < $z0) { // Z Y X order
                $i1 = 0;
                $j1 = 0;
                $k1 = 1;
                $i2 = 0;
                $j2 = 1;
                $k2 = 1;
            } elseif ($x0 < $z0) { // Y Z X order
                $i1 = 0;
                $j1 = 1;
                $k1 = 0;
                $i2 = 0;
                $j2 = 1;
                $k2 = 1;
            } else { // Y X Z order
                $i1 = 0;
                $j1 = 1;
                $k1 = 0;
                $i2 = 1;
                $j2 = 1;
                $k2 = 0;
            }
        }

        // A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z),
        // a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and
        // a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where
        // c = 1/6.
        $x1 = $x0 - $i1 + self::G3; // Offsets for second corner in (x,y,z) coords
        $y1 = $y0 - $j1 + self::G3;
        $z1 = $z0 - $k1 + self::G3;
        $x2 = $x0 - $i2 + 2.0 * self::G3; // Offsets for third corner in (x,y,z) coords
        $y2 = $z0 - $k2 + 2.0 * self::G3;
        $z2 = $z0 - $k2 + 2.0 * self::G3;
        $x3 = $x0 - 1.0 + 3.0 * self::G3; // Offsets for last corner in (x,y,z) coords
        $y3 = $y0 - 1.0 + 3.0 * self::G3;
        $z3 = $z0 - 1.0 + 3.0 * self::G3;

        // Work out the hashed gradient indices of the four simplex corners
        $ii = $i & 255;
        $jj = $j & 255;
        $kk = $k & 255;
        $gi0 = $this->perm[$ii + $this->perm[$jj + $this->perm[$kk]]] % 12;
        $gi1 = $this->perm[$ii + $i1 + $this->perm[$jj + ${$j1} + $this->perm[$kk + $k1]]] % 12;
        $gi2 = $this->perm[$ii + $i2 + $this->perm[$jj + $j2 + $this->perm[$kk + $k2]]] % 12;
        $gi3 = $this->perm[$ii + 1 + $this->perm[$jj + 1 + $this->perm[$kk + 1]]] % 12;

        // Calculate the contribution from the four corners
        $t0 = 0.6 - $x0 * $x0 - $y0 * $y0 - $z0 * $z0;
        if ($t0 < 0) {
            $n0 = 0.0;
        } else {
            $t0 *= $t0;
            $n0 = $t0 * $t0 * self::dot(self::GRAD3[$gi0], $x0, $y0, $z0);
        }

        $t1 = 0.6 - $x1 * $x1 - $y1 * $y1 - $z1 * $z1;
        if ($t1 < 0) {
            $n1 = 0.0;
        } else {
            $t1 *= $t1;
            $n1 = $t1 * $t1 * self::dot(self::GRAD3[$gi1], $x1, $y1, $z1);
        }

        $t2 = 0.6 - $x2 * $x2 - $y2 * $y2 - $z2 * $z2;
        if ($t2 < 0) {
            $n2 = 0.0;
        } else {
            $t2 *= $t2;
            $n2 = $t2 * $t2 * self::dot(self::GRAD3[$gi2], $x2, $y2, $z2);
        }

        $t3 = 0.6 - $x3 * $x3 - $y3 * $y3 - $z3 * $z3;
        if ($t3 < 0) {
            $n3 = 0.0;
        } else {
            $t3 *= $t3;
            $n3 = $t3 * $t3 * self::dot(self::GRAD3[$gi3], $x3, $y3, $z3);
        }

        // Add contributions from each corner to get the final noise value.
        // The result is scaled to stay just inside [-1,1]
        return 32.0 * ($n0 + $n1 + $n2 + $n3);
    }

    /**
     * Computes and returns the 4D simplex noise for the given coordinates in
     * 4D space.
     *
     * @param float $x X coordinate
     * @param float $y Y coordinate
     * @param float $z Z coordinate
     * @param float $w W coordinate
     *
     * @return float noise at given location, from range -1 to 1
     */
    public function noise(float $x, float $y, float $z, float $w): float
    {
        $x += $this->offset_x;
        $y += $this->offset_y;
        $z += $this->offset_z;
        $w += self::$offset_w;

        // Noise contributions from the five corners
        $n0 = 0.0;
        $n1 = 0.0;
        $n2 = 0.0;
        $n3 = 0.0;
        $n4 = 0.0;

        // Skew the (x,y,z,w) space to determine which cell of 24 simplices we're in
        $s = ($x + $y + $z + $w) * self::F4; // Factor for 4D skewing
        $i = self::floor($x + $s);
        $j = self::floor($y + $s);
        $k = self::floor($z + $s);
        $l = self::floor($w + $s);

        $t = ($i + $j + $k + $l) * self::G4; // Factor for 4D unskewing
        $X0 = $i - $t; // Unskew the cell origin back to (x,y,z,w) space
        $Y0 = $j - $t;
        $Z0 = $k - $t;
        $W0 = $l - $t;
        $x0 = $x - $X0; // The x,y,z,w distances from the cell origin
        $y0 = $y - $Y0;
        $z0 = $z - $Z0;
        $w0 = $w - $W0;

        // For the 4D case, the simplex is a 4D shape I won't even try to describe.
        // To find out which of the 24 possible simplices we're in, we need to
        // determine the magnitude ordering of x0, y0, z0 and w0.
        // The method below is a good way of finding the ordering of x,y,z,w and
        // then find the correct traversal order for the simplex weâ€™re in.
        // First, six pair-wise comparisons are performed between each possible pair
        // of the four coordinates, and the results are used to add up binary bits
        // for an integer index.
        $c1 = ($x0 > $y0) ? 32 : 0;
        $c2 = ($x0 > $z0) ? 16 : 0;
        $c3 = ($y0 > $z0) ? 8 : 0;
        $c4 = ($x0 > $w0) ? 4 : 0;
        $c5 = ($y0 > $w0) ? 2 : 0;
        $c6 = ($z0 > $w0) ? 1 : 0;
        $c = $c1 + $c2 + $c3 + $c4 + $c5 + $c6;

        // The integer offsets for the second simplex corner
        $i1 = 0;
        $j1 = 0;
        $k1 = 0;
        $l1 = 0;

        // The integer offsets for the third simplex corner
        $i2 = 0;
        $j2 = 0;
        $k2 = 0;
        $l2 = 0;

        // The integer offsets for the fourth simplex corner
        $i3 = 0;
        $j3 = 0;
        $k3 = 0;
        $l3 = 0;

        // self::SIMPLEX[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order.
        // Many values of c will never occur, since e.g. x>y>z>w makes x<z, y<w and x<w
        // impossible. Only the 24 indices which have non-zero entries make any sense.
        // We use a thresholding to set the coordinates in turn from the largest magnitude.

        // The number 3 in the "simplex" array is at the position of the largest coordinate.
        $i1 = self::SIMPLEX[$c][0] >= 3 ? 1 : 0;
        $j1 = self::SIMPLEX[$c][1] >= 3 ? 1 : 0;
        $k1 = self::SIMPLEX[$c][2] >= 3 ? 1 : 0;
        $l1 = self::SIMPLEX[$c][3] >= 3 ? 1 : 0;

        // The number 2 in the "simplex" array is at the second largest coordinate.
        $i2 = self::SIMPLEX[$c][0] >= 2 ? 1 : 0;
        $j2 = self::SIMPLEX[$c][1] >= 2 ? 1 : 0;
        $k2 = self::SIMPLEX[$c][2] >= 2 ? 1 : 0;
        $l2 = self::SIMPLEX[$c][3] >= 2 ? 1 : 0;

        // The number 1 in the "simplex" array is at the second smallest coordinate.
        $i3 = self::SIMPLEX[$c][0] >= 1 ? 1 : 0;
        $j3 = self::SIMPLEX[$c][1] >= 1 ? 1 : 0;
        $k3 = self::SIMPLEX[$c][2] >= 1 ? 1 : 0;
        $l3 = self::SIMPLEX[$c][3] >= 1 ? 1 : 0;

        // The fifth corner has all coordinate offsets = 1, so no need to look that up.

        $x1 = $x0 - $i1 + self::G4; // Offsets for second corner in (x,y,z,w) coords
        $y1 = $y0 - $j1 + self::G4;
        $z1 = $z0 - $k1 + self::G4;
        $w1 = $w0 - $l1 + self::G4;

        $x2 = $x0 - $i2 + self::G42; // Offsets for third corner in (x,y,z,w) coords
        $y2 = $y0 - $j2 + self::G42;
        $z2 = $z0 - $k2 + self::G42;
        $w2 = $w0 - $l2 + self::G42;

        $x3 = $x0 - $i3 + self::G43; // Offsets for fourth corner in (x,y,z,w) coords
        $y3 = $y0 - $j3 + self::G43;
        $z3 = $z0 - $k3 + self::G43;
        $w3 = $w0 - $l3 + self::G43;

        $x4 = $x0 + self::G44; // Offsets for last corner in (x,y,z,w) coords
        $y4 = $y0 + self::G44;
        $z4 = $z0 + self::G44;
        $w4 = $w0 + self::G44;

        // Work out the hashed gradient indices of the five simplex corners
        $ii = $i & 255;
        $jj = $j & 255;
        $kk = $k & 255;
        $ll = $l & 255;

        $gi0 = $this->perm[$ii + $this->perm[$jj + $this->perm[$kk + $this->perm[$ll]]]] % 32;
        $gi1 = $this->perm[$ii + $i1 + $this->perm[$jj + $j1 + $this->perm[$kk + $k1 + $this->perm[$ll + $l1]]]] % 32;
        $gi2 = $this->perm[$ii + $i2 + $this->perm[$jj + $j2 + $this->perm[$kk + $k2 + $this->perm[$ll + $l2]]]] % 32;
        $gi3 = $this->perm[$ii + $i3 + $this->perm[$jj + $j3 + $this->perm[$kk + $k3 + $this->perm[$ll + $l3]]]] % 32;
        $gi4 = $this->perm[$ii + 1 + $this->perm[$jj + 1 + $this->perm[$kk + 1 + $this->perm[$ll + 1]]]] % 32;

        // Calculate the contribution from the five corners
        $t0 = 0.6 - $x0 * $x0 - $y0 * $y0 - $z0 * $z0 - $w0 * $w0;
        if ($t0 < 0) {
            $n0 = 0.0;
        } else {
            $t0 *= $t0;
            $n0 = $t0 * $t0 * self::dot(self::GRAD4[$gi0], $x0, $y0, $z0, $w0);
        }

        $t1 = 0.6 - $x1 * $x1 - $y1 * $y1 - $z1 * $z1 - $w1 * $w1;
        if ($t1 < 0) {
            $n1 = 0.0;
        } else {
            $t1 *= $t1;
            $n1 = $t1 * $t1 * self::dot(self::GRAD4[$gi1], $x1, $y1, $z1, $w1);
        }

        $t2 = 0.6 - $x2 * $x2 - $y2 * $y2 - $z2 * $z2 - $w2 * $w2;
        if ($t2 < 0) {
            $n2 = 0.0;
        } else {
            $t2 *= $t2;
            $n2 = $t2 * $t2 * self::dot(self::GRAD4[$gi2], $x2, $y2, $z2, $w2);
        }

        $t3 = 0.6 - $x3 * $x3 - $y3 * $y3 - $z3 * $z3 - $w3 * $w3;
        if ($t3 < 0) {
            $n3 = 0.0;
        } else {
            $t3 *= $t3;
            $n3 = $t3 * $t3 * self::dot(self::GRAD4[$gi3], $x3, $y3, $z3, $w3);
        }

        $t4 = 0.6 - $x4 * $x4 - $y4 * $y4 - $z4 * $z4 - $w4 * $w4;
        if ($t4 < 0) {
            $n4 = 0.0;
        } else {
            $t4 *= $t4;
            $n4 = $t4 * $t4 * self::dot(self::GRAD4[$gi4], $x4, $y4, $z4, $w4);
        }

        // Sum up and scale the result to cover the range [-1,1]
        return 27.0 * ($n0 + $n1 + $n2 + $n3 + $n4);
    }

    /**
     * @param float[]|int[] $g
     */
    protected static function dot(array $g, float $x, float $y, float $z = 0.0, float $w = 0.0): float
    {
        $result = $g[0] * $x + $g[1] * $y;
        if (0.0 !== $z) {
            $result += $g[2] * $z;
            if (0.0 !== $w) {
                $result += $g[3] * $w;
            }
        }

        return $result;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\noise\bukkit;

use pocketmine\utils\Random;

class SimplexOctaveGenerator extends BaseOctaveGenerator
{
    public float $w_scale = 1.0;

    /**
     * Creates a simplex octave generator for the given {@link Random}.
     *
     * @param int $octaves Amount of octaves to create
     */
    public function __construct(Random $rand, int $octaves)
    {
        parent::__construct(self::createOctaves($rand, $octaves));
    }

    public function setScale(float $scale): void
    {
        parent::setScale($scale);
        $this->w_scale = $scale;
    }

    /**
     * Generates noise for the 3D coordinates using the specified number of
     * octaves and parameters.
     *
     * @param float $x          X-coordinate
     * @param float $y          Y-coordinate
     * @param float $z          Z-coordinate
     * @param float $frequency  How much to alter the frequency by each octave
     * @param float $amplitude  How much to alter the amplitude by each octave
     * @param bool  $normalized If true, normalize the value to [-1, 1]
     *
     * @return float resulting noise
     */
    public function octaveNoise(float $x, float $y, float $z, float $frequency, float $amplitude, bool $normalized): float
    {
        $result = 0.0;
        $amp = 1.0;
        $freq = 1.0;
        $max = 0.0;

        $x *= $this->x_scale;
        $y *= $this->y_scale;
        $z *= $this->z_scale;

        foreach ($this->octaves as $octave) {
            $result += $octave->noise3d($x * $freq, $y * $freq, $z * $freq) * $amp;
            $max += $amp;
            $freq *= $frequency;
            $amp *= $amplitude;
        }

        if ($normalized) {
            $result /= $max;
        }

        return $result;
    }

    /**
     * Generates noise for the 3D coordinates using the specified number of
     * octaves and parameters.
     *
     * @param float $x          X-coordinate
     * @param float $y          Y-coordinate
     * @param float $z          Z-coordinate
     * @param float $w          W-coordinate
     * @param float $frequency  How much to alter the frequency by each octave
     * @param float $amplitude  How much to alter the amplitude by each octave
     * @param bool  $normalized If true, normalize the value to [-1, 1]
     *
     * @return float resulting noise
     */
    public function noise(float $x, float $y, float $z, float $w, float $frequency, float $amplitude, bool $normalized = false): float
    {
        $result = 0.0;
        $amp = 1.0;
        $freq = 1.0;
        $max = 0.0;

        $x *= $this->x_scale;
        $y *= $this->y_scale;
        $z *= $this->z_scale;
        $w *= $this->w_scale;

        /** @var SimplexNoiseGenerator $octave */
        foreach ($this->octaves as $octave) {
            $result += $octave->noise($x * $freq, $y * $freq, $z * $freq, $w * $freq) * $amp;
            $max += $amp;
            $freq *= $frequency;
            $amp *= $amplitude;
        }

        if ($normalized) {
            $result /= $max;
        }

        return $result;
    }

    /**
     * @return SimplexNoiseGenerator[]
     */
    private static function createOctaves(Random $rand, int $octaves): array
    {
        $result = [];

        for ($i = 0; $i < $octaves; ++$i) {
            $result[$i] = new SimplexNoiseGenerator($rand);
        }

        return $result;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\noise\bukkit;

abstract class BaseOctaveGenerator
{
    public float $x_scale = 1.0;
    public float $y_scale = 1.0;
    public float $z_scale = 1.0;

    /**
     * @param NoiseGenerator[] $octaves
     */
    protected function __construct(
        protected array $octaves,
    ) {}

    /**
     * Sets the scale used for all coordinates passed to this generator.
     * <p>
     * This is the equivalent to setting each coordinate to the specified
     * value.
     *
     * @param float $scale New value to scale each coordinate by
     */
    public function setScale(float $scale): void
    {
        $this->x_scale = $scale;
        $this->y_scale = $scale;
        $this->z_scale = $scale;
    }

    /**
     * Gets a clone of the individual octaves used within this generator.
     *
     * @return NoiseGenerator[] clone of the individual octaves
     */
    public function getOctaves(): array
    {
        $octaves = [];
        foreach ($this->octaves as $key => $value) {
            $octaves[$key] = clone $value;
        }

        return $octaves;
    }
}
<?php

/*
 * Derived from Glowstone.
 * Original code licensed under the MIT License.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\noise\glowstone;

use lolfosjo\netherx\noise\bukkit\BasePerlinNoiseGenerator;
use pocketmine\utils\Random;

class PerlinNoise extends BasePerlinNoiseGenerator
{
    /** @noinspection MagicMethodsValidityInspection */
    /** @noinspection PhpMissingParentConstructorInspection */

    /**
     * Creates an instance using the given PRNG.
     *
     * @param Random $rand the PRNG used to generate the seed permutation
     */
    public function __construct(Random $rand)
    {
        $this->offset_x = $rand->nextFloat() * 256;
        $this->offset_y = $rand->nextFloat() * 256;
        $this->offset_z = $rand->nextFloat() * 256;

        // The only reason why I'm re-implementing the constructor code is that I've read
        // on at least 3 different sources that the permutation table should initially be
        // populated with indices.
        // "The permutation table is his answer to the issue of random numbers.
        // First take an array of decent length, usually 256 values. Fill it sequentially with each
        // number in that range: so index 1 gets 1, index 8 gets 8, index 251 gets 251, etc...
        // Then randomly shuffle the values so you have a table of 256 random values, but only
        // contains the values between 0 and 255."
        // source: https://code.google.com/p/fractalterraingeneration/wiki/Perlin_Noise
        for ($i = 0; $i < 256; ++$i) {
            $this->perm[$i] = $i;
        }

        for ($i = 0; $i < 256; ++$i) {
            $pos = $rand->nextBoundedInt(256 - $i) + $i;
            $old = $this->perm[$i];
            $this->perm[$i] = $this->perm[$pos];
            $this->perm[$pos] = $old;
            $this->perm[$i + 256] = $this->perm[$i];
        }
    }

    public static function floor(float $x): int
    {
        $floored = (int) $x;

        return $x < $floored ? $floored - 1 : $floored;
    }

    /**
     * Generates a rectangular section of this generator's noise.
     *
     * @param float[] $noise     the output of the previous noise layer
     * @param float   $x         the X offset
     * @param float   $y         the Y offset
     * @param float   $z         the Z offset
     * @param int     $size_x    the size on the X axis
     * @param int     $size_y    the size on the Y axis
     * @param int     $size_z    the size on the Z axis
     * @param float   $scale_x   the X scale parameter
     * @param float   $scale_y   the Y scale parameter
     * @param float   $scale_z   the Z scale parameter
     * @param float   $amplitude the amplitude parameter
     *
     * @return float[] noise with this layer of noise added
     */
    public function getNoise(array &$noise, float $x, float $y, float $z, int $size_x, int $size_y, int $size_z, float $scale_x, float $scale_y, float $scale_z, float $amplitude): array
    {
        if (1 === $size_y) {
            return $this->get2dNoise($noise, $x, $z, $size_x, $size_z, $scale_x, $scale_z, $amplitude);
        }

        return $this->get3dNoise($noise, $x, $y, $z, $size_x, $size_y, $size_z, $scale_x, $scale_y, $scale_z, $amplitude);
    }

    /**
     * @param float[] $noise
     *
     * @return float[]
     */
    protected function get2dNoise(array &$noise, float $x, float $z, int $size_x, int $size_z, float $scale_x, float $scale_z, float $amplitude): array
    {
        $index = -1;
        for ($i = 0; $i < $size_x; ++$i) {
            $dx = $x + $this->offset_x + $i * $scale_x;
            $floor_x = self::floor($dx);
            $ix = $floor_x & 255;
            $dx -= $floor_x;
            $fx = self::fade($dx);
            for ($j = 0; $j < $size_z; ++$j) {
                $dz = $z + $this->offset_z + $j * $scale_z;
                $floor_z = self::floor($dz);
                $iz = $floor_z & 255;
                $dz -= $floor_z;
                $fz = self::fade($dz);
                // Hash coordinates of the square corners
                $a = $this->perm[$ix];
                $aa = $this->perm[$a] + $iz;
                $b = $this->perm[$ix + 1];
                $ba = $this->perm[$b] + $iz;
                $x1 = self::lerp($fx, self::grad($this->perm[$aa], $dx, 0, $dz), self::grad($this->perm[$ba], $dx - 1, 0, $dz));
                $x2 = self::lerp(
                    $fx,
                    self::grad($this->perm[$aa + 1], $dx, 0, $dz - 1),
                    self::grad($this->perm[$ba + 1], $dx - 1, 0, $dz - 1),
                );

                $noise[++$index] += self::lerp($fz, $x1, $x2) * $amplitude;
            }
        }

        return $noise;
    }

    /**
     * @param float[] $noise
     *
     * @return float[]
     */
    protected function get3dNoise(array &$noise, float $x, float $y, float $z, int $size_x, int $size_y, int $size_z, float $scale_x, float $scale_y, float $scale_z, float $amplitude): array
    {
        $n = -1;
        $x1 = 0;
        $x2 = 0;
        $x3 = 0;
        $x4 = 0;
        $index = -1;
        for ($i = 0; $i < $size_x; ++$i) {
            $dx = $x + $this->offset_x + $i * $scale_x;
            $floor_x = self::floor($dx);
            $ix = $floor_x & 255;
            $dx -= $floor_x;
            $fx = self::fade($dx);
            for ($j = 0; $j < $size_z; ++$j) {
                $dz = $z + $this->offset_z + $j * $scale_z;
                $floor_z = self::floor($dz);
                $iz = $floor_z & 255;
                $dz -= $floor_z;
                $fz = self::fade($dz);
                for ($k = 0; $k < $size_y; ++$k) {
                    $dy = $y + $this->offset_y + $k * $scale_y;
                    $floor_y = self::floor($dy);
                    $iy = $floor_y & 255;
                    $dy -= $floor_y;
                    $fy = self::fade($dy);
                    if (0 === $k || $iy !== $n) {
                        $n = $iy;
                        // Hash coordinates of the cube corners
                        $a = $this->perm[$ix] + $iy;
                        $aa = $this->perm[$a] + $iz;
                        $ab = $this->perm[$a + 1] + $iz;
                        $b = $this->perm[$ix + 1] + $iy;
                        $ba = $this->perm[$b] + $iz;
                        $bb = $this->perm[$b + 1] + $iz;
                        $x1 = self::lerp($fx, self::grad($this->perm[$aa], $dx, $dy, $dz), self::grad($this->perm[$ba], $dx - 1, $dy, $dz));
                        $x2 = self::lerp(
                            $fx,
                            self::grad($this->perm[$ab], $dx, $dy - 1, $dz),
                            self::grad($this->perm[$bb], $dx - 1, $dy - 1, $dz),
                        );
                        $x3 = self::lerp(
                            $fx,
                            self::grad($this->perm[$aa + 1], $dx, $dy, $dz - 1),
                            self::grad($this->perm[$ba + 1], $dx - 1, $dy, $dz - 1),
                        );
                        $x4 = self::lerp(
                            $fx,
                            self::grad($this->perm[$ab + 1], $dx, $dy - 1, $dz - 1),
                            self::grad($this->perm[$bb + 1], $dx - 1, $dy - 1, $dz - 1),
                        );
                    }
                    $y1 = self::lerp($fy, $x1, $x2);
                    $y2 = self::lerp($fy, $x3, $x4);

                    $noise[++$index] += self::lerp($fz, $y1, $y2) * $amplitude;
                }
            }
        }

        return $noise;
    }
}
<?php

/*
 * Derived from Glowstone.
 * Original code licensed under the MIT License.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\noise\glowstone;

use pocketmine\utils\Random;

class SimplexNoise extends PerlinNoise
{
    protected const SQRT_3 = 1.7320508075688772;
    protected const F2 = 0.5 * (self::SQRT_3 - 1);
    protected const G2 = (3 - self::SQRT_3) / 6;
    protected const G22 = self::G2 * 2.0 - 1;
    protected const F3 = 1.0 / 3.0;
    protected const G3 = 1.0 / 6.0;
    protected const G32 = self::G3 * 2.0;
    protected const G33 = self::G3 * 3.0 - 1.0;

    /** @var int[] */
    protected array $perm_mod_12 = [];

    /** @var Grad[] */
    private static array $grad_3;

    /**
     * Creates a simplex noise generator.
     *
     * @param Random $rand the PRNG to use
     */
    public function __construct(Random $rand)
    {
        parent::__construct($rand);
        for ($i = 0; $i < 512; ++$i) {
            $this->perm_mod_12[$i] = $this->perm[$i] % 12;
        }
    }

    public static function init(): void
    {
        self::$grad_3 = [
            new Grad(1, 1, 0), new Grad(-1, 1, 0), new Grad(1, -1, 0),
            new Grad(-1, -1, 0),
            new Grad(1, 0, 1), new Grad(-1, 0, 1), new Grad(1, 0, -1), new Grad(-1, 0, -1),
            new Grad(0, 1, 1), new Grad(0, -1, 1), new Grad(0, 1, -1), new Grad(0, -1, -1),
        ];
    }

    public static function floor(float $x): int
    {
        return $x > 0 ? (int) $x : (int) $x - 1;
    }

    public function noise3d(float $xin, float $yin = 0.0, float $zin = 0.0): float
    {
        if (0.0 === $yin) {
            return parent::noise3d($xin, $yin, $zin);
        }

        $xin += $this->offset_x;
        $yin += $this->offset_y;
        if (0.0 === $xin) {
            return $this->simplex2D($xin, $yin);
        }

        $zin += $this->offset_z;

        return $this->simplex3D($xin, $yin, $zin);
    }

    protected static function dot(Grad $g, float $x, float $y, float $z = 0.0): float
    {
        return $g->x * $x + $g->y * $y + $g->z * $z;
    }

    /**
     * @param float[] $noise
     *
     * @return float[]
     */
    protected function get2dNoise(array &$noise, float $x, float $z, int $size_x, int $size_y, float $scale_x, float $scale_y, float $amplitude): array
    {
        $index = -1;
        for ($i = 0; $i < $size_y; ++$i) {
            $zin = $this->offset_y + ($z + $i) * $scale_y;
            for ($j = 0; $j < $size_x; ++$j) {
                $xin = $this->offset_x + ($x + $j) * $scale_x;
                $noise[++$index] += $this->simplex2D($xin, $zin) * $amplitude;
            }
        }

        return $noise;
    }

    /**
     * @param float[] $noise
     *
     * @return float[]
     */
    protected function get3dNoise(array &$noise, float $x, float $y, float $z, int $size_x, int $size_y, int $sizeZ, float $scale_x, float $scale_y, float $scale_z, float $amplitude): array
    {
        $index = -1;
        for ($i = 0; $i < $sizeZ; ++$i) {
            $zin = $this->offset_z + ($z + $i) * $scale_z;
            for ($j = 0; $j < $size_x; ++$j) {
                $xin = $this->offset_x + ($x + $j) * $scale_x;
                for ($k = 0; $k < $size_y; ++$k) {
                    $yin = $this->offset_y + ($y + $k) * $scale_y;
                    $noise[++$index] += $this->simplex3D($xin, $yin, $zin) * $amplitude;
                }
            }
        }

        return $noise;
    }

    private function simplex2D(float $xin, float $yin): float
    {
        // Skew the input space to determine which simplex cell we're in
        $s = ($xin + $yin) * self::F2; // Hairy factor for 2D
        $i = self::floor($xin + $s);
        $j = self::floor($yin + $s);
        $t = ($i + $j) * self::G2;
        $dx0 = $i - $t; // Unskew the cell origin back to (x,y) space
        $dy0 = $j - $t;
        $x0 = $xin - $dx0; // The x,y distances from the cell origin
        $y0 = $yin - $dy0;

        // For the 2D case, the simplex shape is an equilateral triangle.

        // Determine which simplex we are in.
        $i1 = 0; // Offsets for second (middle) corner of simplex in (i,j) coords
        $j1 = 0;
        if ($x0 > $y0) {
            $i1 = 1; // lower triangle, XY order: (0,0)->(1,0)->(1,1)
            $j1 = 0;
        } else {
            $i1 = 0; // upper triangle, YX order: (0,0)->(0,1)->(1,1)
            $j1 = 1;
        }

        // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
        // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
        // c = (3-sqrt(3))/6

        $x1 = $x0 - $i1 + self::G2; // Offsets for middle corner in (x,y) unskewed coords
        $y1 = $y0 - $j1 + self::G2;
        $x2 = $x0 + self::G22; // Offsets for last corner in (x,y) unskewed coords
        $y2 = $y0 + self::G22;

        // Work out the hashed gradient indices of the three simplex corners
        $ii = $i & 255;
        $jj = $j & 255;
        $gi0 = $this->perm_mod_12[$ii + $this->perm[$jj]];
        $gi1 = $this->perm_mod_12[$ii + $i1 + $this->perm[$jj + $j1]];
        $gi2 = $this->perm_mod_12[$ii + 1 + $this->perm[$jj + 1]];

        // Calculate the contribution from the three corners
        $t0 = 0.5 - $x0 * $x0 - $y0 * $y0;
        $n0 = 0.0;
        if ($t0 < 0) {
            $n0 = 0.0;
        } else {
            $t0 *= $t0;
            $n0 = $t0 * $t0 * self::dot(self::$grad_3[$gi0], $x0, $y0); // (x,y) of grad_3 used for 2D gradient
        }

        $t1 = 0.5 - $x1 * $x1 - $y1 * $y1;
        $n1 = 0.0;
        if ($t1 < 0) {
            $n1 = 0.0;
        } else {
            $t1 *= $t1;
            $n1 = $t1 * $t1 * self::dot(self::$grad_3[$gi1], $x1, $y1);
        }

        $t2 = 0.5 - $x2 * $x2 - $y2 * $y2;
        $n2 = 0;
        if ($t2 < 0) {
            $n2 = 0.0;
        } else {
            $t2 *= $t2;
            $n2 = $t2 * $t2 * self::dot(self::$grad_3[$gi2], $x2, $y2);
        }

        // Add contributions from each corner to get the final noise value.
        // The result is scaled to return values in the interval [-1,1].
        return 70.0 * ($n0 + $n1 + $n2);
    }

    private function simplex3D(float $xin, float $yin, float $zin): float
    {
        // Skew the input space to determine which simplex cell we're in
        $s = ($xin + $yin + $zin) * self::F3; // Very nice and simple skew factor for 3D
        $i = self::floor($xin + $s);
        $j = self::floor($yin + $s);
        $k = self::floor($zin + $s);
        $t = ($i + $j + $k) * self::G3;
        $dx0 = $i - $t; // Unskew the cell origin back to (x,y,z) space
        $dy0 = $j - $t;
        $dz0 = $k - $t;

        // For the 3D case, the simplex shape is a slightly irregular tetrahedron.

        $i1 = 0; // Offsets for second corner of simplex in (i,j,k) coords
        $j1 = 0;
        $k1 = 0;
        $i2 = 0; // Offsets for third corner of simplex in (i,j,k) coords
        $j2 = 0;
        $k2 = 0;

        $x0 = $xin - $dx0; // The x,y,z distances from the cell origin
        $y0 = $yin - $dy0;
        $z0 = $zin - $dz0;
        // Determine which simplex we are in
        if ($x0 >= $y0) {
            if ($y0 >= $z0) {
                $i1 = 1; // X Y Z order
                $j1 = 0;
                $k1 = 0;
                $i2 = 1;
                $j2 = 1;
                $k2 = 0;
            } elseif ($x0 >= $z0) {
                $i1 = 1; // X Z Y order
                $j1 = 0;
                $k1 = 0;
                $i2 = 1;
                $j2 = 0;
                $k2 = 1;
            } else {
                $i1 = 0; // Z X Y order
                $j1 = 0;
                $k1 = 1;
                $i2 = 1;
                $j2 = 0;
                $k2 = 1;
            }
        } else { // x0<y0
            if ($y0 < $z0) {
                $i1 = 0; // Z Y X order
                $j1 = 0;
                $k1 = 1;
                $i2 = 0;
                $j2 = 1;
                $k2 = 1;
            } elseif ($x0 < $z0) {
                $i1 = 0; // Y Z X order
                $j1 = 1;
                $k1 = 0;
                $i2 = 0;
                $j2 = 1;
                $k2 = 1;
            } else {
                $i1 = 0; // Y X Z order
                $j1 = 1;
                $k1 = 0;
                $i2 = 1;
                $j2 = 1;
                $k2 = 0;
            }
        }

        // A step of (1,0,0) in (i,j,k) means a step of (1-c,-c,-c) in (x,y,z),
        // a step of (0,1,0) in (i,j,k) means a step of (-c,1-c,-c) in (x,y,z), and
        // a step of (0,0,1) in (i,j,k) means a step of (-c,-c,1-c) in (x,y,z), where
        // c = 1/6.
        $x1 = $x0 - $i1 + self::G3; // Offsets for second corner in (x,y,z) coords
        $y1 = $y0 - $j1 + self::G3;
        $z1 = $z0 - $k1 + self::G3;
        $x2 = $x0 - $i2 + self::G32; // Offsets for third corner in (x,y,z) coords
        $y2 = $y0 - $j2 + self::G32;
        $z2 = $z0 - $k2 + self::G32;

        // Work out the hashed gradient indices of the four simplex corners
        $ii = $i & 255;
        $jj = $j & 255;
        $kk = $k & 255;
        $gi0 = $this->perm_mod_12[$ii + $this->perm[$jj + $this->perm[$kk]]];
        $gi1 = $this->perm_mod_12[$ii + $i1 + $this->perm[$jj + $j1 + $this->perm[$kk + $k1]]];
        $gi2 = $this->perm_mod_12[$ii + $i2 + $this->perm[$jj + $j2 + $this->perm[$kk + $k2]]];
        $gi3 = $this->perm_mod_12[$ii + 1 + $this->perm[$jj + 1 + $this->perm[$kk + 1]]];

        // Calculate the contribution from the four corners
        $t0 = 0.5 - $x0 * $x0 - $y0 * $y0 - $z0 * $z0;
        $n0 = 0.0; // Noise contributions from the four corners
        if ($t0 < 0) {
            $n0 = 0.0;
        } else {
            $t0 *= $t0;
            $n0 = $t0 * $t0 * self::dot(self::$grad_3[$gi0], $x0, $y0, $z0);
        }

        $t1 = 0.5 - $x1 * $x1 - $y1 * $y1 - $z1 * $z1;
        $n1 = 0.0;
        if ($t1 < 0) {
            $n1 = 0.0;
        } else {
            $t1 *= $t1;
            $n1 = $t1 * $t1 * self::dot(self::$grad_3[$gi1], $x1, $y1, $z1);
        }

        $t2 = 0.5 - $x2 * $x2 - $y2 * $y2 - $z2 * $z2;
        $n2 = 0.0;
        if ($t2 < 0) {
            $n2 = 0.0;
        } else {
            $t2 *= $t2;
            $n2 = $t2 * $t2 * self::dot(self::$grad_3[$gi2], $x2, $y2, $z2);
        }

        $x3 = $x0 + self::G33; // Offsets for last corner in (x,y,z) coords
        $y3 = $y0 + self::G33;
        $z3 = $z0 + self::G33;
        $t3 = 0.5 - $x3 * $x3 - $y3 * $y3 - $z3 * $z3;
        $n3 = 0.0;
        if ($t3 < 0) {
            $n3 = 0.0;
        } else {
            $t3 *= $t3;
            $n3 = $t3 * $t3 * self::dot(self::$grad_3[$gi3], $x3, $y3, $z3);
        }

        // Add contributions from each corner to get the final noise value.
        // The result is scaled to stay just inside [-1,1]
        return 32.0 * ($n0 + $n1 + $n2 + $n3);
    }
}

// Inner class to speed up gradient computations
// (array access is a lot slower than member access)
final class Grad
{
    public function __construct(
        public float $x,
        public float $y,
        public float $z,
    ) {}
}

SimplexNoise::init();
<?php

/*
 * Derived from Glowstone.
 * Original code licensed under the MIT License.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\noise\glowstone;

use lolfosjo\netherx\noise\bukkit\NoiseGenerator;
use lolfosjo\netherx\noise\bukkit\OctaveGenerator;
use pocketmine\utils\Random;

class PerlinOctaveGenerator extends OctaveGenerator
{
    public int $size_x;
    public int $size_y;
    public int $size_z;

    /** @var float[] */
    protected array $noise;

    /**
     * Creates a generator for multiple layers of Perlin noise.
     *
     * @param NoiseGenerator[] $octaves the noise generators
     * @param int              $size_x  the size on the X axis
     * @param int              $size_y  the size on the Y axis
     * @param int              $size_z  the size on the Z axis
     */
    public function __construct(array $octaves, int $size_x, int $size_y, int $size_z)
    {
        parent::__construct($octaves);
        $this->size_x = $size_x;
        $this->size_y = $size_y;
        $this->size_z = $size_z;
        $this->noise = array_fill(0, $size_x * $size_y * $size_z, 0.0);
    }

    public static function fromRandomAndOctaves(Random $random, int $octaves, int $size_x, int $size_y, int $size_z): self
    {
        return new PerlinOctaveGenerator(self::createOctaves($random, $octaves), $size_x, $size_y, $size_z);
    }

    /**
     * Generates multiple layers of noise.
     *
     * @param float $x           the starting X coordinate
     * @param float $y           the starting Y coordinate
     * @param float $z           the starting Z coordinate
     * @param float $lacunarity  layer n's frequency as a fraction of layer {@code n - 1}'s frequency
     * @param float $persistence layer n's amplitude as a multiple of layer {@code n - 1}'s amplitude
     *
     * @return float[] the noise array
     */
    public function getFractalBrownianMotion(float $x, float $y, float $z, float $lacunarity, float $persistence): array
    {
        $this->noise = array_fill(0, $this->size_x * $this->size_y * $this->size_z, 0.0);

        $freq = 1;
        $amp = 1;

        $x *= $this->x_scale;
        $y *= $this->y_scale;
        $z *= $this->z_scale;

        // fBm
        // the noise have to be periodic over x and z axis: otherwise it can go crazy with high
        // input, leading to strange oddities in terrain generation like the old minecraft farland
        // symptoms.
        /** @var PerlinNoise $octave */
        foreach ($this->octaves as $octave) {
            $dx = $x * $freq;
            $dz = $z * $freq;
            // compute integer part
            $lx = self::floor($dx);
            $lz = self::floor($dz);
            // compute fractional part
            $dx -= $lx;
            $dz -= $lz;
            // wrap integer part to 0..16777216
            $lx %= 16777216;
            $lz %= 16777216;
            // add to fractional part
            $dx += $lx;
            $dz += $lz;

            $dy = $y * $freq;
            $this->noise = $octave->getNoise($this->noise, $dx, $dy, $dz, $this->size_x, $this->size_y, $this->size_z, $this->x_scale * $freq, $this->y_scale * $freq, $this->z_scale * $freq, $amp);
            $freq *= $lacunarity;
            $amp *= $persistence;
        }

        return $this->noise;
    }

    /**
     * @return PerlinNoise[]
     */
    protected static function createOctaves(Random $rand, int $octaves): array
    {
        $result = [];

        for ($i = 0; $i < $octaves; ++$i) {
            $result[$i] = new PerlinNoise($rand);
        }

        return $result;
    }

    protected static function floor(float $x): int
    {
        return $x >= 0 ? (int) $x : (int) $x - 1;
    }
}
<?php

/*
 * Derived from Glowstone.
 * Original code licensed under the MIT License.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\noise\glowstone;

use pocketmine\utils\Random;

class SimplexOctaveGenerator extends PerlinOctaveGenerator
{
    public static function fromRandomAndOctaves(Random $random, int $octaves, int $size_x, int $size_y, int $size_z): self
    {
        return new SimplexOctaveGenerator(self::createOctaves($random, $octaves), $size_x, $size_y, $size_z);
    }

    public function getFractalBrownianMotion(float $x, float $y, float $z, float $lacunarity, float $persistence): array
    {
        $this->noise = array_fill(0, $this->size_x * $this->size_y * $this->size_z, 0.0);

        $freq = 1.0;
        $amp = 1.0;

        // fBm
        /** @var SimplexNoise $octave */
        foreach ($this->octaves as $octave) {
            $this->noise = $octave->getNoise($this->noise, $x, $y, $z, $this->size_x, $this->size_y, $this->size_z, $this->x_scale * $freq, $this->y_scale * $freq, $this->z_scale * $freq, 0.55 / $amp);
            $freq *= $lacunarity;
            $amp *= $persistence;
        }

        return $this->noise;
    }

    /**
     * @return SimplexNoise[]
     */
    protected static function createOctaves(Random $rand, int $octaves): array
    {
        $result = [];

        for ($i = 0; $i < $octaves; ++$i) {
            $result[$i] = new SimplexNoise($rand);
        }

        return $result;
    }
}
<?php

/*
 * Derived from Glowstone.
 * Original code licensed under the MIT License.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether;

use lolfosjo\netherx\noise\glowstone\PerlinOctaveGenerator;
use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

class Density
{
    protected const COORDINATE_SCALE = 684.412;
    protected const HEIGHT_SCALE = 2053.236;
    protected const HEIGHT_NOISE_SCALE_X = 100.0;
    protected const HEIGHT_NOISE_SCALE_Z = 100.0;
    protected const DETAIL_NOISE_SCALE_X = 80.0;
    protected const DETAIL_NOISE_SCALE_Y = 60.0;
    protected const DETAIL_NOISE_SCALE_Z = 80.0;

    /** Must match Surface::LAVA_LEVEL. Lava fills below this Y during raw terrain generation. */
    private const LAVA_LEVEL = 31;

    private PerlinOctaveGenerator $octaveHeight;
    private PerlinOctaveGenerator $octaveRoughness;
    private PerlinOctaveGenerator $octaveRoughness2;
    private PerlinOctaveGenerator $octaveDetail;

    private int $netherrackId;
    private int $stillLavaId;

    private static ?array $nvTable = null;

    public function __construct(Random $noiseRand)
    {
        $this->octaveHeight = PerlinOctaveGenerator::fromRandomAndOctaves($noiseRand, 16, 5, 1, 5);
        $this->octaveHeight->x_scale = static::HEIGHT_NOISE_SCALE_X;
        $this->octaveHeight->z_scale = static::HEIGHT_NOISE_SCALE_Z;

        $this->octaveRoughness = PerlinOctaveGenerator::fromRandomAndOctaves($noiseRand, 16, 5, 17, 5);
        $this->octaveRoughness->x_scale = static::COORDINATE_SCALE;
        $this->octaveRoughness->y_scale = static::HEIGHT_SCALE;
        $this->octaveRoughness->z_scale = static::COORDINATE_SCALE;

        $this->octaveRoughness2 = PerlinOctaveGenerator::fromRandomAndOctaves($noiseRand, 16, 5, 17, 5);
        $this->octaveRoughness2->x_scale = static::COORDINATE_SCALE;
        $this->octaveRoughness2->y_scale = static::HEIGHT_SCALE;
        $this->octaveRoughness2->z_scale = static::COORDINATE_SCALE;

        $this->octaveDetail = PerlinOctaveGenerator::fromRandomAndOctaves($noiseRand, 8, 5, 17, 5);
        $this->octaveDetail->x_scale = static::COORDINATE_SCALE / static::DETAIL_NOISE_SCALE_X;
        $this->octaveDetail->y_scale = static::HEIGHT_SCALE / static::DETAIL_NOISE_SCALE_Y;
        $this->octaveDetail->z_scale = static::COORDINATE_SCALE / static::DETAIL_NOISE_SCALE_Z;

        $this->netherrackId = VanillaBlocks::NETHERRACK()->getStateId();
        $this->stillLavaId = VanillaBlocks::LAVA()->getStateId();

        if (null === self::$nvTable) {
            $kMax = $this->octaveDetail->size_y; // = 17
            $nv = [];
            for ($i = 0; $i < $kMax; ++$i) {
                $nv[$i] = cos($i * M_PI * 6.0 / $kMax) * 2.0;
                $nh = $i > $kMax / 2 ? $kMax - 1 - $i : $i;
                if ($nh < 4.0) {
                    $nh = 4.0 - $nh;
                    $nv[$i] -= $nh * $nh * $nh * 10.0;
                }
            }
            self::$nvTable = $nv;
        }
    }

    public function generateRawTerrain(ChunkManager $world, int $chunkX, int $chunkZ): void
    {
        $density = $this->buildDensityGrid($chunkX << 2, $chunkZ << 2);

        $chunk = $world->getChunk($chunkX, $chunkZ);

        $coordBitSize = Chunk::COORD_BIT_SIZE;

        for ($i = 0; $i < 4; ++$i) {
            for ($j = 0; $j < 4; ++$j) {
                for ($k = 0; $k < 16; ++$k) {
                    $d1 = $density[($k << 6) | ($j << 3) | $i];
                    $d2 = $density[($k << 6) | ($j << 3) | ($i + 1)];
                    $d3 = $density[($k << 6) | (($j + 1) << 3) | $i];
                    $d4 = $density[($k << 6) | (($j + 1) << 3) | ($i + 1)];
                    $d5 = ($density[(($k + 1) << 6) | ($j << 3) | $i] - $d1) / 8;
                    $d6 = ($density[(($k + 1) << 6) | ($j << 3) | ($i + 1)] - $d2) / 8;
                    $d7 = ($density[(($k + 1) << 6) | (($j + 1) << 3) | $i] - $d3) / 8;
                    $d8 = ($density[(($k + 1) << 6) | (($j + 1) << 3) | ($i + 1)] - $d4) / 8;

                    for ($l = 0; $l < 8; ++$l) {
                        $d9 = $d1;
                        $d10 = $d3;

                        $yPos = $l + ($k << 3);
                        $yBlockPos = $yPos & 0xF;
                        $subChunk = $chunk->getSubChunk($yPos >> $coordBitSize);

                        $iBase = $i << 2;
                        $jBase = $j << 2;

                        for ($m = 0; $m < 4; ++$m) {
                            $dens = $d9;
                            $mOff = $m + $iBase;
                            $dStep = ($d10 - $d9) / 4;

                            for ($n = 0; $n < 4; ++$n) {
                                if ($dens > 0) {
                                    $subChunk->setBlockStateId($mOff, $yBlockPos, $n + $jBase, $this->netherrackId);
                                } elseif ($yPos < self::LAVA_LEVEL) {
                                    $subChunk->setBlockStateId($mOff, $yBlockPos, $n + $jBase, $this->stillLavaId);
                                }
                                $dens += $dStep;
                            }
                            $d9 += ($d2 - $d1) / 4;
                            $d10 += ($d4 - $d3) / 4;
                        }
                        $d1 += $d5;
                        $d3 += $d7;
                        $d2 += $d6;
                        $d4 += $d8;
                    }
                }
            }
        }
    }

    private function buildDensityGrid(int $x, int $z): array
    {
        $heightNoise = $this->octaveHeight->getFractalBrownianMotion($x, 0, $z, 0.5, 2.0);
        $roughnessNoise = $this->octaveRoughness->getFractalBrownianMotion($x, 0, $z, 0.5, 2.0);
        $roughness2Noise = $this->octaveRoughness2->getFractalBrownianMotion($x, 0, $z, 0.5, 2.0);
        $detailNoise = $this->octaveDetail->getFractalBrownianMotion($x, 0, $z, 0.5, 2.0);

        $nv = self::$nvTable;
        $kMax = count($nv);

        $index = 0;
        $indexHeight = 0;
        $density = [];

        $kCap = $kMax - 4;

        for ($i = 0; $i < 5; ++$i) {
            for ($j = 0; $j < 5; ++$j) {
                $noiseH = $heightNoise[$indexHeight++] / 8000.0;
                if ($noiseH < 0) {
                    $noiseH = -$noiseH;
                }
                $noiseH = $noiseH * 3.0 - 3.0;
                if ($noiseH < 0) {
                    $noiseH = max($noiseH * 0.5, -1.0) / 1.4 * 0.5;
                } else {
                    $noiseH = min($noiseH, 1.0) / 6.0;
                }
                $noiseH *= $kMax / 16.0;

                for ($k = 0; $k < $kMax; ++$k) {
                    $noiseR = $roughnessNoise[$index] / 512.0;
                    $noiseR2 = $roughness2Noise[$index] / 512.0;
                    $noiseD = ($detailNoise[$index] / 10.0 + 1.0) / 2.0;

                    if ($noiseD < 0) {
                        $dens = $noiseR;
                    } elseif ($noiseD > 1) {
                        $dens = $noiseR2;
                    } else {
                        $dens = $noiseR + ($noiseR2 - $noiseR) * $noiseD;
                    }

                    $dens -= $nv[$k];
                    $dens += $noiseH;

                    if ($k > $kCap) {
                        $lowering = ($k - $kCap) / 3.0;
                        $dens = $dens * (1.0 - $lowering) + (-10.0) * $lowering;
                    }

                    $density[($k << 6) | ($j << 3) | $i] = $dens;
                    ++$index;
                }
            }
        }

        return $density;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator\basaltdelta;

use lolfosjo\netherx\nether\populator\Populator;
use lolfosjo\netherx\noise\glowstone\PerlinOctaveGenerator;
use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

final class BasaltDeltaLavaPopulator extends Populator
{
    private int $basaltId;
    private int $blackstoneId;
    private int $magmaId;
    private int $gravelId;
    private int $lavaId;
    private int $flowingLavaId;
    private int $airId;

    private PerlinOctaveGenerator $surfaceNoise;
    private PerlinOctaveGenerator $surfaceSecNoise;

    public function __construct(int $worldSeed)
    {
        $this->basaltId = VanillaBlocks::BASALT()->getStateId();
        $this->blackstoneId = VanillaBlocks::BLACKSTONE()->getStateId();
        $this->magmaId = VanillaBlocks::MAGMA()->getStateId();
        $this->gravelId = VanillaBlocks::GRAVEL()->getStateId();
        $this->lavaId = VanillaBlocks::LAVA()->getStateId();
        $this->flowingLavaId = VanillaBlocks::LAVA()->getFlowingForm()->getStateId();
        $this->airId = VanillaBlocks::AIR()->getStateId();

        $rand = new Random($worldSeed);
        $this->surfaceNoise = PerlinOctaveGenerator::fromRandomAndOctaves($rand, 2, 16, 128, 16);
        $this->surfaceNoise->setScale(0.0625);

        $rand2 = new Random($worldSeed + 1);
        $this->surfaceSecNoise = PerlinOctaveGenerator::fromRandomAndOctaves($rand2, 2, 16, 128, 16);
        $this->surfaceSecNoise->setScale(0.0625);
    }

    public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random): void
    {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $baseX = $chunkX << 4;
        $baseZ = $chunkZ << 4;
        $sizeY = 128;
        $sizeZ = 16;

        $noiseMain = $this->surfaceNoise->getFractalBrownianMotion($baseX, 0, $baseZ, 2.0, 0.5);
        $noiseSec = $this->surfaceSecNoise->getFractalBrownianMotion($baseX, 0, $baseZ, 2.0, 0.5);

        $amount = $random->nextBoundedInt(64) + 64;
        for ($i = 0; $i < $amount; ++$i) {
            $localX = $random->nextBoundedInt(16);
            $localZ = $random->nextBoundedInt(16);
            $absX = $baseX + $localX;
            $absZ = $baseZ + $localZ;

            $ys = $this->getWorkableYsForLavaLake($world, $chunk, $localX, $localZ, $absX, $absZ);
            foreach ($ys as $y) {
                if ($y < 1 || $y >= 127) {
                    continue;
                }
                $chunk->setBlockStateId($localX, $y, $localZ, $this->flowingLavaId);
            }
        }

        for ($localX = 0; $localX < 16; ++$localX) {
            for ($localZ = 0; $localZ < 16; ++$localZ) {
                $absX = $baseX + $localX;
                $absZ = $baseZ + $localZ;

                for ($y = 1; $y < 127; ++$y) {
                    if ($chunk->getBlockStateId($localX, $y, $localZ) !== $this->gravelId) {
                        continue;
                    }

                    $idx = ($localX * $sizeY + $y) * $sizeZ + $localZ;
                    $secNoise = $noiseSec[$idx];
                    $mainNoise = $noiseMain[$idx];

                    if ($secNoise < -0.9) {
                        $target = $this->blackstoneId;
                    } elseif ($secNoise < 0.8) {
                        $target = $this->basaltId;
                    } else {
                        $target = $this->magmaId;
                    }

                    if ($mainNoise > 0.0) {
                        $chunk->setBlockStateId($localX, $y, $localZ, $target);
                    } else {
                        $hasAirNeighbor = false;
                        $neighbors = [
                            [$absX + 1, $absZ], [$absX - 1, $absZ],
                            [$absX, $absZ + 1], [$absX, $absZ - 1],
                        ];
                        foreach ($neighbors as [$nx, $nz]) {
                            $cx = $nx >> 4;
                            $cz = $nz >> 4;
                            $lx = $nx & 0x0F;
                            $lz = $nz & 0x0F;
                            $neighborChunk = $world->getChunk($cx, $cz);
                            if (null !== $neighborChunk && $neighborChunk->getBlockStateId($lx, $y, $lz) === $this->airId) {
                                $hasAirNeighbor = true;

                                break;
                            }
                        }
                        if ($hasAirNeighbor) {
                            $chunk->setBlockStateId($localX, $y, $localZ, $target);
                        } else {
                            $chunk->setBlockStateId($localX, $y, $localZ, $this->lavaId);
                        }
                    }
                }
            }
        }
    }

    private function getWorkableYsForLavaLake(ChunkManager $world, Chunk $chunk, int $localX, int $localZ, int $absX, int $absZ): array
    {
        $results = [];
        for ($y = 126; $y >= 1; --$y) {
            $block = $chunk->getBlockStateId($localX, $y, $localZ);
            if ($block !== $this->basaltId && $block !== $this->blackstoneId) {
                continue;
            }
            if ($chunk->getBlockStateId($localX, $y + 1, $localZ) !== $this->airId) {
                continue;
            }

            $allSolid = true;
            $neighbors = [
                [$absX + 1, $absZ], [$absX - 1, $absZ],
                [$absX, $absZ + 1], [$absX, $absZ - 1],
            ];
            foreach ($neighbors as [$nx, $nz]) {
                $cx = $nx >> 4;
                $cz = $nz >> 4;
                $lx = $nx & 0x0F;
                $lz = $nz & 0x0F;
                $neighborChunk = $world->getChunk($cx, $cz);
                if (null === $neighborChunk || $neighborChunk->getBlockStateId($lx, $y, $lz) === $this->airId) {
                    $allSolid = false;

                    break;
                }
            }
            if ($allSolid) {
                $results[] = $y;
            }
        }

        return $results;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator\basaltdelta;

use lolfosjo\netherx\nether\populator\Populator;
use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

final class BasaltDeltaPillarPopulator extends Populator
{
    private int $basaltId;
    private int $blackstoneId;
    private int $airId;

    public function __construct()
    {
        $this->basaltId = VanillaBlocks::BASALT()->getStateId();
        $this->blackstoneId = VanillaBlocks::BLACKSTONE()->getStateId();
        $this->airId = VanillaBlocks::AIR()->getStateId();
    }

    public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random): void
    {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $amount = $random->nextBoundedInt(128) + 128;

        for ($i = 0; $i < $amount; ++$i) {
            $localX = $random->nextBoundedInt(16);
            $localZ = $random->nextBoundedInt(16);

            $surfaceYs = $this->getHighestWorkableBlocks($chunk, $localX, $localZ);
            foreach ($surfaceYs as $y) {
                if ($y <= 1 || $y >= 127) {
                    continue;
                }
                if (0 === $random->nextBoundedInt(5)) {
                    continue;
                }
                $height = $random->nextBoundedInt(5) + 1;
                for ($h = 0; $h < $height; ++$h) {
                    $yy = $y + $h;
                    if ($yy >= 127) {
                        break;
                    }
                    $chunk->setBlockStateId($localX, $yy, $localZ, $this->basaltId);
                }
            }
        }
    }

    private function getHighestWorkableBlocks(Chunk $chunk, int $localX, int $localZ): array
    {
        $results = [];
        for ($y = 126; $y >= 2; --$y) {
            if ($chunk->getBlockStateId($localX, $y, $localZ) === $this->blackstoneId) {
                if ($chunk->getBlockStateId($localX, $y + 1, $localZ) === $this->airId) {
                    $results[] = $y + 1;
                }
            }
        }

        return $results;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator\basaltdelta;

use lolfosjo\netherx\nether\populator\Populator;
use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

final class BasaltDeltaMagmaPopulator extends Populator
{
    private int $basaltId;
    private int $blackstoneId;
    private int $magmaId;
    private int $lavaId;
    private int $flowingLavaId;
    private int $airId;

    public function __construct()
    {
        $this->basaltId = VanillaBlocks::BASALT()->getStateId();
        $this->blackstoneId = VanillaBlocks::BLACKSTONE()->getStateId();
        $this->magmaId = VanillaBlocks::MAGMA()->getStateId();
        $this->lavaId = VanillaBlocks::LAVA()->getStateId();
        $this->flowingLavaId = VanillaBlocks::LAVA()->getFlowingForm()->getStateId();
        $this->airId = VanillaBlocks::AIR()->getStateId();
    }

    public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random): void
    {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $amount = $random->nextBoundedInt(4) + 20;

        for ($i = 0; $i < $amount; ++$i) {
            $localX = $random->nextBoundedInt(16);
            $localZ = $random->nextBoundedInt(16);
            $absX = ($chunkX << 4) + $localX;
            $absZ = ($chunkZ << 4) + $localZ;

            $ys = $this->getWorkableYs($world, $chunk, $localX, $localZ, $absX, $absZ);
            foreach ($ys as $y) {
                if ($y < 1 || $y >= 127) {
                    continue;
                }
                $chunk->setBlockStateId($localX, $y, $localZ, $this->magmaId);
            }
        }
    }

    private function getWorkableYs(ChunkManager $world, Chunk $chunk, int $localX, int $localZ, int $absX, int $absZ): array
    {
        $results = [];
        for ($y = 126; $y >= 1; --$y) {
            $block = $chunk->getBlockStateId($localX, $y, $localZ);
            if ($block !== $this->basaltId && $block !== $this->blackstoneId) {
                continue;
            }
            if ($chunk->getBlockStateId($localX, $y + 1, $localZ) !== $this->airId) {
                continue;
            }

            $hasLava = false;
            $neighbors = [
                [$absX + 1, $absZ], [$absX - 1, $absZ],
                [$absX, $absZ + 1], [$absX, $absZ - 1],
            ];
            foreach ($neighbors as [$nx, $nz]) {
                $cx = $nx >> 4;
                $cz = $nz >> 4;
                $lx = $nx & 0x0F;
                $lz = $nz & 0x0F;
                $neighborChunk = $world->getChunk($cx, $cz);
                if (null !== $neighborChunk) {
                    $nbId = $neighborChunk->getBlockStateId($lx, $y, $lz);
                    if ($nbId === $this->lavaId || $nbId === $this->flowingLavaId) {
                        $hasLava = true;

                        break;
                    }
                }
            }
            if ($hasLava) {
                $results[] = $y;
            }
        }

        return $results;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator\crimson;

use lolfosjo\netherx\nether\populator\Populator;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

final class WeepingVinesCeilingPopulator extends Populator
{
    private int $airId;
    private int $weepingVinesId;
    private int $netherRackId;

    public function __construct()
    {
        $this->airId = VanillaBlocks::AIR()->getStateId();
        $this->weepingVinesId = VanillaBlocks::WEEPING_VINES()->getStateId();
        $this->netherRackId = VanillaBlocks::NETHERRACK()->getStateId();
    }

    public function populate(
        ChunkManager $world,
        int $chunkX,
        int $chunkZ,
        Random $random,
    ): void {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $amount = $random->nextBoundedInt(5) + 1;

        for ($i = 0; $i < $amount; ++$i) {
            $localX = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
            $localZ = $random->nextBoundedInt(Chunk::EDGE_LENGTH);

            if (BiomeIds::CRIMSON_FOREST !== $chunk->getBiomeId($localX, 64, $localZ)) {
                continue;
            }

            $ceilingYs = $this->getCeilingWorkableBlocks($chunk, $localX, $localZ);

            foreach ($ceilingYs as $startY) {
                if ($startY <= 1) {
                    continue;
                }

                $maxLength = 0;
                for ($y = $startY; $y > 1; --$y) {
                    if ($chunk->getBlockStateId($localX, $y, $localZ) !== $this->airId) {
                        break;
                    }
                    ++$maxLength;
                }

                if (0 === $maxLength) {
                    continue;
                }

                $length = $maxLength > 1 ? $random->nextBoundedInt($maxLength) : 1;

                for ($yPos = $startY; $yPos > $startY - $length; --$yPos) {
                    $chunk->setBlockStateId($localX, $yPos, $localZ, $this->weepingVinesId);
                }
            }
        }
    }

    private function getCeilingWorkableBlocks(Chunk $chunk, int $localX, int $localZ): array
    {
        $ys = [];

        for ($y = 1; $y <= 127; ++$y) {
            $blockId = $chunk->getBlockStateId($localX, $y, $localZ);

            if ($blockId === $this->netherRackId) {
                $below = $chunk->getBlockStateId($localX, $y - 1, $localZ);
                if ($below === $this->airId) {
                    $ys[] = $y - 1;
                }
            }
        }

        return $ys;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator\crimson;

use lolfosjo\netherx\nether\populator\Populator;
use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\object\NetherTree;

final class HugeCrimsonFungusPopulator extends Populator
{
    private const MAX_Y = 126;
    private const MIN_Y = 1;

    private int $airId;
    private int $crimsonNyliumId;

    public function __construct(
        private readonly int $treeHeight = 7,
    ) {
        $this->airId = VanillaBlocks::AIR()->getStateId();
        $this->crimsonNyliumId = VanillaBlocks::CRIMSON_NYLIUM()->getStateId();
    }

    public function populate(
        ChunkManager $world,
        int $chunkX,
        int $chunkZ,
        Random $random,
    ): void {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $baseX = $chunkX << 4;
        $baseZ = $chunkZ << 4;

        $amount = $random->nextBoundedInt(6) + 4;

        for ($i = 0; $i < $amount; ++$i) {
            $localX = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
            $localZ = $random->nextBoundedInt(Chunk::EDGE_LENGTH);

            $ys = $this->getHighestWorkableBlocks($chunk, $localX, $localZ);

            foreach ($ys as $y) {
                if ($y <= 1) {
                    continue;
                }
                if (1 === $random->nextBoundedInt(4)) {
                    continue;
                }

                $tree = new NetherTree(
                    VanillaBlocks::CRIMSON_STEM(),
                    VanillaBlocks::NETHER_WART_BLOCK(),
                    VanillaBlocks::SHROOMLIGHT(),
                    $this->treeHeight,
                    true,
                    false,
                );

                $transaction = $tree->getBlockTransaction($world, $baseX + $localX, $y, $baseZ + $localZ, $random);
                if (null === $transaction) {
                    continue;
                }

                $transaction->apply();
            }
        }
    }

    private function getHighestWorkableBlocks(Chunk $chunk, int $localX, int $localZ): array
    {
        $blockYs = [];
        for ($y = self::MAX_Y; $y > self::MIN_Y; --$y) {
            if ($chunk->getBlockStateId($localX, $y, $localZ) !== $this->crimsonNyliumId) {
                continue;
            }
            if ($chunk->getBlockStateId($localX, $y + 1, $localZ) !== $this->airId) {
                continue;
            }
            $blockYs[] = $y + 1;
        }

        return $blockYs;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;

final class LavaOrePopulator extends OrePopulator
{
    public function getOreBlock(int $replacedStateId): int
    {
        return VanillaBlocks::LAVA()->getStateId();
    }

    public function getClusterCount(): int
    {
        return 32;
    }

    public function getClusterSize(): int
    {
        return 1;
    }

    public function getMinHeight(): int
    {
        return 1;
    }

    public function getMaxHeight(): int
    {
        return 32;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

abstract class OrePopulator extends Populator
{
    public const CONCENTRATION_UNIFORM = 0;
    public const CONCENTRATION_TRIANGLE = 1;

    private int $netherrackStateId;

    public function __construct()
    {
        $this->netherrackStateId = VanillaBlocks::NETHERRACK()->getStateId();
    }

    abstract public function getOreBlock(int $replacedStateId): int;

    abstract public function getClusterCount(): int;

    abstract public function getClusterSize(): int;

    abstract public function getMinHeight(): int;

    abstract public function getMaxHeight(): int;

    public function getSkipAir(): float
    {
        return 0.0;
    }

    public function getConcentration(): int
    {
        return self::CONCENTRATION_UNIFORM;
    }

    public function isRare(): bool
    {
        return false;
    }

    public function canBeReplaced(int $stateId): bool
    {
        return $stateId === $this->netherrackStateId;
    }

    public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random): void
    {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $baseX = $chunkX << 4;
        $baseZ = $chunkZ << 4;
        $minY = $this->getMinHeight();
        $maxY = $this->getMaxHeight();
        $attempts = $this->resolveAttempts($random);

        for ($i = 0; $i < $attempts; ++$i) {
            $x = $baseX + $random->nextBoundedInt(16);
            $z = $baseZ + $random->nextBoundedInt(16);
            $y = $this->pickY($random, $minY, $maxY);

            $originState = $world->getBlockAt($x, $y, $z)->getStateId();
            if (!$this->canBeReplaced($originState)) {
                continue;
            }

            if (1 === $this->getClusterSize()) {
                $lx = $x & 0x0F;
                $lz = $z & 0x0F;
                if ($y >= 0 && $y < Chunk::MAX_SUBCHUNKS * 16) {
                    $chunk->setBlockStateId($lx, $y, $lz, $this->getOreBlock($originState));
                }
            } else {
                $veinBlocks = $this->spawnVein($world, $random, $x, $y, $z);

                if (0.0 !== $this->getSkipAir() && $this->veinTouchesAir($world, $veinBlocks)) {
                    if ($random->nextFloat() < $this->getSkipAir()) {
                        continue;
                    }
                }

                foreach ($veinBlocks as $entry) {
                    $lx = $entry['x'] & 0x0F;
                    $lz = $entry['z'] & 0x0F;
                    $vy = $entry['y'];
                    $targetChunk = $world->getChunk($entry['x'] >> 4, $entry['z'] >> 4);
                    if (null !== $targetChunk && $vy >= 0 && $vy < Chunk::MAX_SUBCHUNKS * 16) {
                        $targetChunk->setBlockStateId($lx, $vy, $lz, $entry['state']);
                    }
                }
            }
        }
    }

    private function resolveAttempts(Random $random): int
    {
        if (!$this->isRare()) {
            return $this->getClusterCount();
        }

        return 0 === $random->nextBoundedInt($this->getClusterCount()) ? 1 : 0;
    }

    private function pickY(Random $random, int $minY, int $maxY): int
    {
        $range = $maxY - $minY;
        if ($range <= 0) {
            return $minY;
        }

        if (self::CONCENTRATION_TRIANGLE === $this->getConcentration()) {
            return $minY + (int) (($random->nextBoundedInt($range + 1) + $random->nextBoundedInt($range + 1)) / 2);
        }

        return $minY + $random->nextBoundedInt($range + 1);
    }

    private function spawnVein(ChunkManager $world, Random $random, int $ox, int $oy, int $oz): array
    {
        $size = $this->getClusterSize();
        $piScaled = $random->nextFloat() * M_PI;

        $scaleMaxX = ($ox + 8) + sin($piScaled) * $size / 8.0;
        $scaleMinX = ($ox + 8) - sin($piScaled) * $size / 8.0;
        $scaleMaxZ = ($oz + 8) + cos($piScaled) * $size / 8.0;
        $scaleMinZ = ($oz + 8) - cos($piScaled) * $size / 8.0;
        $scaleMaxY = $oy + $random->nextBoundedInt(3) - 2;
        $scaleMinY = $oy + $random->nextBoundedInt(3) - 2;

        $blocks = [];

        for ($i = 0; $i < $size; ++$i) {
            $t = $i / $size;

            $scaleX = $scaleMaxX + ($scaleMinX - $scaleMaxX) * $t;
            $scaleY = $scaleMaxY + ($scaleMinY - $scaleMaxY) * $t;
            $scaleZ = $scaleMaxZ + ($scaleMinZ - $scaleMaxZ) * $t;

            $sineVal = sin(M_PI * $t) + 1.0;
            $randOffset = $random->nextFloat() * $size / 16.0;
            $radXZ = $sineVal * $randOffset + 1.0;
            $radY = $sineVal * $randOffset + 1.0;

            $minX = (int) floor($scaleX - $radXZ / 2.0);
            $minY = (int) floor($scaleY - $radY / 2.0);
            $minZ = (int) floor($scaleZ - $radXZ / 2.0);
            $maxX = (int) floor($scaleX + $radXZ / 2.0);
            $maxY = (int) floor($scaleY + $radY / 2.0);
            $maxZ = (int) floor($scaleZ + $radXZ / 2.0);

            for ($bx = $minX; $bx <= $maxX; ++$bx) {
                $xVal = ($bx + 0.5 - $scaleX) / ($radXZ / 2.0);
                if ($xVal * $xVal >= 1.0) {
                    continue;
                }

                for ($by = $minY; $by <= $maxY; ++$by) {
                    $yVal = ($by + 0.5 - $scaleY) / ($radY / 2.0);
                    if ($xVal * $xVal + $yVal * $yVal >= 1.0) {
                        continue;
                    }

                    for ($bz = $minZ; $bz <= $maxZ; ++$bz) {
                        $zVal = ($bz + 0.5 - $scaleZ) / ($radXZ / 2.0);
                        if ($xVal * $xVal + $yVal * $yVal + $zVal * $zVal >= 1.0) {
                            continue;
                        }

                        $original = $world->getBlockAt($bx, $by, $bz)->getStateId();
                        if (!$this->canBeReplaced($original)) {
                            continue;
                        }

                        $blocks[] = [
                            'x' => $bx,
                            'y' => $by,
                            'z' => $bz,
                            'state' => $this->getOreBlock($original),
                        ];
                    }
                }
            }
        }

        return $blocks;
    }

    private function veinTouchesAir(ChunkManager $world, array $veinBlocks): bool
    {
        $airId = VanillaBlocks::AIR()->getStateId();

        foreach ($veinBlocks as $entry) {
            $x = $entry['x'];
            $y = $entry['y'];
            $z = $entry['z'];

            $neighbors = [
                [$x + 1, $y, $z], [$x - 1, $y, $z],
                [$x, $y + 1, $z], [$x, $y - 1, $z],
                [$x, $y, $z + 1], [$x, $y, $z - 1],
            ];

            foreach ($neighbors as [$nx, $ny, $nz]) {
                if ($world->getBlockAt($nx, $ny, $nz)->getStateId() === $airId) {
                    return true;
                }
            }
        }

        return false;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;

class NetherGravelPopulator extends OrePopulator
{
    public function getOreBlock(int $replacedStateId): int
    {
        return VanillaBlocks::GRAVEL()->getStateId();
    }

    public function getClusterCount(): int
    {
        return 2;
    }

    public function getClusterSize(): int
    {
        return 33;
    }

    public function getMinHeight(): int
    {
        return 5;
    }

    public function getMaxHeight(): int
    {
        return 36;
    }

    public function canBeReplaced(int $stateId): bool
    {
        return parent::canBeReplaced($stateId);
    }

    public function populate(ChunkManager $world, int $chunkX, int $chunkZ, Random $random): void
    {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $centerX = ($chunkX << 4) + 8;
        $centerZ = ($chunkZ << 4) + 8;
        $centerY = (int) (($this->getMinHeight() + $this->getMaxHeight()) / 2);

        if (BiomeIds::BASALT_DELTAS === $chunk->getBiomeId($centerX & 0x0F, $centerY, $centerZ & 0x0F)) {
            return;
        }

        parent::populate($world, $chunkX, $chunkZ, $random);
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;

final class NetherGoldOrePopulator extends OrePopulator
{
    public function getOreBlock(int $replacedStateId): int
    {
        return VanillaBlocks::NETHER_GOLD_ORE()->getStateId();
    }

    public function getClusterCount(): int
    {
        return 10;
    }

    public function getClusterSize(): int
    {
        return 10;
    }

    public function getMinHeight(): int
    {
        return 10;
    }

    public function getMaxHeight(): int
    {
        return 117;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;

class NetherBlackstonePopulator extends NetherGravelPopulator
{
    public function getOreBlock(int $replacedStateId): int
    {
        return VanillaBlocks::BLACKSTONE()->getStateId();
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;

abstract class Populator
{
    /**
     * Important: populators should only write inside the chunk column they're given.
     * Writing into neighbouring chunks can cause race conditions (neighbour might not be generated yet).
     */
    abstract public function populate(
        ChunkManager $world,
        int $chunkX,
        int $chunkZ,
        Random $random,
    ): void;
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;

final class AncientDebrisLargePopulator extends AncientDebrisSmallPopulator
{
    public function getOreBlock(int $replacedStateId): int
    {
        return VanillaBlocks::ANCIENT_DEBRIS()->getStateId();
    }

    public function getClusterCount(): int
    {
        return 2;
    }

    public function getClusterSize(): int
    {
        return 3;
    }

    public function getMinHeight(): int
    {
        return 23;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;

class AncientDebrisSmallPopulator extends OrePopulator
{
    public function getOreBlock(int $replacedStateId): int
    {
        return VanillaBlocks::ANCIENT_DEBRIS()->getStateId();
    }

    public function getClusterCount(): int
    {
        return 3;
    }

    public function getClusterSize(): int
    {
        return 2;
    }

    public function getMinHeight(): int
    {
        return 8;
    }

    public function getMaxHeight(): int
    {
        return 119;
    }

    public function getSkipAir(): float
    {
        return 1.0;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\Block;
use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

final class NetherVegetationPopulator extends Populator
{
    private const SURFACE_SCAN_DEPTH = 5;
    private const MAX_Y = 126;
    private const MIN_Y = 1;

    private int $plantStateId;
    private int $airId;

    /** @var array<int, true> */
    private array $allowedGroundIds;

    public function __construct(
        Block $plant,
        array $allowedGround = [],
        private int $attemptsPerChunk = 8,
        private bool $cluster = false,
        private int $clusterRadius = 4,
        private int $clusterSize = 8,
    ) {
        $this->plantStateId = $plant->getStateId();
        $this->airId = VanillaBlocks::AIR()->getStateId();

        $this->allowedGroundIds = [];
        foreach ($allowedGround as $groundBlock) {
            $this->allowedGroundIds[$groundBlock->getStateId()] = true;
        }
    }

    public function populate(
        ChunkManager $world,
        int $chunkX,
        int $chunkZ,
        Random $random,
    ): void {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        if ($this->cluster) {
            $this->populateClustered($chunk, $random);
        } else {
            $this->populateScattered($chunk, $random);
        }
    }

    private function populateScattered(Chunk $chunk, Random $random): void
    {
        for ($i = 0; $i < $this->attemptsPerChunk; ++$i) {
            $localX = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
            $localZ = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
            $this->tryPlace($chunk, $localX, $localZ);
        }
    }

    private function populateClustered(Chunk $chunk, Random $random): void
    {
        $diameter = $this->clusterRadius * 2 + 1;

        for ($i = 0; $i < $this->attemptsPerChunk; ++$i) {
            $anchorX = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
            $anchorZ = $random->nextBoundedInt(Chunk::EDGE_LENGTH);

            for ($j = 0; $j < $this->clusterSize; ++$j) {
                $localX = $anchorX + $random->nextBoundedInt($diameter) - $this->clusterRadius;
                $localZ = $anchorZ + $random->nextBoundedInt($diameter) - $this->clusterRadius;

                $localX = max(0, min(Chunk::EDGE_LENGTH - 1, $localX));
                $localZ = max(0, min(Chunk::EDGE_LENGTH - 1, $localZ));

                $this->tryPlace($chunk, $localX, $localZ);
            }
        }
    }

    private function tryPlace(Chunk $chunk, int $localX, int $localZ): void
    {
        $surfaceY = $this->findFloorSurfaceY($chunk, $localX, $localZ);
        if ($surfaceY < 0) {
            return;
        }

        $plantY = $surfaceY + 1;
        if ($plantY > self::MAX_Y) {
            return;
        }

        if ([] !== $this->allowedGroundIds) {
            $groundId = $chunk->getBlockStateId($localX, $surfaceY, $localZ);
            if (!isset($this->allowedGroundIds[$groundId])) {
                return;
            }
        }

        if ($chunk->getBlockStateId($localX, $plantY, $localZ) !== $this->airId) {
            return;
        }

        $chunk->setBlockStateId($localX, $plantY, $localZ, $this->plantStateId);
    }

    private function findFloorSurfaceY(Chunk $chunk, int $localX, int $localZ): int
    {
        for ($y = self::MAX_Y; $y >= self::MIN_Y; --$y) {
            if ($chunk->getBlockStateId($localX, $y, $localZ) === $this->airId) {
                continue;
            }
            if ($this->isTop($chunk, $localX, $y, $localZ)) {
                return $y;
            }
        }

        return -1;
    }

    private function isTop(Chunk $chunk, int $x, int $y, int $z): bool
    {
        for ($i = 1; $i <= self::SURFACE_SCAN_DEPTH; ++$i) {
            $yy = $y + $i;
            if ($yy > self::MAX_Y) {
                break;
            }
            if ($chunk->getBlockStateId($x, $yy, $z) === $this->airId) {
                return true;
            }
        }

        return false;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

final class GlowstonePopulator extends Populator
{
    private int $glowstoneId;
    private int $netherrackId;
    private int $airId;

    public function __construct(
        private readonly int $minClusterSize = 40,
        private readonly int $maxClusterSize = 60,
        private readonly int $spawnChance = 11,
    ) {
        $this->glowstoneId = VanillaBlocks::GLOWSTONE()->getStateId();
        $this->netherrackId = VanillaBlocks::NETHERRACK()->getStateId();
        $this->airId = VanillaBlocks::AIR()->getStateId();
    }

    public function populate(
        ChunkManager $world,
        int $chunkX,
        int $chunkZ,
        Random $random,
    ): void {
        if (0 !== $random->nextBoundedInt($this->spawnChance)) {
            return;
        }

        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $baseX = $chunkX << 4;
        $baseZ = $chunkZ << 4;

        $localX = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
        $localZ = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
        $worldX = $baseX + $localX;
        $worldZ = $baseZ + $localZ;

        $startY = $this->findCeilingAir($chunk, $localX, $localZ);
        if ($startY < 0) {
            return;
        }

        if ($chunk->getBlockStateId($localX, $startY, $localZ) === $this->netherrackId) {
            return;
        }

        $chunk->setBlockStateId($localX, $startY, $localZ, $this->glowstoneId);

        $count = $this->minClusterSize + $random->nextBoundedInt($this->maxClusterSize - $this->minClusterSize + 1);
        $cyclesNum = 0;

        while ($count > 0) {
            if ($cyclesNum >= 1500) {
                break;
            }

            $spawnX = $worldX + $random->nextBoundedInt(9) - $random->nextBoundedInt(9);
            $spawnY = $startY - $random->nextBoundedInt(9);
            $spawnZ = $worldZ + $random->nextBoundedInt(9) - $random->nextBoundedInt(9);

            if ($cyclesNum > 0 && 0 === $cyclesNum % 128) {
                $extraX = $worldX + $random->nextBoundedInt(7) - 3;
                $extraY = $startY - $random->nextBoundedInt(5);
                $extraZ = $worldZ + $random->nextBoundedInt(7) - 3;

                $this->setIfInChunk($world, $chunkX, $chunkZ, $extraX, $extraY, $extraZ);
                --$count;
            }

            if ($this->hasGlowstoneNeighbour($world, $spawnX, $spawnY, $spawnZ)) {
                $this->setIfInChunk($world, $chunkX, $chunkZ, $spawnX, $spawnY, $spawnZ);
                --$count;
            }

            ++$cyclesNum;
        }
    }

    private function findCeilingAir(Chunk $chunk, int $localX, int $localZ): int
    {
        for ($y = 125; $y >= 1; --$y) {
            if ($chunk->getBlockStateId($localX, $y, $localZ) === $this->airId) {
                return $y;
            }
        }

        return -1;
    }

    private function hasGlowstoneNeighbour(ChunkManager $world, int $x, int $y, int $z): bool
    {
        $offsets = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];

        foreach ($offsets as [$dx, $dy, $dz]) {
            $ny = $y + $dy;
            if ($ny < 0 || $ny > 127) {
                continue;
            }
            if ($world->getBlockAt($x + $dx, $ny, $z + $dz)->getStateId() === $this->glowstoneId) {
                return true;
            }
        }

        return false;
    }

    private function setIfInChunk(
        ChunkManager $world,
        int $chunkX,
        int $chunkZ,
        int $worldX,
        int $worldY,
        int $worldZ,
    ): void {
        if ($worldY < 1 || $worldY > 125) {
            return;
        }

        $targetChunkX = $worldX >> 4;
        $targetChunkZ = $worldZ >> 4;

        if ($targetChunkX !== $chunkX || $targetChunkZ !== $chunkZ) {
            return;
        }

        $localX = $worldX & 0xF;
        $localZ = $worldZ & 0xF;

        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        if ($chunk->getBlockStateId($localX, $worldY, $localZ) !== $this->airId) {
            return;
        }

        $chunk->setBlockStateId($localX, $worldY, $localZ, $this->glowstoneId);
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;

final class MagmaPopulator extends OrePopulator
{
    public function getOreBlock(int $replacedStateId): int
    {
        return VanillaBlocks::MAGMA()->getStateId();
    }

    public function getClusterCount(): int
    {
        return 9;
    }

    public function getClusterSize(): int
    {
        return 20;
    }

    public function getMinHeight(): int
    {
        return 23;
    }

    public function getMaxHeight(): int
    {
        return 37;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator;

use pocketmine\block\VanillaBlocks;

final class NetherQuartzOrePopulator extends OrePopulator
{
    public function getOreBlock(int $replacedStateId): int
    {
        return VanillaBlocks::NETHER_QUARTZ_ORE()->getStateId();
    }

    public function getClusterCount(): int
    {
        return 20;
    }

    public function getClusterSize(): int
    {
        return 14;
    }

    public function getMinHeight(): int
    {
        return 10;
    }

    public function getMaxHeight(): int
    {
        return 117;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator\warped;

use lolfosjo\netherx\nether\populator\Populator;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

final class WarpedTwistingVinesPopulator extends Populator
{
    private int $airId;
    private int $twistingVinesId;
    private int $warpedNyliumId;
    private int $warpedWartBlockId;

    public function __construct()
    {
        $this->airId = VanillaBlocks::AIR()->getStateId();
        $this->twistingVinesId = VanillaBlocks::TWISTING_VINES()->getStateId();
        $this->warpedNyliumId = VanillaBlocks::WARPED_NYLIUM()->getStateId();
        $this->warpedWartBlockId = VanillaBlocks::WARPED_WART_BLOCK()->getStateId();
    }

    public function populate(
        ChunkManager $world,
        int $chunkX,
        int $chunkZ,
        Random $random,
    ): void {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $amount = $random->nextBoundedInt(6) + 2;

        for ($i = 0; $i < $amount; ++$i) {
            $localX = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
            $localZ = $random->nextBoundedInt(Chunk::EDGE_LENGTH);

            if (BiomeIds::WARPED_FOREST !== $chunk->getBiomeId($localX, 2, $localZ)) {
                continue;
            }

            $workableYs = $this->getHighestWorkableBlocks($chunk, $localX, $localZ);

            foreach ($workableYs as $y) {
                if ($y <= 1) {
                    continue;
                }

                if (0 === $random->nextBoundedInt(5)) {
                    continue;
                }

                $endY = $this->getHighestEndingBlock($chunk, $localX, $y, $localZ);
                $range = $endY - $y + 1;
                $amountToPlace = $range > 0 ? $random->nextBoundedInt($range) : 0;

                for ($yPos = $y; $yPos < $y + (int) ($amountToPlace / 2); ++$yPos) {
                    if ($yPos >= 127) {
                        break;
                    }
                    $chunk->setBlockStateId($localX, $yPos, $localZ, $this->twistingVinesId);
                }
            }
        }
    }

    private function getHighestEndingBlock(Chunk $chunk, int $localX, int $startY, int $localZ): int
    {
        for ($y = $startY; $y < 128; ++$y) {
            $blockId = $chunk->getBlockStateId($localX, $y, $localZ);
            $belowId = $y > 0 ? $chunk->getBlockStateId($localX, $y - 1, $localZ) : $this->airId;

            $isSolid = $blockId !== $this->airId;
            $belowAir = $belowId === $this->airId;

            if ($belowAir && $isSolid) {
                break;
            }
        }

        return $y - 1;
    }

    private function getHighestWorkableBlocks(Chunk $chunk, int $localX, int $localZ): array
    {
        $ys = [];

        for ($y = 128; $y > 0; --$y) {
            $blockId = $chunk->getBlockStateId($localX, $y, $localZ);

            if ($blockId === $this->warpedNyliumId || $blockId === $this->warpedWartBlockId) {
                $above = $chunk->getBlockStateId($localX, $y + 1, $localZ);
                if ($above === $this->airId) {
                    $ys[] = $y + 1;
                }
            }
        }

        return $ys;
    }
}
<?php

/*
 * Derived from PowerNukkitX
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\populator\warped;

use lolfosjo\netherx\nether\populator\Populator;
use pocketmine\block\VanillaBlocks;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\object\NetherTree;

final class HugeWarpedFungusPopulator extends Populator
{
    private const MAX_Y = 126;
    private const MIN_Y = 1;

    private int $airId;
    private int $warpedNyliumId;

    public function __construct(
        private readonly int $treeHeight = 7,
    ) {
        $this->airId = VanillaBlocks::AIR()->getStateId();
        $this->warpedNyliumId = VanillaBlocks::WARPED_NYLIUM()->getStateId();
    }

    public function populate(
        ChunkManager $world,
        int $chunkX,
        int $chunkZ,
        Random $random,
    ): void {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $baseX = $chunkX << 4;
        $baseZ = $chunkZ << 4;

        $amount = $random->nextBoundedInt(6) + 4;

        for ($i = 0; $i < $amount; ++$i) {
            $localX = $random->nextBoundedInt(Chunk::EDGE_LENGTH);
            $localZ = $random->nextBoundedInt(Chunk::EDGE_LENGTH);

            $ys = $this->getHighestWorkableBlocks($chunk, $localX, $localZ);

            foreach ($ys as $y) {
                if ($y <= 1) {
                    continue;
                }
                if (1 === $random->nextBoundedInt(4)) {
                    continue;
                }

                $tree = new NetherTree(
                    VanillaBlocks::WARPED_STEM(),
                    VanillaBlocks::WARPED_WART_BLOCK(),
                    VanillaBlocks::SHROOMLIGHT(),
                    $this->treeHeight,
                    false,
                    false,
                );

                $transaction = $tree->getBlockTransaction($world, $baseX + $localX, $y, $baseZ + $localZ, $random);
                if (null === $transaction) {
                    continue;
                }

                $transaction->apply();
            }
        }
    }

    private function getHighestWorkableBlocks(Chunk $chunk, int $localX, int $localZ): array
    {
        $blockYs = [];
        for ($y = self::MAX_Y; $y > self::MIN_Y; --$y) {
            if ($chunk->getBlockStateId($localX, $y, $localZ) !== $this->warpedNyliumId) {
                continue;
            }
            if ($chunk->getBlockStateId($localX, $y + 1, $localZ) !== $this->airId) {
                continue;
            }
            $blockYs[] = $y + 1;
        }

        return $blockYs;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\biome;

final class BiomeRegistry
{
    /** @var array<int, BiomeDefinition> */
    private array $profiles = [];

    private readonly BiomeDefinition $defaultProfile;

    public function __construct()
    {
        $this->defaultProfile = BiomeDefinition::builder()->build();
    }

    public function register(int $biomeId, BiomeDefinition $profile): void
    {
        $this->profiles[$biomeId] = $profile;
    }

    public function get(int $biomeId): BiomeDefinition
    {
        return $this->profiles[$biomeId] ?? $this->defaultProfile;
    }

    public function has(int $biomeId): bool
    {
        return isset($this->profiles[$biomeId]);
    }

    public function getRegisteredBiomeIds(): array
    {
        return array_keys($this->profiles);
    }

    /** @return array<int, BiomeDefinition> biome ID => definition */
    public function all(): array
    {
        return $this->profiles;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\biome;

use lolfosjo\netherx\nether\populator\Populator;
use lolfosjo\netherx\nether\surface\SurfaceRule;

final class BiomeDefinition
{
    /** @var Populator[] */
    private array $vegetationPopulators;

    private function __construct(
        array $vegetationPopulators,
        private float $temperature,
        private float $humidity,
        private float $offset,
        private ?SurfaceRule $surfaceRule,
    ) {
        $this->vegetationPopulators = $vegetationPopulators;
    }

    /** @return Populator[] */
    public function getVegetationPopulators(): array
    {
        return $this->vegetationPopulators;
    }

    public function getTemperature(): float
    {
        return $this->temperature;
    }

    public function getHumidity(): float
    {
        return $this->humidity;
    }

    public function getOffset(): float
    {
        return $this->offset;
    }

    public function getSurfaceRule(): ?SurfaceRule
    {
        return $this->surfaceRule;
    }

    public static function builder(): self
    {
        return new self([], 0.0, 0.0, 0.0, null);
    }

    public function addVegetation(Populator $populator): self
    {
        $copy = clone $this;
        $copy->vegetationPopulators = [...$this->vegetationPopulators, $populator];

        return $copy;
    }

    public function withClimate(float $temperature, float $humidity, float $offset = 0.0): self
    {
        $copy = clone $this;
        $copy->temperature = $temperature;
        $copy->humidity = $humidity;
        $copy->offset = $offset;

        return $copy;
    }

    public function withSurfaceRule(?SurfaceRule $rule): self
    {
        $copy = clone $this;
        $copy->surfaceRule = $rule;

        return $copy;
    }

    public function build(): self
    {
        return $this;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\biome;

use lolfosjo\netherx\noise\bukkit\SimplexOctaveGenerator;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\utils\Random;
use pocketmine\world\format\Chunk;

class BiomePicker
{
    private SimplexOctaveGenerator $noiseTemperature;
    private SimplexOctaveGenerator $noiseHumidity;
    private SimplexOctaveGenerator $noiseOffset;
    private BiomeRegistry $biomeRegistry;

    public function __construct(Random $random, BiomeSizePreset $preset, BiomeRegistry $biomeRegistry)
    {
        $this->noiseTemperature = new SimplexOctaveGenerator($random, 2);
        $this->noiseTemperature->setScale($preset->temperatureScale());

        $this->noiseHumidity = new SimplexOctaveGenerator($random, 2);
        $this->noiseHumidity->setScale($preset->humidityScale());

        $this->noiseOffset = new SimplexOctaveGenerator($random, 2);
        $this->noiseOffset->setScale($preset->temperatureScale());

        $this->biomeRegistry = $biomeRegistry;
    }

    public function selectBiome(float $worldX, float $worldZ): int
    {
        $temperature = $this->noiseTemperature->noise($worldX, 0, $worldZ, 0.5, 0.5, 1.0);
        $humidity = $this->noiseHumidity->noise($worldX, 0, $worldZ, 0.5, 0.5, 1.0);
        $offset = $this->noiseOffset->noise($worldX, 0, $worldZ, 0.5, 0.5, 1.0);

        $biomeIds = $this->biomeRegistry->getRegisteredBiomeIds();

        if ([] === $biomeIds) {
            return BiomeIds::HELL;
        }

        $bestBiome = $biomeIds[0];
        $bestDistance = PHP_FLOAT_MAX;

        foreach ($biomeIds as $biomeId) {
            $definition = $this->biomeRegistry->get($biomeId);
            $distance = (($temperature - $definition->getTemperature()) ** 2)
                      + (($humidity - $definition->getHumidity()) ** 2)
                      + (($offset - $definition->getOffset()) ** 2);

            if ($distance < $bestDistance) {
                $bestDistance = $distance;
                $bestBiome = $biomeId;
            }
        }

        return $bestBiome;
    }

    public function fillChunkBiomes(Chunk $chunk, int $chunkX, int $chunkZ, int $netherHeight): array
    {
        $baseX = $chunkX * Chunk::EDGE_LENGTH;
        $baseZ = $chunkZ * Chunk::EDGE_LENGTH;
        $biomeMap = [];

        for ($x = 0; $x < Chunk::EDGE_LENGTH; ++$x) {
            for ($z = 0; $z < Chunk::EDGE_LENGTH; ++$z) {
                $biomeId = $this->selectBiome($baseX + $x, $baseZ + $z);
                $biomeMap[$x][$z] = $biomeId;

                for ($y = 0; $y < $netherHeight; ++$y) {
                    $chunk->setBiomeId($x, $y, $z, $biomeId);
                }
            }
        }

        return $biomeMap;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\biome;

use lolfosjo\netherx\nether\populator\basaltdelta\BasaltDeltaLavaPopulator;
use lolfosjo\netherx\nether\populator\basaltdelta\BasaltDeltaMagmaPopulator;
use lolfosjo\netherx\nether\populator\basaltdelta\BasaltDeltaPillarPopulator;
use lolfosjo\netherx\nether\populator\crimson\HugeCrimsonFungusPopulator;
use lolfosjo\netherx\nether\populator\crimson\WeepingVinesCeilingPopulator;
use lolfosjo\netherx\nether\populator\GlowstonePopulator;
use lolfosjo\netherx\nether\populator\NetherVegetationPopulator;
use lolfosjo\netherx\nether\populator\warped\HugeWarpedFungusPopulator;
use lolfosjo\netherx\nether\populator\warped\WarpedTwistingVinesPopulator;
use lolfosjo\netherx\nether\surface\BasaltDeltasSurfaceRule;
use lolfosjo\netherx\nether\surface\CrimsonForestSurfaceRule;
use lolfosjo\netherx\nether\surface\HellSurfaceRule;
use lolfosjo\netherx\nether\surface\SoulsandValleySurfaceRule;
use lolfosjo\netherx\nether\surface\WarpedForestSurfaceRule;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\utils\Random;

class DefaultBiomes
{
    public static function register(BiomeRegistry $registry, int $seed, Random $random): void
    {
        // TODO: rebalance these values (currently non-vanilla tuning)
        $randomHeight = $random->nextBoundedInt(6) + 7;
        $randomCluster = $random->nextBoundedInt(5) + 1;
        $glowstone = new GlowstonePopulator(
            minClusterSize: 40,
            maxClusterSize: 60,
            spawnChance: 11,
        );

        $registry->register(
            BiomeIds::HELL,
            BiomeDefinition::builder()
                ->withClimate(temperature: 0.0, humidity: 0.0, offset: 0.0)
                ->withSurfaceRule(new HellSurfaceRule())
                ->addVegetation($glowstone)
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::FIRE(),
                    allowedGround: [VanillaBlocks::NETHERRACK()],
                    attemptsPerChunk: 1,
                ))
                ->build(),
        );

        $registry->register(
            BiomeIds::SOULSAND_VALLEY,
            BiomeDefinition::builder()
                ->withClimate(temperature: 0.0, humidity: -0.5, offset: 0.0)
                ->withSurfaceRule(new SoulsandValleySurfaceRule())
                ->addVegetation($glowstone)
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::SOUL_FIRE(),
                    allowedGround: [VanillaBlocks::SOUL_SAND()],
                    attemptsPerChunk: 2,
                ))
                ->build(),
        );

        $registry->register(
            BiomeIds::CRIMSON_FOREST,
            BiomeDefinition::builder()
                ->withClimate(temperature: 0.4, humidity: 0.0, offset: 0.0)
                ->withSurfaceRule(new CrimsonForestSurfaceRule())
                ->addVegetation(new HugeCrimsonFungusPopulator(
                    treeHeight: $randomHeight,
                ))
                ->addVegetation(new WeepingVinesCeilingPopulator())
                ->addVegetation($glowstone)
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::CRIMSON_FUNGUS(),
                    allowedGround: [VanillaBlocks::CRIMSON_NYLIUM()],
                    attemptsPerChunk: 4,
                ))
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::WARPED_FUNGUS(),
                    allowedGround: [VanillaBlocks::CRIMSON_NYLIUM()],
                    attemptsPerChunk: 1,
                ))
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::CRIMSON_ROOTS(),
                    allowedGround: [VanillaBlocks::CRIMSON_NYLIUM()],
                    attemptsPerChunk: 6,
                    cluster: true,
                    clusterRadius: 4,
                    clusterSize: $randomCluster,
                ))
                ->build(),
        );

        $registry->register(
            BiomeIds::WARPED_FOREST,
            BiomeDefinition::builder()
                ->withClimate(temperature: 0.0, humidity: 0.5, offset: 0.375)
                ->withSurfaceRule(new WarpedForestSurfaceRule())
                ->addVegetation(new WarpedTwistingVinesPopulator())
                ->addVegetation(new HugeWarpedFungusPopulator(
                    treeHeight: $randomHeight,
                ))
                ->addVegetation($glowstone)
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::WARPED_FUNGUS(),
                    allowedGround: [VanillaBlocks::WARPED_NYLIUM()],
                    attemptsPerChunk: 4,
                ))
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::CRIMSON_FUNGUS(),
                    allowedGround: [VanillaBlocks::WARPED_NYLIUM()],
                    attemptsPerChunk: 1,
                ))
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::NETHER_SPROUTS(),
                    allowedGround: [VanillaBlocks::WARPED_NYLIUM()],
                    attemptsPerChunk: 8,
                ))
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::CRIMSON_ROOTS(),
                    allowedGround: [VanillaBlocks::WARPED_NYLIUM()],
                    attemptsPerChunk: 1,
                ))
                ->addVegetation(new NetherVegetationPopulator(
                    plant: VanillaBlocks::WARPED_ROOTS(),
                    allowedGround: [VanillaBlocks::WARPED_NYLIUM()],
                    attemptsPerChunk: 6,
                    cluster: true,
                    clusterRadius: 4,
                    clusterSize: $randomCluster,
                ))
                ->build(),
        );

        $registry->register(
            BiomeIds::BASALT_DELTAS,
            BiomeDefinition::builder()
                ->withClimate(temperature: -0.5, humidity: 0.0, offset: 0.175)
                ->withSurfaceRule(new BasaltDeltasSurfaceRule())
                ->addVegetation(new BasaltDeltaMagmaPopulator())
                ->addVegetation(new BasaltDeltaPillarPopulator())
                ->addVegetation(new BasaltDeltaLavaPopulator($seed))
                ->build(),
        );
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\biome;

enum BiomeSizePreset: string
{
    case SMALL = 'small';
    case MEDIUM = 'medium';
    case LARGE = 'large';

    public function temperatureScale(): float
    {
        return match ($this) {
            self::SMALL => 1 / 256.0,
            self::MEDIUM => 1 / 512.0,
            self::LARGE => 1 / 1024.0,
        };
    }

    public function humidityScale(): float
    {
        return match ($this) {
            self::SMALL => 1 / 128.0,
            self::MEDIUM => 1 / 256.0,
            self::LARGE => 1 / 512.0,
        };
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\variant;

use lolfosjo\netherx\nether\biome\BiomeSizePreset;
use lolfosjo\netherx\nether\NetherGenerator;

class NetherGeneratorLarge extends NetherGenerator
{
    protected function resolveBiomeSizePreset(): BiomeSizePreset
    {
        return BiomeSizePreset::LARGE;
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\variant;

use lolfosjo\netherx\nether\biome\BiomeSizePreset;
use lolfosjo\netherx\nether\NetherGenerator;

class NetherGeneratorSmall extends NetherGenerator
{
    protected function resolveBiomeSizePreset(): BiomeSizePreset
    {
        return BiomeSizePreset::SMALL;
    }
}
<?php

/*
 * Derived from PowerNukkitX (NetherTerrainStage.java)
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

use pocketmine\block\VanillaBlocks;
use pocketmine\world\format\Chunk;

final class CrimsonForestSurfaceRule implements SurfaceRule
{
    private int $airId;
    private int $netherWartBlockId;
    private int $crimsonNyliumId;

    public function __construct()
    {
        $this->airId = VanillaBlocks::AIR()->getStateId();
        $this->netherWartBlockId = VanillaBlocks::NETHER_WART_BLOCK()->getStateId();
        $this->crimsonNyliumId = VanillaBlocks::CRIMSON_NYLIUM()->getStateId();
    }

    public function apply(Chunk $chunk, int $x, int $y, int $z, SurfaceContext $context): void
    {
        if ($context->above === $this->airId
            && $y > 31
            && $context->stateNoise <= 0.54
        ) {
            $chunk->setBlockStateId(
                $x,
                $y,
                $z,
                $context->netherwartNoise >= 1.17 ? $this->netherWartBlockId : $this->crimsonNyliumId,
            );
        }
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

use pocketmine\world\format\Chunk;

final class SimpleSurfaceRule implements SurfaceRule
{
    public function __construct(
        private readonly ?int $topBlock = null,
        private readonly ?int $ceilingBlock = null,
        private readonly ?int $patchBlock = null,
        private readonly int $patchMinY = 30,
        private readonly int $patchMaxY = 35,
        private readonly float $patchThreshold = -0.012,
    ) {}

    public function apply(Chunk $chunk, int $x, int $y, int $z, SurfaceContext $context): void
    {
        if ($context->isCeil && null !== $this->ceilingBlock) {
            $chunk->setBlockStateId($x, $y, $z, $this->ceilingBlock);

            return;
        }

        if ($context->isTop) {
            if (null !== $this->patchBlock
                && $y >= $this->patchMinY
                && $y <= $this->patchMaxY
                && $context->patchNoise >= $this->patchThreshold
            ) {
                $chunk->setBlockStateId($x, $y, $z, $this->patchBlock);

                return;
            }

            if (null !== $this->topBlock) {
                $chunk->setBlockStateId($x, $y, $z, $this->topBlock);
            }
        }
    }
}
<?php

/*
 * Derived from PowerNukkitX (NetherTerrainStage.java)
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

use pocketmine\block\VanillaBlocks;
use pocketmine\world\format\Chunk;

final class BasaltDeltasSurfaceRule implements SurfaceRule
{
    private int $airId;
    private int $basaltId;
    private int $gravelId;
    private int $blackstoneId;

    public function __construct()
    {
        $this->airId = VanillaBlocks::AIR()->getStateId();
        $this->basaltId = VanillaBlocks::BASALT()->getStateId();
        $this->gravelId = VanillaBlocks::GRAVEL()->getStateId();
        $this->blackstoneId = VanillaBlocks::BLACKSTONE()->getStateId();
    }

    public function apply(Chunk $chunk, int $x, int $y, int $z, SurfaceContext $context): void
    {
        if ($context->isCeil) {
            $chunk->setBlockStateId($x, $y, $z, $this->basaltId);

            return;
        }

        if ($context->above === $this->airId) {
            if ($context->stateNoise >= 0
                || ($y <= 35 && $y >= 30 && $context->patchNoise >= -0.012)
            ) {
                $chunk->setBlockStateId($x, $y, $z, $this->gravelId);

                return;
            }
        }

        if ($context->isTop) {
            $chunk->setBlockStateId($x, $y, $z, $this->blackstoneId);
        }
    }
}
<?php

/*
 * Derived from PowerNukkitX (NetherTerrainStage.java)
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

use pocketmine\block\VanillaBlocks;
use pocketmine\world\format\Chunk;

final class WarpedForestSurfaceRule implements SurfaceRule
{
    private int $airId;
    private int $warpedWartBlockId;
    private int $warpedNyliumId;

    public function __construct()
    {
        $this->airId = VanillaBlocks::AIR()->getStateId();
        $this->warpedWartBlockId = VanillaBlocks::WARPED_WART_BLOCK()->getStateId();
        $this->warpedNyliumId = VanillaBlocks::WARPED_NYLIUM()->getStateId();
    }

    public function apply(Chunk $chunk, int $x, int $y, int $z, SurfaceContext $context): void
    {
        if ($context->above === $this->airId
            && $y > 31
            && $context->stateNoise <= 0.28
        ) {
            $chunk->setBlockStateId(
                $x,
                $y,
                $z,
                $context->netherwartNoise >= 1.17 ? $this->warpedWartBlockId : $this->warpedNyliumId,
            );
        }
    }
}
<?php

/*
 * Derived from PowerNukkitX (NetherTerrainStage.java)
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

use pocketmine\block\VanillaBlocks;
use pocketmine\world\format\Chunk;

final class HellSurfaceRule implements SurfaceRule
{
    private int $gravelId;
    private int $soulSandId;

    public function __construct()
    {
        $this->gravelId = VanillaBlocks::GRAVEL()->getStateId();
        $this->soulSandId = VanillaBlocks::SOUL_SAND()->getStateId();
    }

    public function apply(Chunk $chunk, int $x, int $y, int $z, SurfaceContext $context): void
    {
        if (!$context->isTop) {
            return;
        }

        if ($y > 31 && $y < 35 && $context->soulsandNoise >= -0.012) {
            $chunk->setBlockStateId($x, $y, $z, $this->gravelId);

            return;
        }

        if ($y <= 35 && $y >= 30 && $context->soulsandNoise >= -0.012) {
            $chunk->setBlockStateId($x, $y, $z, $this->soulSandId);
        }
    }
}
<?php

/*
 * Derived from PowerNukkitX (NetherTerrainStage.java)
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

use pocketmine\block\VanillaBlocks;
use pocketmine\world\format\Chunk;

final class SoulsandValleySurfaceRule implements SurfaceRule
{
    private int $soulSandId;
    private int $soulSoilId;

    public function __construct()
    {
        $this->soulSandId = VanillaBlocks::SOUL_SAND()->getStateId();
        $this->soulSoilId = VanillaBlocks::SOUL_SOIL()->getStateId();
    }

    public function apply(Chunk $chunk, int $x, int $y, int $z, SurfaceContext $context): void
    {
        if ($context->isCeil) {
            $chunk->setBlockStateId(
                $x,
                $y,
                $z,
                $context->stateNoise >= 0 ? $this->soulSandId : $this->soulSoilId,
            );

            return;
        }

        if ($context->isTop) {
            if ($context->stateNoise >= 0
                || ($y <= 35 && $y >= 30 && $context->patchNoise >= -0.012)
            ) {
                $chunk->setBlockStateId($x, $y, $z, $this->soulSandId);
            } else {
                $chunk->setBlockStateId($x, $y, $z, $this->soulSoilId);
            }
        }
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

use pocketmine\world\format\Chunk;

interface SurfaceRule
{
    public function apply(Chunk $chunk, int $x, int $y, int $z, SurfaceContext $context): void;
}
<?php

/*
 * Derived from PowerNukkitX (NetherTerrainStage.java)
 * Ported from Java to PHP and modified.
 */

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

use lolfosjo\netherx\nether\biome\BiomeRegistry;
use lolfosjo\netherx\noise\bukkit\SimplexOctaveGenerator;
use pocketmine\block\VanillaBlocks;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;

class Surface
{
    public const LAVA_LEVEL = 31;
    private const BEDROCK_FLOOR = 0;
    private const BEDROCK_CEIL = 127;
    private const SURFACE_SCAN_DEPTH = 4;

    private const SALT_NETHER_STATE = 0x4E455448;
    private const SALT_PATCH = 0x50415443;
    private const SALT_SOULSAND = 0x534F554C;
    private const SALT_NETHERWART = 0x4E455752;

    private int $bedrockId;
    private int $netherrackId;
    private int $airId;

    private SimplexOctaveGenerator $netherStateNoise;
    private SimplexOctaveGenerator $patchNoise;
    private SimplexOctaveGenerator $soulsandNoise;
    private SimplexOctaveGenerator $netherwartNoise;

    private int $bedrockRoughness;
    private BiomeRegistry $biomeRegistry;

    public function __construct(Random $noiseRand, int $bedrockRoughness, BiomeRegistry $biomeRegistry)
    {
        $this->bedrockId = VanillaBlocks::BEDROCK()->getStateId();
        $this->netherrackId = VanillaBlocks::NETHERRACK()->getStateId();
        $this->airId = VanillaBlocks::AIR()->getStateId();

        $this->bedrockRoughness = $bedrockRoughness;
        $this->biomeRegistry = $biomeRegistry;

        $baseSeed = $noiseRand->getSeed();

        $this->netherStateNoise = new SimplexOctaveGenerator(new Random($baseSeed ^ self::SALT_NETHER_STATE), 1);
        $this->netherStateNoise->setScale(1 / 64.0);

        $this->patchNoise = new SimplexOctaveGenerator(new Random($baseSeed ^ self::SALT_PATCH), 1);
        $this->patchNoise->setScale(1 / 32.0);

        $this->soulsandNoise = new SimplexOctaveGenerator(new Random($baseSeed ^ self::SALT_SOULSAND), 1);
        $this->soulsandNoise->setScale(1 / 32.0);

        $this->netherwartNoise = new SimplexOctaveGenerator(new Random($baseSeed ^ self::SALT_NETHERWART), 1);
        $this->netherwartNoise->setScale(1 / 16.0);
    }

    public function generateSurface(ChunkManager $world, int $chunkX, int $chunkZ, Random $random, array $biomeMap): void
    {
        /** @var Chunk $chunk */
        $chunk = $world->getChunk($chunkX, $chunkZ);

        $baseX = $chunkX << 4;
        $baseZ = $chunkZ << 4;

        $this->applyBiomeBlocks($chunk, $baseX, $baseZ, $biomeMap);
        $this->placeBedrock($chunk, $random);
    }

    private function applyBiomeBlocks(Chunk $chunk, int $baseX, int $baseZ, array $biomeMap): void
    {
        $minY = self::BEDROCK_FLOOR + 1;
        $maxY = self::BEDROCK_CEIL - 1;
        $depth = self::SURFACE_SCAN_DEPTH + 1;

        for ($x = 0; $x < Chunk::EDGE_LENGTH; ++$x) {
            for ($z = 0; $z < Chunk::EDGE_LENGTH; ++$z) {
                $biomeId = $biomeMap[$x][$z] ?? BiomeIds::HELL;

                $surfaceRule = $this->biomeRegistry->get($biomeId)->getSurfaceRule();
                if (null === $surfaceRule) {
                    continue;
                }

                $nx = $x + $baseX;
                $nz = $z + $baseZ;

                $ids = [];
                for ($y = $minY; $y <= $maxY; ++$y) {
                    $ids[$y] = $chunk->getBlockStateId($x, $y, $z);
                }

                $nsNoise = $this->netherStateNoise->noise($nx, 64, $nz, 0.5, 0.5, 1.0);
                $patchN = $this->patchNoise->noise($nx, 64, $nz, 0.5, 0.5, 1.0);
                $soulsandN = $this->soulsandNoise->noise($nx, 64, $nz, 0.5, 0.5, 1.0);
                $netherwartN = $this->netherwartNoise->noise($nx, 64, $nz, 0.5, 0.5, 1.0);

                for ($y = $minY; $y < $maxY; ++$y) {
                    if ($ids[$y] !== $this->netherrackId) {
                        continue;
                    }

                    $isTop = false;
                    for ($i = 0; $i < $depth; ++$i) {
                        $yy = $y + $i;
                        if ($yy >= $minY && $yy <= $maxY && ($ids[$yy] ?? $this->airId) === $this->airId) {
                            $isTop = true;

                            break;
                        }
                    }

                    $isCeil = false;
                    for ($i = 0; $i < $depth; ++$i) {
                        $yy = $y - $i;
                        if ($yy >= $minY && $yy <= $maxY && ($ids[$yy] ?? $this->airId) === $this->airId) {
                            $isCeil = true;

                            break;
                        }
                    }

                    $above = $ids[$y + 1] ?? $this->airId;
                    $below = $ids[$y - 1] ?? $this->airId;

                    $context = new SurfaceContext(
                        stateNoise: $nsNoise,
                        patchNoise: $patchN,
                        soulsandNoise: $soulsandN,
                        netherwartNoise: $netherwartN,
                        isTop: $isTop,
                        isCeil: $isCeil,
                        above: $above,
                        below: $below,
                    );

                    $surfaceRule->apply($chunk, $x, $y, $z, $context);
                }
            }
        }
    }

    private function placeBedrock(Chunk $chunk, Random $random): void
    {
        $roughness = $this->bedrockRoughness;

        for ($x = 0; $x < Chunk::EDGE_LENGTH; ++$x) {
            for ($z = 0; $z < Chunk::EDGE_LENGTH; ++$z) {
                $chunk->setBlockStateId($x, self::BEDROCK_FLOOR, $z, $this->bedrockId);
                $chunk->setBlockStateId($x, self::BEDROCK_CEIL, $z, $this->bedrockId);

                for ($i = 1; $i <= $roughness; ++$i) {
                    $y = self::BEDROCK_CEIL - $i;
                    if ($random->nextBoundedInt($roughness + 1) < ($roughness + 1 - $i)) {
                        $chunk->setBlockStateId($x, $y, $z, $this->bedrockId);
                    }
                }

                for ($i = 1; $i <= $roughness; ++$i) {
                    if ($random->nextBoundedInt($roughness + 1) < ($roughness + 1 - $i)) {
                        $chunk->setBlockStateId($x, self::BEDROCK_FLOOR + $i, $z, $this->bedrockId);
                    }
                }
            }
        }
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether\surface;

final class SurfaceContext
{
    public function __construct(
        public readonly float $stateNoise,
        public readonly float $patchNoise,
        public readonly float $soulsandNoise,
        public readonly float $netherwartNoise,
        public readonly bool $isTop,
        public readonly bool $isCeil,
        public readonly int $above,
        public readonly int $below,
    ) {}
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\nether;

use lolfosjo\netherx\nether\biome\BiomeDefinition;
use lolfosjo\netherx\nether\biome\BiomePicker;
use lolfosjo\netherx\nether\biome\BiomeRegistry;
use lolfosjo\netherx\nether\biome\BiomeSizePreset;
use lolfosjo\netherx\nether\biome\DefaultBiomes;
use lolfosjo\netherx\nether\populator\AncientDebrisLargePopulator;
use lolfosjo\netherx\nether\populator\AncientDebrisSmallPopulator;
use lolfosjo\netherx\nether\populator\LavaOrePopulator;
use lolfosjo\netherx\nether\populator\MagmaPopulator;
use lolfosjo\netherx\nether\populator\NetherBlackstonePopulator;
use lolfosjo\netherx\nether\populator\NetherGoldOrePopulator;
use lolfosjo\netherx\nether\populator\NetherGravelPopulator;
use lolfosjo\netherx\nether\populator\NetherQuartzOrePopulator;
use lolfosjo\netherx\nether\populator\Populator;
use lolfosjo\netherx\nether\surface\Surface;
use pocketmine\utils\Random;
use pocketmine\world\ChunkManager;
use pocketmine\world\format\Chunk;
use pocketmine\world\generator\Generator;

class NetherGenerator extends Generator
{
    private const NETHER_HEIGHT = 128;
    private const BEDROCK_FLOOR = 0;
    private const SEED_XOR = 0xDEADBEEF;

    private int $bedrockRoughness = 5;

    private BiomeSizePreset $biomeSizePreset;
    private Density $densityGenerator;
    private Surface $surfaceGenerator;
    private BiomePicker $biomePicker;
    private BiomeRegistry $biomeRegistry;

    /** @var Populator[] */
    private array $generationPopulators = [];

    /** @var Populator[] */
    private array $populators = [];

    public function __construct(int $seed, string $preset)
    {
        parent::__construct($seed, $preset);

        $this->biomeSizePreset = $this->resolveBiomeSizePreset();

        $noiseRand = new Random($this->random->getSeed());

        $this->densityGenerator = new Density($noiseRand);

        $this->biomeRegistry = new BiomeRegistry();
        DefaultBiomes::register($this->biomeRegistry, $this->seed, $this->random);

        $this->surfaceGenerator = new Surface($noiseRand, $this->bedrockRoughness, $this->biomeRegistry);
        $this->biomePicker = new BiomePicker($this->random, $this->biomeSizePreset, $this->biomeRegistry);

        $this->addPopulator(new NetherQuartzOrePopulator());
        $this->addPopulator(new NetherGoldOrePopulator());
        $this->addPopulator(new LavaOrePopulator());
        $this->addPopulator(new NetherGravelPopulator());
        $this->addPopulator(new NetherBlackstonePopulator());
        $this->addPopulator(new MagmaPopulator());
        $this->addPopulator(new AncientDebrisLargePopulator());
        $this->addPopulator(new AncientDebrisSmallPopulator());
    }

    public function registerBiome(int $biomeId, BiomeDefinition $definition): void
    {
        $this->biomeRegistry->register($biomeId, $definition);
    }

    public function getBiomeRegistry(): BiomeRegistry
    {
        return $this->biomeRegistry;
    }

    public function getBedrockRoughness(): int
    {
        return $this->bedrockRoughness;
    }

    public function setBedrockRoughness(int $roughness): void
    {
        $this->bedrockRoughness = $roughness;
        $this->surfaceGenerator = new Surface(
            new Random($this->random->getSeed()),
            $roughness,
            $this->biomeRegistry,
        );
    }

    public function addNetherGenerationPopulator(Populator $populator): void
    {
        $this->generationPopulators[] = $populator;
    }

    public function addPopulator(Populator $populator): void
    {
        $this->populators[] = $populator;
    }

    public function generateChunk(ChunkManager $world, int $chunkX, int $chunkZ): void
    {
        $this->random->setSeed(self::SEED_XOR ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed);

        $chunk = $world->getChunk($chunkX, $chunkZ)
            ?? throw new \InvalidArgumentException("Chunk {$chunkX} {$chunkZ} does not yet exist");

        $this->densityGenerator->generateRawTerrain($world, $chunkX, $chunkZ);
        $biomeMap = $this->biomePicker->fillChunkBiomes($chunk, $chunkX, $chunkZ, self::NETHER_HEIGHT);
        $this->surfaceGenerator->generateSurface($world, $chunkX, $chunkZ, $this->random, $biomeMap);

        foreach ($this->generationPopulators as $populator) {
            $populator->populate($world, $chunkX, $chunkZ, $this->random);
        }
    }

    public function populateChunk(ChunkManager $world, int $chunkX, int $chunkZ): void
    {
        $this->random->setSeed(self::SEED_XOR ^ ($chunkX << 8) ^ $chunkZ ^ $this->seed);

        foreach ($this->populators as $populator) {
            $populator->populate($world, $chunkX, $chunkZ, $this->random);
        }

        $this->applyVegetationPopulators($world, $chunkX, $chunkZ);
    }

    protected function resolveBiomeSizePreset(): BiomeSizePreset
    {
        return BiomeSizePreset::MEDIUM;
    }

    private function applyVegetationPopulators(ChunkManager $world, int $chunkX, int $chunkZ): void
    {
        $chunk = $world->getChunk($chunkX, $chunkZ);
        if (null === $chunk) {
            return;
        }

        $seenProfiles = [];

        for ($x = 0; $x < Chunk::EDGE_LENGTH; ++$x) {
            for ($z = 0; $z < Chunk::EDGE_LENGTH; ++$z) {
                $biomeId = $chunk->getBiomeId($x, self::BEDROCK_FLOOR + 2, $z);
                if (!isset($seenProfiles[$biomeId])) {
                    $seenProfiles[$biomeId] = $this->biomeRegistry->get($biomeId);
                }
            }
        }

        foreach ($seenProfiles as $profile) {
            foreach ($profile->getVegetationPopulators() as $populator) {
                $populator->populate($world, $chunkX, $chunkZ, $this->random);
            }
        }
    }
}
<?php

declare(strict_types=1);

namespace lolfosjo\netherx\listener;

use lolfosjo\netherx\nether\NetherGenerator;
use pocketmine\block\Bed;
use pocketmine\block\VanillaBlocks;
use pocketmine\block\Water;
use pocketmine\event\block\BlockUpdateEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\world\Explosion;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\Position;
use pocketmine\world\World;

class EventListener implements Listener
{
    public function __construct(
        private readonly bool $bedExplosionEnabled = true,
        private readonly bool $blockWaterPlacement = true,
    ) {
    }

    public function onInteract(PlayerInteractEvent $event): void
    {
        if (!$this->bedExplosionEnabled) {
            return;
        }

        if (PlayerInteractEvent::RIGHT_CLICK_BLOCK !== $event->getAction()) {
            return;
        }

        $player = $event->getPlayer();
        $world = $player->getWorld();

        if (!$this->isNetherXWorld($world)) {
            return;
        }

        $block = $event->getBlock();

        if (!$block instanceof Bed) {
            return;
        }

        $event->cancel();

        $bedPos = $block->getPosition();

        $otherHalf = $block->getOtherHalf();

        if (null !== $otherHalf) {
            $world->setBlock($otherHalf->getPosition(), VanillaBlocks::AIR());
        }

        $world->setBlock($bedPos, VanillaBlocks::AIR());

        $explosion = new Explosion(
            Position::fromObject($bedPos, $world),
            5.0,
            $player,
        );

        $explosion->explodeA();
        $explosion->explodeB();
    }

    public function onBlockUpdate(BlockUpdateEvent $event): void
    {
        if (!$this->blockWaterPlacement) {
            return;
        }

        $block = $event->getBlock();

        if (!$block instanceof Water) {
            return;
        }

        $world = $block->getPosition()->getWorld();

        if (!$this->isNetherXWorld($world)) {
            return;
        }

        $world->setBlock($block->getPosition(), VanillaBlocks::AIR());
    }

    private function isNetherXWorld(World $world): bool
    {
        $generatorName = strtolower($world->getProvider()->getWorldData()->getGenerator());
        $registeredName = GeneratorManager::getInstance()->getGeneratorName(NetherGenerator::class);

        return $generatorName === $registeredName;
    }
}name: NetherX
version: 1.1-ALPHA
author: Lolfosjo
main: lolfosjo\netherx\Main
api: 5.0.0~ãë÷fÆù3\2’˜XòŒR´pÍ/G(|t¿¶½~   GBMB