Sidecar generation
This commit is contained in:
@@ -151,7 +151,8 @@ printing the secret.
|
||||
Topdata value encodings under `topdata.value_encodings` are matched by dataset
|
||||
and column. Set `dataset: "*"` to apply an encoding to the same column name in
|
||||
any native topdata dataset; an exact dataset rule takes precedence over the
|
||||
wildcard rule for that column.
|
||||
wildcard rule for that column. Supported compact encodings include packed hex
|
||||
lists, external hex-list sidecar tables, alignment set masks, and ID hex lists.
|
||||
|
||||
Common configurable defaults:
|
||||
|
||||
|
||||
@@ -797,7 +797,7 @@ func validateTopDataValueEncodings(encodings []TopDataValueEncodingConfig) []err
|
||||
failures = append(failures, fmt.Errorf("%s.column is required", prefix))
|
||||
}
|
||||
switch strings.TrimSpace(encoding.Mode) {
|
||||
case "packed_hex_list", "alignment_hex_list", "id_hex_list":
|
||||
case "packed_hex_list", "external_hex_list", "alignment_hex_list", "alignment_set_mask", "id_hex_list":
|
||||
default:
|
||||
failures = append(failures, fmt.Errorf("%s.mode %q is not supported", prefix, encoding.Mode))
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ func buildGenerated2DAAssetGroup(p *project.Project, cfg project.GeneratedTopDat
|
||||
|
||||
results := make([]Generated2DAAsset, 0, len(collected))
|
||||
for _, dataset := range collected {
|
||||
compiled, err := resolveNativeDataset(dataset, keyToID, rowByKey, tableRegistry, nil, project.TopDataClassFeatInjectionConfig{})
|
||||
compiled, err := resolveNativeDataset(dataset, keyToID, rowByKey, tableRegistry, nil, project.TopDataClassFeatInjectionConfig{}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+237
-13
@@ -71,6 +71,144 @@ type resolvedTableRegistry struct {
|
||||
tableByKey map[string]resolvedTable
|
||||
}
|
||||
|
||||
type nativeSidecarCollector struct {
|
||||
tables map[string][]map[string]any
|
||||
}
|
||||
|
||||
func newNativeSidecarCollector() *nativeSidecarCollector {
|
||||
return &nativeSidecarCollector{tables: map[string][]map[string]any{}}
|
||||
}
|
||||
|
||||
func (c *nativeSidecarCollector) addExternalHexList(dataset nativeDataset, row map[string]any, field string, values []int, encoding project.TopDataValueEncodingConfig) (string, error) {
|
||||
if c == nil {
|
||||
return "", fmt.Errorf("external hex list sidecar collector is not available")
|
||||
}
|
||||
ref := sidecarRowReference(dataset.Name, row)
|
||||
if ref == "" {
|
||||
return "", fmt.Errorf("field %s: external hex list requires row id or key", field)
|
||||
}
|
||||
outputName := sidecarOutputName(dataset.OutputName, field)
|
||||
rows := c.tables[outputName]
|
||||
for _, value := range values {
|
||||
parsed, err := parseConfiguredListValue(value, encoding)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rows = append(rows, map[string]any{
|
||||
"id": len(rows),
|
||||
"Label": ref,
|
||||
"Value": "0x" + fmt.Sprintf("%0*X", encoding.HexWidth, parsed),
|
||||
})
|
||||
}
|
||||
c.tables[outputName] = rows
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func (c *nativeSidecarCollector) writeAll(outputDir string) error {
|
||||
if c == nil || len(c.tables) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, outputName := range sortedStringMapKeys(c.tables) {
|
||||
data := map[string]any{
|
||||
"columns": []string{"Label", "Value"},
|
||||
"rows": c.tables[outputName],
|
||||
}
|
||||
if err := write2DA(data, filepath.Join(outputDir, outputName), false, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sidecarOutputName(parentOutputName, field string) string {
|
||||
parent := normalizeSidecarToken(outputStem(parentOutputName))
|
||||
acronym := sidecarTokenAcronym(field)
|
||||
if acronym != "" {
|
||||
candidate := parent + "_" + acronym
|
||||
if len(candidate) <= 16 {
|
||||
return candidate + ".2da"
|
||||
}
|
||||
}
|
||||
stem := normalizeSidecarToken(parent + "_" + field)
|
||||
if len(stem) <= 16 {
|
||||
return stem + ".2da"
|
||||
}
|
||||
hash := stableShortHash(stem)
|
||||
prefixLen := 16 - len(hash) - 1
|
||||
if prefixLen < 1 {
|
||||
prefixLen = 1
|
||||
}
|
||||
return stem[:prefixLen] + "_" + hash + ".2da"
|
||||
}
|
||||
|
||||
func sidecarTokenAcronym(value string) string {
|
||||
var builder strings.Builder
|
||||
previousWasLowerOrDigit := false
|
||||
for _, r := range strings.TrimSpace(value) {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
if builder.Len() == 0 || previousWasLowerOrDigit {
|
||||
builder.WriteByte(byte(r + ('a' - 'A')))
|
||||
}
|
||||
previousWasLowerOrDigit = false
|
||||
continue
|
||||
}
|
||||
if r >= 'a' && r <= 'z' {
|
||||
if builder.Len() == 0 {
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
previousWasLowerOrDigit = true
|
||||
continue
|
||||
}
|
||||
if r >= '0' && r <= '9' {
|
||||
builder.WriteRune(r)
|
||||
previousWasLowerOrDigit = true
|
||||
continue
|
||||
}
|
||||
previousWasLowerOrDigit = false
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func sidecarRowReference(datasetName string, row map[string]any) string {
|
||||
if key, ok := row["key"].(string); ok && key != "" {
|
||||
if index := strings.LastIndex(key, ":"); index >= 0 && index < len(key)-1 {
|
||||
return normalizeSidecarToken(key[index+1:])
|
||||
}
|
||||
return normalizeSidecarToken(key)
|
||||
}
|
||||
if id, ok := row["id"].(int); ok {
|
||||
return fmt.Sprintf("%s_%d", normalizeSidecarToken(datasetName), id)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeSidecarToken(value string) string {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(value))
|
||||
var builder strings.Builder
|
||||
previousUnderscore := false
|
||||
for _, r := range trimmed {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||
builder.WriteRune(r)
|
||||
previousUnderscore = false
|
||||
continue
|
||||
}
|
||||
if !previousUnderscore {
|
||||
builder.WriteByte('_')
|
||||
previousUnderscore = true
|
||||
}
|
||||
}
|
||||
return strings.Trim(builder.String(), "_")
|
||||
}
|
||||
|
||||
func stableShortHash(value string) string {
|
||||
var hash uint32 = 2166136261
|
||||
for _, b := range []byte(value) {
|
||||
hash ^= uint32(b)
|
||||
hash *= 16777619
|
||||
}
|
||||
return fmt.Sprintf("%04x", hash&0xffff)
|
||||
}
|
||||
|
||||
func newResolvedTableRegistry(collected []nativeCollectedDataset) (resolvedTableRegistry, error) {
|
||||
registry := resolvedTableRegistry{
|
||||
stemByKey: map[string]string{},
|
||||
@@ -356,6 +494,7 @@ func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress
|
||||
|
||||
groupStats := nativeCompileGroupStats(collected)
|
||||
currentGroup := ""
|
||||
sidecars := newNativeSidecarCollector()
|
||||
for _, dataset := range collected {
|
||||
group := nativeCompileGroup(dataset.Dataset.Name)
|
||||
if group != currentGroup {
|
||||
@@ -368,7 +507,7 @@ func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress
|
||||
stats.SourceFragments,
|
||||
))
|
||||
}
|
||||
compiled, err := resolveNativeDataset(dataset, globalKeyToID, globalRowByKey, tableRegistry, compiler, p.EffectiveConfig().TopData.ClassFeatInjections)
|
||||
compiled, err := resolveNativeDataset(dataset, globalKeyToID, globalRowByKey, tableRegistry, compiler, p.EffectiveConfig().TopData.ClassFeatInjections, sidecars)
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
@@ -376,6 +515,9 @@ func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress
|
||||
return BuildResult{}, err
|
||||
}
|
||||
}
|
||||
if err := sidecars.writeAll(output2DA); err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
|
||||
filesTLK, err := compiler.finish(outputTLK, filepath.Base(p.TopDataCompiledTLKPath()))
|
||||
if err != nil {
|
||||
@@ -3181,7 +3323,7 @@ func collectPlainDataset(dataset nativeDataset) (nativeCollectedDataset, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveNativeDataset(dataset nativeCollectedDataset, keyToID map[string]int, globalRowByKey map[string]map[string]any, tableRegistry resolvedTableRegistry, compiler *tlkCompiler, classFeatInjections project.TopDataClassFeatInjectionConfig) (map[string]any, error) {
|
||||
func resolveNativeDataset(dataset nativeCollectedDataset, keyToID map[string]int, globalRowByKey map[string]map[string]any, tableRegistry resolvedTableRegistry, compiler *tlkCompiler, classFeatInjections project.TopDataClassFeatInjectionConfig, sidecars *nativeSidecarCollector) (map[string]any, error) {
|
||||
rows := dataset.Rows
|
||||
if strings.HasPrefix(filepath.ToSlash(dataset.Dataset.Name), "classes/feats/") {
|
||||
classKey := "classes:" + dataset.Dataset.Name[strings.LastIndex(dataset.Dataset.Name, "/")+1:]
|
||||
@@ -3201,6 +3343,7 @@ func resolveNativeDataset(dataset nativeCollectedDataset, keyToID map[string]int
|
||||
rowByKey: globalRowByKey,
|
||||
tableRegistry: tableRegistry,
|
||||
compiler: compiler,
|
||||
sidecars: sidecars,
|
||||
cache: map[string]map[string]tlkCompiledValue{},
|
||||
}
|
||||
|
||||
@@ -3818,6 +3961,7 @@ type valueResolver struct {
|
||||
rowByKey map[string]map[string]any
|
||||
tableRegistry resolvedTableRegistry
|
||||
compiler *tlkCompiler
|
||||
sidecars *nativeSidecarCollector
|
||||
cache map[string]map[string]tlkCompiledValue
|
||||
}
|
||||
|
||||
@@ -3901,12 +4045,21 @@ func (r *valueResolver) resolveValue(row map[string]any, field string, value any
|
||||
} else if ok {
|
||||
return r.resolveTLKPayload(row, field, payload, stack)
|
||||
}
|
||||
if encoding, ok := r.valueEncodingForField(field); ok && strings.TrimSpace(encoding.Mode) == "alignment_hex_list" {
|
||||
if encoding, ok := r.valueEncodingForField(field); ok {
|
||||
switch strings.TrimSpace(encoding.Mode) {
|
||||
case "alignment_hex_list":
|
||||
encoded, err := encodeConfiguredAlignmentHexList(value, encoding)
|
||||
if err != nil {
|
||||
return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err)
|
||||
}
|
||||
return tlkCompiledValue{Value: encoded}, nil
|
||||
case "alignment_set_mask":
|
||||
encoded, err := encodeConfiguredAlignmentSetMask(value, encoding)
|
||||
if err != nil {
|
||||
return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err)
|
||||
}
|
||||
return tlkCompiledValue{Value: encoded}, nil
|
||||
}
|
||||
}
|
||||
|
||||
switch typed := value.(type) {
|
||||
@@ -3916,8 +4069,8 @@ func (r *valueResolver) resolveValue(row map[string]any, field string, value any
|
||||
_, hasList := typed["list"]
|
||||
_, hasAllExcept := typed["all_except"]
|
||||
_, hasIDs := typed["ids"]
|
||||
if mode == "packed_hex_list" && (hasList || hasAllExcept) {
|
||||
value, err := encodeConfiguredValueList(typed, encoding)
|
||||
if (mode == "packed_hex_list" || mode == "external_hex_list") && (hasList || hasAllExcept) {
|
||||
value, err := r.encodeConfiguredValueList(row, field, typed, encoding)
|
||||
if err != nil {
|
||||
return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err)
|
||||
}
|
||||
@@ -4003,40 +4156,63 @@ func (r *valueResolver) valueDefaultForField(field string) (any, bool) {
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (r *valueResolver) encodeConfiguredValueList(row map[string]any, field string, obj map[string]any, encoding project.TopDataValueEncodingConfig) (string, error) {
|
||||
mode := strings.TrimSpace(encoding.Mode)
|
||||
if mode != "packed_hex_list" && mode != "external_hex_list" {
|
||||
return "", fmt.Errorf("unsupported value encoding mode %q", encoding.Mode)
|
||||
}
|
||||
values, err := expandConfiguredValueListValues(obj, encoding)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if mode == "external_hex_list" {
|
||||
return r.sidecars.addExternalHexList(r.dataset, row, field, values, encoding)
|
||||
}
|
||||
return formatConfiguredPackedHexList(values, encoding), nil
|
||||
}
|
||||
|
||||
func encodeConfiguredValueList(obj map[string]any, encoding project.TopDataValueEncodingConfig) (string, error) {
|
||||
if strings.TrimSpace(encoding.Mode) != "packed_hex_list" {
|
||||
return "", fmt.Errorf("unsupported value encoding mode %q", encoding.Mode)
|
||||
}
|
||||
values, err := expandConfiguredValueListValues(obj, encoding)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return formatConfiguredPackedHexList(values, encoding), nil
|
||||
}
|
||||
|
||||
func expandConfiguredValueListValues(obj map[string]any, encoding project.TopDataValueEncodingConfig) ([]int, error) {
|
||||
_, hasList := obj["list"]
|
||||
_, hasAllExcept := obj["all_except"]
|
||||
if hasList && hasAllExcept {
|
||||
return "", fmt.Errorf("list encoding object cannot contain both list and all_except")
|
||||
return nil, fmt.Errorf("list encoding object cannot contain both list and all_except")
|
||||
}
|
||||
for key := range obj {
|
||||
if key != "list" && key != "all_except" {
|
||||
return "", fmt.Errorf("list encoding object contains unsupported key %q", key)
|
||||
return nil, fmt.Errorf("list encoding object contains unsupported key %q", key)
|
||||
}
|
||||
}
|
||||
if hasAllExcept {
|
||||
values, err := expandConfiguredValueAllExcept(obj["all_except"], encoding)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
return formatConfiguredPackedHexList(values, encoding), nil
|
||||
return values, nil
|
||||
}
|
||||
rawList, ok := obj["list"].([]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("list must be an array")
|
||||
return nil, fmt.Errorf("list must be an array")
|
||||
}
|
||||
values := make([]int, 0, len(rawList))
|
||||
for _, raw := range rawList {
|
||||
expanded, err := expandConfiguredValueListItem(raw, encoding)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, expanded...)
|
||||
}
|
||||
return formatConfiguredPackedHexList(values, encoding), nil
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func encodeConfiguredAlignmentHexList(raw any, encoding project.TopDataValueEncodingConfig) (string, error) {
|
||||
@@ -4047,8 +4223,31 @@ func encodeConfiguredAlignmentHexList(raw any, encoding project.TopDataValueEnco
|
||||
return formatConfiguredPackedHexList(values, encoding), nil
|
||||
}
|
||||
|
||||
func encodeConfiguredAlignmentSetMask(raw any, encoding project.TopDataValueEncodingConfig) (string, error) {
|
||||
values, err := expandConfiguredAlignmentList(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mask := 0
|
||||
for _, value := range values {
|
||||
if value == 0 {
|
||||
continue
|
||||
}
|
||||
bit, ok := configuredAlignmentSetBit(value)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("alignment value 0x%02X is not a supported exact alignment", value)
|
||||
}
|
||||
mask |= bit
|
||||
}
|
||||
if _, err := parseConfiguredListValue(mask, encoding); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "0x" + fmt.Sprintf("%0*X", encoding.HexWidth, mask), nil
|
||||
}
|
||||
|
||||
func (r *valueResolver) encodeConfiguredIDHexList(obj map[string]any, encoding project.TopDataValueEncodingConfig) (string, error) {
|
||||
if strings.TrimSpace(encoding.Mode) != "id_hex_list" {
|
||||
mode := strings.TrimSpace(encoding.Mode)
|
||||
if mode != "id_hex_list" {
|
||||
return "", fmt.Errorf("unsupported value encoding mode %q", encoding.Mode)
|
||||
}
|
||||
for key := range obj {
|
||||
@@ -4232,6 +4431,31 @@ func configuredAlignmentAxisValue(letter byte, lawChaos bool) (int, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func configuredAlignmentSetBit(value int) (int, bool) {
|
||||
switch value {
|
||||
case 0x0A: // lawful good
|
||||
return 1 << 0, true
|
||||
case 0x03: // lawful neutral
|
||||
return 1 << 1, true
|
||||
case 0x12: // lawful evil
|
||||
return 1 << 2, true
|
||||
case 0x09: // neutral good
|
||||
return 1 << 3, true
|
||||
case 0x01: // true neutral
|
||||
return 1 << 4, true
|
||||
case 0x11: // neutral evil
|
||||
return 1 << 5, true
|
||||
case 0x0C: // chaotic good
|
||||
return 1 << 6, true
|
||||
case 0x05: // chaotic neutral
|
||||
return 1 << 7, true
|
||||
case 0x14: // chaotic evil
|
||||
return 1 << 8, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func formatConfiguredPackedHexList(values []int, encoding project.TopDataValueEncodingConfig) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
|
||||
@@ -290,6 +290,62 @@ func TestBuildNativeEncodesConfiguredAlignmentHexLists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeEncodesConfiguredAlignmentSetMasks(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "core", "modules"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "core", "base.json"), `{
|
||||
"output": "classes.2da",
|
||||
"columns": ["Label", "PreferredAlignments"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "classes:barbarian", "Label": "Barbarian"},
|
||||
{"id": 1, "key": "classes:paladin", "Label": "Paladin"},
|
||||
{"id": 2, "key": "classes:fighter", "Label": "Fighter"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "core", "lock.json"), `{"classes:barbarian":0,"classes:paladin":1,"classes:fighter":2}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "core", "modules", "10_alignments.json"), `{
|
||||
"overrides": [
|
||||
{
|
||||
"key": "classes:barbarian",
|
||||
"PreferredAlignments": {"alignments": ["ng", "cg", "tn", "cn", "ne", "ce"]}
|
||||
},
|
||||
{
|
||||
"key": "classes:paladin",
|
||||
"PreferredAlignments": {"alignments": ["lg"]}
|
||||
},
|
||||
{
|
||||
"key": "classes:fighter",
|
||||
"PreferredAlignments": "0x00"
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ValueEncodings = []project.TopDataValueEncodingConfig{
|
||||
{Dataset: "*", Column: "PreferredAlignments", Mode: "alignment_set_mask", Min: 0, Max: 511, HexWidth: 3},
|
||||
}
|
||||
|
||||
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, "classes.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read classes.2da: %v", err)
|
||||
}
|
||||
got := string(raw)
|
||||
for _, want := range []string{
|
||||
"0\tBarbarian\t0x1F8\n",
|
||||
"1\tPaladin\t0x001\n",
|
||||
"2\tFighter\t0x000\n",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected compiled classes.2da to contain %q, got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeEncodesConfiguredIDHexLists(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
|
||||
@@ -348,6 +404,109 @@ func TestBuildNativeEncodesConfiguredIDHexLists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeEncodesConfiguredExternalHexListsAndPackedIDs(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "modules"))
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "baseitems", "modules"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
|
||||
"output": "feat.2da",
|
||||
"columns": ["LABEL"],
|
||||
"rows": [
|
||||
{"id": 1, "key": "feat:first", "LABEL": "First"},
|
||||
{"id": 2, "key": "feat:second", "LABEL": "Second"},
|
||||
{"id": 3, "key": "feat:third", "LABEL": "Third"},
|
||||
{"id": 7, "key": "feat:seventh", "LABEL": "Seventh"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{
|
||||
"feat:first": 1,
|
||||
"feat:second": 2,
|
||||
"feat:third": 3,
|
||||
"feat:seventh": 7
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "base.json"), `{
|
||||
"output": "racialtypes.2da",
|
||||
"columns": ["Label", "AvailableSkinColors"],
|
||||
"rows": [
|
||||
{"id": 6, "key": "racialtypes:human", "Label": "Human"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "lock.json"), `{"racialtypes:human":6}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "modules", "10_human.json"), `{
|
||||
"overrides": [
|
||||
{
|
||||
"key": "racialtypes:human",
|
||||
"AvailableSkinColors": {"list": [1, 2, 3, 5, {"range": [7, 9]}]}
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "base.json"), `{
|
||||
"output": "baseitems.2da",
|
||||
"columns": ["Label", "ProficiencyFeats"],
|
||||
"rows": [
|
||||
{"id": 42, "key": "baseitems:testclub", "Label": "Test Club"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "lock.json"), `{"baseitems:testclub":42}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "modules", "10_testclub.json"), `{
|
||||
"overrides": [
|
||||
{
|
||||
"key": "baseitems:testclub",
|
||||
"ProficiencyFeats": {"ids": ["feat:first", "feat:second", "feat:third", "feat:seventh"]}
|
||||
}
|
||||
]
|
||||
}`+"\n")
|
||||
|
||||
proj := testProject(root)
|
||||
proj.Config.TopData.ValueEncodings = []project.TopDataValueEncodingConfig{
|
||||
{Dataset: "racialtypes/core", Column: "AvailableSkinColors", Mode: "external_hex_list", Min: 0, Max: 255, HexWidth: 2},
|
||||
{Dataset: "*", Column: "ProficiencyFeats", Mode: "id_hex_list", Min: 0, Max: 65535, HexWidth: 4},
|
||||
}
|
||||
|
||||
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
||||
}
|
||||
racialtypesRaw, err := os.ReadFile(filepath.Join(result.Output2DADir, "racialtypes.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read racialtypes.2da: %v", err)
|
||||
}
|
||||
if got := string(racialtypesRaw); !strings.Contains(got, "6\tHuman\thuman\n") {
|
||||
t.Fatalf("expected compiled racialtypes.2da to contain a sidecar reference, got:\n%s", got)
|
||||
}
|
||||
sidecarOutputName := sidecarOutputName("racialtypes.2da", "AvailableSkinColors")
|
||||
if stem := outputStem(sidecarOutputName); len(stem) > 16 {
|
||||
t.Fatalf("expected generated sidecar stem to fit NWN resource limit, got %q", stem)
|
||||
}
|
||||
sidecarRaw, err := os.ReadFile(filepath.Join(result.Output2DADir, sidecarOutputName))
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", sidecarOutputName, err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"Label\tValue\n",
|
||||
"0\thuman\t0x01\n",
|
||||
"1\thuman\t0x02\n",
|
||||
"2\thuman\t0x03\n",
|
||||
"3\thuman\t0x05\n",
|
||||
"4\thuman\t0x07\n",
|
||||
"5\thuman\t0x08\n",
|
||||
"6\thuman\t0x09\n",
|
||||
} {
|
||||
if got := string(sidecarRaw); !strings.Contains(got, want) {
|
||||
t.Fatalf("expected sidecar 2da to contain %q, got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
baseitemsRaw, err := os.ReadFile(filepath.Join(result.Output2DADir, "baseitems.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read baseitems.2da: %v", err)
|
||||
}
|
||||
if got := string(baseitemsRaw); !strings.Contains(got, "0x0001000200030007") {
|
||||
t.Fatalf("expected compiled baseitems.2da to contain packed IDs, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNativeAppliesWildcardConfiguredValueEncodings(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
|
||||
@@ -1118,6 +1277,7 @@ func TestResolveNativeDatasetPreservesScalarTableReferenceBehavior(t *testing.T)
|
||||
tableRegistry,
|
||||
nil,
|
||||
project.TopDataClassFeatInjectionConfig{},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveNativeDataset failed: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user