DEADSOFTWARE

net: game: other: hash database and resource downloader converted to new dirsys
[d2df-sdl.git] / src / game / g_net.pas
index d3e67cfd6ef34c4ad3fdfc9cc0ed8ab1c1f6b2f4..2e00600ae3ffea0f7dc03ddbe2655724718bba28 100644 (file)
@@ -72,13 +72,21 @@ const
   BANLIST_FILENAME = 'banlist.txt';
   NETDUMP_FILENAME = 'netdump';
 
-  {$IFDEF FREEBSD}
+  {$IF DEFINED(FREEBSD) OR DEFINED(DARWIN)}
     NilThreadId = nil;
   {$ELSE}
     NilThreadId = 0;
   {$ENDIF}
 
 type
+  TNetMapResourceInfo = record
+    wadName: AnsiString; // wad file name, without a path
+    size: Integer; // wad file size (-1: size and hash are not known)
+    hash: TMD5Digest; // wad hash
+  end;
+
+  TNetMapResourceInfoArray = array of TNetMapResourceInfo;
+
   TNetFileTransfer = record
     diskName: string;
     hash: TMD5Digest;
@@ -100,6 +108,7 @@ type
     Peer:     pENetPeer;
     Player:   Word;
     RequestedFullUpdate: Boolean;
+    WaitForFirstSpawn: Boolean; // set to `true` in server, used to spawn a player on first full state request
     RCONAuth: Boolean;
     Voted:    Boolean;
     Transfer: TNetFileTransfer; // only one transfer may be active
@@ -138,9 +147,9 @@ var
   NetPongSock:   ENetSocket = ENET_SOCKET_NULL;
 
   NetUseMaster: Boolean = True;
-  NetSlistAddr: ENetAddress;
   NetSlistIP:   string = 'mpms.doom2d.org';
   NetSlistPort: Word = 25665;
+  NetSlistList: string = 'deadsoftware.ru:25665';
 
   NetClientIP:   string = '127.0.0.1';
   NetClientPort: Word   = 25666;
@@ -230,28 +239,60 @@ procedure g_Net_UnforwardPorts();
 
 function g_Net_UserRequestExit: Boolean;
 
-function g_Net_Wait_MapInfo (var tf: TNetFileTransfer; resList: TStringList): Integer;
+function g_Net_Wait_MapInfo (var tf: TNetFileTransfer; var resList: TNetMapResourceInfoArray): Integer;
 function g_Net_RequestResFileInfo (resIndex: LongInt; out tf: TNetFileTransfer): Integer;
 function g_Net_AbortResTransfer (var tf: TNetFileTransfer): Boolean;
 function g_Net_ReceiveResourceFile (resIndex: LongInt; var tf: TNetFileTransfer; strm: TStream): Integer;
 
+function g_Net_IsNetworkAvailable (): Boolean;
+procedure g_Net_InitLowLevel ();
+procedure g_Net_DeinitLowLevel ();
+
 
 implementation
 
+// *enet_host_service()*
+// fuck! https://www.mail-archive.com/enet-discuss@cubik.org/msg00852.html
+// tl;dr: on shitdows, we can get -1 sometimes, and it is *NOT* a failure.
+//        thank you, enet. let's ignore failures altogether then.
+
 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, ctypes,
+  e_input, e_res,
+  g_nethandler, g_netmsg, g_netmaster, g_player, g_window, g_console,
+  g_main, g_game, g_language, g_weapons, utils, ctypes, g_system,
   g_map;
 
 const
   FILE_CHUNK_SIZE = 8192;
 
 var
+  enet_init_success: Boolean = false;
   g_Net_DownloadTimeout: Single;
   trans_omsg: TMsg;
 
 
+function g_Net_IsNetworkAvailable (): Boolean;
+begin
+  result := enet_init_success;
+end;
+
+procedure g_Net_InitLowLevel ();
+begin
+  if enet_init_success then raise Exception.Create('wuta?!');
+  enet_init_success := (enet_initialize() = 0);
+end;
+
+procedure g_Net_DeinitLowLevel ();
+begin
+  if enet_init_success then
+  begin
+    enet_deinitialize();
+    enet_init_success := false;
+  end;
+end;
+
+
 //**************************************************************************
 //
 // SERVICE FUNCTIONS
@@ -323,6 +364,7 @@ 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);
+  g_Net_Slist_ServerPlayerLeaves();
 end;
 
 
