DEADSOFTWARE

Game: Warn about ports;
[d2df-sdl.git] / src / game / g_net.pas
index 35968491ebc828dc90f30cbcf5cd56d2d447db16..907a5beefd437b163adaf1ef934dbc29c55fee58 100644 (file)
@@ -1,4 +1,4 @@
-(* Copyright (C)  DooM 2D:Forever Developers
+(* Copyright (C)  Doom 2D: Forever Developers
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *)
-{$INCLUDE g_amodes.inc}
+{$INCLUDE ../shared/a_modes.inc}
 unit g_net;
 
 interface
 
 uses
-  e_log, e_fixedbuffer, ENet, Classes;
+  e_log, e_msg, ENet, Classes, MAPDEF{$IFDEF USE_MINIUPNPC}, miniupnpc;{$ELSE};{$ENDIF}
 
 const
-  NET_PROTOCOL_VER = 171;
+  NET_PROTOCOL_VER = 180;
 
   NET_MAXCLIENTS = 24;
   NET_CHANS = 11;
@@ -43,7 +43,8 @@ const
   NET_SERVER = 1;
   NET_CLIENT = 2;
 
-  NET_BUFSIZE = 65536;
+  NET_BUFSIZE = $FFFF;
+  NET_PING_PORT = $DF2D;
 
   NET_EVERYONE = -1;
 
@@ -62,9 +63,17 @@ const
   NET_STATE_AUTH = 1;
   NET_STATE_GAME = 2;
 
+  NET_CONNECT_TIMEOUT = 1000 * 10;
+
   BANLIST_FILENAME = 'banlist.txt';
   NETDUMP_FILENAME = 'netdump';
 
+  {$IFDEF FREEBSD}
+    NilThreadId = nil;
+  {$ELSE}
+    NilThreadId = 0;
+  {$ENDIF}
+
 type
   TNetClient = record
     ID:      Byte;
@@ -116,7 +125,7 @@ var
   NetClientIP:   string = '127.0.0.1';
   NetClientPort: Word   = 25666;
 
-  NetIn, NetOut: TBuffer;
+  NetIn, NetOut: TMsg;
 
   NetClients:     array of TNetClient;
   NetClientCount: Byte = 0;
@@ -136,9 +145,19 @@ var
 
   NetForcePlayerUpdate: Boolean = False;
   NetPredictSelf:       Boolean = True;
-  NetGotKeys:           Boolean = False;
+  NetForwardPorts:      Boolean = False;
 
   NetGotEverything: Boolean = False;
+  NetGotKeys:       Boolean = False;
+
+{$IFDEF USE_MINIUPNPC}
+  NetPortForwarded: Word = 0;
+  NetPongForwarded: Boolean = False;
+  NetIGDControl: AnsiString;
+  NetIGDService: TURLStr;
+{$ENDIF}
+
+  NetPortThread: TThreadID = NilThreadId;
 
   NetDumpFile: TStream;
 
@@ -162,7 +181,7 @@ function  g_Net_Client_ByName(Name: string): pTNetClient;
 function  g_Net_Client_ByPlayer(PID: Word): pTNetClient;
 function  g_Net_ClientName_ByID(ID: Integer): string;
 
-procedure g_Net_SendData(Data:AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
+procedure g_Net_SendData(Data: AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
 function  g_Net_Wait_Event(msgId: Word): TMemoryStream;
 
 function  IpToStr(IP: LongWord): string;
@@ -181,12 +200,18 @@ procedure g_Net_DumpSendBuffer();
 procedure g_Net_DumpRecvBuffer(Buf: penet_uint8; Len: LongWord);
 procedure g_Net_DumpEnd();
 
+function g_Net_ForwardPorts(ForwardPongPort: Boolean = True): Boolean;
+procedure g_Net_UnforwardPorts();
+
 implementation
 
 uses
   SysUtils,
   e_input, g_nethandler, g_netmsg, g_netmaster, g_player, g_window, g_console,
-  g_main, g_game, g_language, g_weapons, utils;
+  g_main, g_game, g_language, g_weapons, utils, ctypes;
+
+var
+  g_Net_DownloadTimeout: Single;
 
 
 { /// SERVICE FUNCTIONS /// }
@@ -248,8 +273,8 @@ var
   IPstr: string;
   IP: LongWord;
 begin
-  e_Buffer_Clear(@NetIn);
-  e_Buffer_Clear(@NetOut);
+  NetIn.Clear();
+  NetOut.Clear();
   SetLength(NetClients, 0);
   NetPeer := nil;
   NetHost := nil;
@@ -282,8 +307,8 @@ end;
 
 procedure g_Net_Cleanup();
 begin
-  e_Buffer_Clear(@NetIn);
-  e_Buffer_Clear(@NetOut);
+  NetIn.Clear();
+  NetOut.Clear();
 
   SetLength(NetClients, 0);
   NetClientCount := 0;
@@ -305,6 +330,12 @@ begin
 
   NetMode := NET_NONE;
 
+  if NetPortThread <> NilThreadId then
+    WaitForThreadTerminate(NetPortThread, 66666);
+
+  NetPortThread := NilThreadId;
+  g_Net_UnforwardPorts();
+
   if NetDump then
     g_Net_DumpEnd();
 end;
@@ -321,6 +352,12 @@ end;
 { /// SERVER FUNCTIONS /// }
 
 
+function ForwardThread(Param: Pointer): PtrInt;
+begin
+  Result := 0;
+  if not g_Net_ForwardPorts() then Result := -1;
+end;
+
 function g_Net_Host(IPAddr: LongWord; Port: enet_uint16; MaxClients: Cardinal = 16): Boolean;
 begin
   if NetMode <> NET_NONE then
@@ -348,6 +385,8 @@ begin
   NetAddr.host := IPAddr;
   NetAddr.port := Port;
 
+  if NetForwardPorts then NetPortThread := BeginThread(ForwardThread);
+
   NetHost := enet_host_create(@NetAddr, NET_MAXCLIENTS, NET_CHANS, 0, 0);
 
   if (NetHost = nil) then
@@ -362,7 +401,7 @@ begin
   if NetPongSock <> ENET_SOCKET_NULL then
   begin
     NetPongAddr.host := IPAddr;
-    NetPongAddr.port := Port + 1;
+    NetPongAddr.port := NET_PING_PORT;
     if enet_socket_bind(NetPongSock, @NetPongAddr) < 0 then
     begin
       enet_socket_destroy(NetPongSock);
@@ -373,7 +412,7 @@ begin
   end;
 
   NetMode := NET_SERVER;
-  e_Buffer_Clear(@NetOut);
+  NetOut.Clear();
 
   if NetDump then
     g_Net_DumpStart();
@@ -414,7 +453,7 @@ begin
   NetMode := NET_NONE;
 
   g_Net_Cleanup;
-  e_WriteLog('NET: Server stopped', MSG_NOTIFY);
+  e_WriteLog('NET: Server stopped', TMsgType.Notify);
 end;
 
 
@@ -433,14 +472,14 @@ begin
     if ID > High(NetClients) then Exit;
     if NetClients[ID].Peer = nil then Exit;
 
-    P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, F);
+    P := enet_packet_create(NetOut.Data, NetOut.CurSize, F);
     if not Assigned(P) then Exit;
 
     enet_peer_send(NetClients[ID].Peer, Chan, P);
   end
   else
   begin
-    P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, F);
+    P := enet_packet_create(NetOut.Data, NetOut.CurSize, F);
     if not Assigned(P) then Exit;
 
     enet_host_broadcast(NetHost, Chan, P);
@@ -448,7 +487,7 @@ begin
 
   if NetDump then g_Net_DumpSendBuffer();
   g_Net_Flush();
-  e_Buffer_Clear(@NetOut);
+  NetOut.Clear();
 end;
 
 procedure g_Net_Host_CheckPings();
@@ -474,22 +513,23 @@ begin
   begin
     ClTime := Int64(Addr(Ping[2])^);
 
-    e_Buffer_Clear(@NetOut);
-    e_Buffer_Write(@NetOut, Byte(Ord('D')));
-    e_Buffer_Write(@NetOut, Byte(Ord('F')));
-    e_Buffer_Write(@NetOut, ClTime);
+    NetOut.Clear();
+    NetOut.Write(Byte(Ord('D')));
+    NetOut.Write(Byte(Ord('F')));
+    NetOut.Write(NetPort);
+    NetOut.Write(ClTime);
     g_Net_Slist_WriteInfo();
     NPl := 0;
     if gPlayer1 <> nil then Inc(NPl);
     if gPlayer2 <> nil then Inc(NPl);
-    e_Buffer_Write(@NetOut, NPl);
-    e_Buffer_Write(@NetOut, gNumBots);
+    NetOut.Write(NPl);
+    NetOut.Write(gNumBots);
 
-    Buf.data := Addr(NetOut.Data[0]);
-    Buf.dataLength := NetOut.WritePos;
+    Buf.data := NetOut.Data;
+    Buf.dataLength := NetOut.CurSize;
     enet_socket_send(NetPongSock, @ClAddr, @Buf, 1);
 
-    e_Buffer_Clear(@NetOut);
+    NetOut.Clear();
   end;
 end;
 
@@ -505,10 +545,8 @@ begin
   Result := 0;
 
   if NetUseMaster then
-  begin
     g_Net_Slist_Check;
-    g_Net_Host_CheckPings;
-  end;
+  g_Net_Host_CheckPings;
 
   while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
   begin
@@ -582,7 +620,7 @@ begin
           TP.Lives := 0;
           TP.Kill(K_SIMPLEKILL, 0, HIT_DISCON);
           g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [TP.Name]), True);
-          e_WriteLog('NET: Client ' + TP.Name + ' [' + IntToStr(ID) + '] disconnected.', MSG_NOTIFY);
+          e_WriteLog('NET: Client ' + TP.Name + ' [' + IntToStr(ID) + '] disconnected.', TMsgType.Notify);
           g_Player_Remove(TP.UID);
         end;
 
@@ -636,7 +674,7 @@ begin
   end
   else
   begin
-    e_WriteLog('NET: Kicked from server: ' + IntToStr(NetEvent.data), MSG_NOTIFY);
+    e_WriteLog('NET: Kicked from server: ' + IntToStr(NetEvent.data), TMsgType.Notify);
     if (NetEvent.data <= NET_DISC_MAX) then
       g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_KICK] +
         _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + NetEvent.data)], True);
