styly-netsync-server 0.10.3__tar.gz → 0.11.0__tar.gz
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.
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/PKG-INFO +69 -17
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/README.md +68 -16
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/pyproject.toml +1 -1
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/binary_serializer.py +251 -11
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/client_simulator.py +10 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/rest_bridge.py +67 -9
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/server.py +338 -30
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/PKG-INFO +69 -17
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/SOURCES.txt +2 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_binary_serializer.py +54 -13
- styly_netsync_server-0.11.0/tests/test_discovery_probe.py +103 -0
- styly_netsync_server-0.11.0/tests/test_object_sync.py +440 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_rest_bridge.py +153 -7
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/LICENSE +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/setup.cfg +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/__init__.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/__main__.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/adapters.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/cli.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/client.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/config.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/default.toml +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/events.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/logging_utils.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/network_utils.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/nv_sync.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/types.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/dependency_links.txt +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/entry_points.txt +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/requires.txt +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/top_level.txt +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_all_run_methods.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_config.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_logging_cli.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_multi_nic.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_nv_protocol.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_port_error_message.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_python_client.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_reconnect_identity.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_room_expiry.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_stealth_heartbeat.py +0 -0
- {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_timing_monotonic.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: styly-netsync-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: STYLY NetSync Server - Multiplayer framework for Location-Based Entertainment VR/MR experiences
|
|
5
5
|
Author-email: "STYLY, Inc." <info@styly.inc>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -81,12 +81,14 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
81
81
|
|
|
82
82
|
## Wire protocol compatibility
|
|
83
83
|
|
|
84
|
-
- Current transform wire protocol is `protocolVersion =
|
|
85
|
-
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact
|
|
86
|
-
-
|
|
84
|
+
- Current transform wire protocol is `protocolVersion = 4`.
|
|
85
|
+
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V4 pose body.
|
|
86
|
+
- v4 vs. v3: `xrOriginDelta` carries a Y component as a 4th `int16` (`dx, dy, dz, dyaw` = 8 bytes vs. v3's 6), so receivers can reconstruct the sender's rig-Y motion (e.g. elevators).
|
|
87
|
+
- Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
|
|
87
88
|
- Deploy Unity and Python updates together when changing transform protocol behavior.
|
|
88
|
-
- Protocol
|
|
89
|
-
- Absolute (`
|
|
89
|
+
- Protocol v4 position quantization ranges:
|
|
90
|
+
- Absolute (`headPosAbs` only): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
|
|
91
|
+
- XROrigin locomotion delta (`xrOriginDelta`, 4×`int16`: `dx, dy, dz, dyaw`): `0.01 m` per unit for translation, `0.1°` for yaw. Receivers reconstruct `physicalPos = invDeltaRot * (headPos − deltaPos)`; it is not on the wire as a separate absolute field.
|
|
90
92
|
- Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
|
|
91
93
|
- These are encoding limits, not a hard world-size cap. Worlds can be larger, but encoded axis values are clamped if they exceed the representable range.
|
|
92
94
|
|
|
@@ -94,18 +96,18 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
94
96
|
|
|
95
97
|
The following options summarize trade-offs when expanding absolute-position range.
|
|
96
98
|
|
|
97
|
-
Assumed baseline (`protocolVersion=
|
|
98
|
-
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `
|
|
99
|
-
- Room per-client entry (`clientNo + poseTime + clientBody`): `
|
|
99
|
+
Assumed baseline (`protocolVersion=4`):
|
|
100
|
+
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `46 bytes` (matches `test_client_body_size_with_full_pose_no_virtuals`).
|
|
101
|
+
- Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
|
|
100
102
|
|
|
101
103
|
| Option | Absolute Position Encoding | Per-axis Range | Client Body Delta | Room Per-client Delta |
|
|
102
104
|
|---|---|---:|---:|---:|
|
|
103
|
-
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`
|
|
104
|
-
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`
|
|
105
|
-
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`
|
|
105
|
+
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`46 -> 46`) | `+0B` (`56 -> 56`) |
|
|
106
|
+
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
107
|
+
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
106
108
|
|
|
107
109
|
Notes:
|
|
108
|
-
-
|
|
110
|
+
- Only `headPosAbs` is on the wire as an absolute field; `physicalPos` is reconstructed from `headPosAbs + xrOriginDelta`. Option B/C deltas therefore apply to `headPosAbs` only.
|
|
109
111
|
- Option B/C can reduce average overhead if `cell` is transmitted only when changed, but that requires extra state and flags in the wire format.
|
|
110
112
|
|
|
111
113
|
## Configuration
|
|
@@ -160,7 +162,7 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
160
162
|
|
|
161
163
|
```json
|
|
162
164
|
{
|
|
163
|
-
"
|
|
165
|
+
"variables": {
|
|
164
166
|
"name": "Jack",
|
|
165
167
|
"lang": "EN"
|
|
166
168
|
}
|
|
@@ -179,11 +181,35 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
179
181
|
```bash
|
|
180
182
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables" \
|
|
181
183
|
-H "Content-Type: application/json" \
|
|
182
|
-
-d '{"
|
|
184
|
+
-d '{"variables":{"name":"Jack","lang":"EN"}}'
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
The response includes the current mapping status (`clientNo` or `null`) and whether each key was `"applied"` or `"queued"`.
|
|
186
188
|
|
|
189
|
+
- Read endpoints:
|
|
190
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables` — returns all client variables for the device.
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{"clientNo": 7, "variables": {"name": "Jack", "lang": "EN"}}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
If the device has no `clientNo` mapping yet, returns `{"clientNo": null, "variables": {}}`.
|
|
197
|
+
|
|
198
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables/{name}` — returns a single variable.
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{"clientNo": 7, "value": "Jack"}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Returns `404` if the device is unmapped or the variable is not set.
|
|
205
|
+
|
|
206
|
+
- Example:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables"
|
|
210
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables/name"
|
|
211
|
+
```
|
|
212
|
+
|
|
187
213
|
### Global variables
|
|
188
214
|
|
|
189
215
|
- Endpoint: `POST /v1/rooms/{roomId}/global-variables`
|
|
@@ -191,7 +217,7 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
191
217
|
|
|
192
218
|
```json
|
|
193
219
|
{
|
|
194
|
-
"
|
|
220
|
+
"variables": {
|
|
195
221
|
"score": "42",
|
|
196
222
|
"stage": "lobby"
|
|
197
223
|
}
|
|
@@ -210,7 +236,33 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
210
236
|
```bash
|
|
211
237
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/global-variables" \
|
|
212
238
|
-H "Content-Type: application/json" \
|
|
213
|
-
-d '{"
|
|
239
|
+
-d '{"variables":{"score":"42","stage":"lobby"}}'
|
|
214
240
|
```
|
|
215
241
|
|
|
216
242
|
The response includes the room ID and whether each key was `"applied"`, `"queued"`, or `"failed"`.
|
|
243
|
+
|
|
244
|
+
- Read endpoints:
|
|
245
|
+
- `GET /v1/rooms/{roomId}/global-variables` — returns all global variables for the room.
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{"variables": {"score": "42", "stage": "lobby"}}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
- `GET /v1/rooms/{roomId}/global-variables/{name}` — returns a single variable.
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{"value": "42"}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Returns `404` if the variable is not set.
|
|
258
|
+
|
|
259
|
+
- Example:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables"
|
|
263
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables/score"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Read consistency
|
|
267
|
+
|
|
268
|
+
GET endpoints return a snapshot of the REST bridge's in-process cache, which is populated by PUB-SUB broadcasts from the server. The first request to a room lazily creates a bridge and may return an empty snapshot until the initial broadcasts arrive — retry after a short delay if needed.
|
|
@@ -42,12 +42,14 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
42
42
|
|
|
43
43
|
## Wire protocol compatibility
|
|
44
44
|
|
|
45
|
-
- Current transform wire protocol is `protocolVersion =
|
|
46
|
-
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact
|
|
47
|
-
-
|
|
45
|
+
- Current transform wire protocol is `protocolVersion = 4`.
|
|
46
|
+
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V4 pose body.
|
|
47
|
+
- v4 vs. v3: `xrOriginDelta` carries a Y component as a 4th `int16` (`dx, dy, dz, dyaw` = 8 bytes vs. v3's 6), so receivers can reconstruct the sender's rig-Y motion (e.g. elevators).
|
|
48
|
+
- Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
|
|
48
49
|
- Deploy Unity and Python updates together when changing transform protocol behavior.
|
|
49
|
-
- Protocol
|
|
50
|
-
- Absolute (`
|
|
50
|
+
- Protocol v4 position quantization ranges:
|
|
51
|
+
- Absolute (`headPosAbs` only): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
|
|
52
|
+
- XROrigin locomotion delta (`xrOriginDelta`, 4×`int16`: `dx, dy, dz, dyaw`): `0.01 m` per unit for translation, `0.1°` for yaw. Receivers reconstruct `physicalPos = invDeltaRot * (headPos − deltaPos)`; it is not on the wire as a separate absolute field.
|
|
51
53
|
- Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
|
|
52
54
|
- These are encoding limits, not a hard world-size cap. Worlds can be larger, but encoded axis values are clamped if they exceed the representable range.
|
|
53
55
|
|
|
@@ -55,18 +57,18 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
55
57
|
|
|
56
58
|
The following options summarize trade-offs when expanding absolute-position range.
|
|
57
59
|
|
|
58
|
-
Assumed baseline (`protocolVersion=
|
|
59
|
-
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `
|
|
60
|
-
- Room per-client entry (`clientNo + poseTime + clientBody`): `
|
|
60
|
+
Assumed baseline (`protocolVersion=4`):
|
|
61
|
+
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `46 bytes` (matches `test_client_body_size_with_full_pose_no_virtuals`).
|
|
62
|
+
- Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
|
|
61
63
|
|
|
62
64
|
| Option | Absolute Position Encoding | Per-axis Range | Client Body Delta | Room Per-client Delta |
|
|
63
65
|
|---|---|---:|---:|---:|
|
|
64
|
-
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`
|
|
65
|
-
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`
|
|
66
|
-
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`
|
|
66
|
+
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`46 -> 46`) | `+0B` (`56 -> 56`) |
|
|
67
|
+
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
68
|
+
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
67
69
|
|
|
68
70
|
Notes:
|
|
69
|
-
-
|
|
71
|
+
- Only `headPosAbs` is on the wire as an absolute field; `physicalPos` is reconstructed from `headPosAbs + xrOriginDelta`. Option B/C deltas therefore apply to `headPosAbs` only.
|
|
70
72
|
- Option B/C can reduce average overhead if `cell` is transmitted only when changed, but that requires extra state and flags in the wire format.
|
|
71
73
|
|
|
72
74
|
## Configuration
|
|
@@ -121,7 +123,7 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
121
123
|
|
|
122
124
|
```json
|
|
123
125
|
{
|
|
124
|
-
"
|
|
126
|
+
"variables": {
|
|
125
127
|
"name": "Jack",
|
|
126
128
|
"lang": "EN"
|
|
127
129
|
}
|
|
@@ -140,11 +142,35 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
140
142
|
```bash
|
|
141
143
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables" \
|
|
142
144
|
-H "Content-Type: application/json" \
|
|
143
|
-
-d '{"
|
|
145
|
+
-d '{"variables":{"name":"Jack","lang":"EN"}}'
|
|
144
146
|
```
|
|
145
147
|
|
|
146
148
|
The response includes the current mapping status (`clientNo` or `null`) and whether each key was `"applied"` or `"queued"`.
|
|
147
149
|
|
|
150
|
+
- Read endpoints:
|
|
151
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables` — returns all client variables for the device.
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{"clientNo": 7, "variables": {"name": "Jack", "lang": "EN"}}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
If the device has no `clientNo` mapping yet, returns `{"clientNo": null, "variables": {}}`.
|
|
158
|
+
|
|
159
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables/{name}` — returns a single variable.
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{"clientNo": 7, "value": "Jack"}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Returns `404` if the device is unmapped or the variable is not set.
|
|
166
|
+
|
|
167
|
+
- Example:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables"
|
|
171
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables/name"
|
|
172
|
+
```
|
|
173
|
+
|
|
148
174
|
### Global variables
|
|
149
175
|
|
|
150
176
|
- Endpoint: `POST /v1/rooms/{roomId}/global-variables`
|
|
@@ -152,7 +178,7 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
152
178
|
|
|
153
179
|
```json
|
|
154
180
|
{
|
|
155
|
-
"
|
|
181
|
+
"variables": {
|
|
156
182
|
"score": "42",
|
|
157
183
|
"stage": "lobby"
|
|
158
184
|
}
|
|
@@ -171,7 +197,33 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
171
197
|
```bash
|
|
172
198
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/global-variables" \
|
|
173
199
|
-H "Content-Type: application/json" \
|
|
174
|
-
-d '{"
|
|
200
|
+
-d '{"variables":{"score":"42","stage":"lobby"}}'
|
|
175
201
|
```
|
|
176
202
|
|
|
177
203
|
The response includes the room ID and whether each key was `"applied"`, `"queued"`, or `"failed"`.
|
|
204
|
+
|
|
205
|
+
- Read endpoints:
|
|
206
|
+
- `GET /v1/rooms/{roomId}/global-variables` — returns all global variables for the room.
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{"variables": {"score": "42", "stage": "lobby"}}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `GET /v1/rooms/{roomId}/global-variables/{name}` — returns a single variable.
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{"value": "42"}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Returns `404` if the variable is not set.
|
|
219
|
+
|
|
220
|
+
- Example:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables"
|
|
224
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables/score"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Read consistency
|
|
228
|
+
|
|
229
|
+
GET endpoints return a snapshot of the REST bridge's in-process cache, which is populated by PUB-SUB broadcasts from the server. The first request to a room lazily creates a bridge and may return an empty snapshot until the initial broadcasts arrive — retry after a short delay if needed.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "styly-netsync-server"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11.0"
|
|
8
8
|
description = "STYLY NetSync Server - Multiplayer framework for Location-Based Entertainment VR/MR experiences"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
{styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/binary_serializer.py
RENAMED
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
logger = logging.getLogger(__name__)
|
|
7
7
|
|
|
8
8
|
# Message type identifiers
|
|
9
|
-
PROTOCOL_VERSION =
|
|
9
|
+
PROTOCOL_VERSION = 4
|
|
10
10
|
MSG_CLIENT_TRANSFORM = 1
|
|
11
11
|
MSG_ROOM_TRANSFORM = 2 # Legacy room transform with short IDs only
|
|
12
12
|
MSG_RPC = 3 # Remote procedure call
|
|
@@ -19,6 +19,11 @@ MSG_CLIENT_VAR_SET = 9 # Set client variable
|
|
|
19
19
|
MSG_CLIENT_VAR_SYNC = 10 # Sync client variables
|
|
20
20
|
MSG_CLIENT_POSE = 11
|
|
21
21
|
MSG_ROOM_POSE = 12
|
|
22
|
+
MSG_OBJECT_POSE = 13 # Client → Server: owned object Transform
|
|
23
|
+
MSG_ROOM_OBJECTS = 14 # Server → Clients (PUB): room object states
|
|
24
|
+
MSG_OBJECT_OWNERSHIP_REQUEST = 15 # Client → Server: RequestOwnership/ReleaseOwnership
|
|
25
|
+
MSG_OBJECT_OWNERSHIP_CHANGED = 16 # Server → Clients (ROUTER): ownership changed
|
|
26
|
+
MSG_OBJECT_OWNERSHIP_REJECTED = 17 # Server → Client (ROUTER): request rejected
|
|
22
27
|
|
|
23
28
|
# Transform data type identifiers (deprecated - kept for reference)
|
|
24
29
|
|
|
@@ -27,7 +32,7 @@ MSG_ROOM_POSE = 12
|
|
|
27
32
|
_max_virtual_transforms = 50
|
|
28
33
|
MAX_VIRTUAL_TRANSFORMS = _max_virtual_transforms # Legacy alias for backward compat
|
|
29
34
|
|
|
30
|
-
# Protocol
|
|
35
|
+
# Protocol v4 transform encoding constants
|
|
31
36
|
ABS_POS_SCALE = 0.01
|
|
32
37
|
LOCO_POS_SCALE = 0.01
|
|
33
38
|
REL_POS_SCALE = 0.005
|
|
@@ -367,7 +372,7 @@ def _create_transform_dict(
|
|
|
367
372
|
|
|
368
373
|
|
|
369
374
|
def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
370
|
-
"""Serialize a client body in protocol
|
|
375
|
+
"""Serialize a client body in protocol v4 compact format."""
|
|
371
376
|
pose_seq = int(client.get("poseSeq", 0)) & 0xFFFF
|
|
372
377
|
head = client.get("head", {}) or {}
|
|
373
378
|
right = client.get("rightHand", {}) or {}
|
|
@@ -375,6 +380,7 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
|
375
380
|
virtuals = client.get("virtuals", []) or []
|
|
376
381
|
has_xr_origin_delta = (
|
|
377
382
|
"xrOriginDeltaX" in client
|
|
383
|
+
or "xrOriginDeltaY" in client
|
|
378
384
|
or "xrOriginDeltaZ" in client
|
|
379
385
|
or "xrOriginDeltaYaw" in client
|
|
380
386
|
)
|
|
@@ -416,6 +422,7 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
|
416
422
|
virtual_valid = head_valid and bool(flags & POSE_FLAG_VIRTUALS_VALID)
|
|
417
423
|
|
|
418
424
|
xr_origin_delta_x = float(client.get("xrOriginDeltaX", 0.0))
|
|
425
|
+
xr_origin_delta_y = float(client.get("xrOriginDeltaY", 0.0))
|
|
419
426
|
xr_origin_delta_z = float(client.get("xrOriginDeltaZ", 0.0))
|
|
420
427
|
xr_origin_delta_yaw = float(client.get("xrOriginDeltaYaw", 0.0))
|
|
421
428
|
head_pos = _transform_get_position(head)
|
|
@@ -425,8 +432,9 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
|
425
432
|
if physical_valid:
|
|
426
433
|
buffer.extend(
|
|
427
434
|
struct.pack(
|
|
428
|
-
"<
|
|
435
|
+
"<hhhh",
|
|
429
436
|
_quantize_signed(xr_origin_delta_x, LOCO_POS_SCALE),
|
|
437
|
+
_quantize_signed(xr_origin_delta_y, LOCO_POS_SCALE),
|
|
430
438
|
_quantize_signed(xr_origin_delta_z, LOCO_POS_SCALE),
|
|
431
439
|
_quantize_signed(xr_origin_delta_yaw, PHYSICAL_YAW_SCALE),
|
|
432
440
|
)
|
|
@@ -777,7 +785,10 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
777
785
|
offset += 1
|
|
778
786
|
|
|
779
787
|
# Validate message type is within valid range
|
|
780
|
-
if
|
|
788
|
+
if (
|
|
789
|
+
message_type < MSG_CLIENT_TRANSFORM
|
|
790
|
+
or message_type > MSG_OBJECT_OWNERSHIP_REJECTED
|
|
791
|
+
):
|
|
781
792
|
# Return invalid message type with None data instead of raising exception
|
|
782
793
|
return message_type, None, b""
|
|
783
794
|
|
|
@@ -806,6 +817,28 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
806
817
|
return message_type, _deserialize_client_var_set(data, offset), b""
|
|
807
818
|
elif message_type == MSG_CLIENT_VAR_SYNC:
|
|
808
819
|
return message_type, _deserialize_client_var_sync(data, offset), b""
|
|
820
|
+
elif message_type == MSG_OBJECT_POSE:
|
|
821
|
+
return message_type, _deserialize_object_pose(data, offset), b""
|
|
822
|
+
elif message_type == MSG_ROOM_OBJECTS:
|
|
823
|
+
return message_type, _deserialize_room_objects(data, offset), b""
|
|
824
|
+
elif message_type == MSG_OBJECT_OWNERSHIP_REQUEST:
|
|
825
|
+
return (
|
|
826
|
+
message_type,
|
|
827
|
+
_deserialize_object_ownership_request(data, offset),
|
|
828
|
+
b"",
|
|
829
|
+
)
|
|
830
|
+
elif message_type == MSG_OBJECT_OWNERSHIP_CHANGED:
|
|
831
|
+
return (
|
|
832
|
+
message_type,
|
|
833
|
+
_deserialize_object_ownership_changed(data, offset),
|
|
834
|
+
b"",
|
|
835
|
+
)
|
|
836
|
+
elif message_type == MSG_OBJECT_OWNERSHIP_REJECTED:
|
|
837
|
+
return (
|
|
838
|
+
message_type,
|
|
839
|
+
_deserialize_object_ownership_rejected(data, offset),
|
|
840
|
+
b"",
|
|
841
|
+
)
|
|
809
842
|
else:
|
|
810
843
|
# Should not reach here due to validation above
|
|
811
844
|
return message_type, None, b""
|
|
@@ -819,7 +852,7 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
819
852
|
|
|
820
853
|
|
|
821
854
|
def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any], int]:
|
|
822
|
-
"""Deserialize protocol
|
|
855
|
+
"""Deserialize protocol v4 compact pose body."""
|
|
823
856
|
result: dict[str, Any] = {}
|
|
824
857
|
result["poseSeq"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
825
858
|
offset += 2
|
|
@@ -844,6 +877,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
844
877
|
head_pos = (0.0, 0.0, 0.0)
|
|
845
878
|
head_rot = (0.0, 0.0, 0.0, 1.0)
|
|
846
879
|
xr_origin_delta_x = 0.0
|
|
880
|
+
xr_origin_delta_y = 0.0
|
|
847
881
|
xr_origin_delta_z = 0.0
|
|
848
882
|
xr_origin_delta_yaw = 0.0
|
|
849
883
|
|
|
@@ -852,11 +886,12 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
852
886
|
raise ValueError(
|
|
853
887
|
"PhysicalValid set but XROrigin delta encoding flag is missing"
|
|
854
888
|
)
|
|
855
|
-
dx_q, dz_q, dyaw_q = struct.unpack("<
|
|
889
|
+
dx_q, dy_q, dz_q, dyaw_q = struct.unpack("<hhhh", data[offset : offset + 8])
|
|
856
890
|
xr_origin_delta_x = _dequantize_signed(dx_q, LOCO_POS_SCALE)
|
|
891
|
+
xr_origin_delta_y = _dequantize_signed(dy_q, LOCO_POS_SCALE)
|
|
857
892
|
xr_origin_delta_z = _dequantize_signed(dz_q, LOCO_POS_SCALE)
|
|
858
893
|
xr_origin_delta_yaw = _dequantize_signed(dyaw_q, PHYSICAL_YAW_SCALE)
|
|
859
|
-
offset +=
|
|
894
|
+
offset += 8
|
|
860
895
|
|
|
861
896
|
if head_valid:
|
|
862
897
|
hx_q, offset = _unpack_int24_le(data, offset)
|
|
@@ -883,7 +918,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
883
918
|
|
|
884
919
|
if physical_valid and head_valid:
|
|
885
920
|
translated_x = head_pos[0] - xr_origin_delta_x
|
|
886
|
-
translated_y = head_pos[1]
|
|
921
|
+
translated_y = head_pos[1] - xr_origin_delta_y
|
|
887
922
|
translated_z = head_pos[2] - xr_origin_delta_z
|
|
888
923
|
physical_pos = _rotate_yaw_vector(
|
|
889
924
|
translated_x,
|
|
@@ -1011,6 +1046,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
1011
1046
|
)
|
|
1012
1047
|
|
|
1013
1048
|
result["xrOriginDeltaX"] = xr_origin_delta_x
|
|
1049
|
+
result["xrOriginDeltaY"] = xr_origin_delta_y
|
|
1014
1050
|
result["xrOriginDeltaZ"] = xr_origin_delta_z
|
|
1015
1051
|
result["xrOriginDeltaYaw"] = xr_origin_delta_yaw
|
|
1016
1052
|
result["physical"] = physical
|
|
@@ -1022,7 +1058,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
1022
1058
|
|
|
1023
1059
|
|
|
1024
1060
|
def _deserialize_client_transform(data: bytes, offset: int) -> dict[str, Any]:
|
|
1025
|
-
"""Deserialize client pose (
|
|
1061
|
+
"""Deserialize client pose (v4) from binary data."""
|
|
1026
1062
|
result: dict[str, Any] = {}
|
|
1027
1063
|
|
|
1028
1064
|
protocol_version = data[offset]
|
|
@@ -1060,7 +1096,7 @@ def _deserialize_rpc_message(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1060
1096
|
|
|
1061
1097
|
|
|
1062
1098
|
def _deserialize_room_transform(data: bytes, offset: int) -> dict[str, Any]:
|
|
1063
|
-
"""Deserialize room pose (
|
|
1099
|
+
"""Deserialize room pose (v4) with client numbers only."""
|
|
1064
1100
|
result: dict[str, Any] = {}
|
|
1065
1101
|
|
|
1066
1102
|
protocol_version = data[offset]
|
|
@@ -1225,3 +1261,207 @@ def _deserialize_client_var_sync(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1225
1261
|
result["clientVariables"][str(client_no)] = variables
|
|
1226
1262
|
|
|
1227
1263
|
return result
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
# ---------------------------------------------------------------------------
|
|
1267
|
+
# NetSyncObject serialization / deserialization
|
|
1268
|
+
# ---------------------------------------------------------------------------
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def serialize_object_pose(data: dict[str, Any]) -> bytes:
|
|
1272
|
+
"""Serialize object pose message (Client -> Server)."""
|
|
1273
|
+
buffer = bytearray()
|
|
1274
|
+
buffer.append(MSG_OBJECT_POSE)
|
|
1275
|
+
buffer.append(PROTOCOL_VERSION)
|
|
1276
|
+
object_id = int(data.get("objectId", 0)) & 0xFFFFFFFF
|
|
1277
|
+
buffer.extend(struct.pack("<I", object_id))
|
|
1278
|
+
buffer.extend(struct.pack("<H", int(data.get("poseSeq", 0)) & 0xFFFF))
|
|
1279
|
+
# Position: int24 x3
|
|
1280
|
+
_pack_int24_le(
|
|
1281
|
+
buffer, _quantize_signed_int24(float(data.get("posX", 0.0)), ABS_POS_SCALE)
|
|
1282
|
+
)
|
|
1283
|
+
_pack_int24_le(
|
|
1284
|
+
buffer, _quantize_signed_int24(float(data.get("posY", 0.0)), ABS_POS_SCALE)
|
|
1285
|
+
)
|
|
1286
|
+
_pack_int24_le(
|
|
1287
|
+
buffer, _quantize_signed_int24(float(data.get("posZ", 0.0)), ABS_POS_SCALE)
|
|
1288
|
+
)
|
|
1289
|
+
# Rotation: smallest-three 32-bit
|
|
1290
|
+
qx = float(data.get("rotX", 0.0))
|
|
1291
|
+
qy = float(data.get("rotY", 0.0))
|
|
1292
|
+
qz = float(data.get("rotZ", 0.0))
|
|
1293
|
+
qw = float(data.get("rotW", 1.0))
|
|
1294
|
+
packed_rot = _compress_quaternion_smallest_three(qx, qy, qz, qw)
|
|
1295
|
+
buffer.extend(struct.pack("<I", packed_rot))
|
|
1296
|
+
return bytes(buffer)
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def serialize_room_objects(
|
|
1300
|
+
room_id: str, broadcast_time: float, objects: list[dict[str, Any]]
|
|
1301
|
+
) -> bytes:
|
|
1302
|
+
"""Serialize room objects broadcast message (Server -> Clients via PUB)."""
|
|
1303
|
+
buffer = bytearray()
|
|
1304
|
+
buffer.append(MSG_ROOM_OBJECTS)
|
|
1305
|
+
buffer.append(PROTOCOL_VERSION)
|
|
1306
|
+
buffer.extend(struct.pack("<d", broadcast_time))
|
|
1307
|
+
buffer.extend(struct.pack("<H", len(objects)))
|
|
1308
|
+
for obj in objects:
|
|
1309
|
+
object_id = int(obj.get("objectId", 0)) & 0xFFFFFFFF
|
|
1310
|
+
buffer.extend(struct.pack("<I", object_id))
|
|
1311
|
+
buffer.extend(struct.pack("<H", int(obj.get("ownerClientNo", 0)) & 0xFFFF))
|
|
1312
|
+
buffer.extend(struct.pack("<H", int(obj.get("poseSeq", 0)) & 0xFFFF))
|
|
1313
|
+
buffer.extend(struct.pack("<d", float(obj.get("poseTime", 0.0))))
|
|
1314
|
+
# body_bytes already contains pos(9B) + rot(4B) = 13 bytes
|
|
1315
|
+
body = obj.get("bodyBytes", b"")
|
|
1316
|
+
if body:
|
|
1317
|
+
buffer.extend(body)
|
|
1318
|
+
else:
|
|
1319
|
+
# Fallback: serialize from individual fields
|
|
1320
|
+
_pack_int24_le(
|
|
1321
|
+
buffer,
|
|
1322
|
+
_quantize_signed_int24(float(obj.get("posX", 0.0)), ABS_POS_SCALE),
|
|
1323
|
+
)
|
|
1324
|
+
_pack_int24_le(
|
|
1325
|
+
buffer,
|
|
1326
|
+
_quantize_signed_int24(float(obj.get("posY", 0.0)), ABS_POS_SCALE),
|
|
1327
|
+
)
|
|
1328
|
+
_pack_int24_le(
|
|
1329
|
+
buffer,
|
|
1330
|
+
_quantize_signed_int24(float(obj.get("posZ", 0.0)), ABS_POS_SCALE),
|
|
1331
|
+
)
|
|
1332
|
+
qx = float(obj.get("rotX", 0.0))
|
|
1333
|
+
qy = float(obj.get("rotY", 0.0))
|
|
1334
|
+
qz = float(obj.get("rotZ", 0.0))
|
|
1335
|
+
qw = float(obj.get("rotW", 1.0))
|
|
1336
|
+
buffer.extend(
|
|
1337
|
+
struct.pack("<I", _compress_quaternion_smallest_three(qx, qy, qz, qw))
|
|
1338
|
+
)
|
|
1339
|
+
return bytes(buffer)
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def serialize_object_ownership_changed(
|
|
1343
|
+
object_id: int, new_owner: int, previous_owner: int
|
|
1344
|
+
) -> bytes:
|
|
1345
|
+
"""Serialize ownership changed notification (Server -> Clients via ROUTER)."""
|
|
1346
|
+
buffer = bytearray()
|
|
1347
|
+
buffer.append(MSG_OBJECT_OWNERSHIP_CHANGED)
|
|
1348
|
+
buffer.extend(struct.pack("<I", object_id & 0xFFFFFFFF))
|
|
1349
|
+
buffer.extend(struct.pack("<H", new_owner & 0xFFFF))
|
|
1350
|
+
buffer.extend(struct.pack("<H", previous_owner & 0xFFFF))
|
|
1351
|
+
return bytes(buffer)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def serialize_object_ownership_rejected(
|
|
1355
|
+
object_id: int, current_owner: int, reason_code: int
|
|
1356
|
+
) -> bytes:
|
|
1357
|
+
"""Serialize ownership rejected notification (Server -> Client via ROUTER)."""
|
|
1358
|
+
buffer = bytearray()
|
|
1359
|
+
buffer.append(MSG_OBJECT_OWNERSHIP_REJECTED)
|
|
1360
|
+
buffer.extend(struct.pack("<I", object_id & 0xFFFFFFFF))
|
|
1361
|
+
buffer.extend(struct.pack("<H", current_owner & 0xFFFF))
|
|
1362
|
+
buffer.append(reason_code & 0xFF)
|
|
1363
|
+
return bytes(buffer)
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
def _deserialize_object_pose(data: bytes, offset: int) -> dict[str, Any]:
|
|
1367
|
+
"""Deserialize object pose (Client -> Server)."""
|
|
1368
|
+
result: dict[str, Any] = {}
|
|
1369
|
+
protocol_version = data[offset]
|
|
1370
|
+
offset += 1
|
|
1371
|
+
if protocol_version != PROTOCOL_VERSION:
|
|
1372
|
+
raise ValueError(f"Unsupported protocol version: {protocol_version}")
|
|
1373
|
+
result["objectId"] = struct.unpack("<I", data[offset : offset + 4])[0]
|
|
1374
|
+
offset += 4
|
|
1375
|
+
result["poseSeq"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1376
|
+
offset += 2
|
|
1377
|
+
# Extract body bytes (pos 9B + rot 4B = 13 bytes) for caching
|
|
1378
|
+
body_start = offset
|
|
1379
|
+
px, offset = _unpack_int24_le(data, offset)
|
|
1380
|
+
py, offset = _unpack_int24_le(data, offset)
|
|
1381
|
+
pz, offset = _unpack_int24_le(data, offset)
|
|
1382
|
+
packed_rot = struct.unpack("<I", data[offset : offset + 4])[0]
|
|
1383
|
+
offset += 4
|
|
1384
|
+
result["bodyBytes"] = data[body_start:offset]
|
|
1385
|
+
result["posX"] = _dequantize_signed(px, ABS_POS_SCALE)
|
|
1386
|
+
result["posY"] = _dequantize_signed(py, ABS_POS_SCALE)
|
|
1387
|
+
result["posZ"] = _dequantize_signed(pz, ABS_POS_SCALE)
|
|
1388
|
+
qx, qy, qz, qw = _decompress_quaternion_smallest_three(packed_rot)
|
|
1389
|
+
result["rotX"] = qx
|
|
1390
|
+
result["rotY"] = qy
|
|
1391
|
+
result["rotZ"] = qz
|
|
1392
|
+
result["rotW"] = qw
|
|
1393
|
+
return result
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def _deserialize_room_objects(data: bytes, offset: int) -> dict[str, Any]:
|
|
1397
|
+
"""Deserialize room objects broadcast."""
|
|
1398
|
+
result: dict[str, Any] = {}
|
|
1399
|
+
protocol_version = data[offset]
|
|
1400
|
+
offset += 1
|
|
1401
|
+
if protocol_version != PROTOCOL_VERSION:
|
|
1402
|
+
raise ValueError(f"Unsupported protocol version: {protocol_version}")
|
|
1403
|
+
result["broadcastTime"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1404
|
+
offset += 8
|
|
1405
|
+
object_count = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1406
|
+
offset += 2
|
|
1407
|
+
objects: list[dict[str, Any]] = []
|
|
1408
|
+
for _ in range(object_count):
|
|
1409
|
+
obj: dict[str, Any] = {}
|
|
1410
|
+
obj["objectId"] = struct.unpack("<I", data[offset : offset + 4])[0]
|
|
1411
|
+
offset += 4
|
|
1412
|
+
obj["ownerClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1413
|
+
offset += 2
|
|
1414
|
+
obj["poseSeq"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1415
|
+
offset += 2
|
|
1416
|
+
obj["poseTime"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1417
|
+
offset += 8
|
|
1418
|
+
px, offset = _unpack_int24_le(data, offset)
|
|
1419
|
+
py, offset = _unpack_int24_le(data, offset)
|
|
1420
|
+
pz, offset = _unpack_int24_le(data, offset)
|
|
1421
|
+
packed_rot = struct.unpack("<I", data[offset : offset + 4])[0]
|
|
1422
|
+
offset += 4
|
|
1423
|
+
obj["posX"] = _dequantize_signed(px, ABS_POS_SCALE)
|
|
1424
|
+
obj["posY"] = _dequantize_signed(py, ABS_POS_SCALE)
|
|
1425
|
+
obj["posZ"] = _dequantize_signed(pz, ABS_POS_SCALE)
|
|
1426
|
+
qx, qy, qz, qw = _decompress_quaternion_smallest_three(packed_rot)
|
|
1427
|
+
obj["rotX"] = qx
|
|
1428
|
+
obj["rotY"] = qy
|
|
1429
|
+
obj["rotZ"] = qz
|
|
1430
|
+
obj["rotW"] = qw
|
|
1431
|
+
objects.append(obj)
|
|
1432
|
+
result["objects"] = objects
|
|
1433
|
+
return result
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def _deserialize_object_ownership_request(data: bytes, offset: int) -> dict[str, Any]:
|
|
1437
|
+
"""Deserialize ownership request (Client -> Server)."""
|
|
1438
|
+
result: dict[str, Any] = {}
|
|
1439
|
+
result["operationType"] = data[offset]
|
|
1440
|
+
offset += 1
|
|
1441
|
+
result["objectId"] = struct.unpack("<I", data[offset : offset + 4])[0]
|
|
1442
|
+
offset += 4
|
|
1443
|
+
return result
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def _deserialize_object_ownership_changed(data: bytes, offset: int) -> dict[str, Any]:
|
|
1447
|
+
"""Deserialize ownership changed notification."""
|
|
1448
|
+
result: dict[str, Any] = {}
|
|
1449
|
+
result["objectId"] = struct.unpack("<I", data[offset : offset + 4])[0]
|
|
1450
|
+
offset += 4
|
|
1451
|
+
result["newOwnerClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1452
|
+
offset += 2
|
|
1453
|
+
result["previousOwnerClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1454
|
+
offset += 2
|
|
1455
|
+
return result
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
def _deserialize_object_ownership_rejected(data: bytes, offset: int) -> dict[str, Any]:
|
|
1459
|
+
"""Deserialize ownership rejected notification."""
|
|
1460
|
+
result: dict[str, Any] = {}
|
|
1461
|
+
result["objectId"] = struct.unpack("<I", data[offset : offset + 4])[0]
|
|
1462
|
+
offset += 4
|
|
1463
|
+
result["currentOwnerClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1464
|
+
offset += 2
|
|
1465
|
+
result["reasonCode"] = data[offset]
|
|
1466
|
+
offset += 1
|
|
1467
|
+
return result
|