DEADSOFTWARE

fd2d90b9d4d541c4275dc644a211c0e15b6f4c35
[d2df-sdl.git] / src / game / g_res_downloader.pas
1 (* Copyright (C) Doom 2D: Forever Developers
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, version 3 of the License ONLY.
6 *
7 * This program is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * GNU General Public License for more details.
11 *
12 * You should have received a copy of the GNU General Public License
13 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14 *)
15 {$INCLUDE ../shared/a_modes.inc}
16 unit g_res_downloader;
18 interface
20 uses sysutils, Classes, md5, g_net, g_netmsg, g_console, g_main, e_log;
23 // download map wad from server (if necessary)
24 // download all required map resource wads too
25 // registers all required replacement wads
26 // returns name of the map wad (relative to mapdir), or empty string on error
27 function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
29 // returns original name, or replacement name
30 function g_Res_FindReplacementWad (oldname: AnsiString): AnsiString;
33 implementation
35 uses g_language, sfs, utils, wadreader, g_game, hashtable;
37 var
38 // cvars
39 g_res_ignore_names: AnsiString = 'standart;shrshade';
40 g_res_ignore_enabled: Boolean = true;
41 // other vars
42 replacements: THashStrStr = nil;
45 //==========================================================================
46 //
47 // getWord
48 //
49 // get next word from a string
50 // words are delimited with ';'
51 // ignores leading and trailing spaces
52 // returns empty string if there are no more words
53 //
54 //==========================================================================
55 function getWord (var list: AnsiString): AnsiString;
56 var
57 pos: Integer;
58 begin
59 result := '';
60 while (length(list) > 0) do
61 begin
62 if (ord(list[1]) <= 32) or (list[1] = ';') or (list[1] = ':') then begin Delete(list, 1, 1); continue; end;
63 pos := 1;
64 while (pos <= length(list)) and (list[pos] <> ';') and (list[pos] <> ':') do Inc(pos);
65 result := Copy(list, 1, pos-1);
66 Delete(list, 1, pos);
67 while (length(result) > 0) and (ord(result[length(result)]) <= 32) do Delete(result, length(result), 1);
68 if (length(result) > 0) then exit;
69 end;
70 end;
73 //==========================================================================
74 //
75 // isIgnoredResWad
76 //
77 // checks if the given resource wad can be ignored
78 //
79 // FIXME: preparse name list?
80 //
81 //==========================================================================
82 function isIgnoredResWad (fname: AnsiString): Boolean;
83 var
84 list: AnsiString;
85 name: AnsiString;
86 begin
87 result := false;
88 if (not g_res_ignore_enabled) then exit;
89 fname := forceFilenameExt(ExtractFileName(fname), '');
90 list := g_res_ignore_names;
91 name := getWord(list);
92 while (length(name) > 0) do
93 begin
94 name := forceFilenameExt(name, '');
95 //writeln('*** name=[', name, ']; fname=[', fname, ']');
96 if (StrEquCI1251(name, fname)) then begin result := true; exit; end;
97 name := getWord(list);
98 end;
99 end;
102 //==========================================================================
103 //
104 // clearReplacementWads
105 //
106 // call this before downloading a new map from a server
107 //
108 //==========================================================================
109 procedure clearReplacementWads ();
110 begin
111 if assigned(replacements) then replacements.clear();
112 e_LogWriteln('cleared replacement wads');
113 end;
116 //==========================================================================
117 //
118 // addReplacementWad
119 //
120 // register new replacement wad
121 //
122 //==========================================================================
123 procedure addReplacementWad (oldname: AnsiString; newDiskName: AnsiString);
124 begin
125 e_LogWritefln('adding replacement wad: oldname=%s; newname=%s', [oldname, newDiskName]);
126 if not assigned(replacements) then replacements := THashStrStr.Create();
127 replacements.put(toLowerCase1251(oldname), newDiskName);
128 end;
131 //==========================================================================
132 //
133 // g_Res_FindReplacementWad
134 //
135 // returns original name, or replacement name
136 //
137 //==========================================================================
138 function g_Res_FindReplacementWad (oldname: AnsiString): AnsiString;
139 var
140 fn: AnsiString;
141 begin
142 //e_LogWritefln('LOOKING for replacement wad for [%s]...', [oldname], TMsgType.Notify);
143 result := oldname;
144 if not assigned(replacements) then exit;
145 if (replacements.get(toLowerCase1251(ExtractFileName(oldname)), fn)) then
146 begin
147 //e_LogWritefln('found replacement wad for [%s] -> [%s]', [oldname, fn], TMsgType.Notify);
148 result := fn;
149 end;
150 end;
153 //==========================================================================
154 //
155 // scanDir
156 //
157 // look for a wad to match the hash
158 // scans subdirs, ignores known wad extensions
159 //
160 // returns found wad disk name, or empty string
161 //
162 //==========================================================================
163 function scanDir (dirName: AnsiString; baseName: AnsiString; const resMd5: TMD5Digest): AnsiString;
164 var
165 searchResult: TSearchRec;
166 dfn: AnsiString;
167 md5: TMD5Digest;
168 dirs: array of AnsiString;
169 f: Integer;
170 begin
171 result := '';
172 SetLength(dirs, 0);
173 if (length(baseName) = 0) then exit;
174 dirName := IncludeTrailingPathDelimiter(dirName);
175 e_LogWritefln('scanning dir `%s` for file `%s`...', [dirName, baseName]);
177 // scan files
178 if (FindFirst(dirName+'*', faAnyFile, searchResult) <> 0) then exit;
179 try
180 repeat
181 if ((searchResult.Attr and faDirectory) = 0) then
182 begin
183 if (isWadNamesEqu(searchResult.Name, baseName)) then
184 begin
185 dfn := dirName+searchResult.Name;
186 if FileExists(dfn) then
187 begin
188 e_LogWritefln(' found `%s`...', [dfn]);
189 md5 := MD5File(dfn);
190 if MD5Match(md5, resMd5) then
191 begin
192 e_LogWritefln(' MATCH `%s`...', [dfn]);
193 SetLength(dirs, 0);
194 result := dfn;
195 exit;
196 end;
197 end;
198 end;
199 end
200 else
201 begin
202 if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
203 begin
204 dfn := dirName+searchResult.Name;
205 SetLength(dirs, Length(dirs)+1);
206 dirs[length(dirs)-1] := dfn;
207 end;
208 end;
209 until (FindNext(searchResult) <> 0);
210 finally
211 FindClose(searchResult);
212 end;
214 // scan subdirs
215 for f := 0 to High(dirs) do
216 begin
217 dfn := dirs[f];
218 result := scanDir(dfn, baseName, resMd5);
219 if (length(result) <> 0) then begin SetLength(dirs, 0); exit; end;
220 end;
221 SetLength(dirs, 0);
222 end;
225 //==========================================================================
226 //
227 // findExistingMapWadWithHash
228 //
229 // find map or resource wad using its base name and hash
230 //
231 // returns found wad disk name, or empty string
232 //
233 //==========================================================================
234 function findExistingMapWadWithHash (fname: AnsiString; const resMd5: TMD5Digest): AnsiString;
235 begin
236 result := scanDir(GameDir+'/maps', ExtractFileName(fname), resMd5);
237 end;
240 //==========================================================================
241 //
242 // findExistingResWadWithHash
243 //
244 // find map or resource wad using its base name and hash
245 //
246 // returns found wad disk name, or empty string
247 //
248 //==========================================================================
249 function findExistingResWadWithHash (fname: AnsiString; const resMd5: TMD5Digest): AnsiString;
250 begin
251 result := scanDir(GameDir+'/wads', ExtractFileName(fname), resMd5);
252 end;
255 //==========================================================================
256 //
257 // g_Res_DownloadMapWAD
258 //
259 // download map wad from server (if necessary)
260 // download all required map resource wads too
261 // registers all required replacement wads
262 //
263 // returns name of the map wad (relative to mapdir), or empty string on error
264 //
265 //==========================================================================
266 function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
267 var
268 tf: TNetFileTransfer;
269 resList: TStringList;
270 f, res: Integer;
271 strm: TStream;
272 fname: AnsiString;
273 wadname: AnsiString;
274 md5: TMD5Digest;
275 begin
276 result := '';
277 clearReplacementWads();
279 resList := TStringList.Create();
281 try
282 g_Res_received_map_start := 1;
283 g_Console_Add(Format(_lc[I_NET_MAP_DL], [FileName]));
284 e_WriteLog('Downloading map `' + FileName + '` from server', TMsgType.Notify);
285 g_Game_SetLoadingText(FileName + '...', 0, False);
286 if (not g_Net_SendMapRequest()) then exit;
288 FileName := ExtractFileName(FileName);
289 if (length(FileName) = 0) then FileName := 'fucked_map_wad.wad';
290 res := g_Net_Wait_MapInfo(tf, resList);
291 if (res <> 0) then exit;
293 // find or download a map
294 result := findExistingMapWadWithHash(tf.diskName, mapHash);
295 if (length(result) = 0) then
296 begin
297 // download map
298 res := g_Net_RequestResFileInfo(-1{map}, tf);
299 if (res <> 0) then
300 begin
301 e_LogWriteln('error requesting map wad');
302 result := '';
303 exit;
304 end;
305 try
306 CreateDir(GameDir+'/maps/downloads');
307 except
308 end;
309 fname := GameDir+'/maps/downloads/'+FileName;
310 try
311 strm := openDiskFileRW(fname);
312 except
313 e_WriteLog('cannot create map file `'+FileName+'`', TMsgType.Fatal);
314 result := '';
315 exit;
316 end;
317 tf.diskName := fname;
318 try
319 res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
320 except
321 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
322 strm.Free;
323 result := '';
324 exit;
325 end;
326 strm.Free;
327 if (res <> 0) then
328 begin
329 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
330 result := '';
331 exit;
332 end;
333 // if it was resumed, check md5 and initiate full download if necessary
334 if tf.resumed then
335 begin
336 md5 := MD5File(fname);
337 // sorry for pasta, i am asshole
338 if not MD5Match(md5, tf.hash) then
339 begin
340 e_LogWritefln('resuming failed; downloading map `%s` from scratch...', [fname]);
341 try
342 DeleteFile(fname);
343 strm := createDiskFile(fname);
344 except
345 e_WriteLog('cannot create map file `'+fname+'`', TMsgType.Fatal);
346 result := '';
347 exit;
348 end;
349 try
350 res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
351 except
352 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
353 strm.Free;
354 result := '';
355 exit;
356 end;
357 strm.Free;
358 if (res <> 0) then
359 begin
360 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
361 result := '';
362 exit;
363 end;
364 end;
365 end;
366 result := fname;
367 end;
369 // download resources
370 for f := 0 to resList.Count-1 do
371 begin
372 res := g_Net_RequestResFileInfo(f, tf);
373 if (res <> 0) then begin result := ''; exit; end;
374 if (isIgnoredResWad(tf.diskName)) then
375 begin
376 // ignored file, abort download
377 g_Net_AbortResTransfer(tf);
378 e_LogWritefln('ignoring wad resource `%s` by user request', [tf.diskName]);
379 continue;
380 end;
381 wadname := findExistingResWadWithHash(tf.diskName, tf.hash);
382 if (length(wadname) <> 0) then
383 begin
384 // already here
385 g_Net_AbortResTransfer(tf);
386 addReplacementWad(tf.diskName, wadname);
387 end
388 else
389 begin
390 try
391 CreateDir(GameDir+'/wads/downloads');
392 except
393 end;
394 fname := GameDir+'/wads/downloads/'+tf.diskName;
395 e_LogWritefln('downloading resource `%s` to `%s`...', [tf.diskName, fname]);
396 try
397 strm := openDiskFileRW(fname);
398 except
399 e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
400 result := '';
401 exit;
402 end;
403 try
404 res := g_Net_ReceiveResourceFile(f, tf, strm);
405 except
406 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
407 strm.Free;
408 result := '';
409 exit;
410 end;
411 strm.Free;
412 if (res <> 0) then
413 begin
414 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
415 result := '';
416 exit;
417 end;
418 // if it was resumed, check md5 and initiate full download if necessary
419 if tf.resumed then
420 begin
421 md5 := MD5File(fname);
422 // sorry for pasta, i am asshole
423 if not MD5Match(md5, tf.hash) then
424 begin
425 e_LogWritefln('resuming failed; downloading resource `%s` to `%s` from scratch...', [tf.diskName, fname]);
426 try
427 DeleteFile(fname);
428 strm := createDiskFile(fname);
429 except
430 e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
431 result := '';
432 exit;
433 end;
434 try
435 res := g_Net_ReceiveResourceFile(f, tf, strm);
436 except
437 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
438 strm.Free;
439 result := '';
440 exit;
441 end;
442 strm.Free;
443 if (res <> 0) then
444 begin
445 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
446 result := '';
447 exit;
448 end;
449 end;
450 end;
451 addReplacementWad(tf.diskName, fname);
452 end;
453 end;
454 finally
455 resList.Free;
456 g_Res_received_map_start := 0;
457 end;
458 end;
461 initialization
462 conRegVar('rdl_ignore_names', @g_res_ignore_names, 'list of resource wad names (without extensions) to ignore in dl hash checks', 'dl ignore wads');
463 conRegVar('rdl_ignore_enabled', @g_res_ignore_enabled, 'enable dl hash check ignore list', 'dl hash check ignore list active');
464 end.