C#
Binary Serialization
Networking
Save System
Overview
One format, three jobs
Most games invent separate structures for saves, network messages and config assets. LAF was designed so the exact same bytes can travel between disk, network and editor without any conversion step.
The core design tension is self-description versus weight. A section stream describes its own shape — a reader can skip what it doesn't recognize, and a message can legally carry one section or all of them. The header sits on top of that as a pure convenience, not a requirement: it lets a reader answer "what's in here" before touching the payload, but a use site that only ever exchanges one known kind of section is free to skip it entirely and save the bytes.
No compile-time schema, no code generation, no boilerplate ScriptableObject per data type. The same reader handles a full save and a partial network update.
Designed for
Save · Net · Delta
File / Packet Layout
Optional header + payload
Optional header (8 bytes, plain text)
Offset Size Field
0 1 Version (bit7 = protected/encrypted, bits0-6 = version)
1 3 ContentFlags
4 4 CRC32 of raw payload (optional)
8 N Payload — section stream, optionally encrypted
The header is a convenience, not a requirement — its presence is a contract between the two ends of a given file kind or endpoint, not something signaled inside the bytes. A use site that only ever exchanges one known section skips the header and saves 8 bytes. ContentFlags, when a header is present, mark which sections are worth unpacking for. Unknown section IDs are kept as opaque blobs and survive round-trips unchanged.
Raw Payload
Section stream
[1] sectionCount
for each section:
[1] sectionId
[2] sectionLength
[N] sectionBody
Because every section is self-describing, a network packet can legally contain just one section or any subset. The same code path that loads a full save also processes a partial update.
Comparison
Same data, different formats
Concrete example used for every format below:
- Name:
Bob
- Age:
18
- Inventory (9 slots):
- Slot 1 → Apple (id = 1), RemainingBites = 3
- Slot 4 → Crowbar (id = 2), Durability = 8
- Slot 6 → Backpack (id = 64), Capacity = 15
- Slot 8 → Hat (id = 580), Durability = 40, Protection = 5, CutProtection = 40%
- Slots 0, 2, 3, 5, 7 → empty (encoded only in the bitmask)
Empty slots are encoded only in a bitmask. Only runtime state is stored.
Max values live in the item definition. All dumps are exact.
JSON 201 bytes
source
{"name":"Bob","age":18,"inventory":{"slotCount":9,"slotMask":173,"items":[{"id":1,"remainingBites":3},
{"id":2,"durability":8},{"id":64,"capacity":15},{"id":580,"durability":40,"protection":5,"cutProtection":40}]}}
hex
7B 22 6E 61 6D 65 22 3A 22 42 6F 62 22 2C 22 61 67 65 22 3A 31 38 2C
22 69 6E 76 65 6E 74 6F 72 79 22 3A 7B 22 73 6C 6F 74 43 6F 75 6E 74 22
3A 39 2C 22 73 6C 6F 74 4D 61 73 6B 22 3A 31 37 33 2C 22 69 74 65 6D
73 22 3A 5B 7B 22 69 64 22 3A 31 2C 22 72 65 6D 61 69 6E 69 6E 67 42
69 74 65 73 22 3A 33 7D 2C 7B 22 69 64 22 3A 32 2C 22 64 75 72 61 62
69 6C 69 74 79 22 3A 38 7D 2C 7B 22 69 64 22 3A 36 34 2C 22 63 61 70
61 63 69 74 79 22 3A 31 35 7D 2C 7B 22 69 64 22 3A 35 38 30 2C 22 64
75 72 61 62 69 6C 69 74 79 22 3A 34 30 2C 22 70 72 6F 74 65 63 74 69
6F 6E 22 3A 35 2C 22 63 75 74 50 72 6F 74 65 63 74 69 6F 6E 22 3A 34
30 7D 5D 7D 7D
Protobuf 52 bytes + schema
schema
message Player {
string name = 1;
int32 age = 2;
Inventory inventory = 3;
}
message Inventory {
int32 slotCount = 1;
uint64 slotMask = 2; // bit i = 1 → slot i empty
repeated Item items = 3; // dense, in ascending slot order
}
message Item {
int32 id = 1;
int32 remainingBites = 2;
int32 durability = 3;
int32 capacity = 4;
int32 protection = 5;
int32 cutProtection = 6;
}
wire bytes
0A 03 42 6F 62 // name = "Bob"
10 12 // age = 18
1A 2C // Inventory
08 09 // slotCount = 9
10 AD 01 // slotMask = 173
1A 06 // Item (Apple)
08 01 // id = 1
10 03 // remainingBites = 3
1A 06 // Item (Crowbar)
08 02 // id = 2
18 08 // durability = 8
1A 06 // Item (Backpack)
08 40 // id = 64
20 0F // capacity = 15
1A 0C // Item (Hat)
08 C4 04 // id = 580
18 28 // durability = 40
28 05 // protection = 5
30 28 // cutProtection = 40
0A 03 42 6F 62 10 12 1A 2C 08 09 10 AD 01 1A 06 08 01 10 03 1A 06 08 02
18 08 1A 06 08 40 20 0F 1A 0C 08 C4 04 18 28 28 05 30 28
MessagePack 109 bytes
wire bytes
83 // map of 3
A4 6E 61 6D 65 // "name"
A3 42 6F 62 // "Bob"
A3 61 67 65 // "age"
12 // 18
A9 69 6E 76 65 6E 74 6F 72 79 // "inventory"
83 // map of 3
A9 73 6C 6F 74 43 6F 75 6E 74 // "slotCount"
09 // 9
A8 73 6C 6F 74 4D 61 73 6B // "slotMask"
CC AD // 173
A5 69 74 65 6D 73 // "items"
94 // array of 4
82 // map of 2
A2 69 64 01 // "id" = 1
AE 72 65 6D 61 69 6E 69 6E 67 42 69 74 65 73 03 // "remainingBites" = 3
82 // map of 2
A2 69 64 02 // "id" = 2
A9 64 75 72 61 62 69 6C 69 74 79 08 // "durability" = 8
82 // map of 2
A2 69 64 40 // "id" = 64
A8 63 61 70 61 63 69 74 79 0F // "capacity" = 15
84 // map of 4
A2 69 64 CD 02 44 // "id" = 580
A9 64 75 72 61 62 69 6C 69 74 79 28 // "durability" = 40
AA 70 72 6F 74 65 63 74 69 6F 6E 05 // "protection" = 5
AD 63 75 74 50 72 6F 74 65 63 74 69 6F 6E 28 // "cutProtection" = 40
83 A4 6E 61 6D 65 A3 42 6F 62 A3 61 67 65 12 A9 69 6E 76 65 6E 74 6F
72 79 83 A9 73 6C 6F 74 43 6F 75 6E 74 09 A8 73 6C 6F 74 4D 61 73 6B
CC AD A5 69 74 65 6D 73 94 82 A2 69 64 01 AE 72 65 6D 61 69 6E 69 6E
67 42 69 74 65 73 03 82 A2 69 64 02 A9 64 75 72 61 62 69 6C 69 74 79
08 82 A2 69 64 40 A8 63 61 70 61 63 69 74 79 0F 84 A2 69 64 CD 02 44
A9 64 75 72 61 62 69 6C 69 74 79 28 AA 70 72 6F 74 65 63 74 69 6F 6E
05 AD 63 75 74 50 72 6F 74 65 63 74 69 6F 6E 28
LAF 43 bytes payload, +8 bytes only if a header is used
raw payload
01 // sectionCount = 1 (Inventory only)
02 // sectionId = Inventory
00 28 // sectionLength = 40
09 // slotCount = 9
AD 00 00 00 00 00 00 00 // slotMask (bits 0,2,3,5,7 = empty)
01 00 // Apple (id=1, roundCount=0)
02 00 // Crowbar (id=2, roundCount=0)
40 00 // Backpack (id=64, roundCount=0)
C4 04 00 // Hat (id=580, roundCount=0)
06 // runCount = 6
01 01 03 // RemainingBites = 3
02 01 08 00 // Durability = 8
03 01 0F // Capacity = 15
02 01 28 00 // Durability = 40
04 01 05 // Protection = 5
05 01 28 // CutProtection = 40
01 02 00 28 09 AD 00 00 00 00 00 00 00 01 00 02 00 40 00 C4 04 00 06
01 01 03 02 01 08 00 03 01 0F 02 01 28 00 04 01 05 05 01 28
Empty slots cost only bits in the mask. Only runtime state is stored.
The same bytes can be a save, a network packet, or a delta — and if an
endpoint only ever exchanges this one section, it can skip the 8-byte
header entirely and send exactly 43 bytes.
| Aspect |
JSON |
Protobuf |
MessagePack |
LAF |
| Size |
201 B |
52 B |
109 B |
43 B (+8 B if a header is used) |
| Schema / codegen |
No |
Required |
No |
No* |
| Send only one section |
Manual |
Usually no |
Manual |
Native |
| Same bytes for save & net |
Possible |
Rarely |
Possible |
Designed for it |
| Unknown data |
Kept |
Often stripped |
Kept |
Opaque sections kept |
* LAF has a fixed section + unit-field layout, but it is not compiled and does not generate code.
The same reader handles full saves and partial network updates.
Features
What the unprotected format gives
• Identical binary layout for save files, network packets and editor assets
• Header is optional — skip it entirely when a single, known section is enough
• Send a full state or any subset of sections — no extra encoding step
• Empty inventory slots cost only bits in an 8-byte mask
• No compile-time schema or code generation
• Runtime item state only (max values stay in item definitions)
• CRC32 is optional and, when used, covers only the raw payload
• Header, when present, is never transformed
• Unknown sections survive round-trip as opaque data
• Editor importer, hex view and save-back tools
Encryption
Optional, not a separate format
Setting the high bit of the version byte marks the payload as encrypted rather than plain — there's no separate file kind or extension for it, just one bit deciding which pipeline a reader applies. Encryption wraps the already-packed payload; the header, when present, doubles as authenticated data.
Results & Source
Screenshots, benchmarks
— .. —