package assets import ( "encoding/json" "fmt" "io/fs" "os" "path/filepath" "sort" "strings" ) // AtlasSource 图集源定义(atlases/*.json,原版 1.20.5+ 结构,设计.md §1.3)。 type AtlasSource struct { Type string `json:"type"` // minecraft:directory / minecraft:single Prefix string `json:"prefix"` // directory 源的键前缀 Source string `json:"source"` // directory 源相对 textures/ 的目录 Resource string `json:"resource"` // single 源资源键(如 minecraft:entity/bell/bell_body) } // Atlas 图集定义。 type Atlas struct { Sources []AtlasSource `json:"sources"` } // LoadAtlas 解析 atlases/.json(如 "blocks"、"gui"、"items")。 func (m *Manager) LoadAtlas(name string) (*Atlas, error) { path, ok := m.find("atlases", name, ".json") if !ok { return nil, fmt.Errorf("assets.LoadAtlas: 图集 %q 不存在", name) } b, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("assets.LoadAtlas %s: %w", name, err) } var a Atlas if err := json.Unmarshal(b, &a); err != nil { return nil, fmt.Errorf("assets.LoadAtlas %s: %w", name, err) } return &a, nil } // ExpandAtlas 展开图集源为有序去重的纹理键列表(无扩展名、无命名空间)。 // 顺序:按包优先级从高到低贡献键,低优先级补缺(渲染.md §4 图集构建依据)。 func (m *Manager) ExpandAtlas(name string) ([]string, error) { a, err := m.LoadAtlas(name) if err != nil { return nil, err } var keys []string seen := make(map[string]struct{}) add := func(k string) { if _, ok := seen[k]; !ok { seen[k] = struct{}{} keys = append(keys, k) } } for _, src := range a.Sources { switch { case strings.HasSuffix(src.Type, ":directory"): // 从高到低遍历包,收集 textures// 下全部 PNG(递归子目录) files, err := m.listTextureDir(src.Source) if err != nil { return nil, err } for _, rel := range files { add(filepath.ToSlash(filepath.Join(src.Prefix, rel))) } case strings.HasSuffix(src.Type, ":single"): // 去掉命名空间前缀(minecraft:entity/x → entity/x) add(strings.TrimPrefix(src.Resource, "minecraft:")) } } return keys, nil } // listTextureDir 汇总全部包 textures// 下的 PNG 相对路径(含子目录)。 // 包优先级高者先列出;单个包内按路径排序,保证确定性。 func (m *Manager) listTextureDir(dir string) ([]string, error) { var rels []string seen := make(map[string]struct{}) for _, root := range m.textureRoots() { base := filepath.Join(root, dir) var names []string err := filepath.WalkDir(base, func(path string, d fs.DirEntry, err error) error { if err != nil { if os.IsNotExist(err) { return nil // 该包没有此目录:跳过 } return err } if d.IsDir() { return nil } if strings.HasSuffix(d.Name(), ".png") { rel, err := filepath.Rel(base, path) if err != nil { return err } names = append(names, filepath.ToSlash(strings.TrimSuffix(rel, ".png"))) } return nil }) if err != nil { return nil, fmt.Errorf("assets.listTextureDir %s: %w", dir, err) } sort.Strings(names) for _, n := range names { if _, ok := seen[n]; ok { continue } seen[n] = struct{}{} rels = append(rels, n) } } return rels, nil } // textureRoots 返回按优先级排序的 textures 根目录列表(含嵌入根)。 func (m *Manager) textureRoots() []string { var roots []string for _, p := range m.packs { for _, ns := range p.namespaces() { roots = append(roots, filepath.Join(p.Root, "assets", ns, "textures")) } } if m.embeddedRoot != "" { roots = append(roots, filepath.Join(m.embeddedRoot, "textures")) } return roots }