23 KiB
Model Compilation Build-Time Contract
Scope
Expand the toolkit so authored ASCII Neverwinter Nights .mdl assets can be
compiled into binary .mdl build artifacts during sow-toolkit build-haks
without changing the authored source tree.
This contract covers:
toolkit/as the implementation ownersow-assets/as the primary consumer repository- any future consumer repo that packages NWN model assets through the same HAK build pipeline
This contract does not cover:
- replacing authored ASCII
.mdlfiles in source control - introducing a separate standalone shell-only build system as the primary path
- downloading or redistributing copyrighted NWN game data
- building an in-game runtime compiler workflow
Goal
Make model compilation a first-class, deterministic, configurable build stage in
the Go toolkit so generated HAKs contain compiled binary models while authored
repositories continue to keep ASCII .mdl files as source of truth.
Why This Exists
Repo-local context already shows that ASCII .mdl files are an intentional part
of authored asset workflows:
- validation reports ASCII/decompiled models as informational, not erroneous
- topdata autogen manifest generation scans authored
.mdlpaths directly - part discovery derives IDs from authored
.mdlfilenames - HAK building already has other generated-asset stages such as NWScript and music conversion
Upstream NWN documentation also supports compiling before release:
nwn.wikidocuments that ASCII models are slower because the game compiles them at load time and recommends shipping compiled modelsnwnmdlcompusage docs confirm ASCII-to-binary compilation as a standard flow- community documentation shows
nwnmdlcomp.exeexpects old NWN registry/data layout and may need a 1.69-era data root
Therefore the correct implementation is not “replace asset scanning with binary models everywhere”. The correct implementation is:
- keep authored ASCII
.mdlinpaths.assets - preserve current validation and manifest derivation behavior against source
- compile only the build payload that becomes generated HAK content
Ownership Split
toolkit/- owns config schema, validation, discovery, cache policy, build staging, and HAK integration
- owns Wine execution and NWN data preparation logic
- owns tests and user-facing CLI behavior
sow-assets/- owns opting into the feature through
nwn-tool.yaml - owns providing local tool binaries and any fake/minimal NWN data tree it wants to use
- owns wrapper scripts and CI invocation details
- owns opting into the feature through
sow-module/- is an indirect consumer only when it consumes generated HAKs
- must not need authored-source changes for this feature to work
Current Project-Needs Analysis
Existing architecture
Current HAK packaging is Go-native and centered on:
cmd/nwn-tool/main.gointernal/app/app.gointernal/pipeline/build.gointernal/project/*internal/validator/*
build-haks already does all asset discovery, planning, manifest writing, HAK
reuse, and generated-asset preparation inside the toolkit. Adding model
compilation as a disconnected shell script would duplicate the existing build
graph and create drift.
Existing relevant behavior
project.AssetExtensionsalready includes.mdlvalidator.ValidateProjectdetects text.mdland reports them viaReport.DecompiledModelsemitValidatorReportalready surfaces that information in CLI outputcollectAssetResourcespackages raw files frompaths.assetsdirectly into HAK resourcesBuildHAKsalready supports generated assets and temporary staging through the music pipeline- script compilation already establishes a precedent for:
- config-backed tool resolution
- environment discovery
- cache directories under
.cache - build-time generated resources that do not modify authored sources
Constraints the plan must preserve
- authored
.mdlfiles remain canonical inputs - autogen and topdata logic must continue to see source
.mdlpaths - validation must still be able to detect ASCII source models
- HAK duplicate detection and manifest generation must remain deterministic
- build output reuse must not falsely reuse HAKs built from stale compiled model content
- consumer repos must be able to opt out cleanly
Architecture Decision
Implement model compilation as a generated-asset overlay inside the existing HAK build pipeline, not as a separate top-level shell build.
That means:
- source scanning still indexes authored
.mdl - a model preparation stage produces compiled build artifacts in cache/staging
collectAssetResourcessubstitutes compiled.mdlbytes for eligible source models during HAK packaging- non-model assets continue to flow unchanged
- the written HAK manifest still records the authored relative asset path, not a cache path
This mirrors the existing music pipeline more than the script pipeline:
- source asset exists in
paths.assets - generated build artifact lives in cache/staging
- HAK includes the generated bytes under the original logical resource name
Non-Goals
- no automatic decompilation
- no mutation of files under
paths.assets - no requirement to compile every
.mdlin every repo by default - no requirement to support Windows-only shell wrappers as the primary interface
- no requirement to support arbitrary model compilers on day one beyond
nwnmdlcomp.exevia Wine - no attempt to “fix” malformed source models automatically
Config Contract
Add a new top-level config section:
models:
compile:
enabled: true
source_extensions: [.mdl]
include: []
exclude: []
cache: "{paths.cache}/models"
compiler:
path: ""
env:
path: SOW_NWN_MODEL_COMPILER
wine: SOW_NWN_WINE
winepath: SOW_NWN_WINEPATH
prefix: SOW_NWN_WINEPREFIX
nwn_data: [SOW_NWN_MODEL_DATA, SOW_NWN_ROOT, NWN_ROOT]
search:
- "{paths.tools}/nwnmdlcomp/nwnmdlcomp.exe"
- "{paths.tools}/nwnmdlcomp.exe"
runtime:
mode: wine
jobs: 1
force: false
setup_registry: true
compiler_cwd: "{paths.tools}/nwnmdlcomp"
data:
root: ""
require_keys: [chitin.key, nwn_base.key, data/nwn_base.key]
writable_aliases: [chitin.key, nwn_base.key]
policy:
compile_ascii_only: true
preserve_binary_sources: true
fail_on_missing_output: true
fail_on_zero_byte_output: true
fail_on_warning: false
Config design rules
models.compile.enabledmust default tofalse- the feature must be opt-in
- config must be repository-driven, not hardcoded per consumer
- search/discovery semantics should match
scripts.compilerwhere practical - path templating should reuse the existing effective config path expansion model
- include/exclude filters apply to asset-relative paths under
paths.assets - default jobs should be serial unless parallel execution is explicitly enabled
Environment precedence
Use this precedence for the compiler executable:
models.compile.compiler.path- env var named by
models.compile.compiler.env.path - configured search paths
Use this precedence for NWN data root:
models.compile.data.root- env vars listed in
models.compile.compiler.env.nwn_data - no implicit auto-discovery unless explicitly implemented and documented
Wine tools should resolve as:
- env override for
wine PATH:wine
and:
- env override for
winepath PATH:winepath
Do not silently guess a fake NWN data tree.
Build Contract
Eligibility rules
A model is eligible for build-time compilation when all of the following are true:
- asset extension is
.mdl models.compile.enabledis true- path passes include/exclude filters
- file is detected as ASCII when
compile_ascii_onlyis true
If a .mdl file is already binary and preserve_binary_sources is true:
- do not recompile it
- package it as-is
- optionally log a verbose skip reason
Integration point
Insert model preparation into planOrBuildHAKs before collectAssetResources
finalizes HAK resource content, alongside other generated-asset preparation.
Expected flow:
- validate project
- prepare music assets if enabled
- prepare compiled model assets if enabled
- collect asset resources using source inventory plus prepared overlays
- plan/write HAKs as today
Prepared asset representation
Introduce a prepared-model result similar in spirit to preparedMusicAssets,
for example:
- generated assets keyed by original asset-relative path
- skipped source-relative paths
- summary counts and loggable actions
- cleanup hook for temporary staging if needed
The HAK layer should continue to see:
- resource name from the original source path stem
- resource type
mdl - relative manifest path equal to the original source path
Only the file bytes should differ.
Cache and staging contract
Compiled models are generated artifacts and must live outside the authored asset tree.
Default cache root:
{paths.cache}/models
Suggested cache layout:
.cache/models/
compiled/<asset-relative-path>.mdl
logs/<asset-relative-path>.log
metadata.json
wine-prefix/ # only if the repo chooses a toolkit-managed prefix
nwn-data-links/ # only if alias files/symlinks are staged
The exact layout may differ, but it must support:
- deterministic mapping from source asset path to compiled output path
- per-model logging for failures
- incremental stale detection
- cleanup without touching authored content
Incremental contract
The plan must support incremental recompilation for persistent cache dirs.
At minimum, recompile when any of these change:
- source file content hash changed
- source file modtime is newer than compiled output
- compiler executable path or content fingerprint changed
- NWN data root fingerprint changed if the implementation tracks it
- relevant config changed
--forceor configforce: trueis active- compiled output is missing or zero bytes
Recommended metadata key:
- source asset relative path
- source SHA-256
- output SHA-256
- compiler path
- compiler binary mtime or hash
- NWN data root path
- build config fingerprint
- compiled timestamp
If metadata is unavailable or invalid, fail open to recompilation, not reuse.
NWN Data Contract
nwnmdlcomp.exe requires a classic NWN data/registry environment.
The implementation must:
- validate the configured NWN data root exists
- require at least one of:
chitin.keynwn_base.keydata/nwn_base.key
- never download game data
- never synthesize copyrighted content
If only data/nwn_base.key exists, the implementation may create a staged alias
view for the compiler by:
- symlinking inside a cache-owned staging dir, or
- copying just the key file names into a cache-owned staging dir when symlinks are unavailable
Do not mutate the user’s real install directory unless they explicitly pointed the config at a writable fake data tree and the implementation clearly documents that behavior.
Wine Registry Contract
When runtime.setup_registry is enabled, prepare the Wine registry keys needed
by nwnmdlcomp.exe:
HKLM\Software\WOW6432Node\BioWare\NWN\Neverwinter
Location = <wine path>
Path = <wine path>
Version = 1.69
GUID = 00000000-0000-0000-0000-000000000000
Language = 0
Implementation rules:
- convert Linux paths with
winepath -w - target the Wine prefix selected by config/env
- make registry setup idempotent
- separate “prepare registry” failure messages from “compile model” failures
The implementation may either:
- manage a dedicated toolkit prefix, or
- use a user-provided existing prefix
The contract should prefer an isolated prefix to avoid polluting unrelated Wine state.
Compiler Invocation Contract
Primary supported compiler:
nwnmdlcomp.exe
Expected invocation shape:
WINEDEBUG=-all wine "$COMPILER" -c "$INPUT_WIN" "$OUTPUT_WIN" -e
Implementation requirements:
- invoke through
os/exec, not shell string concatenation - convert input/output paths with
winepath -w - allow compiler working directory configuration if supermodel resolution or relative lookup requires it
- capture combined stdout/stderr per compile attempt
Supermodel and sibling dependency considerations
The plan must explicitly account for model compilation not being a pure single-file transform.
Potential dependencies include:
setsupermodelreferences- sibling helper models in the same asset family
- textures or material references that may not block compilation but do matter for diagnostics
Minimum contract:
- compile against a staging layout that preserves original relative structure
- ensure each model’s directory context exists in staging
- document that some supermodel cases may require staging additional authored
.mdlsiblings into the compiler-visible workspace
Do not assume “compile one file in isolation” is always sufficient.
Staging strategy
Preferred strategy:
- keep source inventory rooted in
paths.assets - materialize a compiler workspace under cache that mirrors required relative
directories for eligible
.mdl - compile outputs into a sibling compiled tree
- package compiled output bytes while preserving source-relative logical names
This avoids:
- polluting source trees
- output collisions between models with the same basename in different folders
- broken relative assumptions inside asset packs
Logging Contract
Progress output should fit existing ProgressFunc style.
Minimum messages:
Preparing model compiler...Preparing model compilation workspace...Compiling model 12/324: part/belt/pfa0_belt018.mdlReusing compiled model: vfxs/head_accessories/hfx_bandana.mdlSkipping binary model source: creatures/foo.mdlFAILED model compile: placeables/broken_model.mdl
At summary level, surface:
- models considered
- compiled
- reused
- skipped binary
- skipped filtered
- failed
Verbose/debug output may include:
- chosen compiler path
- chosen Wine/Wineprefix/Winepath tools
- chosen NWN data root
- per-model log file location
Error Handling Contract
Model compilation is a build-critical stage once enabled.
The build must fail when an eligible model compile:
- exits non-zero
- does not produce the expected output file
- produces a zero-byte output file
- cannot be staged or fingerprinted
Warnings from the compiler do not fail the build unless future config enables that policy.
Failure output must include:
- original source-relative asset path
- compiler command context sufficient for debugging
- log file path if written
At the end of a failed run, include a concise failed-path summary.
CLI Contract
The canonical user-facing entrypoint remains:
sow-toolkit build-haks
Add targeted flags only if they materially improve operability and match existing CLI patterns. Recommended flags:
build-haks --force-modelsbuild-haks --skip-modelsbuild-haks --model <glob-or-prefix>optional, only if there is a real use case for scoped rebuilds
Flag rules:
- flags override config for the current invocation only
--skip-modelsmust bypass model preparation even if config is enabled--force-modelsmust invalidate incremental reuse for model outputs only
Do not introduce a parallel standalone command as the only way to use the feature. A helper/diagnostic subcommand is acceptable later, but build-haks must own the real integration.
Validation Contract
Current validation already reports authored ASCII .mdl as informational.
That behavior should remain, but the plan should extend validation to cover model
compilation readiness when the feature is enabled.
Recommended validation additions:
- compiler path resolves or search candidates exist
wineandwinepathare resolvable on non-Windows platforms when mode iswine- NWN data root exists and has required keys
- cache path is safe and repo-relative unless explicitly documented otherwise
- include/exclude glob config is well-formed
- jobs is >= 1
Validation severity:
- config/discovery failures that make enabled compilation impossible should be errors
- authored ASCII
.mdldetection remains info - binary
.mdlsources alongside enabled compilation are not errors
Manifest And Reuse Contract
Generated haks.json must continue to describe authored asset-relative paths,
not cache file paths.
HAK reuse logic must account for compiled model output bytes. Reuse must not mistakenly treat an unchanged authored path as unchanged content if the compiled artifact changed.
That means one of:
- the existing asset
ContentIDfor.mdlmust reflect compiled output hash when compilation is enabled, or - chunk hashing must incorporate the prepared compiled output fingerprint
Without this, stale HAK reuse would be incorrect.
Parallelism Contract
Support configurable parallel compilation with conservative defaults.
Requirements:
- default jobs is
1 - jobs > 1 must not corrupt outputs or logs
- each compile must write to a unique output path and unique log path
- shared registry/prefix setup must happen before parallel work or under safe synchronization
If nwnmdlcomp.exe or Wine proves unstable under parallel execution:
- keep serial default
- document the risk
- allow opting into higher parallelism with clear caveats
Testing Contract
Implementation is not complete without automated tests. Use fake executables and temporary directories rather than requiring a real Wine/NWN environment in unit tests.
Unit tests
Add tests for:
- config loading and effective config defaults for
models.compile - compiler path/env/search resolution
- Wine tool resolution
- NWN data root validation and alias handling
- ASCII-vs-binary
.mdleligibility logic - include/exclude filtering
- incremental reuse decisions
- metadata invalidation on source/config/compiler changes
- content hashing based on compiled output, not source text alone
Pipeline tests
Add or extend internal/pipeline/pipeline_test.go coverage for:
- HAK build packages compiled
.mdlbytes instead of source ASCII bytes - non-
.mdlassets remain unchanged - binary source
.mdlpasses through unchanged - failed model compile aborts build
- missing output aborts build
- zero-byte output aborts build
--skip-modelsbypasses compilation--force-modelsrebuilds compiled outputs- unchanged compiled output allows HAK reuse
- changed compiled output forces HAK rewrite
- duplicate asset collision rules still behave as before
Validator tests
Keep and extend tests proving:
- ASCII source models are still reported as informational
- enabled compilation readiness errors surface when config is invalid
Integration/manual acceptance tests
Document real-world manual checks for a workstation with Wine and
nwnmdlcomp.exe available.
Test 1: readiness validation
sow-toolkit validate
Expected:
- no model-compiler readiness errors when config/env are correct
- ASCII
.mdlinfo line still appears for source models
Test 2: single-model HAK packaging
Given an authored ASCII model under paths.assets, run:
sow-toolkit build-haks --force-models
Expected:
- build succeeds
- resulting HAK contains a binary
.mdl - authored source file remains unchanged
Test 3: incremental reuse
Run build-haks twice without changes.
Expected:
- second run reports model reuse
- unchanged HAKs are reused when manifest/chunk fingerprints match
Test 4: source change invalidation
Edit one ASCII .mdl source and rebuild.
Expected:
- only affected compiled output is rebuilt
- affected HAK chunk is rewritten
- unrelated HAK chunks remain reusable
Test 5: missing NWN data
Misconfigure models.compile.data.root and run build.
Expected:
- validation/build fails clearly before or during preparation
- error names the missing key/data requirement
Documentation Contract
Update README.md to include:
- why compiled models are shipped even though ASCII remains authored source
- the new
models.compileconfig section - required local dependencies:
nwnmdlcomp.exewinewinepath- NWN data root
- environment override behavior
- cache/staging behavior
build-haksflags related to model compilation- limitations and troubleshooting notes
Optional but recommended:
tools/nwnmdlcomp/README.mdfor consumer repo operators
Suggested Implementation Breakdown
-
Schema and config foundation
- add
models.compileconfig structs tointernal/project/project.go - add effective config defaults and path expansion in
internal/project/effective.go - add project helper methods for compiler/cache/env resolution
- add
-
Validation foundation
- extend project/config validation for enabled compilation readiness
- extend CLI validation reporting where needed
-
Preparation layer
- add model compiler resolution, Wine resolution, NWN data staging, registry setup, and incremental metadata logic
- add a
preparedModelAssetsrepresentation
-
Pipeline integration
- plug model preparation into
planOrBuildHAKs - teach
collectAssetResourcesto substitute compiled model payloads - ensure
ContentIDand HAK reuse reflect compiled output
- plug model preparation into
-
CLI polish
- add build-haks flags if approved
- add progress messages and summary output
-
Tests
- config tests
- validator tests
- pipeline tests with fake compiler/Wine shims
-
Docs
- repo README updates
- optional operator README under
tools/
Open Design Decisions To Resolve Before Coding
These should be decided explicitly during implementation, not left implicit:
- Should the toolkit own a dedicated Wine prefix under cache by default, or require a user-provided prefix?
- Should NWN data alias files be created in a separate staging tree, or should the configured data root be used directly when already compatible?
- Is parallel model compilation stable enough to expose beyond best-effort?
- Do we want dedicated
build-haksflags now, or only config for the first implementation? - Should per-model logs always be persisted, or only on failure / debug mode?
Acceptance Criteria
The feature is complete when all of the following are true:
sow-toolkit build-hakscan package compiled binary.mdlcontent from authored ASCII.mdlsource whenmodels.compile.enabled: true- authored
.mdlfiles remain unchanged on disk - validation and topdata/autogen behaviors that depend on authored
.mdlsource still work - generated HAK manifests still reference authored asset-relative paths
- HAK reuse logic correctly tracks compiled output changes
- the feature is opt-in, deterministic, and tested
- failures are actionable and name the offending model paths
- README/config docs are updated
Relevant References
- Repo code:
internal/pipeline/build.gointernal/validator/validator.gointernal/topdata/autogen.gointernal/topdata/parts_discovery.goREADME.md
- Upstream docs consulted for this contract:
nwn.wikiMDL page: compiled models are recommended for release because ASCII loads more slowly- Neverwinter Vault
nwnmdlcomppage: standard compile/decompile CLI usage - Neverwinter Vault forum discussions:
nwnmdlcompexpects old NWN registry keys/data layout and some supermodel cases need additional care