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.
Files changed (42) hide show
  1. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/PKG-INFO +69 -17
  2. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/README.md +68 -16
  3. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/pyproject.toml +1 -1
  4. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/binary_serializer.py +251 -11
  5. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/client_simulator.py +10 -0
  6. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/rest_bridge.py +67 -9
  7. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/server.py +338 -30
  8. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/PKG-INFO +69 -17
  9. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/SOURCES.txt +2 -0
  10. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_binary_serializer.py +54 -13
  11. styly_netsync_server-0.11.0/tests/test_discovery_probe.py +103 -0
  12. styly_netsync_server-0.11.0/tests/test_object_sync.py +440 -0
  13. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_rest_bridge.py +153 -7
  14. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/LICENSE +0 -0
  15. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/setup.cfg +0 -0
  16. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/__init__.py +0 -0
  17. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/__main__.py +0 -0
  18. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/adapters.py +0 -0
  19. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/cli.py +0 -0
  20. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/client.py +0 -0
  21. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/config.py +0 -0
  22. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/default.toml +0 -0
  23. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/events.py +0 -0
  24. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/logging_utils.py +0 -0
  25. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/network_utils.py +0 -0
  26. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/nv_sync.py +0 -0
  27. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync/types.py +0 -0
  28. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/dependency_links.txt +0 -0
  29. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/entry_points.txt +0 -0
  30. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/requires.txt +0 -0
  31. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/top_level.txt +0 -0
  32. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_all_run_methods.py +0 -0
  33. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_config.py +0 -0
  34. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_logging_cli.py +0 -0
  35. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_multi_nic.py +0 -0
  36. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_nv_protocol.py +0 -0
  37. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_port_error_message.py +0 -0
  38. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_python_client.py +0 -0
  39. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_reconnect_identity.py +0 -0
  40. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_room_expiry.py +0 -0
  41. {styly_netsync_server-0.10.3 → styly_netsync_server-0.11.0}/tests/test_stealth_heartbeat.py +0 -0
  42. {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.10.3
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 = 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 = 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 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]`.
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=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`
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` (`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%`) |
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
- - Option B/C deltas assume both absolute transforms are present (`physicalPos` and `headPosAbs`).
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
- "vars": {
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 '{"vars":{"name":"Jack","lang":"EN"}}'
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
- "vars": {
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 '{"vars":{"score":"42","stage":"lobby"}}'
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 = 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 = 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 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]`.
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=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`
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` (`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%`) |
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
- - Option B/C deltas assume both absolute transforms are present (`physicalPos` and `headPosAbs`).
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
- "vars": {
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 '{"vars":{"name":"Jack","lang":"EN"}}'
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
- "vars": {
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 '{"vars":{"score":"42","stage":"lobby"}}'
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.10.3"
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"
@@ -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 = 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 v3 transform encoding constants
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 v3 compact format."""
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
- "<hhh",
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 message_type < MSG_CLIENT_TRANSFORM or message_type > MSG_ROOM_POSE:
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 v3 compact pose body."""
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("<hhh", data[offset : offset + 6])
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 += 6
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 (v3) from binary data."""
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 (v3) with client numbers only."""
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