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

132 lines
3.7 KiB
Go
Raw Normal View History

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
}