styly-netsync-server 0.12.0__tar.gz → 0.14.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.12.0 → styly_netsync_server-0.14.0}/PKG-INFO +1 -1
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/pyproject.toml +1 -1
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/binary_serializer.py +38 -31
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/client.py +88 -21
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/client_simulator.py +2 -1
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/rest_bridge.py +74 -64
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/server.py +295 -84
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/PKG-INFO +1 -1
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/SOURCES.txt +2 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_binary_serializer.py +20 -6
- styly_netsync_server-0.14.0/tests/test_client_variable_device_store.py +466 -0
- styly_netsync_server-0.14.0/tests/test_nv_server_ordering.py +147 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_object_sync.py +1 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/LICENSE +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/README.md +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/setup.cfg +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/__init__.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/__main__.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/adapters.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/cli.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/config.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/default.toml +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/events.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/logging_utils.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/network_utils.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/nv_sync.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/types.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/dependency_links.txt +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/entry_points.txt +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/requires.txt +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync_server.egg-info/top_level.txt +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_all_run_methods.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_config.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_discovery_probe.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_logging_cli.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_multi_nic.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_nv_protocol.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_port_error_message.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_python_client.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_reconnect_identity.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_rest_bridge.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_room_expiry.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/tests/test_stealth_heartbeat.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.14.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.14.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
|
|
@@ -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.14.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.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/binary_serializer.py
RENAMED
|
@@ -6,7 +6,10 @@ from typing import Any
|
|
|
6
6
|
logger = logging.getLogger(__name__)
|
|
7
7
|
|
|
8
8
|
# Message type identifiers
|
|
9
|
-
|
|
9
|
+
# v6: Network Variable wire messages dropped the per-write timestamp field.
|
|
10
|
+
# Bumped so mixed old/new builds fail the handshake instead of silently
|
|
11
|
+
# misparsing NV traffic (the version byte rides on transform/object messages).
|
|
12
|
+
PROTOCOL_VERSION = 6
|
|
10
13
|
MSG_CLIENT_TRANSFORM = 1
|
|
11
14
|
MSG_ROOM_TRANSFORM = 2 # Legacy room transform with short IDs only
|
|
12
15
|
MSG_RPC = 3 # Remote procedure call
|
|
@@ -24,6 +27,7 @@ MSG_ROOM_OBJECTS = 14 # Server → Clients (PUB): room object states
|
|
|
24
27
|
MSG_OBJECT_OWNERSHIP_REQUEST = 15 # Client → Server: RequestOwnership/ReleaseOwnership
|
|
25
28
|
MSG_OBJECT_OWNERSHIP_CHANGED = 16 # Server → Clients (ROUTER): ownership changed
|
|
26
29
|
MSG_OBJECT_OWNERSHIP_REJECTED = 17 # Server → Client (ROUTER): request rejected
|
|
30
|
+
MSG_CLIENT_VAR_CLEAR = 18 # Clear all client variables for the sender
|
|
27
31
|
|
|
28
32
|
# Transform data type identifiers (deprecated - kept for reference)
|
|
29
33
|
|
|
@@ -685,7 +689,7 @@ def serialize_global_var_set(data: dict[str, Any]) -> bytes:
|
|
|
685
689
|
"""Serialize global variable set message
|
|
686
690
|
|
|
687
691
|
Args:
|
|
688
|
-
data: Dictionary with senderClientNo, variableName, variableValue
|
|
692
|
+
data: Dictionary with senderClientNo, variableName, variableValue
|
|
689
693
|
"""
|
|
690
694
|
buffer = bytearray()
|
|
691
695
|
|
|
@@ -703,9 +707,6 @@ def serialize_global_var_set(data: dict[str, Any]) -> bytes:
|
|
|
703
707
|
value = data.get("variableValue", "")[:1024]
|
|
704
708
|
_pack_string(buffer, value, use_ushort=True)
|
|
705
709
|
|
|
706
|
-
# Timestamp (8 bytes double)
|
|
707
|
-
buffer.extend(struct.pack("<d", data.get("timestamp", 0.0)))
|
|
708
|
-
|
|
709
710
|
return bytes(buffer)
|
|
710
711
|
|
|
711
712
|
|
|
@@ -728,7 +729,6 @@ def serialize_global_var_sync(data: dict[str, Any]) -> bytes:
|
|
|
728
729
|
for var in variables:
|
|
729
730
|
_pack_string(buffer, var.get("name", "")[:64])
|
|
730
731
|
_pack_string(buffer, var.get("value", "")[:1024], use_ushort=True)
|
|
731
|
-
buffer.extend(struct.pack("<d", var.get("timestamp", 0.0)))
|
|
732
732
|
buffer.extend(struct.pack("<H", var.get("lastWriterClientNo", 0)))
|
|
733
733
|
|
|
734
734
|
return bytes(buffer)
|
|
@@ -738,7 +738,8 @@ def serialize_client_var_set(data: dict[str, Any]) -> bytes:
|
|
|
738
738
|
"""Serialize client variable set message
|
|
739
739
|
|
|
740
740
|
Args:
|
|
741
|
-
data: Dictionary with senderClientNo, targetClientNo, variableName,
|
|
741
|
+
data: Dictionary with senderClientNo, targetClientNo, variableName,
|
|
742
|
+
variableValue
|
|
742
743
|
"""
|
|
743
744
|
buffer = bytearray()
|
|
744
745
|
|
|
@@ -759,8 +760,22 @@ def serialize_client_var_set(data: dict[str, Any]) -> bytes:
|
|
|
759
760
|
value = data.get("variableValue", "")[:1024]
|
|
760
761
|
_pack_string(buffer, value, use_ushort=True)
|
|
761
762
|
|
|
762
|
-
|
|
763
|
-
|
|
763
|
+
return bytes(buffer)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def serialize_client_var_clear(data: dict[str, Any]) -> bytes:
|
|
767
|
+
"""Serialize client variable clear message.
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
data: Dictionary with senderClientNo.
|
|
771
|
+
"""
|
|
772
|
+
buffer = bytearray()
|
|
773
|
+
|
|
774
|
+
# Message type
|
|
775
|
+
buffer.append(MSG_CLIENT_VAR_CLEAR)
|
|
776
|
+
|
|
777
|
+
# Sender client number (2 bytes)
|
|
778
|
+
buffer.extend(struct.pack("<H", data.get("senderClientNo", 0)))
|
|
764
779
|
|
|
765
780
|
return bytes(buffer)
|
|
766
781
|
|
|
@@ -790,7 +805,6 @@ def serialize_client_var_sync(data: dict[str, Any]) -> bytes:
|
|
|
790
805
|
for var in variables:
|
|
791
806
|
_pack_string(buffer, var.get("name", "")[:64])
|
|
792
807
|
_pack_string(buffer, var.get("value", "")[:1024], use_ushort=True)
|
|
793
|
-
buffer.extend(struct.pack("<d", var.get("timestamp", 0.0)))
|
|
794
808
|
buffer.extend(struct.pack("<H", var.get("lastWriterClientNo", 0)))
|
|
795
809
|
|
|
796
810
|
return bytes(buffer)
|
|
@@ -811,10 +825,7 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
811
825
|
offset += 1
|
|
812
826
|
|
|
813
827
|
# Validate message type is within valid range
|
|
814
|
-
if
|
|
815
|
-
message_type < MSG_CLIENT_TRANSFORM
|
|
816
|
-
or message_type > MSG_OBJECT_OWNERSHIP_REJECTED
|
|
817
|
-
):
|
|
828
|
+
if message_type < MSG_CLIENT_TRANSFORM or message_type > MSG_CLIENT_VAR_CLEAR:
|
|
818
829
|
# Return invalid message type with None data instead of raising exception
|
|
819
830
|
return message_type, None, b""
|
|
820
831
|
|
|
@@ -843,6 +854,8 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
843
854
|
return message_type, _deserialize_client_var_set(data, offset), b""
|
|
844
855
|
elif message_type == MSG_CLIENT_VAR_SYNC:
|
|
845
856
|
return message_type, _deserialize_client_var_sync(data, offset), b""
|
|
857
|
+
elif message_type == MSG_CLIENT_VAR_CLEAR:
|
|
858
|
+
return message_type, _deserialize_client_var_clear(data, offset), b""
|
|
846
859
|
elif message_type == MSG_OBJECT_POSE:
|
|
847
860
|
return message_type, _deserialize_object_pose(data, offset), b""
|
|
848
861
|
elif message_type == MSG_ROOM_OBJECTS:
|
|
@@ -897,15 +910,11 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
897
910
|
moving_floor_local = bool(flags & POSE_FLAG_MOVING_FLOOR_LOCAL)
|
|
898
911
|
|
|
899
912
|
physical = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, True)
|
|
900
|
-
head = _create_transform_dict(
|
|
901
|
-
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
|
|
902
|
-
)
|
|
913
|
+
head = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local)
|
|
903
914
|
right = _create_transform_dict(
|
|
904
915
|
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
|
|
905
916
|
)
|
|
906
|
-
left = _create_transform_dict(
|
|
907
|
-
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
|
|
908
|
-
)
|
|
917
|
+
left = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local)
|
|
909
918
|
|
|
910
919
|
head_pos = (0.0, 0.0, 0.0)
|
|
911
920
|
head_rot = (0.0, 0.0, 0.0, 1.0)
|
|
@@ -1231,10 +1240,6 @@ def _deserialize_global_var_set(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1231
1240
|
# Variable value
|
|
1232
1241
|
result["variableValue"], offset = _unpack_string(data, offset, use_ushort=True)
|
|
1233
1242
|
|
|
1234
|
-
# Timestamp (8 bytes double)
|
|
1235
|
-
result["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1236
|
-
offset += 8
|
|
1237
|
-
|
|
1238
1243
|
return result
|
|
1239
1244
|
|
|
1240
1245
|
|
|
@@ -1251,8 +1256,6 @@ def _deserialize_global_var_sync(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1251
1256
|
var = {}
|
|
1252
1257
|
var["name"], offset = _unpack_string(data, offset)
|
|
1253
1258
|
var["value"], offset = _unpack_string(data, offset, use_ushort=True)
|
|
1254
|
-
var["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1255
|
-
offset += 8
|
|
1256
1259
|
var["lastWriterClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1257
1260
|
offset += 2
|
|
1258
1261
|
result["variables"].append(var)
|
|
@@ -1278,9 +1281,15 @@ def _deserialize_client_var_set(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1278
1281
|
# Variable value
|
|
1279
1282
|
result["variableValue"], offset = _unpack_string(data, offset, use_ushort=True)
|
|
1280
1283
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
+
return result
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def _deserialize_client_var_clear(data: bytes, offset: int) -> dict[str, Any]:
|
|
1288
|
+
"""Deserialize client variable clear message."""
|
|
1289
|
+
result: dict[str, Any] = {}
|
|
1290
|
+
|
|
1291
|
+
# Sender client number (2 bytes)
|
|
1292
|
+
result["senderClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1284
1293
|
|
|
1285
1294
|
return result
|
|
1286
1295
|
|
|
@@ -1306,8 +1315,6 @@ def _deserialize_client_var_sync(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1306
1315
|
var = {}
|
|
1307
1316
|
var["name"], offset = _unpack_string(data, offset)
|
|
1308
1317
|
var["value"], offset = _unpack_string(data, offset, use_ushort=True)
|
|
1309
|
-
var["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1310
|
-
offset += 8
|
|
1311
1318
|
var["lastWriterClientNo"] = struct.unpack("<H", data[offset : offset + 2])[
|
|
1312
1319
|
0
|
|
1313
1320
|
]
|
|
@@ -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}")
|
|
@@ -1202,7 +1222,6 @@ class net_sync_manager:
|
|
|
1202
1222
|
"senderClientNo": self._client_no,
|
|
1203
1223
|
"variableName": name,
|
|
1204
1224
|
"variableValue": value,
|
|
1205
|
-
"timestamp": time.time(),
|
|
1206
1225
|
}
|
|
1207
1226
|
message = binary_serializer.serialize_global_var_set(var_data)
|
|
1208
1227
|
return self._enqueue_control(
|
|
@@ -1224,7 +1243,6 @@ class net_sync_manager:
|
|
|
1224
1243
|
"targetClientNo": target_client_no,
|
|
1225
1244
|
"variableName": name,
|
|
1226
1245
|
"variableValue": value,
|
|
1227
|
-
"timestamp": time.time(),
|
|
1228
1246
|
}
|
|
1229
1247
|
message = binary_serializer.serialize_client_var_set(var_data)
|
|
1230
1248
|
return self._enqueue_control(
|
|
@@ -1235,6 +1253,27 @@ class net_sync_manager:
|
|
|
1235
1253
|
logger.error(f"Error queueing client variable: {e}")
|
|
1236
1254
|
return False
|
|
1237
1255
|
|
|
1256
|
+
def clear_my_client_variables(self) -> bool:
|
|
1257
|
+
"""Queue a request to clear this client's variables on the server."""
|
|
1258
|
+
if not self._running or not self._dealer_socket or self._client_no is None:
|
|
1259
|
+
return False
|
|
1260
|
+
|
|
1261
|
+
try:
|
|
1262
|
+
clear_data = {
|
|
1263
|
+
"senderClientNo": self._client_no,
|
|
1264
|
+
}
|
|
1265
|
+
message = binary_serializer.serialize_client_var_clear(clear_data)
|
|
1266
|
+
sent = self._enqueue_control(
|
|
1267
|
+
self._room, message, msg_type="client_variable_clear"
|
|
1268
|
+
)
|
|
1269
|
+
if sent:
|
|
1270
|
+
self._clear_local_client_variables(self._client_no)
|
|
1271
|
+
return sent
|
|
1272
|
+
|
|
1273
|
+
except Exception as e:
|
|
1274
|
+
logger.error(f"Error queueing client variable clear: {e}")
|
|
1275
|
+
return False
|
|
1276
|
+
|
|
1238
1277
|
def _enqueue_control(
|
|
1239
1278
|
self, room_id: str, payload: bytes, msg_type: str = "control"
|
|
1240
1279
|
) -> bool:
|
|
@@ -1294,6 +1333,34 @@ class net_sync_manager:
|
|
|
1294
1333
|
"""Return a copy of all variables for the given client."""
|
|
1295
1334
|
return self._client_variables.get(client_no, {}).copy()
|
|
1296
1335
|
|
|
1336
|
+
def _clear_local_client_variables(self, client_no: int) -> None:
|
|
1337
|
+
"""Clear local client-variable cache for a client and emit events."""
|
|
1338
|
+
old_vars = self._client_variables.get(client_no, {}).copy()
|
|
1339
|
+
self._client_variables[client_no] = {}
|
|
1340
|
+
|
|
1341
|
+
for name, old_value in old_vars.items():
|
|
1342
|
+
with self._lock:
|
|
1343
|
+
self._stats["nv_updates"] += 1
|
|
1344
|
+
|
|
1345
|
+
event: tuple[str, int, str, str | None, str | None] = (
|
|
1346
|
+
"client",
|
|
1347
|
+
client_no,
|
|
1348
|
+
name,
|
|
1349
|
+
old_value,
|
|
1350
|
+
None,
|
|
1351
|
+
)
|
|
1352
|
+
if self._auto_dispatch:
|
|
1353
|
+
self.on_client_variable_changed.invoke(client_no, name, old_value, None)
|
|
1354
|
+
else:
|
|
1355
|
+
try:
|
|
1356
|
+
self._nv_queue.put_nowait(event)
|
|
1357
|
+
except Full:
|
|
1358
|
+
try:
|
|
1359
|
+
self._nv_queue.get_nowait()
|
|
1360
|
+
self._nv_queue.put_nowait(event)
|
|
1361
|
+
except Empty:
|
|
1362
|
+
pass
|
|
1363
|
+
|
|
1297
1364
|
def is_client_stealth_mode(self, client_no: int) -> bool:
|
|
1298
1365
|
"""Check if the client is in stealth mode."""
|
|
1299
1366
|
return self._client_stealth_flags.get(client_no, False)
|
{styly_netsync_server-0.12.0 → styly_netsync_server-0.14.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",
|
|
@@ -646,7 +648,6 @@ class NetworkTransport:
|
|
|
646
648
|
"targetClientNo": sender_client_no, # Set variable for ourselves
|
|
647
649
|
"variableName": var_name,
|
|
648
650
|
"variableValue": value,
|
|
649
|
-
"timestamp": time.time(),
|
|
650
651
|
}
|
|
651
652
|
binary_data = serialize_client_var_set(var_data)
|
|
652
653
|
self.socket.send_multipart(
|
{styly_netsync_server-0.12.0 → styly_netsync_server-0.14.0}/src/styly_netsync/rest_bridge.py
RENAMED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
|
-
from typing import TYPE_CHECKING, Annotated
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Protocol
|
|
7
7
|
|
|
8
8
|
from fastapi import FastAPI, HTTPException
|
|
9
9
|
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -18,7 +18,6 @@ logger = logging.getLogger(__name__)
|
|
|
18
18
|
|
|
19
19
|
MAX_NAME = 64
|
|
20
20
|
MAX_VALUE = 1024
|
|
21
|
-
MAX_CLIENT_VARS = 20
|
|
22
21
|
MAX_GLOBAL_VARS = 100
|
|
23
22
|
|
|
24
23
|
# Constrained string types for variable names and values
|
|
@@ -32,45 +31,26 @@ class UpsertBody(BaseModel):
|
|
|
32
31
|
variables: dict[VarName, VarValue] = Field(default_factory=dict)
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
class
|
|
36
|
-
"""
|
|
34
|
+
class ClientVariableServer(Protocol):
|
|
35
|
+
"""Server-side operations used by REST client-variable endpoints."""
|
|
37
36
|
|
|
38
|
-
def
|
|
39
|
-
self
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
self, room_id: str, device_id: str, kvs: dict[str, str]
|
|
44
|
-
) -> dict[str, str]:
|
|
45
|
-
"""Merge incoming key-values for a device within a room."""
|
|
46
|
-
with self._lock:
|
|
47
|
-
key = (room_id, device_id)
|
|
48
|
-
current = self._data.get(key, {})
|
|
49
|
-
new_keys = [name for name in kvs if name not in current]
|
|
50
|
-
if len(current) + len(new_keys) > MAX_CLIENT_VARS:
|
|
51
|
-
raise ValueError(
|
|
52
|
-
f"Too many client variables (> {MAX_CLIENT_VARS}) for device {device_id}"
|
|
53
|
-
)
|
|
54
|
-
current.update(kvs)
|
|
55
|
-
self._data[key] = current
|
|
56
|
-
return dict(current)
|
|
57
|
-
|
|
58
|
-
def get(self, room_id: str, device_id: str) -> dict[str, str]:
|
|
59
|
-
"""Return a copy of stored variables for a device."""
|
|
60
|
-
with self._lock:
|
|
61
|
-
return dict(self._data.get((room_id, device_id), {}))
|
|
62
|
-
|
|
63
|
-
def all_for_room(self, room_id: str) -> dict[str, dict[str, str]]:
|
|
64
|
-
"""Return all stored variables for the given room."""
|
|
65
|
-
with self._lock:
|
|
66
|
-
result: dict[str, dict[str, str]] = {}
|
|
67
|
-
for (stored_room, device_id), variables in self._data.items():
|
|
68
|
-
if stored_room == room_id:
|
|
69
|
-
result[device_id] = dict(variables)
|
|
70
|
-
return result
|
|
37
|
+
def upsert_client_variables_for_device(
|
|
38
|
+
self, room_id: str, device_id: str, variables: dict[str, str]
|
|
39
|
+
) -> tuple[int | None, dict[str, str]]:
|
|
40
|
+
"""Upsert client variables and return mapping plus per-key states."""
|
|
41
|
+
...
|
|
71
42
|
|
|
43
|
+
def get_client_variables_for_device(
|
|
44
|
+
self, room_id: str, device_id: str
|
|
45
|
+
) -> tuple[int | None, dict[str, str]]:
|
|
46
|
+
"""Return the mapped client number and current variables."""
|
|
47
|
+
...
|
|
72
48
|
|
|
73
|
-
|
|
49
|
+
def delete_client_variables_for_device(
|
|
50
|
+
self, room_id: str, device_id: str, name: str | None = None
|
|
51
|
+
) -> tuple[int | None, int]:
|
|
52
|
+
"""Delete client variables and return mapping plus deletion count."""
|
|
53
|
+
...
|
|
74
54
|
|
|
75
55
|
|
|
76
56
|
class GlobalVarStore:
|
|
@@ -157,7 +137,6 @@ class RoomBridge:
|
|
|
157
137
|
next_handshake_at = now + 0.5
|
|
158
138
|
else:
|
|
159
139
|
try:
|
|
160
|
-
self.flush_all_known_mappings()
|
|
161
140
|
self.flush_global_vars()
|
|
162
141
|
except Exception as exc:
|
|
163
142
|
logger.debug("Flush failed: %s", exc)
|
|
@@ -261,14 +240,6 @@ class RoomBridge:
|
|
|
261
240
|
applied.add(name)
|
|
262
241
|
return applied
|
|
263
242
|
|
|
264
|
-
def flush_all_known_mappings(self) -> None:
|
|
265
|
-
"""Flush queued variables for devices whose client numbers are known."""
|
|
266
|
-
pending = store.all_for_room(self.room_id)
|
|
267
|
-
for device_id, kvs in pending.items():
|
|
268
|
-
client_no = self.get_client_no(device_id)
|
|
269
|
-
if client_no:
|
|
270
|
-
self._apply_to_client(client_no, kvs)
|
|
271
|
-
|
|
272
243
|
def flush_global_vars(self) -> None:
|
|
273
244
|
"""Flush queued global variables for this room."""
|
|
274
245
|
pending = global_store.pop(self.room_id)
|
|
@@ -304,7 +275,12 @@ class BridgeManager:
|
|
|
304
275
|
return bridge
|
|
305
276
|
|
|
306
277
|
|
|
307
|
-
def create_app(
|
|
278
|
+
def create_app(
|
|
279
|
+
server_addr: str,
|
|
280
|
+
dealer_port: int,
|
|
281
|
+
sub_port: int,
|
|
282
|
+
server: ClientVariableServer | None = None,
|
|
283
|
+
) -> FastAPI:
|
|
308
284
|
"""Create the FastAPI application hosting the REST bridge."""
|
|
309
285
|
app = FastAPI(title="NetSync REST Bridge", version="1.0.0")
|
|
310
286
|
|
|
@@ -329,14 +305,17 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
329
305
|
def upsert(room_id: str, device_id: str, body: UpsertBody) -> dict[str, object]:
|
|
330
306
|
if not body.variables:
|
|
331
307
|
raise HTTPException(status_code=400, detail="variables must not be empty")
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
308
|
+
if server is not None:
|
|
309
|
+
try:
|
|
310
|
+
client_no, statuses = server.upsert_client_variables_for_device(
|
|
311
|
+
room_id, device_id, body.variables
|
|
312
|
+
)
|
|
313
|
+
except ValueError as exc:
|
|
314
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
315
|
+
else:
|
|
316
|
+
bridge = manager.get(room_id)
|
|
317
|
+
statuses = bridge.apply_now_or_queue(device_id, body.variables)
|
|
318
|
+
client_no = bridge.get_client_no(device_id)
|
|
340
319
|
|
|
341
320
|
return {
|
|
342
321
|
"roomId": room_id,
|
|
@@ -387,21 +366,26 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
387
366
|
|
|
388
367
|
@app.get("/v1/rooms/{room_id}/devices/{device_id}/client-variables")
|
|
389
368
|
def get_client_variables(room_id: str, device_id: str) -> dict[str, object]:
|
|
390
|
-
|
|
391
|
-
|
|
369
|
+
if server is not None:
|
|
370
|
+
client_no, variables = server.get_client_variables_for_device(
|
|
371
|
+
room_id, device_id
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
bridge = manager.get(room_id)
|
|
375
|
+
client_no, variables = bridge.get_client_variables(device_id)
|
|
392
376
|
return {"clientNo": client_no, "variables": variables}
|
|
393
377
|
|
|
394
378
|
@app.get("/v1/rooms/{room_id}/devices/{device_id}/client-variables/{name}")
|
|
395
379
|
def get_client_variable(
|
|
396
380
|
room_id: str, device_id: str, name: VarName
|
|
397
381
|
) -> dict[str, object]:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
raise HTTPException(
|
|
402
|
-
status_code=404,
|
|
403
|
-
detail=f"Device '{device_id}' has no client mapping",
|
|
382
|
+
if server is not None:
|
|
383
|
+
client_no, variables = server.get_client_variables_for_device(
|
|
384
|
+
room_id, device_id
|
|
404
385
|
)
|
|
386
|
+
else:
|
|
387
|
+
bridge = manager.get(room_id)
|
|
388
|
+
client_no, variables = bridge.get_client_variables(device_id)
|
|
405
389
|
if name not in variables:
|
|
406
390
|
raise HTTPException(
|
|
407
391
|
status_code=404,
|
|
@@ -409,6 +393,32 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
409
393
|
)
|
|
410
394
|
return {"clientNo": client_no, "value": variables[name]}
|
|
411
395
|
|
|
396
|
+
@app.delete("/v1/rooms/{room_id}/devices/{device_id}/client-variables")
|
|
397
|
+
def delete_client_variables(room_id: str, device_id: str) -> dict[str, object]:
|
|
398
|
+
if server is None:
|
|
399
|
+
raise HTTPException(
|
|
400
|
+
status_code=501,
|
|
401
|
+
detail="Client variable deletion requires an injected server",
|
|
402
|
+
)
|
|
403
|
+
client_no, deleted_count = server.delete_client_variables_for_device(
|
|
404
|
+
room_id, device_id
|
|
405
|
+
)
|
|
406
|
+
return {"clientNo": client_no, "deletedCount": deleted_count}
|
|
407
|
+
|
|
408
|
+
@app.delete("/v1/rooms/{room_id}/devices/{device_id}/client-variables/{name}")
|
|
409
|
+
def delete_client_variable(
|
|
410
|
+
room_id: str, device_id: str, name: VarName
|
|
411
|
+
) -> dict[str, object]:
|
|
412
|
+
if server is None:
|
|
413
|
+
raise HTTPException(
|
|
414
|
+
status_code=501,
|
|
415
|
+
detail="Client variable deletion requires an injected server",
|
|
416
|
+
)
|
|
417
|
+
client_no, deleted_count = server.delete_client_variables_for_device(
|
|
418
|
+
room_id, device_id, name
|
|
419
|
+
)
|
|
420
|
+
return {"clientNo": client_no, "deletedCount": deleted_count}
|
|
421
|
+
|
|
412
422
|
return app
|
|
413
423
|
|
|
414
424
|
|