extends Control
## Minimal Hyperswarm relay test.
##
## Proves that game data can flow through a real Hyperswarm connection
## between two peers (discovery + hole-punch + encrypted stream).
##
## Run on TWO machines that have the sidecar running:
## Machine A: starts sidecar, opens this scene
## Machine B: starts sidecar, opens this scene
## Both join the same topic, discover each other, then exchange pings.
const IPC_PORT := 3118
var socket := StreamPeerTCP.new()
var _buffer: String = ""
var _connected := false
var _my_key := ""
var _peer_key := ""
var _peer_name := ""
var _state := "connecting" # connecting → discovering → testing → done
# Ping tracking
var _ping_sent_at := 0 # usec
var _ping_count := 0
var _pong_count := 0
var _rtts: Array[float] = []
var _test_start := 0
var _test_duration := 10000 # 10 seconds
@onready var log_label: RichTextLabel = %LogLabel
@onready var status_label: Label = %StatusLabel
@onready var results_label: Label = %ResultsLabel
func _ready() -> void:
_log("Connecting to sidecar on 127.0.0.1:%d..." % IPC_PORT)
var err := socket.connect_to_host("127.0.0.1", IPC_PORT)
if err != OK:
_log("[ERROR] Failed to connect: %s" % error_string(err))
_state = "error"
func _process(_delta: float) -> void:
socket.poll()
if not _connected:
if socket.get_status() == StreamPeerTCP.STATUS_CONNECTED:
_connected = true
_log("Connected to sidecar IPC")
_state = "discovering"
# Start discovery on the test topic
_send({"method": "start", "gameName": "hyper-tps-relay-test"})
elif socket.get_status() == StreamPeerTCP.STATUS_ERROR:
_log("[ERROR] Connection failed — is sidecar running?")
_state = "error"
return
if socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
_connected = false
_log("[DISCONNECTED]")
_state = "error"
return
# Read data
var available := socket.get_available_bytes()
if available > 0:
var result := socket.get_partial_data(available)
if result[0] == OK:
_buffer += (result[1] as PackedByteArray).get_string_from_utf8()
_parse_buffer()
# Send pings during test
if _state == "testing" and not _peer_key.is_empty():
var now := Time.get_ticks_msec()
if now - _test_start > _test_duration:
_finish_test()
return
# Send ping if no outstanding one (pong-gated)
if _ping_sent_at == 0:
_send_ping()
# Update status
match _state:
"connecting":
status_label.text = "Connecting to sidecar..."
"discovering":
status_label.text = "Discovering peers on Hyperswarm..."
"testing":
var elapsed := (Time.get_ticks_msec() - _test_start) / 1000.0
status_label.text = "Testing: %d pings, %d pongs (%.1fs / %.1fs)" % [_ping_count, _pong_count, elapsed, _test_duration / 1000.0]
"done":
status_label.text = "Test complete"
"error":
status_label.text = "ERROR — check log"
func _parse_buffer() -> void:
while true:
var idx := _buffer.find("\n")
if idx < 0:
break
var line := _buffer.substr(0, idx).strip_edges()
_buffer = _buffer.substr(idx + 1)
if line.length() > 0:
var data: Variant = JSON.parse_string(line)
if data is Dictionary:
_handle_event(data)
func _handle_event(data: Dictionary) -> void:
var event: String = data.get("event", "")
match event:
"identity":
_my_key = data.get("publicKey", "")
_log("Identity: %s (key: %s...)" % [data.get("name", "?"), _my_key.substr(0, 12)])
"lobby-ready":
_log("Lobby ready, waiting for peers...")
"peer-discovered":
var pk: String = data.get("publicKey", "")
var name: String = data.get("name", "Unknown")
_log("Peer discovered: %s (key: %s...)" % [name, pk.substr(0, 12)])
if _peer_key.is_empty():
_peer_key = pk
_peer_name = name
_log("Starting relay ping test with %s..." % name)
_state = "testing"
_test_start = Time.get_ticks_msec()
"peer-gone":
var pk: String = data.get("publicKey", "")
_log("Peer gone: %s..." % pk.substr(0, 12))
if pk == _peer_key:
_peer_key = ""
if _state == "testing":
_log("Peer disconnected during test!")
_finish_test()
"relay-data":
var from_key: String = data.get("from", "")
var payload: Dictionary = data.get("data", {})
_handle_relay(from_key, payload)
"error":
_log("[SIDECAR ERROR] %s" % data.get("message", "?"))
_:
pass # Ignore other events
func _handle_relay(from_key: String, payload: Dictionary) -> void:
var msg_type: String = payload.get("type", "")
if msg_type == "ping":
# Echo back as pong (include original timestamp)
_send({
"method": "relay",
"target": from_key,
"data": {
"type": "pong",
"ts": payload.get("ts", 0),
"size": payload.get("size", 0)
}
})
elif msg_type == "pong":
var sent_ts: int = int(payload.get("ts", 0))
var now_us := Time.get_ticks_usec()
var rtt_ms := (now_us - sent_ts) / 1000.0
_rtts.append(rtt_ms)
_pong_count += 1
_ping_sent_at = 0 # Allow next ping
if _pong_count <= 5 or _pong_count % 50 == 0:
_log(" pong #%d: RTT=%.2fms" % [_pong_count, rtt_ms])
# Stats updated in _finish_test()
func _send_ping() -> void:
_ping_count += 1
_ping_sent_at = Time.get_ticks_usec()
_send({
"method": "relay",
"target": _peer_key,
"data": {
"type": "ping",
"ts": _ping_sent_at,
"seq": _ping_count,
"size": 256
}
})
func _finish_test() -> void:
_state = "done"
_log("")
_log("=== TEST COMPLETE ===")
_log("Pings sent: %d" % _ping_count)
_log("Pongs received: %d" % _pong_count)
_log("Packet loss: %.1f%%" % ((1.0 - float(_pong_count) / max(_ping_count, 1)) * 100.0))
if _rtts.size() > 0:
var sorted := _rtts.duplicate()
sorted.sort()
var avg := 0.0
for r in sorted:
avg += r
avg /= sorted.size()
var p50: float = sorted[int(sorted.size() * 0.5)]
var p95: float = sorted[int(sorted.size() * 0.95)]
var p99: float = sorted[mini(int(sorted.size() * 0.99), sorted.size() - 1)]
var mn: float = sorted[0]
var mx: float = sorted[sorted.size() - 1]
_log("RTT avg=%.2fms min=%.2fms max=%.2fms p50=%.2fms p95=%.2fms p99=%.2fms" % [avg, mn, mx, p50, p95, p99])
_log("Samples: %d over %.1fs" % [_rtts.size(), _test_duration / 1000.0])
results_label.text = "RTT: avg=%.2fms p50=%.2fms p95=%.2fms | %d/%d pongs | loss=%.1f%%" % [
avg, p50, p95, _pong_count, _ping_count,
(1.0 - float(_pong_count) / max(_ping_count, 1)) * 100.0
]
else:
_log("No RTT data — pongs never arrived.")
results_label.text = "FAILED: No pongs received"
func _send(msg: Dictionary) -> void:
if socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
return
var data := (JSON.stringify(msg) + "\n").to_utf8_buffer()
socket.put_data(data)
func _log(text: String) -> void:
print("[RelayTest] ", text)
if log_label:
log_label.append_text(text + "\n")