United Offensive protocol 22 wire-schema appendix¶
Purpose¶
This appendix contains the ordered constants required to implement the
compressed message and snapshot codecs described in
United Offensive network protocol. Descriptor order is part of the wire protocol: it
controls lastChanged interpretation and cannot be rearranged to match a
native structure declaration.
The Bits column means:
0: compact-or-full IEEE-754 float mode;-100: zero-or-16-bit-angle mode;any other value: the physical width is
abs(Bits);negative widths other than
-100do not add a separate sign bit.
All table indexes are zero-based.
Adaptive-Huffman tree update¶
The compressed connect request uses the following dynamic tree. These
rules are included because using a different adaptive-Huffman variant produces
an incompatible connect packet even when NYT handling and bit order look the
same.
Each node has left, right, parent, next, prev, head,
weight, and symbol. next/prev form the weight-ordered node
list. Nodes of one weight share a head slot pointing to that block’s
leader. The initial tree is one NYT node of weight zero; it is the tree root,
list head, list tail, and loc[NYT].
An existing symbol emits its root-to-leaf path (left zero, right one), then increments its leaf. A new symbol emits the NYT path, emits literal value bits 7 through 0, then adds a reference for that symbol. Decoder and encoder perform the same add-reference step after each decoded/encoded byte.
Adding a new symbol performs these operations in order:
Allocate a symbol leaf and then an internal node, both with weight one.
Insert the internal node immediately after NYT in the list, then insert the new leaf immediately after NYT. Join or allocate the corresponding weight-1 leader slots exactly as implied by the neighboring node.
Replace NYT at its former parent (or at the root) with the new internal node.
Set
internal.left = NYTandinternal.right = newLeaf; repair all three parent pointers and publishloc[symbol] = newLeaf.Increment the new internal node’s parent. The two new weight-1 nodes are not separately incremented.
Incrementing a node is recursive:
increment(node):
if node is null: return
if node.next exists and node.next.weight == node.weight:
leader = *node.head
if leader != node.parent:
swap their positions in the binary tree
swap leader and node in the linked list
if node.prev exists and node.prev.weight == node.weight:
*node.head = node.prev
else:
release node.head leader slot
node.weight += 1
if node.next exists and node.next.weight == node.weight:
node.head = node.next.head
else:
node.head = allocate_leader_slot()
*node.head = node
if node.parent exists:
increment(node.parent)
if node.prev == node.parent:
swap node and node.parent in the linked list
if *node.head == node:
*node.head = node.parent
Tree-position swaps exchange parent/child references without changing list
positions. List swaps exchange next/prev positions without changing
tree parents. Leader slots are recycled through a free list. This operation
order, especially the parent exception and the post-recursion list swap, is
protocol-significant.
Fixed-Huffman seed¶
The sequenced-message Huffman tree is initialized by adding byte symbol 0
seed[0] times, byte symbol 1 seed[1] times, and so on through symbol
255. Encoder and decoder build separate but identical trees. They do not
update these trees while transforming a message.
The values are shown as 16 rows of 16 symbols. The row label is the high hexadecimal nibble of the symbol; columns run from low nibble 0 through F.
0 1 2 3 4 5 6 7 8 9 A B C D E F
0 250315 41193 6292 7106 3730 3750 6110 23283 33317 6950 7838 9714 9257 17259 3949 1778
1 8288 1604 1590 1663 1100 1213 1238 1134 1749 1059 1246 1149 1273 4486 2805 3472
2 21819 1159 1670 1066 1043 1012 1053 1070 1726 888 1180 850 960 780 1752 3296
3 10630 4514 5881 2685 4650 3837 2093 1867 2584 1949 1972 940 1134 1788 1670 1206
4 5719 6128 7222 6654 3710 3795 1492 1524 2215 1140 1355 971 2180 1248 1328 1195
5 1770 1078 1264 1266 1168 965 1155 1186 1347 1228 1529 1600 2617 2048 2546 3275
6 2410 3585 2504 2800 2675 6146 3663 2840 14253 3164 2221 1687 3208 2739 3512 4796
7 4091 3515 5288 4016 7937 6031 5360 3924 4892 3743 4566 4807 5852 6400 6225 8291
8 23243 7838 7073 8935 5437 4483 3641 5256 5312 5328 5370 3492 2458 1694 1821 2121
9 1916 1149 1516 1367 1236 1029 1258 1104 1245 1006 1149 1025 1241 952 1287 997
A 1713 1009 1187 879 1099 929 1078 951 1656 930 1153 1030 1262 1062 1214 1060
B 1621 930 1106 912 1034 892 1158 990 1175 850 1121 903 1087 920 1144 1056
C 3462 2240 4397 12136 7758 1345 1307 3278 1950 886 1023 1112 1077 1042 1061 1071
D 1484 1001 1096 915 1052 995 1070 876 1111 851 1059 805 1112 923 1103 817
E 1899 1872 976 841 1127 956 1159 950 7791 954 1289 933 1127 3207 1020 927
F 1355 768 1040 745 952 805 1073 740 1013 805 1008 796 996 1057 11457 13504
Entity-state descriptor tables¶
Every entity record has the same 60-descriptor extent. A one-bit selector
immediately after the 10-bit entity number chooses the vehicle table only
when the transmitted entity type is ET_VEHICLE (numeric value 12). The
tables differ in both order and field modes.
Idx Normal entity field Bits Vehicle entity field Bits
0 pos.trTime 32 pos.trBase[2] 0
1 pos.trBase[0] 0 pos.trBase[0] 0
2 pos.trBase[1] 0 pos.trBase[1] 0
3 pos.trDelta[1] 0 apos.trBase[2] -100
4 pos.trDelta[0] 0 apos.trBase[1] -100
5 angles2[1] 0 apos.trBase[0] -100
6 apos.trBase[1] -100 angles2[1] -100
7 apos.trBase[0] -100 time -16
8 pos.trBase[2] 0 time2 6
9 pos.trDelta[2] 0 origin2[0] 8
10 eventSequence 8 origin2[2] 8
11 eType 8 angles2[0] -100
12 eFlags 24 origin2[1] -100
13 groundEntityNum 10 angles2[2] -100
14 legsAnim 10 leanf -100
15 clientNum 8 loopSound 8
16 apos.trBase[2] -100 eType 8
17 events[0] 8 eFlags 24
18 index 9 eventSequence 8
19 events[1] 8 events[0] 8
20 events[2] 8 events[1] 8
21 events[3] 8 events[2] 8
22 eventParm 8 events[3] 8
23 torsoAnim 10 eventParm 8
24 surfType 8 eventParms[0] 8
25 scale 8 eventParms[1] 8
26 otherEntityNum 10 eventParms[2] 8
27 fWaistPitch 0 eventParms[3] 8
28 pos.trType 8 iHeadIcon 6
29 xmodel 9 iHeadIconTeam 2
30 angles2[0] 0 animMovetype 8
31 fTorsoHeight 0 index 9
32 fTorsoPitch 0 torsoAnim 10
33 apos.trType 8 surfType 8
34 solid 24 scale 8
35 weapon 7 otherEntityNum 10
36 apos.trTime 32 fWaistPitch 0
37 apos.trDelta[0] 0 pos.trType 8
38 eventParms[0] 8 xmodel 9
39 pos.trDuration 32 fTorsoHeight 0
40 animMovetype 4 fTorsoPitch 0
41 eventParms[1] 8 apos.trType 8
42 apos.trDelta[2] 0 solid 24
43 eventParms[2] 8 weapon 7
44 eventParms[3] 8 apos.trTime 32
45 leanf 0 apos.trDelta[0] 0
46 apos.trDelta[1] 0 apos.trDelta[2] 0
47 loopSound 8 apos.trDelta[1] 0
48 attackerEntityNum 10 attackerEntityNum 10
49 iHeadIcon 4 apos.trDuration 32
50 iHeadIconTeam 2 constantLight 32
51 apos.trDuration 32 dmgFlags 32
52 time 32 pos.trTime 32
53 time2 32 pos.trDuration 32
54 origin2[0] 0 groundEntityNum 10
55 origin2[1] 0 legsAnim 10
56 origin2[2] 0 clientNum 8
57 angles2[2] 0 pos.trDelta[1] 0
58 constantLight 32 pos.trDelta[0] 0
59 dmgFlags 32 pos.trDelta[2] 0
Entity record grammar¶
The entity stream is sorted by number and terminated by the 10-bit value 1023. Every other record is:
entityNumber 10 bits
vehicleTable 1 bit
removed 1 bit
if !removed:
changed 1 bit
if changed:
lastChanged 8 bits # count, 1..60
fieldDelta[0 .. lastChanged-1]
An explicitly forced but unchanged record has changed=0. An omitted
unchanged record consumes no bits at all.
Client-state descriptor table¶
The 32-byte name is transmitted as eight raw 32-bit lanes. The first dword of
the native record, clientNum, is the separate six-bit record number and is
not a descriptor entry.
Idx Field Bits
0 team 2
1 name[0] 32
2 name[4] 32
3 attachModelIndex[0] 8
4 attachModelIndex[2] 8
5 attachModelIndex[3] 8
6 modelindex 8
7 attachModelIndex[1] 8
8 name[8] 32
9 name[12] 32
10 attachModelIndex[4] 8
11 name[16] 32
12 attachTagIndex[0] 5
13 attachTagIndex[1] 5
14 attachTagIndex[2] 5
15 attachTagIndex[3] 5
16 attachTagIndex[4] 5
17 attachModelIndex[5] 8
18 attachTagIndex[5] 5
19 name[20] 32
20 name[24] 32
21 name[28] 32
Client record grammar¶
recordPresent 1 bit # 0 terminates the client stream
if recordPresent:
clientNum 6 bits
removed 1 bit
if !removed:
changed 1 bit
if changed:
lastChanged 8 bits # count, 1..22
fieldDelta[0 .. lastChanged-1]
Player-state main descriptor table¶
The 114 main descriptors are followed by stats, ammo, clips, objectives, and HUD arrays as described in the main protocol document. The player-state field codec differs from the generic codec by omitting the generic explicit-zero bit for ordinary integer and float fields.
Idx Field Bits
0 commandTime 32
1 origin[1] 0
2 origin[0] 0
3 origin[2] 0
4 viewangles[1] -100
5 viewangles[0] -100
6 iCompassFriendInfo 32
7 eFlags 24
8 bobCycle 8
9 viewHeightCurrent 0
10 eventSequence 8
11 legsAnim 10
12 pm_flags 24
13 delta_angles[1] 16
14 velocity[1] 0
15 velocity[0] 0
16 iCompassTankInfo 32
17 speed 16
18 mins[0] 0
19 mins[1] 0
20 maxs[0] 0
21 maxs[1] 0
22 maxs[2] 0
23 proneViewHeight -8
24 crouchViewHeight -8
25 standViewHeight -8
26 deadViewHeight -8
27 walkSpeedScale 0
28 runSpeedScale 0
29 sprintSpeedScale 0
30 proneSpeedScale 0
31 crouchSpeedScale 0
32 strafeSpeedScale 0
33 backSpeedScale 0
34 leanSpeedScale 0
35 fatigueScale 0
36 lastSprintTime 0
37 friction 0
38 groundEntityNum 10
39 viewHeightTarget -8
40 events[0] 8
41 weapons[0] 32
42 weaponslots[0] 32
43 gravity 16
44 serverCursorHintString -8
45 viewmodelIndex 8
46 events[1] 8
47 events[2] 8
48 events[3] 8
49 delta_angles[0] 16
50 weapon 7
51 movementDir 8
52 viewHeightLerpTarget -8
53 weaponslots[4] 32
54 proneDirection 0
55 aimSpreadScale 0
56 weapAnim 10
57 jumpTime 32
58 velocity[2] 0
59 weaponTime -16
60 proneTorsoPitch 0
61 proneDirectionPitch 0
62 fTorsoPitch 0
63 fWaistPitch 0
64 fTorsoHeight 0
65 weaponstate 4
66 torsoTimer 16
67 torsoAnim 10
68 eventParms[0] 8
69 vLadderVec[0] 0
70 eventParms[3] 8
71 viewHeightLerpDown 1
72 weaponDelay -16
73 eventParms[1] 8
74 viewHeightLerpTime 32
75 eventParms[2] 8
76 vLadderVec[1] 0
77 fWeaponPosFrac 0
78 pm_type 8
79 legsTimer 16
80 fJumpOriginZ 0
81 leanf 0
82 damageEvent 8
83 damageYaw 8
84 damagePitch 8
85 damageCount 7
86 weaponrechamber[0] 32
87 grenadeTimeLeft -16
88 pm_time -16
89 iFoliageSoundTime 32
90 iFatigueSoundTime 32
91 deltaTime 32
92 serverCursorHint 8
93 serverCursorHintVal 8
94 shellshockIndex 4
95 shellshockTime 32
96 shellshockDuration 16
97 delta_angles[2] 16
98 vLadderVec[2] 0
99 clientNum 8
100 weapons[1] 32
101 weaponrechamber[1] 32
102 viewangles[2] -100
103 viewHeightLerpPosAdj 0
104 mins[2] 0
105 viewlocked 8
106 viewlocked_entNum 16
107 vehPos 4
108 vehType 4
109 vehMotion 2
110 weapons[2] 32
111 weapons[3] 32
112 weaponrechamber[2] 32
113 weaponrechamber[3] 32
Player-state auxiliary arrays¶
Stats have these wire widths:
Index |
Meaning |
Width |
|---|---|---|
0 |
health |
16 |
1 |
dead yaw |
16 |
2 |
maximum health |
16 |
3 |
identified client number |
6 |
4 |
identified client health |
16 |
5 |
spawn count |
8 |
ammo[64] and clips[64] are divided into four consecutive groups of
16. Each transmitted group begins with a 16-bit changed mask, then one 16-bit
value for every set bit in increasing item-index order.
Objective descriptor table¶
There are 16 objective records. The three-bit objective state is transmitted outside this table for every objective when the objective array is present.
Idx Field Bits
0 origin[0] 0
1 origin[1] 0
2 origin[2] 0
3 icon 12
4 entNum 10
5 teamNum 4
Objective state values established by the game are EMPTY=0, ACTIVE=1,
INVISIBLE=2, and CURRENT=4. The three-bit wire domain can represent
unnamed values as well; parsers should validate according to their policy.
HUD-element descriptor table¶
Player state contains two arrays of 63 HUD elements: archival first on the wire, then current. An element type of zero terminates the active prefix when the sender calculates the six-bit element count.
Idx Field Bits
0 color.rgba 32
1 type 4
2 fontScale 0
3 y 10
4 x 10
5 alignY 2
6 alignX 2
7 time 32
8 font 4
9 text 8
10 shaderIndex 8
11 width 10
12 height 10
13 sort 0
14 fromColor.rgba 32
15 fadeStartTime 32
16 fadeTime 16
17 scaleStartTime 32
18 scaleTime 16
19 fromHeight 10
20 value 0
21 label 8
22 fromWidth 10
23 moveStartTime 32
24 moveTime 16
25 fromX 10
26 fromY 10
27 duration 32
28 SCOORD 32
29 TCOORD 32
The five-bit lastField value is an inclusive descriptor index. A valid
value is therefore 0..29. Each element carries deltas for every descriptor
from zero through lastField; unchanged entries within that prefix still
consume their changed bit.
Generic delta record pseudocode¶
The following pseudocode summarizes the common entity/client record body. It does not include the entity vehicle-table bit or the client’s stream-present bit:
if removed:
write_bit(1)
return
write_bit(0)
lastChanged = one_past_highest_different_descriptor(base, value)
if lastChanged == 0:
write_bit(0)
return
write_bit(1)
write_byte(lastChanged)
for i in 0 .. lastChanged-1:
write_field_delta(descriptor[i], base, value)
Generic field pseudocode¶
if old_bits == new_bits:
write_bit(0)
return
write_bit(1)
if descriptor.bits == 0: # float
if new_float == 0:
write_bit(0)
else:
write_bit(1)
if new_float is exact integer in [-4096, 4095]:
write_bit(0)
biased = new_integer + 4096
write_bits(biased, 5)
write_byte(biased >> 5)
else:
write_bit(1)
write_u32(raw_float_bits)
return
if descriptor.bits == -100: # angle16
if new_bits == 0:
write_bit(0)
else:
write_bit(1)
write_angle16(new_float)
return
if new_bits == 0:
write_bit(0)
else:
write_bit(1)
write_bits(new_bits, abs(descriptor.bits))
The physical writer emits a non-byte-multiple low remainder through the bit
cursor, then emits complete bytes through the byte cursor. This is equivalent
to write_bits only if the implementation reproduces the protocol’s dual
cursor behavior described in the main document.
Player-state field pseudocode¶
The player-state main table changes the value body as follows:
if old_bits == new_bits:
write_bit(0)
return
write_bit(1)
if descriptor.bits == 0:
if new_float is exact integer in [-4096, 4095]:
write_bit(0)
biased = new_integer + 4096
write_bits(biased, 5)
write_byte(biased >> 5)
else:
write_bit(1)
write_u32(raw_float_bits)
else if descriptor.bits == -100:
write_bit(new_bits != 0)
if new_bits != 0:
write_angle16(new_float)
else:
write_bits(new_bits, abs(descriptor.bits))
Unlike the generic field codec, a changed ordinary integer can encode zero without another presence bit, and a changed float zero uses the compact integer form.