DEADSOFTWARE

net: some useless code motion and small cleanups in file transfers
authorKetmar Dark <ketmar@ketmar.no-ip.org>
Sat, 12 Oct 2019 23:33:00 +0000 (02:33 +0300)
committerKetmar Dark <ketmar@ketmar.no-ip.org>
Sun, 13 Oct 2019 00:22:55 +0000 (03:22 +0300)
src/game/g_net.pas
src/game/g_res_downloader.pas

index 8f8488f904fad54e4e014f62db6a1afd15de329c..d3e67cfd6ef34c4ad3fdfc9cc0ed8ab1c1f6b2f4 100644 (file)
@@ -230,7 +230,6 @@ procedure g_Net_UnforwardPorts();
 
 function g_Net_UserRequestExit: Boolean;
 
-function g_Net_SendMapRequest (): Boolean;
 function g_Net_Wait_MapInfo (var tf: TNetFileTransfer; resList: TStringList): Integer;
 function g_Net_RequestResFileInfo (resIndex: LongInt; out tf: TNetFileTransfer): Integer;
 function g_Net_AbortResTransfer (var tf: TNetFileTransfer): Boolean;
@@ -253,7 +252,12 @@ var
   trans_omsg: TMsg;
 
 
-{ /// SERVICE FUNCTIONS /// }
+//**************************************************************************
+//
+// SERVICE FUNCTIONS
+//
+//**************************************************************************
+
 procedure clearNetClientTransfers (var nc: TNetClient);
 begin
   nc.Transfer.stream.Free;
@@ -269,6 +273,7 @@ begin
   clearNetClientTransfers(nc);
 end;
 
+
 procedure clearNetClients (clearArray: Boolean);
 var
   f: Integer;
@@ -278,454 +283,106 @@ begin
 end;
 
 
-function g_Net_FindSlot(): Integer;
-var
-  I: Integer;
-  F: Boolean;
-  N, C: Integer;
+function g_Net_UserRequestExit (): Boolean;
 begin
-  N := -1;
-  F := False;
-  C := 0;
-  for I := Low(NetClients) to High(NetClients) do
-  begin
-    if NetClients[I].Used then
-      Inc(C)
-    else
-      if not F then
-      begin
-        F := True;
-        N := I;
-      end;
-  end;
-  if C >= NetMaxClients then
-  begin
-    Result := -1;
-    Exit;
-  end;
-
-  if not F then
-  begin
-    if (Length(NetClients) >= NetMaxClients) then
-      N := -1
-    else
-    begin
-      SetLength(NetClients, Length(NetClients) + 1);
-      N := High(NetClients);
-    end;
-  end;
-
-  if N >= 0 then
-  begin
-    NetClients[N].Used := True;
-    NetClients[N].ID := N;
-    NetClients[N].RequestedFullUpdate := False;
-    NetClients[N].RCONAuth := False;
-    NetClients[N].Voted := False;
-    NetClients[N].Player := 0;
-    clearNetClientTransfers(NetClients[N]); // just in case
-  end;
-
-  Result := N;
+  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_Init(): Boolean;
-var
-  F: TextFile;
-  IPstr: string;
-  IP: LongWord;
-begin
-  NetIn.Clear();
-  NetOut.Clear();
-  NetBuf[NET_UNRELIABLE].Clear();
-  NetBuf[NET_RELIABLE].Clear();
-  //SetLength(NetClients, 0);
-  clearNetClients(true); // clear array
-  NetPeer := nil;
-  NetHost := nil;
-  NetMyID := -1;
-  NetPlrUID1 := -1;
-  NetPlrUID2 := -1;
-  NetAddr.port := 25666;
-  SetLength(NetBannedHosts, 0);
-  if FileExists(DataDir + BANLIST_FILENAME) then
-  begin
-    Assign(F, DataDir + BANLIST_FILENAME);
-    Reset(F);
-    while not EOF(F) do
-    begin
-      Readln(F, IPstr);
-      if StrToIp(IPstr, IP) then
-        g_Net_BanHost(IP);
-    end;
-    CloseFile(F);
-    g_Net_SaveBanList();
-  end;
-
-  Result := (enet_initialize() = 0);
-end;
 
-procedure g_Net_Flush();
-var
-  T: Integer;
-  P: pENetPacket;
-  F, Chan: enet_uint32;
-  I: Integer;
-begin
-  F := 0;
-  Chan := NET_CHAN_GAME;
+//**************************************************************************
+//
+// file transfer declaraions and host packet processor
+//
+//**************************************************************************
 
-  if NetMode = NET_SERVER then
-    for T := NET_UNRELIABLE to NET_RELIABLE do
-    begin
-      if NetBuf[T].CurSize > 0 then
-      begin
-        P := enet_packet_create(NetBuf[T].Data, NetBuf[T].CurSize, F);
-        if not Assigned(P) then continue;
-        enet_host_broadcast(NetHost, Chan, P);
-        NetBuf[T].Clear();
-      end;
+const
+  // server packet type
+  NTF_SERVER_DONE = 10; // done with this file
+  NTF_SERVER_FILE_INFO = 11; // sent after client request
+  NTF_SERVER_CHUNK = 12; // next chunk; chunk number follows
+  NTF_SERVER_ABORT = 13; // server abort
+  NTF_SERVER_MAP_INFO = 14;
 
-      for I := Low(NetClients) to High(NetClients) do
-      begin
-        if not NetClients[I].Used then continue;
-        if NetClients[I].NetOut[T].CurSize <= 0 then continue;
-        P := enet_packet_create(NetClients[I].NetOut[T].Data, NetClients[I].NetOut[T].CurSize, F);
-        if not Assigned(P) then continue;
-        enet_peer_send(NetClients[I].Peer, Chan, P);
-        NetClients[I].NetOut[T].Clear();
-      end;
+  // client packet type
+  NTF_CLIENT_MAP_REQUEST = 100; // map file request; also, returns list of additional wads to download
+  NTF_CLIENT_FILE_REQUEST = 101; // resource file request (by index)
+  NTF_CLIENT_ABORT = 102; // do not send requested file, or abort current transfer
+  NTF_CLIENT_START = 103; // start transfer; client may resume download by sending non-zero starting chunk
+  NTF_CLIENT_ACK = 104; // chunk ack; chunk number follows
 
-      // next and last iteration is always RELIABLE
-      F := LongWord(ENET_PACKET_FLAG_RELIABLE);
-      Chan := NET_CHAN_IMPORTANT;
-    end
-  else if NetMode = NET_CLIENT then
-    for T := NET_UNRELIABLE to NET_RELIABLE do
-    begin
-      if NetBuf[T].CurSize > 0 then
-      begin
-        P := enet_packet_create(NetBuf[T].Data, NetBuf[T].CurSize, F);
-        if not Assigned(P) then continue;
-        enet_peer_send(NetPeer, Chan, P);
-        NetBuf[T].Clear();
-      end;
-      // next and last iteration is always RELIABLE
-      F := LongWord(ENET_PACKET_FLAG_RELIABLE);
-      Chan := NET_CHAN_IMPORTANT;
-    end;
-end;
 
-procedure g_Net_Cleanup();
+// disconnect client due to some file transfer error
+procedure killClientByFT (var nc: TNetClient);
 begin
-  NetIn.Clear();
-  NetOut.Clear();
-  NetBuf[NET_UNRELIABLE].Clear();
-  NetBuf[NET_RELIABLE].Clear();
-
-  //SetLength(NetClients, 0);
-  clearNetClients(true); // clear array
-  NetClientCount := 0;
-
-  NetPeer := nil;
-  NetHost := nil;
-  NetMPeer := nil;
-  NetMHost := nil;
-  NetMyID := -1;
-  NetPlrUID1 := -1;
-  NetPlrUID2 := -1;
-  NetState := NET_STATE_NONE;
-
-  NetPongSock := ENET_SOCKET_NULL;
-
-  NetTimeToMaster := 0;
-  NetTimeToUpdate := 0;
-  NetTimeToReliable := 0;
-
-  NetMode := NET_NONE;
-
-  if NetPortThread <> NilThreadId then
-    WaitForThreadTerminate(NetPortThread, 66666);
-
-  NetPortThread := NilThreadId;
-  g_Net_UnforwardPorts();
-
-  if NetDump then
-    g_Net_DumpEnd();
+  e_LogWritefln('disconnected client #%d due to file transfer error', [nc.ID], TMsgType.Warning);
+  enet_peer_disconnect(nc.Peer, NET_DISC_FILE_TIMEOUT);
+  clearNetClientTransfers(nc);
 end;
 
-procedure g_Net_Free();
-begin
-  g_Net_Cleanup();
 
-  enet_deinitialize();
-  NetInitDone := False;
+// send file transfer message from server to client
+function ftransSendServerMsg (var nc: TNetClient; var m: TMsg): Boolean;
+var
+  pkt: PENetPacket;
+begin
+  result := false;
+  if (m.CurSize < 1) then exit;
+  pkt := enet_packet_create(m.Data, m.CurSize, ENET_PACKET_FLAG_RELIABLE);
+  if not Assigned(pkt) then begin killClientByFT(nc); exit; end;
+  if (enet_peer_send(nc.Peer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then begin killClientByFT(nc); exit; end;
+  result := true;
 end;
 
 
-{ /// SERVER FUNCTIONS /// }
-
-
-function ForwardThread(Param: Pointer): PtrInt;
+// send file transfer message from client to server
+function ftransSendClientMsg (var m: TMsg): Boolean;
+var
+  pkt: PENetPacket;
 begin
-  Result := 0;
-  if not g_Net_ForwardPorts() then Result := -1;
+  result := false;
+  if (m.CurSize < 1) then exit;
+  pkt := enet_packet_create(m.Data, m.CurSize, ENET_PACKET_FLAG_RELIABLE);
+  if not Assigned(pkt) then exit;
+  if (enet_peer_send(NetPeer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then exit;
+  result := true;
 end;
 
-function g_Net_Host(IPAddr: LongWord; Port: enet_uint16; MaxClients: Cardinal = 16): Boolean;
+
+// file chunk sender
+procedure ProcessChunkSend (var nc: TNetClient);
+var
+  tf: ^TNetFileTransfer;
+  ct: Int64;
+  chunks: Integer;
+  rd: Integer;
 begin
-  if NetMode <> NET_NONE then
+  tf := @nc.Transfer;
+  if (tf.stream = nil) then exit;
+  ct := GetTimerMS();
+  // arbitrary timeout number
+  if (ct-tf.lastAckTime >= 5000) then
   begin
-    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_INGAME]);
-    Result := False;
-    Exit;
+    killClientByFT(nc);
+    exit;
   end;
+  // check if we need to send something
+  if (not tf.inProgress) then exit; // waiting for the initial ack
+  // ok, we're sending chunks
+  if (tf.lastAckChunk <> tf.lastSentChunk) then exit;
+  Inc(tf.lastSentChunk);
+  // do it one chunk at a time; client ack will advance our chunk counter
+  chunks := (tf.size+tf.chunkSize-1) div tf.chunkSize;
 
-  Result := True;
-
-  g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST], [Port]));
-  if not NetInitDone then
+  if (tf.lastSentChunk > chunks) then
   begin
-    if (not g_Net_Init()) then
-    begin
-      g_Console_Add(_lc[I_NET_MSG_FERROR] + _lc[I_NET_ERR_ENET]);
-      Result := False;
-      Exit;
-    end
-    else
-      NetInitDone := True;
-  end;
-
-  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
-  begin
-    g_Console_Add(_lc[I_NET_MSG_ERROR] + Format(_lc[I_NET_ERR_HOST], [Port]));
-    Result := False;
-    g_Net_Cleanup;
-    Exit;
-  end;
-
-  NetPongSock := enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
-  if NetPongSock <> ENET_SOCKET_NULL then
-  begin
-    NetPongAddr.host := IPAddr;
-    NetPongAddr.port := NET_PING_PORT;
-    if enet_socket_bind(NetPongSock, @NetPongAddr) < 0 then
-    begin
-      enet_socket_destroy(NetPongSock);
-      NetPongSock := ENET_SOCKET_NULL;
-    end
-    else
-      enet_socket_set_option(NetPongSock, ENET_SOCKOPT_NONBLOCK, 1);
-  end;
-
-  NetMode := NET_SERVER;
-  NetOut.Clear();
-  NetBuf[NET_UNRELIABLE].Clear();
-  NetBuf[NET_RELIABLE].Clear();
-
-  if NetDump then
-    g_Net_DumpStart();
-end;
-
-procedure g_Net_Host_Die();
-var
-  I: Integer;
-begin
-  if NetMode <> NET_SERVER then Exit;
-
-  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_DISCALL]);
-  for I := 0 to High(NetClients) do
-    if NetClients[I].Used then
-      enet_peer_disconnect(NetClients[I].Peer, NET_DISC_DOWN);
-
-  while enet_host_service(NetHost, @NetEvent, 1000) > 0 do
-    if NetEvent.kind = ENET_EVENT_TYPE_RECEIVE then
-      enet_packet_destroy(NetEvent.packet);
-
-  for I := 0 to High(NetClients) do
-    if NetClients[I].Used then
-    begin
-      FreeMemory(NetClients[I].Peer^.data);
-      NetClients[I].Peer^.data := nil;
-      enet_peer_reset(NetClients[I].Peer);
-      NetClients[I].Peer := nil;
-      NetClients[I].Used := False;
-      NetClients[I].NetOut[NET_UNRELIABLE].Free();
-      NetClients[I].NetOut[NET_RELIABLE].Free();
-    end;
-
-  clearNetClients(false); // don't clear array
-  if (NetMPeer <> nil) and (NetMHost <> nil) then g_Net_Slist_Disconnect;
-  if NetPongSock <> ENET_SOCKET_NULL then
-    enet_socket_destroy(NetPongSock);
-
-  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_DIE]);
-  enet_host_destroy(NetHost);
-
-  NetMode := NET_NONE;
-
-  g_Net_Cleanup;
-  e_WriteLog('NET: Server stopped', TMsgType.Notify);
-end;
-
-
-procedure g_Net_Host_Send(ID: Integer; Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
-var
-  T: Integer;
-begin
-  if (Reliable) then
-    T := NET_RELIABLE
-  else
-    T := NET_UNRELIABLE;
-
-  if (ID >= 0) then
-  begin
-    if ID > High(NetClients) then Exit;
-    if NetClients[ID].Peer = nil then Exit;
-    // write size first
-    NetClients[ID].NetOut[T].Write(Integer(NetOut.CurSize));
-    NetClients[ID].NetOut[T].Write(NetOut);
-  end
-  else
-  begin
-    // write size first
-    NetBuf[T].Write(Integer(NetOut.CurSize));
-    NetBuf[T].Write(NetOut);
-  end;
-
-  if NetDump then g_Net_DumpSendBuffer();
-  NetOut.Clear();
-end;
-
-procedure g_Net_Host_CheckPings();
-var
-  ClAddr: ENetAddress;
-  Buf: ENetBuffer;
-  Len: Integer;
-  ClTime: Int64;
-  Ping: array [0..9] of Byte;
-  NPl: Byte;
-begin
-  if NetPongSock = ENET_SOCKET_NULL then Exit;
-
-  Buf.data := Addr(Ping[0]);
-  Buf.dataLength := 2+8;
-
-  Ping[0] := 0;
-
-  Len := enet_socket_receive(NetPongSock, @ClAddr, @Buf, 1);
-  if Len < 0 then Exit;
-
-  if (Ping[0] = Ord('D')) and (Ping[1] = Ord('F')) then
-  begin
-    ClTime := Int64(Addr(Ping[2])^);
-
-    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);
-    NetOut.Write(NPl);
-    NetOut.Write(gNumBots);
-
-    Buf.data := NetOut.Data;
-    Buf.dataLength := NetOut.CurSize;
-    enet_socket_send(NetPongSock, @ClAddr, @Buf, 1);
-
-    NetOut.Clear();
-  end;
-end;
-
-
-const
-  // server packet type
-  NTF_SERVER_DONE = 10; // done with this file
-  NTF_SERVER_FILE_INFO = 11; // sent after client request
-  NTF_SERVER_CHUNK = 12; // next chunk; chunk number follows
-  NTF_SERVER_ABORT = 13; // server abort
-  NTF_SERVER_MAP_INFO = 14;
-
-  // client packet type
-  NTF_CLIENT_MAP_REQUEST = 100; // map file request; also, returns list of additional wads to download
-  NTF_CLIENT_FILE_REQUEST = 101; // resource file request (by index)
-  NTF_CLIENT_ABORT = 102; // do not send requested file, or abort current transfer
-  NTF_CLIENT_START = 103; // start transfer; client may resume download by sending non-zero starting chunk
-  NTF_CLIENT_ACK = 104; // chunk ack; chunk number follows
-
-
-procedure KillClientByFT (var nc: TNetClient);
-begin
-  e_LogWritefln('disconnected client #%d due to file transfer error', [nc.ID], TMsgType.Warning);
-  enet_peer_disconnect(nc.Peer, NET_DISC_FILE_TIMEOUT);
-  clearNetClientTransfers(nc);
-end;
-
-
-function ftransSendServerMsg (var nc: TNetClient; var m: TMsg): Boolean;
-var
-  pkt: PENetPacket;
-begin
-  result := false;
-  if (m.CurSize < 1) then exit;
-  pkt := enet_packet_create(m.Data, m.CurSize, ENET_PACKET_FLAG_RELIABLE);
-  if not Assigned(pkt) then begin KillClientByFT(nc); exit; end;
-  if (enet_peer_send(nc.Peer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then begin KillClientByFT(nc); exit; end;
-  result := true;
-end;
-
-
-function ftransSendClientMsg (var m: TMsg): Boolean;
-var
-  pkt: PENetPacket;
-begin
-  result := false;
-  if (m.CurSize < 1) then exit;
-  pkt := enet_packet_create(m.Data, m.CurSize, ENET_PACKET_FLAG_RELIABLE);
-  if not Assigned(pkt) then exit;
-  if (enet_peer_send(NetPeer, NET_CHAN_DOWNLOAD_EX, pkt) <> 0) then exit;
-  result := true;
-end;
-
-
-procedure ProcessChunkSend (var nc: TNetClient);
-var
-  tf: ^TNetFileTransfer;
-  ct: Int64;
-  chunks: Integer;
-  rd: Integer;
-begin
-  tf := @nc.Transfer;
-  if (tf.stream = nil) then exit;
-  ct := GetTimerMS();
-  // arbitrary timeout number
-  if (ct-tf.lastAckTime >= 5000) then
-  begin
-    KillClientByFT(nc);
-    exit;
-  end;
-  // check if we need to send something
-  if (not tf.inProgress) then exit; // waiting for the initial ack
-  // ok, we're sending chunks
-  if (tf.lastAckChunk <> tf.lastSentChunk) then exit;
-  Inc(tf.lastSentChunk);
-  // do it one chunk at a time; client ack will advance our chunk counter
-  chunks := (tf.size+tf.chunkSize-1) div tf.chunkSize;
-
-  if (tf.lastSentChunk > chunks) then
-  begin
-    KillClientByFT(nc);
-    exit;
+    killClientByFT(nc);
+    exit;
   end;
 
   trans_omsg.Clear();
@@ -752,7 +409,7 @@ begin
       tf.stream.ReadBuffer(tf.diskBuffer^, rd);
       trans_omsg.WriteData(tf.diskBuffer, rd);
     except // sorry
-      KillClientByFT(nc);
+      killClientByFT(nc);
       exit;
     end;
   end;
@@ -761,6 +418,7 @@ begin
 end;
 
 
+// server file transfer packet processor
 // received packet is in `NetEvent`
 procedure ProcessDownloadExPacket ();
 var
@@ -795,7 +453,7 @@ begin
 
   if (NetEvent.packet^.dataLength = 0) then
   begin
-    KillClientByFT(nc^);
+    killClientByFT(nc^);
     exit;
   end;
 
@@ -809,18 +467,18 @@ begin
       begin
         if (tf.stream <> nil) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         if (NetEvent.packet^.dataLength < 2) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         // new transfer request; build packet
         if not msg.Init(NetEvent.packet^.data+1, NetEvent.packet^.dataLength-1, True) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         // get resource index
@@ -828,14 +486,14 @@ begin
         if (ridx < -1) or (ridx >= gExternalResources.Count) then
         begin
           e_LogWritefln('Invalid resource index %d', [ridx], TMsgType.Warning);
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         if (ridx < 0) then fname := MapsDir+gGameSettings.WAD else fname := GameDir+'/wads/'+gExternalResources[ridx];
         if (length(fname) = 0) then
         begin
           e_WriteLog('Invalid filename: '+fname, TMsgType.Warning);
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         tf.diskName := findDiskWad(fname);
@@ -843,7 +501,7 @@ begin
         if (length(tf.diskName) = 0) then
         begin
           e_LogWritefln('NETWORK: file "%s" not found!', [fname], TMsgType.Fatal);
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         // calculate hash
@@ -859,7 +517,7 @@ begin
         if (tf.stream = nil) then
         begin
           e_WriteLog(Format('NETWORK: file "%s" not found!', [fname]), TMsgType.Fatal);
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         e_LogWritefln('client #%d requested resource #%d (file is `%s` : `%s`)', [nc.ID, ridx, fname, tf.diskName]);
@@ -888,30 +546,30 @@ begin
       begin
         if not Assigned(tf.stream) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         if (tf.lastSentChunk <> -1) or (tf.lastAckChunk <> -1) or (tf.inProgress) then
         begin
           // double ack, get lost
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         if (NetEvent.packet^.dataLength < 2) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         // build packet
         if not msg.Init(NetEvent.packet^.data+1, NetEvent.packet^.dataLength-1, True) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         chunk := msg.ReadLongInt();
         if (chunk < 0) or (chunk > (tf.size+tf.chunkSize-1) div tf.chunkSize) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         e_LogWritefln('client #%d started file transfer from chunk %d', [nc.ID, chunk]);
@@ -925,30 +583,30 @@ begin
       begin
         if not Assigned(tf.stream) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         if (tf.lastSentChunk < 0) or (not tf.inProgress) then
         begin
           // double ack, get lost
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         if (NetEvent.packet^.dataLength < 2) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         // build packet
         if not msg.Init(NetEvent.packet^.data+1, NetEvent.packet^.dataLength-1, True) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         chunk := msg.ReadLongInt();
         if (chunk < 0) or (chunk > (tf.size+tf.chunkSize-1) div tf.chunkSize) then
         begin
-          KillClientByFT(nc^);
+          killClientByFT(nc^);
           exit;
         end;
         // do it this way, so client may seek, or request retransfers for some reason
@@ -988,505 +646,361 @@ begin
       end;
     else
       begin
-        KillClientByFT(nc^);
+        killClientByFT(nc^);
         exit;
       end;
   end;
 end;
 
 
-function g_Net_Host_Update(): enet_size_t;
-var
-  IP: string;
-  Port: Word;
-  ID: Integer;
-  TC: pTNetClient;
-  TP: TPlayer;
-  //f: Integer;
-  //ctt: Int64;
-begin
-  IP := '';
-  Result := 0;
-
-  if NetUseMaster then g_Net_Slist_Check;
-  g_Net_Host_CheckPings;
+//**************************************************************************
+//
+// file transfer crap (both client and server)
+//
+//**************************************************************************
 
-  //ctt := -GetTimerMS();
-  // process file transfers
-  {
-  for f := Low(NetClients) to High(NetClients) do
+function getNewTimeoutEnd (): Int64;
+begin
+  result := GetTimerMS();
+  if (g_Net_DownloadTimeout <= 0) then
   begin
-    if (not NetClients[f].Used) then continue;
-    if (NetClients[f].Transfer.stream = nil) then continue;
-    ProcessChunkSend(NetClients[f]);
+    result := result+1000*60*3; // 3 minutes
+  end
+  else
+  begin
+    result := result+trunc(g_Net_DownloadTimeout*1000);
   end;
-  }
-  {
-  ctt := ctt+GetTimerMS();
-  if (ctt > 1) then e_LogWritefln('all transfers: [%d]', [Integer(ctt)]);
-  }
+end;
 
-  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
-  begin
-    case (NetEvent.kind) of
-      ENET_EVENT_TYPE_CONNECT:
-      begin
-        IP := IpToStr(NetEvent.Peer^.address.host);
-        Port := NetEvent.Peer^.address.port;
-        g_Console_Add(_lc[I_NET_MSG] +
-          Format(_lc[I_NET_MSG_HOST_CONN], [IP, Port]));
-
-        if (NetEvent.data <> NET_PROTOCOL_VER) then
-        begin
-          g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
-            _lc[I_NET_DISC_PROTOCOL]);
-          NetEvent.peer^.data := GetMemory(SizeOf(Byte));
-          Byte(NetEvent.peer^.data^) := 255;
-          enet_peer_disconnect(NetEvent.peer, NET_DISC_PROTOCOL);
-          enet_host_flush(NetHost);
-          Exit;
-        end;
-
-        ID := g_Net_FindSlot();
-
-        if ID < 0 then
-        begin
-          g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
-            _lc[I_NET_DISC_FULL]);
-          NetEvent.Peer^.data := GetMemory(SizeOf(Byte));
-          Byte(NetEvent.peer^.data^) := 255;
-          enet_peer_disconnect(NetEvent.peer, NET_DISC_FULL);
-          enet_host_flush(NetHost);
-          Exit;
-        end;
-
-        NetClients[ID].Peer := NetEvent.peer;
-        NetClients[ID].Peer^.data := GetMemory(SizeOf(Byte));
-        Byte(NetClients[ID].Peer^.data^) := ID;
-        NetClients[ID].State := NET_STATE_AUTH;
-        NetClients[ID].RCONAuth := False;
-        NetClients[ID].NetOut[NET_UNRELIABLE].Alloc(NET_BUFSIZE*2);
-        NetClients[ID].NetOut[NET_RELIABLE].Alloc(NET_BUFSIZE*2);
-        clearNetClientTransfers(NetClients[ID]); // just in case
 
