Skip to content

Send Tables

Parses flattened serializers into the schema tree used to decode packet entities and their fields.

See also: Send Tables & Schema

gem.sendtable.parse_send_tables(data: bytes, game_build: int = 0) -> dict[str, Serializer]

Parse a CDemoSendTables payload into a serializer dictionary.

The payload is a varuint32 length prefix followed by a serialized CSVCMsg_FlattenedSerializer protobuf message.

Parameters:

Name Type Description Default
data bytes

Raw bytes from a CDemoSendTables outer message.

required
game_build int

Server build number (from CSVCMsg_ServerInfo). Used to select build-range field patches. Pass 0 to apply only the always-on patches.

0

Returns:

Type Description
dict[str, Serializer]

Mapping of serializer name → Serializer, containing every entity

dict[str, Serializer]

class schema defined in this replay.

Source code in src/gem/sendtable.py
def parse_send_tables(data: bytes, game_build: int = 0) -> dict[str, Serializer]:
    """Parse a CDemoSendTables payload into a serializer dictionary.

    The payload is a varuint32 length prefix followed by a serialized
    ``CSVCMsg_FlattenedSerializer`` protobuf message.

    Args:
        data: Raw bytes from a ``CDemoSendTables`` outer message.
        game_build: Server build number (from ``CSVCMsg_ServerInfo``).
            Used to select build-range field patches.  Pass 0 to apply
            only the always-on patches.

    Returns:
        Mapping of serializer name → Serializer, containing every entity
        class schema defined in this replay.
    """
    # The outer payload is a CDemoSendTables protobuf whose .data field
    # contains a varuint32-prefixed CSVCMsg_FlattenedSerializer.
    outer = CDemoSendTables()
    outer.ParseFromString(data)
    inner = outer.data

    # Strip the varuint32 length prefix from the inner buffer
    pos = 0
    size = 0
    shift = 0
    while True:
        b = inner[pos]
        pos += 1
        size |= (b & 0x7F) << shift
        if not (b & 0x80):
            break
        shift += 7

    msg = CSVCMsg_FlattenedSerializer()
    msg.ParseFromString(inner[pos : pos + size])

    symbols = list(msg.symbols)

    def sym(idx: int | None) -> str:
        return symbols[idx] if idx is not None else ""

    # Select applicable patches
    active_patches = [p for p in _FIELD_PATCHES if p.should_apply(game_build)]

    # Build field type cache (shared across fields with the same type string)
    field_type_cache: dict[str, FieldType] = {}

    # Build field cache (shared across serializers that reference the same field index)
    field_cache: dict[int, Field] = {}

    serializers: dict[str, Serializer] = {}

    for s_proto in msg.serializers:
        s_name = symbols[s_proto.serializer_name_sym]
        s_version = s_proto.serializer_version
        serializer = Serializer(name=s_name, version=s_version)

        for idx in s_proto.fields_index:
            if idx not in field_cache:
                fp = msg.fields[idx]

                encode_flags = fp.encode_flags if fp.HasField("encode_flags") else None
                bit_count = fp.bit_count if fp.HasField("bit_count") else None
                low_value = fp.low_value if fp.HasField("low_value") else None
                high_value = fp.high_value if fp.HasField("high_value") else None

                send_node = sym(fp.send_node_sym if fp.HasField("send_node_sym") else None)
                if send_node == "(root)":
                    send_node = ""

                f = Field(
                    var_name=sym(fp.var_name_sym if fp.HasField("var_name_sym") else None),
                    var_type=sym(fp.var_type_sym if fp.HasField("var_type_sym") else None),
                    send_node=send_node,
                    serializer_name=sym(
                        fp.field_serializer_name_sym
                        if fp.HasField("field_serializer_name_sym")
                        else None
                    ),
                    serializer_version=fp.field_serializer_version,
                    encoder=sym(fp.var_encoder_sym if fp.HasField("var_encoder_sym") else None),
                    encode_flags=encode_flags,
                    bit_count=bit_count,
                    low_value=low_value,
                    high_value=high_value,
                )

                # Patch parent name for old builds
                if game_build <= 990:
                    f.parent_name = s_name

                # Parse and cache the field type
                if f.var_type not in field_type_cache:
                    field_type_cache[f.var_type] = _parse_field_type(f.var_type)
                f.field_type = field_type_cache[f.var_type]

                # Resolve sub-serializer
                if f.serializer_name:
                    f.serializer = serializers.get(f.serializer_name)

                # Apply build-range patches before determining the model
                for patch in active_patches:
                    patch.patch(f)

                # Determine field model
                if f.serializer is not None:
                    if f.field_type.pointer or f.field_type.base_type in _POINTER_TYPES:
                        f.set_model(FIELD_MODEL_FIXED_TABLE)
                    else:
                        f.set_model(FIELD_MODEL_VARIABLE_TABLE)
                elif f.field_type.count > 0 and f.field_type.base_type != "char":
                    f.set_model(FIELD_MODEL_FIXED_ARRAY)
                elif f.field_type.base_type in ("CUtlVector", "CNetworkUtlVectorBase"):
                    f.set_model(FIELD_MODEL_VARIABLE_ARRAY)
                else:
                    f.set_model(FIELD_MODEL_SIMPLE)

                field_cache[idx] = f

            serializer.fields.append(field_cache[idx])

        serializers[s_name] = serializer

    return serializers

