Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e509b7a90a | ||
|
|
0d12674838 | ||
|
|
bc8fa3a6fe |
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: data-driven-design
|
||||
description: Use when adding a mode, switch case, config entry, special-cased name, or per-column/per-dataset handler to a tool that processes authored data — or when a tool "knows" a project's layout, dataset names, key prefixes, or format quirks and could not be open sourced as-is.
|
||||
---
|
||||
|
||||
# Data-Driven Design
|
||||
|
||||
The tool implements **general mechanisms**; the data declares **all specific knowledge**. Every time code names a thing that lives in data — a dataset, a column, a format quirk — the tool stops being a tool and becomes a private extension of one project's layout.
|
||||
|
||||
**The test:** could this repo be open sourced and build a *different* project's data without code changes? Every hardcoded name, mode, and special case is a "no".
|
||||
|
||||
## The two smells
|
||||
|
||||
**1. The mode zoo.** A new data shape arrives and you add a named mode plus a switch case:
|
||||
|
||||
```yaml
|
||||
- {column: AvailableHeadsMale, mode: external_hex_list, hex_width: 3}
|
||||
- {column: PreferredAlignments, mode: alignment_set_mask, hex_width: 3}
|
||||
- {column: ProficiencyFeats, mode: id_hex_list, hex_width: 4}
|
||||
```
|
||||
|
||||
Three "modes" are one concept — *a set of ids with an engine-side encoding* — wearing three names. Each mode is code the data could have been. The fix is a smaller vocabulary of orthogonal primitives declared where the column is defined:
|
||||
|
||||
```json
|
||||
"EquipSlots": {"shape": "id_set", "encode": "bitmask", "max": 31}
|
||||
```
|
||||
|
||||
One parser for `id_set`, one encoder per encoding. Adding the next column is a data edit, zero code. The mode zoo *shrinks*: N special cases collapse into shape × encoding.
|
||||
|
||||
**2. Names in code.** `datasetRows("skills")`, `case "skills":`, `TrimPrefix(key, "skills:")` — the tool assumes the project's layout. When data moves (`skills` → `skills/core`), the tool breaks even though the data is self-consistent. Names must flow *from* the data: the spec that references a dataset names it; a key's namespace is whatever precedes its own colon. If code needs a name, some piece of data already knows it — read it from there.
|
||||
|
||||
## Deciding
|
||||
|
||||
Ask of every constant, case, and config knob: **whose knowledge is this?**
|
||||
|
||||
- *The mechanism's* (how to parse JSON, allocate rows, write a table) → code.
|
||||
- *The data's* (which columns exist, how the engine encodes them, what things are called) → data, even if code is faster to write today.
|
||||
|
||||
Generalize only when it deletes: a primitive that replaces N modes is lazy; a framework "for future shapes" is not. One new shape may take its case *if* the case is spelled as a reusable primitive the next shape can declare.
|
||||
|
||||
## Rationalizations
|
||||
|
||||
| Excuse | Reality |
|
||||
|---|---|
|
||||
| "Just follow the existing pattern, it's proven" | The pattern *is* the defect. Each repetition raises the cost of the real fix. |
|
||||
| "N cases isn't a crisis yet" | The crisis is per-case: every one couples the tool to one project forever. |
|
||||
| "Release is tonight, generalize later" | Later never comes; tonight's mode is tomorrow's baseline. Declaring shape in data is usually the *same* hour of work. |
|
||||
| "This name never changes" | Today's rename broke exactly such a name. Data moves; mechanisms shouldn't care. |
|
||||
|
||||
## Red flags — stop and re-shape
|
||||
|
||||
- A config entry that names both a dataset and a column to select behavior
|
||||
- A new value in a `mode`/`kind`/`type` string enum with its own switch arm
|
||||
- A literal in code that also appears in the data tree (`"skills"`, `"skills:"`)
|
||||
- Per-project constants (row ranges, hex widths, id ceilings) living in Go instead of the dataset that owns them
|
||||
@@ -1,96 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyAppearance(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "appearance")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "appearance")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
moduleNames := []string{
|
||||
"cotblreaver.json",
|
||||
"crawlingclaw.json",
|
||||
"halfogre.json",
|
||||
"zombieknight.json",
|
||||
}
|
||||
targetModulePaths := make([]string, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
targetModulePaths = append(targetModulePaths, filepath.Join(targetModulesDir, name))
|
||||
}
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
allModulesPresent := true
|
||||
for _, path := range targetModulePaths {
|
||||
if !fileExists(path) {
|
||||
allModulesPresent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allModulesPresent {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "appearance.2da"
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
moduleObjs := make([]map[string]any, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
obj, err := loadJSONObject(filepath.Join(legacyDir, "modules", name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
moduleObjs = append(moduleObjs, obj)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
}
|
||||
for i, path := range targetModulePaths {
|
||||
writes = append(writes, struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{path: path, obj: moduleObjs[i]})
|
||||
}
|
||||
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func importLegacyArmor(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "armor")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "armor")
|
||||
targetPath := filepath.Join(targetDir, "armor.json")
|
||||
if _, err := os.Stat(targetPath); err == nil {
|
||||
obj, err := loadJSONObject(targetPath)
|
||||
if err == nil && countLegacyTLKRefsInValue(obj) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rows, err := mergeArmorRows(baseObj, filepath.Join(legacyDir, "modules"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "base.json")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "lock.json")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "modules")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
out := map[string]any{
|
||||
"output": "armor.2da",
|
||||
"columns": baseObj["columns"],
|
||||
"rows": rows,
|
||||
}
|
||||
raw, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
if err := os.WriteFile(targetPath, raw, 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func mergeArmorRows(baseObj map[string]any, modulesDir string) ([]any, error) {
|
||||
rawRows, ok := baseObj["rows"].([]any)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rows := make([]map[string]any, 0, len(rawRows))
|
||||
byID := map[int]map[string]any{}
|
||||
for _, raw := range rawRows {
|
||||
row, ok := deepCopyValue(raw).(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(row, "key")
|
||||
rawID, ok := row["id"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, err := asInt(rawID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row["id"] = id
|
||||
rows = append(rows, row)
|
||||
byID[id] = row
|
||||
}
|
||||
|
||||
modulePaths, err := collectModulePaths(modulesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, path := range modulePaths {
|
||||
obj, err := loadJSONObject(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entries, ok := obj["entries"].(map[string]any); ok {
|
||||
for _, raw := range entries {
|
||||
row, ok := deepCopyValue(raw).(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(row, "key")
|
||||
rawID, ok := row["id"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, err := asInt(rawID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row["id"] = id
|
||||
rows = append(rows, row)
|
||||
byID[id] = row
|
||||
}
|
||||
}
|
||||
if overrides, ok := obj["overrides"].([]any); ok {
|
||||
for _, raw := range overrides {
|
||||
override, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rawID, ok := override["id"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, err := asInt(rawID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row := byID[id]
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
for key, value := range override {
|
||||
if key == "id" || key == "key" || key == "_tlk" || isMetadataField(key) {
|
||||
continue
|
||||
}
|
||||
row[key] = deepCopyValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i]["id"].(int) < rows[j]["id"].(int)
|
||||
})
|
||||
out := make([]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyBaseDialog(referenceBuilderDir, sourceDir string) (int, error) {
|
||||
legacyPath := filepath.Join(referenceBuilderDir, "tlk", "base.json")
|
||||
if _, err := os.Stat(legacyPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(sourceDir, "base_dialog.json")
|
||||
if fileExists(targetPath) {
|
||||
current, err := loadJSONObject(targetPath)
|
||||
if err == nil {
|
||||
if entries, ok := current["entries"].(map[string]any); ok && len(entries) > 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obj, err := loadJSONObject(legacyPath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
raw, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
if err := os.WriteFile(targetPath, raw, 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyBaseitems(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "baseitems")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "baseitems")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
moduleNames := []string{
|
||||
"blunderbuss.json",
|
||||
"coin.json",
|
||||
"estoc.json",
|
||||
"heavymace.json",
|
||||
"maulandfalchion.json",
|
||||
"ovr_baseitems.json",
|
||||
"ovr_cloak255.json",
|
||||
"ovr_helmet255.json",
|
||||
"shortspear.json",
|
||||
}
|
||||
targetModulePaths := make([]string, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
targetModulePaths = append(targetModulePaths, filepath.Join(targetModulesDir, name))
|
||||
}
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
allModulesPresent := true
|
||||
for _, path := range targetModulePaths {
|
||||
if !fileExists(path) {
|
||||
allModulesPresent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allModulesPresent {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "baseitems.2da"
|
||||
canonicalizeWikiMetadataDocument(baseObj)
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
moduleObjs := make([]map[string]any, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
obj, err := loadJSONObject(filepath.Join(legacyDir, "modules", name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
canonicalizeWikiMetadataDocument(obj)
|
||||
moduleObjs = append(moduleObjs, obj)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
}
|
||||
for index, path := range targetModulePaths {
|
||||
writes = append(writes, struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
path: path,
|
||||
obj: moduleObjs[index],
|
||||
})
|
||||
}
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package topdata
|
||||
|
||||
func importLegacyCloakmodel(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
return importLegacyDatasetMirror(referenceBuilderDir, dataDir, "cloakmodel", "cloakmodel.2da", nil)
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyCreaturespeed(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "creaturespeed")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "creaturespeed")
|
||||
targetPath := filepath.Join(targetDir, "creaturespeed.json")
|
||||
if _, err := os.Stat(targetPath); err == nil {
|
||||
obj, err := loadJSONObject(targetPath)
|
||||
if err == nil && countLegacyTLKRefsInValue(obj) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rows, err := mergeCreaturespeedRows(baseObj, filepath.Join(legacyDir, "modules"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "base.json")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "lock.json")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "modules")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
out := map[string]any{
|
||||
"output": "creaturespeed.2da",
|
||||
"columns": baseObj["columns"],
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
raw, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
if err := os.WriteFile(targetPath, raw, 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func mergeCreaturespeedRows(baseObj map[string]any, modulesDir string) ([]any, error) {
|
||||
rawRows, ok := baseObj["rows"].([]any)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(rawRows))
|
||||
byID := map[int]map[string]any{}
|
||||
for _, raw := range rawRows {
|
||||
row, ok := deepCopyValue(raw).(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(row, "key")
|
||||
rows = append(rows, row)
|
||||
if rawID, ok := row["id"]; ok {
|
||||
id, err := asInt(rawID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byID[id] = row
|
||||
}
|
||||
}
|
||||
|
||||
modulePaths, err := collectModulePaths(modulesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, path := range modulePaths {
|
||||
obj, err := loadJSONObject(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
overrideList, ok := obj["overrides"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, raw := range overrideList {
|
||||
override, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rawID, ok := override["id"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
id, err := asInt(rawID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row := byID[id]
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
for key, value := range override {
|
||||
if key == "id" || key == "key" || key == "_tlk" {
|
||||
continue
|
||||
}
|
||||
row[key] = deepCopyValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func removePathIfExists(path string) error {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func importLegacyDamagetypes(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyRoot := filepath.Join(referenceBuilderDir, "data", "damagetypes")
|
||||
if _, err := os.Stat(legacyRoot); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "damagetypes", "registry")
|
||||
targetPath := filepath.Join(targetDir, "types.json")
|
||||
if _, err := os.Stat(targetPath); err == nil {
|
||||
obj, err := loadJSONObject(targetPath)
|
||||
if err == nil && countLegacyTLKRefsInValue(obj) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
coreBase, err := loadJSONObject(filepath.Join(legacyRoot, "core", "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
groupBase, err := loadJSONObject(filepath.Join(legacyRoot, "groups", "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
hitvisualBase, err := loadJSONObject(filepath.Join(legacyRoot, "hitvisual", "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
coreModule, err := loadJSONObject(filepath.Join(legacyRoot, "core", "modules", "add_eos.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
coreLock, err := loadLockfile(filepath.Join(legacyRoot, "core", "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
groupModule, err := loadJSONObject(filepath.Join(legacyRoot, "groups", "modules", "add_eos.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
groupLock, err := loadLockfile(filepath.Join(legacyRoot, "groups", "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
hitvisualModule, err := loadJSONObject(filepath.Join(legacyRoot, "hitvisual", "modules", "add_eos.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
hitvisualLock, err := loadLockfile(filepath.Join(legacyRoot, "hitvisual", "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
rows, lockData, err := mergeLegacyDamagetypes(coreBase, groupBase, hitvisualBase, coreModule, coreLock, groupModule, groupLock, hitvisualModule, hitvisualLock)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(dataDir, "damagetypes", "core")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(dataDir, "damagetypes", "groups")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(dataDir, "damagetypes", "hitvisual")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
typesOut := map[string]any{"rows": rows}
|
||||
raw, err := json.MarshalIndent(typesOut, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
if err := os.WriteFile(targetPath, raw, 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := saveLockfile(filepath.Join(targetDir, damagetypesRegistryLock), lockData); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 2, nil
|
||||
}
|
||||
|
||||
func mergeLegacyDamagetypes(coreBase, groupBase, hitvisualBase, coreModule map[string]any, coreLock map[string]int, groupModule map[string]any, groupLock map[string]int, hitvisualModule map[string]any, hitvisualLock map[string]int) ([]map[string]any, map[string]int, error) {
|
||||
coreRows, err := coerceRowMapSlice(coreBase["rows"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
groupRows, err := coerceRowMapSlice(groupBase["rows"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hitRows, err := coerceRowMapSlice(hitvisualBase["rows"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
groupByID := map[int]map[string]any{}
|
||||
for _, row := range groupRows {
|
||||
id, err := asInt(row["id"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
groupByID[id] = row
|
||||
}
|
||||
hitByID := map[int]map[string]any{}
|
||||
for _, row := range hitRows {
|
||||
id, err := asInt(row["id"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hitByID[id] = row
|
||||
}
|
||||
|
||||
outRows := make([]map[string]any, 0, len(coreRows))
|
||||
lockData := map[string]int{}
|
||||
for _, core := range coreRows {
|
||||
id, err := asInt(core["id"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
groupID, err := asInt(core["DamageTypeGroup"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
group := groupByID[groupID]
|
||||
if group == nil {
|
||||
return nil, nil, fmt.Errorf("core id %d references missing group %d", id, groupID)
|
||||
}
|
||||
hit := hitByID[id]
|
||||
if hit == nil {
|
||||
return nil, nil, fmt.Errorf("core id %d is missing hitvisual row", id)
|
||||
}
|
||||
key := "damagetype:" + normalizeDamagetypeKey(core["Label"])
|
||||
lockData[key] = id
|
||||
outRows = append(outRows, map[string]any{
|
||||
"key": key,
|
||||
"id": id,
|
||||
"label": deepCopyValue(core["Label"]),
|
||||
"charsheet_strref": deepCopyValue(core["CharsheetStrref"]),
|
||||
"damage_type_group": strconvString(groupID),
|
||||
"damage_ranged_projectile": deepCopyValue(core["DamageRangedProjectile"]),
|
||||
"group_label": deepCopyValue(group["Label"]),
|
||||
"feedback_strref": deepCopyValue(group["FeedbackStrref"]),
|
||||
"color_r": deepCopyValue(group["ColorR"]),
|
||||
"color_g": deepCopyValue(group["ColorG"]),
|
||||
"color_b": deepCopyValue(group["ColorB"]),
|
||||
"visual_effect_id": deepCopyValue(hit["VisualEffectID"]),
|
||||
"ranged_effect_id": deepCopyValue(hit["RangedEffectID"]),
|
||||
})
|
||||
}
|
||||
|
||||
coreEntries, err := coerceEntriesMap(coreModule["entries"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
groupEntries, err := coerceEntriesMap(groupModule["entries"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hitEntries, err := coerceEntriesMap(hitvisualModule["entries"])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(coreEntries))
|
||||
for key := range coreEntries {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
a := strings.TrimPrefix(keys[i], "damagetypes:")
|
||||
b := strings.TrimPrefix(keys[j], "damagetypes:")
|
||||
return a < b
|
||||
})
|
||||
for _, coreKey := range keys {
|
||||
core := coreEntries[coreKey]
|
||||
suffix := strings.TrimPrefix(coreKey, "damagetypes:")
|
||||
group := groupEntries["damagetypegroups:"+suffix]
|
||||
if group == nil {
|
||||
return nil, nil, fmt.Errorf("%s: missing matching group entry", coreKey)
|
||||
}
|
||||
hit := hitEntries["damagehitvisual:"+suffix]
|
||||
if hit == nil {
|
||||
return nil, nil, fmt.Errorf("%s: missing matching hitvisual entry", coreKey)
|
||||
}
|
||||
id, ok := coreLock[coreKey]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("%s: missing core lock id", coreKey)
|
||||
}
|
||||
groupKey := "damagetypegroups:" + suffix
|
||||
groupID, ok := groupLock[groupKey]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("%s: missing group lock id", groupKey)
|
||||
}
|
||||
hitKey := "damagehitvisual:" + suffix
|
||||
hitID, ok := hitvisualLock[hitKey]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("%s: missing hitvisual lock id", hitKey)
|
||||
}
|
||||
if hitID != id {
|
||||
return nil, nil, fmt.Errorf("%s: core id %d does not match hitvisual id %d", suffix, id, hitID)
|
||||
}
|
||||
key := "damagetype:" + suffix
|
||||
lockData[key] = id
|
||||
outRows = append(outRows, map[string]any{
|
||||
"key": key,
|
||||
"id": id,
|
||||
"label": deepCopyValue(core["Label"]),
|
||||
"charsheet_strref": deepCopyValue(core["CharsheetStrref"]),
|
||||
"damage_type_group": strconvString(groupID),
|
||||
"damage_ranged_projectile": deepCopyValue(core["DamageRangedProjectile"]),
|
||||
"group_label": deepCopyValue(group["Label"]),
|
||||
"feedback_strref": deepCopyValue(group["FeedbackStrref"]),
|
||||
"color_r": deepCopyValue(group["ColorR"]),
|
||||
"color_g": deepCopyValue(group["ColorG"]),
|
||||
"color_b": deepCopyValue(group["ColorB"]),
|
||||
"visual_effect_id": deepCopyValue(hit["VisualEffectID"]),
|
||||
"ranged_effect_id": deepCopyValue(hit["RangedEffectID"]),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(outRows, func(i, j int) bool {
|
||||
return outRows[i]["id"].(int) < outRows[j]["id"].(int)
|
||||
})
|
||||
return outRows, lockData, nil
|
||||
}
|
||||
|
||||
func coerceRowMapSlice(value any) ([]map[string]any, error) {
|
||||
rawRows, ok := value.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rows must be an array")
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(rawRows))
|
||||
for _, raw := range rawRows {
|
||||
row, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("row must be an object")
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func coerceEntriesMap(value any) (map[string]map[string]any, error) {
|
||||
rawEntries, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("entries must be an object")
|
||||
}
|
||||
entries := make(map[string]map[string]any, len(rawEntries))
|
||||
for key, raw := range rawEntries {
|
||||
row, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("entry %s must be an object", key)
|
||||
}
|
||||
entries[key] = row
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func normalizeDamagetypeKey(value any) string {
|
||||
text := strings.TrimSpace(strings.ToLower(format2DAValue(value)))
|
||||
text = strings.TrimSuffix(text, "damage")
|
||||
text = strings.ReplaceAll(text, " ", "")
|
||||
text = strings.ReplaceAll(text, "_", "")
|
||||
text = strings.ReplaceAll(text, "-", "")
|
||||
return text
|
||||
}
|
||||
|
||||
func strconvString(v int) string {
|
||||
return fmt.Sprintf("%d", v)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyDoortypes(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "doortypes")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "doortypes")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
moduleNames := []string{
|
||||
"add_pld01.json",
|
||||
"add_sic11.json",
|
||||
"add_tapr.json",
|
||||
"add_tdm01.json",
|
||||
"add_tdx01.json",
|
||||
"add_tei01.json",
|
||||
"add_tfm01.json",
|
||||
}
|
||||
targetModulePaths := make([]string, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
targetModulePaths = append(targetModulePaths, filepath.Join(targetModulesDir, name))
|
||||
}
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
allModulesPresent := true
|
||||
for _, path := range targetModulePaths {
|
||||
if !fileExists(path) {
|
||||
allModulesPresent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allModulesPresent {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "doortypes.2da"
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
moduleObjs := make([]map[string]any, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
obj, err := loadJSONObject(filepath.Join(legacyDir, "modules", name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
moduleObjs = append(moduleObjs, obj)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
}
|
||||
for i, path := range targetModulePaths {
|
||||
writes = append(writes, struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{path: path, obj: moduleObjs[i]})
|
||||
}
|
||||
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyFeat(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "feat")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
legacyBasePath := filepath.Join(legacyDir, "base.json")
|
||||
if _, err := os.Stat(legacyBasePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "feat")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
var targetObj map[string]any
|
||||
if _, err := os.Stat(targetBasePath); err == nil {
|
||||
targetObj, err = loadJSONObject(targetBasePath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return 0, err
|
||||
}
|
||||
if targetObj != nil {
|
||||
if rows, ok := targetObj["rows"].([]any); ok && len(rows) > 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(legacyBasePath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "feat.2da"
|
||||
baseObj["compare_reference"] = false
|
||||
canonicalizeWikiMetadataDocument(baseObj)
|
||||
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
raw, err := json.MarshalIndent(baseObj, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
if err := os.WriteFile(targetBasePath, raw, 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package topdata
|
||||
|
||||
func importLegacyGenericdoors(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
return importLegacyDatasetMirror(referenceBuilderDir, dataDir, "genericdoors", "genericdoors.2da", nil)
|
||||
}
|
||||
@@ -601,397 +601,6 @@ func deepCopyValue(value any) any {
|
||||
return out
|
||||
}
|
||||
|
||||
func importLegacyItempropsRegistry(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
legacyDataDir := filepath.Join(referenceBuilderDir, "data")
|
||||
registryDir := filepath.Join(dataDir, filepath.FromSlash(itempropsRegistryDirName))
|
||||
if _, err := os.Stat(filepath.Join(registryDir, "properties.json")); err == nil {
|
||||
if refs, err := countLegacyTLKRefsInRegistry(registryDir); err == nil && refs == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(legacyDataDir, "itemprops")); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
defs, err := collectBaseDataset(nativeDataset{
|
||||
Name: "itemprops/defs",
|
||||
BasePath: filepath.Join(legacyDataDir, "itemprops", "defs", "base.json"),
|
||||
LockPath: filepath.Join(legacyDataDir, "itemprops", "defs", "lock.json"),
|
||||
ModulesDir: filepath.Join(legacyDataDir, "itemprops", "defs", "modules"),
|
||||
Spec: specForDataset("itemprops"),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sets, err := collectBaseDataset(nativeDataset{
|
||||
Name: "itemprops/sets",
|
||||
BasePath: filepath.Join(legacyDataDir, "itemprops", "sets", "base.json"),
|
||||
LockPath: filepath.Join(legacyDataDir, "itemprops", "sets", "lock.json"),
|
||||
ModulesDir: filepath.Join(legacyDataDir, "itemprops", "sets", "modules"),
|
||||
Spec: specForDataset("itemprops"),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
costIndex, err := collectBaseDataset(nativeDataset{
|
||||
Name: "itemprops/costtables/index",
|
||||
BasePath: filepath.Join(legacyDataDir, "itemprops", "costtables", "index", "base.json"),
|
||||
LockPath: filepath.Join(legacyDataDir, "itemprops", "costtables", "index", "lock.json"),
|
||||
ModulesDir: filepath.Join(legacyDataDir, "itemprops", "costtables", "index", "modules"),
|
||||
Spec: specForDataset("itemprops"),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
paramIndex, err := collectBaseDataset(nativeDataset{
|
||||
Name: "itemprops/paramtables/index",
|
||||
BasePath: filepath.Join(legacyDataDir, "itemprops", "paramtables", "index", "base.json"),
|
||||
LockPath: filepath.Join(legacyDataDir, "itemprops", "paramtables", "index", "lock.json"),
|
||||
ModulesDir: filepath.Join(legacyDataDir, "itemprops", "paramtables", "index", "modules"),
|
||||
Spec: specForDataset("itemprops"),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ownedCostTables, err := collectLegacyOwnedItempropTables(filepath.Join(legacyDataDir, "itemprops", "costtables"), "index")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ownedParamTables, err := collectLegacyOwnedItempropTables(filepath.Join(legacyDataDir, "itemprops", "paramtables"), "index")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ownedSubtypeTables, err := collectLegacyOwnedItempropTables(filepath.Join(legacyDataDir, "itemprops", "subtypes"), "")
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
setsByID := map[int]map[string]any{}
|
||||
for _, row := range sets.Rows {
|
||||
setsByID[row["id"].(int)] = row
|
||||
}
|
||||
costByID := map[int]map[string]any{}
|
||||
for _, row := range costIndex.Rows {
|
||||
costByID[row["id"].(int)] = row
|
||||
}
|
||||
paramByID := map[int]map[string]any{}
|
||||
for _, row := range paramIndex.Rows {
|
||||
paramByID[row["id"].(int)] = row
|
||||
}
|
||||
|
||||
registryLock := map[string]int{}
|
||||
legacyKeyByID := map[string]string{}
|
||||
if legacyTLK != nil {
|
||||
for key, id := range legacyTLK.Lock {
|
||||
legacyKeyByID[strconv.Itoa(id)] = key
|
||||
}
|
||||
}
|
||||
properties := make([]map[string]any, 0, len(defs.Rows))
|
||||
subtypeRegistryByKey := map[string]map[string]any{}
|
||||
for _, row := range defs.Rows {
|
||||
rowID := row["id"].(int)
|
||||
key := canonicalItempropPropertyKey(row)
|
||||
registryLock[key] = rowID
|
||||
|
||||
property := map[string]any{
|
||||
"key": key,
|
||||
"label": row["Label"],
|
||||
"name": deepCopyValue(row["Name"]),
|
||||
"property_text": deepCopyValue(row["GameStrRef"]),
|
||||
}
|
||||
if description := nullableRegistryField(row, "Description"); description != nullValue {
|
||||
property["description"] = description
|
||||
}
|
||||
if setRow, ok := setsByID[rowID]; ok {
|
||||
property["availability"] = importLegacyAvailability(setRow)
|
||||
if setName := nullableRegistryField(setRow, "StringRef"); setName != nullValue && setName != stringField(row, "Name") {
|
||||
property["set_name"] = setName
|
||||
}
|
||||
if setLabel := stringField(setRow, "Label"); setLabel != "" && setLabel != stringField(row, "Label") {
|
||||
property["set_label"] = setLabel
|
||||
}
|
||||
} else {
|
||||
property["availability"] = []any{}
|
||||
}
|
||||
|
||||
if subtypeResRef := stringField(row, "SubTypeResRef"); subtypeResRef != "" && subtypeResRef != nullValue {
|
||||
subtypeKey := canonicalModelKey("subtype_models", subtypeResRef)
|
||||
property["subtype"] = map[string]any{"ref": subtypeKey}
|
||||
if _, ok := subtypeRegistryByKey[subtypeKey]; !ok {
|
||||
subtypeRegistryByKey[subtypeKey] = buildLegacySubtypeModel(subtypeKey, subtypeResRef, ownedSubtypeTables)
|
||||
}
|
||||
}
|
||||
costValue := stringField(row, "Cost")
|
||||
costID := stringField(row, "CostTableResRef")
|
||||
if costValue != "" && costValue != nullValue || costID != "" && costID != nullValue {
|
||||
cost := map[string]any{}
|
||||
if costValue != "" && costValue != nullValue {
|
||||
cost["value"] = costValue
|
||||
}
|
||||
if costID != "" && costID != nullValue {
|
||||
id, err := strconv.Atoi(costID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
costRow, ok := costByID[id]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("%s references missing cost table index %s", key, costID)
|
||||
}
|
||||
cost["ref"] = canonicalModelKey("cost_models", stringField(costRow, "Name"))
|
||||
}
|
||||
property["cost"] = cost
|
||||
}
|
||||
if paramID := stringField(row, "Param1ResRef"); paramID != "" && paramID != nullValue {
|
||||
id, err := strconv.Atoi(paramID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
paramRow, ok := paramByID[id]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("%s references missing param table index %s", key, paramID)
|
||||
}
|
||||
property["param"] = map[string]any{"ref": canonicalModelKey("param_models", stringField(paramRow, "TableResRef"))}
|
||||
}
|
||||
if legacyTLK != nil {
|
||||
property["name"] = importLegacyStrrefValue(property["name"], key+".name", legacyTLK, legacyKeyByID)
|
||||
property["property_text"] = importLegacyStrrefValue(property["property_text"], key+".property_text", legacyTLK, legacyKeyByID)
|
||||
if description, ok := property["description"]; ok {
|
||||
property["description"] = importLegacyStrrefValue(description, key+".description", legacyTLK, legacyKeyByID)
|
||||
}
|
||||
if updated, _, err := inlineLegacyTLKValue(property, legacyTLK); err != nil {
|
||||
return 0, err
|
||||
} else if updated {
|
||||
// property updated in place
|
||||
}
|
||||
}
|
||||
properties = append(properties, property)
|
||||
}
|
||||
|
||||
costModels := make([]map[string]any, 0, len(costIndex.Rows))
|
||||
for _, row := range costIndex.Rows {
|
||||
key := canonicalModelKey("cost_models", stringField(row, "Name"))
|
||||
registryLock[key] = row["id"].(int)
|
||||
model := map[string]any{
|
||||
"key": key,
|
||||
"index_name": row["Name"],
|
||||
"label": row["Label"],
|
||||
"client_load": row["ClientLoad"],
|
||||
"table": buildLegacyOwnedModelTable(stringField(row, "Name"), ownedCostTables),
|
||||
}
|
||||
costModels = append(costModels, model)
|
||||
}
|
||||
|
||||
paramModels := make([]map[string]any, 0, len(paramIndex.Rows))
|
||||
for _, row := range paramIndex.Rows {
|
||||
key := canonicalModelKey("param_models", stringField(row, "TableResRef"))
|
||||
registryLock[key] = row["id"].(int)
|
||||
model := map[string]any{
|
||||
"key": key,
|
||||
"name": deepCopyValue(row["Name"]),
|
||||
"label": row["Lable"],
|
||||
"table": buildLegacyOwnedModelTable(stringField(row, "TableResRef"), ownedParamTables),
|
||||
}
|
||||
paramModels = append(paramModels, model)
|
||||
}
|
||||
|
||||
subtypeModels := make([]map[string]any, 0, len(subtypeRegistryByKey))
|
||||
for _, key := range sortedKeysAny(subtypeRegistryByKey) {
|
||||
subtypeModels = append(subtypeModels, subtypeRegistryByKey[key])
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{filepath.Join(registryDir, "properties.json"), map[string]any{"rows": properties}},
|
||||
{filepath.Join(registryDir, "cost_models.json"), map[string]any{"rows": costModels}},
|
||||
{filepath.Join(registryDir, "param_models.json"), map[string]any{"rows": paramModels}},
|
||||
{filepath.Join(registryDir, "subtype_models.json"), map[string]any{"rows": subtypeModels}},
|
||||
{filepath.Join(registryDir, itempropsRegistryLock), anyMapInt(registryLock)},
|
||||
}
|
||||
if err := os.MkdirAll(registryDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, write := range writes {
|
||||
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 countLegacyTLKRefsInRegistry(registryDir string) (int, error) {
|
||||
paths, err := collectDataJSONPaths(registryDir)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
refs := 0
|
||||
for _, path := range paths {
|
||||
obj, err := loadJSONObject(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
refs += countLegacyTLKRefsInValue(obj)
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func collectLegacyOwnedItempropTables(root, skipDir string) (map[string]map[string]any, error) {
|
||||
result := map[string]map[string]any{}
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
if skipDir != "" && entry.Name() == skipDir {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(root, entry.Name())
|
||||
basePath := filepath.Join(dir, "base.json")
|
||||
if _, err := os.Stat(basePath); err != nil {
|
||||
continue
|
||||
}
|
||||
collected, err := collectBaseDataset(nativeDataset{
|
||||
Name: filepath.ToSlash(filepath.Join("itemprops", filepath.Base(root), entry.Name())),
|
||||
BasePath: basePath,
|
||||
LockPath: filepath.Join(dir, "lock.json"),
|
||||
ModulesDir: filepath.Join(dir, "modules"),
|
||||
Spec: specForDataset("itemprops"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[strings.ToLower(outputStem(collected.Dataset.OutputName))] = map[string]any{
|
||||
"ownership": "owned",
|
||||
"resref": outputStem(collected.Dataset.OutputName),
|
||||
"output": collected.Dataset.OutputName,
|
||||
"columns": stringSliceToAny(collected.Columns),
|
||||
"rows": rowsToAny(collected.Rows),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildLegacySubtypeModel(key, resref string, owned map[string]map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
"key": key,
|
||||
"table_resref": resref,
|
||||
"table": buildLegacyOwnedModelTable(resref, owned),
|
||||
}
|
||||
}
|
||||
|
||||
func buildLegacyOwnedModelTable(resref string, owned map[string]map[string]any) map[string]any {
|
||||
if table, ok := owned[strings.ToLower(resref)]; ok {
|
||||
return table
|
||||
}
|
||||
return map[string]any{
|
||||
"ownership": "external",
|
||||
"resref": resref,
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalItempropPropertyKey(row map[string]any) string {
|
||||
if key := stringField(row, "key"); key != "" {
|
||||
return "itemprop:" + strings.TrimPrefix(key, "itempropdef:")
|
||||
}
|
||||
return canonicalModelKey("itemprop", stringField(row, "Label"))
|
||||
}
|
||||
|
||||
func canonicalModelKey(prefix, source string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(source))
|
||||
normalized = strings.ReplaceAll(normalized, " ", "")
|
||||
normalized = strings.ReplaceAll(normalized, "-", "")
|
||||
normalized = strings.ReplaceAll(normalized, "_", "")
|
||||
return prefix + ":" + normalized
|
||||
}
|
||||
|
||||
func importLegacyStrrefValue(value any, fallbackKey string, legacy *legacyTLKData, keyByID map[string]string) any {
|
||||
if legacy == nil {
|
||||
return value
|
||||
}
|
||||
if _, ok, _ := parseTLKPayload(value, true); ok {
|
||||
return value
|
||||
}
|
||||
_ = keyByID
|
||||
key := fallbackKey
|
||||
entry, ok := legacy.Entries[key]
|
||||
if !ok {
|
||||
return value
|
||||
}
|
||||
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
|
||||
}
|
||||
return map[string]any{"tlk": payload}
|
||||
}
|
||||
|
||||
func importLegacyAvailability(row map[string]any) []any {
|
||||
out := make([]any, 0)
|
||||
for _, column := range itempropsAvailabilityColumns {
|
||||
value, ok := row[column]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
text := formatRegistryScalar(value)
|
||||
if text != "1" {
|
||||
continue
|
||||
}
|
||||
out = append(out, normalizeAvailabilityName(column))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func anyMapInt(values map[string]int) map[string]any {
|
||||
out := make(map[string]any, len(values))
|
||||
for key, value := range values {
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stringSliceToAny(values []string) []any {
|
||||
out := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
out = append(out, value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func rowsToAny(rows []map[string]any) []any {
|
||||
out := make([]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, deepCopyValue(row))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sortedKeysAny(values map[string]map[string]any) []string {
|
||||
out := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
out = append(out, key)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func validateItempropsRegistryGraph(dataDir string, report *ValidationReport) {
|
||||
registry, err := loadItempropsRegistry(dataDir)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type legacyDatasetTransform func(relativePath string, obj map[string]any)
|
||||
|
||||
func importLegacyDatasetMirror(referenceBuilderDir, dataDir, datasetName, outputName string, transform legacyDatasetTransform) (int, error) {
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", datasetName)
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, datasetName)
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if outputName != "" {
|
||||
baseObj["output"] = outputName
|
||||
}
|
||||
if transform != nil {
|
||||
transform("base.json", baseObj)
|
||||
}
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if transform != nil {
|
||||
transform("lock.json", lockObj)
|
||||
}
|
||||
|
||||
moduleRelPaths, err := collectJSONRelativePaths(filepath.Join(legacyDir, "modules"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
moduleObjs := make(map[string]map[string]any, len(moduleRelPaths))
|
||||
for _, relPath := range moduleRelPaths {
|
||||
obj, err := loadJSONObject(filepath.Join(legacyDir, "modules", relPath))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
normalizedRel := filepath.ToSlash(relPath)
|
||||
if transform != nil {
|
||||
transform(filepath.ToSlash(filepath.Join("modules", normalizedRel)), obj)
|
||||
}
|
||||
moduleObjs[normalizedRel] = obj
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
updated := 0
|
||||
for _, write := range []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: filepath.Join(targetDir, "base.json"), obj: baseObj},
|
||||
{path: filepath.Join(targetDir, "lock.json"), obj: lockObj},
|
||||
} {
|
||||
changed, err := writeJSONObjectIfChanged(write.path, write.obj)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if changed {
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
if len(moduleObjs) > 0 {
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
for relPath, obj := range moduleObjs {
|
||||
targetPath := filepath.Join(targetModulesDir, filepath.FromSlash(relPath))
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
changed, err := writeJSONObjectIfChanged(targetPath, obj)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if changed {
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
staleModules, err := collectJSONRelativePaths(targetModulesDir)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, relPath := range staleModules {
|
||||
normalizedRel := filepath.ToSlash(relPath)
|
||||
if _, ok := moduleObjs[normalizedRel]; ok {
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(filepath.Join(targetModulesDir, filepath.FromSlash(normalizedRel))); err != nil && !os.IsNotExist(err) {
|
||||
return 0, err
|
||||
}
|
||||
updated++
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func collectJSONRelativePaths(root string) ([]string, error) {
|
||||
if _, err := os.Stat(root); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var relPaths []string
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.EqualFold(filepath.Ext(d.Name()), ".json") {
|
||||
return nil
|
||||
}
|
||||
relPath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPaths = append(relPaths, filepath.ToSlash(relPath))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(relPaths)
|
||||
return relPaths, nil
|
||||
}
|
||||
|
||||
func writeJSONObjectIfChanged(path string, obj map[string]any) (bool, error) {
|
||||
raw, err := json.MarshalIndent(obj, "", " ")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
|
||||
current, err := os.ReadFile(path)
|
||||
if err == nil && string(current) == string(raw) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return false, err
|
||||
}
|
||||
if err := os.WriteFile(path, raw, 0o644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package topdata
|
||||
|
||||
func importLegacyLoadscreens(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
return importLegacyDatasetMirror(referenceBuilderDir, dataDir, "loadscreens", "loadscreens.2da", nil)
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func importLegacyMasterfeats(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "feat", "masterfeats")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "masterfeats")
|
||||
targetPath := filepath.Join(targetDir, "base.json")
|
||||
legacyPlainPath := filepath.Join(targetDir, "masterfeats.json")
|
||||
if _, err := os.Stat(targetPath); err == nil {
|
||||
obj, err := loadJSONObject(targetPath)
|
||||
if err == nil && countLegacyTLKRefsInValue(obj) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
collected, err := importLegacyMasterfeatsDataset(legacyDir)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
referenceIDs, err := collectReferenceLockIDs(filepath.Join(referenceBuilderDir, "data"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
rows := make([]any, 0, len(collected.Rows))
|
||||
for _, row := range collected.Rows {
|
||||
copyRow, ok := deepCopyValue(row).(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if legacyTLK != nil {
|
||||
if _, _, err := inlineLegacyTLKValue(copyRow, legacyTLK); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
copyRow = rewriteImportedIDRefs(copyRow, referenceIDs)
|
||||
rows = append(rows, copyRow)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{
|
||||
path: targetPath,
|
||||
obj: map[string]any{
|
||||
"output": collected.Dataset.OutputName,
|
||||
"columns": stringSliceToAny(collected.Columns),
|
||||
"rows": rows,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: filepath.Join(targetDir, "lock.json"),
|
||||
obj: anyMapInt(collected.LockData),
|
||||
},
|
||||
}
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
}
|
||||
updatedFiles := len(writes)
|
||||
if err := os.Remove(legacyPlainPath); err == nil {
|
||||
updatedFiles++
|
||||
} else if err != nil && !os.IsNotExist(err) {
|
||||
return 0, err
|
||||
}
|
||||
return updatedFiles, nil
|
||||
}
|
||||
|
||||
func importLegacyMasterfeatsDataset(legacyDir string) (nativeCollectedDataset, error) {
|
||||
dataset := nativeDataset{
|
||||
Name: "masterfeats",
|
||||
BasePath: filepath.Join(legacyDir, "masterfeats.json"),
|
||||
LockPath: filepath.Join(legacyDir, "lock.json"),
|
||||
Spec: specForDataset("masterfeats"),
|
||||
}
|
||||
tableData, err := loadJSONObject(dataset.BasePath)
|
||||
if err != nil {
|
||||
return nativeCollectedDataset{}, err
|
||||
}
|
||||
columns, err := parseColumns(tableData, dataset.Name)
|
||||
if err != nil {
|
||||
return nativeCollectedDataset{}, err
|
||||
}
|
||||
rawRows, ok := tableData["rows"].([]any)
|
||||
if !ok {
|
||||
return nativeCollectedDataset{}, nil
|
||||
}
|
||||
lockData, err := loadLockfile(dataset.LockPath)
|
||||
if err != nil {
|
||||
return nativeCollectedDataset{}, err
|
||||
}
|
||||
usedIDs := map[int]struct{}{}
|
||||
for _, rowID := range lockData {
|
||||
usedIDs[rowID] = struct{}{}
|
||||
}
|
||||
|
||||
rows := make([]map[string]any, 0, len(rawRows))
|
||||
explicitID := make([]bool, 0, len(rawRows))
|
||||
for index, raw := range rawRows {
|
||||
rowObj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rowID := index
|
||||
if value, ok := rowObj["id"]; ok {
|
||||
parsed, err := asInt(value)
|
||||
if err != nil {
|
||||
return nativeCollectedDataset{}, err
|
||||
}
|
||||
rowID = parsed
|
||||
}
|
||||
row := map[string]any{"id": rowID}
|
||||
for _, column := range columns {
|
||||
row[column] = nullValue
|
||||
}
|
||||
for key, value := range rowObj {
|
||||
switch key {
|
||||
case "id":
|
||||
case "key":
|
||||
if keyText, ok := value.(string); ok && keyText != "" {
|
||||
row["key"] = keyText
|
||||
}
|
||||
default:
|
||||
columnName, ok := canonicalColumn(columns, key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
row[columnName] = value
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
_, hasExplicitID := rowObj["id"]
|
||||
explicitID = append(explicitID, hasExplicitID)
|
||||
if hasExplicitID {
|
||||
usedIDs[rowID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
nextID := nextAvailableID(usedIDs)
|
||||
lockModified := false
|
||||
for index, row := range rows {
|
||||
key, hasKey := row["key"].(string)
|
||||
if hasKey && key != "" {
|
||||
if lockedID, ok := lockData[key]; ok {
|
||||
row["id"] = lockedID
|
||||
continue
|
||||
}
|
||||
if explicitID[index] {
|
||||
lockData[key] = row["id"].(int)
|
||||
} else {
|
||||
row["id"] = nextID
|
||||
lockData[key] = nextID
|
||||
usedIDs[nextID] = struct{}{}
|
||||
nextID = nextAvailableID(usedIDs)
|
||||
}
|
||||
lockModified = true
|
||||
continue
|
||||
}
|
||||
if !explicitID[index] {
|
||||
row["id"] = index
|
||||
}
|
||||
}
|
||||
if lockModified {
|
||||
_ = lockModified
|
||||
}
|
||||
|
||||
return nativeCollectedDataset{
|
||||
Dataset: nativeDataset{
|
||||
Name: dataset.Name,
|
||||
OutputName: "masterfeats.2da",
|
||||
Columns: columns,
|
||||
Spec: dataset.Spec,
|
||||
},
|
||||
Columns: columns,
|
||||
Rows: rows,
|
||||
LockData: lockData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rewriteImportedIDRefs(value any, ids map[string]int) map[string]any {
|
||||
row, ok := rewriteImportedIDRefsValue(value, ids).(map[string]any)
|
||||
if !ok {
|
||||
return map[string]any{}
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func rewriteImportedIDRefsValue(value any, ids map[string]int) any {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
if len(typed) == 1 {
|
||||
if rawID, ok := typed["id"]; ok {
|
||||
if key, ok := rawID.(string); ok {
|
||||
if resolved, ok := ids[key]; ok {
|
||||
return strconv.Itoa(resolved)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make(map[string]any, len(typed))
|
||||
for key, child := range typed {
|
||||
out[key] = rewriteImportedIDRefsValue(child, ids)
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]any, 0, len(typed))
|
||||
for _, child := range typed {
|
||||
out = append(out, rewriteImportedIDRefsValue(child, ids))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.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
|
||||
}
|
||||
}
|
||||
+18
-78
@@ -2317,6 +2317,18 @@ func (c *featGeneratedContext) familyHasExistingRows(familyKey string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// skillsDatasetName returns the dataset the skill_focus family sources its
|
||||
// children from. The skills dataset location is data-driven via that spec;
|
||||
// "skills" is only the fallback when no spec is loaded.
|
||||
func (c *featGeneratedContext) skillsDatasetName() string {
|
||||
for key, spec := range c.familySpecs {
|
||||
if normalizeKeyIdentity(key) == "skillfocus" && spec.ChildSource.Dataset != "" {
|
||||
return spec.ChildSource.Dataset
|
||||
}
|
||||
}
|
||||
return "skills"
|
||||
}
|
||||
|
||||
func (c *featGeneratedContext) datasetRows(name string) (map[string]map[string]any, error) {
|
||||
if rows, ok := c.rowsByDataset[name]; ok {
|
||||
return rows, nil
|
||||
@@ -2555,7 +2567,10 @@ func buildFamilyExpansionGeneratedModule(path string, obj map[string]any, ctx *f
|
||||
if !include {
|
||||
continue
|
||||
}
|
||||
slug := strings.TrimPrefix(sourceKey, spec.ChildSource.Dataset+":")
|
||||
slug := sourceKey
|
||||
if idx := strings.Index(slug, ":"); idx >= 0 {
|
||||
slug = slug[idx+1:]
|
||||
}
|
||||
featKey, rowID, hasID, err := ctx.resolveGeneratedFeatIdentity(spec, slug, row)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generated feat file %s: %w", path, err)
|
||||
@@ -2655,7 +2670,7 @@ func buildRacialtypesSkillAffinityModule(ctx *featGeneratedContext) (map[string]
|
||||
if len(grants) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
skillRows, err := ctx.datasetRows("skills")
|
||||
skillRows, err := ctx.datasetRows(ctx.skillsDatasetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2854,7 +2869,7 @@ func (c *featGeneratedContext) displayNameForGeneratedSource(dataset string, row
|
||||
return text
|
||||
}
|
||||
switch dataset {
|
||||
case "skills":
|
||||
case c.skillsDatasetName():
|
||||
return displayNameForSkill(row)
|
||||
case "baseitems":
|
||||
return displayNameForBaseitem(row)
|
||||
@@ -6627,81 +6642,6 @@ func marshalOrderedLockfile(keys []string, lockData map[string]int, formatting j
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func loadLegacyTLK(root string) (*legacyTLKData, error) {
|
||||
info, err := os.Stat(root)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil, fmt.Errorf("legacy tlk root must be a directory: %s", root)
|
||||
}
|
||||
|
||||
result := &legacyTLKData{
|
||||
Language: "en",
|
||||
Entries: map[string]tlkEntryData{},
|
||||
Lock: map[string]int{},
|
||||
}
|
||||
|
||||
lockPath := filepath.Join(root, "lock.json")
|
||||
lockData, err := loadLockfile(lockPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
for key, id := range lockData {
|
||||
result.Lock[key] = id
|
||||
}
|
||||
|
||||
basePath := filepath.Join(root, "base.json")
|
||||
if _, err := os.Stat(basePath); err == nil {
|
||||
base, err := loadJSONObject(basePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if language, ok := base["language"].(string); ok && language != "" {
|
||||
result.Language = language
|
||||
}
|
||||
if entries, ok := base["entries"].(map[string]any); ok {
|
||||
normalized, err := normalizeLegacyTLKEntries(entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, entry := range normalized {
|
||||
result.Entries[key] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modulesDir := filepath.Join(root, "modules")
|
||||
modulePaths, err := collectModulePaths(modulesDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return result, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, path := range modulePaths {
|
||||
obj, err := loadJSONObject(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, ok := obj["entries"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
normalized, err := normalizeLegacyTLKEntries(entries)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
for key, entry := range normalized {
|
||||
result.Entries[key] = entry
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func normalizeLegacyTLKEntries(entries map[string]any) (map[string]tlkEntryData, error) {
|
||||
normalized := map[string]tlkEntryData{}
|
||||
for _, key := range sortedKeys(entries) {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package topdata
|
||||
|
||||
func importLegacyPlaceables(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
return importLegacyDatasetMirror(referenceBuilderDir, dataDir, "placeables", "placeables.2da", nil)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package topdata
|
||||
|
||||
func importLegacyPortraits(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
if legacyTLK == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return importLegacyDatasetMirror(referenceBuilderDir, dataDir, "portraits", "portraits.2da", nil)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package topdata
|
||||
|
||||
func importLegacyProgfx(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
return importLegacyDatasetMirror(referenceBuilderDir, dataDir, "progfx", "progfx.2da", nil)
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type legacyRacialtypesFeatTable struct {
|
||||
Output string
|
||||
Rows []map[string]any
|
||||
}
|
||||
|
||||
func importLegacyRacialtypes(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
if strings.TrimSpace(referenceBuilderDir) == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
legacyCoreDir := filepath.Join(referenceBuilderDir, "data", "racialtypes", "core")
|
||||
legacyFeatsDir := filepath.Join(referenceBuilderDir, "data", "racialtypes", "feats")
|
||||
if _, err := os.Stat(legacyCoreDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if _, err := os.Stat(legacyFeatsDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetRoot := filepath.Join(dataDir, "racialtypes", "registry")
|
||||
targetBasePath := filepath.Join(targetRoot, "base.json")
|
||||
targetLockPath := filepath.Join(targetRoot, "lock.json")
|
||||
targetRacesDir := filepath.Join(targetRoot, "races")
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
entries, err := os.ReadDir(targetRacesDir)
|
||||
if err == nil {
|
||||
raceFiles := 0
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(strings.ToLower(entry.Name()), ".json") {
|
||||
raceFiles++
|
||||
}
|
||||
}
|
||||
if raceFiles == 16 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseColumns, baseRows, lockData, raceRows, err := collectLegacyRacialtypesRegistry(legacyCoreDir, legacyFeatsDir, legacyTLK)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := removePathIfExists(filepath.Join(dataDir, "racialtypes", "core")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(dataDir, "racialtypes", "feats")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetRoot, "base_rows.json")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(targetRacesDir); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := os.MkdirAll(targetRacesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj any
|
||||
}{
|
||||
{
|
||||
path: targetBasePath,
|
||||
obj: map[string]any{
|
||||
"columns": stringSliceToAny(baseColumns),
|
||||
"rows": rowsToAny(baseRows),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: targetLockPath,
|
||||
obj: anyMapInt(lockData),
|
||||
},
|
||||
}
|
||||
|
||||
raceKeys := make([]string, 0, len(raceRows))
|
||||
for key := range raceRows {
|
||||
raceKeys = append(raceKeys, key)
|
||||
}
|
||||
slices.Sort(raceKeys)
|
||||
for _, key := range raceKeys {
|
||||
fileName := strings.TrimPrefix(key, "racialtypes:") + ".json"
|
||||
writes = append(writes, struct {
|
||||
path string
|
||||
obj any
|
||||
}{
|
||||
path: filepath.Join(targetRacesDir, fileName),
|
||||
obj: raceRows[key],
|
||||
})
|
||||
}
|
||||
|
||||
for _, write := range writes {
|
||||
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 collectLegacyRacialtypesRegistry(coreDir, featsDir string, legacyTLK *legacyTLKData) ([]string, []map[string]any, map[string]int, map[string]map[string]any, error) {
|
||||
coreCollected, err := collectBaseDataset(nativeDataset{
|
||||
Name: "racialtypes",
|
||||
BasePath: filepath.Join(coreDir, "base.json"),
|
||||
LockPath: filepath.Join(coreDir, "lock.json"),
|
||||
ModulesDir: filepath.Join(coreDir, "modules"),
|
||||
Spec: specForDataset("racialtypes"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
featTables, err := loadLegacyRacialtypesFeatTables(featsDir)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
baseRows := make([]map[string]any, 0)
|
||||
raceRows := map[string]map[string]any{}
|
||||
lockData := map[string]int{}
|
||||
for key, id := range coreCollected.LockData {
|
||||
if strings.HasPrefix(key, "racialtypes:") {
|
||||
lockData[key] = id
|
||||
}
|
||||
}
|
||||
|
||||
for _, collectedRow := range coreCollected.Rows {
|
||||
row, ok := deepCopyValue(collectedRow).(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if legacyTLK != nil {
|
||||
if _, _, err := inlineLegacyTLKValue(row, legacyTLK); err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
key, _ := row["key"].(string)
|
||||
if !strings.HasPrefix(key, "racialtypes:") {
|
||||
assignRacialtypesBaseRowKey(row)
|
||||
if key, _ := row["key"].(string); key != "" {
|
||||
rowID, err := asInt(row["id"])
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
lockData[key] = rowID
|
||||
}
|
||||
baseRows = append(baseRows, row)
|
||||
continue
|
||||
}
|
||||
|
||||
delete(row, "id")
|
||||
delete(row, "key")
|
||||
|
||||
raceFile := map[string]any{
|
||||
"key": key,
|
||||
"core": row,
|
||||
}
|
||||
if tableKey := extractRacialtypesFeatTableKey(row["FeatsTable"]); tableKey != "" {
|
||||
table, ok := featTables[tableKey]
|
||||
if !ok {
|
||||
return nil, nil, nil, nil, fmt.Errorf("%s: unknown feat table %s", key, tableKey)
|
||||
}
|
||||
delete(row, "FeatsTable")
|
||||
raceFile["feat_output"] = table.Output
|
||||
raceFile["feats"] = rowsToAny(table.Rows)
|
||||
}
|
||||
raceRows[key] = raceFile
|
||||
}
|
||||
|
||||
slices.SortFunc(baseRows, func(a, b map[string]any) int {
|
||||
left, _ := asInt(a["id"])
|
||||
right, _ := asInt(b["id"])
|
||||
return left - right
|
||||
})
|
||||
return coreCollected.Columns, baseRows, lockData, raceRows, nil
|
||||
}
|
||||
|
||||
func loadLegacyRacialtypesFeatTables(featsDir string) (map[string]legacyRacialtypesFeatTable, error) {
|
||||
entries, err := os.ReadDir(featsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tables := map[string]legacyRacialtypesFeatTable{}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
obj, err := loadJSONObject(filepath.Join(featsDir, entry.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tableKey, _ := obj["key"].(string)
|
||||
outputName, _ := obj["output"].(string)
|
||||
rawRows, ok := obj["rows"].([]any)
|
||||
if tableKey == "" || outputName == "" || !ok {
|
||||
return nil, fmt.Errorf("%s: racialtypes feat table must define key, output, and rows", entry.Name())
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(rawRows))
|
||||
for index, raw := range rawRows {
|
||||
row, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: row %d must be an object", entry.Name(), index)
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
tables[tableKey] = legacyRacialtypesFeatTable{
|
||||
Output: outputName,
|
||||
Rows: rows,
|
||||
}
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func extractRacialtypesFeatTableKey(value any) string {
|
||||
obj, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
tableKey, _ := obj["table"].(string)
|
||||
return tableKey
|
||||
}
|
||||
|
||||
func assignRacialtypesBaseRowKey(row map[string]any) {
|
||||
name, _ := row["Name"].(string)
|
||||
if strings.TrimSpace(name) == "" || name == nullValue {
|
||||
delete(row, "key")
|
||||
return
|
||||
}
|
||||
label, _ := row["Label"].(string)
|
||||
keySuffix := normalizeRacialtypesKeySuffix(label)
|
||||
if keySuffix == "" {
|
||||
delete(row, "key")
|
||||
return
|
||||
}
|
||||
row["key"] = "racialtypes:" + keySuffix
|
||||
}
|
||||
|
||||
func normalizeRacialtypesKeySuffix(label string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(label))
|
||||
if normalized == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
lastUnderscore := false
|
||||
for _, ch := range normalized {
|
||||
isAlphaNum := (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|
||||
if isAlphaNum {
|
||||
b.WriteRune(ch)
|
||||
lastUnderscore = false
|
||||
continue
|
||||
}
|
||||
if lastUnderscore {
|
||||
continue
|
||||
}
|
||||
b.WriteByte('_')
|
||||
lastUnderscore = true
|
||||
}
|
||||
return strings.Trim(b.String(), "_")
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyRepadjust(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "repadjust")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "repadjust")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
obj, err := loadJSONObject(targetBasePath)
|
||||
if err == nil && countLegacyTLKRefsInValue(obj) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "repadjust.2da"
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
}
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func importLegacyRuleset(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "ruleset")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "ruleset")
|
||||
targetPath := filepath.Join(targetDir, "ruleset.json")
|
||||
if _, err := os.Stat(targetPath); err == nil {
|
||||
obj, err := loadJSONObject(targetPath)
|
||||
if err == nil && countLegacyTLKRefsInValue(obj) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rows, err := mergeRulesetRows(baseObj, filepath.Join(legacyDir, "modules"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "base.json")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "lock.json")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := removePathIfExists(filepath.Join(targetDir, "modules")); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
out := map[string]any{
|
||||
"output": "ruleset.2da",
|
||||
"columns": baseObj["columns"],
|
||||
"rows": rows,
|
||||
}
|
||||
raw, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
raw = append(raw, '\n')
|
||||
if err := os.WriteFile(targetPath, raw, 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func mergeRulesetRows(baseObj map[string]any, modulesDir string) ([]any, error) {
|
||||
rawRows, ok := baseObj["rows"].([]any)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rows := make([]map[string]any, 0, len(rawRows))
|
||||
byLabel := map[string]map[string]any{}
|
||||
for _, raw := range rawRows {
|
||||
row, ok := deepCopyValue(raw).(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(row, "key")
|
||||
if rawID, ok := row["id"]; ok {
|
||||
id, err := asInt(rawID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row["id"] = id
|
||||
}
|
||||
if label, ok := row["Label"].(string); ok && label != "" && label != "****" {
|
||||
if _, exists := byLabel[label]; exists {
|
||||
return nil, fmt.Errorf("duplicate ruleset label %q", label)
|
||||
}
|
||||
byLabel[label] = row
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
modulePaths, err := collectModulePaths(modulesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, path := range modulePaths {
|
||||
obj, err := loadJSONObject(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
overrideList, ok := obj["overrides"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, raw := range overrideList {
|
||||
override, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
matchObj, ok := override["match"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: ruleset override is missing match object", path)
|
||||
}
|
||||
rawLabel, ok := matchObj["Label"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: ruleset override match is missing Label", path)
|
||||
}
|
||||
labels, err := normalizeRulesetMatchLabels(rawLabel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
for _, label := range labels {
|
||||
row, ok := byLabel[label]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: ruleset override target label %q was not found", path, label)
|
||||
}
|
||||
for key, value := range override {
|
||||
if key == "id" || key == "key" || key == "_tlk" || isMetadataField(key) || key == "match" {
|
||||
continue
|
||||
}
|
||||
row[key] = deepCopyValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i]["id"].(int) < rows[j]["id"].(int)
|
||||
})
|
||||
out := make([]any, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func normalizeRulesetMatchLabels(raw any) ([]string, error) {
|
||||
switch typed := raw.(type) {
|
||||
case string:
|
||||
return []string{typed}, nil
|
||||
case []any:
|
||||
out := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
label, ok := item.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ruleset match.Label entries must be strings")
|
||||
}
|
||||
out = append(out, label)
|
||||
}
|
||||
return out, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("ruleset match.Label must be a string or string array")
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacySkills(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "skills")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "skills")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
moduleNames := []string{
|
||||
"add_athletics.json",
|
||||
"add_disguise.json",
|
||||
"add_linguistics.json",
|
||||
"add_perception.json",
|
||||
"add_scriptcraft.json",
|
||||
"add_sensemotive.json",
|
||||
"add_stealth.json",
|
||||
"add_survival.json",
|
||||
"add_userope.json",
|
||||
filepath.Join("craft", "add_craftalchemy.json"),
|
||||
filepath.Join("craft", "add_craftcooking.json"),
|
||||
filepath.Join("craft", "add_craftjewelry.json"),
|
||||
filepath.Join("craft", "add_craftleatherworking.json"),
|
||||
filepath.Join("craft", "add_craftstonework.json"),
|
||||
filepath.Join("craft", "add_crafttextiles.json"),
|
||||
filepath.Join("craft", "ovr_craftarmorsmithing.json"),
|
||||
filepath.Join("craft", "ovr_crafttinkering.json"),
|
||||
filepath.Join("craft", "ovr_craftweaponsmithing.json"),
|
||||
filepath.Join("craft", "ovr_craftwoodworking.json"),
|
||||
filepath.Join("knowledge", "add_knowledgearcana.json"),
|
||||
filepath.Join("knowledge", "add_knowledgearchitecture.json"),
|
||||
filepath.Join("knowledge", "add_knowledgedungeoneering.json"),
|
||||
filepath.Join("knowledge", "add_knowledgegeography.json"),
|
||||
filepath.Join("knowledge", "add_knowledgehistory.json"),
|
||||
filepath.Join("knowledge", "add_knowledgelocal.json"),
|
||||
filepath.Join("knowledge", "add_knowledgenature.json"),
|
||||
filepath.Join("knowledge", "add_knowledgenobility.json"),
|
||||
filepath.Join("knowledge", "add_knowledgeplanar.json"),
|
||||
filepath.Join("knowledge", "add_knowledgereligion.json"),
|
||||
"ovr_acrobatics.json",
|
||||
"ovr_animalhandling.json",
|
||||
"ovr_appraise.json",
|
||||
"ovr_concentration.json",
|
||||
"ovr_disabledevice.json",
|
||||
"ovr_heal.json",
|
||||
"ovr_hiddenskills.json",
|
||||
"ovr_influence.json",
|
||||
"ovr_openlock.json",
|
||||
"ovr_parry.json",
|
||||
"ovr_searchtowis.json",
|
||||
"ovr_sleightofhand.json",
|
||||
"ovr_taunttointimidate.json",
|
||||
"rmv_removedskills.json",
|
||||
}
|
||||
targetModulePaths := make([]string, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
targetModulePaths = append(targetModulePaths, filepath.Join(targetModulesDir, name))
|
||||
}
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
allModulesPresent := true
|
||||
for _, path := range targetModulePaths {
|
||||
if !fileExists(path) {
|
||||
allModulesPresent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allModulesPresent {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "skills.2da"
|
||||
canonicalizeWikiMetadataDocument(baseObj)
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
moduleObjs := make([]map[string]any, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
obj, err := loadJSONObject(filepath.Join(legacyDir, "modules", name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
canonicalizeWikiMetadataDocument(obj)
|
||||
moduleObjs = append(moduleObjs, obj)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
}
|
||||
for i, path := range targetModulePaths {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
writes = append(writes, struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{path: path, obj: moduleObjs[i]})
|
||||
}
|
||||
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package topdata
|
||||
|
||||
func importLegacySkyboxes(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
return importLegacyDatasetMirror(referenceBuilderDir, dataDir, "skyboxes", "skyboxes.2da", nil)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacySpells(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "spells")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "spells")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
moduleNames := []string{
|
||||
"specialattacks.json",
|
||||
"ovr_fixduplicates.json",
|
||||
filepath.Join("bardicmusic", "fascinate.json"),
|
||||
filepath.Join("bardicmusic", "inspirecompetence.json"),
|
||||
filepath.Join("bardicmusic", "inspirecourage.json"),
|
||||
filepath.Join("bardicmusic", "inspiregreatness.json"),
|
||||
filepath.Join("bardicmusic", "inspireheroics.json"),
|
||||
filepath.Join("bardicmusic", "songoffreedom.json"),
|
||||
}
|
||||
targetModulePaths := make([]string, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
targetModulePaths = append(targetModulePaths, filepath.Join(targetModulesDir, name))
|
||||
}
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
allModulesPresent := true
|
||||
for _, path := range targetModulePaths {
|
||||
if !fileExists(path) {
|
||||
allModulesPresent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allModulesPresent {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "spells.2da"
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
moduleObjs := make([]map[string]any, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
obj, err := loadJSONObject(filepath.Join(legacyDir, "modules", name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
moduleObjs = append(moduleObjs, obj)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
}
|
||||
for i, path := range targetModulePaths {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
writes = append(writes, struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{path: path, obj: moduleObjs[i]})
|
||||
}
|
||||
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyTailmodel(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "tailmodel")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "tailmodel")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
targetModulePaths := []string{
|
||||
filepath.Join(targetModulesDir, "add.json"),
|
||||
filepath.Join(targetModulesDir, "ovr_tailprefixes.json"),
|
||||
}
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
allModulesPresent := true
|
||||
for _, path := range targetModulePaths {
|
||||
if !fileExists(path) {
|
||||
allModulesPresent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allModulesPresent {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "tailmodel.2da"
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
moduleNames := []string{"add.json", "ovr_tailprefixes.json"}
|
||||
moduleObjs := make([]map[string]any, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
obj, err := loadJSONObject(filepath.Join(legacyDir, "modules", name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
moduleObjs = append(moduleObjs, obj)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
{path: targetModulePaths[0], obj: moduleObjs[0]},
|
||||
{path: targetModulePaths[1], obj: moduleObjs[1]},
|
||||
}
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
@@ -727,3 +727,36 @@ func sortedKeys[M ~map[string]V, V any](input M) []string {
|
||||
func almostEqualFloat32(a, b float32) bool {
|
||||
return math.Abs(float64(a-b)) < 0.0001
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1867,8 +1867,10 @@ func validateGeneratedFeatFamilies(dataDir string, report *ValidationReport) {
|
||||
}
|
||||
|
||||
required := []requiredFeatFamily{
|
||||
{FamilyKey: "skillfocus", Dataset: "skills", Predicate: "accessible"},
|
||||
{FamilyKey: "greaterskillfocus", Dataset: "skills", Predicate: "accessible"},
|
||||
// skill families: the source dataset is data-driven (family spec), only
|
||||
// the accessibility predicate is required
|
||||
{FamilyKey: "skillfocus", Predicate: "accessible"},
|
||||
{FamilyKey: "greaterskillfocus", Predicate: "accessible"},
|
||||
{FamilyKey: "weaponfocus", Dataset: "baseitems", Column: "WeaponFocusFeat"},
|
||||
{FamilyKey: "weaponspecialization", Dataset: "baseitems", Column: "WeaponSpecializationFeat"},
|
||||
{FamilyKey: "improvedcritical", Dataset: "baseitems", Column: "WeaponImprovedCriticalFeat"},
|
||||
@@ -1882,7 +1884,7 @@ func validateGeneratedFeatFamilies(dataDir string, report *ValidationReport) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if spec.ChildSource.Dataset != requirement.Dataset ||
|
||||
if (requirement.Dataset != "" && spec.ChildSource.Dataset != requirement.Dataset) ||
|
||||
spec.ChildSource.Column != requirement.Column ||
|
||||
spec.ChildSource.Predicate != requirement.Predicate {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
@@ -2081,7 +2083,10 @@ func validateGeneratedFeatFamilyCompleteness(path string, spec familyExpansionSp
|
||||
}
|
||||
if include {
|
||||
if familyAllowlist {
|
||||
slug := strings.TrimPrefix(sourceKey, spec.ChildSource.Dataset+":")
|
||||
slug := sourceKey
|
||||
if idx := strings.Index(slug, ":"); idx >= 0 {
|
||||
slug = slug[idx+1:]
|
||||
}
|
||||
featKey, _, _, err := ctx.resolveGeneratedFeatIdentity(spec, slug, row)
|
||||
if err != nil {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,198 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func importLegacyVFXPersistent(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
if strings.TrimSpace(referenceBuilderDir) == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "vfx_persistent")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "vfx_persistent")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
targetModulePath := filepath.Join(targetModulesDir, "freeformaoes.json")
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) && fileExists(targetModulePath) {
|
||||
baseObj, baseErr := loadJSONObject(targetBasePath)
|
||||
lockObj, lockErr := loadJSONObject(targetLockPath)
|
||||
if baseErr == nil && lockErr == nil && vfxPersistentImportComplete(baseObj, lockObj) {
|
||||
legacyModulePaths, err := collectJSONRelativePaths(filepath.Join(legacyDir, "modules"))
|
||||
if err == nil {
|
||||
targetModulePaths, err := collectJSONRelativePaths(targetModulesDir)
|
||||
if err == nil && equalStringSlices(legacyModulePaths, targetModulePaths) {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
lockObj, err := synthesizeVFXPersistentKeys(baseObj)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
legacyLockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for key, value := range legacyLockObj {
|
||||
lockObj[key] = value
|
||||
}
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
updated := 0
|
||||
for _, write := range []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
} {
|
||||
changed, err := writeJSONObjectIfChanged(write.path, write.obj)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if changed {
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
moduleRelPaths, err := collectJSONRelativePaths(filepath.Join(legacyDir, "modules"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, relPath := range moduleRelPaths {
|
||||
moduleObj, err := loadJSONObject(filepath.Join(legacyDir, "modules", filepath.FromSlash(relPath)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
targetPath := filepath.Join(targetModulesDir, filepath.FromSlash(relPath))
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
changed, err := writeJSONObjectIfChanged(targetPath, moduleObj)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if changed {
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
targetModulePaths, err := collectJSONRelativePaths(targetModulesDir)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
legacyModules := make(map[string]struct{}, len(moduleRelPaths))
|
||||
for _, relPath := range moduleRelPaths {
|
||||
legacyModules[relPath] = struct{}{}
|
||||
}
|
||||
for _, relPath := range targetModulePaths {
|
||||
if _, ok := legacyModules[relPath]; ok {
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(filepath.Join(targetModulesDir, filepath.FromSlash(relPath))); err != nil && !os.IsNotExist(err) {
|
||||
return 0, err
|
||||
}
|
||||
updated++
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func vfxPersistentImportComplete(baseObj, lockObj map[string]any) bool {
|
||||
if !vfxPersistentRowsHaveKeys(baseObj) {
|
||||
return false
|
||||
}
|
||||
expected := map[string]int{
|
||||
"vfx_persistent:blankradius5": 47,
|
||||
"vfx_persistent:blankradius10": 48,
|
||||
"vfx_persistent:blankradius15": 49,
|
||||
"vfx_persistent:blankradius20": 50,
|
||||
"vfx_persistent:blankradius25": 51,
|
||||
"vfx_persistent:blankradius30": 52,
|
||||
}
|
||||
for key, want := range expected {
|
||||
raw, ok := lockObj[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
got, err := asInt(raw)
|
||||
if err != nil || got != want {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func synthesizeVFXPersistentKeys(baseObj map[string]any) (map[string]any, error) {
|
||||
rawRows, ok := baseObj["rows"].([]any)
|
||||
if !ok {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
lockObj := map[string]any{}
|
||||
for _, raw := range rawRows {
|
||||
row, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
label, _ := row["LABEL"].(string)
|
||||
if label == "" {
|
||||
continue
|
||||
}
|
||||
key := "vfx_persistent:" + strings.ToLower(label)
|
||||
row["key"] = key
|
||||
if rawID, ok := row["id"]; ok {
|
||||
lockObj[key] = rawID
|
||||
}
|
||||
}
|
||||
return lockObj, nil
|
||||
}
|
||||
|
||||
func vfxPersistentRowsHaveKeys(baseObj map[string]any) bool {
|
||||
rawRows, ok := baseObj["rows"].([]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, raw := range rawRows {
|
||||
row, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
key, _ := row["key"].(string)
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func equalStringSlices(left, right []string) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for i := range left {
|
||||
if left[i] != right[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package topdata
|
||||
|
||||
func importLegacyVisualeffects(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
return importLegacyDatasetMirror(referenceBuilderDir, dataDir, "visualeffects", "visualeffects.2da", nil)
|
||||
}
|
||||
@@ -569,13 +569,15 @@ func loadWikiContext(dataDir, sourceDir string) (*wikiContext, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loadBase := func(name string) (nativeCollectedDataset, bool, error) {
|
||||
loadBase := func(names ...string) (nativeCollectedDataset, bool, error) {
|
||||
for _, name := range names {
|
||||
for _, dataset := range datasets {
|
||||
if dataset.Name == name {
|
||||
collected, err := collectNativeDataset(dataset)
|
||||
return collected, true, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nativeCollectedDataset{}, false, nil
|
||||
}
|
||||
|
||||
@@ -583,7 +585,7 @@ func loadWikiContext(dataDir, sourceDir string) (*wikiContext, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
skillDataset, skillOK, err := loadBase("skills")
|
||||
skillDataset, skillOK, err := loadBase("skills", "skills/core")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func importLegacyWingmodel(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
|
||||
_ = legacyTLK
|
||||
|
||||
legacyDir := filepath.Join(referenceBuilderDir, "data", "wingmodel")
|
||||
if _, err := os.Stat(legacyDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(dataDir, "wingmodel")
|
||||
targetBasePath := filepath.Join(targetDir, "base.json")
|
||||
targetLockPath := filepath.Join(targetDir, "lock.json")
|
||||
targetModulesDir := filepath.Join(targetDir, "modules")
|
||||
targetModulePaths := []string{
|
||||
filepath.Join(targetModulesDir, "add.json"),
|
||||
filepath.Join(targetModulesDir, "ovr_wingprefixes.json"),
|
||||
}
|
||||
if fileExists(targetBasePath) && fileExists(targetLockPath) {
|
||||
allModulesPresent := true
|
||||
for _, path := range targetModulePaths {
|
||||
if !fileExists(path) {
|
||||
allModulesPresent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allModulesPresent {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
baseObj, err := loadJSONObject(filepath.Join(legacyDir, "base.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
baseObj["output"] = "wingmodel.2da"
|
||||
|
||||
lockObj, err := loadJSONObject(filepath.Join(legacyDir, "lock.json"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
moduleNames := []string{"add.json", "ovr_wingprefixes.json"}
|
||||
moduleObjs := make([]map[string]any, 0, len(moduleNames))
|
||||
for _, name := range moduleNames {
|
||||
obj, err := loadJSONObject(filepath.Join(legacyDir, "modules", name))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
moduleObjs = append(moduleObjs, obj)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(targetModulesDir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
writes := []struct {
|
||||
path string
|
||||
obj map[string]any
|
||||
}{
|
||||
{path: targetBasePath, obj: baseObj},
|
||||
{path: targetLockPath, obj: lockObj},
|
||||
{path: targetModulePaths[0], obj: moduleObjs[0]},
|
||||
{path: targetModulePaths[1], obj: moduleObjs[1]},
|
||||
}
|
||||
for _, write := range writes {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user