Warman doesn't use JSON, XML, Protobuf, or any third-party serialization library. Every piece of data that gets written to disk or sent over the network uses a custom binary format built on top of C#'s BinaryReader and BinaryWriter. This includes save files, room data, terrain points, network ticks, item data, and mod content.
The Interface
The entire system is built on one interface: IBinarySerializable. It has two methods: WriteBytes(BinaryWriter) and ReadBytes(BinaryReader). Any struct or class that implements this interface can be serialized to a byte array and deserialized back. That's the whole contract.
Extension Methods
The BinaryExtension class provides a large set of extension methods that handle Unity types (Vector3, Quaternion, Bounds), game enums (DamageType, EffectType, ItemRarity), arrays, lists, nullable types, and asset references. Writing a Vector3 is three floats. Writing a Quaternion is four floats. Writing an enum is a single byte (or int for the ones that need it). Writing an array writes the length first, then each element.
Asset references (like modifiers, enemy presets, and room files) are serialized by name. On write, the asset's name string gets stored. On read, the name is looked up in a global asset registry to resolve back to the actual ScriptableObject. This means save files and network packets contain string names for assets, not binary pointers, so they survive game updates that change memory layouts.
Why Not JSON?
Three reasons. First, size. A PlayerInputCommand serializes to about 7 bytes in binary. The same data as JSON with property names would be 10-20x larger. For network ticks sent 20 times per second to 8 players, that adds up fast.
Second, speed. Binary read/write is direct memory operations. No parsing, no tokenizing, no string-to-number conversion. Deserializing a room file with 30,000 terrain points needs to be fast, and scanning 30,000 JSON objects isn't.
Versioning
The tradeoff is versioning. Binary formats break when the data shape changes. Adding a field, removing a field, or changing a field's type all require migration logic. Warman handles this with version bytes at the start of critical data structures. A TerrainPoint writes its version (currently 1) as the first byte. The reader checks the version and branches to the correct deserialization path. If a future update adds a new terrain property, it bumps the version to 2 and the reader handles both formats.
For network data, versioning is not needed because all clients run the same game version, enforced at connection time. For save files and mod content, it matters and the version byte approach keeps things backward-compatible without requiring a full migration framework.