Skip to content

Extensions

irrld edited this page Jul 30, 2026 · 1 revision

Optional add-ons that bridge znet to a third-party library, or add a layer the core deliberately does not carry. Each lives in extensions/, links znet the way your own code would, and adds nothing to the core target: a build with all of them off is byte-identical to one from before they existed.

They are separate because they carry dependencies the library refuses to take on. znet needs OpenSSL and zstd; it should not need a maths library because some users serialize vectors.

What is there

Nothing here is required, and nothing depends on anything except what its row says.

Making packets smaller

Dependency
quantize none The compression arithmetic on plain scalars: fixed point, half floats, smallest-three quaternions, octahedral normals. Everything below that compresses a value delegates here, so they all agree bit for bit
bitpack quantize Sub-byte fields in a Buffer. A bool costs 1 bit instead of 8; a value bounded by 200 costs 8 instead of 16. Ranged ints and floats work out their own width
delta bitpack Send only what changed since a baseline the peer acknowledged. Snapshot history, wrap-safe sequence numbers, one flag bit per unchanged field

A six-field player state is 50 bits sent in full and 25 on average as a delta, measured over 3000 ticks with 20% packet loss.

Writing less serialization code

Dependency
reflect none, C++17 Serialize a struct from its fields. A plain aggregate needs nothing declared at all; ZNET_REFLECT covers the rest
json nlohmann-json Length-prefixed json as MessagePack or text, with the depth and size bounds that make parsing network-supplied json safe, plus a ready-made PacketSerializer
flatbuffers FlatBuffers, C++17 FlatBuffers payloads with no way to reach a root without the Verifier having run
entt EnTT, C++17 Buffer archives for entt::snapshot and continuous_loader, so an ECS registry replicates with entity remapping already handled

Engine types on the wire

Dependency
glm glm, quantize Read/write for vec, mat and quat, plus compressed forms: 4-byte quaternions, 4-byte normals, ranged fixed-point positions
box2d Box2D 3.x Planar body state in 10 bytes instead of 28. Orientation is one wrapped angle, not a quaternion
bullet Bullet 3.x Rigid body state in 15 bytes instead of 40
jolt Jolt 5.x, C++17 Rigid body state in 15 bytes instead of 40

Elsewhere

Dependency
spdlog spdlog Routes znet's logging into spdlog with the severity intact, rather than as a pre-formatted line

Using one

Every extension is a header-only INTERFACE target. Link it and include its umbrella header.

target_link_libraries(my_game PRIVATE znet-reflect znet-glm)
#include "znet/ext/reflect/reflect_all.h"
#include "znet/ext/glm/glm.h"

Each target also has a znet:: alias, so znet::reflect works too. Everything lives in namespace znet::ext.

A taste of what that buys, with nothing declared about the struct:

struct Move {
  uint32_t entity;
  float x, y, z;
};

codec->Add(kMove, znet::ext::MakeAutoSerializer<Move>(kMove));

auto packet = std::make_shared<znet::ext::AutoPacket<Move>>(kMove, move);
session->SendPacket(packet);

Build options

On by default. An extension whose dependency is missing skips itself with a message(STATUS ...) rather than failing the build, so leaving them all enabled is safe even on a machine that has none of the libraries.

Option Default
ZNET_BUILD_EXTENSIONS ON OFF skips the whole extensions/ tree
ZNET_EXT_ALLOW_FETCH ON OFF never downloads a missing dependency; the extension skips instead. Set this in an offline or locked-down build
ZNET_EXT_QUANTIZE ON
ZNET_EXT_BITPACK ON
ZNET_EXT_DELTA ON
ZNET_EXT_REFLECT ON
ZNET_EXT_GLM ON
ZNET_EXT_JSON ON
ZNET_EXT_ENTT ON
ZNET_EXT_FLATBUFFERS ON
ZNET_EXT_BOX2D ON
ZNET_EXT_BULLET ON
ZNET_EXT_JOLT ON
ZNET_EXT_SPDLOG ON
cmake -B build -DZNET_EXT_ALLOW_FETCH=OFF -DZNET_EXT_BULLET=OFF

Configuring prints one line per extension, which is the quickest way to see what actually got built:

-- znet-quantize: enabled
-- znet-glm: enabled against glm::glm
-- znet-entt: EnTT requires C++17 and the tree is at C++14; skipping extension
-- znet-box2d: Box2D not installed, fetching it

Where dependencies come from

An installed copy is always preferred: your renderer is already built against a particular glm, and a second one on the include path helps nobody. Only when find_package comes up empty does the build fetch, and every fetch is pinned to a version and a SHA256 and marked SYSTEM, so the dependency's own warnings stay out of your build.

Bullet is worth calling out: its release archive is 136 MB, and its own CMake predates several things modern CMake requires. Both are handled, but if you already have Bullet installed you will want it found rather than fetched.

The MSVC runtime

Everything that ends up in one binary has to agree on the C runtime library, and MSVC refuses a mix with LNK2038 followed by a page of duplicate symbols out of msvcprt.lib. znet takes CMake's default, the DLL runtime (/MD).

Box2D, Bullet and Jolt each choose the static one (/MT) for themselves when built standalone. A fetched copy is put back on this build's runtime after it is configured, so nothing is asked of you.

An installed copy is a different matter: it was compiled before this build existed and nothing at this end can change what it was compiled against. If a vcpkg or hand-built Box2D, Bullet or Jolt used the static runtime, that extension's tests will not link. Either rebuild the dependency against the DLL runtime, or have znet fetch and build its own copy instead:

cmake -B build -DCMAKE_DISABLE_FIND_PACKAGE_Jolt=ON

None of this applies to any other compiler.

The C++17 ones

reflect, entt, flatbuffers and jolt need C++17. znet itself still builds at C++14, and on a -DZNET_CXX_STANDARD=14 tree those four step aside and say so rather than dragging the whole build up. The other eight work at 14.

For reflect the floor is not incidental: structured bindings are what make deducing a struct's fields possible at all, so on C++14 it could only ever offer the macro.

Tests

Each extension has its own suite, wired only when the parent build already has googletest, and run by the usual ctest:

ctest -R ext-           # every extension
ctest -R ext-reflect    # one of them

An extension never fetches a test framework of its own, so vendoring a single extension directory into another project gets you the headers and nothing else.

Writing one

The house rules, if you want to add to this list:

  • header-only INTERFACE target named znet-<thing>, with a znet::<thing> alias
  • public headers under include/znet/ext/<thing>/, umbrella header <thing>.h beside them
  • everything in namespace znet::ext
  • C++14 unless a dependency forces higher, and then gate on ZNET_CXX_STANDARD and skip rather than fail
  • prefer an installed dependency; return() early with a message(STATUS ...) when it is absent
  • tests under tests/, guarded on if(TARGET gtest_main), compiled with the core library's warning set
  • a README documenting the wire format, since that is the part the other end has to match

Then add add_subdirectory(<thing>) to extensions/CMakeLists.txt.

Clone this wiki locally