blocks.json lists a block's states in getPossibleStates() order -- the property cartesian product -- and separately marks which one is the default. The parser declared only id and properties, so the default flag was dropped on the floor and nameToStateID returned states[0]. Those differ for 642 of the 1168 blocks. What that produced, measured rather than guessed: every redstone vein in the world was lit=true and glowing, every sunflower was placed as its own top half with nothing under it, and oak stairs came out upside down and waterlogged. It also quietly disagreed with the StateGrass, StateOakLog and StateOakLeaf constants next door in encode.go, which are the real defaults. Now it starts from the default state and applies the properties it recognises, keeping the default's value for an unknown key or an illegal value -- which is what vanilla does when it reads a palette entry. That matters on the disk path: a chunk written with a property we no longer know used to decode to a random corner state instead of something sane. The signature grows an ok result, because air was doing double duty as both a real block and "no such name". Separately, blockPaletteEntry filled the Properties compound by ranging a Go map. nbt.Compound preserves insertion order precisely so encoding is deterministic, so saving one chunk twice produced different region-file bytes for every multi-property block. Keys are sorted now.
197 lines
5.9 KiB
Go
197 lines
5.9 KiB
Go
package world
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"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
|
|
idsByName map[string][]uint16
|
|
defaultByName map[string]uint16
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// supportsEntitySpawn distinguishes collision floors from decorative blocks.
|
|
// Light opacity is not sufficient here: stairs and slabs can have opacity zero
|
|
// while still supporting an entity.
|
|
func supportsEntitySpawn(id uint16) bool {
|
|
if id == StateAir || id == StateWater {
|
|
return false
|
|
}
|
|
if lightOpacity(id) > 0 {
|
|
return true
|
|
}
|
|
state, ok := stateByID(id)
|
|
if !ok {
|
|
return false
|
|
}
|
|
name := state.Name
|
|
for _, suffix := range []string{
|
|
"_sapling", "_flower", "_tulip", "_mushroom", "_torch",
|
|
"_rail", "_button", "_pressure_plate", "_carpet", "_banner",
|
|
"_sign", "_hanging_sign",
|
|
} {
|
|
if strings.HasSuffix(name, suffix) {
|
|
return false
|
|
}
|
|
}
|
|
switch name {
|
|
case "minecraft:short_grass", "minecraft:tall_grass", "minecraft:fern",
|
|
"minecraft:large_fern", "minecraft:dead_bush", "minecraft:dandelion",
|
|
"minecraft:poppy", "minecraft:allium", "minecraft:azure_bluet",
|
|
"minecraft:oxeye_daisy", "minecraft:cornflower",
|
|
"minecraft:lily_of_the_valley", "minecraft:sunflower":
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func buildStateTable() {
|
|
var blocks map[string]struct {
|
|
States []struct {
|
|
ID int `json:"id"`
|
|
Default bool `json:"default"`
|
|
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)
|
|
idsByName = make(map[string][]uint16, len(blocks))
|
|
defaultByName = make(map[string]uint16, len(blocks))
|
|
for name, b := range blocks {
|
|
for _, s := range b.States {
|
|
if s.ID < 0 || s.ID > 65535 {
|
|
continue
|
|
}
|
|
id := uint16(s.ID)
|
|
stateByIDImpl[id] = stateName{Name: name, Properties: s.Properties}
|
|
idsByName[name] = append(idsByName[name], id)
|
|
if s.Default {
|
|
defaultByName[name] = id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// blockPaletteEntry builds the NBT compound for a block-state ID: {Name,
|
|
// Properties} (Properties omitted when empty). Unknown IDs map to air.
|
|
//
|
|
// Property keys are sorted. nbt.Compound preserves insertion order so that
|
|
// encoding is deterministic, but ranging a Go map is not: the same chunk saved
|
|
// twice produced different region-file bytes for every block with more than one
|
|
// property, which makes a byte-level diff of two saves useless.
|
|
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 {
|
|
keys := make([]string, 0, len(s.Properties))
|
|
for k := range s.Properties {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
props := nbt.NewCompound()
|
|
for _, k := range keys {
|
|
props.Set(k, nbt.String(s.Properties[k]))
|
|
}
|
|
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 resolves a block name plus any properties to a state ID,
|
|
// mirroring how vanilla reads a palette entry: start from the block's default
|
|
// state and apply the properties it recognises, keeping the default's value for
|
|
// anything it does not.
|
|
//
|
|
// It used to return the block's *first* state — blocks.json lists states in
|
|
// StateDefinition.getPossibleStates() order, the property cartesian product,
|
|
// which has nothing to do with the default. For 642 of 1168 blocks those
|
|
// differ, so every caller passing nil got a corner state: redstone ore came out
|
|
// permanently lit, a sunflower came out as its own top half, oak stairs came out
|
|
// upside down and waterlogged. blocks.json marks the default state and the
|
|
// parser was dropping the flag.
|
|
//
|
|
// ok is false only for a name that is not a block at all.
|
|
func nameToStateID(name string, props map[string]string) (uint16, bool) {
|
|
stateByIDOnce.Do(buildStateTable)
|
|
defaultID, ok := defaultByName[name]
|
|
if !ok {
|
|
return StateAir, false
|
|
}
|
|
if len(props) == 0 {
|
|
return defaultID, true
|
|
}
|
|
// Overlay only keys the block actually has; an unknown key or an illegal
|
|
// value leaves the default's value in place, which is what
|
|
// StateHolder.setValue's helper does after logging.
|
|
base := stateByIDImpl[defaultID].Properties
|
|
merged := make(map[string]string, len(base))
|
|
for k, v := range base {
|
|
if override, present := props[k]; present {
|
|
merged[k] = override
|
|
continue
|
|
}
|
|
merged[k] = v
|
|
}
|
|
for _, id := range idsByName[name] {
|
|
if propsMatch(stateByIDImpl[id].Properties, merged) {
|
|
return id, true
|
|
}
|
|
}
|
|
return defaultID, true
|
|
}
|
|
|
|
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
|
|
}
|