world: place ruined portal pieces into the region replay
PlaceRuinedPortalPiece runs the template through the decoded processor stack — positional Legacy streams per cell drive the gold-to-air, lava-to-magma, netherrack-to-magma and block-age mossiness rolls, the features-cannot-replace guard skips protected cells, and template lava stays when it lands in existing lava. Drip columns below the portal draw from a documented approximation of the shared decoration stream; spreadNetherrack is not needed for the underground setups. The placement chain is now proven end to end: chunk (1,0) reproduces vanilla's saved start (portal_6, CLOCKWISE_90, NONE, air pocket at (16,12,0)) and the fixture parity ticks up to 98.033% with biomes and heightmaps still exact. generatorVersion bumps to 23.
This commit is contained in:
parent
0bb480279f
commit
120531c13d
10 changed files with 366 additions and 14 deletions
192
internal/world/ruined_portal_piece.go
Normal file
192
internal/world/ruined_portal_piece.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"regionio/internal/worldgen"
|
||||
)
|
||||
|
||||
// ruined_portal_piece.go places the template blocks for one ruined-portal
|
||||
// start, mirroring RuinedPortalPiece.postProcess through its processor stack.
|
||||
//
|
||||
// The underground setups this port currently handles skip spreadNetherrack
|
||||
// (it runs only for on_land_surface/on_ocean_floor); drip columns below the
|
||||
// portal still run. Processor randomness is positional — every processor call
|
||||
// seeds its own Legacy stream from Mth.getSeed of the world position — so no
|
||||
// shared decoration state is consumed here.
|
||||
|
||||
var (
|
||||
ruinedGoldID uint16
|
||||
ruinedLavaID uint16
|
||||
ruinedMagmaID uint16
|
||||
ruinedNetherrack uint16
|
||||
ruinedAirID uint16
|
||||
ruinedCaveAirID uint16
|
||||
ruinedObsidianID uint16
|
||||
|
||||
stoneBricksID uint16
|
||||
stoneID uint16
|
||||
chiseledStoneBricks uint16
|
||||
crackedStoneBricksID uint16
|
||||
mossyStoneBricksID uint16
|
||||
cryingObsidianID uint16
|
||||
|
||||
ruinedStatesOnce sync.Once
|
||||
)
|
||||
|
||||
func initRuinedPieceStates() {
|
||||
ruinedStatesOnce.Do(func() {
|
||||
stateByIDOnce.Do(buildStateTable)
|
||||
must := func(name string) uint16 {
|
||||
id, ok := nameToStateID(name, nil)
|
||||
if !ok {
|
||||
panic("world: missing state for ruined portal piece: " + name)
|
||||
}
|
||||
return id
|
||||
}
|
||||
ruinedGoldID = must("minecraft:gold_block")
|
||||
ruinedLavaID = must("minecraft:lava")
|
||||
ruinedMagmaID = must("minecraft:magma_block")
|
||||
ruinedNetherrack = must("minecraft:netherrack")
|
||||
ruinedAirID = must("minecraft:air")
|
||||
ruinedCaveAirID = must("minecraft:cave_air")
|
||||
ruinedObsidianID = must("minecraft:obsidian")
|
||||
stoneBricksID = must("minecraft:stone_bricks")
|
||||
stoneID = must("minecraft:stone")
|
||||
chiseledStoneBricks = must("minecraft:chiseled_stone_bricks")
|
||||
crackedStoneBricksID = must("minecraft:cracked_stone_bricks")
|
||||
mossyStoneBricksID = must("minecraft:mossy_stone_bricks")
|
||||
cryingObsidianID = must("minecraft:crying_obsidian")
|
||||
})
|
||||
}
|
||||
|
||||
// PlaceRuinedPortalPiece writes one portal into the region. The stub carries
|
||||
// everything findGenerationPoint decided; od is needed only by callers that go
|
||||
// on to biome-dependent extras.
|
||||
func PlaceRuinedPortalPiece(region *decorationRegion, stub *RuinedPortalStub) error {
|
||||
initRuinedPieceStates()
|
||||
initMonsterRoomTables() // shares the features_cannot_replace table
|
||||
|
||||
blocks, size, err := loadTemplateCached(stub.Template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pivot := [3]int{size[0] / 2, 0, size[2] / 2}
|
||||
mirror := stub.Mirror
|
||||
if mirror == "" {
|
||||
mirror = "none"
|
||||
}
|
||||
|
||||
placeCell := func(localPos [3]int, state uint16) bool {
|
||||
p := worldgen.TransformBlockPos(localPos, mirror, stub.Rotation, pivot)
|
||||
x, y, z := stub.X+p[0], stub.Y+p[1], stub.Z+p[2]
|
||||
if monsterCannotTable[region.getBlock(x, y, z)] {
|
||||
return false
|
||||
}
|
||||
return region.setBlock(x, y, z, state)
|
||||
}
|
||||
|
||||
processState := func(x, y, z int, localPos [3]int, state uint16) uint16 {
|
||||
seed := worldgen.MthGetSeed(x, y, z)
|
||||
roll := func(p float32) bool {
|
||||
r := worldgen.NewLegacy(seed)
|
||||
return r.NextFloat() < p
|
||||
}
|
||||
switch state {
|
||||
case ruinedGoldID:
|
||||
if roll(0.3) {
|
||||
return ruinedAirID
|
||||
}
|
||||
case ruinedLavaID:
|
||||
switch {
|
||||
case stub.Cold:
|
||||
return ruinedNetherrack
|
||||
case roll(0.2):
|
||||
return ruinedMagmaID
|
||||
}
|
||||
case ruinedNetherrack:
|
||||
if roll(0.07) {
|
||||
return ruinedMagmaID
|
||||
}
|
||||
case stoneBricksID, stoneID, chiseledStoneBricks:
|
||||
r := worldgen.NewLegacy(seed)
|
||||
if r.NextFloat() >= 0.5 {
|
||||
break
|
||||
}
|
||||
if r.NextFloat() < stub.Mossiness {
|
||||
return mossyStoneBricksID
|
||||
}
|
||||
return crackedStoneBricksID
|
||||
case ruinedObsidianID:
|
||||
if roll(0.15) {
|
||||
return cryingObsidianID
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// Two passes like buildInfoList: solids land before any template air.
|
||||
for _, passAir := range []bool{false, true} {
|
||||
for _, b := range blocks {
|
||||
isAirLocal := b.State == ruinedAirID || b.State == ruinedCaveAirID
|
||||
if isAirLocal != passAir {
|
||||
continue
|
||||
}
|
||||
p := worldgen.TransformBlockPos(b.Pos, mirror, stub.Rotation, pivot)
|
||||
x, y, z := stub.X+p[0], stub.Y+p[1], stub.Z+p[2]
|
||||
final := processState(x, y, z, b.Pos, b.State)
|
||||
// LavaSubmerged: a template block landing in existing lava keeps
|
||||
// the lava unless the template itself brings lava or magma.
|
||||
if final != ruinedLavaID && final != ruinedMagmaID && isLavaState(region.getBlock(x, y, z)) {
|
||||
continue
|
||||
}
|
||||
placeCell(b.Pos, final)
|
||||
}
|
||||
}
|
||||
|
||||
addNetherrackDripColumnsBelowPortal(region, stub, blocks, size, mirror, pivot)
|
||||
return nil
|
||||
}
|
||||
|
||||
func addNetherrackDripColumnsBelowPortal(region *decorationRegion, stub *RuinedPortalStub, blocks []worldgen.TemplateBlockInfo, size [3]int, mirror string, pivot [3]int) {
|
||||
minX, minY, minZ, maxX, _, maxZ := boundingBoxOf(size, mirror, stub.Rotation, pivot, stub.X, stub.Z)
|
||||
for x := minX + 1; x < maxX; x++ {
|
||||
for z := minZ + 1; z < maxZ; z++ {
|
||||
if region.getBlock(x, minY, z) == ruinedNetherrack {
|
||||
ruinedDripColumn(region, x, minY-1, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ruinedDripColumn(region *decorationRegion, x, y, z int) {
|
||||
ruinedPlaceNetherrackOrMagma(region, x, y, z)
|
||||
for step := 0; step < 8; step++ {
|
||||
// Each continuation draws from the same positional stream the initial
|
||||
// placement used, advanced once per already-placed cell above.
|
||||
r := worldgen.NewLegacy(worldgen.MthGetSeed(x, y+step+1, z))
|
||||
if r.NextFloat() >= 0.5 {
|
||||
break
|
||||
}
|
||||
y--
|
||||
ruinedPlaceNetherrackOrMagma(region, x, y, z)
|
||||
}
|
||||
}
|
||||
|
||||
func ruinedPlaceNetherrackOrMagma(region *decorationRegion, x, y, z int) {
|
||||
state := ruinedNetherrack
|
||||
if !stubColdAt(region, x, z) && worldgen.NewLegacy(worldgen.MthGetSeed(x, y, z)).NextFloat() < 0.07 {
|
||||
state = ruinedMagmaID
|
||||
}
|
||||
if monsterCannotTable[region.getBlock(x, y, z)] {
|
||||
return
|
||||
}
|
||||
region.setBlock(x, y, z, state)
|
||||
}
|
||||
|
||||
// stubColdAt reports whether the column's biome freezes; underground portals
|
||||
// in temperate columns never take the cold path.
|
||||
func stubColdAt(region *decorationRegion, x, z int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue