Native topdata migration summary - Replaced the legacy 2dabuilder runtime path with the native topdata builder. - Native build, validate, compare, and convert flows now cover the canonical topdata pipeline. - compare-topdata is now a native self-check; legacy reference-builder runtime usage was removed. - Parts generation follows the native asset-scan contract and uses sow-assets via project asset resolution. - Generated feat families, class-feat injects, masterfeat/successor expansion, and dataset-derived feat generation were brought to parity and regression-covered. - Remaining mirrored datasets were migrated into native-owned canonical data while preserving lock IDs. - normalize-topdata bridge behavior and migration-era ownership markers were removed. - Documentation was rewritten around the native workflow and active contracts were cleaned up. - Final hardening pass removed stale validator assumptions and cleaned ignored/generated artifacts for PR readiness. Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/2 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
416 lines
11 KiB
Go
416 lines
11 KiB
Go
package topdata
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
|
)
|
|
|
|
type NormalizeResult struct {
|
|
StatePath string
|
|
ScannedFiles int
|
|
UpdatedFiles int
|
|
InlineValues int
|
|
RemainingFiles int
|
|
RemainingLegacyRefs int
|
|
}
|
|
|
|
func NormalizeProject(p *project.Project) (NormalizeResult, error) {
|
|
if !p.HasTopData() {
|
|
return NormalizeResult{}, fmt.Errorf("topdata is not configured for this project")
|
|
}
|
|
|
|
sourceDir := p.TopDataSourceDir()
|
|
dataDir := filepath.Join(sourceDir, "data")
|
|
migrationRoot := resolveMigrationInputRoot(sourceDir, p.TopDataReferenceBuilderDir())
|
|
legacyTLK, err := loadLegacyTLK(filepath.Join(sourceDir, "tlk"))
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
if migrationRoot != "" {
|
|
migrationTLK, err := loadLegacyTLK(filepath.Join(migrationRoot, "tlk"))
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
legacyTLK = mergeLegacyTLK(migrationTLK, legacyTLK)
|
|
}
|
|
baseDialogImported, err := importLegacyBaseDialog(migrationRoot, sourceDir)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
|
|
itempropsImported, err := importLegacyItempropsRegistry(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
masterfeatsImported, err := importLegacyMasterfeats(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
featImported, err := importLegacyFeat(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
creaturespeedImported, err := importLegacyCreaturespeed(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
armorImported, err := importLegacyArmor(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
baseitemsImported, err := importLegacyBaseitems(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
damagetypesImported, err := importLegacyDamagetypes(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
skyboxesImported, err := importLegacySkyboxes(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
vfxPersistentImported, err := importLegacyVFXPersistent(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
loadscreensImported, err := importLegacyLoadscreens(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
progfxImported, err := importLegacyProgfx(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
genericdoorsImported, err := importLegacyGenericdoors(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
cloakmodelImported, err := importLegacyCloakmodel(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
repadjustImported, err := importLegacyRepadjust(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
wingmodelImported, err := importLegacyWingmodel(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
tailmodelImported, err := importLegacyTailmodel(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
doortypesImported, err := importLegacyDoortypes(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
visualeffectsImported, err := importLegacyVisualeffects(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
appearanceImported, err := importLegacyAppearance(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
placeablesImported, err := importLegacyPlaceables(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
rulesetImported, err := importLegacyRuleset(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
skillsImported, err := importLegacySkills(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
spellsImported, err := importLegacySpells(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
racialtypesImported, err := importLegacyRacialtypes(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
classesImported, err := importLegacyClasses(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
portraitsImported, err := importLegacyPortraits(migrationRoot, dataDir, legacyTLK)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
|
|
paths, err := collectDataJSONPaths(dataDir)
|
|
if err != nil {
|
|
return NormalizeResult{}, err
|
|
}
|
|
|
|
result := NormalizeResult{
|
|
StatePath: filepath.Join(sourceDir, tlkStateFile),
|
|
ScannedFiles: len(paths),
|
|
UpdatedFiles: baseDialogImported + itempropsImported + masterfeatsImported + featImported + creaturespeedImported + armorImported + baseitemsImported + damagetypesImported + skyboxesImported + vfxPersistentImported + loadscreensImported + progfxImported + genericdoorsImported + cloakmodelImported + repadjustImported + wingmodelImported + tailmodelImported + doortypesImported + visualeffectsImported + appearanceImported + placeablesImported + rulesetImported + skillsImported + spellsImported + racialtypesImported + classesImported + portraitsImported,
|
|
}
|
|
state, err := loadTLKState(result.StatePath)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if state.Entries == nil {
|
|
state.Entries = map[string]tlkStateMapping{}
|
|
}
|
|
if legacyTLK != nil && state.Language == "" {
|
|
state.Language = legacyTLK.Language
|
|
}
|
|
if legacyTLK != nil {
|
|
for key, id := range legacyTLK.Lock {
|
|
if _, ok := state.Entries[key]; !ok {
|
|
state.Entries[key] = tlkStateMapping{ID: id}
|
|
}
|
|
}
|
|
}
|
|
|
|
if legacyTLK != nil {
|
|
for _, path := range paths {
|
|
updated, count, err := inlineLegacyTLKFile(path, legacyTLK)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if updated {
|
|
result.UpdatedFiles++
|
|
result.InlineValues += count
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := saveTLKState(result.StatePath, state); err != nil {
|
|
return result, err
|
|
}
|
|
if err := removeLegacyTLKAuthoredInput(filepath.Join(sourceDir, "tlk")); err != nil {
|
|
return result, err
|
|
}
|
|
remainingFiles, remainingRefs, err := countLegacyTLKRefs(paths)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.RemainingFiles = remainingFiles
|
|
result.RemainingLegacyRefs = remainingRefs
|
|
return result, nil
|
|
}
|
|
|
|
func mergeLegacyTLK(base, override *legacyTLKData) *legacyTLKData {
|
|
if base == nil && override == nil {
|
|
return nil
|
|
}
|
|
merged := &legacyTLKData{
|
|
Language: "en",
|
|
Entries: map[string]tlkEntryData{},
|
|
Lock: map[string]int{},
|
|
}
|
|
if base != nil {
|
|
if base.Language != "" {
|
|
merged.Language = base.Language
|
|
}
|
|
for key, entry := range base.Entries {
|
|
merged.Entries[key] = entry
|
|
}
|
|
for key, id := range base.Lock {
|
|
merged.Lock[key] = id
|
|
}
|
|
}
|
|
if override != nil {
|
|
if override.Language != "" {
|
|
merged.Language = override.Language
|
|
}
|
|
for key, entry := range override.Entries {
|
|
merged.Entries[key] = entry
|
|
}
|
|
for key, id := range override.Lock {
|
|
merged.Lock[key] = id
|
|
}
|
|
}
|
|
return merged
|
|
}
|
|
|
|
func removeLegacyTLKAuthoredInput(tlkDir string) error {
|
|
modulesDir := filepath.Join(tlkDir, "modules")
|
|
if err := os.RemoveAll(modulesDir); err != nil {
|
|
return fmt.Errorf("remove %s: %w", modulesDir, err)
|
|
}
|
|
lockPath := filepath.Join(tlkDir, "lock.json")
|
|
if err := os.Remove(lockPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove %s: %w", lockPath, err)
|
|
}
|
|
gitkeepPath := filepath.Join(tlkDir, ".gitkeep")
|
|
if err := os.Remove(gitkeepPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("remove %s: %w", gitkeepPath, err)
|
|
}
|
|
if err := os.Remove(tlkDir); err != nil && !os.IsNotExist(err) {
|
|
if !strings.Contains(err.Error(), "directory not empty") {
|
|
return fmt.Errorf("remove %s: %w", tlkDir, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resolveMigrationInputRoot(sourceDir, referenceBuilderDir string) string {
|
|
if strings.TrimSpace(referenceBuilderDir) != "" {
|
|
return referenceBuilderDir
|
|
}
|
|
snapshotRoot := filepath.Join(sourceDir, "migration_snapshot")
|
|
if info, err := os.Stat(snapshotRoot); err == nil && info.IsDir() {
|
|
return snapshotRoot
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func inlineLegacyTLKFile(path string, legacy *legacyTLKData) (bool, int, error) {
|
|
obj, err := loadJSONObject(path)
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
updated, count, err := inlineLegacyTLKValue(obj, legacy)
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
if !updated {
|
|
return false, 0, nil
|
|
}
|
|
raw, err := json.MarshalIndent(obj, "", " ")
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
raw = append(raw, '\n')
|
|
if err := os.WriteFile(path, raw, 0o644); err != nil {
|
|
return false, 0, err
|
|
}
|
|
return true, count, nil
|
|
}
|
|
|
|
func inlineLegacyTLKValue(value any, legacy *legacyTLKData) (bool, int, error) {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
if rawTLK, ok := typed["tlk"]; ok {
|
|
key, ok := rawTLK.(string)
|
|
if ok {
|
|
entry, ok := legacy.Entries[key]
|
|
if !ok {
|
|
return false, 0, nil
|
|
}
|
|
payload := map[string]any{
|
|
"key": key,
|
|
"text": entry.Text,
|
|
}
|
|
if entry.SoundResRef != "" {
|
|
payload["sound_resref"] = entry.SoundResRef
|
|
}
|
|
if entry.SoundLength != 0 {
|
|
payload["sound_length"] = entry.SoundLength
|
|
}
|
|
typed["tlk"] = payload
|
|
return true, 1, nil
|
|
}
|
|
}
|
|
|
|
updated := false
|
|
total := 0
|
|
for key, child := range typed {
|
|
childUpdated, childCount, err := inlineLegacyTLKValue(child, legacy)
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
if childUpdated {
|
|
typed[key] = child
|
|
updated = true
|
|
total += childCount
|
|
}
|
|
}
|
|
return updated, total, nil
|
|
case []any:
|
|
updated := false
|
|
total := 0
|
|
for index, child := range typed {
|
|
childUpdated, childCount, err := inlineLegacyTLKValue(child, legacy)
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
if childUpdated {
|
|
typed[index] = child
|
|
updated = true
|
|
total += childCount
|
|
}
|
|
}
|
|
return updated, total, nil
|
|
default:
|
|
return false, 0, nil
|
|
}
|
|
}
|
|
|
|
func collectDataJSONPaths(dataDir string) ([]string, error) {
|
|
paths := make([]string, 0)
|
|
err := filepath.WalkDir(dataDir, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(strings.ToLower(d.Name()), ".json") && d.Name() != "lock.json" {
|
|
paths = append(paths, path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
func countLegacyTLKRefs(paths []string) (int, int, error) {
|
|
files := 0
|
|
refs := 0
|
|
for _, path := range paths {
|
|
obj, err := loadJSONObject(path)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
count := countLegacyTLKRefsInValue(obj)
|
|
if count == 0 {
|
|
continue
|
|
}
|
|
files++
|
|
refs += count
|
|
}
|
|
return files, refs, nil
|
|
}
|
|
|
|
func countLegacyTLKRefsInValue(value any) int {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
total := 0
|
|
if rawTLK, ok := typed["tlk"]; ok {
|
|
if _, ok := rawTLK.(string); ok {
|
|
total++
|
|
}
|
|
}
|
|
for _, child := range typed {
|
|
total += countLegacyTLKRefsInValue(child)
|
|
}
|
|
return total
|
|
case []any:
|
|
total := 0
|
|
for _, child := range typed {
|
|
total += countLegacyTLKRefsInValue(child)
|
|
}
|
|
return total
|
|
default:
|
|
return 0
|
|
}
|
|
}
|