OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / test_relay.gd

240 lines · gd ref:
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")