styly-netsync-server 0.10.4__tar.gz → 0.12.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.
Files changed (42) hide show
  1. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/PKG-INFO +71 -17
  2. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/README.md +70 -16
  3. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/pyproject.toml +1 -1
  4. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/binary_serializer.py +91 -29
  5. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/rest_bridge.py +67 -9
  6. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/server.py +2 -2
  7. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync_server.egg-info/PKG-INFO +71 -17
  8. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_binary_serializer.py +225 -14
  9. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_rest_bridge.py +153 -7
  10. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/LICENSE +0 -0
  11. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/setup.cfg +0 -0
  12. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/__init__.py +0 -0
  13. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/__main__.py +0 -0
  14. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/adapters.py +0 -0
  15. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/cli.py +0 -0
  16. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/client.py +0 -0
  17. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/client_simulator.py +0 -0
  18. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/config.py +0 -0
  19. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/default.toml +0 -0
  20. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/events.py +0 -0
  21. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/logging_utils.py +0 -0
  22. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/network_utils.py +0 -0
  23. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/nv_sync.py +0 -0
  24. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync/types.py +0 -0
  25. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync_server.egg-info/SOURCES.txt +0 -0
  26. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync_server.egg-info/dependency_links.txt +0 -0
  27. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync_server.egg-info/entry_points.txt +0 -0
  28. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync_server.egg-info/requires.txt +0 -0
  29. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/src/styly_netsync_server.egg-info/top_level.txt +0 -0
  30. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_all_run_methods.py +0 -0
  31. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_config.py +0 -0
  32. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_discovery_probe.py +0 -0
  33. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_logging_cli.py +0 -0
  34. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_multi_nic.py +0 -0
  35. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_nv_protocol.py +0 -0
  36. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_object_sync.py +0 -0
  37. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_port_error_message.py +0 -0
  38. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_python_client.py +0 -0
  39. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_reconnect_identity.py +0 -0
  40. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_room_expiry.py +0 -0
  41. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.0}/tests/test_stealth_heartbeat.py +0 -0
  42. {styly_netsync_server-0.10.4 → styly_netsync_server-0.12.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.10.4
3
+ Version: 0.12.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,16 @@ 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 = 3`.
85
- - Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V3 pose body.
86
- - Legacy transform protocol v2 and JSON transform fallback are not supported.
84
+ - Current transform wire protocol is `protocolVersion = 5`.
85
+ - Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V5 pose body.
86
+ - v5 adds an optional `MovingFloorLocal` pose flag. Bound avatars send head, hands, and virtual transforms in the registered moving floor's local coordinates, and reuse the existing 8-byte physical slot as direct physical position/yaw.
87
+ - Unbound v5 poses keep the v4 `xrOriginDelta` semantics: `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.
88
+ - Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
87
89
  - Deploy Unity and Python updates together when changing transform protocol behavior.
88
- - Protocol v3 position quantization ranges:
89
- - Absolute (`physicalPos`, `headPosAbs`): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
90
+ - Protocol v5 position quantization ranges:
91
+ - Absolute (`headPosAbs` only): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
92
+ - XROrigin locomotion delta for unbound poses (`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.
93
+ - Direct physical payload for moving-floor-local poses (`physical`, 4×`int16`: `x, y, z, yaw`): `0.01 m` per unit for translation, `0.1°` for yaw.
90
94
  - Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
91
95
  - 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
96
 
@@ -94,18 +98,18 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
94
98
 
95
99
  The following options summarize trade-offs when expanding absolute-position range.
96
100
 
97
- Assumed baseline (`protocolVersion=3`):
98
- - Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `49 bytes`
99
- - Room per-client entry (`clientNo + poseTime + clientBody`): `59 bytes`
101
+ Assumed unbound baseline (`protocolVersion=5`, `MovingFloorLocal` off):
102
+ - Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `46 bytes` (matches `test_client_body_size_with_full_pose_no_virtuals`).
103
+ - Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
100
104
 
101
105
  | Option | Absolute Position Encoding | Per-axis Range | Client Body Delta | Room Per-client Delta |
102
106
  |---|---|---:|---:|---:|
103
- | A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`49 -> 49`) | `+0B` (`59 -> 59`) |
104
- | B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`49 -> 55`, `+12.2%`) | `+6B` (`59 -> 65`, `+10.2%`) |
105
- | C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`49 -> 55`, `+12.2%`) | `+6B` (`59 -> 65`, `+10.2%`) |
107
+ | A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`46 -> 46`) | `+0B` (`56 -> 56`) |
108
+ | 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%`) |
109
+ | 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
110
 
107
111
  Notes:
108
- - Option B/C deltas assume both absolute transforms are present (`physicalPos` and `headPosAbs`).
112
+ - 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
113
  - 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
114
 
111
115
  ## Configuration
@@ -160,7 +164,7 @@ The server launches an embedded FastAPI application that exposes REST endpoints
160
164
 
161
165
  ```json
162
166
  {
163
- "vars": {
167
+ "variables": {
164
168
  "name": "Jack",
165
169
  "lang": "EN"
166
170
  }
@@ -179,11 +183,35 @@ The server launches an embedded FastAPI application that exposes REST endpoints
179
183
  ```bash
180
184
  curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables" \
181
185
  -H "Content-Type: application/json" \
182
- -d '{"vars":{"name":"Jack","lang":"EN"}}'
186
+ -d '{"variables":{"name":"Jack","lang":"EN"}}'
183
187
  ```
184
188
 
185
189
  The response includes the current mapping status (`clientNo` or `null`) and whether each key was `"applied"` or `"queued"`.
186
190
 
191
+ - Read endpoints:
192
+ - `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables` — returns all client variables for the device.
193
+
194
+ ```json
195
+ {"clientNo": 7, "variables": {"name": "Jack", "lang": "EN"}}
196
+ ```
197
+
198
+ If the device has no `clientNo` mapping yet, returns `{"clientNo": null, "variables": {}}`.
199
+
200
+ - `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables/{name}` — returns a single variable.
201
+
202
+ ```json
203
+ {"clientNo": 7, "value": "Jack"}
204
+ ```
205
+
206
+ Returns `404` if the device is unmapped or the variable is not set.
207
+
208
+ - Example:
209
+
210
+ ```bash
211
+ curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables"
212
+ curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables/name"
213
+ ```
214
+
187
215
  ### Global variables
188
216
 
189
217
  - Endpoint: `POST /v1/rooms/{roomId}/global-variables`
@@ -191,7 +219,7 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
191
219
 
192
220
  ```json
193
221
  {
194
- "vars": {
222
+ "variables": {
195
223
  "score": "42",
196
224
  "stage": "lobby"
197
225
  }
@@ -210,7 +238,33 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
210
238
  ```bash
211
239
  curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/global-variables" \
212
240
  -H "Content-Type: application/json" \
213
- -d '{"vars":{"score":"42","stage":"lobby"}}'
241
+ -d '{"variables":{"score":"42","stage":"lobby"}}'
214
242
  ```
215
243
 
216
244
  The response includes the room ID and whether each key was `"applied"`, `"queued"`, or `"failed"`.
245
+
246
+ - Read endpoints:
247
+ - `GET /v1/rooms/{roomId}/global-variables` — returns all global variables for the room.
248
+
249
+ ```json
250
+ {"variables": {"score": "42", "stage": "lobby"}}
251
+ ```
252
+
253
+ - `GET /v1/rooms/{roomId}/global-variables/{name}` — returns a single variable.
254
+
255
+ ```json
256
+ {"value": "42"}
257
+ ```
258
+
259
+ Returns `404` if the variable is not set.
260
+
261
+ - Example:
262
+
263
+ ```bash
264
+ curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables"
265
+ curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables/score"
266
+ ```
267
+
268
+ ### Read consistency
269
+
270
+ 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,16 @@ 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 = 3`.
46
- - Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V3 pose body.
47
- - Legacy transform protocol v2 and JSON transform fallback are not supported.
45
+ - Current transform wire protocol is `protocolVersion = 5`.
46
+ - Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V5 pose body.
47
+ - v5 adds an optional `MovingFloorLocal` pose flag. Bound avatars send head, hands, and virtual transforms in the registered moving floor's local coordinates, and reuse the existing 8-byte physical slot as direct physical position/yaw.
48
+ - Unbound v5 poses keep the v4 `xrOriginDelta` semantics: `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.
49
+ - Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
48
50
  - Deploy Unity and Python updates together when changing transform protocol behavior.
49
- - Protocol v3 position quantization ranges:
50
- - Absolute (`physicalPos`, `headPosAbs`): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
51
+ - Protocol v5 position quantization ranges:
52
+ - Absolute (`headPosAbs` only): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
53
+ - XROrigin locomotion delta for unbound poses (`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.
54
+ - Direct physical payload for moving-floor-local poses (`physical`, 4×`int16`: `x, y, z, yaw`): `0.01 m` per unit for translation, `0.1°` for yaw.
51
55
  - Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
52
56
  - 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
57
 
@@ -55,18 +59,18 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
55
59
 
56
60
  The following options summarize trade-offs when expanding absolute-position range.
57
61
 
58
- Assumed baseline (`protocolVersion=3`):
59
- - Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `49 bytes`
60
- - Room per-client entry (`clientNo + poseTime + clientBody`): `59 bytes`
62
+ Assumed unbound baseline (`protocolVersion=5`, `MovingFloorLocal` off):
63
+ - Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `46 bytes` (matches `test_client_body_size_with_full_pose_no_virtuals`).
64
+ - Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
61
65
 
62
66
  | Option | Absolute Position Encoding | Per-axis Range | Client Body Delta | Room Per-client Delta |
63
67
  |---|---|---:|---:|---:|
64
- | A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`49 -> 49`) | `+0B` (`59 -> 59`) |
65
- | B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`49 -> 55`, `+12.2%`) | `+6B` (`59 -> 65`, `+10.2%`) |
66
- | C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`49 -> 55`, `+12.2%`) | `+6B` (`59 -> 65`, `+10.2%`) |
68
+ | A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`46 -> 46`) | `+0B` (`56 -> 56`) |
69
+ | 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%`) |
70
+ | 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
71
 
68
72
  Notes:
69
- - Option B/C deltas assume both absolute transforms are present (`physicalPos` and `headPosAbs`).
73
+ - 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
74
  - 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
75
 
72
76
  ## Configuration
@@ -121,7 +125,7 @@ The server launches an embedded FastAPI application that exposes REST endpoints
121
125
 
122
126
  ```json
123
127
  {
124
- "vars": {
128
+ "variables": {
125
129
  "name": "Jack",
126
130
  "lang": "EN"
127
131
  }
@@ -140,11 +144,35 @@ The server launches an embedded FastAPI application that exposes REST endpoints
140
144
  ```bash
141
145
  curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables" \
142
146
  -H "Content-Type: application/json" \
143
- -d '{"vars":{"name":"Jack","lang":"EN"}}'
147
+ -d '{"variables":{"name":"Jack","lang":"EN"}}'
144
148
  ```
145
149
 
146
150
  The response includes the current mapping status (`clientNo` or `null`) and whether each key was `"applied"` or `"queued"`.
147
151
 
152
+ - Read endpoints:
153
+ - `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables` — returns all client variables for the device.
154
+
155
+ ```json
156
+ {"clientNo": 7, "variables": {"name": "Jack", "lang": "EN"}}
157
+ ```
158
+
159
+ If the device has no `clientNo` mapping yet, returns `{"clientNo": null, "variables": {}}`.
160
+
161
+ - `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables/{name}` — returns a single variable.
162
+
163
+ ```json
164
+ {"clientNo": 7, "value": "Jack"}
165
+ ```
166
+
167
+ Returns `404` if the device is unmapped or the variable is not set.
168
+
169
+ - Example:
170
+
171
+ ```bash
172
+ curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables"
173
+ curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables/name"
174
+ ```
175
+
148
176
  ### Global variables
149
177
 
150
178
  - Endpoint: `POST /v1/rooms/{roomId}/global-variables`
@@ -152,7 +180,7 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
152
180
 
153
181
  ```json
154
182
  {
155
- "vars": {
183
+ "variables": {
156
184
  "score": "42",
157
185
  "stage": "lobby"
158
186
  }
@@ -171,7 +199,33 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
171
199
  ```bash
172
200
  curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/global-variables" \
173
201
  -H "Content-Type: application/json" \
174
- -d '{"vars":{"score":"42","stage":"lobby"}}'
202
+ -d '{"variables":{"score":"42","stage":"lobby"}}'
175
203
  ```
176
204
 
177
205
  The response includes the room ID and whether each key was `"applied"`, `"queued"`, or `"failed"`.
206
+
207
+ - Read endpoints:
208
+ - `GET /v1/rooms/{roomId}/global-variables` — returns all global variables for the room.
209
+
210
+ ```json
211
+ {"variables": {"score": "42", "stage": "lobby"}}
212
+ ```
213
+
214
+ - `GET /v1/rooms/{roomId}/global-variables/{name}` — returns a single variable.
215
+
216
+ ```json
217
+ {"value": "42"}
218
+ ```
219
+
220
+ Returns `404` if the variable is not set.
221
+
222
+ - Example:
223
+
224
+ ```bash
225
+ curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables"
226
+ curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables/score"
227
+ ```
228
+
229
+ ### Read consistency
230
+
231
+ 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.10.4"
7
+ version = "0.12.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"
@@ -6,7 +6,7 @@ from typing import Any
6
6
  logger = logging.getLogger(__name__)
7
7
 
8
8
  # Message type identifiers
9
- PROTOCOL_VERSION = 3
9
+ PROTOCOL_VERSION = 5
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
@@ -32,7 +32,7 @@ MSG_OBJECT_OWNERSHIP_REJECTED = 17 # Server → Client (ROUTER): request reject
32
32
  _max_virtual_transforms = 50
33
33
  MAX_VIRTUAL_TRANSFORMS = _max_virtual_transforms # Legacy alias for backward compat
34
34
 
35
- # Protocol v3 transform encoding constants
35
+ # Protocol v5 transform encoding constants
36
36
  ABS_POS_SCALE = 0.01
37
37
  LOCO_POS_SCALE = 0.01
38
38
  REL_POS_SCALE = 0.005
@@ -69,6 +69,15 @@ POSE_FLAG_HEAD_VALID = 1 << 2
69
69
  POSE_FLAG_RIGHT_VALID = 1 << 3
70
70
  POSE_FLAG_LEFT_VALID = 1 << 4
71
71
  POSE_FLAG_VIRTUALS_VALID = 1 << 5
72
+ POSE_FLAG_MOVING_FLOOR_LOCAL = 1 << 6
73
+
74
+
75
+ def _compute_encoding_flags(flags: int) -> int:
76
+ """Return pose encoding flags for the sanitized pose flags."""
77
+ encoding_flags = ENCODING_FLAGS_DEFAULT
78
+ if flags & POSE_FLAG_MOVING_FLOOR_LOCAL:
79
+ encoding_flags &= ~ENCODING_PHYSICAL_IS_XRORIGIN_DELTA
80
+ return encoding_flags & 0xFF
72
81
 
73
82
 
74
83
  def get_max_virtual_transforms() -> int:
@@ -372,7 +381,7 @@ def _create_transform_dict(
372
381
 
373
382
 
374
383
  def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
375
- """Serialize a client body in protocol v3 compact format."""
384
+ """Serialize a client body in protocol v5 compact format."""
376
385
  pose_seq = int(client.get("poseSeq", 0)) & 0xFFFF
377
386
  head = client.get("head", {}) or {}
378
387
  right = client.get("rightHand", {}) or {}
@@ -380,6 +389,7 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
380
389
  virtuals = client.get("virtuals", []) or []
381
390
  has_xr_origin_delta = (
382
391
  "xrOriginDeltaX" in client
392
+ or "xrOriginDeltaY" in client
383
393
  or "xrOriginDeltaZ" in client
384
394
  or "xrOriginDeltaYaw" in client
385
395
  )
@@ -410,32 +420,51 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
410
420
  POSE_FLAG_RIGHT_VALID | POSE_FLAG_LEFT_VALID | POSE_FLAG_VIRTUALS_VALID
411
421
  )
412
422
 
423
+ encoding_flags = _compute_encoding_flags(flags)
413
424
  buffer.extend(struct.pack("<H", pose_seq))
414
425
  buffer.append(flags)
415
- buffer.append(ENCODING_FLAGS_DEFAULT)
426
+ buffer.append(encoding_flags)
416
427
 
417
428
  physical_valid = bool(flags & POSE_FLAG_PHYSICAL_VALID)
418
429
  head_valid = bool(flags & POSE_FLAG_HEAD_VALID)
419
430
  right_valid = head_valid and bool(flags & POSE_FLAG_RIGHT_VALID)
420
431
  left_valid = head_valid and bool(flags & POSE_FLAG_LEFT_VALID)
421
432
  virtual_valid = head_valid and bool(flags & POSE_FLAG_VIRTUALS_VALID)
433
+ moving_floor_local = bool(flags & POSE_FLAG_MOVING_FLOOR_LOCAL)
422
434
 
423
435
  xr_origin_delta_x = float(client.get("xrOriginDeltaX", 0.0))
436
+ xr_origin_delta_y = float(client.get("xrOriginDeltaY", 0.0))
424
437
  xr_origin_delta_z = float(client.get("xrOriginDeltaZ", 0.0))
425
438
  xr_origin_delta_yaw = float(client.get("xrOriginDeltaYaw", 0.0))
439
+ physical = client.get("physical", {}) or {}
426
440
  head_pos = _transform_get_position(head)
427
441
  head_rot = _transform_get_quaternion(head)
428
442
  head_rot_n = _normalize_quaternion(*head_rot)
429
443
 
430
444
  if physical_valid:
431
- buffer.extend(
432
- struct.pack(
433
- "<hhh",
434
- _quantize_signed(xr_origin_delta_x, LOCO_POS_SCALE),
435
- _quantize_signed(xr_origin_delta_z, LOCO_POS_SCALE),
436
- _quantize_signed(xr_origin_delta_yaw, PHYSICAL_YAW_SCALE),
445
+ if moving_floor_local:
446
+ physical_pos = _transform_get_position(physical)
447
+ physical_rot = _transform_get_quaternion(physical)
448
+ physical_yaw = _quaternion_to_yaw_degrees(*physical_rot)
449
+ buffer.extend(
450
+ struct.pack(
451
+ "<hhhh",
452
+ _quantize_signed(physical_pos[0], LOCO_POS_SCALE),
453
+ _quantize_signed(physical_pos[1], LOCO_POS_SCALE),
454
+ _quantize_signed(physical_pos[2], LOCO_POS_SCALE),
455
+ _quantize_signed(physical_yaw, PHYSICAL_YAW_SCALE),
456
+ )
457
+ )
458
+ else:
459
+ buffer.extend(
460
+ struct.pack(
461
+ "<hhhh",
462
+ _quantize_signed(xr_origin_delta_x, LOCO_POS_SCALE),
463
+ _quantize_signed(xr_origin_delta_y, LOCO_POS_SCALE),
464
+ _quantize_signed(xr_origin_delta_z, LOCO_POS_SCALE),
465
+ _quantize_signed(xr_origin_delta_yaw, PHYSICAL_YAW_SCALE),
466
+ )
437
467
  )
438
- )
439
468
 
440
469
  if head_valid:
441
470
  _pack_int24_le(buffer, _quantize_signed_int24(head_pos[0], ABS_POS_SCALE))
@@ -849,7 +878,7 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
849
878
 
850
879
 
851
880
  def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any], int]:
852
- """Deserialize protocol v3 compact pose body."""
881
+ """Deserialize protocol v5 compact pose body."""
853
882
  result: dict[str, Any] = {}
854
883
  result["poseSeq"] = struct.unpack("<H", data[offset : offset + 2])[0]
855
884
  offset += 2
@@ -865,28 +894,41 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
865
894
  right_valid = head_valid and bool(flags & POSE_FLAG_RIGHT_VALID)
866
895
  left_valid = head_valid and bool(flags & POSE_FLAG_LEFT_VALID)
867
896
  virtual_valid = head_valid and bool(flags & POSE_FLAG_VIRTUALS_VALID)
897
+ moving_floor_local = bool(flags & POSE_FLAG_MOVING_FLOOR_LOCAL)
868
898
 
869
899
  physical = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, True)
870
- head = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, False)
871
- right = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, False)
872
- left = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, False)
900
+ head = _create_transform_dict(
901
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
902
+ )
903
+ right = _create_transform_dict(
904
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
905
+ )
906
+ left = _create_transform_dict(
907
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
908
+ )
873
909
 
