world: port ruined portal generation point and expose pre-carve terrain
generateBaseTerrain now factors the noise+surface stage out of chunk generation so structure placement can read pre-carve heights exactly like vanilla's STRUCTURE_STARTS stage. RuinedPortalGenerationPoint replays the ruined_portals set: the weighted pick across all seven variants on its own stream, each attempt's setup/template/rotation/ mirror draws on a fresh GenerationContext stream (makeRandom reseeds per attempt, confirmed from bytecode), findSuitableY with corner-column settle scan against those heights, and the post-draw 3D-biome filter at quart-snapped coordinates. Known gap, documented in STRUCTURE_NOTES.md: no variant is accepted in the window around the fixture's portal yet while vanilla placed one; the biome/surface inputs need a vanilla-side probe to bisect. Not wired into the production replay until that closes.
This commit is contained in:
parent
6e04bd8bbc
commit
798a3ec7b4
6 changed files with 454 additions and 31 deletions
341
internal/world/ruined_portal.go
Normal file
341
internal/world/ruined_portal.go
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"regionio/internal/worldgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ruined_portal.go ports RuinedPortalStructure.findGenerationPoint — the part
|
||||||
|
// of the structure that runs on the chunk's own Legacy stream and decides
|
||||||
|
// whether a portal starts here, which template it uses, and where it sits
|
||||||
|
// vertically. Piece block placement lands in a follow-up; this file already
|
||||||
|
// consumes exactly vanilla's draws so later structures on the same stream stay
|
||||||
|
// aligned.
|
||||||
|
|
||||||
|
var ruinedPortalTemplates = []string{
|
||||||
|
"ruined_portal/portal_1", "ruined_portal/portal_2", "ruined_portal/portal_3",
|
||||||
|
"ruined_portal/portal_4", "ruined_portal/portal_5", "ruined_portal/portal_6",
|
||||||
|
"ruined_portal/portal_7", "ruined_portal/portal_8", "ruined_portal/portal_9",
|
||||||
|
"ruined_portal/portal_10",
|
||||||
|
}
|
||||||
|
var ruinedPortalGiants = []string{
|
||||||
|
"ruined_portal/giant_portal_1", "ruined_portal/giant_portal_2",
|
||||||
|
"ruined_portal/giant_portal_3",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuinedPortalStub is everything findGenerationPoint decides.
|
||||||
|
type RuinedPortalStub struct {
|
||||||
|
X, Y, Z int
|
||||||
|
Template string
|
||||||
|
Rotation int // none, cw90, cw180, ccw90
|
||||||
|
Mirror string
|
||||||
|
AirPocket bool
|
||||||
|
Mossiness float32
|
||||||
|
Overgrown bool
|
||||||
|
Vines bool
|
||||||
|
Cold bool // resolved by the caller; needs the biome at the stub
|
||||||
|
Blackstone bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleProbability(random *worldgen.Legacy, p float32) bool {
|
||||||
|
if p == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if p == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return random.NextFloat() < p
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomBetweenInclusiveLegacy(random *worldgen.Legacy, lo, hi int) int {
|
||||||
|
return lo + int(random.NextIntN(int32(hi-lo+1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandomWithinInterval(random *worldgen.Legacy, a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return randomBetweenInclusiveLegacy(random, a, b)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
preCarveMu sync.Mutex
|
||||||
|
preCarveCache = map[[2]int32]baseTerrain{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// preCarveTerrainFor returns the noise+surface terrain for one chunk with
|
||||||
|
// carving deliberately skipped: structure placement runs before carvers in
|
||||||
|
// vanilla, and its height queries must not see carved holes. Results are
|
||||||
|
// cached because several placement queries land in the same chunks.
|
||||||
|
func preCarveTerrainFor(od *worldgen.OverworldDensity, seed int64, cx, cz int32) (baseTerrain, error) {
|
||||||
|
key := [2]int32{cx, cz}
|
||||||
|
preCarveMu.Lock()
|
||||||
|
defer preCarveMu.Unlock()
|
||||||
|
if base, ok := preCarveCache[key]; ok {
|
||||||
|
return base, nil
|
||||||
|
}
|
||||||
|
_, fluidPicker, veins, _ := vanillaGeneratorInputs(seed)
|
||||||
|
base := generateBaseTerrain(od, fluidPicker, veins, nil, seed, cx, cz)
|
||||||
|
preCarveCache[key] = base
|
||||||
|
return base, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
templateCacheMu sync.Mutex
|
||||||
|
templateCache = map[string][]worldgen.TemplateBlockInfo{}
|
||||||
|
templateSizes = map[string][3]int{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadTemplateCached(name string) ([]worldgen.TemplateBlockInfo, [3]int, error) {
|
||||||
|
templateCacheMu.Lock()
|
||||||
|
defer templateCacheMu.Unlock()
|
||||||
|
if blocks, ok := templateCache[name]; ok {
|
||||||
|
return blocks, templateSizes[name], nil
|
||||||
|
}
|
||||||
|
raw, err := worldgen.EmbeddedStructureTemplate(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, [3]int{}, err
|
||||||
|
}
|
||||||
|
blocks, size, err := worldgen.LoadResolvedTemplateBytes(raw,
|
||||||
|
func(stateName string, props map[string]string) (uint16, bool) {
|
||||||
|
return nameToStateID(stateName, props)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, [3]int{}, err
|
||||||
|
}
|
||||||
|
templateCache[name] = blocks
|
||||||
|
templateSizes[name] = size
|
||||||
|
return blocks, size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// boundingBoxOf transforms the eight corners of the template cuboid and takes
|
||||||
|
// their min/max, matching StructureTemplate.getBoundingBox over all blocks.
|
||||||
|
func boundingBoxOf(size [3]int, mirror string, rotation int, pivot [3]int, originX, originZ int) (minX, minY, minZ, maxX, maxY, maxZ int) {
|
||||||
|
minX, minY, minZ = int(^uint(0)>>1), int(^uint(0)>>1), int(^uint(0)>>1)
|
||||||
|
maxX, maxY, maxZ = -minX, -minY, -minZ
|
||||||
|
for _, cx := range [3]int{0, size[0] - 1} {
|
||||||
|
for _, cy := range [3]int{0, size[1] - 1} {
|
||||||
|
for _, cz := range [3]int{0, size[2] - 1} {
|
||||||
|
p := worldgen.TransformBlockPos([3]int{cx, cy, cz}, mirror, rotation, pivot)
|
||||||
|
x, y, z := originX+p[0], p[1], originZ+p[2]
|
||||||
|
minX, maxX = min(minX, x), max(maxX, x)
|
||||||
|
minY, maxY = min(minY, y), max(maxY, y)
|
||||||
|
minZ, maxZ = min(minZ, z), max(maxZ, z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuinedPortalGenerationPoint replays the ruined_portals set for one source
|
||||||
|
// chunk: the weighted pick across all seven portal variants, each variant's
|
||||||
|
// own findGenerationPoint draws, and the post-draw 3D-biome filter. It returns
|
||||||
|
// nil when vanilla would have ended up with no valid start here.
|
||||||
|
func RuinedPortalGenerationPoint(od *worldgen.OverworldDensity, sets *worldgen.StructureSets, seed int64, sx, sz int32) (*RuinedPortalStub, error) {
|
||||||
|
set := sets.Sets["minecraft:ruined_portals"]
|
||||||
|
if set == nil || !set.IsStartChunk(seed, sx, sz) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
random := worldgen.NewLegacy(0)
|
||||||
|
random.SetLargeFeatureSeed(seed, int(sx), int(sz))
|
||||||
|
|
||||||
|
indices := make([]int, len(set.Structures))
|
||||||
|
for i := range indices {
|
||||||
|
indices[i] = i
|
||||||
|
}
|
||||||
|
for len(indices) > 0 {
|
||||||
|
total := 0
|
||||||
|
for _, i := range indices {
|
||||||
|
total += set.Structures[i].Weight
|
||||||
|
}
|
||||||
|
pick := int(random.NextIntN(int32(total)))
|
||||||
|
selected := 0
|
||||||
|
for offset, i := range indices {
|
||||||
|
pick -= set.Structures[i].Weight
|
||||||
|
if pick < 0 {
|
||||||
|
selected = offset
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry := indices[selected]
|
||||||
|
def := sets.Structures[set.Structures[entry].Structure]
|
||||||
|
if def == nil || len(def.Setups) == 0 {
|
||||||
|
return nil, fmt.Errorf("portal variant %s missing setups", set.Structures[entry].Structure)
|
||||||
|
}
|
||||||
|
// GenerationContext.random() is lazy: every attempt reseeds a fresh
|
||||||
|
// Legacy stream from the same formula instead of continuing the
|
||||||
|
// previous attempt's stream.
|
||||||
|
variantRandom := worldgen.NewLegacy(0)
|
||||||
|
variantRandom.SetLargeFeatureSeed(seed, int(sx), int(sz))
|
||||||
|
stub := ruinedPortalVariantPoint(od, sets, seed, sx, sz, def, variantRandom)
|
||||||
|
if stub != nil {
|
||||||
|
return stub, nil
|
||||||
|
}
|
||||||
|
// Vanilla removes the failed entry and redraws from the pick stream.
|
||||||
|
indices = append(indices[:selected], indices[selected+1:]...)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruinedPortalVariantPoint(od *worldgen.OverworldDensity, sets *worldgen.StructureSets, seed int64, sx, sz int32, def *worldgen.StructureDef, random *worldgen.Legacy) *RuinedPortalStub {
|
||||||
|
// Setup selection: skipped entirely for single-setup lists.
|
||||||
|
setup := def.Setups[0]
|
||||||
|
if len(def.Setups) > 1 {
|
||||||
|
total := float32(0)
|
||||||
|
for _, s := range def.Setups {
|
||||||
|
total += s.Weight
|
||||||
|
}
|
||||||
|
f := random.NextFloat()
|
||||||
|
for _, s := range def.Setups {
|
||||||
|
f -= s.Weight / total
|
||||||
|
if f < 0 {
|
||||||
|
setup = s
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
airPocket := sampleProbability(random, setup.AirPocketProbability)
|
||||||
|
|
||||||
|
templateName := ruinedPortalTemplates[int(random.NextIntN(int32(len(ruinedPortalTemplates))))]
|
||||||
|
if random.NextFloat() < 0.05 {
|
||||||
|
templateName = ruinedPortalGiants[int(random.NextIntN(int32(len(ruinedPortalGiants))))]
|
||||||
|
}
|
||||||
|
_, size, err := loadTemplateCached(templateName)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rotation := int(random.NextIntN(4))
|
||||||
|
mirror := "none"
|
||||||
|
if random.NextFloat() >= 0.5 {
|
||||||
|
mirror = "front_back"
|
||||||
|
}
|
||||||
|
pivot := [3]int{size[0] / 2, 0, size[2] / 2}
|
||||||
|
|
||||||
|
originX, originZ := int(sx)*16, int(sz)*16
|
||||||
|
minX, minY, minZ, maxX, maxY, maxZ := boundingBoxOf(size, mirror, rotation, pivot, originX, originZ)
|
||||||
|
centerX := minX + (maxX-minX+1)/2
|
||||||
|
centerZ := minZ + (maxZ-minZ+1)/2
|
||||||
|
|
||||||
|
base, err := preCarveTerrainFor(od, seed, int32(centerX>>4), int32(centerZ>>4))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
heightmapY := func(x, z int, oceanFloor bool) int {
|
||||||
|
lx, lz := x-int(base.c.X)*16, z-int(base.c.Z)*16
|
||||||
|
for i := WorldHeight - 1; i >= 0; i-- {
|
||||||
|
s := base.columns[lx][lz][i]
|
||||||
|
y := MinY + i
|
||||||
|
if !oceanFloor {
|
||||||
|
if s != StateAir {
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// OCEAN_FLOOR_WG scans past fluids to the first motion-blocking.
|
||||||
|
if s != StateAir && !isWaterState(s) && stateFlags(s)&flagBlocksMotion != 0 {
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MinY - 1
|
||||||
|
}
|
||||||
|
oceanFloor := setup.Placement == "on_ocean_floor"
|
||||||
|
// getHeight returns one past the top block and vanilla subtracts one, so
|
||||||
|
// surfaceY lands exactly on the topmost non-air block.
|
||||||
|
surfaceY := heightmapY(centerX, centerZ, oceanFloor)
|
||||||
|
|
||||||
|
ySpan := maxY - minY + 1
|
||||||
|
y := ruinedPortalFindSuitableY(random, setup.Placement, airPocket, surfaceY, ySpan,
|
||||||
|
minX, minZ, maxX, maxZ, base)
|
||||||
|
|
||||||
|
stub := &RuinedPortalStub{
|
||||||
|
X: originX, Z: originZ,
|
||||||
|
Template: templateName, Rotation: rotation, Mirror: mirror,
|
||||||
|
AirPocket: airPocket, Mossiness: setup.Mossiness,
|
||||||
|
Overgrown: setup.Overgrown, Vines: setup.Vines,
|
||||||
|
Blackstone: setup.ReplaceWithBlackstone,
|
||||||
|
Y: y,
|
||||||
|
}
|
||||||
|
|
||||||
|
// findValidGenerationPoint filters by the 3D noise biome at the stub
|
||||||
|
// position AFTER every draw has happened. The climate sampler reads
|
||||||
|
// quarter coordinates, so the position snaps down to the 4-block lattice
|
||||||
|
// exactly like QuartPos.fromBlock.
|
||||||
|
qx, qy, qz := (stub.X>>2)<<2, (y>>2)<<2, (stub.Z>>2)<<2
|
||||||
|
s2D := worldgen.SampleColumn2D(od, SeaLevel, qx, qz)
|
||||||
|
biomeAtStub := biomeNameByID(BiomeAt3D(od, s2D, qx, qy, qz))
|
||||||
|
allowed := false
|
||||||
|
for _, name := range sets.BiomesFor(def) {
|
||||||
|
if name == biomeAtStub {
|
||||||
|
allowed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if setup.CanBeCold {
|
||||||
|
stub.Cold = worldgen.ColdEnoughToSnow(biomeAtStub)
|
||||||
|
}
|
||||||
|
return stub
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruinedPortalFindSuitableY(random *worldgen.Legacy, placement string, airPocket bool, surfaceY, ySpan, minX, minZ, maxX, maxZ int, base baseTerrain) int {
|
||||||
|
minCut := MinY + 15
|
||||||
|
var y int
|
||||||
|
switch placement {
|
||||||
|
case "in_nether":
|
||||||
|
if airPocket {
|
||||||
|
y = randomBetweenInclusiveLegacy(random, 32, 100)
|
||||||
|
} else if random.NextFloat() < 0.5 {
|
||||||
|
y = randomBetweenInclusiveLegacy(random, 27, 29)
|
||||||
|
} else {
|
||||||
|
y = randomBetweenInclusiveLegacy(random, 29, 100)
|
||||||
|
}
|
||||||
|
case "in_mountain":
|
||||||
|
y = getRandomWithinInterval(random, 70, surfaceY-ySpan)
|
||||||
|
case "underground":
|
||||||
|
y = getRandomWithinInterval(random, minCut, surfaceY-ySpan)
|
||||||
|
case "partly_buried":
|
||||||
|
y = surfaceY + randomBetweenInclusiveLegacy(random, 2, 8)
|
||||||
|
default:
|
||||||
|
y = surfaceY
|
||||||
|
}
|
||||||
|
|
||||||
|
opaqueAt := func(x, z, yy int) bool {
|
||||||
|
lx, lz := x-int(base.c.X)*16, z-int(base.c.Z)*16
|
||||||
|
if lx < 0 || lx > 15 || lz < 0 || lz > 15 || yy < MinY || yy >= MinY+WorldHeight {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
state := base.columns[lx][lz][yy-MinY]
|
||||||
|
if placement == "on_ocean_floor" {
|
||||||
|
return state != StateAir && !isWaterState(state) && stateFlags(state)&flagBlocksMotion != 0
|
||||||
|
}
|
||||||
|
return state != StateAir
|
||||||
|
}
|
||||||
|
|
||||||
|
for yy := y; yy > minCut; yy-- {
|
||||||
|
opaque := 0
|
||||||
|
done := false
|
||||||
|
for _, corner := range [4][2]int{{minX, minZ}, {maxX, minZ}, {minX, maxZ}, {maxX, maxZ}} {
|
||||||
|
if opaqueAt(corner[0], corner[1], yy) {
|
||||||
|
opaque++
|
||||||
|
if opaque == 3 {
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
return yy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -112,6 +112,28 @@ func generateVanillaWithoutDecoration(od *worldgen.OverworldDensity, fluidPicker
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateVanillaDecorated(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32, withDecoration bool) *Chunk {
|
func generateVanillaDecorated(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32, withDecoration bool) *Chunk {
|
||||||
|
data := generateBaseTerrain(od, fluidPicker, veins, carver, seed, cx, cz)
|
||||||
|
c := data.c
|
||||||
|
surfTop, grass := data.surfTop, data.grass
|
||||||
|
if withDecoration {
|
||||||
|
decorate(c, od, cx, cz, seed, surfTop, grass, data.biomeName)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseTerrain is everything the noise/surface/carve stages produce, kept
|
||||||
|
// together so structure placement can read pre-carve heights exactly the way
|
||||||
|
// vanilla's STRUCTURE_STARTS stage does.
|
||||||
|
type baseTerrain struct {
|
||||||
|
c *Chunk
|
||||||
|
columns *[16][16][WorldHeight]uint16
|
||||||
|
worldSurface *[16][16]int // topmost non-air Y before carving
|
||||||
|
surfTop *[16][16]int
|
||||||
|
grass *[16][16]bool
|
||||||
|
biomeName *[16][16]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateBaseTerrain(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32) baseTerrain {
|
||||||
c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below
|
c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below
|
||||||
baseX, baseZ := int(cx)*16, int(cz)*16
|
baseX, baseZ := int(cx)*16, int(cz)*16
|
||||||
|
|
||||||
|
|
@ -240,10 +262,11 @@ func generateVanillaDecorated(od *worldgen.OverworldDensity, fluidPicker worldge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fillBiomes3D(c, od, s2D, baseX, baseZ)
|
fillBiomes3D(c, od, s2D, baseX, baseZ)
|
||||||
if withDecoration {
|
return baseTerrain{
|
||||||
decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName)
|
c: c, columns: &columns,
|
||||||
|
worldSurface: &worldSurface, surfTop: &surfTop,
|
||||||
|
grass: &grass, biomeName: &biomeName,
|
||||||
}
|
}
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fillBiomes3D assigns a per-cell 4×4×4 biome to every section of the chunk.
|
// fillBiomes3D assigns a per-cell 4×4×4 biome to every section of the chunk.
|
||||||
|
|
|
||||||
|
|
@ -55,19 +55,31 @@ Per set per chunk, in order:
|
||||||
2. placement.isStructureChunk(state, x, z) must hold (the grid above).
|
2. placement.isStructureChunk(state, x, z) must hold (the grid above).
|
||||||
3. Single-entry sets go straight to tryGenerateStructure with **zero draws**.
|
3. Single-entry sets go straight to tryGenerateStructure with **zero draws**.
|
||||||
4. Multi-entry sets: `random = WorldgenRandom(Legacy(0));
|
4. Multi-entry sets: `random = WorldgenRandom(Legacy(0));
|
||||||
random.setLargeFeatureSeed(levelSeed, chunkX, chunkZ)` (the two-long XOR
|
random.setLargeFeatureSeed(levelSeed, chunkX, chunkPos.z)` (the two-long
|
||||||
mix), then loop:
|
XOR mix) drives ONLY the weighted picks; then loop:
|
||||||
- pick = random.nextInt(totalWeight over remaining entries)
|
- pick = random.nextInt(totalWeight over remaining entries)
|
||||||
- walk entries subtracting weight; first negative wins;
|
- walk entries subtracting weight; first negative wins;
|
||||||
- tryGenerateStructure it; on success stop, else REMOVE the entry,
|
- tryGenerateStructure it; on success stop, else REMOVE the entry,
|
||||||
total -= its weight, and repeat **without reseeding** (the stream keeps
|
total -= its weight, and repeat **without reseeding** the pick stream.
|
||||||
running).
|
5. tryGenerateStructure -> Structure.generate builds a GenerationContext whose
|
||||||
5. tryGenerateStructure -> Structure.generate builds its own
|
random comes from `GenerationContext.makeRandom(seed, chunkPos)`:
|
||||||
`WorldgenRandom(LegacyRandomSource(0))` seeded with
|
`new WorldgenRandom(new LegacyRandomSource(0))` +
|
||||||
setLargeFeatureSeed(seed, chunkX, chunkZ) — that is the `context.random()`
|
`setLargeFeatureSeed(seed, chunkX, chunkZ)`. Every attempt therefore starts
|
||||||
every findGenerationPoint draw reads from. Biome check happens inside
|
from an identically seeded FRESH stream — attempts do NOT continue each
|
||||||
generate via the structure's biomes HolderSet against the biome at the
|
other's streams.
|
||||||
generation point (position-dependent, after the stub is found).
|
6. Structure.findValidGenerationPoint runs findGenerationPoint first and filters
|
||||||
|
by isValidBiome AFTERWARDS: the 3D noise biome at the stub position
|
||||||
|
(quart-snapped coordinates) against the structure's biome set.
|
||||||
|
|
||||||
|
Port status: the whole chain above plus findSuitableY live in
|
||||||
|
world/ruined_portal.go, but nothing accepts in the ±3-chunk window around the
|
||||||
|
fixture's portal yet — every variant lands on ocean or lush-caves biomes at
|
||||||
|
its stub while vanilla accepted one here. Open leads, in order of suspicion:
|
||||||
|
(a) our 3D biome sampling off the fixture's 4x4x4 lattice may diverge from
|
||||||
|
vanilla's Climate sampler at arbitrary quart positions; (b) getBaseHeight
|
||||||
|
semantics on water columns; (c) the settle-scan corner sampling. A Java probe
|
||||||
|
dumping vanilla's own stub for seed 12345 chunk (1,0) would settle it
|
||||||
|
decisively.
|
||||||
|
|
||||||
### findGenerationPoint, in draw order
|
### findGenerationPoint, in draw order
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,3 +97,7 @@ func coldEnoughToSnow(biome string) bool {
|
||||||
temperature, ok := biomeTemperature[biome]
|
temperature, ok := biomeTemperature[biome]
|
||||||
return ok && temperature < 0.15
|
return ok && temperature < 0.15
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ColdEnoughToSnow exposes the per-biome snow threshold to structure ports.
|
||||||
|
func ColdEnoughToSnow(biome string) bool { return coldEnoughToSnow(biome) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,28 @@ type TemplateBlockInfo struct {
|
||||||
HasNBT bool
|
HasNBT bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedTemplate is a parsed .nbt structure with every palette entry mapped
|
// LoadResolvedTemplate reads a template file from disk.
|
||||||
// to concrete state IDs through resolve.
|
|
||||||
func LoadResolvedTemplate(path string, resolve func(name string, props map[string]string) (uint16, bool)) ([]TemplateBlockInfo, [3]int, error) {
|
func LoadResolvedTemplate(path string, resolve func(name string, props map[string]string) (uint16, bool)) ([]TemplateBlockInfo, [3]int, error) {
|
||||||
raw, err := readMaybeGzip(path)
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, [3]int{}, err
|
||||||
|
}
|
||||||
|
raw, err = readMaybeGzipBytes(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, [3]int{}, err
|
||||||
|
}
|
||||||
|
return LoadResolvedTemplateBytes(raw, resolve)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbeddedStructureTemplate reads a template from the embedded datapack data
|
||||||
|
// by its resource-path name, e.g. "ruined_portal/portal_1".
|
||||||
|
func EmbeddedStructureTemplate(name string) ([]byte, error) {
|
||||||
|
return dataFS.ReadFile("data/structure_template/" + name + ".nbt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadResolvedTemplateBytes parses template NBT already in memory.
|
||||||
|
func LoadResolvedTemplateBytes(raw []byte, resolve func(name string, props map[string]string) (uint16, bool)) ([]TemplateBlockInfo, [3]int, error) {
|
||||||
|
raw, err := readMaybeGzipBytes(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, [3]int{}, err
|
return nil, [3]int{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -121,11 +139,7 @@ func LoadResolvedTemplate(path string, resolve func(name string, props map[strin
|
||||||
return blocks, size, nil
|
return blocks, size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readMaybeGzip(path string) ([]byte, error) {
|
func readMaybeGzipBytes(b []byte) ([]byte, error) {
|
||||||
b, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(b) >= 2 && b[0] == 0x1f && b[1] == 0x8b {
|
if len(b) >= 2 && b[0] == 0x1f && b[1] == 0x8b {
|
||||||
gr, err := gzip.NewReader(bytes.NewReader(b))
|
gr, err := gzip.NewReader(bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -174,3 +188,4 @@ func MthGetSeed(x, y, z int) int64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,12 +86,42 @@ func (p *rawPlacement) decode() (*Placement, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RuinedPortalSetup is one weighted entry of a ruined portal structure's
|
||||||
|
// "setups" list.
|
||||||
|
type RuinedPortalSetup struct {
|
||||||
|
Placement string `json:"placement"`
|
||||||
|
AirPocketProbability float32 `json:"air_pocket_probability"`
|
||||||
|
Mossiness float32 `json:"mossiness"`
|
||||||
|
Overgrown bool `json:"overgrown"`
|
||||||
|
Vines bool `json:"vines"`
|
||||||
|
ReplaceWithBlackstone bool `json:"replace_with_blackstone"`
|
||||||
|
CanBeCold bool `json:"can_be_cold"`
|
||||||
|
Weight float32 `json:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
// StructureDef is one entry of data/minecraft/worldgen/structure.
|
// StructureDef is one entry of data/minecraft/worldgen/structure.
|
||||||
type StructureDef struct {
|
type StructureDef struct {
|
||||||
Name string `json:"-"`
|
Name string `json:"-"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Biomes string `json:"biomes"`
|
Biomes string `json:"biomes"`
|
||||||
Step string `json:"step"`
|
Step string `json:"step"`
|
||||||
|
Setups []RuinedPortalSetup `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeStructureDef(name string, raw []byte) (*StructureDef, error) {
|
||||||
|
var doc struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Biomes string `json:"biomes"`
|
||||||
|
Step string `json:"step"`
|
||||||
|
Setups []RuinedPortalSetup `json:"setups"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &StructureDef{
|
||||||
|
Name: name, Type: doc.Type, Biomes: strings.TrimPrefix(doc.Biomes, "minecraft:"),
|
||||||
|
Step: doc.Step, Setups: doc.Setups,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StructureSet is one entry of data/minecraft/worldgen/structure_set.
|
// StructureSet is one entry of data/minecraft/worldgen/structure_set.
|
||||||
|
|
@ -183,14 +213,12 @@ func loadStructureSets() (*StructureSets, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var def StructureDef
|
name := "minecraft:" + strings.TrimSuffix(baseName(path), ".json")
|
||||||
if err := json.Unmarshal(raw, &def); err != nil {
|
def, err := decodeStructureDef(name, raw)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", path, err)
|
return nil, fmt.Errorf("%s: %w", path, err)
|
||||||
}
|
}
|
||||||
name := "minecraft:" + strings.TrimSuffix(baseName(path), ".json")
|
out.Structures[name] = def
|
||||||
def.Name = name
|
|
||||||
def.Biomes = strings.TrimPrefix(def.Biomes, "minecraft:")
|
|
||||||
out.Structures[name] = &def
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFiles, err := fs.Glob(dataFS, "data/structure_set/*.json")
|
setFiles, err := fs.Glob(dataFS, "data/structure_set/*.json")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue