DEADSOFTWARE

net: some Preliminary (but half-working) code for new map/resource download scheme
[d2df-sdl.git] / src / game / g_res_downloader.pas
index 04048b693c983c558af1c61168f3948eee2c5823..a092437993d1601f774deb9235ff1ac037916e7d 100644 (file)
@@ -19,44 +19,207 @@ interface
 
 uses sysutils, Classes, md5, g_net, g_netmsg, g_console, g_main, e_log;
 
-function g_Res_SearchSameWAD(const path, filename: string; const resMd5: TMD5Digest): string;
-function g_Res_DownloadWAD(const FileName: string): string;
+function g_Res_SearchSameWAD(const path, filename: AnsiString; const resMd5: TMD5Digest): AnsiString;
+function g_Res_SearchResWad (asMap: Boolean; const resMd5: TMD5Digest): AnsiString;
+
+// download map wad from server (if necessary)
+// download all required map resource wads too
+// returns name of the map wad (relative to mapdir), or empty string on error
+function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
+
+// call this before downloading a new map from a server
+procedure g_Res_ClearReplacementWads ();
+// returns original name, or replacement name
+function g_Res_FindReplacementWad (oldname: AnsiString): AnsiString;
+procedure g_Res_PutReplacementWad (oldname: AnsiString; newDiskName: AnsiString);
+
 
 implementation
 
-uses g_language, sfs, utils, wadreader, g_game;
+uses g_language, sfs, utils, wadreader, g_game, hashtable;
 
 const DOWNLOAD_DIR = 'downloads';
 
-procedure FindFiles(const dirName, filename: string; var files: TStringList);
+type
+  TFileInfo = record
+    diskName: AnsiString; // lowercased
+    baseName: AnsiString; // lowercased
+    md5: TMD5Digest;
+    md5valid: Boolean;
+    nextBaseNameIndex: Integer;
+  end;
+
 var
-  searchResult: TSearchRec;
+  knownFiles: array of TFileInfo;
+  knownHash: THashStrInt = nil; // key: base name; value: index
+  scannedDirs: THashStrInt = nil; // key: lowercased dir name
+  replacements: THashStrStr = nil;
+
+
+function findKnownFile (diskName: AnsiString): Integer;
+var
+  idx: Integer;
+  baseName: AnsiString;
 begin
-  if FindFirst(dirName+'/*', faAnyFile, searchResult) = 0 then
+  result := -1;
+  if not assigned(knownHash) then exit;
+  if (length(diskName) = 0) then exit;
+  baseName := toLowerCase1251(ExtractFileName(diskName));
+  if (not knownHash.get(baseName, idx)) then exit;
+  if (idx < 0) or (idx >= length(knownFiles)) then raise Exception.Create('wutafuck?');
+  while (idx >= 0) do
+  begin
+    if (strEquCI1251(knownFiles[idx].diskName, diskName)) then begin result := idx; exit; end; // i found her!
+    idx := knownFiles[idx].nextBaseNameIndex;
+  end;
+end;
+
+
+function addKnownFile (diskName: AnsiString): Integer;
+var
+  idx: Integer;
+  lastIdx: Integer = -1;
+  baseName: AnsiString;
+  fi: ^TFileInfo;
+begin
+  result := -1;
+  if not assigned(knownHash) then knownHash := THashStrInt.Create();
+  if (length(diskName) = 0) then exit;
+  baseName := toLowerCase1251(ExtractFileName(diskName));
+  if (length(baseName) = 0) then exit;
+  // check if we already have this file
+  if (knownHash.get(baseName, idx)) then
+  begin
+    if (idx < 0) or (idx >= length(knownFiles)) then raise Exception.Create('wutafuck?');
+    while (idx >= 0) do
+    begin
+      if (strEquCI1251(knownFiles[idx].diskName, diskName)) then
+      begin
+        // already here
+        result := idx;
+        exit;
+      end;
+      lastIdx := idx;
+      idx := knownFiles[idx].nextBaseNameIndex;
+    end;
+  end;
+  // this file is not there, append it
+  idx := length(knownFiles);
+  result := idx;
+  SetLength(knownFiles, idx+1); // sorry
+  fi := @knownFiles[idx];
+  fi.diskName := diskName;
+  fi.baseName := baseName;
+  fi.md5valid := false;
+  fi.nextBaseNameIndex := -1;
+  if (lastIdx < 0) then
+  begin
+    // totally new one
+    knownHash.put(baseName, idx);
+  end
+  else
   begin
