class_name LocalBridge ## TCP transport with length-prefixed framing for IPC (D-020). ## ## Wraps StreamPeerTCP with 4-byte big-endian length-prefix framing ## matching server/src/bridge/framing.rs. const MAX_MESSAGE_SIZE: int = 16 * 1024 * 1024 # 16 MB, matching server var _stream: StreamPeerTCP var _read_buffer: PackedByteArray = PackedByteArray() var _pending_length: int = -1 # -1 = awaiting header, >= 0 = awaiting payload var _corrupt: bool = false # Set on unrecoverable framing error func _init() -> void: _stream = StreamPeerTCP.new() ## Initiate connection to host:port. Non-blocking — poll get_status() for result. func connect_to_server(host: String, port: int) -> Error: return _stream.connect_to_host(host, port) ## Poll the TCP stream. Call every frame to drive connection and reads. func poll() -> void: _stream.poll() ## Current TCP connection status. func get_status() -> StreamPeerTCP.Status: if _corrupt: return StreamPeerTCP.STATUS_ERROR return _stream.get_status() ## Whether the TCP stream is in the connected state. func is_connected_to_server() -> bool: return not _corrupt and _stream.get_status() == StreamPeerTCP.STATUS_CONNECTED ## Send a framed message: [4-byte BE length][payload]. ## Concatenates header and payload into a single put_data() call to avoid partial writes. func send_message(payload: PackedByteArray) -> Error: if not is_connected_to_server(): return ERR_CONNECTION_ERROR var len := payload.size() if len > MAX_MESSAGE_SIZE: push_error("LocalBridge: message too large: %d bytes (max %d)" % [len, MAX_MESSAGE_SIZE]) return ERR_PARAMETER_RANGE_ERROR # Build complete frame: [4-byte BE length][payload] var frame := PackedByteArray() frame.resize(4 + len) frame[0] = (len >> 24) & 0xFF frame[1] = (len >> 16) & 0xFF frame[2] = (len >> 8) & 0xFF frame[3] = len & 0xFF for i in range(len): frame[4 + i] = payload[i] return _stream.put_data(frame) ## Non-blocking poll for a complete framed message. ## Returns the payload if a complete message is available, empty array otherwise. ## Call in a loop until empty to drain all buffered messages. func poll_message() -> PackedByteArray: if not is_connected_to_server(): return PackedByteArray() # Read any available bytes into the buffer var available := _stream.get_available_bytes() if available > 0: var result := _stream.get_data(available) if result[0] != OK: return PackedByteArray() _read_buffer.append_array(result[1]) return _try_extract_message() ## Try to extract a complete message from the read buffer. func _try_extract_message() -> PackedByteArray: # Phase 1: read 4-byte header if we don't have a pending length if _pending_length < 0: if _read_buffer.size() < 4: return PackedByteArray() _pending_length = (_read_buffer[0] << 24) | (_read_buffer[1] << 16) | \ (_read_buffer[2] << 8) | _read_buffer[3] _read_buffer = _read_buffer.slice(4) if _pending_length > MAX_MESSAGE_SIZE: # Stream is corrupt — we can't find the next valid frame boundary. # Disconnect rather than silently discarding valid buffered data. push_error( "LocalBridge: incoming message too large: %d bytes (max %d) — disconnecting" % [_pending_length, MAX_MESSAGE_SIZE]) _corrupt = true _pending_length = -1 _read_buffer.clear() disconnect_from_server() return PackedByteArray() # Phase 2: read payload if _read_buffer.size() < _pending_length: return PackedByteArray() var payload := _read_buffer.slice(0, _pending_length) _read_buffer = _read_buffer.slice(_pending_length) _pending_length = -1 return payload ## Close the connection and reset read state. func disconnect_from_server() -> void: _stream.disconnect_from_host() _read_buffer.clear() _pending_length = -1 ## Reset corrupt state (for reconnection after disconnect). func reset() -> void: _corrupt = false _read_buffer.clear() _pending_length = -1 # -- Static helpers for D-030 Layer 2 testing ---------------------------------- # These exist for unit tests that verify framing logic without a live TCP # connection. Not used in production code paths. ## Encode a payload into a framed byte array: [4-byte BE length][payload]. static func frame_encode(payload: PackedByteArray) -> PackedByteArray: var len := payload.size() var frame := PackedByteArray() frame.resize(4 + len) frame[0] = (len >> 24) & 0xFF frame[1] = (len >> 16) & 0xFF frame[2] = (len >> 8) & 0xFF frame[3] = len & 0xFF for i in range(len): frame[4 + i] = payload[i] return frame ## Decode the first framed message from raw bytes. ## Returns { "payload": PackedByteArray, "remainder": PackedByteArray } or null if incomplete. static func frame_decode(data: PackedByteArray) -> Variant: if data.size() < 4: return null var len := (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3] if len > MAX_MESSAGE_SIZE: push_error("LocalBridge: frame too large: %d bytes" % len) return null if data.size() < 4 + len: return null return { "payload": data.slice(4, 4 + len), "remainder": data.slice(4 + len), }