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()