VFX Category Label Generation

This commit is contained in:
2026-05-31 13:47:14 +02:00
parent 67c03fa6bd
commit aa47532ef1
7 changed files with 587 additions and 56 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/head_accessories/hfx_bandana.mdl` - `Reusing compiled model: vfxs/headaccessory/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`
+13 -6
View File
@@ -414,12 +414,19 @@ For `head_visualeffects` autogen, producers can publish the group-to-row policy
in `autogen.producers[].head_visualeffects`, and consumers can override or in `autogen.producers[].head_visualeffects`, and consumers can override or
extend it in `autogen.consumers[].head_visualeffects`. The toolkit uses extend it in `autogen.consumers[].head_visualeffects`. The toolkit uses
compatibility defaults for older configs and manifests, but YAML can own the compatibility defaults for older configs and manifests, but YAML can own the
group prefixes, model stem prefixes to strip, key/label formats, model column, group token source, category source, delimiter, case policy, legacy group
and default row values. Individual groups can override the model column with mapping, model stem prefixes to strip, key/label formats, model column, and
`model_column` or write the discovered model stem to several columns with default row values. `group_token_source: folder_name` uses the manifest folder
`model_columns`, which is useful for root-node visualeffects. Supported format group directly instead of a prefix mapping, and `category_from:
tokens are `{dataset}`, `{group}`, `{prefix}`, `{stem}`, `{stem_upper}`, and immediate_parent` can expose the model's category folder in keys and labels.
`{model_stem}`. `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}`,
`{group}`, `{group_raw}`, `{prefix}`, `{category}`, `{category_upper}`,
`{category_segment}`, `{category_segment_upper}`, `{subgroup}`, `{stem}`,
`{stem_upper}`, `{model_stem}`, and `{delimiter}`.
For `parts_rows` generated assets, `parts_rows.acbonus` can normalize final For `parts_rows` generated assets, `parts_rows.acbonus` can normalize final
ACBONUS values before module overrides are applied. The ACBONUS values before module overrides are applied. The
+111 -14
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", "chest_accessories")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "chestaccessory"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_accessories")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "headaccessory"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_decorations")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "headdecoration"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair")) mustMkdir(t, filepath.Join(root, "assets", "vfxs", "headfeature", "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": ["chest_accessories/**/*.mdl", "head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.mdl"], "include": ["chestaccessory/**/*.mdl", "headaccessory/**/*.mdl", "headdecoration/**/*.mdl", "headfeature/**/*.mdl"],
"derive": { "derive": {
"kind": "model_stem", "kind": "model_stem",
"group_from": "first_path_segment" "group_from": "first_path_segment"
@@ -2212,7 +2212,7 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
}, },
"head_visualeffects": { "head_visualeffects": {
"groups": { "groups": {
"head_accessories": { "headaccessory": {
"prefix": "headaccessory", "prefix": "headaccessory",
"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", "chest_accessories", "tfx_sash.mdl"), "sash") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "chestaccessory", "tfx_sash.mdl"), "sash")
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_accessories", "hfx_bandana.mdl"), "bandana") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "headaccessory", "hfx_bandana.mdl"), "bandana")
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_decorations", "hfx_laurel.mdl"), "laurel") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "headdecoration", "hfx_laurel.mdl"), "laurel")
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair", "hfx_hair_bangs.mdl"), "hair") mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "headfeature", "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": "chest_accessories"`, `"group": "chestaccessory"`,
`"model_stem": "tfx_sash"`, `"model_stem": "tfx_sash"`,
`"group": "head_accessories"`, `"group": "headaccessory"`,
`"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": "head_decorations"`, `"group": "headdecoration"`,
`"model_stem": "hfx_laurel"`, `"model_stem": "hfx_laurel"`,
`"group": "head_features"`, `"group": "headfeature"`,
`"model_stem": "hfx_hair_bangs"`, `"model_stem": "hfx_hair_bangs"`,
} { } {
if !strings.Contains(text, want) { if !strings.Contains(text, want) {
@@ -2285,6 +2285,103 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
} }
} }
func TestBuildHAKsWritesFolderCategoryInHeadVisualeffectsAutogenManifest(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "headfeature", "sinfar", "ears_plt"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{
"module": {
"name": "Test Module",
"resref": "testmod",
"hak_order": ["group:core"]
},
"paths": {
"source": "src",
"assets": "assets",
"build": "build"
},
"autogen": {
"producers": [
{
"id": "head_visualeffects",
"root": "vfxs",
"include": ["headfeature/**/*.mdl"],
"derive": {
"kind": "model_stem",
"group_from": "first_path_segment"
},
"manifest": {
"release_tag": "head-vfx-manifest-current",
"asset_name": "sow-head-vfx-manifest.json",
"cache_name": "sow-head-vfx-manifest.json"
},
"head_visualeffects": {
"groups": {
"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}:{group}{delimiter}{category_segment}{stem}",
"label_format": "{group}{delimiter}{category_segment}{stem}"
}
}
]
},
"haks": [
{
"name": "core",
"priority": 1,
"max_bytes": 0,
"split": false,
"include": ["vfxs/**"]
}
]
}
`)
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "headfeature", "sinfar", "ears_plt", "hfx_Ear_EL_L.mdl"), "ear")
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)
}
if _, err := BuildHAKs(p); err != nil {
t.Fatalf("build haks: %v", err)
}
headRaw, err := os.ReadFile(filepath.Join(root, "build", "sow-head-vfx-manifest.json"))
if err != nil {
t.Fatalf("read head visualeffects manifest: %v", err)
}
text := string(headRaw)
for _, want := range []string{
`"group": "headfeature"`,
`"subgroup": "sinfar/ears_plt"`,
`"category": "ears_plt"`,
`"model_stem": "hfx_Ear_EL_L"`,
`"source": "headfeature/sinfar/ears_plt/hfx_Ear_EL_L.mdl"`,
`"group_token_source": "folder_name"`,
`"category_from": "immediate_parent"`,
} {
if !strings.Contains(text, want) {
t.Fatalf("expected head visualeffects manifest to contain %q, got:\n%s", want, text)
}
}
}
func TestBuildHAKsGeneratesParts2DAAssetsFromLocalModels(t *testing.T) { func TestBuildHAKsGeneratesParts2DAAssetsFromLocalModels(t *testing.T) {
root := t.TempDir() root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src")) mustMkdir(t, filepath.Join(root, "src"))
+52 -1
View File
@@ -388,9 +388,15 @@ type AutogenConsumerConfig struct {
type HeadVisualeffectsConfig struct { type HeadVisualeffectsConfig struct {
Groups map[string]HeadVisualeffectGroupConfig `json:"groups,omitempty" yaml:"groups"` Groups map[string]HeadVisualeffectGroupConfig `json:"groups,omitempty" yaml:"groups"`
GroupTokenSource string `json:"group_token_source,omitempty" yaml:"group_token_source"`
CategoryFrom string `json:"category_from,omitempty" yaml:"category_from"`
Delimiter string `json:"delimiter,omitempty" yaml:"delimiter"`
Case string `json:"case,omitempty" yaml:"case"`
StripModelPrefixes []string `json:"strip_model_prefixes,omitempty" yaml:"strip_model_prefixes"` StripModelPrefixes []string `json:"strip_model_prefixes,omitempty" yaml:"strip_model_prefixes"`
KeyFormat string `json:"key_format,omitempty" yaml:"key_format"` KeyFormat string `json:"key_format,omitempty" yaml:"key_format"`
LabelFormat string `json:"label_format,omitempty" yaml:"label_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"` ModelColumn string `json:"model_column,omitempty" yaml:"model_column"`
RowDefaults map[string]string `json:"row_defaults,omitempty" yaml:"row_defaults"` RowDefaults map[string]string `json:"row_defaults,omitempty" yaml:"row_defaults"`
} }
@@ -1807,15 +1813,45 @@ func validateGeneratedConfig(cfg GeneratedConfig) []error {
func headVisualeffectsConfigConfigured(cfg HeadVisualeffectsConfig) bool { func headVisualeffectsConfigConfigured(cfg HeadVisualeffectsConfig) bool {
return len(cfg.Groups) > 0 || return len(cfg.Groups) > 0 ||
strings.TrimSpace(cfg.GroupTokenSource) != "" ||
strings.TrimSpace(cfg.CategoryFrom) != "" ||
strings.TrimSpace(cfg.Delimiter) != "" ||
strings.TrimSpace(cfg.Case) != "" ||
len(cfg.StripModelPrefixes) > 0 || len(cfg.StripModelPrefixes) > 0 ||
strings.TrimSpace(cfg.KeyFormat) != "" || strings.TrimSpace(cfg.KeyFormat) != "" ||
strings.TrimSpace(cfg.LabelFormat) != "" || strings.TrimSpace(cfg.LabelFormat) != "" ||
len(cfg.LegacyGroups) > 0 ||
strings.TrimSpace(cfg.LegacyKeyFormat) != "" ||
strings.TrimSpace(cfg.ModelColumn) != "" || strings.TrimSpace(cfg.ModelColumn) != "" ||
len(cfg.RowDefaults) > 0 len(cfg.RowDefaults) > 0
} }
func validateHeadVisualeffectsConfig(fieldPrefix string, cfg HeadVisualeffectsConfig) []error { func validateHeadVisualeffectsConfig(fieldPrefix string, cfg HeadVisualeffectsConfig) []error {
var failures []error var failures []error
groupTokenSource := strings.TrimSpace(cfg.GroupTokenSource)
switch groupTokenSource {
case "", "prefix", "folder_name":
default:
failures = append(failures, fmt.Errorf("%s.group_token_source %q is not supported", fieldPrefix, cfg.GroupTokenSource))
}
categoryFrom := strings.TrimSpace(cfg.CategoryFrom)
switch categoryFrom {
case "", "none", "immediate_parent", "full_subgroup":
default:
failures = append(failures, fmt.Errorf("%s.category_from %q is not supported", fieldPrefix, cfg.CategoryFrom))
}
if strings.TrimSpace(cfg.Delimiter) == "" && cfg.Delimiter != "" {
failures = append(failures, fmt.Errorf("%s.delimiter must not be only whitespace", fieldPrefix))
}
if strings.ContainsAny(cfg.Delimiter, " \t\r\n\x00") {
failures = append(failures, fmt.Errorf("%s.delimiter must not contain whitespace or NUL bytes", fieldPrefix))
}
caseMode := strings.TrimSpace(cfg.Case)
switch caseMode {
case "", "preserve", "lower", "upper":
default:
failures = append(failures, fmt.Errorf("%s.case %q is not supported", fieldPrefix, cfg.Case))
}
for group, groupCfg := range cfg.Groups { for group, groupCfg := range cfg.Groups {
group = strings.TrimSpace(group) group = strings.TrimSpace(group)
if group == "" { if group == "" {
@@ -1824,7 +1860,7 @@ func validateHeadVisualeffectsConfig(fieldPrefix string, cfg HeadVisualeffectsCo
} }
failures = append(failures, validateTreeRootPath(fieldPrefix+".groups["+group+"]", group)...) failures = append(failures, validateTreeRootPath(fieldPrefix+".groups["+group+"]", group)...)
prefix := strings.TrimSpace(groupCfg.Prefix) prefix := strings.TrimSpace(groupCfg.Prefix)
if prefix == "" { if prefix == "" && groupTokenSource != "folder_name" {
failures = append(failures, fmt.Errorf("%s.groups[%s].prefix is required", fieldPrefix, group)) failures = append(failures, fmt.Errorf("%s.groups[%s].prefix is required", fieldPrefix, group))
} }
if strings.ContainsAny(prefix, " \t\r\n\x00") { if strings.ContainsAny(prefix, " \t\r\n\x00") {
@@ -1852,6 +1888,21 @@ 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 { for index, prefix := range cfg.StripModelPrefixes {
if strings.TrimSpace(prefix) == "" { if strings.TrimSpace(prefix) == "" {
failures = append(failures, fmt.Errorf("%s.strip_model_prefixes[%d] must not be empty", fieldPrefix, index)) failures = append(failures, fmt.Errorf("%s.strip_model_prefixes[%d] must not be empty", fieldPrefix, index))
+62 -2
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{"head_accessories/**/*.mdl"}, Include: []string{"headaccessory/**/*.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",
@@ -1416,6 +1416,66 @@ func TestValidateLayoutRejectsInvalidHeadVisualeffectsConfig(t *testing.T) {
} }
} }
func TestValidateLayoutRejectsInvalidHeadVisualeffectsNamingConfig(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src"))
mkdirAll(t, filepath.Join(root, "build"))
proj := &Project{
Root: root,
Config: Config{
Module: ModuleConfig{Name: "Test", ResRef: "test"},
Paths: PathConfig{Source: "src", Build: "build"},
Autogen: AutogenConfig{
Consumers: []AutogenConsumerConfig{
{
ID: "head_visualeffects",
Producer: "head_visualeffects",
Dataset: "visualeffects",
Mode: "head_visualeffects",
Root: "vfxs",
Include: []string{"headfeature/**/*.mdl"},
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
Manifest: AutogenManifestConfig{
ReleaseTag: "head-vfx-manifest-current",
AssetName: "sow-head-vfx-manifest.json",
CacheName: "sow-head-vfx-manifest.json",
},
HeadVisualeffects: HeadVisualeffectsConfig{
Groups: map[string]HeadVisualeffectGroupConfig{
"headfeature": {},
},
GroupTokenSource: "unsupported",
CategoryFrom: "unsupported",
Delimiter: " ",
Case: "unsupported",
LegacyGroups: map[string]string{
"head_features": "",
},
},
},
},
},
},
}
err := proj.ValidateLayout()
if err == nil {
t.Fatal("expected head visualeffects naming validation error")
}
for _, needle := range []string{
"autogen.consumers[0].head_visualeffects.group_token_source",
"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)
}
}
}
func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) { func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) {
root := t.TempDir() root := t.TempDir()
mkdirAll(t, filepath.Join(root, "assets")) mkdirAll(t, filepath.Join(root, "assets"))
@@ -1460,7 +1520,7 @@ func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) {
ID: "cachedmodels", ID: "cachedmodels",
Mode: "cachedmodels_rows", Mode: "cachedmodels_rows",
Root: "vfxs", Root: "vfxs",
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"}, Include: []string{"headaccessory/**/*.mdl", "headfeature/**/*.mdl"},
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
}, },
}, },
+196 -18
View File
@@ -29,6 +29,7 @@ type autogenManifestEntry struct {
Source string `json:"source"` Source string `json:"source"`
Group string `json:"group,omitempty"` Group string `json:"group,omitempty"`
Subgroup string `json:"subgroup,omitempty"` Subgroup string `json:"subgroup,omitempty"`
Category string `json:"category,omitempty"`
ModelStem string `json:"model_stem,omitempty"` ModelStem string `json:"model_stem,omitempty"`
RowID int `json:"row_id,omitempty"` RowID int `json:"row_id,omitempty"`
} }
@@ -319,6 +320,7 @@ func deriveAutogenManifestEntry(rel string, derive project.AutogenDeriveConfig)
entry.Group = parts[0] entry.Group = parts[0]
if len(parts) > 2 { if len(parts) > 2 {
entry.Subgroup = strings.Join(parts[1:len(parts)-1], "/") entry.Subgroup = strings.Join(parts[1:len(parts)-1], "/")
entry.Category = parts[len(parts)-2]
} }
} }
@@ -740,10 +742,25 @@ func augmentWithAutogeneratedHeadVisualeffects(collected []nativeCollectedDatase
rowID, ok := lockData[key] rowID, ok := lockData[key]
if !ok { if !ok {
var migratedLegacyKey string
if preservedRowID, preserved := historicalLockData[key]; preserved { if preservedRowID, preserved := historicalLockData[key]; preserved {
rowID = preservedRowID rowID = preservedRowID
} else { } else {
rowID = allocateNextID() 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)
} }
lockData[key] = rowID lockData[key] = rowID
} }
@@ -772,9 +789,15 @@ func nextAvailableAutogenID(used map[int]struct{}) int {
type headVisualeffectsPolicy struct { type headVisualeffectsPolicy struct {
Groups map[string]headVisualeffectsGroupPolicy Groups map[string]headVisualeffectsGroupPolicy
GroupTokenSource string
CategoryFrom string
Delimiter string
Case string
StripModelPrefixes []string StripModelPrefixes []string
KeyFormat string KeyFormat string
LabelFormat string LabelFormat string
LegacyGroups map[string]string
LegacyKeyFormat string
ModelColumn string ModelColumn string
RowDefaults map[string]string RowDefaults map[string]string
} }
@@ -798,11 +821,13 @@ func applyHeadVisualeffectsConfig(policy *headVisualeffectsPolicy, cfg project.H
for group, groupCfg := range cfg.Groups { for group, groupCfg := range cfg.Groups {
group = strings.TrimSpace(group) group = strings.TrimSpace(group)
prefix := strings.TrimSpace(groupCfg.Prefix) prefix := strings.TrimSpace(groupCfg.Prefix)
if group == "" || prefix == "" { if group == "" {
continue continue
} }
groupPolicy := policy.Groups[group] groupPolicy := policy.Groups[group]
groupPolicy.Prefix = prefix if prefix != "" {
groupPolicy.Prefix = prefix
}
if columns := normalizeHeadVisualeffectsModelColumns(groupCfg.ModelColumn, groupCfg.ModelColumns); len(columns) > 0 { if columns := normalizeHeadVisualeffectsModelColumns(groupCfg.ModelColumn, groupCfg.ModelColumns); len(columns) > 0 {
groupPolicy.ModelColumns = columns groupPolicy.ModelColumns = columns
} }
@@ -812,6 +837,18 @@ func applyHeadVisualeffectsConfig(policy *headVisualeffectsPolicy, cfg project.H
mergeStringMap(groupPolicy.RowDefaults, normalizeHeadVisualeffectsRowDefaults(groupCfg.RowDefaults)) mergeStringMap(groupPolicy.RowDefaults, normalizeHeadVisualeffectsRowDefaults(groupCfg.RowDefaults))
policy.Groups[group] = groupPolicy policy.Groups[group] = groupPolicy
} }
if strings.TrimSpace(cfg.GroupTokenSource) != "" {
policy.GroupTokenSource = strings.TrimSpace(cfg.GroupTokenSource)
}
if strings.TrimSpace(cfg.CategoryFrom) != "" {
policy.CategoryFrom = strings.TrimSpace(cfg.CategoryFrom)
}
if cfg.Delimiter != "" {
policy.Delimiter = cfg.Delimiter
}
if strings.TrimSpace(cfg.Case) != "" {
policy.Case = strings.TrimSpace(cfg.Case)
}
if len(cfg.StripModelPrefixes) > 0 { if len(cfg.StripModelPrefixes) > 0 {
policy.StripModelPrefixes = append([]string(nil), cfg.StripModelPrefixes...) policy.StripModelPrefixes = append([]string(nil), cfg.StripModelPrefixes...)
} }
@@ -821,6 +858,22 @@ func applyHeadVisualeffectsConfig(policy *headVisualeffectsPolicy, cfg project.H
if strings.TrimSpace(cfg.LabelFormat) != "" { if strings.TrimSpace(cfg.LabelFormat) != "" {
policy.LabelFormat = 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) != "" { if strings.TrimSpace(cfg.ModelColumn) != "" {
policy.ModelColumn = strings.TrimSpace(cfg.ModelColumn) policy.ModelColumn = strings.TrimSpace(cfg.ModelColumn)
} }
@@ -834,6 +887,10 @@ func defaultHeadVisualeffectsPolicy() headVisualeffectsPolicy {
"head_decorations": {Prefix: "headdecoration"}, "head_decorations": {Prefix: "headdecoration"},
"head_features": {Prefix: "headfeature"}, "head_features": {Prefix: "headfeature"},
}, },
GroupTokenSource: "prefix",
CategoryFrom: "none",
Delimiter: "_",
Case: "preserve",
StripModelPrefixes: []string{"hfx_", "vfx_"}, StripModelPrefixes: []string{"hfx_", "vfx_"},
KeyFormat: "{dataset}:{prefix}_{stem}", KeyFormat: "{dataset}:{prefix}_{stem}",
LabelFormat: "{prefix}_{stem_upper}", LabelFormat: "{prefix}_{stem_upper}",
@@ -882,11 +939,104 @@ func mergeStringMap(target map[string]string, source map[string]string) {
} }
func headVisualeffectIdentity(dataset string, entry autogenManifestEntry, policy headVisualeffectsPolicy) (string, string, string, headVisualeffectsGroupPolicy, bool) { func headVisualeffectIdentity(dataset string, entry autogenManifestEntry, policy headVisualeffectsPolicy) (string, string, string, headVisualeffectsGroupPolicy, bool) {
entry = normalizeHeadVisualeffectEntry(entry, policy)
group, ok := policy.Groups[entry.Group] group, ok := policy.Groups[entry.Group]
if !ok || strings.TrimSpace(entry.ModelStem) == "" { if !ok || strings.TrimSpace(entry.ModelStem) == "" {
return "", "", "", headVisualeffectsGroupPolicy{}, false return "", "", "", headVisualeffectsGroupPolicy{}, false
} }
modelStem := strings.TrimSpace(entry.ModelStem) modelStem := strings.TrimSpace(entry.ModelStem)
stem := stripHeadVisualeffectModelPrefix(modelStem, policy)
stem = strings.TrimSpace(stem)
if stem == "" {
return "", "", "", headVisualeffectsGroupPolicy{}, false
}
category := headVisualeffectCategory(entry, policy)
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),
"category": applyHeadVisualeffectCase(category, policy),
"category_upper": strings.ToUpper(category),
"category_segment": headVisualeffectDelimitedSegment(applyHeadVisualeffectCase(category, policy), policy.Delimiter),
"category_segment_upper": headVisualeffectDelimitedSegment(strings.ToUpper(category), policy.Delimiter),
"subgroup": applyHeadVisualeffectCase(entry.Subgroup, policy),
"stem": applyHeadVisualeffectCase(stem, policy),
"stem_upper": strings.ToUpper(stem),
"model_stem": modelStem,
"delimiter": policy.Delimiter,
}
key := expandHeadVisualeffectsFormat(policy.KeyFormat, values)
label := expandHeadVisualeffectsFormat(policy.LabelFormat, values)
if strings.TrimSpace(key) == "" || strings.TrimSpace(label) == "" {
return "", "", "", headVisualeffectsGroupPolicy{}, false
}
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 {
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) != "" {
parts := strings.Split(filepath.ToSlash(entry.Source), "/")
if len(parts) > 2 {
entry.Subgroup = strings.Join(parts[1:len(parts)-1], "/")
}
}
if strings.TrimSpace(entry.Category) == "" && strings.TrimSpace(entry.Source) != "" {
parts := strings.Split(filepath.ToSlash(entry.Source), "/")
if len(parts) > 2 {
entry.Category = parts[len(parts)-2]
}
}
return entry
}
func stripHeadVisualeffectModelPrefix(modelStem string, policy headVisualeffectsPolicy) string {
stem := modelStem stem := modelStem
for _, prefix := range policy.StripModelPrefixes { for _, prefix := range policy.StripModelPrefixes {
prefix = strings.TrimSpace(prefix) prefix = strings.TrimSpace(prefix)
@@ -895,24 +1045,46 @@ func headVisualeffectIdentity(dataset string, entry autogenManifestEntry, policy
} }
stem = strings.TrimPrefix(stem, prefix) stem = strings.TrimPrefix(stem, prefix)
} }
stem = strings.TrimSpace(stem) return stem
if stem == "" { }
return "", "", "", headVisualeffectsGroupPolicy{}, false
func headVisualeffectGroupToken(groupName string, group headVisualeffectsGroupPolicy, policy headVisualeffectsPolicy) string {
if policy.GroupTokenSource == "folder_name" {
return groupName
} }
values := map[string]string{ if strings.TrimSpace(group.Prefix) != "" {
"dataset": dataset, return group.Prefix
"group": entry.Group,
"prefix": group.Prefix,
"stem": stem,
"stem_upper": strings.ToUpper(stem),
"model_stem": modelStem,
} }
key := expandHeadVisualeffectsFormat(policy.KeyFormat, values) return groupName
label := expandHeadVisualeffectsFormat(policy.LabelFormat, values) }
if strings.TrimSpace(key) == "" || strings.TrimSpace(label) == "" {
return "", "", "", headVisualeffectsGroupPolicy{}, false func headVisualeffectCategory(entry autogenManifestEntry, policy headVisualeffectsPolicy) string {
switch policy.CategoryFrom {
case "immediate_parent":
return strings.TrimSpace(entry.Category)
case "full_subgroup":
return strings.TrimSpace(entry.Subgroup)
default:
return ""
}
}
func headVisualeffectDelimitedSegment(value, delimiter string) string {
if strings.TrimSpace(value) == "" {
return ""
}
return value + delimiter
}
func applyHeadVisualeffectCase(value string, policy headVisualeffectsPolicy) string {
switch policy.Case {
case "lower":
return strings.ToLower(value)
case "upper":
return strings.ToUpper(value)
default:
return value
} }
return key, label, modelStem, group, true
} }
func expandHeadVisualeffectsFormat(format string, values map[string]string) string { func expandHeadVisualeffectsFormat(format string, values map[string]string) string {
@@ -997,9 +1169,15 @@ func formatAutogenManifest(root string, producer project.AutogenProducerConfig,
func autogenHeadVisualeffectsConfigConfigured(cfg project.HeadVisualeffectsConfig) bool { func autogenHeadVisualeffectsConfigConfigured(cfg project.HeadVisualeffectsConfig) bool {
return len(cfg.Groups) > 0 || return len(cfg.Groups) > 0 ||
strings.TrimSpace(cfg.GroupTokenSource) != "" ||
strings.TrimSpace(cfg.CategoryFrom) != "" ||
cfg.Delimiter != "" ||
strings.TrimSpace(cfg.Case) != "" ||
len(cfg.StripModelPrefixes) > 0 || len(cfg.StripModelPrefixes) > 0 ||
strings.TrimSpace(cfg.KeyFormat) != "" || strings.TrimSpace(cfg.KeyFormat) != "" ||
strings.TrimSpace(cfg.LabelFormat) != "" || strings.TrimSpace(cfg.LabelFormat) != "" ||
len(cfg.LegacyGroups) > 0 ||
strings.TrimSpace(cfg.LegacyKeyFormat) != "" ||
strings.TrimSpace(cfg.ModelColumn) != "" || strings.TrimSpace(cfg.ModelColumn) != "" ||
len(cfg.RowDefaults) > 0 len(cfg.RowDefaults) > 0
} }
+152 -14
View File
@@ -7409,12 +7409,150 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
} }
} }
func TestApplyAutogenConsumersUsesFolderDrivenSlashHeadVisualeffectsNames(t *testing.T) {
root := testProjectRoot(t)
overrideRoot := filepath.Join(root, "autogen-assets")
mkdirAll(t, filepath.Join(overrideRoot, "vfxs", "headfeature", "sinfar", "ears_plt"))
writeFile(t, filepath.Join(overrideRoot, "vfxs", "headfeature", "sinfar", "ears_plt", "hfx_Ear_EL_L.mdl"), "mdl\n")
p := testProject(root)
p.Config.Autogen.Consumers = []project.AutogenConsumerConfig{
{
ID: "head_visualeffects",
Producer: "head_visualeffects",
Dataset: "visualeffects",
Mode: "head_visualeffects",
Root: "vfxs",
Include: []string{"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",
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}",
LegacyKeyFormat: "{dataset}:{group}_{stem}",
RowDefaults: map[string]string{
"Type_FD": "D",
"OrientWithGround": "0",
"OrientWithObject": "1",
},
},
},
}
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,
},
},
}
got, err := applyAutogenConsumers(p, collected, nil)
if err != nil {
t.Fatalf("applyAutogenConsumers failed: %v", err)
}
if len(got) != 1 || len(got[0].Rows) != 1 {
t.Fatalf("expected one autogenerated visualeffects row, got %#v", got)
}
row := got[0].Rows[0]
if row["key"] != "visualeffects:headfeature/ears_plt/Ear_EL_L" {
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)
}
if row["Label"] != "headfeature/ears_plt/Ear_EL_L" {
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" {
t.Fatalf("expected configured model columns, got %#v", row)
}
}
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) { func TestBuildNativeUsesConfiguredHeadVisualeffectsGroups(t *testing.T) {
root := t.TempDir() root := t.TempDir()
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", "chest_accessories")) mkdirAll(t, filepath.Join(root, "autogen-assets", "vfxs", "chestaccessory"))
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"), `{
@@ -7422,7 +7560,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", "chest_accessories", "tfx_sash.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "autogen-assets", "vfxs", "chestaccessory", "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:
@@ -7442,7 +7580,7 @@ autogen:
mode: head_visualeffects mode: head_visualeffects
root: vfxs root: vfxs
include: include:
- chest_accessories/**/*.mdl - chestaccessory/**/*.mdl
- head_jewels/**/*.mdl - head_jewels/**/*.mdl
derive: derive:
kind: model_stem kind: model_stem
@@ -7454,7 +7592,7 @@ autogen:
local_override_root: autogen-assets local_override_root: autogen-assets
head_visualeffects: head_visualeffects:
groups: groups:
chest_accessories: chestaccessory:
prefix: chestaccessory prefix: chestaccessory
model_columns: model_columns:
- Imp_Root_S_Node - Imp_Root_S_Node
@@ -7499,16 +7637,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", "chest_accessories")) mkdirAll(t, filepath.Join(root, "assets", "vfxs", "chestaccessory"))
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_accessories")) mkdirAll(t, filepath.Join(root, "assets", "vfxs", "headaccessory"))
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_decorations")) mkdirAll(t, filepath.Join(root, "assets", "vfxs", "headdecoration"))
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair")) mkdirAll(t, filepath.Join(root, "assets", "vfxs", "headfeature", "hair"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "cachedmodels")) mkdirAll(t, filepath.Join(root, "topdata", "data", "cachedmodels"))
writeFile(t, filepath.Join(root, "assets", "vfxs", "chest_accessories", "tfx_sash.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "chestaccessory", "tfx_sash.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_bandana.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "assets", "vfxs", "head_accessories", "hfx_existing.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "headaccessory", "hfx_existing.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "assets", "vfxs", "head_decorations", "hfx_laurel.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "headdecoration", "hfx_laurel.mdl"), "mdl\n")
writeFile(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair", "hfx_hair_bangs.mdl"), "mdl\n") writeFile(t, filepath.Join(root, "assets", "vfxs", "headfeature", "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": [
@@ -7530,7 +7668,7 @@ func TestBuildGenerated2DAAssetsAppendsCachedModelsFromHeadVisualeffects(t *test
ID: "cachedmodels", ID: "cachedmodels",
Mode: "cachedmodels_rows", Mode: "cachedmodels_rows",
Root: "vfxs", Root: "vfxs",
Include: []string{"chest_accessories/**/*.mdl", "head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.mdl"}, Include: []string{"chestaccessory/**/*.mdl", "headaccessory/**/*.mdl", "headdecoration/**/*.mdl", "headfeature/**/*.mdl"},
Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"}, Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
}, },
}, },