package topdata import ( "encoding/json" "os" "path/filepath" "slices" "strings" ) var legacyClassesPlainFamilies = []string{"feats", "skills", "savthr", "bfeat", "pres"} func importLegacyClasses(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) { legacyRoot := filepath.Join(referenceBuilderDir, "data", "classes") if _, err := os.Stat(legacyRoot); err != nil { if os.IsNotExist(err) { return 0, nil } return 0, err } targetRoot := filepath.Join(dataDir, "classes") if canonicalClassesPresent(targetRoot) { return 0, nil } coreCollected, plainCollected, err := collectLegacyClassesDatasets(legacyRoot) if err != nil { return 0, err } tableKeyByOutput := map[string]string{} for _, dataset := range plainCollected { if dataset.TableKey == "" { continue } registerLegacyClassesTableKey(tableKeyByOutput, dataset.TableKey, dataset.Dataset.OutputName) } coreRows := make([]map[string]any, 0, len(coreCollected.Rows)) for _, rawRow := range coreCollected.Rows { row, ok := deepCopyValue(rawRow).(map[string]any) if !ok { continue } if legacyTLK != nil { if _, _, err := inlineLegacyTLKValue(row, legacyTLK); err != nil { return 0, err } } rewriteLegacyClassesCoreRow(row, tableKeyByOutput) coreRows = append(coreRows, row) } if err := removePathIfExists(targetRoot); err != nil { return 0, err } if err := os.MkdirAll(filepath.Join(targetRoot, "core"), 0o755); err != nil { return 0, err } writes := []struct { path string obj map[string]any }{ { path: filepath.Join(targetRoot, "core", "base.json"), obj: map[string]any{ "output": coreCollected.Dataset.OutputName, "columns": stringSliceToAny(coreCollected.Columns), "rows": rowsToAny(coreRows), }, }, { path: filepath.Join(targetRoot, "core", "lock.json"), obj: anyMapInt(coreCollected.LockData), }, } for _, collected := range plainCollected { obj := map[string]any{ "output": collected.Dataset.OutputName, "columns": stringSliceToAny(collected.Columns), "rows": rowsToAny(collected.Rows), } if strings.TrimSpace(collected.TableKey) != "" { obj["key"] = collected.TableKey } writes = append(writes, struct { path string obj map[string]any }{ path: filepath.Join(dataDir, collected.Dataset.Name+".json"), obj: obj, }) } for _, write := range writes { if err := os.MkdirAll(filepath.Dir(write.path), 0o755); err != nil { return 0, err } raw, err := json.MarshalIndent(write.obj, "", " ") if err != nil { return 0, err } raw = append(raw, '\n') if err := os.WriteFile(write.path, raw, 0o644); err != nil { return 0, err } } return len(writes), nil } func canonicalClassesPresent(targetRoot string) bool { if !fileExists(filepath.Join(targetRoot, "core", "base.json")) || !fileExists(filepath.Join(targetRoot, "core", "lock.json")) { return false } for _, family := range legacyClassesPlainFamilies { ok, err := hasJSONFiles(filepath.Join(targetRoot, family)) if err != nil || !ok { return false } } return true } func collectLegacyClassesDatasets(legacyRoot string) (nativeCollectedDataset, []nativeCollectedDataset, error) { coreCollected, err := collectBaseDataset(nativeDataset{ Name: "classes/core", BasePath: filepath.Join(legacyRoot, "core", "base.json"), LockPath: filepath.Join(legacyRoot, "core", "lock.json"), ModulesDir: filepath.Join(legacyRoot, "core", "modules"), OutputName: "classes.2da", Spec: specForDataset("classes"), }) if err != nil { return nativeCollectedDataset{}, nil, err } coreCollected.Dataset.OutputName = "classes.2da" plainCollected := make([]nativeCollectedDataset, 0) for _, family := range legacyClassesPlainFamilies { familyDir := filepath.Join(legacyRoot, family) entries, err := os.ReadDir(familyDir) if err != nil { if os.IsNotExist(err) { continue } return nativeCollectedDataset{}, nil, err } for _, entry := range entries { if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" || strings.HasPrefix(entry.Name(), ".") || entry.Name() == "lock.json" { continue } filePath := filepath.Join(familyDir, entry.Name()) tableData, err := loadJSONObject(filePath) if err != nil { return nativeCollectedDataset{}, nil, err } outputName, _ := tableData["output"].(string) if strings.TrimSpace(outputName) == "" { outputName = strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())) + ".2da" } collected, err := collectPlainDataset(nativeDataset{ Name: filepath.ToSlash(filepath.Join("classes", family, strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())))), BasePath: filePath, LockPath: filepath.Join(familyDir, "lock.json"), OutputName: outputName, Spec: specForDataset(filepath.ToSlash(filepath.Join("classes", family, strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))))), }) if err != nil { return nativeCollectedDataset{}, nil, err } plainCollected = append(plainCollected, collected) } } slices.SortFunc(plainCollected, func(a, b nativeCollectedDataset) int { return strings.Compare(a.Dataset.Name, b.Dataset.Name) }) return coreCollected, plainCollected, nil } func registerLegacyClassesTableKey(tableKeyByOutput map[string]string, tableKey, outputName string) { trimmedOutput := strings.TrimSpace(outputName) if trimmedOutput != "" { tableKeyByOutput[trimmedOutput] = tableKey } trimmedStem := strings.TrimSpace(outputStem(outputName)) if trimmedStem != "" { tableKeyByOutput[trimmedStem] = tableKey } } func rewriteLegacyClassesCoreRow(row map[string]any, tableKeyByOutput map[string]string) { for _, field := range []string{"FeatsTable", "SavingThrowTable", "SkillsTable", "BonusFeatsTable", "PreReqTable"} { tableKey := legacyClassesTableKeyForValue(row[field], tableKeyByOutput) if tableKey == "" { continue } row[field] = map[string]any{"table": tableKey} } } func legacyClassesTableKeyForValue(value any, tableKeyByOutput map[string]string) string { switch typed := value.(type) { case map[string]any: tableKey, _ := typed["table"].(string) return strings.TrimSpace(tableKey) case string: trimmed := strings.TrimSpace(typed) if trimmed == "" || trimmed == nullValue { return "" } if trimmed != strings.ToLower(trimmed) { return "" } if tableKey, ok := tableKeyByOutput[trimmed]; ok { return tableKey } return "" default: return "" } }