Accessories unhardcoded
This commit is contained in:
@@ -401,8 +401,19 @@ and `package_root` plus the repository's HAK include patterns determine where
|
||||
the resources are packaged.
|
||||
|
||||
For `cachedmodels_rows` generated assets, the configured autogen scan appends
|
||||
discovered model stems to the selected `cachedmodels` dataset without `.mdl`
|
||||
extensions and skips models already present in authored rows.
|
||||
discovered model stems from the configured include globs to the selected
|
||||
`cachedmodels` dataset without `.mdl` extensions and skips models already
|
||||
present in authored rows.
|
||||
|
||||
For `head_visualeffects` autogen consumers, repositories can configure the
|
||||
group-to-row policy in `autogen.consumers[].head_visualeffects`. The toolkit
|
||||
uses compatibility defaults for older configs, but YAML can own the group
|
||||
prefixes, model stem prefixes to strip, key/label formats, model column, and
|
||||
default row values. 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}`, `{prefix}`, `{stem}`, `{stem_upper}`, and
|
||||
`{model_stem}`.
|
||||
|
||||
For `parts_rows` generated assets, `parts_rows.acbonus` can normalize final
|
||||
ACBONUS values before module overrides are applied. The
|
||||
|
||||
@@ -2178,7 +2178,9 @@ func TestPlanHAKsWritesManifestWithoutArchives(t *testing.T) {
|
||||
func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mustMkdir(t, filepath.Join(root, "src"))
|
||||
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "chest_accessories"))
|
||||
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_accessories"))
|
||||
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_decorations"))
|
||||
mustMkdir(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair"))
|
||||
mustMkdir(t, filepath.Join(root, "build"))
|
||||
|
||||
@@ -2198,7 +2200,7 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
|
||||
{
|
||||
"id": "head_visualeffects",
|
||||
"root": "vfxs",
|
||||
"include": ["head_accessories/**/*.mdl", "head_features/**/*.mdl"],
|
||||
"include": ["chest_accessories/**/*.mdl", "head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.mdl"],
|
||||
"derive": {
|
||||
"kind": "model_stem",
|
||||
"group_from": "first_path_segment"
|
||||
@@ -2223,7 +2225,9 @@ 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", "head_accessories", "hfx_bandana.mdl"), "bandana")
|
||||
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_decorations", "hfx_laurel.mdl"), "laurel")
|
||||
mustWriteFile(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair", "hfx_hair_bangs.mdl"), "hair")
|
||||
|
||||
p, err := project.Load(root)
|
||||
@@ -2255,8 +2259,12 @@ func TestBuildHAKsWritesConfiguredHeadVisualeffectsAutogenManifest(t *testing.T)
|
||||
text := string(headRaw)
|
||||
for _, want := range []string{
|
||||
`"id": "head_visualeffects"`,
|
||||
`"group": "chest_accessories"`,
|
||||
`"model_stem": "tfx_sash"`,
|
||||
`"group": "head_accessories"`,
|
||||
`"model_stem": "hfx_bandana"`,
|
||||
`"group": "head_decorations"`,
|
||||
`"model_stem": "hfx_laurel"`,
|
||||
`"group": "head_features"`,
|
||||
`"model_stem": "hfx_hair_bangs"`,
|
||||
} {
|
||||
|
||||
+101
-11
@@ -371,17 +371,34 @@ type AutogenProducerConfig struct {
|
||||
}
|
||||
|
||||
type AutogenConsumerConfig struct {
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Producer string `json:"producer" yaml:"producer"`
|
||||
Dataset string `json:"dataset" yaml:"dataset"`
|
||||
Mode string `json:"mode" yaml:"mode"`
|
||||
Optional bool `json:"optional,omitempty" yaml:"optional,omitempty"`
|
||||
Root string `json:"root" yaml:"root"`
|
||||
Include []string `json:"include" yaml:"include"`
|
||||
Derive AutogenDeriveConfig `json:"derive" yaml:"derive"`
|
||||
PartsRows PartsRowsConfig `json:"parts_rows" yaml:"parts_rows"`
|
||||
Manifest AutogenManifestConfig `json:"manifest" yaml:"manifest"`
|
||||
LocalOverrideRoot string `json:"local_override_root,omitempty" yaml:"local_override_root,omitempty"`
|
||||
ID string `json:"id" yaml:"id"`
|
||||
Producer string `json:"producer" yaml:"producer"`
|
||||
Dataset string `json:"dataset" yaml:"dataset"`
|
||||
Mode string `json:"mode" yaml:"mode"`
|
||||
Optional bool `json:"optional,omitempty" yaml:"optional,omitempty"`
|
||||
Root string `json:"root" yaml:"root"`
|
||||
Include []string `json:"include" yaml:"include"`
|
||||
Derive AutogenDeriveConfig `json:"derive" yaml:"derive"`
|
||||
PartsRows PartsRowsConfig `json:"parts_rows" yaml:"parts_rows"`
|
||||
HeadVisualeffects HeadVisualeffectsConfig `json:"head_visualeffects" yaml:"head_visualeffects"`
|
||||
Manifest AutogenManifestConfig `json:"manifest" yaml:"manifest"`
|
||||
LocalOverrideRoot string `json:"local_override_root,omitempty" yaml:"local_override_root,omitempty"`
|
||||
}
|
||||
|
||||
type HeadVisualeffectsConfig struct {
|
||||
Groups map[string]HeadVisualeffectGroupConfig `json:"groups" yaml:"groups"`
|
||||
StripModelPrefixes []string `json:"strip_model_prefixes" yaml:"strip_model_prefixes"`
|
||||
KeyFormat string `json:"key_format" yaml:"key_format"`
|
||||
LabelFormat string `json:"label_format" yaml:"label_format"`
|
||||
ModelColumn string `json:"model_column" yaml:"model_column"`
|
||||
RowDefaults map[string]string `json:"row_defaults" yaml:"row_defaults"`
|
||||
}
|
||||
|
||||
type HeadVisualeffectGroupConfig struct {
|
||||
Prefix string `json:"prefix" yaml:"prefix"`
|
||||
ModelColumn string `json:"model_column" yaml:"model_column"`
|
||||
ModelColumns []string `json:"model_columns" yaml:"model_columns"`
|
||||
RowDefaults map[string]string `json:"row_defaults" yaml:"row_defaults"`
|
||||
}
|
||||
|
||||
type PartsRowsConfig struct {
|
||||
@@ -1721,6 +1738,9 @@ func validateAutogenConfig(cfg AutogenConfig) []error {
|
||||
}
|
||||
failures = append(failures, validateGlobList(fieldPrefix+".include", consumer.Include)...)
|
||||
failures = append(failures, validateAutogenDeriveConfig(fieldPrefix+".derive", consumer.Derive)...)
|
||||
if strings.TrimSpace(consumer.Mode) == "head_visualeffects" && headVisualeffectsConfigConfigured(consumer.HeadVisualeffects) {
|
||||
failures = append(failures, validateHeadVisualeffectsConfig(fieldPrefix+".head_visualeffects", consumer.HeadVisualeffects)...)
|
||||
}
|
||||
failures = append(failures, validateAutogenManifestConfig(fieldPrefix+".manifest", consumer.Manifest)...)
|
||||
}
|
||||
|
||||
@@ -1781,6 +1801,76 @@ func validateGeneratedConfig(cfg GeneratedConfig) []error {
|
||||
return failures
|
||||
}
|
||||
|
||||
func headVisualeffectsConfigConfigured(cfg HeadVisualeffectsConfig) bool {
|
||||
return len(cfg.Groups) > 0 ||
|
||||
len(cfg.StripModelPrefixes) > 0 ||
|
||||
strings.TrimSpace(cfg.KeyFormat) != "" ||
|
||||
strings.TrimSpace(cfg.LabelFormat) != "" ||
|
||||
strings.TrimSpace(cfg.ModelColumn) != "" ||
|
||||
len(cfg.RowDefaults) > 0
|
||||
}
|
||||
|
||||
func validateHeadVisualeffectsConfig(fieldPrefix string, cfg HeadVisualeffectsConfig) []error {
|
||||
var failures []error
|
||||
for group, groupCfg := range cfg.Groups {
|
||||
group = strings.TrimSpace(group)
|
||||
if group == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.groups contains an empty group key", fieldPrefix))
|
||||
continue
|
||||
}
|
||||
failures = append(failures, validateTreeRootPath(fieldPrefix+".groups["+group+"]", group)...)
|
||||
prefix := strings.TrimSpace(groupCfg.Prefix)
|
||||
if prefix == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.groups[%s].prefix is required", fieldPrefix, group))
|
||||
}
|
||||
if strings.ContainsAny(prefix, " \t\r\n\x00") {
|
||||
failures = append(failures, fmt.Errorf("%s.groups[%s].prefix must not contain whitespace or NUL bytes", fieldPrefix, group))
|
||||
}
|
||||
if column := strings.TrimSpace(groupCfg.ModelColumn); column != "" && strings.ContainsAny(column, " \t\r\n\x00") {
|
||||
failures = append(failures, fmt.Errorf("%s.groups[%s].model_column must not contain whitespace or NUL bytes", fieldPrefix, group))
|
||||
}
|
||||
for index, column := range groupCfg.ModelColumns {
|
||||
column = strings.TrimSpace(column)
|
||||
if column == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.groups[%s].model_columns[%d] must not be empty", fieldPrefix, group, index))
|
||||
}
|
||||
if strings.ContainsAny(column, " \t\r\n\x00") {
|
||||
failures = append(failures, fmt.Errorf("%s.groups[%s].model_columns[%d] must not contain whitespace or NUL bytes", fieldPrefix, group, index))
|
||||
}
|
||||
}
|
||||
for column := range groupCfg.RowDefaults {
|
||||
column = strings.TrimSpace(column)
|
||||
if column == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.groups[%s].row_defaults contains an empty column name", fieldPrefix, group))
|
||||
}
|
||||
if strings.ContainsAny(column, " \t\r\n\x00") {
|
||||
failures = append(failures, fmt.Errorf("%s.groups[%s].row_defaults[%s] column name must not contain whitespace or NUL bytes", fieldPrefix, group, column))
|
||||
}
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
if strings.Contains(prefix, "\x00") {
|
||||
failures = append(failures, fmt.Errorf("%s.strip_model_prefixes[%d] must not contain NUL bytes", fieldPrefix, index))
|
||||
}
|
||||
}
|
||||
if column := strings.TrimSpace(cfg.ModelColumn); column != "" && strings.ContainsAny(column, " \t\r\n\x00") {
|
||||
failures = append(failures, fmt.Errorf("%s.model_column must not contain whitespace or NUL bytes", fieldPrefix))
|
||||
}
|
||||
for column := range cfg.RowDefaults {
|
||||
column = strings.TrimSpace(column)
|
||||
if column == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.row_defaults contains an empty column name", fieldPrefix))
|
||||
}
|
||||
if strings.ContainsAny(column, " \t\r\n\x00") {
|
||||
failures = append(failures, fmt.Errorf("%s.row_defaults[%s] column name must not contain whitespace or NUL bytes", fieldPrefix, column))
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func partsRowsConfigConfigured(cfg PartsRowsConfig) bool {
|
||||
if len(cfg.RowDefaults) > 0 {
|
||||
return true
|
||||
|
||||
@@ -1360,6 +1360,62 @@ func TestValidateLayoutRejectsInvalidAutogenConsumerConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLayoutRejectsInvalidHeadVisualeffectsConfig(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{"head_jewels/**/*.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{
|
||||
"head_jewels": {
|
||||
ModelColumns: []string{"Imp Root M Node"},
|
||||
},
|
||||
},
|
||||
StripModelPrefixes: []string{""},
|
||||
ModelColumn: "Imp HeadCon Node",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := proj.ValidateLayout()
|
||||
if err == nil {
|
||||
t.Fatal("expected head visualeffects validation error")
|
||||
}
|
||||
for _, needle := range []string{
|
||||
"autogen.consumers[0].head_visualeffects.groups[head_jewels].prefix",
|
||||
"autogen.consumers[0].head_visualeffects.groups[head_jewels].model_columns[0]",
|
||||
"autogen.consumers[0].head_visualeffects.strip_model_prefixes[0]",
|
||||
"autogen.consumers[0].head_visualeffects.model_column",
|
||||
} {
|
||||
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"))
|
||||
|
||||
+180
-36
@@ -74,7 +74,7 @@ func applyAutogenConsumers(p *project.Project, collected []nativeCollectedDatase
|
||||
case "parts_rows":
|
||||
result = augmentWithAutogeneratedParts(result, autogenPartsInventory(entries))
|
||||
case "head_visualeffects":
|
||||
result, err = augmentWithAutogeneratedHeadVisualeffects(result, entries)
|
||||
result, err = augmentWithAutogeneratedHeadVisualeffects(result, entries, consumer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -660,11 +660,12 @@ func augmentWithAutogeneratedCachedModels(collected []nativeCollectedDataset, en
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func augmentWithAutogeneratedHeadVisualeffects(collected []nativeCollectedDataset, entries []autogenManifestEntry) ([]nativeCollectedDataset, error) {
|
||||
func augmentWithAutogeneratedHeadVisualeffects(collected []nativeCollectedDataset, entries []autogenManifestEntry, consumer project.AutogenConsumerConfig) ([]nativeCollectedDataset, error) {
|
||||
if len(entries) == 0 {
|
||||
return collected, nil
|
||||
}
|
||||
|
||||
policy := resolveHeadVisualeffectsPolicy(consumer)
|
||||
result := append([]nativeCollectedDataset(nil), collected...)
|
||||
for i, dataset := range result {
|
||||
if dataset.Dataset.Name != "visualeffects" {
|
||||
@@ -714,12 +715,12 @@ func augmentWithAutogeneratedHeadVisualeffects(collected []nativeCollectedDatase
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
key, label, ok := headVisualeffectIdentity(entry)
|
||||
key, label, modelStem, groupPolicy, ok := headVisualeffectIdentity(dataset.Dataset.Name, entry, policy)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if existing, exists := rowByKey[key]; exists {
|
||||
applyDiscoveredHeadVisualeffectDefaults(existing, dataset.Columns, entry.ModelStem, label)
|
||||
applyDiscoveredHeadVisualeffectDefaults(existing, dataset.Columns, modelStem, label, policy, groupPolicy)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -732,7 +733,7 @@ func augmentWithAutogeneratedHeadVisualeffects(collected []nativeCollectedDatase
|
||||
}
|
||||
lockData[key] = rowID
|
||||
}
|
||||
newRow := createDefaultHeadVisualeffectRow(dataset.Columns, rowID, key, label, entry.ModelStem)
|
||||
newRow := createDefaultHeadVisualeffectRow(dataset.Columns, rowID, key, label, modelStem, policy, groupPolicy)
|
||||
rows = append(rows, newRow)
|
||||
rowByKey[key] = newRow
|
||||
}
|
||||
@@ -755,25 +756,150 @@ func nextAvailableAutogenID(used map[int]struct{}) int {
|
||||
}
|
||||
}
|
||||
|
||||
func headVisualeffectIdentity(entry autogenManifestEntry) (string, string, bool) {
|
||||
groupPrefix := map[string]string{
|
||||
"head_accessories": "headaccessory",
|
||||
"head_features": "headfeature",
|
||||
}
|
||||
prefix, ok := groupPrefix[entry.Group]
|
||||
if !ok || strings.TrimSpace(entry.ModelStem) == "" {
|
||||
return "", "", false
|
||||
}
|
||||
stem := strings.TrimPrefix(entry.ModelStem, "hfx_")
|
||||
stem = strings.TrimPrefix(stem, "vfx_")
|
||||
stem = strings.TrimSpace(stem)
|
||||
if stem == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return "visualeffects:" + prefix + "_" + stem, prefix + "_" + strings.ToUpper(stem), true
|
||||
type headVisualeffectsPolicy struct {
|
||||
Groups map[string]headVisualeffectsGroupPolicy
|
||||
StripModelPrefixes []string
|
||||
KeyFormat string
|
||||
LabelFormat string
|
||||
ModelColumn string
|
||||
RowDefaults map[string]string
|
||||
}
|
||||
|
||||
func createDefaultHeadVisualeffectRow(columns []string, rowID int, key, label, modelStem string) map[string]any {
|
||||
type headVisualeffectsGroupPolicy struct {
|
||||
Prefix string
|
||||
ModelColumns []string
|
||||
RowDefaults map[string]string
|
||||
}
|
||||
|
||||
func resolveHeadVisualeffectsPolicy(consumer project.AutogenConsumerConfig) headVisualeffectsPolicy {
|
||||
policy := defaultHeadVisualeffectsPolicy()
|
||||
cfg := consumer.HeadVisualeffects
|
||||
for group, groupCfg := range cfg.Groups {
|
||||
group = strings.TrimSpace(group)
|
||||
prefix := strings.TrimSpace(groupCfg.Prefix)
|
||||
if group == "" || prefix == "" {
|
||||
continue
|
||||
}
|
||||
groupPolicy := headVisualeffectsGroupPolicy{
|
||||
Prefix: prefix,
|
||||
ModelColumns: normalizeHeadVisualeffectsModelColumns(groupCfg.ModelColumn, groupCfg.ModelColumns),
|
||||
RowDefaults: normalizeHeadVisualeffectsRowDefaults(groupCfg.RowDefaults),
|
||||
}
|
||||
policy.Groups[group] = groupPolicy
|
||||
}
|
||||
if len(cfg.StripModelPrefixes) > 0 {
|
||||
policy.StripModelPrefixes = append([]string(nil), cfg.StripModelPrefixes...)
|
||||
}
|
||||
if strings.TrimSpace(cfg.KeyFormat) != "" {
|
||||
policy.KeyFormat = strings.TrimSpace(cfg.KeyFormat)
|
||||
}
|
||||
if strings.TrimSpace(cfg.LabelFormat) != "" {
|
||||
policy.LabelFormat = strings.TrimSpace(cfg.LabelFormat)
|
||||
}
|
||||
if strings.TrimSpace(cfg.ModelColumn) != "" {
|
||||
policy.ModelColumn = strings.TrimSpace(cfg.ModelColumn)
|
||||
}
|
||||
mergeStringMap(policy.RowDefaults, normalizeHeadVisualeffectsRowDefaults(cfg.RowDefaults))
|
||||
return policy
|
||||
}
|
||||
|
||||
func defaultHeadVisualeffectsPolicy() headVisualeffectsPolicy {
|
||||
return headVisualeffectsPolicy{
|
||||
Groups: map[string]headVisualeffectsGroupPolicy{
|
||||
"head_accessories": {Prefix: "headaccessory"},
|
||||
"head_decorations": {Prefix: "headdecoration"},
|
||||
"head_features": {Prefix: "headfeature"},
|
||||
},
|
||||
StripModelPrefixes: []string{"hfx_", "vfx_"},
|
||||
KeyFormat: "{dataset}:{prefix}_{stem}",
|
||||
LabelFormat: "{prefix}_{stem_upper}",
|
||||
ModelColumn: "Imp_HeadCon_Node",
|
||||
RowDefaults: map[string]string{
|
||||
"Type_FD": "D",
|
||||
"OrientWithGround": "0",
|
||||
"OrientWithObject": "1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHeadVisualeffectsModelColumns(modelColumn string, modelColumns []string) []string {
|
||||
var result []string
|
||||
if column := strings.TrimSpace(modelColumn); column != "" {
|
||||
result = append(result, column)
|
||||
}
|
||||
for _, column := range modelColumns {
|
||||
column = strings.TrimSpace(column)
|
||||
if column == "" {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(result, column) {
|
||||
result = append(result, column)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeHeadVisualeffectsRowDefaults(defaults map[string]string) map[string]string {
|
||||
result := map[string]string{}
|
||||
for column, value := range defaults {
|
||||
column = strings.TrimSpace(column)
|
||||
if column == "" {
|
||||
continue
|
||||
}
|
||||
result[column] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeStringMap(target map[string]string, source map[string]string) {
|
||||
for key, value := range source {
|
||||
target[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func headVisualeffectIdentity(dataset string, entry autogenManifestEntry, policy headVisualeffectsPolicy) (string, string, string, headVisualeffectsGroupPolicy, bool) {
|
||||
group, ok := policy.Groups[entry.Group]
|
||||
if !ok || strings.TrimSpace(entry.ModelStem) == "" {
|
||||
return "", "", "", headVisualeffectsGroupPolicy{}, false
|
||||
}
|
||||
modelStem := strings.TrimSpace(entry.ModelStem)
|
||||
stem := modelStem
|
||||
for _, prefix := range policy.StripModelPrefixes {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
continue
|
||||
}
|
||||
stem = strings.TrimPrefix(stem, prefix)
|
||||
}
|
||||
stem = strings.TrimSpace(stem)
|
||||
if stem == "" {
|
||||
return "", "", "", headVisualeffectsGroupPolicy{}, false
|
||||
}
|
||||
values := map[string]string{
|
||||
"dataset": dataset,
|
||||
"group": entry.Group,
|
||||
"prefix": group.Prefix,
|
||||
"stem": stem,
|
||||
"stem_upper": strings.ToUpper(stem),
|
||||
"model_stem": modelStem,
|
||||
}
|
||||
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 expandHeadVisualeffectsFormat(format string, values map[string]string) string {
|
||||
result := format
|
||||
for key, value := range values {
|
||||
result = strings.ReplaceAll(result, "{"+key+"}", value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func createDefaultHeadVisualeffectRow(columns []string, rowID int, key, label, modelStem string, policy headVisualeffectsPolicy, group headVisualeffectsGroupPolicy) map[string]any {
|
||||
row := map[string]any{
|
||||
"id": rowID,
|
||||
"key": key,
|
||||
@@ -782,28 +908,36 @@ func createDefaultHeadVisualeffectRow(columns []string, rowID int, key, label, m
|
||||
row[column] = nullValue
|
||||
}
|
||||
row["Label"] = label
|
||||
row["Type_FD"] = "D"
|
||||
row["OrientWithGround"] = "0"
|
||||
row["Imp_HeadCon_Node"] = modelStem
|
||||
row["OrientWithObject"] = "1"
|
||||
for column, value := range policy.RowDefaults {
|
||||
row[column] = value
|
||||
}
|
||||
for column, value := range group.RowDefaults {
|
||||
row[column] = value
|
||||
}
|
||||
for _, column := range headVisualeffectModelColumns(policy, group) {
|
||||
row[column] = modelStem
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func applyDiscoveredHeadVisualeffectDefaults(row map[string]any, columns []string, modelStem, label string) {
|
||||
func applyDiscoveredHeadVisualeffectDefaults(row map[string]any, columns []string, modelStem, label string, policy headVisualeffectsPolicy, group headVisualeffectsGroupPolicy) {
|
||||
if isNullLikeValue(row["Label"]) {
|
||||
row["Label"] = label
|
||||
}
|
||||
if isNullLikeValue(row["Type_FD"]) {
|
||||
row["Type_FD"] = "D"
|
||||
for column, value := range policy.RowDefaults {
|
||||
if isNullLikeValue(row[column]) {
|
||||
row[column] = value
|
||||
}
|
||||
}
|
||||
if isNullLikeValue(row["OrientWithGround"]) {
|
||||
row["OrientWithGround"] = "0"
|
||||
for column, value := range group.RowDefaults {
|
||||
if isNullLikeValue(row[column]) {
|
||||
row[column] = value
|
||||
}
|
||||
}
|
||||
if isNullLikeValue(row["Imp_HeadCon_Node"]) {
|
||||
row["Imp_HeadCon_Node"] = modelStem
|
||||
}
|
||||
if isNullLikeValue(row["OrientWithObject"]) {
|
||||
row["OrientWithObject"] = "1"
|
||||
for _, column := range headVisualeffectModelColumns(policy, group) {
|
||||
if isNullLikeValue(row[column]) {
|
||||
row[column] = modelStem
|
||||
}
|
||||
}
|
||||
for _, column := range columns {
|
||||
if _, ok := row[column]; !ok {
|
||||
@@ -812,6 +946,16 @@ func applyDiscoveredHeadVisualeffectDefaults(row map[string]any, columns []strin
|
||||
}
|
||||
}
|
||||
|
||||
func headVisualeffectModelColumns(policy headVisualeffectsPolicy, group headVisualeffectsGroupPolicy) []string {
|
||||
if len(group.ModelColumns) > 0 {
|
||||
return group.ModelColumns
|
||||
}
|
||||
if strings.TrimSpace(policy.ModelColumn) == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{policy.ModelColumn}
|
||||
}
|
||||
|
||||
func formatAutogenManifest(root string, producer project.AutogenProducerConfig, repo, ref string, entries []autogenManifestEntry) ([]byte, error) {
|
||||
manifest := autogenManifest{
|
||||
ID: producer.ID,
|
||||
|
||||
@@ -7278,8 +7278,10 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
|
||||
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")
|
||||
|
||||
p := testProject(root)
|
||||
@@ -7290,7 +7292,7 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
|
||||
Dataset: "visualeffects",
|
||||
Mode: "head_visualeffects",
|
||||
Root: "vfxs",
|
||||
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
|
||||
Include: []string{"head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.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",
|
||||
@@ -7312,7 +7314,7 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
|
||||
if err != nil {
|
||||
t.Fatalf("applyAutogenConsumers failed: %v", err)
|
||||
}
|
||||
if len(got) != 1 || len(got[0].Rows) != 2 {
|
||||
if len(got) != 1 || len(got[0].Rows) != 3 {
|
||||
t.Fatalf("expected autogenerated visualeffects rows, got %#v", got)
|
||||
}
|
||||
|
||||
@@ -7329,6 +7331,14 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
|
||||
t.Fatalf("unexpected head accessory defaults: %#v", bandana)
|
||||
}
|
||||
|
||||
laurel := rowByKey["visualeffects:headdecoration_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" {
|
||||
t.Fatalf("unexpected head decoration defaults: %#v", laurel)
|
||||
}
|
||||
|
||||
hair := rowByKey["visualeffects:headfeature_hair_bangs"]
|
||||
if hair == nil {
|
||||
t.Fatalf("expected head feature row, got %#v", got[0].Rows)
|
||||
@@ -7340,18 +7350,113 @@ func TestApplyAutogenConsumersAugmentsHeadVisualeffectsFromLocalOverride(t *test
|
||||
if _, ok := got[0].LockData["visualeffects:headaccessory_bandana"]; !ok {
|
||||
t.Fatalf("expected head accessory lock id, got %#v", got[0].LockData)
|
||||
}
|
||||
if _, ok := got[0].LockData["visualeffects:headdecoration_laurel"]; !ok {
|
||||
t.Fatalf("expected head decoration lock id, got %#v", got[0].LockData)
|
||||
}
|
||||
if _, ok := got[0].LockData["visualeffects:headfeature_hair_bangs"]; !ok {
|
||||
t.Fatalf("expected head feature lock id, got %#v", got[0].LockData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeUsesConfiguredHeadVisualeffectsGroups(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "src"))
|
||||
mkdirAll(t, filepath.Join(root, "build"))
|
||||
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", "head_jewels"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "visualeffects", "base.json"), `{
|
||||
"output": "visualeffects.2da",
|
||||
"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": []
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "autogen-assets", "vfxs", "chest_accessories", "tfx_sash.mdl"), "mdl\n")
|
||||
writeFile(t, filepath.Join(root, "autogen-assets", "vfxs", "head_jewels", "hfx_circlet.mdl"), "mdl\n")
|
||||
writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
|
||||
module:
|
||||
name: Test
|
||||
resref: test
|
||||
paths:
|
||||
source: src
|
||||
build: build
|
||||
topdata:
|
||||
source: topdata
|
||||
build: build/topdata
|
||||
autogen:
|
||||
consumers:
|
||||
- id: head_visualeffects
|
||||
producer: head_visualeffects
|
||||
dataset: visualeffects
|
||||
mode: head_visualeffects
|
||||
root: vfxs
|
||||
include:
|
||||
- chest_accessories/**/*.mdl
|
||||
- head_jewels/**/*.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
|
||||
local_override_root: autogen-assets
|
||||
head_visualeffects:
|
||||
groups:
|
||||
chest_accessories:
|
||||
prefix: chestaccessory
|
||||
model_columns:
|
||||
- Imp_Root_S_Node
|
||||
- Imp_Root_M_Node
|
||||
- Imp_Root_L_Node
|
||||
- Imp_Root_H_Node
|
||||
head_jewels:
|
||||
prefix: headjewel
|
||||
label_format: "{prefix}_{stem}"
|
||||
strip_model_prefixes:
|
||||
- hfx_
|
||||
- tfx_
|
||||
row_defaults:
|
||||
Type_FD: F
|
||||
OrientWithGround: "0"
|
||||
OrientWithObject: "0"
|
||||
`)
|
||||
|
||||
proj, err := project.Load(root)
|
||||
if err != nil {
|
||||
t.Fatalf("load project: %v", err)
|
||||
}
|
||||
|
||||
result, err := BuildNative(proj, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative failed: %v", err)
|
||||
}
|
||||
visualeffectsBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "visualeffects.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read visualeffects.2da: %v", err)
|
||||
}
|
||||
text := string(visualeffectsBytes)
|
||||
for _, want := range []string{
|
||||
"0\tchestaccessory_sash\tF\t0\t****\ttfx_sash\ttfx_sash\ttfx_sash\ttfx_sash\t0",
|
||||
"1\theadjewel_circlet\tF\t0\thfx_circlet\t****\t****\t****\t****\t0",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected configured visualeffects output to contain %q, got:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGenerated2DAAssetsAppendsCachedModelsFromHeadVisualeffects(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "chest_accessories"))
|
||||
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_accessories"))
|
||||
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_decorations"))
|
||||
mkdirAll(t, filepath.Join(root, "assets", "vfxs", "head_features", "hair"))
|
||||
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", "head_accessories", "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", "head_decorations", "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, "topdata", "data", "cachedmodels", "base.json"), `{
|
||||
"columns": ["Model"],
|
||||
@@ -7374,7 +7479,7 @@ func TestBuildGenerated2DAAssetsAppendsCachedModelsFromHeadVisualeffects(t *test
|
||||
ID: "cachedmodels",
|
||||
Mode: "cachedmodels_rows",
|
||||
Root: "vfxs",
|
||||
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
|
||||
Include: []string{"chest_accessories/**/*.mdl", "head_accessories/**/*.mdl", "head_decorations/**/*.mdl", "head_features/**/*.mdl"},
|
||||
Derive: project.AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
|
||||
},
|
||||
},
|
||||
@@ -7398,8 +7503,10 @@ func TestBuildGenerated2DAAssetsAppendsCachedModelsFromHeadVisualeffects(t *test
|
||||
for _, want := range []string{
|
||||
"0\tgrn_m_bone",
|
||||
"1\thfx_existing",
|
||||
"2\thfx_bandana",
|
||||
"3\thfx_hair_bangs",
|
||||
"2\ttfx_sash",
|
||||
"3\thfx_bandana",
|
||||
"4\thfx_laurel",
|
||||
"5\thfx_hair_bangs",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected generated cachedmodels output to contain %q, got:\n%s", want, text)
|
||||
|
||||
Reference in New Issue
Block a user