Global json adjustments
This commit is contained in:
+333
-17
@@ -58,6 +58,19 @@ type nativeCollectedDataset struct {
|
||||
LockModified bool
|
||||
}
|
||||
|
||||
type nativeRowSource string
|
||||
|
||||
const (
|
||||
nativeRowSourceBase nativeRowSource = "base"
|
||||
nativeRowSourceEntries nativeRowSource = "entries"
|
||||
nativeRowSourceOverride nativeRowSource = "overrides"
|
||||
)
|
||||
|
||||
type nativeRowIdentity struct {
|
||||
ID int
|
||||
Key string
|
||||
}
|
||||
|
||||
type resolvedTable struct {
|
||||
Key string
|
||||
DatasetName string
|
||||
@@ -1198,6 +1211,87 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
lockAdded := 0
|
||||
lockPruned := 0
|
||||
retiredKeys := map[string]struct{}{}
|
||||
rowIdentity := func(row map[string]any) (nativeRowIdentity, bool) {
|
||||
rowID, ok := row["id"].(int)
|
||||
if !ok {
|
||||
return nativeRowIdentity{}, false
|
||||
}
|
||||
key, _ := row["key"].(string)
|
||||
return nativeRowIdentity{ID: rowID, Key: key}, true
|
||||
}
|
||||
rowSourceByIdentity := map[nativeRowIdentity]nativeRowSource{}
|
||||
sourceForIdentity := func(identity nativeRowIdentity, ok bool) nativeRowSource {
|
||||
if !ok {
|
||||
return nativeRowSourceBase
|
||||
}
|
||||
if source, ok := rowSourceByIdentity[identity]; ok {
|
||||
return source
|
||||
}
|
||||
return nativeRowSourceBase
|
||||
}
|
||||
setRowSource := func(row map[string]any, source nativeRowSource) {
|
||||
identity, ok := rowIdentity(row)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rowSourceByIdentity[identity] = source
|
||||
}
|
||||
moveRowSource := func(before nativeRowIdentity, beforeOK bool, row map[string]any, source nativeRowSource) {
|
||||
after, afterOK := rowIdentity(row)
|
||||
if beforeOK && (!afterOK || before != after) {
|
||||
delete(rowSourceByIdentity, before)
|
||||
}
|
||||
if afterOK {
|
||||
rowSourceByIdentity[after] = source
|
||||
}
|
||||
}
|
||||
rowSource := func(row map[string]any) nativeRowSource {
|
||||
identity, ok := rowIdentity(row)
|
||||
return sourceForIdentity(identity, ok)
|
||||
}
|
||||
type pendingRowSourceMove struct {
|
||||
Row map[string]any
|
||||
Before nativeRowIdentity
|
||||
BeforeOK bool
|
||||
Source nativeRowSource
|
||||
}
|
||||
captureDisplacedRowSource := func(row map[string]any, expanded map[string]any) pendingRowSourceMove {
|
||||
newKeyValue, hasKey := expanded["key"]
|
||||
if !hasKey {
|
||||
return pendingRowSourceMove{}
|
||||
}
|
||||
newKey, ok := newKeyValue.(string)
|
||||
if !ok || newKey == "" || strings.TrimSpace(newKey) == nullValue {
|
||||
return pendingRowSourceMove{}
|
||||
}
|
||||
oldKey, _ := row["key"].(string)
|
||||
if oldKey == newKey {
|
||||
return pendingRowSourceMove{}
|
||||
}
|
||||
conflicting, ok := rowByKey[newKey]
|
||||
if !ok || conflicting == nil {
|
||||
return pendingRowSourceMove{}
|
||||
}
|
||||
conflictingID, conflictingHasID := conflicting["id"].(int)
|
||||
rowID, _ := row["id"].(int)
|
||||
rowHasID := rowID != 0 || row["id"] != nil
|
||||
if conflictingHasID && rowHasID && conflictingID == rowID {
|
||||
return pendingRowSourceMove{}
|
||||
}
|
||||
identity, identityOK := rowIdentity(conflicting)
|
||||
return pendingRowSourceMove{
|
||||
Row: conflicting,
|
||||
Before: identity,
|
||||
BeforeOK: identityOK,
|
||||
Source: rowSource(conflicting),
|
||||
}
|
||||
}
|
||||
moveCapturedRowSource := func(move pendingRowSourceMove) {
|
||||
if move.Row == nil {
|
||||
return
|
||||
}
|
||||
moveRowSource(move.Before, move.BeforeOK, move.Row, move.Source)
|
||||
}
|
||||
featFamilyAliases := generatedFamilyAliases{}
|
||||
if dataset.Name == "feat" {
|
||||
var err error
|
||||
@@ -1257,6 +1351,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, row)
|
||||
setRowSource(row, nativeRowSourceBase)
|
||||
if key, ok := row["key"].(string); ok && key != "" {
|
||||
if baseRowKeyCounts[key] > 1 {
|
||||
if lockedID, ok := lockData[key]; !ok || lockedID != rowID {
|
||||
@@ -1342,6 +1437,8 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
return true
|
||||
}
|
||||
applyOverrideFields := func(override map[string]any, row map[string]any) (bool, string, error) {
|
||||
sourceBefore := rowSource(row)
|
||||
identityBefore, identityBeforeOK := rowIdentity(row)
|
||||
candidate := map[string]any{}
|
||||
for field, value := range row {
|
||||
candidate[field] = value
|
||||
@@ -1383,6 +1480,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
displacedSource := captureDisplacedRowSource(row, expanded)
|
||||
keyChanged, retiredKey, err := updateOverrideRowKey(dataset.Name, row, expanded, rowByKey, lockData)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
@@ -1393,6 +1491,8 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
}
|
||||
row[field] = value
|
||||
}
|
||||
moveRowSource(identityBefore, identityBeforeOK, row, sourceBefore)
|
||||
moveCapturedRowSource(displacedSource)
|
||||
return keyChanged, retiredKey, nil
|
||||
}
|
||||
applyModuleData := func(sourceLabel string, moduleData map[string]any, globalSource bool) error {
|
||||
@@ -1442,6 +1542,8 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
identityBefore, identityBeforeOK := rowIdentity(existing)
|
||||
displacedSource := captureDisplacedRowSource(existing, expanded)
|
||||
keyChanged, retiredKey, err := updateOverrideRowKey(dataset.Name, existing, expanded, rowByKey, lockData)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1458,6 +1560,8 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
}
|
||||
existing[field] = value
|
||||
}
|
||||
moveRowSource(identityBefore, identityBeforeOK, existing, nativeRowSourceEntries)
|
||||
moveCapturedRowSource(displacedSource)
|
||||
continue
|
||||
}
|
||||
rowID, ok := lockedIDForKey(key)
|
||||
@@ -1472,6 +1576,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
return fmt.Errorf("dataset %s: %w", dataset.Name, err)
|
||||
}
|
||||
rows = append(rows, row)
|
||||
setRowSource(row, nativeRowSourceEntries)
|
||||
rowByID[rowID] = row
|
||||
rowByKey[key] = row
|
||||
}
|
||||
@@ -1556,14 +1661,18 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
row["key"] = rawKey
|
||||
}
|
||||
rows = append(rows, row)
|
||||
setRowSource(row, nativeRowSourceOverride)
|
||||
rowByID[rowID] = row
|
||||
if hasKey && rawKey != "" {
|
||||
rowByKey[rawKey] = row
|
||||
}
|
||||
}
|
||||
if overrideRequestsNullRow(override) {
|
||||
sourceBefore := rowSource(row)
|
||||
identityBefore, identityBeforeOK := rowIdentity(row)
|
||||
retireRowIdentity(row, lockData, retiredKeys)
|
||||
nullifyOverrideRow(row, columns, rowByKey)
|
||||
moveRowSource(identityBefore, identityBeforeOK, row, sourceBefore)
|
||||
continue
|
||||
}
|
||||
keyChanged, retiredKey, err := applyOverrideFields(override, row)
|
||||
@@ -1580,6 +1689,66 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
applyGlobalDefaults := func(sourceLabel string, moduleData map[string]any) error {
|
||||
rawDefaults, ok := moduleData["defaults"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
defaultList, ok := rawDefaults.([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("dataset %s: defaults in %s must be an array", dataset.Name, sourceLabel)
|
||||
}
|
||||
for index, rawDefault := range defaultList {
|
||||
defaultRule, ok := rawDefault.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("dataset %s: default %d in %s must be an object", dataset.Name, index, sourceLabel)
|
||||
}
|
||||
rawValues, ok := defaultRule["values"]
|
||||
if !ok {
|
||||
return fmt.Errorf("dataset %s: default %d in %s must contain values", dataset.Name, index, sourceLabel)
|
||||
}
|
||||
values, ok := rawValues.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("dataset %s: default %d values in %s must be an object", dataset.Name, index, sourceLabel)
|
||||
}
|
||||
parsedValues := make([]parsedGlobalDefaultField, 0, len(values))
|
||||
for _, field := range sortedKeys(values) {
|
||||
columnName, ok := canonicalColumn(columns, field)
|
||||
if !ok {
|
||||
return fmt.Errorf("dataset %s: default %d field %q in %s is not a known column", dataset.Name, index, field, sourceLabel)
|
||||
}
|
||||
value, err := parseGlobalDefaultValue(values[field])
|
||||
if err != nil {
|
||||
return fmt.Errorf("dataset %s: default %d field %q in %s: %w", dataset.Name, index, field, sourceLabel, err)
|
||||
}
|
||||
parsedValues = append(parsedValues, parsedGlobalDefaultField{
|
||||
Field: field,
|
||||
Column: columnName,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
for _, row := range rows {
|
||||
matches, err := globalDefaultMatchesRow(defaultRule["match"], rowSource(row))
|
||||
if err != nil {
|
||||
return fmt.Errorf("dataset %s: default %d in %s: %w", dataset.Name, index, sourceLabel, err)
|
||||
}
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
for _, parsedValue := range parsedValues {
|
||||
if !isTopDataNullLike(row[parsedValue.Column]) {
|
||||
continue
|
||||
}
|
||||
value, err := evaluateGlobalDefaultValue(parsedValue.Value, row)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dataset %s: default %d field %q in %s: %w", dataset.Name, index, parsedValue.Field, sourceLabel, err)
|
||||
}
|
||||
row[parsedValue.Column] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
generatedModules, err := collectGeneratedModules(dataset, lockData)
|
||||
if err != nil {
|
||||
@@ -1621,9 +1790,9 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
||||
if err := applyModuleData(module.Name, module.Data, true); err != nil {
|
||||
return nativeCollectedDataset{}, err
|
||||
}
|
||||
}
|
||||
if dataset.Name == "spells" {
|
||||
normalizeCollectedSpellImpactScripts(rows, baseRowKeys)
|
||||
if err := applyGlobalDefaults(module.Name, module.Data); err != nil {
|
||||
return nativeCollectedDataset{}, err
|
||||
}
|
||||
}
|
||||
referencedKeys, err := collectReferencedLockKeys(nativeDatasetSourceDir(dataset))
|
||||
if err != nil {
|
||||
@@ -3513,6 +3682,11 @@ func collectPlainDataset(dataset nativeDataset) (nativeCollectedDataset, error)
|
||||
if err != nil {
|
||||
return nativeCollectedDataset{}, err
|
||||
}
|
||||
for _, module := range globalData {
|
||||
if _, ok := module.Data["defaults"]; ok {
|
||||
return nativeCollectedDataset{}, fmt.Errorf("dataset %s: defaults in %s are only supported for base datasets", dataset.Name, module.Name)
|
||||
}
|
||||
}
|
||||
|
||||
columns, err := parseColumns(tableData, dataset.Name)
|
||||
if err != nil {
|
||||
@@ -6013,21 +6187,163 @@ func updateOverrideRowKey(datasetName string, row map[string]any, expanded map[s
|
||||
return changed, retiredKey, nil
|
||||
}
|
||||
|
||||
func normalizeCollectedSpellImpactScripts(rows []map[string]any, baseRowKeys map[string]struct{}) {
|
||||
for _, row := range rows {
|
||||
rowKey, ok := row["key"].(string)
|
||||
if !ok || rowKey == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := baseRowKeys[rowKey]; ok {
|
||||
continue
|
||||
}
|
||||
rowID, ok := row["id"].(int)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
row["ImpactScript"] = fmt.Sprintf("ss_%d", rowID)
|
||||
func isTopDataNullLike(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
trimmed := strings.TrimSpace(text)
|
||||
return trimmed == "" || trimmed == nullValue
|
||||
}
|
||||
|
||||
type parsedGlobalDefaultField struct {
|
||||
Field string
|
||||
Column string
|
||||
Value parsedGlobalDefaultValue
|
||||
}
|
||||
|
||||
type parsedGlobalDefaultValue struct {
|
||||
Literal any
|
||||
Format string
|
||||
IsFormat bool
|
||||
}
|
||||
|
||||
func globalDefaultMatchesRow(rawMatch any, source nativeRowSource) (bool, error) {
|
||||
if rawMatch == nil {
|
||||
return true, nil
|
||||
}
|
||||
if text, ok := rawMatch.(string); ok {
|
||||
if strings.EqualFold(strings.TrimSpace(text), "all") {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("unsupported defaults match value %q", text)
|
||||
}
|
||||
obj, ok := rawMatch.(map[string]any)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("defaults match must be \"all\" or an object")
|
||||
}
|
||||
rawSource, ok := obj["source"]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("defaults match object must contain source")
|
||||
}
|
||||
sourceText, ok := rawSource.(string)
|
||||
if !ok || strings.TrimSpace(sourceText) == "" {
|
||||
return false, fmt.Errorf("defaults match source must be a non-empty string")
|
||||
}
|
||||
for key := range obj {
|
||||
if key != "source" {
|
||||
return false, fmt.Errorf("defaults match field %q is not supported", key)
|
||||
}
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(sourceText)) {
|
||||
case string(nativeRowSourceBase):
|
||||
return source == nativeRowSourceBase, nil
|
||||
case string(nativeRowSourceEntries):
|
||||
return source == nativeRowSourceEntries, nil
|
||||
case string(nativeRowSourceOverride):
|
||||
return source == nativeRowSourceOverride, nil
|
||||
default:
|
||||
return false, fmt.Errorf("defaults match source %q is not supported", sourceText)
|
||||
}
|
||||
}
|
||||
|
||||
func parseGlobalDefaultValue(value any) (parsedGlobalDefaultValue, error) {
|
||||
obj, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return parsedGlobalDefaultValue{Literal: value}, nil
|
||||
}
|
||||
rawFormat, hasFormat := obj["format"]
|
||||
if !hasFormat {
|
||||
return parsedGlobalDefaultValue{Literal: value}, nil
|
||||
}
|
||||
if len(obj) != 1 {
|
||||
return parsedGlobalDefaultValue{}, fmt.Errorf("default format object must contain only format")
|
||||
}
|
||||
format, ok := rawFormat.(string)
|
||||
if !ok {
|
||||
return parsedGlobalDefaultValue{}, fmt.Errorf("default format must be a string")
|
||||
}
|
||||
if err := validateTopDataRowFormat(format); err != nil {
|
||||
return parsedGlobalDefaultValue{}, err
|
||||
}
|
||||
return parsedGlobalDefaultValue{Format: format, IsFormat: true}, nil
|
||||
}
|
||||
|
||||
func evaluateGlobalDefaultValue(value parsedGlobalDefaultValue, row map[string]any) (any, error) {
|
||||
if !value.IsFormat {
|
||||
return value.Literal, nil
|
||||
}
|
||||
return formatTopDataRowValue(value.Format, row)
|
||||
}
|
||||
|
||||
func validateTopDataRowFormat(format string) error {
|
||||
return walkTopDataRowFormat(format, nil, nil)
|
||||
}
|
||||
|
||||
func formatTopDataRowValue(format string, row map[string]any) (string, error) {
|
||||
var builder strings.Builder
|
||||
err := walkTopDataRowFormat(format, func(text string) {
|
||||
builder.WriteString(text)
|
||||
}, func(token string) error {
|
||||
switch token {
|
||||
case "id":
|
||||
rowID, ok := row["id"].(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("default format token {id} requires numeric row id")
|
||||
}
|
||||
builder.WriteString(strconv.Itoa(rowID))
|
||||
case "key":
|
||||
key, ok := row["key"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("default format token {key} requires row key")
|
||||
}
|
||||
builder.WriteString(key)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func walkTopDataRowFormat(format string, literal func(string), tokenValue func(string) error) error {
|
||||
for index := 0; index < len(format); {
|
||||
switch format[index] {
|
||||
case '{':
|
||||
end := strings.IndexByte(format[index+1:], '}')
|
||||
if end < 0 {
|
||||
return fmt.Errorf("unterminated default format token in %q", format)
|
||||
}
|
||||
token := format[index+1 : index+1+end]
|
||||
switch token {
|
||||
case "id", "key":
|
||||
default:
|
||||
return fmt.Errorf("default format token {%s} is not supported", token)
|
||||
}
|
||||
if tokenValue != nil {
|
||||
if err := tokenValue(token); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
index += end + 2
|
||||
case '}':
|
||||
return fmt.Errorf("unexpected default format token terminator in %q", format)
|
||||
default:
|
||||
start := index
|
||||
for index < len(format) && format[index] != '{' && format[index] != '}' {
|
||||
index++
|
||||
}
|
||||
if literal != nil {
|
||||
literal(format[start:index])
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func write2DA(data map[string]any, path string, denseRows bool, denseRowMax int) error {
|
||||
|
||||
@@ -972,6 +972,9 @@ func validateGlobalJSONFile(path string, obj map[string]any, report *ValidationR
|
||||
if _, ok := obj["overrides"]; ok {
|
||||
validateOverridesFile(path, obj, report)
|
||||
}
|
||||
if _, ok := obj["defaults"]; ok {
|
||||
validateGlobalDefaults(path, obj, report)
|
||||
}
|
||||
if rawPosition, ok := obj["position"]; ok {
|
||||
position, ok := rawPosition.(string)
|
||||
if !ok {
|
||||
@@ -1032,6 +1035,191 @@ func validateGlobalJSONFile(path string, obj map[string]any, report *ValidationR
|
||||
validateRowCollection(path, obj, rows, report)
|
||||
}
|
||||
|
||||
func topdataColumnsForGlobalValidation(path string, obj map[string]any) ([]string, bool) {
|
||||
basePath := filepath.Join(filepath.Dir(path), "base.json")
|
||||
baseData, err := loadJSONObject(basePath)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
datasetName := filepath.ToSlash(filepath.Base(filepath.Dir(path)))
|
||||
columns, err := parseColumns(baseData, datasetName)
|
||||
if err != nil {
|
||||
return nil, true
|
||||
}
|
||||
modulePaths, err := collectModulePaths(filepath.Join(filepath.Dir(path), "modules"))
|
||||
if err == nil {
|
||||
for _, modulePath := range modulePaths {
|
||||
moduleData, err := loadJSONObject(modulePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
columns, err = extendColumns(columns, moduleData, datasetName, modulePath)
|
||||
if err != nil {
|
||||
return columns, true
|
||||
}
|
||||
}
|
||||
}
|
||||
columns, err = extendColumns(columns, obj, datasetName, path)
|
||||
if err != nil {
|
||||
return columns, true
|
||||
}
|
||||
return columns, true
|
||||
}
|
||||
|
||||
func validateGlobalDefaults(path string, obj map[string]any, report *ValidationReport) {
|
||||
rawDefaults, ok := obj["defaults"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defaults, ok := rawDefaults.([]any)
|
||||
if !ok {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: "global defaults must be a JSON array",
|
||||
})
|
||||
return
|
||||
}
|
||||
columns, hasBaseDataset := topdataColumnsForGlobalValidation(path, obj)
|
||||
if !hasBaseDataset {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: "global defaults require a sibling base.json dataset",
|
||||
})
|
||||
return
|
||||
}
|
||||
for index, rawDefault := range defaults {
|
||||
defaultRule, ok := rawDefault.(map[string]any)
|
||||
if !ok {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d must be an object", index),
|
||||
})
|
||||
continue
|
||||
}
|
||||
validateGlobalDefaultMatch(path, index, defaultRule["match"], report)
|
||||
rawValues, ok := defaultRule["values"]
|
||||
if !ok {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d values is required", index),
|
||||
})
|
||||
continue
|
||||
}
|
||||
values, ok := rawValues.(map[string]any)
|
||||
if !ok {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d values must be an object", index),
|
||||
})
|
||||
continue
|
||||
}
|
||||
for field, value := range values {
|
||||
if _, ok := canonicalColumn(columns, field); !ok {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d values.%s is not a known column", index, field),
|
||||
})
|
||||
}
|
||||
validateGlobalDefaultValue(path, index, field, value, report)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateGlobalDefaultMatch(path string, index int, rawMatch any, report *ValidationReport) {
|
||||
if rawMatch == nil {
|
||||
return
|
||||
}
|
||||
if text, ok := rawMatch.(string); ok {
|
||||
if strings.EqualFold(strings.TrimSpace(text), "all") {
|
||||
return
|
||||
}
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d match must be all or an object", index),
|
||||
})
|
||||
return
|
||||
}
|
||||
match, ok := rawMatch.(map[string]any)
|
||||
if !ok {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d match must be all or an object", index),
|
||||
})
|
||||
return
|
||||
}
|
||||
for key := range match {
|
||||
if key != "source" {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d match.%s is not supported", index, key),
|
||||
})
|
||||
}
|
||||
}
|
||||
source, ok := match["source"].(string)
|
||||
if !ok || strings.TrimSpace(source) == "" {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d match.source is required", index),
|
||||
})
|
||||
return
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(source)) {
|
||||
case string(nativeRowSourceBase), string(nativeRowSourceEntries), string(nativeRowSourceOverride):
|
||||
default:
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d match.source is not supported", index),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func validateGlobalDefaultValue(path string, index int, field string, value any, report *ValidationReport) {
|
||||
obj, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rawFormat, hasFormat := obj["format"]
|
||||
if !hasFormat {
|
||||
return
|
||||
}
|
||||
if len(obj) != 1 {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d values.%s format object must contain only format", index, field),
|
||||
})
|
||||
return
|
||||
}
|
||||
format, ok := rawFormat.(string)
|
||||
if !ok {
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d values.%s format must be a string", index, field),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := validateTopDataRowFormat(format); err != nil {
|
||||
message := strings.TrimPrefix(err.Error(), "default ")
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityError,
|
||||
Path: path,
|
||||
Message: fmt.Sprintf("global default %d values.%s %s", index, field, message),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func validateGlobalConditionList(path, label string, raw any, report *ValidationReport) {
|
||||
if raw == nil {
|
||||
return
|
||||
|
||||
@@ -180,6 +180,69 @@ func TestValidateProjectRejectsInvalidModuleColumnExpansionFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProjectRejectsInvalidGlobalJSONDefaults(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "base.json"), `{
|
||||
"output": "spells.2da",
|
||||
"columns": ["Label", "ImpactScript"],
|
||||
"rows": []
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "lock.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
||||
"defaults": [
|
||||
{"match": {"source": "entries"}, "values": {"ImpactScript": {"format": "ss_{missing}"}}},
|
||||
{"match": {"source": "unknown"}, "values": {"ImpactScript": "x"}},
|
||||
{"match": {"source": "entries", "extra": "bad"}, "values": {"ImpactScript": "x"}},
|
||||
{"match": {"source": "entries"}, "values": {"NotAColumn": "x"}},
|
||||
{"match": {"source": "entries"}}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
report := ValidateProject(testProject(root))
|
||||
if !report.HasErrors() {
|
||||
t.Fatal("expected invalid global defaults to fail validation")
|
||||
}
|
||||
text := diagnosticsText(report.Diagnostics)
|
||||
for _, want := range []string{
|
||||
"default 0 values.ImpactScript format token {missing} is not supported",
|
||||
"default 1 match.source is not supported",
|
||||
"default 2 match.extra is not supported",
|
||||
"default 3 values.NotAColumn is not a known column",
|
||||
"default 4 values is required",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected diagnostic %q, got:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProjectRejectsGlobalJSONDefaultsWithoutBaseDataset(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "global.json"), `{
|
||||
"defaults": [
|
||||
{"match": "all", "values": {"ClassSkill": 1}}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "fighter.json"), `{
|
||||
"columns": ["SkillLabel", "ClassSkill"],
|
||||
"rows": [
|
||||
{"id": 0, "SkillLabel": "Concentration", "ClassSkill": 1}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
report := ValidateProject(testProject(root))
|
||||
if !report.HasErrors() {
|
||||
t.Fatal("expected global defaults without a base dataset to fail validation")
|
||||
}
|
||||
if !diagnosticsContain(report.Diagnostics, "global defaults require a sibling base.json dataset") {
|
||||
t.Fatalf("expected unsupported defaults diagnostic, got:\n%s", diagnosticsText(report.Diagnostics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeEncodesConfiguredPackedHexLists(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "modules"))
|
||||
@@ -678,6 +741,54 @@ func TestBuildNativeAppliesConfiguredValueDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeGlobalJSONDefaultsReplaceConfiguredValueDefaults(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "base.json"), `{
|
||||
"output": "racialtypes.2da",
|
||||
"columns": ["Label", "ECL", "DefaultScale"],
|
||||
"rows": [
|
||||
{"id": 6, "key": "racialtypes:human", "Label": "Human"},
|
||||
{"id": 7, "key": "racialtypes:elf", "Label": "Elf", "ECL": null, "DefaultScale": null},
|
||||
{"id": 8, "key": "racialtypes:drow", "Label": "Drow", "ECL": 2, "DefaultScale": "0.95"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "lock.json"), `{"racialtypes:human":6,"racialtypes:elf":7,"racialtypes:drow":8}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "global.json"), `{
|
||||
"defaults": [
|
||||
{
|
||||
"match": "all",
|
||||
"values": {
|
||||
"ECL": 0,
|
||||
"DefaultScale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(result.Output2DADir, "racialtypes.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read racialtypes.2da: %v", err)
|
||||
}
|
||||
got := string(raw)
|
||||
for _, want := range []string{
|
||||
"6\tHuman\t0\t1\n",
|
||||
"7\tElf\t0\t1\n",
|
||||
"8\tDrow\t2\t0.95\n",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected racialtypes.2da to contain %q, got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProjectRejectsInvalidConfiguredPackedHexList(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "modules"))
|
||||
@@ -11754,6 +11865,58 @@ func TestBuildSupportsCanonicalSpells(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCanonicalSpellsImpactScriptsComeFromGlobalDefaults(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells", "modules"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "base.json"), `{
|
||||
"output": "spells.2da",
|
||||
"columns": ["Label", "ImpactScript"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "spells:acid_fog", "Label": "AcidFog", "ImpactScript": "NW_S0_AcidFog"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "lock.json"), `{
|
||||
"spells:acid_fog": 0,
|
||||
"spells:special_attacks": 840
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "specialattacks.json"), `{
|
||||
"entries": {
|
||||
"spells:special_attacks": {
|
||||
"Label": "SpecialAttacks"
|
||||
}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
||||
"defaults": [
|
||||
{
|
||||
"match": {"source": "entries"},
|
||||
"values": {
|
||||
"ImpactScript": {"format": "ss_{id}"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative failed: %v", err)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(result.Output2DADir, "spells.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read spells.2da: %v", err)
|
||||
}
|
||||
text := string(raw)
|
||||
if !strings.Contains(text, "0\tAcidFog\tNW_S0_AcidFog\n") {
|
||||
t.Fatalf("expected base ImpactScript to be preserved, got:\n%s", text)
|
||||
}
|
||||
if !strings.Contains(text, "840\tSpecialAttacks\tss_840\n") {
|
||||
t.Fatalf("expected global default generated ImpactScript, got:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCanonicalSpellsMatchesReference(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
||||
@@ -14019,6 +14182,232 @@ func TestBuildNativeAppliesGlobalJSONAllOverrideToBaseDataset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeAppliesGlobalJSONDefaultsToEntryRows(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells", "modules"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "base.json"), `{
|
||||
"output": "spells.2da",
|
||||
"columns": ["Label", "ImpactScript"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "spells:base_missing", "Label": "BaseMissing"},
|
||||
{"id": 1, "key": "spells:base_null", "Label": "BaseNull", "ImpactScript": null}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "lock.json"), `{
|
||||
"spells:base_missing": 0,
|
||||
"spells:base_null": 1,
|
||||
"spells:new_missing": 10,
|
||||
"spells:new_null": 11,
|
||||
"spells:new_stars": 12,
|
||||
"spells:new_empty": 13,
|
||||
"spells:new_explicit": 14
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "new.json"), `{
|
||||
"entries": {
|
||||
"spells:new_missing": {"Label": "NewMissing"},
|
||||
"spells:new_null": {"Label": "NewNull", "ImpactScript": null},
|
||||
"spells:new_stars": {"Label": "NewStars", "ImpactScript": "****"},
|
||||
"spells:new_empty": {"Label": "NewEmpty", "ImpactScript": ""},
|
||||
"spells:new_explicit": {"Label": "NewExplicit", "ImpactScript": "custom_spell_script"}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
||||
"defaults": [
|
||||
{
|
||||
"match": {"source": "entries"},
|
||||
"values": {
|
||||
"ImpactScript": {"format": "ss_{id}"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative failed: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "spells.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read spells.2da: %v", err)
|
||||
}
|
||||
text := string(got)
|
||||
for _, want := range []string{
|
||||
"0\tBaseMissing\t****\n",
|
||||
"1\tBaseNull\t****\n",
|
||||
"10\tNewMissing\tss_10\n",
|
||||
"11\tNewNull\tss_11\n",
|
||||
"12\tNewStars\tss_12\n",
|
||||
"13\tNewEmpty\tss_13\n",
|
||||
"14\tNewExplicit\tcustom_spell_script\n",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected spells.2da to contain %q, got:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeGlobalJSONDefaultsUseDistinctProvenanceForDuplicateIDs(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells", "modules"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "base.json"), `{
|
||||
"output": "spells.2da",
|
||||
"columns": ["Label", "ImpactScript"],
|
||||
"rows": [
|
||||
{"id": 10, "key": "spells:base", "Label": "BaseDuplicate"},
|
||||
{"id": 10, "key": "spells:entry", "Label": "EntryDuplicate"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "entry.json"), `{
|
||||
"entries": {
|
||||
"spells:entry": {"Label": "EntryDuplicate"}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
||||
"defaults": [
|
||||
{
|
||||
"match": {"source": "entries"},
|
||||
"values": {
|
||||
"ImpactScript": {"format": "ss_{key}"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative failed: %v", err)
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(filepath.Join(result.Output2DADir, "spells.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read spells.2da: %v", err)
|
||||
}
|
||||
got := string(raw)
|
||||
for _, want := range []string{
|
||||
"10\tBaseDuplicate\t****\n",
|
||||
"10\tEntryDuplicate\tss_spells:entry\n",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected spells.2da to contain %q, got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeGlobalJSONDefaultsPreserveProvenanceForDisplacedRows(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells", "modules"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "base.json"), `{
|
||||
"output": "spells.2da",
|
||||
"columns": ["Label", "ImpactScript"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "spells:base", "Label": "BaseMoved"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "lock.json"), `{
|
||||
"spells:base": 0,
|
||||
"spells:entry": 10
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "entry.json"), `{
|
||||
"entries": {
|
||||
"spells:entry": {"Label": "EntryDisplaced"}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
||||
"overrides": [
|
||||
{"id": 0, "key": "spells:entry"}
|
||||
],
|
||||
"defaults": [
|
||||
{
|
||||
"match": {"source": "entries"},
|
||||
"values": {
|
||||
"ImpactScript": "entry_default"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative failed: %v", err)
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(filepath.Join(result.Output2DADir, "spells.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read spells.2da: %v", err)
|
||||
}
|
||||
got := string(raw)
|
||||
for _, want := range []string{
|
||||
"0\tBaseMoved\t****\n",
|
||||
"10\tEntryDisplaced\tentry_default\n",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected spells.2da to contain %q, got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeGlobalJSONDefaultsSupportLiteralAllRows(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "base.json"), `{
|
||||
"output": "racialtypes.2da",
|
||||
"columns": ["Label", "ECL", "DefaultScale"],
|
||||
"rows": [
|
||||
{"id": 6, "key": "racialtypes:human", "Label": "Human"},
|
||||
{"id": 7, "key": "racialtypes:elf", "Label": "Elf", "ECL": null, "DefaultScale": null},
|
||||
{"id": 8, "key": "racialtypes:drow", "Label": "Drow", "ECL": 2, "DefaultScale": "0.95"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "lock.json"), `{
|
||||
"racialtypes:human": 6,
|
||||
"racialtypes:elf": 7,
|
||||
"racialtypes:drow": 8
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "global.json"), `{
|
||||
"defaults": [
|
||||
{
|
||||
"match": "all",
|
||||
"values": {
|
||||
"ECL": 0,
|
||||
"DefaultScale": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ReferenceBuilder = ""
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative failed: %v", err)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(result.Output2DADir, "racialtypes.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read racialtypes.2da: %v", err)
|
||||
}
|
||||
got := string(raw)
|
||||
for _, want := range []string{
|
||||
"6\tHuman\t0\t1\n",
|
||||
"7\tElf\t0\t1\n",
|
||||
"8\tDrow\t2\t0.95\n",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected racialtypes.2da to contain %q, got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeAppliesRecursiveGlobalJSONPlainDatasetInjections(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills", "baseclasses"))
|
||||
@@ -14089,6 +14478,28 @@ func TestBuildNativeAppliesRecursiveGlobalJSONPlainDatasetInjections(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeRejectsGlobalDefaultsOnPlainDataset(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "global.json"), `{
|
||||
"defaults": [
|
||||
{"match": "all", "values": {"ClassSkill": 1}}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "fighter.json"), `{
|
||||
"columns": ["SkillLabel", "ClassSkill"],
|
||||
"rows": [
|
||||
{"id": 0, "SkillLabel": "Concentration", "ClassSkill": 1}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
_, err := BuildNativeWithOptions(testProject(root), NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "defaults") || !strings.Contains(err.Error(), "base.json dataset") {
|
||||
t.Fatalf("expected plain dataset defaults build error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeExtendsConfiguredPlainRowsByRepeatingLastLevel(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "spellsgained"))
|
||||
|
||||
Reference in New Issue
Block a user