Automated parts sorting
This commit is contained in:
@@ -291,10 +291,34 @@ type AutogenConsumerConfig struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type PartsRowsConfig struct {
|
||||
RowDefaults map[string]string `json:"row_defaults" yaml:"row_defaults"`
|
||||
ACBonus PartsRowsACBonusConfig `json:"acbonus" yaml:"acbonus"`
|
||||
Datasets map[string]PartsRowsDatasetConfig `json:"datasets" yaml:"datasets"`
|
||||
}
|
||||
|
||||
type PartsRowsDatasetConfig struct {
|
||||
ACBonus PartsRowsACBonusPolicy `json:"acbonus" yaml:"acbonus"`
|
||||
}
|
||||
|
||||
type PartsRowsACBonusConfig struct {
|
||||
Default PartsRowsACBonusPolicy `json:"default" yaml:"default"`
|
||||
Datasets map[string]PartsRowsACBonusPolicy `json:"datasets" yaml:"datasets"`
|
||||
}
|
||||
|
||||
type PartsRowsACBonusPolicy struct {
|
||||
Strategy string `json:"strategy" yaml:"strategy"`
|
||||
MaxRowID int `json:"max_row_id" yaml:"max_row_id"`
|
||||
Divisor int `json:"divisor" yaml:"divisor"`
|
||||
Format string `json:"format" yaml:"format"`
|
||||
Value string `json:"value" yaml:"value"`
|
||||
}
|
||||
|
||||
type AutogenDeriveConfig struct {
|
||||
Kind string `json:"kind" yaml:"kind"`
|
||||
GroupFrom string `json:"group_from,omitempty" yaml:"group_from,omitempty"`
|
||||
@@ -1363,10 +1387,93 @@ func validateGeneratedConfig(cfg GeneratedConfig) []error {
|
||||
}
|
||||
failures = append(failures, validateGlobList(fieldPrefix+".autogen.include", top2da.Autogen.Include)...)
|
||||
failures = append(failures, validateAutogenDeriveConfig(fieldPrefix+".autogen.derive", top2da.Autogen.Derive)...)
|
||||
if strings.TrimSpace(top2da.Autogen.Mode) == "parts_rows" && partsRowsConfigConfigured(top2da.Autogen.PartsRows) {
|
||||
failures = append(failures, validatePartsRowsConfig(fieldPrefix+".autogen.parts_rows", top2da.Autogen.PartsRows)...)
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func partsRowsConfigConfigured(cfg PartsRowsConfig) bool {
|
||||
if len(cfg.RowDefaults) > 0 {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(cfg.ACBonus.Default.Strategy) != "" {
|
||||
return true
|
||||
}
|
||||
if len(cfg.ACBonus.Datasets) > 0 {
|
||||
return true
|
||||
}
|
||||
if len(cfg.Datasets) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validatePartsRowsConfig(fieldPrefix string, cfg PartsRowsConfig) []error {
|
||||
var failures []error
|
||||
failures = append(failures, validatePartsRowsACBonusPolicy(fieldPrefix+".acbonus.default", cfg.ACBonus.Default, false)...)
|
||||
for dataset, policy := range cfg.ACBonus.Datasets {
|
||||
dataset = strings.TrimSpace(dataset)
|
||||
if dataset == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.acbonus.datasets contains an empty dataset key", fieldPrefix))
|
||||
continue
|
||||
}
|
||||
failures = append(failures, validatePartsRowsACBonusPolicy(fieldPrefix+".acbonus.datasets."+dataset, policy, true)...)
|
||||
}
|
||||
for dataset, datasetCfg := range cfg.Datasets {
|
||||
dataset = strings.TrimSpace(dataset)
|
||||
if dataset == "" {
|
||||
failures = append(failures, fmt.Errorf("%s.datasets contains an empty dataset key", fieldPrefix))
|
||||
continue
|
||||
}
|
||||
failures = append(failures, validatePartsRowsACBonusPolicy(fieldPrefix+".datasets."+dataset+".acbonus", datasetCfg.ACBonus, true)...)
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
func validatePartsRowsACBonusPolicy(fieldPrefix string, policy PartsRowsACBonusPolicy, allowEmpty bool) []error {
|
||||
strategy := strings.TrimSpace(policy.Strategy)
|
||||
if strategy == "" {
|
||||
if allowEmpty {
|
||||
return nil
|
||||
}
|
||||
return []error{fmt.Errorf("%s.strategy is required", fieldPrefix)}
|
||||
}
|
||||
switch strategy {
|
||||
case "descending_row_id_sort_key":
|
||||
var failures []error
|
||||
if policy.MaxRowID <= 0 {
|
||||
failures = append(failures, fmt.Errorf("%s.max_row_id must be greater than 0", fieldPrefix))
|
||||
}
|
||||
if policy.Divisor <= 0 {
|
||||
failures = append(failures, fmt.Errorf("%s.divisor must be greater than 0", fieldPrefix))
|
||||
}
|
||||
if err := validatePartsRowsFormat(policy.Format); err != nil {
|
||||
failures = append(failures, fmt.Errorf("%s.format %q is invalid: %w", fieldPrefix, policy.Format, err))
|
||||
}
|
||||
return failures
|
||||
case "fixed":
|
||||
if strings.TrimSpace(policy.Value) == "" {
|
||||
return []error{fmt.Errorf("%s.value is required for fixed strategy", fieldPrefix)}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return []error{fmt.Errorf("%s.strategy %q is not supported", fieldPrefix, policy.Strategy)}
|
||||
}
|
||||
}
|
||||
|
||||
func validatePartsRowsFormat(format string) error {
|
||||
if strings.TrimSpace(format) == "" {
|
||||
return fmt.Errorf("must not be empty")
|
||||
}
|
||||
formatted := fmt.Sprintf(format, 1.25)
|
||||
if strings.Contains(formatted, "%!") {
|
||||
return fmt.Errorf("must format a numeric value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMusicConfig(cfg MusicConfig) []error {
|
||||
var failures []error
|
||||
if cfg.Defaults != nil {
|
||||
|
||||
@@ -882,6 +882,15 @@ func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) {
|
||||
Root: "part",
|
||||
Include: []string{"**/*.mdl"},
|
||||
Derive: AutogenDeriveConfig{Kind: "trailing_numeric_suffix", GroupFrom: "first_path_segment"},
|
||||
PartsRows: PartsRowsConfig{
|
||||
RowDefaults: map[string]string{"COSTMODIFIER": "0"},
|
||||
ACBonus: PartsRowsACBonusConfig{
|
||||
Default: PartsRowsACBonusPolicy{Strategy: "descending_row_id_sort_key", MaxRowID: 999, Divisor: 100, Format: "%.2f"},
|
||||
Datasets: map[string]PartsRowsACBonusPolicy{
|
||||
"chest": {Strategy: "fixed", Value: "0.00"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -934,6 +943,63 @@ func TestValidateLayoutRejectsEscapingGeneratedTopData2DAConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLayoutRejectsInvalidGeneratedPartsRowsACBonusConfig(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "assets"))
|
||||
mkdirAll(t, filepath.Join(root, "build"))
|
||||
proj := Project{
|
||||
Root: root,
|
||||
Config: Config{
|
||||
Module: ModuleConfig{Name: "Test", ResRef: "testmod"},
|
||||
Paths: PathConfig{Assets: "assets"},
|
||||
Generated: GeneratedConfig{
|
||||
TopData2DA: []GeneratedTopData2DAConfig{
|
||||
{
|
||||
ID: "parts",
|
||||
Source: "topdata",
|
||||
Output: "{paths.cache}/generated-assets/parts-2da",
|
||||
IncludeDatasets: []string{"parts/**"},
|
||||
PackageRoot: "part",
|
||||
Autogen: AutogenConsumerConfig{
|
||||
ID: "parts",
|
||||
Mode: "parts_rows",
|
||||
Root: "part",
|
||||
Include: []string{"**/*.mdl"},
|
||||
Derive: AutogenDeriveConfig{Kind: "trailing_numeric_suffix", GroupFrom: "first_path_segment"},
|
||||
PartsRows: PartsRowsConfig{
|
||||
ACBonus: PartsRowsACBonusConfig{
|
||||
Default: PartsRowsACBonusPolicy{Strategy: "descending_row_id_sort_key", MaxRowID: 0, Divisor: -1, Format: "%d"},
|
||||
Datasets: map[string]PartsRowsACBonusPolicy{
|
||||
"chest": {Strategy: "fixed"},
|
||||
"robe": {Strategy: "unknown"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := proj.ValidateLayout()
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid parts_rows acbonus config to fail")
|
||||
}
|
||||
text := err.Error()
|
||||
for _, want := range []string{
|
||||
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.default.max_row_id",
|
||||
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.default.divisor",
|
||||
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.default.format",
|
||||
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.datasets.chest.value",
|
||||
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.datasets.robe.strategy",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected validation error containing %q, got %v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLayoutRejectsDuplicateHAKNames(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mkdirAll(t, filepath.Join(root, "src"))
|
||||
|
||||
Reference in New Issue
Block a user