diff --git a/README.md b/README.md index 27c431c..a90b14f 100644 --- a/README.md +++ b/README.md @@ -419,11 +419,9 @@ mapping, model stem prefixes to strip, key/label formats, model column, and default row values. `group_token_source: folder_name` uses the manifest folder group directly instead of a prefix mapping, and `category_from: immediate_parent` can expose the model's category folder in keys and labels. -`legacy_groups` maps released manifest group names to current folder groups, -and `legacy_key_format` preserves row IDs when migrating older generated lock -keys. Individual groups can override the model column with `model_column` or -write the discovered model stem to several columns with `model_columns`, which -is useful for root-node visualeffects. Supported format tokens are `{dataset}`, +Individual groups can override the model column with `model_column` or write +the discovered model stem to several columns with `model_columns`, which is +useful for root-node visualeffects. Supported format tokens are `{dataset}`, `{group}`, `{group_raw}`, `{prefix}`, `{category}`, `{category_upper}`, `{category_segment}`, `{category_segment_upper}`, `{subgroup}`, `{stem}`, `{stem_upper}`, `{model_stem}`, and `{delimiter}`. diff --git a/internal/project/project.go b/internal/project/project.go index 7343324..023e38b 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -395,8 +395,6 @@ type HeadVisualeffectsConfig struct { StripModelPrefixes []string `json:"strip_model_prefixes,omitempty" yaml:"strip_model_prefixes"` KeyFormat string `json:"key_format,omitempty" yaml:"key_format"` LabelFormat string `json:"label_format,omitempty" yaml:"label_format"` - LegacyGroups map[string]string `json:"legacy_groups,omitempty" yaml:"legacy_groups"` - LegacyKeyFormat string `json:"legacy_key_format,omitempty" yaml:"legacy_key_format"` ModelColumn string `json:"model_column,omitempty" yaml:"model_column"` RowDefaults map[string]string `json:"row_defaults,omitempty" yaml:"row_defaults"` } @@ -1820,8 +1818,6 @@ func headVisualeffectsConfigConfigured(cfg HeadVisualeffectsConfig) bool { len(cfg.StripModelPrefixes) > 0 || strings.TrimSpace(cfg.KeyFormat) != "" || strings.TrimSpace(cfg.LabelFormat) != "" || - len(cfg.LegacyGroups) > 0 || - strings.TrimSpace(cfg.LegacyKeyFormat) != "" || strings.TrimSpace(cfg.ModelColumn) != "" || len(cfg.RowDefaults) > 0 } @@ -1888,21 +1884,6 @@ func validateHeadVisualeffectsConfig(fieldPrefix string, cfg HeadVisualeffectsCo } } } - for legacyGroup, group := range cfg.LegacyGroups { - legacyGroup = strings.TrimSpace(legacyGroup) - group = strings.TrimSpace(group) - if legacyGroup == "" { - failures = append(failures, fmt.Errorf("%s.legacy_groups contains an empty legacy group key", fieldPrefix)) - continue - } - if group == "" { - failures = append(failures, fmt.Errorf("%s.legacy_groups[%s] must not be empty", fieldPrefix, legacyGroup)) - } - failures = append(failures, validateTreeRootPath(fieldPrefix+".legacy_groups["+legacyGroup+"]", legacyGroup)...) - if group != "" { - failures = append(failures, validateTreeRootPath(fieldPrefix+".legacy_groups["+legacyGroup+"]", group)...) - } - } for index, prefix := range cfg.StripModelPrefixes { if strings.TrimSpace(prefix) == "" { failures = append(failures, fmt.Errorf("%s.strip_model_prefixes[%d] must not be empty", fieldPrefix, index)) diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 74e3ad2..288fec2 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -1449,9 +1449,6 @@ func TestValidateLayoutRejectsInvalidHeadVisualeffectsNamingConfig(t *testing.T) CategoryFrom: "unsupported", Delimiter: " ", Case: "unsupported", - LegacyGroups: map[string]string{ - "head_features": "", - }, }, }, }, @@ -1468,7 +1465,6 @@ func TestValidateLayoutRejectsInvalidHeadVisualeffectsNamingConfig(t *testing.T) "autogen.consumers[0].head_visualeffects.category_from", "autogen.consumers[0].head_visualeffects.delimiter", "autogen.consumers[0].head_visualeffects.case", - "autogen.consumers[0].head_visualeffects.legacy_groups[head_features]", } { if !strings.Contains(err.Error(), needle) { t.Fatalf("expected validation error to mention %s, got %v", needle, err) @@ -1476,6 +1472,47 @@ func TestValidateLayoutRejectsInvalidHeadVisualeffectsNamingConfig(t *testing.T) } } +func TestLoadRejectsLegacyHeadVisualeffectsNamingFields(t *testing.T) { + root := t.TempDir() + writeProjectFile(t, filepath.Join(root, ConfigFile), ` +module: + name: Test Module + resref: testmod +paths: + source: src +autogen: + consumers: + - id: head_visualeffects + producer: head_visualeffects + dataset: visualeffects + mode: head_visualeffects + root: vfxs + include: + - headfeature/**/*.mdl + derive: + kind: model_stem + group_from: first_path_segment + head_visualeffects: + groups: + headfeature: + model_columns: + - Imp_HeadCon_Node + group_token_source: folder_name + category_from: immediate_parent + legacy_groups: + head_features: headfeature + legacy_key_format: "{dataset}:{group}_{stem}" +`) + + _, err := Load(root) + if err == nil { + t.Fatal("expected legacy head visualeffects naming fields to be rejected") + } + if !strings.Contains(err.Error(), "legacy_groups") { + t.Fatalf("expected legacy_groups unknown field error, got %v", err) + } +} + func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "assets")) diff --git a/internal/topdata/autogen.go b/internal/topdata/autogen.go index 93bb549..fd7094e 100644 --- a/internal/topdata/autogen.go +++ b/internal/topdata/autogen.go @@ -742,25 +742,10 @@ func augmentWithAutogeneratedHeadVisualeffects(collected []nativeCollectedDatase rowID, ok := lockData[key] if !ok { - var migratedLegacyKey string if preservedRowID, preserved := historicalLockData[key]; preserved { rowID = preservedRowID } else { - preservedLegacyID := false - for _, legacyKey := range headVisualeffectLegacyKeys(dataset.Dataset.Name, entry, policy) { - if preservedRowID, preserved := historicalLockData[legacyKey]; preserved { - rowID = preservedRowID - preservedLegacyID = true - migratedLegacyKey = legacyKey - break - } - } - if !preservedLegacyID { - rowID = allocateNextID() - } - } - if migratedLegacyKey != "" && migratedLegacyKey != key { - delete(lockData, migratedLegacyKey) + rowID = allocateNextID() } lockData[key] = rowID } @@ -796,8 +781,6 @@ type headVisualeffectsPolicy struct { StripModelPrefixes []string KeyFormat string LabelFormat string - LegacyGroups map[string]string - LegacyKeyFormat string ModelColumn string RowDefaults map[string]string } @@ -858,22 +841,6 @@ func applyHeadVisualeffectsConfig(policy *headVisualeffectsPolicy, cfg project.H if strings.TrimSpace(cfg.LabelFormat) != "" { policy.LabelFormat = strings.TrimSpace(cfg.LabelFormat) } - if len(cfg.LegacyGroups) > 0 { - if policy.LegacyGroups == nil { - policy.LegacyGroups = map[string]string{} - } - for legacyGroup, group := range cfg.LegacyGroups { - legacyGroup = strings.TrimSpace(legacyGroup) - group = strings.TrimSpace(group) - if legacyGroup == "" || group == "" { - continue - } - policy.LegacyGroups[legacyGroup] = group - } - } - if strings.TrimSpace(cfg.LegacyKeyFormat) != "" { - policy.LegacyKeyFormat = strings.TrimSpace(cfg.LegacyKeyFormat) - } if strings.TrimSpace(cfg.ModelColumn) != "" { policy.ModelColumn = strings.TrimSpace(cfg.ModelColumn) } @@ -883,17 +850,18 @@ func applyHeadVisualeffectsConfig(policy *headVisualeffectsPolicy, cfg project.H func defaultHeadVisualeffectsPolicy() headVisualeffectsPolicy { return headVisualeffectsPolicy{ Groups: map[string]headVisualeffectsGroupPolicy{ - "head_accessories": {Prefix: "headaccessory"}, - "head_decorations": {Prefix: "headdecoration"}, - "head_features": {Prefix: "headfeature"}, + "chestaccessory": {}, + "headaccessory": {}, + "headdecoration": {}, + "headfeature": {}, }, - GroupTokenSource: "prefix", - CategoryFrom: "none", - Delimiter: "_", + GroupTokenSource: "folder_name", + CategoryFrom: "immediate_parent", + Delimiter: "/", Case: "preserve", - StripModelPrefixes: []string{"hfx_", "vfx_"}, - KeyFormat: "{dataset}:{prefix}_{stem}", - LabelFormat: "{prefix}_{stem_upper}", + StripModelPrefixes: []string{"hfx_", "tfx_", "vfx_", "cfx_"}, + KeyFormat: "{dataset}:{group}{delimiter}{category_segment}{stem}", + LabelFormat: "{group}{delimiter}{category_segment}{stem}", ModelColumn: "Imp_HeadCon_Node", RowDefaults: map[string]string{ "Type_FD": "D", @@ -939,7 +907,7 @@ func mergeStringMap(target map[string]string, source map[string]string) { } func headVisualeffectIdentity(dataset string, entry autogenManifestEntry, policy headVisualeffectsPolicy) (string, string, string, headVisualeffectsGroupPolicy, bool) { - entry = normalizeHeadVisualeffectEntry(entry, policy) + entry = normalizeHeadVisualeffectEntry(entry) group, ok := policy.Groups[entry.Group] if !ok || strings.TrimSpace(entry.ModelStem) == "" { return "", "", "", headVisualeffectsGroupPolicy{}, false @@ -975,50 +943,12 @@ func headVisualeffectIdentity(dataset string, entry autogenManifestEntry, policy return key, label, modelStem, group, true } -func headVisualeffectLegacyKeys(dataset string, entry autogenManifestEntry, policy headVisualeffectsPolicy) []string { - if strings.TrimSpace(policy.LegacyKeyFormat) == "" { - return nil - } - entry = normalizeHeadVisualeffectEntry(entry, policy) - group, ok := policy.Groups[entry.Group] - if !ok || strings.TrimSpace(entry.ModelStem) == "" { - return nil - } - modelStem := strings.TrimSpace(entry.ModelStem) - stem := strings.TrimSpace(stripHeadVisualeffectModelPrefix(modelStem, policy)) - if stem == "" { - return nil - } - groupToken := headVisualeffectGroupToken(entry.Group, group, policy) - values := map[string]string{ - "dataset": dataset, - "group": applyHeadVisualeffectCase(groupToken, policy), - "group_raw": entry.Group, - "prefix": applyHeadVisualeffectCase(group.Prefix, policy), - "stem": applyHeadVisualeffectCase(stem, policy), - "stem_upper": strings.ToUpper(stem), - "model_stem": modelStem, - "delimiter": policy.Delimiter, - } - legacyKey := strings.TrimSpace(expandHeadVisualeffectsFormat(policy.LegacyKeyFormat, values)) - if legacyKey == "" { - return nil - } - return []string{legacyKey} -} - -func normalizeHeadVisualeffectEntry(entry autogenManifestEntry, policy headVisualeffectsPolicy) autogenManifestEntry { +func normalizeHeadVisualeffectEntry(entry autogenManifestEntry) autogenManifestEntry { entry.Group = strings.TrimSpace(entry.Group) - if mapped, ok := policy.LegacyGroups[entry.Group]; ok && strings.TrimSpace(mapped) != "" { - entry.Group = strings.TrimSpace(mapped) - } if strings.TrimSpace(entry.Group) == "" && strings.TrimSpace(entry.Source) != "" { parts := strings.Split(filepath.ToSlash(entry.Source), "/") if len(parts) > 1 { entry.Group = parts[0] - if mapped, ok := policy.LegacyGroups[entry.Group]; ok && strings.TrimSpace(mapped) != "" { - entry.Group = strings.TrimSpace(mapped) - } } } if strings.TrimSpace(entry.Subgroup) == "" && strings.TrimSpace(entry.Source) != "" { @@ -1176,8 +1106,6 @@ func autogenHeadVisualeffectsConfigConfigured(cfg project.HeadVisualeffectsConfi len(cfg.StripModelPrefixes) > 0 || strings.TrimSpace(cfg.KeyFormat) != "" || strings.TrimSpace(cfg.LabelFormat) != "" || - len(cfg.LegacyGroups) > 0 || - strings.TrimSpace(cfg.LegacyKeyFormat) != "" || strings.TrimSpace(cfg.ModelColumn) != "" || len(cfg.RowDefaults) > 0 } diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index c3e8311..a71b9bb 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -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",