fix(project): a module resref names a file, not a resource (#93)
`module.resref` is the name of the built `.mod` on disk, so the 16-byte resref limit never applied to it — NWN:EE module file names are routinely longer. The blanket check rejected `ShadowsOverWestgate` (19 characters) and blocked sow-module#60: ``` crucible module build module.resref "ShadowsOverWestgate" exceeds 16 characters ``` ## What changed `internal/project/project.go` — `module.resref` is validated as a **file name** now, which still rejects a resref that is a path or empty. The 16-character limit is kept where the value really does become a resref: a project with `paths.assets` and no `haks[]` names its single generated HAK after the module resref (`build.go:1144`), and a HAK name is a resref the engine loads. That case now says what to do about it instead of refusing every long module name. ## Verified - 3 new tests in `internal/project`: a long module name validates and produces `ShadowsOverWestgate.mod`; a resref containing a path is rejected; a long resref that would name a generated HAK is still rejected. - `make check` green. - `crucible module build` in sow-module writes `module/ShadowsOverWestgate.mod` (70 resources). - `crucible topdata validate` in sow-topdata still passes — its assets live under `topdata.assets`, not `paths.assets`, so the HAK guard does not bite. Merge this **first**: sow-module's rename PR cannot go green in CI until this lands and its `flake.lock` is bumped. Refs ShadowsOverWestgate/sow-module#60 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #93 Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #93.
This commit is contained in:
@@ -560,8 +560,18 @@ func (p *Project) ValidateLayout() error {
|
|||||||
if strings.TrimSpace(p.Config.Module.ResRef) == "" {
|
if strings.TrimSpace(p.Config.Module.ResRef) == "" {
|
||||||
failures = append(failures, errors.New("module.resref is required"))
|
failures = append(failures, errors.New("module.resref is required"))
|
||||||
}
|
}
|
||||||
if len(p.Config.Module.ResRef) > 16 {
|
// module.resref names the built .mod FILE, so the 16-byte resref limit does not
|
||||||
failures = append(failures, fmt.Errorf("module.resref %q exceeds 16 characters", p.Config.Module.ResRef))
|
// apply to it — NWN:EE module file names are routinely longer. It is validated as
|
||||||
|
// a file name instead. The limit still binds when the same value has to be a real
|
||||||
|
// resref: with no haks configured, an asset project names its single generated HAK
|
||||||
|
// after it, and a HAK name is a resref the engine loads.
|
||||||
|
if err := validateOutputFileName("module.resref", p.Config.Module.ResRef+".mod", ".mod"); err != nil {
|
||||||
|
failures = append(failures, err)
|
||||||
|
}
|
||||||
|
if len(p.Config.Module.ResRef) > 16 && strings.TrimSpace(p.Config.Paths.Assets) != "" && len(p.Config.HAKs) == 0 {
|
||||||
|
failures = append(failures, fmt.Errorf(
|
||||||
|
"module.resref %q exceeds 16 characters and would name this project's generated HAK; configure haks[] with a shorter name",
|
||||||
|
p.Config.Module.ResRef))
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(p.Config.Paths.Source) == "" && strings.TrimSpace(p.Config.Paths.Assets) == "" && !p.HasTopData() {
|
if strings.TrimSpace(p.Config.Paths.Source) == "" && strings.TrimSpace(p.Config.Paths.Assets) == "" && !p.HasTopData() {
|
||||||
failures = append(failures, errors.New("at least one of paths.source, paths.assets, or topdata.source is required"))
|
failures = append(failures, errors.New("at least one of paths.source, paths.assets, or topdata.source is required"))
|
||||||
|
|||||||
@@ -1093,6 +1093,78 @@ func TestValidateLayoutAllowsMissingAssetsDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// module.resref names the built .mod FILE, not a resource inside an archive, so the
|
||||||
|
// 16-byte resref limit does not apply to it. NWN:EE module file names are commonly
|
||||||
|
// longer (ShadowsOverWestgate.mod is 19). The limit still binds everywhere a resref
|
||||||
|
// really is a resref — see TestValidateLayoutRejectsLongResRefWhenItNamesAHAK.
|
||||||
|
func TestValidateLayoutAllowsLongModuleResRef(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
mkdirAll(t, filepath.Join(root, "src"))
|
||||||
|
mkdirAll(t, filepath.Join(root, "build"))
|
||||||
|
|
||||||
|
proj := &Project{
|
||||||
|
Root: root,
|
||||||
|
Config: Config{
|
||||||
|
Module: ModuleConfig{Name: "Shadows Over Westgate", ResRef: "ShadowsOverWestgate"},
|
||||||
|
Paths: PathConfig{Source: "src", Build: "build"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proj.ValidateLayout(); err != nil {
|
||||||
|
t.Fatalf("ValidateLayout rejected a 19-character module file name: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := filepath.Base(proj.ModuleArchivePath()), "ShadowsOverWestgate.mod"; got != want {
|
||||||
|
t.Fatalf("ModuleArchivePath() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A module.resref that is not a usable file name is still rejected.
|
||||||
|
func TestValidateLayoutRejectsModuleResRefThatIsAPath(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
mkdirAll(t, filepath.Join(root, "src"))
|
||||||
|
|
||||||
|
proj := &Project{
|
||||||
|
Root: root,
|
||||||
|
Config: Config{
|
||||||
|
Module: ModuleConfig{Name: "Test", ResRef: "../escape/mod"},
|
||||||
|
Paths: PathConfig{Source: "src", Build: "build"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := proj.ValidateLayout()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ValidateLayout accepted a module.resref containing a path")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "module.resref") {
|
||||||
|
t.Fatalf("error does not name the offending field: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a project declares no haks, the module resref becomes the name of the single
|
||||||
|
// generated HAK — and a HAK name IS a resref the engine loads. The limit applies
|
||||||
|
// there, so a long name is only allowed for projects that build no HAKs.
|
||||||
|
func TestValidateLayoutRejectsLongResRefWhenItNamesAHAK(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
mkdirAll(t, filepath.Join(root, "src"))
|
||||||
|
mkdirAll(t, filepath.Join(root, "assets"))
|
||||||
|
|
||||||
|
proj := &Project{
|
||||||
|
Root: root,
|
||||||
|
Config: Config{
|
||||||
|
Module: ModuleConfig{Name: "Shadows Over Westgate", ResRef: "ShadowsOverWestgate"},
|
||||||
|
Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := proj.ValidateLayout()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ValidateLayout accepted a 19-character name for a generated HAK")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "16") {
|
||||||
|
t.Fatalf("error does not explain the resref limit: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// paths.build is an OUTPUT dir the builder creates (MkdirAll) before writing, so
|
// paths.build is an OUTPUT dir the builder creates (MkdirAll) before writing, so
|
||||||
// a bare clone with no build dir yet must still validate/build with no pre-step
|
// a bare clone with no build dir yet must still validate/build with no pre-step
|
||||||
// (R2/parity). Only a build path that exists but is not a directory is an error.
|
// (R2/parity). Only a build path that exists but is not a directory is an error.
|
||||||
|
|||||||
Reference in New Issue
Block a user