// 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/"(图集中不存在)→ 全图集灰块。 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 }