-        enet_peer_timeout(NetEvent.peer, ENET_PEER_TIMEOUT_LIMIT * 2, ENET_PEER_TIMEOUT_MINIMUM * 2, ENET_PEER_TIMEOUT_MAXIMUM * 2);
+// send map request to server, and wait for "map info" server reply
+//
+// returns `false` on error or user abort
+// fills:
+//    hash
+//    size
+//    chunkSize
+// returns:
+//  <0 on error
+//   0 on success
+//   1 on user abort
+//   2 on server abort
+// for maps, first `tf.diskName` name will be map wad name, and `tf.hash`/`tf.size` will contain map info
+function g_Net_Wait_MapInfo (var tf: TNetFileTransfer; resList: TStringList): Integer;
+var
+  ev: ENetEvent;
+  rMsgId: Byte;
+  Ptr: Pointer;
+  msg: TMsg;
+  freePacket: Boolean = false;
+  ct, ett: Int64;
+  status: cint;
+  s: AnsiString;
+  rc, f: LongInt;
+begin
+  // send request
+  trans_omsg.Clear();
+  trans_omsg.Write(Byte(NTF_CLIENT_MAP_REQUEST));
+  if not ftransSendClientMsg(trans_omsg) then begin result := -1; exit; end;
 
-        Inc(NetClientCount);
-        g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST_ADD], [ID]));
+  FillChar(ev, SizeOf(ev), 0);
+  Result := -1;
+  try
+    ett := getNewTimeoutEnd();
+    repeat
+      status := enet_host_service(NetHost, @ev, 300);
+      if (status < 0) then
+      begin
+        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' network error', True);
+        Result := -1;
+        exit;
       end;
