Minecraft.psm1

function ConvertFrom-GameTicks {
    <#
    .SYNOPSIS
        Converts a number of Minecraft game ticks into the equivalent real-world time span.
 
    .DESCRIPTION
        The ConvertFrom-GameTicks cmdlet converts a number of game ticks into a [timespan] representing how
        long that is in the real world, at the game's standard rate of 20 ticks per second.
 
    .EXAMPLE
        ConvertFrom-GameTicks -Ticks 24000
 
        Returns a TimeSpan of 00:20:00, i.e. the length of one full Minecraft day/night cycle.
 
    .EXAMPLE
        24000 | ConvertFrom-GameTicks
 
        -Ticks can also be supplied via the pipeline.
    #>

    [CmdletBinding()]
    [OutputType([timespan])]
    Param(
        # The number of game ticks to convert.
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [long]
        $Ticks
    )
    Process {
        [timespan]::FromSeconds($Ticks / 20)
    }
}
function ConvertTo-ChunkCoordinates {
    <#
    .SYNOPSIS
        Converts block coordinates to the coordinates of the chunk that contains them.
 
    .DESCRIPTION
        The ConvertTo-ChunkCoordinates cmdlet converts a pair of block X/Z coordinates into the ChunkX/
        ChunkZ coordinates of the 16x16 chunk that contains them, by dividing each value by 16. The result
        is rounded down to the nearest whole number, matching the game's own chunk indexing.
 
    .EXAMPLE
        ConvertTo-ChunkCoordinates -X 48 -Z -5
 
        Returns the chunk coordinates that contain block X 48, Z -5, i.e. ChunkX 3, ChunkZ -1.
 
    .EXAMPLE
        ConvertTo-ChunkCoordinates 1600 240
 
        X and Z are positional, so they don't need to be named explicitly.
 
    .EXAMPLE
        [pscustomobject]@{ X = 48; Z = -5 } | ConvertTo-ChunkCoordinates
 
        X and Z can also be supplied via the pipeline by property name.
 
    .EXAMPLE
        ConvertTo-ChunkCoordinates -X 48 -Z -5 | Test-SlimeChunk -Seed 12345
 
        The returned ChunkX/ChunkZ properties can be piped directly into Test-SlimeChunk.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    Param(
        # The block X coordinate to convert.
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
        [double]
        $X,

        # The block Z coordinate to convert.
        [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
        [double]
        $Z
    )
    Process {
        [pscustomobject]@{
            ChunkX = [int][Math]::Floor($X / 16)
            ChunkZ = [int][Math]::Floor($Z / 16)
        }
    }
}
function ConvertTo-ExperienceLevel {
    <#
    .SYNOPSIS
        Converts a total experience point count into the equivalent Minecraft experience level.
 
    .DESCRIPTION
        The ConvertTo-ExperienceLevel cmdlet converts a player's total accumulated experience points into
        their current experience level, and how far through that level they are (Progress, a value between
        0 and 1), using the same formula the game itself uses to calculate the points required for each
        level.
 
    .EXAMPLE
        ConvertTo-ExperienceLevel -Points 1200
 
        Returns the level and progress equivalent to 1200 total experience points.
 
    .EXAMPLE
        1200 | ConvertTo-ExperienceLevel
 
        -Points can also be supplied via the pipeline.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    Param(
        # The total accumulated experience points to convert.
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateRange(0, 2147483647)]
        [int]
        $Points
    )
    Process {
        $Level = 0
        while ((ConvertTo-ExperiencePoints -Level ($Level + 1)) -le $Points) {
            $Level++
        }

        $LevelStart = ConvertTo-ExperiencePoints -Level $Level
        $NextLevelStart = ConvertTo-ExperiencePoints -Level ($Level + 1)

        [pscustomobject]@{
            Level    = $Level
            Progress = [Math]::Round(($Points - $LevelStart) / ($NextLevelStart - $LevelStart), 4)
        }
    }
}
function ConvertTo-ExperiencePoints {
    <#
    .SYNOPSIS
        Converts a Minecraft experience level into the total experience points required to reach it.
 
    .DESCRIPTION
        The ConvertTo-ExperiencePoints cmdlet converts an experience level into the total number of
        experience points a player must have accumulated to reach the start of that level, using the same
        piecewise formula the game itself uses (one formula for levels 0-15, another for 16-30, and another
        for 31 and above).
 
    .EXAMPLE
        ConvertTo-ExperiencePoints -Level 30
 
        Returns 1395, the total experience points required to reach level 30.
 
    .EXAMPLE
        30 | ConvertTo-ExperiencePoints
 
        -Level can also be supplied via the pipeline.
    #>

    [CmdletBinding()]
    [OutputType([int])]
    Param(
        # The experience level to convert.
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateRange(0, 2147483647)]
        [int]
        $Level
    )
    Process {
        if ($Level -le 15) {
            [int]([Math]::Pow($Level, 2) + (6 * $Level))
        }
        elseif ($Level -le 30) {
            [int]((2.5 * [Math]::Pow($Level, 2)) - (40.5 * $Level) + 360)
        }
        else {
            [int]((4.5 * [Math]::Pow($Level, 2)) - (162.5 * $Level) + 2220)
        }
    }
}
function ConvertTo-GameTicks {
    <#
    .SYNOPSIS
        Converts a real-world time span into the equivalent number of Minecraft game ticks.
 
    .DESCRIPTION
        The ConvertTo-GameTicks cmdlet converts a [timespan] into the number of game ticks it represents,
        at the game's standard rate of 20 ticks per second.
 
    .EXAMPLE
        ConvertTo-GameTicks -Time (New-TimeSpan -Minutes 20)
 
        Returns 24000, the number of ticks in one full Minecraft day/night cycle.
 
    .EXAMPLE
        New-TimeSpan -Seconds 30 | ConvertTo-GameTicks
 
        -Time can also be supplied via the pipeline.
    #>

    [CmdletBinding()]
    [OutputType([long])]
    Param(
        # The real-world time span to convert.
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [timespan]
        $Time
    )
    Process {
        [long][Math]::Round($Time.TotalSeconds * 20)
    }
}
function ConvertTo-NetherCoordinates {
    <#
    .SYNOPSIS
        Converts Overworld coordinates to their equivalent Nether coordinates.
 
    .DESCRIPTION
        The ConvertTo-NetherCoordinates cmdlet converts a pair of Overworld X/Z coordinates into the equivalent
        Nether coordinates, by dividing each value by 8. The result is rounded down to the nearest whole
        number (block coordinates in Minecraft are always whole numbers), matching the game's own conversion
        behavior.
 
    .EXAMPLE
        ConvertTo-NetherCoordinates -X 800 -Z -400
 
        Returns the Nether coordinates equivalent to Overworld coordinates X 800, Z -400, i.e. X 100, Z -50.
 
    .EXAMPLE
        ConvertTo-NetherCoordinates 1600 240
 
        X and Z are positional, so they don't need to be named explicitly.
 
    .EXAMPLE
        [pscustomobject]@{ X = 240; Z = 88 } | ConvertTo-NetherCoordinates
 
        X and Z can also be supplied via the pipeline by property name, e.g. from an object such as one
        recording a player's Overworld position.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    Param(
        # The Overworld X coordinate to convert.
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
        [double]
        $X,

        # The Overworld Z coordinate to convert.
        [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
        [double]
        $Z
    )
    Process {
        [pscustomobject]@{
            X = [int][Math]::Floor($X / 8)
            Z = [int][Math]::Floor($Z / 8)
        }
    }
}
function ConvertTo-OverworldCoordinates {
    <#
    .SYNOPSIS
        Converts Nether coordinates to their equivalent Overworld coordinates.
 
    .DESCRIPTION
        The ConvertTo-OverworldCoordinates cmdlet converts a pair of Nether X/Z coordinates into the equivalent
        Overworld coordinates, by multiplying each value by 8, reflecting the Nether's 1:8 scale relative to
        the Overworld.
 
    .EXAMPLE
        ConvertTo-OverworldCoordinates -X 100 -Z -50
 
        Returns the Overworld coordinates equivalent to Nether coordinates X 100, Z -50, i.e. X 800, Z -400.
 
    .EXAMPLE
        ConvertTo-OverworldCoordinates 200 30
 
        X and Z are positional, so they don't need to be named explicitly.
 
    .EXAMPLE
        [pscustomobject]@{ X = 30; Z = 11 } | ConvertTo-OverworldCoordinates
 
        X and Z can also be supplied via the pipeline by property name, e.g. from an object such as one
        recording a player's Nether position.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    Param(
        # The Nether X coordinate to convert.
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
        [double]
        $X,

        # The Nether Z coordinate to convert.
        [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
        [double]
        $Z
    )
    Process {
        [pscustomobject]@{
            X = [int]($X * 8)
            Z = [int]($Z * 8)
        }
    }
}
function ConvertTo-RegionCoordinates {
    <#
    .SYNOPSIS
        Converts block coordinates to the coordinates of the region file that contains them.
 
    .DESCRIPTION
        The ConvertTo-RegionCoordinates cmdlet converts a pair of block X/Z coordinates into the
        RegionX/RegionZ coordinates of the 32x32 chunk (512x512 block) region that contains them, by
        dividing each value by 512. The result is rounded down to the nearest whole number, matching the
        game's own region file naming (e.g. r.0.0.mca).
 
    .EXAMPLE
        ConvertTo-RegionCoordinates -X 600 -Z -5
 
        Returns the region coordinates that contain block X 600, Z -5, i.e. RegionX 1, RegionZ -1.
 
    .EXAMPLE
        ConvertTo-RegionCoordinates 1600 240
 
        X and Z are positional, so they don't need to be named explicitly.
 
    .EXAMPLE
        [pscustomobject]@{ X = 600; Z = -5 } | ConvertTo-RegionCoordinates
 
        X and Z can also be supplied via the pipeline by property name.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    Param(
        # The block X coordinate to convert.
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
        [double]
        $X,

        # The block Z coordinate to convert.
        [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
        [double]
        $Z
    )
    Process {
        [pscustomobject]@{
            RegionX = [int][Math]::Floor($X / 512)
            RegionZ = [int][Math]::Floor($Z / 512)
        }
    }
}
function Get-Distance {
    <#
    .SYNOPSIS
        Calculates the horizontal block distance between two sets of coordinates.
 
    .DESCRIPTION
        The Get-Distance cmdlet calculates the straight-line distance between two X/Z coordinate pairs,
        such as those returned by New-Coordinates, ConvertTo-NetherCoordinates or
        ConvertTo-OverworldCoordinates.
 
    .EXAMPLE
        Get-Distance -From (New-Coordinates -X 0 -Z 0) -To (New-Coordinates -X 300 -Z 400)
 
        Returns 500, the distance between the two points.
 
    .EXAMPLE
        New-Coordinates -X 0 -Z 0 | Get-Distance -To (New-Coordinates -X 300 -Z 400)
 
        -From can also be supplied via the pipeline.
    #>

    [CmdletBinding()]
    [OutputType([double])]
    Param(
        # The coordinates to measure from.
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [pscustomobject]
        $From,

        # The coordinates to measure to.
        [Parameter(Mandatory, Position = 1)]
        [pscustomobject]
        $To
    )
    Process {
        [Math]::Round([Math]::Sqrt([Math]::Pow($To.X - $From.X, 2) + [Math]::Pow($To.Z - $From.Z, 2)), 2)
    }
}
function Get-MinecraftDefault {
    <#
    .SYNOPSIS
        Gets the default values previously stored by Set-MinecraftDefault.
 
    .DESCRIPTION
        The Get-MinecraftDefault cmdlet reads the settings stored by Set-MinecraftDefault from
        $HOME/.minecraft/defaults.json, e.g. a default Seed or Edition that Test-SlimeChunk falls back to
        when -Seed or -Edition isn't supplied explicitly. Returns $null (or, with no -Name, an empty
        object) if no defaults have been stored yet.
 
    .EXAMPLE
        Get-MinecraftDefault
 
        Returns all currently stored default values.
 
    .EXAMPLE
        Get-MinecraftDefault -Name Seed
 
        Returns just the stored default Seed value, or $null if one hasn't been set.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    Param(
        # The name of a specific default value to return. If omitted, all stored defaults are returned.
        [Parameter(Position = 0)]
        [string]
        $Name
    )
    $Path = Join-Path -Path $HOME -ChildPath '.minecraft/defaults.json'

    $Defaults = if (Test-Path -Path $Path) {
        Get-Content -Path $Path -Raw | ConvertFrom-Json
    }
    else {
        [pscustomobject]@{}
    }

    if ($Name) {
        $Defaults.$Name
    }
    else {
        $Defaults
    }
}
function New-Coordinates {
    <#
    .SYNOPSIS
        Creates a coordinates object with X and Z properties.
 
    .DESCRIPTION
        The New-Coordinates cmdlet returns a PowerShell object with X and Z properties, for use as input to
        the other coordinate conversion cmdlets in this module, or anywhere else a simple X/Z position is
        needed.
 
    .EXAMPLE
        New-Coordinates -X 800 -Z -400
 
        Returns a coordinates object with X 800, Z -400.
 
    .EXAMPLE
        New-Coordinates 1600 240
 
        X and Z are positional, so they don't need to be named explicitly.
 
    .EXAMPLE
        New-Coordinates -X 800 -Z -400 | ConvertTo-NetherCoordinates
 
        The returned object can be passed directly into ConvertTo-NetherCoordinates or
        ConvertTo-OverworldCoordinates via the pipeline.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Creates an in-memory object only; there is no system state to confirm changing.')]
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    Param(
        # The X coordinate.
        [Parameter(Mandatory, Position = 0)]
        [double]
        $X,

        # The Z coordinate.
        [Parameter(Mandatory, Position = 1)]
        [double]
        $Z
    )
    [pscustomobject]@{
        X = $X
        Z = $Z
    }
}
function Set-MinecraftDefault {
    <#
    .SYNOPSIS
        Stores default values that other cmdlets in this module fall back to when not supplied explicitly.
 
    .DESCRIPTION
        The Set-MinecraftDefault cmdlet saves settings to $HOME/.minecraft/defaults.json so they don't
        need to be supplied on every call. Currently supports:
 
        - A default Seed, which Test-SlimeChunk uses automatically when -Seed isn't supplied explicitly
          (Java Edition only).
        - A default Edition, which Test-SlimeChunk uses automatically when -Edition isn't supplied
          explicitly.
 
        At least one of -Seed or -Edition must be supplied. Either can be set independently -- supplying
        just one leaves any previously stored value for the other untouched.
 
    .EXAMPLE
        Set-MinecraftDefault -Seed 12345
 
        Stores 12345 as the default Seed, so e.g. Test-SlimeChunk -X 48 -Z -32 uses it automatically.
 
    .EXAMPLE
        Set-MinecraftDefault -Edition Bedrock
 
        Stores Bedrock as the default Edition, so Test-SlimeChunk uses it automatically without needing
        -Edition Bedrock on every call.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([pscustomobject])]
    Param(
        # The default world seed to store, for use by cmdlets that accept a -Seed parameter.
        [Parameter()]
        [long]
        $Seed,

        # The default edition to store, for use by cmdlets that accept an -Edition parameter.
        [Parameter()]
        [ValidateSet('Java', 'Bedrock')]
        [string]
        $Edition
    )
    if (-not $PSBoundParameters.ContainsKey('Seed') -and -not $PSBoundParameters.ContainsKey('Edition')) {
        throw 'At least one of -Seed or -Edition must be supplied.'
    }

    $Directory = Join-Path -Path $HOME -ChildPath '.minecraft'
    $Path = Join-Path -Path $Directory -ChildPath 'defaults.json'

    $Defaults = if (Test-Path -Path $Path) {
        Get-Content -Path $Path -Raw | ConvertFrom-Json
    }
    else {
        [pscustomobject]@{}
    }

    $Changes = [System.Collections.Generic.List[string]]::new()

    if ($PSBoundParameters.ContainsKey('Seed')) {
        $Defaults | Add-Member -MemberType NoteProperty -Name 'Seed' -Value $Seed -Force
        $Changes.Add("Seed to $Seed")
    }
    if ($PSBoundParameters.ContainsKey('Edition')) {
        $Defaults | Add-Member -MemberType NoteProperty -Name 'Edition' -Value $Edition -Force
        $Changes.Add("Edition to $Edition")
    }

    if ($PSCmdlet.ShouldProcess($Path, "Set default $($Changes -join ' and ')")) {
        if (-not (Test-Path -Path $Directory)) {
            New-Item -Path $Directory -ItemType Directory -Force | Out-Null
        }

        $Defaults | ConvertTo-Json | Set-Content -Path $Path
        $Defaults
    }
}
function Test-SlimeChunk {
    <#
    .SYNOPSIS
        Tests whether the chunk containing a block coordinate is a slime chunk.
 
    .DESCRIPTION
        The Test-SlimeChunk cmdlet converts a pair of block X/Z coordinates to the chunk that contains
        them (as ConvertTo-ChunkCoordinates does), then tests whether that chunk is a slime chunk, using
        either edition's own algorithm:
 
        - Java (the default): seeds a java.util.Random instance from the world seed and the chunk's
          coordinates, and treats the chunk as a slime chunk if the first number it draws between 0 and 9
          is 0 (a 10% chance per chunk). If -Seed isn't supplied, the default Seed previously stored via
          Set-MinecraftDefault is used instead.
 
        - Bedrock: hashes only the chunk's coordinates (the world seed plays no part) and runs that
          through a Mersenne Twister PRNG, with the same 1-in-10 check. Because the world seed isn't
          used, slime chunks sit at the same coordinates in every Bedrock world, and -Seed (explicit or
          default) is ignored.
 
        If -Edition isn't supplied, the default Edition previously stored via Set-MinecraftDefault is
        used instead, falling back to Java if no default Edition has been stored either.
 
        Slimes can otherwise only naturally spawn in swamp biomes, so slime chunks are the usual way to
        farm them below layer 40 elsewhere.
 
    .EXAMPLE
        Test-SlimeChunk -Seed 12345 -X 48 -Z -32
 
        Returns whether the chunk containing block X 48, Z -32 is a slime chunk for Java world seed 12345.
 
    .EXAMPLE
        Test-SlimeChunk -Edition Bedrock -X 48 -Z -32
 
        Returns whether the chunk containing block X 48, Z -32 is a slime chunk in Bedrock Edition. No
        -Seed is needed, since Bedrock slime chunks don't depend on the world seed.
 
    .EXAMPLE
        [pscustomobject]@{ X = 48; Z = -32 } | Test-SlimeChunk -Seed 12345
 
        X and Z can also be supplied via the pipeline by property name, e.g. from New-Coordinates,
        ConvertTo-NetherCoordinates or ConvertTo-OverworldCoordinates.
 
    .EXAMPLE
        Set-MinecraftDefault -Seed 12345
        Test-SlimeChunk -X 48 -Z -32
 
        Once a default Seed has been stored, -Seed can be omitted (Java only).
 
    .EXAMPLE
        Set-MinecraftDefault -Edition Bedrock
        Test-SlimeChunk -X 48 -Z -32
 
        Once a default Edition has been stored, -Edition can be omitted too.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    Param(
        # The block X coordinate to test.
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
        [double]
        $X,

        # The block Z coordinate to test.
        [Parameter(Mandatory, Position = 1, ValueFromPipelineByPropertyName)]
        [double]
        $Z,

        # The world seed. Only used for the Java edition; if omitted there, the default Seed stored via
        # Set-MinecraftDefault is used instead.
        [Parameter(Position = 2)]
        [long]
        $Seed,

        # Which edition's slime-chunk algorithm to use. If omitted, the default Edition stored via
        # Set-MinecraftDefault is used, falling back to Java if no default Edition has been stored either.
        [ValidateSet('Java', 'Bedrock')]
        [string]
        $Edition
    )
    Begin {
        if ($PSBoundParameters.ContainsKey('Edition')) {
            $EffectiveEdition = $Edition
        }
        else {
            $DefaultEdition = Get-MinecraftDefault -Name 'Edition'
            $EffectiveEdition = if ($DefaultEdition) { $DefaultEdition } else { 'Java' }
        }

        if ($EffectiveEdition -eq 'Java') {
            if ($PSBoundParameters.ContainsKey('Seed')) {
                $EffectiveSeed = $Seed
            }
            else {
                $DefaultSeed = Get-MinecraftDefault -Name 'Seed'
                if ($null -eq $DefaultSeed) {
                    throw 'No -Seed was supplied and no default Seed has been set. Use Set-MinecraftDefault -Seed <value> to set one.'
                }
                $EffectiveSeed = [long]$DefaultSeed
            }
        }
    }
    Process {
        $ChunkX = [int][Math]::Floor($X / 16)
        $ChunkZ = [int][Math]::Floor($Z / 16)
        $Mask32 = [System.Numerics.BigInteger]::Pow(2, 32)

        if ($EffectiveEdition -eq 'Java') {
            # Minecraft derives the per-chunk seed using Java's wrapping 32-bit integer arithmetic, so
            # each intermediate multiplication below is explicitly wrapped to 32 bits to match it -- using
            # the true (unwrapped) product at those steps would produce a different, incorrect seed.
            $Half32 = [System.Numerics.BigInteger]::Pow(2, 31)

            function ConvertTo-WrappedInt32 {
                param([System.Numerics.BigInteger]$Value)
                $Wrapped = (($Value % $Mask32) + $Mask32) % $Mask32
                if ($Wrapped -ge $Half32) { $Wrapped - $Mask32 } else { $Wrapped }
            }

            $ChunkXSquared = ConvertTo-WrappedInt32 ([System.Numerics.BigInteger]$ChunkX * $ChunkX)
            $ChunkZSquared = ConvertTo-WrappedInt32 ([System.Numerics.BigInteger]$ChunkZ * $ChunkZ)

            $Term1 = ConvertTo-WrappedInt32 ($ChunkXSquared * 0x4c1906)
            $Term2 = ConvertTo-WrappedInt32 ([System.Numerics.BigInteger]$ChunkX * 0x5ac0db)
            $Term3 = $ChunkZSquared * [System.Numerics.BigInteger]0x4307a7
            $Term4 = ConvertTo-WrappedInt32 ([System.Numerics.BigInteger]$ChunkZ * 0x5f24f)

            $ChunkSeed = ([System.Numerics.BigInteger]$EffectiveSeed + $Term1 + $Term2 + $Term3 + $Term4) -bxor 0x3ad8025f

            # Java's java.util.Random linear congruential generator.
            $Multiplier = [System.Numerics.BigInteger]0x5DEECE66D
            $Mask48 = ([System.Numerics.BigInteger]1 -shl 48) - 1

            $State = ($ChunkSeed -bxor $Multiplier) -band $Mask48
            $State = ($State * $Multiplier + 0xB) -band $Mask48

            # Equivalent to Random.nextInt(10): take the top 31 bits of the state and reduce modulo 10.
            $Bits = [int]($State -shr 17)
            ($Bits % 10) -eq 0
        }
        else {
            # Bedrock doesn't use the world seed at all -- it hashes only the chunk coordinates, so the
            # same chunk is a slime chunk (or isn't) in every Bedrock world.
            $ProductUnsigned = ((([System.Numerics.BigInteger]$ChunkX * 0x1f1f1f1f) % $Mask32) + $Mask32) % $Mask32
            $ChunkZUnsigned = (([System.Numerics.BigInteger]$ChunkZ % $Mask32) + $Mask32) % $Mask32
            $BedrockSeed = $ProductUnsigned -bxor $ChunkZUnsigned

            $Value = Get-Mt19937FirstUInt32 -Seed $BedrockSeed
            ($Value % 10) -eq 0
        }
    }
}
function Get-Mt19937FirstUInt32 {
    <#
    .SYNOPSIS
        Returns the first 32-bit value produced by a Mersenne Twister (MT19937) PRNG for a given seed.
 
    .DESCRIPTION
        Implements the standard MT19937 algorithm just far enough to extract the very first tempered
        output, which is all that Test-SlimeChunk needs for Bedrock Edition's slime-chunk check. A full
        implementation would track an index and lazily re-twist every 624 values, but since only one
        value is ever drawn here, the state is seeded, twisted once, and the first word is tempered and
        returned directly.
    #>

    [CmdletBinding()]
    [OutputType([System.Numerics.BigInteger])]
    Param(
        # The 32-bit (0 to 2^32-1) seed to initialize the generator with.
        [Parameter(Mandatory)]
        [System.Numerics.BigInteger]
        $Seed
    )
    # A hex literal with its top bit set (e.g. 0x80000000) parses as a *signed* Int32 in PowerShell, so
    # casting it straight to BigInteger carries that sign through as infinite-precision two's complement --
    # corrupting every -band/-bxor against a value with bits set above position 31. The L suffix forces
    # Int64 parsing first, which (unlike the cast) correctly zero-extends the 32-bit pattern as positive.
    $Mask32 = [System.Numerics.BigInteger]::Pow(2, 32)
    $UpperMask = [System.Numerics.BigInteger]0x80000000L
    $LowerMask = [System.Numerics.BigInteger]0x7fffffffL
    $MatrixA = [System.Numerics.BigInteger]0x9908b0dfL

    $StateSize = 624
    $Offset = 397

    $State = [System.Numerics.BigInteger[]]::new($StateSize)
    $State[0] = $Seed % $Mask32
    for ($i = 1; $i -lt $StateSize; $i++) {
        $Previous = $State[$i - 1]
        $State[$i] = ((1812433253 * ($Previous -bxor ($Previous -shr 30))) + $i) % $Mask32
    }

    # The standard twist pass, applied once since only the first output word is ever needed.
    for ($i = 0; $i -lt $StateSize; $i++) {
        $Combined = ($State[$i] -band $UpperMask) -bor ($State[($i + 1) % $StateSize] -band $LowerMask)
        $Shifted = $Combined -shr 1
        if (($Combined % 2) -ne 0) {
            $Shifted = $Shifted -bxor $MatrixA
        }
        $State[$i] = $State[($i + $Offset) % $StateSize] -bxor $Shifted
    }

    $Value = $State[0]
    $Value = $Value -bxor ($Value -shr 11)
    $Value = $Value -bxor (($Value -shl 7) -band 0x9D2C5680L)
    $Value = $Value -bxor (($Value -shl 15) -band 0xEFC60000L)
    $Value = $Value -bxor ($Value -shr 18)
    $Value % $Mask32
}