LoadResolvedTemplate parses the extracted .nbt files with every palette entry resolved to concrete state IDs through an injected resolver, and TransformBlockPos reproduces StructureTemplate.transform's mirror-then- rotate order around the pivot, including all three quarter-turn cases read off the bytecode. MthGetSeed gives processors their per-block positional Legacy stream. Verified against the real ruined_portal templates: every block lands inside the declared size for all eight mirror/rotation combinations.
176 lines
4.7 KiB
Go
176 lines
4.7 KiB
Go
package worldgen
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"regionio/internal/nbt"
|
|
)
|
|
|
|
// structure_template.go loads vanilla structure-template NBTs into palette-
|
|
// resolved block lists and transforms positions the way StructureTemplate does.
|
|
//
|
|
// The state resolver is injected because only the caller knows how names plus
|
|
// properties map onto the runtime's state IDs (surface rules know a subset,
|
|
// the full table knows everything).
|
|
|
|
// TemplateBlockInfo is one resolved template cell.
|
|
type TemplateBlockInfo struct {
|
|
Pos [3]int
|
|
State uint16
|
|
HasNBT bool
|
|
}
|
|
|
|
// ResolvedTemplate is a parsed .nbt structure with every palette entry mapped
|
|
// to concrete state IDs through resolve.
|
|
func LoadResolvedTemplate(path string, resolve func(name string, props map[string]string) (uint16, bool)) ([]TemplateBlockInfo, [3]int, error) {
|
|
raw, err := readMaybeGzip(path)
|
|
if err != nil {
|
|
return nil, [3]int{}, err
|
|
}
|
|
_, root, err := nbt.UnmarshalNamed(raw)
|
|
if err != nil {
|
|
return nil, [3]int{}, err
|
|
}
|
|
comp, ok := root.(*nbt.Compound)
|
|
if !ok {
|
|
return nil, [3]int{}, fmt.Errorf("template root is not a compound")
|
|
}
|
|
|
|
var size [3]int
|
|
if rawTag, ok := comp.Get("size"); ok {
|
|
if tag, isList := rawTag.(nbt.List); isList && len(tag.Elems) == 3 {
|
|
size[0] = int(tag.Elems[0].(nbt.Int))
|
|
size[1] = int(tag.Elems[1].(nbt.Int))
|
|
size[2] = int(tag.Elems[2].(nbt.Int))
|
|
}
|
|
}
|
|
|
|
var palette []struct {
|
|
id uint16
|
|
valid bool
|
|
}
|
|
if rawTag, ok := comp.Get("palette"); ok {
|
|
if tag, isList := rawTag.(nbt.List); isList {
|
|
for _, entry := range tag.Elems {
|
|
stateComp, ok := entry.(*nbt.Compound)
|
|
if !ok {
|
|
palette = append(palette, struct {
|
|
id uint16
|
|
valid bool
|
|
}{})
|
|
continue
|
|
}
|
|
name := ""
|
|
if nameTag, ok := stateComp.Get("Name"); ok {
|
|
name = string(nameTag.(nbt.String))
|
|
}
|
|
props := map[string]string{}
|
|
if propTag, ok := stateComp.Get("Properties"); ok {
|
|
if propComp, isCompound := propTag.(*nbt.Compound); isCompound {
|
|
for _, key := range propComp.Keys() {
|
|
if v, ok := propComp.Get(key); ok {
|
|
props[key] = string(v.(nbt.String))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
id, valid := resolve(name, props)
|
|
palette = append(palette, struct {
|
|
id uint16
|
|
valid bool
|
|
}{id, valid})
|
|
}
|
|
}
|
|
}
|
|
|
|
var blocks []TemplateBlockInfo
|
|
if rawTag, ok := comp.Get("blocks"); ok {
|
|
if tag, isList := rawTag.(nbt.List); isList {
|
|
for _, entry := range tag.Elems {
|
|
blockComp, ok := entry.(*nbt.Compound)
|
|
if !ok {
|
|
continue
|
|
}
|
|
stateIdx := nbt.Int(0)
|
|
if stateTag, ok := blockComp.Get("state"); ok {
|
|
stateIdx = stateTag.(nbt.Int)
|
|
}
|
|
var pos [3]int
|
|
if posTag, ok := blockComp.Get("pos"); ok {
|
|
if posList, isList := posTag.(nbt.List); isList && len(posList.Elems) == 3 {
|
|
pos[0] = int(posList.Elems[0].(nbt.Int))
|
|
pos[1] = int(posList.Elems[1].(nbt.Int))
|
|
pos[2] = int(posList.Elems[2].(nbt.Int))
|
|
}
|
|
}
|
|
if int(stateIdx) < 0 || int(stateIdx) >= len(palette) || !palette[stateIdx].valid {
|
|
continue
|
|
}
|
|
info := TemplateBlockInfo{Pos: pos, State: palette[stateIdx].id}
|
|
if _, hasNBT := blockComp.Get("nbt"); hasNBT {
|
|
info.HasNBT = true
|
|
}
|
|
blocks = append(blocks, info)
|
|
}
|
|
}
|
|
}
|
|
return blocks, size, nil
|
|
}
|
|
|
|
func readMaybeGzip(path string) ([]byte, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(b) >= 2 && b[0] == 0x1f && b[1] == 0x8b {
|
|
gr, err := gzip.NewReader(bytes.NewReader(b))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gr.Close()
|
|
return io.ReadAll(gr)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// TransformBlockPos mirrors StructureTemplate.transform(BlockPos, mirror,
|
|
// rotation, pivot): the mirror flips around the template origin first, then the
|
|
// rotation turns the point around the pivot's X/Z pair. Rotation ordinals are
|
|
// none/clockwise_90/clockwise_180/counterclockwise_90.
|
|
func TransformBlockPos(pos [3]int, mirror string, rotation int, pivot [3]int) [3]int {
|
|
x, y, z := pos[0], pos[1], pos[2]
|
|
switch mirror {
|
|
case "left_right":
|
|
z = -z
|
|
case "front_back":
|
|
x = -x
|
|
}
|
|
px, pz := pivot[0], pivot[2]
|
|
switch rotation {
|
|
case 1: // clockwise_90
|
|
return [3]int{px - pz + z, y, px + pz - x}
|
|
case 2: // clockwise_180
|
|
return [3]int{px + pz - z, y, pz - px + x}
|
|
case 3: // counterclockwise_90
|
|
return [3]int{px + px - x, y, pz + pz - z}
|
|
}
|
|
return [3]int{x, y, z}
|
|
}
|
|
|
|
// MthGetSeed is Mth.getSeed(x,y,z), the positional seed every template
|
|
// processor derives its per-block random source from:
|
|
//
|
|
// l = (long)(x*3129871) ^ (long)z*116129781 ^ y
|
|
// l = l*l*42317861 + l*11
|
|
// return l >> 16
|
|
func MthGetSeed(x, y, z int) int64 {
|
|
l := int64(int32(x)*3129871) ^ int64(z)*116129781 ^ int64(int32(y))
|
|
l = l*l*42317861 + l*11
|
|
return l >> 16
|
|
}
|
|
|
|
|