Global json adjustments
This commit is contained in:
+333
-17
@@ -58,6 +58,19 @@ type nativeCollectedDataset struct {
|
|||||||
LockModified bool
|
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 {
|
type resolvedTable struct {
|
||||||
Key string
|
Key string
|
||||||
DatasetName string
|
DatasetName string
|
||||||
@@ -1198,6 +1211,87 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
lockAdded := 0
|
lockAdded := 0
|
||||||
lockPruned := 0
|
lockPruned := 0
|
||||||
retiredKeys := map[string]struct{}{}
|
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{}
|
featFamilyAliases := generatedFamilyAliases{}
|
||||||
if dataset.Name == "feat" {
|
if dataset.Name == "feat" {
|
||||||
var err error
|
var err error
|
||||||
@@ -1257,6 +1351,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
|
setRowSource(row, nativeRowSourceBase)
|
||||||
if key, ok := row["key"].(string); ok && key != "" {
|
if key, ok := row["key"].(string); ok && key != "" {
|
||||||
if baseRowKeyCounts[key] > 1 {
|
if baseRowKeyCounts[key] > 1 {
|
||||||
if lockedID, ok := lockData[key]; !ok || lockedID != rowID {
|
if lockedID, ok := lockData[key]; !ok || lockedID != rowID {
|
||||||
@@ -1342,6 +1437,8 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
applyOverrideFields := func(override map[string]any, row map[string]any) (bool, string, error) {
|
applyOverrideFields := func(override map[string]any, row map[string]any) (bool, string, error) {
|
||||||
|
sourceBefore := rowSource(row)
|
||||||
|
identityBefore, identityBeforeOK := rowIdentity(row)
|
||||||
candidate := map[string]any{}
|
candidate := map[string]any{}
|
||||||
for field, value := range row {
|
for field, value := range row {
|
||||||
candidate[field] = value
|
candidate[field] = value
|
||||||
@@ -1383,6 +1480,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", err
|
return false, "", err
|
||||||
}
|
}
|
||||||
|
displacedSource := captureDisplacedRowSource(row, expanded)
|
||||||
keyChanged, retiredKey, err := updateOverrideRowKey(dataset.Name, row, expanded, rowByKey, lockData)
|
keyChanged, retiredKey, err := updateOverrideRowKey(dataset.Name, row, expanded, rowByKey, lockData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", err
|
return false, "", err
|
||||||
@@ -1393,6 +1491,8 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
}
|
}
|
||||||
row[field] = value
|
row[field] = value
|
||||||
}
|
}
|
||||||
|
moveRowSource(identityBefore, identityBeforeOK, row, sourceBefore)
|
||||||
|
moveCapturedRowSource(displacedSource)
|
||||||
return keyChanged, retiredKey, nil
|
return keyChanged, retiredKey, nil
|
||||||
}
|
}
|
||||||
applyModuleData := func(sourceLabel string, moduleData map[string]any, globalSource bool) error {
|
applyModuleData := func(sourceLabel string, moduleData map[string]any, globalSource bool) error {
|
||||||
@@ -1442,6 +1542,8 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
identityBefore, identityBeforeOK := rowIdentity(existing)
|
||||||
|
displacedSource := captureDisplacedRowSource(existing, expanded)
|
||||||
keyChanged, retiredKey, err := updateOverrideRowKey(dataset.Name, existing, expanded, rowByKey, lockData)
|
keyChanged, retiredKey, err := updateOverrideRowKey(dataset.Name, existing, expanded, rowByKey, lockData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1458,6 +1560,8 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
}
|
}
|
||||||
existing[field] = value
|
existing[field] = value
|
||||||
}
|
}
|
||||||
|
moveRowSource(identityBefore, identityBeforeOK, existing, nativeRowSourceEntries)
|
||||||
|
moveCapturedRowSource(displacedSource)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rowID, ok := lockedIDForKey(key)
|
rowID, ok := lockedIDForKey(key)
|
||||||
@@ -1472,6 +1576,7 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
return fmt.Errorf("dataset %s: %w", dataset.Name, err)
|
return fmt.Errorf("dataset %s: %w", dataset.Name, err)
|
||||||
}
|
}
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
|
setRowSource(row, nativeRowSourceEntries)
|
||||||
rowByID[rowID] = row
|
rowByID[rowID] = row
|
||||||
rowByKey[key] = row
|
rowByKey[key] = row
|
||||||
}
|
}
|
||||||
@@ -1556,14 +1661,18 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
row["key"] = rawKey
|
row["key"] = rawKey
|
||||||
}
|
}
|
||||||
rows = append(rows, row)
|
rows = append(rows, row)
|
||||||
|
setRowSource(row, nativeRowSourceOverride)
|
||||||
rowByID[rowID] = row
|
rowByID[rowID] = row
|
||||||
if hasKey && rawKey != "" {
|
if hasKey && rawKey != "" {
|
||||||
rowByKey[rawKey] = row
|
rowByKey[rawKey] = row
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if overrideRequestsNullRow(override) {
|
if overrideRequestsNullRow(override) {
|
||||||
|
sourceBefore := rowSource(row)
|
||||||
|
identityBefore, identityBeforeOK := rowIdentity(row)
|
||||||
retireRowIdentity(row, lockData, retiredKeys)
|
retireRowIdentity(row, lockData, retiredKeys)
|
||||||
nullifyOverrideRow(row, columns, rowByKey)
|
nullifyOverrideRow(row, columns, rowByKey)
|
||||||
|
moveRowSource(identityBefore, identityBeforeOK, row, sourceBefore)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keyChanged, retiredKey, err := applyOverrideFields(override, row)
|
keyChanged, retiredKey, err := applyOverrideFields(override, row)
|
||||||
@@ -1580,6 +1689,66 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
}
|
}
|
||||||
return nil
|
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)
|
generatedModules, err := collectGeneratedModules(dataset, lockData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1621,9 +1790,9 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
|
|||||||
if err := applyModuleData(module.Name, module.Data, true); err != nil {
|
if err := applyModuleData(module.Name, module.Data, true); err != nil {
|
||||||
return nativeCollectedDataset{}, err
|
return nativeCollectedDataset{}, err
|
||||||
}
|
}
|
||||||
}
|
if err := applyGlobalDefaults(module.Name, module.Data); err != nil {
|
||||||
if dataset.Name == "spells" {
|
return nativeCollectedDataset{}, err
|
||||||
normalizeCollectedSpellImpactScripts(rows, baseRowKeys)
|
}
|
||||||
}
|
}
|
||||||
referencedKeys, err := collectReferencedLockKeys(nativeDatasetSourceDir(dataset))
|
referencedKeys, err := collectReferencedLockKeys(nativeDatasetSourceDir(dataset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3513,6 +3682,11 @@ func collectPlainDataset(dataset nativeDataset) (nativeCollectedDataset, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nativeCollectedDataset{}, err
|
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)
|
columns, err := parseColumns(tableData, dataset.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6013,21 +6187,163 @@ func updateOverrideRowKey(datasetName string, row map[string]any, expanded map[s
|
|||||||
return changed, retiredKey, nil
|
return changed, retiredKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeCollectedSpellImpactScripts(rows []map[string]any, baseRowKeys map[string]struct{}) {
|
func isTopDataNullLike(value any) bool {
|
||||||
for _, row := range rows {
|
if value == nil {
|
||||||
rowKey, ok := row["key"].(string)
|
return true
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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 {
|
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 {
|
if _, ok := obj["overrides"]; ok {
|
||||||
validateOverridesFile(path, obj, report)
|
validateOverridesFile(path, obj, report)
|
||||||
}
|
}
|
||||||
|
if _, ok := obj["defaults"]; ok {
|
||||||
|
validateGlobalDefaults(path, obj, report)
|
||||||
|
}
|
||||||
if rawPosition, ok := obj["position"]; ok {
|
if rawPosition, ok := obj["position"]; ok {
|
||||||
position, ok := rawPosition.(string)
|
position, ok := rawPosition.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -1032,6 +1035,191 @@ func validateGlobalJSONFile(path string, obj map[string]any, report *ValidationR
|
|||||||
validateRowCollection(path, obj, rows, report)
|
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) {
|
func validateGlobalConditionList(path, label string, raw any, report *ValidationReport) {
|
||||||
if raw == nil {
|
if raw == nil {
|
||||||
return
|
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) {
|
func TestBuildNativeEncodesConfiguredPackedHexLists(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "modules"))
|
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) {
|
func TestValidateProjectRejectsInvalidConfiguredPackedHexList(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "modules"))
|
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) {
|
func TestBuildCanonicalSpellsMatchesReference(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
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) {
|
func TestBuildNativeAppliesRecursiveGlobalJSONPlainDatasetInjections(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills", "baseclasses"))
|
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) {
|
func TestBuildNativeExtendsConfiguredPlainRowsByRepeatingLastLevel(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "spellsgained"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "spellsgained"))
|
||||||
|
|||||||
Reference in New Issue
Block a user