Chunk persistence to Anvil .mca region files
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.
This commit is contained in:
parent
4dcf938a85
commit
d1cc29bb60
10 changed files with 305112 additions and 20 deletions
119
internal/world/state_names.go
Normal file
119
internal/world/state_names.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue