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.
This commit is contained in:
Daniar Mannanov 2026-08-26 02:49:33 +03:00
parent 9454da3ab3
commit 790d7e3144
6 changed files with 469 additions and 1 deletions

View file

@ -36,6 +36,12 @@ func (r *decorationRegion) replayScheduledOres(seed int64, targetX, targetZ int3
if err := r.placeScheduledGeodes(seed); err != nil {
return fmt.Errorf("world: replay source geodes (%d,%d): %w", source.X, source.Z, err)
}
// Stage 3 runs before the ores on purpose: the rooms' cave_air pockets
// are what vanilla's ore ellipsoids roll their air-exposure discards
// against.
if err := r.placeScheduledMonsterRooms(seed); err != nil {
return fmt.Errorf("world: replay source monster rooms (%d,%d): %w", source.X, source.Z, err)
}
if err := r.placeScheduledUndergroundOresStage(seed); err != nil {
return fmt.Errorf("world: replay source underground ores (%d,%d): %w", source.X, source.Z, err)
}

View file

@ -0,0 +1,297 @@
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] }

View file

@ -0,0 +1,128 @@
package world
import (
"testing"
"regionio/internal/worldgen"
)
// buildSolidRegion returns a one-chunk region filled with stone below y=20 so
// room validation has a predictable shell to accept or reject.
func buildSolidRegion(t *testing.T) (*decorationRegion, []*Chunk) {
t.Helper()
chunk := NewChunk(0, 0, BiomePlains)
for y := MinY; y < 24; y++ {
for z := 0; z < 16; z++ {
for x := 0; x < 16; x++ {
chunk.SetBlock(x, y, z, StateStone)
}
}
}
region, err := newDecorationRegion([]*Chunk{chunk})
if err != nil {
t.Fatal(err)
}
if err := region.setSource(0, 0); err != nil {
t.Fatal(err)
}
return region, []*Chunk{chunk}
}
func TestMonsterRoomCarvesShell(t *testing.T) {
initMonsterRoomTables()
region, _ := buildSolidRegion(t)
// A fully sealed shell has no side openings at y=0, so vanilla rejects the
// placement before any write.
random := worldgen.NewWorldgenRandom(1)
if placeMonsterRoom(region, random, 8, 10, 8) {
t.Fatal("room placed in a sealed shell")
}
// Open one column on the +x wall: two adjacent air cells at y=0 and y=1.
region.setBlock(8 + 4, 10, 8, StateAir)
region.setBlock(8 + 4, 11, 8, StateAir)
if !placeMonsterRoom(region, random, 8, 10, 8) {
t.Fatal("room rejected despite an open side column")
}
// The spawner lands at the origin; neighbouring interior cells are air.
if got := region.getBlock(8, 10, 8); got != monsterSpawnerID {
t.Fatalf("origin state = %s, want the spawner", stateLabel(got))
}
if got := region.getBlock(6, 10, 6); !monsterIsAir(got) {
t.Fatalf("interior state = %s, want air", stateLabel(got))
}
// The floor row is cobblestone or mossy cobblestone.
floor := region.getBlock(7, 9, 8)
if floor != monsterCobbleID && floor != monsterMossyID {
t.Fatalf("floor state = %s, want cobblestone family", stateLabel(floor))
}
// The untouched shell above stays stone.
if got := region.getBlock(8, 15, 8); got != StateStone {
t.Fatalf("ceiling state = %s, want stone", stateLabel(got))
}
}
func TestMonsterRoomPlacementIsDeterministic(t *testing.T) {
initMonsterRoomTables()
build := func() *Chunk {
region, _ := buildSolidRegion(t)
region.setBlock(12, 10, 8, StateAir)
region.setBlock(12, 11, 8, StateAir)
random := worldgen.NewWorldgenRandom(42)
placeMonsterRoom(region, random, 8, 10, 8)
return region.chunks[[2]int32{0, 0}]
}
a, b := build(), build()
for y := MinY; y < MinY+WorldHeight; y++ {
for z := 0; z < 16; z++ {
for x := 0; x < 16; x++ {
if a.GetBlock(x, y, z) != b.GetBlock(x, y, z) {
t.Fatalf("nondeterministic block at (%d,%d,%d)", x, y, z)
}
}
}
}
}
func TestMonsterRoomReplayMatchesDirectPass(t *testing.T) {
od, err := worldgen.LoadOverworldFinalDensity(12345)
if err != nil {
t.Fatal(err)
}
fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel)
veins := worldgen.NewOreVeinifier(od)
carver, err := worldgen.NewCarver(od, 12345)
if err != nil {
t.Fatal(err)
}
initCarverReplaceable(carver.ReplaceableBlocks())
generate := func() *Chunk {
var chunks []*Chunk
for cx := int32(-2); cx <= 2; cx++ {
for cz := int32(-2); cz <= 2; cz++ {
chunks = append(chunks, generateVanillaWithoutDecoration(od, fluidPicker, veins, carver, 12345, cx, cz))
}
}
region, err := newDecorationRegion(chunks)
if err != nil {
t.Fatal(err)
}
if err := region.replayScheduledOres(12345, 0, 0); err != nil {
t.Fatal(err)
}
return region.chunks[[2]int32{0, 0}]
}
a, b := generate(), generate()
for y := MinY; y < MinY+WorldHeight; y++ {
for z := 0; z < 16; z++ {
for x := 0; x < 16; x++ {
if a.GetBlock(x, y, z) != b.GetBlock(x, y, z) {
t.Fatalf("region replay nondeterministic at (%d,%d,%d)", x, y, z)
}
}
}
}
}

View file

@ -33,7 +33,7 @@ const dataVersion26 = 4790
// first time it ran: chunkAt prefers the store over the generator, so the
// already-explored area around spawn keeps its old terrain and every later fix
// looks like it did nothing in exactly the place you are standing.
const generatorVersion = 21
const generatorVersion = 22
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
// because it is ours, not part of the vanilla chunk format.

View file

@ -0,0 +1,11 @@
{
"values": [
"minecraft:bedrock",
"minecraft:spawner",
"minecraft:chest",
"minecraft:end_portal_frame",
"minecraft:reinforced_deepslate",
"minecraft:trial_spawner",
"minecraft:vault"
]
}

View file

@ -0,0 +1,26 @@
package worldgen
import (
_ "embed"
"encoding/json"
)
// features_cannot_replace.json is #minecraft:features_cannot_replace, captured
// verbatim from the 26.1.2 server jar. Features guard their writes through
// Feature.safeSetBlock with this tag: an existing state in the tag stays, an
// existing state outside it may be replaced.
//
//go:embed data/tag/features_cannot_replace.json
var featuresCannotReplaceJSON []byte
// FeaturesCannotReplace returns the block names in
// #minecraft:features_cannot_replace.
func FeaturesCannotReplace() ([]string, error) {
var doc struct {
Values []string `json:"values"`
}
if err := json.Unmarshal(featuresCannotReplaceJSON, &doc); err != nil {
return nil, err
}
return doc.Values, nil
}