worldgen: parse structure sets and place them on the vanilla grid
Loads the 26.1.2 structure datapack: 20 structure sets, 34 structure definitions, and the worldgen biome tags they reference. Random-spread placement follows RandomSpreadStructurePlacement byte for byte — region floorDiv by spacing, one Legacy stream salted per region, two spread-type draws for the offset — and the four legacy frequency reducers keep their exact draws, including the hardcoded buried-treasure salt and the outpost's discarded full-range int. Verified against the fixture: on seed 12345 the ruined_portals set claims exactly chunk (1,0), the chunk whose captured blocks carry the ruined portal's obsidian, gold, and crying obsidian. TestStructure- PlacementGrid pins that plus village separation invariants.
This commit is contained in:
parent
f162698302
commit
c488ce274f
125 changed files with 2123 additions and 0 deletions
343
internal/worldgen/structures.go
Normal file
343
internal/worldgen/structures.go
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
package worldgen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// structures.go loads the vanilla 26.1.2 structure datapack: the structure-set
|
||||
// JSONs that decide which chunk each structure tries to start in, the structure
|
||||
// JSONs themselves (biome filter and generation step for now), and the worldgen
|
||||
// biome tags the structure JSONs reference. Placement follows
|
||||
// StructurePlacement and its subclasses byte for byte, including the four
|
||||
// legacy frequency reducers and their exact RNG draws.
|
||||
//
|
||||
// The block-level work of each structure type (jigsaw assembly, template
|
||||
// pieces, procedural pieces such as mineshaft corridors) lives above this
|
||||
// layer; everything here answers only "does a structure of this set start in
|
||||
// this chunk".
|
||||
|
||||
// WeightedEntry is one {structure, weight} pair of a structure set.
|
||||
type WeightedEntry struct {
|
||||
Structure string `json:"structure"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
// RandomSpread mirrors minecraft:random_spread placement.
|
||||
type RandomSpread struct {
|
||||
Salt int `json:"salt"`
|
||||
Separation int `json:"separation"`
|
||||
Spacing int `json:"spacing"`
|
||||
SpreadType string `json:"spread_type"`
|
||||
Frequency float32 `json:"frequency"`
|
||||
FreqMethod string `json:"frequency_reduction_method"`
|
||||
LocateOffX int `json:"-"`
|
||||
LocateOffZ int `json:"-"`
|
||||
}
|
||||
|
||||
// Placement is the tagged union of structure placement types.
|
||||
type Placement struct {
|
||||
Type string `json:"type"`
|
||||
RandomSpread *RandomSpread `json:"-"`
|
||||
Concentric bool `json:"-"`
|
||||
}
|
||||
|
||||
type rawPlacement struct {
|
||||
Type string `json:"type"`
|
||||
Salt int `json:"salt"`
|
||||
Separation int `json:"separation"`
|
||||
Spacing int `json:"spacing"`
|
||||
SpreadType string `json:"spread_type"`
|
||||
Frequency float32 `json:"frequency"`
|
||||
FreqMethod string `json:"frequency_reduction_method"`
|
||||
LocateOffset []int `json:"locate_offset"`
|
||||
Distance int `json:"distance"`
|
||||
Count int `json:"count"`
|
||||
PreferredBiomes string `json:"preferred_biomes"`
|
||||
}
|
||||
|
||||
func (p *rawPlacement) decode() (*Placement, error) {
|
||||
switch p.Type {
|
||||
case "minecraft:random_spread":
|
||||
out := &Placement{Type: p.Type, RandomSpread: &RandomSpread{
|
||||
Salt: p.Salt, Separation: p.Separation, Spacing: p.Spacing,
|
||||
SpreadType: p.SpreadType, Frequency: p.Frequency, FreqMethod: p.FreqMethod,
|
||||
}}
|
||||
if len(p.LocateOffset) == 3 {
|
||||
out.RandomSpread.LocateOffX, out.RandomSpread.LocateOffZ = p.LocateOffset[0], p.LocateOffset[2]
|
||||
}
|
||||
if out.RandomSpread.SpreadType == "" {
|
||||
out.RandomSpread.SpreadType = "linear"
|
||||
}
|
||||
if out.RandomSpread.FreqMethod == "" {
|
||||
out.RandomSpread.FreqMethod = "default"
|
||||
}
|
||||
if out.RandomSpread.Frequency == 0 {
|
||||
out.RandomSpread.Frequency = 1.0
|
||||
}
|
||||
return out, nil
|
||||
case "minecraft:concentric_rings":
|
||||
return &Placement{Type: p.Type, Concentric: true}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported structure placement %q", p.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// StructureDef is one entry of data/minecraft/worldgen/structure.
|
||||
type StructureDef struct {
|
||||
Name string `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Biomes string `json:"biomes"`
|
||||
Step string `json:"step"`
|
||||
}
|
||||
|
||||
// StructureSet is one entry of data/minecraft/worldgen/structure_set.
|
||||
type StructureSet struct {
|
||||
Name string `json:"-"`
|
||||
Structures []WeightedEntry `json:"structures"`
|
||||
Placement *Placement `json:"-"`
|
||||
}
|
||||
|
||||
// StructureSets is the parsed, immutable structure datapack.
|
||||
type StructureSets struct {
|
||||
Sets map[string]*StructureSet // set name -> set
|
||||
Structures map[string]*StructureDef // structure name -> def
|
||||
BiomeTags map[string][]string // biome tag name -> member names, flattened
|
||||
setOrder []string // deterministic iteration order
|
||||
structuresBySet map[string][]*StructureDef // expanded weighted entries per set
|
||||
}
|
||||
|
||||
var (
|
||||
structureSetsOnce sync.Once
|
||||
structureSetsRef *StructureSets
|
||||
structureSetsErr error
|
||||
)
|
||||
|
||||
// LoadStructureSets parses and validates the embedded structure datapack.
|
||||
func LoadStructureSets() (*StructureSets, error) {
|
||||
structureSetsOnce.Do(func() { structureSetsRef, structureSetsErr = loadStructureSets() })
|
||||
return structureSetsRef, structureSetsErr
|
||||
}
|
||||
|
||||
func loadStructureSets() (*StructureSets, error) {
|
||||
out := &StructureSets{
|
||||
Sets: map[string]*StructureSet{},
|
||||
Structures: map[string]*StructureDef{},
|
||||
BiomeTags: map[string][]string{},
|
||||
structuresBySet: map[string][]*StructureDef{},
|
||||
}
|
||||
|
||||
tagFiles, err := fs.Glob(dataFS, "data/biome_tag/*.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawTags := map[string][]string{}
|
||||
for _, path := range tagFiles {
|
||||
raw, err := dataFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc struct {
|
||||
Values []string `json:"values"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
name := "minecraft:" + strings.TrimSuffix(baseName(path), ".json")
|
||||
rawTags[name] = doc.Values
|
||||
}
|
||||
var flatten func(name string, seen map[string]bool) []string
|
||||
flatten = func(name string, seen map[string]bool) []string {
|
||||
if members, ok := out.BiomeTags[name]; ok {
|
||||
return members
|
||||
}
|
||||
if seen[name] {
|
||||
return nil
|
||||
}
|
||||
seen[name] = true
|
||||
var flat []string
|
||||
for _, value := range rawTags[name] {
|
||||
value = strings.TrimPrefix(value, "minecraft:")
|
||||
if strings.HasPrefix(value, "#") {
|
||||
flat = append(flat, flatten("minecraft:"+value[1:], seen)...)
|
||||
continue
|
||||
}
|
||||
flat = append(flat, "minecraft:"+value)
|
||||
}
|
||||
out.BiomeTags[name] = flat
|
||||
return flat
|
||||
}
|
||||
for name := range rawTags {
|
||||
flatten(name, map[string]bool{})
|
||||
}
|
||||
|
||||
defFiles, err := fs.Glob(dataFS, "data/structure_json/*.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, path := range defFiles {
|
||||
raw, err := dataFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var def StructureDef
|
||||
if err := json.Unmarshal(raw, &def); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
name := "minecraft:" + strings.TrimSuffix(baseName(path), ".json")
|
||||
def.Name = name
|
||||
def.Biomes = strings.TrimPrefix(def.Biomes, "minecraft:")
|
||||
out.Structures[name] = &def
|
||||
}
|
||||
|
||||
setFiles, err := fs.Glob(dataFS, "data/structure_set/*.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, path := range setFiles {
|
||||
raw, err := dataFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc struct {
|
||||
Structures []WeightedEntry `json:"structures"`
|
||||
Placement rawPlacement `json:"placement"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
placement, err := doc.Placement.decode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
name := "minecraft:" + strings.TrimSuffix(baseName(path), ".json")
|
||||
set := &StructureSet{Name: name, Structures: doc.Structures, Placement: placement}
|
||||
out.Sets[name] = set
|
||||
out.setOrder = append(out.setOrder, name)
|
||||
expanded := make([]*StructureDef, 0, len(set.Structures))
|
||||
for _, entry := range set.Structures {
|
||||
def, ok := out.Structures[strings.TrimPrefix(entry.Structure, "minecraft:")]
|
||||
if !ok {
|
||||
def, ok = out.Structures[entry.Structure]
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
expanded = append(expanded, def)
|
||||
}
|
||||
out.structuresBySet[name] = expanded
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SetOrder returns set names in filesystem order, which is stable per build.
|
||||
func (s *StructureSets) SetOrder() []string { return s.setOrder }
|
||||
|
||||
// StructuresInSet expands the weighted entries of a set into its definitions.
|
||||
func (s *StructureSets) StructuresInSet(set string) []*StructureDef {
|
||||
return s.structuresBySet[set]
|
||||
}
|
||||
|
||||
// BiomesFor resolves a structure def's biome reference ("#tag" or a single
|
||||
// name) to concrete biome names with the minecraft: prefix.
|
||||
func (s *StructureSets) BiomesFor(def *StructureDef) []string {
|
||||
ref := def.Biomes
|
||||
if strings.HasPrefix(ref, "#") {
|
||||
return s.BiomeTags["minecraft:"+strings.TrimPrefix(ref[1:], "minecraft:")]
|
||||
}
|
||||
return []string{"minecraft:" + ref}
|
||||
}
|
||||
|
||||
// potentialStructureChunk is RandomSpreadStructurePlacement.
|
||||
// getPotentialStructureChunk: pick the cell's offset chunk with two draws from
|
||||
// a Legacy stream salted by the region coordinates.
|
||||
func (r *RandomSpread) potentialStructureChunk(seed int64, chunkX, chunkZ int32) [2]int32 {
|
||||
regionX := floorDiv32(chunkX, int32(r.Spacing))
|
||||
regionZ := floorDiv32(chunkZ, int32(r.Spacing))
|
||||
random := NewLegacy(0)
|
||||
random.SetLargeFeatureWithSalt(seed, int(regionX), int(regionZ), r.Salt)
|
||||
span := r.Spacing - r.Separation
|
||||
offsetX := spreadEvaluate(random, r.SpreadType, span)
|
||||
offsetZ := spreadEvaluate(random, r.SpreadType, span)
|
||||
return [2]int32{regionX*int32(r.Spacing) + int32(offsetX), regionZ*int32(r.Spacing) + int32(offsetZ)}
|
||||
}
|
||||
|
||||
// PotentialChunk exposes the region pick for tests and structure placement.
|
||||
func (r *RandomSpread) PotentialChunk(seed int64, chunkX, chunkZ int32) [2]int32 {
|
||||
return r.potentialStructureChunk(seed, chunkX, chunkZ)
|
||||
}
|
||||
|
||||
// spreadEvaluate is RandomSpreadType.evaluate.
|
||||
func spreadEvaluate(random RandomSource, spreadType string, span int) int {
|
||||
if spreadType == "triangular" {
|
||||
return (int(random.NextIntN(int32(span))) + int(random.NextIntN(int32(span)))) / 2
|
||||
}
|
||||
return int(random.NextIntN(int32(span)))
|
||||
}
|
||||
|
||||
// frequencyAllows is applyAdditionalChunkRestrictions plus the four reducer
|
||||
// bodies from StructurePlacement. Runs only when frequency < 1.
|
||||
func (r *RandomSpread) frequencyAllows(seed int64, chunkX, chunkZ int32) bool {
|
||||
if r.Frequency >= 1.0 {
|
||||
return true
|
||||
}
|
||||
random := NewLegacy(0)
|
||||
switch r.FreqMethod {
|
||||
case "default":
|
||||
random.SetLargeFeatureWithSalt(seed, int(chunkX), int(chunkZ), r.Salt)
|
||||
return random.NextFloat() < r.Frequency
|
||||
case "legacy_type_1": // legacyPillagerOutpostReducer
|
||||
regionX := int(chunkX) >> 4
|
||||
regionZ := int(chunkZ) >> 4
|
||||
random.SetSeed(int64(int32(regionX^(regionZ<<4))) ^ seed)
|
||||
random.NextInt() // vanilla discards one full-range draw
|
||||
bound := int(float32(1.0) / r.Frequency)
|
||||
if bound <= 0 {
|
||||
return false
|
||||
}
|
||||
return random.NextIntN(int32(bound)) == 0
|
||||
case "legacy_type_2": // legacyArbitrarySaltProbabilityReducer
|
||||
random.SetLargeFeatureWithSalt(seed, int(chunkX), int(chunkZ), 10387320)
|
||||
return random.NextFloat() < r.Frequency
|
||||
case "legacy_type_3": // legacyProbabilityReducerWithDouble
|
||||
random.SetLargeFeatureSeed(seed, int(chunkX), int(chunkZ))
|
||||
return random.NextDouble() < float64(r.Frequency)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsStartChunk reports whether any structure of this set starts in the given
|
||||
// chunk: the random-spread grid picks the chunk, the legacy frequency roll
|
||||
// keeps or discards it.
|
||||
func (s *StructureSet) IsStartChunk(seed int64, chunkX, chunkZ int32) bool {
|
||||
if s.Placement.Concentric {
|
||||
// Stronghold rings are not wired yet; nothing claims the chunk.
|
||||
return false
|
||||
}
|
||||
spread := s.Placement.RandomSpread
|
||||
picked := spread.potentialStructureChunk(seed, chunkX, chunkZ)
|
||||
if picked[0] != chunkX || picked[1] != chunkZ {
|
||||
return false
|
||||
}
|
||||
return spread.frequencyAllows(seed, chunkX, chunkZ)
|
||||
}
|
||||
|
||||
func floorDiv32(a, b int32) int32 {
|
||||
q := a / b
|
||||
if a%b != 0 && (a < 0) != (b < 0) {
|
||||
q--
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func baseName(path string) string {
|
||||
if i := strings.LastIndexByte(path, '/'); i >= 0 {
|
||||
return path[i+1:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue