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
+52 -1
View File
@@ -388,9 +388,15 @@ type AutogenConsumerConfig struct {
type HeadVisualeffectsConfig struct {
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"`
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"`
}
@@ -1807,15 +1813,45 @@ func validateGeneratedConfig(cfg GeneratedConfig) []error {
func headVisualeffectsConfigConfigured(cfg HeadVisualeffectsConfig) bool {
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 ||
strings.TrimSpace(cfg.KeyFormat) != "" ||
strings.TrimSpace(cfg.LabelFormat) != "" ||
len(cfg.LegacyGroups) > 0 ||
strings.TrimSpace(cfg.LegacyKeyFormat) != "" ||
strings.TrimSpace(cfg.ModelColumn) != "" ||
len(cfg.RowDefaults) > 0
}
func validateHeadVisualeffectsConfig(fieldPrefix string, cfg HeadVisualeffectsConfig) []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 {
group = strings.TrimSpace(group)
if group == "" {
@@ -1824,7 +1860,7 @@ func validateHeadVisualeffectsConfig(fieldPrefix string, cfg HeadVisualeffectsCo
}
failures = append(failures, validateTreeRootPath(fieldPrefix+".groups["+group+"]", group)...)
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))
}
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 {
if strings.TrimSpace(prefix) == "" {
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",
Mode: "unsupported",
Root: "vfxs",
Include: []string{"head_accessories/**/*.mdl"},
Include: []string{"headaccessory/**/*.mdl"},
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
Manifest: AutogenManifestConfig{
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) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "assets"))
@@ -1460,7 +1520,7 @@ func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) {
ID: "cachedmodels",
Mode: "cachedmodels_rows",
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"},
},
},