-    try
-      repeat
-        if (searchResult.Attr and faDirectory) = 0 then
+    knownFiles[lastIdx].nextBaseNameIndex := idx;
+  end;
+end;
+
+
+function getKnownFileWithMD5 (diskDir: AnsiString; baseName: AnsiString; const md5: TMD5Digest): AnsiString;
+var
+  idx: Integer;
+begin
+  result := '';
+  if not assigned(knownHash) then exit;
+  if (not knownHash.get(toLowerCase1251(baseName), idx)) then exit;
+  if (idx < 0) or (idx >= length(knownFiles)) then raise Exception.Create('wutafuck?');
+  while (idx >= 0) do
+  begin
+    if (strEquCI1251(knownFiles[idx].diskName, IncludeTrailingPathDelimiter(diskDir)+baseName)) then
+    begin
+      if (not knownFiles[idx].md5valid) then
+      begin
+        knownFiles[idx].md5 := MD5File(knownFiles[idx].diskName);
+        knownFiles[idx].md5valid := true;
+      end;
+      if (MD5Match(knownFiles[idx].md5, md5)) then
+      begin
+        result := knownFiles[idx].diskName;
+        exit;
+      end;
+    end;
+    idx := knownFiles[idx].nextBaseNameIndex;
+  end;
+end;
+
+
+// call this before downloading a new map from a server
+procedure g_Res_ClearReplacementWads ();
+begin
+  if assigned(replacements) then replacements.clear();
+  e_LogWriteln('cleared replacement wads');
+end;
+
+
+// returns original name, or replacement name
+function g_Res_FindReplacementWad (oldname: AnsiString): AnsiString;
+var
+  fn: AnsiString;
+begin
+  result := oldname;
+  if not assigned(replacements) then exit;
+  if (replacements.get(toLowerCase1251(ExtractFileName(oldname)), fn)) then result := fn;
+end;
+
+
+procedure g_Res_PutReplacementWad (oldname: AnsiString; newDiskName: AnsiString);
+begin
+  e_LogWritefln('adding replacement wad: oldname=%s; newname=%s', [oldname, newDiskName]);
+  replacements.put(toLowerCase1251(oldname), newDiskName);
+end;
+
+
+procedure scanDir (const dirName: AnsiString; calcMD5: Boolean);
+var
+  searchResult: TSearchRec;
+  dfn: AnsiString;
+  idx: Integer;
+begin
+  if not assigned(scannedDirs) then scannedDirs := THashStrInt.Create();
+  dfn := toLowerCase1251(IncludeTrailingPathDelimiter(dirName));
+  if scannedDirs.has(dfn) then exit;
+  scannedDirs.put(dfn, 42);
+
+  if (FindFirst(dirName+'/*', faAnyFile, searchResult) <> 0) then exit;
+  try
+    repeat
+      if (searchResult.Attr and faDirectory) = 0 then
+      begin
+        dfn := dirName+'/'+searchResult.Name;
+        idx := addKnownFile(dfn);
+        if (calcMD5) and (idx >= 0) then
         begin
-          if StrEquCI1251(searchResult.Name, filename) then
+          if (not knownFiles[idx].md5valid) then
           begin
-            files.Add(dirName+'/'+filename);
-            Exit;
+            knownFiles[idx].md5 := MD5File(knownFiles[idx].diskName);
+            knownFiles[idx].md5valid := true;
           end;
-        end
-        else if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
-          FindFiles(IncludeTrailingPathDelimiter(dirName)+searchResult.Name, filename, files);
-      until FindNext(searchResult) <> 0;
-    finally
-      FindClose(searchResult);
-    end;
+        end;
+      end
+      else if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
+      begin
+        scanDir(IncludeTrailingPathDelimiter(dirName)+searchResult.Name, calcMD5);
+      end;
+    until (FindNext(searchResult) <> 0);
+  finally
+    FindClose(searchResult);
   end;
 end;
 
-function CompareFileHash(const filename: string; const resMd5: TMD5Digest): Boolean;
+
+function CompareFileHash(const filename: AnsiString; const resMd5: TMD5Digest): Boolean;
 var
   gResHash: TMD5Digest;
-  fname: string;
+  fname: AnsiString;
 begin
   fname := findDiskWad(filename);
   if length(fname) = 0 then begin result := false; exit; end;
@@ -64,115 +227,197 @@ begin
   Result := MD5Match(gResHash, resMd5);
 end;
 
-function CheckFileHash(const path, filename: string; const resMd5: TMD5Digest): Boolean;
+function CheckFileHash(const path, filename: AnsiString; const resMd5: TMD5Digest): Boolean;
 var
-  fname: string;
+  fname: AnsiString;
 begin
   fname := findDiskWad(path+filename);
   if length(fname) = 0 then begin result := false; exit; end;
   Result := FileExists(fname) and CompareFileHash(fname, resMd5);
 end;
 
-function g_Res_SearchSameWAD(const path, filename: string; const resMd5: TMD5Digest): string;
+
+function g_Res_SearchResWad (asMap: Boolean; const resMd5: TMD5Digest): AnsiString;
 var
-  res: string;
-  files: TStringList;
-  i: Integer;
+  f: Integer;
 begin
-  Result := '';
-
-  if CheckFileHash(path, filename, resMd5) then
+  result := '';
+  //if not assigned(scannedDirs) then scannedDirs := THashStrInt.Create();
+  if (asMap) then
   begin
-    Result := path + filename;
-    Exit;
+    scanDir(GameDir+'/maps/downloads', true);
+  end
+  else
+  begin
+    scanDir(GameDir+'/wads/downloads', true);
   end;
-
-  files := TStringList.Create;
-
-  FindFiles(path, filename, files);
-  for i := 0 to files.Count - 1 do
+  for f := Low(knownFiles) to High(knownFiles) do
   begin
-    res := files.Strings[i];
-    if CompareFileHash(res, resMd5) then
+    if (not knownFiles[f].md5valid) then continue;
+    if (MD5Match(knownFiles[f].md5, resMd5)) then
     begin
-      Result := res;
-      Break;
+      result := knownFiles[f].diskName;
+      exit;
     end;
   end;
+  //resStream := createDiskFile(GameDir+'/wads/'+mapData.ExternalResources[i].Name);
+end;
+
 
-  files.Free;
+function g_Res_SearchSameWAD (const path, filename: AnsiString; const resMd5: TMD5Digest): AnsiString;
+begin
+  scanDir(path, false);
+  result := getKnownFileWithMD5(path, filename, resMd5);
 end;
 
-function SaveWAD(const path, filename: string; const data: array of Byte): string;
+
+function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
 var
-  resFile: TStream;
-  dpt: string;
+  tf: TNetFileTransfer;
+  resList: TStringList;
+  f, res: Integer;
+  strm: TStream;
+  mmd5: TMD5Digest;
+  fname: AnsiString;
+  idx: Integer;
+  wadname: AnsiString;
 begin
+  //SetLength(mapData.ExternalResources, 0);
+  //result := g_Res_SearchResWad(true{asMap}, mapHash);
+  result := '';
+  g_Res_ClearReplacementWads();
+
   try
-    result := path+DOWNLOAD_DIR+'/'+filename;
-    dpt := path+DOWNLOAD_DIR;
-    if not findFileCI(dpt, true) then CreateDir(dpt);
-    resFile := createDiskFile(result);
-    resFile.WriteBuffer(data[0], Length(data));
-    resFile.Free
+    CreateDir(GameDir+'/maps/downloads');
   except
-    Result := '';
   end;
-end;
 
-function g_Res_DownloadWAD(const FileName: string): string;
-var
-  msgStream: TMemoryStream;
-  resStream: TStream;
-  mapData: TMapDataMsg;
-  i: Integer;
-  resData: TResDataMsg;
-begin
-  SetLength(mapData.ExternalResources, 0);
-  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);
-  MC_SEND_MapRequest();
-
-  msgStream := g_Net_Wait_Event(NET_MSG_MAP_RESPONSE);
-  if msgStream <> nil then
-  begin
-    mapData := MapDataFromMsgStream(msgStream);
-    msgStream.Free;
-  end else
-    mapData.FileSize := 0;
+  try
+    CreateDir(GameDir+'/wads/downloads');
+  except
+  end;
 
