Initial sow-tools import

This commit is contained in:
2026-04-02 17:19:23 +02:00
commit b7f188779a
1370 changed files with 7357 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
package pipeline
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/gff"
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
)
type ExtractResult struct {
ModulePath string
HAKPaths []string
Written int
Skipped int
}
func Extract(p *project.Project) (ExtractResult, error) {
modulePath := filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod")
input, err := os.Open(modulePath)
if err != nil {
return ExtractResult{}, fmt.Errorf("open module archive: %w", err)
}
defer input.Close()
archive, err := erf.Read(input)
if err != nil {
return ExtractResult{}, fmt.Errorf("read module archive: %w", err)
}
var result ExtractResult
result.ModulePath = modulePath
var failures []error
written, skipped, errs := extractArchiveResources(p, archive)
result.Written += written
result.Skipped += skipped
failures = append(failures, errs...)
hakPaths, err := filepath.Glob(filepath.Join(p.BuildDir(), "*.hak"))
if err != nil {
return result, fmt.Errorf("scan hak archives: %w", err)
}
slices.Sort(hakPaths)
for _, hakPath := range hakPaths {
input, err := os.Open(hakPath)
if err != nil {
failures = append(failures, fmt.Errorf("open hak archive %s: %w", hakPath, err))
continue
}
hakArchive, err := erf.Read(input)
input.Close()
if err != nil {
failures = append(failures, fmt.Errorf("read hak archive %s: %w", hakPath, err))
continue
}
result.HAKPaths = append(result.HAKPaths, hakPath)
written, skipped, errs := extractArchiveResources(p, hakArchive)
result.Written += written
result.Skipped += skipped
failures = append(failures, errs...)
}
if len(failures) > 0 {
return result, errors.Join(failures...)
}
return result, nil
}
func extractArchiveResources(p *project.Project, archive erf.Archive) (int, int, []error) {
var failures []error
writtenCount := 0
skippedCount := 0
for _, resource := range archive.Resources {
target, data, err := extractedFile(p, resource)
if err != nil {
failures = append(failures, err)
continue
}
written, err := writeSafely(target, data)
if err != nil {
failures = append(failures, err)
continue
}
if written {
writtenCount++
} else {
skippedCount++
}
}
return writtenCount, skippedCount, failures
}
func extractedFile(p *project.Project, resource erf.Resource) (string, []byte, error) {
extension, ok := erf.ExtensionForResourceType(resource.Type)
if !ok {
return "", nil, fmt.Errorf("unsupported resource type 0x%04X for %s", resource.Type, resource.Name)
}
switch extension {
case "nss":
return filepath.Join(p.SourceDir(), "scripts", resource.Name+".nss"), resource.Data, nil
case "utc", "utd", "ute", "uti", "utm", "utp", "uts", "utt", "utw",
"are", "dlg", "fac", "gic", "git", "ifo", "itp", "jrl":
document, err := gff.Read(bytes.NewReader(resource.Data))
if err != nil {
return "", nil, fmt.Errorf("decode gff %s.%s: %w", resource.Name, extension, err)
}
formatted, err := json.MarshalIndent(document, "", " ")
if err != nil {
return "", nil, fmt.Errorf("marshal json %s.%s: %w", resource.Name, extension, err)
}
formatted = append(formatted, '\n')
return filepath.Join(p.SourceDir(), sourceSubdir(extension), resource.Name+"."+extension+".json"), formatted, nil
default:
return filepath.Join(p.AssetsDir(), extension, resource.Name+"."+extension), resource.Data, nil
}
}
func writeSafely(path string, data []byte) (bool, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return false, fmt.Errorf("create parent directory for %s: %w", path, err)
}
existing, err := os.ReadFile(path)
if err == nil {
if bytes.Equal(existing, data) {
return false, nil
}
return false, fmt.Errorf("refusing to overwrite existing file with different contents: %s", path)
}
if !errors.Is(err, os.ErrNotExist) {
return false, fmt.Errorf("check existing file %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return false, fmt.Errorf("write %s: %w", path, err)
}
return true, nil
}
func sourceSubdir(extension string) string {
switch strings.ToLower(extension) {
case "are":
return "areas"
case "dlg":
return "dialogs"
case "fac":
return "factions"
case "gic":
return "instance"
case "git":
return "instance"
case "ifo":
return "module"
case "itp":
return "palettes"
case "jrl":
return "journal"
case "utc":
return filepath.Join("blueprints", "creatures")
case "utd":
return filepath.Join("blueprints", "doors")
case "ute":
return filepath.Join("blueprints", "encounters")
case "uti":
return filepath.Join("blueprints", "items")
case "utm":
return filepath.Join("blueprints", "merchants")
case "utp":
return filepath.Join("blueprints", "placeables")
case "uts":
return filepath.Join("blueprints", "sounds")
case "utt":
return filepath.Join("blueprints", "triggers")
case "utw":
return filepath.Join("blueprints", "waypoints")
default:
return extension
}
}