@@ -650,7 +688,7 @@ begin
   g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_CLIENT_DISC]);
 
   g_Net_Cleanup;
-  e_WriteLog('NET: Disconnected', MSG_NOTIFY);
+  e_WriteLog('NET: Disconnected', TMsgType.Notify);
 end;
 
 procedure g_Net_Client_Send(Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
@@ -663,13 +701,13 @@ begin
   else
     F := 0;
 
-  P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, F);
+  P := enet_packet_create(NetOut.Data, NetOut.CurSize, F);
   if not Assigned(P) then Exit;
 
   enet_peer_send(NetPeer, Chan, P);
   if NetDump then g_Net_DumpSendBuffer();
   g_Net_Flush();
-  e_Buffer_Clear(@NetOut);
+  NetOut.Clear();
 end;
 
 function g_Net_Client_Update(): enet_size_t;
@@ -720,6 +758,7 @@ end;
 function g_Net_Connect(IP: string; Port: enet_uint16): Boolean;
 var
   OuterLoop: Boolean;
+  TimeoutTime, T: Int64;
 begin
   if NetMode <> NET_NONE then
   begin
@@ -768,6 +807,9 @@ begin
     Exit;
   end;
 
+  // предупредить что ждем слишком долго через N секунд
+  TimeoutTime := GetTimer() + NET_CONNECT_TIMEOUT;
+
   OuterLoop := True;
   while OuterLoop do
   begin
