initial
This commit is contained in:
@@ -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/...
|
||||
```
|
||||
Reference in New Issue
Block a user