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.
119 lines
3.2 KiB
Go
119 lines
3.2 KiB
Go
package world
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"sync"
|
|
|
|
"regionio/internal/nbt"
|
|
)
|
|
|
|
// state_names.go bridges in-memory uint16 block-state IDs and the named palette
|
|
// form used by on-disk chunk NBT ({Name, Properties}). The wire/network format
|
|
// uses integer IDs; the disk format uses names, so serialization needs both
|
|
// directions. The table is built once from the embedded blocks.json report.
|
|
|
|
//go:embed blocks.json
|
|
var blocksReportJSON []byte
|
|
|
|
// stateName describes one block state for the on-disk palette.
|
|
type stateName struct {
|
|
Name string
|
|
Properties map[string]string // nil when the block has no state properties
|
|
}
|
|
|
|
var (
|
|
stateByIDOnce sync.Once
|
|
stateByIDImpl map[uint16]stateName
|
|
)
|
|
|
|
// stateByID returns the named form of a block-state ID, building the lookup
|
|
// table on first use. The default state of each block (or its lowest-id state)
|
|
// is recorded; this matches what our generator emits, where each StateXxx
|
|
// constant is the default-state ID. For multi-state blocks the full ID→state
|
|
// map is loaded so every state round-trips.
|
|
func stateByID(id uint16) (stateName, bool) {
|
|
stateByIDOnce.Do(buildStateTable)
|
|
s, ok := stateByIDImpl[id]
|
|
return s, ok
|
|
}
|
|
|
|
func buildStateTable() {
|
|
var blocks map[string]struct {
|
|
States []struct {
|
|
ID int `json:"id"`
|
|
Properties map[string]string `json:"properties"`
|
|
} `json:"states"`
|
|
}
|
|
if err := json.Unmarshal(blocksReportJSON, &blocks); err != nil {
|
|
panic("world: parsing embedded blocks.json: " + err.Error())
|
|
}
|
|
stateByIDImpl = make(map[uint16]stateName, 30000)
|
|
for name, b := range blocks {
|
|
for _, s := range b.States {
|
|
if s.ID < 0 || s.ID > 65535 {
|
|
continue
|
|
}
|
|
stateByIDImpl[uint16(s.ID)] = stateName{Name: name, Properties: s.Properties}
|
|
}
|
|
}
|
|
}
|
|
|
|
// blockPaletteEntry builds the NBT compound for a block-state ID: {Name,
|
|
// Properties} (Properties omitted when empty). Unknown IDs map to air.
|
|
func blockPaletteEntry(id uint16) *nbt.Compound {
|
|
s, ok := stateByID(id)
|
|
if !ok {
|
|
s = stateName{Name: "minecraft:air"}
|
|
}
|
|
c := nbt.NewCompound().Set("Name", nbt.String(s.Name))
|
|
if len(s.Properties) > 0 {
|
|
props := nbt.NewCompound()
|
|
for k, v := range s.Properties {
|
|
props.Set(k, nbt.String(v))
|
|
}
|
|
c.Set("Properties", props)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// paletteEntryKey is a stable hashable key for deduplicating palette entries by
|
|
// (name, properties) during reverse lookup.
|
|
type paletteEntryKey struct {
|
|
name string
|
|
sig string
|
|
}
|
|
|
|
// nameToStateID returns the block-state ID for a (name, properties) pair from
|
|
// the loaded table. It is used when decoding on-disk chunk NBT back into a
|
|
// Chunk. Unknown names/properties map to air (0).
|
|
func nameToStateID(name string, props map[string]string) uint16 {
|
|
stateByIDOnce.Do(buildStateTable)
|
|
for id, s := range stateByIDImpl {
|
|
if s.Name != name {
|
|
continue
|
|
}
|
|
if propsMatch(s.Properties, props) {
|
|
return id
|
|
}
|
|
}
|
|
// Fall back to any state of that block if properties don't match exactly.
|
|
for id, s := range stateByIDImpl {
|
|
if s.Name == name {
|
|
return id
|
|
}
|
|
}
|
|
return StateAir
|
|
}
|
|
|
|
func propsMatch(a, b map[string]string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for k, v := range a {
|
|
if b[k] != v {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|