Autogen categories

This commit is contained in:
2026-05-31 14:25:28 +02:00
parent aa47532ef1
commit c57b2e21a2
5 changed files with 103 additions and 222 deletions
+46 -109
View File
@@ -7328,12 +7328,12 @@ func TestApplyAutogenConsumersAugmentsPartsFromLocalOverride(t *testing.T) {
func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *testing.T) {
root := testProjectRoot(t)
overrideRoot := filepath.Join(root, "autogen-assets")
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "head_accessories"))
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "head_decorations"))
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "head_features", "hair"))
writeFile(t, filepath.Join(overrideRoot, "vfxs", "head_accessories", "hfx_bandana.mdl"), "mdl\n")
writeFile(t, filepath.Join(overrideRoot, "vfxs", "head_decorations", "hfx_laurel.mdl"), "mdl\n")
writeFile(t, filepath.Join(overrideRoot, "vfxs", "head_features", "hair", "hfx_hair_bangs.mdl"), "mdl\n")
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "headaccessory", "hat"))
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "headdecoration", "laurel"))
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "headfeature", "hair"))
writeFile(t, filepath.Join(overrideRoot, "vfxs", "headaccessory", "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", "headfeature", "hair", "hfx_hair_bangs.mdl"), "mdl\n")
p := testProject(root)
p.Config.Autogen.Consumers = []project.AutogenConsumerConfig{
@@ -7343,7 +7343,7 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
Dataset: "visualeffects",
Mode: "head_visualeffects",
Root: "vfxs",
Include: []string{"head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.mdl"},
Include: []string{"headaccessory/**/*.mdl", "headdecoration/**/*.mdl", "headfeature/**/*.mdl"},
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"},
LocalOverrideRoot: "autogen-assets",
@@ -7374,37 +7374,37 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
rowByKey[row["key"].(string)] = row
}
bandana := rowByKey["visualeffects:headaccessory_bandana"]
bandana := rowByKey["visualeffects:headaccessory/hat/bandana"]
if bandana == nil {
t.Fatalf("expected head accessory row, got %#v", got[0].Rows)
}
if bandana["Label"] != "headaccessory_BANDANA" || bandana["Imp_HeadCon_Node"] != "hfx_bandana" || bandana["OrientWithObject"] != "1" {
if bandana["Label"] != "headaccessory/hat/bandana" || bandana["Imp_HeadCon_Node"] != "hfx_bandana" || bandana["OrientWithObject"] != "1" {
t.Fatalf("unexpected head accessory defaults: %#v", bandana)
}
laurel := rowByKey["visualeffects:headdecoration_laurel"]
laurel := rowByKey["visualeffects:headdecoration/laurel/laurel"]
if laurel == nil {
t.Fatalf("expected head decoration row, got %#v", got[0].Rows)
}
if laurel["Label"] != "headdecoration_LAUREL" || laurel["Imp_HeadCon_Node"] != "hfx_laurel" || laurel["Type_FD"] != "D" {
if laurel["Label"] != "headdecoration/laurel/laurel" || laurel["Imp_HeadCon_Node"] != "hfx_laurel" || laurel["Type_FD"] != "D" {
t.Fatalf("unexpected head decoration defaults: %#v", laurel)
}
hair := rowByKey["visualeffects:headfeature_hair_bangs"]
hair := rowByKey["visualeffects:headfeature/hair/hair_bangs"]
if hair == nil {
t.Fatalf("expected head feature row, got %#v", got[0].Rows)
}
if hair["Label"] != "headfeature_HAIR_BANGS" || hair["Imp_HeadCon_Node"] != "hfx_hair_bangs" || hair["Type_FD"] != "D" {
if hair["Label"] != "headfeature/hair/hair_bangs" || hair["Imp_HeadCon_Node"] != "hfx_hair_bangs" || hair["Type_FD"] != "D" {
t.Fatalf("unexpected head feature defaults: %#v", hair)
}
if _, ok := got[0].LockData["visualeffects:headaccessory_bandana"]; !ok {
if _, ok := got[0].LockData["visualeffects:headaccessory/hat/bandana"]; !ok {
t.Fatalf("expected head accessory lock id, got %#v", got[0].LockData)
}
if _, ok := got[0].LockData["visualeffects:headdecoration_laurel"]; !ok {
if _, ok := got[0].LockData["visualeffects:headdecoration/laurel/laurel"]; !ok {
t.Fatalf("expected head decoration lock id, got %#v", got[0].LockData)
}
if _, ok := got[0].LockData["visualeffects:headfeature_hair_bangs"]; !ok {
if _, ok := got[0].LockData["visualeffects:headfeature/hair/hair_bangs"]; !ok {
t.Fatalf("expected head feature lock id, got %#v", got[0].LockData)
}
}
@@ -7440,7 +7440,6 @@ func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *tes
StripModelPrefixes: []string{"hfx_"},
KeyFormat: "{dataset}:{group}{delimiter}{category_segment}{stem}",
LabelFormat: "{group}{delimiter}{category_segment}{stem}",
LegacyKeyFormat: "{dataset}:{group}_{stem}",
RowDefaults: map[string]string{
"Type_FD": "D",
"OrientWithGround": "0",
@@ -7456,7 +7455,7 @@ func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *tes
Columns: []string{"Label", "Type_FD", "OrientWithGround", "Imp_HeadCon_Node", "Imp_Root_H_Node", "OrientWithObject"},
Rows: nil,
LockData: map[string]int{
"visualeffects:headfeature_Ear_EL_L": 11030,
"visualeffects:headfeature/ears_plt/Ear_EL_L": 11030,
},
},
}
@@ -7473,7 +7472,7 @@ func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *tes
t.Fatalf("expected folder-driven key, got %#v", row)
}
if row["id"] != 11030 {
t.Fatalf("expected legacy 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" {
t.Fatalf("expected preserve-case slash label, got %#v", row)
@@ -7483,70 +7482,6 @@ func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *tes
}
}
func TestHeadVisualeffectsConsumerMapsLegacyManifestGroupsToFolderDrivenNames(t *testing.T) {
collected := []nativeCollectedDataset{
{
Dataset: nativeDataset{Name: "visualeffects", Kind: nativeDatasetBase},
Columns: []string{"Label", "Type_FD", "OrientWithGround", "Imp_HeadCon_Node", "Imp_Root_H_Node", "OrientWithObject"},
Rows: nil,
LockData: map[string]int{
"visualeffects:headfeature_ear_el_l": 11030,
},
},
}
entries := []autogenManifestEntry{
{
Group: "head_features",
Source: "head_features/sinfar/ears_plt/hfx_ear_el_l.mdl",
ModelStem: "hfx_ear_el_l",
},
}
consumer := project.AutogenConsumerConfig{
Dataset: "visualeffects",
HeadVisualeffects: project.HeadVisualeffectsConfig{
Groups: map[string]project.HeadVisualeffectGroupConfig{
"headfeature": {
ModelColumns: []string{"Imp_HeadCon_Node", "Imp_Root_H_Node"},
},
},
GroupTokenSource: "folder_name",
CategoryFrom: "immediate_parent",
Delimiter: "/",
Case: "preserve",
StripModelPrefixes: []string{
"hfx_",
},
KeyFormat: "{dataset}:{group}{delimiter}{category_segment}{stem}",
LabelFormat: "{group}{delimiter}{category_segment}{stem}",
LegacyGroups: map[string]string{"head_features": "headfeature"},
LegacyKeyFormat: "{dataset}:{group}_{stem}",
RowDefaults: map[string]string{
"Type_FD": "D",
"OrientWithGround": "0",
"OrientWithObject": "1",
},
},
}
got, err := augmentWithAutogeneratedHeadVisualeffects(collected, entries, consumer, nil)
if err != nil {
t.Fatalf("augmentWithAutogeneratedHeadVisualeffects failed: %v", err)
}
if len(got) != 1 || len(got[0].Rows) != 1 {
t.Fatalf("expected one autogenerated row, got %#v", got)
}
row := got[0].Rows[0]
if row["key"] != "visualeffects:headfeature/ears_plt/ear_el_l" || row["id"] != 11030 {
t.Fatalf("expected migrated slash key with preserved ID, got %#v", row)
}
if _, exists := got[0].LockData["visualeffects:headfeature/ears_plt/ear_el_l"]; !exists {
t.Fatalf("expected new slash key in lock data, got %#v", got[0].LockData)
}
if _, exists := got[0].LockData["visualeffects:headfeature_ear_el_l"]; exists {
t.Fatalf("expected legacy key to be removed from active lock data, got %#v", got[0].LockData)
}
}
func TestBuildNativeUsesConfiguredHeadVisualeffectsGroups(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src"))
@@ -12873,18 +12808,20 @@ func TestBuildNativeWithReleasedHeadVisualeffectsManifest(t *testing.T) {
"generated_at": "2026-04-25T00:00:00Z",
"head_visualeffects": {
"groups": {
"head_accessories": {
"prefix": "headaccessory",
"headaccessory": {
"model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"]
},
"head_features": {
"prefix": "headfeature",
"headfeature": {
"model_columns": ["Imp_HeadCon_Node", "Imp_Root_H_Node"]
}
},
"group_token_source": "folder_name",
"category_from": "immediate_parent",
"delimiter": "/",
"case": "preserve",
"strip_model_prefixes": ["hfx_"],
"key_format": "{dataset}:{prefix}_{stem}",
"label_format": "{prefix}_{stem_upper}",
"key_format": "{dataset}:{group}{delimiter}{category_segment}{stem}",
"label_format": "{group}{delimiter}{category_segment}{stem}",
"row_defaults": {
"Type_FD": "D",
"OrientWithGround": "0",
@@ -12893,14 +12830,14 @@ func TestBuildNativeWithReleasedHeadVisualeffectsManifest(t *testing.T) {
},
"entries": [
{
"group": "head_accessories",
"group": "headaccessory",
"model_stem": "hfx_bandana",
"source": "head_accessories/hfx_bandana.mdl"
"source": "headaccessory/hfx_bandana.mdl"
},
{
"group": "head_features",
"group": "headfeature",
"model_stem": "hfx_hair_bangs",
"source": "head_features/hair/hfx_hair_bangs.mdl"
"source": "headfeature/hair/hfx_hair_bangs.mdl"
}
]
}` + "\n"
@@ -12940,7 +12877,7 @@ func TestBuildNativeWithReleasedHeadVisualeffectsManifest(t *testing.T) {
Dataset: "visualeffects",
Mode: "head_visualeffects",
Root: "vfxs",
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"},
Derive: project.AutogenDeriveConfig{
Kind: "model_stem",
GroupFrom: "first_path_segment",
@@ -12967,8 +12904,8 @@ func TestBuildNativeWithReleasedHeadVisualeffectsManifest(t *testing.T) {
}
text := string(visualeffectsBytes)
for _, want := range []string{
"0\theadaccessory_BANDANA\tD\t0\thfx_bandana\thfx_bandana\t1",
"1\theadfeature_HAIR_BANGS\tD\t0\thfx_hair_bangs\thfx_hair_bangs\t1",
"0\theadaccessory/bandana\tD\t0\thfx_bandana\thfx_bandana\t1",
"1\theadfeature/hair/hair_bangs\tD\t0\thfx_hair_bangs\thfx_hair_bangs\t1",
"17\tEXISTING\tF\t0\t****\t****\t0",
} {
if !strings.Contains(text, want) {
@@ -12995,7 +12932,7 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
}`+"\n")
writeFile(t, filepath.Join(projRoot, "topdata", "data", "visualeffects", "lock.json"), `{
"visualeffects:existing": 17,
"visualeffects:headaccessory_bandana": 42
"visualeffects:headaccessory/bandana": 42
}`+"\n")
mkdirAll(t, filepath.Join(projRoot, "reference"))
writeFile(t, filepath.Join(projRoot, "reference", "build.py"), "print('ok')\n")
@@ -13006,9 +12943,9 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
"generated_at": "2026-04-25T00:00:00Z",
"entries": [
{
"group": "head_features",
"group": "headfeature",
"model_stem": "hfx_hair_bangs",
"source": "head_features/hair/hfx_hair_bangs.mdl"
"source": "headfeature/hair/hfx_hair_bangs.mdl"
}
]
}`+"\n")
@@ -13022,9 +12959,9 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
"generated_at": "2026-04-25T12:00:00Z",
"entries": [
{
"group": "head_accessories",
"group": "headaccessory",
"model_stem": "hfx_bandana",
"source": "head_accessories/hfx_bandana.mdl"
"source": "headaccessory/hfx_bandana.mdl"
}
]
}` + "\n"
@@ -13064,7 +13001,7 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
Dataset: "visualeffects",
Mode: "head_visualeffects",
Root: "vfxs",
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"},
Derive: project.AutogenDeriveConfig{
Kind: "model_stem",
GroupFrom: "first_path_segment",
@@ -13090,7 +13027,7 @@ func TestBuildNativePrefersRemoteReleasedHeadVisualeffectsManifestOverFreshCache
t.Fatalf("read visualeffects.2da: %v", err)
}
text := string(visualeffectsBytes)
if !strings.Contains(text, "42\theadaccessory_BANDANA\tD\t0\thfx_bandana\t1") {
if !strings.Contains(text, "42\theadaccessory/bandana\tD\t0\thfx_bandana\t1") {
t.Fatalf("expected remote manifest entry with preserved lock id, got:\n%s", text)
}
if strings.Contains(text, "HAIR_BANGS") {
@@ -13115,7 +13052,7 @@ func TestBuildPackageInvalidatesFreshAutogenCacheWhenReleaseAssetIsNewer(t *test
setCompiledOutputTimes(t, result, outputTime)
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":"head_accessories","model_stem":"hfx_bandana","source":"head_accessories/hfx_bandana.mdl"}]}`+"\n")
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")
setFileTime(t, cachePath, time.Now())
var server *httptest.Server
@@ -13126,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"}]}`))
case "/downloads/sow-head-vfx-manifest.json":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"head_visualeffects","entries":[{"group":"head_accessories","model_stem":"hfx_bandana","source":"head_accessories/hfx_bandana.mdl"}]}`))
_, _ = w.Write([]byte(`{"id":"head_visualeffects","entries":[{"group":"headaccessory","model_stem":"hfx_bandana","source":"headaccessory/hfx_bandana.mdl"}]}`))
default:
http.NotFound(w, r)
}
@@ -13144,7 +13081,7 @@ func TestBuildPackageInvalidatesFreshAutogenCacheWhenReleaseAssetIsNewer(t *test
Mode: "head_visualeffects",
Optional: true,
Root: "vfxs",
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"},
Derive: project.AutogenDeriveConfig{
Kind: "model_stem",
GroupFrom: "first_path_segment",
@@ -13469,7 +13406,7 @@ func TestBuildPackageFailsWhenAutogenManifestCacheIsStale(t *testing.T) {
Mode: "head_visualeffects",
Optional: true,
Root: "vfxs",
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"},
Derive: project.AutogenDeriveConfig{
Kind: "model_stem",
GroupFrom: "first_path_segment",
@@ -13482,7 +13419,7 @@ func TestBuildPackageFailsWhenAutogenManifestCacheIsStale(t *testing.T) {
},
}
cachePath := filepath.Join(root, ".cache", "sow-head-vfx-manifest.json")
writeFile(t, cachePath, `{"id":"head_visualeffects","entries":[{"group":"head_accessories","model_stem":"hfx_bandana","source":"head_accessories/hfx_bandana.mdl"}]}`+"\n")
writeFile(t, cachePath, `{"id":"head_visualeffects","entries":[{"group":"headaccessory","model_stem":"hfx_bandana","source":"headaccessory/hfx_bandana.mdl"}]}`+"\n")
setFileTime(t, cachePath, time.Now().Add(-2*autogenManifestCacheMaxAge))
if _, err := BuildPackage(proj, nil); err == nil || !strings.Contains(err.Error(), "older than source file") {
@@ -14417,7 +14354,7 @@ func testProject(root string) *project.Project {
Mode: "head_visualeffects",
Optional: true,
Root: "vfxs",
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"},
Derive: project.AutogenDeriveConfig{
Kind: "model_stem",
GroupFrom: "first_path_segment",