Initial sow-tools import
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const ConfigFile = "nwn-tool.json"
|
||||
|
||||
var SourceExtensions = []string{
|
||||
".are", ".dlg", ".fac", ".git", ".ifo", ".jrl",
|
||||
".utc", ".utd", ".ute", ".uti", ".utm", ".utp", ".uts", ".utt", ".utw",
|
||||
}
|
||||
|
||||
var AssetExtensions = []string{
|
||||
".2da", ".bik", ".dds", ".dwk", ".itp", ".lod", ".lyt", ".mdl", ".mdx", ".mtr", ".plt", ".pwk", ".set", ".shd", ".tga", ".txi", ".uti", ".vis", ".wav", ".wok",
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Root string
|
||||
Config Config
|
||||
Inventory Inventory
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Module ModuleConfig `json:"module"`
|
||||
Paths PathConfig `json:"paths"`
|
||||
HAKs []HAKConfig `json:"haks"`
|
||||
}
|
||||
|
||||
type ModuleConfig struct {
|
||||
Name string `json:"name"`
|
||||
ResRef string `json:"resref"`
|
||||
Description string `json:"description"`
|
||||
HAKOrder []string `json:"hak_order"`
|
||||
}
|
||||
|
||||
type PathConfig struct {
|
||||
Source string `json:"source"`
|
||||
Assets string `json:"assets"`
|
||||
Build string `json:"build"`
|
||||
}
|
||||
|
||||
type HAKConfig struct {
|
||||
Name string `json:"name"`
|
||||
Priority int `json:"priority"`
|
||||
MaxBytes int64 `json:"max_bytes"`
|
||||
Split bool `json:"split"`
|
||||
Include []string `json:"include"`
|
||||
}
|
||||
|
||||
type Inventory struct {
|
||||
SourceFiles []string
|
||||
ScriptFiles []string
|
||||
AssetFiles []string
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
type InventoryReport struct {
|
||||
SourceFiles int
|
||||
ScriptFiles int
|
||||
AssetFiles int
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
func FindRoot(start string) (string, error) {
|
||||
current := start
|
||||
for {
|
||||
candidate := filepath.Join(current, ConfigFile)
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return current, nil
|
||||
}
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
return "", fmt.Errorf("could not find %s from %s", ConfigFile, start)
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
func Load(root string) (*Project, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(root, ConfigFile))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", ConfigFile, err)
|
||||
}
|
||||
|
||||
cfg := defaultConfig()
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse %s: %w", ConfigFile, err)
|
||||
}
|
||||
|
||||
return &Project{
|
||||
Root: root,
|
||||
Config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Project) ValidateLayout() error {
|
||||
var failures []error
|
||||
|
||||
if strings.TrimSpace(p.Config.Module.Name) == "" {
|
||||
failures = append(failures, errors.New("module.name is required"))
|
||||
}
|
||||
if strings.TrimSpace(p.Config.Module.ResRef) == "" {
|
||||
failures = append(failures, errors.New("module.resref is required"))
|
||||
}
|
||||
if len(p.Config.Module.ResRef) > 16 {
|
||||
failures = append(failures, fmt.Errorf("module.resref %q exceeds 16 characters", p.Config.Module.ResRef))
|
||||
}
|
||||
|
||||
for index, hak := range p.Config.HAKs {
|
||||
if strings.TrimSpace(hak.Name) == "" {
|
||||
failures = append(failures, fmt.Errorf("haks[%d].name is required", index))
|
||||
}
|
||||
if hak.Priority <= 0 {
|
||||
failures = append(failures, fmt.Errorf("haks[%d].priority must be greater than zero", index))
|
||||
}
|
||||
if hak.MaxBytes < 0 {
|
||||
failures = append(failures, fmt.Errorf("haks[%d].max_bytes must be zero or greater", index))
|
||||
}
|
||||
if len(hak.Include) == 0 {
|
||||
failures = append(failures, fmt.Errorf("haks[%d].include must have at least one pattern", index))
|
||||
}
|
||||
}
|
||||
|
||||
for label, path := range map[string]string{
|
||||
"paths.source": p.SourceDir(),
|
||||
"paths.assets": p.AssetsDir(),
|
||||
"paths.build": p.BuildDir(),
|
||||
} {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
failures = append(failures, fmt.Errorf("%s: %w", label, err))
|
||||
continue
|
||||
}
|
||||
if !info.IsDir() {
|
||||
failures = append(failures, fmt.Errorf("%s must be a directory: %s", label, path))
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
return errors.Join(failures...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Project) Scan() error {
|
||||
sourceFiles, sourceExts, err := scanDir(p.SourceDir(), func(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return ext == ".json" || ext == ".nss"
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("scan source tree: %w", err)
|
||||
}
|
||||
|
||||
assetFiles, _, err := scanDir(p.AssetsDir(), func(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return slices.Contains(AssetExtensions, ext)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("scan assets tree: %w", err)
|
||||
}
|
||||
|
||||
var scripts []string
|
||||
var sources []string
|
||||
extensionSet := map[string]struct{}{}
|
||||
for _, ext := range sourceExts {
|
||||
extensionSet[ext] = struct{}{}
|
||||
}
|
||||
|
||||
for _, path := range sourceFiles {
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".nss":
|
||||
scripts = append(scripts, path)
|
||||
case ".json":
|
||||
sources = append(sources, path)
|
||||
}
|
||||
}
|
||||
|
||||
extensions := make([]string, 0, len(extensionSet))
|
||||
for ext := range extensionSet {
|
||||
extensions = append(extensions, ext)
|
||||
}
|
||||
slices.Sort(extensions)
|
||||
|
||||
p.Inventory = Inventory{
|
||||
SourceFiles: sources,
|
||||
ScriptFiles: scripts,
|
||||
AssetFiles: assetFiles,
|
||||
Extensions: extensions,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Project) SourceDir() string {
|
||||
return filepath.Join(p.Root, p.Config.Paths.Source)
|
||||
}
|
||||
|
||||
func (p *Project) AssetsDir() string {
|
||||
return filepath.Join(p.Root, p.Config.Paths.Assets)
|
||||
}
|
||||
|
||||
func (p *Project) BuildDir() string {
|
||||
return filepath.Join(p.Root, p.Config.Paths.Build)
|
||||
}
|
||||
|
||||
func (i Inventory) Report() InventoryReport {
|
||||
return InventoryReport{
|
||||
SourceFiles: len(i.SourceFiles),
|
||||
ScriptFiles: len(i.ScriptFiles),
|
||||
AssetFiles: len(i.AssetFiles),
|
||||
Extensions: i.Extensions,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
Paths: PathConfig{
|
||||
Source: "src",
|
||||
Assets: "assets",
|
||||
Build: "build",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func scanDir(root string, include func(path string) bool) ([]string, []string, error) {
|
||||
var files []string
|
||||
extSet := map[string]struct{}{}
|
||||
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !include(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
files = append(files, rel)
|
||||
|
||||
if strings.HasSuffix(rel, ".json") {
|
||||
base := strings.TrimSuffix(filepath.Base(rel), ".json")
|
||||
ext := strings.ToLower(filepath.Ext(base))
|
||||
if ext != "" {
|
||||
extSet[ext] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
slices.Sort(files)
|
||||
|
||||
exts := make([]string, 0, len(extSet))
|
||||
for ext := range extSet {
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
slices.Sort(exts)
|
||||
return files, exts, nil
|
||||
}
|
||||
Reference in New Issue
Block a user