DEADSOFTWARE

85438c474b4cae73bb9774cb945c740e9d792cb3
[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 (allowRescan: Boolean=false);
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 (allowRescan: Boolean=false);
101 var
102 st: TStream;
103 upmap: Boolean;
104 upres: Boolean;
105 forcesave: Boolean;
106 begin
107 if not assigned(knownMaps) then
108 begin
109 // create and load a know map database, if necessary
110 knownMaps := TFileHashDB.Create(GameDir+'/maps/');
111 knownRes := TFileHashDB.Create(GameDir+'/wads/');
112 saveDBsToDiskEnabled := true;
113 // load map database
114 st := nil;
115 try
116 st := openDiskFileRO(GameDir+'/data/maphash.db');
117 knownMaps.loadFrom(st);
118 e_LogWriteln('loaded map database');
119 except
120 end;
121 st.Free;
122 // load resource database
123 st := nil;
124 try
125 st := openDiskFileRO(GameDir+'/data/reshash.db');
126 knownRes.loadFrom(st);
127 e_LogWriteln('loaded resource database');
128 except
129 end;
130 st.Free;
131 forcesave := true;
132 end
133 else
134 begin
135 if (not allowRescan) then exit;
136 forcesave := false;
137 end;
138 // rescan dirs
139 e_LogWriteln('refreshing map database');
140 upmap := knownMaps.scanFiles();
141 e_LogWriteln('refreshing resource database');
142 upres := knownRes.scanFiles();
143 // save databases
144 if (forcesave) then begin upmap := true; upres := true; end;
145 if upmap or upres then saveDatabases(upmap, upres);
146 end;
149 //==========================================================================
150 //
151 // getWord
152 //
153 // get next word from a string
154 // words are delimited with ';'
155 // ignores leading and trailing spaces
156 // returns empty string if there are no more words
157 //
158 //==========================================================================
159 function getWord (var list: AnsiString): AnsiString;
160 var
161 pos: Integer;
162 begin
163 result := '';
164 while (length(list) > 0) do
165 begin
166 if (ord(list[1]) <= 32) or (list[1] = ';') or (list[1] = ':') then begin Delete(list, 1, 1); continue; end;
167 pos := 1;
168 while (pos <= length(list)) and (list[pos] <> ';') and (list[pos] <> ':') do Inc(pos);
169 result := Copy(list, 1, pos-1);
170 Delete(list, 1, pos);
171 while (length(result) > 0) and (ord(result[length(result)]) <= 32) do Delete(result, length(result), 1);
172 if (length(result) > 0) then exit;
173 end;
174 end;
177 //==========================================================================
178 //
179 // isIgnoredResWad
180 //
181 // checks if the given resource wad can be ignored
182 //
183 // FIXME: preparse name list?
184 //
185 //==========================================================================
186 function isIgnoredResWad (fname: AnsiString): Boolean;
187 var
188 list: AnsiString;
189 name: AnsiString;
190 begin
191 result := false;
192 if (not g_res_ignore_enabled) then exit;
193 fname := forceFilenameExt(ExtractFileName(fname), '');
194 list := g_res_ignore_names;
195 name := getWord(list);
196 while (length(name) > 0) do
197 begin
198 name := forceFilenameExt(name, '');
199 //writeln('*** name=[', name, ']; fname=[', fname, ']');
200 if (StrEquCI1251(name, fname)) then begin result := true; exit; end;
201 name := getWord(list);
202 end;
203 end;
206 //==========================================================================
207 //
208 // clearReplacementWads
209 //
210 // call this before downloading a new map from a server
211 //
212 //==========================================================================
213 procedure clearReplacementWads ();
214 begin
215 if assigned(replacements) then replacements.clear();
216 e_LogWriteln('cleared replacement wads');
217 end;
220 //==========================================================================
221 //
222 // addReplacementWad
223 //
224 // register new replacement wad
225 //
226 //==========================================================================
227 procedure addReplacementWad (oldname: AnsiString; newDiskName: AnsiString);
228 begin
229 e_LogWritefln('adding replacement wad: oldname=%s; newname=%s', [oldname, newDiskName]);
230 if not assigned(replacements) then replacements := THashStrStr.Create();
231 replacements.put(toLowerCase1251(oldname), newDiskName);
232 end;
235 //==========================================================================
236 //
237 // g_Res_FindReplacementWad
238 //
239 // returns original name, or replacement name
240 //
241 //==========================================================================
242 function g_Res_FindReplacementWad (oldname: AnsiString): AnsiString;
243 var
244 fn: AnsiString;
245 begin
246 //e_LogWritefln('LOOKING for replacement wad for [%s]...', [oldname], TMsgType.Notify);
247 result := oldname;
248 if not assigned(replacements) then exit;
249 if (replacements.get(toLowerCase1251(ExtractFileName(oldname)), fn)) then
250 begin
251 //e_LogWritefln('found replacement wad for [%s] -> [%s]', [oldname, fn], TMsgType.Notify);
252 result := fn;
253 end;
254 end;
257 //==========================================================================
258 //
259 // findExistingMapWadWithHash
260 //
261 // find map or resource wad using its base name and hash
262 //
263 // returns found wad disk name, or empty string
264 //
265 //==========================================================================
266 function findExistingMapWadWithHash (fname: AnsiString; const resMd5: TMD5Digest): AnsiString;
267 begin
268 //result := scanDir(GameDir+'/maps', ExtractFileName(fname), resMd5);
269 result := knownMaps.findByHash(resMd5);
270 if (length(result) > 0) then
271 begin
272 result := GameDir+'/maps/'+result;
273 if not FileExists(result) then
274 begin
275 if (knownMaps.scanFiles()) then saveDatabases(true, false);
276 result := '';
277 end;
278 end;
279 end;
282 //==========================================================================
283 //
284 // findExistingResWadWithHash
285 //
286 // find map or resource wad using its base name and hash
287 //
288 // returns found wad disk name, or empty string
289 //
290 //==========================================================================
291 function findExistingResWadWithHash (fname: AnsiString; const resMd5: TMD5Digest): AnsiString;
292 begin
293 //result := scanDir(GameDir+'/wads', ExtractFileName(fname), resMd5);
294 result := knownRes.findByHash(resMd5);
295 if (length(result) > 0) then
296 begin
297 result := GameDir+'/wads/'+result;
298 if not FileExists(result) then
299 begin
300 if (knownRes.scanFiles()) then saveDatabases(false, true);
301 result := '';
302 end;
303 end;
304 end;
307 //==========================================================================
308 //
309 // generateFileName
310 //
311 // generate new file name based on the given one and the hash
312 // you can pass files with pathes here too
313 //
314 //==========================================================================
315 function generateFileName (fname: AnsiString; const hash: TMD5Digest): AnsiString;
316 var
317 mds: AnsiString;
318 path: AnsiString;
319 base: AnsiString;
320 ext: AnsiString;
321 begin
322 mds := MD5Print(hash);
323 if (length(mds) > 16) then mds := Copy(mds, 1, 16);
324 mds := '_'+mds;
325 if (length(fname) = 0) then begin result := mds; exit; end;
326 path := ExtractFilePath(fname);
327 base := ExtractFileName(fname);
328 ext := getFilenameExt(base);
329 base := forceFilenameExt(base, '');
330 if (length(path) > 0) then result := IncludeTrailingPathDelimiter(path) else result := '';
331 result := result+base+mds+ext;
332 end;
335 //==========================================================================
336 //
337 // g_Res_DownloadMapWAD
338 //
339 // download map wad from server (if necessary)
340 // download all required map resource wads too
341 // registers all required replacement wads
342 //
343 // returns name of the map wad (relative to mapdir), or empty string on error
344 //
345 //==========================================================================
346 function g_Res_DownloadMapWAD (FileName: AnsiString; const mapHash: TMD5Digest): AnsiString;
347 var
348 tf: TNetFileTransfer;
349 resList: array of TNetMapResourceInfo = nil;
350 f, res: Integer;
351 strm: TStream;
352 fname: AnsiString;
353 wadname: AnsiString;
354 md5: TMD5Digest;
355 mapdbUpdated: Boolean = false;
356 resdbUpdated: Boolean = false;
357 transStarted: Boolean;
358 begin
359 result := '';
360 clearReplacementWads();
361 g_Res_CreateDatabases();
363 try
364 g_Res_received_map_start := 1;
365 g_Console_Add(Format(_lc[I_NET_MAP_DL], [FileName]));
366 e_WriteLog('Downloading map `' + FileName + '` from server', TMsgType.Notify);
367 g_Game_SetLoadingText(FileName + '...', 0, False);
369 FileName := ExtractFileName(FileName);
370 if (length(FileName) = 0) then FileName := 'fucked_map_wad.wad';
372 // this also sends map request
373 res := g_Net_Wait_MapInfo(tf, resList);
374 if (res <> 0) then exit;
376 // find or download a map
377 result := findExistingMapWadWithHash(tf.diskName, mapHash);
378 if (length(result) = 0) then
379 begin
380 // download map
381 res := g_Net_RequestResFileInfo(-1{map}, tf);
382 if (res <> 0) then
383 begin
384 e_LogWriteln('error requesting map wad');
385 result := '';
386 exit;
387 end;
388 try
389 CreateDir(GameDir+'/maps/downloads');
390 except
391 end;
392 fname := GameDir+'/maps/downloads/'+generateFileName(FileName, mapHash);
393 tf.diskName := fname;
394 try
395 strm := openDiskFileRW(fname);
396 except
397 e_WriteLog('cannot create map file `'+FileName+'`', TMsgType.Fatal);
398 result := '';
399 exit;
400 end;
401 try
402 res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
403 except
404 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
405 strm.Free;
406 result := '';
407 exit;
408 end;
409 strm.Free;
410 if (res <> 0) then
411 begin
412 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
413 result := '';
414 exit;
415 end;
416 // if it was resumed, check md5 and initiate full download if necessary
417 if tf.resumed then
418 begin
419 md5 := MD5File(fname);
420 // sorry for pasta, i am asshole
421 if not MD5Match(md5, tf.hash) then
422 begin
423 e_LogWritefln('resuming failed; downloading map `%s` from scratch...', [fname]);
424 try
425 DeleteFile(fname);
426 strm := createDiskFile(fname);
427 except
428 e_WriteLog('cannot create map file `'+fname+'`', TMsgType.Fatal);
429 result := '';
430 exit;
431 end;
432 try
433 res := g_Net_ReceiveResourceFile(-1{map}, tf, strm);
434 except
435 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
436 strm.Free;
437 result := '';
438 exit;
439 end;
440 strm.Free;
441 if (res <> 0) then
442 begin
443 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
444 result := '';
445 exit;
446 end;
447 end;
448 end;
449 if (knownMaps.addWithHash(fname, mapHash)) then mapdbUpdated := true;
450 result := fname;
451 end;
453 // download resources
454 for f := 0 to High(resList) do
455 begin
456 // if we got a new-style reslist packet, use received data to check for resource files
457 if (resList[f].size < 0) then
458 begin
459 // old-style packet
460 transStarted := true;
461 res := g_Net_RequestResFileInfo(f, tf);
462 if (res <> 0) then begin result := ''; exit; end;
463 end
464 else
465 begin
466 // new-style packet
467 transStarted := false;
468 tf.diskName := resList[f].wadName;
469 tf.hash := resList[f].hash;
470 tf.size := resList[f].size;
471 end;
472 if (isIgnoredResWad(tf.diskName)) then
473 begin
474 // ignored file, abort download
475 if (transStarted) then g_Net_AbortResTransfer(tf);
476 e_LogWritefln('ignoring wad resource `%s` by user request', [tf.diskName]);
477 continue;
478 end;
479 wadname := findExistingResWadWithHash(tf.diskName, tf.hash);
480 if (length(wadname) <> 0) then
481 begin
482 // already here
483 if (transStarted) then g_Net_AbortResTransfer(tf);
484 addReplacementWad(tf.diskName, wadname);
485 end
486 else
487 begin
488 if (not transStarted) then
489 begin
490 res := g_Net_RequestResFileInfo(f, tf);
491 if (res <> 0) then begin result := ''; exit; end;
492 end;
493 try
494 CreateDir(GameDir+'/wads/downloads');
495 except
496 end;
497 fname := GameDir+'/wads/downloads/'+generateFileName(tf.diskName, tf.hash);
498 e_LogWritefln('downloading resource `%s` to `%s`...', [tf.diskName, fname]);
499 try
500 strm := openDiskFileRW(fname);
501 except
502 e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
503 result := '';
504 exit;
505 end;
506 try
507 res := g_Net_ReceiveResourceFile(f, tf, strm);
508 except
509 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
510 strm.Free;
511 result := '';
512 exit;
513 end;
514 strm.Free;
515 if (res <> 0) then
516 begin
517 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
518 result := '';
519 exit;
520 end;
521 // if it was resumed, check md5 and initiate full download if necessary
522 if tf.resumed then
523 begin
524 md5 := MD5File(fname);
525 // sorry for pasta, i am asshole
526 if not MD5Match(md5, tf.hash) then
527 begin
528 e_LogWritefln('resuming failed; downloading resource `%s` to `%s` from scratch...', [tf.diskName, fname]);
529 try
530 DeleteFile(fname);
531 strm := createDiskFile(fname);
532 except
533 e_WriteLog('cannot create resource file `'+fname+'`', TMsgType.Fatal);
534 result := '';
535 exit;
536 end;
537 try
538 res := g_Net_ReceiveResourceFile(f, tf, strm);
539 except
540 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
541 strm.Free;
542 result := '';
543 exit;
544 end;
545 strm.Free;
546 if (res <> 0) then
547 begin
548 e_WriteLog('error downloading map file `'+FileName+'`', TMsgType.Fatal);
549 result := '';
550 exit;
551 end;
552 end;
553 end;
554 addReplacementWad(tf.diskName, fname);
555 if (knownRes.addWithHash(fname, tf.hash)) then resdbUpdated := true;
556 end;
557 end;
558 finally
559 SetLength(resList, 0);
560 g_Res_received_map_start := 0;
561 end;
563 if saveDBsToDiskEnabled and (mapdbUpdated or resdbUpdated) then saveDatabases(mapdbUpdated, resdbUpdated);
564 end;
567 initialization
568 conRegVar('rdl_ignore_names', @g_res_ignore_names, 'list of resource wad names (without extensions) to ignore in dl hash checks', 'dl ignore wads');
569 conRegVar('rdl_ignore_enabled', @g_res_ignore_enabled, 'enable dl hash check ignore list', 'dl hash check ignore list active');
570 conRegVar('rdl_hashdb_save_enabled', @g_res_save_databases, 'enable saving map/resource hash databases to disk', 'controls storing hash databases to disk');
571 end.