trap switches
[dd2d.git] / wadarc.d
blob23290f3a5e26894c515ee5c4fe0c098c102d9804
1 /* DooM2D: Midnight on the Firing Line
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module wadarc is aliced;
19 private:
21 static import core.sync.mutex;
22 import std.stdio : File;
24 import console;
26 //import iv.encoding;
27 import iv.ziparc;
29 import koi8;
32 // ////////////////////////////////////////////////////////////////////////// //
33 //public __gshared WadArchive[] wadList;
34 __gshared ZipArchive[] pk3List;
35 __gshared core.sync.mutex.Mutex glock;
36 __gshared string dataPath;
39 shared static this () {
40 glock = new core.sync.mutex.Mutex;
44 public void setDataPath (const(char)[] path) {
45 while (path.length > 1 && path[0] == '/' && path[1] == '/') path = path[1..$];
46 if (path != "/") while (path.length && path[$-1] == '/') path = path[0..$-1];
47 dataPath = path.idup~"/";
50 public string getDataPath () { pragma(inline, true); return dataPath; }
54 public void addWad (string fname) {
55 if (glock !is null) glock.lock();
56 scope(exit) if (glock !is null) glock.unlock();
57 conwriteln("adding '", fname, "'...");
58 wadList ~= new WadArchive(fname);
63 public void addPK3 (string fname) {
64 if (glock !is null) glock.lock();
65 scope(exit) if (glock !is null) glock.unlock();
66 conwriteln("; adding '", fname, "'...");
67 pk3List ~= new ZipArchive(fname);
71 public File openFile (string fname) {
72 if (glock !is null) glock.lock();
73 scope(exit) if (glock !is null) glock.unlock();
74 try {
75 return File(dataPath~fname);
76 } catch (Exception) {}
77 // now try pk3
78 foreach_reverse (ZipArchive pk3; pk3List) {
79 try {
80 return pk3.fopen(fname);
81 } catch (Exception) {}
83 return File(fname); // to throw a correct exception, lol
87 public string loadTextFile (string fname) {
88 auto fl = openFile(fname);
89 auto sz = fl.size;
90 if (sz < 0 || sz > 1024*1024) throw new Exception("invalid text file size: '"~fname~"'");
91 if (sz == 0) return "";
92 auto res = new char[](cast(uint)sz);
93 if (fl.rawRead(res[]).length != res.length) throw new Exception("error reading text file '"~fname~"'");
94 import std.exception : assumeUnique;
95 return res.assumeUnique;
99 // ////////////////////////////////////////////////////////////////////////// //
101 final class WadArchive {
102 public:
103 import std.stdio : File;
105 private:
106 static struct FileInfo {
107 uint ofs;
108 uint size;
109 string path;
110 string name;
113 // for dir range
114 public static struct DirEntry {
115 string path;
116 string name;
117 uint size;
120 private:
121 File zfl;
122 ulong flstpos;
123 FileInfo[] dir;
125 public:
126 this (string fname) {
127 import std.stdio : File;
128 initLock();
129 zfl = File(fname);
130 open(zfl);
131 scope(failure) { zfl.close; zfl = zfl.init; }
134 // it now owns the file (if no exception was thrown)
135 this (File fl) {
136 initLock();
137 open(fl);
138 scope(success) zfl = fl;
141 @property auto files () {
142 static struct Range {
143 private:
144 WadArchive me;
145 uint curindex;
147 nothrow @safe @nogc:
148 this (WadArchive ame, uint aidx=0) { me = ame; curindex = aidx; }
150 public:
151 @property bool empty () const { return (curindex >= me.dir.length); }
152 @property DirEntry front () const {
153 return DirEntry(
154 (curindex < me.dir.length ? me.dir[cast(usize)curindex].path : null),
155 (curindex < me.dir.length ? me.dir[cast(usize)curindex].name : null),
156 (curindex < me.dir.length ? me.dir[cast(usize)curindex].size : 0));
158 @property Range save () { return Range(me, curindex); }
159 void popFront () { if (curindex < me.dir.length) ++curindex; }
160 @property uint length () const { return me.dir.length; }
161 @property uint position () const { return curindex; } // current position
162 @property void position (uint np) { curindex = np; }
163 void rewind () { curindex = 0; }
165 return Range(this);
168 File fopen (ref in DirEntry de) {
169 string dname = de.name;
170 usize pos = dname.length;
171 while (pos > 0 && dname.ptr[pos-1] != '.') --pos;
172 if (pos > 1) dname = dname[0..pos-1];
173 foreach (immutable idx, ref fi; dir) {
174 if (/*fi.path == de.path &&*/ fi.name == dname) return openDirEntry(idx, fi.name);
176 throw new NamedException!"WadArchive"("file not found");
179 File fopen (const(char)[] fname) {
180 DirEntry de;
181 auto pos = fname.length;
182 while (pos > 0 && fname[pos-1] != '/') --pos;
183 if (pos) {
184 de.path = cast(string)fname[0..pos]; // it's safe here
185 de.name = cast(string)fname[pos..$]; // it's safe here
186 //conwriteln("[", de.path, "] [", de.name, "]");
187 } else {
188 de.name = cast(string)fname; // it's safe here
190 return fopen(de);
193 private:
194 void cleanup () {
195 dir.length = 0;
196 if (zfl.isOpen) zfl.close;
197 zfl = zfl.init;
200 void open (File fl) {
201 import std.uni : icmp;
203 immutable string[$] msn = [
204 "SARG", "TROO", "POSS", "SPOS", "CYBR", "CPOS", "BOSS", "BOS2", "HEAD", "SKUL",
205 "PAIN", "SPID", "BSPI", "FATT", "SKEL", "VILE", "FISH", "BAR1", "ROBO", "PLAY"
208 string curpath;
210 string fixName (const(char)[] name) {
211 return name.idup;
212 if (name.length >= 4 && name[0..4] == "stcf") return name.idup~".vga";
213 if (name.length >= 4 && name[0..4] == "stbf") return name.idup~".vga";
214 if (name.length >= 5 && name[0..5] == "winum") return name.idup~".vga";
215 if (name == "wicolon" || name == "wiminus" || name == "wipcnt") return name.idup~".vga";
216 if (name.length > 3 && name[0..3] == "map") return name.idup~".d2m";
217 if (name == "playpal") return "playpal.pal";
218 switch (name) {
219 case "endoom": return "endoom.b80";
220 case "endanim": return "endanim.a8";
221 case "end2anim": return "end2anim.a8";
222 case "darts": return "darts.a8";
223 case "colormap": return "colormap.tbl";
224 case "mixmap": return "mixmap.tbl";
225 case "titlepic": return "titlepic.vga";
226 case "interpic": return "interpic.vga";
227 case "cd1pic": return "cd1pic.vga";
228 case "endpic": return "endpic.vraw";
229 case "m_therml": return "m_therml.vga";
230 case "m_thermm": return "m_thermm.vga";
231 case "m_thermo": return "m_thermo.vga";
232 case "m_thermr": return "m_thermr.vga";
233 case "m_lscntr": return "m_lscntr.vga";
234 case "m_lsleft": return "m_lsleft.vga";
235 case "m_lsrght": return "m_lsrght.vga";
236 default:
239 import std.algorithm;
240 if (curpath == "sounds/") return name.idup~".snd";
241 if (curpath == "music/") return (name.length > 3 && name[0..3] == "dmi" ? name.idup~".dmi" : name.idup~".dmm");
242 if (curpath == "tilegfx/") return name.idup~".vga";
243 if (curpath.startsWith("sprites/")) return name.idup~".vga";
244 return name.idup;
247 string fixPath (const(char)[] name) {
248 switch (name) {
249 case "m_therml":
250 case "m_thermm":
251 case "m_thermo":
252 case "m_thermr":
253 case "m_lscntr":
254 case "m_lsleft":
255 case "m_lsrght":
256 return "menugfx/";
257 case "rsky1":
258 case "rsky2":
259 case "rsky3":
260 return "sprites/sky/";
261 default:
263 if (name.length >= 4 && name[0..4] == "stcf") return "fonts/stcf/";
264 if (name.length >= 4 && name[0..4] == "stbf") return "fonts/stbf/";
265 if (name.length >= 5 && name[0..5] == "winum") return "fonts/winum/";
266 if (name == "wicolon" || name == "wiminus" || name == "wipcnt") return "fonts/winum/";
267 if (name.length > 3 && name[0..3] == "map") return "maps/";
268 if (name == "d_start") curpath = "sounds/";
269 if (name == "m_start") curpath = "music/";
270 if (name == "w_start") curpath = "tilegfx/";
271 if (name == "s_start") curpath = "sprites/";
272 if (curpath == "sprites/" && name.length > 4) {
273 switch (name[0..4]) {
274 case "sarg": return "sprites/monsters/demon/";
275 case "troo": return "sprites/monsters/imp/";
276 case "poss": return "sprites/monsters/zombie/";
277 case "spos": return "sprites/monsters/sergeant/";
278 case "cybr": return "sprites/monsters/cyberdemon/";
279 case "cpos": return "sprites/monsters/chaingunner/";
280 case "boss": return "sprites/monsters/baron/";
281 case "bos2": return "sprites/monsters/knight/";
282 case "head": return "sprites/monsters/cacodemon/";
283 case "skul": return "sprites/monsters/soul/";
284 case "pain": return "sprites/monsters/painel/";
285 case "spid": return "sprites/monsters/mastermind/";
286 case "bspi": return "sprites/monsters/arachnotron/";
287 case "fatt": return "sprites/monsters/mancubus/";
288 case "skel": return "sprites/monsters/revenant/";
289 case "vile": return "sprites/monsters/archvile/";
290 case "fish": return "sprites/monsters/fish/";
291 case "bar1": return "sprites/monsters/barrel/";
292 case "robo": return "sprites/monsters/robot/";
293 case "play": return "sprites/monsters/player/";
294 default:
297 return curpath;
300 import core.stdc.stdio : SEEK_CUR, SEEK_END;
301 scope(failure) cleanup();
303 uint readU32 () {
304 ubyte[4] data;
305 if (fl.rawRead(data[]).length != data.length) throw new NamedException!"WadArchive"("reading error");
306 return cast(uint)(data[0]+0x100*data[1]+0x10000*data[2]+0x1000000*data[3]);
309 uint lmpofs;
310 uint lmpsize;
311 char[8] lmpname;
313 flstpos = fl.tell;
315 if (fl.rawRead(lmpname[0..4]).length != 4) throw new NamedException!"WadArchive"("reading error");
316 if (lmpname[0..4] != "PWAD" && lmpname[0..4] != "IWAD") throw new NamedException!"WadArchive"("not a WAD file");
317 auto count = readU32();
318 auto dofs = readU32();
319 if (count == 0) return;
320 if (dofs < 3*4 || count == uint.max) throw new NamedException!"WadArchive"("invalid WAD file");
321 fl.seek(dofs-3*4, SEEK_CUR);
322 while (count-- > 0) {
323 lmpofs = readU32();
324 lmpsize = readU32();
325 if (fl.rawRead(lmpname[]).length != 8) throw new NamedException!"WAD"("reading error");
326 int pos = 0;
327 while (pos < 8 && lmpname[pos] != 0) {
328 if (lmpname[pos] >= 'A' && lmpname[pos] <= 'Z') lmpname[pos] += 32;
329 ++pos;
331 if (pos == 0) continue;
332 auto nm = lmpname[0..pos];
333 if (nm.length > 6 && nm[$-6..$] == "_start") {
334 fixPath(nm);
335 continue;
337 if (nm.length > 4 && nm[$-4..$] == "_end") {
338 curpath = null;
339 continue;
341 uint fidx = uint.max;
342 foreach (immutable idx, ref de; dir) if (de.name == lmpname[0..pos]) { fidx = cast(uint)idx; break; }
343 if (fidx >= dir.length) {
344 fidx = cast(uint)dir.length;
345 dir ~= FileInfo();
347 dir[fidx].ofs = lmpofs;
348 dir[fidx].size = lmpsize;
349 //dir[fidx].name = fixName(nm);
350 //dir[fidx].path = fixPath(nm);
352 import std.uni : toLower;
353 dir[fidx].name = koi8lotranslit(recodeToKOI8(recode(fixName(nm), "utf-8", "cp866").toLower, "utf-8"));
354 dir[fidx].path = koi8lotranslit(recodeToKOI8(recode(fixPath(nm), "utf-8", "cp866").toLower, "utf-8"));
356 //debug conwriteln(dir[fidx].path, " : ", dir[fidx].name);
358 debug conwriteln(dir.length, " files found");
362 // ////////////////////////////////////////////////////////////////////// //
363 static import core.sync.mutex;
365 core.sync.mutex.Mutex lock;
367 void initLock () {
368 lock = new core.sync.mutex.Mutex;
371 auto openDirEntry (uint idx, string filename) {
372 import core.sys.linux.stdio : fopencookie;
373 import core.stdc.stdio : FILE;
374 import core.stdc.stdio : fopen, fclose;
375 import core.stdc.stdlib : calloc, free;
376 import etc.c.zlib;
377 import std.internal.cstring : tempCString;
378 import core.memory : GC;
380 if (!zfl.isOpen) throw new NamedException!"WadArchive"("archive wasn't opened");
381 if (idx >= dir.length) throw new NamedException!"WadArchive"("invalid dir index");
383 // create cookied `FILE*`
384 auto fc = cast(InnerFileCookied*)calloc(1, InnerFileCookied.sizeof);
385 scope(exit) if (fc !is null) free(fc);
386 if (fc is null) {
387 import core.exception : onOutOfMemoryErrorNoGC;
388 onOutOfMemoryErrorNoGC();
390 (*fc) = InnerFileCookied.init;
391 (*fc).stpos = flstpos+dir[idx].ofs;
392 (*fc).size = cast(uint)dir[idx].size;
393 (*fc).lock = lock;
394 GC.addRange(fc, InnerFileCookied.sizeof);
395 (*fc).xfl = zfl;
396 // open `cooked` file
397 FILE* fres = fopencookie(cast(void*)fc, "r", fcdatpkCallbacks);
398 if (fres is null) {
399 // alas
400 if ((*fc).fl !is null) fclose((*fc).fl);
401 try { (*fc).xfl.detach(); } catch (Exception) {}
402 throw new NamedException!"WadArchive"("can't open cookied file");
404 // ok
405 fc = null;
406 return File(fres, filename);
410 // ////////////////////////////////////////////////////////////////////// //
411 // "inner" file processor; processes both packed and unpacked files
412 // can be used as normal disk file processor too
413 static struct InnerFileCookied {
414 private import core.sys.posix.sys.types : ssize_t, off64_t = off_t;
415 private import core.stdc.stdio : FILE;
417 enum ibsize = 32768;
419 core.sync.mutex.Mutex lock;
420 // note that either one of `fl` or `xfl` must be opened and operational
421 FILE* fl; // disk file, can be `null`
422 File xfl; // disk file, can be closed
423 long stpos; // starting position
424 uint size; // unpacked size
425 uint pos; // current file position
426 //uint prpos; // previous file position
427 //uint pkpos; // current position in DAT
428 //ubyte[] pkb; // packed data
429 bool eoz;
431 @disable this (this);
433 nothrow:
434 ~this () { close(); }
436 @property bool isOpen () @safe /*@nogc*/ { return (fl !is null || xfl.isOpen); }
438 void close () {
439 import core.memory : GC;
440 import core.stdc.stdlib : free;
442 if (lock !is null) lock.lock();
443 scope(exit) if (lock !is null) lock.unlock();
444 if (fl !is null) {
445 import core.stdc.stdio : fclose;
446 fclose(fl);
447 fl = null;
449 try { xfl.detach(); } catch (Exception) {} // it's safe to detach closed File
451 eoz = true;
454 ssize_t read (void* buf, size_t count) {
455 if (buf is null) return -1;
456 if (count == 0 || size == 0) return 0;
457 lock.lock();
458 scope(exit) lock.unlock();
459 if (!isOpen) return -1; // read error
460 if (pos >= size) return 0; // EOF
462 import core.stdc.stdio : ferror, fread;
463 import core.sys.posix.stdio : fseeko;
464 if (size-pos < count) count = cast(size_t)(size-pos);
465 if (fl !is null) {
466 // `FILE*`
467 if (fseeko(fl, stpos+pos, 0) < 0) return -1;
468 auto rd = fread(buf, 1, count, fl);
469 if (rd != count && (rd < 0 || ferror(fl))) rd = -1;
470 if (rd > 0) pos += rd;
471 return rd;
472 } else {
473 // std.stdio.File
474 try {
475 xfl.seek(stpos+pos, 0);
476 auto rd = xfl.rawRead(buf[0..count]);
477 pos += rd.length;
478 return (rd.length == count ? rd.length : -1);
479 } catch (Exception) {} //BAD DOGGY!
480 return -1;
485 long seek (long ofs, int whence) {
486 lock.lock();
487 scope(exit) lock.unlock();
488 if (!isOpen) return -1;
489 //TODO: overflow checks
490 switch (whence) {
491 case 0: // SEEK_SET
492 break;
493 case 1: // SEEK_CUR
494 ofs += pos;
495 break;
496 case 2: // SEEK_END
497 if (ofs > 0) ofs = 0;
498 ofs += size;
499 break;
500 default:
501 return -1;
503 if (ofs < 0) return -1;
504 if (ofs > size) ofs = size;
505 pos = cast(uint)ofs;
506 return ofs;
511 static:
512 // ////////////////////////////////////////////////////////////////////// //
513 extern(C) nothrow {
514 import core.sys.linux.stdio : cookie_io_functions_t;
515 import core.sys.posix.sys.types : ssize_t, off64_t = off_t;
517 ssize_t fcdatpkRead (void* cookie, char* buf, size_t count) {
518 //conwriteln("reading ", count, " bytes");
519 import core.stdc.errno;
520 auto fc = cast(InnerFileCookied*)cookie;
521 auto res = fc.read(buf, count);
522 if (res < 0) { errno = EIO; return -1; }
523 return res;
526 ssize_t fcdatpkWrite (void* cookie, const(char)* buf, size_t count) {
527 //conwriteln("writing ", count, " bytes");
528 import core.stdc.errno;
529 errno = EIO; //FIXME: find better code
530 return 0; // error; write should not return `-1`
533 int fcdatpkSeek (void* cookie, off64_t* offset, int whence) {
534 //conwriteln("seeking ", *offset, " bytes, whence=", whence);
535 import core.stdc.errno;
536 auto fc = cast(InnerFileCookied*)cookie;
537 auto res = fc.seek(*offset, whence);
538 if (res < 0) { errno = EIO; return -1; }
539 *offset = cast(off64_t)res;
540 return 0;
543 int fcdatpkClose (void* cookie) {
544 import core.memory : GC;
545 import core.stdc.stdlib : free;
546 //conwriteln("closing");
547 auto fc = cast(InnerFileCookied*)cookie;
548 //fc.close();
549 GC.removeRange(cookie);
550 try { fc.__dtor(); } catch (Exception) {}
551 // no need to run finalizers, we SHOULD NOT have any
552 //try { GC.runFinalizers(cookie[0..InnerFileCookied.sizeof]); } catch (Exception) {}
553 //fc.xfl.__dtor();
554 free(cookie);
555 //conwriteln("closed");
556 return 0;
560 __gshared cookie_io_functions_t fcdatpkCallbacks = cookie_io_functions_t(
561 /*.read =*/ &fcdatpkRead,
562 /*.write =*/ &fcdatpkWrite,
563 /*.seek =*/ &fcdatpkSeek,
564 /*.close =*/ &fcdatpkClose,
567 static:
568 T[] xalloc(T) (size_t len) {
569 import core.stdc.stdlib : malloc;
570 if (len < 1) return null;
571 auto res = cast(T*)malloc(len*T.sizeof);
572 if (res is null) {
573 import core.exception : onOutOfMemoryErrorNoGC;
574 onOutOfMemoryErrorNoGC();
576 res[0..len] = T.init;
577 return res[0..len];
580 void xfree(T) (ref T[] slc) {
581 if (slc.ptr !is null) {
582 import core.stdc.stdlib : free;
583 free(slc.ptr);
585 slc = null;