Skip to content

Field Paths & Huffman Coding

When an entity is updated, the payload does not send a full copy of every field. It sends a list of (field path, value) pairs — only the fields that changed. A field path is a compact address into the entity's field tree, and these addresses are Huffman-coded for maximum compression.


What a field path is

An entity's fields are organised in a hierarchy. Top-level fields are at depth 0; nested sub-serialiser fields are at depth 1, 2, etc. A field path is an array of up to 7 integers, each indexing one level of the hierarchy:

Entity: CDOTA_Unit_Hero_Axe
  field [0]:  m_iTeamNum        (simple int)
  field [1]:  m_iHealth         (simple int)
  field [2]:  m_flMana          (quantized float)
  field [3]:  CBodyComponent    (sub-serialiser)
    field [0]: m_cellX          (uint32)
    field [1]: m_cellY          (uint32)
    field [2]: m_vecX           (float)
    ...

Path [1]       → m_iHealth
Path [3, 0]    → CBodyComponent.m_cellX
Path [3, 2]    → CBodyComponent.m_vecX

The path always starts at the root of the serialiser.


Initial state and mutation

Field paths are decoded incrementally within a single entity packet. The path starts at [-1, 0, 0, 0, 0, 0, 0] with last = 0 (the index of the last active element). A sequence of operations mutates the current path state. When the sequence ends, you have read all the (path, value) pairs for this entity.

Each operation is read from the bit stream as a Huffman code and executed immediately. Operations may:

  • Increment one or more path elements by a fixed amount
  • Push a new level (go one level deeper into a sub-serialiser)
  • Pop one or more levels
  • Advance a specific element by a delta read from subsequent bits

The 40 operations

There are exactly 40 operations, each identified by an index 0–39. They are named descriptively, e.g.:

Op Name What it does
0 PlusOne Increment the last element by 1
1 PlusTwo Increment the last element by 2
2 PlusThree Increment the last element by 3
3 PlusFour Increment the last element by 4
4 PlusN Read 3 bits → increment last element by (n+5)
5 PushOneLeftDeltaZeroRightZero Push new level; last element unchanged
6 PushOneLeftDeltaZeroRightNonZero Push; read 3 bits for new element delta
7 PushOneLeftDeltaOneRightZero Push; increment by 1 at new level
... ... ...
39 FieldPathEncodeFinish End of field path list for this entity

PlusOne is by far the most common operation (weight 36271) because successive fields are usually updated together. It gets the shortest Huffman code.

The full list is in src/gem/field_path.py, translated from refs/manta/field_path.go.


Huffman coding

Huffman coding assigns shorter bit sequences to more frequent symbols. The 40 operations have pre-defined weights (frequencies observed across many replays). gem builds a Huffman tree from these weights once at startup and uses it for every entity decode.

Weight table excerpt (highest to lowest frequency):

PlusOne                   36271
FieldPathEncodeFinish     25474
PushOneLeftDeltaOneRightZero   10334
PlusN                      4128
...
NonTopologicalConsecutive   1

In a Huffman tree, the most frequent symbol (PlusOne, weight 36271) gets the shortest code — typically 1 or 2 bits. The rarest symbols get codes up to 17 bits long.


O(1) decode table

Walking a Huffman tree bit-by-bit is slow (one branch per bit). gem uses a flat 17-bit decode table instead: peek the next 17 bits from the stream, use them as an index into a 131072-entry table, and get back (op_index, bits_consumed) in O(1).

# Pseudo-code for the decode loop
while True:
    bits = reader.peek_bits(17)
    op_index, consumed = DECODE_TABLE[bits]
    reader.skip_bits(consumed)
    execute_op(op_index, path)
    if op_index == FINISH_OP:
        break

The table is built once in src/gem/field_path.py at module load time.


Reading a full entity delta

The complete sequence for one entity update:

1. Read paths until FieldPathEncodeFinish fires:
   a. Peek 17 bits → look up (op, bits_consumed) → skip those bits → mutate path
   b. Repeat

2. For each path collected:
   a. Navigate the serialiser tree using the path indices
   b. Call the field's decoder(bit_reader) → value
   c. Store the value in the entity's FieldState

3. Fire EntityTracker callbacks

The serialiser navigation uses the path indices to walk the Serializer.fields list. For arrays (FIXED_ARRAY, VARIABLE_ARRAY), path[depth+1] is the element index. For sub-serialisers (FIXED_TABLE, VARIABLE_TABLE), path[depth+1] is the field index within the sub-serialiser.


gem implementation

Source: src/gem/field_path.py

The FieldPath class uses __slots__ for performance (it is allocated for every entity update). The read_field_paths(reader, serializer) function returns a list of (FieldPath, Field) pairs, ready for decoding.

The Huffman decode table (_HUFFMAN_TABLE) is a pre-built list[tuple[int, int]] of 131072 entries, one per 17-bit bit-pattern.

Reference: refs/manta/field_path.go