DEADSOFTWARE

Game: Warn about ports;
[d2df-sdl.git] / src / game / g_net.pas
index 85263e51e7e709388809188f5c664c832f9f84e3..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
@@ -19,10 +19,10 @@ unit g_net;
 interface
 
 uses
-  e_log, e_msg, ENet, Classes, MAPDEF;
+  e_log, e_msg, ENet, Classes, MAPDEF{$IFDEF USE_MINIUPNPC}, miniupnpc;{$ELSE};{$ENDIF}
 
 const
-  NET_PROTOCOL_VER = 173;
+  NET_PROTOCOL_VER = 180;
 
   NET_MAXCLIENTS = 24;
   NET_CHANS = 11;
@@ -44,6 +44,7 @@ const
   NET_CLIENT = 2;
 
   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;
@@ -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 /// }
@@ -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);
@@ -477,6 +516,7 @@ begin
     NetOut.Clear();
     NetOut.Write(Byte(Ord('D')));
     NetOut.Write(Byte(Ord('F')));
+    NetOut.Write(NetPort);
     NetOut.Write(ClTime);
     g_Net_Slist_WriteInfo();
     NPl := 0;
@@ -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
@@ -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
@@ -787,15 +829,23 @@ begin
       end;
     end;
 
-    ProcessLoading(true);
+    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
@@ -876,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;
@@ -905,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(true);
-
-    e_PollInput();
-
-    if e_KeyPressed(IK_ESCAPE) or e_KeyPressed(IK_SPACE) then
-      break;
-  end;
-  Result := msgStream;
+  until (status <> 2) or UserRequestExit();
+  Result := stream
 end;
 
 function g_Net_IsHostBanned(IP: LongWord; Perm: Boolean = False): Boolean;
@@ -1101,14 +1161,136 @@ begin
   NetDumpFile := nil;
 end;
 
-initialization
+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.