@@ -777,7 +819,7 @@ begin
       begin
         g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_CLIENT_DONE]);
         NetMode := NET_CLIENT;
-        e_Buffer_Clear(@NetOut);
+        NetOut.Clear();
         enet_peer_timeout(NetPeer, ENET_PEER_TIMEOUT_LIMIT * 2, ENET_PEER_TIMEOUT_MINIMUM * 2, ENET_PEER_TIMEOUT_MAXIMUM * 2);
         NetClientIP := IP;
         NetClientPort := Port;
@@ -787,15 +829,23 @@ begin
       end;
     end;
 
-    ProcessLoading();
+    T := GetTimer();
+    if T > TimeoutTime then
+    begin
+      TimeoutTime := T + NET_CONNECT_TIMEOUT * 100; // одного предупреждения хватит
+      g_Console_Add(_lc[I_NET_MSG_TIMEOUT_WARN], True);
+      g_Console_Add(Format(_lc[I_NET_MSG_PORTS], [Integer(Port), Integer(NET_PING_PORT)]), True);
+    end;
 
-    e_PollInput();
+    ProcessLoading(true);
 
-    if e_KeyPressed(IK_ESCAPE) or e_KeyPressed(IK_SPACE) then
+    if e_KeyPressed(IK_SPACE) or e_KeyPressed(IK_ESCAPE) or e_KeyPressed(VK_ESCAPE) or
+       e_KeyPressed(JOY0_JUMP) or e_KeyPressed(JOY1_JUMP) or e_KeyPressed(JOY2_JUMP) or e_KeyPressed(JOY3_JUMP) then
       OuterLoop := False;
   end;
 
   g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_TIMEOUT], True);
