Categories with underscore

This commit is contained in:
2026-05-31 14:49:52 +02:00
parent c57b2e21a2
commit 3e169276f7
5 changed files with 106 additions and 106 deletions
+1 -1
View File
@@ -423,7 +423,7 @@ Minimum messages:
- `Preparing model compiler...` - `Preparing model compiler...`
- `Preparing model compilation workspace...` - `Preparing model compilation workspace...`
- `Compiling model 12/324: part/belt/pfa0_belt018.mdl` - `Compiling model 12/324: part/belt/pfa0_belt018.mdl`
- `Reusing compiled model: vfxs/headaccessory/hfx_bandana.mdl` - `Reusing compiled model: vfxs/head_accessories/hfx_bandana.mdl`
- `Skipping binary model source: creatures/foo.mdl` - `Skipping binary model source: creatures/foo.mdl`
- `FAILED model compile: placeables/broken_model.mdl` - `FAILED model compile: placeables/broken_model.mdl`
+21 -21
View File
@@ -2178,10 +2178,10 @@ func TestPlanHAKsWritesManifestWithoutArchives(t *testing.T) {
func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T) { func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T) {
root := t.TempDir() root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src")) mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "chestaccessory")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "chest_accessories"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "headaccessory")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_accessories"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "headdecoration")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_decorations"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "headfeature", "hair")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair"))
mustMkdir(t, filepath.Join(root, "build")) mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{ mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
@@ -2200,7 +2200,7 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
{ {
"id": "head_visualeffects", "id": "head_visualeffects",
"root": "vfxs", "root": "vfxs",
"include": ["chestaccessory/**/*.mdl", "headaccessory/**/*.mdl", "headdecoration/**/*.mdl", "headfeature/**/*.mdl"], "include": ["chest_accessories/**/*.mdl", "head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.mdl"],
"derive": { "derive": {
"kind": "model_stem", "kind": "model_stem",
"group_from": "first_path_segment" "group_from": "first_path_segment"
@@ -2212,8 +2212,8 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
}, },
"head_visualeffects": { "head_visualeffects": {
"groups": { "groups": {
"headaccessory": { "head_accessories": {
"prefix": "headaccessory", "prefix": "head_accessories",
"model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"] "model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"]
} }
} }
@@ -2233,10 +2233,10 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
} }
`) `)
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "chestaccessory", "tfx_sash.mdl"), "sash") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "chest_accessories", "tfx_sash.mdl"), "sash")
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "headaccessory", "hfx_bandana.mdl"), "bandana") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_accessories", "hfx_bandana.mdl"), "bandana")
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "headdecoration", "hfx_laurel.mdl"), "laurel") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_decorations", "hfx_laurel.mdl"), "laurel")
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "headfeature", "hair", "hfx_hair_bangs.mdl"), "hair") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair", "hfx_hair_bangs.mdl"), "hair")
p, err := project.Load(root) p, err := project.Load(root)
if err != nil { if err != nil {
@@ -2267,16 +2267,16 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
text := string(headRaw) text := string(headRaw)
for _, want := range []string{ for _, want := range []string{
`"id": "head_visualeffects"`, `"id": "head_visualeffects"`,
`"group": "chestaccessory"`, `"group": "chest_accessories"`,
`"model_stem": "tfx_sash"`, `"model_stem": "tfx_sash"`,
`"group": "headaccessory"`, `"group": "head_accessories"`,
`"model_stem": "hfx_bandana"`, `"model_stem": "hfx_bandana"`,
`"head_visualeffects": {`, `"head_visualeffects": {`,
`"model_columns": [`, `"model_columns": [`,
`"Imp_Root_H_Node"`, `"Imp_Root_H_Node"`,
`"group": "headdecoration"`, `"group": "head_decorations"`,
`"model_stem": "hfx_laurel"`, `"model_stem": "hfx_laurel"`,
`"group": "headfeature"`, `"group": "head_features"`,
`"model_stem": "hfx_hair_bangs"`, `"model_stem": "hfx_hair_bangs"`,
} { } {
if !strings.Contains(text, want) { if !strings.Contains(text, want) {
@@ -2288,7 +2288,7 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
func TestBuildHAKsWritesFolderCategoryInHeadVisualeffectsAutogenManifest(t *testing.T) { func TestBuildHAKsWritesFolderCategoryInHeadVisualeffectsAutogenManifest(t *testing.T) {
root := t.TempDir() root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src")) mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "headfeature", "sinfar", "ears_plt")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_features", "sinfar", "ears_plt"))
mustMkdir(t, filepath.Join(root, "build")) mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{ mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
@@ -2307,7 +2307,7 @@ func TestBuildHAKsWritesFolderCategoryInHeadVisualeffectsAutogenManifest(t *test
{ {
"id": "head_visualeffects", "id": "head_visualeffects",
"root": "vfxs", "root": "vfxs",
"include": ["headfeature/**/*.mdl"], "include": ["head_features/**/*.mdl"],
"derive": { "derive": {
"kind": "model_stem", "kind": "model_stem",
"group_from": "first_path_segment" "group_from": "first_path_segment"
@@ -2319,7 +2319,7 @@ func TestBuildHAKsWritesFolderCategoryInHeadVisualeffectsAutogenManifest(t *test
}, },
"head_visualeffects": { "head_visualeffects": {
"groups": { "groups": {
"headfeature": { "head_features": {
"model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"] "model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"]
} }
}, },
@@ -2346,7 +2346,7 @@ func TestBuildHAKsWritesFolderCategoryInHeadVisualeffectsAutogenManifest(t *test
} }
`) `)
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "headfeature", "sinfar", "ears_plt", "hfx_Ear_EL_L.mdl"), "ear") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_features", "sinfar", "ears_plt", "hfx_Ear_EL_L.mdl"), "ear")
p, err := project.Load(root) p, err := project.Load(root)
if err != nil { if err != nil {
@@ -2368,11 +2368,11 @@ func TestBuildHAKsWritesFolderCategoryInHeadVisualeffectsAutogenManifest(t *test
} }
text := string(headRaw) text := string(headRaw)
for _, want := range []string{ for _, want := range []string{
`"group": "headfeature"`, `"group": "head_features"`,
`"subgroup": "sinfar/ears_plt"`, `"subgroup": "sinfar/ears_plt"`,
`"category": "ears_plt"`, `"category": "ears_plt"`,
`"model_stem": "hfx_Ear_EL_L"`, `"model_stem": "hfx_Ear_EL_L"`,
`"source": "headfeature/sinfar/ears_plt/hfx_Ear_EL_L.mdl"`, `"source": "head_features/sinfar/ears_plt/hfx_Ear_EL_L.mdl"`,
`"group_token_source": "folder_name"`, `"group_token_source": "folder_name"`,
`"category_from": "immediate_parent"`, `"category_from": "immediate_parent"`,
} { } {
+7 -7
View File
@@ -1332,7 +1332,7 @@ func TestValidateLayoutRejectsInvalidAutogenConsumerConfig(t *testing.T) {
Dataset: "visualeffects", Dataset: "visualeffects",
Mode: "unsupported", Mode: "unsupported",
Root: "vfxs", Root: "vfxs",
Include: []string{"headaccessory/**/*.mdl"}, Include: []string{"head_accessories/**/*.mdl"},
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
Manifest: AutogenManifestConfig{ Manifest: AutogenManifestConfig{
ReleaseTag: "head-vfx-manifest-current", ReleaseTag: "head-vfx-manifest-current",
@@ -1434,7 +1434,7 @@ func TestValidateLayoutRejectsInvalidHeadVisualeffectsNamingConfig(t *testing.T)
Dataset: "visualeffects", Dataset: "visualeffects",
Mode: "head_visualeffects", Mode: "head_visualeffects",
Root: "vfxs", Root: "vfxs",
Include: []string{"headfeature/**/*.mdl"}, Include: []string{"head_features/**/*.mdl"},
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
Manifest: AutogenManifestConfig{ Manifest: AutogenManifestConfig{
ReleaseTag: "head-vfx-manifest-current", ReleaseTag: "head-vfx-manifest-current",
@@ -1443,7 +1443,7 @@ func TestValidateLayoutRejectsInvalidHeadVisualeffectsNamingConfig(t *testing.T)
}, },
HeadVisualeffects: HeadVisualeffectsConfig{ HeadVisualeffects: HeadVisualeffectsConfig{
Groups: map[string]HeadVisualeffectGroupConfig{ Groups: map[string]HeadVisualeffectGroupConfig{
"headfeature": {}, "head_features": {},
}, },
GroupTokenSource: "unsupported", GroupTokenSource: "unsupported",
CategoryFrom: "unsupported", CategoryFrom: "unsupported",
@@ -1488,19 +1488,19 @@ autogen:
mode: head_visualeffects mode: head_visualeffects
root: vfxs root: vfxs
include: include:
- headfeature/**/*.mdl - head_features/**/*.mdl
derive: derive:
kind: model_stem kind: model_stem
group_from: first_path_segment group_from: first_path_segment
head_visualeffects: head_visualeffects:
groups: groups:
headfeature: head_features:
model_columns: model_columns:
- Imp_HeadCon_Node - Imp_HeadCon_Node
group_token_source: folder_name group_token_source: folder_name
category_from: immediate_parent category_from: immediate_parent
legacy_groups: legacy_groups:
head_features: headfeature head_features: head_features
legacy_key_format: "{dataset}:{group}_{stem}" legacy_key_format: "{dataset}:{group}_{stem}"
`) `)
@@ -1557,7 +1557,7 @@ func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) {
ID: "cachedmodels", ID: "cachedmodels",
Mode: "cachedmodels_rows", Mode: "cachedmodels_rows",
Root: "vfxs", Root: "vfxs",
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"}, Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
}, },
}, },
+4 -4
View File
@@ -850,10 +850,10 @@ func applyHeadVisualeffectsConfig(policy *headVisualeffectsPolicy, cfg project.H
func defaultHeadVisualeffectsPolicy() headVisualeffectsPolicy { func defaultHeadVisualeffectsPolicy() headVisualeffectsPolicy {
return headVisualeffectsPolicy{ return headVisualeffectsPolicy{
Groups: map[string]headVisualeffectsGroupPolicy{ Groups: map[string]headVisualeffectsGroupPolicy{
"chestaccessory": {}, "chest_accessories": {},
"headaccessory": {}, "head_accessories": {},
"headdecoration": {}, "head_decorations": {},
"headfeature": {}, "head_features": {},
}, },
GroupTokenSource: "folder_name", GroupTokenSource: "folder_name",
CategoryFrom: "immediate_parent", CategoryFrom: "immediate_parent",
+73 -73
View File
@@ -6858,7 +6858,7 @@ func TestNormalizeProjectImportsLegacyVisualeffects(t *testing.T) {
}`+"\n") }`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "visualeffects", "lock.json"), `{ writeFile(t, filepath.Join(root, "reference", "data", "visualeffects", "lock.json"), `{
"visualeffects:chatbubble": 10100, "visualeffects:chatbubble": 10100,
"visualeffects:headaccessory_antlers_l": 10101, "visualeffects:head_accessories_antlers_l": 10101,
"visualeffects:vcm_hitmasscrit": 10125, "visualeffects:vcm_hitmasscrit": 10125,
"visualeffects:com_hitnegative": 288, "visualeffects:com_hitnegative": 288,
"visualeffects:fnf_blinddeaf": 18 "visualeffects:fnf_blinddeaf": 18
@@ -6919,8 +6919,8 @@ func TestNormalizeProjectImportsLegacyVisualeffects(t *testing.T) {
}`+"\n") }`+"\n")
writeFile(t, filepath.Join(root, "reference", "data", "visualeffects", "modules", "accessories", "add_eos.json"), `{ writeFile(t, filepath.Join(root, "reference", "data", "visualeffects", "modules", "accessories", "add_eos.json"), `{
"entries": { "entries": {
"visualeffects:headaccessory_antlers_l": { "visualeffects:head_accessories_antlers_l": {
"Label": "headaccessory_ANTLERS_L", "Label": "head_accessories_ANTLERS_L",
"Type_FD": "D", "Type_FD": "D",
"OrientWithGround": "0", "OrientWithGround": "0",
"Imp_HeadCon_Node": "hfx_antlers_l", "Imp_HeadCon_Node": "hfx_antlers_l",
@@ -6986,7 +6986,7 @@ func TestNormalizeProjectImportsLegacyVisualeffects(t *testing.T) {
t.Fatalf("read visualeffects lock: %v", err) t.Fatalf("read visualeffects lock: %v", err)
} }
lockText := string(lockRaw) lockText := string(lockRaw)
if !strings.Contains(lockText, `"visualeffects:chatbubble": 10100`) || !strings.Contains(lockText, `"visualeffects:headaccessory_antlers_l": 10101`) { if !strings.Contains(lockText, `"visualeffects:chatbubble": 10100`) || !strings.Contains(lockText, `"visualeffects:head_accessories_antlers_l": 10101`) {
t.Fatalf("expected imported visualeffects lock IDs, got:\n%s", lockText) t.Fatalf("expected imported visualeffects lock IDs, got:\n%s", lockText)
} }
} }
@@ -7005,7 +7005,7 @@ func TestBuildSupportsCanonicalVisualeffects(t *testing.T) {
}`+"\n") }`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "visualeffects", "lock.json"), `{ writeFile(t, filepath.Join(root, "topdata", "data", "visualeffects", "lock.json"), `{
"visualeffects:chatbubble": 10100, "visualeffects:chatbubble": 10100,
"visualeffects:headaccessory_antlers_l": 10101, "visualeffects:head_accessories_antlers_l": 10101,
"visualeffects:vcm_hitmasscrit": 10125, "visualeffects:vcm_hitmasscrit": 10125,
"visualeffects:com_hitnegative": 288, "visualeffects:com_hitnegative": 288,
"visualeffects:fnf_blinddeaf": 18 "visualeffects:fnf_blinddeaf": 18
@@ -7066,8 +7066,8 @@ func TestBuildSupportsCanonicalVisualeffects(t *testing.T) {
}`+"\n") }`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "visualeffects", "modules", "accessories", "add_eos.json"), `{ writeFile(t, filepath.Join(root, "topdata", "data", "visualeffects", "modules", "accessories", "add_eos.json"), `{
"entries": { "entries": {
"visualeffects:headaccessory_antlers_l": { "visualeffects:head_accessories_antlers_l": {
"Label": "headaccessory_ANTLERS_L", "Label": "head_accessories_ANTLERS_L",
"Type_FD": "D", "Type_FD": "D",
"OrientWithGround": "0", "OrientWithGround": "0",
"Imp_HeadCon_Node": "hfx_antlers_l", "Imp_HeadCon_Node": "hfx_antlers_l",
@@ -7113,7 +7113,7 @@ func TestBuildSupportsCanonicalVisualeffects(t *testing.T) {
"18\tVFX_FNF_BLINDDEAF\tF\t0", "18\tVFX_FNF_BLINDDEAF\tF\t0",
"288\tVFX_COM_HIT_NEGATIVE\tF\t0", "288\tVFX_COM_HIT_NEGATIVE\tF\t0",
"10100\tVFX_DUR_CHAT_BUBBLE\tD\t0", "10100\tVFX_DUR_CHAT_BUBBLE\tD\t0",
"10101\theadaccessory_ANTLERS_L\tD\t0", "10101\thead_accessories_ANTLERS_L\tD\t0",
"10125\tVFX_COM_HIT_MASSIVE_CRITICALS\tF\t0", "10125\tVFX_COM_HIT_MASSIVE_CRITICALS\tF\t0",
"scm_hitsmite", "scm_hitsmite",
"vff_explblind_l", "vff_explblind_l",
@@ -7147,7 +7147,7 @@ with open(os.path.join(out, "visualeffects.2da"), "w", encoding="utf-8") as f:
f.write("18\tVFX_FNF_BLINDDEAF\tF\t0\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\tvff_explblind_l\t0\n") f.write("18\tVFX_FNF_BLINDDEAF\tF\t0\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\tvff_explblind_l\t0\n")
f.write("288\tVFX_COM_HIT_NEGATIVE\tF\t0\t****\t****\t****\t****\t****\t****\tscm_hitsmite\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t0\n") f.write("288\tVFX_COM_HIT_NEGATIVE\tF\t0\t****\t****\t****\t****\t****\t****\tscm_hitsmite\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t0\n")
f.write("10100\tVFX_DUR_CHAT_BUBBLE\tD\t0\tvdr_chatbubble\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t0\n") f.write("10100\tVFX_DUR_CHAT_BUBBLE\tD\t0\tvdr_chatbubble\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t0\n")
f.write("10101\theadaccessory_ANTLERS_L\tD\t0\thfx_antlers_l\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t0\n") f.write("10101\thead_accessories_ANTLERS_L\tD\t0\thfx_antlers_l\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t0\n")
f.write("10125\tVFX_COM_HIT_MASSIVE_CRITICALS\tF\t0\t****\tvcm_hitmasscrit\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t0\n") f.write("10125\tVFX_COM_HIT_MASSIVE_CRITICALS\tF\t0\t****\tvcm_hitmasscrit\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t****\t0\n")
with open(os.path.join(tlk, "sow_tlk.tlk"), "wb") as f: with open(os.path.join(tlk, "sow_tlk.tlk"), "wb") as f:
f.write(b"TLK") f.write(b"TLK")
@@ -7163,7 +7163,7 @@ with open(os.path.join(tlk, "sow_tlk.tlk"), "wb") as f:
}`+"\n") }`+"\n")
writeFile(t, filepath.Join(root, target, "data", "visualeffects", "lock.json"), `{ writeFile(t, filepath.Join(root, target, "data", "visualeffects", "lock.json"), `{
"visualeffects:chatbubble": 10100, "visualeffects:chatbubble": 10100,
"visualeffects:headaccessory_antlers_l": 10101, "visualeffects:head_accessories_antlers_l": 10101,
"visualeffects:vcm_hitmasscrit": 10125, "visualeffects:vcm_hitmasscrit": 10125,
"visualeffects:com_hitnegative": 288, "visualeffects:com_hitnegative": 288,
"visualeffects:fnf_blinddeaf": 18 "visualeffects:fnf_blinddeaf": 18
@@ -7224,8 +7224,8 @@ with open(os.path.join(tlk, "sow_tlk.tlk"), "wb") as f:
}`+"\n") }`+"\n")
writeFile(t, filepath.Join(root, target, "data", "visualeffects", "modules", "accessories", "add_eos.json"), `{ writeFile(t, filepath.Join(root, target, "data", "visualeffects", "modules", "accessories", "add_eos.json"), `{
"entries": { "entries": {
"visualeffects:headaccessory_antlers_l": { "visualeffects:head_accessories_antlers_l": {
"Label": "headaccessory_ANTLERS_L", "Label": "head_accessories_ANTLERS_L",
"Type_FD": "D", "Type_FD": "D",
"OrientWithGround": "0", "OrientWithGround": "0",
"Imp_HeadCon_Node": "hfx_antlers_l", "Imp_HeadCon_Node": "hfx_antlers_l",
@@ -7328,12 +7328,12 @@ func TestApplyAutogenConsumersAugmentsPartsFromLocalOverride(t *testing.T) {
func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *testing.T) { func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
overrideRoot := filepath.Join(root, "autogen-assets") overrideRoot := filepath.Join(root, "autogen-assets")
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "headaccessory", "hat")) mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "head_accessories", "hat"))
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "headdecoration", "laurel")) mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "head_decorations", "laurel"))
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "headfeature", "hair")) mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "head_features", "hair"))
writeFile(t, filepath.Join(overrideRoot, "vfxs", "headaccessory", "hat", "hfx_bandana.mdl"), "mdl\n") writeFile(t, filepath.Join(overrideRoot, "vfxs", "head_accessories", "hat", "hfx_bandana.mdl"), "mdl\n")
writeFile(t, filepath.Join(overrideRoot, "vfxs", "headdecoration", "laurel", "hfx_laurel.mdl"), "mdl\n") writeFile(t, filepath.Join(overrideRoot, "vfxs", "head_decorations", "laurel", "hfx_laurel.mdl"), "mdl\n")
writeFile(t, filepath.Join(overrideRoot, "vfxs", "headfeature", "hair", "hfx_hair_bangs.mdl"), "mdl\n") writeFile(t, filepath.Join(overrideRoot, "vfxs", "head_features", "hair", "hfx_hair_bangs.mdl"), "mdl\n")
p := testProject(root) p := testProject(root)
p.Config.Autogen.Consumers = []project.AutogenConsumerConfig{ p.Config.Autogen.Consumers = []project.AutogenConsumerConfig{
@@ -7343,7 +7343,7 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
Dataset: "visualeffects", Dataset: "visualeffects",
Mode: "head_visualeffects", Mode: "head_visualeffects",
Root: "vfxs", Root: "vfxs",
Include: []string{"headaccessory/**/*.mdl", "headdecoration/**/*.mdl", "headfeature/**/*.mdl"}, Include: []string{"head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.mdl"},
Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
Manifest: project.AutogenManifestConfig{ReleaseTag: "head-vfx-manifest-current", AssetName: "sow-head-vfx-manifest.json", CacheName: "sow-head-vfx-manifest.json"}, Manifest: project.AutogenManifestConfig{ReleaseTag: "head-vfx-manifest-current", AssetName: "sow-head-vfx-manifest.json", CacheName: "sow-head-vfx-manifest.json"},
LocalOverrideRoot: "autogen-assets", LocalOverrideRoot: "autogen-assets",
@@ -7374,37 +7374,37 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
rowByKey[row["key"].(string)] = row rowByKey[row["key"].(string)] = row
} }
bandana := rowByKey["visualeffects:headaccessory/hat/bandana"] bandana := rowByKey["visualeffects:head_accessories/hat/bandana"]
if bandana == nil { if bandana == nil {
t.Fatalf("expected head accessory row, got %#v", got[0].Rows) t.Fatalf("expected head accessory row, got %#v", got[0].Rows)
} }
if bandana["Label"] != "headaccessory/hat/bandana" || bandana["Imp_HeadCon_Node"] != "hfx_bandana" || bandana["OrientWithObject"] != "1" { if bandana["Label"] != "head_accessories/hat/bandana" || bandana["Imp_HeadCon_Node"] != "hfx_bandana" || bandana["OrientWithObject"] != "1" {
t.Fatalf("unexpected head accessory defaults: %#v", bandana) t.Fatalf("unexpected head accessory defaults: %#v", bandana)
} }
laurel := rowByKey["visualeffects:headdecoration/laurel/laurel"] laurel := rowByKey["visualeffects:head_decorations/laurel/laurel"]
if laurel == nil { if laurel == nil {
t.Fatalf("expected head decoration row, got %#v", got[0].Rows) t.Fatalf("expected head decoration row, got %#v", got[0].Rows)
} }
if laurel["Label"] != "headdecoration/laurel/laurel" || laurel["Imp_HeadCon_Node"] != "hfx_laurel" || laurel["Type_FD"] != "D" { if laurel["Label"] != "head_decorations/laurel/laurel" || laurel["Imp_HeadCon_Node"] != "hfx_laurel" || laurel["Type_FD"] != "D" {
t.Fatalf("unexpected head decoration defaults: %#v", laurel) t.Fatalf("unexpected head decoration defaults: %#v", laurel)
} }
hair := rowByKey["visualeffects:headfeature/hair/hair_bangs"] hair := rowByKey["visualeffects:head_features/hair/hair_bangs"]
if hair == nil { if hair == nil {
t.Fatalf("expected head feature row, got %#v", got[0].Rows) t.Fatalf("expected head feature row, got %#v", got[0].Rows)
} }
if hair["Label"] != "headfeature/hair/hair_bangs" || hair["Imp_HeadCon_Node"] != "hfx_hair_bangs" || hair["Type_FD"] != "D" { if hair["Label"] != "head_features/hair/hair_bangs" || hair["Imp_HeadCon_Node"] != "hfx_hair_bangs" || hair["Type_FD"] != "D" {
t.Fatalf("unexpected head feature defaults: %#v", hair) t.Fatalf("unexpected head feature defaults: %#v", hair)
} }
if _, ok := got[0].LockData["visualeffects:headaccessory/hat/bandana"]; !ok { if _, ok := got[0].LockData["visualeffects:head_accessories/hat/bandana"]; !ok {
t.Fatalf("expected head accessory lock id, got %#v", got[0].LockData) t.Fatalf("expected head accessory lock id, got %#v", got[0].LockData)
} }
if _, ok := got[0].LockData["visualeffects:headdecoration/laurel/laurel"]; !ok { if _, ok := got[0].LockData["visualeffects:head_decorations/laurel/laurel"]; !ok {
t.Fatalf("expected head decoration lock id, got %#v", got[0].LockData) t.Fatalf("expected head decoration lock id, got %#v", got[0].LockData)
} }
if _, ok := got[0].LockData["visualeffects:headfeature/hair/hair_bangs"]; !ok { if _, ok := got[0].LockData["visualeffects:head_features/hair/hair_bangs"]; !ok {
t.Fatalf("expected head feature lock id, got %#v", got[0].LockData) t.Fatalf("expected head feature lock id, got %#v", got[0].LockData)
} }
} }
@@ -7412,8 +7412,8 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *testing.T) { func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
overrideRoot := filepath.Join(root, "autogen-assets") overrideRoot := filepath.Join(root, "autogen-assets")
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "headfeature", "sinfar", "ears_plt")) mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "head_features", "sinfar", "ears_plt"))
writeFile(t, filepath.Join(overrideRoot, "vfxs", "headfeature", "sinfar", "ears_plt", "hfx_Ear_EL_L.mdl"), "mdl\n") writeFile(t, filepath.Join(overrideRoot, "vfxs", "head_features", "sinfar", "ears_plt", "hfx_Ear_EL_L.mdl"), "mdl\n")
p := testProject(root) p := testProject(root)
p.Config.Autogen.Consumers = []project.AutogenConsumerConfig{ p.Config.Autogen.Consumers = []project.AutogenConsumerConfig{
@@ -7423,13 +7423,13 @@ func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *tes
Dataset: "visualeffects", Dataset: "visualeffects",
Mode: "head_visualeffects", Mode: "head_visualeffects",
Root: "vfxs", Root: "vfxs",
Include: []string{"headfeature/**/*.mdl"}, Include: []string{"head_features/**/*.mdl"},
Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
Manifest: project.AutogenManifestConfig{ReleaseTag: "head-vfx-manifest-current", AssetName: "sow-head-vfx-manifest.json", CacheName: "sow-head-vfx-manifest.json"}, Manifest: project.AutogenManifestConfig{ReleaseTag: "head-vfx-manifest-current", AssetName: "sow-head-vfx-manifest.json", CacheName: "sow-head-vfx-manifest.json"},
LocalOverrideRoot: "autogen-assets", LocalOverrideRoot: "autogen-assets",
HeadVisualeffects: project.HeadVisualeffectsConfig{ HeadVisualeffects: project.HeadVisualeffectsConfig{
Groups: map[string]project.HeadVisualeffectGroupConfig{ Groups: map[string]project.HeadVisualeffectGroupConfig{
"headfeature": { "head_features": {
ModelColumns: []string{"Imp_HeadCon_Node", "Imp_Root_H_Node"}, ModelColumns: []string{"Imp_HeadCon_Node", "Imp_Root_H_Node"},
}, },
}, },
@@ -7455,7 +7455,7 @@ func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *tes
Columns: []string{"Label", "Type_FD", "OrientWithGround", "Imp_HeadCon_Node", "Imp_Root_H_Node", "OrientWithObject"}, Columns: []string{"Label", "Type_FD", "OrientWithGround", "Imp_HeadCon_Node", "Imp_Root_H_Node", "OrientWithObject"},
Rows: nil, Rows: nil,
LockData: map[string]int{ LockData: map[string]int{
"visualeffects:headfeature/ears_plt/Ear_EL_L": 11030, "visualeffects:head_features/ears_plt/Ear_EL_L": 11030,
}, },
}, },
} }
@@ -7468,13 +7468,13 @@ func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *tes
t.Fatalf("expected one autogenerated visualeffects row, got %#v", got) t.Fatalf("expected one autogenerated visualeffects row, got %#v", got)
} }
row := got[0].Rows[0] row := got[0].Rows[0]
if row["key"] != "visualeffects:headfeature/ears_plt/Ear_EL_L" { if row["key"] != "visualeffects:head_features/ears_plt/Ear_EL_L" {
t.Fatalf("expected folder-driven key, got %#v", row) t.Fatalf("expected folder-driven key, got %#v", row)
} }
if row["id"] != 11030 { if row["id"] != 11030 {
t.Fatalf("expected existing lock id to be preserved, got %#v", row) t.Fatalf("expected existing lock id to be preserved, got %#v", row)
} }
if row["Label"] != "headfeature/ears_plt/Ear_EL_L" { if row["Label"] != "head_features/ears_plt/Ear_EL_L" {
t.Fatalf("expected preserve-case slash label, got %#v", row) t.Fatalf("expected preserve-case slash label, got %#v", row)
} }
if row["Imp_HeadCon_Node"] != "hfx_Ear_EL_L" || row["Imp_Root_H_Node"] != "hfx_Ear_EL_L" { if row["Imp_HeadCon_Node"] != "hfx_Ear_EL_L" || row["Imp_Root_H_Node"] != "hfx_Ear_EL_L" {
@@ -7487,7 +7487,7 @@ func TestBuildNativeUsesConfiguredHeadVisualeffectsGroups(t *testing.T) {
mkdirAll(t, filepath.Join(root, "src")) mkdirAll(t, filepath.Join(root, "src"))
mkdirAll(t, filepath.Join(root, "build")) mkdirAll(t, filepath.Join(root, "build"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "visualeffects")) mkdirAll(t, filepath.Join(root, "topdata", "data", "visualeffects"))
mkdirAll(t, filepath.Join(root, "autogen-assets", "vfxs", "chestaccessory")) mkdirAll(t, filepath.Join(root, "autogen-assets", "vfxs", "chest_accessories"))
mkdirAll(t, filepath.Join(root, "autogen-assets", "vfxs", "head_jewels")) mkdirAll(t, filepath.Join(root, "autogen-assets", "vfxs", "head_jewels"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n") writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "visualeffects", "base.json"), `{ writeFile(t, filepath.Join(root, "topdata", "data", "visualeffects", "base.json"), `{
@@ -7495,7 +7495,7 @@ func TestBuildNativeUsesConfiguredHeadVisualeffectsGroups(t *testing.T) {
"columns": ["Label", "Type_FD", "OrientWithGround", "Imp_HeadCon_Node", "Imp_Root_S_Node", "Imp_Root_M_Node", "Imp_Root_L_Node", "Imp_Root_H_Node", "OrientWithObject"], "columns": ["Label", "Type_FD", "OrientWithGround", "Imp_HeadCon_Node", "Imp_Root_S_Node", "Imp_Root_M_Node", "Imp_Root_L_Node", "Imp_Root_H_Node", "OrientWithObject"],
"rows": [] "rows": []
}`+"\n") }`+"\n")
writeFile(t, filepath.Join(root, "autogen-assets", "vfxs", "chestaccessory", "tfx_sash.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "autogen-assets", "vfxs", "chest_accessories", "tfx_sash.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "autogen-assets", "vfxs", "head_jewels", "hfx_circlet.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "autogen-assets", "vfxs", "head_jewels", "hfx_circlet.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "nwn-tool.yaml"), ` writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module: module:
@@ -7515,7 +7515,7 @@ autogen:
mode: head_visualeffects mode: head_visualeffects
root: vfxs root: vfxs
include: include:
- chestaccessory/**/*.mdl - chest_accessories/**/*.mdl
- head_jewels/**/*.mdl - head_jewels/**/*.mdl
derive: derive:
kind: model_stem kind: model_stem
@@ -7527,8 +7527,8 @@ autogen:
local_override_root: autogen-assets local_override_root: autogen-assets
head_visualeffects: head_visualeffects:
groups: groups:
chestaccessory: chest_accessories:
prefix: chestaccessory prefix: chest_accessories
model_columns: model_columns:
- Imp_Root_S_Node - Imp_Root_S_Node
- Imp_Root_M_Node - Imp_Root_M_Node
@@ -7561,7 +7561,7 @@ autogen:
} }
text := string(visualeffectsBytes) text := string(visualeffectsBytes)
for _, want := range []string{ for _, want := range []string{
"0\tchestaccessory_sash\tF\t0\t****\ttfx_sash\ttfx_sash\ttfx_sash\ttfx_sash\t0", "0\tchest_accessories_sash\tF\t0\t****\ttfx_sash\ttfx_sash\ttfx_sash\ttfx_sash\t0",
"1\theadjewel_circlet\tF\t0\thfx_circlet\t****\t****\t****\t****\t0", "1\theadjewel_circlet\tF\t0\thfx_circlet\t****\t****\t****\t****\t0",
} { } {
if !strings.Contains(text, want) { if !strings.Contains(text, want) {
@@ -7572,16 +7572,16 @@ autogen:
func TestBuildGenerated2DAAssetsAppendsCachedModelsFromHeadVisualeffects(t *testing.T) { func TestBuildGenerated2DAAssetsAppendsCachedModelsFromHeadVisualeffects(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "chestaccessory")) mkdirAll(t, filepath.Join(root, "assets", "vfxs", "chest_accessories"))
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "headaccessory")) mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_accessories"))
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "headdecoration")) mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_decorations"))
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "headfeature", "hair")) mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "cachedmodels")) mkdirAll(t, filepath.Join(root, "topdata", "data", "cachedmodels"))
writeFile(t, filepath.Join(root, "assets", "vfxs", "chestaccessory", "tfx_sash.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "chest_accessories", "tfx_sash.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "assets", "vfxs", "headaccessory", "hfx_bandana.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "head_accessories", "hfx_bandana.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "assets", "vfxs", "headaccessory", "hfx_existing.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "head_accessories", "hfx_existing.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "assets", "vfxs", "headdecoration", "hfx_laurel.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "head_decorations", "hfx_laurel.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "assets", "vfxs", "headfeature", "hair", "hfx_hair_bangs.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair", "hfx_hair_bangs.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "topdata", "data", "cachedmodels", "base.json"), `{ writeFile(t, filepath.Join(root, "topdata", "data", "cachedmodels", "base.json"), `{
"columns": ["Model"], "columns": ["Model"],
"rows": [ "rows": [
@@ -7603,7 +7603,7 @@ func TestBuildGenerated2DAAssetsAppendsCachedModelsFromHeadVisualeffects(t *test
ID: "cachedmodels", ID: "cachedmodels",
Mode: "cachedmodels_rows", Mode: "cachedmodels_rows",
Root: "vfxs", Root: "vfxs",
Include: []string{"chestaccessory/**/*.mdl", "headaccessory/**/*.mdl", "headdecoration/**/*.mdl", "headfeature/**/*.mdl"}, Include: []string{"chest_accessories/**/*.mdl", "head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.mdl"},
Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
}, },
}, },
@@ -12808,10 +12808,10 @@ func TestBuildNativeWithReleasedHeadVisualeffectsManifest(t *testing.T) {
"generated_at": "2026-04-25T00:00:00Z", "generated_at": "2026-04-25T00:00:00Z",
"head_visualeffects": { "head_visualeffects": {
"groups": { "groups": {
"headaccessory": { "head_accessories": {
"model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"] "model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"]
}, },
"headfeature": { "head_features": {
"model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"] "model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"]
} }
}, },
@@ -12830,14 +12830,14 @@ func TestBuildNativeWithReleasedHeadVisualeffectsManifest(t *testing.T) {
}, },
"entries": [ "entries": [
{ {
"group": "headaccessory", "group": "head_accessories",
"model_stem": "hfx_bandana", "model_stem": "hfx_bandana",
"source": "headaccessory/hfx_bandana.mdl" "source": "head_accessories/hfx_bandana.mdl"
}, },
{ {
"group": "headfeature", "group": "head_features",
"model_stem": "hfx_hair_bangs", "model_stem": "hfx_hair_bangs",
"source": "headfeature/hair/hfx_hair_bangs.mdl" "source": "head_features/hair/hfx_hair_bangs.mdl"
} }
] ]
}` + "\n" }` + "\n"
@@ -12877,7 +12877,7 @@ func TestBuildNativeWithReleasedHeadVisualeffectsManifest(t *testing.T) {
Dataset: "visualeffects", Dataset: "visualeffects",
Mode: "head_visualeffects", Mode: "head_visualeffects",
Root: "vfxs", Root: "vfxs",
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"}, Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Derive: project.AutogenDeriveConfig{ Derive: project.AutogenDeriveConfig{
Kind: "model_stem", Kind: "model_stem",
GroupFrom: "first_path_segment", GroupFrom: "first_path_segment",
@@ -12904,8 +12904,8 @@ func TestBuildNativeWithReleasedHeadVisualeffectsManifest(t *testing.T) {
} }
text := string(visualeffectsBytes) text := string(visualeffectsBytes)
for _, want := range []string{ for _, want := range []string{
"0\theadaccessory/bandana\tD\t0\thfx_bandana\thfx_bandana\t1", "0\thead_accessories/bandana\tD\t0\thfx_bandana\thfx_bandana\t1",
"1\theadfeature/hair/hair_bangs\tD\t0\thfx_hair_bangs\thfx_hair_bangs\t1", "1\thead_features/hair/hair_bangs\tD\t0\thfx_hair_bangs\thfx_hair_bangs\t1",
"17\tEXISTING\tF\t0\t****\t****\t0", "17\tEXISTING\tF\t0\t****\t****\t0",
} { } {
if !strings.Contains(text, want) { if !strings.Contains(text, want) {
@@ -12932,7 +12932,7 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
}`+"\n") }`+"\n")
writeFile(t, filepath.Join(projRoot, "topdata", "data", "visualeffects", "lock.json"), `{ writeFile(t, filepath.Join(projRoot, "topdata", "data", "visualeffects", "lock.json"), `{
"visualeffects:existing": 17, "visualeffects:existing": 17,
"visualeffects:headaccessory/bandana": 42 "visualeffects:head_accessories/bandana": 42
}`+"\n") }`+"\n")
mkdirAll(t, filepath.Join(projRoot, "reference")) mkdirAll(t, filepath.Join(projRoot, "reference"))
writeFile(t, filepath.Join(projRoot, "reference", "build.py"), "print('ok')\n") writeFile(t, filepath.Join(projRoot, "reference", "build.py"), "print('ok')\n")
@@ -12943,9 +12943,9 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
"generated_at": "2026-04-25T00:00:00Z", "generated_at": "2026-04-25T00:00:00Z",
"entries": [ "entries": [
{ {
"group": "headfeature", "group": "head_features",
"model_stem": "hfx_hair_bangs", "model_stem": "hfx_hair_bangs",
"source": "headfeature/hair/hfx_hair_bangs.mdl" "source": "head_features/hair/hfx_hair_bangs.mdl"
} }
] ]
}`+"\n") }`+"\n")
@@ -12959,9 +12959,9 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
"generated_at": "2026-04-25T12:00:00Z", "generated_at": "2026-04-25T12:00:00Z",
"entries": [ "entries": [
{ {
"group": "headaccessory", "group": "head_accessories",
"model_stem": "hfx_bandana", "model_stem": "hfx_bandana",
"source": "headaccessory/hfx_bandana.mdl" "source": "head_accessories/hfx_bandana.mdl"
} }
] ]
}` + "\n" }` + "\n"
@@ -13001,7 +13001,7 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
Dataset: "visualeffects", Dataset: "visualeffects",
Mode: "head_visualeffects", Mode: "head_visualeffects",
Root: "vfxs", Root: "vfxs",
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"}, Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Derive: project.AutogenDeriveConfig{ Derive: project.AutogenDeriveConfig{
Kind: "model_stem", Kind: "model_stem",
GroupFrom: "first_path_segment", GroupFrom: "first_path_segment",
@@ -13027,7 +13027,7 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
t.Fatalf("read visualeffects.2da: %v", err) t.Fatalf("read visualeffects.2da: %v", err)
} }
text := string(visualeffectsBytes) text := string(visualeffectsBytes)
if !strings.Contains(text, "42\theadaccessory/bandana\tD\t0\thfx_bandana\t1") { if !strings.Contains(text, "42\thead_accessories/bandana\tD\t0\thfx_bandana\t1") {
t.Fatalf("expected remote manifest entry with preserved lock id, got:\n%s", text) t.Fatalf("expected remote manifest entry with preserved lock id, got:\n%s", text)
} }
if strings.Contains(text, "HAIR_BANGS") { if strings.Contains(text, "HAIR_BANGS") {
@@ -13052,7 +13052,7 @@ func TestBuildPackageInvalidatesFreshAutogenCacheWhenReleaseAssetIsNewer(t *test
setCompiledOutputTimes(t, result, outputTime) setCompiledOutputTimes(t, result, outputTime)
cachePath := filepath.Join(root, ".cache", "sow-head-vfx-manifest.json") cachePath := filepath.Join(root, ".cache", "sow-head-vfx-manifest.json")
writeFile(t, cachePath, `{"id":"head_visualeffects","generated_at":"2026-04-25T10:00:00Z","entries":[{"group":"headaccessory","model_stem":"hfx_bandana","source":"headaccessory/hfx_bandana.mdl"}]}`+"\n") writeFile(t, cachePath, `{"id":"head_visualeffects","generated_at":"2026-04-25T10:00:00Z","entries":[{"group":"head_accessories","model_stem":"hfx_bandana","source":"head_accessories/hfx_bandana.mdl"}]}`+"\n")
setFileTime(t, cachePath, time.Now()) setFileTime(t, cachePath, time.Now())
var server *httptest.Server var server *httptest.Server
@@ -13063,7 +13063,7 @@ func TestBuildPackageInvalidatesFreshAutogenCacheWhenReleaseAssetIsNewer(t *test
_, _ = w.Write([]byte(`{"assets":[{"name":"sow-head-vfx-manifest.json","updated_at":"3026-04-25T12:00:00Z","browser_download_url":"` + server.URL + `/downloads/sow-head-vfx-manifest.json"}]}`)) _, _ = w.Write([]byte(`{"assets":[{"name":"sow-head-vfx-manifest.json","updated_at":"3026-04-25T12:00:00Z","browser_download_url":"` + server.URL + `/downloads/sow-head-vfx-manifest.json"}]}`))
case "/downloads/sow-head-vfx-manifest.json": case "/downloads/sow-head-vfx-manifest.json":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"head_visualeffects","entries":[{"group":"headaccessory","model_stem":"hfx_bandana","source":"headaccessory/hfx_bandana.mdl"}]}`)) _, _ = w.Write([]byte(`{"id":"head_visualeffects","entries":[{"group":"head_accessories","model_stem":"hfx_bandana","source":"head_accessories/hfx_bandana.mdl"}]}`))
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }
@@ -13081,7 +13081,7 @@ func TestBuildPackageInvalidatesFreshAutogenCacheWhenReleaseAssetIsNewer(t *test
Mode: "head_visualeffects", Mode: "head_visualeffects",
Optional: true, Optional: true,
Root: "vfxs", Root: "vfxs",
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"}, Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Derive: project.AutogenDeriveConfig{ Derive: project.AutogenDeriveConfig{
Kind: "model_stem", Kind: "model_stem",
GroupFrom: "first_path_segment", GroupFrom: "first_path_segment",
@@ -13406,7 +13406,7 @@ func TestBuildPackageFailsWhenAutogenManifestCacheIsStale(t *testing.T) {
Mode: "head_visualeffects", Mode: "head_visualeffects",
Optional: true, Optional: true,
Root: "vfxs", Root: "vfxs",
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"}, Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Derive: project.AutogenDeriveConfig{ Derive: project.AutogenDeriveConfig{
Kind: "model_stem", Kind: "model_stem",
GroupFrom: "first_path_segment", GroupFrom: "first_path_segment",
@@ -13419,7 +13419,7 @@ func TestBuildPackageFailsWhenAutogenManifestCacheIsStale(t *testing.T) {
}, },
} }
cachePath := filepath.Join(root, ".cache", "sow-head-vfx-manifest.json") cachePath := filepath.Join(root, ".cache", "sow-head-vfx-manifest.json")
writeFile(t, cachePath, `{"id":"head_visualeffects","entries":[{"group":"headaccessory","model_stem":"hfx_bandana","source":"headaccessory/hfx_bandana.mdl"}]}`+"\n") writeFile(t, cachePath, `{"id":"head_visualeffects","entries":[{"group":"head_accessories","model_stem":"hfx_bandana","source":"head_accessories/hfx_bandana.mdl"}]}`+"\n")
setFileTime(t, cachePath, time.Now().Add(-2*autogenManifestCacheMaxAge)) setFileTime(t, cachePath, time.Now().Add(-2*autogenManifestCacheMaxAge))
if _, err := BuildPackage(proj, nil); err == nil || !strings.Contains(err.Error(), "older than source file") { if _, err := BuildPackage(proj, nil); err == nil || !strings.Contains(err.Error(), "older than source file") {
@@ -14354,7 +14354,7 @@ func testProject(root string) *project.Project {
Mode: "head_visualeffects", Mode: "head_visualeffects",
Optional: true, Optional: true,
Root: "vfxs", Root: "vfxs",
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"}, Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Derive: project.AutogenDeriveConfig{ Derive: project.AutogenDeriveConfig{
Kind: "model_stem", Kind: "model_stem",
GroupFrom: "first_path_segment", GroupFrom: "first_path_segment",