Compile referenced NWScript during module builds
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user