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.
297 lines
9.4 KiB
Go
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] }
|