Sidecar generation
This commit is contained in:
+241
-17
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user