@@ -433,8 +475,9 @@ var
   ridx: Integer;
   dfn: AnsiString;
   md5: TMD5Digest;
-  st: TStream;
+  //st: TStream;
   size: LongInt;
+  fi: TDiskFileInfo;
 begin
   // find client index by peer
   for f := Low(NetClients) to High(NetClients) do
@@ -483,13 +526,13 @@ begin
         end;
         // get resource index
         ridx := msg.ReadLongInt();
-        if (ridx < -1) or (ridx >= gExternalResources.Count) then
+        if (ridx < -1) or (ridx >= length(gExternalResources)) then
         begin
           e_LogWritefln('Invalid resource index %d', [ridx], TMsgType.Warning);
           killClientByFT(nc^);
           exit;
         end;
-        if (ridx < 0) then fname := MapsDir+gGameSettings.WAD else fname := GameDir+'/wads/'+gExternalResources[ridx];
+        if (ridx < 0) then fname := gGameSettings.WAD else fname := gExternalResources[ridx].diskName;
         if (length(fname) = 0) then
         begin
           e_WriteLog('Invalid filename: '+fname, TMsgType.Warning);
@@ -497,7 +540,6 @@ begin
           exit;
         end;
         tf.diskName := findDiskWad(fname);
-        //if (length(tf.diskName) = 0) then tf.diskName := findDiskWad(GameDir+'/wads/'+fname);
         if (length(tf.diskName) = 0) then
         begin
           e_LogWritefln('NETWORK: file "%s" not found!', [fname], TMsgType.Fatal);
@@ -505,8 +547,8 @@ begin
           exit;
         end;
         // calculate hash
-        //TODO: cache hashes
-        tf.hash := MD5File(tf.diskName);
+        //tf.hash := MD5File(tf.diskName);
+        if (ridx < 0) then tf.hash := gWADHash else tf.hash := gExternalResources[ridx].hash;
         // create file stream
         tf.diskName := findDiskWad(fname);
         try
@@ -619,13 +661,23 @@ begin
       begin
         e_LogWritefln('client #%d requested map info', [nc.ID]);
         trans_omsg.Clear();
-        dfn := findDiskWad(MapsDir+gGameSettings.WAD);
+        dfn := findDiskWad(gGameSettings.WAD);
         if (dfn = '') then dfn := '!wad_not_found!.wad'; //FIXME
-        md5 := MD5File(dfn);
+        //md5 := MD5File(dfn);
+        md5 := gWADHash;
+        if (not GetDiskFileInfo(dfn, fi)) then
+        begin
+          e_LogWritefln('client #%d requested map info, but i cannot get file info', [nc.ID]);
+          killClientByFT(nc^);
+          exit;
+        end;
+        size := fi.size;
+        {
         st := openDiskFileRO(dfn);
         if not assigned(st) then exit; //wtf?!
         size := st.size;
         st.Free;
+        }
         // packet type
         trans_omsg.Write(Byte(NTF_SERVER_MAP_INFO));
         // map wad name
@@ -635,11 +687,17 @@ begin
         // map wad size
         trans_omsg.Write(size);
         // number of external resources for map
-        trans_omsg.Write(LongInt(gExternalResources.Count));
+        trans_omsg.Write(LongInt(length(gExternalResources)));
         // external resource names
-        for f := 0 to gExternalResources.Count-1 do
+        for f := 0 to High(gExternalResources) do
         begin
-          trans_omsg.Write(ExtractFileName(gExternalResources[f])); // GameDir+'/wads/'+ResList.Strings[i]
+          // old style packet
+          //trans_omsg.Write(ExtractFileName(gExternalResources[f])); // GameDir+'/wads/'+ResList.Strings[i]
+          // new style packet
+          trans_omsg.Write('!');
+          trans_omsg.Write(LongInt(gExternalResources[f].size));
+          trans_omsg.Write(gExternalResources[f].hash);
+          trans_omsg.Write(ExtractFileName(gExternalResources[f].diskName));
         end;
         // send packet
         if not ftransSendServerMsg(nc^, trans_omsg) then exit;
@@ -677,16 +735,18 @@ end;
 //
 // returns `false` on error or user abort
 // fills:
-//    hash
-//    size
-//    chunkSize
+//    diskName: map wad file name (without a path)
+//    hash: map wad hash
+//    size: map wad size
+//    chunkSize: set too
+//    resList: list of resource wads
 // 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;
