The one-time legacy normalize migration finished; NormalizeProject had no production callers. Delete migrate.go, the 26 *_migrate.go files, the importLegacyItempropsRegistry cluster, loadLegacyTLK, and the 48 test blocks that only exercised them. mergeLegacyTLK moves to tlk_native.go because the live TLK compiler still uses it for base_dialog.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
824 lines
24 KiB
Go
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),
|
|
})
|
|
}
|
|
}
|