-
-      ENET_EVENT_TYPE_RECEIVE:
+      if (status = 0) then
       begin
-        //e_LogWritefln('RECEIVE: chan=%u', [NetEvent.channelID]);
-        if (NetEvent.channelID = NET_CHAN_DOWNLOAD_EX) then
-        begin
-          ProcessDownloadExPacket();
-        end
-        else
+        // check for timeout
+        ct := GetTimerMS();
+        if (ct >= ett) then
         begin
-          ID := Byte(NetEvent.peer^.data^);
-          if ID > High(NetClients) then Exit;
-          TC := @NetClients[ID];
-
-          if NetDump then g_Net_DumpRecvBuffer(NetEvent.packet^.data, NetEvent.packet^.dataLength);
-          g_Net_Host_HandlePacket(TC, NetEvent.packet, g_Net_HostMsgHandler);
+          g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' timeout reached', True);
+          Result := -1;
+          exit;
         end;
-      end;
-
-      ENET_EVENT_TYPE_DISCONNECT:
+      end
+      else
       begin
-        ID := Byte(NetEvent.peer^.data^);
-        if ID > High(NetClients) then Exit;
-        clearNetClient(NetClients[ID]);
-        TC := @NetClients[ID];
-        if TC = nil then Exit;
-
-        if not (TC^.Used) then Exit;
-
-        TP := g_Player_Get(TC^.Player);
-
-        if TP <> nil then
-        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.', TMsgType.Notify);
-          g_Player_Remove(TP.UID);
+        // some event
+        case ev.kind of
+          ENET_EVENT_TYPE_RECEIVE:
+            begin
+              freePacket := true;
+              if (ev.channelID <> NET_CHAN_DOWNLOAD_EX) then
+              begin
+                //e_LogWritefln('g_Net_Wait_MapInfo: skip message from non-transfer channel', []);
+                freePacket := false;
+                g_Net_Client_HandlePacket(ev.packet, g_Net_ClientLightMsgHandler);
+                if (g_Res_received_map_start < 0) then begin result := -666; exit; end;
+              end
+              else
+              begin
+                ett := getNewTimeoutEnd();
+                if (ev.packet.dataLength < 1) then
+                begin
+                  e_LogWritefln('g_Net_Wait_MapInfo: invalid server packet (no data)', []);
+                  Result := -1;
+                  exit;
+                end;
+                Ptr := ev.packet^.data;
+                rMsgId := Byte(Ptr^);
+                e_LogWritefln('g_Net_Wait_MapInfo: got message %u from server (dataLength=%u)', [rMsgId, ev.packet^.dataLength]);
+                if (rMsgId = NTF_SERVER_FILE_INFO) then
+                begin
+                  e_LogWritefln('g_Net_Wait_MapInfo: waiting for map info reply, but got file info reply', []);
+                  Result := -1;
+                  exit;
+                end
+                else if (rMsgId = NTF_SERVER_ABORT) then
+                begin
+                  e_LogWritefln('g_Net_Wait_MapInfo: server aborted transfer', []);
+                  Result := 2;
+                  exit;
+                end
+                else if (rMsgId = NTF_SERVER_MAP_INFO) then
+                begin
+                  e_LogWritefln('g_Net_Wait_MapInfo: creating map info packet...', []);
+                  if not msg.Init(ev.packet^.data+1, ev.packet^.dataLength-1, True) then exit;
+                  e_LogWritefln('g_Net_Wait_MapInfo: parsing map info packet (rd=%d; max=%d)...', [msg.ReadCount, msg.MaxSize]);
+                  resList.Clear();
+                  // map wad name
+                  tf.diskName := msg.ReadString();
+                  e_LogWritefln('g_Net_Wait_MapInfo: map wad is `%s`', [tf.diskName]);
+                  // map wad md5
+                  tf.hash := msg.ReadMD5();
+                  // map wad size
+                  tf.size := msg.ReadLongInt();
+                  e_LogWritefln('g_Net_Wait_MapInfo: map wad size is %d', [tf.size]);
+                  // number of external resources for map
+                  rc := msg.ReadLongInt();
+                  if (rc < 0) or (rc > 1024) then
+                  begin
+                    e_LogWritefln('g_Net_Wait_Event: invalid number of map external resources (%d)', [rc]);
+                    Result := -1;
+                    exit;
+                  end;
+                  e_LogWritefln('g_Net_Wait_MapInfo: map external resource count is %d', [rc]);
+                  // external resource names
+                  for f := 0 to rc-1 do
+                  begin
+                    s := ExtractFileName(msg.ReadString());
+                    if (length(s) = 0) then
+                    begin
+                      Result := -1;
+                      exit;
+                    end;
+                    resList.append(s);
+                  end;
+                  e_LogWritefln('g_Net_Wait_MapInfo: got map info', []);
+                  Result := 0; // success
+                  exit;
+                end
+                else
+                begin
+                  e_LogWritefln('g_Net_Wait_Event: invalid server packet type', []);
+                  Result := -1;
+                  exit;
+                end;
+              end;
+            end;
+          ENET_EVENT_TYPE_DISCONNECT:
+            begin
+              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);
+              Result := -1;
+              exit;
+            end;
+          else
+            begin
+              g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' unknown ENet event ' + IntToStr(Ord(ev.kind)), True);
+              result := -1;
+              exit;
+            end;
         end;
-
-        TC^.Used := False;
-        TC^.State := NET_STATE_NONE;
-        TC^.Peer := nil;
-        TC^.Player := 0;
-        TC^.RequestedFullUpdate := False;
-        TC^.NetOut[NET_UNRELIABLE].Free();
-        TC^.NetOut[NET_RELIABLE].Free();
-
-        FreeMemory(NetEvent.peer^.data);
-        NetEvent.peer^.data := nil;
-        g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST_DISC], [ID]));
-        Dec(NetClientCount);
-
-        if NetUseMaster then g_Net_Slist_Update;
+        if (freePacket) then begin freePacket := false; enet_packet_destroy(ev.packet); end;
       end;
-    end;
+      ProcessLoading();
+      if g_Net_UserRequestExit() then
+      begin
+        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' user abort', True);
+        Result := 1;
+        exit;
+      end;
+    until false;
+  finally
+    if (freePacket) then enet_packet_destroy(ev.packet);
   end;
 end;
 
 
