Files
archvillainette e509b7a90a
test / test (push) Successful in 1m16s
build-binaries / build-binaries (push) Successful in 2m7s
Remove completed legacy-migration cluster from topdata (#45)
The legacy normalize migration is done (confirmed). This deletes the dead migration path: NormalizeProject + 26 *_migrate.go files, the importLegacyItempropsRegistry cluster, loadLegacyTLK, and their tests (~7.9k lines, -33 files worth). mergeLegacyTLK moved into tlk_native.go since the live TLK compiler still uses it for base_dialog.json. make check passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #45
Reviewed-by: gitea-bot <gitea-bot@noreply.git.westgate.pw>
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
2026-07-19 08:55:24 +00:00

824 lines
24 KiB
Go

package topdata
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
)
const (
itempropsRegistryDirName = "itemprops/registry"
itempropsRegistryLock = "lock.json"
)
var itempropsAvailabilityColumns = []string{
"0_Melee",
"1_Ranged",
"2_Thrown",
"3_Staves",
"4_Rods",
"5_Ammo",
"6_Arm_Shld",
"7_Helm",
"8_Potions",
"9_Scrolls",
"10_Wands",
"11_Thieves",
"12_TrapKits",
"13_Hide",
"14_Claw",
"15_Misc_Uneq",
"16_Misc",
"17_No_Props",
"18_Containers",
"19_HealerKit",
"20_Torch",
"21_Glove",
}
var itempropsAvailabilityAliases = map[string]string{
"0_melee": "melee",
"1_ranged": "ranged",
"2_thrown": "thrown",
"3_staves": "staves",
"4_rods": "rods",
"5_ammo": "ammo",
"6_arm_shld": "arm_shld",
"7_helm": "helm",
"8_potions": "potions",
"9_scrolls": "scrolls",
"10_wands": "wands",
"11_thieves": "thieves",
"12_trapkits": "trapkits",
"13_hide": "hide",
"14_claw": "claw",
"15_misc_uneq": "misc_uneq",
"16_misc": "misc",
"17_no_props": "no_props",
"18_containers": "containers",
"19_healerkit": "healerkit",
"20_torch": "torch",
"21_glove": "glove",
}
var itempropsAvailabilityToColumn = map[string]string{
"melee": "0_Melee",
"ranged": "1_Ranged",
"thrown": "2_Thrown",
"staves": "3_Staves",
"rods": "4_Rods",
"ammo": "5_Ammo",
"arm_shld": "6_Arm_Shld",
"helm": "7_Helm",
"potions": "8_Potions",
"scrolls": "9_Scrolls",
"wands": "10_Wands",
"thieves": "11_Thieves",
"trapkits": "12_TrapKits",
"hide": "13_Hide",
"claw": "14_Claw",
"misc_uneq": "15_Misc_Uneq",
"misc": "16_Misc",
"no_props": "17_No_Props",
"containers": "18_Containers",
"healerkit": "19_HealerKit",
"torch": "20_Torch",
"glove": "21_Glove",
}
type itempropsRegistry struct {
RootPath string
LockPath string
LockData map[string]int
Properties []map[string]any
CostModels []map[string]any
ParamModels []map[string]any
SubtypeModels []map[string]any
}
func loadItempropsRegistry(dataDir string) (*itempropsRegistry, error) {
root := filepath.Join(dataDir, filepath.FromSlash(itempropsRegistryDirName))
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("itemprops registry root must be a directory: %s", root)
}
lockData, err := loadLockfile(filepath.Join(root, itempropsRegistryLock))
if err != nil {
return nil, err
}
properties, err := loadRegistryRows(filepath.Join(root, "properties.json"))
if err != nil {
return nil, err
}
costModels, err := loadRegistryRows(filepath.Join(root, "cost_models.json"))
if err != nil {
return nil, err
}
paramModels, err := loadRegistryRows(filepath.Join(root, "param_models.json"))
if err != nil {
return nil, err
}
subtypeModels, err := loadRegistryRows(filepath.Join(root, "subtype_models.json"))
if err != nil {
return nil, err
}
return &itempropsRegistry{
RootPath: root,
LockPath: filepath.Join(root, itempropsRegistryLock),
LockData: lockData,
Properties: properties,
CostModels: costModels,
ParamModels: paramModels,
SubtypeModels: subtypeModels,
}, nil
}
func loadRegistryRows(path string) ([]map[string]any, error) {
obj, err := loadJSONObject(path)
if err != nil {
return nil, err
}
rawRows, ok := obj["rows"].([]any)
if !ok {
return nil, fmt.Errorf("%s: rows must be an array", path)
}
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", path, index)
}
rows = append(rows, row)
}
return rows, nil
}
func collectItempropsRegistryDatasets(dataDir string, persistLocks bool) ([]nativeCollectedDataset, error) {
registry, err := loadItempropsRegistry(dataDir)
if err != nil {
return nil, err
}
if registry == nil {
return nil, nil
}
propertyIDs, lockModified, err := assignRegistryIDs(registry.Properties, registry.LockData, "itemprop:")
if err != nil {
return nil, err
}
costIDs, costModified, err := assignRegistryIDs(registry.CostModels, registry.LockData, "cost_models:")
if err != nil {
return nil, err
}
paramIDs, paramModified, err := assignRegistryIDs(registry.ParamModels, registry.LockData, "param_models:")
if err != nil {
return nil, err
}
lockModified = lockModified || costModified || paramModified
if lockModified && persistLocks {
if err := saveLockfile(registry.LockPath, registry.LockData); err != nil {
return nil, err
}
}
subtypeByKey := map[string]map[string]any{}
for _, row := range registry.SubtypeModels {
key := stringField(row, "key")
if key != "" {
subtypeByKey[key] = row
}
}
costByKey := map[string]map[string]any{}
for _, row := range registry.CostModels {
key := stringField(row, "key")
if key != "" {
costByKey[key] = row
}
}
paramByKey := map[string]map[string]any{}
for _, row := range registry.ParamModels {
key := stringField(row, "key")
if key != "" {
paramByKey[key] = row
}
}
datasets := make([]nativeCollectedDataset, 0)
itempropsRows := make([]map[string]any, 0, len(registry.Properties))
itempropdefRows := make([]map[string]any, 0, len(registry.Properties))
propertyRowsSorted := slices.Clone(registry.Properties)
slices.SortFunc(propertyRowsSorted, func(a, b map[string]any) int {
return propertyIDs[stringField(a, "key")] - propertyIDs[stringField(b, "key")]
})
for _, property := range propertyRowsSorted {
key := stringField(property, "key")
rowID := propertyIDs[key]
itempropsRow := map[string]any{
"id": rowID,
"key": "itemprops:" + strings.TrimPrefix(key, "itemprop:"),
"StringRef": itempropsSetName(property),
"Label": itempropsSetLabel(property),
}
for _, column := range itempropsAvailabilityColumns {
itempropsRow[column] = nullValue
}
availability, err := canonicalAvailability(property)
if err != nil {
return nil, fmt.Errorf("%s: %w", key, err)
}
for _, group := range availability {
column := itempropsAvailabilityToColumn[group]
itempropsRow[column] = "1"
}
itempropsRows = append(itempropsRows, itempropsRow)
subtypeResRef := nullValue
if subtypeKey, ok := refField(property["subtype"]); ok {
subtype, ok := subtypeByKey[subtypeKey]
if !ok {
return nil, fmt.Errorf("%s: unknown subtype ref %s", key, subtypeKey)
}
subtypeResRef = stringField(subtype, "table_resref")
if subtypeResRef == "" {
return nil, fmt.Errorf("%s: subtype model %s is missing table_resref", key, subtypeKey)
}
}
costValue := nullValue
costRefID := nullValue
if rawCost, ok := property["cost"].(map[string]any); ok {
if value, ok := rawCost["value"]; ok {
costValue = formatRegistryScalar(value)
}
costRefKey, ok := refField(rawCost)
if ok {
id, ok := costIDs[costRefKey]
if !ok {
return nil, fmt.Errorf("%s: unknown cost ref %s", key, costRefKey)
}
costRefID = strconv.Itoa(id)
}
}
paramRefID := nullValue
if paramKey, ok := refField(property["param"]); ok {
id, ok := paramIDs[paramKey]
if !ok {
return nil, fmt.Errorf("%s: unknown param ref %s", key, paramKey)
}
paramRefID = strconv.Itoa(id)
}
itempropdefRows = append(itempropdefRows, map[string]any{
"id": rowID,
"key": "itempropdef:" + strings.TrimPrefix(key, "itemprop:"),
"Name": deepCopyValue(property["name"]),
"Label": itempropsDefLabel(property),
"SubTypeResRef": subtypeResRef,
"Cost": costValue,
"CostTableResRef": costRefID,
"Param1ResRef": paramRefID,
"GameStrRef": deepCopyValue(property["property_text"]),
"Description": nullableRegistryField(property, "description"),
})
}
costRows := make([]map[string]any, 0, len(registry.CostModels))
costRowsSorted := slices.Clone(registry.CostModels)
slices.SortFunc(costRowsSorted, func(a, b map[string]any) int {
return costIDs[stringField(a, "key")] - costIDs[stringField(b, "key")]
})
for _, costModel := range costRowsSorted {
key := stringField(costModel, "key")
costRows = append(costRows, map[string]any{
"id": costIDs[key],
"key": key,
"Name": stringField(costModel, "index_name"),
"Label": stringField(costModel, "label"),
"ClientLoad": formatRegistryScalar(costModel["client_load"]),
})
}
paramRows := make([]map[string]any, 0, len(registry.ParamModels))
paramRowsSorted := slices.Clone(registry.ParamModels)
slices.SortFunc(paramRowsSorted, func(a, b map[string]any) int {
return paramIDs[stringField(a, "key")] - paramIDs[stringField(b, "key")]
})
for _, paramModel := range paramRowsSorted {
key := stringField(paramModel, "key")
table, ok := paramModel["table"].(map[string]any)
if !ok {
return nil, fmt.Errorf("%s: param model missing table", key)
}
paramRows = append(paramRows, map[string]any{
"id": paramIDs[key],
"key": key,
"Name": nullableRegistryField(paramModel, "name"),
"Lable": stringField(paramModel, "label"),
"TableResRef": stringField(table, "resref"),
})
}
datasets = append(datasets,
newGeneratedDataset("itemprops/registry/itemprops", "itemprops.2da", itempropsAvailabilityColumnsWithMeta(), itempropsRows),
newGeneratedDataset("itemprops/registry/itempropdef", "itempropdef.2da", []string{"Name", "Label", "SubTypeResRef", "Cost", "CostTableResRef", "Param1ResRef", "GameStrRef", "Description"}, itempropdefRows),
newGeneratedDataset("itemprops/registry/costtable_index", "iprp_costtable.2da", []string{"Name", "Label", "ClientLoad"}, costRows),
newGeneratedDataset("itemprops/registry/paramtable_index", "iprp_paramtable.2da", []string{"Name", "Lable", "TableResRef"}, paramRows),
)
auxDatasets, err := collectItempropsAuxiliaryDatasets(registry.CostModels, "cost_models", "itemprops/registry/cost_models")
if err != nil {
return nil, err
}
datasets = append(datasets, auxDatasets...)
auxDatasets, err = collectItempropsAuxiliaryDatasets(registry.ParamModels, "param_models", "itemprops/registry/param_models")
if err != nil {
return nil, err
}
datasets = append(datasets, auxDatasets...)
auxDatasets, err = collectItempropsAuxiliaryDatasets(registry.SubtypeModels, "subtype_models", "itemprops/registry/subtype_models")
if err != nil {
return nil, err
}
datasets = append(datasets, auxDatasets...)
return datasets, nil
}
func newGeneratedDataset(name, outputName string, columns []string, rows []map[string]any) nativeCollectedDataset {
return nativeCollectedDataset{
Dataset: nativeDataset{
Kind: nativeDatasetPlain,
Name: name,
OutputName: outputName,
Spec: specForDataset(name),
},
Columns: columns,
Rows: rows,
LockData: map[string]int{},
}
}
func itempropsAvailabilityColumnsWithMeta() []string {
columns := append([]string{}, itempropsAvailabilityColumns...)
columns = append(columns, "StringRef", "Label")
return columns
}
func collectItempropsAuxiliaryDatasets(models []map[string]any, keyPrefix, namePrefix string) ([]nativeCollectedDataset, error) {
out := make([]nativeCollectedDataset, 0)
for _, model := range models {
key := stringField(model, "key")
table, ok := model["table"].(map[string]any)
if !ok {
continue
}
if !strings.EqualFold(stringField(table, "ownership"), "owned") {
continue
}
outputName := stringField(table, "output")
if outputName == "" {
outputName = stringField(table, "resref") + ".2da"
}
rawColumns, ok := table["columns"].([]any)
if !ok {
return nil, fmt.Errorf("%s: owned table is missing columns", key)
}
columns := make([]string, 0, len(rawColumns))
for _, rawColumn := range rawColumns {
column, ok := rawColumn.(string)
if !ok {
return nil, fmt.Errorf("%s: owned table columns must be strings", key)
}
columns = append(columns, column)
}
rawRows, ok := table["rows"].([]any)
if !ok {
return nil, fmt.Errorf("%s: owned table rows must be an array", key)
}
rows := make([]map[string]any, 0, len(rawRows))
for index, rawRow := range rawRows {
row, ok := rawRow.(map[string]any)
if !ok {
return nil, fmt.Errorf("%s: owned table row %d must be an object", key, index)
}
canonical, err := canonicalizeBaseRow(nativeDataset{Name: keyPrefix + "/" + strings.TrimPrefix(key, keyPrefix+":"), Spec: specForDataset("itemprops")}, columns, row, index)
if err != nil {
return nil, err
}
rows = append(rows, canonical)
}
out = append(out, newGeneratedDataset(namePrefix+"/"+strings.TrimPrefix(key, keyPrefix+":"), outputName, columns, rows))
}
return out, nil
}
func assignRegistryIDs(rows []map[string]any, lockData map[string]int, requiredPrefix string) (map[string]int, bool, error) {
ids := map[string]int{}
used := map[int]struct{}{}
for key, id := range lockData {
if strings.HasPrefix(key, requiredPrefix) {
used[id] = struct{}{}
}
}
modified := false
for _, row := range rows {
key := stringField(row, "key")
if key == "" {
return nil, false, fmt.Errorf("registry row is missing key")
}
if !strings.HasPrefix(key, requiredPrefix) {
return nil, false, fmt.Errorf("registry key %q must use prefix %q", key, requiredPrefix)
}
if id, ok := lockData[key]; ok {
ids[key] = id
used[id] = struct{}{}
continue
}
}
nextID := nextAvailableID(used)
for _, row := range rows {
key := stringField(row, "key")
if _, ok := ids[key]; ok {
continue
}
lockData[key] = nextID
ids[key] = nextID
used[nextID] = struct{}{}
nextID = nextAvailableID(used)
modified = true
}
return ids, modified, nil
}
func canonicalAvailability(row map[string]any) ([]string, error) {
rawAvailability, ok := row["availability"]
if !ok {
return nil, nil
}
rawList, ok := rawAvailability.([]any)
if !ok {
return nil, fmt.Errorf("availability must be an array")
}
out := make([]string, 0, len(rawList))
seen := map[string]struct{}{}
for _, raw := range rawList {
text, ok := raw.(string)
if !ok {
return nil, fmt.Errorf("availability values must be strings")
}
normalized := normalizeAvailabilityName(text)
if _, ok := itempropsAvailabilityToColumn[normalized]; !ok {
return nil, fmt.Errorf("unknown availability group %q", text)
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
out = append(out, normalized)
}
slices.Sort(out)
return out, nil
}
func normalizeAvailabilityName(name string) string {
normalized := strings.TrimSpace(strings.ToLower(name))
normalized = strings.ReplaceAll(normalized, " ", "_")
normalized = strings.ReplaceAll(normalized, "-", "_")
if alias, ok := itempropsAvailabilityAliases[normalized]; ok {
return alias
}
return normalized
}
func refField(value any) (string, bool) {
if value == nil {
return "", false
}
refMap, ok := value.(map[string]any)
if !ok {
return "", false
}
rawRef, ok := refMap["ref"]
if !ok {
return "", false
}
text, ok := rawRef.(string)
return text, ok && strings.TrimSpace(text) != ""
}
func stringField(obj map[string]any, field string) string {
raw, ok := obj[field]
if !ok || raw == nil {
return ""
}
switch typed := raw.(type) {
case string:
return typed
case float64:
if typed == float64(int64(typed)) {
return strconv.FormatInt(int64(typed), 10)
}
return strconv.FormatFloat(typed, 'f', -1, 64)
default:
return ""
}
}
func nullableRegistryField(obj map[string]any, field string) any {
value, ok := obj[field]
if !ok || value == nil {
return nullValue
}
if text, ok := value.(string); ok && strings.TrimSpace(text) == "" {
return nullValue
}
return deepCopyValue(value)
}
func itempropsDefLabel(obj map[string]any) string {
return stringField(obj, "label")
}
func itempropsSetName(obj map[string]any) any {
if value, ok := obj["set_name"]; ok {
return deepCopyValue(value)
}
return deepCopyValue(obj["name"])
}
func itempropsSetLabel(obj map[string]any) string {
if label := stringField(obj, "set_label"); label != "" {
return label
}
return itempropsDefLabel(obj)
}
func formatRegistryScalar(value any) string {
if value == nil {
return nullValue
}
switch typed := value.(type) {
case string:
if strings.TrimSpace(typed) == "" {
return nullValue
}
return typed
case float64:
if typed == float64(int64(typed)) {
return strconv.FormatInt(int64(typed), 10)
}
return strconv.FormatFloat(typed, 'f', -1, 64)
case int:
return strconv.Itoa(typed)
default:
return fmt.Sprint(typed)
}
}
func deepCopyValue(value any) any {
raw, err := json.Marshal(value)
if err != nil {
return value
}
var out any
if err := json.Unmarshal(raw, &out); err != nil {
return value
}
return out
}
func validateItempropsRegistryGraph(dataDir string, report *ValidationReport) {
registry, err := loadItempropsRegistry(dataDir)
if err != nil {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(dataDir, filepath.FromSlash(itempropsRegistryDirName)),
Message: err.Error(),
})
return
}
if registry == nil {
return
}
seenPropertyKeys := map[string]struct{}{}
seenDefLabels := map[string]string{}
seenSetLabels := map[string]string{}
seenCostKeys := map[string]struct{}{}
seenParamKeys := map[string]struct{}{}
seenSubtypeKeys := map[string]string{}
ownedOutputs := map[string]string{}
for _, row := range registry.CostModels {
key := stringField(row, "key")
if key == "" {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "cost_models.json"),
Message: "cost model row is missing key",
})
continue
}
if _, ok := seenCostKeys[key]; ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "cost_models.json"),
Message: fmt.Sprintf("duplicate cost model key %q", key),
})
}
seenCostKeys[key] = struct{}{}
validateOwnedRegistryTable(filepath.Join(registry.RootPath, "cost_models.json"), key, row, ownedOutputs, report)
}
for _, row := range registry.ParamModels {
key := stringField(row, "key")
if key == "" {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "param_models.json"),
Message: "param model row is missing key",
})
continue
}
if _, ok := seenParamKeys[key]; ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "param_models.json"),
Message: fmt.Sprintf("duplicate param model key %q", key),
})
}
seenParamKeys[key] = struct{}{}
validateOwnedRegistryTable(filepath.Join(registry.RootPath, "param_models.json"), key, row, ownedOutputs, report)
}
for _, row := range registry.SubtypeModels {
key := stringField(row, "key")
if key == "" {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "subtype_models.json"),
Message: "subtype model row is missing key",
})
continue
}
resref := stringField(row, "table_resref")
if existing, ok := seenSubtypeKeys[key]; ok {
if !strings.EqualFold(existing, resref) {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "subtype_models.json"),
Message: fmt.Sprintf("duplicate subtype model key %q", key),
})
}
continue
}
seenSubtypeKeys[key] = resref
validateOwnedRegistryTable(filepath.Join(registry.RootPath, "subtype_models.json"), key, row, ownedOutputs, report)
}
for _, row := range registry.Properties {
key := stringField(row, "key")
if key == "" {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: "property row is missing key",
})
continue
}
if _, ok := seenPropertyKeys[key]; ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("duplicate property key %q", key),
})
}
seenPropertyKeys[key] = struct{}{}
defLabel := itempropsDefLabel(row)
if defLabel == "" {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("%s is missing label", key),
})
} else if owner, ok := seenDefLabels[strings.ToLower(defLabel)]; ok && owner != key {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("generated itempropdef label collision between %q and %q", owner, key),
})
} else {
seenDefLabels[strings.ToLower(defLabel)] = key
}
setLabel := itempropsSetLabel(row)
if setLabel == "" {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("%s is missing itemprops set label", key),
})
} else if owner, ok := seenSetLabels[strings.ToLower(setLabel)]; ok && owner != key {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("generated itemprops set label collision between %q and %q", owner, key),
})
} else {
seenSetLabels[strings.ToLower(setLabel)] = key
}
if _, err := canonicalAvailability(row); err != nil {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("%s: %v", key, err),
})
}
if subtypeKey, ok := refField(row["subtype"]); ok {
if _, exists := seenSubtypeKeys[subtypeKey]; !exists {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("%s: unknown subtype ref %s", key, subtypeKey),
})
}
}
if rawCost, ok := row["cost"].(map[string]any); ok {
if costKey, ok := refField(rawCost); ok {
if _, exists := seenCostKeys[costKey]; !exists {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("%s: unknown cost ref %s", key, costKey),
})
}
}
}
if paramKey, ok := refField(row["param"]); ok {
if _, exists := seenParamKeys[paramKey]; !exists {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: filepath.Join(registry.RootPath, "properties.json"),
Message: fmt.Sprintf("%s: unknown param ref %s", key, paramKey),
})
}
}
}
}
func validateOwnedRegistryTable(path, key string, row map[string]any, ownedOutputs map[string]string, report *ValidationReport) {
table, ok := row["table"].(map[string]any)
if !ok {
return
}
if !strings.EqualFold(stringField(table, "ownership"), "owned") {
return
}
resref := stringField(table, "resref")
if resref == "" {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: path,
Message: fmt.Sprintf("%s: owned table is missing resref", key),
})
return
}
outputName := stringField(table, "output")
if outputName == "" {
outputName = resref + ".2da"
}
if owner, ok := ownedOutputs[strings.ToLower(outputName)]; ok && owner != key {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: path,
Message: fmt.Sprintf("owned table output collision between %q and %q", owner, key),
})
} else {
ownedOutputs[strings.ToLower(outputName)] = key
}
if _, ok := table["columns"].([]any); !ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: path,
Message: fmt.Sprintf("%s: owned table columns must be an array", key),
})
}
if _, ok := table["rows"].([]any); !ok {
report.Diagnostics = append(report.Diagnostics, Diagnostic{
Severity: SeverityError,
Path: path,
Message: fmt.Sprintf("%s: owned table rows must be an array", key),
})
}
}