RegionIO/cmd/vanillacapture/main.go
Daniar Mannanov d3e645daf5 vanillacapture: capture featureless base terrain
-featureless derives a world datapack from the server jar (inner server
or bundler layout): every biome keeps its carvers but loses its feature
stages and every structure set loses its structures, so the capture is
pure noise terrain with surface rules, carvers, aquifers, and veins.
-blocks-only writes a blocks-only RIOBASE1 fixture for it. -port avoids
colliding with something else already listening on 25565.
2026-08-26 02:05:42 +03:00

507 lines
14 KiB
Go

// Command vanillacapture runs the official server for fixed chunks and writes a
// block-by-block parity fixture consumed by internal/world tests.
package main
import (
"archive/zip"
"bufio"
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"regionio/internal/world"
)
const (
fixtureMagic = "RIOPAR02"
baseFixtureMagic = "RIOBASE1"
)
type chunkPos struct{ x, z int32 }
func main() {
serverJar := flag.String("server", "server.jar", "Mojang bundler server.jar")
java := flag.String("java", "java", "Java 25 executable")
seed := flag.Int64("seed", 12345, "world seed")
chunksFlag := flag.String("chunks", "0,0;1,0;0,1;-1,-1", "semicolon-separated chunk coordinates")
output := flag.String("output", "internal/world/testdata/vanilla_overworld_12345.bin", "output fixture")
keep := flag.Bool("keep", false, "keep the temporary vanilla world")
featureless := flag.Bool("featureless", false, "install a derived datapack that empties biome features and structure sets, capturing base terrain only")
blocksOnly := flag.Bool("blocks-only", false, "write a blocks-only fixture (RIOBASE1) without biome cells or heightmaps")
port := flag.Int("port", 25565, "server listen port; change it when 25565 is taken on this host")
flag.Parse()
chunks, err := parseChunks(*chunksFlag)
if err != nil {
fatal(err)
}
jar, err := filepath.Abs(*serverJar)
if err != nil {
fatal(err)
}
if _, err := os.Stat(jar); err != nil {
fatal(fmt.Errorf("server jar: %w", err))
}
if err := requireJava25(*java); err != nil {
fatal(err)
}
work, err := os.MkdirTemp("", "regionio-vanilla-capture-")
if err != nil {
fatal(err)
}
if !*keep {
defer os.RemoveAll(work)
} else {
fmt.Fprintf(os.Stderr, "vanilla workspace: %s\n", work)
}
if *featureless {
packs := filepath.Join(work, "world", "datapacks")
if err := installFeaturelessDatapack(jar, packs); err != nil {
fatal(err)
}
}
if err := prepareServer(work, *seed, *port); err != nil {
fatal(err)
}
if err := runServer(*java, jar, work, chunks); err != nil {
fatal(err)
}
overworld := filepath.Join(work, "world", "dimensions", "minecraft", "overworld")
if *blocksOnly {
if err := writeBaseFixture(overworld, *output, *seed, chunks); err != nil {
fatal(err)
}
} else {
if err := writeFixture(overworld, *output, *seed, chunks); err != nil {
fatal(err)
}
}
fmt.Printf("wrote %s: seed %d, %d chunks\n", *output, *seed, len(chunks))
}
func requireJava25(java string) error {
cmd := exec.Command(java, "-version")
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("Java 25 is required: %w", err)
}
version := string(out)
if !strings.Contains(version, `version "25`) && !strings.Contains(version, `openjdk 25`) {
return fmt.Errorf("Java 25 is required; %s -version returned %q", java, strings.TrimSpace(version))
}
return nil
}
func prepareServer(dir string, seed int64, port int) error {
if err := os.WriteFile(filepath.Join(dir, "eula.txt"), []byte("eula=true\n"), 0o644); err != nil {
return err
}
properties := fmt.Sprintf("level-seed=%d\nonline-mode=false\nspawn-protection=0\nview-distance=2\nsimulation-distance=2\nmax-tick-time=-1\nserver-port=%d\n", seed, port)
return os.WriteFile(filepath.Join(dir, "server.properties"), []byte(properties), 0o644)
}
func runServer(java, jar, dir string, chunks []chunkPos) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, java, "-Xms1G", "-Xmx2G", "-jar", jar, "nogui")
cmd.Dir = dir
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
output, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
return err
}
events := make(chan string, 4)
scanDone := make(chan error, 1)
go func() {
scanner := bufio.NewScanner(output)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
if strings.Contains(line, "Done (") || strings.Contains(line, "Saved the game") {
events <- line
}
}
close(events)
scanDone <- scanner.Err()
}()
if err := waitFor(events, "Done (", 5*time.Minute); err != nil {
_ = stdin.Close()
_ = cmd.Wait()
return fmt.Errorf("vanilla startup: %w", err)
}
for _, chunk := range chunks {
x, z := int64(chunk.x)*16, int64(chunk.z)*16
if _, err := fmt.Fprintf(stdin, "execute in minecraft:overworld run forceload add %d %d\n", x, z); err != nil {
return err
}
}
// Force-load tickets are processed asynchronously by the server tick. Give
// terrain, decoration, and lighting time to reach FULL before flushing.
time.Sleep(20 * time.Second)
if _, err := io.WriteString(stdin, "save-all flush\n"); err != nil {
return err
}
if err := waitFor(events, "Saved the game", 5*time.Minute); err != nil {
return fmt.Errorf("vanilla save: %w", err)
}
if _, err := io.WriteString(stdin, "stop\n"); err != nil {
return err
}
_ = stdin.Close()
if err := cmd.Wait(); err != nil {
return err
}
if err := <-scanDone; err != nil {
return err
}
return ctx.Err()
}
func waitFor(lines <-chan string, text string, timeout time.Duration) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case line, ok := <-lines:
if !ok {
return errors.New("server exited before expected log message")
}
if strings.Contains(line, text) {
return nil
}
case <-timer.C:
return fmt.Errorf("timeout waiting for %q", text)
}
}
}
func writeFixture(worldDir, output string, seed int64, chunks []chunkPos) error {
store, err := world.NewStore(worldDir)
if err != nil {
return err
}
defer store.Close()
if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil {
return err
}
tmp := output + ".tmp"
f, err := os.Create(tmp)
if err != nil {
return err
}
ok := false
defer func() {
_ = f.Close()
if !ok {
_ = os.Remove(tmp)
}
}()
var header [24]byte
copy(header[:8], fixtureMagic)
binary.BigEndian.PutUint64(header[8:16], uint64(seed))
binary.BigEndian.PutUint32(header[16:20], uint32(len(chunks)))
binary.BigEndian.PutUint32(header[20:24], 4790)
if _, err := f.Write(header[:]); err != nil {
return err
}
var value [8]byte
for _, pos := range chunks {
chunk, err := store.LoadVanillaChunk(pos.x, pos.z)
if err != nil {
return fmt.Errorf("load vanilla chunk (%d,%d): %w", pos.x, pos.z, err)
}
binary.BigEndian.PutUint32(value[:4], uint32(pos.x))
binary.BigEndian.PutUint32(value[4:], uint32(pos.z))
if _, err := f.Write(value[:]); err != nil {
return err
}
for y := world.MinY; y < world.MinY+world.WorldHeight; y++ {
for z := 0; z < 16; z++ {
for x := 0; x < 16; x++ {
binary.BigEndian.PutUint16(value[:2], chunk.GetBlock(x, y, z))
if _, err := f.Write(value[:2]); err != nil {
return err
}
}
}
}
for y := world.MinY; y < world.MinY+world.WorldHeight; y += 4 {
for z := 0; z < 16; z += 4 {
for x := 0; x < 16; x += 4 {
binary.BigEndian.PutUint16(value[:2], chunk.GetBiome(x, y, z))
if _, err := f.Write(value[:2]); err != nil {
return err
}
}
}
}
heightmaps, err := store.LoadVanillaHeightmaps(pos.x, pos.z)
if err != nil {
return fmt.Errorf("load vanilla heightmaps (%d,%d): %w", pos.x, pos.z, err)
}
for _, heightmap := range heightmaps {
for _, y := range heightmap {
binary.BigEndian.PutUint16(value[:2], uint16(y))
if _, err := f.Write(value[:2]); err != nil {
return err
}
}
}
}
if err := f.Sync(); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Rename(tmp, output); err != nil {
return err
}
ok = true
return nil
}
func writeBaseFixture(worldDir, output string, seed int64, chunks []chunkPos) error {
store, err := world.NewStore(worldDir)
if err != nil {
return err
}
defer store.Close()
if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil {
return err
}
tmp := output + ".tmp"
f, err := os.Create(tmp)
if err != nil {
return err
}
ok := false
defer func() {
_ = f.Close()
if !ok {
_ = os.Remove(tmp)
}
}()
var header [16]byte
copy(header[:8], baseFixtureMagic)
binary.BigEndian.PutUint64(header[8:16], uint64(seed))
if _, err := f.Write(header[:]); err != nil {
return err
}
var value [8]byte
for _, pos := range chunks {
chunk, err := store.LoadVanillaChunk(pos.x, pos.z)
if err != nil {
return fmt.Errorf("load vanilla chunk (%d,%d): %w", pos.x, pos.z, err)
}
binary.BigEndian.PutUint32(value[:4], uint32(pos.x))
binary.BigEndian.PutUint32(value[4:], uint32(pos.z))
if _, err := f.Write(value[:]); err != nil {
return err
}
for y := world.MinY; y < world.MinY+world.WorldHeight; y++ {
for z := 0; z < 16; z++ {
for x := 0; x < 16; x++ {
binary.BigEndian.PutUint16(value[:2], chunk.GetBlock(x, y, z))
if _, err := f.Write(value[:2]); err != nil {
return err
}
}
}
}
}
if err := f.Sync(); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Rename(tmp, output); err != nil {
return err
}
ok = true
return nil
}
// installFeaturelessDatapack derives a world datapack from the server jar's own
// worldgen data: every biome keeps its carvers but loses its feature stages,
// and every structure set loses its structures. A server started with the pack
// installed therefore generates noise terrain with surface rules, carvers,
// aquifers, and noise-router veins — and nothing else — which is exactly the
// ground truth the undecorated side of our pipeline should reproduce.
//
// Both jar shapes work: the deobfuscated inner server, whose data/ entries are
// read directly, and the official bundler, which stores that same inner server
// under META-INF/versions/.
func installFeaturelessDatapack(jar, packsDir string) error {
r, err := zip.OpenReader(jar)
if err != nil {
return err
}
defer r.Close()
type source struct {
biomes map[string][]byte
sets map[string][]byte
}
read := func(entries []*zip.File) (map[string][]byte, map[string][]byte, error) {
biomes := make(map[string][]byte)
sets := make(map[string][]byte)
for _, entry := range entries {
dir, name := filepath.Split(filepath.ToSlash(entry.Name))
if !strings.HasSuffix(name, ".json") {
continue
}
raw, err := readZipEntry(entry)
if err != nil {
return nil, nil, err
}
switch dir {
case "data/minecraft/worldgen/biome/":
biomes[name] = raw
case "data/minecraft/worldgen/structure_set/":
sets[name] = raw
}
}
return biomes, sets, nil
}
topBiomes, topSets, err := read(r.File)
if err != nil {
return err
}
biomes, sets := topBiomes, topSets
if len(biomes) == 0 {
for _, entry := range r.File {
if !strings.HasPrefix(entry.Name, "META-INF/versions/") || !strings.HasSuffix(entry.Name, ".jar") {
continue
}
nestedRaw, err := readZipEntry(entry)
if err != nil {
return err
}
nested, err := zip.NewReader(bytes.NewReader(nestedRaw), int64(len(nestedRaw)))
if err != nil {
return fmt.Errorf("%s: %w", entry.Name, err)
}
nestedBiomes, nestedSets, err := read(nested.File)
if err != nil {
return err
}
if len(nestedBiomes) > 0 {
biomes, sets = nestedBiomes, nestedSets
break
}
}
}
if len(biomes) == 0 {
return errors.New("no biome JSONs found in server jar; is this a Mojang server jar?")
}
root := filepath.Join(packsDir, "regionio_featureless")
biomeDir := filepath.Join(root, "data", "minecraft", "worldgen", "biome")
setDir := filepath.Join(root, "data", "minecraft", "worldgen", "structure_set")
if err := os.MkdirAll(biomeDir, 0o755); err != nil {
return err
}
if err := os.MkdirAll(setDir, 0o755); err != nil {
return err
}
mcmeta := `{"pack":{"description":"RegionIO base-terrain capture: features and structure sets removed","min_format":[101,1],"max_format":101}}`
if err := os.WriteFile(filepath.Join(root, "pack.mcmeta"), []byte(mcmeta), 0o644); err != nil {
return err
}
for name, raw := range biomes {
var doc map[string]any
if err := json.Unmarshal(raw, &doc); err != nil {
return fmt.Errorf("biome %s: %w", name, err)
}
doc["features"] = []any{}
out, err := json.Marshal(doc)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(biomeDir, name), out, 0o644); err != nil {
return err
}
}
for name, raw := range sets {
var doc map[string]any
if err := json.Unmarshal(raw, &doc); err != nil {
return fmt.Errorf("structure set %s: %w", name, err)
}
doc["structures"] = []any{}
out, err := json.Marshal(doc)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(setDir, name), out, 0o644); err != nil {
return err
}
}
fmt.Fprintf(os.Stderr, "featureless datapack: %d biomes, %d structure sets\n", len(biomes), len(sets))
return nil
}
func readZipEntry(entry *zip.File) ([]byte, error) {
rc, err := entry.Open()
if err != nil {
return nil, err
}
defer rc.Close()
return io.ReadAll(rc)
}
func parseChunks(raw string) ([]chunkPos, error) {
parts := strings.Split(raw, ";")
chunks := make([]chunkPos, 0, len(parts))
seen := make(map[chunkPos]bool)
for _, part := range parts {
coords := strings.Split(strings.TrimSpace(part), ",")
if len(coords) != 2 {
return nil, fmt.Errorf("invalid chunk %q", part)
}
x, err := strconv.ParseInt(strings.TrimSpace(coords[0]), 10, 32)
if err != nil {
return nil, err
}
z, err := strconv.ParseInt(strings.TrimSpace(coords[1]), 10, 32)
if err != nil {
return nil, err
}
pos := chunkPos{int32(x), int32(z)}
if !seen[pos] {
chunks = append(chunks, pos)
seen[pos] = true
}
}
if len(chunks) == 0 {
return nil, errors.New("no chunks requested")
}
return chunks, nil
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, "vanillacapture:", err)
os.Exit(1)
}