+  g_Console_Add(Format(_lc[I_NET_MSG_PORTS], [Integer(Port), Integer(NET_PING_PORT)]), True);
   if NetPeer <> nil then enet_peer_reset(NetPeer);
   if NetHost <> nil then
   begin
@@ -810,15 +860,11 @@ function IpToStr(IP: LongWord): string;
 var
   Ptr: Pointer;
 begin
-  Result := '';
   Ptr := Addr(IP);
-
-  e_Raw_Seek(0);
-  Result := Result + IntToStr(e_Raw_Read_Byte(Ptr)) + '.';
-  Result := Result + IntToStr(e_Raw_Read_Byte(Ptr)) + '.';
-  Result := Result + IntToStr(e_Raw_Read_Byte(Ptr)) + '.';
-  Result := Result + IntToStr(e_Raw_Read_Byte(Ptr));
-  e_Raw_Seek(0);
+  Result :=          IntToStr(PByte(Ptr + 0)^) + '.';
+  Result := Result + IntToStr(PByte(Ptr + 1)^) + '.';
+  Result := Result + IntToStr(PByte(Ptr + 2)^) + '.';
+  Result := Result + IntToStr(PByte(Ptr + 3)^);
 end;
 
 function StrToIp(IPstr: string; var IP: LongWord): Boolean;
@@ -880,7 +926,7 @@ begin
     end;
 end;
 
