132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
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/<name>.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/<source>/ 下全部 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/<dir>/ 下的 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
|
||
}
|