182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
|
|
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
|
|||
|
|
}
|