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];