-  for i := 0 to High(mapData.ExternalResources) do
-  begin
-    if not CheckFileHash(GameDir + '/wads/',
-                         mapData.ExternalResources[i].Name,
-                         mapData.ExternalResources[i].md5) then
-    begin
-      g_Console_Add(Format(_lc[I_NET_WAD_DL],
-                           [mapData.ExternalResources[i].Name]));
-      e_WriteLog('Downloading Wad `' + mapData.ExternalResources[i].Name +
-                 '` from server', TMsgType.Notify);
-      g_Game_SetLoadingText(mapData.ExternalResources[i].Name + '...', 0, False);
-      MC_SEND_ResRequest(mapData.ExternalResources[i].Name);
+  resList := TStringList.Create();
 
-      msgStream := g_Net_Wait_Event(NET_MSG_RES_RESPONSE);
-      if msgStream = nil then
-        continue;
+  try
+    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);
+    //MC_SEND_MapRequest();
+    if (not g_Net_SendMapRequest()) then exit;
 
-      resData := ResDataFromMsgStream(msgStream);
+    FileName := ExtractFileName(FileName);
+    if (length(FileName) = 0) then FileName := 'fucked_map_wad.wad';
+    res := g_Net_Wait_MapInfo(tf, resList);
+    if (res <> 0) then exit;
 
-      resStream := createDiskFile(GameDir+'/wads/'+mapData.ExternalResources[i].Name);
-      resStream.WriteBuffer(resData.FileData[0], resData.FileSize);
+    // find or download a map
+    result := g_Res_SearchResWad(true{asMap}, mapHash);
+    if (length(result) = 0) then
+    begin
+      // download map
+      res := g_Net_RequestResFileInfo(-1{map}, tf);
+      if (res <> 0) then
+      begin
+        e_LogWriteln('error requesting map wad');
+        result := '';
+        exit;
+      end;
+      fname := GameDir+'/maps/downloads/'+FileName;
+      try
+        strm := createDiskFile(fname);
+      except
+        e_WriteLog('cannot create map file `'+FileName+'`', TMsgType.Fatal);
+        result := '';
+        exit;
+      end;
+      try
+        res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
+      except
+        e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+        strm.Free;
+        result := '';
+        exit;
+      end;
+      strm.Free;
+      if (res <> 0) then
+      begin
+        e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+        result := '';
+        exit;
+      end;
+      mmd5 := MD5File(fname);
+      if (not MD5Match(mmd5, mapHash)) then
+      begin
+        e_WriteLog('error downloading map file `'+FileName+'` (bad hash)', TMsgType.Fatal);
+        result := '';
+        exit;
+      end;
+      idx := addKnownFile(fname);
+      if (idx < 0) then
+      begin
+        e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+        result := '';
+        exit;
+      end;
+      knownFiles[idx].md5 := mmd5;
+      knownFiles[idx].md5valid := true;
+      result := fname;
+    end;
 
-      resData.FileData := nil;
-      resStream.Free;
-      msgStream.Free;
+    // download resources
+    for f := 0 to resList.Count-1 do
+    begin
+      res := g_Net_RequestResFileInfo(f, tf);
+      if (res <> 0) then begin result := ''; exit; end;
+      wadname := g_Res_SearchResWad(false{asMap}, tf.hash);
+      if (length(wadname) <> 0) then
+      begin
+        // already here
+        g_Net_AbortResTransfer(tf);
+        g_Res_PutReplacementWad(tf.diskName, wadname);
+      end
+      else
+      begin
+        fname := GameDir+'/wads/downloads/'+tf.diskName;
+        try
+          strm := createDiskFile(fname);
+        except
+          e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
+          result := '';
+          exit;
+        end;
+        try
+          res := g_Net_ReceiveResourceFile(f, tf, strm);
+        except
+          e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+          strm.Free;
+          result := '';
+          exit;
+        end;
+        strm.Free;
+        if (res <> 0) then
+        begin
+          e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+          result := '';
+          exit;
+        end;
+        idx := addKnownFile(fname);
+        if (idx < 0) then
+        begin
+          e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
+          result := '';
+          exit;
+        end;
+        knownFiles[idx].md5 := tf.hash;
+        knownFiles[idx].md5valid := true;
+        g_Res_PutReplacementWad(tf.diskName, fname);
+      end;
     end;
+  finally
+    resList.Free;
   end;
-
-  Result := SaveWAD(MapsDir, ExtractFileName(FileName), mapData.FileData);
-  if mapData.FileSize = 0 then
-    DeleteFile(Result);
 end;
 
+
 end.