initial
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
.cache
|
||||
.claude
|
||||
.codex
|
||||
.DS_Store
|
||||
|
||||
.idea
|
||||
bin
|
||||
obj
|
||||
|
||||
Library
|
||||
Logs
|
||||
Temp
|
||||
UserSettings
|
||||
|
||||
ragon
|
||||
@@ -0,0 +1,23 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- errcheck
|
||||
- govet
|
||||
- staticcheck
|
||||
- unused
|
||||
- gocritic
|
||||
- misspell
|
||||
- prealloc
|
||||
- unconvert
|
||||
- unparam
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- goimports
|
||||
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes:
|
||||
- gorena/server
|
||||
@@ -0,0 +1,146 @@
|
||||
<p align="center">
|
||||
<img src="images/logo.png" alt="arpack logo" width="240">
|
||||
</p>
|
||||
|
||||
# ArPack
|
||||
|
||||
Binary serialization code generator for Go and C#. Define messages once as Go structs — get zero-allocation `Marshal`/`Unmarshal` for Go and `unsafe` pointer-based `Serialize`/`Deserialize` for C#.
|
||||
|
||||
Built for game networking where every byte and allocation matters.
|
||||
|
||||
## Features
|
||||
|
||||
- **Single source of truth** — define messages in Go, generate both Go and C# code
|
||||
- **Float quantization** — compress `float32`/`float64` to 8 or 16 bits with a `pack` struct tag
|
||||
- **Boolean packing** — consecutive `bool` fields are packed into single bytes (up to 8 per byte)
|
||||
- **Enums** — `type Opcode uint16` + `const` block becomes a C# `enum`
|
||||
- **Nested types, fixed arrays, slices** — full support for complex message structures
|
||||
- **Cross-language binary compatibility** — Go and C# produce identical wire formats
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go install edmand46/apack@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
arpack -in messages.go -out-go ./gen -out-cs ../Unity/Assets/Scripts
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|---|---|
|
||||
| `-in` | Input Go file with struct definitions (required) |
|
||||
| `-out-go` | Output directory for generated Go code |
|
||||
| `-out-cs` | Output directory for generated C# code |
|
||||
| `-cs-namespace` | C# namespace (default: `Arpack.Messages`) |
|
||||
|
||||
At least one of `-out-go` or `-out-cs` is required.
|
||||
|
||||
**Output files:**
|
||||
- Go: `{name}_gen.go`
|
||||
- C#: `{Name}.gen.cs`
|
||||
|
||||
## Schema Definition
|
||||
|
||||
Messages are defined as Go structs in a single `.go` file:
|
||||
|
||||
```go
|
||||
package messages
|
||||
|
||||
// Quantized 3D vector — 6 bytes instead of 12
|
||||
type Vector3 struct {
|
||||
X float32 `pack:"min=-500,max=500,bits=16"`
|
||||
Y float32 `pack:"min=-500,max=500,bits=16"`
|
||||
Z float32 `pack:"min=-500,max=500,bits=16"`
|
||||
}
|
||||
|
||||
// Enum
|
||||
type Opcode uint16
|
||||
|
||||
const (
|
||||
OpcodeUnknown Opcode = iota
|
||||
OpcodeAuthorize
|
||||
OpcodeJoinRoom
|
||||
)
|
||||
|
||||
type MoveMessage struct {
|
||||
Position Vector3 // nested type
|
||||
Velocity [3]float32 // fixed-length array
|
||||
Waypoints []Vector3 // variable-length slice
|
||||
PlayerID uint32
|
||||
Active bool // 3 consecutive bools →
|
||||
Visible bool // packed into 1 byte
|
||||
Ghost bool
|
||||
Name string
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Types
|
||||
|
||||
| Type | Wire Size |
|
||||
|---|---|
|
||||
| `bool` (packed) | 1 bit (up to 8 per byte) |
|
||||
| `int8`, `uint8` | 1 byte |
|
||||
| `int16`, `uint16` | 2 bytes |
|
||||
| `int32`, `uint32`, `float32` | 4 bytes |
|
||||
| `int64`, `uint64`, `float64` | 8 bytes |
|
||||
| `string` | 2-byte length prefix + UTF-8 |
|
||||
| `[N]T` | N × sizeof(T) |
|
||||
| `[]T` | 2-byte length prefix + N × sizeof(T) |
|
||||
|
||||
### Float Quantization
|
||||
|
||||
Use the `pack` struct tag to compress floats:
|
||||
|
||||
```go
|
||||
X float32 `pack:"min=-500,max=500,bits=16"` // 2 bytes instead of 4
|
||||
Y float32 `pack:"min=0,max=1,bits=8"` // 1 byte instead of 4
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|---|---|
|
||||
| `min` | Minimum expected value |
|
||||
| `max` | Maximum expected value |
|
||||
| `bits` | Target size: `8` (uint8) or `16` (uint16) |
|
||||
|
||||
Values are linearly mapped: `encoded = (value - min) / (max - min) * maxUint`.
|
||||
|
||||
## Generated Code
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
func (m *MoveMessage) Marshal(buf []byte) []byte
|
||||
func (m *MoveMessage) Unmarshal(data []byte) (int, error)
|
||||
```
|
||||
|
||||
`Marshal` appends to the buffer and returns it. `Unmarshal` reads from the buffer and returns bytes consumed.
|
||||
|
||||
### C#
|
||||
|
||||
```csharp
|
||||
public unsafe int Serialize(byte* buffer)
|
||||
public static unsafe int Deserialize(byte* buffer, out MoveMessage msg)
|
||||
```
|
||||
|
||||
Uses unsafe pointers for zero-copy serialization. Returns bytes written/consumed.
|
||||
|
||||
## Wire Format
|
||||
|
||||
- Little-endian byte order
|
||||
- No message framing — fields are written in declaration order
|
||||
- Variable-length fields (`string`, `[]T`) prefixed with `uint16` length
|
||||
- Booleans packed as bitfields (LSB first, up to 8 per byte)
|
||||
- Quantized floats stored as `uint8` or `uint16`
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Unit tests (parser + generator)
|
||||
go test ./tools/arpack/...
|
||||
|
||||
# End-to-end cross-language tests (requires dotnet SDK)
|
||||
go test ./tools/arpack/e2e/...
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"edmand46/arpack/generator"
|
||||
"edmand46/arpack/parser"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
in := flag.String("in", "", "input Go file with struct definitions")
|
||||
outGo := flag.String("out-go", "", "output directory for generated Go code")
|
||||
outCS := flag.String("out-cs", "", "output directory for generated C# code")
|
||||
namespace := flag.String("cs-namespace", "Arpack.Messages", "C# namespace")
|
||||
flag.Parse()
|
||||
|
||||
if *in == "" {
|
||||
log.Fatal("arpack: -in is required")
|
||||
}
|
||||
if *outGo == "" && *outCS == "" {
|
||||
log.Fatal("arpack: at least one of -out-go or -out-cs is required")
|
||||
}
|
||||
|
||||
schema, err := parser.ParseSchemaFile(*in)
|
||||
if err != nil {
|
||||
log.Fatalf("arpack: parse error: %v", err)
|
||||
}
|
||||
msgs := schema.Messages
|
||||
if len(msgs) == 0 {
|
||||
log.Fatalf("arpack: no structs found in %s", *in)
|
||||
}
|
||||
|
||||
baseName := strings.TrimSuffix(filepath.Base(*in), ".go")
|
||||
|
||||
if *outGo != "" {
|
||||
pkgName := filepath.Base(*outGo)
|
||||
if pkgName == "." || pkgName == "" {
|
||||
pkgName = msgs[0].PackageName
|
||||
}
|
||||
|
||||
src, err := generator.GenerateGoSchema(schema, pkgName)
|
||||
if err != nil {
|
||||
log.Fatalf("arpack: Go generation error: %v", err)
|
||||
}
|
||||
|
||||
outPath := filepath.Join(*outGo, baseName+"_gen.go")
|
||||
if err := os.MkdirAll(*outGo, 0755); err != nil {
|
||||
log.Fatalf("arpack: mkdir %s: %v", *outGo, err)
|
||||
}
|
||||
if err := os.WriteFile(outPath, src, 0644); err != nil {
|
||||
log.Fatalf("arpack: write %s: %v", outPath, err)
|
||||
}
|
||||
|
||||
fmt.Printf("arpack: wrote %s\n", outPath)
|
||||
}
|
||||
|
||||
if *outCS != "" {
|
||||
src, err := generator.GenerateCSharpSchema(schema, *namespace)
|
||||
if err != nil {
|
||||
log.Fatalf("arpack: C# generation error: %v", err)
|
||||
}
|
||||
|
||||
outPath := filepath.Join(*outCS, toTitle(baseName)+".gen.cs")
|
||||
if err := os.MkdirAll(*outCS, 0755); err != nil {
|
||||
log.Fatalf("arpack: mkdir %s: %v", *outCS, err)
|
||||
}
|
||||
if err := os.WriteFile(outPath, src, 0644); err != nil {
|
||||
log.Fatalf("arpack: write %s: %v", outPath, err)
|
||||
}
|
||||
|
||||
fmt.Printf("arpack: wrote %s\n", outPath)
|
||||
}
|
||||
}
|
||||
|
||||
func toTitle(s string) string {
|
||||
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
|
||||
}
|
||||
+521
@@ -0,0 +1,521 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"edmand46/arpack/generator"
|
||||
"edmand46/arpack/parser"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const samplePath = "../testdata/sample.go"
|
||||
|
||||
// TestE2E_CrossLanguage гоняет сериализацию в обе стороны: Go → C# и C# → Go.
|
||||
func TestE2E_CrossLanguage(t *testing.T) {
|
||||
if _, err := exec.LookPath("dotnet"); err != nil {
|
||||
t.Skip("dotnet not found, skipping cross-language e2e test")
|
||||
}
|
||||
|
||||
schema, err := parser.ParseSchemaFile(samplePath)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
|
||||
goSrc, err := generator.GenerateGoSchema(schema, "main")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateGoSchema: %v", err)
|
||||
}
|
||||
|
||||
csSrc, err := generator.GenerateCSharpSchema(schema, "Ragono.Messages")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCSharpSchema: %v", err)
|
||||
}
|
||||
|
||||
goDir := buildGoHarness(t, goSrc)
|
||||
csDir := buildCSHarness(t, csSrc)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
typ string
|
||||
epsilon float64
|
||||
}{
|
||||
{"Vector3", "Vector3", 0.02}, // quantized float32 → допустимая погрешность
|
||||
{"SpawnMessage", "SpawnMessage", 0.02}, // mix: int, nested, []string, []byte
|
||||
{"MoveMessage", "MoveMessage", 0.02}, // bool bit packing: Active, Visible, Ghost
|
||||
{"EnvelopeMessage", "EnvelopeMessage", 0},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run("Go_to_CS/"+tc.name, func(t *testing.T) {
|
||||
hex := runHarness(t, goDir, "go", "ser", tc.typ, "")
|
||||
out := runHarness(t, csDir, "cs", "deser", tc.typ, hex)
|
||||
checkOutput(t, tc.typ, out, tc.epsilon)
|
||||
})
|
||||
t.Run("CS_to_Go/"+tc.name, func(t *testing.T) {
|
||||
hex := runHarness(t, csDir, "cs", "ser", tc.typ, "")
|
||||
out := runHarness(t, goDir, "go", "deser", tc.typ, hex)
|
||||
checkOutput(t, tc.typ, out, tc.epsilon)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Build helpers ---
|
||||
|
||||
func buildGoHarness(t *testing.T, generatedSrc []byte) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
|
||||
// Читаем sample.go и меняем package на main
|
||||
sampleSrc, err := os.ReadFile(samplePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read sample: %v", err)
|
||||
}
|
||||
sampleSrc = bytes.Replace(sampleSrc, []byte("package messages"), []byte("package main"), 1)
|
||||
|
||||
// Generated код уже имеет package main (мы передали "main" в GenerateGo)
|
||||
write(t, filepath.Join(dir, "messages.go"), sampleSrc)
|
||||
write(t, filepath.Join(dir, "messages_arpack.go"), generatedSrc)
|
||||
write(t, filepath.Join(dir, "main.go"), []byte(goHarnessSource))
|
||||
write(t, filepath.Join(dir, "go.mod"), []byte("module arpack_e2e\n\ngo 1.21\n"))
|
||||
|
||||
mustRun(t, dir, "go", "build", "-o", "harness", ".")
|
||||
return dir
|
||||
}
|
||||
|
||||
func buildCSHarness(t *testing.T, generatedSrc []byte) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
|
||||
write(t, filepath.Join(dir, "Messages.cs"), generatedSrc)
|
||||
write(t, filepath.Join(dir, "Program.cs"), []byte(csHarnessSource))
|
||||
write(t, filepath.Join(dir, "E2EHarness.csproj"), []byte(csProjSource))
|
||||
|
||||
mustRun(t, dir, "dotnet", "build", "-c", "Release", "-o", "out")
|
||||
return dir
|
||||
}
|
||||
|
||||
// --- Harness runners ---
|
||||
|
||||
func runHarness(t *testing.T, dir, lang, op, typ, hexInput string) string {
|
||||
t.Helper()
|
||||
var cmd *exec.Cmd
|
||||
if lang == "go" {
|
||||
args := []string{op, typ}
|
||||
if hexInput != "" {
|
||||
args = append(args, hexInput)
|
||||
}
|
||||
cmd = exec.Command(filepath.Join(dir, "harness"), args...)
|
||||
} else {
|
||||
args := []string{op, typ}
|
||||
if hexInput != "" {
|
||||
args = append(args, hexInput)
|
||||
}
|
||||
cmd = exec.Command("dotnet", append([]string{filepath.Join(dir, "out", "E2EHarness.dll")}, args...)...)
|
||||
}
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%s harness %s %s failed: %v\n%s", lang, op, typ, err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// --- Output verification ---
|
||||
|
||||
// checkOutput парсит key=value вывод и сравнивает с ожидаемыми значениями.
|
||||
func checkOutput(t *testing.T, typ, output string, epsilon float64) {
|
||||
t.Helper()
|
||||
t.Logf("output for %s:\n%s", typ, output)
|
||||
|
||||
kv := parseKV(output)
|
||||
|
||||
switch typ {
|
||||
case "Vector3":
|
||||
assertFloat(t, kv, "X", 123.45, epsilon)
|
||||
assertFloat(t, kv, "Y", -200.0, epsilon)
|
||||
assertFloat(t, kv, "Z", 0.0, epsilon)
|
||||
|
||||
case "SpawnMessage":
|
||||
assertInt(t, kv, "EntityID", 42)
|
||||
assertFloat(t, kv, "Position.X", 10.0, epsilon)
|
||||
assertFloat(t, kv, "Position.Y", 20.0, epsilon)
|
||||
assertFloat(t, kv, "Position.Z", 30.0, epsilon)
|
||||
assertInt(t, kv, "Health", -100)
|
||||
assertStr(t, kv, "Tags[0]", "hero")
|
||||
assertStr(t, kv, "Tags[1]", "player")
|
||||
assertInt(t, kv, "Data[0]", 1)
|
||||
assertInt(t, kv, "Data[1]", 2)
|
||||
assertInt(t, kv, "Data[2]", 3)
|
||||
|
||||
case "MoveMessage":
|
||||
assertInt(t, kv, "PlayerID", 777)
|
||||
assertStr(t, kv, "Active", "true")
|
||||
assertStr(t, kv, "Visible", "false")
|
||||
assertStr(t, kv, "Ghost", "true")
|
||||
assertStr(t, kv, "Name", "TestPlayer")
|
||||
case "EnvelopeMessage":
|
||||
assertInt(t, kv, "Code", 2)
|
||||
assertInt(t, kv, "Counter", 7)
|
||||
}
|
||||
}
|
||||
|
||||
func parseKV(s string) map[string]string {
|
||||
m := map[string]string{}
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if idx := strings.IndexByte(line, '='); idx >= 0 {
|
||||
m[line[:idx]] = line[idx+1:]
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func assertFloat(t *testing.T, kv map[string]string, key string, want, eps float64) {
|
||||
t.Helper()
|
||||
s, ok := kv[key]
|
||||
if !ok {
|
||||
t.Errorf("missing key %q in output", key)
|
||||
return
|
||||
}
|
||||
got, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
t.Errorf("%s: cannot parse %q as float: %v", key, s, err)
|
||||
return
|
||||
}
|
||||
if math.Abs(got-want) > eps {
|
||||
t.Errorf("%s: got %v, want %v (±%v)", key, got, want, eps)
|
||||
}
|
||||
}
|
||||
|
||||
func assertInt(t *testing.T, kv map[string]string, key string, want int64) {
|
||||
t.Helper()
|
||||
s, ok := kv[key]
|
||||
if !ok {
|
||||
t.Errorf("missing key %q in output", key)
|
||||
return
|
||||
}
|
||||
got, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
t.Errorf("%s: cannot parse %q as int: %v", key, s, err)
|
||||
return
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("%s: got %d, want %d", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertStr(t *testing.T, kv map[string]string, key, want string) {
|
||||
t.Helper()
|
||||
got, ok := kv[key]
|
||||
if !ok {
|
||||
t.Errorf("missing key %q in output", key)
|
||||
return
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("%s: got %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Utilities ---
|
||||
|
||||
func write(t *testing.T, path string, data []byte) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatalf("write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRun(t *testing.T, dir string, name string, args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Dir = dir
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s %v failed: %v\n%s", name, args, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Go harness source ---
|
||||
|
||||
const goHarnessSource = `package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
op := os.Args[1] // ser | deser
|
||||
typ := os.Args[2] // Vector3 | SpawnMessage | ...
|
||||
|
||||
switch op + ":" + typ {
|
||||
|
||||
case "ser:Vector3":
|
||||
v := Vector3{X: 123.45, Y: -200, Z: 0}
|
||||
fmt.Println(hex.EncodeToString(v.Marshal(nil)))
|
||||
|
||||
case "deser:Vector3":
|
||||
data, _ := hex.DecodeString(strings.TrimSpace(os.Args[3]))
|
||||
var v Vector3
|
||||
v.Unmarshal(data)
|
||||
fmt.Printf("X=%v\nY=%v\nZ=%v\n", v.X, v.Y, v.Z)
|
||||
|
||||
case "ser:SpawnMessage":
|
||||
msg := SpawnMessage{
|
||||
EntityID: 42,
|
||||
Position: Vector3{X: 10, Y: 20, Z: 30},
|
||||
Health: -100,
|
||||
Tags: []string{"hero", "player"},
|
||||
Data: []uint8{1, 2, 3},
|
||||
}
|
||||
fmt.Println(hex.EncodeToString(msg.Marshal(nil)))
|
||||
|
||||
case "deser:SpawnMessage":
|
||||
data, _ := hex.DecodeString(strings.TrimSpace(os.Args[3]))
|
||||
var msg SpawnMessage
|
||||
msg.Unmarshal(data)
|
||||
fmt.Printf("EntityID=%d\n", msg.EntityID)
|
||||
fmt.Printf("Position.X=%v\n", msg.Position.X)
|
||||
fmt.Printf("Position.Y=%v\n", msg.Position.Y)
|
||||
fmt.Printf("Position.Z=%v\n", msg.Position.Z)
|
||||
fmt.Printf("Health=%d\n", msg.Health)
|
||||
for i, tag := range msg.Tags {
|
||||
fmt.Printf("Tags[%d]=%s\n", i, tag)
|
||||
}
|
||||
for i, b := range msg.Data {
|
||||
fmt.Printf("Data[%d]=%d\n", i, b)
|
||||
}
|
||||
|
||||
case "ser:MoveMessage":
|
||||
msg := MoveMessage{
|
||||
Position: Vector3{X: 50, Y: -100, Z: 0},
|
||||
Velocity: [3]float32{1.5, -2.5, 0},
|
||||
Waypoints: []Vector3{{X: 10, Y: 20, Z: 0}},
|
||||
PlayerID: 777,
|
||||
Active: true,
|
||||
Visible: false,
|
||||
Ghost: true,
|
||||
Name: "TestPlayer",
|
||||
}
|
||||
fmt.Println(hex.EncodeToString(msg.Marshal(nil)))
|
||||
|
||||
case "deser:MoveMessage":
|
||||
data, _ := hex.DecodeString(strings.TrimSpace(os.Args[3]))
|
||||
var msg MoveMessage
|
||||
msg.Unmarshal(data)
|
||||
fmt.Printf("PlayerID=%d\n", msg.PlayerID)
|
||||
fmt.Printf("Active=%v\n", msg.Active)
|
||||
fmt.Printf("Visible=%v\n", msg.Visible)
|
||||
fmt.Printf("Ghost=%v\n", msg.Ghost)
|
||||
fmt.Printf("Name=%s\n", msg.Name)
|
||||
|
||||
case "ser:EnvelopeMessage":
|
||||
msg := EnvelopeMessage{
|
||||
Code: OpcodeJoinRoom,
|
||||
Counter: 7,
|
||||
}
|
||||
fmt.Println(hex.EncodeToString(msg.Marshal(nil)))
|
||||
|
||||
case "deser:EnvelopeMessage":
|
||||
data, _ := hex.DecodeString(strings.TrimSpace(os.Args[3]))
|
||||
var msg EnvelopeMessage
|
||||
msg.Unmarshal(data)
|
||||
fmt.Printf("Code=%d\n", msg.Code)
|
||||
fmt.Printf("Counter=%d\n", msg.Counter)
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown op:type %s:%s\n", op, typ)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// --- C# harness source ---
|
||||
|
||||
const csHarnessSource = `using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Ragono.Messages;
|
||||
|
||||
unsafe class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||
string op = args[0]; // ser | deser
|
||||
string typ = args[1]; // Vector3 | SpawnMessage | ...
|
||||
|
||||
switch (op + ":" + typ)
|
||||
{
|
||||
case "ser:Vector3":
|
||||
SerVector3();
|
||||
break;
|
||||
case "deser:Vector3":
|
||||
DeserVector3(args[2]);
|
||||
break;
|
||||
case "ser:SpawnMessage":
|
||||
SerSpawnMessage();
|
||||
break;
|
||||
case "deser:SpawnMessage":
|
||||
DeserSpawnMessage(args[2]);
|
||||
break;
|
||||
case "ser:MoveMessage":
|
||||
SerMoveMessage();
|
||||
break;
|
||||
case "deser:MoveMessage":
|
||||
DeserMoveMessage(args[2]);
|
||||
break;
|
||||
case "ser:EnvelopeMessage":
|
||||
SerEnvelopeMessage();
|
||||
break;
|
||||
case "deser:EnvelopeMessage":
|
||||
DeserEnvelopeMessage(args[2]);
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"unknown op:type {op}:{typ}");
|
||||
Environment.Exit(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static unsafe void SerVector3()
|
||||
{
|
||||
var msg = new Vector3 { X = 123.45f, Y = -200.0f, Z = 0.0f };
|
||||
byte[] buf = new byte[64];
|
||||
fixed (byte* ptr = buf)
|
||||
{
|
||||
int n = msg.Serialize(ptr);
|
||||
Console.WriteLine(Convert.ToHexString(buf, 0, n).ToLower());
|
||||
}
|
||||
}
|
||||
|
||||
static unsafe void DeserVector3(string hexStr)
|
||||
{
|
||||
byte[] data = Convert.FromHexString(hexStr);
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
Vector3.Deserialize(ptr, out Vector3 msg);
|
||||
Console.WriteLine($"X={msg.X:G9}");
|
||||
Console.WriteLine($"Y={msg.Y:G9}");
|
||||
Console.WriteLine($"Z={msg.Z:G9}");
|
||||
}
|
||||
}
|
||||
|
||||
static unsafe void SerSpawnMessage()
|
||||
{
|
||||
var msg = new SpawnMessage
|
||||
{
|
||||
EntityID = 42,
|
||||
Position = new Vector3 { X = 10.0f, Y = 20.0f, Z = 30.0f },
|
||||
Health = -100,
|
||||
Tags = new string[] { "hero", "player" },
|
||||
Data = new byte[] { 1, 2, 3 },
|
||||
};
|
||||
byte[] buf = new byte[512];
|
||||
fixed (byte* ptr = buf)
|
||||
{
|
||||
int n = msg.Serialize(ptr);
|
||||
Console.WriteLine(Convert.ToHexString(buf, 0, n).ToLower());
|
||||
}
|
||||
}
|
||||
|
||||
static unsafe void DeserSpawnMessage(string hexStr)
|
||||
{
|
||||
byte[] data = Convert.FromHexString(hexStr);
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
SpawnMessage.Deserialize(ptr, out SpawnMessage msg);
|
||||
Console.WriteLine($"EntityID={msg.EntityID}");
|
||||
Console.WriteLine($"Position.X={msg.Position.X:G9}");
|
||||
Console.WriteLine($"Position.Y={msg.Position.Y:G9}");
|
||||
Console.WriteLine($"Position.Z={msg.Position.Z:G9}");
|
||||
Console.WriteLine($"Health={msg.Health}");
|
||||
if (msg.Tags != null)
|
||||
for (int i = 0; i < msg.Tags.Length; i++)
|
||||
Console.WriteLine($"Tags[{i}]={msg.Tags[i]}");
|
||||
if (msg.Data != null)
|
||||
for (int i = 0; i < msg.Data.Length; i++)
|
||||
Console.WriteLine($"Data[{i}]={msg.Data[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
static unsafe void SerMoveMessage()
|
||||
{
|
||||
var msg = new MoveMessage
|
||||
{
|
||||
Position = new Vector3 { X = 50.0f, Y = -100.0f, Z = 0.0f },
|
||||
Velocity = new float[] { 1.5f, -2.5f, 0.0f },
|
||||
Waypoints = new Vector3[] { new Vector3 { X = 10.0f, Y = 20.0f, Z = 0.0f } },
|
||||
PlayerID = 777,
|
||||
Active = true,
|
||||
Visible = false,
|
||||
Ghost = true,
|
||||
Name = "TestPlayer",
|
||||
};
|
||||
byte[] buf = new byte[512];
|
||||
fixed (byte* ptr = buf)
|
||||
{
|
||||
int n = msg.Serialize(ptr);
|
||||
Console.WriteLine(Convert.ToHexString(buf, 0, n).ToLower());
|
||||
}
|
||||
}
|
||||
|
||||
static unsafe void DeserMoveMessage(string hexStr)
|
||||
{
|
||||
byte[] data = Convert.FromHexString(hexStr);
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
MoveMessage.Deserialize(ptr, out MoveMessage msg);
|
||||
Console.WriteLine($"PlayerID={msg.PlayerID}");
|
||||
Console.WriteLine($"Active={msg.Active.ToString().ToLower()}");
|
||||
Console.WriteLine($"Visible={msg.Visible.ToString().ToLower()}");
|
||||
Console.WriteLine($"Ghost={msg.Ghost.ToString().ToLower()}");
|
||||
Console.WriteLine($"Name={msg.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
static unsafe void SerEnvelopeMessage()
|
||||
{
|
||||
var msg = new EnvelopeMessage
|
||||
{
|
||||
Code = Opcode.JoinRoom,
|
||||
Counter = 7,
|
||||
};
|
||||
byte[] buf = new byte[64];
|
||||
fixed (byte* ptr = buf)
|
||||
{
|
||||
int n = msg.Serialize(ptr);
|
||||
Console.WriteLine(Convert.ToHexString(buf, 0, n).ToLower());
|
||||
}
|
||||
}
|
||||
|
||||
static unsafe void DeserEnvelopeMessage(string hexStr)
|
||||
{
|
||||
byte[] data = Convert.FromHexString(hexStr);
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
EnvelopeMessage.Deserialize(ptr, out EnvelopeMessage msg);
|
||||
Console.WriteLine($"Code={(ushort)msg.Code}");
|
||||
Console.WriteLine($"Counter={msg.Counter}");
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var csProjSource = fmt.Sprintf(`<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
`)
|
||||
@@ -0,0 +1,478 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"edmand46/arpack/parser"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateCSharp генерирует C# unsafe код для списка сообщений.
|
||||
// namespace — пространство имён (например, "Ragono.Messages").
|
||||
func GenerateCSharp(messages []parser.Message, namespace string) ([]byte, error) {
|
||||
return GenerateCSharpSchema(parser.Schema{Messages: messages}, namespace)
|
||||
}
|
||||
|
||||
// GenerateCSharpSchema генерирует C# код для полной схемы, включая enum-ы.
|
||||
func GenerateCSharpSchema(schema parser.Schema, namespace string) ([]byte, error) {
|
||||
messages := schema.Messages
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("// <auto-generated> arpack </auto-generated>\n")
|
||||
b.WriteString("// Code generated by arpack. DO NOT EDIT.\n")
|
||||
b.WriteString("#pragma warning disable CS8500\n\n")
|
||||
|
||||
b.WriteString("using System;\n")
|
||||
if needsTextEncoding(messages) {
|
||||
b.WriteString("using System.Text;\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
fmt.Fprintf(&b, "namespace %s\n{\n", namespace)
|
||||
|
||||
enumNames := make(map[string]struct{}, len(schema.Enums))
|
||||
for _, enum := range schema.Enums {
|
||||
enumNames[enum.Name] = struct{}{}
|
||||
}
|
||||
|
||||
wroteSection := false
|
||||
for _, enum := range schema.Enums {
|
||||
writeCSharpEnum(&b, enum)
|
||||
b.WriteString("\n")
|
||||
wroteSection = true
|
||||
}
|
||||
|
||||
for i, msg := range messages {
|
||||
if err := writeCSharpMessage(&b, msg, enumNames); err != nil {
|
||||
return nil, fmt.Errorf("message %s: %w", msg.Name, err)
|
||||
}
|
||||
if i < len(messages)-1 {
|
||||
b.WriteString("\n")
|
||||
} else if wroteSection {
|
||||
// leave a single blank line between the last enum and the first struct only
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("}\n")
|
||||
return []byte(b.String()), nil
|
||||
}
|
||||
|
||||
func writeCSharpEnum(b *strings.Builder, enum parser.Enum) {
|
||||
fmt.Fprintf(b, " public enum %s : %s\n {\n", enum.Name, csharpEnumBaseType(enum))
|
||||
for i, value := range enum.Values {
|
||||
fmt.Fprintf(b, " %s = %s", csharpEnumValueName(enum.Name, value.Name), value.Value)
|
||||
if i < len(enum.Values)-1 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString(" }\n")
|
||||
}
|
||||
|
||||
func writeCSharpMessage(b *strings.Builder, msg parser.Message, enumNames map[string]struct{}) error {
|
||||
segs := segmentFields(msg.Fields)
|
||||
|
||||
b.WriteString(" public unsafe struct ")
|
||||
b.WriteString(msg.Name)
|
||||
b.WriteString("\n {\n")
|
||||
|
||||
// Поля
|
||||
for _, f := range msg.Fields {
|
||||
fmt.Fprintf(b, " public %s %s;\n", csharpTypeName(f, enumNames), f.Name)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
// Serialize
|
||||
b.WriteString(" public int Serialize(byte* buffer)\n")
|
||||
b.WriteString(" {\n")
|
||||
b.WriteString(" byte* ptr = buffer;\n")
|
||||
for i, seg := range segs {
|
||||
if seg.single != nil {
|
||||
if err := writeCSharpSerializeField(b, *seg.single, " ", enumNames); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
writeCSharpBoolGroupSerialize(b, seg.bools, i, " ")
|
||||
}
|
||||
}
|
||||
b.WriteString(" return (int)(ptr - buffer);\n")
|
||||
b.WriteString(" }\n\n")
|
||||
|
||||
// Deserialize
|
||||
fmt.Fprintf(b, " public static int Deserialize(byte* buffer, out %s msg)\n", msg.Name)
|
||||
b.WriteString(" {\n")
|
||||
b.WriteString(" byte* ptr = buffer;\n")
|
||||
b.WriteString(" msg = default;\n")
|
||||
for i, seg := range segs {
|
||||
if seg.single != nil {
|
||||
if err := writeCSharpDeserializeField(b, "msg", *seg.single, " ", enumNames); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
writeCSharpBoolGroupDeserialize(b, "msg", seg.bools, i, " ")
|
||||
}
|
||||
}
|
||||
b.WriteString(" return (int)(ptr - buffer);\n")
|
||||
b.WriteString(" }\n")
|
||||
|
||||
b.WriteString(" }\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCSharpBoolGroupSerialize(b *strings.Builder, bools []parser.Field, groupIdx int, indent string) {
|
||||
varName := fmt.Sprintf("_boolByte%d", groupIdx)
|
||||
fmt.Fprintf(b, "%sbyte %s = 0;\n", indent, varName)
|
||||
for bit, f := range bools {
|
||||
fmt.Fprintf(b, "%sif (%s) %s |= (byte)(1 << %d);\n", indent, f.Name, varName, bit)
|
||||
}
|
||||
fmt.Fprintf(b, "%s*ptr = %s; ptr++;\n", indent, varName)
|
||||
}
|
||||
|
||||
func writeCSharpBoolGroupDeserialize(b *strings.Builder, recv string, bools []parser.Field, groupIdx int, indent string) {
|
||||
varName := fmt.Sprintf("_boolByte%d", groupIdx)
|
||||
fmt.Fprintf(b, "%sbyte %s = *ptr; ptr++;\n", indent, varName)
|
||||
for bit, f := range bools {
|
||||
fmt.Fprintf(b, "%s%s.%s = (%s & (1 << %d)) != 0;\n", indent, recv, f.Name, varName, bit)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Serialize ---
|
||||
|
||||
func writeCSharpSerializeField(b *strings.Builder, f parser.Field, indent string, enumNames map[string]struct{}) error {
|
||||
switch f.Kind {
|
||||
case parser.KindPrimitive:
|
||||
return writeCSharpSerializePrimitive(b, f.Name, f, indent, enumNames)
|
||||
case parser.KindNested:
|
||||
fmt.Fprintf(b, "%sptr += %s.Serialize(ptr);\n", indent, f.Name)
|
||||
case parser.KindFixedArray:
|
||||
iVar := "_i" + f.Name
|
||||
fmt.Fprintf(b, "%sfor (int %s = 0; %s < %d; %s++)\n%s{\n",
|
||||
indent, iVar, iVar, f.FixedLen, iVar, indent)
|
||||
elemField := parser.Field{
|
||||
Name: f.Name + "[" + iVar + "]",
|
||||
Kind: f.Elem.Kind,
|
||||
Primitive: f.Elem.Primitive,
|
||||
NamedType: f.Elem.NamedType,
|
||||
Quant: f.Elem.Quant,
|
||||
TypeName: f.Elem.TypeName,
|
||||
Elem: f.Elem.Elem,
|
||||
FixedLen: f.Elem.FixedLen,
|
||||
}
|
||||
if err := writeCSharpSerializeField(b, elemField, indent+" ", enumNames); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(b, "%s}\n", indent)
|
||||
case parser.KindSlice:
|
||||
fmt.Fprintf(b, "%s*(ushort*)ptr = (ushort)(%s?.Length ?? 0); ptr += 2;\n", indent, f.Name)
|
||||
fmt.Fprintf(b, "%sif (%s != null)\n%s{\n", indent, f.Name, indent)
|
||||
iVar := "_i" + f.Name
|
||||
fmt.Fprintf(b, "%s for (int %s = 0; %s < %s.Length; %s++)\n%s {\n",
|
||||
indent, iVar, iVar, f.Name, iVar, indent)
|
||||
elemField := parser.Field{
|
||||
Name: f.Name + "[" + iVar + "]",
|
||||
Kind: f.Elem.Kind,
|
||||
Primitive: f.Elem.Primitive,
|
||||
NamedType: f.Elem.NamedType,
|
||||
Quant: f.Elem.Quant,
|
||||
TypeName: f.Elem.TypeName,
|
||||
Elem: f.Elem.Elem,
|
||||
FixedLen: f.Elem.FixedLen,
|
||||
}
|
||||
if err := writeCSharpSerializeField(b, elemField, indent+" ", enumNames); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(b, "%s }\n%s}\n", indent, indent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCSharpSerializePrimitive(
|
||||
b *strings.Builder,
|
||||
access string,
|
||||
f parser.Field,
|
||||
indent string,
|
||||
enumNames map[string]struct{},
|
||||
) error {
|
||||
if f.Quant != nil {
|
||||
return writeCSharpSerializeQuant(b, access, f, indent)
|
||||
}
|
||||
valueExpr := csharpSerializeValueExpr(access, f, enumNames)
|
||||
switch f.Primitive {
|
||||
case parser.KindFloat32:
|
||||
fmt.Fprintf(b, "%s*(float*)ptr = %s; ptr += 4;\n", indent, valueExpr)
|
||||
case parser.KindFloat64:
|
||||
fmt.Fprintf(b, "%s*(double*)ptr = %s; ptr += 8;\n", indent, valueExpr)
|
||||
case parser.KindInt8:
|
||||
fmt.Fprintf(b, "%s*(sbyte*)ptr = %s; ptr += 1;\n", indent, valueExpr)
|
||||
case parser.KindUint8:
|
||||
fmt.Fprintf(b, "%s*ptr = %s; ptr += 1;\n", indent, valueExpr)
|
||||
case parser.KindBool:
|
||||
fmt.Fprintf(b, "%s*ptr = (byte)(%s ? 1 : 0); ptr += 1;\n", indent, valueExpr)
|
||||
case parser.KindInt16:
|
||||
fmt.Fprintf(b, "%s*(short*)ptr = %s; ptr += 2;\n", indent, valueExpr)
|
||||
case parser.KindUint16:
|
||||
fmt.Fprintf(b, "%s*(ushort*)ptr = %s; ptr += 2;\n", indent, valueExpr)
|
||||
case parser.KindInt32:
|
||||
fmt.Fprintf(b, "%s*(int*)ptr = %s; ptr += 4;\n", indent, valueExpr)
|
||||
case parser.KindUint32:
|
||||
fmt.Fprintf(b, "%s*(uint*)ptr = %s; ptr += 4;\n", indent, valueExpr)
|
||||
case parser.KindInt64:
|
||||
fmt.Fprintf(b, "%s*(long*)ptr = %s; ptr += 8;\n", indent, valueExpr)
|
||||
case parser.KindUint64:
|
||||
fmt.Fprintf(b, "%s*(ulong*)ptr = %s; ptr += 8;\n", indent, valueExpr)
|
||||
case parser.KindString:
|
||||
// UTF-8: uint16 byteCount + bytes
|
||||
lenVar := "_slen" + sanitizeVarName(access)
|
||||
fmt.Fprintf(b, "%sint %s = %s != null ? Encoding.UTF8.GetByteCount(%s) : 0;\n",
|
||||
indent, lenVar, valueExpr, valueExpr)
|
||||
fmt.Fprintf(b, "%s*(ushort*)ptr = (ushort)%s; ptr += 2;\n", indent, lenVar)
|
||||
fmt.Fprintf(b, "%sif (%s != null && %s > 0)\n%s{\n", indent, valueExpr, lenVar, indent)
|
||||
fmt.Fprintf(b, "%s fixed (char* _chars%s = %s)\n%s {\n",
|
||||
indent, sanitizeVarName(access), valueExpr, indent)
|
||||
fmt.Fprintf(b, "%s Encoding.UTF8.GetBytes(_chars%s, %s.Length, ptr, %s);\n",
|
||||
indent, sanitizeVarName(access), valueExpr, lenVar)
|
||||
fmt.Fprintf(b, "%s }\n", indent)
|
||||
fmt.Fprintf(b, "%s}\n", indent)
|
||||
fmt.Fprintf(b, "%sptr += %s;\n", indent, lenVar)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCSharpSerializeQuant(b *strings.Builder, access string, f parser.Field, indent string) error {
|
||||
q := f.Quant
|
||||
maxUint := q.MaxUint()
|
||||
if q.Bits == 8 {
|
||||
fmt.Fprintf(b, "%s*ptr = (byte)((%s - (%gf)) / (%gf - (%gf)) * %gf); ptr += 1;\n",
|
||||
indent, access, q.Min, q.Max, q.Min, maxUint)
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s*(ushort*)ptr = (ushort)((%s - (%gf)) / (%gf - (%gf)) * %gf); ptr += 2;\n",
|
||||
indent, access, q.Min, q.Max, q.Min, maxUint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Deserialize ---
|
||||
|
||||
func writeCSharpDeserializeField(
|
||||
b *strings.Builder,
|
||||
recv string,
|
||||
f parser.Field,
|
||||
indent string,
|
||||
enumNames map[string]struct{},
|
||||
) error {
|
||||
access := recv + "." + f.Name
|
||||
switch f.Kind {
|
||||
case parser.KindPrimitive:
|
||||
return writeCSharpDeserializePrimitive(b, access, f, indent, enumNames)
|
||||
case parser.KindNested:
|
||||
fmt.Fprintf(b, "%sptr += %s.Deserialize(ptr, out %s);\n", indent, f.TypeName, access)
|
||||
case parser.KindFixedArray:
|
||||
iVar := "_i" + f.Name
|
||||
fmt.Fprintf(b, "%s%s = new %s[%d];\n", indent, access, csharpTypeName(*f.Elem, enumNames), f.FixedLen)
|
||||
fmt.Fprintf(b, "%sfor (int %s = 0; %s < %d; %s++)\n%s{\n",
|
||||
indent, iVar, iVar, f.FixedLen, iVar, indent)
|
||||
elemField := parser.Field{
|
||||
Name: f.Name + "[" + iVar + "]",
|
||||
Kind: f.Elem.Kind,
|
||||
Primitive: f.Elem.Primitive,
|
||||
NamedType: f.Elem.NamedType,
|
||||
Quant: f.Elem.Quant,
|
||||
TypeName: f.Elem.TypeName,
|
||||
Elem: f.Elem.Elem,
|
||||
FixedLen: f.Elem.FixedLen,
|
||||
}
|
||||
if err := writeCSharpDeserializeField(b, recv, elemField, indent+" ", enumNames); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(b, "%s}\n", indent)
|
||||
case parser.KindSlice:
|
||||
lenVar := "_len" + sanitizeVarName(f.Name)
|
||||
fmt.Fprintf(b, "%sint %s = *(ushort*)ptr; ptr += 2;\n", indent, lenVar)
|
||||
fmt.Fprintf(b, "%s%s = new %s[%s];\n", indent, access, csharpTypeName(*f.Elem, enumNames), lenVar)
|
||||
iVar := "_i" + sanitizeVarName(f.Name)
|
||||
fmt.Fprintf(b, "%sfor (int %s = 0; %s < %s; %s++)\n%s{\n",
|
||||
indent, iVar, iVar, lenVar, iVar, indent)
|
||||
elemField := parser.Field{
|
||||
Name: f.Name + "[" + iVar + "]",
|
||||
Kind: f.Elem.Kind,
|
||||
Primitive: f.Elem.Primitive,
|
||||
NamedType: f.Elem.NamedType,
|
||||
Quant: f.Elem.Quant,
|
||||
TypeName: f.Elem.TypeName,
|
||||
Elem: f.Elem.Elem,
|
||||
FixedLen: f.Elem.FixedLen,
|
||||
}
|
||||
if err := writeCSharpDeserializeField(b, recv, elemField, indent+" ", enumNames); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(b, "%s}\n", indent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCSharpDeserializePrimitive(
|
||||
b *strings.Builder,
|
||||
access string,
|
||||
f parser.Field,
|
||||
indent string,
|
||||
enumNames map[string]struct{},
|
||||
) error {
|
||||
if f.Quant != nil {
|
||||
return writeCSharpDeserializeQuant(b, access, f, indent)
|
||||
}
|
||||
switch f.Primitive {
|
||||
case parser.KindFloat32:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 4;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(float*)ptr", f, enumNames))
|
||||
case parser.KindFloat64:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 8;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(double*)ptr", f, enumNames))
|
||||
case parser.KindInt8:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 1;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(sbyte*)ptr", f, enumNames))
|
||||
case parser.KindUint8:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 1;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*ptr", f, enumNames))
|
||||
case parser.KindBool:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 1;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*ptr != 0", f, enumNames))
|
||||
case parser.KindInt16:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 2;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(short*)ptr", f, enumNames))
|
||||
case parser.KindUint16:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 2;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(ushort*)ptr", f, enumNames))
|
||||
case parser.KindInt32:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 4;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(int*)ptr", f, enumNames))
|
||||
case parser.KindUint32:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 4;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(uint*)ptr", f, enumNames))
|
||||
case parser.KindInt64:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 8;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(long*)ptr", f, enumNames))
|
||||
case parser.KindUint64:
|
||||
fmt.Fprintf(b, "%s%s = %s; ptr += 8;\n", indent, access,
|
||||
csharpDeserializeValueExpr("*(ulong*)ptr", f, enumNames))
|
||||
case parser.KindString:
|
||||
lenVar := "_slen" + sanitizeVarName(access)
|
||||
fmt.Fprintf(b, "%sint %s = *(ushort*)ptr; ptr += 2;\n", indent, lenVar)
|
||||
expr := fmt.Sprintf("%s > 0 ? Encoding.UTF8.GetString(ptr, %s) : string.Empty", lenVar, lenVar)
|
||||
fmt.Fprintf(b, "%s%s = %s;\n", indent, access, csharpDeserializeValueExpr(expr, f, enumNames))
|
||||
fmt.Fprintf(b, "%sptr += %s;\n", indent, lenVar)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCSharpDeserializeQuant(b *strings.Builder, access string, f parser.Field, indent string) error {
|
||||
q := f.Quant
|
||||
maxUint := q.MaxUint()
|
||||
if q.Bits == 8 {
|
||||
if f.Primitive == parser.KindFloat32 {
|
||||
fmt.Fprintf(b, "%s%s = (float)(*ptr) / %gf * (%gf - (%gf)) + (%gf); ptr += 1;\n",
|
||||
indent, access, maxUint, q.Max, q.Min, q.Min)
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s%s = (double)(*ptr) / %g * (%g - (%g)) + (%g); ptr += 1;\n",
|
||||
indent, access, maxUint, q.Max, q.Min, q.Min)
|
||||
}
|
||||
} else {
|
||||
if f.Primitive == parser.KindFloat32 {
|
||||
fmt.Fprintf(b, "%s%s = (float)(*(ushort*)ptr) / %gf * (%gf - (%gf)) + (%gf); ptr += 2;\n",
|
||||
indent, access, maxUint, q.Max, q.Min, q.Min)
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s%s = (double)(*(ushort*)ptr) / %g * (%g - (%g)) + (%g); ptr += 2;\n",
|
||||
indent, access, maxUint, q.Max, q.Min, q.Min)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func needsTextEncoding(messages []parser.Message) bool {
|
||||
for _, msg := range messages {
|
||||
for _, f := range msg.Fields {
|
||||
if fieldNeedsEncoding(f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fieldNeedsEncoding(f parser.Field) bool {
|
||||
switch f.Kind {
|
||||
case parser.KindPrimitive:
|
||||
return f.Primitive == parser.KindString
|
||||
case parser.KindFixedArray, parser.KindSlice:
|
||||
if f.Elem != nil {
|
||||
return fieldNeedsEncoding(*f.Elem)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func csharpTypeName(f parser.Field, enumNames map[string]struct{}) string {
|
||||
switch f.Kind {
|
||||
case parser.KindPrimitive:
|
||||
if csharpIsEnumType(f, enumNames) {
|
||||
return f.NamedType
|
||||
}
|
||||
return f.CSharpPrimitiveTypeName()
|
||||
case parser.KindNested:
|
||||
return f.TypeName
|
||||
case parser.KindFixedArray:
|
||||
return csharpTypeName(*f.Elem, enumNames) + "[]"
|
||||
case parser.KindSlice:
|
||||
return csharpTypeName(*f.Elem, enumNames) + "[]"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func csharpSerializeValueExpr(access string, f parser.Field, enumNames map[string]struct{}) string {
|
||||
if !csharpIsEnumType(f, enumNames) {
|
||||
return access
|
||||
}
|
||||
return "(" + f.CSharpPrimitiveTypeName() + ")" + access
|
||||
}
|
||||
|
||||
func csharpDeserializeValueExpr(expr string, f parser.Field, enumNames map[string]struct{}) string {
|
||||
if !csharpIsEnumType(f, enumNames) {
|
||||
return expr
|
||||
}
|
||||
return "(" + f.NamedType + ")(" + expr + ")"
|
||||
}
|
||||
|
||||
func csharpIsEnumType(f parser.Field, enumNames map[string]struct{}) bool {
|
||||
if f.NamedType == "" {
|
||||
return false
|
||||
}
|
||||
_, ok := enumNames[f.NamedType]
|
||||
return ok
|
||||
}
|
||||
|
||||
func csharpEnumBaseType(enum parser.Enum) string {
|
||||
field := parser.Field{Primitive: enum.Primitive}
|
||||
return field.CSharpPrimitiveTypeName()
|
||||
}
|
||||
|
||||
func csharpEnumValueName(enumName, valueName string) string {
|
||||
if !strings.HasPrefix(valueName, enumName) || len(valueName) == len(enumName) {
|
||||
return valueName
|
||||
}
|
||||
|
||||
suffix := valueName[len(enumName):]
|
||||
if suffix == "" {
|
||||
return valueName
|
||||
}
|
||||
if suffix[0] == '_' {
|
||||
suffix = suffix[1:]
|
||||
}
|
||||
if suffix == "" {
|
||||
return valueName
|
||||
}
|
||||
|
||||
first := suffix[0]
|
||||
if !((first >= 'A' && first <= 'Z') || (first >= '0' && first <= '9') || first == '_') {
|
||||
return valueName
|
||||
}
|
||||
|
||||
return suffix
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"edmand46/arpack/parser"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const samplePath = "../testdata/sample.go"
|
||||
|
||||
func TestGenerateGo_Compiles(t *testing.T) {
|
||||
msgs, err := parser.ParseFile(samplePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFile: %v", err)
|
||||
}
|
||||
|
||||
src, err := GenerateGo(msgs, "messages")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateGo: %v", err)
|
||||
}
|
||||
|
||||
if len(src) == 0 {
|
||||
t.Fatal("GenerateGo returned empty output")
|
||||
}
|
||||
|
||||
// Проверяем что содержит нужные функции
|
||||
code := string(src)
|
||||
for _, name := range []string{"Vector3", "MoveMessage", "SpawnMessage", "EnvelopeMessage"} {
|
||||
if !strings.Contains(code, "func (m *"+name+") Marshal") {
|
||||
t.Errorf("missing Marshal for %s", name)
|
||||
}
|
||||
if !strings.Contains(code, "func (m *"+name+") Unmarshal") {
|
||||
t.Errorf("missing Unmarshal for %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Generated Go (%d bytes):\n%s", len(src), code)
|
||||
}
|
||||
|
||||
func TestGenerateGo_RoundTrip(t *testing.T) {
|
||||
msgs, err := parser.ParseFile(samplePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFile: %v", err)
|
||||
}
|
||||
|
||||
src, err := GenerateGo(msgs, "messages")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateGo: %v", err)
|
||||
}
|
||||
|
||||
// Записываем в temp dir вместе с оригинальными структурами и тестом round-trip
|
||||
dir := t.TempDir()
|
||||
|
||||
// Копируем testdata/sample.go (определения структур)
|
||||
sampleSrc, err := os.ReadFile(samplePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile sample: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "messages.go"), sampleSrc, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Сохраняем сгенерированный код
|
||||
if err := os.WriteFile(filepath.Join(dir, "messages_arpack.go"), src, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Пишем round-trip тест
|
||||
roundTrip := `package messages
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRoundTrip_Vector3(t *testing.T) {
|
||||
orig := Vector3{X: 123.45, Y: -200.0, Z: 0.0}
|
||||
buf := orig.Marshal(nil)
|
||||
var got Vector3
|
||||
n, err := got.Unmarshal(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if n != len(buf) {
|
||||
t.Errorf("consumed %d bytes, want %d", n, len(buf))
|
||||
}
|
||||
// Квантизация 16-бит даёт точность ≈ 1000/65535 ≈ 0.015
|
||||
const eps = float32(0.02)
|
||||
if math.Abs(float64(got.X-orig.X)) > float64(eps) {
|
||||
t.Errorf("X: got %v, want %v", got.X, orig.X)
|
||||
}
|
||||
if math.Abs(float64(got.Y-orig.Y)) > float64(eps) {
|
||||
t.Errorf("Y: got %v, want %v", got.Y, orig.Y)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip_SpawnMessage(t *testing.T) {
|
||||
orig := SpawnMessage{
|
||||
EntityID: 42,
|
||||
Position: Vector3{X: 10, Y: 20, Z: 30},
|
||||
Health: -100,
|
||||
Tags: []string{"hero", "player"},
|
||||
Data: []uint8{1, 2, 3},
|
||||
}
|
||||
buf := orig.Marshal(nil)
|
||||
var got SpawnMessage
|
||||
_, err := got.Unmarshal(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if got.EntityID != orig.EntityID {
|
||||
t.Errorf("EntityID: got %d, want %d", got.EntityID, orig.EntityID)
|
||||
}
|
||||
if got.Health != orig.Health {
|
||||
t.Errorf("Health: got %d, want %d", got.Health, orig.Health)
|
||||
}
|
||||
if len(got.Tags) != len(orig.Tags) {
|
||||
t.Fatalf("Tags len: got %d, want %d", len(got.Tags), len(orig.Tags))
|
||||
}
|
||||
for i, tag := range orig.Tags {
|
||||
if got.Tags[i] != tag {
|
||||
t.Errorf("Tags[%d]: got %q, want %q", i, got.Tags[i], tag)
|
||||
}
|
||||
}
|
||||
if len(got.Data) != len(orig.Data) {
|
||||
t.Fatalf("Data len: got %d, want %d", len(got.Data), len(orig.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip_MoveMessage(t *testing.T) {
|
||||
orig := MoveMessage{
|
||||
Position: Vector3{X: 100, Y: -50, Z: 0},
|
||||
Velocity: [3]float32{1.5, -2.5, 0},
|
||||
Waypoints: []Vector3{{X: 10, Y: 20, Z: 0}, {X: -10, Y: 0, Z: 100}},
|
||||
PlayerID: 999,
|
||||
Active: true,
|
||||
Visible: false,
|
||||
Ghost: true,
|
||||
Name: "Alice",
|
||||
}
|
||||
buf := orig.Marshal(nil)
|
||||
var got MoveMessage
|
||||
_, err := got.Unmarshal(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if got.PlayerID != orig.PlayerID {
|
||||
t.Errorf("PlayerID: got %d, want %d", got.PlayerID, orig.PlayerID)
|
||||
}
|
||||
if got.Name != orig.Name {
|
||||
t.Errorf("Name: got %q, want %q", got.Name, orig.Name)
|
||||
}
|
||||
if got.Active != orig.Active {
|
||||
t.Errorf("Active: got %v, want %v", got.Active, orig.Active)
|
||||
}
|
||||
if got.Visible != orig.Visible {
|
||||
t.Errorf("Visible: got %v, want %v", got.Visible, orig.Visible)
|
||||
}
|
||||
if got.Ghost != orig.Ghost {
|
||||
t.Errorf("Ghost: got %v, want %v", got.Ghost, orig.Ghost)
|
||||
}
|
||||
if len(got.Waypoints) != len(orig.Waypoints) {
|
||||
t.Fatalf("Waypoints len: got %d, want %d", len(got.Waypoints), len(orig.Waypoints))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip_EnvelopeMessage(t *testing.T) {
|
||||
orig := EnvelopeMessage{
|
||||
Code: OpcodeJoinRoom,
|
||||
Counter: 7,
|
||||
}
|
||||
buf := orig.Marshal(nil)
|
||||
var got EnvelopeMessage
|
||||
_, err := got.Unmarshal(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if got.Code != orig.Code {
|
||||
t.Errorf("Code: got %v, want %v", got.Code, orig.Code)
|
||||
}
|
||||
if got.Counter != orig.Counter {
|
||||
t.Errorf("Counter: got %d, want %d", got.Counter, orig.Counter)
|
||||
}
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "roundtrip_test.go"), []byte(roundTrip), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// go.mod для temp пакета
|
||||
goMod := "module messages\n\ngo 1.21\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Запускаем go test
|
||||
cmd := exec.Command("go", "test", "./...")
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("go test failed:\n%s", out)
|
||||
}
|
||||
t.Logf("go test output:\n%s", out)
|
||||
}
|
||||
|
||||
func TestGenerateCSharp_Output(t *testing.T) {
|
||||
schema, err := parser.ParseSchemaFile(samplePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSchemaFile: %v", err)
|
||||
}
|
||||
|
||||
src, err := GenerateCSharpSchema(schema, "Ragono.Messages")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCSharpSchema: %v", err)
|
||||
}
|
||||
|
||||
code := string(src)
|
||||
|
||||
// Проверяем структуру выходного кода
|
||||
for _, name := range []string{"Vector3", "MoveMessage", "SpawnMessage", "EnvelopeMessage"} {
|
||||
if !strings.Contains(code, "public unsafe struct "+name) {
|
||||
t.Errorf("missing struct %s", name)
|
||||
}
|
||||
if !strings.Contains(code, "public int Serialize") {
|
||||
t.Errorf("missing Serialize in %s", name)
|
||||
}
|
||||
if !strings.Contains(code, "public static int Deserialize") {
|
||||
t.Errorf("missing Deserialize in %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Unsafe паттерны
|
||||
if !strings.Contains(code, "*(ushort*)ptr") {
|
||||
t.Error("missing unsafe ushort pointer cast")
|
||||
}
|
||||
if !strings.Contains(code, "byte* ptr = buffer") {
|
||||
t.Error("missing byte* ptr pattern")
|
||||
}
|
||||
|
||||
// Нет BinaryWriter
|
||||
if strings.Contains(code, "BinaryWriter") || strings.Contains(code, "BinaryReader") {
|
||||
t.Error("should not contain BinaryWriter/BinaryReader")
|
||||
}
|
||||
if !strings.Contains(code, "public enum Opcode : ushort") {
|
||||
t.Error("missing Opcode enum")
|
||||
}
|
||||
if !strings.Contains(code, "Authorize = 1") || !strings.Contains(code, "JoinRoom = 2") {
|
||||
t.Error("missing enum values for Opcode")
|
||||
}
|
||||
if !strings.Contains(code, "public Opcode Code;") {
|
||||
t.Error("EnvelopeMessage.Code should use generated enum type")
|
||||
}
|
||||
|
||||
t.Logf("Generated C# (%d bytes):\n%s", len(src), code)
|
||||
}
|
||||
|
||||
func TestBoolPacking_GoCode(t *testing.T) {
|
||||
msgs, err := parser.ParseFile(samplePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFile: %v", err)
|
||||
}
|
||||
|
||||
src, err := GenerateGo(msgs, "messages")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateGo: %v", err)
|
||||
}
|
||||
code := string(src)
|
||||
|
||||
// Должны быть битовые операции
|
||||
if !strings.Contains(code, "_boolByte") {
|
||||
t.Error("missing bool bit-packing variable (_boolByte)")
|
||||
}
|
||||
if !strings.Contains(code, "|= 1 <<") {
|
||||
t.Error("missing bit OR operation for bool packing")
|
||||
}
|
||||
if !strings.Contains(code, "&(1<<") {
|
||||
t.Error("missing bit AND operation for bool unpacking")
|
||||
}
|
||||
|
||||
// НЕ должно быть per-byte записи для bool-полей из MoveMessage
|
||||
// (Active, Visible, Ghost упакованы в 1 байт)
|
||||
if strings.Contains(code, "append(buf, 1)") {
|
||||
t.Error("should not emit per-byte bool encoding for packed bools")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolPacking_WireSize(t *testing.T) {
|
||||
src := `package p
|
||||
type Flags struct {
|
||||
A bool
|
||||
B bool
|
||||
C bool
|
||||
D bool
|
||||
E bool
|
||||
F bool
|
||||
G bool
|
||||
H bool
|
||||
I bool
|
||||
}
|
||||
`
|
||||
msgs, err := parser.ParseSource(src)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
// 9 bool: первые 8 → 1 байт, последний → 1 байт = 2 байта
|
||||
got := packedMinWireSize(msgs[0].Fields)
|
||||
if got != 2 {
|
||||
t.Errorf("packedMinWireSize(9 consecutive bools): got %d, want 2", got)
|
||||
}
|
||||
|
||||
src3 := `package p
|
||||
type Flags3 struct {
|
||||
A bool
|
||||
B bool
|
||||
C bool
|
||||
}
|
||||
`
|
||||
msgs3, err := parser.ParseSource(src3)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
// 3 bool → 1 байт
|
||||
got3 := packedMinWireSize(msgs3[0].Fields)
|
||||
if got3 != 1 {
|
||||
t.Errorf("packedMinWireSize(3 consecutive bools): got %d, want 1", got3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolPacking_SegmentFields(t *testing.T) {
|
||||
src := `package p
|
||||
type Msg struct {
|
||||
A bool
|
||||
B bool
|
||||
ID uint32
|
||||
C bool
|
||||
}
|
||||
`
|
||||
msgs, err := parser.ParseSource(src)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
segs := segmentFields(msgs[0].Fields)
|
||||
// Ожидаем: [bool-group(A,B)] [single(ID)] [bool-group(C)]
|
||||
if len(segs) != 3 {
|
||||
t.Fatalf("expected 3 segments, got %d", len(segs))
|
||||
}
|
||||
if len(segs[0].bools) != 2 {
|
||||
t.Errorf("seg[0]: expected 2 bools, got %d", len(segs[0].bools))
|
||||
}
|
||||
if segs[1].single == nil || segs[1].single.Name != "ID" {
|
||||
t.Errorf("seg[1]: expected single field ID")
|
||||
}
|
||||
if len(segs[2].bools) != 1 {
|
||||
t.Errorf("seg[2]: expected 1 bool, got %d", len(segs[2].bools))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolPacking_CSharpCode(t *testing.T) {
|
||||
schema, err := parser.ParseSchemaFile(samplePath)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSchemaFile: %v", err)
|
||||
}
|
||||
|
||||
src, err := GenerateCSharpSchema(schema, "Ragono.Messages")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCSharpSchema: %v", err)
|
||||
}
|
||||
code := string(src)
|
||||
|
||||
if !strings.Contains(code, "_boolByte") {
|
||||
t.Error("C#: missing bool bit-packing variable")
|
||||
}
|
||||
if !strings.Contains(code, "|= (byte)(1 <<") {
|
||||
t.Error("C#: missing bit OR for bool packing")
|
||||
}
|
||||
if !strings.Contains(code, "& (1 <<") {
|
||||
t.Error("C#: missing bit AND for bool unpacking")
|
||||
}
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"edmand46/arpack/parser"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateGo генерирует Go-код сериализации для списка сообщений.
|
||||
// pkgName — имя пакета в котором будет сгенерированный файл.
|
||||
func GenerateGo(messages []parser.Message, pkgName string) ([]byte, error) {
|
||||
return GenerateGoSchema(parser.Schema{Messages: messages}, pkgName)
|
||||
}
|
||||
|
||||
// GenerateGoSchema генерирует Go-код сериализации для полной схемы.
|
||||
func GenerateGoSchema(schema parser.Schema, pkgName string) ([]byte, error) {
|
||||
messages := schema.Messages
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("// Code generated by arpack. DO NOT EDIT.\n\n")
|
||||
fmt.Fprintf(&b, "package %s\n\n", pkgName)
|
||||
|
||||
b.WriteString("import (\n")
|
||||
b.WriteString("\t\"encoding/binary\"\n")
|
||||
b.WriteString("\t\"errors\"\n")
|
||||
if needsMathImport(messages) {
|
||||
b.WriteString("\t\"math\"\n")
|
||||
}
|
||||
b.WriteString(")\n\n")
|
||||
|
||||
for _, msg := range messages {
|
||||
if err := writeGoMessage(&b, msg); err != nil {
|
||||
return nil, fmt.Errorf("message %s: %w", msg.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
src := b.String()
|
||||
formatted, err := format.Source([]byte(src))
|
||||
if err != nil {
|
||||
return []byte(src), fmt.Errorf("go/format: %w\n\nSource:\n%s", err, src)
|
||||
}
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
func writeGoMessage(b *strings.Builder, msg parser.Message) error {
|
||||
segs := segmentFields(msg.Fields)
|
||||
|
||||
// Marshal
|
||||
fmt.Fprintf(b, "func (m *%s) Marshal(buf []byte) []byte {\n", msg.Name)
|
||||
for i, seg := range segs {
|
||||
if seg.single != nil {
|
||||
if err := writeGoMarshalField(b, "m", *seg.single, "\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
writeGoBoolGroupMarshal(b, "m", seg.bools, i, "\t")
|
||||
}
|
||||
}
|
||||
b.WriteString("\treturn buf\n}\n\n")
|
||||
|
||||
// Unmarshal — возвращает кол-во потреблённых байт
|
||||
fmt.Fprintf(b, "func (m *%s) Unmarshal(data []byte) (int, error) {\n", msg.Name)
|
||||
minSize := packedMinWireSize(msg.Fields)
|
||||
fmt.Fprintf(b, "\tif len(data) < %d {\n", minSize)
|
||||
fmt.Fprintf(b, "\t\treturn 0, errors.New(\"arpack: buffer too short for %s\")\n", msg.Name)
|
||||
b.WriteString("\t}\n")
|
||||
b.WriteString("\toffset := 0\n")
|
||||
for i, seg := range segs {
|
||||
if seg.single != nil {
|
||||
if err := writeGoUnmarshalField(b, "m", *seg.single, "\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
writeGoBoolGroupUnmarshal(b, "m", seg.bools, i, "\t")
|
||||
}
|
||||
}
|
||||
b.WriteString("\treturn offset, nil\n}\n\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeGoBoolGroupMarshal(b *strings.Builder, recv string, bools []parser.Field, groupIdx int, indent string) {
|
||||
varName := fmt.Sprintf("_boolByte%d", groupIdx)
|
||||
fmt.Fprintf(b, "%svar %s uint8\n", indent, varName)
|
||||
for bit, f := range bools {
|
||||
fmt.Fprintf(b, "%sif %s.%s { %s |= 1 << %d }\n", indent, recv, f.Name, varName, bit)
|
||||
}
|
||||
fmt.Fprintf(b, "%sbuf = append(buf, %s)\n", indent, varName)
|
||||
}
|
||||
|
||||
func writeGoBoolGroupUnmarshal(b *strings.Builder, recv string, bools []parser.Field, groupIdx int, indent string) {
|
||||
varName := fmt.Sprintf("_boolByte%d", groupIdx)
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+1 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s := data[offset]; offset++\n", indent, varName)
|
||||
for bit, f := range bools {
|
||||
expr := fmt.Sprintf("%s&(1<<%d) != 0", varName, bit)
|
||||
fmt.Fprintf(b, "%s%s.%s = %s\n", indent, recv, f.Name, goUnmarshalValueExpr(expr, f))
|
||||
}
|
||||
}
|
||||
|
||||
// --- Marshal ---
|
||||
|
||||
func writeGoMarshalField(b *strings.Builder, recv string, f parser.Field, indent string) error {
|
||||
access := recv + "." + f.Name
|
||||
switch f.Kind {
|
||||
case parser.KindPrimitive:
|
||||
return writeGoMarshalPrimitive(b, access, f, indent)
|
||||
case parser.KindNested:
|
||||
fmt.Fprintf(b, "%sbuf = %s.Marshal(buf)\n", indent, access)
|
||||
case parser.KindFixedArray:
|
||||
fmt.Fprintf(b, "%sfor _i%s := 0; _i%s < %d; _i%s++ {\n",
|
||||
indent, f.Name, f.Name, f.FixedLen, f.Name)
|
||||
elemField := parser.Field{
|
||||
Name: f.Name + "[_i" + f.Name + "]",
|
||||
Kind: f.Elem.Kind,
|
||||
Primitive: f.Elem.Primitive,
|
||||
NamedType: f.Elem.NamedType,
|
||||
Quant: f.Elem.Quant,
|
||||
TypeName: f.Elem.TypeName,
|
||||
Elem: f.Elem.Elem,
|
||||
FixedLen: f.Elem.FixedLen,
|
||||
}
|
||||
if err := writeGoMarshalField(b, recv, elemField, indent+"\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(b, "%s}\n", indent)
|
||||
case parser.KindSlice:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint16(buf, uint16(len(%s)))\n", indent, access)
|
||||
fmt.Fprintf(b, "%sfor _i%s := range %s {\n", indent, f.Name, access)
|
||||
elemField := parser.Field{
|
||||
Name: f.Name + "[_i" + f.Name + "]",
|
||||
Kind: f.Elem.Kind,
|
||||
Primitive: f.Elem.Primitive,
|
||||
NamedType: f.Elem.NamedType,
|
||||
Quant: f.Elem.Quant,
|
||||
TypeName: f.Elem.TypeName,
|
||||
Elem: f.Elem.Elem,
|
||||
FixedLen: f.Elem.FixedLen,
|
||||
}
|
||||
if err := writeGoMarshalField(b, recv, elemField, indent+"\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(b, "%s}\n", indent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeGoMarshalPrimitive(b *strings.Builder, access string, f parser.Field, indent string) error {
|
||||
if f.Quant != nil {
|
||||
return writeGoMarshalQuant(b, access, f, indent)
|
||||
}
|
||||
valueExpr := goMarshalValueExpr(access, f)
|
||||
switch f.Primitive {
|
||||
case parser.KindFloat32:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(%s))\n", indent, valueExpr)
|
||||
case parser.KindFloat64:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(%s))\n", indent, valueExpr)
|
||||
case parser.KindInt8:
|
||||
fmt.Fprintf(b, "%sbuf = append(buf, uint8(%s))\n", indent, valueExpr)
|
||||
case parser.KindUint8:
|
||||
fmt.Fprintf(b, "%sbuf = append(buf, %s)\n", indent, valueExpr)
|
||||
case parser.KindBool:
|
||||
fmt.Fprintf(b, "%sif %s { buf = append(buf, 1) } else { buf = append(buf, 0) }\n", indent, valueExpr)
|
||||
case parser.KindInt16:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint16(buf, uint16(%s))\n", indent, valueExpr)
|
||||
case parser.KindUint16:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint16(buf, %s)\n", indent, valueExpr)
|
||||
case parser.KindInt32:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint32(buf, uint32(%s))\n", indent, valueExpr)
|
||||
case parser.KindUint32:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint32(buf, %s)\n", indent, valueExpr)
|
||||
case parser.KindInt64:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint64(buf, uint64(%s))\n", indent, valueExpr)
|
||||
case parser.KindUint64:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint64(buf, %s)\n", indent, valueExpr)
|
||||
case parser.KindString:
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint16(buf, uint16(len(%s)))\n", indent, valueExpr)
|
||||
fmt.Fprintf(b, "%sbuf = append(buf, %s...)\n", indent, valueExpr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeGoMarshalQuant(b *strings.Builder, access string, f parser.Field, indent string) error {
|
||||
q := f.Quant
|
||||
varName := "_q" + sanitizeVarName(access)
|
||||
valueExpr := goMarshalValueExpr(access, f)
|
||||
if q.Bits == 8 {
|
||||
fmt.Fprintf(b, "%s%s := uint8((%s - (%g)) / (%g - (%g)) * %g)\n",
|
||||
indent, varName, valueExpr, q.Min, q.Max, q.Min, q.MaxUint())
|
||||
fmt.Fprintf(b, "%sbuf = append(buf, %s)\n", indent, varName)
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s%s := uint16((%s - (%g)) / (%g - (%g)) * %g)\n",
|
||||
indent, varName, valueExpr, q.Min, q.Max, q.Min, q.MaxUint())
|
||||
fmt.Fprintf(b, "%sbuf = binary.LittleEndian.AppendUint16(buf, %s)\n", indent, varName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Unmarshal ---
|
||||
|
||||
func writeGoUnmarshalField(b *strings.Builder, recv string, f parser.Field, indent string) error {
|
||||
access := recv + "." + f.Name
|
||||
switch f.Kind {
|
||||
case parser.KindPrimitive:
|
||||
return writeGoUnmarshalPrimitive(b, access, f, indent)
|
||||
|
||||
case parser.KindNested:
|
||||
nVar := "_n" + sanitizeVarName(f.Name)
|
||||
fmt.Fprintf(b, "%s%s, _err := %s.Unmarshal(data[offset:])\n", indent, nVar, access)
|
||||
fmt.Fprintf(b, "%sif _err != nil { return 0, _err }\n", indent)
|
||||
fmt.Fprintf(b, "%soffset += %s\n", indent, nVar)
|
||||
|
||||
case parser.KindFixedArray:
|
||||
iVar := "_i" + f.Name
|
||||
fmt.Fprintf(b, "%sfor %s := 0; %s < %d; %s++ {\n", indent, iVar, iVar, f.FixedLen, iVar)
|
||||
elemField := parser.Field{
|
||||
Name: f.Name + "[" + iVar + "]",
|
||||
Kind: f.Elem.Kind,
|
||||
Primitive: f.Elem.Primitive,
|
||||
NamedType: f.Elem.NamedType,
|
||||
Quant: f.Elem.Quant,
|
||||
TypeName: f.Elem.TypeName,
|
||||
Elem: f.Elem.Elem,
|
||||
FixedLen: f.Elem.FixedLen,
|
||||
}
|
||||
if err := writeGoUnmarshalField(b, recv, elemField, indent+"\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(b, "%s}\n", indent)
|
||||
|
||||
case parser.KindSlice:
|
||||
lenVar := "_len" + sanitizeVarName(f.Name)
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+2 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s := int(binary.LittleEndian.Uint16(data[offset:]))\n", indent, lenVar)
|
||||
fmt.Fprintf(b, "%soffset += 2\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = make(%s, %s)\n", indent, access, f.GoTypeName(), lenVar)
|
||||
iVar := "_i" + sanitizeVarName(f.Name)
|
||||
fmt.Fprintf(b, "%sfor %s := 0; %s < %s; %s++ {\n", indent, iVar, iVar, lenVar, iVar)
|
||||
elemField := parser.Field{
|
||||
Name: f.Name + "[" + iVar + "]",
|
||||
Kind: f.Elem.Kind,
|
||||
Primitive: f.Elem.Primitive,
|
||||
NamedType: f.Elem.NamedType,
|
||||
Quant: f.Elem.Quant,
|
||||
TypeName: f.Elem.TypeName,
|
||||
Elem: f.Elem.Elem,
|
||||
FixedLen: f.Elem.FixedLen,
|
||||
}
|
||||
if err := writeGoUnmarshalField(b, recv, elemField, indent+"\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(b, "%s}\n", indent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeGoUnmarshalPrimitive(b *strings.Builder, access string, f parser.Field, indent string) error {
|
||||
if f.Quant != nil {
|
||||
return writeGoUnmarshalQuant(b, access, f, indent)
|
||||
}
|
||||
switch f.Primitive {
|
||||
case parser.KindFloat32:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+4 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("math.Float32frombits(binary.LittleEndian.Uint32(data[offset:]))", f))
|
||||
fmt.Fprintf(b, "%soffset += 4\n", indent)
|
||||
case parser.KindFloat64:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+8 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("math.Float64frombits(binary.LittleEndian.Uint64(data[offset:]))", f))
|
||||
fmt.Fprintf(b, "%soffset += 8\n", indent)
|
||||
case parser.KindInt8:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+1 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("int8(data[offset])", f))
|
||||
fmt.Fprintf(b, "%soffset += 1\n", indent)
|
||||
case parser.KindUint8:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+1 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("data[offset]", f))
|
||||
fmt.Fprintf(b, "%soffset += 1\n", indent)
|
||||
case parser.KindBool:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+1 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("data[offset] != 0", f))
|
||||
fmt.Fprintf(b, "%soffset += 1\n", indent)
|
||||
case parser.KindInt16:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+2 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("int16(binary.LittleEndian.Uint16(data[offset:]))", f))
|
||||
fmt.Fprintf(b, "%soffset += 2\n", indent)
|
||||
case parser.KindUint16:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+2 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("binary.LittleEndian.Uint16(data[offset:])", f))
|
||||
fmt.Fprintf(b, "%soffset += 2\n", indent)
|
||||
case parser.KindInt32:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+4 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("int32(binary.LittleEndian.Uint32(data[offset:]))", f))
|
||||
fmt.Fprintf(b, "%soffset += 4\n", indent)
|
||||
case parser.KindUint32:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+4 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("binary.LittleEndian.Uint32(data[offset:])", f))
|
||||
fmt.Fprintf(b, "%soffset += 4\n", indent)
|
||||
case parser.KindInt64:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+8 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("int64(binary.LittleEndian.Uint64(data[offset:]))", f))
|
||||
fmt.Fprintf(b, "%soffset += 8\n", indent)
|
||||
case parser.KindUint64:
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+8 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("binary.LittleEndian.Uint64(data[offset:])", f))
|
||||
fmt.Fprintf(b, "%soffset += 8\n", indent)
|
||||
case parser.KindString:
|
||||
lenVar := "_slen" + sanitizeVarName(access)
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+2 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s := int(binary.LittleEndian.Uint16(data[offset:]))\n", indent, lenVar)
|
||||
fmt.Fprintf(b, "%soffset += 2\n", indent)
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+%s { return 0, errors.New(\"arpack: buffer too short\") }\n", indent, lenVar)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr("string(data[offset : offset+"+lenVar+"])",
|
||||
f))
|
||||
fmt.Fprintf(b, "%soffset += %s\n", indent, lenVar)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeGoUnmarshalQuant(b *strings.Builder, access string, f parser.Field, indent string) error {
|
||||
q := f.Quant
|
||||
varName := "_q" + sanitizeVarName(access)
|
||||
maxUint := q.MaxUint()
|
||||
if q.Bits == 8 {
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+1 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s := data[offset]\n", indent, varName)
|
||||
fmt.Fprintf(b, "%soffset += 1\n", indent)
|
||||
if f.Primitive == parser.KindFloat32 {
|
||||
expr := fmt.Sprintf("float32(%s) / %g * (%g - (%g)) + (%g)", varName, maxUint, q.Max, q.Min, q.Min)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr(expr, f))
|
||||
} else {
|
||||
expr := fmt.Sprintf("float64(%s) / %g * (%g - (%g)) + (%g)", varName, maxUint, q.Max, q.Min, q.Min)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr(expr, f))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(b, "%sif len(data) < offset+2 { return 0, errors.New(\"arpack: buffer too short\") }\n", indent)
|
||||
fmt.Fprintf(b, "%s%s := binary.LittleEndian.Uint16(data[offset:])\n", indent, varName)
|
||||
fmt.Fprintf(b, "%soffset += 2\n", indent)
|
||||
if f.Primitive == parser.KindFloat32 {
|
||||
expr := fmt.Sprintf("float32(%s) / %g * (%g - (%g)) + (%g)", varName, maxUint, q.Max, q.Min, q.Min)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr(expr, f))
|
||||
} else {
|
||||
expr := fmt.Sprintf("float64(%s) / %g * (%g - (%g)) + (%g)", varName, maxUint, q.Max, q.Min, q.Min)
|
||||
fmt.Fprintf(b, "%s%s = %s\n", indent, access, goUnmarshalValueExpr(expr, f))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func goMarshalValueExpr(access string, f parser.Field) string {
|
||||
if f.NamedType == "" {
|
||||
return access
|
||||
}
|
||||
return f.GoPrimitiveTypeName() + "(" + access + ")"
|
||||
}
|
||||
|
||||
func goUnmarshalValueExpr(expr string, f parser.Field) string {
|
||||
if f.NamedType == "" {
|
||||
return expr
|
||||
}
|
||||
return f.NamedType + "(" + expr + ")"
|
||||
}
|
||||
|
||||
// sanitizeVarName превращает "m.Pos[_i]" в "_mPos_i".
|
||||
func sanitizeVarName(s string) string {
|
||||
var b strings.Builder
|
||||
for _, c := range s {
|
||||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') {
|
||||
b.WriteRune(c)
|
||||
} else {
|
||||
b.WriteRune('_')
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func needsMathImport(messages []parser.Message) bool {
|
||||
for _, msg := range messages {
|
||||
for _, f := range msg.Fields {
|
||||
if needsMathField(f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func needsMathField(f parser.Field) bool {
|
||||
switch f.Kind {
|
||||
case parser.KindPrimitive:
|
||||
return f.Quant == nil && (f.Primitive == parser.KindFloat32 || f.Primitive == parser.KindFloat64)
|
||||
case parser.KindFixedArray, parser.KindSlice:
|
||||
if f.Elem != nil {
|
||||
return needsMathField(*f.Elem)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package generator
|
||||
|
||||
import "edmand46/arpack/parser"
|
||||
|
||||
// segment — либо группа bool (1–8 полей → 1 байт), либо одиночное поле.
|
||||
type segment struct {
|
||||
bools []parser.Field // non-empty: bool-группа
|
||||
single *parser.Field // non-nil: любое не-bool поле
|
||||
}
|
||||
|
||||
// isBoolField возвращает true если поле — нативный bool (не массив, не слайс).
|
||||
func isBoolField(f parser.Field) bool {
|
||||
return f.Kind == parser.KindPrimitive && f.Primitive == parser.KindBool
|
||||
}
|
||||
|
||||
// segmentFields разбивает поля структуры на сегменты.
|
||||
// Последовательные bool-поля группируются по 8 в один сегмент.
|
||||
func segmentFields(fields []parser.Field) []segment {
|
||||
var segs []segment
|
||||
i := 0
|
||||
for i < len(fields) {
|
||||
if !isBoolField(fields[i]) {
|
||||
f := fields[i]
|
||||
segs = append(segs, segment{single: &f})
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// Собираем последовательные bool-поля группами по 8
|
||||
for i < len(fields) && isBoolField(fields[i]) {
|
||||
var group []parser.Field
|
||||
for i < len(fields) && isBoolField(fields[i]) && len(group) < 8 {
|
||||
group = append(group, fields[i])
|
||||
i++
|
||||
}
|
||||
segs = append(segs, segment{bools: group})
|
||||
}
|
||||
}
|
||||
return segs
|
||||
}
|
||||
|
||||
// packedMinWireSize вычисляет минимальный размер буфера с учётом упаковки bool.
|
||||
func packedMinWireSize(fields []parser.Field) int {
|
||||
total := 0
|
||||
for _, seg := range segmentFields(fields) {
|
||||
if seg.single != nil {
|
||||
s := seg.single.WireSize()
|
||||
if s == -1 {
|
||||
total += 2
|
||||
} else {
|
||||
total += s
|
||||
}
|
||||
} else {
|
||||
// Группа bool → 1 байт
|
||||
total += 1
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 341 KiB |
@@ -0,0 +1,414 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
goparser "go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseFile парсит Go-файл и возвращает список сообщений.
|
||||
func ParseFile(path string) ([]Message, error) {
|
||||
schema, err := ParseSchemaFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return schema.Messages, nil
|
||||
}
|
||||
|
||||
// ParseSource парсит исходный код из строки (удобно для тестов).
|
||||
func ParseSource(src string) ([]Message, error) {
|
||||
schema, err := ParseSchemaSource(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return schema.Messages, nil
|
||||
}
|
||||
|
||||
// ParseSchemaFile парсит файл и возвращает полную схему: сообщения и enum-ы.
|
||||
func ParseSchemaFile(path string) (Schema, error) {
|
||||
fset := token.NewFileSet()
|
||||
|
||||
f, err := goparser.ParseFile(fset, path, nil, 0)
|
||||
if err != nil {
|
||||
return Schema{}, fmt.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
return parseASTFile(fset, f)
|
||||
}
|
||||
|
||||
// ParseSchemaSource парсит исходник и возвращает полную схему.
|
||||
func ParseSchemaSource(src string) (Schema, error) {
|
||||
fset := token.NewFileSet()
|
||||
|
||||
f, err := goparser.ParseFile(fset, "source.go", src, 0)
|
||||
if err != nil {
|
||||
return Schema{}, fmt.Errorf("parse source: %w", err)
|
||||
}
|
||||
|
||||
return parseASTFile(fset, f)
|
||||
}
|
||||
|
||||
func parseASTFile(fset *token.FileSet, f *ast.File) (Schema, error) {
|
||||
pkgName := f.Name.Name
|
||||
|
||||
knownStructs := map[string]bool{}
|
||||
namedPrimitives := map[string]PrimitiveKind{}
|
||||
var enumOrder []string
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok || genDecl.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch t := typeSpec.Type.(type) {
|
||||
case *ast.StructType:
|
||||
knownStructs[typeSpec.Name.Name] = true
|
||||
case *ast.Ident:
|
||||
primKind, isPrimitive := goPrimitiveKind(t.Name)
|
||||
if !isPrimitive {
|
||||
continue
|
||||
}
|
||||
namedPrimitives[typeSpec.Name.Name] = primKind
|
||||
if IsIntegralPrimitive(primKind) {
|
||||
enumOrder = append(enumOrder, typeSpec.Name.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info, err := typeCheckFile(fset, f)
|
||||
if err != nil {
|
||||
return Schema{}, err
|
||||
}
|
||||
|
||||
schema := Schema{PackageName: pkgName}
|
||||
enumIndex := make(map[string]int, len(enumOrder))
|
||||
for _, name := range enumOrder {
|
||||
enumIndex[name] = len(schema.Enums)
|
||||
schema.Enums = append(schema.Enums, Enum{
|
||||
Name: name,
|
||||
Primitive: namedPrimitives[name],
|
||||
})
|
||||
}
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch genDecl.Tok {
|
||||
case token.TYPE:
|
||||
for _, spec := range genDecl.Specs {
|
||||
typeSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
msg, err := parseStruct(pkgName, typeSpec.Name.Name, structType, knownStructs, namedPrimitives)
|
||||
if err != nil {
|
||||
return Schema{}, fmt.Errorf("struct %s: %w", typeSpec.Name.Name, err)
|
||||
}
|
||||
|
||||
schema.Messages = append(schema.Messages, msg)
|
||||
}
|
||||
case token.CONST:
|
||||
if err := parseConstDecls(genDecl, info, enumIndex, &schema); err != nil {
|
||||
return Schema{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func typeCheckFile(fset *token.FileSet, f *ast.File) (*types.Info, error) {
|
||||
info := &types.Info{
|
||||
Defs: make(map[*ast.Ident]types.Object),
|
||||
}
|
||||
|
||||
cfg := &types.Config{
|
||||
Importer: importer.Default(),
|
||||
}
|
||||
|
||||
if _, err := cfg.Check(f.Name.Name, fset, []*ast.File{f}, info); err != nil {
|
||||
return nil, fmt.Errorf("typecheck %s: %w", f.Name.Name, err)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func parseConstDecls(genDecl *ast.GenDecl, info *types.Info, enumIndex map[string]int, schema *Schema) error {
|
||||
for _, spec := range genDecl.Specs {
|
||||
valueSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, name := range valueSpec.Names {
|
||||
obj, ok := info.Defs[name].(*types.Const)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
named, ok := obj.Type().(*types.Named)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
idx, ok := enumIndex[named.Obj().Name()]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
schema.Enums[idx].Values = append(schema.Enums[idx].Values, EnumValue{
|
||||
Name: name.Name,
|
||||
Value: obj.Val().String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseStruct(
|
||||
pkg string,
|
||||
name string,
|
||||
st *ast.StructType,
|
||||
knownStructs map[string]bool,
|
||||
namedPrimitives map[string]PrimitiveKind,
|
||||
) (Message, error) {
|
||||
msg := Message{PackageName: pkg, Name: name}
|
||||
|
||||
for _, astField := range st.Fields.List {
|
||||
if len(astField.Names) == 0 {
|
||||
continue // embedded field, пропускаем
|
||||
}
|
||||
|
||||
var rawTag string
|
||||
if astField.Tag != nil {
|
||||
tag := reflect.StructTag(strings.Trim(astField.Tag.Value, "`"))
|
||||
rawTag = tag.Get("pack")
|
||||
}
|
||||
|
||||
for _, fieldName := range astField.Names {
|
||||
field, err := parseFieldType(fieldName.Name, astField.Type, rawTag, knownStructs, namedPrimitives)
|
||||
if err != nil {
|
||||
return Message{}, fmt.Errorf("field %s: %w", fieldName.Name, err)
|
||||
}
|
||||
|
||||
msg.Fields = append(msg.Fields, field)
|
||||
}
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func parseFieldType(
|
||||
name string,
|
||||
expr ast.Expr,
|
||||
rawTag string,
|
||||
knownStructs map[string]bool,
|
||||
namedPrimitives map[string]PrimitiveKind,
|
||||
) (Field, error) {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return parsePrimitiveOrNested(name, t.Name, rawTag, knownStructs, namedPrimitives)
|
||||
|
||||
case *ast.ArrayType:
|
||||
if t.Len == nil {
|
||||
elem, err := parseFieldType("", t.Elt, rawTag, knownStructs, namedPrimitives)
|
||||
if err != nil {
|
||||
return Field{}, fmt.Errorf("slice element: %w", err)
|
||||
}
|
||||
|
||||
return Field{
|
||||
Name: name,
|
||||
Kind: KindSlice,
|
||||
Elem: &elem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
n, err := parseArrayLen(t.Len)
|
||||
if err != nil {
|
||||
return Field{}, fmt.Errorf("array length: %w", err)
|
||||
}
|
||||
|
||||
elem, err := parseFieldType("", t.Elt, rawTag, knownStructs, namedPrimitives)
|
||||
if err != nil {
|
||||
return Field{}, fmt.Errorf("array element: %w", err)
|
||||
}
|
||||
|
||||
return Field{
|
||||
Name: name,
|
||||
Kind: KindFixedArray,
|
||||
Elem: &elem,
|
||||
FixedLen: n,
|
||||
}, nil
|
||||
|
||||
case *ast.StarExpr:
|
||||
return Field{}, fmt.Errorf("pointer types not supported")
|
||||
|
||||
case *ast.SelectorExpr:
|
||||
return Field{}, fmt.Errorf("external package types not supported (use only types from the same file)")
|
||||
}
|
||||
|
||||
return Field{}, fmt.Errorf("unsupported type expression %T", expr)
|
||||
}
|
||||
|
||||
func parsePrimitiveOrNested(
|
||||
name string,
|
||||
typeName string,
|
||||
rawTag string,
|
||||
knownStructs map[string]bool,
|
||||
namedPrimitives map[string]PrimitiveKind,
|
||||
) (Field, error) {
|
||||
primKind, isPrimitive := goPrimitiveKind(typeName)
|
||||
if !isPrimitive {
|
||||
if namedPrimitive, ok := namedPrimitives[typeName]; ok {
|
||||
return buildPrimitiveField(name, typeName, namedPrimitive, rawTag)
|
||||
}
|
||||
|
||||
if !knownStructs[typeName] {
|
||||
return Field{}, fmt.Errorf("unknown type %q (not a primitive and not defined in the same file)", typeName)
|
||||
}
|
||||
return Field{
|
||||
Name: name,
|
||||
Kind: KindNested,
|
||||
TypeName: typeName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return buildPrimitiveField(name, "", primKind, rawTag)
|
||||
}
|
||||
|
||||
func buildPrimitiveField(name, namedType string, primKind PrimitiveKind, rawTag string) (Field, error) {
|
||||
field := Field{
|
||||
Name: name,
|
||||
Kind: KindPrimitive,
|
||||
Primitive: primKind,
|
||||
NamedType: namedType,
|
||||
}
|
||||
|
||||
if rawTag != "" {
|
||||
if primKind != KindFloat32 && primKind != KindFloat64 {
|
||||
typeLabel := field.GoTypeName()
|
||||
return Field{}, fmt.Errorf("arpack tag can only be applied to float32/float64, got %s", typeLabel)
|
||||
}
|
||||
quant, err := parseQuantTag(rawTag)
|
||||
if err != nil {
|
||||
return Field{}, fmt.Errorf("arpack tag: %w", err)
|
||||
}
|
||||
field.Quant = quant
|
||||
}
|
||||
|
||||
return field, nil
|
||||
}
|
||||
|
||||
func parseQuantTag(tag string) (*QuantInfo, error) {
|
||||
info := &QuantInfo{Bits: 16}
|
||||
parts := strings.Split(tag, ",")
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
kv := strings.SplitN(p, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
return nil, fmt.Errorf("invalid tag part %q (expected key=value)", p)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(kv[0])
|
||||
val := strings.TrimSpace(kv[1])
|
||||
|
||||
switch key {
|
||||
case "min":
|
||||
v, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("min: %w", err)
|
||||
}
|
||||
|
||||
info.Min = v
|
||||
case "max":
|
||||
v, err := strconv.ParseFloat(val, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("max: %w", err)
|
||||
}
|
||||
|
||||
info.Max = v
|
||||
case "bits":
|
||||
v, err := strconv.Atoi(val)
|
||||
if err != nil || (v != 8 && v != 16) {
|
||||
return nil, fmt.Errorf("bits must be 8 or 16, got %q", val)
|
||||
}
|
||||
|
||||
info.Bits = v
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown tag key %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
if info.Max <= info.Min {
|
||||
return nil, fmt.Errorf("max (%.6g) must be greater than min (%.6g)", info.Max, info.Min)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func parseArrayLen(expr ast.Expr) (int, error) {
|
||||
lit, ok := expr.(*ast.BasicLit)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("array length must be a literal integer constant")
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(lit.Value)
|
||||
if err != nil || n <= 0 {
|
||||
return 0, fmt.Errorf("array length must be a positive integer, got %q", lit.Value)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func goPrimitiveKind(name string) (PrimitiveKind, bool) {
|
||||
switch name {
|
||||
case "float32":
|
||||
return KindFloat32, true
|
||||
case "float64":
|
||||
return KindFloat64, true
|
||||
case "int8":
|
||||
return KindInt8, true
|
||||
case "int16":
|
||||
return KindInt16, true
|
||||
case "int32", "int":
|
||||
return KindInt32, true
|
||||
case "int64":
|
||||
return KindInt64, true
|
||||
case "uint8", "byte":
|
||||
return KindUint8, true
|
||||
case "uint16":
|
||||
return KindUint16, true
|
||||
case "uint32", "uint":
|
||||
return KindUint32, true
|
||||
case "uint64":
|
||||
return KindUint64, true
|
||||
case "bool":
|
||||
return KindBool, true
|
||||
case "string":
|
||||
return KindString, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const sampleSource = `package messages
|
||||
|
||||
type Vector3 struct {
|
||||
X float32 ` + "`" + `pack:"min=-500,max=500,bits=16"` + "`" + `
|
||||
Y float32 ` + "`" + `pack:"min=-500,max=500,bits=16"` + "`" + `
|
||||
Z float32 ` + "`" + `pack:"min=-500,max=500,bits=16"` + "`" + `
|
||||
}
|
||||
|
||||
type MoveMessage struct {
|
||||
Position Vector3
|
||||
Velocity [3]float32
|
||||
Waypoints []Vector3
|
||||
PlayerID uint32
|
||||
Name string
|
||||
Active bool
|
||||
}
|
||||
|
||||
type SpawnMessage struct {
|
||||
ID uint64
|
||||
Position Vector3
|
||||
Tags [4]string
|
||||
Data []uint8
|
||||
}
|
||||
`
|
||||
|
||||
const enumSource = `package messages
|
||||
|
||||
type Opcode uint16
|
||||
|
||||
const (
|
||||
OpcodeUnknown Opcode = iota
|
||||
OpcodeAuthorize
|
||||
OpcodeJoinRoom
|
||||
)
|
||||
|
||||
type EnvelopeMessage struct {
|
||||
Code Opcode
|
||||
Counter uint8
|
||||
}
|
||||
`
|
||||
|
||||
func TestParseSource_Primitives(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
if len(msgs) != 3 {
|
||||
t.Fatalf("expected 3 messages, got %d", len(msgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSource_Vector3(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
|
||||
v3 := msgs[0]
|
||||
if v3.Name != "Vector3" {
|
||||
t.Fatalf("expected Vector3, got %s", v3.Name)
|
||||
}
|
||||
if len(v3.Fields) != 3 {
|
||||
t.Fatalf("expected 3 fields, got %d", len(v3.Fields))
|
||||
}
|
||||
|
||||
for _, f := range v3.Fields {
|
||||
if f.Kind != KindPrimitive {
|
||||
t.Errorf("field %s: expected KindPrimitive, got %d", f.Name, f.Kind)
|
||||
}
|
||||
if f.Primitive != KindFloat32 {
|
||||
t.Errorf("field %s: expected KindFloat32, got %d", f.Name, f.Primitive)
|
||||
}
|
||||
if f.Quant == nil {
|
||||
t.Errorf("field %s: expected quant info, got nil", f.Name)
|
||||
continue
|
||||
}
|
||||
if f.Quant.Min != -500 || f.Quant.Max != 500 || f.Quant.Bits != 16 {
|
||||
t.Errorf("field %s: wrong quant info %+v", f.Name, f.Quant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSource_MoveMessage(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
|
||||
msg := msgs[1]
|
||||
if msg.Name != "MoveMessage" {
|
||||
t.Fatalf("expected MoveMessage, got %s", msg.Name)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
kind FieldKind
|
||||
typeName string
|
||||
fixedLen int
|
||||
elemKind FieldKind
|
||||
}{
|
||||
{"Position", KindNested, "Vector3", 0, 0},
|
||||
{"Velocity", KindFixedArray, "", 3, KindPrimitive},
|
||||
{"Waypoints", KindSlice, "", 0, KindNested},
|
||||
{"PlayerID", KindPrimitive, "", 0, 0},
|
||||
{"Name", KindPrimitive, "", 0, 0},
|
||||
{"Active", KindPrimitive, "", 0, 0},
|
||||
}
|
||||
|
||||
if len(msg.Fields) != len(tests) {
|
||||
t.Fatalf("expected %d fields, got %d", len(tests), len(msg.Fields))
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
f := msg.Fields[i]
|
||||
if f.Name != tc.name {
|
||||
t.Errorf("[%d] expected field %s, got %s", i, tc.name, f.Name)
|
||||
}
|
||||
if f.Kind != tc.kind {
|
||||
t.Errorf("field %s: expected kind %d, got %d", tc.name, tc.kind, f.Kind)
|
||||
}
|
||||
if tc.typeName != "" && f.TypeName != tc.typeName {
|
||||
t.Errorf("field %s: expected TypeName %s, got %s", tc.name, tc.typeName, f.TypeName)
|
||||
}
|
||||
if tc.fixedLen > 0 {
|
||||
if f.FixedLen != tc.fixedLen {
|
||||
t.Errorf("field %s: expected FixedLen %d, got %d", tc.name, tc.fixedLen, f.FixedLen)
|
||||
}
|
||||
if f.Elem == nil {
|
||||
t.Errorf("field %s: Elem is nil", tc.name)
|
||||
} else if f.Elem.Kind != tc.elemKind {
|
||||
t.Errorf("field %s: Elem.Kind expected %d, got %d", tc.name, tc.elemKind, f.Elem.Kind)
|
||||
}
|
||||
}
|
||||
if tc.kind == KindSlice {
|
||||
if f.Elem == nil {
|
||||
t.Errorf("field %s: Elem is nil for slice", tc.name)
|
||||
} else if f.Elem.Kind != tc.elemKind {
|
||||
t.Errorf("field %s: Elem.Kind expected %d, got %d", tc.name, tc.elemKind, f.Elem.Kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSource_SpawnMessage(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
|
||||
msg := msgs[2]
|
||||
if msg.Name != "SpawnMessage" {
|
||||
t.Fatalf("expected SpawnMessage, got %s", msg.Name)
|
||||
}
|
||||
|
||||
// Tags: [4]string
|
||||
tags := msg.Fields[2]
|
||||
if tags.Kind != KindFixedArray || tags.FixedLen != 4 {
|
||||
t.Errorf("Tags: expected KindFixedArray[4], got kind=%d fixedLen=%d", tags.Kind, tags.FixedLen)
|
||||
}
|
||||
if tags.Elem == nil || tags.Elem.Primitive != KindString {
|
||||
t.Errorf("Tags: expected string element")
|
||||
}
|
||||
|
||||
// Data: []uint8
|
||||
data := msg.Fields[3]
|
||||
if data.Kind != KindSlice {
|
||||
t.Errorf("Data: expected KindSlice, got %d", data.Kind)
|
||||
}
|
||||
if data.Elem == nil || data.Elem.Primitive != KindUint8 {
|
||||
t.Errorf("Data: expected uint8 element")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSchemaSource_Enums(t *testing.T) {
|
||||
schema, err := ParseSchemaSource(enumSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSchemaSource: %v", err)
|
||||
}
|
||||
|
||||
if len(schema.Messages) != 1 {
|
||||
t.Fatalf("expected 1 message, got %d", len(schema.Messages))
|
||||
}
|
||||
if len(schema.Enums) != 1 {
|
||||
t.Fatalf("expected 1 enum, got %d", len(schema.Enums))
|
||||
}
|
||||
|
||||
enum := schema.Enums[0]
|
||||
if enum.Name != "Opcode" {
|
||||
t.Fatalf("expected enum Opcode, got %s", enum.Name)
|
||||
}
|
||||
if enum.Primitive != KindUint16 {
|
||||
t.Fatalf("expected Opcode base kind uint16, got %d", enum.Primitive)
|
||||
}
|
||||
if len(enum.Values) != 3 {
|
||||
t.Fatalf("expected 3 enum values, got %d", len(enum.Values))
|
||||
}
|
||||
if enum.Values[1].Name != "OpcodeAuthorize" || enum.Values[1].Value != "1" {
|
||||
t.Fatalf("unexpected enum value %#v", enum.Values[1])
|
||||
}
|
||||
|
||||
field := schema.Messages[0].Fields[0]
|
||||
if field.Kind != KindPrimitive {
|
||||
t.Fatalf("expected EnvelopeMessage.Code to be primitive, got %d", field.Kind)
|
||||
}
|
||||
if field.NamedType != "Opcode" {
|
||||
t.Fatalf("expected named type Opcode, got %q", field.NamedType)
|
||||
}
|
||||
if field.Primitive != KindUint16 {
|
||||
t.Fatalf("expected underlying uint16, got %d", field.Primitive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuantTag_Errors(t *testing.T) {
|
||||
cases := []struct {
|
||||
src string
|
||||
wantErr bool
|
||||
}{
|
||||
{`package p; type T struct { X float32 ` + "`" + `pack:"min=0,max=100"` + "`" + ` }`, false},
|
||||
{`package p; type T struct { X float32 ` + "`" + `pack:"min=100,max=0"` + "`" + ` }`, true}, // max < min
|
||||
{`package p; type T struct { X float32 ` + "`" + `pack:"min=0,max=100,bits=32"` + "`" + ` }`, true}, // bad bits
|
||||
{`package p; type T struct { X int32 ` + "`" + `pack:"min=0,max=100"` + "`" + ` }`, true}, // quant на int
|
||||
{`package p; type T struct { X float32 ` + "`" + `pack:"foo=1"` + "`" + ` }`, true}, // unknown key
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
_, err := ParseSource(tc.src)
|
||||
if tc.wantErr && err == nil {
|
||||
t.Errorf("[%d] expected error, got nil", i)
|
||||
}
|
||||
if !tc.wantErr && err != nil {
|
||||
t.Errorf("[%d] unexpected error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireSize(t *testing.T) {
|
||||
msgs, err := ParseSource(sampleSource)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSource: %v", err)
|
||||
}
|
||||
|
||||
v3 := msgs[0]
|
||||
// Vector3: 3 × uint16 (квантизованные float32) = 6 байт
|
||||
if v3.MinWireSize() != 6 {
|
||||
t.Errorf("Vector3.MinWireSize: expected 6, got %d", v3.MinWireSize())
|
||||
}
|
||||
if v3.HasVariableFields() {
|
||||
t.Errorf("Vector3 should not have variable fields")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedUnknownType(t *testing.T) {
|
||||
src := `package p
|
||||
type Msg struct {
|
||||
Pos UnknownType
|
||||
}
|
||||
`
|
||||
_, err := ParseSource(src)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown nested type, got nil")
|
||||
}
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
package parser
|
||||
|
||||
// PrimitiveKind — конкретный примитивный тип.
|
||||
type PrimitiveKind int
|
||||
|
||||
const (
|
||||
KindFloat32 PrimitiveKind = iota
|
||||
KindFloat64
|
||||
KindInt8
|
||||
KindInt16
|
||||
KindInt32
|
||||
KindInt64
|
||||
KindUint8
|
||||
KindUint16
|
||||
KindUint32
|
||||
KindUint64
|
||||
KindBool
|
||||
KindString
|
||||
)
|
||||
|
||||
// FieldKind — категория поля.
|
||||
type FieldKind int
|
||||
|
||||
const (
|
||||
KindPrimitive FieldKind = iota // float, int, uint, bool, string
|
||||
KindNested // ссылка на другой Message
|
||||
KindFixedArray // [N]T
|
||||
KindSlice // []T
|
||||
)
|
||||
|
||||
// QuantInfo описывает квантизацию float → uint8/uint16.
|
||||
type QuantInfo struct {
|
||||
Min float64
|
||||
Max float64
|
||||
Bits int // 8 или 16, default 16
|
||||
}
|
||||
|
||||
// MaxUint — максимальное целое значение для данного числа бит.
|
||||
func (q *QuantInfo) MaxUint() float64 {
|
||||
if q.Bits == 8 {
|
||||
return 255
|
||||
}
|
||||
return 65535
|
||||
}
|
||||
|
||||
// WireBytes — размер на проводе в байтах.
|
||||
func (q *QuantInfo) WireBytes() int {
|
||||
return q.Bits / 8
|
||||
}
|
||||
|
||||
// Field — одно поле структуры-сообщения.
|
||||
type Field struct {
|
||||
Name string
|
||||
Kind FieldKind
|
||||
|
||||
// KindPrimitive
|
||||
Primitive PrimitiveKind
|
||||
NamedType string
|
||||
Quant *QuantInfo // nil если нет квантизации
|
||||
|
||||
// KindNested
|
||||
TypeName string
|
||||
|
||||
// KindFixedArray / KindSlice
|
||||
Elem *Field
|
||||
FixedLen int // >0 только для KindFixedArray
|
||||
}
|
||||
|
||||
// WireSize — размер в байтах на проводе.
|
||||
// Возвращает -1 для полей переменного размера.
|
||||
func (f *Field) WireSize() int {
|
||||
switch f.Kind {
|
||||
case KindPrimitive:
|
||||
if f.Quant != nil {
|
||||
return f.Quant.WireBytes()
|
||||
}
|
||||
return primitiveWireSize(f.Primitive)
|
||||
case KindNested:
|
||||
return -1 // зависит от конкретного типа, узнаём через Message.MinWireSize
|
||||
case KindFixedArray:
|
||||
elemSize := f.Elem.WireSize()
|
||||
if elemSize == -1 {
|
||||
return -1
|
||||
}
|
||||
return f.FixedLen * elemSize
|
||||
case KindSlice:
|
||||
return -1 // uint16 len + переменная часть
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func primitiveWireSize(k PrimitiveKind) int {
|
||||
switch k {
|
||||
case KindFloat32, KindInt32, KindUint32:
|
||||
return 4
|
||||
case KindFloat64, KindInt64, KindUint64:
|
||||
return 8
|
||||
case KindInt16, KindUint16:
|
||||
return 2
|
||||
case KindInt8, KindUint8, KindBool:
|
||||
return 1
|
||||
case KindString:
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// IsIntegralPrimitive — подходит ли тип как базовый для enum.
|
||||
func IsIntegralPrimitive(k PrimitiveKind) bool {
|
||||
switch k {
|
||||
case KindInt8, KindInt16, KindInt32, KindInt64, KindUint8, KindUint16, KindUint32, KindUint64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GoTypeName — имя типа в Go.
|
||||
func (f *Field) GoTypeName() string {
|
||||
switch f.Kind {
|
||||
case KindPrimitive:
|
||||
if f.NamedType != "" {
|
||||
return f.NamedType
|
||||
}
|
||||
return primitiveGoName(f.Primitive)
|
||||
case KindNested:
|
||||
return f.TypeName
|
||||
case KindFixedArray:
|
||||
return "[" + itoa(f.FixedLen) + "]" + f.Elem.GoTypeName()
|
||||
case KindSlice:
|
||||
return "[]" + f.Elem.GoTypeName()
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// CSharpTypeName — имя типа в C#.
|
||||
func (f *Field) CSharpTypeName() string {
|
||||
switch f.Kind {
|
||||
case KindPrimitive:
|
||||
if f.NamedType != "" {
|
||||
return f.NamedType
|
||||
}
|
||||
return primitiveCSharpName(f.Primitive)
|
||||
case KindNested:
|
||||
return f.TypeName
|
||||
case KindFixedArray:
|
||||
return f.Elem.CSharpTypeName() + "[]"
|
||||
case KindSlice:
|
||||
return f.Elem.CSharpTypeName() + "[]"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func primitiveGoName(k PrimitiveKind) string {
|
||||
switch k {
|
||||
case KindFloat32:
|
||||
return "float32"
|
||||
case KindFloat64:
|
||||
return "float64"
|
||||
case KindInt8:
|
||||
return "int8"
|
||||
case KindInt16:
|
||||
return "int16"
|
||||
case KindInt32:
|
||||
return "int32"
|
||||
case KindInt64:
|
||||
return "int64"
|
||||
case KindUint8:
|
||||
return "uint8"
|
||||
case KindUint16:
|
||||
return "uint16"
|
||||
case KindUint32:
|
||||
return "uint32"
|
||||
case KindUint64:
|
||||
return "uint64"
|
||||
case KindBool:
|
||||
return "bool"
|
||||
case KindString:
|
||||
return "string"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// GoPrimitiveTypeName — базовый примитивный тип поля в Go.
|
||||
func (f *Field) GoPrimitiveTypeName() string {
|
||||
return primitiveGoName(f.Primitive)
|
||||
}
|
||||
|
||||
func primitiveCSharpName(k PrimitiveKind) string {
|
||||
switch k {
|
||||
case KindFloat32:
|
||||
return "float"
|
||||
case KindFloat64:
|
||||
return "double"
|
||||
case KindInt8:
|
||||
return "sbyte"
|
||||
case KindInt16:
|
||||
return "short"
|
||||
case KindInt32:
|
||||
return "int"
|
||||
case KindInt64:
|
||||
return "long"
|
||||
case KindUint8:
|
||||
return "byte"
|
||||
case KindUint16:
|
||||
return "ushort"
|
||||
case KindUint32:
|
||||
return "uint"
|
||||
case KindUint64:
|
||||
return "ulong"
|
||||
case KindBool:
|
||||
return "bool"
|
||||
case KindString:
|
||||
return "string"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// CSharpPrimitiveTypeName — базовый примитивный тип поля в C#.
|
||||
func (f *Field) CSharpPrimitiveTypeName() string {
|
||||
return primitiveCSharpName(f.Primitive)
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
buf := [20]byte{}
|
||||
pos := len(buf)
|
||||
for n > 0 {
|
||||
pos--
|
||||
buf[pos] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[pos:])
|
||||
}
|
||||
|
||||
// Message — описание одной структуры-сообщения.
|
||||
type Message struct {
|
||||
PackageName string
|
||||
Name string
|
||||
Fields []Field
|
||||
}
|
||||
|
||||
// EnumValue — одно именованное значение enum.
|
||||
type EnumValue struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Enum — enum-подобный тип на основе именованного примитива.
|
||||
type Enum struct {
|
||||
Name string
|
||||
Primitive PrimitiveKind
|
||||
Values []EnumValue
|
||||
}
|
||||
|
||||
// Schema — полная модель входного файла.
|
||||
type Schema struct {
|
||||
PackageName string
|
||||
Messages []Message
|
||||
Enums []Enum
|
||||
}
|
||||
|
||||
// MinWireSize — минимальный гарантированный размер в байтах.
|
||||
// Для вложенных типов считается только если размер известен заранее.
|
||||
// Строки и слайсы считаются как 2 байта (length prefix).
|
||||
func (m *Message) MinWireSize() int {
|
||||
total := 0
|
||||
for _, f := range m.Fields {
|
||||
s := f.WireSize()
|
||||
if s == -1 {
|
||||
total += 2 // минимум: length prefix
|
||||
} else {
|
||||
total += s
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// HasVariableFields — есть ли поля переменного размера.
|
||||
func (m *Message) HasVariableFields() bool {
|
||||
for _, f := range m.Fields {
|
||||
if f.WireSize() == -1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
package messages
|
||||
|
||||
// Vector3 — трёхмерный вектор с квантизацией.
|
||||
type Vector3 struct {
|
||||
X float32 `pack:"min=-500,max=500,bits=16"`
|
||||
Y float32 `pack:"min=-500,max=500,bits=16"`
|
||||
Z float32 `pack:"min=-500,max=500,bits=16"`
|
||||
}
|
||||
|
||||
type Opcode uint16
|
||||
|
||||
const (
|
||||
OpcodeUnknown Opcode = iota
|
||||
OpcodeAuthorize
|
||||
OpcodeJoinRoom
|
||||
)
|
||||
|
||||
// MoveMessage содержит всё многообразие поддерживаемых типов.
|
||||
type MoveMessage struct {
|
||||
Position Vector3 // вложенный тип
|
||||
Velocity [3]float32 // фиксированный массив без квантизации
|
||||
Waypoints []Vector3 // слайс вложенных типов
|
||||
PlayerID uint32
|
||||
// 3 подряд bool → упаковываются в 1 байт
|
||||
Active bool
|
||||
Visible bool
|
||||
Ghost bool
|
||||
Name string
|
||||
}
|
||||
|
||||
// SpawnMessage — пример с целочисленными полями и массивами примитивов.
|
||||
type SpawnMessage struct {
|
||||
EntityID uint64
|
||||
Position Vector3
|
||||
Health int16
|
||||
Tags []string
|
||||
Data []uint8
|
||||
}
|
||||
|
||||
type EnvelopeMessage struct {
|
||||
Code Opcode
|
||||
Counter uint8
|
||||
}
|
||||
Reference in New Issue
Block a user