-procedure g_Net_SendData(Data:AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
+procedure g_Net_SendData(Data: AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
 var
   P: pENetPacket;
   F: enet_uint32;
@@ -909,61 +955,71 @@ begin
   enet_host_flush(NetHost);
 end;
 
+function UserRequestExit: Boolean;
+begin
+  Result := e_KeyPressed(IK_SPACE) or
+            e_KeyPressed(IK_ESCAPE) or
+            e_KeyPressed(VK_ESCAPE) or
+            e_KeyPressed(JOY0_JUMP) or
+            e_KeyPressed(JOY1_JUMP) or
+            e_KeyPressed(JOY2_JUMP) or
+            e_KeyPressed(JOY3_JUMP)
+end;
+
 function g_Net_Wait_Event(msgId: Word): TMemoryStream;
-var
-  downloadEvent: ENetEvent;
-  OuterLoop: Boolean;
-  MID: Byte;
-  Ptr: Pointer;
-  msgStream: TMemoryStream;
+  var
+    ev: ENetEvent;
+    rMsgId: Byte;
+    Ptr: Pointer;
+    stream: TMemoryStream;
+    status: cint;
 begin
-  FillChar(downloadEvent, SizeOf(downloadEvent), 0);
-  msgStream := nil;
-  OuterLoop := True;
-  while OuterLoop do
-  begin
-    while (enet_host_service(NetHost, @downloadEvent, 0) > 0) do
+  FillChar(ev, SizeOf(ev), 0);
+  stream := nil;
+  repeat
+    status := enet_host_service(NetHost, @ev, Trunc(g_Net_DownloadTimeout * 1000));
+    if status > 0 then
     begin
-      if (downloadEvent.kind = ENET_EVENT_TYPE_RECEIVE) then
-      begin
-        Ptr := downloadEvent.packet^.data;
-
-        MID := Byte(Ptr^);
-
-        if (MID = msgId) then
+      case ev.kind of
+        ENET_EVENT_TYPE_RECEIVE:
+          begin
+            Ptr := ev.packet^.data;
+            rMsgId := Byte(Ptr^);
+            if rMsgId = msgId then
+            begin
+              stream := TMemoryStream.Create;
+              stream.SetSize(ev.packet^.dataLength);
+              stream.WriteBuffer(Ptr^, ev.packet^.dataLength);
+              stream.Seek(0, soFromBeginning);
+              status := 1 (* received *)
+            end
+            else
+            begin
+              (* looks that game state always received, so ignore it *)
+              e_LogWritefln('g_Net_Wait_Event(%s): skip message %s', [msgId, rMsgId]);
+              status := 2 (* continue *)
+            end
+          end;
+      ENET_EVENT_TYPE_DISCONNECT:
         begin
-          msgStream := TMemoryStream.Create;
-          msgStream.SetSize(downloadEvent.packet^.dataLength);
-          msgStream.WriteBuffer(Ptr^, downloadEvent.packet^.dataLength);
-          msgStream.Seek(0, soFromBeginning);
-
-          OuterLoop := False;
-          enet_packet_destroy(downloadEvent.packet);
-          break;
-        end
-        else begin
-          enet_packet_destroy(downloadEvent.packet);
+          if (ev.data <= NET_DISC_MAX) then
+            g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' ' + _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + ev.data)], True);
+          status := -2 (* error: disconnected *)
         end;
-      end
       else
-        if (downloadEvent.kind = ENET_EVENT_TYPE_DISCONNECT) then
-        begin
-          if (downloadEvent.data <= NET_DISC_MAX) then
-            g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' ' +
-            _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + downloadEvent.data)], True);
-          OuterLoop := False;
-          Break;
-        end;
+        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' unknown ENet event ' + IntToStr(Ord(ev.kind)), True);
+        status := -3 (* error: unknown event *)
+      end;
+      enet_packet_destroy(ev.packet)
+    end
+    else
+    begin
+      g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' timeout reached', True);
+      status := 0 (* error: timeout *)
     end;
-
-    ProcessLoading();
-
-    e_PollInput();
-
-    if e_KeyPressed(IK_ESCAPE) or e_KeyPressed(IK_SPACE) then
-      break;
-  end;
-  Result := msgStream;
+    ProcessLoading(true);
+  until (status <> 2) or UserRequestExit();
+  Result := stream
 end;
 
 function g_Net_IsHostBanned(IP: LongWord; Perm: Boolean = False): Boolean;
@@ -1085,9 +1141,9 @@ end;
 procedure g_Net_DumpSendBuffer();
 begin
   writeInt(NetDumpFile, gTime);
-  writeInt(NetDumpFile, LongWord(NetOut.Len));
+  writeInt(NetDumpFile, LongWord(NetOut.CurSize));
   writeInt(NetDumpFile, Byte(1));
-  NetDumpFile.WriteBuffer(NetOut.Data[0], NetOut.Len);
+  NetDumpFile.WriteBuffer(NetOut.Data^, NetOut.CurSize);
 end;
 
 procedure g_Net_DumpRecvBuffer(Buf: penet_uint8; Len: LongWord);
@@ -1105,4 +1161,136 @@ begin
   NetDumpFile := nil;
 end;
 
