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.
343 lines
11 KiB
Go
343 lines
11 KiB
Go
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
|
|
}
|
|
|