Files
ngzz-mc/internal/block/registry.go
2026-09-19 14:21:08 +08:00

320 lines
8.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package block 方块注册表:加载 blocks.json,提供方块定义与状态查询(设计.md §6.1)。
//
// 设计要点:
// - 运行时状态 = 数字 ID(uint16)+ meta(uint8),打包为 State(uint32);
// - ID 一旦发布不可变(ID 稳定性铁律,热更新.md §4.4);
// - isSolid / isOpaque / light 等判定为「单一来源」,光照、物理、渲染共用。
package block
import (
"encoding/json"
"fmt"
"os"
"strings"
)
// State 运行时方块状态:ID(低 16 位)+ meta(高 8 位)。
type State uint32
// Air 空气(ID 0)。
const Air State = 0
// NewState 打包状态。
func NewState(id uint16, meta uint8) State { return State(uint32(id) | uint32(meta)<<16) }
// ID 返回方块 ID。
func (s State) ID() uint16 { return uint16(s & 0xFFFF) }
// Meta 返回附加数据(朝向/含水/阶段等)。
func (s State) Meta() uint8 { return uint8(s >> 16) }
// Def 方块定义(blocks.json 条目,设计.md §2.3)。
type Def struct {
ID uint16 `json:"id"`
Name string `json:"-"`
Hardness float64 `json:"hardness"`
Resistance float64 `json:"resistance"`
Light uint8 `json:"light"`
Transparent bool `json:"transparent"`
Solid bool `json:"solid"`
Cutout bool `json:"cutout"`
Translucent bool `json:"translucent"`
Fluid bool `json:"fluid"`
RequiredTier int `json:"required_tier"`
SoundGroup string `json:"sound_group"`
Drops DropList `json:"drops"`
NoDrop bool `json:"no_drop"` // 破坏不掉落(流体/基岩/玻璃)
Textures map[string]string `json:"textures"`
Tint string `json:"tint"` // 植被染色表:grass/foliage(渲染.md §4.1)
}
// DropRule 一条掉落规则(挖矿与矿物.md §4)。
type DropRule struct {
Item string `json:"item"`
Min int `json:"min"`
Max int `json:"max"`
Chance float64 `json:"chance"` // 0 表示 1;否则 0–1
Tool string `json:"tool"`
RequiredTier int `json:"required_tier"`
OrElse string `json:"or_else"`
}
// DropList 支持 JSON 字符串(旧式 "cobblestone")或规则数组。
type DropList []DropRule
// UnmarshalJSON 兼容 "drops": "dirt" 与 "drops": [{...}]。
func (d *DropList) UnmarshalJSON(b []byte) error {
if len(b) == 0 || string(b) == "null" {
return nil
}
if b[0] == '"' {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
*d = DropList{{Item: s, Min: 1, Max: 1}}
return nil
}
var arr []DropRule
if err := json.Unmarshal(b, &arr); err != nil {
return err
}
*d = arr
return nil
}
// DropResult 一次掷骰结果。
type DropResult struct {
Item string
Count int
}
// RollDrops 按工具种类/等级掷掉落(创造模式由调用方跳过)。
func (d Def) RollDrops(toolType string, toolTier int, rnd func() float64) []DropResult {
if d.NoDrop {
return nil
}
if rnd == nil {
rnd = func() float64 { return 1 }
}
if len(d.Drops) == 0 {
if d.Name == "" || d.Fluid {
return nil
}
return []DropResult{{Item: d.Name, Count: 1}}
}
var out []DropResult
for _, r := range d.Drops {
if r.Tool != "" && r.Tool != toolType {
continue
}
if r.RequiredTier > 0 && toolTier < r.RequiredTier {
continue
}
ch := r.Chance
if ch <= 0 {
ch = 1
}
itemName := r.Item
if rnd() > ch {
if r.OrElse == "" {
continue
}
itemName = r.OrElse
}
if itemName == "" {
continue
}
minN, maxN := r.Min, r.Max
if minN <= 0 {
minN = 1
}
if maxN < minN {
maxN = minN
}
n := minN
if maxN > minN {
span := maxN - minN + 1
n = minN + int(rnd()*float64(span))
if n > maxN {
n = maxN
}
}
out = append(out, DropResult{Item: itemName, Count: n})
}
return out
}
// BreakSound / PlaceSound / HitSound / StepSound 按 sound_group 发事件(音频.md §6.1)。
func (d Def) soundGroup() string {
if d.SoundGroup != "" {
return d.SoundGroup
}
return "stone"
}
func (d Def) BreakSound() string { return "block." + d.soundGroup() + ".break" }
func (d Def) PlaceSound() string { return "block." + d.soundGroup() + ".place" }
func (d Def) HitSound() string { return "block." + d.soundGroup() + ".hit" }
func (d Def) StepSound() string { return "block." + d.soundGroup() + ".step" }
// Registry 方块注册表。
type Registry struct {
byID []Def
byName map[string]uint16
}
// Load 解析 blocks.json 构建注册表(按 id 索引;ID 0 保留给空气,其余不要求连续但必须有唯一)。
func Load(path string) (*Registry, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("block.Load %s: %w", path, err)
}
var doc struct {
Blocks map[string]Def `json:"blocks"`
}
if err := json.Unmarshal(b, &doc); err != nil {
return nil, fmt.Errorf("block.Load %s: %w", path, err)
}
r := &Registry{byName: make(map[string]uint16, len(doc.Blocks))}
maxID := uint16(0)
for name, d := range doc.Blocks {
if d.ID == 0 {
return nil, fmt.Errorf("block.Load: 方块 %q 使用保留 ID 0(空气)", name)
}
if d.ID > maxID {
maxID = d.ID
}
if _, dup := r.byName[name]; dup {
return nil, fmt.Errorf("block.Load: 方块重名 %q", name)
}
r.byName[name] = d.ID
}
// 按 ID 建立索引(下标 0 = 空气零值)
r.byID = make([]Def, int(maxID)+1)
for name, d := range doc.Blocks {
d.Name = name
if r.byID[d.ID].ID != 0 {
return nil, fmt.Errorf("block.Load: ID %d 被重复使用", d.ID)
}
r.byID[d.ID] = d
}
return r, nil
}
// Get 按 ID 取定义;越界返回零值(视为空气)。
func (r *Registry) Get(id uint16) Def {
if int(id) < len(r.byID) {
return r.byID[id]
}
return Def{}
}
// All 返回全部方块定义(下标即 ID,含空槽 0——小地图配色表等遍历用)。
func (r *Registry) All() []Def { return r.byID }
// ID 按名称取 ID。
func (r *Registry) ID(name string) (uint16, bool) {
id, ok := r.byName[name]
return id, ok
}
// Name 按 ID 取名称。
func (r *Registry) Name(id uint16) string { return r.Get(id).Name }
// IsSolid 实体可站立/碰撞判定(场景/物理与碰撞.md §2)。
func (r *Registry) IsSolid(s State) bool { return s != Air && r.Get(s.ID()).Solid }
// IsOpaque 完全遮光判定(场景/光照.md §7 分类表)。
func (r *Registry) IsOpaque(s State) bool {
if s == Air {
return false
}
d := r.Get(s.ID())
return !d.Transparent
}
// Light 光源亮度(0–15,场景/光照.md §4)。
func (r *Registry) Light(s State) uint8 { return r.Get(s.ID()).Light }
// IsCutout alpha test 类型(树叶/玻璃,渲染.md §6)。
func (r *Registry) IsCutout(s State) bool { return r.Get(s.ID()).Cutout }
// IsTranslucent 半透明类型(水/彩色玻璃,需要排序 pass)。
func (r *Registry) IsTranslucent(s State) bool { return r.Get(s.ID()).Translucent }
// IsFluid 液体判定(水/岩浆)。
func (r *Registry) IsFluid(s State) bool { return r.Get(s.ID()).Fluid }
// Texture 取方块某个面的纹理键(如 "up"/"side"/"all")。
// 原版 blocks.json 惯例:四个侧面统一用 "side"(南面可用 "front" 覆盖,如熔炉)。
// mesh 层传入 east/west/south/north 方位名,这里回退到 side/front,
// 否则 grass_block 等立面会落到 "block/<name>"(图集中不存在)→ 全图集灰块。
func (r *Registry) Texture(s State, face string) string {
d := r.Get(s.ID())
if t, ok := d.Textures[face]; ok {
return t
}
switch face {
case "south": // 南面优先 front(熔炉门面),其次 side
if t, ok := d.Textures["front"]; ok {
return t
}
if t, ok := d.Textures["side"]; ok {
return t
}
case "east", "west", "north":
if t, ok := d.Textures["side"]; ok {
return t
}
}
if t, ok := d.Textures["all"]; ok {
return t
}
return "block/" + d.Name
}
// TintTextureKeys 返回「染色表名 → 涉及纹理键」:图集构建后按染色表给这些
// 纹理乘色(渲染.md §4.1)。跳过被无 tint 方块共享的纹理(dirt)和已着色侧面。
func (r *Registry) TintTextureKeys() map[string][]string {
shared := make(map[string]struct{})
for _, d := range r.byID {
if d.ID == 0 || d.Tint != "" {
continue
}
for _, k := range d.Textures {
shared[k] = struct{}{}
}
}
out := make(map[string][]string)
for _, d := range r.byID {
if d.ID == 0 || d.Tint == "" {
continue
}
seen := make(map[string]struct{})
for _, k := range d.Textures {
if _, dup := seen[k]; dup {
continue
}
if !tintableKey(d.Tint, k, shared) {
continue
}
seen[k] = struct{}{}
out[d.Tint] = append(out[d.Tint], k)
}
}
return out
}
func tintableKey(tint, key string, shared map[string]struct{}) bool {
if _, ok := shared[key]; ok {
return false
}
if strings.Contains(key, "dirt") {
return false
}
if tint == "grass" && strings.Contains(key, "side") {
return false
}
return true
}