gem.sendtable.Serializer dataclass

A named, versioned entity class schema with an ordered list of fields.

Attributes:

Name Type Description
name str

Entity class name (e.g. "CDOTA_Unit_Hero_Axe").

version int

Schema version integer.

fields list[Field]

Ordered list of Field objects.

Source code in src/gem/sendtable.py
@dataclass
class Serializer:
    """A named, versioned entity class schema with an ordered list of fields.

    Attributes:
        name: Entity class name (e.g. ``"CDOTA_Unit_Hero_Axe"``).
        version: Schema version integer.
        fields: Ordered list of Field objects.
    """

    name: str
    version: int
    fields: list[Field] = field(default_factory=list)

    def __repr__(self) -> str:
        return f"Serializer({self.name!r}, v{self.version}, {len(self.fields)} fields)"

gem.sendtable.Field dataclass

One property of a serializer, with its type and decoder.

Attributes:

Name Type Description
var_name str

Property name (e.g. "m_iHealth").

var_type str

Raw type string from the send table.

send_node str

Network send node path (empty string for root).

serializer_name str

Name of the associated sub-serializer, if any.

serializer_version int

Version of the associated sub-serializer.

encoder str

Encoder hint (e.g. "coord", "simtime").

encode_flags int | None

QFD encode flags bitmask.

bit_count int | None

QFD / angle bit width.

low_value float | None

QFD lower bound.

high_value float | None

QFD upper bound.

parent_name str

Serializer that owns this field (set for build ≤ 990).

field_type FieldType

Parsed FieldType.

serializer Serializer | None

Resolved sub-serializer for nested types, or None.

model int

One of the FIELD_MODEL_* constants.

decoder FieldDecoder | None

Callable for simple / fixed-array fields.

base_decoder FieldDecoder | None

Callable for the length prefix of array / table fields.

child_decoder FieldDecoder | None

Callable for variable-array elements.

Source code in src/gem/sendtable.py
@dataclass
class Field:
    """One property of a serializer, with its type and decoder.

    Attributes:
        var_name: Property name (e.g. ``"m_iHealth"``).
        var_type: Raw type string from the send table.
        send_node: Network send node path (empty string for root).
        serializer_name: Name of the associated sub-serializer, if any.
        serializer_version: Version of the associated sub-serializer.
        encoder: Encoder hint (e.g. ``"coord"``, ``"simtime"``).
        encode_flags: QFD encode flags bitmask.
        bit_count: QFD / angle bit width.
        low_value: QFD lower bound.
        high_value: QFD upper bound.
        parent_name: Serializer that owns this field (set for build ≤ 990).
        field_type: Parsed FieldType.
        serializer: Resolved sub-serializer for nested types, or None.
        model: One of the FIELD_MODEL_* constants.
        decoder: Callable for simple / fixed-array fields.
        base_decoder: Callable for the length prefix of array / table fields.
        child_decoder: Callable for variable-array elements.
    """

    var_name: str
    var_type: str
    send_node: str
    serializer_name: str
    serializer_version: int
    encoder: str
    encode_flags: int | None
    bit_count: int | None
    low_value: float | None
    high_value: float | None
    parent_name: str = ""
    field_type: FieldType = field(default_factory=lambda: FieldType(""))
    serializer: Serializer | None = None
    model: int = FIELD_MODEL_SIMPLE
    decoder: FieldDecoder | None = None
    base_decoder: FieldDecoder | None = None
    child_decoder: FieldDecoder | None = None

    def set_model(self, model: int) -> None:
        """Assign the field model and wire up the appropriate decoders.

        Args:
            model: One of the FIELD_MODEL_* constants.
        """
        from gem.field_decoder import boolean_decoder, unsigned_decoder

        self.model = model
        if model in (FIELD_MODEL_SIMPLE, FIELD_MODEL_FIXED_ARRAY):
            self.decoder = find_decoder(self)
        elif model == FIELD_MODEL_FIXED_TABLE:
            self.base_decoder = boolean_decoder
        elif model == FIELD_MODEL_VARIABLE_ARRAY:
            self.base_decoder = unsigned_decoder
            generic_base = (
                self.field_type.generic_type.base_type if self.field_type.generic_type else ""
            )
            self.child_decoder = find_decoder_by_base_type(generic_base)
        elif model == FIELD_MODEL_VARIABLE_TABLE:
            self.base_decoder = unsigned_decoder

    def model_name(self) -> str:
        """Return a human-readable model name for debugging.

        Returns:
            One of ``"simple"``, ``"fixed-array"``, ``"fixed-table"``,
            ``"variable-array"``, ``"variable-table"``.
        """
        return {
            FIELD_MODEL_SIMPLE: "simple",
            FIELD_MODEL_FIXED_ARRAY: "fixed-array",
            FIELD_MODEL_FIXED_TABLE: "fixed-table",
            FIELD_MODEL_VARIABLE_ARRAY: "variable-array",
            FIELD_MODEL_VARIABLE_TABLE: "variable-table",
        }.get(self.model, "unknown")