874
910
  head_pos = (0.0, 0.0, 0.0)
875
911
  head_rot = (0.0, 0.0, 0.0, 1.0)
876
912
  xr_origin_delta_x = 0.0
913
+ xr_origin_delta_y = 0.0
877
914
  xr_origin_delta_z = 0.0
878
915
  xr_origin_delta_yaw = 0.0
879
916
 
880
917
  if physical_valid:
881
- if (encoding_flags & ENCODING_PHYSICAL_IS_XRORIGIN_DELTA) == 0:
918
+ if (
919
+ not moving_floor_local
920
+ and (encoding_flags & ENCODING_PHYSICAL_IS_XRORIGIN_DELTA) == 0
921
+ ):
882
922
  raise ValueError(
883
923
  "PhysicalValid set but XROrigin delta encoding flag is missing"
884
924
  )
885
- dx_q, dz_q, dyaw_q = struct.unpack("<hhh", data[offset : offset + 6])
886
- xr_origin_delta_x = _dequantize_signed(dx_q, LOCO_POS_SCALE)
887
- xr_origin_delta_z = _dequantize_signed(dz_q, LOCO_POS_SCALE)
888
- xr_origin_delta_yaw = _dequantize_signed(dyaw_q, PHYSICAL_YAW_SCALE)
889
- offset += 6
925
+ dx_q, dy_q, dz_q, dyaw_q = struct.unpack("<hhhh", data[offset : offset + 8])
926
+ if not moving_floor_local:
927
+ xr_origin_delta_x = _dequantize_signed(dx_q, LOCO_POS_SCALE)
928
+ xr_origin_delta_y = _dequantize_signed(dy_q, LOCO_POS_SCALE)
929
+ xr_origin_delta_z = _dequantize_signed(dz_q, LOCO_POS_SCALE)
930
+ xr_origin_delta_yaw = _dequantize_signed(dyaw_q, PHYSICAL_YAW_SCALE)
931
+ offset += 8
890
932
 
