diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..41f42a8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,4 @@ +--- +description: +alwaysApply: true +--- diff --git a/internal/app/app.go b/internal/app/app.go index 7e2cd80..0e00688 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -358,6 +358,9 @@ func runExtract(ctx context) error { if len(result.HAKPaths) > 0 { fmt.Fprintf(ctx.stdout, "hak archives: %d\n", len(result.HAKPaths)) } + if len(result.DeletedArchivePaths) > 0 { + fmt.Fprintf(ctx.stdout, "deleted archives: %d\n", len(result.DeletedArchivePaths)) + } fmt.Fprintf(ctx.stdout, "written: %d\n", result.Written) fmt.Fprintf(ctx.stdout, "overwritten: %d\n", result.Overwritten) fmt.Fprintf(ctx.stdout, "removed: %d\n", result.Removed) @@ -444,7 +447,7 @@ func runApplyHAKManifest(ctx context) error { return err } - manifestPath := filepath.Join(p.BuildDir(), "haks.json") + manifestPath := p.HAKManifestPath() if len(ctx.args) > 1 { manifestPath = ctx.args[1] if !filepath.IsAbs(manifestPath) { @@ -788,7 +791,7 @@ func loadProject(cwd string) (*project.Project, error) { return nil, errors.Join(failures...) } -return p, nil + return p, nil } func printUsage(w io.Writer) { diff --git a/internal/pipeline/apply_manifest.go b/internal/pipeline/apply_manifest.go index 57be6e0..7b5dbea 100644 --- a/internal/pipeline/apply_manifest.go +++ b/internal/pipeline/apply_manifest.go @@ -18,7 +18,7 @@ type ApplyManifestResult struct { func ApplyHAKManifest(p *project.Project, manifestPath string) (ApplyManifestResult, error) { if manifestPath == "" { - manifestPath = filepath.Join(p.BuildDir(), "haks.json") + manifestPath = p.HAKManifestPath() } raw, err := os.ReadFile(manifestPath) diff --git a/internal/pipeline/build.go b/internal/pipeline/build.go index 4ad76a2..e30ecdb 100644 --- a/internal/pipeline/build.go +++ b/internal/pipeline/build.go @@ -133,7 +133,7 @@ func buildModule(p *project.Project, progress ProgressFunc) (BuildResult, error) } progressf(progress, "Writing module archive...") - outputPath := filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod") + outputPath := p.ModuleArchivePath() output, err := os.Create(outputPath) if err != nil { return BuildResult{}, fmt.Errorf("create module archive: %w", err) @@ -197,7 +197,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo } var previousManifest *BuildManifest if sourceManifest == nil || writeArchives { - previousManifest, err = loadPreviousBuildManifest(p.BuildDir()) + previousManifest, err = loadPreviousBuildManifest(p.HAKManifestPath()) if err != nil { return BuildResult{}, err } @@ -206,7 +206,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo result := BuildResult{HAKAssets: len(assetResources)} if len(assetResources) == 0 { progressf(progress, "Cleaning previous generated HAKs...") - if err := cleanupGeneratedHAKs(p.BuildDir(), p.Config.Module.ResRef); err != nil { + if err := cleanupGeneratedHAKs(p); err != nil { return BuildResult{}, err } return result, nil @@ -254,7 +254,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo manifest.HAKs = append(manifest.HAKs, manifestEntry) } - manifestPath := filepath.Join(p.BuildDir(), "haks.json") + manifestPath := p.HAKManifestPath() manifestBytes, err := json.MarshalIndent(manifest, "", " ") if err != nil { return BuildResult{}, fmt.Errorf("marshal hak manifest: %w", err) @@ -263,7 +263,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo if !writeArchives { progressf(progress, "Cleaning previous generated HAKs...") - if err := cleanupGeneratedHAKs(p.BuildDir(), p.Config.Module.ResRef); err != nil { + if err := cleanupGeneratedHAKs(p); err != nil { return BuildResult{}, err } progressf(progress, "Writing HAK manifest...") @@ -278,7 +278,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo result.HAKPaths = make([]string, 0, len(chunks)) for index, chunk := range chunks { - hakPath := filepath.Join(p.BuildDir(), chunk.Name+".hak") + hakPath := p.HAKArchivePath(chunk.Name) manifestEntry := manifest.HAKs[index] reused, err := reuseExistingHAK(previousManifest, manifestEntry, hakPath) if err != nil { @@ -305,7 +305,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo } progressf(progress, "Cleaning stale generated HAKs...") if !preserveExistingHAKs { - if err := cleanupStaleGeneratedHAKs(p.BuildDir(), p.Config.Module.ResRef, currentNames); err != nil { + if err := cleanupStaleGeneratedHAKs(p, currentNames); err != nil { return BuildResult{}, err } } @@ -1427,8 +1427,7 @@ func chunkContentHash(chunk hakChunk) string { return fmt.Sprintf("%x", sum.Sum(nil)) } -func loadPreviousBuildManifest(buildDir string) (*BuildManifest, error) { - manifestPath := filepath.Join(buildDir, "haks.json") +func loadPreviousBuildManifest(manifestPath string) (*BuildManifest, error) { raw, err := os.ReadFile(manifestPath) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -1475,8 +1474,8 @@ func reuseExistingHAK(previousManifest *BuildManifest, current BuildManifestHAK, return true, nil } -func cleanupStaleGeneratedHAKs(buildDir, moduleResRef string, keepNames map[string]struct{}) error { - paths, err := manifestHAKPaths(buildDir) +func cleanupStaleGeneratedHAKs(p *project.Project, keepNames map[string]struct{}) error { + paths, err := manifestHAKPaths(p) if err != nil { return err } @@ -1490,8 +1489,8 @@ func cleanupStaleGeneratedHAKs(buildDir, moduleResRef string, keepNames map[stri } } - legacy := filepath.Join(buildDir, moduleResRef+".hak") - if _, keep := keepNames[moduleResRef]; !keep { + legacy := p.HAKArchivePath(p.Config.Module.ResRef) + if _, keep := keepNames[p.Config.Module.ResRef]; !keep { if err := os.Remove(legacy); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("remove legacy generated hak %s: %w", legacy, err) } @@ -1514,8 +1513,8 @@ func matchPathPattern(path, pattern string) bool { return matchSegments(pathSegs, patternSegs) } -func cleanupGeneratedHAKs(buildDir, moduleResRef string) error { - paths, err := manifestHAKPaths(buildDir) +func cleanupGeneratedHAKs(p *project.Project) error { + paths, err := manifestHAKPaths(p) if err != nil { return err } @@ -1525,12 +1524,12 @@ func cleanupGeneratedHAKs(buildDir, moduleResRef string) error { } } - legacy := filepath.Join(buildDir, moduleResRef+".hak") + legacy := p.HAKArchivePath(p.Config.Module.ResRef) if err := os.Remove(legacy); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("remove legacy generated hak %s: %w", legacy, err) } - manifest := filepath.Join(buildDir, "haks.json") + manifest := p.HAKManifestPath() if err := os.Remove(manifest); err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("remove previous hak manifest: %w", err) } @@ -1538,7 +1537,7 @@ func cleanupGeneratedHAKs(buildDir, moduleResRef string) error { } func plannedModuleHAKOrder(p *project.Project) ([]string, error) { - manifest, err := loadPreviousBuildManifest(p.BuildDir()) + manifest, err := loadPreviousBuildManifest(p.HAKManifestPath()) if err != nil { return nil, err } diff --git a/internal/pipeline/compare.go b/internal/pipeline/compare.go index bac4756..fb0ef3c 100644 --- a/internal/pipeline/compare.go +++ b/internal/pipeline/compare.go @@ -15,7 +15,6 @@ import ( "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/gff" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project" - "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/topdata" ) type CompareResult struct { @@ -31,12 +30,12 @@ type resourceExpectation struct { } func Compare(p *project.Project) (CompareResult, error) { - modulePath := filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod") + modulePath := p.ModuleArchivePath() var hakPaths []string if len(p.Inventory.AssetFiles) > 0 { var err error - hakPaths, err = manifestHAKPaths(p.BuildDir()) + hakPaths, err = manifestHAKPaths(p) if err != nil { return CompareResult{}, err } @@ -294,8 +293,8 @@ func resourceKey(name, extension string) string { return strings.ToLower(name) + "." + strings.TrimPrefix(strings.ToLower(extension), ".") } -func manifestHAKPaths(buildDir string) ([]string, error) { - manifestPath := filepath.Join(buildDir, "haks.json") +func manifestHAKPaths(p *project.Project) ([]string, error) { + manifestPath := p.HAKManifestPath() raw, err := os.ReadFile(manifestPath) if err == nil { var manifest BuildManifest @@ -304,7 +303,7 @@ func manifestHAKPaths(buildDir string) ([]string, error) { } paths := make([]string, 0, len(manifest.HAKs)) for _, hak := range manifest.HAKs { - paths = append(paths, filepath.Join(buildDir, hak.Name+".hak")) + paths = append(paths, p.HAKArchivePath(hak.Name)) } return paths, nil } @@ -312,14 +311,14 @@ func manifestHAKPaths(buildDir string) ([]string, error) { return nil, fmt.Errorf("read hak manifest: %w", err) } - legacy := filepath.Join(buildDir, "*.hak") + legacy := filepath.Join(p.BuildDir(), "*.hak") paths, err := filepath.Glob(legacy) if err != nil { return nil, fmt.Errorf("scan hak archives: %w", err) } filtered := make([]string, 0, len(paths)) for _, path := range paths { - if filepath.Base(path) == topdata.PackageHAKFileName { + if filepath.Base(path) == p.TopDataPackageHAKName() { continue } filtered = append(filtered, path) diff --git a/internal/pipeline/extract.go b/internal/pipeline/extract.go index b1a4e38..57f99e5 100644 --- a/internal/pipeline/extract.go +++ b/internal/pipeline/extract.go @@ -16,12 +16,13 @@ import ( ) type ExtractResult struct { - ModulePath string - HAKPaths []string - Written int - Overwritten int - Removed int - Skipped int + ModulePath string + HAKPaths []string + DeletedArchivePaths []string + Written int + Overwritten int + Removed int + Skipped int } func Extract(p *project.Project, files ...string) (ExtractResult, error) { @@ -36,7 +37,7 @@ func Extract(p *project.Project, files ...string) (ExtractResult, error) { shouldExtractMod := len(allowed) == 0 || allowed[p.Config.Module.ResRef+".mod"] if shouldExtractMod { - modulePath := filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod") + modulePath := p.ModuleArchivePath() input, err := os.Open(modulePath) if err != nil { return ExtractResult{}, fmt.Errorf("open module archive: %w", err) @@ -94,6 +95,12 @@ func Extract(p *project.Project, files ...string) (ExtractResult, error) { if len(failures) > 0 { return result, errors.Join(failures...) } + if p.Config.Extract.DeleteModuleArchiveAfterSuccess && result.ModulePath != "" { + if err := os.Remove(result.ModulePath); err != nil { + return result, fmt.Errorf("delete consumed module archive %s: %w", result.ModulePath, err) + } + result.DeletedArchivePaths = append(result.DeletedArchivePaths, result.ModulePath) + } return result, nil } diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index d1b3504..3e7e4d1 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -175,6 +175,141 @@ func TestExtractReadsHAKAssets(t *testing.T) { } } +func TestExtractDeletesConsumedModuleArchiveAfterSuccessWhenConfigured(t *testing.T) { + root := t.TempDir() + mustMkdir(t, filepath.Join(root, "src", "module")) + mustMkdir(t, filepath.Join(root, "assets")) + mustMkdir(t, filepath.Join(root, "build")) + + mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{ + "module": { + "name": "Test Module", + "resref": "testmod" + }, + "paths": { + "source": "src", + "assets": "assets", + "build": "build" + }, + "extract": { + "delete_module_archive_after_success": true + } +} +`) + mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{ + "file_type": "IFO ", + "file_version": "V3.2", + "root": { + "struct_type": 0, + "fields": [ + { + "label": "Mod_Name", + "type": "CExoString", + "value": "Original Module" + } + ] + } +} +`) + + p, err := project.Load(root) + if err != nil { + t.Fatalf("load project: %v", err) + } + if err := p.ValidateLayout(); err != nil { + t.Fatalf("validate layout: %v", err) + } + if err := p.Scan(); err != nil { + t.Fatalf("scan: %v", err) + } + + buildResult, err := BuildModule(p) + if err != nil { + t.Fatalf("build module: %v", err) + } + if err := os.Remove(filepath.Join(root, "src", "module", "module.ifo.json")); err != nil { + t.Fatalf("remove source file: %v", err) + } + if err := p.Scan(); err != nil { + t.Fatalf("rescan: %v", err) + } + + result, err := Extract(p) + if err != nil { + t.Fatalf("extract: %v", err) + } + if len(result.DeletedArchivePaths) != 1 { + t.Fatalf("expected 1 deleted archive path, got %#v", result.DeletedArchivePaths) + } + if result.DeletedArchivePaths[0] != buildResult.ModulePath { + t.Fatalf("expected deleted archive %s, got %#v", buildResult.ModulePath, result.DeletedArchivePaths) + } + if _, err := os.Stat(buildResult.ModulePath); !os.IsNotExist(err) { + t.Fatalf("expected consumed module archive to be removed, stat err=%v", err) + } + if _, err := os.Stat(filepath.Join(root, "src", "module", "module.ifo.json")); err != nil { + t.Fatalf("expected extracted module file: %v", err) + } +} + +func TestExtractKeepsConsumedModuleArchiveWhenExtractionFails(t *testing.T) { + root := t.TempDir() + mustMkdir(t, filepath.Join(root, "src")) + mustMkdir(t, filepath.Join(root, "assets")) + mustMkdir(t, filepath.Join(root, "build")) + + mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{ + "module": { + "name": "Test Module", + "resref": "testmod" + }, + "paths": { + "source": "src", + "assets": "assets", + "build": "build" + }, + "extract": { + "delete_module_archive_after_success": true + } +} +`) + + p, err := project.Load(root) + if err != nil { + t.Fatalf("load project: %v", err) + } + if err := p.ValidateLayout(); err != nil { + t.Fatalf("validate layout: %v", err) + } + if err := p.Scan(); err != nil { + t.Fatalf("scan: %v", err) + } + + modFile, err := os.Create(p.ModuleArchivePath()) + if err != nil { + t.Fatalf("create mod: %v", err) + } + if err := erf.Write(modFile, erf.New("MOD ", []erf.Resource{ + {Name: "broken", Type: 0xFFFF, Data: []byte("bad")}, + })); err != nil { + t.Fatalf("write mod: %v", err) + } + if err := modFile.Close(); err != nil { + t.Fatalf("close mod: %v", err) + } + + result, err := Extract(p) + if err == nil { + t.Fatal("expected extract to fail") + } + if len(result.DeletedArchivePaths) != 0 { + t.Fatalf("expected no deleted archive paths, got %#v", result.DeletedArchivePaths) + } + if _, statErr := os.Stat(p.ModuleArchivePath()); statErr != nil { + t.Fatalf("expected consumed module archive to remain after failed extract, stat err=%v", statErr) + } +} + func TestBuildSplitsConfiguredHAKs(t *testing.T) { root := t.TempDir() mustMkdir(t, filepath.Join(root, "src")) diff --git a/internal/project/project.go b/internal/project/project.go index 49db49d..7ae8daf 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -63,10 +63,13 @@ type TopDataConfig struct { Build string `json:"build"` ReferenceBuilder string `json:"reference_builder"` Assets string `json:"assets"` + PackageHAK string `json:"package_hak"` + PackageTLK string `json:"package_tlk"` } type ExtractConfig struct { - IgnoreExtensions []string `json:"ignore_extensions"` + IgnoreExtensions []string `json:"ignore_extensions"` + DeleteModuleArchiveAfterSuccess bool `json:"delete_module_archive_after_success,omitempty"` } type Inventory struct { @@ -127,6 +130,17 @@ func (p *Project) ValidateLayout() error { if len(p.Config.Module.ResRef) > 16 { failures = append(failures, fmt.Errorf("module.resref %q exceeds 16 characters", p.Config.Module.ResRef)) } + if p.HasTopData() { + if err := validateOutputFileName("topdata.package_hak", p.TopDataPackageHAKName(), ".hak"); err != nil { + failures = append(failures, err) + } + if err := validateOutputFileName("topdata.package_tlk", p.TopDataPackageTLKName(), ".tlk"); err != nil { + failures = append(failures, err) + } + if err := validateOutputFileName("topdata.compiled_tlk", filepath.Base(p.TopDataCompiledTLKPath()), ".tlk"); err != nil { + failures = append(failures, err) + } + } for index, hak := range p.Config.HAKs { if strings.TrimSpace(hak.Name) == "" { @@ -244,6 +258,18 @@ func (p *Project) BuildDir() string { return filepath.Join(p.Root, p.Config.Paths.Build) } +func (p *Project) ModuleArchivePath() string { + return filepath.Join(p.BuildDir(), p.Config.Module.ResRef+".mod") +} + +func (p *Project) HAKManifestPath() string { + return filepath.Join(p.BuildDir(), "haks.json") +} + +func (p *Project) HAKArchivePath(name string) string { + return filepath.Join(p.BuildDir(), strings.TrimSpace(name)+".hak") +} + func (p *Project) CloneWithHAKNames(names []string) (*Project, error) { if len(names) == 0 { return p, nil @@ -341,12 +367,70 @@ func (p *Project) TopDataAssetsDir() string { return absPath } +func (p *Project) TopDataUsesRepoCache() bool { + return sameCleanPath(p.TopDataBuildDir(), filepath.Join(p.Root, ".cache")) +} + +func (p *Project) TopDataCompiled2DADir() string { + return filepath.Join(p.TopDataBuildDir(), "2da") +} + +func (p *Project) TopDataCompiledTLKPath() string { + if p.TopDataUsesRepoCache() { + return filepath.Join(p.BuildDir(), "sow_tlk.tlk") + } + return filepath.Join(p.TopDataBuildDir(), "sow_tlk.tlk") +} + +func (p *Project) TopDataCompiledTLKDir() string { + return filepath.Dir(p.TopDataCompiledTLKPath()) +} + +func (p *Project) TopDataWikiRootDir() string { + if p.TopDataUsesRepoCache() { + return filepath.Join(p.Root, ".cache", "wiki") + } + return filepath.Join(p.TopDataBuildDir(), "wiki") +} + +func (p *Project) TopDataWikiPagesDir() string { + return filepath.Join(p.TopDataWikiRootDir(), "pages") +} + +func (p *Project) TopDataWikiStatePath() string { + return filepath.Join(p.TopDataWikiRootDir(), "state.json") +} + +func (p *Project) TopDataPackageHAKName() string { + name := strings.TrimSpace(p.Config.TopData.PackageHAK) + if name == "" { + name = "sow_top.hak" + } + return name +} + +func (p *Project) TopDataPackageTLKName() string { + name := strings.TrimSpace(p.Config.TopData.PackageTLK) + if name == "" { + name = "sow_tlk.tlk" + } + return name +} + +func (p *Project) TopDataPackageHAKPath() string { + return filepath.Join(p.BuildDir(), p.TopDataPackageHAKName()) +} + +func (p *Project) TopDataPackageTLKPath() string { + return filepath.Join(p.BuildDir(), p.TopDataPackageTLKName()) +} + func (i Inventory) Report() InventoryReport { return InventoryReport{ SourceFiles: len(i.SourceFiles), ScriptFiles: len(i.ScriptFiles), AssetFiles: len(i.AssetFiles), - Extensions: i.Extensions, + Extensions: i.Extensions, } } @@ -356,7 +440,9 @@ func defaultConfig() Config { Build: "build", }, TopData: TopDataConfig{ - Build: ".cache", + Build: ".cache", + PackageHAK: "sow_top.hak", + PackageTLK: "sow_tlk.tlk", }, } } @@ -406,3 +492,21 @@ func scanDir(root string, include func(path string) bool) ([]string, []string, e slices.Sort(exts) return files, exts, nil } + +func validateOutputFileName(field, name, expectedExt string) error { + trimmed := strings.TrimSpace(name) + if trimmed == "" { + return fmt.Errorf("%s is required", field) + } + if filepath.Base(trimmed) != trimmed { + return fmt.Errorf("%s must be a file name, not a path: %q", field, name) + } + if !strings.EqualFold(filepath.Ext(trimmed), expectedExt) { + return fmt.Errorf("%s must use %s extension: %q", field, expectedExt, name) + } + return nil +} + +func sameCleanPath(a, b string) bool { + return filepath.Clean(a) == filepath.Clean(b) +} diff --git a/internal/project/project_test.go b/internal/project/project_test.go index c7bc0be..3c56d35 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "slices" + "strings" "testing" ) @@ -83,6 +84,67 @@ func TestTopDataAssetsDirReturnsEmptyForNoConfig(t *testing.T) { } } +func TestModuleArchivePathUsesConfiguredBuildDirAndResRef(t *testing.T) { + root := t.TempDir() + mkdirAll(t, filepath.Join(root, "output")) + + proj := &Project{ + Root: root, + Config: Config{ + Module: ModuleConfig{Name: "Test", ResRef: "testmod"}, + Paths: PathConfig{Build: "output"}, + }, + } + + got := proj.ModuleArchivePath() + want := filepath.Join(root, "output", "testmod.mod") + if got != want { + t.Fatalf("expected %s, got %s", want, got) + } +} + +func TestHAKPathsUseConfiguredBuildDir(t *testing.T) { + root := t.TempDir() + mkdirAll(t, filepath.Join(root, "output")) + + proj := &Project{ + Root: root, + Config: Config{ + Paths: PathConfig{Build: "output"}, + }, + } + + if got, want := proj.HAKManifestPath(), filepath.Join(root, "output", "haks.json"); got != want { + t.Fatalf("expected manifest path %s, got %s", want, got) + } + if got, want := proj.HAKArchivePath("core_01"), filepath.Join(root, "output", "core_01.hak"); got != want { + t.Fatalf("expected hak path %s, got %s", want, got) + } +} + +func TestTopDataPackagePathsUseConfiguredNamesAndBuildDir(t *testing.T) { + root := t.TempDir() + mkdirAll(t, filepath.Join(root, "output")) + + proj := &Project{ + Root: root, + Config: Config{ + Paths: PathConfig{Build: "output"}, + TopData: TopDataConfig{ + PackageHAK: "custom_top.hak", + PackageTLK: "custom_dialog.tlk", + }, + }, + } + + if got, want := proj.TopDataPackageHAKPath(), filepath.Join(root, "output", "custom_top.hak"); got != want { + t.Fatalf("expected top hak path %s, got %s", want, got) + } + if got, want := proj.TopDataPackageTLKPath(), filepath.Join(root, "output", "custom_dialog.tlk"); got != want { + t.Fatalf("expected top tlk path %s, got %s", want, got) + } +} + func TestValidateLayoutAllowsMissingAssetsDir(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "src")) @@ -104,6 +166,36 @@ func TestValidateLayoutAllowsMissingAssetsDir(t *testing.T) { } } +func TestValidateLayoutRejectsInvalidTopDataPackageOutputNames(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: "Test", ResRef: "test"}, + Paths: PathConfig{Source: "src", Build: "build"}, + TopData: TopDataConfig{ + Source: "topdata", + PackageHAK: "nested/custom_top.hak", + PackageTLK: "custom_dialog.txt", + }, + }, + } + + err := proj.ValidateLayout() + if err == nil { + t.Fatal("expected topdata output validation error") + } + if !strings.Contains(err.Error(), "topdata.package_hak") { + t.Fatalf("expected package_hak validation error, got %v", err) + } + if !strings.Contains(err.Error(), "topdata.package_tlk") { + t.Fatalf("expected package_tlk validation error, got %v", err) + } +} + func TestScanAllowsMissingAssetsDir(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "src")) diff --git a/internal/topdata/top_package.go b/internal/topdata/top_package.go index de54653..63621bd 100644 --- a/internal/topdata/top_package.go +++ b/internal/topdata/top_package.go @@ -22,41 +22,31 @@ const ( ) func compiled2DAOutputDir(p *project.Project) string { - return filepath.Join(p.TopDataBuildDir(), "2da") -} - -func defaultTopDataCacheDir(p *project.Project) string { - return filepath.Join(p.Root, ".cache") + return p.TopDataCompiled2DADir() } func topDataBuildUsesRepoCache(p *project.Project) bool { - return samePath(p.TopDataBuildDir(), defaultTopDataCacheDir(p)) + return p.TopDataUsesRepoCache() } func compiledTLKOutputPath(p *project.Project) string { - if topDataBuildUsesRepoCache(p) { - return filepath.Join(p.BuildDir(), defaultTLKName) - } - return filepath.Join(p.TopDataBuildDir(), defaultTLKName) + return p.TopDataCompiledTLKPath() } func compiledTLKOutputDir(p *project.Project) string { - return filepath.Dir(compiledTLKOutputPath(p)) + return p.TopDataCompiledTLKDir() } func wikiOutputRootDir(p *project.Project) string { - if topDataBuildUsesRepoCache(p) { - return filepath.Join(defaultTopDataCacheDir(p), wikiRootDirName) - } - return filepath.Join(p.TopDataBuildDir(), wikiRootDirName) + return p.TopDataWikiRootDir() } func wikiOutputPagesDir(p *project.Project) string { - return filepath.Join(wikiOutputRootDir(p), wikiPagesDirName) + return p.TopDataWikiPagesDir() } func wikiOutputStatePath(p *project.Project) string { - return filepath.Join(wikiOutputRootDir(p), wikiStateFileName) + return p.TopDataWikiStatePath() } func legacyCompiled2DAOutputDir(p *project.Project) string { @@ -106,7 +96,7 @@ func BuildAndPackageWithOptions(p *project.Project, opts BuildAndPackageOptions, return PackageResult{}, err } if ok { - progress("Compiled topdata outputs are current; refreshing sow_top.hak and sow_tlk.tlk only.") + progress(fmt.Sprintf("Compiled topdata outputs are current; refreshing %s and %s only.", p.TopDataPackageHAKName(), p.TopDataPackageTLKName())) return packageBuiltTopData(p, nativeResult, progress) } } @@ -133,11 +123,10 @@ func packageBuiltTopData(p *project.Project, nativeResult BuildResult, progress if progress == nil { progress = func(string) {} } - buildDir := p.BuildDir() - outputHAK := filepath.Join(buildDir, PackageHAKFileName) - outputTLK := filepath.Join(buildDir, PackageTLKFileName) + outputHAK := p.TopDataPackageHAKPath() + outputTLK := p.TopDataPackageTLKPath() - progress("Packaging compiled topdata resources into sow_top.hak...") + progress(fmt.Sprintf("Packaging compiled topdata resources into %s...", filepath.Base(outputHAK))) resources, assetFiles, err := collectTopPackageResources(p, nativeResult.Output2DADir) if err != nil { return PackageResult{}, err @@ -146,7 +135,7 @@ func packageBuiltTopData(p *project.Project, nativeResult BuildResult, progress return PackageResult{}, err } - progress("Preparing compiled TLK release output...") + progress(fmt.Sprintf("Preparing compiled TLK release output %s...", filepath.Base(outputTLK))) sourceTLK := compiledTLKOutputPath(p) if nativeResult.OutputTLKDir != "" { sourceTLK, err = resolveCompiledTLKPath(nativeResult.OutputTLKDir) @@ -446,8 +435,8 @@ func currentTopPackageResult(p *project.Project) (PackageResult, bool, error) { return PackageResult{}, ok, err } - outputHAK := filepath.Join(p.BuildDir(), PackageHAKFileName) - outputTLK := filepath.Join(p.BuildDir(), PackageTLKFileName) + outputHAK := p.TopDataPackageHAKPath() + outputTLK := p.TopDataPackageTLKPath() hakInfo, err := os.Stat(outputHAK) if err != nil { if os.IsNotExist(err) { diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index 699fca6..cfe8d28 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -9182,6 +9182,41 @@ func TestBuildAndPackageIncludesCompiled2DAAndTopAssets(t *testing.T) { } } +func TestBuildAndPackageHonorsConfiguredTopPackageOutputNames(t *testing.T) { + root := testProjectRoot(t) + mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust")) + mkdirAll(t, filepath.Join(root, "topdata", "assets", "gui")) + writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n") + writeFile(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), `{ + "output": "repadjust.2da", + "columns": ["Label"], + "rows": [{"id": 0, "Label": "TEST_LABEL"}] +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "assets", "gui", "testicon.png"), "icon-data") + + proj := testProject(root) + proj.Config.TopData.ReferenceBuilder = "" + proj.Config.TopData.PackageHAK = "custom_top.hak" + proj.Config.TopData.PackageTLK = "custom_dialog.tlk" + + result, err := BuildAndPackage(proj, nil) + if err != nil { + t.Fatalf("BuildAndPackage failed: %v", err) + } + if result.OutputHAKPath != proj.TopDataPackageHAKPath() { + t.Fatalf("unexpected hak path: %s", result.OutputHAKPath) + } + if result.OutputTLKPath != proj.TopDataPackageTLKPath() { + t.Fatalf("unexpected tlk path: %s", result.OutputTLKPath) + } + if _, err := os.Stat(proj.TopDataPackageHAKPath()); err != nil { + t.Fatalf("expected configured packaged hak output: %v", err) + } + if _, err := os.Stat(proj.TopDataPackageTLKPath()); err != nil { + t.Fatalf("expected configured packaged tlk output: %v", err) + } +} + func TestBuildAndPackageNoOpsWhenOutputsAreCurrent(t *testing.T) { root := topPackageTestProject(t) proj := testProject(root)