From 44e6fab372cc3a799763ab266e50935b91b10994 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Sat, 23 May 2026 00:29:04 +0200 Subject: [PATCH] Asset-owned topdata --- README.md | 5 +- internal/pipeline/pipeline_test.go | 118 +++++++++++++++++++++++++++++ internal/project/project.go | 7 +- internal/project/project_test.go | 42 ++++++++++ 4 files changed, 168 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0240314..b6da11f 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,10 @@ documents otherwise. `generated_assets.topdata_2da` is for HAK repositories that need a small native-topdata subset packaged as HAK resources. It builds only `.2da` output, injects those generated files into `build-haks`, and rejects TLK-backed text -because this path does not produce TLK packages. +because this path does not produce TLK packages. Static generated groups can +omit `autogen.mode`; in that case the selected datasets are compiled directly, +and `package_root` plus the repository's HAK include patterns determine where +the resources are packaged. For `cachedmodels_rows` generated assets, the configured autogen scan appends discovered model stems to the selected `cachedmodels` dataset without `.mdl` diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index fab49fa..92c02ee 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -2398,6 +2398,124 @@ func TestBuildHAKsGeneratesParts2DAAssetsFromLocalModels(t *testing.T) { } } +func TestBuildHAKsPackagesStaticGeneratedTopDataByConfiguredRoot(t *testing.T) { + root := t.TempDir() + mustMkdir(t, filepath.Join(root, "src")) + mustMkdir(t, filepath.Join(root, "assets")) + mustMkdir(t, filepath.Join(root, "topdata", "data", "tailmodel")) + mustMkdir(t, filepath.Join(root, "topdata", "data", "skyboxes")) + mustMkdir(t, filepath.Join(root, "build")) + + mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{ + "module": { + "name": "Test Module", + "resref": "testmod", + "hak_order": ["group:sow_part", "group:sow_envi"] + }, + "paths": { + "source": "src", + "assets": "assets", + "build": "build" + }, + "generated_assets": { + "topdata_2da": [ + { + "id": "part_topdata", + "source": "topdata", + "output": "{paths.cache}/generated-assets/part-topdata-2da", + "include_datasets": ["tailmodel"], + "package_root": "part" + }, + { + "id": "envi_topdata", + "source": "topdata", + "output": "{paths.cache}/generated-assets/envi-topdata-2da", + "include_datasets": ["skyboxes"], + "package_root": "envi" + } + ] + }, + "haks": [ + { + "name": "sow_part", + "priority": 1, + "max_bytes": 0, + "split": false, + "include": ["part/**"] + }, + { + "name": "sow_envi", + "priority": 2, + "max_bytes": 0, + "split": false, + "include": ["envi/**"] + } + ] +} +`) + mustWriteFile(t, filepath.Join(root, "topdata", "data", "tailmodel", "base.json"), `{ + "output": "tailmodel.2da", + "columns": ["LABEL", "MODEL", "ENVMAP"], + "rows": [ + {"id": 0, "LABEL": "(None)", "MODEL": "****", "ENVMAP": "****"}, + {"id": 1, "LABEL": "Lizard", "MODEL": "c_tailliz", "ENVMAP": "default"} + ] +} +`) + mustWriteFile(t, filepath.Join(root, "topdata", "data", "skyboxes", "base.json"), `{ + "output": "skyboxes.2da", + "columns": ["LABEL", "STRING_REF", "CYCLICAL", "DAWN", "DAY", "DUSK", "NIGHT"], + "rows": [ + {"id": 0, "LABEL": "(None)", "STRING_REF": "****", "CYCLICAL": "****", "DAWN": "****", "DAY": "****", "DUSK": "****", "NIGHT": "****"}, + {"id": 1, "LABEL": "Grass_Clear", "STRING_REF": "****", "CYCLICAL": "1", "DAWN": "Skyda_001", "DAY": "Sky_001", "DUSK": "Skyd_001", "NIGHT": "Skyn_001"} + ] +} +`) + + 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) + } + + result, err := BuildHAKs(p) + if err != nil { + t.Fatalf("build haks: %v", err) + } + if len(result.HAKPaths) != 2 { + t.Fatalf("expected two HAKs, got %#v", result.HAKPaths) + } + raw, err := os.ReadFile(filepath.Join(root, "build", "haks.json")) + if err != nil { + t.Fatalf("read haks manifest: %v", err) + } + manifestText := string(raw) + for _, want := range []string{ + `"part/tailmodel.2da"`, + `"envi/skyboxes.2da"`, + } { + if !strings.Contains(manifestText, want) { + t.Fatalf("expected generated static 2da asset %q in manifest, got:\n%s", want, manifestText) + } + } + if strings.Contains(manifestText, `"part/skyboxes.2da"`) || strings.Contains(manifestText, `"envi/tailmodel.2da"`) { + t.Fatalf("expected generated static 2da assets to follow configured package roots, got:\n%s", manifestText) + } + for _, generated := range []string{ + filepath.Join(root, ".cache", "generated-assets", "part-topdata-2da", "tailmodel.2da"), + filepath.Join(root, ".cache", "generated-assets", "envi-topdata-2da", "skyboxes.2da"), + } { + if _, err := os.Stat(generated); err != nil { + t.Fatalf("expected generated 2da %s: %v", generated, err) + } + } +} + func TestBuildHAKsRejectsTLKBackedGeneratedParts2DAAssets(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 5f73a40..d73d2a1 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -1457,10 +1457,11 @@ func validateGeneratedConfig(cfg GeneratedConfig) []error { failures = append(failures, fmt.Errorf("%s.include_datasets must contain at least one pattern", fieldPrefix)) } failures = append(failures, validateGlobList(fieldPrefix+".include_datasets", top2da.IncludeDatasets)...) - switch strings.TrimSpace(top2da.Autogen.Mode) { + autogenMode := strings.TrimSpace(top2da.Autogen.Mode) + switch autogenMode { case "parts_rows", "cachedmodels_rows": case "": - failures = append(failures, fmt.Errorf("%s.autogen.mode is required", fieldPrefix)) + continue default: failures = append(failures, fmt.Errorf("%s.autogen.mode %q is not supported", fieldPrefix, top2da.Autogen.Mode)) } @@ -1473,7 +1474,7 @@ func validateGeneratedConfig(cfg GeneratedConfig) []error { } failures = append(failures, validateGlobList(fieldPrefix+".autogen.include", top2da.Autogen.Include)...) failures = append(failures, validateAutogenDeriveConfig(fieldPrefix+".autogen.derive", top2da.Autogen.Derive)...) - if strings.TrimSpace(top2da.Autogen.Mode) == "parts_rows" && partsRowsConfigConfigured(top2da.Autogen.PartsRows) { + if autogenMode == "parts_rows" && partsRowsConfigConfigured(top2da.Autogen.PartsRows) { failures = append(failures, validatePartsRowsConfig(fieldPrefix+".autogen.parts_rows", top2da.Autogen.PartsRows)...) } } diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 3e46f47..217f446 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -1092,6 +1092,13 @@ func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) { Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, }, }, + { + ID: "static_topdata", + Source: "topdata", + Output: "{paths.cache}/generated-assets/static-topdata-2da", + IncludeDatasets: []string{"tailmodel", "wingmodel"}, + PackageRoot: "part", + }, }, }, }, @@ -1106,6 +1113,41 @@ func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) { } } +func TestValidateLayoutRejectsUnsupportedGeneratedTopDataAutogenMode(t *testing.T) { + root := t.TempDir() + mkdirAll(t, filepath.Join(root, "assets")) + mkdirAll(t, filepath.Join(root, "build")) + proj := Project{ + Root: root, + Config: Config{ + Module: ModuleConfig{Name: "Test", ResRef: "testmod"}, + Paths: PathConfig{Assets: "assets"}, + Generated: GeneratedConfig{ + TopData2DA: []GeneratedTopData2DAConfig{ + { + ID: "bad", + Source: "topdata", + Output: "{paths.cache}/generated-assets/bad-2da", + IncludeDatasets: []string{"tailmodel"}, + PackageRoot: "part", + Autogen: AutogenConsumerConfig{ + Mode: "unknown_rows", + Root: "part", + Include: []string{"**/*.mdl"}, + Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, + }, + }, + }, + }, + }, + } + + err := proj.ValidateLayout() + if err == nil || !strings.Contains(err.Error(), `generated_assets.topdata_2da[0].autogen.mode "unknown_rows" is not supported`) { + t.Fatalf("expected unsupported generated autogen mode validation error, got %v", err) + } +} + func TestValidateLayoutRejectsEscapingGeneratedTopData2DAConfig(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "assets"))