Merge remote-tracking branch 'origin/ci'

This commit is contained in:
2026-02-25 13:09:47 +01:00
22 changed files with 1302 additions and 28 deletions
+65 -2
View File
@@ -1,7 +1,7 @@
extends Node
# Connection states
enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, ERROR }
enum ConnectionState { DISCONNECTED, CONNECTING, HANDSHAKING, CONNECTED, ERROR }
var state: ConnectionState = ConnectionState.DISCONNECTED
var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects to real server
@@ -21,9 +21,15 @@ const CONNECT_RETRY_INTERVAL: float = 0.1 # Seconds between retry attempts
var _connect_retries: int = 0
var _retry_timer: float = 0.0
# Handshake state (#556)
const HANDSHAKE_TIMEOUT_USEC: int = 5_000_000 # 5 seconds
var _handshake_start_usec: int = 0
# Signals
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
signal snapshot_received(snapshot: Dictionary)
signal handshake_complete(protocol_version: int)
signal handshake_failed(reason: String)
func _ready() -> void:
if test_mode:
@@ -159,7 +165,8 @@ func _process(delta: float) -> void:
_bridge.poll()
match _bridge.get_status():
StreamPeerTCP.STATUS_CONNECTED:
_set_state(ConnectionState.CONNECTED)
_handshake_start_usec = Time.get_ticks_usec()
_set_state(ConnectionState.HANDSHAKING)
StreamPeerTCP.STATUS_CONNECTING:
pass # Still connecting, wait
StreamPeerTCP.STATUS_ERROR:
@@ -172,6 +179,62 @@ func _process(delta: float) -> void:
_bridge = null # Reset and retry
return
# HANDSHAKING state: read first framed message, validate HandshakeMessage (#556)
if state == ConnectionState.HANDSHAKING:
if _bridge == null:
_set_state(ConnectionState.ERROR)
return
_bridge.poll()
# Check connection dropped during handshake
var bridge_status := _bridge.get_status()
if bridge_status == StreamPeerTCP.STATUS_ERROR or bridge_status == StreamPeerTCP.STATUS_NONE:
var reason := "Connection dropped during handshake"
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge = null
_set_state(ConnectionState.ERROR)
return
# Check timeout
if Time.get_ticks_usec() - _handshake_start_usec > HANDSHAKE_TIMEOUT_USEC:
var reason := "Handshake timeout: no message received within 5 seconds"
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge.disconnect_from_server()
_set_state(ConnectionState.ERROR)
return
# Try to read first message
var msg := _bridge.poll_message()
if msg.is_empty():
return # Not ready yet, continue polling
# Decode HandshakeMessage: { "protocol_version": N }
var decoded: Variant = Messagepack.decode(msg)
if decoded.status != null or not (decoded.value is Dictionary) \
or not decoded.value.has("protocol_version"):
var reason := "Handshake decode failed: malformed HandshakeMessage"
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge.disconnect_from_server()
_set_state(ConnectionState.ERROR)
return
var server_version: int = decoded.value["protocol_version"]
if server_version != Protocol.PROTOCOL_VERSION:
var reason := "Protocol version mismatch: server=%d, client=%d" % [
server_version, Protocol.PROTOCOL_VERSION]
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge.disconnect_from_server()
_set_state(ConnectionState.ERROR)
return
handshake_complete.emit(server_version)
_set_state(ConnectionState.CONNECTED)
return
if _bridge == null:
return
+1
View File
@@ -0,0 +1 @@
��
@@ -0,0 +1 @@
うtickヲaction→Interactげtarget_entity_idc、verb、Talk
@@ -0,0 +1 @@
うtickヲactionゥMoveNorth
Binary file not shown.
Binary file not shown.
+182
View File
@@ -0,0 +1,182 @@
## D-030 Layer 1: Cross-language IPC fixture tests (#271)
## Validates that Protocol.gd decodes the #271 named fixtures identically to Rust.
## Fixtures generated by: cargo test --test gen_fixtures -- --ignored
## Rust validation: server/tests/serialization.rs (fixture_* tests)
class_name TestIpcFixtures
extends GdUnitTestSuite
const FIXTURE_DIR = "res://tests/fixtures/msgpack/"
func _load_fixture(name: String) -> PackedByteArray:
var path = FIXTURE_DIR + name + ".msgpack"
var file = FileAccess.open(path, FileAccess.READ)
assert_that(file).is_not_null().override_failure_message(
"Fixture not found: %s — run 'make fixtures' to regenerate" % path
)
return file.get_buffer(file.get_length())
# -- snapshot_minimal ----------------------------------------------------------
func test_fixture_snapshot_minimal_version() -> void:
var bytes = _load_fixture("snapshot_minimal")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot).is_not_null()
assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION)
func test_fixture_snapshot_minimal_tick() -> void:
var bytes = _load_fixture("snapshot_minimal")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.tick).is_equal(0)
func test_fixture_snapshot_minimal_entity_count() -> void:
var bytes = _load_fixture("snapshot_minimal")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.entities.size()).is_equal(1)
func test_fixture_snapshot_minimal_entity_kind() -> void:
var bytes = _load_fixture("snapshot_minimal")
var snapshot = Protocol.decode_snapshot(bytes)
var entity = snapshot.entities[0]
assert_that(entity.entity_id).is_equal(1)
# entity.kind is {"variant": "Player", "data": null} from _decode_enum_variant
assert_that(entity.kind.variant).is_equal("Player")
func test_fixture_snapshot_minimal_no_monologue() -> void:
var bytes = _load_fixture("snapshot_minimal")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.current_monologue).is_null()
func test_fixture_snapshot_minimal_no_dialogue() -> void:
var bytes = _load_fixture("snapshot_minimal")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.dialogue_response).is_null()
# -- snapshot_full -------------------------------------------------------------
func test_fixture_snapshot_full_tick() -> void:
var bytes = _load_fixture("snapshot_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot).is_not_null()
assert_that(snapshot.tick).is_equal(42)
func test_fixture_snapshot_full_monologue_id() -> void:
var bytes = _load_fixture("snapshot_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.current_monologue).is_not_null()
assert_that(snapshot.current_monologue.id).is_equal("test_monologue_001")
func test_fixture_snapshot_full_monologue_text() -> void:
var bytes = _load_fixture("snapshot_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.current_monologue.text).is_equal("Something feels off about this place.")
func test_fixture_snapshot_full_dialogue_speaker() -> void:
var bytes = _load_fixture("snapshot_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.dialogue_response).is_not_null()
# dialogue_response has: line_id, text, speaker_entity_id (per protocol.gd v8 decode)
assert_that(snapshot.dialogue_response.speaker_entity_id).is_equal(99)
func test_fixture_snapshot_full_inventory() -> void:
var bytes = _load_fixture("snapshot_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.player_inventory.size()).is_equal(1)
assert_that(snapshot.player_inventory[0].name).is_equal("Forged Customs Cert")
func test_fixture_snapshot_full_poi_list() -> void:
var bytes = _load_fixture("snapshot_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.poi_list.size()).is_equal(1)
assert_that(snapshot.poi_list[0].poi_id).is_equal("docking_bay_7")
func test_fixture_snapshot_full_player_knowledge_entity() -> void:
var bytes = _load_fixture("snapshot_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.player_knowledge).is_not_null()
assert_that(snapshot.player_knowledge.entities.size()).is_equal(1)
assert_that(snapshot.player_knowledge.entities[0].name).is_equal("Kael")
func test_fixture_snapshot_full_player_knowledge_fact() -> void:
var bytes = _load_fixture("snapshot_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.player_knowledge.facts.size()).is_equal(1)
assert_that(snapshot.player_knowledge.facts[0].fact_id).is_equal("poi.docking_bay_7")
# -- player_input_move ---------------------------------------------------------
func test_fixture_player_input_move_tick() -> void:
var bytes = _load_fixture("player_input_move")
var input = Protocol.decode_player_input(bytes)
assert_that(input).is_not_null()
assert_that(input.tick).is_equal(1)
func test_fixture_player_input_move_action() -> void:
var bytes = _load_fixture("player_input_move")
var input = Protocol.decode_player_input(bytes)
# action is {"variant": "MoveNorth", "data": null} from _decode_enum_variant
assert_that(input.action.variant).is_equal("MoveNorth")
# -- player_input_interact -----------------------------------------------------
func test_fixture_player_input_interact_tick() -> void:
var bytes = _load_fixture("player_input_interact")
var input = Protocol.decode_player_input(bytes)
assert_that(input).is_not_null()
assert_that(input.tick).is_equal(2)
func test_fixture_player_input_interact_action() -> void:
var bytes = _load_fixture("player_input_interact")
var input = Protocol.decode_player_input(bytes)
# Interact is a struct variant: {"variant": "Interact", "data": {"target_entity_id": 99, "verb": "Talk"}}
assert_that(input.action.variant).is_equal("Interact")
func test_fixture_player_input_interact_target() -> void:
var bytes = _load_fixture("player_input_interact")
var input = Protocol.decode_player_input(bytes)
assert_that(input.action.data.target_entity_id).is_equal(99)
func test_fixture_player_input_interact_verb() -> void:
var bytes = _load_fixture("player_input_interact")
var input = Protocol.decode_player_input(bytes)
assert_that(input.action.data.verb).is_equal("Talk")
# -- malformed -----------------------------------------------------------------
func test_fixture_malformed_snapshot_fails() -> void:
var bytes = _load_fixture("malformed")
# Intentionally truncated — decode_snapshot must return null (not crash)
var result = Protocol.decode_snapshot(bytes)
assert_that(result).is_null().override_failure_message(
"malformed fixture should not decode as a valid ObserverSnapshot"
)
func test_fixture_malformed_input_fails() -> void:
var bytes = _load_fixture("malformed")
# Intentionally truncated — decode_player_input must return null (not crash)
var result = Protocol.decode_player_input(bytes)
assert_that(result).is_null().override_failure_message(
"malformed fixture should not decode as a valid PlayerInput"
)