The world now survives restarts: chunks load from disk (read-through cache) and player edits persist via async autosave + a final SaveAll on shutdown. RegionIO finally does region I/O. - world/regionfile.go: Anvil .mca container — 8192-byte header (offset + timestamp tables), 4096-byte sectors, zlib chunk records. - world/compress.go: zlib deflate/inflate for chunk payloads. - world/store.go: chunk <-> Level-nested NBT (per-section block_states/biomes palettes, WORLD_SURFACE heightmap, DataVersion 4790, yPos -4) via the existing nbt package; Store opens one RegionFile per region with proper floor-division coords. - world/state_names.go: id->name bridge from the embedded blocks.json report so network int-IDs round-trip through the disk named palette. - world/encode.go: GetBiome read accessor for serialization. - world/cache.go: read-through (disk then generation), dirty tracking, StartAutosave (returns a done channel so the saver exits before Close), SaveAll, NewCacheWithStore. - server.go + main.go: Config.WorldDir (default "world"), -world flag, autosave loop every 30s, SaveAll + store Close on signal. - Tests: region round-trip/absent/overwrite, chunk NBT round-trip, end-to-end save-reload, negative chunk coords, autosave persistence.
77 lines
2.4 KiB
Go
77 lines
2.4 KiB
Go
// Command regionio starts a RegionIO Minecraft server core.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"regionio/internal/network"
|
|
"regionio/internal/server"
|
|
)
|
|
|
|
func main() {
|
|
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
}))
|
|
|
|
cfg := server.DefaultConfig()
|
|
|
|
// World seed: -seed flag takes precedence, then REGIONIO_SEED env, then the
|
|
// default (0). Accepted formats: decimal, or "0x" hex. An invalid value is
|
|
// fatal — a wrong seed silently generates a different world than intended.
|
|
seedFlag := flag.Int64("seed", parseSeedEnv(os.Getenv("REGIONIO_SEED"), cfg.WorldSeed, log),
|
|
"world seed (overrides REGIONIO_SEED)")
|
|
worldDir := flag.String("world", cfg.WorldDir, "world directory (empty = in-memory only)")
|
|
flag.Parse()
|
|
cfg.WorldSeed = *seedFlag
|
|
cfg.WorldDir = *worldDir
|
|
log.Info("using world seed", "seed", cfg.WorldSeed, "worldDir", cfg.WorldDir)
|
|
|
|
srv, err := server.New(cfg)
|
|
if err != nil {
|
|
log.Error("failed to create server", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Start the chunk autosave loop. It flushes dirty chunks every 30s and does
|
|
// a final SaveAll when the context (cancelled by signal) ends.
|
|
saveCtx, saveStop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
autosaveDone := srv.Chunks().StartAutosave(saveCtx, log, 30*time.Second)
|
|
|
|
ln := network.NewListener(srv, log)
|
|
|
|
// ListenAndServe blocks until the listener stops (on the same signals).
|
|
// The autosave context is separate so the final flush runs after the
|
|
// listener exits; stop it here to trigger that final SaveAll, then wait for
|
|
// the saver to finish before releasing the store file handles.
|
|
if err := ln.ListenAndServe(saveCtx); err != nil {
|
|
log.Error("server stopped", "err", err)
|
|
}
|
|
saveStop()
|
|
<-autosaveDone
|
|
if srv.Store() != nil {
|
|
srv.Store().Close()
|
|
}
|
|
}
|
|
|
|
// parseSeedEnv parses the REGIONIO_SEED env var. It returns fallback when the
|
|
// variable is empty, and logs+returns fallback when parsing fails (so a typo
|
|
// does not silently change the world).
|
|
func parseSeedEnv(raw string, fallback int64, log *slog.Logger) int64 {
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
// strconv.ParseInt with base 0 handles decimal and "0x" hex prefixes.
|
|
v, err := strconv.ParseInt(raw, 0, 64)
|
|
if err != nil {
|
|
log.Error("invalid REGIONIO_SEED, falling back to default", "raw", raw, "err", err)
|
|
return fallback
|
|
}
|
|
return v
|
|
}
|