+function g_Net_Wait_MapInfo (var tf: TNetFileTransfer; var resList: TNetMapResourceInfoArray): Integer;
 var
   ev: ENetEvent;
   rMsgId: Byte;
@@ -697,7 +757,10 @@ var
   status: cint;
   s: AnsiString;
   rc, f: LongInt;
+  ri: ^TNetMapResourceInfo;
 begin
+  SetLength(resList, 0);
+
   // send request
   trans_omsg.Clear();
   trans_omsg.Write(Byte(NTF_CLIENT_MAP_REQUEST));
@@ -709,13 +772,15 @@ begin
     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;
-      if (status = 0) then
+      }
+      if (status <= 0) then
       begin
         // check for timeout
         ct := GetTimerMS();
@@ -769,7 +834,7 @@ 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();
+                  SetLength(resList, 0); // just in case
                   // map wad name
                   tf.diskName := msg.ReadString();
                   e_LogWritefln('g_Net_Wait_MapInfo: map wad is `%s`', [tf.diskName]);
@@ -787,16 +852,28 @@ begin
                     exit;
                   end;
                   e_LogWritefln('g_Net_Wait_MapInfo: map external resource count is %d', [rc]);
+                  SetLength(resList, rc);
                   // external resource names
                   for f := 0 to rc-1 do
                   begin
-                    s := ExtractFileName(msg.ReadString());
-                    if (length(s) = 0) then
+                    ri := @resList[f];
+                    s := msg.ReadString();
+                    if (length(s) = 0) then begin result := -1; exit; end;
+                    if (s = '!') then
+                    begin
+                      // extended packet
+                      ri.size := msg.ReadLongInt();
+                      ri.hash := msg.ReadMD5();
+                      ri.wadName := ExtractFileName(msg.ReadString());
+                      if (length(ri.wadName) = 0) or (ri.size < 0) then begin result := -1; exit; end;
+                    end
+                    else
                     begin
-                      Result := -1;
-                      exit;
+                      // old-style packet, only name
+                      ri.wadName := ExtractFileName(s);
+                      if (length(ri.wadName) = 0) then begin result := -1; exit; end;
+                      ri.size := -1; // unknown
                     end;
-                    resList.append(s);
                   end;
                   e_LogWritefln('g_Net_Wait_MapInfo: got map info', []);
                   Result := 0; // success
@@ -876,13 +953,15 @@ begin
     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;
-      if (status = 0) then
+      }
+      if (status <= 0) then
       begin
         // check for timeout
         ct := GetTimerMS();
@@ -1058,13 +1137,15 @@ begin
     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
+      }
+      if (status <= 0) then
       begin
         // check for timeout
         ct := GetTimerMS();
@@ -1232,6 +1313,7 @@ begin
     NetClients[N].Used := True;
     NetClients[N].ID := N;
     NetClients[N].RequestedFullUpdate := False;
+    NetClients[N].WaitForFirstSpawn := False;
     NetClients[N].RCONAuth := False;
     NetClients[N].Voted := False;
     NetClients[N].Player := 0;
@@ -1241,11 +1323,13 @@ begin
   Result := N;
 end;
 
+
 function g_Net_Init(): Boolean;
 var
   F: TextFile;
   IPstr: string;
   IP: LongWord;
+  path: AnsiString;
 begin
   NetIn.Clear();
   NetOut.Clear();
@@ -1260,9 +1344,10 @@ begin
   NetPlrUID2 := -1;
   NetAddr.port := 25666;
   SetLength(NetBannedHosts, 0);
-  if FileExists(DataDir + BANLIST_FILENAME) then
+  path := BANLIST_FILENAME;
+  if e_FindResource(DataDirs, path) = true then
   begin
-    Assign(F, DataDir + BANLIST_FILENAME);
+    Assign(F, path);
     Reset(F);
     while not EOF(F) do
     begin
@@ -1274,7 +1359,8 @@ begin
     g_Net_SaveBanList();
   end;
 
-  Result := (enet_initialize() = 0);
+  //Result := (enet_initialize() = 0);
+  Result := enet_init_success;
 end;
 
 procedure g_Net_Flush();
@@ -1341,8 +1427,7 @@ begin
 
   NetPeer := nil;
   NetHost := nil;
