SendTable Layer
This page explains src/gem/sendtable.py, the schema-construction layer.
Prerequisites:
Where this layer sits
sendtable.py runs when parser receives outer DEM_SendTables.
Its job is to convert raw send-table payloads into a runtime schema:
dict[str, Serializer]That schema is then used by entities.py to decode field deltas.
Input and output
Input:
- bytes from
CDemoSendTablesouter payload (data: bytes) - optional
game_buildfor build-specific patches
Output:
- mapping of serializer name ->
Serializer - each serializer contains ordered
Fielddefinitions with decoders wired
Constants explained
| Constant | Value | Meaning | Why it matters |
|---|---|---|---|
_POINTER_TYPES | set of type names | Known embedded-by-pointer types in Source 2 schema. | Forces fixed-table modeling for these fields. |
_ITEM_COUNTS | e.g. MAX_ITEM_STOCKS: 8 | Engine array-size symbols -> numeric counts. | Converts symbolic array lengths into concrete sizes. |
FIELD_MODEL_SIMPLE | 0 | Scalar field model. | Direct single-value decode path. |
FIELD_MODEL_FIXED_ARRAY | 1 | Fixed-length array model. | Decoder applies repeated element decode with known length. |
FIELD_MODEL_FIXED_TABLE | 2 | Pointer-like nested table model. | Nested serializer with fixed presence model. |
FIELD_MODEL_VARIABLE_ARRAY | 3 | Variable-length array model. | Needs length prefix + child element decoder. |
FIELD_MODEL_VARIABLE_TABLE | 4 | Variable nested table model. | Nested serializer with variable instances. |
_FIELD_TYPE_RE | regex | Parses C++-style type strings (generic/pointer/array). | Turns raw type text into structured FieldType. |
Core data structures
FieldType
Represents parsed type metadata from strings like:
uint32CUtlVector< int32 >CHandle[24]CBodyComponent*
Fields:
base_typegeneric_typepointercount
Field
Represents one property in a serializer schema.
Key things it carries:
- name/type metadata (
var_name,var_type, encoder hints) - linked nested serializer (if any)
- field model (
simple,fixed-array, etc) - concrete decoder functions selected by
set_model(...)
set_model is the bridge from schema metadata to actual decode behavior.
Serializer
Container for one class schema:
nameversion- ordered
fields
entities.py depends on this order for field path resolution and field-state decode.
Build-range patch subsystem
sendtable.py includes patch hooks to match known build-specific quirks.
Patch components:
_FieldPatch(min_build, max_build, patch_fn)_make_patches()defines patch rulesactive_patchesselected bygame_build
Examples of patched behavior:
- force encoder hints (
coord,normal,QAngle,simtime,runetime) - adjust special ranges (example: mana max for old builds)
- patch 64-bit encoded fields in specific build windows
Why patches exist:
- replay schema can be inconsistent across game builds
- these patches normalize behavior to keep decode parity with reference parsers
parse_send_tables(...) step-by-step
Core flow:
- Parse outer protobuf
CDemoSendTables. - Read varuint length prefix in
outer.data. - Parse inner
CSVCMsg_FlattenedSerializerpayload. - Build symbol table (
msg.symbols) for string lookups. - Select active build patches.
- Build caches:
- type cache (
str -> FieldType) - field cache (
field_index -> Field)
- type cache (
- Iterate each serializer in protobuf and populate
Serializer.fields. - For each field:
- resolve symbol-backed names/types/encoders
- parse
FieldType - resolve nested serializer (if known)
- apply active patches
- choose model + wire decoder(s)
- Return final serializer mapping.
Field model decision logic
Decision order in code:
- If field has nested serializer:
- pointer / pointer-type ->
FIELD_MODEL_FIXED_TABLE - otherwise ->
FIELD_MODEL_VARIABLE_TABLE
- pointer / pointer-type ->
- Else if
count > 0and notchar->FIELD_MODEL_FIXED_ARRAY - Else if base type is vector container (
CUtlVector,CNetworkUtlVectorBase) ->FIELD_MODEL_VARIABLE_ARRAY - Else ->
FIELD_MODEL_SIMPLE
This decision is critical because entities.py decode path depends on model shape.
Real output snapshot
Using fixture tests/fixtures/8520014563.dem:
serializer_count 3224
0 CEntityIdentity 0 1
1 CBodyComponentBaseAnimating 2 28
2 CRenderComponent 0 0
3 CDestructiblePartsSystemComponent 0 3
4 EntityRenderAttribute_t 0 2
sample_field CEntityIdentity m_nameStringableIndex int32 simpleHow to read it:
- Total serializers available for replay decode.
- Each serializer has version + field count.
- Sample field shows resolved name, type, and chosen model.
What can go wrong at this layer
- Wrong symbol/type parsing -> decoders wired to wrong model.
- Missing/incorrect build patch -> subtle field decode drift.
- Wrong nested serializer linkage -> broken table/array path decode.
Symptoms usually appear later as entity fields being wrong or missing.