Compile referenced NWScript during module builds

This commit is contained in:
2026-04-02 22:04:58 +02:00
parent 2f3bdc73d2
commit 38ee1e21e0
2 changed files with 322 additions and 0 deletions
+182
View File
@@ -6,7 +6,9 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
@@ -222,10 +224,190 @@ func collectModuleResources(p *project.Project, moduleHakOrder []string) ([]erf.
moduleResources = append(moduleResources, resource)
}
compiledScripts, err := compileReferencedScripts(p)
if err != nil {
return nil, err
}
moduleResources = append(moduleResources, compiledScripts...)
sortResources(moduleResources)
return moduleResources, nil
}
func compileReferencedScripts(p *project.Project) ([]erf.Resource, error) {
referenced, err := referencedScriptResrefs(p)
if err != nil {
return nil, err
}
if len(referenced) == 0 {
return nil, nil
}
scriptSources := make(map[string]string, len(p.Inventory.ScriptFiles))
for _, rel := range p.Inventory.ScriptFiles {
resref := strings.ToLower(strings.TrimSuffix(filepath.Base(rel), filepath.Ext(rel)))
scriptSources[resref] = filepath.Join(p.SourceDir(), filepath.FromSlash(rel))
}
compiler, err := resolveScriptCompiler(p)
if err != nil {
return nil, err
}
tmpDir, err := os.MkdirTemp("", "sow-script-compile-*")
if err != nil {
return nil, fmt.Errorf("create script compiler temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
resources := make([]erf.Resource, 0, len(referenced))
scriptsDir := filepath.Join(p.SourceDir(), "scripts")
for _, resref := range referenced {
sourcePath, ok := scriptSources[resref]
if !ok {
continue
}
outputPath := filepath.Join(tmpDir, resref+".ncs")
cmd := exec.Command(compiler, "--dirs", scriptsDir, "-o", outputPath, sourcePath)
cmd.Env = os.Environ()
output, err := cmd.CombinedOutput()
if err != nil {
message := strings.TrimSpace(string(output))
if message == "" {
message = err.Error()
}
return nil, fmt.Errorf("compile script %s: %s", filepath.Base(sourcePath), message)
}
resource, err := rawResource(outputPath)
if err != nil {
return nil, fmt.Errorf("load compiled script %s: %w", filepath.Base(outputPath), err)
}
resources = append(resources, resource)
}
return resources, nil
}
func referencedScriptResrefs(p *project.Project) ([]string, error) {
referenced := map[string]struct{}{}
for _, rel := range p.Inventory.SourceFiles {
abs := filepath.Join(p.SourceDir(), filepath.FromSlash(rel))
document, _, _, err := validatorLoadDocument(abs)
if err != nil {
return nil, err
}
validatorWalkFields(document.Root, func(fieldLabel, value string) {
if !validatorIsScriptField(fieldLabel) {
return
}
value = strings.TrimSpace(value)
if value == "" {
return
}
referenced[strings.ToLower(value)] = struct{}{}
})
}
resrefs := make([]string, 0, len(referenced))
for resref := range referenced {
resrefs = append(resrefs, resref)
}
slices.Sort(resrefs)
return resrefs, nil
}
func resolveScriptCompiler(p *project.Project) (string, error) {
if configured := strings.TrimSpace(os.Getenv("SOW_NWN_SCRIPT_COMPILER")); configured != "" {
return configured, nil
}
candidates := []string{
filepath.Join(p.Root, "tools", "script-compiler", "nwn_script_comp"),
filepath.Join(p.Root, "tools", "script-compiler", "nwn_script_comp.exe"),
filepath.Join(p.Root, "tools", "nwn_script_comp"),
filepath.Join(p.Root, "tools", "nwn_script_comp.exe"),
}
if runtime.GOOS == "windows" {
candidates = []string{
filepath.Join(p.Root, "tools", "script-compiler", "nwn_script_comp.exe"),
filepath.Join(p.Root, "tools", "script-compiler", "nwn_script_comp"),
filepath.Join(p.Root, "tools", "nwn_script_comp.exe"),
filepath.Join(p.Root, "tools", "nwn_script_comp"),
}
}
for _, candidate := range candidates {
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
return candidate, nil
}
}
if compiler, err := exec.LookPath("nwn_script_comp"); err == nil {
return compiler, nil
}
if runtime.GOOS == "windows" {
if compiler, err := exec.LookPath("nwn_script_comp.exe"); err == nil {
return compiler, nil
}
}
return "", errors.New("script compiler not found; install nwn_script_comp into ./tools or set SOW_NWN_SCRIPT_COMPILER")
}
func validatorLoadDocument(path string) (gff.Document, string, string, error) {
raw, err := os.ReadFile(path)
if err != nil {
return gff.Document{}, "", "", fmt.Errorf("read %s: %w", path, err)
}
stem := strings.TrimSuffix(filepath.Base(path), ".json")
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(stem)), ".")
if extension == "" {
return gff.Document{}, "", "", fmt.Errorf("missing resource extension before .json: %s", path)
}
resref := strings.ToLower(strings.TrimSuffix(stem, "."+extension))
var document gff.Document
if err := json.Unmarshal(raw, &document); err != nil {
return gff.Document{}, "", "", fmt.Errorf("parse %s: %w", path, err)
}
return document, resref, extension, nil
}
func validatorWalkFields(root gff.Struct, visit func(string, string)) {
walkScriptFields(root, visit)
}
func validatorIsScriptField(label string) bool {
return strings.HasPrefix(label, "On") || strings.HasSuffix(label, "Script") || strings.Contains(label, "Script")
}
func walkScriptFields(s gff.Struct, visit func(string, string)) {
for _, field := range s.Fields {
if value, ok := fieldStringValueForScripts(field.Value); ok {
visit(field.Label, value)
}
switch typed := field.Value.(type) {
case gff.Struct:
walkScriptFields(typed, visit)
case gff.ListValue:
for _, item := range typed {
walkScriptFields(item, visit)
}
}
}
}
func fieldStringValueForScripts(value gff.Value) (string, bool) {
switch typed := value.(type) {
case gff.StringValue:
return string(typed), true
case gff.ResRefValue:
return string(typed), true
default:
return "", false
}
}
func collectAssetResources(p *project.Project) ([]assetResource, error) {
var hakResources []assetResource