891
933
  if head_valid:
892
934
  hx_q, offset = _unpack_int24_le(data, offset)
@@ -908,12 +950,31 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
908
950
  head_rot[1],
909
951
  head_rot[2],
910
952
  head_rot[3],
911
- False,
953
+ moving_floor_local,
912
954
  )
913
955
 
914
- if physical_valid and head_valid:
956
+ if physical_valid and moving_floor_local:
957
+ physical_pos = (
958
+ _dequantize_signed(dx_q, LOCO_POS_SCALE),
959
+ _dequantize_signed(dy_q, LOCO_POS_SCALE),
960
+ _dequantize_signed(dz_q, LOCO_POS_SCALE),
961
+ )
962
+ physical_rot = _yaw_degrees_to_quaternion(
963
+ _dequantize_signed(dyaw_q, PHYSICAL_YAW_SCALE)
964
+ )
965
+ physical = _create_transform_dict(
966
+ physical_pos[0],
967
+ physical_pos[1],
968
+ physical_pos[2],
969
+ physical_rot[0],
970
+ physical_rot[1],
971
+ physical_rot[2],
972
+ physical_rot[3],
973
+ True,
974
+ )
975
+ elif physical_valid and head_valid:
915
976
  translated_x = head_pos[0] - xr_origin_delta_x