-  NetMPeer := nil;
-  NetMHost := nil;
+  g_Net_Slist_ServerClosed();
   NetMyID := -1;
   NetPlrUID1 := -1;
   NetPlrUID2 := -1;
@@ -1370,7 +1455,7 @@ procedure g_Net_Free();
 begin
   g_Net_Cleanup();
 
-  enet_deinitialize();
+  //enet_deinitialize();
   NetInitDone := False;
 end;
 
@@ -1477,7 +1562,7 @@ begin
     end;
 
   clearNetClients(false); // don't clear array
-  if (NetMPeer <> nil) and (NetMHost <> nil) then g_Net_Slist_Disconnect;
+  g_Net_Slist_ServerClosed();
   if NetPongSock <> ENET_SOCKET_NULL then
     enet_socket_destroy(NetPongSock);
 
@@ -1547,7 +1632,7 @@ begin
     NetOut.Write(Byte(Ord('F')));
     NetOut.Write(NetPort);
     NetOut.Write(ClTime);
-    g_Net_Slist_WriteInfo();
+    TMasterHost.writeInfo(NetOut);
     NPl := 0;
     if gPlayer1 <> nil then Inc(NPl);
     if gPlayer2 <> nil then Inc(NPl);
@@ -1574,8 +1659,8 @@ begin
   IP := '';
   Result := 0;
 
-  if NetUseMaster then g_Net_Slist_Check;
-  g_Net_Host_CheckPings;
+  if NetUseMaster then g_Net_Slist_Pulse();
+  g_Net_Host_CheckPings();
 
   while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
   begin
@@ -1670,6 +1755,7 @@ begin
         TC^.Peer := nil;
         TC^.Player := 0;
         TC^.RequestedFullUpdate := False;
+        TC^.WaitForFirstSpawn := False;
         TC^.NetOut[NET_UNRELIABLE].Free();
         TC^.NetOut[NET_RELIABLE].Free();
 
@@ -1678,7 +1764,7 @@ begin
         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 NetUseMaster then g_Net_Slist_ServerPlayerLeaves();
       end;
     end;
   end;
@@ -1855,7 +1941,7 @@ begin
   end;
 
   // предупредить что ждем слишком долго через N секунд
-  TimeoutTime := GetTimer() + NET_CONNECT_TIMEOUT;
+  TimeoutTime := sys_GetTicks() + NET_CONNECT_TIMEOUT;
 
   OuterLoop := True;
   while OuterLoop do
@@ -1876,7 +1962,7 @@ begin
       end;
     end;
 
-    T := GetTimer();
+    T := sys_GetTicks();
     if T > TimeoutTime then
     begin
       TimeoutTime := T + NET_CONNECT_TIMEOUT * 100; // одного предупреждения хватит
@@ -2100,22 +2186,28 @@ procedure g_Net_SaveBanList();
 var
   F: TextFile;
   I: Integer;
+  path: AnsiString;
 begin
-  Assign(F, DataDir + BANLIST_FILENAME);
-  Rewrite(F);
-  if NetBannedHosts <> nil then
-    for I := 0 to High(NetBannedHosts) do
-      if NetBannedHosts[I].Perm and (NetBannedHosts[I].IP > 0) then
-        Writeln(F, IpToStr(NetBannedHosts[I].IP));
-  CloseFile(F);
+  path := e_GetWriteableDir(DataDirs);
+  if path <> '' then
+  begin
+    path := e_CatPath(path, BANLIST_FILENAME);
+    Assign(F, path);
+    Rewrite(F);
+    if NetBannedHosts <> nil then
+      for I := 0 to High(NetBannedHosts) do
+        if NetBannedHosts[I].Perm and (NetBannedHosts[I].IP > 0) then
+          Writeln(F, IpToStr(NetBannedHosts[I].IP));
+    CloseFile(F)
+  end
 end;
 
 procedure g_Net_DumpStart();
 begin
   if NetMode = NET_SERVER then
-    NetDumpFile := createDiskFile(NETDUMP_FILENAME + '_server')
+    NetDumpFile := e_CreateResource(LogDirs, NETDUMP_FILENAME + '_server')
   else
-    NetDumpFile := createDiskFile(NETDUMP_FILENAME + '_client');
+    NetDumpFile := e_CreateResource(LogDirs, NETDUMP_FILENAME + '_client');
 end;
 
 procedure g_Net_DumpSendBuffer();