@dcl/protocol 1.0.0-27226386025.commit-c056d32 → 1.0.0-27358757907.commit-3c70e8a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,3 +67,201 @@ In this case, there is no problem with when each PR is merged. It's recommendabl
67
67
  ## Comms
68
68
 
69
69
  TODO
70
+
71
+ ---
72
+
73
+ # Bitwise Serialization Plugin (`protoc-gen-bitwise`)
74
+
75
+ A custom protoc plugin that generates C# partial classes with typed float
76
+ accessors for quantized `uint32` fields in high-frequency MMO networking
77
+ messages (position deltas, player input, etc.). It runs alongside
78
+ `--csharp_out` in the same protoc invocation; the two output files coexist
79
+ via C# `partial class`.
80
+
81
+ ## How it works
82
+
83
+ Protobuf encodes `uint32` values as varints, which are already compact for
84
+ small values: a value up to 2¹⁴−1 costs 2 bytes, up to 2²¹−1 costs 3 bytes.
85
+ Rather than a separate binary packing layer, the plugin leverages this:
86
+
87
+ 1. Declare quantized fields as `uint32` in the `.proto` schema and annotate
88
+ them with `[(decentraland.common.quantized)]` to specify the float range
89
+ and bit resolution.
90
+ 2. `--csharp_out` generates the standard protobuf class with the raw `uint32`
91
+ property (e.g. `PositionX`).
92
+ 3. `--bitwise_out` (this plugin) generates a `partial class` extension with a
93
+ cached float accessor (e.g. `PositionXQuantized`) that encodes/decodes
94
+ transparently via `Quantize.Encode` / `Quantize.Decode`.
95
+
96
+ The wire representation is a standard protobuf message — any protobuf-capable
97
+ client can read it without knowledge of the plugin.
98
+
99
+ ## Prerequisites
100
+
101
+ | Requirement | Version |
102
+ |---|---|
103
+ | Python | 3.10+ |
104
+ | `protobuf` Python package | 4.x or 3.20+ |
105
+ | `protoc` | 3.19+ |
106
+
107
+ ```bash
108
+ pip install protobuf
109
+ ```
110
+
111
+ ## Step 1 — Annotate your `.proto` file
112
+
113
+ Declare quantized fields as `uint32` and import `options.proto`:
114
+
115
+ ```protobuf
116
+ syntax = "proto3";
117
+
118
+ import "decentraland/common/options.proto";
119
+
120
+ package decentraland.kernel.comms.v3;
121
+
122
+ message PositionDelta {
123
+ // Float range [-100, 100] quantized to 16 bits ≈ 0.003-unit precision.
124
+ // Stored as uint32 on the wire; protobuf encodes it as a 3-byte varint.
125
+ uint32 dx = 1 [(decentraland.common.quantized) = { min: -100.0, max: 100.0, bits: 16 }];
126
+ uint32 dy = 2 [(decentraland.common.quantized) = { min: -100.0, max: 100.0, bits: 16 }];
127
+ uint32 dz = 3 [(decentraland.common.quantized) = { min: -100.0, max: 100.0, bits: 16 }];
128
+
129
+ // Unannotated uint32: protobuf varint encodes small values compactly by default.
130
+ uint32 entity_id = 4 [(decentraland.common.bit_packed) = { bits: 20 }];
131
+ }
132
+ ```
133
+
134
+ ### Annotation reference
135
+
136
+ | Annotation | Target type | Parameters | Effect |
137
+ |---|---|---|---|
138
+ | `[(decentraland.common.quantized)]` | `uint32` | `min`, `max`, `bits` | Plugin emits a cached `float {Name}Quantized` accessor |
139
+ | `[(decentraland.common.bit_packed)]` | `uint32` | `bits` | Documents the value range; protobuf handles varint compaction automatically |
140
+
141
+ ### Wire cost at worst-case (all bits set)
142
+
143
+ | Quantization bits | Max value | Varint bytes | Tag (field ≤ 15) | Total per field |
144
+ |---|---|---|---|---|
145
+ | 8 | 255 | 2 | 1 | 3 B |
146
+ | 12 | 4 095 | 2 | 1 | 3 B |
147
+ | 14 | 16 383 | 2 | 1 | 3 B |
148
+ | 16 | 65 535 | 3 | 1 | 4 B |
149
+ | 20 | 1 048 575 | 3 | 1 | 4 B |
150
+
151
+ Proto3 omits fields equal to their default value (0), so average cost is lower.
152
+
153
+ ## Step 2 — Run protoc
154
+
155
+ ```bash
156
+ protoc \
157
+ --proto_path=proto \
158
+ --proto_path=/path/to/google/protobuf/include \
159
+ --csharp_out=generated/cs \
160
+ --plugin=protoc-gen-bitwise=protoc-gen-bitwise/plugin.py \
161
+ --bitwise_out=generated/cs \
162
+ proto/decentraland/kernel/comms/v3/comms.proto
163
+ ```
164
+
165
+ The plugin emits one `*.Bitwise.cs` file (PascalCase, flat in the output
166
+ directory) for each `.proto` file that contains at least one `[(quantized)]`
167
+ field.
168
+
169
+ ## Step 3 — Copy the runtime
170
+
171
+ Copy `Quantize.cs` into your project:
172
+
173
+ ```
174
+ Assets/
175
+ └── Scripts/
176
+ └── Networking/
177
+ └── Bitwise/
178
+ └── Quantize.cs ← protoc-gen-bitwise/runtime/cs/Quantize.cs
179
+ ```
180
+
181
+ `Quantize.cs` lives in the `Decentraland.Networking.Bitwise` namespace and
182
+ provides two static methods used by the generated accessors:
183
+
184
+ ```csharp
185
+ public static class Quantize
186
+ {
187
+ public static uint Encode(float value, float min, float max, int bits);
188
+ public static float Decode(uint encoded, float min, float max, int bits);
189
+ }
190
+ ```
191
+
192
+ ## Step 4 — Use the generated code
193
+
194
+ The plugin emits a `partial class` that adds float accessors on top of the
195
+ standard protobuf-generated `uint32` properties:
196
+
197
+ ```csharp
198
+ using Decentraland.Kernel.Comms.V3;
199
+
200
+ // --- Build and send ---
201
+ var delta = new PositionDelta();
202
+ delta.DxQuantized = 3.14f; // encodes to uint32, stored in delta.Dx
203
+ delta.DyQuantized = 0f;
204
+ delta.DzQuantized = -7.5f;
205
+ delta.EntityId = 42u;
206
+
207
+ byte[] bytes = delta.ToByteArray(); // standard protobuf serialization
208
+ SendOnChannel1(bytes);
209
+
210
+ // --- Receive and read ---
211
+ var received = PositionDelta.Parser.ParseFrom(receivedBytes);
212
+ float x = received.DxQuantized; // decoded on first access, cached thereafter
213
+ float y = received.DyQuantized;
214
+ float z = received.DzQuantized;
215
+
216
+ // If raw uint32 fields are mutated directly after construction, invalidate the cache:
217
+ received.ResetDecodedCache();
218
+ ```
219
+
220
+ ## Generated file example
221
+
222
+ For the `PositionDelta` message above the plugin emits `PositionDelta.Bitwise.cs`:
223
+
224
+ ```csharp
225
+ // <auto-generated>
226
+ // Generated by protoc-gen-bitwise. DO NOT EDIT.
227
+ // Source: decentraland/kernel/comms/v3/comms.proto
228
+ // </auto-generated>
229
+
230
+ using Decentraland.Networking.Bitwise;
231
+
232
+ namespace Decentraland.Kernel.Comms.V3
233
+ {
234
+ public partial class PositionDelta
235
+ {
236
+ private float? _dx;
237
+ public float DxQuantized
238
+ {
239
+ get => _dx ??= Quantize.Decode(Dx, -100.0f, 100.0f, 16);
240
+ set { _dx = value; Dx = Quantize.Encode(value, -100.0f, 100.0f, 16); }
241
+ }
242
+
243
+ private float? _dy;
244
+ public float DyQuantized
245
+ {
246
+ get => _dy ??= Quantize.Decode(Dy, -100.0f, 100.0f, 16);
247
+ set { _dy = value; Dy = Quantize.Encode(value, -100.0f, 100.0f, 16); }
248
+ }
249
+
250
+ private float? _dz;
251
+ public float DzQuantized
252
+ {
253
+ get => _dz ??= Quantize.Decode(Dz, -100.0f, 100.0f, 16);
254
+ set { _dz = value; Dz = Quantize.Encode(value, -100.0f, 100.0f, 16); }
255
+ }
256
+
257
+ /// <summary>Clears all cached decoded values. Call after mutating raw uint32 fields directly.</summary>
258
+ public void ResetDecodedCache()
259
+ {
260
+ _dx = null;
261
+ _dy = null;
262
+ _dz = null;
263
+ }
264
+ }
265
+
266
+ } // namespace Decentraland.Kernel.Comms.V3
267
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcl/protocol",
3
- "version": "1.0.0-27226386025.commit-c056d32",
3
+ "version": "1.0.0-27358757907.commit-3c70e8a",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,10 +28,11 @@
28
28
  "protobufjs": "7.2.4"
29
29
  },
30
30
  "files": [
31
- "proto",
31
+ "proto/decentraland",
32
32
  "out-ts",
33
33
  "out-js",
34
- "public"
34
+ "public",
35
+ "protoc-gen-bitwise"
35
36
  ],
36
- "commit": "c056d32602c680fe758e8d6e3eedcd30b11aaaba"
37
+ "commit": "3c70e8a31967c1171f5470426006675976fcad11"
37
38
  }
@@ -0,0 +1,30 @@
1
+ syntax = "proto3";
2
+
3
+ package decentraland.common;
4
+
5
+ import "google/protobuf/descriptor.proto";
6
+
7
+ // Options for quantizing a float value stored as a uint32 field.
8
+ // The float is clamped to [min, max] and uniformly quantized to N bits;
9
+ // protobuf encodes the resulting uint32 as a varint on the wire.
10
+ // The generator emits a float {Name}Quantized accessor in the partial class.
11
+ message QuantizedFloatOptions {
12
+ float min = 1;
13
+ float max = 2;
14
+ uint32 bits = 3;
15
+ }
16
+
17
+ // Options for bit-packing an integer field into fewer than 32 bits.
18
+ message BitPackedOptions {
19
+ uint32 bits = 1;
20
+ }
21
+
22
+ extend google.protobuf.FieldOptions {
23
+ // Apply to uint32 fields to enable quantized float encoding.
24
+ // Example: uint32 dx = 1 [(decentraland.common.quantized) = { min: -100.0, max: 100.0, bits: 16 }];
25
+ QuantizedFloatOptions quantized = 50001;
26
+
27
+ // Apply to uint32 fields to pack into fewer bits.
28
+ // Example: uint32 entity_id = 4 [(decentraland.common.bit_packed) = { bits: 20 }];
29
+ BitPackedOptions bit_packed = 50002;
30
+ }
@@ -0,0 +1,132 @@
1
+ syntax = "proto3";
2
+
3
+ package decentraland.common;
4
+
5
+ import "decentraland/common/options.proto";
6
+
7
+ // High-frequency player state messages sent on Channel 1 (unreliable sequenced).
8
+ //
9
+ // Every message below uses the protoc-gen-bitwise annotations to minimise
10
+ // wire size. Wire costs are listed per-message so the trade-offs are clear.
11
+ //
12
+ // Annotation cheat-sheet:
13
+ // [(decentraland.common.quantized) = { min: F, max: F, bits: N }]
14
+ // → stores a float as a uint32 quantized to N bits over [min, max].
15
+ // → protobuf encodes the uint32 as a varint: values ≤ 2^14-1 cost 2 bytes,
16
+ // values ≤ 2^21-1 cost 3 bytes; the generated partial class adds a
17
+ // float {Name}Quantized accessor that encodes/decodes transparently.
18
+ // [(decentraland.common.bit_packed) = { bits: N }]
19
+ // → documents that this uint32 uses at most N bits; protobuf encodes it
20
+ // as a varint (same savings, no generated accessor needed).
21
+ // (no annotation)
22
+ // → standard protobuf encoding (bool/double/etc. at natural width).
23
+ //
24
+ // Varint byte costs at worst-case (all bits set):
25
+ // ≤ 7 bits → 1 byte (max 127)
26
+ // ≤ 14 bits → 2 bytes (max 16 383)
27
+ // ≤ 21 bits → 3 bytes (max 2 097 151)
28
+ // Proto3 omits fields equal to their default value (0), so average cost is lower.
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // PositionDelta — Δ position relative to last acknowledged full snapshot.
32
+ //
33
+ // Sent every client tick (~10 Hz) on Channel 1.
34
+ //
35
+ // Field | Type | Range | Q bits | Wire worst-case
36
+ // ------------|--------|----------------|--------|-----------------------
37
+ // dx | uint32 | [-100, 100] | 16 | tag 1B + varint 3B = 4B
38
+ // dy | uint32 | [-100, 100] | 16 | tag 1B + varint 3B = 4B
39
+ // dz | uint32 | [-100, 100] | 16 | tag 1B + varint 3B = 4B
40
+ // entity_id | uint32 | [0, 1 048 575] | 20 | tag 1B + varint 3B = 4B
41
+ // sequence | uint32 | [0, 4 095] | 12 | tag 1B + varint 2B = 3B
42
+ // ---------------------------------------------------------------------------
43
+ // Worst-case: 19 B (vs. 20 B raw: 3×float + 2×uint32)
44
+ // Step: dx/dy/dz ≈ 0.003 units
45
+ // ---------------------------------------------------------------------------
46
+ message PositionDelta {
47
+ uint32 dx = 1 [(decentraland.common.quantized) = { min: -100.0, max: 100.0, bits: 16 }];
48
+ uint32 dy = 2 [(decentraland.common.quantized) = { min: -100.0, max: 100.0, bits: 16 }];
49
+ uint32 dz = 3 [(decentraland.common.quantized) = { min: -100.0, max: 100.0, bits: 16 }];
50
+ uint32 entity_id = 4 [(decentraland.common.bit_packed) = { bits: 20 }];
51
+ uint32 sequence = 5 [(decentraland.common.bit_packed) = { bits: 12 }];
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // PlayerInput — client input snapshot for server-side reconciliation.
56
+ //
57
+ // Sent every client frame (~30 Hz) on Channel 1.
58
+ //
59
+ // Field | Type | Range | Q bits | Wire worst-case
60
+ // ------------|--------|----------------|--------|-----------------------
61
+ // move_x | uint32 | [-1, 1] | 8 | tag 1B + varint 2B = 3B
62
+ // move_z | uint32 | [-1, 1] | 8 | tag 1B + varint 2B = 3B
63
+ // yaw | uint32 | [-180, 180] | 12 | tag 1B + varint 2B = 3B
64
+ // buttons | uint32 | bitmask | 8 | tag 1B + varint 2B = 3B
65
+ // sequence | uint32 | [0, 4 095] | 12 | tag 1B + varint 2B = 3B
66
+ // ---------------------------------------------------------------------------
67
+ // Worst-case: 15 B (vs. 20 B raw: 3×float + 2×uint32)
68
+ // Step: move_x/move_z ≈ 0.008; yaw ≈ 0.088°
69
+ // ---------------------------------------------------------------------------
70
+ message PlayerInput {
71
+ // Normalised joystick axes in [-1, 1].
72
+ uint32 move_x = 1 [(decentraland.common.quantized) = { min: -1.0, max: 1.0, bits: 8 }];
73
+ uint32 move_z = 2 [(decentraland.common.quantized) = { min: -1.0, max: 1.0, bits: 8 }];
74
+
75
+ // Horizontal look direction in degrees.
76
+ uint32 yaw = 3 [(decentraland.common.quantized) = { min: -180.0, max: 180.0, bits: 12 }];
77
+
78
+ // Bitmask of active buttons (see ButtonFlags below).
79
+ uint32 buttons = 4 [(decentraland.common.bit_packed) = { bits: 8 }];
80
+
81
+ // Rolling input sequence number used by the server for reconciliation.
82
+ uint32 sequence = 5 [(decentraland.common.bit_packed) = { bits: 12 }];
83
+ }
84
+
85
+ // Bitmask values for PlayerInput.buttons.
86
+ enum ButtonFlags {
87
+ BF_NONE = 0;
88
+ BF_JUMP = 1; // bit 0
89
+ BF_SPRINT = 2; // bit 1
90
+ BF_INTERACT = 4; // bit 2
91
+ BF_EMOTE = 8; // bit 3
92
+ BF_CROUCH = 16; // bit 4
93
+ // bits 5-7 reserved
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // AvatarStateSnapshot — full authoritative state, sent on Channel 0 (reliable)
98
+ // or on resync requests. Demonstrates wider ranges and mixed encodings.
99
+ //
100
+ // Field | Type | Range | Q bits | Wire worst-case
101
+ // -----------------|--------|------------------|--------|-----------------------
102
+ // x | uint32 | [-4096, 4096] | 16 | tag 1B + varint 3B = 4B
103
+ // y | uint32 | [-256, 256] | 14 | tag 1B + varint 2B = 3B
104
+ // z | uint32 | [-4096, 4096] | 16 | tag 1B + varint 3B = 4B
105
+ // pitch | uint32 | [-90, 90] | 10 | tag 1B + varint 2B = 3B
106
+ // yaw | uint32 | [-180, 180] | 12 | tag 1B + varint 2B = 3B
107
+ // entity_id | uint32 | [0, 1 048 575] | 20 | tag 1B + varint 3B = 4B
108
+ // animation_state | uint32 | [0, 63] | 6 | tag 1B + varint 1B = 2B
109
+ // is_grounded | bool | — | — | tag 1B + varint 1B = 2B
110
+ // timestamp | double | — | — | tag 1B + fixed64 8B = 9B
111
+ // ---------------------------------------------------------------------------
112
+ // Worst-case: 34 B (vs. 45 B raw: 5×float + 2×uint32 + bool + double)
113
+ // Step: x/z ≈ 0.125 units; y ≈ 0.031 units; pitch ≈ 0.176°; yaw ≈ 0.088°
114
+ // ---------------------------------------------------------------------------
115
+ message AvatarStateSnapshot {
116
+ // World-space position.
117
+ uint32 x = 1 [(decentraland.common.quantized) = { min: -4096.0, max: 4096.0, bits: 16 }];
118
+ uint32 y = 2 [(decentraland.common.quantized) = { min: -256.0, max: 256.0, bits: 14 }];
119
+ uint32 z = 3 [(decentraland.common.quantized) = { min: -4096.0, max: 4096.0, bits: 16 }];
120
+
121
+ // View angles.
122
+ uint32 pitch = 4 [(decentraland.common.quantized) = { min: -90.0, max: 90.0, bits: 10 }];
123
+ uint32 yaw = 5 [(decentraland.common.quantized) = { min: -180.0, max: 180.0, bits: 12 }];
124
+
125
+ // Identity and animation state.
126
+ uint32 entity_id = 6 [(decentraland.common.bit_packed) = { bits: 20 }];
127
+ uint32 animation_state = 7 [(decentraland.common.bit_packed) = { bits: 6 }];
128
+
129
+ // Un-annotated fields — encoded at their natural width.
130
+ bool is_grounded = 8;
131
+ double timestamp = 9; // server epoch milliseconds
132
+ }
@@ -0,0 +1,77 @@
1
+ syntax = "proto3";
2
+
3
+ package decentraland.pulse;
4
+
5
+ import "decentraland/common/vectors.proto";
6
+ import "decentraland/pulse/pulse_shared.proto";
7
+
8
+ message HandshakeRequest {
9
+ bytes auth_chain = 1;
10
+ int32 profile_version = 2;
11
+ optional PlayerInitialState initial_state = 3;
12
+ }
13
+
14
+ // Describes the initial state of the player if (re-)connected in the middle of the session
15
+ message PlayerInitialState {
16
+ PlayerState state = 1;
17
+ optional string emote_id = 2;
18
+ optional uint32 emote_duration_ms = 3;
19
+ // Indicates how many milliseconds ago the emote was started
20
+ optional uint32 emote_start_offset_ms = 4;
21
+ // Non-empty realm identifier. Rejected if empty.
22
+ string realm = 5;
23
+ optional int32 emote_mask = 6;
24
+ }
25
+
26
+ // Similarly to the LiveKit pipeline, a peer announces the version of its profile but
27
+ // it only does it when it's changed as it's sent reliably and stored on the server for other peers
28
+ message ProfileVersionAnnouncement {
29
+ int32 version = 1;
30
+ }
31
+
32
+ // Since the server doesn't simulate the scenes state, it trusts the values from the client
33
+ message PlayerStateInput {
34
+ PlayerState state = 1;
35
+ }
36
+
37
+ // Client sends resync request if it has a gap in the known sequences and, thus, can't apply a delta.
38
+ // It's sent reliably to prevent further desynchronization
39
+ message ResyncRequest {
40
+ uint32 subject_id = 1;
41
+ // highest seq the client actually has for this subject
42
+ uint32 known_seq = 2;
43
+ }
44
+
45
+ // Client → Server. Client requests to start an emote.
46
+ message EmoteStart {
47
+ string emote_id = 1;
48
+ optional uint32 duration_ms = 2;
49
+ PlayerState player_state = 3;
50
+ optional int32 mask = 4;
51
+ }
52
+
53
+ // Client → Server. Client requests to stop a looping emote.
54
+ // One-shot emotes are terminated by the server timer.
55
+ message EmoteStop {
56
+ }
57
+
58
+ // Client → Server. Also announces the peer's realm; peers in different realms never see each
59
+ // other. Must be the first gameplay message after handshake. Same-realm re-teleports are valid.
60
+ message TeleportRequest {
61
+ int32 parcel_index = 1;
62
+ decentraland.common.Vector3 position = 2;
63
+ // Non-empty realm identifier. Rejected if empty.
64
+ string realm = 3;
65
+ }
66
+
67
+ message ClientMessage {
68
+ oneof message {
69
+ HandshakeRequest handshake = 1;
70
+ PlayerStateInput input = 2;
71
+ ResyncRequest resync = 3;
72
+ ProfileVersionAnnouncement profile_announcement = 4;
73
+ EmoteStart emote_start = 5;
74
+ EmoteStop emote_stop = 6;
75
+ TeleportRequest teleport = 7;
76
+ }
77
+ }
@@ -0,0 +1,138 @@
1
+ syntax = "proto3";
2
+
3
+ package decentraland.pulse;
4
+
5
+ import "decentraland/common/options.proto";
6
+ import "decentraland/pulse/pulse_shared.proto";
7
+
8
+ message HandshakeResponse {
9
+ bool success = 1;
10
+ optional string error = 2;
11
+ }
12
+
13
+ message PlayerProfileVersionsAnnounced {
14
+ uint32 subject_id = 1;
15
+ int32 version = 2;
16
+ }
17
+
18
+ message PlayerStateDeltaTier0 {
19
+ uint32 subject_id = 1;
20
+
21
+ // Between two consecutive server simulation ticks, the subject may have sent multiple inputs, each incrementing Seq by 1. So
22
+ // the delta's NewSeq will naturally jump by more than 1 compared to the previous delta — even with zero packet loss.
23
+ // Without the "BaselineSeq" the client has no way to detect the package loss
24
+ uint32 baseline_seq = 2;
25
+ uint32 new_seq = 3;
26
+ uint32 server_tick = 4;
27
+
28
+ // While the player doesn't cross the parcel, this field is omitted from diff
29
+ optional int32 parcel_index = 5;
30
+
31
+ // X position inside the parcel
32
+ optional uint32 position_x = 6 [(decentraland.common.quantized) = { min: 0, max: 16, bits: 8 }];
33
+
34
+ // Y position
35
+ optional uint32 position_y = 7 [(decentraland.common.quantized) = { min: 0, max: 200, bits: 13 }];
36
+
37
+ // Z position inside the parcel
38
+ optional uint32 position_z = 8 [(decentraland.common.quantized) = { min: 0, max: 16, bits: 8 }];
39
+
40
+ optional uint32 velocity_x = 9 [(decentraland.common.quantized) = { min: -50, max: 50, bits: 8 }];
41
+ optional uint32 velocity_y = 10 [(decentraland.common.quantized) = { min: -50, max: 50, bits: 8 }];
42
+ optional uint32 velocity_z = 11 [(decentraland.common.quantized) = { min: -50, max: 50, bits: 8 }];
43
+
44
+ optional uint32 rotation_y = 12 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
45
+ optional uint32 movement_blend = 13 [(decentraland.common.quantized) = { min: 0, max: 3, bits: 5 }];
46
+ optional uint32 slide_blend = 14 [(decentraland.common.quantized) = { min: 0, max: 1, bits: 4 }];
47
+ optional uint32 head_yaw = 15 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
48
+ optional uint32 head_pitch = 16 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
49
+
50
+ optional uint32 state_flags = 17;
51
+ optional GlideState glide_state = 18;
52
+
53
+ optional int32 jump_count = 19;
54
+
55
+ // Absolute world hit position the player is pointing at.
56
+ // Only meaningful when POINTING_AT is set in `state_flags`.
57
+ //
58
+ // X/Z use 17 bits over the world span (~±3000m, covers GenesisCity + border +
59
+ // raycast cutoff) which yields ~0.046m steps — at least as fine as the player's
60
+ // own parcel_index + position_x/z combined precision (0.0625m), so no separate
61
+ // parcel index is needed for point-at.
62
+ //
63
+ // Y uses 7 bits over the player altitude range (matches position_y), step ~1.6m.
64
+ // Point At is not supposed to change frequently so the wire overhead should be minimal
65
+ optional uint32 point_at_x = 20 [(decentraland.common.quantized) = { min: -3000.0, max: 3000.0, bits: 17 }];
66
+ optional uint32 point_at_y = 21 [(decentraland.common.quantized) = { min: 0.0, max: 200.0, bits: 7 }];
67
+ optional uint32 point_at_z = 22 [(decentraland.common.quantized) = { min: -3000.0, max: 3000.0, bits: 17 }];
68
+ }
69
+
70
+ // Full State is sent to the client when it is out of sync, and can't recover with a diff only
71
+ message PlayerStateFull {
72
+ uint32 subject_id = 1;
73
+ uint32 sequence = 2;
74
+ uint32 server_tick = 3;
75
+ PlayerState state = 4;
76
+ }
77
+
78
+ // Notification to the client, that a peer has joined, it can mean connection or entering the area of interest, it's up to the server to decide
79
+ message PlayerJoined {
80
+ string user_id = 1;
81
+ int32 profile_version = 2;
82
+ PlayerStateFull state = 3;
83
+ }
84
+
85
+ // Notification to the client, that a peer has left, it can mean disconnection or leaving the area of interest
86
+ message PlayerLeft {
87
+ uint32 subject_id = 1;
88
+ }
89
+
90
+ enum EmoteStopReason {
91
+ COMPLETED = 0; // one-shot timer expired on the server
92
+ CANCELLED = 1; // client sent EmoteStop (looping emotes)
93
+ }
94
+
95
+ // Server → All Observers
96
+ // Full player state is piggybacked to ensure the emote is played in the right place.
97
+ // Observers use server_tick to scrub animation forward by transit latency.
98
+ message EmoteStarted {
99
+ uint32 subject_id = 1;
100
+ uint32 sequence = 2;
101
+ uint32 server_tick = 3;
102
+ string emote_id = 4;
103
+ PlayerState player_state = 5;
104
+ optional int32 mask = 6;
105
+ }
106
+
107
+ // Server → All Observers
108
+ // Client resumes MovementInput only after receiving this.
109
+ // Carries full PlayerState so the client can snap to the correct position on resume.
110
+ message EmoteStopped {
111
+ uint32 subject_id = 1;
112
+ uint32 server_tick = 2;
113
+ EmoteStopReason reason = 3;
114
+ uint32 sequence = 4;
115
+ PlayerState player_state = 5;
116
+ }
117
+
118
+ // Server → All Observers
119
+ message TeleportPerformed {
120
+ uint32 subject_id = 1;
121
+ uint32 sequence = 2;
122
+ uint32 server_tick = 3;
123
+ PlayerState state = 4;
124
+ }
125
+
126
+ message ServerMessage {
127
+ oneof message {
128
+ HandshakeResponse handshake = 1;
129
+ PlayerStateFull player_state_full = 2;
130
+ PlayerStateDeltaTier0 player_state_delta = 3;
131
+ PlayerJoined player_joined = 4;
132
+ PlayerLeft player_left = 5;
133
+ PlayerProfileVersionsAnnounced player_profile_version_announced = 6;
134
+ EmoteStarted emote_started = 7;
135
+ EmoteStopped emote_stopped = 8;
136
+ TeleportPerformed teleported = 9;
137
+ }
138
+ }
@@ -0,0 +1,48 @@
1
+ syntax = "proto3";
2
+
3
+ package decentraland.pulse;
4
+
5
+ import "decentraland/common/vectors.proto";
6
+
7
+ enum PlayerAnimationFlags {
8
+ NONE = 0;
9
+ GROUNDED = 1;
10
+ LONG_JUMP = 2;
11
+ LONG_FALL = 4;
12
+ FALLING = 8;
13
+ STUNNED = 16;
14
+ HEAD_YAW = 32;
15
+ HEAD_PITCH = 64;
16
+ POINTING_AT = 128;
17
+ }
18
+
19
+ enum GlideState {
20
+ PROP_CLOSED = 0;
21
+ OPENING_PROP = 1;
22
+ GLIDING = 2;
23
+ CLOSING_PROP = 3;
24
+ }
25
+
26
+ message PlayerState {
27
+ int32 parcel_index = 1;
28
+
29
+ decentraland.common.Vector3 position = 2;
30
+ decentraland.common.Vector3 velocity = 3;
31
+
32
+ float rotation_y = 4;
33
+
34
+ float movement_blend = 5;
35
+ float slide_blend = 6;
36
+
37
+ optional float head_yaw = 7;
38
+ optional float head_pitch = 8;
39
+
40
+ uint32 state_flags = 9;
41
+ GlideState glide_state = 10;
42
+
43
+ int32 jump_count = 11;
44
+
45
+ // Absolute world hit position the player is pointing at.
46
+ // Only meaningful when POINTING_AT is set in `state_flags`.
47
+ optional decentraland.common.Vector3 point_at = 12;
48
+ }