package world import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "sync" "regionio/internal/nbt" "regionio/internal/registry" ) // store.go is the persistence layer between the in-memory Chunk model and the // on-disk Anvil region files. It converts a Chunk to/from vanilla's chunk NBT // (26.1.2: flat root, per-section block_states/biomes, heightmaps, yPos) and // routes the compressed NBT through RegionFile. // // The store keeps one RegionFile per region (32×32 chunks), opened lazily and // cached for the process lifetime. // dataVersion26 is the Minecraft world (NBT) DataVersion for 26.1.2, captured // from versions/.../server.jar's version.json "world_version". const dataVersion26 = 4790 // generatorVersion identifies the output of the current chunk generator. Every // chunk we save carries it, and loading rejects any chunk stamped differently. // // BUMP THIS in any commit that changes what the generator produces. // // Without it a world directory silently pins whatever the generator did the // first time it ran: chunkAt prefers the store over the generator, so the // already-explored area around spawn keeps its old terrain and every later fix // looks like it did nothing in exactly the place you are standing. const generatorVersion = 17 // generatorVersionTag is the NBT key holding generatorVersion. It is namespaced // because it is ours, not part of the vanilla chunk format. const generatorVersionTag = "RegionIOGeneratorVersion" // minYSection is the on-disk "yPos": the section index at MinY (-64 → -4), // since sections are 16 blocks tall and the overworld is 24 sections from // section index -4 to 19. const minYSection = -4 // mkdirAll is a thin wrapper over os.MkdirAll kept here so the persistence // layer reads as a self-contained unit. func mkdirAll(path string) error { return os.MkdirAll(path, 0o755) } // biomeNameByID resolves a numeric biome ID back to its registry name. It scans // the synced biome registry once per call (cheap; 65 entries). Returns // "minecraft:plains" as a safe fallback for unknown IDs. func biomeNameByID(id uint16) string { for _, reg := range registry.Synced() { if reg.Name != "minecraft:worldgen/biome" { continue } if int(id) < len(reg.Entries) { return reg.Entries[id] } break } return "minecraft:plains" } // biomeIDByName is the reverse of biomeNameByID for decoding on-disk chunk NBT. func biomeIDByName(name string) uint16 { if id := registry.Index("minecraft:worldgen/biome", name); id >= 0 { return uint16(id) } return BiomePlains } // Store reads and writes chunks under a world directory's region/ folder. type Store struct { dir string mu sync.Mutex regions map[[2]int]*RegionFile metaMu sync.Mutex meta worldMetadata } const worldMetadataFile = "regionio-world.json" type worldMetadata struct { Format int `json:"format"` Seed int64 `json:"seed"` // GameTime and DayTime persist the world clock. A file written before they // existed simply lacks them, and the world resumes at dawn as it used to. GameTime int64 `json:"gameTime"` DayTime int64 `json:"dayTime"` } // NewStore opens (or creates) the world directory at dir, ensuring region/ // exists. Chunks are loaded/saved relative to dir/region. func NewStore(dir string) (*Store, error) { return newStore(dir, nil) } // NewStoreForSeed opens a persistent world and records its generation seed. // Reopening the same directory with another seed is rejected to prevent seams // between previously stored chunks and newly generated terrain. func NewStoreForSeed(dir string, seed int64) (*Store, error) { return newStore(dir, &seed) } func newStore(dir string, seed *int64) (*Store, error) { regionDir := filepath.Join(dir, "region") if err := mkdirAll(regionDir); err != nil { return nil, err } store := &Store{dir: dir, regions: make(map[[2]int]*RegionFile)} if seed != nil { meta, err := validateWorldMetadata(dir, *seed) if err != nil { return nil, err } store.meta = meta } return store, nil } // WorldTime returns the clock stored with the world. It is zero for a world // opened without a seed (which skips the metadata file) or written before the // clock was persisted. func (s *Store) WorldTime() (gameTime, dayTime int64) { s.metaMu.Lock() defer s.metaMu.Unlock() return s.meta.GameTime, s.meta.DayTime } // SaveWorldTime rewrites the metadata file with a new clock. It is a no-op for // a world with no metadata file, which has no seed to write back. func (s *Store) SaveWorldTime(gameTime, dayTime int64) error { s.metaMu.Lock() defer s.metaMu.Unlock() if s.meta.Format == 0 { return nil } if s.meta.GameTime == gameTime && s.meta.DayTime == dayTime { return nil } meta := s.meta meta.GameTime, meta.DayTime = gameTime, dayTime if err := writeWorldMetadata(s.dir, meta); err != nil { return err } s.meta = meta return nil } func validateWorldMetadata(dir string, seed int64) (worldMetadata, error) { path := filepath.Join(dir, worldMetadataFile) raw, err := os.ReadFile(path) if err == nil { var meta worldMetadata if err := json.Unmarshal(raw, &meta); err != nil { return worldMetadata{}, fmt.Errorf("world: decode %s: %w", path, err) } if meta.Format != 1 { return worldMetadata{}, fmt.Errorf("world: unsupported metadata format %d", meta.Format) } if meta.Seed != seed { return worldMetadata{}, fmt.Errorf("world: seed mismatch for %s: stored %d, configured %d", dir, meta.Seed, seed) } return meta, nil } if !os.IsNotExist(err) { return worldMetadata{}, err } meta := worldMetadata{Format: 1, Seed: seed} return meta, writeWorldMetadata(dir, meta) } // writeWorldMetadata replaces the metadata file atomically: write a temporary // beside it, fsync, then rename over the original. func writeWorldMetadata(dir string, meta worldMetadata) error { path := filepath.Join(dir, worldMetadataFile) raw, err := json.MarshalIndent(meta, "", " ") if err != nil { return err } raw = append(raw, '\n') tmp, err := os.CreateTemp(dir, ".regionio-world-*.tmp") if err != nil { return err } tmpName := tmp.Name() defer os.Remove(tmpName) if _, err := tmp.Write(raw); err != nil { tmp.Close() return err } if err := tmp.Sync(); err != nil { tmp.Close() return err } if err := tmp.Close(); err != nil { return err } return os.Rename(tmpName, path) } // regionFor returns the cached RegionFile for the chunk's region, opening it on // first use. Caller is responsible for any higher-level locking; the RegionFile // itself is goroutine-safe. func (s *Store) regionFor(cx, cz int32) (*RegionFile, error) { rx, rz, _, _ := regionIndex(cx, cz) key := [2]int{rx, rz} s.mu.Lock() rf, ok := s.regions[key] s.mu.Unlock() if ok { return rf, nil } rf, err := OpenRegion(filepath.Join(s.dir, "region"), rx, rz) if err != nil { return nil, err } s.mu.Lock() // Another goroutine may have opened the same region concurrently. if existing, dup := s.regions[key]; dup { rf.Close() rf = existing } else { s.regions[key] = rf } s.mu.Unlock() return rf, nil } // LoadChunk reads and decodes the chunk at (cx, cz). It returns ErrChunkNotFound // when the chunk is not stored. func (s *Store) LoadChunk(cx, cz int32) (*Chunk, error) { return s.loadChunk(cx, cz, true) } // LoadVanillaChunk reads an official-server chunk without requiring RegionIO's // generator stamp. It exists for parity tooling; runtime world loading must use // LoadChunk so stale RegionIO terrain still regenerates. func (s *Store) LoadVanillaChunk(cx, cz int32) (*Chunk, error) { return s.loadChunk(cx, cz, false) } // LoadVanillaHeightmaps reads the three packed heightmaps directly from an // official chunk. Unlike Chunk.ParityHeightmaps this does not recompute them // with RegionIO predicates, so parity reports remain independent. func (s *Store) LoadVanillaHeightmaps(cx, cz int32) ([3][256]int16, error) { var out [3][256]int16 rx, rz, lx, lz := regionIndex(cx, cz) rf, err := s.regionFor(cx, cz) if err != nil { return out, err } raw, err := rf.ReadChunk(lx, lz) if err != nil { return out, err } _, tag, err := nbt.UnmarshalNamed(raw) if err != nil { return out, err } root, ok := tag.(*nbt.Compound) if !ok { return out, errors.New("world: vanilla chunk root is not a compound") } if int32(nbtAsInt(root, "xPos")) != int32(rx*32+lx) || int32(nbtAsInt(root, "zPos")) != int32(rz*32+lz) { return out, errors.New("world: vanilla heightmap chunk coordinates mismatch") } heightmaps, ok := root.Get("Heightmaps") if !ok { return out, errors.New("world: vanilla chunk missing Heightmaps") } compound, ok := heightmaps.(*nbt.Compound) if !ok { return out, errors.New("world: vanilla Heightmaps is not a compound") } for kind, name := range []string{"WORLD_SURFACE", "MOTION_BLOCKING", "MOTION_BLOCKING_NO_LEAVES"} { tag, ok := compound.Get(name) if !ok { return out, fmt.Errorf("world: vanilla Heightmaps missing %s", name) } longs, ok := tag.(nbt.LongArray) if !ok || len(longs) != 37 { return out, fmt.Errorf("world: vanilla heightmap %s has invalid length", name) } for index := 0; index < 256; index++ { longIndex := index / 7 bitOffset := uint((index % 7) * 9) value := (uint64(longs[longIndex]) >> bitOffset) & 0x1ff out[kind][index] = int16(MinY + int(value) - 1) } } return out, nil } func (s *Store) loadChunk(cx, cz int32, requireGeneratorVersion bool) (*Chunk, error) { rx, rz, lx, lz := regionIndex(cx, cz) rf, err := s.regionFor(cx, cz) if err != nil { return nil, err } raw, err := rf.ReadChunk(lx, lz) if err != nil { return nil, err } _, tag, err := nbt.UnmarshalNamed(raw) if err != nil { return nil, fmt.Errorf("world: decode chunk (%d,%d) NBT: %w", cx, cz, err) } root, ok := tag.(*nbt.Compound) if !ok { return nil, fmt.Errorf("world: chunk (%d,%d) root is not a compound", cx, cz) } return nbtToChunkVersioned(root, rx, rz, lx, lz, requireGeneratorVersion) } // SaveChunk encodes the chunk and writes it to its region file. func (s *Store) SaveChunk(c *Chunk) error { snapshot, _ := c.snapshot() return s.saveSnapshot(snapshot) } // saveSnapshot writes a detached chunk snapshot without copying it again. func (s *Store) saveSnapshot(c *Chunk) error { rf, err := s.regionFor(c.X, c.Z) if err != nil { return err } raw := nbt.MarshalNamed("", chunkToNBT(c)) _, _, lx, lz := regionIndex(c.X, c.Z) return rf.WriteChunk(lx, lz, raw) } // Close releases all open region files. Called on shutdown. func (s *Store) Close() error { s.mu.Lock() defer s.mu.Unlock() var firstErr error for _, rf := range s.regions { if err := rf.Close(); err != nil && firstErr == nil { firstErr = err } } s.regions = nil return firstErr } // chunkToNBT builds the on-disk NBT for a chunk. The wire Encode() format is // not reusable here: disk uses named palettes and per-section biomes. // // The layout is vanilla Anvil, flat at the root. It used to nest everything // under a "Level" compound, which is where chunk data lived until 1.18 and // where SerializableChunkData has not looked since — so nothing outside this // package could read our region files, and we could not read a world the // official server generated. That last part is what the surface-height parity // capture needs. func chunkToNBT(c *Chunk) *nbt.Compound { root := nbt.NewCompound(). Set("DataVersion", nbt.Int(dataVersion26)). Set(generatorVersionTag, nbt.Int(generatorVersion)). Set("xPos", nbt.Int(c.X)). Set("zPos", nbt.Int(c.Z)). Set("yPos", nbt.Int(int32(minYSection))). Set("Status", nbt.String("minecraft:full")). Set("LastUpdate", nbt.Long(0)). Set("InhabitedTime", nbt.Long(0)) if c.lightReady { root.Set("isLightOn", nbt.Byte(1)) } // Sections: one compound per vertical section, including empty ones so the // section Y range is contiguous (vanilla expects all sections present for // the full height, though absent sections are tolerated as air). sections := nbt.List{ElemID: nbt.TagCompound} for si := 0; si < SectionCount; si++ { sections.Elems = append(sections.Elems, sectionToNBT(c, si)) } root.Set("sections", sections) root.Set("Heightmaps", buildHeightmaps(c)) // Required-but-empty fields so vanilla loads the chunk without complaints. root.Set("block_entities", nbt.List{ElemID: nbt.TagCompound}) root.Set("structures", nbt.NewCompound()) return root } // sectionToNBT builds one section compound: Y + block_states + biomes. Palettes // are emitted even for single-value sections (no "data" array) which vanilla // reads as "the whole section is this one entry". func sectionToNBT(c *Chunk, si int) *nbt.Compound { yIdx := int32(si + minYSection) // Vanilla writes Y as a byte and reads it with getByteOr; an Int here makes // every section decode as index 0 on the other side. sec := nbt.NewCompound().Set("Y", nbt.Byte(int8(yIdx))) // Block states: build a palette of distinct IDs in the section, then a packed // long array of indices (only when more than one distinct value). var palette []uint16 indexOf := map[uint16]int{} blockStates := nbt.NewCompound() hasBlocks := c.sections[si] != nil if hasBlocks { s := c.sections[si] // Collect palette in first-seen order. for _, id := range s { if _, ok := indexOf[id]; !ok { indexOf[id] = len(palette) palette = append(palette, id) } } palList := nbt.List{ElemID: nbt.TagCompound} for _, id := range palette { palList.Elems = append(palList.Elems, blockPaletteEntry(id)) } blockStates.Set("palette", palList) if bits := blockStorageBits(len(palette)); bits > 0 { blockStates.Set("data", packIndices(s[:], indexOf, bits)) } } else { // Empty section → air palette, no data. blockStates.Set("palette", nbt.List{ ElemID: nbt.TagCompound, Elems: []nbt.Tag{blockPaletteEntry(StateAir)}, }) } sec.Set("block_states", blockStates) // Biomes: 4×4×4 cells. Per-section array if present, else the uniform biome. biomes := nbt.NewCompound() biomePalette := []uint16{c.biome} biomeIndexOf := map[uint16]int{c.biome: 0} if c.biomes[si] != nil { biomePalette = biomePalette[:0] biomeIndexOf = map[uint16]int{} for _, id := range c.biomes[si] { if _, ok := biomeIndexOf[id]; !ok { biomeIndexOf[id] = len(biomePalette) biomePalette = append(biomePalette, id) } } } biomePalList := nbt.List{ElemID: nbt.TagString} for _, id := range biomePalette { biomePalList.Elems = append(biomePalList.Elems, nbt.String(biomeNameByID(id))) } biomes.Set("palette", biomePalList) if bits := biomeStorageBits(len(biomePalette)); c.biomes[si] != nil && bits > 0 { biomes.Set("data", packIndices(c.biomes[si][:], biomeIndexOf, bits)) } sec.Set("biomes", biomes) if c.lightReady { if sky := c.skyLight[si]; sky != nil { sec.Set("SkyLight", nbt.ByteArray(append([]byte(nil), sky[:]...))) } if block := c.blockLight[si]; block != nil { sec.Set("BlockLight", nbt.ByteArray(append([]byte(nil), block[:]...))) } } return sec } // buildHeightmaps emits a minimal WORLD_SURFACE heightmap (the first non-air // block per column, packed 9 bits/value, 7 per long like vanilla). Other // heightmaps are omitted; vanilla recomputes what it needs. func buildHeightmaps(c *Chunk) *nbt.Compound { const bits = 9 longs := make(nbt.LongArray, 37) // 256 values × 9 bits / 64 ≈ 36, +1 perLong := 64 / bits // 7 for x := 0; x < 16; x++ { for z := 0; z < 16; z++ { h := topNonAirY(c, x, z) // heightmap value is (y - MinY + 1); store absolute block count. val := int64(h - MinY + 1) if val < 0 { val = 0 } idx := z*16 + x longIdx := idx / perLong bitOff := (idx % perLong) * bits longs[longIdx] |= val << uint(bitOff) } } return nbt.NewCompound().Set("WORLD_SURFACE", longs) } // topNonAirY returns the Y of the highest non-air block in column (x,z), or // MinY-1 if the column is empty. func topNonAirY(c *Chunk, x, z int) int { for si := SectionCount - 1; si >= 0; si-- { s := c.sections[si] if s == nil { continue } for ly := 15; ly >= 0; ly-- { if s[blockIndex(x, MinY+si*16+ly, z)] != StateAir { return MinY + si*16 + ly } } } return MinY - 1 } // blockStorageBits is Strategy$1.getConfigurationForPaletteSize(...).bitsInStorage() // for a block palette: nothing at all for a single entry, and never fewer than // four bits otherwise. Vanilla's tableswitch sends bit counts 1 through 4 all to // the same four-bit linear configuration, so a palette of 2..16 states is stored // four bits wide even though two would fit. Packing it tighter, as we did, // produces a long array of the wrong length and vanilla refuses the section. func blockStorageBits(paletteSize int) int { bits := bitsFor(paletteSize) if bits > 0 && bits < 4 { return 4 } return bits } // biomeStorageBits is the same for a biome palette, where Strategy$2 has no // floor: the width really is ceil(log2(size)), and a Global configuration above // three bits still stores palette indices, just at its own width. func biomeStorageBits(paletteSize int) int { return bitsFor(paletteSize) } // packIndices packs a slice of IDs into a long array at the given bit width, // with no value spanning a long boundary — vanilla's SimpleBitStorage layout. // A width of zero means the container carries no data array at all. func packIndices(ids []uint16, indexOf map[uint16]int, bits int) nbt.LongArray { if bits < 1 { return nil } perLong := 64 / bits numLongs := (len(ids) + perLong - 1) / perLong longs := make(nbt.LongArray, numLongs) for i, id := range ids { idx := int64(indexOf[id]) longIdx := i / perLong bitOff := (i % perLong) * bits longs[longIdx] |= idx << uint(bitOff) } return longs } // nbtToChunk decodes the Level-nested chunk NBT back into a Chunk. The chunk's // absolute coordinates are derived from the on-disk xPos/zPos (authoritative); // the region/local coords passed in are used only to validate. func nbtToChunk(root *nbt.Compound, regionX, regionZ, localX, localZ int) (*Chunk, error) { return nbtToChunkVersioned(root, regionX, regionZ, localX, localZ, true) } func nbtToChunkVersioned(root *nbt.Compound, regionX, regionZ, localX, localZ int, requireGeneratorVersion bool) (*Chunk, error) { // Reject anything the current generator did not produce so the caller // regenerates instead of serving stale terrain. Chunks written before the // stamp existed have no tag and decode as 0, so they are invalidated too. // This is per-chunk on purpose: the world metadata file guards the seed, // which is a hard mismatch, while a generator change is routine and should // quietly regenerate rather than refuse to open the world. if requireGeneratorVersion && nbtAsInt(root, generatorVersionTag) != generatorVersion { return nil, ErrChunkNotFound } cx := int32(nbtAsInt(root, "xPos")) cz := int32(nbtAsInt(root, "zPos")) wantX := int32(regionX*32 + localX) wantZ := int32(regionZ*32 + localZ) if cx != wantX || cz != wantZ { return nil, fmt.Errorf("world: chunk coordinates (%d,%d) do not match region slot (%d,%d)", cx, cz, wantX, wantZ) } c := &Chunk{X: cx, Z: cz, biome: BiomePlains} if lightTag, ok := root.Get("isLightOn"); ok { if enabled, ok := lightTag.(nbt.Byte); ok && enabled != 0 { c.lightReady = true } } secTag, ok := root.Get("sections") if !ok { return nil, errors.New("world: chunk NBT missing sections") } secList, ok := secTag.(nbt.List) if !ok || secList.ElemID != nbt.TagCompound { return nil, errors.New("world: chunk sections is not a compound list") } seenSections := make(map[int]bool, len(secList.Elems)) for index, st := range secList.Elems { sc, ok := st.(*nbt.Compound) if !ok { return nil, fmt.Errorf("world: section %d is not a compound", index) } yIdx, ok := nbtAsSectionY(sc, "Y") if !ok { return nil, fmt.Errorf("world: section %d has no valid Y", index) } si := yIdx - minYSection if si < 0 || si >= SectionCount { continue } if seenSections[si] { return nil, fmt.Errorf("world: duplicate section Y %d", yIdx) } seenSections[si] = true if err := readBlockStates(c, si, sc); err != nil { return nil, fmt.Errorf("world: section Y %d block states: %w", yIdx, err) } if err := readBiomes(c, si, sc); err != nil { return nil, fmt.Errorf("world: section Y %d biomes: %w", yIdx, err) } readLightSection(c, si, sc) } return c, nil } func readLightSection(c *Chunk, si int, sc *nbt.Compound) { read := func(name string) *[2048]byte { tag, ok := sc.Get(name) if !ok { return nil } data, ok := tag.(nbt.ByteArray) if !ok || len(data) != 2048 { c.lightReady = false return nil } out := new([2048]byte) copy(out[:], data) return out } c.skyLight[si] = read("SkyLight") c.blockLight[si] = read("BlockLight") } // readBlockStates decodes a section's block_states {palette, data?} into the // chunk's section array. A palette of size 1 fills the whole section; otherwise // the packed data array is unpacked. func readBlockStates(c *Chunk, si int, sc *nbt.Compound) error { bsTag, ok := sc.Get("block_states") if !ok { return errors.New("missing block_states") } bs, ok := bsTag.(*nbt.Compound) if !ok { return errors.New("block_states is not a compound") } palTag, ok := bs.Get("palette") if !ok { return errors.New("missing palette") } pal, ok := palTag.(nbt.List) if !ok || pal.ElemID != nbt.TagCompound { return errors.New("palette is not a compound list") } if len(pal.Elems) == 0 || len(pal.Elems) > totalBlockStates { return fmt.Errorf("palette size %d out of range", len(pal.Elems)) } // Decode palette entries to state IDs. ids := make([]uint16, len(pal.Elems)) for i, e := range pal.Elems { ec, ok := e.(*nbt.Compound) if !ok { return fmt.Errorf("palette entry %d is not a compound", i) } nameTag, ok := ec.Get("Name") if !ok { return fmt.Errorf("palette entry %d has no Name", i) } nameValue, ok := nameTag.(nbt.String) if !ok || nameValue == "" { return fmt.Errorf("palette entry %d has invalid Name", i) } name := string(nameValue) props := readProps(ec) var resolved bool ids[i], resolved = nameToStateID(name, props) if !resolved { return fmt.Errorf("unknown block state %q", name) } } c.section(si) // ensure allocated s := c.sections[si] if len(ids) == 1 { var fill [sectionVol]uint16 for i := range fill { fill[i] = ids[0] } c.sections[si] = &fill return nil } dataTag, ok := bs.Get("data") if !ok { return errors.New("multi-entry palette has no data") } data, ok := dataTag.(nbt.LongArray) if !ok { return errors.New("data is not a long array") } bits := blockStorageBits(len(ids)) if err := validatePackedData(len(s), bits, data); err != nil { return err } return unpackIndices(s[:], ids, data, bits) } // readBiomes decodes a section's biomes {palette, data?} into the per-cell array. func readBiomes(c *Chunk, si int, sc *nbt.Compound) error { bTag, ok := sc.Get("biomes") if !ok { return errors.New("missing biomes") } bc, ok := bTag.(*nbt.Compound) if !ok { return errors.New("biomes is not a compound") } palTag, ok := bc.Get("palette") if !ok { return errors.New("missing palette") } pal, ok := palTag.(nbt.List) if !ok || pal.ElemID != nbt.TagString { return errors.New("palette is not a string list") } if len(pal.Elems) == 0 || len(pal.Elems) > totalBiomes { return fmt.Errorf("palette size %d out of range", len(pal.Elems)) } ids := make([]uint16, len(pal.Elems)) for i, e := range pal.Elems { name, ok := e.(nbt.String) if !ok { return fmt.Errorf("palette entry %d is not a string", i) } id := registry.Index("minecraft:worldgen/biome", string(name)) if id < 0 { return fmt.Errorf("unknown biome %q", name) } ids[i] = uint16(id) } if len(ids) == 1 { cells := new([biomeCellsPerSection]uint16) for i := range cells { cells[i] = ids[0] } c.biomes[si] = cells return nil } dataTag, ok := bc.Get("data") if !ok { return errors.New("multi-entry palette has no data") } data, ok := dataTag.(nbt.LongArray) if !ok { return errors.New("data is not a long array") } bits := biomeStorageBits(len(ids)) cells := new([biomeCellsPerSection]uint16) if err := validatePackedData(len(cells), bits, data); err != nil { return err } if err := unpackIndices(cells[:], ids, data, bits); err != nil { return err } c.biomes[si] = cells return nil } func readProps(c *nbt.Compound) map[string]string { pTag, ok := c.Get("Properties") if !ok { return nil } pc, ok := pTag.(*nbt.Compound) if !ok { return nil } out := make(map[string]string, pc.Len()) for _, k := range pc.Keys() { v, _ := pc.Get(k) if s, ok := v.(nbt.String); ok { out[k] = string(s) } } return out } func nbtAsInt(c *nbt.Compound, name string) int32 { if t, ok := c.Get(name); ok { if v, ok := t.(nbt.Int); ok { return int32(v) } } return 0 } // nbtAsSectionY reads a section index, which vanilla writes as a byte. It also // accepts a short or an int so a chunk written before we matched vanilla still // decodes, and reports whether the tag was there at all — a section with no Y // is not section 0, it is malformed. func nbtAsSectionY(c *nbt.Compound, name string) (int, bool) { t, ok := c.Get(name) if !ok { return 0, false } switch v := t.(type) { case nbt.Byte: return int(int8(v)), true case nbt.Short: return int(int16(v)), true case nbt.Int: return int(int32(v)), true } return 0, false } func nbtAsString(c *nbt.Compound, name string) nbt.String { if t, ok := c.Get(name); ok { if v, ok := t.(nbt.String); ok { return v } } return "minecraft:air" } // unpackIndices reverses packIndices: fills dst with palette IDs using the // packed long array. func validatePackedData(entries, bits int, data nbt.LongArray) error { if bits < 1 { return errors.New("invalid zero-bit packed data") } perLong := 64 / bits want := (entries + perLong - 1) / perLong if len(data) != want { return fmt.Errorf("packed data has %d longs, want %d", len(data), want) } return nil } func unpackIndices(dst []uint16, ids []uint16, data nbt.LongArray, bits int) error { if bits < 1 { return errors.New("invalid zero-bit packed data") } perLong := 64 / bits mask := int64(1)<> uint(bitOff)) & mask) if idx < 0 || idx >= len(ids) { return fmt.Errorf("palette index %d out of range %d", idx, len(ids)) } dst[i] = ids[idx] } return nil }