@dcl/protocol 1.0.0-29286492043.commit-0010e70 → 1.0.0-29287211316.commit-e44c721
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 +217 -0
- package/out-js/decentraland/kernel/comms/rfc4/comms.gen.d.ts +21 -0
- package/out-js/decentraland/kernel/comms/rfc4/comms.gen.js +124 -4
- package/out-js/decentraland/kernel/comms/rfc4/comms.gen.js.map +1 -1
- package/out-js/decentraland/sdk/components/avatar_shape.gen.d.ts +8 -0
- package/out-js/decentraland/sdk/components/avatar_shape.gen.js +35 -1
- package/out-js/decentraland/sdk/components/avatar_shape.gen.js.map +1 -1
- package/out-ts/decentraland/kernel/comms/rfc4/comms.gen.ts +166 -2
- package/out-ts/decentraland/sdk/components/avatar_shape.gen.ts +34 -0
- package/package.json +9 -6
- package/proto/decentraland/common/options.proto +51 -0
- package/proto/decentraland/common/quantization_example.proto +164 -0
- package/proto/decentraland/kernel/comms/rfc4/comms.proto +11 -0
- package/proto/decentraland/pulse/pulse_client.proto +79 -0
- package/proto/decentraland/pulse/pulse_server.proto +144 -0
- package/proto/decentraland/pulse/pulse_shared.proto +57 -0
- package/proto/decentraland/sdk/components/avatar_shape.proto +5 -0
- package/proto/decentraland/sdk/components/light_source.proto +1 -1
- package/proto/decentraland/sdk/components/virtual_camera.proto +2 -0
- package/protoc-gen-bitwise/generator_csharp.js +248 -0
- package/protoc-gen-bitwise/options.js +139 -0
- package/protoc-gen-bitwise/plugin.js +87 -0
- package/protoc-gen-bitwise/runtime/cs/Quantize.cs +70 -0
- package/protoc-gen-bitwise/wire.js +239 -0
- package/proto/buf.yaml +0 -47
- package/proto/google/LICENSE +0 -27
- package/proto/google/README.md +0 -1
- package/proto/google/api/annotations.json +0 -83
- package/proto/google/api/annotations.proto +0 -11
- package/proto/google/api/http.json +0 -86
- package/proto/google/api/http.proto +0 -31
- package/proto/google/protobuf/api.json +0 -118
- package/proto/google/protobuf/api.proto +0 -34
- package/proto/google/protobuf/descriptor.json +0 -739
- package/proto/google/protobuf/descriptor.proto +0 -286
- package/proto/google/protobuf/source_context.json +0 -20
- package/proto/google/protobuf/source_context.proto +0 -7
- package/proto/google/protobuf/type.json +0 -202
- package/proto/google/protobuf/type.proto +0 -89
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
// Power-law quantized: a sign bit + 7-bit magnitude curve over [-50, 50]. Zero is exactly
|
|
41
|
+
// representable (a stopped peer reports an exact 0 instead of the linear quantizer's ±0.196
|
|
42
|
+
// residual that drove foreign-avatar drift), and pow=2 concentrates resolution at the low speeds
|
|
43
|
+
// where avatars actually move, leaving only the visually-irrelevant extremes coarse.
|
|
44
|
+
optional uint32 velocity_x = 9 [(decentraland.common.quantized_power) = { max: 50, pow: 2, bits: 8 }];
|
|
45
|
+
optional uint32 velocity_y = 10 [(decentraland.common.quantized_power) = { max: 50, pow: 2, bits: 8 }];
|
|
46
|
+
optional uint32 velocity_z = 11 [(decentraland.common.quantized_power) = { max: 50, pow: 2, bits: 8 }];
|
|
47
|
+
|
|
48
|
+
optional uint32 rotation_y = 12 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
|
|
49
|
+
optional uint32 movement_blend = 13 [(decentraland.common.quantized) = { min: 0, max: 3, bits: 5 }];
|
|
50
|
+
optional uint32 slide_blend = 14 [(decentraland.common.quantized) = { min: 0, max: 1, bits: 4 }];
|
|
51
|
+
optional uint32 head_yaw = 15 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
|
|
52
|
+
optional uint32 head_pitch = 16 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
|
|
53
|
+
|
|
54
|
+
optional uint32 state_flags = 17;
|
|
55
|
+
optional GlideState glide_state = 18;
|
|
56
|
+
|
|
57
|
+
optional int32 jump_count = 19;
|
|
58
|
+
|
|
59
|
+
// Absolute world hit position the player is pointing at.
|
|
60
|
+
// Only meaningful when POINTING_AT is set in `state_flags`.
|
|
61
|
+
//
|
|
62
|
+
// X/Z use 17 bits over the world span (~±3000m, covers GenesisCity + border +
|
|
63
|
+
// raycast cutoff) which yields ~0.046m steps — at least as fine as the player's
|
|
64
|
+
// own parcel_index + position_x/z combined precision (0.0625m), so no separate
|
|
65
|
+
// parcel index is needed for point-at.
|
|
66
|
+
//
|
|
67
|
+
// Y uses 7 bits over the player altitude range (matches position_y), step ~1.6m.
|
|
68
|
+
// Point At is not supposed to change frequently so the wire overhead should be minimal
|
|
69
|
+
optional uint32 point_at_x = 20 [(decentraland.common.quantized) = { min: -3000.0, max: 3000.0, bits: 17 }];
|
|
70
|
+
optional uint32 point_at_y = 21 [(decentraland.common.quantized) = { min: 0.0, max: 200.0, bits: 7 }];
|
|
71
|
+
optional uint32 point_at_z = 22 [(decentraland.common.quantized) = { min: -3000.0, max: 3000.0, bits: 17 }];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Full State is sent to the client when it is out of sync, and can't recover with a diff only
|
|
75
|
+
message PlayerStateFull {
|
|
76
|
+
uint32 subject_id = 1;
|
|
77
|
+
uint32 sequence = 2;
|
|
78
|
+
uint32 server_tick = 3;
|
|
79
|
+
PlayerState state = 4;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 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
|
|
83
|
+
message PlayerJoined {
|
|
84
|
+
string user_id = 1;
|
|
85
|
+
int32 profile_version = 2;
|
|
86
|
+
PlayerStateFull state = 3;
|
|
87
|
+
string realm = 4;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Notification to the client, that a peer has left, it can mean disconnection or leaving the area of interest
|
|
91
|
+
message PlayerLeft {
|
|
92
|
+
uint32 subject_id = 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
enum EmoteStopReason {
|
|
96
|
+
COMPLETED = 0; // one-shot timer expired on the server
|
|
97
|
+
CANCELLED = 1; // client sent EmoteStop (looping emotes)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Server → All Observers
|
|
101
|
+
// Full player state is piggybacked to ensure the emote is played in the right place.
|
|
102
|
+
// Observers use server_tick to scrub animation forward by transit latency.
|
|
103
|
+
message EmoteStarted {
|
|
104
|
+
uint32 subject_id = 1;
|
|
105
|
+
uint32 sequence = 2;
|
|
106
|
+
uint32 server_tick = 3;
|
|
107
|
+
string emote_id = 4;
|
|
108
|
+
PlayerState player_state = 5;
|
|
109
|
+
optional int32 mask = 6;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Server → All Observers
|
|
113
|
+
// Client resumes MovementInput only after receiving this.
|
|
114
|
+
// Carries full PlayerState so the client can snap to the correct position on resume.
|
|
115
|
+
message EmoteStopped {
|
|
116
|
+
uint32 subject_id = 1;
|
|
117
|
+
uint32 server_tick = 2;
|
|
118
|
+
EmoteStopReason reason = 3;
|
|
119
|
+
uint32 sequence = 4;
|
|
120
|
+
PlayerState player_state = 5;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Server → All Observers
|
|
124
|
+
message TeleportPerformed {
|
|
125
|
+
uint32 subject_id = 1;
|
|
126
|
+
uint32 sequence = 2;
|
|
127
|
+
uint32 server_tick = 3;
|
|
128
|
+
PlayerState state = 4;
|
|
129
|
+
string realm = 5;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
message ServerMessage {
|
|
133
|
+
oneof message {
|
|
134
|
+
HandshakeResponse handshake = 1;
|
|
135
|
+
PlayerStateFull player_state_full = 2;
|
|
136
|
+
PlayerStateDeltaTier0 player_state_delta = 3;
|
|
137
|
+
PlayerJoined player_joined = 4;
|
|
138
|
+
PlayerLeft player_left = 5;
|
|
139
|
+
PlayerProfileVersionsAnnounced player_profile_version_announced = 6;
|
|
140
|
+
EmoteStarted emote_started = 7;
|
|
141
|
+
EmoteStopped emote_stopped = 8;
|
|
142
|
+
TeleportPerformed teleported = 9;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package decentraland.pulse;
|
|
4
|
+
|
|
5
|
+
import "decentraland/common/options.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
|
+
// Quantized identically to PlayerStateDeltaTier0 so a full state and a stream of deltas land on the same grid.
|
|
27
|
+
message PlayerState {
|
|
28
|
+
int32 parcel_index = 1;
|
|
29
|
+
|
|
30
|
+
// Position inside the parcel (X/Z) and world altitude (Y).
|
|
31
|
+
uint32 position_x = 2 [(decentraland.common.quantized) = { min: 0, max: 16, bits: 8 }];
|
|
32
|
+
uint32 position_y = 3 [(decentraland.common.quantized) = { min: 0, max: 200, bits: 13 }];
|
|
33
|
+
uint32 position_z = 4 [(decentraland.common.quantized) = { min: 0, max: 16, bits: 8 }];
|
|
34
|
+
|
|
35
|
+
uint32 velocity_x = 5 [(decentraland.common.quantized_power) = { max: 50, pow: 2, bits: 8 }];
|
|
36
|
+
uint32 velocity_y = 6 [(decentraland.common.quantized_power) = { max: 50, pow: 2, bits: 8 }];
|
|
37
|
+
uint32 velocity_z = 7 [(decentraland.common.quantized_power) = { max: 50, pow: 2, bits: 8 }];
|
|
38
|
+
|
|
39
|
+
uint32 rotation_y = 8 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
|
|
40
|
+
|
|
41
|
+
uint32 movement_blend = 9 [(decentraland.common.quantized) = { min: 0, max: 3, bits: 5 }];
|
|
42
|
+
uint32 slide_blend = 10 [(decentraland.common.quantized) = { min: 0, max: 1, bits: 4 }];
|
|
43
|
+
|
|
44
|
+
optional uint32 head_yaw = 11 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
|
|
45
|
+
optional uint32 head_pitch = 12 [(decentraland.common.quantized) = { min: 0, max: 360.0, bits: 7 }];
|
|
46
|
+
|
|
47
|
+
uint32 state_flags = 13;
|
|
48
|
+
GlideState glide_state = 14;
|
|
49
|
+
|
|
50
|
+
int32 jump_count = 15;
|
|
51
|
+
|
|
52
|
+
// Absolute world hit position the player is pointing at.
|
|
53
|
+
// Only meaningful when POINTING_AT is set in `state_flags`.
|
|
54
|
+
optional uint32 point_at_x = 16 [(decentraland.common.quantized) = { min: -3000.0, max: 3000.0, bits: 17 }];
|
|
55
|
+
optional uint32 point_at_y = 17 [(decentraland.common.quantized) = { min: 0.0, max: 200.0, bits: 7 }];
|
|
56
|
+
optional uint32 point_at_z = 18 [(decentraland.common.quantized) = { min: -3000.0, max: 3000.0, bits: 17 }];
|
|
57
|
+
}
|
|
@@ -25,4 +25,4 @@ message PBLightSource {
|
|
|
25
25
|
optional float inner_angle = 9; // default = 21.8. Inner angle can't be higher than outer angle, otherwise will default to same value. Min value is 0. Max value is 179.
|
|
26
26
|
optional float outer_angle = 10; // default = 30. Outer angle can't be lower than inner angle, otherwise will inner angle will be set to same value. Max value is 179.
|
|
27
27
|
}
|
|
28
|
-
}
|
|
28
|
+
}
|
|
@@ -10,7 +10,9 @@ option (common.ecs_component_id) = 1076;
|
|
|
10
10
|
// an 'instant' transition (like using speed/time = 0)
|
|
11
11
|
// * The lookAtEntity defines to which entity the Camera has to look at constantly (independent from
|
|
12
12
|
// the holding entity transform).
|
|
13
|
+
// * The fov defines the Field of View of the virtual camera
|
|
13
14
|
message PBVirtualCamera {
|
|
14
15
|
optional common.CameraTransition default_transition = 1;
|
|
15
16
|
optional uint32 look_at_entity = 2;
|
|
17
|
+
optional float fov = 3; // default: 60
|
|
16
18
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* C# code generator for the protoc-gen-bitwise plugin.
|
|
5
|
+
*
|
|
6
|
+
* For every proto message that contains at least one uint32 field annotated
|
|
7
|
+
* with [(quantized)], this module emits a C# partial class that adds a computed
|
|
8
|
+
* float property named {FieldName}Quantized. The getter decodes the stored
|
|
9
|
+
* uint32 to a float; the setter encodes a float back to a uint32. Standard
|
|
10
|
+
* protobuf handles serialization of the uint32 wire field; this class adds a
|
|
11
|
+
* typed float accessor on top.
|
|
12
|
+
*
|
|
13
|
+
* Only uint32 fields are supported. bit_packed and unannotated fields are
|
|
14
|
+
* passed through without generating any accessor.
|
|
15
|
+
*
|
|
16
|
+
* Port of the original generator_csharp.py — output is intended to be
|
|
17
|
+
* byte-for-byte identical.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { getFieldOptions } = require('./options')
|
|
21
|
+
|
|
22
|
+
// FieldDescriptorProto type/label constants.
|
|
23
|
+
const TYPE_UINT32 = 13
|
|
24
|
+
const LABEL_REPEATED = 3
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Mirrors Python str.capitalize(): upper-first, lowercase the rest. */
|
|
31
|
+
function capitalize(word) {
|
|
32
|
+
if (word.length === 0) return ''
|
|
33
|
+
return word[0].toUpperCase() + word.slice(1).toLowerCase()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** position_x -> PositionX */
|
|
37
|
+
function snakeToPascal(name) {
|
|
38
|
+
return name.split('_').map(capitalize).join('')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** decentraland.kernel.comms.v3 -> Decentraland.Kernel.Comms.V3 */
|
|
42
|
+
function packageToNamespace(pkg) {
|
|
43
|
+
if (!pkg) return 'Generated'
|
|
44
|
+
return pkg.split('.').map(capitalize).join('.')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format a number with C printf %g semantics at `precision` significant digits:
|
|
49
|
+
* shortest of fixed/scientific, with trailing zeros and a trailing dot removed.
|
|
50
|
+
* Reproduces Python's `f'{value:.{precision}g}'`.
|
|
51
|
+
*/
|
|
52
|
+
function formatG(value, precision) {
|
|
53
|
+
if (precision <= 0) precision = 1
|
|
54
|
+
if (value === 0) return '0'
|
|
55
|
+
if (!Number.isFinite(value)) return value > 0 ? 'inf' : 'nan'
|
|
56
|
+
|
|
57
|
+
const negative = value < 0
|
|
58
|
+
const v = Math.abs(value)
|
|
59
|
+
|
|
60
|
+
// Correctly-rounded scientific form yields the decimal exponent X (handling
|
|
61
|
+
// carry such as 9.999 -> 1.0e+1).
|
|
62
|
+
const sci = v.toExponential(precision - 1)
|
|
63
|
+
const eIdx = sci.indexOf('e')
|
|
64
|
+
const X = parseInt(sci.slice(eIdx + 1), 10)
|
|
65
|
+
|
|
66
|
+
let result
|
|
67
|
+
if (X >= -4 && X < precision) {
|
|
68
|
+
// Fixed notation with (precision - 1 - X) fraction digits.
|
|
69
|
+
const fractionDigits = precision - 1 - X
|
|
70
|
+
result = v.toFixed(fractionDigits >= 0 ? fractionDigits : 0)
|
|
71
|
+
if (result.indexOf('.') !== -1) {
|
|
72
|
+
result = result.replace(/0+$/, '').replace(/\.$/, '')
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Scientific notation; printf prints at least two exponent digits.
|
|
76
|
+
let mantissa = sci.slice(0, eIdx)
|
|
77
|
+
if (mantissa.indexOf('.') !== -1) {
|
|
78
|
+
mantissa = mantissa.replace(/0+$/, '').replace(/\.$/, '')
|
|
79
|
+
}
|
|
80
|
+
const expSign = X < 0 ? '-' : '+'
|
|
81
|
+
let expDigits = String(Math.abs(X))
|
|
82
|
+
if (expDigits.length < 2) expDigits = '0' + expDigits
|
|
83
|
+
result = mantissa + 'e' + expSign + expDigits
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (negative ? '-' : '') + result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Format a value as a C# float literal (e.g. -100.0f). */
|
|
90
|
+
function formatFloat(value) {
|
|
91
|
+
let text = formatG(value, 8)
|
|
92
|
+
if (text.indexOf('.') === -1 && text.indexOf('e') === -1 && text.indexOf('E') === -1) {
|
|
93
|
+
text += '.0'
|
|
94
|
+
}
|
|
95
|
+
return text + 'f'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Format a quantization step size for a doc comment (e.g. "≈ 0.003"). */
|
|
99
|
+
function formatStep(step) {
|
|
100
|
+
return '≈ ' + formatG(step, 6)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Per-message code generation
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate a C# partial class for a proto message, or null if it has no
|
|
109
|
+
* quantized uint32 fields. Returns an array of lines (no trailing newline).
|
|
110
|
+
*/
|
|
111
|
+
function generateMessage(msgProto, indent) {
|
|
112
|
+
const i = indent || ' '
|
|
113
|
+
const props = []
|
|
114
|
+
|
|
115
|
+
for (const field of msgProto.field) {
|
|
116
|
+
// Repeated/map fields are not supported.
|
|
117
|
+
if (field.label === LABEL_REPEATED) continue
|
|
118
|
+
// Only uint32 fields are candidates for quantized accessors.
|
|
119
|
+
if (field.type !== TYPE_UINT32) continue
|
|
120
|
+
|
|
121
|
+
const { quantized, quantizedPower } = getFieldOptions(field.optionsRaw)
|
|
122
|
+
const propName = snakeToPascal(field.name)
|
|
123
|
+
|
|
124
|
+
let doc, getExpr, setExpr, step, bits
|
|
125
|
+
if (quantized !== null) {
|
|
126
|
+
const mn = formatFloat(quantized.min)
|
|
127
|
+
const mx = formatFloat(quantized.max)
|
|
128
|
+
bits = quantized.bits
|
|
129
|
+
// Uniform quantizer — the step is constant across the whole range.
|
|
130
|
+
step = (quantized.max - quantized.min) / ((1 << bits) - 1)
|
|
131
|
+
doc = `Range [${mn}, ${mx}], ${bits} bits, step ${formatStep(step)}.`
|
|
132
|
+
getExpr = `Quantize.Decode(${propName}, ${mn}, ${mx}, ${bits})`
|
|
133
|
+
setExpr = `Quantize.Encode(value, ${mn}, ${mx}, ${bits})`
|
|
134
|
+
} else if (quantizedPower !== null) {
|
|
135
|
+
const mx = formatFloat(quantizedPower.max)
|
|
136
|
+
const pw = formatFloat(quantizedPower.pow)
|
|
137
|
+
bits = quantizedPower.bits
|
|
138
|
+
// Power curve is non-uniform: the finest step sits next to zero (first magnitude code),
|
|
139
|
+
// the COARSEST at the top of the range. The coarsest step upper-bounds the error for any
|
|
140
|
+
// value, so that's what the exposed {Name}QuantizedStep const carries (safe as a tolerance).
|
|
141
|
+
const magSteps = (1 << (bits - 1)) - 1
|
|
142
|
+
const nearZeroStep = quantizedPower.max * Math.pow(1 / magSteps, quantizedPower.pow)
|
|
143
|
+
step = quantizedPower.max * (1 - Math.pow((magSteps - 1) / magSteps, quantizedPower.pow))
|
|
144
|
+
doc =
|
|
145
|
+
`Range [-${mx}, ${mx}], power ${pw}, ${bits} bits ` +
|
|
146
|
+
`(sign + ${bits - 1}-bit magnitude), near-zero step ${formatStep(nearZeroStep)}.`
|
|
147
|
+
getExpr = `Quantize.DecodePower(${propName}, ${mx}, ${pw}, ${bits})`
|
|
148
|
+
setExpr = `Quantize.EncodePower(value, ${mx}, ${pw}, ${bits})`
|
|
149
|
+
} else {
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Highest code the encoder can emit for this field. Both the linear quantizer (top code
|
|
154
|
+
// `2^bits - 1`) and the power quantizer (`(magnitude << 1) | sign` with an `bits-1`-bit
|
|
155
|
+
// magnitude, so top code `((2^(bits-1)-1) << 1) | 1 == 2^bits - 1`) share this bound.
|
|
156
|
+
const maxCode = 2 ** bits - 1
|
|
157
|
+
|
|
158
|
+
props.push({ propName, doc, getExpr, setExpr, step, maxCode })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (props.length === 0) return null
|
|
162
|
+
|
|
163
|
+
const lines = []
|
|
164
|
+
lines.push(`public partial class ${msgProto.name}`)
|
|
165
|
+
lines.push('{')
|
|
166
|
+
|
|
167
|
+
// Decode on every get, encode on every set — no backing cache. The raw uint32 property is the
|
|
168
|
+
// single source of truth, so the float accessor can never disagree with the code on the wire
|
|
169
|
+
// (get-after-set returns the on-grid value a receiver decodes) and there is no stale-cache
|
|
170
|
+
// hazard when the raw field is mutated directly. Decode is a multiply-add; only power fields
|
|
171
|
+
// pay a MathF.Pow.
|
|
172
|
+
for (const { propName, doc, getExpr, setExpr, step } of props) {
|
|
173
|
+
lines.push(`${i}/// <summary>Coarsest quantization step of <see cref="${propName}Quantized"/>. Safe as an equality tolerance.</summary>`)
|
|
174
|
+
lines.push(`${i}public const float ${propName}QuantizedStep = ${formatFloat(step)};`)
|
|
175
|
+
lines.push(`${i}/// <summary>Float accessor for <see cref="${propName}"/>. ${doc}</summary>`)
|
|
176
|
+
lines.push(`${i}public float ${propName}Quantized`)
|
|
177
|
+
lines.push(`${i}{`)
|
|
178
|
+
lines.push(`${i}${i}get => ${getExpr};`)
|
|
179
|
+
lines.push(`${i}${i}set => ${propName} = ${setExpr};`)
|
|
180
|
+
lines.push(`${i}}`)
|
|
181
|
+
lines.push('')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
lines.push(`${i}/// <summary>`)
|
|
185
|
+
lines.push(`${i}/// True when every quantized field holds a wire code within its declared bit width`)
|
|
186
|
+
lines.push(`${i}/// (<c>0 .. 2^bits-1</c>). The encoder never emits a code above this bound, so a larger`)
|
|
187
|
+
lines.push(`${i}/// value is a malformed/hostile message: decoding it would land far outside the field's`)
|
|
188
|
+
lines.push(`${i}/// <c>[min, max]</c> and, since the server relays raw codes verbatim, poison every observer.`)
|
|
189
|
+
lines.push(`${i}/// Reject before storing or relaying. Pure integer comparison — no decode.`)
|
|
190
|
+
lines.push(`${i}/// </summary>`)
|
|
191
|
+
lines.push(`${i}public bool AreQuantizedFieldsInRange() =>`)
|
|
192
|
+
props.forEach(({ propName, maxCode }, idx) => {
|
|
193
|
+
const prefix = idx === 0 ? '' : '&& '
|
|
194
|
+
const suffix = idx === props.length - 1 ? ';' : ''
|
|
195
|
+
lines.push(`${i}${i}${prefix}${propName} <= ${maxCode}u${suffix}`)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
lines.push('}')
|
|
199
|
+
return lines
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Per-file code generation (public entry point)
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate a C# source file for a FileDescriptorProto, or null if the file
|
|
208
|
+
* contains no quantized uint32 fields.
|
|
209
|
+
* @returns {{name: string, content: string} | null}
|
|
210
|
+
*/
|
|
211
|
+
function generateCsharp(fileProto) {
|
|
212
|
+
const namespace = packageToNamespace(fileProto.package)
|
|
213
|
+
|
|
214
|
+
const protoFile = fileProto.name.split('/').pop()
|
|
215
|
+
const stem = snakeToPascal(protoFile.replace('.proto', ''))
|
|
216
|
+
const outName = `${stem}.Bitwise.cs`
|
|
217
|
+
|
|
218
|
+
const header = [
|
|
219
|
+
'// <auto-generated>',
|
|
220
|
+
'// Generated by protoc-gen-bitwise. DO NOT EDIT.',
|
|
221
|
+
`// Source: ${fileProto.name}`,
|
|
222
|
+
'// </auto-generated>',
|
|
223
|
+
'',
|
|
224
|
+
'using Decentraland.Networking.Bitwise;',
|
|
225
|
+
'',
|
|
226
|
+
`namespace ${namespace}`,
|
|
227
|
+
'{',
|
|
228
|
+
]
|
|
229
|
+
const footer = ['', `} // namespace ${namespace}`]
|
|
230
|
+
|
|
231
|
+
const body = []
|
|
232
|
+
for (const msg of fileProto.messageType) {
|
|
233
|
+
const msgLines = generateMessage(msg)
|
|
234
|
+
if (msgLines === null) continue
|
|
235
|
+
// Indent each line by 4 spaces (inside the namespace block).
|
|
236
|
+
for (const line of msgLines) {
|
|
237
|
+
body.push(line ? ' ' + line : '')
|
|
238
|
+
}
|
|
239
|
+
body.push('')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (body.length === 0) return null
|
|
243
|
+
|
|
244
|
+
const content = header.concat(body, footer).join('\n') + '\n'
|
|
245
|
+
return { name: outName, content }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = { generateCsharp }
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parser for the custom bitwise field options defined in options.proto.
|
|
5
|
+
*
|
|
6
|
+
* The descriptor decoder hands us the raw serialized FieldOptions bytes (it
|
|
7
|
+
* declares `options` as opaque bytes). We walk those bytes looking for the
|
|
8
|
+
* custom extension field numbers — protobuf preserves unknown/unregistered
|
|
9
|
+
* extension bytes, so they are always present even though no runtime here knows
|
|
10
|
+
* the extension schema. This mirrors the original options_pb2.py.
|
|
11
|
+
*
|
|
12
|
+
* Wire format: tag = (field_number << 3) | wire_type
|
|
13
|
+
* wire_type 0 = varint, 1 = 64-bit, 2 = length-delimited, 5 = 32-bit
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { readVarint, skipField } = require('./wire')
|
|
17
|
+
|
|
18
|
+
// Extension field numbers as defined in options.proto.
|
|
19
|
+
const QUANTIZED_FIELD_NUMBER = 50001
|
|
20
|
+
const BIT_PACKED_FIELD_NUMBER = 50002
|
|
21
|
+
const QUANTIZED_POWER_FIELD_NUMBER = 50003
|
|
22
|
+
|
|
23
|
+
/** Parse a serialized QuantizedFloatOptions message: { min, max, bits }. */
|
|
24
|
+
function parseQuantized(data) {
|
|
25
|
+
const opts = { min: 0.0, max: 0.0, bits: 0 }
|
|
26
|
+
let pos = 0
|
|
27
|
+
while (pos < data.length) {
|
|
28
|
+
let tag
|
|
29
|
+
;[tag, pos] = readVarint(data, pos)
|
|
30
|
+
const fieldNum = tag >>> 3
|
|
31
|
+
const wireType = tag & 0x7
|
|
32
|
+
if (fieldNum === 1 && wireType === 5) {
|
|
33
|
+
// min (float)
|
|
34
|
+
opts.min = data.readFloatLE(pos)
|
|
35
|
+
pos += 4
|
|
36
|
+
} else if (fieldNum === 2 && wireType === 5) {
|
|
37
|
+
// max (float)
|
|
38
|
+
opts.max = data.readFloatLE(pos)
|
|
39
|
+
pos += 4
|
|
40
|
+
} else if (fieldNum === 3 && wireType === 0) {
|
|
41
|
+
// bits (uint32)
|
|
42
|
+
;[opts.bits, pos] = readVarint(data, pos)
|
|
43
|
+
} else {
|
|
44
|
+
pos = skipField(data, pos, wireType)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return opts
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse a serialized QuantizedPowerFloatOptions message: { max, pow, bits }. */
|
|
51
|
+
function parseQuantizedPower(data) {
|
|
52
|
+
const opts = { max: 0.0, pow: 0.0, bits: 0 }
|
|
53
|
+
let pos = 0
|
|
54
|
+
while (pos < data.length) {
|
|
55
|
+
let tag
|
|
56
|
+
;[tag, pos] = readVarint(data, pos)
|
|
57
|
+
const fieldNum = tag >>> 3
|
|
58
|
+
const wireType = tag & 0x7
|
|
59
|
+
if (fieldNum === 1 && wireType === 5) {
|
|
60
|
+
// max (float)
|
|
61
|
+
opts.max = data.readFloatLE(pos)
|
|
62
|
+
pos += 4
|
|
63
|
+
} else if (fieldNum === 2 && wireType === 5) {
|
|
64
|
+
// pow (float)
|
|
65
|
+
opts.pow = data.readFloatLE(pos)
|
|
66
|
+
pos += 4
|
|
67
|
+
} else if (fieldNum === 3 && wireType === 0) {
|
|
68
|
+
// bits (uint32)
|
|
69
|
+
;[opts.bits, pos] = readVarint(data, pos)
|
|
70
|
+
} else {
|
|
71
|
+
pos = skipField(data, pos, wireType)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return opts
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Parse a serialized BitPackedOptions message: { bits }. */
|
|
78
|
+
function parseBitPacked(data) {
|
|
79
|
+
const opts = { bits: 0 }
|
|
80
|
+
let pos = 0
|
|
81
|
+
while (pos < data.length) {
|
|
82
|
+
let tag
|
|
83
|
+
;[tag, pos] = readVarint(data, pos)
|
|
84
|
+
const fieldNum = tag >>> 3
|
|
85
|
+
const wireType = tag & 0x7
|
|
86
|
+
if (fieldNum === 1 && wireType === 0) {
|
|
87
|
+
// bits (uint32)
|
|
88
|
+
;[opts.bits, pos] = readVarint(data, pos)
|
|
89
|
+
} else {
|
|
90
|
+
pos = skipField(data, pos, wireType)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return opts
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract custom bitwise options from raw FieldOptions bytes.
|
|
98
|
+
*
|
|
99
|
+
* @param {Buffer|null} optionsRaw serialized FieldOptions, or null when unset.
|
|
100
|
+
* @returns {{quantized: object|null, bitPacked: object|null, quantizedPower: object|null}}
|
|
101
|
+
*/
|
|
102
|
+
function getFieldOptions(optionsRaw) {
|
|
103
|
+
if (!optionsRaw || optionsRaw.length === 0) {
|
|
104
|
+
return { quantized: null, bitPacked: null, quantizedPower: null }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let quantized = null
|
|
108
|
+
let bitPacked = null
|
|
109
|
+
let quantizedPower = null
|
|
110
|
+
let pos = 0
|
|
111
|
+
|
|
112
|
+
while (pos < optionsRaw.length) {
|
|
113
|
+
let tag
|
|
114
|
+
;[tag, pos] = readVarint(optionsRaw, pos)
|
|
115
|
+
const fieldNum = tag >>> 3
|
|
116
|
+
const wireType = tag & 0x7
|
|
117
|
+
|
|
118
|
+
if (wireType === 2) {
|
|
119
|
+
let len
|
|
120
|
+
;[len, pos] = readVarint(optionsRaw, pos)
|
|
121
|
+
const valueBytes = optionsRaw.subarray(pos, pos + len)
|
|
122
|
+
pos += len
|
|
123
|
+
if (fieldNum === QUANTIZED_FIELD_NUMBER) {
|
|
124
|
+
quantized = parseQuantized(valueBytes)
|
|
125
|
+
} else if (fieldNum === BIT_PACKED_FIELD_NUMBER) {
|
|
126
|
+
bitPacked = parseBitPacked(valueBytes)
|
|
127
|
+
} else if (fieldNum === QUANTIZED_POWER_FIELD_NUMBER) {
|
|
128
|
+
quantizedPower = parseQuantizedPower(valueBytes)
|
|
129
|
+
}
|
|
130
|
+
// else: unknown length-delimited field — already consumed.
|
|
131
|
+
} else {
|
|
132
|
+
pos = skipField(optionsRaw, pos, wireType)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { quantized, bitPacked, quantizedPower }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { getFieldOptions }
|