├── .editorconfig ├── addons ├── nodetunnel │ ├── NodeTunnelPeer.gd.uid │ ├── internal │ │ ├── ByteUtils.gd.uid │ │ ├── _NodeTunnelTCP.gd.uid │ │ ├── _NodeTunnelUDP.gd.uid │ │ ├── _PacketManager.gd.uid │ │ ├── _NodeTunnelTCP.gd │ │ ├── ByteUtils.gd │ │ ├── _NodeTunnelUDP.gd │ │ └── _PacketManager.gd │ ├── nodetunnel_plugin.gd.uid │ ├── demo │ │ ├── node_tunnel_demo.gd.uid │ │ ├── player │ │ │ ├── node_tunnel_demo_player.gd.uid │ │ │ ├── node_tunnel_demo_sprite.png │ │ │ ├── node_tunnel_demo_player.gd │ │ │ ├── node_tunnel_demo_sprite.png.import │ │ │ └── node_tunnel_demo_player.tscn │ │ ├── node_tunnel_demo.tscn │ │ └── node_tunnel_demo.gd │ ├── plugin.cfg │ ├── nodetunnel_plugin.gd │ ├── README.md │ └── NodeTunnelPeer.gd ├── .DS_Store └── nodetunnel.zip ├── .gitignore ├── .DS_Store ├── .gitattributes ├── project.godot ├── icon.svg ├── icon.svg.import └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | -------------------------------------------------------------------------------- /addons/nodetunnel/NodeTunnelPeer.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cqp27t51qx2j2 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | /android/ 4 | -------------------------------------------------------------------------------- /addons/nodetunnel/internal/ByteUtils.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bn6n3mdxa3qvs 2 | -------------------------------------------------------------------------------- /addons/nodetunnel/nodetunnel_plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dwr63fil1rwp1 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curtjs/nodetunnel/HEAD/.DS_Store -------------------------------------------------------------------------------- /addons/nodetunnel/demo/node_tunnel_demo.gd.uid: -------------------------------------------------------------------------------- 1 | uid://du23v82cdps36 2 | -------------------------------------------------------------------------------- /addons/nodetunnel/internal/_NodeTunnelTCP.gd.uid: -------------------------------------------------------------------------------- 1 | uid://vn0nnvu60gws 2 | -------------------------------------------------------------------------------- /addons/nodetunnel/internal/_NodeTunnelUDP.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c7odr0jkslhp5 2 | -------------------------------------------------------------------------------- /addons/nodetunnel/internal/_PacketManager.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d2yxilmwtu7gh 2 | -------------------------------------------------------------------------------- /addons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curtjs/nodetunnel/HEAD/addons/.DS_Store -------------------------------------------------------------------------------- /addons/nodetunnel/demo/player/node_tunnel_demo_player.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ce3ifw4v3jgvw 2 | -------------------------------------------------------------------------------- /addons/nodetunnel.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curtjs/nodetunnel/HEAD/addons/nodetunnel.zip -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /addons/nodetunnel/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="NodeTunnel" 4 | description="" 5 | author="curtjs" 6 | version="0.1" 7 | script="nodetunnel_plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/nodetunnel/demo/player/node_tunnel_demo_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curtjs/nodetunnel/HEAD/addons/nodetunnel/demo/player/node_tunnel_demo_sprite.png -------------------------------------------------------------------------------- /addons/nodetunnel/nodetunnel_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree() -> void: 5 | print("NodeTunnel addon enabled") 6 | 7 | func _exit_tree() -> void: 8 | print("NodeTunnel addon disabled") 9 | -------------------------------------------------------------------------------- /addons/nodetunnel/README.md: -------------------------------------------------------------------------------- 1 | # NodeTunnel 2 | 3 | Easy P2P multiplayer for Godot through relay servers. 4 | 5 | ## Quick Start 6 | 7 | ```gdscript 8 | var peer = NodeTunnelPeer.new() 9 | multiplayer.multiplayer_peer = peer 10 | 11 | peer.connect_to_relay("nodetunnel.io", 9998) 12 | await peer.relay_connected 13 | 14 | # Host or join 15 | peer.host() # To host 16 | peer.join("HOST_OID") # To join 17 | -------------------------------------------------------------------------------- /addons/nodetunnel/demo/player/node_tunnel_demo_player.gd: -------------------------------------------------------------------------------- 1 | extends CharacterBody2D 2 | 3 | # Same as any other movement script 4 | # Uses MultiplayerSynchronizer to sync position 5 | 6 | const SPEED = 250.0 7 | 8 | 9 | func _enter_tree() -> void: 10 | set_multiplayer_authority(name.to_int()) 11 | 12 | 13 | func _physics_process(delta: float) -> void: 14 | if !is_multiplayer_authority(): 15 | return 16 | 17 | var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") 18 | velocity = input_dir * SPEED 19 | 20 | move_and_slide() 21 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="nodetunnel" 14 | run/main_scene="uid://dxvqdifsxtr1h" 15 | config/features=PackedStringArray("4.4", "Forward Plus") 16 | config/icon="res://icon.svg" 17 | 18 | [editor_plugins] 19 | 20 | enabled=PackedStringArray("res://addons/nodetunnel/plugin.cfg") 21 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://clqj3ibtnaggk" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/nodetunnel/demo/player/node_tunnel_demo_sprite.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ct65mgn332epf" 6 | path="res://.godot/imported/node_tunnel_demo_sprite.png-a658bfb367cf526a7313a53de4dd2fef.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/nodetunnel/demo/player/node_tunnel_demo_sprite.png" 14 | dest_files=["res://.godot/imported/node_tunnel_demo_sprite.png-a658bfb367cf526a7313a53de4dd2fef.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/nodetunnel/demo/player/node_tunnel_demo_player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://k75j6hnh43t3"] 2 | 3 | [ext_resource type="Script" uid="uid://ce3ifw4v3jgvw" path="res://addons/nodetunnel/demo/player/node_tunnel_demo_player.gd" id="1_7jw1k"] 4 | [ext_resource type="Texture2D" uid="uid://ct65mgn332epf" path="res://addons/nodetunnel/demo/player/node_tunnel_demo_sprite.png" id="1_sjhmu"] 5 | 6 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_sjhmu"] 7 | size = Vector2(128, 128) 8 | 9 | [sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_xryeg"] 10 | properties/0/path = NodePath(".:position") 11 | properties/0/spawn = true 12 | properties/0/replication_mode = 1 13 | 14 | [node name="NodeTunnelDemoPlayer" type="CharacterBody2D"] 15 | script = ExtResource("1_7jw1k") 16 | 17 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 18 | shape = SubResource("RectangleShape2D_sjhmu") 19 | 20 | [node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] 21 | replication_config = SubResource("SceneReplicationConfig_xryeg") 22 | 23 | [node name="Sprite2D" type="Sprite2D" parent="."] 24 | scale = Vector2(0.5, 0.5) 25 | texture = ExtResource("1_sjhmu") 26 | -------------------------------------------------------------------------------- /addons/nodetunnel/internal/_NodeTunnelTCP.gd: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # ⚠️ WARNING: INTERNAL NODETUNNEL CODE - DO NOT MODIFY ⚠️ 3 | # ============================================================================ 4 | # This class is part of NodeTunnel's internal networking implementation. 5 | # 6 | # 🚫 DO NOT: 7 | # - Call these methods directly 8 | # - Modify this code 9 | # - Inherit from this class 10 | # - Copy this code 11 | # 12 | # ✅ USE INSTEAD: NodeTunnelPeer class for all networking operations 13 | # 14 | # Modifying this code WILL break your networking and void support! 15 | # ============================================================================ 16 | 17 | class_name _NodeTunnelTCP 18 | 19 | # Connection state 20 | signal tcp_connected 21 | @warning_ignore("unused_signal") 22 | signal relay_connected(oid: String) 23 | 24 | # Connection state 25 | var tcp: StreamPeerTCP 26 | var connected = false 27 | var oid = "" 28 | 29 | # Message buffering 30 | var _buffer = PackedByteArray() 31 | 32 | func _init() -> void: 33 | tcp = StreamPeerTCP.new() 34 | tcp.big_endian = true 35 | 36 | func poll(packet_manager: _PacketManager) -> void: 37 | tcp.poll() 38 | 39 | if tcp.get_status() != StreamPeerTCP.STATUS_CONNECTED: 40 | return 41 | 42 | if connected == false: 43 | tcp_connected.emit() 44 | connected = true 45 | 46 | var available = tcp.get_available_bytes() 47 | if available > 0: 48 | var new_data = tcp.get_data(available)[1] 49 | _buffer.append_array(new_data) 50 | 51 | while _buffer.size() >= 4: 52 | var msg_len = ByteUtils.unpack_u32(_buffer, 0) 53 | 54 | if _buffer.size() >= 4 + msg_len: 55 | var msg_data = _buffer.slice(4, 4 + msg_len) 56 | _buffer = _buffer.slice(4 + msg_len) 57 | 58 | packet_manager.handle_msg(msg_data) 59 | else: 60 | break 61 | -------------------------------------------------------------------------------- /addons/nodetunnel/internal/ByteUtils.gd: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # ⚠️ WARNING: INTERNAL NODETUNNEL CODE - DO NOT MODIFY ⚠️ 3 | # ============================================================================ 4 | # This class is part of NodeTunnel's internal networking implementation. 5 | # 6 | # 🚫 DO NOT: 7 | # - Call these methods directly 8 | # - Modify this code 9 | # - Inherit from this class 10 | # - Copy this code 11 | # 12 | # ✅ USE INSTEAD: NodeTunnelPeer class for all networking operations 13 | # 14 | # Modifying this code WILL break your networking and void support! 15 | # ============================================================================ 16 | 17 | class_name ByteUtils 18 | 19 | static func unpack_u32(data: PackedByteArray, offset: int) -> int: 20 | if offset + 4 > data.size(): 21 | push_warning("ByteUtils.unpack_u32: Not enough data at offset %d (need 4 bytes, have %d)" % [offset, data.size() - offset]) 22 | return 0 23 | 24 | # Extract 4 bytes in big-endian order 25 | var big_endian_bytes = data.slice(offset, offset + 4) 26 | 27 | # Convert to little-endian for Godot's decode_u32 28 | var little_endian_bytes = PackedByteArray([ 29 | big_endian_bytes[3], 30 | big_endian_bytes[2], 31 | big_endian_bytes[1], 32 | big_endian_bytes[0] 33 | ]) 34 | 35 | return little_endian_bytes.decode_u32(0) 36 | 37 | static func pack_u32(value: int) -> PackedByteArray: 38 | if value < 0 or value > 0xFFFFFFFF: 39 | push_warning("ByteUtils.pack_u32: Value %d is outside valid range (0 to 4294967295)" % value) 40 | value = value & 0xFFFFFFFF # Clamp to 32-bit range 41 | 42 | # Encode in little-endian first 43 | var little_endian_bytes = PackedByteArray() 44 | little_endian_bytes.resize(4) 45 | little_endian_bytes.encode_u32(0, value) 46 | 47 | # Convert to big-endian for network transmission 48 | var big_endian_bytes = PackedByteArray([ 49 | little_endian_bytes[3], 50 | little_endian_bytes[2], 51 | little_endian_bytes[1], 52 | little_endian_bytes[0] 53 | ]) 54 | 55 | return big_endian_bytes 56 | -------------------------------------------------------------------------------- /addons/nodetunnel/demo/node_tunnel_demo.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://dxvqdifsxtr1h"] 2 | 3 | [ext_resource type="Script" uid="uid://du23v82cdps36" path="res://addons/nodetunnel/demo/node_tunnel_demo.gd" id="1_pe4km"] 4 | 5 | [node name="NodeTunnelDemo" type="Node2D"] 6 | script = ExtResource("1_pe4km") 7 | 8 | [node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="."] 9 | _spawnable_scenes = PackedStringArray("uid://k75j6hnh43t3") 10 | spawn_path = NodePath("..") 11 | 12 | [node name="UI" type="CanvasLayer" parent="."] 13 | 14 | [node name="Control" type="Control" parent="UI"] 15 | layout_mode = 3 16 | anchors_preset = 15 17 | anchor_right = 1.0 18 | anchor_bottom = 1.0 19 | grow_horizontal = 2 20 | grow_vertical = 2 21 | 22 | [node name="ConnectionControls" type="VBoxContainer" parent="UI/Control"] 23 | unique_name_in_owner = true 24 | layout_mode = 1 25 | anchors_preset = 8 26 | anchor_left = 0.5 27 | anchor_top = 0.5 28 | anchor_right = 0.5 29 | anchor_bottom = 0.5 30 | offset_left = -80.5 31 | offset_top = -64.0 32 | offset_right = 80.5 33 | offset_bottom = 64.0 34 | grow_horizontal = 2 35 | grow_vertical = 2 36 | 37 | [node name="Host" type="Button" parent="UI/Control/ConnectionControls"] 38 | layout_mode = 2 39 | text = "Host" 40 | 41 | [node name="HSeparator" type="HSeparator" parent="UI/Control/ConnectionControls"] 42 | layout_mode = 2 43 | 44 | [node name="HostID" type="LineEdit" parent="UI/Control/ConnectionControls"] 45 | unique_name_in_owner = true 46 | layout_mode = 2 47 | placeholder_text = "Host ID" 48 | 49 | [node name="Join" type="Button" parent="UI/Control/ConnectionControls"] 50 | layout_mode = 2 51 | text = "Join" 52 | 53 | [node name="IDLabel" type="Label" parent="UI/Control/ConnectionControls"] 54 | unique_name_in_owner = true 55 | layout_mode = 2 56 | text = "Online ID: ABCD1234" 57 | 58 | [node name="LeaveRoom" type="Button" parent="UI/Control"] 59 | unique_name_in_owner = true 60 | visible = false 61 | layout_mode = 0 62 | offset_right = 8.0 63 | offset_bottom = 8.0 64 | text = "Leave Room" 65 | 66 | [connection signal="pressed" from="UI/Control/ConnectionControls/Host" to="." method="_on_host_pressed"] 67 | [connection signal="pressed" from="UI/Control/ConnectionControls/Join" to="." method="_on_join_pressed"] 68 | [connection signal="pressed" from="UI/Control/LeaveRoom" to="." method="_on_leave_room_pressed"] 69 | -------------------------------------------------------------------------------- /addons/nodetunnel/demo/node_tunnel_demo.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | const PLAYER_SCENE = preload("res://addons/nodetunnel/demo/player/node_tunnel_demo_player.tscn") 4 | 5 | var peer: NodeTunnelPeer 6 | 7 | 8 | func _ready() -> void: 9 | # Create the NodeTunnelPeer 10 | peer = NodeTunnelPeer.new() 11 | #peer.debug_enabled = true # Enable debugging if needed 12 | 13 | # Always set the global peer *before* attempting to connect 14 | multiplayer.multiplayer_peer = peer 15 | 16 | # Connect to the public relay 17 | peer.connect_to_relay("relay.nodetunnel.io", 9998) 18 | 19 | # Wait until we have connected to the relay 20 | await peer.relay_connected 21 | 22 | # Attach peer_connected signal 23 | peer.peer_connected.connect(_add_player) 24 | 25 | # Attach peer_disconnected signal 26 | peer.peer_disconnected.connect(_remove_player) 27 | 28 | # Attach room_left signal 29 | peer.room_left.connect(_cleanup_room) 30 | 31 | # At this point, we can access the online ID that the server generated for us 32 | %IDLabel.text = "Online ID: " + peer.online_id 33 | 34 | 35 | func _on_host_pressed() -> void: 36 | print("Online ID: ", peer.online_id) 37 | 38 | # Host a game, must be done *after* relay connection is made 39 | peer.host() 40 | 41 | # Copy online id to clipboard 42 | DisplayServer.clipboard_set(peer.online_id) 43 | 44 | # Wait until peer has started hosting 45 | await peer.hosting 46 | 47 | # Spawn the host player 48 | _add_player() 49 | 50 | # Hide the UI 51 | %ConnectionControls.hide() 52 | 53 | # Show leave room button 54 | %LeaveRoom.show() 55 | 56 | 57 | func _on_join_pressed() -> void: 58 | # Join a game, must be done *after* relay connection is made 59 | # Requires the online ID of the host peer 60 | peer.join(%HostID.text) 61 | 62 | # Wait until peer has finished joining 63 | await peer.joined 64 | 65 | # Hide the UI 66 | %ConnectionControls.hide() 67 | 68 | # Show leave room button 69 | %LeaveRoom.show() 70 | 71 | # Same as any other Godot game 72 | # Uses the MultiplayerSpawner node's auto-spawn list to spawn players 73 | func _add_player(peer_id: int = 1) -> void: 74 | if !multiplayer.is_server(): return 75 | 76 | print("Player Joined: ", peer_id) 77 | var player = PLAYER_SCENE.instantiate() 78 | player.name = str(peer_id) 79 | add_child(player) 80 | 81 | 82 | func _remove_player(peer_id: int) -> void: 83 | if !multiplayer.is_server(): return 84 | 85 | var player = get_node(str(peer_id)) 86 | player.queue_free() 87 | 88 | 89 | func _on_leave_room_pressed() -> void: 90 | # Tells NodeTunnel to remove this peer from the room 91 | # Will eventually result in `peer.room_left` being emitted 92 | peer.leave_room() 93 | 94 | 95 | # This function runs whenever this peer gets removed from a room, 96 | # whether it's intentional or due to the host leaving. 97 | # See peer.room_left.connect(_cleanup_room) in the _ready() function 98 | func _cleanup_room() -> void: 99 | # Hide the leave room button 100 | %LeaveRoom.hide() 101 | 102 | # Show the main menu again 103 | %ConnectionControls.show() 104 | -------------------------------------------------------------------------------- /addons/nodetunnel/internal/_NodeTunnelUDP.gd: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # ⚠️ WARNING: INTERNAL NODETUNNEL CODE - DO NOT MODIFY ⚠️ 3 | # ============================================================================ 4 | # This class is part of NodeTunnel's internal networking implementation. 5 | # 6 | # 🚫 DO NOT: 7 | # - Call these methods directly 8 | # - Modify this code 9 | # - Inherit from this class 10 | # - Copy this code 11 | # 12 | # ✅ USE INSTEAD: NodeTunnelPeer class for all networking operations 13 | # 14 | # Modifying this code WILL break your networking and void support! 15 | # ============================================================================ 16 | 17 | class_name _NodeTunnelUDP 18 | 19 | var socket: PacketPeerUDP 20 | var relay_host: String 21 | var relay_port: int 22 | var online_id: String 23 | var connected: bool = false 24 | 25 | signal udp_connected 26 | signal packet_received(from_oid: String, data: PackedByteArray) 27 | 28 | func _init() -> void: 29 | socket = PacketPeerUDP.new() 30 | 31 | func connect_to_relay(host: String, port: int, oid: String): 32 | relay_port = port 33 | online_id = oid 34 | 35 | if not host.is_valid_ip_address(): 36 | var ip = IP.resolve_hostname(host) 37 | if ip == "": 38 | print("Failed to resolve hostname: ", host) 39 | return 40 | relay_host = ip 41 | print("Resolved ", host, " to ", ip) 42 | else: 43 | relay_host = host 44 | 45 | var res = socket.connect_to_host(relay_host, port) 46 | if res != OK: 47 | NodeTunnelPeer._log_error("Failed to connect to UDP socket: " + str(res)) 48 | 49 | func send_connect(): 50 | send_packet("SERVER", "UDP_CONNECT".to_utf8_buffer()) 51 | 52 | func handle_connect_res(): 53 | udp_connected.emit() 54 | connected = true 55 | 56 | func send_packet(to_oid: String, data: PackedByteArray): 57 | var packet = PackedByteArray() 58 | 59 | packet.append_array(ByteUtils.pack_u32(online_id.length())) 60 | packet.append_array(online_id.to_utf8_buffer()) 61 | 62 | packet.append_array(ByteUtils.pack_u32(to_oid.length())) 63 | packet.append_array(to_oid.to_utf8_buffer()) 64 | 65 | packet.append_array(data) 66 | 67 | socket.put_packet(packet) 68 | 69 | func poll(): 70 | while socket.get_available_packet_count() > 0: 71 | var packet = socket.get_packet() 72 | 73 | if packet.size() < 8: 74 | continue 75 | 76 | var offset = 0 77 | 78 | var sender_oid_len = ByteUtils.unpack_u32(packet, offset) 79 | offset += 4 80 | 81 | if packet.size() < offset + sender_oid_len + 4: 82 | continue 83 | 84 | var sender_oid = packet.slice(offset, offset + sender_oid_len).get_string_from_utf8() 85 | offset += sender_oid_len 86 | 87 | var target_oid_len = ByteUtils.unpack_u32(packet, offset) 88 | offset += 4 89 | 90 | if packet.size() < offset + target_oid_len: 91 | continue 92 | 93 | var _target_oid = packet.slice(offset, offset + target_oid_len).get_string_from_utf8() 94 | offset += target_oid_len 95 | 96 | var game_data = packet.slice(offset) 97 | 98 | if sender_oid == "SERVER": 99 | var message = game_data.get_string_from_utf8() 100 | if message == "UDP_CONNECT_RES": 101 | handle_connect_res() 102 | else: 103 | packet_received.emit(sender_oid, game_data) 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeTunnel 🚇 2 | 3 | **Easy P2P multiplayer networking for Godot through relay servers** 4 | 5 | > ⚠️ **EARLY DEVELOPMENT** - This is experimental software! Expect bugs, breaking changes, and dragons. Not recommended for production use. Please report any issues you run into! 6 | 7 | NodeTunnel provides peer-to-peer multiplayer without NAT traversal, port forwarding, or dedicated servers. Simply connect through a relay server and start playing! 8 | 9 | ## ✨ Features 10 | 11 | - **Drop-in replacement** for Godot's built-in multiplayer 12 | - **No port forwarding** or firewall configuration needed 13 | - **Works behind NAT** and restrictive networks 14 | - **Easy host/join** workflow with shareable room codes 15 | - **Compatible** with existing Godot networking code 16 | 17 | ## 🚀 Quick Start 18 | 19 | ### YouTube Tutorial 20 | [![Click here to view](http://img.youtube.com/vi/frNKdfPQfxA/0.jpg)](http://www.youtube.com/watch?v=frNKdfPQfxA "I Fixed Godot's Biggest Multiplayer Problem") 21 | 22 | ### Installation 23 | 24 | 1. Download from [GitHub Releases](https://github.com/curtjs/nodetunnel/releases) 25 | 2. Extract to your project's `addons/` folder 26 | 3. Enable "NodeTunnel" in Project Settings > Plugins 27 | 28 | ### Basic Usage 29 | 30 | ```gdscript 31 | extends Node2D 32 | 33 | func _ready(): 34 | var peer = NodeTunnelPeer.new() 35 | multiplayer.multiplayer_peer = peer 36 | 37 | # Connect to the free public relay 38 | # Note that this **must** be done before hosting/joining 39 | peer.connect_to_relay("relay.nodetunnel.io", 9998) 40 | await peer.relay_connected 41 | print("Connected! Your ID: ", peer.online_id) 42 | 43 | func host(): 44 | # Host a game 45 | peer.host() 46 | await peer.hosting 47 | print("Share this ID: ", peer.online_id) 48 | 49 | func join(): 50 | # Join a game 51 | peer.join(host_id) 52 | await peer.joined 53 | 54 | # Use normal Godot multiplayer from here! 55 | @rpc("any_peer") 56 | func player_moved(position: Vector2): 57 | print("Player moved to: ", position) 58 | ``` 59 | 60 | ## 🎮 How It Works 61 | 62 | 1. **Connect** to a relay server 63 | 2. **Get an Online ID** (like `ABC12345`) 64 | 3. **Host** or **join** using someone's ID 65 | 4. **Play** using normal Godot multiplayer 66 | 67 | The relay forwards packets between players, so everyone can connect regardless of network setup. 68 | 69 | ## 🌐 Free Public Relay 70 | 71 | I provide a free relay server for testing: 72 | 73 | - **Host**: `relay.nodetunnel.io:9998` 74 | - **Uptime**: See [nodetunnel.io](nodetunnel.io) 75 | 76 | **Note**: Don't rely on this for anything important! Server source code will be available soon for self-hosting. 77 | 78 | ## 📚 API Reference 79 | 80 | ### NodeTunnelPeer 81 | 82 | #### Signals 83 | - `relay_connected(online_id: String)` - Connected to relay 84 | - `hosting` - Started hosting 85 | - `joined` - Joined a session 86 | - `room_left` - Disconnected from the room 87 | 88 | #### Methods 89 | - `connect_to_relay(host: String, port: int)` - Connect to relay 90 | - `host()` - Start hosting 91 | - `join(host_oid: String)` - Join using host's online ID 92 | - `leave_room()` - Leaves the current room, will delete the room when called from host 93 | - `disconnect_from_relay()` - Disconnect 94 | 95 | #### Properties 96 | - `online_id: String` - Your unique session ID 97 | - `debug_enabled: bool` - Enable debug logging 98 | 99 | ## 🔧 Troubleshooting 100 | 101 | **Enable debug logging:** 102 | ```gdscript 103 | peer.debug_enabled = true 104 | ``` 105 | 106 | **Common issues:** 107 | - Check internet connection 108 | - Verify online IDs are correct 109 | - Make sure both players use the same relay server 110 | 111 | ## ⚠️ Limitations 112 | 113 | - **Early alpha** - expect bugs and breaking changes 114 | - **WebGL not supported** (use WebRTC for web games) 115 | - **Free relay** has no uptime guarantees 116 | - **API will change** without notice 117 | 118 | ## 📄 License 119 | 120 | MIT License 121 | 122 | ## 🆘 Support 123 | 124 | - 🐛 **Issues**: [GitHub Issues](https://github.com/curtjs/nodetunnel/issues) 125 | - 💬 **Discord**: [Discord Server](https://discord.com/invite/qxjZ3hFVVR) 126 | -------------------------------------------------------------------------------- /addons/nodetunnel/internal/_PacketManager.gd: -------------------------------------------------------------------------------- 1 | # ============================================================================ 2 | # ⚠️ WARNING: INTERNAL NODETUNNEL CODE - DO NOT MODIFY ⚠️ 3 | # ============================================================================ 4 | # This class is part of NodeTunnel's internal networking implementation. 5 | # 6 | # 🚫 DO NOT: 7 | # - Call these methods directly 8 | # - Modify this code 9 | # - Inherit from this class 10 | # - Copy this code 11 | # 12 | # ✅ USE INSTEAD: NodeTunnelPeer class for all networking operations 13 | # 14 | # Modifying this code WILL break your networking and void support! 15 | # ============================================================================ 16 | 17 | class_name _PacketManager 18 | 19 | signal connect_res(oid: String) 20 | signal host_res 21 | signal join_res 22 | signal peer_list_res(nid_to_oid: Dictionary[int, String]) 23 | signal leave_room_res 24 | 25 | enum PacketType { 26 | CONNECT, 27 | HOST, 28 | JOIN, 29 | PEERLIST, 30 | LEAVE_ROOM 31 | } 32 | 33 | func handle_msg(data: PackedByteArray) -> void: 34 | var pkt_type = ByteUtils.unpack_u32(data, 0) 35 | var payload = data.slice(4) 36 | 37 | match pkt_type: 38 | PacketType.CONNECT: 39 | var oid = _handle_connect_res(payload) 40 | connect_res.emit(oid) 41 | PacketType.HOST: 42 | host_res.emit() 43 | PacketType.JOIN: 44 | join_res.emit() 45 | PacketType.PEERLIST: 46 | var p_list = _handle_peer_list(payload) 47 | peer_list_res.emit(p_list) 48 | PacketType.LEAVE_ROOM: 49 | leave_room_res.emit() 50 | 51 | ## Sends a connect request to the server 52 | func send_conect(tcp: StreamPeerTCP) -> void: 53 | var msg = PackedByteArray() 54 | msg.append_array(ByteUtils.pack_u32(PacketType.CONNECT)) 55 | 56 | tcp.put_data(ByteUtils.pack_u32(msg.size())) 57 | tcp.put_data(msg) 58 | 59 | ## Handles the connect response 60 | ## Returns OID 61 | func _handle_connect_res(data: PackedByteArray) -> String: 62 | var oid_len = ByteUtils.unpack_u32(data, 0) 63 | var oid = data.slice(4, 4 + oid_len).get_string_from_utf8() 64 | return oid 65 | 66 | ## Sends a host request to the server 67 | func send_host(tcp: StreamPeerTCP, oid: String) -> void: 68 | var msg = PackedByteArray() 69 | msg.append_array(ByteUtils.pack_u32(PacketType.HOST)) 70 | msg.append_array(ByteUtils.pack_u32(oid.length())) 71 | msg.append_array(oid.to_utf8_buffer()) 72 | 73 | tcp.put_data(ByteUtils.pack_u32(msg.size())) 74 | tcp.put_data(msg) 75 | 76 | ## Sends a join request to the server 77 | func send_join(tcp: StreamPeerTCP, oid: String, host_oid: String) -> void: 78 | var msg = PackedByteArray() 79 | msg.append_array(ByteUtils.pack_u32(PacketType.JOIN)) 80 | 81 | msg.append_array(ByteUtils.pack_u32(oid.length())) 82 | msg.append_array(oid.to_utf8_buffer()) 83 | 84 | msg.append_array(ByteUtils.pack_u32(host_oid.length())) 85 | msg.append_array(host_oid.to_utf8_buffer()) 86 | 87 | tcp.put_data(ByteUtils.pack_u32(msg.size())) 88 | tcp.put_data(msg) 89 | 90 | ## Sends a leave room request to the server 91 | func send_leave_room(tcp: StreamPeerTCP) -> void: 92 | var msg = PackedByteArray() 93 | msg.append_array(ByteUtils.pack_u32(PacketType.LEAVE_ROOM)) 94 | 95 | tcp.put_data(ByteUtils.pack_u32(msg.size())) 96 | tcp.put_data(msg) 97 | 98 | ## Requests the peer list 99 | func req_p_list(tcp: StreamPeerTCP) -> void: 100 | var msg = PackedByteArray() 101 | msg.append_array(ByteUtils.pack_u32(PacketType.PEERLIST)) 102 | 103 | tcp.put_data(ByteUtils.pack_u32(msg.size())) 104 | tcp.put_data(msg) 105 | 106 | ## Handles receiving the peer list 107 | func _handle_peer_list(data: PackedByteArray) -> Dictionary[int, String]: 108 | var offset = 0 109 | var p_count = ByteUtils.unpack_u32(data, offset) 110 | offset += 4 111 | 112 | var nid_to_oid: Dictionary[int, String] = {} 113 | 114 | for i in range(p_count): 115 | var oid_len = ByteUtils.unpack_u32(data, offset) 116 | offset += 4 117 | var oid = data.slice(offset, offset + oid_len).get_string_from_utf8() 118 | offset += oid_len 119 | 120 | var nid = ByteUtils.unpack_u32(data, offset) 121 | offset += 4 122 | 123 | nid_to_oid[nid] = oid 124 | 125 | return nid_to_oid 126 | -------------------------------------------------------------------------------- /addons/nodetunnel/NodeTunnelPeer.gd: -------------------------------------------------------------------------------- 1 | class_name NodeTunnelPeer extends MultiplayerPeerExtension 2 | ## A relay-based multiplayer peer that connects through NodeTunnel servers 3 | ## 4 | ## Provides P2P-style multiplayer networking through a relay server without 5 | ## requiring direct connections between clients. Integrates with Godot's 6 | ## MultiplayerAPI system. 7 | 8 | ## Connection states for tracking relay connection progress 9 | enum ConnectionState { 10 | DISCONNECTED, 11 | CONNECTING, 12 | CONNECTED, 13 | HOSTING, 14 | JOINED 15 | } 16 | 17 | # Public signals 18 | 19 | ## Fires when [member connect_to_relay] succeeds. 20 | ## [br] 21 | ## [br][param online_id] The online ID from NodeTunnel 22 | signal relay_connected(online_id: String) 23 | ## Fires when [member host] succeeds. 24 | signal hosting 25 | ## Fires when [member join] succeeds. 26 | signal joined 27 | ## Fires when this peer leaves a room 28 | ## Also fires when the room host leaves and kicks this peer from the room 29 | signal room_left 30 | 31 | # Connection configuration 32 | var relay_host: String 33 | var relay_port: int 34 | var online_id: String = "" 35 | var connection_state: ConnectionState = ConnectionState.DISCONNECTED 36 | 37 | # Multiplayer peer state 38 | var unique_id: int = 0 39 | var connected_peers: Dictionary[int, bool] = {} 40 | var connection_status: MultiplayerPeer.ConnectionStatus = MultiplayerPeer.CONNECTION_CONNECTING 41 | var target_peer: int = 0 42 | var current_transfer_mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE 43 | var current_transfer_channel: int = 0 44 | 45 | # Internal networking components 46 | var _packet_manager: _PacketManager 47 | var _tcp_handler: _NodeTunnelTCP 48 | var _udp_handler: _NodeTunnelUDP 49 | 50 | # Peer management 51 | var _numeric_to_online_id: Dictionary[int, String] = {} 52 | var _peer_list_ready: bool = false 53 | var _peer_leaving_room: bool = false 54 | 55 | # Packet management 56 | var _incoming_packets: Array = [] 57 | var _udp_packet_buffer: Array = [] 58 | 59 | # Debug configuration 60 | static var debug_enabled: bool = false 61 | 62 | ## Packet data container for multiplayer system 63 | class PacketData: 64 | var data: PackedByteArray 65 | var from_peer: int 66 | var channel: int 67 | var mode: MultiplayerPeer.TransferMode 68 | 69 | func _init(p_data: PackedByteArray, p_from: int, p_channel: int = 0, p_mode: MultiplayerPeer.TransferMode = MultiplayerPeer.TRANSFER_MODE_RELIABLE): 70 | data = p_data 71 | from_peer = p_from 72 | channel = p_channel 73 | mode = p_mode 74 | 75 | func _init(): 76 | _packet_manager = _PacketManager.new() 77 | _tcp_handler = _NodeTunnelTCP.new() 78 | _udp_handler = _NodeTunnelUDP.new() 79 | _packet_manager.peer_list_res.connect(_handle_peer_list) 80 | _packet_manager.leave_room_res.connect(_handle_leave_room) 81 | 82 | # ============================================================================ 83 | # PUBLIC API 84 | # ============================================================================ 85 | 86 | ## Connect to a NodeTunnel relay server 87 | ## [br] 88 | ## [br][param host]: The relay server hostname or IP address 89 | ## [br][param port]: The TCP port for the relay server (typically 9998) 90 | func connect_to_relay(node_tunnel_address: String, node_tunnel_port: int) -> void: 91 | if connection_state != ConnectionState.DISCONNECTED: 92 | _log_warning("Already connected or connecting to relay") 93 | return 94 | 95 | connection_state = ConnectionState.CONNECTING 96 | connection_status = MultiplayerPeer.CONNECTION_CONNECTING 97 | relay_host = node_tunnel_address 98 | relay_port = node_tunnel_port 99 | 100 | _tcp_handler.tcp.connect_to_host(relay_host, relay_port) 101 | await _tcp_handler.tcp_connected 102 | 103 | _packet_manager.send_conect(_tcp_handler.tcp) 104 | online_id = await _packet_manager.connect_res 105 | 106 | _udp_handler.connect_to_relay(relay_host, 9999, online_id) 107 | _udp_handler.packet_received.connect(_handle_udp_packet) 108 | 109 | connection_state = ConnectionState.CONNECTED 110 | connection_status = MultiplayerPeer.CONNECTION_CONNECTED 111 | _log("Connected to relay with OID: " + online_id) 112 | relay_connected.emit(online_id) 113 | 114 | ## Start hosting a multiplayer session 115 | func host() -> void: 116 | if connection_state != ConnectionState.CONNECTED: 117 | _log_error("Must be connected to relay before hosting") 118 | return 119 | 120 | if !_udp_handler.connected: 121 | _log("Sending UDP Connect Request") 122 | _udp_handler.send_connect() 123 | await _udp_handler.udp_connected 124 | _log("UDP Connected") 125 | 126 | _log("Sending TCP Host Request") 127 | _packet_manager.send_host(_tcp_handler.tcp, online_id) 128 | await _packet_manager.peer_list_res 129 | _log("Peer List Received") 130 | 131 | connection_state = ConnectionState.HOSTING 132 | connection_status = MultiplayerPeer.CONNECTION_CONNECTED 133 | _log("Started hosting session") 134 | hosting.emit() 135 | 136 | ## Join a multiplayer session using the host's online ID 137 | ## [br] 138 | ## [br][param host_oid]: The online ID of the hosting peer 139 | func join(host_oid: String) -> void: 140 | if connection_state != ConnectionState.CONNECTED: 141 | _log_error("Must be connected to relay before joining") 142 | return 143 | 144 | if host_oid.is_empty(): 145 | _log_error("Host OID cannot be empty") 146 | return 147 | 148 | _udp_handler.send_connect() 149 | await _udp_handler.udp_connected 150 | 151 | _packet_manager.send_join(_tcp_handler.tcp, online_id, host_oid) 152 | await _packet_manager.peer_list_res 153 | 154 | connection_state = ConnectionState.JOINED 155 | connection_status = MultiplayerPeer.CONNECTION_CONNECTED 156 | _log("Joined session with host: " + host_oid) 157 | joined.emit() 158 | 159 | func leave_room() -> void: 160 | if connection_state != ConnectionState.JOINED && connection_state != ConnectionState.HOSTING: 161 | _log_error("Must be in a room before attempting to leave!") 162 | return 163 | 164 | _peer_leaving_room = true 165 | 166 | _packet_manager.send_leave_room(_tcp_handler.tcp) 167 | 168 | ## Disconnect from the relay server and clean up all connections 169 | func disconnect_from_relay() -> void: 170 | _reset_connection() 171 | online_id = "" 172 | _log("Disconnected from relay") 173 | 174 | # ============================================================================ 175 | # PRIVATE NETWORKING METHODS 176 | # ============================================================================ 177 | 178 | ## WARNING: Internal NodeTunnel Code 179 | ## [b]Do not call this method directly![/b] 180 | func _send_relay_data(to_peer_numeric: int, data: PackedByteArray) -> void: 181 | var to_oid = "0" # Broadcast by default 182 | 183 | if to_peer_numeric != 0 && _numeric_to_online_id.has(to_peer_numeric): 184 | to_oid = _numeric_to_online_id[to_peer_numeric] 185 | 186 | _udp_handler.send_packet(to_oid, data) 187 | 188 | ## WARNING: Internal NodeTunnel Code 189 | ## [b]Do not call this method directly![/b] 190 | func _handle_udp_packet(from_oid: String, data: PackedByteArray) -> void: 191 | if !_peer_list_ready || _numeric_to_online_id.is_empty(): 192 | _udp_packet_buffer.append({"from_oid": from_oid, "data": data}) 193 | return 194 | 195 | _process_udp_packet(from_oid, data) 196 | 197 | ## WARNING: Internal NodeTunnel Code 198 | ## [b]Do not call this method directly![/b] 199 | func _process_udp_packet(from_oid: String, data: PackedByteArray) -> void: 200 | var from_nid = _get_numeric_id_for_online_id(from_oid) 201 | 202 | if from_nid == 0: 203 | return 204 | if from_nid == unique_id: 205 | return 206 | 207 | var packet_data = PacketData.new(data, from_nid, current_transfer_channel, current_transfer_mode) 208 | _incoming_packets.append(packet_data) 209 | 210 | ## WARNING: Internal NodeTunnel Code 211 | ## [b]Do not call this method directly![/b] 212 | func _handle_peer_list(numeric_to_online_id: Dictionary[int, String]) -> void: 213 | # Store old state before clearing 214 | var old_connected_peers = connected_peers.duplicate() 215 | 216 | # Emit disconnection signals for peers that are no longer in the list 217 | for old_nid in old_connected_peers.keys(): 218 | if !numeric_to_online_id.has(old_nid): 219 | peer_disconnected.emit(old_nid) 220 | 221 | # Update peer mappings 222 | connected_peers.clear() 223 | _numeric_to_online_id = numeric_to_online_id 224 | 225 | # Process new peer connections 226 | for nid in numeric_to_online_id.keys(): 227 | var was_connected = old_connected_peers.has(nid) 228 | connected_peers[nid] = true 229 | 230 | if numeric_to_online_id[nid] == online_id: 231 | unique_id = nid 232 | elif !was_connected: 233 | peer_connected.emit(nid) 234 | 235 | _peer_list_ready = true 236 | 237 | # Process any buffered UDP packets 238 | for packet in _udp_packet_buffer: 239 | _process_udp_packet(packet.from_oid, packet.data) 240 | _udp_packet_buffer.clear() 241 | 242 | _log("Updated peer list: " + str(connected_peers)) 243 | 244 | func _handle_leave_room() -> void: 245 | for p in connected_peers: 246 | peer_disconnected.emit(p) 247 | 248 | connection_state = ConnectionState.CONNECTED 249 | connection_status = MultiplayerPeer.CONNECTION_CONNECTING 250 | connected_peers.clear() 251 | _incoming_packets.clear() 252 | _udp_packet_buffer.clear() 253 | _numeric_to_online_id.clear() 254 | unique_id = 0 255 | _peer_list_ready = false 256 | 257 | room_left.emit() 258 | 259 | # ============================================================================ 260 | # UTILITY METHODS 261 | # ============================================================================ 262 | 263 | ## WARNING: Internal NodeTunnel Code 264 | ## [b]Do not call this method directly![/b] 265 | func _get_numeric_id_for_online_id(oid: String) -> int: 266 | for nid in _numeric_to_online_id.keys(): 267 | if _numeric_to_online_id[nid] == oid: 268 | return nid 269 | return 0 270 | 271 | ## WARNING: Internal NodeTunnel Code 272 | ## [b]Do not call this method directly![/b] 273 | func _reset_connection() -> void: 274 | connection_state = ConnectionState.DISCONNECTED 275 | connection_status = MultiplayerPeer.CONNECTION_DISCONNECTED 276 | connected_peers.clear() 277 | _incoming_packets.clear() 278 | _udp_packet_buffer.clear() 279 | _numeric_to_online_id.clear() 280 | unique_id = 0 281 | _peer_list_ready = false 282 | 283 | ## WARNING: Internal NodeTunnel Code 284 | ## [b]Do not call this method directly![/b] 285 | static func _log(message: String) -> void: 286 | if debug_enabled: 287 | print("[NodeTunnel] " + message) 288 | 289 | ## WARNING: Internal NodeTunnel Code 290 | ## [b]Do not call this method directly![/b] 291 | static func _log_warning(message: String) -> void: 292 | if debug_enabled: 293 | push_warning("[NodeTunnel] " + message) 294 | 295 | ## WARNING: Internal NodeTunnel Code 296 | ## [b]Do not call this method directly![/b] 297 | static func _log_error(message: String) -> void: 298 | push_error("[NodeTunnel] " + message) 299 | 300 | # ============================================================================ 301 | # MULTIPLAYER PEER EXTENSION IMPLEMENTATION 302 | # ============================================================================ 303 | 304 | ## WARNING: Called automatically by MultiplayerAPI 305 | ## [b]Do not call this method directly![/b] 306 | func _get_connection_status() -> MultiplayerPeer.ConnectionStatus: 307 | return connection_status 308 | 309 | ## WARNING: Called automatically by MultiplayerAPI 310 | ## [b]Do not call this method directly![/b] 311 | func _get_unique_id() -> int: 312 | return unique_id 313 | 314 | ## WARNING: Called automatically by MultiplayerAPI 315 | ## [b]Do not call this method directly![/b] 316 | func _is_server() -> bool: 317 | return unique_id == 1 318 | 319 | ## WARNING: Called automatically by MultiplayerAPI 320 | ## [b]Do not call this method directly![/b] 321 | func _is_server_relay_supported() -> bool: 322 | return true 323 | 324 | ## WARNING: Called automatically by MultiplayerAPI 325 | ## [b]Do not call this method directly![/b] 326 | func _get_packet_script() -> PackedByteArray: 327 | if _incoming_packets.is_empty(): 328 | return PackedByteArray() 329 | 330 | var packet_data = _incoming_packets.pop_front() 331 | return packet_data.data 332 | 333 | ## WARNING: Called automatically by MultiplayerAPI 334 | ## [b]Do not call this method directly![/b] 335 | func _put_packet_script(p_buffer: PackedByteArray) -> Error: 336 | if connection_state != ConnectionState.HOSTING and connection_state != ConnectionState.JOINED: 337 | return ERR_UNCONFIGURED 338 | 339 | _send_relay_data(target_peer, p_buffer) 340 | return OK 341 | 342 | ## WARNING: Called automatically by MultiplayerAPI 343 | ## [b]Do not call this method directly![/b] 344 | func _get_available_packet_count() -> int: 345 | return _incoming_packets.size() 346 | 347 | ## WARNING: Called automatically by MultiplayerAPI 348 | ## [b]Do not call this method directly![/b] 349 | func _get_max_packet_size() -> int: 350 | return 1400 # Safe UDP packet size 351 | 352 | ## WARNING: Called automatically by MultiplayerAPI 353 | ## [b]Do not call this method directly![/b] 354 | func _get_packet_peer() -> int: 355 | if _incoming_packets.is_empty(): 356 | return 0 357 | return _incoming_packets[0].from_peer 358 | 359 | ## WARNING: Called automatically by MultiplayerAPI 360 | ## [b]Do not call this method directly![/b] 361 | func _get_packet_channel() -> int: 362 | if _incoming_packets.is_empty(): 363 | return 0 364 | return _incoming_packets[0].channel 365 | 366 | ## WARNING: Called automatically by MultiplayerAPI 367 | ## [b]Do not call this method directly![/b] 368 | func _get_packet_mode() -> MultiplayerPeer.TransferMode: 369 | if _incoming_packets.is_empty(): 370 | return MultiplayerPeer.TRANSFER_MODE_RELIABLE 371 | return _incoming_packets[0].mode 372 | 373 | ## WARNING: Called automatically by MultiplayerAPI 374 | ## [b]Do not call this method directly![/b] 375 | func _set_target_peer(p_peer: int) -> void: 376 | target_peer = p_peer 377 | 378 | ## WARNING: Called automatically by MultiplayerAPI 379 | ## [b]Do not call this method directly![/b] 380 | func _set_transfer_channel(p_channel: int) -> void: 381 | current_transfer_channel = p_channel 382 | 383 | ## WARNING: Called automatically by MultiplayerAPI 384 | ## [b]Do not call this method directly![/b] 385 | func _get_transfer_channel() -> int: 386 | return current_transfer_channel 387 | 388 | ## WARNING: Called automatically by MultiplayerAPI 389 | ## [b]Do not call this method directly![/b] 390 | func _set_transfer_mode(p_mode: MultiplayerPeer.TransferMode) -> void: 391 | current_transfer_mode = p_mode 392 | 393 | ## WARNING: Called automatically by MultiplayerAPI 394 | ## [b]Do not call this method directly![/b] 395 | func _get_transfer_mode() -> MultiplayerPeer.TransferMode: 396 | return current_transfer_mode 397 | 398 | ## WARNING: Called automatically by MultiplayerAPI 399 | ## [b]Do not call this method directly![/b] 400 | func _close() -> void: 401 | disconnect_from_relay() 402 | 403 | ## WARNING: Called automatically by MultiplayerAPI 404 | ## [b]Do not call this method directly![/b] 405 | func _disconnect_peer(_p_peer: int, _p_force: bool = false) -> void: 406 | # Cannot directly disconnect peers in relay mode 407 | pass 408 | 409 | ## WARNING: Called automatically by MultiplayerAPI 410 | ## [b]Do not call this method directly![/b] 411 | func _is_refusing_new_connections() -> bool: 412 | return refuse_new_connections 413 | 414 | ## WARNING: Called automatically by MultiplayerAPI 415 | ## [b]Do not call this method directly![/b] 416 | func _set_refuse_new_connections(p_enable: bool) -> void: 417 | refuse_new_connections = p_enable 418 | 419 | ## WARNING: Called automatically by MultiplayerAPI 420 | ## [b]Do not call this method directly![/b] 421 | func _poll() -> void: 422 | _tcp_handler.poll(_packet_manager) 423 | _udp_handler.poll() 424 | --------------------------------------------------------------------------------