RegionIO/cmd/regionio/main.go
Master290 57214fbd76 Run the world clock so day and night happen
set_time was declared nowhere and sent never, so the client's sky was frozen
wherever it started: the sun did not move, night did not fall, and nothing in
the world had a time.

26.1.2 replaced the old (gameTime, dayTime, doDaylightCycle) triple with a
registry of named clocks. The packet now carries a fixed-width game time plus a
map from world clock to (totalTicks, partialTick, rate); the client advances
each clock locally at its rate and drives the minecraft:day timeline -- a
keyframe track over a 24000-tick period -- from the overworld clock. The clock
registry is already among the 28 we sync, so the id is looked up rather than
hardcoded and the server refuses to start if it is missing.

The counter rides the entity tick loop because that is the only loop already
running at 20 TPS. It belongs on the single authoritative tick the engine still
needs; putting a sixth ticker beside the five that exist would make that worse.
Broadcast every second, which is what vanilla does -- the client interpolates in
between, so the resend only corrects drift.

The clock also persists now. The world metadata file was written once and never
touched again; it is atomically rewritable, carries gameTime and dayTime, and a
file written before those fields existed still opens and resumes at dawn as it
did. Saved every 30 seconds alongside the chunk autosave, and once more on
shutdown after the final flush.
2026-07-27 02:45:28 +03:00

89 lines
3 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)")
port := flag.Int("port", cfg.Port, "TCP listen port")
worldDir := flag.String("world", cfg.WorldDir, "world directory (empty = in-memory only)")
maxCache := flag.Int("maxcache", cfg.MaxCachedChunks, "max cached chunks, LRU eviction (0 = unbounded)")
viewDistance := flag.Int("viewdistance", cfg.MaxViewDistance, "maximum client chunk view radius (2-16)")
flag.Parse()
cfg.WorldSeed = *seedFlag
cfg.Port = *port
cfg.WorldDir = *worldDir
cfg.MaxCachedChunks = *maxCache
cfg.MaxViewDistance = *viewDistance
log.Info("using world seed", "seed", cfg.WorldSeed, "worldDir", cfg.WorldDir,
"maxcache", cfg.MaxCachedChunks, "viewdistance", cfg.MaxViewDistance)
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)
srv.StartSpawning(saveCtx, log)
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 err := srv.SaveWorldTime(); err != nil {
log.Error("saving world clock", "err", err)
}
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
}