Automated parts sorting
This commit is contained in:
@@ -69,7 +69,11 @@ func buildGenerated2DAAssetGroup(p *project.Project, cfg project.GeneratedTopDat
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collected, err = applyPartOverrides(sourceDir, collected)
|
||||
collected, err = normalizePartsRowsACBonus(collected, consumer.PartsRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collected, err = applyPartOverridesWithConfig(sourceDir, collected, consumer.PartsRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -127,7 +131,7 @@ func applyAutogenConsumersForGeneratedAssets(collected []nativeCollectedDataset,
|
||||
}
|
||||
switch consumer.Mode {
|
||||
case "parts_rows":
|
||||
return augmentWithAutogeneratedParts(collected, autogenPartsInventory(entries)), nil
|
||||
return augmentWithAutogeneratedPartsWithConfig(collected, autogenPartsInventory(entries), consumer.PartsRows), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported generated topdata 2DA autogen mode %q", consumer.Mode)
|
||||
}
|
||||
|
||||
@@ -212,11 +212,24 @@ func CreateDefaultPartRow(rowID int) map[string]any {
|
||||
}
|
||||
|
||||
func CreateDefaultPartRowForColumns(rowID int, columns []string) map[string]any {
|
||||
return createDefaultPartRowForDataset(rowID, "", columns, project.PartsRowsConfig{})
|
||||
}
|
||||
|
||||
func createDefaultPartRowForDataset(rowID int, datasetName string, columns []string, cfg project.PartsRowsConfig) map[string]any {
|
||||
row := map[string]any{
|
||||
"id": rowID,
|
||||
"COSTMODIFIER": "0",
|
||||
"ACBONUS": "0.00",
|
||||
}
|
||||
for field, value := range cfg.RowDefaults {
|
||||
if strings.TrimSpace(field) == "" {
|
||||
continue
|
||||
}
|
||||
row[field] = value
|
||||
}
|
||||
if value, ok := configuredPartsRowsACBonusValue(rowID, datasetName, cfg); ok {
|
||||
row["ACBONUS"] = value
|
||||
}
|
||||
for _, column := range columns {
|
||||
if strings.HasPrefix(column, "HIDE") {
|
||||
row[column] = "0"
|
||||
@@ -236,12 +249,27 @@ func isUnsetPartValue(value any) bool {
|
||||
}
|
||||
|
||||
func applyDiscoveredPartDefaults(row map[string]any, columns []string) {
|
||||
applyDiscoveredPartDefaultsWithConfig(row, "", columns, project.PartsRowsConfig{})
|
||||
}
|
||||
|
||||
func applyDiscoveredPartDefaultsWithConfig(row map[string]any, datasetName string, columns []string, cfg project.PartsRowsConfig) {
|
||||
if isUnsetPartValue(row["COSTMODIFIER"]) {
|
||||
row["COSTMODIFIER"] = "0"
|
||||
}
|
||||
for field, value := range cfg.RowDefaults {
|
||||
if strings.TrimSpace(field) == "" {
|
||||
continue
|
||||
}
|
||||
if isUnsetPartValue(row[field]) {
|
||||
row[field] = value
|
||||
}
|
||||
}
|
||||
if isUnsetPartValue(row["ACBONUS"]) {
|
||||
row["ACBONUS"] = "0.00"
|
||||
}
|
||||
if value, ok := configuredPartsRowsACBonusValue(rowIDFromPartRow(row), datasetName, cfg); ok {
|
||||
row["ACBONUS"] = value
|
||||
}
|
||||
for _, column := range columns {
|
||||
if !strings.HasPrefix(column, "HIDE") {
|
||||
continue
|
||||
@@ -252,6 +280,11 @@ func applyDiscoveredPartDefaults(row map[string]any, columns []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func rowIDFromPartRow(row map[string]any) int {
|
||||
rowID, _ := row["id"].(int)
|
||||
return rowID
|
||||
}
|
||||
|
||||
func resolvePartsRoot(root string) (string, error) {
|
||||
if strings.TrimSpace(root) == "" {
|
||||
return "", nil
|
||||
@@ -354,6 +387,10 @@ func loadPartOverrides(path string) ([]map[string]any, error) {
|
||||
}
|
||||
|
||||
func applyPartOverrides(sourceDir string, collected []nativeCollectedDataset) ([]nativeCollectedDataset, error) {
|
||||
return applyPartOverridesWithConfig(sourceDir, collected, project.PartsRowsConfig{})
|
||||
}
|
||||
|
||||
func applyPartOverridesWithConfig(sourceDir string, collected []nativeCollectedDataset, cfg project.PartsRowsConfig) ([]nativeCollectedDataset, error) {
|
||||
result := make([]nativeCollectedDataset, len(collected))
|
||||
copy(result, collected)
|
||||
for i, dataset := range result {
|
||||
@@ -393,7 +430,7 @@ func applyPartOverrides(sourceDir string, collected []nativeCollectedDataset) ([
|
||||
}
|
||||
row, ok := rowByID[rowID]
|
||||
if !ok {
|
||||
row = CreateDefaultPartRowForColumns(rowID, dataset.Columns)
|
||||
row = createDefaultPartRowForDataset(rowID, dataset.Dataset.Name, dataset.Columns, cfg)
|
||||
rows = append(rows, row)
|
||||
rowByID[rowID] = row
|
||||
}
|
||||
@@ -415,6 +452,88 @@ func applyPartOverrides(sourceDir string, collected []nativeCollectedDataset) ([
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func normalizePartsRowsACBonus(collected []nativeCollectedDataset, cfg project.PartsRowsConfig) ([]nativeCollectedDataset, error) {
|
||||
if !partsRowsACBonusConfigured(cfg) && len(cfg.RowDefaults) == 0 {
|
||||
return collected, nil
|
||||
}
|
||||
result := make([]nativeCollectedDataset, len(collected))
|
||||
copy(result, collected)
|
||||
for i, dataset := range result {
|
||||
if !strings.HasPrefix(dataset.Dataset.Name, "parts/") {
|
||||
continue
|
||||
}
|
||||
rows := make([]map[string]any, len(dataset.Rows))
|
||||
for index, row := range dataset.Rows {
|
||||
cloned := cloneRowMap(row)
|
||||
for field, value := range cfg.RowDefaults {
|
||||
if strings.TrimSpace(field) == "" {
|
||||
continue
|
||||
}
|
||||
if isUnsetPartValue(cloned[field]) {
|
||||
cloned[field] = value
|
||||
}
|
||||
}
|
||||
rowID, ok := cloned["id"].(int)
|
||||
if ok {
|
||||
if value, configured := configuredPartsRowsACBonusValue(rowID, dataset.Dataset.Name, cfg); configured {
|
||||
cloned["ACBONUS"] = value
|
||||
}
|
||||
}
|
||||
rows[index] = cloned
|
||||
}
|
||||
result[i].Rows = rows
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func partsRowsACBonusConfigured(cfg project.PartsRowsConfig) bool {
|
||||
if strings.TrimSpace(cfg.ACBonus.Default.Strategy) != "" {
|
||||
return true
|
||||
}
|
||||
if len(cfg.ACBonus.Datasets) > 0 {
|
||||
return true
|
||||
}
|
||||
for _, dataset := range cfg.Datasets {
|
||||
if strings.TrimSpace(dataset.ACBonus.Strategy) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func configuredPartsRowsACBonusValue(rowID int, datasetName string, cfg project.PartsRowsConfig) (string, bool) {
|
||||
policy, ok := partsRowsACBonusPolicyForDataset(datasetName, cfg)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
switch strings.TrimSpace(policy.Strategy) {
|
||||
case "descending_row_id_sort_key":
|
||||
format := strings.TrimSpace(policy.Format)
|
||||
if format == "" {
|
||||
format = "%.2f"
|
||||
}
|
||||
return fmt.Sprintf(format, float64(policy.MaxRowID-rowID)/float64(policy.Divisor)), true
|
||||
case "fixed":
|
||||
return policy.Value, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func partsRowsACBonusPolicyForDataset(datasetName string, cfg project.PartsRowsConfig) (project.PartsRowsACBonusPolicy, bool) {
|
||||
category := extractPartCategory(datasetName)
|
||||
if policy, ok := cfg.ACBonus.Datasets[category]; ok && strings.TrimSpace(policy.Strategy) != "" {
|
||||
return policy, true
|
||||
}
|
||||
if dataset, ok := cfg.Datasets[category]; ok && strings.TrimSpace(dataset.ACBonus.Strategy) != "" {
|
||||
return dataset.ACBonus, true
|
||||
}
|
||||
if strings.TrimSpace(cfg.ACBonus.Default.Strategy) != "" {
|
||||
return cfg.ACBonus.Default, true
|
||||
}
|
||||
return project.PartsRowsACBonusPolicy{}, false
|
||||
}
|
||||
|
||||
func loadPartOverridesForCategory(sourceDir, category string) ([]map[string]any, error) {
|
||||
overrides := []map[string]any{}
|
||||
|
||||
@@ -503,6 +622,10 @@ func normalizePartOverrideValue(value any) any {
|
||||
// retain authored values unless they still use placeholder values, in which case
|
||||
// discovery activates them with engine-visible defaults.
|
||||
func augmentWithAutogeneratedParts(collected []nativeCollectedDataset, autogenerated map[string]map[int]struct{}) []nativeCollectedDataset {
|
||||
return augmentWithAutogeneratedPartsWithConfig(collected, autogenerated, project.PartsRowsConfig{})
|
||||
}
|
||||
|
||||
func augmentWithAutogeneratedPartsWithConfig(collected []nativeCollectedDataset, autogenerated map[string]map[int]struct{}, cfg project.PartsRowsConfig) []nativeCollectedDataset {
|
||||
if len(autogenerated) == 0 {
|
||||
return collected
|
||||
}
|
||||
@@ -538,11 +661,11 @@ func augmentWithAutogeneratedParts(collected []nativeCollectedDataset, autogener
|
||||
|
||||
for rowID := range ids {
|
||||
if row, exists := existingRows[rowID]; exists {
|
||||
applyDiscoveredPartDefaults(row, dataset.Columns)
|
||||
applyDiscoveredPartDefaultsWithConfig(row, dataset.Dataset.Name, dataset.Columns, cfg)
|
||||
continue
|
||||
}
|
||||
if _, exists := existingRows[rowID]; !exists {
|
||||
newRow := CreateDefaultPartRowForColumns(rowID, dataset.Columns)
|
||||
newRow := createDefaultPartRowForDataset(rowID, dataset.Dataset.Name, dataset.Columns, cfg)
|
||||
newRows = append(newRows, newRow)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10525,6 +10525,72 @@ func TestAugmentWithAutogeneratedPartsRespectsExistingIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePartsRowsACBonusUsesConfiguredPolicyBeforeOverrides(t *testing.T) {
|
||||
collected := []nativeCollectedDataset{
|
||||
{
|
||||
Dataset: nativeDataset{Name: "parts/belt", OutputName: "parts_belt.2da"},
|
||||
Columns: []string{"COSTMODIFIER", "ACBONUS"},
|
||||
Rows: []map[string]any{
|
||||
{"id": 1, "COSTMODIFIER": "5", "ACBONUS": "0.90"},
|
||||
{"id": 999, "COSTMODIFIER": "0", "ACBONUS": "****"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Dataset: nativeDataset{Name: "parts/chest", OutputName: "parts_chest.2da"},
|
||||
Columns: []string{"COSTMODIFIER", "ACBONUS"},
|
||||
Rows: []map[string]any{
|
||||
{"id": 14, "COSTMODIFIER": "0", "ACBONUS": "8.00"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Dataset: nativeDataset{Name: "parts/robe", OutputName: "parts_robe.2da"},
|
||||
Columns: []string{"COSTMODIFIER", "ACBONUS"},
|
||||
Rows: []map[string]any{
|
||||
{"id": 95, "COSTMODIFIER": "0", "ACBONUS": "0.00"},
|
||||
},
|
||||
},
|
||||
}
|
||||
cfg := project.PartsRowsConfig{
|
||||
RowDefaults: map[string]string{"COSTMODIFIER": "0"},
|
||||
ACBonus: project.PartsRowsACBonusConfig{
|
||||
Default: project.PartsRowsACBonusPolicy{Strategy: "descending_row_id_sort_key", MaxRowID: 999, Divisor: 100, Format: "%.2f"},
|
||||
Datasets: map[string]project.PartsRowsACBonusPolicy{
|
||||
"chest": {Strategy: "fixed", Value: "0.00"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := normalizePartsRowsACBonus(collected, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizePartsRowsACBonus failed: %v", err)
|
||||
}
|
||||
|
||||
beltRows := rowsByID(got[0].Rows)
|
||||
if got := beltRows[1]["ACBONUS"]; got != "9.98" {
|
||||
t.Fatalf("expected authored belt ACBONUS to be replaced with computed sort key 9.98, got %v", got)
|
||||
}
|
||||
if got := beltRows[999]["ACBONUS"]; got != "0.00" {
|
||||
t.Fatalf("expected highest belt row to sort at 0.00, got %v", got)
|
||||
}
|
||||
chestRows := rowsByID(got[1].Rows)
|
||||
if got := chestRows[14]["ACBONUS"]; got != "0.00" {
|
||||
t.Fatalf("expected chest ACBONUS to be forced to 0.00, got %v", got)
|
||||
}
|
||||
robeRows := rowsByID(got[2].Rows)
|
||||
if got := robeRows[95]["ACBONUS"]; got != "9.04" {
|
||||
t.Fatalf("expected robe to use computed non-chest sort key, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func rowsByID(rows []map[string]any) map[int]map[string]any {
|
||||
out := map[int]map[string]any{}
|
||||
for _, row := range rows {
|
||||
id, _ := row["id"].(int)
|
||||
out[id] = row
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestAugmentWithAutogeneratedPartsSortsRows(t *testing.T) {
|
||||
collected := []nativeCollectedDataset{
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user