assets builder tools (#39)
Reviewed-on: #39 Reviewed-by: xtul <mpiasecki720@protonmail.com>
This commit was merged in pull request #39.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user