Sidecar generation

This commit is contained in:
2026-05-26 16:11:45 +02:00
parent 28d487565a
commit c5a18df730
5 changed files with 405 additions and 20 deletions
+2 -1
View File
@@ -151,7 +151,8 @@ printing the secret.
Topdata value encodings under `topdata.value_encodings` are matched by dataset 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 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 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: Common configurable defaults:
+1 -1
View File
@@ -797,7 +797,7 @@ func validateTopDataValueEncodings(encodings []TopDataValueEncodingConfig) []err
failures = append(failures, fmt.Errorf("%s.column is required", prefix)) failures = append(failures, fmt.Errorf("%s.column is required", prefix))
} }
switch strings.TrimSpace(encoding.Mode) { 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: default:
failures = append(failures, fmt.Errorf("%s.mode %q is not supported", prefix, encoding.Mode)) failures = append(failures, fmt.Errorf("%s.mode %q is not supported", prefix, encoding.Mode))
} }
+1 -1
View File
@@ -99,7 +99,7 @@ func buildGenerated2DAAssetGroup(p *project.Project, cfg project.GeneratedTopDat
results := make([]Generated2DAAsset, 0, len(collected)) results := make([]Generated2DAAsset, 0, len(collected))
for _, dataset := range 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 { if err != nil {
return nil, err return nil, err
} }
+237 -13
View File
@@ -71,6 +71,144 @@ type resolvedTableRegistry struct {
tableByKey map[string]resolvedTable 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) { func newResolvedTableRegistry(collected []nativeCollectedDataset) (resolvedTableRegistry, error) {
registry := resolvedTableRegistry{ registry := resolvedTableRegistry{
stemByKey: map[string]string{}, stemByKey: map[string]string{},
@@ -356,6 +494,7 @@ func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress
groupStats := nativeCompileGroupStats(collected) groupStats := nativeCompileGroupStats(collected)
currentGroup := "" currentGroup := ""
sidecars := newNativeSidecarCollector()
for _, dataset := range collected { for _, dataset := range collected {
group := nativeCompileGroup(dataset.Dataset.Name) group := nativeCompileGroup(dataset.Dataset.Name)
if group != currentGroup { if group != currentGroup {
@@ -368,7 +507,7 @@ func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress
stats.SourceFragments, 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 { if err != nil {
return BuildResult{}, err return BuildResult{}, err
} }
@@ -376,6 +515,9 @@ func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress
return BuildResult{}, err return BuildResult{}, err
} }
} }
if err := sidecars.writeAll(output2DA); err != nil {
return BuildResult{}, err
}
filesTLK, err := compiler.finish(outputTLK, filepath.Base(p.TopDataCompiledTLKPath())) filesTLK, err := compiler.finish(outputTLK, filepath.Base(p.TopDataCompiledTLKPath()))
if err != nil { if err != nil {
@@ -3181,7 +3323,7 @@ func collectPlainDataset(dataset nativeDataset) (nativeCollectedDataset, error)
}, nil }, 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 rows := dataset.Rows
if strings.HasPrefix(filepath.ToSlash(dataset.Dataset.Name), "classes/feats/") { if strings.HasPrefix(filepath.ToSlash(dataset.Dataset.Name), "classes/feats/") {
classKey := "classes:" + dataset.Dataset.Name[strings.LastIndex(dataset.Dataset.Name, "/")+1:] 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, rowByKey: globalRowByKey,
tableRegistry: tableRegistry, tableRegistry: tableRegistry,
compiler: compiler, compiler: compiler,
sidecars: sidecars,
cache: map[string]map[string]tlkCompiledValue{}, cache: map[string]map[string]tlkCompiledValue{},
} }
@@ -3818,6 +3961,7 @@ type valueResolver struct {
rowByKey map[string]map[string]any rowByKey map[string]map[string]any
tableRegistry resolvedTableRegistry tableRegistry resolvedTableRegistry
compiler *tlkCompiler compiler *tlkCompiler
sidecars *nativeSidecarCollector
cache map[string]map[string]tlkCompiledValue 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 { } else if ok {
return r.resolveTLKPayload(row, field, payload, stack) 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) encoded, err := encodeConfiguredAlignmentHexList(value, encoding)
if err != nil { if err != nil {
return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err) return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err)
} }
return tlkCompiledValue{Value: encoded}, nil 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) { switch typed := value.(type) {
@@ -3916,8 +4069,8 @@ func (r *valueResolver) resolveValue(row map[string]any, field string, value any
_, hasList := typed["list"] _, hasList := typed["list"]
_, hasAllExcept := typed["all_except"] _, hasAllExcept := typed["all_except"]
_, hasIDs := typed["ids"] _, hasIDs := typed["ids"]
if mode == "packed_hex_list" && (hasList || hasAllExcept) { if (mode == "packed_hex_list" || mode == "external_hex_list") && (hasList || hasAllExcept) {
value, err := encodeConfiguredValueList(typed, encoding) value, err := r.encodeConfiguredValueList(row, field, typed, encoding)
if err != nil { if err != nil {
return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err) return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err)
} }
@@ -4003,40 +4156,63 @@ func (r *valueResolver) valueDefaultForField(field string) (any, bool) {
return value, ok 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) { func encodeConfiguredValueList(obj map[string]any, encoding project.TopDataValueEncodingConfig) (string, error) {
if strings.TrimSpace(encoding.Mode) != "packed_hex_list" { if strings.TrimSpace(encoding.Mode) != "packed_hex_list" {
return "", fmt.Errorf("unsupported value encoding mode %q", encoding.Mode) 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"] _, hasList := obj["list"]
_, hasAllExcept := obj["all_except"] _, hasAllExcept := obj["all_except"]
if hasList && hasAllExcept { 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 { for key := range obj {
if key != "list" && key != "all_except" { 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 { if hasAllExcept {
values, err := expandConfiguredValueAllExcept(obj["all_except"], encoding) values, err := expandConfiguredValueAllExcept(obj["all_except"], encoding)
if err != nil { if err != nil {
return "", err return nil, err
} }
return formatConfiguredPackedHexList(values, encoding), nil return values, nil
} }
rawList, ok := obj["list"].([]any) rawList, ok := obj["list"].([]any)
if !ok { 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)) values := make([]int, 0, len(rawList))
for _, raw := range rawList { for _, raw := range rawList {
expanded, err := expandConfiguredValueListItem(raw, encoding) expanded, err := expandConfiguredValueListItem(raw, encoding)
if err != nil { if err != nil {
return "", err return nil, err
} }
values = append(values, expanded...) values = append(values, expanded...)
} }
return formatConfiguredPackedHexList(values, encoding), nil return values, nil
} }
func encodeConfiguredAlignmentHexList(raw any, encoding project.TopDataValueEncodingConfig) (string, error) { 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 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) { 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) return "", fmt.Errorf("unsupported value encoding mode %q", encoding.Mode)
} }
for key := range obj { 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 { func formatConfiguredPackedHexList(values []int, encoding project.TopDataValueEncodingConfig) string {
if len(values) == 0 { if len(values) == 0 {
return "" return ""
+160
View File
@@ -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) { func TestBuildNativeEncodesConfiguredIDHexLists(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat")) 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) { func TestBuildNativeAppliesWildcardConfiguredValueEncodings(t *testing.T) {
root := testProjectRoot(t) root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat")) mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
@@ -1118,6 +1277,7 @@ func TestResolveNativeDatasetPreservesScalarTableReferenceBehavior(t *testing.T)
tableRegistry, tableRegistry,
nil, nil,
project.TopDataClassFeatInjectionConfig{}, project.TopDataClassFeatInjectionConfig{},
nil,
) )
if err != nil { if err != nil {
t.Fatalf("resolveNativeDataset failed: %v", err) t.Fatalf("resolveNativeDataset failed: %v", err)