916
- translated_y = head_pos[1]
977
+ translated_y = head_pos[1] - xr_origin_delta_y
917
978
  translated_z = head_pos[2] - xr_origin_delta_z
918
979
  physical_pos = _rotate_yaw_vector(
919
980
  translated_x,
@@ -965,7 +1026,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
965
1026
  abs_rot[1],
966
1027
  abs_rot[2],
967
1028
  abs_rot[3],
968
- False,
1029
+ moving_floor_local,
969
1030
  )
970
1031
 
971
1032
  if left_valid:
@@ -994,7 +1055,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
994
1055
  abs_rot[1],
995
1056
  abs_rot[2],
996
1057
  abs_rot[3],
997
- False,
1058
+ moving_floor_local,
998
1059
  )
999
1060
 
1000
1061
  virtual_count = data[offset]
@@ -1036,11 +1097,12 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
1036
1097
  abs_rot[1],
1037
1098
  abs_rot[2],
1038
1099
  abs_rot[3],
1039
- False,
1100
+ moving_floor_local,
1040
1101
  )
1041
1102
  )
1042
1103
 
1043
1104
  result["xrOriginDeltaX"] = xr_origin_delta_x
1105
+ result["xrOriginDeltaY"] = xr_origin_delta_y
1044
1106
  result["xrOriginDeltaZ"] = xr_origin_delta_z
1045
1107
  result["xrOriginDeltaYaw"] = xr_origin_delta_yaw
1046
1108
  result["physical"] = physical
@@ -1052,7 +1114,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
1052
1114
 
1053
1115
 
1054
1116
  def _deserialize_client_transform(data: bytes, offset: int) -> dict[str, Any]:
1055
- """Deserialize client pose (v3) from binary data."""
1117
+ """Deserialize client pose (v5) from binary data."""
1056
1118
  result: dict[str, Any] = {}
1057
1119
 
1058
1120
  protocol_version = data[offset]
@@ -1090,7 +1152,7 @@ def _deserialize_rpc_message(data: bytes, offset: int) -> dict[str, Any]:
1090
1152
 
1091
1153
 
1092
1154
  def _deserialize_room_transform(data: bytes, offset: int) -> dict[str, Any]:
1093
- """Deserialize room pose (v3) with client numbers only."""
1155
+ """Deserialize room pose (v5) with client numbers only."""
1094
1156
  result: dict[str, Any] = {}
1095
1157
 
1096
1158
  protocol_version = data[offset]