RegionIO/internal/world/ruined_portal_piece.go
Daniar Mannanov e0623be504 world: place template air, flood connected pockets, age stairs and slabs
The portal's air pocket is placed for real now: vanilla's fluid ticks
then flood every pocket cell connected to surrounding water while
sealed cells stay air, so the piece ends with a bounded BFS over its
own air cells from water neighbours. BlockAgeProcessor gains the
stairs/slab branches with their property-preserving mossy variants,
and horizontal facings rotate with the piece (mirror first, then
quarter turns).

Portal box match against the fixture jumps from 52.7% to 84.1%;
region parity rises to 98.047% with biomes and heightmaps exact.
Remaining box diffs trace to drip columns reading the shared decoration
stream, which is approximated per-cell.
2026-08-26 15:51:03 +03:00

377 lines
10 KiB
Go

package world
import (
"strings"
"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)
}
orientState := func(state uint16) uint16 {
if mirror == "none" && stub.Rotation == 0 {
return state
}
s, ok := stateByID(state)
if !ok {
return state
}
facing, hasFacing := s.Properties["facing"]
if !hasFacing {
return state
}
switch facing {
case "north", "east", "south", "west":
default:
return state
}
steps := stub.Rotation
if mirror == "front_back" {
switch facing {
case "east":
facing = "west"
case "west":
facing = "east"
}
} else if mirror == "left_right" {
switch facing {
case "north":
facing = "south"
case "south":
facing = "north"
}
}
for i := 0; i < steps; i++ {
switch facing {
case "north":
facing = "east"
case "east":
facing = "south"
case "south":
facing = "west"
case "west":
facing = "north"
}
}
props := make(map[string]string, len(s.Properties))
for k, v := range s.Properties {
props[k] = v
}
props["facing"] = facing
if id, resolved := nameToStateID(s.Name, props); resolved {
return id
}
return 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 {
case state == ruinedGoldID:
if roll(0.3) {
return ruinedAirID
}
case state == ruinedLavaID:
switch {
case stub.Cold:
return ruinedNetherrack
case roll(0.2):
return ruinedMagmaID
}
case state == ruinedNetherrack:
if roll(0.07) {
return ruinedMagmaID
}
case isStoneFamilyForAge(state):
r := worldgen.NewLegacy(seed)
if r.NextFloat() >= 0.5 {
break
}
if r.NextFloat() < stub.Mossiness {
return mossyStoneBricksID
}
return crackedStoneBricksID
case isStairState(state):
r := worldgen.NewLegacy(seed)
if r.NextFloat() >= 0.5 {
break
}
mossyStairs := withProps("minecraft:mossy_stone_brick_stairs", state)
mossySlab := mustState("minecraft:mossy_stone_brick_slab", nil)
nonMossy := []uint16{mustState("minecraft:stone_slab", nil), mustState("minecraft:stone_brick_slab", nil)}
if r.NextFloat() < stub.Mossiness {
pick := int(r.NextIntN(2))
if pick == 0 {
return mossyStairs
}
return mossySlab
}
return nonMossy[pick2(r)]
case isSlabState(state):
r := worldgen.NewLegacy(seed)
if r.NextFloat() < stub.Mossiness {
return withProps("minecraft:mossy_stone_brick_slab", state)
}
case state == ruinedObsidianID:
if roll(0.15) {
return cryingObsidianID
}
}
return state
}
// Two passes like buildInfoList: solids land before any template air.
var airLocals [][3]int
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]
if passAir {
airLocals = append(airLocals, b.Pos)
if !isLavaState(region.getBlock(x, y, z)) {
placeCell(b.Pos, ruinedAirID)
}
continue
}
final := processState(x, y, z, b.Pos, b.State)
final = orientState(final)
// 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)
}
}
floodWaterIntoAir(region, airLocals, mirror, stub.Rotation, pivot, stub)
addNetherrackDripColumnsBelowPortal(region, stub, blocks, size, mirror, pivot)
return nil
}
// floodWaterIntoAir replicates vanilla's scheduled fluid ticks over a freshly
// placed air pocket: every pocket cell connected to surrounding water floods,
// while cells sealed off by solids stay air.
func floodWaterIntoAir(region *decorationRegion, airLocals [][3]int, mirror string, rotation int, pivot [3]int, stub *RuinedPortalStub) {
type cell struct{ x, y, z int }
templateAir := map[[3]int]bool{}
var airCells []cell
for _, local := range airLocals {
p := worldgen.TransformBlockPos(local, mirror, rotation, pivot)
w := cell{stub.X + p[0], stub.Y + p[1], stub.Z + p[2]}
if !monsterIsAir(region.getBlock(w.x, w.y, w.z)) {
continue
}
templateAir[[3]int{w.x, w.y, w.z}] = true
airCells = append(airCells, w)
}
flooded := map[[3]int]bool{}
queue := []cell{}
for _, c := range airCells {
key := [3]int{c.x, c.y, c.z}
if flooded[key] {
continue
}
touchesWater := false
for _, o := range [][3]int{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}} {
if isWaterState(region.getBlock(c.x+o[0], c.y+o[1], c.z+o[2])) {
touchesWater = true
break
}
}
if touchesWater {
queue = append(queue, c)
flooded[key] = true
}
}
for len(queue) > 0 {
c := queue[0]
queue = queue[1:]
region.setBlock(c.x, c.y, c.z, StateWater)
for _, o := range [][3]int{{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}} {
n := cell{c.x + o[0], c.y + o[1], c.z + o[2]}
key := [3]int{n.x, n.y, n.z}
if flooded[key] || !templateAir[key] || !monsterIsAir(region.getBlock(n.x, n.y, n.z)) {
continue
}
flooded[key] = true
queue = append(queue, n)
}
}
}
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
}
func pick2(r *worldgen.Legacy) int { return int(r.NextIntN(2)) }
func mustState(name string, props map[string]string) uint16 {
id, ok := nameToStateID(name, props)
if !ok {
panic("world: missing state: " + name)
}
return id
}
func withProps(name string, from uint16) uint16 {
if s, ok := stateByID(from); ok {
if id, resolved := nameToStateID(name, s.Properties); resolved {
return id
}
}
return 0
}
func isStairState(state uint16) bool {
if s, ok := stateByID(state); ok {
return strings.HasSuffix(s.Name, "_stairs")
}
return false
}
func isSlabState(state uint16) bool {
if s, ok := stateByID(state); ok {
return strings.HasSuffix(s.Name, "_slab")
}
return false
}
// isStoneFamilyForAge covers BlockAgeProcessor's full-block branch:
// stone_bricks, stone, and chiseled_stone_bricks.
func isStoneFamilyForAge(state uint16) bool {
switch state {
case stoneBricksID, stoneID, chiseledStoneBricks:
return true
}
return false
}