extends Node # Autoload (project.godot [autoload] MatchSim). Phase 2 simulation RPCs: # match_config (server assigns arena + deterministic slot order from # MatchNet.roster), input (client -> server, per-tick action), snapshot # (server -> client, NetCodec-packed body state), and a small score_update # for the HUD. Lives on an autoload per §1.3's derived decision ("All # hot-path RPCs live on autoloads") even though these are scoped to # whichever match happens to be running — a scene-node RPC target would # need matching NodePaths across peers, which an autoload sidesteps # entirely, and it's what lets NetworkedMatch itself stay a plain scene # node with no networking-identity concerns of its own. # # Channel intent per §2.1: 0 reliable (match_config, score_update), 1 # unreliable-ordered (input), 2 unreliable-ordered (snapshot) — not yet # verified against ENet's own reserved system channel offset (§2.1's own # "verify empirically" hedge); if that turns out to matter these indices # will need adjusting, not the RPC design itself. const NetCodec = preload("res://scripts/net_codec.gd") signal match_config_received(arena_path: String, peer_ids: PackedInt32Array, teams: PackedInt32Array, spawn_indices: PackedInt32Array) signal input_received(peer_id: int, decoded: Dictionary) # decoded: see NetCodec.unpack_input signal snapshot_received(decoded: Dictionary) # decoded: see NetCodec.unpack_snapshot signal score_update_received(score: Dictionary) # Server only: the last match_config actually sent, so a client whose own # scene load (and therefore its match_config_received listener) finishes # AFTER the server already broadcast can still get it — a one-shot # broadcast alone is racy against however long the client takes to reach # the point where it's listening, and Godot signals never buffer for a # late connection. request_match_config() closes that race by turning # delivery into "ask until you get it" instead of "hope you were already # listening." Also covers a late joiner mid-match (Phase 5 will still need # to add live match *state*, not just this static config, for that case). var _last_match_config: Dictionary = {} func send_match_config(arena_path: String, peer_ids: PackedInt32Array, teams: PackedInt32Array, spawn_indices: PackedInt32Array) -> void: _last_match_config = { "arena_path": arena_path, "peer_ids": peer_ids, "teams": teams, "spawn_indices": spawn_indices, } _match_config.rpc(arena_path, peer_ids, teams, spawn_indices) func request_match_config() -> void: _request_match_config.rpc_id(1) func send_input(bytes: PackedByteArray) -> void: # bytes is already fully packed (any timestamps it carries are already # fixed), so wrapping the dispatch itself is enough — task 2.8. NetSim.send(func() -> void: _recv_input.rpc_id(1, bytes), 1) func send_snapshot(peer_id: int, bytes: PackedByteArray) -> void: NetSim.send(func() -> void: _snapshot.rpc_id(peer_id, bytes), peer_id) func send_score_update(score: Dictionary) -> void: _score_update.rpc(score) @rpc("authority", "call_remote", "reliable", 0) func _match_config(arena_path: String, peer_ids: PackedInt32Array, teams: PackedInt32Array, spawn_indices: PackedInt32Array) -> void: match_config_received.emit(arena_path, peer_ids, teams, spawn_indices) @rpc("any_peer", "call_remote", "reliable", 0) func _request_match_config() -> void: if not multiplayer.is_server() or _last_match_config.is_empty(): return var peer_id := multiplayer.get_remote_sender_id() _match_config.rpc_id( peer_id, _last_match_config["arena_path"], _last_match_config["peer_ids"], _last_match_config["teams"], _last_match_config["spawn_indices"] ) @rpc("any_peer", "call_remote", "unreliable_ordered", 1) func _recv_input(bytes: PackedByteArray) -> void: if not multiplayer.is_server(): return var peer_id := multiplayer.get_remote_sender_id() var decoded := NetCodec.unpack_input(bytes) input_received.emit(peer_id, decoded) @rpc("authority", "call_remote", "unreliable_ordered", 2) func _snapshot(bytes: PackedByteArray) -> void: var decoded := NetCodec.unpack_snapshot(bytes) snapshot_received.emit(decoded) @rpc("authority", "call_remote", "reliable", 0) func _score_update(score: Dictionary) -> void: score_update_received.emit(score)