package world import ( "fmt" "maps" "math" "math/bits" "slices" "sort" "sync" "github.com/brentp/intintmap" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/segmentio/fasthash/fnv1" ) // DefaultBlockRegistry is the default (vanilla) block registry used by Dragonfly when no custom registry is provided. // // The registry is populated during package init (block states + block implementations) and is finalized on first use // by `server.Config.New()`, `world.Config.New()`, or `mcdb.Config.Open()`. Callers that need custom blocks should // create a new registry using `NewBlockRegistry()` and pass it through config instead of mutating this value. var DefaultBlockRegistry = &BasicBlockRegistry{ blockProperties: make(map[string]map[string]any), stateRuntimeIDs: make(map[stateHash]uint32), customBlocks: make(map[string]CustomBlock), } // BlockRegistry converts between runtime IDs and block states/implementations. // // A BlockRegistry has a build/finalize lifecycle: // - During setup, blocks and block states may be registered using RegisterBlockState/RegisterBlock. // - After calling Finalize, the registry becomes immutable and is ready for use in chunk encoding/decoding and // network serialization. RegisterBlock/RegisterBlockState will panic after finalization. // // The interface is split because the chunk package cannot import world.Block. type BlockRegistry interface { chunk.BlockRegistry // BlockByRuntimeID looks up a Block by runtime ID. If the runtime ID is unknown/out of range, ok is false. BlockByRuntimeID(rid uint32) (Block, bool) // BlockByRuntimeIDOrAir looks up a Block by runtime ID. If not found, an air block is returned. BlockByRuntimeIDOrAir(rid uint32) Block // BlockRuntimeID looks up the runtime ID of a previously registered Block. BlockRuntimeID(block Block) (rid uint32) // RegisterBlock registers a Block implementation for a previously registered block state. RegisterBlock(block Block) // RegisterBlockState registers a block state that blocks may encode to. RegisterBlockState(blockState BlockState) // CustomBlocks returns custom blocks registered in this registry, keyed by identifier. CustomBlocks() map[string]CustomBlock // BlockByName looks up a Block by full identifier and properties. BlockByName(name string, properties map[string]any) (Block, bool) // Blocks returns all blocks registered in the registry, indexed by runtime ID. Blocks() []Block // Air returns the air block registered in the registry. Air() Block // Finalize finalizes the registry, building derived lookup tables required for runtime usage. Finalize is // idempotent. Finalize() // BitSize returns the number of bits used by BlockHash (depends on the number of registered blocks). BitSize() int // BlockHash returns a unique identifier of the block including the block states. The hash is internal to Dragonfly // and is used for fast map lookups; it does not need to match any in-game identifiers. BlockHash(b Block) uint64 // RuntimeIDToHash resolves a runtime ID to its network block hash. RuntimeIDToHash(runtimeID uint32) (hash uint32, ok bool) } const ( blockFlagNBT uint16 = 1 << iota blockFlagRandomTick blockFlagLiquid blockFlagLiquidDisplacing ) // blockInfo is a packed set of per-runtime-ID properties used in hot paths (lighting, ticking, fluid checks, etc.). // // Layout (uint16): // - bits 0..3: boolean flags (blockFlag*). // - bits 8..11: 4-bit light emission level (0-15). // - bits 12..15: 4-bit light filtering level (0-15). // // Values are computed during BlockRegistry.Finalize() and stored in BasicBlockRegistry.blockInfos, indexed by runtime ID. type blockInfo uint16 func (b *blockInfo) set(flag uint16) { *b |= blockInfo(flag) } func (b blockInfo) get(flag uint16) bool { return uint16(b)&flag != 0 } func (b *blockInfo) setLight(light uint8) { // Overwrite the 4-bit light emission field. *b &^= blockInfo(0xF) << 8 *b |= blockInfo(light&0xF) << 8 } func (b *blockInfo) setLightFilter(light uint8) { // Overwrite the 4-bit light filtering field. *b &^= blockInfo(0xF) << 12 *b |= blockInfo(light&0xF) << 12 } func (b blockInfo) getLight() uint8 { return uint8((b >> 8) & 0xF) } func (b blockInfo) getLightFilter() uint8 { return uint8((b >> 12) & 0xF) } // BasicBlockRegistry is the default BlockRegistry implementation used by Dragonfly. type BasicBlockRegistry struct { mu sync.Mutex finalized bool bitSize int hashes *intintmap.Map networkhashToRids map[uint32]uint32 ridsToNetworkhash []uint32 // stateRuntimeIDs holds a map for looking up the runtime ID of a block by the stateHash it produces. stateRuntimeIDs map[stateHash]uint32 blockProperties map[string]map[string]any // blocks holds a list of all registered Blocks indexed by their runtime ID. Blocks that were not explicitly // registered are of the type unknownBlock. blocks []Block // customBlocks maps a custom block's identifier to the custom block. customBlocks map[string]CustomBlock blockInfos []blockInfo airRID uint32 } func (br *BasicBlockRegistry) BitSize() int { if !br.finalized { panic("BlockRegistry.BitSize called on non finalized BlockRegistry") } return br.bitSize } func (br *BasicBlockRegistry) BlockCount() int { if !br.finalized { panic("BlockRegistry.BlockCount called on non finalized BlockRegistry") } return len(br.blockInfos) } func (br *BasicBlockRegistry) RandomTickBlock(rid uint32) bool { if !br.finalized { panic("BlockRegistry.RandomTickBlock called on non finalized BlockRegistry") } return br.blockInfos[rid].get(blockFlagRandomTick) } func (br *BasicBlockRegistry) FilteringBlock(rid uint32) uint8 { if !br.finalized { panic("BlockRegistry.FilteringBlock called on non finalized BlockRegistry") } return br.blockInfos[rid].getLightFilter() } func (br *BasicBlockRegistry) LightBlock(rid uint32) uint8 { if !br.finalized { panic("BlockRegistry.LightBlock called on non finalized BlockRegistry") } return br.blockInfos[rid].getLight() } func (br *BasicBlockRegistry) NBTBlock(rid uint32) bool { if !br.finalized { panic("BlockRegistry.NBTBlock called on non finalized BlockRegistry") } return br.blockInfos[rid].get(blockFlagNBT) } func (br *BasicBlockRegistry) LiquidDisplacingBlock(rid uint32) bool { if !br.finalized { panic("BlockRegistry.LiquidDisplacingBlock called on non finalized BlockRegistry") } return br.blockInfos[rid].get(blockFlagLiquidDisplacing) } func (br *BasicBlockRegistry) LiquidBlock(rid uint32) bool { if !br.finalized { panic("BlockRegistry.LiquidBlock called on non finalized BlockRegistry") } return br.blockInfos[rid].get(blockFlagLiquid) } func (br *BasicBlockRegistry) Blocks() []Block { if !br.finalized { panic("BlockRegistry.Blocks called on non finalized BlockRegistry") } return slices.Clone(br.blocks) } func (br *BasicBlockRegistry) HashToRuntimeID(hash uint32) (rid uint32, ok bool) { if !br.finalized { panic("BlockRegistry.HashToRuntimeID called on non finalized BlockRegistry") } rid, ok = br.networkhashToRids[hash] return } func (br *BasicBlockRegistry) RuntimeIDToHash(runtimeID uint32) (hash uint32, ok bool) { if !br.finalized { panic("BlockRegistry.RuntimeIDToHash called on non finalized BlockRegistry") } if runtimeID >= uint32(len(br.ridsToNetworkhash)) { return 0, false } return br.ridsToNetworkhash[runtimeID], true } // Clone returns an independent copy of the registry. If the source registry is finalized, the clone is also finalized. // If the source is not finalized, the clone remains mutable. func (br *BasicBlockRegistry) Clone() *BasicBlockRegistry { br.mu.Lock() defer br.mu.Unlock() br2 := &BasicBlockRegistry{ blockProperties: make(map[string]map[string]any, len(br.blockProperties)), stateRuntimeIDs: make(map[stateHash]uint32, len(br.stateRuntimeIDs)), customBlocks: make(map[string]CustomBlock, len(br.customBlocks)), finalized: br.finalized, bitSize: br.bitSize, airRID: br.airRID, } for k, v := range br.blockProperties { br2.blockProperties[k] = maps.Clone(v) } maps.Copy(br2.stateRuntimeIDs, br.stateRuntimeIDs) maps.Copy(br2.customBlocks, br.customBlocks) br2.blocks = make([]Block, len(br.blocks)) copy(br2.blocks, br.blocks) br2.blockInfos = append([]blockInfo(nil), br.blockInfos...) if br.finalized { br2.hashes = intintmap.New(len(br.blocks), 0.999) for rid, b := range br2.blocks { if _, hash := b.Hash(); hash == math.MaxUint64 { continue } br2.hashes.Put(int64(br2.BlockHash(b)), int64(rid)) } br2.networkhashToRids = make(map[uint32]uint32, len(br.networkhashToRids)) maps.Copy(br2.networkhashToRids, br.networkhashToRids) br2.ridsToNetworkhash = append([]uint32(nil), br.ridsToNetworkhash...) } return br2 } // NewBlockRegistry returns a mutable registry seeded with all vanilla block states and block implementations. // Callers may RegisterBlockState/RegisterBlock and must call Finalize() before using the registry for world/chunk // serialization. The returned registry is independent from DefaultBlockRegistry. func NewBlockRegistry() BlockRegistry { // Clone() produces a fully populated registry. We then mark it mutable again so that additional block states and // blocks can be registered before finalizing. br := DefaultBlockRegistry.Clone() br.finalized = false br.bitSize = 0 br.hashes = nil br.networkhashToRids = nil br.ridsToNetworkhash = nil br.blockInfos = nil return br } // RegisterBlock registers the Block passed. The EncodeBlock method will be used to encode and decode the // block passed. RegisterBlock panics if the block properties returned were not valid, existing properties. func (br *BasicBlockRegistry) RegisterBlock(b Block) { br.mu.Lock() defer br.mu.Unlock() if br.finalized { panic("BlockRegistry.RegisterBlock called on finalized BlockRegistry") } name, properties := b.EncodeBlock() if _, ok := b.(CustomBlock); ok { br.registerBlockStateLocked(BlockState{Name: name, Properties: properties}) } rid, ok := br.stateRuntimeIDs[stateHash{name: name, properties: hashProperties(properties)}] if !ok { // We assume all blocks must have all their states registered beforehand. Vanilla blocks will have // this done through registering of all states present in the block_states.nbt file. panic(fmt.Sprintf("block state returned is not registered (%v {%#v})", name, properties)) } if _, ok := br.blocks[rid].(unknownBlock); !ok { panic(fmt.Sprintf("block with name and properties %v {%#v} already registered", name, properties)) } br.blocks[rid] = b if c, ok := b.(CustomBlock); ok { if _, ok := br.customBlocks[name]; !ok { br.customBlocks[name] = c } } } // RegisterBlockState registers a BlockState to the registry. The function panics if the properties the // BlockState holds are invalid or if the BlockState was already registered. func (br *BasicBlockRegistry) RegisterBlockState(s BlockState) { br.mu.Lock() defer br.mu.Unlock() br.registerBlockStateLocked(s) } // registerBlockStateLocked is the implementation of RegisterBlockState. br.mu must be held by the caller. func (br *BasicBlockRegistry) registerBlockStateLocked(s BlockState) { if br.finalized { panic("BlockRegistry.RegisterBlockState called on finalized BlockRegistry") } h := stateHash{name: s.Name, properties: hashProperties(s.Properties)} if _, ok := br.stateRuntimeIDs[h]; ok { panic(fmt.Sprintf("cannot register the same state twice (%+v)", s)) } if _, ok := br.blockProperties[s.Name]; !ok { br.blockProperties[s.Name] = s.Properties } rid := uint32(len(br.blocks)) br.blocks = append(br.blocks, unknownBlock{BlockState: s}) br.stateRuntimeIDs[h] = rid } func (br *BasicBlockRegistry) Finalize() { br.mu.Lock() defer br.mu.Unlock() if br.finalized { // Finalize is intentionally idempotent so code can defensively call it when a registry is provided through // config. RegisterBlock/RegisterBlockState still panic after finalization. return } br.bitSize = bits.Len64(uint64(len(br.blocks))) sort.SliceStable(br.blocks, func(i, j int) bool { var nameOne string if b1, ok := br.blocks[i].(unknownBlock); ok { nameOne = b1.Name } else { nameOne, _ = br.blocks[i].EncodeBlock() } var nameTwo string if b2, ok := br.blocks[j].(unknownBlock); ok { nameTwo = b2.Name } else { nameTwo, _ = br.blocks[j].EncodeBlock() } return fnv1.HashString64(nameOne) < fnv1.HashString64(nameTwo) }) br.blockInfos = make([]blockInfo, len(br.blocks)) br.hashes = intintmap.New(len(br.blocks), 0.999) br.networkhashToRids = make(map[uint32]uint32, len(br.blocks)) br.ridsToNetworkhash = make([]uint32, len(br.blocks)) br.stateRuntimeIDs = make(map[stateHash]uint32, len(br.blocks)) networkHashScratch := make([]byte, 0, 0xff) foundAir := false for idx, b := range br.blocks { rid := uint32(idx) name, properties := b.EncodeBlock() h := stateHash{name: name, properties: hashProperties(properties)} if name == "minecraft:air" { br.airRID = rid foundAir = true } if _, ok := br.stateRuntimeIDs[h]; ok { panic(fmt.Sprintf("cannot register the same state twice (%s %+v)", name, properties)) } br.stateRuntimeIDs[h] = rid var info blockInfo // Default to fully opaque. Blocks that implement lightDiffuser may override this (e.g., air -> 0, leaves -> 1-14). info.setLightFilter(15) if diffuser, ok := b.(lightDiffuser); ok { info.setLightFilter(diffuser.LightDiffusionLevel()) } if emitter, ok := b.(lightEmitter); ok { info.setLight(emitter.LightEmissionLevel()) } if _, ok := b.(NBTer); ok { info.set(blockFlagNBT) } if _, ok := b.(RandomTicker); ok { info.set(blockFlagRandomTick) } if _, ok := b.(Liquid); ok { info.set(blockFlagLiquid) } if _, ok := b.(LiquidDisplacer); ok { info.set(blockFlagLiquidDisplacing) } br.blockInfos[rid] = info if _, hash := b.Hash(); hash != math.MaxUint64 { h := int64(br.BlockHash(b)) if other, ok := br.hashes.Get(h); ok { panic(fmt.Sprintf("block %#v with hash %v already registered by %#v", b, h, br.blocks[other])) } br.hashes.Put(h, int64(rid)) } var netHash uint32 netHash, networkHashScratch = networkBlockHash(name, properties, networkHashScratch) if other, ok := br.networkhashToRids[netHash]; ok { otherName, otherProperties := br.blocks[other].EncodeBlock() panic(fmt.Sprintf("network block hash collision for (%s %+v) and (%s %+v)", name, properties, otherName, otherProperties)) } br.networkhashToRids[netHash] = rid br.ridsToNetworkhash[rid] = netHash } if !foundAir { panic("BlockRegistry.Finalize: no minecraft:air block state registered") } br.finalized = true } // AirRuntimeID returns the runtime ID of the air block. func (br *BasicBlockRegistry) AirRuntimeID() uint32 { if !br.finalized { panic("BlockRegistry.AirRuntimeID called on non finalized BlockRegistry") } return br.airRID } // RuntimeIDToState returns the name and state properties of a block by its runtime ID. func (br *BasicBlockRegistry) RuntimeIDToState(runtimeID uint32) (name string, properties map[string]any, found bool) { if !br.finalized { panic("BlockRegistry.RuntimeIDToState called on non finalized BlockRegistry") } if runtimeID >= uint32(len(br.blocks)) { return "", nil, false } name, properties = br.blocks[runtimeID].EncodeBlock() return name, properties, true } // StateToRuntimeID returns the runtime ID of a block by its name and state properties. func (br *BasicBlockRegistry) StateToRuntimeID(name string, properties map[string]any) (runtimeID uint32, found bool) { if !br.finalized { panic("BlockRegistry.StateToRuntimeID called on non finalized BlockRegistry") } if rid, ok := br.stateRuntimeIDs[stateHash{name: name, properties: hashProperties(properties)}]; ok { return rid, true } rid, ok := br.stateRuntimeIDs[stateHash{name: name, properties: hashProperties(br.blockProperties[name])}] return rid, ok } // BlockHash returns a unique identifier of the block including the block states. This function is used internally // to convert a block to a single integer which can be used in map lookups. The hash produced therefore does not // need to match anything in the game, but it must be unique among all registered blocks. // The tool in `/cmd/blockhash` may be used to automatically generate block hashes of blocks in a package. func (br *BasicBlockRegistry) BlockHash(b Block) uint64 { base, hash := b.Hash() return base | (hash << uint64(br.bitSize)) } // BlockRuntimeID attempts to return a runtime ID of a block previously registered using RegisterBlock(). // If the runtime ID cannot be found because the Block wasn't registered, BlockRuntimeID will panic. func (br *BasicBlockRegistry) BlockRuntimeID(b Block) uint32 { if !br.finalized { panic("BlockRegistry.BlockRuntimeID called on non finalized BlockRegistry") } if b == nil { return br.airRID } if _, hash := b.Hash(); hash != math.MaxUint64 { // b is not an unknownBlock. if rid, ok := br.hashes.Get(int64(br.BlockHash(b))); ok { return uint32(rid) } panic(fmt.Sprintf("cannot find block by non-0 hash of block %#v", b)) } return br.slowBlockRuntimeID(b) } func (br *BasicBlockRegistry) BlockByRuntimeIDOrAir(rid uint32) Block { bl, _ := br.BlockByRuntimeID(rid) return bl } // slowBlockRuntimeID finds the runtime ID of a Block by hashing the properties produced by calling the // Block.EncodeBlock method and looking it up in the stateRuntimeIDs map. func (br *BasicBlockRegistry) slowBlockRuntimeID(b Block) uint32 { name, properties := b.EncodeBlock() rid, ok := br.stateRuntimeIDs[stateHash{name: name, properties: hashProperties(properties)}] if !ok { panic(fmt.Sprintf("cannot find block by (name + properties): %#v", b)) } return rid } // BlockByRuntimeID attempts to return a Block by its runtime ID. If not found, the bool returned is // false. If found, the block is non-nil and the bool true. func (br *BasicBlockRegistry) BlockByRuntimeID(rid uint32) (Block, bool) { if !br.finalized { panic("BlockRegistry.BlockByRuntimeID called on non finalized BlockRegistry") } if rid >= uint32(len(br.blocks)) { return br.Air(), false } return br.blocks[rid], true } // BlockByName attempts to return a Block by its name and properties. If not found, the bool returned is // false. func (br *BasicBlockRegistry) BlockByName(name string, properties map[string]any) (Block, bool) { if !br.finalized { panic("BlockRegistry.BlockByName called on non finalized BlockRegistry") } rid, ok := br.stateRuntimeIDs[stateHash{name: name, properties: hashProperties(properties)}] if !ok { return nil, false } return br.blocks[rid], true } // CustomBlocks returns a map of all custom blocks registered with their names as keys. func (br *BasicBlockRegistry) CustomBlocks() map[string]CustomBlock { return maps.Clone(br.customBlocks) } // Air returns an air block. func (br *BasicBlockRegistry) Air() Block { if !br.finalized { panic("BlockRegistry.Air called on non finalized BlockRegistry") } if br.airRID >= uint32(len(br.blocks)) { // This should never happen for a valid registry (Finalize enforces air exists). panic("BlockRegistry.Air: air runtime ID out of range") } return br.blocks[br.airRID] }