-{ /// CLIENT FUNCTIONS /// }
-
-
-procedure g_Net_Disconnect(Forced: Boolean = False);
-begin
-  if NetMode <> NET_CLIENT then Exit;
-  if (NetHost = nil) or (NetPeer = nil) then Exit;
-
-  if not Forced then
-  begin
-    enet_peer_disconnect(NetPeer, NET_DISC_NONE);
-
-    while (enet_host_service(NetHost, @NetEvent, 1500) > 0) do
-    begin
-      if (NetEvent.kind = ENET_EVENT_TYPE_DISCONNECT) then
-      begin
-        NetPeer := nil;
-        break;
-      end;
-
-      if (NetEvent.kind = ENET_EVENT_TYPE_RECEIVE) then
-        enet_packet_destroy(NetEvent.packet);
-    end;
-
-    if NetPeer <> nil then
-    begin
-      enet_peer_reset(NetPeer);
-      NetPeer := nil;
-    end;
-  end
-  else
-  begin
-    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);
-  end;
-
-  if NetHost <> nil then
-  begin
-    enet_host_destroy(NetHost);
-    NetHost := nil;
-  end;
-  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_CLIENT_DISC]);
-
-  g_Net_Cleanup;
-  e_WriteLog('NET: Disconnected', TMsgType.Notify);
-end;
-
-procedure g_Net_Client_Send(Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
+// send file request to server, and wait for server reply
+//
+// returns `false` on error or user abort
+// fills:
+//    diskName (actually, base name)
+//    hash
+//    size
+//    chunkSize
+// returns:
+//  <0 on error
+//   0 on success
+//   1 on user abort
+//   2 on server abort
+// for maps, first `tf.diskName` name will be map wad name, and `tf.hash`/`tf.size` will contain map info
+function g_Net_RequestResFileInfo (resIndex: LongInt; out tf: TNetFileTransfer): Integer;
 var
-  T: Integer;
+  ev: ENetEvent;
+  rMsgId: Byte;
+  Ptr: Pointer;
+  msg: TMsg;
+  freePacket: Boolean = false;
+  ct, ett: Int64;
+  status: cint;
 begin
-  if (Reliable) then
-    T := NET_RELIABLE
-  else
-    T := NET_UNRELIABLE;
-
-  // write size first
-  NetBuf[T].Write(Integer(NetOut.CurSize));
-  NetBuf[T].Write(NetOut);
-
-  if NetDump then g_Net_DumpSendBuffer();
-  NetOut.Clear();
-  g_Net_Flush(); // FIXME: for now, send immediately
-end;
+  // send request
+  trans_omsg.Clear();
+  trans_omsg.Write(Byte(NTF_CLIENT_FILE_REQUEST));
+  trans_omsg.Write(resIndex);
+  if not ftransSendClientMsg(trans_omsg) then begin result := -1; exit; end;
 
-function g_Net_Client_Update(): enet_size_t;
-begin
-  Result := 0;
-  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
-  begin
-    case NetEvent.kind of
-      ENET_EVENT_TYPE_RECEIVE:
+  FillChar(ev, SizeOf(ev), 0);
+  Result := -1;
+  try
+    ett := getNewTimeoutEnd();
+    repeat
+      status := enet_host_service(NetHost, @ev, 300);
+      if (status < 0) then
       begin
-        if (NetEvent.channelID = NET_CHAN_DOWNLOAD_EX) then continue; // ignore all download packets, they're processed by separate code
-        if NetDump then g_Net_DumpRecvBuffer(NetEvent.packet^.data, NetEvent.packet^.dataLength);
-        g_Net_Client_HandlePacket(NetEvent.packet, g_Net_ClientMsgHandler);
+        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' network error', True);
+        Result := -1;
+        exit;
       end;
-
-      ENET_EVENT_TYPE_DISCONNECT:
+      if (status = 0) then
       begin
-        g_Net_Disconnect(True);
-        Result := 1;
-        Exit;
-      end;
-    end;
-  end
-end;
-
-function g_Net_Client_UpdateWhileLoading(): enet_size_t;
-begin
-  Result := 0;
-  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
-  begin
-    case NetEvent.kind of
-      ENET_EVENT_TYPE_RECEIVE:
+        // check for timeout
+        ct := GetTimerMS();
+        if (ct >= ett) then
+        begin
+          g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' timeout reached', True);
+          Result := -1;
+          exit;
+        end;
+      end
+      else
       begin
-        if (NetEvent.channelID = NET_CHAN_DOWNLOAD_EX) then continue; // ignore all download packets, they're processed by separate code
-        if NetDump then g_Net_DumpRecvBuffer(NetEvent.packet^.data, NetEvent.packet^.dataLength);
-        g_Net_Client_HandlePacket(NetEvent.packet, g_Net_ClientLightMsgHandler);
+        // some event
+        case ev.kind of
+          ENET_EVENT_TYPE_RECEIVE:
+            begin
+              freePacket := true;
+              if (ev.channelID <> NET_CHAN_DOWNLOAD_EX) then
+              begin
+                //e_LogWriteln('g_Net_Wait_Event: skip message from non-transfer channel');
+                freePacket := false;
+                g_Net_Client_HandlePacket(ev.packet, g_Net_ClientLightMsgHandler);
+                if (g_Res_received_map_start < 0) then begin result := -666; exit; end;
+              end
+              else
+              begin
+                ett := getNewTimeoutEnd();
+                if (ev.packet.dataLength < 1) then
+                begin
+                  e_LogWriteln('g_Net_Wait_Event: invalid server packet (no data)');
+                  Result := -1;
+                  exit;
+                end;
+                Ptr := ev.packet^.data;
+                rMsgId := Byte(Ptr^);
+                e_LogWritefln('received transfer packet with id %d (%u bytes)', [rMsgId, ev.packet^.dataLength]);
+                if (rMsgId = NTF_SERVER_FILE_INFO) then
+                begin
+                  if not msg.Init(ev.packet^.data+1, ev.packet^.dataLength-1, True) then exit;
+                  tf.hash := msg.ReadMD5();
+                  tf.size := msg.ReadLongInt();
+                  tf.chunkSize := msg.ReadLongInt();
+                  tf.diskName := ExtractFileName(msg.readString());
+                  if (tf.size < 0) or (tf.chunkSize <> FILE_CHUNK_SIZE) or (length(tf.diskName) = 0) then
+                  begin
+                    e_LogWritefln('g_Net_RequestResFileInfo: invalid file info packet', []);
+                    Result := -1;
+                    exit;
+                  end;
+                  e_LogWritefln('got file info for resource #%d: size=%d; name=%s', [resIndex, tf.size, tf.diskName]);
+                  Result := 0; // success
+                  exit;
+                end
+                else if (rMsgId = NTF_SERVER_ABORT) then
+                begin
+                  e_LogWriteln('g_Net_RequestResFileInfo: server aborted transfer');
+                  Result := 2;
+                  exit;
+                end
+                else if (rMsgId = NTF_SERVER_MAP_INFO) then
+                begin
+                  e_LogWriteln('g_Net_RequestResFileInfo: waiting for map info reply, but got file info reply');
+                  Result := -1;
+                  exit;
+                end
+                else
+                begin
+                  e_LogWriteln('g_Net_RequestResFileInfo: invalid server packet type');
+                  Result := -1;
+                  exit;
+                end;
+              end;
+            end;
+          ENET_EVENT_TYPE_DISCONNECT:
+            begin
+              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);
+              Result := -1;
+              exit;
+            end;
+          else
+            begin
+              g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' unknown ENet event ' + IntToStr(Ord(ev.kind)), True);
+              result := -1;
+              exit;
+            end;
+        end;
+        if (freePacket) then begin freePacket := false; enet_packet_destroy(ev.packet); end;
       end;
-
-      ENET_EVENT_TYPE_DISCONNECT:
+      ProcessLoading();
+      if g_Net_UserRequestExit() then
       begin
-        g_Net_Disconnect(True);
+        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' user abort', True);
         Result := 1;
-        Exit;
+        exit;
       end;
-    end;
+    until false;
+  finally
+    if (freePacket) then enet_packet_destroy(ev.packet);
   end;
-  g_Net_Flush();
 end;
 
-function g_Net_Connect(IP: string; Port: enet_uint16): Boolean;
-var
-  OuterLoop: Boolean;
-  TimeoutTime, T: Int64;
-begin
-  if NetMode <> NET_NONE then
-  begin
-    g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_ERR_INGAME], True);
-    Result := False;
-    Exit;
-  end;
-
-  Result := True;
 
