1 /**************************************************************************
2 * This program is free software; you can redistribute it and/or
3 * modify it under the terms of the GNU General Public License
4 * as published by the Free Software Foundation; either version 3
5 * of the License, or (at your option) any later version.
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 //#define PXCOLDET_TEST
15 // ////////////////////////////////////////////////////////////////////////// //
16 class CollisionMask : Object;
19 int height; // in pixels
21 array!int maskHLine; // used for rect checks
22 int x0, y0, x1, y1; // bounding box; right-bottom inclusive
29 final bool isEmptyMask { get { return (mask.length == 0); } }
32 final bool isRectOverlaps (int rx, int ry, int rw, int rh) {
33 //writeln(" me:(", x0, ",", y0, ")-(", x1, ",", y1, "); it:(", rx, ",", ry, ")-(", rx+rw-1, ",", ry+rh-1, ")");
36 rx <= x1 && ry <= y1 &&
37 rx+rw > x0 && ry+rh > y0;
41 final void setPixel (int x, int y, optional bool v) {
42 if (!specified_v) v = true;
43 if (x < 0 || y < 0 || x >= width*32 || y >= height) return;
49 mask[ix, y] &= ~(1<<dx);
54 final bool colCheck (CollisionMask m1, int dx, int dy) {
55 if (!m1 || mask.length == 0 || m1.mask.length == 0) return false;
56 // check for sane dimensions
57 if (width < 1 || height < 1 || m1.width < 1 || m1.height < 1) return false;
58 // check coords (use boundig boxes)
59 if (dx+m1.x0 > x1 || dy+m1.y0 > y1 || dx+m1.x1 < x0 || dy+m1.y1 < y0) return false;
60 // check if both bounding boxes are solid
61 if (bbsolid && m1.bbsolid) return true; // yeah, we're done
64 if (dumpCheck) writeln("ofs: (", dx, ",", dy, "); self: ", width, "x", height, "; m1: ", m1.width, "x", m1.height, "; dx/32=", dx/32, "; dxend=", (dx+m1.width*32-1)/32+1);
66 // calculate x bounds on self
67 int sx0 = max(0, dx/32);
68 int sx1 = min(width, (dx+m1.width*32-1)/32+1);
69 // calculate y bounds on self
71 int sy1 = min(height, dy+m1.height);
72 // calculate x start on m1
73 int mx0 = -min(0, dx/32);
74 // calculate y start on m1
75 int my0 = -min(0, dy);
79 if (dumpCheck) writeln(" srect:(", sx0, ",", sy0, ")-(", sx1, ",", sy1, "); mofs:(", mx0, ",", my0, "); dm32=", dm32);
87 // shift self to the right
91 if (dumpCheck) writeln(" SELFRIGHT: ssr=", ssr, "; ssl=", ssl);
93 foreach (int sy; sy0..sy1) {
94 foreach (int sx; sx0..sx1) {
96 v = (m1.mask[sx+mx0, sy+my0]<<ssr)|(m1.mask[sx+mx0+1, sy+my0]>>>ssl);
98 if (dumpCheck) writeln(" ", va("v=%x m1=%x self=%x (sx=%d; sy=%d) (mx=%d; my=%d)", v, m1.mask[sx+mx0, sy+my0], mask[sx, sy], sx, sy, sx+mx0, sy+my0));
101 if (mask[sx, sy]&v) return true;
104 } else if (dm32 > 0) {
105 // shift self to the left
109 if (dumpCheck) writeln(" SELFLEFT: ssr=", ssr, "; ssl=", ssl);
111 foreach (int sy; sy0..sy1) {
112 foreach (int sx; sx0..sx1) {
114 v = (mask[sx, sy]<<ssl)|(mask[sx+1, sy]>>>ssr); // sprite mask is one int wider, so it is ok
116 if (dumpCheck) writeln(" ", va("v=%x m1=%x self=%x (sx=%d; sy=%d) (mx=%d; my=%d)", v, m1.mask[sx+mx0, sy+my0], mask[sx, sy], sx, sy, sx+mx0, sy+my0));
119 if (v&m1.mask[sx+mx0, sy+my0]) return true;
122 } else /*if (dm32 == 0)*/ {
123 foreach (int sy; sy0..sy1) {
124 foreach (int sx; sx0..sx1) {
126 if (mask[sx, sy]&m1.mask[sx+mx0, sy+my0]) return true;
136 final bool colCheckRect (int dx, int dy, int m1x0, int m1y0, int m1x1, int m1y1/*, optional bool dbgdump*/) {
137 if (mask.length == 0) return false;
138 // check for sane dimensions
139 if (width < 1 || height < 1 || m1x1 < m1x0 || m1y1 < m1y0) return false;
140 //!if (dbgdump) writeln("0: dx=", dx, "; dy=", dy, "; m1x0=", m1x0, "; m1x1=", m1x1, "; x0=", x0, "; x1=", x1);
141 m1x0 += dx; m1y0 += dy;
142 m1x1 += dx; m1y1 += dy;
143 // check coords (use bounding boxes)
144 if (m1x0 > x1 || m1y0 > y1 || m1x1 < x0 || m1y1 < y0) return false;
145 // check if both bounding boxes are solid
146 if (bbsolid) return true; // yeah, we're done
147 // intersecting rects
148 //!if (dbgdump) writeln("1: dx=", dx, "; dy=", dy, "; m1x0=", m1x0, "; m1x1=", m1x1, "; x0=", x0, "; x1=", x1);
149 m1x0 = max(m1x0, x0);
150 m1x1 = min(m1x1, x1);
152 //TODO: we can create partial hline here, but meh...
154 if (maskHLine.length < hlwdt) maskHLine.length = hlwdt;
155 //!if (dbgdump) writeln("2: m1x0=", m1x0, "; m1x1=", m1x1, "; hlwdt=", hlwdt, "; x0=", x0, "; x1=", x1);
157 foreach (int vidx; 0..hlwdt) { int vv = vidx*32; maskHLine[vidx] = (vv <= m1x1 && vv+32 > m1x0 ? -1 : 0); }
159 maskHLine[m1x0>>5] >>>= (m1x0&0x1f);
160 //!if (dbgdump) writeln(" 3: ", m1x0>>5, " : ", va("%x", maskHLine[m1x0>>5]));
161 //writeln(m1x1&0x1f, " : ", m1x1&0x1f != 0x1f);
162 //!if (dbgdump) writeln(" 4: ", m1x1>>5, " : ", m1x1&0x1f, "; ", va("%x", -1<<(31-(m1x1&0x1f))), " : ", va("%x", maskHLine[m1x1>>5]), " : ", va("%x", maskHLine[m1x1>>5]&(-1<<(31-(m1x1&0x1f)))));
163 maskHLine[m1x1>>5] &= -1<<(31-(m1x1&0x1f));
164 //!if (dbgdump) writeln(" ** ", va("%x", 0x0fffffff&0xf8000000));
165 //!if (dbgdump) foreach (int vidx; 0..hlwdt) write(" ", va("%x", maskHLine[vidx])); writeln;
166 // calculate x bounds on self
167 int sx1 = min(width, hlwdt);
168 // calculate y bounds on self
169 int sy0 = max(0, m1y0);
170 int sy1 = min(height, m1y1+1);
173 writeln(" CR: me:(", x0, ",", y0, ")-(", x1, ",", y1, "); it:(", m1x0, ",", m1y0, ")-(", m1x1, ",", m1y1, ")");
174 writeln(" dx=", dx, "; sy0=", sy0, "; sx1=", sx1, "; sy1=", sy1, "; hlwdt=", hlwdt, "; ", va("%x", maskHLine[hlwdt-1]));
177 // skip zeroes at the left part
179 while (sx0 < sx1 && !maskHLine[sx0]) ++sx0;
180 if (sx0 >= sx1) return false;
181 // skip zeroes at the right part
182 while (sx1 > 0 && !maskHLine[sx1-1]) --sx1;
183 if (sx0 >= sx1) return false;
184 foreach (int sy; sy0..sy1) {
185 foreach (int sx; sx0..sx1) {
187 //!if (dbgdump) writeln(" sx=", sx, "; m=", va("%x", mask[sx, sy]), "; hm=", va("%x", maskHLine[sx]));
188 if (mask[sx, sy]&maskHLine[sx]) return true;
195 static final CollisionMask Create (SpriteFrame frm, bool mirrored, optional bool dumpCheck) {
196 CollisionMask res = SpawnObject(CollisionMask);
198 res.dumpCheck = dumpCheck;
200 if (!frm || frm.width < 1 || frm.height < 1) return res;
201 int elw = (frm.width+31)/32;
202 if (elw < 1) FatalError("WUTA...");
204 res.height = frm.height;
207 res.x1 = frm.width-1;
208 res.y1 = frm.height-1;
209 res.mask.setSize(elw+1, frm.height); // +1 to avoid one check in coldet
211 // detect bounding box
214 int xmin = int.max, xmax = int.min;
215 int ymin = int.max, ymax = int.min;
216 foreach (int y; 0..frm.height) {
217 foreach (int x; 0..frm.width) {
218 if (frm.getMaskPixelEx(x, y, mirrored)) {
219 res.setPixel(x, y, true);
228 // check if we have at least one set pixel
230 // clear mask and exit
235 res.x0 = xmin; res.y0 = ymin;
236 res.x1 = xmax; res.y1 = ymax;
238 // detect solid bounding box
239 foreach (int y; ymin..ymax+1) {
240 foreach (int x; xmin..xmax+1) {
241 if (!frm.getMaskPixelEx(x, y, mirrored)) {
248 if (dumpCheck) writeln("size: ", frm.width, "x", frm.height, "; bbox:(", res.x0, ",", res.y0, ")-(", res.x1, ",", res.y1, "); solid:", (res.bbsolid ? "tan" : "ona"));
254 // ////////////////////////////////////////////////////////////////////////// //
255 class SpriteFrame : Object transient;
262 CollisionMask colmask, colmaskMirrored; // will be lazily created
263 string mask; // collision bitmask; can be empty
267 bool precise; // use precise pixel-perfect collision detection?
271 final int width { get wdt; }
272 final int height { get hgt; }
275 override void Destroy () {
281 final void blitAt (int x, int y, int scale, optional float angle) {
283 tex.blitAt(x, y, scale, angle:angle!optional);
285 int w = wdt, h = hgt;
286 tex.blitExt(x, y, x+w*scale, y+h*scale, 0, 0, w, h, angle:angle!optional);
291 final void processMask () {
293 if (!mask || bw < 1 || bh < 1) return;
294 foreach (int idx; 0..mask.length) if (mask[idx]) { maskEmpty = false; return; }
298 final bool getMaskPixel (int x, int y) {
299 if (maskEmpty) return false;
300 if (x < bx || y < by || x >= bx+bw || y >= by+bh) return false;
301 if (!mask) return true;
302 int w = tex.width, h = tex.height;
303 if (x < 0 || y < 0 || x >= w || y >= h) return false;
304 return !!(mask[y*((w+7)/8)+x/8]&(0x80>>(x&7)));
308 final bool getMaskPixelEx (int x, int y, bool mirrored) {
309 if (maskEmpty) return false;
310 if (mirrored) x = tex.width-x-1;
311 return getMaskPixel(x, y);
315 final bool isEmptyPixelMask { get maskEmpty; }
318 final CollisionMask getColMask (bool mirrored) {
319 CollisionMask res = (mirrored ? colmaskMirrored : colmask);
321 res = CollisionMask.Create(self, mirrored);
322 if (mirrored) colmaskMirrored = res; else colmask = res;
329 final void getBBox (out int x0, out int y0, out int x1, out int y1, optional bool doMirror) {
342 final bool checkPoint (int dx, int dy, bool doMirror) {
343 return (precise ? getMaskPixelEx(dx, dy, doMirror) : dx >= bx && dy >= by && dx < bx+bw && dy < by+bh);
347 final bool checkRect (int rx, int ry, int rw, int rh, bool doMirror) {
348 if (rw < 1 || rh < 1 || maskEmpty || bw < 1 || bh < 1) return false;
349 if (rx+rw > bx && ry+rh > by && rx < bx+bw && ry < by+bh) {
350 if (!precise) return true;
351 auto cm = getColMask(doMirror);
352 return (rx+rw > cm.x0 && ry+rh > cm.y0 && rx <= cm.x1 && ry <= cm.y1);
358 final bool checkRectFrm (SpriteFrame frm, int dx, int dy, optional bool mirrorSelf, optional bool mirrorFrm) {
359 if (!frm || bw < 1 || bh < 1 || frm.bw < 1 || frm.bh < 1) return false;
360 int selfx0, selfy0, selfx1, selfy1;
361 getBBox(out selfx0, out selfy0, out selfx1, out selfy1, mirrorSelf);
362 int frmx0, frmy0, frmx1, frmy1;
363 frm.getBBox(out frmx0, out frmy0, out frmx1, out frmy1, mirrorFrm);
364 if (dx+frmx0 > selfx1 || dy+frmy0 > selfy1 || dx+frmx1 < selfx0 || dy+frmy1 < selfy0) return false;
369 final bool pixelCheck (SpriteFrame frm, int dx, int dy, optional bool mirrorSelf, optional bool mirrorFrm/*, optional bool dbgdump*/) {
370 if (precise && maskEmpty) return false;
371 if (!frm || bw < 1 || bh < 1 || frm.bw < 1 || frm.bh < 1) return false;
372 if (frm.precise && frm.maskEmpty) return false;
373 int selfx0, selfy0, selfx1, selfy1;
374 getBBox(out selfx0, out selfy0, out selfx1, out selfy1, mirrorSelf);
375 int frmx0, frmy0, frmx1, frmy1;
376 frm.getBBox(out frmx0, out frmy0, out frmx1, out frmy1, mirrorFrm);
377 if (dx+frmx0 > selfx1 || dy+frmy0 > selfy1 || dx+frmx1 < selfx0 || dy+frmy1 < selfy0) return false;
378 if (!precise && !frm.precise) return true; // bboxes overlaps, it is enough
379 //!if (dbgdump) writeln("!!! (precise=", precise, "; frm.precise=", frm.precise, "; myempty=", maskEmpty, "; frmempty=", frm.maskEmpty, ")");
380 CollisionMask selfCM, frmCM;
381 // if our mask is empty, it means "bbcheck with other mask"
383 if (!frm.precise) return true;
384 if (frm.maskEmpty) FatalError("WTF?! (0)");
385 frmCM = frm.getColMask(mirrorFrm);
386 //!if (dbgdump) writeln("0:!!! dx=", dx, "; dy=", dy, "; me:(", bx-dx, ",", by-dy, ")-(", bx-dx+bw-1, ",", by-dy+bh-1, "); it:(", frmCM.x0, ",", frmCM.y0, ")-(", frmCM.x1, ",", frmCM.y1, ")");
387 //!if (dbgdump) writeln("0:!!! dx=", -(dx+frmx0), "; dy=", -(dy+frmy0), "; self=(", selfx0, ",", selfy0, ")-(", selfx1, ",", selfy1, ")");
388 return frmCM.colCheckRect(-dx, -dy, selfx0, selfy0, selfx1, selfy1/*, dbgdump!optional*/);
390 // if their mask is empty, it means "bbcheck with other mask"
393 if (!precise) return true;
394 if (maskEmpty) FatalError("WTF?! (1)");
395 selfCM = getColMask(mirrorSelf);
396 return selfCM.colCheckRect(dx, dy, frmx0, frmy0, frmx1, frmy1/*, dbgdump!optional*/);
398 //if (maskEmpty || frm.maskEmpty) return false;
400 foreach (auto y; frm.by..frm.by+frm.bh) {
401 foreach (auto x; frm.bx..frm.bx+frm.bw) {
402 if (frm.getMaskPixelEx(x, y, mirrorFrm) && getMaskPixelEx(dx+x, dy+y, mirrorSelf)) return true;
407 // lazy colmask creation
408 selfCM = getColMask(mirrorSelf);
409 frmCM = frm.getColMask(mirrorFrm);
411 return selfCM.colCheck(frmCM, dx, dy);
416 // ////////////////////////////////////////////////////////////////////////// //
417 class SpriteImage : Object transient;
420 array!SpriteFrame frames;
421 bool precise; // use precise pixel-perfect collision detection?
424 override void Destroy () {
425 foreach (ref auto fr; frames) delete fr;
430 static final int roundToPOW (int n) {
442 static final SpriteImage Load (string fname, bool allowNPot) {
443 auto fl = FileReader.Open(fname);
444 if (!fl) return none;
446 string sign = fl.readBuf(4, true); // exact
447 if (sign != "SPR0" || fl.error) { delete fl; return none; }
449 int nlen = fl.readU8();
450 if (fl.error) { delete fl; return none; }
453 sprName = fl.readBuf(nlen, true); // exact
454 if (fl.error) { delete fl; return none; }
457 int frameCount = fl.readU8();
459 int frameW = fl.readU16();
460 int frameH = fl.readU16();
462 int xofs = fl.readI16();
463 int yofs = fl.readI16();
465 int flags = fl.readU8(); // bit 0: precise collision flag (disk shape is emulated with precise mask)
466 if (fl.error) { delete fl; return none; }
467 if (frameW < 1 || frameH < 1 || frameW > 8192 || frameH > 8192) { delete fl; return none; }
469 int txw = (allowNPot ? frameW : roundToPOW(frameW));
470 int txh = (allowNPot ? frameH : roundToPOW(frameH));
472 auto spimg = SpawnObject(SpriteImage);
473 spimg.Name = name(sprName);
474 spimg.frames.length = frameCount;
475 spimg.precise = ((flags&0x01) != 0);
476 //writeln(fname, " : ", spimg.precise);
478 foreach (auto fridx; 0..frameCount) {
480 int x0 = fl.readI16();
481 int y0 = fl.readI16();
482 int x1 = fl.readI16();
483 int y1 = fl.readI16();
484 if (fl.error) { delete fl; delete spimg; return none; }
488 int mwdt = (frameW+7)/8;
489 cmask = fl.readBuf(mwdt*frameH, true); // exact
490 if (fl.error) { delete fl; delete spimg; return none; }
493 //name frmName = name(va("%s_frame_%d", sprName, fridx));
494 auto tex = GLTexture.CreateEmpty(txw, txh/*, frmName*/);
495 foreach (int y; 0..frameH) {
496 foreach (int x; 0..frameW) {
497 ubyte r = fl.readU8();
498 ubyte g = fl.readU8();
499 ubyte b = fl.readU8();
500 ubyte a = 255-fl.readU8();
502 writeln("error loading sprite file '", fname, "' (x=", x, "; y=", y, ")");
503 delete fl; delete spimg; delete tex;
506 //write("(", a, ",", r, ",", g, ",", b, ") ");
507 tex.setPixel(x, y, (a<<24)|(r<<16)|(g<<8)|b);
509 foreach (int x; frameW..txw) tex.setPixel(x, y, 0xff_00_00_00);
512 foreach (int y; frameH..txh) foreach (int x; frameW..txw) tex.setPixel(x, y, 0xff_00_00_00);
513 // update texture image
515 // create sprite frame
516 auto frm = SpawnObject(SpriteFrame);
517 //frm.Name = frmName;
524 frm.goodSize = (txw == frameW && txh == frameH);
528 frm.bw = (x1 < x0 ? 0 : x1-x0+1);
529 frm.bh = (y1 < y0 ? 0 : y1-y0+1);
530 frm.precise = spimg.precise;
532 spimg.frames[fridx] = frm;
539 // ////////////////////////////////////////////////////////////////////////// //
540 class SpriteStore : Object transient;
542 array!SpriteImage sprbyid;
550 private final void loadSprite (name Name) {
551 if (!Name) FatalError("cannot load namelss sprite");
554 if (!GLVideo.isInitialized) {
555 writeln("preloading sprite '", Name, "'");
557 allowNPot = (GLVideo.glHasNPOT ? 1 : 0);
558 if (allowNPot) writeln("*** NPOT textures allowed");
563 auto spr = SpriteImage.Load(va("%s/%n.spr", sprPath, Name), (allowNPot > 0));
564 if (!spr) FatalError("cannot load sprite '%n'", Name);
566 sprbyid[NameToIIndex(/*spr.*/Name)] = spr;
569 writeln("sprite: <", spr.Name, ">, ", spr.frames.length, " frames; id=", NameToIIndex(spr.Name));
570 foreach (auto fidx, SpriteFrame frm; spr.frames) {
571 writeln(" === FRAME #", fidx, " ===");
572 //writeln(" name: <", frm.Name, ">");
573 writeln(" index: <", frm.index, ">");
574 writeln(" size: (", frm.tex.width, "x", frm.tex.height, ")");
575 writeln(" offset: (", frm.xofs, ",", frm.yofs, ")");
576 writeln(" bbox: (", frm.bx, ",", frm.by, ")x(", frm.bw, ",", frm.bh, ")");
577 if (frm.mask) writeln(" has collision mask", (frm.maskEmpty ? " (empty)" : ""));
583 final void loadFont (name Name) {
584 if (fontName != Name) {
586 fontImg = opIndex(Name);
591 final int getFontHeight (optional int scale) {
592 if (!specified_scale) scale = 1;
593 if (scale < 1 || !fontImg) return 0;
594 return fontImg.frames[0].height*scale;
598 final int getCharWidth (int ch, optional int scale) {
599 if (ch < 32) return 0;
600 if (!specified_scale) scale = 1;
601 if (scale < 1 || !fontImg) return 0;
602 auto flen = fontImg.frames.length;
603 if (flen < 1) return 0;
604 if (ch < 32) ch = 32;
605 if (ch >= "a" && ch <= "z" && ch-32 >= flen) ch -= 32;
607 if (ch < 0 || ch >= flen) ch = 0;
608 return fontImg.frames[ch].width*scale;
612 final int getTextWidth (string str, optional int scale, optional bool processHighlights1, optional bool processHighlights2) {
613 if (!specified_scale) scale = 1;
616 foreach (auto sidx; 0..str.length) {
618 if (processHighlights1) {
619 if (ch == '~' && (!inHigh || inHigh == 1)) {
624 if (processHighlights2) {
625 if (ch == '|' && (!inHigh || inHigh == 2)) {
630 res += getCharWidth(ch, scale);
636 // returns char width
637 final int renderChar (int x, int y, int ch, optional int scale) {
638 if (ch < 32) return 0;
639 if (!specified_scale) scale = 1;
640 if (scale < 1 || !fontImg) return 0;
641 auto flen = fontImg.frames.length;
642 if (flen < 1) return 0;
643 if (ch < 32) ch = 32;
644 if (ch >= "a" && ch <= "z" && ch-32 >= flen) ch -= 32;
646 if (ch < 0 || ch >= flen) ch = 0;
647 fontImg.frames[ch].blitAt(x, y, scale);
648 return fontImg.frames[ch].width*scale;
652 final void renderText (int x, int y, string str, optional int scale) {
653 if (!specified_scale) scale = 1;
654 if (scale < 1 || !fontImg) return;
655 auto flen = fontImg.frames.length;
656 if (flen < 1) return;
657 foreach (auto sidx; 0..str.length) {
659 if (ch >= "a" && ch <= "z" && ch-32 >= flen) ch -= 32;
661 if (ch >= 0 && ch < flen) {
662 fontImg.frames[ch].blitAt(x, y, scale);
663 x += fontImg.frames[ch].width*scale;
665 x += fontImg.frames[0].width*scale;
671 final void renderTextHiChar (int x, int y, string str, optional int scale, optional int hotCharOfs, optional int hotCharColor) {
672 if (!specified_scale) scale = 1;
673 if (scale < 1 || !fontImg) return;
674 auto flen = fontImg.frames.length;
675 if (flen < 1) return;
676 auto oclr = GLVideo.color;
677 foreach (auto sidx; 0..str.length) {
679 if (ch >= "a" && ch <= "z" && ch-32 >= flen) ch -= 32;
681 if (ch >= 0 && ch < flen) {
682 if (specified_hotCharOfs && specified_hotCharColor) {
683 if (hotCharOfs == sidx) GLVideo.color = hotCharColor;
685 fontImg.frames[ch].blitAt(x, y, scale);
686 x += fontImg.frames[ch].width*scale;
687 GLVideo.color = oclr;
689 x += fontImg.frames[0].width*scale;
692 GLVideo.color = oclr;
696 final void renderTextWithHighlight (int x, int y, string str, optional int scale, int hiColor, optional int hiColor1) {
697 if (!specified_scale) scale = 1;
698 if (scale < 1 || !fontImg) return;
699 auto flen = fontImg.frames.length;
700 if (flen < 1) return;
701 auto oclr = GLVideo.color;
703 foreach (auto sidx; 0..str.length) {
705 if (ch == '~' && (!inHigh || inHigh == 1)) {
707 GLVideo.color = (inHigh ? hiColor : oclr);
710 if (specified_hiColor1 && ch == '|' && (!inHigh || inHigh == 2)) {
712 GLVideo.color = (inHigh ? hiColor1 : oclr);
715 if (ch >= "a" && ch <= "z" && ch-32 >= flen) ch -= 32;
717 if (ch >= 0 && ch < flen) {
718 fontImg.frames[ch].blitAt(x, y, scale);
719 x += fontImg.frames[ch].width*scale;
721 x += fontImg.frames[0].width*scale;
724 GLVideo.color = oclr;
728 // this knows about '\n' too
729 final void renderTextWrapped (int x, int y, int wdt, string str, optional int scale) {
730 if (!specified_scale) scale = 1;
731 if (scale < 1 || !fontImg || fontImg.frames.length < 1) return;
732 //auto flen = fontImg.frames.length;
735 bool skipSpaces = false;
736 while (sidx < str.length) {
738 if (ch == '\r') { ++sidx; continue; }
739 if (ch == '\n') { x = x0; y += fontImg.frames[0].height*scale; ++sidx; skipSpaces = false; continue; }
740 if (skipSpaces && ch <= 32) { ++sidx; continue; }
741 // find end of the current word (and calculate word width)
745 while (epos < str.length && str[epos] <= 32 && str[epos] != '\n') wwdt += getCharWidth(str[epos++], scale!optional);
747 while (epos < str.length && str[epos] > 32) wwdt += getCharWidth(str[epos++], scale!optional);
748 // do we need to wrap here?
749 if (x > x0 && x+wwdt-x0 > wdt) {
751 // skip spaces on wrapping, they aren't needed
752 if (!skipSpaces) { while (sidx < str.length && str[sidx] <= 32 && str[sidx] != '\n') ++sidx; }
753 // move to the next line
755 y += fontImg.frames[0].height*scale;
758 while (sidx < epos) x += renderChar(x, y, str[sidx++], scale!optional);
759 x += getCharWidth(32, scale!optional);
760 // skip the following spaces
767 final int getMultilineTextWidth (string str, optional int scale, optional bool processHighlights1, optional bool processHighlights2) {
768 if (!specified_scale) scale = 1;
769 int curwdt = 0, maxwdt = 0;
771 foreach (auto sidx; 0..str.length) {
773 if (processHighlights1) {
774 if (ch == '~' && (!inHigh || inHigh == 1)) {
779 if (processHighlights2) {
780 if (ch == '|' && (!inHigh || inHigh == 2)) {
786 maxwdt = max(maxwdt, curwdt);
789 curwdt += getCharWidth(str[sidx], scale);
792 return max(curwdt, maxwdt);
797 final int getMultilineTextHeight (string str, optional int scale) {
798 if (!specified_scale) scale = 1;
800 int lend = str.length;
801 while (lend > 0 && str[lend-1] == '\n') --lend;
802 foreach (auto sidx; 0..lend) if (str[sidx] == '\n') ++lineCount;
803 return lineCount*getFontHeight(scale);
807 // `x` is a center point; `y` is a starting line
808 final void renderMultilineTextCentered (int x, int y, string str, optional int scale, optional int highColor1, optional int highColor2) {
809 if (!specified_scale) scale = 1;
810 if (scale < 1 || !fontImg) return;
811 auto flen = fontImg.frames.length;
812 if (flen < 1) return;
813 highColor1 = (highColor1&0xff_ff_ff)|(GLVideo.color&0xff_00_00_00);
814 highColor2 = (highColor2&0xff_ff_ff)|(GLVideo.color&0xff_00_00_00);
816 int endpos = str.length;
817 while (endpos > 0 && str[endpos] == '\n') --endpos;
820 foreach (auto sidx; pos..endpos) if (str[sidx] == '\n') ++lc;
822 y = (GLVideo.screenHeight-lc*getFontHeight(scale!optional))/2;
824 y = (-y-lc*getFontHeight(scale!optional))/2;
827 while (pos < endpos) {
829 while (epos < str.length && str[epos] != '\n') ++epos;
830 string s = str[pos..epos];
832 if (specified_highColor1 || specified_highColor2) {
833 renderTextWithHighlight(x-getTextWidth(s, scale, specified_highColor1, specified_highColor2)/2, y, s, scale, highColor1!optional, highColor2!optional);
835 renderText(x-getTextWidth(s, scale)/2, y, s, scale);
837 y += fontImg.frames[0].height*scale;
842 final void renderMultilineText (int x, int y, string str, optional int scale, optional int highColor1, optional int highColor2) {
843 if (!specified_scale) scale = 1;
844 if (scale < 1 || !fontImg) return;
845 auto flen = fontImg.frames.length;
846 if (flen < 1) return;
847 highColor1 = (highColor1&0xff_ff_ff)|(GLVideo.color&0xff_00_00_00);
848 highColor2 = (highColor2&0xff_ff_ff)|(GLVideo.color&0xff_00_00_00);
850 int endpos = str.length;
851 while (endpos > 0 && str[endpos] == '\n') --endpos;
854 foreach (auto sidx; pos..endpos) if (str[sidx] == '\n') ++lc;
856 y = (GLVideo.screenHeight-lc*getFontHeight(scale!optional))/2;
858 y = (-y-lc*getFontHeight(scale!optional))/2;
861 while (pos < endpos) {
863 while (epos < str.length && str[epos] != '\n') ++epos;
864 string s = str[pos..epos];
866 if (specified_highColor1 || specified_highColor2) {
867 renderTextWithHighlight(x, y, s, scale, highColor1!optional, highColor2!optional);
869 renderText(x, y, s, scale);
871 y += fontImg.frames[0].height*scale;
876 final SpriteImage opIndex (name Name) {
877 int id = NameToIIndex(Name);
878 if (id <= 0) return none; // just in case
879 if (id >= sprbyid.length || !sprbyid[id]) loadSprite(Name);
880 return (id >= 0 && id < sprbyid.length ? sprbyid[id] : none);
885 //sprPath = "data/gfx/sprites";