From 67c03fa6bd7c56c1379bc27c81ac9f8b7f699cf9 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Sun, 31 May 2026 08:17:41 +0200 Subject: [PATCH] Fix alignment/ids output --- README.md | 5 ++++ internal/topdata/native.go | 32 ++++++++++++++++++++ internal/topdata/topdata_test.go | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/README.md b/README.md index fba461c..8da2371 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,11 @@ 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. Supported compact encodings include packed hex lists, external hex-list sidecar tables, alignment set masks, and ID hex lists. +Explicit `{"alignments": [...]}` and `{"ids": [...]}` objects are portable in +any native topdata dataset even without a column-specific rule; configured +rules still override when a column needs custom bounds, widths, or alignment +encoding mode. Plain `{"list": [...]}` objects remain config-driven because +numeric lists need table-specific output semantics. Class spell availability can be authored as JSON spellbooks under `topdata/data/classes/spellbooks/`. Each file names a `classes:*` row, and the diff --git a/internal/topdata/native.go b/internal/topdata/native.go index 14e8cec..5ca02d1 100644 --- a/internal/topdata/native.go +++ b/internal/topdata/native.go @@ -4252,6 +4252,20 @@ func (r *valueResolver) resolveValue(row map[string]any, field string, value any return tlkCompiledValue{Value: value}, nil } } + if _, hasAlignments := typed["alignments"]; hasAlignments { + encoded, err := encodeConfiguredAlignmentSetMask(typed, defaultExplicitAlignmentSetMaskEncoding()) + if err != nil { + return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err) + } + return tlkCompiledValue{Value: encoded}, nil + } + if _, hasIDs := typed["ids"]; hasIDs { + value, err := r.encodeConfiguredIDHexList(typed, defaultExplicitIDHexListEncoding()) + if err != nil { + return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err) + } + return tlkCompiledValue{Value: value}, nil + } if refID, ok := typed["id"]; ok { if refKey, ok := refID.(string); ok && strings.Contains(refKey, ":") { id, ok := r.keyToID[refKey] @@ -4413,6 +4427,24 @@ func encodeConfiguredAlignmentSetMask(raw any, encoding project.TopDataValueEnco return "0x" + fmt.Sprintf("%0*X", encoding.HexWidth, mask), nil } +func defaultExplicitAlignmentSetMaskEncoding() project.TopDataValueEncodingConfig { + return project.TopDataValueEncodingConfig{ + Mode: "alignment_set_mask", + Min: 0, + Max: 511, + HexWidth: 3, + } +} + +func defaultExplicitIDHexListEncoding() project.TopDataValueEncodingConfig { + return project.TopDataValueEncodingConfig{ + Mode: "id_hex_list", + Min: 0, + Max: 65535, + HexWidth: 4, + } +} + func (r *valueResolver) encodeConfiguredIDHexList(obj map[string]any, encoding project.TopDataValueEncodingConfig) (string, error) { mode := strings.TrimSpace(encoding.Mode) if mode != "id_hex_list" { diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index 20664c3..bc8d03a 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -586,6 +586,57 @@ func TestBuildNativeAppliesWildcardConfiguredValueEncodings(t *testing.T) { } } +func TestBuildNativeEncodesExplicitAlignmentAndIDListsInAnyDataset(t *testing.T) { + root := testProjectRoot(t) + mkdirAll(t, filepath.Join(root, "topdata", "data", "feat")) + mkdirAll(t, filepath.Join(root, "topdata", "data", "futuretable", "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": 2, "key": "feat:first_future", "LABEL": "FirstFuture"}, + {"id": 17, "key": "feat:second_future", "LABEL": "SecondFuture"} + ] +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{ + "feat:first_future": 2, + "feat:second_future": 17 +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "futuretable", "base.json"), `{ + "output": "futuretable.2da", + "columns": ["Label", "AnyAlignmentMask", "AnyIDList"], + "rows": [ + {"id": 0, "key": "futuretable:first", "Label": "First"} + ] +}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "futuretable", "lock.json"), `{"futuretable:first":0}`+"\n") + writeFile(t, filepath.Join(root, "topdata", "data", "futuretable", "modules", "10_first.json"), `{ + "overrides": [ + { + "key": "futuretable:first", + "AnyAlignmentMask": {"alignments": ["lg", "tn", "ce"]}, + "AnyIDList": {"ids": ["feat:first_future", {"id": "feat:second_future"}]} + } + ] +}`+"\n") + + proj := testProject(root) + + 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, "futuretable.2da")) + if err != nil { + t.Fatalf("read futuretable.2da: %v", err) + } + got := string(raw) + if !strings.Contains(got, "0\tFirst\t0x111\t0x00020011\n") { + t.Fatalf("expected generic explicit list encodings in futuretable.2da, got:\n%s", got) + } +} + func TestBuildNativeAppliesConfiguredValueDefaults(t *testing.T) { root := testProjectRoot(t) mkdirAll(t, filepath.Join(root, "topdata", "data", "racialtypes", "core", "modules"))