removed "koi8" module (it's in iv.vfs now)
[dd2d.git] / d2dmap.d
blobe1c66c7466e79a8cbced14ca4c59b1fc51c09f4e
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 d2dmap is aliced;
19 private:
21 import arsd.color;
22 import iv.stream;
24 import glutils;
25 import console;
26 import wadarc;
28 import d2dgfx;
29 //import d2dtpl;
31 //import iv.encoding;
33 import iv.vfs.koi8;
36 // ////////////////////////////////////////////////////////////////////////// //
37 Color getPixel (TrueColorImage img, int x, int y) {
38 if (x >= 0 && y >= 0 && x < img.width && y < img.height) {
39 return img.imageData.colors.ptr[y*img.width+x];
40 } else {
41 return Color(0, 0, 0, 0);
46 void putPixel (TrueColorImage img, int x, int y, Color clr) {
47 if (x >= 0 && y >= 0 && x < img.width && y < img.height && clr.a != 0) {
48 img.imageData.colors.ptr[y*img.width+x] = clr;
53 void fixPixelA (TrueColorImage img, int x, int y, ubyte a) {
54 if (x >= 0 && y >= 0 && x < img.width && y < img.height) {
55 img.imageData.colors.ptr[y*img.width+x].a = a;
60 void fixPixelTileA (TrueColorImage img, int x, int y, ubyte a) {
61 foreach (int dy; 0..8) {
62 foreach (int dx; 0..8) {
63 fixPixelA(img, x+dx, y+dy, a);
69 void putPixelTile (TrueColorImage img, int x, int y, Color clr) {
70 foreach (int dy; 0..8) {
71 foreach (int dx; 0..8) {
72 putPixel(img, x+dx, y+dy, clr);
78 void putTile (TrueColorImage img, int x, int y, D2DImage wt) {
79 if (wt is null) return;
80 x -= wt.sx;
81 y -= wt.sy;
82 auto s = wt.data.ptr;
83 foreach (int dy; 0..wt.height) {
84 foreach (int dx; 0..wt.width) {
85 img.putPixel(x+dx, y, d2dpal[*s++]);
87 ++y;
92 // temp
93 static ubyte clampByte (ubyte b, int delta) {
94 delta += b;
95 return cast(ubyte)(delta < 0 ? 0 : delta > 255 ? 255 : delta);
99 // ////////////////////////////////////////////////////////////////////////// //
100 public final class LevelMap {
101 private:
102 enum MapVersion = 2; // óÁÍÁÑ ÐÏÓÌÅÄÎÑÑ ×ÅÒÓÉÑ ËÁÒÔÙ
103 public enum MapSize = 100;
105 public:
106 // tile type
107 enum : ubyte {
108 TILE_EMPTY = 0,
109 TILE_WALL = 1,
110 TILE_DOORC = 2, // closed door
111 TILE_DOORO = 3, // opened door
112 TILE_STEP = 4,
113 TILE_WATER = 5,
114 TILE_ACID1 = 6,
115 TILE_ACID2 = 7,
116 TILE_MBLOCK = 8, // just blocks monsters
117 TILE_LIFTU = 9,
118 TILE_LIFTD = 10,
120 TILE_ACTTRAP = 255,
123 enum : short {
124 MB_COMMENT = -1,
125 MB_END = 0,
126 MB_WALLNAMES,
127 MB_BACK,
128 MB_WTYPE,
129 MB_FRONT,
130 MB_THING,
131 MB_SWITCH,
132 MB_MUSIC, // usually 8 bytes
133 MB_SKY, // ushort: [1..3]
134 MB_SWITCH2,
137 enum {
138 SW_PL_PRESS = 1<<0,
139 SW_MN_PRESS = 1<<1,
140 SW_PL_NEAR = 1<<2,
141 SW_MN_NEAR = 1<<3,
142 SW_KEY_R = 1<<4,
143 SW_KEY_G = 1<<5,
144 SW_KEY_B = 1<<6,
147 static struct MapThing {
148 // thing flags
149 enum : ushort {
150 DirRight = 0x0001,
151 DeathMatch = 0x0010, // ÐÏÑ×ÌÑÅÔÓÑ ÔÏÌØËÏ × DeathMatch'Å
154 short x, y; // ËÏÏÒÄÉÎÁÔÙ
155 ushort type; // ÔÉÐ
156 ushort flags; // ÆÌÁÇÉ
158 @property const pure nothrow @safe @nogc {
159 bool left () => ((flags&DirRight) == 0);
160 bool right () => ((flags&DirRight) != 0);
161 bool dmonly () => ((flags&DeathMatch) != 0);
165 static struct MapSwitch {
166 ubyte x, y; // ËÏÏÒÄÉÎÁÔÙ/8
167 ubyte type; // ÔÉÐ
168 ubyte tm; // ÄÏÌÖÎÏ ÂÙÔØ 0
169 ubyte a, b; // ÏÂÙÞÎÏ - ËÏÏÒÄÉÎÁÔÙ/8 Ä×ÅÒÉ
170 ushort c; // ÎÅ ÉÓÐÏÌØÚÕÅÔÓÑ (×ÒÏÄÅ ÂÙ)
171 ubyte flags; // ÆÌÁÇÉ (SW_*)
174 public:
175 enum { Type, Front, Back, AllLiquids, LiquidMask, LightMask, MapTexturesMax } // tile types, Front+: megatexture types
176 ubyte[][3] tiles; // Type, Front, Back
177 string[] wallnames; // "_water_0": water, "_water_1": acid, "_water_2": lava
178 ubyte[] walltypes; // bit0: solid/non-solid(0x01); bit1: 0x02 if "vtrap01"
179 D2DImage[] textures;
180 int width, height;
181 Texture[MapTexturesMax] texgl;
182 TrueColorImage[MapTexturesMax] teximgs;
183 D2DImage skytex;
184 Texture skytexgl;
186 MapThing[] things;
187 MapSwitch[] switches;
189 this () {}
191 this (string fname) { load(fname); }
192 this(ST) (auto ref ST fl) { load(fl); }
194 bool hasLiquidAt (int x, int y) {
195 if (x < 0 || y < 0 || x >= MapSize || y >= MapSize) return false;
196 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
197 return (wallnames[tt] == "_water_0" || wallnames[tt] == "_water_1" || wallnames[tt] == "_water_2");
200 //FIXME: recreate textures cleanly
201 void clearMegaTextures () {
202 //foreach (Texture tex; texgl) if (tex !is null) tex.clear;
203 //texgl[] = null;
206 // build OpenGL "megatextures" with the whole level on it
207 void oglBuildMega (uint buildMask=0xffff_ffffu) {
208 if (skytexgl is null) skytexgl = new Texture(skytex.asTCImage, Texture.Option.Linear, Texture.Option.Repeat);
209 //auto img = new TrueColorImage(width*8, height*8);
210 foreach (immutable type; Front..LightMask+1) {
211 if (teximgs[type] is null) {
212 buildMask |= 1<<type;
213 teximgs[type] = new TrueColorImage(width*8, height*8);
215 if ((buildMask&(1<<type)) == 0) continue;
216 auto img = teximgs[type];
217 img.imageData.colors[] = Color(0, 0, 0, 0);
218 if (type == Back) {
220 foreach (int y; 0..(height*8+skytex.height-1)/skytex.height) {
221 foreach (int x; 0..(width*8+skytex.width-1)/skytex.width) {
222 img.putTile(x*skytex.width, y*skytex.height, skytex);
227 foreach (int y; 0..height) {
228 foreach (int x; 0..width) {
229 if (type == LightMask) {
230 // in the lightmask texture, we should have only occluders' pixels
231 auto tt = tiles.ptr[Type].ptr[y*MapSize+x];
232 if ((tt&0x80) == 0 && (tt == TILE_WALL || tt == TILE_DOORC)) {
233 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Back].ptr[y*MapSize+x]]);
234 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Front].ptr[y*MapSize+x]]);
235 } /*else if (tt != TILE_LIFTU && tt != TILE_LIFTD) {
236 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Front].ptr[y*MapSize+x]]);
238 } else if (type == AllLiquids) {
239 // texture with liquid background, for distortion
240 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
241 if (wallnames[tt] == "_water_0" || wallnames[tt] == "_water_1" || wallnames[tt] == "_water_2") {
242 tt = tiles.ptr[Back].ptr[y*MapSize+x];
243 img.putTile(x*8, y*8, textures.ptr[tt]);
244 foreach (int dy; 0..8) {
245 foreach (int dx; 0..8) {
246 //img.putPixel(x*8+dx, y*8+dy, Color((tt == 3 ? 128 : 0), (tt == 2 ? 128 : 0), (tt == 1 ? 128 : 0), 128));
247 auto c = img.getPixel(x*8+dx, y*8+dy);
248 if (c.a == 0) img.putPixel(x*8+dx, y*8+dy, Color(0, 0, 0, 255));
252 } else if (type == LiquidMask) {
253 // texture with liquid colors, will be blended on top of the level
254 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
255 auto wclr = Color(0, 0, 0, 0);
256 if (wallnames[tt] == "_water_0") wclr = Color(0, 0, 100, 128); // water
257 else if (wallnames[tt] == "_water_1") wclr = Color(24, 140, 0, 128); // acid
258 else if (wallnames[tt] == "_water_2") wclr = Color(160, 0, 0, 128); // lava
259 if (wclr.a) {
260 img.putPixelTile(x*8, y*8, wclr);
261 // if this is top one, make some light border
262 if (!hasLiquidAt(x, y-1)) {
263 // border
264 auto wcc = wclr.lighten(0.6);
265 wcc.a = wclr.a;
266 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+0, wcc);
267 wcc = wclr.lighten(0.4);
268 wcc.a = wclr.a;
269 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+1, wcc);
270 wcc = wclr.lighten(0.2);
271 wcc.a = wclr.a;
272 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+2, wcc);
275 } else {
276 auto tf = tiles.ptr[Front].ptr[y*MapSize+x];
277 auto tb = tiles.ptr[Back].ptr[y*MapSize+x];
278 auto tt = tiles.ptr[type].ptr[y*MapSize+x];
279 if (wallnames[tf] == "_water_0" || wallnames[tf] == "_water_1" || wallnames[tf] == "_water_2" ||
280 wallnames[tb] == "_water_0" || wallnames[tb] == "_water_1" || wallnames[tb] == "_water_2") {
281 } else {
282 img.putTile(x*8, y*8, textures.ptr[tt]);
288 import std.string : format;
289 import arsd.png : writePng;
290 writePng("zpng%02s.png".format(type), img);
292 if (texgl[type] is null) {
293 texgl[type] = new Texture(img, Texture.Option.Nearest, Texture.Option.Clamp);
294 } else {
295 texgl[type].setFromImage(img, 0, 0);
300 void clear () {
301 tiles[] = null;
302 wallnames = null;
303 walltypes = null;
304 textures = null;
305 foreach (Texture tex; texgl) if (tex !is null) tex.clear;
306 texgl[] = null;
307 skytex = null;
308 things = null;
309 switches = null;
312 void dump (int idx) {
313 static char to62 (ubyte b) { pragma(inline, true); return cast(char)(b < 10 ? '0'+b : (b-10 < 26 ? 'A'+(b-10) : 'a'+(b-10-26))); }
314 foreach (immutable y; 0..MapSize) {
315 foreach (immutable x; 0..MapSize) {
316 conwrite(to62(tiles[idx][y*MapSize+x]));
318 conwriteln;
322 // true: found
323 bool getThingPos (ushort id, int* x=null, int* y=null, ushort* flags=null) {
324 foreach (ref th; things[]) {
325 if (th.type == id) {
326 if (x !is null) *x = th.x;
327 if (y !is null) *y = th.y;
328 if (flags !is null) *flags = th.flags;
329 return true;
332 if (x !is null) *x = 0;
333 if (y !is null) *y = 0;
334 if (flags !is null) *flags = 0;
335 return false;
338 private:
339 void calcMapSize () {
341 bool isEmpty(string dir) (int x, int y) if (dir == "col" || dir == "row") {
342 while (x < MapSize && y < MapSize) {
343 if (tiles[0][y*MapSize+x] || tiles[1][y*MapSize+x] || tiles[2][y*MapSize+x]) return false;
344 static if (dir == "row") ++x; else ++y;
346 return true;
348 width = height = MapSize;
349 // fix width
350 while (width > 0 && isEmpty!"col"(width-1, 0)) --width;
351 // fix height
352 while (height > 0 && isEmpty!"row"(0, height-1)) --height;
354 width = height = MapSize;
357 void load (string fname) {
358 load(openFile(fname));
361 void load(ST) (auto ref ST st) if (isReadableStream!ST) {
362 clear();
363 scope(failure) clear;
365 char[8] sign;
366 st.rawReadExact(sign[]);
367 if (sign != "Doom2D\x1a\x00") throw new Exception("invalid map signature");
368 if (st.readNum!ushort() != MapVersion) throw new Exception("invalid map version");
370 // load map blocks
371 foreach (ref a; tiles[]) a = new ubyte[](MapSize*MapSize);
372 char[$] skyname = "sprites/sky/rsky1.vga";
373 for (;;) {
374 auto btype = st.readNum!ushort();
375 if (btype == MB_END) break; // no more blocks
376 auto bsubtype = st.readNum!ushort();
377 auto bsize = st.readNum!uint();
378 if (bsize == 0) continue; // skip this block, it has no data (wtf?!)
379 // various tile types
380 switch (btype) {
381 case MB_SKY:
382 if (bsize != 2) throw new Exception("invalid sky data size");
383 ushort num = st.readNum!ushort();
384 if (num >= 1 && num <= 3) skyname[$-5] = cast(char)('0'+num);
385 break;
386 case MB_BACK:
387 case MB_FRONT:
388 case MB_WTYPE:
389 if (bsubtype > 1) throw new Exception("unknown tile block subtype");
390 int idx = (btype == MB_BACK ? Back : (btype == MB_FRONT ? Front : Type));
391 //ubyte[MapSize*MapSize] data = 0;
392 auto data = tiles[idx];
393 if (bsubtype == 0) {
394 if (bsize != data.length) throw new Exception("invalid tile data size");
395 st.rawReadExact(data[]);
396 } else {
397 // unpack RLE data
398 auto pkdata = new ubyte[](bsize);
399 st.rawReadExact(pkdata[]);
400 int spos = 0, opos = 0;
401 while (spos < pkdata.length) {
402 ubyte b = pkdata[spos++];
403 if (b != 255) {
404 data[opos++] = b;
405 } else {
406 int count = pkdata[spos++];
407 count |= pkdata[spos++]<<8;
408 b = pkdata[spos++];
409 while (count-- > 0) data[opos++] = b;
412 assert(opos == data.length);
414 // copy unpacked data
415 //foreach (immutable y; 0..MapSize) tiles[idx][y*MapSize] = data[y*MapSize..(y+1)*MapSize];
416 break;
417 case MB_WALLNAMES:
418 wallnames.length = 0;
419 wallnames ~= null;
420 //wallnames[] = null;
421 //walltypes.length = 0;
422 while (bsize >= 8+1) {
423 char[8] texname = 0;
424 st.rawReadExact(texname[]);
425 auto type = st.readNum!ubyte();
426 //char[] tn;
427 string tns;
428 foreach (char ch; texname) {
429 if (ch == 0) break;
430 if (ch >= 'A' && ch <= 'Z') ch += 32;
431 ch = dos2koi8(ch);
432 tns ~= ch;
433 //tn = texname[0..idx+1];
435 import std.uni : toLower;
436 wallnames ~= koi8lotranslit(tns).toLower;
437 type = (type ? 1 : 0);
438 if (wallnames[$-1] == "vtrap01") type |= 0x02;
439 //wallnames ~= recodeToKOI8(recode(tn.idup, "utf-8", "cp866").toLower, "utf-8");
440 //debug { conwriteln(wallnames.length-1, " : ", wallnames[$-1]); }
441 //if (wallnames[$-1][$-1] == '_') wallnames[$-1] ~= "1";
442 //wallnames[$-1] ~= ".vga";
443 //conwriteln(wallnames[$-1]);
444 walltypes ~= type;
445 bsize -= 8+1;
447 if (bsize != 0) throw new Exception("invalid texture chunk size");
448 debug { conwriteln(wallnames.length, " textures loaded"); }
449 break;
450 case MB_THING:
451 while (bsize >= 8) {
452 bsize -= 8;
453 MapThing t = void;
454 t.x = st.readNum!short();
455 t.y = st.readNum!short();
456 t.type = st.readNum!ushort();
457 t.flags = st.readNum!ushort();
458 if (t.type != 0) things ~= t;
460 if (bsize != 0) throw new Exception("invalid thing chunk size");
461 break;
462 case MB_SWITCH2:
463 while (bsize >= 9) {
464 bsize -= 9;
465 MapSwitch sw = void;
466 sw.x = st.readNum!ubyte();
467 sw.y = st.readNum!ubyte();
468 sw.type = st.readNum!ubyte();
469 sw.tm = st.readNum!ubyte();
470 sw.a = st.readNum!ubyte();
471 sw.b = st.readNum!ubyte();
472 sw.c = st.readNum!ushort();
473 sw.flags = st.readNum!ubyte();
474 switches ~= sw;
476 if (bsize != 0) throw new Exception("invalid thing chunk size");
477 break;
478 default:
479 auto pkdata = new ubyte[](bsize);
480 st.rawReadExact(pkdata[]);
481 break;
484 calcMapSize();
485 // load textures
486 textures.length = 0;
487 foreach (immutable idx, string name; wallnames) {
488 if (name.length == 0 || name[0] == '_') {
489 textures ~= null;
490 continue;
491 } else {
492 textures ~= new D2DImage("tilegfx/"~name~".vga");
495 assert(textures.length == wallnames.length);
496 // fix tiles
497 foreach (immutable y; 0..height) {
498 foreach (immutable x; 0..width) {
499 if (tiles[Front][y*MapSize+x] >= textures.length) tiles[Front][y*MapSize+x] = 0;
500 if (tiles[Back][y*MapSize+x] >= textures.length) tiles[Back][y*MapSize+x] = 0;
503 skytex = new D2DImage(skyname.idup); //"sprites/rsky1.vga");
504 //skytex = new D2DImage("sprites/rsky3.vga");
505 skytex.sx = skytex.sy = 0;
506 debug { conwriteln(width, "x", height); }