From e86e839ed25e76d142cb37005a9ecbc22541f337 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Sat, 1 Aug 2026 09:56:26 +0200 Subject: [PATCH] fix(project): a module resref names a file, not a resource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. It is now validated as a file name instead, which still rejects a resref that is a path or an empty string. The 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, and a HAK name is a resref the engine loads. That case now says what to do about it rather than refusing every long module name. Co-Authored-By: Claude Opus 5 (1M context) --- internal/project/project.go | 14 ++++++- internal/project/project_test.go | 72 ++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/internal/project/project.go b/internal/project/project.go index 03954ed..ea47786 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -560,8 +560,18 @@ func (p *Project) ValidateLayout() error { if strings.TrimSpace(p.Config.Module.ResRef) == "" { failures = append(failures, errors.New("module.resref is required")) } - if len(p.Config.Module.ResRef) > 16 { - failures = append(failures, fmt.Errorf("module.resref %q exceeds 16 characters", p.Config.Module.ResRef)) + // module.resref names the built .mod FILE, so the 16-byte resref limit does not + // 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() { failures = append(failures, errors.New("at least one of paths.source, paths.assets, or topdata.source is required")) diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 8ceaa0e..0932fe1 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -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 // 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. -- 2.54.0