Files
ngzz-mc/internal/assets/model.go

182 lines
4.8 KiB
Go
Raw 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 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/<id>.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
}