diff --git a/AM2R.project.gmx b/AM2R.project.gmx index 4cd7065..2197e2b 100644 --- a/AM2R.project.gmx +++ b/AM2R.project.gmx @@ -5687,6 +5687,29 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -5741,6 +5764,7 @@ + @@ -5927,6 +5951,7 @@ + @@ -6223,15 +6248,8 @@ - - - - - - - diff --git a/objects/oBomb.object.gmx b/objects/oBomb.object.gmx index 2b73427..cb8124b 100644 --- a/objects/oBomb.object.gmx +++ b/objects/oBomb.object.gmx @@ -51,7 +51,7 @@ if(instance_exists(oClient)){ buffer_write(bombBuffer, buffer_s16, x); buffer_write(bombBuffer, buffer_s16, y); buffer_write(bombBuffer, buffer_u8, global.sax); - var result = network_send_packet(oClient.socket, bombBuffer, buffer_tell(bombBuffer)); + var result = udp_send_unreliable(oClient.socket, oClient.server_ip, oClient.server_port, bombBuffer, buffer_tell(bombBuffer)); buffer_delete(bombBuffer); } } diff --git a/objects/oClient.object.gmx b/objects/oClient.object.gmx index 2708374..91f9ea1 100644 --- a/objects/oClient.object.gmx +++ b/objects/oClient.object.gmx @@ -74,8 +74,6 @@ syncedDifficulty = 1; elm = 0; state = undefined; seed = undefined; -var type; -type = network_socket_tcp; syncedDifficulty = global.difficulty; elm = oControl.mod_monstersextremecheck; @@ -87,8 +85,16 @@ port = ini_read_real("ModSettings", "port", 64198); reactorsequence = string_lower(ini_read_string("ModSettings", "reactorsequence", "synced")); preferredcolor = string_lower(ini_read_string("ModSettings", "preferredcolor", "random")); showName = string_lower(ini_read_string("ModSettings", "showname", "enabled")); +// NAT traversal settings +var nat_enabled_str = ini_read_string("ModSettings", "use_nat_traversal", "disabled"); +use_nat_traversal = (nat_enabled_str == "enabled"); +mediation_server_ip = ini_read_string("ModSettings", "mediation_server_ip", "127.0.0.1"); +mediation_server_port = ini_read_real("ModSettings", "mediation_server_port", 6510); ini_close(); +// Initialize NAT constants +nat_constants(); + var nameLength = string_length(name); if(nameLength > 15){ name = string_copy(name, 1, 15); @@ -103,8 +109,46 @@ if(string_length(name) == 0){ instance_destroy(); } -socket = network_create_socket(type); -isConnected = network_connect(socket, ipaddress, port); +// Use UDP socket instead of TCP - proxy handles TCP connection +// Use a specific local port so we can report it to the mediation server +var local_udp_port = irandom_range(49152, 65535); +socket = network_create_socket_ext(network_socket_udp, local_udp_port); +server_ip = ipaddress; +server_port = port; + +// Initialize NAT traversal if enabled +if (use_nat_traversal) { + nat_init_client(); + nat_local_port = local_udp_port; // Store our actual local port for NAT detection + nat_connect_mediation(); + isConnected = -1; // Will be set to 1 when NAT connection completes + nat_keepalive_timer = 0; + NAT_KEEPALIVE_INTERVAL = 150; // Send keepalive every 5 seconds (150 steps at 30fps) +} else { + // UDP doesn't have a "connect" - we just start sending + isConnected = 1; // Assume connected, will be confirmed by server response +} + +// Fragment reassembly data structures +fragment_buffers = ds_map_create(); // fragment_id -> ds_map of fragments +fragment_timestamps = ds_map_create(); // fragment_id -> creation time +FRAGMENT_TIMEOUT = 5000; // 5 seconds - discard incomplete fragments after this +UDP_MAX_FRAGMENT_SIZE = 1400; + +// Reliable UDP with ACK +received_sequences = ds_map_create(); // Track received sequence numbers to detect duplicates +SEQUENCE_CACHE_SIZE = 1000; // How many sequence numbers to remember +next_sequence_number = 0; // For client-side reliable sends +pending_acks = ds_map_create(); // Track our sent reliable packets waiting for ACK + +// Adaptive RTT tracking for retransmit timeout +smoothed_rtt = 100; // Initial estimate: 100ms +rtt_variance = 50; // Initial variance +BASE_RETRANSMIT_TIMEOUT = 200; // Minimum timeout +MAX_RETRANSMIT_TIMEOUT = 3000; // Maximum timeout (3 seconds) +MAX_RETRANSMITS = 5; + +show_debug_message("Client: Using UDP to connect to proxy at " + server_ip + ":" + string(server_port)); switch(preferredcolor){ case "green": @@ -290,7 +334,9 @@ beginReceiveTimer = 300; buffer_write(buffer, buffer_u8, 254); buffer_write(buffer, buffer_u8, global.clientID); buffer_write(buffer, buffer_u8, global.sax); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); +} else { + popup_text("Failed to connect"); } if(!noName){ @@ -319,6 +365,43 @@ oControl.alarm[10] = 90; if(banned) popup_text("Banned from server"); if(kicked) popup_text("Kicked from server"); +// Clean up NAT traversal resources +if (use_nat_traversal) { + nat_cleanup(); +} + +// Clean up fragment reassembly buffers +var frag_id = ds_map_find_first(fragment_buffers); +while (!is_undefined(frag_id)) { + var frag_map = ds_map_find_value(fragment_buffers, frag_id); + var frag_idx = ds_map_find_first(frag_map); + while (!is_undefined(frag_idx)) { + var frag_buffer = ds_map_find_value(frag_map, frag_idx); + if (is_real(frag_buffer) && buffer_exists(frag_buffer)) { + buffer_delete(frag_buffer); + } + frag_idx = ds_map_find_next(frag_map, frag_idx); + } + ds_map_destroy(frag_map); + frag_id = ds_map_find_next(fragment_buffers, frag_id); +} +ds_map_destroy(fragment_buffers); +ds_map_destroy(fragment_timestamps); +ds_map_destroy(received_sequences); + +// Clean up pending ACKs +if (!is_undefined(pending_acks)) { + var seq_num = ds_map_find_first(pending_acks); + while (!is_undefined(seq_num)) { + var ack_data = ds_map_find_value(pending_acks, seq_num); + var pkt_buffer = ds_map_find_value(ack_data, "buffer"); + buffer_delete(pkt_buffer); + ds_map_destroy(ack_data); + seq_num = ds_map_find_next(pending_acks, seq_num); + } + ds_map_destroy(pending_acks); +} + if(global.sax && global.saxmode){ global.sax = 0; load_character_vars(); @@ -375,7 +458,7 @@ buffer_write(buffer, buffer_s16, posX); buffer_write(buffer, buffer_s16, posY); buffer_write(buffer, buffer_u8, global.sax); buffer_write(buffer, buffer_u8, global.spectator); -var result = network_send_packet(socket, buffer, buffer_tell(buffer)); +var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); alarm[11] = 20; @@ -497,7 +580,7 @@ if(sameRoom){ buffer_write(buffer, buffer_u8, global.mosaic); buffer_write(buffer, buffer_u8, global.reform); buffer_write(buffer, buffer_u8, oCharacter.visible); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); if(global.roomPrev != room){ global.roomPrev = room; } @@ -547,7 +630,7 @@ if(mosaicEffect){ buffer_write(buffer, buffer_u8, 12); buffer_write(buffer, buffer_u8, global.maxsmissiles); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); maxsmissilesChanged = true; } @@ -598,7 +681,7 @@ if(maxsmissilesChanged){ buffer_write(buffer, buffer_u8, 11); buffer_write(buffer, buffer_u16, global.maxmissiles); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); maxmissilesChanged = true; } @@ -649,7 +732,7 @@ if(maxmissilesChanged){ buffer_write(buffer, buffer_u8, 10); buffer_write(buffer, buffer_u8, global.itemstaken); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); itemstakenChanged = true; } @@ -738,7 +821,7 @@ if(mapChanged){ } buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(!mapChanged){ @@ -805,7 +888,7 @@ if(eventChanged){ buffer_write(buffer, buffer_u8, 8); buffer_write(buffer, buffer_string, strict_compress(ds_list_write(global.onlineEvent))); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(!eventChanged){ @@ -866,7 +949,7 @@ if(metdeadChanged){ buffer_write(buffer, buffer_u8, 7); buffer_write(buffer, buffer_string, strict_compress(ds_list_write(global.onlineMetdead))); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(!metdeadChanged){ @@ -927,7 +1010,7 @@ if(itemChanged){ buffer_write(buffer, buffer_u8, 6); buffer_write(buffer, buffer_string, strict_compress(ds_list_write(global.onlineItem))); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(!itemChanged){ @@ -977,7 +1060,7 @@ if(itemChanged){ buffer_write(buffer, buffer_u8, 4); buffer_write(buffer, buffer_s8, global.monstersleft); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); metroidCountChanged = true; } @@ -998,7 +1081,7 @@ if(global.monstersareaPrev != global.monstersarea){ buffer_write(buffer, buffer_u8, 5); buffer_write(buffer, buffer_s8, global.monstersarea); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); metroidCountChanged = true; } @@ -1171,7 +1254,7 @@ if(canSend){ buffer_write(buffer, buffer_string, strict_compress(ds_grid_write(global.onlineSend))); buffer_write(buffer, buffer_u8, global.clientID); buffer_write(buffer, buffer_u32, global.packetID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); show_debug_message("packetID = " + string(global.packetID)); global.packetID++; ds_grid_clear(global.onlineSend, -1); @@ -1220,7 +1303,7 @@ if(instance_exists(oQueen) && instance_exists(oQueenHead) && !is buffer_write(buffer, buffer_u8, global.clientID); buffer_write(buffer, buffer_s8, oQueen.phase); buffer_write(buffer, buffer_s8, oQueen.state); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } } @@ -1257,7 +1340,91 @@ if(itemCount != global.itemstaken){ 1 - if(!connected){ + // Handle NAT traversal hole punching and keepalive +if (use_nat_traversal) { + nat_step_hole_punching(); + + // Send keepalive to mediation server to prevent timeout + nat_keepalive_timer++; + if (nat_keepalive_timer >= NAT_KEEPALIVE_INTERVAL) { + nat_keepalive_timer = 0; + if (mediation_connected && mediation_socket != -1) { + var params = ds_map_create(); + nat_send_tcp_message(mediation_socket, MSG_KEEP_ALIVE, params); + ds_map_destroy(params); + } + } +} + +// Handle retransmissions for client-side reliable sends +var current = current_time; + +// Calculate adaptive retransmit timeout: RTO = smoothed_rtt + (4 * rtt_variance) +var retransmit_timeout = clamp(smoothed_rtt + (4 * rtt_variance), BASE_RETRANSMIT_TIMEOUT, MAX_RETRANSMIT_TIMEOUT); + +if (!is_undefined(pending_acks)) { + var seq_num = ds_map_find_first(pending_acks); + while (!is_undefined(seq_num)) { + var next_seq = ds_map_find_next(pending_acks, seq_num); + var ack_data = ds_map_find_value(pending_acks, seq_num); + var send_time = ds_map_find_value(ack_data, "send_time"); + var retransmits = ds_map_find_value(ack_data, "retransmits"); + + if (current - send_time > retransmit_timeout) { + if (retransmits < MAX_RETRANSMITS) { + var pkt_buffer = ds_map_find_value(ack_data, "buffer"); + var pkt_size = ds_map_find_value(ack_data, "size"); + var pkt_ip = ds_map_find_value(ack_data, "ip"); + var pkt_port = ds_map_find_value(ack_data, "port"); + + network_send_udp(socket, pkt_ip, pkt_port, pkt_buffer, pkt_size); + ds_map_replace(ack_data, "send_time", current); + ds_map_replace(ack_data, "retransmits", retransmits + 1); + } else { + var pkt_buffer = ds_map_find_value(ack_data, "buffer"); + buffer_delete(pkt_buffer); + ds_map_destroy(ack_data); + ds_map_delete(pending_acks, seq_num); + } + } + seq_num = next_seq; + } +} + +// Clean up old incomplete fragments to prevent memory leaks +var frag_id = ds_map_find_first(fragment_timestamps); +while (!is_undefined(frag_id)) { + var next_id = ds_map_find_next(fragment_timestamps, frag_id); + var timestamp = ds_map_find_value(fragment_timestamps, frag_id); + + // If fragment is older than timeout, clean it up + if (current - timestamp > FRAGMENT_TIMEOUT) { + show_debug_message("Client: Fragment timeout for ID " + string(frag_id) + " - cleaning up incomplete fragments"); + + // Clean up the fragment buffers + if (ds_map_exists(fragment_buffers, frag_id)) { + var frag_map = ds_map_find_value(fragment_buffers, frag_id); + var total = ds_map_find_value(frag_map, "total"); + + // Delete any stored fragment buffers + for (var i = 0; i < total; i++) { + if (ds_map_exists(frag_map, i)) { + var frag_buf = ds_map_find_value(frag_map, i); + buffer_delete(frag_buf); + } + } + + ds_map_destroy(frag_map); + ds_map_delete(fragment_buffers, frag_id); + } + + ds_map_delete(fragment_timestamps, frag_id); + } + + frag_id = next_id; +} + +if(!connected){ if(isConnected >= 0){ connected = true; popup_text("Connected"); @@ -1277,9 +1444,11 @@ if(itemCount != global.itemstaken){ buffer_write(buffer, buffer_u8, 1); buffer_write(buffer, buffer_string, name + "," + global.multitroid_version); buffer_write(buffer, buffer_u8, global.sax); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); instance_create(x, y, oNametag); - } else { + } else if (!use_nat_traversal) { + // Only fail immediately if not using NAT traversal + // With NAT, isConnected starts at -1 and will be set to 1 when connection completes instance_destroy(); popup_text("Failed to connect"); } @@ -1468,7 +1637,7 @@ if(maxpbombsTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 13); buffer_write(buffer, buffer_u8, global.maxpbombs); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); maxpbombsChanged = true; } @@ -1500,7 +1669,7 @@ if(maxhealthTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 14); buffer_write(buffer, buffer_u16, global.maxhealth); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); maxhealthChanged = true; } @@ -1532,7 +1701,7 @@ if(etanksTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 15); buffer_write(buffer, buffer_u8, global.etanks); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); etanksChanged = true; } @@ -1564,7 +1733,7 @@ if(mtanksTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 16); buffer_write(buffer, buffer_u8, global.mtanks); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); mtanksChanged = true; } @@ -1596,7 +1765,7 @@ if(stanksTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 17); buffer_write(buffer, buffer_u8, global.stanks); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); stanksChanged = true; } @@ -1628,7 +1797,7 @@ if(ptanksTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 18); buffer_write(buffer, buffer_u8, global.ptanks); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); ptanksChanged = true; } @@ -1660,7 +1829,7 @@ if(healthTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 25); buffer_write(buffer, buffer_s16, global.playerhealth); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); healthTimer = true; } @@ -1692,7 +1861,7 @@ if(missilesTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 26); buffer_write(buffer, buffer_s16, global.missiles); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); missilesTimer = true; } @@ -1724,7 +1893,7 @@ if(smissilesTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 27); buffer_write(buffer, buffer_s16, global.smissiles); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); smissilesTimer = true; } @@ -1756,7 +1925,7 @@ if(pbombsTimer == 0 && connected){ buffer_write(buffer, buffer_u8, 28); buffer_write(buffer, buffer_s16, global.pbombs); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); pbombsTimer = true; } @@ -1800,7 +1969,7 @@ if(pingTimer <= 0){ buffer_write(buffer, buffer_u8, 103); buffer_write(buffer, buffer_u32, current_time); buffer_write(buffer, buffer_u16, global.ping); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(resetTimer == 0){ @@ -1818,7 +1987,7 @@ if(resetTimer == 0){ buffer_write(buffer, buffer_s32, bufferSize); buffer_write(buffer, buffer_u8, 20); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(currentItemsTimer == 0 && connected){ @@ -1857,10 +2026,11 @@ if(currentItemsTimer == 0 && connected){ buffer_write(buffer, buffer_u8, global.pbeam); buffer_write(buffer, buffer_u8, global.sbeam); buffer_write(buffer, buffer_u8, global.cbeam); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(beginReceiveTimer == 0 && connected){ + show_debug_message("Starting all alarms - position sync will now begin"); alarm[0] = 5; alarm[2] = 5; alarm[3] = 5; @@ -2099,12 +2269,224 @@ if(connected && room == rm_a1h01 && global.event[51] == 0 && if(!global.acceptPacket) exit; var type_event = ds_map_find_value(async_load, "type"); +var event_socket = ds_map_find_value(async_load, "id"); + +// Handle NAT mediation server TCP messages +if (use_nat_traversal && event_socket == mediation_socket && type_event == network_type_data) { + var _buffer = ds_map_find_value(async_load, "buffer"); + buffer_seek(_buffer, buffer_seek_start, 0); + var data = buffer_read(_buffer, buffer_text); + + // Append to TCP buffer (messages may be fragmented) + nat_tcp_buffer += data; + + // Try to parse complete JSON messages + var brace_count = 0; + var start_index = 0; + var i = 1; + while (i <= string_length(nat_tcp_buffer)) { + var ch = string_char_at(nat_tcp_buffer, i); + if (ch == "{") { + if (brace_count == 0) start_index = i; + brace_count++; + } else if (ch == "}") { + brace_count--; + if (brace_count == 0 && start_index > 0) { + var json_str = string_copy(nat_tcp_buffer, start_index, i - start_index + 1); + var msg = json_parse_nat_message(json_str); + if (msg != noone) { + nat_handle_tcp_message(msg); + ds_map_destroy(msg); + } + nat_tcp_buffer = string_delete(nat_tcp_buffer, 1, i); + i = 0; + } + } + i++; + } + exit; +} + +// Handle NAT connection established event +if (use_nat_traversal && event_socket == mediation_socket && type_event == network_type_connect) { + show_debug_message("NAT: TCP connected to mediation server"); + exit; +} switch(type_event){ case network_type_data: var _buffer = ds_map_find_value(async_load, "buffer"); var bufferSize = buffer_get_size(_buffer); buffer_seek(_buffer, buffer_seek_start, 0); + var needs_cleanup = false; + + // Debug: Log ALL incoming UDP packets + var recv_socket = ds_map_find_value(async_load, "id"); + var recv_ip = ds_map_find_value(async_load, "ip"); + var recv_port = ds_map_find_value(async_load, "port"); + show_debug_message("Client: Received UDP on socket " + string(recv_socket) + " from " + recv_ip + ":" + string(recv_port) + ", size=" + string(bufferSize)); + + // Check for NAT hole punch packets during hole punching phase + if (use_nat_traversal) { + var sender_ip = ds_map_find_value(async_load, "ip"); + var sender_port = ds_map_find_value(async_load, "port"); + var receiving_socket = ds_map_find_value(async_load, "id"); + + // Debug: Log all incoming UDP when using NAT + if (nat_state == NAT_STATE_CONNECTED) { + show_debug_message("NAT: Received UDP after connection: from " + sender_ip + ":" + string(sender_port) + " on socket " + string(receiving_socket)); + } + + if (nat_state == NAT_STATE_PUNCHING) { + if (nat_handle_udp_message(_buffer, sender_ip, sender_port, receiving_socket)) { + exit; // Was a NAT message, don't process further + } + } + buffer_seek(_buffer, buffer_seek_start, 0); // Reset for normal processing + } + + // Check if this is an ACK packet from server (starts with 255 marker) + if (bufferSize >= 5) { + if (buffer_peek(_buffer, 0, buffer_u8) == 255) { + buffer_seek(_buffer, buffer_seek_start, 0); + buffer_read(_buffer, buffer_u8); + var acked_seq = buffer_read(_buffer, buffer_u32); + + if (ds_map_exists(pending_acks, acked_seq)) { + var ack_data = ds_map_find_value(pending_acks, acked_seq); + + // Measure RTT and update smoothed estimates (RFC 6298) + var send_time = ds_map_find_value(ack_data, "send_time"); + var measured_rtt = current_time - send_time; + + // First measurement + if (smoothed_rtt == 100) { + smoothed_rtt = measured_rtt; + rtt_variance = measured_rtt / 2; + } else { + // Update using exponential weighted moving average + var alpha = 0.125; // Weight for new sample + var beta = 0.25; // Weight for variance + + rtt_variance = (1 - beta) * rtt_variance + beta * abs(smoothed_rtt - measured_rtt); + smoothed_rtt = (1 - alpha) * smoothed_rtt + alpha * measured_rtt; + } + + var ack_buffer = ds_map_find_value(ack_data, "buffer"); + buffer_delete(ack_buffer); + ds_map_destroy(ack_data); + ds_map_delete(pending_acks, acked_seq); + } + exit; + } + } + + // Check if this is a reliable packet (starts with 254 marker) + if (bufferSize >= 5 && buffer_peek(_buffer, 0, buffer_u8) == 254) { + // Reliable packet format: [0xFE][seq_num (u32)][original_packet] + buffer_seek(_buffer, buffer_seek_start, 0); + buffer_read(_buffer, buffer_u8); // Skip marker + var seq_num = buffer_read(_buffer, buffer_u32); + + // Send ACK back to server + var ack_buffer = buffer_create(5, buffer_fixed, 1); + buffer_write(ack_buffer, buffer_u8, 255); // ACK marker + buffer_write(ack_buffer, buffer_u32, seq_num); + network_send_udp(socket, server_ip, server_port, ack_buffer, 5); + buffer_delete(ack_buffer); + + // Check if we've already processed this sequence number (duplicate) + if (ds_map_exists(received_sequences, seq_num)) { + exit; // Duplicate packet, ignore + } + + // Mark as received and trim cache if needed + ds_map_add(received_sequences, seq_num, current_time); + if (ds_map_size(received_sequences) > SEQUENCE_CACHE_SIZE) { + // Remove oldest entry + var oldest_seq = ds_map_find_first(received_sequences); + ds_map_delete(received_sequences, oldest_seq); + } + + // Extract original packet (skip the 5-byte reliable header) + var original_size = bufferSize - 5; + var original_buffer = buffer_create(original_size, buffer_fixed, 1); + buffer_copy(_buffer, 5, original_size, original_buffer, 0); + _buffer = original_buffer; + bufferSize = original_size; + buffer_seek(_buffer, buffer_seek_start, 0); + needs_cleanup = true; + } + + // Check if this is a fragmented packet (no size prefix, starts with fragment header) + var first_bytes = buffer_peek(_buffer, 0, buffer_u16); + var has_size_prefix = (bufferSize > 4 && buffer_peek(_buffer, 0, buffer_s32) + 4 == bufferSize); + var UDP_MAX_FRAGMENT_SIZE = 1200; // Must match server setting + + if (!has_size_prefix && bufferSize >= 4) { + // This might be a fragment - check fragment header + var fragment_id = buffer_read(_buffer, buffer_u16); + var fragment_index = buffer_read(_buffer, buffer_u8); + var total_fragments = buffer_read(_buffer, buffer_u8); + + // If total_fragments looks reasonable (1-255), treat as fragment + if (total_fragments > 0 && total_fragments < 256 && fragment_index < total_fragments) { + // Get or create fragment map for this packet + var frag_map; + if (ds_map_exists(fragment_buffers, fragment_id)) { + frag_map = ds_map_find_value(fragment_buffers, fragment_id); + } else { + frag_map = ds_map_create(); + ds_map_add(fragment_buffers, fragment_id, frag_map); + ds_map_add(frag_map, "total", total_fragments); + ds_map_add(frag_map, "received", 0); + ds_map_add(fragment_timestamps, fragment_id, current_time); + } + + // Store this fragment + var fragment_data_size = bufferSize - 4; + var fragment_buffer = buffer_create(fragment_data_size, buffer_fixed, 1); + buffer_copy(_buffer, 4, fragment_data_size, fragment_buffer, 0); + ds_map_add(frag_map, fragment_index, fragment_buffer); + ds_map_replace(frag_map, "received", ds_map_find_value(frag_map, "received") + 1); + + // Check if we have all fragments + if (ds_map_find_value(frag_map, "received") == total_fragments) { + // Reassemble the complete packet + var max_payload = UDP_MAX_FRAGMENT_SIZE - 4; + var total_size = 0; + for (var i = 0; i < total_fragments; i++) { + var frag_buf = ds_map_find_value(frag_map, i); + total_size += buffer_get_size(frag_buf); + } + + // Reassemble without adding size prefix - it's already in the fragment data + var reassembled = buffer_create(total_size, buffer_fixed, 1); + var write_pos = 0; + for (var i = 0; i < total_fragments; i++) { + var frag_buf = ds_map_find_value(frag_map, i); + var frag_size = buffer_get_size(frag_buf); + buffer_copy(frag_buf, 0, frag_size, reassembled, write_pos); + write_pos += frag_size; + buffer_delete(frag_buf); + } + + ds_map_destroy(frag_map); + ds_map_delete(fragment_buffers, fragment_id); + ds_map_delete(fragment_timestamps, fragment_id); + + // Now process the reassembled packet as normal + _buffer = reassembled; + bufferSize = total_size; + buffer_seek(_buffer, buffer_seek_start, 0); + } else { + // Waiting for more fragments + exit; + } + } + } + + // Normal packet processing (original code) var bufferSizePacket = buffer_read(_buffer, buffer_s32); //show_debug_message(string(bufferSize) + " " + string(bufferSizePacket)); if(!is_real(bufferSizePacket)) exit; @@ -2563,6 +2945,7 @@ switch(type_event){ case 0: global.clientID = buffer_read(_buffer, buffer_u8); show_debug_message(string(msgid) + " " + string(global.clientID)); + popup_text("Received ID " + string(global.clientID)); global.receiving = true; buffer_delete(buffer); var size, type, alignment; @@ -2582,7 +2965,7 @@ switch(type_event){ buffer_write(buffer, buffer_u8, global.clientID); buffer_write(buffer, buffer_u8, preferredcolor); buffer_write(buffer, buffer_u8, global.sax); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); break; case 1: ds_grid_read(global.onlineReceived, strict_decompress(buffer_read(_buffer, buffer_string))); @@ -3364,7 +3747,7 @@ switch(type_event){ buffer_write(buffer, buffer_u8, global.smissiles); buffer_write(buffer, buffer_u8, global.pbombs); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } } else { global.playerhealthPrev = playerhealth; @@ -3418,7 +3801,7 @@ switch(type_event){ buffer_write(buffer, buffer_u8, global.smissiles); buffer_write(buffer, buffer_u8, global.pbombs); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } } else { global.missilesPrev = missiles; @@ -3472,7 +3855,7 @@ switch(type_event){ buffer_write(buffer, buffer_u8, global.smissiles); buffer_write(buffer, buffer_u8, global.pbombs); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } } else { global.smissilesPrev = smissiles; @@ -3526,7 +3909,7 @@ switch(type_event){ buffer_write(buffer, buffer_u8, global.smissiles); buffer_write(buffer, buffer_u8, global.pbombs); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } } else { global.pbombsPrev = pbombs; @@ -3844,7 +4227,7 @@ switch(type_event){ for(var i=0; i<array_length_1d(global.item); i++){ buffer_write(itemBuffer, buffer_u8, global.item[i]); } - network_send_packet(socket, itemBuffer, buffer_tell(itemBuffer)); + network_send_udp(socket, server_ip, server_port, itemBuffer, buffer_tell(itemBuffer)); buffer_delete(itemBuffer); } break; @@ -3949,7 +4332,7 @@ switch(type_event){ for(var i=0; i<array_length_1d(global.event); i++){ buffer_write(eventBuffer, buffer_u8, global.event[i]); } - network_send_packet(socket, eventBuffer, buffer_tell(eventBuffer)); + network_send_udp(socket, server_ip, server_port, eventBuffer, buffer_tell(eventBuffer)); buffer_delete(eventBuffer); } break; @@ -4101,7 +4484,7 @@ switch(type_event){ for(var i=0; i<array_length_1d(global.metdead); i++){ buffer_write(metdeadBuffer, buffer_u8, global.metdead[i]); } - network_send_packet(socket, metdeadBuffer, buffer_tell(metdeadBuffer)); + network_send_udp(socket, server_ip, server_port, metdeadBuffer, buffer_tell(metdeadBuffer)); buffer_delete(metdeadBuffer); } break; @@ -4144,7 +4527,7 @@ switch(type_event){ buffer_write(dmapBuffer, buffer_u8, global.dmap[i, j]); } } - network_send_packet(socket, dmapBuffer, buffer_tell(dmapBuffer)); + network_send_udp(socket, server_ip, server_port, dmapBuffer, buffer_tell(dmapBuffer)); buffer_delete(dmapBuffer); } @@ -4201,7 +4584,7 @@ buffer_write(buffer11, buffer_s16, room); buffer_write(buffer11, buffer_s16, global.currX); buffer_write(buffer11, buffer_s16, global.currY); buffer_write(buffer11, buffer_s8, global.sax); -var result = network_send_packet(socket, buffer11, buffer_tell(buffer11)); +var result = network_send_udp(socket, server_ip, server_port, buffer11, buffer_tell(buffer11)); buffer_delete(buffer11); @@ -4241,7 +4624,7 @@ buffer_write(buffer10, buffer_s32, bufferSize); buffer_write(buffer10, buffer_u8, 35); buffer_write(buffer10, buffer_s16, global.mapposx); buffer_write(buffer10, buffer_s16, global.mapposy); -var result = network_send_packet(socket, buffer10, buffer_tell(buffer10)); +var result = network_send_udp(socket, server_ip, server_port, buffer10, buffer_tell(buffer10)); buffer_delete(buffer10); @@ -4278,7 +4661,7 @@ buffer_seek(buffer9, buffer_seek_start, 0); buffer_write(buffer9, buffer_s32, bufferSize); buffer_write(buffer9, buffer_u8, 34); buffer_write(buffer9, buffer_u8, global.saxmode); -var result = network_send_packet(socket, buffer9, buffer_tell(buffer9)); +var result = network_send_udp(socket, server_ip, server_port, buffer9, buffer_tell(buffer9)); buffer_delete(buffer9); @@ -4315,7 +4698,7 @@ buffer_seek(buffer8, buffer_seek_start, 0); buffer_write(buffer8, buffer_s32, bufferSize); buffer_write(buffer8, buffer_u8, 33); buffer_write(buffer8, buffer_u8, global.lobbyLocked); -var result = network_send_packet(socket, buffer8, buffer_tell(buffer8)); +var result = network_send_udp(socket, server_ip, server_port, buffer8, buffer_tell(buffer8)); buffer_delete(buffer8); @@ -4351,7 +4734,7 @@ var bufferSize = buffer_tell(buffer7); buffer_seek(buffer7, buffer_seek_start, 0); buffer_write(buffer7, buffer_s32, bufferSize); buffer_write(buffer7, buffer_u8, 32); -var result = network_send_packet(socket, buffer7, buffer_tell(buffer7)); +var result = network_send_udp(socket, server_ip, server_port, buffer7, buffer_tell(buffer7)); buffer_delete(buffer7); @@ -4394,7 +4777,7 @@ buffer_write(buffer6, buffer_u8, global.otherAbsorbID); buffer_write(buffer6, buffer_s16, global.otherAbsorbRelativeX); buffer_write(buffer6, buffer_s16, global.otherAbsorbRelativeY); buffer_write(buffer6, buffer_u8, global.otherAbsorbSpriteHeight); -var result = network_send_packet(socket, buffer6, buffer_tell(buffer6)); +var result = network_send_udp(socket, server_ip, server_port, buffer6, buffer_tell(buffer6)); buffer_delete(buffer6); @@ -4443,7 +4826,7 @@ buffer_write(buffer5, buffer_u8, global.checkBeam); buffer_write(buffer5, buffer_u8, global.checkMissile); buffer_write(buffer5, buffer_u8, global.checkDamage); buffer_write(buffer5, buffer_u8, global.checkFreeze); -var result = network_send_packet(socket, buffer5, buffer_tell(buffer5)); +var result = network_send_udp(socket, server_ip, server_port, buffer5, buffer_tell(buffer5)); buffer_delete(buffer5); @@ -4481,7 +4864,7 @@ buffer_seek(buffer4, buffer_seek_start, 0); buffer_write(buffer4, buffer_s32, bufferSize); buffer_write(buffer4, buffer_u8, 105); buffer_write(buffer4, buffer_u8, global.otherID); -var result = network_send_packet(socket, buffer4, buffer_tell(buffer4)); +var result = network_send_udp(socket, server_ip, server_port, buffer4, buffer_tell(buffer4)); buffer_delete(buffer4); @@ -4518,7 +4901,7 @@ buffer_seek(buffer3, buffer_seek_start, 0); buffer_write(buffer3, buffer_s32, bufferSize); buffer_write(buffer3, buffer_u8, 30); buffer_write(buffer3, buffer_u8, elm); -var result = network_send_packet(socket, buffer3, buffer_tell(buffer3)); +var result = network_send_udp(socket, server_ip, server_port, buffer3, buffer_tell(buffer3)); buffer_delete(buffer3); @@ -4555,7 +4938,7 @@ buffer_seek(buffer2, buffer_seek_start, 0); buffer_write(buffer2, buffer_s32, bufferSize); buffer_write(buffer2, buffer_u8, 29); buffer_write(buffer2, buffer_u8, syncedDifficulty); -var result = network_send_packet(socket, buffer2, buffer_tell(buffer2)); +var result = network_send_udp(socket, server_ip, server_port, buffer2, buffer_tell(buffer2)); buffer_delete(buffer2); @@ -4596,7 +4979,7 @@ buffer_delete(buffer2); buffer_write(buffer, buffer_u8, 19); buffer_write(buffer, buffer_s32, global.gametime); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(room == rm_score){ @@ -4642,7 +5025,7 @@ if(room == rm_score){ buffer_write(buffer, buffer_u8, 254); buffer_write(buffer, buffer_u8, global.clientID); buffer_write(buffer, buffer_u8, global.sax); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); } if(!noName){ diff --git a/objects/oControl.object.gmx b/objects/oControl.object.gmx index 18fd711..61a048c 100644 --- a/objects/oControl.object.gmx +++ b/objects/oControl.object.gmx @@ -407,6 +407,9 @@ if(!(file_exists(working_directory + "\multitroid\mod_settings.ini"))){ ini_write_real("ModSettings", "censorip", 0); ini_write_real("ModSettings", "selfpalette", 0); ini_write_string("ModSettings", "moderatorpass", "123456789"); + ini_write_string("ModSettings", "use_nat_traversal", "enabled"); + ini_write_string("ModSettings", "mediation_server_ip", "150.136.166.80"); + ini_write_real("ModSettings", "mediation_server_port", 7000); ini_close(); } ini_open(working_directory + "\multitroid\mod_settings.ini"); @@ -420,6 +423,9 @@ palette = ini_read_real("ModSettings", "palette", 0); censorip = ini_read_real("ModSettings", "censorip", 0); useselfpalette = ini_read_real("ModSettings", "selfpalette", 0); password = ini_read_string("ModSettings", "moderatorpass", "123456789"); +use_nat_server = ini_read_string("ModSettings", "use_nat_traversal", "disabled"); +nat_server = ini_read_string("ModSettings", "mediation_server_ip", "150.136.166.80"); +nat_server_port = ini_read_real("ModSettings", "mediation_server_port", 7000); ini_close(); var nameLength = string_length(name); diff --git a/objects/oOptionsMod.object.gmx b/objects/oOptionsMod.object.gmx index 0ea0b3b..5edaa13 100644 --- a/objects/oOptionsMod.object.gmx +++ b/objects/oOptionsMod.object.gmx @@ -49,6 +49,9 @@ num_customseed = tempnumcounter++; num_reset = tempnumcounter++; num_locklobby = tempnumcounter++; num_saxmode = tempnumcounter++; +num_natenabled = tempnumcounter++; +num_natserver = tempnumcounter++; +num_natport = tempnumcounter++; num_connection = tempnumcounter++; lastitem = tempnumcounter; // This value will be one number higher than the last number in the list above. @@ -58,7 +61,10 @@ name = oControl.name; port = string(oControl.port); showname = 1; sequence = 1; +use_nat_server = 1; password = oControl.password; +nat_server = oControl.nat_server; +nat_server_port = string(oControl.nat_server_port); menuDifficulty = global.difficulty; @@ -92,6 +98,15 @@ switch(oControl.showname){ break; } +switch(oControl.use_nat_server){ + case "enabled": + use_nat_server = 1; + break; + case "disabled": + use_nat_server = 0; + break; +} + vTargetY = 18 * lastitem + 30; editing = 0; @@ -128,9 +143,11 @@ tip[num_customseed] = "Enter a custom seed"; tip[num_reset] = "Reset the server"; tip[num_locklobby] = "Lock the lobby"; tip[num_saxmode] = "Toggle SA-X Mode"; +tip[num_natenabled] = "Enable/disable nat traversal"; +tip[num_natserver] = "Set the IP address of the NAT server"; +tip[num_natport] = "Set the port of the NAT server"; tip[num_connection] = "Connect/disconnect to the server"; - tip[lastitem] = get_text("GlobalOptions", "ExitMain_Tip"); global.tiptext = tip[global.curropt]; alarm[1] = 1; @@ -334,6 +351,7 @@ if (active) { if (global.curropt == num_censorip) oControl.censorip = !oControl.censorip; if (global.curropt == num_showname) showname = !showname; if (global.curropt == num_selfpalette) oControl.useselfpalette = !oControl.useselfpalette; + if (global.curropt == num_natenabled) use_nat_server = !use_nat_server; //if (global.shaders_compiled) { //if (global.curropt == num_MSRfusion) oControl.msr_fusionsuit = !oControl.msr_fusionsuit; //} @@ -383,6 +401,21 @@ if (active) { editing = !editing; op[num_ip].editing = editing; } + if (global.curropt == num_natserver && os_type != os_android) { + editing = !editing; + op[num_natserver].editing = editing; + if(editing){ + keyboard_string = nat_server; + } + sfx_play(sndMenuSel); + event_user(2); + } else if(global.curropt == num_natserver && os_type == os_android){ + if(!editing){ + msg = get_string_async("Type in the NAT Server IP to connect to.", nat_server); + } + editing = !editing; + op[num_natserver].editing = editing; + } if (global.curropt == num_port && os_type != os_android) { editing = !editing; op[num_port].editing = editing; @@ -398,6 +431,21 @@ if (active) { editing = !editing; op[num_port].editing = editing; } + if (global.curropt == num_natport && os_type != os_android) { + editing = !editing; + op[num_natport].editing = editing; + if(editing){ + keyboard_string = nat_server_port; + } + sfx_play(sndMenuSel); + event_user(2); + } else if(global.curropt = num_natport && os_type == os_android){ + if(!editing){ + msg = get_integer_async("Type in the NAT server port.", real(nat_server_port)); + } + editing = !editing; + op[num_natport].editing = editing; + } if (global.curropt == num_name && os_type != os_android) { editing = !editing; op[num_name].editing = editing; @@ -502,11 +550,21 @@ if (active) { op[num_ip].optext = ip; } + if(editing && global.curropt == num_natserver && os_type != os_android){ + nat_server = keyboard_string; + op[num_natserver].optext = nat_server; + } + if(editing && global.curropt == num_port && os_type != os_android){ port = keyboard_string; op[num_port].optext = port; } + if(editing && global.curropt == num_natport && os_type != os_android){ + nat_server_port = keyboard_string; + op[num_natport].optext = nat_server_port; + } + if(editing && global.curropt == num_name && os_type != os_android){ var keyboardLength = string_length(keyboard_string); if(keyboardLength > 15){ @@ -681,6 +739,15 @@ if(!done){ break; } + switch(use_nat_server){ + case 0: + op[num_natenabled].optext = "Disabled"; + break; + case 1: + op[num_natenabled].optext = "Enabled"; + break; + } + if(oControl.censorip && !op[num_ip].editing){ op[num_ip].optext = ""; init = 1; @@ -691,6 +758,12 @@ if(!done){ keyboard_string = clipboard_get_text(); } } + + if(op[num_natserver].editing){ + if(keyboard_check(vk_control) && keyboard_check_pressed(ord('V'))){ + keyboard_string = clipboard_get_text(); + } + } } @@ -821,6 +894,8 @@ if(oControl.censorip){ } else op[num_ip].optext = ip; op[num_port].optext = port; +op[num_natserver].optext = nat_server; +op[num_natport].optext = nat_server_port; op[num_name].optext = name; op[num_moderator].optext = password; op[num_customseed].optext = string(global.multiCustomSeed); @@ -891,6 +966,17 @@ switch(showname){ break; } +switch(use_nat_server){ + case 0: + op[num_natenabled].optext = "enabled"; + oControl.use_nat_server = "disabled"; + break; + case 1: + op[num_natenabled].optext = "enabled"; + oControl.use_nat_server = "enabled"; + break; +} + switch(oControl.preferredcolor){ case 1: color = "green"; @@ -1026,12 +1112,17 @@ ini_write_real("ModSettings", "palette", oControl.palette); ini_write_real("ModSettings", "censorip", oControl.censorip); ini_write_real("ModSettings", "selfpalette", oControl.useselfpalette); ini_write_string("ModSettings", "moderatorpass", password); +ini_write_string("ModSettings", "use_nat_traversal", oControl.use_nat_server); +ini_write_string("ModSettings", "mediation_server_ip", nat_server); +ini_write_real("ModSettings", "mediation_server_port", real(nat_server_port)); ini_close(); oControl.ipaddress = ip; oControl.name = name; oControl.port = real(port); oControl.password = password; +oControl.nat_server = nat_server; +oControl.nat_server_port = real(nat_server_port); @@ -1188,6 +1279,18 @@ op[num_saxmode].label = "SA-X Mode"; op[num_saxmode].enabled = 0; canedit[num_saxmode] = 0; +op[num_natenabled] = instance_create(x, y + sep * tempcount++, oOptionLR); +op[num_natenabled].optionid = num_natenabled; +op[num_natenabled].label = "NAT Traversal"; + +op[num_natserver] = instance_create(x, y + sep * tempcount++, oOptionLRMod); +op[num_natserver].optionid = num_natserver; +op[num_natserver].label = "NAT Server"; + +op[num_natport] = instance_create(x, y + sep * tempcount++, oOptionLRMod); +op[num_natport].optionid = num_natport; +op[num_natport].label = "NAT Server Port"; + op[num_connection] = instance_create(x, y + sep * tempcount++, oPauseOption); op[num_connection].optionid = num_connection; op[num_connection].label = connectiontxt; diff --git a/objects/oPBomb.object.gmx b/objects/oPBomb.object.gmx index a5d3af5..644bac6 100644 --- a/objects/oPBomb.object.gmx +++ b/objects/oPBomb.object.gmx @@ -52,7 +52,7 @@ if(instance_exists(oClient)){ buffer_write(pbombBuffer, buffer_s16, x); buffer_write(pbombBuffer, buffer_s16, y); buffer_write(pbombBuffer, buffer_u8, global.sax); - var result = network_send_packet(oClient.socket, pbombBuffer, buffer_tell(pbombBuffer)); + var result = udp_send_unreliable(oClient.socket, oClient.server_ip, oClient.server_port, pbombBuffer, buffer_tell(pbombBuffer)); buffer_delete(pbombBuffer); } } diff --git a/scripts/json_encode_nat_message.gml b/scripts/json_encode_nat_message.gml new file mode 100644 index 0000000..d3f4797 --- /dev/null +++ b/scripts/json_encode_nat_message.gml @@ -0,0 +1,35 @@ +/// json_encode_nat_message(msg_type, params_map) +// Creates a JSON string from message type and parameters +// msg_type: MSG_* constant +// params_map: ds_map of key-value pairs (or noone for no params) +// Returns: JSON string ready to send + +var msg_type = argument0; +var params = argument1; + +var json = '{"ID":' + string(msg_type); + +if (params != noone && ds_exists(params, ds_type_map)) { + var key = ds_map_find_first(params); + while (!is_undefined(key)) { + var value = ds_map_find_value(params, key); + + // Add comma before each additional field + json += ','; + + // Add the key + json += '"' + string(key) + '":'; + + // Add the value (handle strings vs numbers) + if (is_string(value)) { + json += '"' + string(value) + '"'; + } else { + json += string(value); + } + + key = ds_map_find_next(params, key); + } +} + +json += '}'; +return json; diff --git a/scripts/json_parse_nat_message.gml b/scripts/json_parse_nat_message.gml new file mode 100644 index 0000000..46a2ffe --- /dev/null +++ b/scripts/json_parse_nat_message.gml @@ -0,0 +1,82 @@ +/// json_parse_nat_message(json_string) +// Parses a JSON message into a ds_map +// json_string: JSON string received from server +// Returns: ds_map with parsed values (caller must destroy!) +// Returns noone if parsing fails + +var json_str = argument0; + +// Create result map +var result = ds_map_create(); + +// Basic JSON parser - extracts key-value pairs from {"key":value,"key2":value2} +// This is simplified for our specific use case + +// Remove surrounding braces and whitespace +json_str = string_replace_all(json_str, " ", ""); +json_str = string_replace_all(json_str, chr(9), ""); // tabs +json_str = string_replace_all(json_str, chr(10), ""); // newlines +json_str = string_replace_all(json_str, chr(13), ""); // carriage returns + +if (string_char_at(json_str, 1) == "{") { + json_str = string_copy(json_str, 2, string_length(json_str) - 2); +} + +// Split by commas (but not commas inside quotes) +var in_quotes = false; +var current_pair = ""; +var pos = 1; + +while (pos <= string_length(json_str)) { + var ch = string_char_at(json_str, pos); + + if (ch == '"') { + in_quotes = !in_quotes; + current_pair += ch; + } else if (ch == "," && !in_quotes) { + // Process this key-value pair + var colon_pos = string_pos(":", current_pair); + if (colon_pos > 0) { + var key = string_copy(current_pair, 1, colon_pos - 1); + var value = string_copy(current_pair, colon_pos + 1, string_length(current_pair) - colon_pos); + + // Remove quotes from key + key = string_replace_all(key, '"', ""); + + // Check if value is a string (has quotes) or number + if (string_char_at(value, 1) == '"') { + // String value - remove quotes + value = string_replace_all(value, '"', ""); + ds_map_add(result, key, value); + } else { + // Numeric value + ds_map_add(result, key, real(value)); + } + } + current_pair = ""; + } else { + current_pair += ch; + } + + pos++; +} + +// Process last pair +if (current_pair != "") { + var colon_pos = string_pos(":", current_pair); + if (colon_pos > 0) { + var key = string_copy(current_pair, 1, colon_pos - 1); + var value = string_copy(current_pair, colon_pos + 1, string_length(current_pair) - colon_pos); + + key = string_replace_all(key, '"', ""); + + if (string_char_at(value, 1) == '"') { + value = string_replace_all(value, '"', ""); + ds_map_add(result, key, value); + } else { + ds_map_add(result, key, real(value)); + } + } +} + +return result; diff --git a/scripts/nat_cleanup.gml b/scripts/nat_cleanup.gml new file mode 100644 index 0000000..a9b1919 --- /dev/null +++ b/scripts/nat_cleanup.gml @@ -0,0 +1,23 @@ +/// nat_cleanup() +// Cleans up NAT traversal resources +// Call this in oClient Destroy event + +if (mediation_socket != -1) { + network_destroy(mediation_socket); + mediation_socket = -1; +} + +// Clean up random sockets for symmetric NAT +if (ds_exists(nat_random_sockets, ds_type_list)) { + for (var i = ds_list_size(nat_random_sockets) - 1; i >= 0; i--) { + var sock = ds_list_find_value(nat_random_sockets, i); + network_destroy(sock); + } + ds_list_destroy(nat_random_sockets); +} + +mediation_connected = false; +nat_state = NAT_STATE_DISCONNECTED; +nat_type = NAT_TYPE_UNKNOWN; + +show_debug_message("NAT client cleaned up"); diff --git a/scripts/nat_connect_mediation.gml b/scripts/nat_connect_mediation.gml new file mode 100644 index 0000000..92c3f13 --- /dev/null +++ b/scripts/nat_connect_mediation.gml @@ -0,0 +1,29 @@ +/// nat_connect_mediation() +// Connects to mediation server and starts NAT detection +// Call this when client wants to use NAT traversal + +if (!use_nat_traversal) { + show_debug_message("NAT traversal not enabled"); + exit; +} + +if (mediation_socket != -1) { + show_debug_message("Already connected to mediation server"); + exit; +} + +// Create TCP connection to mediation server +mediation_socket = network_create_socket(network_socket_tcp); +var result = network_connect_raw(mediation_socket, mediation_server_ip, mediation_server_port); + +if (result >= 0) { + nat_state = NAT_STATE_CONNECTING; + show_debug_message("NAT: Connecting to mediation server at " + mediation_server_ip + ":" + string(mediation_server_port)); + popup_text("Starting NAT traversal..."); +} else { + show_debug_message("NAT: Failed to connect to mediation server"); + network_destroy(mediation_socket); + mediation_socket = -1; + nat_state = NAT_STATE_ERROR; + popup_text("NAT connection failed"); +} diff --git a/scripts/nat_constants.gml b/scripts/nat_constants.gml new file mode 100644 index 0000000..fc1b458 --- /dev/null +++ b/scripts/nat_constants.gml @@ -0,0 +1,41 @@ +/// nat_constants() +// NAT traversal constants - call once at game start to define globals +// These must match the Node.js mediation server + +// NAT States +globalvar NAT_STATE_DISCONNECTED; NAT_STATE_DISCONNECTED = 0; +globalvar NAT_STATE_CONNECTING; NAT_STATE_CONNECTING = 1; +globalvar NAT_STATE_NAT_DETECTING; NAT_STATE_NAT_DETECTING = 2; +globalvar NAT_STATE_WAITING; NAT_STATE_WAITING = 3; +globalvar NAT_STATE_PUNCHING; NAT_STATE_PUNCHING = 4; +globalvar NAT_STATE_CONNECTED; NAT_STATE_CONNECTED = 5; +globalvar NAT_STATE_ERROR; NAT_STATE_ERROR = 6; + +// NAT Types +globalvar NAT_TYPE_UNKNOWN; NAT_TYPE_UNKNOWN = -1; +globalvar NAT_TYPE_DIRECT_MAPPING; NAT_TYPE_DIRECT_MAPPING = 0; +globalvar NAT_TYPE_RESTRICTED; NAT_TYPE_RESTRICTED = 1; +globalvar NAT_TYPE_SYMMETRIC; NAT_TYPE_SYMMETRIC = 2; + +// Message Types (must match mediation server constants.js) +globalvar MSG_CONNECTED; MSG_CONNECTED = 0; +globalvar MSG_NAT_TYPE_REQUEST; MSG_NAT_TYPE_REQUEST = 1; +globalvar MSG_NAT_TEST_BEGIN; MSG_NAT_TEST_BEGIN = 2; +globalvar MSG_NAT_TEST; MSG_NAT_TEST = 3; +globalvar MSG_NAT_TYPE_RESPONSE; MSG_NAT_TYPE_RESPONSE = 4; +globalvar MSG_KEEP_ALIVE; MSG_KEEP_ALIVE = 5; +globalvar MSG_CONNECTION_REQUEST; MSG_CONNECTION_REQUEST = 6; +globalvar MSG_CONNECTION_BEGIN; MSG_CONNECTION_BEGIN = 7; +globalvar MSG_SERVER_NOT_AVAILABLE; MSG_SERVER_NOT_AVAILABLE = 8; +globalvar MSG_HOLE_PUNCH_ATTEMPT; MSG_HOLE_PUNCH_ATTEMPT = 9; +globalvar MSG_NAT_TUNNEL_DATA; MSG_NAT_TUNNEL_DATA = 10; +globalvar MSG_SYMMETRIC_HOLE_PUNCH; MSG_SYMMETRIC_HOLE_PUNCH = 11; +globalvar MSG_CONNECTION_COMPLETE; MSG_CONNECTION_COMPLETE = 12; +globalvar MSG_RECEIVED_PEER; MSG_RECEIVED_PEER = 13; +globalvar MSG_CONNECTION_TIMEOUT; MSG_CONNECTION_TIMEOUT = 14; +globalvar MSG_SERVER_REGISTER; MSG_SERVER_REGISTER = 22; + +// Mediation server ports +globalvar NAT_MEDIATION_PORT; NAT_MEDIATION_PORT = 6510; +globalvar NAT_TEST_PORT_ONE; NAT_TEST_PORT_ONE = 6511; +globalvar NAT_TEST_PORT_TWO; NAT_TEST_PORT_TWO = 6512; diff --git a/scripts/nat_handle_tcp_message.gml b/scripts/nat_handle_tcp_message.gml new file mode 100644 index 0000000..bef5c58 --- /dev/null +++ b/scripts/nat_handle_tcp_message.gml @@ -0,0 +1,155 @@ +/// nat_handle_tcp_message(message_map) +// Handles incoming TCP messages from mediation server +// message_map: ds_map returned from json_parse_nat_message() +// Call this from Async Networking event + +var msg = argument0; + +if (!ds_exists(msg, ds_type_map)) { + exit; +} + +var msg_id = ds_map_find_value(msg, "ID"); +if (is_undefined(msg_id)) { + exit; +} + +switch (msg_id) { + case MSG_CONNECTED: + // Connected to mediation server - start NAT detection + mediation_connected = true; + nat_state = NAT_STATE_NAT_DETECTING; + + // Send NAT type request + var params = ds_map_create(); + ds_map_add(params, "LocalPort", nat_local_port); + ds_map_add(params, "ClientID", nat_client_id); + nat_send_tcp_message(mediation_socket, MSG_NAT_TYPE_REQUEST, params); + ds_map_destroy(params); + break; + + case MSG_NAT_TEST_BEGIN: + // Mediation server wants us to send UDP to test ports + var test_port_one = ds_map_find_value(msg, "NATTestPortOne"); + var test_port_two = ds_map_find_value(msg, "NATTestPortTwo"); + + if (!is_undefined(test_port_one) && !is_undefined(test_port_two)) { + // Send NAT test packets to both ports + var test_params = ds_map_create(); + ds_map_add(test_params, "ClientID", nat_client_id); + var test_json = json_encode_nat_message(MSG_NAT_TEST, test_params); + ds_map_destroy(test_params); + + var test_buf = buffer_create(string_length(test_json) + 1, buffer_fixed, 1); + buffer_seek(test_buf, buffer_seek_start, 0); + buffer_write(test_buf, buffer_text, test_json); + + network_send_udp_raw(socket, mediation_server_ip, test_port_one, test_buf, buffer_tell(test_buf)); + network_send_udp_raw(socket, mediation_server_ip, test_port_two, test_buf, buffer_tell(test_buf)); + + buffer_delete(test_buf); + } + break; + + case MSG_NAT_TYPE_RESPONSE: + // Received NAT type from server + nat_type = ds_map_find_value(msg, "NATType"); + + if (!is_undefined(nat_type)) { + nat_state = NAT_STATE_WAITING; + + var type_name = "Unknown"; + if (nat_type == NAT_TYPE_DIRECT_MAPPING) type_name = "Direct Mapping (Full Cone)"; + else if (nat_type == NAT_TYPE_RESTRICTED) type_name = "Restricted"; + else if (nat_type == NAT_TYPE_SYMMETRIC) type_name = "Symmetric"; + + // Now request connection to the game server + nat_request_connection(); + } + break; + + case MSG_CONNECTION_BEGIN: + // Mediation server is coordinating connection - start hole punching + var endpoint_str = ds_map_find_value(msg, "EndpointString"); + var peer_nat = ds_map_find_value(msg, "NATType"); + var conn_id = ds_map_find_value(msg, "ConnectionID"); + + if (!is_undefined(endpoint_str) && !is_undefined(peer_nat)) { + // Parse endpoint "ip:port" + var colon_pos = string_pos(":", endpoint_str); + if (colon_pos > 0) { + nat_peer_ip = string_copy(endpoint_str, 1, colon_pos - 1); + nat_peer_port = real(string_copy(endpoint_str, colon_pos + 1, string_length(endpoint_str) - colon_pos)); + nat_peer_nat_type = peer_nat; + nat_connection_id = conn_id; + + // Check for unsupported Symmetric-to-Symmetric + if (nat_type == NAT_TYPE_SYMMETRIC && nat_peer_nat_type == NAT_TYPE_SYMMETRIC) { + show_message("Connection not possible - both sides have Symmetric NAT"); + nat_state = NAT_STATE_ERROR; + exit; + } + + // Start hole punching + nat_state = NAT_STATE_PUNCHING; + nat_hole_punch_timer = 0; + nat_hole_punch_count = 0; + nat_hole_punch_received = 0; + } + } + break; + + case MSG_CONNECTION_COMPLETE: + // Connection established! + nat_state = NAT_STATE_CONNECTED; + + // Update oClient to use the discovered endpoint + server_ip = nat_peer_ip; + server_port = nat_peer_port; + + // Mark as connected so game protocol can start + isConnected = 1; + connected = true; // Mark as connected to prevent duplicate connection flow in Step event + + popup_text("Connected to server!"); + + // Send initial connection packet to server + buffer_delete(buffer); + var size, type, alignment; + size = 1024; + type = buffer_grow; + alignment = 1; + buffer = buffer_create(size, type, alignment); + buffer_seek(buffer, buffer_seek_start, 0); + buffer_write(buffer, buffer_u8, 1); + buffer_write(buffer, buffer_string, name + "," + global.multitroid_version); + buffer_write(buffer, buffer_u8, global.sax); + var bufferSize = buffer_tell(buffer); + buffer_seek(buffer, buffer_seek_start, 0); + buffer_write(buffer, buffer_s32, bufferSize); + buffer_write(buffer, buffer_u8, 1); + buffer_write(buffer, buffer_string, name + "," + global.multitroid_version); + buffer_write(buffer, buffer_u8, global.sax); + network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); + + // Create nametag instance + instance_create(x, y, oNametag); + + // Disconnect from mediation server - we don't need it anymore + if (mediation_socket != -1) { + network_destroy(mediation_socket); + mediation_socket = -1; + mediation_connected = false; + } + break; + + case MSG_SERVER_NOT_AVAILABLE: + show_message("Server not available"); + nat_state = NAT_STATE_ERROR; + break; + + case MSG_CONNECTION_TIMEOUT: + show_message("Connection timeout"); + nat_state = NAT_STATE_ERROR; + break; +} diff --git a/scripts/nat_handle_udp_message.gml b/scripts/nat_handle_udp_message.gml new file mode 100644 index 0000000..28cdf13 --- /dev/null +++ b/scripts/nat_handle_udp_message.gml @@ -0,0 +1,46 @@ +/// nat_handle_udp_message(buffer, sender_ip, sender_port, receiving_socket) +// Handles incoming UDP messages that might be hole punch packets +// Call this from Async Networking event for UDP messages +// receiving_socket: The socket that received this packet (to detect random socket success) +// Returns true if it was a NAT message, false otherwise + +var buf = argument0; +var sender_ip = argument1; +var sender_port = argument2; +var receiving_socket = argument3; + +// Only process if we're in hole punching state +if (nat_state != NAT_STATE_PUNCHING) { + return false; +} + +// During hole punching, count ANY packet from the expected peer IP as successful hole punch +// This is because the server may transition to sending game protocol before we've received enough packets +if (sender_ip == nat_peer_ip) { + nat_hole_punch_received++; + + // Update peer port if different + if (sender_port != nat_peer_port) { + nat_peer_port = sender_port; + } + + // If we're symmetric NAT and received on one of our random sockets, note it but DON'T replace yet + // We'll switch after hole punching completes to avoid losing async events + if (nat_type == NAT_TYPE_SYMMETRIC && receiving_socket != socket) { + // Check if this is one of our random sockets + var socket_index = ds_list_find_index(nat_random_sockets, receiving_socket); + if (socket_index >= 0 && nat_successful_socket == noone) { + // Remember which socket succeeded, but don't switch yet + nat_successful_socket = receiving_socket; + + // Remove from random sockets list so we don't destroy it + ds_list_delete(nat_random_sockets, socket_index); + } + } + + // Return true to indicate we handled this packet during hole punching + return true; +} + +// Not from expected peer during hole punching - ignore +return false; diff --git a/scripts/nat_init_client.gml b/scripts/nat_init_client.gml new file mode 100644 index 0000000..3b1c1a4 --- /dev/null +++ b/scripts/nat_init_client.gml @@ -0,0 +1,41 @@ +/// nat_init_client() +// Initializes NAT traversal variables for client +// Call this in oClient Create event AFTER config is loaded + +// Mediation server connection (config variables loaded separately) +mediation_socket = -1; +mediation_connected = false; + +// NAT state machine +nat_state = NAT_STATE_DISCONNECTED; + +// NAT detection +nat_type = NAT_TYPE_UNKNOWN; +nat_local_port = 0; +nat_external_port_one = 0; +nat_external_port_two = 0; + +// Client ID (random string for mediation server) +nat_client_id = string(floor(random(999999))); + +// Connection coordination +nat_connection_id = -1; +nat_peer_ip = ""; +nat_peer_port = 0; +nat_peer_nat_type = NAT_TYPE_UNKNOWN; + +// Hole punching +nat_hole_punch_timer = 0; +nat_hole_punch_count = 0; +nat_hole_punch_received = 0; +nat_hole_punch_confirmed = false; +NAT_HOLE_PUNCH_THRESHOLD = 5; +NAT_HOLE_PUNCH_INTERVAL = 10; // Send every 10 steps (~100ms) + +// Birthday paradox sockets for symmetric NAT +nat_random_sockets = ds_list_create(); // List of random UDP sockets +nat_random_sockets_created = false; +nat_successful_socket = noone; // Track which random socket succeeded + +// TCP message buffer for partial messages +nat_tcp_buffer = ""; diff --git a/scripts/nat_request_connection.gml b/scripts/nat_request_connection.gml new file mode 100644 index 0000000..839d1de --- /dev/null +++ b/scripts/nat_request_connection.gml @@ -0,0 +1,17 @@ +/// nat_request_connection() +// Requests connection to game server through mediation server +// Call this after NAT type is detected + +if (nat_state != NAT_STATE_WAITING) { + show_debug_message("NAT: Cannot request connection - not in WAITING state"); + exit; +} + +// Send connection request to mediation server +var params = ds_map_create(); +ds_map_add(params, "EndpointString", server_ip); // The game server IP we want to connect to +ds_map_add(params, "NATType", nat_type); +nat_send_tcp_message(mediation_socket, MSG_CONNECTION_REQUEST, params); +ds_map_destroy(params); + +show_debug_message("NAT: Sent connection request for server " + server_ip); diff --git a/scripts/nat_send_hole_punch.gml b/scripts/nat_send_hole_punch.gml new file mode 100644 index 0000000..a30ff01 --- /dev/null +++ b/scripts/nat_send_hole_punch.gml @@ -0,0 +1,23 @@ +/// nat_send_hole_punch(udp_socket, target_ip, target_port) +// Sends a UDP hole punch packet to peer +// udp_socket: UDP socket ID +// target_ip: Peer's IP address +// target_port: Peer's UDP port + +var udp_socket = argument0; +var target_ip = argument1; +var target_port = argument2; + +// Create simple hole punch message +var params = ds_map_create(); +var json_str = json_encode_nat_message(MSG_HOLE_PUNCH_ATTEMPT, params); +ds_map_destroy(params); + +// Send as UDP +var buf = buffer_create(string_length(json_str) + 1, buffer_fixed, 1); +buffer_seek(buf, buffer_seek_start, 0); +buffer_write(buf, buffer_text, json_str); + +network_send_udp_raw(udp_socket, target_ip, target_port, buf, buffer_tell(buf)); + +buffer_delete(buf); diff --git a/scripts/nat_send_tcp_message.gml b/scripts/nat_send_tcp_message.gml new file mode 100644 index 0000000..c74bf67 --- /dev/null +++ b/scripts/nat_send_tcp_message.gml @@ -0,0 +1,23 @@ +/// nat_send_tcp_message(tcp_socket, msg_type, params_map) +// Sends a JSON message over TCP to mediation server +// tcp_socket: TCP socket ID +// msg_type: MSG_* constant +// params_map: ds_map of parameters (or noone) + +var tcp_socket = argument0; +var msg_type = argument1; +var params = argument2; + +// Encode to JSON +var json_str = json_encode_nat_message(msg_type, params); + +// Create buffer with JSON string +var buf = buffer_create(string_length(json_str) + 1, buffer_fixed, 1); +buffer_seek(buf, buffer_seek_start, 0); +buffer_write(buf, buffer_text, json_str); + +// Send over TCP +network_send_packet(tcp_socket, buf, buffer_tell(buf)); + +// Clean up +buffer_delete(buf); diff --git a/scripts/nat_step_hole_punching.gml b/scripts/nat_step_hole_punching.gml new file mode 100644 index 0000000..f3a6f0a --- /dev/null +++ b/scripts/nat_step_hole_punching.gml @@ -0,0 +1,119 @@ +/// nat_step_hole_punching() +// Handles hole punching in Step event +// Call this from oClient Step event when using NAT traversal + +if (!use_nat_traversal) exit; + +// Only run hole punching if in the PUNCHING state +if (nat_state != NAT_STATE_PUNCHING) exit; + +nat_hole_punch_timer++; + +// If WE are symmetric, create 256 random UDP sockets (birthday paradox) +if (nat_type == NAT_TYPE_SYMMETRIC && !nat_random_sockets_created) { + for (var i = 0; i < 256; i++) { + var random_socket = network_create_socket_ext(network_socket_udp, 0); + if (random_socket >= 0) { + ds_list_add(nat_random_sockets, random_socket); + } + } + nat_random_sockets_created = true; +} + +// Send hole punch packet every NAT_HOLE_PUNCH_INTERVAL steps +if (nat_hole_punch_timer >= NAT_HOLE_PUNCH_INTERVAL) { + nat_hole_punch_timer = 0; + nat_hole_punch_count++; + + // Determine punch strategy based on OUR NAT type and PEER's NAT type + if (nat_type == NAT_TYPE_SYMMETRIC) { + // WE are symmetric - send from all 256 random sockets to peer's known port + // This creates 256 different source ports, leveraging birthday paradox + var socket_count = ds_list_size(nat_random_sockets); + for (var i = 0; i < socket_count; i++) { + var random_socket = ds_list_find_value(nat_random_sockets, i); + nat_send_hole_punch(random_socket, nat_peer_ip, nat_peer_port); + } + } else if (nat_peer_nat_type == NAT_TYPE_SYMMETRIC) { + // PEER is symmetric - we scan random ports to find their dynamic port + for (var i = 0; i < 100; i++) { + var random_port = irandom_range(1024, 65535); + nat_send_hole_punch(socket, nat_peer_ip, random_port); + } + } else { + // Neither is symmetric - simple bidirectional punch to known port + nat_send_hole_punch(socket, nat_peer_ip, nat_peer_port); + } + + // After receiving enough packets, confirm connection (only once) + if (nat_hole_punch_received >= NAT_HOLE_PUNCH_THRESHOLD && !nat_hole_punch_confirmed) { + // Send confirmation to mediation server + var params = ds_map_create(); + ds_map_add(params, "ConnectionID", nat_connection_id); + ds_map_add(params, "IsServer", false); + nat_send_tcp_message(mediation_socket, MSG_RECEIVED_PEER, params); + ds_map_destroy(params); + + nat_hole_punch_confirmed = true; + + // Connection is established - update state + nat_state = NAT_STATE_CONNECTED; + server_ip = nat_peer_ip; + server_port = nat_peer_port; + isConnected = 1; + connected = true; // Mark as connected to prevent duplicate connection flow in Step event + + popup_text("Connected!"); + + // If symmetric NAT and we found a successful socket, switch to it NOW before sending initial packet + if (nat_successful_socket != noone) { + // Destroy old main socket + network_destroy(socket); + + // Switch to successful socket + socket = nat_successful_socket; + + // Clean up remaining random sockets + for (var i = ds_list_size(nat_random_sockets) - 1; i >= 0; i--) { + var sock = ds_list_find_value(nat_random_sockets, i); + network_destroy(sock); + } + ds_list_clear(nat_random_sockets); + } + + // Send initial connection packet to server + buffer_delete(buffer); + var size, type, alignment; + size = 1024; + type = buffer_grow; + alignment = 1; + buffer = buffer_create(size, type, alignment); + buffer_seek(buffer, buffer_seek_start, 0); + buffer_write(buffer, buffer_u8, 1); + buffer_write(buffer, buffer_string, name + "," + global.multitroid_version); + buffer_write(buffer, buffer_u8, global.sax); + var bufferSize = buffer_tell(buffer); + buffer_seek(buffer, buffer_seek_start, 0); + buffer_write(buffer, buffer_s32, bufferSize); + buffer_write(buffer, buffer_u8, 1); + buffer_write(buffer, buffer_string, name + "," + global.multitroid_version); + buffer_write(buffer, buffer_u8, global.sax); + network_send_udp(socket, server_ip, server_port, buffer, buffer_tell(buffer)); + + // Create nametag instance + instance_create(x, y, oNametag); + + // Disconnect from mediation server + if (mediation_socket != -1) { + network_destroy(mediation_socket); + mediation_socket = -1; + mediation_connected = false; + } + } + + // Timeout after too many attempts + if (nat_hole_punch_count > 300) { // 30 seconds + popup_text("Connection timeout"); + nat_state = NAT_STATE_ERROR; + } +} diff --git a/scripts/send_seed.gml b/scripts/send_seed.gml index 257caa8..47a7d43 100644 --- a/scripts/send_seed.gml +++ b/scripts/send_seed.gml @@ -22,5 +22,5 @@ buffer_write(seedBuffer, buffer_s32, bufferSize); buffer_write(seedBuffer, buffer_u8, 3); buffer_write(seedBuffer, buffer_f64, currSeed); buffer_write(seedBuffer, buffer_u8, global.clientID); -var result = network_send_packet(oClient.socket, seedBuffer, buffer_tell(seedBuffer)); +var result = udp_send_reliable(oClient.socket, oClient.server_ip, oClient.server_port, seedBuffer, buffer_tell(seedBuffer)); popup_text(str); diff --git a/scripts/shoot_beam.gml b/scripts/shoot_beam.gml index a30d8c6..20c1789 100644 --- a/scripts/shoot_beam.gml +++ b/scripts/shoot_beam.gml @@ -272,7 +272,7 @@ if(instance_exists(oClient)){ buffer_write(beamBuffer, buffer_s16, beamY); buffer_write(beamBuffer, buffer_u8, chargebeam); buffer_write(beamBuffer, buffer_u8, global.sax); - var result = network_send_packet(oClient.socket, beamBuffer, buffer_tell(beamBuffer)); + var result = udp_send_unreliable(oClient.socket, oClient.server_ip, oClient.server_port, beamBuffer, buffer_tell(beamBuffer)); buffer_delete(beamBuffer); } } diff --git a/scripts/shoot_missile.gml b/scripts/shoot_missile.gml index daa1cbb..764eb86 100644 --- a/scripts/shoot_missile.gml +++ b/scripts/shoot_missile.gml @@ -89,7 +89,7 @@ if(instance_exists(oClient) && instance_exists(oCharacter) && missileX != 0 && m buffer_write(missileBuffer, buffer_s8, oCharacter.xVel); buffer_write(missileBuffer, buffer_s8, oCharacter.yVel); buffer_write(missileBuffer, buffer_u8, global.icemissiles); - var result = network_send_packet(oClient.socket, missileBuffer, buffer_tell(missileBuffer)); + var result = udp_send_unreliable(oClient.socket, oClient.server_ip, oClient.server_port, missileBuffer, buffer_tell(missileBuffer)); buffer_delete(missileBuffer); } } diff --git a/scripts/udp_client_cleanup.gml b/scripts/udp_client_cleanup.gml new file mode 100644 index 0000000..01e0713 --- /dev/null +++ b/scripts/udp_client_cleanup.gml @@ -0,0 +1,22 @@ +/// udp_client_cleanup() +// Clean up client UDP resources + +// Clean up fragment buffers +var frag_id = ds_map_find_first(global.fragment_buffers); +while (!is_undefined(frag_id)) { + var frag_map = ds_map_find_value(global.fragment_buffers, frag_id); + var frag_idx = ds_map_find_first(frag_map); + while (!is_undefined(frag_idx)) { + var frag_buffer = ds_map_find_value(frag_map, frag_idx); + if (is_real(frag_buffer) && buffer_exists(frag_buffer)) { + buffer_delete(frag_buffer); + } + frag_idx = ds_map_find_next(frag_map, frag_idx); + } + ds_map_destroy(frag_map); + frag_id = ds_map_find_next(global.fragment_buffers, frag_id); +} + +ds_map_destroy(global.fragment_buffers); +ds_map_destroy(global.fragment_timestamps); +ds_map_destroy(global.received_sequences); diff --git a/scripts/udp_client_cleanup_old_fragments.gml b/scripts/udp_client_cleanup_old_fragments.gml new file mode 100644 index 0000000..1bc21ee --- /dev/null +++ b/scripts/udp_client_cleanup_old_fragments.gml @@ -0,0 +1,35 @@ +/// udp_client_cleanup_old_fragments() +// Clean up incomplete fragment packets that have timed out +// Call this in Step event + +var current = current_time; +var frag_id = ds_map_find_first(global.fragment_timestamps); + +while (!is_undefined(frag_id)) { + var next_id = ds_map_find_next(global.fragment_timestamps, frag_id); + var timestamp = ds_map_find_value(global.fragment_timestamps, frag_id); + + // If fragment is older than timeout, clean it up + if (current - timestamp > global.FRAGMENT_TIMEOUT) { + // Clean up the fragment buffers + if (ds_map_exists(global.fragment_buffers, frag_id)) { + var frag_map = ds_map_find_value(global.fragment_buffers, frag_id); + var total = ds_map_find_value(frag_map, "total"); + + // Delete any stored fragment buffers + for (var i = 0; i < total; i++) { + if (ds_map_exists(frag_map, i)) { + var frag_buf = ds_map_find_value(frag_map, i); + buffer_delete(frag_buf); + } + } + + ds_map_destroy(frag_map); + ds_map_delete(global.fragment_buffers, frag_id); + } + + ds_map_delete(global.fragment_timestamps, frag_id); + } + + frag_id = next_id; +} diff --git a/scripts/udp_client_init.gml b/scripts/udp_client_init.gml new file mode 100644 index 0000000..6985042 --- /dev/null +++ b/scripts/udp_client_init.gml @@ -0,0 +1,26 @@ +/// udp_client_init(server_ip, server_port) +// Initialize UDP client for connecting to proxy server +// Returns socket ID + +var ip = argument0; +var port = argument1; + +// Create UDP socket +var sock = network_create_socket_ext(network_socket_udp, port); + +// Initialize client data structures +global.udp_client_socket = sock; +global.udp_client_server_ip = ip; +global.udp_client_server_port = port; + +// Fragment reassembly +global.fragment_buffers = ds_map_create(); +global.fragment_timestamps = ds_map_create(); +global.FRAGMENT_TIMEOUT = 5000; +global.UDP_MAX_FRAGMENT_SIZE = 1400; + +// Reliable UDP with ACK +global.received_sequences = ds_map_create(); +global.SEQUENCE_CACHE_SIZE = 1000; + +return sock; diff --git a/scripts/udp_send.gml b/scripts/udp_send.gml new file mode 100644 index 0000000..3b10466 --- /dev/null +++ b/scripts/udp_send.gml @@ -0,0 +1,11 @@ +/// udp_send(socket, ip, port, buffer, size) +// Wrapper for network_send_udp - drop-in replacement for network_send_packet +// Use this instead of network_send_packet for UDP client connections + +var sock = argument0; +var ip = argument1; +var port = argument2; +var buff = argument3; +var sz = argument4; + +return network_send_udp(sock, ip, port, buff, sz); diff --git a/scripts/udp_send_reliable.gml b/scripts/udp_send_reliable.gml new file mode 100644 index 0000000..37b96b0 --- /dev/null +++ b/scripts/udp_send_reliable.gml @@ -0,0 +1,41 @@ +/// udp_send_reliable(socket, ip, port, buffer, size) +// Send a UDP packet with ACK/retransmission reliability +// Use this for critical packets that must arrive (items, events, etc.) + +var sock = argument0; +var ip = argument1; +var port = argument2; +var buff = argument3; +var sz = argument4; + +// If oClient doesn't exist, fall back to unreliable send +if (!instance_exists(oClient)) { + return network_send_udp(sock, ip, port, buff, sz); +} + +// Wrap with sequence number for ACK: [254][seq_num (u32)][original_packet] +var seq_num = oClient.next_sequence_number; +oClient.next_sequence_number = (oClient.next_sequence_number + 1) mod 4294967296; + +var reliable_buffer = buffer_create(sz + 5, buffer_fixed, 1); +buffer_write(reliable_buffer, buffer_u8, 254); // Reliable packet marker +buffer_write(reliable_buffer, buffer_u32, seq_num); +buffer_copy(buff, 0, sz, reliable_buffer, 5); + +var result = network_send_udp(sock, ip, port, reliable_buffer, sz + 5); + +// Track for retransmission on client side +if (is_undefined(oClient.pending_acks)) { + oClient.pending_acks = ds_map_create(); +} + +var ack_data = ds_map_create(); +ds_map_add(ack_data, "buffer", reliable_buffer); +ds_map_add(ack_data, "size", sz + 5); +ds_map_add(ack_data, "send_time", current_time); +ds_map_add(ack_data, "retransmits", 0); +ds_map_add(ack_data, "ip", ip); +ds_map_add(ack_data, "port", port); +ds_map_add(oClient.pending_acks, seq_num, ack_data); + +return result; diff --git a/scripts/udp_send_unreliable.gml b/scripts/udp_send_unreliable.gml new file mode 100644 index 0000000..dae5968 --- /dev/null +++ b/scripts/udp_send_unreliable.gml @@ -0,0 +1,11 @@ +/// udp_send_unreliable(socket, ip, port, buffer, size) +// Send a UDP packet without reliability (fast, no ACK needed) +// Use this for frequent updates where latest data is most important (positions, etc.) + +var sock = argument0; +var ip = argument1; +var port = argument2; +var buff = argument3; +var sz = argument4; + +return network_send_udp(sock, ip, port, buff, sz); diff --git a/scripts/update_variables.gml b/scripts/update_variables.gml index 021ee02..5d13535 100644 --- a/scripts/update_variables.gml +++ b/scripts/update_variables.gml @@ -603,7 +603,7 @@ switch(argument0){ buffer_write(buffer, buffer_u8, global.smissiles); buffer_write(buffer, buffer_u8, global.pbombs); buffer_write(buffer, buffer_u8, global.clientID); - var result = network_send_packet(socket, buffer, buffer_tell(buffer)); + var result = udp_send_reliable(socket, oClient.server_ip, oClient.server_port, buffer, buffer_tell(buffer)); } } else { global.onlineList[# 1, i] = global.onlineReceived[# 1, i];