styly-netsync-server 0.11.0__tar.gz → 0.13.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.11.0 → styly_netsync_server-0.13.0}/PKG-INFO +9 -7
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/README.md +8 -6
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/pyproject.toml +1 -1
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/binary_serializer.py +118 -32
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/client.py +89 -19
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/client_simulator.py +2 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/rest_bridge.py +74 -64
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/server.py +239 -33
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/PKG-INFO +9 -7
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/SOURCES.txt +1 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_binary_serializer.py +196 -12
- styly_netsync_server-0.13.0/tests/test_client_variable_device_store.py +468 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/LICENSE +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/setup.cfg +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/__init__.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/__main__.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/adapters.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/cli.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/config.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/default.toml +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/events.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/logging_utils.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/network_utils.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/nv_sync.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/types.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/dependency_links.txt +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/entry_points.txt +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/requires.txt +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/top_level.txt +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_all_run_methods.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_config.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_discovery_probe.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_logging_cli.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_multi_nic.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_nv_protocol.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_object_sync.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_port_error_message.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_python_client.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_reconnect_identity.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_rest_bridge.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_room_expiry.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/tests/test_stealth_heartbeat.py +0 -0
- {styly_netsync_server-0.11.0 → styly_netsync_server-0.13.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.13.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,14 +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 =
|
|
85
|
-
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact
|
|
86
|
-
-
|
|
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.
|
|
87
88
|
- Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
|
|
88
89
|
- Deploy Unity and Python updates together when changing transform protocol behavior.
|
|
89
|
-
- Protocol
|
|
90
|
+
- Protocol v5 position quantization ranges:
|
|
90
91
|
- 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.
|
|
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.
|
|
92
94
|
- Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
|
|
93
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.
|
|
94
96
|
|
|
@@ -96,7 +98,7 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
96
98
|
|
|
97
99
|
The following options summarize trade-offs when expanding absolute-position range.
|
|
98
100
|
|
|
99
|
-
Assumed baseline (`protocolVersion=
|
|
101
|
+
Assumed unbound baseline (`protocolVersion=5`, `MovingFloorLocal` off):
|
|
100
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`).
|
|
101
103
|
- Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
|
|
102
104
|
|
|
@@ -42,14 +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 =
|
|
46
|
-
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact
|
|
47
|
-
-
|
|
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.
|
|
48
49
|
- Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
|
|
49
50
|
- Deploy Unity and Python updates together when changing transform protocol behavior.
|
|
50
|
-
- Protocol
|
|
51
|
+
- Protocol v5 position quantization ranges:
|
|
51
52
|
- 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.
|
|
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.
|
|
53
55
|
- Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
|
|
54
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.
|
|
55
57
|
|
|
@@ -57,7 +59,7 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
57
59
|
|
|
58
60
|
The following options summarize trade-offs when expanding absolute-position range.
|
|
59
61
|
|
|
60
|
-
Assumed baseline (`protocolVersion=
|
|
62
|
+
Assumed unbound baseline (`protocolVersion=5`, `MovingFloorLocal` off):
|
|
61
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`).
|
|
62
64
|
- Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
|
|
63
65
|
|
|
@@ -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.13.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.11.0 → styly_netsync_server-0.13.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 = 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
|
|
@@ -24,6 +24,7 @@ MSG_ROOM_OBJECTS = 14 # Server → Clients (PUB): room object states
|
|
|
24
24
|
MSG_OBJECT_OWNERSHIP_REQUEST = 15 # Client → Server: RequestOwnership/ReleaseOwnership
|
|
25
25
|
MSG_OBJECT_OWNERSHIP_CHANGED = 16 # Server → Clients (ROUTER): ownership changed
|
|
26
26
|
MSG_OBJECT_OWNERSHIP_REJECTED = 17 # Server → Client (ROUTER): request rejected
|
|
27
|
+
MSG_CLIENT_VAR_CLEAR = 18 # Clear all client variables for the sender
|
|
27
28
|
|
|
28
29
|
# Transform data type identifiers (deprecated - kept for reference)
|
|
29
30
|
|
|
@@ -32,7 +33,7 @@ MSG_OBJECT_OWNERSHIP_REJECTED = 17 # Server → Client (ROUTER): request reject
|
|
|
32
33
|
_max_virtual_transforms = 50
|
|
33
34
|
MAX_VIRTUAL_TRANSFORMS = _max_virtual_transforms # Legacy alias for backward compat
|
|
34
35
|
|
|
35
|
-
# Protocol
|
|
36
|
+
# Protocol v5 transform encoding constants
|
|
36
37
|
ABS_POS_SCALE = 0.01
|
|
37
38
|
LOCO_POS_SCALE = 0.01
|
|
38
39
|
REL_POS_SCALE = 0.005
|
|
@@ -69,6 +70,15 @@ POSE_FLAG_HEAD_VALID = 1 << 2
|
|
|
69
70
|
POSE_FLAG_RIGHT_VALID = 1 << 3
|
|
70
71
|
POSE_FLAG_LEFT_VALID = 1 << 4
|
|
71
72
|
POSE_FLAG_VIRTUALS_VALID = 1 << 5
|
|
73
|
+
POSE_FLAG_MOVING_FLOOR_LOCAL = 1 << 6
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _compute_encoding_flags(flags: int) -> int:
|
|
77
|
+
"""Return pose encoding flags for the sanitized pose flags."""
|
|
78
|
+
encoding_flags = ENCODING_FLAGS_DEFAULT
|
|
79
|
+
if flags & POSE_FLAG_MOVING_FLOOR_LOCAL:
|
|
80
|
+
encoding_flags &= ~ENCODING_PHYSICAL_IS_XRORIGIN_DELTA
|
|
81
|
+
return encoding_flags & 0xFF
|
|
72
82
|
|
|
73
83
|
|
|
74
84
|
def get_max_virtual_transforms() -> int:
|
|
@@ -372,7 +382,7 @@ def _create_transform_dict(
|
|
|
372
382
|
|
|
373
383
|
|
|
374
384
|
def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
375
|
-
"""Serialize a client body in protocol
|
|
385
|
+
"""Serialize a client body in protocol v5 compact format."""
|
|
376
386
|
pose_seq = int(client.get("poseSeq", 0)) & 0xFFFF
|
|
377
387
|
head = client.get("head", {}) or {}
|
|
378
388
|
right = client.get("rightHand", {}) or {}
|
|
@@ -411,34 +421,51 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
|
411
421
|
POSE_FLAG_RIGHT_VALID | POSE_FLAG_LEFT_VALID | POSE_FLAG_VIRTUALS_VALID
|
|
412
422
|
)
|
|
413
423
|
|
|
424
|
+
encoding_flags = _compute_encoding_flags(flags)
|
|
414
425
|
buffer.extend(struct.pack("<H", pose_seq))
|
|
415
426
|
buffer.append(flags)
|
|
416
|
-
buffer.append(
|
|
427
|
+
buffer.append(encoding_flags)
|
|
417
428
|
|
|
418
429
|
physical_valid = bool(flags & POSE_FLAG_PHYSICAL_VALID)
|
|
419
430
|
head_valid = bool(flags & POSE_FLAG_HEAD_VALID)
|
|
420
431
|
right_valid = head_valid and bool(flags & POSE_FLAG_RIGHT_VALID)
|
|
421
432
|
left_valid = head_valid and bool(flags & POSE_FLAG_LEFT_VALID)
|
|
422
433
|
virtual_valid = head_valid and bool(flags & POSE_FLAG_VIRTUALS_VALID)
|
|
434
|
+
moving_floor_local = bool(flags & POSE_FLAG_MOVING_FLOOR_LOCAL)
|
|
423
435
|
|
|
424
436
|
xr_origin_delta_x = float(client.get("xrOriginDeltaX", 0.0))
|
|
425
437
|
xr_origin_delta_y = float(client.get("xrOriginDeltaY", 0.0))
|
|
426
438
|
xr_origin_delta_z = float(client.get("xrOriginDeltaZ", 0.0))
|
|
427
439
|
xr_origin_delta_yaw = float(client.get("xrOriginDeltaYaw", 0.0))
|
|
440
|
+
physical = client.get("physical", {}) or {}
|
|
428
441
|
head_pos = _transform_get_position(head)
|
|
429
442
|
head_rot = _transform_get_quaternion(head)
|
|
430
443
|
head_rot_n = _normalize_quaternion(*head_rot)
|
|
431
444
|
|
|
432
445
|
if physical_valid:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
446
|
+
if moving_floor_local:
|
|
447
|
+
physical_pos = _transform_get_position(physical)
|
|
448
|
+
physical_rot = _transform_get_quaternion(physical)
|
|
449
|
+
physical_yaw = _quaternion_to_yaw_degrees(*physical_rot)
|
|
450
|
+
buffer.extend(
|
|
451
|
+
struct.pack(
|
|
452
|
+
"<hhhh",
|
|
453
|
+
_quantize_signed(physical_pos[0], LOCO_POS_SCALE),
|
|
454
|
+
_quantize_signed(physical_pos[1], LOCO_POS_SCALE),
|
|
455
|
+
_quantize_signed(physical_pos[2], LOCO_POS_SCALE),
|
|
456
|
+
_quantize_signed(physical_yaw, PHYSICAL_YAW_SCALE),
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
else:
|
|
460
|
+
buffer.extend(
|
|
461
|
+
struct.pack(
|
|
462
|
+
"<hhhh",
|
|
463
|
+
_quantize_signed(xr_origin_delta_x, LOCO_POS_SCALE),
|
|
464
|
+
_quantize_signed(xr_origin_delta_y, LOCO_POS_SCALE),
|
|
465
|
+
_quantize_signed(xr_origin_delta_z, LOCO_POS_SCALE),
|
|
466
|
+
_quantize_signed(xr_origin_delta_yaw, PHYSICAL_YAW_SCALE),
|
|
467
|
+
)
|
|
440
468
|
)
|
|
441
|
-
)
|
|
442
469
|
|
|
443
470
|
if head_valid:
|
|
444
471
|
_pack_int24_le(buffer, _quantize_signed_int24(head_pos[0], ABS_POS_SCALE))
|
|
@@ -739,6 +766,26 @@ def serialize_client_var_set(data: dict[str, Any]) -> bytes:
|
|
|
739
766
|
return bytes(buffer)
|
|
740
767
|
|
|
741
768
|
|
|
769
|
+
def serialize_client_var_clear(data: dict[str, Any]) -> bytes:
|
|
770
|
+
"""Serialize client variable clear message.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
data: Dictionary with senderClientNo and timestamp.
|
|
774
|
+
"""
|
|
775
|
+
buffer = bytearray()
|
|
776
|
+
|
|
777
|
+
# Message type
|
|
778
|
+
buffer.append(MSG_CLIENT_VAR_CLEAR)
|
|
779
|
+
|
|
780
|
+
# Sender client number (2 bytes)
|
|
781
|
+
buffer.extend(struct.pack("<H", data.get("senderClientNo", 0)))
|
|
782
|
+
|
|
783
|
+
# Timestamp (8 bytes double)
|
|
784
|
+
buffer.extend(struct.pack("<d", data.get("timestamp", 0.0)))
|
|
785
|
+
|
|
786
|
+
return bytes(buffer)
|
|
787
|
+
|
|
788
|
+
|
|
742
789
|
def serialize_client_var_sync(data: dict[str, Any]) -> bytes:
|
|
743
790
|
"""Serialize client variable sync message
|
|
744
791
|
|
|
@@ -785,10 +832,7 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
785
832
|
offset += 1
|
|
786
833
|
|
|
787
834
|
# Validate message type is within valid range
|
|
788
|
-
if
|
|
789
|
-
message_type < MSG_CLIENT_TRANSFORM
|
|
790
|
-
or message_type > MSG_OBJECT_OWNERSHIP_REJECTED
|
|
791
|
-
):
|
|
835
|
+
if message_type < MSG_CLIENT_TRANSFORM or message_type > MSG_CLIENT_VAR_CLEAR:
|
|
792
836
|
# Return invalid message type with None data instead of raising exception
|
|
793
837
|
return message_type, None, b""
|
|
794
838
|
|
|
@@ -817,6 +861,8 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
817
861
|
return message_type, _deserialize_client_var_set(data, offset), b""
|
|
818
862
|
elif message_type == MSG_CLIENT_VAR_SYNC:
|
|
819
863
|
return message_type, _deserialize_client_var_sync(data, offset), b""
|
|
864
|
+
elif message_type == MSG_CLIENT_VAR_CLEAR:
|
|
865
|
+
return message_type, _deserialize_client_var_clear(data, offset), b""
|
|
820
866
|
elif message_type == MSG_OBJECT_POSE:
|
|
821
867
|
return message_type, _deserialize_object_pose(data, offset), b""
|
|
822
868
|
elif message_type == MSG_ROOM_OBJECTS:
|
|
@@ -852,7 +898,7 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
852
898
|
|
|
853
899
|
|
|
854
900
|
def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any], int]:
|
|
855
|
-
"""Deserialize protocol
|
|
901
|
+
"""Deserialize protocol v5 compact pose body."""
|
|
856
902
|
result: dict[str, Any] = {}
|
|
857
903
|
result["poseSeq"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
858
904
|
offset += 2
|
|
@@ -868,11 +914,14 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
868
914
|
right_valid = head_valid and bool(flags & POSE_FLAG_RIGHT_VALID)
|
|
869
915
|
left_valid = head_valid and bool(flags & POSE_FLAG_LEFT_VALID)
|
|
870
916
|
virtual_valid = head_valid and bool(flags & POSE_FLAG_VIRTUALS_VALID)
|
|
917
|
+
moving_floor_local = bool(flags & POSE_FLAG_MOVING_FLOOR_LOCAL)
|
|
871
918
|
|
|
872
919
|
physical = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, True)
|
|
873
|
-
head = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0,
|
|
874
|
-
right = _create_transform_dict(
|
|
875
|
-
|
|
920
|
+
head = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local)
|
|
921
|
+
right = _create_transform_dict(
|
|
922
|
+
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
|
|
923
|
+
)
|
|
924
|
+
left = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local)
|
|
876
925
|
|
|
877
926
|
head_pos = (0.0, 0.0, 0.0)
|
|
878
927
|
head_rot = (0.0, 0.0, 0.0, 1.0)
|
|
@@ -882,15 +931,19 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
882
931
|
xr_origin_delta_yaw = 0.0
|
|
883
932
|
|
|
884
933
|
if physical_valid:
|
|
885
|
-
if (
|
|
934
|
+
if (
|
|
935
|
+
not moving_floor_local
|
|
936
|
+
and (encoding_flags & ENCODING_PHYSICAL_IS_XRORIGIN_DELTA) == 0
|
|
937
|
+
):
|
|
886
938
|
raise ValueError(
|
|
887
939
|
"PhysicalValid set but XROrigin delta encoding flag is missing"
|
|
888
940
|
)
|
|
889
941
|
dx_q, dy_q, dz_q, dyaw_q = struct.unpack("<hhhh", data[offset : offset + 8])
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
942
|
+
if not moving_floor_local:
|
|
943
|
+
xr_origin_delta_x = _dequantize_signed(dx_q, LOCO_POS_SCALE)
|
|
944
|
+
xr_origin_delta_y = _dequantize_signed(dy_q, LOCO_POS_SCALE)
|
|
945
|
+
xr_origin_delta_z = _dequantize_signed(dz_q, LOCO_POS_SCALE)
|
|
946
|
+
xr_origin_delta_yaw = _dequantize_signed(dyaw_q, PHYSICAL_YAW_SCALE)
|
|
894
947
|
offset += 8
|
|
895
948
|
|
|
896
949
|
if head_valid:
|
|
@@ -913,10 +966,29 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
913
966
|
head_rot[1],
|
|
914
967
|
head_rot[2],
|
|
915
968
|
head_rot[3],
|
|
916
|
-
|
|
969
|
+
moving_floor_local,
|
|
917
970
|
)
|
|
918
971
|
|
|
919
|
-
if physical_valid and
|
|
972
|
+
if physical_valid and moving_floor_local:
|
|
973
|
+
physical_pos = (
|
|
974
|
+
_dequantize_signed(dx_q, LOCO_POS_SCALE),
|
|
975
|
+
_dequantize_signed(dy_q, LOCO_POS_SCALE),
|
|
976
|
+
_dequantize_signed(dz_q, LOCO_POS_SCALE),
|
|
977
|
+
)
|
|
978
|
+
physical_rot = _yaw_degrees_to_quaternion(
|
|
979
|
+
_dequantize_signed(dyaw_q, PHYSICAL_YAW_SCALE)
|
|
980
|
+
)
|
|
981
|
+
physical = _create_transform_dict(
|
|
982
|
+
physical_pos[0],
|
|
983
|
+
physical_pos[1],
|
|
984
|
+
physical_pos[2],
|
|
985
|
+
physical_rot[0],
|
|
986
|
+
physical_rot[1],
|
|
987
|
+
physical_rot[2],
|
|
988
|
+
physical_rot[3],
|
|
989
|
+
True,
|
|
990
|
+
)
|
|
991
|
+
elif physical_valid and head_valid:
|
|
920
992
|
translated_x = head_pos[0] - xr_origin_delta_x
|
|
921
993
|
translated_y = head_pos[1] - xr_origin_delta_y
|
|
922
994
|
translated_z = head_pos[2] - xr_origin_delta_z
|
|
@@ -970,7 +1042,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
970
1042
|
abs_rot[1],
|
|
971
1043
|
abs_rot[2],
|
|
972
1044
|
abs_rot[3],
|
|
973
|
-
|
|
1045
|
+
moving_floor_local,
|
|
974
1046
|
)
|
|
975
1047
|
|
|
976
1048
|
if left_valid:
|
|
@@ -999,7 +1071,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
999
1071
|
abs_rot[1],
|
|
1000
1072
|
abs_rot[2],
|
|
1001
1073
|
abs_rot[3],
|
|
1002
|
-
|
|
1074
|
+
moving_floor_local,
|
|
1003
1075
|
)
|
|
1004
1076
|
|
|
1005
1077
|
virtual_count = data[offset]
|
|
@@ -1041,7 +1113,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
1041
1113
|
abs_rot[1],
|
|
1042
1114
|
abs_rot[2],
|
|
1043
1115
|
abs_rot[3],
|
|
1044
|
-
|
|
1116
|
+
moving_floor_local,
|
|
1045
1117
|
)
|
|
1046
1118
|
)
|
|
1047
1119
|
|
|
@@ -1058,7 +1130,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
1058
1130
|
|
|
1059
1131
|
|
|
1060
1132
|
def _deserialize_client_transform(data: bytes, offset: int) -> dict[str, Any]:
|
|
1061
|
-
"""Deserialize client pose (
|
|
1133
|
+
"""Deserialize client pose (v5) from binary data."""
|
|
1062
1134
|
result: dict[str, Any] = {}
|
|
1063
1135
|
|
|
1064
1136
|
protocol_version = data[offset]
|
|
@@ -1096,7 +1168,7 @@ def _deserialize_rpc_message(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1096
1168
|
|
|
1097
1169
|
|
|
1098
1170
|
def _deserialize_room_transform(data: bytes, offset: int) -> dict[str, Any]:
|
|
1099
|
-
"""Deserialize room pose (
|
|
1171
|
+
"""Deserialize room pose (v5) with client numbers only."""
|
|
1100
1172
|
result: dict[str, Any] = {}
|
|
1101
1173
|
|
|
1102
1174
|
protocol_version = data[offset]
|
|
@@ -1229,6 +1301,20 @@ def _deserialize_client_var_set(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1229
1301
|
return result
|
|
1230
1302
|
|
|
1231
1303
|
|
|
1304
|
+
def _deserialize_client_var_clear(data: bytes, offset: int) -> dict[str, Any]:
|
|
1305
|
+
"""Deserialize client variable clear message."""
|
|
1306
|
+
result: dict[str, Any] = {}
|
|
1307
|
+
|
|
1308
|
+
# Sender client number (2 bytes)
|
|
1309
|
+
result["senderClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1310
|
+
offset += 2
|
|
1311
|
+
|
|
1312
|
+
# Timestamp (8 bytes double)
|
|
1313
|
+
result["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1314
|
+
|
|
1315
|
+
return result
|
|
1316
|
+
|
|
1317
|
+
|
|
1232
1318
|
def _deserialize_client_var_sync(data: bytes, offset: int) -> dict[str, Any]:
|
|
1233
1319
|
"""Deserialize client variable sync message"""
|
|
1234
1320
|
result: dict[str, Any] = {"clientVariables": {}}
|
|
@@ -1027,7 +1027,11 @@ class net_sync_manager:
|
|
|
1027
1027
|
logger.error(f"Error processing global var sync: {e}")
|
|
1028
1028
|
|
|
1029
1029
|
def _process_client_var_sync(self, msg_data: dict[str, Any]) -> None:
|
|
1030
|
-
"""Process client variable sync.
|
|
1030
|
+
"""Process client variable sync.
|
|
1031
|
+
|
|
1032
|
+
Each included client number is a full authoritative snapshot. Missing
|
|
1033
|
+
keys are removed from the local cache for that client.
|
|
1034
|
+
"""
|
|
1031
1035
|
try:
|
|
1032
1036
|
client_variables = msg_data.get("clientVariables", {})
|
|
1033
1037
|
|
|
@@ -1037,34 +1041,50 @@ class net_sync_manager:
|
|
|
1037
1041
|
except ValueError:
|
|
1038
1042
|
continue
|
|
1039
1043
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1044
|
+
old_vars = self._client_variables.get(client_no, {})
|
|
1045
|
+
new_vars: dict[str, str] = {}
|
|
1042
1046
|
|
|
1043
1047
|
for var in variables:
|
|
1044
1048
|
name = var.get("name", "")
|
|
1045
1049
|
value = var.get("value", "")
|
|
1046
|
-
|
|
1050
|
+
if name:
|
|
1051
|
+
new_vars[name] = value
|
|
1047
1052
|
|
|
1048
|
-
|
|
1053
|
+
changed_events: list[tuple[int, str, str | None, str | None]] = []
|
|
1054
|
+
for name in set(old_vars) - set(new_vars):
|
|
1055
|
+
changed_events.append((client_no, name, old_vars.get(name), None))
|
|
1049
1056
|
|
|
1057
|
+
for name, value in new_vars.items():
|
|
1058
|
+
old_value = old_vars.get(name)
|
|
1050
1059
|
if old_value != value:
|
|
1051
|
-
|
|
1052
|
-
self._stats["nv_updates"] += 1
|
|
1060
|
+
changed_events.append((client_no, name, old_value, value))
|
|
1053
1061
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1062
|
+
self._client_variables[client_no] = new_vars
|
|
1063
|
+
|
|
1064
|
+
for event_client_no, name, old_value, new_value in changed_events:
|
|
1065
|
+
with self._lock:
|
|
1066
|
+
self._stats["nv_updates"] += 1
|
|
1067
|
+
|
|
1068
|
+
event: tuple[str, int, str, str | None, str | None] = (
|
|
1069
|
+
"client",
|
|
1070
|
+
event_client_no,
|
|
1071
|
+
name,
|
|
1072
|
+
old_value,
|
|
1073
|
+
new_value,
|
|
1074
|
+
)
|
|
1075
|
+
if self._auto_dispatch:
|
|
1076
|
+
self.on_client_variable_changed.invoke(
|
|
1077
|
+
event_client_no, name, old_value, new_value
|
|
1078
|
+
)
|
|
1079
|
+
else:
|
|
1080
|
+
try:
|
|
1081
|
+
self._nv_queue.put_nowait(event)
|
|
1082
|
+
except Full:
|
|
1060
1083
|
try:
|
|
1084
|
+
self._nv_queue.get_nowait()
|
|
1061
1085
|
self._nv_queue.put_nowait(event)
|
|
1062
|
-
except
|
|
1063
|
-
|
|
1064
|
-
self._nv_queue.get_nowait()
|
|
1065
|
-
self._nv_queue.put_nowait(event)
|
|
1066
|
-
except Empty:
|
|
1067
|
-
pass
|
|
1086
|
+
except Empty:
|
|
1087
|
+
pass
|
|
1068
1088
|
|
|
1069
1089
|
except Exception as e:
|
|
1070
1090
|
logger.error(f"Error processing client var sync: {e}")
|
|
@@ -1235,6 +1255,28 @@ class net_sync_manager:
|
|
|
1235
1255
|
logger.error(f"Error queueing client variable: {e}")
|
|
1236
1256
|
return False
|
|
1237
1257
|
|
|
1258
|
+
def clear_my_client_variables(self) -> bool:
|
|
1259
|
+
"""Queue a request to clear this client's variables on the server."""
|
|
1260
|
+
if not self._running or not self._dealer_socket or self._client_no is None:
|
|
1261
|
+
return False
|
|
1262
|
+
|
|
1263
|
+
try:
|
|
1264
|
+
clear_data = {
|
|
1265
|
+
"senderClientNo": self._client_no,
|
|
1266
|
+
"timestamp": time.time(),
|
|
1267
|
+
}
|
|
1268
|
+
message = binary_serializer.serialize_client_var_clear(clear_data)
|
|
1269
|
+
sent = self._enqueue_control(
|
|
1270
|
+
self._room, message, msg_type="client_variable_clear"
|
|
1271
|
+
)
|
|
1272
|
+
if sent:
|
|
1273
|
+
self._clear_local_client_variables(self._client_no)
|
|
1274
|
+
return sent
|
|
1275
|
+
|
|
1276
|
+
except Exception as e:
|
|
1277
|
+
logger.error(f"Error queueing client variable clear: {e}")
|
|
1278
|
+
return False
|
|
1279
|
+
|
|
1238
1280
|
def _enqueue_control(
|
|
1239
1281
|
self, room_id: str, payload: bytes, msg_type: str = "control"
|
|
1240
1282
|
) -> bool:
|
|
@@ -1294,6 +1336,34 @@ class net_sync_manager:
|
|
|
1294
1336
|
"""Return a copy of all variables for the given client."""
|
|
1295
1337
|
return self._client_variables.get(client_no, {}).copy()
|
|
1296
1338
|
|
|
1339
|
+
def _clear_local_client_variables(self, client_no: int) -> None:
|
|
1340
|
+
"""Clear local client-variable cache for a client and emit events."""
|
|
1341
|
+
old_vars = self._client_variables.get(client_no, {}).copy()
|
|
1342
|
+
self._client_variables[client_no] = {}
|
|
1343
|
+
|
|
1344
|
+
for name, old_value in old_vars.items():
|
|
1345
|
+
with self._lock:
|
|
1346
|
+
self._stats["nv_updates"] += 1
|
|
1347
|
+
|
|
1348
|
+
event: tuple[str, int, str, str | None, str | None] = (
|
|
1349
|
+
"client",
|
|
1350
|
+
client_no,
|
|
1351
|
+
name,
|
|
1352
|
+
old_value,
|
|
1353
|
+
None,
|
|
1354
|
+
)
|
|
1355
|
+
if self._auto_dispatch:
|
|
1356
|
+
self.on_client_variable_changed.invoke(client_no, name, old_value, None)
|
|
1357
|
+
else:
|
|
1358
|
+
try:
|
|
1359
|
+
self._nv_queue.put_nowait(event)
|
|
1360
|
+
except Full:
|
|
1361
|
+
try:
|
|
1362
|
+
self._nv_queue.get_nowait()
|
|
1363
|
+
self._nv_queue.put_nowait(event)
|
|
1364
|
+
except Empty:
|
|
1365
|
+
pass
|
|
1366
|
+
|
|
1297
1367
|
def is_client_stealth_mode(self, client_no: int) -> bool:
|
|
1298
1368
|
"""Check if the client is in stealth mode."""
|
|
1299
1369
|
return self._client_stealth_flags.get(client_no, False)
|
{styly_netsync_server-0.11.0 → styly_netsync_server-0.13.0}/src/styly_netsync/client_simulator.py
RENAMED
|
@@ -46,6 +46,7 @@ import zmq
|
|
|
46
46
|
# Import public APIs from styly_netsync module
|
|
47
47
|
from styly_netsync.binary_serializer import (
|
|
48
48
|
MSG_CLIENT_POSE,
|
|
49
|
+
MSG_CLIENT_VAR_CLEAR,
|
|
49
50
|
MSG_CLIENT_VAR_SET,
|
|
50
51
|
MSG_CLIENT_VAR_SYNC,
|
|
51
52
|
MSG_DEVICE_ID_MAPPING,
|
|
@@ -95,6 +96,7 @@ MESSAGE_TYPE_NAMES: dict[int, str] = {
|
|
|
95
96
|
MSG_GLOBAL_VAR_SYNC: "GLOBAL_VAR_SYNC",
|
|
96
97
|
MSG_CLIENT_VAR_SET: "CLIENT_VAR_SET",
|
|
97
98
|
MSG_CLIENT_VAR_SYNC: "CLIENT_VAR_SYNC",
|
|
99
|
+
MSG_CLIENT_VAR_CLEAR: "CLIENT_VAR_CLEAR",
|
|
98
100
|
MSG_OBJECT_POSE: "OBJECT_POSE",
|
|
99
101
|
MSG_ROOM_OBJECTS: "ROOM_OBJECTS",
|
|
100
102
|
MSG_OBJECT_OWNERSHIP_REQUEST: "OBJECT_OWNERSHIP_REQUEST",
|