OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / integration / layer2_peer_a.gd

154 lines · gd ref:
extends SceneTree
## Layer 2 — Peer A: DHT Announcer + Listener
##
## Matches JS test pattern: both sides wait for full round-trip.
## 1. join_dht → bootstrap into local testnet
## 2. listen_on_dht → accept incoming DHT connections
## 3. announce_topic → make discoverable
## 4. Wait for peer_connected (Peer B connects via connect_to_peer)
## 5. Exchange packets (A sends → B echoes → A receives → A sends "done" → B receives)

const TIMEOUT := 30.0
const STATUS_FILE := "/tmp/layer2_peer_a_status.txt"
const POLL_HZ := 0.05

var _peer: Object
var _done := false
var _connected_id := -1
var _sent_hello := false
var _received_echo := false


func _init() -> void:
	_write_status("STARTING")
	_start.call_deferred()


func _start() -> void:
	# Read bootstrap
	var f := FileAccess.open("/tmp/layer_bootstrap.txt", FileAccess.READ)
	if f == null:
		_fail("Cannot read bootstrap")
		return
	var bootstrap := PackedStringArray(f.get_as_text().strip_edges().split(","))
	f.close()
	print("[A] bootstrap: %s" % str(bootstrap))

	# Single peer — DHT mode
	_peer = ClassDB.instantiate(&"HyperswarmMultiplayerPeer")
	var kp: Dictionary = _peer.generate_dht_keypair()
	var pk: PackedByteArray = kp["public_key"]
	var sk: PackedByteArray = kp["secret_key"]
	print("[A] dht pk: %s" % pk.hex_encode().left(16))

	# Write our public key so Peer B knows who we are
	_write_file("/tmp/layer2_peer_a_dht_pk.txt", pk.hex_encode())

	# Connect signals
	_peer.peer_connected.connect(func(id: int) -> void:
		print("[A] peer_connected: id=%d" % id)
		_connected_id = id
		_write_status("CONNECTED")
	)

	# Join DHT
	var err = _peer.join_dht(pk, sk, bootstrap)
	if err != OK:
		_fail("join_dht: %d" % err)
		return

	# Wait for DHT to become active
	var elapsed := 0.0
	while elapsed < 10.0:
		_peer.poll()
		if _peer.get_dht_status().get("active", false):
			break
		await create_timer(POLL_HZ).timeout
		elapsed += POLL_HZ

	if not _peer.get_dht_status().get("active", false):
		_fail("DHT not active after 10s")
		return

	print("[A] DHT active")

	# Listen for incoming DHT connections
	err = _peer.listen_on_dht()
	if err != OK:
		_fail("listen_on_dht: %d" % err)
		return

	# Announce topic so Peer B can discover us
	var topic := "test-p2p-layer2".sha256_buffer()
	err = _peer.announce_topic(topic)
	if err != OK:
		_fail("announce_topic: %d" % err)
		return

	_write_status("LISTENING")
	print("[A] listening on DHT, announced topic, waiting for peer B...")

	# Main poll loop — matches JS pattern: wait for full round-trip
	elapsed = 0.0
	while elapsed < TIMEOUT and not _done:
		_peer.poll()

		# Once connected, send hello
		if _connected_id > 0 and not _sent_hello:
			_sent_hello = true
			var msg := "hello from A".to_utf8_buffer()
			_peer.set_target_peer(_connected_id)
			_peer.set_transfer_mode(MultiplayerPeer.TRANSFER_MODE_RELIABLE)
			_peer.put_packet(msg)
			print("[A] sent 'hello from A'")

		# Check for incoming packets
		if _connected_id > 0:
			while _peer.get_available_packet_count() > 0:
				var pkt: PackedByteArray = _peer.get_packet()
				var text := pkt.get_string_from_utf8()
				print("[A] received: '%s'" % text)
				if text == "hello from B":
					_received_echo = true
					# Send "done" so B knows we got it (JS pattern: both sides confirm)
					var ack := "done".to_utf8_buffer()
					_peer.set_target_peer(_connected_id)
					_peer.set_transfer_mode(MultiplayerPeer.TRANSFER_MODE_RELIABLE)
					_peer.put_packet(ack)
					print("[A] sent 'done' — exchange complete")
					_write_status("PASS")
					_done = true
					break

		await create_timer(POLL_HZ).timeout
		elapsed += POLL_HZ

	if not _done:
		_fail("Timeout after %.1fs (connected=%s sent=%s received=%s)" % [
			elapsed, str(_connected_id > 0), str(_sent_hello), str(_received_echo)])
	else:
		# Flush: keep polling so the "done" packet actually gets sent
		for i in range(40):
			_peer.poll()
			await create_timer(POLL_HZ).timeout
		_peer.close()
	quit()


func _write_file(path: String, content: String) -> void:
	var wf := FileAccess.open(path, FileAccess.WRITE)
	wf.store_string(content)
	wf.close()


func _write_status(status: String) -> void:
	_write_file(STATUS_FILE, status)


func _fail(msg: String) -> void:
	print("[A] FAIL: %s" % msg)
	_write_status("FAIL: %s" % msg)
	if _peer:
		_peer.close()
	quit()