RegionIO/internal/world/monster_rooms.go
Daniar Mannanov 790d7e3144 world: replay stage-3 monster rooms
Ports MonsterRoomFeature from bytecode with vanilla draw order and loop
structure: shell validation (solid floor and ceiling, one to five open
side columns), interior cave_air carving, cobblestone walls with the
mossy three-in-four floor roll drawn only where vanilla draws it, chest
placement faced away from its single opaque neighbour with the loot-seed
long consumed even though block entities are not modelled yet, and the
spawner's one-draw mob pick. Writes go through Feature.safeSetBlock
semantics against #minecraft:features_cannot_replace, captured verbatim
from the jar into the embedded datapack data.

The pass slots between geodes and the ores so the rooms' cave_air
pockets exist before ore ellipsoids roll their air-exposure discards.
The committed fixture's four chunks happen to contain no dungeons, so
measured parity holds at 98.028%, but the generator's output can differ
elsewhere: generatorVersion bumps to 22.
2026-08-26 02:49:33 +03:00

297 lines
9.4 KiB
Go

package world
import (
"sync"
"regionio/internal/worldgen"
)
// monster_rooms.go ports net.minecraft.world.level.levelgen.feature.
// MonsterRoomFeature, the stage-3 underground-structures feature behind the
// monster_room and monster_room_deep placed features. Both share one configured
// feature with an empty config; only their placement modifiers differ, and the
// generic placement driver already handles those.
//
// The rooms matter far beyond their own blocks: they open cave_air pockets
// during the stage that runs before the ores. Ore ellipsoids crossing those
// pockets roll air-exposure discards, so replaying monster rooms in the right
// order is what lets the ore schedule see the world vanilla's ores saw.
//
// The port follows bytecode fidelity like the carvers do: every random draw,
// loop order, and early return is vanilla's. Block entities are not modelled
// yet — chests place as plain block states and the spawner's mob pick still
// consumes its draw so later placement positions stay on vanilla's stream.
const undergroundStructuresStage = 3
var (
monsterRoomOnce sync.Once
monsterRoomCannotTable []bool
monsterRoomStatesOnce sync.Once
monsterCaveAirID uint16
monsterChestIDs map[string]uint16
monsterSpawnerID uint16
monsterCobbleID uint16
monsterMossyID uint16
monsterAirIDs map[uint16]bool
monsterCannotTable []bool
)
func initMonsterRoomTables() {
monsterRoomOnce.Do(func() {
names, err := worldgen.FeaturesCannotReplace()
if err != nil {
panic(err)
}
stateByIDOnce.Do(buildStateTable)
monsterCannotTable = make([]bool, totalBlockStates)
for _, name := range names {
for _, id := range idsByName[name] {
if int(id) < len(monsterCannotTable) {
monsterCannotTable[id] = true
}
}
}
})
monsterRoomStatesOnce.Do(func() {
stateByIDOnce.Do(buildStateTable)
monsterCaveAirID = monsterMustState("minecraft:cave_air", nil)
monsterSpawnerID = monsterMustState("minecraft:spawner", nil)
monsterCobbleID = monsterMustState("minecraft:cobblestone", nil)
monsterMossyID = monsterMustState("minecraft:mossy_cobblestone", nil)
monsterChestIDs = map[string]uint16{}
for _, facing := range []string{"north", "south", "west", "east"} {
monsterChestIDs[facing] = monsterMustState("minecraft:chest", map[string]string{
"facing": facing, "type": "single", "waterlogged": "false",
})
}
monsterAirIDs = map[uint16]bool{
mustMonsterAir("minecraft:air"): true,
mustMonsterAir("minecraft:cave_air"): true,
}
})
}
func monsterMustState(name string, props map[string]string) uint16 {
id, ok := nameToStateID(name, props)
if !ok {
panic("world: missing block state for monster rooms: " + name)
}
return id
}
func mustMonsterAir(name string) uint16 {
return monsterMustState(name, nil)
}
// monsterSafeSetBlock is Feature.safeSetBlock: replace unless the existing
// state is in #minecraft:features_cannot_replace.
func (r *decorationRegion) monsterSafeSetBlock(x, y, z int, state uint16) bool {
if monsterCannotTable[r.getBlock(x, y, z)] {
return false
}
return r.setBlock(x, y, z, state)
}
// placeScheduledMonsterRooms replays the vanilla stage-3 schedule from one
// source center into the mutable decoration region.
func (r *decorationRegion) placeScheduledMonsterRooms(seed int64) error {
set, err := worldgen.LoadFeatureSet()
if err != nil {
return err
}
initMonsterRoomTables()
if err := r.ensureSourceNeighborhood(); err != nil {
return err
}
schedule, err := set.FeatureSchedule(possibleBiomeOrder(), r.sourceBiomes(), undergroundStructuresStage)
if err != nil {
return err
}
random, decorationSeed := worldgen.DecorationRandom(seed, int(r.sourceX), int(r.sourceZ))
origin := worldgen.FeaturePosition{X: int(r.sourceX) << 4, Y: MinY, Z: int(r.sourceZ) << 4}
for _, scheduled := range schedule {
placed, ok := set.Placed[scheduled.Name]
if !ok {
continue
}
configured, ok := set.Configured[placed.Feature]
if !ok || configured.Type != "minecraft:monster_room" {
continue
}
random.SetFeatureSeed(decorationSeed, scheduled.Index, undergroundStructuresStage)
context := r.placementContext(func(position worldgen.FeaturePosition) bool {
return r.biomeAllowsFeature(set, scheduled.Name, undergroundStructuresStage, position)
})
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
placeMonsterRoom(r, random, position.X, position.Y, position.Z)
return nil
}); err != nil {
return err
}
}
return nil
}
// placeMonsterRoom ports MonsterRoomFeature.place for one origin.
func placeMonsterRoom(r *decorationRegion, random worldgen.RandomSource, ox, oy, oz int) bool {
initMonsterRoomTables()
j := int(random.NextIntN(2)) + 2 // x half-extent minus walls: 2..3
o := int(random.NextIntN(2)) + 2 // z half-extent minus walls: 2..3
k, l := -j-1, j+1 // full x span, walls included
p, q := -o-1, o+1 // full z span
// Pass 1 validates the shell: solid floor at y=-1 and ceiling at y=4 for
// every column, and between one and five open side openings at y=0.
openings := 0
for x := k; x <= l; x++ {
for y := -1; y <= 4; y++ {
for z := p; z <= q; z++ {
solid := monsterIsSolid(r.getBlock(ox+x, oy+y, oz+z))
if y == -1 && !solid {
return false
}
if y == 4 && !solid {
return false
}
if (x == k || x == l || z == p || z == q) && y == 0 {
if monsterIsAir(r.getBlock(ox+x, oy, oz+z)) &&
monsterIsAir(r.getBlock(ox+x, oy+1, oz+z)) {
openings++
}
}
}
}
}
if openings < 1 || openings > 5 {
return false
}
// Pass 2 carves: interior becomes cave air; walls become cobblestone, with
// a mossy three-in-four roll on the floor row; a wall block whose own floor
// was already carved becomes cave air outright, bypassing the replaceable
// guard just as vanilla's unconditional setBlock does. The mossy roll only
// draws for a solid non-chest cell that reaches the wall branch — the same
// conditions under which vanilla reaches its nextInt(4).
for x := k; x <= l; x++ {
for y := 3; y >= -1; y-- {
for z := p; z <= q; z++ {
bx, by, bz := ox+x, oy+y, oz+z
current := r.getBlock(bx, by, bz)
interior := x != k && y != -1 && z != p && x != l && y != 4 && z != q
if interior {
if current == monsterChestIDs["north"] ||
current == monsterChestIDs["south"] ||
current == monsterChestIDs["west"] ||
current == monsterChestIDs["east"] ||
current == monsterSpawnerID {
continue
}
r.monsterSafeSetBlock(bx, by, bz, monsterCaveAirID)
continue
}
// The mossy roll draws only here, exactly where vanilla's
// floor branch sits; every other wall row goes cobblestone
// without a draw.
placeWall := func() {
state := monsterCobbleID
if y == -1 && random.NextIntN(4) != 0 {
state = monsterMossyID
}
r.monsterSafeSetBlock(bx, by, bz, state)
}
if by < MinY {
if !monsterIsSolid(current) || isMonsterChest(current) {
continue
}
placeWall()
continue
}
if !monsterIsSolid(r.getBlock(bx, by-1, bz)) {
r.setBlock(bx, by, bz, monsterCaveAirID)
continue
}
if !monsterIsSolid(current) || isMonsterChest(current) {
continue
}
placeWall()
}
}
}
// Pass 3 tries two chest spots, three times each: an air cell with exactly
// one opaque horizontal neighbour, faced away from it. An adjacent chest
// leaves the new one at its default facing instead.
for attempt := 0; attempt < 2; attempt++ {
for try := 0; try < 3; try++ {
cx := ox + int(random.NextIntN(int32(j*2+1))) - j
cz := oz + int(random.NextIntN(int32(o*2+1))) - o
cy := oy
if !monsterIsAir(r.getBlock(cx, cy, cz)) {
continue
}
solids := 0
var solidDir [2]int
adjacentChest := false
for _, d := range [4][2]int{{0, -1}, {1, 0}, {0, 1}, {-1, 0}} {
neighbor := r.getBlock(cx+d[0], cy, cz+d[1])
if isMonsterChest(neighbor) {
adjacentChest = true
continue
}
if monsterIsSolidRender(neighbor) {
solids++
solidDir = d
}
}
if solids != 1 {
continue
}
facing := "north"
if !adjacentChest {
switch {
case solidDir[0] == 1:
facing = "west"
case solidDir[0] == -1:
facing = "east"
case solidDir[1] == 1:
facing = "north"
default:
facing = "south"
}
}
if r.monsterSafeSetBlock(cx, cy, cz, monsterChestIDs[facing]) {
// Vanilla seeds the chest's loot table from one long here.
random.NextLong()
}
}
}
// Pass 4 places the spawner at the center; picking its mob is one draw.
if r.monsterSafeSetBlock(ox, oy, oz, monsterSpawnerID) {
random.NextIntN(4) // skeleton / zombie / zombie / spider
}
return true
}
func isMonsterChest(state uint16) bool {
return state == monsterChestIDs["north"] || state == monsterChestIDs["south"] ||
state == monsterChestIDs["west"] || state == monsterChestIDs["east"]
}
// monsterIsSolid approximates BlockState.isSolid, which in this version reads
// the state's blocks-motion property.
func monsterIsSolid(state uint16) bool {
return stateFlags(state)&flagBlocksMotion != 0
}
// monsterIsSolidRender approximates BlockState.isSolidRender: an occluding,
// fully opaque cube.
func monsterIsSolidRender(state uint16) bool {
f := stateFlags(state)
return f&flagCanOcclude != 0 && lightOpacity(state) == 15
}
func monsterIsAir(state uint16) bool { return monsterAirIDs[state] }