DEADSOFTWARE

961a84c99260bfac880e028fde712135fc07435d
[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 // g_Res_DownloadMapWAD
372 //
373 // download map wad from server (if necessary)
374 // download all required map resource wads too
375 // registers all required replacement wads
376 //
377 // returns name of the map wad (relative to mapdir), or empty string on error
378 //
379 //==========================================================================
380 function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
381 var
382 tf: TNetFileTransfer;
383 resList: TStringList;
384 f, res: Integer;
385 strm: TStream;
386 fname: AnsiString;
387 wadname: AnsiString;
388 md5: TMD5Digest;
389 mapdbUpdated: Boolean = false;
390 resdbUpdated: Boolean = false;
391 begin
392 result := '';
393 clearReplacementWads();
395 resList := TStringList.Create();
397 try
398 g_Res_received_map_start := 1;
399 g_Console_Add(Format(_lc[I_NET_MAP_DL], [FileName]));
400 e_WriteLog('Downloading map `' + FileName + '` from server', TMsgType.Notify);
401 g_Game_SetLoadingText(FileName + '...', 0, False);
402 if (not g_Net_SendMapRequest()) then exit;
404 FileName := ExtractFileName(FileName);
405 if (length(FileName) = 0) then FileName := 'fucked_map_wad.wad';
406 res := g_Net_Wait_MapInfo(tf, resList);
407 if (res <> 0) then exit;
409 // find or download a map
410 result := findExistingMapWadWithHash(tf.diskName, mapHash);
411 if (length(result) = 0) then
412 begin
413 // download map
414 res := g_Net_RequestResFileInfo(-1{map}, tf);
415 if (res <> 0) then
416 begin
417 e_LogWriteln('error requesting map wad');
418 result := '';
419 exit;
420 end;
421 try
422 CreateDir(GameDir+'/maps/downloads');
423 except
424 end;
425 fname := GameDir+'/maps/downloads/'+FileName;
426 try
427 strm := openDiskFileRW(fname);
428 except
429 e_WriteLog('cannot create map file `'+FileName+'`', TMsgType.Fatal);
430 result := '';
431 exit;
432 end;
433 tf.diskName := fname;
434 try
435 res := g_Net_ReceiveResourceFile(-1{map}, 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 // if it was resumed, check md5 and initiate full download if necessary
450 if tf.resumed then
451 begin
452 md5 := MD5File(fname);
453 // sorry for pasta, i am asshole
454 if not MD5Match(md5, tf.hash) then
455 begin
456 e_LogWritefln('resuming failed; downloading map `%s` from scratch...', [fname]);
457 try
458 DeleteFile(fname);
459 strm := createDiskFile(fname);
460 except
461 e_WriteLog('cannot create map file `'+fname+'`', TMsgType.Fatal);
462 result := '';
463 exit;
464 end;
465 try
466 res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
467 except
468 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
469 strm.Free;
470 result := '';
471 exit;
472 end;
473 strm.Free;
474 if (res <> 0) then
475 begin
476 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
477 result := '';
478 exit;
479 end;
480 end;
481 end;
482 if (knownMaps.addWithHash(fname, mapHash)) then mapdbUpdated := true;
483 result := fname;
484 end;
486 // download resources
487 for f := 0 to resList.Count-1 do
488 begin
489 res := g_Net_RequestResFileInfo(f, tf);
490 if (res <> 0) then begin result := ''; exit; end;
491 if (isIgnoredResWad(tf.diskName)) then
492 begin
493 // ignored file, abort download
494 g_Net_AbortResTransfer(tf);
495 e_LogWritefln('ignoring wad resource `%s` by user request', [tf.diskName]);
496 continue;
497 end;
498 wadname := findExistingResWadWithHash(tf.diskName, tf.hash);
499 if (length(wadname) <> 0) then
500 begin
501 // already here
502 g_Net_AbortResTransfer(tf);
503 addReplacementWad(tf.diskName, wadname);
504 end
505 else
506 begin
507 try
508 CreateDir(GameDir+'/wads/downloads');
509 except
510 end;
511 fname := GameDir+'/wads/downloads/'+tf.diskName;
512 e_LogWritefln('downloading resource `%s` to `%s`...', [tf.diskName, fname]);
513 try
514 strm := openDiskFileRW(fname);
515 except
516 e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
517 result := '';
518 exit;
519 end;
520 try
521 res := g_Net_ReceiveResourceFile(f, tf, strm);
522 except
523 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
524 strm.Free;
525 result := '';
526 exit;
527 end;
528 strm.Free;
529 if (res <> 0) then
530 begin
531 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
532 result := '';
533 exit;
534 end;
535 // if it was resumed, check md5 and initiate full download if necessary
536 if tf.resumed then
537 begin
538 md5 := MD5File(fname);
539 // sorry for pasta, i am asshole
540 if not MD5Match(md5, tf.hash) then
541 begin
542 e_LogWritefln('resuming failed; downloading resource `%s` to `%s` from scratch...', [tf.diskName, fname]);
543 try
544 DeleteFile(fname);
545 strm := createDiskFile(fname);
546 except
547 e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
548 result := '';
549 exit;
550 end;
551 try
552 res := g_Net_ReceiveResourceFile(f, tf, strm);
553 except
554 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
555 strm.Free;
556 result := '';
557 exit;
558 end;
559 strm.Free;
560 if (res <> 0) then
561 begin
562 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
563 result := '';
564 exit;
565 end;
566 end;
567 end;
568 addReplacementWad(tf.diskName, fname);
569 if (knownRes.addWithHash(fname, tf.hash)) then resdbUpdated := true;
570 end;
571 end;
572 finally
573 resList.Free;
574 g_Res_received_map_start := 0;
575 end;
577 if saveDBsToDiskEnabled and (mapdbUpdated or resdbUpdated) then saveDatabases(mapdbUpdated, resdbUpdated);
578 end;
581 initialization
582 conRegVar('rdl_ignore_names', @g_res_ignore_names, 'list of resource wad names (without extensions) to ignore in dl hash checks', 'dl ignore wads');
583 conRegVar('rdl_ignore_enabled', @g_res_ignore_enabled, 'enable dl hash check ignore list', 'dl hash check ignore list active');
584 conRegVar('rdl_hashdb_save_enabled', @g_res_save_databases, 'enable saving map/resource hash databases to disk', 'controls storing hash databases to disk');
585 end.