191 lines
5.5 KiB
Go
191 lines
5.5 KiB
Go
package assets
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/assets/mdl"
|
|
)
|
|
|
|
// runCompile compiles ASCII .mdl models to binary in place, one at a time (the
|
|
// engine's development/ and modelcompiler/ folders are flat, single-slot).
|
|
func runCompile(args []string, stdout, stderr io.Writer, getenv func(string) string) int {
|
|
fs := flag.NewFlagSet("compile", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
nwn := fs.String("nwn", "", "path to the NWN install root or nwmain-linux binary")
|
|
nonRecursive := fs.Bool("non-recursive", false, "do not descend into subdirectories")
|
|
if err := fs.Parse(args); err != nil {
|
|
return exitUsage
|
|
}
|
|
dirs := fs.Args()
|
|
if len(dirs) == 0 {
|
|
fmt.Fprintln(stderr, "assets compile: usage: compile [--nwn INSTALL] <dir>...")
|
|
return exitUsage
|
|
}
|
|
|
|
home := getenv("HOME")
|
|
userData := filepath.Join(home, ".local", "share", "Neverwinter Nights")
|
|
dev := filepath.Join(userData, "development")
|
|
mc := filepath.Join(userData, "modelcompiler")
|
|
|
|
nwmain := findNWMain(*nwn, home)
|
|
if nwmain == "" {
|
|
fmt.Fprintln(stderr, "assets compile: nwmain-linux not found — pass --nwn <install> "+
|
|
"(Steam/GOG/Beamdog install root or the nwmain-linux binary)")
|
|
return exitTool
|
|
}
|
|
binDir := filepath.Dir(nwmain)
|
|
|
|
// Headless wrap: no DISPLAY + xvfb-run present -> run under a virtual X.
|
|
var wrap []string
|
|
if getenv("DISPLAY") == "" {
|
|
if xvfb := look("xvfb-run"); xvfb != "" {
|
|
wrap = []string{xvfb, "-a", "--server-args=-screen 0 1024x768x24"}
|
|
}
|
|
}
|
|
|
|
files, err := walk(dirs, mdlExt, !*nonRecursive)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, "assets compile:", err)
|
|
return exitUsage
|
|
}
|
|
|
|
failed := false
|
|
for _, src := range files {
|
|
if err := compileOne(src, binDir, nwmain, dev, mc, wrap, userData, stdout, stderr); err != nil {
|
|
fmt.Fprintf(stderr, "assets compile: %s: %v\n", src, err)
|
|
failed = true
|
|
}
|
|
}
|
|
if failed {
|
|
return exitFail
|
|
}
|
|
return exitOK
|
|
}
|
|
|
|
// findNWMain resolves the nwmain-linux binary from --nwn or standard roots.
|
|
func findNWMain(override, home string) string {
|
|
if override != "" {
|
|
if fi, err := os.Stat(override); err == nil && !fi.IsDir() {
|
|
return override
|
|
}
|
|
cand := filepath.Join(override, "bin", "linux-x86", "nwmain-linux")
|
|
if fi, err := os.Stat(cand); err == nil && !fi.IsDir() {
|
|
return cand
|
|
}
|
|
return ""
|
|
}
|
|
return look(
|
|
filepath.Join(home, ".local/share/Steam/steamapps/common/Neverwinter Nights/bin/linux-x86/nwmain-linux"),
|
|
filepath.Join(home, "GOG Games/Neverwinter Nights Enhanced Edition/game/bin/linux-x86/nwmain-linux"),
|
|
filepath.Join(home, ".steam/steam/steamapps/common/Neverwinter Nights/bin/linux-x86/nwmain-linux"),
|
|
)
|
|
}
|
|
|
|
// compileOne compiles a single model. Binary models are reported and skipped
|
|
// (not an error). A name-mismatched model is aborted with guidance.
|
|
func compileOne(src, binDir, nwmain, dev, mc string, wrap []string, userData string, stdout, stderr io.Writer) error {
|
|
ascii, err := mdl.IsASCII(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ascii {
|
|
fmt.Fprintf(stdout, "skip (already compiled): %s\n", src)
|
|
return nil
|
|
}
|
|
mismatches, err := mdl.CheckNames(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(mismatches) > 0 {
|
|
return fmt.Errorf("model names differ from the file stem; run `crucible assets fix-mdl` first")
|
|
}
|
|
|
|
stem := mdl.ExpectedNameStem(src)
|
|
name := strings.ToLower(filepath.Base(src))
|
|
devFile := filepath.Join(dev, name)
|
|
// Collision guard: a pre-existing slot aborts before any mutation.
|
|
if _, err := os.Stat(devFile); err == nil {
|
|
return fmt.Errorf("development slot already occupied: %s", devFile)
|
|
}
|
|
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(devFile, data, 0o644); err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(devFile)
|
|
|
|
// Run the engine with cwd = the binary dir.
|
|
cmd := append(append([]string{}, wrap...), nwmain, "compilemodel", stem)
|
|
if out, err := runner(binDir, nil, cmd[0], cmd[1:]...); err != nil {
|
|
printEngineLog(userData, stderr)
|
|
return fmt.Errorf("engine: %v: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
|
|
// Collect the compiled artifact from modelcompiler/ (match by stem, any case).
|
|
compiled, err := findCompiled(mc, stem)
|
|
if err != nil {
|
|
printEngineLog(userData, stderr)
|
|
return err
|
|
}
|
|
defer os.Remove(compiled)
|
|
|
|
out, err := os.ReadFile(compiled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Move it back over the source path, lowercased.
|
|
dst := filepath.Join(filepath.Dir(src), name)
|
|
if err := os.WriteFile(dst, out, 0o644); err != nil {
|
|
return err
|
|
}
|
|
if dst != src {
|
|
_ = os.Remove(src)
|
|
}
|
|
fmt.Fprintf(stdout, "compiled: %s\n", dst)
|
|
return nil
|
|
}
|
|
|
|
// findCompiled returns the modelcompiler/ artifact whose stem matches (case-
|
|
// insensitively).
|
|
func findCompiled(mc, stem string) (string, error) {
|
|
entries, err := os.ReadDir(mc)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
want := strings.ToLower(stem) + ".mdl"
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
if strings.ToLower(e.Name()) == want {
|
|
return filepath.Join(mc, e.Name()), nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("engine produced no compiled model for %q", stem)
|
|
}
|
|
|
|
// printEngineLog appends a short tail of the engine log as advisory diagnostics.
|
|
func printEngineLog(userData string, stderr io.Writer) {
|
|
log := filepath.Join(userData, "logs", "nwengineLog.txt")
|
|
data, err := os.ReadFile(log)
|
|
if err != nil {
|
|
return
|
|
}
|
|
lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
|
|
if len(lines) > 8 {
|
|
lines = lines[len(lines)-8:]
|
|
}
|
|
fmt.Fprintf(stderr, " engine log tail (%s):\n", log)
|
|
for _, l := range lines {
|
|
fmt.Fprintf(stderr, " %s\n", l)
|
|
}
|
|
}
|