-  g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_CLIENT_CONN],
-    [IP, Port]));
-  if not NetInitDone then
-  begin
-    if (not g_Net_Init()) then
-    begin
-      g_Console_Add(_lc[I_NET_MSG_FERROR] + _lc[I_NET_ERR_ENET], True);
-      Result := False;
-      Exit;
-    end
-    else
-      NetInitDone := True;
-  end;
-
-  NetHost := enet_host_create(nil, 1, NET_CHANS, 0, 0);
-
-  if (NetHost = nil) then
-  begin
-    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CLIENT], True);
-    g_Net_Cleanup;
-    Result := False;
-    Exit;
-  end;
-
-  enet_address_set_host(@NetAddr, PChar(Addr(IP[1])));
-  NetAddr.port := Port;
-
-  NetPeer := enet_host_connect(NetHost, @NetAddr, NET_CHANS, NET_PROTOCOL_VER);
-
-  if (NetPeer = nil) then
-  begin
-    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CLIENT], True);
-    enet_host_destroy(NetHost);
-    g_Net_Cleanup;
-    Result := False;
-    Exit;
-  end;
-
-  // предупредить что ждем слишком долго через N секунд
-  TimeoutTime := GetTimer() + NET_CONNECT_TIMEOUT;
-
-  OuterLoop := True;
-  while OuterLoop do
-  begin
-    while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
-    begin
-      if (NetEvent.kind = ENET_EVENT_TYPE_CONNECT) then
-      begin
-        g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_CLIENT_DONE]);
-        NetMode := NET_CLIENT;
-        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;
-        if NetDump then
-          g_Net_DumpStart();
-        Exit;
-      end;
-    end;
-
-    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;
-
-    ProcessLoading(true);
-
-    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
-    enet_host_destroy(NetHost);
-    NetHost := nil;
-  end;
-  g_Net_Cleanup();
-  Result := False;
-end;
-
-function IpToStr(IP: LongWord): string;
-var
-  Ptr: Pointer;
-begin
-  Ptr := Addr(IP);
-  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;
-var
-  EAddr: ENetAddress;
-begin
-  Result := enet_address_set_host(@EAddr, PChar(@IPstr[1])) = 0;
-  IP := EAddr.host;
-end;
-
-function g_Net_Client_ByName(Name: string): pTNetClient;
-var
-  a: Integer;
-  pl: TPlayer;
-begin
-  Result := nil;
-  for a := Low(NetClients) to High(NetClients) do
-    if (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
-    begin
-      pl := g_Player_Get(NetClients[a].Player);
-      if pl = nil then continue;
-      if Copy(LowerCase(pl.Name), 1, Length(Name)) <> LowerCase(Name) then continue;
-      if NetClients[a].Peer <> nil then
-      begin
-        Result := @NetClients[a];
-        Exit;
-      end;
-    end;
-end;
-
-function g_Net_Client_ByPlayer(PID: Word): pTNetClient;
-var
-  a: Integer;
-begin
-  Result := nil;
-  for a := Low(NetClients) to High(NetClients) do
-    if (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
-      if NetClients[a].Player = PID then
-      begin
-        Result := @NetClients[a];
-        Exit;
-      end;
-end;
-
-function g_Net_ClientName_ByID(ID: Integer): string;
-var
-  a: Integer;
-  pl: TPlayer;
-begin
-  Result := '';
-  if ID = NET_EVERYONE then
-    Exit;
-  for a := Low(NetClients) to High(NetClients) do
-    if (NetClients[a].ID = ID) and (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
-    begin
-      pl := g_Player_Get(NetClients[a].Player);
-      if pl = nil then Exit;
-      Result := pl.Name;
-    end;
-end;
-
-procedure g_Net_SendData(Data: AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
-var
-  P: pENetPacket;
-  F: enet_uint32;
-  dataLength: Cardinal;
-begin
-  dataLength := Length(Data);
-
-  if (Reliable) then
-    F := LongWord(ENET_PACKET_FLAG_RELIABLE)
-  else
-    F := 0;
-
-  if (peer <> nil) then
-  begin
-    P := enet_packet_create(@Data[0], dataLength, F);
-    if not Assigned(P) then Exit;
-    enet_peer_send(peer, Chan, P);
-  end
-  else
-  begin
-    P := enet_packet_create(@Data[0], dataLength, F);
-    if not Assigned(P) then Exit;
-    enet_host_broadcast(NetHost, Chan, P);
-  end;
-
-  enet_host_flush(NetHost);
-end;
-
-function g_Net_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 getNewTimeoutEnd (): Int64;
-begin
-  result := GetTimerMS();
-  if (g_Net_DownloadTimeout <= 0) then
-  begin
-    result := result+1000*60*3; // 3 minutes
-  end
-  else
-  begin
-    result := result+trunc(g_Net_DownloadTimeout*1000);
-  end;
-end;
-
-
-function g_Net_SendMapRequest (): Boolean;
-begin
-  result := false;
-  e_LogWritefln('sending map request...', []);
-  // send request
-  trans_omsg.Clear();
-  trans_omsg.Write(Byte(NTF_CLIENT_MAP_REQUEST));
-  e_LogWritefln('  request size is %d', [trans_omsg.CurSize]);
-  result := ftransSendClientMsg(trans_omsg);
-  if result then enet_host_flush(NetHost);
-end;
+// call this to cancel file transfer requested by `g_Net_RequestResFileInfo()`
+function g_Net_AbortResTransfer (var tf: TNetFileTransfer): Boolean;
+begin
+  result := false;
+  e_LogWritefln('aborting file transfer...', []);
+  // send request
+  trans_omsg.Clear();
+  trans_omsg.Write(Byte(NTF_CLIENT_ABORT));
+  result := ftransSendClientMsg(trans_omsg);
+  if result then enet_host_flush(NetHost);
+end;
 
 
+// call this to start file transfer requested by `g_Net_RequestResFileInfo()`
+//
 // returns `false` on error or user abort
 // fills:
 //    hash
@@ -1498,7 +1012,7 @@ end;
 //   1 on user abort
 //   2 on server abort
 // for maps, first `tf.diskName` name will be map wad name, and `tf.hash`/`tf.size` will contain map info
-function g_Net_Wait_MapInfo (var tf: TNetFileTransfer; resList: TStringList): Integer;
+function g_Net_ReceiveResourceFile (resIndex: LongInt; var tf: TNetFileTransfer; strm: TStream): Integer;
 var
   ev: ENetEvent;
   rMsgId: Byte;
@@ -1507,14 +1021,42 @@ var
   freePacket: Boolean = false;
   ct, ett: Int64;
   status: cint;
-  s: AnsiString;
-  rc, f: LongInt;
+  nextChunk: Integer = 0;
+  chunkTotal: Integer;
+  chunk: Integer;
+  csize: Integer;
+  buf: PChar = nil;
+  resumed: Boolean;
+  //stx: Int64;
 begin
+  tf.resumed := false;
+  e_LogWritefln('file `%s`, size=%d (%d)', [tf.diskName, Integer(strm.size), tf.size], TMsgType.Notify);
+  // check if we should resume downloading
+  resumed := (strm.size > tf.chunkSize) and (strm.size < tf.size);
+  // send request
+  trans_omsg.Clear();
+  trans_omsg.Write(Byte(NTF_CLIENT_START));
+  if resumed then chunk := strm.size div tf.chunkSize else chunk := 0;
+  trans_omsg.Write(LongInt(chunk));
+  if not ftransSendClientMsg(trans_omsg) then begin result := -1; exit; end;
+
+  strm.Seek(chunk*tf.chunkSize, soFromBeginning);
+  chunkTotal := (tf.size+tf.chunkSize-1) div tf.chunkSize;
+  e_LogWritefln('receiving file `%s` (%d chunks)', [tf.diskName, chunkTotal], TMsgType.Notify);
+  g_Game_SetLoadingText('downloading "'+ExtractFileName(tf.diskName)+'"', chunkTotal, False);
+  tf.resumed := resumed;
+
+  if (chunk > 0) then g_Game_StepLoading(chunk);
+  nextChunk := chunk;
+
+  // wait for reply data
   FillChar(ev, SizeOf(ev), 0);
   Result := -1;
+  GetMem(buf, tf.chunkSize);
   try
     ett := getNewTimeoutEnd();
     repeat
+      //stx := -GetTimerMS();
       status := enet_host_service(NetHost, @ev, 300);
       if (status < 0) then
       begin
@@ -1542,79 +1084,73 @@ begin
               freePacket := true;
               if (ev.channelID <> NET_CHAN_DOWNLOAD_EX) then
               begin
-                //e_LogWritefln('g_Net_Wait_MapInfo: skip message from non-transfer channel', []);
+                //e_LogWritefln('g_Net_Wait_Event: skip message from non-transfer channel', []);
                 freePacket := false;
                 g_Net_Client_HandlePacket(ev.packet, g_Net_ClientLightMsgHandler);
                 if (g_Res_received_map_start < 0) then begin result := -666; exit; end;
               end
               else
               begin
+                //stx := stx+GetTimerMS();
+                //e_LogWritefln('g_Net_ReceiveResourceFile: stx=%d', [Integer(stx)]);
+                //stx := -GetTimerMS();
                 ett := getNewTimeoutEnd();
                 if (ev.packet.dataLength < 1) then
                 begin
-                  e_LogWritefln('g_Net_Wait_MapInfo: invalid server packet (no data)', []);
+                  e_LogWritefln('g_Net_ReceiveResourceFile: invalid server packet (no data)', []);
                   Result := -1;
                   exit;
                 end;
                 Ptr := ev.packet^.data;
                 rMsgId := Byte(Ptr^);
-                e_LogWritefln('g_Net_Wait_MapInfo: got message %u from server (dataLength=%u)', [rMsgId, ev.packet^.dataLength]);
-                if (rMsgId = NTF_SERVER_FILE_INFO) then
+                if (rMsgId = NTF_SERVER_DONE) then
                 begin
-                  e_LogWritefln('g_Net_Wait_MapInfo: waiting for map info reply, but got file info reply', []);
-                  Result := -1;
+                  e_LogWritefln('file transfer complete.', []);
+                  result := 0;
                   exit;
                 end
-                else if (rMsgId = NTF_SERVER_ABORT) then
+                else if (rMsgId = NTF_SERVER_CHUNK) then
                 begin
-                  e_LogWritefln('g_Net_Wait_MapInfo: server aborted transfer', []);
-                  Result := 2;
-                  exit;
-                end
-                else if (rMsgId = NTF_SERVER_MAP_INFO) then
-                begin
-                  e_LogWritefln('g_Net_Wait_MapInfo: creating map info packet...', []);
                   if not msg.Init(ev.packet^.data+1, ev.packet^.dataLength-1, True) then exit;
-                  e_LogWritefln('g_Net_Wait_MapInfo: parsing map info packet (rd=%d; max=%d)...', [msg.ReadCount, msg.MaxSize]);
-                  resList.Clear();
-                  // map wad name
-                  tf.diskName := msg.ReadString();
-                  e_LogWritefln('g_Net_Wait_MapInfo: map wad is `%s`', [tf.diskName]);
-                  // map wad md5
-                  tf.hash := msg.ReadMD5();
-                  // map wad size
-                  tf.size := msg.ReadLongInt();
-                  e_LogWritefln('g_Net_Wait_MapInfo: map wad size is %d', [tf.size]);
-                  // number of external resources for map
-                  rc := msg.ReadLongInt();
-                  if (rc < 0) or (rc > 1024) then
+                  chunk := msg.ReadLongInt();
+                  csize := msg.ReadLongInt();
+                  if (chunk <> nextChunk) then
                   begin
-                    e_LogWritefln('g_Net_Wait_Event: invalid number of map external resources (%d)', [rc]);
+                    e_LogWritefln('received chunk %d, but expected chunk %d', [chunk, nextChunk]);
                     Result := -1;
                     exit;
                   end;
-                  e_LogWritefln('g_Net_Wait_MapInfo: map external resource count is %d', [rc]);
-                  // external resource names
-                  for f := 0 to rc-1 do
+                  if (csize < 0) or (csize > tf.chunkSize) then
                   begin
-                    s := ExtractFileName(msg.ReadString());
-                    if (length(s) = 0) then
-                    begin
-                      Result := -1;
-                      exit;
-                    end;
-                    resList.append(s);
+                    e_LogWritefln('received chunk with size %d, but expected chunk size is %d', [csize, tf.chunkSize]);
+                    Result := -1;
+                    exit;
                   end;
-                  e_LogWritefln('g_Net_Wait_MapInfo: got map info', []);
-                  Result := 0; // success
+                  //e_LogWritefln('got chunk #%d of #%d (csize=%d)', [chunk, (tf.size+tf.chunkSize-1) div tf.chunkSize, csize]);
+                  msg.ReadData(buf, csize);
+                  strm.WriteBuffer(buf^, csize);
+                  nextChunk := chunk+1;
+                  g_Game_StepLoading();
+                  // send ack
+                  trans_omsg.Clear();
+                  trans_omsg.Write(Byte(NTF_CLIENT_ACK));
+                  trans_omsg.Write(LongInt(chunk));
+                  if not ftransSendClientMsg(trans_omsg) then begin result := -1; exit; end;
+                end
+                else if (rMsgId = NTF_SERVER_ABORT) then
+                begin
+                  e_LogWritefln('g_Net_ReceiveResourceFile: server aborted transfer', []);
+                  Result := 2;
                   exit;
                 end
                 else
                 begin
-                  e_LogWritefln('g_Net_Wait_Event: invalid server packet type', []);
+                  e_LogWritefln('g_Net_ReceiveResourceFile: invalid server packet type', []);
                   Result := -1;
                   exit;
                 end;
+                //stx := stx+GetTimerMS();
+                //e_LogWritefln('g_Net_ReceiveResourceFile: process stx=%d', [Integer(stx)]);
               end;
             end;
           ENET_EVENT_TYPE_DISCONNECT:
@@ -1642,349 +1178,829 @@ begin
       end;
     until false;
   finally
+    FreeMem(buf);
     if (freePacket) then enet_packet_destroy(ev.packet);
   end;
 end;
 
 
-// returns `false` on error or user abort
-// fills:
-//    diskName (actually, base name)
-//    hash
-//    size
-//    chunkSize
-// returns:
-//  <0 on error
-//   0 on success
-//   1 on user abort
-//   2 on server abort
-// for maps, first `tf.diskName` name will be map wad name, and `tf.hash`/`tf.size` will contain map info
-function g_Net_RequestResFileInfo (resIndex: LongInt; out tf: TNetFileTransfer): Integer;
+//**************************************************************************
+//
+// common functions
+//
+//**************************************************************************
+
+function g_Net_FindSlot(): Integer;
 var
-  ev: ENetEvent;
-  rMsgId: Byte;
-  Ptr: Pointer;
-  msg: TMsg;
-  freePacket: Boolean = false;
-  ct, ett: Int64;
-  status: cint;
+  I: Integer;
+  F: Boolean;
+  N, C: Integer;
 begin
-  // send request
-  trans_omsg.Clear();
-  trans_omsg.Write(Byte(NTF_CLIENT_FILE_REQUEST));
-  trans_omsg.Write(resIndex);
-  if not ftransSendClientMsg(trans_omsg) then begin result := -1; exit; end;
+  N := -1;
+  F := False;
+  C := 0;
+  for I := Low(NetClients) to High(NetClients) do
+  begin
+    if NetClients[I].Used then
+      Inc(C)
+    else
+      if not F then
+      begin
+        F := True;
+        N := I;
+      end;
+  end;
+  if C >= NetMaxClients then
+  begin
+    Result := -1;
+    Exit;
+  end;
 
-  FillChar(ev, SizeOf(ev), 0);
-  Result := -1;
-  try
-    ett := getNewTimeoutEnd();
-    repeat
-      status := enet_host_service(NetHost, @ev, 300);
-      if (status < 0) then
+  if not F then
+  begin
+    if (Length(NetClients) >= NetMaxClients) then
+      N := -1
+    else
+    begin
+      SetLength(NetClients, Length(NetClients) + 1);
+      N := High(NetClients);
+    end;
+  end;
+
+  if N >= 0 then
+  begin
+    NetClients[N].Used := True;
+    NetClients[N].ID := N;
+    NetClients[N].RequestedFullUpdate := False;
+    NetClients[N].RCONAuth := False;
+    NetClients[N].Voted := False;
+    NetClients[N].Player := 0;
+    clearNetClientTransfers(NetClients[N]); // just in case
+  end;
+
+  Result := N;
+end;
+
+function g_Net_Init(): Boolean;
+var
+  F: TextFile;
+  IPstr: string;
+  IP: LongWord;
+begin
+  NetIn.Clear();
+  NetOut.Clear();
+  NetBuf[NET_UNRELIABLE].Clear();
+  NetBuf[NET_RELIABLE].Clear();
+  //SetLength(NetClients, 0);
+  clearNetClients(true); // clear array
+  NetPeer := nil;
+  NetHost := nil;
+  NetMyID := -1;
+  NetPlrUID1 := -1;
+  NetPlrUID2 := -1;
+  NetAddr.port := 25666;
+  SetLength(NetBannedHosts, 0);
+  if FileExists(DataDir + BANLIST_FILENAME) then
+  begin
+    Assign(F, DataDir + BANLIST_FILENAME);
+    Reset(F);
+    while not EOF(F) do
+    begin
+      Readln(F, IPstr);
+      if StrToIp(IPstr, IP) then
+        g_Net_BanHost(IP);
+    end;
+    CloseFile(F);
+    g_Net_SaveBanList();
+  end;
+
+  Result := (enet_initialize() = 0);
+end;
+
+procedure g_Net_Flush();
+var
+  T: Integer;
+  P: pENetPacket;
+  F, Chan: enet_uint32;
+  I: Integer;
+begin
+  F := 0;
+  Chan := NET_CHAN_GAME;
+
+  if NetMode = NET_SERVER then
+    for T := NET_UNRELIABLE to NET_RELIABLE do
+    begin
+      if NetBuf[T].CurSize > 0 then
       begin
-        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' network error', True);
-        Result := -1;
-        exit;
+        P := enet_packet_create(NetBuf[T].Data, NetBuf[T].CurSize, F);
+        if not Assigned(P) then continue;
+        enet_host_broadcast(NetHost, Chan, P);
+        NetBuf[T].Clear();
       end;
-      if (status = 0) then
+
+      for I := Low(NetClients) to High(NetClients) do
       begin
-        // check for timeout
-        ct := GetTimerMS();
-        if (ct >= ett) then
-        begin
-          g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' timeout reached', True);
-          Result := -1;
-          exit;
-        end;
-      end
-      else
+        if not NetClients[I].Used then continue;
+        if NetClients[I].NetOut[T].CurSize <= 0 then continue;
+        P := enet_packet_create(NetClients[I].NetOut[T].Data, NetClients[I].NetOut[T].CurSize, F);
+        if not Assigned(P) then continue;
+        enet_peer_send(NetClients[I].Peer, Chan, P);
+        NetClients[I].NetOut[T].Clear();
+      end;
+
+      // next and last iteration is always RELIABLE
+      F := LongWord(ENET_PACKET_FLAG_RELIABLE);
+      Chan := NET_CHAN_IMPORTANT;
+    end
+  else if NetMode = NET_CLIENT then
+    for T := NET_UNRELIABLE to NET_RELIABLE do
+    begin
+      if NetBuf[T].CurSize > 0 then
       begin
-        // some event
-        case ev.kind of
-          ENET_EVENT_TYPE_RECEIVE:
-            begin
-              freePacket := true;
-              if (ev.channelID <> NET_CHAN_DOWNLOAD_EX) then
-              begin
-                //e_LogWriteln('g_Net_Wait_Event: skip message from non-transfer channel');
-                freePacket := false;
-                g_Net_Client_HandlePacket(ev.packet, g_Net_ClientLightMsgHandler);
-                if (g_Res_received_map_start < 0) then begin result := -666; exit; end;
-              end
-              else
-              begin
-                ett := getNewTimeoutEnd();
-                if (ev.packet.dataLength < 1) then
-                begin
-                  e_LogWriteln('g_Net_Wait_Event: invalid server packet (no data)');
-                  Result := -1;
-                  exit;
-                end;
-                Ptr := ev.packet^.data;
-                rMsgId := Byte(Ptr^);
-                e_LogWritefln('received transfer packet with id %d (%u bytes)', [rMsgId, ev.packet^.dataLength]);
-                if (rMsgId = NTF_SERVER_FILE_INFO) then
-                begin
-                  if not msg.Init(ev.packet^.data+1, ev.packet^.dataLength-1, True) then exit;
-                  tf.hash := msg.ReadMD5();
-                  tf.size := msg.ReadLongInt();
-                  tf.chunkSize := msg.ReadLongInt();
-                  tf.diskName := ExtractFileName(msg.readString());
-                  if (tf.size < 0) or (tf.chunkSize <> FILE_CHUNK_SIZE) or (length(tf.diskName) = 0) then
-                  begin
-                    e_LogWritefln('g_Net_RequestResFileInfo: invalid file info packet', []);
-                    Result := -1;
-                    exit;
-                  end;
-                  e_LogWritefln('got file info for resource #%d: size=%d; name=%s', [resIndex, tf.size, tf.diskName]);
-                  Result := 0; // success
-                  exit;
-                end
-                else if (rMsgId = NTF_SERVER_ABORT) then
-                begin
-                  e_LogWriteln('g_Net_RequestResFileInfo: server aborted transfer');
-                  Result := 2;
-                  exit;
-                end
-                else if (rMsgId = NTF_SERVER_MAP_INFO) then
-                begin
-                  e_LogWriteln('g_Net_RequestResFileInfo: waiting for map info reply, but got file info reply');
-                  Result := -1;
-                  exit;
-                end
-                else
-                begin
-                  e_LogWriteln('g_Net_RequestResFileInfo: invalid server packet type');
-                  Result := -1;
-                  exit;
-                end;
-              end;
-            end;
-          ENET_EVENT_TYPE_DISCONNECT:
-            begin
-              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);
-              Result := -1;
-              exit;
-            end;
-          else
-            begin
-              g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' unknown ENet event ' + IntToStr(Ord(ev.kind)), True);
-              result := -1;
-              exit;
-            end;
+        P := enet_packet_create(NetBuf[T].Data, NetBuf[T].CurSize, F);
+        if not Assigned(P) then continue;
+        enet_peer_send(NetPeer, Chan, P);
+        NetBuf[T].Clear();
+      end;
+      // next and last iteration is always RELIABLE
+      F := LongWord(ENET_PACKET_FLAG_RELIABLE);
+      Chan := NET_CHAN_IMPORTANT;
+    end;
+end;
+
+procedure g_Net_Cleanup();
+begin
+  NetIn.Clear();
+  NetOut.Clear();
+  NetBuf[NET_UNRELIABLE].Clear();
+  NetBuf[NET_RELIABLE].Clear();
+
+  //SetLength(NetClients, 0);
+  clearNetClients(true); // clear array
+  NetClientCount := 0;
+
+  NetPeer := nil;
+  NetHost := nil;
+  NetMPeer := nil;
+  NetMHost := nil;
+  NetMyID := -1;
+  NetPlrUID1 := -1;
+  NetPlrUID2 := -1;
+  NetState := NET_STATE_NONE;
+
+  NetPongSock := ENET_SOCKET_NULL;
+
+  NetTimeToMaster := 0;
+  NetTimeToUpdate := 0;
+  NetTimeToReliable := 0;
+
+  NetMode := NET_NONE;
+
+  if NetPortThread <> NilThreadId then
+    WaitForThreadTerminate(NetPortThread, 66666);
+
+  NetPortThread := NilThreadId;
+  g_Net_UnforwardPorts();
+
+  if NetDump then
+    g_Net_DumpEnd();
+end;
+
+procedure g_Net_Free();
+begin
+  g_Net_Cleanup();
+
+  enet_deinitialize();
+  NetInitDone := False;
+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
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_INGAME]);
+    Result := False;
+    Exit;
+  end;
+
+  Result := True;
+
+  g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST], [Port]));
+  if not NetInitDone then
+  begin
+    if (not g_Net_Init()) then
+    begin
+      g_Console_Add(_lc[I_NET_MSG_FERROR] + _lc[I_NET_ERR_ENET]);
+      Result := False;
+      Exit;
+    end
+    else
+      NetInitDone := True;
+  end;
+
+  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
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + Format(_lc[I_NET_ERR_HOST], [Port]));
+    Result := False;
+    g_Net_Cleanup;
+    Exit;
+  end;
+
+  NetPongSock := enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+  if NetPongSock <> ENET_SOCKET_NULL then
+  begin
+    NetPongAddr.host := IPAddr;
+    NetPongAddr.port := NET_PING_PORT;
+    if enet_socket_bind(NetPongSock, @NetPongAddr) < 0 then
+    begin
+      enet_socket_destroy(NetPongSock);
+      NetPongSock := ENET_SOCKET_NULL;
+    end
+    else
+      enet_socket_set_option(NetPongSock, ENET_SOCKOPT_NONBLOCK, 1);
+  end;
+
+  NetMode := NET_SERVER;
+  NetOut.Clear();
+  NetBuf[NET_UNRELIABLE].Clear();
+  NetBuf[NET_RELIABLE].Clear();
+
+  if NetDump then
+    g_Net_DumpStart();
+end;
+
+procedure g_Net_Host_Die();
+var
+  I: Integer;
+begin
+  if NetMode <> NET_SERVER then Exit;
+
+  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_DISCALL]);
+  for I := 0 to High(NetClients) do
+    if NetClients[I].Used then
+      enet_peer_disconnect(NetClients[I].Peer, NET_DISC_DOWN);
+
+  while enet_host_service(NetHost, @NetEvent, 1000) > 0 do
+    if NetEvent.kind = ENET_EVENT_TYPE_RECEIVE then
+      enet_packet_destroy(NetEvent.packet);
+
+  for I := 0 to High(NetClients) do
+    if NetClients[I].Used then
+    begin
+      FreeMemory(NetClients[I].Peer^.data);
+      NetClients[I].Peer^.data := nil;
+      enet_peer_reset(NetClients[I].Peer);
+      NetClients[I].Peer := nil;
+      NetClients[I].Used := False;
+      NetClients[I].NetOut[NET_UNRELIABLE].Free();
+      NetClients[I].NetOut[NET_RELIABLE].Free();
+    end;
+
+  clearNetClients(false); // don't clear array
+  if (NetMPeer <> nil) and (NetMHost <> nil) then g_Net_Slist_Disconnect;
+  if NetPongSock <> ENET_SOCKET_NULL then
+    enet_socket_destroy(NetPongSock);
+
+  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_DIE]);
+  enet_host_destroy(NetHost);
+
+  NetMode := NET_NONE;
+
+  g_Net_Cleanup;
+  e_WriteLog('NET: Server stopped', TMsgType.Notify);
+end;
+
+
+procedure g_Net_Host_Send(ID: Integer; Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
+var
+  T: Integer;
+begin
+  if (Reliable) then
+    T := NET_RELIABLE
+  else
+    T := NET_UNRELIABLE;
+
+  if (ID >= 0) then
+  begin
+    if ID > High(NetClients) then Exit;
+    if NetClients[ID].Peer = nil then Exit;
+    // write size first
+    NetClients[ID].NetOut[T].Write(Integer(NetOut.CurSize));
+    NetClients[ID].NetOut[T].Write(NetOut);
+  end
+  else
+  begin
+    // write size first
+    NetBuf[T].Write(Integer(NetOut.CurSize));
+    NetBuf[T].Write(NetOut);
+  end;
+
+  if NetDump then g_Net_DumpSendBuffer();
+  NetOut.Clear();
+end;
+
+procedure g_Net_Host_CheckPings();
+var
+  ClAddr: ENetAddress;
+  Buf: ENetBuffer;
+  Len: Integer;
+  ClTime: Int64;
+  Ping: array [0..9] of Byte;
+  NPl: Byte;
+begin
+  if NetPongSock = ENET_SOCKET_NULL then Exit;
+
+  Buf.data := Addr(Ping[0]);
+  Buf.dataLength := 2+8;
+
+  Ping[0] := 0;
+
+  Len := enet_socket_receive(NetPongSock, @ClAddr, @Buf, 1);
+  if Len < 0 then Exit;
+
+  if (Ping[0] = Ord('D')) and (Ping[1] = Ord('F')) then
+  begin
+    ClTime := Int64(Addr(Ping[2])^);
+
+    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);
+    NetOut.Write(NPl);
+    NetOut.Write(gNumBots);
+
+    Buf.data := NetOut.Data;
+    Buf.dataLength := NetOut.CurSize;
+    enet_socket_send(NetPongSock, @ClAddr, @Buf, 1);
+
+    NetOut.Clear();
+  end;
+end;
+
+
+function g_Net_Host_Update(): enet_size_t;
+var
+  IP: string;
+  Port: Word;
+  ID: Integer;
+  TC: pTNetClient;
+  TP: TPlayer;
+begin
+  IP := '';
+  Result := 0;
+
+  if NetUseMaster then g_Net_Slist_Check;
+  g_Net_Host_CheckPings;
+
+  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+  begin
+    case (NetEvent.kind) of
+      ENET_EVENT_TYPE_CONNECT:
+      begin
+        IP := IpToStr(NetEvent.Peer^.address.host);
+        Port := NetEvent.Peer^.address.port;
+        g_Console_Add(_lc[I_NET_MSG] +
+          Format(_lc[I_NET_MSG_HOST_CONN], [IP, Port]));
+
+        if (NetEvent.data <> NET_PROTOCOL_VER) then
+        begin
+          g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
+            _lc[I_NET_DISC_PROTOCOL]);
+          NetEvent.peer^.data := GetMemory(SizeOf(Byte));
+          Byte(NetEvent.peer^.data^) := 255;
+          enet_peer_disconnect(NetEvent.peer, NET_DISC_PROTOCOL);
+          enet_host_flush(NetHost);
+          Exit;
         end;
