fixed alot of warnings, and some bugs, so it can be compiled with the current vccrun
[k8vacspelynky.git] / packages / Sprites / SpriteStore.vc
blobb8e227ea75b79afcc697e525ada28e1471ce8611
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.
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 //#define PXCOLDET_TEST
15 // ////////////////////////////////////////////////////////////////////////// //
16 class CollisionMask : Object;
18 int width; // in ints
19 int height; // in pixels
20 array!int mask;
21 array!int maskHLine; // used for rect checks
22 int x0, y0, x1, y1; // bounding box; right-bottom inclusive
23 bool bbsolid;
24 #ifdef PXCOLDET_TEST
25 bool dumpCheck;
26 #endif
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, ")");
34   return
35     rw > 0 && rh > 0 &&
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;
44   int ix = x/32;
45   int dx = 31-x%32;
46   if (v) {
47     mask[ix, y] |= 1<<dx;
48   } else {
49     mask[ix, y] &= ~(1<<dx);
50   }
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
62   // intersecting rects
63 #ifdef PXCOLDET_TEST
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);
65 #endif
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
70   int sy0 = max(0, dy);
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);
76   int dm32 = dx%32;
77   // debug
78 #ifdef PXCOLDET_TEST
79   if (dumpCheck) writeln(" srect:(", sx0, ",", sy0, ")-(", sx1, ",", sy1, "); mofs:(", mx0, ",", my0, "); dm32=", dm32);
80 #endif
81   // compensate loops
82   mx0 -= sx0;
83   my0 -= sy0;
84   int v;
85   // do checks
86   if (dm32 < 0) {
87     // shift self to the right
88     int ssr = -dm32;
89     int ssl = 32-ssr;
90 #ifdef PXCOLDET_TEST
91     if (dumpCheck) writeln(" SELFRIGHT: ssr=", ssr, "; ssl=", ssl);
92 #endif
93     foreach (int sy; sy0..sy1) {
94       foreach (int sx; sx0..sx1) {
95         // get bits
96         v = (m1.mask[sx+mx0, sy+my0]<<ssr)|(m1.mask[sx+mx0+1, sy+my0]>>>ssl);
97 #ifdef PXCOLDET_TEST
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));
99 #endif
100         // do check
101         if (mask[sx, sy]&v) return true;
102       }
103     }
104   } else if (dm32 > 0) {
105     // shift self to the left
106     int ssl = dm32;
107     int ssr = 32-ssl;
108 #ifdef PXCOLDET_TEST
109     if (dumpCheck) writeln(" SELFLEFT: ssr=", ssr, "; ssl=", ssl);
110 #endif
111     foreach (int sy; sy0..sy1) {
112       foreach (int sx; sx0..sx1) {
113         // get bits
114         v = (mask[sx, sy]<<ssl)|(mask[sx+1, sy]>>>ssr); // sprite mask is one int wider, so it is ok
115 #ifdef PXCOLDET_TEST
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));
117 #endif
118         // do check
119         if (v&m1.mask[sx+mx0, sy+my0]) return true;
120       }
121     }
122   } else /*if (dm32 == 0)*/ {
123     foreach (int sy; sy0..sy1) {
124       foreach (int sx; sx0..sx1) {
125         // do check
126         if (mask[sx, sy]&m1.mask[sx+mx0, sy+my0]) return true;
127       }
128     }
129   }
130   return false;
134 // m is inclusive
135 // d is offset for m
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);
151   // create hline
152   //TODO: we can create partial hline here, but meh...
153   int hlwdt = width;
154   if (maskHLine.length < hlwdt) maskHLine.length = hlwdt;
155   //!if (dbgdump) writeln("2: m1x0=", m1x0, "; m1x1=", m1x1, "; hlwdt=", hlwdt, "; x0=", x0, "; x1=", x1);
156   // fill it
157   foreach (int vidx; 0..hlwdt) { int vv = vidx*32; maskHLine[vidx] = (vv <= m1x1 && vv+32 > m1x0 ? -1 : 0); }
158   // fix left part
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);
171   /*
172   if (dbgdump) {
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]));
175   }
176   */
177   // skip zeroes at the left part
178   int sx0 = 0;
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) {
186       // do check
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;
189     }
190   }
191   return false;
195 static final CollisionMask Create (SpriteFrame frm, bool mirrored, optional bool dumpCheck) {
196   CollisionMask res = SpawnObject(CollisionMask);
197 #ifdef PXCOLDET_TEST
198   res.dumpCheck = dumpCheck;
199 #endif
200   if (!frm || frm.width < 1 || frm.height < 1) return res;
201   int elw = (frm.width+31)/32;
202   if (elw < 1) FatalError("WUTA...");
203   res.width = elw;
204   res.height = frm.height;
205   res.x0 = 0;
206   res.y0 = 0;
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
210   // fill mask
211   // detect bounding box
212   // detect empty mask
213   bool hasPix = false;
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);
220         hasPix = true;
221         xmin = min(xmin, x);
222         ymin = min(ymin, y);
223         xmax = max(xmax, x);
224         ymax = max(ymax, y);
225       }
226     }
227   }
228   // check if we have at least one set pixel
229   if (!hasPix) {
230     // clear mask and exit
231     res.mask.length = 0;
232     return res;
233   }
234   // set bounding box
235   res.x0 = xmin; res.y0 = ymin;
236   res.x1 = xmax; res.y1 = ymax;
237   res.bbsolid = true;
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)) {
242         res.bbsolid = false;
243         break;
244       }
245     }
246   }
247 #ifdef PXCOLDET_TEST
248   if (dumpCheck) writeln("size: ", frm.width, "x", frm.height, "; bbox:(", res.x0, ",", res.y0, ")-(", res.x1, ",", res.y1, "); solid:", (res.bbsolid ? "tan" : "ona"));
249 #endif
250   return res;
254 // ////////////////////////////////////////////////////////////////////////// //
255 class SpriteFrame : Object transient;
257 GLTexture tex;
258 //name Name;
259 int index;
260 int xofs, yofs;
261 int wdt, hgt;
262 CollisionMask colmask, colmaskMirrored; // will be lazily created
263 string mask; // collision bitmask; can be empty
264 // bounding box
265 int bx, by, bw, bh;
266 bool maskEmpty;
267 bool precise; // use precise pixel-perfect collision detection?
268 bool goodSize;
271 final int width { get wdt; }
272 final int height { get hgt; }
275 override void Destroy () {
276   delete tex;
277   ::Destroy();
281 final void blitAt (int x, int y, int scale, optional float angle) {
282   if (goodSize) {
283     tex.blitAt(x, y, scale, angle:angle!optional);
284   } else {
285     int w = wdt, h = hgt;
286     tex.blitExt(x, y, x+w*scale, y+h*scale, 0, 0, w, h, angle:angle!optional);
287   }
291 final void processMask () {
292   maskEmpty = true;
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);
320   if (!res) {
321     res = CollisionMask.Create(self, mirrored);
322     if (mirrored) colmaskMirrored = res; else colmask = res;
323   }
324   return res;
328 // inclusive
329 final void getBBox (out int x0, out int y0, out int x1, out int y1, optional bool doMirror) {
330   y0 = by;
331   y1 = y0+bh-1;
332   if (!doMirror) {
333     x0 = bx;
334     x1 = x0+bw-1;
335   } else {
336     x1 = width-bx-1;
337     x0 = x1-bw+1;
338   }
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);
353   }
354   return false;
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;
365   return true;
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"
382   if (maskEmpty) {
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*/);
389   }
390   // if their mask is empty, it means "bbcheck with other mask"
391   if (frm.maskEmpty) {
392     //writeln("!!! 1");
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*/);
397   }
398   //if (maskEmpty || frm.maskEmpty) return false;
399 #ifdef PXCOLDET_TEST
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;
403     }
404   }
405   return false;
406 #else
407   // lazy colmask creation
408   selfCM = getColMask(mirrorSelf);
409   frmCM = frm.getColMask(mirrorFrm);
410   // check it
411   return selfCM.colCheck(frmCM, dx, dy);
412 #endif
416 // ////////////////////////////////////////////////////////////////////////// //
417 class SpriteImage : Object transient;
419 name Name;
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;
426   ::Destroy();
430 static final int roundToPOW (int n) {
431   --n;
432   n |= n>>1;
433   n |= n>>2;
434   n |= n>>4;
435   n |= n>>8;
436   n |= n>>16;
437   ++n;
438   return n;
442 static final SpriteImage Load (string fname, bool allowNPot) {
443   auto fl = FileReader.Open(fname);
444   if (!fl) return none;
445   // signature
446   string sign = fl.readBuf(4, true); // exact
447   if (sign != "SPR0" || fl.error) { delete fl; return none; }
448   // name
449   int nlen = fl.readU8();
450   if (fl.error) { delete fl; return none; }
451   string sprName;
452   if (nlen) {
453     sprName = fl.readBuf(nlen, true); // exact
454     if (fl.error) { delete fl; return none; }
455   }
456   // number of frames
457   int frameCount = fl.readU8();
458   // frame dimensions
459   int frameW = fl.readU16();
460   int frameH = fl.readU16();
461   // frame offset
462   int xofs = fl.readI16();
463   int yofs = fl.readI16();
464   // flags
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);
477   // load frames
478   foreach (auto fridx; 0..frameCount) {
479     // read bounding box
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; }
485     // collision mask
486     string cmask;
487     if (flags&0x01) {
488       int mwdt = (frameW+7)/8;
489       cmask = fl.readBuf(mwdt*frameH, true); // exact
490       if (fl.error) { delete fl; delete spimg; return none; }
491     }
492     // image
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();
501         if (fl.error) {
502           writeln("error loading sprite file '", fname, "' (x=", x, "; y=", y, ")");
503           delete fl; delete spimg; delete tex;
504           return none;
505         }
506         //write("(", a, ",", r, ",", g, ",", b, ") ");
507         tex.setPixel(x, y, (a<<24)|(r<<16)|(g<<8)|b);
508       }
509       foreach (int x; frameW..txw) tex.setPixel(x, y, 0xff_00_00_00);
510       //writeln();
511     }
512     foreach (int y; frameH..txh) foreach (int x; frameW..txw) tex.setPixel(x, y, 0xff_00_00_00);
513     // update texture image
514     tex.upload();
515     // create sprite frame
516     auto frm = SpawnObject(SpriteFrame);
517     //frm.Name = frmName;
518     frm.index = fridx;
519     frm.tex = tex;
520     frm.xofs = xofs;
521     frm.yofs = yofs;
522     frm.wdt = frameW;
523     frm.hgt = frameH;
524     frm.goodSize = (txw == frameW && txh == frameH);
525     frm.mask = cmask;
526     frm.bx = x0;
527     frm.by = y0;
528     frm.bw = (x1 < x0 ? 0 : x1-x0+1);
529     frm.bh = (y1 < y0 ? 0 : y1-y0+1);
530     frm.precise = spimg.precise;
531     frm.processMask();
532     spimg.frames[fridx] = frm;
533   }
534   delete fl;
535   return spimg;
539 // ////////////////////////////////////////////////////////////////////////// //
540 class SpriteStore : Object transient;
542 array!SpriteImage sprbyid;
543 string sprPath;
544 bool bDumpLoaded;
545 int allowNPot = -1;
546 SpriteImage fontImg;
547 name fontName;
550 private final void loadSprite (name Name) {
551   if (!Name) FatalError("cannot load namelss sprite");
553   if (allowNPot < 0) {
554     if (!GLVideo.isInitialized) {
555       writeln("preloading sprite '", Name, "'");
556     } else {
557       allowNPot = (GLVideo.glHasNPOT ? 1 : 0);
558       if (allowNPot) writeln("*** NPOT textures allowed");
559       //allowNPot = 0;
560     }
561   }
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;
568   if (bDumpLoaded) {
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)" : ""));
578     }
579   }
583 final void loadFont (name Name) {
584   if (fontName != Name) {
585     fontName = Name;
586     fontImg = opIndex(Name);
587   }
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;
606   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;
614   int res = 0;
615   int inHigh = 0;
616   foreach (auto sidx; 0..str.length) {
617     auto ch = str[sidx];
618     if (processHighlights1) {
619       if (ch == '~' && (!inHigh || inHigh == 1)) {
620         inHigh = !inHigh;
621         continue;
622       }
623     }
624     if (processHighlights2) {
625       if (ch == '|' && (!inHigh || inHigh == 2)) {
626         inHigh = 2-inHigh;
627         continue;
628       }
629     }
630     res += getCharWidth(ch, scale);
631   }
632   return res;
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;
645   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) {
658     auto ch = str[sidx];
659     if (ch >= "a" && ch <= "z" && ch-32 >= flen) ch -= 32;
660     ch -= 32;
661     if (ch >= 0 && ch < flen) {
662       fontImg.frames[ch].blitAt(x, y, scale);
663       x += fontImg.frames[ch].width*scale;
664     } else {
665       x += fontImg.frames[0].width*scale;
666     }
667   }
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) {
678     auto ch = str[sidx];
679     if (ch >= "a" && ch <= "z" && ch-32 >= flen) ch -= 32;
680     ch -= 32;
681     if (ch >= 0 && ch < flen) {
682       if (specified_hotCharOfs && specified_hotCharColor) {
683         if (hotCharOfs == sidx) GLVideo.color = hotCharColor;
684       }
685       fontImg.frames[ch].blitAt(x, y, scale);
686       x += fontImg.frames[ch].width*scale;
687       GLVideo.color = oclr;
688     } else {
689       x += fontImg.frames[0].width*scale;
690     }
691   }
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;
702   int inHigh = 0;
703   foreach (auto sidx; 0..str.length) {
704     auto ch = str[sidx];
705     if (ch == '~' && (!inHigh || inHigh == 1)) {
706       inHigh = !inHigh;
707       GLVideo.color = (inHigh ? hiColor : oclr);
708       continue;
709     }
710     if (specified_hiColor1 && ch == '|' && (!inHigh || inHigh == 2)) {
711       inHigh = 2-inHigh;
712       GLVideo.color = (inHigh ? hiColor1 : oclr);
713       continue;
714     }
715     if (ch >= "a" && ch <= "z" && ch-32 >= flen) ch -= 32;
716     ch -= 32;
717     if (ch >= 0 && ch < flen) {
718       fontImg.frames[ch].blitAt(x, y, scale);
719       x += fontImg.frames[ch].width*scale;
720     } else {
721       x += fontImg.frames[0].width*scale;
722     }
723   }
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;
733   int x0 = x;
734   int sidx = 0;
735   bool skipSpaces = false;
736   while (sidx < str.length) {
737     auto ch = str[sidx];
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)
742     int epos = sidx;
743     int wwdt = 0;
744     if (!skipSpaces) {
745       while (epos < str.length && str[epos] <= 32 && str[epos] != '\n') wwdt += getCharWidth(str[epos++], scale!optional);
746     }
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) {
750       // yes, do wrapping
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
754       x = x0;
755       y += fontImg.frames[0].height*scale;
756     }
757     // print it
758     while (sidx < epos) x += renderChar(x, y, str[sidx++], scale!optional);
759     x += getCharWidth(32, scale!optional);
760     // skip the following spaces
761     skipSpaces = true;
762   }
766 // splitted by "\n"
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;
770   int inHigh = 0;
771   foreach (auto sidx; 0..str.length) {
772     auto ch = str[sidx];
773     if (processHighlights1) {
774       if (ch == '~' && (!inHigh || inHigh == 1)) {
775         inHigh = !inHigh;
776         continue;
777       }
778     }
779     if (processHighlights2) {
780       if (ch == '|' && (!inHigh || inHigh == 2)) {
781         inHigh = 2-inHigh;
782         continue;
783       }
784     }
785     if (ch == '\n') {
786       maxwdt = max(maxwdt, curwdt);
787       curwdt = 0;
788     } else {
789       curwdt += getCharWidth(str[sidx], scale);
790     }
791   }
792   return max(curwdt, maxwdt);
796 // splitted by "\n"
797 final int getMultilineTextHeight (string str, optional int scale) {
798   if (!specified_scale) scale = 1;
799   int lineCount = 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);
815   int pos = 0;
816   int endpos = str.length;
817   while (endpos > 0 && str[endpos] == '\n') --endpos;
818   if (y < 0) {
819     int lc = 1;
820     foreach (auto sidx; pos..endpos) if (str[sidx] == '\n') ++lc;
821     if (y == int.min) {
822       y = (GLVideo.screenHeight-lc*getFontHeight(scale!optional))/2;
823     } else {
824       y = (-y-lc*getFontHeight(scale!optional))/2;
825     }
826   }
827   while (pos < endpos) {
828     int epos = pos;
829     while (epos < str.length && str[epos] != '\n') ++epos;
830     string s = str[pos..epos];
831     pos = epos+1;
832     if (specified_highColor1 || specified_highColor2) {
833       renderTextWithHighlight(x-getTextWidth(s, scale, specified_highColor1, specified_highColor2)/2, y, s, scale, highColor1!optional, highColor2!optional);
834     } else {
835       renderText(x-getTextWidth(s, scale)/2, y, s, scale);
836     }
837     y += fontImg.frames[0].height*scale;
838   }
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);
849   int pos = 0;
850   int endpos = str.length;
851   while (endpos > 0 && str[endpos] == '\n') --endpos;
852   if (y < 0) {
853     int lc = 1;
854     foreach (auto sidx; pos..endpos) if (str[sidx] == '\n') ++lc;
855     if (y == int.min) {
856       y = (GLVideo.screenHeight-lc*getFontHeight(scale!optional))/2;
857     } else {
858       y = (-y-lc*getFontHeight(scale!optional))/2;
859     }
860   }
861   while (pos < endpos) {
862     int epos = pos;
863     while (epos < str.length && str[epos] != '\n') ++epos;
864     string s = str[pos..epos];
865     pos = epos+1;
866     if (specified_highColor1 || specified_highColor2) {
867       renderTextWithHighlight(x, y, s, scale, highColor1!optional, highColor2!optional);
868     } else {
869       renderText(x, y, s, scale);
870     }
871     y += fontImg.frames[0].height*scale;
872   }
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);
884 defaultproperties {
885   //sprPath = "data/gfx/sprites";
886   sprPath = "sprites";