DEADSOFTWARE

68ceb0a71aa8be99baf763cbf8d98ba5b8ad366c
[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;
32 // call this somewhere in startup sequence
33 procedure g_Res_CreateDatabases ();
36 implementation
38 uses g_language, sfs, utils, wadreader, g_game, hashtable, fhashdb;
40 var
41 // cvars
42 g_res_ignore_names: AnsiString = 'standart;shrshade';
43 g_res_ignore_enabled: Boolean = true;
44 g_res_save_databases: Boolean = true;
45 // other vars
46 replacements: THashStrStr = nil;
47 knownMaps: TFileHashDB = nil;
48 knownRes: TFileHashDB = nil;
49 saveDBsToDiskEnabled: Boolean = false; // this will be set to `true` if initial database saving succeed
52 //==========================================================================
53 //
54 // saveDatabases
55 //
56 //==========================================================================
57 procedure saveDatabases (saveMap, saveRes: Boolean);
58 var
59 err: Boolean;
60 st: TStream;
61 begin
62 if (not saveDBsToDiskEnabled) or (not g_res_save_databases) then exit;
63 // rescan dirs
64 // save map database
65 if (saveMap) then
66 begin
67 err := true;
68 st := nil;
69 try
70 st := createDiskFile(GameDir+'/data/maphash.db');
71 knownMaps.saveTo(st);
72 err := false;
73 except
74 end;
75 st.Free;
76 if (err) then begin saveDBsToDiskEnabled := false; e_LogWriteln('cannot write map database, disk refresh disabled'); exit; end;
77 end;
78 // save resource database
79 if (saveRes) then
80 begin
81 err := true;
82 st := nil;
83 try
84 st := createDiskFile(GameDir+'/data/reshash.db');
85 knownRes.saveTo(st);
86 err := false;
87 except
88 end;
89 st.Free;
90 if (err) then begin saveDBsToDiskEnabled := false; e_LogWriteln('cannot write resource database, disk refresh disabled'); exit; end;
91 end;
92 end;
95 //==========================================================================
96 //
97 // g_Res_CreateDatabases
98 //
99 //==========================================================================
100 procedure g_Res_CreateDatabases ();
101 var
102 st: TStream;
103 begin
104 // create and load a know map database, if necessary
105 knownMaps.Free;
106 knownMaps := TFileHashDB.Create(GameDir+'/maps/');
107 knownRes := TFileHashDB.Create(GameDir+'/wads/');
108 saveDBsToDiskEnabled := true;
109 // load map database
110 st := nil;
111 try
112 st := openDiskFileRO(GameDir+'/data/maphash.db');
113 knownMaps.loadFrom(st);
114 e_LogWriteln('loaded map database');
115 except
116 end;
117 st.Free;
118 // load resource database
119 st := nil;
120 try
121 st := openDiskFileRO(GameDir+'/data/reshash.db');
122 knownRes.loadFrom(st);
123 e_LogWriteln('loaded resource database');
124 except
125 end;
126 st.Free;
127 // rescan dirs
128 e_LogWriteln('refreshing map database');
129 knownMaps.scanFiles();
130 e_LogWriteln('refreshing resource database');
131 knownRes.scanFiles();
132 // save databases
133 saveDatabases(true, true);
134 end;
137 //==========================================================================
138 //
139 // getWord
140 //
141 // get next word from a string
142 // words are delimited with ';'
143 // ignores leading and trailing spaces
144 // returns empty string if there are no more words
145 //
146 //==========================================================================
147 function getWord (var list: AnsiString): AnsiString;
148 var
149 pos: Integer;
150 begin
151 result := '';
152 while (length(list) > 0) do
153 begin
154 if (ord(list[1]) <= 32) or (list[1] = ';') or (list[1] = ':') then begin Delete(list, 1, 1); continue; end;
155 pos := 1;
156 while (pos <= length(list)) and (list[pos] <> ';') and (list[pos] <> ':') do Inc(pos);
157 result := Copy(list, 1, pos-1);
158 Delete(list, 1, pos);
159 while (length(result) > 0) and (ord(result[length(result)]) <= 32) do Delete(result, length(result), 1);
160 if (length(result) > 0) then exit;
161 end;
162 end;
165 //==========================================================================
166 //
167 // isIgnoredResWad
168 //
169 // checks if the given resource wad can be ignored
170 //
171 // FIXME: preparse name list?
172 //
173 //==========================================================================
174 function isIgnoredResWad (fname: AnsiString): Boolean;
175 var
176 list: AnsiString;
177 name: AnsiString;
178 begin
179 result := false;
180 if (not g_res_ignore_enabled) then exit;
181 fname := forceFilenameExt(ExtractFileName(fname), '');
182 list := g_res_ignore_names;
183 name := getWord(list);
184 while (length(name) > 0) do
185 begin
186 name := forceFilenameExt(name, '');
187 //writeln('*** name=[', name, ']; fname=[', fname, ']');
188 if (StrEquCI1251(name, fname)) then begin result := true; exit; end;
189 name := getWord(list);
190 end;
191 end;
194 //==========================================================================
195 //
196 // clearReplacementWads
197 //
198 // call this before downloading a new map from a server
199 //
200 //==========================================================================
201 procedure clearReplacementWads ();
202 begin
203 if assigned(replacements) then replacements.clear();
204 e_LogWriteln('cleared replacement wads');
205 end;
208 //==========================================================================
209 //
210 // addReplacementWad
211 //
212 // register new replacement wad
213 //
214 //==========================================================================
215 procedure addReplacementWad (oldname: AnsiString; newDiskName: AnsiString);
216 begin
217 e_LogWritefln('adding replacement wad: oldname=%s; newname=%s', [oldname, newDiskName]);
218 if not assigned(replacements) then replacements := THashStrStr.Create();
219 replacements.put(toLowerCase1251(oldname), newDiskName);
220 end;
223 //==========================================================================
224 //
225 // g_Res_FindReplacementWad
226 //
227 // returns original name, or replacement name
228 //
229 //==========================================================================
230 function g_Res_FindReplacementWad (oldname: AnsiString): AnsiString;
231 var
232 fn: AnsiString;
233 begin
234 //e_LogWritefln('LOOKING for replacement wad for [%s]...', [oldname], TMsgType.Notify);
235 result := oldname;
236 if not assigned(replacements) then exit;
237 if (replacements.get(toLowerCase1251(ExtractFileName(oldname)), fn)) then
238 begin
239 //e_LogWritefln('found replacement wad for [%s] -> [%s]', [oldname, fn], TMsgType.Notify);
240 result := fn;
241 end;
242 end;
245 //==========================================================================
246 //
247 // scanDir
248 //
249 // look for a wad to match the hash
250 // scans subdirs, ignores known wad extensions
251 //
252 // returns found wad disk name, or empty string
253 //
254 //==========================================================================
255 (*
256 function scanDir (dirName: AnsiString; baseName: AnsiString; const resMd5: TMD5Digest): AnsiString;
257 var
258 searchResult: TSearchRec;
259 dfn: AnsiString;
260 md5: TMD5Digest;
261 dirs: array of AnsiString;
262 f: Integer;
263 begin
264 result := '';
265 SetLength(dirs, 0);
266 if (length(baseName) = 0) then exit;
267 dirName := IncludeTrailingPathDelimiter(dirName);
268 e_LogWritefln('scanning dir `%s` for file `%s`...', [dirName, baseName]);
270 // scan files
271 if (FindFirst(dirName+'*', faAnyFile, searchResult) <> 0) then exit;
272 try
273 repeat
274 if ((searchResult.Attr and faDirectory) = 0) then
275 begin
276 if (isWadNamesEqu(searchResult.Name, baseName)) then
277 begin
278 dfn := dirName+searchResult.Name;
279 if FileExists(dfn) then
280 begin
281 e_LogWritefln(' found `%s`...', [dfn]);
282 md5 := MD5File(dfn);
283 if MD5Match(md5, resMd5) then
284 begin
285 e_LogWritefln(' MATCH `%s`...', [dfn]);
286 SetLength(dirs, 0);
287 result := dfn;
288 exit;
289 end;
290 end;
291 end;
292 end
293 else
294 begin
295 if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
296 begin
297 dfn := dirName+searchResult.Name;
298 SetLength(dirs, Length(dirs)+1);
299 dirs[length(dirs)-1] := dfn;
300 end;
301 end;
302 until (FindNext(searchResult) <> 0);
303 finally
304 FindClose(searchResult);
305 end;
307 // scan subdirs
308 for f := 0 to High(dirs) do
309 begin
310 dfn := dirs[f];
311 result := scanDir(dfn, baseName, resMd5);
312 if (length(result) <> 0) then begin SetLength(dirs, 0); exit; end;
313 end;
314 SetLength(dirs, 0);
315 end;
316 *)
319 //==========================================================================
320 //
321 // findExistingMapWadWithHash
322 //
323 // find map or resource wad using its base name and hash
324 //
325 // returns found wad disk name, or empty string
326 //
327 //==========================================================================
328 function findExistingMapWadWithHash (fname: AnsiString; const resMd5: TMD5Digest): AnsiString;
329 begin
330 //result := scanDir(GameDir+'/maps', ExtractFileName(fname), resMd5);
331 result := knownMaps.findByHash(resMd5);
332 if (length(result) > 0) then
333 begin
334 result := GameDir+'/maps/'+result;
335 if not FileExists(result) then
336 begin
337 if (knownMaps.scanFiles()) then saveDatabases(true, false);
338 result := '';
339 end;
340 end;
341 end;
344 //==========================================================================
345 //
346 // findExistingResWadWithHash
347 //
348 // find map or resource wad using its base name and hash
349 //
350 // returns found wad disk name, or empty string
351 //
352 //==========================================================================
353 function findExistingResWadWithHash (fname: AnsiString; const resMd5: TMD5Digest): AnsiString;
354 begin
355 //result := scanDir(GameDir+'/wads', ExtractFileName(fname), resMd5);
356 result := knownRes.findByHash(resMd5);
357 if (length(result) > 0) then
358 begin
359 result := GameDir+'/wads/'+result;
360 if not FileExists(result) then
361 begin
362 if (knownRes.scanFiles()) then saveDatabases(false, true);
363 result := '';
364 end;
365 end;
366 end;
369 //==========================================================================
370 //
371 // generateFileName
372 //
373 // generate new file name based on the given one and the hash
374 // you can pass files with pathes here too
375 //
376 //==========================================================================
377 function generateFileName (fname: AnsiString; const hash: TMD5Digest): AnsiString;
378 var
379 mds: AnsiString;
380 path: AnsiString;
381 base: AnsiString;
382 ext: AnsiString;
383 begin
384 mds := MD5Print(hash);
385 if (length(mds) > 16) then mds := Copy(mds, 1, 16);
386 mds := '_'+mds;
387 if (length(fname) = 0) then begin result := mds; exit; end;
388 path := ExtractFilePath(fname);
389 base := ExtractFileName(fname);
390 ext := getFilenameExt(base);
391 base := forceFilenameExt(base, '');
392 if (length(path) > 0) then result := IncludeTrailingPathDelimiter(path) else result := '';
393 result := result+base+mds+ext;
394 end;
397 //==========================================================================
398 //
399 // g_Res_DownloadMapWAD
400 //
401 // download map wad from server (if necessary)
402 // download all required map resource wads too
403 // registers all required replacement wads
404 //
405 // returns name of the map wad (relative to mapdir), or empty string on error
406 //
407 //==========================================================================
408 function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
409 var
410 tf: TNetFileTransfer;
411 resList: TStringList;
412 f, res: Integer;
413 strm: TStream;
414 fname: AnsiString;
415 wadname: AnsiString;
416 md5: TMD5Digest;
417 mapdbUpdated: Boolean = false;
418 resdbUpdated: Boolean = false;
419 begin
420 result := '';
421 clearReplacementWads();
423 resList := TStringList.Create();
425 try
426 g_Res_received_map_start := 1;
427 g_Console_Add(Format(_lc[I_NET_MAP_DL], [FileName]));
428 e_WriteLog('Downloading map `' + FileName + '` from server', TMsgType.Notify);
429 g_Game_SetLoadingText(FileName + '...', 0, False);
431 FileName := ExtractFileName(FileName);
432 if (length(FileName) = 0) then FileName := 'fucked_map_wad.wad';
434 // this also sends map request
435 res := g_Net_Wait_MapInfo(tf, resList);
436 if (res <> 0) then exit;
438 // find or download a map
439 result := findExistingMapWadWithHash(tf.diskName, mapHash);
440 if (length(result) = 0) then
441 begin
442 // download map
443 res := g_Net_RequestResFileInfo(-1{map}, tf);
444 if (res <> 0) then
445 begin
446 e_LogWriteln('error requesting map wad');
447 result := '';
448 exit;
449 end;
450 try
451 CreateDir(GameDir+'/maps/downloads');
452 except
453 end;
454 fname := GameDir+'/maps/downloads/'+generateFileName(FileName, mapHash);
455 tf.diskName := fname;
456 try
457 strm := openDiskFileRW(fname);
458 except
459 e_WriteLog('cannot create map file `'+FileName+'`', TMsgType.Fatal);
460 result := '';
461 exit;
462 end;
463 try
464 res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
465 except
466 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
467 strm.Free;
468 result := '';
469 exit;
470 end;
471 strm.Free;
472 if (res <> 0) then
473 begin
474 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
475 result := '';
476 exit;
477 end;
478 // if it was resumed, check md5 and initiate full download if necessary
479 if tf.resumed then
480 begin
481 md5 := MD5File(fname);
482 // sorry for pasta, i am asshole
483 if not MD5Match(md5, tf.hash) then
484 begin
485 e_LogWritefln('resuming failed; downloading map `%s` from scratch...', [fname]);
486 try
487 DeleteFile(fname);
488 strm := createDiskFile(fname);
489 except
490 e_WriteLog('cannot create map file `'+fname+'`', TMsgType.Fatal);
491 result := '';
492 exit;
493 end;
494 try
495 res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
496 except
497 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
498 strm.Free;
499 result := '';
500 exit;
501 end;
502 strm.Free;
503 if (res <> 0) then
504 begin
505 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
506 result := '';
507 exit;
508 end;
509 end;
510 end;
511 if (knownMaps.addWithHash(fname, mapHash)) then mapdbUpdated := true;
512 result := fname;
513 end;
515 // download resources
516 for f := 0 to resList.Count-1 do
517 begin
518 res := g_Net_RequestResFileInfo(f, tf);
519 if (res <> 0) then begin result := ''; exit; end;
520 if (isIgnoredResWad(tf.diskName)) then
521 begin
522 // ignored file, abort download
523 g_Net_AbortResTransfer(tf);
524 e_LogWritefln('ignoring wad resource `%s` by user request', [tf.diskName]);
525 continue;
526 end;
527 wadname := findExistingResWadWithHash(tf.diskName, tf.hash);
528 if (length(wadname) <> 0) then
529 begin
530 // already here
531 g_Net_AbortResTransfer(tf);
532 addReplacementWad(tf.diskName, wadname);
533 end
534 else
535 begin
536 try
537 CreateDir(GameDir+'/wads/downloads');
538 except
539 end;
540 fname := GameDir+'/wads/downloads/'+generateFileName(tf.diskName, tf.hash);
541 e_LogWritefln('downloading resource `%s` to `%s`...', [tf.diskName, fname]);
542 try
543 strm := openDiskFileRW(fname);
544 except
545 e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
546 result := '';
547 exit;
548 end;
549 try
550 res := g_Net_ReceiveResourceFile(f, tf, strm);
551 except
552 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
553 strm.Free;
554 result := '';
555 exit;
556 end;
557 strm.Free;
558 if (res <> 0) then
559 begin
560 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
561 result := '';
562 exit;
563 end;
564 // if it was resumed, check md5 and initiate full download if necessary
565 if tf.resumed then
566 begin
567 md5 := MD5File(fname);
568 // sorry for pasta, i am asshole
569 if not MD5Match(md5, tf.hash) then
570 begin
571 e_LogWritefln('resuming failed; downloading resource `%s` to `%s` from scratch...', [tf.diskName, fname]);
572 try
573 DeleteFile(fname);
574 strm := createDiskFile(fname);
575 except
576 e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
577 result := '';
578 exit;
579 end;
580 try
581 res := g_Net_ReceiveResourceFile(f, tf, strm);
582 except
583 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
584 strm.Free;
585 result := '';
586 exit;
587 end;
588 strm.Free;
589 if (res <> 0) then
590 begin
591 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
592 result := '';
593 exit;
594 end;
595 end;
596 end;
597 addReplacementWad(tf.diskName, fname);
598 if (knownRes.addWithHash(fname, tf.hash)) then resdbUpdated := true;
599 end;
600 end;
601 finally
602 resList.Free;
603 g_Res_received_map_start := 0;
604 end;
606 if saveDBsToDiskEnabled and (mapdbUpdated or resdbUpdated) then saveDatabases(mapdbUpdated, resdbUpdated);
607 end;
610 initialization
611 conRegVar('rdl_ignore_names', @g_res_ignore_names, 'list of resource wad names (without extensions) to ignore in dl hash checks', 'dl ignore wads');
612 conRegVar('rdl_ignore_enabled', @g_res_ignore_enabled, 'enable dl hash check ignore list', 'dl hash check ignore list active');
613 conRegVar('rdl_hashdb_save_enabled', @g_res_save_databases, 'enable saving map/resource hash databases to disk', 'controls storing hash databases to disk');
614 end.