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
+241 -17
View File
@@ -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" {
encoded, err := encodeConfiguredAlignmentHexList(value, encoding)
if err != nil {
return tlkCompiledValue{}, fmt.Errorf("field %s: %w", field, err)
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
}
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 ""