+function g_Net_ForwardPorts(ForwardPongPort: Boolean = True): Boolean;
+{$IFDEF USE_MINIUPNPC}
+var
+  DevList: PUPNPDev;
+  Urls: TUPNPUrls;
+  Data: TIGDDatas;
+  LanAddr: array [0..255] of Char;
+  StrPort: AnsiString;
+  Err, I: Integer;
+begin
+  Result := False;
+
+  if NetPortForwarded = NetPort then
+  begin
+    Result := True;
+    exit;
+  end;
+
+  NetPongForwarded := False;
+  NetPortForwarded := 0;
+
+  DevList := upnpDiscover(1000, nil, nil, 0, 0, 2, Addr(Err));
+  if DevList = nil then
+  begin
+    conwritefln('port forwarding failed: upnpDiscover() failed: %d', [Err]);
+    exit;
+  end;
+
+  I := UPNP_GetValidIGD(DevList, @Urls, @Data, Addr(LanAddr[0]), 256);
+
+  if I = 0 then
+  begin
+    conwriteln('port forwarding failed: could not find an IGD device on this LAN');
+    FreeUPNPDevList(DevList);
+    FreeUPNPUrls(@Urls);
+    exit;
+  end;
+
+  StrPort := IntToStr(NetPort);
+  I := UPNP_AddPortMapping(
+    Urls.controlURL, Addr(data.first.servicetype[1]),
+    PChar(StrPort), PChar(StrPort), Addr(LanAddr[0]), PChar('D2DF'),
+    PChar('UDP'), nil, PChar('0')
+  );
+
+  if I <> 0 then
+  begin
+    conwritefln('forwarding port %d failed: error %d', [NetPort, I]);
+    FreeUPNPDevList(DevList);
+    FreeUPNPUrls(@Urls);
+    exit;
+  end;
+
+  if ForwardPongPort then
+  begin
+    StrPort := IntToStr(NET_PING_PORT);
+    I := UPNP_AddPortMapping(
+      Urls.controlURL, Addr(data.first.servicetype[1]),
+      PChar(StrPort), PChar(StrPort), Addr(LanAddr[0]), PChar('D2DF'),
+      PChar('UDP'), nil, PChar('0')
+    );
+
+    if I <> 0 then
+    begin
+      conwritefln('forwarding port %d failed: error %d', [NetPort + 1, I]);
+      NetPongForwarded := False;
+    end
+    else
+    begin
+      conwritefln('forwarded port %d successfully', [NetPort + 1]);
+      NetPongForwarded := True;
+    end;
+  end;
+
+  conwritefln('forwarded port %d successfully', [NetPort]);
+  NetIGDControl := AnsiString(Urls.controlURL);
+  NetIGDService := data.first.servicetype;
+  NetPortForwarded := NetPort;
+
+  FreeUPNPDevList(DevList);
+  FreeUPNPUrls(@Urls);
+  Result := True;
+end;
+{$ELSE}
+begin
+  Result := False;
+end;
+{$ENDIF}
+
+procedure g_Net_UnforwardPorts();
+{$IFDEF USE_MINIUPNPC}
+var
+  I: Integer;
+  StrPort: AnsiString;
+begin
+  if NetPortForwarded = 0 then Exit;
+
+  conwriteln('unforwarding ports...');
+
+  StrPort := IntToStr(NetPortForwarded);
+  I := UPNP_DeletePortMapping(
+    PChar(NetIGDControl), Addr(NetIGDService[1]),
+    PChar(StrPort), PChar('UDP'), nil
+  );
+  conwritefln('  port %d: %d', [NetPortForwarded, I]);
+
+  if NetPongForwarded then
+  begin
+    NetPongForwarded := False;
+    StrPort := IntToStr(NetPortForwarded + 1);
+    I := UPNP_DeletePortMapping(
+      PChar(NetIGDControl), Addr(NetIGDService[1]),
+      PChar(StrPort), PChar('UDP'), nil
+    );
+    conwritefln('  port %d: %d', [NetPortForwarded + 1, I]);
+  end;
+
+  NetPortForwarded := 0;
+end;
+{$ELSE}
+begin
+end;
+{$ENDIF}
+
+initialization
+  conRegVar('cl_downloadtimeout', @g_Net_DownloadTimeout, 0.0, 1000000.0, '', 'timeout in seconds, 0 to disable it');
+  g_Net_DownloadTimeout := 60;
+  NetIn.Alloc(NET_BUFSIZE);
+  NetOut.Alloc(NET_BUFSIZE);
+finalization
+  NetIn.Free();
+  NetOut.Free();
 end.