set_model(model: int) -> None

Assign the field model and wire up the appropriate decoders.

Parameters:

Name Type Description Default
model int

One of the FIELD_MODEL_* constants.

required
Source code in src/gem/sendtable.py
def set_model(self, model: int) -> None:
    """Assign the field model and wire up the appropriate decoders.

    Args:
        model: One of the FIELD_MODEL_* constants.
    """
    from gem.field_decoder import boolean_decoder, unsigned_decoder

    self.model = model
    if model in (FIELD_MODEL_SIMPLE, FIELD_MODEL_FIXED_ARRAY):
        self.decoder = find_decoder(self)
    elif model == FIELD_MODEL_FIXED_TABLE:
        self.base_decoder = boolean_decoder
    elif model == FIELD_MODEL_VARIABLE_ARRAY:
        self.base_decoder = unsigned_decoder
        generic_base = (
            self.field_type.generic_type.base_type if self.field_type.generic_type else ""
        )
        self.child_decoder = find_decoder_by_base_type(generic_base)
    elif model == FIELD_MODEL_VARIABLE_TABLE:
        self.base_decoder = unsigned_decoder

model_name() -> str

Return a human-readable model name for debugging.

Returns:

Type Description
str

One of "simple", "fixed-array", "fixed-table",

str

"variable-array", "variable-table".

Source code in src/gem/sendtable.py
def model_name(self) -> str:
    """Return a human-readable model name for debugging.

    Returns:
        One of ``"simple"``, ``"fixed-array"``, ``"fixed-table"``,
        ``"variable-array"``, ``"variable-table"``.
    """
    return {
        FIELD_MODEL_SIMPLE: "simple",
        FIELD_MODEL_FIXED_ARRAY: "fixed-array",
        FIELD_MODEL_FIXED_TABLE: "fixed-table",
        FIELD_MODEL_VARIABLE_ARRAY: "variable-array",
        FIELD_MODEL_VARIABLE_TABLE: "variable-table",
    }.get(self.model, "unknown")

gem.sendtable.FieldType dataclass

Parsed representation of a C++ field type string.

Examples::

"uint32"               → base_type="uint32"
"CUtlVector< int32 >"  → base_type="CUtlVector", generic_type=FieldType("int32")
"CHandle[24]"          → base_type="CHandle", count=24
"CBodyComponent*"      → base_type="CBodyComponent", pointer=True

Attributes:

Name Type Description
base_type str

The root type name without generics, pointer, or array.

generic_type FieldType | None

The inner type for generic containers, or None.

pointer bool

True if the field is a pointer type.

count int

Array element count (0 = not an array).

Source code in src/gem/sendtable.py
@dataclass
class FieldType:
    """Parsed representation of a C++ field type string.

    Examples::

        "uint32"               → base_type="uint32"
        "CUtlVector< int32 >"  → base_type="CUtlVector", generic_type=FieldType("int32")
        "CHandle[24]"          → base_type="CHandle", count=24
        "CBodyComponent*"      → base_type="CBodyComponent", pointer=True

    Attributes:
        base_type: The root type name without generics, pointer, or array.
        generic_type: The inner type for generic containers, or None.
        pointer: True if the field is a pointer type.
        count: Array element count (0 = not an array).
    """

    base_type: str
    generic_type: FieldType | None = None
    pointer: bool = False
    count: int = 0

    def __str__(self) -> str:
        s = self.base_type
        if self.generic_type:
            s += f"<{self.generic_type}>"
        if self.pointer:
            s += "*"
        if self.count:
            s += f"[{self.count}]"
        return s