-        if (freePacket) then begin freePacket := false; enet_packet_destroy(ev.packet); end;
+
+        ID := g_Net_FindSlot();
+
+        if ID < 0 then
+        begin
+          g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
+            _lc[I_NET_DISC_FULL]);
+          NetEvent.Peer^.data := GetMemory(SizeOf(Byte));
+          Byte(NetEvent.peer^.data^) := 255;
+          enet_peer_disconnect(NetEvent.peer, NET_DISC_FULL);
+          enet_host_flush(NetHost);
+          Exit;
+        end;
+
+        NetClients[ID].Peer := NetEvent.peer;
+        NetClients[ID].Peer^.data := GetMemory(SizeOf(Byte));
+        Byte(NetClients[ID].Peer^.data^) := ID;
+        NetClients[ID].State := NET_STATE_AUTH;
+        NetClients[ID].RCONAuth := False;
+        NetClients[ID].NetOut[NET_UNRELIABLE].Alloc(NET_BUFSIZE*2);
+        NetClients[ID].NetOut[NET_RELIABLE].Alloc(NET_BUFSIZE*2);
+        clearNetClientTransfers(NetClients[ID]); // just in case
+
+        enet_peer_timeout(NetEvent.peer, ENET_PEER_TIMEOUT_LIMIT * 2, ENET_PEER_TIMEOUT_MINIMUM * 2, ENET_PEER_TIMEOUT_MAXIMUM * 2);
+
+        Inc(NetClientCount);
+        g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST_ADD], [ID]));
+      end;
+
+      ENET_EVENT_TYPE_RECEIVE:
+      begin
+        //e_LogWritefln('RECEIVE: chan=%u', [NetEvent.channelID]);
+        if (NetEvent.channelID = NET_CHAN_DOWNLOAD_EX) then
+        begin
+          ProcessDownloadExPacket();
+        end
+        else
+        begin
+          ID := Byte(NetEvent.peer^.data^);
+          if ID > High(NetClients) then Exit;
+          TC := @NetClients[ID];
+
+          if NetDump then g_Net_DumpRecvBuffer(NetEvent.packet^.data, NetEvent.packet^.dataLength);
+          g_Net_Host_HandlePacket(TC, NetEvent.packet, g_Net_HostMsgHandler);
+        end;
+      end;
+
+      ENET_EVENT_TYPE_DISCONNECT:
+      begin
+        ID := Byte(NetEvent.peer^.data^);
+        if ID > High(NetClients) then Exit;
+        clearNetClient(NetClients[ID]);
+        TC := @NetClients[ID];
+        if TC = nil then Exit;
+
+        if not (TC^.Used) then Exit;
+
+        TP := g_Player_Get(TC^.Player);
+
+        if TP <> nil then
+        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.', TMsgType.Notify);
+          g_Player_Remove(TP.UID);
+        end;
+
+        TC^.Used := False;
+        TC^.State := NET_STATE_NONE;
+        TC^.Peer := nil;
+        TC^.Player := 0;
+        TC^.RequestedFullUpdate := False;
+        TC^.NetOut[NET_UNRELIABLE].Free();
+        TC^.NetOut[NET_RELIABLE].Free();
+
+        FreeMemory(NetEvent.peer^.data);
+        NetEvent.peer^.data := nil;
+        g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST_DISC], [ID]));
+        Dec(NetClientCount);
+
+        if NetUseMaster then g_Net_Slist_Update;
       end;
