package assets import ( "encoding/json" "fmt" "os" "sort" "strings" ) // BlockModel 原版方块模型(子集:parent / textures / elements,设计.md §2.2)。 type BlockModel struct { Parent string `json:"parent"` Textures map[string]string `json:"textures"` Elements []Element `json:"elements"` AmbientOcclusion *bool `json:"ambientocclusion"` } // Element 模型元素(立方体)。 type Element struct { From [3]float32 `json:"from"` To [3]float32 `json:"to"` Faces map[string]Face `json:"faces"` } // Face 元素面。 type Face struct { UV [4]int `json:"uv"` Texture string `json:"texture"` // "#变量" 或 "minecraft:block/x" CullFace string `json:"cullface"` TintIndex int `json:"tintindex"` } // BlockState 原版方块状态(blockstates/.json)。 type BlockState struct { Variants map[string]any `json:"variants"` // string | []Variant Multipart []any `json:"multipart"` // 后置支持(设计.md §2.2) } // Variant 一个状态变体。 type Variant struct { Model string `json:"model"` X int `json:"x"` Y int `json:"y"` UVLock bool `json:"uvlock"` Weight int `json:"weight"` } // LoadModel 解析模型并展开 parent 继承链(含循环检测)。 // 返回的模型:纹理变量已合并子代覆盖、元素取自继承链末端(子代无元素时)。 func (m *Manager) LoadModel(key string) (*BlockModel, error) { return m.loadModel(key, map[string]bool{}) } func (m *Manager) loadModel(key string, stack map[string]bool) (*BlockModel, error) { key = strings.TrimPrefix(key, "minecraft:") path, ok := m.find("models", key, ".json") if !ok { return nil, fmt.Errorf("assets.loadModel: 模型 %q 不存在", key) } if stack[key] { return nil, fmt.Errorf("assets.loadModel: 模型继承循环 %q", key) } stack[key] = true defer delete(stack, key) b, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("assets.loadModel %s: %w", key, err) } var mod BlockModel if err := json.Unmarshal(b, &mod); err != nil { return nil, fmt.Errorf("assets.loadModel %s: %w", key, err) } // 展开 parent 链:纹理变量子代覆盖父代,元素/环境光遮蔽继承 if mod.Parent != "" { parent, err := m.loadModel(mod.Parent, stack) if err != nil { return nil, err } merged := make(map[string]string, len(parent.Textures)+len(mod.Textures)) for k, v := range parent.Textures { merged[k] = v } for k, v := range mod.Textures { merged[k] = v } mod.Textures = merged if len(mod.Elements) == 0 { mod.Elements = parent.Elements } if mod.AmbientOcclusion == nil { mod.AmbientOcclusion = parent.AmbientOcclusion } } // 解析纹理变量引用("#all" → 变量表取值) for k, v := range mod.Textures { if strings.HasPrefix(v, "#") { mod.Textures[k] = mod.Textures[strings.TrimPrefix(v, "#")] } } return &mod, nil } // ResolvedTexture 返回模型纹理变量的最终值(去命名空间前缀)。 func (mod *BlockModel) ResolvedTexture(name string) string { v, ok := mod.Textures[name] if !ok { v = name } return strings.TrimPrefix(v, "minecraft:") } // LoadBlockState 解析方块状态文件。 func (m *Manager) LoadBlockState(id string) (*BlockState, error) { path, ok := m.find("blockstates", id, ".json") if !ok { return nil, fmt.Errorf("assets.LoadBlockState: 方块状态 %q 不存在", id) } b, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("assets.LoadBlockState %s: %w", id, err) } var bs BlockState if err := json.Unmarshal(b, &bs); err != nil { return nil, fmt.Errorf("assets.LoadBlockState %s: %w", id, err) } return &bs, nil } // VariantsOf 返回匹配属性的变体列表:先精确匹配属性键,再回退空键(缺省变体)。 func (bs *BlockState) VariantsOf(props map[string]string) []Variant { raw, ok := bs.Variants[stateKey(props)] if !ok { raw, ok = bs.Variants[""] } if !ok { return nil } return parseVariants(raw) } // stateKey 把属性映射拼成原版变体键(字典序,如 "facing=north,half=lower")。 func stateKey(props map[string]string) string { if len(props) == 0 { return "" } ks := make([]string, 0, len(props)) for k := range props { ks = append(ks, k) } sort.Strings(ks) parts := make([]string, 0, len(ks)) for _, k := range ks { parts = append(parts, k+"="+props[k]) } return strings.Join(parts, ",") } // parseVariants 解析变体值:string(单模型)或对象数组。 func parseVariants(raw any) []Variant { switch v := raw.(type) { case string: return []Variant{{Model: v}} case []any: out := make([]Variant, 0, len(v)) for _, it := range v { b, err := json.Marshal(it) if err != nil { continue } var vv Variant if json.Unmarshal(b, &vv) == nil { out = append(out, vv) } } return out } return nil }