koi moved to separate module
[dd2d.git] / d2dmap.d
blobf33eef8dd9b3c1d62e630002c57de410fa341294
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 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 private import std.stdio : File;
104 enum MapVersion = 2; // óÁÍÁÑ ÐÏÓÌÅÄÎÑÑ ×ÅÒÓÉÑ ËÁÒÔÙ
105 public enum MapSize = 100;
107 public:
108 // tile type
109 enum : ubyte {
110 TILE_EMPTY = 0,
111 TILE_WALL = 1,
112 TILE_DOORC = 2, // closed door
113 TILE_DOORO = 3, // opened door
114 TILE_STEP = 4,
115 TILE_WATER = 5,
116 TILE_ACID1 = 6,
117 TILE_ACID2 = 7,
118 TILE_MBLOCK = 8, // just blocks monsters
119 TILE_LIFTU = 9,
120 TILE_LIFTD = 10,
122 TILE_ACTTRAP = 255,
125 enum : short {
126 MB_COMMENT = -1,
127 MB_END = 0,
128 MB_WALLNAMES,
129 MB_BACK,
130 MB_WTYPE,
131 MB_FRONT,
132 MB_THING,
133 MB_SWITCH,
134 MB_MUSIC, // usually 8 bytes
135 MB_SKY, // ushort: [1..3]
136 MB_SWITCH2,
139 enum {
140 SW_PL_PRESS = 1<<0,
141 SW_MN_PRESS = 1<<1,
142 SW_PL_NEAR = 1<<2,
143 SW_MN_NEAR = 1<<3,
144 SW_KEY_R = 1<<4,
145 SW_KEY_G = 1<<5,
146 SW_KEY_B = 1<<6,
149 static struct MapThing {
150 // thing flags
151 enum : ushort {
152 DirRight = 0x0001,
153 DeathMatch = 0x0010, // ÐÏÑ×ÌÑÅÔÓÑ ÔÏÌØËÏ × DeathMatch'Å
156 short x, y; // ËÏÏÒÄÉÎÁÔÙ
157 ushort type; // ÔÉÐ
158 ushort flags; // ÆÌÁÇÉ
160 @property const pure nothrow @safe @nogc {
161 bool left () => ((flags&DirRight) == 0);
162 bool right () => ((flags&DirRight) != 0);
163 bool dmonly () => ((flags&DeathMatch) != 0);
167 static struct MapSwitch {
168 ubyte x, y; // ËÏÏÒÄÉÎÁÔÙ/8
169 ubyte type; // ÔÉÐ
170 ubyte tm; // ÄÏÌÖÎÏ ÂÙÔØ 0
171 ubyte a, b; // ÏÂÙÞÎÏ - ËÏÏÒÄÉÎÁÔÙ/8 Ä×ÅÒÉ
172 ushort c; // ÎÅ ÉÓÐÏÌØÚÕÅÔÓÑ (×ÒÏÄÅ ÂÙ)
173 ubyte flags; // ÆÌÁÇÉ (SW_*)
176 public:
177 enum { Type, Front, Back, AllLiquids, LiquidMask, LightMask, MapTexturesMax } // tile types, Front+: megatexture types
178 ubyte[][3] tiles; // Type, Front, Back
179 string[] wallnames; // "_water_0": water, "_water_1": acid, "_water_2": lava
180 ubyte[] walltypes; // bit0: solid/non-solid(0x01); bit1: 0x02 if "vtrap01"
181 D2DImage[] textures;
182 int width, height;
183 Texture[MapTexturesMax] texgl;
184 TrueColorImage[MapTexturesMax] teximgs;
185 D2DImage skytex;
186 Texture skytexgl;
188 MapThing[] things;
189 MapSwitch[] switches;
191 this () {}
193 this (string fname) { load(fname); }
194 this (File fl) { load(fl); }
196 bool hasLiquidAt (int x, int y) {
197 if (x < 0 || y < 0 || x >= MapSize || y >= MapSize) return false;
198 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
199 return (wallnames[tt] == "_water_0" || wallnames[tt] == "_water_1" || wallnames[tt] == "_water_2");
202 //FIXME: recreate textures cleanly
203 void clearMegaTextures () {
204 //foreach (Texture tex; texgl) if (tex !is null) tex.clear;
205 //texgl[] = null;
208 // build OpenGL "megatextures" with the whole level on it
209 void oglBuildMega (uint buildMask=0xffff_ffffu) {
210 if (skytexgl is null) skytexgl = new Texture(skytex.asTCImage, Texture.Option.Linear, Texture.Option.Repeat);
211 //auto img = new TrueColorImage(width*8, height*8);
212 foreach (immutable type; Front..LightMask+1) {
213 if (teximgs[type] is null) {
214 buildMask |= 1<<type;
215 teximgs[type] = new TrueColorImage(width*8, height*8);
217 if ((buildMask&(1<<type)) == 0) continue;
218 auto img = teximgs[type];
219 img.imageData.colors[] = Color(0, 0, 0, 0);
220 if (type == Back) {
222 foreach (int y; 0..(height*8+skytex.height-1)/skytex.height) {
223 foreach (int x; 0..(width*8+skytex.width-1)/skytex.width) {
224 img.putTile(x*skytex.width, y*skytex.height, skytex);
229 foreach (int y; 0..height) {
230 foreach (int x; 0..width) {
231 if (type == LightMask) {
232 // in the lightmask texture, we should have only occluders' pixels
233 auto tt = tiles.ptr[Type].ptr[y*MapSize+x];
234 if (tt == TILE_WALL || tt == TILE_DOORC) {
235 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Back].ptr[y*MapSize+x]]);
236 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Front].ptr[y*MapSize+x]]);
237 } /*else if (tt != TILE_LIFTU && tt != TILE_LIFTD) {
238 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Front].ptr[y*MapSize+x]]);
240 } else if (type == AllLiquids) {
241 // texture with liquid background, for distortion
242 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
243 if (wallnames[tt] == "_water_0" || wallnames[tt] == "_water_1" || wallnames[tt] == "_water_2") {
244 tt = tiles.ptr[Back].ptr[y*MapSize+x];
245 img.putTile(x*8, y*8, textures.ptr[tt]);
246 foreach (int dy; 0..8) {
247 foreach (int dx; 0..8) {
248 //img.putPixel(x*8+dx, y*8+dy, Color((tt == 3 ? 128 : 0), (tt == 2 ? 128 : 0), (tt == 1 ? 128 : 0), 128));
249 auto c = img.getPixel(x*8+dx, y*8+dy);
250 if (c.a == 0) img.putPixel(x*8+dx, y*8+dy, Color(0, 0, 0, 255));
254 } else if (type == LiquidMask) {
255 // texture with liquid colors, will be blended on top of the level
256 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
257 auto wclr = Color(0, 0, 0, 0);
258 if (wallnames[tt] == "_water_0") wclr = Color(0, 0, 100, 128); // water
259 else if (wallnames[tt] == "_water_1") wclr = Color(24, 140, 0, 128); // acid
260 else if (wallnames[tt] == "_water_2") wclr = Color(160, 0, 0, 128); // lava
261 if (wclr.a) {
262 img.putPixelTile(x*8, y*8, wclr);
263 // if this is top one, make some light border
264 if (!hasLiquidAt(x, y-1)) {
265 // border
266 auto wcc = wclr.lighten(0.6);
267 wcc.a = wclr.a;
268 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+0, wcc);
269 wcc = wclr.lighten(0.4);
270 wcc.a = wclr.a;
271 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+1, wcc);
272 wcc = wclr.lighten(0.2);
273 wcc.a = wclr.a;
274 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+2, wcc);
277 } else {
278 auto tf = tiles.ptr[Front].ptr[y*MapSize+x];
279 auto tb = tiles.ptr[Back].ptr[y*MapSize+x];
280 auto tt = tiles.ptr[type].ptr[y*MapSize+x];
281 if (wallnames[tf] == "_water_0" || wallnames[tf] == "_water_1" || wallnames[tf] == "_water_2" ||
282 wallnames[tb] == "_water_0" || wallnames[tb] == "_water_1" || wallnames[tb] == "_water_2") {
283 } else {
284 img.putTile(x*8, y*8, textures.ptr[tt]);
290 import std.string : format;
291 import arsd.png : writePng;
292 writePng("zpng%02s.png".format(type), img);
294 if (texgl[type] is null) {
295 texgl[type] = new Texture(img, Texture.Option.Nearest, Texture.Option.Clamp);
296 } else {
297 texgl[type].setFromImage(img, 0, 0);
302 void clear () {
303 tiles[] = null;
304 wallnames = null;
305 walltypes = null;
306 textures = null;
307 foreach (Texture tex; texgl) if (tex !is null) tex.clear;
308 texgl[] = null;
309 skytex = null;
310 things = null;
311 switches = null;
314 void dump (int idx) {
315 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))); }
316 foreach (immutable y; 0..MapSize) {
317 foreach (immutable x; 0..MapSize) {
318 conwrite(to62(tiles[idx][y*MapSize+x]));
320 conwriteln;
324 // true: found
325 bool getThingPos (ushort id, int* x=null, int* y=null, ushort* flags=null) {
326 foreach (ref th; things[]) {
327 if (th.type == id) {
328 if (x !is null) *x = th.x;
329 if (y !is null) *y = th.y;
330 if (flags !is null) *flags = th.flags;
331 return true;
334 if (x !is null) *x = 0;
335 if (y !is null) *y = 0;
336 if (flags !is null) *flags = 0;
337 return false;
340 private:
341 void calcMapSize () {
342 bool isEmpty(string dir) (int x, int y) if (dir == "col" || dir == "row") {
343 while (x < MapSize && y < MapSize) {
344 if (tiles[0][y*MapSize+x] || tiles[1][y*MapSize+x] || tiles[2][y*MapSize+x]) return false;
345 static if (dir == "row") ++x; else ++y;
347 return true;
349 width = height = MapSize;
350 // fix width
351 while (width > 0 && isEmpty!"col"(width-1, 0)) --width;
352 // fix height
353 while (height > 0 && isEmpty!"row"(0, height-1)) --height;
356 void load (string fname) {
357 import std.stdio : File;
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); }