package topdata import ( "fmt" "os" "path/filepath" "strings" "gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project" ) type Generated2DAAsset struct { Rel string SourcePath string } func BuildGenerated2DAAssets(p *project.Project, progress func(string)) ([]Generated2DAAsset, error) { if progress == nil { progress = func(string) {} } configs := p.EffectiveConfig().Generated.TopData2DA if len(configs) == 0 { return nil, nil } results := make([]Generated2DAAsset, 0) for _, cfg := range configs { progress(fmt.Sprintf("Building generated 2DA assets for %s...", cfg.ID)) generated, err := buildGenerated2DAAssetGroup(p, cfg, progress) if err != nil { return nil, err } results = append(results, generated...) } return results, nil } func buildGenerated2DAAssetGroup(p *project.Project, cfg project.GeneratedTopData2DAConfig, progress func(string)) ([]Generated2DAAsset, error) { sourceDir := filepath.Join(p.Root, filepath.FromSlash(strings.TrimSpace(cfg.Source))) dataDir := filepath.Join(sourceDir, "data") outputDir := filepath.Join(p.Root, filepath.FromSlash(strings.TrimSpace(cfg.Output))) datasets, err := discoverNativeDatasets(dataDir) if err != nil { return nil, fmt.Errorf("discover generated topdata 2DA datasets %s: %w", cfg.ID, err) } selected := make([]nativeDataset, 0, len(datasets)) for _, dataset := range datasets { if generatedDatasetIncluded(dataset.Name, cfg.IncludeDatasets) { selected = append(selected, dataset) } } if len(selected) == 0 { return nil, fmt.Errorf("generated topdata 2DA config %s matched no datasets under %s", cfg.ID, dataDir) } collected := make([]nativeCollectedDataset, 0, len(selected)) for _, dataset := range selected { collectedDataset, err := collectNativeDataset(dataset) if err != nil { return nil, err } collected = append(collected, collectedDataset) } consumer := cfg.Autogen consumer.LocalOverrideRoot = p.AssetsDir() collected, err = applyAutogenConsumersForGeneratedAssets(collected, consumer, progress) if err != nil { return nil, err } 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 } if err := rejectGenerated2DATLKValues(cfg.ID, collected); err != nil { return nil, err } tableRegistry, err := newResolvedTableRegistry(collected) if err != nil { return nil, err } keyToID, rowByKey := generated2DAGlobalRows(collected) if err := os.RemoveAll(outputDir); err != nil { return nil, fmt.Errorf("clean generated topdata 2DA output %s: %w", outputDir, err) } if err := os.MkdirAll(outputDir, 0o755); err != nil { return nil, fmt.Errorf("create generated topdata 2DA output %s: %w", outputDir, err) } results := make([]Generated2DAAsset, 0, len(collected)) for _, dataset := range collected { compiled, err := resolveNativeDataset(dataset, keyToID, rowByKey, tableRegistry, nil) if err != nil { return nil, err } outputPath := filepath.Join(outputDir, dataset.Dataset.OutputName) denseRows := dataset.Dataset.Kind == nativeDatasetBase || (consumer.Mode == "parts_rows" && isPartsDataset(dataset.Dataset.Name)) if err := write2DA(compiled, outputPath, denseRows); err != nil { return nil, err } results = append(results, Generated2DAAsset{ Rel: filepath.ToSlash(filepath.Join(cfg.PackageRoot, dataset.Dataset.OutputName)), SourcePath: outputPath, }) } return results, nil } func applyAutogenConsumersForGeneratedAssets(collected []nativeCollectedDataset, consumer project.AutogenConsumerConfig, progress func(string)) ([]nativeCollectedDataset, error) { if strings.TrimSpace(consumer.Mode) == "" { return collected, nil } if progress == nil { progress = func(string) {} } if !autogenConsumerTargetsCollectedDataset(collected, consumer) { return collected, nil } if progress != nil { progress(fmt.Sprintf("Scanning local generated 2DA autogen input for %s from %s...", consumer.ID, consumer.LocalOverrideRoot)) } entries, err := scanLocalAutogenEntries(consumer.LocalOverrideRoot, consumer) if err != nil { return nil, err } switch consumer.Mode { case "parts_rows": return augmentWithAutogeneratedPartsWithConfig(collected, autogenPartsInventory(entries), consumer.PartsRows), nil default: return nil, fmt.Errorf("unsupported generated topdata 2DA autogen mode %q", consumer.Mode) } } func generatedDatasetIncluded(name string, patterns []string) bool { name = filepath.ToSlash(strings.TrimSpace(name)) for _, pattern := range patterns { pattern = filepath.ToSlash(strings.TrimSpace(pattern)) if pattern == "" { continue } if pattern == name { return true } if strings.HasSuffix(pattern, "/**") { prefix := strings.TrimSuffix(pattern, "/**") if name == prefix || strings.HasPrefix(name, prefix+"/") { return true } } } return false } func generated2DAGlobalRows(collected []nativeCollectedDataset) (map[string]int, map[string]map[string]any) { keyToID := map[string]int{} rowByKey := map[string]map[string]any{} for _, dataset := range collected { for key, rowID := range dataset.LockData { keyToID[key] = rowID } for _, row := range dataset.Rows { key, _ := row["key"].(string) if key == "" { continue } if rowID, ok := row["id"].(int); ok { keyToID[key] = rowID rowByKey[key] = row } } } return keyToID, rowByKey } func rejectGenerated2DATLKValues(configID string, collected []nativeCollectedDataset) error { for _, dataset := range collected { for _, row := range dataset.Rows { for field, value := range row { if field == "id" || field == "key" { continue } allowBare := columnMatchesSpec(dataset.Dataset.Spec, field) if _, ok, err := parseTLKPayload(value, allowBare); err != nil { return fmt.Errorf("generated topdata 2DA config %s dataset %s field %s has invalid TLK payload: %w", configID, dataset.Dataset.Name, field, err) } else if ok { return fmt.Errorf("generated topdata 2DA config %s dataset %s field %s uses TLK-backed text; 2DA-only generated asset builds do not generate TLK output", configID, dataset.Dataset.Name, field) } } } } return nil }