-      ProcessLoading();
-      if g_Net_UserRequestExit() then
+    end;
+  end;
+end;
+
+
+//**************************************************************************
+//
+// CLIENT FUNCTIONS
+//
+//**************************************************************************
+
+procedure g_Net_Disconnect(Forced: Boolean = False);
+begin
+  if NetMode <> NET_CLIENT then Exit;
+  if (NetHost = nil) or (NetPeer = nil) then Exit;
+
+  if not Forced then
+  begin
+    enet_peer_disconnect(NetPeer, NET_DISC_NONE);
+
+    while (enet_host_service(NetHost, @NetEvent, 1500) > 0) do
+    begin
+      if (NetEvent.kind = ENET_EVENT_TYPE_DISCONNECT) then
+      begin
+        NetPeer := nil;
+        break;
+      end;
+
+      if (NetEvent.kind = ENET_EVENT_TYPE_RECEIVE) then
+        enet_packet_destroy(NetEvent.packet);
+    end;
+
+    if NetPeer <> nil then
+    begin
+      enet_peer_reset(NetPeer);
+      NetPeer := nil;
+    end;
+  end
+  else
+  begin
+    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);
+  end;
+
+  if NetHost <> nil then
+  begin
+    enet_host_destroy(NetHost);
+    NetHost := nil;
+  end;
+  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_CLIENT_DISC]);
+
+  g_Net_Cleanup;
+  e_WriteLog('NET: Disconnected', TMsgType.Notify);
+end;
+
+procedure g_Net_Client_Send(Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
+var
+  T: Integer;
+begin
+  if (Reliable) then
+    T := NET_RELIABLE
+  else
+    T := NET_UNRELIABLE;
+
+  // write size first
+  NetBuf[T].Write(Integer(NetOut.CurSize));
+  NetBuf[T].Write(NetOut);
+
+  if NetDump then g_Net_DumpSendBuffer();
+  NetOut.Clear();
+  g_Net_Flush(); // FIXME: for now, send immediately
+end;
+
+function g_Net_Client_Update(): enet_size_t;
+begin
+  Result := 0;
+  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+  begin
+    case NetEvent.kind of
+      ENET_EVENT_TYPE_RECEIVE:
+      begin
+        if (NetEvent.channelID = NET_CHAN_DOWNLOAD_EX) then continue; // ignore all download packets, they're processed by separate code
+        if NetDump then g_Net_DumpRecvBuffer(NetEvent.packet^.data, NetEvent.packet^.dataLength);
+        g_Net_Client_HandlePacket(NetEvent.packet, g_Net_ClientMsgHandler);
+      end;
+
+      ENET_EVENT_TYPE_DISCONNECT:
+      begin
+        g_Net_Disconnect(True);
+        Result := 1;
+        Exit;
+      end;
+    end;
+  end
+end;
+
+function g_Net_Client_UpdateWhileLoading(): enet_size_t;
+begin
+  Result := 0;
+  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+  begin
+    case NetEvent.kind of
+      ENET_EVENT_TYPE_RECEIVE:
+      begin
+        if (NetEvent.channelID = NET_CHAN_DOWNLOAD_EX) then continue; // ignore all download packets, they're processed by separate code
+        if NetDump then g_Net_DumpRecvBuffer(NetEvent.packet^.data, NetEvent.packet^.dataLength);
+        g_Net_Client_HandlePacket(NetEvent.packet, g_Net_ClientLightMsgHandler);
+      end;
+
+      ENET_EVENT_TYPE_DISCONNECT:
+      begin
+        g_Net_Disconnect(True);
+        Result := 1;
+        Exit;
+      end;
+    end;
+  end;
+  g_Net_Flush();
+end;
+
+function g_Net_Connect(IP: string; Port: enet_uint16): Boolean;
+var
+  OuterLoop: Boolean;
+  TimeoutTime, T: Int64;
+begin
+  if NetMode <> NET_NONE then
+  begin
+    g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_ERR_INGAME], True);
+    Result := False;
+    Exit;
+  end;
+
+  Result := True;
+
+  g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_CLIENT_CONN],
+    [IP, Port]));
+  if not NetInitDone then
+  begin
+    if (not g_Net_Init()) then
+    begin
+      g_Console_Add(_lc[I_NET_MSG_FERROR] + _lc[I_NET_ERR_ENET], True);
+      Result := False;
+      Exit;
+    end
+    else
+      NetInitDone := True;
+  end;
+
+  NetHost := enet_host_create(nil, 1, NET_CHANS, 0, 0);
+
+  if (NetHost = nil) then
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CLIENT], True);
+    g_Net_Cleanup;
+    Result := False;
+    Exit;
+  end;
+
+  enet_address_set_host(@NetAddr, PChar(Addr(IP[1])));
+  NetAddr.port := Port;
+
+  NetPeer := enet_host_connect(NetHost, @NetAddr, NET_CHANS, NET_PROTOCOL_VER);
+
+  if (NetPeer = nil) then
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CLIENT], True);
+    enet_host_destroy(NetHost);
+    g_Net_Cleanup;
+    Result := False;
+    Exit;
+  end;
+
+  // предупредить что ждем слишком долго через N секунд
+  TimeoutTime := GetTimer() + NET_CONNECT_TIMEOUT;
+
+  OuterLoop := True;
+  while OuterLoop do
+  begin
+    while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+    begin
+      if (NetEvent.kind = ENET_EVENT_TYPE_CONNECT) then
+      begin
+        g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_CLIENT_DONE]);
+        NetMode := NET_CLIENT;
+        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;
+        if NetDump then
+          g_Net_DumpStart();
+        Exit;
+      end;
+    end;
+
+    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;
+
+    ProcessLoading(true);
+
+    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
+    enet_host_destroy(NetHost);
+    NetHost := nil;
+  end;
+  g_Net_Cleanup();
+  Result := False;
+end;
+
+function IpToStr(IP: LongWord): string;
+var
+  Ptr: Pointer;
+begin
+  Ptr := Addr(IP);
+  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;
+var
+  EAddr: ENetAddress;
+begin
+  Result := enet_address_set_host(@EAddr, PChar(@IPstr[1])) = 0;
+  IP := EAddr.host;
+end;
+
+function g_Net_Client_ByName(Name: string): pTNetClient;
+var
+  a: Integer;
+  pl: TPlayer;
+begin
+  Result := nil;
+  for a := Low(NetClients) to High(NetClients) do
+    if (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
+    begin
+      pl := g_Player_Get(NetClients[a].Player);
+      if pl = nil then continue;
+      if Copy(LowerCase(pl.Name), 1, Length(Name)) <> LowerCase(Name) then continue;
+      if NetClients[a].Peer <> nil then
       begin
-        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' user abort', True);
-        Result := 1;
-        exit;
+        Result := @NetClients[a];
+        Exit;
       end;
