diff --git a/internal/worldgen/data/structure_template/ruined_portal/giant_portal_1.nbt b/internal/worldgen/data/structure_template/ruined_portal/giant_portal_1.nbt new file mode 100644 index 0000000..1fc2e2b Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/giant_portal_1.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/giant_portal_2.nbt b/internal/worldgen/data/structure_template/ruined_portal/giant_portal_2.nbt new file mode 100644 index 0000000..3aa35c9 Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/giant_portal_2.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/giant_portal_3.nbt b/internal/worldgen/data/structure_template/ruined_portal/giant_portal_3.nbt new file mode 100644 index 0000000..659803f Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/giant_portal_3.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_1.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_1.nbt new file mode 100644 index 0000000..02cd408 Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_1.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_10.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_10.nbt new file mode 100644 index 0000000..77f49b0 Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_10.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_2.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_2.nbt new file mode 100644 index 0000000..2f3d3c4 Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_2.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_3.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_3.nbt new file mode 100644 index 0000000..3252787 Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_3.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_4.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_4.nbt new file mode 100644 index 0000000..6cbedac Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_4.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_5.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_5.nbt new file mode 100644 index 0000000..aefbf66 Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_5.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_6.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_6.nbt new file mode 100644 index 0000000..5626a6f Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_6.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_7.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_7.nbt new file mode 100644 index 0000000..3b3b890 Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_7.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_8.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_8.nbt new file mode 100644 index 0000000..e2ea435 Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_8.nbt differ diff --git a/internal/worldgen/data/structure_template/ruined_portal/portal_9.nbt b/internal/worldgen/data/structure_template/ruined_portal/portal_9.nbt new file mode 100644 index 0000000..12afd6d Binary files /dev/null and b/internal/worldgen/data/structure_template/ruined_portal/portal_9.nbt differ diff --git a/internal/worldgen/structure_template.go b/internal/worldgen/structure_template.go new file mode 100644 index 0000000..99c7da4 --- /dev/null +++ b/internal/worldgen/structure_template.go @@ -0,0 +1,176 @@ +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 +} + + diff --git a/internal/worldgen/structure_template_test.go b/internal/worldgen/structure_template_test.go new file mode 100644 index 0000000..63aecb2 --- /dev/null +++ b/internal/worldgen/structure_template_test.go @@ -0,0 +1,66 @@ +package worldgen + +import ( + "os" + "path/filepath" + "testing" +) + +// TestLoadRuinedPortalTemplate loads a real vanilla template from the +// extracted datapack data and checks that every block resolves and that the +// transform keeps points inside the rotated bounding box. +func TestLoadRuinedPortalTemplate(t *testing.T) { + path := filepath.Join("data", "structure_template", "ruined_portal", "portal_1.nbt") + if _, err := os.Stat(path); err != nil { + t.Skipf("template data not installed: %v", err) + } + blocks, size, err := LoadResolvedTemplate(path, func(name string, props map[string]string) (uint16, bool) { + // The resolver is not under test here; accept every palette entry. + return 1, true + }) + if err != nil { + t.Fatal(err) + } + if len(blocks) == 0 { + t.Fatal("template parsed to zero blocks") + } + if size[0] <= 0 || size[1] <= 0 || size[2] <= 0 { + t.Fatalf("implausible size %v", size) + } + for _, b := range blocks { + if b.Pos[0] < 0 || b.Pos[0] >= size[0] || b.Pos[1] < 0 || b.Pos[1] >= size[1] || b.Pos[2] < 0 || b.Pos[2] >= size[2] { + t.Fatalf("block %v outside size %v", b.Pos, size) + } + } + + pivot := [3]int{size[0] / 2, 0, size[2] / 2} + for rotation := 0; rotation < 4; rotation++ { + for _, mirror := range []string{"none", "front_back"} { + for _, b := range blocks { + out := TransformBlockPos(b.Pos, mirror, rotation, pivot) + if out[1] != b.Pos[1] { + t.Fatalf("transform moved Y: %v -> %v", b.Pos, out) + } + } + } + } + + // A clockwise quarter turn around the pivot must map the corner (0,z) to + // (px - pz + z, y, px + pz) per the bytecode formula. + got := TransformBlockPos([3]int{0, 5, 3}, "none", 1, pivot) + wantX := pivot[0] - pivot[2] + 3 + wantZ := pivot[0] + pivot[2] - 0 + if got[0] != wantX || got[2] != wantZ { + t.Fatalf("clockwise_90 of (0,5,3) = %v, want (%d,5,%d)", got, wantX, wantZ) + } +} + +func TestMthGetSeedMatchesKnownShape(t *testing.T) { + // Sanity only: the seed is signed arithmetic; assert determinism plus the + // documented shift structure. + a := MthGetSeed(10, -40, 7) + b := MthGetSeed(10, -40, 7) + if a != b { + t.Fatalf("positional seed unstable: %d vs %d", a, b) + } +}