-    until false;
-  finally
-    if (freePacket) then enet_packet_destroy(ev.packet);
-  end;
+    end;
 end;
 
-
-function g_Net_AbortResTransfer (var tf: TNetFileTransfer): Boolean;
+function g_Net_Client_ByPlayer(PID: Word): pTNetClient;
+var
+  a: Integer;
 begin
-  result := false;
-  e_LogWritefln('aborting file transfer...', []);
-  // send request
-  trans_omsg.Clear();
-  trans_omsg.Write(Byte(NTF_CLIENT_ABORT));
-  result := ftransSendClientMsg(trans_omsg);
-  if result then enet_host_flush(NetHost);
+  Result := nil;
+  for a := Low(NetClients) to High(NetClients) do
+    if (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
+      if NetClients[a].Player = PID then
+      begin
+        Result := @NetClients[a];
+        Exit;
+      end;
 end;
 
-
-// returns `false` on error or user abort
-// fills:
-//    hash
-//    size
-//    chunkSize
-// returns:
-//  <0 on error
-//   0 on success
-//   1 on user abort
-//   2 on server abort
-// for maps, first `tf.diskName` name will be map wad name, and `tf.hash`/`tf.size` will contain map info
-function g_Net_ReceiveResourceFile (resIndex: LongInt; var tf: TNetFileTransfer; strm: TStream): Integer;
+function g_Net_ClientName_ByID(ID: Integer): string;
 var
-  ev: ENetEvent;
-  rMsgId: Byte;
-  Ptr: Pointer;
-  msg: TMsg;
-  freePacket: Boolean = false;
-  ct, ett: Int64;
-  status: cint;
-  nextChunk: Integer = 0;
-  chunkTotal: Integer;
-  chunk: Integer;
-  csize: Integer;
-  buf: PChar = nil;
-  resumed: Boolean;
-  //stx: Int64;
+  a: Integer;
+  pl: TPlayer;
 begin
-  tf.resumed := false;
-  e_LogWritefln('file `%s`, size=%d (%d)', [tf.diskName, Integer(strm.size), tf.size], TMsgType.Notify);
-  // check if we should resume downloading
-  resumed := (strm.size > tf.chunkSize) and (strm.size < tf.size);
-  // send request
-  trans_omsg.Clear();
-  trans_omsg.Write(Byte(NTF_CLIENT_START));
-  if resumed then chunk := strm.size div tf.chunkSize else chunk := 0;
-  trans_omsg.Write(LongInt(chunk));
-  if not ftransSendClientMsg(trans_omsg) then begin result := -1; exit; end;
+  Result := '';
+  if ID = NET_EVERYONE then
+    Exit;
+  for a := Low(NetClients) to High(NetClients) do
+    if (NetClients[a].ID = ID) and (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
+    begin
+      pl := g_Player_Get(NetClients[a].Player);
+      if pl = nil then Exit;
+      Result := pl.Name;
+    end;
+end;
 
-  strm.Seek(chunk*tf.chunkSize, soFromBeginning);
-  chunkTotal := (tf.size+tf.chunkSize-1) div tf.chunkSize;
-  e_LogWritefln('receiving file `%s` (%d chunks)', [tf.diskName, chunkTotal], TMsgType.Notify);
-  g_Game_SetLoadingText('downloading "'+ExtractFileName(tf.diskName)+'"', chunkTotal, False);
-  tf.resumed := resumed;
+procedure g_Net_SendData(Data: AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
+var
+  P: pENetPacket;
+  F: enet_uint32;
+  dataLength: Cardinal;
+begin
+  dataLength := Length(Data);
 
-  if (chunk > 0) then g_Game_StepLoading(chunk);
-  nextChunk := chunk;
+  if (Reliable) then
+    F := LongWord(ENET_PACKET_FLAG_RELIABLE)
+  else
+    F := 0;
 
-  // wait for reply data
-  FillChar(ev, SizeOf(ev), 0);
-  Result := -1;
-  GetMem(buf, tf.chunkSize);
-  try
-    ett := getNewTimeoutEnd();
-    repeat
-      //stx := -GetTimerMS();
-      status := enet_host_service(NetHost, @ev, 300);
-      if (status < 0) then
-      begin
-        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' network error', True);
-        Result := -1;
-        exit;
-      end;
-      if (status = 0) then
-      begin
-        // check for timeout
-        ct := GetTimerMS();
-        if (ct >= ett) then
-        begin
-          g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' timeout reached', True);
-          Result := -1;
-          exit;
-        end;
-      end
-      else
-      begin
-        // some event
-        case ev.kind of
-          ENET_EVENT_TYPE_RECEIVE:
-            begin
-              freePacket := true;
-              if (ev.channelID <> NET_CHAN_DOWNLOAD_EX) then
-              begin
-                //e_LogWritefln('g_Net_Wait_Event: skip message from non-transfer channel', []);
-                freePacket := false;
-                g_Net_Client_HandlePacket(ev.packet, g_Net_ClientLightMsgHandler);
-                if (g_Res_received_map_start < 0) then begin result := -666; exit; end;
-              end
-              else
-              begin
-                //stx := stx+GetTimerMS();
-                //e_LogWritefln('g_Net_ReceiveResourceFile: stx=%d', [Integer(stx)]);
-                //stx := -GetTimerMS();
-                ett := getNewTimeoutEnd();
-                if (ev.packet.dataLength < 1) then
-                begin
-                  e_LogWritefln('g_Net_ReceiveResourceFile: invalid server packet (no data)', []);
-                  Result := -1;
-                  exit;
-                end;
-                Ptr := ev.packet^.data;
-                rMsgId := Byte(Ptr^);
-                if (rMsgId = NTF_SERVER_DONE) then
-                begin
-                  e_LogWritefln('file transfer complete.', []);
-                  result := 0;
-                  exit;
-                end
-                else if (rMsgId = NTF_SERVER_CHUNK) then
-                begin
-                  if not msg.Init(ev.packet^.data+1, ev.packet^.dataLength-1, True) then exit;
-                  chunk := msg.ReadLongInt();
-                  csize := msg.ReadLongInt();
-                  if (chunk <> nextChunk) then
-                  begin
-                    e_LogWritefln('received chunk %d, but expected chunk %d', [chunk, nextChunk]);
-                    Result := -1;
-                    exit;
-                  end;
-                  if (csize < 0) or (csize > tf.chunkSize) then
-                  begin
-                    e_LogWritefln('received chunk with size %d, but expected chunk size is %d', [csize, tf.chunkSize]);
-                    Result := -1;
-                    exit;
-                  end;
-                  //e_LogWritefln('got chunk #%d of #%d (csize=%d)', [chunk, (tf.size+tf.chunkSize-1) div tf.chunkSize, csize]);
-                  msg.ReadData(buf, csize);
-                  strm.WriteBuffer(buf^, csize);
-                  nextChunk := chunk+1;
-                  g_Game_StepLoading();
-                  // send ack
-                  trans_omsg.Clear();
-                  trans_omsg.Write(Byte(NTF_CLIENT_ACK));
-                  trans_omsg.Write(LongInt(chunk));
-                  if not ftransSendClientMsg(trans_omsg) then begin result := -1; exit; end;
-                end
-                else if (rMsgId = NTF_SERVER_ABORT) then
-                begin
-                  e_LogWritefln('g_Net_ReceiveResourceFile: server aborted transfer', []);
-                  Result := 2;
-                  exit;
-                end
-                else
-                begin
-                  e_LogWritefln('g_Net_ReceiveResourceFile: invalid server packet type', []);
-                  Result := -1;
-                  exit;
-                end;
-                //stx := stx+GetTimerMS();
-                //e_LogWritefln('g_Net_ReceiveResourceFile: process stx=%d', [Integer(stx)]);
-              end;
-            end;
-          ENET_EVENT_TYPE_DISCONNECT:
-            begin
-              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);
-              Result := -1;
-              exit;
-            end;
-          else
-            begin
-              g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' unknown ENet event ' + IntToStr(Ord(ev.kind)), True);
-              result := -1;
-              exit;
-            end;
-        end;
-        if (freePacket) then begin freePacket := false; enet_packet_destroy(ev.packet); end;
-      end;
-      ProcessLoading();
-      if g_Net_UserRequestExit() then
-      begin
-        g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' user abort', True);
-        Result := 1;
-        exit;
-      end;
-    until false;
-  finally
-    FreeMem(buf);
-    if (freePacket) then enet_packet_destroy(ev.packet);
+  if (peer <> nil) then
+  begin
+    P := enet_packet_create(@Data[0], dataLength, F);
+    if not Assigned(P) then Exit;
+    enet_peer_send(peer, Chan, P);
+  end
+  else
+  begin
+    P := enet_packet_create(@Data[0], dataLength, F);
+    if not Assigned(P) then Exit;
+    enet_host_broadcast(NetHost, Chan, P);
   end;
-end;
 
+  enet_host_flush(NetHost);
+end;
 
 function g_Net_IsHostBanned(IP: LongWord; Perm: Boolean = False): Boolean;
 var
@@ -2249,6 +2265,7 @@ begin
 end;
 {$ENDIF}
 
+
 initialization
   conRegVar('cl_downloadtimeout', @g_Net_DownloadTimeout, 0.0, 1000000.0, '', 'timeout in seconds, 0 to disable it');
   SetLength(NetClients, 0);
index 098e0dd706313aa02bc4a6037541bd241f66f21b..68ceb0a71aa8be99baf763cbf8d98ba5b8ad366c 100644 (file)
@@ -427,10 +427,11 @@ begin
     g_Console_Add(Format(_lc[I_NET_MAP_DL], [FileName]));
     e_WriteLog('Downloading map `' + FileName + '` from server', TMsgType.Notify);
     g_Game_SetLoadingText(FileName + '...', 0, False);
-    if (not g_Net_SendMapRequest()) then exit;
 
     FileName := ExtractFileName(FileName);
     if (length(FileName) = 0) then FileName := 'fucked_map_wad.wad';
+
+    // this also sends map request
     res := g_Net_Wait_MapInfo(tf, resList);
     if (res <> 0) then exit;