summaryrefslogtreecommitdiff
path: root/src/engine
diff options
context:
space:
mode:
authorxolatile2025-07-16 23:07:43 +0200
committerxolatile2025-07-16 23:07:43 +0200
commit7256502afa0babe60fcafbd2888cd3e33c3f9b6b (patch)
tree8a8495662a69bdadc4b5d9152656b9f02a44d668 /src/engine
parentbc596ac9d4cdd00abf537b88d3c544be161330cc (diff)
downloadxolatile-badassbug-7256502afa0babe60fcafbd2888cd3e33c3f9b6b.tar.xz
xolatile-badassbug-7256502afa0babe60fcafbd2888cd3e33c3f9b6b.tar.zst
Source code, broken...
Diffstat (limited to 'src/engine')
-rw-r--r--src/engine/3dgui.cpp1398
-rw-r--r--src/engine/animmodel.h1617
-rw-r--r--src/engine/bih.cpp330
-rw-r--r--src/engine/bih.h79
-rw-r--r--src/engine/blend.cpp862
-rw-r--r--src/engine/blob.cpp727
-rw-r--r--src/engine/client.cpp274
-rw-r--r--src/engine/command.cpp3484
-rw-r--r--src/engine/console.cpp785
-rw-r--r--src/engine/decal.cpp642
-rw-r--r--src/engine/depthfx.h193
-rw-r--r--src/engine/dynlight.cpp227
-rw-r--r--src/engine/engine.h613
-rw-r--r--src/engine/explosion.h249
-rw-r--r--src/engine/glare.cpp71
-rw-r--r--src/engine/grass.cpp356
-rw-r--r--src/engine/lensflare.h193
-rw-r--r--src/engine/lightmap.cpp2729
-rw-r--r--src/engine/lightmap.h146
-rw-r--r--src/engine/lightning.h123
-rw-r--r--src/engine/main.cpp1422
-rw-r--r--src/engine/master.cpp718
-rw-r--r--src/engine/material.cpp886
-rw-r--r--src/engine/md3.h183
-rw-r--r--src/engine/md5.h419
-rw-r--r--src/engine/menus.cpp783
-rw-r--r--src/engine/model.h90
-rw-r--r--src/engine/movie.cpp1157
-rw-r--r--src/engine/mpr.h575
-rw-r--r--src/engine/normal.cpp383
-rw-r--r--src/engine/obj.h191
-rw-r--r--src/engine/octa.cpp1880
-rw-r--r--src/engine/octa.h340
-rw-r--r--src/engine/octaedit.cpp2977
-rw-r--r--src/engine/octarender.cpp1803
-rw-r--r--src/engine/physics.cpp2057
-rw-r--r--src/engine/pvs.cpp1315
-rw-r--r--src/engine/ragdoll.h534
-rw-r--r--src/engine/rendergl.cpp2388
-rw-r--r--src/engine/rendermodel.cpp1142
-rw-r--r--src/engine/renderparticles.cpp1551
-rw-r--r--src/engine/rendersky.cpp774
-rw-r--r--src/engine/rendertarget.h464
-rw-r--r--src/engine/rendertext.cpp392
-rw-r--r--src/engine/renderva.cpp1896
-rw-r--r--src/engine/server.cpp1154
-rw-r--r--src/engine/serverbrowser.cpp751
-rw-r--r--src/engine/shader.cpp1522
-rw-r--r--src/engine/shadowmap.cpp329
-rw-r--r--src/engine/skelmodel.h1861
-rw-r--r--src/engine/smd.h447
-rw-r--r--src/engine/sound.cpp991
-rw-r--r--src/engine/textedit.h770
-rw-r--r--src/engine/texture.cpp3644
-rw-r--r--src/engine/texture.h779
-rw-r--r--src/engine/vertmodel.h490
-rw-r--r--src/engine/water.cpp1061
-rw-r--r--src/engine/world.cpp1391
-rw-r--r--src/engine/world.h59
-rw-r--r--src/engine/worldio.cpp1388
60 files changed, 58055 insertions, 0 deletions
diff --git a/src/engine/3dgui.cpp b/src/engine/3dgui.cpp
new file mode 100644
index 0000000..f1f6ef2
--- /dev/null
+++ b/src/engine/3dgui.cpp
@@ -0,0 +1,1398 @@
+// creates multiple gui windows that float inside the 3d world
+
+// special feature is that its mostly *modeless*: you can use this menu while playing, without turning menus on or off
+// implementationwise, it is *stateless*: it keeps no internal gui structure, hit tests are instant, usage & implementation is greatly simplified
+
+#include "engine.h"
+
+#include "textedit.h"
+
+static bool layoutpass, actionon = false;
+static int mousebuttons = 0;
+static struct gui *windowhit = NULL;
+
+static float firstx, firsty;
+
+enum {FIELDCOMMIT, FIELDABORT, FIELDEDIT, FIELDSHOW, FIELDKEY};
+
+static int fieldmode = FIELDSHOW;
+static bool fieldsactive = false;
+
+static bool hascursor;
+static float cursorx = 0.5f, cursory = 0.5f;
+
+#define SHADOW 4
+#define ICON_SIZE (FONTH-SHADOW)
+#define SKIN_W 256
+#define SKIN_H 128
+#define SKIN_SCALE 4
+#define INSERT (3*SKIN_SCALE)
+#define MAXCOLUMNS 16
+
+VARP(guiautotab, 6, 16, 40);
+VARP(guiclicktab, 0, 0, 1);
+VARP(guifadein, 0, 1, 1);
+VARP(guipreviewtime, 0, 15, 1000);
+
+static int lastpreview = 0;
+
+static inline bool throttlepreview(bool loaded)
+{
+ if(loaded) return true;
+ if(totalmillis - lastpreview < guipreviewtime) return false;
+ lastpreview = totalmillis;
+ return true;
+}
+
+struct gui : g3d_gui
+{
+ struct list
+ {
+ int parent, w, h, springs, curspring, column;
+ };
+
+ int firstlist, nextlist;
+ int columns[MAXCOLUMNS];
+
+ static vector<list> lists;
+ static float hitx, hity;
+ static int curdepth, curlist, xsize, ysize, curx, cury;
+ static bool shouldmergehits, shouldautotab;
+
+ static void reset()
+ {
+ lists.setsize(0);
+ }
+
+ static int ty, tx, tpos, *tcurrent, tcolor; //tracking tab size and position since uses different layout method...
+
+ bool allowautotab(bool on)
+ {
+ bool oldval = shouldautotab;
+ shouldautotab = on;
+ return oldval;
+ }
+
+ void autotab()
+ {
+ if(tcurrent)
+ {
+ if(layoutpass && !tpos) tcurrent = NULL; //disable tabs because you didn't start with one
+ if(shouldautotab && !curdepth && (layoutpass ? 0 : cury) + ysize > guiautotab*FONTH) tab(NULL, tcolor);
+ }
+ }
+
+ bool shouldtab()
+ {
+ if(tcurrent && shouldautotab)
+ {
+ if(layoutpass)
+ {
+ int space = guiautotab*FONTH - ysize;
+ if(space < 0) return true;
+ int l = lists[curlist].parent;
+ while(l >= 0)
+ {
+ space -= lists[l].h;
+ if(space < 0) return true;
+ l = lists[l].parent;
+ }
+ }
+ else
+ {
+ int space = guiautotab*FONTH - cury;
+ if(ysize > space) return true;
+ int l = lists[curlist].parent;
+ while(l >= 0)
+ {
+ if(lists[l].h > space) return true;
+ l = lists[l].parent;
+ }
+ }
+ }
+ return false;
+ }
+
+ bool visible() { return (!tcurrent || tpos==*tcurrent) && !layoutpass; }
+
+ //tab is always at top of page
+ void tab(const char *name, int color)
+ {
+ if(curdepth != 0) return;
+ if(color) tcolor = color;
+ tpos++;
+ if(!name) name = intstr(tpos);
+ int w = max(text_width(name) - 2*INSERT, 0);
+ if(layoutpass)
+ {
+ ty = max(ty, ysize);
+ ysize = 0;
+ }
+ else
+ {
+ cury = -ysize;
+ int h = FONTH-2*INSERT,
+ x1 = curx + tx,
+ x2 = x1 + w + ((skinx[3]-skinx[2]) + (skinx[5]-skinx[4]))*SKIN_SCALE,
+ y1 = cury - ((skiny[6]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE-h,
+ y2 = cury;
+ bool hit = tcurrent && windowhit==this && hitx>=x1 && hity>=y1 && hitx<x2 && hity<y2;
+ if(hit && (!guiclicktab || mousebuttons&G3D_DOWN))
+ *tcurrent = tpos; //roll-over to switch tab
+
+ drawskin(x1-skinx[visible()?2:6]*SKIN_SCALE, y1-skiny[1]*SKIN_SCALE, w, h, visible()?10:19, 9, gui2d ? 1 : 2, light, alpha);
+ text_(name, x1 + (skinx[3]-skinx[2])*SKIN_SCALE - (w ? INSERT : INSERT/2), y1 + (skiny[2]-skiny[1])*SKIN_SCALE - INSERT, tcolor, visible());
+ }
+ tx += w + ((skinx[5]-skinx[4]) + (skinx[3]-skinx[2]))*SKIN_SCALE;
+ }
+
+ bool ishorizontal() const { return curdepth&1; }
+ bool isvertical() const { return !ishorizontal(); }
+
+ void pushlist()
+ {
+ if(layoutpass)
+ {
+ if(curlist>=0)
+ {
+ lists[curlist].w = xsize;
+ lists[curlist].h = ysize;
+ }
+ list &l = lists.add();
+ l.parent = curlist;
+ l.springs = 0;
+ l.column = -1;
+ curlist = lists.length()-1;
+ xsize = ysize = 0;
+ }
+ else
+ {
+ curlist = nextlist++;
+ if(curlist >= lists.length()) // should never get here unless script code doesn't use same amount of lists in layout and render passes
+ {
+ list &l = lists.add();
+ l.parent = curlist;
+ l.springs = 0;
+ l.column = -1;
+ l.w = l.h = 0;
+ }
+ list &l = lists[curlist];
+ l.curspring = 0;
+ if(l.springs > 0)
+ {
+ if(ishorizontal()) xsize = l.w; else ysize = l.h;
+ }
+ else
+ {
+ xsize = l.w;
+ ysize = l.h;
+ }
+ }
+ curdepth++;
+ }
+
+ void poplist()
+ {
+ if(!lists.inrange(curlist)) return;
+ list &l = lists[curlist];
+ if(layoutpass)
+ {
+ l.w = xsize;
+ l.h = ysize;
+ if(l.column >= 0) columns[l.column] = max(columns[l.column], ishorizontal() ? ysize : xsize);
+ }
+ curlist = l.parent;
+ curdepth--;
+ if(lists.inrange(curlist))
+ {
+ int w = xsize, h = ysize;
+ if(ishorizontal()) cury -= h; else curx -= w;
+ list &p = lists[curlist];
+ xsize = p.w;
+ ysize = p.h;
+ if(!layoutpass && p.springs > 0)
+ {
+ list &s = lists[p.parent];
+ if(ishorizontal()) xsize = s.w; else ysize = s.h;
+ }
+ layout(w, h);
+ }
+ }
+
+ int text (const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, false, false); }
+ int button(const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, true, false); }
+ int title (const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, false, true); }
+
+ void separator() { autotab(); line_(FONTH/3); }
+ void progress(float percent) { autotab(); line_((FONTH*4)/5, percent); }
+
+ //use to set min size (useful when you have progress bars)
+ void strut(float size) { layout(isvertical() ? int(size*FONTW) : 0, isvertical() ? 0 : int(size*FONTH)); }
+ //add space between list items
+ void space(float size) { layout(isvertical() ? 0 : int(size*FONTW), isvertical() ? int(size*FONTH) : 0); }
+
+ void spring(int weight)
+ {
+ if(curlist < 0) return;
+ list &l = lists[curlist];
+ if(layoutpass) { if(l.parent >= 0) l.springs += weight; return; }
+ int nextspring = min(l.curspring + weight, l.springs);
+ if(nextspring <= l.curspring) return;
+ if(ishorizontal())
+ {
+ int w = xsize - l.w;
+ layout((w*nextspring)/l.springs - (w*l.curspring)/l.springs, 0);
+ }
+ else
+ {
+ int h = ysize - l.h;
+ layout(0, (h*nextspring)/l.springs - (h*l.curspring)/l.springs);
+ }
+ l.curspring = nextspring;
+ }
+
+ void column(int col)
+ {
+ if(curlist < 0 || !layoutpass || col < 0 || col >= MAXCOLUMNS) return;
+ list &l = lists[curlist];
+ l.column = col;
+ }
+
+ int layout(int w, int h)
+ {
+ if(layoutpass)
+ {
+ if(ishorizontal())
+ {
+ xsize += w;
+ ysize = max(ysize, h);
+ }
+ else
+ {
+ xsize = max(xsize, w);
+ ysize += h;
+ }
+ return 0;
+ }
+ else
+ {
+ bool hit = ishit(w, h);
+ if(ishorizontal()) curx += w;
+ else cury += h;
+ return (hit && visible()) ? mousebuttons|G3D_ROLLOVER : 0;
+ }
+ }
+
+ bool mergehits(bool on)
+ {
+ bool oldval = shouldmergehits;
+ shouldmergehits = on;
+ return oldval;
+ }
+
+ bool ishit(int w, int h, int x = curx, int y = cury)
+ {
+ if(shouldmergehits) return windowhit==this && (ishorizontal() ? hitx>=x && hitx<x+w : hity>=y && hity<y+h);
+ if(ishorizontal()) h = ysize;
+ else w = xsize;
+ return windowhit==this && hitx>=x && hity>=y && hitx<x+w && hity<y+h;
+ }
+
+ int image(Texture *t, float scale, const char *overlaid)
+ {
+ autotab();
+ if(scale==0) scale = 1;
+ int size = (int)(scale*2*FONTH)-SHADOW;
+ if(visible()) icon_(t, overlaid!=NULL, curx, cury, size, ishit(size+SHADOW, size+SHADOW), overlaid);
+ return layout(size+SHADOW, size+SHADOW);
+ }
+
+ int texture(VSlot &vslot, float scale, bool overlaid)
+ {
+ autotab();
+ if(scale==0) scale = 1;
+ int size = (int)(scale*2*FONTH)-SHADOW;
+ if(visible()) previewslot(vslot, overlaid, curx, cury, size, ishit(size+SHADOW, size+SHADOW));
+ return layout(size+SHADOW, size+SHADOW);
+ }
+
+ int playerpreview(int model, int team, int weap, float sizescale, const char *overlaid)
+ {
+ autotab();
+ if(sizescale==0) sizescale = 1;
+ int size = (int)(sizescale*2*FONTH)-SHADOW;
+ if(model>=0 && visible())
+ {
+ bool hit = ishit(size+SHADOW, size+SHADOW);
+ float xs = size, ys = size, xi = curx, yi = cury;
+ if(overlaid && hit && actionon)
+ {
+ hudnotextureshader->set();
+ gle::colorf(0, 0, 0, 0.75f);
+ rect_(xi+SHADOW, yi+SHADOW, xs, ys);
+ hudshader->set();
+ }
+ int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))),
+ x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y))));
+ glDisable(GL_BLEND);
+ modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL);
+ game::renderplayerpreview(model, team, weap);
+ modelpreview::end();
+ hudshader->set();
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND);
+ if(overlaid)
+ {
+ if(hit)
+ {
+ hudnotextureshader->set();
+ glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+ gle::colorf(1, 0.5f, 0.5f);
+ rect_(xi, yi, xs, ys);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ hudshader->set();
+ }
+ if(overlaid[0]) text_(overlaid, xi + xs/12, yi + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit, hit);
+ if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+ gle::color(light);
+ glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+ rect_(xi, yi, xs, ys, 0);
+ }
+ }
+ return layout(size+SHADOW, size+SHADOW);
+ }
+
+ int modelpreview(const char *name, int anim, float sizescale, const char *overlaid, bool throttle)
+ {
+ autotab();
+ if(sizescale==0) sizescale = 1;
+ int size = (int)(sizescale*2*FONTH)-SHADOW;
+ if(name[0] && visible() && (!throttle || throttlepreview(modelloaded(name))))
+ {
+ bool hit = ishit(size+SHADOW, size+SHADOW);
+ float xs = size, ys = size, xi = curx, yi = cury;
+ if(overlaid && hit && actionon)
+ {
+ hudnotextureshader->set();
+ gle::colorf(0, 0, 0, 0.75f);
+ rect_(xi+SHADOW, yi+SHADOW, xs, ys);
+ hudshader->set();
+ }
+ int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))),
+ x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y))));
+ glDisable(GL_BLEND);
+ modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL);
+ model *m = loadmodel(name);
+ if(m)
+ {
+ entitylight light;
+ light.color = vec(1, 1, 1);
+ light.dir = vec(0, -1, 2).normalize();
+ vec center, radius;
+ m->boundbox(center, radius);
+ float yaw;
+ vec o = calcmodelpreviewpos(radius, yaw).sub(center);
+ rendermodel(&light, name, anim, o, yaw, 0, 0, NULL, NULL, 0);
+ }
+ modelpreview::end();
+ hudshader->set();
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND);
+ if(overlaid)
+ {
+ if(hit)
+ {
+ hudnotextureshader->set();
+ glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+ gle::colorf(1, 0.5f, 0.5f);
+ rect_(xi, yi, xs, ys);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ hudshader->set();
+ }
+ if(overlaid[0]) text_(overlaid, xi + xs/12, yi + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit, hit);
+ if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+ gle::color(light);
+ glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+ rect_(xi, yi, xs, ys, 0);
+ }
+ }
+ return layout(size+SHADOW, size+SHADOW);
+ }
+
+ int prefabpreview(const char *prefab, const vec &color, float sizescale, const char *overlaid, bool throttle)
+ {
+ autotab();
+ if(sizescale==0) sizescale = 1;
+ int size = (int)(sizescale*2*FONTH)-SHADOW;
+ if(prefab[0] && visible() && (!throttle || throttlepreview(prefabloaded(prefab))))
+ {
+ bool hit = ishit(size+SHADOW, size+SHADOW);
+ float xs = size, ys = size, xi = curx, yi = cury;
+ if(overlaid && hit && actionon)
+ {
+ hudnotextureshader->set();
+ gle::colorf(0, 0, 0, 0.75f);
+ rect_(xi+SHADOW, yi+SHADOW, xs, ys);
+ hudshader->set();
+ }
+ int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))),
+ x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y))));
+ glDisable(GL_BLEND);
+ modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL);
+ previewprefab(prefab, color);
+ modelpreview::end();
+ hudshader->set();
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND);
+ if(overlaid)
+ {
+ if(hit)
+ {
+ hudnotextureshader->set();
+ glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+ gle::colorf(1, 0.5f, 0.5f);
+ rect_(xi, yi, xs, ys);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ hudshader->set();
+ }
+ if(overlaid[0]) text_(overlaid, xi + FONTH/2, yi + FONTH/2, hit ? 0xFF0000 : 0xFFFFFF, hit, hit);
+ if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+ gle::color(light);
+ glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+ rect_(xi, yi, xs, ys, 0);
+ }
+ }
+ return layout(size+SHADOW, size+SHADOW);
+ }
+
+ void slider(int &val, int vmin, int vmax, int color, const char *label)
+ {
+ autotab();
+ int x = curx;
+ int y = cury;
+ line_((FONTH*2)/3);
+ if(visible())
+ {
+ if(!label) label = intstr(val);
+ int w = text_width(label);
+
+ bool hit;
+ int px, py, offset = vmin < vmax ? clamp(val, vmin, vmax) : clamp(val, vmax, vmin);
+ if(ishorizontal())
+ {
+ hit = ishit(FONTH, ysize, x, y);
+ px = x + (FONTH-w)/2;
+ py = y + (ysize-FONTH) - ((ysize-FONTH)*(offset-vmin))/((vmax==vmin) ? 1 : (vmax-vmin)); //vmin at bottom
+ }
+ else
+ {
+ hit = ishit(xsize, FONTH, x, y);
+ px = x + FONTH/2 - w/2 + ((xsize-w)*(offset-vmin))/((vmax==vmin) ? 1 : (vmax-vmin)); //vmin at left
+ py = y;
+ }
+
+ if(hit) color = 0xFF0000;
+ text_(label, px, py, color, hit && actionon, hit);
+ if(hit && actionon)
+ {
+ int vnew = (vmin < vmax ? 1 : -1)+vmax-vmin;
+ if(ishorizontal()) vnew = int((vnew*(y+ysize-FONTH/2-hity))/(ysize-FONTH));
+ else vnew = int((vnew*(hitx-x-FONTH/2))/(xsize-w));
+ vnew += vmin;
+ vnew = vmin < vmax ? clamp(vnew, vmin, vmax) : clamp(vnew, vmax, vmin);
+ if(vnew != val) val = vnew;
+ }
+ }
+ }
+
+ char *field(const char *name, int color, int length, int height, const char *initval, int initmode)
+ {
+ return field_(name, color, length, height, initval, initmode, FIELDEDIT);
+ }
+
+ char *keyfield(const char *name, int color, int length, int height, const char *initval, int initmode)
+ {
+ return field_(name, color, length, height, initval, initmode, FIELDKEY);
+ }
+
+ char *field_(const char *name, int color, int length, int height, const char *initval, int initmode, int fieldtype = FIELDEDIT)
+ {
+ editor *e = useeditor(name, initmode, false, initval); // generate a new editor if necessary
+ if(layoutpass)
+ {
+ if(initval && e->mode==EDITORFOCUSED && (e!=currentfocus() || fieldmode == FIELDSHOW))
+ {
+ if(strcmp(e->lines[0].text, initval)) e->clear(initval);
+ }
+ e->linewrap = (length<0);
+ e->maxx = (e->linewrap) ? -1 : length;
+ e->maxy = (height<=0)?1:-1;
+ e->pixelwidth = abs(length)*FONTW;
+ if(e->linewrap && e->maxy==1)
+ {
+ int temp;
+ text_bounds(e->lines[0].text, temp, e->pixelheight, e->pixelwidth); //only single line editors can have variable height
+ }
+ else
+ e->pixelheight = FONTH*max(height, 1);
+ }
+ int h = e->pixelheight;
+ int w = e->pixelwidth + FONTW;
+
+ bool wasvertical = isvertical();
+ if(wasvertical && e->maxy != 1) pushlist();
+
+ char *result = NULL;
+ if(visible() && !layoutpass)
+ {
+ e->rendered = true;
+
+ bool hit = ishit(w, h);
+ if(hit)
+ {
+ if(mousebuttons&G3D_DOWN) //mouse request focus
+ {
+ if(fieldtype==FIELDKEY) e->clear();
+ useeditor(name, initmode, true);
+ e->mark(false);
+ fieldmode = fieldtype;
+ }
+ }
+ bool editing = (fieldmode != FIELDSHOW) && (e==currentfocus());
+ if(hit && editing && (mousebuttons&G3D_PRESSED)!=0 && fieldtype==FIELDEDIT) e->hit(int(floor(hitx-(curx+FONTW/2))), int(floor(hity-cury)), (mousebuttons&G3D_DRAGGED)!=0); //mouse request position
+ if(editing && ((fieldmode==FIELDCOMMIT) || (fieldmode==FIELDABORT) || !hit)) // commit field if user pressed enter or wandered out of focus
+ {
+ if(fieldmode==FIELDCOMMIT || (fieldmode!=FIELDABORT && !hit)) result = e->currentline().text;
+ e->active = (e->mode!=EDITORFOCUSED);
+ fieldmode = FIELDSHOW;
+ }
+ else fieldsactive = true;
+
+ e->draw(curx+FONTW/2, cury, color, hit && editing);
+
+ hudnotextureshader->set();
+ glDisable(GL_BLEND);
+ if(editing) gle::colorf(1, 0, 0);
+ else gle::colorub(color>>16, (color>>8)&0xFF, color&0xFF);
+ rect_(curx, cury, w, h, true);
+ glEnable(GL_BLEND);
+ hudshader->set();
+ }
+ layout(w, h);
+
+ if(e->maxy != 1)
+ {
+ int slines = e->limitscrolly();
+ if(slines > 0)
+ {
+ int pos = e->scrolly;
+ slider(e->scrolly, slines, 0, color, NULL);
+ if(pos != e->scrolly) e->cy = e->scrolly;
+ }
+ if(wasvertical) poplist();
+ }
+
+ return result;
+ }
+
+ void rect_(float x, float y, float w, float h, bool lines = false)
+ {
+ gle::defvertex(2);
+ gle::begin(lines ? GL_LINE_LOOP : GL_TRIANGLE_STRIP);
+ gle::attribf(x, y);
+ gle::attribf(x + w, y);
+ if(lines) gle::attribf(x + w, y + h);
+ gle::attribf(x, y + h);
+ if(!lines) gle::attribf(x + w, y + h);
+ xtraverts += gle::end();
+ }
+
+ void rect_(float x, float y, float w, float h, int usetc)
+ {
+ gle::defvertex(2);
+ gle::deftexcoord0();
+ gle::begin(GL_TRIANGLE_STRIP);
+ static const vec2 tc[5] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1), vec2(0, 0) };
+ gle::attribf(x, y); gle::attrib(tc[usetc]);
+ gle::attribf(x + w, y); gle::attrib(tc[usetc+1]);
+ gle::attribf(x, y + h); gle::attrib(tc[usetc+3]);
+ gle::attribf(x + w, y + h); gle::attrib(tc[usetc+2]);
+ xtraverts += gle::end();
+ }
+
+ void text_(const char *text, int x, int y, int color, bool shadow, bool force = false)
+ {
+ if(shadow) draw_text(text, x+SHADOW, y+SHADOW, 0x00, 0x00, 0x00, -0xC0);
+ draw_text(text, x, y, color>>16, (color>>8)&0xFF, color&0xFF, force ? -0xFF : 0xFF);
+ }
+
+ void background(int color, int inheritw, int inherith)
+ {
+ if(layoutpass) return;
+ hudnotextureshader->set();
+ gle::colorub(color>>16, (color>>8)&0xFF, color&0xFF, 0x80);
+ int w = xsize, h = ysize;
+ if(inheritw>0)
+ {
+ int parentw = curlist, parentdepth = 0;
+ for(;parentdepth < inheritw && lists[parentw].parent>=0; parentdepth++)
+ parentw = lists[parentw].parent;
+ list &p = lists[parentw];
+ w = p.springs > 0 && (curdepth-parentdepth)&1 ? lists[p.parent].w : p.w;
+ }
+ if(inherith>0)
+ {
+ int parenth = curlist, parentdepth = 0;
+ for(;parentdepth < inherith && lists[parenth].parent>=0; parentdepth++)
+ parenth = lists[parenth].parent;
+ list &p = lists[parenth];
+ h = p.springs > 0 && !((curdepth-parentdepth)&1) ? lists[p.parent].h : p.h;
+ }
+ rect_(curx, cury, w, h);
+ hudshader->set();
+ }
+
+ void icon_(Texture *t, bool overlaid, int x, int y, int size, bool hit, const char *title = NULL)
+ {
+ float scale = float(size)/max(t->xs, t->ys); //scale and preserve aspect ratio
+ float xs = t->xs*scale, ys = t->ys*scale;
+ x += int((size-xs)/2);
+ y += int((size-ys)/2);
+ const vec &color = hit ? vec(1, 0.5f, 0.5f) : (overlaid ? vec(1, 1, 1) : light);
+ glBindTexture(GL_TEXTURE_2D, t->id);
+ if(hit && actionon)
+ {
+ gle::colorf(0, 0, 0, 0.75f);
+ rect_(x+SHADOW, y+SHADOW, xs, ys, 0);
+ }
+ gle::color(color);
+ rect_(x, y, xs, ys, 0);
+
+ if(overlaid)
+ {
+ if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+ glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+ gle::color(light);
+ rect_(x, y, xs, ys, 0);
+ if(title) text_(title, x + xs/12, y + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit && actionon, hit);
+ }
+ }
+
+ void previewslot(VSlot &vslot, bool overlaid, int x, int y, int size, bool hit)
+ {
+ Slot &slot = *vslot.slot;
+ if(slot.sts.empty()) return;
+ VSlot *layer = NULL;
+ Texture *t = NULL, *glowtex = NULL, *layertex = NULL;
+ if(slot.loaded)
+ {
+ t = slot.sts[0].t;
+ if(t == notexture) return;
+ Slot &slot = *vslot.slot;
+ if(slot.texmask&(1<<TEX_GLOW)) { loopvj(slot.sts) if(slot.sts[j].type==TEX_GLOW) { glowtex = slot.sts[j].t; break; } }
+ if(vslot.layer)
+ {
+ layer = &lookupvslot(vslot.layer);
+ if(!layer->slot->sts.empty()) layertex = layer->slot->sts[0].t;
+ }
+ }
+ else if(slot.thumbnail && slot.thumbnail != notexture) t = slot.thumbnail;
+ else return;
+ float xt = min(1.0f, t->xs/(float)t->ys), yt = min(1.0f, t->ys/(float)t->xs), xs = size, ys = size;
+ if(hit && actionon)
+ {
+ hudnotextureshader->set();
+ gle::colorf(0, 0, 0, 0.75f);
+ rect_(x+SHADOW, y+SHADOW, xs, ys);
+ hudshader->set();
+ }
+ SETSHADER(hudrgb);
+ gle::defvertex(2);
+ gle::deftexcoord0();
+ const vec &color = hit ? vec(1, 0.5f, 0.5f) : (overlaid ? vec(1, 1, 1) : light);
+ vec2 tc[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) };
+ float xoff = vslot.offset.x, yoff = vslot.offset.y;
+ if(vslot.rotation)
+ {
+ const texrotation &r = texrotations[vslot.rotation];
+ if(r.swapxy) { swap(xoff, yoff); loopk(4) swap(tc[k].x, tc[k].y); }
+ if(r.flipx) { xoff *= -1; loopk(4) tc[k].x *= -1; }
+ if(r.flipy) { yoff *= -1; loopk(4) tc[k].y *= -1; }
+ }
+ loopk(4) { tc[k].x = tc[k].x/xt - xoff/t->xs; tc[k].y = tc[k].y/yt - yoff/t->ys; }
+ if(slot.loaded) gle::color(vec(color).mul(vslot.colorscale));
+ else gle::color(color);
+ glBindTexture(GL_TEXTURE_2D, t->id);
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attribf(x, y); gle::attrib(tc[0]);
+ gle::attribf(x+xs, y); gle::attrib(tc[1]);
+ gle::attribf(x, y+ys); gle::attrib(tc[3]);
+ gle::attribf(x+xs, y+ys); gle::attrib(tc[2]);
+ gle::end();
+ if(glowtex)
+ {
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+ glBindTexture(GL_TEXTURE_2D, glowtex->id);
+ if(hit || overlaid) gle::color(vec(vslot.glowcolor).mul(color));
+ else gle::color(vslot.glowcolor);
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attribf(x, y); gle::attrib(tc[0]);
+ gle::attribf(x+xs, y); gle::attrib(tc[1]);
+ gle::attribf(x, y+ys); gle::attrib(tc[3]);
+ gle::attribf(x+xs, y+ys); gle::attrib(tc[2]);
+ gle::end();
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+ if(layertex)
+ {
+ glBindTexture(GL_TEXTURE_2D, layertex->id);
+ gle::color(vec(color).mul(layer->colorscale));
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attribf(x+xs/2, y+ys/2); gle::attrib(tc[0]);
+ gle::attribf(x+xs, y+ys/2); gle::attrib(tc[1]);
+ gle::attribf(x+xs/2, y+ys); gle::attrib(tc[3]);
+ gle::attribf(x+xs, y+ys); gle::attrib(tc[2]);
+ gle::end();
+ }
+
+ hudshader->set();
+ if(overlaid)
+ {
+ if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+ glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+ gle::color(light);
+ rect_(x, y, xs, ys, 0);
+ }
+ }
+
+ void line_(int size, float percent = 1.0f)
+ {
+ if(visible())
+ {
+ if(!slidertex) slidertex = textureload("data/guislider.png", 3);
+ glBindTexture(GL_TEXTURE_2D, slidertex->id);
+ if(percent < 0.99f)
+ {
+ gle::colorf(light.x, light.y, light.z, 0.375f);
+ if(ishorizontal())
+ rect_(curx + FONTH/2 - size/2, cury, size, ysize, 0);
+ else
+ rect_(curx, cury + FONTH/2 - size/2, xsize, size, 1);
+ }
+ gle::color(light);
+ if(ishorizontal())
+ rect_(curx + FONTH/2 - size/2, cury + ysize*(1-percent), size, ysize*percent, 0);
+ else
+ rect_(curx, cury + FONTH/2 - size/2, xsize*percent, size, 1);
+ }
+ layout(ishorizontal() ? FONTH : 0, ishorizontal() ? 0 : FONTH);
+ }
+
+ void textbox(const char *text, int width, int height, int color)
+ {
+ width *= FONTW;
+ height *= FONTH;
+ int w, h;
+ text_bounds(text, w, h, width);
+ if(h > height) height = h;
+ if(visible()) draw_text(text, curx, cury, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, -1, width);
+ layout(width, height);
+ }
+
+ int button_(const char *text, int color, const char *icon, bool clickable, bool center)
+ {
+ const int padding = 10;
+ int w = 0;
+ if(icon) w += ICON_SIZE;
+ if(icon && text) w += padding;
+ if(text) w += text_width(text);
+
+ if(visible())
+ {
+ bool hit = ishit(w, FONTH);
+ if(hit && clickable) color = 0xFF0000;
+ int x = curx;
+ if(isvertical() && center) x += (xsize-w)/2;
+
+ if(icon)
+ {
+ if(icon[0] != ' ')
+ {
+ const char *ext = strrchr(icon, '.');
+ defformatstring(tname, "packages/icons/%s%s", icon, ext ? "" : ".jpg");
+ icon_(textureload(tname, 3), false, x, cury, ICON_SIZE, clickable && hit);
+ }
+ x += ICON_SIZE;
+ }
+ if(icon && text) x += padding;
+ if(text) text_(text, x, cury, color, center || (hit && clickable && actionon), hit && clickable);
+ }
+ return layout(w, FONTH);
+ }
+
+ static Texture *skintex, *overlaytex, *slidertex;
+ static const int skinx[], skiny[];
+ static const struct patch { ushort left, right, top, bottom; uchar flags; } patches[];
+
+ static void drawskin(int x, int y, int gapw, int gaph, int start, int n, int passes = 1, const vec &light = vec(1, 1, 1), float alpha = 0.80f)//int vleft, int vright, int vtop, int vbottom, int start, int n)
+ {
+ if(!skintex) skintex = textureload("data/guiskin.png", 3);
+ glBindTexture(GL_TEXTURE_2D, skintex->id);
+ int gapx1 = INT_MAX, gapy1 = INT_MAX, gapx2 = INT_MAX, gapy2 = INT_MAX;
+ float wscale = 1.0f/(SKIN_W*SKIN_SCALE), hscale = 1.0f/(SKIN_H*SKIN_SCALE);
+
+ loopj(passes)
+ {
+ bool quads = false;
+ if(passes>1) glDepthFunc(j ? GL_LEQUAL : GL_GREATER);
+ gle::color(j ? light : vec(1, 1, 1), passes<=1 || j ? alpha : alpha/2); //ghost when its behind something in depth
+ loopi(n)
+ {
+ const patch &p = patches[start+i];
+ int left = skinx[p.left]*SKIN_SCALE, right = skinx[p.right]*SKIN_SCALE,
+ top = skiny[p.top]*SKIN_SCALE, bottom = skiny[p.bottom]*SKIN_SCALE;
+ float tleft = left*wscale, tright = right*wscale,
+ ttop = top*hscale, tbottom = bottom*hscale;
+ if(p.flags&0x1)
+ {
+ gapx1 = left;
+ gapx2 = right;
+ }
+ else if(left >= gapx2)
+ {
+ left += gapw - (gapx2-gapx1);
+ right += gapw - (gapx2-gapx1);
+ }
+ if(p.flags&0x10)
+ {
+ gapy1 = top;
+ gapy2 = bottom;
+ }
+ else if(top >= gapy2)
+ {
+ top += gaph - (gapy2-gapy1);
+ bottom += gaph - (gapy2-gapy1);
+ }
+
+ //multiple tiled quads if necessary rather than a single stretched one
+ int ystep = bottom-top;
+ int yo = y+top;
+ while(ystep > 0)
+ {
+ if(p.flags&0x10 && yo+ystep-(y+top) > gaph)
+ {
+ ystep = gaph+y+top-yo;
+ tbottom = ttop+ystep*hscale;
+ }
+ int xstep = right-left;
+ int xo = x+left;
+ float tright2 = tright;
+ while(xstep > 0)
+ {
+ if(p.flags&0x01 && xo+xstep-(x+left) > gapw)
+ {
+ xstep = gapw+x+left-xo;
+ tright = tleft+xstep*wscale;
+ }
+ if(!quads)
+ {
+ quads = true;
+ gle::defvertex(2);
+ gle::deftexcoord0();
+ gle::begin(GL_QUADS);
+ }
+ gle::attribf(xo, yo); gle::attribf(tleft, ttop);
+ gle::attribf(xo+xstep, yo); gle::attribf(tright, ttop);
+ gle::attribf(xo+xstep, yo+ystep); gle::attribf(tright, tbottom);
+ gle::attribf(xo, yo+ystep); gle::attribf(tleft, tbottom);
+ if(!(p.flags&0x01)) break;
+ xo += xstep;
+ }
+ tright = tright2;
+ if(!(p.flags&0x10)) break;
+ yo += ystep;
+ }
+ }
+ if(quads) xtraverts += gle::end();
+ else break; //if it didn't happen on the first pass, it won't happen on the second..
+ }
+ if(passes>1) glDepthFunc(GL_ALWAYS);
+ }
+
+ vec origin, scale, *savedorigin;
+ float dist;
+ g3d_callback *cb;
+ bool gui2d;
+
+ static float basescale, maxscale;
+ static bool passthrough;
+ static float alpha;
+ static vec light;
+
+ void adjustscale()
+ {
+ int w = xsize + (skinx[2]-skinx[1])*SKIN_SCALE + (skinx[10]-skinx[9])*SKIN_SCALE, h = ysize + (skiny[9]-skiny[7])*SKIN_SCALE;
+ if(tcurrent) h += ((skiny[5]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE + FONTH-2*INSERT;
+ else h += (skiny[6]-skiny[3])*SKIN_SCALE;
+
+ float aspect = forceaspect ? 1.0f/forceaspect : float(screenh)/float(screenw), fit = 1.0f;
+ if(w*aspect*basescale>1.0f) fit = 1.0f/(w*aspect*basescale);
+ if(h*basescale*fit>maxscale) fit *= maxscale/(h*basescale*fit);
+ origin = vec(0.5f-((w-xsize)/2 - (skinx[2]-skinx[1])*SKIN_SCALE)*aspect*scale.x*fit, 0.5f + (0.5f*h-(skiny[9]-skiny[7])*SKIN_SCALE)*scale.y*fit, 0);
+ scale = vec(aspect*scale.x*fit, scale.y*fit, 1);
+ }
+
+ void start(int starttime, float initscale, int *tab, bool allowinput)
+ {
+ if(gui2d)
+ {
+ initscale *= 0.025f;
+ if(allowinput) hascursor = true;
+ }
+ basescale = initscale;
+ if(layoutpass) scale.x = scale.y = scale.z = guifadein ? basescale*min((totalmillis-starttime)/300.0f, 1.0f) : basescale;
+ alpha = allowinput ? 0.80f : 0.60f;
+ passthrough = scale.x<basescale || !allowinput;
+ curdepth = -1;
+ curlist = -1;
+ tpos = 0;
+ tx = 0;
+ ty = 0;
+ tcurrent = tab;
+ tcolor = 0xFFFFFF;
+ pushlist();
+ if(layoutpass)
+ {
+ firstlist = nextlist = curlist;
+ memset(columns, 0, sizeof(columns));
+ }
+ else
+ {
+ if(tcurrent && !*tcurrent) tcurrent = NULL;
+ cury = -ysize;
+ curx = -xsize/2;
+
+ if(gui2d)
+ {
+ hudmatrix.ortho(0, 1, 1, 0, -1, 1);
+ hudmatrix.translate(origin);
+ hudmatrix.scale(scale);
+
+ light = vec(1, 1, 1);
+ }
+ else
+ {
+ float yaw = atan2f(origin.y-camera1->o.y, origin.x-camera1->o.x);
+ hudmatrix = camprojmatrix;
+ hudmatrix.translate(origin);
+ hudmatrix.rotate_around_z(yaw - 90*RAD);
+ hudmatrix.rotate_around_x(-90*RAD);
+ hudmatrix.scale(-scale.x, scale.y, scale.z);
+
+ vec dir;
+ lightreaching(origin, light, dir, false, 0, 0.5f);
+ float intensity = vec(yaw, 0.0f).dot(dir);
+ light.mul(1.0f + max(intensity, 0.0f));
+ }
+
+ resethudmatrix();
+ hudshader->set();
+
+ drawskin(curx-skinx[2]*SKIN_SCALE, cury-skiny[6]*SKIN_SCALE, xsize, ysize, 0, 9, gui2d ? 1 : 2, light, alpha);
+ if(!tcurrent) drawskin(curx-skinx[5]*SKIN_SCALE, cury-skiny[6]*SKIN_SCALE, xsize, 0, 9, 1, gui2d ? 1 : 2, light, alpha);
+ }
+ }
+
+ void adjusthorizontalcolumn(int col, int i)
+ {
+ int h = columns[col], dh = 0;
+ for(int d = 1; i >= 0; d ^= 1)
+ {
+ list &p = lists[i];
+ if(d&1) { dh = h - p.h; if(dh <= 0) break; p.h = h; }
+ else { p.h += dh; h = p.h; }
+ i = p.parent;
+ }
+ ysize += max(dh, 0);
+ }
+
+ void adjustverticalcolumn(int col, int i)
+ {
+ int w = columns[col], dw = 0;
+ for(int d = 0; i >= 0; d ^= 1)
+ {
+ list &p = lists[i];
+ if(d&1) { p.w += dw; w = p.w; }
+ else { dw = w - p.w; if(dw <= 0) break; p.w = w; }
+ i = p.parent;
+ }
+ xsize = max(xsize, w);
+ }
+
+ void adjustcolumns()
+ {
+ if(lists.inrange(curlist))
+ {
+ list &l = lists[curlist];
+ if(l.column >= 0) columns[l.column] = max(columns[l.column], ishorizontal() ? ysize : xsize);
+ }
+ int parent = -1, depth = 0;
+ for(int i = firstlist; i < lists.length(); i++)
+ {
+ list &l = lists[i];
+ if(l.parent > parent) { parent = l.parent; depth++; }
+ else if(l.parent < parent)
+ {
+ while(parent > l.parent && depth > 0)
+ {
+ parent = lists[parent].parent;
+ depth--;
+ }
+ }
+ if(l.column >= 0)
+ {
+ if(depth&1) adjusthorizontalcolumn(l.column, i);
+ else adjustverticalcolumn(l.column, i);
+ }
+ }
+ }
+
+ void end()
+ {
+ if(layoutpass)
+ {
+ adjustcolumns();
+ xsize = max(tx, xsize);
+ ysize = max(ty, ysize);
+ ysize = max(ysize, (skiny[7]-skiny[6])*SKIN_SCALE);
+ if(tcurrent) *tcurrent = max(1, min(*tcurrent, tpos));
+ if(gui2d) adjustscale();
+ if(!windowhit && !passthrough)
+ {
+ float dist = 0;
+ if(gui2d)
+ {
+ hitx = (cursorx - origin.x)/scale.x;
+ hity = (cursory - origin.y)/scale.y;
+ }
+ else
+ {
+ plane p;
+ p.toplane(vec(origin).sub(camera1->o).set(2, 0).normalize(), origin);
+ if(p.rayintersect(camera1->o, camdir, dist) && dist>=0)
+ {
+ vec hitpos(camdir);
+ hitpos.mul(dist).add(camera1->o).sub(origin);
+ hitx = vec(-p.y, p.x, 0).dot(hitpos)/scale.x;
+ hity = -hitpos.z/scale.y;
+ }
+ }
+ if((mousebuttons & G3D_PRESSED) && (fabs(hitx-firstx) > 2 || fabs(hity - firsty) > 2)) mousebuttons |= G3D_DRAGGED;
+ if(dist>=0 && hitx>=-xsize/2 && hitx<=xsize/2 && hity<=0)
+ {
+ if(hity>=-ysize || (tcurrent && hity>=-ysize-(FONTH-2*INSERT)-((skiny[6]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE && hitx<=tx-xsize/2))
+ windowhit = this;
+ }
+ }
+ }
+ else
+ {
+ if(tcurrent && tx<xsize) drawskin(curx+tx-skinx[5]*SKIN_SCALE, -ysize-skiny[6]*SKIN_SCALE, xsize-tx, FONTH, 9, 1, gui2d ? 1 : 2, light, alpha);
+ }
+ poplist();
+ }
+
+ void draw()
+ {
+ cb->gui(*this, layoutpass);
+ }
+};
+
+Texture *gui::skintex = NULL, *gui::overlaytex = NULL, *gui::slidertex = NULL;
+
+//chop skin into a grid
+const int gui::skiny[] = {0, 7, 21, 34, 43, 48, 56, 104, 111, 117, 128},
+ gui::skinx[] = {0, 11, 23, 37, 105, 119, 137, 151, 215, 229, 246, 256};
+//Note: skinx[3]-skinx[2] = skinx[7]-skinx[6]
+// skinx[5]-skinx[4] = skinx[9]-skinx[8]
+const gui::patch gui::patches[] =
+{ //arguably this data can be compressed - it depends on what else needs to be skinned in the future
+ {1,2,3,6, 0}, // body
+ {2,9,5,6, 0x01},
+ {9,10,3,6, 0},
+
+ {1,2,6,7, 0x10},
+ {2,9,6,7, 0x11},
+ {9,10,6,7, 0x10},
+
+ {1,2,7,9, 0},
+ {2,9,7,9, 0x01},
+ {9,10,7,9, 0},
+
+ {5,6,3,5, 0x01}, // top
+
+ {2,3,1,2, 0}, // selected tab
+ {3,4,1,2, 0x01},
+ {4,5,1,2, 0},
+ {2,3,2,3, 0x10},
+ {3,4,2,3, 0x11},
+ {4,5,2,3, 0x10},
+ {2,3,3,5, 0},
+ {3,4,3,5, 0x01},
+ {4,5,3,5, 0},
+
+ {6,7,1,2, 0}, // deselected tab
+ {7,8,1,2, 0x01},
+ {8,9,1,2, 0},
+ {6,7,2,3, 0x10},
+ {7,8,2,3, 0x11},
+ {8,9,2,3, 0x10},
+ {6,7,3,5, 0},
+ {7,8,3,5, 0x01},
+ {8,9,3,5, 0},
+};
+
+vector<gui::list> gui::lists;
+float gui::basescale, gui::maxscale = 1, gui::hitx, gui::hity, gui::alpha;
+bool gui::passthrough, gui::shouldmergehits = false, gui::shouldautotab = true;
+vec gui::light;
+int gui::curdepth, gui::curlist, gui::xsize, gui::ysize, gui::curx, gui::cury;
+int gui::ty, gui::tx, gui::tpos, *gui::tcurrent, gui::tcolor;
+static vector<gui> guis2d, guis3d;
+
+VARP(guipushdist, 1, 4, 64);
+
+bool g3d_input(const char *str, int len)
+{
+ editor *e = currentfocus();
+ if(fieldmode == FIELDKEY || fieldmode == FIELDSHOW || !e) return false;
+
+ e->input(str, len);
+ return true;
+}
+
+bool g3d_key(int code, bool isdown)
+{
+ editor *e = currentfocus();
+ if(fieldmode == FIELDKEY)
+ {
+ switch(code)
+ {
+ case SDLK_ESCAPE:
+ if(isdown) fieldmode = FIELDCOMMIT;
+ return true;
+ }
+ const char *keyname = getkeyname(code);
+ if(keyname && isdown)
+ {
+ if(e->lines.length()!=1 || !e->lines[0].empty()) e->insert(" ");
+ e->insert(keyname);
+ }
+ return true;
+ }
+
+ if(code==-1 && g3d_windowhit(isdown, true)) return true;
+ else if(code==-3 && g3d_windowhit(isdown, false)) return true;
+
+ if(fieldmode == FIELDSHOW || !e)
+ {
+ if(windowhit) switch(code)
+ {
+ case -4: // window "management"
+ if(isdown)
+ {
+ if(windowhit->gui2d)
+ {
+ vec origin = *guis2d.last().savedorigin;
+ int i = windowhit - &guis2d[0];
+ for(int j = guis2d.length()-1; j > i; j--) *guis2d[j].savedorigin = *guis2d[j-1].savedorigin;
+ *windowhit->savedorigin = origin;
+ if(guis2d.length() > 1)
+ {
+ if(camera1->o.dist(*windowhit->savedorigin) <= camera1->o.dist(*guis2d.last().savedorigin))
+ windowhit->savedorigin->add(camdir);
+ }
+ }
+ else windowhit->savedorigin->add(vec(camdir).mul(guipushdist));
+ }
+ return true;
+ case -5:
+ if(isdown)
+ {
+ if(windowhit->gui2d)
+ {
+ vec origin = *guis2d[0].savedorigin;
+ loopj(guis2d.length()-1) *guis2d[j].savedorigin = *guis2d[j + 1].savedorigin;
+ *guis2d.last().savedorigin = origin;
+ if(guis2d.length() > 1)
+ {
+ if(camera1->o.dist(*guis2d.last().savedorigin) >= camera1->o.dist(*guis2d[0].savedorigin))
+ guis2d.last().savedorigin->sub(camdir);
+ }
+ }
+ else windowhit->savedorigin->sub(vec(camdir).mul(guipushdist));
+ }
+ return true;
+ }
+
+ return false;
+ }
+ switch(code)
+ {
+ case SDLK_ESCAPE: //cancel editing without commit
+ if(isdown) fieldmode = FIELDABORT;
+ return true;
+ case SDLK_RETURN:
+ case SDLK_TAB:
+ if(e->maxy != 1) break;
+ case SDLK_KP_ENTER:
+ if(isdown) fieldmode = FIELDCOMMIT; //signal field commit (handled when drawing field)
+ return true;
+ }
+ if(isdown) e->key(code);
+ return true;
+}
+
+void g3d_cursorpos(float &x, float &y)
+{
+ if(guis2d.length()) { x = cursorx; y = cursory; }
+ else x = y = 0.5f;
+}
+
+void g3d_resetcursor()
+{
+ cursorx = cursory = 0.5f;
+}
+
+FVARP(guisens, 1e-3f, 1, 1e3f);
+
+bool g3d_movecursor(int dx, int dy)
+{
+ if(!guis2d.length() || !hascursor) return false;
+ const float CURSORSCALE = 500.0f;
+ cursorx = max(0.0f, min(1.0f, cursorx+guisens*dx*(screenh/(screenw*CURSORSCALE))));
+ cursory = max(0.0f, min(1.0f, cursory+guisens*dy/CURSORSCALE));
+ return true;
+}
+
+VARNP(guifollow, useguifollow, 0, 1, 1);
+VARNP(gui2d, usegui2d, 0, 1, 1);
+
+void g3d_addgui(g3d_callback *cb, vec &origin, int flags)
+{
+ bool gui2d = flags&GUI_FORCE_2D || (flags&GUI_2D && usegui2d) || mainmenu;
+ if(!gui2d && flags&GUI_FOLLOW && useguifollow) origin.z = player->o.z-(player->eyeheight-1);
+ gui &g = (gui2d ? guis2d : guis3d).add();
+ g.cb = cb;
+ g.origin = origin;
+ g.savedorigin = &origin;
+ g.dist = flags&GUI_BOTTOM && gui2d ? 1e16f : camera1->o.dist(g.origin);
+ g.gui2d = gui2d;
+}
+
+void g3d_limitscale(float scale)
+{
+ gui::maxscale = scale;
+}
+
+static inline bool g3d_sort(const gui &a, const gui &b) { return a.dist < b.dist; }
+
+bool g3d_windowhit(bool on, bool act)
+{
+ extern int cleargui(int n);
+ if(act)
+ {
+ if(actionon || windowhit)
+ {
+ if(on) { firstx = gui::hitx; firsty = gui::hity; }
+ mousebuttons |= (actionon=on) ? G3D_DOWN : G3D_UP;
+ }
+ } else if(!on && windowhit) cleargui(1);
+ return (guis2d.length() && hascursor) || (windowhit && !windowhit->gui2d);
+}
+
+void g3d_render()
+{
+ windowhit = NULL;
+ if(actionon) mousebuttons |= G3D_PRESSED;
+
+ gui::reset();
+ guis2d.shrink(0);
+ guis3d.shrink(0);
+
+ // call all places in the engine that may want to render a gui from here, they call g3d_addgui()
+ extern void g3d_texturemenu();
+
+ if(!mainmenu) g3d_texturemenu();
+ g3d_mainmenu();
+ if(!mainmenu) game::g3d_gamemenus();
+
+ guis2d.sort(g3d_sort);
+ guis3d.sort(g3d_sort);
+
+ readyeditors();
+ fieldsactive = false;
+
+ hascursor = false;
+
+ layoutpass = true;
+ loopv(guis2d) guis2d[i].draw();
+ loopv(guis3d) guis3d[i].draw();
+ layoutpass = false;
+
+ if(guis3d.length())
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ glEnable(GL_DEPTH_TEST);
+ glDepthFunc(GL_ALWAYS);
+ glDepthMask(GL_FALSE);
+
+ loopvrev(guis3d) guis3d[i].draw();
+
+ glDepthFunc(GL_LESS);
+ glDepthMask(GL_TRUE);
+ glDisable(GL_DEPTH_TEST);
+
+ glDisable(GL_BLEND);
+ }
+}
+
+void g3d_render2d()
+{
+ if(guis2d.length())
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ loopvrev(guis2d) guis2d[i].draw();
+
+ glDisable(GL_BLEND);
+ }
+
+ flusheditors();
+ if(!fieldsactive) fieldmode = FIELDSHOW; //didn't draw any fields, so lose focus - mainly for menu closed
+ textinput(fieldmode!=FIELDSHOW, TI_GUI);
+ keyrepeat(fieldmode!=FIELDSHOW, KR_GUI);
+
+ mousebuttons = 0;
+}
+
+void consolebox(int x1, int y1, int x2, int y2)
+{
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ float bw = x2 - x1, bh = y2 - y1, aspect = bw/bh, sh = bh, sw = sh*aspect;
+ bw *= float(4*FONTH)/(SKIN_H*SKIN_SCALE);
+ bh *= float(4*FONTH)/(SKIN_H*SKIN_SCALE);
+ sw /= bw + (gui::skinx[2]-gui::skinx[1] + gui::skinx[10]-gui::skinx[9])*SKIN_SCALE;
+ sh /= bh + (gui::skiny[9]-gui::skiny[7] + gui::skiny[6]-gui::skiny[4])*SKIN_SCALE;
+ pushhudmatrix();
+ hudmatrix.translate(x1, y1, 0);
+ hudmatrix.scale(sw, sh, 1);
+ flushhudmatrix();
+ gui::drawskin(-gui::skinx[1]*SKIN_SCALE, -gui::skiny[4]*SKIN_SCALE, int(bw), int(bh), 0, 9, 1, vec(1, 1, 1), 0.60f);
+ gui::drawskin((-gui::skinx[1] + gui::skinx[2] - gui::skinx[5])*SKIN_SCALE, -gui::skiny[4]*SKIN_SCALE, int(bw), 0, 9, 1, 1, vec(1, 1, 1), 0.60f);
+ pophudmatrix();
+}
+
diff --git a/src/engine/animmodel.h b/src/engine/animmodel.h
new file mode 100644
index 0000000..16d5189
--- /dev/null
+++ b/src/engine/animmodel.h
@@ -0,0 +1,1617 @@
+VARFP(envmapmodels, 0, 1, 1, preloadmodelshaders(true));
+VARFP(bumpmodels, 0, 1, 1, preloadmodelshaders(true));
+VARP(fullbrightmodels, 0, 0, 200);
+
+struct animmodel : model
+{
+ struct animspec
+ {
+ int frame, range;
+ float speed;
+ int priority;
+ };
+
+ struct animpos
+ {
+ int anim, fr1, fr2;
+ float t;
+
+ void setframes(const animinfo &info)
+ {
+ anim = info.anim;
+ if(info.range<=1)
+ {
+ fr1 = 0;
+ t = 0;
+ }
+ else
+ {
+ int time = info.anim&ANIM_SETTIME ? info.basetime : lastmillis-info.basetime;
+ fr1 = (int)(time/info.speed); // round to full frames
+ t = (time-fr1*info.speed)/info.speed; // progress of the frame, value from 0.0f to 1.0f
+ }
+ if(info.anim&ANIM_LOOP)
+ {
+ fr1 = fr1%info.range+info.frame;
+ fr2 = fr1+1;
+ if(fr2>=info.frame+info.range) fr2 = info.frame;
+ }
+ else
+ {
+ fr1 = min(fr1, info.range-1)+info.frame;
+ fr2 = min(fr1+1, info.frame+info.range-1);
+ }
+ if(info.anim&ANIM_REVERSE)
+ {
+ fr1 = (info.frame+info.range-1)-(fr1-info.frame);
+ fr2 = (info.frame+info.range-1)-(fr2-info.frame);
+ }
+ }
+
+ bool operator==(const animpos &a) const { return fr1==a.fr1 && fr2==a.fr2 && (fr1==fr2 || t==a.t); }
+ bool operator!=(const animpos &a) const { return fr1!=a.fr1 || fr2!=a.fr2 || (fr1!=fr2 && t!=a.t); }
+ };
+
+ struct part;
+
+ struct animstate
+ {
+ part *owner;
+ animpos cur, prev;
+ float interp;
+
+ bool operator==(const animstate &a) const { return cur==a.cur && (interp<1 ? interp==a.interp && prev==a.prev : a.interp>=1); }
+ bool operator!=(const animstate &a) const { return cur!=a.cur || (interp<1 ? interp!=a.interp || prev!=a.prev : a.interp<1); }
+ };
+
+ struct linkedpart;
+ struct mesh;
+
+ struct shaderparams
+ {
+ float spec, ambient, glow, glowdelta, glowpulse, specglare, glowglare, fullbright, envmapmin, envmapmax, scrollu, scrollv, alphatest;
+
+ shaderparams() : spec(1.0f), ambient(0.3f), glow(3.0f), glowdelta(0), glowpulse(0), specglare(1), glowglare(1), fullbright(0), envmapmin(0), envmapmax(0), scrollu(0), scrollv(0), alphatest(0.9f) {}
+ };
+
+ struct shaderparamskey
+ {
+ static hashtable<shaderparams, shaderparamskey> keys;
+ static int firstversion, lastversion;
+
+ int version;
+
+ shaderparamskey() : version(-1) {}
+
+ bool checkversion()
+ {
+ if(version >= firstversion) return true;
+ version = lastversion;
+ if(++lastversion <= 0)
+ {
+ enumerate(keys, shaderparamskey, key, key.version = -1);
+ firstversion = 0;
+ lastversion = 1;
+ version = 0;
+ }
+ return false;
+ }
+
+ static inline void invalidate()
+ {
+ firstversion = lastversion;
+ }
+ };
+
+ struct skin : shaderparams
+ {
+ part *owner;
+ Texture *tex, *masks, *envmap, *normalmap;
+ Shader *shader;
+ bool alphablend, cullface;
+ shaderparamskey *key;
+
+ skin() : owner(0), tex(notexture), masks(notexture), envmap(NULL), normalmap(NULL), shader(NULL), alphablend(true), cullface(true), key(NULL) {}
+
+ bool masked() const { return masks != notexture; }
+ bool envmapped() { return envmapmax>0 && envmapmodels; }
+ bool bumpmapped() { return normalmap && bumpmodels; }
+ bool tangents() { return bumpmapped(); }
+ bool alphatested() const { return alphatest > 0 && tex->type&Texture::ALPHA; }
+
+ void setkey()
+ {
+ key = &shaderparamskey::keys[*this];
+ }
+
+ void setshaderparams(mesh *m, const animstate *as)
+ {
+ if(!Shader::lastshader) return;
+
+ float mincolor = as->cur.anim&ANIM_FULLBRIGHT ? fullbrightmodels/100.0f : 0.0f;
+ if(fullbright)
+ {
+ gle::colorf(fullbright/2, fullbright/2, fullbright/2, transparent);
+ }
+ else
+ {
+ gle::color(vec(lightcolor).max(mincolor), transparent);
+ }
+
+ if(key->checkversion() && Shader::lastshader->owner == key) return;
+ Shader::lastshader->owner = key;
+
+ if(alphatested()) LOCALPARAMF(alphatest, alphatest);
+
+ if(fullbright)
+ {
+ LOCALPARAMF(lightscale, 0, 0, 2);
+ }
+ else
+ {
+ float bias = max(mincolor-1.0f, 0.2f), scale = 0.5f*max(0.8f-bias, 0.0f),
+ minshade = scale*max(ambient, mincolor);
+ LOCALPARAMF(lightscale, scale - minshade, scale, minshade + bias);
+ }
+ float curglow = glow;
+ if(glowpulse > 0)
+ {
+ float curpulse = lastmillis*glowpulse;
+ curpulse -= floor(curpulse);
+ curglow += glowdelta*2*fabs(curpulse - 0.5f);
+ }
+ LOCALPARAMF(maskscale, 0.5f*spec, 0.5f*curglow, 16*specglare, 4*glowglare);
+ LOCALPARAMF(texscroll, scrollu*lastmillis/1000.0f, scrollv*lastmillis/1000.0f);
+ if(envmapped()) LOCALPARAMF(envmapscale, envmapmin-envmapmax, envmapmax);
+ }
+
+ Shader *loadshader()
+ {
+ #define DOMODELSHADER(name, body) \
+ do { \
+ static Shader *name##shader = NULL; \
+ if(!name##shader) name##shader = useshaderbyname(#name); \
+ body; \
+ } while(0)
+ #define LOADMODELSHADER(name) DOMODELSHADER(name, return name##shader)
+ #define SETMODELSHADER(m, name) DOMODELSHADER(name, (m)->setshader(name##shader))
+ if(shader) return shader;
+
+ string opts;
+ int optslen = 0;
+ if(alphatested()) opts[optslen++] = 'a';
+ if(owner->tangents()) opts[optslen++] = 'q';
+ if(bumpmapped()) opts[optslen++] = 'n';
+ if(envmapped()) opts[optslen++] = 'e';
+ if(masked()) opts[optslen++] = 'm';
+ if(!fullbright && (masked() || spec>=0.01f)) opts[optslen++] = 's';
+ opts[optslen++] = '\0';
+
+ defformatstring(name, "model%s", opts);
+ shader = generateshader(name, "modelshader \"%s\"", opts);
+ return shader;
+ }
+
+ void cleanup()
+ {
+ if(shader && shader->standard) shader = NULL;
+ }
+
+ void preloadBIH()
+ {
+ if(tex->type&Texture::ALPHA && !tex->alphamask) loadalphamask(tex);
+ }
+
+ void preloadshader(bool force)
+ {
+ if(force) cleanup();
+ loadshader();
+ }
+
+ void setshader(mesh *m, const animstate *as)
+ {
+ m->setshader(loadshader());
+ }
+
+ void bind(mesh *b, const animstate *as)
+ {
+ if(!cullface && enablecullface) { glDisable(GL_CULL_FACE); enablecullface = false; }
+ else if(cullface && !enablecullface) { glEnable(GL_CULL_FACE); enablecullface = true; }
+
+ if(as->cur.anim&ANIM_NOSKIN)
+ {
+ if(enablealphablend) { glDisable(GL_BLEND); enablealphablend = false; }
+ if(shadowmapping) SETMODELSHADER(b, shadowmapcaster);
+ else /*if(as->cur.anim&ANIM_SHADOW)*/ SETMODELSHADER(b, notexturemodel);
+ return;
+ }
+ setshader(b, as);
+ setshaderparams(b, as);
+ int activetmu = 0;
+ if(tex!=lasttex)
+ {
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+ lasttex = tex;
+ }
+ if(bumpmapped() && normalmap !=lastnormalmap)
+ {
+ glActiveTexture_(GL_TEXTURE3);
+ activetmu = 3;
+ glBindTexture(GL_TEXTURE_2D, normalmap->id);
+ lastnormalmap = normalmap;
+ }
+ if(tex->type&Texture::ALPHA)
+ {
+ if(alphablend)
+ {
+ if(!enablealphablend && !reflecting && !refracting)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ enablealphablend = true;
+ }
+ }
+ else if(enablealphablend) { glDisable(GL_BLEND); enablealphablend = false; }
+ }
+ else if(enablealphablend && transparent>=1) { glDisable(GL_BLEND); enablealphablend = false; }
+ if(masked() && masks!=lastmasks)
+ {
+ glActiveTexture_(GL_TEXTURE1);
+ activetmu = 1;
+ glBindTexture(GL_TEXTURE_2D, masks->id);
+ lastmasks = masks;
+ }
+ if(envmapped())
+ {
+ GLuint emtex = envmap ? envmap->id : closestenvmaptex;
+ if(lastenvmaptex!=emtex)
+ {
+ glActiveTexture_(GL_TEXTURE2);
+ activetmu = 2;
+ glBindTexture(GL_TEXTURE_CUBE_MAP, emtex);
+ lastenvmaptex = emtex;
+ }
+ }
+ if(activetmu != 0) glActiveTexture_(GL_TEXTURE0);
+ }
+ };
+
+ struct meshgroup;
+
+ struct mesh
+ {
+ meshgroup *group;
+ char *name;
+ bool noclip;
+
+ mesh() : group(NULL), name(NULL), noclip(false)
+ {
+ }
+
+ virtual ~mesh()
+ {
+ DELETEA(name);
+ }
+
+ virtual void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m) {}
+
+ virtual void genBIH(BIH::mesh &m) {}
+ void genBIH(skin &s, vector<BIH::mesh> &bih, const matrix4x3 &t)
+ {
+ BIH::mesh &m = bih.add();
+ m.xform = t;
+ m.tex = s.tex;
+ if(s.tex->type&Texture::ALPHA) m.flags |= BIH::MESH_ALPHA;
+ if(noclip) m.flags |= BIH::MESH_NOCLIP;
+ if(s.cullface) m.flags |= BIH::MESH_CULLFACE;
+ genBIH(m);
+ while(bih.last().numtris > BIH::mesh::MAXTRIS)
+ {
+ BIH::mesh &overflow = bih.dup();
+ overflow.tris += BIH::mesh::MAXTRIS;
+ overflow.numtris -= BIH::mesh::MAXTRIS;
+ bih[bih.length()-2].numtris = BIH::mesh::MAXTRIS;
+ }
+ }
+
+ virtual void setshader(Shader *s)
+ {
+ if(glaring) s->setvariant(0, 1);
+ else s->set();
+ }
+
+ template<class V, class T> void smoothnorms(V *verts, int numverts, T *tris, int numtris, float limit, bool areaweight)
+ {
+ hashtable<vec, int> share;
+ int *next = new int[numverts];
+ memset(next, -1, numverts*sizeof(int));
+ loopi(numverts)
+ {
+ V &v = verts[i];
+ v.norm = vec(0, 0, 0);
+ int idx = share.access(v.pos, i);
+ if(idx != i) { next[i] = next[idx]; next[idx] = i; }
+ }
+ loopi(numtris)
+ {
+ T &t = tris[i];
+ V &v1 = verts[t.vert[0]], &v2 = verts[t.vert[1]], &v3 = verts[t.vert[2]];
+ vec norm;
+ norm.cross(vec(v2.pos).sub(v1.pos), vec(v3.pos).sub(v1.pos));
+ if(!areaweight) norm.normalize();
+ v1.norm.add(norm);
+ v2.norm.add(norm);
+ v3.norm.add(norm);
+ }
+ vec *norms = new vec[numverts];
+ memclear(norms, numverts);
+ loopi(numverts)
+ {
+ V &v = verts[i];
+ norms[i].add(v.norm);
+ if(next[i] >= 0)
+ {
+ float vlimit = limit*v.norm.magnitude();
+ for(int j = next[i]; j >= 0; j = next[j])
+ {
+ V &o = verts[j];
+ if(v.norm.dot(o.norm) >= vlimit*o.norm.magnitude())
+ {
+ norms[i].add(o.norm);
+ norms[j].add(v.norm);
+ }
+ }
+ }
+ }
+ loopi(numverts) verts[i].norm = norms[i].normalize();
+ delete[] next;
+ delete[] norms;
+ }
+
+ template<class V, class T> void buildnorms(V *verts, int numverts, T *tris, int numtris, bool areaweight)
+ {
+ loopi(numverts) verts[i].norm = vec(0, 0, 0);
+ loopi(numtris)
+ {
+ T &t = tris[i];
+ V &v1 = verts[t.vert[0]], &v2 = verts[t.vert[1]], &v3 = verts[t.vert[2]];
+ vec norm;
+ norm.cross(vec(v2.pos).sub(v1.pos), vec(v3.pos).sub(v1.pos));
+ if(!areaweight) norm.normalize();
+ v1.norm.add(norm);
+ v2.norm.add(norm);
+ v3.norm.add(norm);
+ }
+ loopi(numverts) verts[i].norm.normalize();
+ }
+
+ template<class V, class T> void buildnorms(V *verts, int numverts, T *tris, int numtris, bool areaweight, int numframes)
+ {
+ if(!numverts) return;
+ loopi(numframes) buildnorms(&verts[i*numverts], numverts, tris, numtris, areaweight);
+ }
+
+ static inline void fixqtangent(quat &q, float bt)
+ {
+ static const float bias = -1.5f/65535, biasscale = sqrtf(1 - bias*bias);
+ if(bt < 0)
+ {
+ if(q.w >= 0) q.neg();
+ if(q.w > bias) { q.mul3(biasscale); q.w = bias; }
+ }
+ else if(q.w < 0) q.neg();
+ }
+
+ template<class V> static inline void calctangent(V &v, const vec &n, const vec &t, float bt)
+ {
+ matrix3 m;
+ m.c = n;
+ m.a = t;
+ m.b.cross(m.c, m.a);
+ quat q(m);
+ fixqtangent(q, bt);
+ v.tangent = q;
+ }
+
+ template<class B, class V, class TC, class T> void calctangents(B *bumpverts, V *verts, TC *tcverts, int numverts, T *tris, int numtris, bool areaweight)
+ {
+ vec *tangent = new vec[2*numverts], *bitangent = tangent+numverts;
+ memclear(tangent, 2*numverts);
+ loopi(numtris)
+ {
+ const T &t = tris[i];
+ const vec &e0 = verts[t.vert[0]].pos;
+ vec e1 = vec(verts[t.vert[1]].pos).sub(e0), e2 = vec(verts[t.vert[2]].pos).sub(e0);
+
+ const vec2 &tc0 = tcverts[t.vert[0]].tc,
+ &tc1 = tcverts[t.vert[1]].tc,
+ &tc2 = tcverts[t.vert[2]].tc;
+ float u1 = tc1.x - tc0.x, v1 = tc1.y - tc0.y,
+ u2 = tc2.x - tc0.x, v2 = tc2.y - tc0.y;
+ vec u(e2), v(e2);
+ u.mul(v1).sub(vec(e1).mul(v2));
+ v.mul(u1).sub(vec(e1).mul(u2));
+
+ if(vec().cross(e2, e1).dot(vec().cross(v, u)) >= 0)
+ {
+ u.neg();
+ v.neg();
+ }
+
+ if(!areaweight)
+ {
+ u.normalize();
+ v.normalize();
+ }
+
+ loopj(3)
+ {
+ tangent[t.vert[j]].sub(u);
+ bitangent[t.vert[j]].add(v);
+ }
+ }
+ loopi(numverts)
+ {
+ const vec &n = verts[i].norm,
+ &t = tangent[i],
+ &bt = bitangent[i];
+ B &bv = bumpverts[i];
+ matrix3 m;
+ m.c = n;
+ (m.a = t).project(m.c).normalize();
+ m.b.cross(m.c, m.a);
+ quat q(m);
+ fixqtangent(q, m.b.dot(bt));
+ bv.tangent = q;
+ }
+ delete[] tangent;
+ }
+
+ template<class B, class V, class TC, class T> void calctangents(B *bumpverts, V *verts, TC *tcverts, int numverts, T *tris, int numtris, bool areaweight, int numframes)
+ {
+ loopi(numframes) calctangents(&bumpverts[i*numverts], &verts[i*numverts], tcverts, numverts, tris, numtris, areaweight);
+ }
+ };
+
+ struct meshgroup
+ {
+ meshgroup *next;
+ int shared;
+ char *name;
+ vector<mesh *> meshes;
+
+ meshgroup() : next(NULL), shared(0), name(NULL)
+ {
+ }
+
+ virtual ~meshgroup()
+ {
+ DELETEA(name);
+ meshes.deletecontents();
+ DELETEP(next);
+ }
+
+ virtual int findtag(const char *name) { return -1; }
+ virtual void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n) {}
+
+ void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
+ {
+ loopv(meshes) meshes[i]->calcbb(bbmin, bbmax, m);
+ }
+
+ void genBIH(vector<skin> &skins, vector<BIH::mesh> &bih, const matrix4x3 &t)
+ {
+ loopv(meshes) meshes[i]->genBIH(skins[i], bih, t);
+ }
+
+ virtual void *animkey() { return this; }
+ virtual int totalframes() const { return 1; }
+ bool hasframe(int i) const { return i>=0 && i<totalframes(); }
+ bool hasframes(int i, int n) const { return i>=0 && i+n<=totalframes(); }
+ int clipframes(int i, int n) const { return min(n, totalframes() - i); }
+
+ virtual void cleanup() {}
+ virtual void preload(part *p) {}
+ virtual void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p) {}
+
+ void bindpos(GLuint ebuf, GLuint vbuf, void *v, int stride)
+ {
+ if(lastebuf!=ebuf)
+ {
+ gle::bindebo(ebuf);
+ lastebuf = ebuf;
+ }
+ if(lastvbuf!=vbuf)
+ {
+ gle::bindvbo(vbuf);
+ if(!lastvbuf) gle::enablevertex();
+ gle::vertexpointer(stride, v);
+ lastvbuf = vbuf;
+ }
+ }
+
+ void bindtc(void *v, int stride)
+ {
+ if(!enabletc)
+ {
+ gle::enabletexcoord0();
+ enabletc = true;
+ }
+ if(lasttcbuf!=lastvbuf)
+ {
+ gle::texcoord0pointer(stride, v);
+ lasttcbuf = lastvbuf;
+ }
+ }
+
+ void bindnormals(void *v, int stride)
+ {
+ if(!enablenormals)
+ {
+ gle::enablenormal();
+ enablenormals = true;
+ }
+ if(lastnbuf!=lastvbuf)
+ {
+ gle::normalpointer(stride, v);
+ lastnbuf = lastvbuf;
+ }
+ }
+
+ void bindtangents(void *v, int stride)
+ {
+ if(!enabletangents)
+ {
+ gle::enabletangent();
+ enabletangents = true;
+ }
+ if(lastxbuf!=lastvbuf)
+ {
+ gle::tangentpointer(stride, v, GL_SHORT);
+ lastxbuf = lastvbuf;
+ }
+ }
+
+ void bindbones(void *wv, void *bv, int stride)
+ {
+ if(!enablebones)
+ {
+ gle::enableboneweight();
+ gle::enableboneindex();
+ enablebones = true;
+ }
+ if(lastbbuf!=lastvbuf)
+ {
+ gle::boneweightpointer(stride, wv);
+ gle::boneindexpointer(stride, bv);
+ lastbbuf = lastvbuf;
+ }
+ }
+ };
+
+ virtual meshgroup *loadmeshes(const char *name, va_list args) { return NULL; }
+
+ meshgroup *sharemeshes(const char *name, ...)
+ {
+ static hashnameset<meshgroup *> meshgroups;
+ if(!meshgroups.access(name))
+ {
+ va_list args;
+ va_start(args, name);
+ meshgroup *group = loadmeshes(name, args);
+ va_end(args);
+ if(!group) return NULL;
+ meshgroups.add(group);
+ }
+ return meshgroups[name];
+ }
+
+ struct linkedpart
+ {
+ part *p;
+ int tag, anim, basetime;
+ vec translate;
+ vec *pos;
+ matrix4 matrix;
+
+ linkedpart() : p(NULL), tag(-1), anim(-1), basetime(0), translate(0, 0, 0), pos(NULL) {}
+ };
+
+ struct part
+ {
+ animmodel *model;
+ int index;
+ meshgroup *meshes;
+ vector<linkedpart> links;
+ vector<skin> skins;
+ vector<animspec> *anims[MAXANIMPARTS];
+ int numanimparts;
+ float pitchscale, pitchoffset, pitchmin, pitchmax;
+ vec translate;
+
+ part(animmodel *model, int index = 0) : model(model), index(index), meshes(NULL), numanimparts(1), pitchscale(1), pitchoffset(0), pitchmin(0), pitchmax(0), translate(0, 0, 0)
+ {
+ loopk(MAXANIMPARTS) anims[k] = NULL;
+ }
+ virtual ~part()
+ {
+ loopk(MAXANIMPARTS) DELETEA(anims[k]);
+ }
+
+ virtual void cleanup()
+ {
+ if(meshes) meshes->cleanup();
+ loopv(skins) skins[i].cleanup();
+ }
+
+ void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
+ {
+ matrix4x3 t = m;
+ t.scale(model->scale);
+ t.translate(translate);
+ meshes->calcbb(bbmin, bbmax, t);
+ loopv(links)
+ {
+ matrix4x3 n;
+ meshes->concattagtransform(this, links[i].tag, m, n);
+ n.translate(links[i].translate, model->scale);
+ links[i].p->calcbb(bbmin, bbmax, n);
+ }
+ }
+
+ void genBIH(vector<BIH::mesh> &bih, const matrix4x3 &m)
+ {
+ matrix4x3 t = m;
+ t.scale(model->scale);
+ t.translate(translate);
+ meshes->genBIH(skins, bih, t);
+ loopv(links)
+ {
+ matrix4x3 n;
+ meshes->concattagtransform(this, links[i].tag, m, n);
+ n.translate(links[i].translate, model->scale);
+ links[i].p->genBIH(bih, n);
+ }
+ }
+
+ bool link(part *p, const char *tag, const vec &translate = vec(0, 0, 0), int anim = -1, int basetime = 0, vec *pos = NULL)
+ {
+ int i = meshes ? meshes->findtag(tag) : -1;
+ if(i<0)
+ {
+ loopv(links) if(links[i].p && links[i].p->link(p, tag, translate, anim, basetime, pos)) return true;
+ return false;
+ }
+ linkedpart &l = links.add();
+ l.p = p;
+ l.tag = i;
+ l.anim = anim;
+ l.basetime = basetime;
+ l.translate = translate;
+ l.pos = pos;
+ return true;
+ }
+
+ bool unlink(part *p)
+ {
+ loopvrev(links) if(links[i].p==p) { links.remove(i, 1); return true; }
+ loopv(links) if(links[i].p && links[i].p->unlink(p)) return true;
+ return false;
+ }
+
+ void initskins(Texture *tex = notexture, Texture *masks = notexture, int limit = 0)
+ {
+ if(!limit)
+ {
+ if(!meshes) return;
+ limit = meshes->meshes.length();
+ }
+ while(skins.length() < limit)
+ {
+ skin &s = skins.add();
+ s.owner = this;
+ s.tex = tex;
+ s.masks = masks;
+ }
+ }
+
+ bool envmapped()
+ {
+ loopv(skins) if(skins[i].envmapped()) return true;
+ return false;
+ }
+
+ bool tangents()
+ {
+ loopv(skins) if(skins[i].tangents()) return true;
+ return false;
+ }
+
+ void preloadBIH()
+ {
+ loopv(skins) skins[i].preloadBIH();
+ }
+
+ void preloadshaders(bool force)
+ {
+ loopv(skins) skins[i].preloadshader(force);
+ }
+
+ void preloadmeshes()
+ {
+ if(meshes) meshes->preload(this);
+ }
+
+ virtual void getdefaultanim(animinfo &info, int anim, uint varseed, dynent *d)
+ {
+ info.frame = 0;
+ info.range = 1;
+ }
+
+ bool calcanim(int animpart, int anim, int basetime, int basetime2, dynent *d, int interp, animinfo &info, int &aitime)
+ {
+ uint varseed = uint((size_t)d);
+ info.anim = anim;
+ info.basetime = basetime;
+ info.varseed = varseed;
+ info.speed = anim&ANIM_SETSPEED ? basetime2 : 100.0f;
+ if((anim&ANIM_INDEX)==ANIM_ALL)
+ {
+ info.frame = 0;
+ info.range = meshes->totalframes();
+ }
+ else
+ {
+ animspec *spec = NULL;
+ if(anims[animpart])
+ {
+ int primaryidx = anim&ANIM_INDEX;
+ if(primaryidx < NUMANIMS)
+ {
+ vector<animspec> &primary = anims[animpart][primaryidx];
+ if(primary.length()) spec = &primary[uint(varseed + basetime)%primary.length()];
+ }
+ if((anim>>ANIM_SECONDARY)&(ANIM_INDEX|ANIM_DIR))
+ {
+ int secondaryidx = (anim>>ANIM_SECONDARY)&ANIM_INDEX;
+ if(secondaryidx < NUMANIMS)
+ {
+ vector<animspec> &secondary = anims[animpart][secondaryidx];
+ if(secondary.length())
+ {
+ animspec &spec2 = secondary[uint(varseed + basetime2)%secondary.length()];
+ if(!spec || spec2.priority > spec->priority)
+ {
+ spec = &spec2;
+ info.anim >>= ANIM_SECONDARY;
+ info.basetime = basetime2;
+ }
+ }
+ }
+ }
+ }
+ if(spec)
+ {
+ info.frame = spec->frame;
+ info.range = spec->range;
+ if(spec->speed>0) info.speed = 1000.0f/spec->speed;
+ }
+ else getdefaultanim(info, anim, uint(varseed + info.basetime), d);
+ }
+
+ info.anim &= (1<<ANIM_SECONDARY)-1;
+ info.anim |= anim&ANIM_FLAGS;
+ if((info.anim&ANIM_CLAMP) != ANIM_CLAMP)
+ {
+ if(info.anim&(ANIM_LOOP|ANIM_START|ANIM_END))
+ {
+ info.anim &= ~ANIM_SETTIME;
+ if(!info.basetime) info.basetime = -((int)(size_t)d&0xFFF);
+ }
+ if(info.anim&(ANIM_START|ANIM_END))
+ {
+ if(info.anim&ANIM_END) info.frame += info.range-1;
+ info.range = 1;
+ }
+ }
+
+ if(!meshes->hasframes(info.frame, info.range))
+ {
+ if(!meshes->hasframe(info.frame)) return false;
+ info.range = meshes->clipframes(info.frame, info.range);
+ }
+
+ if(d && interp>=0)
+ {
+ animinterpinfo &ai = d->animinterp[interp];
+ if((info.anim&ANIM_CLAMP)==ANIM_CLAMP) aitime = min(aitime, int(info.range*info.speed*0.5e-3f));
+ void *ak = meshes->animkey();
+ if(d->ragdoll && !(anim&ANIM_RAGDOLL))
+ {
+ ai.prev.range = ai.cur.range = 0;
+ ai.lastswitch = -1;
+ }
+ else if(ai.lastmodel!=ak || ai.lastswitch<0 || lastmillis-d->lastrendered>aitime)
+ {
+ ai.prev = ai.cur = info;
+ ai.lastswitch = lastmillis-aitime*2;
+ }
+ else if(ai.cur!=info)
+ {
+ if(lastmillis-ai.lastswitch>aitime/2) ai.prev = ai.cur;
+ ai.cur = info;
+ ai.lastswitch = lastmillis;
+ }
+ else if(info.anim&ANIM_SETTIME) ai.cur.basetime = info.basetime;
+ ai.lastmodel = ak;
+ }
+ return true;
+ }
+
+ void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d)
+ {
+ animstate as[MAXANIMPARTS];
+ render(anim, basetime, basetime2, pitch, axis, forward, d, as);
+ }
+
+ void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d, animstate *as)
+ {
+ if(!(anim&ANIM_REUSE)) loopi(numanimparts)
+ {
+ animinfo info;
+ int interp = d && index+numanimparts<=MAXANIMPARTS ? index+i : -1, aitime = animationinterpolationtime;
+ if(!calcanim(i, anim, basetime, basetime2, d, interp, info, aitime)) return;
+ animstate &p = as[i];
+ p.owner = this;
+ p.cur.setframes(info);
+ p.interp = 1;
+ if(interp>=0 && d->animinterp[interp].prev.range>0)
+ {
+ int diff = lastmillis-d->animinterp[interp].lastswitch;
+ if(diff<aitime)
+ {
+ p.prev.setframes(d->animinterp[interp].prev);
+ p.interp = diff/float(aitime);
+ }
+ }
+ }
+
+ vec oaxis, oforward;
+ matrixstack[matrixpos].transposedtransformnormal(axis, oaxis);
+ float pitchamount = pitchscale*pitch + pitchoffset;
+ if((pitchmin || pitchmax) && pitchmin <= pitchmax) pitchamount = clamp(pitchamount, pitchmin, pitchmax);
+ if(as->cur.anim&ANIM_NOPITCH || (as->interp < 1 && as->prev.anim&ANIM_NOPITCH))
+ pitchamount *= (as->cur.anim&ANIM_NOPITCH ? 0 : as->interp) + (as->interp < 1 && as->prev.anim&ANIM_NOPITCH ? 0 : 1-as->interp);
+ if(pitchamount)
+ {
+ ++matrixpos;
+ matrixstack[matrixpos] = matrixstack[matrixpos-1];
+ matrixstack[matrixpos].rotate(pitchamount*RAD, oaxis);
+ }
+ matrixstack[matrixpos].transposedtransformnormal(forward, oforward);
+
+ if(!(anim&ANIM_NORENDER))
+ {
+ matrix4 modelmatrix;
+ modelmatrix.mul(shadowmapping ? shadowmatrix : camprojmatrix, matrixstack[matrixpos]);
+ if(model->scale!=1) modelmatrix.scale(model->scale);
+ if(!translate.iszero()) modelmatrix.translate(translate);
+ GLOBALPARAM(modelmatrix, modelmatrix);
+
+ if(!(anim&ANIM_NOSKIN))
+ {
+ if(envmapped()) GLOBALPARAM(modelworld, matrix3(matrixstack[matrixpos]));
+
+ vec odir, ocampos;
+ matrixstack[matrixpos].transposedtransformnormal(lightdir, odir);
+ GLOBALPARAM(lightdir, odir);
+ matrixstack[matrixpos].transposedtransform(camera1->o, ocampos);
+ ocampos.div(model->scale).sub(translate);
+ GLOBALPARAM(modelcamera, ocampos);
+ }
+ }
+
+ meshes->render(as, pitch, oaxis, oforward, d, this);
+
+ if(!(anim&ANIM_REUSE))
+ {
+ loopv(links)
+ {
+ linkedpart &link = links[i];
+ link.matrix.translate(links[i].translate, model->scale);
+
+ matrixpos++;
+ matrixstack[matrixpos].mul(matrixstack[matrixpos-1], link.matrix);
+
+ if(link.pos) *link.pos = matrixstack[matrixpos].gettranslation();
+
+ if(!link.p)
+ {
+ matrixpos--;
+ continue;
+ }
+
+ int nanim = anim, nbasetime = basetime, nbasetime2 = basetime2;
+ if(link.anim>=0)
+ {
+ nanim = link.anim | (anim&ANIM_FLAGS);
+ nbasetime = link.basetime;
+ nbasetime2 = 0;
+ }
+ link.p->render(nanim, nbasetime, nbasetime2, pitch, axis, forward, d);
+
+ matrixpos--;
+ }
+ }
+
+ if(pitchamount) matrixpos--;
+ }
+
+ void setanim(int animpart, int num, int frame, int range, float speed, int priority = 0)
+ {
+ if(animpart<0 || animpart>=MAXANIMPARTS) return;
+ if(frame<0 || range<=0 || !meshes || !meshes->hasframes(frame, range))
+ {
+ conoutf(CON_ERROR, "invalid frame %d, range %d in model %s", frame, range, model->name);
+ return;
+ }
+ if(!anims[animpart]) anims[animpart] = new vector<animspec>[NUMANIMS];
+ animspec &spec = anims[animpart][num].add();
+ spec.frame = frame;
+ spec.range = range;
+ spec.speed = speed;
+ spec.priority = priority;
+ }
+
+ virtual void loaded()
+ {
+ meshes->shared++;
+ loopv(skins) skins[i].setkey();
+ }
+ };
+
+ enum
+ {
+ LINK_TAG = 0,
+ LINK_COOP,
+ LINK_REUSE
+ };
+
+ virtual int linktype(animmodel *m) const { return LINK_TAG; }
+
+ void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d, modelattach *a)
+ {
+ int numtags = 0;
+ if(a)
+ {
+ int index = parts.last()->index + parts.last()->numanimparts;
+ for(int i = 0; a[i].tag; i++)
+ {
+ numtags++;
+
+ animmodel *m = (animmodel *)a[i].m;
+ if(!m)
+ {
+ if(a[i].pos) link(NULL, a[i].tag, vec(0, 0, 0), 0, 0, a[i].pos);
+ continue;
+ }
+ part *p = m->parts[0];
+ switch(linktype(m))
+ {
+ case LINK_TAG:
+ p->index = link(p, a[i].tag, vec(0, 0, 0), a[i].anim, a[i].basetime, a[i].pos) ? index : -1;
+ break;
+
+ case LINK_COOP:
+ p->index = index;
+ break;
+
+ default:
+ continue;
+ }
+ index += p->numanimparts;
+ }
+ }
+
+ animstate as[MAXANIMPARTS];
+ parts[0]->render(anim, basetime, basetime2, pitch, axis, forward, d, as);
+
+ if(a) for(int i = numtags-1; i >= 0; i--)
+ {
+ animmodel *m = (animmodel *)a[i].m;
+ if(!m)
+ {
+ if(a[i].pos) unlink(NULL);
+ continue;
+ }
+ part *p = m->parts[0];
+ switch(linktype(m))
+ {
+ case LINK_TAG:
+ if(p->index >= 0) unlink(p);
+ p->index = 0;
+ break;
+
+ case LINK_COOP:
+ p->render(anim, basetime, basetime2, pitch, axis, forward, d);
+ p->index = 0;
+ break;
+
+ case LINK_REUSE:
+ p->render(anim | ANIM_REUSE, basetime, basetime2, pitch, axis, forward, d, as);
+ break;
+ }
+ }
+ }
+
+ void render(int anim, int basetime, int basetime2, const vec &o, float yaw, float pitch, dynent *d, modelattach *a, const vec &color, const vec &dir, float trans)
+ {
+ yaw += spinyaw*lastmillis/1000.0f;
+ pitch += offsetpitch + spinpitch*lastmillis/1000.0f;
+
+ vec axis(0, -1, 0), forward(1, 0, 0);
+
+ matrixpos = 0;
+ matrixstack[0].identity();
+ if(!d || !d->ragdoll || anim&ANIM_RAGDOLL)
+ {
+ matrixstack[0].settranslation(o);
+ matrixstack[0].rotate_around_z(yaw*RAD);
+ matrixstack[0].transformnormal(vec(axis), axis);
+ matrixstack[0].transformnormal(vec(forward), forward);
+ if(offsetyaw) matrixstack[0].rotate_around_z(offsetyaw*RAD);
+ }
+ else pitch = 0;
+
+ if(anim&ANIM_NORENDER)
+ {
+ render(anim, basetime, basetime2, pitch, axis, forward, d, a);
+ if(d) d->lastrendered = lastmillis;
+ return;
+ }
+
+ if(!(anim&ANIM_NOSKIN))
+ {
+ transparent = trans;
+ lightdir = dir;
+ lightcolor = color;
+
+ if(envmapped())
+ {
+ setupenvmap:
+ closestenvmaptex = lookupenvmap(closestenvmap(o));
+ GLOBALPARAM(lightdirworld, dir);
+ }
+ else if(a) for(int i = 0; a[i].tag; i++) if(a[i].m && a[i].m->envmapped()) goto setupenvmap;
+ }
+
+ if(depthoffset && !enabledepthoffset)
+ {
+ enablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+ enabledepthoffset = true;
+ }
+
+ if(transparent<1)
+ {
+ if(anim&ANIM_GHOST)
+ {
+ glDepthFunc(GL_GREATER);
+ glDepthMask(GL_FALSE);
+ }
+ else if(alphadepth)
+ {
+ glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+ render(anim|ANIM_NOSKIN, basetime, basetime2, pitch, axis, forward, d, a);
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, fading ? GL_FALSE : GL_TRUE);
+
+ glDepthFunc(GL_LEQUAL);
+ }
+
+ if(!enablealphablend)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ enablealphablend = true;
+ }
+ }
+
+ render(anim, basetime, basetime2, pitch, axis, forward, d, a);
+
+ if(transparent<1 && (alphadepth || anim&ANIM_GHOST))
+ {
+ glDepthFunc(GL_LESS);
+ if(anim&ANIM_GHOST) glDepthMask(GL_TRUE);
+ }
+
+ if(d) d->lastrendered = lastmillis;
+ }
+
+ vector<part *> parts;
+
+ animmodel(const char *name) : model(name)
+ {
+ }
+
+ ~animmodel()
+ {
+ parts.deletecontents();
+ }
+
+ void cleanup()
+ {
+ loopv(parts) parts[i]->cleanup();
+ }
+
+ virtual void flushpart() {}
+
+ part &addpart()
+ {
+ flushpart();
+ part *p = new part(this, parts.length());
+ parts.add(p);
+ return *p;
+ }
+
+ void initmatrix(matrix4x3 &m)
+ {
+ m.identity();
+ if(offsetyaw) m.rotate_around_z(offsetyaw*RAD);
+ if(offsetpitch) m.rotate_around_y(-offsetpitch*RAD);
+ }
+
+ void genBIH(vector<BIH::mesh> &bih)
+ {
+ if(parts.empty()) return;
+ matrix4x3 m;
+ initmatrix(m);
+ parts[0]->genBIH(bih, m);
+ }
+
+ void preloadBIH()
+ {
+ model::preloadBIH();
+ if(bih) loopv(parts) parts[i]->preloadBIH();
+ }
+
+ BIH *setBIH()
+ {
+ if(bih) return bih;
+ vector<BIH::mesh> meshes;
+ genBIH(meshes);
+ bih = new BIH(meshes);
+ return bih;
+ }
+
+ bool link(part *p, const char *tag, const vec &translate = vec(0, 0, 0), int anim = -1, int basetime = 0, vec *pos = NULL)
+ {
+ if(parts.empty()) return false;
+ return parts[0]->link(p, tag, translate, anim, basetime, pos);
+ }
+
+ bool unlink(part *p)
+ {
+ if(parts.empty()) return false;
+ return parts[0]->unlink(p);
+ }
+
+ bool envmapped()
+ {
+ loopv(parts) if(parts[i]->envmapped()) return true;
+ return false;
+ }
+
+ virtual bool flipy() const { return false; }
+ virtual bool loadconfig() { return false; }
+ virtual bool loaddefaultparts() { return false; }
+ virtual void startload() {}
+ virtual void endload() {}
+
+ bool load()
+ {
+ startload();
+ bool success = loadconfig() && parts.length(); // configured model, will call the model commands below
+ if(!success)
+ success = loaddefaultparts(); // model without configuration, try default tris and skin
+ flushpart();
+ endload();
+ if(flipy()) translate.y = -translate.y;
+
+ if(!success) return false;
+ loopv(parts) if(!parts[i]->meshes) return false;
+
+ loaded();
+ return true;
+ }
+
+ void preloadshaders(bool force)
+ {
+ loopv(parts) parts[i]->preloadshaders(force);
+ }
+
+ void preloadmeshes()
+ {
+ loopv(parts) parts[i]->preloadmeshes();
+ }
+
+ void setshader(Shader *shader)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].shader = shader;
+ }
+
+ void setenvmap(float envmapmin, float envmapmax, Texture *envmap)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins)
+ {
+ skin &s = parts[i]->skins[j];
+ if(envmapmax)
+ {
+ s.envmapmin = envmapmin;
+ s.envmapmax = envmapmax;
+ }
+ if(envmap) s.envmap = envmap;
+ }
+ }
+
+ void setspec(float spec)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].spec = spec;
+ }
+
+ void setambient(float ambient)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].ambient = ambient;
+ }
+
+ void setglow(float glow, float delta, float pulse)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins)
+ {
+ skin &s = parts[i]->skins[j];
+ s.glow = glow;
+ s.glowdelta = delta;
+ s.glowpulse = pulse;
+ }
+ }
+
+ void setglare(float specglare, float glowglare)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins)
+ {
+ skin &s = parts[i]->skins[j];
+ s.specglare = specglare;
+ s.glowglare = glowglare;
+ }
+ }
+
+ void setalphatest(float alphatest)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].alphatest = alphatest;
+ }
+
+ void setalphablend(bool alphablend)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].alphablend = alphablend;
+ }
+
+ void setfullbright(float fullbright)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].fullbright = fullbright;
+ }
+
+ void setcullface(bool cullface)
+ {
+ if(parts.empty()) loaddefaultparts();
+ loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].cullface = cullface;
+ }
+
+ void calcbb(vec &center, vec &radius)
+ {
+ if(parts.empty()) return;
+ vec bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f);
+ matrix4x3 m;
+ initmatrix(m);
+ parts[0]->calcbb(bbmin, bbmax, m);
+ radius = bbmax;
+ radius.sub(bbmin);
+ radius.mul(0.5f);
+ center = bbmin;
+ center.add(radius);
+ }
+
+ virtual void loaded()
+ {
+ scale /= 4;
+ if(parts.length()) parts[0]->translate = translate;
+ loopv(parts) parts[i]->loaded();
+ }
+
+ static bool enabletc, enablealphablend, enablecullface, enablenormals, enabletangents, enablebones, enabledepthoffset;
+ static vec lightdir, lightcolor;
+ static float transparent, lastalphatest;
+ static GLuint lastvbuf, lasttcbuf, lastnbuf, lastxbuf, lastbbuf, lastebuf, lastenvmaptex, closestenvmaptex;
+ static Texture *lasttex, *lastmasks, *lastnormalmap;
+ static int matrixpos;
+ static matrix4 matrixstack[64];
+
+ void startrender()
+ {
+ enabletc = enablealphablend = enablenormals = enabletangents = enablebones = enabledepthoffset = false;
+ enablecullface = true;
+ lastalphatest = -1;
+ lastvbuf = lasttcbuf = lastxbuf = lastnbuf = lastbbuf = lastebuf = lastenvmaptex = closestenvmaptex = 0;
+ lasttex = lastmasks = lastnormalmap = NULL;
+ transparent = 1;
+ shaderparamskey::invalidate();
+ }
+
+ static void disablebones()
+ {
+ gle::disableboneweight();
+ gle::disableboneindex();
+ enablebones = false;
+ }
+
+ static void disabletangents()
+ {
+ gle::disabletangent();
+ enabletangents = false;
+ }
+
+ static void disabletc()
+ {
+ gle::disabletexcoord0();
+ enabletc = false;
+ }
+
+ static void disablenormals()
+ {
+ gle::disablenormal();
+ enablenormals = false;
+ }
+
+ static void disablevbo()
+ {
+ if(lastebuf) gle::clearebo();
+ if(lastvbuf)
+ {
+ gle::clearvbo();
+ gle::disablevertex();
+ }
+ if(enabletc) disabletc();
+ if(enablenormals) disablenormals();
+ if(enabletangents) disabletangents();
+ if(enablebones) disablebones();
+ lastvbuf = lasttcbuf = lastxbuf = lastnbuf = lastbbuf = lastebuf = 0;
+ }
+
+ void endrender()
+ {
+ if(lastvbuf || lastebuf) disablevbo();
+ if(enablealphablend) glDisable(GL_BLEND);
+ if(!enablecullface) glEnable(GL_CULL_FACE);
+ if(enabledepthoffset) disablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+ }
+};
+
+bool animmodel::enabletc = false, animmodel::enablealphablend = false,
+ animmodel::enablecullface = true,
+ animmodel::enablenormals = false, animmodel::enabletangents = false, animmodel::enablebones = false, animmodel::enabledepthoffset = false;
+vec animmodel::lightdir(0, 0, 1), animmodel::lightcolor(1, 1, 1);
+float animmodel::transparent = 1, animmodel::lastalphatest = -1;
+GLuint animmodel::lastvbuf = 0, animmodel::lasttcbuf = 0, animmodel::lastnbuf = 0, animmodel::lastxbuf = 0, animmodel::lastbbuf = 0,
+ animmodel::lastebuf = 0, animmodel::lastenvmaptex = 0, animmodel::closestenvmaptex = 0;
+Texture *animmodel::lasttex = NULL, *animmodel::lastmasks = NULL, *animmodel::lastnormalmap = NULL;
+int animmodel::matrixpos = 0;
+matrix4 animmodel::matrixstack[64];
+
+static inline uint hthash(const animmodel::shaderparams &k)
+{
+ return memhash(&k, sizeof(k));
+}
+
+static inline bool htcmp(const animmodel::shaderparams &x, const animmodel::shaderparams &y)
+{
+ return !memcmp(&x, &y, sizeof(animmodel::shaderparams));
+}
+
+hashtable<animmodel::shaderparams, animmodel::shaderparamskey> animmodel::shaderparamskey::keys;
+int animmodel::shaderparamskey::firstversion = 0, animmodel::shaderparamskey::lastversion = 1;
+
+template<class MDL, class BASE> struct modelloader : BASE
+{
+ static MDL *loading;
+ static string dir;
+
+ modelloader(const char *name) : BASE(name) {}
+
+ static bool animated() { return true; }
+ static bool multiparted() { return true; }
+ static bool multimeshed() { return true; }
+
+ void startload()
+ {
+ loading = (MDL *)this;
+ }
+
+ void endload()
+ {
+ loading = NULL;
+ }
+
+ bool loadconfig()
+ {
+ formatstring(dir, "packages/models/%s", BASE::name);
+ defformatstring(cfgname, "packages/models/%s/%s.cfg", BASE::name, MDL::formatname());
+
+ identflags &= ~IDF_PERSIST;
+ bool success = execfile(cfgname, false);
+ identflags |= IDF_PERSIST;
+ return success;
+ }
+};
+
+template<class MDL, class BASE> MDL *modelloader<MDL, BASE>::loading = NULL;
+template<class MDL, class BASE> string modelloader<MDL, BASE>::dir = {'\0'}; // crashes clang if "" is used here
+
+template<class MDL, class MESH> struct modelcommands
+{
+ typedef struct MDL::part part;
+ typedef struct MDL::skin skin;
+
+ static void setdir(char *name)
+ {
+ if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ formatstring(MDL::dir, "packages/models/%s", name);
+ }
+
+ #define loopmeshes(meshname, m, body) \
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } \
+ part &mdl = *MDL::loading->parts.last(); \
+ if(!mdl.meshes) return; \
+ loopv(mdl.meshes->meshes) \
+ { \
+ MESH &m = *(MESH *)mdl.meshes->meshes[i]; \
+ if(!strcmp(meshname, "*") || (m.name && !strcmp(m.name, meshname))) \
+ { \
+ body; \
+ } \
+ }
+
+ #define loopskins(meshname, s, body) loopmeshes(meshname, m, { skin &s = mdl.skins[i]; body; })
+
+ static void setskin(char *meshname, char *tex, char *masks, float *envmapmax, float *envmapmin)
+ {
+ loopskins(meshname, s,
+ s.tex = textureload(makerelpath(MDL::dir, tex), 0, true, false);
+ if(*masks)
+ {
+ s.masks = textureload(makerelpath(MDL::dir, masks), 0, true, false);
+ s.envmapmax = *envmapmax;
+ s.envmapmin = *envmapmin;
+ }
+ );
+ }
+
+ static void setspec(char *meshname, int *percent)
+ {
+ float spec = 1.0f;
+ if(*percent>0) spec = *percent/100.0f;
+ else if(*percent<0) spec = 0.0f;
+ loopskins(meshname, s, s.spec = spec);
+ }
+
+ static void setambient(char *meshname, int *percent)
+ {
+ float ambient = 0.3f;
+ if(*percent>0) ambient = *percent/100.0f;
+ else if(*percent<0) ambient = 0.0f;
+ loopskins(meshname, s, s.ambient = ambient);
+ }
+
+ static void setglow(char *meshname, int *percent, int *delta, float *pulse)
+ {
+ float glow = 3.0f, glowdelta = *delta/100.0f, glowpulse = *pulse > 0 ? *pulse/1000.0f : 0;
+ if(*percent>0) glow = *percent/100.0f;
+ else if(*percent<0) glow = 0.0f;
+ glowdelta -= glow;
+ loopskins(meshname, s, { s.glow = glow; s.glowdelta = glowdelta; s.glowpulse = glowpulse; });
+ }
+
+ static void setglare(char *meshname, float *specglare, float *glowglare)
+ {
+ loopskins(meshname, s, { s.specglare = *specglare; s.glowglare = *glowglare; });
+ }
+
+ static void setalphatest(char *meshname, float *cutoff)
+ {
+ loopskins(meshname, s, s.alphatest = max(0.0f, min(1.0f, *cutoff)));
+ }
+
+ static void setalphablend(char *meshname, int *blend)
+ {
+ loopskins(meshname, s, s.alphablend = *blend!=0);
+ }
+
+ static void setcullface(char *meshname, int *cullface)
+ {
+ loopskins(meshname, s, s.cullface = *cullface!=0);
+ }
+
+ static void setenvmap(char *meshname, char *envmap)
+ {
+ Texture *tex = cubemapload(envmap);
+ loopskins(meshname, s, s.envmap = tex);
+ }
+
+ static void setbumpmap(char *meshname, char *normalmapfile)
+ {
+ Texture *normalmaptex = textureload(makerelpath(MDL::dir, normalmapfile), 0, true, false);
+ loopskins(meshname, s, s.normalmap = normalmaptex);
+ }
+
+ static void setfullbright(char *meshname, float *fullbright)
+ {
+ loopskins(meshname, s, s.fullbright = *fullbright);
+ }
+
+ static void setshader(char *meshname, char *shader)
+ {
+ loopskins(meshname, s, s.shader = lookupshaderbyname(shader));
+ }
+
+ static void setscroll(char *meshname, float *scrollu, float *scrollv)
+ {
+ loopskins(meshname, s, { s.scrollu = *scrollu; s.scrollv = *scrollv; });
+ }
+
+ static void setnoclip(char *meshname, int *noclip)
+ {
+ loopmeshes(meshname, m, m.noclip = *noclip!=0);
+ }
+
+ static void setlink(int *parent, int *child, char *tagname, float *x, float *y, float *z)
+ {
+ if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ if(!MDL::loading->parts.inrange(*parent) || !MDL::loading->parts.inrange(*child)) { conoutf(CON_ERROR, "no models loaded to link"); return; }
+ if(!MDL::loading->parts[*parent]->link(MDL::loading->parts[*child], tagname, vec(*x, *y, *z))) conoutf(CON_ERROR, "could not link model %s", MDL::loading->name);
+ }
+
+ template<class F> void modelcommand(F *fun, const char *suffix, const char *args)
+ {
+ defformatstring(name, "%s%s", MDL::formatname(), suffix);
+ addcommand(newstring(name), (void (*)())fun, args);
+ }
+
+ modelcommands()
+ {
+ modelcommand(setdir, "dir", "s");
+ if(MDL::multimeshed())
+ {
+ modelcommand(setskin, "skin", "sssff");
+ modelcommand(setspec, "spec", "si");
+ modelcommand(setambient, "ambient", "si");
+ modelcommand(setglow, "glow", "siif");
+ modelcommand(setglare, "glare", "sff");
+ modelcommand(setalphatest, "alphatest", "sf");
+ modelcommand(setalphablend, "alphablend", "si");
+ modelcommand(setcullface, "cullface", "si");
+ modelcommand(setenvmap, "envmap", "ss");
+ modelcommand(setbumpmap, "bumpmap", "ss");
+ modelcommand(setfullbright, "fullbright", "sf");
+ modelcommand(setshader, "shader", "ss");
+ modelcommand(setscroll, "scroll", "sff");
+ modelcommand(setnoclip, "noclip", "si");
+ }
+ if(MDL::multiparted()) modelcommand(setlink, "link", "iisfff");
+ }
+};
+
diff --git a/src/engine/bih.cpp b/src/engine/bih.cpp
new file mode 100644
index 0000000..e735f38
--- /dev/null
+++ b/src/engine/bih.cpp
@@ -0,0 +1,330 @@
+#include "engine.h"
+
+bool BIH::triintersect(const mesh &m, int tidx, const vec &mo, const vec &mray, float maxdist, float &dist, int mode)
+{
+ const tri &t = m.tris[tidx];
+ vec a = m.getpos(t.vert[0]), b = m.getpos(t.vert[1]).sub(a), c = m.getpos(t.vert[2]).sub(a),
+ n = vec().cross(b, c), r = vec(a).sub(mo), e = vec().cross(r, mray);
+ float det = mray.dot(n), v, w, f;
+ if(det >= 0)
+ {
+ if(!(mode&RAY_SHADOW) && m.flags&MESH_CULLFACE) return false;
+ v = e.dot(c);
+ if(v < 0 || v > det) return false;
+ w = -e.dot(b);
+ if(w < 0 || v + w > det) return false;
+ f = r.dot(n)*m.scale;
+ if(f < 0 || f > maxdist*det || !det) return false;
+ }
+ else
+ {
+ v = e.dot(c);
+ if(v > 0 || v < det) return false;
+ w = -e.dot(b);
+ if(w > 0 || v + w < det) return false;
+ f = r.dot(n)*m.scale;
+ if(f > 0 || f < maxdist*det) return false;
+ }
+ float invdet = 1/det;
+ if(m.flags&MESH_ALPHA && (mode&RAY_ALPHAPOLY)==RAY_ALPHAPOLY && (m.tex->alphamask || (lightmapping <= 1 && loadalphamask(m.tex))))
+ {
+ vec2 at = m.gettc(t.vert[0]), bt = m.gettc(t.vert[1]).sub(at).mul(v*invdet), ct = m.gettc(t.vert[2]).sub(at).mul(w*invdet);
+ at.add(bt).add(ct);
+ int si = clamp(int(m.tex->xs * at.x), 0, m.tex->xs-1),
+ ti = clamp(int(m.tex->ys * at.y), 0, m.tex->ys-1);
+ if(!(m.tex->alphamask[ti*((m.tex->xs+7)/8) + si/8] & (1<<(si%8)))) return false;
+ }
+ dist = f*invdet;
+ return true;
+}
+
+struct traversestate
+{
+ BIH::node *node;
+ float tmin, tmax;
+};
+
+inline bool BIH::traverse(const mesh &m, const vec &o, const vec &ray, const vec &invray, float maxdist, float &dist, int mode, node *curnode, float tmin, float tmax)
+{
+ traversestate stack[128];
+ int stacksize = 0;
+ ivec order(ray.x>0 ? 0 : 1, ray.y>0 ? 0 : 1, ray.z>0 ? 0 : 1);
+ vec mo = m.invxform.transform(o), mray = m.invxformnorm.transform(ray);
+ for(;;)
+ {
+ int axis = curnode->axis();
+ int nearidx = order[axis], faridx = nearidx^1;
+ float nearsplit = (curnode->split[nearidx] - o[axis])*invray[axis],
+ farsplit = (curnode->split[faridx] - o[axis])*invray[axis];
+
+ if(nearsplit <= tmin)
+ {
+ if(farsplit < tmax)
+ {
+ if(!curnode->isleaf(faridx))
+ {
+ curnode += curnode->childindex(faridx);
+ tmin = max(tmin, farsplit);
+ continue;
+ }
+ else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true;
+ }
+ }
+ else if(curnode->isleaf(nearidx))
+ {
+ if(triintersect(m, curnode->childindex(nearidx), mo, mray, maxdist, dist, mode)) return true;
+ if(farsplit < tmax)
+ {
+ if(!curnode->isleaf(faridx))
+ {
+ curnode += curnode->childindex(faridx);
+ tmin = max(tmin, farsplit);
+ continue;
+ }
+ else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true;
+ }
+ }
+ else
+ {
+ if(farsplit < tmax)
+ {
+ if(!curnode->isleaf(faridx))
+ {
+ if(stacksize < int(sizeof(stack)/sizeof(stack[0])))
+ {
+ traversestate &save = stack[stacksize++];
+ save.node = curnode + curnode->childindex(faridx);
+ save.tmin = max(tmin, farsplit);
+ save.tmax = tmax;
+ }
+ else
+ {
+ if(traverse(m, o, ray, invray, maxdist, dist, mode, curnode + curnode->childindex(nearidx), tmin, min(tmax, nearsplit))) return true;
+ curnode += curnode->childindex(faridx);
+ tmin = max(tmin, farsplit);
+ continue;
+ }
+ }
+ else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true;
+ }
+ curnode += curnode->childindex(nearidx);
+ tmax = min(tmax, nearsplit);
+ continue;
+ }
+ if(stacksize <= 0) return false;
+ traversestate &restore = stack[--stacksize];
+ curnode = restore.node;
+ tmin = restore.tmin;
+ tmax = restore.tmax;
+ }
+}
+
+inline bool BIH::traverse(const vec &o, const vec &ray, float maxdist, float &dist, int mode)
+{
+ vec invray(ray.x ? 1/ray.x : 1e16f, ray.y ? 1/ray.y : 1e16f, ray.z ? 1/ray.z : 1e16f);
+ loopi(nummeshes)
+ {
+ mesh &m = meshes[i];
+ if(!(mode&RAY_SHADOW) && m.flags&MESH_NOCLIP) continue;
+ float t1 = (m.bbmin.x - o.x)*invray.x,
+ t2 = (m.bbmax.x - o.x)*invray.x,
+ tmin, tmax;
+ if(invray.x > 0) { tmin = t1; tmax = t2; } else { tmin = t2; tmax = t1; }
+ t1 = (m.bbmin.y - o.y)*invray.y;
+ t2 = (m.bbmax.y - o.y)*invray.y;
+ if(invray.y > 0) { tmin = max(tmin, t1); tmax = min(tmax, t2); } else { tmin = max(tmin, t2); tmax = min(tmax, t1); }
+ t1 = (m.bbmin.z - o.z)*invray.z;
+ t2 = (m.bbmax.z - o.z)*invray.z;
+ if(invray.z > 0) { tmin = max(tmin, t1); tmax = min(tmax, t2); } else { tmin = max(tmin, t2); tmax = min(tmax, t1); }
+ tmax = min(tmax, maxdist);
+ if(tmin < tmax && traverse(m, o, ray, invray, maxdist, dist, mode, m.nodes, tmin, tmax)) return true;
+ }
+ return false;
+}
+
+void BIH::build(mesh &m, ushort *indices, int numindices, const ivec &vmin, const ivec &vmax)
+{
+ int axis = 2;
+ loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k;
+
+ ivec leftmin, leftmax, rightmin, rightmax;
+ int splitleft, splitright;
+ int left, right;
+ loopk(3)
+ {
+ leftmin = rightmin = ivec(INT_MAX, INT_MAX, INT_MAX);
+ leftmax = rightmax = ivec(INT_MIN, INT_MIN, INT_MIN);
+ int split = (vmax[axis] + vmin[axis])/2;
+ for(left = 0, right = numindices, splitleft = SHRT_MIN, splitright = SHRT_MAX; left < right;)
+ {
+ const tribb &tri = m.tribbs[indices[left]];
+ ivec trimin = ivec(tri.center).sub(ivec(tri.radius)),
+ trimax = ivec(tri.center).add(ivec(tri.radius));
+ int amin = trimin[axis], amax = trimax[axis];
+ if(max(split - amin, 0) > max(amax - split, 0))
+ {
+ ++left;
+ splitleft = max(splitleft, amax);
+ leftmin.min(trimin);
+ leftmax.max(trimax);
+ }
+ else
+ {
+ --right;
+ swap(indices[left], indices[right]);
+ splitright = min(splitright, amin);
+ rightmin.min(trimin);
+ rightmax.max(trimax);
+ }
+ }
+ if(left > 0 && right < numindices) break;
+ axis = (axis+1)%3;
+ }
+
+ if(!left || right==numindices)
+ {
+ leftmin = rightmin = ivec(INT_MAX, INT_MAX, INT_MAX);
+ leftmax = rightmax = ivec(INT_MIN, INT_MIN, INT_MIN);
+ left = right = numindices/2;
+ splitleft = SHRT_MIN;
+ splitright = SHRT_MAX;
+ loopi(numindices)
+ {
+ const tribb &tri = m.tribbs[indices[i]];
+ ivec trimin = ivec(tri.center).sub(ivec(tri.radius)),
+ trimax = ivec(tri.center).add(ivec(tri.radius));
+ if(i < left)
+ {
+ splitleft = max(splitleft, trimax[axis]);
+ leftmin.min(trimin);
+ leftmax.max(trimax);
+ }
+ else
+ {
+ splitright = min(splitright, trimin[axis]);
+ rightmin.min(trimin);
+ rightmax.max(trimax);
+ }
+ }
+ }
+
+ int offset = m.numnodes++;
+ node &curnode = m.nodes[offset];
+ curnode.split[0] = short(splitleft);
+ curnode.split[1] = short(splitright);
+
+ if(left==1) curnode.child[0] = (axis<<14) | indices[0];
+ else
+ {
+ curnode.child[0] = (axis<<14) | (m.numnodes - offset);
+ build(m, indices, left, leftmin, leftmax);
+ }
+
+ if(numindices-right==1) curnode.child[1] = (1<<15) | (left==1 ? 1<<14 : 0) | indices[right];
+ else
+ {
+ curnode.child[1] = (left==1 ? 1<<14 : 0) | (m.numnodes - offset);
+ build(m, &indices[right], numindices-right, rightmin, rightmax);
+ }
+}
+
+BIH::BIH(vector<mesh> &buildmeshes)
+ : meshes(NULL), nummeshes(0), nodes(NULL), numnodes(0), tribbs(NULL), numtris(0), bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f), center(0, 0, 0), radius(0), entradius(0)
+{
+ if(buildmeshes.empty()) return;
+ loopv(buildmeshes) numtris += buildmeshes[i].numtris;
+ if(!numtris) return;
+
+ nummeshes = buildmeshes.length();
+ meshes = new mesh[nummeshes];
+ memcpy(meshes, buildmeshes.getbuf(), sizeof(mesh)*buildmeshes.length());
+ tribbs = new tribb[numtris];
+ tribb *dsttri = tribbs;
+ loopi(nummeshes)
+ {
+ mesh &m = meshes[i];
+ m.scale = m.xform.a.magnitude();
+ m.invscale = 1/m.scale;
+ m.xformnorm = matrix3(m.xform);
+ m.xformnorm.normalize();
+ m.invxform.invert(m.xform);
+ m.invxformnorm = matrix3(m.invxform);
+ m.invxformnorm.normalize();
+ m.tribbs = dsttri;
+ const tri *srctri = m.tris;
+ vec mmin(1e16f, 1e16f, 1e16f), mmax(-1e16f, -1e16f, -1e16f);
+ loopj(m.numtris)
+ {
+ vec s0 = m.getpos(srctri->vert[0]), s1 = m.getpos(srctri->vert[1]), s2 = m.getpos(srctri->vert[2]),
+ v0 = m.xform.transform(s0), v1 = m.xform.transform(s1), v2 = m.xform.transform(s2),
+ vmin = vec(v0).min(v1).min(v2),
+ vmax = vec(v0).max(v1).max(v2);
+ mmin.min(vmin);
+ mmax.max(vmax);
+ ivec imin = ivec::floor(vmin), imax = ivec::ceil(vmax);
+ dsttri->center = svec(ivec(imin).add(imax).div(2));
+ dsttri->radius = svec(ivec(imax).sub(imin).add(1).div(2));
+ ++srctri;
+ ++dsttri;
+ }
+ loopk(3) if(fabs(mmax[k] - mmin[k]) < 0.125f)
+ {
+ float mid = (mmin[k] + mmax[k]) / 2;
+ mmin[k] = mid - 0.0625f;
+ mmax[k] = mid + 0.0625f;
+ }
+ m.bbmin = mmin;
+ m.bbmax = mmax;
+ bbmin.min(mmin);
+ bbmax.max(mmax);
+ }
+
+ center = vec(bbmin).add(bbmax).mul(0.5f);
+ radius = vec(bbmax).sub(bbmin).mul(0.5f).magnitude();
+ entradius = max(bbmin.squaredlen(), bbmax.squaredlen());
+
+ nodes = new node[numtris];
+ node *curnode = nodes;
+ ushort *indices = new ushort[numtris];
+ loopi(nummeshes)
+ {
+ mesh &m = meshes[i];
+ m.nodes = curnode;
+ loopj(m.numtris) indices[j] = j;
+ build(m, indices, m.numtris, ivec::floor(m.bbmin), ivec::ceil(m.bbmax));
+ curnode += m.numnodes;
+ }
+ delete[] indices;
+ numnodes = int(curnode - nodes);
+}
+
+BIH::~BIH()
+{
+ delete[] meshes;
+ delete[] nodes;
+ delete[] tribbs;
+}
+
+bool mmintersect(const extentity &e, const vec &o, const vec &ray, float maxdist, int mode, float &dist)
+{
+ model *m = loadmapmodel(e.attr2);
+ if(!m) return false;
+ if(mode&RAY_SHADOW)
+ {
+ if(!m->shadow || e.flags&EF_NOSHADOW) return false;
+ }
+ else if((mode&RAY_ENTS)!=RAY_ENTS && (!m->collide || e.flags&EF_NOCOLLIDE)) return false;
+ if(!m->bih && (lightmapping > 1 || !m->setBIH())) return false;
+ vec mo = vec(o).sub(e.o), mray(ray);
+ float v = mo.dot(mray), inside = m->bih->entradius - mo.squaredlen();
+ if((inside < 0 && v > 0) || inside + v*v < 0) return false;
+ int yaw = e.attr1;
+ if(yaw != 0)
+ {
+ const vec2 &rot = sincosmod360(-yaw);
+ mo.rotate_around_z(rot);
+ mray.rotate_around_z(rot);
+ }
+ return m->bih->traverse(mo, mray, maxdist ? maxdist : 1e16f, dist, mode);
+}
+
diff --git a/src/engine/bih.h b/src/engine/bih.h
new file mode 100644
index 0000000..0f9482f
--- /dev/null
+++ b/src/engine/bih.h
@@ -0,0 +1,79 @@
+struct BIH
+{
+ struct node
+ {
+ short split[2];
+ ushort child[2];
+
+ int axis() const { return child[0]>>14; }
+ int childindex(int which) const { return child[which]&0x3FFF; }
+ bool isleaf(int which) const { return (child[1]&(1<<(14+which)))!=0; }
+ };
+
+ struct tri
+ {
+ ushort vert[3];
+ };
+
+ struct tribb
+ {
+ svec center, radius;
+
+ bool outside(const ivec &bo, const ivec &br) const
+ {
+ return abs(bo.x - center.x) > br.x + radius.x ||
+ abs(bo.y - center.y) > br.y + radius.y ||
+ abs(bo.z - center.z) > br.z + radius.z;
+ }
+ };
+
+ enum { MESH_NOCLIP = 1<<0, MESH_ALPHA = 1<<1, MESH_CULLFACE = 1<<2 };
+
+ struct mesh
+ {
+ enum { MAXTRIS = 1<<14 };
+
+ matrix4x3 xform, invxform;
+ matrix3 xformnorm, invxformnorm;
+ float scale, invscale;
+ node *nodes;
+ int numnodes;
+ const tri *tris;
+ const tribb *tribbs;
+ int numtris;
+ const uchar *pos, *tc;
+ int posstride, tcstride;
+ Texture *tex;
+ int flags;
+ vec bbmin, bbmax;
+
+ mesh() : numnodes(0), numtris(0), tex(NULL), flags(0) {}
+
+ vec getpos(int i) const { return *(const vec *)(pos + i*posstride); }
+ vec2 gettc(int i) const { return *(const vec2 *)(tc + i*tcstride); }
+ };
+
+ mesh *meshes;
+ int nummeshes;
+ node *nodes;
+ int numnodes;
+ tribb *tribbs;
+ int numtris;
+ vec bbmin, bbmax, center;
+ float radius, entradius;
+
+ BIH(vector<mesh> &buildmeshes);
+
+ ~BIH();
+
+ void build(mesh &m, ushort *indices, int numindices, const ivec &vmin, const ivec &vmax);
+
+ bool traverse(const vec &o, const vec &ray, float maxdist, float &dist, int mode);
+ bool traverse(const mesh &m, const vec &o, const vec &ray, const vec &invray, float maxdist, float &dist, int mode, node *curnode, float tmin, float tmax);
+ bool triintersect(const mesh &m, int tidx, const vec &mo, const vec &mray, float maxdist, float &dist, int mode);
+
+ void preload();
+};
+
+extern bool mmintersect(const extentity &e, const vec &o, const vec &ray, float maxdist, int mode, float &dist);
+
diff --git a/src/engine/blend.cpp b/src/engine/blend.cpp
new file mode 100644
index 0000000..16f21c2
--- /dev/null
+++ b/src/engine/blend.cpp
@@ -0,0 +1,862 @@
+#include "engine.h"
+
+enum
+{
+ BM_BRANCH = 0,
+ BM_SOLID,
+ BM_IMAGE
+};
+
+struct BlendMapBranch;
+struct BlendMapSolid;
+struct BlendMapImage;
+
+struct BlendMapNode
+{
+ union
+ {
+ BlendMapBranch *branch;
+ BlendMapSolid *solid;
+ BlendMapImage *image;
+ };
+
+ void cleanup(int type);
+ void splitsolid(uchar &type, uchar val);
+};
+
+struct BlendMapBranch
+{
+ uchar type[4];
+ BlendMapNode children[4];
+
+ ~BlendMapBranch()
+ {
+ loopi(4) children[i].cleanup(type[i]);
+ }
+
+ uchar shrink(BlendMapNode &child, int quadrant);
+};
+
+struct BlendMapSolid
+{
+ uchar val;
+
+ BlendMapSolid(uchar val) : val(val) {}
+};
+
+#define BM_SCALE 1
+#define BM_IMAGE_SIZE 64
+
+struct BlendMapImage
+{
+ uchar data[BM_IMAGE_SIZE*BM_IMAGE_SIZE];
+};
+
+void BlendMapNode::cleanup(int type)
+{
+ switch(type)
+ {
+ case BM_BRANCH: delete branch; break;
+ case BM_IMAGE: delete image; break;
+ }
+}
+
+#define DEFBMSOLIDS(n) n, n+1, n+2, n+3, n+4, n+5, n+6, n+7, n+8, n+9, n+10, n+11, n+12, n+13, n+14, n+15
+
+static BlendMapSolid bmsolids[256] =
+{
+ DEFBMSOLIDS(0x00), DEFBMSOLIDS(0x10), DEFBMSOLIDS(0x20), DEFBMSOLIDS(0x30),
+ DEFBMSOLIDS(0x40), DEFBMSOLIDS(0x50), DEFBMSOLIDS(0x60), DEFBMSOLIDS(0x70),
+ DEFBMSOLIDS(0x80), DEFBMSOLIDS(0x90), DEFBMSOLIDS(0xA0), DEFBMSOLIDS(0xB0),
+ DEFBMSOLIDS(0xC0), DEFBMSOLIDS(0xD0), DEFBMSOLIDS(0xE0), DEFBMSOLIDS(0xF0),
+};
+
+void BlendMapNode::splitsolid(uchar &type, uchar val)
+{
+ cleanup(type);
+ type = BM_BRANCH;
+ branch = new BlendMapBranch;
+ loopi(4)
+ {
+ branch->type[i] = BM_SOLID;
+ branch->children[i].solid = &bmsolids[val];
+ }
+}
+
+uchar BlendMapBranch::shrink(BlendMapNode &child, int quadrant)
+{
+ uchar childtype = type[quadrant];
+ child = children[quadrant];
+ type[quadrant] = BM_SOLID;
+ children[quadrant].solid = &bmsolids[0];
+ return childtype;
+}
+
+struct BlendMapRoot : BlendMapNode
+{
+ uchar type;
+
+ BlendMapRoot() : type(BM_SOLID) { solid = &bmsolids[0xFF]; }
+ BlendMapRoot(uchar type, const BlendMapNode &node) : BlendMapNode(node), type(type) {}
+
+ void cleanup() { BlendMapNode::cleanup(type); }
+
+ void shrink(int quadrant)
+ {
+ if(type == BM_BRANCH)
+ {
+ BlendMapRoot oldroot = *this;
+ type = branch->shrink(*this, quadrant);
+ oldroot.cleanup();
+ }
+ }
+};
+
+static BlendMapRoot blendmap;
+
+struct BlendMapCache
+{
+ BlendMapRoot node;
+ int scale;
+ ivec2 origin;
+};
+
+BlendMapCache *newblendmapcache() { return new BlendMapCache; }
+
+void freeblendmapcache(BlendMapCache *&cache) { delete cache; cache = NULL; }
+
+bool setblendmaporigin(BlendMapCache *cache, const ivec &o, int size)
+{
+ if(blendmap.type!=BM_BRANCH)
+ {
+ cache->node = blendmap;
+ cache->scale = worldscale-BM_SCALE;
+ cache->origin = ivec2(0, 0);
+ return cache->node.solid!=&bmsolids[0xFF];
+ }
+
+ BlendMapBranch *bm = blendmap.branch;
+ int bmscale = worldscale-BM_SCALE, bmsize = 1<<bmscale,
+ x = o.x>>BM_SCALE, y = o.y>>BM_SCALE,
+ x1 = max(x-1, 0), y1 = max(y-1, 0),
+ x2 = min(((o.x + size + (1<<BM_SCALE)-1)>>BM_SCALE) + 1, bmsize),
+ y2 = min(((o.y + size + (1<<BM_SCALE)-1)>>BM_SCALE) + 1, bmsize),
+ diff = (x1^x2)|(y1^y2);
+ if(diff < bmsize) while(!(diff&(1<<(bmscale-1))))
+ {
+ bmscale--;
+ int n = (((y1>>bmscale)&1)<<1) | ((x1>>bmscale)&1);
+ if(bm->type[n]!=BM_BRANCH)
+ {
+ cache->node = BlendMapRoot(bm->type[n], bm->children[n]);
+ cache->scale = bmscale;
+ cache->origin = ivec2(x1&(~0U<<bmscale), y1&(~0U<<bmscale));
+ return cache->node.solid!=&bmsolids[0xFF];
+ }
+ bm = bm->children[n].branch;
+ }
+
+ cache->node.type = BM_BRANCH;
+ cache->node.branch = bm;
+ cache->scale = bmscale;
+ cache->origin = ivec2(x1&(~0U<<bmscale), y1&(~0U<<bmscale));
+ return true;
+}
+
+bool hasblendmap(BlendMapCache *cache)
+{
+ return cache->node.solid!=&bmsolids[0xFF];
+}
+
+static uchar lookupblendmap(int x, int y, BlendMapBranch *bm, int bmscale)
+{
+ for(;;)
+ {
+ bmscale--;
+ int n = (((y>>bmscale)&1)<<1) | ((x>>bmscale)&1);
+ switch(bm->type[n])
+ {
+ case BM_SOLID: return bm->children[n].solid->val;
+ case BM_IMAGE: return bm->children[n].image->data[(y&((1<<bmscale)-1))*BM_IMAGE_SIZE + (x&((1<<bmscale)-1))];
+ }
+ bm = bm->children[n].branch;
+ }
+}
+
+uchar lookupblendmap(BlendMapCache *cache, const vec &pos)
+{
+ if(cache->node.type==BM_SOLID) return cache->node.solid->val;
+
+ uchar vals[4], *val = vals;
+ float bx = pos.x/(1<<BM_SCALE) - 0.5f, by = pos.y/(1<<BM_SCALE) - 0.5f;
+ int ix = (int)floor(bx), iy = (int)floor(by),
+ rx = ix-cache->origin.x, ry = iy-cache->origin.y;
+ loop(vy, 2) loop(vx, 2)
+ {
+ int cx = clamp(rx+vx, 0, (1<<cache->scale)-1), cy = clamp(ry+vy, 0, (1<<cache->scale)-1);
+ if(cache->node.type==BM_IMAGE)
+ *val++ = cache->node.image->data[cy*BM_IMAGE_SIZE + cx];
+ else *val++ = lookupblendmap(cx, cy, cache->node.branch, cache->scale);
+ }
+ float fx = bx - ix, fy = by - iy;
+ return uchar((1-fy)*((1-fx)*vals[0] + fx*vals[1]) +
+ fy*((1-fx)*vals[2] + fx*vals[3]));
+}
+
+static void fillblendmap(uchar &type, BlendMapNode &node, int size, uchar val, int x1, int y1, int x2, int y2)
+{
+ if(max(x1, y1) <= 0 && min(x2, y2) >= size)
+ {
+ node.cleanup(type);
+ type = BM_SOLID;
+ node.solid = &bmsolids[val];
+ return;
+ }
+
+ if(type==BM_BRANCH)
+ {
+ size /= 2;
+ if(y1 < size)
+ {
+ if(x1 < size) fillblendmap(node.branch->type[0], node.branch->children[0], size, val,
+ x1, y1, min(x2, size), min(y2, size));
+ if(x2 > size) fillblendmap(node.branch->type[1], node.branch->children[1], size, val,
+ max(x1-size, 0), y1, x2-size, min(y2, size));
+ }
+ if(y2 > size)
+ {
+ if(x1 < size) fillblendmap(node.branch->type[2], node.branch->children[2], size, val,
+ x1, max(y1-size, 0), min(x2, size), y2-size);
+ if(x2 > size) fillblendmap(node.branch->type[3], node.branch->children[3], size, val,
+ max(x1-size, 0), max(y1-size, 0), x2-size, y2-size);
+ }
+ loopi(4) if(node.branch->type[i]!=BM_SOLID || node.branch->children[i].solid->val!=val) return;
+ node.cleanup(type);
+ type = BM_SOLID;
+ node.solid = &bmsolids[val];
+ return;
+ }
+ else if(type==BM_SOLID)
+ {
+ uchar oldval = node.solid->val;
+ if(oldval==val) return;
+
+ if(size > BM_IMAGE_SIZE)
+ {
+ node.splitsolid(type, oldval);
+ fillblendmap(type, node, size, val, x1, y1, x2, y2);
+ return;
+ }
+
+ type = BM_IMAGE;
+ node.image = new BlendMapImage;
+ memset(node.image->data, oldval, sizeof(node.image->data));
+ }
+
+ uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1];
+ loopi(y2-y1)
+ {
+ memset(dst, val, x2-x1);
+ dst += BM_IMAGE_SIZE;
+ }
+}
+
+void fillblendmap(int x, int y, int w, int h, uchar val)
+{
+ int bmsize = worldsize>>BM_SCALE,
+ x1 = clamp(x, 0, bmsize),
+ y1 = clamp(y, 0, bmsize),
+ x2 = clamp(x+w, 0, bmsize),
+ y2 = clamp(y+h, 0, bmsize);
+ if(max(x1, y1) >= bmsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return;
+ fillblendmap(blendmap.type, blendmap, bmsize, val, x1, y1, x2, y2);
+}
+
+static void invertblendmap(uchar &type, BlendMapNode &node, int size, int x1, int y1, int x2, int y2)
+{
+ if(type==BM_BRANCH)
+ {
+ size /= 2;
+ if(y1 < size)
+ {
+ if(x1 < size) invertblendmap(node.branch->type[0], node.branch->children[0], size,
+ x1, y1, min(x2, size), min(y2, size));
+ if(x2 > size) invertblendmap(node.branch->type[1], node.branch->children[1], size,
+ max(x1-size, 0), y1, x2-size, min(y2, size));
+ }
+ if(y2 > size)
+ {
+ if(x1 < size) invertblendmap(node.branch->type[2], node.branch->children[2], size,
+ x1, max(y1-size, 0), min(x2, size), y2-size);
+ if(x2 > size) invertblendmap(node.branch->type[3], node.branch->children[3], size,
+ max(x1-size, 0), max(y1-size, 0), x2-size, y2-size);
+ }
+ return;
+ }
+ else if(type==BM_SOLID)
+ {
+ fillblendmap(type, node, size, 255-node.solid->val, x1, y1, x2, y2);
+ }
+ else if(type==BM_IMAGE)
+ {
+ uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1];
+ loopi(y2-y1)
+ {
+ loopj(x2-x1) dst[j] = 255-dst[j];
+ dst += BM_IMAGE_SIZE;
+ }
+ }
+}
+
+void invertblendmap(int x, int y, int w, int h)
+{
+ int bmsize = worldsize>>BM_SCALE,
+ x1 = clamp(x, 0, bmsize),
+ y1 = clamp(y, 0, bmsize),
+ x2 = clamp(x+w, 0, bmsize),
+ y2 = clamp(y+h, 0, bmsize);
+ if(max(x1, y1) >= bmsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return;
+ invertblendmap(blendmap.type, blendmap, bmsize, x1, y1, x2, y2);
+}
+
+static void optimizeblendmap(uchar &type, BlendMapNode &node)
+{
+ switch(type)
+ {
+ case BM_IMAGE:
+ {
+ uint val = node.image->data[0];
+ val |= val<<8;
+ val |= val<<16;
+ for(uint *data = (uint *)node.image->data, *end = &data[sizeof(node.image->data)/sizeof(uint)]; data < end; data++)
+ if(*data != val) return;
+ node.cleanup(type);
+ type = BM_SOLID;
+ node.solid = &bmsolids[val&0xFF];
+ break;
+ }
+ case BM_BRANCH:
+ {
+ loopi(4) optimizeblendmap(node.branch->type[i], node.branch->children[i]);
+ if(node.branch->type[3]!=BM_SOLID) return;
+ uint val = node.branch->children[3].solid->val;
+ loopi(3) if(node.branch->type[i]!=BM_SOLID || node.branch->children[i].solid->val != val) return;
+ node.cleanup(type);
+ type = BM_SOLID;
+ node.solid = &bmsolids[val];
+ break;
+ }
+ }
+}
+
+void optimizeblendmap()
+{
+ optimizeblendmap(blendmap.type, blendmap);
+}
+
+VARF(blendpaintmode, 0, 0, 5,
+{
+ if(!blendpaintmode) stoppaintblendmap();
+});
+
+static void blitblendmap(uchar &type, BlendMapNode &node, int bmx, int bmy, int bmsize, uchar *src, int sx, int sy, int sw, int sh, int smode)
+{
+ if(type==BM_BRANCH)
+ {
+ bmsize /= 2;
+ if(sy < bmy + bmsize)
+ {
+ if(sx < bmx + bmsize) blitblendmap(node.branch->type[0], node.branch->children[0], bmx, bmy, bmsize, src, sx, sy, sw, sh, smode);
+ if(sx + sw > bmx + bmsize) blitblendmap(node.branch->type[1], node.branch->children[1], bmx+bmsize, bmy, bmsize, src, sx, sy, sw, sh, smode);
+ }
+ if(sy + sh > bmy + bmsize)
+ {
+ if(sx < bmx + bmsize) blitblendmap(node.branch->type[2], node.branch->children[2], bmx, bmy+bmsize, bmsize, src, sx, sy, sw, sh, smode);
+ if(sx + sw > bmx + bmsize) blitblendmap(node.branch->type[3], node.branch->children[3], bmx+bmsize, bmy+bmsize, bmsize, src, sx, sy, sw, sh, smode);
+ }
+ return;
+ }
+ if(type==BM_SOLID)
+ {
+ uchar val = node.solid->val;
+ if(bmsize > BM_IMAGE_SIZE)
+ {
+ node.splitsolid(type, val);
+ blitblendmap(type, node, bmx, bmy, bmsize, src, sx, sy, sw, sh, smode);
+ return;
+ }
+
+ type = BM_IMAGE;
+ node.image = new BlendMapImage;
+ memset(node.image->data, val, sizeof(node.image->data));
+ }
+
+ int x1 = clamp(sx - bmx, 0, bmsize), y1 = clamp(sy - bmy, 0, bmsize),
+ x2 = clamp(sx+sw - bmx, 0, bmsize), y2 = clamp(sy+sh - bmy, 0, bmsize);
+ uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1];
+ src += max(bmy - sy, 0)*sw + max(bmx - sx, 0);
+ loopi(y2-y1)
+ {
+ switch(smode)
+ {
+ case 1:
+ memcpy(dst, src, x2 - x1);
+ break;
+
+ case 2:
+ loopi(x2 - x1) dst[i] = min(dst[i], src[i]);
+ break;
+
+ case 3:
+ loopi(x2 - x1) dst[i] = max(dst[i], src[i]);
+ break;
+
+ case 4:
+ loopi(x2 - x1) dst[i] = min(dst[i], uchar(0xFF - src[i]));
+ break;
+
+ case 5:
+ loopi(x2 - x1) dst[i] = max(dst[i], uchar(0xFF - src[i]));
+ break;
+ }
+ dst += BM_IMAGE_SIZE;
+ src += sw;
+ }
+}
+
+void blitblendmap(uchar *src, int sx, int sy, int sw, int sh, int smode)
+{
+ int bmsize = worldsize>>BM_SCALE;
+ if(max(sx, sy) >= bmsize || min(sx+sw, sy+sh) <= 0 || min(sw, sh) <= 0) return;
+ blitblendmap(blendmap.type, blendmap, 0, 0, bmsize, src, sx, sy, sw, sh, smode);
+}
+
+void resetblendmap()
+{
+ blendmap.cleanup();
+ blendmap.type = BM_SOLID;
+ blendmap.solid = &bmsolids[0xFF];
+}
+
+void enlargeblendmap()
+{
+ if(blendmap.type == BM_SOLID) return;
+ BlendMapBranch *branch = new BlendMapBranch;
+ branch->type[0] = blendmap.type;
+ branch->children[0] = blendmap;
+ loopi(3)
+ {
+ branch->type[i+1] = BM_SOLID;
+ branch->children[i+1].solid = &bmsolids[0xFF];
+ }
+ blendmap.type = BM_BRANCH;
+ blendmap.branch = branch;
+}
+
+void shrinkblendmap(int octant)
+{
+ blendmap.shrink(octant&3);
+}
+
+void moveblendmap(uchar type, BlendMapNode &node, int size, int x, int y, int dx, int dy)
+{
+ if(type == BM_BRANCH)
+ {
+ size /= 2;
+ moveblendmap(node.branch->type[0], node.branch->children[0], size, x, y, dx, dy);
+ moveblendmap(node.branch->type[1], node.branch->children[1], size, x + size, y, dx, dy);
+ moveblendmap(node.branch->type[2], node.branch->children[2], size, x, y + size, dx, dy);
+ moveblendmap(node.branch->type[3], node.branch->children[3], size, x + size, y + size, dx, dy);
+ return;
+ }
+ else if(type == BM_SOLID)
+ {
+ fillblendmap(x+dx, y+dy, size, size, node.solid->val);
+ }
+ else if(type == BM_IMAGE)
+ {
+ blitblendmap(node.image->data, x+dx, y+dy, size, size, 1);
+ }
+}
+
+void moveblendmap(int dx, int dy)
+{
+ BlendMapRoot old = blendmap;
+ blendmap.type = BM_SOLID;
+ blendmap.solid = &bmsolids[0xFF];
+ moveblendmap(old.type, old, worldsize>>BM_SCALE, 0, 0, dx, dy);
+ old.cleanup();
+}
+
+struct BlendBrush
+{
+ char *name;
+ int w, h;
+ uchar *data;
+ GLuint tex;
+
+ BlendBrush(const char *name, int w, int h) :
+ name(newstring(name)), w(w), h(h), data(new uchar[w*h]), tex(0)
+ {}
+
+ ~BlendBrush()
+ {
+ cleanup();
+ delete[] name;
+ if(data) delete[] data;
+ }
+
+ void cleanup()
+ {
+ if(tex) { glDeleteTextures(1, &tex); tex = 0; }
+ }
+
+ void gentex()
+ {
+ if(!tex) glGenTextures(1, &tex);
+ uchar *buf = new uchar[2*w*h];
+ uchar *dst = buf, *src = data;
+ loopi(h)
+ {
+ loopj(w) *dst++ = 255 - *src++;
+ }
+ createtexture(tex, w, h, buf, 3, 1, hasTRG ? GL_R8 : GL_LUMINANCE8);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
+ GLfloat border[4] = { 0, 0, 0, 0 };
+ glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
+ delete[] buf;
+ }
+
+ void reorient(bool flipx, bool flipy, bool swapxy)
+ {
+ uchar *rdata = new uchar[w*h];
+ int stridex = 1, stridey = 1;
+ if(swapxy) stridex *= h; else stridey *= w;
+ uchar *src = data, *dst = rdata;
+ if(flipx) { dst += (w-1)*stridex; stridex = -stridex; }
+ if(flipy) { dst += (h-1)*stridey; stridey = -stridey; }
+ loopi(h)
+ {
+ uchar *curdst = dst;
+ loopj(w)
+ {
+ *curdst = *src++;
+ curdst += stridex;
+ }
+ dst += stridey;
+ }
+ if(swapxy) swap(w, h);
+ delete[] data;
+ data = rdata;
+ if(tex) gentex();
+ }
+};
+
+static vector<BlendBrush *> brushes;
+static int curbrush = -1;
+
+void cleanupblendmap()
+{
+ loopv(brushes) brushes[i]->cleanup();
+}
+
+void clearblendbrushes()
+{
+ while(brushes.length()) delete brushes.pop();
+ curbrush = -1;
+}
+
+void delblendbrush(const char *name)
+{
+ loopv(brushes) if(!strcmp(brushes[i]->name, name))
+ {
+ delete brushes[i];
+ brushes.remove(i--);
+ }
+ curbrush = brushes.empty() ? -1 : clamp(curbrush, 0, brushes.length()-1);
+}
+
+void addblendbrush(const char *name, const char *imgname)
+{
+ delblendbrush(name);
+
+ ImageData s;
+ if(!loadimage(imgname, s)) { conoutf(CON_ERROR, "could not load blend brush image %s", imgname); return; }
+ if(max(s.w, s.h) > (1<<12))
+ {
+ conoutf(CON_ERROR, "blend brush image size exceeded %dx%d pixels: %s", 1<<12, 1<<12, imgname);
+ return;
+ }
+
+ BlendBrush *brush = new BlendBrush(name, s.w, s.h);
+
+ uchar *dst = brush->data, *srcrow = s.data;
+ loopi(s.h)
+ {
+ for(uchar *src = srcrow, *end = &srcrow[s.w*s.bpp]; src < end; src += s.bpp)
+ *dst++ = src[0];
+ srcrow += s.pitch;
+ }
+
+ brushes.add(brush);
+ if(curbrush < 0) curbrush = 0;
+ else if(curbrush >= brushes.length()) curbrush = brushes.length()-1;
+
+}
+
+void nextblendbrush(int *dir)
+{
+ curbrush += *dir < 0 ? -1 : 1;
+ if(brushes.empty()) curbrush = -1;
+ else if(!brushes.inrange(curbrush)) curbrush = *dir < 0 ? brushes.length()-1 : 0;
+}
+
+void setblendbrush(const char *name)
+{
+ loopv(brushes) if(!strcmp(brushes[i]->name, name)) { curbrush = i; break; }
+}
+
+void getblendbrushname(int *n)
+{
+ result(brushes.inrange(*n) ? brushes[*n]->name : "");
+}
+
+void curblendbrush()
+{
+ intret(curbrush);
+}
+
+COMMAND(clearblendbrushes, "");
+COMMAND(delblendbrush, "s");
+COMMAND(addblendbrush, "ss");
+COMMAND(nextblendbrush, "i");
+COMMAND(setblendbrush, "s");
+COMMAND(getblendbrushname, "i");
+COMMAND(curblendbrush, "");
+
+extern int nompedit;
+
+bool canpaintblendmap(bool brush = true, bool sel = false, bool msg = true)
+{
+ if(noedit(!sel, msg) || (nompedit && multiplayer())) return false;
+ if(!blendpaintmode)
+ {
+ if(msg) conoutf(CON_ERROR, "operation only allowed in blend paint mode");
+ return false;
+ }
+ if(brush && !brushes.inrange(curbrush))
+ {
+ if(msg) conoutf(CON_ERROR, "no blend brush selected");
+ return false;
+ }
+ return true;
+}
+
+void rotateblendbrush(int *val)
+{
+ if(!canpaintblendmap()) return;
+ BlendBrush *brush = brushes[curbrush];
+ const texrotation &r = texrotations[*val < 0 ? 3 : clamp(*val, 1, 7)];
+ brush->reorient(r.flipx, r.flipy, r.swapxy);
+}
+
+COMMAND(rotateblendbrush, "i");
+
+void paintblendmap(bool msg)
+{
+ if(!canpaintblendmap(true, false, msg)) return;
+
+ BlendBrush *brush = brushes[curbrush];
+ int x = (int)floor(clamp(worldpos.x, 0.0f, float(worldsize))/(1<<BM_SCALE) - 0.5f*brush->w),
+ y = (int)floor(clamp(worldpos.y, 0.0f, float(worldsize))/(1<<BM_SCALE) - 0.5f*brush->h);
+ blitblendmap(brush->data, x, y, brush->w, brush->h, blendpaintmode);
+ previewblends(ivec((x-1)<<BM_SCALE, (y-1)<<BM_SCALE, 0),
+ ivec((x+brush->w+1)<<BM_SCALE, (y+brush->h+1)<<BM_SCALE, worldsize));
+}
+
+VAR(paintblendmapdelay, 1, 500, 3000);
+VAR(paintblendmapinterval, 1, 30, 3000);
+
+int paintingblendmap = 0, lastpaintblendmap = 0;
+
+void stoppaintblendmap()
+{
+ paintingblendmap = 0;
+ lastpaintblendmap = 0;
+}
+
+void trypaintblendmap()
+{
+ if(!paintingblendmap || totalmillis - paintingblendmap < paintblendmapdelay) return;
+ if(lastpaintblendmap)
+ {
+ int diff = totalmillis - lastpaintblendmap;
+ if(diff < paintblendmapinterval) return;
+ lastpaintblendmap = (diff - diff%paintblendmapinterval) + lastpaintblendmap;
+ }
+ else lastpaintblendmap = totalmillis;
+ paintblendmap(false);
+}
+
+ICOMMAND(paintblendmap, "D", (int *isdown),
+{
+ if(*isdown)
+ {
+ if(!paintingblendmap) { paintblendmap(true); paintingblendmap = totalmillis; }
+ }
+ else stoppaintblendmap();
+});
+
+void clearblendmapsel()
+{
+ if(noedit(false) || (nompedit && multiplayer())) return;
+ extern selinfo sel;
+ int x1 = sel.o.x>>BM_SCALE, y1 = sel.o.y>>BM_SCALE,
+ x2 = (sel.o.x+sel.s.x*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE,
+ y2 = (sel.o.y+sel.s.y*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE;
+ fillblendmap(x1, y1, x2-x1, y2-y1, 0xFF);
+ previewblends(ivec(x1<<BM_SCALE, y1<<BM_SCALE, 0),
+ ivec(x2<<BM_SCALE, y2<<BM_SCALE, worldsize));
+}
+
+COMMAND(clearblendmapsel, "");
+
+void invertblendmapsel()
+{
+ if(noedit(false) || (nompedit && multiplayer())) return;
+ extern selinfo sel;
+ int x1 = sel.o.x>>BM_SCALE, y1 = sel.o.y>>BM_SCALE,
+ x2 = (sel.o.x+sel.s.x*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE,
+ y2 = (sel.o.y+sel.s.y*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE;
+ invertblendmap(x1, y1, x2-x1, y2-y1);
+ previewblends(ivec(x1<<BM_SCALE, y1<<BM_SCALE, 0),
+ ivec(x2<<BM_SCALE, y2<<BM_SCALE, worldsize));
+}
+
+COMMAND(invertblendmapsel, "");
+
+void invertblendmap()
+{
+ if(noedit(false) || (nompedit && multiplayer())) return;
+ invertblendmap(0, 0, worldsize>>BM_SCALE, worldsize>>BM_SCALE);
+ previewblends(ivec(0, 0, 0), ivec(worldsize, worldsize, worldsize));
+}
+
+COMMAND(invertblendmap, "");
+
+void showblendmap()
+{
+ if(noedit(true) || (nompedit && multiplayer())) return;
+ previewblends(ivec(0, 0, 0), ivec(worldsize, worldsize, worldsize));
+}
+
+COMMAND(showblendmap, "");
+COMMAND(optimizeblendmap, "");
+ICOMMAND(clearblendmap, "", (),
+{
+ if(noedit(true) || (nompedit && multiplayer())) return;
+ resetblendmap();
+ showblendmap();
+});
+
+ICOMMAND(moveblendmap, "ii", (int *dx, int *dy),
+{
+ if(noedit(true) || (nompedit && multiplayer())) return;
+ if(*dx%(BM_IMAGE_SIZE<<BM_SCALE) || *dy%(BM_IMAGE_SIZE<<BM_SCALE))
+ {
+ conoutf(CON_ERROR, "blendmap movement must be in multiples of %d", BM_IMAGE_SIZE<<BM_SCALE);
+ return;
+ }
+ if(*dx <= -worldsize || *dx >= worldsize || *dy <= -worldsize || *dy >= worldsize)
+ resetblendmap();
+ else
+ moveblendmap(*dx>>BM_SCALE, *dy>>BM_SCALE);
+ showblendmap();
+});
+
+void renderblendbrush()
+{
+ if(!blendpaintmode || !brushes.inrange(curbrush)) return;
+
+ BlendBrush *brush = brushes[curbrush];
+ int x1 = (int)floor(clamp(worldpos.x, 0.0f, float(worldsize))/(1<<BM_SCALE) - 0.5f*brush->w) << BM_SCALE,
+ y1 = (int)floor(clamp(worldpos.y, 0.0f, float(worldsize))/(1<<BM_SCALE) - 0.5f*brush->h) << BM_SCALE,
+ x2 = x1 + (brush->w << BM_SCALE),
+ y2 = y1 + (brush->h << BM_SCALE);
+
+ if(max(x1, y1) >= worldsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return;
+
+ if(!brush->tex) brush->gentex();
+ renderblendbrush(brush->tex, x1, y1, x2 - x1, y2 - y1);
+}
+
+bool loadblendmap(stream *f, uchar &type, BlendMapNode &node)
+{
+ type = f->getchar();
+ switch(type)
+ {
+ case BM_SOLID:
+ {
+ int val = f->getchar();
+ if(val<0 || val>0xFF) return false;
+ node.solid = &bmsolids[val];
+ break;
+ }
+
+ case BM_IMAGE:
+ node.image = new BlendMapImage;
+ if(f->read(node.image->data, sizeof(node.image->data)) != sizeof(node.image->data))
+ return false;
+ break;
+
+ case BM_BRANCH:
+ node.branch = new BlendMapBranch;
+ loopi(4) { node.branch->type[i] = BM_SOLID; node.branch->children[i].solid = &bmsolids[0xFF]; }
+ loopi(4) if(!loadblendmap(f, node.branch->type[i], node.branch->children[i]))
+ return false;
+ break;
+
+ default:
+ type = BM_SOLID;
+ node.solid = &bmsolids[0xFF];
+ return false;
+ }
+ return true;
+}
+
+bool loadblendmap(stream *f, int info)
+{
+ resetblendmap();
+ return loadblendmap(f, blendmap.type, blendmap);
+}
+
+void saveblendmap(stream *f, uchar type, BlendMapNode &node)
+{
+ f->putchar(type);
+ switch(type)
+ {
+ case BM_SOLID:
+ f->putchar(node.solid->val);
+ break;
+
+ case BM_IMAGE:
+ f->write(node.image->data, sizeof(node.image->data));
+ break;
+
+ case BM_BRANCH:
+ loopi(4) saveblendmap(f, node.branch->type[i], node.branch->children[i]);
+ break;
+ }
+}
+
+void saveblendmap(stream *f)
+{
+ saveblendmap(f, blendmap.type, blendmap);
+}
+
+uchar shouldsaveblendmap()
+{
+ return blendmap.solid!=&bmsolids[0xFF] ? 1 : 0;
+}
+
diff --git a/src/engine/blob.cpp b/src/engine/blob.cpp
new file mode 100644
index 0000000..a587971
--- /dev/null
+++ b/src/engine/blob.cpp
@@ -0,0 +1,727 @@
+#include "engine.h"
+
+extern int intel_mapbufferrange_bug;
+
+VARNP(blobs, showblobs, 0, 1, 1);
+VARFP(blobintensity, 0, 60, 100, resetblobs());
+VARFP(blobheight, 1, 32, 128, resetblobs());
+VARFP(blobfadelow, 1, 8, 32, resetblobs());
+VARFP(blobfadehigh, 1, 8, 32, resetblobs());
+VARFP(blobmargin, 0, 1, 16, resetblobs());
+
+VAR(dbgblob, 0, 0, 1);
+
+enum
+{
+ BL_DUP = 1<<0,
+ BL_RENDER = 1<<1
+};
+
+struct blobinfo
+{
+ vec o;
+ float radius;
+ int millis;
+ ushort startindex, endindex, startvert, endvert, next, flags;
+};
+
+struct blobvert
+{
+ vec pos;
+ bvec4 color;
+ vec2 tc;
+};
+
+struct blobrenderer
+{
+ const char *texname;
+ Texture *tex;
+ ushort *cache;
+ int cachesize;
+ blobinfo *blobs;
+ int maxblobs, startblob, endblob;
+ blobvert *verts;
+ int maxverts, startvert, endvert, availverts;
+ ushort *indexes;
+ int maxindexes, startindex, endindex, availindexes;
+ GLuint ebo, vbo;
+ ushort *edata;
+ blobvert *vdata;
+ int numedata, numvdata;
+ blobinfo *startrender, *endrender;
+
+ blobinfo *lastblob;
+
+ vec blobmin, blobmax;
+ ivec bbmin, bbmax;
+ float blobalphalow, blobalphahigh;
+ uchar blobalpha;
+
+ blobrenderer(const char *texname)
+ : texname(texname), tex(NULL),
+ cache(NULL), cachesize(0),
+ blobs(NULL), maxblobs(0), startblob(0), endblob(0),
+ verts(NULL), maxverts(0), startvert(0), endvert(0), availverts(0),
+ indexes(NULL), maxindexes(0), startindex(0), endindex(0), availindexes(0),
+ ebo(0), vbo(0), edata(NULL), vdata(NULL), numedata(0), numvdata(0),
+ startrender(NULL), endrender(NULL), lastblob(NULL)
+ {}
+
+ ~blobrenderer()
+ {
+ DELETEA(cache);
+ DELETEA(blobs);
+ DELETEA(verts);
+ DELETEA(indexes);
+ }
+
+ void cleanup()
+ {
+ if(ebo) { glDeleteBuffers_(1, &ebo); ebo = 0; }
+ if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+ DELETEA(edata);
+ DELETEA(vdata);
+ numedata = numvdata = 0;
+ startrender = endrender = NULL;
+ }
+
+ void init(int tris)
+ {
+ cleanup();
+ if(cache)
+ {
+ DELETEA(cache);
+ cachesize = 0;
+ }
+ if(blobs)
+ {
+ DELETEA(blobs);
+ maxblobs = startblob = endblob = 0;
+ }
+ if(verts)
+ {
+ DELETEA(verts);
+ maxverts = startvert = endvert = availverts = 0;
+ }
+ if(indexes)
+ {
+ DELETEA(indexes);
+ maxindexes = startindex = endindex = availindexes = 0;
+ }
+ if(!tris) return;
+ tex = textureload(texname, 3);
+ cachesize = tris/2;
+ cache = new ushort[cachesize];
+ memset(cache, 0xFF, cachesize * sizeof(ushort));
+ maxblobs = tris/2;
+ blobs = new blobinfo[maxblobs];
+ memclear(blobs, maxblobs);
+ maxindexes = tris*3 + 3;
+ availindexes = maxindexes - 3;
+ indexes = new ushort[maxindexes];
+ maxverts = min(tris*3/2 + 1, (1<<16)-1);
+ availverts = maxverts - 1;
+ verts = new blobvert[maxverts];
+ }
+
+ bool freeblob()
+ {
+ blobinfo &b = blobs[startblob];
+ if(&b == lastblob) return false;
+
+ if(b.flags & BL_RENDER) flushblobs();
+
+ startblob++;
+ if(startblob >= maxblobs) startblob = 0;
+
+ startvert = b.endvert;
+ if(startvert>=maxverts) startvert = 0;
+ availverts += b.endvert - b.startvert;
+
+ startindex = b.endindex;
+ if(startindex>=maxindexes) startindex = 0;
+ availindexes += b.endindex - b.startindex;
+
+ b.millis = lastreset;
+ b.flags = 0;
+
+ return true;
+ }
+
+ blobinfo &newblob(const vec &o, float radius)
+ {
+ blobinfo &b = blobs[endblob];
+ int next = endblob + 1;
+ if(next>=maxblobs) next = 0;
+ if(next==startblob)
+ {
+ lastblob = &b;
+ freeblob();
+ }
+ endblob = next;
+ b.o = o;
+ b.radius = radius;
+ b.millis = totalmillis;
+ b.flags = 0;
+ b.next = 0xFFFF;
+ b.startindex = b.endindex = endindex;
+ b.startvert = b.endvert = endvert;
+ lastblob = &b;
+ return b;
+ }
+
+ template<int C>
+ static int split(const vec *in, int numin, float below, float above, vec *out)
+ {
+ int numout = 0;
+ const vec *p = &in[numin-1];
+ float pc = (*p)[C];
+ loopi(numin)
+ {
+ const vec &v = in[i];
+ float c = v[C];
+ if(c < below)
+ {
+ if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ }
+ else if(c > above)
+ {
+ if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ }
+ else if(pc < below)
+ {
+ if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ }
+ else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ out[numout++] = v;
+ p = &v;
+ pc = c;
+ }
+ return numout;
+ }
+
+ template<int C>
+ static int clip(const vec *in, int numin, float below, float above, vec *out)
+ {
+ int numout = 0;
+ const vec *p = &in[numin-1];
+ float pc = (*p)[C];
+ loopi(numin)
+ {
+ const vec &v = in[i];
+ float c = v[C];
+ if(c < below)
+ {
+ if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ }
+ else if(c > above)
+ {
+ if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ }
+ else
+ {
+ if(pc < below)
+ {
+ if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ }
+ else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ out[numout++] = v;
+ }
+ p = &v;
+ pc = c;
+ }
+ return numout;
+ }
+
+ void dupblob()
+ {
+ if(lastblob->startvert >= lastblob->endvert)
+ {
+ lastblob->startindex = lastblob->endindex = endindex;
+ lastblob->startvert = lastblob->endvert = endvert;
+ return;
+ }
+ blobinfo &b = newblob(lastblob->o, lastblob->radius);
+ b.flags |= BL_DUP;
+ }
+
+ inline int addvert(const vec &pos)
+ {
+ blobvert &v = verts[endvert];
+ v.pos = pos;
+ v.tc = vec2((pos.x - blobmin.x) / (blobmax.x - blobmin.x),
+ (pos.y - blobmin.y) / (blobmax.y - blobmin.y));
+ uchar alpha;
+ if(pos.z < blobmin.z + blobfadelow) alpha = uchar(blobalphalow * (pos.z - blobmin.z));
+ else if(pos.z > blobmax.z - blobfadehigh) alpha = uchar(blobalphahigh * (blobmax.z - pos.z));
+ else alpha = blobalpha;
+ v.color = bvec4(255, 255, 255, alpha);
+ return endvert++;
+ }
+
+ void addtris(const vec *v, int numv)
+ {
+ if(endvert != int(lastblob->endvert) || endindex != int(lastblob->endindex)) dupblob();
+ for(const vec *cur = &v[2], *end = &v[numv];;)
+ {
+ int limit = maxverts - endvert - 2;
+ if(limit <= 0)
+ {
+ while(availverts < limit+2) if(!freeblob()) return;
+ availverts -= limit+2;
+ lastblob->endvert = maxverts;
+ endvert = 0;
+ dupblob();
+ limit = maxverts - 2;
+ }
+ limit = min(int(end - cur), min(limit, (maxindexes - endindex)/3));
+ while(availverts < limit+2) if(!freeblob()) return;
+ while(availindexes < limit*3) if(!freeblob()) return;
+
+ int i1 = addvert(v[0]), i2 = addvert(cur[-1]);
+ loopk(limit)
+ {
+ indexes[endindex++] = i1;
+ indexes[endindex++] = i2;
+ i2 = addvert(*cur++);
+ indexes[endindex++] = i2;
+ }
+
+ availverts -= endvert - lastblob->endvert;
+ availindexes -= endindex - lastblob->endindex;
+ lastblob->endvert = endvert;
+ lastblob->endindex = endindex;
+ if(endvert >= maxverts) endvert = 0;
+ if(endindex >= maxindexes) endindex = 0;
+
+ if(cur >= end) break;
+ dupblob();
+ }
+ }
+
+ void gentris(cube &cu, int orient, const ivec &o, int size, materialsurface *mat = NULL, int vismask = 0)
+ {
+ vec pos[MAXFACEVERTS+8];
+ int dim = dimension(orient), numverts = 0, numplanes = 1, flat = -1;
+ if(mat)
+ {
+ switch(orient)
+ {
+ #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+ case orient: v0 v1 v2 v3 break;
+ #define GENFACEVERT(orient, vert, x,y,z, xv,yv,zv) \
+ pos[numverts++] = vec(x xv, y yv, z zv);
+ GENFACEVERTS(o.x, o.x, o.y, o.y, o.z, o.z, , + mat->csize, , + mat->rsize, + 0.1f, - 0.1f);
+ #undef GENFACEORIENT
+ #undef GENFACEVERT
+ default:
+ return;
+ }
+ flat = dim;
+ }
+ else if(cu.texture[orient] == DEFAULT_SKY) return;
+ else if(cu.ext && (numverts = cu.ext->surfaces[orient].numverts&MAXFACEVERTS))
+ {
+ vertinfo *verts = cu.ext->verts() + cu.ext->surfaces[orient].verts;
+ ivec vo = ivec(o).mask(~0xFFF).shl(3);
+ loopj(numverts) pos[j] = vec(verts[j].getxyz().add(vo)).mul(1/8.0f);
+ if(numverts >= 4 && !(cu.merged&(1<<orient)) && !flataxisface(cu, orient) && faceconvexity(verts, numverts, size)) numplanes++;
+ else flat = dim;
+ }
+ else if(cu.merged&(1<<orient)) return;
+ else if(!vismask || (vismask&0x40 && visibleface(cu, orient, o, size, MAT_AIR, (cu.material&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA)))
+ {
+ ivec v[4];
+ genfaceverts(cu, orient, v);
+ int vis = 3, convex = faceconvexity(v, vis), order = convex < 0 ? 1 : 0;
+ vec vo(o);
+ pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+ if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+ pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+ if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+ if(convex) numplanes++;
+ else flat = dim;
+ }
+ else return;
+
+ if(flat >= 0)
+ {
+ float offset = pos[0][dim];
+ if(offset < blobmin[dim] || offset > blobmax[dim]) return;
+ flat = dim;
+ }
+
+ vec vmin = pos[0], vmax = pos[0];
+ for(int i = 1; i < numverts; i++) { vmin.min(pos[i]); vmax.max(pos[i]); }
+ if(vmax.x < blobmin.x || vmin.x > blobmax.x || vmax.y < blobmin.y || vmin.y > blobmax.y ||
+ vmax.z < blobmin.z || vmin.z > blobmax.z)
+ return;
+
+ vec v1[MAXFACEVERTS+6+4], v2[MAXFACEVERTS+6+4];
+ loopl(numplanes)
+ {
+ vec *v = pos;
+ int numv = numverts;
+ if(numplanes >= 2)
+ {
+ if(l) { pos[1] = pos[2]; pos[2] = pos[3]; }
+ numv = 3;
+ }
+ if(vec().cross(v[0], v[1], v[2]).z <= 0) continue;
+ #define CLIPSIDE(clip, below, above) \
+ { \
+ vec *in = v; \
+ v = in==v1 ? v2 : v1; \
+ numv = clip(in, numv, below, above, v); \
+ if(numv < 3) continue; \
+ }
+ if(flat!=0) CLIPSIDE(clip<0>, blobmin.x, blobmax.x);
+ if(flat!=1) CLIPSIDE(clip<1>, blobmin.y, blobmax.y);
+ if(flat!=2)
+ {
+ CLIPSIDE(clip<2>, blobmin.z, blobmax.z);
+ CLIPSIDE(split<2>, blobmin.z + blobfadelow, blobmax.z - blobfadehigh);
+ }
+ addtris(v, numv);
+ }
+ }
+
+ void findmaterials(vtxarray *va)
+ {
+ materialsurface *matbuf = va->matbuf;
+ int matsurfs = va->matsurfs;
+ loopi(matsurfs)
+ {
+ materialsurface &m = matbuf[i];
+ if(!isclipped(m.material&MATF_VOLUME) || m.orient == O_BOTTOM) { i += m.skip; continue; }
+ int dim = dimension(m.orient), c = C[dim], r = R[dim];
+ for(;;)
+ {
+ materialsurface &m = matbuf[i];
+ if(m.o[dim] >= blobmin[dim] && m.o[dim] <= blobmax[dim] &&
+ m.o[c] + m.csize >= blobmin[c] && m.o[c] <= blobmax[c] &&
+ m.o[r] + m.rsize >= blobmin[r] && m.o[r] <= blobmax[r])
+ {
+ static cube dummy;
+ gentris(dummy, m.orient, m.o, max(m.csize, m.rsize), &m);
+ }
+ if(i+1 >= matsurfs) break;
+ materialsurface &n = matbuf[i+1];
+ if(n.material != m.material || n.orient != m.orient) break;
+ i++;
+ }
+ }
+ }
+
+ void findescaped(cube *cu, const ivec &o, int size, int escaped)
+ {
+ loopi(8)
+ {
+ if(escaped&(1<<i))
+ {
+ ivec co(i, o, size);
+ if(cu[i].children) findescaped(cu[i].children, co, size>>1, cu[i].escaped);
+ else
+ {
+ int vismask = cu[i].merged;
+ if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+ }
+ }
+ }
+ }
+
+ void gentris(cube *cu, const ivec &o, int size, int escaped = 0)
+ {
+ int overlap = octaboxoverlap(o, size, bbmin, bbmax);
+ loopi(8)
+ {
+ if(overlap&(1<<i))
+ {
+ ivec co(i, o, size);
+ if(cu[i].ext && cu[i].ext->va && cu[i].ext->va->matsurfs)
+ findmaterials(cu[i].ext->va);
+ if(cu[i].children) gentris(cu[i].children, co, size>>1, cu[i].escaped);
+ else
+ {
+ int vismask = cu[i].visible;
+ if(vismask&0xC0)
+ {
+ if(vismask&0x80) loopj(6) gentris(cu[i], j, co, size, NULL, vismask);
+ else loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+ }
+ }
+ }
+ else if(escaped&(1<<i))
+ {
+ ivec co(i, o, size);
+ if(cu[i].children) findescaped(cu[i].children, co, size>>1, cu[i].escaped);
+ else
+ {
+ int vismask = cu[i].merged;
+ if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+ }
+ }
+ }
+ }
+
+ blobinfo *addblob(const vec &o, float radius, float fade)
+ {
+ lastblob = &blobs[endblob];
+ blobinfo &b = newblob(o, radius);
+ blobmin = blobmax = o;
+ blobmin.x -= radius;
+ blobmin.y -= radius;
+ blobmin.z -= blobheight + blobfadelow;
+ blobmax.x += radius;
+ blobmax.y += radius;
+ blobmax.z += blobfadehigh;
+ (bbmin = ivec(blobmin)).sub(2);
+ (bbmax = ivec(blobmax)).add(2);
+ float scale = fade*blobintensity*255/100.0f;
+ blobalphalow = scale / blobfadelow;
+ blobalphahigh = scale / blobfadehigh;
+ blobalpha = uchar(scale);
+ gentris(worldroot, ivec(0, 0, 0), worldsize>>1);
+ return !(b.flags & BL_DUP) ? &b : NULL;
+ }
+
+ static void setuprenderstate()
+ {
+ foggedshader->set();
+
+ enablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+
+ glDepthMask(GL_FALSE);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ if(!dbgblob) glEnable(GL_BLEND);
+
+ gle::enablevertex();
+ gle::enabletexcoord0();
+ gle::enablecolor();
+ }
+
+ static void cleanuprenderstate()
+ {
+ gle::disablevertex();
+ gle::disabletexcoord0();
+ gle::disablecolor();
+
+ gle::clearvbo();
+ if(glversion >= 300) gle::clearebo();
+
+ glDepthMask(GL_TRUE);
+ glDisable(GL_BLEND);
+
+ disablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+ }
+
+ static int lastreset;
+
+ static void reset()
+ {
+ lastreset = totalmillis;
+ }
+
+ static blobrenderer *lastrender;
+
+ void fadeblob(blobinfo *b, float fade)
+ {
+ float minz = b->o.z - (blobheight + blobfadelow), maxz = b->o.z + blobfadehigh,
+ scale = fade*blobintensity*255/100.0f, scalelow = scale / blobfadelow, scalehigh = scale / blobfadehigh;
+ uchar alpha = uchar(scale);
+ b->millis = totalmillis;
+ do
+ {
+ if(b->endvert - b->startvert >= 3) for(blobvert *v = &verts[b->startvert], *end = &verts[b->endvert]; v < end; v++)
+ {
+ float z = v->pos.z;
+ if(z < minz + blobfadelow) v->color.a = uchar(scalelow * (z - minz));
+ else if(z > maxz - blobfadehigh) v->color.a = uchar(scalehigh * (maxz - z));
+ else v->color.a = alpha;
+ }
+ int offset = b - &blobs[0] + 1;
+ if(offset >= maxblobs) offset = 0;
+ if(offset < endblob ? offset > startblob || startblob > endblob : offset > startblob) b = &blobs[offset];
+ else break;
+ } while(b->flags & BL_DUP);
+ }
+
+ void renderblob(const vec &o, float radius, float fade)
+ {
+ if(!blobs) initblobs();
+
+ if(glversion < 300 && lastrender != this)
+ {
+ if(!lastrender) setuprenderstate();
+ gle::vertexpointer(sizeof(blobvert), verts->pos.v);
+ gle::texcoord0pointer(sizeof(blobvert), verts->tc.v);
+ gle::colorpointer(sizeof(blobvert), verts->color.v);
+ if(!lastrender || lastrender->tex != tex) glBindTexture(GL_TEXTURE_2D, tex->id);
+ lastrender = this;
+ }
+
+ union { int i; float f; } ox, oy;
+ ox.f = o.x; oy.f = o.y;
+ uint hash = uint(ox.i^~oy.i^(INT_MAX-oy.i)^uint(radius));
+ hash %= cachesize;
+ blobinfo *b = &blobs[cache[hash]];
+ if(b >= &blobs[maxblobs] || b->millis - lastreset <= 0 || b->o!=o || b->radius!=radius)
+ {
+ b = addblob(o, radius, fade);
+ cache[hash] = ushort(b - blobs);
+ if(!b) return;
+ }
+ else if(fade < 1 && totalmillis - b->millis > 0) fadeblob(b, fade);
+ do
+ {
+ if(b->endvert - b->startvert >= 3)
+ {
+ if(glversion >= 300)
+ {
+ if(!startrender) { numedata = numvdata = 0; startrender = endrender = b; }
+ else { endrender->next = ushort(b - blobs); endrender = b; }
+ b->flags |= BL_RENDER;
+ b->next = 0xFFFF;
+ numedata += b->endindex - b->startindex;
+ numvdata += b->endvert - b->startvert;
+ }
+ else
+ {
+ glDrawRangeElements_(GL_TRIANGLES, b->startvert, b->endvert-1, b->endindex - b->startindex, GL_UNSIGNED_SHORT, &indexes[b->startindex]);
+ xtravertsva += b->endvert - b->startvert;
+ }
+ }
+ int offset = b - &blobs[0] + 1;
+ if(offset >= maxblobs) offset = 0;
+ if(offset < endblob ? offset > startblob || startblob > endblob : offset > startblob) b = &blobs[offset];
+ else break;
+ } while(b->flags & BL_DUP);
+ }
+
+ void flushblobs()
+ {
+ if(glversion < 300 || !startrender) return;
+
+ if(lastrender != this)
+ {
+ if(!lastrender) setuprenderstate();
+ lastrender = this;
+ }
+
+ if(!ebo) glGenBuffers_(1, &ebo);
+ if(!vbo) glGenBuffers_(1, &vbo);
+
+ gle::bindebo(ebo);
+ glBufferData_(GL_ELEMENT_ARRAY_BUFFER, maxindexes*sizeof(ushort), NULL, GL_STREAM_DRAW);
+ gle::bindvbo(vbo);
+ glBufferData_(GL_ARRAY_BUFFER, maxverts*sizeof(blobvert), NULL, GL_STREAM_DRAW);
+
+ ushort *estart;
+ blobvert *vstart;
+ if(intel_mapbufferrange_bug)
+ {
+ if(!edata) edata = new ushort[maxindexes];
+ if(!vdata) vdata = new blobvert[maxverts];
+ estart = edata;
+ vstart = vdata;
+ }
+ else
+ {
+ estart = (ushort *)glMapBufferRange_(GL_ELEMENT_ARRAY_BUFFER, 0, numedata*sizeof(ushort), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT|GL_MAP_UNSYNCHRONIZED_BIT);
+ vstart = (blobvert *)glMapBufferRange_(GL_ARRAY_BUFFER, 0, numvdata*sizeof(blobvert), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT|GL_MAP_UNSYNCHRONIZED_BIT);
+ if(!estart || !vstart)
+ {
+ if(estart) glUnmapBuffer_(GL_ELEMENT_ARRAY_BUFFER);
+ if(vstart) glUnmapBuffer_(GL_ARRAY_BUFFER);
+ for(blobinfo *b = startrender;; b = &blobs[b->next])
+ {
+ b->flags &= ~BL_RENDER;
+ if(b->next >= maxblobs) break;
+ }
+ startrender = endrender = NULL;
+ return;
+ }
+ }
+
+ ushort *edst = estart;
+ blobvert *vdst = vstart;
+ for(blobinfo *b = startrender;; b = &blobs[b->next])
+ {
+ b->flags &= ~BL_RENDER;
+ ushort offset = ushort(vdst - vstart) - b->startvert;
+ for(int i = b->startindex; i < b->endindex; ++i)
+ *edst++ = indexes[i] + offset;
+ memcpy(vdst, &verts[b->startvert], (b->endvert - b->startvert)*sizeof(blobvert));
+ vdst += b->endvert - b->startvert;
+ if(b->next >= maxblobs) break;
+ }
+ startrender = endrender = NULL;
+
+ if(intel_mapbufferrange_bug)
+ {
+ glBufferSubData_(GL_ELEMENT_ARRAY_BUFFER, 0, numedata*sizeof(ushort), estart);
+ glBufferSubData_(GL_ARRAY_BUFFER, 0, numvdata*sizeof(blobvert), vstart);
+ }
+ else
+ {
+ glUnmapBuffer_(GL_ELEMENT_ARRAY_BUFFER);
+ glUnmapBuffer_(GL_ARRAY_BUFFER);
+ }
+
+ const blobvert *ptr = 0;
+ gle::vertexpointer(sizeof(blobvert), ptr->pos.v);
+ gle::texcoord0pointer(sizeof(blobvert), ptr->tc.v);
+ gle::colorpointer(sizeof(blobvert), ptr->color.v);
+
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+
+ glDrawRangeElements_(GL_TRIANGLES, 0, numvdata-1, numedata, GL_UNSIGNED_SHORT, (ushort *)0);
+ }
+};
+
+int blobrenderer::lastreset = 0;
+blobrenderer *blobrenderer::lastrender = NULL;
+
+VARFP(blobstattris, 128, 4096, 16384, initblobs(BLOB_STATIC));
+VARFP(blobdyntris, 128, 4096, 16384, initblobs(BLOB_DYNAMIC));
+
+static blobrenderer blobs[] =
+{
+ blobrenderer("<grey>packages/particles/blob.png"),
+ blobrenderer("<grey>packages/particles/blob.png")
+};
+
+void initblobs(int type)
+{
+ if(type < 0 || (type==BLOB_STATIC && blobs[BLOB_STATIC].blobs)) blobs[BLOB_STATIC].init(showblobs ? blobstattris : 0);
+ if(type < 0 || (type==BLOB_DYNAMIC && blobs[BLOB_DYNAMIC].blobs)) blobs[BLOB_DYNAMIC].init(showblobs ? blobdyntris : 0);
+}
+
+void resetblobs()
+{
+ blobrenderer::reset();
+}
+
+void renderblob(int type, const vec &o, float radius, float fade)
+{
+ if(!showblobs) return;
+ if(refracting < 0 && o.z - blobheight - blobfadelow >= reflectz) return;
+ blobs[type].renderblob(o, radius + blobmargin, fade);
+}
+
+void flushblobs()
+{
+ loopi(sizeof(blobs)/sizeof(blobs[0])) blobs[i].flushblobs();
+ if(blobrenderer::lastrender) blobrenderer::cleanuprenderstate();
+ blobrenderer::lastrender = NULL;
+}
+
+void cleanupblobs()
+{
+ loopi(sizeof(blobs)/sizeof(blobs[0])) blobs[i].cleanup();
+}
+
diff --git a/src/engine/client.cpp b/src/engine/client.cpp
new file mode 100644
index 0000000..3cfef8e
--- /dev/null
+++ b/src/engine/client.cpp
@@ -0,0 +1,274 @@
+// client.cpp, mostly network related client game code
+
+#include "engine.h"
+
+ENetHost *clienthost = NULL;
+ENetPeer *curpeer = NULL, *connpeer = NULL;
+int connmillis = 0, connattempts = 0, discmillis = 0;
+
+bool multiplayer(bool msg)
+{
+ bool val = curpeer || hasnonlocalclients();
+ if(val && msg) conoutf(CON_ERROR, "operation not available in multiplayer");
+ return val;
+}
+
+void setrate(int rate)
+{
+ if(!curpeer) return;
+ enet_host_bandwidth_limit(clienthost, rate*1024, rate*1024);
+}
+
+VARF(rate, 0, 0, 1024, setrate(rate));
+
+void throttle();
+
+VARF(throttle_interval, 0, 5, 30, throttle());
+VARF(throttle_accel, 0, 2, 32, throttle());
+VARF(throttle_decel, 0, 2, 32, throttle());
+
+void throttle()
+{
+ if(!curpeer) return;
+ ASSERT(ENET_PEER_PACKET_THROTTLE_SCALE==32);
+ enet_peer_throttle_configure(curpeer, throttle_interval*1000, throttle_accel, throttle_decel);
+}
+
+bool isconnected(bool attempt, bool local)
+{
+ return curpeer || (attempt && connpeer) || (local && haslocalclients());
+}
+
+ICOMMAND(isconnected, "bb", (int *attempt, int *local), intret(isconnected(*attempt > 0, *local != 0) ? 1 : 0));
+
+const ENetAddress *connectedpeer()
+{
+ return curpeer ? &curpeer->address : NULL;
+}
+
+ICOMMAND(connectedip, "", (),
+{
+ const ENetAddress *address = connectedpeer();
+ string hostname;
+ result(address && enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0 ? hostname : "");
+});
+
+ICOMMAND(connectedport, "", (),
+{
+ const ENetAddress *address = connectedpeer();
+ intret(address ? address->port : -1);
+});
+
+void abortconnect()
+{
+ if(!connpeer) return;
+ game::connectfail();
+ if(connpeer->state!=ENET_PEER_STATE_DISCONNECTED) enet_peer_reset(connpeer);
+ connpeer = NULL;
+ if(curpeer) return;
+ enet_host_destroy(clienthost);
+ clienthost = NULL;
+}
+
+SVARP(connectname, "");
+VARP(connectport, 0, 0, 0xFFFF);
+
+void connectserv(const char *servername, int serverport, const char *serverpassword)
+{
+ if(connpeer)
+ {
+ conoutf("aborting connection attempt");
+ abortconnect();
+ }
+
+ if(serverport <= 0) serverport = server::serverport();
+
+ ENetAddress address;
+ address.port = serverport;
+
+ if(servername)
+ {
+ if(strcmp(servername, connectname)) setsvar("connectname", servername);
+ if(serverport != connectport) setvar("connectport", serverport);
+ addserver(servername, serverport, serverpassword && serverpassword[0] ? serverpassword : NULL);
+ conoutf("attempting to connect to %s:%d", servername, serverport);
+ if(!resolverwait(servername, &address))
+ {
+ conoutf(CON_ERROR, "\f3could not resolve server %s", servername);
+ return;
+ }
+ }
+ else
+ {
+ setsvar("connectname", "");
+ setvar("connectport", 0);
+ conoutf("attempting to connect over LAN");
+ address.host = ENET_HOST_BROADCAST;
+ }
+
+ if(!clienthost)
+ {
+ clienthost = enet_host_create(NULL, 2, server::numchannels(), rate*1024, rate*1024);
+ if(!clienthost)
+ {
+ conoutf(CON_ERROR, "\f3could not connect to server");
+ return;
+ }
+ clienthost->duplicatePeers = 0;
+ }
+
+ connpeer = enet_host_connect(clienthost, &address, server::numchannels(), 0);
+ enet_host_flush(clienthost);
+ connmillis = totalmillis;
+ connattempts = 0;
+
+ game::connectattempt(servername ? servername : "", serverpassword ? serverpassword : "", address);
+}
+
+void reconnect(const char *serverpassword)
+{
+ if(!connectname[0] || connectport <= 0)
+ {
+ conoutf(CON_ERROR, "no previous connection");
+ return;
+ }
+
+ connectserv(connectname, connectport, serverpassword);
+}
+
+void disconnect(bool async, bool cleanup)
+{
+ if(curpeer)
+ {
+ if(!discmillis)
+ {
+ enet_peer_disconnect(curpeer, DISC_NONE);
+ enet_host_flush(clienthost);
+ discmillis = totalmillis;
+ }
+ if(curpeer->state!=ENET_PEER_STATE_DISCONNECTED)
+ {
+ if(async) return;
+ enet_peer_reset(curpeer);
+ }
+ curpeer = NULL;
+ discmillis = 0;
+ conoutf("disconnected");
+ game::gamedisconnect(cleanup);
+ mainmenu = 1;
+ }
+ if(!connpeer && clienthost)
+ {
+ enet_host_destroy(clienthost);
+ clienthost = NULL;
+ }
+}
+
+void trydisconnect(bool local)
+{
+ if(connpeer)
+ {
+ conoutf("aborting connection attempt");
+ abortconnect();
+ }
+ else if(curpeer)
+ {
+ conoutf("attempting to disconnect...");
+ disconnect(!discmillis);
+ }
+ else if(local && haslocalclients()) localdisconnect();
+ else conoutf(CON_WARN, "not connected");
+}
+
+ICOMMAND(connect, "sis", (char *name, int *port, char *pw), connectserv(name, *port, pw));
+ICOMMAND(lanconnect, "is", (int *port, char *pw), connectserv(NULL, *port, pw));
+COMMAND(reconnect, "s");
+ICOMMAND(disconnect, "b", (int *local), trydisconnect(*local != 0));
+ICOMMAND(localconnect, "", (), { if(!isconnected()) localconnect(); });
+ICOMMAND(localdisconnect, "", (), { if(haslocalclients()) localdisconnect(); });
+
+void sendclientpacket(ENetPacket *packet, int chan)
+{
+ if(curpeer) enet_peer_send(curpeer, chan, packet);
+ else localclienttoserver(chan, packet);
+}
+
+void flushclient()
+{
+ if(clienthost) enet_host_flush(clienthost);
+}
+
+void neterr(const char *s, bool disc)
+{
+ conoutf(CON_ERROR, "\f3illegal network message (%s)", s);
+ if(disc) disconnect();
+}
+
+void localservertoclient(int chan, ENetPacket *packet) // processes any updates from the server
+{
+ packetbuf p(packet);
+ game::parsepacketclient(chan, p);
+}
+
+void clientkeepalive() { if(clienthost) enet_host_service(clienthost, NULL, 0); }
+
+void gets2c() // get updates from the server
+{
+ ENetEvent event;
+ if(!clienthost) return;
+ if(connpeer && totalmillis/3000 > connmillis/3000)
+ {
+ conoutf("attempting to connect...");
+ connmillis = totalmillis;
+ ++connattempts;
+ if(connattempts > 3)
+ {
+ conoutf(CON_ERROR, "\f3could not connect to server");
+ abortconnect();
+ return;
+ }
+ }
+ while(clienthost && enet_host_service(clienthost, &event, 0)>0)
+ switch(event.type)
+ {
+ case ENET_EVENT_TYPE_CONNECT:
+ disconnect(false, false);
+ localdisconnect(false);
+ curpeer = connpeer;
+ connpeer = NULL;
+ conoutf("connected to server");
+ throttle();
+ if(rate) setrate(rate);
+ game::gameconnect(true);
+ break;
+
+ case ENET_EVENT_TYPE_RECEIVE:
+ if(discmillis) conoutf("attempting to disconnect...");
+ else localservertoclient(event.channelID, event.packet);
+ enet_packet_destroy(event.packet);
+ break;
+
+ case ENET_EVENT_TYPE_DISCONNECT:
+ if(event.data>=DISC_NUM) event.data = DISC_NONE;
+ if(event.peer==connpeer)
+ {
+ conoutf(CON_ERROR, "\f3could not connect to server");
+ abortconnect();
+ }
+ else
+ {
+ if(!discmillis || event.data)
+ {
+ const char *msg = disconnectreason(event.data);
+ if(msg) conoutf(CON_ERROR, "\f3server network error, disconnecting (%s) ...", msg);
+ else conoutf(CON_ERROR, "\f3server network error, disconnecting...");
+ }
+ disconnect();
+ }
+ return;
+
+ default:
+ break;
+ }
+}
+
diff --git a/src/engine/command.cpp b/src/engine/command.cpp
new file mode 100644
index 0000000..74efff2
--- /dev/null
+++ b/src/engine/command.cpp
@@ -0,0 +1,3484 @@
+// command.cpp: implements the parsing and execution of a tiny script language which
+// is largely backwards compatible with the quake console language.
+
+#include "engine.h"
+
+hashnameset<ident> idents; // contains ALL vars/commands/aliases
+vector<ident *> identmap;
+ident *dummyident = NULL;
+
+int identflags = 0;
+
+enum
+{
+ MAXARGS = 25,
+ MAXCOMARGS = 12
+};
+
+VARN(numargs, _numargs, MAXARGS, 0, 0);
+
+static inline void freearg(tagval &v)
+{
+ switch(v.type)
+ {
+ case VAL_STR: delete[] v.s; break;
+ case VAL_CODE: if(v.code[-1] == CODE_START) delete[] (uchar *)&v.code[-1]; break;
+ }
+}
+
+static inline void forcenull(tagval &v)
+{
+ switch(v.type)
+ {
+ case VAL_NULL: return;
+ }
+ freearg(v);
+ v.setnull();
+}
+
+static inline float forcefloat(tagval &v)
+{
+ float f = 0.0f;
+ switch(v.type)
+ {
+ case VAL_INT: f = v.i; break;
+ case VAL_STR: f = parsefloat(v.s); break;
+ case VAL_MACRO: f = parsefloat(v.s); break;
+ case VAL_FLOAT: return v.f;
+ }
+ freearg(v);
+ v.setfloat(f);
+ return f;
+}
+
+static inline int forceint(tagval &v)
+{
+ int i = 0;
+ switch(v.type)
+ {
+ case VAL_FLOAT: i = v.f; break;
+ case VAL_STR: i = parseint(v.s); break;
+ case VAL_MACRO: i = parseint(v.s); break;
+ case VAL_INT: return v.i;
+ }
+ freearg(v);
+ v.setint(i);
+ return i;
+}
+
+static inline const char *forcestr(tagval &v)
+{
+ const char *s = "";
+ switch(v.type)
+ {
+ case VAL_FLOAT: s = floatstr(v.f); break;
+ case VAL_INT: s = intstr(v.i); break;
+ case VAL_STR: case VAL_MACRO: return v.s;
+ }
+ freearg(v);
+ v.setstr(newstring(s));
+ return s;
+}
+
+static inline void forcearg(tagval &v, int type)
+{
+ switch(type)
+ {
+ case RET_STR: if(v.type != VAL_STR) forcestr(v); break;
+ case RET_INT: if(v.type != VAL_INT) forceint(v); break;
+ case RET_FLOAT: if(v.type != VAL_FLOAT) forcefloat(v); break;
+ }
+}
+
+static inline ident *forceident(tagval &v)
+{
+ switch(v.type)
+ {
+ case VAL_IDENT: return v.id;
+ case VAL_MACRO:
+ {
+ ident *id = newident(v.s, IDF_UNKNOWN);
+ v.setident(id);
+ return id;
+ }
+ case VAL_STR:
+ {
+ ident *id = newident(v.s, IDF_UNKNOWN);
+ delete[] v.s;
+ v.setident(id);
+ return id;
+ }
+ }
+ freearg(v);
+ v.setident(dummyident);
+ return dummyident;
+}
+
+void tagval::cleanup()
+{
+ freearg(*this);
+}
+
+static inline void freeargs(tagval *args, int &oldnum, int newnum)
+{
+ for(int i = newnum; i < oldnum; i++) freearg(args[i]);
+ oldnum = newnum;
+}
+
+static inline void cleancode(ident &id)
+{
+ if(id.code)
+ {
+ id.code[0] -= 0x100;
+ if(int(id.code[0]) < 0x100) delete[] id.code;
+ id.code = NULL;
+ }
+}
+
+struct nullval : tagval
+{
+ nullval() { setnull(); }
+} nullval;
+tagval noret = nullval, *commandret = &noret;
+
+void clear_command()
+{
+ enumerate(idents, ident, i,
+ {
+ if(i.type==ID_ALIAS)
+ {
+ DELETEA(i.name);
+ i.forcenull();
+ DELETEA(i.code);
+ }
+ });
+}
+
+void clearoverride(ident &i)
+{
+ if(!(i.flags&IDF_OVERRIDDEN)) return;
+ switch(i.type)
+ {
+ case ID_ALIAS:
+ if(i.valtype==VAL_STR)
+ {
+ if(!i.val.s[0]) break;
+ delete[] i.val.s;
+ }
+ cleancode(i);
+ i.valtype = VAL_STR;
+ i.val.s = newstring("");
+ break;
+ case ID_VAR:
+ *i.storage.i = i.overrideval.i;
+ i.changed();
+ break;
+ case ID_FVAR:
+ *i.storage.f = i.overrideval.f;
+ i.changed();
+ break;
+ case ID_SVAR:
+ delete[] *i.storage.s;
+ *i.storage.s = i.overrideval.s;
+ i.changed();
+ break;
+ }
+ i.flags &= ~IDF_OVERRIDDEN;
+}
+
+void clearoverrides()
+{
+ enumerate(idents, ident, i, clearoverride(i));
+}
+
+static bool initedidents = false;
+static vector<ident> *identinits = NULL;
+
+static inline ident *addident(const ident &id)
+{
+ if(!initedidents)
+ {
+ if(!identinits) identinits = new vector<ident>;
+ identinits->add(id);
+ return NULL;
+ }
+ ident &def = idents.access(id.name, id);
+ def.index = identmap.length();
+ return identmap.add(&def);
+}
+
+static bool initidents()
+{
+ initedidents = true;
+ for(int i = 0; i < MAXARGS; i++)
+ {
+ defformatstring(argname, "arg%d", i+1);
+ newident(argname, IDF_ARG);
+ }
+ dummyident = newident("//dummy", IDF_UNKNOWN);
+ if(identinits)
+ {
+ loopv(*identinits) addident((*identinits)[i]);
+ DELETEP(identinits);
+ }
+ return true;
+}
+UNUSED static bool forceinitidents = initidents();
+
+static const char *sourcefile = NULL, *sourcestr = NULL;
+
+static const char *debugline(const char *p, const char *fmt)
+{
+ if(!sourcestr) return fmt;
+ int num = 1;
+ const char *line = sourcestr;
+ for(;;)
+ {
+ const char *end = strchr(line, '\n');
+ if(!end) end = line + strlen(line);
+ if(p >= line && p <= end)
+ {
+ static string buf;
+ if(sourcefile) formatstring(buf, "%s:%d: %s", sourcefile, num, fmt);
+ else formatstring(buf, "%d: %s", num, fmt);
+ return buf;
+ }
+ if(!*end) break;
+ line = end + 1;
+ num++;
+ }
+ return fmt;
+}
+
+static struct identlink
+{
+ ident *id;
+ identlink *next;
+ int usedargs;
+ identstack *argstack;
+} noalias = { NULL, NULL, (1<<MAXARGS)-1, NULL }, *aliasstack = &noalias;
+
+VAR(dbgalias, 0, 4, 1000);
+
+static void debugalias()
+{
+ if(!dbgalias) return;
+ int total = 0, depth = 0;
+ for(identlink *l = aliasstack; l != &noalias; l = l->next) total++;
+ for(identlink *l = aliasstack; l != &noalias; l = l->next)
+ {
+ ident *id = l->id;
+ ++depth;
+ if(depth < dbgalias) conoutf(CON_ERROR, " %d) %s", total-depth+1, id->name);
+ else if(l->next == &noalias) conoutf(CON_ERROR, depth == dbgalias ? " %d) %s" : " ..%d) %s", total-depth+1, id->name);
+ }
+}
+
+static int nodebug = 0;
+
+static void debugcode(const char *fmt, ...) PRINTFARGS(1, 2);
+
+static void debugcode(const char *fmt, ...)
+{
+ if(nodebug) return;
+
+ va_list args;
+ va_start(args, fmt);
+ conoutfv(CON_ERROR, fmt, args);
+ va_end(args);
+
+ debugalias();
+}
+
+static void debugcodeline(const char *p, const char *fmt, ...) PRINTFARGS(2, 3);
+
+static void debugcodeline(const char *p, const char *fmt, ...)
+{
+ if(nodebug) return;
+
+ va_list args;
+ va_start(args, fmt);
+ conoutfv(CON_ERROR, debugline(p, fmt), args);
+ va_end(args);
+
+ debugalias();
+}
+
+ICOMMAND(nodebug, "e", (uint *body), { nodebug++; executeret(body, *commandret); nodebug--; });
+
+void addident(ident *id)
+{
+ addident(*id);
+}
+
+static inline void pusharg(ident &id, const tagval &v, identstack &stack)
+{
+ stack.val = id.val;
+ stack.valtype = id.valtype;
+ stack.next = id.stack;
+ id.stack = &stack;
+ id.setval(v);
+ cleancode(id);
+}
+
+static inline void poparg(ident &id)
+{
+ if(!id.stack) return;
+ identstack *stack = id.stack;
+ if(id.valtype == VAL_STR) delete[] id.val.s;
+ id.setval(*stack);
+ cleancode(id);
+ id.stack = stack->next;
+}
+
+ICOMMAND(push, "rte", (ident *id, tagval *v, uint *code),
+{
+ if(id->type != ID_ALIAS || id->index < MAXARGS) return;
+ identstack stack;
+ pusharg(*id, *v, stack);
+ v->type = VAL_NULL;
+ id->flags &= ~IDF_UNKNOWN;
+ executeret(code, *commandret);
+ poparg(*id);
+});
+
+static inline void pushalias(ident &id, identstack &stack)
+{
+ if(id.type == ID_ALIAS && id.index >= MAXARGS)
+ {
+ pusharg(id, nullval, stack);
+ id.flags &= ~IDF_UNKNOWN;
+ }
+}
+
+static inline void popalias(ident &id)
+{
+ if(id.type == ID_ALIAS && id.index >= MAXARGS) poparg(id);
+}
+
+KEYWORD(local, ID_LOCAL);
+
+static inline bool checknumber(const char *s)
+{
+ if(isdigit(s[0])) return true;
+ else switch(s[0])
+ {
+ case '+': case '-': return isdigit(s[1]) || (s[1] == '.' && isdigit(s[2]));
+ case '.': return isdigit(s[1]) != 0;
+ default: return false;
+ }
+}
+
+ident *newident(const char *name, int flags)
+{
+ ident *id = idents.access(name);
+ if(!id)
+ {
+ if(checknumber(name))
+ {
+ debugcode("number %s is not a valid identifier name", name);
+ return dummyident;
+ }
+ id = addident(ident(ID_ALIAS, newstring(name), flags));
+ }
+ return id;
+}
+
+ident *writeident(const char *name, int flags)
+{
+ ident *id = newident(name, flags);
+ if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index)))
+ {
+ pusharg(*id, nullval, aliasstack->argstack[id->index]);
+ aliasstack->usedargs |= 1<<id->index;
+ }
+ return id;
+}
+
+ident *readident(const char *name)
+{
+ ident *id = idents.access(name);
+ if(id && id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index)))
+ return NULL;
+ return id;
+}
+
+void resetvar(char *name)
+{
+ ident *id = idents.access(name);
+ if(!id) return;
+ if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name);
+ else clearoverride(*id);
+}
+
+COMMAND(resetvar, "s");
+
+static inline void setarg(ident &id, tagval &v)
+{
+ if(aliasstack->usedargs&(1<<id.index))
+ {
+ if(id.valtype == VAL_STR) delete[] id.val.s;
+ id.setval(v);
+ cleancode(id);
+ }
+ else
+ {
+ pusharg(id, v, aliasstack->argstack[id.index]);
+ aliasstack->usedargs |= 1<<id.index;
+ }
+}
+
+static inline void setalias(ident &id, tagval &v)
+{
+ if(id.valtype == VAL_STR) delete[] id.val.s;
+ id.setval(v);
+ cleancode(id);
+ id.flags = (id.flags & identflags) | identflags;
+}
+
+static void setalias(const char *name, tagval &v)
+{
+ ident *id = idents.access(name);
+ if(id)
+ {
+ if(id->type == ID_ALIAS)
+ {
+ if(id->index < MAXARGS) setarg(*id, v); else setalias(*id, v);
+ }
+ else
+ {
+ debugcode("cannot redefine builtin %s with an alias", id->name);
+ freearg(v);
+ }
+ }
+ else if(checknumber(name))
+ {
+ debugcode("cannot alias number %s", name);
+ freearg(v);
+ }
+ else
+ {
+ addident(ident(ID_ALIAS, newstring(name), v, identflags));
+ }
+}
+
+void alias(const char *name, const char *str)
+{
+ tagval v;
+ v.setstr(newstring(str));
+ setalias(name, v);
+}
+
+void alias(const char *name, tagval &v)
+{
+ setalias(name, v);
+}
+
+ICOMMAND(alias, "st", (const char *name, tagval *v),
+{
+ setalias(name, *v);
+ v->type = VAL_NULL;
+});
+
+// variable's and commands are registered through globals, see cube.h
+
+int variable(const char *name, int min, int cur, int max, int *storage, identfun fun, int flags)
+{
+ addident(ident(ID_VAR, name, min, max, storage, (void *)fun, flags));
+ return cur;
+}
+
+float fvariable(const char *name, float min, float cur, float max, float *storage, identfun fun, int flags)
+{
+ addident(ident(ID_FVAR, name, min, max, storage, (void *)fun, flags));
+ return cur;
+}
+
+char *svariable(const char *name, const char *cur, char **storage, identfun fun, int flags)
+{
+ addident(ident(ID_SVAR, name, storage, (void *)fun, flags));
+ return newstring(cur);
+}
+
+#define _GETVAR(id, vartype, name, retval) \
+ ident *id = idents.access(name); \
+ if(!id || id->type!=vartype) return retval;
+#define GETVAR(id, name, retval) _GETVAR(id, ID_VAR, name, retval)
+#define OVERRIDEVAR(errorval, saveval, resetval, clearval) \
+ if(identflags&IDF_OVERRIDDEN || id->flags&IDF_OVERRIDE) \
+ { \
+ if(id->flags&IDF_PERSIST) \
+ { \
+ debugcode("cannot override persistent variable %s", id->name); \
+ errorval; \
+ } \
+ if(!(id->flags&IDF_OVERRIDDEN)) { saveval; id->flags |= IDF_OVERRIDDEN; } \
+ else { clearval; } \
+ } \
+ else \
+ { \
+ if(id->flags&IDF_OVERRIDDEN) { resetval; id->flags &= ~IDF_OVERRIDDEN; } \
+ clearval; \
+ }
+
+void setvar(const char *name, int i, bool dofunc, bool doclamp)
+{
+ GETVAR(id, name, );
+ OVERRIDEVAR(return, id->overrideval.i = *id->storage.i, , )
+ if(doclamp) *id->storage.i = clamp(i, id->minval, id->maxval);
+ else *id->storage.i = i;
+ if(dofunc) id->changed();
+}
+void setfvar(const char *name, float f, bool dofunc, bool doclamp)
+{
+ _GETVAR(id, ID_FVAR, name, );
+ OVERRIDEVAR(return, id->overrideval.f = *id->storage.f, , );
+ if(doclamp) *id->storage.f = clamp(f, id->minvalf, id->maxvalf);
+ else *id->storage.f = f;
+ if(dofunc) id->changed();
+}
+void setsvar(const char *name, const char *str, bool dofunc)
+{
+ _GETVAR(id, ID_SVAR, name, );
+ OVERRIDEVAR(return, id->overrideval.s = *id->storage.s, delete[] id->overrideval.s, delete[] *id->storage.s);
+ *id->storage.s = newstring(str);
+ if(dofunc) id->changed();
+}
+int getvar(const char *name)
+{
+ GETVAR(id, name, 0);
+ return *id->storage.i;
+}
+int getvarmin(const char *name)
+{
+ GETVAR(id, name, 0);
+ return id->minval;
+}
+int getvarmax(const char *name)
+{
+ GETVAR(id, name, 0);
+ return id->maxval;
+}
+float getfvarmin(const char *name)
+{
+ _GETVAR(id, ID_FVAR, name, 0);
+ return id->minvalf;
+}
+float getfvarmax(const char *name)
+{
+ _GETVAR(id, ID_FVAR, name, 0);
+ return id->maxvalf;
+}
+
+ICOMMAND(getvarmin, "s", (char *s), intret(getvarmin(s)));
+ICOMMAND(getvarmax, "s", (char *s), intret(getvarmax(s)));
+ICOMMAND(getfvarmin, "s", (char *s), floatret(getfvarmin(s)));
+ICOMMAND(getfvarmax, "s", (char *s), floatret(getfvarmax(s)));
+
+bool identexists(const char *name) { return idents.access(name)!=NULL; }
+ident *getident(const char *name) { return idents.access(name); }
+
+void touchvar(const char *name)
+{
+ ident *id = idents.access(name);
+ if(id) switch(id->type)
+ {
+ case ID_VAR:
+ case ID_FVAR:
+ case ID_SVAR:
+ id->changed();
+ break;
+ }
+}
+
+const char *getalias(const char *name)
+{
+ ident *i = idents.access(name);
+ return i && i->type==ID_ALIAS && (i->index >= MAXARGS || aliasstack->usedargs&(1<<i->index)) ? i->getstr() : "";
+}
+
+ICOMMAND(getalias, "s", (char *s), result(getalias(s)));
+
+int clampvar(ident *id, int val, int minval, int maxval)
+{
+ if(val < minval) val = minval;
+ else if(val > maxval) val = maxval;
+ else return val;
+ debugcode(id->flags&IDF_HEX ?
+ (minval <= 255 ? "valid range for %s is %d..0x%X" : "valid range for %s is 0x%X..0x%X") :
+ "valid range for %s is %d..%d",
+ id->name, minval, maxval);
+ return val;
+}
+
+void setvarchecked(ident *id, int val)
+{
+ if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name);
+#ifndef STANDALONE
+ else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle())
+#else
+ else
+#endif
+ {
+ OVERRIDEVAR(return, id->overrideval.i = *id->storage.i, , )
+ if(val < id->minval || val > id->maxval) val = clampvar(id, val, id->minval, id->maxval);
+ *id->storage.i = val;
+ id->changed(); // call trigger function if available
+#ifndef STANDALONE
+ if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id);
+#endif
+ }
+}
+
+static inline void setvarchecked(ident *id, tagval *args, int numargs)
+{
+ int val = forceint(args[0]);
+ if(id->flags&IDF_HEX && numargs > 1)
+ {
+ val = (val << 16) | (forceint(args[1])<<8);
+ if(numargs > 2) val |= forceint(args[2]);
+ }
+ setvarchecked(id, val);
+}
+
+float clampfvar(ident *id, float val, float minval, float maxval)
+{
+ if(val < minval) val = minval;
+ else if(val > maxval) val = maxval;
+ else return val;
+ debugcode("valid range for %s is %s..%s", id->name, floatstr(minval), floatstr(maxval));
+ return val;
+}
+
+void setfvarchecked(ident *id, float val)
+{
+ if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name);
+#ifndef STANDALONE
+ else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle())
+#else
+ else
+#endif
+ {
+ OVERRIDEVAR(return, id->overrideval.f = *id->storage.f, , );
+ if(val < id->minvalf || val > id->maxvalf) val = clampfvar(id, val, id->minvalf, id->maxvalf);
+ *id->storage.f = val;
+ id->changed();
+#ifndef STANDALONE
+ if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id);
+#endif
+ }
+}
+
+void setsvarchecked(ident *id, const char *val)
+{
+ if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name);
+#ifndef STANDALONE
+ else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle())
+#else
+ else
+#endif
+ {
+ OVERRIDEVAR(return, id->overrideval.s = *id->storage.s, delete[] id->overrideval.s, delete[] *id->storage.s);
+ *id->storage.s = newstring(val);
+ id->changed();
+#ifndef STANDALONE
+ if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id);
+#endif
+ }
+}
+
+ICOMMAND(set, "rt", (ident *id, tagval *v),
+{
+ switch(id->type)
+ {
+ case ID_ALIAS:
+ if(id->index < MAXARGS) setarg(*id, *v); else setalias(*id, *v);
+ v->type = VAL_NULL;
+ break;
+ case ID_VAR:
+ setvarchecked(id, forceint(*v));
+ break;
+ case ID_FVAR:
+ setfvarchecked(id, forcefloat(*v));
+ break;
+ case ID_SVAR:
+ setsvarchecked(id, forcestr(*v));
+ break;
+ case ID_COMMAND:
+ if(id->flags&IDF_EMUVAR)
+ {
+ execute(id, v, 1);
+ v->type = VAL_NULL;
+ break;
+ }
+ // fall through
+ default:
+ debugcode("cannot redefine builtin %s with an alias", id->name);
+ break;
+ }
+});
+
+bool addcommand(const char *name, identfun fun, const char *args)
+{
+ uint argmask = 0;
+ int numargs = 0, flags = 0;
+ bool limit = true;
+ for(const char *fmt = args; *fmt; fmt++) switch(*fmt)
+ {
+ case 'i': case 'b': case 'f': case 't': case 'N': case 'D': if(numargs < MAXARGS) numargs++; break;
+ case '$': flags |= IDF_EMUVAR; // fall through
+ case 's': case 'e': case 'r': if(numargs < MAXARGS) { argmask |= 1<<numargs; numargs++; } break;
+ case '1': case '2': case '3': case '4': if(numargs < MAXARGS) fmt -= *fmt-'0'+1; break;
+ case 'C': case 'V': limit = false; break;
+ default: fatal("builtin %s declared with illegal type: %s", name, args); break;
+ }
+ if(limit && numargs > MAXCOMARGS) fatal("builtin %s declared with too many args: %d", name, numargs);
+ addident(ident(ID_COMMAND, name, args, argmask, numargs, (void *)fun, flags));
+ return false;
+}
+
+bool addkeyword(int type, const char *name)
+{
+ addident(ident(type, name, "", 0, 0, NULL));
+ return true;
+}
+
+const char *parsestring(const char *p)
+{
+ for(; *p; p++) switch(*p)
+ {
+ case '\r':
+ case '\n':
+ case '\"':
+ return p;
+ case '^':
+ if(*++p) break;
+ return p;
+ }
+ return p;
+}
+
+int unescapestring(char *dst, const char *src, const char *end)
+{
+ char *start = dst;
+ while(src < end)
+ {
+ int c = *src++;
+ if(c == '^')
+ {
+ if(src >= end) break;
+ int e = *src++;
+ switch(e)
+ {
+ case 'n': *dst++ = '\n'; break;
+ case 't': *dst++ = '\t'; break;
+ case 'f': *dst++ = '\f'; break;
+ default: *dst++ = e; break;
+ }
+ }
+ else *dst++ = c;
+ }
+ return dst - start;
+}
+
+static char *conc(vector<char> &buf, tagval *v, int n, bool space, const char *prefix = NULL, int prefixlen = 0)
+{
+ if(prefix)
+ {
+ buf.put(prefix, prefixlen);
+ if(space && n) buf.add(' ');
+ }
+ loopi(n)
+ {
+ const char *s = "";
+ int len = 0;
+ switch(v[i].type)
+ {
+ case VAL_INT: s = intstr(v[i].i); break;
+ case VAL_FLOAT: s = floatstr(v[i].f); break;
+ case VAL_STR: s = v[i].s; break;
+ case VAL_MACRO: s = v[i].s; len = v[i].code[-1]>>8; goto haslen;
+ }
+ len = int(strlen(s));
+ haslen:
+ buf.put(s, len);
+ if(i == n-1) break;
+ if(space) buf.add(' ');
+ }
+ buf.add('\0');
+ return buf.getbuf();
+}
+
+static char *conc(tagval *v, int n, bool space, const char *prefix, int prefixlen)
+{
+ static int vlen[MAXARGS];
+ static char numbuf[3*MAXSTRLEN];
+ int len = prefixlen, numlen = 0, i = 0;
+ for(; i < n; i++) switch(v[i].type)
+ {
+ case VAL_MACRO: len += (vlen[i] = v[i].code[-1]>>8); break;
+ case VAL_STR: len += (vlen[i] = int(strlen(v[i].s))); break;
+ case VAL_INT:
+ if(numlen + MAXSTRLEN > int(sizeof(numbuf))) goto overflow;
+ intformat(&numbuf[numlen], v[i].i);
+ numlen += (vlen[i] = strlen(&numbuf[numlen]));
+ break;
+ case VAL_FLOAT:
+ if(numlen + MAXSTRLEN > int(sizeof(numbuf))) goto overflow;
+ floatformat(&numbuf[numlen], v[i].f);
+ numlen += (vlen[i] = strlen(&numbuf[numlen]));
+ break;
+ default: vlen[i] = 0; break;
+ }
+overflow:
+ if(space) len += max(prefix ? i : i-1, 0);
+ char *buf = newstring(len + numlen);
+ int offset = 0, numoffset = 0;
+ if(prefix)
+ {
+ memcpy(buf, prefix, prefixlen);
+ offset += prefixlen;
+ if(space && i) buf[offset++] = ' ';
+ }
+ loopj(i)
+ {
+ if(v[j].type == VAL_INT || v[j].type == VAL_FLOAT)
+ {
+ memcpy(&buf[offset], &numbuf[numoffset], vlen[j]);
+ numoffset += vlen[j];
+ }
+ else if(vlen[j]) memcpy(&buf[offset], v[j].s, vlen[j]);
+ offset += vlen[j];
+ if(j==i-1) break;
+ if(space) buf[offset++] = ' ';
+ }
+ buf[offset] = '\0';
+ if(i < n)
+ {
+ char *morebuf = conc(&v[i], n-i, space, buf, offset);
+ delete[] buf;
+ return morebuf;
+ }
+ return buf;
+}
+
+static inline char *conc(tagval *v, int n, bool space)
+{
+ return conc(v, n, space, NULL, 0);
+}
+
+static inline char *conc(tagval *v, int n, bool space, const char *prefix)
+{
+ return conc(v, n, space, prefix, strlen(prefix));
+}
+
+static inline void skipcomments(const char *&p)
+{
+ for(;;)
+ {
+ p += strspn(p, " \t\r");
+ if(p[0]!='/' || p[1]!='/') break;
+ p += strcspn(p, "\n\0");
+ }
+}
+
+static inline char *cutstring(const char *&p, int &len)
+{
+ p++;
+ const char *end = parsestring(p);
+ char *s = newstring(end - p);
+ len = unescapestring(s, p, end);
+ s[len] = '\0';
+ p = end;
+ if(*p=='\"') p++;
+ return s;
+}
+
+static inline const char *parseword(const char *p)
+{
+ const int maxbrak = 100;
+ static char brakstack[maxbrak];
+ int brakdepth = 0;
+ for(;; p++)
+ {
+ p += strcspn(p, "\"/;()[] \t\r\n\0");
+ switch(p[0])
+ {
+ case '"': case ';': case ' ': case '\t': case '\r': case '\n': case '\0': return p;
+ case '/': if(p[1] == '/') return p; break;
+ case '[': case '(': if(brakdepth >= maxbrak) return p; brakstack[brakdepth++] = p[0]; break;
+ case ']': if(brakdepth <= 0 || brakstack[--brakdepth] != '[') return p; break;
+ case ')': if(brakdepth <= 0 || brakstack[--brakdepth] != '(') return p; break;
+ }
+ }
+ return p;
+}
+
+static inline char *cutword(const char *&p, int &len)
+{
+ const char *word = p;
+ p = parseword(p);
+ len = p-word;
+ if(!len) return NULL;
+ return newstring(word, len);
+}
+
+static inline void compilestr(vector<uint> &code, const char *word, int len, bool macro = false)
+{
+ if(len <= 3 && !macro)
+ {
+ uint op = CODE_VALI|RET_STR;
+ for(int i = 0; i < len; i++) op |= uint(uchar(word[i]))<<((i+1)*8);
+ code.add(op);
+ return;
+ }
+ code.add((macro ? CODE_MACRO : CODE_VAL|RET_STR)|(len<<8));
+ code.put((const uint *)word, len/sizeof(uint));
+ size_t endlen = len%sizeof(uint);
+ union
+ {
+ char c[sizeof(uint)];
+ uint u;
+ } end;
+ end.u = 0;
+ memcpy(end.c, word + len - endlen, endlen);
+ code.add(end.u);
+}
+
+static inline void compilestr(vector<uint> &code, const char *word = NULL)
+{
+ if(!word) { code.add(CODE_VALI|RET_STR); return; }
+ compilestr(code, word, int(strlen(word)));
+}
+
+static inline void compileint(vector<uint> &code, int i)
+{
+ if(i >= -0x800000 && i <= 0x7FFFFF)
+ code.add(CODE_VALI|RET_INT|(i<<8));
+ else
+ {
+ code.add(CODE_VAL|RET_INT);
+ code.add(i);
+ }
+}
+
+static inline void compilenull(vector<uint> &code)
+{
+ code.add(CODE_VALI|RET_NULL);
+}
+
+static inline void compileblock(vector<uint> &code)
+{
+ int start = code.length();
+ code.add(CODE_BLOCK);
+ code.add(CODE_OFFSET|((start+2)<<8));
+ code.add(CODE_EXIT);
+ code[start] |= uint(code.length() - (start + 1))<<8;
+}
+
+static inline void compileident(vector<uint> &code, ident *id)
+{
+ code.add((id->index < MAXARGS ? CODE_IDENTARG : CODE_IDENT)|(id->index<<8));
+}
+
+static inline void compileident(vector<uint> &code, const char *word = NULL)
+{
+ compileident(code, word ? newident(word, IDF_UNKNOWN) : dummyident);
+}
+
+static inline void compileint(vector<uint> &code, const char *word = NULL)
+{
+ return compileint(code, word ? parseint(word) : 0);
+}
+
+static inline void compilefloat(vector<uint> &code, float f)
+{
+ if(int(f) == f && f >= -0x800000 && f <= 0x7FFFFF)
+ code.add(CODE_VALI|RET_FLOAT|(int(f)<<8));
+ else
+ {
+ union { float f; uint u; } conv;
+ conv.f = f;
+ code.add(CODE_VAL|RET_FLOAT);
+ code.add(conv.u);
+ }
+}
+
+static inline void compilefloat(vector<uint> &code, const char *word = NULL)
+{
+ return compilefloat(code, word ? parsefloat(word) : 0.0f);
+}
+
+static bool compilearg(vector<uint> &code, const char *&p, int wordtype);
+static void compilestatements(vector<uint> &code, const char *&p, int rettype, int brak = '\0');
+
+static inline void compileval(vector<uint> &code, int wordtype, char *word, int wordlen)
+{
+ switch(wordtype)
+ {
+ case VAL_STR: compilestr(code, word, wordlen, true); break;
+ case VAL_ANY: compilestr(code, word, wordlen); break;
+ case VAL_FLOAT: compilefloat(code, word); break;
+ case VAL_INT: compileint(code, word); break;
+ case VAL_CODE:
+ {
+ int start = code.length();
+ code.add(CODE_BLOCK);
+ code.add(CODE_OFFSET|((start+2)<<8));
+ const char *p = word;
+ if(p) compilestatements(code, p, VAL_ANY);
+ code.add(CODE_EXIT|RET_STR);
+ code[start] |= uint(code.length() - (start + 1))<<8;
+ break;
+ }
+ case VAL_IDENT: compileident(code, word); break;
+ default:
+ break;
+ }
+}
+
+static bool compileword(vector<uint> &code, const char *&p, int wordtype, char *&word, int &wordlen);
+
+static void compilelookup(vector<uint> &code, const char *&p, int ltype)
+{
+ char *lookup = NULL;
+ int lookuplen = 0;
+ switch(*++p)
+ {
+ case '(':
+ case '[':
+ if(!compileword(code, p, VAL_STR, lookup, lookuplen)) goto invalid;
+ break;
+ case '$':
+ compilelookup(code, p, VAL_STR);
+ break;
+ case '\"':
+ lookup = cutstring(p, lookuplen);
+ goto lookupid;
+ default:
+ {
+ lookup = cutword(p, lookuplen);
+ if(!lookup) goto invalid;
+ lookupid:
+ ident *id = newident(lookup, IDF_UNKNOWN);
+ if(id) switch(id->type)
+ {
+ case ID_VAR: code.add(CODE_IVAR|((ltype >= VAL_ANY ? VAL_INT : ltype)<<CODE_RET)|(id->index<<8)); goto done;
+ case ID_FVAR: code.add(CODE_FVAR|((ltype >= VAL_ANY ? VAL_FLOAT : ltype)<<CODE_RET)|(id->index<<8)); goto done;
+ case ID_SVAR: code.add(CODE_SVAR|((ltype >= VAL_ANY ? VAL_STR : ltype)<<CODE_RET)|(id->index<<8)); goto done;
+ case ID_ALIAS: code.add((id->index < MAXARGS ? CODE_LOOKUPARG : CODE_LOOKUP)|((ltype >= VAL_ANY ? VAL_STR : ltype)<<CODE_RET)|(id->index<<8)); goto done;
+ case ID_COMMAND:
+ {
+ int comtype = CODE_COM, numargs = 0;
+ code.add(CODE_ENTER);
+ for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt)
+ {
+ case 's': compilestr(code, NULL, 0, true); numargs++; break;
+ case 'i': compileint(code); numargs++; break;
+ case 'b': compileint(code, INT_MIN); numargs++; break;
+ case 'f': compilefloat(code); numargs++; break;
+ case 't': compilenull(code); numargs++; break;
+ case 'e': compileblock(code); numargs++; break;
+ case 'r': compileident(code); numargs++; break;
+ case '$': compileident(code, id); numargs++; break;
+ case 'N': compileint(code, -1); numargs++; break;
+#ifndef STANDALONE
+ case 'D': comtype = CODE_COMD; numargs++; break;
+#endif
+ case 'C': comtype = CODE_COMC; numargs = 1; goto endfmt;
+ case 'V': comtype = CODE_COMV; numargs = 2; goto endfmt;
+ case '1': case '2': case '3': case '4': break;
+ }
+ endfmt:
+ code.add(comtype|(ltype < VAL_ANY ? ltype<<CODE_RET : 0)|(id->index<<8));
+ code.add(CODE_EXIT|(ltype < VAL_ANY ? ltype<<CODE_RET : 0));
+ goto done;
+ }
+ default: goto invalid;
+ }
+ compilestr(code, lookup, lookuplen, true);
+ break;
+ }
+ }
+ code.add(CODE_LOOKUPU|((ltype < VAL_ANY ? ltype<<CODE_RET : 0)));
+done:
+ delete[] lookup;
+ switch(ltype)
+ {
+ case VAL_CODE: code.add(CODE_COMPILE); break;
+ case VAL_IDENT: code.add(CODE_IDENTU); break;
+ }
+ return;
+invalid:
+ switch(ltype)
+ {
+ case VAL_NULL: case VAL_ANY: compilenull(code); break;
+ default: compileval(code, ltype, NULL, 0); break;
+ }
+}
+
+static bool compileblockstr(vector<uint> &code, const char *str, const char *end, bool macro)
+{
+ int start = code.length();
+ code.add(macro ? CODE_MACRO : CODE_VAL|RET_STR);
+ char *buf = (char *)code.reserve((end-str)/sizeof(uint)+1).buf;
+ int len = 0;
+ while(str < end)
+ {
+ int n = strcspn(str, "\r/\"@]\0");
+ memcpy(&buf[len], str, n);
+ len += n;
+ str += n;
+ switch(*str)
+ {
+ case '\r': str++; break;
+ case '\"':
+ {
+ const char *start = str;
+ str = parsestring(str+1);
+ if(*str=='\"') str++;
+ memcpy(&buf[len], start, str-start);
+ len += str-start;
+ break;
+ }
+ case '/':
+ if(str[1] == '/')
+ {
+ size_t comment = strcspn(str, "\n\0");
+ if (iscubepunct(str[2]))
+ {
+ memcpy(&buf[len], str, comment);
+ len += comment;
+ }
+ str += comment;
+ }
+ else buf[len++] = *str++;
+ break;
+ case '@':
+ case ']':
+ if(str < end) { buf[len++] = *str++; break; }
+ case '\0': goto done;
+ }
+ }
+done:
+ memset(&buf[len], '\0', sizeof(uint)-len%sizeof(uint));
+ code.advance(len/sizeof(uint)+1);
+ code[start] |= len<<8;
+ return true;
+}
+
+static bool compileblocksub(vector<uint> &code, const char *&p)
+{
+ char *lookup = NULL;
+ int lookuplen = 0;
+ switch(*p)
+ {
+ case '(':
+ if(!compilearg(code, p, VAL_STR)) return false;
+ break;
+ case '[':
+ if(!compilearg(code, p, VAL_STR)) return false;
+ code.add(CODE_LOOKUPU|RET_STR);
+ break;
+ case '\"':
+ lookup = cutstring(p, lookuplen);
+ goto lookupid;
+ default:
+ {
+ {
+ const char *start = p;
+ while(iscubealnum(*p) || *p=='_') p++;
+ lookuplen = p-start;
+ if(!lookuplen) return false;
+ lookup = newstring(start, lookuplen);
+ }
+ lookupid:
+ ident *id = newident(lookup, IDF_UNKNOWN);
+ if(id) switch(id->type)
+ {
+ case ID_VAR: code.add(CODE_IVAR|RET_STR|(id->index<<8)); goto done;
+ case ID_FVAR: code.add(CODE_FVAR|RET_STR|(id->index<<8)); goto done;
+ case ID_SVAR: code.add(CODE_SVAR|RET_STR|(id->index<<8)); goto done;
+ case ID_ALIAS: code.add((id->index < MAXARGS ? CODE_LOOKUPARG : CODE_LOOKUP)|RET_STR|(id->index<<8)); goto done;
+ }
+ compilestr(code, lookup, lookuplen, true);
+ code.add(CODE_LOOKUPU|RET_STR);
+ done:
+ delete[] lookup;
+ break;
+ }
+ }
+ return true;
+}
+
+static void compileblock(vector<uint> &code, const char *&p, int wordtype)
+{
+ const char *line = p, *start = p;
+ int concs = 0;
+ for(int brak = 1; brak;)
+ {
+ p += strcspn(p, "@\"/[]\0");
+ int c = *p++;
+ switch(c)
+ {
+ case '\0':
+ debugcodeline(line, "missing \"]\"");
+ p--;
+ goto done;
+ case '\"':
+ p = parsestring(p);
+ if(*p=='\"') p++;
+ break;
+ case '/':
+ if(*p=='/') p += strcspn(p, "\n\0");
+ break;
+ case '[': brak++; break;
+ case ']': brak--; break;
+ case '@':
+ {
+ const char *esc = p;
+ while(*p == '@') p++;
+ int level = p - (esc - 1);
+ if(brak > level) continue;
+ else if(brak < level) debugcodeline(line, "too many @s");
+ if(!concs) code.add(CODE_ENTER);
+ if(concs + 2 > MAXARGS)
+ {
+ code.add(CODE_CONCW|RET_STR|(concs<<8));
+ concs = 1;
+ }
+ if(compileblockstr(code, start, esc-1, true)) concs++;
+ if(compileblocksub(code, p)) concs++;
+ if(!concs) code.pop();
+ else start = p;
+ break;
+ }
+ }
+ }
+done:
+ if(p-1 > start)
+ {
+ if(!concs) switch(wordtype)
+ {
+ case VAL_CODE:
+ {
+ p = start;
+ int inst = code.length();
+ code.add(CODE_BLOCK);
+ code.add(CODE_OFFSET|((inst+2)<<8));
+ compilestatements(code, p, VAL_ANY, ']');
+ code.add(CODE_EXIT);
+ code[inst] |= uint(code.length() - (inst + 1))<<8;
+ return;
+ }
+ case VAL_IDENT:
+ {
+ char *name = newstring(start, p-1-start);
+ compileident(code, name);
+ delete[] name;
+ return;
+ }
+ }
+ compileblockstr(code, start, p-1, concs > 0);
+ if(concs > 1) concs++;
+ }
+ if(concs)
+ {
+ code.add(CODE_CONCM|(wordtype < VAL_ANY ? wordtype<<CODE_RET : RET_STR)|(concs<<8));
+ code.add(CODE_EXIT|(wordtype < VAL_ANY ? wordtype<<CODE_RET : RET_STR));
+ }
+ switch(wordtype)
+ {
+ case VAL_CODE: if(!concs && p-1 <= start) compileblock(code); else code.add(CODE_COMPILE); break;
+ case VAL_IDENT: if(!concs && p-1 <= start) compileident(code); else code.add(CODE_IDENTU); break;
+ case VAL_STR: case VAL_NULL: case VAL_ANY:
+ if(!concs && p-1 <= start) compilestr(code);
+ break;
+ default:
+ if(!concs)
+ {
+ if(p-1 <= start) compileval(code, wordtype, NULL, 0);
+ else code.add(CODE_FORCE|(wordtype<<CODE_RET));
+ }
+ break;
+ }
+}
+
+static bool compileword(vector<uint> &code, const char *&p, int wordtype, char *&word, int &wordlen)
+{
+ skipcomments(p);
+ switch(*p)
+ {
+ case '\"': word = cutstring(p, wordlen); break;
+ case '$': compilelookup(code, p, wordtype); return true;
+ case '(':
+ p++;
+ code.add(CODE_ENTER);
+ compilestatements(code, p, VAL_ANY, ')');
+ code.add(CODE_EXIT|(wordtype < VAL_ANY ? wordtype<<CODE_RET : 0));
+ switch(wordtype)
+ {
+ case VAL_CODE: code.add(CODE_COMPILE); break;
+ case VAL_IDENT: code.add(CODE_IDENTU); break;
+ }
+ return true;
+ case '[':
+ p++;
+ compileblock(code, p, wordtype);
+ return true;
+ default: word = cutword(p, wordlen); break;
+ }
+ return word!=NULL;
+}
+
+static inline bool compilearg(vector<uint> &code, const char *&p, int wordtype)
+{
+ char *word = NULL;
+ int wordlen = 0;
+ bool more = compileword(code, p, wordtype, word, wordlen);
+ if(!more) return false;
+ if(word)
+ {
+ compileval(code, wordtype, word, wordlen);
+ delete[] word;
+ }
+ return true;
+}
+
+static void compilestatements(vector<uint> &code, const char *&p, int rettype, int brak)
+{
+ const char *line = p;
+ char *idname = NULL;
+ int idlen = 0;
+ ident *id = NULL;
+ int numargs = 0;
+ for(;;)
+ {
+ skipcomments(p);
+ idname = NULL;
+ bool more = compileword(code, p, VAL_ANY, idname, idlen);
+ if(!more) goto endstatement;
+ skipcomments(p);
+ if(p[0] == '=') switch(p[1])
+ {
+ case '/':
+ if(p[2] != '/') break;
+ case ';': case ' ': case '\t': case '\r': case '\n': case '\0':
+ p++;
+ if(idname)
+ {
+ id = newident(idname, IDF_UNKNOWN);
+ if(id) switch(id->type)
+ {
+ case ID_ALIAS:
+ if(!(more = compilearg(code, p, VAL_ANY))) compilestr(code);
+ code.add((id->index < MAXARGS ? CODE_ALIASARG : CODE_ALIAS)|(id->index<<8));
+ goto endcommand;
+ case ID_VAR:
+ if(!(more = compilearg(code, p, VAL_INT))) compileint(code);
+ code.add(CODE_IVAR1|(id->index<<8));
+ goto endcommand;
+ case ID_FVAR:
+ if(!(more = compilearg(code, p, VAL_FLOAT))) compilefloat(code);
+ code.add(CODE_FVAR1|(id->index<<8));
+ goto endcommand;
+ case ID_SVAR:
+ if(!(more = compilearg(code, p, VAL_STR))) compilestr(code);
+ code.add(CODE_SVAR1|(id->index<<8));
+ goto endcommand;
+ case ID_COMMAND:
+ if(id->flags&IDF_EMUVAR) goto compilecommand;
+ break;
+ }
+ compilestr(code, idname, idlen, true);
+ delete[] idname;
+ }
+ if(!(more = compilearg(code, p, VAL_ANY))) compilestr(code);
+ code.add(CODE_ALIASU);
+ goto endstatement;
+ }
+ compilecommand:
+ numargs = 0;
+ if(!idname)
+ {
+ noid:
+ while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++;
+ code.add(CODE_CALLU);
+ }
+ else
+ {
+ id = idents.access(idname);
+ if(!id)
+ {
+ if(!checknumber(idname)) { compilestr(code, idname, idlen); delete[] idname; goto noid; }
+ char *end = idname;
+ int val = int(strtoul(idname, &end, 0));
+ if(*end) compilestr(code, idname, idlen);
+ else compileint(code, val);
+ code.add(CODE_RESULT);
+ }
+ else switch(id->type)
+ {
+ case ID_ALIAS:
+ while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++;
+ code.add((id->index < MAXARGS ? CODE_CALLARG : CODE_CALL)|(id->index<<8));
+ break;
+ case ID_COMMAND:
+ {
+ int comtype = CODE_COM, fakeargs = 0;
+ bool rep = false;
+ for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt)
+ {
+ case 's':
+ if(more) more = compilearg(code, p, VAL_STR);
+ if(!more)
+ {
+ if(rep) break;
+ compilestr(code, NULL, 0, true);
+ fakeargs++;
+ }
+ else if(!fmt[1])
+ {
+ int numconc = 0;
+ while(numargs + numconc < MAXARGS && (more = compilearg(code, p, VAL_STR))) numconc++;
+ if(numconc > 0) code.add(CODE_CONC|RET_STR|((numconc+1)<<8));
+ }
+ numargs++;
+ break;
+ case 'i': if(more) more = compilearg(code, p, VAL_INT); if(!more) { if(rep) break; compileint(code); fakeargs++; } numargs++; break;
+ case 'b': if(more) more = compilearg(code, p, VAL_INT); if(!more) { if(rep) break; compileint(code, INT_MIN); fakeargs++; } numargs++; break;
+ case 'f': if(more) more = compilearg(code, p, VAL_FLOAT); if(!more) { if(rep) break; compilefloat(code); fakeargs++; } numargs++; break;
+ case 't': if(more) more = compilearg(code, p, VAL_ANY); if(!more) { if(rep) break; compilenull(code); fakeargs++; } numargs++; break;
+ case 'e': if(more) more = compilearg(code, p, VAL_CODE); if(!more) { if(rep) break; compileblock(code); fakeargs++; } numargs++; break;
+ case 'r': if(more) more = compilearg(code, p, VAL_IDENT); if(!more) { if(rep) break; compileident(code); fakeargs++; } numargs++; break;
+ case '$': compileident(code, id); numargs++; break;
+ case 'N': compileint(code, numargs-fakeargs); numargs++; break;
+#ifndef STANDALONE
+ case 'D': comtype = CODE_COMD; numargs++; break;
+#endif
+ case 'C': comtype = CODE_COMC; if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++; numargs = 1; goto endfmt;
+ case 'V': comtype = CODE_COMV; if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++; numargs = 2; goto endfmt;
+ case '1': case '2': case '3': case '4':
+ if(more && numargs < MAXARGS)
+ {
+ int numrep = *fmt-'0'+1;
+ fmt -= numrep;
+ rep = true;
+ }
+ else for(; numargs > MAXARGS; numargs--) code.add(CODE_POP);
+ break;
+ }
+ endfmt:
+ code.add(comtype|(rettype < VAL_ANY ? rettype<<CODE_RET : 0)|(id->index<<8));
+ break;
+ }
+ case ID_LOCAL:
+ if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_IDENT))) numargs++;
+ if(more) while((more = compilearg(code, p, VAL_ANY))) code.add(CODE_POP);
+ code.add(CODE_LOCAL);
+ break;
+ case ID_VAR:
+ if(!(more = compilearg(code, p, VAL_INT))) code.add(CODE_PRINT|(id->index<<8));
+ else if(!(id->flags&IDF_HEX) || !(more = compilearg(code, p, VAL_INT))) code.add(CODE_IVAR1|(id->index<<8));
+ else if(!(more = compilearg(code, p, VAL_INT))) code.add(CODE_IVAR2|(id->index<<8));
+ else code.add(CODE_IVAR3|(id->index<<8));
+ break;
+ case ID_FVAR:
+ if(!(more = compilearg(code, p, VAL_FLOAT))) code.add(CODE_PRINT|(id->index<<8));
+ else code.add(CODE_FVAR1|(id->index<<8));
+ break;
+ case ID_SVAR:
+ if(!(more = compilearg(code, p, VAL_STR))) code.add(CODE_PRINT|(id->index<<8));
+ else
+ {
+ int numconc = 0;
+ while(numconc+1 < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numconc++;
+ if(numconc > 0) code.add(CODE_CONC|RET_STR|((numconc+1)<<8));
+ code.add(CODE_SVAR1|(id->index<<8));
+ }
+ break;
+ }
+ endcommand:
+ delete[] idname;
+ }
+ endstatement:
+ if(more) while(compilearg(code, p, VAL_ANY)) code.add(CODE_POP);
+ p += strcspn(p, ")];/\n\0");
+ int c = *p++;
+ switch(c)
+ {
+ case '\0':
+ if(c != brak) debugcodeline(line, "missing \"%c\"", brak);
+ p--;
+ return;
+
+ case ')':
+ case ']':
+ if(c == brak) return;
+ debugcodeline(line, "unexpected \"%c\"", c);
+ break;
+
+ case '/':
+ if(*p == '/') p += strcspn(p, "\n\0");
+ goto endstatement;
+ }
+ }
+}
+
+static void compilemain(vector<uint> &code, const char *p, int rettype = VAL_ANY)
+{
+ code.add(CODE_START);
+ compilestatements(code, p, VAL_ANY);
+ code.add(CODE_EXIT|(rettype < VAL_ANY ? rettype<<CODE_RET : 0));
+}
+
+uint *compilecode(const char *p)
+{
+ vector<uint> buf;
+ buf.reserve(64);
+ compilemain(buf, p);
+ uint *code = new uint[buf.length()];
+ memcpy(code, buf.getbuf(), buf.length()*sizeof(uint));
+ code[0] += 0x100;
+ return code;
+}
+
+void keepcode(uint *code)
+{
+ if(!code) return;
+ switch(*code&CODE_OP_MASK)
+ {
+ case CODE_START:
+ *code += 0x100;
+ return;
+ }
+ switch(code[-1]&CODE_OP_MASK)
+ {
+ case CODE_START:
+ code[-1] += 0x100;
+ break;
+ case CODE_OFFSET:
+ code -= int(code[-1]>>8);
+ *code += 0x100;
+ break;
+ }
+}
+
+void freecode(uint *code)
+{
+ if(!code) return;
+ switch(*code&CODE_OP_MASK)
+ {
+ case CODE_START:
+ *code -= 0x100;
+ if(int(*code) < 0x100) delete[] code;
+ return;
+ }
+ switch(code[-1]&CODE_OP_MASK)
+ {
+ case CODE_START:
+ code[-1] -= 0x100;
+ if(int(code[-1]) < 0x100) delete[] &code[-1];
+ break;
+ case CODE_OFFSET:
+ code -= int(code[-1]>>8);
+ *code -= 0x100;
+ if(int(*code) < 0x100) delete[] code;
+ break;
+ }
+}
+
+void printvar(ident *id, int i)
+{
+ if(i < 0) conoutf(CON_INFO, id->index, "%s = %d", id->name, i);
+ else if(id->flags&IDF_HEX && id->maxval==0xFFFFFF)
+ conoutf(CON_INFO, id->index, "%s = 0x%.6X (%d, %d, %d)", id->name, i, (i>>16)&0xFF, (i>>8)&0xFF, i&0xFF);
+ else
+ conoutf(CON_INFO, id->index, id->flags&IDF_HEX ? "%s = 0x%X" : "%s = %d", id->name, i);
+}
+
+void printfvar(ident *id, float f)
+{
+ conoutf(CON_INFO, id->index, "%s = %s", id->name, floatstr(f));
+}
+
+void printsvar(ident *id, const char *s)
+{
+ conoutf(CON_INFO, id->index, strchr(s, '"') ? "%s = [%s]" : "%s = \"%s\"", id->name, s);
+}
+
+template <class V>
+static void printvar(ident *id, int type, V &val)
+{
+ switch(type)
+ {
+ case VAL_INT: printvar(id, val.getint()); break;
+ case VAL_FLOAT: printfvar(id, val.getfloat()); break;
+ default: printsvar(id, val.getstr()); break;
+ }
+}
+
+void printvar(ident *id)
+{
+ switch(id->type)
+ {
+ case ID_VAR: printvar(id, *id->storage.i); break;
+ case ID_FVAR: printfvar(id, *id->storage.f); break;
+ case ID_SVAR: printsvar(id, *id->storage.s); break;
+ case ID_ALIAS: printvar(id, id->valtype, *id); break;
+ case ID_COMMAND:
+ if(id->flags&IDF_EMUVAR)
+ {
+ tagval result;
+ executeret(id, NULL, 0, true, result);
+ printvar(id, result.type, result);
+ freearg(result);
+ }
+ break;
+ }
+}
+ICOMMAND(printvar, "r", (ident *id), printvar(id));
+
+typedef void (__cdecl *comfun)();
+typedef void (__cdecl *comfun1)(void *);
+typedef void (__cdecl *comfun2)(void *, void *);
+typedef void (__cdecl *comfun3)(void *, void *, void *);
+typedef void (__cdecl *comfun4)(void *, void *, void *, void *);
+typedef void (__cdecl *comfun5)(void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun6)(void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun7)(void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun8)(void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun9)(void *, void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun10)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun11)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun12)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfunv)(tagval *, int);
+
+static const uint *skipcode(const uint *code, tagval &result)
+{
+ int depth = 0;
+ for(;;)
+ {
+ uint op = *code++;
+ switch(op&0xFF)
+ {
+ case CODE_MACRO:
+ case CODE_VAL|RET_STR:
+ {
+ uint len = op>>8;
+ code += len/sizeof(uint) + 1;
+ continue;
+ }
+ case CODE_BLOCK:
+ {
+ uint len = op>>8;
+ code += len;
+ continue;
+ }
+ case CODE_ENTER:
+ ++depth;
+ continue;
+ case CODE_EXIT|RET_NULL: case CODE_EXIT|RET_STR: case CODE_EXIT|RET_INT: case CODE_EXIT|RET_FLOAT:
+ if(depth <= 0)
+ {
+ forcearg(result, op&CODE_RET_MASK);
+ return code;
+ }
+ --depth;
+ continue;
+ }
+ }
+}
+
+static inline void callcommand(ident *id, tagval *args, int numargs, bool lookup = false)
+{
+ int i = -1, fakeargs = 0;
+ bool rep = false;
+ for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt)
+ {
+ case 'i': if(++i >= numargs) { if(rep) break; args[i].setint(0); fakeargs++; } else forceint(args[i]); break;
+ case 'b': if(++i >= numargs) { if(rep) break; args[i].setint(INT_MIN); fakeargs++; } else forceint(args[i]); break;
+ case 'f': if(++i >= numargs) { if(rep) break; args[i].setfloat(0.0f); fakeargs++; } else forcefloat(args[i]); break;
+ case 's': if(++i >= numargs) { if(rep) break; args[i].setstr(newstring("")); fakeargs++; } else forcestr(args[i]); break;
+ case 't': if(++i >= numargs) { if(rep) break; args[i].setnull(); fakeargs++; } break;
+ case 'e':
+ if(++i >= numargs)
+ {
+ if(rep) break;
+ static uint buf[2] = { CODE_START + 0x100, CODE_EXIT };
+ args[i].setcode(buf);
+ fakeargs++;
+ }
+ else
+ {
+ vector<uint> buf;
+ buf.reserve(64);
+ compilemain(buf, numargs <= i ? "" : args[i].getstr());
+ freearg(args[i]);
+ args[i].setcode(buf.getbuf()+1);
+ buf.disown();
+ }
+ break;
+ case 'r': if(++i >= numargs) { if(rep) break; args[i].setident(dummyident); fakeargs++; } else forceident(args[i]); break;
+ case '$': if(++i < numargs) freearg(args[i]); args[i].setident(id); break;
+ case 'N': if(++i < numargs) freearg(args[i]); args[i].setint(lookup ? -1 : i-fakeargs); break;
+#ifndef STANDALONE
+ case 'D': if(++i < numargs) freearg(args[i]); args[i].setint(addreleaseaction(conc(args, i, true, id->name)) ? 1 : 0); fakeargs++; break;
+#endif
+ case 'C': { i = max(i+1, numargs); vector<char> buf; ((comfun1)id->fun)(conc(buf, args, i, true)); goto cleanup; }
+ case 'V': i = max(i+1, numargs); ((comfunv)id->fun)(args, i); goto cleanup;
+ case '1': case '2': case '3': case '4': if(i+1 < numargs) { fmt -= *fmt-'0'+1; rep = true; } break;
+ }
+ #define ARG(n) (id->argmask&(1<<n) ? (void *)args[n].s : (void *)&args[n].i)
+ #define CALLCOM(n) \
+ switch(n) \
+ { \
+ case 0: ((comfun)id->fun)(); break; \
+ case 1: ((comfun1)id->fun)(ARG(0)); break; \
+ case 2: ((comfun2)id->fun)(ARG(0), ARG(1)); break; \
+ case 3: ((comfun3)id->fun)(ARG(0), ARG(1), ARG(2)); break; \
+ case 4: ((comfun4)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3)); break; \
+ case 5: ((comfun5)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4)); break; \
+ case 6: ((comfun6)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5)); break; \
+ case 7: ((comfun7)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6)); break; \
+ case 8: ((comfun8)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7)); break; \
+ case 9: ((comfun9)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8)); break; \
+ case 10: ((comfun10)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9)); break; \
+ case 11: ((comfun11)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9), ARG(10)); break; \
+ case 12: ((comfun12)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9), ARG(10), ARG(11)); break; \
+ }
+ ++i;
+ CALLCOM(i)
+cleanup:
+ loopk(i) freearg(args[k]);
+ for(; i < numargs; i++) freearg(args[i]);
+}
+
+#define MAXRUNDEPTH 255
+static int rundepth = 0;
+
+static const uint *runcode(const uint *code, tagval &result)
+{
+ result.setnull();
+ if(rundepth >= MAXRUNDEPTH)
+ {
+ debugcode("exceeded recursion limit");
+ return skipcode(code, result);
+ }
+ ++rundepth;
+ ident *id = NULL;
+ int numargs = 0;
+ tagval args[MAXARGS+1], *prevret = commandret;
+ commandret = &result;
+ for(;;)
+ {
+ uint op = *code++;
+ switch(op&0xFF)
+ {
+ case CODE_START: case CODE_OFFSET: continue;
+
+ case CODE_POP:
+ freearg(args[--numargs]);
+ continue;
+ case CODE_ENTER:
+ code = runcode(code, args[numargs++]);
+ continue;
+ case CODE_EXIT|RET_NULL: case CODE_EXIT|RET_STR: case CODE_EXIT|RET_INT: case CODE_EXIT|RET_FLOAT:
+ forcearg(result, op&CODE_RET_MASK);
+ goto exit;
+ case CODE_PRINT:
+ printvar(identmap[op>>8]);
+ continue;
+ case CODE_LOCAL:
+ {
+ identstack locals[MAXARGS];
+ freearg(result);
+ loopi(numargs) pushalias(*args[i].id, locals[i]);
+ code = runcode(code, result);
+ loopi(numargs) popalias(*args[i].id);
+ goto exit;
+ }
+
+ case CODE_MACRO:
+ {
+ uint len = op>>8;
+ args[numargs++].setmacro(code);
+ code += len/sizeof(uint) + 1;
+ continue;
+ }
+
+ case CODE_VAL|RET_STR:
+ {
+ uint len = op>>8;
+ args[numargs++].setstr(newstring((const char *)code, len));
+ code += len/sizeof(uint) + 1;
+ continue;
+ }
+ case CODE_VALI|RET_STR:
+ {
+ char s[4] = { char((op>>8)&0xFF), char((op>>16)&0xFF), char((op>>24)&0xFF), '\0' };
+ args[numargs++].setstr(newstring(s));
+ continue;
+ }
+ case CODE_VAL|RET_NULL:
+ case CODE_VALI|RET_NULL: args[numargs++].setnull(); continue;
+ case CODE_VAL|RET_INT: args[numargs++].setint(int(*code++)); continue;
+ case CODE_VALI|RET_INT: args[numargs++].setint(int(op)>>8); continue;
+ case CODE_VAL|RET_FLOAT: args[numargs++].setfloat(*(const float *)code++); continue;
+ case CODE_VALI|RET_FLOAT: args[numargs++].setfloat(float(int(op)>>8)); continue;
+
+ case CODE_FORCE|RET_STR: forcestr(args[numargs-1]); continue;
+ case CODE_FORCE|RET_INT: forceint(args[numargs-1]); continue;
+ case CODE_FORCE|RET_FLOAT: forcefloat(args[numargs-1]); continue;
+
+ case CODE_RESULT|RET_NULL: case CODE_RESULT|RET_STR: case CODE_RESULT|RET_INT: case CODE_RESULT|RET_FLOAT:
+ litval:
+ freearg(result);
+ result = args[0];
+ forcearg(result, op&CODE_RET_MASK);
+ args[0].setnull();
+ freeargs(args, numargs, 0);
+ continue;
+
+ case CODE_BLOCK:
+ {
+ uint len = op>>8;
+ args[numargs++].setcode(code+1);
+ code += len;
+ continue;
+ }
+ case CODE_COMPILE:
+ {
+ tagval &arg = args[numargs-1];
+ vector<uint> buf;
+ switch(arg.type)
+ {
+ case VAL_INT: buf.reserve(8); buf.add(CODE_START); compileint(buf, arg.i); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break;
+ case VAL_FLOAT: buf.reserve(8); buf.add(CODE_START); compilefloat(buf, arg.f); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break;
+ case VAL_STR: case VAL_MACRO: buf.reserve(64); compilemain(buf, arg.s); freearg(arg); break;
+ default: buf.reserve(8); buf.add(CODE_START); compilenull(buf); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break;
+ }
+ arg.setcode(buf.getbuf()+1);
+ buf.disown();
+ continue;
+ }
+
+ case CODE_IDENT:
+ args[numargs++].setident(identmap[op>>8]);
+ continue;
+ case CODE_IDENTARG:
+ {
+ ident *id = identmap[op>>8];
+ if(!(aliasstack->usedargs&(1<<id->index)))
+ {
+ pusharg(*id, nullval, aliasstack->argstack[id->index]);
+ aliasstack->usedargs |= 1<<id->index;
+ }
+ args[numargs++].setident(id);
+ continue;
+ }
+ case CODE_IDENTU:
+ {
+ tagval &arg = args[numargs-1];
+ ident *id = arg.type == VAL_STR || arg.type == VAL_MACRO ? newident(arg.s, IDF_UNKNOWN) : dummyident;
+ if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index)))
+ {
+ pusharg(*id, nullval, aliasstack->argstack[id->index]);
+ aliasstack->usedargs |= 1<<id->index;
+ }
+ freearg(arg);
+ arg.setident(id);
+ continue;
+ }
+
+ case CODE_LOOKUPU|RET_STR:
+ #define LOOKUPU(aval, sval, ival, fval, nval) { \
+ tagval &arg = args[numargs-1]; \
+ if(arg.type != VAL_STR && arg.type != VAL_MACRO) continue; \
+ id = idents.access(arg.s); \
+ if(id) switch(id->type) \
+ { \
+ case ID_ALIAS: \
+ if(id->flags&IDF_UNKNOWN) break; \
+ freearg(arg); \
+ if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index))) { nval; continue; } \
+ aval; \
+ continue; \
+ case ID_SVAR: freearg(arg); sval; continue; \
+ case ID_VAR: freearg(arg); ival; continue; \
+ case ID_FVAR: freearg(arg); fval; continue; \
+ case ID_COMMAND: \
+ { \
+ freearg(arg); \
+ arg.setnull(); \
+ commandret = &arg; \
+ tagval buf[MAXARGS]; \
+ callcommand(id, buf, 0, true); \
+ forcearg(arg, op&CODE_RET_MASK); \
+ commandret = &result; \
+ continue; \
+ } \
+ default: freearg(arg); nval; continue; \
+ } \
+ debugcode("unknown alias lookup: %s", arg.s); \
+ freearg(arg); \
+ nval; \
+ continue; \
+ }
+ LOOKUPU(arg.setstr(newstring(id->getstr())),
+ arg.setstr(newstring(*id->storage.s)),
+ arg.setstr(newstring(intstr(*id->storage.i))),
+ arg.setstr(newstring(floatstr(*id->storage.f))),
+ arg.setstr(newstring("")));
+ case CODE_LOOKUP|RET_STR:
+ #define LOOKUP(aval) { \
+ id = identmap[op>>8]; \
+ if(id->flags&IDF_UNKNOWN) debugcode("unknown alias lookup: %s", id->name); \
+ aval; \
+ continue; \
+ }
+ LOOKUP(args[numargs++].setstr(newstring(id->getstr())));
+ case CODE_LOOKUPARG|RET_STR:
+ #define LOOKUPARG(aval, nval) { \
+ id = identmap[op>>8]; \
+ if(!(aliasstack->usedargs&(1<<id->index))) { nval; continue; } \
+ aval; \
+ continue; \
+ }
+ LOOKUPARG(args[numargs++].setstr(newstring(id->getstr())), args[numargs++].setstr(newstring("")));
+ case CODE_LOOKUPU|RET_INT:
+ LOOKUPU(arg.setint(id->getint()),
+ arg.setint(parseint(*id->storage.s)),
+ arg.setint(*id->storage.i),
+ arg.setint(int(*id->storage.f)),
+ arg.setint(0));
+ case CODE_LOOKUP|RET_INT:
+ LOOKUP(args[numargs++].setint(id->getint()));
+ case CODE_LOOKUPARG|RET_INT:
+ LOOKUPARG(args[numargs++].setint(id->getint()), args[numargs++].setint(0));
+ case CODE_LOOKUPU|RET_FLOAT:
+ LOOKUPU(arg.setfloat(id->getfloat()),
+ arg.setfloat(parsefloat(*id->storage.s)),
+ arg.setfloat(float(*id->storage.i)),
+ arg.setfloat(*id->storage.f),
+ arg.setfloat(0.0f));
+ case CODE_LOOKUP|RET_FLOAT:
+ LOOKUP(args[numargs++].setfloat(id->getfloat()));
+ case CODE_LOOKUPARG|RET_FLOAT:
+ LOOKUPARG(args[numargs++].setfloat(id->getfloat()), args[numargs++].setfloat(0.0f));
+ case CODE_LOOKUPU|RET_NULL:
+ LOOKUPU(id->getval(arg),
+ arg.setstr(newstring(*id->storage.s)),
+ arg.setint(*id->storage.i),
+ arg.setfloat(*id->storage.f),
+ arg.setnull());
+ case CODE_LOOKUP|RET_NULL:
+ LOOKUP(id->getval(args[numargs++]));
+ case CODE_LOOKUPARG|RET_NULL:
+ LOOKUPARG(id->getval(args[numargs++]), args[numargs++].setnull());
+
+ case CODE_SVAR|RET_STR: case CODE_SVAR|RET_NULL: args[numargs++].setstr(newstring(*identmap[op>>8]->storage.s)); continue;
+ case CODE_SVAR|RET_INT: args[numargs++].setint(parseint(*identmap[op>>8]->storage.s)); continue;
+ case CODE_SVAR|RET_FLOAT: args[numargs++].setfloat(parsefloat(*identmap[op>>8]->storage.s)); continue;
+ case CODE_SVAR1: setsvarchecked(identmap[op>>8], args[0].s); freeargs(args, numargs, 0); continue;
+
+ case CODE_IVAR|RET_INT: case CODE_IVAR|RET_NULL: args[numargs++].setint(*identmap[op>>8]->storage.i); continue;
+ case CODE_IVAR|RET_STR: args[numargs++].setstr(newstring(intstr(*identmap[op>>8]->storage.i))); continue;
+ case CODE_IVAR|RET_FLOAT: args[numargs++].setfloat(float(*identmap[op>>8]->storage.i)); continue;
+ case CODE_IVAR1: setvarchecked(identmap[op>>8], args[0].i); numargs = 0; continue;
+ case CODE_IVAR2: setvarchecked(identmap[op>>8], (args[0].i<<16)|(args[1].i<<8)); numargs = 0; continue;
+ case CODE_IVAR3: setvarchecked(identmap[op>>8], (args[0].i<<16)|(args[1].i<<8)|args[2].i); numargs = 0; continue;
+
+ case CODE_FVAR|RET_FLOAT: case CODE_FVAR|RET_NULL: args[numargs++].setfloat(*identmap[op>>8]->storage.f); continue;
+ case CODE_FVAR|RET_STR: args[numargs++].setstr(newstring(floatstr(*identmap[op>>8]->storage.f))); continue;
+ case CODE_FVAR|RET_INT: args[numargs++].setint(int(*identmap[op>>8]->storage.f)); continue;
+ case CODE_FVAR1: setfvarchecked(identmap[op>>8], args[0].f); numargs = 0; continue;
+
+ case CODE_COM|RET_NULL: case CODE_COM|RET_STR: case CODE_COM|RET_FLOAT: case CODE_COM|RET_INT:
+ id = identmap[op>>8];
+#ifndef STANDALONE
+ callcom:
+#endif
+ forcenull(result);
+ CALLCOM(numargs)
+ forceresult:
+ freeargs(args, numargs, 0);
+ forcearg(result, op&CODE_RET_MASK);
+ continue;
+#ifndef STANDALONE
+ case CODE_COMD|RET_NULL: case CODE_COMD|RET_STR: case CODE_COMD|RET_FLOAT: case CODE_COMD|RET_INT:
+ id = identmap[op>>8];
+ args[numargs].setint(addreleaseaction(conc(args, numargs, true, id->name)) ? 1 : 0);
+ numargs++;
+ goto callcom;
+#endif
+ case CODE_COMV|RET_NULL: case CODE_COMV|RET_STR: case CODE_COMV|RET_FLOAT: case CODE_COMV|RET_INT:
+ id = identmap[op>>8];
+ forcenull(result);
+ ((comfunv)id->fun)(args, numargs);
+ goto forceresult;
+ case CODE_COMC|RET_NULL: case CODE_COMC|RET_STR: case CODE_COMC|RET_FLOAT: case CODE_COMC|RET_INT:
+ id = identmap[op>>8];
+ forcenull(result);
+ {
+ vector<char> buf;
+ buf.reserve(MAXSTRLEN);
+ ((comfun1)id->fun)(conc(buf, args, numargs, true));
+ }
+ goto forceresult;
+
+ case CODE_CONC|RET_NULL: case CODE_CONC|RET_STR: case CODE_CONC|RET_FLOAT: case CODE_CONC|RET_INT:
+ case CODE_CONCW|RET_NULL: case CODE_CONCW|RET_STR: case CODE_CONCW|RET_FLOAT: case CODE_CONCW|RET_INT:
+ {
+ int numconc = op>>8;
+ char *s = conc(&args[numargs-numconc], numconc, (op&CODE_OP_MASK)==CODE_CONC);
+ freeargs(args, numargs, numargs-numconc);
+ args[numargs++].setstr(s);
+ forcearg(args[numargs-1], op&CODE_RET_MASK);
+ continue;
+ }
+
+ case CODE_CONCM|RET_NULL: case CODE_CONCM|RET_STR: case CODE_CONCM|RET_FLOAT: case CODE_CONCM|RET_INT:
+ {
+ int numconc = op>>8;
+ char *s = conc(&args[numargs-numconc], numconc, false);
+ freeargs(args, numargs, numargs-numconc);
+ result.setstr(s);
+ forcearg(result, op&CODE_RET_MASK);
+ continue;
+ }
+
+ case CODE_ALIAS:
+ setalias(*identmap[op>>8], args[--numargs]);
+ freeargs(args, numargs, 0);
+ continue;
+ case CODE_ALIASARG:
+ setarg(*identmap[op>>8], args[--numargs]);
+ freeargs(args, numargs, 0);
+ continue;
+ case CODE_ALIASU:
+ forcestr(args[0]);
+ setalias(args[0].s, args[--numargs]);
+ freeargs(args, numargs, 0);
+ continue;
+
+ case CODE_CALL|RET_NULL: case CODE_CALL|RET_STR: case CODE_CALL|RET_FLOAT: case CODE_CALL|RET_INT:
+ #define CALLALIAS(offset) { \
+ identstack argstack[MAXARGS]; \
+ for(int i = 0; i < numargs-offset; i++) \
+ pusharg(*identmap[i], args[i+offset], argstack[i]); \
+ int oldargs = _numargs, newargs = numargs-offset; \
+ _numargs = newargs; \
+ int oldflags = identflags; \
+ identflags |= id->flags&IDF_OVERRIDDEN; \
+ identlink aliaslink = { id, aliasstack, (1<<newargs)-1, argstack }; \
+ aliasstack = &aliaslink; \
+ if(!id->code) id->code = compilecode(id->getstr()); \
+ uint *code = id->code; \
+ code[0] += 0x100; \
+ runcode(code+1, result); \
+ code[0] -= 0x100; \
+ if(int(code[0]) < 0x100) delete[] code; \
+ aliasstack = aliaslink.next; \
+ identflags = oldflags; \
+ for(int i = 0; i < newargs; i++) \
+ poparg(*identmap[i]); \
+ for(int argmask = aliaslink.usedargs&(~0U<<newargs), i = newargs; argmask; i++) \
+ if(argmask&(1<<i)) { poparg(*identmap[i]); argmask &= ~(1<<i); } \
+ forcearg(result, op&CODE_RET_MASK); \
+ _numargs = oldargs; \
+ numargs = 0; \
+ }
+ forcenull(result);
+ id = identmap[op>>8];
+ if(id->flags&IDF_UNKNOWN)
+ {
+ debugcode("unknown command: %s", id->name);
+ goto forceresult;
+ }
+ CALLALIAS(0);
+ continue;
+ case CODE_CALLARG|RET_NULL: case CODE_CALLARG|RET_STR: case CODE_CALLARG|RET_FLOAT: case CODE_CALLARG|RET_INT:
+ forcenull(result);
+ id = identmap[op>>8];
+ if(!(aliasstack->usedargs&(1<<id->index))) goto forceresult;
+ CALLALIAS(0);
+ continue;
+
+ case CODE_CALLU|RET_NULL: case CODE_CALLU|RET_STR: case CODE_CALLU|RET_FLOAT: case CODE_CALLU|RET_INT:
+ if(args[0].type != VAL_STR) goto litval;
+ id = idents.access(args[0].s);
+ if(!id)
+ {
+ noid:
+ if(checknumber(args[0].s)) goto litval;
+ debugcode("unknown command: %s", args[0].s);
+ forcenull(result);
+ goto forceresult;
+ }
+ forcenull(result);
+ switch(id->type)
+ {
+ case ID_COMMAND:
+ freearg(args[0]);
+ callcommand(id, args+1, numargs-1);
+ forcearg(result, op&CODE_RET_MASK);
+ numargs = 0;
+ continue;
+ case ID_LOCAL:
+ {
+ identstack locals[MAXARGS];
+ freearg(args[0]);
+ loopj(numargs-1) pushalias(*forceident(args[j+1]), locals[j]);
+ code = runcode(code, result);
+ loopj(numargs-1) popalias(*args[j+1].id);
+ goto exit;
+ }
+ case ID_VAR:
+ if(numargs <= 1) printvar(id); else setvarchecked(id, &args[1], numargs-1);
+ goto forceresult;
+ case ID_FVAR:
+ if(numargs <= 1) printvar(id); else setfvarchecked(id, forcefloat(args[1]));
+ goto forceresult;
+ case ID_SVAR:
+ if(numargs <= 1) printvar(id); else setsvarchecked(id, forcestr(args[1]));
+ goto forceresult;
+ case ID_ALIAS:
+ if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index))) goto forceresult;
+ if(id->valtype==VAL_NULL) goto noid;
+ freearg(args[0]);
+ CALLALIAS(1);
+ continue;
+ default:
+ goto forceresult;
+ }
+ }
+ }
+exit:
+ commandret = prevret;
+ --rundepth;
+ return code;
+}
+
+void executeret(const uint *code, tagval &result)
+{
+ runcode(code, result);
+}
+
+void executeret(const char *p, tagval &result)
+{
+ vector<uint> code;
+ code.reserve(64);
+ compilemain(code, p, VAL_ANY);
+ runcode(code.getbuf()+1, result);
+ if(int(code[0]) >= 0x100) code.disown();
+}
+
+void executeret(ident *id, tagval *args, int numargs, bool lookup, tagval &result)
+{
+ result.setnull();
+ ++rundepth;
+ tagval *prevret = commandret;
+ commandret = &result;
+ if(rundepth > MAXRUNDEPTH) debugcode("exceeded recursion limit");
+ else if(id) switch(id->type)
+ {
+ default:
+ if(!id->fun) break;
+ // fall-through
+ case ID_COMMAND:
+ if(numargs < id->numargs)
+ {
+ tagval buf[MAXARGS];
+ memcpy(buf, args, numargs*sizeof(tagval));
+ callcommand(id, buf, numargs, lookup);
+ }
+ else callcommand(id, args, numargs, lookup);
+ numargs = 0;
+ break;
+ case ID_VAR:
+ if(numargs <= 0) printvar(id); else setvarchecked(id, args, numargs);
+ break;
+ case ID_FVAR:
+ if(numargs <= 0) printvar(id); else setfvarchecked(id, forcefloat(args[0]));
+ break;
+ case ID_SVAR:
+ if(numargs <= 0) printvar(id); else setsvarchecked(id, forcestr(args[0]));
+ break;
+ case ID_ALIAS:
+ if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index))) break;
+ if(id->valtype==VAL_NULL) break;
+ #define op RET_NULL
+ CALLALIAS(0);
+ #undef op
+ break;
+ }
+ freeargs(args, numargs, 0);
+ commandret = prevret;
+ --rundepth;
+}
+
+char *executestr(const uint *code)
+{
+ tagval result;
+ runcode(code, result);
+ if(result.type == VAL_NULL) return NULL;
+ forcestr(result);
+ return result.s;
+}
+
+char *executestr(const char *p)
+{
+ tagval result;
+ executeret(p, result);
+ if(result.type == VAL_NULL) return NULL;
+ forcestr(result);
+ return result.s;
+}
+
+char *executestr(ident *id, tagval *args, int numargs, bool lookup)
+{
+ tagval result;
+ executeret(id, args, numargs, lookup, result);
+ if(result.type == VAL_NULL) return NULL;
+ forcestr(result);
+ return result.s;
+}
+
+char *execidentstr(const char *name, bool lookup)
+{
+ ident *id = idents.access(name);
+ return id ? executestr(id, NULL, 0, lookup) : NULL;
+}
+
+int execute(const uint *code)
+{
+ tagval result;
+ runcode(code, result);
+ int i = result.getint();
+ freearg(result);
+ return i;
+}
+
+int execute(const char *p)
+{
+ vector<uint> code;
+ code.reserve(64);
+ compilemain(code, p, VAL_INT);
+ tagval result;
+ runcode(code.getbuf()+1, result);
+ if(int(code[0]) >= 0x100) code.disown();
+ int i = result.getint();
+ freearg(result);
+ return i;
+}
+
+int execute(ident *id, tagval *args, int numargs, bool lookup)
+{
+ tagval result;
+ executeret(id, args, numargs, lookup, result);
+ int i = result.getint();
+ freearg(result);
+ return i;
+}
+
+int execident(const char *name, int noid, bool lookup)
+{
+ ident *id = idents.access(name);
+ return id ? execute(id, NULL, 0, lookup) : noid;
+}
+
+static inline bool getbool(const char *s)
+{
+ switch(s[0])
+ {
+ case '+': case '-':
+ switch(s[1])
+ {
+ case '0': break;
+ case '.': return !isdigit(s[2]) || parsefloat(s) != 0;
+ default: return true;
+ }
+ // fall through
+ case '0':
+ {
+ char *end;
+ int val = int(strtoul((char *)s, &end, 0));
+ if(val) return true;
+ switch(*end)
+ {
+ case 'e': case '.': return parsefloat(s) != 0;
+ default: return false;
+ }
+ }
+ case '.': return !isdigit(s[1]) || parsefloat(s) != 0;
+ case '\0': return false;
+ default: return true;
+ }
+}
+
+static inline bool getbool(const tagval &v)
+{
+ switch(v.type)
+ {
+ case VAL_FLOAT: return v.f!=0;
+ case VAL_INT: return v.i!=0;
+ case VAL_STR: case VAL_MACRO: return getbool(v.s);
+ default: return false;
+ }
+}
+
+bool executebool(const uint *code)
+{
+ tagval result;
+ runcode(code, result);
+ bool b = getbool(result);
+ freearg(result);
+ return b;
+}
+
+bool executebool(const char *p)
+{
+ tagval result;
+ executeret(p, result);
+ bool b = getbool(result);
+ freearg(result);
+ return b;
+}
+
+bool executebool(ident *id, tagval *args, int numargs, bool lookup)
+{
+ tagval result;
+ executeret(id, args, numargs, lookup, result);
+ bool b = getbool(result);
+ freearg(result);
+ return b;
+}
+
+bool execidentbool(const char *name, bool noid, bool lookup)
+{
+ ident *id = idents.access(name);
+ return id ? executebool(id, NULL, 0, lookup) : noid;
+}
+
+bool execfile(const char *cfgfile, bool msg)
+{
+ string s;
+ copystring(s, cfgfile);
+ char *buf = loadfile(path(s), NULL);
+ if(!buf)
+ {
+ if(msg) conoutf(CON_ERROR, "could not read \"%s\"", cfgfile);
+ return false;
+ }
+ const char *oldsourcefile = sourcefile, *oldsourcestr = sourcestr;
+ sourcefile = cfgfile;
+ sourcestr = buf;
+ execute(buf);
+ sourcefile = oldsourcefile;
+ sourcestr = oldsourcestr;
+ delete[] buf;
+ return true;
+}
+ICOMMAND(exec, "sb", (char *file, int *msg), intret(execfile(file, *msg != 0) ? 1 : 0));
+
+const char *escapestring(const char *s)
+{
+ static vector<char> strbuf[3];
+ static int stridx = 0;
+ stridx = (stridx + 1)%3;
+ vector<char> &buf = strbuf[stridx];
+ buf.setsize(0);
+ buf.add('"');
+ for(; *s; s++) switch(*s)
+ {
+ case '\n': buf.put("^n", 2); break;
+ case '\t': buf.put("^t", 2); break;
+ case '\f': buf.put("^f", 2); break;
+ case '"': buf.put("^\"", 2); break;
+ case '^': buf.put("^^", 2); break;
+ default: buf.add(*s); break;
+ }
+ buf.put("\"\0", 2);
+ return buf.getbuf();
+}
+
+ICOMMAND(escape, "s", (char *s), result(escapestring(s)));
+ICOMMAND(unescape, "s", (char *s),
+{
+ int len = strlen(s);
+ char *d = newstring(len);
+ d[unescapestring(d, s, &s[len])] = '\0';
+ stringret(d);
+});
+
+const char *escapeid(const char *s)
+{
+ const char *end = s + strcspn(s, "\"/;()[]@ \f\t\r\n\0");
+ return *end ? escapestring(s) : s;
+}
+
+bool validateblock(const char *s)
+{
+ const int maxbrak = 100;
+ static char brakstack[maxbrak];
+ int brakdepth = 0;
+ for(; *s; s++) switch(*s)
+ {
+ case '[': case '(': if(brakdepth >= maxbrak) return false; brakstack[brakdepth++] = *s; break;
+ case ']': if(brakdepth <= 0 || brakstack[--brakdepth] != '[') return false; break;
+ case ')': if(brakdepth <= 0 || brakstack[--brakdepth] != '(') return false; break;
+ case '"': s = parsestring(s + 1); if(*s != '"') return false; break;
+ case '/': if(s[1] == '/') return false; break;
+ case '@': case '\f': return false;
+ }
+ return brakdepth == 0;
+}
+
+#ifndef STANDALONE
+void writecfg(const char *name)
+{
+ stream *f = openutf8file(path(name && name[0] ? name : game::savedconfig(), true), "w");
+ if(!f) return;
+ f->printf("// automatically written on exit, DO NOT MODIFY\n// delete this file to have %s overwrite these settings\n// modify settings in game, or put settings in %s to override anything\n\n", game::defaultconfig(), game::autoexec());
+ game::writeclientinfo(f);
+ f->printf("\n");
+ writecrosshairs(f);
+ vector<ident *> ids;
+ enumerate(idents, ident, id, ids.add(&id));
+ ids.sortname();
+ loopv(ids)
+ {
+ ident &id = *ids[i];
+ if(id.flags&IDF_PERSIST) switch(id.type)
+ {
+ case ID_VAR: f->printf("%s %d\n", escapeid(id), *id.storage.i); break;
+ case ID_FVAR: f->printf("%s %s\n", escapeid(id), floatstr(*id.storage.f)); break;
+ case ID_SVAR: f->printf("%s %s\n", escapeid(id), escapestring(*id.storage.s)); break;
+ }
+ }
+ f->printf("\n");
+ writebinds(f);
+ f->printf("\n");
+ loopv(ids)
+ {
+ ident &id = *ids[i];
+ if(id.type==ID_ALIAS && id.flags&IDF_PERSIST && !(id.flags&IDF_OVERRIDDEN)) switch(id.valtype)
+ {
+ case VAL_STR:
+ if(!id.val.s[0]) break;
+ if(!validateblock(id.val.s)) { f->printf("%s = %s\n", escapeid(id), escapestring(id.val.s)); break; }
+ case VAL_FLOAT:
+ case VAL_INT:
+ f->printf("%s = [%s]\n", escapeid(id), id.getstr()); break;
+ }
+ }
+ f->printf("\n");
+ writecompletions(f);
+ delete f;
+}
+
+COMMAND(writecfg, "s");
+#endif
+
+void changedvars()
+{
+ vector<ident *> ids;
+ enumerate(idents, ident, id, if(id.flags&IDF_OVERRIDDEN) ids.add(&id));
+ ids.sortname();
+ loopv(ids) printvar(ids[i]);
+}
+COMMAND(changedvars, "");
+
+// below the commands that implement a small imperative language. thanks to the semantics of
+// () and [] expressions, any control construct can be defined trivially.
+
+static string retbuf[4];
+static int retidx = 0;
+
+const char *intstr(int v)
+{
+ retidx = (retidx + 1)%4;
+ intformat(retbuf[retidx], v);
+ return retbuf[retidx];
+}
+
+void intret(int v)
+{
+ commandret->setint(v);
+}
+
+const char *floatstr(float v)
+{
+ retidx = (retidx + 1)%4;
+ floatformat(retbuf[retidx], v);
+ return retbuf[retidx];
+}
+
+void floatret(float v)
+{
+ commandret->setfloat(v);
+}
+
+#undef ICOMMANDNAME
+#define ICOMMANDNAME(name) _stdcmd
+
+ICOMMAND(do, "e", (uint *body), executeret(body, *commandret));
+ICOMMAND(if, "tee", (tagval *cond, uint *t, uint *f), executeret(getbool(*cond) ? t : f, *commandret));
+ICOMMAND(?, "ttt", (tagval *cond, tagval *t, tagval *f), result(*(getbool(*cond) ? t : f)));
+
+ICOMMAND(pushif, "rte", (ident *id, tagval *v, uint *code),
+{
+ if(id->type != ID_ALIAS || id->index < MAXARGS) return;
+ if(getbool(*v))
+ {
+ identstack stack;
+ pusharg(*id, *v, stack);
+ v->type = VAL_NULL;
+ id->flags &= ~IDF_UNKNOWN;
+ executeret(code, *commandret);
+ poparg(*id);
+ }
+});
+
+void loopiter(ident *id, identstack &stack, const tagval &v)
+{
+ if(id->stack != &stack)
+ {
+ pusharg(*id, v, stack);
+ id->flags &= ~IDF_UNKNOWN;
+ }
+ else
+ {
+ if(id->valtype == VAL_STR) delete[] id->val.s;
+ cleancode(*id);
+ id->setval(v);
+ }
+}
+
+void loopend(ident *id, identstack &stack)
+{
+ if(id->stack == &stack) poparg(*id);
+}
+
+static inline void setiter(ident &id, int i, identstack &stack)
+{
+ if(id.stack == &stack)
+ {
+ if(id.valtype != VAL_INT)
+ {
+ if(id.valtype == VAL_STR) delete[] id.val.s;
+ cleancode(id);
+ id.valtype = VAL_INT;
+ }
+ id.val.i = i;
+ }
+ else
+ {
+ tagval t;
+ t.setint(i);
+ pusharg(id, t, stack);
+ id.flags &= ~IDF_UNKNOWN;
+ }
+}
+ICOMMAND(loop, "rie", (ident *id, int *n, uint *body),
+{
+ if(*n <= 0 || id->type!=ID_ALIAS) return;
+ identstack stack;
+ loopi(*n)
+ {
+ setiter(*id, i, stack);
+ execute(body);
+ }
+ poparg(*id);
+});
+ICOMMAND(loopwhile, "riee", (ident *id, int *n, uint *cond, uint *body),
+{
+ if(*n <= 0 || id->type!=ID_ALIAS) return;
+ identstack stack;
+ loopi(*n)
+ {
+ setiter(*id, i, stack);
+ if(!executebool(cond)) break;
+ execute(body);
+ }
+ poparg(*id);
+});
+ICOMMAND(while, "ee", (uint *cond, uint *body), while(executebool(cond)) execute(body));
+
+char *loopconc(ident *id, int n, uint *body, bool space)
+{
+ identstack stack;
+ vector<char> s;
+ loopi(n)
+ {
+ setiter(*id, i, stack);
+ tagval v;
+ executeret(body, v);
+ const char *vstr = v.getstr();
+ int len = strlen(vstr);
+ if(space && i) s.add(' ');
+ s.put(vstr, len);
+ freearg(v);
+ }
+ if(n > 0) poparg(*id);
+ s.add('\0');
+ return newstring(s.getbuf(), s.length()-1);
+}
+
+ICOMMAND(loopconcat, "rie", (ident *id, int *n, uint *body),
+{
+ if(*n > 0 && id->type==ID_ALIAS) commandret->setstr(loopconc(id, *n, body, true));
+});
+
+ICOMMAND(loopconcatword, "rie", (ident *id, int *n, uint *body),
+{
+ if(*n > 0 && id->type==ID_ALIAS) commandret->setstr(loopconc(id, *n, body, false));
+});
+
+void concat(tagval *v, int n)
+{
+ commandret->setstr(conc(v, n, true));
+}
+COMMAND(concat, "V");
+
+void concatword(tagval *v, int n)
+{
+ commandret->setstr(conc(v, n, false));
+}
+COMMAND(concatword, "V");
+
+void append(ident *id, tagval *v, bool space)
+{
+ if(id->type != ID_ALIAS || v->type == VAL_NULL) return;
+ if(id->valtype == VAL_NULL)
+ {
+ noprefix:
+ if(id->index < MAXARGS) setarg(*id, *v); else setalias(*id, *v);
+ v->type = VAL_NULL;
+ }
+ else
+ {
+ const char *prefix = id->getstr();
+ if(!prefix[0]) goto noprefix;
+ tagval r;
+ r.setstr(conc(v, 1, space, prefix));
+ if(id->index < MAXARGS) setarg(*id, r); else setalias(*id, r);
+ }
+}
+ICOMMAND(append, "rt", (ident *id, tagval *v), append(id, v, true));
+ICOMMAND(appendword, "rt", (ident *id, tagval *v), append(id, v, false));
+
+void result(tagval &v)
+{
+ *commandret = v;
+ v.type = VAL_NULL;
+}
+
+void stringret(char *s)
+{
+ commandret->setstr(s);
+}
+
+void result(const char *s)
+{
+ commandret->setstr(newstring(s));
+}
+
+ICOMMAND(result, "t", (tagval *v),
+{
+ *commandret = *v;
+ v->type = VAL_NULL;
+});
+
+void format(tagval *args, int numargs)
+{
+ vector<char> s;
+ const char *f = args[0].getstr();
+ while(*f)
+ {
+ int c = *f++;
+ if(c == '%')
+ {
+ int i = *f++;
+ if(i >= '1' && i <= '9')
+ {
+ i -= '0';
+ const char *sub = i < numargs ? args[i].getstr() : "";
+ while(*sub) s.add(*sub++);
+ }
+ else s.add(i);
+ }
+ else s.add(c);
+ }
+ s.add('\0');
+ result(s.getbuf());
+}
+COMMAND(format, "V");
+
+static const char *liststart = NULL, *listend = NULL, *listquotestart = NULL, *listquoteend = NULL;
+
+static inline void skiplist(const char *&p)
+{
+ for(;;)
+ {
+ p += strspn(p, " \t\r\n");
+ if(p[0]!='/' || p[1]!='/') break;
+ p += strcspn(p, "\n\0");
+ }
+}
+
+static bool parselist(const char *&s, const char *&start = liststart, const char *&end = listend, const char *&quotestart = listquotestart, const char *&quoteend = listquoteend)
+{
+ skiplist(s);
+ switch(*s)
+ {
+ case '"': quotestart = s++; start = s; s = parsestring(s); end = s; if(*s == '"') s++; quoteend = s; break;
+ case '(': case '[':
+ quotestart = s;
+ start = s+1;
+ for(int braktype = *s++, brak = 1;;)
+ {
+ s += strcspn(s, "\"/;()[]\0");
+ int c = *s++;
+ switch(c)
+ {
+ case '\0': s--; quoteend = end = s; return true;
+ case '"': s = parsestring(s); if(*s == '"') s++; break;
+ case '/': if(*s == '/') s += strcspn(s, "\n\0"); break;
+ case '(': case '[': if(c == braktype) brak++; break;
+ case ')': if(braktype == '(' && --brak <= 0) goto endblock; break;
+ case ']': if(braktype == '[' && --brak <= 0) goto endblock; break;
+ }
+ }
+ endblock:
+ end = s-1;
+ quoteend = s;
+ break;
+ case '\0': case ')': case ']': return false;
+ default: quotestart = start = s; s = parseword(s); quoteend = end = s; break;
+ }
+ skiplist(s);
+ if(*s == ';') s++;
+ return true;
+}
+
+void explodelist(const char *s, vector<char *> &elems, int limit)
+{
+ const char *start, *end;
+ while((limit < 0 || elems.length() < limit) && parselist(s, start, end))
+ elems.add(newstring(start, end-start));
+}
+
+char *indexlist(const char *s, int pos)
+{
+ loopi(pos) if(!parselist(s)) return newstring("");
+ const char *start, *end;
+ return parselist(s, start, end) ? newstring(start, end-start) : newstring("");
+}
+
+int listlen(const char *s)
+{
+ int n = 0;
+ while(parselist(s)) n++;
+ return n;
+}
+ICOMMAND(listlen, "s", (char *s), intret(listlen(s)));
+
+void at(tagval *args, int numargs)
+{
+ if(!numargs) return;
+ const char *start = args[0].getstr(), *end = start + strlen(start);
+ for(int i = 1; i < numargs; i++)
+ {
+ const char *list = start;
+ int pos = args[i].getint();
+ for(; pos > 0; pos--) if(!parselist(list)) break;
+ if(pos > 0 || !parselist(list, start, end)) start = end = "";
+ }
+ commandret->setstr(newstring(start, end-start));
+}
+COMMAND(at, "si1V");
+
+void substr(char *s, int *start, int *count, int *numargs)
+{
+ int len = strlen(s), offset = clamp(*start, 0, len);
+ commandret->setstr(newstring(&s[offset], *numargs >= 3 ? clamp(*count, 0, len - offset) : len - offset));
+}
+COMMAND(substr, "siiN");
+
+void chopstr(char *s, int *lim, char *ellipsis)
+{
+ int len = strlen(s), maxlen = abs(*lim);
+ if(len > maxlen)
+ {
+ int elen = strlen(ellipsis);
+ maxlen = max(maxlen, elen);
+ char *chopped = newstring(maxlen);
+ if(*lim < 0)
+ {
+ memcpy(chopped, ellipsis, elen);
+ memcpy(&chopped[elen], &s[len - (maxlen - elen)], maxlen - elen);
+ }
+ else
+ {
+ memcpy(chopped, s, maxlen - elen);
+ memcpy(&chopped[maxlen - elen], ellipsis, elen);
+ }
+ chopped[maxlen] = '\0';
+ commandret->setstr(chopped);
+ }
+ else result(s);
+}
+COMMAND(chopstr, "sis");
+
+void sublist(const char *s, int *skip, int *count, int *numargs)
+{
+ int offset = max(*skip, 0), len = *numargs >= 3 ? max(*count, 0) : -1;
+ loopi(offset) if(!parselist(s)) break;
+ if(len < 0) { if(offset > 0) skiplist(s); commandret->setstr(newstring(s)); return; }
+ const char *list = s, *start, *end, *qstart, *qend = s;
+ if(len > 0 && parselist(s, start, end, list, qend)) while(--len > 0 && parselist(s, start, end, qstart, qend));
+ commandret->setstr(newstring(list, qend - list));
+}
+COMMAND(sublist, "siiN");
+
+ICOMMAND(stripcolors, "s", (char *s),
+{
+ int len = strlen(s);
+ char *d = newstring(len);
+ filtertext(d, s, true, false, len);
+ stringret(d);
+});
+
+static inline void setiter(ident &id, char *val, identstack &stack)
+{
+ if(id.stack == &stack)
+ {
+ if(id.valtype == VAL_STR) delete[] id.val.s;
+ else id.valtype = VAL_STR;
+ cleancode(id);
+ id.val.s = val;
+ }
+ else
+ {
+ tagval t;
+ t.setstr(val);
+ pusharg(id, t, stack);
+ id.flags &= ~IDF_UNKNOWN;
+ }
+}
+
+void listfind(ident *id, const char *list, const uint *body)
+{
+ if(id->type!=ID_ALIAS) { intret(-1); return; }
+ identstack stack;
+ int n = -1;
+ for(const char *s = list, *start, *end; parselist(s, start, end);)
+ {
+ ++n;
+ char *val = newstring(start, end-start);
+ setiter(*id, val, stack);
+ if(executebool(body)) { intret(n); goto found; }
+ }
+ intret(-1);
+found:
+ if(n >= 0) poparg(*id);
+}
+COMMAND(listfind, "rse");
+
+void looplist(ident *id, const char *list, const uint *body)
+{
+ if(id->type!=ID_ALIAS) return;
+ identstack stack;
+ int n = 0;
+ for(const char *s = list, *start, *end; parselist(s, start, end); n++)
+ {
+ char *val = newstring(start, end-start);
+ setiter(*id, val, stack);
+ execute(body);
+ }
+ if(n) poparg(*id);
+}
+COMMAND(looplist, "rse");
+
+void loopsublist(ident *id, const char *list, int *skip, int *count, const uint *body)
+{
+ if(id->type!=ID_ALIAS) return;
+ identstack stack;
+ int n = 0, offset = max(*skip, 0), len = *count < 0 ? INT_MAX : offset + *count;
+ for(const char *s = list, *start, *end; parselist(s, start, end) && n < len; n++) if(n >= offset)
+ {
+ char *val = newstring(start, end-start);
+ setiter(*id, val, stack);
+ execute(body);
+ }
+ if(n) poparg(*id);
+}
+COMMAND(loopsublist, "rsiie");
+
+void looplistconc(ident *id, const char *list, const uint *body, bool space)
+{
+ if(id->type!=ID_ALIAS) return;
+ identstack stack;
+ vector<char> r;
+ int n = 0;
+ for(const char *s = list, *start, *end; parselist(s, start, end); n++)
+ {
+ char *val = newstring(start, end-start);
+ setiter(*id, val, stack);
+
+ if(n && space) r.add(' ');
+
+ tagval v;
+ executeret(body, v);
+ const char *vstr = v.getstr();
+ int len = strlen(vstr);
+ r.put(vstr, len);
+ freearg(v);
+ }
+ if(n) poparg(*id);
+ r.add('\0');
+ commandret->setstr(newstring(r.getbuf(), r.length()-1));
+}
+ICOMMAND(looplistconcat, "rse", (ident *id, char *list, uint *body), looplistconc(id, list, body, true));
+ICOMMAND(looplistconcatword, "rse", (ident *id, char *list, uint *body), looplistconc(id, list, body, false));
+
+void listfilter(ident *id, const char *list, const uint *body)
+{
+ if(id->type!=ID_ALIAS) return;
+ identstack stack;
+ vector<char> r;
+ int n = 0;
+ for(const char *s = list, *start, *end, *quotestart, *quoteend; parselist(s, start, end, quotestart, quoteend); n++)
+ {
+ char *val = newstring(start, end-start);
+ setiter(*id, val, stack);
+
+ if(executebool(body))
+ {
+ if(r.length()) r.add(' ');
+ r.put(quotestart, quoteend-quotestart);
+ }
+ }
+ if(n) poparg(*id);
+ r.add('\0');
+ commandret->setstr(newstring(r.getbuf(), r.length()-1));
+}
+COMMAND(listfilter, "rse");
+
+void prettylist(const char *s, const char *conj)
+{
+ vector<char> p;
+ const char *start, *end;
+ for(int len = listlen(s), n = 0; parselist(s, start, end); n++)
+ {
+ p.put(start, end - start);
+ if(n+1 < len)
+ {
+ if(len > 2 || !conj[0]) p.add(',');
+ if(n+2 == len && conj[0])
+ {
+ p.add(' ');
+ p.put(conj, strlen(conj));
+ }
+ p.add(' ');
+ }
+ }
+ p.add('\0');
+ result(p.getbuf());
+}
+COMMAND(prettylist, "ss");
+
+int listincludes(const char *list, const char *needle, int needlelen)
+{
+ int offset = 0;
+ for(const char *s = list, *start, *end; parselist(s, start, end);)
+ {
+ int len = end - start;
+ if(needlelen == len && !strncmp(needle, start, len)) return offset;
+ offset++;
+ }
+ return -1;
+}
+ICOMMAND(indexof, "ss", (char *list, char *elem), intret(listincludes(list, elem, strlen(elem))));
+
+char *listdel(const char *s, const char *del)
+{
+ vector<char> p;
+ for(const char *start, *end, *qstart, *qend; parselist(s, start, end, qstart, qend);)
+ {
+ if(listincludes(del, start, end-start) < 0)
+ {
+ if(!p.empty()) p.add(' ');
+ p.put(qstart, qend-qstart);
+ }
+ }
+ p.add('\0');
+ return newstring(p.getbuf(), p.length()-1);
+}
+ICOMMAND(listdel, "ss", (char *list, char *del), commandret->setstr(listdel(list, del)));
+
+void listsplice(const char *s, const char *vals, int *skip, int *count)
+{
+ int offset = max(*skip, 0), len = max(*count, 0);
+ const char *list = s, *start, *end, *qstart, *qend = s;
+ loopi(offset) if(!parselist(s, start, end, qstart, qend)) break;
+ vector<char> p;
+ if(qend > list) p.put(list, qend-list);
+ if(*vals)
+ {
+ if(!p.empty()) p.add(' ');
+ p.put(vals, strlen(vals));
+ }
+ loopi(len) if(!parselist(s)) break;
+ skiplist(s);
+ switch(*s)
+ {
+ case '\0': case ')': case ']': break;
+ default:
+ if(!p.empty()) p.add(' ');
+ p.put(s, strlen(s));
+ break;
+ }
+ p.add('\0');
+ commandret->setstr(newstring(p.getbuf(), p.length()-1));
+}
+COMMAND(listsplice, "ssii");
+
+ICOMMAND(loopfiles, "rsse", (ident *id, char *dir, char *ext, uint *body),
+{
+ if(id->type!=ID_ALIAS) return;
+ identstack stack;
+ vector<char *> files;
+ listfiles(dir, ext[0] ? ext : NULL, files);
+ loopvrev(files)
+ {
+ char *file = files[i];
+ bool redundant = false;
+ loopj(i) if(!strcmp(files[j], file)) { redundant = true; break; }
+ if(redundant) delete[] files.removeunordered(i);
+ }
+ loopv(files)
+ {
+ char *file = files[i];
+ if(i)
+ {
+ if(id->valtype == VAL_STR) delete[] id->val.s;
+ else id->valtype = VAL_STR;
+ id->val.s = file;
+ }
+ else
+ {
+ tagval t;
+ t.setstr(file);
+ pusharg(*id, t, stack);
+ id->flags &= ~IDF_UNKNOWN;
+ }
+ execute(body);
+ }
+ if(files.length()) poparg(*id);
+});
+
+void findfile_(char *name)
+{
+ string fname;
+ copystring(fname, name);
+ path(fname);
+ intret(
+#ifndef STANDALONE
+ findzipfile(fname) ||
+#endif
+ fileexists(fname, "e") || findfile(fname, "e") ? 1 : 0
+ );
+}
+COMMANDN(findfile, findfile_, "s");
+
+struct sortitem
+{
+ const char *str, *quotestart, *quoteend;
+};
+
+struct sortfun
+{
+ ident *x, *y;
+ uint *body;
+
+ bool operator()(const sortitem &xval, const sortitem &yval)
+ {
+ if(x->valtype != VAL_MACRO) x->valtype = VAL_MACRO;
+ cleancode(*x);
+ x->val.code = (const uint *)xval.str;
+ if(y->valtype != VAL_MACRO) y->valtype = VAL_MACRO;
+ cleancode(*y);
+ y->val.code = (const uint *)yval.str;
+ return executebool(body);
+ }
+};
+
+void sortlist(char *list, ident *x, ident *y, uint *body)
+{
+ if(x == y || x->type != ID_ALIAS || y->type != ID_ALIAS) return;
+
+ vector<sortitem> items;
+ int macrolen = strlen(list), total = 0;
+ char *macros = newstring(list, macrolen);
+ const char *curlist = list, *start, *end, *quotestart, *quoteend;
+ while(parselist(curlist, start, end, quotestart, quoteend))
+ {
+ macros[end - list] = '\0';
+ sortitem item = { &macros[start - list], quotestart, quoteend };
+ items.add(item);
+ total += int(quoteend - quotestart);
+ }
+
+ identstack xstack, ystack;
+ pusharg(*x, nullval, xstack); x->flags &= ~IDF_UNKNOWN;
+ pusharg(*y, nullval, ystack); y->flags &= ~IDF_UNKNOWN;
+
+ sortfun f = { x, y, body };
+ items.sort(f);
+
+ poparg(*x);
+ poparg(*y);
+
+ char *sorted = macros;
+ int sortedlen = total + max(items.length() - 1, 0);
+ if(macrolen < sortedlen)
+ {
+ delete[] macros;
+ sorted = newstring(sortedlen);
+ }
+
+ int offset = 0;
+ loopv(items)
+ {
+ sortitem &item = items[i];
+ int len = int(item.quoteend - item.quotestart);
+ if(i) sorted[offset++] = ' ';
+ memcpy(&sorted[offset], item.quotestart, len);
+ offset += len;
+ }
+ sorted[offset] = '\0';
+
+ commandret->setstr(sorted);
+}
+COMMAND(sortlist, "srre");
+
+ICOMMAND(+, "ii", (int *a, int *b), intret(*a + *b));
+ICOMMAND(*, "ii", (int *a, int *b), intret(*a * *b));
+ICOMMAND(-, "ii", (int *a, int *b), intret(*a - *b));
+ICOMMAND(+f, "ff", (float *a, float *b), floatret(*a + *b));
+ICOMMAND(*f, "ff", (float *a, float *b), floatret(*a * *b));
+ICOMMAND(-f, "ff", (float *a, float *b), floatret(*a - *b));
+ICOMMAND(=, "ii", (int *a, int *b), intret((int)(*a == *b)));
+ICOMMAND(!=, "ii", (int *a, int *b), intret((int)(*a != *b)));
+ICOMMAND(<, "ii", (int *a, int *b), intret((int)(*a < *b)));
+ICOMMAND(>, "ii", (int *a, int *b), intret((int)(*a > *b)));
+ICOMMAND(<=, "ii", (int *a, int *b), intret((int)(*a <= *b)));
+ICOMMAND(>=, "ii", (int *a, int *b), intret((int)(*a >= *b)));
+ICOMMAND(=f, "ff", (float *a, float *b), intret((int)(*a == *b)));
+ICOMMAND(!=f, "ff", (float *a, float *b), intret((int)(*a != *b)));
+ICOMMAND(<f, "ff", (float *a, float *b), intret((int)(*a < *b)));
+ICOMMAND(>f, "ff", (float *a, float *b), intret((int)(*a > *b)));
+ICOMMAND(<=f, "ff", (float *a, float *b), intret((int)(*a <= *b)));
+ICOMMAND(>=f, "ff", (float *a, float *b), intret((int)(*a >= *b)));
+ICOMMAND(^, "ii", (int *a, int *b), intret(*a ^ *b));
+ICOMMAND(!, "t", (tagval *a), intret(!getbool(*a)));
+ICOMMAND(&, "ii", (int *a, int *b), intret(*a & *b));
+ICOMMAND(|, "ii", (int *a, int *b), intret(*a | *b));
+ICOMMAND(~, "i", (int *a), intret(~*a));
+ICOMMAND(^~, "ii", (int *a, int *b), intret(*a ^ ~*b));
+ICOMMAND(&~, "ii", (int *a, int *b), intret(*a & ~*b));
+ICOMMAND(|~, "ii", (int *a, int *b), intret(*a | ~*b));
+ICOMMAND(<<, "ii", (int *a, int *b), intret(*b < 32 ? *a << max(*b, 0) : 0));
+ICOMMAND(>>, "ii", (int *a, int *b), intret(*a >> clamp(*b, 0, 31)));
+ICOMMAND(&&, "e1V", (tagval *args, int numargs),
+{
+ if(!numargs) intret(1);
+ else loopi(numargs)
+ {
+ if(i) freearg(*commandret);
+ executeret(args[i].code, *commandret);
+ if(!getbool(*commandret)) break;
+ }
+});
+ICOMMAND(||, "e1V", (tagval *args, int numargs),
+{
+ if(!numargs) intret(0);
+ else loopi(numargs)
+ {
+ if(i) freearg(*commandret);
+ executeret(args[i].code, *commandret);
+ if(getbool(*commandret)) break;
+ }
+});
+
+ICOMMAND(div, "ii", (int *a, int *b), intret(*b ? *a / *b : 0));
+ICOMMAND(mod, "ii", (int *a, int *b), intret(*b ? *a % *b : 0));
+ICOMMAND(divf, "ff", (float *a, float *b), floatret(*b ? *a / *b : 0));
+ICOMMAND(modf, "ff", (float *a, float *b), floatret(*b ? fmod(*a, *b) : 0));
+ICOMMAND(sin, "f", (float *a), floatret(sin(*a*RAD)));
+ICOMMAND(cos, "f", (float *a), floatret(cos(*a*RAD)));
+ICOMMAND(tan, "f", (float *a), floatret(tan(*a*RAD)));
+ICOMMAND(asin, "f", (float *a), floatret(asin(*a)/RAD));
+ICOMMAND(acos, "f", (float *a), floatret(acos(*a)/RAD));
+ICOMMAND(atan, "f", (float *a), floatret(atan(*a)/RAD));
+ICOMMAND(atan2, "ff", (float *y, float *x), floatret(atan2(*y, *x)/RAD));
+ICOMMAND(sqrt, "f", (float *a), floatret(sqrt(*a)));
+ICOMMAND(pow, "ff", (float *a, float *b), floatret(pow(*a, *b)));
+ICOMMAND(loge, "f", (float *a), floatret(log(*a)));
+ICOMMAND(log2, "f", (float *a), floatret(log(*a)/M_LN2));
+ICOMMAND(log10, "f", (float *a), floatret(log10(*a)));
+ICOMMAND(exp, "f", (float *a), floatret(exp(*a)));
+ICOMMAND(min, "V", (tagval *args, int numargs),
+{
+ int val = numargs > 0 ? args[numargs - 1].getint() : 0;
+ loopi(numargs - 1) val = min(val, args[i].getint());
+ intret(val);
+});
+ICOMMAND(max, "V", (tagval *args, int numargs),
+{
+ int val = numargs > 0 ? args[numargs - 1].getint() : 0;
+ loopi(numargs - 1) val = max(val, args[i].getint());
+ intret(val);
+});
+ICOMMAND(minf, "V", (tagval *args, int numargs),
+{
+ float val = numargs > 0 ? args[numargs - 1].getfloat() : 0.0f;
+ loopi(numargs - 1) val = min(val, args[i].getfloat());
+ floatret(val);
+});
+ICOMMAND(maxf, "V", (tagval *args, int numargs),
+{
+ float val = numargs > 0 ? args[numargs - 1].getfloat() : 0.0f;
+ loopi(numargs - 1) val = max(val, args[i].getfloat());
+ floatret(val);
+});
+ICOMMAND(abs, "i", (int *n), intret(abs(*n)));
+ICOMMAND(absf, "f", (float *n), floatret(fabs(*n)));
+
+ICOMMAND(floor, "f", (float *n), floatret(floor(*n)));
+ICOMMAND(ceil, "f", (float *n), floatret(ceil(*n)));
+ICOMMAND(round, "ff", (float *n, float *k),
+{
+ double step = *k;
+ double r = *n;
+ if(step > 0)
+ {
+ r += step * (r < 0 ? -0.5 : 0.5);
+ r -= fmod(r, step);
+ }
+ else r = r < 0 ? ceil(r - 0.5) : floor(r + 0.5);
+ floatret(float(r));
+});
+
+ICOMMAND(cond, "ee2V", (tagval *args, int numargs),
+{
+ for(int i = 0; i < numargs; i += 2)
+ {
+ if(i+1 < numargs)
+ {
+ if(executebool(args[i].code))
+ {
+ executeret(args[i+1].code, *commandret);
+ break;
+ }
+ }
+ else
+ {
+ executeret(args[i].code, *commandret);
+ break;
+ }
+ }
+});
+#define CASECOMMAND(name, fmt, type, acc, compare) \
+ ICOMMAND(name, fmt "te2V", (tagval *args, int numargs), \
+ { \
+ type val = acc; \
+ int i; \
+ for(i = 1; i+1 < numargs; i += 2) \
+ { \
+ if(compare) \
+ { \
+ executeret(args[i+1].code, *commandret); \
+ return; \
+ } \
+ } \
+ })
+CASECOMMAND(case, "i", int, args[0].getint(), args[i].type == VAL_NULL || args[i].getint() == val);
+CASECOMMAND(casef, "f", float, args[0].getfloat(), args[i].type == VAL_NULL || args[i].getfloat() == val);
+CASECOMMAND(cases, "s", const char *, args[0].getstr(), args[i].type == VAL_NULL || !strcmp(args[i].getstr(), val));
+
+ICOMMAND(rnd, "ii", (int *a, int *b), intret(*a - *b > 0 ? rnd(*a - *b) + *b : *b));
+ICOMMAND(rndstr, "i", (int *len),
+{
+ int n = clamp(*len, 0, 10000);
+ char *s = newstring(n);
+ for(int i = 0; i < n;)
+ {
+ uint r = randomMT();
+ for(int j = min(i + 4, n); i < j; i++)
+ {
+ s[i] = (r%255) + 1;
+ r /= 255;
+ }
+ }
+ s[n] = '\0';
+ stringret(s);
+});
+
+ICOMMAND(strcmp, "ss", (char *a, char *b), intret(strcmp(a,b)==0));
+ICOMMAND(=s, "ss", (char *a, char *b), intret(strcmp(a,b)==0));
+ICOMMAND(!=s, "ss", (char *a, char *b), intret(strcmp(a,b)!=0));
+ICOMMAND(<s, "ss", (char *a, char *b), intret(strcmp(a,b)<0));
+ICOMMAND(>s, "ss", (char *a, char *b), intret(strcmp(a,b)>0));
+ICOMMAND(<=s, "ss", (char *a, char *b), intret(strcmp(a,b)<=0));
+ICOMMAND(>=s, "ss", (char *a, char *b), intret(strcmp(a,b)>=0));
+ICOMMAND(echo, "C", (char *s), conoutf(CON_ECHO, "\f1%s", s));
+ICOMMAND(error, "C", (char *s), conoutf(CON_ERROR, "%s", s));
+ICOMMAND(strstr, "ss", (char *a, char *b), { char *s = strstr(a, b); intret(s ? s-a : -1); });
+ICOMMAND(strlen, "s", (char *s), intret(strlen(s)));
+ICOMMAND(strcode, "si", (char *s, int *i), intret(*i > 0 ? (memchr(s, 0, *i) ? 0 : uchar(s[*i])) : uchar(s[0])));
+ICOMMAND(codestr, "i", (int *i), { char *s = newstring(1); s[0] = char(*i); s[1] = '\0'; stringret(s); });
+ICOMMAND(struni, "si", (char *s, int *i), intret(*i > 0 ? (memchr(s, 0, *i) ? 0 : cube2uni(s[*i])) : cube2uni(s[0])));
+ICOMMAND(unistr, "i", (int *i), { char *s = newstring(1); s[0] = uni2cube(*i); s[1] = '\0'; stringret(s); });
+
+int naturalsort(const char *a, const char *b)
+{
+ for(;;)
+ {
+ int ac = *a, bc = *b;
+ if(!ac) return bc ? -1 : 0;
+ else if(!bc) return 1;
+ else if(isdigit(ac) && isdigit(bc))
+ {
+ while(*a == '0') a++;
+ while(*b == '0') b++;
+ const char *a0 = a, *b0 = b;
+ while(isdigit(*a)) a++;
+ while(isdigit(*b)) b++;
+ int alen = a - a0, blen = b - b0;
+ if(alen != blen) return alen - blen;
+ int n = memcmp(a0, b0, alen);
+ if(n < 0) return -1;
+ else if(n > 0) return 1;
+ }
+ else if(ac != bc) return ac - bc;
+ else { ++a; ++b; }
+ }
+}
+ICOMMAND(naturalsort, "ss", (char *a, char *b), intret(naturalsort(a,b)<=0));
+
+#define STRMAPCOMMAND(name, map) \
+ ICOMMAND(name, "s", (char *s), \
+ { \
+ int len = strlen(s); \
+ char *m = newstring(len); \
+ loopi(len) m[i] = map(s[i]); \
+ m[len] = '\0'; \
+ stringret(m); \
+ })
+
+STRMAPCOMMAND(strlower, cubelower);
+STRMAPCOMMAND(strupper, cubeupper);
+
+char *strreplace(const char *s, const char *oldval, const char *newval)
+{
+ vector<char> buf;
+
+ int oldlen = strlen(oldval);
+ if(!oldlen) return newstring(s);
+ for(;;)
+ {
+ const char *found = strstr(s, oldval);
+ if(found)
+ {
+ while(s < found) buf.add(*s++);
+ for(const char *n = newval; *n; n++) buf.add(*n);
+ s = found + oldlen;
+ }
+ else
+ {
+ while(*s) buf.add(*s++);
+ buf.add('\0');
+ return newstring(buf.getbuf(), buf.length());
+ }
+ }
+}
+
+ICOMMAND(strreplace, "sss", (char *s, char *o, char *n), commandret->setstr(strreplace(s, o, n)));
+
+void strsplice(const char *s, const char *vals, int *skip, int *count)
+{
+ int slen = strlen(s), vlen = strlen(vals),
+ offset = clamp(*skip, 0, slen),
+ len = clamp(*count, 0, slen - offset);
+ char *p = newstring(slen - len + vlen);
+ if(offset) memcpy(p, s, offset);
+ if(vlen) memcpy(&p[offset], vals, vlen);
+ if(offset + len < slen) memcpy(&p[offset + vlen], &s[offset + len], slen - (offset + len));
+ p[slen - len + vlen] = '\0';
+ commandret->setstr(p);
+}
+COMMAND(strsplice, "ssii");
+
+#ifndef STANDALONE
+ICOMMAND(getmillis, "i", (int *total), intret(*total ? totalmillis : lastmillis));
+
+struct sleepcmd
+{
+ int delay, millis, flags;
+ char *command;
+};
+vector<sleepcmd> sleepcmds;
+
+void addsleep(int *msec, char *cmd)
+{
+ sleepcmd &s = sleepcmds.add();
+ s.delay = max(*msec, 1);
+ s.millis = lastmillis;
+ s.command = newstring(cmd);
+ s.flags = identflags;
+}
+
+COMMANDN(sleep, addsleep, "is");
+
+void checksleep(int millis)
+{
+ loopv(sleepcmds)
+ {
+ sleepcmd &s = sleepcmds[i];
+ if(millis - s.millis >= s.delay)
+ {
+ char *cmd = s.command; // execute might create more sleep commands
+ s.command = NULL;
+ int oldflags = identflags;
+ identflags = s.flags;
+ execute(cmd);
+ identflags = oldflags;
+ delete[] cmd;
+ if(sleepcmds.inrange(i) && !sleepcmds[i].command) sleepcmds.remove(i--);
+ }
+ }
+}
+
+void clearsleep(bool clearoverrides)
+{
+ int len = 0;
+ loopv(sleepcmds) if(sleepcmds[i].command)
+ {
+ if(clearoverrides && !(sleepcmds[i].flags&IDF_OVERRIDDEN)) sleepcmds[len++] = sleepcmds[i];
+ else delete[] sleepcmds[i].command;
+ }
+ sleepcmds.shrink(len);
+}
+
+void clearsleep_(int *clearoverrides)
+{
+ clearsleep(*clearoverrides!=0 || identflags&IDF_OVERRIDDEN);
+}
+
+COMMANDN(clearsleep, clearsleep_, "i");
+#endif
+
diff --git a/src/engine/console.cpp b/src/engine/console.cpp
new file mode 100644
index 0000000..d6e83a1
--- /dev/null
+++ b/src/engine/console.cpp
@@ -0,0 +1,785 @@
+// console.cpp: the console buffer, its display, and command line control
+
+#include "engine.h"
+
+#define MAXCONLINES 1000
+struct cline { char *line; int type, outtime; };
+reversequeue<cline, MAXCONLINES> conlines;
+
+int commandmillis = -1;
+string commandbuf;
+char *commandaction = NULL, *commandprompt = NULL;
+enum { CF_COMPLETE = 1<<0, CF_EXECUTE = 1<<1 };
+int commandflags = 0, commandpos = -1;
+
+VARFP(maxcon, 10, 200, MAXCONLINES, { while(conlines.length() > maxcon) delete[] conlines.pop().line; });
+
+#define CONSTRLEN 512
+
+VARP(contags, 0, 3, 3);
+
+void conline(int type, const char *sf) // add a line to the console buffer
+{
+ char *buf = NULL;
+ if(type&CON_TAG_MASK) for(int i = conlines.length()-1; i >= max(conlines.length()-contags, 0); i--)
+ {
+ int prev = conlines.removing(i).type;
+ if(!(prev&CON_TAG_MASK)) break;
+ if(type == prev)
+ {
+ buf = conlines.remove(i).line;
+ break;
+ }
+ }
+ if(!buf) buf = conlines.length() >= maxcon ? conlines.remove().line : newstring("", CONSTRLEN-1);
+ cline &cl = conlines.add();
+ cl.line = buf;
+ cl.type = type;
+ cl.outtime = totalmillis; // for how long to keep line on screen
+ copystring(cl.line, sf, CONSTRLEN);
+}
+
+void conoutfv(int type, const char *fmt, va_list args)
+{
+ static char buf[CONSTRLEN];
+ vformatstring(buf, fmt, args, sizeof(buf));
+ conline(type, buf);
+ logoutf("%s", buf);
+}
+
+VAR(fullconsole, 0, 0, 1);
+ICOMMAND(toggleconsole, "", (), { fullconsole ^= 1; });
+
+int rendercommand(int x, int y, int w)
+{
+ if(commandmillis < 0) return 0;
+
+ defformatstring(s, "%s %s", commandprompt ? commandprompt : ">", commandbuf);
+ int width, height;
+ text_bounds(s, width, height, w);
+ y -= height;
+ draw_text(s, x, y, 0xFF, 0xFF, 0xFF, 0xFF, (commandpos>=0) ? (commandpos+1+(commandprompt?strlen(commandprompt):1)) : strlen(s), w);
+ return height;
+}
+
+VARP(consize, 0, 5, 100);
+VARP(miniconsize, 0, 5, 100);
+VARP(miniconwidth, 0, 40, 100);
+VARP(confade, 0, 30, 60);
+VARP(miniconfade, 0, 30, 60);
+VARP(fullconsize, 0, 75, 100);
+HVARP(confilter, 0, 0x7FFFFFF, 0x7FFFFFF);
+HVARP(fullconfilter, 0, 0x7FFFFFF, 0x7FFFFFF);
+HVARP(miniconfilter, 0, 0, 0x7FFFFFF);
+
+int conskip = 0, miniconskip = 0;
+
+void setconskip(int &skip, int filter, int n)
+{
+ filter &= CON_FLAGS;
+ int offset = abs(n), dir = n < 0 ? -1 : 1;
+ skip = clamp(skip, 0, conlines.length()-1);
+ while(offset)
+ {
+ skip += dir;
+ if(!conlines.inrange(skip))
+ {
+ skip = clamp(skip, 0, conlines.length()-1);
+ return;
+ }
+ if(conlines[skip].type&filter) --offset;
+ }
+}
+
+ICOMMAND(conskip, "i", (int *n), setconskip(conskip, fullconsole ? fullconfilter : confilter, *n));
+ICOMMAND(miniconskip, "i", (int *n), setconskip(miniconskip, miniconfilter, *n));
+
+ICOMMAND(clearconsole, "", (), { while(conlines.length()) delete[] conlines.pop().line; });
+
+int drawconlines(int conskip, int confade, int conwidth, int conheight, int conoff, int filter, int y = 0, int dir = 1)
+{
+ filter &= CON_FLAGS;
+ int numl = conlines.length(), offset = min(conskip, numl);
+
+ if(confade)
+ {
+ if(!conskip)
+ {
+ numl = 0;
+ loopvrev(conlines) if(totalmillis-conlines[i].outtime < confade*1000) { numl = i+1; break; }
+ }
+ else offset--;
+ }
+
+ int totalheight = 0;
+ loopi(numl) //determine visible height
+ {
+ // shuffle backwards to fill if necessary
+ int idx = offset+i < numl ? offset+i : --offset;
+ if(!(conlines[idx].type&filter)) continue;
+ char *line = conlines[idx].line;
+ int width, height;
+ text_bounds(line, width, height, conwidth);
+ if(totalheight + height > conheight) { numl = i; if(offset == idx) ++offset; break; }
+ totalheight += height;
+ }
+ if(dir > 0) y = conoff;
+ loopi(numl)
+ {
+ int idx = offset + (dir > 0 ? numl-i-1 : i);
+ if(!(conlines[idx].type&filter)) continue;
+ char *line = conlines[idx].line;
+ int width, height;
+ text_bounds(line, width, height, conwidth);
+ if(dir <= 0) y -= height;
+ draw_text(line, conoff, y, 0xFF, 0xFF, 0xFF, 0xFF, -1, conwidth);
+ if(dir > 0) y += height;
+ }
+ return y+conoff;
+}
+
+int renderconsole(int w, int h, int abovehud) // render buffer taking into account time & scrolling
+{
+ int conpad = fullconsole ? 0 : FONTH/4,
+ conoff = fullconsole ? FONTH : FONTH/3,
+ conheight = min(fullconsole ? ((h*fullconsize/100)/FONTH)*FONTH : FONTH*consize, h - 2*(conpad + conoff)),
+ conwidth = w - 2*(conpad + conoff) - (fullconsole ? 0 : game::clipconsole(w, h));
+
+ extern void consolebox(int x1, int y1, int x2, int y2);
+ if(fullconsole) consolebox(conpad, conpad, conwidth+conpad+2*conoff, conheight+conpad+2*conoff);
+
+ int y = drawconlines(conskip, fullconsole ? 0 : confade, conwidth, conheight, conpad+conoff, fullconsole ? fullconfilter : confilter);
+ if(!fullconsole && (miniconsize && miniconwidth))
+ drawconlines(miniconskip, miniconfade, (miniconwidth*(w - 2*(conpad + conoff)))/100, min(FONTH*miniconsize, abovehud - y), conpad+conoff, miniconfilter, abovehud, -1);
+ return fullconsole ? conheight + 2*(conpad + conoff) : y;
+}
+
+// keymap is defined externally in keymap.cfg
+
+struct keym
+{
+ enum
+ {
+ ACTION_DEFAULT = 0,
+ ACTION_SPECTATOR,
+ ACTION_EDITING,
+ NUMACTIONS
+ };
+
+ int code;
+ char *name;
+ char *actions[NUMACTIONS];
+ bool pressed;
+
+ keym() : code(-1), name(NULL), pressed(false) { loopi(NUMACTIONS) actions[i] = newstring(""); }
+ ~keym() { DELETEA(name); loopi(NUMACTIONS) DELETEA(actions[i]); }
+};
+
+hashtable<int, keym> keyms(128);
+
+void keymap(int *code, char *key)
+{
+ if(identflags&IDF_OVERRIDDEN) { conoutf(CON_ERROR, "cannot override keymap %d", *code); return; }
+ keym &km = keyms[*code];
+ km.code = *code;
+ DELETEA(km.name);
+ km.name = newstring(key);
+}
+
+COMMAND(keymap, "is");
+
+keym *keypressed = NULL;
+char *keyaction = NULL;
+
+const char *getkeyname(int code)
+{
+ keym *km = keyms.access(code);
+ return km ? km->name : NULL;
+}
+
+void searchbinds(char *action, int type)
+{
+ vector<char> names;
+ enumerate(keyms, keym, km,
+ {
+ if(!strcmp(km.actions[type], action))
+ {
+ if(names.length()) names.add(' ');
+ names.put(km.name, strlen(km.name));
+ }
+ });
+ names.add('\0');
+ result(names.getbuf());
+}
+
+keym *findbind(char *key)
+{
+ enumerate(keyms, keym, km,
+ {
+ if(!strcasecmp(km.name, key)) return &km;
+ });
+ return NULL;
+}
+
+void getbind(char *key, int type)
+{
+ keym *km = findbind(key);
+ result(km ? km->actions[type] : "");
+}
+
+void bindkey(char *key, char *action, int state, const char *cmd)
+{
+ if(identflags&IDF_OVERRIDDEN) { conoutf(CON_ERROR, "cannot override %s \"%s\"", cmd, key); return; }
+ keym *km = findbind(key);
+ if(!km) { conoutf(CON_ERROR, "unknown key \"%s\"", key); return; }
+ char *&binding = km->actions[state];
+ if(!keypressed || keyaction!=binding) delete[] binding;
+ // trim white-space to make searchbinds more reliable
+ while(iscubespace(*action)) action++;
+ int len = strlen(action);
+ while(len>0 && iscubespace(action[len-1])) len--;
+ binding = newstring(action, len);
+}
+
+ICOMMAND(bind, "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_DEFAULT, "bind"));
+ICOMMAND(specbind, "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_SPECTATOR, "specbind"));
+ICOMMAND(editbind, "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_EDITING, "editbind"));
+ICOMMAND(getbind, "s", (char *key), getbind(key, keym::ACTION_DEFAULT));
+ICOMMAND(getspecbind, "s", (char *key), getbind(key, keym::ACTION_SPECTATOR));
+ICOMMAND(geteditbind, "s", (char *key), getbind(key, keym::ACTION_EDITING));
+ICOMMAND(searchbinds, "s", (char *action), searchbinds(action, keym::ACTION_DEFAULT));
+ICOMMAND(searchspecbinds, "s", (char *action), searchbinds(action, keym::ACTION_SPECTATOR));
+ICOMMAND(searcheditbinds, "s", (char *action), searchbinds(action, keym::ACTION_EDITING));
+
+void inputcommand(char *init, char *action = NULL, char *prompt = NULL, char *flags = NULL) // turns input to the command line on or off
+{
+ commandmillis = init ? totalmillis : -1;
+ textinput(commandmillis >= 0, TI_CONSOLE);
+ keyrepeat(commandmillis >= 0, KR_CONSOLE);
+ copystring(commandbuf, init ? init : "");
+ DELETEA(commandaction);
+ DELETEA(commandprompt);
+ commandpos = -1;
+ if(action && action[0]) commandaction = newstring(action);
+ if(prompt && prompt[0]) commandprompt = newstring(prompt);
+ commandflags = 0;
+ if(flags) while(*flags) switch(*flags++)
+ {
+ case 'c': commandflags |= CF_COMPLETE; break;
+ case 'x': commandflags |= CF_EXECUTE; break;
+ case 's': commandflags |= CF_COMPLETE|CF_EXECUTE; break;
+ }
+ else if(init) commandflags |= CF_COMPLETE|CF_EXECUTE;
+}
+
+ICOMMAND(saycommand, "C", (char *init), inputcommand(init));
+COMMAND(inputcommand, "ssss");
+
+void pasteconsole()
+{
+ if(!SDL_HasClipboardText()) return;
+ char *cb = SDL_GetClipboardText();
+ if(!cb) return;
+ size_t cblen = strlen(cb),
+ commandlen = strlen(commandbuf),
+ decoded = decodeutf8((uchar *)&commandbuf[commandlen], sizeof(commandbuf)-1-commandlen, (const uchar *)cb, cblen);
+ commandbuf[commandlen + decoded] = '\0';
+ SDL_free(cb);
+}
+
+struct hline
+{
+ char *buf, *action, *prompt;
+ int flags;
+
+ hline() : buf(NULL), action(NULL), prompt(NULL), flags(0) {}
+ ~hline()
+ {
+ DELETEA(buf);
+ DELETEA(action);
+ DELETEA(prompt);
+ }
+
+ void restore()
+ {
+ copystring(commandbuf, buf);
+ if(commandpos >= (int)strlen(commandbuf)) commandpos = -1;
+ DELETEA(commandaction);
+ DELETEA(commandprompt);
+ if(action) commandaction = newstring(action);
+ if(prompt) commandprompt = newstring(prompt);
+ commandflags = flags;
+ }
+
+ bool shouldsave()
+ {
+ return strcmp(commandbuf, buf) ||
+ (commandaction ? !action || strcmp(commandaction, action) : action!=NULL) ||
+ (commandprompt ? !prompt || strcmp(commandprompt, prompt) : prompt!=NULL) ||
+ commandflags != flags;
+ }
+
+ void save()
+ {
+ buf = newstring(commandbuf);
+ if(commandaction) action = newstring(commandaction);
+ if(commandprompt) prompt = newstring(commandprompt);
+ flags = commandflags;
+ }
+
+ void run()
+ {
+ if(flags&CF_EXECUTE && buf[0]=='/') execute(buf+1);
+ else if(action)
+ {
+ alias("commandbuf", buf);
+ execute(action);
+ }
+ else game::toserver(buf);
+ }
+};
+vector<hline *> history;
+int histpos = 0;
+
+VARP(maxhistory, 0, 1000, 10000);
+
+void history_(int *n)
+{
+ static bool inhistory = false;
+ if(!inhistory && history.inrange(*n))
+ {
+ inhistory = true;
+ history[history.length()-*n-1]->run();
+ inhistory = false;
+ }
+}
+
+COMMANDN(history, history_, "i");
+
+struct releaseaction
+{
+ keym *key;
+ char *action;
+};
+vector<releaseaction> releaseactions;
+
+const char *addreleaseaction(char *s)
+{
+ if(!keypressed) { delete[] s; return NULL; }
+ releaseaction &ra = releaseactions.add();
+ ra.key = keypressed;
+ ra.action = s;
+ return keypressed->name;
+}
+
+void onrelease(const char *s)
+{
+ addreleaseaction(newstring(s));
+}
+
+COMMAND(onrelease, "s");
+
+void execbind(keym &k, bool isdown)
+{
+ loopv(releaseactions)
+ {
+ releaseaction &ra = releaseactions[i];
+ if(ra.key==&k)
+ {
+ if(!isdown) execute(ra.action);
+ delete[] ra.action;
+ releaseactions.remove(i--);
+ }
+ }
+ if(isdown)
+ {
+ int state = keym::ACTION_DEFAULT;
+ if(!mainmenu)
+ {
+ if(editmode) state = keym::ACTION_EDITING;
+ else if(player->state==CS_SPECTATOR) state = keym::ACTION_SPECTATOR;
+ }
+ char *&action = k.actions[state][0] ? k.actions[state] : k.actions[keym::ACTION_DEFAULT];
+ keyaction = action;
+ keypressed = &k;
+ execute(keyaction);
+ keypressed = NULL;
+ if(keyaction!=action) delete[] keyaction;
+ }
+ k.pressed = isdown;
+}
+
+bool consoleinput(const char *str, int len)
+{
+ if(commandmillis < 0) return false;
+
+ resetcomplete();
+ int cmdlen = (int)strlen(commandbuf), cmdspace = int(sizeof(commandbuf)) - (cmdlen+1);
+ len = min(len, cmdspace);
+ if(commandpos<0)
+ {
+ memcpy(&commandbuf[cmdlen], str, len);
+ }
+ else
+ {
+ memmove(&commandbuf[commandpos+len], &commandbuf[commandpos], cmdlen - commandpos);
+ memcpy(&commandbuf[commandpos], str, len);
+ commandpos += len;
+ }
+ commandbuf[cmdlen + len] = '\0';
+
+ return true;
+}
+
+bool consolekey(int code, bool isdown)
+{
+ if(commandmillis < 0) return false;
+
+ #ifdef __APPLE__
+ #define MOD_KEYS (KMOD_LGUI|KMOD_RGUI)
+ #else
+ #define MOD_KEYS (KMOD_LCTRL|KMOD_RCTRL)
+ #endif
+
+ if(isdown)
+ {
+ switch(code)
+ {
+ case SDLK_RETURN:
+ case SDLK_KP_ENTER:
+ break;
+
+ case SDLK_HOME:
+ if(strlen(commandbuf)) commandpos = 0;
+ break;
+
+ case SDLK_END:
+ commandpos = -1;
+ break;
+
+ case SDLK_DELETE:
+ {
+ int len = (int)strlen(commandbuf);
+ if(commandpos<0) break;
+ memmove(&commandbuf[commandpos], &commandbuf[commandpos+1], len - commandpos);
+ resetcomplete();
+ if(commandpos>=len-1) commandpos = -1;
+ break;
+ }
+
+ case SDLK_BACKSPACE:
+ {
+ int len = (int)strlen(commandbuf), i = commandpos>=0 ? commandpos : len;
+ if(i<1) break;
+ memmove(&commandbuf[i-1], &commandbuf[i], len - i + 1);
+ resetcomplete();
+ if(commandpos>0) commandpos--;
+ else if(!commandpos && len<=1) commandpos = -1;
+ break;
+ }
+
+ case SDLK_LEFT:
+ if(commandpos>0) commandpos--;
+ else if(commandpos<0) commandpos = (int)strlen(commandbuf)-1;
+ break;
+
+ case SDLK_RIGHT:
+ if(commandpos>=0 && ++commandpos>=(int)strlen(commandbuf)) commandpos = -1;
+ break;
+
+ case SDLK_UP:
+ if(histpos > history.length()) histpos = history.length();
+ if(histpos > 0) history[--histpos]->restore();
+ break;
+
+ case SDLK_DOWN:
+ if(histpos + 1 < history.length()) history[++histpos]->restore();
+ break;
+
+ case SDLK_TAB:
+ if(commandflags&CF_COMPLETE)
+ {
+ complete(commandbuf, sizeof(commandbuf), commandflags&CF_EXECUTE ? "/" : NULL);
+ if(commandpos>=0 && commandpos>=(int)strlen(commandbuf)) commandpos = -1;
+ }
+ break;
+
+ case SDLK_v:
+ if(SDL_GetModState()&MOD_KEYS) pasteconsole();
+ break;
+ }
+ }
+ else
+ {
+ if(code==SDLK_RETURN || code==SDLK_KP_ENTER)
+ {
+ hline *h = NULL;
+ if(commandbuf[0])
+ {
+ if(history.empty() || history.last()->shouldsave())
+ {
+ if(maxhistory && history.length() >= maxhistory)
+ {
+ loopi(history.length()-maxhistory+1) delete history[i];
+ history.remove(0, history.length()-maxhistory+1);
+ }
+ history.add(h = new hline)->save();
+ }
+ else h = history.last();
+ }
+ histpos = history.length();
+ inputcommand(NULL);
+ if(h) h->run();
+ }
+ else if(code==SDLK_ESCAPE)
+ {
+ histpos = history.length();
+ inputcommand(NULL);
+ }
+ }
+
+ return true;
+}
+
+void processtextinput(const char *str, int len)
+{
+ if(!g3d_input(str, len))
+ consoleinput(str, len);
+}
+
+void processkey(int code, bool isdown, int modstate)
+{
+ switch(code)
+ {
+ case SDLK_LGUI: case SDLK_RGUI:
+ return;
+ }
+ keym *haskey = keyms.access(code);
+ if(haskey && haskey->pressed) execbind(*haskey, isdown); // allow pressed keys to release
+ else if(!g3d_key(code, isdown)) // 3D GUI mouse button intercept
+ {
+ if(!consolekey(code, isdown))
+ {
+ if(modstate&KMOD_GUI) return;
+ if(haskey) execbind(*haskey, isdown);
+ }
+ }
+}
+
+void clear_console()
+{
+ keyms.clear();
+}
+
+void writebinds(stream *f)
+{
+ static const char * const cmds[3] = { "bind", "specbind", "editbind" };
+ vector<keym *> binds;
+ enumerate(keyms, keym, km, binds.add(&km));
+ binds.sortname();
+ loopj(3)
+ {
+ loopv(binds)
+ {
+ keym &km = *binds[i];
+ if(*km.actions[j])
+ {
+ if(validateblock(km.actions[j])) f->printf("%s %s [%s]\n", cmds[j], escapestring(km.name), km.actions[j]);
+ else f->printf("%s %s %s\n", cmds[j], escapestring(km.name), escapestring(km.actions[j]));
+ }
+ }
+ }
+}
+
+// tab-completion of all idents and base maps
+
+enum { FILES_DIR = 0, FILES_VAR, FILES_LIST };
+
+struct fileskey
+{
+ int type;
+ const char *dir, *ext;
+
+ fileskey() {}
+ fileskey(int type, const char *dir, const char *ext) : type(type), dir(dir), ext(ext) {}
+};
+
+static void cleanfilesdir(char *dir)
+{
+ int dirlen = (int)strlen(dir);
+ while(dirlen > 0 && (dir[dirlen-1] == '/' || dir[dirlen-1] == '\\'))
+ dir[--dirlen] = '\0';
+}
+
+struct filesval
+{
+ int type;
+ char *dir, *ext;
+ vector<char *> files;
+ int millis;
+
+ filesval(int type, const char *dir, const char *ext) : type(type), dir(newstring(dir)), ext(ext && ext[0] ? newstring(ext) : NULL), millis(-1) {}
+ ~filesval() { DELETEA(dir); DELETEA(ext); files.deletearrays(); }
+
+ void update()
+ {
+ if((type!=FILES_DIR && type!=FILES_VAR) || millis >= commandmillis) return;
+ files.deletearrays();
+ if(type==FILES_VAR)
+ {
+ string buf;
+ buf[0] = '\0';
+ if(ident *id = readident(dir)) switch(id->type)
+ {
+ case ID_SVAR: copystring(buf, *id->storage.s); break;
+ case ID_ALIAS: copystring(buf, id->getstr()); break;
+ }
+ if(!buf[0]) copystring(buf, ".");
+ cleanfilesdir(buf);
+ listfiles(buf, ext, files);
+ }
+ else listfiles(dir, ext, files);
+ files.sort();
+ loopv(files) if(i && !strcmp(files[i], files[i-1])) delete[] files.remove(i--);
+ millis = totalmillis;
+ }
+};
+
+static inline bool htcmp(const fileskey &x, const fileskey &y)
+{
+ return x.type==y.type && !strcmp(x.dir, y.dir) && (x.ext == y.ext || (x.ext && y.ext && !strcmp(x.ext, y.ext)));
+}
+
+static inline uint hthash(const fileskey &k)
+{
+ return hthash(k.dir);
+}
+
+static hashtable<fileskey, filesval *> completefiles;
+static hashtable<char *, filesval *> completions;
+
+int completesize = 0;
+char *lastcomplete = NULL;
+
+void resetcomplete() { completesize = 0; }
+
+void addcomplete(char *command, int type, char *dir, char *ext)
+{
+ if(identflags&IDF_OVERRIDDEN)
+ {
+ conoutf(CON_ERROR, "cannot override complete %s", command);
+ return;
+ }
+ if(!dir[0])
+ {
+ filesval **hasfiles = completions.access(command);
+ if(hasfiles) *hasfiles = NULL;
+ return;
+ }
+ if(type==FILES_DIR) cleanfilesdir(dir);
+ if(ext)
+ {
+ if(strchr(ext, '*')) ext[0] = '\0';
+ if(!ext[0]) ext = NULL;
+ }
+ fileskey key(type, dir, ext);
+ filesval **val = completefiles.access(key);
+ if(!val)
+ {
+ filesval *f = new filesval(type, dir, ext);
+ if(type==FILES_LIST) explodelist(dir, f->files);
+ val = &completefiles[fileskey(type, f->dir, f->ext)];
+ *val = f;
+ }
+ filesval **hasfiles = completions.access(command);
+ if(hasfiles) *hasfiles = *val;
+ else completions[newstring(command)] = *val;
+}
+
+void addfilecomplete(char *command, char *dir, char *ext)
+{
+ addcomplete(command, FILES_DIR, dir, ext);
+}
+
+void addvarcomplete(char *command, char *var, char *ext)
+{
+ addcomplete(command, FILES_VAR, var, ext);
+}
+
+void addlistcomplete(char *command, char *list)
+{
+ addcomplete(command, FILES_LIST, list, NULL);
+}
+
+COMMANDN(complete, addfilecomplete, "sss");
+COMMANDN(varcomplete, addvarcomplete, "sss");
+COMMANDN(listcomplete, addlistcomplete, "ss");
+
+void complete(char *s, int maxlen, const char *cmdprefix)
+{
+ int cmdlen = 0;
+ if(cmdprefix)
+ {
+ cmdlen = strlen(cmdprefix);
+ if(strncmp(s, cmdprefix, cmdlen)) prependstring(s, cmdprefix, maxlen);
+ }
+ if(!s[cmdlen]) return;
+ if(!completesize) { completesize = (int)strlen(&s[cmdlen]); DELETEA(lastcomplete); }
+
+ filesval *f = NULL;
+ if(completesize)
+ {
+ char *end = strchr(&s[cmdlen], ' ');
+ if(end) f = completions.find(stringslice(&s[cmdlen], end), NULL);
+ }
+
+ const char *nextcomplete = NULL;
+ if(f) // complete using filenames
+ {
+ int commandsize = strchr(&s[cmdlen], ' ')+1-s;
+ f->update();
+ loopv(f->files)
+ {
+ if(strncmp(f->files[i], &s[commandsize], completesize+cmdlen-commandsize)==0 &&
+ (!lastcomplete || strcmp(f->files[i], lastcomplete) > 0) && (!nextcomplete || strcmp(f->files[i], nextcomplete) < 0))
+ nextcomplete = f->files[i];
+ }
+ cmdprefix = s;
+ cmdlen = commandsize;
+ }
+ else // complete using command names
+ {
+ enumerate(idents, ident, id,
+ if(strncmp(id.name, &s[cmdlen], completesize)==0 &&
+ (!lastcomplete || strcmp(id.name, lastcomplete) > 0) && (!nextcomplete || strcmp(id.name, nextcomplete) < 0))
+ nextcomplete = id.name;
+ );
+ }
+ DELETEA(lastcomplete);
+ if(nextcomplete)
+ {
+ cmdlen = min(cmdlen, maxlen-1);
+ if(cmdlen) memmove(s, cmdprefix, cmdlen);
+ copystring(&s[cmdlen], nextcomplete, maxlen-cmdlen);
+ lastcomplete = newstring(nextcomplete);
+ }
+}
+
+void writecompletions(stream *f)
+{
+ vector<char *> cmds;
+ enumeratekt(completions, char *, k, filesval *, v, { if(v) cmds.add(k); });
+ cmds.sort();
+ loopv(cmds)
+ {
+ char *k = cmds[i];
+ filesval *v = completions[k];
+ if(v->type==FILES_LIST)
+ {
+ if(validateblock(v->dir)) f->printf("listcomplete %s [%s]\n", escapeid(k), v->dir);
+ else f->printf("listcomplete %s %s\n", escapeid(k), escapestring(v->dir));
+ }
+ else f->printf("%s %s %s %s\n", v->type==FILES_VAR ? "varcomplete" : "complete", escapeid(k), escapestring(v->dir), escapestring(v->ext ? v->ext : "*"));
+ }
+}
+
diff --git a/src/engine/decal.cpp b/src/engine/decal.cpp
new file mode 100644
index 0000000..98787dd
--- /dev/null
+++ b/src/engine/decal.cpp
@@ -0,0 +1,642 @@
+#include "engine.h"
+
+struct decalvert
+{
+ vec pos;
+ bvec4 color;
+ vec2 tc;
+};
+
+struct decalinfo
+{
+ int millis;
+ bvec color;
+ ushort startvert, endvert;
+};
+
+enum
+{
+ DF_RND4 = 1<<0,
+ DF_ROTATE = 1<<1,
+ DF_INVMOD = 1<<2,
+ DF_OVERBRIGHT = 1<<3,
+ DF_ADD = 1<<4,
+ DF_SATURATE = 1<<5
+};
+
+VARFP(maxdecaltris, 1, 1024, 16384, initdecals());
+VARP(decalfade, 1000, 10000, 60000);
+VAR(dbgdec, 0, 0, 1);
+
+struct decalrenderer
+{
+ const char *texname;
+ int flags, fadeintime, fadeouttime, timetolive;
+ Texture *tex;
+ decalinfo *decals;
+ int maxdecals, startdecal, enddecal;
+ decalvert *verts;
+ int maxverts, startvert, endvert, lastvert, availverts;
+ GLuint vbo;
+ bool dirty;
+
+ decalrenderer(const char *texname, int flags = 0, int fadeintime = 0, int fadeouttime = 1000, int timetolive = -1)
+ : texname(texname), flags(flags),
+ fadeintime(fadeintime), fadeouttime(fadeouttime), timetolive(timetolive),
+ tex(NULL),
+ decals(NULL), maxdecals(0), startdecal(0), enddecal(0),
+ verts(NULL), maxverts(0), startvert(0), endvert(0), lastvert(0), availverts(0),
+ vbo(0), dirty(false),
+ decalu(0), decalv(0)
+ {
+ }
+
+ ~decalrenderer()
+ {
+ DELETEA(decals);
+ DELETEA(verts);
+ }
+
+ void init(int tris)
+ {
+ if(decals)
+ {
+ DELETEA(decals);
+ maxdecals = startdecal = enddecal = 0;
+ }
+ if(verts)
+ {
+ DELETEA(verts);
+ maxverts = startvert = endvert = lastvert = availverts = 0;
+ }
+ decals = new decalinfo[tris];
+ maxdecals = tris;
+ tex = textureload(texname, 3);
+ maxverts = tris*3 + 3;
+ availverts = maxverts - 3;
+ verts = new decalvert[maxverts];
+ }
+
+ int hasdecals()
+ {
+ return enddecal < startdecal ? maxdecals - (startdecal - enddecal) : enddecal - startdecal;
+ }
+
+ void cleanup()
+ {
+ if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+ }
+
+ void cleardecals()
+ {
+ startdecal = enddecal = 0;
+ startvert = endvert = lastvert = 0;
+ availverts = maxverts - 3;
+ dirty = true;
+ }
+
+ int freedecal()
+ {
+ if(startdecal==enddecal) return 0;
+
+ decalinfo &d = decals[startdecal];
+ startdecal++;
+ if(startdecal >= maxdecals) startdecal = 0;
+
+ int removed = d.endvert < d.startvert ? maxverts - (d.startvert - d.endvert) : d.endvert - d.startvert;
+ startvert = d.endvert;
+ if(startvert==endvert) startvert = endvert = lastvert = 0;
+ availverts += removed;
+
+ return removed;
+ }
+
+ void fadedecal(decalinfo &d, uchar alpha)
+ {
+ bvec rgb;
+ if(flags&DF_OVERBRIGHT) rgb = bvec(128, 128, 128);
+ else
+ {
+ rgb = d.color;
+ if(flags&(DF_ADD|DF_INVMOD)) rgb.scale(alpha, 255);
+ }
+ bvec4 color(rgb, alpha);
+
+ decalvert *vert = &verts[d.startvert],
+ *end = &verts[d.endvert < d.startvert ? maxverts : d.endvert];
+ while(vert < end)
+ {
+ vert->color = color;
+ vert++;
+ }
+ if(d.endvert < d.startvert)
+ {
+ vert = verts;
+ end = &verts[d.endvert];
+ while(vert < end)
+ {
+ vert->color = color;
+ vert++;
+ }
+ }
+ dirty = true;
+ }
+
+ void clearfadeddecals()
+ {
+ int threshold = lastmillis - (timetolive>=0 ? timetolive : decalfade) - fadeouttime;
+ decalinfo *d = &decals[startdecal],
+ *end = &decals[enddecal < startdecal ? maxdecals : enddecal];
+ while(d < end && d->millis <= threshold) d++;
+ if(d >= end && enddecal < startdecal)
+ {
+ d = decals;
+ end = &decals[enddecal];
+ while(d < end && d->millis <= threshold) d++;
+ }
+ int prevstart = startdecal;
+ startdecal = d - decals;
+ if(prevstart == startdecal) return;
+ if(startdecal!=enddecal) startvert = decals[startdecal].startvert;
+ else startvert = endvert = lastvert = 0;
+ availverts = endvert < startvert ? startvert - endvert - 3 : maxverts - 3 - (endvert - startvert);
+ dirty = true;
+ }
+
+ void fadeindecals()
+ {
+ if(!fadeintime) return;
+ decalinfo *d = &decals[enddecal],
+ *end = &decals[enddecal < startdecal ? 0 : startdecal];
+ while(d > end)
+ {
+ d--;
+ int fade = lastmillis - d->millis;
+ if(fade >= fadeintime) return;
+ fadedecal(*d, (fade<<8)/fadeintime);
+ }
+ if(enddecal < startdecal)
+ {
+ d = &decals[maxdecals];
+ end = &decals[startdecal];
+ while(d > end)
+ {
+ d--;
+ int fade = lastmillis - d->millis;
+ if(fade >= fadeintime) return;
+ fadedecal(*d, (fade<<8)/fadeintime);
+ }
+ }
+ }
+
+ void fadeoutdecals()
+ {
+ decalinfo *d = &decals[startdecal],
+ *end = &decals[enddecal < startdecal ? maxdecals : enddecal];
+ int offset = (timetolive>=0 ? timetolive : decalfade) + fadeouttime - lastmillis;
+ while(d < end)
+ {
+ int fade = d->millis + offset;
+ if(fade >= fadeouttime) return;
+ fadedecal(*d, (fade<<8)/fadeouttime);
+ d++;
+ }
+ if(enddecal < startdecal)
+ {
+ d = decals;
+ end = &decals[enddecal];
+ while(d < end)
+ {
+ int fade = d->millis + offset;
+ if(fade >= fadeouttime) return;
+ fadedecal(*d, (fade<<8)/fadeouttime);
+ d++;
+ }
+ }
+ }
+
+ static void setuprenderstate()
+ {
+ enablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+
+ glDepthMask(GL_FALSE);
+ glEnable(GL_BLEND);
+
+ gle::enablevertex();
+ gle::enabletexcoord0();
+ gle::enablecolor();
+ }
+
+ static void cleanuprenderstate()
+ {
+ gle::clearvbo();
+
+ gle::disablevertex();
+ gle::disabletexcoord0();
+ gle::disablecolor();
+
+ glDepthMask(GL_TRUE);
+ glDisable(GL_BLEND);
+
+ disablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+ }
+
+ void render()
+ {
+ if(startvert==endvert) return;
+
+ if(flags&DF_OVERBRIGHT)
+ {
+ glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);
+ SETSHADER(overbrightdecal);
+ }
+ else
+ {
+ if(flags&DF_INVMOD) { glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR); zerofogcolor(); }
+ else if(flags&DF_ADD) { glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR); zerofogcolor(); }
+ else glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ if(flags&DF_SATURATE) SETSHADER(saturatedecal);
+ else foggedshader->set();
+ }
+
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+
+ if(!vbo) { glGenBuffers_(1, &vbo); dirty = true; }
+ gle::bindvbo(vbo);
+
+ int count = endvert < startvert ? maxverts - startvert : endvert - startvert;
+ if(dirty)
+ {
+ glBufferData_(GL_ARRAY_BUFFER, maxverts*sizeof(decalvert), NULL, GL_STREAM_DRAW);
+ glBufferSubData_(GL_ARRAY_BUFFER, 0, count*sizeof(decalvert), &verts[startvert]);
+ if(endvert < startvert)
+ {
+ glBufferSubData_(GL_ARRAY_BUFFER, count*sizeof(decalvert), endvert*sizeof(decalvert), verts);
+ count += endvert;
+ }
+ dirty = false;
+ }
+ else if(endvert < startvert) count += endvert;
+
+ const decalvert *ptr = 0;
+ gle::vertexpointer(sizeof(decalvert), ptr->pos.v);
+ gle::texcoord0pointer(sizeof(decalvert), ptr->tc.v);
+ gle::colorpointer(sizeof(decalvert), ptr->color.v);
+
+ glDrawArrays(GL_TRIANGLES, 0, count);
+ xtravertsva += count;
+
+ if(flags&(DF_ADD|DF_INVMOD)) resetfogcolor();
+
+ extern int intel_vertexarray_bug;
+ if(intel_vertexarray_bug) glFlush();
+ }
+
+ decalinfo &newdecal()
+ {
+ decalinfo &d = decals[enddecal];
+ int next = enddecal + 1;
+ if(next>=maxdecals) next = 0;
+ if(next==startdecal) freedecal();
+ enddecal = next;
+ dirty = true;
+ return d;
+ }
+
+ ivec bbmin, bbmax;
+ vec decalcenter, decalnormal, decaltangent, decalbitangent;
+ float decalradius, decalu, decalv;
+ bvec4 decalcolor;
+
+ void adddecal(const vec &center, const vec &dir, float radius, const bvec &color, int info)
+ {
+ if(dir.iszero()) return;
+
+ int bbradius = int(ceil(radius));
+ bbmin = ivec(center).sub(bbradius);
+ bbmax = ivec(center).add(bbradius);
+
+ decalcolor = bvec4(color, 255);
+ decalcenter = center;
+ decalradius = radius;
+ decalnormal = dir;
+#if 0
+ decaltangent.orthogonal(dir);
+#else
+ decaltangent = vec(dir.z, -dir.x, dir.y);
+ decaltangent.sub(vec(dir).mul(decaltangent.dot(dir)));
+#endif
+ if(flags&DF_ROTATE) decaltangent.rotate(rnd(360)*RAD, dir);
+ decaltangent.normalize();
+ decalbitangent.cross(decaltangent, dir);
+ if(flags&DF_RND4)
+ {
+ decalu = 0.5f*(info&1);
+ decalv = 0.5f*((info>>1)&1);
+ }
+
+ lastvert = endvert;
+ gentris(worldroot, ivec(0, 0, 0), worldsize>>1);
+ if(dbgdec)
+ {
+ int nverts = endvert < lastvert ? endvert + maxverts - lastvert : endvert - lastvert;
+ conoutf(CON_DEBUG, "tris = %d, verts = %d, total tris = %d", nverts/3, nverts, (maxverts - 3 - availverts)/3);
+ }
+ if(endvert==lastvert) return;
+
+ decalinfo &d = newdecal();
+ d.color = color;
+ d.millis = lastmillis;
+ d.startvert = lastvert;
+ d.endvert = endvert;
+ }
+
+ static int clip(const vec *in, int numin, const vec &dir, float below, float above, vec *out)
+ {
+ int numout = 0;
+ const vec *p = &in[numin-1];
+ float pc = dir.dot(*p);
+ loopi(numin)
+ {
+ const vec &v = in[i];
+ float c = dir.dot(v);
+ if(c < below)
+ {
+ if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ }
+ else if(c > above)
+ {
+ if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ }
+ else
+ {
+ if(pc < below)
+ {
+ if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+ }
+ else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+ out[numout++] = v;
+ }
+ p = &v;
+ pc = c;
+ }
+ return numout;
+ }
+
+ void gentris(cube &cu, int orient, const ivec &o, int size, materialsurface *mat = NULL, int vismask = 0)
+ {
+ vec pos[MAXFACEVERTS+4];
+ int numverts = 0, numplanes = 1;
+ vec planes[2];
+ if(mat)
+ {
+ planes[0] = vec(0, 0, 0);
+ switch(orient)
+ {
+ #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+ case orient: \
+ planes[0][dimension(orient)] = dimcoord(orient) ? 1 : -1; \
+ v0 v1 v2 v3 \
+ break;
+ #define GENFACEVERT(orient, vert, x,y,z, xv,yv,zv) \
+ pos[numverts++] = vec(x xv, y yv, z zv);
+ GENFACEVERTS(o.x, o.x, o.y, o.y, o.z, o.z, , + mat->csize, , + mat->rsize, + 0.1f, - 0.1f);
+ #undef GENFACEORIENT
+ #undef GENFACEVERT
+ }
+ }
+ else if(cu.texture[orient] == DEFAULT_SKY) return;
+ else if(cu.ext && (numverts = cu.ext->surfaces[orient].numverts&MAXFACEVERTS))
+ {
+ vertinfo *verts = cu.ext->verts() + cu.ext->surfaces[orient].verts;
+ ivec vo = ivec(o).mask(~0xFFF).shl(3);
+ loopj(numverts) pos[j] = vec(verts[j].getxyz().add(vo)).mul(1/8.0f);
+ planes[0].cross(pos[0], pos[1], pos[2]).normalize();
+ if(numverts >= 4 && !(cu.merged&(1<<orient)) && !flataxisface(cu, orient) && faceconvexity(verts, numverts, size))
+ {
+ planes[1].cross(pos[0], pos[2], pos[3]).normalize();
+ numplanes++;
+ }
+ }
+ else if(cu.merged&(1<<orient)) return;
+ else if(!vismask || (vismask&0x40 && visibleface(cu, orient, o, size, MAT_AIR, (cu.material&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA)))
+ {
+ ivec v[4];
+ genfaceverts(cu, orient, v);
+ int vis = 3, convex = faceconvexity(v, vis), order = convex < 0 ? 1 : 0;
+ vec vo(o);
+ pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+ if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+ pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+ if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+ planes[0].cross(pos[0], pos[1], pos[2]).normalize();
+ if(convex) { planes[1].cross(pos[0], pos[2], pos[3]).normalize(); numplanes++; }
+ }
+ else return;
+
+ loopl(numplanes)
+ {
+ const vec &n = planes[l];
+ float facing = n.dot(decalnormal);
+ if(facing <= 0) continue;
+ vec p = vec(pos[0]).sub(decalcenter);
+#if 0
+ // intersect ray along decal normal with plane
+ float dist = n.dot(p) / facing;
+ if(fabs(dist) > decalradius) continue;
+ vec pcenter = vec(decalnormal).mul(dist).add(decalcenter);
+#else
+ // travel back along plane normal from the decal center
+ float dist = n.dot(p);
+ if(fabs(dist) > decalradius) continue;
+ vec pcenter = vec(n).mul(dist).add(decalcenter);
+#endif
+ vec ft, fb;
+ ft.orthogonal(n);
+ ft.normalize();
+ fb.cross(ft, n);
+ vec pt = vec(ft).mul(ft.dot(decaltangent)).add(vec(fb).mul(fb.dot(decaltangent))).normalize(),
+ pb = vec(ft).mul(ft.dot(decalbitangent)).add(vec(fb).mul(fb.dot(decalbitangent))).normalize();
+ // orthonormalize projected bitangent to prevent streaking
+ pb.sub(vec(pt).mul(pt.dot(pb))).normalize();
+ vec v1[MAXFACEVERTS+4], v2[MAXFACEVERTS+4];
+ float ptc = pt.dot(pcenter), pbc = pb.dot(pcenter);
+ int numv;
+ if(numplanes >= 2)
+ {
+ if(l) { pos[1] = pos[2]; pos[2] = pos[3]; }
+ numv = clip(pos, 3, pt, ptc - decalradius, ptc + decalradius, v1);
+ if(numv<3) continue;
+ }
+ else
+ {
+ numv = clip(pos, numverts, pt, ptc - decalradius, ptc + decalradius, v1);
+ if(numv<3) continue;
+ }
+ numv = clip(v1, numv, pb, pbc - decalradius, pbc + decalradius, v2);
+ if(numv<3) continue;
+ float tsz = flags&DF_RND4 ? 0.5f : 1.0f, scale = tsz*0.5f/decalradius,
+ tu = decalu + tsz*0.5f - ptc*scale, tv = decalv + tsz*0.5f - pbc*scale;
+ pt.mul(scale); pb.mul(scale);
+ decalvert dv1 = { v2[0], decalcolor, vec2(pt.dot(v2[0]) + tu, pb.dot(v2[0]) + tv) },
+ dv2 = { v2[1], decalcolor, vec2(pt.dot(v2[1]) + tu, pb.dot(v2[1]) + tv) };
+ int totalverts = 3*(numv-2);
+ if(totalverts > maxverts-3) return;
+ while(availverts < totalverts)
+ {
+ if(!freedecal()) return;
+ }
+ availverts -= totalverts;
+ loopk(numv-2)
+ {
+ verts[endvert++] = dv1;
+ verts[endvert++] = dv2;
+ dv2.pos = v2[k+2];
+ dv2.tc = vec2(pt.dot(v2[k+2]) + tu, pb.dot(v2[k+2]) + tv);
+ verts[endvert++] = dv2;
+ if(endvert>=maxverts) endvert = 0;
+ }
+ }
+ }
+
+ void findmaterials(vtxarray *va)
+ {
+ materialsurface *matbuf = va->matbuf;
+ int matsurfs = va->matsurfs;
+ loopi(matsurfs)
+ {
+ materialsurface &m = matbuf[i];
+ if(!isclipped(m.material&MATF_VOLUME)) { i += m.skip; continue; }
+ int dim = dimension(m.orient), dc = dimcoord(m.orient);
+ if(dc ? decalnormal[dim] <= 0 : decalnormal[dim] >= 0) { i += m.skip; continue; }
+ int c = C[dim], r = R[dim];
+ for(;;)
+ {
+ materialsurface &m = matbuf[i];
+ if(m.o[dim] >= bbmin[dim] && m.o[dim] <= bbmax[dim] &&
+ m.o[c] + m.csize >= bbmin[c] && m.o[c] <= bbmax[c] &&
+ m.o[r] + m.rsize >= bbmin[r] && m.o[r] <= bbmax[r])
+ {
+ static cube dummy;
+ gentris(dummy, m.orient, m.o, max(m.csize, m.rsize), &m);
+ }
+ if(i+1 >= matsurfs) break;
+ materialsurface &n = matbuf[i+1];
+ if(n.material != m.material || n.orient != m.orient) break;
+ i++;
+ }
+ }
+ }
+
+ void findescaped(cube *cu, const ivec &o, int size, int escaped)
+ {
+ loopi(8)
+ {
+ if(escaped&(1<<i))
+ {
+ ivec co(i, o, size);
+ if(cu[i].children) findescaped(cu[i].children, co, size>>1, cu[i].escaped);
+ else
+ {
+ int vismask = cu[i].merged;
+ if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+ }
+ }
+ }
+ }
+
+ void gentris(cube *cu, const ivec &o, int size, int escaped = 0)
+ {
+ int overlap = octaboxoverlap(o, size, bbmin, bbmax);
+ loopi(8)
+ {
+ if(overlap&(1<<i))
+ {
+ ivec co(i, o, size);
+ if(cu[i].ext && cu[i].ext->va && cu[i].ext->va->matsurfs)
+ findmaterials(cu[i].ext->va);
+ if(cu[i].children) gentris(cu[i].children, co, size>>1, cu[i].escaped);
+ else
+ {
+ int vismask = cu[i].visible;
+ if(vismask&0xC0)
+ {
+ if(vismask&0x80) loopj(6) gentris(cu[i], j, co, size, NULL, vismask);
+ else loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+ }
+ }
+ }
+ else if(escaped&(1<<i))
+ {
+ ivec co(i, o, size);
+ if(cu[i].children) findescaped(cu[i].children, co, size>>1, cu[i].escaped);
+ else
+ {
+ int vismask = cu[i].merged;
+ if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+ }
+ }
+ }
+ }
+};
+
+decalrenderer decals[] =
+{
+ decalrenderer("<grey>packages/particles/scorch.png", DF_ROTATE, 500),
+ decalrenderer("<grey>packages/particles/blood.png", DF_RND4|DF_ROTATE|DF_INVMOD),
+ decalrenderer("<grey>packages/particles/bullet.png", DF_OVERBRIGHT)
+};
+
+void initdecals()
+{
+ loopi(sizeof(decals)/sizeof(decals[0])) decals[i].init(maxdecaltris);
+}
+
+void cleardecals()
+{
+ loopi(sizeof(decals)/sizeof(decals[0])) decals[i].cleardecals();
+}
+
+void cleanupdecals()
+{
+ loopi(sizeof(decals)/sizeof(decals[0])) decals[i].cleanup();
+}
+
+VARNP(decals, showdecals, 0, 1, 1);
+
+void renderdecals(bool mainpass)
+{
+ bool rendered = false;
+ loopi(sizeof(decals)/sizeof(decals[0]))
+ {
+ decalrenderer &d = decals[i];
+ if(mainpass)
+ {
+ d.clearfadeddecals();
+ d.fadeindecals();
+ d.fadeoutdecals();
+ }
+ if(!showdecals || !d.hasdecals()) continue;
+ if(!rendered)
+ {
+ rendered = true;
+ decalrenderer::setuprenderstate();
+ }
+ d.render();
+ }
+ if(!rendered) return;
+ decalrenderer::cleanuprenderstate();
+}
+
+VARP(maxdecaldistance, 1, 512, 10000);
+
+void adddecal(int type, const vec &center, const vec &surface, float radius, const bvec &color, int info)
+{
+ if(!showdecals || type<0 || (size_t)type>=sizeof(decals)/sizeof(decals[0]) || center.dist(camera1->o) - radius > maxdecaldistance) return;
+ decalrenderer &d = decals[type];
+ d.adddecal(center, surface, radius, color, info);
+}
+
diff --git a/src/engine/depthfx.h b/src/engine/depthfx.h
new file mode 100644
index 0000000..a9c0fdf
--- /dev/null
+++ b/src/engine/depthfx.h
@@ -0,0 +1,193 @@
+// eye space depth texture for soft particles, done at low res then blurred to prevent ugly jaggies
+VARP(depthfxfpscale, 1, 1<<12, 1<<16);
+VARP(depthfxscale, 1, 1<<6, 1<<8);
+VARP(depthfxblend, 1, 16, 64);
+VARP(depthfxpartblend, 1, 8, 64);
+VAR(depthfxmargin, 0, 16, 64);
+VAR(depthfxbias, 0, 1, 64);
+
+extern void cleanupdepthfx();
+VARFP(fpdepthfx, 0, 0, 1, cleanupdepthfx());
+VARP(depthfxemuprecision, 0, 1, 1);
+VARFP(depthfxsize, 6, 7, 12, cleanupdepthfx());
+VARP(depthfx, 0, 1, 1);
+VARP(depthfxparts, 0, 1, 1);
+VARP(blurdepthfx, 0, 1, 7);
+VARP(blurdepthfxsigma, 1, 50, 200);
+VAR(depthfxscissor, 0, 2, 2);
+VAR(debugdepthfx, 0, 0, 1);
+
+#define MAXDFXRANGES 4
+
+void *depthfxowners[MAXDFXRANGES];
+float depthfxranges[MAXDFXRANGES];
+int numdepthfxranges = 0;
+vec depthfxmin(1e16f, 1e16f, 1e16f), depthfxmax(1e16f, 1e16f, 1e16f);
+
+static struct depthfxtexture : rendertarget
+{
+ const GLenum *colorformats() const
+ {
+ static const GLenum colorfmts[] = { GL_RG16F, GL_RGB16F, GL_RGBA, GL_RGBA8, GL_RGB, GL_RGB8, GL_FALSE };
+ return &colorfmts[fpdepthfx && hasTF ? (hasTRG ? 0 : 1) : 2];
+ }
+
+ float eyedepth(const vec &p) const
+ {
+ return max(-cammatrix.transform<vec>(p).z, 0.0f);
+ }
+
+ void addscissorvert(const vec &v, float &sx1, float &sy1, float &sx2, float &sy2)
+ {
+ vec p = camprojmatrix.perspectivetransform(v);
+ sx1 = min(sx1, p.x);
+ sy1 = min(sy1, p.y);
+ sx2 = max(sx2, p.x);
+ sy2 = max(sy2, p.y);
+ }
+
+ bool addscissorbox(const vec &center, float size)
+ {
+ float sx1, sy1, sx2, sy2;
+ calcspherescissor(center, size, sx1, sy1, sx2, sy2);
+ return addblurtiles(sx1, sy1, sx2, sy2);
+ }
+
+ bool addscissorbox(const vec &bbmin, const vec &bbmax)
+ {
+ float sx1 = 1, sy1 = 1, sx2 = -1, sy2 = -1;
+ loopi(8)
+ {
+ vec v(i&1 ? bbmax.x : bbmin.x, i&2 ? bbmax.y : bbmin.y, i&4 ? bbmax.z : bbmin.z);
+ addscissorvert(v, sx1, sy1, sx2, sy2);
+ }
+ return addblurtiles(sx1, sy1, sx2, sy2);
+ }
+
+ bool screenrect() const { return true; }
+ bool filter() const { return blurdepthfx!=0; }
+ bool highprecision() const { return colorfmt==GL_RG16F || colorfmt==GL_RGB16F; }
+ bool emulatehighprecision() const { return depthfxemuprecision && !blurdepthfx; }
+
+ bool shouldrender()
+ {
+ extern void finddepthfxranges();
+ finddepthfxranges();
+ return (numdepthfxranges && scissorx1 < scissorx2 && scissory1 < scissory2) || debugdepthfx;
+ }
+
+ bool dorender()
+ {
+ glClearColor(1, 1, 1, 1);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ depthfxing = true;
+ refracting = -1;
+
+ extern void renderdepthobstacles(const vec &bbmin, const vec &bbmax, float scale, float *ranges, int numranges);
+ float scale = depthfxscale;
+ float *ranges = depthfxranges;
+ int numranges = numdepthfxranges;
+ if(highprecision())
+ {
+ scale = depthfxfpscale;
+ ranges = NULL;
+ numranges = 0;
+ }
+ else if(emulatehighprecision())
+ {
+ scale = depthfxfpscale;
+ ranges = NULL;
+ numranges = -3;
+ }
+ renderdepthobstacles(depthfxmin, depthfxmax, scale, ranges, numranges);
+
+ refracting = 0;
+ depthfxing = false;
+
+ return numdepthfxranges > 0;
+ }
+
+ void dodebug(int w, int h)
+ {
+ if(numdepthfxranges > 0)
+ {
+ gle::colorf(0, 1, 0);
+ debugscissor(w, h, true);
+ gle::colorf(0, 0, 1);
+ debugblurtiles(w, h, true);
+ gle::colorf(1, 1, 1);
+ }
+ }
+} depthfxtex;
+
+void cleanupdepthfx()
+{
+ depthfxtex.cleanup(true);
+}
+
+void viewdepthfxtex()
+{
+ if(!depthfx) return;
+ depthfxtex.debug();
+}
+
+bool depthfxing = false;
+
+bool binddepthfxtex()
+{
+ if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0)
+ {
+ glActiveTexture_(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, depthfxtex.rendertex);
+ glActiveTexture_(GL_TEXTURE0);
+ return true;
+ }
+ return false;
+}
+
+void binddepthfxparams(float blend, float minblend = 0, bool allow = true, void *owner = NULL)
+{
+ if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0)
+ {
+ float scale = 0, offset = -1, texscale = 0;
+ if(!depthfxtex.highprecision())
+ {
+ float select[4] = { 0, 0, 0, 0 };
+ if(!depthfxtex.emulatehighprecision())
+ {
+ loopi(numdepthfxranges) if(depthfxowners[i]==owner)
+ {
+ select[i] = float(depthfxscale)/blend;
+ scale = 1.0f/blend;
+ offset = -float(depthfxranges[i] - depthfxbias)/blend;
+ break;
+ }
+ }
+ else if(allow)
+ {
+ select[0] = float(depthfxfpscale)/blend;
+ select[1] = select[0]/256;
+ select[2] = select[1]/256;
+ scale = 1.0f/blend;
+ offset = 0;
+ }
+ LOCALPARAMF(depthfxselect, select[0], select[1], select[2], select[3]);
+ }
+ else if(allow)
+ {
+ scale = 1.0f/blend;
+ offset = 0;
+ texscale = float(depthfxfpscale)/blend;
+ }
+ LOCALPARAMF(depthfxparams, scale, offset, texscale, minblend);
+ }
+}
+
+void drawdepthfxtex()
+{
+ if(!depthfx) return;
+
+ depthfxtex.render(1<<depthfxsize, 1<<depthfxsize, blurdepthfx, blurdepthfxsigma/100.0f);
+}
+
diff --git a/src/engine/dynlight.cpp b/src/engine/dynlight.cpp
new file mode 100644
index 0000000..835c631
--- /dev/null
+++ b/src/engine/dynlight.cpp
@@ -0,0 +1,227 @@
+#include "engine.h"
+
+VARP(maxdynlights, 0, min(3, MAXDYNLIGHTS), MAXDYNLIGHTS);
+VARP(dynlightdist, 0, 1024, 10000);
+
+struct dynlight
+{
+ vec o, hud;
+ float radius, initradius, curradius, dist;
+ vec color, initcolor, curcolor;
+ int fade, peak, expire, flags;
+ physent *owner;
+
+ void calcradius()
+ {
+ if(fade + peak > 0)
+ {
+ int remaining = expire - lastmillis;
+ if(flags&DL_EXPAND)
+ curradius = initradius + (radius - initradius) * (1.0f - remaining/float(fade + peak));
+ else if(!(flags&DL_FLASH) && remaining > fade)
+ curradius = initradius + (radius - initradius) * (1.0f - float(remaining - fade)/peak);
+ else if(flags&DL_SHRINK)
+ curradius = (radius*remaining)/fade;
+ else curradius = radius;
+ }
+ else curradius = radius;
+ }
+
+ void calccolor()
+ {
+ if(flags&DL_FLASH || peak <= 0) curcolor = color;
+ else
+ {
+ int peaking = expire - lastmillis - fade;
+ if(peaking <= 0) curcolor = color;
+ else curcolor.lerp(initcolor, color, 1.0f - float(peaking)/peak);
+ }
+
+ float intensity = 1.0f;
+ if(fade > 0)
+ {
+ int fading = expire - lastmillis;
+ if(fading < fade) intensity = float(fading)/fade;
+ }
+ curcolor.mul(intensity);
+ // KLUGE: this prevents nvidia drivers from trying to recompile dynlight fragment programs
+ loopk(3) if(fmod(curcolor[k], 1.0f/256) < 0.001f) curcolor[k] += 0.001f;
+ }
+};
+
+vector<dynlight> dynlights;
+vector<dynlight *> closedynlights;
+
+void adddynlight(const vec &o, float radius, const vec &color, int fade, int peak, int flags, float initradius, const vec &initcolor, physent *owner)
+{
+ if(!maxdynlights) return;
+ if(o.dist(camera1->o) > dynlightdist || radius <= 0) return;
+
+ int insert = 0, expire = fade + peak + lastmillis;
+ loopvrev(dynlights) if(expire>=dynlights[i].expire) { insert = i+1; break; }
+ dynlight d;
+ d.o = d.hud = o;
+ d.radius = radius;
+ d.initradius = initradius;
+ d.color = color;
+ d.initcolor = initcolor;
+ d.fade = fade;
+ d.peak = peak;
+ d.expire = expire;
+ d.flags = flags;
+ d.owner = owner;
+ dynlights.insert(insert, d);
+}
+
+void cleardynlights()
+{
+ int faded = -1;
+ loopv(dynlights) if(lastmillis<dynlights[i].expire) { faded = i; break; }
+ if(faded<0) dynlights.setsize(0);
+ else if(faded>0) dynlights.remove(0, faded);
+}
+
+void removetrackeddynlights(physent *owner)
+{
+ loopvrev(dynlights) if(owner ? dynlights[i].owner == owner : dynlights[i].owner != NULL) dynlights.remove(i);
+}
+
+void updatedynlights()
+{
+ cleardynlights();
+ game::adddynlights();
+
+ loopv(dynlights)
+ {
+ dynlight &d = dynlights[i];
+ if(d.owner) game::dynlighttrack(d.owner, d.o, d.hud);
+ d.calcradius();
+ d.calccolor();
+ }
+}
+
+int finddynlights()
+{
+ closedynlights.setsize(0);
+ if(!maxdynlights) return 0;
+ physent e;
+ e.type = ENT_CAMERA;
+ loopvj(dynlights)
+ {
+ dynlight &d = dynlights[j];
+ if(d.curradius <= 0) continue;
+ d.dist = camera1->o.dist(d.o) - d.curradius;
+ if(d.dist > dynlightdist || isfoggedsphere(d.curradius, d.o) || pvsoccludedsphere(d.o, d.curradius))
+ continue;
+ if(reflecting || refracting > 0)
+ {
+ if(d.o.z + d.curradius < reflectz) continue;
+ }
+ else if(refracting < 0 && d.o.z - d.curradius > reflectz) continue;
+ e.o = d.o;
+ e.radius = e.xradius = e.yradius = e.eyeheight = e.aboveeye = d.curradius;
+ if(!collide(&e, vec(0, 0, 0), 0, false)) continue;
+
+ int insert = 0;
+ loopvrev(closedynlights) if(d.dist >= closedynlights[i]->dist) { insert = i+1; break; }
+ closedynlights.insert(insert, &d);
+ if(closedynlights.length() >= DYNLIGHTMASK) break;
+ }
+ return closedynlights.length();
+}
+
+bool getdynlight(int n, vec &o, float &radius, vec &color)
+{
+ if(!closedynlights.inrange(n)) return false;
+ dynlight &d = *closedynlights[n];
+ o = d.o;
+ radius = d.curradius;
+ color = d.curcolor;
+ return true;
+}
+
+void dynlightreaching(const vec &target, vec &color, vec &dir, bool hud)
+{
+ vec dyncolor(0, 0, 0);//, dyndir(0, 0, 0);
+ loopv(dynlights)
+ {
+ dynlight &d = dynlights[i];
+ if(d.curradius<=0) continue;
+
+ vec ray(hud ? d.hud : d.o);
+ ray.sub(target);
+ float mag = ray.squaredlen();
+ if(mag >= d.curradius*d.curradius) continue;
+
+ vec color = d.curcolor;
+ color.mul(1 - sqrtf(mag)/d.curradius);
+ dyncolor.add(color);
+ //dyndir.add(ray.mul(intensity/mag));
+ }
+#if 0
+ if(!dyndir.iszero())
+ {
+ dyndir.normalize();
+ float x = dyncolor.magnitude(), y = color.magnitude();
+ if(x+y>0)
+ {
+ dir.mul(x);
+ dyndir.mul(y);
+ dir.add(dyndir).div(x+y);
+ if(dir.iszero()) dir = vec(0, 0, 1);
+ else dir.normalize();
+ }
+ }
+#endif
+ color.add(dyncolor);
+}
+
+void calcdynlightmask(vtxarray *va)
+{
+ uint mask = 0;
+ int offset = 0;
+ loopv(closedynlights)
+ {
+ dynlight &d = *closedynlights[i];
+ if(d.o.dist_to_bb(va->geommin, va->geommax) >= d.curradius) continue;
+
+ mask |= (i+1)<<offset;
+ offset += DYNLIGHTBITS;
+ if(offset >= maxdynlights*DYNLIGHTBITS) break;
+ }
+ va->dynlightmask = mask;
+}
+
+int setdynlights(vtxarray *va)
+{
+ if(closedynlights.empty() || !va->dynlightmask) return 0;
+
+ extern bool minimizedynlighttcusage();
+
+ static vec4 posv[MAXDYNLIGHTS];
+ static vec colorv[MAXDYNLIGHTS];
+
+ int index = 0;
+ for(uint mask = va->dynlightmask; mask; mask >>= DYNLIGHTBITS, index++)
+ {
+ dynlight &d = *closedynlights[(mask&DYNLIGHTMASK)-1];
+
+ float scale = 1.0f/d.curradius;
+ vec origin = vec(d.o).mul(-scale);
+
+ if(index>0 && minimizedynlighttcusage())
+ {
+ scale /= posv[0].w;
+ origin.sub(vec(posv[0]).mul(scale));
+ }
+
+ posv[index] = vec4(origin, scale);
+ colorv[index] = d.curcolor;
+ }
+
+ GLOBALPARAMV(dynlightpos, posv, index);
+ GLOBALPARAMV(dynlightcolor, colorv, index);
+
+ return index;
+}
+
diff --git a/src/engine/engine.h b/src/engine/engine.h
new file mode 100644
index 0000000..9f41f45
--- /dev/null
+++ b/src/engine/engine.h
@@ -0,0 +1,613 @@
+#ifndef __ENGINE_H__
+#define __ENGINE_H__
+
+#include "cube.h"
+#include "world.h"
+
+#ifndef STANDALONE
+
+#include "octa.h"
+#include "lightmap.h"
+#include "bih.h"
+#include "texture.h"
+#include "model.h"
+
+extern dynent *player;
+extern physent *camera1; // special ent that acts as camera, same object as player1 in FPS mode
+
+extern int worldscale, worldsize;
+extern int mapversion;
+extern char *maptitle;
+extern vector<ushort> texmru;
+extern int xtraverts, xtravertsva;
+extern const ivec cubecoords[8];
+extern const ivec facecoords[6][4];
+extern const uchar fv[6][4];
+extern const uchar fvmasks[64];
+extern const uchar faceedgesidx[6][4];
+extern bool inbetweenframes, renderedframe;
+
+extern SDL_Window *screen;
+extern int screenw, screenh;
+extern int zpass;
+
+extern vector<int> entgroup;
+
+// rendertext
+struct font
+{
+ struct charinfo
+ {
+ short x, y, w, h, offsetx, offsety, advance, tex;
+ };
+
+ char *name;
+ vector<Texture *> texs;
+ vector<charinfo> chars;
+ int charoffset, defaultw, defaulth, scale;
+
+ font() : name(NULL) {}
+ ~font() { DELETEA(name); }
+};
+
+#define FONTH (curfont->scale)
+#define FONTW (FONTH/2)
+#define MINRESW 640
+#define MINRESH 480
+
+extern font *curfont;
+extern const matrix4x3 *textmatrix;
+
+extern void reloadfonts();
+
+// texture
+extern int hwtexsize, hwcubetexsize, hwmaxaniso, maxtexsize;
+
+extern Texture *textureload(const char *name, int clamp = 0, bool mipit = true, bool msg = true);
+extern int texalign(const void *data, int w, int bpp);
+extern void cleanuptexture(Texture *t);
+extern uchar *loadalphamask(Texture *t);
+extern void loadlayermasks();
+extern Texture *cubemapload(const char *name, bool mipit = true, bool msg = true, bool transient = false);
+extern void drawcubemap(int size, const vec &o, float yaw, float pitch, const cubemapside &side, bool onlysky = false);
+extern void loadshaders();
+extern void setuptexparameters(int tnum, void *pixels, int clamp, int filter, GLenum format = GL_RGB, GLenum target = GL_TEXTURE_2D, bool swizzle = false);
+extern void createtexture(int tnum, int w, int h, void *pixels, int clamp, int filter, GLenum component = GL_RGB, GLenum target = GL_TEXTURE_2D, int pw = 0, int ph = 0, int pitch = 0, bool resize = true, GLenum format = GL_FALSE, bool swizzle = false);
+extern void blurtexture(int n, int bpp, int w, int h, uchar *dst, const uchar *src, int margin = 0);
+extern void blurnormals(int n, int w, int h, bvec *dst, const bvec *src, int margin = 0);
+extern void renderpostfx();
+extern void initenvmaps();
+extern void genenvmaps();
+extern ushort closestenvmap(const vec &o);
+extern ushort closestenvmap(int orient, const ivec &co, int size);
+extern GLuint lookupenvmap(ushort emid);
+extern GLuint lookupenvmap(Slot &slot);
+extern bool reloadtexture(Texture &tex);
+extern bool reloadtexture(const char *name);
+extern void setuptexcompress();
+extern void clearslots();
+extern void compacteditvslots();
+extern void compactmruvslots();
+extern void compactvslots(cube *c, int n = 8);
+extern void compactvslot(int &index);
+extern void compactvslot(VSlot &vs);
+extern int compactvslots();
+extern void reloadtextures();
+extern void cleanuptextures();
+
+// shadowmap
+
+extern int shadowmap, shadowmapcasters;
+extern bool shadowmapping;
+extern matrix4 shadowmatrix;
+
+extern bool isshadowmapcaster(const vec &o, float rad);
+extern bool addshadowmapcaster(const vec &o, float xyrad, float zrad);
+extern bool isshadowmapreceiver(vtxarray *va);
+extern void rendershadowmap();
+extern void pushshadowmap();
+extern void popshadowmap();
+extern void rendershadowmapreceivers();
+extern void guessshadowdir();
+
+// pvs
+extern void clearpvs();
+extern bool pvsoccluded(const ivec &bbmin, const ivec &bbmax);
+extern bool pvsoccludedsphere(const vec &center, float radius);
+extern bool waterpvsoccluded(int height);
+extern void setviewcell(const vec &p);
+extern void savepvs(stream *f);
+extern void loadpvs(stream *f, int numpvs);
+extern int getnumviewcells();
+
+static inline bool pvsoccluded(const ivec &bborigin, int size)
+{
+ return pvsoccluded(bborigin, ivec(bborigin).add(size));
+}
+
+// rendergl
+extern bool hasVAO, hasFBO, hasAFBO, hasDS, hasTF, hasTRG, hasTSW, hasS3TC, hasFXT1, hasLATC, hasRGTC, hasAF, hasFBB, hasUBO, hasMBR;
+extern int glversion, glslversion, glcompat;
+
+enum { DRAWTEX_NONE = 0, DRAWTEX_ENVMAP, DRAWTEX_MINIMAP, DRAWTEX_MODELPREVIEW };
+
+extern float curfov, fovy, aspect, forceaspect;
+extern int drawtex;
+extern bool renderedgame;
+extern const matrix4 viewmatrix;
+extern matrix4 cammatrix, projmatrix, camprojmatrix, invcammatrix, invcamprojmatrix;
+extern bvec fogcolor;
+extern vec curfogcolor;
+extern int fog;
+extern float curfogstart, curfogend;
+
+extern void gl_checkextensions();
+extern void gl_init();
+extern void gl_resize();
+extern void cleanupgl();
+extern void rendergame(bool mainpass = false);
+extern void invalidatepostfx();
+extern void gl_drawhud();
+extern void gl_drawframe();
+extern void gl_drawmainmenu();
+extern void drawminimap();
+extern void drawtextures();
+extern void enablepolygonoffset(GLenum type);
+extern void disablepolygonoffset(GLenum type);
+extern void calcspherescissor(const vec &center, float size, float &sx1, float &sy1, float &sx2, float &sy2);
+extern int pushscissor(float sx1, float sy1, float sx2, float sy2);
+extern void popscissor();
+extern void recomputecamera();
+extern void screenquad();
+extern void screenquad(float sw, float sh);
+extern void screenquadflipped(float sw, float sh);
+extern void screenquad(float sw, float sh, float sw2, float sh2);
+extern void screenquadoffset(float x, float y, float w, float h);
+extern void screenquadoffset(float x, float y, float w, float h, float x2, float y2, float w2, float h2);
+extern void hudquad(float x, float y, float w, float h, float tx = 0, float ty = 0, float tw = 1, float th = 1);
+extern void setfogcolor(const vec &v);
+extern void zerofogcolor();
+extern void resetfogcolor();
+extern void setfogdist(float start, float end);
+extern void clearfogdist();
+extern void resetfogdist();
+extern void writecrosshairs(stream *f);
+
+namespace modelpreview
+{
+ extern void start(int x, int y, int w, int h, bool background = true);
+ extern void end();
+}
+
+// renderextras
+extern void render3dbox(vec &o, float tofloor, float toceil, float xradius, float yradius = 0);
+
+// octa
+extern cube *newcubes(uint face = F_EMPTY, int mat = MAT_AIR);
+extern cubeext *growcubeext(cubeext *ext, int maxverts);
+extern void setcubeext(cube &c, cubeext *ext);
+extern cubeext *newcubeext(cube &c, int maxverts = 0, bool init = true);
+extern void getcubevector(cube &c, int d, int x, int y, int z, ivec &p);
+extern void setcubevector(cube &c, int d, int x, int y, int z, const ivec &p);
+extern int familysize(const cube &c);
+extern void freeocta(cube *c);
+extern void discardchildren(cube &c, bool fixtex = false, int depth = 0);
+extern void optiface(uchar *p, cube &c);
+extern void validatec(cube *c, int size = 0);
+extern bool isvalidcube(const cube &c);
+extern ivec lu;
+extern int lusize;
+extern cube &lookupcube(const ivec &to, int tsize = 0, ivec &ro = lu, int &rsize = lusize);
+extern const cube *neighbourstack[32];
+extern int neighbourdepth;
+extern const cube &neighbourcube(const cube &c, int orient, const ivec &co, int size, ivec &ro = lu, int &rsize = lusize);
+extern void resetclipplanes();
+extern int getmippedtexture(const cube &p, int orient);
+extern void forcemip(cube &c, bool fixtex = true);
+extern bool subdividecube(cube &c, bool fullcheck=true, bool brighten=true);
+extern void edgespan2vectorcube(cube &c);
+extern int faceconvexity(const ivec v[4]);
+extern int faceconvexity(const ivec v[4], int &vis);
+extern int faceconvexity(const vertinfo *verts, int numverts, int size);
+extern int faceconvexity(const cube &c, int orient);
+extern void calcvert(const cube &c, const ivec &co, int size, ivec &vert, int i, bool solid = false);
+extern void calcvert(const cube &c, const ivec &co, int size, vec &vert, int i, bool solid = false);
+extern uint faceedges(const cube &c, int orient);
+extern bool collapsedface(const cube &c, int orient);
+extern bool touchingface(const cube &c, int orient);
+extern bool flataxisface(const cube &c, int orient);
+extern bool collideface(const cube &c, int orient);
+extern int genclipplane(const cube &c, int i, vec *v, plane *clip);
+extern void genclipplanes(const cube &c, const ivec &co, int size, clipplanes &p, bool collide = true);
+extern bool visibleface(const cube &c, int orient, const ivec &co, int size, ushort mat = MAT_AIR, ushort nmat = MAT_AIR, ushort matmask = MATF_VOLUME);
+extern int classifyface(const cube &c, int orient, const ivec &co, int size);
+extern int visibletris(const cube &c, int orient, const ivec &co, int size, ushort nmat = MAT_ALPHA, ushort matmask = MAT_ALPHA);
+extern int visibleorient(const cube &c, int orient);
+extern void genfaceverts(const cube &c, int orient, ivec v[4]);
+extern int calcmergedsize(int orient, const ivec &co, int size, const vertinfo *verts, int numverts);
+extern void invalidatemerges(cube &c, const ivec &co, int size, bool msg);
+extern void calcmerges();
+
+extern int mergefaces(int orient, facebounds *m, int sz);
+extern void mincubeface(const cube &cu, int orient, const ivec &o, int size, const facebounds &orig, facebounds &cf, ushort nmat = MAT_AIR, ushort matmask = MATF_VOLUME);
+
+static inline cubeext &ext(cube &c)
+{
+ return *(c.ext ? c.ext : newcubeext(c));
+}
+
+// ents
+extern char *entname(entity &e);
+extern bool haveselent();
+extern undoblock *copyundoents(undoblock *u);
+extern void pasteundoent(int idx, const entity &ue);
+extern void pasteundoents(undoblock *u);
+
+// octaedit
+extern void cancelsel();
+extern void rendertexturepanel(int w, int h);
+extern void addundo(undoblock *u);
+extern void commitchanges(bool force = false);
+extern void rendereditcursor();
+extern void tryedit();
+
+extern bool prefabloaded(const char *name);
+extern void renderprefab(const char *name, const vec &o, float yaw, float pitch, float roll, float size = 1, const vec &color = vec(1, 1, 1));
+extern void previewprefab(const char *name, const vec &color);
+
+// octarender
+extern vector<tjoint> tjoints;
+extern vector<vtxarray *> varoot, valist;
+
+extern ushort encodenormal(const vec &n);
+extern vec decodenormal(ushort norm);
+extern void guessnormals(const vec *pos, int numverts, vec *normals);
+extern void reduceslope(ivec &n);
+extern void findtjoints();
+extern void octarender();
+extern void allchanged(bool load = false);
+extern void clearvas(cube *c);
+extern void destroyva(vtxarray *va, bool reparent = true);
+extern bool readva(vtxarray *va, ushort *&edata, vertex *&vdata);
+extern void updatevabb(vtxarray *va, bool force = false);
+extern void updatevabbs(bool force = false);
+
+// renderva
+extern vtxarray *visibleva, *reflectedva;
+
+extern void visiblecubes(bool cull = true);
+extern void setvfcP(float z = -1, const vec &bbmin = vec(-1, -1, -1), const vec &bbmax = vec(1, 1, 1));
+extern void savevfcP();
+extern void restorevfcP();
+extern void rendergeom(float causticspass = 0, bool fogpass = false);
+extern void renderalphageom(bool fogpass = false);
+extern void rendermapmodels();
+extern void renderreflectedgeom(bool causticspass = false, bool fogpass = false);
+extern void renderreflectedmapmodels();
+extern void renderoutline();
+extern bool rendersky(bool explicitonly = false);
+
+extern bool isfoggedsphere(float rad, const vec &cv);
+extern int isvisiblesphere(float rad, const vec &cv);
+extern bool bboccluded(const ivec &bo, const ivec &br);
+extern occludequery *newquery(void *owner);
+extern void startquery(occludequery *query);
+extern void endquery(occludequery *query);
+extern bool checkquery(occludequery *query, bool nowait = false);
+extern void resetqueries();
+extern int getnumqueries();
+extern void startbb(bool mask = true);
+extern void endbb(bool mask = true);
+extern void drawbb(const ivec &bo, const ivec &br);
+
+extern int oqfrags;
+
+// dynlight
+
+extern void updatedynlights();
+extern int finddynlights();
+extern void calcdynlightmask(vtxarray *va);
+extern int setdynlights(vtxarray *va);
+extern bool getdynlight(int n, vec &o, float &radius, vec &color);
+
+// material
+
+extern int showmat;
+
+extern int findmaterial(const char *name);
+extern const char *findmaterialname(int mat);
+extern const char *getmaterialdesc(int mat, const char *prefix = "");
+extern void genmatsurfs(const cube &c, const ivec &co, int size, vector<materialsurface> &matsurfs);
+extern void rendermatsurfs(materialsurface *matbuf, int matsurfs);
+extern void rendermatgrid(materialsurface *matbuf, int matsurfs);
+extern int optimizematsurfs(materialsurface *matbuf, int matsurfs);
+extern void setupmaterials(int start = 0, int len = 0);
+extern void rendermaterials();
+extern int visiblematerial(const cube &c, int orient, const ivec &co, int size, ushort matmask = MATF_VOLUME);
+
+// water
+extern int refracting, refractfog;
+extern bool reflecting, fading, fogging;
+extern float reflectz;
+extern int reflectdist, vertwater, waterrefract, waterreflect, waterfade, caustics, waterfallrefract;
+
+#define GETMATIDXVAR(name, var, type) \
+ type get##name##var(int mat) \
+ { \
+ switch(mat&MATF_INDEX) \
+ { \
+ default: case 0: return name##var; \
+ case 1: return name##2##var; \
+ case 2: return name##3##var; \
+ case 3: return name##4##var; \
+ } \
+ }
+
+extern const bvec &getwatercolor(int mat);
+extern const bvec &getwaterfallcolor(int mat);
+extern int getwaterfog(int mat);
+extern const bvec &getlavacolor(int mat);
+extern int getlavafog(int mat);
+extern const bvec &getglasscolor(int mat);
+
+extern void cleanreflections();
+extern void queryreflections();
+extern void drawreflections();
+extern void renderwater();
+extern void setuplava(Texture *tex, float scale);
+extern void renderlava(const materialsurface &m);
+extern void flushlava();
+extern void loadcaustics(bool force = false);
+extern void preloadwatershaders(bool force = false);
+
+// glare
+extern bool glaring;
+
+extern void drawglaretex();
+extern void addglare();
+
+// depthfx
+extern bool depthfxing;
+
+extern void drawdepthfxtex();
+
+// server
+extern vector<const char *> gameargs;
+
+extern void initserver(bool listen, bool dedicated);
+extern void cleanupserver();
+extern void serverslice(bool dedicated, uint timeout);
+extern void updatetime();
+
+extern ENetSocket connectmaster(bool wait);
+extern void localclienttoserver(int chan, ENetPacket *);
+extern void localconnect();
+extern bool serveroption(char *opt);
+
+// serverbrowser
+extern bool resolverwait(const char *name, ENetAddress *address);
+extern int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &address);
+extern void addserver(const char *name, int port = 0, const char *password = NULL, bool keep = false);
+extern void writeservercfg();
+
+// client
+extern void localdisconnect(bool cleanup = true);
+extern void localservertoclient(int chan, ENetPacket *packet);
+extern void connectserv(const char *servername, int port, const char *serverpassword);
+extern void abortconnect();
+extern void clientkeepalive();
+
+// command
+extern hashnameset<ident> idents;
+extern int identflags;
+
+extern void clearoverrides();
+extern void writecfg(const char *name = NULL);
+
+extern void checksleep(int millis);
+extern void clearsleep(bool clearoverrides = true);
+
+// console
+extern void processtextinput(const char *str, int len);
+extern void processkey(int code, bool isdown, int modstate = 0);
+extern int rendercommand(int x, int y, int w);
+extern int renderconsole(int w, int h, int abovehud);
+extern void conoutf(const char *s, ...) PRINTFARGS(1, 2);
+extern void conoutf(int type, const char *s, ...) PRINTFARGS(2, 3);
+extern void resetcomplete();
+extern void complete(char *s, int maxlen, const char *cmdprefix);
+const char *getkeyname(int code);
+extern const char *addreleaseaction(char *s);
+extern void writebinds(stream *f);
+extern void writecompletions(stream *f);
+
+// main
+enum
+{
+ NOT_INITING = 0,
+ INIT_GAME,
+ INIT_LOAD,
+ INIT_RESET
+};
+extern int initing, numcpus;
+
+enum
+{
+ CHANGE_GFX = 1<<0,
+ CHANGE_SOUND = 1<<1
+};
+extern bool initwarning(const char *desc, int level = INIT_RESET, int type = CHANGE_GFX);
+
+extern bool grabinput, minimized;
+
+extern bool interceptkey(int sym);
+
+extern float loadprogress;
+extern void renderbackground(const char *caption = NULL, Texture *mapshot = NULL, const char *mapname = NULL, const char *mapinfo = NULL, bool restore = false, bool force = false);
+extern void renderprogress(float bar, const char *text, GLuint tex = 0, bool background = false);
+
+extern void getfps(int &fps, int &bestdiff, int &worstdiff);
+extern void swapbuffers(bool overlay = true);
+extern int getclockmillis();
+
+enum { KR_CONSOLE = 1<<0, KR_GUI = 1<<1, KR_EDITMODE = 1<<2 };
+
+extern void keyrepeat(bool on, int mask = ~0);
+
+enum { TI_CONSOLE = 1<<0, TI_GUI = 1<<1 };
+
+extern void textinput(bool on, int mask = ~0);
+
+// menu
+extern void menuprocess();
+extern void addchange(const char *desc, int type);
+extern void clearchanges(int type);
+
+// physics
+extern void mousemove(int dx, int dy);
+extern bool overlapsdynent(const vec &o, float radius);
+extern void rotatebb(vec &center, vec &radius, int yaw);
+extern float shadowray(const vec &o, const vec &ray, float radius, int mode, extentity *t = NULL);
+struct ShadowRayCache;
+extern ShadowRayCache *newshadowraycache();
+extern void freeshadowraycache(ShadowRayCache *&cache);
+extern void resetshadowraycache(ShadowRayCache *cache);
+extern float shadowray(ShadowRayCache *cache, const vec &o, const vec &ray, float radius, int mode, extentity *t = NULL);
+
+// world
+
+extern vector<int> outsideents;
+
+extern void entcancel();
+extern void entitiesinoctanodes();
+extern void attachentities();
+extern void freeoctaentities(cube &c);
+extern bool pointinsel(const selinfo &sel, const vec &o);
+
+extern void resetmap();
+extern void startmap(const char *name);
+
+// rendermodel
+struct mapmodelinfo { string name; model *m; };
+
+extern bool modelloaded(const char *name);
+extern void findanims(const char *pattern, vector<int> &anims);
+extern void loadskin(const char *dir, const char *altdir, Texture *&skin, Texture *&masks);
+extern mapmodelinfo *getmminfo(int i);
+extern void startmodelquery(occludequery *query);
+extern void endmodelquery();
+extern void preloadmodelshaders(bool force = false);
+extern void preloadusedmapmodels(bool msg = false, bool bih = false);
+
+static inline model *loadmapmodel(int n)
+{
+ extern vector<mapmodelinfo> mapmodels;
+ if(mapmodels.inrange(n))
+ {
+ model *m = mapmodels[n].m;
+ return m ? m : loadmodel(NULL, n);
+ }
+ return NULL;
+}
+
+// renderparticles
+extern void initparticles();
+extern void clearparticles();
+extern void clearparticleemitters();
+extern void seedparticles();
+extern void updateparticles();
+extern void renderparticles(bool mainpass = false);
+extern bool printparticles(extentity &e, char *buf, int len);
+
+// decal
+extern void initdecals();
+extern void cleardecals();
+extern void renderdecals(bool mainpass = false);
+
+// blob
+
+enum
+{
+ BLOB_STATIC = 0,
+ BLOB_DYNAMIC
+};
+
+extern int showblobs;
+
+extern void initblobs(int type = -1);
+extern void resetblobs();
+extern void renderblob(int type, const vec &o, float radius, float fade = 1);
+extern void flushblobs();
+
+// rendersky
+extern int explicitsky;
+extern double skyarea;
+extern char *skybox;
+
+extern void setupsky();
+extern void drawskybox(int farplane, bool limited, bool force = false);
+extern bool limitsky();
+extern bool shouldrenderskyenvmap();
+extern bool shouldclearskyboxglare();
+
+// 3dgui
+extern void g3d_render();
+extern void g3d_render2d();
+extern bool g3d_windowhit(bool on, bool act);
+extern bool g3d_key(int code, bool isdown);
+extern bool g3d_input(const char *str, int len);
+// menus
+extern int mainmenu;
+
+extern void clearmainmenu();
+extern void g3d_mainmenu();
+
+// sound
+extern void clearmapsounds();
+extern void checkmapsounds();
+extern void updatesounds();
+extern void preloadmapsounds();
+
+extern void initmumble();
+extern void closemumble();
+extern void updatemumble();
+
+// grass
+extern void generategrass();
+extern void rendergrass();
+extern void cleanupgrass();
+
+// blendmap
+extern int blendpaintmode;
+
+struct BlendMapCache;
+extern BlendMapCache *newblendmapcache();
+extern void freeblendmapcache(BlendMapCache *&cache);
+extern bool setblendmaporigin(BlendMapCache *cache, const ivec &o, int size);
+extern bool hasblendmap(BlendMapCache *cache);
+extern uchar lookupblendmap(BlendMapCache *cache, const vec &pos);
+extern void resetblendmap();
+extern void enlargeblendmap();
+extern void shrinkblendmap(int octant);
+extern void optimizeblendmap();
+extern void stoppaintblendmap();
+extern void trypaintblendmap();
+extern void renderblendbrush(GLuint tex, float x, float y, float w, float h);
+extern void renderblendbrush();
+extern bool loadblendmap(stream *f, int info);
+extern void saveblendmap(stream *f);
+extern uchar shouldsaveblendmap();
+
+// recorder
+
+namespace recorder
+{
+ extern void stop();
+ extern void capture(bool overlay = true);
+ extern void cleanup();
+}
+
+#endif
+
+#endif
+
diff --git a/src/engine/explosion.h b/src/engine/explosion.h
new file mode 100644
index 0000000..626d5bf
--- /dev/null
+++ b/src/engine/explosion.h
@@ -0,0 +1,249 @@
+namespace sphere
+{
+
+ struct vert
+ {
+ vec pos;
+ ushort s, t;
+ } *verts = NULL;
+ GLushort *indices = NULL;
+ int numverts = 0, numindices = 0;
+ GLuint vbuf = 0, ebuf = 0;
+
+ void init(int slices, int stacks)
+ {
+ numverts = (stacks+1)*(slices+1);
+ verts = new vert[numverts];
+ float ds = 1.0f/slices, dt = 1.0f/stacks, t = 1.0f;
+ loopi(stacks+1)
+ {
+ float rho = M_PI*(1-t), s = 0.0f, sinrho = i && i < stacks ? sin(rho) : 0, cosrho = !i ? 1 : (i < stacks ? cos(rho) : -1);
+ loopj(slices+1)
+ {
+ float theta = j==slices ? 0 : 2*M_PI*s;
+ vert &v = verts[i*(slices+1) + j];
+ v.pos = vec(-sin(theta)*sinrho, cos(theta)*sinrho, cosrho);
+ v.s = ushort(s*0xFFFF);
+ v.t = ushort(t*0xFFFF);
+ s += ds;
+ }
+ t -= dt;
+ }
+
+ numindices = (stacks-1)*slices*3*2;
+ indices = new ushort[numindices];
+ GLushort *curindex = indices;
+ loopi(stacks)
+ {
+ loopk(slices)
+ {
+ int j = i%2 ? slices-k-1 : k;
+ if(i)
+ {
+ *curindex++ = i*(slices+1)+j;
+ *curindex++ = (i+1)*(slices+1)+j;
+ *curindex++ = i*(slices+1)+j+1;
+ }
+ if(i+1 < stacks)
+ {
+ *curindex++ = i*(slices+1)+j+1;
+ *curindex++ = (i+1)*(slices+1)+j;
+ *curindex++ = (i+1)*(slices+1)+j+1;
+ }
+ }
+ }
+
+ if(!vbuf) glGenBuffers_(1, &vbuf);
+ gle::bindvbo(vbuf);
+ glBufferData_(GL_ARRAY_BUFFER, numverts*sizeof(vert), verts, GL_STATIC_DRAW);
+ DELETEA(verts);
+
+ if(!ebuf) glGenBuffers_(1, &ebuf);
+ gle::bindebo(ebuf);
+ glBufferData_(GL_ELEMENT_ARRAY_BUFFER, numindices*sizeof(GLushort), indices, GL_STATIC_DRAW);
+ DELETEA(indices);
+ }
+
+ void enable()
+ {
+ if(!vbuf) init(12, 6);
+
+ gle::bindvbo(vbuf);
+ gle::bindebo(ebuf);
+
+ gle::vertexpointer(sizeof(vert), &verts->pos);
+ gle::texcoord0pointer(sizeof(vert), &verts->s, GL_UNSIGNED_SHORT, 2, GL_TRUE);
+ gle::enablevertex();
+ gle::enabletexcoord0();
+ }
+
+ void draw()
+ {
+ glDrawRangeElements_(GL_TRIANGLES, 0, numverts-1, numindices, GL_UNSIGNED_SHORT, indices);
+ xtraverts += numindices;
+ glde++;
+ }
+
+ void disable()
+ {
+ gle::disablevertex();
+ gle::disabletexcoord0();
+
+ gle::clearvbo();
+ gle::clearebo();
+ }
+
+ void cleanup()
+ {
+ if(vbuf) { glDeleteBuffers_(1, &vbuf); vbuf = 0; }
+ if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
+ }
+}
+
+static const float WOBBLE = 1.25f;
+
+struct fireballrenderer : listrenderer
+{
+ fireballrenderer(const char *texname)
+ : listrenderer(texname, 0, PT_FIREBALL|PT_GLARE|PT_SHADER)
+ {}
+
+ void startrender()
+ {
+ if(glaring) SETSHADER(explosionglare);
+ else if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0)
+ {
+ if(!depthfxtex.highprecision()) SETSHADER(explosionsoft8);
+ else SETSHADER(explosionsoft);
+ }
+ else SETSHADER(explosion);
+
+ sphere::enable();
+ }
+
+ void endrender()
+ {
+ sphere::disable();
+ }
+
+ void cleanup()
+ {
+ sphere::cleanup();
+ }
+
+ int finddepthfxranges(void **owners, float *ranges, int numranges, int maxranges, vec &bbmin, vec &bbmax)
+ {
+ static struct fireballent : physent
+ {
+ fireballent()
+ {
+ type = ENT_CAMERA;
+ }
+ } e;
+
+ for(listparticle *p = list; p; p = p->next)
+ {
+ int ts = p->fade <= 5 ? 1 : lastmillis-p->millis;
+ float pmax = p->val,
+ size = p->fade ? float(ts)/p->fade : 1,
+ psize = (p->size + pmax * size)*WOBBLE;
+ if(2*(p->size + pmax)*WOBBLE < depthfxblend ||
+ (!depthfxtex.highprecision() && !depthfxtex.emulatehighprecision() && psize > depthfxscale - depthfxbias) ||
+ isfoggedsphere(psize, p->o)) continue;
+
+ e.o = p->o;
+ e.radius = e.xradius = e.yradius = e.eyeheight = e.aboveeye = psize;
+ if(!::collide(&e, vec(0, 0, 0), 0, false)) continue;
+
+ if(depthfxscissor==2 && !depthfxtex.addscissorbox(p->o, psize)) continue;
+
+ vec dir = camera1->o;
+ dir.sub(p->o);
+ float dist = dir.magnitude();
+ dir.mul(psize/dist).add(p->o);
+ float depth = depthfxtex.eyedepth(dir);
+
+ loopk(3)
+ {
+ bbmin[k] = min(bbmin[k], p->o[k] - psize);
+ bbmax[k] = max(bbmax[k], p->o[k] + psize);
+ }
+
+ int pos = numranges;
+ loopi(numranges) if(depth < ranges[i]) { pos = i; break; }
+ if(pos >= maxranges) continue;
+
+ if(numranges > pos)
+ {
+ int moved = min(numranges-pos, maxranges-(pos+1));
+ memmove(&ranges[pos+1], &ranges[pos], moved*sizeof(float));
+ memmove(&owners[pos+1], &owners[pos], moved*sizeof(void *));
+ }
+ if(numranges < maxranges) numranges++;
+
+ ranges[pos] = depth;
+ owners[pos] = p;
+ }
+
+ return numranges;
+ }
+
+ void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity)
+ {
+ pe.maxfade = max(pe.maxfade, fade);
+ pe.extendbb(o, (size+1+pe.ent->attr2)*WOBBLE);
+ }
+
+ void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+ {
+ float pmax = p->val,
+ size = p->fade ? float(ts)/p->fade : 1,
+ psize = p->size + pmax * size;
+
+ if(isfoggedsphere(psize*WOBBLE, p->o)) return;
+
+ vec dir = vec(o).sub(camera1->o), s, t;
+ float dist = dir.magnitude();
+ bool inside = dist <= psize*WOBBLE;
+ if(inside)
+ {
+ s = camright;
+ t = camup;
+ }
+ else
+ {
+ if(reflecting) { dir.z = o.z - reflectz; dist = dir.magnitude(); }
+ float mag2 = dir.magnitude2();
+ dir.x /= mag2;
+ dir.y /= mag2;
+ dir.z /= dist;
+ s = vec(dir.y, -dir.x, 0);
+ t = vec(dir.x*dir.z, dir.y*dir.z, -mag2/dist);
+ }
+
+ matrix3 rot(lastmillis/1000.0f*143*RAD, vec(1/SQRT3, 1/SQRT3, 1/SQRT3));
+ LOCALPARAM(texgenS, rot.transposedtransform(s));
+ LOCALPARAM(texgenT, rot.transposedtransform(t));
+
+ matrix4 m(rot, o);
+ m.scale(psize, psize, inside ? -psize : psize);
+ m.mul(camprojmatrix, m);
+ LOCALPARAM(explosionmatrix, m);
+
+ LOCALPARAM(center, o);
+ LOCALPARAMF(millis, lastmillis/1000.0f);
+ LOCALPARAMF(blendparams, inside ? 0.5f : 4, inside ? 0.25f : 0);
+ binddepthfxparams(depthfxblend, inside ? blend/(2*255.0f) : 0, 2*(p->size + pmax)*WOBBLE >= depthfxblend, p);
+
+ int passes = !reflecting && !refracting && inside ? 2 : 1;
+ loopi(passes)
+ {
+ gle::color(p->color, i ? blend/2 : blend);
+ if(i) glDepthFunc(GL_GEQUAL);
+ sphere::draw();
+ if(i) glDepthFunc(GL_LESS);
+ }
+ }
+};
+static fireballrenderer fireballs("packages/particles/explosion.png"), bluefireballs("packages/particles/plasma.png");
+
diff --git a/src/engine/glare.cpp b/src/engine/glare.cpp
new file mode 100644
index 0000000..dc639ad
--- /dev/null
+++ b/src/engine/glare.cpp
@@ -0,0 +1,71 @@
+#include "engine.h"
+#include "rendertarget.h"
+
+static struct glaretexture : rendertarget
+{
+ bool dorender()
+ {
+ extern void drawglare();
+ drawglare();
+ return true;
+ }
+} glaretex;
+
+void cleanupglare()
+{
+ glaretex.cleanup(true);
+}
+
+VARFP(glaresize, 6, 8, 10, cleanupglare());
+VARP(glare, 0, 0, 1);
+VARP(blurglare, 0, 4, 7);
+VARP(blurglareaspect, 0, 1, 1);
+VARP(blurglaresigma, 1, 50, 200);
+
+VAR(debugglare, 0, 0, 1);
+
+void viewglaretex()
+{
+ if(!glare) return;
+ glaretex.debug();
+}
+
+bool glaring = false;
+
+void drawglaretex()
+{
+ if(!glare) return;
+
+ int w = 1<<glaresize, h = 1<<glaresize, blury = blurglare;
+ if(blurglare && blurglareaspect)
+ {
+ while(h > (1<<5) && (screenw*h)/w >= (screenh*4)/3) h /= 2;
+ blury = ((1 + 4*blurglare)*(screenw*h)/w + screenh*2)/(screenh*4);
+ blury = clamp(blury, 1, MAXBLURRADIUS);
+ }
+
+ glaretex.render(w, h, blurglare, blurglaresigma/100.0f, blury);
+}
+
+FVAR(glaremod, 0.5f, 0.75f, 1);
+FVARP(glarescale, 0, 1, 8);
+
+void addglare()
+{
+ if(!glare) return;
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE);
+
+ SETSHADER(screenrect);
+
+ glBindTexture(GL_TEXTURE_2D, glaretex.rendertex);
+
+ float g = glarescale*glaremod;
+ gle::colorf(g, g, g);
+
+ screenquad(1, 1);
+
+ glDisable(GL_BLEND);
+}
+
diff --git a/src/engine/grass.cpp b/src/engine/grass.cpp
new file mode 100644
index 0000000..f91b6dc
--- /dev/null
+++ b/src/engine/grass.cpp
@@ -0,0 +1,356 @@
+#include "engine.h"
+
+VARP(grass, 0, 0, 1);
+VAR(dbggrass, 0, 0, 1);
+VARP(grassdist, 0, 256, 10000);
+FVARP(grasstaper, 0, 0.2, 1);
+FVARP(grassstep, 0.5, 3, 8);
+VARP(grassheight, 1, 4, 64);
+VARP(grassmargin, 0, 8, 32);
+FVAR(grassmarginfade, 0, 0.5f, 1);
+
+#define NUMGRASSWEDGES 8
+
+static struct grasswedge
+{
+ vec dir, across, edge1, edge2;
+ plane bound1, bound2;
+ bvec4 vertbounds;
+
+ grasswedge(int i) :
+ dir(2*M_PI*(i+0.5f)/float(NUMGRASSWEDGES), 0),
+ across(2*M_PI*((i+0.5f)/float(NUMGRASSWEDGES) + 0.25f), 0),
+ edge1(vec(2*M_PI*i/float(NUMGRASSWEDGES), 0).div(cos(M_PI/NUMGRASSWEDGES))),
+ edge2(vec(2*M_PI*(i+1)/float(NUMGRASSWEDGES), 0).div(cos(M_PI/NUMGRASSWEDGES))),
+ bound1(vec(2*M_PI*(i/float(NUMGRASSWEDGES) - 0.25f), 0), 0),
+ bound2(vec(2*M_PI*((i+1)/float(NUMGRASSWEDGES) + 0.25f), 0), 0)
+ {
+ across.div(-across.dot(bound1));
+
+ bvec vertbound1(bound1), vertbound2(bound2);
+ vertbounds = bvec4(vertbound1.x, vertbound1.y, vertbound2.x, vertbound2.y);
+ vertbounds.flip();
+ }
+} grasswedges[NUMGRASSWEDGES] = { 0, 1, 2, 3, 4, 5, 6, 7 };
+
+struct grassvert
+{
+ vec pos;
+ bvec4 color;
+ vec2 tc;
+ svec2 lm;
+ bvec4 bounds;
+};
+
+static vector<grassvert> grassverts;
+static GLuint grassvbo = 0;
+static int grassvbosize = 0;
+
+struct grassgroup
+{
+ const grasstri *tri;
+ float dist;
+ int tex, lmtex, offset, numquads;
+};
+
+static vector<grassgroup> grassgroups;
+
+#define NUMGRASSOFFSETS 32
+
+static float grassoffsets[NUMGRASSOFFSETS] = { -1 }, grassanimoffsets[NUMGRASSOFFSETS];
+static int lastgrassanim = -1;
+
+VARR(grassanimmillis, 0, 3000, 60000);
+FVARR(grassanimscale, 0, 0.03f, 1);
+
+static void animategrass()
+{
+ loopi(NUMGRASSOFFSETS) grassanimoffsets[i] = grassanimscale*sinf(2*M_PI*(grassoffsets[i] + lastmillis/float(grassanimmillis)));
+ lastgrassanim = lastmillis;
+}
+
+VARR(grassscale, 1, 2, 64);
+bvec grasscolor(255, 255, 255);
+HVARFR(grasscolour, 0, 0xFFFFFF, 0xFFFFFF,
+{
+ if(!grasscolour) grasscolour = 0xFFFFFF;
+ grasscolor = bvec((grasscolour>>16)&0xFF, (grasscolour>>8)&0xFF, grasscolour&0xFF);
+});
+FVARR(grassalpha, 0, 1, 1);
+
+static void gengrassquads(grassgroup *&group, const grasswedge &w, const grasstri &g, Texture *tex)
+{
+ float t = camera1->o.dot(w.dir);
+ int tstep = int(ceil(t/grassstep));
+ float tstart = tstep*grassstep,
+ t0 = w.dir.dot(g.v[0]), t1 = w.dir.dot(g.v[1]), t2 = w.dir.dot(g.v[2]), t3 = w.dir.dot(g.v[3]),
+ tmin = min(min(t0, t1), min(t2, t3)),
+ tmax = max(max(t0, t1), max(t2, t3));
+ if(tmax < tstart || tmin > t + grassdist) return;
+
+ int minstep = max(int(ceil(tmin/grassstep)) - tstep, 1),
+ maxstep = int(floor(min(tmax, t + grassdist)/grassstep)) - tstep,
+ numsteps = maxstep - minstep + 1;
+
+ float texscale = (grassscale*tex->ys)/float(grassheight*tex->xs), animscale = grassheight*texscale;
+ vec tc;
+ tc.cross(g.surface, w.dir).mul(texscale);
+
+ int offset = tstep + maxstep;
+ if(offset < 0) offset = NUMGRASSOFFSETS - (-offset)%NUMGRASSOFFSETS;
+ offset += numsteps + NUMGRASSOFFSETS - numsteps%NUMGRASSOFFSETS;
+
+ float leftdist = t0;
+ const vec *leftv = &g.v[0];
+ if(t1 > leftdist) { leftv = &g.v[1]; leftdist = t1; }
+ if(t2 > leftdist) { leftv = &g.v[2]; leftdist = t2; }
+ if(t3 > leftdist) { leftv = &g.v[3]; leftdist = t3; }
+ float rightdist = leftdist;
+ const vec *rightv = leftv;
+
+ vec across(w.across.x, w.across.y, g.surface.zdelta(w.across)), leftdir(0, 0, 0), rightdir(0, 0, 0), leftp = *leftv, rightp = *rightv;
+ float taperdist = grassdist*grasstaper,
+ taperscale = 1.0f / (grassdist - taperdist),
+ dist = maxstep*grassstep + tstart,
+ leftb = 0, rightb = 0, leftdb = 0, rightdb = 0;
+ for(int i = maxstep; i >= minstep; i--, offset--, leftp.add(leftdir), rightp.add(rightdir), leftb += leftdb, rightb += rightdb, dist -= grassstep)
+ {
+ if(dist <= leftdist)
+ {
+ const vec *prev = leftv;
+ float prevdist = leftdist;
+ if(--leftv < g.v) leftv += g.numv;
+ leftdist = leftv->dot(w.dir);
+ if(dist <= leftdist)
+ {
+ prev = leftv;
+ prevdist = leftdist;
+ if(--leftv < g.v) leftv += g.numv;
+ leftdist = leftv->dot(w.dir);
+ }
+ leftdir = vec(*leftv).sub(*prev);
+ leftdir.mul(grassstep/-w.dir.dot(leftdir));
+ leftp = vec(leftdir).mul((prevdist - dist)/grassstep).add(*prev);
+ leftb = w.bound1.dist(leftp);
+ leftdb = w.bound1.dot(leftdir);
+ }
+ if(dist <= rightdist)
+ {
+ const vec *prev = rightv;
+ float prevdist = rightdist;
+ if(++rightv >= &g.v[g.numv]) rightv = g.v;
+ rightdist = rightv->dot(w.dir);
+ if(dist <= rightdist)
+ {
+ prev = rightv;
+ prevdist = rightdist;
+ if(++rightv >= &g.v[g.numv]) rightv = g.v;
+ rightdist = rightv->dot(w.dir);
+ }
+ rightdir = vec(*rightv).sub(*prev);
+ rightdir.mul(grassstep/-w.dir.dot(rightdir));
+ rightp = vec(rightdir).mul((prevdist - dist)/grassstep).add(*prev);
+ rightb = w.bound2.dist(rightp);
+ rightdb = w.bound2.dot(rightdir);
+ }
+ vec p1 = leftp, p2 = rightp;
+ if(leftb > grassmargin)
+ {
+ if(w.bound1.dist(p2) >= grassmargin) continue;
+ p1.add(vec(across).mul(leftb - grassmargin));
+ }
+ if(rightb > grassmargin)
+ {
+ if(w.bound2.dist(p1) >= grassmargin) continue;
+ p2.sub(vec(across).mul(rightb - grassmargin));
+ }
+
+ if(!group)
+ {
+ group = &grassgroups.add();
+ group->tri = &g;
+ group->tex = tex->id;
+ extern bool brightengeom;
+ extern int fullbright;
+ int lmid = brightengeom && (g.lmid < LMID_RESERVED || (fullbright && editmode)) ? LMID_BRIGHT : g.lmid;
+ group->lmtex = lightmaptexs.inrange(lmid) ? lightmaptexs[lmid].id : notexture->id;
+ group->offset = grassverts.length()/4;
+ group->numquads = 0;
+ if(lastgrassanim!=lastmillis) animategrass();
+ }
+
+ group->numquads++;
+
+ float tcoffset = grassoffsets[offset%NUMGRASSOFFSETS],
+ animoffset = animscale*grassanimoffsets[offset%NUMGRASSOFFSETS],
+ tc1 = tc.dot(p1) + tcoffset, tc2 = tc.dot(p2) + tcoffset,
+ fade = dist - t > taperdist ? (grassdist - (dist - t))*taperscale : 1,
+ height = grassheight * fade;
+ svec2 lm1(short(g.tcu.dot(p1)), short(g.tcv.dot(p1))),
+ lm2(short(g.tcu.dot(p2)), short(g.tcv.dot(p2)));
+ bvec4 color(grasscolor, uchar(fade*grassalpha*255));
+
+ #define GRASSVERT(n, tcv, modify) { \
+ grassvert &gv = grassverts.add(); \
+ gv.pos = p##n; \
+ gv.color = color; \
+ gv.tc = vec2(tc##n, tcv); \
+ gv.lm = lm##n; \
+ gv.bounds = w.vertbounds; \
+ modify; \
+ }
+
+ GRASSVERT(2, 0, { gv.pos.z += height; gv.tc.x += animoffset; });
+ GRASSVERT(1, 0, { gv.pos.z += height; gv.tc.x += animoffset; });
+ GRASSVERT(1, 1, );
+ GRASSVERT(2, 1, );
+ }
+}
+
+static void gengrassquads(vtxarray *va)
+{
+ loopv(va->grasstris)
+ {
+ grasstri &g = va->grasstris[i];
+ if(isfoggedsphere(g.radius, g.center)) continue;
+ float dist = g.center.dist(camera1->o);
+ if(dist - g.radius > grassdist) continue;
+
+ Slot &s = *lookupvslot(g.texture, false).slot;
+ if(!s.grasstex)
+ {
+ if(!s.autograss) continue;
+ s.grasstex = textureload(s.autograss, 2);
+ }
+
+ grassgroup *group = NULL;
+ loopi(NUMGRASSWEDGES)
+ {
+ grasswedge &w = grasswedges[i];
+ if(w.bound1.dist(g.center) > g.radius + grassmargin || w.bound2.dist(g.center) > g.radius + grassmargin) continue;
+ gengrassquads(group, w, g, s.grasstex);
+ }
+ if(group) group->dist = dist;
+ }
+}
+
+static inline bool comparegrassgroups(const grassgroup &x, const grassgroup &y)
+{
+ return x.dist > y.dist;
+}
+
+void generategrass()
+{
+ if(!grass || !grassdist) return;
+
+ grassgroups.setsize(0);
+ grassverts.setsize(0);
+
+ if(grassoffsets[0] < 0) loopi(NUMGRASSOFFSETS) grassoffsets[i] = rnd(0x1000000)/float(0x1000000);
+
+ loopi(NUMGRASSWEDGES)
+ {
+ grasswedge &w = grasswedges[i];
+ w.bound1.offset = -camera1->o.dot(w.bound1);
+ w.bound2.offset = -camera1->o.dot(w.bound2);
+ }
+
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(va->grasstris.empty() || va->occluded >= OCCLUDE_GEOM) continue;
+ if(va->distance > grassdist) continue;
+ if(reflecting || refracting>0 ? va->o.z+va->size<reflectz : va->o.z>=reflectz) continue;
+ gengrassquads(va);
+ }
+
+ if(grassgroups.empty()) return;
+
+ grassgroups.sort(comparegrassgroups);
+
+ if(!grassvbo) glGenBuffers_(1, &grassvbo);
+ gle::bindvbo(grassvbo);
+ int size = grassverts.length()*sizeof(grassvert);
+ grassvbosize = max(grassvbosize, size);
+ glBufferData_(GL_ARRAY_BUFFER, grassvbosize, size == grassvbosize ? grassverts.getbuf() : NULL, GL_STREAM_DRAW);
+ if(size != grassvbosize) glBufferSubData_(GL_ARRAY_BUFFER, 0, size, grassverts.getbuf());
+ gle::clearvbo();
+}
+
+void rendergrass()
+{
+ if(!grass || !grassdist || grassgroups.empty() || dbggrass) return;
+
+ glDisable(GL_CULL_FACE);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+ glDepthMask(GL_FALSE);
+
+ GLOBALPARAM(camera, camera1->o);
+
+ SETSHADER(grass);
+
+ LOCALPARAMF(grassmargin, grassmargin, grassmargin ? grassmarginfade / grassmargin : 0.0f, grassmargin ? grassmarginfade : 1.0f);
+
+ gle::bindvbo(grassvbo);
+
+ const grassvert *ptr = 0;
+ gle::vertexpointer(sizeof(grassvert), ptr->pos.v);
+ gle::colorpointer(sizeof(grassvert), ptr->color.v);
+ gle::texcoord0pointer(sizeof(grassvert), ptr->tc.v);
+ gle::texcoord1pointer(sizeof(grassvert), ptr->lm.v, GL_SHORT);
+ gle::tangentpointer(sizeof(grassvert), ptr->bounds.v, GL_BYTE);
+ gle::enablevertex();
+ gle::enablecolor();
+ gle::enabletexcoord0();
+ gle::enabletexcoord1();
+ gle::enabletangent();
+ gle::enablequads();
+
+ int texid = -1, lmtexid = -1;
+ loopv(grassgroups)
+ {
+ grassgroup &g = grassgroups[i];
+
+ if(reflecting || refracting)
+ {
+ if(refracting < 0 ? g.tri->minz > reflectz : g.tri->maxz + grassheight < reflectz) continue;
+ if(isfoggedsphere(g.tri->radius, g.tri->center)) continue;
+ }
+
+ if(texid != g.tex)
+ {
+ glBindTexture(GL_TEXTURE_2D, g.tex);
+ texid = g.tex;
+ }
+ if(lmtexid != g.lmtex)
+ {
+ glActiveTexture_(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, g.lmtex);
+ glActiveTexture_(GL_TEXTURE0);
+ lmtexid = g.lmtex;
+ }
+
+ gle::drawquads(g.offset, g.numquads);
+ xtravertsva += 4*g.numquads;
+ }
+
+ gle::disablequads();
+ gle::disablevertex();
+ gle::disablecolor();
+ gle::disabletexcoord0();
+ gle::disabletexcoord1();
+ gle::disabletangent();
+
+ gle::clearvbo();
+
+ glDisable(GL_BLEND);
+ glDepthMask(GL_TRUE);
+ glEnable(GL_CULL_FACE);
+}
+
+void cleanupgrass()
+{
+ if(grassvbo) { glDeleteBuffers_(1, &grassvbo); grassvbo = 0; }
+ grassvbosize = 0;
+}
+
diff --git a/src/engine/lensflare.h b/src/engine/lensflare.h
new file mode 100644
index 0000000..1618e52
--- /dev/null
+++ b/src/engine/lensflare.h
@@ -0,0 +1,193 @@
+static struct flaretype
+{
+ int type; /* flaretex index, 0..5, -1 for 6+random shine */
+ float loc; /* postion on axis */
+ float scale; /* texture scaling */
+ uchar alpha; /* color alpha */
+} flaretypes[] =
+{
+ {2, 1.30f, 0.04f, 153}, //flares
+ {3, 1.00f, 0.10f, 102},
+ {1, 0.50f, 0.20f, 77},
+ {3, 0.20f, 0.05f, 77},
+ {0, 0.00f, 0.04f, 77},
+ {5, -0.25f, 0.07f, 127},
+ {5, -0.40f, 0.02f, 153},
+ {5, -0.60f, 0.04f, 102},
+ {5, -1.00f, 0.03f, 51},
+ {-1, 1.00f, 0.30f, 255}, //shine - red, green, blue
+ {-2, 1.00f, 0.20f, 255},
+ {-3, 1.00f, 0.25f, 255}
+};
+
+struct flare
+{
+ vec o, center;
+ float size;
+ bvec color;
+ bool sparkle;
+};
+
+VAR(flarelights, 0, 0, 1);
+VARP(flarecutoff, 0, 1000, 10000);
+VARP(flaresize, 20, 100, 500);
+
+struct flarerenderer : partrenderer
+{
+ int maxflares, numflares;
+ unsigned int shinetime;
+ flare *flares;
+
+ flarerenderer(const char *texname, int maxflares)
+ : partrenderer(texname, 3, PT_FLARE|PT_SHADER), maxflares(maxflares), numflares(0), shinetime(0)
+ {
+ flares = new flare[maxflares];
+ }
+ ~flarerenderer()
+ {
+ delete[] flares;
+ }
+
+ void reset()
+ {
+ numflares = 0;
+ }
+
+ void newflare(vec &o, const vec &center, uchar r, uchar g, uchar b, float mod, float size, bool sun, bool sparkle)
+ {
+ if(numflares >= maxflares) return;
+ vec target; //occlusion check (neccessary as depth testing is turned off)
+ if(!raycubelos(o, camera1->o, target)) return;
+ flare &f = flares[numflares++];
+ f.o = o;
+ f.center = center;
+ f.size = size;
+ f.color = bvec(uchar(r*mod), uchar(g*mod), uchar(b*mod));
+ f.sparkle = sparkle;
+ }
+
+ void addflare(vec &o, uchar r, uchar g, uchar b, bool sun, bool sparkle)
+ {
+ //frustrum + fog check
+ if(isvisiblesphere(0.0f, o) > (sun?VFC_FOGGED:VFC_FULL_VISIBLE)) return;
+ //find closest point between camera line of sight and flare pos
+ vec flaredir = vec(o).sub(camera1->o);
+ vec center = vec(camdir).mul(flaredir.dot(camdir)).add(camera1->o);
+ float mod, size;
+ if(sun) //fixed size
+ {
+ mod = 1.0;
+ size = flaredir.magnitude() * flaresize / 100.0f;
+ }
+ else
+ {
+ mod = (flarecutoff-vec(o).sub(center).squaredlen())/flarecutoff;
+ if(mod < 0.0f) return;
+ size = flaresize / 5.0f;
+ }
+ newflare(o, center, r, g, b, mod, size, sun, sparkle);
+ }
+
+ void makelightflares()
+ {
+ numflares = 0; //regenerate flarelist each frame
+ shinetime = lastmillis/10;
+
+ if(editmode || !flarelights) return;
+
+ const vector<extentity *> &ents = entities::getents();
+ extern const vector<int> &checklightcache(int x, int y);
+ const vector<int> &lights = checklightcache(int(camera1->o.x), int(camera1->o.y));
+ loopv(lights)
+ {
+ entity &e = *ents[lights[i]];
+ if(e.type != ET_LIGHT) continue;
+ bool sun = (e.attr1==0);
+ float radius = float(e.attr1);
+ vec flaredir = vec(e.o).sub(camera1->o);
+ float len = flaredir.magnitude();
+ if(!sun && (len > radius)) continue;
+ if(isvisiblesphere(0.0f, e.o) > (sun?VFC_FOGGED:VFC_FULL_VISIBLE)) continue;
+ vec center = vec(camdir).mul(flaredir.dot(camdir)).add(camera1->o);
+ float mod, size;
+ if(sun) //fixed size
+ {
+ mod = 1.0;
+ size = len * flaresize / 100.0f;
+ }
+ else
+ {
+ mod = (radius-len)/radius;
+ size = flaresize / 5.0f;
+ }
+ newflare(e.o, center, e.attr2, e.attr3, e.attr4, mod, size, sun, sun);
+ }
+ }
+
+ int count()
+ {
+ return numflares;
+ }
+
+ bool haswork()
+ {
+ return (numflares != 0) && !glaring && !reflecting && !refracting;
+ }
+
+ void render()
+ {
+ textureshader->set();
+ glDisable(GL_DEPTH_TEST);
+ if(!tex) tex = textureload(texname);
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+ gle::defattrib(gle::ATTRIB_VERTEX, 3, GL_FLOAT);
+ gle::defattrib(gle::ATTRIB_TEXCOORD0, 2, GL_FLOAT);
+ gle::defattrib(gle::ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE);
+ gle::begin(GL_QUADS);
+ loopi(numflares)
+ {
+ const flare &f = flares[i];
+ vec center = f.center;
+ vec axis = vec(f.o).sub(center);
+ bvec4 color(f.color, 255);
+ loopj(f.sparkle?12:9)
+ {
+ const flaretype &ft = flaretypes[j];
+ vec o = vec(axis).mul(ft.loc).add(center);
+ float sz = ft.scale * f.size;
+ int tex = ft.type;
+ if(ft.type < 0) //sparkles - always done last
+ {
+ shinetime = (shinetime + 1) % 10;
+ tex = 6+shinetime;
+ color.r = 0;
+ color.g = 0;
+ color.b = 0;
+ color[-ft.type-1] = f.color[-ft.type-1]; //only want a single channel
+ }
+ color.a = ft.alpha;
+ const float tsz = 0.25; //flares are aranged in 4x4 grid
+ float tx = tsz*(tex&0x03), ty = tsz*((tex>>2)&0x03);
+ gle::attribf(o.x+(-camright.x+camup.x)*sz, o.y+(-camright.y+camup.y)*sz, o.z+(-camright.z+camup.z)*sz);
+ gle::attribf(tx, ty+tsz);
+ gle::attrib(color);
+ gle::attribf(o.x+( camright.x+camup.x)*sz, o.y+( camright.y+camup.y)*sz, o.z+( camright.z+camup.z)*sz);
+ gle::attribf(tx+tsz, ty+tsz);
+ gle::attrib(color);
+ gle::attribf(o.x+( camright.x-camup.x)*sz, o.y+( camright.y-camup.y)*sz, o.z+( camright.z-camup.z)*sz);
+ gle::attribf(tx+tsz, ty);
+ gle::attrib(color);
+ gle::attribf(o.x+(-camright.x-camup.x)*sz, o.y+(-camright.y-camup.y)*sz, o.z+(-camright.z-camup.z)*sz);
+ gle::attribf(tx, ty);
+ gle::attrib(color);
+ }
+ }
+ gle::end();
+ glEnable(GL_DEPTH_TEST);
+ }
+
+ //square per round hole - use addflare(..) instead
+ particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity = 0) { return NULL; }
+};
+static flarerenderer flares("<grey>packages/particles/lensflares.png", 64);
+
diff --git a/src/engine/lightmap.cpp b/src/engine/lightmap.cpp
new file mode 100644
index 0000000..b4090e2
--- /dev/null
+++ b/src/engine/lightmap.cpp
@@ -0,0 +1,2729 @@
+#include "engine.h"
+
+#define MAXLIGHTMAPTASKS 4096
+#define LIGHTMAPBUFSIZE (2*1024*1024)
+
+struct lightmapinfo;
+struct lightmaptask;
+
+struct lightmapworker
+{
+ uchar *buf;
+ int bufstart, bufused;
+ lightmapinfo *firstlightmap, *lastlightmap, *curlightmaps;
+ cube *c;
+ cubeext *ext;
+ uchar *colorbuf;
+ bvec *raybuf;
+ uchar *ambient, *blur;
+ vec *colordata, *raydata;
+ int type, bpp, w, h, orient, rotate;
+ VSlot *vslot;
+ Slot *slot;
+ vector<const extentity *> lights;
+ ShadowRayCache *shadowraycache;
+ BlendMapCache *blendmapcache;
+ bool needspace, doneworking;
+ SDL_cond *spacecond;
+ SDL_Thread *thread;
+
+ lightmapworker();
+ ~lightmapworker();
+
+ void reset();
+ bool setupthread();
+ void cleanupthread();
+
+ static int work(void *data);
+};
+
+struct lightmapinfo
+{
+ lightmapinfo *next;
+ cube *c;
+ uchar *colorbuf;
+ bvec *raybuf;
+ bool packed;
+ int type, w, h, bpp, bufsize, surface, layers;
+};
+
+struct lightmaptask
+{
+ ivec o;
+ int size, usefaces, progress;
+ cube *c;
+ cubeext *ext;
+ lightmapinfo *lightmaps;
+ lightmapworker *worker;
+};
+
+struct lightmapext
+{
+ cube *c;
+ cubeext *ext;
+};
+
+static vector<lightmapworker *> lightmapworkers;
+static vector<lightmaptask> lightmaptasks[2];
+static vector<lightmapext> lightmapexts;
+static int packidx = 0, allocidx = 0;
+static SDL_mutex *lightlock = NULL, *tasklock = NULL;
+static SDL_cond *fullcond = NULL, *emptycond = NULL;
+
+int lightmapping = 0;
+
+vector<LightMap> lightmaps;
+
+VARR(lightprecision, 1, 32, 1024);
+VARR(lighterror, 1, 8, 16);
+VARR(bumperror, 1, 3, 16);
+VARR(lightlod, 0, 0, 10);
+bvec ambientcolor(0x19, 0x19, 0x19), skylightcolor(0, 0, 0);
+HVARFR(ambient, 1, 0x191919, 0xFFFFFF,
+{
+ if(ambient <= 255) ambient |= (ambient<<8) | (ambient<<16);
+ ambientcolor = bvec((ambient>>16)&0xFF, (ambient>>8)&0xFF, ambient&0xFF);
+});
+HVARFR(skylight, 0, 0, 0xFFFFFF,
+{
+ if(skylight <= 255) skylight |= (skylight<<8) | (skylight<<16);
+ skylightcolor = bvec((skylight>>16)&0xFF, (skylight>>8)&0xFF, skylight&0xFF);
+});
+
+extern void setupsunlight();
+bvec sunlightcolor(0, 0, 0);
+HVARFR(sunlight, 0, 0, 0xFFFFFF,
+{
+ if(sunlight <= 255) sunlight |= (sunlight<<8) | (sunlight<<16);
+ sunlightcolor = bvec((sunlight>>16)&0xFF, (sunlight>>8)&0xFF, sunlight&0xFF);
+ setupsunlight();
+});
+FVARFR(sunlightscale, 0, 1, 16, setupsunlight());
+vec sunlightdir(0, 0, 1);
+extern void setsunlightdir();
+VARFR(sunlightyaw, 0, 0, 360, setsunlightdir());
+VARFR(sunlightpitch, -90, 90, 90, setsunlightdir());
+
+void setsunlightdir()
+{
+ sunlightdir = vec(sunlightyaw*RAD, sunlightpitch*RAD);
+ loopk(3) if(fabs(sunlightdir[k]) < 1e-5f) sunlightdir[k] = 0;
+ sunlightdir.normalize();
+ setupsunlight();
+}
+
+entity sunlightent;
+void setupsunlight()
+{
+ memclear(sunlightent);
+ sunlightent.type = ET_LIGHT;
+ sunlightent.attr1 = 0;
+ sunlightent.attr2 = int(sunlightcolor.x*sunlightscale);
+ sunlightent.attr3 = int(sunlightcolor.y*sunlightscale);
+ sunlightent.attr4 = int(sunlightcolor.z*sunlightscale);
+ float dist = min(min(sunlightdir.x ? 1/fabs(sunlightdir.x) : 1e16f, sunlightdir.y ? 1/fabs(sunlightdir.y) : 1e16f), sunlightdir.z ? 1/fabs(sunlightdir.z) : 1e16f);
+ sunlightent.o = vec(sunlightdir).mul(dist*worldsize).add(vec(worldsize/2, worldsize/2, worldsize/2));
+}
+
+VARR(skytexturelight, 0, 1, 1);
+extern int useskytexture;
+
+static const surfaceinfo brightsurfaces[6] =
+{
+ brightsurface,
+ brightsurface,
+ brightsurface,
+ brightsurface,
+ brightsurface,
+ brightsurface
+};
+
+void brightencube(cube &c)
+{
+ if(!c.ext) newcubeext(c, 0, false);
+ memcpy(c.ext->surfaces, brightsurfaces, sizeof(brightsurfaces));
+}
+
+void setsurfaces(cube &c, const surfaceinfo *surfs, const vertinfo *verts, int numverts)
+{
+ if(!c.ext || c.ext->maxverts < numverts) newcubeext(c, numverts, false);
+ memcpy(c.ext->surfaces, surfs, sizeof(c.ext->surfaces));
+ memcpy(c.ext->verts(), verts, numverts*sizeof(vertinfo));
+}
+
+void setsurface(cube &c, int orient, const surfaceinfo &src, const vertinfo *srcverts, int numsrcverts)
+{
+ int dstoffset = 0;
+ if(!c.ext) newcubeext(c, numsrcverts, true);
+ else
+ {
+ int numbefore = 0, beforeoffset = 0;
+ loopi(orient)
+ {
+ surfaceinfo &surf = c.ext->surfaces[i];
+ int numverts = surf.totalverts();
+ if(!numverts) continue;
+ numbefore += numverts;
+ beforeoffset = surf.verts + numverts;
+ }
+ int numafter = 0, afteroffset = c.ext->maxverts;
+ for(int i = 5; i > orient; i--)
+ {
+ surfaceinfo &surf = c.ext->surfaces[i];
+ int numverts = surf.totalverts();
+ if(!numverts) continue;
+ numafter += numverts;
+ afteroffset = surf.verts;
+ }
+ if(afteroffset - beforeoffset >= numsrcverts) dstoffset = beforeoffset;
+ else
+ {
+ cubeext *ext = c.ext;
+ if(numbefore + numsrcverts + numafter > c.ext->maxverts)
+ {
+ ext = growcubeext(c.ext, numbefore + numsrcverts + numafter);
+ memcpy(ext->surfaces, c.ext->surfaces, sizeof(ext->surfaces));
+ }
+ int offset = 0;
+ if(numbefore == beforeoffset)
+ {
+ if(numbefore && c.ext != ext) memcpy(ext->verts(), c.ext->verts(), numbefore*sizeof(vertinfo));
+ offset = numbefore;
+ }
+ else loopi(orient)
+ {
+ surfaceinfo &surf = ext->surfaces[i];
+ int numverts = surf.totalverts();
+ if(!numverts) continue;
+ memmove(ext->verts() + offset, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo));
+ surf.verts = offset;
+ offset += numverts;
+ }
+ dstoffset = offset;
+ offset += numsrcverts;
+ if(numafter && offset > afteroffset)
+ {
+ offset += numafter;
+ for(int i = 5; i > orient; i--)
+ {
+ surfaceinfo &surf = ext->surfaces[i];
+ int numverts = surf.totalverts();
+ if(!numverts) continue;
+ offset -= numverts;
+ memmove(ext->verts() + offset, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo));
+ surf.verts = offset;
+ }
+ }
+ if(c.ext != ext) setcubeext(c, ext);
+ }
+ }
+ surfaceinfo &dst = c.ext->surfaces[orient];
+ dst = src;
+ dst.verts = dstoffset;
+ if(srcverts) memcpy(c.ext->verts() + dstoffset, srcverts, numsrcverts*sizeof(vertinfo));
+}
+
+// quality parameters, set by the calclight arg
+VARN(lmshadows, lmshadows_, 0, 2, 2);
+VARN(lmaa, lmaa_, 0, 3, 3);
+VARN(lerptjoints, lerptjoints_, 0, 1, 1);
+static int lmshadows = 2, lmaa = 3, lerptjoints = 1;
+
+static uint progress = 0, taskprogress = 0;
+static GLuint progresstex = 0;
+static int progresstexticks = 0, progresslightmap = -1;
+
+bool calclight_canceled = false;
+volatile bool check_calclight_progress = false;
+
+void check_calclight_canceled()
+{
+ if(interceptkey(SDLK_ESCAPE))
+ {
+ calclight_canceled = true;
+ loopv(lightmapworkers) lightmapworkers[i]->doneworking = true;
+ }
+ if(!calclight_canceled) check_calclight_progress = false;
+}
+
+void show_calclight_progress()
+{
+ float bar1 = float(progress) / float(allocnodes);
+ defformatstring(text1, "%d%% using %d textures", int(bar1 * 100), lightmaps.length());
+
+ if(LM_PACKW <= hwtexsize && !progresstex)
+ {
+ glGenTextures(1, &progresstex);
+ createtexture(progresstex, LM_PACKW, LM_PACKH, NULL, 3, 1, GL_RGB);
+ }
+
+ // only update once a sec (4 * 250 ms ticks) to not kill performance
+ if(progresstex && !calclight_canceled && progresslightmap >= 0 && !(progresstexticks++ % 4))
+ {
+ if(tasklock) SDL_LockMutex(tasklock);
+ LightMap &lm = lightmaps[progresslightmap];
+ uchar *data = lm.data;
+ int bpp = lm.bpp;
+ if(tasklock) SDL_UnlockMutex(tasklock);
+ glBindTexture(GL_TEXTURE_2D, progresstex);
+ glPixelStorei(GL_UNPACK_ALIGNMENT, texalign(data, LM_PACKW, bpp));
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LM_PACKW, LM_PACKH, bpp > 3 ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, data);
+ }
+ renderprogress(bar1, text1, progresstexticks ? progresstex : 0);
+}
+
+#define CHECK_PROGRESS_LOCKED(exit, before, after) CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, before, after)
+#define CHECK_PROGRESS(exit) CHECK_PROGRESS_LOCKED(exit, , )
+
+bool PackNode::insert(ushort &tx, ushort &ty, ushort tw, ushort th)
+{
+ if((available < tw && available < th) || w < tw || h < th)
+ return false;
+ if(child1)
+ {
+ bool inserted = child1->insert(tx, ty, tw, th) ||
+ child2->insert(tx, ty, tw, th);
+ available = max(child1->available, child2->available);
+ if(!available) clear();
+ return inserted;
+ }
+ if(w == tw && h == th)
+ {
+ available = 0;
+ tx = x;
+ ty = y;
+ return true;
+ }
+
+ if(w - tw > h - th)
+ {
+ child1 = new PackNode(x, y, tw, h);
+ child2 = new PackNode(x + tw, y, w - tw, h);
+ }
+ else
+ {
+ child1 = new PackNode(x, y, w, th);
+ child2 = new PackNode(x, y + th, w, h - th);
+ }
+
+ bool inserted = child1->insert(tx, ty, tw, th);
+ available = max(child1->available, child2->available);
+ return inserted;
+}
+
+bool LightMap::insert(ushort &tx, ushort &ty, uchar *src, ushort tw, ushort th)
+{
+ if((type&LM_TYPE) != LM_BUMPMAP1 && !packroot.insert(tx, ty, tw, th))
+ return false;
+
+ copy(tx, ty, src, tw, th);
+ return true;
+}
+
+void LightMap::copy(ushort tx, ushort ty, uchar *src, ushort tw, ushort th)
+{
+ uchar *dst = data + bpp * tx + ty * bpp * LM_PACKW;
+ loopi(th)
+ {
+ memcpy(dst, src, bpp * tw);
+ dst += bpp * LM_PACKW;
+ src += bpp * tw;
+ }
+ ++lightmaps;
+ lumels += tw * th;
+}
+
+static void insertunlit(int i)
+{
+ LightMap &l = lightmaps[i];
+ if((l.type&LM_TYPE) == LM_BUMPMAP1)
+ {
+ l.unlitx = l.unlity = -1;
+ return;
+ }
+ ushort x, y;
+ uchar unlit[4] = { ambientcolor[0], ambientcolor[1], ambientcolor[2], 255 };
+ if(l.insert(x, y, unlit, 1, 1))
+ {
+ if((l.type&LM_TYPE) == LM_BUMPMAP0)
+ {
+ bvec front(128, 128, 255);
+ ASSERT(lightmaps[i+1].insert(x, y, front.v, 1, 1));
+ }
+ l.unlitx = x;
+ l.unlity = y;
+ }
+}
+
+struct layoutinfo
+{
+ ushort x, y, lmid;
+ uchar w, h;
+};
+
+static void insertlightmap(lightmapinfo &li, layoutinfo &si)
+{
+ loopv(lightmaps)
+ {
+ if(lightmaps[i].type == li.type && lightmaps[i].insert(si.x, si.y, li.colorbuf, si.w, si.h))
+ {
+ si.lmid = i + LMID_RESERVED;
+ if((li.type&LM_TYPE) == LM_BUMPMAP0) ASSERT(lightmaps[i+1].insert(si.x, si.y, (uchar *)li.raybuf, si.w, si.h));
+ return;
+ }
+ }
+
+ progresslightmap = lightmaps.length();
+
+ si.lmid = lightmaps.length() + LMID_RESERVED;
+ LightMap &l = lightmaps.add();
+ l.type = li.type;
+ l.bpp = li.bpp;
+ l.data = new uchar[li.bpp*LM_PACKW*LM_PACKH];
+ memset(l.data, 0, li.bpp*LM_PACKW*LM_PACKH);
+ ASSERT(l.insert(si.x, si.y, li.colorbuf, si.w, si.h));
+ if((li.type&LM_TYPE) == LM_BUMPMAP0)
+ {
+ LightMap &r = lightmaps.add();
+ r.type = LM_BUMPMAP1 | (li.type&~LM_TYPE);
+ r.bpp = 3;
+ r.data = new uchar[3*LM_PACKW*LM_PACKH];
+ memset(r.data, 0, 3*LM_PACKW*LM_PACKH);
+ ASSERT(r.insert(si.x, si.y, (uchar *)li.raybuf, si.w, si.h));
+ }
+}
+
+static void copylightmap(lightmapinfo &li, layoutinfo &si)
+{
+ lightmaps[si.lmid-LMID_RESERVED].copy(si.x, si.y, li.colorbuf, si.w, si.h);
+ if((li.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(si.lmid+1-LMID_RESERVED))
+ lightmaps[si.lmid+1-LMID_RESERVED].copy(si.x, si.y, (uchar *)li.raybuf, si.w, si.h);
+}
+
+static inline bool htcmp(const lightmapinfo &k, const layoutinfo &v)
+{
+ int kw = k.w, kh = k.h;
+ if(kw != v.w || kh != v.h) return false;
+ LightMap &vlm = lightmaps[v.lmid - LMID_RESERVED];
+ int ktype = k.type;
+ if(ktype != vlm.type) return false;
+ int kbpp = k.bpp;
+ const uchar *kcolor = k.colorbuf, *vcolor = vlm.data + kbpp*(v.x + v.y*LM_PACKW);
+ loopi(kh)
+ {
+ if(memcmp(kcolor, vcolor, kbpp*kw)) return false;
+ kcolor += kbpp*kw;
+ vcolor += kbpp*LM_PACKW;
+ }
+ if((ktype&LM_TYPE) != LM_BUMPMAP0) return true;
+ const bvec *kdir = k.raybuf, *vdir = (const bvec *)lightmaps[v.lmid+1 - LMID_RESERVED].data + (v.x + v.y*LM_PACKW);
+ loopi(kh)
+ {
+ if(memcmp(kdir, vdir, kw*sizeof(bvec))) return false;
+ kdir += kw;
+ vdir += LM_PACKW;
+ }
+ return true;
+}
+
+static inline uint hthash(const lightmapinfo &k)
+{
+ int kw = k.w, kh = k.h, kbpp = k.bpp;
+ uint hash = kw + (kh<<8);
+ const uchar *color = k.colorbuf;
+ loopi(kw*kh)
+ {
+ hash ^= color[0] + (color[1] << 4) + (color[2] << 8);
+ color += kbpp;
+ }
+ return hash;
+}
+
+static hashset<layoutinfo> compressed;
+
+VAR(lightcompress, 0, 3, 6);
+
+static bool packlightmap(lightmapinfo &l, layoutinfo &surface)
+{
+ surface.w = l.w;
+ surface.h = l.h;
+ if((int)l.w <= lightcompress && (int)l.h <= lightcompress)
+ {
+ layoutinfo *val = compressed.access(l);
+ if(!val)
+ {
+ insertlightmap(l, surface);
+ compressed[l] = surface;
+ }
+ else
+ {
+ surface.x = val->x;
+ surface.y = val->y;
+ surface.lmid = val->lmid;
+ return false;
+ }
+ }
+ else insertlightmap(l, surface);
+ return true;
+}
+
+static void updatelightmap(const layoutinfo &surface)
+{
+ if(max(LM_PACKW, LM_PACKH) > hwtexsize || !lightmaps.inrange(surface.lmid-LMID_RESERVED)) return;
+
+ LightMap &lm = lightmaps[surface.lmid-LMID_RESERVED];
+ if(lm.tex < 0)
+ {
+ lm.offsetx = lm.offsety = 0;
+ lm.tex = lightmaptexs.length();
+ LightMapTexture &tex = lightmaptexs.add();
+ tex.type = lm.type;
+ tex.w = LM_PACKW;
+ tex.h = LM_PACKH;
+ tex.unlitx = lm.unlitx;
+ tex.unlity = lm.unlity;
+ glGenTextures(1, &tex.id);
+ createtexture(tex.id, tex.w, tex.h, NULL, 3, 1, tex.type&LM_ALPHA ? GL_RGBA : GL_RGB);
+ if((lm.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(surface.lmid+1-LMID_RESERVED))
+ {
+ LightMap &lm2 = lightmaps[surface.lmid+1-LMID_RESERVED];
+ lm2.offsetx = lm2.offsety = 0;
+ lm2.tex = lightmaptexs.length();
+ LightMapTexture &tex2 = lightmaptexs.add();
+ tex2.type = (lm.type&~LM_TYPE) | LM_BUMPMAP0;
+ tex2.w = LM_PACKW;
+ tex2.h = LM_PACKH;
+ tex2.unlitx = lm2.unlitx;
+ tex2.unlity = lm2.unlity;
+ glGenTextures(1, &tex2.id);
+ createtexture(tex2.id, tex2.w, tex2.h, NULL, 3, 1, GL_RGB);
+ }
+ }
+
+ glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, LM_PACKW);
+
+ glBindTexture(GL_TEXTURE_2D, lightmaptexs[lm.tex].id);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, lm.offsetx + surface.x, lm.offsety + surface.y, surface.w, surface.h, lm.type&LM_ALPHA ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, &lm.data[(surface.y*LM_PACKW + surface.x)*lm.bpp]);
+ if((lm.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(surface.lmid+1-LMID_RESERVED))
+ {
+ LightMap &lm2 = lightmaps[surface.lmid+1-LMID_RESERVED];
+ glBindTexture(GL_TEXTURE_2D, lightmaptexs[lm2.tex].id);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, lm2.offsetx + surface.x, lm2.offsety + surface.y, surface.w, surface.h, GL_RGB, GL_UNSIGNED_BYTE, &lm2.data[(surface.y*LM_PACKW + surface.x)*3]);
+ }
+
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+}
+
+
+static uint generatelumel(lightmapworker *w, const float tolerance, uint lightmask, const vector<const extentity *> &lights, const vec &target, const vec &normal, vec &sample, int x, int y)
+{
+ vec avgray(0, 0, 0);
+ float r = 0, g = 0, b = 0;
+ uint lightused = 0;
+ loopv(lights)
+ {
+ if(lightmask&(1<<i)) continue;
+ const extentity &light = *lights[i];
+ vec ray = target;
+ ray.sub(light.o);
+ float mag = ray.magnitude();
+ if(!mag) continue;
+ float attenuation = 1;
+ if(light.attr1)
+ {
+ attenuation -= mag / float(light.attr1);
+ if(attenuation <= 0) continue;
+ }
+ ray.mul(1.0f / mag);
+ float angle = -ray.dot(normal);
+ if(angle <= 0) continue;
+ if(light.attached && light.attached->type==ET_SPOTLIGHT)
+ {
+ vec spot = vec(light.attached->o).sub(light.o).normalize();
+ float maxatten = sincos360[clamp(int(light.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten);
+ if(spotatten <= 0) continue;
+ attenuation *= spotatten;
+ }
+ if(lmshadows && mag)
+ {
+ float dist = shadowray(w->shadowraycache, light.o, ray, mag - tolerance, RAY_SHADOW | (lmshadows > 1 ? RAY_ALPHAPOLY : 0));
+ if(dist < mag - tolerance) continue;
+ }
+ lightused |= 1<<i;
+ float intensity;
+ switch(w->type&LM_TYPE)
+ {
+ case LM_BUMPMAP0:
+ intensity = attenuation;
+ avgray.add(ray.mul(-attenuation));
+ break;
+ default:
+ intensity = angle * attenuation;
+ break;
+ }
+ r += intensity * float(light.attr2);
+ g += intensity * float(light.attr3);
+ b += intensity * float(light.attr4);
+ }
+ if(sunlight)
+ {
+ float angle = sunlightdir.dot(normal);
+ if(angle > 0 &&
+ (!lmshadows ||
+ shadowray(w->shadowraycache, vec(sunlightdir).mul(tolerance).add(target), sunlightdir, 1e16f, RAY_SHADOW | (lmshadows > 1 ? RAY_ALPHAPOLY : 0) | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0)) > 1e15f))
+ {
+ float intensity;
+ switch(w->type&LM_TYPE)
+ {
+ case LM_BUMPMAP0:
+ intensity = 1;
+ avgray.add(sunlightdir);
+ break;
+ default:
+ intensity = angle;
+ break;
+ }
+ r += intensity * (sunlightcolor.x*sunlightscale);
+ g += intensity * (sunlightcolor.y*sunlightscale);
+ b += intensity * (sunlightcolor.z*sunlightscale);
+ }
+ }
+ switch(w->type&LM_TYPE)
+ {
+ case LM_BUMPMAP0:
+ if(avgray.iszero()) break;
+ // transform to tangent space
+ extern const vec orientation_tangent[8][3];
+ extern const vec orientation_bitangent[8][3];
+ vec S(orientation_tangent[w->rotate][dimension(w->orient)]),
+ T(orientation_bitangent[w->rotate][dimension(w->orient)]);
+ normal.orthonormalize(S, T);
+ avgray.normalize();
+ w->raydata[y*w->w+x].add(vec(S.dot(avgray)/S.magnitude(), T.dot(avgray)/T.magnitude(), normal.dot(avgray)));
+ break;
+ }
+ sample.x = min(255.0f, max(r, float(ambientcolor[0])));
+ sample.y = min(255.0f, max(g, float(ambientcolor[1])));
+ sample.z = min(255.0f, max(b, float(ambientcolor[2])));
+ return lightused;
+}
+
+static bool lumelsample(const vec &sample, int aasample, int stride)
+{
+ if(sample.x >= int(ambientcolor[0])+1 || sample.y >= int(ambientcolor[1])+1 || sample.z >= int(ambientcolor[2])+1) return true;
+#define NCHECK(n) \
+ if((n).x >= int(ambientcolor[0])+1 || (n).y >= int(ambientcolor[1])+1 || (n).z >= int(ambientcolor[2])+1) \
+ return true;
+ const vec *n = &sample - stride - aasample;
+ NCHECK(n[0]); NCHECK(n[aasample]); NCHECK(n[2*aasample]);
+ n += stride;
+ NCHECK(n[0]); NCHECK(n[2*aasample]);
+ n += stride;
+ NCHECK(n[0]); NCHECK(n[aasample]); NCHECK(n[2*aasample]);
+ return false;
+}
+
+static void calcskylight(lightmapworker *w, const vec &o, const vec &normal, float tolerance, uchar *skylight, int flags = RAY_ALPHAPOLY, extentity *t = NULL)
+{
+ static const vec rays[17] =
+ {
+ vec(cosf(21*RAD)*cosf(50*RAD), sinf(21*RAD)*cosf(50*RAD), sinf(50*RAD)),
+ vec(cosf(111*RAD)*cosf(50*RAD), sinf(111*RAD)*cosf(50*RAD), sinf(50*RAD)),
+ vec(cosf(201*RAD)*cosf(50*RAD), sinf(201*RAD)*cosf(50*RAD), sinf(50*RAD)),
+ vec(cosf(291*RAD)*cosf(50*RAD), sinf(291*RAD)*cosf(50*RAD), sinf(50*RAD)),
+
+ vec(cosf(66*RAD)*cosf(70*RAD), sinf(66*RAD)*cosf(70*RAD), sinf(70*RAD)),
+ vec(cosf(156*RAD)*cosf(70*RAD), sinf(156*RAD)*cosf(70*RAD), sinf(70*RAD)),
+ vec(cosf(246*RAD)*cosf(70*RAD), sinf(246*RAD)*cosf(70*RAD), sinf(70*RAD)),
+ vec(cosf(336*RAD)*cosf(70*RAD), sinf(336*RAD)*cosf(70*RAD), sinf(70*RAD)),
+
+ vec(0, 0, 1),
+
+ vec(cosf(43*RAD)*cosf(60*RAD), sinf(43*RAD)*cosf(60*RAD), sinf(60*RAD)),
+ vec(cosf(133*RAD)*cosf(60*RAD), sinf(133*RAD)*cosf(60*RAD), sinf(60*RAD)),
+ vec(cosf(223*RAD)*cosf(60*RAD), sinf(223*RAD)*cosf(60*RAD), sinf(60*RAD)),
+ vec(cosf(313*RAD)*cosf(60*RAD), sinf(313*RAD)*cosf(60*RAD), sinf(60*RAD)),
+
+ vec(cosf(88*RAD)*cosf(80*RAD), sinf(88*RAD)*cosf(80*RAD), sinf(80*RAD)),
+ vec(cosf(178*RAD)*cosf(80*RAD), sinf(178*RAD)*cosf(80*RAD), sinf(80*RAD)),
+ vec(cosf(268*RAD)*cosf(80*RAD), sinf(268*RAD)*cosf(80*RAD), sinf(80*RAD)),
+ vec(cosf(358*RAD)*cosf(80*RAD), sinf(358*RAD)*cosf(80*RAD), sinf(80*RAD)),
+
+ };
+ flags |= RAY_SHADOW;
+ if(skytexturelight) flags |= RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0);
+ int hit = 0;
+ if(w) loopi(17)
+ {
+ if(normal.dot(rays[i])>=0 && shadowray(w->shadowraycache, vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++;
+ }
+ else loopi(17)
+ {
+ if(normal.dot(rays[i])>=0 && shadowray(vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++;
+ }
+
+ loopk(3) skylight[k] = uchar(ambientcolor[k] + (max(skylightcolor[k], ambientcolor[k]) - ambientcolor[k])*hit/17.0f);
+}
+
+static inline bool hasskylight()
+{
+ return skylightcolor[0]>ambientcolor[0] || skylightcolor[1]>ambientcolor[1] || skylightcolor[2]>ambientcolor[2];
+}
+
+VARR(blurlms, 0, 0, 2);
+VARR(blurskylight, 0, 0, 2);
+
+static inline void generatealpha(lightmapworker *w, float tolerance, const vec &pos, uchar &alpha)
+{
+ alpha = lookupblendmap(w->blendmapcache, pos);
+ if(w->slot->layermask)
+ {
+ static const int sdim[] = { 1, 0, 0 }, tdim[] = { 2, 2, 1 };
+ int dim = dimension(w->orient);
+ float k = 8.0f/w->vslot->scale,
+ s = (pos[sdim[dim]] * k - w->vslot->offset.y) / w->slot->layermaskscale,
+ t = (pos[tdim[dim]] * (dim <= 1 ? -k : k) - w->vslot->offset.y) / w->slot->layermaskscale;
+ const texrotation &r = texrotations[w->rotate];
+ if(r.swapxy) swap(s, t);
+ if(r.flipx) s = -s;
+ if(r.flipy) t = -t;
+ const ImageData &mask = *w->slot->layermask;
+ int mx = int(floor(s))%mask.w, my = int(floor(t))%mask.h;
+ if(mx < 0) mx += mask.w;
+ if(my < 0) my += mask.h;
+ uchar maskval = mask.data[mask.bpp*(mx + 1) - 1 + mask.pitch*my];
+ switch(w->slot->layermaskmode)
+ {
+ case 2: alpha = min(alpha, maskval); break;
+ case 3: alpha = max(alpha, maskval); break;
+ case 4: alpha = min(alpha, uchar(0xFF - maskval)); break;
+ case 5: alpha = max(alpha, uchar(0xFF - maskval)); break;
+ default: alpha = maskval; break;
+ }
+ }
+}
+
+VAR(edgetolerance, 1, 4, 64);
+VAR(adaptivesample, 0, 2, 2);
+
+enum
+{
+ NO_SURFACE = 0,
+ SURFACE_AMBIENT_BOTTOM,
+ SURFACE_AMBIENT_TOP,
+ SURFACE_LIGHTMAP_BOTTOM,
+ SURFACE_LIGHTMAP_TOP,
+ SURFACE_LIGHTMAP_BLEND
+};
+
+#define SURFACE_AMBIENT SURFACE_AMBIENT_BOTTOM
+#define SURFACE_LIGHTMAP SURFACE_LIGHTMAP_BOTTOM
+
+static bool generatelightmap(lightmapworker *w, float lpu, const lerpvert *lv, int numv, vec origin1, const vec &xstep1, const vec &ystep1, vec origin2, const vec &xstep2, const vec &ystep2, float side0, float sidestep)
+{
+ static const float aacoords[8][2] =
+ {
+ {0.0f, 0.0f},
+ {-0.5f, -0.5f},
+ {0.0f, -0.5f},
+ {-0.5f, 0.0f},
+
+ {0.3f, -0.6f},
+ {0.6f, 0.3f},
+ {-0.3f, 0.6f},
+ {-0.6f, -0.3f},
+ };
+ float tolerance = 0.5 / lpu;
+ uint lightmask = 0, lightused = 0;
+ vec offsets1[8], offsets2[8];
+ loopi(8)
+ {
+ offsets1[i] = vec(xstep1).mul(aacoords[i][0]).add(vec(ystep1).mul(aacoords[i][1]));
+ offsets2[i] = vec(xstep2).mul(aacoords[i][0]).add(vec(ystep2).mul(aacoords[i][1]));
+ }
+ if((w->type&LM_TYPE) == LM_BUMPMAP0) memclear(w->raydata, (LM_MAXW + 4)*(LM_MAXH + 4));
+
+ origin1.sub(vec(ystep1).add(xstep1).mul(blurlms));
+ origin2.sub(vec(ystep2).add(xstep2).mul(blurlms));
+
+ int aasample = min(1 << lmaa, 4);
+ int stride = aasample*(w->w+1);
+ vec *sample = w->colordata;
+ uchar *skylight = w->ambient;
+ lerpbounds start, end;
+ initlerpbounds(-blurlms, -blurlms, lv, numv, start, end);
+ float sidex = side0 + blurlms*sidestep;
+ for(int y = 0; y < w->h; ++y, sidex += sidestep)
+ {
+ vec normal, nstep;
+ lerpnormal(-blurlms, y - blurlms, lv, numv, start, end, normal, nstep);
+
+ for(int x = 0; x < w->w; ++x, normal.add(nstep), skylight += w->bpp)
+ {
+#define EDGE_TOLERANCE(x, y) \
+ (x < blurlms \
+ || x+1 > w->w - blurlms \
+ || y < blurlms \
+ || y+1 > w->h - blurlms \
+ ? edgetolerance : 1)
+ float t = EDGE_TOLERANCE(x, y) * tolerance;
+ vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2);
+ lightused |= generatelumel(w, t, 0, w->lights, u, vec(normal).normalize(), *sample, x, y);
+ if(hasskylight())
+ {
+ if((w->type&LM_TYPE)==LM_BUMPMAP0 || !adaptivesample || sample->x<skylightcolor[0] || sample->y<skylightcolor[1] || sample->z<skylightcolor[2])
+ calcskylight(w, u, normal, t, skylight, lmshadows > 1 ? RAY_ALPHAPOLY : 0);
+ else loopk(3) skylight[k] = max(skylightcolor[k], ambientcolor[k]);
+ }
+ else loopk(3) skylight[k] = ambientcolor[k];
+ if(w->type&LM_ALPHA) generatealpha(w, t, u, skylight[3]);
+ sample += aasample;
+ }
+ sample += aasample;
+ }
+ if(adaptivesample > 1 && min(w->w, w->h) >= 2) lightmask = ~lightused;
+ sample = w->colordata;
+ initlerpbounds(-blurlms, -blurlms, lv, numv, start, end);
+ sidex = side0 + blurlms*sidestep;
+ for(int y = 0; y < w->h; ++y, sidex += sidestep)
+ {
+ vec normal, nstep;
+ lerpnormal(-blurlms, y - blurlms, lv, numv, start, end, normal, nstep);
+
+ for(int x = 0; x < w->w; ++x, normal.add(nstep))
+ {
+ vec &center = *sample++;
+ if(adaptivesample && x > 0 && x+1 < w->w && y > 0 && y+1 < w->h && !lumelsample(center, aasample, stride))
+ loopi(aasample-1) *sample++ = center;
+ else
+ {
+#define AA_EDGE_TOLERANCE(x, y, i) EDGE_TOLERANCE(x + aacoords[i][0], y + aacoords[i][1])
+ vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2);
+ const vec *offsets = x < sidex ? offsets1 : offsets2;
+ vec n = vec(normal).normalize();
+ loopi(aasample-1)
+ generatelumel(w, AA_EDGE_TOLERANCE(x, y, i+1) * tolerance, lightmask, w->lights, vec(u).add(offsets[i+1]), n, *sample++, x, y);
+ if(lmaa == 3)
+ {
+ loopi(4)
+ {
+ vec s;
+ generatelumel(w, AA_EDGE_TOLERANCE(x, y, i+4) * tolerance, lightmask, w->lights, vec(u).add(offsets[i+4]), n, s, x, y);
+ center.add(s);
+ }
+ center.div(5);
+ }
+ }
+ }
+ if(aasample > 1)
+ {
+ vec u = w->w < sidex ? vec(xstep1).mul(w->w).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(w->w).add(vec(ystep2).mul(y)).add(origin2);
+ const vec *offsets = w->w < sidex ? offsets1 : offsets2;
+ vec n = vec(normal).normalize();
+ generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[1]), n, sample[1], w->w-1, y);
+ if(aasample > 2)
+ generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[3]), n, sample[3], w->w-1, y);
+ }
+ sample += aasample;
+ }
+
+ if(aasample > 1)
+ {
+ vec normal, nstep;
+ lerpnormal(-blurlms, w->h - blurlms, lv, numv, start, end, normal, nstep);
+
+ for(int x = 0; x <= w->w; ++x, normal.add(nstep))
+ {
+ vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(w->h)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(w->h)).add(origin2);
+ const vec *offsets = x < sidex ? offsets1 : offsets2;
+ vec n = vec(normal).normalize();
+ generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[1]), n, sample[1], min(x, w->w-1), w->h-1);
+ if(aasample > 2)
+ generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[2]), n, sample[2], min(x, w->w-1), w->h-1);
+ sample += aasample;
+ }
+ }
+ return true;
+}
+
+static int finishlightmap(lightmapworker *w)
+{
+ if(hasskylight() && blurskylight && (w->w>1 || w->h>1))
+ {
+ blurtexture(blurskylight, w->bpp, w->w, w->h, w->blur, w->ambient);
+ swap(w->blur, w->ambient);
+ }
+ vec *sample = w->colordata;
+ int aasample = min(1 << lmaa, 4), stride = aasample*(w->w+1);
+ float weight = 1.0f / (1.0f + 4.0f*lmaa),
+ cweight = weight * (lmaa == 3 ? 5.0f : 1.0f);
+ uchar *skylight = w->ambient;
+ vec *ray = w->raydata;
+ uchar *dstcolor = blurlms && (w->w > 1 || w->h > 1) ? w->blur : w->colorbuf;
+ uchar mincolor[4] = { 255, 255, 255, 255 }, maxcolor[4] = { 0, 0, 0, 0 };
+ bvec *dstray = blurlms && (w->w > 1 || w->h > 1) ? (bvec *)w->raydata : w->raybuf;
+ bvec minray(255, 255, 255), maxray(0, 0, 0);
+ loop(y, w->h)
+ {
+ loop(x, w->w)
+ {
+ vec l(0, 0, 0);
+ const vec &center = *sample++;
+ loopi(aasample-1) l.add(*sample++);
+ if(aasample > 1)
+ {
+ l.add(sample[1]);
+ if(aasample > 2) l.add(sample[3]);
+ }
+ vec *next = sample + stride - aasample;
+ if(aasample > 1)
+ {
+ l.add(next[1]);
+ if(aasample > 2) l.add(next[2]);
+ l.add(next[aasample+1]);
+ }
+
+ int r = int(center.x*cweight + l.x*weight),
+ g = int(center.y*cweight + l.y*weight),
+ b = int(center.z*cweight + l.z*weight),
+ ar = skylight[0], ag = skylight[1], ab = skylight[2];
+ dstcolor[0] = max(ar, r);
+ dstcolor[1] = max(ag, g);
+ dstcolor[2] = max(ab, b);
+ loopk(3)
+ {
+ mincolor[k] = min(mincolor[k], dstcolor[k]);
+ maxcolor[k] = max(maxcolor[k], dstcolor[k]);
+ }
+ if(w->type&LM_ALPHA)
+ {
+ dstcolor[3] = skylight[3];
+ mincolor[3] = min(mincolor[3], dstcolor[3]);
+ maxcolor[3] = max(maxcolor[3], dstcolor[3]);
+ }
+ if((w->type&LM_TYPE) == LM_BUMPMAP0)
+ {
+ if(ray->iszero()) dstray[0] = bvec(128, 128, 255);
+ else
+ {
+ // bias the normals towards the amount of ambient/skylight in the lumel
+ // this is necessary to prevent the light values in shaders from dropping too far below the skylight (to the ambient) if N.L is small
+ ray->normalize();
+ int l = max(r, max(g, b)), a = max(ar, max(ag, ab));
+ ray->mul(max(l-a, 0));
+ ray->z += a;
+ dstray[0] = bvec(ray->normalize());
+ }
+ loopk(3)
+ {
+ minray[k] = min(minray[k], dstray[0][k]);
+ maxray[k] = max(maxray[k], dstray[0][k]);
+ }
+ ray++;
+ dstray++;
+ }
+ dstcolor += w->bpp;
+ skylight += w->bpp;
+ }
+ sample += aasample;
+ }
+ if(int(maxcolor[0]) - int(mincolor[0]) <= lighterror &&
+ int(maxcolor[1]) - int(mincolor[1]) <= lighterror &&
+ int(maxcolor[2]) - int(mincolor[2]) <= lighterror &&
+ mincolor[3] >= maxcolor[3])
+ {
+ uchar color[3];
+ loopk(3) color[k] = (int(maxcolor[k]) + int(mincolor[k])) / 2;
+ if(color[0] <= int(ambientcolor[0]) + lighterror &&
+ color[1] <= int(ambientcolor[1]) + lighterror &&
+ color[2] <= int(ambientcolor[2]) + lighterror &&
+ (maxcolor[3]==0 || mincolor[3]==255))
+ return mincolor[3]==255 ? SURFACE_AMBIENT_TOP : SURFACE_AMBIENT_BOTTOM;
+ if((w->type&LM_TYPE) != LM_BUMPMAP0 ||
+ (int(maxray.x) - int(minray.x) <= bumperror &&
+ int(maxray.y) - int(minray.z) <= bumperror &&
+ int(maxray.z) - int(minray.z) <= bumperror))
+
+ {
+ memcpy(w->colorbuf, color, 3);
+ if(w->type&LM_ALPHA) w->colorbuf[3] = mincolor[3];
+ if((w->type&LM_TYPE) == LM_BUMPMAP0)
+ {
+ loopk(3) w->raybuf[0][k] = uchar((int(maxray[k])+int(minray[k]))/2);
+ }
+ w->lastlightmap->w = w->w = 1;
+ w->lastlightmap->h = w->h = 1;
+ }
+ }
+ if(blurlms && (w->w>1 || w->h>1))
+ {
+ blurtexture(blurlms, w->bpp, w->w, w->h, w->colorbuf, w->blur, blurlms);
+ if((w->type&LM_TYPE) == LM_BUMPMAP0) blurnormals(blurlms, w->w, w->h, w->raybuf, (const bvec *)w->raydata, blurlms);
+ w->lastlightmap->w = (w->w -= 2*blurlms);
+ w->lastlightmap->h = (w->h -= 2*blurlms);
+ }
+ if(mincolor[3]==255) return SURFACE_LIGHTMAP_TOP;
+ else if(maxcolor[3]==0) return SURFACE_LIGHTMAP_BOTTOM;
+ else return SURFACE_LIGHTMAP_BLEND;
+}
+
+static int previewlightmapalpha(lightmapworker *w, float lpu, const vec &origin1, const vec &xstep1, const vec &ystep1, const vec &origin2, const vec &xstep2, const vec &ystep2, float side0, float sidestep)
+{
+ extern int fullbrightlevel;
+ float tolerance = 0.5 / lpu;
+ uchar *dst = w->colorbuf;
+ uchar minalpha = 255, maxalpha = 0;
+ float sidex = side0;
+ for(int y = 0; y < w->h; ++y, sidex += sidestep)
+ {
+ for(int x = 0; x < w->w; ++x, dst += 4)
+ {
+ vec u = x < sidex ?
+ vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) :
+ vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2);
+ loopk(3) dst[k] = fullbrightlevel;
+ generatealpha(w, tolerance, u, dst[3]);
+ minalpha = min(minalpha, dst[3]);
+ maxalpha = max(maxalpha, dst[3]);
+ }
+ }
+ if(minalpha==255) return SURFACE_AMBIENT_TOP;
+ if(maxalpha==0) return SURFACE_AMBIENT_BOTTOM;
+ if(minalpha==maxalpha) w->w = w->h = 1;
+ if((w->type&LM_TYPE) == LM_BUMPMAP0) loopi(w->w*w->h) w->raybuf[i] = bvec(128, 128, 255);
+ return SURFACE_LIGHTMAP_BLEND;
+}
+
+static void clearsurfaces(cube *c)
+{
+ loopi(8)
+ {
+ if(c[i].ext)
+ {
+ loopj(6)
+ {
+ surfaceinfo &surf = c[i].ext->surfaces[j];
+ if(!surf.used()) continue;
+ surf.clear();
+ int numverts = surf.numverts&MAXFACEVERTS;
+ if(numverts)
+ {
+ if(!(c[i].merged&(1<<j))) { surf.numverts &= ~MAXFACEVERTS; continue; }
+
+ vertinfo *verts = c[i].ext->verts() + surf.verts;
+ loopk(numverts)
+ {
+ vertinfo &v = verts[k];
+ v.u = 0;
+ v.v = 0;
+ v.norm = 0;
+ }
+ }
+ }
+ }
+ if(c[i].children) clearsurfaces(c[i].children);
+ }
+}
+
+#define LIGHTCACHESIZE 1024
+
+static struct lightcacheentry
+{
+ int x, y;
+ vector<int> lights;
+} lightcache[LIGHTCACHESIZE];
+
+#define LIGHTCACHEHASH(x, y) (((((x)^(y))<<5) + (((x)^(y))>>5)) & (LIGHTCACHESIZE - 1))
+
+VARF(lightcachesize, 4, 6, 12, clearlightcache());
+
+void clearlightcache(int id)
+{
+ if(id >= 0)
+ {
+ const extentity &light = *entities::getents()[id];
+ int radius = light.attr1;
+ if(radius)
+ {
+ for(int x = int(max(light.o.x-radius, 0.0f))>>lightcachesize, ex = int(min(light.o.x+radius, worldsize-1.0f))>>lightcachesize; x <= ex; x++)
+ for(int y = int(max(light.o.y-radius, 0.0f))>>lightcachesize, ey = int(min(light.o.y+radius, worldsize-1.0f))>>lightcachesize; y <= ey; y++)
+ {
+ lightcacheentry &lce = lightcache[LIGHTCACHEHASH(x, y)];
+ if(lce.x != x || lce.y != y) continue;
+ lce.x = -1;
+ lce.lights.setsize(0);
+ }
+ return;
+ }
+ }
+
+ for(lightcacheentry *lce = lightcache; lce < &lightcache[LIGHTCACHESIZE]; lce++)
+ {
+ lce->x = -1;
+ lce->lights.setsize(0);
+ }
+}
+
+const vector<int> &checklightcache(int x, int y)
+{
+ x >>= lightcachesize;
+ y >>= lightcachesize;
+ lightcacheentry &lce = lightcache[LIGHTCACHEHASH(x, y)];
+ if(lce.x == x && lce.y == y) return lce.lights;
+
+ lce.lights.setsize(0);
+ int csize = 1<<lightcachesize, cx = x<<lightcachesize, cy = y<<lightcachesize;
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents)
+ {
+ const extentity &light = *ents[i];
+ switch(light.type)
+ {
+ case ET_LIGHT:
+ {
+ int radius = light.attr1;
+ if(radius > 0)
+ {
+ if(light.o.x + radius < cx || light.o.x - radius > cx + csize ||
+ light.o.y + radius < cy || light.o.y - radius > cy + csize)
+ continue;
+ }
+ break;
+ }
+ default: continue;
+ }
+ lce.lights.add(i);
+ }
+
+ lce.x = x;
+ lce.y = y;
+ return lce.lights;
+}
+
+static inline void addlight(lightmapworker *w, const extentity &light, int cx, int cy, int cz, int size, const vec *v, const vec *n, int numv)
+{
+ int radius = light.attr1;
+ if(radius > 0)
+ {
+ if(light.o.x + radius < cx || light.o.x - radius > cx + size ||
+ light.o.y + radius < cy || light.o.y - radius > cy + size ||
+ light.o.z + radius < cz || light.o.z - radius > cz + size)
+ return;
+ }
+
+ loopi(4)
+ {
+ vec p(light.o);
+ p.sub(v[i]);
+ float dist = p.dot(n[i]);
+ if(dist >= 0 && (!radius || dist < radius))
+ {
+ w->lights.add(&light);
+ break;
+ }
+ }
+}
+
+static bool findlights(lightmapworker *w, int cx, int cy, int cz, int size, const vec *v, const vec *n, int numv, const Slot &slot, const VSlot &vslot)
+{
+ w->lights.setsize(0);
+ const vector<extentity *> &ents = entities::getents();
+ static volatile bool usinglightcache = false;
+ if(size <= 1<<lightcachesize && (!lightlock || !usinglightcache))
+ {
+ if(lightlock) { SDL_LockMutex(lightlock); usinglightcache = true; }
+ const vector<int> &lights = checklightcache(cx, cy);
+ loopv(lights)
+ {
+ const extentity &light = *ents[lights[i]];
+ switch(light.type)
+ {
+ case ET_LIGHT: addlight(w, light, cx, cy, cz, size, v, n, numv); break;
+ }
+ }
+ if(lightlock) { usinglightcache = false; SDL_UnlockMutex(lightlock); }
+ }
+ else loopv(ents)
+ {
+ const extentity &light = *ents[i];
+ switch(light.type)
+ {
+ case ET_LIGHT: addlight(w, light, cx, cy, cz, size, v, n, numv); break;
+ }
+ }
+ if(vslot.layer && (setblendmaporigin(w->blendmapcache, ivec(cx, cy, cz), size) || slot.layermask)) return true;
+ return w->lights.length() || hasskylight() || sunlight;
+}
+
+static int packlightmaps(lightmapworker *w = NULL)
+{
+ int numpacked = 0;
+ for(; packidx < lightmaptasks[0].length(); packidx++, numpacked++)
+ {
+ lightmaptask &t = lightmaptasks[0][packidx];
+ if(!t.lightmaps) break;
+ if(t.ext && t.c->ext != t.ext)
+ {
+ lightmapext &e = lightmapexts.add();
+ e.c = t.c;
+ e.ext = t.ext;
+ }
+ progress = t.progress;
+ lightmapinfo *l = t.lightmaps;
+ if(l == (lightmapinfo *)-1) continue;
+ int space = 0;
+ for(; l && l->c == t.c; l = l->next)
+ {
+ l->packed = true;
+ space += l->bufsize;
+ if(l->surface < 0 || !t.ext) continue;
+ surfaceinfo &surf = t.ext->surfaces[l->surface];
+ layoutinfo layout;
+ packlightmap(*l, layout);
+ int numverts = surf.numverts&MAXFACEVERTS;
+ vertinfo *verts = t.ext->verts() + surf.verts;
+ if(l->layers&LAYER_DUP)
+ {
+ if(l->type&LM_ALPHA) surf.lmid[0] = layout.lmid;
+ else { surf.lmid[1] = layout.lmid; verts += numverts; }
+ }
+ else
+ {
+ if(l->layers&LAYER_TOP) surf.lmid[0] = layout.lmid;
+ if(l->layers&LAYER_BOTTOM) surf.lmid[1] = layout.lmid;
+ }
+ ushort offsetx = layout.x*((USHRT_MAX+1)/LM_PACKW), offsety = layout.y*((USHRT_MAX+1)/LM_PACKH);
+ loopk(numverts)
+ {
+ vertinfo &v = verts[k];
+ v.u += offsetx;
+ v.v += offsety;
+ }
+ }
+ if(t.worker == w)
+ {
+ w->bufused -= space;
+ w->bufstart = (w->bufstart + space)%LIGHTMAPBUFSIZE;
+ w->firstlightmap = l;
+ if(!l)
+ {
+ w->lastlightmap = NULL;
+ w->bufstart = w->bufused = 0;
+ }
+ }
+ if(t.worker->needspace) SDL_CondSignal(t.worker->spacecond);
+ }
+ return numpacked;
+}
+
+static lightmapinfo *alloclightmap(lightmapworker *w)
+{
+ int needspace1 = sizeof(lightmapinfo) + w->w*w->h*w->bpp,
+ needspace2 = (w->type&LM_TYPE) == LM_BUMPMAP0 ? w->w*w->h*3 : 0,
+ needspace = needspace1 + needspace2,
+ bufend = (w->bufstart + w->bufused)%LIGHTMAPBUFSIZE,
+ availspace = LIGHTMAPBUFSIZE - w->bufused,
+ availspace1 = min(availspace, LIGHTMAPBUFSIZE - bufend),
+ availspace2 = min(availspace, w->bufstart);
+ if(availspace < needspace || (max(availspace1, availspace2) < needspace && (availspace1 < needspace1 || availspace2 < needspace2)))
+ {
+ if(tasklock) SDL_LockMutex(tasklock);
+ while(!w->doneworking)
+ {
+ lightmapinfo *l = w->firstlightmap;
+ for(; l && l->packed; l = l->next)
+ {
+ w->bufused -= l->bufsize;
+ w->bufstart = (w->bufstart + l->bufsize)%LIGHTMAPBUFSIZE;
+ }
+ w->firstlightmap = l;
+ if(!l)
+ {
+ w->lastlightmap = NULL;
+ w->bufstart = w->bufused = 0;
+ }
+ bufend = (w->bufstart + w->bufused)%LIGHTMAPBUFSIZE;
+ availspace = LIGHTMAPBUFSIZE - w->bufused;
+ availspace1 = min(availspace, LIGHTMAPBUFSIZE - bufend);
+ availspace2 = min(availspace, w->bufstart);
+ if(availspace >= needspace && (max(availspace1, availspace2) >= needspace || (availspace1 >= needspace1 && availspace2 >= needspace2))) break;
+ if(packlightmaps(w)) continue;
+ if(!w->spacecond || !tasklock) break;
+ w->needspace = true;
+ SDL_CondWait(w->spacecond, tasklock);
+ w->needspace = false;
+ }
+ if(tasklock) SDL_UnlockMutex(tasklock);
+ }
+ int usedspace = needspace;
+ lightmapinfo *l = NULL;
+ if(availspace1 >= needspace1)
+ {
+ l = (lightmapinfo *)&w->buf[bufend];
+ w->colorbuf = (uchar *)(l + 1);
+ if((w->type&LM_TYPE) != LM_BUMPMAP0) w->raybuf = NULL;
+ else if(availspace1 >= needspace) w->raybuf = (bvec *)&w->buf[bufend + needspace1];
+ else
+ {
+ w->raybuf = (bvec *)w->buf;
+ usedspace += availspace1 - needspace1;
+ }
+ }
+ else if(availspace2 >= needspace)
+ {
+ usedspace += availspace1;
+ l = (lightmapinfo *)w->buf;
+ w->colorbuf = (uchar *)(l + 1);
+ w->raybuf = (w->type&LM_TYPE) == LM_BUMPMAP0 ? (bvec *)&w->buf[needspace1] : NULL;
+ }
+ else return NULL;
+ w->bufused += usedspace;
+ l->next = NULL;
+ l->c = w->c;
+ l->type = w->type;
+ l->w = w->w;
+ l->h = w->h;
+ l->bpp = w->bpp;
+ l->colorbuf = w->colorbuf;
+ l->raybuf = w->raybuf;
+ l->packed = false;
+ l->bufsize = usedspace;
+ l->surface = -1;
+ l->layers = 0;
+ if(!w->firstlightmap) w->firstlightmap = l;
+ if(w->lastlightmap) w->lastlightmap->next = l;
+ w->lastlightmap = l;
+ if(!w->curlightmaps) w->curlightmaps = l;
+ return l;
+}
+
+static void freelightmap(lightmapworker *w)
+{
+ lightmapinfo *l = w->lastlightmap;
+ if(!l || l->surface >= 0) return;
+ if(w->firstlightmap == w->lastlightmap)
+ {
+ w->firstlightmap = w->lastlightmap = w->curlightmaps = NULL;
+ w->bufstart = w->bufused = 0;
+ }
+ else
+ {
+ w->bufused -= l->bufsize - sizeof(lightmapinfo);
+ l->bufsize = sizeof(lightmapinfo);
+ l->packed = true;
+ }
+ if(w->curlightmaps == l) w->curlightmaps = NULL;
+}
+
+static int setupsurface(lightmapworker *w, plane planes[2], int numplanes, const vec *p, const vec *n, int numverts, vertinfo *litverts, bool preview = false)
+{
+ vec u, v, t;
+ vec2 c[MAXFACEVERTS];
+
+ u = vec(p[2]).sub(p[0]).normalize();
+ v.cross(planes[0], u);
+ c[0] = vec2(0, 0);
+ if(numplanes >= 2) t.cross(planes[1], u); else t = v;
+ vec r1 = vec(p[1]).sub(p[0]);
+ c[1] = vec2(r1.dot(u), min(r1.dot(v), 0.0f));
+ c[2] = vec2(vec(p[2]).sub(p[0]).dot(u), 0);
+ for(int i = 3; i < numverts; i++)
+ {
+ vec r = vec(p[i]).sub(p[0]);
+ c[i] = vec2(r.dot(u), max(r.dot(t), 0.0f));
+ }
+
+ float carea = 1e16f;
+ vec2 cx(0, 0), cy(0, 0), co(0, 0), cmin(0, 0), cmax(0, 0);
+ loopi(numverts)
+ {
+ vec2 px = vec2(c[i+1 < numverts ? i+1 : 0]).sub(c[i]);
+ float len = px.squaredlen();
+ if(!len) continue;
+ px.mul(1/sqrtf(len));
+ vec2 py(-px.y, px.x), pmin(0, 0), pmax(0, 0);
+ if(numplanes >= 2 && (i == 0 || i >= 3)) px.neg();
+ loopj(numverts)
+ {
+ vec2 rj = vec2(c[j]).sub(c[i]), pj(rj.dot(px), rj.dot(py));
+ pmin.x = min(pmin.x, pj.x);
+ pmin.y = min(pmin.y, pj.y);
+ pmax.x = max(pmax.x, pj.x);
+ pmax.y = max(pmax.y, pj.y);
+ }
+ float area = (pmax.x-pmin.x)*(pmax.y-pmin.y);
+ if(area < carea) { carea = area; cx = px; cy = py; co = c[i]; cmin = pmin; cmax = pmax; }
+ }
+
+ int scale = int(min(cmax.x - cmin.x, cmax.y - cmin.y));
+ float lpu = 16.0f / float(lightlod && scale < (1 << lightlod) ? max(lightprecision / 2, 1) : lightprecision);
+ int lw = clamp(int(ceil((cmax.x - cmin.x + 1)*lpu)), LM_MINW, LM_MAXW), lh = clamp(int(ceil((cmax.y - cmin.y + 1)*lpu)), LM_MINH, LM_MAXH);
+ w->w = lw;
+ w->h = lh;
+ if(!preview)
+ {
+ w->w += 2*blurlms;
+ w->h += 2*blurlms;
+ }
+ if(!alloclightmap(w)) return NO_SURFACE;
+
+ vec2 cscale = vec2(cmax).sub(cmin).div(vec2(lw-1, lh-1)),
+ comin = vec2(cx).mul(cmin.x).add(vec2(cy).mul(cmin.y)).add(co);
+ loopi(numverts)
+ {
+ vec2 ri = vec2(c[i]).sub(comin);
+ c[i] = vec2(ri.dot(cx)/cscale.x, ri.dot(cy)/cscale.y);
+ }
+
+ vec xstep1 = vec(v).mul(cx.y).add(vec(u).mul(cx.x)).mul(cscale.x),
+ ystep1 = vec(v).mul(cy.y).add(vec(u).mul(cy.x)).mul(cscale.y),
+ origin1 = vec(v).mul(comin.y).add(vec(u).mul(comin.x)).add(p[0]),
+ xstep2 = xstep1, ystep2 = ystep1, origin2 = origin1;
+ float side0 = LM_MAXW + 1, sidestep = 0;
+ if(numplanes >= 2)
+ {
+ xstep2 = vec(t).mul(cx.y).add(vec(u).mul(cx.x)).mul(cscale.x);
+ ystep2 = vec(t).mul(cy.y).add(vec(u).mul(cy.x)).mul(cscale.y);
+ origin2 = vec(t).mul(comin.y).add(vec(u).mul(comin.x)).add(p[0]);
+ if(cx.y) { side0 = comin.y/-(cx.y*cscale.x); sidestep = cy.y*cscale.y/-(cx.y*cscale.x); }
+ else if(cy.y) { side0 = ceil(comin.y/-(cy.y*cscale.y))*(LM_MAXW + 1); sidestep = -(LM_MAXW + 1); if(cy.y < 0) { side0 = (LM_MAXW + 1) - side0; sidestep = -sidestep; } }
+ else side0 = comin.y <= 0 ? LM_MAXW + 1 : -1;
+ }
+
+ int surftype = NO_SURFACE;
+ if(preview)
+ {
+ surftype = previewlightmapalpha(w, lpu, origin1, xstep1, ystep1, origin2, xstep2, ystep2, side0, sidestep);
+ }
+ else
+ {
+ lerpvert lv[MAXFACEVERTS];
+ int numv = numverts;
+ calclerpverts(c, n, lv, numv);
+
+ if(!generatelightmap(w, lpu, lv, numv, origin1, xstep1, ystep1, origin2, xstep2, ystep2, side0, sidestep)) return NO_SURFACE;
+ surftype = finishlightmap(w);
+ }
+ if(surftype<SURFACE_LIGHTMAP) return surftype;
+
+ vec2 texscale(float(USHRT_MAX+1)/LM_PACKW, float(USHRT_MAX+1)/LM_PACKH);
+ if(lw != w->w) texscale.x *= float(w->w - 1) / (lw - 1);
+ if(lh != w->h) texscale.y *= float(w->h - 1) / (lh - 1);
+ loopk(numverts)
+ {
+ litverts[k].u = ushort(floor(clamp(c[k].x*texscale.x, 0.0f, float(USHRT_MAX))));
+ litverts[k].v = ushort(floor(clamp(c[k].y*texscale.y, 0.0f, float(USHRT_MAX))));
+ }
+ return surftype;
+}
+
+static void removelmalpha(lightmapworker *w)
+{
+ if(!(w->type&LM_ALPHA)) return;
+ for(uchar *dst = w->colorbuf, *src = w->colorbuf, *end = &src[w->w*w->h*4];
+ src < end;
+ dst += 3, src += 4)
+ {
+ dst[0] = src[0];
+ dst[1] = src[1];
+ dst[2] = src[2];
+ }
+ w->type &= ~LM_ALPHA;
+ w->bpp = 3;
+ w->lastlightmap->type = w->type;
+ w->lastlightmap->bpp = w->bpp;
+}
+
+static lightmapinfo *setupsurfaces(lightmapworker *w, lightmaptask &task)
+{
+ cube &c = *task.c;
+ const ivec &co = task.o;
+ int size = task.size, usefacemask = task.usefaces;
+
+ w->curlightmaps = NULL;
+ w->c = &c;
+
+ surfaceinfo surfaces[6];
+ vertinfo litverts[6*2*MAXFACEVERTS];
+ int numlitverts = 0;
+ memclear(surfaces);
+ loopi(6)
+ {
+ int usefaces = usefacemask&0xF;
+ usefacemask >>= 4;
+ if(!usefaces)
+ {
+ if(!c.ext) continue;
+ surfaceinfo &surf = surfaces[i];
+ surf = c.ext->surfaces[i];
+ int numverts = surf.totalverts();
+ if(numverts)
+ {
+ memcpy(&litverts[numlitverts], c.ext->verts() + surf.verts, numverts*sizeof(vertinfo));
+ surf.verts = numlitverts;
+ numlitverts += numverts;
+ }
+ continue;
+ }
+
+ VSlot &vslot = lookupvslot(c.texture[i], false),
+ *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, false) : NULL;
+ Shader *shader = vslot.slot->shader;
+ int shadertype = shader->type;
+ if(layer) shadertype |= layer->slot->shader->type;
+
+ surfaceinfo &surf = surfaces[i];
+ vertinfo *curlitverts = &litverts[numlitverts];
+ int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0;
+ ivec mo(co);
+ int msz = size, convex = 0;
+ if(numverts)
+ {
+ vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts;
+ loopj(numverts) curlitverts[j].set(verts[j].getxyz());
+ if(c.merged&(1<<i))
+ {
+ msz = 1<<calcmergedsize(i, mo, size, verts, numverts);
+ mo.mask(~(msz-1));
+
+ if(!(surf.numverts&MAXFACEVERTS))
+ {
+ surf.verts = numlitverts;
+ surf.numverts |= numverts;
+ numlitverts += numverts;
+ }
+ }
+ else if(!flataxisface(c, i)) convex = faceconvexity(verts, numverts, size);
+ }
+ else
+ {
+ ivec v[4];
+ genfaceverts(c, i, v);
+ if(!flataxisface(c, i)) convex = faceconvexity(v);
+ int order = usefaces&4 || convex < 0 ? 1 : 0;
+ ivec vo = ivec(co).mask(0xFFF).shl(3);
+ curlitverts[numverts++].set(v[order].mul(size).add(vo));
+ if(usefaces&1) curlitverts[numverts++].set(v[order+1].mul(size).add(vo));
+ curlitverts[numverts++].set(v[order+2].mul(size).add(vo));
+ if(usefaces&2) curlitverts[numverts++].set(v[(order+3)&3].mul(size).add(vo));
+ }
+
+ vec pos[MAXFACEVERTS], n[MAXFACEVERTS], po(ivec(co).mask(~0xFFF));
+ loopj(numverts) pos[j] = vec(curlitverts[j].getxyz()).mul(1.0f/8).add(po);
+
+ plane planes[2];
+ int numplanes = 0;
+ planes[numplanes++].toplane(pos[0], pos[1], pos[2]);
+ if(numverts < 4 || !convex) loopk(numverts) findnormal(pos[k], planes[0], n[k]);
+ else
+ {
+ planes[numplanes++].toplane(pos[0], pos[2], pos[3]);
+ vec avg = vec(planes[0]).add(planes[1]).normalize();
+ findnormal(pos[0], avg, n[0]);
+ findnormal(pos[1], planes[0], n[1]);
+ findnormal(pos[2], avg, n[2]);
+ for(int k = 3; k < numverts; k++) findnormal(pos[k], planes[1], n[k]);
+ }
+
+ if(shadertype&(SHADER_NORMALSLMS | SHADER_ENVMAP))
+ {
+ loopk(numverts) curlitverts[k].norm = encodenormal(n[k]);
+ if(!(surf.numverts&MAXFACEVERTS))
+ {
+ surf.verts = numlitverts;
+ surf.numverts |= numverts;
+ numlitverts += numverts;
+ }
+ }
+
+ if(!findlights(w, mo.x, mo.y, mo.z, msz, pos, n, numverts, *vslot.slot, vslot))
+ {
+ if(surf.numverts&MAXFACEVERTS) surf.numverts |= LAYER_TOP;
+ continue;
+ }
+
+ w->slot = vslot.slot;
+ w->vslot = &vslot;
+ w->type = shader->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE;
+ if(layer) w->type |= LM_ALPHA;
+ w->bpp = w->type&LM_ALPHA ? 4 : 3;
+ w->orient = i;
+ w->rotate = vslot.rotation;
+ int surftype = setupsurface(w, planes, numplanes, pos, n, numverts, curlitverts);
+ switch(surftype)
+ {
+ case SURFACE_LIGHTMAP_BOTTOM:
+ if((shader->type^layer->slot->shader->type)&SHADER_NORMALSLMS ||
+ (shader->type&SHADER_NORMALSLMS && vslot.rotation!=layer->rotation))
+ {
+ freelightmap(w);
+ break;
+ }
+ // fall through
+ case SURFACE_LIGHTMAP_BLEND:
+ case SURFACE_LIGHTMAP_TOP:
+ {
+ if(!(surf.numverts&MAXFACEVERTS))
+ {
+ surf.verts = numlitverts;
+ surf.numverts |= numverts;
+ numlitverts += numverts;
+ }
+
+ w->lastlightmap->surface = i;
+ w->lastlightmap->layers = (surftype==SURFACE_LIGHTMAP_BOTTOM ? LAYER_BOTTOM : LAYER_TOP);
+ if(surftype==SURFACE_LIGHTMAP_BLEND)
+ {
+ surf.numverts |= LAYER_BLEND;
+ w->lastlightmap->layers = LAYER_TOP;
+ if((shader->type^layer->slot->shader->type)&SHADER_NORMALSLMS ||
+ (shader->type&SHADER_NORMALSLMS && vslot.rotation!=layer->rotation))
+ break;
+ w->lastlightmap->layers |= LAYER_BOTTOM;
+ }
+ else
+ {
+ if(surftype==SURFACE_LIGHTMAP_BOTTOM)
+ {
+ surf.numverts |= LAYER_BOTTOM;
+ w->lastlightmap->layers = LAYER_BOTTOM;
+ }
+ else
+ {
+ surf.numverts |= LAYER_TOP;
+ w->lastlightmap->layers = LAYER_TOP;
+ }
+ if(w->type&LM_ALPHA) removelmalpha(w);
+ }
+ continue;
+ }
+
+ case SURFACE_AMBIENT_BOTTOM:
+ freelightmap(w);
+ surf.numverts |= layer ? LAYER_BOTTOM : LAYER_TOP;
+ continue;
+
+ case SURFACE_AMBIENT_TOP:
+ freelightmap(w);
+ surf.numverts |= LAYER_TOP;
+ continue;
+
+ default:
+ freelightmap(w);
+ continue;
+ }
+
+ w->slot = layer->slot;
+ w->vslot = layer;
+ w->type = layer->slot->shader->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE;
+ w->bpp = 3;
+ w->rotate = layer->rotation;
+ vertinfo *blendverts = surf.numverts&MAXFACEVERTS ? &curlitverts[numverts] : curlitverts;
+ switch(setupsurface(w, planes, numplanes, pos, n, numverts, blendverts))
+ {
+ case SURFACE_LIGHTMAP_TOP:
+ {
+ if(!(surf.numverts&MAXFACEVERTS))
+ {
+ surf.verts = numlitverts;
+ surf.numverts |= numverts;
+ numlitverts += numverts;
+ }
+ else if(!(surf.numverts&LAYER_DUP))
+ {
+ surf.numverts |= LAYER_DUP;
+ w->lastlightmap->layers |= LAYER_DUP;
+ loopk(numverts)
+ {
+ vertinfo &src = curlitverts[k];
+ vertinfo &dst = blendverts[k];
+ dst.setxyz(src.getxyz());
+ dst.norm = src.norm;
+ }
+ numlitverts += numverts;
+ }
+ surf.numverts |= LAYER_BOTTOM;
+ w->lastlightmap->layers |= LAYER_BOTTOM;
+
+ w->lastlightmap->surface = i;
+ break;
+ }
+
+ case SURFACE_AMBIENT_TOP:
+ {
+ freelightmap(w);
+ surf.numverts |= LAYER_BOTTOM;
+ break;
+ }
+
+ default: freelightmap(w); break;
+ }
+ }
+ loopk(6)
+ {
+ surfaceinfo &surf = surfaces[k];
+ if(surf.used())
+ {
+ cubeext *ext = c.ext && c.ext->maxverts >= numlitverts ? c.ext : growcubeext(c.ext, numlitverts);
+ memcpy(ext->surfaces, surfaces, sizeof(ext->surfaces));
+ memcpy(ext->verts(), litverts, numlitverts*sizeof(vertinfo));
+ task.ext = ext;
+ break;
+ }
+ }
+ return w->curlightmaps ? w->curlightmaps : (lightmapinfo *)-1;
+}
+
+int lightmapworker::work(void *data)
+{
+ lightmapworker *w = (lightmapworker *)data;
+ SDL_LockMutex(tasklock);
+ while(!w->doneworking)
+ {
+ if(allocidx < lightmaptasks[0].length())
+ {
+ lightmaptask &t = lightmaptasks[0][allocidx++];
+ t.worker = w;
+ SDL_UnlockMutex(tasklock);
+ lightmapinfo *l = setupsurfaces(w, t);
+ SDL_LockMutex(tasklock);
+ t.lightmaps = l;
+ packlightmaps(w);
+ }
+ else
+ {
+ if(packidx >= lightmaptasks[0].length()) SDL_CondSignal(emptycond);
+ SDL_CondWait(fullcond, tasklock);
+ }
+ }
+ SDL_UnlockMutex(tasklock);
+ return 0;
+}
+
+static bool processtasks(bool finish = false)
+{
+ if(tasklock) SDL_LockMutex(tasklock);
+ while(finish || lightmaptasks[1].length())
+ {
+ if(packidx >= lightmaptasks[0].length())
+ {
+ if(lightmaptasks[1].empty()) break;
+ lightmaptasks[0].setsize(0);
+ lightmaptasks[0].move(lightmaptasks[1]);
+ packidx = allocidx = 0;
+ if(fullcond) SDL_CondBroadcast(fullcond);
+ }
+ else if(lightmapping > 1)
+ {
+ SDL_CondWaitTimeout(emptycond, tasklock, 250);
+ CHECK_PROGRESS_LOCKED({ SDL_UnlockMutex(tasklock); return false; }, SDL_UnlockMutex(tasklock), SDL_LockMutex(tasklock));
+ }
+ else
+ {
+ while(allocidx < lightmaptasks[0].length())
+ {
+ lightmaptask &t = lightmaptasks[0][allocidx++];
+ t.worker = lightmapworkers[0];
+ t.lightmaps = setupsurfaces(lightmapworkers[0], t);
+ packlightmaps(lightmapworkers[0]);
+ CHECK_PROGRESS(return false);
+ }
+ }
+ }
+ if(tasklock) SDL_UnlockMutex(tasklock);
+ return true;
+}
+
+static void generatelightmaps(cube *c, const ivec &co, int size)
+{
+ CHECK_PROGRESS(return);
+
+ taskprogress++;
+
+ loopi(8)
+ {
+ ivec o(i, co, size);
+ if(c[i].children)
+ generatelightmaps(c[i].children, o, size >> 1);
+ else if(!isempty(c[i]))
+ {
+ if(c[i].ext)
+ {
+ loopj(6)
+ {
+ surfaceinfo &surf = c[i].ext->surfaces[j];
+ if(surf.lmid[0] >= LMID_RESERVED || surf.lmid[1] >= LMID_RESERVED) goto nextcube;
+ surf.clear();
+ }
+ }
+ int usefacemask = 0;
+ loopj(6) if(c[i].texture[j] != DEFAULT_SKY && (!(c[i].merged&(1<<j)) || (c[i].ext && c[i].ext->surfaces[j].numverts&MAXFACEVERTS)))
+ {
+ usefacemask |= visibletris(c[i], j, o, size)<<(4*j);
+ }
+ if(usefacemask)
+ {
+ lightmaptask &t = lightmaptasks[1].add();
+ t.o = o;
+ t.size = size;
+ t.usefaces = usefacemask;
+ t.c = &c[i];
+ t.ext = NULL;
+ t.lightmaps = NULL;
+ t.progress = taskprogress;
+ if(lightmaptasks[1].length() >= MAXLIGHTMAPTASKS && !processtasks()) return;
+ }
+ }
+ nextcube:;
+ }
+}
+
+static bool previewblends(lightmapworker *w, cube &c, const ivec &co, int size)
+{
+ if(isempty(c) || c.material&MAT_ALPHA) return false;
+
+ int usefacemask = 0;
+ loopi(6) if(c.texture[i] != DEFAULT_SKY && lookupvslot(c.texture[i], false).layer)
+ usefacemask |= visibletris(c, i, co, size)<<(4*i);
+ if(!usefacemask) return false;
+
+ if(!setblendmaporigin(w->blendmapcache, co, size))
+ {
+ if(!c.ext) return false;
+ bool blends = false;
+ loopi(6) if(c.ext->surfaces[i].numverts&LAYER_BOTTOM)
+ {
+ c.ext->surfaces[i].brighten();
+ blends = true;
+ }
+ return blends;
+ }
+
+ w->firstlightmap = w->lastlightmap = w->curlightmaps = NULL;
+ w->bufstart = w->bufused = 0;
+ w->c = &c;
+
+ surfaceinfo surfaces[6];
+ vertinfo litverts[6*2*MAXFACEVERTS];
+ int numlitverts = 0;
+ memcpy(surfaces, c.ext ? c.ext->surfaces : brightsurfaces, sizeof(surfaces));
+ loopi(6)
+ {
+ int usefaces = usefacemask&0xF;
+ usefacemask >>= 4;
+ if(!usefaces)
+ {
+ surfaceinfo &surf = surfaces[i];
+ int numverts = surf.totalverts();
+ if(numverts)
+ {
+ memcpy(&litverts[numlitverts], c.ext->verts() + surf.verts, numverts*sizeof(vertinfo));
+ surf.verts = numlitverts;
+ numlitverts += numverts;
+ }
+ continue;
+ }
+
+ VSlot &vslot = lookupvslot(c.texture[i], false),
+ &layer = lookupvslot(vslot.layer, false);
+ Shader *shader = vslot.slot->shader;
+ int shadertype = shader->type | layer.slot->shader->type;
+
+ vertinfo *curlitverts = &litverts[numlitverts];
+ int numverts = 0;
+ ivec v[4];
+ genfaceverts(c, i, v);
+ int convex = flataxisface(c, i) ? 0 : faceconvexity(v),
+ order = usefaces&4 || convex < 0 ? 1 : 0;
+ ivec vo = ivec(co).mask(0xFFF).shl(3);
+ curlitverts[numverts++].set(v[order].mul(size).add(vo));
+ if(usefaces&1) curlitverts[numverts++].set(v[order+1].mul(size).add(vo));
+ curlitverts[numverts++].set(v[order+2].mul(size).add(vo));
+ if(usefaces&2) curlitverts[numverts++].set(v[(order+3)&3].mul(size).add(vo));
+
+ vec pos[4], n[4], po(ivec(co).mask(~0xFFF));
+ loopj(numverts) pos[j] = vec(curlitverts[j].getxyz()).mul(1.0f/8).add(po);
+
+ plane planes[2];
+ int numplanes = 0;
+ planes[numplanes++].toplane(pos[0], pos[1], pos[2]);
+ if(numverts < 4 || !convex) loopk(numverts) n[k] = planes[0];
+ else
+ {
+ planes[numplanes++].toplane(pos[0], pos[2], pos[3]);
+ vec avg = vec(planes[0]).add(planes[1]).normalize();
+ n[0] = avg;
+ n[1] = planes[0];
+ n[2] = avg;
+ for(int k = 3; k < numverts; k++) n[k] = planes[1];
+ }
+
+ surfaceinfo &surf = surfaces[i];
+ w->slot = vslot.slot;
+ w->vslot = &vslot;
+ w->type = shadertype&SHADER_NORMALSLMS ? LM_BUMPMAP0|LM_ALPHA : LM_DIFFUSE|LM_ALPHA;
+ w->bpp = 4;
+ w->orient = i;
+ w->rotate = vslot.rotation;
+ int surftype = setupsurface(w, planes, numplanes, pos, n, numverts, curlitverts, true);
+ switch(surftype)
+ {
+ case SURFACE_AMBIENT_TOP:
+ surf = brightsurface;
+ continue;
+
+ case SURFACE_AMBIENT_BOTTOM:
+ surf = brightbottomsurface;
+ continue;
+
+ case SURFACE_LIGHTMAP_BLEND:
+ {
+ if(surf.numverts == (LAYER_BLEND|numverts) &&
+ surf.lmid[0] == surf.lmid[1] &&
+ (surf.numverts&MAXFACEVERTS) == numverts &&
+ !memcmp(curlitverts, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo)) &&
+ lightmaps.inrange(surf.lmid[0]-LMID_RESERVED) &&
+ lightmaps[surf.lmid[0]-LMID_RESERVED].type==w->type)
+ {
+ vertinfo *oldverts = c.ext->verts() + surf.verts;
+ layoutinfo layout;
+ layout.w = w->w;
+ layout.h = w->h;
+ layout.x = (oldverts[0].x - curlitverts[0].x)/((USHRT_MAX+1)/LM_PACKW);
+ layout.y = (oldverts[0].y - curlitverts[0].y)/((USHRT_MAX+1)/LM_PACKH);
+ if(LM_PACKW - layout.x >= w->w && LM_PACKH - layout.y >= w->h)
+ {
+ layout.lmid = surf.lmid[0];
+ copylightmap(*w->lastlightmap, layout);
+ updatelightmap(layout);
+ surf.verts = numlitverts;
+ numlitverts += numverts;
+ continue;
+ }
+ }
+
+ surf.verts = numlitverts;
+ surf.numverts = LAYER_BLEND|numverts;
+ numlitverts += numverts;
+ layoutinfo layout;
+ if(packlightmap(*w->lastlightmap, layout)) updatelightmap(layout);
+ surf.lmid[0] = surf.lmid[1] = layout.lmid;
+ ushort offsetx = layout.x*((USHRT_MAX+1)/LM_PACKW), offsety = layout.y*((USHRT_MAX+1)/LM_PACKH);
+ loopk(numverts)
+ {
+ vertinfo &v = curlitverts[k];
+ v.u += offsetx;
+ v.v += offsety;
+ }
+ continue;
+ }
+ }
+ }
+
+ setsurfaces(c, surfaces, litverts, numlitverts);
+ return true;
+}
+
+static bool previewblends(lightmapworker *w, cube *c, const ivec &co, int size, const ivec &bo, const ivec &bs)
+{
+ bool changed = false;
+ loopoctabox(co, size, bo, bs)
+ {
+ ivec o(i, co, size);
+ cubeext *ext = c[i].ext;
+ if(ext && ext->va && ext->va->hasmerges)
+ {
+ changed = true;
+ destroyva(ext->va);
+ ext->va = NULL;
+ invalidatemerges(c[i], co, size, true);
+ }
+ if(c[i].children ? previewblends(w, c[i].children, o, size/2, bo, bs) : previewblends(w, c[i], o, size))
+ {
+ changed = true;
+ ext = c[i].ext;
+ if(ext && ext->va)
+ {
+ destroyva(ext->va);
+ ext->va = NULL;
+ }
+ }
+ }
+ return changed;
+}
+
+void previewblends(const ivec &bo, const ivec &bs)
+{
+ loadlayermasks();
+ if(lightmapworkers.empty()) lightmapworkers.add(new lightmapworker);
+ lightmapworkers[0]->reset();
+ if(previewblends(lightmapworkers[0], worldroot, ivec(0, 0, 0), worldsize/2, bo, bs))
+ commitchanges(true);
+}
+
+void cleanuplightmaps()
+{
+ loopv(lightmaps)
+ {
+ LightMap &lm = lightmaps[i];
+ lm.tex = lm.offsetx = lm.offsety = -1;
+ }
+ loopv(lightmaptexs) glDeleteTextures(1, &lightmaptexs[i].id);
+ lightmaptexs.shrink(0);
+ if(progresstex) { glDeleteTextures(1, &progresstex); progresstex = 0; }
+}
+
+void resetlightmaps(bool fullclean)
+{
+ cleanuplightmaps();
+ lightmaps.shrink(0);
+ compressed.clear();
+ clearlightcache();
+ if(fullclean) while(lightmapworkers.length()) delete lightmapworkers.pop();
+}
+
+lightmapworker::lightmapworker()
+{
+ buf = new uchar[LIGHTMAPBUFSIZE];
+ bufstart = bufused = 0;
+ firstlightmap = lastlightmap = curlightmaps = NULL;
+ ambient = new uchar[4*(LM_MAXW + 4)*(LM_MAXH + 4)];
+ blur = new uchar[4*(LM_MAXW + 4)*(LM_MAXH + 4)];
+ colordata = new vec[4*(LM_MAXW+1 + 4)*(LM_MAXH+1 + 4)];
+ raydata = new vec[(LM_MAXW + 4)*(LM_MAXH + 4)];
+ shadowraycache = newshadowraycache();
+ blendmapcache = newblendmapcache();
+ needspace = doneworking = false;
+ spacecond = NULL;
+ thread = NULL;
+}
+
+lightmapworker::~lightmapworker()
+{
+ cleanupthread();
+ delete[] buf;
+ delete[] ambient;
+ delete[] blur;
+ delete[] colordata;
+ delete[] raydata;
+ freeshadowraycache(shadowraycache);
+ freeblendmapcache(blendmapcache);
+}
+
+void lightmapworker::cleanupthread()
+{
+ if(spacecond) { SDL_DestroyCond(spacecond); spacecond = NULL; }
+ thread = NULL;
+}
+
+void lightmapworker::reset()
+{
+ bufstart = bufused = 0;
+ firstlightmap = lastlightmap = curlightmaps = NULL;
+ needspace = doneworking = false;
+ resetshadowraycache(shadowraycache);
+}
+
+bool lightmapworker::setupthread()
+{
+ if(!spacecond) spacecond = SDL_CreateCond();
+ if(!spacecond) return false;
+ thread = SDL_CreateThread(work, "lightmap worker", this);
+ return thread!=NULL;
+}
+
+static Uint32 calclighttimer(Uint32 interval, void *param)
+{
+ check_calclight_progress = true;
+ return interval;
+}
+
+bool setlightmapquality(int quality)
+{
+ switch(quality)
+ {
+ case 1: lmshadows = 2; lmaa = 3; lerptjoints = 1; break;
+ case 0: lmshadows = lmshadows_; lmaa = lmaa_; lerptjoints = lerptjoints_; break;
+ case -1: lmshadows = 1; lmaa = 0; lerptjoints = 0; break;
+ default: return false;
+ }
+ return true;
+}
+
+VARP(lightthreads, 0, 0, 16);
+
+#define ALLOCLOCK(name, init) { if(lightmapping > 1) name = init(); if(!name) lightmapping = 1; }
+#define FREELOCK(name, destroy) { if(name) { destroy(name); name = NULL; } }
+
+static void cleanuplocks()
+{
+ FREELOCK(lightlock, SDL_DestroyMutex);
+ FREELOCK(tasklock, SDL_DestroyMutex);
+ FREELOCK(fullcond, SDL_DestroyCond);
+ FREELOCK(emptycond, SDL_DestroyCond);
+}
+
+static void setupthreads(int numthreads)
+{
+ loopi(2) lightmaptasks[i].setsize(0);
+ lightmapexts.setsize(0);
+ packidx = allocidx = 0;
+ lightmapping = numthreads;
+ if(lightmapping > 1)
+ {
+ ALLOCLOCK(lightlock, SDL_CreateMutex);
+ ALLOCLOCK(tasklock, SDL_CreateMutex);
+ ALLOCLOCK(fullcond, SDL_CreateCond);
+ ALLOCLOCK(emptycond, SDL_CreateCond);
+ }
+ while(lightmapworkers.length() < lightmapping) lightmapworkers.add(new lightmapworker);
+ loopi(lightmapping)
+ {
+ lightmapworker *w = lightmapworkers[i];
+ w->reset();
+ if(lightmapping <= 1 || w->setupthread()) continue;
+ w->cleanupthread();
+ lightmapping = i >= 1 ? max(i, 2) : 1;
+ break;
+ }
+ if(lightmapping <= 1) cleanuplocks();
+}
+
+static void cleanupthreads()
+{
+ processtasks(true);
+ if(lightmapping > 1)
+ {
+ SDL_LockMutex(tasklock);
+ loopv(lightmapworkers) lightmapworkers[i]->doneworking = true;
+ SDL_CondBroadcast(fullcond);
+ loopv(lightmapworkers)
+ {
+ lightmapworker *w = lightmapworkers[i];
+ if(w->needspace && w->spacecond) SDL_CondSignal(w->spacecond);
+ }
+ SDL_UnlockMutex(tasklock);
+ loopv(lightmapworkers)
+ {
+ lightmapworker *w = lightmapworkers[i];
+ if(w->thread) SDL_WaitThread(w->thread, NULL);
+ }
+ }
+ loopv(lightmapexts)
+ {
+ lightmapext &e = lightmapexts[i];
+ setcubeext(*e.c, e.ext);
+ }
+ loopv(lightmapworkers) lightmapworkers[i]->cleanupthread();
+ cleanuplocks();
+ lightmapping = 0;
+}
+
+void calclight(int *quality)
+{
+ if(!setlightmapquality(*quality))
+ {
+ conoutf(CON_ERROR, "valid range for calclight quality is -1..1");
+ return;
+ }
+ renderbackground("computing lightmaps... (esc to abort)");
+ mpremip(true);
+ optimizeblendmap();
+ loadlayermasks();
+ int numthreads = lightthreads > 0 ? lightthreads : numcpus;
+ if(numthreads > 1) preloadusedmapmodels(false, true);
+ resetlightmaps(false);
+ clearsurfaces(worldroot);
+ taskprogress = progress = 0;
+ progresstexticks = 0;
+ progresslightmap = -1;
+ calclight_canceled = false;
+ check_calclight_progress = false;
+ SDL_TimerID timer = SDL_AddTimer(250, calclighttimer, NULL);
+ Uint32 start = SDL_GetTicks();
+ calcnormals(lerptjoints > 0);
+ show_calclight_progress();
+ setupthreads(numthreads);
+ generatelightmaps(worldroot, ivec(0, 0, 0), worldsize >> 1);
+ cleanupthreads();
+ clearnormals();
+ Uint32 end = SDL_GetTicks();
+ if(timer) SDL_RemoveTimer(timer);
+ uint total = 0, lumels = 0;
+ loopv(lightmaps)
+ {
+ insertunlit(i);
+ if(!editmode) lightmaps[i].finalize();
+ total += lightmaps[i].lightmaps;
+ lumels += lightmaps[i].lumels;
+ }
+ if(!editmode) compressed.clear();
+ initlights();
+ renderbackground("lighting done...");
+ allchanged();
+ if(calclight_canceled)
+ conoutf("calclight aborted");
+ else
+ conoutf("generated %d lightmaps using %d%% of %d textures (%.1f seconds)",
+ total,
+ lightmaps.length() ? lumels * 100 / (lightmaps.length() * LM_PACKW * LM_PACKH) : 0,
+ lightmaps.length(),
+ (end - start) / 1000.0f);
+}
+
+COMMAND(calclight, "i");
+
+VAR(patchnormals, 0, 0, 1);
+
+void patchlight(int *quality)
+{
+ if(noedit(true)) return;
+ if(!setlightmapquality(*quality))
+ {
+ conoutf(CON_ERROR, "valid range for patchlight quality is -1..1");
+ return;
+ }
+ renderbackground("patching lightmaps... (esc to abort)");
+ loadlayermasks();
+ int numthreads = lightthreads > 0 ? lightthreads : numcpus;
+ if(numthreads > 1) preloadusedmapmodels(false, true);
+ cleanuplightmaps();
+ taskprogress = progress = 0;
+ progresstexticks = 0;
+ progresslightmap = -1;
+ int total = 0, lumels = 0;
+ loopv(lightmaps)
+ {
+ if((lightmaps[i].type&LM_TYPE) != LM_BUMPMAP1) progresslightmap = i;
+ total -= lightmaps[i].lightmaps;
+ lumels -= lightmaps[i].lumels;
+ }
+ calclight_canceled = false;
+ check_calclight_progress = false;
+ SDL_TimerID timer = SDL_AddTimer(250, calclighttimer, NULL);
+ if(patchnormals) renderprogress(0, "computing normals...");
+ Uint32 start = SDL_GetTicks();
+ if(patchnormals) calcnormals(lerptjoints > 0);
+ show_calclight_progress();
+ setupthreads(numthreads);
+ generatelightmaps(worldroot, ivec(0, 0, 0), worldsize >> 1);
+ cleanupthreads();
+ if(patchnormals) clearnormals();
+ Uint32 end = SDL_GetTicks();
+ if(timer) SDL_RemoveTimer(timer);
+ loopv(lightmaps)
+ {
+ total += lightmaps[i].lightmaps;
+ lumels += lightmaps[i].lumels;
+ }
+ initlights();
+ renderbackground("lighting done...");
+ allchanged();
+ if(calclight_canceled)
+ conoutf("patchlight aborted");
+ else
+ conoutf("patched %d lightmaps using %d%% of %d textures (%.1f seconds)",
+ total,
+ lightmaps.length() ? lumels * 100 / (lightmaps.length() * LM_PACKW * LM_PACKH) : 0,
+ lightmaps.length(),
+ (end - start) / 1000.0f);
+}
+
+COMMAND(patchlight, "i");
+
+void clearlightmaps()
+{
+ if(noedit(true)) return;
+ renderprogress(0, "clearing lightmaps...");
+ resetlightmaps(false);
+ clearsurfaces(worldroot);
+ initlights();
+ allchanged();
+}
+
+COMMAND(clearlightmaps, "");
+
+void setfullbrightlevel(int fullbrightlevel)
+{
+ if(lightmaptexs.length() > LMID_BRIGHT)
+ {
+ uchar bright[3] = { uchar(fullbrightlevel), uchar(fullbrightlevel), uchar(fullbrightlevel) };
+ createtexture(lightmaptexs[LMID_BRIGHT].id, 1, 1, bright, 0, 1);
+ }
+ initlights();
+}
+
+VARF(fullbright, 0, 0, 1, if(lightmaptexs.length()) { initlights(); lightents(); });
+VARF(fullbrightlevel, 0, 128, 255, setfullbrightlevel(fullbrightlevel));
+
+vector<LightMapTexture> lightmaptexs;
+
+static void rotatenormals(LightMap &lmlv, int x, int y, int w, int h, bool flipx, bool flipy, bool swapxy)
+{
+ uchar *lv = lmlv.data + 3*(y*LM_PACKW + x);
+ int stride = 3*(LM_PACKW-w);
+ loopi(h)
+ {
+ loopj(w)
+ {
+ if(flipx) lv[0] = 255 - lv[0];
+ if(flipy) lv[1] = 255 - lv[1];
+ if(swapxy) swap(lv[0], lv[1]);
+ lv += 3;
+ }
+ lv += stride;
+ }
+}
+
+static void rotatenormals(cube *c)
+{
+ loopi(8)
+ {
+ cube &ch = c[i];
+ if(ch.children)
+ {
+ rotatenormals(ch.children);
+ continue;
+ }
+ else if(!ch.ext) continue;
+ loopj(6) if(lightmaps.inrange(ch.ext->surfaces[j].lmid[0]+1-LMID_RESERVED))
+ {
+ VSlot &vslot = lookupvslot(ch.texture[j], false);
+ if(!vslot.rotation) continue;
+ surfaceinfo &surface = ch.ext->surfaces[j];
+ int numverts = surface.numverts&MAXFACEVERTS;
+ if(!numverts) continue;
+ LightMap &lmlv = lightmaps[surface.lmid[0]+1-LMID_RESERVED];
+ if((lmlv.type&LM_TYPE)!=LM_BUMPMAP1) continue;
+ ushort x1 = USHRT_MAX, y1 = USHRT_MAX, x2 = 0, y2 = 0;
+ vertinfo *verts = ch.ext->verts() + surface.verts;
+ loopk(numverts)
+ {
+ vertinfo &v = verts[k];
+ x1 = min(x1, v.u);
+ y1 = min(y1, v.u);
+ x2 = max(x2, v.u);
+ y2 = max(y2, v.v);
+ }
+ if(x1 > x2 || y1 > y2) continue;
+ x1 /= (USHRT_MAX+1)/LM_PACKW;
+ y1 /= (USHRT_MAX+1)/LM_PACKH;
+ x2 /= (USHRT_MAX+1)/LM_PACKW;
+ y2 /= (USHRT_MAX+1)/LM_PACKH;
+ const texrotation &r = texrotations[vslot.rotation < 4 ? 4-vslot.rotation : vslot.rotation];
+ rotatenormals(lmlv, x1, y1, x2-x1, y1-y1, r.flipx, r.flipy, r.swapxy);
+ }
+ }
+}
+
+void fixlightmapnormals()
+{
+ rotatenormals(worldroot);
+}
+
+void fixrotatedlightmaps(cube &c, const ivec &co, int size)
+{
+ if(c.children)
+ {
+ loopi(8) fixrotatedlightmaps(c.children[i], ivec(i, co, size>>1), size>>1);
+ return;
+ }
+ if(!c.ext) return;
+ loopi(6)
+ {
+ if(c.merged&(1<<i)) continue;
+ surfaceinfo &surf = c.ext->surfaces[i];
+ int numverts = surf.numverts&MAXFACEVERTS;
+ if(numverts!=4 || (surf.lmid[0] < LMID_RESERVED && surf.lmid[1] < LMID_RESERVED)) continue;
+ vertinfo *verts = c.ext->verts() + surf.verts;
+ int vis = visibletris(c, i, co, size);
+ if(!vis || vis==3) continue;
+ if((verts[0].u != verts[1].u || verts[0].v != verts[1].v) &&
+ (verts[0].u != verts[3].u || verts[0].v != verts[3].v) &&
+ (verts[2].u != verts[1].u || verts[2].v != verts[1].v) &&
+ (verts[2].u != verts[3].u || verts[2].v != verts[3].v))
+ continue;
+ if(vis&4)
+ {
+ vertinfo tmp = verts[0];
+ verts[0].x = verts[1].x; verts[0].y = verts[1].y; verts[0].z = verts[1].z;
+ verts[1].x = verts[2].x; verts[1].y = verts[2].y; verts[1].z = verts[2].z;
+ verts[2].x = verts[3].x; verts[2].y = verts[3].y; verts[2].z = verts[3].z;
+ verts[3].x = tmp.x; verts[3].y = tmp.y; verts[3].z = tmp.z;
+ if(surf.numverts&LAYER_DUP) loopk(4)
+ {
+ vertinfo &v = verts[k], &b = verts[k+4];
+ b.x = v.x;
+ b.y = v.y;
+ b.z = v.z;
+ }
+ }
+ surf.numverts = (surf.numverts & ~MAXFACEVERTS) | 3;
+ if(vis&2)
+ {
+ verts[1] = verts[2]; verts[2] = verts[3];
+ if(surf.numverts&LAYER_DUP) { verts[3] = verts[4]; verts[4] = verts[6]; verts[5] = verts[7]; }
+ }
+ else if(surf.numverts&LAYER_DUP) { verts[3] = verts[4]; verts[4] = verts[5]; verts[5] = verts[6]; }
+ }
+}
+
+void fixrotatedlightmaps()
+{
+ loopi(8) fixrotatedlightmaps(worldroot[i], ivec(i, ivec(0, 0, 0), worldsize>>1), worldsize>>1);
+}
+
+static void copylightmap(LightMap &lm, uchar *dst, size_t stride)
+{
+ const uchar *c = lm.data;
+ loopi(LM_PACKH)
+ {
+ memcpy(dst, c, lm.bpp*LM_PACKW);
+ c += lm.bpp*LM_PACKW;
+ dst += stride;
+ }
+}
+
+void genreservedlightmaptexs()
+{
+ while(lightmaptexs.length() < LMID_RESERVED)
+ {
+ LightMapTexture &tex = lightmaptexs.add();
+ tex.type = lightmaptexs.length()&1 ? LM_DIFFUSE : LM_BUMPMAP1;
+ glGenTextures(1, &tex.id);
+ }
+ uchar unlit[3] = { ambientcolor[0], ambientcolor[1], ambientcolor[2] };
+ createtexture(lightmaptexs[LMID_AMBIENT].id, 1, 1, unlit, 0, 1);
+ bvec front(128, 128, 255);
+ createtexture(lightmaptexs[LMID_AMBIENT1].id, 1, 1, &front, 0, 1);
+ uchar bright[3] = { uchar(fullbrightlevel), uchar(fullbrightlevel), uchar(fullbrightlevel) };
+ createtexture(lightmaptexs[LMID_BRIGHT].id, 1, 1, bright, 0, 1);
+ createtexture(lightmaptexs[LMID_BRIGHT1].id, 1, 1, &front, 0, 1);
+ uchar dark[3] = { 0, 0, 0 };
+ createtexture(lightmaptexs[LMID_DARK].id, 1, 1, dark, 0, 1);
+ createtexture(lightmaptexs[LMID_DARK1].id, 1, 1, &front, 0, 1);
+}
+
+static void findunlit(int i)
+{
+ LightMap &lm = lightmaps[i];
+ if(lm.unlitx>=0) return;
+ else if((lm.type&LM_TYPE)==LM_BUMPMAP0)
+ {
+ if(i+1>=lightmaps.length() || (lightmaps[i+1].type&LM_TYPE)!=LM_BUMPMAP1) return;
+ }
+ else if((lm.type&LM_TYPE)!=LM_DIFFUSE) return;
+ uchar *data = lm.data;
+ loop(y, 2) loop(x, LM_PACKW)
+ {
+ if(!data[0] && !data[1] && !data[2])
+ {
+ memcpy(data, ambientcolor.v, 3);
+ if((lm.type&LM_TYPE)==LM_BUMPMAP0) ((bvec *)lightmaps[i+1].data)[y*LM_PACKW + x] = bvec(128, 128, 255);
+ lm.unlitx = x;
+ lm.unlity = y;
+ return;
+ }
+ if(data[0]==ambientcolor[0] && data[1]==ambientcolor[1] && data[2]==ambientcolor[2])
+ {
+ if((lm.type&LM_TYPE)!=LM_BUMPMAP0 || ((bvec *)lightmaps[i+1].data)[y*LM_PACKW + x] == bvec(128, 128, 255))
+ {
+ lm.unlitx = x;
+ lm.unlity = y;
+ return;
+ }
+ }
+ data += lm.bpp;
+ }
+}
+
+VARF(roundlightmaptex, 0, 4, 16, { cleanuplightmaps(); initlights(); allchanged(); });
+VARF(batchlightmaps, 0, 4, 256, { cleanuplightmaps(); initlights(); allchanged(); });
+
+void genlightmaptexs(int flagmask, int flagval)
+{
+ if(lightmaptexs.length() < LMID_RESERVED) genreservedlightmaptexs();
+
+ int remaining[LM_TYPE+1] = { 0 }, total = 0;
+ loopv(lightmaps)
+ {
+ LightMap &lm = lightmaps[i];
+ if(lm.tex >= 0 || (lm.type&flagmask)!=flagval) continue;
+ int type = lm.type&LM_TYPE;
+ remaining[type]++;
+ total++;
+ if(lm.unlitx < 0) findunlit(i);
+ }
+
+ int sizelimit = (maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize)/max(LM_PACKW, LM_PACKH);
+ sizelimit = min(batchlightmaps, sizelimit*sizelimit);
+ while(total)
+ {
+ int type = LM_DIFFUSE;
+ LightMap *firstlm = NULL;
+ loopv(lightmaps)
+ {
+ LightMap &lm = lightmaps[i];
+ if(lm.tex >= 0 || (lm.type&flagmask) != flagval) continue;
+ type = lm.type&LM_TYPE;
+ firstlm = &lm;
+ break;
+ }
+ if(!firstlm) break;
+ int used = 0, uselimit = min(remaining[type], sizelimit);
+ do used++; while((1<<used) <= uselimit);
+ used--;
+ int oldval = remaining[type];
+ remaining[type] -= 1<<used;
+ if(remaining[type] && (2<<used) <= min(roundlightmaptex, sizelimit))
+ {
+ remaining[type] -= min(remaining[type], 1<<used);
+ used++;
+ }
+ total -= oldval - remaining[type];
+ LightMapTexture &tex = lightmaptexs.add();
+ tex.type = firstlm->type;
+ tex.w = LM_PACKW<<((used+1)/2);
+ tex.h = LM_PACKH<<(used/2);
+ int bpp = firstlm->bpp;
+ uchar *data = used ? new uchar[bpp*tex.w*tex.h] : NULL;
+ int offsetx = 0, offsety = 0;
+ loopv(lightmaps)
+ {
+ LightMap &lm = lightmaps[i];
+ if(lm.tex >= 0 || (lm.type&flagmask) != flagval || (lm.type&LM_TYPE) != type) continue;
+
+ lm.tex = lightmaptexs.length()-1;
+ lm.offsetx = offsetx;
+ lm.offsety = offsety;
+ if(tex.unlitx < 0 && lm.unlitx >= 0)
+ {
+ tex.unlitx = offsetx + lm.unlitx;
+ tex.unlity = offsety + lm.unlity;
+ }
+
+ if(data) copylightmap(lm, &data[bpp*(offsety*tex.w + offsetx)], bpp*tex.w);
+
+ offsetx += LM_PACKW;
+ if(offsetx >= tex.w) { offsetx = 0; offsety += LM_PACKH; }
+ if(offsety >= tex.h) break;
+ }
+
+ glGenTextures(1, &tex.id);
+ createtexture(tex.id, tex.w, tex.h, data ? data : firstlm->data, 3, 1, bpp==4 ? GL_RGBA : GL_RGB);
+ if(data) delete[] data;
+ }
+}
+
+bool brightengeom = false, shouldlightents = false;
+
+void clearlights()
+{
+ clearlightcache();
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents)
+ {
+ extentity &e = *ents[i];
+ e.light.color = vec(1, 1, 1);
+ e.light.dir = vec(0, 0, 1);
+ }
+ shouldlightents = false;
+
+ genlightmaptexs(LM_ALPHA, 0);
+ genlightmaptexs(LM_ALPHA, LM_ALPHA);
+ brightengeom = true;
+}
+
+void lightent(extentity &e, float height)
+{
+ if(e.type==ET_LIGHT) return;
+ float ambient = 0.0f;
+ if(e.type==ET_MAPMODEL)
+ {
+ model *m = loadmodel(NULL, e.attr2);
+ if(m) height = m->above()*0.75f;
+ }
+ else if(e.type>=ET_GAMESPECIFIC) ambient = 0.4f;
+ vec target(e.o.x, e.o.y, e.o.z + height);
+ lightreaching(target, e.light.color, e.light.dir, false, &e, ambient);
+}
+
+void lightents(bool force)
+{
+ if(!force && !shouldlightents) return;
+
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents) lightent(*ents[i]);
+
+ shouldlightents = false;
+}
+
+void initlights()
+{
+ if((fullbright && editmode) || lightmaps.empty())
+ {
+ clearlights();
+ return;
+ }
+
+ clearlightcache();
+ genlightmaptexs(LM_ALPHA, 0);
+ genlightmaptexs(LM_ALPHA, LM_ALPHA);
+ brightengeom = false;
+ shouldlightents = true;
+}
+
+static inline void fastskylight(const vec &o, float tolerance, uchar *skylight, int flags = RAY_ALPHAPOLY, extentity *t = NULL, bool fast = false)
+{
+ flags |= RAY_SHADOW;
+ if(skytexturelight) flags |= RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0);
+ if(fast)
+ {
+ static const vec ray(0, 0, 1);
+ if(shadowray(vec(ray).mul(tolerance).add(o), ray, 1e16f, flags, t)>1e15f)
+ memcpy(skylight, skylightcolor.v, 3);
+ else memcpy(skylight, ambientcolor.v, 3);
+ }
+ else
+ {
+ static const vec rays[5] =
+ {
+ vec(cosf(66*RAD)*cosf(65*RAD), sinf(66*RAD)*cosf(65*RAD), sinf(65*RAD)),
+ vec(cosf(156*RAD)*cosf(65*RAD), sinf(156*RAD)*cosf(65*RAD), sinf(65*RAD)),
+ vec(cosf(246*RAD)*cosf(65*RAD), sinf(246*RAD)*cosf(65*RAD), sinf(65*RAD)),
+ vec(cosf(336*RAD)*cosf(65*RAD), sinf(336*RAD)*cosf(65*RAD), sinf(65*RAD)),
+ vec(0, 0, 1),
+ };
+ int hit = 0;
+ loopi(5) if(shadowray(vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++;
+ loopk(3) skylight[k] = uchar(ambientcolor[k] + (max(skylightcolor[k], ambientcolor[k]) - ambientcolor[k])*hit/5.0f);
+ }
+}
+
+void lightreaching(const vec &target, vec &color, vec &dir, bool fast, extentity *t, float ambient)
+{
+ if((fullbright && editmode) || lightmaps.empty())
+ {
+ color = vec(1, 1, 1);
+ dir = vec(0, 0, 1);
+ return;
+ }
+
+ color = dir = vec(0, 0, 0);
+ const vector<extentity *> &ents = entities::getents();
+ const vector<int> &lights = checklightcache(int(target.x), int(target.y));
+ loopv(lights)
+ {
+ extentity &e = *ents[lights[i]];
+ if(e.type != ET_LIGHT)
+ continue;
+
+ vec ray(target);
+ ray.sub(e.o);
+ float mag = ray.magnitude();
+ if(e.attr1 && mag >= float(e.attr1))
+ continue;
+
+ if(mag < 1e-4f) ray = vec(0, 0, -1);
+ else
+ {
+ ray.div(mag);
+ if(shadowray(e.o, ray, mag, RAY_SHADOW | RAY_POLY, t) < mag)
+ continue;
+ }
+
+ float intensity = 1;
+ if(e.attr1)
+ intensity -= mag / float(e.attr1);
+ if(e.attached && e.attached->type==ET_SPOTLIGHT)
+ {
+ vec spot = vec(e.attached->o).sub(e.o).normalize();
+ float maxatten = sincos360[clamp(int(e.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten);
+ if(spotatten <= 0) continue;
+ intensity *= spotatten;
+ }
+
+ //if(target==player->o)
+ //{
+ // conoutf(CON_DEBUG, "%d - %f %f", i, intensity, mag);
+ //}
+
+ vec lightcol = vec(e.attr2, e.attr3, e.attr4).mul(1.0f/255);
+ color.add(vec(lightcol).mul(intensity));
+ dir.add(vec(ray).mul(-intensity*lightcol.x*lightcol.y*lightcol.z));
+ }
+ if(sunlight && shadowray(target, sunlightdir, 1e16f, RAY_SHADOW | RAY_POLY | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0), t) > 1e15f)
+ {
+ vec lightcol = vec(sunlightcolor.x, sunlightcolor.y, sunlightcolor.z).mul(sunlightscale/255);
+ color.add(lightcol);
+ dir.add(vec(sunlightdir).mul(lightcol.x*lightcol.y*lightcol.z));
+ }
+ if(hasskylight())
+ {
+ uchar skylight[3];
+ if(t) calcskylight(NULL, target, vec(0, 0, 0), 0.5f, skylight, RAY_POLY, t);
+ else fastskylight(target, 0.5f, skylight, RAY_POLY, t, fast);
+ loopk(3) color[k] = min(1.5f, max(max(skylight[k]/255.0f, ambient), color[k]));
+ }
+ else loopk(3) color[k] = min(1.5f, max(max(ambientcolor[k]/255.0f, ambient), color[k]));
+ if(dir.iszero()) dir = vec(0, 0, 1);
+ else dir.normalize();
+}
+
+entity *brightestlight(const vec &target, const vec &dir)
+{
+ if(sunlight && sunlightdir.dot(dir) > 0 && shadowray(target, sunlightdir, 1e16f, RAY_SHADOW | RAY_POLY | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0)) > 1e15f)
+ return &sunlightent;
+ const vector<extentity *> &ents = entities::getents();
+ const vector<int> &lights = checklightcache(int(target.x), int(target.y));
+ extentity *brightest = NULL;
+ float bintensity = 0;
+ loopv(lights)
+ {
+ extentity &e = *ents[lights[i]];
+ if(e.type != ET_LIGHT || vec(e.o).sub(target).dot(dir)<0)
+ continue;
+
+ vec ray(target);
+ ray.sub(e.o);
+ float mag = ray.magnitude();
+ if(e.attr1 && mag >= float(e.attr1))
+ continue;
+
+ ray.div(mag);
+ if(shadowray(e.o, ray, mag, RAY_SHADOW | RAY_POLY) < mag)
+ continue;
+ float intensity = 1;
+ if(e.attr1)
+ intensity -= mag / float(e.attr1);
+ if(e.attached && e.attached->type==ET_SPOTLIGHT)
+ {
+ vec spot = vec(e.attached->o).sub(e.o).normalize();
+ float maxatten = sincos360[clamp(int(e.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten);
+ if(spotatten <= 0) continue;
+ intensity *= spotatten;
+ }
+
+ if(!brightest || intensity > bintensity)
+ {
+ brightest = &e;
+ bintensity = intensity;
+ }
+ }
+ return brightest;
+}
+
+void dumplms()
+{
+ loopv(lightmaps)
+ {
+ ImageData temp(LM_PACKW, LM_PACKH, lightmaps[i].bpp, lightmaps[i].data);
+ const char *map = game::getclientmap(), *name = strrchr(map, '/');
+ defformatstring(buf, "lightmap_%s_%d.png", name ? name+1 : map, i);
+ savepng(buf, temp, true);
+ }
+}
+
+COMMAND(dumplms, "");
+
diff --git a/src/engine/lightmap.h b/src/engine/lightmap.h
new file mode 100644
index 0000000..51ddc73
--- /dev/null
+++ b/src/engine/lightmap.h
@@ -0,0 +1,146 @@
+#define LM_MINW 2
+#define LM_MINH 2
+#define LM_MAXW 128
+#define LM_MAXH 128
+#define LM_PACKW 512
+#define LM_PACKH 512
+
+struct PackNode
+{
+ PackNode *child1, *child2;
+ ushort x, y, w, h;
+ int available;
+
+ PackNode() : child1(0), child2(0), x(0), y(0), w(LM_PACKW), h(LM_PACKH), available(min(LM_PACKW, LM_PACKH)) {}
+ PackNode(ushort x, ushort y, ushort w, ushort h) : child1(0), child2(0), x(x), y(y), w(w), h(h), available(min(w, h)) {}
+
+ void clear()
+ {
+ DELETEP(child1);
+ DELETEP(child2);
+ }
+
+ ~PackNode()
+ {
+ clear();
+ }
+
+ bool insert(ushort &tx, ushort &ty, ushort tw, ushort th);
+};
+
+enum
+{
+ LM_DIFFUSE = 0,
+ LM_BUMPMAP0,
+ LM_BUMPMAP1,
+ LM_TYPE = 0x0F,
+
+ LM_ALPHA = 1<<4,
+ LM_FLAGS = 0xF0
+};
+
+struct LightMap
+{
+ int type, bpp, tex, offsetx, offsety;
+ PackNode packroot;
+ uint lightmaps, lumels;
+ int unlitx, unlity;
+ uchar *data;
+
+ LightMap()
+ : type(LM_DIFFUSE), bpp(3), tex(-1), offsetx(-1), offsety(-1),
+ lightmaps(0), lumels(0), unlitx(-1), unlity(-1),
+ data(NULL)
+ {
+ }
+
+ ~LightMap()
+ {
+ if(data) delete[] data;
+ }
+
+ void finalize()
+ {
+ packroot.clear();
+ packroot.available = 0;
+ }
+
+ void copy(ushort tx, ushort ty, uchar *src, ushort tw, ushort th);
+ bool insert(ushort &tx, ushort &ty, uchar *src, ushort tw, ushort th);
+};
+
+extern vector<LightMap> lightmaps;
+
+struct LightMapTexture
+{
+ int w, h, type;
+ GLuint id;
+ int unlitx, unlity;
+
+ LightMapTexture()
+ : w(0), h(0), type(LM_DIFFUSE), id(0), unlitx(-1), unlity(-1)
+ {}
+};
+
+extern vector<LightMapTexture> lightmaptexs;
+
+extern bvec ambientcolor, skylightcolor, sunlightcolor;
+extern float sunlightscale;
+extern vec sunlightdir;
+
+extern void clearlights();
+extern void initlights();
+extern void lightents(bool force = false);
+extern void clearlightcache(int id = -1);
+extern void resetlightmaps(bool fullclean = true);
+extern void brightencube(cube &c);
+extern void setsurfaces(cube &c, const surfaceinfo *surfs, const vertinfo *verts, int numverts);
+extern void setsurface(cube &c, int orient, const surfaceinfo &surf, const vertinfo *verts, int numverts);
+extern void previewblends(const ivec &bo, const ivec &bs);
+
+struct lerpvert
+{
+ vec normal;
+ vec2 tc;
+
+ bool operator==(const lerpvert &l) const { return tc == l.tc;; }
+ bool operator!=(const lerpvert &l) const { return tc != l.tc; }
+};
+
+struct lerpbounds
+{
+ const lerpvert *min;
+ const lerpvert *max;
+ float u, ustep;
+ vec normal, nstep;
+ int winding;
+};
+
+extern void calcnormals(bool lerptjoints = false);
+extern void clearnormals();
+extern void findnormal(const vec &key, const vec &surface, vec &v);
+extern void calclerpverts(const vec2 *c, const vec *n, lerpvert *lv, int &numv);
+extern void initlerpbounds(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end);
+extern void lerpnormal(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end, vec &normal, vec &nstep);
+
+#define CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, before, after) \
+ if(check_calclight_progress) \
+ { \
+ if(!calclight_canceled) \
+ { \
+ before; \
+ show_calclight_progress(); \
+ check_calclight_canceled(); \
+ after; \
+ } \
+ if(calclight_canceled) { exit; } \
+ }
+#define CHECK_CALCLIGHT_PROGRESS(exit, show_calclight_progress) CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, , )
+
+extern bool calclight_canceled;
+extern volatile bool check_calclight_progress;
+
+extern void check_calclight_canceled();
+
+extern int lightmapping;
+
diff --git a/src/engine/lightning.h b/src/engine/lightning.h
new file mode 100644
index 0000000..bc6e21c
--- /dev/null
+++ b/src/engine/lightning.h
@@ -0,0 +1,123 @@
+#define MAXLIGHTNINGSTEPS 64
+#define LIGHTNINGSTEP 8
+int lnjitterx[2][MAXLIGHTNINGSTEPS], lnjittery[2][MAXLIGHTNINGSTEPS];
+int lnjitterframe = 0, lastlnjitter = 0;
+
+VAR(lnjittermillis, 0, 100, 1000);
+VAR(lnjitterradius, 0, 4, 100);
+FVAR(lnjitterscale, 0, 0.5f, 10);
+VAR(lnscrollmillis, 1, 300, 5000);
+FVAR(lnscrollscale, 0, 0.125f, 10);
+FVAR(lnblendpower, 0, 0.25f, 1000);
+
+static void calclightningjitter(int frame)
+{
+ loopi(MAXLIGHTNINGSTEPS)
+ {
+ lnjitterx[lnjitterframe][i] = -lnjitterradius + rnd(2*lnjitterradius + 1);
+ lnjittery[lnjitterframe][i] = -lnjitterradius + rnd(2*lnjitterradius + 1);
+ }
+}
+
+static void setuplightning()
+{
+ if(!lastlnjitter || lastmillis-lastlnjitter > lnjittermillis)
+ {
+ if(!lastlnjitter) calclightningjitter(lnjitterframe);
+ lastlnjitter = lastmillis - (lastmillis%lnjittermillis);
+ calclightningjitter(lnjitterframe ^= 1);
+ }
+}
+
+static void renderlightning(Texture *tex, const vec &o, const vec &d, float sz)
+{
+ vec step(d);
+ step.sub(o);
+ float len = step.magnitude();
+ int numsteps = clamp(int(ceil(len/LIGHTNINGSTEP)), 2, MAXLIGHTNINGSTEPS);
+ step.div(numsteps+1);
+ int jitteroffset = detrnd(int(d.x+d.y+d.z), MAXLIGHTNINGSTEPS);
+ vec cur(o), up, right;
+ up.orthogonal(step);
+ up.normalize();
+ right.cross(up, step);
+ right.normalize();
+ float scroll = -float(lastmillis%lnscrollmillis)/lnscrollmillis,
+ scrollscale = lnscrollscale*(LIGHTNINGSTEP*tex->ys)/(sz*tex->xs),
+ blend = pow(clamp(float(lastmillis - lastlnjitter)/lnjittermillis, 0.0f, 1.0f), lnblendpower),
+ jitter0 = (1-blend)*lnjitterscale*sz/lnjitterradius, jitter1 = blend*lnjitterscale*sz/lnjitterradius;
+ gle::begin(GL_TRIANGLE_STRIP);
+ loopj(numsteps)
+ {
+ vec next(cur);
+ next.add(step);
+ if(j+1==numsteps) next = d;
+ else
+ {
+ int lj = (j+jitteroffset)%MAXLIGHTNINGSTEPS;
+ next.add(vec(right).mul((jitter1*lnjitterx[lnjitterframe][lj] + jitter0*lnjitterx[lnjitterframe^1][lj])));
+ next.add(vec(up).mul((jitter1*lnjittery[lnjitterframe][lj] + jitter0*lnjittery[lnjitterframe^1][lj])));
+ }
+ vec dir1 = next, dir2 = next, across;
+ dir1.sub(cur);
+ dir2.sub(camera1->o);
+ across.cross(dir2, dir1).normalize().mul(sz);
+ gle::attribf(cur.x-across.x, cur.y-across.y, cur.z-across.z);
+ gle::attribf(scroll, 1);
+ gle::attribf(cur.x+across.x, cur.y+across.y, cur.z+across.z);
+ gle::attribf(scroll, 0);
+ scroll += scrollscale;
+ if(j+1==numsteps)
+ {
+ gle::attribf(next.x-across.x, next.y-across.y, next.z-across.z);
+ gle::attribf(scroll, 1);
+ gle::attribf(next.x+across.x, next.y+across.y, next.z+across.z);
+ gle::attribf(scroll, 0);
+ }
+ cur = next;
+ }
+ gle::end();
+}
+
+struct lightningrenderer : listrenderer
+{
+ lightningrenderer()
+ : listrenderer("packages/particles/lightning.jpg", 2, PT_LIGHTNING|PT_TRACK|PT_GLARE)
+ {}
+
+ void startrender()
+ {
+ glDisable(GL_CULL_FACE);
+ gle::defattrib(gle::ATTRIB_VERTEX, 3, GL_FLOAT);
+ gle::defattrib(gle::ATTRIB_TEXCOORD0, 2, GL_FLOAT);
+ }
+
+ void endrender()
+ {
+ glEnable(GL_CULL_FACE);
+ }
+
+ void update()
+ {
+ setuplightning();
+ }
+
+ void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity)
+ {
+ pe.maxfade = max(pe.maxfade, fade);
+ pe.extendbb(o, size);
+ pe.extendbb(d, size);
+ }
+
+ void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+ {
+ blend = min(blend<<2, 255);
+ if(type&PT_MOD) //multiply alpha into color
+ gle::colorub((p->color.r*blend)>>8, (p->color.g*blend)>>8, (p->color.b*blend)>>8);
+ else
+ gle::color(p->color, blend);
+ renderlightning(tex, o, d, p->size);
+ }
+};
+static lightningrenderer lightnings;
+
diff --git a/src/engine/main.cpp b/src/engine/main.cpp
new file mode 100644
index 0000000..f522479
--- /dev/null
+++ b/src/engine/main.cpp
@@ -0,0 +1,1422 @@
+// main.cpp: initialisation & main loop
+
+#include "engine.h"
+
+#ifdef SDL_VIDEO_DRIVER_X11
+#include "SDL_syswm.h"
+#endif
+
+extern void cleargamma();
+
+void cleanup()
+{
+ recorder::stop();
+ cleanupserver();
+ SDL_ShowCursor(SDL_TRUE);
+ SDL_SetRelativeMouseMode(SDL_FALSE);
+ if(screen) SDL_SetWindowGrab(screen, SDL_FALSE);
+ cleargamma();
+ freeocta(worldroot);
+ extern void clear_command(); clear_command();
+ extern void clear_console(); clear_console();
+ extern void clear_mdls(); clear_mdls();
+ extern void clear_sound(); clear_sound();
+ closelogfile();
+ #ifdef __APPLE__
+ if(screen) SDL_SetWindowFullscreen(screen, 0);
+ #endif
+ SDL_Quit();
+}
+
+extern void writeinitcfg();
+
+void quit() // normal exit
+{
+ writeinitcfg();
+ writeservercfg();
+ abortconnect();
+ disconnect();
+ localdisconnect();
+ writecfg();
+ cleanup();
+ exit(EXIT_SUCCESS);
+}
+
+void fatal(const char *s, ...) // failure exit
+{
+ static int errors = 0;
+ errors++;
+
+ if(errors <= 2) // print up to one extra recursive error
+ {
+ defvformatstring(msg,s,s);
+ logoutf("%s", msg);
+
+ if(errors <= 1) // avoid recursion
+ {
+ if(SDL_WasInit(SDL_INIT_VIDEO))
+ {
+ SDL_ShowCursor(SDL_TRUE);
+ SDL_SetRelativeMouseMode(SDL_FALSE);
+ if(screen) SDL_SetWindowGrab(screen, SDL_FALSE);
+ cleargamma();
+ #ifdef __APPLE__
+ if(screen) SDL_SetWindowFullscreen(screen, 0);
+ #endif
+ }
+ SDL_Quit();
+ SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Cube 2: Sauerbraten fatal error", msg, NULL);
+ }
+ }
+
+ exit(EXIT_FAILURE);
+}
+
+int curtime = 0, lastmillis = 1, elapsedtime = 0, totalmillis = 1;
+
+dynent *player = NULL;
+
+int initing = NOT_INITING;
+
+bool initwarning(const char *desc, int level, int type)
+{
+ if(initing < level)
+ {
+ addchange(desc, type);
+ return true;
+ }
+ return false;
+}
+
+VAR(desktopw, 1, 0, 0);
+VAR(desktoph, 1, 0, 0);
+int screenw = 0, screenh = 0;
+SDL_Window *screen = NULL;
+SDL_GLContext glcontext = NULL;
+
+#define SCR_MINW 320
+#define SCR_MINH 200
+#define SCR_MAXW 10000
+#define SCR_MAXH 10000
+#define SCR_DEFAULTW 1024
+#define SCR_DEFAULTH 768
+VARF(scr_w, SCR_MINW, -1, SCR_MAXW, initwarning("screen resolution"));
+VARF(scr_h, SCR_MINH, -1, SCR_MAXH, initwarning("screen resolution"));
+VARF(depthbits, 0, 0, 32, initwarning("depth-buffer precision"));
+VARF(fsaa, -1, -1, 16, initwarning("anti-aliasing"));
+
+void writeinitcfg()
+{
+ stream *f = openutf8file("init.cfg", "w");
+ if(!f) return;
+ f->printf("// automatically written on exit, DO NOT MODIFY\n// modify settings in game\n");
+ extern int fullscreen, fullscreendesktop;
+ f->printf("fullscreen %d\n", fullscreen);
+ f->printf("fullscreendesktop %d\n", fullscreendesktop);
+ f->printf("scr_w %d\n", scr_w);
+ f->printf("scr_h %d\n", scr_h);
+ f->printf("depthbits %d\n", depthbits);
+ f->printf("fsaa %d\n", fsaa);
+ extern int usesound, soundchans, soundfreq, soundbufferlen;
+ extern char *audiodriver;
+ f->printf("usesound %d\n", usesound);
+ f->printf("soundchans %d\n", soundchans);
+ f->printf("soundfreq %d\n", soundfreq);
+ f->printf("soundbufferlen %d\n", soundbufferlen);
+ if(audiodriver[0]) f->printf("audiodriver %s\n", escapestring(audiodriver));
+ delete f;
+}
+
+COMMAND(quit, "");
+
+static void getbackgroundres(int &w, int &h)
+{
+ float wk = 1, hk = 1;
+ if(w < 1024) wk = 1024.0f/w;
+ if(h < 768) hk = 768.0f/h;
+ wk = hk = max(wk, hk);
+ w = int(ceil(w*wk));
+ h = int(ceil(h*hk));
+}
+
+string backgroundcaption = "";
+Texture *backgroundmapshot = NULL;
+string backgroundmapname = "";
+char *backgroundmapinfo = NULL;
+
+void setbackgroundinfo(const char *caption = NULL, Texture *mapshot = NULL, const char *mapname = NULL, const char *mapinfo = NULL)
+{
+ renderedframe = false;
+ copystring(backgroundcaption, caption ? caption : "");
+ backgroundmapshot = mapshot;
+ copystring(backgroundmapname, mapname ? mapname : "");
+ if(mapinfo != backgroundmapinfo)
+ {
+ DELETEA(backgroundmapinfo);
+ if(mapinfo) backgroundmapinfo = newstring(mapinfo);
+ }
+}
+
+void restorebackground(bool force = false)
+{
+ if(renderedframe)
+ {
+ if(!force) return;
+ setbackgroundinfo();
+ }
+ renderbackground(backgroundcaption[0] ? backgroundcaption : NULL, backgroundmapshot, backgroundmapname[0] ? backgroundmapname : NULL, backgroundmapinfo, true);
+}
+
+void bgquad(float x, float y, float w, float h, float tx = 0, float ty = 0, float tw = 1, float th = 1)
+{
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attribf(x, y); gle::attribf(tx, ty);
+ gle::attribf(x+w, y); gle::attribf(tx + tw, ty);
+ gle::attribf(x, y+h); gle::attribf(tx, ty + th);
+ gle::attribf(x+w, y+h); gle::attribf(tx + tw, ty + th);
+ gle::end();
+}
+
+#include <sys/stat.h>
+
+static bool file_does_indeed_exist(const char *name) {
+ struct stat buffer;
+ return !stat(name, & buffer);
+}
+
+void renderbackground(const char *caption, Texture *mapshot, const char *mapname, const char *mapinfo, bool restore, bool force)
+{
+ if(!inbetweenframes && !force) return;
+
+ if(!restore || force) stopsounds(); // stop sounds while loading
+
+ int w = screenw, h = screenh;
+ if(forceaspect) w = int(ceil(h*forceaspect));
+ getbackgroundres(w, h);
+ gettextres(w, h);
+
+ static int lastupdate = -1, lastw = -1, lasth = -1;
+ static float backgroundu = 0, backgroundv = 0, detailu = 0, detailv = 0;
+ static int numdecals = 0;
+ static struct decal { float x, y, size; int side; } decals[12];
+ if((renderedframe && !mainmenu && lastupdate != lastmillis) || lastw != w || lasth != h)
+ {
+ lastupdate = lastmillis;
+ lastw = w;
+ lasth = h;
+
+ backgroundu = rndscale(1);
+ backgroundv = rndscale(1);
+ detailu = rndscale(1);
+ detailv = rndscale(1);
+ numdecals = sizeof(decals)/sizeof(decals[0]);
+ numdecals = numdecals/3 + rnd((numdecals*2)/3 + 1);
+ float maxsize = min(w, h)/16.0f;
+ loopi(numdecals)
+ {
+ decal d = { rndscale(w), rndscale(h), maxsize/2 + rndscale(maxsize/2), rnd(2) };
+ decals[i] = d;
+ }
+ }
+ else if(lastupdate != lastmillis) lastupdate = lastmillis;
+
+ //~if (mapname) {
+ //~defformatstring(backpath, "background/%s.png", mapname);
+ //~if (file_does_indeed_exist(backpath)) {
+ //~settexture(backpath, 0);
+ //~bgquad(0, 0, 1920, 1080, 0, 0, 1920, 1080);
+ //~gle::begin(GL_TRIANGLE_STRIP);
+ //~gle::attribf(0.0f, 0.0f); gle::attribf(0.0f, 0.0f);
+ //~gle::attribf(1.0f, 0.0f); gle::attribf(1920.0f, 0.0f);
+ //~gle::attribf(0.0f, 1.0f); gle::attribf(0.0f, 1.0f);
+ //~gle::attribf(1.0f, 1.0f); gle::attribf(1920.0f, 1.0f);
+ //~gle::end();
+ //~}
+ //~} else {
+ loopi(restore ? 1 : 3)
+ {
+ hudmatrix.ortho(0, w, h, 0, -1, 1);
+ resethudmatrix();
+
+ hudshader->set();
+ gle::colorf(1, 1, 1);
+
+ gle::defvertex(2);
+ gle::deftexcoord0();
+
+ //~defformatstring(backpath, "background/%s.png", mapname);
+ //~if (file_does_indeed_exist(backpath)) {
+ //~settexture(backpath, 0);
+ settexture("background/daemex.png", 0);
+ bgquad(0, 0, screenw, screenh, 0, 0, 1, 1);
+ //~settexture("data/background.png", 0);
+ //~float bu = w*0.67f/256.0f + backgroundu, bv = h*0.67f/256.0f + backgroundv;
+ //~bgquad(0, 0, w, h, 0, 0, bu, bv);
+ //~glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glEnable(GL_BLEND);
+ //~settexture("data/background_detail.png", 0);
+ //~float du = w*0.8f/512.0f + detailu, dv = h*0.8f/512.0f + detailv;
+ //~bgquad(0, 0, w, h, 0, 0, du, dv);
+ //~settexture("data/background_decal.png", 3);
+ //~gle::begin(GL_QUADS);
+ //~loopj(numdecals)
+ //~{
+ //~float hsz = decals[j].size, hx = clamp(decals[j].x, hsz, w-hsz), hy = clamp(decals[j].y, hsz, h-hsz), side = decals[j].side;
+ //~gle::attribf(hx-hsz, hy-hsz); gle::attribf(side, 0);
+ //~gle::attribf(hx+hsz, hy-hsz); gle::attribf(1-side, 0);
+ //~gle::attribf(hx+hsz, hy+hsz); gle::attribf(1-side, 1);
+ //~gle::attribf(hx-hsz, hy+hsz); gle::attribf(side, 1);
+ //~}
+ //~gle::end();
+ float lh = 0.5f*min(w, h), lw = lh*2,
+ lx = 0.5f*(w - lw), ly = 0.5f*(h*0.5f - lh);
+ settexture((maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize) >= 1024 && (screenw > 1280 || screenh > 800) ? "data/logo_1024.png" : "data/logo.png", 3);
+ bgquad(lx, ly, lw, lh);
+ if(caption)
+ {
+ int tw = text_width(caption);
+ float tsz = 0.04f*min(w, h)/FONTH,
+ tx = 0.5f*(w - tw*tsz), ty = h - 0.075f*1.5f*min(w, h) - 1.25f*FONTH*tsz;
+ pushhudmatrix();
+ hudmatrix.translate(tx, ty, 0);
+ hudmatrix.scale(tsz, tsz, 1);
+ flushhudmatrix();
+ draw_text(caption, 0, 0);
+ pophudmatrix();
+ }
+ if(mapshot || mapname)
+ {
+ int infowidth = 12*FONTH;
+ float sz = 0.35f*min(w, h), msz = (0.75f*min(w, h) - sz)/(infowidth + FONTH), x = 0.5f*(w-sz), y = ly+lh - sz/15;
+ if(mapinfo)
+ {
+ int mw, mh;
+ text_bounds(mapinfo, mw, mh, infowidth);
+ x -= 0.5f*(mw*msz + FONTH*msz);
+ }
+ if(mapshot && mapshot!=notexture)
+ {
+ glBindTexture(GL_TEXTURE_2D, mapshot->id);
+ bgquad(x, y, sz, sz);
+ }
+ else
+ {
+ int qw, qh;
+ text_bounds("?", qw, qh);
+ float qsz = sz*0.5f/max(qw, qh);
+ pushhudmatrix();
+ hudmatrix.translate(x + 0.5f*(sz - qw*qsz), y + 0.5f*(sz - qh*qsz), 0);
+ hudmatrix.scale(qsz, qsz, 1);
+ flushhudmatrix();
+ draw_text("?", 0, 0);
+ pophudmatrix();
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+ settexture("data/mapshot_frame.png", 3);
+ bgquad(x, y, sz, sz);
+ if(mapname)
+ {
+ int tw = text_width(mapname);
+ float tsz = sz/(8*FONTH),
+ tx = 0.9f*sz - tw*tsz, ty = 0.9f*sz - FONTH*tsz;
+ if(tx < 0.1f*sz) { tsz = 0.1f*sz/tw; tx = 0.1f; }
+ pushhudmatrix();
+ hudmatrix.translate(x+tx, y+ty, 0);
+ hudmatrix.scale(tsz, tsz, 1);
+ flushhudmatrix();
+ draw_text(mapname, 0, 0);
+ pophudmatrix();
+ }
+ if(mapinfo)
+ {
+ pushhudmatrix();
+ hudmatrix.translate(x+sz+FONTH*msz, y, 0);
+ hudmatrix.scale(msz, msz, 1);
+ flushhudmatrix();
+ draw_text(mapinfo, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, -1, infowidth);
+ pophudmatrix();
+ }
+ }
+ glDisable(GL_BLEND);
+ if(!restore) swapbuffers(false);
+ }
+ //~}
+
+ if(!restore) setbackgroundinfo(caption, mapshot, mapname, mapinfo);
+}
+
+VAR(progressbackground, 0, 0, 1);
+
+float loadprogress = 0;
+
+void renderprogress(float bar, const char *text, GLuint tex, bool background) // also used during loading
+{
+ if(!inbetweenframes || drawtex) return;
+
+ extern int menufps, maxfps;
+ int fps = menufps ? (maxfps ? min(maxfps, menufps) : menufps) : maxfps;
+ if(fps)
+ {
+ static int lastprogress = 0;
+ int ticks = SDL_GetTicks(), diff = ticks - lastprogress;
+ if(bar > 0 && diff >= 0 && diff < (1000 + fps-1)/fps) return;
+ lastprogress = ticks;
+ }
+
+ clientkeepalive(); // make sure our connection doesn't time out while loading maps etc.
+
+ SDL_PumpEvents(); // keep the event queue awake to avoid 'beachball' cursor
+
+ extern int mesa_swap_bug, curvsync;
+ bool forcebackground = progressbackground || (mesa_swap_bug && (curvsync || totalmillis==1));
+ if(background || forcebackground) restorebackground(forcebackground);
+
+ int w = screenw, h = screenh;
+ if(forceaspect) w = int(ceil(h*forceaspect));
+ getbackgroundres(w, h);
+ gettextres(w, h);
+
+ hudmatrix.ortho(0, w, h, 0, -1, 1);
+ resethudmatrix();
+
+ hudshader->set();
+ gle::colorf(1, 1, 1);
+
+ gle::defvertex(2);
+ gle::deftexcoord0();
+
+ float fh = 0.075f*min(w, h), fw = fh*10,
+ fx = renderedframe ? w - fw - fh/4 : 0.5f*(w - fw),
+ fy = renderedframe ? fh/4 : h - fh*1.5f,
+ fu1 = 0/512.0f, fu2 = 511/512.0f,
+ fv1 = 0/64.0f, fv2 = 52/64.0f;
+ settexture("data/loading_frame.png", 3);
+ bgquad(fx, fy, fw, fh, fu1, fv1, fu2-fu1, fv2-fv1);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ float bw = fw*(511 - 2*17)/511.0f, bh = fh*20/52.0f,
+ bx = fx + fw*17/511.0f, by = fy + fh*16/52.0f,
+ bv1 = 0/32.0f, bv2 = 20/32.0f,
+ su1 = 0/32.0f, su2 = 7/32.0f, sw = fw*7/511.0f,
+ eu1 = 23/32.0f, eu2 = 30/32.0f, ew = fw*7/511.0f,
+ mw = bw - sw - ew,
+ ex = bx+sw + max(mw*bar, fw*7/511.0f);
+ if(bar > 0)
+ {
+ settexture("data/loading_bar.png", 3);
+ gle::begin(GL_QUADS);
+ gle::attribf(bx, by); gle::attribf(su1, bv1);
+ gle::attribf(bx+sw, by); gle::attribf(su2, bv1);
+ gle::attribf(bx+sw, by+bh); gle::attribf(su2, bv2);
+ gle::attribf(bx, by+bh); gle::attribf(su1, bv2);
+
+ gle::attribf(bx+sw, by); gle::attribf(su2, bv1);
+ gle::attribf(ex, by); gle::attribf(eu1, bv1);
+ gle::attribf(ex, by+bh); gle::attribf(eu1, bv2);
+ gle::attribf(bx+sw, by+bh); gle::attribf(su2, bv2);
+
+ gle::attribf(ex, by); gle::attribf(eu1, bv1);
+ gle::attribf(ex+ew, by); gle::attribf(eu2, bv1);
+ gle::attribf(ex+ew, by+bh); gle::attribf(eu2, bv2);
+ gle::attribf(ex, by+bh); gle::attribf(eu1, bv2);
+ gle::end();
+ }
+
+ if(text)
+ {
+ int tw = text_width(text);
+ float tsz = bh*0.8f/FONTH;
+ if(tw*tsz > mw) tsz = mw/tw;
+ pushhudmatrix();
+ hudmatrix.translate(bx+sw, by + (bh - FONTH*tsz)/2, 0);
+ hudmatrix.scale(tsz, tsz, 1);
+ flushhudmatrix();
+ draw_text(text, 0, 0);
+ pophudmatrix();
+ }
+
+ glDisable(GL_BLEND);
+
+ if(tex)
+ {
+ glBindTexture(GL_TEXTURE_2D, tex);
+ float sz = 0.35f*min(w, h), x = 0.5f*(w-sz), y = 0.5f*min(w, h) - sz/15;
+ bgquad(x, y, sz, sz);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ settexture("data/mapshot_frame.png", 3);
+ bgquad(x, y, sz, sz);
+ glDisable(GL_BLEND);
+ }
+
+ swapbuffers(false);
+}
+
+int keyrepeatmask = 0, textinputmask = 0;
+Uint32 textinputtime = 0;
+VAR(textinputfilter, 0, 5, 1000);
+
+void keyrepeat(bool on, int mask)
+{
+ if(on) keyrepeatmask |= mask;
+ else keyrepeatmask &= ~mask;
+}
+
+void textinput(bool on, int mask)
+{
+ if(on)
+ {
+ if(!textinputmask)
+ {
+ SDL_StartTextInput();
+ textinputtime = SDL_GetTicks();
+ }
+ textinputmask |= mask;
+ }
+ else if(textinputmask)
+ {
+ textinputmask &= ~mask;
+ if(!textinputmask) SDL_StopTextInput();
+ }
+}
+
+#ifdef WIN32
+// SDL_WarpMouseInWindow behaves erratically on Windows, so force relative mouse instead.
+VARN(relativemouse, userelativemouse, 1, 1, 0);
+#else
+VARNP(relativemouse, userelativemouse, 0, 1, 1);
+#endif
+
+bool shouldgrab = false, grabinput = false, minimized = false, canrelativemouse = true, relativemouse = false;
+
+#ifdef SDL_VIDEO_DRIVER_X11
+VAR(sdl_xgrab_bug, 0, 0, 1);
+#endif
+
+void inputgrab(bool on, bool delay = false)
+{
+#ifdef SDL_VIDEO_DRIVER_X11
+ bool wasrelativemouse = relativemouse;
+#endif
+ if(on)
+ {
+ SDL_ShowCursor(SDL_FALSE);
+ if(canrelativemouse && userelativemouse)
+ {
+ if(SDL_SetRelativeMouseMode(SDL_TRUE) >= 0)
+ {
+ SDL_SetWindowGrab(screen, SDL_TRUE);
+ relativemouse = true;
+ }
+ else
+ {
+ SDL_SetWindowGrab(screen, SDL_FALSE);
+ canrelativemouse = false;
+ relativemouse = false;
+ }
+ }
+ }
+ else
+ {
+ SDL_ShowCursor(SDL_TRUE);
+ if(relativemouse)
+ {
+ SDL_SetWindowGrab(screen, SDL_FALSE);
+ SDL_SetRelativeMouseMode(SDL_FALSE);
+ relativemouse = false;
+ }
+ }
+ shouldgrab = delay;
+
+#ifdef SDL_VIDEO_DRIVER_X11
+ if((relativemouse || wasrelativemouse) && sdl_xgrab_bug)
+ {
+ // Workaround for buggy SDL X11 pointer grabbing
+ union { SDL_SysWMinfo info; uchar buf[sizeof(SDL_SysWMinfo) + 128]; };
+ SDL_GetVersion(&info.version);
+ if(SDL_GetWindowWMInfo(screen, &info) && info.subsystem == SDL_SYSWM_X11)
+ {
+ if(relativemouse)
+ {
+ uint mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask;
+ XGrabPointer(info.info.x11.display, info.info.x11.window, True, mask, GrabModeAsync, GrabModeAsync, info.info.x11.window, None, CurrentTime);
+ }
+ else XUngrabPointer(info.info.x11.display, CurrentTime);
+ }
+ }
+#endif
+}
+
+bool initwindowpos = false;
+
+void setfullscreen(bool enable)
+{
+ if(!screen) return;
+ //initwarning(enable ? "fullscreen" : "windowed");
+ extern int fullscreendesktop;
+ SDL_SetWindowFullscreen(screen, enable ? (fullscreendesktop ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN) : 0);
+ if(!enable)
+ {
+ SDL_SetWindowSize(screen, scr_w, scr_h);
+ if(initwindowpos)
+ {
+ int winx = SDL_WINDOWPOS_CENTERED, winy = SDL_WINDOWPOS_CENTERED;
+ SDL_SetWindowPosition(screen, winx, winy);
+ initwindowpos = false;
+ }
+ }
+}
+
+#ifdef _DEBUG
+VARF(fullscreen, 0, 0, 1, setfullscreen(fullscreen!=0));
+#else
+VARF(fullscreen, 0, 1, 1, setfullscreen(fullscreen!=0));
+#endif
+
+void resetfullscreen()
+{
+ setfullscreen(false);
+ setfullscreen(true);
+}
+
+VARF(fullscreendesktop, 0, 0, 1, if(fullscreen) resetfullscreen());
+
+void screenres(int w, int h)
+{
+ scr_w = clamp(w, SCR_MINW, SCR_MAXW);
+ scr_h = clamp(h, SCR_MINH, SCR_MAXH);
+ if(screen)
+ {
+ if(fullscreendesktop)
+ {
+ scr_w = min(scr_w, desktopw);
+ scr_h = min(scr_h, desktoph);
+ }
+ if(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN)
+ {
+ if(fullscreendesktop) gl_resize();
+ else resetfullscreen();
+ initwindowpos = true;
+ }
+ else
+ {
+ SDL_SetWindowSize(screen, scr_w, scr_h);
+ SDL_SetWindowPosition(screen, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
+ initwindowpos = false;
+ }
+ }
+ else
+ {
+ initwarning("screen resolution");
+ }
+}
+
+ICOMMAND(screenres, "ii", (int *w, int *h), screenres(*w, *h));
+
+static void setgamma(int val)
+{
+ if(screen && SDL_SetWindowBrightness(screen, val/100.0f) < 0) conoutf(CON_ERROR, "Could not set gamma: %s", SDL_GetError());
+}
+
+static int curgamma = 100;
+VARFNP(gamma, reqgamma, 30, 100, 300,
+{
+ if(initing || reqgamma == curgamma) return;
+ curgamma = reqgamma;
+ setgamma(curgamma);
+});
+
+void restoregamma()
+{
+ if(initing || reqgamma == 100) return;
+ curgamma = reqgamma;
+ setgamma(curgamma);
+}
+
+void cleargamma()
+{
+ if(curgamma != 100 && screen) SDL_SetWindowBrightness(screen, 1.0f);
+}
+
+int curvsync = -1;
+void restorevsync()
+{
+ if(initing || !glcontext) return;
+ extern int vsync, vsynctear;
+ if(!SDL_GL_SetSwapInterval(vsync ? (vsynctear ? -1 : 1) : 0))
+ curvsync = vsync;
+}
+
+VARFP(vsync, 0, 0, 1, restorevsync());
+VARFP(vsynctear, 0, 0, 1, { if(vsync) restorevsync(); });
+
+void setupscreen()
+{
+ if(glcontext)
+ {
+ SDL_GL_DeleteContext(glcontext);
+ glcontext = NULL;
+ }
+ if(screen)
+ {
+ SDL_DestroyWindow(screen);
+ screen = NULL;
+ }
+ curvsync = -1;
+
+ SDL_Rect desktop;
+ if(SDL_GetDisplayBounds(0, &desktop) < 0) fatal("failed querying desktop bounds: %s", SDL_GetError());
+ desktopw = desktop.w;
+ desktoph = desktop.h;
+
+ if(scr_h < 0) scr_h = fullscreen ? desktoph : SCR_DEFAULTH;
+ if(scr_w < 0) scr_w = (scr_h*desktopw)/desktoph;
+ scr_w = clamp(scr_w, SCR_MINW, SCR_MAXW);
+ scr_h = clamp(scr_h, SCR_MINH, SCR_MAXH);
+ if(fullscreendesktop)
+ {
+ scr_w = min(scr_w, desktopw);
+ scr_h = min(scr_h, desktoph);
+ }
+
+ int winx = SDL_WINDOWPOS_UNDEFINED, winy = SDL_WINDOWPOS_UNDEFINED, winw = scr_w, winh = scr_h, flags = SDL_WINDOW_RESIZABLE;
+ if(fullscreen)
+ {
+ if(fullscreendesktop)
+ {
+ winw = desktopw;
+ winh = desktoph;
+ flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
+ }
+ else flags |= SDL_WINDOW_FULLSCREEN;
+ initwindowpos = true;
+ }
+
+ SDL_GL_ResetAttributes();
+ SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+ #if !defined(WIN32) && !defined(__APPLE__)
+ SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
+ SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
+ #endif
+ static const int configs[] =
+ {
+ 0x3, /* try everything */
+ 0x2, 0x1, /* try disabling one at a time */
+ 0 /* try disabling everything */
+ };
+ int config = 0;
+ if(!depthbits) SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
+ if(!fsaa)
+ {
+ SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
+ SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 0);
+ }
+ loopi(sizeof(configs)/sizeof(configs[0]))
+ {
+ config = configs[i];
+ if(!depthbits && config&1) continue;
+ if(fsaa<=0 && config&2) continue;
+ if(depthbits) SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, config&1 ? depthbits : 24);
+ if(fsaa>0)
+ {
+ SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, config&2 ? 1 : 0);
+ SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, config&2 ? fsaa : 0);
+ }
+ screen = SDL_CreateWindow("Cube 2: Sauerbraten", winx, winy, winw, winh, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS | flags);
+ if(!screen) continue;
+
+ #ifdef __APPLE__
+ static const int glversions[] = { 32, 20 };
+ #else
+ static const int glversions[] = { 33, 32, 31, 30, 20 };
+ #endif
+ loopj(sizeof(glversions)/sizeof(glversions[0]))
+ {
+ glcompat = glversions[j] <= 30 ? 1 : 0;
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, glversions[j] / 10);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, glversions[j] % 10);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, glversions[j] >= 32 ? SDL_GL_CONTEXT_PROFILE_CORE : 0);
+ glcontext = SDL_GL_CreateContext(screen);
+ if(glcontext) break;
+ }
+ if(glcontext) break;
+ }
+ if(!screen) fatal("failed to create OpenGL window: %s", SDL_GetError());
+ else if(!glcontext) fatal("failed to create OpenGL context: %s", SDL_GetError());
+ else
+ {
+ if(depthbits && (config&1)==0) conoutf(CON_WARN, "%d bit z-buffer not supported - disabling", depthbits);
+ if(fsaa>0 && (config&2)==0) conoutf(CON_WARN, "%dx anti-aliasing not supported - disabling", fsaa);
+ }
+
+ SDL_SetWindowMinimumSize(screen, SCR_MINW, SCR_MINH);
+ SDL_SetWindowMaximumSize(screen, SCR_MAXW, SCR_MAXH);
+
+ SDL_GetWindowSize(screen, &screenw, &screenh);
+}
+
+void resetgl()
+{
+ clearchanges(CHANGE_GFX);
+
+ renderbackground("resetting OpenGL");
+
+ extern void cleanupva();
+ extern void cleanupparticles();
+ extern void cleanupdecals();
+ extern void cleanupblobs();
+ extern void cleanupsky();
+ extern void cleanupmodels();
+ extern void cleanupprefabs();
+ extern void cleanuplightmaps();
+ extern void cleanupblendmap();
+ extern void cleanshadowmap();
+ extern void cleanreflections();
+ extern void cleanupglare();
+ extern void cleanupdepthfx();
+ recorder::cleanup();
+ cleanupva();
+ cleanupparticles();
+ cleanupdecals();
+ cleanupblobs();
+ cleanupsky();
+ cleanupmodels();
+ cleanupprefabs();
+ cleanuptextures();
+ cleanuplightmaps();
+ cleanupblendmap();
+ cleanshadowmap();
+ cleanreflections();
+ cleanupglare();
+ cleanupdepthfx();
+ cleanupshaders();
+ cleanupgl();
+
+ setupscreen();
+ inputgrab(grabinput);
+ gl_init();
+
+ inbetweenframes = false;
+ if(!reloadtexture(*notexture) ||
+ !reloadtexture("data/logo.png") ||
+ !reloadtexture("data/logo_1024.png") ||
+ !reloadtexture("data/background.png") ||
+ !reloadtexture("data/background_detail.png") ||
+ !reloadtexture("data/background_decal.png") ||
+ !reloadtexture("data/mapshot_frame.png") ||
+ !reloadtexture("data/loading_frame.png") ||
+ !reloadtexture("data/loading_bar.png"))
+ fatal("failed to reload core texture");
+ reloadfonts();
+ inbetweenframes = true;
+ renderbackground("initializing...");
+ restoregamma();
+ restorevsync();
+ reloadshaders();
+ reloadtextures();
+ initlights();
+ allchanged(true);
+}
+
+COMMAND(resetgl, "");
+
+static queue<SDL_Event, 32> events;
+
+static inline bool filterevent(const SDL_Event &event)
+{
+ switch(event.type)
+ {
+ case SDL_MOUSEMOTION:
+ if(grabinput && !relativemouse && !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN))
+ {
+ if(event.motion.x == screenw / 2 && event.motion.y == screenh / 2)
+ return false; // ignore any motion events generated by SDL_WarpMouse
+ #ifdef __APPLE__
+ if(event.motion.y == 0)
+ return false; // let mac users drag windows via the title bar
+ #endif
+ }
+ break;
+ }
+ return true;
+}
+
+template <int SIZE> static inline bool pumpevents(queue<SDL_Event, SIZE> &events)
+{
+ while(events.empty())
+ {
+ SDL_PumpEvents();
+ databuf<SDL_Event> buf = events.reserve(events.capacity());
+ int n = SDL_PeepEvents(buf.getbuf(), buf.remaining(), SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT);
+ if(n <= 0) return false;
+ loopi(n) if(filterevent(buf.buf[i])) buf.put(buf.buf[i]);
+ events.addbuf(buf);
+ }
+ return true;
+}
+
+static int interceptkeysym = 0;
+
+static int interceptevents(void *data, SDL_Event *event)
+{
+ switch(event->type)
+ {
+ case SDL_MOUSEMOTION: return 0;
+ case SDL_KEYDOWN:
+ if(event->key.keysym.sym == interceptkeysym)
+ {
+ interceptkeysym = -interceptkeysym;
+ return 0;
+ }
+ break;
+ }
+ return 1;
+}
+
+static void clearinterceptkey()
+{
+ SDL_DelEventWatch(interceptevents, NULL);
+ interceptkeysym = 0;
+}
+
+bool interceptkey(int sym)
+{
+ if(!interceptkeysym)
+ {
+ interceptkeysym = sym;
+ SDL_FilterEvents(interceptevents, NULL);
+ if(interceptkeysym < 0)
+ {
+ interceptkeysym = 0;
+ return true;
+ }
+ SDL_AddEventWatch(interceptevents, NULL);
+ }
+ else if(abs(interceptkeysym) != sym) interceptkeysym = sym;
+ SDL_PumpEvents();
+ if(interceptkeysym < 0)
+ {
+ clearinterceptkey();
+ interceptkeysym = sym;
+ SDL_FilterEvents(interceptevents, NULL);
+ interceptkeysym = 0;
+ return true;
+ }
+ return false;
+}
+
+static void ignoremousemotion()
+{
+ SDL_PumpEvents();
+ SDL_FlushEvent(SDL_MOUSEMOTION);
+}
+
+static void resetmousemotion()
+{
+ if(grabinput && !relativemouse && !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN))
+ {
+ SDL_WarpMouseInWindow(screen, screenw / 2, screenh / 2);
+ }
+}
+
+static void checkmousemotion(int &dx, int &dy)
+{
+ while(pumpevents(events))
+ {
+ SDL_Event &event = events.removing();
+ if(event.type != SDL_MOUSEMOTION) return;
+ dx += event.motion.xrel;
+ dy += event.motion.yrel;
+ events.remove();
+ }
+}
+
+void checkinput()
+{
+ if(interceptkeysym) clearinterceptkey();
+ //int lasttype = 0, lastbut = 0;
+ bool mousemoved = false;
+ int focused = 0;
+ while(pumpevents(events))
+ {
+ SDL_Event &event = events.remove();
+
+ if(focused && event.type!=SDL_WINDOWEVENT) { if(grabinput != (focused>0)) inputgrab(grabinput = focused>0, shouldgrab); focused = 0; }
+
+ switch(event.type)
+ {
+ case SDL_QUIT:
+ quit();
+ return;
+
+ case SDL_TEXTINPUT:
+ if(textinputmask && int(event.text.timestamp-textinputtime) >= textinputfilter)
+ {
+ uchar buf[SDL_TEXTINPUTEVENT_TEXT_SIZE+1];
+ size_t len = decodeutf8(buf, sizeof(buf)-1, (const uchar *)event.text.text, strlen(event.text.text));
+ if(len > 0) { buf[len] = '\0'; processtextinput((const char *)buf, len); }
+ }
+ break;
+
+ case SDL_KEYDOWN:
+ case SDL_KEYUP:
+ if(keyrepeatmask || !event.key.repeat)
+ processkey(event.key.keysym.sym, event.key.state==SDL_PRESSED, event.key.keysym.mod | SDL_GetModState());
+ break;
+
+ case SDL_WINDOWEVENT:
+ switch(event.window.event)
+ {
+ case SDL_WINDOWEVENT_CLOSE:
+ quit();
+ break;
+
+ case SDL_WINDOWEVENT_FOCUS_GAINED:
+ shouldgrab = true;
+ break;
+ case SDL_WINDOWEVENT_ENTER:
+ shouldgrab = false;
+ focused = 1;
+ break;
+
+ case SDL_WINDOWEVENT_LEAVE:
+ case SDL_WINDOWEVENT_FOCUS_LOST:
+ shouldgrab = false;
+ focused = -1;
+ break;
+
+ case SDL_WINDOWEVENT_MINIMIZED:
+ minimized = true;
+ break;
+
+ case SDL_WINDOWEVENT_MAXIMIZED:
+ case SDL_WINDOWEVENT_RESTORED:
+ minimized = false;
+ break;
+
+ case SDL_WINDOWEVENT_RESIZED:
+ break;
+
+ case SDL_WINDOWEVENT_SIZE_CHANGED:
+ {
+ SDL_GetWindowSize(screen, &screenw, &screenh);
+ if(!fullscreendesktop || !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN))
+ {
+ scr_w = clamp(screenw, SCR_MINW, SCR_MAXW);
+ scr_h = clamp(screenh, SCR_MINH, SCR_MAXH);
+ }
+ gl_resize();
+ break;
+ }
+ }
+ break;
+
+ case SDL_MOUSEMOTION:
+ if(grabinput)
+ {
+ int dx = event.motion.xrel, dy = event.motion.yrel;
+ checkmousemotion(dx, dy);
+ if(!g3d_movecursor(dx, dy)) mousemove(dx, dy);
+ mousemoved = true;
+ }
+ else if(shouldgrab) inputgrab(grabinput = true);
+ break;
+
+ case SDL_MOUSEBUTTONDOWN:
+ case SDL_MOUSEBUTTONUP:
+ //if(lasttype==event.type && lastbut==event.button.button) break; // why?? get event twice without it
+ switch(event.button.button)
+ {
+ case SDL_BUTTON_LEFT: processkey(-1, event.button.state==SDL_PRESSED); break;
+ case SDL_BUTTON_MIDDLE: processkey(-2, event.button.state==SDL_PRESSED); break;
+ case SDL_BUTTON_RIGHT: processkey(-3, event.button.state==SDL_PRESSED); break;
+ case SDL_BUTTON_X1: processkey(-6, event.button.state==SDL_PRESSED); break;
+ case SDL_BUTTON_X2: processkey(-7, event.button.state==SDL_PRESSED); break;
+ }
+ //lasttype = event.type;
+ //lastbut = event.button.button;
+ break;
+
+ case SDL_MOUSEWHEEL:
+ if(event.wheel.y > 0) { processkey(-4, true); processkey(-4, false); }
+ else if(event.wheel.y < 0) { processkey(-5, true); processkey(-5, false); }
+ break;
+ }
+ }
+ if(focused) { if(grabinput != (focused>0)) inputgrab(grabinput = focused>0, shouldgrab); focused = 0; }
+ if(mousemoved) resetmousemotion();
+}
+
+void swapbuffers(bool overlay)
+{
+ recorder::capture(overlay);
+ gle::disable();
+ SDL_GL_SwapWindow(screen);
+}
+
+VAR(menufps, 0, 60, 1000);
+VARP(maxfps, 0, 200, 1000);
+
+void limitfps(int &millis, int curmillis)
+{
+ int limit = (mainmenu || minimized) && menufps ? (maxfps ? min(maxfps, menufps) : menufps) : maxfps;
+ if(!limit) return;
+ static int fpserror = 0;
+ int delay = 1000/limit - (millis-curmillis);
+ if(delay < 0) fpserror = 0;
+ else
+ {
+ fpserror += 1000%limit;
+ if(fpserror >= limit)
+ {
+ ++delay;
+ fpserror -= limit;
+ }
+ if(delay > 0)
+ {
+ SDL_Delay(delay);
+ millis += delay;
+ }
+ }
+}
+
+#if defined(WIN32) && !defined(_DEBUG) && !defined(__GNUC__)
+void stackdumper(unsigned int type, EXCEPTION_POINTERS *ep)
+{
+ if(!ep) fatal("unknown type");
+ EXCEPTION_RECORD *er = ep->ExceptionRecord;
+ CONTEXT *context = ep->ContextRecord;
+ char out[512];
+ formatstring(out, "Cube 2: Sauerbraten Win32 Exception: 0x%x [0x%x]\n\n", er->ExceptionCode, er->ExceptionCode==EXCEPTION_ACCESS_VIOLATION ? er->ExceptionInformation[1] : -1);
+ SymInitialize(GetCurrentProcess(), NULL, TRUE);
+#ifdef _AMD64_
+ STACKFRAME64 sf = {{context->Rip, 0, AddrModeFlat}, {}, {context->Rbp, 0, AddrModeFlat}, {context->Rsp, 0, AddrModeFlat}, 0};
+ while(::StackWalk64(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(), GetCurrentThread(), &sf, context, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL))
+ {
+ union { IMAGEHLP_SYMBOL64 sym; char symext[sizeof(IMAGEHLP_SYMBOL64) + sizeof(string)]; };
+ sym.SizeOfStruct = sizeof(sym);
+ sym.MaxNameLength = sizeof(symext) - sizeof(sym);
+ IMAGEHLP_LINE64 line;
+ line.SizeOfStruct = sizeof(line);
+ DWORD64 symoff;
+ DWORD lineoff;
+ if(SymGetSymFromAddr64(GetCurrentProcess(), sf.AddrPC.Offset, &symoff, &sym) && SymGetLineFromAddr64(GetCurrentProcess(), sf.AddrPC.Offset, &lineoff, &line))
+#else
+ STACKFRAME sf = {{context->Eip, 0, AddrModeFlat}, {}, {context->Ebp, 0, AddrModeFlat}, {context->Esp, 0, AddrModeFlat}, 0};
+ while(::StackWalk(IMAGE_FILE_MACHINE_I386, GetCurrentProcess(), GetCurrentThread(), &sf, context, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL))
+ {
+ union { IMAGEHLP_SYMBOL sym; char symext[sizeof(IMAGEHLP_SYMBOL) + sizeof(string)]; };
+ sym.SizeOfStruct = sizeof(sym);
+ sym.MaxNameLength = sizeof(symext) - sizeof(sym);
+ IMAGEHLP_LINE line;
+ line.SizeOfStruct = sizeof(line);
+ DWORD symoff, lineoff;
+ if(SymGetSymFromAddr(GetCurrentProcess(), sf.AddrPC.Offset, &symoff, &sym) && SymGetLineFromAddr(GetCurrentProcess(), sf.AddrPC.Offset, &lineoff, &line))
+#endif
+ {
+ char *del = strrchr(line.FileName, '\\');
+ concformatstring(out, "%s - %s [%d]\n", sym.Name, del ? del + 1 : line.FileName, line.LineNumber);
+ }
+ }
+ fatal(out);
+}
+#endif
+
+#define MAXFPSHISTORY 60
+
+int fpspos = 0, fpshistory[MAXFPSHISTORY];
+
+void resetfpshistory()
+{
+ loopi(MAXFPSHISTORY) fpshistory[i] = 1;
+ fpspos = 0;
+}
+
+void updatefpshistory(int millis)
+{
+ fpshistory[fpspos++] = max(1, min(1000, millis));
+ if(fpspos>=MAXFPSHISTORY) fpspos = 0;
+}
+
+void getfps(int &fps, int &bestdiff, int &worstdiff)
+{
+ int total = fpshistory[MAXFPSHISTORY-1], best = total, worst = total;
+ loopi(MAXFPSHISTORY-1)
+ {
+ int millis = fpshistory[i];
+ total += millis;
+ if(millis < best) best = millis;
+ if(millis > worst) worst = millis;
+ }
+
+ fps = (1000*MAXFPSHISTORY)/total;
+ bestdiff = 1000/best-fps;
+ worstdiff = fps-1000/worst;
+}
+
+void getfps_(int *raw)
+{
+ int fps, bestdiff, worstdiff;
+ if(*raw) fps = 1000/fpshistory[(fpspos+MAXFPSHISTORY-1)%MAXFPSHISTORY];
+ else getfps(fps, bestdiff, worstdiff);
+ intret(fps);
+}
+
+COMMANDN(getfps, getfps_, "i");
+
+bool inbetweenframes = false, renderedframe = true;
+
+static bool findarg(int argc, char **argv, const char *str)
+{
+ for(int i = 1; i<argc; i++) if(strstr(argv[i], str)==argv[i]) return true;
+ return false;
+}
+
+static int clockrealbase = 0, clockvirtbase = 0;
+static void clockreset() { clockrealbase = SDL_GetTicks(); clockvirtbase = totalmillis; }
+VARFP(clockerror, 990000, 1000000, 1010000, clockreset());
+VARFP(clockfix, 0, 0, 1, clockreset());
+
+int getclockmillis()
+{
+ int millis = SDL_GetTicks() - clockrealbase;
+ if(clockfix) millis = int(millis*(double(clockerror)/1000000));
+ millis += clockvirtbase;
+ return max(millis, totalmillis);
+}
+
+VAR(numcpus, 1, 1, 16);
+
+int main(int argc, char **argv)
+{
+ #ifdef WIN32
+ //atexit((void (__cdecl *)(void))_CrtDumpMemoryLeaks);
+ #ifndef _DEBUG
+ #ifndef __GNUC__
+ __try {
+ #endif
+ #endif
+ #endif
+
+ setlogfile(NULL);
+
+ int dedicated = 0;
+ char *load = NULL, *initscript = NULL;
+
+ initing = INIT_RESET;
+ // set home dir first
+ for(int i = 1; i<argc; i++) if(argv[i][0]=='-' && argv[i][1] == 'q') { sethomedir(&argv[i][2]); break; }
+ // set log after home dir, but before anything else
+ for(int i = 1; i<argc; i++) if(argv[i][0]=='-' && argv[i][1] == 'g')
+ {
+ const char *file = argv[i][2] ? &argv[i][2] : "log.txt";
+ setlogfile(file);
+ logoutf("Setting log file: %s", file);
+ break;
+ }
+ execfile("init.cfg", false);
+ for(int i = 1; i<argc; i++)
+ {
+ if(argv[i][0]=='-') switch(argv[i][1])
+ {
+ case 'q': if(homedir[0]) logoutf("Using home directory: %s", homedir); break;
+ case 'r': /* compat, ignore */ break;
+ case 'k':
+ {
+ const char *dir = addpackagedir(&argv[i][2]);
+ if(dir) logoutf("Adding package directory: %s", dir);
+ break;
+ }
+ case 'g': break;
+ case 'd': dedicated = atoi(&argv[i][2]); if(dedicated<=0) dedicated = 2; break;
+ case 'w': scr_w = clamp(atoi(&argv[i][2]), SCR_MINW, SCR_MAXW); if(!findarg(argc, argv, "-h")) scr_h = -1; break;
+ case 'h': scr_h = clamp(atoi(&argv[i][2]), SCR_MINH, SCR_MAXH); if(!findarg(argc, argv, "-w")) scr_w = -1; break;
+ case 'z': depthbits = atoi(&argv[i][2]); break;
+ case 'b': /* compat, ignore */ break;
+ case 'a': fsaa = atoi(&argv[i][2]); break;
+ case 'v': /* compat, ignore */ break;
+ case 't': fullscreen = atoi(&argv[i][2]); break;
+ case 's': /* compat, ignore */ break;
+ case 'f': /* compat, ignore */ break;
+ case 'l':
+ {
+ char pkgdir[] = "packages/";
+ load = strstr(path(&argv[i][2]), path(pkgdir));
+ if(load) load += sizeof(pkgdir)-1;
+ else load = &argv[i][2];
+ break;
+ }
+ case 'x': initscript = &argv[i][2]; break;
+ default: if(!serveroption(argv[i])) gameargs.add(argv[i]); break;
+ }
+ else gameargs.add(argv[i]);
+ }
+ initing = NOT_INITING;
+
+ numcpus = clamp(SDL_GetCPUCount(), 1, 16);
+
+ if(dedicated <= 1)
+ {
+ logoutf("init: sdl");
+
+ if(SDL_Init(SDL_INIT_TIMER|SDL_INIT_VIDEO|SDL_INIT_AUDIO)<0) fatal("Unable to initialize SDL: %s", SDL_GetError());
+
+#ifdef SDL_VIDEO_DRIVER_X11
+ SDL_version version;
+ SDL_GetVersion(&version);
+ if (SDL_VERSIONNUM(version.major, version.minor, version.patch) <= SDL_VERSIONNUM(2, 0, 12))
+ sdl_xgrab_bug = 1;
+#endif
+ }
+
+ logoutf("init: net");
+ if(enet_initialize()<0) fatal("Unable to initialise network module");
+ atexit(enet_deinitialize);
+ enet_time_set(0);
+
+ logoutf("init: game");
+ game::parseoptions(gameargs);
+ initserver(dedicated>0, dedicated>1); // never returns if dedicated
+ ASSERT(dedicated <= 1);
+ game::initclient();
+
+ logoutf("init: video");
+ SDL_SetHint(SDL_HINT_GRAB_KEYBOARD, "0");
+ #if !defined(WIN32) && !defined(__APPLE__)
+ SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
+ #endif
+ setupscreen();
+ SDL_ShowCursor(SDL_FALSE);
+ SDL_StopTextInput(); // workaround for spurious text-input events getting sent on first text input toggle?
+
+ logoutf("init: gl");
+ gl_checkextensions();
+ gl_init();
+ notexture = textureload("packages/textures/notexture.png");
+ if(!notexture) fatal("could not find core textures");
+
+ logoutf("init: console");
+ if(!execfile("data/stdlib.cfg", false)) fatal("cannot find data files (you are running from the wrong folder, try .bat file in the main folder)"); // this is the first file we load.
+ if(!execfile("data/font.cfg", false)) fatal("cannot find font definitions");
+ if(!setfont("default")) fatal("no default font specified");
+
+ inbetweenframes = true;
+ renderbackground("initializing...");
+
+ logoutf("init: world");
+ camera1 = player = game::iterdynents(0);
+ emptymap(0, true, NULL, false);
+
+ logoutf("init: sound");
+ initsound();
+
+ logoutf("init: cfg");
+ initing = INIT_LOAD;
+ execfile("data/keymap.cfg");
+ execfile("data/stdedit.cfg");
+ execfile("data/sounds.cfg");
+ execfile("data/menus.cfg");
+ execfile("data/heightmap.cfg");
+ execfile("data/blendbrush.cfg");
+ defformatstring(gamecfgname, "data/game_%s.cfg", game::gameident());
+ execfile(gamecfgname);
+ if(game::savedservers()) execfile(game::savedservers(), false);
+
+ identflags |= IDF_PERSIST;
+
+ if(!execfile(game::savedconfig(), false))
+ {
+ execfile(game::defaultconfig());
+ writecfg(game::restoreconfig());
+ }
+ execfile(game::autoexec(), false);
+
+ identflags &= ~IDF_PERSIST;
+
+ initing = INIT_GAME;
+ game::loadconfigs();
+
+ initing = NOT_INITING;
+
+ logoutf("init: render");
+ restoregamma();
+ restorevsync();
+ loadshaders();
+ initparticles();
+ initdecals();
+
+ identflags |= IDF_PERSIST;
+
+ logoutf("init: mainloop");
+
+ if(execfile("once.cfg", false)) remove(findfile("once.cfg", "rb"));
+
+ if(load)
+ {
+ logoutf("init: localconnect");
+ //localconnect();
+ game::changemap(load);
+ }
+
+ if(initscript) execute(initscript);
+
+ initmumble();
+ resetfpshistory();
+
+ inputgrab(grabinput = true);
+ ignoremousemotion();
+
+ for(;;)
+ {
+ static int frames = 0;
+ int millis = getclockmillis();
+ limitfps(millis, totalmillis);
+ elapsedtime = millis - totalmillis;
+ static int timeerr = 0;
+ int scaledtime = game::scaletime(elapsedtime) + timeerr;
+ curtime = scaledtime/100;
+ timeerr = scaledtime%100;
+ if(!multiplayer(false) && curtime>200) curtime = 200;
+ if(game::ispaused()) curtime = 0;
+ lastmillis += curtime;
+ totalmillis = millis;
+ updatetime();
+
+ checkinput();
+ menuprocess();
+ tryedit();
+
+ if(lastmillis) game::updateworld();
+
+ checksleep(lastmillis);
+
+ serverslice(false, 0);
+
+ if(frames) updatefpshistory(elapsedtime);
+ frames++;
+
+ // miscellaneous general game effects
+ recomputecamera();
+ updateparticles();
+ updatesounds();
+
+ if(minimized) continue;
+
+ inbetweenframes = false;
+ if(mainmenu) gl_drawmainmenu();
+ else gl_drawframe();
+ swapbuffers();
+ renderedframe = inbetweenframes = true;
+ }
+
+ ASSERT(0);
+ return EXIT_FAILURE;
+
+ #if defined(WIN32) && !defined(_DEBUG) && !defined(__GNUC__)
+ } __except(stackdumper(0, GetExceptionInformation()), EXCEPTION_CONTINUE_SEARCH) { return 0; }
+ #endif
+}
diff --git a/src/engine/master.cpp b/src/engine/master.cpp
new file mode 100644
index 0000000..2522e97
--- /dev/null
+++ b/src/engine/master.cpp
@@ -0,0 +1,718 @@
+#ifdef WIN32
+#define FD_SETSIZE 4096
+#else
+#include <sys/types.h>
+#undef __FD_SETSIZE
+#define __FD_SETSIZE 4096
+#endif
+
+#include "cube.h"
+#include <signal.h>
+#include <enet/time.h>
+
+#define INPUT_LIMIT 4096
+#define OUTPUT_LIMIT (64*1024)
+#define CLIENT_TIME (3*60*1000)
+#define AUTH_TIME (30*1000)
+#define AUTH_LIMIT 100
+#define AUTH_THROTTLE 1000
+#define CLIENT_LIMIT 4096
+#define DUP_LIMIT 16
+#define PING_TIME 3000
+#define PING_RETRY 5
+#define KEEPALIVE_TIME (65*60*1000)
+#define SERVER_LIMIT 4096
+#define SERVER_DUP_LIMIT 10
+
+FILE *logfile = NULL;
+
+struct userinfo
+{
+ char *name;
+ void *pubkey;
+};
+hashnameset<userinfo> users;
+
+void adduser(char *name, char *pubkey)
+{
+ name = newstring(name);
+ userinfo &u = users[name];
+ u.name = name;
+ u.pubkey = parsepubkey(pubkey);
+}
+COMMAND(adduser, "ss");
+
+void clearusers()
+{
+ enumerate(users, userinfo, u, { delete[] u.name; freepubkey(u.pubkey); });
+ users.clear();
+}
+COMMAND(clearusers, "");
+
+vector<ipmask> bans, servbans, gbans;
+
+void clearbans()
+{
+ bans.shrink(0);
+ servbans.shrink(0);
+ gbans.shrink(0);
+}
+COMMAND(clearbans, "");
+
+void addban(vector<ipmask> &bans, const char *name)
+{
+ ipmask ban;
+ ban.parse(name);
+ bans.add(ban);
+}
+ICOMMAND(ban, "s", (char *name), addban(bans, name));
+ICOMMAND(servban, "s", (char *name), addban(servbans, name));
+ICOMMAND(gban, "s", (char *name), addban(gbans, name));
+
+bool checkban(vector<ipmask> &bans, enet_uint32 host)
+{
+ loopv(bans) if(bans[i].check(host)) return true;
+ return false;
+}
+
+struct authreq
+{
+ enet_uint32 reqtime;
+ uint id;
+ void *answer;
+};
+
+struct gameserver
+{
+ ENetAddress address;
+ string ip;
+ int port, numpings;
+ enet_uint32 lastping, lastpong;
+};
+vector<gameserver *> gameservers;
+
+struct messagebuf
+{
+ vector<messagebuf *> &owner;
+ vector<char> buf;
+ int refs;
+
+ messagebuf(vector<messagebuf *> &owner) : owner(owner), refs(0) {}
+
+ const char *getbuf() { return buf.getbuf(); }
+ int length() { return buf.length(); }
+ void purge();
+
+ bool equals(const messagebuf &m) const
+ {
+ return buf.length() == m.buf.length() && !memcmp(buf.getbuf(), m.buf.getbuf(), buf.length());
+ }
+
+ bool endswith(const messagebuf &m) const
+ {
+ return buf.length() >= m.buf.length() && !memcmp(&buf[buf.length() - m.buf.length()], m.buf.getbuf(), m.buf.length());
+ }
+
+ void concat(const messagebuf &m)
+ {
+ if(buf.length() && buf.last() == '\0') buf.pop();
+ buf.put(m.buf.getbuf(), m.buf.length());
+ }
+};
+vector<messagebuf *> gameserverlists, gbanlists;
+bool updateserverlist = true;
+
+struct client
+{
+ ENetAddress address;
+ ENetSocket socket;
+ char input[INPUT_LIMIT];
+ messagebuf *message;
+ vector<char> output;
+ int inputpos, outputpos;
+ enet_uint32 connecttime, lastinput;
+ int servport;
+ enet_uint32 lastauth;
+ vector<authreq> authreqs;
+ bool shouldpurge;
+ bool registeredserver;
+
+ client() : message(NULL), inputpos(0), outputpos(0), servport(-1), lastauth(0), shouldpurge(false), registeredserver(false) {}
+};
+vector<client *> clients;
+
+ENetSocket serversocket = ENET_SOCKET_NULL;
+
+time_t starttime;
+enet_uint32 servtime = 0;
+
+void fatal(const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ vfprintf(logfile, fmt, args);
+ fputc('\n', logfile);
+ va_end(args);
+ exit(EXIT_FAILURE);
+}
+
+void conoutfv(int type, const char *fmt, va_list args)
+{
+ vfprintf(logfile, fmt, args);
+ fputc('\n', logfile);
+}
+
+void purgeclient(int n)
+{
+ client &c = *clients[n];
+ if(c.message) c.message->purge();
+ enet_socket_destroy(c.socket);
+ delete clients[n];
+ clients.remove(n);
+}
+
+void output(client &c, const char *msg, int len = 0)
+{
+ if(!len) len = strlen(msg);
+ c.output.put(msg, len);
+}
+
+void outputf(client &c, const char *fmt, ...)
+{
+ string msg;
+ va_list args;
+ va_start(args, fmt);
+ vformatstring(msg, fmt, args);
+ va_end(args);
+
+ output(c, msg);
+}
+
+ENetSocket pingsocket = ENET_SOCKET_NULL;
+
+bool setuppingsocket(ENetAddress *address)
+{
+ if(pingsocket != ENET_SOCKET_NULL) return true;
+ pingsocket = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+ if(pingsocket == ENET_SOCKET_NULL) return false;
+ if(address && enet_socket_bind(pingsocket, address) < 0) return false;
+ enet_socket_set_option(pingsocket, ENET_SOCKOPT_NONBLOCK, 1);
+ return true;
+}
+
+void setupserver(int port, const char *ip = NULL)
+{
+ ENetAddress address;
+ address.host = ENET_HOST_ANY;
+ address.port = port;
+
+ if(ip)
+ {
+ if(enet_address_set_host(&address, ip)<0)
+ fatal("failed to resolve server address: %s", ip);
+ }
+ serversocket = enet_socket_create(ENET_SOCKET_TYPE_STREAM);
+ if(serversocket==ENET_SOCKET_NULL ||
+ enet_socket_set_option(serversocket, ENET_SOCKOPT_REUSEADDR, 1) < 0 ||
+ enet_socket_bind(serversocket, &address) < 0 ||
+ enet_socket_listen(serversocket, -1) < 0)
+ fatal("failed to create server socket");
+ if(enet_socket_set_option(serversocket, ENET_SOCKOPT_NONBLOCK, 1)<0)
+ fatal("failed to make server socket non-blocking");
+ if(!setuppingsocket(&address))
+ fatal("failed to create ping socket");
+
+ enet_time_set(0);
+
+ starttime = time(NULL);
+ char *ct = ctime(&starttime);
+ if(strchr(ct, '\n')) *strchr(ct, '\n') = '\0';
+ conoutf("*** Starting master server on %s %d at %s ***", ip ? ip : "localhost", port, ct);
+}
+
+void genserverlist()
+{
+ if(!updateserverlist) return;
+ while(gameserverlists.length() && gameserverlists.last()->refs<=0)
+ delete gameserverlists.pop();
+ messagebuf *l = new messagebuf(gameserverlists);
+ loopv(gameservers)
+ {
+ gameserver &s = *gameservers[i];
+ if(!s.lastpong) continue;
+ defformatstring(cmd, "addserver %s %d\n", s.ip, s.port);
+ l->buf.put(cmd, strlen(cmd));
+ }
+ l->buf.add('\0');
+ gameserverlists.add(l);
+ updateserverlist = false;
+}
+
+void gengbanlist()
+{
+ messagebuf *l = new messagebuf(gbanlists);
+ const char *header = "cleargbans\n";
+ l->buf.put(header, strlen(header));
+ string cmd = "addgban ";
+ int cmdlen = strlen(cmd);
+ loopv(gbans)
+ {
+ ipmask &b = gbans[i];
+ l->buf.put(cmd, cmdlen + b.print(&cmd[cmdlen]));
+ l->buf.add('\n');
+ }
+ if(gbanlists.length() && gbanlists.last()->equals(*l))
+ {
+ delete l;
+ return;
+ }
+ while(gbanlists.length() && gbanlists.last()->refs<=0)
+ delete gbanlists.pop();
+ loopv(gbanlists)
+ {
+ messagebuf *m = gbanlists[i];
+ if(m->refs > 0 && !m->endswith(*l)) m->concat(*l);
+ }
+ gbanlists.add(l);
+ loopv(clients)
+ {
+ client &c = *clients[i];
+ if(c.servport >= 0 && !c.message)
+ {
+ c.message = l;
+ c.message->refs++;
+ }
+ }
+}
+
+void addgameserver(client &c)
+{
+ if(gameservers.length() >= SERVER_LIMIT) return;
+ int dups = 0;
+ loopv(gameservers)
+ {
+ gameserver &s = *gameservers[i];
+ if(s.address.host != c.address.host) continue;
+ ++dups;
+ if(s.port == c.servport)
+ {
+ s.lastping = 0;
+ s.numpings = 0;
+ return;
+ }
+ }
+ if(dups >= SERVER_DUP_LIMIT)
+ {
+ outputf(c, "failreg too many servers on ip\n");
+ return;
+ }
+ string hostname;
+ if(enet_address_get_host_ip(&c.address, hostname, sizeof(hostname)) < 0)
+ {
+ outputf(c, "failreg failed resolving ip\n");
+ return;
+ }
+ gameserver &s = *gameservers.add(new gameserver);
+ s.address.host = c.address.host;
+ s.address.port = c.servport+1;
+ copystring(s.ip, hostname);
+ s.port = c.servport;
+ s.numpings = 0;
+ s.lastping = s.lastpong = 0;
+}
+
+client *findclient(gameserver &s)
+{
+ loopv(clients)
+ {
+ client &c = *clients[i];
+ if(s.address.host == c.address.host && s.port == c.servport)
+ return &c;
+ }
+ return NULL;
+}
+
+void servermessage(gameserver &s, const char *msg)
+{
+ client *c = findclient(s);
+ if(c) outputf(*c, msg);
+}
+
+void checkserverpongs()
+{
+ ENetBuffer buf;
+ ENetAddress addr;
+ static uchar pong[MAXTRANS];
+ for(;;)
+ {
+ buf.data = pong;
+ buf.dataLength = sizeof(pong);
+ int len = enet_socket_receive(pingsocket, &addr, &buf, 1);
+ if(len <= 0) break;
+ loopv(gameservers)
+ {
+ gameserver &s = *gameservers[i];
+ if(s.address.host == addr.host && s.address.port == addr.port)
+ {
+ if(s.lastping && (!s.lastpong || ENET_TIME_GREATER(s.lastping, s.lastpong)))
+ {
+ client *c = findclient(s);
+ if(c)
+ {
+ c->registeredserver = true;
+ outputf(*c, "succreg\n");
+ if(!c->message && gbanlists.length())
+ {
+ c->message = gbanlists.last();
+ c->message->refs++;
+ }
+ }
+ }
+ if(!s.lastpong) updateserverlist = true;
+ s.lastpong = servtime ? servtime : 1;
+ break;
+ }
+ }
+ }
+}
+
+void bangameservers()
+{
+ loopvrev(gameservers) if(checkban(servbans, gameservers[i]->address.host))
+ {
+ delete gameservers.remove(i);
+ updateserverlist = true;
+ }
+}
+
+void checkgameservers()
+{
+ ENetBuffer buf;
+ loopv(gameservers)
+ {
+ gameserver &s = *gameservers[i];
+ if(s.lastping && s.lastpong && ENET_TIME_LESS_EQUAL(s.lastping, s.lastpong))
+ {
+ if(ENET_TIME_DIFFERENCE(servtime, s.lastpong) > KEEPALIVE_TIME)
+ {
+ delete gameservers.remove(i--);
+ updateserverlist = true;
+ }
+ }
+ else if(!s.lastping || ENET_TIME_DIFFERENCE(servtime, s.lastping) > PING_TIME)
+ {
+ if(s.numpings >= PING_RETRY)
+ {
+ servermessage(s, "failreg failed pinging server\n");
+ delete gameservers.remove(i--);
+ updateserverlist = true;
+ }
+ else
+ {
+ static const uchar ping[] = { 1 };
+ buf.data = (void *)ping;
+ buf.dataLength = sizeof(ping);
+ s.numpings++;
+ s.lastping = servtime ? servtime : 1;
+ enet_socket_send(pingsocket, &s.address, &buf, 1);
+ }
+ }
+ }
+}
+
+void messagebuf::purge()
+{
+ refs = max(refs - 1, 0);
+ if(refs<=0 && owner.last()!=this)
+ {
+ owner.removeobj(this);
+ delete this;
+ }
+}
+
+void purgeauths(client &c)
+{
+ int expired = 0;
+ loopv(c.authreqs)
+ {
+ if(ENET_TIME_DIFFERENCE(servtime, c.authreqs[i].reqtime) >= AUTH_TIME)
+ {
+ outputf(c, "failauth %u\n", c.authreqs[i].id);
+ freechallenge(c.authreqs[i].answer);
+ expired = i + 1;
+ }
+ else break;
+ }
+ if(expired > 0) c.authreqs.remove(0, expired);
+}
+
+void reqauth(client &c, uint id, char *name)
+{
+ if(ENET_TIME_DIFFERENCE(servtime, c.lastauth) < AUTH_THROTTLE)
+ return;
+
+ c.lastauth = servtime;
+
+ purgeauths(c);
+
+ time_t t = time(NULL);
+ char *ct = ctime(&t);
+ if(ct)
+ {
+ char *newline = strchr(ct, '\n');
+ if(newline) *newline = '\0';
+ }
+ string ip;
+ if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-");
+ conoutf("%s: attempting \"%s\" as %u from %s", ct ? ct : "-", name, id, ip);
+
+ userinfo *u = users.access(name);
+ if(!u)
+ {
+ outputf(c, "failauth %u\n", id);
+ return;
+ }
+
+ if(c.authreqs.length() >= AUTH_LIMIT)
+ {
+ outputf(c, "failauth %u\n", c.authreqs[0].id);
+ freechallenge(c.authreqs[0].answer);
+ c.authreqs.remove(0);
+ }
+
+ authreq &a = c.authreqs.add();
+ a.reqtime = servtime;
+ a.id = id;
+ uint seed[3] = { uint(starttime), servtime, randomMT() };
+ static vector<char> buf;
+ buf.setsize(0);
+ a.answer = genchallenge(u->pubkey, seed, sizeof(seed), buf);
+
+ outputf(c, "chalauth %u %s\n", id, buf.getbuf());
+}
+
+void confauth(client &c, uint id, const char *val)
+{
+ purgeauths(c);
+
+ loopv(c.authreqs) if(c.authreqs[i].id == id)
+ {
+ string ip;
+ if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-");
+ if(checkchallenge(val, c.authreqs[i].answer))
+ {
+ outputf(c, "succauth %u\n", id);
+ conoutf("succeeded %u from %s", id, ip);
+ }
+ else
+ {
+ outputf(c, "failauth %u\n", id);
+ conoutf("failed %u from %s", id, ip);
+ }
+ freechallenge(c.authreqs[i].answer);
+ c.authreqs.remove(i--);
+ return;
+ }
+ outputf(c, "failauth %u\n", id);
+}
+
+bool checkclientinput(client &c)
+{
+ if(c.inputpos<0) return true;
+ char *end = (char *)memchr(c.input, '\n', c.inputpos);
+ while(end)
+ {
+ *end++ = '\0';
+ c.lastinput = servtime;
+
+ int port;
+ uint id;
+ string user, val;
+ if(!strncmp(c.input, "list", 4) && (!c.input[4] || c.input[4] == '\n' || c.input[4] == '\r'))
+ {
+ genserverlist();
+ if(gameserverlists.empty() || c.message) return false;
+ c.message = gameserverlists.last();
+ c.message->refs++;
+ c.output.setsize(0);
+ c.outputpos = 0;
+ c.shouldpurge = true;
+ return true;
+ }
+ else if(sscanf(c.input, "regserv %d", &port) == 1)
+ {
+ if(checkban(servbans, c.address.host)) return false;
+ if(port < 0 || port > 0xFFFF-1 || (c.servport >= 0 && port != c.servport)) outputf(c, "failreg invalid port\n");
+ else
+ {
+ c.servport = port;
+ addgameserver(c);
+ }
+ }
+ else if(sscanf(c.input, "reqauth %u %100s", &id, user) == 2)
+ {
+ reqauth(c, id, user);
+ }
+ else if(sscanf(c.input, "confauth %u %100s", &id, val) == 2)
+ {
+ confauth(c, id, val);
+ }
+ c.inputpos = &c.input[c.inputpos] - end;
+ memmove(c.input, end, c.inputpos);
+
+ end = (char *)memchr(c.input, '\n', c.inputpos);
+ }
+ return c.inputpos<(int)sizeof(c.input);
+}
+
+ENetSocketSet readset, writeset;
+
+void checkclients()
+{
+ ENetSocketSet readset, writeset;
+ ENetSocket maxsock = max(serversocket, pingsocket);
+ ENET_SOCKETSET_EMPTY(readset);
+ ENET_SOCKETSET_EMPTY(writeset);
+ ENET_SOCKETSET_ADD(readset, serversocket);
+ ENET_SOCKETSET_ADD(readset, pingsocket);
+ loopv(clients)
+ {
+ client &c = *clients[i];
+ if(c.authreqs.length()) purgeauths(c);
+ if(c.message || c.output.length()) ENET_SOCKETSET_ADD(writeset, c.socket);
+ else ENET_SOCKETSET_ADD(readset, c.socket);
+ maxsock = max(maxsock, c.socket);
+ }
+ if(enet_socketset_select(maxsock, &readset, &writeset, 1000)<=0) return;
+
+ if(ENET_SOCKETSET_CHECK(readset, pingsocket)) checkserverpongs();
+ if(ENET_SOCKETSET_CHECK(readset, serversocket))
+ {
+ ENetAddress address;
+ ENetSocket clientsocket = enet_socket_accept(serversocket, &address);
+ if(clients.length()>=CLIENT_LIMIT || checkban(bans, address.host)) enet_socket_destroy(clientsocket);
+ else if(clientsocket!=ENET_SOCKET_NULL)
+ {
+ int dups = 0, oldest = -1;
+ loopv(clients) if(clients[i]->address.host == address.host)
+ {
+ dups++;
+ if(oldest<0 || clients[i]->connecttime < clients[oldest]->connecttime) oldest = i;
+ }
+ if(dups >= DUP_LIMIT) purgeclient(oldest);
+
+ client *c = new client;
+ c->address = address;
+ c->socket = clientsocket;
+ c->connecttime = servtime;
+ c->lastinput = servtime;
+ clients.add(c);
+ }
+ }
+
+ loopv(clients)
+ {
+ client &c = *clients[i];
+ if((c.message || c.output.length()) && ENET_SOCKETSET_CHECK(writeset, c.socket))
+ {
+ const char *data = c.output.length() ? c.output.getbuf() : c.message->getbuf();
+ int len = c.output.length() ? c.output.length() : c.message->length();
+ ENetBuffer buf;
+ buf.data = (void *)&data[c.outputpos];
+ buf.dataLength = len-c.outputpos;
+ int res = enet_socket_send(c.socket, NULL, &buf, 1);
+ if(res>=0)
+ {
+ c.outputpos += res;
+ if(c.outputpos>=len)
+ {
+ if(c.output.length()) c.output.setsize(0);
+ else
+ {
+ c.message->purge();
+ c.message = NULL;
+ }
+ c.outputpos = 0;
+ if(!c.message && c.output.empty() && c.shouldpurge)
+ {
+ purgeclient(i--);
+ continue;
+ }
+ }
+ }
+ else { purgeclient(i--); continue; }
+ }
+ if(ENET_SOCKETSET_CHECK(readset, c.socket))
+ {
+ ENetBuffer buf;
+ buf.data = &c.input[c.inputpos];
+ buf.dataLength = sizeof(c.input) - c.inputpos;
+ int res = enet_socket_receive(c.socket, NULL, &buf, 1);
+ if(res>0)
+ {
+ c.inputpos += res;
+ c.input[min(c.inputpos, (int)sizeof(c.input)-1)] = '\0';
+ if(!checkclientinput(c)) { purgeclient(i--); continue; }
+ }
+ else { purgeclient(i--); continue; }
+ }
+ if(c.output.length() > OUTPUT_LIMIT) { purgeclient(i--); continue; }
+ if(ENET_TIME_DIFFERENCE(servtime, c.lastinput) >= (c.registeredserver ? KEEPALIVE_TIME : CLIENT_TIME)) { purgeclient(i--); continue; }
+ }
+}
+
+void banclients()
+{
+ loopvrev(clients) if(checkban(bans, clients[i]->address.host)) purgeclient(i);
+}
+
+volatile int reloadcfg = 1;
+
+#ifndef WIN32
+void reloadsignal(int signum)
+{
+ reloadcfg = 1;
+}
+#endif
+
+int main(int argc, char **argv)
+{
+ if(enet_initialize()<0) fatal("Unable to initialise network module");
+ atexit(enet_deinitialize);
+
+ const char *dir = "", *ip = NULL;
+ int port = 28787;
+ if(argc>=2) dir = argv[1];
+ if(argc>=3) port = atoi(argv[2]);
+ if(argc>=4) ip = argv[3];
+ defformatstring(logname, "%smaster.log", dir);
+ defformatstring(cfgname, "%smaster.cfg", dir);
+ path(logname);
+ path(cfgname);
+ logfile = fopen(logname, "a");
+ if(!logfile) logfile = stdout;
+ setvbuf(logfile, NULL, _IOLBF, BUFSIZ);
+#ifndef WIN32
+ signal(SIGUSR1, reloadsignal);
+#endif
+ setupserver(port, ip);
+ for(;;)
+ {
+ if(reloadcfg)
+ {
+ conoutf("reloading master.cfg");
+ execfile(cfgname);
+ bangameservers();
+ banclients();
+ gengbanlist();
+ reloadcfg = 0;
+ }
+
+ servtime = enet_time_get();
+ checkclients();
+ checkgameservers();
+ }
+
+ return EXIT_SUCCESS;
+}
+
diff --git a/src/engine/material.cpp b/src/engine/material.cpp
new file mode 100644
index 0000000..59f47f8
--- /dev/null
+++ b/src/engine/material.cpp
@@ -0,0 +1,886 @@
+#include "engine.h"
+
+struct QuadNode
+{
+ int x, y, size;
+ uint filled;
+ QuadNode *child[4];
+
+ QuadNode(int x, int y, int size) : x(x), y(y), size(size), filled(0) { loopi(4) child[i] = 0; }
+
+ void clear()
+ {
+ loopi(4) DELETEP(child[i]);
+ }
+
+ ~QuadNode()
+ {
+ clear();
+ }
+
+ void insert(int mx, int my, int msize)
+ {
+ if(size == msize)
+ {
+ filled = 0xF;
+ return;
+ }
+ int csize = size>>1, i = 0;
+ if(mx >= x+csize) i |= 1;
+ if(my >= y+csize) i |= 2;
+ if(csize == msize)
+ {
+ filled |= (1 << i);
+ return;
+ }
+ if(!child[i]) child[i] = new QuadNode(i&1 ? x+csize : x, i&2 ? y+csize : y, csize);
+ child[i]->insert(mx, my, msize);
+ loopj(4) if(child[j])
+ {
+ if(child[j]->filled == 0xF)
+ {
+ DELETEP(child[j]);
+ filled |= (1 << j);
+ }
+ }
+ }
+
+ void genmatsurf(ushort mat, uchar orient, uchar visible, int x, int y, int z, int size, materialsurface *&matbuf)
+ {
+ materialsurface &m = *matbuf++;
+ m.material = mat;
+ m.orient = orient;
+ m.visible = visible;
+ m.csize = size;
+ m.rsize = size;
+ int dim = dimension(orient);
+ m.o[C[dim]] = x;
+ m.o[R[dim]] = y;
+ m.o[dim] = z;
+ }
+
+ void genmatsurfs(ushort mat, uchar orient, uchar flags, int z, materialsurface *&matbuf)
+ {
+ if(filled == 0xF) genmatsurf(mat, orient, flags, x, y, z, size, matbuf);
+ else if(filled)
+ {
+ int csize = size>>1;
+ loopi(4) if(filled & (1 << i))
+ genmatsurf(mat, orient, flags, i&1 ? x+csize : x, i&2 ? y+csize : y, z, csize, matbuf);
+ }
+ loopi(4) if(child[i]) child[i]->genmatsurfs(mat, orient, flags, z, matbuf);
+ }
+};
+
+static float wfwave;
+
+static const bvec4 matnormals[6] =
+{
+ bvec4(0x80, 0, 0),
+ bvec4(0x7F, 0, 0),
+ bvec4(0, 0x80, 0),
+ bvec4(0, 0x7F, 0),
+ bvec4(0, 0, 0x80),
+ bvec4(0, 0, 0x7F)
+};
+
+static void renderwaterfall(const materialsurface &m, float offset)
+{
+ if(gle::attribbuf.empty())
+ {
+ gle::defvertex();
+ gle::defnormal(4, GL_BYTE);
+ gle::begin(GL_QUADS);
+ }
+ float x = m.o.x, y = m.o.y, zmin = m.o.z, zmax = zmin;
+ if(m.ends&1) zmin += -WATER_OFFSET-WATER_AMPLITUDE;
+ if(m.ends&2) zmax += wfwave;
+ int csize = m.csize, rsize = m.rsize;
+ switch(m.orient)
+ {
+ #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+ case orient: v0 v1 v2 v3 break;
+ #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
+ { \
+ gle::attribf(mx sx, my sy, mz sz); \
+ gle::attrib(matnormals[orient]); \
+ }
+ GENFACEVERTSXY(x, x, y, y, zmin, zmax, /**/, + csize, /**/, + rsize, + offset, - offset)
+ #undef GENFACEORIENT
+ #undef GENFACEVERT
+ }
+}
+
+static void drawmaterial(const materialsurface &m, float offset, const bvec4 &color)
+{
+ if(gle::attribbuf.empty())
+ {
+ gle::defvertex();
+ gle::defcolor(4, GL_UNSIGNED_BYTE);
+ gle::begin(GL_QUADS);
+ }
+ float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize, rsize = m.rsize;
+ switch(m.orient)
+ {
+ #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+ case orient: v0 v1 v2 v3 break;
+ #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
+ { \
+ gle::attribf(mx sx, my sy, mz sz); \
+ gle::attrib(color); \
+ }
+ GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset)
+ #undef GENFACEORIENT
+ #undef GENFACEVERT
+ }
+}
+
+const struct material
+{
+ const char *name;
+ ushort id;
+} materials[] =
+{
+ {"air", MAT_AIR},
+ {"water", MAT_WATER}, {"water1", MAT_WATER}, {"water2", MAT_WATER+1}, {"water3", MAT_WATER+2}, {"water4", MAT_WATER+3},
+ {"glass", MAT_GLASS}, {"glass1", MAT_GLASS}, {"glass2", MAT_GLASS+1}, {"glass3", MAT_GLASS+2}, {"glass4", MAT_GLASS+3},
+ {"lava", MAT_LAVA}, {"lava1", MAT_LAVA}, {"lava2", MAT_LAVA+1}, {"lava3", MAT_LAVA+2}, {"lava4", MAT_LAVA+3},
+ {"clip", MAT_CLIP},
+ {"noclip", MAT_NOCLIP},
+ {"gameclip", MAT_GAMECLIP},
+ {"death", MAT_DEATH},
+ {"alpha", MAT_ALPHA}
+};
+
+int findmaterial(const char *name)
+{
+ loopi(sizeof(materials)/sizeof(material))
+ {
+ if(!strcmp(materials[i].name, name)) return materials[i].id;
+ }
+ return -1;
+}
+
+const char *findmaterialname(int mat)
+{
+ loopi(sizeof(materials)/sizeof(materials[0])) if(materials[i].id == mat) return materials[i].name;
+ return NULL;
+}
+
+const char *getmaterialdesc(int mat, const char *prefix)
+{
+ static const ushort matmasks[] = { MATF_VOLUME|MATF_INDEX, MATF_CLIP, MAT_DEATH, MAT_ALPHA };
+ static string desc;
+ desc[0] = '\0';
+ loopi(sizeof(matmasks)/sizeof(matmasks[0])) if(mat&matmasks[i])
+ {
+ const char *matname = findmaterialname(mat&matmasks[i]);
+ if(matname)
+ {
+ concatstring(desc, desc[0] ? ", " : prefix);
+ concatstring(desc, matname);
+ }
+ }
+ return desc;
+}
+
+int visiblematerial(const cube &c, int orient, const ivec &co, int size, ushort matmask)
+{
+ ushort mat = c.material&matmask;
+ switch(mat)
+ {
+ case MAT_AIR:
+ break;
+
+ case MAT_LAVA:
+ case MAT_WATER:
+ if(visibleface(c, orient, co, size, mat, MAT_AIR, matmask))
+ return (orient != O_BOTTOM ? MATSURF_VISIBLE : MATSURF_EDIT_ONLY);
+ break;
+
+ case MAT_GLASS:
+ if(visibleface(c, orient, co, size, MAT_GLASS, MAT_AIR, matmask))
+ return MATSURF_VISIBLE;
+ break;
+
+ default:
+ if(visibleface(c, orient, co, size, mat, MAT_AIR, matmask))
+ return MATSURF_EDIT_ONLY;
+ break;
+ }
+ return MATSURF_NOT_VISIBLE;
+}
+
+void genmatsurfs(const cube &c, const ivec &co, int size, vector<materialsurface> &matsurfs)
+{
+ loopi(6)
+ {
+ static const ushort matmasks[] = { MATF_VOLUME|MATF_INDEX, MATF_CLIP, MAT_DEATH, MAT_ALPHA };
+ loopj(sizeof(matmasks)/sizeof(matmasks[0]))
+ {
+ int matmask = matmasks[j];
+ int vis = visiblematerial(c, i, co, size, matmask&~MATF_INDEX);
+ if(vis != MATSURF_NOT_VISIBLE)
+ {
+ materialsurface m;
+ m.material = c.material&matmask;
+ m.orient = i;
+ m.visible = vis;
+ m.o = co;
+ m.csize = m.rsize = size;
+ if(dimcoord(i)) m.o[dimension(i)] += size;
+ matsurfs.add(m);
+ break;
+ }
+ }
+ }
+}
+
+static inline bool mergematcmp(const materialsurface &x, const materialsurface &y)
+{
+ int dim = dimension(x.orient), c = C[dim], r = R[dim];
+ if(x.o[r] + x.rsize < y.o[r] + y.rsize) return true;
+ if(x.o[r] + x.rsize > y.o[r] + y.rsize) return false;
+ return x.o[c] < y.o[c];
+}
+
+static int mergematr(materialsurface *m, int sz, materialsurface &n)
+{
+ int dim = dimension(n.orient), c = C[dim], r = R[dim];
+ for(int i = sz-1; i >= 0; --i)
+ {
+ if(m[i].o[r] + m[i].rsize < n.o[r]) break;
+ if(m[i].o[r] + m[i].rsize == n.o[r] && m[i].o[c] == n.o[c] && m[i].csize == n.csize)
+ {
+ n.o[r] = m[i].o[r];
+ n.rsize += m[i].rsize;
+ memmove(&m[i], &m[i+1], (sz - (i+1)) * sizeof(materialsurface));
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int mergematc(materialsurface &m, materialsurface &n)
+{
+ int dim = dimension(n.orient), c = C[dim], r = R[dim];
+ if(m.o[r] == n.o[r] && m.rsize == n.rsize && m.o[c] + m.csize == n.o[c])
+ {
+ n.o[c] = m.o[c];
+ n.csize += m.csize;
+ return 1;
+ }
+ return 0;
+}
+
+static int mergemat(materialsurface *m, int sz, materialsurface &n)
+{
+ for(bool merged = false; sz; merged = true)
+ {
+ int rmerged = mergematr(m, sz, n);
+ sz -= rmerged;
+ if(!rmerged && merged) break;
+ if(!sz) break;
+ int cmerged = mergematc(m[sz-1], n);
+ sz -= cmerged;
+ if(!cmerged) break;
+ }
+ m[sz++] = n;
+ return sz;
+}
+
+static int mergemats(materialsurface *m, int sz)
+{
+ quicksort(m, sz, mergematcmp);
+
+ int nsz = 0;
+ loopi(sz) nsz = mergemat(m, nsz, m[i]);
+ return nsz;
+}
+
+static inline bool optmatcmp(const materialsurface &x, const materialsurface &y)
+{
+ if(x.material < y.material) return true;
+ if(x.material > y.material) return false;
+ if(x.orient > y.orient) return true;
+ if(x.orient < y.orient) return false;
+ int dim = dimension(x.orient);
+ return x.o[dim] < y.o[dim];
+}
+
+VARF(optmats, 0, 1, 1, allchanged());
+
+int optimizematsurfs(materialsurface *matbuf, int matsurfs)
+{
+ quicksort(matbuf, matsurfs, optmatcmp);
+ if(!optmats) return matsurfs;
+ materialsurface *cur = matbuf, *end = matbuf+matsurfs;
+ while(cur < end)
+ {
+ materialsurface *start = cur++;
+ int dim = dimension(start->orient);
+ while(cur < end &&
+ cur->material == start->material &&
+ cur->orient == start->orient &&
+ cur->visible == start->visible &&
+ cur->o[dim] == start->o[dim])
+ ++cur;
+ if(!isliquid(start->material&MATF_VOLUME) || start->orient != O_TOP || !vertwater)
+ {
+ if(start!=matbuf) memmove(matbuf, start, (cur-start)*sizeof(materialsurface));
+ matbuf += mergemats(matbuf, cur-start);
+ }
+ else if(cur-start>=4)
+ {
+ QuadNode vmats(0, 0, worldsize);
+ loopi(cur-start) vmats.insert(start[i].o[C[dim]], start[i].o[R[dim]], start[i].csize);
+ vmats.genmatsurfs(start->material, start->orient, start->visible, start->o[dim], matbuf);
+ }
+ else
+ {
+ if(start!=matbuf) memmove(matbuf, start, (cur-start)*sizeof(materialsurface));
+ matbuf += cur-start;
+ }
+ }
+ return matsurfs - (end-matbuf);
+}
+
+struct waterinfo
+{
+ materialsurface *m;
+ double depth, area;
+};
+
+void setupmaterials(int start, int len)
+{
+ int hasmat = 0;
+ vector<waterinfo> water;
+ unionfind uf;
+ if(!len) len = valist.length();
+ for(int i = start; i < len; i++)
+ {
+ vtxarray *va = valist[i];
+ materialsurface *skip = NULL;
+ loopj(va->matsurfs)
+ {
+ materialsurface &m = va->matbuf[j];
+ int matvol = m.material&MATF_VOLUME;
+ if(matvol==MAT_WATER && m.orient==O_TOP)
+ {
+ m.index = water.length();
+ loopvk(water)
+ {
+ materialsurface &n = *water[k].m;
+ if(m.material!=n.material || m.o.z!=n.o.z) continue;
+ if(n.o.x+n.rsize==m.o.x || m.o.x+m.rsize==n.o.x)
+ {
+ if(n.o.y+n.csize>m.o.y && n.o.y<m.o.y+m.csize) uf.unite(m.index, n.index);
+ }
+ else if(n.o.y+n.csize==m.o.y || m.o.y+m.csize==n.o.y)
+ {
+ if(n.o.x+n.rsize>m.o.x && n.o.x<m.o.x+m.rsize) uf.unite(m.index, n.index);
+ }
+ }
+ waterinfo &wi = water.add();
+ wi.m = &m;
+ vec center(m.o.x+m.rsize/2, m.o.y+m.csize/2, m.o.z-WATER_OFFSET);
+ m.light = brightestlight(center, vec(0, 0, 1));
+ float depth = raycube(center, vec(0, 0, -1), 10000);
+ wi.depth = double(depth)*m.rsize*m.csize;
+ wi.area = m.rsize*m.csize;
+ }
+ else if(isliquid(matvol) && m.orient!=O_BOTTOM && m.orient!=O_TOP)
+ {
+ m.ends = 0;
+ int dim = dimension(m.orient), coord = dimcoord(m.orient);
+ ivec o(m.o);
+ o.z -= 1;
+ o[dim] += coord ? 1 : -1;
+ int minc = o[dim^1], maxc = minc + (C[dim]==2 ? m.rsize : m.csize);
+ ivec co;
+ int csize;
+ while(o[dim^1] < maxc)
+ {
+ cube &c = lookupcube(o, 0, co, csize);
+ if(isliquid(c.material&MATF_VOLUME)) { m.ends |= 1; break; }
+ o[dim^1] += csize;
+ }
+ o[dim^1] = minc;
+ o.z += R[dim]==2 ? m.rsize : m.csize;
+ o[dim] -= coord ? 2 : -2;
+ while(o[dim^1] < maxc)
+ {
+ cube &c = lookupcube(o, 0, co, csize);
+ if(visiblematerial(c, O_TOP, co, csize)) { m.ends |= 2; break; }
+ o[dim^1] += csize;
+ }
+ }
+ else if(matvol==MAT_GLASS)
+ {
+ int dim = dimension(m.orient);
+ vec center(m.o);
+ center[R[dim]] += m.rsize/2;
+ center[C[dim]] += m.csize/2;
+ m.envmap = closestenvmap(center);
+ }
+ if(matvol) hasmat |= 1<<m.material;
+ m.skip = 0;
+ if(skip && m.material == skip->material && m.orient == skip->orient && skip->skip < 0xFFFF)
+ skip->skip++;
+ else
+ skip = &m;
+ }
+ }
+ loopv(water)
+ {
+ int root = uf.find(i);
+ if(i==root) continue;
+ materialsurface &m = *water[i].m, &n = *water[root].m;
+ if(m.light && (!m.light->attr1 || !n.light || (n.light->attr1 && m.light->attr1 > n.light->attr1))) n.light = m.light;
+ water[root].depth += water[i].depth;
+ water[root].area += water[i].area;
+ }
+ loopv(water)
+ {
+ int root = uf.find(i);
+ water[i].m->light = water[root].m->light;
+ water[i].m->depth = (short)(water[root].depth/water[root].area);
+ }
+ if(hasmat&(0xF<<MAT_WATER))
+ {
+ loadcaustics(true);
+ preloadwatershaders(true);
+ loopi(4) if(hasmat&(1<<(MAT_WATER+i))) lookupmaterialslot(MAT_WATER+i);
+ }
+ if(hasmat&(0xF<<MAT_LAVA))
+ {
+ useshaderbyname("lava");
+ useshaderbyname("lavaglare");
+ loopi(4) if(hasmat&(1<<(MAT_LAVA+i))) lookupmaterialslot(MAT_LAVA+i);
+ }
+ if(hasmat&(0xF<<MAT_GLASS)) useshaderbyname("glass");
+}
+
+VARP(showmat, 0, 1, 1);
+
+static int sortdim[3];
+static ivec sortorigin;
+static bool sortedit;
+
+static inline bool vismatcmp(const materialsurface *xm, const materialsurface *ym)
+{
+ const materialsurface &x = *xm, &y = *ym;
+ if(!sortedit)
+ {
+ if((x.material&MATF_VOLUME) == MAT_LAVA) { if((y.material&MATF_VOLUME) != MAT_LAVA) return true; }
+ else if((y.material&MATF_VOLUME) == MAT_LAVA) return false;
+ }
+ int xdim = dimension(x.orient), ydim = dimension(y.orient);
+ loopi(3)
+ {
+ int dim = sortdim[i], xmin, xmax, ymin, ymax;
+ xmin = xmax = x.o[dim];
+ if(dim==C[xdim]) xmax += x.csize;
+ else if(dim==R[xdim]) xmax += x.rsize;
+ ymin = ymax = y.o[dim];
+ if(dim==C[ydim]) ymax += y.csize;
+ else if(dim==R[ydim]) ymax += y.rsize;
+ if(xmax > ymin && ymax > xmin) continue;
+ int c = sortorigin[dim];
+ if(c > xmin && c < xmax) return sortedit;
+ if(c > ymin && c < ymax) return !sortedit;
+ xmin = abs(xmin - c);
+ xmax = abs(xmax - c);
+ ymin = abs(ymin - c);
+ ymax = abs(ymax - c);
+ if(max(xmin, xmax) <= min(ymin, ymax)) return sortedit;
+ else if(max(ymin, ymax) <= min(xmin, xmax)) return !sortedit;
+ }
+ if(x.material < y.material) return sortedit;
+ if(x.material > y.material) return !sortedit;
+ return false;
+}
+
+void sortmaterials(vector<materialsurface *> &vismats)
+{
+ sortorigin = ivec(camera1->o);
+ if(reflecting) sortorigin.z = int(reflectz - (camera1->o.z - reflectz));
+ vec dir;
+ vecfromyawpitch(camera1->yaw, reflecting ? -camera1->pitch : camera1->pitch, 1, 0, dir);
+ loopi(3) { dir[i] = fabs(dir[i]); sortdim[i] = i; }
+ if(dir[sortdim[2]] > dir[sortdim[1]]) swap(sortdim[2], sortdim[1]);
+ if(dir[sortdim[1]] > dir[sortdim[0]]) swap(sortdim[1], sortdim[0]);
+ if(dir[sortdim[2]] > dir[sortdim[1]]) swap(sortdim[2], sortdim[1]);
+
+ for(vtxarray *va = reflecting ? reflectedva : visibleva; va; va = reflecting ? va->rnext : va->next)
+ {
+ if(!va->matsurfs || va->occluded >= OCCLUDE_BB) continue;
+ if(reflecting || refracting>0 ? va->o.z+va->size <= reflectz : va->o.z >= reflectz) continue;
+ loopi(va->matsurfs)
+ {
+ materialsurface &m = va->matbuf[i];
+ if(!editmode || !showmat || drawtex)
+ {
+ int matvol = m.material&MATF_VOLUME;
+ if(matvol==MAT_WATER && (m.orient==O_TOP || (refracting<0 && reflectz>worldsize))) { i += m.skip; continue; }
+ if(m.visible == MATSURF_EDIT_ONLY) { i += m.skip; continue; }
+ if(glaring && matvol!=MAT_LAVA) { i += m.skip; continue; }
+ }
+ else if(glaring) continue;
+ vismats.add(&m);
+ }
+ }
+ sortedit = editmode && showmat && !drawtex;
+ vismats.sort(vismatcmp);
+}
+
+void rendermatgrid(vector<materialsurface *> &vismats)
+{
+ enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+ int lastmat = -1;
+ bvec4 color(0, 0, 0, 0);
+ loopvrev(vismats)
+ {
+ materialsurface &m = *vismats[i];
+ if(m.material != lastmat)
+ {
+ switch(m.material&~MATF_INDEX)
+ {
+ case MAT_WATER: color = bvec4( 0, 0, 85, 255); break; // blue
+ case MAT_CLIP: color = bvec4(85, 0, 0, 255); break; // red
+ case MAT_GLASS: color = bvec4( 0, 85, 85, 255); break; // cyan
+ case MAT_NOCLIP: color = bvec4( 0, 85, 0, 255); break; // green
+ case MAT_LAVA: color = bvec4(85, 40, 0, 255); break; // orange
+ case MAT_GAMECLIP: color = bvec4(85, 85, 0, 255); break; // yellow
+ case MAT_DEATH: color = bvec4(40, 40, 40, 255); break; // black
+ case MAT_ALPHA: color = bvec4(85, 0, 85, 255); break; // pink
+ default: continue;
+ }
+ lastmat = m.material;
+ }
+ drawmaterial(m, -0.1f, color);
+ }
+ xtraverts += gle::end();
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+ disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+}
+
+#define GLASSVARS(name) \
+ bvec name##color(0x20, 0x80, 0xC0); \
+ HVARFR(name##colour, 0, 0x2080C0, 0xFFFFFF, \
+ { \
+ if(!name##colour) name##colour = 0x2080C0; \
+ name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \
+ });
+
+GLASSVARS(glass)
+GLASSVARS(glass2)
+GLASSVARS(glass3)
+GLASSVARS(glass4)
+
+GETMATIDXVAR(glass, colour, int)
+GETMATIDXVAR(glass, color, const bvec &)
+
+VARP(glassenv, 0, 1, 1);
+
+static void drawglass(const materialsurface &m, float offset)
+{
+ if(gle::attribbuf.empty())
+ {
+ gle::defvertex();
+ gle::defnormal(4, GL_BYTE);
+ gle::begin(GL_QUADS);
+ }
+ float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize, rsize = m.rsize;
+ switch(m.orient)
+ {
+ #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+ case orient: v0 v1 v2 v3 break;
+ #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
+ { \
+ gle::attribf(mx sx, my sy, mz sz); \
+ gle::attrib(matnormals[orient]); \
+ }
+ GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset)
+ #undef GENFACEORIENT
+ #undef GENFACEVERT
+ }
+}
+
+VARFP(waterfallenv, 0, 1, 1, preloadwatershaders());
+
+static inline void changematerial(int mat, int orient)
+{
+ switch(mat&~MATF_INDEX)
+ {
+ case MAT_LAVA:
+ if(orient==O_TOP) flushlava();
+ else xtraverts += gle::end();
+ break;
+ default:
+ xtraverts += gle::end();
+ break;
+ }
+}
+
+void rendermaterials()
+{
+ vector<materialsurface *> vismats;
+ sortmaterials(vismats);
+ if(vismats.empty()) return;
+
+ glDisable(GL_CULL_FACE);
+
+ MSlot *mslot = NULL;
+ int lastorient = -1, lastmat = -1, usedwaterfall = -1;
+ bool depth = true, blended = false;
+ ushort envmapped = EMID_NONE;
+
+ GLOBALPARAM(camera, camera1->o);
+
+ int lastfogtype = 1;
+ if(editmode && showmat && !drawtex)
+ {
+ glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
+ glEnable(GL_BLEND); blended = true;
+ foggednotextureshader->set();
+ zerofogcolor(); lastfogtype = 0;
+ bvec4 color(0, 0, 0, 0);
+ loopv(vismats)
+ {
+ const materialsurface &m = *vismats[i];
+ if(lastmat!=m.material)
+ {
+ switch(m.material&~MATF_INDEX)
+ {
+ case MAT_WATER: color = bvec4(255, 128, 0, 255); break; // blue
+ case MAT_CLIP: color = bvec4( 0, 255, 255, 255); break; // red
+ case MAT_GLASS: color = bvec4(255, 0, 0, 255); break; // cyan
+ case MAT_NOCLIP: color = bvec4(255, 0, 255, 255); break; // green
+ case MAT_LAVA: color = bvec4( 0, 128, 255, 255); break; // orange
+ case MAT_GAMECLIP: color = bvec4( 0, 0, 255, 255); break; // yellow
+ case MAT_DEATH: color = bvec4(192, 192, 192, 255); break; // black
+ case MAT_ALPHA: color = bvec4( 0, 255, 0, 255); break; // pink
+ default: continue;
+ }
+ lastmat = m.material;
+ }
+ drawmaterial(m, -0.1f, color);
+ }
+ xtraverts += gle::end();
+ }
+ else loopv(vismats)
+ {
+ const materialsurface &m = *vismats[i];
+ int matvol = m.material&~MATF_INDEX;
+ if(lastmat!=m.material || lastorient!=m.orient || (matvol==MAT_GLASS && envmapped && m.envmap != envmapped))
+ {
+ int fogtype = lastfogtype;
+ switch(matvol)
+ {
+ case MAT_WATER:
+ if(m.orient == O_TOP) continue;
+ if(lastmat == m.material) break;
+ mslot = &lookupmaterialslot(m.material, false);
+ if(!mslot->loaded || !mslot->sts.inrange(1)) continue;
+ else
+ {
+ changematerial(lastmat, lastorient);
+ glBindTexture(GL_TEXTURE_2D, mslot->sts[1].t->id);
+
+ bvec wfcol = getwaterfallcolor(m.material);
+ if(wfcol.iszero()) wfcol = getwatercolor(m.material);
+ gle::color(wfcol, 192);
+
+ int wfog = getwaterfog(m.material);
+ if(!wfog && !waterfallenv)
+ {
+ foggednotextureshader->set();
+ fogtype = 1;
+ if(blended) { glDisable(GL_BLEND); blended = false; }
+ if(!depth) { glDepthMask(GL_TRUE); depth = true; }
+ }
+ else if((!waterfallrefract || reflecting || refracting) && !waterfallenv)
+ {
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR);
+ SETSHADER(waterfall);
+ fogtype = 0;
+ if(!blended) { glEnable(GL_BLEND); blended = true; }
+ if(depth) { glDepthMask(GL_FALSE); depth = false; }
+ }
+ else
+ {
+ fogtype = 1;
+
+ if(waterfallrefract && wfog && !reflecting && !refracting)
+ {
+ if(waterfallenv) SETSHADER(waterfallenvrefract);
+ else SETSHADER(waterfallrefract);
+ if(blended) { glDisable(GL_BLEND); blended = false; }
+ if(!depth) { glDepthMask(GL_TRUE); depth = true; }
+ }
+ else
+ {
+ SETSHADER(waterfallenv);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ if(wfog)
+ {
+ if(!blended) { glEnable(GL_BLEND); blended = true; }
+ if(depth) { glDepthMask(GL_FALSE); depth = false; }
+ }
+ else
+ {
+ if(blended) { glDisable(GL_BLEND); blended = false; }
+ if(!depth) { glDepthMask(GL_TRUE); depth = true; }
+ }
+ }
+
+ if(usedwaterfall != m.material)
+ {
+ Texture *dudv = mslot->sts.inrange(5) ? mslot->sts[5].t : notexture;
+ float scale = TEX_SCALE/(dudv->ys*mslot->scale);
+ LOCALPARAMF(dudvoffset, 0, scale*16*lastmillis/1000.0f);
+
+ glActiveTexture_(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, mslot->sts.inrange(4) ? mslot->sts[4].t->id : notexture->id);
+ glActiveTexture_(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, mslot->sts.inrange(5) ? mslot->sts[5].t->id : notexture->id);
+ if(waterfallenv)
+ {
+ glActiveTexture_(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(*mslot));
+ }
+ if(waterfallrefract && (!reflecting || !refracting) && usedwaterfall < 0)
+ {
+ glActiveTexture_(GL_TEXTURE4);
+ extern void setupwaterfallrefract();
+ setupwaterfallrefract();
+ }
+ glActiveTexture_(GL_TEXTURE0);
+
+ usedwaterfall = m.material;
+ }
+ }
+ float angle = fmod(float(lastmillis/600.0f/(2*M_PI)), 1.0f),
+ s = angle - int(angle) - 0.5f;
+ s *= 8 - fabs(s)*16;
+ wfwave = vertwater ? WATER_AMPLITUDE*s-WATER_OFFSET : -WATER_OFFSET;
+ float scroll = 16.0f*lastmillis/1000.0f;
+ float xscale = TEX_SCALE/(mslot->sts[1].t->xs*mslot->scale);
+ float yscale = -TEX_SCALE/(mslot->sts[1].t->ys*mslot->scale);
+ LOCALPARAMF(waterfalltexgen, xscale, yscale, 0.0f, scroll);
+ }
+ break;
+
+ case MAT_LAVA:
+ if(lastmat==m.material && lastorient!=O_TOP && m.orient!=O_TOP) break;
+ mslot = &lookupmaterialslot(m.material, false);
+ if(!mslot->loaded) continue;
+ else
+ {
+ int subslot = m.orient==O_TOP ? 0 : 1;
+ if(!mslot->sts.inrange(subslot)) continue;
+ changematerial(lastmat, lastorient);
+ glBindTexture(GL_TEXTURE_2D, mslot->sts[subslot].t->id);
+ }
+ if(lastmat!=m.material)
+ {
+ if(!depth) { glDepthMask(GL_TRUE); depth = true; }
+ if(blended) { glDisable(GL_BLEND); blended = false; }
+ float t = lastmillis/2000.0f;
+ t -= floor(t);
+ t = 1.0f - 2*fabs(t-0.5f);
+ extern int glare;
+ if(glare) t = 0.625f + 0.075f*t;
+ else t = 0.5f + 0.5f*t;
+ gle::colorf(t, t, t);
+ if(glaring) SETSHADER(lavaglare); else SETSHADER(lava);
+ fogtype = 1;
+ }
+ if(m.orient!=O_TOP)
+ {
+ float angle = fmod(float(lastmillis/2000.0f/(2*M_PI)), 1.0f),
+ s = angle - int(angle) - 0.5f;
+ s *= 8 - fabs(s)*16;
+ wfwave = vertwater ? WATER_AMPLITUDE*s-WATER_OFFSET : -WATER_OFFSET;
+ float scroll = 16.0f*lastmillis/3000.0f;
+ float xscale = TEX_SCALE/(mslot->sts[1].t->xs*mslot->scale);
+ float yscale = -TEX_SCALE/(mslot->sts[1].t->ys*mslot->scale);
+ LOCALPARAMF(lavatexgen, xscale, yscale, 0.0f, scroll);
+ }
+ else setuplava(mslot->sts[0].t, mslot->scale);
+ break;
+
+ case MAT_GLASS:
+ if((m.envmap==EMID_NONE || !glassenv || envmapped==m.envmap) && lastmat==m.material) break;
+ changematerial(lastmat, lastorient);
+ if(m.envmap!=EMID_NONE && glassenv && envmapped!=m.envmap)
+ {
+ glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(m.envmap));
+ envmapped = m.envmap;
+ }
+ if(lastmat!=m.material)
+ {
+ if(!blended) { glEnable(GL_BLEND); blended = true; }
+ if(depth) { glDepthMask(GL_FALSE); depth = false; }
+ const bvec &gcol = getglasscolor(m.material);
+ if(m.envmap!=EMID_NONE && glassenv)
+ {
+ glBlendFunc(GL_ONE, GL_SRC_ALPHA);
+ gle::color(gcol);
+ SETSHADER(glass);
+ }
+ else
+ {
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ gle::color(gcol, 40);
+ foggednotextureshader->set();
+ fogtype = 1;
+ }
+ }
+ break;
+
+ default: continue;
+ }
+ lastmat = m.material;
+ lastorient = m.orient;
+ if(fogtype!=lastfogtype)
+ {
+ if(fogtype) resetfogcolor();
+ else zerofogcolor();
+ lastfogtype = fogtype;
+ }
+ }
+ switch(matvol)
+ {
+ case MAT_WATER:
+ renderwaterfall(m, 0.1f);
+ break;
+
+ case MAT_LAVA:
+ if(m.orient==O_TOP) renderlava(m);
+ else renderwaterfall(m, 0.1f);
+ break;
+
+ case MAT_GLASS:
+ drawglass(m, 0.1f);
+ break;
+ }
+ }
+
+ if(lastorient >= 0) changematerial(lastmat, lastorient);
+
+ if(!depth) glDepthMask(GL_TRUE);
+ if(blended) glDisable(GL_BLEND);
+ if(!lastfogtype) resetfogcolor();
+ extern int wireframe;
+ if(editmode && showmat && !drawtex && !wireframe)
+ {
+ foggednotextureshader->set();
+ rendermatgrid(vismats);
+ }
+
+ glEnable(GL_CULL_FACE);
+}
+
diff --git a/src/engine/md3.h b/src/engine/md3.h
new file mode 100644
index 0000000..d560255
--- /dev/null
+++ b/src/engine/md3.h
@@ -0,0 +1,183 @@
+struct md3;
+
+struct md3frame
+{
+ vec bbmin, bbmax, origin;
+ float radius;
+ uchar name[16];
+};
+
+struct md3tag
+{
+ char name[64];
+ float translation[3];
+ float rotation[3][3];
+};
+
+struct md3vertex
+{
+ short vertex[3];
+ short normal;
+};
+
+struct md3triangle
+{
+ int vertexindices[3];
+};
+
+struct md3header
+{
+ char id[4];
+ int version;
+ char name[64];
+ int flags;
+ int numframes, numtags, nummeshes, numskins;
+ int ofs_frames, ofs_tags, ofs_meshes, ofs_eof; // offsets
+};
+
+struct md3meshheader
+{
+ char id[4];
+ char name[64];
+ int flags;
+ int numframes, numshaders, numvertices, numtriangles;
+ int ofs_triangles, ofs_shaders, ofs_uv, ofs_vertices, meshsize; // offsets
+};
+
+struct md3 : vertloader<md3>
+{
+ md3(const char *name) : vertloader(name) {}
+
+ static const char *formatname() { return "md3"; }
+ bool flipy() const { return true; }
+ int type() const { return MDL_MD3; }
+
+ struct md3meshgroup : vertmeshgroup
+ {
+ bool load(const char *path)
+ {
+ stream *f = openfile(path, "rb");
+ if(!f) return false;
+ md3header header;
+ f->read(&header, sizeof(md3header));
+ lilswap(&header.version, 1);
+ lilswap(&header.flags, 9);
+ if(strncmp(header.id, "IDP3", 4) != 0 || header.version != 15) // header check
+ {
+ delete f;
+ conoutf(CON_ERROR, "md3: corrupted header");
+ return false;
+ }
+
+ name = newstring(path);
+
+ numframes = header.numframes;
+
+ int mesh_offset = header.ofs_meshes;
+ loopi(header.nummeshes)
+ {
+ vertmesh &m = *new vertmesh;
+ m.group = this;
+ meshes.add(&m);
+
+ md3meshheader mheader;
+ f->seek(mesh_offset, SEEK_SET);
+ f->read(&mheader, sizeof(md3meshheader));
+ lilswap(&mheader.flags, 10);
+
+ m.name = newstring(mheader.name);
+
+ m.numtris = mheader.numtriangles;
+ m.tris = new tri[m.numtris];
+ f->seek(mesh_offset + mheader.ofs_triangles, SEEK_SET);
+ loopj(m.numtris)
+ {
+ md3triangle tri;
+ f->read(&tri, sizeof(md3triangle)); // read the triangles
+ lilswap(tri.vertexindices, 3);
+ loopk(3) m.tris[j].vert[k] = (ushort)tri.vertexindices[k];
+ }
+
+ m.numverts = mheader.numvertices;
+ m.tcverts = new tcvert[m.numverts];
+ f->seek(mesh_offset + mheader.ofs_uv , SEEK_SET);
+ f->read(m.tcverts, m.numverts*2*sizeof(float)); // read the UV data
+ lilswap(&m.tcverts[0].tc.x, 2*m.numverts);
+
+ m.verts = new vert[numframes*m.numverts];
+ f->seek(mesh_offset + mheader.ofs_vertices, SEEK_SET);
+ loopj(numframes*m.numverts)
+ {
+ md3vertex v;
+ f->read(&v, sizeof(md3vertex)); // read the vertices
+ lilswap(v.vertex, 4);
+
+ m.verts[j].pos = vec(v.vertex[0]/64.0f, -v.vertex[1]/64.0f, v.vertex[2]/64.0f);
+
+ float lng = (v.normal&0xFF)*2*M_PI/255.0f; // decode vertex normals
+ float lat = ((v.normal>>8)&0xFF)*2*M_PI/255.0f;
+ m.verts[j].norm = vec(cosf(lat)*sinf(lng), -sinf(lat)*sinf(lng), cosf(lng));
+ }
+
+ mesh_offset += mheader.meshsize;
+ }
+
+ numtags = header.numtags;
+ if(numtags)
+ {
+ tags = new tag[numframes*numtags];
+ f->seek(header.ofs_tags, SEEK_SET);
+ md3tag tag;
+
+ loopi(header.numframes*header.numtags)
+ {
+ f->read(&tag, sizeof(md3tag));
+ lilswap(tag.translation, 12);
+ if(tag.name[0] && i<header.numtags) tags[i].name = newstring(tag.name);
+ matrix4x3 &m = tags[i].transform;
+ tag.translation[1] *= -1;
+ // undo the -y
+ loopj(3) tag.rotation[1][j] *= -1;
+ // then restore it
+ loopj(3) tag.rotation[j][1] *= -1;
+ m.a = vec(tag.rotation[0]);
+ m.b = vec(tag.rotation[1]);
+ m.c = vec(tag.rotation[2]);
+ m.d = vec(tag.translation);
+ }
+ }
+
+ delete f;
+ return true;
+ }
+ };
+
+ meshgroup *loadmeshes(const char *name, va_list args)
+ {
+ md3meshgroup *group = new md3meshgroup;
+ if(!group->load(name)) { delete group; return NULL; }
+ return group;
+ }
+
+ bool loaddefaultparts()
+ {
+ const char *pname = parentdir(name);
+ part &mdl = addpart();
+ defformatstring(name1, "packages/models/%s/tris.md3", name);
+ mdl.meshes = sharemeshes(path(name1));
+ if(!mdl.meshes)
+ {
+ defformatstring(name2, "packages/models/%s/tris.md3", pname); // try md3 in parent folder (vert sharing)
+ mdl.meshes = sharemeshes(path(name2));
+ if(!mdl.meshes) return false;
+ }
+ Texture *tex, *masks;
+ loadskin(name, pname, tex, masks);
+ mdl.initskins(tex, masks);
+ if(tex==notexture) conoutf(CON_ERROR, "could not load model skin for %s", name1);
+ return true;
+ }
+};
+
+vertcommands<md3> md3commands;
+
diff --git a/src/engine/md5.h b/src/engine/md5.h
new file mode 100644
index 0000000..0d59586
--- /dev/null
+++ b/src/engine/md5.h
@@ -0,0 +1,419 @@
+struct md5;
+
+struct md5joint
+{
+ vec pos;
+ quat orient;
+};
+
+struct md5weight
+{
+ int joint;
+ float bias;
+ vec pos;
+};
+
+struct md5vert
+{
+ vec2 tc;
+ ushort start, count;
+};
+
+struct md5hierarchy
+{
+ string name;
+ int parent, flags, start;
+};
+
+struct md5 : skelloader<md5>
+{
+ md5(const char *name) : skelloader(name) {}
+
+ static const char *formatname() { return "md5"; }
+ int type() const { return MDL_MD5; }
+
+ struct md5mesh : skelmesh
+ {
+ md5weight *weightinfo;
+ int numweights;
+ md5vert *vertinfo;
+
+ md5mesh() : weightinfo(NULL), numweights(0), vertinfo(NULL)
+ {
+ }
+
+ ~md5mesh()
+ {
+ cleanup();
+ }
+
+ void cleanup()
+ {
+ DELETEA(weightinfo);
+ DELETEA(vertinfo);
+ }
+
+ void buildverts(vector<md5joint> &joints)
+ {
+ loopi(numverts)
+ {
+ md5vert &v = vertinfo[i];
+ vec pos(0, 0, 0);
+ loopk(v.count)
+ {
+ md5weight &w = weightinfo[v.start+k];
+ md5joint &j = joints[w.joint];
+ vec wpos = j.orient.rotate(w.pos);
+ wpos.add(j.pos);
+ wpos.mul(w.bias);
+ pos.add(wpos);
+ }
+ vert &vv = verts[i];
+ vv.pos = pos;
+ vv.tc = v.tc;
+
+ blendcombo c;
+ int sorted = 0;
+ loopj(v.count)
+ {
+ md5weight &w = weightinfo[v.start+j];
+ sorted = c.addweight(sorted, w.bias, w.joint);
+ }
+ c.finalize(sorted);
+ vv.blend = addblendcombo(c);
+ }
+ }
+
+ void load(stream *f, char *buf, size_t bufsize)
+ {
+ md5weight w;
+ md5vert v;
+ tri t;
+ int index;
+
+ while(f->getline(buf, bufsize) && buf[0]!='}')
+ {
+ if(strstr(buf, "// meshes:"))
+ {
+ char *start = strchr(buf, ':')+1;
+ if(*start==' ') start++;
+ char *end = start + strlen(start)-1;
+ while(end >= start && isspace(*end)) end--;
+ name = newstring(start, end+1-start);
+ }
+ else if(strstr(buf, "shader"))
+ {
+ char *start = strchr(buf, '"'), *end = start ? strchr(start+1, '"') : NULL;
+ if(start && end)
+ {
+ char *texname = newstring(start+1, end-(start+1));
+ part *p = loading->parts.last();
+ p->initskins(notexture, notexture, group->meshes.length());
+ skin &s = p->skins.last();
+ s.tex = textureload(makerelpath(dir, texname), 0, true, false);
+ delete[] texname;
+ }
+ }
+ else if(sscanf(buf, " numverts %d", &numverts)==1)
+ {
+ numverts = max(numverts, 0);
+ if(numverts)
+ {
+ vertinfo = new md5vert[numverts];
+ verts = new vert[numverts];
+ }
+ }
+ else if(sscanf(buf, " numtris %d", &numtris)==1)
+ {
+ numtris = max(numtris, 0);
+ if(numtris) tris = new tri[numtris];
+ }
+ else if(sscanf(buf, " numweights %d", &numweights)==1)
+ {
+ numweights = max(numweights, 0);
+ if(numweights) weightinfo = new md5weight[numweights];
+ }
+ else if(sscanf(buf, " vert %d ( %f %f ) %hu %hu", &index, &v.tc.x, &v.tc.y, &v.start, &v.count)==5)
+ {
+ if(index>=0 && index<numverts) vertinfo[index] = v;
+ }
+ else if(sscanf(buf, " tri %d %hu %hu %hu", &index, &t.vert[0], &t.vert[1], &t.vert[2])==4)
+ {
+ if(index>=0 && index<numtris) tris[index] = t;
+ }
+ else if(sscanf(buf, " weight %d %d %f ( %f %f %f ) ", &index, &w.joint, &w.bias, &w.pos.x, &w.pos.y, &w.pos.z)==6)
+ {
+ w.pos.y = -w.pos.y;
+ if(index>=0 && index<numweights) weightinfo[index] = w;
+ }
+ }
+ }
+ };
+
+ struct md5meshgroup : skelmeshgroup
+ {
+ md5meshgroup()
+ {
+ }
+
+ bool loadmesh(const char *filename, float smooth)
+ {
+ stream *f = openfile(filename, "r");
+ if(!f) return false;
+
+ char buf[512];
+ vector<md5joint> basejoints;
+ while(f->getline(buf, sizeof(buf)))
+ {
+ int tmp;
+ if(sscanf(buf, " MD5Version %d", &tmp)==1)
+ {
+ if(tmp!=10) { delete f; return false; }
+ }
+ else if(sscanf(buf, " numJoints %d", &tmp)==1)
+ {
+ if(tmp<1) { delete f; return false; }
+ if(skel->numbones>0) continue;
+ skel->numbones = tmp;
+ skel->bones = new boneinfo[skel->numbones];
+ }
+ else if(sscanf(buf, " numMeshes %d", &tmp)==1)
+ {
+ if(tmp<1) { delete f; return false; }
+ }
+ else if(strstr(buf, "joints {"))
+ {
+ string name;
+ int parent;
+ md5joint j;
+ while(f->getline(buf, sizeof(buf)) && buf[0]!='}')
+ {
+ char *curbuf = buf, *curname = name;
+ bool allowspace = false;
+ while(*curbuf && isspace(*curbuf)) curbuf++;
+ if(*curbuf == '"') { curbuf++; allowspace = true; }
+ while(*curbuf && curname < &name[sizeof(name)-1])
+ {
+ char c = *curbuf++;
+ if(c == '"') break;
+ if(isspace(c) && !allowspace) break;
+ *curname++ = c;
+ }
+ *curname = '\0';
+ if(sscanf(curbuf, " %d ( %f %f %f ) ( %f %f %f )",
+ &parent, &j.pos.x, &j.pos.y, &j.pos.z,
+ &j.orient.x, &j.orient.y, &j.orient.z)==7)
+ {
+ j.pos.y = -j.pos.y;
+ j.orient.x = -j.orient.x;
+ j.orient.z = -j.orient.z;
+ if(basejoints.length()<skel->numbones)
+ {
+ if(!skel->bones[basejoints.length()].name)
+ skel->bones[basejoints.length()].name = newstring(name);
+ skel->bones[basejoints.length()].parent = parent;
+ }
+ j.orient.restorew();
+ basejoints.add(j);
+ }
+ }
+ if(basejoints.length()!=skel->numbones) { delete f; return false; }
+ }
+ else if(strstr(buf, "mesh {"))
+ {
+ md5mesh *m = new md5mesh;
+ m->group = this;
+ meshes.add(m);
+ m->load(f, buf, sizeof(buf));
+ if(!m->numtris || !m->numverts)
+ {
+ conoutf(CON_WARN, "empty mesh in %s", filename);
+ meshes.removeobj(m);
+ delete m;
+ }
+ }
+ }
+
+ if(skel->shared <= 1)
+ {
+ skel->linkchildren();
+ loopv(basejoints)
+ {
+ boneinfo &b = skel->bones[i];
+ b.base = dualquat(basejoints[i].orient, basejoints[i].pos);
+ (b.invbase = b.base).invert();
+ }
+ }
+
+ loopv(meshes)
+ {
+ md5mesh &m = *(md5mesh *)meshes[i];
+ m.buildverts(basejoints);
+ if(smooth <= 1) m.smoothnorms(smooth);
+ else m.buildnorms();
+ m.cleanup();
+ }
+
+ sortblendcombos();
+
+ delete f;
+ return true;
+ }
+
+ skelanimspec *loadanim(const char *filename)
+ {
+ skelanimspec *sa = skel->findskelanim(filename);
+ if(sa) return sa;
+
+ stream *f = openfile(filename, "r");
+ if(!f) return NULL;
+
+ vector<md5hierarchy> hierarchy;
+ vector<md5joint> basejoints;
+ int animdatalen = 0, animframes = 0;
+ float *animdata = NULL;
+ dualquat *animbones = NULL;
+ char buf[512];
+ while(f->getline(buf, sizeof(buf)))
+ {
+ int tmp;
+ if(sscanf(buf, " MD5Version %d", &tmp)==1)
+ {
+ if(tmp!=10) { delete f; return NULL; }
+ }
+ else if(sscanf(buf, " numJoints %d", &tmp)==1)
+ {
+ if(tmp!=skel->numbones) { delete f; return NULL; }
+ }
+ else if(sscanf(buf, " numFrames %d", &animframes)==1)
+ {
+ if(animframes<1) { delete f; return NULL; }
+ }
+ else if(sscanf(buf, " frameRate %d", &tmp)==1);
+ else if(sscanf(buf, " numAnimatedComponents %d", &animdatalen)==1)
+ {
+ if(animdatalen>0) animdata = new float[animdatalen];
+ }
+ else if(strstr(buf, "bounds {"))
+ {
+ while(f->getline(buf, sizeof(buf)) && buf[0]!='}');
+ }
+ else if(strstr(buf, "hierarchy {"))
+ {
+ while(f->getline(buf, sizeof(buf)) && buf[0]!='}')
+ {
+ md5hierarchy h;
+ if(sscanf(buf, " %100s %d %d %d", h.name, &h.parent, &h.flags, &h.start)==4)
+ hierarchy.add(h);
+ }
+ }
+ else if(strstr(buf, "baseframe {"))
+ {
+ while(f->getline(buf, sizeof(buf)) && buf[0]!='}')
+ {
+ md5joint j;
+ if(sscanf(buf, " ( %f %f %f ) ( %f %f %f )", &j.pos.x, &j.pos.y, &j.pos.z, &j.orient.x, &j.orient.y, &j.orient.z)==6)
+ {
+ j.pos.y = -j.pos.y;
+ j.orient.x = -j.orient.x;
+ j.orient.z = -j.orient.z;
+ j.orient.restorew();
+ basejoints.add(j);
+ }
+ }
+ if(basejoints.length()!=skel->numbones) { delete f; if(animdata) delete[] animdata; return NULL; }
+ animbones = new dualquat[(skel->numframes+animframes)*skel->numbones];
+ if(skel->framebones)
+ {
+ memcpy(animbones, skel->framebones, skel->numframes*skel->numbones*sizeof(dualquat));
+ delete[] skel->framebones;
+ }
+ skel->framebones = animbones;
+ animbones += skel->numframes*skel->numbones;
+
+ sa = &skel->addskelanim(filename);
+ sa->frame = skel->numframes;
+ sa->range = animframes;
+
+ skel->numframes += animframes;
+ }
+ else if(sscanf(buf, " frame %d", &tmp)==1)
+ {
+ for(int numdata = 0; f->getline(buf, sizeof(buf)) && buf[0]!='}';)
+ {
+ for(char *src = buf, *next = src; numdata < animdatalen; numdata++, src = next)
+ {
+ animdata[numdata] = strtod(src, &next);
+ if(next <= src) break;
+ }
+ }
+ dualquat *frame = &animbones[tmp*skel->numbones];
+ loopv(basejoints)
+ {
+ md5hierarchy &h = hierarchy[i];
+ md5joint j = basejoints[i];
+ if(h.start < animdatalen && h.flags)
+ {
+ float *jdata = &animdata[h.start];
+ if(h.flags&1) j.pos.x = *jdata++;
+ if(h.flags&2) j.pos.y = -*jdata++;
+ if(h.flags&4) j.pos.z = *jdata++;
+ if(h.flags&8) j.orient.x = -*jdata++;
+ if(h.flags&16) j.orient.y = *jdata++;
+ if(h.flags&32) j.orient.z = -*jdata++;
+ j.orient.restorew();
+ }
+ frame[i] = dualquat(j.orient, j.pos);
+ if(adjustments.inrange(i)) adjustments[i].adjust(frame[i]);
+ frame[i].mul(skel->bones[i].invbase);
+ if(h.parent >= 0) frame[i].mul(skel->bones[h.parent].base, dualquat(frame[i]));
+ frame[i].fixantipodal(skel->framebones[i]);
+ }
+ }
+ }
+
+ if(animdata) delete[] animdata;
+ delete f;
+
+ return sa;
+ }
+
+ bool load(const char *meshfile, float smooth)
+ {
+ name = newstring(meshfile);
+
+ if(!loadmesh(meshfile, smooth)) return false;
+
+ return true;
+ }
+ };
+
+ meshgroup *loadmeshes(const char *name, va_list args)
+ {
+ md5meshgroup *group = new md5meshgroup;
+ group->shareskeleton(va_arg(args, char *));
+ if(!group->load(name, va_arg(args, double))) { delete group; return NULL; }
+ return group;
+ }
+
+ bool loaddefaultparts()
+ {
+ skelpart &mdl = addpart();
+ mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0;
+ adjustments.setsize(0);
+ const char *fname = name + strlen(name);
+ do --fname; while(fname >= name && *fname!='/' && *fname!='\\');
+ fname++;
+ defformatstring(meshname, "packages/models/%s/%s.md5mesh", name, fname);
+ mdl.meshes = sharemeshes(path(meshname), NULL, 2.0);
+ if(!mdl.meshes) return false;
+ mdl.initanimparts();
+ mdl.initskins();
+ defformatstring(animname, "packages/models/%s/%s.md5anim", name, fname);
+ ((md5meshgroup *)mdl.meshes)->loadanim(path(animname));
+ return true;
+ }
+};
+
+skelcommands<md5> md5commands;
+
diff --git a/src/engine/menus.cpp b/src/engine/menus.cpp
new file mode 100644
index 0000000..f2d8e3b
--- /dev/null
+++ b/src/engine/menus.cpp
@@ -0,0 +1,783 @@
+// menus.cpp: ingame menu system (also used for scores and serverlist)
+
+#include "engine.h"
+
+#define GUI_TITLE_COLOR 0xFFDD88
+#define GUI_BUTTON_COLOR 0xFFFFFF
+#define GUI_TEXT_COLOR 0xDDFFDD
+
+static vec menupos;
+static int menustart = 0;
+static g3d_gui *cgui = NULL;
+
+VAR(guitabnum, 1, 0, 0);
+
+struct menu : g3d_callback
+{
+ char *name, *header;
+ uint *contents, *init, *onclear;
+ bool showtab, keeptab;
+ int menutab;
+
+ menu() : name(NULL), header(NULL), contents(NULL), init(NULL), onclear(NULL), showtab(true), keeptab(false), menutab(1) {}
+
+ void gui(g3d_gui &g, bool firstpass)
+ {
+ cgui = &g;
+ guitabnum = menutab;
+ cgui->start(menustart, 0.03f, showtab ? &menutab : NULL);
+ if(showtab) cgui->tab(header ? header : name, GUI_TITLE_COLOR);
+ execute(contents);
+ cgui->end();
+ cgui = NULL;
+ guitabnum = 0;
+ }
+
+ virtual void clear()
+ {
+ if(onclear) { freecode(onclear); onclear = NULL; }
+ }
+};
+
+struct delayedupdate
+{
+ enum
+ {
+ INT,
+ FLOAT,
+ STRING,
+ ACTION
+ } type;
+ ident *id;
+ union
+ {
+ int i;
+ float f;
+ char *s;
+ } val;
+ delayedupdate() : type(ACTION), id(NULL) { val.s = NULL; }
+ ~delayedupdate() { if(type == STRING || type == ACTION) DELETEA(val.s); }
+
+ void schedule(const char *s) { type = ACTION; val.s = newstring(s); }
+ void schedule(ident *var, int i) { type = INT; id = var; val.i = i; }
+ void schedule(ident *var, float f) { type = FLOAT; id = var; val.f = f; }
+ void schedule(ident *var, char *s) { type = STRING; id = var; val.s = newstring(s); }
+
+ int getint() const
+ {
+ switch(type)
+ {
+ case INT: return val.i;
+ case FLOAT: return int(val.f);
+ case STRING: return int(strtol(val.s, NULL, 0));
+ default: return 0;
+ }
+ }
+
+ float getfloat() const
+ {
+ switch(type)
+ {
+ case INT: return float(val.i);
+ case FLOAT: return val.f;
+ case STRING: return float(parsefloat(val.s));
+ default: return 0;
+ }
+ }
+
+ const char *getstring() const
+ {
+ switch(type)
+ {
+ case INT: return intstr(val.i);
+ case FLOAT: return intstr(int(floor(val.f)));
+ case STRING: return val.s;
+ default: return "";
+ }
+ }
+
+ void run()
+ {
+ if(type == ACTION) { if(val.s) execute(val.s); }
+ else if(id) switch(id->type)
+ {
+ case ID_VAR: setvarchecked(id, getint()); break;
+ case ID_FVAR: setfvarchecked(id, getfloat()); break;
+ case ID_SVAR: setsvarchecked(id, getstring()); break;
+ case ID_ALIAS: alias(id->name, getstring()); break;
+ }
+ }
+};
+
+static hashnameset<menu> guis;
+static vector<menu *> guistack;
+static vector<delayedupdate> updatelater;
+static bool shouldclearmenu = true, clearlater = false;
+
+VARP(menudistance, 16, 40, 256);
+VARP(menuautoclose, 32, 120, 4096);
+
+vec menuinfrontofplayer()
+{
+ vec dir;
+ vecfromyawpitch(camera1->yaw, 0, 1, 0, dir);
+ dir.mul(menudistance).add(camera1->o);
+ dir.z -= player->eyeheight-1;
+ return dir;
+}
+
+void popgui()
+{
+ menu *m = guistack.pop();
+ m->clear();
+}
+
+void removegui(menu *m)
+{
+ loopv(guistack) if(guistack[i]==m)
+ {
+ guistack.remove(i);
+ m->clear();
+ return;
+ }
+}
+
+void pushgui(menu *m, int pos = -1)
+{
+ if(guistack.empty())
+ {
+ menupos = menuinfrontofplayer();
+ g3d_resetcursor();
+ }
+ if(pos < 0) guistack.add(m);
+ else guistack.insert(pos, m);
+ if(pos < 0 || pos==guistack.length()-1)
+ {
+ if(!m->keeptab) m->menutab = 1;
+ menustart = totalmillis;
+ }
+ if(m->init) execute(m->init);
+}
+
+void restoregui(int pos)
+{
+ int clear = guistack.length()-pos-1;
+ loopi(clear) popgui();
+ menustart = totalmillis;
+}
+
+void showgui(const char *name)
+{
+ menu *m = guis.access(name);
+ if(!m) return;
+ int pos = guistack.find(m);
+ if(pos<0) pushgui(m);
+ else restoregui(pos);
+}
+
+void hidegui(const char *name)
+{
+ menu *m = guis.access(name);
+ if(m) removegui(m);
+}
+
+int cleargui(int n)
+{
+ int clear = guistack.length();
+ if(mainmenu && !isconnected(true) && clear > 0 && guistack[0]->name && !strcmp(guistack[0]->name, "main"))
+ {
+ clear--;
+ if(!clear) return 1;
+ }
+ if(n>0) clear = min(clear, n);
+ loopi(clear) popgui();
+ if(!guistack.empty()) restoregui(guistack.length()-1);
+ return clear;
+}
+
+void clearguis(int level = -1)
+{
+ if(level < 0) level = guistack.length();
+ loopvrev(guistack)
+ {
+ menu *m = guistack[i];
+ if(m->onclear)
+ {
+ uint *action = m->onclear;
+ m->onclear = NULL;
+ execute(action);
+ freecode(action);
+ }
+ }
+ cleargui(level);
+}
+
+void guionclear(char *action)
+{
+ if(guistack.empty()) return;
+ menu *m = guistack.last();
+ if(m->onclear) { freecode(m->onclear); m->onclear = NULL; }
+ if(action[0]) m->onclear = compilecode(action);
+}
+
+void guistayopen(uint *contents)
+{
+ bool oldclearmenu = shouldclearmenu;
+ shouldclearmenu = false;
+ execute(contents);
+ shouldclearmenu = oldclearmenu;
+}
+
+void guinoautotab(uint *contents)
+{
+ if(!cgui) return;
+ bool oldval = cgui->allowautotab(false);
+ execute(contents);
+ cgui->allowautotab(oldval);
+}
+
+void guimerge(uint *contents)
+{
+ if(!cgui) return;
+ bool oldval = cgui->mergehits(true);
+ execute(contents);
+ cgui->mergehits(oldval);
+}
+
+//@DOC name and icon are optional
+void guibutton(char *name, char *action, char *icon)
+{
+ if(!cgui) return;
+ bool hideicon = !strcmp(icon, "0");
+ int ret = cgui->button(name, GUI_BUTTON_COLOR, hideicon ? NULL : (icon[0] ? icon : (strstr(action, "showgui") ? "menu" : "action")));
+ if(ret&G3D_UP)
+ {
+ updatelater.add().schedule(action[0] ? action : name);
+ if(shouldclearmenu) clearlater = true;
+ }
+ else if(ret&G3D_ROLLOVER)
+ {
+ alias("guirollovername", name);
+ alias("guirolloveraction", action);
+ }
+}
+
+void guiimage(char *path, char *action, float *scale, int *overlaid, char *alt, char *title)
+{
+ if(!cgui) return;
+ Texture *t = textureload(path, 0, true, false);
+ if(t==notexture)
+ {
+ if(alt[0]) t = textureload(alt, 0, true, false);
+ if(t==notexture) return;
+ }
+ int ret = cgui->image(t, *scale, *overlaid!=0 ? title : NULL);
+ if(ret&G3D_UP)
+ {
+ if(*action)
+ {
+ updatelater.add().schedule(action);
+ if(shouldclearmenu) clearlater = true;
+ }
+ }
+ else if(ret&G3D_ROLLOVER)
+ {
+ alias("guirolloverimgpath", path);
+ alias("guirolloverimgaction", action);
+ }
+}
+
+void guicolor(int *color)
+{
+ if(cgui)
+ {
+ defformatstring(desc, "0x%06X", *color);
+ cgui->text(desc, *color, NULL);
+ }
+}
+
+void guitextbox(char *text, int *width, int *height, int *color)
+{
+ if(cgui && text[0]) cgui->textbox(text, *width ? *width : 12, *height ? *height : 1, *color ? *color : 0xFFFFFF);
+}
+
+void guitext(char *name, char *icon)
+{
+ bool hideicon = !strcmp(icon, "0");
+ if(cgui) cgui->text(name, !hideicon && icon[0] ? GUI_BUTTON_COLOR : GUI_TEXT_COLOR, hideicon ? NULL : (icon[0] ? icon : "info"));
+}
+
+void guititle(char *name)
+{
+ if(cgui) cgui->title(name, GUI_TITLE_COLOR);
+}
+
+void guitab(char *name)
+{
+ if(cgui) cgui->tab(name, GUI_TITLE_COLOR);
+}
+
+void guibar()
+{
+ if(cgui) cgui->separator();
+}
+
+void guistrut(float *strut, int *alt)
+{
+ if(cgui)
+ {
+ if(*alt) cgui->strut(*strut); else cgui->space(*strut);
+ }
+}
+
+void guispring(int *weight)
+{
+ if(cgui) cgui->spring(max(*weight, 1));
+}
+
+void guicolumn(int *col)
+{
+ if(cgui) cgui->column(*col);
+}
+
+template<class T> static void updateval(char *var, T val, char *onchange)
+{
+ ident *id = writeident(var);
+ updatelater.add().schedule(id, val);
+ if(onchange[0]) updatelater.add().schedule(onchange);
+}
+
+static int getval(char *var)
+{
+ ident *id = readident(var);
+ if(!id) return 0;
+ switch(id->type)
+ {
+ case ID_VAR: return *id->storage.i;
+ case ID_FVAR: return int(*id->storage.f);
+ case ID_SVAR: return parseint(*id->storage.s);
+ case ID_ALIAS: return id->getint();
+ default: return 0;
+ }
+}
+
+static float getfval(char *var)
+{
+ ident *id = readident(var);
+ if(!id) return 0;
+ switch(id->type)
+ {
+ case ID_VAR: return *id->storage.i;
+ case ID_FVAR: return *id->storage.f;
+ case ID_SVAR: return parsefloat(*id->storage.s);
+ case ID_ALIAS: return id->getfloat();
+ default: return 0;
+ }
+}
+
+static const char *getsval(char *var)
+{
+ ident *id = readident(var);
+ if(!id) return "";
+ switch(id->type)
+ {
+ case ID_VAR: return intstr(*id->storage.i);
+ case ID_FVAR: return floatstr(*id->storage.f);
+ case ID_SVAR: return *id->storage.s;
+ case ID_ALIAS: return id->getstr();
+ default: return "";
+ }
+}
+
+void guislider(char *var, int *min, int *max, char *onchange)
+{
+ if(!cgui) return;
+ int oldval = getval(var), val = oldval, vmin = *max > INT_MIN ? *min : getvarmin(var), vmax = *max > INT_MIN ? *max : getvarmax(var);
+ cgui->slider(val, vmin, vmax, GUI_TITLE_COLOR);
+ if(val != oldval) updateval(var, val, onchange);
+}
+
+void guilistslider(char *var, char *list, char *onchange)
+{
+ if(!cgui) return;
+ vector<int> vals;
+ list += strspn(list, "\n\t ");
+ while(*list)
+ {
+ vals.add(parseint(list));
+ list += strcspn(list, "\n\t \0");
+ list += strspn(list, "\n\t ");
+ }
+ if(vals.empty()) return;
+ int val = getval(var), oldoffset = vals.length()-1, offset = oldoffset;
+ loopv(vals) if(val <= vals[i]) { oldoffset = offset = i; break; }
+ cgui->slider(offset, 0, vals.length()-1, GUI_TITLE_COLOR, intstr(val));
+ if(offset != oldoffset) updateval(var, vals[offset], onchange);
+}
+
+void guinameslider(char *var, char *names, char *list, char *onchange)
+{
+ if(!cgui) return;
+ vector<int> vals;
+ list += strspn(list, "\n\t ");
+ while(*list)
+ {
+ vals.add(parseint(list));
+ list += strcspn(list, "\n\t \0");
+ list += strspn(list, "\n\t ");
+ }
+ if(vals.empty()) return;
+ int val = getval(var), oldoffset = vals.length()-1, offset = oldoffset;
+ loopv(vals) if(val <= vals[i]) { oldoffset = offset = i; break; }
+ char *label = indexlist(names, offset);
+ cgui->slider(offset, 0, vals.length()-1, GUI_TITLE_COLOR, label);
+ if(offset != oldoffset) updateval(var, vals[offset], onchange);
+ delete[] label;
+}
+
+void guicheckbox(char *name, char *var, float *on, float *off, char *onchange)
+{
+ bool enabled = getfval(var)!=*off;
+ if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "checkbox_on" : "checkbox_off")&G3D_UP)
+ {
+ updateval(var, enabled ? *off : (*on || *off ? *on : 1.0f), onchange);
+ }
+}
+
+void guiradio(char *name, char *var, float *n, char *onchange)
+{
+ bool enabled = getfval(var)==*n;
+ if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "radio_on" : "radio_off")&G3D_UP)
+ {
+ if(!enabled) updateval(var, *n, onchange);
+ }
+}
+
+void guibitfield(char *name, char *var, int *mask, char *onchange)
+{
+ int val = getval(var);
+ bool enabled = (val & *mask) != 0;
+ if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "checkbox_on" : "checkbox_off")&G3D_UP)
+ {
+ updateval(var, enabled ? val & ~*mask : val | *mask, onchange);
+ }
+}
+
+//-ve length indicates a wrapped text field of any (approx 260 chars) length, |length| is the field width
+void guifield(char *var, int *maxlength, char *onchange)
+{
+ if(!cgui) return;
+ const char *initval = getsval(var);
+ char *result = cgui->field(var, GUI_BUTTON_COLOR, *maxlength ? *maxlength : 12, 0, initval);
+ if(result) updateval(var, result, onchange);
+}
+
+//-ve maxlength indicates a wrapped text field of any (approx 260 chars) length, |maxlength| is the field width
+void guieditor(char *name, int *maxlength, int *height, int *mode)
+{
+ if(!cgui) return;
+ cgui->field(name, GUI_BUTTON_COLOR, *maxlength ? *maxlength : 12, *height, NULL, *mode<=0 ? EDITORFOREVER : *mode);
+ //returns a non-NULL pointer (the currentline) when the user commits, could then manipulate via text* commands
+}
+
+//-ve length indicates a wrapped text field of any (approx 260 chars) length, |length| is the field width
+void guikeyfield(char *var, int *maxlength, char *onchange)
+{
+ if(!cgui) return;
+ const char *initval = getsval(var);
+ char *result = cgui->keyfield(var, GUI_BUTTON_COLOR, *maxlength ? *maxlength : -8, 0, initval);
+ if(result) updateval(var, result, onchange);
+}
+
+//use text<action> to do more...
+
+
+void guilist(uint *contents)
+{
+ if(!cgui) return;
+ cgui->pushlist();
+ execute(contents);
+ cgui->poplist();
+}
+
+void guialign(int *align, uint *contents)
+{
+ if(!cgui) return;
+ cgui->pushlist();
+ if(*align >= 0) cgui->spring();
+ execute(contents);
+ if(*align == 0) cgui->spring();
+ cgui->poplist();
+}
+
+void newgui(char *name, char *contents, char *header, char *init)
+{
+ menu *m = guis.access(name);
+ if(!m)
+ {
+ name = newstring(name);
+ m = &guis[name];
+ m->name = name;
+ }
+ else
+ {
+ DELETEA(m->header);
+ freecode(m->contents);
+ freecode(m->init);
+ }
+ if(header && header[0])
+ {
+ char *end = NULL;
+ int val = strtol(header, &end, 0);
+ if(end && !*end)
+ {
+ m->header = NULL;
+ m->showtab = val != 0;
+ }
+ else
+ {
+ m->header = newstring(header);
+ m->showtab = true;
+ }
+ }
+ else
+ {
+ m->header = NULL;
+ m->showtab = true;
+ }
+ m->contents = compilecode(contents);
+ m->init = init && init[0] ? compilecode(init) : NULL;
+}
+
+menu *guiserversmenu = NULL;
+
+void guiservers(uint *header, int *pagemin, int *pagemax)
+{
+ extern const char *showservers(g3d_gui *cgui, uint *header, int pagemin, int pagemax);
+ if(cgui)
+ {
+ const char *command = showservers(cgui, header, *pagemin, *pagemax > 0 ? *pagemax : INT_MAX);
+ if(command)
+ {
+ updatelater.add().schedule(command);
+ if(shouldclearmenu) clearlater = true;
+ guiserversmenu = clearlater || guistack.empty() ? NULL : guistack.last();
+ }
+ }
+}
+
+void notifywelcome()
+{
+ if(guiserversmenu)
+ {
+ if(guistack.length() && guistack.last() == guiserversmenu) clearguis();
+ guiserversmenu = NULL;
+ }
+}
+
+COMMAND(newgui, "ssss");
+COMMAND(guibutton, "sss");
+COMMAND(guitext, "ss");
+COMMAND(guiservers, "eii");
+ICOMMAND(cleargui, "i", (int *n), intret(cleargui(*n)));
+COMMAND(showgui, "s");
+COMMAND(hidegui, "s");
+COMMAND(guionclear, "s");
+COMMAND(guistayopen, "e");
+COMMAND(guinoautotab, "e");
+COMMAND(guimerge, "e");
+ICOMMAND(guikeeptab, "b", (int *keeptab), if(guistack.length()) guistack.last()->keeptab = *keeptab!=0);
+COMMAND(guilist, "e");
+COMMAND(guialign, "ie");
+COMMAND(guititle, "s");
+COMMAND(guibar,"");
+COMMAND(guistrut,"fi");
+COMMAND(guispring, "i");
+COMMAND(guicolumn, "i");
+COMMAND(guiimage,"ssfiss");
+COMMAND(guislider,"sbbs");
+COMMAND(guilistslider, "sss");
+COMMAND(guinameslider, "ssss");
+COMMAND(guiradio,"ssfs");
+COMMAND(guibitfield, "ssis");
+COMMAND(guicheckbox, "ssffs");
+COMMAND(guitab, "s");
+COMMAND(guifield, "sis");
+COMMAND(guikeyfield, "sis");
+COMMAND(guieditor, "siii");
+COMMAND(guicolor, "i");
+COMMAND(guitextbox, "siii");
+
+void guiplayerpreview(int *model, int *team, int *weap, char *action, float *scale, int *overlaid, char *title)
+{
+ if(!cgui) return;
+ int ret = cgui->playerpreview(*model, *team, *weap, *scale, *overlaid!=0 ? title : NULL);
+ if(ret&G3D_UP)
+ {
+ if(*action)
+ {
+ updatelater.add().schedule(action);
+ if(shouldclearmenu) clearlater = true;
+ }
+ }
+}
+COMMAND(guiplayerpreview, "iiisfis");
+
+void guimodelpreview(char *model, char *animspec, char *action, float *scale, int *overlaid, char *title, int *throttle)
+{
+ if(!cgui) return;
+ int anim = ANIM_ALL;
+ if(animspec[0])
+ {
+ if(isdigit(animspec[0]))
+ {
+ anim = parseint(animspec);
+ if(anim >= 0) anim %= ANIM_INDEX;
+ else anim = ANIM_ALL;
+ }
+ else
+ {
+ vector<int> anims;
+ findanims(animspec, anims);
+ if(anims.length()) anim = anims[0];
+ }
+ }
+ int ret = cgui->modelpreview(model, anim|ANIM_LOOP, *scale, *overlaid!=0 ? title : NULL, *throttle!=0);
+ if(ret&G3D_UP)
+ {
+ if(*action)
+ {
+ updatelater.add().schedule(action);
+ if(shouldclearmenu) clearlater = true;
+ }
+ }
+ else if(ret&G3D_ROLLOVER)
+ {
+ alias("guirolloverpreviewname", model);
+ alias("guirolloverpreviewaction", action);
+ }
+}
+COMMAND(guimodelpreview, "sssfisi");
+
+void guiprefabpreview(char *prefab, int *color, char *action, float *scale, int *overlaid, char *title, int *throttle)
+{
+ if(!cgui) return;
+ int ret = cgui->prefabpreview(prefab, vec::hexcolor(*color), *scale, *overlaid!=0 ? title : NULL, *throttle!=0);
+ if(ret&G3D_UP)
+ {
+ if(*action)
+ {
+ updatelater.add().schedule(action);
+ if(shouldclearmenu) clearlater = true;
+ }
+ }
+ else if(ret&G3D_ROLLOVER)
+ {
+ alias("guirolloverpreviewname", prefab);
+ alias("guirolloverpreviewaction", action);
+ }
+}
+COMMAND(guiprefabpreview, "sisfisi");
+
+struct change
+{
+ int type;
+ const char *desc;
+
+ change() {}
+ change(int type, const char *desc) : type(type), desc(desc) {}
+};
+static vector<change> needsapply;
+
+static struct applymenu : menu
+{
+ void gui(g3d_gui &g, bool firstpass)
+ {
+ if(guistack.empty()) return;
+ g.start(menustart, 0.03f);
+ g.text("the following settings have changed:", GUI_TEXT_COLOR, "info");
+ loopv(needsapply) g.text(needsapply[i].desc, GUI_TEXT_COLOR, "info");
+ g.separator();
+ g.text("apply changes now?", GUI_TEXT_COLOR, "info");
+ if(g.button("yes", GUI_BUTTON_COLOR, "action")&G3D_UP)
+ {
+ int changetypes = 0;
+ loopv(needsapply) changetypes |= needsapply[i].type;
+ if(changetypes&CHANGE_GFX) updatelater.add().schedule("resetgl");
+ if(changetypes&CHANGE_SOUND) updatelater.add().schedule("resetsound");
+ clearlater = true;
+ }
+ if(g.button("no", GUI_BUTTON_COLOR, "action")&G3D_UP)
+ clearlater = true;
+ g.end();
+ }
+
+ void clear()
+ {
+ menu::clear();
+ needsapply.shrink(0);
+ }
+} applymenu;
+
+VARP(applydialog, 0, 1, 1);
+
+static bool processingmenu = false;
+
+void addchange(const char *desc, int type)
+{
+ if(!applydialog) return;
+ loopv(needsapply) if(!strcmp(needsapply[i].desc, desc)) return;
+ needsapply.add(change(type, desc));
+ if(needsapply.length() && guistack.find(&applymenu) < 0)
+ pushgui(&applymenu, processingmenu ? max(guistack.length()-1, 0) : -1);
+}
+
+void clearchanges(int type)
+{
+ loopv(needsapply)
+ {
+ if(needsapply[i].type&type)
+ {
+ needsapply[i].type &= ~type;
+ if(!needsapply[i].type) needsapply.remove(i--);
+ }
+ }
+ if(needsapply.empty()) removegui(&applymenu);
+}
+
+void menuprocess()
+{
+ processingmenu = true;
+ int wasmain = mainmenu, level = guistack.length();
+ loopv(updatelater) updatelater[i].run();
+ updatelater.shrink(0);
+ if(wasmain > mainmenu || clearlater)
+ {
+ if(wasmain > mainmenu || level==guistack.length()) clearguis(level);
+ clearlater = false;
+ }
+ if(mainmenu && !isconnected(true) && guistack.empty()) showgui("main");
+ processingmenu = false;
+}
+
+VAR(mainmenu, 1, 1, 0);
+
+void clearmainmenu()
+{
+ if(mainmenu && isconnected())
+ {
+ mainmenu = 0;
+ if(!processingmenu) cleargui();
+ }
+}
+
+void g3d_mainmenu()
+{
+ if(!guistack.empty())
+ {
+ extern int usegui2d;
+ if(!mainmenu && !usegui2d && camera1->o.dist(menupos) > menuautoclose) cleargui();
+ else g3d_addgui(guistack.last(), menupos, GUI_2D | GUI_FOLLOW);
+ }
+}
+
diff --git a/src/engine/model.h b/src/engine/model.h
new file mode 100644
index 0000000..97264bd
--- /dev/null
+++ b/src/engine/model.h
@@ -0,0 +1,90 @@
+enum { MDL_MD2 = 0, MDL_MD3, MDL_MD5, MDL_OBJ, MDL_SMD, MDL_IQM, NUMMODELTYPES };
+
+struct model
+{
+ char *name;
+ float spinyaw, spinpitch, offsetyaw, offsetpitch;
+ bool collide, ellipsecollide, shadow, alphadepth, depthoffset;
+ float scale;
+ vec translate;
+ BIH *bih;
+ vec bbcenter, bbradius, bbextend, collidecenter, collideradius;
+ float rejectradius, eyeheight, collidexyradius, collideheight;
+ int batch;
+
+ model(const char *name) : name(name ? newstring(name) : NULL), spinyaw(0), spinpitch(0), offsetyaw(0), offsetpitch(0), collide(true), ellipsecollide(false), shadow(true), alphadepth(true), depthoffset(false), scale(1.0f), translate(0, 0, 0), bih(0), bbcenter(0, 0, 0), bbradius(-1, -1, -1), bbextend(0, 0, 0), collidecenter(0, 0, 0), collideradius(-1, -1, -1), rejectradius(-1), eyeheight(0.9f), collidexyradius(0), collideheight(0), batch(-1) {}
+ virtual ~model() { DELETEA(name); DELETEP(bih); }
+ virtual void calcbb(vec &center, vec &radius) = 0;
+ virtual void render(int anim, int basetime, int basetime2, const vec &o, float yaw, float pitch, dynent *d, modelattach *a = NULL, const vec &color = vec(0, 0, 0), const vec &dir = vec(0, 0, 0), float transparent = 1) = 0;
+ virtual bool load() = 0;
+ virtual int type() const = 0;
+ virtual BIH *setBIH() { return 0; }
+ virtual bool envmapped() { return false; }
+ virtual bool skeletal() const { return false; }
+
+ virtual void setshader(Shader *shader) {}
+ virtual void setenvmap(float envmapmin, float envmapmax, Texture *envmap) {}
+ virtual void setspec(float spec) {}
+ virtual void setambient(float ambient) {}
+ virtual void setglow(float glow, float glowdelta, float glowpulse) {}
+ virtual void setglare(float specglare, float glowglare) {}
+ virtual void setalphatest(float alpha) {}
+ virtual void setalphablend(bool blend) {}
+ virtual void setfullbright(float fullbright) {}
+ virtual void setcullface(bool cullface) {}
+
+ virtual void preloadBIH() { if(!bih) setBIH(); }
+ virtual void preloadshaders(bool force = false) {}
+ virtual void preloadmeshes() {}
+ virtual void cleanup() {}
+
+ virtual void startrender() {}
+ virtual void endrender() {}
+
+ void boundbox(vec &center, vec &radius)
+ {
+ if(bbradius.x < 0)
+ {
+ calcbb(bbcenter, bbradius);
+ bbradius.add(bbextend);
+ }
+ center = bbcenter;
+ radius = bbradius;
+ }
+
+ float collisionbox(vec &center, vec &radius)
+ {
+ if(collideradius.x < 0)
+ {
+ boundbox(collidecenter, collideradius);
+ if(collidexyradius)
+ {
+ collidecenter.x = collidecenter.y = 0;
+ collideradius.x = collideradius.y = collidexyradius;
+ }
+ if(collideheight)
+ {
+ collidecenter.z = collideradius.z = collideheight/2;
+ }
+ rejectradius = vec(collidecenter).abs().add(collideradius).magnitude();
+ }
+ center = collidecenter;
+ radius = collideradius;
+ return rejectradius;
+ }
+
+ float boundsphere(vec &center)
+ {
+ vec radius;
+ boundbox(center, radius);
+ return radius.magnitude();
+ }
+
+ float above()
+ {
+ vec center, radius;
+ boundbox(center, radius);
+ return center.z+radius.z;
+ }
+};
+
diff --git a/src/engine/movie.cpp b/src/engine/movie.cpp
new file mode 100644
index 0000000..25cb491
--- /dev/null
+++ b/src/engine/movie.cpp
@@ -0,0 +1,1157 @@
+// Feedback on playing videos:
+// quicktime - ok
+// vlc - ok
+// xine - ok
+// mplayer - ok
+// totem - ok
+// avidemux - ok - 3Apr09-RockKeyman:had to swap UV channels as it showed up blue
+// kino - ok
+
+#include "engine.h"
+#include "SDL_mixer.h"
+
+VAR(dbgmovie, 0, 0, 1);
+
+struct aviindexentry
+{
+ int frame, type, size;
+ uint offset;
+
+ aviindexentry() {}
+ aviindexentry(int frame, int type, int size, uint offset) : frame(frame), type(type), size(size), offset(offset) {}
+};
+
+struct avisegmentinfo
+{
+ stream::offset offset, videoindexoffset, soundindexoffset;
+ int firstindex;
+ uint videoindexsize, soundindexsize, indexframes, videoframes, soundframes;
+
+ avisegmentinfo() {}
+ avisegmentinfo(stream::offset offset, int firstindex) : offset(offset), videoindexoffset(0), soundindexoffset(0), firstindex(firstindex), videoindexsize(0), soundindexsize(0), indexframes(0), videoframes(0), soundframes(0) {}
+};
+
+struct aviwriter
+{
+ stream *f;
+ uchar *yuv;
+ uint videoframes;
+ stream::offset totalsize;
+ const uint videow, videoh, videofps;
+ string filename;
+
+ int soundfrequency, soundchannels;
+ Uint16 soundformat;
+
+ vector<aviindexentry> index;
+ vector<avisegmentinfo> segments;
+
+ stream::offset fileframesoffset, fileextframesoffset, filevideooffset, filesoundoffset, superindexvideooffset, superindexsoundoffset;
+
+ enum { MAX_CHUNK_DEPTH = 16, MAX_SUPER_INDEX = 1024 };
+ stream::offset chunkoffsets[MAX_CHUNK_DEPTH];
+ int chunkdepth;
+
+ aviindexentry &addindex(int frame, int type, int size)
+ {
+ avisegmentinfo &seg = segments.last();
+ int i = index.length();
+ while(--i >= seg.firstindex)
+ {
+ aviindexentry &e = index[i];
+ if(frame > e.frame || (frame == e.frame && type <= e.type)) break;
+ }
+ return index.insert(i + 1, aviindexentry(frame, type, size, uint(totalsize - chunkoffsets[chunkdepth])));
+ }
+
+ double filespaceguess()
+ {
+ return double(totalsize);
+ }
+
+ void startchunk(const char *fcc, uint size = 0)
+ {
+ f->write(fcc, 4);
+ f->putlil<uint>(size);
+ totalsize += 4 + 4;
+ chunkoffsets[++chunkdepth] = totalsize;
+ totalsize += size;
+ }
+
+ void listchunk(const char *fcc, const char *lfcc)
+ {
+ startchunk(fcc);
+ f->write(lfcc, 4);
+ totalsize += 4;
+ }
+
+ void endchunk()
+ {
+ ASSERT(chunkdepth >= 0);
+ --chunkdepth;
+ }
+
+ void endlistchunk()
+ {
+ ASSERT(chunkdepth >= 0);
+ int size = int(totalsize - chunkoffsets[chunkdepth]);
+ f->seek(-4 - size, SEEK_CUR);
+ f->putlil(size);
+ f->seek(0, SEEK_END);
+ if(size & 1) { f->putchar(0x00); totalsize++; }
+ endchunk();
+ }
+
+ void writechunk(const char *fcc, const void *data, uint len) // simplify startchunk()/endchunk() to avoid f->seek()
+ {
+ f->write(fcc, 4);
+ f->putlil(len);
+ f->write(data, len);
+ totalsize += 4 + 4 + len;
+ if(len & 1) { f->putchar(0x00); totalsize++; }
+ }
+
+ void close()
+ {
+ if(!f) return;
+ flushsegment();
+
+ uint soundindexes = 0, videoindexes = 0, soundframes = 0, videoframes = 0, indexframes = 0;
+ loopv(segments)
+ {
+ avisegmentinfo &seg = segments[i];
+ if(seg.soundindexsize) soundindexes++;
+ videoindexes++;
+ soundframes += seg.soundframes;
+ videoframes += seg.videoframes;
+ indexframes += seg.indexframes;
+ }
+ if(dbgmovie) conoutf(CON_DEBUG, "fileframes: sound=%d, video=%d+%d(dups)\n", soundframes, videoframes, indexframes-videoframes);
+ f->seek(fileframesoffset, SEEK_SET);
+ f->putlil<uint>(segments[0].indexframes);
+ f->seek(filevideooffset, SEEK_SET);
+ f->putlil<uint>(segments[0].videoframes);
+ if(segments[0].soundframes > 0)
+ {
+ f->seek(filesoundoffset, SEEK_SET);
+ f->putlil<uint>(segments[0].soundframes);
+ }
+ f->seek(fileextframesoffset, SEEK_SET);
+ f->putlil<uint>(indexframes); // total video frames
+
+ f->seek(superindexvideooffset + 2 + 2, SEEK_SET);
+ f->putlil<uint>(videoindexes);
+ f->seek(superindexvideooffset + 2 + 2 + 4 + 4 + 4 + 4 + 4, SEEK_SET);
+ loopv(segments)
+ {
+ avisegmentinfo &seg = segments[i];
+ f->putlil<uint>(seg.videoindexoffset&stream::offset(0xFFFFFFFFU));
+ f->putlil<uint>(seg.videoindexoffset>>32);
+ f->putlil<uint>(seg.videoindexsize);
+ f->putlil<uint>(seg.indexframes);
+ }
+
+ if(soundindexes > 0)
+ {
+ f->seek(superindexsoundoffset + 2 + 2, SEEK_SET);
+ f->putlil<uint>(soundindexes);
+ f->seek(superindexsoundoffset + 2 + 2 + 4 + 4 + 4 + 4 + 4, SEEK_SET);
+ loopv(segments)
+ {
+ avisegmentinfo &seg = segments[i];
+ if(!seg.soundindexsize) continue;
+ f->putlil<uint>(seg.soundindexoffset&stream::offset(0xFFFFFFFFU));
+ f->putlil<uint>(seg.soundindexoffset>>32);
+ f->putlil<uint>(seg.soundindexsize);
+ f->putlil<uint>(seg.soundframes);
+ }
+ }
+
+ f->seek(0, SEEK_END);
+
+ DELETEP(f);
+ }
+
+ aviwriter(const char *name, uint w, uint h, uint fps, bool sound) : f(NULL), yuv(NULL), videoframes(0), totalsize(0), videow(w&~1), videoh(h&~1), videofps(fps), soundfrequency(0),soundchannels(0),soundformat(0)
+ {
+ copystring(filename, name);
+ path(filename);
+ if(!strrchr(filename, '.')) concatstring(filename, ".avi");
+
+ extern bool nosound; // sound.cpp
+ if(sound && !nosound)
+ {
+ Mix_QuerySpec(&soundfrequency, &soundformat, &soundchannels);
+ const char *desc;
+ switch(soundformat)
+ {
+ case AUDIO_U8: desc = "u8"; break;
+ case AUDIO_S8: desc = "s8"; break;
+ case AUDIO_U16LSB: desc = "u16l"; break;
+ case AUDIO_U16MSB: desc = "u16b"; break;
+ case AUDIO_S16LSB: desc = "s16l"; break;
+ case AUDIO_S16MSB: desc = "s16b"; break;
+ default: desc = "unkn";
+ }
+ if(dbgmovie) conoutf(CON_DEBUG, "soundspec: %dhz %s x %d", soundfrequency, desc, soundchannels);
+ }
+ }
+
+ ~aviwriter()
+ {
+ close();
+ if(yuv) delete [] yuv;
+ }
+
+ bool open()
+ {
+ f = openfile(filename, "wb");
+ if(!f) return false;
+
+ chunkdepth = -1;
+
+ listchunk("RIFF", "AVI ");
+
+ listchunk("LIST", "hdrl");
+
+ startchunk("avih", 56);
+ f->putlil<uint>(1000000 / videofps); // microsecsperframe
+ f->putlil<uint>(0); // maxbytespersec
+ f->putlil<uint>(0); // reserved
+ f->putlil<uint>(0x10 | 0x20); // flags - hasindex|mustuseindex
+ fileframesoffset = f->tell();
+ f->putlil<uint>(0); // totalvideoframes
+ f->putlil<uint>(0); // initialframes
+ f->putlil<uint>(soundfrequency > 0 ? 2 : 1); // streams
+ f->putlil<uint>(0); // buffersize
+ f->putlil<uint>(videow); // video width
+ f->putlil<uint>(videoh); // video height
+ loopi(4) f->putlil<uint>(0); // reserved
+ endchunk(); // avih
+
+ listchunk("LIST", "strl");
+
+ startchunk("strh", 56);
+ f->write("vids", 4); // fcctype
+ f->write("I420", 4); // fcchandler
+ f->putlil<uint>(0); // flags
+ f->putlil<uint>(0); // priority
+ f->putlil<uint>(0); // initialframes
+ f->putlil<uint>(1); // scale
+ f->putlil<uint>(videofps); // rate
+ f->putlil<uint>(0); // start
+ filevideooffset = f->tell();
+ f->putlil<uint>(0); // length
+ f->putlil<uint>(videow*videoh*3/2); // suggested buffersize
+ f->putlil<uint>(0); // quality
+ f->putlil<uint>(0); // samplesize
+ f->putlil<ushort>(0); // frame left
+ f->putlil<ushort>(0); // frame top
+ f->putlil<ushort>(videow); // frame right
+ f->putlil<ushort>(videoh); // frame bottom
+ endchunk(); // strh
+
+ startchunk("strf", 40);
+ f->putlil<uint>(40); //headersize
+ f->putlil<uint>(videow); // width
+ f->putlil<uint>(videoh); // height
+ f->putlil<ushort>(3); // planes
+ f->putlil<ushort>(12); // bitcount
+ f->write("I420", 4); // compression
+ f->putlil<uint>(videow*videoh*3/2); // imagesize
+ f->putlil<uint>(0); // xres
+ f->putlil<uint>(0); // yres;
+ f->putlil<uint>(0); // colorsused
+ f->putlil<uint>(0); // colorsrequired
+ endchunk(); // strf
+
+ startchunk("indx", 24 + 16*MAX_SUPER_INDEX);
+ superindexvideooffset = f->tell();
+ f->putlil<ushort>(4); // longs per entry
+ f->putlil<ushort>(0); // index of indexes
+ f->putlil<uint>(0); // entries in use
+ f->write("00dc", 4); // chunk id
+ f->putlil<uint>(0); // reserved 1
+ f->putlil<uint>(0); // reserved 2
+ f->putlil<uint>(0); // reserved 3
+ loopi(MAX_SUPER_INDEX)
+ {
+ f->putlil<uint>(0); // offset low
+ f->putlil<uint>(0); // offset high
+ f->putlil<uint>(0); // size
+ f->putlil<uint>(0); // duration
+ }
+ endchunk(); // indx
+
+ startchunk("vprp", 68);
+ f->putlil<uint>(0); // video format token
+ f->putlil<uint>(0); // video standard
+ f->putlil<uint>(videofps); // vertical refresh rate
+ f->putlil<uint>(videow); // horizontal total
+ f->putlil<uint>(videoh); // vertical total
+ int gcd = screenw, rem = screenh;
+ while(rem > 0) { gcd %= rem; swap(gcd, rem); }
+ f->putlil<ushort>(screenh/gcd); // aspect denominator
+ f->putlil<ushort>(screenw/gcd); // aspect numerator
+ f->putlil<uint>(videow); // frame width
+ f->putlil<uint>(videoh); // frame height
+ f->putlil<uint>(1); // fields per frame
+ f->putlil<uint>(videoh); // compressed bitmap height
+ f->putlil<uint>(videow); // compressed bitmap width
+ f->putlil<uint>(videoh); // valid bitmap height
+ f->putlil<uint>(videow); // valid bitmap width
+ f->putlil<uint>(0); // valid bitmap x offset
+ f->putlil<uint>(0); // valid bitmap y offset
+ f->putlil<uint>(0); // video x offset
+ f->putlil<uint>(0); // video y start
+ endchunk(); // vprp
+
+ endlistchunk(); // LIST strl
+
+ if(soundfrequency > 0)
+ {
+ const int bps = (soundformat==AUDIO_U8 || soundformat == AUDIO_S8) ? 1 : 2;
+
+ listchunk("LIST", "strl");
+
+ startchunk("strh", 56);
+ f->write("auds", 4); // fcctype
+ f->putlil<uint>(1); // fcchandler - normally 4cc, but audio is a special case
+ f->putlil<uint>(0); // flags
+ f->putlil<uint>(0); // priority
+ f->putlil<uint>(0); // initialframes
+ f->putlil<uint>(1); // scale
+ f->putlil<uint>(soundfrequency); // rate
+ f->putlil<uint>(0); // start
+ filesoundoffset = f->tell();
+ f->putlil<uint>(0); // length
+ f->putlil<uint>(soundfrequency*bps*soundchannels/2); // suggested buffer size (this is a half second)
+ f->putlil<uint>(0); // quality
+ f->putlil<uint>(bps*soundchannels); // samplesize
+ f->putlil<ushort>(0); // frame left
+ f->putlil<ushort>(0); // frame top
+ f->putlil<ushort>(0); // frame right
+ f->putlil<ushort>(0); // frame bottom
+ endchunk(); // strh
+
+ startchunk("strf", 18);
+ f->putlil<ushort>(1); // format (uncompressed PCM)
+ f->putlil<ushort>(soundchannels); // channels
+ f->putlil<uint>(soundfrequency); // sampleframes per second
+ f->putlil<uint>(soundfrequency*bps*soundchannels); // average bytes per second
+ f->putlil<ushort>(bps*soundchannels); // block align <-- guess
+ f->putlil<ushort>(bps*8); // bits per sample
+ f->putlil<ushort>(0); // size
+ endchunk(); //strf
+
+ startchunk("indx", 24 + 16*MAX_SUPER_INDEX);
+ superindexsoundoffset = f->tell();
+ f->putlil<ushort>(4); // longs per entry
+ f->putlil<ushort>(0); // index of indexes
+ f->putlil<uint>(0); // entries in use
+ f->write("01wb", 4); // chunk id
+ f->putlil<uint>(0); // reserved 1
+ f->putlil<uint>(0); // reserved 2
+ f->putlil<uint>(0); // reserved 3
+ loopi(MAX_SUPER_INDEX)
+ {
+ f->putlil<uint>(0); // offset low
+ f->putlil<uint>(0); // offset high
+ f->putlil<uint>(0); // size
+ f->putlil<uint>(0); // duration
+ }
+ endchunk(); // indx
+
+ endlistchunk(); // LIST strl
+ }
+
+ listchunk("LIST", "odml");
+ startchunk("dmlh", 4);
+ fileextframesoffset = f->tell();
+ f->putlil<uint>(0);
+ endchunk(); // dmlh
+ endlistchunk(); // LIST odml
+
+ listchunk("LIST", "INFO");
+ const char *software = "Cube 2: Sauerbraten";
+ writechunk("ISFT", software, strlen(software)+1);
+ endlistchunk(); // LIST INFO
+
+ endlistchunk(); // LIST hdrl
+
+ nextsegment();
+
+ return true;
+ }
+
+ static inline void boxsample(const uchar *src, const uint stride,
+ const uint area, const uint w, uint h,
+ const uint xlow, const uint xhigh, const uint ylow, const uint yhigh,
+ uint &bdst, uint &gdst, uint &rdst)
+ {
+ const uchar *end = &src[w<<2];
+ uint bt = 0, gt = 0, rt = 0;
+ for(const uchar *cur = &src[4]; cur < end; cur += 4)
+ {
+ bt += cur[0];
+ gt += cur[1];
+ rt += cur[2];
+ }
+ bt = ylow*(bt + ((src[0]*xlow + end[0]*xhigh)>>12));
+ gt = ylow*(gt + ((src[1]*xlow + end[1]*xhigh)>>12));
+ rt = ylow*(rt + ((src[2]*xlow + end[2]*xhigh)>>12));
+ if(h)
+ {
+ for(src += stride, end += stride; --h; src += stride, end += stride)
+ {
+ uint b = 0, g = 0, r = 0;
+ for(const uchar *cur = &src[4]; cur < end; cur += 4)
+ {
+ b += cur[0];
+ g += cur[1];
+ r += cur[2];
+ }
+ bt += (b<<12) + src[0]*xlow + end[0]*xhigh;
+ gt += (g<<12) + src[1]*xlow + end[1]*xhigh;
+ rt += (r<<12) + src[2]*xlow + end[2]*xhigh;
+ }
+ uint b = 0, g = 0, r = 0;
+ for(const uchar *cur = &src[4]; cur < end; cur += 4)
+ {
+ b += cur[0];
+ g += cur[1];
+ r += cur[2];
+ }
+ bt += yhigh*(b + ((src[0]*xlow + end[0]*xhigh)>>12));
+ gt += yhigh*(g + ((src[1]*xlow + end[1]*xhigh)>>12));
+ rt += yhigh*(r + ((src[2]*xlow + end[2]*xhigh)>>12));
+ }
+ bdst = (bt*area)>>24;
+ gdst = (gt*area)>>24;
+ rdst = (rt*area)>>24;
+ }
+
+ void scaleyuv(const uchar *pixels, uint srcw, uint srch)
+ {
+ const int flip = -1;
+ const uint planesize = videow * videoh;
+ if(!yuv) yuv = new uchar[(planesize*3)/2];
+ uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
+ const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
+ if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
+
+ const uint stride = srcw<<2;
+ srcw &= ~1;
+ srch &= ~1;
+ const uint wfrac = (srcw<<12)/videow, hfrac = (srch<<12)/videoh,
+ area = ((ullong)planesize<<12)/(srcw*srch + 1),
+ dw = videow*wfrac, dh = videoh*hfrac;
+
+ for(uint y = 0; y < dh;)
+ {
+ uint yn = y + hfrac - 1, yi = y>>12, h = (yn>>12) - yi, ylow = ((yn|(-int(h)>>24))&0xFFFU) + 1 - (y&0xFFFU), yhigh = (yn&0xFFFU) + 1;
+ y += hfrac;
+ uint y2n = y + hfrac - 1, y2i = y>>12, h2 = (y2n>>12) - y2i, y2low = ((y2n|(-int(h2)>>24))&0xFFFU) + 1 - (y&0xFFFU), y2high = (y2n&0xFFFU) + 1;
+ y += hfrac;
+
+ const uchar *src = &pixels[yi*stride], *src2 = &pixels[y2i*stride];
+ uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
+ for(uint x = 0; x < dw;)
+ {
+ uint xn = x + wfrac - 1, xi = x>>12, w = (xn>>12) - xi, xlow = ((w+0xFFFU)&0x1000U) - (x&0xFFFU), xhigh = (xn&0xFFFU) + 1;
+ x += wfrac;
+ uint x2n = x + wfrac - 1, x2i = x>>12, w2 = (x2n>>12) - x2i, x2low = ((w2+0xFFFU)&0x1000U) - (x&0xFFFU), x2high = (x2n&0xFFFU) + 1;
+ x += wfrac;
+
+ uint b1, g1, r1, b2, g2, r2, b3, g3, r3, b4, g4, r4;
+ boxsample(&src[xi<<2], stride, area, w, h, xlow, xhigh, ylow, yhigh, b1, g1, r1);
+ boxsample(&src[x2i<<2], stride, area, w2, h, x2low, x2high, ylow, yhigh, b2, g2, r2);
+ boxsample(&src2[xi<<2], stride, area, w, h2, xlow, xhigh, y2low, y2high, b3, g3, r3);
+ boxsample(&src2[x2i<<2], stride, area, w2, h2, x2low, x2high, y2low, y2high, b4, g4, r4);
+
+
+ // Y = 16 + 65.481*R + 128.553*G + 24.966*B
+ // Cb = 128 - 37.797*R - 74.203*G + 112.0*B
+ // Cr = 128 + 112.0*R - 93.786*G - 18.214*B
+ *ydst++ = ((16<<12) + 1052*r1 + 2065*g1 + 401*b1)>>12;
+ *ydst++ = ((16<<12) + 1052*r2 + 2065*g2 + 401*b2)>>12;
+ *ydst2++ = ((16<<12) + 1052*r3 + 2065*g3 + 401*b3)>>12;;
+ *ydst2++ = ((16<<12) + 1052*r4 + 2065*g4 + 401*b4)>>12;;
+
+ const uint b = b1 + b2 + b3 + b4,
+ g = g1 + g2 + g3 + g4,
+ r = r1 + r2 + r3 + r4;
+ // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4
+ *udst++ = ((128<<12) - 152*r - 298*g + 450*b)>>12;
+ *vdst++ = ((128<<12) + 450*r - 377*g - 73*b)>>12;
+ }
+
+ yplane += 2*ystride;
+ uplane += uvstride;
+ vplane += uvstride;
+ }
+ }
+
+ void encodeyuv(const uchar *pixels)
+ {
+ const int flip = -1;
+ const uint planesize = videow * videoh;
+ if(!yuv) yuv = new uchar[(planesize*3)/2];
+ uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
+ const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
+ if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
+
+ const uint stride = videow<<2;
+ const uchar *src = pixels, *yend = src + videoh*stride;
+ while(src < yend)
+ {
+ const uchar *src2 = src + stride, *xend = src2;
+ uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
+ while(src < xend)
+ {
+ const uint b1 = src[0], g1 = src[1], r1 = src[2],
+ b2 = src[4], g2 = src[5], r2 = src[6],
+ b3 = src2[0], g3 = src2[1], r3 = src2[2],
+ b4 = src2[4], g4 = src2[5], r4 = src2[6];
+
+ // Y = 16 + 65.481*R + 128.553*G + 24.966*B
+ // Cb = 128 - 37.797*R - 74.203*G + 112.0*B
+ // Cr = 128 + 112.0*R - 93.786*G - 18.214*B
+ *ydst++ = ((16<<12) + 1052*r1 + 2065*g1 + 401*b1)>>12;
+ *ydst++ = ((16<<12) + 1052*r2 + 2065*g2 + 401*b2)>>12;
+ *ydst2++ = ((16<<12) + 1052*r3 + 2065*g3 + 401*b3)>>12;;
+ *ydst2++ = ((16<<12) + 1052*r4 + 2065*g4 + 401*b4)>>12;;
+
+ const uint b = b1 + b2 + b3 + b4,
+ g = g1 + g2 + g3 + g4,
+ r = r1 + r2 + r3 + r4;
+ // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4
+ *udst++ = ((128<<12) - 152*r - 298*g + 450*b)>>12;
+ *vdst++ = ((128<<12) + 450*r - 377*g - 73*b)>>12;
+
+ src += 8;
+ src2 += 8;
+ }
+ src = src2;
+ yplane += 2*ystride;
+ uplane += uvstride;
+ vplane += uvstride;
+ }
+ }
+
+ void compressyuv(const uchar *pixels)
+ {
+ const int flip = -1;
+ const uint planesize = videow * videoh;
+ if(!yuv) yuv = new uchar[(planesize*3)/2];
+ uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
+ const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
+ if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
+
+ const uint stride = videow<<2;
+ const uchar *src = pixels, *yend = src + videoh*stride;
+ while(src < yend)
+ {
+ const uchar *src2 = src + stride, *xend = src2;
+ uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
+ while(src < xend)
+ {
+ *ydst++ = src[0];
+ *ydst++ = src[4];
+ *ydst2++ = src2[0];
+ *ydst2++ = src2[4];
+
+ *udst++ = (uint(src[1]) + uint(src[5]) + uint(src2[1]) + uint(src2[5])) >> 2;
+ *vdst++ = (uint(src[2]) + uint(src[6]) + uint(src2[2]) + uint(src2[6])) >> 2;
+
+ src += 8;
+ src2 += 8;
+ }
+ src = src2;
+ yplane += 2*ystride;
+ uplane += uvstride;
+ vplane += uvstride;
+ }
+ }
+
+ bool writesound(uchar *data, uint framesize, uint frame)
+ {
+ // do conversion in-place to little endian format
+ // note that xoring by half the range yields the same bit pattern as subtracting the range regardless of signedness
+ // ... so can toggle signedness just by xoring the high byte with 0x80
+ switch(soundformat)
+ {
+ case AUDIO_U8:
+ for(uchar *dst = data, *end = &data[framesize]; dst < end; dst++) *dst ^= 0x80;
+ break;
+ case AUDIO_S8:
+ break;
+ case AUDIO_U16LSB:
+ for(uchar *dst = &data[1], *end = &data[framesize]; dst < end; dst += 2) *dst ^= 0x80;
+ break;
+ case AUDIO_U16MSB:
+ for(ushort *dst = (ushort *)data, *end = (ushort *)&data[framesize]; dst < end; dst++)
+#if SDL_BYTEORDER == SDL_BIG_ENDIAN
+ *dst = endianswap(*dst) ^ 0x0080;
+#else
+ *dst = endianswap(*dst) ^ 0x8000;
+#endif
+ break;
+ case AUDIO_S16LSB:
+ break;
+ case AUDIO_S16MSB:
+ endianswap((short *)data, framesize/2);
+ break;
+ }
+
+ if(totalsize - segments.last().offset + framesize > 1000*1000*1000 && !nextsegment()) return false;
+
+ addindex(frame, 1, framesize);
+
+ writechunk("01wb", data, framesize);
+
+ return true;
+ }
+
+
+ enum
+ {
+ VID_RGB = 0,
+ VID_YUV,
+ VID_YUV420
+ };
+
+ void flushsegment()
+ {
+ endlistchunk(); // LIST movi
+
+ avisegmentinfo &seg = segments.last();
+
+ uint indexframes = 0, videoframes = 0, soundframes = 0;
+ for(int i = seg.firstindex; i < index.length(); i++)
+ {
+ aviindexentry &e = index[i];
+ if(e.type) soundframes++;
+ else
+ {
+ if(i == seg.firstindex || e.offset != index[i-1].offset)
+ videoframes++;
+ indexframes++;
+ }
+ }
+
+ if(segments.length() == 1)
+ {
+ startchunk("idx1", index.length()*16);
+ loopv(index)
+ {
+ aviindexentry &entry = index[i];
+ // printf("%3d %s %08x\n", i, (entry.type==1)?"s":"v", entry.offset);
+ f->write(entry.type ? "01wb" : "00dc", 4); // chunkid
+ f->putlil<uint>(0x10); // flags - KEYFRAME
+ f->putlil<uint>(entry.offset); // offset (relative to movi)
+ f->putlil<uint>(entry.size); // size
+ }
+ endchunk();
+ }
+
+ seg.videoframes = videoframes;
+ seg.videoindexoffset = totalsize;
+ startchunk("ix00", 24 + indexframes*8);
+ f->putlil<ushort>(2); // longs per entry
+ f->putlil<ushort>(0x0100); // index of chunks
+ f->putlil<uint>(indexframes); // entries in use
+ f->write("00dc", 4); // chunk id
+ f->putlil<uint>(seg.offset&stream::offset(0xFFFFFFFFU)); // offset low
+ f->putlil<uint>(seg.offset>>32); // offset high
+ f->putlil<uint>(0); // reserved 3
+ for(int i = seg.firstindex; i < index.length(); i++)
+ {
+ aviindexentry &e = index[i];
+ if(e.type) continue;
+ f->putlil<uint>(e.offset + 4 + 4);
+ f->putlil<uint>(e.size);
+ }
+ endchunk(); // ix00
+ seg.videoindexsize = uint(totalsize - seg.videoindexoffset);
+
+ if(soundframes)
+ {
+ seg.soundframes = soundframes;
+ seg.soundindexoffset = totalsize;
+ startchunk("ix01", 24 + soundframes*8);
+ f->putlil<ushort>(2); // longs per entry
+ f->putlil<ushort>(0x0100); // index of chunks
+ f->putlil<uint>(soundframes); // entries in use
+ f->write("01wb", 4); // chunk id
+ f->putlil<uint>(seg.offset&stream::offset(0xFFFFFFFFU)); // offset low
+ f->putlil<uint>(seg.offset>>32); // offset high
+ f->putlil<uint>(0); // reserved 3
+ for(int i = seg.firstindex; i < index.length(); i++)
+ {
+ aviindexentry &e = index[i];
+ if(!e.type) continue;
+ f->putlil<uint>(e.offset + 4 + 4);
+ f->putlil<uint>(e.size);
+ }
+ endchunk(); // ix01
+ seg.soundindexsize = uint(totalsize - seg.soundindexoffset);
+ }
+
+ endlistchunk(); // RIFF AVI/AVIX
+ }
+
+ bool nextsegment()
+ {
+ if(segments.length())
+ {
+ if(segments.length() >= MAX_SUPER_INDEX) return false;
+ flushsegment();
+ listchunk("RIFF", "AVIX");
+ }
+ listchunk("LIST", "movi");
+ segments.add(avisegmentinfo(chunkoffsets[chunkdepth], index.length()));
+ return true;
+ }
+
+ bool writevideoframe(const uchar *pixels, uint srcw, uint srch, int format, uint frame)
+ {
+ if(frame < videoframes) return true;
+
+ switch(format)
+ {
+ case VID_RGB:
+ if(srcw != videow || srch != videoh) scaleyuv(pixels, srcw, srch);
+ else encodeyuv(pixels);
+ break;
+ case VID_YUV:
+ compressyuv(pixels);
+ break;
+ }
+
+ const uint framesize = (videow * videoh * 3) / 2;
+ if(totalsize - segments.last().offset + framesize > 1000*1000*1000 && !nextsegment()) return false;
+
+ while(videoframes <= frame) addindex(videoframes++, 0, framesize);
+
+ writechunk("00dc", format == VID_YUV420 ? pixels : yuv, framesize);
+
+ return true;
+ }
+
+};
+
+VAR(movieaccelblit, 0, 0, 1);
+VAR(movieaccelyuv, 0, 1, 1);
+VARP(movieaccel, 0, 1, 1);
+VARP(moviesync, 0, 0, 1);
+FVARP(movieminquality, 0, 0, 1);
+
+namespace recorder
+{
+ static enum { REC_OK = 0, REC_USERHALT, REC_TOOSLOW, REC_FILERROR } state = REC_OK;
+
+ static aviwriter *file = NULL;
+ static int starttime = 0;
+
+ static int stats[1000];
+ static int statsindex = 0;
+ static uint dps = 0; // dropped frames per sample
+
+ enum { MAXSOUNDBUFFERS = 128 }; // sounds queue up until there is a video frame, so at low fps you'll need a bigger queue
+ struct soundbuffer
+ {
+ uchar *sound;
+ uint size, maxsize;
+ uint frame;
+
+ soundbuffer() : sound(NULL), maxsize(0) {}
+ ~soundbuffer() { cleanup(); }
+
+ void load(uchar *stream, uint len, uint fnum)
+ {
+ if(len > maxsize)
+ {
+ DELETEA(sound);
+ sound = new uchar[len];
+ maxsize = len;
+ }
+ size = len;
+ frame = fnum;
+ memcpy(sound, stream, len);
+ }
+
+ void cleanup() { DELETEA(sound); maxsize = 0; }
+ };
+ static queue<soundbuffer, MAXSOUNDBUFFERS> soundbuffers;
+ static SDL_mutex *soundlock = NULL;
+
+ enum { MAXVIDEOBUFFERS = 2 }; // double buffer
+ struct videobuffer
+ {
+ uchar *video;
+ uint w, h, bpp, frame;
+ int format;
+
+ videobuffer() : video(NULL){}
+ ~videobuffer() { cleanup(); }
+
+ void init(int nw, int nh, int nbpp)
+ {
+ DELETEA(video);
+ w = nw;
+ h = nh;
+ bpp = nbpp;
+ video = new uchar[w*h*bpp];
+ format = -1;
+ }
+
+ void cleanup() { DELETEA(video); }
+ };
+ static queue<videobuffer, MAXVIDEOBUFFERS> videobuffers;
+ static uint lastframe = ~0U;
+
+ static GLuint scalefb = 0, scaletex[2] = { 0, 0 };
+ static uint scalew = 0, scaleh = 0;
+ static GLuint encodefb = 0, encoderb = 0;
+
+ static SDL_Thread *thread = NULL;
+ static SDL_mutex *videolock = NULL;
+ static SDL_cond *shouldencode = NULL, *shouldread = NULL;
+
+ bool isrecording() { return file != NULL; }
+
+ float calcquality()
+ {
+ return 1.0f - float(dps)/float(dps+file->videofps); // strictly speaking should lock to read dps - 1.0=perfect, 0.5=half of frames are beingdropped
+ }
+
+ int gettime()
+ {
+ return inbetweenframes ? getclockmillis() : totalmillis;
+ }
+
+ int videoencoder(void *data) // runs on a separate thread
+ {
+ for(int numvid = 0, numsound = 0;;)
+ {
+ SDL_LockMutex(videolock);
+ for(; numvid > 0; numvid--) videobuffers.remove();
+ SDL_CondSignal(shouldread);
+ while(videobuffers.empty() && state == REC_OK) SDL_CondWait(shouldencode, videolock);
+ if(state != REC_OK) { SDL_UnlockMutex(videolock); break; }
+ videobuffer &m = videobuffers.removing();
+ numvid++;
+ SDL_UnlockMutex(videolock);
+
+ if(file->soundfrequency > 0)
+ {
+ // chug data from lock protected buffer to avoid holding lock while writing to file
+ SDL_LockMutex(soundlock);
+ for(; numsound > 0; numsound--) soundbuffers.remove();
+ for(; numsound < soundbuffers.length(); numsound++)
+ {
+ soundbuffer &s = soundbuffers.removing(numsound);
+ if(s.frame > m.frame) break; // sync with video
+ }
+ SDL_UnlockMutex(soundlock);
+ loopi(numsound)
+ {
+ soundbuffer &s = soundbuffers.removing(i);
+ if(!file->writesound(s.sound, s.size, s.frame)) state = REC_FILERROR;
+ }
+ }
+
+ int duplicates = m.frame - (int)file->videoframes + 1;
+ if(duplicates > 0) // determine how many frames have been dropped over the sample window
+ {
+ dps -= stats[statsindex];
+ stats[statsindex] = duplicates-1;
+ dps += stats[statsindex];
+ statsindex = (statsindex+1)%file->videofps;
+ }
+ //printf("frame %d->%d (%d dps): sound = %d bytes\n", file->videoframes, nextframenum, dps, m.soundlength);
+ if(calcquality() < movieminquality) state = REC_TOOSLOW;
+ else if(!file->writevideoframe(m.video, m.w, m.h, m.format, m.frame)) state = REC_FILERROR;
+
+ m.frame = ~0U;
+ }
+
+ return 0;
+ }
+
+ void soundencoder(void *udata, Uint8 *stream, int len) // callback occurs on a separate thread
+ {
+ SDL_LockMutex(soundlock);
+ if(soundbuffers.full())
+ {
+ if(movieminquality >= 1) state = REC_TOOSLOW;
+ }
+ else if(state == REC_OK)
+ {
+ uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000;
+ soundbuffer &s = soundbuffers.add();
+ s.load((uchar *)stream, len, nextframe);
+ }
+ SDL_UnlockMutex(soundlock);
+ }
+
+ void start(const char *filename, int videofps, int videow, int videoh, bool sound)
+ {
+ if(file) return;
+
+ useshaderbyname("moviergb");
+ useshaderbyname("movieyuv");
+ useshaderbyname("moviey");
+ useshaderbyname("movieu");
+ useshaderbyname("moviev");
+
+ int fps, bestdiff, worstdiff;
+ getfps(fps, bestdiff, worstdiff);
+ if(videofps > fps) conoutf(CON_WARN, "frame rate may be too low to capture at %d fps", videofps);
+
+ if(videow%2) videow += 1;
+ if(videoh%2) videoh += 1;
+
+ file = new aviwriter(filename, videow, videoh, videofps, sound);
+ if(!file->open())
+ {
+ conoutf(CON_ERROR, "unable to create file %s", filename);
+ DELETEP(file);
+ return;
+ }
+ conoutf("movie recording to: %s %dx%d @ %dfps%s", file->filename, file->videow, file->videoh, file->videofps, (file->soundfrequency>0)?" + sound":"");
+
+ starttime = gettime();
+ loopi(file->videofps) stats[i] = 0;
+ statsindex = 0;
+ dps = 0;
+
+ lastframe = ~0U;
+ videobuffers.clear();
+ loopi(MAXVIDEOBUFFERS)
+ {
+ uint w = screenw, h = screenw;
+ videobuffers.data[i].init(w, h, 4);
+ videobuffers.data[i].frame = ~0U;
+ }
+
+ soundbuffers.clear();
+
+ soundlock = SDL_CreateMutex();
+ videolock = SDL_CreateMutex();
+ shouldencode = SDL_CreateCond();
+ shouldread = SDL_CreateCond();
+ thread = SDL_CreateThread(videoencoder, "video encoder", NULL);
+ if(file->soundfrequency > 0) Mix_SetPostMix(soundencoder, NULL);
+ }
+
+ void cleanup()
+ {
+ if(scalefb) { glDeleteFramebuffers_(1, &scalefb); scalefb = 0; }
+ if(scaletex[0] || scaletex[1]) { glDeleteTextures(2, scaletex); memset(scaletex, 0, sizeof(scaletex)); }
+ scalew = scaleh = 0;
+ if(encodefb) { glDeleteFramebuffers_(1, &encodefb); encodefb = 0; }
+ if(encoderb) { glDeleteRenderbuffers_(1, &encoderb); encoderb = 0; }
+ }
+
+ void stop()
+ {
+ if(!file) return;
+ if(state == REC_OK) state = REC_USERHALT;
+ if(file->soundfrequency > 0) Mix_SetPostMix(NULL, NULL);
+
+ SDL_LockMutex(videolock); // wakeup thread enough to kill it
+ SDL_CondSignal(shouldencode);
+ SDL_UnlockMutex(videolock);
+
+ SDL_WaitThread(thread, NULL); // block until thread is finished
+
+ cleanup();
+
+ loopi(MAXVIDEOBUFFERS) videobuffers.data[i].cleanup();
+ loopi(MAXSOUNDBUFFERS) soundbuffers.data[i].cleanup();
+
+ SDL_DestroyMutex(soundlock);
+ SDL_DestroyMutex(videolock);
+ SDL_DestroyCond(shouldencode);
+ SDL_DestroyCond(shouldread);
+
+ soundlock = videolock = NULL;
+ shouldencode = shouldread = NULL;
+ thread = NULL;
+
+ static const char * const mesgs[] = { "ok", "stopped", "computer too slow", "file error"};
+ conoutf("movie recording halted: %s, %d frames", mesgs[state], file->videoframes);
+
+ DELETEP(file);
+ state = REC_OK;
+ }
+
+ void readbuffer(videobuffer &m, uint nextframe)
+ {
+ bool accelyuv = movieaccelyuv && !(m.w%8),
+ usefbo = movieaccel && file->videow <= (uint)screenw && file->videoh <= (uint)screenh && (accelyuv || file->videow < (uint)screenw || file->videoh < (uint)screenh);
+ uint w = screenw, h = screenh;
+ if(usefbo) { w = file->videow; h = file->videoh; }
+ if(w != m.w || h != m.h) m.init(w, h, 4);
+ m.format = aviwriter::VID_RGB;
+ m.frame = nextframe;
+
+ glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w, 4));
+ if(usefbo)
+ {
+ uint tw = screenw, th = screenh;
+ if(hasFBB && movieaccelblit) { tw = max(tw/2, m.w); th = max(th/2, m.h); }
+ if(tw != scalew || th != scaleh)
+ {
+ if(!scalefb) glGenFramebuffers_(1, &scalefb);
+ loopi(2)
+ {
+ if(!scaletex[i]) glGenTextures(1, &scaletex[i]);
+ createtexture(scaletex[i], tw, th, NULL, 3, 1, GL_RGB);
+ }
+ scalew = tw;
+ scaleh = th;
+ }
+ if(accelyuv && (!encodefb || !encoderb))
+ {
+ if(!encodefb) glGenFramebuffers_(1, &encodefb);
+ glBindFramebuffer_(GL_FRAMEBUFFER, encodefb);
+ if(!encoderb) glGenRenderbuffers_(1, &encoderb);
+ glBindRenderbuffer_(GL_RENDERBUFFER, encoderb);
+ glRenderbufferStorage_(GL_RENDERBUFFER, GL_RGBA, (m.w*3)/8, m.h);
+ glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, encoderb);
+ glBindRenderbuffer_(GL_RENDERBUFFER, 0);
+ glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+ }
+
+ if(tw < (uint)screenw || th < (uint)screenh)
+ {
+ glBindFramebuffer_(GL_READ_FRAMEBUFFER, 0);
+ glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, scalefb);
+ glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scaletex[0], 0);
+ glBlitFramebuffer_(0, 0, screenw, screenh, 0, 0, tw, th, GL_COLOR_BUFFER_BIT, GL_LINEAR);
+ glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, 0);
+ }
+ else
+ {
+ glBindTexture(GL_TEXTURE_2D, scaletex[0]);
+ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh);
+ }
+
+ GLOBALPARAMF(moviescale, 1.0f/scalew, 1.0f/scaleh);
+ if(tw > m.w || th > m.h || (!accelyuv && tw >= m.w && th >= m.h))
+ {
+ glBindFramebuffer_(GL_FRAMEBUFFER, scalefb);
+ do
+ {
+ uint dw = max(tw/2, m.w), dh = max(th/2, m.h);
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scaletex[1], 0);
+ glViewport(0, 0, dw, dh);
+ glBindTexture(GL_TEXTURE_2D, scaletex[0]);
+ if(dw == m.w && dh == m.h && !accelyuv) { SETSHADER(movieyuv); m.format = aviwriter::VID_YUV; }
+ else SETSHADER(moviergb);
+ screenquad(tw/float(scalew), th/float(scaleh));
+ tw = dw;
+ th = dh;
+ swap(scaletex[0], scaletex[1]);
+ } while(tw > m.w || th > m.h);
+ }
+ if(accelyuv)
+ {
+ glBindFramebuffer_(GL_FRAMEBUFFER, encodefb);
+ glBindTexture(GL_TEXTURE_2D, scaletex[0]);
+ glViewport(0, 0, m.w/4, m.h); SETSHADER(moviey); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
+ glViewport(m.w/4, 0, m.w/8, m.h/2); SETSHADER(movieu); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
+ glViewport(m.w/4, m.h/2, m.w/8, m.h/2); SETSHADER(moviev); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
+ const uint planesize = m.w * m.h;
+ glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w/4, 4));
+ glReadPixels(0, 0, m.w/4, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
+ glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize], m.w/8, 4));
+ glReadPixels(m.w/4, 0, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize]);
+ glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize + planesize/4], m.w/8, 4));
+ glReadPixels(m.w/4, m.h/2, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize + planesize/4]);
+ m.format = aviwriter::VID_YUV420;
+ }
+ else
+ {
+ glBindFramebuffer_(GL_FRAMEBUFFER, scalefb);
+ glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
+ }
+ glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+ glViewport(0, 0, screenw, screenh);
+
+ }
+ else glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
+ }
+
+ bool readbuffer()
+ {
+ if(!file) return false;
+ if(state != REC_OK)
+ {
+ stop();
+ return false;
+ }
+ SDL_LockMutex(videolock);
+ if(moviesync && videobuffers.full()) SDL_CondWait(shouldread, videolock);
+ uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000;
+ if(!videobuffers.full() && (lastframe == ~0U || nextframe > lastframe))
+ {
+ videobuffer &m = videobuffers.adding();
+ SDL_UnlockMutex(videolock);
+ readbuffer(m, nextframe);
+ SDL_LockMutex(videolock);
+ lastframe = nextframe;
+ videobuffers.add();
+ SDL_CondSignal(shouldencode);
+ }
+ SDL_UnlockMutex(videolock);
+ return true;
+ }
+
+ void drawhud()
+ {
+ int w = screenw, h = screenh;
+ if(forceaspect) w = int(ceil(h*forceaspect));
+ gettextres(w, h);
+
+ hudmatrix.ortho(0, w, h, 0, -1, 1);
+ hudmatrix.scale(1/3.0f, 1/3.0f, 1);
+ resethudmatrix();
+ hudshader->set();
+
+ glEnable(GL_BLEND);
+
+ double totalsize = file->filespaceguess();
+ const char *unit = "KB";
+ if(totalsize >= 1e9) { totalsize /= 1e9; unit = "GB"; }
+ else if(totalsize >= 1e6) { totalsize /= 1e6; unit = "MB"; }
+ else totalsize /= 1e3;
+
+ draw_textf("recorded %.1f%s %d%%", w*3-10*FONTH, h*3-FONTH-FONTH*3/2, totalsize, unit, int(calcquality()*100));
+
+ glDisable(GL_BLEND);
+ }
+
+ void capture(bool overlay)
+ {
+ if(readbuffer() && overlay) drawhud();
+ }
+}
+
+VARP(moview, 0, 320, 10000);
+VARP(movieh, 0, 240, 10000);
+VARP(moviefps, 1, 24, 1000);
+VARP(moviesound, 0, 1, 1);
+
+void movie(char *name)
+{
+ if(name[0] == '\0') recorder::stop();
+ else if(!recorder::isrecording()) recorder::start(name, moviefps, moview ? moview : screenw, movieh ? movieh : screenh, moviesound!=0);
+}
+
+COMMAND(movie, "s");
+ICOMMAND(movierecording, "", (), intret(recorder::isrecording() ? 1 : 0));
+
diff --git a/src/engine/mpr.h b/src/engine/mpr.h
new file mode 100644
index 0000000..b4cfb59
--- /dev/null
+++ b/src/engine/mpr.h
@@ -0,0 +1,575 @@
+// This code is based off the Minkowski Portal Refinement algorithm by Gary Snethen in XenoCollide & Game Programming Gems 7.
+
+namespace mpr
+{
+ struct CubePlanes
+ {
+ const clipplanes &p;
+
+ CubePlanes(const clipplanes &p) : p(p) {}
+
+ vec center() const { return p.o; }
+
+ vec supportpoint(const vec &n) const
+ {
+ int besti = 7;
+ float bestd = n.dot(p.v[7]);
+ loopi(7)
+ {
+ float d = n.dot(p.v[i]);
+ if(d > bestd) { besti = i; bestd = d; }
+ }
+ return p.v[besti];
+ }
+ };
+
+ struct SolidCube
+ {
+ vec o;
+ int size;
+
+ SolidCube(float x, float y, float z, int size) : o(x, y, z), size(size) {}
+ SolidCube(const vec &o, int size) : o(o), size(size) {}
+ SolidCube(const ivec &o, int size) : o(o), size(size) {}
+
+ vec center() const { return vec(o).add(size/2); }
+
+ vec supportpoint(const vec &n) const
+ {
+ vec p(o);
+ if(n.x > 0) p.x += size;
+ if(n.y > 0) p.y += size;
+ if(n.z > 0) p.z += size;
+ return p;
+ }
+ };
+
+ struct Ent
+ {
+ physent *ent;
+
+ Ent(physent *ent) : ent(ent) {}
+
+ vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight)/2); }
+ };
+
+ struct EntOBB : Ent
+ {
+ matrix3 orient;
+ float zmargin;
+
+ EntOBB(physent *ent, float zmargin = 0) : Ent(ent), zmargin(zmargin)
+ {
+ orient.setyaw(ent->yaw*RAD);
+ }
+
+ vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight - zmargin)/2); }
+
+ vec contactface(const vec &wn, const vec &wdir) const
+ {
+ vec n = orient.transform(wn).div(vec(ent->xradius, ent->yradius, (ent->aboveeye + ent->eyeheight + zmargin)/2)),
+ dir = orient.transform(wdir),
+ an(fabs(n.x), fabs(n.y), dir.z ? fabs(n.z) : 0),
+ fn(0, 0, 0);
+ if(an.x > an.y)
+ {
+ if(an.x > an.z) fn.x = n.x*dir.x < 0 ? (n.x > 0 ? 1 : -1) : 0;
+ else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+ }
+ else if(an.y > an.z) fn.y = n.y*dir.y < 0 ? (n.y > 0 ? 1 : -1) : 0;
+ else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+ return orient.transposedtransform(fn);
+ }
+
+ vec localsupportpoint(const vec &ln) const
+ {
+ return vec(ln.x > 0 ? ent->xradius : -ent->xradius,
+ ln.y > 0 ? ent->yradius : -ent->yradius,
+ ln.z > 0 ? ent->aboveeye : -ent->eyeheight - zmargin);
+ }
+
+ vec supportpoint(const vec &n) const
+ {
+ return orient.transposedtransform(localsupportpoint(orient.transform(n))).add(ent->o);
+ }
+
+ float supportcoordneg(float a, float b, float c) const
+ {
+ return localsupportpoint(vec(-a, -b, -c)).dot(vec(a, b, c));
+ }
+ float supportcoord(float a, float b, float c) const
+ {
+ return localsupportpoint(vec(a, b, c)).dot(vec(a, b, c));
+ }
+
+ float left() const { return supportcoordneg(orient.a.x, orient.b.x, orient.c.x) + ent->o.x; }
+ float right() const { return supportcoord(orient.a.x, orient.b.x, orient.c.x) + ent->o.x; }
+ float back() const { return supportcoordneg(orient.a.y, orient.b.y, orient.c.y) + ent->o.y; }
+ float front() const { return supportcoord(orient.a.y, orient.b.y, orient.c.y) + ent->o.y; }
+ float bottom() const { return ent->o.z - ent->eyeheight - zmargin; }
+ float top() const { return ent->o.z + ent->aboveeye; }
+ };
+
+ struct EntFuzzy : Ent
+ {
+ EntFuzzy(physent *ent) : Ent(ent) {}
+
+ float left() const { return ent->o.x - ent->radius; }
+ float right() const { return ent->o.x + ent->radius; }
+ float back() const { return ent->o.y - ent->radius; }
+ float front() const { return ent->o.y + ent->radius; }
+ float bottom() const { return ent->o.z - ent->eyeheight; }
+ float top() const { return ent->o.z + ent->aboveeye; }
+ };
+
+ struct EntCylinder : EntFuzzy
+ {
+ float zmargin;
+
+ EntCylinder(physent *ent, float zmargin = 0) : EntFuzzy(ent), zmargin(zmargin) {}
+
+ vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight - zmargin)/2); }
+
+ float bottom() const { return ent->o.z - ent->eyeheight - zmargin; }
+
+ vec contactface(const vec &n, const vec &dir) const
+ {
+ float dxy = n.dot2(n)/(ent->radius*ent->radius), dz = n.z*n.z*4/(ent->aboveeye + ent->eyeheight + zmargin);
+ vec fn(0, 0, 0);
+ if(dz > dxy && dir.z) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+ else if(n.dot2(dir) < 0)
+ {
+ fn.x = n.x;
+ fn.y = n.y;
+ fn.normalize();
+ }
+ return fn;
+ }
+
+ vec supportpoint(const vec &n) const
+ {
+ vec p(ent->o);
+ if(n.z > 0) p.z += ent->aboveeye;
+ else p.z -= ent->eyeheight + zmargin;
+ if(n.x || n.y)
+ {
+ float r = ent->radius / n.magnitude2();
+ p.x += n.x*r;
+ p.y += n.y*r;
+ }
+ return p;
+ }
+ };
+
+ struct EntCapsule : EntFuzzy
+ {
+ EntCapsule(physent *ent) : EntFuzzy(ent) {}
+
+ vec supportpoint(const vec &n) const
+ {
+ vec p(ent->o);
+ if(n.z > 0) p.z += ent->aboveeye - ent->radius;
+ else p.z -= ent->eyeheight - ent->radius;
+ p.add(vec(n).mul(ent->radius / n.magnitude()));
+ return p;
+ }
+ };
+
+ struct EntEllipsoid : EntFuzzy
+ {
+ EntEllipsoid(physent *ent) : EntFuzzy(ent) {}
+
+ vec supportpoint(const vec &dir) const
+ {
+ vec p(ent->o), n = vec(dir).normalize();
+ p.x += ent->radius*n.x;
+ p.y += ent->radius*n.y;
+ p.z += (ent->aboveeye + ent->eyeheight)/2*(1 + n.z) - ent->eyeheight;
+ return p;
+ }
+ };
+
+ struct Model
+ {
+ vec o, radius;
+ matrix3 orient;
+
+ Model(const vec &ent, const vec &center, const vec &radius, int yaw) : o(ent), radius(radius)
+ {
+ orient.setyaw(yaw*RAD);
+ o.add(orient.transposedtransform(center));
+ }
+
+ vec center() const { return o; }
+ };
+
+ struct ModelOBB : Model
+ {
+ ModelOBB(const vec &ent, const vec &center, const vec &radius, int yaw) :
+ Model(ent, center, radius, yaw)
+ {}
+
+ vec contactface(const vec &wn, const vec &wdir) const
+ {
+ vec n = orient.transform(wn).div(radius), dir = orient.transform(wdir),
+ an(fabs(n.x), fabs(n.y), dir.z ? fabs(n.z) : 0),
+ fn(0, 0, 0);
+ if(an.x > an.y)
+ {
+ if(an.x > an.z) fn.x = n.x*dir.x < 0 ? (n.x > 0 ? 1 : -1) : 0;
+ else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+ }
+ else if(an.y > an.z) fn.y = n.y*dir.y < 0 ? (n.y > 0 ? 1 : -1) : 0;
+ else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+ return orient.transposedtransform(fn);
+ }
+
+ vec supportpoint(const vec &n) const
+ {
+ vec ln = orient.transform(n), p(0, 0, 0);
+ if(ln.x > 0) p.x += radius.x;
+ else p.x -= radius.x;
+ if(ln.y > 0) p.y += radius.y;
+ else p.y -= radius.y;
+ if(ln.z > 0) p.z += radius.z;
+ else p.z -= radius.z;
+ return orient.transposedtransform(p).add(o);
+ }
+ };
+
+ struct ModelEllipse : Model
+ {
+ ModelEllipse(const vec &ent, const vec &center, const vec &radius, int yaw) :
+ Model(ent, center, radius, yaw)
+ {}
+
+ vec contactface(const vec &wn, const vec &wdir) const
+ {
+ vec n = orient.transform(wn).div(radius), dir = orient.transform(wdir);
+ float dxy = n.dot2(n), dz = n.z*n.z;
+ vec fn(0, 0, 0);
+ if(dz > dxy && dir.z) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+ else if(n.dot2(dir) < 0)
+ {
+ fn.x = n.x*radius.y;
+ fn.y = n.y*radius.x;
+ fn.normalize();
+ }
+ return orient.transposedtransform(fn);
+ }
+
+ vec supportpoint(const vec &n) const
+ {
+ vec ln = orient.transform(n), p(0, 0, 0);
+ if(ln.z > 0) p.z += radius.z;
+ else p.z -= radius.z;
+ if(ln.x || ln.y)
+ {
+ float r = ln.magnitude2();
+ p.x += ln.x*radius.x/r;
+ p.y += ln.y*radius.y/r;
+ }
+ return orient.transposedtransform(p).add(o);
+ }
+ };
+
+ const float boundarytolerance = 1e-3f;
+
+ template<class T, class U>
+ bool collide(const T &p1, const U &p2)
+ {
+ // v0 = center of Minkowski difference
+ vec v0 = p2.center().sub(p1.center());
+ if(v0.iszero()) return true; // v0 and origin overlap ==> hit
+
+ // v1 = support in direction of origin
+ vec n = vec(v0).neg();
+ vec v1 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
+ if(v1.dot(n) <= 0) return false; // origin outside v1 support plane ==> miss
+
+ // v2 = support perpendicular to plane containing origin, v0 and v1
+ n.cross(v1, v0);
+ if(n.iszero()) return true; // v0, v1 and origin colinear (and origin inside v1 support plane) == > hit
+ vec v2 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
+ if(v2.dot(n) <= 0) return false; // origin outside v2 support plane ==> miss
+
+ // v3 = support perpendicular to plane containing v0, v1 and v2
+ n.cross(v0, v1, v2);
+
+ // If the origin is on the - side of the plane, reverse the direction of the plane
+ if(n.dot(v0) > 0)
+ {
+ swap(v1, v2);
+ n.neg();
+ }
+
+ ///
+ // Phase One: Find a valid portal
+
+ loopi(100)
+ {
+ // Obtain the next support point
+ vec v3 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
+ if(v3.dot(n) <= 0) return false; // origin outside v3 support plane ==> miss
+
+ // If origin is outside (v1,v0,v3), then portal is invalid -- eliminate v2 and find new support outside face
+ vec v3xv0;
+ v3xv0.cross(v3, v0);
+ if(v1.dot(v3xv0) < 0)
+ {
+ v2 = v3;
+ n.cross(v0, v1, v3);
+ continue;
+ }
+
+ // If origin is outside (v3,v0,v2), then portal is invalid -- eliminate v1 and find new support outside face
+ if(v2.dot(v3xv0) > 0)
+ {
+ v1 = v3;
+ n.cross(v0, v3, v2);
+ continue;
+ }
+
+ ///
+ // Phase Two: Refine the portal
+
+ for(int j = 0;; j++)
+ {
+ // Compute outward facing normal of the portal
+ n.cross(v1, v2, v3);
+
+ // If the origin is inside the portal, we have a hit
+ if(n.dot(v1) >= 0) return true;
+
+ n.normalize();
+
+ // Find the support point in the direction of the portal's normal
+ vec v4 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
+
+ // If the origin is outside the support plane or the boundary is thin enough, we have a miss
+ if(v4.dot(n) <= 0 || vec(v4).sub(v3).dot(n) <= boundarytolerance || j > 100) return false;
+
+ // Test origin against the three planes that separate the new portal candidates: (v1,v4,v0) (v2,v4,v0) (v3,v4,v0)
+ // Note: We're taking advantage of the triple product identities here as an optimization
+ // (v1 % v4) * v0 == v1 * (v4 % v0) > 0 if origin inside (v1, v4, v0)
+ // (v2 % v4) * v0 == v2 * (v4 % v0) > 0 if origin inside (v2, v4, v0)
+ // (v3 % v4) * v0 == v3 * (v4 % v0) > 0 if origin inside (v3, v4, v0)
+ vec v4xv0;
+ v4xv0.cross(v4, v0);
+ if(v1.dot(v4xv0) > 0)
+ {
+ if(v2.dot(v4xv0) > 0) v1 = v4; // Inside v1 & inside v2 ==> eliminate v1
+ else v3 = v4; // Inside v1 & outside v2 ==> eliminate v3
+ }
+ else
+ {
+ if(v3.dot(v4xv0) > 0) v2 = v4; // Outside v1 & inside v3 ==> eliminate v2
+ else v1 = v4; // Outside v1 & outside v3 ==> eliminate v1
+ }
+ }
+ }
+ return false;
+ }
+
+ template<class T, class U>
+ bool collide(const T &p1, const U &p2, vec *contactnormal, vec *contactpoint1, vec *contactpoint2)
+ {
+ // v0 = center of Minkowski sum
+ vec v01 = p1.center();
+ vec v02 = p2.center();
+ vec v0 = vec(v02).sub(v01);
+
+ // Avoid case where centers overlap -- any direction is fine in this case
+ if(v0.iszero()) v0 = vec(0, 0, 1e-5f);
+
+ // v1 = support in direction of origin
+ vec n = vec(v0).neg();
+ vec v11 = p1.supportpoint(vec(n).neg());
+ vec v12 = p2.supportpoint(n);
+ vec v1 = vec(v12).sub(v11);
+ if(v1.dot(n) <= 0)
+ {
+ if(contactnormal) *contactnormal = n;
+ return false;
+ }
+
+ // v2 - support perpendicular to v1,v0
+ n.cross(v1, v0);
+ if(n.iszero())
+ {
+ n = vec(v1).sub(v0);
+ n.normalize();
+ if(contactnormal) *contactnormal = n;
+ if(contactpoint1) *contactpoint1 = v11;
+ if(contactpoint2) *contactpoint2 = v12;
+ return true;
+ }
+ vec v21 = p1.supportpoint(vec(n).neg());
+ vec v22 = p2.supportpoint(n);
+ vec v2 = vec(v22).sub(v21);
+ if(v2.dot(n) <= 0)
+ {
+ if(contactnormal) *contactnormal = n;
+ return false;
+ }
+
+ // Determine whether origin is on + or - side of plane (v1,v0,v2)
+ n.cross(v0, v1, v2);
+ ASSERT( !n.iszero() );
+ // If the origin is on the - side of the plane, reverse the direction of the plane
+ if(n.dot(v0) > 0)
+ {
+ swap(v1, v2);
+ swap(v11, v21);
+ swap(v12, v22);
+ n.neg();
+ }
+
+ ///
+ // Phase One: Identify a portal
+
+ loopi(100)
+ {
+ // Obtain the support point in a direction perpendicular to the existing plane
+ // Note: This point is guaranteed to lie off the plane
+ vec v31 = p1.supportpoint(vec(n).neg());
+ vec v32 = p2.supportpoint(n);
+ vec v3 = vec(v32).sub(v31);
+ if(v3.dot(n) <= 0)
+ {
+ if(contactnormal) *contactnormal = n;
+ return false;
+ }
+
+ // If origin is outside (v1,v0,v3), then eliminate v2 and loop
+ vec v3xv0;
+ v3xv0.cross(v3, v0);
+ if(v1.dot(v3xv0) < 0)
+ {
+ v2 = v3;
+ v21 = v31;
+ v22 = v32;
+ n.cross(v0, v1, v3);
+ continue;
+ }
+
+ // If origin is outside (v3,v0,v2), then eliminate v1 and loop
+ if(v2.dot(v3xv0) > 0)
+ {
+ v1 = v3;
+ v11 = v31;
+ v12 = v32;
+ n.cross(v0, v3, v2);
+ continue;
+ }
+
+ bool hit = false;
+
+ ///
+ // Phase Two: Refine the portal
+
+ // We are now inside of a wedge...
+ for(int j = 0;; j++)
+ {
+ // Compute normal of the wedge face
+ n.cross(v1, v2, v3);
+
+ // Can this happen??? Can it be handled more cleanly?
+ if(n.iszero())
+ {
+ ASSERT(0);
+ return true;
+ }
+
+ n.normalize();
+
+ // If the origin is inside the wedge, we have a hit
+ if(n.dot(v1) >= 0 && !hit)
+ {
+ if(contactnormal) *contactnormal = n;
+
+ // Compute the barycentric coordinates of the origin
+ if(contactpoint1 || contactpoint2)
+ {
+ float b0 = v3.scalartriple(v1, v2),
+ b1 = v0.scalartriple(v3, v2),
+ b2 = v3.scalartriple(v0, v1),
+ b3 = v0.scalartriple(v2, v1),
+ sum = b0 + b1 + b2 + b3;
+ if(sum <= 0)
+ {
+ b0 = 0;
+ b1 = n.scalartriple(v2, v3);
+ b2 = n.scalartriple(v3, v1);
+ b3 = n.scalartriple(v1, v2);
+ sum = b1 + b2 + b3;
+ }
+ if(contactpoint1)
+ *contactpoint1 = (vec(v01).mul(b0).add(vec(v11).mul(b1)).add(vec(v21).mul(b2)).add(vec(v31).mul(b3))).mul(1.0f/sum);
+ if(contactpoint2)
+ *contactpoint2 = (vec(v02).mul(b0).add(vec(v12).mul(b1)).add(vec(v22).mul(b2)).add(vec(v32).mul(b3))).mul(1.0f/sum);
+ }
+
+ // HIT!!!
+ hit = true;
+ }
+
+ // Find the support point in the direction of the wedge face
+ vec v41 = p1.supportpoint(vec(n).neg());
+ vec v42 = p2.supportpoint(n);
+ vec v4 = vec(v42).sub(v41);
+
+ // If the boundary is thin enough or the origin is outside the support plane for the newly discovered vertex, then we can terminate
+ if(v4.dot(n) <= 0 || vec(v4).sub(v3).dot(n) <= boundarytolerance || j > 100)
+ {
+ if(contactnormal) *contactnormal = n;
+ return hit;
+ }
+
+ // Test origin against the three planes that separate the new portal candidates: (v1,v4,v0) (v2,v4,v0) (v3,v4,v0)
+ // Note: We're taking advantage of the triple product identities here as an optimization
+ // (v1 % v4) * v0 == v1 * (v4 % v0) > 0 if origin inside (v1, v4, v0)
+ // (v2 % v4) * v0 == v2 * (v4 % v0) > 0 if origin inside (v2, v4, v0)
+ // (v3 % v4) * v0 == v3 * (v4 % v0) > 0 if origin inside (v3, v4, v0)
+ vec v4xv0;
+ v4xv0.cross(v4, v0);
+ if(v1.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d1 = (v4,v0,v1)
+ {
+ if(v2.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d2 = (v4,v0,v2)
+ {
+ // Inside d1 & inside d2 ==> eliminate v1
+ v1 = v4;
+ v11 = v41;
+ v12 = v42;
+ }
+ else
+ {
+ // Inside d1 & outside d2 ==> eliminate v3
+ v3 = v4;
+ v31 = v41;
+ v32 = v42;
+ }
+ }
+ else
+ {
+ if(v3.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d3 = (v4,v0,v3)
+ {
+ // Outside d1 & inside d3 ==> eliminate v2
+ v2 = v4;
+ v21 = v41;
+ v22 = v42;
+ }
+ else
+ {
+ // Outside d1 & outside d3 ==> eliminate v1
+ v1 = v4;
+ v11 = v41;
+ v12 = v42;
+ }
+ }
+ }
+ }
+ return false;
+ }
+}
+
diff --git a/src/engine/normal.cpp b/src/engine/normal.cpp
new file mode 100644
index 0000000..d8641ab
--- /dev/null
+++ b/src/engine/normal.cpp
@@ -0,0 +1,383 @@
+#include "engine.h"
+
+struct normalgroup
+{
+ vec pos;
+ int flat, normals, tnormals;
+
+ normalgroup() : flat(0), normals(-1), tnormals(-1) {}
+ normalgroup(const vec &pos) : pos(pos), flat(0), normals(-1), tnormals(-1) {}
+};
+
+static inline bool htcmp(const vec &v, const normalgroup &n) { return v == n.pos; }
+
+struct normal
+{
+ int next;
+ vec surface;
+};
+
+struct tnormal
+{
+ int next;
+ float offset;
+ int normals[2];
+ normalgroup *groups[2];
+};
+
+hashset<normalgroup> normalgroups(1<<16);
+vector<normal> normals;
+vector<tnormal> tnormals;
+
+VARR(lerpangle, 0, 44, 180);
+
+static float lerpthreshold = 0;
+static bool usetnormals = true;
+
+static int addnormal(const vec &key, const vec &surface)
+{
+ normalgroup &g = normalgroups.access(key, key);
+ normal &n = normals.add();
+ n.next = g.normals;
+ n.surface = surface;
+ return g.normals = normals.length()-1;
+}
+
+static void addtnormal(const vec &key, float offset, int normal1, int normal2, normalgroup *group1, normalgroup *group2)
+{
+ normalgroup &g = normalgroups.access(key, key);
+ tnormal &n = tnormals.add();
+ n.next = g.tnormals;
+ n.offset = offset;
+ n.normals[0] = normal1;
+ n.normals[1] = normal2;
+ n.groups[0] = group1;
+ n.groups[1] = group2;
+ g.tnormals = tnormals.length()-1;
+}
+
+static int addnormal(const vec &key, int axis)
+{
+ normalgroup &g = normalgroups.access(key, key);
+ g.flat += 1<<(4*axis);
+ return axis - 6;
+}
+
+static inline void findnormal(const normalgroup &g, const vec &surface, vec &v)
+{
+ v = vec(0, 0, 0);
+ int total = 0;
+ if(surface.x >= lerpthreshold) { int n = (g.flat>>4)&0xF; v.x += n; total += n; }
+ else if(surface.x <= -lerpthreshold) { int n = g.flat&0xF; v.x -= n; total += n; }
+ if(surface.y >= lerpthreshold) { int n = (g.flat>>12)&0xF; v.y += n; total += n; }
+ else if(surface.y <= -lerpthreshold) { int n = (g.flat>>8)&0xF; v.y -= n; total += n; }
+ if(surface.z >= lerpthreshold) { int n = (g.flat>>20)&0xF; v.z += n; total += n; }
+ else if(surface.z <= -lerpthreshold) { int n = (g.flat>>16)&0xF; v.z -= n; total += n; }
+ for(int cur = g.normals; cur >= 0;)
+ {
+ normal &o = normals[cur];
+ if(o.surface.dot(surface) >= lerpthreshold)
+ {
+ v.add(o.surface);
+ total++;
+ }
+ cur = o.next;
+ }
+ if(total > 1) v.normalize();
+ else if(!total) v = surface;
+}
+
+static inline bool findtnormal(const normalgroup &g, const vec &surface, vec &v)
+{
+ float bestangle = lerpthreshold;
+ tnormal *bestnorm = NULL;
+ for(int cur = g.tnormals; cur >= 0;)
+ {
+ tnormal &o = tnormals[cur];
+ static const vec flats[6] = { vec(-1, 0, 0), vec(1, 0, 0), vec(0, -1, 0), vec(0, 1, 0), vec(0, 0, -1), vec(0, 0, 1) };
+ vec n1 = o.normals[0] < 0 ? flats[o.normals[0]+6] : normals[o.normals[0]].surface,
+ n2 = o.normals[1] < 0 ? flats[o.normals[1]+6] : normals[o.normals[1]].surface,
+ nt;
+ nt.lerp(n1, n2, o.offset).normalize();
+ float tangle = nt.dot(surface);
+ if(tangle >= bestangle)
+ {
+ bestangle = tangle;
+ bestnorm = &o;
+ }
+ cur = o.next;
+ }
+ if(!bestnorm) return false;
+ vec n1, n2;
+ findnormal(*bestnorm->groups[0], surface, n1);
+ findnormal(*bestnorm->groups[1], surface, n2);
+ v.lerp(n1, n2, bestnorm->offset).normalize();
+ return true;
+}
+
+void findnormal(const vec &key, const vec &surface, vec &v)
+{
+ const normalgroup *g = normalgroups.access(key);
+ if(!g) v = surface;
+ else if(g->tnormals < 0 || !findtnormal(*g, surface, v))
+ findnormal(*g, surface, v);
+}
+
+VARR(lerpsubdiv, 0, 2, 4);
+VARR(lerpsubdivsize, 4, 4, 128);
+
+static uint progress = 0;
+
+void show_addnormals_progress()
+{
+ float bar1 = float(progress) / float(allocnodes);
+ renderprogress(bar1, "computing normals...");
+}
+
+void addnormals(cube &c, const ivec &o, int size)
+{
+ CHECK_CALCLIGHT_PROGRESS(return, show_addnormals_progress);
+
+ if(c.children)
+ {
+ progress++;
+ size >>= 1;
+ loopi(8) addnormals(c.children[i], ivec(i, o, size), size);
+ return;
+ }
+ else if(isempty(c)) return;
+
+ vec pos[MAXFACEVERTS];
+ int norms[MAXFACEVERTS];
+ int tj = usetnormals && c.ext ? c.ext->tjoints : -1, vis;
+ loopi(6) if((vis = visibletris(c, i, o, size)))
+ {
+ CHECK_CALCLIGHT_PROGRESS(return, show_addnormals_progress);
+ if(c.texture[i] == DEFAULT_SKY) continue;
+
+ vec planes[2];
+ int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0, convex = 0, numplanes = 0;
+ if(numverts)
+ {
+ vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts;
+ vec vo(ivec(o).mask(~0xFFF));
+ loopj(numverts)
+ {
+ vertinfo &v = verts[j];
+ pos[j] = vec(v.x, v.y, v.z).mul(1.0f/8).add(vo);
+ }
+ if(!(c.merged&(1<<i)) && !flataxisface(c, i)) convex = faceconvexity(verts, numverts, size);
+ }
+ else if(c.merged&(1<<i)) continue;
+ else
+ {
+ ivec v[4];
+ genfaceverts(c, i, v);
+ if(!flataxisface(c, i)) convex = faceconvexity(v);
+ int order = vis&4 || convex < 0 ? 1 : 0;
+ vec vo(o);
+ pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+ if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+ pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+ if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+ }
+
+ if(!flataxisface(c, i))
+ {
+ planes[numplanes++].cross(pos[0], pos[1], pos[2]).normalize();
+ if(convex) planes[numplanes++].cross(pos[0], pos[2], pos[3]).normalize();
+ }
+
+ if(!numplanes) loopk(numverts) norms[k] = addnormal(pos[k], i);
+ else if(numplanes==1) loopk(numverts) norms[k] = addnormal(pos[k], planes[0]);
+ else
+ {
+ vec avg = vec(planes[0]).add(planes[1]).normalize();
+ norms[0] = addnormal(pos[0], avg);
+ norms[1] = addnormal(pos[1], planes[0]);
+ norms[2] = addnormal(pos[2], avg);
+ for(int k = 3; k < numverts; k++) norms[k] = addnormal(pos[k], planes[1]);
+ }
+
+ while(tj >= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next;
+ while(tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1))
+ {
+ int edge = tjoints[tj].edge, e1 = edge%(MAXFACEVERTS+1), e2 = (e1+1)%numverts;
+ const vec &v1 = pos[e1], &v2 = pos[e2];
+ ivec d(vec(v2).sub(v1).mul(8));
+ int axis = abs(d.x) > abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2);
+ if(d[axis] < 0) d.neg();
+ reduceslope(d);
+ int origin = int(min(v1[axis], v2[axis])*8)&~0x7FFF,
+ offset1 = (int(v1[axis]*8) - origin) / d[axis],
+ offset2 = (int(v2[axis]*8) - origin) / d[axis];
+ vec o = vec(v1).sub(vec(d).mul(offset1/8.0f)), n1, n2;
+ float doffset = 1.0f / (offset2 - offset1);
+
+ while(tj >= 0)
+ {
+ tjoint &t = tjoints[tj];
+ if(t.edge != edge) break;
+ float offset = (t.offset - offset1) * doffset;
+ vec tpos = vec(d).mul(t.offset/8.0f).add(o);
+ addtnormal(tpos, offset, norms[e1], norms[e2], normalgroups.access(v1), normalgroups.access(v2));
+ tj = t.next;
+ }
+ }
+ }
+}
+
+void calcnormals(bool lerptjoints)
+{
+ if(!lerpangle) return;
+ usetnormals = lerptjoints;
+ if(usetnormals) findtjoints();
+ lerpthreshold = cos(lerpangle*RAD) - 1e-5f;
+ progress = 1;
+ loopi(8) addnormals(worldroot[i], ivec(i, ivec(0, 0, 0), worldsize/2), worldsize/2);
+}
+
+void clearnormals()
+{
+ normalgroups.clear();
+ normals.setsize(0);
+ tnormals.setsize(0);
+}
+
+void calclerpverts(const vec2 *c, const vec *n, lerpvert *lv, int &numv)
+{
+ int i = 0;
+ loopj(numv)
+ {
+ if(j)
+ {
+ if(c[j] == c[j-1] && n[j] == n[j-1]) continue;
+ if(j == numv-1 && c[j] == c[0] && n[j] == n[0]) continue;
+ }
+ lv[i].normal = n[j];
+ lv[i].tc = c[j];
+ i++;
+ }
+ numv = i;
+}
+
+void setlerpstep(float v, lerpbounds &bounds)
+{
+ if(bounds.min->tc.y + 1 > bounds.max->tc.y)
+ {
+ bounds.nstep = vec(0, 0, 0);
+ bounds.normal = bounds.min->normal;
+ if(bounds.min->normal != bounds.max->normal)
+ {
+ bounds.normal.add(bounds.max->normal);
+ bounds.normal.normalize();
+ }
+ bounds.ustep = 0;
+ bounds.u = bounds.min->tc.x;
+ return;
+ }
+
+ bounds.nstep = bounds.max->normal;
+ bounds.nstep.sub(bounds.min->normal);
+ bounds.nstep.div(bounds.max->tc.y-bounds.min->tc.y);
+
+ bounds.normal = bounds.nstep;
+ bounds.normal.mul(v - bounds.min->tc.y);
+ bounds.normal.add(bounds.min->normal);
+
+ bounds.ustep = (bounds.max->tc.x-bounds.min->tc.x) / (bounds.max->tc.y-bounds.min->tc.y);
+ bounds.u = bounds.ustep * (v-bounds.min->tc.y) + bounds.min->tc.x;
+}
+
+void initlerpbounds(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end)
+{
+ const lerpvert *first = &lv[0], *second = NULL;
+ loopi(numv-1)
+ {
+ if(lv[i+1].tc.y < first->tc.y) { second = first; first = &lv[i+1]; }
+ else if(!second || lv[i+1].tc.y < second->tc.y) second = &lv[i+1];
+ }
+
+ if(int(first->tc.y) < int(second->tc.y)) { start.min = end.min = first; }
+ else if(first->tc.x > second->tc.x) { start.min = second; end.min = first; }
+ else { start.min = first; end.min = second; }
+
+ if((lv[1].tc.x - lv->tc.x)*(lv[2].tc.y - lv->tc.y) > (lv[1].tc.y - lv->tc.y)*(lv[2].tc.x - lv->tc.x))
+ {
+ start.winding = end.winding = 1;
+ start.max = (start.min == lv ? &lv[numv-1] : start.min-1);
+ end.max = (end.min == &lv[numv-1] ? lv : end.min+1);
+ }
+ else
+ {
+ start.winding = end.winding = -1;
+ start.max = (start.min == &lv[numv-1] ? lv : start.min+1);
+ end.max = (end.min == lv ? &lv[numv-1] : end.min-1);
+ }
+
+ setlerpstep(v, start);
+ setlerpstep(v, end);
+}
+
+void updatelerpbounds(float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end)
+{
+ if(v >= start.max->tc.y)
+ {
+ const lerpvert *next = start.winding > 0 ?
+ (start.max == lv ? &lv[numv-1] : start.max-1) :
+ (start.max == &lv[numv-1] ? lv : start.max+1);
+ if(next->tc.y > start.max->tc.y)
+ {
+ start.min = start.max;
+ start.max = next;
+ setlerpstep(v, start);
+ }
+ }
+ if(v >= end.max->tc.y)
+ {
+ const lerpvert *next = end.winding > 0 ?
+ (end.max == &lv[numv-1] ? lv : end.max+1) :
+ (end.max == lv ? &lv[numv-1] : end.max-1);
+ if(next->tc.y > end.max->tc.y)
+ {
+ end.min = end.max;
+ end.max = next;
+ setlerpstep(v, end);
+ }
+ }
+}
+
+void lerpnormal(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end, vec &normal, vec &nstep)
+{
+ updatelerpbounds(v, lv, numv, start, end);
+
+ if(start.u + 1 > end.u)
+ {
+ nstep = vec(0, 0, 0);
+ normal = start.normal;
+ normal.add(end.normal);
+ normal.normalize();
+ }
+ else
+ {
+ vec nstart(start.normal), nend(end.normal);
+ nstart.normalize();
+ nend.normalize();
+
+ nstep = nend;
+ nstep.sub(nstart);
+ nstep.div(end.u-start.u);
+
+ normal = nstep;
+ normal.mul(u-start.u);
+ normal.add(nstart);
+ normal.normalize();
+ }
+
+ start.normal.add(start.nstep);
+ start.u += start.ustep;
+
+ end.normal.add(end.nstep);
+ end.u += end.ustep;
+}
+
diff --git a/src/engine/obj.h b/src/engine/obj.h
new file mode 100644
index 0000000..9cdf46a
--- /dev/null
+++ b/src/engine/obj.h
@@ -0,0 +1,191 @@
+struct obj;
+
+struct obj : vertloader<obj>
+{
+ obj(const char *name) : vertloader(name) {}
+
+ static const char *formatname() { return "obj"; }
+ static bool animated() { return false; }
+ bool flipy() const { return true; }
+ int type() const { return MDL_OBJ; }
+
+ struct objmeshgroup : vertmeshgroup
+ {
+ void parsevert(char *s, vector<vec> &out)
+ {
+ vec &v = out.add(vec(0, 0, 0));
+ while(isalpha(*s)) s++;
+ loopi(3)
+ {
+ v[i] = strtod(s, &s);
+ while(isspace(*s)) s++;
+ if(!*s) break;
+ }
+ }
+
+ bool load(const char *filename, float smooth)
+ {
+ int len = strlen(filename);
+ if(len < 4 || strcasecmp(&filename[len-4], ".obj")) return false;
+
+ stream *file = openfile(filename, "rb");
+ if(!file) return false;
+
+ name = newstring(filename);
+
+ numframes = 1;
+
+ vector<vec> attrib[3];
+ char buf[512];
+
+ hashtable<ivec, int> verthash;
+ vector<vert> verts;
+ vector<tcvert> tcverts;
+ vector<tri> tris;
+
+ #define STARTMESH do { \
+ vertmesh &m = *new vertmesh; \
+ m.group = this; \
+ m.name = meshname[0] ? newstring(meshname) : NULL; \
+ meshes.add(&m); \
+ curmesh = &m; \
+ verthash.clear(); \
+ verts.setsize(0); \
+ tcverts.setsize(0); \
+ tris.setsize(0); \
+ } while(0)
+
+ #define FLUSHMESH do { \
+ curmesh->numverts = verts.length(); \
+ if(verts.length()) \
+ { \
+ curmesh->verts = new vert[verts.length()]; \
+ memcpy(curmesh->verts, verts.getbuf(), verts.length()*sizeof(vert)); \
+ curmesh->tcverts = new tcvert[verts.length()]; \
+ memcpy(curmesh->tcverts, tcverts.getbuf(), tcverts.length()*sizeof(tcvert)); \
+ } \
+ curmesh->numtris = tris.length(); \
+ if(tris.length()) \
+ { \
+ curmesh->tris = new tri[tris.length()]; \
+ memcpy(curmesh->tris, tris.getbuf(), tris.length()*sizeof(tri)); \
+ } \
+ if(attrib[2].empty()) \
+ { \
+ if(smooth <= 1) curmesh->smoothnorms(smooth); \
+ else curmesh->buildnorms(); \
+ } \
+ } while(0)
+
+ string meshname = "";
+ vertmesh *curmesh = NULL;
+ while(file->getline(buf, sizeof(buf)))
+ {
+ char *c = buf;
+ while(isspace(*c)) c++;
+ switch(*c)
+ {
+ case '#': continue;
+ case 'v':
+ if(isspace(c[1])) parsevert(c, attrib[0]);
+ else if(c[1]=='t') parsevert(c, attrib[1]);
+ else if(c[1]=='n') parsevert(c, attrib[2]);
+ break;
+ case 'g':
+ {
+ while(isalpha(*c)) c++;
+ while(isspace(*c)) c++;
+ char *name = c;
+ size_t namelen = strlen(name);
+ while(namelen > 0 && isspace(name[namelen-1])) namelen--;
+ copystring(meshname, name, min(namelen+1, sizeof(meshname)));
+
+ if(curmesh) FLUSHMESH;
+ curmesh = NULL;
+ break;
+ }
+ case 'f':
+ {
+ if(!curmesh) STARTMESH;
+ int v0 = -1, v1 = -1;
+ while(isalpha(*c)) c++;
+ for(;;)
+ {
+ while(isspace(*c)) c++;
+ if(!*c) break;
+ ivec vkey(-1, -1, -1);
+ loopi(3)
+ {
+ vkey[i] = strtol(c, &c, 10);
+ if(vkey[i] < 0) vkey[i] = attrib[i].length() + vkey[i];
+ else vkey[i]--;
+ if(!attrib[i].inrange(vkey[i])) vkey[i] = -1;
+ if(*c!='/') break;
+ c++;
+ }
+ int *index = verthash.access(vkey);
+ if(!index)
+ {
+ index = &verthash[vkey];
+ *index = verts.length();
+ vert &v = verts.add();
+ v.pos = vkey.x < 0 ? vec(0, 0, 0) : attrib[0][vkey.x];
+ v.pos = vec(v.pos.z, -v.pos.x, v.pos.y);
+ v.norm = vkey.z < 0 ? vec(0, 0, 0) : attrib[2][vkey.z];
+ v.norm = vec(v.norm.z, -v.norm.x, v.norm.y);
+ tcvert &tcv = tcverts.add();
+ tcv.tc = vkey.y < 0 ? vec2(0, 0) : vec2(attrib[1][vkey.y].x, 1-attrib[1][vkey.y].y);
+ }
+ if(v0 < 0) v0 = *index;
+ else if(v1 < 0) v1 = *index;
+ else
+ {
+ tri &t = tris.add();
+ t.vert[0] = ushort(*index);
+ t.vert[1] = ushort(v1);
+ t.vert[2] = ushort(v0);
+ v1 = *index;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if(curmesh) FLUSHMESH;
+
+ delete file;
+
+ return true;
+ }
+ };
+
+ meshgroup *loadmeshes(const char *name, va_list args)
+ {
+ objmeshgroup *group = new objmeshgroup;
+ if(!group->load(name, va_arg(args, double))) { delete group; return NULL; }
+ return group;
+ }
+
+ bool loaddefaultparts()
+ {
+ part &mdl = addpart();
+ const char *pname = parentdir(name);
+ defformatstring(name1, "packages/models/%s/tris.obj", name);
+ mdl.meshes = sharemeshes(path(name1), 2.0);
+ if(!mdl.meshes)
+ {
+ defformatstring(name2, "packages/models/%s/tris.obj", pname); // try obj in parent folder (vert sharing)
+ mdl.meshes = sharemeshes(path(name2), 2.0);
+ if(!mdl.meshes) return false;
+ }
+ Texture *tex, *masks;
+ loadskin(name, pname, tex, masks);
+ mdl.initskins(tex, masks);
+ if(tex==notexture) conoutf(CON_ERROR, "could not load model skin for %s", name1);
+ return true;
+ }
+};
+
+vertcommands<obj> objcommands;
+
diff --git a/src/engine/octa.cpp b/src/engine/octa.cpp
new file mode 100644
index 0000000..e4f0901
--- /dev/null
+++ b/src/engine/octa.cpp
@@ -0,0 +1,1880 @@
+// core world management routines
+
+#include "engine.h"
+
+cube *worldroot = newcubes(F_SOLID);
+int allocnodes = 0;
+
+cubeext *growcubeext(cubeext *old, int maxverts)
+{
+ cubeext *ext = (cubeext *)new uchar[sizeof(cubeext) + maxverts*sizeof(vertinfo)];
+ if(old)
+ {
+ ext->va = old->va;
+ ext->ents = old->ents;
+ ext->tjoints = old->tjoints;
+ }
+ else
+ {
+ ext->va = NULL;
+ ext->ents = NULL;
+ ext->tjoints = -1;
+ }
+ ext->maxverts = maxverts;
+ return ext;
+}
+
+void setcubeext(cube &c, cubeext *ext)
+{
+ cubeext *old = c.ext;
+ if(old == ext) return;
+ c.ext = ext;
+ if(old) delete[] (uchar *)old;
+}
+
+cubeext *newcubeext(cube &c, int maxverts, bool init)
+{
+ if(c.ext && c.ext->maxverts >= maxverts) return c.ext;
+ cubeext *ext = growcubeext(c.ext, maxverts);
+ if(init)
+ {
+ if(c.ext)
+ {
+ memcpy(ext->surfaces, c.ext->surfaces, sizeof(ext->surfaces));
+ memcpy(ext->verts(), c.ext->verts(), c.ext->maxverts*sizeof(vertinfo));
+ }
+ else memset(ext->surfaces, 0, sizeof(ext->surfaces));
+ }
+ setcubeext(c, ext);
+ return ext;
+}
+
+cube *newcubes(uint face, int mat)
+{
+ cube *c = new cube[8];
+ loopi(8)
+ {
+ c->children = NULL;
+ c->ext = NULL;
+ c->visible = 0;
+ c->merged = 0;
+ setfaces(*c, face);
+ loopl(6) c->texture[l] = DEFAULT_GEOM;
+ c->material = mat;
+ c++;
+ }
+ allocnodes++;
+ return c-8;
+}
+
+int familysize(const cube &c)
+{
+ int size = 1;
+ if(c.children) loopi(8) size += familysize(c.children[i]);
+ return size;
+}
+
+void freeocta(cube *c)
+{
+ if(!c) return;
+ loopi(8) discardchildren(c[i]);
+ delete[] c;
+ allocnodes--;
+}
+
+void freecubeext(cube &c)
+{
+ if(c.ext)
+ {
+ delete[] (uchar *)c.ext;
+ c.ext = NULL;
+ }
+}
+
+void discardchildren(cube &c, bool fixtex, int depth)
+{
+ c.material = MAT_AIR;
+ c.visible = 0;
+ c.merged = 0;
+ if(c.ext)
+ {
+ if(c.ext->va) destroyva(c.ext->va);
+ c.ext->va = NULL;
+ c.ext->tjoints = -1;
+ freeoctaentities(c);
+ freecubeext(c);
+ }
+ if(c.children)
+ {
+ uint filled = F_EMPTY;
+ loopi(8)
+ {
+ discardchildren(c.children[i], fixtex, depth+1);
+ filled |= c.children[i].faces[0];
+ }
+ if(fixtex)
+ {
+ loopi(6) c.texture[i] = getmippedtexture(c, i);
+ if(depth > 0 && filled != F_EMPTY) c.faces[0] = F_SOLID;
+ }
+ DELETEA(c.children);
+ allocnodes--;
+ }
+}
+
+void getcubevector(cube &c, int d, int x, int y, int z, ivec &p)
+{
+ ivec v(d, x, y, z);
+
+ loopi(3)
+ p[i] = edgeget(cubeedge(c, i, v[R[i]], v[C[i]]), v[D[i]]);
+}
+
+void setcubevector(cube &c, int d, int x, int y, int z, const ivec &p)
+{
+ ivec v(d, x, y, z);
+
+ loopi(3)
+ edgeset(cubeedge(c, i, v[R[i]], v[C[i]]), v[D[i]], p[i]);
+}
+
+static inline void getcubevector(cube &c, int i, ivec &p)
+{
+ p.x = edgeget(cubeedge(c, 0, (i>>R[0])&1, (i>>C[0])&1), (i>>D[0])&1);
+ p.y = edgeget(cubeedge(c, 1, (i>>R[1])&1, (i>>C[1])&1), (i>>D[1])&1);
+ p.z = edgeget(cubeedge(c, 2, (i>>R[2])&1, (i>>C[2])&1), (i>>D[2])&1);
+}
+
+static inline void setcubevector(cube &c, int i, const ivec &p)
+{
+ edgeset(cubeedge(c, 0, (i>>R[0])&1, (i>>C[0])&1), (i>>D[0])&1, p.x);
+ edgeset(cubeedge(c, 1, (i>>R[1])&1, (i>>C[1])&1), (i>>D[1])&1, p.y);
+ edgeset(cubeedge(c, 2, (i>>R[2])&1, (i>>C[2])&1), (i>>D[2])&1, p.z);
+}
+
+void optiface(uchar *p, cube &c)
+{
+ uint f = *(uint *)p;
+ if(((f>>4)&0x0F0F0F0FU) == (f&0x0F0F0F0FU)) emptyfaces(c);
+}
+
+void printcube()
+{
+ cube &c = lookupcube(lu); // assume this is cube being pointed at
+ conoutf(CON_DEBUG, "= %p = (%d, %d, %d) @ %d", (void *)&c, lu.x, lu.y, lu.z, lusize);
+ conoutf(CON_DEBUG, " x %.8x", c.faces[0]);
+ conoutf(CON_DEBUG, " y %.8x", c.faces[1]);
+ conoutf(CON_DEBUG, " z %.8x", c.faces[2]);
+}
+
+COMMAND(printcube, "");
+
+bool isvalidcube(const cube &c)
+{
+ clipplanes p;
+ genclipplanes(c, ivec(0, 0, 0), 256, p);
+ loopi(8) // test that cube is convex
+ {
+ vec v = p.v[i];
+ loopj(p.size) if(p.p[j].dist(v)>1e-3f) return false;
+ }
+ return true;
+}
+
+void validatec(cube *c, int size)
+{
+ loopi(8)
+ {
+ if(c[i].children)
+ {
+ if(size<=1)
+ {
+ solidfaces(c[i]);
+ discardchildren(c[i], true);
+ }
+ else validatec(c[i].children, size>>1);
+ }
+ else if(size > 0x1000)
+ {
+ subdividecube(c[i], true, false);
+ validatec(c[i].children, size>>1);
+ }
+ else
+ {
+ loopj(3)
+ {
+ uint f = c[i].faces[j], e0 = f&0x0F0F0F0FU, e1 = (f>>4)&0x0F0F0F0FU;
+ if(e0 == e1 || ((e1+0x07070707U)|(e1-e0))&0xF0F0F0F0U)
+ {
+ emptyfaces(c[i]);
+ break;
+ }
+ }
+ }
+ }
+}
+
+ivec lu;
+int lusize;
+cube &lookupcube(const ivec &to, int tsize, ivec &ro, int &rsize)
+{
+ int tx = clamp(to.x, 0, worldsize-1),
+ ty = clamp(to.y, 0, worldsize-1),
+ tz = clamp(to.z, 0, worldsize-1);
+ int scale = worldscale-1, csize = abs(tsize);
+ cube *c = &worldroot[octastep(tx, ty, tz, scale)];
+ if(!(csize>>scale)) do
+ {
+ if(!c->children)
+ {
+ if(tsize > 0) do
+ {
+ subdividecube(*c);
+ scale--;
+ c = &c->children[octastep(tx, ty, tz, scale)];
+ } while(!(csize>>scale));
+ break;
+ }
+ scale--;
+ c = &c->children[octastep(tx, ty, tz, scale)];
+ } while(!(csize>>scale));
+ ro = ivec(tx, ty, tz).mask(~0U<<scale);
+ rsize = 1<<scale;
+ return *c;
+}
+
+int lookupmaterial(const vec &v)
+{
+ ivec o(v);
+ if(!insideworld(o)) return MAT_AIR;
+ int scale = worldscale-1;
+ cube *c = &worldroot[octastep(o.x, o.y, o.z, scale)];
+ while(c->children)
+ {
+ scale--;
+ c = &c->children[octastep(o.x, o.y, o.z, scale)];
+ }
+ return c->material;
+}
+
+const cube *neighbourstack[32];
+int neighbourdepth = -1;
+
+const cube &neighbourcube(const cube &c, int orient, const ivec &co, int size, ivec &ro, int &rsize)
+{
+ ivec n = co;
+ int dim = dimension(orient);
+ uint diff = n[dim];
+ if(dimcoord(orient)) n[dim] += size; else n[dim] -= size;
+ diff ^= n[dim];
+ if(diff >= uint(worldsize)) { ro = n; rsize = size; return c; }
+ int scale = worldscale;
+ const cube *nc = worldroot;
+ if(neighbourdepth >= 0)
+ {
+ scale -= neighbourdepth + 1;
+ diff >>= scale;
+ do { scale++; diff >>= 1; } while(diff);
+ nc = neighbourstack[worldscale - scale];
+ }
+ scale--;
+ nc = &nc[octastep(n.x, n.y, n.z, scale)];
+ if(!(size>>scale) && nc->children) do
+ {
+ scale--;
+ nc = &nc->children[octastep(n.x, n.y, n.z, scale)];
+ } while(!(size>>scale) && nc->children);
+ ro = n.mask(~0U<<scale);
+ rsize = 1<<scale;
+ return *nc;
+}
+
+////////// (re)mip //////////
+
+int getmippedtexture(const cube &p, int orient)
+{
+ cube *c = p.children;
+ int d = dimension(orient), dc = dimcoord(orient), texs[4] = { -1, -1, -1, -1 }, numtexs = 0;
+ loop(x, 2) loop(y, 2)
+ {
+ int n = octaindex(d, x, y, dc);
+ if(isempty(c[n]))
+ {
+ n = oppositeocta(d, n);
+ if(isempty(c[n]))
+ continue;
+ }
+ int tex = c[n].texture[orient];
+ if(tex > DEFAULT_SKY) loopi(numtexs) if(texs[i] == tex) return tex;
+ texs[numtexs++] = tex;
+ }
+ loopirev(numtexs) if(!i || texs[i] > DEFAULT_SKY) return texs[i];
+ return DEFAULT_GEOM;
+}
+
+void forcemip(cube &c, bool fixtex)
+{
+ cube *ch = c.children;
+ emptyfaces(c);
+
+ loopi(8) loopj(8)
+ {
+ int n = i^(j==3 ? 4 : (j==4 ? 3 : j));
+ if(!isempty(ch[n])) // breadth first search for cube near vert
+ {
+ ivec v;
+ getcubevector(ch[n], i, v);
+ // adjust vert to parent size
+ setcubevector(c, i, ivec(n, v, 8).shr(1));
+ break;
+ }
+ }
+
+ if(fixtex) loopj(6)
+ c.texture[j] = getmippedtexture(c, j);
+}
+
+static int midedge(const ivec &a, const ivec &b, int xd, int yd, bool &perfect)
+{
+ int ax = a[xd], ay = a[yd], bx = b[xd], by = b[yd];
+ if(ay==by) return ay;
+ if(ax==bx) { perfect = false; return ay; }
+ bool crossx = (ax<8 && bx>8) || (ax>8 && bx<8);
+ bool crossy = (ay<8 && by>8) || (ay>8 && by<8);
+ if(crossy && !crossx) { midedge(a,b,yd,xd,perfect); return 8; } // to test perfection
+ if(ax<=8 && bx<=8) return ax>bx ? ay : by;
+ if(ax>=8 && bx>=8) return ax<bx ? ay : by;
+ int risex = (by-ay)*(8-ax)*256;
+ int s = risex/(bx-ax);
+ int y = s/256 + ay;
+ if(((abs(s)&0xFF)!=0) || // ie: rounding error
+ (crossy && y!=8) ||
+ (y<0 || y>16)) perfect = false;
+ return crossy ? 8 : min(max(y, 0), 16);
+}
+
+static inline bool crosscenter(const ivec &a, const ivec &b, int xd, int yd)
+{
+ int ax = a[xd], ay = a[yd], bx = b[xd], by = b[yd];
+ return (((ax <= 8 && bx <= 8) || (ax >= 8 && bx >= 8)) &&
+ ((ay <= 8 && by <= 8) || (ay >= 8 && by >= 8))) ||
+ (ax + bx == 16 && ay + by == 16);
+}
+
+bool subdividecube(cube &c, bool fullcheck, bool brighten)
+{
+ if(c.children) return true;
+ if(c.ext) memset(c.ext->surfaces, 0, sizeof(c.ext->surfaces));
+ if(isempty(c) || isentirelysolid(c))
+ {
+ c.children = newcubes(isempty(c) ? F_EMPTY : F_SOLID, c.material);
+ loopi(8)
+ {
+ loopl(6) c.children[i].texture[l] = c.texture[l];
+ if(brighten && !isempty(c)) brightencube(c.children[i]);
+ }
+ return true;
+ }
+ cube *ch = c.children = newcubes(F_SOLID, c.material);
+ bool perfect = true;
+ ivec v[8];
+ loopi(8)
+ {
+ getcubevector(c, i, v[i]);
+ v[i].mul(2);
+ }
+
+ loopj(6)
+ {
+ int d = dimension(j), z = dimcoord(j);
+ const ivec &v00 = v[octaindex(d, 0, 0, z)],
+ &v10 = v[octaindex(d, 1, 0, z)],
+ &v01 = v[octaindex(d, 0, 1, z)],
+ &v11 = v[octaindex(d, 1, 1, z)];
+ int e[3][3];
+ // corners
+ e[0][0] = v00[d];
+ e[0][2] = v01[d];
+ e[2][0] = v10[d];
+ e[2][2] = v11[d];
+ // edges
+ e[0][1] = midedge(v00, v01, C[d], d, perfect);
+ e[1][0] = midedge(v00, v10, R[d], d, perfect);
+ e[1][2] = midedge(v11, v01, R[d], d, perfect);
+ e[2][1] = midedge(v11, v10, C[d], d, perfect);
+ // center
+ bool p1 = perfect, p2 = perfect;
+ int c1 = midedge(v00, v11, R[d], d, p1);
+ int c2 = midedge(v01, v10, R[d], d, p2);
+ if(z ? c1 > c2 : c1 < c2)
+ {
+ e[1][1] = c1;
+ perfect = p1 && (c1 == c2 || crosscenter(v00, v11, C[d], R[d]));
+ }
+ else
+ {
+ e[1][1] = c2;
+ perfect = p2 && (c1 == c2 || crosscenter(v01, v10, C[d], R[d]));
+ }
+
+ loopi(8)
+ {
+ ch[i].texture[j] = c.texture[j];
+ int rd = (i>>R[d])&1, cd = (i>>C[d])&1, dd = (i>>D[d])&1;
+ edgeset(cubeedge(ch[i], d, 0, 0), z, clamp(e[rd][cd] - dd*8, 0, 8));
+ edgeset(cubeedge(ch[i], d, 1, 0), z, clamp(e[1+rd][cd] - dd*8, 0, 8));
+ edgeset(cubeedge(ch[i], d, 0, 1), z, clamp(e[rd][1+cd] - dd*8, 0, 8));
+ edgeset(cubeedge(ch[i], d, 1, 1), z, clamp(e[1+rd][1+cd] - dd*8, 0, 8));
+ }
+ }
+
+ validatec(ch);
+ if(fullcheck) loopi(8) if(!isvalidcube(ch[i])) // not so good...
+ {
+ emptyfaces(ch[i]);
+ perfect=false;
+ }
+ if(brighten) loopi(8) if(!isempty(ch[i])) brightencube(ch[i]);
+ return perfect;
+}
+
+bool crushededge(uchar e, int dc) { return dc ? e==0 : e==0x88; }
+
+int visibleorient(const cube &c, int orient)
+{
+ loopi(2)
+ {
+ int a = faceedgesidx[orient][i*2 + 0];
+ int b = faceedgesidx[orient][i*2 + 1];
+ loopj(2)
+ {
+ if(crushededge(c.edges[a],j) &&
+ crushededge(c.edges[b],j) &&
+ touchingface(c, orient)) return ((a>>2)<<1) + j;
+ }
+ }
+ return orient;
+}
+
+VAR(mipvis, 0, 0, 1);
+
+static int remipprogress = 0, remiptotal = 0;
+
+bool remip(cube &c, const ivec &co, int size)
+{
+ cube *ch = c.children;
+ if(!ch)
+ {
+ if(size<<1 <= 0x1000) return true;
+ subdividecube(c);
+ ch = c.children;
+ }
+ else if((remipprogress++&0xFFF)==1) renderprogress(float(remipprogress)/remiptotal, "remipping...");
+
+ bool perfect = true;
+ loopi(8)
+ {
+ ivec o(i, co, size);
+ if(!remip(ch[i], o, size>>1)) perfect = false;
+ }
+
+ solidfaces(c); // so texmip is more consistent
+ loopj(6)
+ c.texture[j] = getmippedtexture(c, j); // parents get child texs regardless
+
+ if(!perfect) return false;
+ if(size<<1 > 0x1000) return false;
+
+ ushort mat = MAT_AIR;
+ loopi(8)
+ {
+ mat = ch[i].material;
+ if((mat&MATF_CLIP) == MAT_NOCLIP || mat&MAT_ALPHA)
+ {
+ if(i > 0) return false;
+ while(++i < 8) if(ch[i].material != mat) return false;
+ break;
+ }
+ else if(!isentirelysolid(ch[i]))
+ {
+ while(++i < 8)
+ {
+ int omat = ch[i].material;
+ if(isentirelysolid(ch[i]) ? (omat&MATF_CLIP) == MAT_NOCLIP || omat&MAT_ALPHA : mat != omat) return false;
+ }
+ break;
+ }
+ }
+
+ cube n = c;
+ n.ext = NULL;
+ forcemip(n);
+ n.children = NULL;
+ if(!subdividecube(n, false, false))
+ { freeocta(n.children); return false; }
+
+ cube *nh = n.children;
+ uchar vis[6] = {0, 0, 0, 0, 0, 0};
+ loopi(8)
+ {
+ if(ch[i].faces[0] != nh[i].faces[0] ||
+ ch[i].faces[1] != nh[i].faces[1] ||
+ ch[i].faces[2] != nh[i].faces[2])
+ { freeocta(nh); return false; }
+
+ if(isempty(ch[i]) && isempty(nh[i])) continue;
+
+ ivec o(i, co, size);
+ loop(orient, 6)
+ if(visibleface(ch[i], orient, o, size, MAT_AIR, (mat&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA))
+ {
+ if(ch[i].texture[orient] != n.texture[orient]) { freeocta(nh); return false; }
+ vis[orient] |= 1<<i;
+ }
+ }
+ if(mipvis) loop(orient, 6)
+ {
+ int mask = 0;
+ loop(x, 2) loop(y, 2) mask |= 1<<octaindex(dimension(orient), x, y, dimcoord(orient));
+ if(vis[orient]&mask && (vis[orient]&mask)!=mask) { freeocta(nh); return false; }
+ }
+
+ freeocta(nh);
+ discardchildren(c);
+ loopi(3) c.faces[i] = n.faces[i];
+ c.material = mat;
+ loopi(6) if(vis[i]) c.visible |= 1<<i;
+ if(c.visible) c.visible |= 0x40;
+ brightencube(c);
+ return true;
+}
+
+void mpremip(bool local)
+{
+ extern selinfo sel;
+ if(local) game::edittrigger(sel, EDIT_REMIP);
+ remipprogress = 1;
+ remiptotal = allocnodes;
+ loopi(8)
+ {
+ ivec o(i, ivec(0, 0, 0), worldsize>>1);
+ remip(worldroot[i], o, worldsize>>2);
+ }
+ calcmerges();
+ if(!local) allchanged();
+}
+
+void remip_()
+{
+ mpremip(true);
+ allchanged();
+}
+
+COMMANDN(remip, remip_, "");
+
+static inline int edgeval(cube &c, const ivec &p, int dim, int coord)
+{
+ return edgeget(cubeedge(c, dim, p[R[dim]]>>3, p[C[dim]]>>3), coord);
+}
+
+void genvertp(cube &c, ivec &p1, ivec &p2, ivec &p3, plane &pl, bool solid = false)
+{
+ int dim = 0;
+ if(p1.y==p2.y && p2.y==p3.y) dim = 1;
+ else if(p1.z==p2.z && p2.z==p3.z) dim = 2;
+
+ int coord = p1[dim];
+ ivec v1(p1), v2(p2), v3(p3);
+ v1[dim] = solid ? coord*8 : edgeval(c, p1, dim, coord);
+ v2[dim] = solid ? coord*8 : edgeval(c, p2, dim, coord);
+ v3[dim] = solid ? coord*8 : edgeval(c, p3, dim, coord);
+
+ pl.toplane(vec(v1), vec(v2), vec(v3));
+}
+
+static bool threeplaneintersect(plane &pl1, plane &pl2, plane &pl3, vec &dest)
+{
+ vec &t1 = dest, t2, t3, t4;
+ t1.cross(pl1, pl2); t4 = t1; t1.mul(pl3.offset);
+ t2.cross(pl3, pl1); t2.mul(pl2.offset);
+ t3.cross(pl2, pl3); t3.mul(pl1.offset);
+ t1.add(t2);
+ t1.add(t3);
+ t1.mul(-1);
+ float d = t4.dot(pl3);
+ if(d==0) return false;
+ t1.div(d);
+ return true;
+}
+
+static void genedgespanvert(ivec &p, cube &c, vec &v)
+{
+ ivec p1(8-p.x, p.y, p.z);
+ ivec p2(p.x, 8-p.y, p.z);
+ ivec p3(p.x, p.y, 8-p.z);
+
+ plane plane1, plane2, plane3;
+ genvertp(c, p, p1, p2, plane1);
+ genvertp(c, p, p2, p3, plane2);
+ genvertp(c, p, p3, p1, plane3);
+ if(plane1==plane2) genvertp(c, p, p1, p2, plane1, true);
+ if(plane1==plane3) genvertp(c, p, p1, p2, plane1, true);
+ if(plane2==plane3) genvertp(c, p, p2, p3, plane2, true);
+
+ ASSERT(threeplaneintersect(plane1, plane2, plane3, v));
+ //ASSERT(v.x>=0 && v.x<=8);
+ //ASSERT(v.y>=0 && v.y<=8);
+ //ASSERT(v.z>=0 && v.z<=8);
+ v.x = max(0.0f, min(8.0f, v.x));
+ v.y = max(0.0f, min(8.0f, v.y));
+ v.z = max(0.0f, min(8.0f, v.z));
+}
+
+void edgespan2vectorcube(cube &c)
+{
+ if(isentirelysolid(c) || isempty(c)) return;
+ cube o = c;
+ loop(x, 2) loop(y, 2) loop(z, 2)
+ {
+ ivec p(8*x, 8*y, 8*z);
+ vec v;
+ genedgespanvert(p, o, v);
+
+ edgeset(cubeedge(c, 0, y, z), x, int(v.x+0.49f));
+ edgeset(cubeedge(c, 1, z, x), y, int(v.y+0.49f));
+ edgeset(cubeedge(c, 2, x, y), z, int(v.z+0.49f));
+ }
+}
+
+const ivec cubecoords[8] = // verts of bounding cube
+{
+#define GENCUBEVERT(n, x, y, z) ivec(x, y, z),
+ GENCUBEVERTS(0, 8, 0, 8, 0, 8)
+#undef GENCUBEVERT
+};
+
+template<class T>
+static inline void gencubevert(const cube &c, int i, T &v)
+{
+ switch(i)
+ {
+ default:
+#define GENCUBEVERT(n, x, y, z) \
+ case n: \
+ v = T(edgeget(cubeedge(c, 0, y, z), x), \
+ edgeget(cubeedge(c, 1, z, x), y), \
+ edgeget(cubeedge(c, 2, x, y), z)); \
+ break;
+ GENCUBEVERTS(0, 1, 0, 1, 0, 1)
+#undef GENCUBEVERT
+ }
+}
+
+void genfaceverts(const cube &c, int orient, ivec v[4])
+{
+ switch(orient)
+ {
+ default:
+#define GENFACEORIENT(o, v0, v1, v2, v3) \
+ case o: v0 v1 v2 v3 break;
+#define GENFACEVERT(o, n, x,y,z, xv,yv,zv) \
+ v[n] = ivec(edgeget(cubeedge(c, 0, y, z), x), \
+ edgeget(cubeedge(c, 1, z, x), y), \
+ edgeget(cubeedge(c, 2, x, y), z));
+ GENFACEVERTS(0, 1, 0, 1, 0, 1, , , , , , )
+ #undef GENFACEORIENT
+ #undef GENFACEVERT
+ }
+}
+
+const ivec facecoords[6][4] =
+{
+#define GENFACEORIENT(o, v0, v1, v2, v3) \
+ { v0, v1, v2, v3 },
+#define GENFACEVERT(o, n, x,y,z, xv,yv,zv) \
+ ivec(x,y,z)
+ GENFACEVERTS(0, 8, 0, 8, 0, 8, , , , , , )
+#undef GENFACEORIENT
+#undef GENFACEVERT
+};
+
+const uchar fv[6][4] = // indexes for cubecoords, per each vert of a face orientation
+{
+ { 2, 1, 6, 5 },
+ { 3, 4, 7, 0 },
+ { 4, 5, 6, 7 },
+ { 1, 2, 3, 0 },
+ { 6, 1, 0, 7 },
+ { 5, 4, 3, 2 },
+};
+
+const uchar fvmasks[64] = // mask of verts used given a mask of visible face orientations
+{
+ 0x00, 0x66, 0x99, 0xFF, 0xF0, 0xF6, 0xF9, 0xFF,
+ 0x0F, 0x6F, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xC3, 0xE7, 0xDB, 0xFF, 0xF3, 0xF7, 0xFB, 0xFF,
+ 0xCF, 0xEF, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x3C, 0x7E, 0xBD, 0xFF, 0xFC, 0xFE, 0xFD, 0xFF,
+ 0x3F, 0x7F, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+};
+
+const uchar faceedgesidx[6][4] = // ordered edges surrounding each orient
+{//0..1 = row edges, 2..3 = column edges
+ { 4, 5, 8, 10 },
+ { 6, 7, 9, 11 },
+ { 8, 9, 0, 2 },
+ { 10, 11, 1, 3 },
+ { 0, 1, 4, 6 },
+ { 2, 3, 5, 7 },
+};
+
+bool flataxisface(const cube &c, int orient)
+{
+ uint face = c.faces[dimension(orient)];
+ if(dimcoord(orient)) face >>= 4;
+ return (face&0x0F0F0F0F) == 0x01010101*(face&0x0F);
+}
+
+bool collideface(const cube &c, int orient)
+{
+ if(flataxisface(c, orient))
+ {
+ uchar r1 = c.edges[faceedgesidx[orient][0]], r2 = c.edges[faceedgesidx[orient][1]];
+ if(uchar((r1>>4)|(r2&0xF0)) == uchar((r1&0x0F)|(r2<<4))) return false;
+ uchar c1 = c.edges[faceedgesidx[orient][2]], c2 = c.edges[faceedgesidx[orient][3]];
+ if(uchar((c1>>4)|(c2&0xF0)) == uchar((c1&0x0F)|(c2<<4))) return false;
+ }
+ return true;
+}
+
+bool touchingface(const cube &c, int orient)
+{
+ uint face = c.faces[dimension(orient)];
+ return dimcoord(orient) ? (face&0xF0F0F0F0)==0x80808080 : (face&0x0F0F0F0F)==0;
+}
+
+bool notouchingface(const cube &c, int orient)
+{
+ uint face = c.faces[dimension(orient)];
+ return dimcoord(orient) ? (face&0x80808080)==0 : ((0x88888888-face)&0x08080808) == 0;
+}
+
+int faceconvexity(const ivec v[4])
+{
+ ivec n;
+ n.cross(ivec(v[1]).sub(v[0]), ivec(v[2]).sub(v[0]));
+ return ivec(v[0]).sub(v[3]).dot(n);
+ // 1 if convex, -1 if concave, 0 if flat
+}
+
+int faceconvexity(const vertinfo *verts, int numverts, int size)
+{
+ if(numverts < 4) return 0;
+ ivec v0 = verts[0].getxyz(),
+ e1 = verts[1].getxyz().sub(v0),
+ e2 = verts[2].getxyz().sub(v0),
+ n;
+ if(size >= (8<<5))
+ {
+ if(size >= (8<<10)) n.cross(e1.shr(10), e2.shr(10));
+ else n.cross(e1, e2).shr(10);
+ }
+ else n.cross(e1, e2);
+ return verts[3].getxyz().sub(v0).dot(n);
+}
+
+int faceconvexity(const ivec v[4], int &vis)
+{
+ ivec e1, e2, e3, n;
+ n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));
+ int convex = (e3 = v[0]).sub(v[3]).dot(n);
+ if(!convex)
+ {
+ if(ivec().cross(e3, e2).iszero()) { if(!n.iszero()) vis = 1; }
+ else if(n.iszero()) { vis = 2; }
+ return 0;
+ }
+ return convex;
+}
+
+int faceconvexity(const cube &c, int orient)
+{
+ if(flataxisface(c, orient)) return 0;
+ ivec v[4];
+ genfaceverts(c, orient, v);
+ return faceconvexity(v);
+}
+
+int faceorder(const cube &c, int orient) // gets above 'fv' so that each face is convex
+{
+ return faceconvexity(c, orient)<0 ? 1 : 0;
+}
+
+static inline void faceedges(const cube &c, int orient, uchar edges[4])
+{
+ loopk(4) edges[k] = c.edges[faceedgesidx[orient][k]];
+}
+
+uint faceedges(const cube &c, int orient)
+{
+ union { uchar edges[4]; uint face; } u;
+ faceedges(c, orient, u.edges);
+ return u.face;
+}
+
+static inline int genfacevecs(const cube &cu, int orient, const ivec &pos, int size, bool solid, ivec2 *fvecs, const ivec *v = NULL)
+{
+ int i = 0;
+ if(solid)
+ {
+ switch(orient)
+ {
+ #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+ case orient: \
+ { \
+ if(dimcoord(orient)) { v0 v1 v2 v3 } else { v3 v2 v1 v0 } \
+ break; \
+ }
+ #define GENFACEVERT(orient, vert, xv,yv,zv, x,y,z) \
+ { ivec2 &f = fvecs[i]; x ((xv)<<3); y ((yv)<<3); z ((zv)<<3); i++; }
+ GENFACEVERTS(pos.x, pos.x+size, pos.y, pos.y+size, pos.z, pos.z+size, f.x = , f.x = , f.y = , f.y = , (void), (void))
+ #undef GENFACEVERT
+ }
+ return 4;
+ }
+ ivec buf[4];
+ if(!v) { genfaceverts(cu, orient, buf); v = buf; }
+ ivec2 prev(INT_MAX, INT_MAX);
+ switch(orient)
+ {
+ #define GENFACEVERT(orient, vert, sx,sy,sz, dx,dy,dz) \
+ { \
+ const ivec &e = v[vert]; \
+ ivec ef; \
+ ef.dx = e.sx; ef.dy = e.sy; ef.dz = e.sz; \
+ if(ef.z == dimcoord(orient)*8) \
+ { \
+ ivec2 &f = fvecs[i]; \
+ ivec pf; \
+ pf.dx = pos.sx; pf.dy = pos.sy; pf.dz = pos.sz; \
+ f = ivec2(ef.x*size + (pf.x<<3), ef.y*size + (pf.y<<3)); \
+ if(f != prev) { prev = f; i++; } \
+ } \
+ }
+ GENFACEVERTS(x, x, y, y, z, z, x, x, y, y, z, z)
+ #undef GENFACEORIENT
+ #undef GENFACEVERT
+ }
+ if(fvecs[0] == prev) i--;
+ return i;
+}
+
+static inline int clipfacevecy(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 &r)
+{
+ if(dir.x >= 0)
+ {
+ if(cx <= o.x || cx >= o.x+dir.x) return 0;
+ }
+ else if(cx <= o.x+dir.x || cx >= o.x) return 0;
+
+ int t = (o.y-cy) + (cx-o.x)*dir.y/dir.x;
+ if(t <= 0 || t >= size) return 0;
+
+ r.x = cx;
+ r.y = cy + t;
+ return 1;
+}
+
+static inline int clipfacevecx(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 &r)
+{
+ if(dir.y >= 0)
+ {
+ if(cy <= o.y || cy >= o.y+dir.y) return 0;
+ }
+ else if(cy <= o.y+dir.y || cy >= o.y) return 0;
+
+ int t = (o.x-cx) + (cy-o.y)*dir.x/dir.y;
+ if(t <= 0 || t >= size) return 0;
+
+ r.x = cx + t;
+ r.y = cy;
+ return 1;
+}
+
+static inline int clipfacevec(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 *rvecs)
+{
+ int r = 0;
+
+ if(o.x >= cx && o.x <= cx+size &&
+ o.y >= cy && o.y <= cy+size &&
+ ((o.x != cx && o.x != cx+size) || (o.y != cy && o.y != cy+size)))
+ {
+ rvecs[0].x = o.x;
+ rvecs[0].y = o.y;
+ r++;
+ }
+
+ r += clipfacevecx(o, dir, cx, cy, size, rvecs[r]);
+ r += clipfacevecx(o, dir, cx, cy+size, size, rvecs[r]);
+ r += clipfacevecy(o, dir, cx, cy, size, rvecs[r]);
+ r += clipfacevecy(o, dir, cx+size, cy, size, rvecs[r]);
+
+ ASSERT(r <= 2);
+ return r;
+}
+
+static inline bool insideface(const ivec2 *p, int nump, const ivec2 *o, int numo)
+{
+ int bounds = 0;
+ ivec2 prev = o[numo-1];
+ loopi(numo)
+ {
+ const ivec2 &cur = o[i];
+ ivec2 dir(cur.x-prev.x, cur.y-prev.y);
+ int offset = dir.x*prev.y - dir.y*prev.x;
+ loopj(nump) if(dir.x*p[j].y - dir.y*p[j].x > offset) return false;
+ bounds++;
+ prev = cur;
+ }
+ return bounds>=3;
+}
+
+static inline int clipfacevecs(const ivec2 *o, int numo, int cx, int cy, int size, ivec2 *rvecs)
+{
+ cx <<= 3;
+ cy <<= 3;
+ size <<= 3;
+
+ int r = 0;
+ ivec2 prev = o[numo-1];
+ loopi(numo)
+ {
+ const ivec2 &cur = o[i];
+ r += clipfacevec(prev, ivec2(cur.x-prev.x, cur.y-prev.y), cx, cy, size, &rvecs[r]);
+ prev = cur;
+ }
+ ivec2 corner[4] = {ivec2(cx, cy), ivec2(cx+size, cy), ivec2(cx+size, cy+size), ivec2(cx, cy+size)};
+ loopi(4) if(insideface(&corner[i], 1, o, numo)) rvecs[r++] = corner[i];
+ ASSERT(r <= 8);
+ return r;
+}
+
+bool collapsedface(const cube &c, int orient)
+{
+ int e0 = c.edges[faceedgesidx[orient][0]], e1 = c.edges[faceedgesidx[orient][1]],
+ e2 = c.edges[faceedgesidx[orient][2]], e3 = c.edges[faceedgesidx[orient][3]],
+ face = dimension(orient)*4,
+ f0 = c.edges[face+0], f1 = c.edges[face+1],
+ f2 = c.edges[face+2], f3 = c.edges[face+3];
+ if(dimcoord(orient)) { f0 >>= 4; f1 >>= 4; f2 >>= 4; f3 >>= 4; }
+ else { f0 &= 0xF; f1 &= 0xF; f2 &= 0xF; f3 &= 0xF; }
+ ivec v0(e0&0xF, e2&0xF, f0),
+ v1(e0>>4, e3&0xF, f1),
+ v2(e1>>4, e3>>4, f3),
+ v3(e1&0xF, e2>>4, f2);
+ return ivec().cross(v1.sub(v0), v2.sub(v0)).iszero() &&
+ ivec().cross(v2, v3.sub(v0)).iszero();
+}
+
+static inline bool occludesface(const cube &c, int orient, const ivec &o, int size, const ivec &vo, int vsize, ushort vmat, ushort nmat, ushort matmask, const ivec2 *vf, int numv)
+{
+ int dim = dimension(orient);
+ if(!c.children)
+ {
+ if(nmat != MAT_AIR && (c.material&matmask) == nmat)
+ {
+ ivec2 nf[8];
+ return clipfacevecs(vf, numv, o[C[dim]], o[R[dim]], size, nf) < 3;
+ }
+ if(isentirelysolid(c)) return true;
+ if(vmat != MAT_AIR && ((c.material&matmask) == vmat || (isliquid(vmat) && isclipped(c.material&MATF_VOLUME)))) return true;
+ if(touchingface(c, orient) && faceedges(c, orient) == F_SOLID) return true;
+ ivec2 cf[8];
+ int numc = clipfacevecs(vf, numv, o[C[dim]], o[R[dim]], size, cf);
+ if(numc < 3) return true;
+ if(isempty(c) || notouchingface(c, orient)) return false;
+ ivec2 of[4];
+ int numo = genfacevecs(c, orient, o, size, false, of);
+ return numo >= 3 && insideface(cf, numc, of, numo);
+ }
+
+ size >>= 1;
+ int coord = dimcoord(orient);
+ loopi(8) if(octacoord(dim, i) == coord)
+ {
+ if(!occludesface(c.children[i], orient, ivec(i, o, size), size, vo, vsize, vmat, nmat, matmask, vf, numv)) return false;
+ }
+
+ return true;
+}
+
+bool visibleface(const cube &c, int orient, const ivec &co, int size, ushort mat, ushort nmat, ushort matmask)
+{
+ if(mat != MAT_AIR)
+ {
+ if(faceedges(c, orient)==F_SOLID && touchingface(c, orient)) return false;
+ }
+ else
+ {
+ if(collapsedface(c, orient)) return false;
+ if(!touchingface(c, orient)) return true;
+ }
+
+ ivec no;
+ int nsize;
+ const cube &o = neighbourcube(c, orient, co, size, no, nsize);
+ if(&o==&c) return false;
+
+ int opp = opposite(orient);
+ if(nsize > size || (nsize == size && !o.children))
+ {
+ if(nmat != MAT_AIR && (o.material&matmask) == nmat) return true;
+ if(isentirelysolid(o)) return false;
+ if(mat != MAT_AIR && ((o.material&matmask) == mat || (isliquid(mat) && (o.material&MATF_VOLUME) == MAT_GLASS))) return false;
+ if(isempty(o) || notouchingface(o, opp)) return true;
+ if(touchingface(o, opp) && faceedges(o, opp) == F_SOLID) return false;
+
+ ivec vo = ivec(co).mask(0xFFF);
+ no.mask(0xFFF);
+ ivec2 cf[4], of[4];
+ int numc = genfacevecs(c, orient, vo, size, mat != MAT_AIR, cf),
+ numo = genfacevecs(o, opp, no, nsize, false, of);
+ return numo < 3 || !insideface(cf, numc, of, numo);
+ }
+
+
+ ivec vo = ivec(co).mask(0xFFF);
+ no.mask(0xFFF);
+ ivec2 cf[4];
+ int numc = genfacevecs(c, orient, vo, size, mat != MAT_AIR, cf);
+ return !occludesface(o, opp, no, nsize, vo, size, mat, nmat, matmask, cf, numc);
+}
+
+int classifyface(const cube &c, int orient, const ivec &co, int size)
+{
+ if(collapsedface(c, orient)) return 0;
+ int vismask = (c.material&MATF_CLIP) == MAT_NOCLIP ? 1 : 3;
+ if(!touchingface(c, orient)) return vismask;
+
+ ivec no;
+ int nsize;
+ const cube &o = neighbourcube(c, orient, co, size, no, nsize);
+ if(&o==&c) return 0;
+
+ int vis = 0, opp = opposite(orient);
+ if(nsize > size || (nsize == size && !o.children))
+ {
+ if((~c.material & o.material) & MAT_ALPHA) vis |= 1;
+ if((o.material&MATF_CLIP) == MAT_NOCLIP) vis |= vismask&2;
+ if(vis == vismask || isentirelysolid(o)) return vis;
+ if(isempty(o) || notouchingface(o, opp)) return vismask;
+ if(touchingface(o, opp) && faceedges(o, opp) == F_SOLID) return vis;
+
+ ivec vo = ivec(co).mask(0xFFF);
+ no.mask(0xFFF);
+ ivec2 cf[4], of[4];
+ int numc = genfacevecs(c, orient, vo, size, false, cf),
+ numo = genfacevecs(o, opp, no, nsize, false, of);
+ if(numo < 3 || !insideface(cf, numc, of, numo)) return vismask;
+ return vis;
+ }
+
+ ivec vo = ivec(co).mask(0xFFF);
+ no.mask(0xFFF);
+ ivec2 cf[4];
+ int numc = genfacevecs(c, orient, vo, size, false, cf);
+ if(!occludesface(o, opp, no, nsize, vo, size, MAT_AIR, (c.material&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA, cf, numc)) vis |= 1;
+ if(vismask&2 && !occludesface(o, opp, no, nsize, vo, size, MAT_AIR, MAT_NOCLIP, MATF_CLIP, cf, numc)) vis |= 2;
+ return vis;
+}
+
+// more expensive version that checks both triangles of a face independently
+int visibletris(const cube &c, int orient, const ivec &co, int size, ushort nmat, ushort matmask)
+{
+ int vis = 3, touching = 0xF;
+ ivec v[4], e1, e2, e3, n;
+ genfaceverts(c, orient, v);
+ n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));
+ int convex = (e3 = v[0]).sub(v[3]).dot(n);
+ if(!convex)
+ {
+ if(ivec().cross(e3, e2).iszero() || v[1] == v[3]) { if(n.iszero()) return 0; vis = 1; touching = 0xF&~(1<<3); }
+ else if(n.iszero()) { vis = 2; touching = 0xF&~(1<<1); }
+ }
+
+ int dim = dimension(orient), coord = dimcoord(orient);
+ if(v[0][dim] != coord*8) touching &= ~(1<<0);
+ if(v[1][dim] != coord*8) touching &= ~(1<<1);
+ if(v[2][dim] != coord*8) touching &= ~(1<<2);
+ if(v[3][dim] != coord*8) touching &= ~(1<<3);
+ static const int notouchmasks[2][16] = // mask of triangles not touching
+ { // order 0: flat or convex
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+ { 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 1, 3, 0 },
+ // order 1: concave
+ { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 2, 0 },
+ };
+ int order = convex < 0 ? 1 : 0, notouch = notouchmasks[order][touching];
+ if((vis&notouch)==vis) return vis;
+
+ ivec no;
+ int nsize;
+ const cube &o = neighbourcube(c, orient, co, size, no, nsize);
+ if(&o==&c) return 0;
+
+ if((c.material&matmask) == nmat) nmat = MAT_AIR;
+
+ ivec vo = ivec(co).mask(0xFFF);
+ no.mask(0xFFF);
+ ivec2 cf[4], of[4];
+ int opp = opposite(orient), numo = 0, numc;
+ if(nsize > size || (nsize == size && !o.children))
+ {
+ if(isempty(o) || notouchingface(o, opp)) return vis;
+ if(nmat != MAT_AIR && (o.material&matmask) == nmat) return vis;
+ if(isentirelysolid(o) || (touchingface(o, opp) && faceedges(o, opp) == F_SOLID)) return vis&notouch;
+
+ numc = genfacevecs(c, orient, vo, size, false, cf, v);
+ numo = genfacevecs(o, opp, no, nsize, false, of);
+ if(numo < 3) return vis;
+ if(insideface(cf, numc, of, numo)) return vis&notouch;
+ }
+ else
+ {
+ numc = genfacevecs(c, orient, vo, size, false, cf, v);
+ if(occludesface(o, opp, no, nsize, vo, size, MAT_AIR, nmat, matmask, cf, numc)) return vis&notouch;
+ }
+ if(vis != 3 || notouch) return vis;
+
+ static const int triverts[2][2][2][3] =
+ { // order
+ { // coord
+ { { 1, 2, 3 }, { 0, 1, 3 } }, // verts
+ { { 0, 1, 2 }, { 0, 2, 3 } }
+ },
+ { // coord
+ { { 0, 1, 2 }, { 3, 0, 2 } }, // verts
+ { { 1, 2, 3 }, { 1, 3, 0 } }
+ }
+ };
+
+ do
+ {
+ loopi(2)
+ {
+ const int *verts = triverts[order][coord][i];
+ ivec2 tf[3] = { cf[verts[0]], cf[verts[1]], cf[verts[2]] };
+ if(numo > 0) { if(!insideface(tf, 3, of, numo)) continue; }
+ else if(!occludesface(o, opp, no, nsize, vo, size, MAT_AIR, nmat, matmask, tf, 3)) continue;
+ return vis & ~(1<<i);
+ }
+ vis |= 4;
+ } while(++order <= 1);
+
+ return 3;
+}
+
+void calcvert(const cube &c, const ivec &co, int size, ivec &v, int i, bool solid)
+{
+ if(solid) v = cubecoords[i]; else gencubevert(c, i, v);
+ // avoid overflow
+ if(size>=8) v.mul(size/8);
+ else v.div(8/size);
+ v.add(ivec(co).shl(3));
+}
+
+void calcvert(const cube &c, const ivec &co, int size, vec &v, int i, bool solid)
+{
+ if(solid) v = vec(cubecoords[i]); else gencubevert(c, i, v);
+ v.mul(size/8.0f).add(vec(co));
+}
+
+int genclipplane(const cube &c, int orient, vec *v, plane *clip)
+{
+ int planes = 0, convex = faceconvexity(c, orient), order = convex < 0 ? 1 : 0;
+ const vec &v0 = v[fv[orient][order]], &v1 = v[fv[orient][order+1]], &v2 = v[fv[orient][order+2]], &v3 = v[fv[orient][(order+3)&3]];
+ if(v0==v2) return 0;
+ if(v0!=v1 && v1!=v2) clip[planes++].toplane(v0, v1, v2);
+ if(v0!=v3 && v2!=v3 && (!planes || convex)) clip[planes++].toplane(v0, v2, v3);
+ return planes;
+}
+
+void genclipplanes(const cube &c, const ivec &co, int size, clipplanes &p, bool collide)
+{
+ // generate tight bounding box
+ calcvert(c, co, size, p.v[0], 0);
+ vec mx = p.v[0], mn = p.v[0];
+ for(int i = 1; i < 8; i++)
+ {
+ calcvert(c, co, size, p.v[i], i);
+ mx.max(p.v[i]);
+ mn.min(p.v[i]);
+ }
+
+ p.r = mx.sub(mn).mul(0.5f);
+ p.o = mn.add(p.r);
+
+ p.size = 0;
+ p.visible = 0;
+ if(collide || (c.visible&0xC0) == 0x40)
+ {
+ loopi(6) if(c.visible&(1<<i))
+ {
+ int vis;
+ if(flataxisface(c, i)) p.visible |= 1<<i;
+ else if((vis = visibletris(c, i, co, size, MAT_NOCLIP, MATF_CLIP)))
+ {
+ int convex = faceconvexity(c, i), order = vis&4 || convex < 0 ? 1 : 0;
+ const vec &v0 = p.v[fv[i][order]], &v1 = p.v[fv[i][order+1]], &v2 = p.v[fv[i][order+2]], &v3 = p.v[fv[i][(order+3)&3]];
+ if(vis&1) { p.side[p.size] = i; p.p[p.size++].toplane(v0, v1, v2); }
+ if(vis&2 && (!(vis&1) || convex)) { p.side[p.size] = i; p.p[p.size++].toplane(v0, v2, v3); }
+ }
+ }
+ }
+ else if(c.visible&0x80)
+ {
+ int vis;
+ loopi(6) if((vis = visibletris(c, i, co, size)))
+ {
+ if(flataxisface(c, i)) p.visible |= 1<<i;
+ else
+ {
+ int convex = faceconvexity(c, i), order = vis&4 || convex < 0 ? 1 : 0;
+ const vec &v0 = p.v[fv[i][order]], &v1 = p.v[fv[i][order+1]], &v2 = p.v[fv[i][order+2]], &v3 = p.v[fv[i][(order+3)&3]];
+ if(vis&1) { p.side[p.size] = i; p.p[p.size++].toplane(v0, v1, v2); }
+ if(vis&2 && (!(vis&1) || convex)) { p.side[p.size] = i; p.p[p.size++].toplane(v0, v2, v3); }
+ }
+ }
+ }
+}
+
+static inline bool mergefacecmp(const facebounds &x, const facebounds &y)
+{
+ if(x.v2 < y.v2) return true;
+ if(x.v2 > y.v2) return false;
+ if(x.u1 < y.u1) return true;
+ if(x.u1 > y.u1) return false;
+ return false;
+}
+
+static int mergefacev(int orient, facebounds *m, int sz, facebounds &n)
+{
+ for(int i = sz-1; i >= 0; --i)
+ {
+ if(m[i].v2 < n.v1) break;
+ if(m[i].v2 == n.v1 && m[i].u1 == n.u1 && m[i].u2 == n.u2)
+ {
+ n.v1 = m[i].v1;
+ memmove(&m[i], &m[i+1], (sz - (i+1)) * sizeof(facebounds));
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int mergefaceu(int orient, facebounds &m, facebounds &n)
+{
+ if(m.v1 == n.v1 && m.v2 == n.v2 && m.u2 == n.u1)
+ {
+ n.u1 = m.u1;
+ return 1;
+ }
+ return 0;
+}
+
+static int mergeface(int orient, facebounds *m, int sz, facebounds &n)
+{
+ for(bool merged = false; sz; merged = true)
+ {
+ int vmerged = mergefacev(orient, m, sz, n);
+ sz -= vmerged;
+ if(!vmerged && merged) break;
+ if(!sz) break;
+ int umerged = mergefaceu(orient, m[sz-1], n);
+ sz -= umerged;
+ if(!umerged) break;
+ }
+ m[sz++] = n;
+ return sz;
+}
+
+int mergefaces(int orient, facebounds *m, int sz)
+{
+ quicksort(m, sz, mergefacecmp);
+
+ int nsz = 0;
+ loopi(sz) nsz = mergeface(orient, m, nsz, m[i]);
+ return nsz;
+}
+
+struct cfkey
+{
+ uchar orient;
+ ushort material, tex;
+ ivec n;
+ int offset;
+};
+
+static inline bool htcmp(const cfkey &x, const cfkey &y)
+{
+ return x.orient == y.orient && x.tex == y.tex && x.n == y.n && x.offset == y.offset && x.material==y.material;
+}
+
+static inline uint hthash(const cfkey &k)
+{
+ return hthash(k.n)^k.offset^k.tex^k.orient^k.material;
+}
+
+void mincubeface(const cube &cu, int orient, const ivec &o, int size, const facebounds &orig, facebounds &cf, ushort nmat, ushort matmask)
+{
+ int dim = dimension(orient);
+ if(cu.children)
+ {
+ size >>= 1;
+ int coord = dimcoord(orient);
+ loopi(8) if(octacoord(dim, i) == coord)
+ mincubeface(cu.children[i], orient, ivec(i, o, size), size, orig, cf, nmat, matmask);
+ return;
+ }
+ int c = C[dim], r = R[dim];
+ ushort uco = (o[c]&0xFFF)<<3, vco = (o[r]&0xFFF)<<3;
+ ushort uc1 = uco, vc1 = vco, uc2 = ushort(size<<3)+uco, vc2 = ushort(size<<3)+vco;
+ uc1 = max(uc1, orig.u1);
+ uc2 = min(uc2, orig.u2);
+ vc1 = max(vc1, orig.v1);
+ vc2 = min(vc2, orig.v2);
+ if(!isempty(cu) && touchingface(cu, orient) && !(nmat!=MAT_AIR && (cu.material&matmask)==nmat))
+ {
+ uchar r1 = cu.edges[faceedgesidx[orient][0]], r2 = cu.edges[faceedgesidx[orient][1]],
+ c1 = cu.edges[faceedgesidx[orient][2]], c2 = cu.edges[faceedgesidx[orient][3]];
+ ushort u1 = max(c1&0xF, c2&0xF)*size+uco, u2 = min(c1>>4, c2>>4)*size+uco,
+ v1 = max(r1&0xF, r2&0xF)*size+vco, v2 = min(r1>>4, r2>>4)*size+vco;
+ u1 = max(u1, orig.u1);
+ u2 = min(u2, orig.u2);
+ v1 = max(v1, orig.v1);
+ v2 = min(v2, orig.v2);
+ if(v2-v1==vc2-vc1)
+ {
+ if(u2-u1==uc2-uc1) return;
+ if(u1==uc1) uc1 = u2;
+ if(u2==uc2) uc2 = u1;
+ }
+ else if(u2-u1==uc2-uc1)
+ {
+ if(v1==vc1) vc1 = v2;
+ if(v2==vc2) vc2 = v1;
+ }
+ }
+ if(uc1==uc2 || vc1==vc2) return;
+ cf.u1 = min(cf.u1, uc1);
+ cf.u2 = max(cf.u2, uc2);
+ cf.v1 = min(cf.v1, vc1);
+ cf.v2 = max(cf.v2, vc2);
+}
+
+bool mincubeface(const cube &cu, int orient, const ivec &co, int size, facebounds &orig)
+{
+ ivec no;
+ int nsize;
+ const cube &nc = neighbourcube(cu, orient, co, size, no, nsize);
+ facebounds mincf;
+ mincf.u1 = orig.u2;
+ mincf.u2 = orig.u1;
+ mincf.v1 = orig.v2;
+ mincf.v2 = orig.v1;
+ mincubeface(nc, opposite(orient), no, nsize, orig, mincf, cu.material&MAT_ALPHA ? MAT_AIR : MAT_ALPHA, MAT_ALPHA);
+ bool smaller = false;
+ if(mincf.u1 > orig.u1) { orig.u1 = mincf.u1; smaller = true; }
+ if(mincf.u2 < orig.u2) { orig.u2 = mincf.u2; smaller = true; }
+ if(mincf.v1 > orig.v1) { orig.v1 = mincf.v1; smaller = true; }
+ if(mincf.v2 < orig.v2) { orig.v2 = mincf.v2; smaller = true; }
+ return smaller;
+}
+
+VAR(maxmerge, 0, 6, 12);
+VAR(minface, 0, 4, 12);
+
+struct pvert
+{
+ ushort x, y;
+
+ pvert() {}
+ pvert(ushort x, ushort y) : x(x), y(y) {}
+
+ bool operator==(const pvert &o) const { return x == o.x && y == o.y; }
+ bool operator!=(const pvert &o) const { return x != o.x || y != o.y; }
+};
+
+struct pedge
+{
+ pvert from, to;
+
+ pedge() {}
+ pedge(const pvert &from, const pvert &to) : from(from), to(to) {}
+
+ bool operator==(const pedge &o) const { return from == o.from && to == o.to; }
+ bool operator!=(const pedge &o) const { return from != o.from || to != o.to; }
+};
+
+static inline uint hthash(const pedge &x) { return uint(x.from.x)^(uint(x.from.y)<<8); }
+static inline bool htcmp(const pedge &x, const pedge &y) { return x == y; }
+
+struct poly
+{
+ cube *c;
+ int numverts;
+ bool merged;
+ pvert verts[MAXFACEVERTS];
+};
+
+bool clippoly(poly &p, const facebounds &b)
+{
+ pvert verts1[MAXFACEVERTS+4], verts2[MAXFACEVERTS+4];
+ int numverts1 = 0, numverts2 = 0, px = p.verts[p.numverts-1].x, py = p.verts[p.numverts-1].y;
+ loopi(p.numverts)
+ {
+ int x = p.verts[i].x, y = p.verts[i].y;
+ if(x < b.u1)
+ {
+ if(px > b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px));
+ if(px > b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px));
+ }
+ else if(x > b.u2)
+ {
+ if(px < b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px));
+ if(px < b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px));
+ }
+ else
+ {
+ if(px < b.u1)
+ {
+ if(x > b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px));
+ }
+ else if(px > b.u2 && x < b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px));
+ verts1[numverts1++] = pvert(x, y);
+ }
+ px = x;
+ py = y;
+ }
+ if(numverts1 < 3) return false;
+ px = verts1[numverts1-1].x;
+ py = verts1[numverts1-1].y;
+ loopi(numverts1)
+ {
+ int x = verts1[i].x, y = verts1[i].y;
+ if(y < b.v1)
+ {
+ if(py > b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2);
+ if(py > b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1);
+ }
+ else if(y > b.v2)
+ {
+ if(py < b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1);
+ if(py < b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2);
+ }
+ else
+ {
+ if(py < b.v1)
+ {
+ if(y > b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1);
+ }
+ else if(py > b.v2 && y < b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2);
+ verts2[numverts2++] = pvert(x, y);
+ }
+ px = x;
+ py = y;
+ }
+ if(numverts2 < 3) return false;
+ if(numverts2 > MAXFACEVERTS) return false;
+ memcpy(p.verts, verts2, numverts2*sizeof(pvert));
+ p.numverts = numverts2;
+ return true;
+}
+
+bool genpoly(cube &cu, int orient, const ivec &o, int size, int vis, ivec &n, int &offset, poly &p)
+{
+ int dim = dimension(orient), coord = dimcoord(orient);
+ ivec v[4];
+ genfaceverts(cu, orient, v);
+ if(flataxisface(cu, orient))
+ {
+ n = ivec(0, 0, 0);
+ n[dim] = coord ? 1 : -1;
+ }
+ else
+ {
+ if(faceconvexity(v)) return false;
+ n.cross(ivec(v[1]).sub(v[0]), ivec(v[2]).sub(v[0]));
+ if(n.iszero()) n.cross(ivec(v[2]).sub(v[0]), ivec(v[3]).sub(v[0]));
+ reduceslope(n);
+ }
+
+ ivec po = ivec(o).mask(0xFFF).shl(3);
+ loopk(4) v[k].mul(size).add(po);
+ offset = -n.dot(v[3]);
+
+ int r = R[dim], c = C[dim], order = vis&4 ? 1 : 0;
+ p.numverts = 0;
+ if(coord)
+ {
+ const ivec &v0 = v[order]; p.verts[p.numverts++] = pvert(v0[c], v0[r]);
+ if(vis&1) { const ivec &v1 = v[order+1]; p.verts[p.numverts++] = pvert(v1[c], v1[r]); }
+ const ivec &v2 = v[order+2]; p.verts[p.numverts++] = pvert(v2[c], v2[r]);
+ if(vis&2) { const ivec &v3 = v[(order+3)&3]; p.verts[p.numverts++] = pvert(v3[c], v3[r]); }
+ }
+ else
+ {
+ if(vis&2) { const ivec &v3 = v[(order+3)&3]; p.verts[p.numverts++] = pvert(v3[c], v3[r]); }
+ const ivec &v2 = v[order+2]; p.verts[p.numverts++] = pvert(v2[c], v2[r]);
+ if(vis&1) { const ivec &v1 = v[order+1]; p.verts[p.numverts++] = pvert(v1[c], v1[r]); }
+ const ivec &v0 = v[order]; p.verts[p.numverts++] = pvert(v0[c], v0[r]);
+ }
+
+ if(faceedges(cu, orient)!=F_SOLID)
+ {
+ int px = int(p.verts[p.numverts-2].x) - int(p.verts[p.numverts-3].x), py = int(p.verts[p.numverts-2].y) - int(p.verts[p.numverts-3].y),
+ cx = int(p.verts[p.numverts-1].x) - int(p.verts[p.numverts-2].x), cy = int(p.verts[p.numverts-1].y) - int(p.verts[p.numverts-2].y),
+ dir = px*cy - py*cx;
+ if(dir > 0) return false;
+ if(!dir) { if(p.numverts < 4) return false; p.verts[p.numverts-2] = p.verts[p.numverts-1]; p.numverts--; }
+ px = cx; py = cy;
+ cx = int(p.verts[0].x) - int(p.verts[p.numverts-1].x); cy = int(p.verts[0].y) - int(p.verts[p.numverts-1].y);
+ dir = px*cy - py*cx;
+ if(dir > 0) return false;
+ if(!dir) { if(p.numverts < 4) return false; p.numverts--; }
+ px = cx; py = cy;
+ cx = int(p.verts[1].x) - int(p.verts[0].x); cy = int(p.verts[1].y) - int(p.verts[0].y);
+ dir = px*cy - py*cx;
+ if(dir > 0) return false;
+ if(!dir) { if(p.numverts < 4) return false; p.verts[0] = p.verts[p.numverts-1]; p.numverts--; }
+ px = cx; py = cy;
+ cx = int(p.verts[2].x) - int(p.verts[1].x); cy = int(p.verts[2].y) - int(p.verts[1].y);
+ dir = px*cy - py*cx;
+ if(dir > 0) return false;
+ if(!dir) { if(p.numverts < 4) return false; p.verts[1] = p.verts[2]; p.verts[2] = p.verts[3]; p.numverts--; }
+ }
+
+ p.c = &cu;
+ p.merged = false;
+
+ if(minface && size >= 1<<minface && touchingface(cu, orient))
+ {
+ facebounds b;
+ b.u1 = b.u2 = p.verts[0].x;
+ b.v1 = b.v2 = p.verts[0].y;
+ for(int i = 1; i < p.numverts; i++)
+ {
+ const pvert &v = p.verts[i];
+ b.u1 = min(b.u1, v.x);
+ b.u2 = max(b.u2, v.x);
+ b.v1 = min(b.v1, v.y);
+ b.v2 = max(b.v2, v.y);
+ }
+ if(mincubeface(cu, orient, o, size, b) && clippoly(p, b))
+ p.merged = true;
+ }
+
+ return true;
+}
+
+struct plink : pedge
+{
+ int polys[2];
+
+ plink() { clear(); }
+ plink(const pedge &p) : pedge(p) { clear(); }
+
+ void clear() { polys[0] = polys[1] = -1; }
+};
+
+bool mergepolys(int orient, hashset<plink> &links, vector<plink *> &queue, int owner, poly &p, poly &q, const pedge &e)
+{
+ int pe = -1, qe = -1;
+ loopi(p.numverts) if(p.verts[i] == e.from) { pe = i; break; }
+ loopi(q.numverts) if(q.verts[i] == e.to) { qe = i; break; }
+ if(pe < 0 || qe < 0) return false;
+ if(p.verts[(pe+1)%p.numverts] != e.to || q.verts[(qe+1)%q.numverts] != e.from) return false;
+ /*
+ * c----d
+ * | |
+ * F----T
+ * | P |
+ * b----a
+ */
+ pvert verts[2*MAXFACEVERTS];
+ int numverts = 0, index = pe+2; // starts at A = T+1, ends at F = T+p.numverts
+ loopi(p.numverts-1)
+ {
+ if(index >= p.numverts) index -= p.numverts;
+ verts[numverts++] = p.verts[index++];
+ }
+ index = qe+2; // starts at C = T+2 = F+1, ends at T = T+q.numverts
+ int px = int(verts[numverts-1].x) - int(verts[numverts-2].x), py = int(verts[numverts-1].y) - int(verts[numverts-2].y);
+ loopi(q.numverts-1)
+ {
+ if(index >= q.numverts) index -= q.numverts;
+ pvert &src = q.verts[index++];
+ int cx = int(src.x) - int(verts[numverts-1].x), cy = int(src.y) - int(verts[numverts-1].y),
+ dir = px*cy - py*cx;
+ if(dir > 0) return false;
+ if(!dir) numverts--;
+ verts[numverts++] = src;
+ px = cx;
+ py = cy;
+ }
+ int cx = int(verts[0].x) - int(verts[numverts-1].x), cy = int(verts[0].y) - int(verts[numverts-1].y),
+ dir = px*cy - py*cx;
+ if(dir > 0) return false;
+ if(!dir) numverts--;
+
+ if(numverts > MAXFACEVERTS) return false;
+
+ q.merged = true;
+ q.numverts = 0;
+
+ p.merged = true;
+ p.numverts = numverts;
+ memcpy(p.verts, verts, numverts*sizeof(pvert));
+
+ int prev = p.numverts-1;
+ loopj(p.numverts)
+ {
+ pedge e(p.verts[prev], p.verts[j]);
+ int order = e.from.x > e.to.x || (e.from.x == e.to.x && e.from.y > e.to.y) ? 1 : 0;
+ if(order) swap(e.from, e.to);
+ plink &l = links.access(e, e);
+ bool shouldqueue = l.polys[order] < 0 && l.polys[order^1] >= 0;
+ l.polys[order] = owner;
+ if(shouldqueue) queue.add(&l);
+ prev = j;
+ }
+
+ return true;
+}
+
+void addmerge(cube &cu, int orient, const ivec &co, const ivec &n, int offset, poly &p)
+{
+ cu.merged |= 1<<orient;
+ if(!p.numverts)
+ {
+ if(cu.ext) cu.ext->surfaces[orient] = ambientsurface;
+ return;
+ }
+ surfaceinfo surf = brightsurface;
+ vertinfo verts[MAXFACEVERTS];
+ surf.numverts |= p.numverts;
+ int dim = dimension(orient), coord = dimcoord(orient), c = C[dim], r = R[dim];
+ loopk(p.numverts)
+ {
+ pvert &src = p.verts[coord ? k : p.numverts-1-k];
+ vertinfo &dst = verts[k];
+ ivec v;
+ v[c] = src.x;
+ v[r] = src.y;
+ v[dim] = -(offset + n[c]*src.x + n[r]*src.y)/n[dim];
+ dst.set(v);
+ }
+ if(cu.ext)
+ {
+ const surfaceinfo &oldsurf = cu.ext->surfaces[orient];
+ int numverts = oldsurf.numverts&MAXFACEVERTS;
+ if(numverts == p.numverts)
+ {
+ ivec v0 = verts[0].getxyz();
+ const vertinfo *oldverts = cu.ext->verts() + oldsurf.verts;
+ loopj(numverts) if(v0 == oldverts[j].getxyz())
+ {
+ for(int k = 1; k < numverts; k++)
+ {
+ if(++j >= numverts) j = 0;
+ if(verts[k].getxyz() != oldverts[j].getxyz()) goto nomatch;
+ }
+ return;
+ }
+ nomatch:;
+ }
+ }
+ setsurface(cu, orient, surf, verts, p.numverts);
+}
+
+static inline void clearmerge(cube &c, int orient)
+{
+ if(c.merged&(1<<orient))
+ {
+ c.merged &= ~(1<<orient);
+ if(c.ext) c.ext->surfaces[orient] = brightsurface;
+ }
+}
+
+void addmerges(int orient, const ivec &co, const ivec &n, int offset, vector<poly> &polys)
+{
+ loopv(polys)
+ {
+ poly &p = polys[i];
+ if(p.merged) addmerge(*p.c, orient, co, n, offset, p);
+ else clearmerge(*p.c, orient);
+ }
+}
+
+void mergepolys(int orient, const ivec &co, const ivec &n, int offset, vector<poly> &polys)
+{
+ if(polys.length() <= 1) { addmerges(orient, co, n, offset, polys); return; }
+ hashset<plink> links(polys.length() <= 32 ? 128 : 1024);
+ vector<plink *> queue;
+ loopv(polys)
+ {
+ poly &p = polys[i];
+ int prev = p.numverts-1;
+ loopj(p.numverts)
+ {
+ pedge e(p.verts[prev], p.verts[j]);
+ int order = e.from.x > e.to.x || (e.from.x == e.to.x && e.from.y > e.to.y) ? 1 : 0;
+ if(order) swap(e.from, e.to);
+ plink &l = links.access(e, e);
+ l.polys[order] = i;
+ if(l.polys[0] >= 0 && l.polys[1] >= 0) queue.add(&l);
+ prev = j;
+ }
+ }
+ vector<plink *> nextqueue;
+ while(queue.length())
+ {
+ loopv(queue)
+ {
+ plink &l = *queue[i];
+ if(l.polys[0] >= 0 && l.polys[1] >= 0)
+ mergepolys(orient, links, nextqueue, l.polys[0], polys[l.polys[0]], polys[l.polys[1]], l);
+ }
+ queue.setsize(0);
+ queue.move(nextqueue);
+ }
+ addmerges(orient, co, n, offset, polys);
+}
+
+static int genmergeprogress = 0;
+
+struct cfpolys
+{
+ vector<poly> polys;
+};
+
+static hashtable<cfkey, cfpolys> cpolys;
+
+void genmerges(cube *c = worldroot, const ivec &o = ivec(0, 0, 0), int size = worldsize>>1)
+{
+ if((genmergeprogress++&0xFFF)==0) renderprogress(float(genmergeprogress)/allocnodes, "merging faces...");
+ neighbourstack[++neighbourdepth] = c;
+ loopi(8)
+ {
+ ivec co(i, o, size);
+ int vis;
+ if(c[i].children) genmerges(c[i].children, co, size>>1);
+ else if(!isempty(c[i])) loopj(6) if((vis = visibletris(c[i], j, co, size)))
+ {
+ cfkey k;
+ poly p;
+ if(size < 1<<maxmerge && c != worldroot)
+ {
+ if(genpoly(c[i], j, co, size, vis, k.n, k.offset, p))
+ {
+ k.orient = j;
+ k.tex = c[i].texture[j];
+ k.material = c[i].material&MAT_ALPHA;
+ cpolys[k].polys.add(p);
+ continue;
+ }
+ }
+ else if(minface && size >= 1<<minface && touchingface(c[i], j))
+ {
+ if(genpoly(c[i], j, co, size, vis, k.n, k.offset, p) && p.merged)
+ {
+ addmerge(c[i], j, co, k.n, k.offset, p);
+ continue;
+ }
+ }
+ clearmerge(c[i], j);
+ }
+ if((size == 1<<maxmerge || c == worldroot) && cpolys.numelems)
+ {
+ enumeratekt(cpolys, cfkey, key, cfpolys, val,
+ {
+ mergepolys(key.orient, co, key.n, key.offset, val.polys);
+ });
+ cpolys.clear();
+ }
+ }
+ --neighbourdepth;
+}
+
+int calcmergedsize(int orient, const ivec &co, int size, const vertinfo *verts, int numverts)
+{
+ ushort x1 = verts[0].x, y1 = verts[0].y, z1 = verts[0].z,
+ x2 = x1, y2 = y1, z2 = z1;
+ for(int i = 1; i < numverts; i++)
+ {
+ const vertinfo &v = verts[i];
+ x1 = min(x1, v.x);
+ x2 = max(x2, v.x);
+ y1 = min(y1, v.y);
+ y2 = max(y2, v.y);
+ z1 = min(z1, v.z);
+ z2 = max(z2, v.z);
+ }
+ int bits = 0;
+ while(1<<bits < size) ++bits;
+ bits += 3;
+ ivec mo(co);
+ mo.mask(0xFFF);
+ mo.shl(3);
+ while(bits<15)
+ {
+ mo.mask(~((1<<bits)-1));
+ if(mo.x <= x1 && mo.x + (1<<bits) >= x2 &&
+ mo.y <= y1 && mo.y + (1<<bits) >= y2 &&
+ mo.z <= z1 && mo.z + (1<<bits) >= z2)
+ break;
+ bits++;
+ }
+ return bits-3;
+}
+
+static void invalidatemerges(cube &c)
+{
+ if(c.merged)
+ {
+ brightencube(c);
+ c.merged = 0;
+ }
+ if(c.ext)
+ {
+ if(c.ext->va)
+ {
+ if(!(c.ext->va->hasmerges&(MERGE_PART | MERGE_ORIGIN))) return;
+ destroyva(c.ext->va);
+ c.ext->va = NULL;
+ }
+ if(c.ext->tjoints >= 0) c.ext->tjoints = -1;
+ }
+ if(c.children) loopi(8) invalidatemerges(c.children[i]);
+}
+
+static int invalidatedmerges = 0;
+
+void invalidatemerges(cube &c, const ivec &co, int size, bool msg)
+{
+ if(msg && invalidatedmerges!=totalmillis)
+ {
+ renderprogress(0, "invalidating merged surfaces...");
+ invalidatedmerges = totalmillis;
+ }
+ invalidatemerges(c);
+}
+
+void calcmerges()
+{
+ genmergeprogress = 0;
+ genmerges();
+}
+
diff --git a/src/engine/octa.h b/src/engine/octa.h
new file mode 100644
index 0000000..6b7bfca
--- /dev/null
+++ b/src/engine/octa.h
@@ -0,0 +1,340 @@
+// 6-directional octree heightfield map format
+
+struct elementset
+{
+ ushort texture, lmid, envmap;
+ uchar dim, layer;
+ ushort length[2], minvert[2], maxvert[2];
+};
+
+enum
+{
+ EMID_NONE = 0,
+ EMID_CUSTOM,
+ EMID_SKY,
+ EMID_RESERVED
+};
+
+struct materialsurface
+{
+ ivec o;
+ ushort csize, rsize;
+ ushort material, skip;
+ uchar orient, visible;
+ union
+ {
+ short index;
+ short depth;
+ };
+ union
+ {
+ entity *light;
+ ushort envmap;
+ uchar ends;
+ };
+};
+
+struct vertinfo
+{
+ ushort x, y, z, u, v, norm;
+
+ void setxyz(ushort a, ushort b, ushort c) { x = a; y = b; z = c; }
+ void setxyz(const ivec &v) { setxyz(v.x, v.y, v.z); }
+ void set(ushort a, ushort b, ushort c, ushort s = 0, ushort t = 0, ushort n = 0) { setxyz(a, b, c); u = s; v = t; norm = n; }
+ void set(const ivec &v, ushort s = 0, ushort t = 0, ushort n = 0) { set(v.x, v.y, v.z, s, t, n); }
+ ivec getxyz() const { return ivec(x, y, z); }
+};
+
+enum
+{
+ LAYER_TOP = (1<<5),
+ LAYER_BOTTOM = (1<<6),
+ LAYER_DUP = (1<<7),
+
+ LAYER_BLEND = LAYER_TOP|LAYER_BOTTOM,
+
+ MAXFACEVERTS = 15
+};
+
+enum { LMID_AMBIENT = 0, LMID_AMBIENT1, LMID_BRIGHT, LMID_BRIGHT1, LMID_DARK, LMID_DARK1, LMID_RESERVED };
+
+struct surfaceinfo
+{
+ uchar lmid[2];
+ uchar verts, numverts;
+
+ int totalverts() const { return numverts&LAYER_DUP ? (numverts&MAXFACEVERTS)*2 : numverts&MAXFACEVERTS; }
+ bool used() const { return lmid[0] != LMID_AMBIENT || lmid[1] != LMID_AMBIENT || numverts&~LAYER_TOP; }
+ void clear() { lmid[0] = LMID_AMBIENT; lmid[1] = LMID_AMBIENT; numverts = (numverts&MAXFACEVERTS) | LAYER_TOP; }
+ void brighten() { lmid[0] = LMID_BRIGHT; lmid[1] = LMID_AMBIENT; numverts = (numverts&MAXFACEVERTS) | LAYER_TOP; }
+};
+
+static const surfaceinfo ambientsurface = {{LMID_AMBIENT, LMID_AMBIENT}, 0, LAYER_TOP};
+static const surfaceinfo brightsurface = {{LMID_BRIGHT, LMID_AMBIENT}, 0, LAYER_TOP};
+static const surfaceinfo brightbottomsurface = {{LMID_AMBIENT, LMID_BRIGHT}, 0, LAYER_BOTTOM};
+
+struct grasstri
+{
+ vec v[4];
+ int numv;
+ vec4 tcu, tcv;
+ plane surface;
+ vec center;
+ float radius;
+ float minz, maxz;
+ ushort texture, lmid;
+};
+
+struct occludequery
+{
+ void *owner;
+ GLuint id;
+ int fragments;
+};
+
+struct vtxarray;
+
+struct octaentities
+{
+ vector<int> mapmodels;
+ vector<int> other;
+ occludequery *query;
+ octaentities *next, *rnext;
+ int distance;
+ ivec o;
+ int size;
+ ivec bbmin, bbmax;
+
+ octaentities(const ivec &o, int size) : query(0), o(o), size(size), bbmin(o), bbmax(o)
+ {
+ bbmin.add(size);
+ }
+};
+
+enum
+{
+ OCCLUDE_NOTHING = 0,
+ OCCLUDE_GEOM,
+ OCCLUDE_BB,
+ OCCLUDE_PARENT
+};
+
+enum
+{
+ MERGE_ORIGIN = 1<<0,
+ MERGE_PART = 1<<1,
+ MERGE_USE = 1<<2
+};
+
+struct vtxarray
+{
+ vtxarray *parent;
+ vector<vtxarray *> children;
+ vtxarray *next, *rnext; // linked list of visible VOBs
+ vertex *vdata; // vertex data
+ ushort voffset; // offset into vertex data
+ ushort *edata, *skydata; // vertex indices
+ GLuint vbuf, ebuf, skybuf; // VBOs
+ ushort minvert, maxvert; // DRE info
+ elementset *eslist; // List of element indices sets (range) per texture
+ materialsurface *matbuf; // buffer of material surfaces
+ int verts, tris, texs, blendtris, blends, alphabacktris, alphaback, alphafronttris, alphafront, alphatris, texmask, sky, explicitsky, skyfaces, skyclip, matsurfs, distance;
+ double skyarea;
+ ivec o;
+ int size; // location and size of cube.
+ ivec geommin, geommax; // BB of geom
+ ivec shadowmapmin, shadowmapmax; // BB of shadowmapped surfaces
+ ivec matmin, matmax; // BB of any materials
+ ivec bbmin, bbmax; // BB of everything including children
+ uchar curvfc, occluded;
+ occludequery *query;
+ vector<octaentities *> mapmodels;
+ vector<grasstri> grasstris;
+ int hasmerges, mergelevel;
+ uint dynlightmask;
+ bool shadowed;
+};
+
+struct cube;
+
+struct clipplanes
+{
+ vec o, r, v[8];
+ plane p[12];
+ uchar side[12];
+ uchar size, visible;
+ const cube *owner;
+ int version;
+};
+
+struct facebounds
+{
+ ushort u1, u2, v1, v2;
+
+ bool empty() const { return u1 >= u2 || v1 >= v2; }
+};
+
+struct tjoint
+{
+ int next;
+ ushort offset;
+ uchar edge;
+};
+
+struct cubeext
+{
+ vtxarray *va; // vertex array for children, or NULL
+ octaentities *ents; // map entities inside cube
+ surfaceinfo surfaces[6]; // render info for each surface
+ int tjoints; // linked list of t-joints
+ uchar maxverts; // allocated space for verts
+
+ vertinfo *verts() { return (vertinfo *)(this+1); }
+};
+
+struct cube
+{
+ cube *children; // points to 8 cube structures which are its children, or NULL. -Z first, then -Y, -X
+ cubeext *ext; // extended info for the cube
+ union
+ {
+ uchar edges[12]; // edges of the cube, each uchar is 2 4bit values denoting the range.
+ // see documentation jpgs for more info.
+ uint faces[3]; // 4 edges of each dimension together representing 2 perpendicular faces
+ };
+ ushort texture[6]; // one for each face. same order as orient.
+ ushort material; // empty-space material
+ uchar merged; // merged faces of the cube
+ union
+ {
+ uchar escaped; // mask of which children have escaped merges
+ uchar visible; // visibility info for faces
+ };
+};
+
+struct block3
+{
+ ivec o, s;
+ int grid, orient;
+ block3() {}
+ block3(const selinfo &sel) : o(sel.o), s(sel.s), grid(sel.grid), orient(sel.orient) {}
+ cube *c() { return (cube *)(this+1); }
+ int size() const { return s.x*s.y*s.z; }
+};
+
+struct editinfo
+{
+ block3 *copy;
+ editinfo() : copy(NULL) {}
+};
+
+struct undoent { int i; entity e; };
+struct undoblock // undo header, all data sits in payload
+{
+ undoblock *prev, *next;
+ int size, timestamp, numents; // if numents is 0, is a cube undo record, otherwise an entity undo record
+
+ block3 *block() { return (block3 *)(this + 1); }
+ uchar *gridmap()
+ {
+ block3 *ub = block();
+ return (uchar *)(ub->c() + ub->size());
+ }
+ undoent *ents() { return (undoent *)(this + 1); }
+};
+
+extern cube *worldroot; // the world data. only a ptr to 8 cubes (ie: like cube.children above)
+extern int wtris, wverts, vtris, vverts, glde, gbatches, rplanes;
+extern int allocnodes, allocva, selchildcount, selchildmat;
+
+const uint F_EMPTY = 0; // all edges in the range (0,0)
+const uint F_SOLID = 0x80808080; // all edges in the range (0,8)
+
+#define isempty(c) ((c).faces[0]==F_EMPTY)
+#define isentirelysolid(c) ((c).faces[0]==F_SOLID && (c).faces[1]==F_SOLID && (c).faces[2]==F_SOLID)
+#define setfaces(c, face) { (c).faces[0] = (c).faces[1] = (c).faces[2] = face; }
+#define solidfaces(c) setfaces(c, F_SOLID)
+#define emptyfaces(c) setfaces(c, F_EMPTY)
+
+#define edgemake(a, b) ((b)<<4|a)
+#define edgeget(edge, coord) ((coord) ? (edge)>>4 : (edge)&0xF)
+#define edgeset(edge, coord, val) ((edge) = ((coord) ? ((edge)&0xF)|((val)<<4) : ((edge)&0xF0)|(val)))
+
+#define cubeedge(c, d, x, y) ((c).edges[(((d)<<2)+((y)<<1)+(x))])
+
+#define octadim(d) (1<<(d)) // creates mask for bit of given dimension
+#define octacoord(d, i) (((i)&octadim(d))>>(d))
+#define oppositeocta(d, i) ((i)^octadim(D[d]))
+#define octaindex(d,x,y,z) (((z)<<D[d])+((y)<<C[d])+((x)<<R[d]))
+#define octastep(x, y, z, scale) (((((z)>>(scale))&1)<<2) | ((((y)>>(scale))&1)<<1) | (((x)>>(scale))&1))
+
+static inline uchar octaboxoverlap(const ivec &o, int size, const ivec &bbmin, const ivec &bbmax)
+{
+ uchar p = 0xFF; // bitmask of possible collisions with octants. 0 bit = 0 octant, etc
+ ivec mid = ivec(o).add(size);
+ if(mid.z <= bbmin.z) p &= 0xF0; // not in a -ve Z octant
+ else if(mid.z >= bbmax.z) p &= 0x0F; // not in a +ve Z octant
+ if(mid.y <= bbmin.y) p &= 0xCC; // not in a -ve Y octant
+ else if(mid.y >= bbmax.y) p &= 0x33; // etc..
+ if(mid.x <= bbmin.x) p &= 0xAA;
+ else if(mid.x >= bbmax.x) p &= 0x55;
+ return p;
+}
+
+#define loopoctabox(o, size, bbmin, bbmax) uchar possible = octaboxoverlap(o, size, bbmin, bbmax); loopi(8) if(possible&(1<<i))
+#define loopoctaboxsize(o, size, bborigin, bbsize) uchar possible = octaboxoverlap(o, size, bborigin, ivec(bborigin).add(bbsize)); loopi(8) if(possible&(1<<i))
+
+enum
+{
+ O_LEFT = 0,
+ O_RIGHT,
+ O_BACK,
+ O_FRONT,
+ O_BOTTOM,
+ O_TOP
+};
+
+#define dimension(orient) ((orient)>>1)
+#define dimcoord(orient) ((orient)&1)
+#define opposite(orient) ((orient)^1)
+
+enum
+{
+ VFC_FULL_VISIBLE = 0,
+ VFC_PART_VISIBLE,
+ VFC_FOGGED,
+ VFC_NOT_VISIBLE,
+ PVS_FULL_VISIBLE,
+ PVS_PART_VISIBLE,
+ PVS_FOGGED
+};
+
+#define GENCUBEVERTS(x0,x1, y0,y1, z0,z1) \
+ GENCUBEVERT(0, x1, y1, z0) \
+ GENCUBEVERT(1, x0, y1, z0) \
+ GENCUBEVERT(2, x0, y1, z1) \
+ GENCUBEVERT(3, x1, y1, z1) \
+ GENCUBEVERT(4, x1, y0, z1) \
+ GENCUBEVERT(5, x0, y0, z1) \
+ GENCUBEVERT(6, x0, y0, z0) \
+ GENCUBEVERT(7, x1, y0, z0)
+
+#define GENFACEVERTX(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv)
+#define GENFACEVERTSX(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+ GENFACEORIENT(0, GENFACEVERTX(0,0, x0,y1,z1, d0,r1,c1), GENFACEVERTX(0,1, x0,y1,z0, d0,r1,c0), GENFACEVERTX(0,2, x0,y0,z0, d0,r0,c0), GENFACEVERTX(0,3, x0,y0,z1, d0,r0,c1)) \
+ GENFACEORIENT(1, GENFACEVERTX(1,0, x1,y1,z1, d1,r1,c1), GENFACEVERTX(1,1, x1,y0,z1, d1,r0,c1), GENFACEVERTX(1,2, x1,y0,z0, d1,r0,c0), GENFACEVERTX(1,3, x1,y1,z0, d1,r1,c0))
+#define GENFACEVERTY(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv)
+#define GENFACEVERTSY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+ GENFACEORIENT(2, GENFACEVERTY(2,0, x1,y0,z1, c1,d0,r1), GENFACEVERTY(2,1, x0,y0,z1, c0,d0,r1), GENFACEVERTY(2,2, x0,y0,z0, c0,d0,r0), GENFACEVERTY(2,3, x1,y0,z0, c1,d0,r0)) \
+ GENFACEORIENT(3, GENFACEVERTY(3,0, x0,y1,z0, c0,d1,r0), GENFACEVERTY(3,1, x0,y1,z1, c0,d1,r1), GENFACEVERTY(3,2, x1,y1,z1, c1,d1,r1), GENFACEVERTY(3,3, x1,y1,z0, c1,d1,r0))
+#define GENFACEVERTZ(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv)
+#define GENFACEVERTSZ(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+ GENFACEORIENT(4, GENFACEVERTZ(4,0, x0,y0,z0, r0,c0,d0), GENFACEVERTZ(4,1, x0,y1,z0, r0,c1,d0), GENFACEVERTZ(4,2, x1,y1,z0, r1,c1,d0), GENFACEVERTZ(4,3, x1,y0,z0, r1,c0,d0)) \
+ GENFACEORIENT(5, GENFACEVERTZ(5,0, x0,y0,z1, r0,c0,d1), GENFACEVERTZ(5,1, x1,y0,z1, r1,c0,d1), GENFACEVERTZ(5,2, x1,y1,z1, r1,c1,d1), GENFACEVERTZ(5,3, x0,y1,z1, r0,c1,d1))
+#define GENFACEVERTSXY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+ GENFACEVERTSX(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+ GENFACEVERTSY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1)
+#define GENFACEVERTS(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+ GENFACEVERTSXY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+ GENFACEVERTSZ(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1)
+
diff --git a/src/engine/octaedit.cpp b/src/engine/octaedit.cpp
new file mode 100644
index 0000000..5baa6f0
--- /dev/null
+++ b/src/engine/octaedit.cpp
@@ -0,0 +1,2977 @@
+#include "engine.h"
+
+extern int outline;
+
+bool boxoutline = false;
+
+void boxs(int orient, vec o, const vec &s, float size)
+{
+ int d = dimension(orient), dc = dimcoord(orient);
+ float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0;
+ o[D[d]] += dc * s[D[d]] + f;
+
+ vec r(0, 0, 0), c(0, 0, 0);
+ r[R[d]] = s[R[d]];
+ c[C[d]] = s[C[d]];
+
+ vec v1 = o, v2 = vec(o).add(r), v3 = vec(o).add(r).add(c), v4 = vec(o).add(c);
+
+ r[R[d]] = 0.5f*size;
+ c[C[d]] = 0.5f*size;
+
+ gle::defvertex();
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attrib(vec(v1).sub(r).sub(c));
+ gle::attrib(vec(v1).add(r).add(c));
+ gle::attrib(vec(v2).add(r).sub(c));
+ gle::attrib(vec(v2).sub(r).add(c));
+ gle::attrib(vec(v3).add(r).add(c));
+ gle::attrib(vec(v3).sub(r).sub(c));
+ gle::attrib(vec(v4).sub(r).add(c));
+ gle::attrib(vec(v4).add(r).sub(c));
+ gle::attrib(vec(v1).sub(r).sub(c));
+ gle::attrib(vec(v1).add(r).add(c));
+ xtraverts += gle::end();
+}
+
+void boxs(int orient, vec o, const vec &s)
+{
+ int d = dimension(orient), dc = dimcoord(orient);
+ float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0;
+ o[D[d]] += dc * s[D[d]] + f;
+
+ gle::defvertex();
+ gle::begin(GL_LINE_LOOP);
+
+ gle::attrib(o); o[R[d]] += s[R[d]];
+ gle::attrib(o); o[C[d]] += s[C[d]];
+ gle::attrib(o); o[R[d]] -= s[R[d]];
+ gle::attrib(o);
+
+ xtraverts += gle::end();
+}
+
+void boxs3D(const vec &o, vec s, int g)
+{
+ s.mul(g);
+ loopi(6)
+ boxs(i, o, s);
+}
+
+void boxsgrid(int orient, vec o, vec s, int g)
+{
+ int d = dimension(orient), dc = dimcoord(orient);
+ float ox = o[R[d]],
+ oy = o[C[d]],
+ xs = s[R[d]],
+ ys = s[C[d]],
+ f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0;
+
+ o[D[d]] += dc * s[D[d]]*g + f;
+
+ gle::defvertex();
+ gle::begin(GL_LINES);
+ loop(x, xs)
+ {
+ o[R[d]] += g;
+ gle::attrib(o);
+ o[C[d]] += ys*g;
+ gle::attrib(o);
+ o[C[d]] = oy;
+ }
+ loop(y, ys)
+ {
+ o[C[d]] += g;
+ o[R[d]] = ox;
+ gle::attrib(o);
+ o[R[d]] += xs*g;
+ gle::attrib(o);
+ }
+ xtraverts += gle::end();
+}
+
+selinfo sel, lastsel, savedsel;
+
+int orient = 0;
+int gridsize = 8;
+ivec cor, lastcor;
+ivec cur, lastcur;
+
+extern int entediting;
+bool editmode = false;
+bool havesel = false;
+bool hmapsel = false;
+int horient = 0;
+
+extern int entmoving;
+
+VARF(dragging, 0, 0, 1,
+ if(!dragging || cor[0]<0) return;
+ lastcur = cur;
+ lastcor = cor;
+ sel.grid = gridsize;
+ sel.orient = orient;
+);
+
+int moving = 0;
+ICOMMAND(moving, "b", (int *n),
+{
+ if(*n >= 0)
+ {
+ if(!*n || (moving<=1 && !pointinsel(sel, vec(cur).add(1)))) moving = 0;
+ else if(!moving) moving = 1;
+ }
+ intret(moving);
+});
+
+VARF(gridpower, 0, 3, 12,
+{
+ if(dragging) return;
+ gridsize = 1<<gridpower;
+ if(gridsize>=worldsize) gridsize = worldsize/2;
+ cancelsel();
+});
+
+VAR(passthroughsel, 0, 0, 1);
+VAR(editing, 1, 0, 0);
+VAR(selectcorners, 0, 0, 1);
+VARF(hmapedit, 0, 0, 1, horient = sel.orient);
+
+void forcenextundo() { lastsel.orient = -1; }
+
+extern void hmapcancel();
+
+void cubecancel()
+{
+ havesel = false;
+ moving = dragging = hmapedit = passthroughsel = 0;
+ forcenextundo();
+ hmapcancel();
+}
+
+void cancelsel()
+{
+ cubecancel();
+ entcancel();
+}
+
+void toggleedit(bool force)
+{
+ if(!force)
+ {
+ if(!isconnected()) return;
+ if(player->state!=CS_ALIVE && player->state!=CS_DEAD && player->state!=CS_EDITING) return; // do not allow dead players to edit to avoid state confusion
+ if(!game::allowedittoggle()) return; // not in most multiplayer modes
+ }
+ if(!(editmode = !editmode))
+ {
+ player->state = player->editstate;
+ player->o.z -= player->eyeheight; // entinmap wants feet pos
+ entinmap(player); // find spawn closest to current floating pos
+ }
+ else
+ {
+ game::resetgamestate();
+ player->editstate = player->state;
+ player->state = CS_EDITING;
+ }
+ cancelsel();
+ stoppaintblendmap();
+ keyrepeat(editmode);
+ editing = entediting = editmode;
+ extern int fullbright;
+ if(fullbright) { initlights(); lightents(); }
+ if(!force) game::edittoggled(editmode);
+}
+
+VARP(editinview, 0, 1, 1);
+
+bool noedit(bool view, bool msg)
+{
+ if(!editmode) { if(msg) conoutf(CON_ERROR, "operation only allowed in edit mode"); return true; }
+ if(view || haveselent()) return false;
+ float r = 1.0f;
+ vec o(sel.o), s(sel.s);
+ s.mul(float(sel.grid) / 2.0f);
+ o.add(s);
+ r = float(max(s.x, max(s.y, s.z)));
+ bool viewable = (isvisiblesphere(r, o) != VFC_NOT_VISIBLE);
+ if(viewable || !editinview) return false;
+ if(msg) conoutf(CON_ERROR, "selection not in view");
+ return true;
+}
+
+void reorient()
+{
+ sel.cx = 0;
+ sel.cy = 0;
+ sel.cxs = sel.s[R[dimension(orient)]]*2;
+ sel.cys = sel.s[C[dimension(orient)]]*2;
+ sel.orient = orient;
+}
+
+void selextend()
+{
+ if(noedit(true)) return;
+ loopi(3)
+ {
+ if(cur[i]<sel.o[i])
+ {
+ sel.s[i] += (sel.o[i]-cur[i])/sel.grid;
+ sel.o[i] = cur[i];
+ }
+ else if(cur[i]>=sel.o[i]+sel.s[i]*sel.grid)
+ {
+ sel.s[i] = (cur[i]-sel.o[i])/sel.grid+1;
+ }
+ }
+}
+
+ICOMMAND(edittoggle, "", (), toggleedit(false));
+COMMAND(entcancel, "");
+COMMAND(cubecancel, "");
+COMMAND(cancelsel, "");
+COMMAND(reorient, "");
+COMMAND(selextend, "");
+
+ICOMMAND(selmoved, "", (), { if(noedit(true)) return; intret(sel.o != savedsel.o ? 1 : 0); });
+ICOMMAND(selsave, "", (), { if(noedit(true)) return; savedsel = sel; });
+ICOMMAND(selrestore, "", (), { if(noedit(true)) return; sel = savedsel; });
+ICOMMAND(selswap, "", (), { if(noedit(true)) return; swap(sel, savedsel); });
+
+ICOMMAND(getselpos, "", (),
+{
+ if(noedit(true)) return;
+ defformatstring(pos, "%s %s %s", floatstr(sel.o.x), floatstr(sel.o.y), floatstr(sel.o.z));
+ result(pos);
+});
+
+void setselpos(int *x, int *y, int *z)
+{
+ if(noedit(moving!=0)) return;
+ havesel = true;
+ sel.o = ivec(*x, *y, *z).mask(~(gridsize-1));
+}
+COMMAND(setselpos, "iii");
+
+void movesel(int *dir, int *dim)
+{
+ if(noedit(moving!=0)) return;
+ if(*dim < 0 || *dim > 2) return;
+ sel.o[*dim] += *dir * sel.grid;
+}
+COMMAND(movesel, "ii");
+
+///////// selection support /////////////
+
+cube &blockcube(int x, int y, int z, const block3 &b, int rgrid) // looks up a world cube, based on coordinates mapped by the block
+{
+ int dim = dimension(b.orient), dc = dimcoord(b.orient);
+ ivec s(dim, x*b.grid, y*b.grid, dc*(b.s[dim]-1)*b.grid);
+ s.add(b.o);
+ if(dc) s[dim] -= z*b.grid; else s[dim] += z*b.grid;
+ return lookupcube(s, rgrid);
+}
+
+#define loopxy(b) loop(y,(b).s[C[dimension((b).orient)]]) loop(x,(b).s[R[dimension((b).orient)]])
+#define loopxyz(b, r, f) { loop(z,(b).s[D[dimension((b).orient)]]) loopxy((b)) { cube &c = blockcube(x,y,z,b,r); f; } }
+#define loopselxyz(f) { if(local) makeundo(); loopxyz(sel, sel.grid, f); changed(sel); }
+#define selcube(x, y, z) blockcube(x, y, z, sel, sel.grid)
+
+////////////// cursor ///////////////
+
+int selchildcount = 0, selchildmat = -1;
+
+ICOMMAND(havesel, "", (), intret(havesel ? selchildcount : 0));
+
+void countselchild(cube *c, const ivec &cor, int size)
+{
+ ivec ss = ivec(sel.s).mul(sel.grid);
+ loopoctaboxsize(cor, size, sel.o, ss)
+ {
+ ivec o(i, cor, size);
+ if(c[i].children) countselchild(c[i].children, o, size/2);
+ else
+ {
+ selchildcount++;
+ if(c[i].material != MAT_AIR && selchildmat != MAT_AIR)
+ {
+ if(selchildmat < 0) selchildmat = c[i].material;
+ else if(selchildmat != c[i].material) selchildmat = MAT_AIR;
+ }
+ }
+ }
+}
+
+void normalizelookupcube(const ivec &o)
+{
+ if(lusize>gridsize)
+ {
+ lu.x += (o.x-lu.x)/gridsize*gridsize;
+ lu.y += (o.y-lu.y)/gridsize*gridsize;
+ lu.z += (o.z-lu.z)/gridsize*gridsize;
+ }
+ else if(gridsize>lusize)
+ {
+ lu.x &= ~(gridsize-1);
+ lu.y &= ~(gridsize-1);
+ lu.z &= ~(gridsize-1);
+ }
+ lusize = gridsize;
+}
+
+void updateselection()
+{
+ sel.o.x = min(lastcur.x, cur.x);
+ sel.o.y = min(lastcur.y, cur.y);
+ sel.o.z = min(lastcur.z, cur.z);
+ sel.s.x = abs(lastcur.x-cur.x)/sel.grid+1;
+ sel.s.y = abs(lastcur.y-cur.y)/sel.grid+1;
+ sel.s.z = abs(lastcur.z-cur.z)/sel.grid+1;
+}
+
+bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first)
+{
+ plane pl(d, off);
+ float dist = 0.0f;
+ if(!pl.rayintersect(player->o, ray, dist))
+ return false;
+
+ dest = vec(ray).mul(dist).add(player->o);
+ if(first) handle = vec(dest).sub(o);
+ dest.sub(handle);
+ return true;
+}
+
+inline bool isheightmap(int orient, int d, bool empty, cube *c);
+extern void entdrag(const vec &ray);
+extern bool hoveringonent(int ent, int orient);
+extern void renderentselection(const vec &o, const vec &ray, bool entmoving);
+extern float rayent(const vec &o, const vec &ray, float radius, int mode, int size, int &orient, int &ent);
+
+VAR(gridlookup, 0, 0, 1);
+VAR(passthroughcube, 0, 1, 1);
+VAR(passthroughent, 0, 1, 1);
+VARF(passthrough, 0, 0, 1, { passthroughsel = passthrough; entcancel(); });
+
+void rendereditcursor()
+{
+ int d = dimension(sel.orient),
+ od = dimension(orient),
+ odc = dimcoord(orient);
+
+ bool hidecursor = g3d_windowhit(true, false) || blendpaintmode, hovering = false;
+ hmapsel = false;
+
+ if(moving)
+ {
+ static vec dest, handle;
+ if(editmoveplane(vec(sel.o), camdir, od, sel.o[D[od]]+odc*sel.grid*sel.s[D[od]], handle, dest, moving==1))
+ {
+ if(moving==1)
+ {
+ dest.add(handle);
+ handle = vec(ivec(handle).mask(~(sel.grid-1)));
+ dest.sub(handle);
+ moving = 2;
+ }
+ ivec o = ivec(dest).mask(~(sel.grid-1));
+ sel.o[R[od]] = o[R[od]];
+ sel.o[C[od]] = o[C[od]];
+ }
+ }
+ else
+ if(entmoving)
+ {
+ entdrag(camdir);
+ }
+ else
+ {
+ ivec w;
+ float sdist = 0, wdist = 0, t;
+ int entorient = 0, ent = -1;
+
+ wdist = rayent(player->o, camdir, 1e16f,
+ (editmode && showmat ? RAY_EDITMAT : 0) // select cubes first
+ | (!dragging && entediting && (!passthrough || !passthroughent) ? RAY_ENTS : 0)
+ | RAY_SKIPFIRST
+ | (passthroughcube || passthrough ? RAY_PASS : 0), gridsize, entorient, ent);
+
+ if((havesel || dragging) && !passthroughsel && !hmapedit) // now try selecting the selection
+ if(rayboxintersect(vec(sel.o), vec(sel.s).mul(sel.grid), player->o, camdir, sdist, orient))
+ { // and choose the nearest of the two
+ if(sdist < wdist)
+ {
+ wdist = sdist;
+ ent = -1;
+ }
+ }
+
+ if((hovering = hoveringonent(hidecursor ? -1 : ent, entorient)))
+ {
+ if(!havesel)
+ {
+ selchildcount = 0;
+ selchildmat = -1;
+ sel.s = ivec(0, 0, 0);
+ }
+ }
+ else
+ {
+ vec w = vec(camdir).mul(wdist+0.05f).add(player->o);
+ if(!insideworld(w))
+ {
+ loopi(3) wdist = min(wdist, ((camdir[i] > 0 ? worldsize : 0) - player->o[i]) / camdir[i]);
+ w = vec(camdir).mul(wdist-0.05f).add(player->o);
+ if(!insideworld(w))
+ {
+ wdist = 0;
+ loopi(3) w[i] = clamp(player->o[i], 0.0f, float(worldsize));
+ }
+ }
+ cube *c = &lookupcube(ivec(w));
+ if(gridlookup && !dragging && !moving && !havesel && hmapedit!=1) gridsize = lusize;
+ int mag = lusize / gridsize;
+ normalizelookupcube(ivec(w));
+ if(sdist == 0 || sdist > wdist) rayboxintersect(vec(lu), vec(gridsize), player->o, camdir, t=0, orient); // just getting orient
+ cur = lu;
+ cor = ivec(vec(w).mul(2).div(gridsize));
+ od = dimension(orient);
+ d = dimension(sel.orient);
+
+ if(hmapedit==1 && dimcoord(horient) == (camdir[dimension(horient)]<0))
+ {
+ hmapsel = isheightmap(horient, dimension(horient), false, c);
+ if(hmapsel)
+ od = dimension(orient = horient);
+ }
+
+ if(dragging)
+ {
+ updateselection();
+ sel.cx = min(cor[R[d]], lastcor[R[d]]);
+ sel.cy = min(cor[C[d]], lastcor[C[d]]);
+ sel.cxs = max(cor[R[d]], lastcor[R[d]]);
+ sel.cys = max(cor[C[d]], lastcor[C[d]]);
+
+ if(!selectcorners)
+ {
+ sel.cx &= ~1;
+ sel.cy &= ~1;
+ sel.cxs &= ~1;
+ sel.cys &= ~1;
+ sel.cxs -= sel.cx-2;
+ sel.cys -= sel.cy-2;
+ }
+ else
+ {
+ sel.cxs -= sel.cx-1;
+ sel.cys -= sel.cy-1;
+ }
+
+ sel.cx &= 1;
+ sel.cy &= 1;
+ havesel = true;
+ }
+ else if(!havesel)
+ {
+ sel.o = lu;
+ sel.s.x = sel.s.y = sel.s.z = 1;
+ sel.cx = sel.cy = 0;
+ sel.cxs = sel.cys = 2;
+ sel.grid = gridsize;
+ sel.orient = orient;
+ d = od;
+ }
+
+ sel.corner = (cor[R[d]]-(lu[R[d]]*2)/gridsize)+(cor[C[d]]-(lu[C[d]]*2)/gridsize)*2;
+ selchildcount = 0;
+ selchildmat = -1;
+ countselchild(worldroot, ivec(0, 0, 0), worldsize/2);
+ if(mag>=1 && selchildcount==1)
+ {
+ selchildmat = c->material;
+ if(mag>1) selchildcount = -mag;
+ }
+ }
+ }
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE);
+
+ // cursors
+
+ notextureshader->set();
+
+ renderentselection(player->o, camdir, entmoving!=0);
+
+ boxoutline = outline!=0;
+
+ enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+ if(!moving && !hovering && !hidecursor)
+ {
+ if(hmapedit==1)
+ gle::colorub(0, hmapsel ? 255 : 40, 0);
+ else
+ gle::colorub(120,120,120);
+ boxs(orient, vec(lu), vec(lusize));
+ }
+
+ // selections
+ if(havesel || moving)
+ {
+ d = dimension(sel.orient);
+ gle::colorub(50,50,50); // grid
+ boxsgrid(sel.orient, vec(sel.o), vec(sel.s), sel.grid);
+ gle::colorub(200,0,0); // 0 reference
+ boxs3D(vec(sel.o).sub(0.5f*min(gridsize*0.25f, 2.0f)), vec(min(gridsize*0.25f, 2.0f)), 1);
+ gle::colorub(200,200,200);// 2D selection box
+ vec co(sel.o.v), cs(sel.s.v);
+ co[R[d]] += 0.5f*(sel.cx*gridsize);
+ co[C[d]] += 0.5f*(sel.cy*gridsize);
+ cs[R[d]] = 0.5f*(sel.cxs*gridsize);
+ cs[C[d]] = 0.5f*(sel.cys*gridsize);
+ cs[D[d]] *= gridsize;
+ boxs(sel.orient, co, cs);
+ if(hmapedit==1) // 3D selection box
+ gle::colorub(0,120,0);
+ else
+ gle::colorub(0,0,120);
+ boxs3D(vec(sel.o), vec(sel.s), sel.grid);
+ }
+
+ disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+ boxoutline = false;
+
+ glDisable(GL_BLEND);
+}
+
+void tryedit()
+{
+ extern int hidehud;
+ if(!editmode || hidehud || mainmenu) return;
+ if(blendpaintmode) trypaintblendmap();
+}
+
+//////////// ready changes to vertex arrays ////////////
+
+static bool haschanged = false;
+
+void readychanges(const ivec &bbmin, const ivec &bbmax, cube *c, const ivec &cor, int size)
+{
+ loopoctabox(cor, size, bbmin, bbmax)
+ {
+ ivec o(i, cor, size);
+ if(c[i].ext)
+ {
+ if(c[i].ext->va) // removes va s so that octarender will recreate
+ {
+ int hasmerges = c[i].ext->va->hasmerges;
+ destroyva(c[i].ext->va);
+ c[i].ext->va = NULL;
+ if(hasmerges) invalidatemerges(c[i], o, size, true);
+ }
+ freeoctaentities(c[i]);
+ c[i].ext->tjoints = -1;
+ }
+ if(c[i].children)
+ {
+ if(size<=1)
+ {
+ solidfaces(c[i]);
+ discardchildren(c[i], true);
+ brightencube(c[i]);
+ }
+ else readychanges(bbmin, bbmax, c[i].children, o, size/2);
+ }
+ else brightencube(c[i]);
+ }
+}
+
+void commitchanges(bool force)
+{
+ if(!force && !haschanged) return;
+ haschanged = false;
+
+ int oldlen = valist.length();
+ resetclipplanes();
+ entitiesinoctanodes();
+ inbetweenframes = false;
+ octarender();
+ inbetweenframes = true;
+ setupmaterials(oldlen);
+ invalidatepostfx();
+ updatevabbs();
+ resetblobs();
+}
+
+void changed(const block3 &sel, bool commit = true)
+{
+ if(sel.s.iszero()) return;
+ readychanges(ivec(sel.o).sub(1), ivec(sel.s).mul(sel.grid).add(sel.o).add(1), worldroot, ivec(0, 0, 0), worldsize/2);
+ haschanged = true;
+
+ if(commit) commitchanges();
+}
+
+//////////// copy and undo /////////////
+static inline void copycube(const cube &src, cube &dst)
+{
+ dst = src;
+ dst.visible = 0;
+ dst.merged = 0;
+ dst.ext = NULL; // src cube is responsible for va destruction
+ if(src.children)
+ {
+ dst.children = newcubes(F_EMPTY);
+ loopi(8) copycube(src.children[i], dst.children[i]);
+ }
+}
+
+static inline void pastecube(const cube &src, cube &dst)
+{
+ discardchildren(dst);
+ copycube(src, dst);
+}
+
+void blockcopy(const block3 &s, int rgrid, block3 *b)
+{
+ *b = s;
+ cube *q = b->c();
+ loopxyz(s, rgrid, copycube(c, *q++));
+}
+
+block3 *blockcopy(const block3 &s, int rgrid)
+{
+ int bsize = sizeof(block3)+sizeof(cube)*s.size();
+ if(bsize <= 0 || bsize > (100<<20)) return NULL;
+ block3 *b = (block3 *)new (false) uchar[bsize];
+ if(b) blockcopy(s, rgrid, b);
+ return b;
+}
+
+void freeblock(block3 *b, bool alloced = true)
+{
+ cube *q = b->c();
+ loopi(b->size()) discardchildren(*q++);
+ if(alloced) delete[] b;
+}
+
+void selgridmap(selinfo &sel, uchar *g) // generates a map of the cube sizes at each grid point
+{
+ loopxyz(sel, -sel.grid, (*g++ = bitscan(lusize), (void)c));
+}
+
+void freeundo(undoblock *u)
+{
+ if(!u->numents) freeblock(u->block(), false);
+ delete[] (uchar *)u;
+}
+
+void pasteundoblock(block3 *b, uchar *g)
+{
+ cube *s = b->c();
+ loopxyz(*b, 1<<min(int(*g++), worldscale-1), pastecube(*s++, c));
+}
+
+void pasteundo(undoblock *u)
+{
+ if(u->numents) pasteundoents(u);
+ else pasteundoblock(u->block(), u->gridmap());
+}
+
+static inline int undosize(undoblock *u)
+{
+ if(u->numents) return u->numents*sizeof(undoent);
+ else
+ {
+ block3 *b = u->block();
+ cube *q = b->c();
+ int size = b->size(), total = size;
+ loopj(size) total += familysize(*q++)*sizeof(cube);
+ return total;
+ }
+}
+
+struct undolist
+{
+ undoblock *first, *last;
+
+ undolist() : first(NULL), last(NULL) {}
+
+ bool empty() { return !first; }
+
+ void add(undoblock *u)
+ {
+ u->next = NULL;
+ u->prev = last;
+ if(!first) first = last = u;
+ else
+ {
+ last->next = u;
+ last = u;
+ }
+ }
+
+ undoblock *popfirst()
+ {
+ undoblock *u = first;
+ first = first->next;
+ if(first) first->prev = NULL;
+ else last = NULL;
+ return u;
+ }
+
+ undoblock *poplast()
+ {
+ undoblock *u = last;
+ last = last->prev;
+ if(last) last->next = NULL;
+ else first = NULL;
+ return u;
+ }
+};
+
+undolist undos, redos;
+VARP(undomegs, 0, 8, 100); // bounded by n megs
+int totalundos = 0;
+
+void pruneundos(int maxremain) // bound memory
+{
+ while(totalundos > maxremain && !undos.empty())
+ {
+ undoblock *u = undos.popfirst();
+ totalundos -= u->size;
+ freeundo(u);
+ }
+ //conoutf(CON_DEBUG, "undo: %d of %d(%%%d)", totalundos, undomegs<<20, totalundos*100/(undomegs<<20));
+ while(!redos.empty())
+ {
+ undoblock *u = redos.popfirst();
+ totalundos -= u->size;
+ freeundo(u);
+ }
+}
+
+void clearundos() { pruneundos(0); }
+
+COMMAND(clearundos, "");
+
+undoblock *newundocube(selinfo &s)
+{
+ int ssize = s.size(),
+ selgridsize = ssize,
+ blocksize = sizeof(block3)+ssize*sizeof(cube);
+ if(blocksize <= 0 || blocksize > (undomegs<<20)) return NULL;
+ undoblock *u = (undoblock *)new (false) uchar[sizeof(undoblock) + blocksize + selgridsize];
+ if(!u) return NULL;
+ u->numents = 0;
+ block3 *b = u->block();
+ blockcopy(s, -s.grid, b);
+ uchar *g = u->gridmap();
+ selgridmap(s, g);
+ return u;
+}
+
+void addundo(undoblock *u)
+{
+ u->size = undosize(u);
+ u->timestamp = totalmillis;
+ undos.add(u);
+ totalundos += u->size;
+ pruneundos(undomegs<<20);
+}
+
+VARP(nompedit, 0, 1, 1);
+
+void makeundo(selinfo &s)
+{
+ undoblock *u = newundocube(s);
+ if(u) addundo(u);
+}
+
+void makeundo() // stores state of selected cubes before editing
+{
+ if(lastsel==sel || sel.s.iszero()) return;
+ lastsel=sel;
+ makeundo(sel);
+}
+
+static inline int countblock(cube *c, int n = 8)
+{
+ int r = 0;
+ loopi(n) if(c[i].children) r += countblock(c[i].children); else ++r;
+ return r;
+}
+
+static int countblock(block3 *b) { return countblock(b->c(), b->size()); }
+
+void swapundo(undolist &a, undolist &b, int op)
+{
+ if(noedit()) return;
+ if(a.empty()) { conoutf(CON_WARN, "nothing more to %s", op == EDIT_REDO ? "redo" : "undo"); return; }
+ int ts = a.last->timestamp;
+ if(multiplayer(false))
+ {
+ int n = 0, ops = 0;
+ for(undoblock *u = a.last; u && ts==u->timestamp; u = u->prev)
+ {
+ ++ops;
+ n += u->numents ? u->numents : countblock(u->block());
+ if(ops > 10 || n > 2500)
+ {
+ conoutf(CON_WARN, "undo too big for multiplayer");
+ if(nompedit) { multiplayer(); return; }
+ op = -1;
+ break;
+ }
+ }
+ }
+ selinfo l = sel;
+ while(!a.empty() && ts==a.last->timestamp)
+ {
+ if(op >= 0) game::edittrigger(sel, op);
+ undoblock *u = a.poplast(), *r;
+ if(u->numents) r = copyundoents(u);
+ else
+ {
+ block3 *ub = u->block();
+ l.o = ub->o;
+ l.s = ub->s;
+ l.grid = ub->grid;
+ l.orient = ub->orient;
+ r = newundocube(l);
+ }
+ if(r)
+ {
+ r->size = u->size;
+ r->timestamp = totalmillis;
+ b.add(r);
+ }
+ pasteundo(u);
+ if(!u->numents) changed(*u->block(), false);
+ freeundo(u);
+ }
+ commitchanges();
+ if(!hmapsel)
+ {
+ sel = l;
+ reorient();
+ }
+ forcenextundo();
+}
+
+void editundo() { swapundo(undos, redos, EDIT_UNDO); }
+void editredo() { swapundo(redos, undos, EDIT_REDO); }
+
+// guard against subdivision
+#define protectsel(f) { undoblock *_u = newundocube(sel); f; if(_u) { pasteundo(_u); freeundo(_u); } }
+
+vector<editinfo *> editinfos;
+editinfo *localedit = NULL;
+
+template<class B>
+static void packcube(cube &c, B &buf)
+{
+ if(c.children)
+ {
+ buf.put(0xFF);
+ loopi(8) packcube(c.children[i], buf);
+ }
+ else
+ {
+ cube data = c;
+ lilswap(data.texture, 6);
+ buf.put(c.material&0xFF);
+ buf.put(c.material>>8);
+ buf.put(data.edges, sizeof(data.edges));
+ buf.put((uchar *)data.texture, sizeof(data.texture));
+ }
+}
+
+template<class B>
+static bool packblock(block3 &b, B &buf)
+{
+ if(b.size() <= 0 || b.size() > (1<<20)) return false;
+ block3 hdr = b;
+ lilswap(hdr.o.v, 3);
+ lilswap(hdr.s.v, 3);
+ lilswap(&hdr.grid, 1);
+ lilswap(&hdr.orient, 1);
+ buf.put((const uchar *)&hdr, sizeof(hdr));
+ cube *c = b.c();
+ loopi(b.size()) packcube(c[i], buf);
+ return true;
+}
+
+struct vslothdr
+{
+ ushort index;
+ ushort slot;
+};
+
+static void packvslots(cube &c, vector<uchar> &buf, vector<ushort> &used)
+{
+ if(c.children)
+ {
+ loopi(8) packvslots(c.children[i], buf, used);
+ }
+ else loopi(6)
+ {
+ ushort index = c.texture[i];
+ if(vslots.inrange(index) && vslots[index]->changed && used.find(index) < 0)
+ {
+ used.add(index);
+ VSlot &vs = *vslots[index];
+ vslothdr &hdr = *(vslothdr *)buf.pad(sizeof(vslothdr));
+ hdr.index = index;
+ hdr.slot = vs.slot->index;
+ lilswap(&hdr.index, 2);
+ packvslot(buf, vs);
+ }
+ }
+}
+
+static void packvslots(block3 &b, vector<uchar> &buf)
+{
+ vector<ushort> used;
+ cube *c = b.c();
+ loopi(b.size()) packvslots(c[i], buf, used);
+ memset(buf.pad(sizeof(vslothdr)), 0, sizeof(vslothdr));
+}
+
+template<class B>
+static void unpackcube(cube &c, B &buf)
+{
+ int mat = buf.get();
+ if(mat == 0xFF)
+ {
+ c.children = newcubes(F_EMPTY);
+ loopi(8) unpackcube(c.children[i], buf);
+ }
+ else
+ {
+ c.material = mat | (buf.get()<<8);
+ buf.get(c.edges, sizeof(c.edges));
+ buf.get((uchar *)c.texture, sizeof(c.texture));
+ lilswap(c.texture, 6);
+ }
+}
+
+template<class B>
+static bool unpackblock(block3 *&b, B &buf)
+{
+ if(b) { freeblock(b); b = NULL; }
+ block3 hdr;
+ if(buf.get((uchar *)&hdr, sizeof(hdr)) < int(sizeof(hdr))) return false;
+ lilswap(hdr.o.v, 3);
+ lilswap(hdr.s.v, 3);
+ lilswap(&hdr.grid, 1);
+ lilswap(&hdr.orient, 1);
+ if(hdr.size() > (1<<20) || hdr.grid <= 0 || hdr.grid > (1<<12)) return false;
+ b = (block3 *)new (false) uchar[sizeof(block3)+hdr.size()*sizeof(cube)];
+ if(!b) return false;
+ *b = hdr;
+ cube *c = b->c();
+ memset(c, 0, b->size()*sizeof(cube));
+ loopi(b->size()) unpackcube(c[i], buf);
+ return true;
+}
+
+struct vslotmap
+{
+ int index;
+ VSlot *vslot;
+
+ vslotmap() {}
+ vslotmap(int index, VSlot *vslot) : index(index), vslot(vslot) {}
+};
+static vector<vslotmap> unpackingvslots;
+
+static void unpackvslots(cube &c, ucharbuf &buf)
+{
+ if(c.children)
+ {
+ loopi(8) unpackvslots(c.children[i], buf);
+ }
+ else loopi(6)
+ {
+ ushort tex = c.texture[i];
+ loopvj(unpackingvslots) if(unpackingvslots[j].index == tex) { c.texture[i] = unpackingvslots[j].vslot->index; break; }
+ }
+}
+
+static void unpackvslots(block3 &b, ucharbuf &buf)
+{
+ while(buf.remaining() >= int(sizeof(vslothdr)))
+ {
+ vslothdr &hdr = *(vslothdr *)buf.pad(sizeof(vslothdr));
+ lilswap(&hdr.index, 2);
+ if(!hdr.index) break;
+ VSlot &vs = *lookupslot(hdr.slot, false).variants;
+ VSlot ds;
+ if(!unpackvslot(buf, ds, false)) break;
+ if(vs.index < 0 || vs.index == DEFAULT_SKY) continue;
+ VSlot *edit = editvslot(vs, ds);
+ unpackingvslots.add(vslotmap(hdr.index, edit ? edit : &vs));
+ }
+
+ cube *c = b.c();
+ loopi(b.size()) unpackvslots(c[i], buf);
+
+ unpackingvslots.setsize(0);
+}
+
+static bool compresseditinfo(const uchar *inbuf, int inlen, uchar *&outbuf, int &outlen)
+{
+ uLongf len = compressBound(inlen);
+ if(len > (1<<20)) return false;
+ outbuf = new (false) uchar[len];
+ if(!outbuf || compress2((Bytef *)outbuf, &len, (const Bytef *)inbuf, inlen, Z_BEST_COMPRESSION) != Z_OK || len > (1<<16))
+ {
+ delete[] outbuf;
+ outbuf = NULL;
+ return false;
+ }
+ outlen = len;
+ return true;
+}
+
+static bool uncompresseditinfo(const uchar *inbuf, int inlen, uchar *&outbuf, int &outlen)
+{
+ if(compressBound(outlen) > (1<<20)) return false;
+ uLongf len = outlen;
+ outbuf = new (false) uchar[len];
+ if(!outbuf || uncompress((Bytef *)outbuf, &len, (const Bytef *)inbuf, inlen) != Z_OK)
+ {
+ delete[] outbuf;
+ outbuf = NULL;
+ return false;
+ }
+ outlen = len;
+ return true;
+}
+
+bool packeditinfo(editinfo *e, int &inlen, uchar *&outbuf, int &outlen)
+{
+ vector<uchar> buf;
+ if(!e || !e->copy || !packblock(*e->copy, buf)) return false;
+ packvslots(*e->copy, buf);
+ inlen = buf.length();
+ return compresseditinfo(buf.getbuf(), buf.length(), outbuf, outlen);
+}
+
+bool unpackeditinfo(editinfo *&e, const uchar *inbuf, int inlen, int outlen)
+{
+ if(e && e->copy) { freeblock(e->copy); e->copy = NULL; }
+ uchar *outbuf = NULL;
+ if(!uncompresseditinfo(inbuf, inlen, outbuf, outlen)) return false;
+ ucharbuf buf(outbuf, outlen);
+ if(!e) e = editinfos.add(new editinfo);
+ if(!unpackblock(e->copy, buf))
+ {
+ delete[] outbuf;
+ return false;
+ }
+ unpackvslots(*e->copy, buf);
+ delete[] outbuf;
+ return true;
+}
+
+void freeeditinfo(editinfo *&e)
+{
+ if(!e) return;
+ editinfos.removeobj(e);
+ if(e->copy) freeblock(e->copy);
+ delete e;
+ e = NULL;
+}
+
+bool packundo(undoblock *u, int &inlen, uchar *&outbuf, int &outlen)
+{
+ vector<uchar> buf;
+ buf.reserve(512);
+ *(ushort *)buf.pad(2) = lilswap(ushort(u->numents));
+ if(u->numents)
+ {
+ undoent *ue = u->ents();
+ loopi(u->numents)
+ {
+ *(ushort *)buf.pad(2) = lilswap(ushort(ue[i].i));
+ entity &e = *(entity *)buf.pad(sizeof(entity));
+ e = ue[i].e;
+ lilswap(&e.o.x, 3);
+ lilswap(&e.attr1, 5);
+ }
+ }
+ else
+ {
+ block3 &b = *u->block();
+ if(!packblock(b, buf)) return false;
+ buf.put(u->gridmap(), b.size());
+ packvslots(b, buf);
+ }
+ inlen = buf.length();
+ return compresseditinfo(buf.getbuf(), buf.length(), outbuf, outlen);
+}
+
+bool unpackundo(const uchar *inbuf, int inlen, int outlen)
+{
+ uchar *outbuf = NULL;
+ if(!uncompresseditinfo(inbuf, inlen, outbuf, outlen)) return false;
+ ucharbuf buf(outbuf, outlen);
+ if(buf.remaining() < 2)
+ {
+ delete[] outbuf;
+ return false;
+ }
+ int numents = lilswap(*(const ushort *)buf.pad(2));
+ if(numents)
+ {
+ if(buf.remaining() < numents*int(2 + sizeof(entity)))
+ {
+ delete[] outbuf;
+ return false;
+ }
+ loopi(numents)
+ {
+ int idx = lilswap(*(const ushort *)buf.pad(2));
+ entity &e = *(entity *)buf.pad(sizeof(entity));
+ lilswap(&e.o.x, 3);
+ lilswap(&e.attr1, 5);
+ pasteundoent(idx, e);
+ }
+ }
+ else
+ {
+ block3 *b = NULL;
+ if(!unpackblock(b, buf) || b->grid >= worldsize || buf.remaining() < b->size())
+ {
+ freeblock(b);
+ delete[] outbuf;
+ return false;
+ }
+ uchar *g = buf.pad(b->size());
+ unpackvslots(*b, buf);
+ pasteundoblock(b, g);
+ changed(*b, false);
+ freeblock(b);
+ }
+ delete[] outbuf;
+ commitchanges();
+ return true;
+}
+
+bool packundo(int op, int &inlen, uchar *&outbuf, int &outlen)
+{
+ switch(op)
+ {
+ case EDIT_UNDO: return !undos.empty() && packundo(undos.last, inlen, outbuf, outlen);
+ case EDIT_REDO: return !redos.empty() && packundo(redos.last, inlen, outbuf, outlen);
+ default: return false;
+ }
+}
+
+struct prefabheader
+{
+ char magic[4];
+ int version;
+};
+
+struct prefab : editinfo
+{
+ char *name;
+ GLuint ebo, vbo;
+ int numtris, numverts;
+
+ prefab() : name(NULL), ebo(0), vbo(0), numtris(0), numverts(0) {}
+ ~prefab() { DELETEA(name); if(copy) freeblock(copy); }
+
+ void cleanup()
+ {
+ if(ebo) { glDeleteBuffers_(1, &ebo); ebo = 0; }
+ if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+ numtris = numverts = 0;
+ }
+};
+
+static hashnameset<prefab> prefabs;
+
+void cleanupprefabs()
+{
+ enumerate(prefabs, prefab, p, p.cleanup());
+}
+
+void delprefab(char *name)
+{
+ prefab *p = prefabs.access(name);
+ if(p)
+ {
+ p->cleanup();
+ prefabs.remove(name);
+ conoutf("deleted prefab %s", name);
+ }
+}
+COMMAND(delprefab, "s");
+
+void saveprefab(char *name)
+{
+ if(!name[0] || noedit(true) || (nompedit && multiplayer())) return;
+ prefab *b = prefabs.access(name);
+ if(!b)
+ {
+ b = &prefabs[name];
+ b->name = newstring(name);
+ }
+ if(b->copy) freeblock(b->copy);
+ protectsel(b->copy = blockcopy(block3(sel), sel.grid));
+ changed(sel);
+ defformatstring(filename, strpbrk(name, "/\\") ? "packages/%s.obr" : "packages/prefab/%s.obr", name);
+ path(filename);
+ stream *f = opengzfile(filename, "wb");
+ if(!f) { conoutf(CON_ERROR, "could not write prefab to %s", filename); return; }
+ prefabheader hdr;
+ memcpy(hdr.magic, "OEBR", 4);
+ hdr.version = 0;
+ lilswap(&hdr.version, 1);
+ f->write(&hdr, sizeof(hdr));
+ streambuf<uchar> s(f);
+ if(!packblock(*b->copy, s)) { delete f; conoutf(CON_ERROR, "could not pack prefab %s", filename); return; }
+ delete f;
+ conoutf("wrote prefab file %s", filename);
+}
+COMMAND(saveprefab, "s");
+
+void pasteblock(block3 &b, selinfo &sel, bool local)
+{
+ sel.s = b.s;
+ int o = sel.orient;
+ sel.orient = b.orient;
+ cube *s = b.c();
+ loopselxyz(if(!isempty(*s) || s->children || s->material != MAT_AIR) pastecube(*s, c); s++); // 'transparent'. old opaque by 'delcube; paste'
+ sel.orient = o;
+}
+
+bool prefabloaded(const char *name)
+{
+ return prefabs.access(name) != NULL;
+}
+
+prefab *loadprefab(const char *name, bool msg = true)
+{
+ prefab *b = prefabs.access(name);
+ if(b) return b;
+
+ defformatstring(filename, strpbrk(name, "/\\") ? "packages/%s.obr" : "packages/prefab/%s.obr", name);
+ path(filename);
+ stream *f = opengzfile(filename, "rb");
+ if(!f) { if(msg) conoutf(CON_ERROR, "could not read prefab %s", filename); return NULL; }
+ prefabheader hdr;
+ if(f->read(&hdr, sizeof(hdr)) != sizeof(prefabheader) || memcmp(hdr.magic, "OEBR", 4)) { delete f; if(msg) conoutf(CON_ERROR, "prefab %s has malformatted header", filename); return NULL; }
+ lilswap(&hdr.version, 1);
+ if(hdr.version != 0) { delete f; if(msg) conoutf(CON_ERROR, "prefab %s uses unsupported version", filename); return NULL; }
+ streambuf<uchar> s(f);
+ block3 *copy = NULL;
+ if(!unpackblock(copy, s)) { delete f; if(msg) conoutf(CON_ERROR, "could not unpack prefab %s", filename); return NULL; }
+ delete f;
+
+ b = &prefabs[name];
+ b->name = newstring(name);
+ b->copy = copy;
+
+ return b;
+}
+
+void pasteprefab(char *name)
+{
+ if(!name[0] || noedit() || (nompedit && multiplayer())) return;
+ prefab *b = loadprefab(name, true);
+ if(b) pasteblock(*b->copy, sel, true);
+}
+COMMAND(pasteprefab, "s");
+
+struct prefabmesh
+{
+ struct vertex { vec pos; bvec4 norm; };
+
+ static const int SIZE = 1<<9;
+ int table[SIZE];
+ vector<vertex> verts;
+ vector<int> chain;
+ vector<ushort> tris;
+
+ prefabmesh() { memset(table, -1, sizeof(table)); }
+
+ int addvert(const vertex &v)
+ {
+ uint h = hthash(v.pos)&(SIZE-1);
+ for(int i = table[h]; i>=0; i = chain[i])
+ {
+ const vertex &c = verts[i];
+ if(c.pos==v.pos && c.norm==v.norm) return i;
+ }
+ if(verts.length() >= USHRT_MAX) return -1;
+ verts.add(v);
+ chain.add(table[h]);
+ return table[h] = verts.length()-1;
+ }
+
+ int addvert(const vec &pos, const bvec &norm)
+ {
+ vertex vtx;
+ vtx.pos = pos;
+ vtx.norm = norm;
+ return addvert(vtx);
+ }
+
+ void setup(prefab &p)
+ {
+ if(tris.empty()) return;
+
+ p.cleanup();
+
+ loopv(verts) verts[i].norm.flip();
+ if(!p.vbo) glGenBuffers_(1, &p.vbo);
+ gle::bindvbo(p.vbo);
+ glBufferData_(GL_ARRAY_BUFFER, verts.length()*sizeof(vertex), verts.getbuf(), GL_STATIC_DRAW);
+ gle::clearvbo();
+ p.numverts = verts.length();
+
+ if(!p.ebo) glGenBuffers_(1, &p.ebo);
+ gle::bindebo(p.ebo);
+ glBufferData_(GL_ELEMENT_ARRAY_BUFFER, tris.length()*sizeof(ushort), tris.getbuf(), GL_STATIC_DRAW);
+ gle::clearebo();
+ p.numtris = tris.length()/3;
+ }
+
+};
+
+static void genprefabmesh(prefabmesh &r, cube &c, const ivec &co, int size)
+{
+ if(c.children)
+ {
+ neighbourstack[++neighbourdepth] = c.children;
+ loopi(8)
+ {
+ ivec o(i, co, size/2);
+ genprefabmesh(r, c.children[i], o, size/2);
+ }
+ --neighbourdepth;
+ }
+ else if(!isempty(c))
+ {
+ int vis;
+ loopi(6) if((vis = visibletris(c, i, co, size)))
+ {
+ ivec v[4];
+ genfaceverts(c, i, v);
+ int convex = 0;
+ if(!flataxisface(c, i)) convex = faceconvexity(v);
+ int order = vis&4 || convex < 0 ? 1 : 0, numverts = 0;
+ vec vo(co), pos[4], norm[4];
+ pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+ if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+ pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+ if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+ guessnormals(pos, numverts, norm);
+ int index[4];
+ loopj(numverts) index[j] = r.addvert(pos[j], bvec(norm[j]));
+ loopj(numverts-2) if(index[0]!=index[j+1] && index[j+1]!=index[j+2] && index[j+2]!=index[0])
+ {
+ r.tris.add(index[0]);
+ r.tris.add(index[j+1]);
+ r.tris.add(index[j+2]);
+ }
+ }
+ }
+}
+
+void genprefabmesh(prefab &p)
+{
+ block3 b = *p.copy;
+ b.o = ivec(0, 0, 0);
+
+ cube *oldworldroot = worldroot;
+ int oldworldscale = worldscale, oldworldsize = worldsize;
+
+ worldroot = newcubes();
+ worldscale = 1;
+ worldsize = 2;
+ while(worldsize < max(max(b.s.x, b.s.y), b.s.z)*b.grid)
+ {
+ worldscale++;
+ worldsize *= 2;
+ }
+
+ cube *s = p.copy->c();
+ loopxyz(b, b.grid, if(!isempty(*s) || s->children) pastecube(*s, c); s++);
+
+ prefabmesh r;
+ neighbourstack[++neighbourdepth] = worldroot;
+ loopi(8) genprefabmesh(r, worldroot[i], ivec(i, ivec(0, 0, 0), worldsize/2), worldsize/2);
+ --neighbourdepth;
+ r.setup(p);
+
+ freeocta(worldroot);
+
+ worldroot = oldworldroot;
+ worldscale = oldworldscale;
+ worldsize = oldworldsize;
+
+ useshaderbyname("prefab");
+}
+
+extern int outlinecolour;
+
+static void renderprefab(prefab &p, const vec &o, float yaw, float pitch, float roll, float size, const vec &color)
+{
+ if(!p.numtris)
+ {
+ genprefabmesh(p);
+ if(!p.numtris) return;
+ }
+
+ block3 &b = *p.copy;
+
+ matrix4 m;
+ m.identity();
+ m.settranslation(o);
+ if(yaw) m.rotate_around_z(yaw*RAD);
+ if(pitch) m.rotate_around_x(pitch*RAD);
+ if(roll) m.rotate_around_y(-roll*RAD);
+ matrix3 w(m);
+ if(size > 0 && size != 1) m.scale(size);
+ m.translate(vec(b.s).mul(-b.grid*0.5f));
+
+ gle::bindvbo(p.vbo);
+ gle::bindebo(p.ebo);
+ gle::enablevertex();
+ gle::enablenormal();
+ prefabmesh::vertex *v = (prefabmesh::vertex *)0;
+ gle::vertexpointer(sizeof(prefabmesh::vertex), v->pos.v);
+ gle::normalpointer(sizeof(prefabmesh::vertex), v->norm.v, GL_BYTE);
+
+ matrix4 pm;
+ pm.mul(camprojmatrix, m);
+ GLOBALPARAM(prefabmatrix, pm);
+ GLOBALPARAM(prefabworld, w);
+ SETSHADER(prefab);
+ gle::color(color);
+ glDrawRangeElements_(GL_TRIANGLES, 0, p.numverts-1, p.numtris*3, GL_UNSIGNED_SHORT, (ushort *)0);
+
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+ enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+ pm.mul(camprojmatrix, m);
+ GLOBALPARAM(prefabmatrix, pm);
+ SETSHADER(prefab);
+ gle::color(vec::hexcolor(outlinecolour));
+ glDrawRangeElements_(GL_TRIANGLES, 0, p.numverts-1, p.numtris*3, GL_UNSIGNED_SHORT, (ushort *)0);
+
+ disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+ gle::disablevertex();
+ gle::disablenormal();
+ gle::clearebo();
+ gle::clearvbo();
+}
+
+void renderprefab(const char *name, const vec &o, float yaw, float pitch, float roll, float size, const vec &color)
+{
+ prefab *p = loadprefab(name, false);
+ if(p) renderprefab(*p, o, yaw, pitch, roll, size, color);
+}
+
+void previewprefab(const char *name, const vec &color)
+{
+ prefab *p = loadprefab(name, false);
+ if(p)
+ {
+ block3 &b = *p->copy;
+ float yaw;
+ vec o = calcmodelpreviewpos(vec(b.s).mul(b.grid*0.5f), yaw);
+ renderprefab(*p, o, yaw, 0, 0, 1, color);
+ }
+}
+
+void mpcopy(editinfo *&e, selinfo &sel, bool local)
+{
+ if(local) game::edittrigger(sel, EDIT_COPY);
+ if(e==NULL) e = editinfos.add(new editinfo);
+ if(e->copy) freeblock(e->copy);
+ e->copy = NULL;
+ protectsel(e->copy = blockcopy(block3(sel), sel.grid));
+ changed(sel);
+}
+
+void mppaste(editinfo *&e, selinfo &sel, bool local)
+{
+ if(e==NULL) return;
+ if(local) game::edittrigger(sel, EDIT_PASTE);
+ if(e->copy) pasteblock(*e->copy, sel, local);
+}
+
+void copy()
+{
+ if(noedit(true)) return;
+ mpcopy(localedit, sel, true);
+}
+
+void pastehilite()
+{
+ if(!localedit) return;
+ sel.s = localedit->copy->s;
+ reorient();
+ havesel = true;
+}
+
+void paste()
+{
+ if(noedit(true)) return;
+ mppaste(localedit, sel, true);
+}
+
+COMMAND(copy, "");
+COMMAND(pastehilite, "");
+COMMAND(paste, "");
+COMMANDN(undo, editundo, "");
+COMMANDN(redo, editredo, "");
+
+static vector<int *> editingvslots;
+struct vslotref
+{
+ vslotref(int &index) { editingvslots.add(&index); }
+ ~vslotref() { editingvslots.pop(); }
+};
+#define editingvslot(...) vslotref vslotrefs[] = { __VA_ARGS__ }; (void)vslotrefs;
+
+void compacteditvslots()
+{
+ loopv(editingvslots) if(*editingvslots[i]) compactvslot(*editingvslots[i]);
+ loopv(unpackingvslots) compactvslot(*unpackingvslots[i].vslot);
+ loopv(editinfos)
+ {
+ editinfo *e = editinfos[i];
+ compactvslots(e->copy->c(), e->copy->size());
+ }
+ for(undoblock *u = undos.first; u; u = u->next)
+ if(!u->numents)
+ compactvslots(u->block()->c(), u->block()->size());
+ for(undoblock *u = redos.first; u; u = u->next)
+ if(!u->numents)
+ compactvslots(u->block()->c(), u->block()->size());
+}
+
+///////////// height maps ////////////////
+
+#define MAXBRUSH 64
+#define MAXBRUSHC 63
+#define MAXBRUSH2 32
+int brush[MAXBRUSH][MAXBRUSH];
+VAR(brushx, 0, MAXBRUSH2, MAXBRUSH);
+VAR(brushy, 0, MAXBRUSH2, MAXBRUSH);
+bool paintbrush = 0;
+int brushmaxx = 0, brushminx = MAXBRUSH;
+int brushmaxy = 0, brushminy = MAXBRUSH;
+
+void clearbrush()
+{
+ memset(brush, 0, sizeof brush);
+ brushmaxx = brushmaxy = 0;
+ brushminx = brushminy = MAXBRUSH;
+ paintbrush = false;
+}
+
+void brushvert(int *x, int *y, int *v)
+{
+ *x += MAXBRUSH2 - brushx + 1; // +1 for automatic padding
+ *y += MAXBRUSH2 - brushy + 1;
+ if(*x<0 || *y<0 || *x>=MAXBRUSH || *y>=MAXBRUSH) return;
+ brush[*x][*y] = clamp(*v, 0, 8);
+ paintbrush = paintbrush || (brush[*x][*y] > 0);
+ brushmaxx = min(MAXBRUSH-1, max(brushmaxx, *x+1));
+ brushmaxy = min(MAXBRUSH-1, max(brushmaxy, *y+1));
+ brushminx = max(0, min(brushminx, *x-1));
+ brushminy = max(0, min(brushminy, *y-1));
+}
+
+vector<int> htextures;
+
+COMMAND(clearbrush, "");
+COMMAND(brushvert, "iii");
+void hmapcancel() { htextures.setsize(0); }
+COMMAND(hmapcancel, "");
+ICOMMAND(hmapselect, "", (),
+ int t = lookupcube(cur).texture[orient];
+ int i = htextures.find(t);
+ if(i<0)
+ htextures.add(t);
+ else
+ htextures.remove(i);
+);
+
+inline bool isheightmap(int o, int d, bool empty, cube *c)
+{
+ return havesel ||
+ (empty && isempty(*c)) ||
+ htextures.empty() ||
+ htextures.find(c->texture[o]) >= 0;
+}
+
+namespace hmap
+{
+# define PAINTED 1
+# define NOTHMAP 2
+# define MAPPED 16
+ uchar flags[MAXBRUSH][MAXBRUSH];
+ cube *cmap[MAXBRUSHC][MAXBRUSHC][4];
+ int mapz[MAXBRUSHC][MAXBRUSHC];
+ int map [MAXBRUSH][MAXBRUSH];
+
+ selinfo changes;
+ bool selecting;
+ int d, dc, dr, dcr, biasup, br, hws, fg;
+ int gx, gy, gz, mx, my, mz, nx, ny, nz, bmx, bmy, bnx, bny;
+ uint fs;
+ selinfo hundo;
+
+ cube *getcube(ivec t, int f)
+ {
+ t[d] += dcr*f*gridsize;
+ if(t[d] > nz || t[d] < mz) return NULL;
+ cube *c = &lookupcube(t, gridsize);
+ if(c->children) forcemip(*c, false);
+ discardchildren(*c, true);
+ if(!isheightmap(sel.orient, d, true, c)) return NULL;
+ if (t.x < changes.o.x) changes.o.x = t.x;
+ else if(t.x > changes.s.x) changes.s.x = t.x;
+ if (t.y < changes.o.y) changes.o.y = t.y;
+ else if(t.y > changes.s.y) changes.s.y = t.y;
+ if (t.z < changes.o.z) changes.o.z = t.z;
+ else if(t.z > changes.s.z) changes.s.z = t.z;
+ return c;
+ }
+
+ uint getface(cube *c, int d)
+ {
+ return 0x0f0f0f0f & ((dc ? c->faces[d] : 0x88888888 - c->faces[d]) >> fs);
+ }
+
+ void pushside(cube &c, int d, int x, int y, int z)
+ {
+ ivec a;
+ getcubevector(c, d, x, y, z, a);
+ a[R[d]] = 8 - a[R[d]];
+ setcubevector(c, d, x, y, z, a);
+ }
+
+ void addpoint(int x, int y, int z, int v)
+ {
+ if(!(flags[x][y] & MAPPED))
+ map[x][y] = v + (z*8);
+ flags[x][y] |= MAPPED;
+ }
+
+ void select(int x, int y, int z)
+ {
+ if((NOTHMAP & flags[x][y]) || (PAINTED & flags[x][y])) return;
+ ivec t(d, x+gx, y+gy, dc ? z : hws-z);
+ t.shl(gridpower);
+
+ // selections may damage; must makeundo before
+ hundo.o = t;
+ hundo.o[D[d]] -= dcr*gridsize*2;
+ makeundo(hundo);
+
+ cube **c = cmap[x][y];
+ loopk(4) c[k] = NULL;
+ c[1] = getcube(t, 0);
+ if(!c[1] || !isempty(*c[1]))
+ { // try up
+ c[2] = c[1];
+ c[1] = getcube(t, 1);
+ if(!c[1] || isempty(*c[1])) { c[0] = c[1]; c[1] = c[2]; c[2] = NULL; }
+ else { z++; t[d]+=fg; }
+ }
+ else // drop down
+ {
+ z--;
+ t[d]-= fg;
+ c[0] = c[1];
+ c[1] = getcube(t, 0);
+ }
+
+ if(!c[1] || isempty(*c[1])) { flags[x][y] |= NOTHMAP; return; }
+
+ flags[x][y] |= PAINTED;
+ mapz [x][y] = z;
+
+ if(!c[0]) c[0] = getcube(t, 1);
+ if(!c[2]) c[2] = getcube(t, -1);
+ c[3] = getcube(t, -2);
+ c[2] = !c[2] || isempty(*c[2]) ? NULL : c[2];
+ c[3] = !c[3] || isempty(*c[3]) ? NULL : c[3];
+
+ uint face = getface(c[1], d);
+ if(face == 0x08080808 && (!c[0] || !isempty(*c[0]))) { flags[x][y] |= NOTHMAP; return; }
+ if(c[1]->faces[R[d]] == F_SOLID) // was single
+ face += 0x08080808;
+ else // was pair
+ face += c[2] ? getface(c[2], d) : 0x08080808;
+ face += 0x08080808; // c[3]
+ uchar *f = (uchar*)&face;
+ addpoint(x, y, z, f[0]);
+ addpoint(x+1, y, z, f[1]);
+ addpoint(x, y+1, z, f[2]);
+ addpoint(x+1, y+1, z, f[3]);
+
+ if(selecting) // continue to adjacent cubes
+ {
+ if(x>bmx) select(x-1, y, z);
+ if(x<bnx) select(x+1, y, z);
+ if(y>bmy) select(x, y-1, z);
+ if(y<bny) select(x, y+1, z);
+ }
+ }
+
+ void ripple(int x, int y, int z, bool force)
+ {
+ if(force) select(x, y, z);
+ if((NOTHMAP & flags[x][y]) || !(PAINTED & flags[x][y])) return;
+
+ bool changed = false;
+ int *o[4], best, par, q = 0;
+ loopi(2) loopj(2) o[i+j*2] = &map[x+i][y+j];
+ #define pullhmap(I, LT, GT, M, N, A) do { \
+ best = I; \
+ loopi(4) if(*o[i] LT best) best = *o[q = i] - M; \
+ par = (best&(~7)) + N; \
+ /* dual layer for extra smoothness */ \
+ if(*o[q^3] GT par && !(*o[q^1] LT par || *o[q^2] LT par)) { \
+ if(*o[q^3] GT par A 8 || *o[q^1] != par || *o[q^2] != par) { \
+ *o[q^3] = (*o[q^3] GT par A 8 ? par A 8 : *o[q^3]); \
+ *o[q^1] = *o[q^2] = par; \
+ changed = true; \
+ } \
+ /* single layer */ \
+ } else { \
+ loopj(4) if(*o[j] GT par) { \
+ *o[j] = par; \
+ changed = true; \
+ } \
+ } \
+ } while(0)
+
+ if(biasup)
+ pullhmap(0, >, <, 1, 0, -);
+ else
+ pullhmap(worldsize*8, <, >, 0, 8, +);
+
+ cube **c = cmap[x][y];
+ int e[2][2];
+ int notempty = 0;
+
+ loopk(4) if(c[k]) {
+ loopi(2) loopj(2) {
+ e[i][j] = min(8, map[x+i][y+j] - (mapz[x][y]+3-k)*8);
+ notempty |= e[i][j] > 0;
+ }
+ if(notempty)
+ {
+ c[k]->texture[sel.orient] = c[1]->texture[sel.orient];
+ solidfaces(*c[k]);
+ loopi(2) loopj(2)
+ {
+ int f = e[i][j];
+ if(f<0 || (f==0 && e[1-i][j]==0 && e[i][1-j]==0))
+ {
+ f=0;
+ pushside(*c[k], d, i, j, 0);
+ pushside(*c[k], d, i, j, 1);
+ }
+ edgeset(cubeedge(*c[k], d, i, j), dc, dc ? f : 8-f);
+ }
+ }
+ else
+ emptyfaces(*c[k]);
+ }
+
+ if(!changed) return;
+ if(x>mx) ripple(x-1, y, mapz[x][y], true);
+ if(x<nx) ripple(x+1, y, mapz[x][y], true);
+ if(y>my) ripple(x, y-1, mapz[x][y], true);
+ if(y<ny) ripple(x, y+1, mapz[x][y], true);
+
+#define DIAGONAL_RIPPLE(a,b,exp) if(exp) { \
+ if(flags[x a][ y] & PAINTED) \
+ ripple(x a, y b, mapz[x a][y], true); \
+ else if(flags[x][y b] & PAINTED) \
+ ripple(x a, y b, mapz[x][y b], true); \
+ }
+
+ DIAGONAL_RIPPLE(-1, -1, (x>mx && y>my)); // do diagonals because adjacents
+ DIAGONAL_RIPPLE(-1, +1, (x>mx && y<ny)); // won't unless changed
+ DIAGONAL_RIPPLE(+1, +1, (x<nx && y<ny));
+ DIAGONAL_RIPPLE(+1, -1, (x<nx && y>my));
+ }
+
+#define loopbrush(i) for(int x=bmx; x<=bnx+i; x++) for(int y=bmy; y<=bny+i; y++)
+
+ void paint()
+ {
+ loopbrush(1)
+ map[x][y] -= dr * brush[x][y];
+ }
+
+ void smooth()
+ {
+ int sum, div;
+ loopbrush(-2)
+ {
+ sum = 0;
+ div = 9;
+ loopi(3) loopj(3)
+ if(flags[x+i][y+j] & MAPPED)
+ sum += map[x+i][y+j];
+ else div--;
+ if(div)
+ map[x+1][y+1] = sum / div;
+ }
+ }
+
+ void rippleandset()
+ {
+ loopbrush(0)
+ ripple(x, y, gz, false);
+ }
+
+ void run(int dir, int mode)
+ {
+ d = dimension(sel.orient);
+ dc = dimcoord(sel.orient);
+ dcr= dc ? 1 : -1;
+ dr = dir>0 ? 1 : -1;
+ br = dir>0 ? 0x08080808 : 0;
+ // biasup = mode == dir<0;
+ biasup = dir<0;
+ bool paintme = paintbrush;
+ int cx = (sel.corner&1 ? 0 : -1);
+ int cy = (sel.corner&2 ? 0 : -1);
+ hws= (worldsize>>gridpower);
+ gx = (cur[R[d]] >> gridpower) + cx - MAXBRUSH2;
+ gy = (cur[C[d]] >> gridpower) + cy - MAXBRUSH2;
+ gz = (cur[D[d]] >> gridpower);
+ fs = dc ? 4 : 0;
+ fg = dc ? gridsize : -gridsize;
+ mx = max(0, -gx); // ripple range
+ my = max(0, -gy);
+ nx = min(MAXBRUSH-1, hws-gx) - 1;
+ ny = min(MAXBRUSH-1, hws-gy) - 1;
+ if(havesel)
+ { // selection range
+ bmx = mx = max(mx, (sel.o[R[d]]>>gridpower)-gx);
+ bmy = my = max(my, (sel.o[C[d]]>>gridpower)-gy);
+ bnx = nx = min(nx, (sel.s[R[d]]+(sel.o[R[d]]>>gridpower))-gx-1);
+ bny = ny = min(ny, (sel.s[C[d]]+(sel.o[C[d]]>>gridpower))-gy-1);
+ }
+ if(havesel && mode<0) // -ve means smooth selection
+ paintme = false;
+ else
+ { // brush range
+ bmx = max(mx, brushminx);
+ bmy = max(my, brushminy);
+ bnx = min(nx, brushmaxx-1);
+ bny = min(ny, brushmaxy-1);
+ }
+ nz = worldsize-gridsize;
+ mz = 0;
+ hundo.s = ivec(d,1,1,5);
+ hundo.orient = sel.orient;
+ hundo.grid = gridsize;
+ forcenextundo();
+
+ changes.grid = gridsize;
+ changes.s = changes.o = cur;
+ memset(map, 0, sizeof map);
+ memset(flags, 0, sizeof flags);
+
+ selecting = true;
+ select(clamp(MAXBRUSH2-cx, bmx, bnx),
+ clamp(MAXBRUSH2-cy, bmy, bny),
+ dc ? gz : hws - gz);
+ selecting = false;
+ if(paintme)
+ paint();
+ else
+ smooth();
+ rippleandset(); // pull up points to cubify, and set
+ changes.s.sub(changes.o).shr(gridpower).add(1);
+ changed(changes);
+ }
+}
+
+void edithmap(int dir, int mode) {
+ if((nompedit && multiplayer()) || !hmapsel) return;
+ hmap::run(dir, mode);
+}
+
+///////////// main cube edit ////////////////
+
+int bounded(int n) { return n<0 ? 0 : (n>8 ? 8 : n); }
+
+void pushedge(uchar &edge, int dir, int dc)
+{
+ int ne = bounded(edgeget(edge, dc)+dir);
+ edgeset(edge, dc, ne);
+ int oe = edgeget(edge, 1-dc);
+ if((dir<0 && dc && oe>ne) || (dir>0 && dc==0 && oe<ne)) edgeset(edge, 1-dc, ne);
+}
+
+void linkedpush(cube &c, int d, int x, int y, int dc, int dir)
+{
+ ivec v, p;
+ getcubevector(c, d, x, y, dc, v);
+
+ loopi(2) loopj(2)
+ {
+ getcubevector(c, d, i, j, dc, p);
+ if(v==p)
+ pushedge(cubeedge(c, d, i, j), dir, dc);
+ }
+}
+
+static ushort getmaterial(cube &c)
+{
+ if(c.children)
+ {
+ ushort mat = getmaterial(c.children[7]);
+ loopi(7) if(mat != getmaterial(c.children[i])) return MAT_AIR;
+ return mat;
+ }
+ return c.material;
+}
+
+VAR(invalidcubeguard, 0, 1, 1);
+
+void mpeditface(int dir, int mode, selinfo &sel, bool local)
+{
+ if(mode==1 && (sel.cx || sel.cy || sel.cxs&1 || sel.cys&1)) mode = 0;
+ int d = dimension(sel.orient);
+ int dc = dimcoord(sel.orient);
+ int seldir = dc ? -dir : dir;
+
+ if(local)
+ game::edittrigger(sel, EDIT_FACE, dir, mode);
+
+ if(mode==1)
+ {
+ int h = sel.o[d]+dc*sel.grid;
+ if(((dir>0) == dc && h<=0) || ((dir<0) == dc && h>=worldsize)) return;
+ if(dir<0) sel.o[d] += sel.grid * seldir;
+ }
+
+ if(dc) sel.o[d] += sel.us(d)-sel.grid;
+ sel.s[d] = 1;
+
+ loopselxyz(
+ if(c.children) solidfaces(c);
+ ushort mat = getmaterial(c);
+ discardchildren(c, true);
+ c.material = mat;
+ if(mode==1) // fill command
+ {
+ if(dir<0)
+ {
+ solidfaces(c);
+ cube &o = blockcube(x, y, 1, sel, -sel.grid);
+ loopi(6)
+ c.texture[i] = o.children ? DEFAULT_GEOM : o.texture[i];
+ }
+ else
+ emptyfaces(c);
+ }
+ else
+ {
+ uint bak = c.faces[d];
+ uchar *p = (uchar *)&c.faces[d];
+
+ if(mode==2)
+ linkedpush(c, d, sel.corner&1, sel.corner>>1, dc, seldir); // corner command
+ else
+ {
+ loop(mx,2) loop(my,2) // pull/push edges command
+ {
+ if(x==0 && mx==0 && sel.cx) continue;
+ if(y==0 && my==0 && sel.cy) continue;
+ if(x==sel.s[R[d]]-1 && mx==1 && (sel.cx+sel.cxs)&1) continue;
+ if(y==sel.s[C[d]]-1 && my==1 && (sel.cy+sel.cys)&1) continue;
+ if(p[mx+my*2] != ((uchar *)&bak)[mx+my*2]) continue;
+
+ linkedpush(c, d, mx, my, dc, seldir);
+ }
+ }
+
+ optiface(p, c);
+ if(invalidcubeguard==1 && !isvalidcube(c))
+ {
+ uint newbak = c.faces[d];
+ uchar *m = (uchar *)&bak;
+ uchar *n = (uchar *)&newbak;
+ loopk(4) if(n[k] != m[k]) // tries to find partial edit that is valid
+ {
+ c.faces[d] = bak;
+ c.edges[d*4+k] = n[k];
+ if(isvalidcube(c))
+ m[k] = n[k];
+ }
+ c.faces[d] = bak;
+ }
+ }
+ );
+ if (mode==1 && dir>0)
+ sel.o[d] += sel.grid * seldir;
+}
+
+void editface(int *dir, int *mode)
+{
+ if(noedit(moving!=0)) return;
+ if(hmapedit!=1)
+ mpeditface(*dir, *mode, sel, true);
+ else
+ edithmap(*dir, *mode);
+}
+
+VAR(selectionsurf, 0, 0, 1);
+
+void pushsel(int *dir)
+{
+ if(noedit(moving!=0)) return;
+ int d = dimension(orient);
+ int s = dimcoord(orient) ? -*dir : *dir;
+ sel.o[d] += s*sel.grid;
+ if(selectionsurf==1)
+ {
+ player->o[d] += s*sel.grid;
+ player->resetinterp();
+ }
+}
+
+void mpdelcube(selinfo &sel, bool local)
+{
+ if(local) game::edittrigger(sel, EDIT_DELCUBE);
+ loopselxyz(discardchildren(c, true); emptyfaces(c));
+}
+
+void delcube()
+{
+ if(noedit(true)) return;
+ mpdelcube(sel, true);
+}
+
+COMMAND(pushsel, "i");
+COMMAND(editface, "ii");
+COMMAND(delcube, "");
+
+/////////// texture editing //////////////////
+
+int curtexindex = -1, lasttex = 0, lasttexmillis = -1;
+int texpaneltimer = 0;
+vector<ushort> texmru;
+
+void tofronttex() // maintain most recently used of the texture lists when applying texture
+{
+ int c = curtexindex;
+ if(texmru.inrange(c))
+ {
+ texmru.insert(0, texmru.remove(c));
+ curtexindex = -1;
+ }
+}
+
+selinfo repsel;
+int reptex = -1;
+
+static vector<vslotmap> remappedvslots;
+
+VAR(usevdelta, 1, 0, 0);
+
+static VSlot *remapvslot(int index, bool delta, const VSlot &ds)
+{
+ loopv(remappedvslots) if(remappedvslots[i].index == index) return remappedvslots[i].vslot;
+ VSlot &vs = lookupvslot(index, false);
+ if(vs.index < 0 || vs.index == DEFAULT_SKY) return NULL;
+ VSlot *edit = NULL;
+ if(delta)
+ {
+ VSlot ms;
+ mergevslot(ms, vs, ds);
+ edit = ms.changed ? editvslot(vs, ms) : vs.slot->variants;
+ }
+ else edit = ds.changed ? editvslot(vs, ds) : vs.slot->variants;
+ if(!edit) edit = &vs;
+ remappedvslots.add(vslotmap(vs.index, edit));
+ return edit;
+}
+
+static void remapvslots(cube &c, bool delta, const VSlot &ds, int orient, bool &findrep, VSlot *&findedit)
+{
+ if(c.children)
+ {
+ loopi(8) remapvslots(c.children[i], delta, ds, orient, findrep, findedit);
+ return;
+ }
+ static VSlot ms;
+ if(orient<0) loopi(6)
+ {
+ VSlot *edit = remapvslot(c.texture[i], delta, ds);
+ if(edit)
+ {
+ c.texture[i] = edit->index;
+ if(!findedit) findedit = edit;
+ }
+ }
+ else
+ {
+ int i = visibleorient(c, orient);
+ VSlot *edit = remapvslot(c.texture[i], delta, ds);
+ if(edit)
+ {
+ if(findrep)
+ {
+ if(reptex < 0) reptex = c.texture[i];
+ else if(reptex != c.texture[i]) findrep = false;
+ }
+ c.texture[i] = edit->index;
+ if(!findedit) findedit = edit;
+ }
+ }
+}
+
+void edittexcube(cube &c, int tex, int orient, bool &findrep)
+{
+ if(orient<0) loopi(6) c.texture[i] = tex;
+ else
+ {
+ int i = visibleorient(c, orient);
+ if(findrep)
+ {
+ if(reptex < 0) reptex = c.texture[i];
+ else if(reptex != c.texture[i]) findrep = false;
+ }
+ c.texture[i] = tex;
+ }
+ if(c.children) loopi(8) edittexcube(c.children[i], tex, orient, findrep);
+}
+
+VAR(allfaces, 0, 0, 1);
+
+void mpeditvslot(int delta, VSlot &ds, int allfaces, selinfo &sel, bool local)
+{
+ if(local)
+ {
+ game::edittrigger(sel, EDIT_VSLOT, delta, allfaces, 0, &ds);
+ if(!(lastsel==sel)) tofronttex();
+ if(allfaces || !(repsel == sel)) reptex = -1;
+ repsel = sel;
+ }
+ bool findrep = local && !allfaces && reptex < 0;
+ VSlot *findedit = NULL;
+ loopselxyz(remapvslots(c, delta != 0, ds, allfaces ? -1 : sel.orient, findrep, findedit));
+ remappedvslots.setsize(0);
+ if(local && findedit)
+ {
+ lasttex = findedit->index;
+ lasttexmillis = totalmillis;
+ curtexindex = texmru.find(lasttex);
+ if(curtexindex < 0)
+ {
+ curtexindex = texmru.length();
+ texmru.add(lasttex);
+ }
+ }
+}
+
+bool mpeditvslot(int delta, int allfaces, selinfo &sel, ucharbuf &buf)
+{
+ VSlot ds;
+ if(!unpackvslot(buf, ds, delta != 0)) return false;
+ editingvslot(ds.layer);
+ mpeditvslot(delta, ds, allfaces, sel, false);
+ return true;
+}
+
+void vdelta(char *body)
+{
+ if(noedit()) return;
+ usevdelta++;
+ execute(body);
+ usevdelta--;
+}
+COMMAND(vdelta, "s");
+
+void vrotate(int *n)
+{
+ if(noedit()) return;
+ VSlot ds;
+ ds.changed = 1<<VSLOT_ROTATION;
+ ds.rotation = usevdelta ? *n : clamp(*n, 0, 7);
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vrotate, "i");
+ICOMMAND(getvrotate, "i", (int *tex), intret(lookupvslot(*tex, false).rotation));
+
+void voffset(int *x, int *y)
+{
+ if(noedit()) return;
+ VSlot ds;
+ ds.changed = 1<<VSLOT_OFFSET;
+ ds.offset = usevdelta ? ivec2(*x, *y) : ivec2(*x, *y).max(0);
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(voffset, "ii");
+ICOMMAND(getvoffset, "i", (int *tex),
+{
+ VSlot &vslot = lookupvslot(*tex, false);
+ defformatstring(str, "%d %d", vslot.offset.x, vslot.offset.y);
+ result(str);
+});
+
+void vscroll(float *s, float *t)
+{
+ if(noedit()) return;
+ VSlot ds;
+ ds.changed = 1<<VSLOT_SCROLL;
+ ds.scroll = vec2(*s, *t).div(1000);
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vscroll, "ff");
+ICOMMAND(getvscroll, "i", (int *tex),
+{
+ VSlot &vslot = lookupvslot(*tex, false);
+ defformatstring(str, "%s %s", floatstr(vslot.scroll.x), floatstr(vslot.scroll.y));
+ result(str);
+});
+
+void vscale(float *scale)
+{
+ if(noedit()) return;
+ VSlot ds;
+ ds.changed = 1<<VSLOT_SCALE;
+ ds.scale = *scale <= 0 ? 1 : (usevdelta ? *scale : clamp(*scale, 1/8.0f, 8.0f));
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vscale, "f");
+ICOMMAND(getvscale, "i", (int *tex), floatret(lookupvslot(*tex, false).scale));
+
+void vlayer(int *n)
+{
+ if(noedit()) return;
+ VSlot ds;
+ ds.changed = 1<<VSLOT_LAYER;
+ if(vslots.inrange(*n))
+ {
+ ds.layer = *n;
+ if(vslots[ds.layer]->changed && nompedit && multiplayer()) return;
+ }
+ editingvslot(ds.layer);
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vlayer, "i");
+ICOMMAND(getvlayer, "i", (int *tex), intret(lookupvslot(*tex, false).layer));
+
+void valpha(float *front, float *back)
+{
+ if(noedit()) return;
+ VSlot ds;
+ ds.changed = 1<<VSLOT_ALPHA;
+ ds.alphafront = clamp(*front, 0.0f, 1.0f);
+ ds.alphaback = clamp(*back, 0.0f, 1.0f);
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(valpha, "ff");
+ICOMMAND(getvalpha, "i", (int *tex),
+{
+ VSlot &vslot = lookupvslot(*tex, false);
+ defformatstring(str, "%s %s", floatstr(vslot.alphafront), floatstr(vslot.alphaback));
+ result(str);
+});
+
+void vcolor(float *r, float *g, float *b)
+{
+ if(noedit()) return;
+ VSlot ds;
+ ds.changed = 1<<VSLOT_COLOR;
+ ds.colorscale = vec(clamp(*r, 0.0f, 1.0f), clamp(*g, 0.0f, 1.0f), clamp(*b, 0.0f, 1.0f));
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vcolor, "fff");
+ICOMMAND(getvcolor, "i", (int *tex),
+{
+ VSlot &vslot = lookupvslot(*tex, false);
+ defformatstring(str, "%s %s %s", floatstr(vslot.colorscale.r), floatstr(vslot.colorscale.g), floatstr(vslot.colorscale.b));
+ result(str);
+});
+
+void vreset()
+{
+ if(noedit()) return;
+ VSlot ds;
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vreset, "");
+
+void vshaderparam(const char *name, float *x, float *y, float *z, float *w)
+{
+ if(noedit()) return;
+ VSlot ds;
+ ds.changed = 1<<VSLOT_SHPARAM;
+ if(name[0])
+ {
+ SlotShaderParam p = { getshaderparamname(name), -1, {*x, *y, *z, *w} };
+ ds.params.add(p);
+ }
+ mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vshaderparam, "sffff");
+ICOMMAND(getvshaderparam, "is", (int *tex, const char *name),
+{
+ VSlot &vslot = lookupvslot(*tex, false);
+ loopv(vslot.params)
+ {
+ SlotShaderParam &p = vslot.params[i];
+ if(!strcmp(p.name, name))
+ {
+ defformatstring(str, "%s %s %s %s", floatstr(p.val[0]), floatstr(p.val[1]), floatstr(p.val[2]), floatstr(p.val[3]));
+ result(str);
+ return;
+ }
+ }
+});
+ICOMMAND(getvshaderparamnames, "i", (int *tex),
+{
+ VSlot &vslot = lookupvslot(*tex, false);
+ vector<char> str;
+ loopv(vslot.params)
+ {
+ SlotShaderParam &p = vslot.params[i];
+ if(i) str.put(' ');
+ str.put(p.name, strlen(p.name));
+ }
+ str.add('\0');
+ stringret(newstring(str.getbuf(), str.length()-1));
+});
+
+void mpedittex(int tex, int allfaces, selinfo &sel, bool local)
+{
+ if(local)
+ {
+ game::edittrigger(sel, EDIT_TEX, tex, allfaces);
+ if(allfaces || !(repsel == sel)) reptex = -1;
+ repsel = sel;
+ }
+ bool findrep = local && !allfaces && reptex < 0;
+ loopselxyz(edittexcube(c, tex, allfaces ? -1 : sel.orient, findrep));
+}
+
+static int unpacktex(int &tex, ucharbuf &buf, bool insert = true)
+{
+ if(tex < 0x10000) return true;
+ VSlot ds;
+ if(!unpackvslot(buf, ds, false)) return false;
+ VSlot &vs = *lookupslot(tex & 0xFFFF, false).variants;
+ if(vs.index < 0 || vs.index == DEFAULT_SKY) return false;
+ VSlot *edit = insert ? editvslot(vs, ds) : findvslot(*vs.slot, vs, ds);
+ if(!edit) return false;
+ tex = edit->index;
+ return true;
+}
+
+int shouldpacktex(int index)
+{
+ if(vslots.inrange(index))
+ {
+ VSlot &vs = *vslots[index];
+ if(vs.changed) return 0x10000 + vs.slot->index;
+ }
+ return 0;
+}
+
+
+bool mpedittex(int tex, int allfaces, selinfo &sel, ucharbuf &buf)
+{
+ if(!unpacktex(tex, buf)) return false;
+ mpedittex(tex, allfaces, sel, false);
+ return true;
+}
+
+void filltexlist()
+{
+ if(texmru.length()!=vslots.length())
+ {
+ loopvrev(texmru) if(texmru[i]>=vslots.length())
+ {
+ if(curtexindex > i) curtexindex--;
+ else if(curtexindex == i) curtexindex = -1;
+ texmru.remove(i);
+ }
+ loopv(vslots) if(texmru.find(i)<0) texmru.add(i);
+ }
+}
+
+void compactmruvslots()
+{
+ remappedvslots.setsize(0);
+ loopvrev(texmru)
+ {
+ if(vslots.inrange(texmru[i]))
+ {
+ VSlot &vs = *vslots[texmru[i]];
+ if(vs.index >= 0)
+ {
+ texmru[i] = vs.index;
+ continue;
+ }
+ }
+ if(curtexindex > i) curtexindex--;
+ else if(curtexindex == i) curtexindex = -1;
+ texmru.remove(i);
+ }
+ if(vslots.inrange(lasttex))
+ {
+ VSlot &vs = *vslots[lasttex];
+ lasttex = vs.index >= 0 ? vs.index : 0;
+ }
+ else lasttex = 0;
+ reptex = vslots.inrange(reptex) ? vslots[reptex]->index : -1;
+}
+
+void edittex(int i, bool save = true)
+{
+ lasttex = i;
+ lasttexmillis = totalmillis;
+ if(save)
+ {
+ loopvj(texmru) if(texmru[j]==lasttex) { curtexindex = j; break; }
+ }
+ mpedittex(i, allfaces, sel, true);
+}
+
+void edittex_(int *dir)
+{
+ if(noedit()) return;
+ filltexlist();
+ if(texmru.empty()) return;
+ texpaneltimer = 5000;
+ if(!(lastsel==sel)) tofronttex();
+ curtexindex = clamp(curtexindex<0 ? 0 : curtexindex+*dir, 0, texmru.length()-1);
+ edittex(texmru[curtexindex], false);
+}
+
+void gettex()
+{
+ if(noedit(true)) return;
+ filltexlist();
+ int tex = -1;
+ loopxyz(sel, sel.grid, tex = c.texture[sel.orient]);
+ loopv(texmru) if(texmru[i]==tex)
+ {
+ curtexindex = i;
+ tofronttex();
+ return;
+ }
+}
+
+void getcurtex()
+{
+ if(noedit(true)) return;
+ filltexlist();
+ int index = curtexindex < 0 ? 0 : curtexindex;
+ if(!texmru.inrange(index)) return;
+ intret(texmru[index]);
+}
+
+void getseltex()
+{
+ if(noedit(true)) return;
+ cube &c = lookupcube(sel.o, -sel.grid);
+ if(c.children || isempty(c)) return;
+ intret(c.texture[sel.orient]);
+}
+
+void gettexname(int *tex, int *subslot)
+{
+ if(noedit(true) || *tex<0) return;
+ VSlot &vslot = lookupvslot(*tex, false);
+ Slot &slot = *vslot.slot;
+ if(!slot.sts.inrange(*subslot)) return;
+ result(slot.sts[*subslot].name);
+}
+
+void getslottex(int *idx)
+{
+ if(*idx < 0 || !slots.inrange(*idx)) { intret(-1); return; }
+ Slot &slot = lookupslot(*idx, false);
+ intret(slot.variants->index);
+}
+
+COMMANDN(edittex, edittex_, "i");
+COMMAND(gettex, "");
+COMMAND(getcurtex, "");
+COMMAND(getseltex, "");
+ICOMMAND(getreptex, "", (), { if(!noedit()) intret(vslots.inrange(reptex) ? reptex : -1); });
+COMMAND(gettexname, "ii");
+ICOMMAND(numvslots, "", (), intret(vslots.length()));
+ICOMMAND(numslots, "", (), intret(slots.length()));
+COMMAND(getslottex, "i");
+ICOMMAND(texloaded, "i", (int *tex), intret(slots.inrange(*tex) && slots[*tex]->loaded ? 1 : 0));
+
+void replacetexcube(cube &c, int oldtex, int newtex)
+{
+ loopi(6) if(c.texture[i] == oldtex) c.texture[i] = newtex;
+ if(c.children) loopi(8) replacetexcube(c.children[i], oldtex, newtex);
+}
+
+void mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, bool local)
+{
+ if(local) game::edittrigger(sel, EDIT_REPLACE, oldtex, newtex, insel ? 1 : 0);
+ if(insel)
+ {
+ loopselxyz(replacetexcube(c, oldtex, newtex));
+ }
+ else
+ {
+ loopi(8) replacetexcube(worldroot[i], oldtex, newtex);
+ }
+ allchanged();
+}
+
+bool mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, ucharbuf &buf)
+{
+ if(!unpacktex(oldtex, buf, false)) return false;
+ editingvslot(oldtex);
+ if(!unpacktex(newtex, buf)) return false;
+ mpreplacetex(oldtex, newtex, insel, sel, false);
+ return true;
+}
+
+void replace(bool insel)
+{
+ if(noedit()) return;
+ if(reptex < 0) { conoutf(CON_ERROR, "can only replace after a texture edit"); return; }
+ mpreplacetex(reptex, lasttex, insel, sel, true);
+}
+
+ICOMMAND(replace, "", (), replace(false));
+ICOMMAND(replacesel, "", (), replace(true));
+
+////////// flip and rotate ///////////////
+uint dflip(uint face) { return face==F_EMPTY ? face : 0x88888888 - (((face&0xF0F0F0F0)>>4) | ((face&0x0F0F0F0F)<<4)); }
+uint cflip(uint face) { return ((face&0xFF00FF00)>>8) | ((face&0x00FF00FF)<<8); }
+uint rflip(uint face) { return ((face&0xFFFF0000)>>16)| ((face&0x0000FFFF)<<16); }
+uint mflip(uint face) { return (face&0xFF0000FF) | ((face&0x00FF0000)>>8) | ((face&0x0000FF00)<<8); }
+
+void flipcube(cube &c, int d)
+{
+ swap(c.texture[d*2], c.texture[d*2+1]);
+ c.faces[D[d]] = dflip(c.faces[D[d]]);
+ c.faces[C[d]] = cflip(c.faces[C[d]]);
+ c.faces[R[d]] = rflip(c.faces[R[d]]);
+ if(c.children)
+ {
+ loopi(8) if(i&octadim(d)) swap(c.children[i], c.children[i-octadim(d)]);
+ loopi(8) flipcube(c.children[i], d);
+ }
+}
+
+void rotatequad(cube &a, cube &b, cube &c, cube &d)
+{
+ cube t = a; a = b; b = c; c = d; d = t;
+}
+
+void rotatecube(cube &c, int d) // rotates cube clockwise. see pics in cvs for help.
+{
+ c.faces[D[d]] = cflip (mflip(c.faces[D[d]]));
+ c.faces[C[d]] = dflip (mflip(c.faces[C[d]]));
+ c.faces[R[d]] = rflip (mflip(c.faces[R[d]]));
+ swap(c.faces[R[d]], c.faces[C[d]]);
+
+ swap(c.texture[2*R[d]], c.texture[2*C[d]+1]);
+ swap(c.texture[2*C[d]], c.texture[2*R[d]+1]);
+ swap(c.texture[2*C[d]], c.texture[2*C[d]+1]);
+
+ if(c.children)
+ {
+ int row = octadim(R[d]);
+ int col = octadim(C[d]);
+ for(int i=0; i<=octadim(d); i+=octadim(d)) rotatequad
+ (
+ c.children[i+row],
+ c.children[i],
+ c.children[i+col],
+ c.children[i+col+row]
+ );
+ loopi(8) rotatecube(c.children[i], d);
+ }
+}
+
+void mpflip(selinfo &sel, bool local)
+{
+ if(local)
+ {
+ game::edittrigger(sel, EDIT_FLIP);
+ makeundo();
+ }
+ int zs = sel.s[dimension(sel.orient)];
+ loopxy(sel)
+ {
+ loop(z,zs) flipcube(selcube(x, y, z), dimension(sel.orient));
+ loop(z,zs/2)
+ {
+ cube &a = selcube(x, y, z);
+ cube &b = selcube(x, y, zs-z-1);
+ swap(a, b);
+ }
+ }
+ changed(sel);
+}
+
+void flip()
+{
+ if(noedit()) return;
+ mpflip(sel, true);
+}
+
+void mprotate(int cw, selinfo &sel, bool local)
+{
+ if(local) game::edittrigger(sel, EDIT_ROTATE, cw);
+ int d = dimension(sel.orient);
+ if(!dimcoord(sel.orient)) cw = -cw;
+ int m = sel.s[C[d]] < sel.s[R[d]] ? C[d] : R[d];
+ int ss = sel.s[m] = max(sel.s[R[d]], sel.s[C[d]]);
+ if(local) makeundo();
+ loop(z,sel.s[D[d]]) loopi(cw>0 ? 1 : 3)
+ {
+ loopxy(sel) rotatecube(selcube(x,y,z), d);
+ loop(y,ss/2) loop(x,ss-1-y*2) rotatequad
+ (
+ selcube(ss-1-y, x+y, z),
+ selcube(x+y, y, z),
+ selcube(y, ss-1-x-y, z),
+ selcube(ss-1-x-y, ss-1-y, z)
+ );
+ }
+ changed(sel);
+}
+
+void rotate(int *cw)
+{
+ if(noedit()) return;
+ mprotate(*cw, sel, true);
+}
+
+COMMAND(flip, "");
+COMMAND(rotate, "i");
+
+enum { EDITMATF_EMPTY = 0x10000, EDITMATF_NOTEMPTY = 0x20000, EDITMATF_SOLID = 0x30000, EDITMATF_NOTSOLID = 0x40000 };
+static const struct { const char *name; int filter; } editmatfilters[] =
+{
+ { "empty", EDITMATF_EMPTY },
+ { "notempty", EDITMATF_NOTEMPTY },
+ { "solid", EDITMATF_SOLID },
+ { "notsolid", EDITMATF_NOTSOLID }
+};
+
+void setmat(cube &c, ushort mat, ushort matmask, ushort filtermat, ushort filtermask, int filtergeom)
+{
+ if(c.children)
+ loopi(8) setmat(c.children[i], mat, matmask, filtermat, filtermask, filtergeom);
+ else if((c.material&filtermask) == filtermat)
+ {
+ switch(filtergeom)
+ {
+ case EDITMATF_EMPTY: if(isempty(c)) break; return;
+ case EDITMATF_NOTEMPTY: if(!isempty(c)) break; return;
+ case EDITMATF_SOLID: if(isentirelysolid(c)) break; return;
+ case EDITMATF_NOTSOLID: if(!isentirelysolid(c)) break; return;
+ }
+ if(mat!=MAT_AIR)
+ {
+ c.material &= matmask;
+ c.material |= mat;
+ }
+ else c.material = MAT_AIR;
+ }
+}
+
+void mpeditmat(int matid, int filter, selinfo &sel, bool local)
+{
+ if(local) game::edittrigger(sel, EDIT_MAT, matid, filter);
+
+ ushort filtermat = 0, filtermask = 0, matmask;
+ int filtergeom = 0;
+ if(filter >= 0)
+ {
+ filtermat = filter&0xFFFF;
+ filtermask = filtermat&(MATF_VOLUME|MATF_INDEX) ? MATF_VOLUME|MATF_INDEX : (filtermat&MATF_CLIP ? MATF_CLIP : filtermat);
+ filtergeom = filter&~0xFFFF;
+ }
+ if(matid < 0)
+ {
+ matid = 0;
+ matmask = filtermask;
+ if(isclipped(filtermat&MATF_VOLUME)) matmask &= ~MATF_CLIP;
+ if(isdeadly(filtermat&MATF_VOLUME)) matmask &= ~MAT_DEATH;
+ }
+ else
+ {
+ matmask = matid&(MATF_VOLUME|MATF_INDEX) ? 0 : (matid&MATF_CLIP ? ~MATF_CLIP : ~matid);
+ if(isclipped(matid&MATF_VOLUME)) matid |= MAT_CLIP;
+ if(isdeadly(matid&MATF_VOLUME)) matid |= MAT_DEATH;
+ }
+ loopselxyz(setmat(c, matid, matmask, filtermat, filtermask, filtergeom));
+}
+
+void editmat(char *name, char *filtername)
+{
+ if(noedit()) return;
+ int filter = -1;
+ if(filtername[0])
+ {
+ loopi(sizeof(editmatfilters)/sizeof(editmatfilters[0])) if(!strcmp(editmatfilters[i].name, filtername)) { filter = editmatfilters[i].filter; break; }
+ if(filter < 0) filter = findmaterial(filtername);
+ if(filter < 0)
+ {
+ conoutf(CON_ERROR, "unknown material \"%s\"", filtername);
+ return;
+ }
+ }
+ int id = -1;
+ if(name[0] || filter < 0)
+ {
+ id = findmaterial(name);
+ if(id<0) { conoutf(CON_ERROR, "unknown material \"%s\"", name); return; }
+ }
+ mpeditmat(id, filter, sel, true);
+}
+
+COMMAND(editmat, "ss");
+
+extern int menudistance, menuautoclose;
+
+VARP(texguiwidth, 1, 15, 1000);
+VARP(texguiheight, 1, 8, 1000);
+FVARP(texguiscale, 0.1f, 1.5f, 10.0f);
+VARP(texguitime, 0, 15, 1000);
+VARP(texguiname, 0, 1, 1);
+
+static int lastthumbnail = 0;
+
+VARP(texgui2d, 0, 1, 1);
+VAR(texguinum, 1, -1, 0);
+
+struct texturegui : g3d_callback
+{
+ bool menuon;
+ vec menupos;
+ int menustart, menutab;
+
+ texturegui() : menustart(-1) {}
+
+ void gui(g3d_gui &g, bool firstpass)
+ {
+ int origtab = menutab, numtabs = max((slots.length() + texguiwidth*texguiheight - 1)/(texguiwidth*texguiheight), 1);
+ if(!firstpass) texguinum = -1;
+ g.start(menustart, 0.04f, &menutab);
+ bool oldautotab = g.allowautotab(false);
+ loopi(numtabs)
+ {
+ g.tab(!i ? "Textures" : NULL, 0xFFDD88);
+ if(i+1 != origtab) continue; //don't load textures on non-visible tabs!
+ Slot *rollover = NULL;
+ loop(h, texguiheight)
+ {
+ g.pushlist();
+ loop(w, texguiwidth)
+ {
+ extern VSlot dummyvslot;
+ int ti = (i*texguiheight+h)*texguiwidth+w;
+ if(ti<slots.length())
+ {
+ Slot &slot = lookupslot(ti, false);
+ VSlot &vslot = *slot.variants;
+ if(slot.sts.empty()) continue;
+ else if(!slot.loaded && !slot.thumbnail)
+ {
+ if(totalmillis-lastthumbnail<texguitime)
+ {
+ g.texture(dummyvslot, texguiscale, false); //create an empty space
+ continue;
+ }
+ loadthumbnail(slot);
+ lastthumbnail = totalmillis;
+ }
+ int ret = g.texture(vslot, texguiscale, true);
+ if(ret&G3D_ROLLOVER) { rollover = &slot; texguinum = ti; }
+ if(ret&G3D_UP && (slot.loaded || slot.thumbnail!=notexture))
+ {
+ edittex(vslot.index);
+ hudshader->set();
+ }
+ }
+ else
+ {
+ g.texture(dummyvslot, texguiscale, false); //create an empty space
+ }
+ }
+ g.poplist();
+ }
+ if(texguiname)
+ {
+ if(rollover)
+ {
+ defformatstring(name, "%d \f7:\fc %s", texguinum, rollover->sts[0].name);
+ g.title(name, 0xFFDD88);
+ }
+ else g.space(1);
+ }
+ }
+ g.allowautotab(oldautotab);
+ g.end();
+ }
+
+ void showtextures(bool on)
+ {
+ if(on == menuon) return;
+ if((menuon = on))
+ {
+ if(menustart <= lasttexmillis)
+ menutab = 1+clamp(lookupvslot(lasttex, false).slot->index, 0, slots.length()-1)/(texguiwidth*texguiheight);
+ menupos = menuinfrontofplayer();
+ menustart = starttime();
+ }
+ else texguinum = -1;
+ }
+
+ void show()
+ {
+ if(!menuon) return;
+ filltexlist();
+ extern int usegui2d;
+ if(!editmode || ((!texgui2d || !usegui2d) && camera1->o.dist(menupos) > menuautoclose)) { menuon = false; texguinum = -1; }
+ else g3d_addgui(this, menupos, texgui2d ? GUI_2D : 0);
+ }
+} gui;
+
+void g3d_texturemenu()
+{
+ gui.show();
+}
+
+void showtexgui(int *n)
+{
+ if(!editmode) { conoutf(CON_ERROR, "operation only allowed in edit mode"); return; }
+ gui.showtextures(*n==0 ? !gui.menuon : *n==1);
+}
+
+// 0/noargs = toggle, 1 = on, other = off - will autoclose if too far away or exit editmode
+COMMAND(showtexgui, "i");
+
+bool cleartexgui()
+{
+ if(!gui.menuon) return false;
+ gui.showtextures(false);
+ return true;
+}
+ICOMMAND(cleartexgui, "", (), intret(cleartexgui() ? 1 : 0));
+
+void rendertexturepanel(int w, int h)
+{
+ if((texpaneltimer -= curtime)>0 && editmode)
+ {
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ pushhudmatrix();
+ hudmatrix.scale(h/1800.0f, h/1800.0f, 1);
+ flushhudmatrix(false);
+ SETSHADER(hudrgb);
+
+ int y = 50, gap = 10;
+
+ gle::defvertex(2);
+ gle::deftexcoord0();
+
+ loopi(7)
+ {
+ int s = (i == 3 ? 285 : 220), ti = curtexindex+i-3;
+ if(texmru.inrange(ti))
+ {
+ VSlot &vslot = lookupvslot(texmru[ti]), *layer = NULL;
+ Slot &slot = *vslot.slot;
+ Texture *tex = slot.sts.empty() ? notexture : slot.sts[0].t, *glowtex = NULL, *layertex = NULL;
+ if(slot.texmask&(1<<TEX_GLOW))
+ {
+ loopvj(slot.sts) if(slot.sts[j].type==TEX_GLOW) { glowtex = slot.sts[j].t; break; }
+ }
+ if(vslot.layer)
+ {
+ layer = &lookupvslot(vslot.layer);
+ layertex = layer->slot->sts.empty() ? notexture : layer->slot->sts[0].t;
+ }
+ float sx = min(1.0f, tex->xs/(float)tex->ys), sy = min(1.0f, tex->ys/(float)tex->xs);
+ int x = w*1800/h-s-50, r = s;
+ vec2 tc[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) };
+ float xoff = vslot.offset.x, yoff = vslot.offset.y;
+ if(vslot.rotation)
+ {
+ const texrotation &r = texrotations[vslot.rotation];
+ if(r.swapxy) { swap(xoff, yoff); loopk(4) swap(tc[k].x, tc[k].y); }
+ if(r.flipx) { xoff *= -1; loopk(4) tc[k].x *= -1; }
+ if(r.flipy) { yoff *= -1; loopk(4) tc[k].y *= -1; }
+ }
+ loopk(4) { tc[k].x = tc[k].x/sx - xoff/tex->xs; tc[k].y = tc[k].y/sy - yoff/tex->ys; }
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+ loopj(glowtex ? 3 : 2)
+ {
+ if(j < 2) gle::color(vec(vslot.colorscale).mul(j), texpaneltimer/1000.0f);
+ else
+ {
+ glBindTexture(GL_TEXTURE_2D, glowtex->id);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+ gle::color(vslot.glowcolor, texpaneltimer/1000.0f);
+ }
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attribf(x, y); gle::attrib(tc[0]);
+ gle::attribf(x+r, y); gle::attrib(tc[1]);
+ gle::attribf(x, y+r); gle::attrib(tc[3]);
+ gle::attribf(x+r, y+r); gle::attrib(tc[2]);
+ xtraverts += gle::end();
+ if(j==1 && layertex)
+ {
+ gle::color(layer->colorscale, texpaneltimer/1000.0f);
+ glBindTexture(GL_TEXTURE_2D, layertex->id);
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attribf(x+r/2, y+r/2); gle::attrib(tc[0]);
+ gle::attribf(x+r, y+r/2); gle::attrib(tc[1]);
+ gle::attribf(x+r/2, y+r); gle::attrib(tc[3]);
+ gle::attribf(x+r, y+r); gle::attrib(tc[2]);
+ xtraverts += gle::end();
+ }
+ if(!j)
+ {
+ r -= 10;
+ x += 5;
+ y += 5;
+ }
+ else if(j == 2) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+ }
+ y += s+gap;
+ }
+
+ pophudmatrix(true, false);
+ hudshader->set();
+ }
+}
diff --git a/src/engine/octarender.cpp b/src/engine/octarender.cpp
new file mode 100644
index 0000000..faf609f
--- /dev/null
+++ b/src/engine/octarender.cpp
@@ -0,0 +1,1803 @@
+// octarender.cpp: fill vertex arrays with different cube surfaces.
+
+#include "engine.h"
+
+struct vboinfo
+{
+ int uses;
+};
+
+hashtable<GLuint, vboinfo> vbos;
+
+VAR(printvbo, 0, 0, 1);
+VARFN(vbosize, maxvbosize, 0, 1<<14, 1<<16, allchanged());
+
+enum
+{
+ VBO_VBUF = 0,
+ VBO_EBUF,
+ VBO_SKYBUF,
+ NUMVBO
+};
+
+static vector<uchar> vbodata[NUMVBO];
+static vector<vtxarray *> vbovas[NUMVBO];
+static int vbosize[NUMVBO];
+
+void destroyvbo(GLuint vbo)
+{
+ vboinfo *exists = vbos.access(vbo);
+ if(!exists) return;
+ vboinfo &vbi = *exists;
+ if(vbi.uses <= 0) return;
+ vbi.uses--;
+ if(!vbi.uses)
+ {
+ glDeleteBuffers_(1, &vbo);
+ vbos.remove(vbo);
+ }
+}
+
+void genvbo(int type, void *buf, int len, vtxarray **vas, int numva)
+{
+ gle::disable();
+
+ GLuint vbo;
+ glGenBuffers_(1, &vbo);
+ GLenum target = type==VBO_VBUF ? GL_ARRAY_BUFFER : GL_ELEMENT_ARRAY_BUFFER;
+ glBindBuffer_(target, vbo);
+ glBufferData_(target, len, buf, GL_STATIC_DRAW);
+ glBindBuffer_(target, 0);
+
+ vboinfo &vbi = vbos[vbo];
+ vbi.uses = numva;
+
+ if(printvbo) conoutf(CON_DEBUG, "vbo %d: type %d, size %d, %d uses", vbo, type, len, numva);
+
+ loopi(numva)
+ {
+ vtxarray *va = vas[i];
+ switch(type)
+ {
+ case VBO_VBUF:
+ va->vbuf = vbo;
+ break;
+ case VBO_EBUF:
+ va->ebuf = vbo;
+ break;
+ case VBO_SKYBUF:
+ va->skybuf = vbo;
+ break;
+ }
+ }
+}
+
+bool readva(vtxarray *va, ushort *&edata, vertex *&vdata)
+{
+ if(!va->vbuf || !va->ebuf) return false;
+
+ edata = new ushort[3*va->tris];
+ vdata = new vertex[va->verts];
+
+ gle::bindebo(va->ebuf);
+ glGetBufferSubData_(GL_ELEMENT_ARRAY_BUFFER, (size_t)va->edata, 3*va->tris*sizeof(ushort), edata);
+ gle::clearebo();
+
+ gle::bindvbo(va->vbuf);
+ glGetBufferSubData_(GL_ARRAY_BUFFER, va->voffset*sizeof(vertex), va->verts*sizeof(vertex), vdata);
+ gle::clearvbo();
+ return true;
+}
+
+void flushvbo(int type = -1)
+{
+ if(type < 0)
+ {
+ loopi(NUMVBO) flushvbo(i);
+ return;
+ }
+
+ vector<uchar> &data = vbodata[type];
+ if(data.empty()) return;
+ vector<vtxarray *> &vas = vbovas[type];
+ genvbo(type, data.getbuf(), data.length(), vas.getbuf(), vas.length());
+ data.setsize(0);
+ vas.setsize(0);
+ vbosize[type] = 0;
+}
+
+uchar *addvbo(vtxarray *va, int type, int numelems, int elemsize)
+{
+ vbosize[type] += numelems;
+
+ vector<uchar> &data = vbodata[type];
+ vector<vtxarray *> &vas = vbovas[type];
+
+ vas.add(va);
+
+ int len = numelems*elemsize;
+ uchar *buf = data.reserve(len).buf;
+ data.advance(len);
+ return buf;
+}
+
+struct verthash
+{
+ static const int SIZE = 1<<13;
+ int table[SIZE];
+ vector<vertex> verts;
+ vector<int> chain;
+
+ verthash() { clearverts(); }
+
+ void clearverts()
+ {
+ memset(table, -1, sizeof(table));
+ chain.setsize(0);
+ verts.setsize(0);
+ }
+
+ int addvert(const vertex &v)
+ {
+ uint h = hthash(v.pos)&(SIZE-1);
+ for(int i = table[h]; i>=0; i = chain[i])
+ {
+ const vertex &c = verts[i];
+ if(c.pos==v.pos && c.tc==v.tc && c.norm==v.norm && c.tangent==v.tangent && (v.lm.iszero() || c.lm==v.lm))
+ return i;
+ }
+ if(verts.length() >= USHRT_MAX) return -1;
+ verts.add(v);
+ chain.add(table[h]);
+ return table[h] = verts.length()-1;
+ }
+
+ int addvert(const vec &pos, const vec2 &tc = vec2(0, 0), const svec2 &lm = svec2(0, 0), const bvec &norm = bvec(128, 128, 128), const bvec4 &tangent = bvec4(128, 128, 128, 128))
+ {
+ vertex vtx;
+ vtx.pos = pos;
+ vtx.tc = tc;
+ vtx.lm = lm;
+ vtx.norm = norm;
+ vtx.tangent = tangent;
+ return addvert(vtx);
+ }
+};
+
+enum
+{
+ NO_ALPHA = 0,
+ ALPHA_BACK,
+ ALPHA_FRONT
+};
+
+struct sortkey
+{
+ ushort tex, lmid, envmap;
+ uchar dim, layer, alpha;
+
+ sortkey() {}
+ sortkey(ushort tex, ushort lmid, uchar dim, uchar layer = LAYER_TOP, ushort envmap = EMID_NONE, uchar alpha = NO_ALPHA)
+ : tex(tex), lmid(lmid), envmap(envmap), dim(dim), layer(layer), alpha(alpha)
+ {}
+
+ bool operator==(const sortkey &o) const { return tex==o.tex && lmid==o.lmid && envmap==o.envmap && dim==o.dim && layer==o.layer && alpha==o.alpha; }
+};
+
+struct sortval
+{
+ int unlit;
+ vector<ushort> tris[2];
+
+ sortval() : unlit(0) {}
+};
+
+static inline bool htcmp(const sortkey &x, const sortkey &y)
+{
+ return x == y;
+}
+
+static inline uint hthash(const sortkey &k)
+{
+ return k.tex + k.lmid*9741;
+}
+
+struct vacollect : verthash
+{
+ ivec origin;
+ int size;
+ hashtable<sortkey, sortval> indices;
+ vector<sortkey> texs;
+ vector<grasstri> grasstris;
+ vector<materialsurface> matsurfs;
+ vector<octaentities *> mapmodels;
+ vector<ushort> skyindices, explicitskyindices;
+ vector<facebounds> skyfaces[6];
+ int worldtris, skytris, skymask, skyclip, skyarea;
+
+ void clear()
+ {
+ clearverts();
+ worldtris = skytris = 0;
+ skymask = 0;
+ skyclip = INT_MAX;
+ skyarea = 0;
+ indices.clear();
+ skyindices.setsize(0);
+ explicitskyindices.setsize(0);
+ matsurfs.setsize(0);
+ mapmodels.setsize(0);
+ grasstris.setsize(0);
+ texs.setsize(0);
+ loopi(6) skyfaces[i].setsize(0);
+ }
+
+ void remapunlit(vector<sortkey> &remap)
+ {
+ uint lastlmid[8] = { LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT },
+ firstlmid[8] = { LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT };
+ int firstlit[8] = { -1, -1, -1, -1, -1, -1, -1, -1 };
+ loopv(texs)
+ {
+ sortkey &k = texs[i];
+ if(k.lmid>=LMID_RESERVED)
+ {
+ LightMapTexture &lmtex = lightmaptexs[k.lmid];
+ int type = lmtex.type&LM_TYPE;
+ if(k.layer==LAYER_BLEND) type += 2;
+ else if(k.alpha) type += 4 + 2*(k.alpha-1);
+ lastlmid[type] = lmtex.unlitx>=0 ? k.lmid : LMID_AMBIENT;
+ if(firstlmid[type]==LMID_AMBIENT && lastlmid[type]!=LMID_AMBIENT)
+ {
+ firstlit[type] = i;
+ firstlmid[type] = lastlmid[type];
+ }
+ }
+ else if(k.lmid==LMID_AMBIENT)
+ {
+ Shader *s = lookupvslot(k.tex, false).slot->shader;
+ int type = s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE;
+ if(k.layer==LAYER_BLEND) type += 2;
+ else if(k.alpha) type += 4 + 2*(k.alpha-1);
+ if(lastlmid[type]!=LMID_AMBIENT)
+ {
+ sortval &t = indices[k];
+ if(t.unlit<=0) t.unlit = lastlmid[type];
+ }
+ }
+ }
+ loopj(2)
+ {
+ int offset = 2*j;
+ if(firstlmid[offset]==LMID_AMBIENT && firstlmid[offset+1]==LMID_AMBIENT) continue;
+ loopi(max(firstlit[offset], firstlit[offset+1]))
+ {
+ sortkey &k = texs[i];
+ if((j ? k.layer!=LAYER_BLEND : k.layer==LAYER_BLEND) || k.alpha) continue;
+ if(k.lmid!=LMID_AMBIENT) continue;
+ Shader *s = lookupvslot(k.tex, false).slot->shader;
+ int type = offset + (s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE);
+ if(firstlmid[type]==LMID_AMBIENT) continue;
+ indices[k].unlit = firstlmid[type];
+ }
+ }
+ loopj(2)
+ {
+ int offset = 4 + 2*j;
+ if(firstlmid[offset]==LMID_AMBIENT && firstlmid[offset+1]==LMID_AMBIENT) continue;
+ loopi(max(firstlit[offset], firstlit[offset+1]))
+ {
+ sortkey &k = texs[i];
+ if(k.alpha != j+1) continue;
+ if(k.lmid!=LMID_AMBIENT) continue;
+ Shader *s = lookupvslot(k.tex, false).slot->shader;
+ int type = offset + (s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE);
+ if(firstlmid[type]==LMID_AMBIENT) continue;
+ indices[k].unlit = firstlmid[type];
+ }
+ }
+ loopv(remap)
+ {
+ sortkey &k = remap[i];
+ sortval &t = indices[k];
+ if(t.unlit<=0) continue;
+ LightMapTexture &lm = lightmaptexs[t.unlit];
+ svec2 lmtc(short(ceil((lm.unlitx + 0.5f) * SHRT_MAX/lm.w)),
+ short(ceil((lm.unlity + 0.5f) * SHRT_MAX/lm.h)));
+ loopl(2) loopvj(t.tris[l])
+ {
+ vertex &vtx = verts[t.tris[l][j]];
+ if(vtx.lm.iszero()) vtx.lm = lmtc;
+ else if(vtx.lm != lmtc)
+ {
+ vertex vtx2 = vtx;
+ vtx2.lm = lmtc;
+ t.tris[l][j] = addvert(vtx2);
+ }
+ }
+ sortval *dst = indices.access(sortkey(k.tex, t.unlit, k.dim, k.layer, k.envmap, k.alpha));
+ if(dst) loopl(2) loopvj(t.tris[l]) dst->tris[l].add(t.tris[l][j]);
+ }
+ }
+
+ void optimize()
+ {
+ vector<sortkey> remap;
+ enumeratekt(indices, sortkey, k, sortval, t,
+ loopl(2) if(t.tris[l].length() && t.unlit<=0)
+ {
+ if(k.lmid>=LMID_RESERVED && lightmaptexs[k.lmid].unlitx>=0)
+ {
+ sortkey ukey(k.tex, LMID_AMBIENT, k.dim, k.layer, k.envmap, k.alpha);
+ sortval *uval = indices.access(ukey);
+ if(uval && uval->unlit<=0)
+ {
+ if(uval->unlit<0) texs.removeobj(ukey);
+ else remap.add(ukey);
+ uval->unlit = k.lmid;
+ }
+ }
+ else if(k.lmid==LMID_AMBIENT)
+ {
+ remap.add(k);
+ t.unlit = -1;
+ }
+ texs.add(k);
+ break;
+ }
+ );
+ texs.sort(texsort);
+
+ remapunlit(remap);
+
+ matsurfs.shrink(optimizematsurfs(matsurfs.getbuf(), matsurfs.length()));
+ }
+
+ static inline bool texsort(const sortkey &x, const sortkey &y)
+ {
+ if(x.alpha < y.alpha) return true;
+ if(x.alpha > y.alpha) return false;
+ if(x.layer < y.layer) return true;
+ if(x.layer > y.layer) return false;
+ if(x.tex == y.tex)
+ {
+ if(x.lmid < y.lmid) return true;
+ if(x.lmid > y.lmid) return false;
+ if(x.envmap < y.envmap) return true;
+ if(x.envmap > y.envmap) return false;
+ if(x.dim < y.dim) return true;
+ if(x.dim > y.dim) return false;
+ return false;
+ }
+ VSlot &xs = lookupvslot(x.tex, false), &ys = lookupvslot(y.tex, false);
+ if(xs.slot->shader < ys.slot->shader) return true;
+ if(xs.slot->shader > ys.slot->shader) return false;
+ if(xs.slot->params.length() < ys.slot->params.length()) return true;
+ if(xs.slot->params.length() > ys.slot->params.length()) return false;
+ if(x.tex < y.tex) return true;
+ else return false;
+ }
+
+#define GENVERTS(type, ptr, body) do \
+ { \
+ type *f = (type *)ptr; \
+ loopv(verts) \
+ { \
+ const vertex &v = verts[i]; \
+ body; \
+ f++; \
+ } \
+ } while(0)
+
+ void genverts(void *buf)
+ {
+ GENVERTS(vertex, buf, { *f = v; f->norm.flip(); f->tangent.flip(); });
+ }
+
+ void setupdata(vtxarray *va)
+ {
+ va->verts = verts.length();
+ va->tris = worldtris/3;
+ va->vbuf = 0;
+ va->vdata = 0;
+ va->minvert = 0;
+ va->maxvert = va->verts-1;
+ va->voffset = 0;
+ if(va->verts)
+ {
+ if(vbosize[VBO_VBUF] + verts.length() > maxvbosize ||
+ vbosize[VBO_EBUF] + worldtris > USHRT_MAX ||
+ vbosize[VBO_SKYBUF] + skytris > USHRT_MAX)
+ flushvbo();
+
+ va->voffset = vbosize[VBO_VBUF];
+ uchar *vdata = addvbo(va, VBO_VBUF, va->verts, sizeof(vertex));
+ genverts(vdata);
+ va->minvert += va->voffset;
+ va->maxvert += va->voffset;
+ }
+
+ va->matbuf = NULL;
+ va->matsurfs = matsurfs.length();
+ if(va->matsurfs)
+ {
+ va->matbuf = new materialsurface[matsurfs.length()];
+ memcpy(va->matbuf, matsurfs.getbuf(), matsurfs.length()*sizeof(materialsurface));
+ }
+
+ va->skybuf = 0;
+ va->skydata = 0;
+ va->sky = skyindices.length();
+ va->explicitsky = explicitskyindices.length();
+ if(va->sky + va->explicitsky)
+ {
+ va->skydata += vbosize[VBO_SKYBUF];
+ ushort *skydata = (ushort *)addvbo(va, VBO_SKYBUF, va->sky+va->explicitsky, sizeof(ushort));
+ memcpy(skydata, skyindices.getbuf(), va->sky*sizeof(ushort));
+ memcpy(skydata+va->sky, explicitskyindices.getbuf(), va->explicitsky*sizeof(ushort));
+ if(va->voffset) loopi(va->sky+va->explicitsky) skydata[i] += va->voffset;
+ }
+
+ va->eslist = NULL;
+ va->texs = texs.length();
+ va->blendtris = 0;
+ va->blends = 0;
+ va->alphabacktris = 0;
+ va->alphaback = 0;
+ va->alphafronttris = 0;
+ va->alphafront = 0;
+ va->ebuf = 0;
+ va->edata = 0;
+ va->texmask = 0;
+ if(va->texs)
+ {
+ va->eslist = new elementset[va->texs];
+ va->edata += vbosize[VBO_EBUF];
+ ushort *edata = (ushort *)addvbo(va, VBO_EBUF, worldtris, sizeof(ushort)), *curbuf = edata;
+ loopv(texs)
+ {
+ const sortkey &k = texs[i];
+ const sortval &t = indices[k];
+ elementset &e = va->eslist[i];
+ e.texture = k.tex;
+ e.lmid = t.unlit>0 ? t.unlit : k.lmid;
+ e.dim = k.dim;
+ e.layer = k.layer;
+ e.envmap = k.envmap;
+ ushort *startbuf = curbuf;
+ loopl(2)
+ {
+ e.minvert[l] = USHRT_MAX;
+ e.maxvert[l] = 0;
+
+ if(t.tris[l].length())
+ {
+ memcpy(curbuf, t.tris[l].getbuf(), t.tris[l].length() * sizeof(ushort));
+
+ loopvj(t.tris[l])
+ {
+ curbuf[j] += va->voffset;
+ e.minvert[l] = min(e.minvert[l], curbuf[j]);
+ e.maxvert[l] = max(e.maxvert[l], curbuf[j]);
+ }
+
+ curbuf += t.tris[l].length();
+ }
+ e.length[l] = curbuf-startbuf;
+ }
+ if(k.layer==LAYER_BLEND) { va->texs--; va->tris -= e.length[1]/3; va->blends++; va->blendtris += e.length[1]/3; }
+ else if(k.alpha==ALPHA_BACK) { va->texs--; va->tris -= e.length[1]/3; va->alphaback++; va->alphabacktris += e.length[1]/3; }
+ else if(k.alpha==ALPHA_FRONT) { va->texs--; va->tris -= e.length[1]/3; va->alphafront++; va->alphafronttris += e.length[1]/3; }
+
+ Slot &slot = *lookupvslot(k.tex, false).slot;
+ loopvj(slot.sts) va->texmask |= 1<<slot.sts[j].type;
+ if(slot.shader->type&SHADER_ENVMAP) va->texmask |= 1<<TEX_ENVMAP;
+ }
+ }
+
+ va->alphatris = va->alphabacktris + va->alphafronttris;
+
+ if(grasstris.length())
+ {
+ va->grasstris.move(grasstris);
+ useshaderbyname("grass");
+ }
+
+ if(mapmodels.length()) va->mapmodels.put(mapmodels.getbuf(), mapmodels.length());
+ }
+
+ bool emptyva()
+ {
+ return verts.empty() && matsurfs.empty() && skyindices.empty() && explicitskyindices.empty() && grasstris.empty() && mapmodels.empty();
+ }
+} vc;
+
+int recalcprogress = 0;
+#define progress(s) if((recalcprogress++&0xFFF)==0) renderprogress(recalcprogress/(float)allocnodes, s);
+
+vector<tjoint> tjoints;
+
+vec shadowmapmin, shadowmapmax;
+
+int calcshadowmask(vec *pos, int numpos)
+{
+ extern vec shadowdir;
+ int mask = 0, used = 1;
+ vec pe = vec(pos[1]).sub(pos[0]);
+ loopk(numpos-2)
+ {
+ vec e = vec(pos[k+2]).sub(pos[0]);
+ if(vec().cross(pe, e).dot(shadowdir)>0)
+ {
+ mask |= 1<<k;
+ used |= 6<<k;
+ }
+ pe = e;
+ }
+ if(!mask) return 0;
+ loopk(numpos) if(used&(1<<k))
+ {
+ const vec &v = pos[k];
+ shadowmapmin.min(v);
+ shadowmapmax.max(v);
+ }
+ return mask;
+}
+
+VARFP(filltjoints, 0, 1, 1, allchanged());
+
+void reduceslope(ivec &n)
+{
+ int mindim = -1, minval = 64;
+ loopi(3) if(n[i])
+ {
+ int val = abs(n[i]);
+ if(mindim < 0 || val < minval)
+ {
+ mindim = i;
+ minval = val;
+ }
+ }
+ if(!(n[R[mindim]]%minval) && !(n[C[mindim]]%minval)) n.div(minval);
+ while(!((n.x|n.y|n.z)&1)) n.shr(1);
+}
+
+// [rotation][dimension]
+extern const vec orientation_tangent[8][3] =
+{
+ { vec(0, 1, 0), vec( 1, 0, 0), vec( 1, 0, 0) },
+ { vec(0, 0, -1), vec( 0, 0, -1), vec( 0, 1, 0) },
+ { vec(0, -1, 0), vec(-1, 0, 0), vec(-1, 0, 0) },
+ { vec(0, 0, 1), vec( 0, 0, 1), vec( 0, -1, 0) },
+ { vec(0, -1, 0), vec(-1, 0, 0), vec(-1, 0, 0) },
+ { vec(0, 1, 0), vec( 1, 0, 0), vec( 1, 0, 0) },
+ { vec(0, 0, -1), vec( 0, 0, -1), vec( 0, 1, 0) },
+ { vec(0, 0, 1), vec( 0, 0, 1), vec( 0, -1, 0) },
+};
+extern const vec orientation_bitangent[8][3] =
+{
+ { vec(0, 0, -1), vec( 0, 0, -1), vec( 0, 1, 0) },
+ { vec(0, -1, 0), vec(-1, 0, 0), vec(-1, 0, 0) },
+ { vec(0, 0, 1), vec( 0, 0, 1), vec( 0, -1, 0) },
+ { vec(0, 1, 0), vec( 1, 0, 0), vec( 1, 0, 0) },
+ { vec(0, 0, -1), vec( 0, 0, -1), vec( 0, 1, 0) },
+ { vec(0, 0, 1), vec( 0, 0, 1), vec( 0, -1, 0) },
+ { vec(0, 1, 0), vec( 1, 0, 0), vec( 1, 0, 0) },
+ { vec(0, -1, 0), vec(-1, 0, 0), vec(-1, 0, 0) },
+};
+
+void addtris(const sortkey &key, int orient, vertex *verts, int *index, int numverts, int convex, int shadowmask, int tj)
+{
+ int &total = key.tex==DEFAULT_SKY ? vc.skytris : vc.worldtris;
+ int edge = orient*(MAXFACEVERTS+1);
+ loopi(numverts-2) if(index[0]!=index[i+1] && index[i+1]!=index[i+2] && index[i+2]!=index[0])
+ {
+ vector<ushort> &idxs = key.tex==DEFAULT_SKY ? vc.explicitskyindices : vc.indices[key].tris[(shadowmask>>i)&1];
+ int left = index[0], mid = index[i+1], right = index[i+2], start = left, i0 = left, i1 = -1;
+ loopk(4)
+ {
+ int i2 = -1, ctj = -1, cedge = -1;
+ switch(k)
+ {
+ case 1: i1 = i2 = mid; cedge = edge+i+1; break;
+ case 2: if(i1 != mid || i0 == left) { i0 = i1; i1 = right; } i2 = right; if(i+1 == numverts-2) cedge = edge+i+2; break;
+ case 3: if(i0 == start) { i0 = i1; i1 = left; } i2 = left; // fall-through
+ default: if(!i) cedge = edge; break;
+ }
+ if(i1 != i2)
+ {
+ if(total + 3 > USHRT_MAX) return;
+ total += 3;
+ idxs.add(i0);
+ idxs.add(i1);
+ idxs.add(i2);
+ i1 = i2;
+ }
+ if(cedge >= 0)
+ {
+ for(ctj = tj;;)
+ {
+ if(ctj < 0) break;
+ if(tjoints[ctj].edge < cedge) { ctj = tjoints[ctj].next; continue; }
+ if(tjoints[ctj].edge != cedge) ctj = -1;
+ break;
+ }
+ }
+ if(ctj >= 0)
+ {
+ int e1 = cedge%(MAXFACEVERTS+1), e2 = (e1+1)%numverts;
+ vertex &v1 = verts[e1], &v2 = verts[e2];
+ ivec d(vec(v2.pos).sub(v1.pos).mul(8));
+ int axis = abs(d.x) > abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2);
+ if(d[axis] < 0) d.neg();
+ reduceslope(d);
+ int origin = int(min(v1.pos[axis], v2.pos[axis])*8)&~0x7FFF,
+ offset1 = (int(v1.pos[axis]*8) - origin) / d[axis],
+ offset2 = (int(v2.pos[axis]*8) - origin) / d[axis];
+ vec o = vec(v1.pos).sub(vec(d).mul(offset1/8.0f));
+ float doffset = 1.0f / (offset2 - offset1);
+
+ if(i1 < 0) for(;;)
+ {
+ tjoint &t = tjoints[ctj];
+ if(t.next < 0 || tjoints[t.next].edge != cedge) break;
+ ctj = t.next;
+ }
+ while(ctj >= 0)
+ {
+ tjoint &t = tjoints[ctj];
+ if(t.edge != cedge) break;
+ float offset = (t.offset - offset1) * doffset;
+ vertex vt;
+ vt.pos = vec(d).mul(t.offset/8.0f).add(o);
+ vt.tc.lerp(v1.tc, v2.tc, offset);
+ vt.lm.x = short(v1.lm.x + (v2.lm.x-v1.lm.x)*offset),
+ vt.lm.y = short(v1.lm.y + (v2.lm.y-v1.lm.y)*offset);
+ vt.norm.lerp(v1.norm, v2.norm, offset);
+ vt.tangent.lerp(v1.tangent, v2.tangent, offset);
+ int i2 = vc.addvert(vt);
+ if(i2 < 0) return;
+ if(i1 >= 0)
+ {
+ if(total + 3 > USHRT_MAX) return;
+ total += 3;
+ idxs.add(i0);
+ idxs.add(i1);
+ idxs.add(i2);
+ i1 = i2;
+ }
+ else start = i0 = i2;
+ ctj = t.next;
+ }
+ }
+ }
+ }
+}
+
+void addgrasstri(int face, vertex *verts, int numv, ushort texture, ushort lmid)
+{
+ grasstri &g = vc.grasstris.add();
+ int i1, i2, i3, i4;
+ if(numv <= 3 && face%2) { i1 = face+1; i2 = face+2; i3 = i4 = 0; }
+ else { i1 = 0; i2 = face+1; i3 = face+2; i4 = numv > 3 ? face+3 : i3; }
+ g.v[0] = verts[i1].pos;
+ g.v[1] = verts[i2].pos;
+ g.v[2] = verts[i3].pos;
+ g.v[3] = verts[i4].pos;
+ g.numv = numv;
+
+ g.surface.toplane(g.v[0], g.v[1], g.v[2]);
+ if(g.surface.z <= 0) { vc.grasstris.pop(); return; }
+
+ g.minz = min(min(g.v[0].z, g.v[1].z), min(g.v[2].z, g.v[3].z));
+ g.maxz = max(max(g.v[0].z, g.v[1].z), max(g.v[2].z, g.v[3].z));
+
+ g.center = vec(0, 0, 0);
+ loopk(numv) g.center.add(g.v[k]);
+ g.center.div(numv);
+ g.radius = 0;
+ loopk(numv) g.radius = max(g.radius, g.v[k].dist(g.center));
+
+ vec area, bx, by;
+ area.cross(vec(g.v[1]).sub(g.v[0]), vec(g.v[2]).sub(g.v[0]));
+ float scale;
+ int px, py;
+
+ if(fabs(area.x) >= fabs(area.y) && fabs(area.x) >= fabs(area.z))
+ scale = 1/area.x, px = 1, py = 2;
+ else if(fabs(area.y) >= fabs(area.x) && fabs(area.y) >= fabs(area.z))
+ scale = -1/area.y, px = 0, py = 2;
+ else
+ scale = 1/area.z, px = 0, py = 1;
+
+ bx.x = (g.v[2][py] - g.v[0][py])*scale;
+ bx.y = (g.v[2][px] - g.v[0][px])*scale;
+ bx.z = bx.x*g.v[2][px] - bx.y*g.v[2][py];
+
+ by.x = (g.v[2][py] - g.v[1][py])*scale;
+ by.y = (g.v[2][px] - g.v[1][px])*scale;
+ by.z = by.x*g.v[1][px] - by.y*g.v[1][py] - 1;
+ by.sub(bx);
+
+ float tc1u = verts[i1].lm.x,
+ tc1v = verts[i1].lm.y,
+ tc2u = verts[i2].lm.x - verts[i1].lm.x,
+ tc2v = verts[i2].lm.y - verts[i1].lm.y,
+ tc3u = verts[i3].lm.x - verts[i1].lm.x,
+ tc3v = verts[i3].lm.y - verts[i1].lm.y;
+
+ g.tcu = vec4(0, 0, 0, tc1u - (bx.z*tc2u + by.z*tc3u));
+ g.tcu[px] = bx.x*tc2u + by.x*tc3u;
+ g.tcu[py] = -(bx.y*tc2u + by.y*tc3u);
+
+ g.tcv = vec4(0, 0, 0, tc1v - (bx.z*tc2v + by.z*tc3v));
+ g.tcv[px] = bx.x*tc2v + by.x*tc3v;
+ g.tcv[py] = -(bx.y*tc2v + by.y*tc3v);
+
+ g.texture = texture;
+ g.lmid = lmid;
+}
+
+static inline void calctexgen(VSlot &vslot, int dim, vec4 &sgen, vec4 &tgen)
+{
+ Texture *tex = vslot.slot->sts.empty() ? notexture : vslot.slot->sts[0].t;
+ const texrotation &r = texrotations[vslot.rotation];
+ float k = TEX_SCALE/vslot.scale,
+ xs = r.flipx ? -tex->xs : tex->xs,
+ ys = r.flipy ? -tex->ys : tex->ys,
+ sk = k/xs, tk = k/ys,
+ soff = -(r.swapxy ? vslot.offset.y : vslot.offset.x)/xs,
+ toff = -(r.swapxy ? vslot.offset.x : vslot.offset.y)/ys;
+ static const int si[] = { 1, 0, 0 }, ti[] = { 2, 2, 1 };
+ int sdim = si[dim], tdim = ti[dim];
+ sgen = vec4(0, 0, 0, soff);
+ tgen = vec4(0, 0, 0, toff);
+ if(r.swapxy)
+ {
+ sgen[tdim] = (dim <= 1 ? -sk : sk);
+ tgen[sdim] = tk;
+ }
+ else
+ {
+ sgen[sdim] = sk;
+ tgen[tdim] = (dim <= 1 ? -tk : tk);
+ }
+}
+
+ushort encodenormal(const vec &n)
+{
+ if(n.iszero()) return 0;
+ int yaw = int(-atan2(n.x, n.y)/RAD), pitch = int(asin(n.z)/RAD);
+ return ushort(clamp(pitch + 90, 0, 180)*360 + (yaw < 0 ? yaw%360 + 360 : yaw%360) + 1);
+}
+
+vec decodenormal(ushort norm)
+{
+ if(!norm) return vec(0, 0, 1);
+ norm--;
+ const vec2 &yaw = sincos360[norm%360], &pitch = sincos360[norm/360+270];
+ return vec(-yaw.y*pitch.x, yaw.x*pitch.x, pitch.y);
+}
+
+void guessnormals(const vec *pos, int numverts, vec *normals)
+{
+ vec n1, n2;
+ n1.cross(pos[0], pos[1], pos[2]);
+ if(numverts != 4)
+ {
+ n1.normalize();
+ loopk(numverts) normals[k] = n1;
+ return;
+ }
+ n2.cross(pos[0], pos[2], pos[3]);
+ if(n1.iszero())
+ {
+ n2.normalize();
+ loopk(4) normals[k] = n2;
+ return;
+ }
+ else n1.normalize();
+ if(n2.iszero())
+ {
+ loopk(4) normals[k] = n1;
+ return;
+ }
+ else n2.normalize();
+ vec avg = vec(n1).add(n2).normalize();
+ normals[0] = avg;
+ normals[1] = n1;
+ normals[2] = avg;
+ normals[3] = n2;
+}
+
+void addcubeverts(VSlot &vslot, int orient, int size, vec *pos, int convex, ushort texture, ushort lmid, vertinfo *vinfo, int numverts, int tj = -1, ushort envmap = EMID_NONE, int grassy = 0, bool alpha = false, int layer = LAYER_TOP)
+{
+ int dim = dimension(orient);
+ int shadowmask = texture==DEFAULT_SKY || alpha ? 0 : calcshadowmask(pos, numverts);
+
+ LightMap *lm = NULL;
+ LightMapTexture *lmtex = NULL;
+ if(lightmaps.inrange(lmid-LMID_RESERVED))
+ {
+ lm = &lightmaps[lmid-LMID_RESERVED];
+ if((lm->type&LM_TYPE)==LM_DIFFUSE ||
+ ((lm->type&LM_TYPE)==LM_BUMPMAP0 &&
+ lightmaps.inrange(lmid+1-LMID_RESERVED) &&
+ (lightmaps[lmid+1-LMID_RESERVED].type&LM_TYPE)==LM_BUMPMAP1))
+ lmtex = &lightmaptexs[lm->tex];
+ else lm = NULL;
+ }
+
+ vec4 sgen, tgen;
+ calctexgen(vslot, dim, sgen, tgen);
+ vertex verts[MAXFACEVERTS];
+ int index[MAXFACEVERTS];
+ loopk(numverts)
+ {
+ vertex &v = verts[k];
+ v.pos = pos[k];
+ v.tc = vec2(sgen.dot(v.pos), tgen.dot(v.pos));
+ if(lmtex)
+ {
+ v.lm = svec2(short(ceil((lm->offsetx + vinfo[k].u*(float(LM_PACKW)/float(USHRT_MAX+1)) + 0.5f) * float(SHRT_MAX)/lmtex->w)),
+ short(ceil((lm->offsety + vinfo[k].v*(float(LM_PACKH)/float(USHRT_MAX+1)) + 0.5f) * float(SHRT_MAX)/lmtex->h)));
+ }
+ else v.lm = svec2(0, 0);
+ if(vinfo && vinfo[k].norm)
+ {
+ vec n = decodenormal(vinfo[k].norm), t = orientation_tangent[vslot.rotation][dim];
+ t.project(n).normalize();
+ v.norm = bvec(n);
+ v.tangent = bvec4(bvec(t), orientation_bitangent[vslot.rotation][dim].scalartriple(n, t) < 0 ? 0 : 255);
+ }
+ else
+ {
+ v.norm = vinfo && vinfo[k].norm && envmap != EMID_NONE ? bvec(decodenormal(vinfo[k].norm)) : bvec(128, 128, 255);
+ v.tangent = bvec4(255, 128, 128, 255);
+ }
+ index[k] = vc.addvert(v);
+ if(index[k] < 0) return;
+ }
+
+ if(texture == DEFAULT_SKY)
+ {
+ loopk(numverts) vc.skyclip = min(vc.skyclip, int(pos[k].z*8)>>3);
+ vc.skymask |= 0x3F&~(1<<orient);
+ }
+
+ if(lmid >= LMID_RESERVED) lmid = lm ? lm->tex : LMID_AMBIENT;
+
+ sortkey key(texture, lmid, !vslot.scroll.iszero() ? dim : 3, layer == LAYER_BLEND ? LAYER_BLEND : LAYER_TOP, envmap, alpha ? (vslot.alphaback ? ALPHA_BACK : (vslot.alphafront ? ALPHA_FRONT : NO_ALPHA)) : NO_ALPHA);
+ addtris(key, orient, verts, index, numverts, convex, shadowmask, tj);
+
+ if(grassy)
+ {
+ for(int i = 0; i < numverts-2; i += 2)
+ {
+ int faces = 0;
+ if(index[0]!=index[i+1] && index[i+1]!=index[i+2] && index[i+2]!=index[0]) faces |= 1;
+ if(i+3 < numverts && index[0]!=index[i+2] && index[i+2]!=index[i+3] && index[i+3]!=index[0]) faces |= 2;
+ if(grassy > 1 && faces==3) addgrasstri(i, verts, 4, texture, lmid);
+ else
+ {
+ if(faces&1) addgrasstri(i, verts, 3, texture, lmid);
+ if(faces&2) addgrasstri(i+1, verts, 3, texture, lmid);
+ }
+ }
+ }
+}
+
+struct edgegroup
+{
+ ivec slope, origin;
+ int axis;
+};
+
+static uint hthash(const edgegroup &g)
+{
+ return g.slope.x^(g.slope.y<<2)^(g.slope.z<<4)^g.origin.x^g.origin.y^g.origin.z;
+}
+
+static bool htcmp(const edgegroup &x, const edgegroup &y)
+{
+ return x.slope==y.slope && x.origin==y.origin;
+}
+
+enum
+{
+ CE_START = 1<<0,
+ CE_END = 1<<1,
+ CE_FLIP = 1<<2,
+ CE_DUP = 1<<3
+};
+
+struct cubeedge
+{
+ cube *c;
+ int next, offset;
+ ushort size;
+ uchar index, flags;
+};
+
+vector<cubeedge> cubeedges;
+hashtable<edgegroup, int> edgegroups(1<<13);
+
+void gencubeedges(cube &c, const ivec &co, int size)
+{
+ ivec pos[MAXFACEVERTS];
+ int vis;
+ loopi(6) if((vis = visibletris(c, i, co, size)))
+ {
+ int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0;
+ if(numverts)
+ {
+ vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts;
+ ivec vo = ivec(co).mask(~0xFFF).shl(3);
+ loopj(numverts)
+ {
+ vertinfo &v = verts[j];
+ pos[j] = ivec(v.x, v.y, v.z).add(vo);
+ }
+ }
+ else if(c.merged&(1<<i)) continue;
+ else
+ {
+ ivec v[4];
+ genfaceverts(c, i, v);
+ int order = vis&4 || (!flataxisface(c, i) && faceconvexity(v) < 0) ? 1 : 0;
+ ivec vo = ivec(co).shl(3);
+ pos[numverts++] = v[order].mul(size).add(vo);
+ if(vis&1) pos[numverts++] = v[order+1].mul(size).add(vo);
+ pos[numverts++] = v[order+2].mul(size).add(vo);
+ if(vis&2) pos[numverts++] = v[(order+3)&3].mul(size).add(vo);
+ }
+ loopj(numverts)
+ {
+ int e1 = j, e2 = j+1 < numverts ? j+1 : 0;
+ ivec d = pos[e2];
+ d.sub(pos[e1]);
+ if(d.iszero()) continue;
+ int axis = abs(d.x) > abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2);
+ if(d[axis] < 0)
+ {
+ d.neg();
+ swap(e1, e2);
+ }
+ reduceslope(d);
+
+ int t1 = pos[e1][axis]/d[axis],
+ t2 = pos[e2][axis]/d[axis];
+ edgegroup g;
+ g.origin = ivec(pos[e1]).sub(ivec(d).mul(t1));
+ g.slope = d;
+ g.axis = axis;
+ cubeedge ce;
+ ce.c = &c;
+ ce.offset = t1;
+ ce.size = t2 - t1;
+ ce.index = i*(MAXFACEVERTS+1)+j;
+ ce.flags = CE_START | CE_END | (e1!=j ? CE_FLIP : 0);
+ ce.next = -1;
+
+ bool insert = true;
+ int *exists = edgegroups.access(g);
+ if(exists)
+ {
+ int prev = -1, cur = *exists;
+ while(cur >= 0)
+ {
+ cubeedge &p = cubeedges[cur];
+ if(ce.offset <= p.offset+p.size)
+ {
+ if(ce.offset < p.offset) break;
+ if(p.flags&CE_DUP ?
+ ce.offset+ce.size <= p.offset+p.size :
+ ce.offset==p.offset && ce.size==p.size)
+ {
+ p.flags |= CE_DUP;
+ insert = false;
+ break;
+ }
+ if(ce.offset == p.offset+p.size) ce.flags &= ~CE_START;
+ }
+ prev = cur;
+ cur = p.next;
+ }
+ if(insert)
+ {
+ ce.next = cur;
+ while(cur >= 0)
+ {
+ cubeedge &p = cubeedges[cur];
+ if(ce.offset+ce.size==p.offset) { ce.flags &= ~CE_END; break; }
+ cur = p.next;
+ }
+ if(prev>=0) cubeedges[prev].next = cubeedges.length();
+ else *exists = cubeedges.length();
+ }
+ }
+ else edgegroups[g] = cubeedges.length();
+
+ if(insert) cubeedges.add(ce);
+ }
+ }
+}
+
+void gencubeedges(cube *c = worldroot, const ivec &co = ivec(0, 0, 0), int size = worldsize>>1)
+{
+ progress("fixing t-joints...");
+ neighbourstack[++neighbourdepth] = c;
+ loopi(8)
+ {
+ ivec o(i, co, size);
+ if(c[i].ext) c[i].ext->tjoints = -1;
+ if(c[i].children) gencubeedges(c[i].children, o, size>>1);
+ else if(!isempty(c[i])) gencubeedges(c[i], o, size);
+ }
+ --neighbourdepth;
+}
+
+void gencubeverts(cube &c, const ivec &co, int size, int csi)
+{
+ if(!(c.visible&0xC0)) return;
+
+ int vismask = ~c.merged & 0x3F;
+ if(!(c.visible&0x80)) vismask &= c.visible;
+ if(!vismask) return;
+
+ int tj = filltjoints && c.ext ? c.ext->tjoints : -1, vis;
+ loopi(6) if(vismask&(1<<i) && (vis = visibletris(c, i, co, size)))
+ {
+ vec pos[MAXFACEVERTS];
+ vertinfo *verts = NULL;
+ int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0, convex = 0;
+ if(numverts)
+ {
+ verts = c.ext->verts() + c.ext->surfaces[i].verts;
+ vec vo(ivec(co).mask(~0xFFF));
+ loopj(numverts) pos[j] = vec(verts[j].getxyz()).mul(1.0f/8).add(vo);
+ if(!flataxisface(c, i)) convex = faceconvexity(verts, numverts, size);
+ }
+ else
+ {
+ ivec v[4];
+ genfaceverts(c, i, v);
+ if(!flataxisface(c, i)) convex = faceconvexity(v);
+ int order = vis&4 || convex < 0 ? 1 : 0;
+ vec vo(co);
+ pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+ if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+ pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+ if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+ }
+
+ VSlot &vslot = lookupvslot(c.texture[i], true),
+ *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, true) : NULL;
+ ushort envmap = vslot.slot->shader->type&SHADER_ENVMAP ? (vslot.slot->texmask&(1<<TEX_ENVMAP) ? EMID_CUSTOM : closestenvmap(i, co, size)) : EMID_NONE,
+ envmap2 = layer && layer->slot->shader->type&SHADER_ENVMAP ? (layer->slot->texmask&(1<<TEX_ENVMAP) ? EMID_CUSTOM : closestenvmap(i, co, size)) : EMID_NONE;
+ while(tj >= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next;
+ int hastj = tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1) ? tj : -1;
+ int grassy = vslot.slot->autograss && i!=O_BOTTOM ? (vis!=3 || convex ? 1 : 2) : 0;
+ if(!c.ext)
+ addcubeverts(vslot, i, size, pos, convex, c.texture[i], LMID_AMBIENT, NULL, numverts, hastj, envmap, grassy, (c.material&MAT_ALPHA)!=0);
+ else
+ {
+ const surfaceinfo &surf = c.ext->surfaces[i];
+ if(!surf.numverts || surf.numverts&LAYER_TOP)
+ addcubeverts(vslot, i, size, pos, convex, c.texture[i], surf.lmid[0], verts, numverts, hastj, envmap, grassy, (c.material&MAT_ALPHA)!=0, LAYER_TOP|(surf.numverts&LAYER_BLEND));
+ if(surf.numverts&LAYER_BOTTOM)
+ addcubeverts(layer ? *layer : vslot, i, size, pos, convex, vslot.layer, surf.lmid[1], surf.numverts&LAYER_DUP ? verts + numverts : verts, numverts, hastj, envmap2);
+ }
+ }
+}
+
+static inline bool skyoccluded(cube &c, int orient)
+{
+ return touchingface(c, orient) && faceedges(c, orient) == F_SOLID;
+}
+
+static int dummyskyfaces[6];
+static inline int hasskyfaces(cube &c, const ivec &co, int size, int faces[6] = dummyskyfaces)
+{
+ int numfaces = 0;
+ if(isempty(c) || c.material&MAT_ALPHA)
+ {
+ if(co.x == 0) faces[numfaces++] = O_LEFT;
+ if(co.x + size == worldsize) faces[numfaces++] = O_RIGHT;
+ if(co.y == 0) faces[numfaces++] = O_BACK;
+ if(co.y + size == worldsize) faces[numfaces++] = O_FRONT;
+ if(co.z == 0) faces[numfaces++] = O_BOTTOM;
+ if(co.z + size == worldsize) faces[numfaces++] = O_TOP;
+ }
+ else if(!isentirelysolid(c))
+ {
+ if(co.x == 0 && !skyoccluded(c, O_LEFT)) faces[numfaces++] = O_LEFT;
+ if(co.x + size == worldsize && !skyoccluded(c, O_RIGHT)) faces[numfaces++] = O_RIGHT;
+ if(co.y == 0 && !skyoccluded(c, O_BACK)) faces[numfaces++] = O_BACK;
+ if(co.y + size == worldsize && !skyoccluded(c, O_FRONT)) faces[numfaces++] = O_FRONT;
+ if(co.z == 0 && !skyoccluded(c, O_BOTTOM)) faces[numfaces++] = O_BOTTOM;
+ if(co.z + size == worldsize && !skyoccluded(c, O_TOP)) faces[numfaces++] = O_TOP;
+ }
+ return numfaces;
+}
+
+void minskyface(cube &cu, int orient, const ivec &co, int size, facebounds &orig)
+{
+ facebounds mincf;
+ mincf.u1 = orig.u2;
+ mincf.u2 = orig.u1;
+ mincf.v1 = orig.v2;
+ mincf.v2 = orig.v1;
+ mincubeface(cu, orient, co, size, orig, mincf, MAT_ALPHA, MAT_ALPHA);
+ orig.u1 = max(mincf.u1, orig.u1);
+ orig.u2 = min(mincf.u2, orig.u2);
+ orig.v1 = max(mincf.v1, orig.v1);
+ orig.v2 = min(mincf.v2, orig.v2);
+}
+
+void genskyfaces(cube &c, const ivec &o, int size)
+{
+ int faces[6], numfaces = hasskyfaces(c, o, size, faces);
+ if(!numfaces) return;
+
+ loopi(numfaces)
+ {
+ int orient = faces[i], dim = dimension(orient);
+ facebounds m;
+ m.u1 = (o[C[dim]]&0xFFF)<<3;
+ m.u2 = m.u1 + (size<<3);
+ m.v1 = (o[R[dim]]&0xFFF)<<3;
+ m.v2 = m.v1 + (size<<3);
+ minskyface(c, orient, o, size, m);
+ if(m.u1 >= m.u2 || m.v1 >= m.v2) continue;
+ vc.skyarea += (int(m.u2-m.u1)*int(m.v2-m.v1) + (1<<(2*3))-1)>>(2*3);
+ vc.skyfaces[orient].add(m);
+ }
+}
+
+void addskyverts(const ivec &o, int size)
+{
+ loopi(6)
+ {
+ int dim = dimension(i), c = C[dim], r = R[dim];
+ vector<facebounds> &sf = vc.skyfaces[i];
+ if(sf.empty()) continue;
+ vc.skymask |= 0x3F&~(1<<opposite(i));
+ sf.setsize(mergefaces(i, sf.getbuf(), sf.length()));
+ loopvj(sf)
+ {
+ facebounds &m = sf[j];
+ int index[4];
+ loopk(4)
+ {
+ const ivec &coords = facecoords[opposite(i)][k];
+ vec v;
+ v[dim] = o[dim];
+ if(coords[dim]) v[dim] += size;
+ v[c] = (o[c]&~0xFFF) + (coords[c] ? m.u2 : m.u1)/8.0f;
+ v[r] = (o[r]&~0xFFF) + (coords[r] ? m.v2 : m.v1)/8.0f;
+ index[k] = vc.addvert(v);
+ if(index[k] < 0) goto nextskyface;
+ vc.skyclip = min(vc.skyclip, int(v.z*8)>>3);
+ }
+ if(vc.skytris + 6 > USHRT_MAX) break;
+ vc.skytris += 6;
+ vc.skyindices.add(index[0]);
+ vc.skyindices.add(index[1]);
+ vc.skyindices.add(index[2]);
+
+ vc.skyindices.add(index[0]);
+ vc.skyindices.add(index[2]);
+ vc.skyindices.add(index[3]);
+ nextskyface:;
+ }
+ }
+}
+
+////////// Vertex Arrays //////////////
+
+int allocva = 0;
+int wtris = 0, wverts = 0, vtris = 0, vverts = 0, glde = 0, gbatches = 0;
+vector<vtxarray *> valist, varoot;
+
+vtxarray *newva(const ivec &co, int size)
+{
+ vc.optimize();
+
+ vtxarray *va = new vtxarray;
+ va->parent = NULL;
+ va->o = co;
+ va->size = size;
+ va->skyarea = vc.skyarea;
+ va->skyfaces = vc.skymask;
+ va->skyclip = vc.skyclip < INT_MAX ? vc.skyclip : INT_MAX;
+ va->curvfc = VFC_NOT_VISIBLE;
+ va->occluded = OCCLUDE_NOTHING;
+ va->query = NULL;
+ va->bbmin = ivec(-1, -1, -1);
+ va->bbmax = ivec(-1, -1, -1);
+ va->hasmerges = 0;
+ va->mergelevel = -1;
+
+ vc.setupdata(va);
+
+ wverts += va->verts;
+ wtris += va->tris + va->blends + va->alphatris;
+ allocva++;
+ valist.add(va);
+
+ return va;
+}
+
+void destroyva(vtxarray *va, bool reparent)
+{
+ wverts -= va->verts;
+ wtris -= va->tris + va->blends + va->alphatris;
+ allocva--;
+ valist.removeobj(va);
+ if(!va->parent) varoot.removeobj(va);
+ if(reparent)
+ {
+ if(va->parent) va->parent->children.removeobj(va);
+ loopv(va->children)
+ {
+ vtxarray *child = va->children[i];
+ child->parent = va->parent;
+ if(child->parent) child->parent->children.add(child);
+ }
+ }
+ if(va->vbuf) destroyvbo(va->vbuf);
+ if(va->ebuf) destroyvbo(va->ebuf);
+ if(va->skybuf) destroyvbo(va->skybuf);
+ if(va->eslist) delete[] va->eslist;
+ if(va->matbuf) delete[] va->matbuf;
+ delete va;
+}
+
+void clearvas(cube *c)
+{
+ loopi(8)
+ {
+ if(c[i].ext)
+ {
+ if(c[i].ext->va) destroyva(c[i].ext->va, false);
+ c[i].ext->va = NULL;
+ c[i].ext->tjoints = -1;
+ }
+ if(c[i].children) clearvas(c[i].children);
+ }
+}
+
+void updatevabb(vtxarray *va, bool force)
+{
+ if(!force && va->bbmin.x >= 0) return;
+
+ va->bbmin = va->geommin;
+ va->bbmax = va->geommax;
+ va->bbmin.min(va->matmin);
+ va->bbmax.max(va->matmax);
+ loopv(va->children)
+ {
+ vtxarray *child = va->children[i];
+ updatevabb(child, force);
+ va->bbmin.min(child->bbmin);
+ va->bbmax.max(child->bbmax);
+ }
+ loopv(va->mapmodels)
+ {
+ octaentities *oe = va->mapmodels[i];
+ va->bbmin.min(oe->bbmin);
+ va->bbmax.max(oe->bbmax);
+ }
+ va->bbmin.max(va->o);
+ va->bbmax.min(ivec(va->o).add(va->size));
+
+ if(va->skyfaces)
+ {
+ va->skyfaces |= 0x80;
+ if(va->sky) loop(dim, 3) if(va->skyfaces&(3<<(2*dim)))
+ {
+ int r = R[dim], c = C[dim];
+ if((va->skyfaces&(1<<(2*dim)) && va->o[dim] < va->bbmin[dim]) ||
+ (va->skyfaces&(2<<(2*dim)) && va->o[dim]+va->size > va->bbmax[dim]) ||
+ va->o[r] < va->bbmin[r] || va->o[r]+va->size > va->bbmax[r] ||
+ va->o[c] < va->bbmin[c] || va->o[c]+va->size > va->bbmax[c])
+ {
+ va->skyfaces &= ~0x80;
+ break;
+ }
+ }
+ }
+}
+
+void updatevabbs(bool force)
+{
+ loopv(varoot) updatevabb(varoot[i], force);
+}
+
+struct mergedface
+{
+ uchar orient, lmid, numverts;
+ ushort mat, tex, envmap;
+ vertinfo *verts;
+ int tjoints;
+};
+
+#define MAXMERGELEVEL 12
+static int vahasmerges = 0, vamergemax = 0;
+static vector<mergedface> vamerges[MAXMERGELEVEL+1];
+
+int genmergedfaces(cube &c, const ivec &co, int size, int minlevel = -1)
+{
+ if(!c.ext || isempty(c)) return -1;
+ int tj = c.ext->tjoints, maxlevel = -1;
+ loopi(6) if(c.merged&(1<<i))
+ {
+ surfaceinfo &surf = c.ext->surfaces[i];
+ int numverts = surf.numverts&MAXFACEVERTS;
+ if(!numverts)
+ {
+ if(minlevel < 0) vahasmerges |= MERGE_PART;
+ continue;
+ }
+ mergedface mf;
+ mf.orient = i;
+ mf.mat = c.material;
+ mf.tex = c.texture[i];
+ mf.envmap = EMID_NONE;
+ mf.lmid = surf.lmid[0];
+ mf.numverts = surf.numverts;
+ mf.verts = c.ext->verts() + surf.verts;
+ mf.tjoints = -1;
+ int level = calcmergedsize(i, co, size, mf.verts, mf.numverts&MAXFACEVERTS);
+ if(level > minlevel)
+ {
+ maxlevel = max(maxlevel, level);
+
+ while(tj >= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next;
+ if(tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1)) mf.tjoints = tj;
+
+ VSlot &vslot = lookupvslot(mf.tex, true),
+ *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, true) : NULL;
+ if(vslot.slot->shader->type&SHADER_ENVMAP)
+ mf.envmap = vslot.slot->texmask&(1<<TEX_ENVMAP) ? EMID_CUSTOM : closestenvmap(i, co, size);
+ ushort envmap2 = layer && layer->slot->shader->type&SHADER_ENVMAP ? (layer->slot->texmask&(1<<TEX_ENVMAP) ? EMID_CUSTOM : closestenvmap(i, co, size)) : EMID_NONE;
+
+ if(surf.numverts&LAYER_TOP) vamerges[level].add(mf);
+ if(surf.numverts&LAYER_BOTTOM)
+ {
+ mf.tex = vslot.layer;
+ mf.envmap = envmap2;
+ mf.lmid = surf.lmid[1];
+ mf.numverts &= ~LAYER_TOP;
+ if(surf.numverts&LAYER_DUP) mf.verts += numverts;
+ vamerges[level].add(mf);
+ }
+ }
+ }
+ if(maxlevel >= 0)
+ {
+ vamergemax = max(vamergemax, maxlevel);
+ vahasmerges |= MERGE_ORIGIN;
+ }
+ return maxlevel;
+}
+
+int findmergedfaces(cube &c, const ivec &co, int size, int csi, int minlevel)
+{
+ if(c.ext && c.ext->va && !(c.ext->va->hasmerges&MERGE_ORIGIN)) return c.ext->va->mergelevel;
+ else if(c.children)
+ {
+ int maxlevel = -1;
+ loopi(8)
+ {
+ ivec o(i, co, size/2);
+ int level = findmergedfaces(c.children[i], o, size/2, csi-1, minlevel);
+ maxlevel = max(maxlevel, level);
+ }
+ return maxlevel;
+ }
+ else if(c.ext && c.merged) return genmergedfaces(c, co, size, minlevel);
+ else return -1;
+}
+
+void addmergedverts(int level, const ivec &o)
+{
+ vector<mergedface> &mfl = vamerges[level];
+ if(mfl.empty()) return;
+ vec vo(ivec(o).mask(~0xFFF));
+ vec pos[MAXFACEVERTS];
+ loopv(mfl)
+ {
+ mergedface &mf = mfl[i];
+ int numverts = mf.numverts&MAXFACEVERTS;
+ loopi(numverts)
+ {
+ vertinfo &v = mf.verts[i];
+ pos[i] = vec(v.x, v.y, v.z).mul(1.0f/8).add(vo);
+ }
+ VSlot &vslot = lookupvslot(mf.tex, true);
+ int grassy = vslot.slot->autograss && mf.orient!=O_BOTTOM && mf.numverts&LAYER_TOP ? 2 : 0;
+ addcubeverts(vslot, mf.orient, 1<<level, pos, 0, mf.tex, mf.lmid, mf.verts, numverts, mf.tjoints, mf.envmap, grassy, (mf.mat&MAT_ALPHA)!=0, mf.numverts&LAYER_BLEND);
+ vahasmerges |= MERGE_USE;
+ }
+ mfl.setsize(0);
+}
+
+void rendercube(cube &c, const ivec &co, int size, int csi, int &maxlevel) // creates vertices and indices ready to be put into a va
+{
+ //if(size<=16) return;
+ if(c.ext && c.ext->va)
+ {
+ maxlevel = max(maxlevel, c.ext->va->mergelevel);
+ return; // don't re-render
+ }
+
+ if(c.children)
+ {
+ neighbourstack[++neighbourdepth] = c.children;
+ c.escaped = 0;
+ loopi(8)
+ {
+ ivec o(i, co, size/2);
+ int level = -1;
+ rendercube(c.children[i], o, size/2, csi-1, level);
+ if(level >= csi)
+ c.escaped |= 1<<i;
+ maxlevel = max(maxlevel, level);
+ }
+ --neighbourdepth;
+
+ if(csi <= MAXMERGELEVEL && vamerges[csi].length()) addmergedverts(csi, co);
+
+ if(c.ext)
+ {
+ if(c.ext->ents && c.ext->ents->mapmodels.length()) vc.mapmodels.add(c.ext->ents);
+ }
+ return;
+ }
+
+ genskyfaces(c, co, size);
+
+ if(!isempty(c))
+ {
+ gencubeverts(c, co, size, csi);
+ if(c.merged) maxlevel = max(maxlevel, genmergedfaces(c, co, size));
+ }
+ if(c.material != MAT_AIR) genmatsurfs(c, co, size, vc.matsurfs);
+
+ if(c.ext)
+ {
+ if(c.ext->ents && c.ext->ents->mapmodels.length()) vc.mapmodels.add(c.ext->ents);
+ }
+
+ if(csi <= MAXMERGELEVEL && vamerges[csi].length()) addmergedverts(csi, co);
+}
+
+void calcgeombb(const ivec &co, int size, ivec &bbmin, ivec &bbmax)
+{
+ vec vmin(co), vmax = vmin;
+ vmin.add(size);
+
+ loopv(vc.verts)
+ {
+ const vec &v = vc.verts[i].pos;
+ vmin.min(v);
+ vmax.max(v);
+ }
+
+ bbmin = ivec(vmin.mul(8)).shr(3);
+ bbmax = ivec(vmax.mul(8)).add(7).shr(3);
+}
+
+void calcmatbb(const ivec &co, int size, ivec &bbmin, ivec &bbmax)
+{
+ bbmax = co;
+ (bbmin = bbmax).add(size);
+ loopv(vc.matsurfs)
+ {
+ materialsurface &m = vc.matsurfs[i];
+ switch(m.material&MATF_VOLUME)
+ {
+ case MAT_WATER:
+ case MAT_GLASS:
+ case MAT_LAVA:
+ break;
+
+ default:
+ continue;
+ }
+
+ int dim = dimension(m.orient),
+ r = R[dim],
+ c = C[dim];
+ bbmin[dim] = min(bbmin[dim], m.o[dim]);
+ bbmax[dim] = max(bbmax[dim], m.o[dim]);
+
+ bbmin[r] = min(bbmin[r], m.o[r]);
+ bbmax[r] = max(bbmax[r], m.o[r] + m.rsize);
+
+ bbmin[c] = min(bbmin[c], m.o[c]);
+ bbmax[c] = max(bbmax[c], m.o[c] + m.csize);
+ }
+}
+
+void setva(cube &c, const ivec &co, int size, int csi)
+{
+ ASSERT(size <= 0x1000);
+
+ int vamergeoffset[MAXMERGELEVEL+1];
+ loopi(MAXMERGELEVEL+1) vamergeoffset[i] = vamerges[i].length();
+
+ vc.origin = co;
+ vc.size = size;
+
+ shadowmapmin = vec(co).add(size);
+ shadowmapmax = vec(co);
+
+ int maxlevel = -1;
+ rendercube(c, co, size, csi, maxlevel);
+
+ ivec bbmin, bbmax;
+
+ calcgeombb(co, size, bbmin, bbmax);
+
+ addskyverts(co, size);
+
+ if(size == min(0x1000, worldsize/2) || !vc.emptyva())
+ {
+ vtxarray *va = newva(co, size);
+ ext(c).va = va;
+ va->geommin = bbmin;
+ va->geommax = bbmax;
+ calcmatbb(co, size, va->matmin, va->matmax);
+ va->shadowmapmin = ivec(shadowmapmin.mul(8)).shr(3);
+ va->shadowmapmax = ivec(shadowmapmax.mul(8)).add(7).shr(3);
+ va->hasmerges = vahasmerges;
+ va->mergelevel = vamergemax;
+ }
+ else
+ {
+ loopi(MAXMERGELEVEL+1) vamerges[i].setsize(vamergeoffset[i]);
+ }
+
+ vc.clear();
+}
+
+static inline int setcubevisibility(cube &c, const ivec &co, int size)
+{
+ int numvis = 0, vismask = 0, collidemask = 0, checkmask = 0;
+ loopi(6)
+ {
+ int facemask = classifyface(c, i, co, size);
+ if(facemask&1)
+ {
+ vismask |= 1<<i;
+ if(c.merged&(1<<i))
+ {
+ if(c.ext && c.ext->surfaces[i].numverts&MAXFACEVERTS) numvis++;
+ }
+ else
+ {
+ numvis++;
+ if(c.texture[i] != DEFAULT_SKY && !(c.ext && c.ext->surfaces[i].numverts&MAXFACEVERTS)) checkmask |= 1<<i;
+ }
+ }
+ if(facemask&2 && collideface(c, i)) collidemask |= 1<<i;
+ }
+ c.visible = collidemask | (vismask ? (vismask != collidemask ? (checkmask ? 0x80|0x40 : 0x80) : 0x40) : 0);
+ return numvis;
+}
+
+VARF(vafacemax, 64, 384, 256*256, allchanged());
+VARF(vafacemin, 0, 96, 256*256, allchanged());
+VARF(vacubesize, 32, 128, 0x1000, allchanged());
+
+int updateva(cube *c, const ivec &co, int size, int csi)
+{
+ progress("recalculating geometry...");
+ int ccount = 0, cmergemax = vamergemax, chasmerges = vahasmerges;
+ neighbourstack[++neighbourdepth] = c;
+ loopi(8) // counting number of semi-solid/solid children cubes
+ {
+ int count = 0, childpos = varoot.length();
+ ivec o(i, co, size);
+ vamergemax = 0;
+ vahasmerges = 0;
+ if(c[i].ext && c[i].ext->va)
+ {
+ varoot.add(c[i].ext->va);
+ if(c[i].ext->va->hasmerges&MERGE_ORIGIN) findmergedfaces(c[i], o, size, csi, csi);
+ }
+ else
+ {
+ if(c[i].children) count += updateva(c[i].children, o, size/2, csi-1);
+ else
+ {
+ if(!isempty(c[i])) count += setcubevisibility(c[i], o, size);
+ count += hasskyfaces(c[i], o, size);
+ }
+ int tcount = count + (csi <= MAXMERGELEVEL ? vamerges[csi].length() : 0);
+ if(tcount > vafacemax || (tcount >= vafacemin && size >= vacubesize) || size == min(0x1000, worldsize/2))
+ {
+ loadprogress = clamp(recalcprogress/float(allocnodes), 0.0f, 1.0f);
+ setva(c[i], o, size, csi);
+ if(c[i].ext && c[i].ext->va)
+ {
+ while(varoot.length() > childpos)
+ {
+ vtxarray *child = varoot.pop();
+ c[i].ext->va->children.add(child);
+ child->parent = c[i].ext->va;
+ }
+ varoot.add(c[i].ext->va);
+ if(vamergemax > size)
+ {
+ cmergemax = max(cmergemax, vamergemax);
+ chasmerges |= vahasmerges&~MERGE_USE;
+ }
+ continue;
+ }
+ else count = 0;
+ }
+ }
+ if(csi+1 <= MAXMERGELEVEL && vamerges[csi].length()) vamerges[csi+1].move(vamerges[csi]);
+ cmergemax = max(cmergemax, vamergemax);
+ chasmerges |= vahasmerges;
+ ccount += count;
+ }
+ --neighbourdepth;
+ vamergemax = cmergemax;
+ vahasmerges = chasmerges;
+
+ return ccount;
+}
+
+void addtjoint(const edgegroup &g, const cubeedge &e, int offset)
+{
+ int vcoord = (g.slope[g.axis]*offset + g.origin[g.axis]) & 0x7FFF;
+ tjoint &tj = tjoints.add();
+ tj.offset = vcoord / g.slope[g.axis];
+ tj.edge = e.index;
+
+ int prev = -1, cur = ext(*e.c).tjoints;
+ while(cur >= 0)
+ {
+ tjoint &o = tjoints[cur];
+ if(tj.edge < o.edge || (tj.edge==o.edge && (e.flags&CE_FLIP ? tj.offset > o.offset : tj.offset < o.offset))) break;
+ prev = cur;
+ cur = o.next;
+ }
+
+ tj.next = cur;
+ if(prev < 0) e.c->ext->tjoints = tjoints.length()-1;
+ else tjoints[prev].next = tjoints.length()-1;
+}
+
+void findtjoints(int cur, const edgegroup &g)
+{
+ int active = -1;
+ while(cur >= 0)
+ {
+ cubeedge &e = cubeedges[cur];
+ int prevactive = -1, curactive = active;
+ while(curactive >= 0)
+ {
+ cubeedge &a = cubeedges[curactive];
+ if(a.offset+a.size <= e.offset)
+ {
+ if(prevactive >= 0) cubeedges[prevactive].next = a.next;
+ else active = a.next;
+ }
+ else
+ {
+ prevactive = curactive;
+ if(!(a.flags&CE_DUP))
+ {
+ if(e.flags&CE_START && e.offset > a.offset && e.offset < a.offset+a.size)
+ addtjoint(g, a, e.offset);
+ if(e.flags&CE_END && e.offset+e.size > a.offset && e.offset+e.size < a.offset+a.size)
+ addtjoint(g, a, e.offset+e.size);
+ }
+ if(!(e.flags&CE_DUP))
+ {
+ if(a.flags&CE_START && a.offset > e.offset && a.offset < e.offset+e.size)
+ addtjoint(g, e, a.offset);
+ if(a.flags&CE_END && a.offset+a.size > e.offset && a.offset+a.size < e.offset+e.size)
+ addtjoint(g, e, a.offset+a.size);
+ }
+ }
+ curactive = a.next;
+ }
+ int next = e.next;
+ e.next = active;
+ active = cur;
+ cur = next;
+ }
+}
+
+void findtjoints()
+{
+ recalcprogress = 0;
+ gencubeedges();
+ tjoints.setsize(0);
+ enumeratekt(edgegroups, edgegroup, g, int, e, findtjoints(e, g));
+ cubeedges.setsize(0);
+ edgegroups.clear();
+}
+
+void octarender() // creates va s for all leaf cubes that don't already have them
+{
+ int csi = 0;
+ while(1<<csi < worldsize) csi++;
+
+ recalcprogress = 0;
+ varoot.setsize(0);
+ updateva(worldroot, ivec(0, 0, 0), worldsize/2, csi-1);
+ loadprogress = 0;
+ flushvbo();
+
+ explicitsky = 0;
+ skyarea = 0;
+ loopv(valist)
+ {
+ vtxarray *va = valist[i];
+ explicitsky += va->explicitsky;
+ skyarea += va->skyarea;
+ }
+
+ visibleva = NULL;
+}
+
+void precachetextures()
+{
+ vector<int> texs;
+ loopv(valist)
+ {
+ vtxarray *va = valist[i];
+ loopj(va->texs + va->blends) if(texs.find(va->eslist[j].texture) < 0) texs.add(va->eslist[j].texture);
+ }
+ loopv(texs)
+ {
+ loadprogress = float(i+1)/texs.length();
+ lookupvslot(texs[i]);
+ }
+ loadprogress = 0;
+}
+
+void allchanged(bool load)
+{
+ renderprogress(0, "clearing vertex arrays...");
+ clearvas(worldroot);
+ resetqueries();
+ resetclipplanes();
+ if(load)
+ {
+ setupsky();
+ initenvmaps();
+ }
+ guessshadowdir();
+ entitiesinoctanodes();
+ tjoints.setsize(0);
+ if(filltjoints) findtjoints();
+ octarender();
+ if(load) precachetextures();
+ setupmaterials();
+ invalidatepostfx();
+ updatevabbs(true);
+ resetblobs();
+ lightents();
+ if(load)
+ {
+ seedparticles();
+ drawtextures();
+ }
+}
+
+void recalc()
+{
+ allchanged(true);
+}
+
+COMMAND(recalc, "");
+
diff --git a/src/engine/physics.cpp b/src/engine/physics.cpp
new file mode 100644
index 0000000..6e78863
--- /dev/null
+++ b/src/engine/physics.cpp
@@ -0,0 +1,2057 @@
+// physics.cpp: no physics books were hurt nor consulted in the construction of this code.
+// All physics computations and constants were invented on the fly and simply tweaked until
+// they "felt right", and have no basis in reality. Collision detection is simplistic but
+// very robust (uses discrete steps at fixed fps).
+
+#include "engine.h"
+#include "mpr.h"
+
+const int MAXCLIPPLANES = 1024;
+static clipplanes clipcache[MAXCLIPPLANES];
+static int clipcacheversion = -2;
+
+static inline clipplanes &getclipplanes(const cube &c, const ivec &o, int size, bool collide = true, int offset = 0)
+{
+ clipplanes &p = clipcache[int(&c - worldroot)&(MAXCLIPPLANES-1)];
+ if(p.owner != &c || p.version != clipcacheversion+offset)
+ {
+ p.owner = &c;
+ p.version = clipcacheversion+offset;
+ genclipplanes(c, o, size, p, collide);
+ }
+ return p;
+}
+
+void resetclipplanes()
+{
+ clipcacheversion += 2;
+ if(!clipcacheversion)
+ {
+ memclear(clipcache);
+ clipcacheversion = 2;
+ }
+}
+
+///////////////////////// ray - cube collision ///////////////////////////////////////////////
+
+#define INTERSECTPLANES(setentry, exit) \
+ float enterdist = -1e16f, exitdist = 1e16f; \
+ loopi(p.size) \
+ { \
+ float pdist = p.p[i].dist(v), facing = ray.dot(p.p[i]); \
+ if(facing < 0) \
+ { \
+ pdist /= -facing; \
+ if(pdist > enterdist) \
+ { \
+ if(pdist > exitdist) exit; \
+ enterdist = pdist; \
+ setentry; \
+ } \
+ } \
+ else if(facing > 0) \
+ { \
+ pdist /= -facing; \
+ if(pdist < exitdist) \
+ { \
+ if(pdist < enterdist) exit; \
+ exitdist = pdist; \
+ } \
+ } \
+ else if(pdist > 0) exit; \
+ }
+
+#define INTERSECTBOX(setentry, exit) \
+ loop(i, 3) \
+ { \
+ if(ray[i]) \
+ { \
+ float prad = fabs(p.r[i] * invray[i]), pdist = (p.o[i] - v[i]) * invray[i], pmin = pdist - prad, pmax = pdist + prad; \
+ if(pmin > enterdist) \
+ { \
+ if(pmin > exitdist) exit; \
+ enterdist = pmin; \
+ setentry; \
+ } \
+ if(pmax < exitdist) \
+ { \
+ if(pmax < enterdist) exit; \
+ exitdist = pmax; \
+ } \
+ } \
+ else if(v[i] < p.o[i]-p.r[i] || v[i] > p.o[i]+p.r[i]) exit; \
+ }
+
+vec hitsurface;
+
+static inline bool raycubeintersect(const clipplanes &p, const cube &c, const vec &v, const vec &ray, const vec &invray, float &dist)
+{
+ int entry = -1, bbentry = -1;
+ INTERSECTPLANES(entry = i, return false);
+ INTERSECTBOX(bbentry = i, return false);
+ if(exitdist < 0) return false;
+ dist = max(enterdist+0.1f, 0.0f);
+ if(bbentry>=0) { hitsurface = vec(0, 0, 0); hitsurface[bbentry] = ray[bbentry]>0 ? -1 : 1; }
+ else hitsurface = p.p[entry];
+ return true;
+}
+
+extern void entselectionbox(const entity &e, vec &eo, vec &es);
+float hitentdist;
+int hitent, hitorient;
+
+static float disttoent(octaentities *oc, const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+ vec eo, es;
+ int orient = -1;
+ float dist = radius, f = 0.0f;
+ const vector<extentity *> &ents = entities::getents();
+
+ #define entintersect(mask, type, func) {\
+ if((mode&(mask))==(mask)) loopv(oc->type) \
+ { \
+ extentity &e = *ents[oc->type[i]]; \
+ if(!(e.flags&EF_OCTA) || &e==t) continue; \
+ func; \
+ if(f<dist && f>0 && vec(ray).mul(f).add(o).insidebb(oc->o, oc->size)) \
+ { \
+ hitentdist = dist = f; \
+ hitent = oc->type[i]; \
+ hitorient = orient; \
+ } \
+ } \
+ }
+
+ entintersect(RAY_POLY, mapmodels,
+ if(!mmintersect(e, o, ray, radius, mode, f)) continue;
+ );
+
+ entintersect(RAY_ENTS, other,
+ entselectionbox(e, eo, es);
+ if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
+ );
+
+ entintersect(RAY_ENTS, mapmodels,
+ entselectionbox(e, eo, es);
+ if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
+ );
+
+ return dist;
+}
+
+static float disttooutsideent(const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+ vec eo, es;
+ int orient;
+ float dist = radius, f = 0.0f;
+ const vector<extentity *> &ents = entities::getents();
+ loopv(outsideents)
+ {
+ extentity &e = *ents[outsideents[i]];
+ if(!(e.flags&EF_OCTA) || &e==t) continue;
+ entselectionbox(e, eo, es);
+ if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
+ if(f<dist && f>0)
+ {
+ hitentdist = dist = f;
+ hitent = outsideents[i];
+ hitorient = orient;
+ }
+ }
+ return dist;
+}
+
+// optimized shadow version
+static float shadowent(octaentities *oc, const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+ float dist = radius, f = 0.0f;
+ const vector<extentity *> &ents = entities::getents();
+ loopv(oc->mapmodels)
+ {
+ extentity &e = *ents[oc->mapmodels[i]];
+ if(!(e.flags&EF_OCTA) || &e==t) continue;
+ if(!mmintersect(e, o, ray, radius, mode, f)) continue;
+ if(f>0 && f<dist) dist = f;
+ }
+ return dist;
+}
+
+#define INITRAYCUBE \
+ float dist = 0, dent = radius > 0 ? radius : 1e16f; \
+ vec v(o), invray(ray.x ? 1/ray.x : 1e16f, ray.y ? 1/ray.y : 1e16f, ray.z ? 1/ray.z : 1e16f); \
+ cube *levels[20]; \
+ levels[worldscale] = worldroot; \
+ int lshift = worldscale, elvl = mode&RAY_BB ? worldscale : 0; \
+ ivec lsizemask(invray.x>0 ? 1 : 0, invray.y>0 ? 1 : 0, invray.z>0 ? 1 : 0); \
+
+#define CHECKINSIDEWORLD \
+ if(!insideworld(o)) \
+ { \
+ float disttoworld = 0, exitworld = 1e16f; \
+ loopi(3) \
+ { \
+ float c = v[i]; \
+ if(c<0 || c>=worldsize) \
+ { \
+ float d = ((invray[i]>0?0:worldsize)-c)*invray[i]; \
+ if(d<0) return (radius>0?radius:-1); \
+ disttoworld = max(disttoworld, 0.1f + d); \
+ } \
+ float e = ((invray[i]>0?worldsize:0)-c)*invray[i]; \
+ exitworld = min(exitworld, e); \
+ } \
+ if(disttoworld > exitworld) return (radius>0?radius:-1); \
+ v.add(vec(ray).mul(disttoworld)); \
+ dist += disttoworld; \
+ }
+
+#define DOWNOCTREE(disttoent, earlyexit) \
+ cube *lc = levels[lshift]; \
+ for(;;) \
+ { \
+ lshift--; \
+ lc += octastep(x, y, z, lshift); \
+ if(lc->ext && lc->ext->ents && lshift < elvl) \
+ { \
+ float edist = disttoent(lc->ext->ents, o, ray, dent, mode, t); \
+ if(edist < dent) \
+ { \
+ earlyexit return min(edist, dist); \
+ elvl = lshift; \
+ dent = min(dent, edist); \
+ } \
+ } \
+ if(lc->children==NULL) break; \
+ lc = lc->children; \
+ levels[lshift] = lc; \
+ }
+
+#define FINDCLOSEST(xclosest, yclosest, zclosest) \
+ float dx = (lo.x+(lsizemask.x<<lshift)-v.x)*invray.x, \
+ dy = (lo.y+(lsizemask.y<<lshift)-v.y)*invray.y, \
+ dz = (lo.z+(lsizemask.z<<lshift)-v.z)*invray.z; \
+ float disttonext = dx; \
+ xclosest; \
+ if(dy < disttonext) { disttonext = dy; yclosest; } \
+ if(dz < disttonext) { disttonext = dz; zclosest; } \
+ disttonext += 0.1f; \
+ v.add(vec(ray).mul(disttonext)); \
+ dist += disttonext;
+
+#define UPOCTREE(exitworld) \
+ x = int(v.x); \
+ y = int(v.y); \
+ z = int(v.z); \
+ uint diff = uint(lo.x^x)|uint(lo.y^y)|uint(lo.z^z); \
+ if(diff >= uint(worldsize)) exitworld; \
+ diff >>= lshift; \
+ if(!diff) exitworld; \
+ do \
+ { \
+ lshift++; \
+ diff >>= 1; \
+ } while(diff);
+
+float raycube(const vec &o, const vec &ray, float radius, int mode, int size, extentity *t)
+{
+ if(ray.iszero()) return 0;
+
+ INITRAYCUBE;
+ CHECKINSIDEWORLD;
+
+ int closest = -1, x = int(v.x), y = int(v.y), z = int(v.z);
+ for(;;)
+ {
+ DOWNOCTREE(disttoent, if(mode&RAY_SHADOW));
+
+ int lsize = 1<<lshift;
+
+ cube &c = *lc;
+ if((dist>0 || !(mode&RAY_SKIPFIRST)) &&
+ (((mode&RAY_CLIPMAT) && isclipped(c.material&MATF_VOLUME)) ||
+ ((mode&RAY_EDITMAT) && c.material != MAT_AIR) ||
+ (!(mode&RAY_PASS) && lsize==size && !isempty(c)) ||
+ isentirelysolid(c) ||
+ dent < dist))
+ {
+ if(closest >= 0) { hitsurface = vec(0, 0, 0); hitsurface[closest] = ray[closest]>0 ? -1 : 1; }
+ return min(dent, dist);
+ }
+
+ ivec lo(x&(~0U<<lshift), y&(~0U<<lshift), z&(~0U<<lshift));
+
+ if(!isempty(c))
+ {
+ const clipplanes &p = getclipplanes(c, lo, lsize, false, 1);
+ float f = 0;
+ if(raycubeintersect(p, c, v, ray, invray, f) && (dist+f>0 || !(mode&RAY_SKIPFIRST)))
+ return min(dent, dist+f);
+ }
+
+ FINDCLOSEST(closest = 0, closest = 1, closest = 2);
+
+ if(radius>0 && dist>=radius) return min(dent, dist);
+
+ UPOCTREE(return min(dent, radius>0 ? radius : dist));
+ }
+}
+
+// optimized version for lightmap shadowing... every cycle here counts!!!
+float shadowray(const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+ INITRAYCUBE;
+ CHECKINSIDEWORLD;
+
+ int side = O_BOTTOM, x = int(v.x), y = int(v.y), z = int(v.z);
+ for(;;)
+ {
+ DOWNOCTREE(shadowent, );
+
+ cube &c = *lc;
+ ivec lo(x&(~0U<<lshift), y&(~0U<<lshift), z&(~0U<<lshift));
+
+ if(!isempty(c) && !(c.material&MAT_ALPHA))
+ {
+ if(isentirelysolid(c))
+ {
+ if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY)
+ {
+ if(mode&RAY_SKYTEX) return radius;
+ }
+ else return dist;
+ }
+ else
+ {
+ const clipplanes &p = getclipplanes(c, lo, 1<<lshift, false, 1);
+ INTERSECTPLANES(side = p.side[i], goto nextcube);
+ INTERSECTBOX(side = (i<<1) + 1 - lsizemask[i], goto nextcube);
+ if(exitdist >= 0)
+ {
+ if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY)
+ {
+ if(mode&RAY_SKYTEX) return radius;
+ }
+ else return dist+max(enterdist+0.1f, 0.0f);
+ }
+ }
+ }
+
+ nextcube:
+ FINDCLOSEST(side = O_RIGHT - lsizemask.x, side = O_FRONT - lsizemask.y, side = O_TOP - lsizemask.z);
+
+ if(dist>=radius) return dist;
+
+ UPOCTREE(return radius);
+ }
+}
+
+// thread safe version
+
+struct ShadowRayCache
+{
+ clipplanes clipcache[MAXCLIPPLANES];
+ int version;
+
+ ShadowRayCache() : version(-1) {}
+};
+
+ShadowRayCache *newshadowraycache() { return new ShadowRayCache; }
+
+void freeshadowraycache(ShadowRayCache *&cache) { delete cache; cache = NULL; }
+
+void resetshadowraycache(ShadowRayCache *cache)
+{
+ cache->version++;
+ if(!cache->version)
+ {
+ memclear(cache->clipcache);
+ cache->version = 1;
+ }
+}
+
+float shadowray(ShadowRayCache *cache, const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+ INITRAYCUBE;
+ CHECKINSIDEWORLD;
+
+ int side = O_BOTTOM, x = int(v.x), y = int(v.y), z = int(v.z);
+ for(;;)
+ {
+ DOWNOCTREE(shadowent, );
+
+ cube &c = *lc;
+ ivec lo(x&(~0U<<lshift), y&(~0U<<lshift), z&(~0U<<lshift));
+
+ if(!isempty(c) && !(c.material&MAT_ALPHA))
+ {
+ if(isentirelysolid(c))
+ {
+ if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY)
+ {
+ if(mode&RAY_SKYTEX) return radius;
+ }
+ else return dist;
+ }
+ else
+ {
+ clipplanes &p = cache->clipcache[int(&c - worldroot)&(MAXCLIPPLANES-1)];
+ if(p.owner != &c || p.version != cache->version) { p.owner = &c; p.version = cache->version; genclipplanes(c, lo, 1<<lshift, p, false); }
+ INTERSECTPLANES(side = p.side[i], goto nextcube);
+ INTERSECTBOX(side = (i<<1) + 1 - lsizemask[i], goto nextcube);
+ if(exitdist >= 0)
+ {
+ if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY)
+ {
+ if(mode&RAY_SKYTEX) return radius;
+ }
+ else return dist+max(enterdist+0.1f, 0.0f);
+ }
+ }
+ }
+
+ nextcube:
+ FINDCLOSEST(side = O_RIGHT - lsizemask.x, side = O_FRONT - lsizemask.y, side = O_TOP - lsizemask.z);
+
+ if(dist>=radius) return dist;
+
+ UPOCTREE(return radius);
+ }
+}
+
+float rayent(const vec &o, const vec &ray, float radius, int mode, int size, int &orient, int &ent)
+{
+ hitent = -1;
+ hitentdist = radius;
+ hitorient = -1;
+ float dist = raycube(o, ray, radius, mode, size);
+ if((mode&RAY_ENTS) == RAY_ENTS)
+ {
+ float dent = disttooutsideent(o, ray, dist < 0 ? 1e16f : dist, mode, NULL);
+ if(dent < 1e15f && (dist < 0 || dent < dist)) dist = dent;
+ }
+ orient = hitorient;
+ ent = hitentdist == dist ? hitent : -1;
+ return dist;
+}
+
+float raycubepos(const vec &o, const vec &ray, vec &hitpos, float radius, int mode, int size)
+{
+ hitpos = ray;
+ float dist = raycube(o, ray, radius, mode, size);
+ if(radius>0 && dist>=radius) dist = radius;
+ hitpos.mul(dist).add(o);
+ return dist;
+}
+
+bool raycubelos(const vec &o, const vec &dest, vec &hitpos)
+{
+ vec ray(dest);
+ ray.sub(o);
+ float mag = ray.magnitude();
+ ray.mul(1/mag);
+ float distance = raycubepos(o, ray, hitpos, mag, RAY_CLIPMAT|RAY_POLY);
+ return distance >= mag;
+}
+
+float rayfloor(const vec &o, vec &floor, int mode, float radius)
+{
+ if(o.z<=0) return -1;
+ hitsurface = vec(0, 0, 1);
+ float dist = raycube(o, vec(0, 0, -1), radius, mode);
+ if(dist<0 || (radius>0 && dist>=radius)) return dist;
+ floor = hitsurface;
+ return dist;
+}
+
+///////////////////////// entity collision ///////////////////////////////////////////////
+
+// info about collisions
+int collideinside; // whether an internal collision happened
+physent *collideplayer; // whether the collection hit a player
+vec collidewall; // just the normal vectors.
+
+const float STAIRHEIGHT = 4.1f;
+const float FLOORZ = 0.867f;
+const float SLOPEZ = 0.5f;
+const float WALLZ = 0.2f;
+extern const float JUMPVEL = 125.0f;
+extern const float GRAVITY = 200.0f;
+
+bool ellipseboxcollide(physent *d, const vec &dir, const vec &o, const vec &center, float yaw, float xr, float yr, float hi, float lo)
+{
+ float below = (o.z+center.z-lo) - (d->o.z+d->aboveeye),
+ above = (d->o.z-d->eyeheight) - (o.z+center.z+hi);
+ if(below>=0 || above>=0) return false;
+
+ vec yo(d->o);
+ yo.sub(o);
+ yo.rotate_around_z(-yaw*RAD);
+ yo.sub(center);
+
+ float dx = clamp(yo.x, -xr, xr) - yo.x, dy = clamp(yo.y, -yr, yr) - yo.y,
+ dist = sqrtf(dx*dx + dy*dy) - d->radius;
+ if(dist < 0)
+ {
+ int sx = yo.x <= -xr ? -1 : (yo.x >= xr ? 1 : 0),
+ sy = yo.y <= -yr ? -1 : (yo.y >= yr ? 1 : 0);
+ if(dist > (yo.z < 0 ? below : above) && (sx || sy))
+ {
+ vec ydir(dir);
+ ydir.rotate_around_z(-yaw*RAD);
+ if(sx*yo.x - xr > sy*yo.y - yr)
+ {
+ if(dir.iszero() || sx*ydir.x < -1e-6f)
+ {
+ collidewall = vec(sx, 0, 0);
+ collidewall.rotate_around_z(yaw*RAD);
+ return true;
+ }
+ }
+ else if(dir.iszero() || sy*ydir.y < -1e-6f)
+ {
+ collidewall = vec(0, sy, 0);
+ collidewall.rotate_around_z(yaw*RAD);
+ return true;
+ }
+ }
+ if(yo.z < 0)
+ {
+ if(dir.iszero() || (dir.z > 0 && (d->type>=ENT_INANIMATE || below >= d->zmargin-(d->eyeheight+d->aboveeye)/4.0f)))
+ {
+ collidewall = vec(0, 0, -1);
+ return true;
+ }
+ }
+ else if(dir.iszero() || (dir.z < 0 && (d->type>=ENT_INANIMATE || above >= d->zmargin-(d->eyeheight+d->aboveeye)/3.0f)))
+ {
+ collidewall = vec(0, 0, 1);
+ return true;
+ }
+ collideinside++;
+ }
+ return false;
+}
+
+bool ellipsecollide(physent *d, const vec &dir, const vec &o, const vec &center, float yaw, float xr, float yr, float hi, float lo)
+{
+ float below = (o.z+center.z-lo) - (d->o.z+d->aboveeye),
+ above = (d->o.z-d->eyeheight) - (o.z+center.z+hi);
+ if(below>=0 || above>=0) return false;
+ vec yo(center);
+ yo.rotate_around_z(yaw*RAD);
+ yo.add(o);
+ float x = yo.x - d->o.x, y = yo.y - d->o.y;
+ float angle = atan2f(y, x), dangle = angle-(d->yaw+90)*RAD, eangle = angle-(yaw+90)*RAD;
+ float dx = d->xradius*cosf(dangle), dy = d->yradius*sinf(dangle);
+ float ex = xr*cosf(eangle), ey = yr*sinf(eangle);
+ float dist = sqrtf(x*x + y*y) - sqrtf(dx*dx + dy*dy) - sqrtf(ex*ex + ey*ey);
+ if(dist < 0)
+ {
+ if(dist > (d->o.z < yo.z ? below : above) && (dir.iszero() || x*dir.x + y*dir.y > 0))
+ {
+ collidewall = vec(-x, -y, 0);
+ if(!collidewall.iszero()) collidewall.normalize();
+ return true;
+ }
+ if(d->o.z < yo.z)
+ {
+ if(dir.iszero() || (dir.z > 0 && (d->type>=ENT_INANIMATE || below >= d->zmargin-(d->eyeheight+d->aboveeye)/4.0f)))
+ {
+ collidewall = vec(0, 0, -1);
+ return true;
+ }
+ }
+ else if(dir.iszero() || (dir.z < 0 && (d->type>=ENT_INANIMATE || above >= d->zmargin-(d->eyeheight+d->aboveeye)/3.0f)))
+ {
+ collidewall = vec(0, 0, 1);
+ return true;
+ }
+ collideinside++;
+ }
+ return false;
+}
+
+#define DYNENTCACHESIZE 1024
+
+static uint dynentframe = 0;
+
+static struct dynentcacheentry
+{
+ int x, y;
+ uint frame;
+ vector<physent *> dynents;
+} dynentcache[DYNENTCACHESIZE];
+
+void cleardynentcache()
+{
+ dynentframe++;
+ if(!dynentframe || dynentframe == 1) loopi(DYNENTCACHESIZE) dynentcache[i].frame = 0;
+ if(!dynentframe) dynentframe = 1;
+}
+
+VARF(dynentsize, 4, 7, 12, cleardynentcache());
+
+#define DYNENTHASH(x, y) (((((x)^(y))<<5) + (((x)^(y))>>5)) & (DYNENTCACHESIZE - 1))
+
+const vector<physent *> &checkdynentcache(int x, int y)
+{
+ dynentcacheentry &dec = dynentcache[DYNENTHASH(x, y)];
+ if(dec.x == x && dec.y == y && dec.frame == dynentframe) return dec.dynents;
+ dec.x = x;
+ dec.y = y;
+ dec.frame = dynentframe;
+ dec.dynents.shrink(0);
+ int numdyns = game::numdynents(), dsize = 1<<dynentsize, dx = x<<dynentsize, dy = y<<dynentsize;
+ loopi(numdyns)
+ {
+ dynent *d = game::iterdynents(i);
+ if(d->state != CS_ALIVE ||
+ d->o.x+d->radius <= dx || d->o.x-d->radius >= dx+dsize ||
+ d->o.y+d->radius <= dy || d->o.y-d->radius >= dy+dsize)
+ continue;
+ dec.dynents.add(d);
+ }
+ return dec.dynents;
+}
+
+#define loopdynentcache(curx, cury, o, radius) \
+ for(int curx = max(int(o.x-radius), 0)>>dynentsize, endx = min(int(o.x+radius), worldsize-1)>>dynentsize; curx <= endx; curx++) \
+ for(int cury = max(int(o.y-radius), 0)>>dynentsize, endy = min(int(o.y+radius), worldsize-1)>>dynentsize; cury <= endy; cury++)
+
+void updatedynentcache(physent *d)
+{
+ loopdynentcache(x, y, d->o, d->radius)
+ {
+ dynentcacheentry &dec = dynentcache[DYNENTHASH(x, y)];
+ if(dec.x != x || dec.y != y || dec.frame != dynentframe || dec.dynents.find(d) >= 0) continue;
+ dec.dynents.add(d);
+ }
+}
+
+bool overlapsdynent(const vec &o, float radius)
+{
+ loopdynentcache(x, y, o, radius)
+ {
+ const vector<physent *> &dynents = checkdynentcache(x, y);
+ loopv(dynents)
+ {
+ physent *d = dynents[i];
+ if(o.dist(d->o)-d->radius < radius) return true;
+ }
+ }
+ return false;
+}
+
+template<class E, class O>
+static inline bool plcollide(physent *d, const vec &dir, physent *o)
+{
+ E entvol(d);
+ O obvol(o);
+ vec cp;
+ if(mpr::collide(entvol, obvol, NULL, NULL, &cp))
+ {
+ vec wn = vec(cp).sub(obvol.center());
+ collidewall = obvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir);
+ if(!collidewall.iszero()) return true;
+ collideinside++;
+ }
+ return false;
+}
+
+static inline bool plcollide(physent *d, const vec &dir, physent *o)
+{
+ switch(d->collidetype)
+ {
+ case COLLIDE_ELLIPSE:
+ case COLLIDE_ELLIPSE_PRECISE:
+ if(o->collidetype == COLLIDE_OBB) return ellipseboxcollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight);
+ else return ellipsecollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight);
+ case COLLIDE_OBB:
+ if(o->collidetype == COLLIDE_OBB) return plcollide<mpr::EntOBB, mpr::EntOBB>(d, dir, o);
+ else return plcollide<mpr::EntOBB, mpr::EntCylinder>(d, dir, o);
+ default: return false;
+ }
+}
+
+bool plcollide(physent *d, const vec &dir, bool insideplayercol) // collide with player or monster
+{
+ if(d->type==ENT_CAMERA || d->state!=CS_ALIVE) return false;
+ int lastinside = collideinside;
+ physent *insideplayer = NULL;
+ loopdynentcache(x, y, d->o, d->radius)
+ {
+ const vector<physent *> &dynents = checkdynentcache(x, y);
+ loopv(dynents)
+ {
+ physent *o = dynents[i];
+ if(o==d || d->o.reject(o->o, d->radius+o->radius)) continue;
+ if(plcollide(d, dir, o))
+ {
+ collideplayer = o;
+ game::dynentcollide(d, o, collidewall);
+ return true;
+ }
+ if(collideinside > lastinside)
+ {
+ lastinside = collideinside;
+ insideplayer = o;
+ }
+ }
+ }
+ if(insideplayer && insideplayercol)
+ {
+ collideplayer = insideplayer;
+ game::dynentcollide(d, insideplayer, vec(0, 0, 0));
+ return true;
+ }
+ return false;
+}
+
+void rotatebb(vec &center, vec &radius, int yaw)
+{
+ if(yaw < 0) yaw = 360 + yaw%360;
+ else if(yaw >= 360) yaw %= 360;
+ const vec2 &rot = sincos360[yaw];
+ vec2 oldcenter(center), oldradius(radius);
+ center.x = oldcenter.x*rot.x - oldcenter.y*rot.y;
+ center.y = oldcenter.y*rot.x + oldcenter.x*rot.y;
+ radius.x = fabs(oldradius.x*rot.x) + fabs(oldradius.y*rot.y);
+ radius.y = fabs(oldradius.y*rot.x) + fabs(oldradius.x*rot.y);
+}
+
+template<class E, class M>
+static inline bool mmcollide(physent *d, const vec &dir, const extentity &e, const vec &center, const vec &radius, float yaw)
+{
+ E entvol(d);
+ M mdlvol(e.o, center, radius, yaw);
+ vec cp;
+ if(mpr::collide(entvol, mdlvol, NULL, NULL, &cp))
+ {
+ vec wn = vec(cp).sub(mdlvol.center());
+ collidewall = mdlvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir);
+ if(!collidewall.iszero()) return true;
+ collideinside++;
+ }
+ return false;
+}
+
+bool mmcollide(physent *d, const vec &dir, octaentities &oc) // collide with a mapmodel
+{
+ const vector<extentity *> &ents = entities::getents();
+ loopv(oc.mapmodels)
+ {
+ extentity &e = *ents[oc.mapmodels[i]];
+ if(e.flags&EF_NOCOLLIDE) continue;
+ model *m = loadmapmodel(e.attr2);
+ if(!m || !m->collide) continue;
+
+ vec center, radius;
+ float rejectradius = m->collisionbox(center, radius);
+ if(d->o.reject(e.o, d->radius + rejectradius)) continue;
+
+ float yaw = e.attr1;
+ switch(d->collidetype)
+ {
+ case COLLIDE_ELLIPSE:
+ case COLLIDE_ELLIPSE_PRECISE:
+ if(m->ellipsecollide)
+ {
+ if(ellipsecollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return true;
+ }
+ else if(ellipseboxcollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return true;
+ break;
+ case COLLIDE_OBB:
+ if(m->ellipsecollide)
+ {
+ if(mmcollide<mpr::EntOBB, mpr::ModelEllipse>(d, dir, e, center, radius, yaw)) return true;
+ }
+ else if(mmcollide<mpr::EntOBB, mpr::ModelOBB>(d, dir, e, center, radius, yaw)) return true;
+ break;
+ default: continue;
+ }
+ }
+ return false;
+}
+
+template<class E>
+static bool fuzzycollidesolid(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with solid cube geometry
+{
+ int crad = size/2;
+ if(fabs(d->o.x - co.x - crad) > d->radius + crad || fabs(d->o.y - co.y - crad) > d->radius + crad ||
+ d->o.z + d->aboveeye < co.z || d->o.z - d->eyeheight > co.z + size)
+ return false;
+
+ E entvol(d);
+ collidewall = vec(0, 0, 0);
+ float bestdist = -1e10f;
+ int visible = isentirelysolid(c) ? c.visible : 0xFF;
+ #define CHECKSIDE(side, distval, dotval, margin, normal) if(visible&(1<<side)) do \
+ { \
+ float dist = distval; \
+ if(dist > 0) return false; \
+ if(dist <= bestdist) continue; \
+ if(!dir.iszero()) \
+ { \
+ if(dotval >= -cutoff*dir.magnitude()) continue; \
+ if(d->type<ENT_CAMERA && dotval < 0 && dist < margin) continue; \
+ } \
+ collidewall = normal; \
+ bestdist = dist; \
+ } while(0)
+ CHECKSIDE(O_LEFT, co.x - (d->o.x + d->radius), -dir.x, -d->radius, vec(-1, 0, 0));
+ CHECKSIDE(O_RIGHT, d->o.x - d->radius - (co.x + size), dir.x, -d->radius, vec(1, 0, 0));
+ CHECKSIDE(O_BACK, co.y - (d->o.y + d->radius), -dir.y, -d->radius, vec(0, -1, 0));
+ CHECKSIDE(O_FRONT, d->o.y - d->radius - (co.y + size), dir.y, -d->radius, vec(0, 1, 0));
+ CHECKSIDE(O_BOTTOM, co.z - (d->o.z + d->aboveeye), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1));
+ CHECKSIDE(O_TOP, d->o.z - d->eyeheight - (co.z + size), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+ if(collidewall.iszero())
+ {
+ collideinside++;
+ return false;
+ }
+ return true;
+}
+
+template<class E>
+static inline bool clampcollide(const clipplanes &p, const E &entvol, const plane &w, const vec &pw)
+{
+ if(w.x && (w.y || w.z) && fabs(pw.x - p.o.x) > p.r.x)
+ {
+ vec c = entvol.center();
+ float fv = pw.x < p.o.x ? p.o.x-p.r.x : p.o.x+p.r.x, fdist = (w.x*fv + w.y*c.y + w.z*c.z + w.offset) / (w.y*w.y + w.z*w.z);
+ vec fdir(fv - c.x, -w.y*fdist, -w.z*fdist);
+ if((pw.y-c.y-fdir.y)*w.y + (pw.z-c.z-fdir.z)*w.z >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true;
+ }
+ if(w.y && (w.x || w.z) && fabs(pw.y - p.o.y) > p.r.y)
+ {
+ vec c = entvol.center();
+ float fv = pw.y < p.o.y ? p.o.y-p.r.y : p.o.y+p.r.y, fdist = (w.x*c.x + w.y*fv + w.z*c.z + w.offset) / (w.x*w.x + w.z*w.z);
+ vec fdir(-w.x*fdist, fv - c.y, -w.z*fdist);
+ if((pw.x-c.x-fdir.x)*w.x + (pw.z-c.z-fdir.z)*w.z >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true;
+ }
+ if(w.z && (w.x || w.y) && fabs(pw.z - p.o.z) > p.r.z)
+ {
+ vec c = entvol.center();
+ float fv = pw.z < p.o.z ? p.o.z-p.r.z : p.o.z+p.r.z, fdist = (w.x*c.x + w.y*c.y + w.z*fv + w.offset) / (w.x*w.x + w.y*w.y);
+ vec fdir(-w.x*fdist, -w.y*fdist, fv - c.z);
+ if((pw.x-c.x-fdir.x)*w.x + (pw.y-c.y-fdir.y)*w.y >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true;
+ }
+ return false;
+}
+
+template<class E>
+static bool fuzzycollideplanes(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with deformed cube geometry
+{
+ const clipplanes &p = getclipplanes(c, co, size);
+
+ if(fabs(d->o.x - p.o.x) > p.r.x + d->radius || fabs(d->o.y - p.o.y) > p.r.y + d->radius ||
+ d->o.z + d->aboveeye < p.o.z - p.r.z || d->o.z - d->eyeheight > p.o.z + p.r.z)
+ return false;
+
+ collidewall = vec(0, 0, 0);
+ float bestdist = -1e10f;
+ int visible = p.visible;
+ CHECKSIDE(O_LEFT, p.o.x - p.r.x - (d->o.x + d->radius), -dir.x, -d->radius, vec(-1, 0, 0));
+ CHECKSIDE(O_RIGHT, d->o.x - d->radius - (p.o.x + p.r.x), dir.x, -d->radius, vec(1, 0, 0));
+ CHECKSIDE(O_BACK, p.o.y - p.r.y - (d->o.y + d->radius), -dir.y, -d->radius, vec(0, -1, 0));
+ CHECKSIDE(O_FRONT, d->o.y - d->radius - (p.o.y + p.r.y), dir.y, -d->radius, vec(0, 1, 0));
+ CHECKSIDE(O_BOTTOM, p.o.z - p.r.z - (d->o.z + d->aboveeye), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1));
+ CHECKSIDE(O_TOP, d->o.z - d->eyeheight - (p.o.z + p.r.z), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+ E entvol(d);
+ int bestplane = -1;
+ loopi(p.size)
+ {
+ const plane &w = p.p[i];
+ vec pw = entvol.supportpoint(vec(w).neg());
+ float dist = w.dist(pw);
+ if(dist >= 0) return false;
+ if(dist <= bestdist) continue;
+ bestplane = -1;
+ bestdist = dist;
+ if(!dir.iszero())
+ {
+ if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
+ if(d->type<ENT_CAMERA &&
+ dist < (dir.z*w.z < 0 ?
+ d->zmargin-(d->eyeheight+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
+ ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0)))
+ continue;
+ }
+ if(clampcollide(p, entvol, w, pw)) continue;
+ bestplane = i;
+ }
+ if(bestplane >= 0) collidewall = p.p[bestplane];
+ else if(collidewall.iszero())
+ {
+ collideinside++;
+ return false;
+ }
+ return true;
+}
+
+template<class E>
+static bool cubecollidesolid(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with solid cube geometry
+{
+ int crad = size/2;
+ if(fabs(d->o.x - co.x - crad) > d->radius + crad || fabs(d->o.y - co.y - crad) > d->radius + crad ||
+ d->o.z + d->aboveeye < co.z || d->o.z - d->eyeheight > co.z + size)
+ return false;
+
+ E entvol(d);
+ bool collided = mpr::collide(mpr::SolidCube(co, size), entvol);
+ if(!collided) return false;
+
+ collidewall = vec(0, 0, 0);
+ float bestdist = -1e10f;
+ int visible = isentirelysolid(c) ? c.visible : 0xFF;
+ CHECKSIDE(O_LEFT, co.x - entvol.right(), -dir.x, -d->radius, vec(-1, 0, 0));
+ CHECKSIDE(O_RIGHT, entvol.left() - (co.x + size), dir.x, -d->radius, vec(1, 0, 0));
+ CHECKSIDE(O_BACK, co.y - entvol.front(), -dir.y, -d->radius, vec(0, -1, 0));
+ CHECKSIDE(O_FRONT, entvol.back() - (co.y + size), dir.y, -d->radius, vec(0, 1, 0));
+ CHECKSIDE(O_BOTTOM, co.z - entvol.top(), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1));
+ CHECKSIDE(O_TOP, entvol.bottom() - (co.z + size), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+ if(collidewall.iszero())
+ {
+ collideinside++;
+ return false;
+ }
+ return true;
+}
+
+template<class E>
+static bool cubecollideplanes(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with deformed cube geometry
+{
+ const clipplanes &p = getclipplanes(c, co, size);
+
+ if(fabs(d->o.x - p.o.x) > p.r.x + d->radius || fabs(d->o.y - p.o.y) > p.r.y + d->radius ||
+ d->o.z + d->aboveeye < p.o.z - p.r.z || d->o.z - d->eyeheight > p.o.z + p.r.z)
+ return false;
+
+ E entvol(d);
+ bool collided = mpr::collide(mpr::CubePlanes(p), entvol);
+ if(!collided) return false;
+
+ collidewall = vec(0, 0, 0);
+ float bestdist = -1e10f;
+ int visible = p.visible;
+ CHECKSIDE(O_LEFT, p.o.x - p.r.x - entvol.right(), -dir.x, -d->radius, vec(-1, 0, 0));
+ CHECKSIDE(O_RIGHT, entvol.left() - (p.o.x + p.r.x), dir.x, -d->radius, vec(1, 0, 0));
+ CHECKSIDE(O_BACK, p.o.y - p.r.y - entvol.front(), -dir.y, -d->radius, vec(0, -1, 0));
+ CHECKSIDE(O_FRONT, entvol.back() - (p.o.y + p.r.y), dir.y, -d->radius, vec(0, 1, 0));
+ CHECKSIDE(O_BOTTOM, p.o.z - p.r.z - entvol.top(), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1));
+ CHECKSIDE(O_TOP, entvol.bottom() - (p.o.z + p.r.z), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+ int bestplane = -1;
+ loopi(p.size)
+ {
+ const plane &w = p.p[i];
+ vec pw = entvol.supportpoint(vec(w).neg());
+ float dist = w.dist(pw);
+ if(dist <= bestdist) continue;
+ bestplane = -1;
+ bestdist = dist;
+ if(!dir.iszero())
+ {
+ if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
+ if(d->type<ENT_CAMERA &&
+ dist < (dir.z*w.z < 0 ?
+ d->zmargin-(d->eyeheight+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
+ ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0)))
+ continue;
+ }
+ if(clampcollide(p, entvol, w, pw)) continue;
+ bestplane = i;
+ }
+ if(bestplane >= 0) collidewall = p.p[bestplane];
+ else if(collidewall.iszero())
+ {
+ collideinside++;
+ return false;
+ }
+ return true;
+}
+
+static inline bool cubecollide(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size, bool solid)
+{
+ switch(d->collidetype)
+ {
+ case COLLIDE_OBB:
+ if(isentirelysolid(c) || solid) return cubecollidesolid<mpr::EntOBB>(d, dir, cutoff, c, co, size);
+ else return cubecollideplanes<mpr::EntOBB>(d, dir, cutoff, c, co, size);
+ case COLLIDE_ELLIPSE:
+ if(isentirelysolid(c) || solid) return fuzzycollidesolid<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+ else return fuzzycollideplanes<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+ case COLLIDE_ELLIPSE_PRECISE:
+ if(isentirelysolid(c) || solid) return cubecollidesolid<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+ else return cubecollideplanes<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+ default: return false;
+ }
+}
+
+static inline bool octacollide(physent *d, const vec &dir, float cutoff, const ivec &bo, const ivec &bs, const cube *c, const ivec &cor, int size) // collide with octants
+{
+ loopoctabox(cor, size, bo, bs)
+ {
+ if(c[i].ext && c[i].ext->ents) if(mmcollide(d, dir, *c[i].ext->ents)) return true;
+ ivec o(i, cor, size);
+ if(c[i].children)
+ {
+ if(octacollide(d, dir, cutoff, bo, bs, c[i].children, o, size>>1)) return true;
+ }
+ else
+ {
+ bool solid = false;
+ switch(c[i].material&MATF_CLIP)
+ {
+ case MAT_NOCLIP: continue;
+ case MAT_GAMECLIP: if(d->type==ENT_AI) solid = true; break;
+ case MAT_CLIP: if(isclipped(c[i].material&MATF_VOLUME) || d->type<ENT_CAMERA) solid = true; break;
+ }
+ if(!solid && isempty(c[i])) continue;
+ if(cubecollide(d, dir, cutoff, c[i], o, size, solid)) return true;
+ }
+ }
+ return false;
+}
+
+static inline bool octacollide(physent *d, const vec &dir, float cutoff, const ivec &bo, const ivec &bs)
+{
+ int diff = (bo.x^bs.x) | (bo.y^bs.y) | (bo.z^bs.z),
+ scale = worldscale-1;
+ if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|bs.x|bs.y|bs.z) >= uint(worldsize))
+ return octacollide(d, dir, cutoff, bo, bs, worldroot, ivec(0, 0, 0), worldsize>>1);
+ const cube *c = &worldroot[octastep(bo.x, bo.y, bo.z, scale)];
+ if(c->ext && c->ext->ents && mmcollide(d, dir, *c->ext->ents)) return true;
+ scale--;
+ while(c->children && !(diff&(1<<scale)))
+ {
+ c = &c->children[octastep(bo.x, bo.y, bo.z, scale)];
+ if(c->ext && c->ext->ents && mmcollide(d, dir, *c->ext->ents)) return true;
+ scale--;
+ }
+ if(c->children) return octacollide(d, dir, cutoff, bo, bs, c->children, ivec(bo).mask(~((2<<scale)-1)), 1<<scale);
+ bool solid = false;
+ switch(c->material&MATF_CLIP)
+ {
+ case MAT_NOCLIP: return false;
+ case MAT_GAMECLIP: if(d->type==ENT_AI) solid = true; break;
+ case MAT_CLIP: if(isclipped(c->material&MATF_VOLUME) || d->type<ENT_CAMERA) solid = true; break;
+ }
+ if(!solid && isempty(*c)) return false;
+ int csize = 2<<scale, cmask = ~(csize-1);
+ return cubecollide(d, dir, cutoff, *c, ivec(bo).mask(cmask), csize, solid);
+}
+
+// all collision happens here
+bool collide(physent *d, const vec &dir, float cutoff, bool playercol, bool insideplayercol)
+{
+ collideinside = 0;
+ collideplayer = NULL;
+ collidewall = vec(0, 0, 0);
+ ivec bo(int(d->o.x-d->radius), int(d->o.y-d->radius), int(d->o.z-d->eyeheight)),
+ bs(int(d->o.x+d->radius), int(d->o.y+d->radius), int(d->o.z+d->aboveeye));
+ bs.add(1); // guard space for rounding errors
+ return octacollide(d, dir, cutoff, bo, bs) || (playercol && plcollide(d, dir, insideplayercol));
+}
+
+void recalcdir(physent *d, const vec &oldvel, vec &dir)
+{
+ float speed = oldvel.magnitude();
+ if(speed > 1e-6f)
+ {
+ float step = dir.magnitude();
+ dir = d->vel;
+ dir.add(d->falling);
+ dir.mul(step/speed);
+ }
+}
+
+void slideagainst(physent *d, vec &dir, const vec &obstacle, bool foundfloor, bool slidecollide)
+{
+ vec wall(obstacle);
+ if(foundfloor ? wall.z > 0 : slidecollide)
+ {
+ wall.z = 0;
+ if(!wall.iszero()) wall.normalize();
+ }
+ vec oldvel(d->vel);
+ oldvel.add(d->falling);
+ d->vel.project(wall);
+ d->falling.project(wall);
+ recalcdir(d, oldvel, dir);
+}
+
+void switchfloor(physent *d, vec &dir, const vec &floor)
+{
+ if(floor.z >= FLOORZ) d->falling = vec(0, 0, 0);
+
+ vec oldvel(d->vel);
+ oldvel.add(d->falling);
+ if(dir.dot(floor) >= 0)
+ {
+ if(d->physstate < PHYS_SLIDE || fabs(dir.dot(d->floor)) > 0.01f*dir.magnitude()) return;
+ d->vel.projectxy(floor, 0.0f);
+ }
+ else d->vel.projectxy(floor);
+ d->falling.project(floor);
+ recalcdir(d, oldvel, dir);
+}
+
+bool trystepup(physent *d, vec &dir, const vec &obstacle, float maxstep, const vec &floor)
+{
+ vec old(d->o), stairdir = (obstacle.z >= 0 && obstacle.z < SLOPEZ ? vec(-obstacle.x, -obstacle.y, 0) : vec(dir.x, dir.y, 0)).rescale(1);
+ bool cansmooth = true;
+ /* check if there is space atop the stair to move to */
+ if(d->physstate != PHYS_STEP_UP)
+ {
+ vec checkdir = stairdir;
+ checkdir.mul(0.1f);
+ checkdir.z += maxstep + 0.1f;
+ d->o.add(checkdir);
+ if(collide(d))
+ {
+ d->o = old;
+ if(!collide(d, vec(0, 0, -1), SLOPEZ)) return false;
+ cansmooth = false;
+ }
+ }
+
+ if(cansmooth)
+ {
+ vec checkdir = stairdir;
+ checkdir.z += 1;
+ checkdir.mul(maxstep);
+ d->o = old;
+ d->o.add(checkdir);
+ int scale = 2;
+ if(collide(d, checkdir))
+ {
+ if(!collide(d, vec(0, 0, -1), SLOPEZ))
+ {
+ d->o = old;
+ return false;
+ }
+ d->o.add(checkdir);
+ if(collide(d, vec(0, 0, -1), SLOPEZ)) scale = 1;
+ }
+ if(scale != 1)
+ {
+ d->o = old;
+ d->o.sub(checkdir.mul(vec(2, 2, 1)));
+ if(!collide(d, vec(0, 0, -1), SLOPEZ)) scale = 1;
+ }
+
+ d->o = old;
+ vec smoothdir(dir.x, dir.y, 0);
+ float magxy = smoothdir.magnitude();
+ if(magxy > 1e-9f)
+ {
+ if(magxy > scale*dir.z)
+ {
+ smoothdir.mul(1/magxy);
+ smoothdir.z = 1.0f/scale;
+ smoothdir.mul(dir.magnitude()/smoothdir.magnitude());
+ }
+ else smoothdir.z = dir.z;
+ d->o.add(smoothdir);
+ d->o.z += maxstep + 0.1f;
+ if(!collide(d, smoothdir))
+ {
+ d->o.z -= maxstep + 0.1f;
+ if(d->physstate == PHYS_FALL || d->floor != floor)
+ {
+ d->timeinair = 0;
+ d->floor = floor;
+ switchfloor(d, dir, d->floor);
+ }
+ d->physstate = PHYS_STEP_UP;
+ return true;
+ }
+ }
+ }
+
+ /* try stepping up */
+ d->o = old;
+ d->o.z += dir.magnitude();
+ if(!collide(d, vec(0, 0, 1)))
+ {
+ if(d->physstate == PHYS_FALL || d->floor != floor)
+ {
+ d->timeinair = 0;
+ d->floor = floor;
+ switchfloor(d, dir, d->floor);
+ }
+ if(cansmooth) d->physstate = PHYS_STEP_UP;
+ return true;
+ }
+ d->o = old;
+ return false;
+}
+
+bool trystepdown(physent *d, vec &dir, float step, float xy, float z, bool init = false)
+{
+ vec stepdir(dir.x, dir.y, 0);
+ stepdir.z = -stepdir.magnitude2()*z/xy;
+ if(!stepdir.z) return false;
+ stepdir.normalize();
+
+ vec old(d->o);
+ d->o.add(vec(stepdir).mul(STAIRHEIGHT/fabs(stepdir.z))).z -= STAIRHEIGHT;
+ d->zmargin = -STAIRHEIGHT;
+ if(collide(d, vec(0, 0, -1), SLOPEZ))
+ {
+ d->o = old;
+ d->o.add(vec(stepdir).mul(step));
+ d->zmargin = 0;
+ if(!collide(d, vec(0, 0, -1)))
+ {
+ vec stepfloor(stepdir);
+ stepfloor.mul(-stepfloor.z).z += 1;
+ stepfloor.normalize();
+ if(d->physstate >= PHYS_SLOPE && d->floor != stepfloor)
+ {
+ // prevent alternating step-down/step-up states if player would keep bumping into the same floor
+ vec stepped(d->o);
+ d->o.z -= 0.5f;
+ d->zmargin = -0.5f;
+ if(collide(d, stepdir) && collidewall == d->floor)
+ {
+ d->o = old;
+ if(!init) { d->o.x += dir.x; d->o.y += dir.y; if(dir.z <= 0 || collide(d, dir)) d->o.z += dir.z; }
+ d->zmargin = 0;
+ d->physstate = PHYS_STEP_DOWN;
+ d->timeinair = 0;
+ return true;
+ }
+ d->o = init ? old : stepped;
+ d->zmargin = 0;
+ }
+ else if(init) d->o = old;
+ switchfloor(d, dir, stepfloor);
+ d->floor = stepfloor;
+ d->physstate = PHYS_STEP_DOWN;
+ d->timeinair = 0;
+ return true;
+ }
+ }
+ d->o = old;
+ d->zmargin = 0;
+ return false;
+}
+
+bool trystepdown(physent *d, vec &dir, bool init = false)
+{
+ if((!d->move && !d->strafe) || !game::allowmove(d)) return false;
+ vec old(d->o);
+ d->o.z -= STAIRHEIGHT;
+ d->zmargin = -STAIRHEIGHT;
+ if(!collide(d, vec(0, 0, -1), SLOPEZ))
+ {
+ d->o = old;
+ d->zmargin = 0;
+ return false;
+ }
+ d->o = old;
+ d->zmargin = 0;
+ float step = dir.magnitude();
+#if 1
+ // weaker check, just enough to avoid hopping up slopes
+ if(trystepdown(d, dir, step, 4, 1, init)) return true;
+#else
+ if(trystepdown(d, dir, step, 2, 1, init)) return true;
+ if(trystepdown(d, dir, step, 1, 1, init)) return true;
+ if(trystepdown(d, dir, step, 1, 2, init)) return true;
+#endif
+ return false;
+}
+
+void falling(physent *d, vec &dir, const vec &floor)
+{
+ if(floor.z > 0.0f && floor.z < SLOPEZ)
+ {
+ if(floor.z >= WALLZ) switchfloor(d, dir, floor);
+ d->timeinair = 0;
+ d->physstate = PHYS_SLIDE;
+ d->floor = floor;
+ }
+ else if(d->physstate < PHYS_SLOPE || dir.dot(d->floor) > 0.01f*dir.magnitude() || (floor.z != 0.0f && floor.z != 1.0f) || !trystepdown(d, dir, true))
+ d->physstate = PHYS_FALL;
+}
+
+void landing(physent *d, vec &dir, const vec &floor, bool collided)
+{
+#if 0
+ if(d->physstate == PHYS_FALL)
+ {
+ d->timeinair = 0;
+ if(dir.z < 0.0f) dir.z = d->vel.z = 0.0f;
+ }
+#endif
+ switchfloor(d, dir, floor);
+ d->timeinair = 0;
+ if((d->physstate!=PHYS_STEP_UP && d->physstate!=PHYS_STEP_DOWN) || !collided)
+ d->physstate = floor.z >= FLOORZ ? PHYS_FLOOR : PHYS_SLOPE;
+ d->floor = floor;
+}
+
+bool findfloor(physent *d, bool collided, const vec &obstacle, bool &slide, vec &floor)
+{
+ bool found = false;
+ vec moved(d->o);
+ d->o.z -= 0.1f;
+ if(collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? SLOPEZ : FLOORZ))
+ {
+ floor = collidewall;
+ found = true;
+ }
+ else if(collided && obstacle.z >= SLOPEZ)
+ {
+ floor = obstacle;
+ found = true;
+ slide = false;
+ }
+ else if(d->physstate == PHYS_STEP_UP || d->physstate == PHYS_SLIDE)
+ {
+ if(collide(d, vec(0, 0, -1)) && collidewall.z > 0.0f)
+ {
+ floor = collidewall;
+ if(floor.z >= SLOPEZ) found = true;
+ }
+ }
+ else if(d->physstate >= PHYS_SLOPE && d->floor.z < 1.0f)
+ {
+ if(collide(d, vec(d->floor).neg(), 0.95f) || collide(d, vec(0, 0, -1)))
+ {
+ floor = collidewall;
+ if(floor.z >= SLOPEZ && floor.z < 1.0f) found = true;
+ }
+ }
+ if(collided && (!found || obstacle.z > floor.z))
+ {
+ floor = obstacle;
+ slide = !found && (floor.z < WALLZ || floor.z >= SLOPEZ);
+ }
+ d->o = moved;
+ return found;
+}
+
+bool move(physent *d, vec &dir)
+{
+ vec old(d->o);
+ bool collided = false, slidecollide = false;
+ vec obstacle;
+ d->o.add(dir);
+ if(collide(d, dir) || ((d->type==ENT_AI || d->type==ENT_INANIMATE) && collide(d, vec(0, 0, 0), 0, false)))
+ {
+ obstacle = collidewall;
+ /* check to see if there is an obstacle that would prevent this one from being used as a floor (or ceiling bump) */
+ if(d->type==ENT_PLAYER && ((collidewall.z>=SLOPEZ && dir.z<0) || (collidewall.z<=-SLOPEZ && dir.z>0)) && (dir.x || dir.y) && collide(d, vec(dir.x, dir.y, 0)))
+ {
+ if(collidewall.dot(dir) >= 0) slidecollide = true;
+ obstacle = collidewall;
+ }
+ d->o = old;
+ d->o.z -= STAIRHEIGHT;
+ d->zmargin = -STAIRHEIGHT;
+ if(d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR || (collide(d, vec(0, 0, -1), SLOPEZ) && (d->physstate==PHYS_STEP_UP || d->physstate==PHYS_STEP_DOWN || collidewall.z>=FLOORZ)))
+ {
+ d->o = old;
+ d->zmargin = 0;
+ if(trystepup(d, dir, obstacle, STAIRHEIGHT, d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR ? d->floor : vec(collidewall))) return true;
+ }
+ else
+ {
+ d->o = old;
+ d->zmargin = 0;
+ }
+ /* can't step over the obstacle, so just slide against it */
+ collided = true;
+ }
+ else if(d->physstate == PHYS_STEP_UP)
+ {
+ if(collide(d, vec(0, 0, -1), SLOPEZ))
+ {
+ d->o = old;
+ if(trystepup(d, dir, vec(0, 0, 1), STAIRHEIGHT, vec(collidewall))) return true;
+ d->o.add(dir);
+ }
+ }
+ else if(d->physstate == PHYS_STEP_DOWN && dir.dot(d->floor) <= 1e-6f)
+ {
+ vec moved(d->o);
+ d->o = old;
+ if(trystepdown(d, dir)) return true;
+ d->o = moved;
+ }
+ vec floor(0, 0, 0);
+ bool slide = collided,
+ found = findfloor(d, collided, obstacle, slide, floor);
+ if(slide || (!collided && floor.z > 0 && floor.z < WALLZ))
+ {
+ slideagainst(d, dir, slide ? obstacle : floor, found, slidecollide);
+ //if(d->type == ENT_AI || d->type == ENT_INANIMATE)
+ d->blocked = true;
+ }
+ if(found) landing(d, dir, floor, collided);
+ else falling(d, dir, floor);
+ return !collided;
+}
+
+bool bounce(physent *d, float secs, float elasticity, float waterfric, float grav)
+{
+ // make sure bouncers don't start inside geometry
+ if(d->physstate!=PHYS_BOUNCE && collide(d, vec(0, 0, 0), 0, false)) return true;
+ int mat = lookupmaterial(vec(d->o.x, d->o.y, d->o.z + (d->aboveeye - d->eyeheight)/2));
+ bool water = isliquid(mat);
+ if(water)
+ {
+ d->vel.z -= grav*GRAVITY/16*secs;
+ d->vel.mul(max(1.0f - secs/waterfric, 0.0f));
+ }
+ else d->vel.z -= grav*GRAVITY*secs;
+ vec old(d->o);
+ loopi(2)
+ {
+ vec dir(d->vel);
+ dir.mul(secs);
+ d->o.add(dir);
+ if(!collide(d, dir, 0, true, true))
+ {
+ if(collideinside)
+ {
+ d->o = old;
+ d->vel.mul(-elasticity);
+ }
+ break;
+ }
+ else if(collideplayer) break;
+ d->o = old;
+ game::bounced(d, collidewall);
+ float c = collidewall.dot(d->vel),
+ k = 1.0f + (1.0f-elasticity)*c/d->vel.magnitude();
+ d->vel.mul(k);
+ d->vel.sub(vec(collidewall).mul(elasticity*2.0f*c));
+ }
+ if(d->physstate!=PHYS_BOUNCE)
+ {
+ // make sure bouncers don't start inside geometry
+ if(d->o == old) return !collideplayer;
+ d->physstate = PHYS_BOUNCE;
+ }
+ return collideplayer!=NULL;
+}
+
+void avoidcollision(physent *d, const vec &dir, physent *obstacle, float space)
+{
+ float rad = obstacle->radius+d->radius;
+ vec bbmin(obstacle->o);
+ bbmin.x -= rad;
+ bbmin.y -= rad;
+ bbmin.z -= obstacle->eyeheight+d->aboveeye;
+ bbmin.sub(space);
+ vec bbmax(obstacle->o);
+ bbmax.x += rad;
+ bbmax.y += rad;
+ bbmax.z += obstacle->aboveeye+d->eyeheight;
+ bbmax.add(space);
+
+ loopi(3) if(d->o[i] <= bbmin[i] || d->o[i] >= bbmax[i]) return;
+
+ float mindist = 1e16f;
+ loopi(3) if(dir[i] != 0)
+ {
+ float dist = ((dir[i] > 0 ? bbmax[i] : bbmin[i]) - d->o[i]) / dir[i];
+ mindist = min(mindist, dist);
+ }
+ if(mindist >= 0.0f && mindist < 1e15f) d->o.add(vec(dir).mul(mindist));
+}
+
+bool movecamera(physent *pl, const vec &dir, float dist, float stepdist)
+{
+ int steps = (int)ceil(dist/stepdist);
+ if(steps <= 0) return true;
+
+ vec d(dir);
+ d.mul(dist/steps);
+ loopi(steps)
+ {
+ vec oldpos(pl->o);
+ pl->o.add(d);
+ if(collide(pl, vec(0, 0, 0), 0, false))
+ {
+ pl->o = oldpos;
+ return false;
+ }
+ }
+ return true;
+}
+
+bool droptofloor(vec &o, float radius, float height)
+{
+ static struct dropent : physent
+ {
+ dropent()
+ {
+ type = ENT_BOUNCE;
+ vel = vec(0, 0, -1);
+ }
+ } d;
+ d.o = o;
+ if(!insideworld(d.o))
+ {
+ if(d.o.z < worldsize) return false;
+ d.o.z = worldsize - 1e-3f;
+ if(!insideworld(d.o)) return false;
+ }
+ vec v(0.0001f, 0.0001f, -1);
+ v.normalize();
+ if(raycube(d.o, v, worldsize) >= worldsize) return false;
+ d.radius = d.xradius = d.yradius = radius;
+ d.eyeheight = height;
+ d.aboveeye = radius;
+ if(!movecamera(&d, d.vel, worldsize, 1))
+ {
+ o = d.o;
+ return true;
+ }
+ return false;
+}
+
+float dropheight(entity &e)
+{
+ switch(e.type)
+ {
+ case ET_PARTICLES:
+ case ET_MAPMODEL: return 0.0f;
+ default:
+ if(e.type >= ET_GAMESPECIFIC) return entities::dropheight(e);
+ return 4.0f;
+ }
+}
+
+void dropenttofloor(entity *e)
+{
+ droptofloor(e->o, 1.0f, dropheight(*e));
+}
+
+void phystest()
+{
+ static const char * const states[] = {"float", "fall", "slide", "slope", "floor", "step up", "step down", "bounce"};
+ printf ("PHYS(pl): %s, air %d, floor: (%f, %f, %f), vel: (%f, %f, %f), g: (%f, %f, %f)\n", states[player->physstate], player->timeinair, player->floor.x, player->floor.y, player->floor.z, player->vel.x, player->vel.y, player->vel.z, player->falling.x, player->falling.y, player->falling.z);
+ printf ("PHYS(cam): %s, air %d, floor: (%f, %f, %f), vel: (%f, %f, %f), g: (%f, %f, %f)\n", states[camera1->physstate], camera1->timeinair, camera1->floor.x, camera1->floor.y, camera1->floor.z, camera1->vel.x, camera1->vel.y, camera1->vel.z, camera1->falling.x, camera1->falling.y, camera1->falling.z);
+}
+
+COMMAND(phystest, "");
+
+void vecfromyawpitch(float yaw, float pitch, int move, int strafe, vec &m)
+{
+ if(move)
+ {
+ m.x = move*-sinf(RAD*yaw);
+ m.y = move*cosf(RAD*yaw);
+ }
+ else m.x = m.y = 0;
+
+ if(pitch)
+ {
+ m.x *= cosf(RAD*pitch);
+ m.y *= cosf(RAD*pitch);
+ m.z = move*sinf(RAD*pitch);
+ }
+ else m.z = 0;
+
+ if(strafe)
+ {
+ m.x += strafe*cosf(RAD*yaw);
+ m.y += strafe*sinf(RAD*yaw);
+ }
+}
+
+void vectoyawpitch(const vec &v, float &yaw, float &pitch)
+{
+ if(v.iszero()) yaw = pitch = 0;
+ else
+ {
+ yaw = -atan2(v.x, v.y)/RAD;
+ pitch = asin(v.z/v.magnitude())/RAD;
+ }
+}
+
+#define PHYSFRAMETIME 5
+
+VARP(maxroll, 0, 0, 20);
+FVAR(straferoll, 0, 0.033f, 90);
+FVAR(faderoll, 0, 0.95f, 1);
+VAR(floatspeed, 1, 100, 10000);
+
+void modifyvelocity(physent *pl, bool local, bool water, bool floating, int curtime)
+{
+ bool allowmove = game::allowmove(pl);
+ if(floating)
+ {
+ if(pl->jumping && allowmove)
+ {
+ pl->jumping = false;
+ pl->vel.z = max(pl->vel.z, JUMPVEL);
+ }
+ }
+ else if(pl->physstate >= PHYS_SLOPE || water)
+ {
+ if(water && !pl->inwater) pl->vel.div(8);
+ if(pl->jumping && allowmove)
+ {
+ pl->jumping = false;
+
+ pl->vel.z = max(pl->vel.z, JUMPVEL); // physics impulse upwards
+ if(water) { pl->vel.x /= 8.0f; pl->vel.y /= 8.0f; } // dampen velocity change even harder, gives correct water feel
+
+ game::physicstrigger(pl, local, 1, 0);
+ }
+ }
+ if(!floating && pl->physstate == PHYS_FALL) pl->timeinair = min(pl->timeinair + curtime, 1000);
+
+ vec m(0.0f, 0.0f, 0.0f);
+ if((pl->move || pl->strafe) && allowmove)
+ {
+ vecfromyawpitch(pl->yaw, floating || water || pl->type==ENT_CAMERA ? pl->pitch : 0, pl->move, pl->strafe, m);
+
+ if(!floating && pl->physstate >= PHYS_SLOPE)
+ {
+ /* move up or down slopes in air
+ * but only move up slopes in water
+ */
+ float dz = -(m.x*pl->floor.x + m.y*pl->floor.y)/pl->floor.z;
+ m.z = water ? max(m.z, dz) : dz;
+ }
+
+ m.normalize();
+ }
+
+ vec d(m);
+ d.mul(pl->maxspeed);
+ if(pl->type==ENT_PLAYER)
+ {
+ if(floating)
+ {
+ if(pl==player) d.mul(floatspeed/100.0f);
+ }
+ else if(!water && allowmove) d.mul((pl->move && !pl->strafe ? 1.3f : 1.0f) * (pl->physstate < PHYS_SLOPE ? 1.3f : 1.0f));
+ }
+ float fric = water && !floating ? 20.0f : (pl->physstate >= PHYS_SLOPE || floating ? 6.0f : 30.0f);
+ pl->vel.lerp(d, pl->vel, pow(1 - 1/fric, curtime/20.0f));
+// old fps friction
+// float friction = water && !floating ? 20.0f : (pl->physstate >= PHYS_SLOPE || floating ? 6.0f : 30.0f);
+// float fpsfric = min(curtime/(20.0f*friction), 1.0f);
+// pl->vel.lerp(pl->vel, d, fpsfric);
+}
+
+void modifygravity(physent *pl, bool water, int curtime)
+{
+ float secs = curtime/1000.0f;
+ vec g(0, 0, 0);
+ if(pl->physstate == PHYS_FALL) g.z -= GRAVITY*secs;
+ else if(pl->floor.z > 0 && pl->floor.z < FLOORZ)
+ {
+ g.z = -1;
+ g.project(pl->floor);
+ g.normalize();
+ g.mul(GRAVITY*secs);
+ }
+ if(!water || !game::allowmove(pl) || (!pl->move && !pl->strafe)) pl->falling.add(g);
+
+ if(water || pl->physstate >= PHYS_SLOPE)
+ {
+ float fric = water ? 2.0f : 6.0f,
+ c = water ? 1.0f : clamp((pl->floor.z - SLOPEZ)/(FLOORZ-SLOPEZ), 0.0f, 1.0f);
+ pl->falling.mul(pow(1 - c/fric, curtime/20.0f));
+// old fps friction
+// float friction = water ? 2.0f : 6.0f,
+// fpsfric = friction/curtime*20.0f,
+// c = water ? 1.0f : clamp((pl->floor.z - SLOPEZ)/(FLOORZ-SLOPEZ), 0.0f, 1.0f);
+// pl->falling.mul(1 - c/fpsfric);
+ }
+}
+
+// main physics routine, moves a player/monster for a curtime step
+// moveres indicated the physics precision (which is lower for monsters and multiplayer prediction)
+// local is false for multiplayer prediction
+
+bool moveplayer(physent *pl, int moveres, bool local, int curtime)
+{
+ int material = lookupmaterial(vec(pl->o.x, pl->o.y, pl->o.z + (3*pl->aboveeye - pl->eyeheight)/4));
+ bool water = isliquid(material&MATF_VOLUME);
+ bool floating = pl->type==ENT_PLAYER && (pl->state==CS_EDITING || pl->state==CS_SPECTATOR);
+ float secs = curtime/1000.f;
+
+ // apply gravity
+ if(!floating) modifygravity(pl, water, curtime);
+ // apply any player generated changes in velocity
+ modifyvelocity(pl, local, water, floating, curtime);
+
+ vec d(pl->vel);
+ if(!floating && water) d.mul(0.5f);
+ d.add(pl->falling);
+ d.mul(secs);
+
+ pl->blocked = false;
+
+ if(floating) // just apply velocity
+ {
+ if(pl->physstate != PHYS_FLOAT)
+ {
+ pl->physstate = PHYS_FLOAT;
+ pl->timeinair = 0;
+ pl->falling = vec(0, 0, 0);
+ }
+ pl->o.add(d);
+ }
+ else // apply velocity with collision
+ {
+ const float f = 1.0f/moveres;
+ const int timeinair = pl->timeinair;
+ int collisions = 0;
+
+ d.mul(f);
+ loopi(moveres) if(!move(pl, d) && ++collisions<5) i--; // discrete steps collision detection & sliding
+ if(timeinair > 800 && !pl->timeinair && !water) // if we land after long time must have been a high jump, make thud sound
+ {
+ game::physicstrigger(pl, local, -1, 0);
+ }
+ }
+
+ if(pl->state==CS_ALIVE) updatedynentcache(pl);
+
+ // automatically apply smooth roll when strafing
+
+ if(pl->strafe && maxroll) pl->roll = clamp(pl->roll - pow(clamp(1.0f + pl->strafe*pl->roll/maxroll, 0.0f, 1.0f), 0.33f)*pl->strafe*curtime*straferoll, -maxroll, maxroll);
+ else pl->roll *= curtime == PHYSFRAMETIME ? faderoll : pow(faderoll, curtime/float(PHYSFRAMETIME));
+
+ // play sounds on water transitions
+
+ if(pl->inwater && !water)
+ {
+ material = lookupmaterial(vec(pl->o.x, pl->o.y, pl->o.z + (pl->aboveeye - pl->eyeheight)/2));
+ water = isliquid(material&MATF_VOLUME);
+ }
+ if(!pl->inwater && water) game::physicstrigger(pl, local, 0, -1, material&MATF_VOLUME);
+ else if(pl->inwater && !water) game::physicstrigger(pl, local, 0, 1, pl->inwater);
+ pl->inwater = water ? material&MATF_VOLUME : MAT_AIR;
+
+ if(pl->state==CS_ALIVE && (pl->o.z < 0 || material&MAT_DEATH)) game::suicide(pl);
+
+ return true;
+}
+
+int physsteps = 0, physframetime = PHYSFRAMETIME, lastphysframe = 0;
+
+void physicsframe() // optimally schedule physics frames inside the graphics frames
+{
+ int diff = lastmillis - lastphysframe;
+ if(diff <= 0) physsteps = 0;
+ else
+ {
+ physframetime = clamp(game::scaletime(PHYSFRAMETIME)/100, 1, PHYSFRAMETIME);
+ physsteps = (diff + physframetime - 1)/physframetime;
+ lastphysframe += physsteps * physframetime;
+ }
+ cleardynentcache();
+}
+
+VAR(physinterp, 0, 1, 1);
+
+void interppos(physent *pl)
+{
+ pl->o = pl->newpos;
+
+ int diff = lastphysframe - lastmillis;
+ if(diff <= 0 || !physinterp) return;
+
+ vec deltapos(pl->deltapos);
+ deltapos.mul(min(diff, physframetime)/float(physframetime));
+ pl->o.add(deltapos);
+}
+
+void moveplayer(physent *pl, int moveres, bool local)
+{
+ if(physsteps <= 0)
+ {
+ if(local) interppos(pl);
+ return;
+ }
+
+ if(local) pl->o = pl->newpos;
+ loopi(physsteps-1) moveplayer(pl, moveres, local, physframetime);
+ if(local) pl->deltapos = pl->o;
+ moveplayer(pl, moveres, local, physframetime);
+ if(local)
+ {
+ pl->newpos = pl->o;
+ pl->deltapos.sub(pl->newpos);
+ interppos(pl);
+ }
+}
+
+bool bounce(physent *d, float elasticity, float waterfric, float grav)
+{
+ if(physsteps <= 0)
+ {
+ interppos(d);
+ return false;
+ }
+
+ d->o = d->newpos;
+ bool hitplayer = false;
+ loopi(physsteps-1)
+ {
+ if(bounce(d, physframetime/1000.0f, elasticity, waterfric, grav)) hitplayer = true;
+ }
+ d->deltapos = d->o;
+ if(bounce(d, physframetime/1000.0f, elasticity, waterfric, grav)) hitplayer = true;
+ d->newpos = d->o;
+ d->deltapos.sub(d->newpos);
+ interppos(d);
+ return hitplayer;
+}
+
+void updatephysstate(physent *d)
+{
+ if(d->physstate == PHYS_FALL) return;
+ d->timeinair = 0;
+ vec old(d->o);
+ /* Attempt to reconstruct the floor state.
+ * May be inaccurate since movement collisions are not considered.
+ * If good floor is not found, just keep the old floor and hope it's correct enough.
+ */
+ switch(d->physstate)
+ {
+ case PHYS_SLOPE:
+ case PHYS_FLOOR:
+ case PHYS_STEP_DOWN:
+ d->o.z -= 0.15f;
+ if(collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? SLOPEZ : FLOORZ))
+ d->floor = collidewall;
+ break;
+
+ case PHYS_STEP_UP:
+ d->o.z -= STAIRHEIGHT+0.15f;
+ if(collide(d, vec(0, 0, -1), SLOPEZ))
+ d->floor = collidewall;
+ break;
+
+ case PHYS_SLIDE:
+ d->o.z -= 0.15f;
+ if(collide(d, vec(0, 0, -1)) && collidewall.z < SLOPEZ)
+ d->floor = collidewall;
+ break;
+ }
+ if(d->physstate > PHYS_FALL && d->floor.z <= 0) d->floor = vec(0, 0, 1);
+ d->o = old;
+}
+
+const float PLATFORMMARGIN = 0.2f;
+const float PLATFORMBORDER = 10.0f;
+
+struct platforment
+{
+ physent *d;
+ int stacks, chains;
+
+ platforment() {}
+ platforment(physent *d) : d(d), stacks(-1), chains(-1) {}
+
+ bool operator==(const physent *o) const { return d == o; }
+};
+
+struct platformcollision
+{
+ platforment *ent;
+ int next;
+
+ platformcollision() {}
+ platformcollision(platforment *ent, int next) : ent(ent), next(next) {}
+};
+
+template<class E, class O>
+static inline bool platformcollide(physent *d, const vec &dir, physent *o, float margin)
+{
+ E entvol(d);
+ O obvol(o, margin);
+ vec cp;
+ if(mpr::collide(entvol, obvol, NULL, NULL, &cp))
+ {
+ vec wn = vec(cp).sub(obvol.center());
+ return !obvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir).iszero();
+ }
+ return false;
+}
+
+bool platformcollide(physent *d, physent *o, const vec &dir, float margin = 0)
+{
+ if(d->collidetype == COLLIDE_OBB)
+ {
+ if(o->collidetype == COLLIDE_OBB) return platformcollide<mpr::EntOBB, mpr::EntOBB>(d, dir, o, margin);
+ else return platformcollide<mpr::EntOBB, mpr::EntCylinder>(d, dir, o, margin);
+
+ }
+ else if(o->collidetype == COLLIDE_OBB) return ellipseboxcollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight + margin);
+ else return ellipsecollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight + margin);
+}
+
+bool moveplatform(physent *p, const vec &dir)
+{
+ if(!insideworld(p->newpos)) return false;
+
+ vec oldpos(p->o);
+ (p->o = p->newpos).add(dir);
+ if(collide(p, dir, 0, dir.z<=0))
+ {
+ p->o = oldpos;
+ return false;
+ }
+ p->o = oldpos;
+
+ static vector<platforment> ents;
+ ents.setsize(0);
+ for(int x = int(max(p->o.x-p->radius-PLATFORMBORDER, 0.0f))>>dynentsize, ex = int(min(p->o.x+p->radius+PLATFORMBORDER, worldsize-1.0f))>>dynentsize; x <= ex; x++)
+ for(int y = int(max(p->o.y-p->radius-PLATFORMBORDER, 0.0f))>>dynentsize, ey = int(min(p->o.y+p->radius+PLATFORMBORDER, worldsize-1.0f))>>dynentsize; y <= ey; y++)
+ {
+ const vector<physent *> &dynents = checkdynentcache(x, y);
+ loopv(dynents)
+ {
+ physent *d = dynents[i];
+ if(p==d || d->o.z-d->eyeheight < p->o.z+p->aboveeye || p->o.reject(d->o, p->radius+PLATFORMBORDER+d->radius) || ents.find(d) >= 0) continue;
+ ents.add(d);
+ }
+ }
+ static vector<platforment *> passengers, colliders;
+ passengers.setsize(0);
+ colliders.setsize(0);
+ static vector<platformcollision> collisions;
+ collisions.setsize(0);
+ // build up collision DAG of colliders to be pushed off, and DAG of stacked passengers
+ loopv(ents)
+ {
+ platforment &ent = ents[i];
+ physent *d = ent.d;
+ // check if the dynent is on top of the platform
+ if(platformcollide(p, d, vec(0, 0, 1), PLATFORMMARGIN)) passengers.add(&ent);
+ vec doldpos(d->o);
+ (d->o = d->newpos).add(dir);
+ if(collide(d, dir, 0, false)) colliders.add(&ent);
+ d->o = doldpos;
+ loopvj(ents)
+ {
+ platforment &o = ents[j];
+ if(platformcollide(d, o.d, dir))
+ {
+ collisions.add(platformcollision(&ent, o.chains));
+ o.chains = collisions.length() - 1;
+ }
+ if(d->o.z < o.d->o.z && platformcollide(d, o.d, vec(0, 0, 1), PLATFORMMARGIN))
+ {
+ collisions.add(platformcollision(&o, ent.stacks));
+ ent.stacks = collisions.length() - 1;
+ }
+ }
+ }
+ loopv(colliders) // propagate collisions
+ {
+ platforment *ent = colliders[i];
+ for(int n = ent->chains; n>=0; n = collisions[n].next)
+ {
+ platforment *o = collisions[n].ent;
+ if(colliders.find(o)<0) colliders.add(o);
+ }
+ }
+ if(dir.z>0)
+ {
+ loopv(passengers) // if any stacked passengers collide, stop the platform
+ {
+ platforment *ent = passengers[i];
+ if(colliders.find(ent)>=0) return false;
+ for(int n = ent->stacks; n>=0; n = collisions[n].next)
+ {
+ platforment *o = collisions[n].ent;
+ if(passengers.find(o)<0) passengers.add(o);
+ }
+ }
+ loopv(passengers)
+ {
+ physent *d = passengers[i]->d;
+ d->o.add(dir);
+ d->newpos.add(dir);
+ if(dir.x || dir.y) updatedynentcache(d);
+ }
+ }
+ else loopv(passengers) // move any stacked passengers who aren't colliding with non-passengers
+ {
+ platforment *ent = passengers[i];
+ if(colliders.find(ent)>=0) continue;
+
+ physent *d = ent->d;
+ d->o.add(dir);
+ d->newpos.add(dir);
+ if(dir.x || dir.y) updatedynentcache(d);
+
+ for(int n = ent->stacks; n>=0; n = collisions[n].next)
+ {
+ platforment *o = collisions[n].ent;
+ if(passengers.find(o)<0) passengers.add(o);
+ }
+ }
+
+ p->o.add(dir);
+ p->newpos.add(dir);
+ if(dir.x || dir.y) updatedynentcache(p);
+
+ return true;
+}
+
+#define dir(name,v,d,s,os) ICOMMAND(name, "D", (int *down), { player->s = *down!=0; player->v = player->s ? d : (player->os ? -(d) : 0); });
+
+dir(backward, move, -1, k_down, k_up);
+dir(forward, move, 1, k_up, k_down);
+dir(left, strafe, 1, k_left, k_right);
+dir(right, strafe, -1, k_right, k_left);
+
+ICOMMAND(jump, "D", (int *down), { if(!*down || game::canjump()) player->jumping = *down!=0; });
+ICOMMAND(attack, "D", (int *down), { game::doattack(*down!=0); });
+
+bool entinmap(dynent *d, bool avoidplayers) // brute force but effective way to find a free spawn spot in the map
+{
+ d->o.z += d->eyeheight; // pos specified is at feet
+ vec orig = d->o;
+ loopi(100) // try max 100 times
+ {
+ if(i)
+ {
+ d->o = orig;
+ d->o.x += (rnd(21)-10)*i/5; // increasing distance
+ d->o.y += (rnd(21)-10)*i/5;
+ d->o.z += (rnd(21)-10)*i/5;
+ }
+
+ if(!collide(d) && !collideinside)
+ {
+ if(collideplayer)
+ {
+ if(!avoidplayers) continue;
+ d->o = orig;
+ d->resetinterp();
+ return false;
+ }
+
+ d->resetinterp();
+ return true;
+ }
+ }
+ // leave ent at original pos, possibly stuck
+ d->o = orig;
+ d->resetinterp();
+ conoutf(CON_WARN, "can't find entity spawn spot! (%.1f, %.1f, %.1f)", d->o.x, d->o.y, d->o.z);
+ return false;
+}
+
diff --git a/src/engine/pvs.cpp b/src/engine/pvs.cpp
new file mode 100644
index 0000000..0f29bf5
--- /dev/null
+++ b/src/engine/pvs.cpp
@@ -0,0 +1,1315 @@
+#include "engine.h"
+
+enum
+{
+ PVS_HIDE_GEOM = 1<<0,
+ PVS_HIDE_BB = 1<<1
+};
+
+struct pvsnode
+{
+ bvec edges;
+ uchar flags;
+ uint children;
+};
+
+static vector<pvsnode> origpvsnodes;
+
+static bool mergepvsnodes(pvsnode &p, pvsnode *children)
+{
+ loopi(7) if(children[i].flags!=children[7].flags) return false;
+ bvec bbs[4];
+ loop(x, 2) loop(y, 2)
+ {
+ const bvec &lo = children[octaindex(2, x, y, 0)].edges,
+ &hi = children[octaindex(2, x, y, 1)].edges;
+ if(lo.x!=0xFF && (lo.x&0x11 || lo.y&0x11 || lo.z&0x11)) return false;
+ if(hi.x!=0xFF && (hi.x&0x11 || hi.y&0x11 || hi.z&0x11)) return false;
+
+#define MERGEBBS(res, coord, row, col) \
+ if(lo.coord==0xFF) \
+ { \
+ if(hi.coord!=0xFF) \
+ { \
+ res.coord = ((hi.coord&~0x11)>>1) + 0x44; \
+ res.row = hi.row; \
+ res.col = hi.col; \
+ } \
+ } \
+ else if(hi.coord==0xFF) \
+ { \
+ res.coord = (lo.coord&0xEE)>>1; \
+ res.row = lo.row; \
+ res.col = lo.col; \
+ } \
+ else if(lo.row!=hi.row || lo.col!=hi.col || (lo.coord&0xF0)!=0x80 || (hi.coord&0xF)!=0) return false; \
+ else \
+ { \
+ res.coord = ((lo.coord&~0xF1)>>1) | (((hi.coord&~0x1F)>>1) + 0x40); \
+ res.row = lo.row; \
+ res.col = lo.col; \
+ }
+
+ bvec &res = bbs[x + 2*y];
+ MERGEBBS(res, z, x, y);
+ res.x = lo.x;
+ res.y = lo.y;
+ }
+ loop(x, 2)
+ {
+ bvec &lo = bbs[x], &hi = bbs[x+2];
+ MERGEBBS(lo, y, x, z);
+ }
+ bvec &lo = bbs[0], &hi = bbs[1];
+ MERGEBBS(p.edges, x, y, z);
+
+ return true;
+}
+
+static void genpvsnodes(cube *c, int parent = 0, const ivec &co = ivec(0, 0, 0), int size = worldsize/2)
+{
+ int index = origpvsnodes.length();
+ loopi(8)
+ {
+ ivec o(i, co, size);
+ pvsnode &n = origpvsnodes.add();
+ n.flags = 0;
+ n.children = 0;
+ if(c[i].children || isempty(c[i]) || c[i].material&MAT_ALPHA) memset(n.edges.v, 0xFF, 3);
+ else loopk(3)
+ {
+ uint face = c[i].faces[k];
+ if(face==F_SOLID) n.edges[k] = 0x80;
+ else
+ {
+ uchar low = max(max(face&0xF, (face>>8)&0xF), max((face>>16)&0xF, (face>>24)&0xF)),
+ high = min(min((face>>4)&0xF, (face>>12)&0xF), min((face>>20)&0xF, (face>>28)&0xF));
+ if(size<8)
+ {
+ if(low&((8/size)-1)) { low += 8/size - (low&((8/size)-1)); }
+ if(high&((8/size)-1)) high &= ~(8/size-1);
+ }
+ if(low >= high) { memset(n.edges.v, 0xFF, 3); break; }
+ n.edges[k] = low | (high<<4);
+ }
+ }
+ }
+ int branches = 0;
+ loopi(8) if(c[i].children)
+ {
+ ivec o(i, co, size);
+ genpvsnodes(c[i].children, index+i, o, size>>1);
+ if(origpvsnodes[index+i].children) branches++;
+ }
+ if(!branches && mergepvsnodes(origpvsnodes[parent], &origpvsnodes[index])) origpvsnodes.setsize(index);
+ else origpvsnodes[parent].children = index;
+}
+
+struct shaftplane
+{
+ float r, c, offset;
+ uchar rnear, cnear, rfar, cfar;
+};
+
+struct shaftbb
+{
+ union
+ {
+ ushort v[6];
+ struct { usvec min, max; };
+ };
+
+ shaftbb() {}
+ shaftbb(const ivec &o, int size)
+ {
+ min.x = o.x;
+ min.y = o.y;
+ min.z = o.z;
+ max.x = o.x + size;
+ max.y = o.y + size;
+ max.z = o.z + size;
+ }
+ shaftbb(const ivec &o, int size, const bvec &edges)
+ {
+ min.x = o.x + (size*(edges.x&0xF))/8;
+ min.y = o.y + (size*(edges.y&0xF))/8;
+ min.z = o.z + (size*(edges.z&0xF))/8;
+ max.x = o.x + (size*(edges.x>>4))/8;
+ max.y = o.y + (size*(edges.y>>4))/8;
+ max.z = o.z + (size*(edges.z>>4))/8;
+ }
+
+ ushort &operator[](int i) { return v[i]; }
+ ushort operator[](int i) const { return v[i]; }
+
+ bool contains(const shaftbb &o) const
+ {
+ return min.x<=o.min.x && min.y<=o.min.y && min.z<=o.min.z &&
+ max.x>=o.max.x && max.y>=o.max.y && max.z>=o.max.z;
+ }
+
+ bool outside(const ivec &o, int size) const
+ {
+ return o.x>=max.x || o.y>=max.y || o.z>=max.z ||
+ o.x+size<=min.x || o.y+size<=min.y || o.z+size<=min.z;
+ }
+
+ bool outside(const shaftbb &o) const
+ {
+ return o.min.x>max.x || o.min.y>max.y || o.min.z>max.z ||
+ o.max.x<min.x || o.max.y<min.y || o.max.z<min.z;
+ }
+
+ bool notinside(const shaftbb &o) const
+ {
+ return o.min.x<min.x || o.min.y<min.y || o.min.z<min.z ||
+ o.max.x>max.x || o.max.y>max.y || o.max.z>max.z;
+ }
+};
+
+struct shaft
+{
+ shaftbb bounds;
+ shaftplane planes[8];
+ int numplanes;
+
+ shaft(const shaftbb &from, const shaftbb &to)
+ {
+ calcshaft(from, to);
+ }
+
+ void calcshaft(const shaftbb &from, const shaftbb &to)
+ {
+ uchar match = 0, color = 0;
+ loopi(3)
+ {
+ if(to.min[i] < from.min[i]) { color |= 1<<i; bounds.min[i] = 0; }
+ else if(to.min[i] > from.min[i]) bounds.min[i] = to.min[i]+1;
+ else { match |= 1<<i; bounds.min[i] = to.min[i]; }
+
+ if(to.max[i] > from.max[i]) { color |= 8<<i; bounds.max[i] = USHRT_MAX; }
+ else if(to.max[i] < from.max[i]) bounds.max[i] = to.max[i]-1;
+ else { match |= 8<<i; bounds.max[i] = to.max[i]; }
+ }
+ numplanes = 0;
+ loopi(5) if(!(match&(1<<i))) for(int j = i+1; j<6; j++) if(!(match&(1<<j)) && i+3!=j && ((color>>i)^(color>>j))&1)
+ {
+ int r = i%3, c = j%3, d = (r+1)%3;
+ if(d==c) d = (c+1)%3;
+ shaftplane &p = planes[numplanes++];
+ p.r = from[j] - to[j];
+ if(i<3 ? p.r >= 0 : p.r < 0)
+ {
+ p.r = -p.r;
+ p.c = from[i] - to[i];
+ }
+ else p.c = to[i] - from[i];
+ p.offset = -(from[i]*p.r + from[j]*p.c);
+ p.rnear = p.r >= 0 ? r : 3+r;
+ p.cnear = p.c >= 0 ? c : 3+c;
+ p.rfar = p.r < 0 ? r : 3+r;
+ p.cfar = p.c < 0 ? c : 3+c;
+ }
+ }
+
+ bool outside(const shaftbb &o) const
+ {
+ if(bounds.outside(o)) return true;
+
+ for(const shaftplane *p = planes; p < &planes[numplanes]; p++)
+ {
+ if(o[p->rnear]*p->r + o[p->cnear]*p->c + p->offset > 0) return true;
+ }
+ return false;
+ }
+
+ bool inside(const shaftbb &o) const
+ {
+ if(bounds.notinside(o)) return false;
+
+ for(const shaftplane *p = planes; p < &planes[numplanes]; p++)
+ {
+ if(o[p->rfar]*p->r + o[p->cfar]*p->c + p->offset > 0) return false;
+ }
+ return true;
+ }
+};
+
+struct pvsdata
+{
+ int offset, len;
+
+ pvsdata() {}
+ pvsdata(int offset, int len) : offset(offset), len(len) {}
+};
+
+static vector<uchar> pvsbuf;
+
+static inline uint hthash(const pvsdata &k)
+{
+ uint h = 5381;
+ loopi(k.len) h = ((h<<5)+h)^pvsbuf[k.offset+i];
+ return h;
+}
+
+static inline bool htcmp(const pvsdata &x, const pvsdata &y)
+{
+ return x.len==y.len && !memcmp(&pvsbuf[x.offset], &pvsbuf[y.offset], x.len);
+}
+
+static SDL_mutex *pvsmutex = NULL;
+static hashtable<pvsdata, int> pvscompress;
+static vector<pvsdata> pvs;
+
+static SDL_mutex *viewcellmutex = NULL;
+struct viewcellrequest
+{
+ int *result;
+ ivec o;
+ int size;
+};
+static vector<viewcellrequest> viewcellrequests;
+
+static bool genpvs_canceled = false;
+static int numviewcells = 0;
+
+VAR(maxpvsblocker, 1, 512, 1<<16);
+VAR(pvsleafsize, 1, 64, 1024);
+
+#define MAXWATERPVS 32
+
+static struct
+{
+ int height;
+ vector<materialsurface *> matsurfs;
+} waterplanes[MAXWATERPVS];
+static vector<materialsurface *> waterfalls;
+uint numwaterplanes = 0;
+
+struct pvsworker
+{
+ pvsworker() : thread(NULL), pvsnodes(new pvsnode[origpvsnodes.length()])
+ {
+ }
+ ~pvsworker()
+ {
+ delete[] pvsnodes;
+ }
+
+ SDL_Thread *thread;
+ pvsnode *pvsnodes;
+
+ shaftbb viewcellbb;
+
+ pvsnode *levels[32];
+ int curlevel;
+ ivec origin;
+
+ void resetlevels()
+ {
+ curlevel = worldscale;
+ levels[curlevel] = &pvsnodes[0];
+ origin = ivec(0, 0, 0);
+ }
+
+ int hasvoxel(const ivec &p, int coord, int dir, int ocoord = 0, int odir = 0, int *omin = NULL)
+ {
+ uint diff = (origin.x^p.x)|(origin.y^p.y)|(origin.z^p.z);
+ if(diff >= uint(worldsize)) return 0;
+ diff >>= curlevel;
+ while(diff)
+ {
+ curlevel++;
+ diff >>= 1;
+ }
+
+ pvsnode *cur = levels[curlevel];
+ while(cur->children && !(cur->flags&PVS_HIDE_BB))
+ {
+ cur = &pvsnodes[cur->children];
+ curlevel--;
+ cur += ((p.z>>(curlevel-2))&4) | ((p.y>>(curlevel-1))&2) | ((p.x>>curlevel)&1);
+ levels[curlevel] = cur;
+ }
+
+ origin = ivec(p.x&(~0U<<curlevel), p.y&(~0U<<curlevel), p.z&(~0U<<curlevel));
+
+ if(cur->flags&PVS_HIDE_BB || cur->edges==bvec(0x80, 0x80, 0x80))
+ {
+ if(omin)
+ {
+ int step = origin[ocoord] + (odir<<curlevel) - p[ocoord] + odir - 1;
+ if(odir ? step < *omin : step > *omin) *omin = step;
+ }
+ return origin[coord] + (dir<<curlevel) - p[coord] + dir - 1;
+ }
+
+ if(cur->edges.x==0xFF) return 0;
+ ivec bbp(p);
+ bbp.sub(origin);
+ ivec bbmin, bbmax;
+ bbmin.x = ((cur->edges.x&0xF)<<curlevel)/8;
+ if(bbp.x < bbmin.x) return 0;
+ bbmax.x = ((cur->edges.x>>4)<<curlevel)/8;
+ if(bbp.x >= bbmax.x) return 0;
+ bbmin.y = ((cur->edges.y&0xF)<<curlevel)/8;
+ if(bbp.y < bbmin.y) return 0;
+ bbmax.y = ((cur->edges.y>>4)<<curlevel)/8;
+ if(bbp.y >= bbmax.y) return 0;
+ bbmin.z = ((cur->edges.z&0xF)<<curlevel)/8;
+ if(bbp.z < bbmin.z) return 0;
+ bbmax.z = ((cur->edges.z>>4)<<curlevel)/8;
+ if(bbp.z >= bbmax.z) return 0;
+
+ if(omin)
+ {
+ int step = (odir ? bbmax[ocoord] : bbmin[ocoord]) - bbp[ocoord] + (odir - 1);
+ if(odir ? step < *omin : step > *omin) *omin = step;
+ }
+ return (dir ? bbmax[coord] : bbmin[coord]) - bbp[coord] + (dir - 1);
+ }
+
+ void hidepvs(pvsnode &p)
+ {
+ if(p.children)
+ {
+ pvsnode *children = &pvsnodes[p.children];
+ loopi(8) hidepvs(children[i]);
+ p.flags |= PVS_HIDE_BB;
+ return;
+ }
+ p.flags |= PVS_HIDE_BB;
+ if(p.edges.x!=0xFF) p.flags |= PVS_HIDE_GEOM;
+ }
+
+ void shaftcullpvs(shaft &s, pvsnode &p, const ivec &co = ivec(0, 0, 0), int size = worldsize)
+ {
+ if(p.flags&PVS_HIDE_BB) return;
+ shaftbb bb(co, size);
+ if(s.outside(bb)) return;
+ if(s.inside(bb)) { hidepvs(p); return; }
+ if(p.children)
+ {
+ pvsnode *children = &pvsnodes[p.children];
+ uchar flags = 0xFF;
+ loopi(8)
+ {
+ ivec o(i, co, size>>1);
+ shaftcullpvs(s, children[i], o, size>>1);
+ flags &= children[i].flags;
+ }
+ if(flags & PVS_HIDE_BB) p.flags |= PVS_HIDE_BB;
+ return;
+ }
+ if(p.edges.x==0xFF) return;
+ shaftbb geom(co, size, p.edges);
+ if(s.inside(geom)) p.flags |= PVS_HIDE_GEOM;
+ }
+
+ queue<shaftbb, 32> prevblockers;
+
+ struct cullorder
+ {
+ int index, dist;
+
+ cullorder() {}
+ cullorder(int index, int dist) : index(index), dist(dist) {}
+ };
+
+ void cullpvs(pvsnode &p, const ivec &co = ivec(0, 0, 0), int size = worldsize)
+ {
+ if(p.flags&(PVS_HIDE_BB | PVS_HIDE_GEOM) || genpvs_canceled) return;
+ if(p.children && !(p.flags&PVS_HIDE_BB))
+ {
+ pvsnode *children = &pvsnodes[p.children];
+ int csize = size>>1;
+ ivec dmin = ivec(co).add(csize>>1).sub(ivec(viewcellbb.min).add(ivec(viewcellbb.max)).shr(1)), dmax = ivec(dmin).add(csize);
+ dmin.mul(dmin);
+ dmax.mul(dmax);
+ ivec diff = ivec(dmax).sub(dmin);
+ cullorder order[8];
+ int dir = 0;
+ if(diff.x < 0) { diff.x = -diff.x; dir |= 1; }
+ if(diff.y < 0) { diff.y = -diff.y; dir |= 2; }
+ if(diff.z < 0) { diff.z = -diff.z; dir |= 4; }
+ order[0] = cullorder(0, 0);
+ order[7] = cullorder(7, diff.x + diff.y + diff.z);
+ order[1] = cullorder(1, diff.x);
+ order[2] = cullorder(2, diff.y);
+ order[3] = cullorder(4, diff.z);
+ if(order[2].dist < order[1].dist) swap(order[1], order[2]);
+ if(order[3].dist < order[2].dist) swap(order[2], order[3]);
+ if(order[2].dist < order[1].dist) swap(order[1], order[2]);
+ cullorder dxy(order[1].index|order[2].index, order[1].dist+order[2].dist),
+ dxz(order[1].index|order[3].index, order[1].dist+order[3].dist),
+ dyz(order[2].index|order[3].index, order[2].dist+order[3].dist);
+ int j;
+ for(j = 4; j > 0 && dxy.dist < order[j-1].dist; --j) order[j] = order[j-1];
+ order[j] = dxy;
+ for(j = 5; j > 0 && dxz.dist < order[j-1].dist; --j) order[j] = order[j-1];
+ order[j] = dxz;
+ for(j = 6; j > 0 && dyz.dist < order[j-1].dist; --j) order[j] = order[j-1];
+ order[j] = dyz;
+ loopi(8)
+ {
+ int index = order[i].index^dir;
+ ivec o(index, co, csize);
+ cullpvs(children[index], o, csize);
+ }
+ if(!(p.flags & PVS_HIDE_BB)) return;
+ }
+ bvec edges = p.children ? bvec(0x80, 0x80, 0x80) : p.edges;
+ if(edges.x==0xFF) return;
+ shaftbb geom(co, size, edges);
+ ivec diff = ivec(geom.max).sub(ivec(viewcellbb.min)).abs();
+ cullorder order[3] = { cullorder(0, diff.x), cullorder(1, diff.y), cullorder(2, diff.z) };
+ if(order[1].dist > order[0].dist) swap(order[0], order[1]);
+ if(order[2].dist > order[1].dist) swap(order[1], order[2]);
+ if(order[1].dist > order[0].dist) swap(order[0], order[1]);
+ loopi(6)
+ {
+ int dim = order[i >= 3 ? i-3 : i].index, dc = (i >= 3) != (geom.max[dim] <= viewcellbb.min[dim]) ? 1 : 0, r = R[dim], c = C[dim];
+ int ccenter = geom.min[c];
+ if(geom.min[r]==geom.max[r] || geom.min[c]==geom.max[c]) continue;
+ while(ccenter < geom.max[c])
+ {
+ ivec rmin;
+ rmin[dim] = geom[dim + 3*dc] + (dc ? -1 : 0);
+ rmin[r] = geom.min[r];
+ rmin[c] = ccenter;
+ ivec rmax = rmin;
+ rmax[r] = geom.max[r] - 1;
+ int rcenter = (rmin[r] + rmax[r])/2;
+ resetlevels();
+ for(int minstep = -1, maxstep = 1; (minstep || maxstep) && rmax[r] - rmin[r] < maxpvsblocker;)
+ {
+ if(minstep) minstep = hasvoxel(rmin, r, 0);
+ if(maxstep) maxstep = hasvoxel(rmax, r, 1);
+ rmin[r] += minstep;
+ rmax[r] += maxstep;
+ }
+ rmin[r] = rcenter + (rmin[r] - rcenter)/2;
+ rmax[r] = rcenter + (rmax[r] - rcenter)/2;
+ if(rmin[r]>=geom.min[r] && rmax[r]<geom.max[r]) { rmin[r] = geom.min[r]; rmax[r] = geom.max[r] - 1; }
+ ivec cmin = rmin, cmax = rmin;
+ if(rmin[r]>=geom.min[r] && rmax[r]<geom.max[r])
+ {
+ cmin[c] = geom.min[c];
+ cmax[c] = geom.max[c]-1;
+ }
+ int cminstep = -1, cmaxstep = 1;
+ for(; (cminstep || cmaxstep) && cmax[c] - cmin[c] < maxpvsblocker;)
+ {
+ if(cminstep)
+ {
+ cmin[c] += cminstep; cminstep = INT_MIN;
+ cmin[r] = rmin[r];
+ resetlevels();
+ for(int rstep = 1; rstep && cmin[r] <= rmax[r];)
+ {
+ rstep = hasvoxel(cmin, r, 1, c, 0, &cminstep);
+ cmin[r] += rstep;
+ }
+ if(cmin[r] <= rmax[r]) cminstep = 0;
+ }
+ if(cmaxstep)
+ {
+ cmax[c] += cmaxstep; cmaxstep = INT_MAX;
+ cmax[r] = rmin[r];
+ resetlevels();
+ for(int rstep = 1; rstep && cmax[r] <= rmax[r];)
+ {
+ rstep = hasvoxel(cmax, r, 1, c, 1, &cmaxstep);
+ cmax[r] += rstep;
+ }
+ if(cmax[r] <= rmax[r]) cmaxstep = 0;
+ }
+ }
+ if(!cminstep) cmin[c]++;
+ if(!cmaxstep) cmax[c]--;
+ ivec emin = rmin, emax = rmax;
+ if(cmin[c]>=geom.min[c] && cmax[c]<geom.max[c])
+ {
+ if(emin[r]>geom.min[r]) emin[r] = geom.min[r];
+ if(emax[r]<geom.max[r]-1) emax[r] = geom.max[r]-1;
+ }
+ int rminstep = -1, rmaxstep = 1;
+ for(; (rminstep || rmaxstep) && emax[r] - emin[r] < maxpvsblocker;)
+ {
+ if(rminstep)
+ {
+ emin[r] += -1; rminstep = INT_MIN;
+ emin[c] = cmin[c];
+ resetlevels();
+ for(int cstep = 1; cstep && emin[c] <= cmax[c];)
+ {
+ cstep = hasvoxel(emin, c, 1, r, 0, &rminstep);
+ emin[c] += cstep;
+ }
+ if(emin[c] <= cmax[c]) rminstep = 0;
+ }
+ if(rmaxstep)
+ {
+ emax[r] += 1; rmaxstep = INT_MAX;
+ emax[c] = cmin[c];
+ resetlevels();
+ for(int cstep = 1; cstep && emax[c] <= cmax[c];)
+ {
+ cstep = hasvoxel(emax, c, 1, r, 1, &rmaxstep);
+ emax[c] += cstep;
+ }
+ if(emax[c] <= cmax[c]) rmaxstep = 0;
+ }
+ }
+ if(!rminstep) emin[r]++;
+ if(!rmaxstep) emax[r]--;
+ shaftbb bb;
+ bb.min[dim] = rmin[dim];
+ bb.max[dim] = rmin[dim]+1;
+ bb.min[r] = emin[r];
+ bb.max[r] = emax[r]+1;
+ bb.min[c] = cmin[c];
+ bb.max[c] = cmax[c]+1;
+ if(bb.min[dim] >= viewcellbb.max[dim] || bb.max[dim] <= viewcellbb.min[dim])
+ {
+ int ddir = bb.min[dim] >= viewcellbb.max[dim] ? 1 : -1,
+ dval = ddir>0 ? USHRT_MAX-1 : 0,
+ dlimit = maxpvsblocker,
+ numsides = 0;
+ loopj(4)
+ {
+ ivec dmax;
+ int odim = j < 2 ? c : r;
+ if(j&1)
+ {
+ if(bb.max[odim] >= viewcellbb.max[odim]) continue;
+ dmax[odim] = bb.max[odim]-1;
+ }
+ else
+ {
+ if(bb.min[odim] <= viewcellbb.min[odim]) continue;
+ dmax[odim] = bb.min[odim];
+ }
+ numsides++;
+ dmax[dim] = bb.min[dim];
+ int stepdim = j < 2 ? r : c, stepstart = bb.min[stepdim], stepend = bb.max[stepdim];
+ int dstep = ddir;
+ for(; dstep && ddir*(dmax[dim] - (int)bb.min[dim]) < dlimit;)
+ {
+ dmax[dim] += dstep; dstep = ddir > 0 ? INT_MAX : INT_MIN;
+ dmax[stepdim] = stepstart;
+ resetlevels();
+ for(int step = 1; step && dmax[stepdim] < stepend;)
+ {
+ step = hasvoxel(dmax, stepdim, 1, dim, (ddir+1)/2, &dstep);
+ dmax[stepdim] += step;
+ }
+ if(dmax[stepdim] < stepend) dstep = 0;
+ }
+ dlimit = min(dlimit, ddir*(dmax[dim] - (int)bb.min[dim]));
+ if(!dstep) dmax[dim] -= ddir;
+ if(ddir>0) dval = min(dval, dmax[dim]);
+ else dval = max(dval, dmax[dim]);
+ }
+ if(numsides>0)
+ {
+ if(ddir>0) bb.max[dim] = dval+1;
+ else bb.min[dim] = dval;
+ }
+ //printf("(%d,%d,%d) x %d,%d,%d, side %d, ccenter = %d, origin = (%d,%d,%d), size = %d\n", bb.min.x, bb.min.y, bb.min.z, bb.max.x-bb.min.x, bb.max.y-bb.min.y, bb.max.z-bb.min.z, i, ccenter, co.x, co.y, co.z, size);
+ }
+ bool dup = false;
+ loopvj(prevblockers)
+ {
+ if(prevblockers[j].contains(bb)) { dup = true; break; }
+ }
+ if(!dup)
+ {
+ shaft s(viewcellbb, bb);
+ shaftcullpvs(s, pvsnodes[0]);
+ prevblockers.add(bb);
+ }
+ if(bb.contains(geom)) return;
+ ccenter = cmax[c] + 1;
+ }
+ }
+ }
+
+ bool compresspvs(pvsnode &p, int size, int threshold)
+ {
+ if(!p.children) return true;
+ if(p.flags&PVS_HIDE_BB) { p.children = 0; return true; }
+ pvsnode *children = &pvsnodes[p.children];
+ bool canreduce = true;
+ loopi(8)
+ {
+ if(!compresspvs(children[i], size/2, threshold)) canreduce = false;
+ }
+ if(canreduce)
+ {
+ int hide = children[7].flags&PVS_HIDE_BB;
+ loopi(7) if((children[i].flags&PVS_HIDE_BB)!=hide) canreduce = false;
+ if(canreduce)
+ {
+ p.flags = (p.flags & ~PVS_HIDE_BB) | hide;
+ p.children = 0;
+ return true;
+ }
+ }
+ if(size <= threshold)
+ {
+ p.children = 0;
+ return true;
+ }
+ return false;
+ }
+
+ vector<uchar> outbuf;
+
+ bool serializepvs(pvsnode &p, int storage = -1)
+ {
+ if(!p.children)
+ {
+ outbuf.add(0xFF);
+ loopi(8) outbuf.add(p.flags&PVS_HIDE_BB ? 0xFF : 0);
+ return true;
+ }
+ int index = outbuf.length();
+ pvsnode *children = &pvsnodes[p.children];
+ int i = 0;
+ uchar leafvalues = 0;
+ if(storage>=0)
+ {
+ for(; i < 8; i++)
+ {
+ pvsnode &child = children[i];
+ if(child.flags&PVS_HIDE_BB) leafvalues |= 1<<i;
+ else if(child.children) break;
+ }
+ if(i==8) { outbuf[storage] = leafvalues; return false; }
+ // if offset won't fit, just mark the space as a visible to avoid problems
+ int offset = (index - storage + 8)/9;
+ if(offset>255) { outbuf[storage] = 0; return false; }
+ outbuf[storage] = uchar(offset);
+ }
+ outbuf.add(0);
+ loopj(8) outbuf.add(leafvalues&(1<<j) ? 0xFF : 0);
+ uchar leafmask = (1<<i)-1;
+ for(; i < 8; i++)
+ {
+ pvsnode &child = children[i];
+ if(child.children) { if(!serializepvs(child, index+1+i)) leafmask |= 1<<i; }
+ else { leafmask |= 1<<i; outbuf[index+1+i] = child.flags&PVS_HIDE_BB ? 0xFF : 0; }
+ }
+ outbuf[index] = leafmask;
+ return true;
+ }
+
+ bool materialoccluded(pvsnode &p, const ivec &co, int size, const ivec &bbmin, const ivec &bbmax)
+ {
+ pvsnode *children = &pvsnodes[p.children];
+ loopoctabox(co, size, bbmin, bbmax)
+ {
+ ivec o(i, co, size);
+ if(children[i].flags & PVS_HIDE_BB) continue;
+ if(!children[i].children || !materialoccluded(children[i], o, size/2, bbmin, bbmax)) return false;
+ }
+ return true;
+ }
+
+ bool materialoccluded(vector<materialsurface *> &matsurfs)
+ {
+ if(pvsnodes[0].flags & PVS_HIDE_BB) return true;
+ if(!pvsnodes[0].children) return false;
+ loopv(matsurfs)
+ {
+ materialsurface &m = *matsurfs[i];
+ ivec bbmin(m.o), bbmax(m.o);
+ int dim = dimension(m.orient);
+ bbmin[dim] += dimcoord(m.orient) ? -2 : 2;
+ bbmax[C[dim]] += m.csize;
+ bbmax[R[dim]] += m.rsize;
+ if(!materialoccluded(pvsnodes[0], ivec(0, 0, 0), worldsize/2, bbmin, bbmax)) return false;
+ }
+ return true;
+ }
+
+ int wateroccluded, waterbytes;
+
+ void calcpvs(const ivec &co, int size)
+ {
+ loopk(3)
+ {
+ viewcellbb.min[k] = co[k];
+ viewcellbb.max[k] = co[k]+size;
+ }
+ memcpy(pvsnodes, origpvsnodes.getbuf(), origpvsnodes.length()*sizeof(pvsnode));
+ prevblockers.clear();
+ cullpvs(pvsnodes[0]);
+
+ wateroccluded = 0;
+ loopi(numwaterplanes)
+ {
+ if(waterplanes[i].height < 0)
+ {
+ if(waterfalls.length() && materialoccluded(waterfalls)) wateroccluded |= 1<<i;
+ }
+ else if(waterplanes[i].matsurfs.length() && materialoccluded(waterplanes[i].matsurfs)) wateroccluded |= 1<<i;
+ }
+ waterbytes = 0;
+ loopi(4) if(wateroccluded&(0xFF<<(i*8))) waterbytes = i+1;
+
+ compresspvs(pvsnodes[0], worldsize, pvsleafsize);
+ outbuf.setsize(0);
+ serializepvs(pvsnodes[0]);
+ }
+
+ uchar *testviewcell(const ivec &co, int size, int *waterpvs = NULL, int *len = NULL)
+ {
+ calcpvs(co, size);
+
+ uchar *buf = new uchar[outbuf.length()];
+ memcpy(buf, outbuf.getbuf(), outbuf.length());
+ if(waterpvs) *waterpvs = wateroccluded;
+ if(len) *len = outbuf.length();
+ return buf;
+ }
+
+ int genviewcell(const ivec &co, int size)
+ {
+ calcpvs(co, size);
+
+ if(pvsmutex) SDL_LockMutex(pvsmutex);
+ numviewcells++;
+ pvsdata key(pvsbuf.length(), waterbytes + outbuf.length());
+ loopi(waterbytes) pvsbuf.add((wateroccluded>>(i*8))&0xFF);
+ pvsbuf.put(outbuf.getbuf(), outbuf.length());
+ int *val = pvscompress.access(key);
+ if(val) pvsbuf.setsize(key.offset);
+ else
+ {
+ val = &pvscompress[key];
+ *val = pvs.length();
+ pvs.add(key);
+ }
+ if(pvsmutex) SDL_UnlockMutex(pvsmutex);
+ return *val;
+ }
+
+ static int run(void *data)
+ {
+ pvsworker *w = (pvsworker *)data;
+ SDL_LockMutex(viewcellmutex);
+ while(viewcellrequests.length())
+ {
+ viewcellrequest req = viewcellrequests.pop();
+ SDL_UnlockMutex(viewcellmutex);
+ int result = w->genviewcell(req.o, req.size);
+ SDL_LockMutex(viewcellmutex);
+ *req.result = result;
+ }
+ SDL_UnlockMutex(viewcellmutex);
+ return 0;
+ }
+};
+
+struct viewcellnode
+{
+ uchar leafmask;
+ union viewcellchild
+ {
+ int pvs;
+ viewcellnode *node;
+ } children[8];
+
+ viewcellnode() : leafmask(0xFF)
+ {
+ loopi(8) children[i].pvs = -1;
+ }
+ ~viewcellnode()
+ {
+ loopi(8) if(!(leafmask&(1<<i))) delete children[i].node;
+ }
+};
+
+VARP(pvsthreads, 0, 0, 16);
+static vector<pvsworker *> pvsworkers;
+
+static volatile bool check_genpvs_progress = false;
+
+static Uint32 genpvs_timer(Uint32 interval, void *param)
+{
+ check_genpvs_progress = true;
+ return interval;
+}
+
+static int totalviewcells = 0;
+
+static void show_genpvs_progress(int unique = pvs.length(), int processed = numviewcells)
+{
+ float bar1 = float(processed) / float(totalviewcells>0 ? totalviewcells : 1);
+
+ defformatstring(text1, "%d%% - %d of %d view cells (%d unique)", int(bar1 * 100), processed, totalviewcells, unique);
+
+ renderprogress(bar1, text1);
+
+ if(interceptkey(SDLK_ESCAPE)) genpvs_canceled = true;
+ check_genpvs_progress = false;
+}
+
+static shaftbb pvsbounds;
+
+static void calcpvsbounds()
+{
+ loopk(3) pvsbounds.min[k] = USHRT_MAX;
+ loopk(3) pvsbounds.max[k] = 0;
+ loopv(valist)
+ {
+ vtxarray *va = valist[i];
+ loopk(3)
+ {
+ if(va->geommin[k]>va->geommax[k]) continue;
+ pvsbounds.min[k] = min(pvsbounds.min[k], (ushort)va->geommin[k]);
+ pvsbounds.max[k] = max(pvsbounds.max[k], (ushort)va->geommax[k]);
+ }
+ }
+}
+
+static inline bool isallclip(cube *c)
+{
+ loopi(8)
+ {
+ cube &h = c[i];
+ if(h.children ? !isallclip(h.children) : (!isentirelysolid(h) && (h.material&MATF_CLIP)!=MAT_CLIP))
+ return false;
+ }
+ return true;
+}
+
+static int countviewcells(cube *c, const ivec &co, int size, int threshold)
+{
+ int count = 0;
+ loopi(8)
+ {
+ ivec o(i, co, size);
+ if(pvsbounds.outside(o, size)) continue;
+ cube &h = c[i];
+ if(h.children)
+ {
+ if(size>threshold)
+ {
+ count += countviewcells(h.children, o, size>>1, threshold);
+ continue;
+ }
+ if(isallclip(h.children)) continue;
+ }
+ else if(isentirelysolid(h) || (h.material&MATF_CLIP)==MAT_CLIP) continue;
+ count++;
+ }
+ return count;
+}
+
+static void genviewcells(viewcellnode &p, cube *c, const ivec &co, int size, int threshold)
+{
+ if(genpvs_canceled) return;
+ loopi(8)
+ {
+ ivec o(i, co, size);
+ if(pvsbounds.outside(o, size)) continue;
+ cube &h = c[i];
+ if(h.children)
+ {
+ if(size>threshold)
+ {
+ p.leafmask &= ~(1<<i);
+ p.children[i].node = new viewcellnode;
+ genviewcells(*p.children[i].node, h.children, o, size>>1, threshold);
+ continue;
+ }
+ if(isallclip(h.children)) continue;
+ }
+ else if(isentirelysolid(h) || (h.material&MATF_CLIP)==MAT_CLIP) continue;
+ if(pvsworkers.length())
+ {
+ if(genpvs_canceled) return;
+ p.children[i].pvs = pvsworkers[0]->genviewcell(o, size);
+ if(check_genpvs_progress) show_genpvs_progress();
+ }
+ else
+ {
+ viewcellrequest &req = viewcellrequests.add();
+ req.result = &p.children[i].pvs;
+ req.o = o;
+ req.size = size;
+ }
+ }
+}
+
+static viewcellnode *viewcells = NULL;
+static int lockedwaterplanes[MAXWATERPVS];
+static uchar *curpvs = NULL, *lockedpvs = NULL;
+static int curwaterpvs = 0, lockedwaterpvs = 0;
+
+static inline pvsdata *lookupviewcell(const vec &p)
+{
+ uint x = uint(floor(p.x)), y = uint(floor(p.y)), z = uint(floor(p.z));
+ if(!viewcells || (x|y|z)>=uint(worldsize)) return NULL;
+ viewcellnode *vc = viewcells;
+ for(int scale = worldscale-1; scale>=0; scale--)
+ {
+ int i = octastep(x, y, z, scale);
+ if(vc->leafmask&(1<<i))
+ {
+ return vc->children[i].pvs>=0 ? &pvs[vc->children[i].pvs] : NULL;
+ }
+ vc = vc->children[i].node;
+ }
+ return NULL;
+}
+
+static void lockpvs_(bool lock)
+{
+ if(lockedpvs) DELETEA(lockedpvs);
+ if(!lock) return;
+ pvsdata *d = lookupviewcell(camera1->o);
+ if(!d) return;
+ int wbytes = d->len%9, len = d->len - wbytes;
+ lockedpvs = new uchar[len];
+ memcpy(lockedpvs, &pvsbuf[d->offset + wbytes], len);
+ lockedwaterpvs = 0;
+ loopi(wbytes) lockedwaterpvs |= pvsbuf[d->offset + i] << (i*8);
+ loopi(MAXWATERPVS) lockedwaterplanes[i] = waterplanes[i].height;
+ conoutf("locked view cell at %.1f, %.1f, %.1f", camera1->o.x, camera1->o.y, camera1->o.z);
+}
+
+VARF(lockpvs, 0, 0, 1, lockpvs_(lockpvs!=0));
+
+VARN(pvs, usepvs, 0, 1, 1);
+VARN(waterpvs, usewaterpvs, 0, 1, 1);
+
+void setviewcell(const vec &p)
+{
+ if(!usepvs) curpvs = NULL;
+ else if(lockedpvs)
+ {
+ curpvs = lockedpvs;
+ curwaterpvs = lockedwaterpvs;
+ }
+ else
+ {
+ pvsdata *d = lookupviewcell(p);
+ curpvs = d ? &pvsbuf[d->offset] : NULL;
+ curwaterpvs = 0;
+ if(d)
+ {
+ loopi(d->len%9) curwaterpvs |= *curpvs++ << (i*8);
+ }
+ }
+ if(!usepvs || !usewaterpvs) curwaterpvs = 0;
+}
+
+void clearpvs()
+{
+ DELETEP(viewcells);
+ pvs.setsize(0);
+ pvsbuf.setsize(0);
+ curpvs = NULL;
+ numwaterplanes = 0;
+ lockpvs = 0;
+ lockpvs_(false);
+}
+
+COMMAND(clearpvs, "");
+
+static void findwaterplanes()
+{
+ loopi(MAXWATERPVS)
+ {
+ waterplanes[i].height = -1;
+ waterplanes[i].matsurfs.setsize(0);
+ }
+ waterfalls.setsize(0);
+ numwaterplanes = 0;
+ loopv(valist)
+ {
+ vtxarray *va = valist[i];
+ loopj(va->matsurfs)
+ {
+ materialsurface &m = va->matbuf[j];
+ if((m.material&MATF_VOLUME)!=MAT_WATER || m.orient==O_BOTTOM) { j += m.skip; continue; }
+ if(m.orient!=O_TOP)
+ {
+ waterfalls.add(&m);
+ continue;
+ }
+ loopk(numwaterplanes) if(waterplanes[k].height == m.o.z)
+ {
+ waterplanes[k].matsurfs.add(&m);
+ goto nextmatsurf;
+ }
+ if(numwaterplanes < MAXWATERPVS)
+ {
+ waterplanes[numwaterplanes].height = m.o.z;
+ waterplanes[numwaterplanes].matsurfs.add(&m);
+ numwaterplanes++;
+ }
+ nextmatsurf:;
+ }
+ }
+ if(waterfalls.length() > 0 && numwaterplanes < MAXWATERPVS) numwaterplanes++;
+}
+
+void testpvs(int *vcsize)
+{
+ lockpvs_(false);
+
+ uint oldnumwaterplanes = numwaterplanes;
+ int oldwaterplanes[MAXWATERPVS];
+ loopi(numwaterplanes) oldwaterplanes[i] = waterplanes[i].height;
+
+ findwaterplanes();
+
+ pvsnode &root = origpvsnodes.add();
+ memset(root.edges.v, 0xFF, 3);
+ root.flags = 0;
+ root.children = 0;
+ genpvsnodes(worldroot);
+
+ genpvs_canceled = false;
+ check_genpvs_progress = false;
+
+ int size = *vcsize>0 ? *vcsize : 32;
+ for(int mask = 1; mask < size; mask <<= 1) size &= ~mask;
+
+ ivec o = ivec(camera1->o).mask(~(size-1));
+ pvsworker w;
+ int len;
+ lockedpvs = w.testviewcell(o, size, &lockedwaterpvs, &len);
+ loopi(MAXWATERPVS) lockedwaterplanes[i] = waterplanes[i].height;
+ lockpvs = 1;
+ conoutf("generated test view cell of size %d at %.1f, %.1f, %.1f (%d B)", size, camera1->o.x, camera1->o.y, camera1->o.z, len);
+
+ origpvsnodes.setsize(0);
+ numwaterplanes = oldnumwaterplanes;
+ loopi(numwaterplanes) waterplanes[i].height = oldwaterplanes[i];
+}
+
+COMMAND(testpvs, "i");
+
+void genpvs(int *viewcellsize)
+{
+ if(worldsize > 1<<15)
+ {
+ conoutf(CON_ERROR, "map is too large for PVS");
+ return;
+ }
+
+ renderbackground("generating PVS (esc to abort)");
+ genpvs_canceled = false;
+ Uint32 start = SDL_GetTicks();
+
+ renderprogress(0, "finding view cells");
+
+ clearpvs();
+ calcpvsbounds();
+ findwaterplanes();
+
+ pvsnode &root = origpvsnodes.add();
+ memset(root.edges.v, 0xFF, 3);
+ root.flags = 0;
+ root.children = 0;
+ genpvsnodes(worldroot);
+
+ totalviewcells = countviewcells(worldroot, ivec(0, 0, 0), worldsize>>1, *viewcellsize>0 ? *viewcellsize : 32);
+ numviewcells = 0;
+ genpvs_canceled = false;
+ check_genpvs_progress = false;
+ SDL_TimerID timer = 0;
+ int numthreads = pvsthreads > 0 ? pvsthreads : numcpus;
+ if(numthreads<=1)
+ {
+ pvsworkers.add(new pvsworker);
+ timer = SDL_AddTimer(500, genpvs_timer, NULL);
+ }
+ viewcells = new viewcellnode;
+ genviewcells(*viewcells, worldroot, ivec(0, 0, 0), worldsize>>1, *viewcellsize>0 ? *viewcellsize : 32);
+ if(numthreads<=1)
+ {
+ SDL_RemoveTimer(timer);
+ }
+ else
+ {
+ renderprogress(0, "creating threads");
+ if(!pvsmutex) pvsmutex = SDL_CreateMutex();
+ if(!viewcellmutex) viewcellmutex = SDL_CreateMutex();
+ loopi(numthreads)
+ {
+ pvsworker *w = pvsworkers.add(new pvsworker);
+ w->thread = SDL_CreateThread(pvsworker::run, "pvs worker", w);
+ }
+ show_genpvs_progress(0, 0);
+ while(!genpvs_canceled)
+ {
+ SDL_Delay(500);
+ SDL_LockMutex(viewcellmutex);
+ int unique = pvs.length(), processed = numviewcells, remaining = viewcellrequests.length();
+ SDL_UnlockMutex(viewcellmutex);
+ show_genpvs_progress(unique, processed);
+ if(!remaining) break;
+ }
+ SDL_LockMutex(viewcellmutex);
+ viewcellrequests.setsize(0);
+ SDL_UnlockMutex(viewcellmutex);
+ loopv(pvsworkers) SDL_WaitThread(pvsworkers[i]->thread, NULL);
+ }
+ pvsworkers.deletecontents();
+
+ origpvsnodes.setsize(0);
+ pvscompress.clear();
+
+ Uint32 end = SDL_GetTicks();
+ if(genpvs_canceled)
+ {
+ clearpvs();
+ conoutf("genpvs aborted");
+ }
+ else conoutf("generated %d unique view cells totaling %.1f kB and averaging %d B (%.1f seconds)",
+ pvs.length(), pvsbuf.length()/1024.0f, pvsbuf.length()/max(pvs.length(), 1), (end - start) / 1000.0f);
+}
+
+COMMAND(genpvs, "i");
+
+void pvsstats()
+{
+ conoutf("%d unique view cells totaling %.1f kB and averaging %d B",
+ pvs.length(), pvsbuf.length()/1024.0f, pvsbuf.length()/max(pvs.length(), 1));
+}
+
+COMMAND(pvsstats, "");
+
+static inline bool pvsoccluded(uchar *buf, const ivec &co, int size, const ivec &bbmin, const ivec &bbmax)
+{
+ uchar leafmask = buf[0];
+ loopoctabox(co, size, bbmin, bbmax)
+ {
+ ivec o(i, co, size);
+ if(leafmask&(1<<i))
+ {
+ uchar leafvalues = buf[1+i];
+ if(!leafvalues || (leafvalues!=0xFF && octaboxoverlap(o, size>>1, bbmin, bbmax)&~leafvalues))
+ return false;
+ }
+ else if(!pvsoccluded(buf+9*buf[1+i], o, size>>1, bbmin, bbmax)) return false;
+ }
+ return true;
+}
+
+static inline bool pvsoccluded(uchar *buf, const ivec &bbmin, const ivec &bbmax)
+{
+ int diff = (bbmin.x^bbmax.x) | (bbmin.y^bbmax.y) | (bbmin.z^bbmax.z);
+ if(diff&~((1<<worldscale)-1)) return false;
+ int scale = worldscale-1;
+ while(!(diff&(1<<scale)))
+ {
+ int i = octastep(bbmin.x, bbmin.y, bbmin.z, scale);
+ scale--;
+ uchar leafmask = buf[0];
+ if(leafmask&(1<<i))
+ {
+ uchar leafvalues = buf[1+i];
+ return leafvalues && (leafvalues==0xFF || !(octaboxoverlap(ivec(bbmin).mask(~((2<<scale)-1)), 1<<scale, bbmin, bbmax)&~leafvalues));
+ }
+ buf += 9*buf[1+i];
+ }
+ return pvsoccluded(buf, ivec(bbmin).mask(~((2<<scale)-1)), 1<<scale, bbmin, bbmax);
+}
+
+bool pvsoccluded(const ivec &bbmin, const ivec &bbmax)
+{
+ return curpvs!=NULL && pvsoccluded(curpvs, bbmin, bbmax);
+}
+
+bool pvsoccludedsphere(const vec &center, float radius)
+{
+ if(curpvs==NULL) return false;
+ ivec bbmin(vec(center).sub(radius)), bbmax(vec(center).add(radius+1));
+ return pvsoccluded(curpvs, bbmin, bbmax);
+}
+
+bool waterpvsoccluded(int height)
+{
+ if(!curwaterpvs) return false;
+ if(lockedpvs)
+ {
+ loopi(MAXWATERPVS) if(lockedwaterplanes[i]==height) return (curwaterpvs&(1<<i))!=0;
+ }
+ else
+ {
+ loopi(numwaterplanes) if(waterplanes[i].height==height) return (curwaterpvs&(1<<i))!=0;
+ }
+ return false;
+}
+
+void saveviewcells(stream *f, viewcellnode &p)
+{
+ f->putchar(p.leafmask);
+ loopi(8)
+ {
+ if(p.leafmask&(1<<i)) f->putlil<int>(p.children[i].pvs);
+ else saveviewcells(f, *p.children[i].node);
+ }
+}
+
+void savepvs(stream *f)
+{
+ uint totallen = pvsbuf.length() | (numwaterplanes>0 ? 0x80000000U : 0);
+ f->putlil<uint>(totallen);
+ if(numwaterplanes>0)
+ {
+ f->putlil<uint>(numwaterplanes);
+ loopi(numwaterplanes)
+ {
+ f->putlil<int>(waterplanes[i].height);
+ if(waterplanes[i].height < 0) break;
+ }
+ }
+ loopv(pvs) f->putlil<ushort>(pvs[i].len);
+ f->write(pvsbuf.getbuf(), pvsbuf.length());
+ saveviewcells(f, *viewcells);
+}
+
+viewcellnode *loadviewcells(stream *f)
+{
+ viewcellnode *p = new viewcellnode;
+ p->leafmask = f->getchar();
+ loopi(8)
+ {
+ if(p->leafmask&(1<<i)) p->children[i].pvs = f->getlil<int>();
+ else p->children[i].node = loadviewcells(f);
+ }
+ return p;
+}
+
+void loadpvs(stream *f, int numpvs)
+{
+ uint totallen = f->getlil<uint>();
+ if(totallen & 0x80000000U)
+ {
+ totallen &= ~0x80000000U;
+ numwaterplanes = f->getlil<uint>();
+ loopi(numwaterplanes) waterplanes[i].height = f->getlil<int>();
+ }
+ int offset = 0;
+ loopi(numpvs)
+ {
+ ushort len = f->getlil<ushort>();
+ pvs.add(pvsdata(offset, len));
+ offset += len;
+ }
+ f->read(pvsbuf.reserve(totallen).buf, totallen);
+ pvsbuf.advance(totallen);
+ viewcells = loadviewcells(f);
+}
+
+int getnumviewcells() { return pvs.length(); }
+
diff --git a/src/engine/ragdoll.h b/src/engine/ragdoll.h
new file mode 100644
index 0000000..9f6f641
--- /dev/null
+++ b/src/engine/ragdoll.h
@@ -0,0 +1,534 @@
+struct ragdollskel
+{
+ struct vert
+ {
+ vec pos;
+ float radius, weight;
+ };
+
+ struct tri
+ {
+ int vert[3];
+
+ bool shareverts(const tri &t) const
+ {
+ loopi(3) loopj(3) if(vert[i] == t.vert[j]) return true;
+ return false;
+ }
+ };
+
+ struct distlimit
+ {
+ int vert[2];
+ float mindist, maxdist;
+ };
+
+ struct rotlimit
+ {
+ int tri[2];
+ float maxangle;
+ matrix3 middle;
+ };
+
+ struct rotfriction
+ {
+ int tri[2];
+ matrix3 middle;
+ };
+
+ struct joint
+ {
+ int bone, tri, vert[3];
+ float weight;
+ matrix4x3 orient;
+ };
+
+ struct reljoint
+ {
+ int bone, parent;
+ };
+
+ bool loaded, animjoints;
+ int eye;
+ vector<vert> verts;
+ vector<tri> tris;
+ vector<distlimit> distlimits;
+ vector<rotlimit> rotlimits;
+ vector<rotfriction> rotfrictions;
+ vector<joint> joints;
+ vector<reljoint> reljoints;
+
+ ragdollskel() : loaded(false), animjoints(false), eye(-1) {}
+
+ void setupjoints()
+ {
+ loopv(verts) verts[i].weight = 0;
+ loopv(joints)
+ {
+ joint &j = joints[i];
+ j.weight = 0;
+ vec pos(0, 0, 0);
+ loopk(3) if(j.vert[k]>=0)
+ {
+ pos.add(verts[j.vert[k]].pos);
+ j.weight++;
+ verts[j.vert[k]].weight++;
+ }
+ if(j.weight) j.weight = 1/j.weight;
+ pos.mul(j.weight);
+
+ tri &t = tris[j.tri];
+ matrix4x3 &m = j.orient;
+ const vec &v1 = verts[t.vert[0]].pos,
+ &v2 = verts[t.vert[1]].pos,
+ &v3 = verts[t.vert[2]].pos;
+ m.a = vec(v2).sub(v1).normalize();
+ m.c.cross(m.a, vec(v3).sub(v1)).normalize();
+ m.b.cross(m.c, m.a);
+ m.d = pos;
+ m.transpose();
+ }
+ loopv(verts) if(verts[i].weight) verts[i].weight = 1/verts[i].weight;
+ reljoints.shrink(0);
+ }
+
+ void setuprotfrictions()
+ {
+ rotfrictions.shrink(0);
+ loopv(tris) for(int j = i+1; j < tris.length(); j++) if(tris[i].shareverts(tris[j]))
+ {
+ rotfriction &r = rotfrictions.add();
+ r.tri[0] = i;
+ r.tri[1] = j;
+ }
+ }
+
+ void setup()
+ {
+ setupjoints();
+ setuprotfrictions();
+
+ loaded = true;
+ }
+
+ void addreljoint(int bone, int parent)
+ {
+ reljoint &r = reljoints.add();
+ r.bone = bone;
+ r.parent = parent;
+ }
+};
+
+struct ragdolldata
+{
+ struct vert
+ {
+ vec oldpos, pos, newpos;
+ float weight;
+ bool collided, stuck;
+
+ vert() : pos(0, 0, 0), newpos(0, 0, 0), weight(0), collided(false), stuck(true) {}
+ };
+
+ ragdollskel *skel;
+ int millis, collidemillis, collisions, floating, lastmove, unsticks;
+ vec offset, center;
+ float radius, timestep, scale;
+ vert *verts;
+ matrix3 *tris;
+ matrix4x3 *animjoints;
+ dualquat *reljoints;
+
+ ragdolldata(ragdollskel *skel, float scale = 1)
+ : skel(skel),
+ millis(lastmillis),
+ collidemillis(0),
+ collisions(0),
+ floating(0),
+ lastmove(lastmillis),
+ unsticks(INT_MAX),
+ radius(0),
+ timestep(0),
+ scale(scale),
+ verts(new vert[skel->verts.length()]),
+ tris(new matrix3[skel->tris.length()]),
+ animjoints(!skel->animjoints || skel->joints.empty() ? NULL : new matrix4x3[skel->joints.length()]),
+ reljoints(skel->reljoints.empty() ? NULL : new dualquat[skel->reljoints.length()])
+ {
+ }
+
+ ~ragdolldata()
+ {
+ delete[] verts;
+ delete[] tris;
+ if(animjoints) delete[] animjoints;
+ if(reljoints) delete[] reljoints;
+ }
+
+ void calcanimjoint(int i, const matrix4x3 &anim)
+ {
+ if(!animjoints) return;
+ ragdollskel::joint &j = skel->joints[i];
+ vec pos(0, 0, 0);
+ loopk(3) if(j.vert[k]>=0) pos.add(verts[j.vert[k]].pos);
+ pos.mul(j.weight);
+
+ ragdollskel::tri &t = skel->tris[j.tri];
+ matrix4x3 m;
+ const vec &v1 = verts[t.vert[0]].pos,
+ &v2 = verts[t.vert[1]].pos,
+ &v3 = verts[t.vert[2]].pos;
+ m.a = vec(v2).sub(v1).normalize();
+ m.c.cross(m.a, vec(v3).sub(v1)).normalize();
+ m.b.cross(m.c, m.a);
+ m.d = pos;
+ animjoints[i].transposemul(m, anim);
+ }
+
+ void calctris()
+ {
+ loopv(skel->tris)
+ {
+ ragdollskel::tri &t = skel->tris[i];
+ matrix3 &m = tris[i];
+ const vec &v1 = verts[t.vert[0]].pos,
+ &v2 = verts[t.vert[1]].pos,
+ &v3 = verts[t.vert[2]].pos;
+ m.a = vec(v2).sub(v1).normalize();
+ m.c.cross(m.a, vec(v3).sub(v1)).normalize();
+ m.b.cross(m.c, m.a);
+ }
+ }
+
+ void calcboundsphere()
+ {
+ center = vec(0, 0, 0);
+ loopv(skel->verts) center.add(verts[i].pos);
+ center.div(skel->verts.length());
+ radius = 0;
+ loopv(skel->verts) radius = max(radius, verts[i].pos.dist(center));
+ }
+
+ void init(dynent *d)
+ {
+ extern int ragdolltimestepmin;
+ float ts = ragdolltimestepmin/1000.0f;
+ loopv(skel->verts) (verts[i].oldpos = verts[i].pos).sub(vec(d->vel).add(d->falling).mul(ts));
+ timestep = ts;
+
+ calctris();
+ calcboundsphere();
+ offset = d->o;
+ offset.sub(skel->eye >= 0 ? verts[skel->eye].pos : center);
+ offset.z += (d->eyeheight + d->aboveeye)/2;
+ }
+
+ void move(dynent *pl, float ts);
+ void updatepos();
+ void constrain();
+ void constraindist();
+ void applyrotlimit(ragdollskel::tri &t1, ragdollskel::tri &t2, float angle, const vec &axis);
+ void constrainrot();
+ void calcrotfriction();
+ void applyrotfriction(float ts);
+ void tryunstick(float speed);
+
+ static inline bool collidevert(const vec &pos, const vec &dir, float radius)
+ {
+ static struct vertent : physent
+ {
+ vertent()
+ {
+ type = ENT_BOUNCE;
+ radius = xradius = yradius = eyeheight = aboveeye = 1;
+ }
+ } v;
+ v.o = pos;
+ if(v.radius != radius) v.radius = v.xradius = v.yradius = v.eyeheight = v.aboveeye = radius;
+ return collide(&v, dir, 0, false);
+ }
+};
+
+/*
+ seed particle position = avg(modelview * base2anim * spherepos)
+ mapped transform = invert(curtri) * origtrig
+ parented transform = parent{invert(curtri) * origtrig} * (invert(parent{base2anim}) * base2anim)
+*/
+
+void ragdolldata::constraindist()
+{
+ float invscale = 1.0f/scale;
+ loopv(skel->distlimits)
+ {
+ ragdollskel::distlimit &d = skel->distlimits[i];
+ vert &v1 = verts[d.vert[0]], &v2 = verts[d.vert[1]];
+ vec dir = vec(v2.pos).sub(v1.pos);
+ float dist = dir.magnitude()*invscale, cdist;
+ if(dist < d.mindist) cdist = d.mindist;
+ else if(dist > d.maxdist) cdist = d.maxdist;
+ else continue;
+ if(dist > 1e-4f) dir.mul(cdist*0.5f/dist);
+ else dir = vec(0, 0, cdist*0.5f/invscale);
+ vec center = vec(v1.pos).add(v2.pos).mul(0.5f);
+ v1.newpos.add(vec(center).sub(dir));
+ v1.weight++;
+ v2.newpos.add(vec(center).add(dir));
+ v2.weight++;
+ }
+}
+
+inline void ragdolldata::applyrotlimit(ragdollskel::tri &t1, ragdollskel::tri &t2, float angle, const vec &axis)
+{
+ vert &v1a = verts[t1.vert[0]], &v1b = verts[t1.vert[1]], &v1c = verts[t1.vert[2]],
+ &v2a = verts[t2.vert[0]], &v2b = verts[t2.vert[1]], &v2c = verts[t2.vert[2]];
+ vec m1 = vec(v1a.pos).add(v1b.pos).add(v1c.pos).div(3),
+ m2 = vec(v2a.pos).add(v2b.pos).add(v2c.pos).div(3),
+ q1a, q1b, q1c, q2a, q2b, q2c;
+ float w1 = q1a.cross(axis, vec(v1a.pos).sub(m1)).magnitude() +
+ q1b.cross(axis, vec(v1b.pos).sub(m1)).magnitude() +
+ q1c.cross(axis, vec(v1c.pos).sub(m1)).magnitude(),
+ w2 = q2a.cross(axis, vec(v2a.pos).sub(m2)).magnitude() +
+ q2b.cross(axis, vec(v2b.pos).sub(m2)).magnitude() +
+ q2c.cross(axis, vec(v2c.pos).sub(m2)).magnitude();
+ angle /= w1 + w2 + 1e-9f;
+ float a1 = angle*w2, a2 = -angle*w1,
+ s1 = sinf(a1), s2 = sinf(a2);
+ vec c1 = vec(axis).mul(1 - cosf(a1)), c2 = vec(axis).mul(1 - cosf(a2));
+ v1a.newpos.add(vec().cross(c1, q1a).madd(q1a, s1).add(v1a.pos));
+ v1a.weight++;
+ v1b.newpos.add(vec().cross(c1, q1b).madd(q1b, s1).add(v1b.pos));
+ v1b.weight++;
+ v1c.newpos.add(vec().cross(c1, q1c).madd(q1c, s1).add(v1c.pos));
+ v1c.weight++;
+ v2a.newpos.add(vec().cross(c2, q2a).madd(q2a, s2).add(v2a.pos));
+ v2a.weight++;
+ v2b.newpos.add(vec().cross(c2, q2b).madd(q2b, s2).add(v2b.pos));
+ v2b.weight++;
+ v2c.newpos.add(vec().cross(c2, q2c).madd(q2c, s2).add(v2c.pos));
+ v2c.weight++;
+}
+
+void ragdolldata::constrainrot()
+{
+ loopv(skel->rotlimits)
+ {
+ ragdollskel::rotlimit &r = skel->rotlimits[i];
+ matrix3 rot;
+ rot.mul(tris[r.tri[0]], r.middle);
+ rot.multranspose(tris[r.tri[1]]);
+
+ vec axis;
+ float angle;
+ if(!rot.calcangleaxis(angle, axis)) continue;
+ angle = r.maxangle - fabs(angle);
+ if(angle >= 0) continue;
+ angle += 1e-3f;
+
+ applyrotlimit(skel->tris[r.tri[0]], skel->tris[r.tri[1]], angle, axis);
+ }
+}
+
+VAR(ragdolltimestepmin, 1, 5, 50);
+VAR(ragdolltimestepmax, 1, 10, 50);
+FVAR(ragdollrotfric, 0, 0.85f, 1);
+FVAR(ragdollrotfricstop, 0, 0.1f, 1);
+
+void ragdolldata::calcrotfriction()
+{
+ loopv(skel->rotfrictions)
+ {
+ ragdollskel::rotfriction &r = skel->rotfrictions[i];
+ r.middle.transposemul(tris[r.tri[0]], tris[r.tri[1]]);
+ }
+}
+
+void ragdolldata::applyrotfriction(float ts)
+{
+ calctris();
+ float stopangle = 2*M_PI*ts*ragdollrotfricstop, rotfric = 1.0f - pow(ragdollrotfric, ts*1000.0f/ragdolltimestepmin);
+ loopv(skel->rotfrictions)
+ {
+ ragdollskel::rotfriction &r = skel->rotfrictions[i];
+ matrix3 rot;
+ rot.mul(tris[r.tri[0]], r.middle);
+ rot.multranspose(tris[r.tri[1]]);
+
+ vec axis;
+ float angle;
+ if(rot.calcangleaxis(angle, axis))
+ {
+ angle *= -(fabs(angle) >= stopangle ? rotfric : 1.0f);
+ applyrotlimit(skel->tris[r.tri[0]], skel->tris[r.tri[1]], angle, axis);
+ }
+ }
+ loopv(skel->verts)
+ {
+ vert &v = verts[i];
+ if(v.weight) v.pos = v.newpos.div(v.weight);
+ v.newpos = vec(0, 0, 0);
+ v.weight = 0;
+ }
+}
+
+void ragdolldata::tryunstick(float speed)
+{
+ vec unstuck(0, 0, 0);
+ int stuck = 0;
+ loopv(skel->verts)
+ {
+ vert &v = verts[i];
+ if(v.stuck)
+ {
+ if(collidevert(v.pos, vec(0, 0, 0), skel->verts[i].radius)) { stuck++; continue; }
+ v.stuck = false;
+ }
+ unstuck.add(v.pos);
+ }
+ unsticks = 0;
+ if(!stuck || stuck >= skel->verts.length()) return;
+ unstuck.div(skel->verts.length() - stuck);
+ loopv(skel->verts)
+ {
+ vert &v = verts[i];
+ if(v.stuck)
+ {
+ v.pos.add(vec(unstuck).sub(v.pos).rescale(speed));
+ unsticks++;
+ }
+ }
+}
+
+void ragdolldata::updatepos()
+{
+ loopv(skel->verts)
+ {
+ vert &v = verts[i];
+ if(v.weight)
+ {
+ v.newpos.div(v.weight);
+ if(!collidevert(v.newpos, vec(v.newpos).sub(v.pos), skel->verts[i].radius)) v.pos = v.newpos;
+ else
+ {
+ vec dir = vec(v.newpos).sub(v.oldpos);
+ if(dir.dot(collidewall) < 0) v.oldpos = vec(v.pos).sub(dir.reflect(collidewall));
+ v.collided = true;
+ }
+ }
+ v.newpos = vec(0, 0, 0);
+ v.weight = 0;
+ }
+}
+
+VAR(ragdollconstrain, 1, 5, 100);
+
+void ragdolldata::constrain()
+{
+ loopi(ragdollconstrain)
+ {
+ constraindist();
+ updatepos();
+
+ calctris();
+ constrainrot();
+ updatepos();
+ }
+}
+
+FVAR(ragdollbodyfric, 0, 0.95f, 1);
+FVAR(ragdollbodyfricscale, 0, 2, 10);
+FVAR(ragdollwaterfric, 0, 0.85f, 1);
+FVAR(ragdollgroundfric, 0, 0.8f, 1);
+FVAR(ragdollairfric, 0, 0.996f, 1);
+FVAR(ragdollunstick, 0, 10, 1e3f);
+VAR(ragdollexpireoffset, 0, 1500, 30000);
+VAR(ragdollwaterexpireoffset, 0, 3000, 30000);
+
+void ragdolldata::move(dynent *pl, float ts)
+{
+ extern const float GRAVITY;
+ if(collidemillis && lastmillis > collidemillis) return;
+
+ int material = lookupmaterial(vec(center.x, center.y, center.z + radius/2));
+ bool water = isliquid(material&MATF_VOLUME);
+ if(!pl->inwater && water) game::physicstrigger(pl, true, 0, -1, material&MATF_VOLUME);
+ else if(pl->inwater && !water)
+ {
+ material = lookupmaterial(center);
+ water = isliquid(material&MATF_VOLUME);
+ if(!water) game::physicstrigger(pl, true, 0, 1, pl->inwater);
+ }
+ pl->inwater = water ? material&MATF_VOLUME : MAT_AIR;
+
+ calcrotfriction();
+ float tsfric = timestep ? ts/timestep : 1,
+ airfric = ragdollairfric + min((ragdollbodyfricscale*collisions)/skel->verts.length(), 1.0f)*(ragdollbodyfric - ragdollairfric);
+ collisions = 0;
+ loopv(skel->verts)
+ {
+ vert &v = verts[i];
+ vec dpos = vec(v.pos).sub(v.oldpos);
+ dpos.z -= GRAVITY*ts*ts;
+ if(water) dpos.z += 0.25f*sinf(detrnd(size_t(this)+i, 360)*RAD + lastmillis/10000.0f*M_PI)*ts;
+ dpos.mul(pow((water ? ragdollwaterfric : 1.0f) * (v.collided ? ragdollgroundfric : airfric), ts*1000.0f/ragdolltimestepmin)*tsfric);
+ v.oldpos = v.pos;
+ v.pos.add(dpos);
+ }
+ applyrotfriction(ts);
+ loopv(skel->verts)
+ {
+ vert &v = verts[i];
+ if(v.pos.z < 0) { v.pos.z = 0; v.oldpos = v.pos; collisions++; }
+ vec dir = vec(v.pos).sub(v.oldpos);
+ v.collided = collidevert(v.pos, dir, skel->verts[i].radius);
+ if(v.collided)
+ {
+ v.pos = v.oldpos;
+ v.oldpos.sub(dir.reflect(collidewall));
+ collisions++;
+ }
+ }
+
+ if(unsticks && ragdollunstick) tryunstick(ts*ragdollunstick);
+
+ timestep = ts;
+ if(collisions)
+ {
+ floating = 0;
+ if(!collidemillis) collidemillis = lastmillis + (water ? ragdollwaterexpireoffset : ragdollexpireoffset);
+ }
+ else if(++floating > 1 && lastmillis < collidemillis) collidemillis = 0;
+
+ constrain();
+ calctris();
+ calcboundsphere();
+}
+
+FVAR(ragdolleyesmooth, 0, 0.5f, 1);
+VAR(ragdolleyesmoothmillis, 1, 250, 10000);
+
+void moveragdoll(dynent *d)
+{
+ if(!curtime || !d->ragdoll) return;
+
+ if(!d->ragdoll->collidemillis || lastmillis < d->ragdoll->collidemillis)
+ {
+ int lastmove = d->ragdoll->lastmove;
+ while(d->ragdoll->lastmove + (lastmove == d->ragdoll->lastmove ? ragdolltimestepmin : ragdolltimestepmax) <= lastmillis)
+ {
+ int timestep = min(ragdolltimestepmax, lastmillis - d->ragdoll->lastmove);
+ d->ragdoll->move(d, timestep/1000.0f);
+ d->ragdoll->lastmove += timestep;
+ }
+ }
+
+ vec eye = d->ragdoll->skel->eye >= 0 ? d->ragdoll->verts[d->ragdoll->skel->eye].pos : d->ragdoll->center;
+ eye.add(d->ragdoll->offset);
+ float k = pow(ragdolleyesmooth, float(curtime)/ragdolleyesmoothmillis);
+ d->o.mul(k).add(eye.mul(1-k));
+}
+
+void cleanragdoll(dynent *d)
+{
+ DELETEP(d->ragdoll);
+}
+
diff --git a/src/engine/rendergl.cpp b/src/engine/rendergl.cpp
new file mode 100644
index 0000000..6049951
--- /dev/null
+++ b/src/engine/rendergl.cpp
@@ -0,0 +1,2388 @@
+// rendergl.cpp: core opengl rendering stuff
+
+#include "engine.h"
+
+bool hasVAO = false, hasFBO = false, hasAFBO = false, hasDS = false, hasTF = false, hasTRG = false, hasTSW = false, hasS3TC = false, hasFXT1 = false, hasLATC = false, hasRGTC = false, hasAF = false, hasFBB = false, hasUBO = false, hasMBR = false;
+
+VAR(glversion, 1, 0, 0);
+VAR(glslversion, 1, 0, 0);
+VAR(glcompat, 1, 0, 0);
+
+// OpenGL 1.3
+#ifdef WIN32
+PFNGLACTIVETEXTUREPROC glActiveTexture_ = NULL;
+
+PFNGLBLENDEQUATIONEXTPROC glBlendEquation_ = NULL;
+PFNGLBLENDCOLOREXTPROC glBlendColor_ = NULL;
+
+PFNGLTEXIMAGE3DPROC glTexImage3D_ = NULL;
+PFNGLTEXSUBIMAGE3DPROC glTexSubImage3D_ = NULL;
+PFNGLCOPYTEXSUBIMAGE3DPROC glCopyTexSubImage3D_ = NULL;
+
+PFNGLCOMPRESSEDTEXIMAGE3DPROC glCompressedTexImage3D_ = NULL;
+PFNGLCOMPRESSEDTEXIMAGE2DPROC glCompressedTexImage2D_ = NULL;
+PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC glCompressedTexSubImage3D_ = NULL;
+PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC glCompressedTexSubImage2D_ = NULL;
+PFNGLGETCOMPRESSEDTEXIMAGEPROC glGetCompressedTexImage_ = NULL;
+
+PFNGLDRAWRANGEELEMENTSPROC glDrawRangeElements_ = NULL;
+#endif
+
+// OpenGL 2.0
+#ifndef __APPLE__
+PFNGLMULTIDRAWARRAYSPROC glMultiDrawArrays_ = NULL;
+PFNGLMULTIDRAWELEMENTSPROC glMultiDrawElements_ = NULL;
+
+PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate_ = NULL;
+PFNGLBLENDEQUATIONSEPARATEPROC glBlendEquationSeparate_ = NULL;
+PFNGLSTENCILOPSEPARATEPROC glStencilOpSeparate_ = NULL;
+PFNGLSTENCILFUNCSEPARATEPROC glStencilFuncSeparate_ = NULL;
+PFNGLSTENCILMASKSEPARATEPROC glStencilMaskSeparate_ = NULL;
+
+PFNGLGENBUFFERSPROC glGenBuffers_ = NULL;
+PFNGLBINDBUFFERPROC glBindBuffer_ = NULL;
+PFNGLMAPBUFFERPROC glMapBuffer_ = NULL;
+PFNGLUNMAPBUFFERPROC glUnmapBuffer_ = NULL;
+PFNGLBUFFERDATAPROC glBufferData_ = NULL;
+PFNGLBUFFERSUBDATAPROC glBufferSubData_ = NULL;
+PFNGLDELETEBUFFERSPROC glDeleteBuffers_ = NULL;
+PFNGLGETBUFFERSUBDATAPROC glGetBufferSubData_ = NULL;
+
+PFNGLGENQUERIESPROC glGenQueries_ = NULL;
+PFNGLDELETEQUERIESPROC glDeleteQueries_ = NULL;
+PFNGLBEGINQUERYPROC glBeginQuery_ = NULL;
+PFNGLENDQUERYPROC glEndQuery_ = NULL;
+PFNGLGETQUERYIVPROC glGetQueryiv_ = NULL;
+PFNGLGETQUERYOBJECTIVPROC glGetQueryObjectiv_ = NULL;
+PFNGLGETQUERYOBJECTUIVPROC glGetQueryObjectuiv_ = NULL;
+
+PFNGLCREATEPROGRAMPROC glCreateProgram_ = NULL;
+PFNGLDELETEPROGRAMPROC glDeleteProgram_ = NULL;
+PFNGLUSEPROGRAMPROC glUseProgram_ = NULL;
+PFNGLCREATESHADERPROC glCreateShader_ = NULL;
+PFNGLDELETESHADERPROC glDeleteShader_ = NULL;
+PFNGLSHADERSOURCEPROC glShaderSource_ = NULL;
+PFNGLCOMPILESHADERPROC glCompileShader_ = NULL;
+PFNGLGETSHADERIVPROC glGetShaderiv_ = NULL;
+PFNGLGETPROGRAMIVPROC glGetProgramiv_ = NULL;
+PFNGLATTACHSHADERPROC glAttachShader_ = NULL;
+PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog_ = NULL;
+PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog_ = NULL;
+PFNGLLINKPROGRAMPROC glLinkProgram_ = NULL;
+PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation_ = NULL;
+PFNGLUNIFORM1FPROC glUniform1f_ = NULL;
+PFNGLUNIFORM2FPROC glUniform2f_ = NULL;
+PFNGLUNIFORM3FPROC glUniform3f_ = NULL;
+PFNGLUNIFORM4FPROC glUniform4f_ = NULL;
+PFNGLUNIFORM1FVPROC glUniform1fv_ = NULL;
+PFNGLUNIFORM2FVPROC glUniform2fv_ = NULL;
+PFNGLUNIFORM3FVPROC glUniform3fv_ = NULL;
+PFNGLUNIFORM4FVPROC glUniform4fv_ = NULL;
+PFNGLUNIFORM1IPROC glUniform1i_ = NULL;
+PFNGLUNIFORM2IPROC glUniform2i_ = NULL;
+PFNGLUNIFORM3IPROC glUniform3i_ = NULL;
+PFNGLUNIFORM4IPROC glUniform4i_ = NULL;
+PFNGLUNIFORM1IVPROC glUniform1iv_ = NULL;
+PFNGLUNIFORM2IVPROC glUniform2iv_ = NULL;
+PFNGLUNIFORM3IVPROC glUniform3iv_ = NULL;
+PFNGLUNIFORM4IVPROC glUniform4iv_ = NULL;
+PFNGLUNIFORMMATRIX2FVPROC glUniformMatrix2fv_ = NULL;
+PFNGLUNIFORMMATRIX3FVPROC glUniformMatrix3fv_ = NULL;
+PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv_ = NULL;
+PFNGLBINDATTRIBLOCATIONPROC glBindAttribLocation_ = NULL;
+PFNGLGETACTIVEUNIFORMPROC glGetActiveUniform_ = NULL;
+PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray_ = NULL;
+PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray_ = NULL;
+
+PFNGLVERTEXATTRIB1FPROC glVertexAttrib1f_ = NULL;
+PFNGLVERTEXATTRIB1FVPROC glVertexAttrib1fv_ = NULL;
+PFNGLVERTEXATTRIB1SPROC glVertexAttrib1s_ = NULL;
+PFNGLVERTEXATTRIB1SVPROC glVertexAttrib1sv_ = NULL;
+PFNGLVERTEXATTRIB2FPROC glVertexAttrib2f_ = NULL;
+PFNGLVERTEXATTRIB2FVPROC glVertexAttrib2fv_ = NULL;
+PFNGLVERTEXATTRIB2SPROC glVertexAttrib2s_ = NULL;
+PFNGLVERTEXATTRIB2SVPROC glVertexAttrib2sv_ = NULL;
+PFNGLVERTEXATTRIB3FPROC glVertexAttrib3f_ = NULL;
+PFNGLVERTEXATTRIB3FVPROC glVertexAttrib3fv_ = NULL;
+PFNGLVERTEXATTRIB3SPROC glVertexAttrib3s_ = NULL;
+PFNGLVERTEXATTRIB3SVPROC glVertexAttrib3sv_ = NULL;
+PFNGLVERTEXATTRIB4FPROC glVertexAttrib4f_ = NULL;
+PFNGLVERTEXATTRIB4FVPROC glVertexAttrib4fv_ = NULL;
+PFNGLVERTEXATTRIB4SPROC glVertexAttrib4s_ = NULL;
+PFNGLVERTEXATTRIB4SVPROC glVertexAttrib4sv_ = NULL;
+PFNGLVERTEXATTRIB4BVPROC glVertexAttrib4bv_ = NULL;
+PFNGLVERTEXATTRIB4IVPROC glVertexAttrib4iv_ = NULL;
+PFNGLVERTEXATTRIB4UBVPROC glVertexAttrib4ubv_ = NULL;
+PFNGLVERTEXATTRIB4UIVPROC glVertexAttrib4uiv_ = NULL;
+PFNGLVERTEXATTRIB4USVPROC glVertexAttrib4usv_ = NULL;
+PFNGLVERTEXATTRIB4NBVPROC glVertexAttrib4Nbv_ = NULL;
+PFNGLVERTEXATTRIB4NIVPROC glVertexAttrib4Niv_ = NULL;
+PFNGLVERTEXATTRIB4NUBPROC glVertexAttrib4Nub_ = NULL;
+PFNGLVERTEXATTRIB4NUBVPROC glVertexAttrib4Nubv_ = NULL;
+PFNGLVERTEXATTRIB4NUIVPROC glVertexAttrib4Nuiv_ = NULL;
+PFNGLVERTEXATTRIB4NUSVPROC glVertexAttrib4Nusv_ = NULL;
+PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer_ = NULL;
+
+PFNGLDRAWBUFFERSPROC glDrawBuffers_ = NULL;
+#endif
+
+// OpenGL 3.0
+PFNGLGETSTRINGIPROC glGetStringi_ = NULL;
+PFNGLBINDFRAGDATALOCATIONPROC glBindFragDataLocation_ = NULL;
+
+// GL_EXT_framebuffer_object
+PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer_ = NULL;
+PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers_ = NULL;
+PFNGLGENFRAMEBUFFERSPROC glGenRenderbuffers_ = NULL;
+PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage_ = NULL;
+PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus_ = NULL;
+PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer_ = NULL;
+PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers_ = NULL;
+PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers_ = NULL;
+PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D_ = NULL;
+PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer_ = NULL;
+PFNGLGENERATEMIPMAPPROC glGenerateMipmap_ = NULL;
+
+// GL_EXT_framebuffer_blit
+PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer_ = NULL;
+
+// GL_ARB_uniform_buffer_object
+PFNGLGETUNIFORMINDICESPROC glGetUniformIndices_ = NULL;
+PFNGLGETACTIVEUNIFORMSIVPROC glGetActiveUniformsiv_ = NULL;
+PFNGLGETUNIFORMBLOCKINDEXPROC glGetUniformBlockIndex_ = NULL;
+PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv_ = NULL;
+PFNGLUNIFORMBLOCKBINDINGPROC glUniformBlockBinding_ = NULL;
+PFNGLBINDBUFFERBASEPROC glBindBufferBase_ = NULL;
+PFNGLBINDBUFFERRANGEPROC glBindBufferRange_ = NULL;
+
+// GL_ARB_map_buffer_range
+PFNGLMAPBUFFERRANGEPROC glMapBufferRange_ = NULL;
+PFNGLFLUSHMAPPEDBUFFERRANGEPROC glFlushMappedBufferRange_ = NULL;
+
+// GL_ARB_vertex_array_object
+PFNGLBINDVERTEXARRAYPROC glBindVertexArray_ = NULL;
+PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays_ = NULL;
+PFNGLGENVERTEXARRAYSPROC glGenVertexArrays_ = NULL;
+PFNGLISVERTEXARRAYPROC glIsVertexArray_ = NULL;
+
+void *getprocaddress(const char *name)
+{
+ return SDL_GL_GetProcAddress(name);
+}
+
+VARP(ati_skybox_bug, 0, 0, 1);
+VAR(ati_minmax_bug, 0, 0, 1);
+VAR(ati_cubemap_bug, 0, 0, 1);
+VAR(intel_vertexarray_bug, 0, 0, 1);
+VAR(intel_mapbufferrange_bug, 0, 0, 1);
+VAR(mesa_swap_bug, 0, 0, 1);
+VAR(minimizetcusage, 1, 0, 0);
+VAR(useubo, 1, 0, 0);
+VAR(usetexcompress, 1, 0, 0);
+VAR(rtscissor, 0, 1, 1);
+VAR(blurtile, 0, 1, 1);
+VAR(rtsharefb, 0, 1, 1);
+
+VAR(dbgexts, 0, 0, 1);
+
+hashset<const char *> glexts;
+
+void parseglexts()
+{
+ if(glversion >= 300)
+ {
+ GLint numexts = 0;
+ glGetIntegerv(GL_NUM_EXTENSIONS, &numexts);
+ loopi(numexts)
+ {
+ const char *ext = (const char *)glGetStringi_(GL_EXTENSIONS, i);
+ glexts.add(newstring(ext));
+ }
+ }
+ else
+ {
+ const char *exts = (const char *)glGetString(GL_EXTENSIONS);
+ for(;;)
+ {
+ while(*exts == ' ') exts++;
+ if(!*exts) break;
+ const char *ext = exts;
+ while(*exts && *exts != ' ') exts++;
+ if(exts > ext) glexts.add(newstring(ext, size_t(exts-ext)));
+ }
+ }
+}
+
+bool hasext(const char *ext)
+{
+ return glexts.access(ext)!=NULL;
+}
+
+void gl_checkextensions()
+{
+ const char *vendor = (const char *)glGetString(GL_VENDOR);
+ const char *renderer = (const char *)glGetString(GL_RENDERER);
+ const char *version = (const char *)glGetString(GL_VERSION);
+ conoutf(CON_INIT, "Renderer: %s (%s)", renderer, vendor);
+ conoutf(CON_INIT, "Driver: %s", version);
+
+ bool mesa = false, intel = false, ati = false, nvidia = false;
+ if(strstr(renderer, "Mesa") || strstr(version, "Mesa"))
+ {
+ mesa = true;
+ if(strstr(renderer, "Intel")) intel = true;
+ }
+ else if(strstr(vendor, "NVIDIA"))
+ nvidia = true;
+ else if(strstr(vendor, "ATI") || strstr(vendor, "Advanced Micro Devices"))
+ ati = true;
+ else if(strstr(vendor, "Intel"))
+ intel = true;
+
+ uint glmajorversion, glminorversion;
+ if(sscanf(version, " %u.%u", &glmajorversion, &glminorversion) != 2) glversion = 100;
+ else glversion = glmajorversion*100 + glminorversion*10;
+
+ if(glversion < 200) fatal("OpenGL 2.0 or greater is required!");
+
+#ifdef WIN32
+ glActiveTexture_ = (PFNGLACTIVETEXTUREPROC) getprocaddress("glActiveTexture");
+
+ glBlendEquation_ = (PFNGLBLENDEQUATIONPROC) getprocaddress("glBlendEquation");
+ glBlendColor_ = (PFNGLBLENDCOLORPROC) getprocaddress("glBlendColor");
+
+ glTexImage3D_ = (PFNGLTEXIMAGE3DPROC) getprocaddress("glTexImage3D");
+ glTexSubImage3D_ = (PFNGLTEXSUBIMAGE3DPROC) getprocaddress("glTexSubImage3D");
+ glCopyTexSubImage3D_ = (PFNGLCOPYTEXSUBIMAGE3DPROC) getprocaddress("glCopyTexSubImage3D");
+
+ glCompressedTexImage3D_ = (PFNGLCOMPRESSEDTEXIMAGE3DPROC) getprocaddress("glCompressedTexImage3D");
+ glCompressedTexImage2D_ = (PFNGLCOMPRESSEDTEXIMAGE2DPROC) getprocaddress("glCompressedTexImage2D");
+ glCompressedTexSubImage3D_ = (PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC) getprocaddress("glCompressedTexSubImage3D");
+ glCompressedTexSubImage2D_ = (PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC) getprocaddress("glCompressedTexSubImage2D");
+ glGetCompressedTexImage_ = (PFNGLGETCOMPRESSEDTEXIMAGEPROC) getprocaddress("glGetCompressedTexImage");
+
+ glDrawRangeElements_ = (PFNGLDRAWRANGEELEMENTSPROC) getprocaddress("glDrawRangeElements");
+#endif
+
+#ifndef __APPLE__
+ glMultiDrawArrays_ = (PFNGLMULTIDRAWARRAYSPROC) getprocaddress("glMultiDrawArrays");
+ glMultiDrawElements_ = (PFNGLMULTIDRAWELEMENTSPROC) getprocaddress("glMultiDrawElements");
+
+ glBlendFuncSeparate_ = (PFNGLBLENDFUNCSEPARATEPROC) getprocaddress("glBlendFuncSeparate");
+ glBlendEquationSeparate_ = (PFNGLBLENDEQUATIONSEPARATEPROC) getprocaddress("glBlendEquationSeparate");
+ glStencilOpSeparate_ = (PFNGLSTENCILOPSEPARATEPROC) getprocaddress("glStencilOpSeparate");
+ glStencilFuncSeparate_ = (PFNGLSTENCILFUNCSEPARATEPROC) getprocaddress("glStencilFuncSeparate");
+ glStencilMaskSeparate_ = (PFNGLSTENCILMASKSEPARATEPROC) getprocaddress("glStencilMaskSeparate");
+
+ glGenBuffers_ = (PFNGLGENBUFFERSPROC) getprocaddress("glGenBuffers");
+ glBindBuffer_ = (PFNGLBINDBUFFERPROC) getprocaddress("glBindBuffer");
+ glMapBuffer_ = (PFNGLMAPBUFFERPROC) getprocaddress("glMapBuffer");
+ glUnmapBuffer_ = (PFNGLUNMAPBUFFERPROC) getprocaddress("glUnmapBuffer");
+ glBufferData_ = (PFNGLBUFFERDATAPROC) getprocaddress("glBufferData");
+ glBufferSubData_ = (PFNGLBUFFERSUBDATAPROC) getprocaddress("glBufferSubData");
+ glDeleteBuffers_ = (PFNGLDELETEBUFFERSPROC) getprocaddress("glDeleteBuffers");
+ glGetBufferSubData_ = (PFNGLGETBUFFERSUBDATAPROC) getprocaddress("glGetBufferSubData");
+
+ glGetQueryiv_ = (PFNGLGETQUERYIVPROC) getprocaddress("glGetQueryiv");
+ glGenQueries_ = (PFNGLGENQUERIESPROC) getprocaddress("glGenQueries");
+ glDeleteQueries_ = (PFNGLDELETEQUERIESPROC) getprocaddress("glDeleteQueries");
+ glBeginQuery_ = (PFNGLBEGINQUERYPROC) getprocaddress("glBeginQuery");
+ glEndQuery_ = (PFNGLENDQUERYPROC) getprocaddress("glEndQuery");
+ glGetQueryObjectiv_ = (PFNGLGETQUERYOBJECTIVPROC) getprocaddress("glGetQueryObjectiv");
+ glGetQueryObjectuiv_ = (PFNGLGETQUERYOBJECTUIVPROC) getprocaddress("glGetQueryObjectuiv");
+
+ glCreateProgram_ = (PFNGLCREATEPROGRAMPROC) getprocaddress("glCreateProgram");
+ glDeleteProgram_ = (PFNGLDELETEPROGRAMPROC) getprocaddress("glDeleteProgram");
+ glUseProgram_ = (PFNGLUSEPROGRAMPROC) getprocaddress("glUseProgram");
+ glCreateShader_ = (PFNGLCREATESHADERPROC) getprocaddress("glCreateShader");
+ glDeleteShader_ = (PFNGLDELETESHADERPROC) getprocaddress("glDeleteShader");
+ glShaderSource_ = (PFNGLSHADERSOURCEPROC) getprocaddress("glShaderSource");
+ glCompileShader_ = (PFNGLCOMPILESHADERPROC) getprocaddress("glCompileShader");
+ glGetShaderiv_ = (PFNGLGETSHADERIVPROC) getprocaddress("glGetShaderiv");
+ glGetProgramiv_ = (PFNGLGETPROGRAMIVPROC) getprocaddress("glGetProgramiv");
+ glAttachShader_ = (PFNGLATTACHSHADERPROC) getprocaddress("glAttachShader");
+ glGetProgramInfoLog_ = (PFNGLGETPROGRAMINFOLOGPROC) getprocaddress("glGetProgramInfoLog");
+ glGetShaderInfoLog_ = (PFNGLGETSHADERINFOLOGPROC) getprocaddress("glGetShaderInfoLog");
+ glLinkProgram_ = (PFNGLLINKPROGRAMPROC) getprocaddress("glLinkProgram");
+ glGetUniformLocation_ = (PFNGLGETUNIFORMLOCATIONPROC) getprocaddress("glGetUniformLocation");
+ glUniform1f_ = (PFNGLUNIFORM1FPROC) getprocaddress("glUniform1f");
+ glUniform2f_ = (PFNGLUNIFORM2FPROC) getprocaddress("glUniform2f");
+ glUniform3f_ = (PFNGLUNIFORM3FPROC) getprocaddress("glUniform3f");
+ glUniform4f_ = (PFNGLUNIFORM4FPROC) getprocaddress("glUniform4f");
+ glUniform1fv_ = (PFNGLUNIFORM1FVPROC) getprocaddress("glUniform1fv");
+ glUniform2fv_ = (PFNGLUNIFORM2FVPROC) getprocaddress("glUniform2fv");
+ glUniform3fv_ = (PFNGLUNIFORM3FVPROC) getprocaddress("glUniform3fv");
+ glUniform4fv_ = (PFNGLUNIFORM4FVPROC) getprocaddress("glUniform4fv");
+ glUniform1i_ = (PFNGLUNIFORM1IPROC) getprocaddress("glUniform1i");
+ glUniform2i_ = (PFNGLUNIFORM2IPROC) getprocaddress("glUniform2i");
+ glUniform3i_ = (PFNGLUNIFORM3IPROC) getprocaddress("glUniform3i");
+ glUniform4i_ = (PFNGLUNIFORM4IPROC) getprocaddress("glUniform4i");
+ glUniform1iv_ = (PFNGLUNIFORM1IVPROC) getprocaddress("glUniform1iv");
+ glUniform2iv_ = (PFNGLUNIFORM2IVPROC) getprocaddress("glUniform2iv");
+ glUniform3iv_ = (PFNGLUNIFORM3IVPROC) getprocaddress("glUniform3iv");
+ glUniform4iv_ = (PFNGLUNIFORM4IVPROC) getprocaddress("glUniform4iv");
+ glUniformMatrix2fv_ = (PFNGLUNIFORMMATRIX2FVPROC) getprocaddress("glUniformMatrix2fv");
+ glUniformMatrix3fv_ = (PFNGLUNIFORMMATRIX3FVPROC) getprocaddress("glUniformMatrix3fv");
+ glUniformMatrix4fv_ = (PFNGLUNIFORMMATRIX4FVPROC) getprocaddress("glUniformMatrix4fv");
+ glBindAttribLocation_ = (PFNGLBINDATTRIBLOCATIONPROC) getprocaddress("glBindAttribLocation");
+ glGetActiveUniform_ = (PFNGLGETACTIVEUNIFORMPROC) getprocaddress("glGetActiveUniform");
+ glEnableVertexAttribArray_ = (PFNGLENABLEVERTEXATTRIBARRAYPROC) getprocaddress("glEnableVertexAttribArray");
+ glDisableVertexAttribArray_ = (PFNGLDISABLEVERTEXATTRIBARRAYPROC) getprocaddress("glDisableVertexAttribArray");
+
+ glVertexAttrib1f_ = (PFNGLVERTEXATTRIB1FPROC) getprocaddress("glVertexAttrib1f");
+ glVertexAttrib1fv_ = (PFNGLVERTEXATTRIB1FVPROC) getprocaddress("glVertexAttrib1fv");
+ glVertexAttrib1s_ = (PFNGLVERTEXATTRIB1SPROC) getprocaddress("glVertexAttrib1s");
+ glVertexAttrib1sv_ = (PFNGLVERTEXATTRIB1SVPROC) getprocaddress("glVertexAttrib1sv");
+ glVertexAttrib2f_ = (PFNGLVERTEXATTRIB2FPROC) getprocaddress("glVertexAttrib2f");
+ glVertexAttrib2fv_ = (PFNGLVERTEXATTRIB2FVPROC) getprocaddress("glVertexAttrib2fv");
+ glVertexAttrib2s_ = (PFNGLVERTEXATTRIB2SPROC) getprocaddress("glVertexAttrib2s");
+ glVertexAttrib2sv_ = (PFNGLVERTEXATTRIB2SVPROC) getprocaddress("glVertexAttrib2sv");
+ glVertexAttrib3f_ = (PFNGLVERTEXATTRIB3FPROC) getprocaddress("glVertexAttrib3f");
+ glVertexAttrib3fv_ = (PFNGLVERTEXATTRIB3FVPROC) getprocaddress("glVertexAttrib3fv");
+ glVertexAttrib3s_ = (PFNGLVERTEXATTRIB3SPROC) getprocaddress("glVertexAttrib3s");
+ glVertexAttrib3sv_ = (PFNGLVERTEXATTRIB3SVPROC) getprocaddress("glVertexAttrib3sv");
+ glVertexAttrib4f_ = (PFNGLVERTEXATTRIB4FPROC) getprocaddress("glVertexAttrib4f");
+ glVertexAttrib4fv_ = (PFNGLVERTEXATTRIB4FVPROC) getprocaddress("glVertexAttrib4fv");
+ glVertexAttrib4s_ = (PFNGLVERTEXATTRIB4SPROC) getprocaddress("glVertexAttrib4s");
+ glVertexAttrib4sv_ = (PFNGLVERTEXATTRIB4SVPROC) getprocaddress("glVertexAttrib4sv");
+ glVertexAttrib4bv_ = (PFNGLVERTEXATTRIB4BVPROC) getprocaddress("glVertexAttrib4bv");
+ glVertexAttrib4iv_ = (PFNGLVERTEXATTRIB4IVPROC) getprocaddress("glVertexAttrib4iv");
+ glVertexAttrib4ubv_ = (PFNGLVERTEXATTRIB4UBVPROC) getprocaddress("glVertexAttrib4ubv");
+ glVertexAttrib4uiv_ = (PFNGLVERTEXATTRIB4UIVPROC) getprocaddress("glVertexAttrib4uiv");
+ glVertexAttrib4usv_ = (PFNGLVERTEXATTRIB4USVPROC) getprocaddress("glVertexAttrib4usv");
+ glVertexAttrib4Nbv_ = (PFNGLVERTEXATTRIB4NBVPROC) getprocaddress("glVertexAttrib4Nbv");
+ glVertexAttrib4Niv_ = (PFNGLVERTEXATTRIB4NIVPROC) getprocaddress("glVertexAttrib4Niv");
+ glVertexAttrib4Nub_ = (PFNGLVERTEXATTRIB4NUBPROC) getprocaddress("glVertexAttrib4Nub");
+ glVertexAttrib4Nubv_ = (PFNGLVERTEXATTRIB4NUBVPROC) getprocaddress("glVertexAttrib4Nubv");
+ glVertexAttrib4Nuiv_ = (PFNGLVERTEXATTRIB4NUIVPROC) getprocaddress("glVertexAttrib4Nuiv");
+ glVertexAttrib4Nusv_ = (PFNGLVERTEXATTRIB4NUSVPROC) getprocaddress("glVertexAttrib4Nusv");
+ glVertexAttribPointer_ = (PFNGLVERTEXATTRIBPOINTERPROC) getprocaddress("glVertexAttribPointer");
+
+ glDrawBuffers_ = (PFNGLDRAWBUFFERSPROC) getprocaddress("glDrawBuffers");
+#endif
+
+ if(glversion >= 300)
+ {
+ glGetStringi_ = (PFNGLGETSTRINGIPROC) getprocaddress("glGetStringi");
+ glBindFragDataLocation_ = (PFNGLBINDFRAGDATALOCATIONPROC)getprocaddress("glBindFragDataLocation");
+ }
+
+ const char *glslstr = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);
+ uint glslmajorversion, glslminorversion;
+ if(glslstr && sscanf(glslstr, " %u.%u", &glslmajorversion, &glslminorversion) == 2) glslversion = glslmajorversion*100 + glslminorversion;
+
+ if(glslversion < 120) fatal("GLSL 1.20 or greater is required!");
+
+ parseglexts();
+
+ GLint val;
+ glGetIntegerv(GL_MAX_TEXTURE_SIZE, &val);
+ hwtexsize = val;
+ glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &val);
+ hwcubetexsize = val;
+
+ if(glversion >= 300 || hasext("GL_ARB_texture_float") || hasext("GL_ATI_texture_float"))
+ {
+ hasTF = true;
+ if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_float extension.");
+ shadowmap = 1;
+ extern int smoothshadowmappeel;
+ smoothshadowmappeel = 1;
+ }
+
+ if(glversion >= 300 || hasext("GL_ARB_texture_rg"))
+ {
+ hasTRG = true;
+ if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_rg extension.");
+ }
+
+ if(glversion >= 300 || hasext("GL_ARB_framebuffer_object"))
+ {
+ glBindRenderbuffer_ = (PFNGLBINDRENDERBUFFERPROC) getprocaddress("glBindRenderbuffer");
+ glDeleteRenderbuffers_ = (PFNGLDELETERENDERBUFFERSPROC) getprocaddress("glDeleteRenderbuffers");
+ glGenRenderbuffers_ = (PFNGLGENFRAMEBUFFERSPROC) getprocaddress("glGenRenderbuffers");
+ glRenderbufferStorage_ = (PFNGLRENDERBUFFERSTORAGEPROC) getprocaddress("glRenderbufferStorage");
+ glCheckFramebufferStatus_ = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) getprocaddress("glCheckFramebufferStatus");
+ glBindFramebuffer_ = (PFNGLBINDFRAMEBUFFERPROC) getprocaddress("glBindFramebuffer");
+ glDeleteFramebuffers_ = (PFNGLDELETEFRAMEBUFFERSPROC) getprocaddress("glDeleteFramebuffers");
+ glGenFramebuffers_ = (PFNGLGENFRAMEBUFFERSPROC) getprocaddress("glGenFramebuffers");
+ glFramebufferTexture2D_ = (PFNGLFRAMEBUFFERTEXTURE2DPROC) getprocaddress("glFramebufferTexture2D");
+ glFramebufferRenderbuffer_ = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)getprocaddress("glFramebufferRenderbuffer");
+ glGenerateMipmap_ = (PFNGLGENERATEMIPMAPPROC) getprocaddress("glGenerateMipmap");
+ glBlitFramebuffer_ = (PFNGLBLITFRAMEBUFFERPROC) getprocaddress("glBlitFramebuffer");
+ hasAFBO = hasFBO = hasFBB = hasDS = true;
+ if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_framebuffer_object extension.");
+ }
+ else if(hasext("GL_EXT_framebuffer_object"))
+ {
+ glBindRenderbuffer_ = (PFNGLBINDRENDERBUFFERPROC) getprocaddress("glBindRenderbufferEXT");
+ glDeleteRenderbuffers_ = (PFNGLDELETERENDERBUFFERSPROC) getprocaddress("glDeleteRenderbuffersEXT");
+ glGenRenderbuffers_ = (PFNGLGENFRAMEBUFFERSPROC) getprocaddress("glGenRenderbuffersEXT");
+ glRenderbufferStorage_ = (PFNGLRENDERBUFFERSTORAGEPROC) getprocaddress("glRenderbufferStorageEXT");
+ glCheckFramebufferStatus_ = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) getprocaddress("glCheckFramebufferStatusEXT");
+ glBindFramebuffer_ = (PFNGLBINDFRAMEBUFFERPROC) getprocaddress("glBindFramebufferEXT");
+ glDeleteFramebuffers_ = (PFNGLDELETEFRAMEBUFFERSPROC) getprocaddress("glDeleteFramebuffersEXT");
+ glGenFramebuffers_ = (PFNGLGENFRAMEBUFFERSPROC) getprocaddress("glGenFramebuffersEXT");
+ glFramebufferTexture2D_ = (PFNGLFRAMEBUFFERTEXTURE2DPROC) getprocaddress("glFramebufferTexture2DEXT");
+ glFramebufferRenderbuffer_ = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)getprocaddress("glFramebufferRenderbufferEXT");
+ glGenerateMipmap_ = (PFNGLGENERATEMIPMAPPROC) getprocaddress("glGenerateMipmapEXT");
+ hasFBO = true;
+ if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_framebuffer_object extension.");
+
+ if(hasext("GL_EXT_framebuffer_blit"))
+ {
+ glBlitFramebuffer_ = (PFNGLBLITFRAMEBUFFERPROC) getprocaddress("glBlitFramebufferEXT");
+ hasFBB = true;
+ if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_framebuffer_blit extension.");
+ }
+
+ if(hasext("GL_EXT_packed_depth_stencil") || hasext("GL_NV_packed_depth_stencil"))
+ {
+ hasDS = true;
+ if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_packed_depth_stencil extension.");
+ }
+ }
+ else fatal("Framebuffer object support is required!");
+
+ extern int fpdepthfx;
+ if(ati)
+ {
+ //conoutf(CON_WARN, "WARNING: ATI cards may show garbage in skybox. (use \"/ati_skybox_bug 1\" to fix)");
+
+ minimizetcusage = 1;
+ if(hasTF && hasTRG) fpdepthfx = 1;
+ // On Catalyst 10.2, issuing an occlusion query on the first draw using a given cubemap texture causes a nasty crash
+ ati_cubemap_bug = 1;
+ }
+ else if(nvidia)
+ {
+ reservevpparams = 10;
+ rtsharefb = 0; // work-around for strange driver stalls involving when using many FBOs
+ extern int filltjoints;
+ if(glversion < 300 && !hasext("GL_EXT_gpu_shader4")) filltjoints = 0; // DX9 or less NV cards seem to not cause many sparklies
+
+ if(hasTF && hasTRG) fpdepthfx = 1;
+ }
+ else
+ {
+ if(intel)
+ {
+#ifdef WIN32
+ intel_vertexarray_bug = 1;
+ // MapBufferRange is buggy on older Intel drivers on Windows
+ if(glversion <= 310) intel_mapbufferrange_bug = 1;
+#endif
+ }
+
+ reservevpparams = 20;
+
+ if(mesa) mesa_swap_bug = 1;
+ }
+
+ if(glversion >= 300 || hasext("GL_ARB_map_buffer_range"))
+ {
+ glMapBufferRange_ = (PFNGLMAPBUFFERRANGEPROC) getprocaddress("glMapBufferRange");
+ glFlushMappedBufferRange_ = (PFNGLFLUSHMAPPEDBUFFERRANGEPROC)getprocaddress("glFlushMappedBufferRange");
+ hasMBR = true;
+ if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_map_buffer_range.");
+ }
+
+ if(glversion >= 310 || hasext("GL_ARB_uniform_buffer_object"))
+ {
+ glGetUniformIndices_ = (PFNGLGETUNIFORMINDICESPROC) getprocaddress("glGetUniformIndices");
+ glGetActiveUniformsiv_ = (PFNGLGETACTIVEUNIFORMSIVPROC) getprocaddress("glGetActiveUniformsiv");
+ glGetUniformBlockIndex_ = (PFNGLGETUNIFORMBLOCKINDEXPROC) getprocaddress("glGetUniformBlockIndex");
+ glGetActiveUniformBlockiv_ = (PFNGLGETACTIVEUNIFORMBLOCKIVPROC)getprocaddress("glGetActiveUniformBlockiv");
+ glUniformBlockBinding_ = (PFNGLUNIFORMBLOCKBINDINGPROC) getprocaddress("glUniformBlockBinding");
+ glBindBufferBase_ = (PFNGLBINDBUFFERBASEPROC) getprocaddress("glBindBufferBase");
+ glBindBufferRange_ = (PFNGLBINDBUFFERRANGEPROC) getprocaddress("glBindBufferRange");
+
+ useubo = 1;
+ hasUBO = true;
+ if(glversion < 310 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_uniform_buffer_object extension.");
+ }
+
+ if(glversion >= 300 || hasext("GL_ARB_vertex_array_object"))
+ {
+ glBindVertexArray_ = (PFNGLBINDVERTEXARRAYPROC) getprocaddress("glBindVertexArray");
+ glDeleteVertexArrays_ = (PFNGLDELETEVERTEXARRAYSPROC)getprocaddress("glDeleteVertexArrays");
+ glGenVertexArrays_ = (PFNGLGENVERTEXARRAYSPROC) getprocaddress("glGenVertexArrays");
+ glIsVertexArray_ = (PFNGLISVERTEXARRAYPROC) getprocaddress("glIsVertexArray");
+ hasVAO = true;
+ if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_vertex_array_object extension.");
+ }
+ else if(hasext("GL_APPLE_vertex_array_object"))
+ {
+ glBindVertexArray_ = (PFNGLBINDVERTEXARRAYPROC) getprocaddress("glBindVertexArrayAPPLE");
+ glDeleteVertexArrays_ = (PFNGLDELETEVERTEXARRAYSPROC)getprocaddress("glDeleteVertexArraysAPPLE");
+ glGenVertexArrays_ = (PFNGLGENVERTEXARRAYSPROC) getprocaddress("glGenVertexArraysAPPLE");
+ glIsVertexArray_ = (PFNGLISVERTEXARRAYPROC) getprocaddress("glIsVertexArrayAPPLE");
+ hasVAO = true;
+ if(dbgexts) conoutf(CON_INIT, "Using GL_APPLE_vertex_array_object extension.");
+ }
+
+ if(glversion >= 330 || hasext("GL_ARB_texture_swizzle") || hasext("GL_EXT_texture_swizzle"))
+ {
+ hasTSW = true;
+ if(glversion < 330 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_swizzle extension.");
+ }
+
+ if(hasext("GL_EXT_texture_compression_s3tc"))
+ {
+ hasS3TC = true;
+#ifdef __APPLE__
+ usetexcompress = 1;
+#else
+ if(!mesa) usetexcompress = 2;
+#endif
+ if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_s3tc extension.");
+ }
+ else if(hasext("GL_EXT_texture_compression_dxt1") && hasext("GL_ANGLE_texture_compression_dxt3") && hasext("GL_ANGLE_texture_compression_dxt5"))
+ {
+ hasS3TC = true;
+ if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_dxt1 extension.");
+ }
+ if(hasext("GL_3DFX_texture_compression_FXT1"))
+ {
+ hasFXT1 = true;
+ if(mesa) usetexcompress = max(usetexcompress, 1);
+ if(dbgexts) conoutf(CON_INIT, "Using GL_3DFX_texture_compression_FXT1.");
+ }
+ if(hasext("GL_EXT_texture_compression_latc"))
+ {
+ hasLATC = true;
+ if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_latc extension.");
+ }
+ if(glversion >= 300 || hasext("GL_ARB_texture_compression_rgtc") || hasext("GL_EXT_texture_compression_rgtc"))
+ {
+ hasRGTC = true;
+ if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_compression_rgtc extension.");
+ }
+
+ if(hasext("GL_EXT_texture_filter_anisotropic"))
+ {
+ GLint val;
+ glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &val);
+ hwmaxaniso = val;
+ hasAF = true;
+ if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_filter_anisotropic extension.");
+ }
+
+ if(glversion >= 300 || hasext("GL_EXT_gpu_shader4"))
+ {
+ // on DX10 or above class cards (i.e. GF8 or RadeonHD) enable expensive features
+ extern int grass, glare, maxdynlights, depthfxsize, blurdepthfx, texcompress;
+ grass = 1;
+ waterfallrefract = 1;
+ glare = 1;
+ maxdynlights = MAXDYNLIGHTS;
+ depthfxsize = 10;
+ blurdepthfx = 0;
+ texcompress = max(texcompress, 1024 + 1);
+ }
+}
+
+void glext(char *ext)
+{
+ intret(hasext(ext) ? 1 : 0);
+}
+COMMAND(glext, "s");
+
+void gl_resize()
+{
+ glViewport(0, 0, screenw, screenh);
+}
+
+void gl_init()
+{
+ glClearColor(0, 0, 0, 0);
+ glClearDepth(1);
+ glDepthFunc(GL_LESS);
+ glDisable(GL_DEPTH_TEST);
+
+ glEnable(GL_LINE_SMOOTH);
+ //glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
+
+ glFrontFace(GL_CW);
+ glCullFace(GL_BACK);
+ glDisable(GL_CULL_FACE);
+
+ gle::setup();
+
+ setupshaders();
+
+ setuptexcompress();
+
+ gl_resize();
+}
+
+VAR(wireframe, 0, 0, 1);
+
+ICOMMAND(getcamyaw, "", (), floatret(camera1->yaw));
+ICOMMAND(getcampitch, "", (), floatret(camera1->pitch));
+ICOMMAND(getcamroll, "", (), floatret(camera1->roll));
+ICOMMAND(getcampos, "", (),
+{
+ defformatstring(pos, "%s %s %s", floatstr(camera1->o.x), floatstr(camera1->o.y), floatstr(camera1->o.z));
+ result(pos);
+});
+
+vec worldpos, camdir, camright, camup;
+
+void setcammatrix()
+{
+ // move from RH to Z-up LH quake style worldspace
+ cammatrix = viewmatrix;
+ cammatrix.rotate_around_y(camera1->roll*RAD);
+ cammatrix.rotate_around_x(camera1->pitch*-RAD);
+ cammatrix.rotate_around_z(camera1->yaw*-RAD);
+ cammatrix.translate(vec(camera1->o).neg());
+
+ cammatrix.transposedtransformnormal(vec(viewmatrix.b), camdir);
+ cammatrix.transposedtransformnormal(vec(viewmatrix.a).neg(), camright);
+ cammatrix.transposedtransformnormal(vec(viewmatrix.c), camup);
+
+ if(!drawtex)
+ {
+ if(raycubepos(camera1->o, camdir, worldpos, 0, RAY_CLIPMAT|RAY_SKIPFIRST) == -1)
+ worldpos = vec(camdir).mul(2*worldsize).add(camera1->o); // if nothing is hit, just far away in the view direction
+ }
+}
+
+void setcamprojmatrix(bool init = true, bool flush = false)
+{
+ if(init)
+ {
+ setcammatrix();
+ }
+
+ camprojmatrix.muld(projmatrix, cammatrix);
+
+ if(init)
+ {
+ invcammatrix.invert(cammatrix);
+ invcamprojmatrix.invert(camprojmatrix);
+ }
+
+ GLOBALPARAM(camprojmatrix, camprojmatrix);
+
+ if(fogging)
+ {
+ vec fogplane(cammatrix.c);
+ fogplane.x /= projmatrix.a.x;
+ fogplane.y /= projmatrix.b.y;
+ fogplane.z /= projmatrix.c.w;
+ GLOBALPARAMF(fogplane, fogplane.x, fogplane.y, 0, fogplane.z);
+ }
+ else
+ {
+ vec2 lineardepthscale = projmatrix.lineardepthscale();
+ GLOBALPARAMF(fogplane, 0, 0, lineardepthscale.x, lineardepthscale.y);
+ }
+
+ if(flush && Shader::lastshader) Shader::lastshader->flushparams();
+}
+
+matrix4 hudmatrix, hudmatrixstack[64];
+int hudmatrixpos = 0;
+
+void resethudmatrix()
+{
+ hudmatrixpos = 0;
+ GLOBALPARAM(hudmatrix, hudmatrix);
+}
+
+void pushhudmatrix()
+{
+ if(hudmatrixpos >= 0 && hudmatrixpos < int(sizeof(hudmatrixstack)/sizeof(hudmatrixstack[0]))) hudmatrixstack[hudmatrixpos] = hudmatrix;
+ ++hudmatrixpos;
+}
+
+void flushhudmatrix(bool flushparams)
+{
+ GLOBALPARAM(hudmatrix, hudmatrix);
+ if(flushparams && Shader::lastshader) Shader::lastshader->flushparams();
+}
+
+void pophudmatrix(bool flush, bool flushparams)
+{
+ --hudmatrixpos;
+ if(hudmatrixpos >= 0 && hudmatrixpos < int(sizeof(hudmatrixstack)/sizeof(hudmatrixstack[0])))
+ {
+ hudmatrix = hudmatrixstack[hudmatrixpos];
+ if(flush) flushhudmatrix(flushparams);
+ }
+}
+
+void pushhudscale(float sx, float sy)
+{
+ if(!sy) sy = sx;
+ pushhudmatrix();
+ hudmatrix.scale(sx, sy, 1);
+ flushhudmatrix();
+}
+
+void pushhudtranslate(float tx, float ty, float sx, float sy)
+{
+ if(!sy) sy = sx;
+ pushhudmatrix();
+ hudmatrix.translate(tx, ty, 0);
+ if(sy) hudmatrix.scale(sx, sy, 1);
+ flushhudmatrix();
+}
+
+float curfov = 100, curavatarfov = 65, fovy, aspect;
+int farplane;
+VARP(zoominvel, 0, 250, 5000);
+VARP(zoomoutvel, 0, 100, 5000);
+VARP(zoomfov, 10, 35, 60);
+VARP(fov, 10, 100, 150);
+VAR(avatarzoomfov, 10, 25, 60);
+VAR(avatarfov, 10, 65, 150);
+FVAR(avatardepth, 0, 0.5f, 1);
+FVARNP(aspect, forceaspect, 0, 0, 1e3f);
+
+static float zoomprogress = 0;
+VAR(zoom, -1, 0, 1);
+
+void disablezoom()
+{
+ zoom = 0;
+ zoomprogress = 0;
+}
+
+void computezoom()
+{
+ if(!zoom) { zoomprogress = 0; curfov = fov; curavatarfov = avatarfov; return; }
+ if(zoom > 0) zoomprogress = zoominvel ? min(zoomprogress + float(elapsedtime) / zoominvel, 1.0f) : 1;
+ else
+ {
+ zoomprogress = zoomoutvel ? max(zoomprogress - float(elapsedtime) / zoomoutvel, 0.0f) : 0;
+ if(zoomprogress <= 0) zoom = 0;
+ }
+ curfov = zoomfov*zoomprogress + fov*(1 - zoomprogress);
+ curavatarfov = avatarzoomfov*zoomprogress + avatarfov*(1 - zoomprogress);
+}
+
+FVARP(zoomsens, 1e-3f, 1, 1000);
+FVARP(zoomaccel, 0, 0, 1000);
+VARP(zoomautosens, 0, 1, 1);
+FVARP(sensitivity, 1e-3f, 3, 1000);
+FVARP(sensitivityscale, 1e-3f, 1, 1000);
+VARP(invmouse, 0, 0, 1);
+FVARP(mouseaccel, 0, 0, 1000);
+
+VAR(thirdperson, 0, 0, 2);
+FVAR(thirdpersondistance, 0, 20, 50);
+FVAR(thirdpersonup, -25, 0, 25);
+FVAR(thirdpersonside, -25, 0, 25);
+physent *camera1 = NULL;
+bool detachedcamera = false;
+bool isthirdperson() { return player!=camera1 || detachedcamera || reflecting; }
+
+void fixcamerarange()
+{
+ const float MAXPITCH = 90.0f;
+ if(camera1->pitch>MAXPITCH) camera1->pitch = MAXPITCH;
+ if(camera1->pitch<-MAXPITCH) camera1->pitch = -MAXPITCH;
+ while(camera1->yaw<0.0f) camera1->yaw += 360.0f;
+ while(camera1->yaw>=360.0f) camera1->yaw -= 360.0f;
+}
+
+void mousemove(int dx, int dy)
+{
+ if(!game::allowmouselook()) return;
+ float cursens = sensitivity, curaccel = mouseaccel;
+ if(zoom)
+ {
+ if(zoomautosens)
+ {
+ cursens = float(sensitivity*zoomfov)/fov;
+ curaccel = float(mouseaccel*zoomfov)/fov;
+ }
+ else
+ {
+ cursens = zoomsens;
+ curaccel = zoomaccel;
+ }
+ }
+ if(curaccel && curtime && (dx || dy)) cursens += curaccel * sqrtf(dx*dx + dy*dy)/curtime;
+ cursens /= 33.0f*sensitivityscale;
+ camera1->yaw += dx*cursens;
+ camera1->pitch -= dy*cursens*(invmouse ? -1 : 1);
+ fixcamerarange();
+ if(camera1!=player && !detachedcamera)
+ {
+ player->yaw = camera1->yaw;
+ player->pitch = camera1->pitch;
+ }
+}
+
+void recomputecamera()
+{
+ game::setupcamera();
+ computezoom();
+
+ bool allowthirdperson = game::allowthirdperson();
+ bool shoulddetach = (allowthirdperson && thirdperson > 1) || game::detachcamera();
+ if((!allowthirdperson || !thirdperson) && !shoulddetach)
+ {
+ camera1 = player;
+ detachedcamera = false;
+ }
+ else
+ {
+ static physent tempcamera;
+ camera1 = &tempcamera;
+ if(detachedcamera && shoulddetach) camera1->o = player->o;
+ else
+ {
+ *camera1 = *player;
+ detachedcamera = shoulddetach;
+ }
+ camera1->reset();
+ camera1->type = ENT_CAMERA;
+ camera1->move = -1;
+ camera1->eyeheight = camera1->aboveeye = camera1->radius = camera1->xradius = camera1->yradius = 2;
+
+ matrix3 orient;
+ orient.identity();
+ orient.rotate_around_z(camera1->yaw*RAD);
+ orient.rotate_around_x(camera1->pitch*RAD);
+ orient.rotate_around_y(camera1->roll*-RAD);
+ vec dir = vec(orient.b).neg(), side = vec(orient.a).neg(), up = orient.c;
+
+ if(game::collidecamera())
+ {
+ movecamera(camera1, dir, thirdpersondistance, 1);
+ movecamera(camera1, dir, clamp(thirdpersondistance - camera1->o.dist(player->o), 0.0f, 1.0f), 0.1f);
+ if(thirdpersonup)
+ {
+ vec pos = camera1->o;
+ float dist = fabs(thirdpersonup);
+ if(thirdpersonup < 0) up.neg();
+ movecamera(camera1, up, dist, 1);
+ movecamera(camera1, up, clamp(dist - camera1->o.dist(pos), 0.0f, 1.0f), 0.1f);
+ }
+ if(thirdpersonside)
+ {
+ vec pos = camera1->o;
+ float dist = fabs(thirdpersonside);
+ if(thirdpersonside < 0) side.neg();
+ movecamera(camera1, side, dist, 1);
+ movecamera(camera1, side, clamp(dist - camera1->o.dist(pos), 0.0f, 1.0f), 0.1f);
+ }
+ }
+ else
+ {
+ camera1->o.add(vec(dir).mul(thirdpersondistance));
+ if(thirdpersonup) camera1->o.add(vec(up).mul(thirdpersonup));
+ if(thirdpersonside) camera1->o.add(vec(side).mul(thirdpersonside));
+ }
+ }
+
+ setviewcell(camera1->o);
+}
+
+extern const matrix4 viewmatrix(vec(-1, 0, 0), vec(0, 0, 1), vec(0, -1, 0));
+matrix4 cammatrix, projmatrix, camprojmatrix, invcammatrix, invcamprojmatrix;
+
+FVAR(nearplane, 0.01f, 0.54f, 2.0f);
+
+vec calcavatarpos(const vec &pos, float dist)
+{
+ vec eyepos;
+ cammatrix.transform(pos, eyepos);
+ GLdouble ydist = nearplane * tan(curavatarfov/2*RAD), xdist = ydist * aspect;
+ vec4 scrpos;
+ scrpos.x = eyepos.x*nearplane/xdist;
+ scrpos.y = eyepos.y*nearplane/ydist;
+ scrpos.z = (eyepos.z*(farplane + nearplane) - 2*nearplane*farplane) / (farplane - nearplane);
+ scrpos.w = -eyepos.z;
+
+ vec worldpos = invcamprojmatrix.perspectivetransform(scrpos);
+ vec dir = vec(worldpos).sub(camera1->o).rescale(dist);
+ return dir.add(camera1->o);
+}
+
+VAR(reflectclip, 0, 6, 64);
+VAR(reflectclipavatar, -64, 0, 64);
+
+matrix4 clipmatrix, noclipmatrix;
+
+void renderavatar()
+{
+ if(isthirdperson()) return;
+
+ matrix4 oldprojmatrix = projmatrix;
+ projmatrix.perspective(curavatarfov, aspect, nearplane, farplane);
+ projmatrix.scalez(avatardepth);
+ setcamprojmatrix(false);
+
+ game::renderavatar();
+
+ projmatrix = oldprojmatrix;
+ setcamprojmatrix(false);
+}
+
+FVAR(polygonoffsetfactor, -1e4f, -3.0f, 1e4f);
+FVAR(polygonoffsetunits, -1e4f, -3.0f, 1e4f);
+FVAR(depthoffset, -1e4f, 0.01f, 1e4f);
+
+matrix4 nooffsetmatrix;
+
+void enablepolygonoffset(GLenum type)
+{
+ if(!depthoffset)
+ {
+ glPolygonOffset(polygonoffsetfactor, polygonoffsetunits);
+ glEnable(type);
+ return;
+ }
+
+ bool clipped = reflectz < 1e15f && reflectclip;
+
+ nooffsetmatrix = projmatrix;
+ projmatrix.d.z += depthoffset * (clipped ? noclipmatrix.c.z : projmatrix.c.z);
+ setcamprojmatrix(false, true);
+}
+
+void disablepolygonoffset(GLenum type)
+{
+ if(!depthoffset)
+ {
+ glDisable(type);
+ return;
+ }
+
+ projmatrix = nooffsetmatrix;
+ setcamprojmatrix(false, true);
+}
+
+void calcspherescissor(const vec &center, float size, float &sx1, float &sy1, float &sx2, float &sy2)
+{
+ vec worldpos(center), e;
+ if(reflecting) worldpos.z = 2*reflectz - worldpos.z;
+ cammatrix.transform(worldpos, e);
+ if(e.z > 2*size) { sx1 = sy1 = 1; sx2 = sy2 = -1; return; }
+ float zzrr = e.z*e.z - size*size,
+ dx = e.x*e.x + zzrr, dy = e.y*e.y + zzrr,
+ focaldist = 1.0f/tan(fovy*0.5f*RAD);
+ sx1 = sy1 = -1;
+ sx2 = sy2 = 1;
+ #define CHECKPLANE(c, dir, focaldist, low, high) \
+ do { \
+ float nzc = (cz*cz + 1) / (cz dir drt) - cz, \
+ pz = (d##c)/(nzc*e.c - e.z); \
+ if(pz > 0) \
+ { \
+ float c = (focaldist)*nzc, \
+ pc = pz*nzc; \
+ if(pc < e.c) low = c; \
+ else if(pc > e.c) high = c; \
+ } \
+ } while(0)
+ if(dx > 0)
+ {
+ float cz = e.x/e.z, drt = sqrtf(dx)/size;
+ CHECKPLANE(x, -, focaldist/aspect, sx1, sx2);
+ CHECKPLANE(x, +, focaldist/aspect, sx1, sx2);
+ }
+ if(dy > 0)
+ {
+ float cz = e.y/e.z, drt = sqrtf(dy)/size;
+ CHECKPLANE(y, -, focaldist, sy1, sy2);
+ CHECKPLANE(y, +, focaldist, sy1, sy2);
+ }
+}
+
+static int scissoring = 0;
+static GLint oldscissor[4];
+
+int pushscissor(float sx1, float sy1, float sx2, float sy2)
+{
+ scissoring = 0;
+
+ if(sx1 <= -1 && sy1 <= -1 && sx2 >= 1 && sy2 >= 1) return 0;
+
+ sx1 = max(sx1, -1.0f);
+ sy1 = max(sy1, -1.0f);
+ sx2 = min(sx2, 1.0f);
+ sy2 = min(sy2, 1.0f);
+
+ GLint viewport[4];
+ glGetIntegerv(GL_VIEWPORT, viewport);
+ int sx = viewport[0] + int(floor((sx1+1)*0.5f*viewport[2])),
+ sy = viewport[1] + int(floor((sy1+1)*0.5f*viewport[3])),
+ sw = viewport[0] + int(ceil((sx2+1)*0.5f*viewport[2])) - sx,
+ sh = viewport[1] + int(ceil((sy2+1)*0.5f*viewport[3])) - sy;
+ if(sw <= 0 || sh <= 0) return 0;
+
+ if(glIsEnabled(GL_SCISSOR_TEST))
+ {
+ glGetIntegerv(GL_SCISSOR_BOX, oldscissor);
+ sw += sx;
+ sh += sy;
+ sx = max(sx, int(oldscissor[0]));
+ sy = max(sy, int(oldscissor[1]));
+ sw = min(sw, int(oldscissor[0] + oldscissor[2])) - sx;
+ sh = min(sh, int(oldscissor[1] + oldscissor[3])) - sy;
+ if(sw <= 0 || sh <= 0) return 0;
+ scissoring = 2;
+ }
+ else scissoring = 1;
+
+ glScissor(sx, sy, sw, sh);
+ if(scissoring<=1) glEnable(GL_SCISSOR_TEST);
+
+ return scissoring;
+}
+
+void popscissor()
+{
+ if(scissoring>1) glScissor(oldscissor[0], oldscissor[1], oldscissor[2], oldscissor[3]);
+ else if(scissoring) glDisable(GL_SCISSOR_TEST);
+ scissoring = 0;
+}
+
+static GLuint screenquadvbo = 0;
+
+static void setupscreenquad()
+{
+ if(!screenquadvbo)
+ {
+ glGenBuffers_(1, &screenquadvbo);
+ gle::bindvbo(screenquadvbo);
+ vec2 verts[4] = { vec2(1, -1), vec2(-1, -1), vec2(1, 1), vec2(-1, 1) };
+ glBufferData_(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
+ gle::clearvbo();
+ }
+}
+
+static void cleanupscreenquad()
+{
+ if(screenquadvbo) { glDeleteBuffers_(1, &screenquadvbo); screenquadvbo = 0; }
+}
+
+void screenquad()
+{
+ setupscreenquad();
+ gle::bindvbo(screenquadvbo);
+ gle::enablevertex();
+ gle::vertexpointer(sizeof(vec2), (const vec2 *)0, GL_FLOAT, 2);
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+ gle::disablevertex();
+ gle::clearvbo();
+}
+
+static LocalShaderParam screentexcoord[2] = { LocalShaderParam("screentexcoord0"), LocalShaderParam("screentexcoord1") };
+
+static inline void setscreentexcoord(int i, float w, float h, float x = 0, float y = 0)
+{
+ screentexcoord[i].setf(w*0.5f, h*0.5f, x + w*0.5f, y + fabs(h)*0.5f);
+}
+
+void screenquad(float sw, float sh)
+{
+ setscreentexcoord(0, sw, sh);
+ screenquad();
+}
+
+void screenquadflipped(float sw, float sh)
+{
+ setscreentexcoord(0, sw, -sh);
+ screenquad();
+}
+
+void screenquad(float sw, float sh, float sw2, float sh2)
+{
+ setscreentexcoord(0, sw, sh);
+ setscreentexcoord(1, sw2, sh2);
+ screenquad();
+}
+
+void screenquadoffset(float x, float y, float w, float h)
+{
+ setscreentexcoord(0, w, h, x, y);
+ screenquad();
+}
+
+void screenquadoffset(float x, float y, float w, float h, float x2, float y2, float w2, float h2)
+{
+ setscreentexcoord(0, w, h, x, y);
+ setscreentexcoord(1, w2, h2, x2, y2);
+ screenquad();
+}
+
+#define HUDQUAD(x1, y1, x2, y2, sx1, sy1, sx2, sy2) { \
+ gle::defvertex(2); \
+ gle::deftexcoord0(); \
+ gle::begin(GL_TRIANGLE_STRIP); \
+ gle::attribf(x2, y1); gle::attribf(sx2, sy1); \
+ gle::attribf(x1, y1); gle::attribf(sx1, sy1); \
+ gle::attribf(x2, y2); gle::attribf(sx2, sy2); \
+ gle::attribf(x1, y2); gle::attribf(sx1, sy2); \
+ gle::end(); \
+}
+
+void hudquad(float x, float y, float w, float h, float tx, float ty, float tw, float th)
+{
+ HUDQUAD(x, y, x+w, y+h, tx, ty, tx+tw, ty+th);
+}
+
+VARR(fog, 16, 4000, 1000024);
+bvec fogcolor(0x80, 0x99, 0xB3);
+HVARFR(fogcolour, 0, 0x8099B3, 0xFFFFFF,
+{
+ fogcolor = bvec((fogcolour>>16)&0xFF, (fogcolour>>8)&0xFF, fogcolour&0xFF);
+});
+
+static float findsurface(int fogmat, const vec &v, int &abovemat)
+{
+ fogmat &= MATF_VOLUME;
+ ivec o(v), co;
+ int csize;
+ do
+ {
+ cube &c = lookupcube(o, 0, co, csize);
+ int mat = c.material&MATF_VOLUME;
+ if(mat != fogmat)
+ {
+ abovemat = isliquid(mat) ? c.material : MAT_AIR;
+ return o.z;
+ }
+ o.z = co.z + csize;
+ }
+ while(o.z < worldsize);
+ abovemat = MAT_AIR;
+ return worldsize;
+}
+
+static void blendfog(int fogmat, float blend, float logblend, float &start, float &end, vec &fogc)
+{
+ switch(fogmat&MATF_VOLUME)
+ {
+ case MAT_WATER:
+ {
+ const bvec &wcol = getwatercolor(fogmat);
+ int wfog = getwaterfog(fogmat);
+ fogc.madd(wcol.tocolor(), blend);
+ end += logblend*min(fog, max(wfog*4, 32));
+ break;
+ }
+
+ case MAT_LAVA:
+ {
+ const bvec &lcol = getlavacolor(fogmat);
+ int lfog = getlavafog(fogmat);
+ fogc.madd(lcol.tocolor(), blend);
+ end += logblend*min(fog, max(lfog*4, 32));
+ break;
+ }
+
+ default:
+ fogc.madd(fogcolor.tocolor(), blend);
+ start += logblend*(fog+64)/8;
+ end += logblend*fog;
+ break;
+ }
+}
+
+vec oldfogcolor(0, 0, 0), curfogcolor(0, 0, 0);
+float oldfogstart = 0, oldfogend = 1000000, curfogstart = 0, curfogend = 1000000;
+
+void setfogcolor(const vec &v)
+{
+ GLOBALPARAM(fogcolor, v);
+}
+
+void zerofogcolor()
+{
+ setfogcolor(vec(0, 0, 0));
+}
+
+void resetfogcolor()
+{
+ setfogcolor(curfogcolor);
+}
+
+void pushfogcolor(const vec &v)
+{
+ oldfogcolor = curfogcolor;
+ curfogcolor = v;
+ resetfogcolor();
+}
+
+void popfogcolor()
+{
+ curfogcolor = oldfogcolor;
+ resetfogcolor();
+}
+
+void setfogdist(float start, float end)
+{
+ GLOBALPARAMF(fogparams, 1/(end - start), end/(end - start));
+}
+
+void clearfogdist()
+{
+ setfogdist(0, 1000000);
+}
+
+void resetfogdist()
+{
+ setfogdist(curfogstart, curfogend);
+}
+
+void pushfogdist(float start, float end)
+{
+ oldfogstart = curfogstart;
+ oldfogend = curfogend;
+ curfogstart = start;
+ curfogend = end;
+ resetfogdist();
+}
+
+void popfogdist()
+{
+ curfogstart = oldfogstart;
+ curfogend = oldfogend;
+ resetfogdist();
+}
+
+static void resetfog()
+{
+ resetfogcolor();
+ resetfogdist();
+
+ glClearColor(curfogcolor.r, curfogcolor.g, curfogcolor.b, 1.0f);
+}
+
+static void setfog(int fogmat, float below = 1, int abovemat = MAT_AIR)
+{
+ float logscale = 256, logblend = log(1 + (logscale - 1)*below) / log(logscale);
+
+ curfogstart = curfogend = 0;
+ curfogcolor = vec(0, 0, 0);
+ blendfog(fogmat, below, logblend, curfogstart, curfogend, curfogcolor);
+ if(below < 1) blendfog(abovemat, 1-below, 1-logblend, curfogstart, curfogend, curfogcolor);
+
+ resetfog();
+}
+
+static void setnofog(const vec &color = vec(0, 0, 0))
+{
+ curfogstart = 0;
+ curfogend = 1000000;
+ curfogcolor = color;
+
+ resetfog();
+}
+
+static void blendfogoverlay(int fogmat, float blend, vec &overlay)
+{
+ float maxc;
+ switch(fogmat&MATF_VOLUME)
+ {
+ case MAT_WATER:
+ {
+ const bvec &wcol = getwatercolor(fogmat);
+ maxc = max(wcol.r, max(wcol.g, wcol.b));
+ overlay.madd(vec(wcol.r, wcol.g, wcol.b).div(min(32.0f + maxc*7.0f/8.0f, 255.0f)).max(0.4f), blend);
+ break;
+ }
+
+ case MAT_LAVA:
+ {
+ const bvec &lcol = getlavacolor(fogmat);
+ maxc = max(lcol.r, max(lcol.g, lcol.b));
+ overlay.madd(vec(lcol.r, lcol.g, lcol.b).div(min(32.0f + maxc*7.0f/8.0f, 255.0f)).max(0.4f), blend);
+ break;
+ }
+
+ default:
+ overlay.add(blend);
+ break;
+ }
+}
+
+void drawfogoverlay(int fogmat, float fogblend, int abovemat)
+{
+ SETSHADER(fogoverlay);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+ vec overlay(0, 0, 0);
+ blendfogoverlay(fogmat, fogblend, overlay);
+ blendfogoverlay(abovemat, 1-fogblend, overlay);
+
+ gle::color(overlay);
+ screenquad();
+
+ glDisable(GL_BLEND);
+}
+
+bool renderedgame = false;
+
+void rendergame(bool mainpass)
+{
+ game::rendergame(mainpass);
+ if(!shadowmapping) renderedgame = true;
+}
+
+VARP(skyboxglare, 0, 1, 1);
+
+void drawglare()
+{
+ glaring = true;
+ refracting = -1;
+
+ pushfogcolor(vec(0, 0, 0));
+
+ glClearColor(0, 0, 0, 1);
+ glClear((skyboxglare && !shouldclearskyboxglare() ? 0 : GL_COLOR_BUFFER_BIT) | GL_DEPTH_BUFFER_BIT);
+
+ rendergeom();
+
+ if(skyboxglare) drawskybox(farplane, false);
+
+ renderreflectedmapmodels();
+ rendergame();
+ renderavatar();
+
+ renderwater();
+ rendermaterials();
+ renderalphageom();
+ renderparticles();
+
+ popfogcolor();
+
+ refracting = 0;
+ glaring = false;
+}
+
+VARP(reflectmms, 0, 1, 1);
+VARR(refractsky, 0, 0, 1);
+
+matrix4 noreflectmatrix;
+
+void drawreflection(float z, bool refract, int fogdepth, const bvec &col)
+{
+ reflectz = z < 0 ? 1e16f : z;
+ reflecting = !refract;
+ refracting = refract ? (z < 0 || camera1->o.z >= z ? -1 : 1) : 0;
+ fading = waterrefract && waterfade && z>=0;
+ fogging = refracting<0 && z>=0;
+ refractfog = fogdepth;
+
+ if(fogging)
+ {
+ pushfogdist(camera1->o.z - z, camera1->o.z - (z - max(refractfog, 1)));
+ pushfogcolor(col.tocolor());
+ }
+ else
+ {
+ vec color(0, 0, 0);
+ float start = 0, end = 0;
+ blendfog(MAT_AIR, 1, 1, start, end, color);
+ pushfogdist(start, end);
+ pushfogcolor(color);
+ }
+
+ if(fading)
+ {
+ float scale = fogging ? -0.25f : 0.25f, offset = 2*fabs(scale) - scale*z;
+ GLOBALPARAMF(waterfadeparams, scale, offset, -scale, offset + camera1->o.z*scale);
+ }
+
+ if(reflecting)
+ {
+ noreflectmatrix = cammatrix;
+ cammatrix.reflectz(z);
+
+ glFrontFace(GL_CCW);
+ }
+
+ if(reflectclip && z>=0)
+ {
+ float zoffset = reflectclip/4.0f, zclip;
+ if(refracting<0)
+ {
+ zclip = z+zoffset;
+ if(camera1->o.z<=zclip) zclip = z;
+ }
+ else
+ {
+ zclip = z-zoffset;
+ if(camera1->o.z>=zclip && camera1->o.z<=z+4.0f) zclip = z;
+ if(reflecting) zclip = 2*z - zclip;
+ }
+ plane clipplane;
+ invcammatrix.transposedtransform(plane(0, 0, refracting>0 ? 1 : -1, refracting>0 ? -zclip : zclip), clipplane);
+ clipmatrix.clip(clipplane, projmatrix);
+ noclipmatrix = projmatrix;
+ projmatrix = clipmatrix;
+ }
+
+ setcamprojmatrix(false, true);
+
+ renderreflectedgeom(refracting<0 && z>=0 && caustics, fogging);
+
+ if(reflecting || refracting>0 || (refracting<0 && refractsky) || z<0)
+ {
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ if(reflectclip && z>=0)
+ {
+ projmatrix = noclipmatrix;
+ setcamprojmatrix(false, true);
+ }
+ drawskybox(farplane, false);
+ if(reflectclip && z>=0)
+ {
+ projmatrix = clipmatrix;
+ setcamprojmatrix(false, true);
+ }
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+ }
+ else if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+ renderdecals();
+
+ if(reflectmms) renderreflectedmapmodels();
+ rendergame();
+
+ if(refracting && z>=0 && !isthirdperson() && fabs(camera1->o.z-z) <= 0.5f*(player->eyeheight + player->aboveeye))
+ {
+ matrix4 oldprojmatrix = projmatrix, avatarproj;
+ avatarproj.perspective(curavatarfov, aspect, nearplane, farplane);
+ if(reflectclip)
+ {
+ matrix4 avatarclip;
+ plane clipplane;
+ invcammatrix.transposedtransform(plane(0, 0, refracting, reflectclipavatar/4.0f - refracting*z), clipplane);
+ avatarclip.clip(clipplane, avatarproj);
+ projmatrix = avatarclip;
+ }
+ else projmatrix = avatarproj;
+ setcamprojmatrix(false, true);
+ game::renderavatar();
+ projmatrix = oldprojmatrix;
+ setcamprojmatrix(false, true);
+ }
+
+ if(refracting) rendergrass();
+ rendermaterials();
+ renderalphageom(fogging);
+ renderparticles();
+
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+ if(reflectclip && z>=0) projmatrix = noclipmatrix;
+
+ if(reflecting)
+ {
+ cammatrix = noreflectmatrix;
+
+ glFrontFace(GL_CW);
+ }
+
+ popfogdist();
+ popfogcolor();
+
+ reflectz = 1e16f;
+ refracting = 0;
+ reflecting = fading = fogging = false;
+
+ setcamprojmatrix(false, true);
+}
+
+int drawtex = 0;
+
+void drawcubemap(int size, const vec &o, float yaw, float pitch, const cubemapside &side, bool onlysky)
+{
+ drawtex = DRAWTEX_ENVMAP;
+
+ physent *oldcamera = camera1;
+ static physent cmcamera;
+ cmcamera = *player;
+ cmcamera.reset();
+ cmcamera.type = ENT_CAMERA;
+ cmcamera.o = o;
+ cmcamera.yaw = yaw;
+ cmcamera.pitch = pitch;
+ cmcamera.roll = 0;
+ camera1 = &cmcamera;
+ setviewcell(camera1->o);
+
+ int fogmat = lookupmaterial(o)&(MATF_VOLUME|MATF_INDEX);
+
+ setfog(fogmat);
+
+ int farplane = worldsize*2;
+
+ projmatrix.perspective(90.0f, 1.0f, nearplane, farplane);
+ if(!side.flipx || !side.flipy) projmatrix.scalexy(!side.flipx ? -1 : 1, !side.flipy ? -1 : 1);
+ if(side.swapxy)
+ {
+ swap(projmatrix.a.x, projmatrix.a.y);
+ swap(projmatrix.b.x, projmatrix.b.y);
+ swap(projmatrix.c.x, projmatrix.c.y);
+ swap(projmatrix.d.x, projmatrix.d.y);
+ }
+ setcamprojmatrix();
+
+ xtravertsva = xtraverts = glde = gbatches = 0;
+
+ visiblecubes();
+
+ if(onlysky) drawskybox(farplane, false, true);
+ else
+ {
+ glClear(GL_DEPTH_BUFFER_BIT);
+
+ glEnable(GL_CULL_FACE);
+ glEnable(GL_DEPTH_TEST);
+
+ if(limitsky()) drawskybox(farplane, true);
+
+ rendergeom();
+
+ if(!limitsky()) drawskybox(farplane, false);
+
+// queryreflections();
+
+ rendermapmodels();
+ renderalphageom();
+
+// drawreflections();
+
+// renderwater();
+// rendermaterials();
+
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_CULL_FACE);
+ }
+
+ camera1 = oldcamera;
+ drawtex = 0;
+}
+
+VAR(modelpreviewfov, 10, 20, 100);
+VAR(modelpreviewpitch, -90, -15, 90);
+
+namespace modelpreview
+{
+ physent *oldcamera;
+ physent camera;
+
+ float oldaspect, oldfovy, oldfov;
+ int oldfarplane;
+ matrix4 oldprojmatrix;
+
+ void start(int x, int y, int w, int h, bool background)
+ {
+ drawtex = DRAWTEX_MODELPREVIEW;
+
+ glViewport(x, y, w, h);
+ glScissor(x, y, w, h);
+ glEnable(GL_SCISSOR_TEST);
+
+ oldcamera = camera1;
+ camera = *camera1;
+ camera.reset();
+ camera.type = ENT_CAMERA;
+ camera.o = vec(0, 0, 0);
+ camera.yaw = 0;
+ camera.pitch = modelpreviewpitch;
+ camera.roll = 0;
+ camera1 = &camera;
+
+ oldaspect = aspect;
+ oldfovy = fovy;
+ oldfov = curfov;
+ oldfarplane = farplane;
+ oldprojmatrix = projmatrix;
+
+ aspect = w/float(h);
+ fovy = modelpreviewfov;
+ curfov = 2*atan2(tan(fovy/2*RAD), 1/aspect)/RAD;
+ farplane = 1024;
+
+ clearfogdist();
+ zerofogcolor();
+ glClearColor(0, 0, 0, 1);
+
+ glClear((background ? GL_COLOR_BUFFER_BIT : 0) | GL_DEPTH_BUFFER_BIT);
+
+ projmatrix.perspective(fovy, aspect, nearplane, farplane);
+ setcamprojmatrix();
+
+ glEnable(GL_CULL_FACE);
+ glEnable(GL_DEPTH_TEST);
+ }
+
+ void end()
+ {
+ glDisable(GL_CULL_FACE);
+ glDisable(GL_DEPTH_TEST);
+
+ resetfogdist();
+ resetfogcolor();
+ glClearColor(curfogcolor.r, curfogcolor.g, curfogcolor.b, 1);
+
+ aspect = oldaspect;
+ fovy = oldfovy;
+ curfov = oldfov;
+ farplane = oldfarplane;
+
+ camera1 = oldcamera;
+ drawtex = 0;
+
+ glDisable(GL_SCISSOR_TEST);
+ glViewport(0, 0, screenw, screenh);
+
+ projmatrix = oldprojmatrix;
+ setcamprojmatrix();
+ }
+}
+
+vec calcmodelpreviewpos(const vec &radius, float &yaw)
+{
+ yaw = fmod(lastmillis/10000.0f*360.0f, 360.0f);
+ float dist = 1.15f*max(radius.magnitude2()/aspect, radius.magnitude())/sinf(fovy/2*RAD);
+ return vec(0, dist, 0).rotate_around_x(camera1->pitch*RAD);
+}
+
+GLuint minimaptex = 0;
+vec minimapcenter(0, 0, 0), minimapradius(0, 0, 0), minimapscale(0, 0, 0);
+
+void clearminimap()
+{
+ if(minimaptex) { glDeleteTextures(1, &minimaptex); minimaptex = 0; }
+}
+
+VARR(minimapheight, 0, 0, 2<<16);
+bvec minimapcolor(0, 0, 0);
+HVARFR(minimapcolour, 0, 0, 0xFFFFFF,
+{
+ minimapcolor = bvec((minimapcolour>>16)&0xFF, (minimapcolour>>8)&0xFF, minimapcolour&0xFF);
+});
+VARR(minimapclip, 0, 0, 1);
+VARFP(minimapsize, 7, 8, 10, { if(minimaptex) drawminimap(); });
+
+void bindminimap()
+{
+ glBindTexture(GL_TEXTURE_2D, minimaptex);
+}
+
+void clipminimap(ivec &bbmin, ivec &bbmax, cube *c = worldroot, const ivec &co = ivec(0, 0, 0), int size = worldsize>>1)
+{
+ loopi(8)
+ {
+ ivec o(i, co, size);
+ if(c[i].children) clipminimap(bbmin, bbmax, c[i].children, o, size>>1);
+ else if(!isentirelysolid(c[i]) && (c[i].material&MATF_CLIP)!=MAT_CLIP)
+ {
+ loopk(3) bbmin[k] = min(bbmin[k], o[k]);
+ loopk(3) bbmax[k] = max(bbmax[k], o[k] + size);
+ }
+ }
+}
+
+void drawminimap()
+{
+ if(!game::needminimap()) { clearminimap(); return; }
+
+ renderprogress(0, "generating mini-map...", 0, !renderedframe);
+
+ int size = 1<<minimapsize, sizelimit = min(hwtexsize, min(screenw, screenh));
+ while(size > sizelimit) size /= 2;
+ if(!minimaptex) glGenTextures(1, &minimaptex);
+
+ ivec bbmin(worldsize, worldsize, worldsize), bbmax(0, 0, 0);
+ loopv(valist)
+ {
+ vtxarray *va = valist[i];
+ loopk(3)
+ {
+ if(va->geommin[k]>va->geommax[k]) continue;
+ bbmin[k] = min(bbmin[k], va->geommin[k]);
+ bbmax[k] = max(bbmax[k], va->geommax[k]);
+ }
+ }
+ if(minimapclip)
+ {
+ ivec clipmin(worldsize, worldsize, worldsize), clipmax(0, 0, 0);
+ clipminimap(clipmin, clipmax);
+ loopk(2) bbmin[k] = max(bbmin[k], clipmin[k]);
+ loopk(2) bbmax[k] = min(bbmax[k], clipmax[k]);
+ }
+
+ minimapradius = vec(bbmax).sub(vec(bbmin)).mul(0.5f);
+ minimapcenter = vec(bbmin).add(minimapradius);
+ minimapradius.x = minimapradius.y = max(minimapradius.x, minimapradius.y);
+ minimapscale = vec((0.5f - 1.0f/size)/minimapradius.x, (0.5f - 1.0f/size)/minimapradius.y, 1.0f);
+
+ drawtex = DRAWTEX_MINIMAP;
+
+ physent *oldcamera = camera1;
+ static physent cmcamera;
+ cmcamera = *player;
+ cmcamera.reset();
+ cmcamera.type = ENT_CAMERA;
+ cmcamera.o = vec(minimapcenter.x, minimapcenter.y, max(minimapcenter.z + minimapradius.z + 1, float(minimapheight)));
+ cmcamera.yaw = 0;
+ cmcamera.pitch = -90;
+ cmcamera.roll = 0;
+ camera1 = &cmcamera;
+ setviewcell(vec(-1, -1, -1));
+
+ projmatrix.ortho(-minimapradius.x, minimapradius.x, -minimapradius.y, minimapradius.y, 0, camera1->o.z + 1);
+ projmatrix.a.mul(-1);
+ setcamprojmatrix();
+
+ setnofog(minimapcolor.tocolor());
+
+ glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
+
+ glViewport(0, 0, size, size);
+
+ glEnable(GL_CULL_FACE);
+ glEnable(GL_DEPTH_TEST);
+
+ glFrontFace(GL_CCW);
+
+ xtravertsva = xtraverts = glde = gbatches = 0;
+
+ visiblecubes(false);
+ queryreflections();
+ drawreflections();
+
+ loopi(minimapheight > 0 && minimapheight < minimapcenter.z + minimapradius.z ? 2 : 1)
+ {
+ if(i)
+ {
+ glClear(GL_DEPTH_BUFFER_BIT);
+ camera1->o.z = minimapheight;
+ setcamprojmatrix();
+ }
+ rendergeom();
+ rendermapmodels();
+ renderwater();
+ rendermaterials();
+ renderalphageom();
+ }
+
+ glFrontFace(GL_CW);
+
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_CULL_FACE);
+
+ glViewport(0, 0, screenw, screenh);
+
+ camera1 = oldcamera;
+ drawtex = 0;
+
+ glBindTexture(GL_TEXTURE_2D, minimaptex);
+ glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5, 0, 0, size, size, 0);
+ setuptexparameters(minimaptex, NULL, 3, 1, GL_RGB5, GL_TEXTURE_2D);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
+ GLfloat border[4] = { minimapcolor.x/255.0f, minimapcolor.y/255.0f, minimapcolor.z/255.0f, 1.0f };
+ glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
+ glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+bool deferdrawtextures = false;
+
+void drawtextures()
+{
+ if(minimized) { deferdrawtextures = true; return; }
+ deferdrawtextures = false;
+ genenvmaps();
+ drawminimap();
+}
+
+GLuint motiontex = 0;
+int motionw = 0, motionh = 0, lastmotion = 0;
+
+void cleanupmotionblur()
+{
+ if(motiontex) { glDeleteTextures(1, &motiontex); motiontex = 0; }
+ motionw = motionh = 0;
+ lastmotion = 0;
+}
+
+VARFP(motionblur, 0, 0, 1, { if(!motionblur) cleanupmotionblur(); });
+VARP(motionblurmillis, 1, 5, 1000);
+FVARP(motionblurscale, 0, 0.5f, 1);
+
+void addmotionblur()
+{
+ if(!motionblur || max(screenw, screenh) > hwtexsize) return;
+
+ if(game::ispaused()) { lastmotion = 0; return; }
+
+ if(!motiontex || motionw != screenw || motionh != screenh)
+ {
+ if(!motiontex) glGenTextures(1, &motiontex);
+ motionw = screenw;
+ motionh = screenh;
+ lastmotion = 0;
+ createtexture(motiontex, motionw, motionh, NULL, 3, 0, GL_RGB);
+ }
+
+ glBindTexture(GL_TEXTURE_2D, motiontex);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ SETSHADER(screenrect);
+
+ gle::colorf(1, 1, 1, lastmotion ? pow(motionblurscale, max(float(lastmillis - lastmotion)/motionblurmillis, 1.0f)) : 0);
+ screenquad(1, 1);
+
+ glDisable(GL_BLEND);
+
+ if(lastmillis - lastmotion >= motionblurmillis)
+ {
+ lastmotion = lastmillis - lastmillis%motionblurmillis;
+
+ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh);
+ }
+}
+
+bool dopostfx = false;
+
+void invalidatepostfx()
+{
+ dopostfx = false;
+}
+
+int xtraverts, xtravertsva;
+
+void gl_drawframe()
+{
+ if(deferdrawtextures) drawtextures();
+
+ updatedynlights();
+
+ int w = screenw, h = screenh;
+ aspect = forceaspect ? forceaspect : w/float(h);
+ fovy = 2*atan2(tan(curfov/2*RAD), aspect)/RAD;
+
+ int fogmat = lookupmaterial(camera1->o)&(MATF_VOLUME|MATF_INDEX), abovemat = MAT_AIR;
+ float fogblend = 1.0f, causticspass = 0.0f;
+ if(isliquid(fogmat&MATF_VOLUME))
+ {
+ float z = findsurface(fogmat, camera1->o, abovemat) - WATER_OFFSET;
+ if(camera1->o.z < z + 1) fogblend = min(z + 1 - camera1->o.z, 1.0f);
+ else fogmat = abovemat;
+ if(caustics && (fogmat&MATF_VOLUME)==MAT_WATER && camera1->o.z < z)
+ causticspass = min(z - camera1->o.z, 1.0f);
+ }
+ else fogmat = MAT_AIR;
+ setfog(fogmat, fogblend, abovemat);
+ if(fogmat!=MAT_AIR)
+ {
+ float blend = abovemat==MAT_AIR ? fogblend : 1.0f;
+ fovy += blend*sinf(lastmillis/1000.0)*2.0f;
+ aspect += blend*sinf(lastmillis/1000.0+M_PI)*0.1f;
+ }
+
+ farplane = worldsize*2;
+
+ projmatrix.perspective(fovy, aspect, nearplane, farplane);
+ setcamprojmatrix();
+
+ glEnable(GL_CULL_FACE);
+ glEnable(GL_DEPTH_TEST);
+
+ xtravertsva = xtraverts = glde = gbatches = 0;
+
+ visiblecubes();
+
+ glClear(GL_DEPTH_BUFFER_BIT|(wireframe && editmode ? GL_COLOR_BUFFER_BIT : 0));
+
+ if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+
+ if(limitsky()) drawskybox(farplane, true);
+
+ rendergeom(causticspass);
+
+ extern int outline;
+ if(!wireframe && editmode && outline) renderoutline();
+
+ queryreflections();
+
+ generategrass();
+
+ if(!limitsky()) drawskybox(farplane, false);
+
+ renderdecals(true);
+
+ rendermapmodels();
+ rendergame(true);
+ renderavatar();
+
+ if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+ drawglaretex();
+ drawdepthfxtex();
+ drawreflections();
+
+ if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+
+ renderwater();
+ rendergrass();
+
+ rendermaterials();
+ renderalphageom();
+
+ if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+ renderparticles(true);
+
+ glDisable(GL_CULL_FACE);
+ glDisable(GL_DEPTH_TEST);
+
+ addmotionblur();
+ addglare();
+ if(isliquid(fogmat&MATF_VOLUME)) drawfogoverlay(fogmat, fogblend, abovemat);
+ renderpostfx();
+
+ gl_drawhud();
+
+ renderedgame = false;
+}
+
+void gl_drawmainmenu()
+{
+ xtravertsva = xtraverts = glde = gbatches = 0;
+
+ renderbackground(NULL, NULL, NULL, NULL, true, true);
+ renderpostfx();
+
+ gl_drawhud();
+}
+
+VARNP(damagecompass, usedamagecompass, 0, 1, 1);
+VARP(damagecompassfade, 1, 1000, 10000);
+VARP(damagecompasssize, 1, 30, 100);
+VARP(damagecompassalpha, 1, 25, 100);
+VARP(damagecompassmin, 1, 25, 1000);
+VARP(damagecompassmax, 1, 200, 1000);
+
+float damagedirs[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
+
+void damagecompass(int n, const vec &loc)
+{
+ if(!usedamagecompass || minimized) return;
+ vec delta(loc);
+ delta.sub(camera1->o);
+ float yaw = 0, pitch;
+ if(delta.magnitude() > 4)
+ {
+ vectoyawpitch(delta, yaw, pitch);
+ yaw -= camera1->yaw;
+ }
+ if(yaw >= 360) yaw = fmod(yaw, 360);
+ else if(yaw < 0) yaw = 360 - fmod(-yaw, 360);
+ int dir = (int(yaw+22.5f)%360)/45;
+ damagedirs[dir] += max(n, damagecompassmin)/float(damagecompassmax);
+ if(damagedirs[dir]>1) damagedirs[dir] = 1;
+}
+
+void drawdamagecompass(int w, int h)
+{
+ hudnotextureshader->set();
+
+ int dirs = 0;
+ float size = damagecompasssize/100.0f*min(h, w)/2.0f;
+ loopi(8) if(damagedirs[i]>0)
+ {
+ if(!dirs)
+ {
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ gle::colorf(1, 0, 0, damagecompassalpha/100.0f);
+ gle::defvertex();
+ gle::begin(GL_TRIANGLES);
+ }
+ dirs++;
+
+ float logscale = 32,
+ scale = log(1 + (logscale - 1)*damagedirs[i]) / log(logscale),
+ offset = -size/2.0f-min(h, w)/4.0f;
+ matrix4x3 m;
+ m.identity();
+ m.settranslation(w/2, h/2, 0);
+ m.rotate_around_z(i*45*RAD);
+ m.translate(0, offset, 0);
+ m.scale(size*scale);
+
+ gle::attrib(m.transform(vec2(1, 1)));
+ gle::attrib(m.transform(vec2(-1, 1)));
+ gle::attrib(m.transform(vec2(0, 0)));
+
+ // fade in log space so short blips don't disappear too quickly
+ scale -= float(curtime)/damagecompassfade;
+ damagedirs[i] = scale > 0 ? (pow(logscale, scale) - 1) / (logscale - 1) : 0;
+ }
+ if(dirs) gle::end();
+}
+
+int damageblendmillis = 0;
+
+VARFP(damagescreen, 0, 1, 1, { if(!damagescreen) damageblendmillis = 0; });
+VARP(damagescreenfactor, 1, 7, 100);
+VARP(damagescreenalpha, 1, 45, 100);
+VARP(damagescreenfade, 0, 125, 1000);
+VARP(damagescreenmin, 1, 10, 1000);
+VARP(damagescreenmax, 1, 100, 1000);
+
+void damageblend(int n)
+{
+ if(!damagescreen || minimized) return;
+ if(lastmillis > damageblendmillis) damageblendmillis = lastmillis;
+ damageblendmillis += clamp(n, damagescreenmin, damagescreenmax)*damagescreenfactor;
+}
+
+void drawdamagescreen(int w, int h)
+{
+ if(lastmillis >= damageblendmillis) return;
+
+ hudshader->set();
+
+ static Texture *damagetex = NULL;
+ if(!damagetex) damagetex = textureload("packages/hud/damage.png", 3);
+
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+ glBindTexture(GL_TEXTURE_2D, damagetex->id);
+ float fade = damagescreenalpha/100.0f;
+ if(damageblendmillis - lastmillis < damagescreenfade)
+ fade *= float(damageblendmillis - lastmillis)/damagescreenfade;
+ gle::colorf(fade, fade, fade, fade);
+
+ hudquad(0, 0, w, h);
+}
+
+void cleardamagescreen()
+{
+ damageblendmillis = 0;
+ loopi(8) damagedirs[i] = 0;
+}
+
+VAR(hidestats, 0, 0, 1);
+VAR(hidehud, 0, 0, 1);
+
+VARP(crosshairsize, 0, 15, 50);
+VARP(cursorsize, 0, 30, 50);
+VARP(crosshairfx, 0, 1, 1);
+VARP(crosshaircolors, 0, 1, 1);
+
+#define MAXCROSSHAIRS 4
+static Texture *crosshairs[MAXCROSSHAIRS] = { NULL, NULL, NULL, NULL };
+
+void loadcrosshair(const char *name, int i)
+{
+ if(i < 0 || i >= MAXCROSSHAIRS) return;
+ crosshairs[i] = name ? textureload(name, 3, true) : notexture;
+ if(crosshairs[i] == notexture)
+ {
+ name = game::defaultcrosshair(i);
+ if(!name) name = "data/crosshair.png";
+ crosshairs[i] = textureload(name, 3, true);
+ }
+}
+
+void loadcrosshair_(const char *name, int *i)
+{
+ loadcrosshair(name, *i);
+}
+
+COMMANDN(loadcrosshair, loadcrosshair_, "si");
+
+ICOMMAND(getcrosshair, "i", (int *i),
+{
+ const char *name = "";
+ if(*i >= 0 && *i < MAXCROSSHAIRS)
+ {
+ name = crosshairs[*i] ? crosshairs[*i]->name : game::defaultcrosshair(*i);
+ if(!name) name = "data/crosshair.png";
+ }
+ result(name);
+});
+
+void writecrosshairs(stream *f)
+{
+ loopi(MAXCROSSHAIRS) if(crosshairs[i] && crosshairs[i]!=notexture)
+ f->printf("loadcrosshair %s %d\n", escapestring(crosshairs[i]->name), i);
+ f->printf("\n");
+}
+
+void drawcrosshair(int w, int h)
+{
+ bool windowhit = g3d_windowhit(true, false);
+ if(!windowhit && (hidehud || mainmenu)) return; //(hidehud || player->state==CS_SPECTATOR || player->state==CS_DEAD)) return;
+
+ vec color(1, 1, 1);
+ float cx = 0.5f, cy = 0.5f, chsize;
+ Texture *crosshair;
+ if(windowhit)
+ {
+ static Texture *cursor = NULL;
+ if(!cursor) cursor = textureload("data/guicursor.png", 3, true);
+ crosshair = cursor;
+ chsize = cursorsize*w/900.0f;
+ g3d_cursorpos(cx, cy);
+ }
+ else
+ {
+ int index = game::selectcrosshair(color);
+ if(index < 0) return;
+ if(!crosshairfx) index = 0;
+ if(!crosshairfx || !crosshaircolors) color = vec(1, 1, 1);
+ crosshair = crosshairs[index];
+ if(!crosshair)
+ {
+ loadcrosshair(NULL, index);
+ crosshair = crosshairs[index];
+ }
+ chsize = crosshairsize*w/900.0f;
+ }
+ if(crosshair->type&Texture::ALPHA) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ else glBlendFunc(GL_ONE, GL_ONE);
+ float x = cx*w - (windowhit ? 0 : chsize/2.0f);
+ float y = cy*h - (windowhit ? 0 : chsize/2.0f);
+ glBindTexture(GL_TEXTURE_2D, crosshair->id);
+
+ hudshader->set();
+ gle::color(color);
+ hudquad(x, y, chsize, chsize);
+}
+
+VARP(wallclock, 0, 0, 1);
+VARP(wallclock24, 0, 0, 1);
+VARP(wallclocksecs, 0, 0, 1);
+
+static time_t walltime = 0;
+
+VARP(showfps, 0, 1, 1);
+VARP(showfpsrange, 0, 0, 1);
+VAR(showeditstats, 0, 0, 1);
+VAR(statrate, 1, 200, 1000);
+
+FVARP(conscale, 1e-3f, 0.33f, 1e3f);
+
+void gl_drawhud()
+{
+ g3d_render();
+
+ int w = screenw, h = screenh;
+ if(forceaspect) w = int(ceil(h*forceaspect));
+
+ if(editmode && !hidehud && !mainmenu)
+ {
+ glEnable(GL_DEPTH_TEST);
+ glDepthMask(GL_FALSE);
+
+ renderblendbrush();
+
+ rendereditcursor();
+
+ glDepthMask(GL_TRUE);
+ glDisable(GL_DEPTH_TEST);
+ }
+
+ gettextres(w, h);
+
+ hudmatrix.ortho(0, w, h, 0, -1, 1);
+ resethudmatrix();
+
+ gle::colorf(1, 1, 1);
+
+ extern int debugsm;
+ if(debugsm)
+ {
+ extern void viewshadowmap();
+ viewshadowmap();
+ }
+
+ extern int debugglare;
+ if(debugglare)
+ {
+ extern void viewglaretex();
+ viewglaretex();
+ }
+
+ extern int debugdepthfx;
+ if(debugdepthfx)
+ {
+ extern void viewdepthfxtex();
+ viewdepthfxtex();
+ }
+
+ glEnable(GL_BLEND);
+
+ extern void debugparticles();
+ debugparticles();
+
+ if(!mainmenu)
+ {
+ drawdamagescreen(w, h);
+ drawdamagecompass(w, h);
+ }
+
+ hudshader->set();
+
+ int conw = int(w/conscale), conh = int(h/conscale), abovehud = conh - FONTH, limitgui = abovehud;
+ if(!hidehud && !mainmenu)
+ {
+ if(!hidestats)
+ {
+ pushhudmatrix();
+ hudmatrix.scale(conscale, conscale, 1);
+ flushhudmatrix();
+
+ int roffset = 0;
+ if(showfps)
+ {
+ static int lastfps = 0, prevfps[3] = { 0, 0, 0 }, curfps[3] = { 0, 0, 0 };
+ if(totalmillis - lastfps >= statrate)
+ {
+ memcpy(prevfps, curfps, sizeof(prevfps));
+ lastfps = totalmillis - (totalmillis%statrate);
+ }
+ int nextfps[3];
+ getfps(nextfps[0], nextfps[1], nextfps[2]);
+ loopi(3) if(prevfps[i]==curfps[i]) curfps[i] = nextfps[i];
+ if(showfpsrange) draw_textf("fps %d+%d-%d", conw-7*FONTH, conh-FONTH*3/2, curfps[0], curfps[1], curfps[2]);
+ else draw_textf("fps %d", conw-5*FONTH, conh-FONTH*3/2, curfps[0]);
+ roffset += FONTH;
+ }
+
+ if(wallclock)
+ {
+ if(!walltime) { walltime = time(NULL); walltime -= totalmillis/1000; if(!walltime) walltime++; }
+ time_t walloffset = walltime + totalmillis/1000;
+ struct tm *localvals = localtime(&walloffset);
+ static string buf;
+ if(localvals && strftime(buf, sizeof(buf), wallclocksecs ? (wallclock24 ? "%H:%M:%S" : "%I:%M:%S%p") : (wallclock24 ? "%H:%M" : "%I:%M%p"), localvals))
+ {
+ // hack because not all platforms (windows) support %P lowercase option
+ // also strip leading 0 from 12 hour time
+ char *dst = buf;
+ const char *src = &buf[!wallclock24 && buf[0]=='0' ? 1 : 0];
+ while(*src) *dst++ = tolower(*src++);
+ *dst++ = '\0';
+ draw_text(buf, conw-5*FONTH, conh-FONTH*3/2-roffset);
+ roffset += FONTH;
+ }
+ }
+
+ if(editmode || showeditstats)
+ {
+ static int laststats = 0, prevstats[8] = { 0, 0, 0, 0, 0, 0, 0 }, curstats[8] = { 0, 0, 0, 0, 0, 0, 0 };
+ if(totalmillis - laststats >= statrate)
+ {
+ memcpy(prevstats, curstats, sizeof(prevstats));
+ laststats = totalmillis - (totalmillis%statrate);
+ }
+ int nextstats[8] =
+ {
+ vtris*100/max(wtris, 1),
+ vverts*100/max(wverts, 1),
+ xtraverts/1024,
+ xtravertsva/1024,
+ glde,
+ gbatches,
+ getnumqueries(),
+ rplanes
+ };
+ loopi(8) if(prevstats[i]==curstats[i]) curstats[i] = nextstats[i];
+
+ abovehud -= 2*FONTH;
+ draw_textf("wtr:%dk(%d%%) wvt:%dk(%d%%) evt:%dk eva:%dk", FONTH/2, abovehud, wtris/1024, curstats[0], wverts/1024, curstats[1], curstats[2], curstats[3]);
+ draw_textf("ond:%d va:%d gl:%d(%d) oq:%d lm:%d rp:%d pvs:%d", FONTH/2, abovehud+FONTH, allocnodes*8, allocva, curstats[4], curstats[5], curstats[6], lightmaps.length(), curstats[7], getnumviewcells());
+ limitgui = abovehud;
+ }
+
+ if(editmode)
+ {
+ abovehud -= FONTH;
+ draw_textf("cube %s%d%s", FONTH/2, abovehud, selchildcount<0 ? "1/" : "", abs(selchildcount), showmat && selchildmat > 0 ? getmaterialdesc(selchildmat, ": ") : "");
+
+ if(char *editinfo = execidentstr("edithud"))
+ {
+ if(editinfo[0])
+ {
+ int tw, th;
+ text_bounds(editinfo, tw, th);
+ th += FONTH-1; th -= th%FONTH;
+ abovehud -= max(th, FONTH);
+ draw_text(editinfo, FONTH/2, abovehud);
+ }
+ DELETEA(editinfo);
+ }
+ }
+ else if(char *gameinfo = execidentstr("gamehud"))
+ {
+ if(gameinfo[0])
+ {
+ int tw, th;
+ text_bounds(gameinfo, tw, th);
+ th += FONTH-1; th -= th%FONTH;
+ roffset += max(th, FONTH);
+ draw_text(gameinfo, conw-max(5*FONTH, 2*FONTH+tw), conh-FONTH/2-roffset);
+ }
+ DELETEA(gameinfo);
+ }
+
+ pophudmatrix();
+ }
+
+ if(hidestats || (!editmode && !showeditstats))
+ {
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ game::gameplayhud(w, h);
+ limitgui = abovehud = min(abovehud, int(conh*game::abovegameplayhud(w, h)));
+ }
+
+ rendertexturepanel(w, h);
+ }
+
+ glDisable(GL_BLEND);
+
+ g3d_limitscale((2*limitgui - conh) / float(conh));
+ g3d_render2d();
+
+ glEnable(GL_BLEND);
+
+ hudmatrix.ortho(0, w, h, 0, -1, 1);
+ resethudmatrix();
+
+ pushhudmatrix();
+ hudmatrix.scale(conscale, conscale, 1);
+ flushhudmatrix();
+ abovehud -= rendercommand(FONTH/2, abovehud - FONTH/2, conw-FONTH);
+ extern int fullconsole;
+ if(!hidehud || fullconsole) renderconsole(conw, conh, abovehud - FONTH/2);
+ pophudmatrix();
+
+ drawcrosshair(w, h);
+
+ glDisable(GL_BLEND);
+}
+
+void cleanupgl()
+{
+ cleanupmotionblur();
+
+ clearminimap();
+
+ cleanupscreenquad();
+
+ gle::cleanup();
+}
+
+
diff --git a/src/engine/rendermodel.cpp b/src/engine/rendermodel.cpp
new file mode 100644
index 0000000..dc05e69
--- /dev/null
+++ b/src/engine/rendermodel.cpp
@@ -0,0 +1,1142 @@
+#include "engine.h"
+
+VAR(oqdynent, 0, 1, 1);
+VAR(animationinterpolationtime, 0, 150, 1000);
+
+model *loadingmodel = NULL;
+
+#include "ragdoll.h"
+#include "animmodel.h"
+#include "vertmodel.h"
+#include "skelmodel.h"
+
+static model *(__cdecl *modeltypes[NUMMODELTYPES])(const char *);
+
+static int addmodeltype(int type, model *(__cdecl *loader)(const char *))
+{
+ modeltypes[type] = loader;
+ return type;
+}
+
+#define MODELTYPE(modeltype, modelclass) \
+static model *__loadmodel__##modelclass(const char *filename) \
+{ \
+ return new modelclass(filename); \
+} \
+UNUSED static int __dummy__##modelclass = addmodeltype((modeltype), __loadmodel__##modelclass);
+
+#include "md2.h"
+#include "md3.h"
+#include "md5.h"
+#include "obj.h"
+#include "smd.h"
+#include "iqm.h"
+
+MODELTYPE(MDL_MD2, md2);
+MODELTYPE(MDL_MD3, md3);
+MODELTYPE(MDL_MD5, md5);
+MODELTYPE(MDL_OBJ, obj);
+MODELTYPE(MDL_SMD, smd);
+MODELTYPE(MDL_IQM, iqm);
+
+#define checkmdl if(!loadingmodel) { conoutf(CON_ERROR, "not loading a model"); return; }
+
+void mdlcullface(int *cullface)
+{
+ checkmdl;
+ loadingmodel->setcullface(*cullface!=0);
+}
+
+COMMAND(mdlcullface, "i");
+
+void mdlcollide(int *collide)
+{
+ checkmdl;
+ loadingmodel->collide = *collide!=0;
+}
+
+COMMAND(mdlcollide, "i");
+
+void mdlellipsecollide(int *collide)
+{
+ checkmdl;
+ loadingmodel->ellipsecollide = *collide!=0;
+}
+
+COMMAND(mdlellipsecollide, "i");
+
+void mdlspec(int *percent)
+{
+ checkmdl;
+ float spec = 1.0f;
+ if(*percent>0) spec = *percent/100.0f;
+ else if(*percent<0) spec = 0.0f;
+ loadingmodel->setspec(spec);
+}
+
+COMMAND(mdlspec, "i");
+
+void mdlambient(int *percent)
+{
+ checkmdl;
+ float ambient = 0.3f;
+ if(*percent>0) ambient = *percent/100.0f;
+ else if(*percent<0) ambient = 0.0f;
+ loadingmodel->setambient(ambient);
+}
+
+COMMAND(mdlambient, "i");
+
+void mdlalphatest(float *cutoff)
+{
+ checkmdl;
+ loadingmodel->setalphatest(max(0.0f, min(1.0f, *cutoff)));
+}
+
+COMMAND(mdlalphatest, "f");
+
+void mdlalphablend(int *blend)
+{
+ checkmdl;
+ loadingmodel->setalphablend(*blend!=0);
+}
+
+COMMAND(mdlalphablend, "i");
+
+void mdlalphadepth(int *depth)
+{
+ checkmdl;
+ loadingmodel->alphadepth = *depth!=0;
+}
+
+COMMAND(mdlalphadepth, "i");
+
+void mdldepthoffset(int *offset)
+{
+ checkmdl;
+ loadingmodel->depthoffset = *offset!=0;
+}
+
+COMMAND(mdldepthoffset, "i");
+
+void mdlglow(int *percent, int *delta, float *pulse)
+{
+ checkmdl;
+ float glow = 3.0f, glowdelta = *delta/100.0f, glowpulse = *pulse > 0 ? *pulse/1000.0f : 0;
+ if(*percent>0) glow = *percent/100.0f;
+ else if(*percent<0) glow = 0.0f;
+ glowdelta -= glow;
+ loadingmodel->setglow(glow, glowdelta, glowpulse);
+}
+
+COMMAND(mdlglow, "iif");
+
+void mdlglare(float *specglare, float *glowglare)
+{
+ checkmdl;
+ loadingmodel->setglare(*specglare, *glowglare);
+}
+
+COMMAND(mdlglare, "ff");
+
+void mdlenvmap(float *envmapmax, float *envmapmin, char *envmap)
+{
+ checkmdl;
+ loadingmodel->setenvmap(*envmapmin, *envmapmax, envmap[0] ? cubemapload(envmap) : NULL);
+}
+
+COMMAND(mdlenvmap, "ffs");
+
+void mdlfullbright(float *fullbright)
+{
+ checkmdl;
+ loadingmodel->setfullbright(*fullbright);
+}
+
+COMMAND(mdlfullbright, "f");
+
+void mdlshader(char *shader)
+{
+ checkmdl;
+ loadingmodel->setshader(lookupshaderbyname(shader));
+}
+
+COMMAND(mdlshader, "s");
+
+void mdlspin(float *yaw, float *pitch)
+{
+ checkmdl;
+ loadingmodel->spinyaw = *yaw;
+ loadingmodel->spinpitch = *pitch;
+}
+
+COMMAND(mdlspin, "ff");
+
+void mdlscale(int *percent)
+{
+ checkmdl;
+ float scale = 1.0f;
+ if(*percent>0) scale = *percent/100.0f;
+ loadingmodel->scale = scale;
+}
+
+COMMAND(mdlscale, "i");
+
+void mdltrans(float *x, float *y, float *z)
+{
+ checkmdl;
+ loadingmodel->translate = vec(*x, *y, *z);
+}
+
+COMMAND(mdltrans, "fff");
+
+void mdlyaw(float *angle)
+{
+ checkmdl;
+ loadingmodel->offsetyaw = *angle;
+}
+
+COMMAND(mdlyaw, "f");
+
+void mdlpitch(float *angle)
+{
+ checkmdl;
+ loadingmodel->offsetpitch = *angle;
+}
+
+COMMAND(mdlpitch, "f");
+
+void mdlshadow(int *shadow)
+{
+ checkmdl;
+ loadingmodel->shadow = *shadow!=0;
+}
+
+COMMAND(mdlshadow, "i");
+
+void mdlbb(float *rad, float *h, float *eyeheight)
+{
+ checkmdl;
+ loadingmodel->collidexyradius = *rad;
+ loadingmodel->collideheight = *h;
+ loadingmodel->eyeheight = *eyeheight;
+}
+
+COMMAND(mdlbb, "fff");
+
+void mdlextendbb(float *x, float *y, float *z)
+{
+ checkmdl;
+ loadingmodel->bbextend = vec(*x, *y, *z);
+}
+
+COMMAND(mdlextendbb, "fff");
+
+void mdlname()
+{
+ checkmdl;
+ result(loadingmodel->name);
+}
+
+COMMAND(mdlname, "");
+
+#define checkragdoll \
+ checkmdl; \
+ if(!loadingmodel->skeletal()) { conoutf(CON_ERROR, "not loading a skeletal model"); return; } \
+ skelmodel *m = (skelmodel *)loadingmodel; \
+ if(m->parts.empty()) return; \
+ skelmodel::skelmeshgroup *meshes = (skelmodel::skelmeshgroup *)m->parts.last()->meshes; \
+ if(!meshes) return; \
+ skelmodel::skeleton *skel = meshes->skel; \
+ if(!skel->ragdoll) skel->ragdoll = new ragdollskel; \
+ ragdollskel *ragdoll = skel->ragdoll; \
+ if(ragdoll->loaded) return;
+
+
+void rdvert(float *x, float *y, float *z, float *radius)
+{
+ checkragdoll;
+ ragdollskel::vert &v = ragdoll->verts.add();
+ v.pos = vec(*x, *y, *z);
+ v.radius = *radius > 0 ? *radius : 1;
+}
+COMMAND(rdvert, "ffff");
+
+void rdeye(int *v)
+{
+ checkragdoll;
+ ragdoll->eye = *v;
+}
+COMMAND(rdeye, "i");
+
+void rdtri(int *v1, int *v2, int *v3)
+{
+ checkragdoll;
+ ragdollskel::tri &t = ragdoll->tris.add();
+ t.vert[0] = *v1;
+ t.vert[1] = *v2;
+ t.vert[2] = *v3;
+}
+COMMAND(rdtri, "iii");
+
+void rdjoint(int *n, int *t, int *v1, int *v2, int *v3)
+{
+ checkragdoll;
+ if(*n < 0 || *n >= skel->numbones) return;
+ ragdollskel::joint &j = ragdoll->joints.add();
+ j.bone = *n;
+ j.tri = *t;
+ j.vert[0] = *v1;
+ j.vert[1] = *v2;
+ j.vert[2] = *v3;
+}
+COMMAND(rdjoint, "iibbb");
+
+void rdlimitdist(int *v1, int *v2, float *mindist, float *maxdist)
+{
+ checkragdoll;
+ ragdollskel::distlimit &d = ragdoll->distlimits.add();
+ d.vert[0] = *v1;
+ d.vert[1] = *v2;
+ d.mindist = *mindist;
+ d.maxdist = max(*maxdist, *mindist);
+}
+COMMAND(rdlimitdist, "iiff");
+
+void rdlimitrot(int *t1, int *t2, float *maxangle, float *qx, float *qy, float *qz, float *qw)
+{
+ checkragdoll;
+ ragdollskel::rotlimit &r = ragdoll->rotlimits.add();
+ r.tri[0] = *t1;
+ r.tri[1] = *t2;
+ r.maxangle = *maxangle * RAD;
+ r.middle = matrix3(quat(*qx, *qy, *qz, *qw));
+}
+COMMAND(rdlimitrot, "iifffff");
+
+void rdanimjoints(int *on)
+{
+ checkragdoll;
+ ragdoll->animjoints = *on!=0;
+}
+COMMAND(rdanimjoints, "i");
+
+// mapmodels
+
+vector<mapmodelinfo> mapmodels;
+
+void mmodel(char *name)
+{
+ mapmodelinfo &mmi = mapmodels.add();
+ copystring(mmi.name, name);
+ mmi.m = NULL;
+}
+
+void mapmodelcompat(int *rad, int *h, int *tex, char *name, char *shadow)
+{
+ mmodel(name);
+}
+
+void mapmodelreset(int *n)
+{
+ if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return;
+ mapmodels.shrink(clamp(*n, 0, mapmodels.length()));
+}
+
+mapmodelinfo *getmminfo(int i) { return mapmodels.inrange(i) ? &mapmodels[i] : 0; }
+const char *mapmodelname(int i) { return mapmodels.inrange(i) ? mapmodels[i].name : NULL; }
+
+COMMAND(mmodel, "s");
+COMMANDN(mapmodel, mapmodelcompat, "iiiss");
+COMMAND(mapmodelreset, "i");
+ICOMMAND(mapmodelname, "i", (int *index), { result(mapmodels.inrange(*index) ? mapmodels[*index].name : ""); });
+ICOMMAND(mapmodelloaded, "i", (int *index), { intret(mapmodels.inrange(*index) && mapmodels[*index].m ? 1 : 0); });
+ICOMMAND(nummapmodels, "", (), { intret(mapmodels.length()); });
+ICOMMAND(mapmodelfind, "s", (char *name), { int found = -1; loopv(mapmodels) if(strstr(mapmodels[i].name, name)) { found = i; break; } intret(found); });
+
+// model registry
+
+hashnameset<model *> models;
+vector<const char *> preloadmodels;
+
+void preloadmodel(const char *name)
+{
+ if(!name || !name[0] || models.access(name)) return;
+ preloadmodels.add(newstring(name));
+}
+
+void flushpreloadedmodels(bool msg)
+{
+ loopv(preloadmodels)
+ {
+ loadprogress = float(i+1)/preloadmodels.length();
+ model *m = loadmodel(preloadmodels[i], -1, msg);
+ if(!m) { if(msg) conoutf(CON_WARN, "could not load model: %s", preloadmodels[i]); }
+ else
+ {
+ m->preloadmeshes();
+ }
+ }
+ preloadmodels.deletearrays();
+ loadprogress = 0;
+}
+
+void preloadusedmapmodels(bool msg, bool bih)
+{
+ vector<extentity *> &ents = entities::getents();
+ vector<int> mapmodels;
+ loopv(ents)
+ {
+ extentity &e = *ents[i];
+ if(e.type==ET_MAPMODEL && e.attr2 >= 0 && mapmodels.find(e.attr2) < 0) mapmodels.add(e.attr2);
+ }
+
+ loopv(mapmodels)
+ {
+ loadprogress = float(i+1)/mapmodels.length();
+ int mmindex = mapmodels[i];
+ mapmodelinfo *mmi = getmminfo(mmindex);
+ if(!mmi) { if(msg) conoutf(CON_WARN, "could not find map model: %d", mmindex); }
+ else if(mmi->name[0] && !loadmodel(NULL, mmindex, msg)) { if(msg) conoutf(CON_WARN, "could not load model: %s", mmi->name); }
+ else if(mmi->m)
+ {
+ if(bih) mmi->m->preloadBIH();
+ mmi->m->preloadmeshes();
+ }
+ }
+ loadprogress = 0;
+}
+
+bool modelloaded(const char *name)
+{
+ return models.find(name, NULL) != NULL;
+}
+
+model *loadmodel(const char *name, int i, bool msg)
+{
+ if(!name)
+ {
+ if(!mapmodels.inrange(i)) return NULL;
+ mapmodelinfo &mmi = mapmodels[i];
+ if(mmi.m) return mmi.m;
+ name = mmi.name;
+ }
+ model **mm = models.access(name);
+ model *m;
+ if(mm) m = *mm;
+ else
+ {
+ if(!name[0] || loadingmodel || lightmapping > 1) return NULL;
+ if(msg)
+ {
+ defformatstring(filename, "packages/models/%s", name);
+ renderprogress(loadprogress, filename);
+ }
+ loopi(NUMMODELTYPES)
+ {
+ m = modeltypes[i](name);
+ if(!m) continue;
+ loadingmodel = m;
+ if(m->load()) break;
+ DELETEP(m);
+ }
+ loadingmodel = NULL;
+ if(!m) return NULL;
+ models.access(m->name, m);
+ m->preloadshaders();
+ }
+ if(mapmodels.inrange(i) && !mapmodels[i].m) mapmodels[i].m = m;
+ return m;
+}
+
+void preloadmodelshaders(bool force)
+{
+ if(initing) return;
+ enumerate(models, model *, m, m->preloadshaders(force));
+}
+
+void clear_mdls()
+{
+ enumerate(models, model *, m, delete m);
+}
+
+void cleanupmodels()
+{
+ enumerate(models, model *, m, m->cleanup());
+}
+
+void clearmodel(char *name)
+{
+ model **m = models.access(name);
+ if(!m) { conoutf(CON_WARN, "model %s is not loaded", name); return; }
+ loopv(mapmodels) if(mapmodels[i].m==*m) mapmodels[i].m = NULL;
+ models.remove(name);
+ (*m)->cleanup();
+ delete *m;
+ conoutf("cleared model %s", name);
+}
+
+COMMAND(clearmodel, "s");
+
+bool modeloccluded(const vec &center, float radius)
+{
+ ivec bbmin(vec(center).sub(radius)), bbmax(ivec(center).add(radius+1));
+ return pvsoccluded(bbmin, bbmax) || bboccluded(bbmin, bbmax);
+}
+
+VAR(showboundingbox, 0, 0, 2);
+
+void render2dbox(vec &o, float x, float y, float z)
+{
+ gle::begin(GL_LINE_LOOP);
+ gle::attribf(o.x, o.y, o.z);
+ gle::attribf(o.x, o.y, o.z+z);
+ gle::attribf(o.x+x, o.y+y, o.z+z);
+ gle::attribf(o.x+x, o.y+y, o.z);
+ xtraverts += gle::end();
+}
+
+void render3dbox(vec &o, float tofloor, float toceil, float xradius, float yradius)
+{
+ if(yradius<=0) yradius = xradius;
+ vec c = o;
+ c.sub(vec(xradius, yradius, tofloor));
+ float xsz = xradius*2, ysz = yradius*2;
+ float h = tofloor+toceil;
+ gle::colorf(1, 1, 1);
+ gle::defvertex();
+ render2dbox(c, xsz, 0, h);
+ render2dbox(c, 0, ysz, h);
+ c.add(vec(xsz, ysz, 0));
+ render2dbox(c, -xsz, 0, h);
+ render2dbox(c, 0, -ysz, h);
+}
+
+void renderellipse(vec &o, float xradius, float yradius, float yaw)
+{
+ gle::colorf(0.5f, 0.5f, 0.5f);
+ gle::defvertex();
+ gle::begin(GL_LINE_LOOP);
+ loopi(15)
+ {
+ const vec2 &sc = sincos360[i*(360/15)];
+ gle::attrib(vec(xradius*sc.x, yradius*sc.y, 0).rotate_around_z((yaw+90)*RAD).add(o));
+ }
+ xtraverts += gle::end();
+}
+
+struct batchedmodel
+{
+ vec pos, color, dir;
+ int anim;
+ float yaw, pitch, transparent;
+ int basetime, basetime2, flags;
+ dynent *d;
+ int attached;
+ occludequery *query;
+};
+struct modelbatch
+{
+ model *m;
+ int flags;
+ vector<batchedmodel> batched;
+};
+static vector<modelbatch *> batches;
+static vector<modelattach> modelattached;
+static int numbatches = -1;
+static occludequery *modelquery = NULL;
+
+void startmodelbatches()
+{
+ numbatches = 0;
+ modelattached.setsize(0);
+}
+
+modelbatch &addbatchedmodel(model *m)
+{
+ modelbatch *b = NULL;
+ if(m->batch>=0 && m->batch<numbatches && batches[m->batch]->m==m) b = batches[m->batch];
+ else
+ {
+ if(numbatches<batches.length())
+ {
+ b = batches[numbatches];
+ b->batched.setsize(0);
+ }
+ else b = batches.add(new modelbatch);
+ b->m = m;
+ b->flags = 0;
+ m->batch = numbatches++;
+ }
+ return *b;
+}
+
+void renderbatchedmodel(model *m, batchedmodel &b)
+{
+ modelattach *a = NULL;
+ if(b.attached>=0) a = &modelattached[b.attached];
+
+ int anim = b.anim;
+ if(shadowmapping)
+ {
+ anim |= ANIM_NOSKIN;
+ GLOBALPARAMF(shadowintensity, b.transparent);
+ }
+ else
+ {
+ if(b.flags&MDL_FULLBRIGHT) anim |= ANIM_FULLBRIGHT;
+ if(b.flags&MDL_GHOST) anim |= ANIM_GHOST;
+ }
+
+ m->render(anim, b.basetime, b.basetime2, b.pos, b.yaw, b.pitch, b.d, a, b.color, b.dir, b.transparent);
+}
+
+struct transparentmodel
+{
+ model *m;
+ batchedmodel *batched;
+ float dist;
+};
+
+static inline bool sorttransparentmodels(const transparentmodel &x, const transparentmodel &y)
+{
+ return x.dist < y.dist;
+}
+
+void endmodelbatches()
+{
+ vector<transparentmodel> transparent;
+ loopi(numbatches)
+ {
+ modelbatch &b = *batches[i];
+ if(b.batched.empty()) continue;
+ if(b.flags&(MDL_SHADOW|MDL_DYNSHADOW))
+ {
+ vec center, bbradius;
+ b.m->boundbox(center, bbradius);
+ loopvj(b.batched)
+ {
+ batchedmodel &bm = b.batched[j];
+ if(bm.flags&(MDL_SHADOW|MDL_DYNSHADOW))
+ renderblob(bm.flags&MDL_DYNSHADOW ? BLOB_DYNAMIC : BLOB_STATIC, bm.d && bm.d->ragdoll ? bm.d->ragdoll->center : bm.pos, bm.d ? bm.d->radius : max(bbradius.x, bbradius.y), bm.transparent);
+ }
+ flushblobs();
+ }
+ bool rendered = false;
+ occludequery *query = NULL;
+ if(b.flags&MDL_GHOST)
+ {
+ loopvj(b.batched)
+ {
+ batchedmodel &bm = b.batched[j];
+ if((bm.flags&(MDL_CULL_VFC|MDL_GHOST))!=MDL_GHOST || bm.query) continue;
+ if(!rendered) { b.m->startrender(); rendered = true; }
+ renderbatchedmodel(b.m, bm);
+ }
+ if(rendered)
+ {
+ b.m->endrender();
+ rendered = false;
+ }
+ }
+ loopvj(b.batched)
+ {
+ batchedmodel &bm = b.batched[j];
+ if(bm.flags&(MDL_CULL_VFC|MDL_GHOST)) continue;
+ if(bm.query!=query)
+ {
+ if(query) endquery(query);
+ query = bm.query;
+ if(query) startquery(query);
+ }
+ if(bm.transparent < 1 && (!query || query->owner==bm.d) && !shadowmapping)
+ {
+ transparentmodel &tm = transparent.add();
+ tm.m = b.m;
+ tm.batched = &bm;
+ tm.dist = camera1->o.dist(bm.d && bm.d->ragdoll ? bm.d->ragdoll->center : bm.pos);
+ continue;
+ }
+ if(!rendered) { b.m->startrender(); rendered = true; }
+ renderbatchedmodel(b.m, bm);
+ }
+ if(query) endquery(query);
+ if(rendered) b.m->endrender();
+ }
+ if(transparent.length())
+ {
+ transparent.sort(sorttransparentmodels);
+ model *lastmodel = NULL;
+ occludequery *query = NULL;
+ loopv(transparent)
+ {
+ transparentmodel &tm = transparent[i];
+ if(lastmodel!=tm.m)
+ {
+ if(lastmodel) lastmodel->endrender();
+ (lastmodel = tm.m)->startrender();
+ }
+ if(query!=tm.batched->query)
+ {
+ if(query) endquery(query);
+ query = tm.batched->query;
+ if(query) startquery(query);
+ }
+ renderbatchedmodel(tm.m, *tm.batched);
+ }
+ if(query) endquery(query);
+ if(lastmodel) lastmodel->endrender();
+ }
+ numbatches = -1;
+}
+
+void startmodelquery(occludequery *query)
+{
+ modelquery = query;
+}
+
+void endmodelquery()
+{
+ int querybatches = 0;
+ loopi(numbatches)
+ {
+ modelbatch &b = *batches[i];
+ if(b.batched.empty() || b.batched.last().query!=modelquery) continue;
+ querybatches++;
+ }
+ if(querybatches<=1)
+ {
+ if(!querybatches) modelquery->fragments = 0;
+ modelquery = NULL;
+ return;
+ }
+ int minattached = modelattached.length();
+ startquery(modelquery);
+ loopi(numbatches)
+ {
+ modelbatch &b = *batches[i];
+ if(b.batched.empty() || b.batched.last().query!=modelquery) continue;
+ b.m->startrender();
+ do
+ {
+ batchedmodel &bm = b.batched.pop();
+ if(bm.attached>=0) minattached = min(minattached, bm.attached);
+ renderbatchedmodel(b.m, bm);
+ }
+ while(b.batched.length() && b.batched.last().query==modelquery);
+ b.m->endrender();
+ }
+ endquery(modelquery);
+ modelquery = NULL;
+ modelattached.setsize(minattached);
+}
+
+VAR(maxmodelradiusdistance, 10, 200, 1000);
+
+static inline void enablecullmodelquery()
+{
+ startbb();
+}
+
+static inline void rendercullmodelquery(model *m, dynent *d, const vec &center, float radius)
+{
+ if(fabs(camera1->o.x-center.x) < radius+1 &&
+ fabs(camera1->o.y-center.y) < radius+1 &&
+ fabs(camera1->o.z-center.z) < radius+1)
+ {
+ d->query = NULL;
+ return;
+ }
+ d->query = newquery(d);
+ if(!d->query) return;
+ startquery(d->query);
+ int br = int(radius*2)+1;
+ drawbb(ivec(int(center.x-radius), int(center.y-radius), int(center.z-radius)), ivec(br, br, br));
+ endquery(d->query);
+}
+
+static inline void disablecullmodelquery()
+{
+ endbb();
+}
+
+static inline int cullmodel(model *m, const vec &center, float radius, int flags, dynent *d = NULL, bool shadow = false)
+{
+ if(flags&MDL_CULL_DIST && center.dist(camera1->o)/radius>maxmodelradiusdistance) return MDL_CULL_DIST;
+ if(flags&MDL_CULL_VFC)
+ {
+ if(reflecting || refracting)
+ {
+ if(reflecting || refracting>0)
+ {
+ if(center.z+radius<=reflectz) return MDL_CULL_VFC;
+ }
+ else
+ {
+ if(fogging && center.z+radius<reflectz-refractfog) return MDL_CULL_VFC;
+ if(!shadow && center.z-radius>=reflectz) return MDL_CULL_VFC;
+ }
+ if(center.dist(camera1->o)-radius>reflectdist) return MDL_CULL_VFC;
+ }
+ if(isfoggedsphere(radius, center)) return MDL_CULL_VFC;
+ if(shadowmapping && !isshadowmapcaster(center, radius)) return MDL_CULL_VFC;
+ }
+ if(shadowmapping)
+ {
+ if(d)
+ {
+ if(flags&MDL_CULL_OCCLUDED && d->occluded>=OCCLUDE_PARENT) return MDL_CULL_OCCLUDED;
+ if(flags&MDL_CULL_QUERY && d->occluded+1>=OCCLUDE_BB && d->query && d->query->owner==d && checkquery(d->query)) return MDL_CULL_QUERY;
+ }
+ if(!addshadowmapcaster(center, radius, radius)) return MDL_CULL_VFC;
+ }
+ else if(flags&MDL_CULL_OCCLUDED && modeloccluded(center, radius))
+ {
+ if(!reflecting && !refracting && d) d->occluded = OCCLUDE_PARENT;
+ return MDL_CULL_OCCLUDED;
+ }
+ else if(flags&MDL_CULL_QUERY && d->query && d->query->owner==d && checkquery(d->query))
+ {
+ if(!reflecting && !refracting && d->occluded<OCCLUDE_BB) d->occluded++;
+ return MDL_CULL_QUERY;
+ }
+ return 0;
+}
+
+void rendermodel(entitylight *light, const char *mdl, int anim, const vec &o, float yaw, float pitch, int flags, dynent *d, modelattach *a, int basetime, int basetime2, float trans)
+{
+ if(shadowmapping && !(flags&(MDL_SHADOW|MDL_DYNSHADOW))) return;
+ model *m = loadmodel(mdl);
+ if(!m) return;
+ vec center(0, 0, 0), bbradius(0, 0, 0);
+ float radius = 0;
+ bool shadow = !shadowmap && !glaring && (flags&(MDL_SHADOW|MDL_DYNSHADOW)) && showblobs;
+
+ if(flags&(MDL_CULL_VFC|MDL_CULL_DIST|MDL_CULL_OCCLUDED|MDL_CULL_QUERY|MDL_SHADOW|MDL_DYNSHADOW))
+ {
+ if(flags&MDL_CULL_QUERY)
+ {
+ if(!oqfrags || !oqdynent || !d) flags &= ~MDL_CULL_QUERY;
+ }
+
+ m->boundbox(center, bbradius);
+ radius = bbradius.magnitude();
+ if(d && d->ragdoll)
+ {
+ radius = max(radius, d->ragdoll->radius);
+ center = d->ragdoll->center;
+ }
+ else
+ {
+ center.rotate_around_z(yaw*RAD);
+ center.add(o);
+ }
+
+ int culled = cullmodel(m, center, radius, flags, d, shadow);
+ if(culled)
+ {
+ if(culled&(MDL_CULL_OCCLUDED|MDL_CULL_QUERY) && flags&MDL_CULL_QUERY && !reflecting && !refracting)
+ {
+ enablecullmodelquery();
+ rendercullmodelquery(m, d, center, radius);
+ disablecullmodelquery();
+ }
+ return;
+ }
+
+ if(reflecting || refracting || shadowmapping) flags &= ~MDL_CULL_QUERY;
+ }
+
+ if(flags&MDL_NORENDER) anim |= ANIM_NORENDER;
+ else if(showboundingbox && !shadowmapping && !reflecting && !refracting && editmode)
+ {
+ notextureshader->set();
+ if(d && showboundingbox==1)
+ {
+ render3dbox(d->o, d->eyeheight, d->aboveeye, d->radius);
+ renderellipse(d->o, d->xradius, d->yradius, d->yaw);
+ }
+ else
+ {
+ vec center, radius;
+ if(showboundingbox==1) m->collisionbox(center, radius);
+ else m->boundbox(center, radius);
+ rotatebb(center, radius, int(yaw));
+ center.add(o);
+ render3dbox(center, radius.z, radius.z, radius.x, radius.y);
+ }
+ }
+
+ vec lightcolor(1, 1, 1), lightdir(0, 0, 1);
+ if(!shadowmapping)
+ {
+ vec pos = o;
+ if(d)
+ {
+ if(!reflecting && !refracting) d->occluded = OCCLUDE_NOTHING;
+ if(!light) light = &d->light;
+ if(flags&MDL_LIGHT && light->millis!=lastmillis)
+ {
+ if(d->ragdoll)
+ {
+ pos = d->ragdoll->center;
+ pos.z += radius/2;
+ }
+ else if(d->type < ENT_CAMERA) pos.z += 0.75f*(d->eyeheight + d->aboveeye);
+ lightreaching(pos, light->color, light->dir, (flags&MDL_LIGHT_FAST)!=0);
+ dynlightreaching(pos, light->color, light->dir, (flags&MDL_HUD)!=0);
+ game::lighteffects(d, light->color, light->dir);
+ light->millis = lastmillis;
+ }
+ }
+ else if(flags&MDL_LIGHT)
+ {
+ if(!light)
+ {
+ lightreaching(pos, lightcolor, lightdir, (flags&MDL_LIGHT_FAST)!=0);
+ dynlightreaching(pos, lightcolor, lightdir, (flags&MDL_HUD)!=0);
+ }
+ else if(light->millis!=lastmillis)
+ {
+ lightreaching(pos, light->color, light->dir, (flags&MDL_LIGHT_FAST)!=0);
+ dynlightreaching(pos, light->color, light->dir, (flags&MDL_HUD)!=0);
+ light->millis = lastmillis;
+ }
+ }
+ if(light) { lightcolor = light->color; lightdir = light->dir; }
+ if(flags&MDL_DYNLIGHT) dynlightreaching(pos, lightcolor, lightdir, (flags&MDL_HUD)!=0);
+ }
+
+ if(a) for(int i = 0; a[i].tag; i++)
+ {
+ if(a[i].name) a[i].m = loadmodel(a[i].name);
+ //if(a[i].m && a[i].m->type()!=m->type()) a[i].m = NULL;
+ }
+
+ if(numbatches>=0)
+ {
+ modelbatch &mb = addbatchedmodel(m);
+ batchedmodel &b = mb.batched.add();
+ b.query = modelquery;
+ b.pos = o;
+ b.color = lightcolor;
+ b.dir = lightdir;
+ b.anim = anim;
+ b.yaw = yaw;
+ b.pitch = pitch;
+ b.basetime = basetime;
+ b.basetime2 = basetime2;
+ b.transparent = trans;
+ b.flags = flags & ~(MDL_CULL_VFC | MDL_CULL_DIST | MDL_CULL_OCCLUDED);
+ if(!shadow || reflecting || refracting>0)
+ {
+ b.flags &= ~(MDL_SHADOW|MDL_DYNSHADOW);
+ if((flags&MDL_CULL_VFC) && refracting<0 && center.z-radius>=reflectz) b.flags |= MDL_CULL_VFC;
+ }
+ mb.flags |= b.flags;
+ b.d = d;
+ b.attached = a ? modelattached.length() : -1;
+ if(a) for(int i = 0;; i++) { modelattached.add(a[i]); if(!a[i].tag) break; }
+ if(flags&MDL_CULL_QUERY) d->query = b.query = newquery(d);
+ return;
+ }
+
+ if(shadow && !reflecting && refracting<=0)
+ {
+ renderblob(flags&MDL_DYNSHADOW ? BLOB_DYNAMIC : BLOB_STATIC, d && d->ragdoll ? center : o, d ? d->radius : max(bbradius.x, bbradius.y), trans);
+ flushblobs();
+ if((flags&MDL_CULL_VFC) && refracting<0 && center.z-radius>=reflectz) return;
+ }
+
+ m->startrender();
+
+ if(shadowmapping)
+ {
+ anim |= ANIM_NOSKIN;
+ GLOBALPARAMF(shadowintensity, trans);
+ }
+ else
+ {
+ if(flags&MDL_FULLBRIGHT) anim |= ANIM_FULLBRIGHT;
+ if(flags&MDL_GHOST) anim |= ANIM_GHOST;
+ }
+
+ if(flags&MDL_CULL_QUERY)
+ {
+ d->query = newquery(d);
+ if(d->query) startquery(d->query);
+ }
+
+ m->render(anim, basetime, basetime2, o, yaw, pitch, d, a, lightcolor, lightdir, trans);
+
+ if(flags&MDL_CULL_QUERY && d->query) endquery(d->query);
+
+ m->endrender();
+}
+
+void abovemodel(vec &o, const char *mdl)
+{
+ model *m = loadmodel(mdl);
+ if(!m) return;
+ o.z += m->above();
+}
+
+bool matchanim(const char *name, const char *pattern)
+{
+ for(;; pattern++)
+ {
+ const char *s = name;
+ char c;
+ for(;; pattern++)
+ {
+ c = *pattern;
+ if(!c || c=='|') break;
+ else if(c=='*')
+ {
+ if(!*s || iscubespace(*s)) break;
+ do s++; while(*s && !iscubespace(*s));
+ }
+ else if(c!=*s) break;
+ else s++;
+ }
+ if(!*s && (!c || c=='|')) return true;
+ pattern = strchr(pattern, '|');
+ if(!pattern) break;
+ }
+ return false;
+}
+
+void findanims(const char *pattern, vector<int> &anims)
+{
+ loopi(sizeof(animnames)/sizeof(animnames[0])) if(matchanim(animnames[i], pattern)) anims.add(i);
+}
+
+ICOMMAND(findanims, "s", (char *name),
+{
+ vector<int> anims;
+ findanims(name, anims);
+ vector<char> buf;
+ string num;
+ loopv(anims)
+ {
+ formatstring(num, "%d", anims[i]);
+ if(i > 0) buf.add(' ');
+ buf.put(num, strlen(num));
+ }
+ buf.add('\0');
+ result(buf.getbuf());
+});
+
+void loadskin(const char *dir, const char *altdir, Texture *&skin, Texture *&masks) // model skin sharing
+{
+#define ifnoload(tex, path) if((tex = textureload(path, 0, true, false))==notexture)
+#define tryload(tex, prefix, cmd, name) \
+ ifnoload(tex, makerelpath(mdir, name ".jpg", prefix, cmd)) \
+ { \
+ ifnoload(tex, makerelpath(mdir, name ".png", prefix, cmd)) \
+ { \
+ ifnoload(tex, makerelpath(maltdir, name ".jpg", prefix, cmd)) \
+ { \
+ ifnoload(tex, makerelpath(maltdir, name ".png", prefix, cmd)) return; \
+ } \
+ } \
+ }
+
+ defformatstring(mdir, "packages/models/%s", dir);
+ defformatstring(maltdir, "packages/models/%s", altdir);
+ masks = notexture;
+ tryload(skin, NULL, NULL, "skin");
+ tryload(masks, NULL, NULL, "masks");
+}
+
+// convenient function that covers the usual anims for players/monsters/npcs
+
+VAR(animoverride, -1, 0, NUMANIMS-1);
+VAR(testanims, 0, 0, 1);
+VAR(testpitch, -90, 0, 90);
+
+void renderclient(dynent *d, const char *mdlname, modelattach *attachments, int hold, int attack, int attackdelay, int lastaction, int lastpain, float fade, bool ragdoll)
+{
+ int anim = hold ? hold : ANIM_IDLE|ANIM_LOOP;
+ float yaw = testanims && d==player ? 0 : d->yaw+90,
+ pitch = testpitch && d==player ? testpitch : d->pitch;
+ vec o = d->feetpos();
+ int basetime = 0;
+ if(animoverride) anim = (animoverride<0 ? ANIM_ALL : animoverride)|ANIM_LOOP;
+ else if(d->state==CS_DEAD)
+ {
+ anim = ANIM_DYING|ANIM_NOPITCH;
+ basetime = lastpain;
+ if(ragdoll)
+ {
+ if(!d->ragdoll || d->ragdoll->millis < basetime)
+ {
+ DELETEP(d->ragdoll);
+ anim |= ANIM_RAGDOLL;
+ }
+ }
+ else if(lastmillis-basetime>1000) anim = ANIM_DEAD|ANIM_LOOP|ANIM_NOPITCH;
+ }
+ else if(d->state==CS_EDITING || d->state==CS_SPECTATOR) anim = ANIM_EDIT|ANIM_LOOP;
+ else if(d->state==CS_LAGGED) anim = ANIM_LAG|ANIM_LOOP;
+ else
+ {
+ if(lastmillis-lastpain < 300)
+ {
+ anim = ANIM_PAIN;
+ basetime = lastpain;
+ }
+ else if(lastpain < lastaction && (attack < 0 || (d->type != ENT_AI && lastmillis-lastaction < attackdelay)))
+ {
+ anim = attack < 0 ? -attack : attack;
+ basetime = lastaction;
+ }
+
+ if(d->inwater && d->physstate<=PHYS_FALL) anim |= (((game::allowmove(d) && (d->move || d->strafe)) || d->vel.z+d->falling.z>0 ? ANIM_SWIM : ANIM_SINK)|ANIM_LOOP)<<ANIM_SECONDARY;
+ else if(d->timeinair>100) anim |= (ANIM_JUMP|ANIM_END)<<ANIM_SECONDARY;
+ else if(game::allowmove(d) && (d->move || d->strafe))
+ {
+ if(d->move>0) anim |= (ANIM_FORWARD|ANIM_LOOP)<<ANIM_SECONDARY;
+ else if(d->strafe)
+ {
+ if(d->move<0) anim |= ((d->strafe>0 ? ANIM_RIGHT : ANIM_LEFT)|ANIM_REVERSE|ANIM_LOOP)<<ANIM_SECONDARY;
+ else anim |= ((d->strafe>0 ? ANIM_LEFT : ANIM_RIGHT)|ANIM_LOOP)<<ANIM_SECONDARY;
+ }
+ else if(d->move<0) anim |= (ANIM_BACKWARD|ANIM_LOOP)<<ANIM_SECONDARY;
+ }
+
+ if((anim&ANIM_INDEX)==ANIM_IDLE && (anim>>ANIM_SECONDARY)&ANIM_INDEX) anim >>= ANIM_SECONDARY;
+ }
+ if(d->ragdoll && (!ragdoll || (anim&ANIM_INDEX)!=ANIM_DYING)) DELETEP(d->ragdoll);
+ if(!((anim>>ANIM_SECONDARY)&ANIM_INDEX)) anim |= (ANIM_IDLE|ANIM_LOOP)<<ANIM_SECONDARY;
+ int flags = MDL_LIGHT;
+ if(d!=player && !(anim&ANIM_RAGDOLL)) flags |= MDL_CULL_VFC | MDL_CULL_OCCLUDED | MDL_CULL_QUERY;
+ if(d->type==ENT_PLAYER) flags |= MDL_FULLBRIGHT;
+ else flags |= MDL_CULL_DIST;
+ if(d->state==CS_LAGGED) fade = min(fade, 0.3f);
+ else flags |= MDL_DYNSHADOW;
+ if(drawtex == DRAWTEX_MODELPREVIEW) flags &= ~(MDL_LIGHT | MDL_FULLBRIGHT | MDL_CULL_VFC | MDL_CULL_OCCLUDED | MDL_CULL_QUERY | MDL_CULL_DIST | MDL_DYNSHADOW);
+ rendermodel(NULL, mdlname, anim, o, yaw, pitch, flags, d, attachments, basetime, 0, fade);
+}
+
+void setbbfrommodel(dynent *d, const char *mdl)
+{
+ model *m = loadmodel(mdl);
+ if(!m) return;
+ vec center, radius;
+ m->collisionbox(center, radius);
+ if(d->type==ENT_INANIMATE && !m->ellipsecollide)
+ d->collidetype = COLLIDE_OBB;
+ d->xradius = radius.x + fabs(center.x);
+ d->yradius = radius.y + fabs(center.y);
+ d->radius = d->collidetype==COLLIDE_OBB ? sqrtf(d->xradius*d->xradius + d->yradius*d->yradius) : max(d->xradius, d->yradius);
+ d->eyeheight = (center.z-radius.z) + radius.z*2*m->eyeheight;
+ d->aboveeye = radius.z*2*(1.0f-m->eyeheight);
+ if (d->aboveeye + d->eyeheight <= 0.5f)
+ {
+ float zrad = (0.5f - (d->aboveeye + d->eyeheight)) / 2;
+ d->aboveeye += zrad;
+ d->eyeheight += zrad;
+ }
+}
+
diff --git a/src/engine/renderparticles.cpp b/src/engine/renderparticles.cpp
new file mode 100644
index 0000000..17350cf
--- /dev/null
+++ b/src/engine/renderparticles.cpp
@@ -0,0 +1,1551 @@
+// renderparticles.cpp
+
+#include "engine.h"
+#include "rendertarget.h"
+
+Shader *particleshader = NULL, *particlenotextureshader = NULL;
+
+VARP(particlesize, 20, 100, 500);
+
+// Check canemitparticles() to limit the rate that paricles can be emitted for models/sparklies
+// Automatically stops particles being emitted when paused or in reflective drawing
+VARP(emitmillis, 1, 17, 1000);
+static int lastemitframe = 0, emitoffset = 0;
+static bool canemit = false, regenemitters = false, canstep = false;
+
+static bool canemitparticles()
+{
+ if(reflecting || refracting) return false;
+ return canemit || emitoffset;
+}
+
+VARP(showparticles, 0, 1, 1);
+VAR(cullparticles, 0, 1, 1);
+VAR(replayparticles, 0, 1, 1);
+VARN(seedparticles, seedmillis, 0, 3000, 10000);
+VAR(dbgpcull, 0, 0, 1);
+VAR(dbgpseed, 0, 0, 1);
+
+struct particleemitter
+{
+ extentity *ent;
+ vec bbmin, bbmax;
+ vec center;
+ float radius;
+ ivec cullmin, cullmax;
+ int maxfade, lastemit, lastcull;
+
+ particleemitter(extentity *ent)
+ : ent(ent), bbmin(ent->o), bbmax(ent->o), maxfade(-1), lastemit(0), lastcull(0)
+ {}
+
+ void finalize()
+ {
+ center = vec(bbmin).add(bbmax).mul(0.5f);
+ radius = bbmin.dist(bbmax)/2;
+ cullmin = ivec(int(floor(bbmin.x)), int(floor(bbmin.y)), int(floor(bbmin.z)));
+ cullmax = ivec(int(ceil(bbmax.x)), int(ceil(bbmax.y)), int(ceil(bbmax.z)));
+ if(dbgpseed) conoutf(CON_DEBUG, "radius: %f, maxfade: %d", radius, maxfade);
+ }
+
+ void extendbb(const vec &o, float size = 0)
+ {
+ bbmin.x = min(bbmin.x, o.x - size);
+ bbmin.y = min(bbmin.y, o.y - size);
+ bbmin.z = min(bbmin.z, o.z - size);
+ bbmax.x = max(bbmax.x, o.x + size);
+ bbmax.y = max(bbmax.y, o.y + size);
+ bbmax.z = max(bbmax.z, o.z + size);
+ }
+
+ void extendbb(float z, float size = 0)
+ {
+ bbmin.z = min(bbmin.z, z - size);
+ bbmax.z = max(bbmax.z, z + size);
+ }
+};
+
+static vector<particleemitter> emitters;
+static particleemitter *seedemitter = NULL;
+
+void clearparticleemitters()
+{
+ emitters.setsize(0);
+ regenemitters = true;
+}
+
+void addparticleemitters()
+{
+ emitters.setsize(0);
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents)
+ {
+ extentity &e = *ents[i];
+ if(e.type != ET_PARTICLES) continue;
+ emitters.add(particleemitter(&e));
+ }
+ regenemitters = false;
+}
+
+enum
+{
+ PT_PART = 0,
+ PT_TAPE,
+ PT_TRAIL,
+ PT_TEXT,
+ PT_TEXTICON,
+ PT_METER,
+ PT_METERVS,
+ PT_FIREBALL,
+ PT_LIGHTNING,
+ PT_FLARE,
+
+ PT_MOD = 1<<8,
+ PT_RND4 = 1<<9,
+ PT_LERP = 1<<10, // use very sparingly - order of blending issues
+ PT_TRACK = 1<<11,
+ PT_GLARE = 1<<12,
+ PT_SOFT = 1<<13,
+ PT_HFLIP = 1<<14,
+ PT_VFLIP = 1<<15,
+ PT_ROT = 1<<16,
+ PT_CULL = 1<<17,
+ PT_FEW = 1<<18,
+ PT_ICON = 1<<19,
+ PT_NOTEX = 1<<20,
+ PT_SHADER = 1<<21,
+ PT_FLIP = PT_HFLIP | PT_VFLIP | PT_ROT
+};
+
+const char *partnames[] = { "part", "tape", "trail", "text", "texticon", "meter", "metervs", "fireball", "lightning", "flare" };
+
+struct particle
+{
+ vec o, d;
+ int gravity, fade, millis;
+ bvec color;
+ uchar flags;
+ float size;
+ union
+ {
+ const char *text;
+ float val;
+ physent *owner;
+ struct
+ {
+ uchar color2[3];
+ uchar progress;
+ };
+ };
+};
+
+struct partvert
+{
+ vec pos;
+ bvec4 color;
+ vec2 tc;
+};
+
+#define COLLIDERADIUS 8.0f
+#define COLLIDEERROR 1.0f
+
+struct partrenderer
+{
+ Texture *tex;
+ const char *texname;
+ int texclamp;
+ uint type;
+ int collide;
+ string info;
+
+ partrenderer(const char *texname, int texclamp, int type, int collide = 0)
+ : tex(NULL), texname(texname), texclamp(texclamp), type(type), collide(collide)
+ {
+ }
+ partrenderer(int type, int collide = 0)
+ : tex(NULL), texname(NULL), texclamp(0), type(type), collide(collide)
+ {
+ }
+ virtual ~partrenderer()
+ {
+ }
+
+ virtual void init(int n) { }
+ virtual void reset() = 0;
+ virtual void resettracked(physent *owner) { }
+ virtual particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity = 0) = 0;
+ virtual int adddepthfx(vec &bbmin, vec &bbmax) { return 0; }
+ virtual void update() { }
+ virtual void render() = 0;
+ virtual bool haswork() = 0;
+ virtual int count() = 0; //for debug
+ virtual void cleanup() {}
+
+ virtual void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity)
+ {
+ }
+
+ //blend = 0 => remove it
+ void calc(particle *p, int &blend, int &ts, vec &o, vec &d, bool step = true)
+ {
+ o = p->o;
+ d = p->d;
+ if(type&PT_TRACK && p->owner) game::particletrack(p->owner, o, d);
+ if(p->fade <= 5)
+ {
+ ts = 1;
+ blend = 255;
+ }
+ else
+ {
+ ts = lastmillis-p->millis;
+ blend = max(255 - (ts<<8)/p->fade, 0);
+ if(p->gravity)
+ {
+ if(ts > p->fade) ts = p->fade;
+ float t = ts;
+ o.add(vec(d).mul(t/5000.0f));
+ o.z -= t*t/(2.0f * 5000.0f * p->gravity);
+ }
+ if(collide && o.z < p->val && step)
+ {
+ if(collide >= 0)
+ {
+ vec surface;
+ float floorz = rayfloor(vec(o.x, o.y, p->val), surface, RAY_CLIPMAT, COLLIDERADIUS);
+ float collidez = floorz<0 ? o.z-COLLIDERADIUS : p->val - floorz;
+ if(o.z >= collidez+COLLIDEERROR)
+ p->val = collidez+COLLIDEERROR;
+ else
+ {
+ adddecal(collide, vec(o.x, o.y, collidez), vec(p->o).sub(o).normalize(), 2*p->size, p->color, type&PT_RND4 ? (p->flags>>5)&3 : 0);
+ blend = 0;
+ }
+ }
+ else blend = 0;
+ }
+ }
+ }
+
+ const char *debuginfo()
+ {
+ formatstring(info, "%d\t%s(", count(), partnames[type&0xFF]);
+ if(type&PT_GLARE) concatstring(info, "g,");
+ if(type&PT_SOFT) concatstring(info, "s,");
+ if(type&PT_LERP) concatstring(info, "l,");
+ if(type&PT_MOD) concatstring(info, "m,");
+ if(type&PT_RND4) concatstring(info, "r,");
+ if(type&PT_FLIP) concatstring(info, "f,");
+ if(collide) concatstring(info, "c,");
+ int len = strlen(info);
+ info[len-1] = info[len-1] == ',' ? ')' : '\0';
+ if(texname)
+ {
+ const char *title = strrchr(texname, '/');
+ if(title) concformatstring(info, ": %s", title+1);
+ }
+ return info;
+ }
+};
+
+struct listparticle : particle
+{
+ listparticle *next;
+};
+
+VARP(outlinemeters, 0, 0, 1);
+
+struct listrenderer : partrenderer
+{
+ static listparticle *parempty;
+ listparticle *list;
+
+ listrenderer(const char *texname, int texclamp, int type, int collide = 0)
+ : partrenderer(texname, texclamp, type, collide), list(NULL)
+ {
+ }
+ listrenderer(int type, int collide = 0)
+ : partrenderer(type, collide), list(NULL)
+ {
+ }
+
+ virtual ~listrenderer()
+ {
+ }
+
+ virtual void killpart(listparticle *p)
+ {
+ }
+
+ void reset()
+ {
+ if(!list) return;
+ listparticle *p = list;
+ for(;;)
+ {
+ killpart(p);
+ if(p->next) p = p->next;
+ else break;
+ }
+ p->next = parempty;
+ parempty = list;
+ list = NULL;
+ }
+
+ void resettracked(physent *owner)
+ {
+ if(!(type&PT_TRACK)) return;
+ for(listparticle **prev = &list, *cur = list; cur; cur = *prev)
+ {
+ if(!owner || cur->owner==owner)
+ {
+ *prev = cur->next;
+ cur->next = parempty;
+ parempty = cur;
+ }
+ else prev = &cur->next;
+ }
+ }
+
+ particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity)
+ {
+ if(!parempty)
+ {
+ listparticle *ps = new listparticle[256];
+ loopi(255) ps[i].next = &ps[i+1];
+ ps[255].next = parempty;
+ parempty = ps;
+ }
+ listparticle *p = parempty;
+ parempty = p->next;
+ p->next = list;
+ list = p;
+ p->o = o;
+ p->d = d;
+ p->gravity = gravity;
+ p->fade = fade;
+ p->millis = lastmillis + emitoffset;
+ p->color = bvec(color>>16, (color>>8)&0xFF, color&0xFF);
+ p->size = size;
+ p->owner = NULL;
+ p->flags = 0;
+ return p;
+ }
+
+ int count()
+ {
+ int num = 0;
+ listparticle *lp;
+ for(lp = list; lp; lp = lp->next) num++;
+ return num;
+ }
+
+ bool haswork()
+ {
+ return (list != NULL);
+ }
+
+ virtual void startrender() = 0;
+ virtual void endrender() = 0;
+ virtual void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) = 0;
+
+ void render()
+ {
+ startrender();
+ if(texname)
+ {
+ if(!tex) tex = textureload(texname, texclamp);
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+ }
+
+ for(listparticle **prev = &list, *p = list; p; p = *prev)
+ {
+ vec o, d;
+ int blend, ts;
+ calc(p, blend, ts, o, d, canstep);
+ if(blend > 0)
+ {
+ renderpart(p, o, d, blend, ts);
+
+ if(p->fade > 5 || !canstep)
+ {
+ prev = &p->next;
+ continue;
+ }
+ }
+ //remove
+ *prev = p->next;
+ p->next = parempty;
+ killpart(p);
+ parempty = p;
+ }
+
+ endrender();
+ }
+};
+
+listparticle *listrenderer::parempty = NULL;
+
+struct meterrenderer : listrenderer
+{
+ meterrenderer(int type)
+ : listrenderer(type|PT_NOTEX|PT_LERP)
+ {}
+
+ void startrender()
+ {
+ glDisable(GL_BLEND);
+ gle::defvertex();
+ }
+
+ void endrender()
+ {
+ glEnable(GL_BLEND);
+ }
+
+ void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+ {
+ int basetype = type&0xFF;
+ float scale = FONTH*p->size/80.0f, right = 8, left = p->progress/100.0f*right;
+ matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o);
+ m.scale(scale);
+ m.translate(-right/2.0f, 0, 0);
+
+ if(outlinemeters)
+ {
+ gle::colorf(0, 0.8f, 0);
+ gle::begin(GL_TRIANGLE_STRIP);
+ loopk(10)
+ {
+ const vec2 &sc = sincos360[k*(180/(10-1))];
+ float c = (0.5f + 0.1f)*sc.y, s = 0.5f - (0.5f + 0.1f)*sc.x;
+ gle::attrib(m.transform(vec2(-c, s)));
+ gle::attrib(m.transform(vec2(right + c, s)));
+ }
+ gle::end();
+ }
+
+ if(basetype==PT_METERVS) gle::colorub(p->color2[0], p->color2[1], p->color2[2]);
+ else gle::colorf(0, 0, 0);
+ gle::begin(GL_TRIANGLE_STRIP);
+ loopk(10)
+ {
+ const vec2 &sc = sincos360[k*(180/(10-1))];
+ float c = 0.5f*sc.y, s = 0.5f - 0.5f*sc.x;
+ gle::attrib(m.transform(vec2(left + c, s)));
+ gle::attrib(m.transform(vec2(right + c, s)));
+ }
+ gle::end();
+
+ if(outlinemeters)
+ {
+ gle::colorf(0, 0.8f, 0);
+ gle::begin(GL_TRIANGLE_FAN);
+ loopk(10)
+ {
+ const vec2 &sc = sincos360[k*(180/(10-1))];
+ float c = (0.5f + 0.1f)*sc.y, s = 0.5f - (0.5f + 0.1f)*sc.x;
+ gle::attrib(m.transform(vec2(left + c, s)));
+ }
+ gle::end();
+ }
+
+ gle::color(p->color);
+ gle::begin(GL_TRIANGLE_STRIP);
+ loopk(10)
+ {
+ const vec2 &sc = sincos360[k*(180/(10-1))];
+ float c = 0.5f*sc.y, s = 0.5f - 0.5f*sc.x;
+ gle::attrib(m.transform(vec2(-c, s)));
+ gle::attrib(m.transform(vec2(left + c, s)));
+ }
+ gle::end();
+ }
+};
+static meterrenderer meters(PT_METER), metervs(PT_METERVS);
+
+struct textrenderer : listrenderer
+{
+ textrenderer(int type)
+ : listrenderer(type)
+ {}
+
+ void startrender()
+ {
+ }
+
+ void endrender()
+ {
+ }
+
+ void killpart(listparticle *p)
+ {
+ if(p->text && p->flags&1) delete[] p->text;
+ }
+
+ void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+ {
+ float scale = p->size/80.0f, xoff = -(text_width(p->text) + ((p->flags>>1)&7)*FONTH)/2, yoff = 0;
+
+ matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o);
+ m.scale(scale);
+ m.translate(xoff, yoff, 50);
+
+ textmatrix = &m;
+ draw_text(p->text, 0, 0, p->color.r, p->color.g, p->color.b, blend);
+ textmatrix = NULL;
+ }
+};
+static textrenderer texts(PT_TEXT|PT_LERP);
+
+struct texticonrenderer : listrenderer
+{
+ texticonrenderer(const char *texname, int type)
+ : listrenderer(texname, 3, type)
+ {}
+
+ void startrender()
+ {
+ gle::defvertex();
+ gle::deftexcoord0();
+ gle::defcolor(4, GL_UNSIGNED_BYTE);
+ gle::begin(GL_QUADS);
+ }
+
+ void endrender()
+ {
+ gle::end();
+ }
+
+ void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+ {
+ float scale = p->size/80.0f, xoff = p->val, yoff = 0;
+
+ matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o);
+ m.scale(scale);
+ m.translate(xoff, yoff, 50);
+
+ float tx = 0.25f*(p->flags&3), ty = 0.25f*((p->flags>>2)&3);
+
+ gle::attrib(m.transform(vec2(0, 0)));
+ gle::attrib(tx, ty);
+ gle::attrib(p->color, blend);
+ gle::attrib(m.transform(vec2(FONTH, 0)));
+ gle::attrib(tx + 0.25f, ty);
+ gle::attrib(p->color, blend);
+ gle::attrib(m.transform(vec2(FONTH, FONTH)));
+ gle::attrib(tx + 0.25f, ty + 0.25f);
+ gle::attrib(p->color, blend);
+ gle::attrib(m.transform(vec2(0, FONTH)));
+ gle::attrib(tx, ty + 0.25f);
+ gle::attrib(p->color, blend);
+ }
+};
+static texticonrenderer texticons("packages/hud/items.png", PT_TEXTICON|PT_LERP);
+
+template<int T>
+static inline void modifyblend(const vec &o, int &blend)
+{
+ blend = min(blend<<2, 255);
+}
+
+template<>
+inline void modifyblend<PT_TAPE>(const vec &o, int &blend)
+{
+}
+
+template<int T>
+static inline void genpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs)
+{
+ vec udir = vec(camup).sub(camright).mul(size);
+ vec vdir = vec(camup).add(camright).mul(size);
+ vs[0].pos = vec(o.x + udir.x, o.y + udir.y, o.z + udir.z);
+ vs[1].pos = vec(o.x + vdir.x, o.y + vdir.y, o.z + vdir.z);
+ vs[2].pos = vec(o.x - udir.x, o.y - udir.y, o.z - udir.z);
+ vs[3].pos = vec(o.x - vdir.x, o.y - vdir.y, o.z - vdir.z);
+}
+
+template<>
+inline void genpos<PT_TAPE>(const vec &o, const vec &d, float size, int ts, int grav, partvert *vs)
+{
+ vec dir1 = d, dir2 = d, c;
+ dir1.sub(o);
+ dir2.sub(camera1->o);
+ c.cross(dir2, dir1).normalize().mul(size);
+ vs[0].pos = vec(d.x-c.x, d.y-c.y, d.z-c.z);
+ vs[1].pos = vec(o.x-c.x, o.y-c.y, o.z-c.z);
+ vs[2].pos = vec(o.x+c.x, o.y+c.y, o.z+c.z);
+ vs[3].pos = vec(d.x+c.x, d.y+c.y, d.z+c.z);
+}
+
+template<>
+inline void genpos<PT_TRAIL>(const vec &o, const vec &d, float size, int ts, int grav, partvert *vs)
+{
+ vec e = d;
+ if(grav) e.z -= float(ts)/grav;
+ e.div(-75.0f).add(o);
+ genpos<PT_TAPE>(o, e, size, ts, grav, vs);
+}
+
+template<int T>
+static inline void genrotpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs, int rot)
+{
+ genpos<T>(o, d, size, grav, ts, vs);
+}
+
+#define ROTCOEFFS(n) { \
+ vec(-1, 1, 0).rotate_around_z(n*2*M_PI/32.0f), \
+ vec( 1, 1, 0).rotate_around_z(n*2*M_PI/32.0f), \
+ vec( 1, -1, 0).rotate_around_z(n*2*M_PI/32.0f), \
+ vec(-1, -1, 0).rotate_around_z(n*2*M_PI/32.0f) \
+}
+static const vec rotcoeffs[32][4] =
+{
+ ROTCOEFFS(0), ROTCOEFFS(1), ROTCOEFFS(2), ROTCOEFFS(3), ROTCOEFFS(4), ROTCOEFFS(5), ROTCOEFFS(6), ROTCOEFFS(7),
+ ROTCOEFFS(8), ROTCOEFFS(9), ROTCOEFFS(10), ROTCOEFFS(11), ROTCOEFFS(12), ROTCOEFFS(13), ROTCOEFFS(14), ROTCOEFFS(15),
+ ROTCOEFFS(16), ROTCOEFFS(17), ROTCOEFFS(18), ROTCOEFFS(19), ROTCOEFFS(20), ROTCOEFFS(21), ROTCOEFFS(22), ROTCOEFFS(7),
+ ROTCOEFFS(24), ROTCOEFFS(25), ROTCOEFFS(26), ROTCOEFFS(27), ROTCOEFFS(28), ROTCOEFFS(29), ROTCOEFFS(30), ROTCOEFFS(31),
+};
+
+template<>
+inline void genrotpos<PT_PART>(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs, int rot)
+{
+ const vec *coeffs = rotcoeffs[rot];
+ (vs[0].pos = o).add(vec(camright).mul(coeffs[0].x*size)).add(vec(camup).mul(coeffs[0].y*size));
+ (vs[1].pos = o).add(vec(camright).mul(coeffs[1].x*size)).add(vec(camup).mul(coeffs[1].y*size));
+ (vs[2].pos = o).add(vec(camright).mul(coeffs[2].x*size)).add(vec(camup).mul(coeffs[2].y*size));
+ (vs[3].pos = o).add(vec(camright).mul(coeffs[3].x*size)).add(vec(camup).mul(coeffs[3].y*size));
+}
+
+template<int T>
+static inline void seedpos(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav)
+{
+ if(grav)
+ {
+ vec end(o);
+ float t = fade;
+ end.add(vec(d).mul(t/5000.0f));
+ end.z -= t*t/(2.0f * 5000.0f * grav);
+ pe.extendbb(end, size);
+
+ float tpeak = d.z*grav;
+ if(tpeak > 0 && tpeak < fade) pe.extendbb(o.z + 1.5f*d.z*tpeak/5000.0f, size);
+ }
+}
+
+template<>
+inline void seedpos<PT_TAPE>(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav)
+{
+ pe.extendbb(d, size);
+}
+
+template<>
+inline void seedpos<PT_TRAIL>(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav)
+{
+ vec e = d;
+ if(grav) e.z -= float(fade)/grav;
+ e.div(-75.0f).add(o);
+ pe.extendbb(e, size);
+}
+
+template<int T>
+struct varenderer : partrenderer
+{
+ partvert *verts;
+ particle *parts;
+ int maxparts, numparts, lastupdate, rndmask;
+ GLuint vbo;
+
+ varenderer(const char *texname, int type, int collide = 0)
+ : partrenderer(texname, 3, type, collide),
+ verts(NULL), parts(NULL), maxparts(0), numparts(0), lastupdate(-1), rndmask(0), vbo(0)
+ {
+ if(type & PT_HFLIP) rndmask |= 0x01;
+ if(type & PT_VFLIP) rndmask |= 0x02;
+ if(type & PT_ROT) rndmask |= 0x1F<<2;
+ if(type & PT_RND4) rndmask |= 0x03<<5;
+ }
+
+ void cleanup()
+ {
+ if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+ }
+
+ void init(int n)
+ {
+ DELETEA(parts);
+ DELETEA(verts);
+ parts = new particle[n];
+ verts = new partvert[n*4];
+ maxparts = n;
+ numparts = 0;
+ lastupdate = -1;
+ }
+
+ void reset()
+ {
+ numparts = 0;
+ lastupdate = -1;
+ }
+
+ void resettracked(physent *owner)
+ {
+ if(!(type&PT_TRACK)) return;
+ loopi(numparts)
+ {
+ particle *p = parts+i;
+ if(!owner || (p->owner == owner)) p->fade = -1;
+ }
+ lastupdate = -1;
+ }
+
+ int count()
+ {
+ return numparts;
+ }
+
+ bool haswork()
+ {
+ return (numparts > 0);
+ }
+
+ particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity)
+ {
+ particle *p = parts + (numparts < maxparts ? numparts++ : rnd(maxparts)); //next free slot, or kill a random kitten
+ p->o = o;
+ p->d = d;
+ p->gravity = gravity;
+ p->fade = fade;
+ p->millis = lastmillis + emitoffset;
+ p->color = bvec(color>>16, (color>>8)&0xFF, color&0xFF);
+ p->size = size;
+ p->owner = NULL;
+ p->flags = 0x80 | (rndmask ? rnd(0x80) & rndmask : 0);
+ lastupdate = -1;
+ return p;
+ }
+
+ void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity)
+ {
+ pe.maxfade = max(pe.maxfade, fade);
+ size *= SQRT2;
+ pe.extendbb(o, size);
+
+ seedpos<T>(pe, o, d, fade, size, gravity);
+ if(!gravity) return;
+
+ vec end(o);
+ float t = fade;
+ end.add(vec(d).mul(t/5000.0f));
+ end.z -= t*t/(2.0f * 5000.0f * gravity);
+ pe.extendbb(end, size);
+
+ float tpeak = d.z*gravity;
+ if(tpeak > 0 && tpeak < fade) pe.extendbb(o.z + 1.5f*d.z*tpeak/5000.0f, size);
+ }
+
+ void genverts(particle *p, partvert *vs, bool regen)
+ {
+ vec o, d;
+ int blend, ts;
+
+ calc(p, blend, ts, o, d);
+ if(blend <= 1 || p->fade <= 5) p->fade = -1; //mark to remove on next pass (i.e. after render)
+
+ modifyblend<T>(o, blend);
+
+ if(regen)
+ {
+ p->flags &= ~0x80;
+
+ #define SETTEXCOORDS(u1c, u2c, v1c, v2c, body) \
+ { \
+ float u1 = u1c, u2 = u2c, v1 = v1c, v2 = v2c; \
+ body; \
+ vs[0].tc = vec2(u1, v1); \
+ vs[1].tc = vec2(u2, v1); \
+ vs[2].tc = vec2(u2, v2); \
+ vs[3].tc = vec2(u1, v2); \
+ }
+ if(type&PT_RND4)
+ {
+ float tx = 0.5f*((p->flags>>5)&1), ty = 0.5f*((p->flags>>6)&1);
+ SETTEXCOORDS(tx, tx + 0.5f, ty, ty + 0.5f,
+ {
+ if(p->flags&0x01) swap(u1, u2);
+ if(p->flags&0x02) swap(v1, v2);
+ });
+ }
+ else if(type&PT_ICON)
+ {
+ float tx = 0.25f*(p->flags&3), ty = 0.25f*((p->flags>>2)&3);
+ SETTEXCOORDS(tx, tx + 0.25f, ty, ty + 0.25f, {});
+ }
+ else SETTEXCOORDS(0, 1, 0, 1, {});
+
+ #define SETCOLOR(r, g, b, a) \
+ do { \
+ bvec4 col(r, g, b, a); \
+ loopi(4) vs[i].color = col; \
+ } while(0)
+ #define SETMODCOLOR SETCOLOR((p->color.r*blend)>>8, (p->color.g*blend)>>8, (p->color.b*blend)>>8, 255)
+ if(type&PT_MOD) SETMODCOLOR;
+ else SETCOLOR(p->color.r, p->color.g, p->color.b, blend);
+ }
+ else if(type&PT_MOD) SETMODCOLOR;
+ else loopi(4) vs[i].color.a = blend;
+
+ if(type&PT_ROT) genrotpos<T>(o, d, p->size, ts, p->gravity, vs, (p->flags>>2)&0x1F);
+ else genpos<T>(o, d, p->size, ts, p->gravity, vs);
+ }
+
+ void genverts()
+ {
+ loopi(numparts)
+ {
+ particle *p = &parts[i];
+ partvert *vs = &verts[i*4];
+ if(p->fade < 0)
+ {
+ do
+ {
+ --numparts;
+ if(numparts <= i) return;
+ }
+ while(parts[numparts].fade < 0);
+ *p = parts[numparts];
+ genverts(p, vs, true);
+ }
+ else genverts(p, vs, (p->flags&0x80)!=0);
+ }
+ }
+
+ void update()
+ {
+ if(lastmillis == lastupdate && vbo) return;
+ lastupdate = lastmillis;
+
+ genverts();
+
+ if(!vbo) glGenBuffers_(1, &vbo);
+ gle::bindvbo(vbo);
+ glBufferData_(GL_ARRAY_BUFFER, maxparts*4*sizeof(partvert), NULL, GL_STREAM_DRAW);
+ glBufferSubData_(GL_ARRAY_BUFFER, 0, numparts*4*sizeof(partvert), verts);
+ gle::clearvbo();
+ }
+
+ void render()
+ {
+ if(!tex) tex = textureload(texname, texclamp);
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+
+ gle::bindvbo(vbo);
+ const partvert *ptr = 0;
+ gle::vertexpointer(sizeof(partvert), ptr->pos.v);
+ gle::texcoord0pointer(sizeof(partvert), ptr->tc.v);
+ gle::colorpointer(sizeof(partvert), ptr->color.v);
+ gle::enablevertex();
+ gle::enabletexcoord0();
+ gle::enablecolor();
+ gle::enablequads();
+
+ gle::drawquads(0, numparts);
+
+ gle::disablequads();
+ gle::disablevertex();
+ gle::disabletexcoord0();
+ gle::disablecolor();
+ gle::clearvbo();
+ }
+};
+typedef varenderer<PT_PART> quadrenderer;
+typedef varenderer<PT_TAPE> taperenderer;
+typedef varenderer<PT_TRAIL> trailrenderer;
+
+#include "depthfx.h"
+#include "explosion.h"
+#include "lensflare.h"
+#include "lightning.h"
+
+struct softquadrenderer : quadrenderer
+{
+ softquadrenderer(const char *texname, int type, int collide = 0)
+ : quadrenderer(texname, type|PT_SOFT, collide)
+ {
+ }
+
+ int adddepthfx(vec &bbmin, vec &bbmax)
+ {
+ if(!depthfxtex.highprecision() && !depthfxtex.emulatehighprecision()) return 0;
+ int numsoft = 0;
+ loopi(numparts)
+ {
+ particle &p = parts[i];
+ float radius = p.size*SQRT2;
+ vec o, d;
+ int blend, ts;
+ calc(&p, blend, ts, o, d, false);
+ if(!isfoggedsphere(radius, p.o) && (depthfxscissor!=2 || depthfxtex.addscissorbox(p.o, radius)))
+ {
+ numsoft++;
+ loopk(3)
+ {
+ bbmin[k] = min(bbmin[k], o[k] - radius);
+ bbmax[k] = max(bbmax[k], o[k] + radius);
+ }
+ }
+ }
+ return numsoft;
+ }
+};
+
+static partrenderer *parts[] =
+{
+ new quadrenderer("<grey>packages/particles/blood.png", PT_PART|PT_FLIP|PT_MOD|PT_RND4, DECAL_BLOOD), // blood spats (note: rgb is inverted)
+ new trailrenderer("packages/particles/base.png", PT_TRAIL|PT_LERP), // water, entity
+ new quadrenderer("<grey>packages/particles/smoke.png", PT_PART|PT_FLIP|PT_LERP), // smoke
+ new quadrenderer("<grey>packages/particles/steam.png", PT_PART|PT_FLIP), // steam
+ new quadrenderer("<grey>packages/particles/flames.png", PT_PART|PT_HFLIP|PT_RND4|PT_GLARE), // flame on - no flipping please, they have orientation
+ new quadrenderer("packages/particles/ball1.png", PT_PART|PT_FEW|PT_GLARE), // fireball1
+ new quadrenderer("packages/particles/ball2.png", PT_PART|PT_FEW|PT_GLARE), // fireball2
+ new quadrenderer("packages/particles/ball3.png", PT_PART|PT_FEW|PT_GLARE), // fireball3
+ new taperenderer("packages/particles/flare.jpg", PT_TAPE|PT_GLARE), // streak
+ &lightnings, // lightning
+ &fireballs, // explosion fireball
+ &bluefireballs, // bluish explosion fireball
+ new quadrenderer("packages/particles/spark.png", PT_PART|PT_FLIP|PT_GLARE), // sparks
+ new quadrenderer("packages/particles/base.png", PT_PART|PT_FLIP|PT_GLARE), // edit mode entities
+ new quadrenderer("<grey>packages/particles/snow.png", PT_PART|PT_FLIP|PT_RND4, -1), // colliding snow
+ new quadrenderer("packages/particles/muzzleflash1.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash
+ new quadrenderer("packages/particles/muzzleflash2.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash
+ new quadrenderer("packages/particles/muzzleflash3.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash
+ new quadrenderer("packages/hud/items.png", PT_PART|PT_FEW|PT_ICON), // hud icon
+ new quadrenderer("<colorify:1/1/1>packages/hud/items.png", PT_PART|PT_FEW|PT_ICON), // grey hud icon
+ &texts, // text
+ &texticons, // text icons
+ &meters, // meter
+ &metervs, // meter vs.
+ &flares // lens flares - must be done last
+};
+
+void finddepthfxranges()
+{
+ depthfxmin = vec(1e16f, 1e16f, 1e16f);
+ depthfxmax = vec(0, 0, 0);
+ numdepthfxranges = fireballs.finddepthfxranges(depthfxowners, depthfxranges, 0, MAXDFXRANGES, depthfxmin, depthfxmax);
+ numdepthfxranges = bluefireballs.finddepthfxranges(depthfxowners, depthfxranges, numdepthfxranges, MAXDFXRANGES, depthfxmin, depthfxmax);
+ loopk(3)
+ {
+ depthfxmin[k] -= depthfxmargin;
+ depthfxmax[k] += depthfxmargin;
+ }
+ if(depthfxparts)
+ {
+ loopi(sizeof(parts)/sizeof(parts[0]))
+ {
+ partrenderer *p = parts[i];
+ if(p->type&PT_SOFT && p->adddepthfx(depthfxmin, depthfxmax))
+ {
+ if(!numdepthfxranges)
+ {
+ numdepthfxranges = 1;
+ depthfxowners[0] = NULL;
+ depthfxranges[0] = 0;
+ }
+ }
+ }
+ }
+ if(depthfxscissor<2 && numdepthfxranges>0) depthfxtex.addscissorbox(depthfxmin, depthfxmax);
+}
+
+VARFP(maxparticles, 10, 4000, 40000, initparticles());
+VARFP(fewparticles, 10, 100, 40000, initparticles());
+
+void initparticles()
+{
+ if(!particleshader) particleshader = lookupshaderbyname("particle");
+ if(!particlenotextureshader) particlenotextureshader = lookupshaderbyname("particlenotexture");
+ loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->init(parts[i]->type&PT_FEW ? min(fewparticles, maxparticles) : maxparticles);
+}
+
+void clearparticles()
+{
+ loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->reset();
+ clearparticleemitters();
+}
+
+void cleanupparticles()
+{
+ loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->cleanup();
+}
+
+void removetrackedparticles(physent *owner)
+{
+ loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->resettracked(owner);
+}
+
+VARP(particleglare, 0, 2, 100);
+
+VARN(debugparticles, dbgparts, 0, 0, 1);
+
+void debugparticles()
+{
+ if(!dbgparts) return;
+ int n = sizeof(parts)/sizeof(parts[0]);
+ pushhudmatrix();
+ hudmatrix.ortho(0, FONTH*n*2*screenw/float(screenh), FONTH*n*2, 0, -1, 1); //squeeze into top-left corner
+ flushhudmatrix();
+ hudshader->set();
+ loopi(n) draw_text(parts[i]->info, FONTH, (i+n/2)*FONTH);
+ pophudmatrix();
+}
+
+void renderparticles(bool mainpass)
+{
+ canstep = mainpass;
+ //want to debug BEFORE the lastpass render (that would delete particles)
+ if(dbgparts && mainpass) loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->debuginfo();
+
+ if(glaring && !particleglare) return;
+
+ loopi(sizeof(parts)/sizeof(parts[0]))
+ {
+ if(glaring && !(parts[i]->type&PT_GLARE)) continue;
+ parts[i]->update();
+ }
+
+ bool rendered = false;
+ uint lastflags = PT_LERP|PT_SHADER,
+ flagmask = PT_LERP|PT_MOD|PT_SHADER|PT_NOTEX;
+
+ if(binddepthfxtex()) flagmask |= PT_SOFT;
+
+ loopi(sizeof(parts)/sizeof(parts[0]))
+ {
+ partrenderer *p = parts[i];
+ if(glaring && !(p->type&PT_GLARE)) continue;
+ if(!p->haswork()) continue;
+
+ if(!rendered)
+ {
+ rendered = true;
+ glDepthMask(GL_FALSE);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ if(glaring) GLOBALPARAMF(colorscale, particleglare, particleglare, particleglare, 1);
+ else GLOBALPARAMF(colorscale, 1, 1, 1, 1);
+ }
+
+ uint flags = p->type & flagmask, changedbits = (flags ^ lastflags);
+ if(changedbits)
+ {
+ if(changedbits&PT_LERP)
+ {
+ if(flags&PT_LERP) resetfogcolor();
+ else zerofogcolor();
+ }
+ if(changedbits&(PT_LERP|PT_MOD))
+ {
+ if(flags&PT_LERP) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ else if(flags&PT_MOD) glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
+ else glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+ }
+ if(!(flags&PT_SHADER))
+ {
+ if(changedbits&(PT_SOFT|PT_SHADER|PT_NOTEX|PT_LERP))
+ {
+ if(flags&PT_SOFT)
+ {
+ if(!depthfxtex.highprecision()) SETSHADER(particlesoft8);
+ else SETSHADER(particlesoft);
+
+ binddepthfxparams(depthfxpartblend);
+ }
+ else if(flags&PT_NOTEX) particlenotextureshader->set();
+ else particleshader->set();
+ }
+ }
+ lastflags = flags;
+ }
+ p->render();
+ }
+
+ if(rendered)
+ {
+ if(lastflags&(PT_LERP|PT_MOD)) glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+ if(!(lastflags&PT_LERP)) resetfogcolor();
+ glDisable(GL_BLEND);
+ glDepthMask(GL_TRUE);
+ }
+}
+
+static int addedparticles = 0;
+
+static inline particle *newparticle(const vec &o, const vec &d, int fade, int type, int color, float size, int gravity = 0)
+{
+ static particle dummy;
+ if(seedemitter)
+ {
+ parts[type]->seedemitter(*seedemitter, o, d, fade, size, gravity);
+ return &dummy;
+ }
+ if(fade + emitoffset < 0) return &dummy;
+ addedparticles++;
+ return parts[type]->addpart(o, d, fade, color, size, gravity);
+}
+
+VARP(maxparticledistance, 256, 1024, 4096);
+
+static void splash(int type, int color, int radius, int num, int fade, const vec &p, float size, int gravity)
+{
+ if(camera1->o.dist(p) > maxparticledistance && !seedemitter) return;
+ float collidez = parts[type]->collide ? p.z - raycube(p, vec(0, 0, -1), COLLIDERADIUS, RAY_CLIPMAT) + (parts[type]->collide >= 0 ? COLLIDEERROR : 0) : -1;
+ int fmin = 1;
+ int fmax = fade*3;
+ loopi(num)
+ {
+ int x, y, z;
+ do
+ {
+ x = rnd(radius*2)-radius;
+ y = rnd(radius*2)-radius;
+ z = rnd(radius*2)-radius;
+ }
+ while(x*x+y*y+z*z>radius*radius);
+ vec tmp = vec((float)x, (float)y, (float)z);
+ int f = (num < 10) ? (fmin + rnd(fmax)) : (fmax - (i*(fmax-fmin))/(num-1)); //help deallocater by using fade distribution rather than random
+ newparticle(p, tmp, f, type, color, size, gravity)->val = collidez;
+ }
+}
+
+static void regularsplash(int type, int color, int radius, int num, int fade, const vec &p, float size, int gravity, int delay = 0)
+{
+ if(!canemitparticles() || (delay > 0 && rnd(delay) != 0)) return;
+ splash(type, color, radius, num, fade, p, size, gravity);
+}
+
+bool canaddparticles()
+{
+ return !renderedgame && !shadowmapping && !minimized;
+}
+
+void regular_particle_splash(int type, int num, int fade, const vec &p, int color, float size, int radius, int gravity, int delay)
+{
+ if(!canaddparticles()) return;
+ regularsplash(type, color, radius, num, fade, p, size, gravity, delay);
+}
+
+void particle_splash(int type, int num, int fade, const vec &p, int color, float size, int radius, int gravity)
+{
+ if(!canaddparticles()) return;
+ splash(type, color, radius, num, fade, p, size, gravity);
+}
+
+VARP(maxtrail, 1, 500, 10000);
+
+void particle_trail(int type, int fade, const vec &s, const vec &e, int color, float size, int gravity)
+{
+ if(!canaddparticles()) return;
+ vec v;
+ float d = e.dist(s, v);
+ int steps = clamp(int(d*2), 1, maxtrail);
+ v.div(steps);
+ vec p = s;
+ loopi(steps)
+ {
+ p.add(v);
+ vec tmp = vec(float(rnd(11)-5), float(rnd(11)-5), float(rnd(11)-5));
+ newparticle(p, tmp, rnd(fade)+fade, type, color, size, gravity);
+ }
+}
+
+VARP(particletext, 0, 1, 1);
+VARP(maxparticletextdistance, 0, 128, 10000);
+
+void particle_text(const vec &s, const char *t, int type, int fade, int color, float size, int gravity, int icons)
+{
+ if(!canaddparticles()) return;
+ if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return;
+ particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity);
+ p->text = t;
+ p->flags = icons<<1;
+}
+
+void particle_textcopy(const vec &s, const char *t, int type, int fade, int color, float size, int gravity)
+{
+ if(!canaddparticles()) return;
+ if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return;
+ particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity);
+ p->text = newstring(t);
+ p->flags = 1;
+}
+
+void particle_texticon(const vec &s, int ix, int iy, float offset, int type, int fade, int color, float size, int gravity)
+{
+ if(!canaddparticles()) return;
+ if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return;
+ particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity);
+ p->flags |= ix | (iy<<2);
+ p->val = offset;
+}
+
+void particle_icon(const vec &s, int ix, int iy, int type, int fade, int color, float size, int gravity)
+{
+ if(!canaddparticles()) return;
+ particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity);
+ p->flags |= ix | (iy<<2);
+}
+
+void particle_meter(const vec &s, float val, int type, int fade, int color, int color2, float size)
+{
+ if(!canaddparticles()) return;
+ particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size);
+ p->color2[0] = color2>>16;
+ p->color2[1] = (color2>>8)&0xFF;
+ p->color2[2] = color2&0xFF;
+ p->progress = clamp(int(val*100), 0, 100);
+}
+
+void particle_flare(const vec &p, const vec &dest, int fade, int type, int color, float size, physent *owner)
+{
+ if(!canaddparticles()) return;
+ newparticle(p, dest, fade, type, color, size)->owner = owner;
+}
+
+void particle_fireball(const vec &dest, float maxsize, int type, int fade, int color, float size)
+{
+ if(!canaddparticles()) return;
+ float growth = maxsize - size;
+ if(fade < 0) fade = int(growth*20);
+ newparticle(dest, vec(0, 0, 1), fade, type, color, size)->val = growth;
+}
+
+//dir = 0..6 where 0=up
+static inline vec offsetvec(vec o, int dir, int dist)
+{
+ vec v = vec(o);
+ v[(2+dir)%3] += (dir>2)?(-dist):dist;
+ return v;
+}
+
+//converts a 16bit color to 24bit
+static inline int colorfromattr(int attr)
+{
+ return (((attr&0xF)<<4) | ((attr&0xF0)<<8) | ((attr&0xF00)<<12)) + 0x0F0F0F;
+}
+
+/* Experiments in shapes...
+ * dir: (where dir%3 is similar to offsetvec with 0=up)
+ * 0..2 circle
+ * 3.. 5 cylinder shell
+ * 6..11 cone shell
+ * 12..14 plane volume
+ * 15..20 line volume, i.e. wall
+ * 21 sphere
+ * 24..26 flat plane
+ * +32 to inverse direction
+ */
+void regularshape(int type, int radius, int color, int dir, int num, int fade, const vec &p, float size, int gravity, int vel = 200)
+{
+ if(!canemitparticles()) return;
+
+ int basetype = parts[type]->type&0xFF;
+ bool flare = (basetype == PT_TAPE) || (basetype == PT_LIGHTNING),
+ inv = (dir&0x20)!=0, taper = (dir&0x40)!=0 && !seedemitter;
+ dir &= 0x1F;
+ loopi(num)
+ {
+ vec to, from;
+ if(dir < 12)
+ {
+ const vec2 &sc = sincos360[rnd(360)];
+ to[dir%3] = sc.y*radius;
+ to[(dir+1)%3] = sc.x*radius;
+ to[(dir+2)%3] = 0.0;
+ to.add(p);
+ if(dir < 3) //circle
+ from = p;
+ else if(dir < 6) //cylinder
+ {
+ from = to;
+ to[(dir+2)%3] += radius;
+ from[(dir+2)%3] -= radius;
+ }
+ else //cone
+ {
+ from = p;
+ to[(dir+2)%3] += (dir < 9)?radius:(-radius);
+ }
+ }
+ else if(dir < 15) //plane
+ {
+ to[dir%3] = float(rnd(radius<<4)-(radius<<3))/8.0;
+ to[(dir+1)%3] = float(rnd(radius<<4)-(radius<<3))/8.0;
+ to[(dir+2)%3] = radius;
+ to.add(p);
+ from = to;
+ from[(dir+2)%3] -= 2*radius;
+ }
+ else if(dir < 21) //line
+ {
+ if(dir < 18)
+ {
+ to[dir%3] = float(rnd(radius<<4)-(radius<<3))/8.0;
+ to[(dir+1)%3] = 0.0;
+ }
+ else
+ {
+ to[dir%3] = 0.0;
+ to[(dir+1)%3] = float(rnd(radius<<4)-(radius<<3))/8.0;
+ }
+ to[(dir+2)%3] = 0.0;
+ to.add(p);
+ from = to;
+ to[(dir+2)%3] += radius;
+ }
+ else if(dir < 24) //sphere
+ {
+ to = vec(2*M_PI*float(rnd(1000))/1000.0, M_PI*float(rnd(1000)-500)/1000.0).mul(radius);
+ to.add(p);
+ from = p;
+ }
+ else if(dir < 27) // flat plane
+ {
+ to[dir%3] = float(rndscale(2*radius)-radius);
+ to[(dir+1)%3] = float(rndscale(2*radius)-radius);
+ to[(dir+2)%3] = 0.0;
+ to.add(p);
+ from = to;
+ }
+ else from = to = p;
+
+ if(inv) swap(from, to);
+
+ if(taper)
+ {
+ float dist = clamp(from.dist2(camera1->o)/maxparticledistance, 0.0f, 1.0f);
+ if(dist > 0.2f)
+ {
+ dist = 1 - (dist - 0.2f)/0.8f;
+ if(rnd(0x10000) > dist*dist*0xFFFF) continue;
+ }
+ }
+
+ if(flare)
+ newparticle(from, to, rnd(fade*3)+1, type, color, size, gravity);
+ else
+ {
+ vec d = vec(to).sub(from).rescale(vel); //velocity
+ particle *n = newparticle(from, d, rnd(fade*3)+1, type, color, size, gravity);
+ if(parts[type]->collide)
+ n->val = from.z - raycube(from, vec(0, 0, -1), parts[type]->collide >= 0 ? COLLIDERADIUS : max(from.z, 0.0f), RAY_CLIPMAT) + (parts[type]->collide >= 0 ? COLLIDEERROR : 0);
+ }
+ }
+}
+
+static void regularflame(int type, const vec &p, float radius, float height, int color, int density = 3, float scale = 2.0f, float speed = 200.0f, float fade = 600.0f, int gravity = -15)
+{
+ if(!canemitparticles()) return;
+
+ float size = scale * min(radius, height);
+ vec v(0, 0, min(1.0f, height)*speed);
+ loopi(density)
+ {
+ vec s = p;
+ s.x += rndscale(radius*2.0f)-radius;
+ s.y += rndscale(radius*2.0f)-radius;
+ newparticle(s, v, rnd(max(int(fade*height), 1))+1, type, color, size, gravity);
+ }
+}
+
+void regular_particle_flame(int type, const vec &p, float radius, float height, int color, int density, float scale, float speed, float fade, int gravity)
+{
+ if(!canaddparticles()) return;
+ regularflame(type, p, radius, height, color, density, scale, speed, fade, gravity);
+}
+
+static void makeparticles(entity &e)
+{
+ switch(e.attr1)
+ {
+ case 0: //fire and smoke - <radius> <height> <rgb> - 0 values default to compat for old maps
+ {
+ //regularsplash(PART_FIREBALL1, 0xFFC8C8, 150, 1, 40, e.o, 4.8f);
+ //regularsplash(PART_SMOKE, 0x897661, 50, 1, 200, vec(e.o.x, e.o.y, e.o.z+3.0f), 2.4f, -20, 3);
+ float radius = e.attr2 ? float(e.attr2)/100.0f : 1.5f,
+ height = e.attr3 ? float(e.attr3)/100.0f : radius/3;
+ regularflame(PART_FLAME, e.o, radius, height, e.attr4 ? colorfromattr(e.attr4) : 0x903020, 3, 2.0f);
+ regularflame(PART_SMOKE, vec(e.o.x, e.o.y, e.o.z + 4.0f*min(radius, height)), radius, height, 0x303020, 1, 4.0f, 100.0f, 2000.0f, -20);
+ break;
+ }
+ case 1: //steam vent - <dir>
+ regularsplash(PART_STEAM, 0x897661, 50, 1, 200, offsetvec(e.o, e.attr2, rnd(10)), 2.4f, -20);
+ break;
+ case 2: //water fountain - <dir>
+ {
+ int color;
+ if(e.attr3 > 0) color = colorfromattr(e.attr3);
+ else
+ {
+ int mat = MAT_WATER + clamp(-e.attr3, 0, 3);
+ const bvec &wfcol = getwaterfallcolor(mat);
+ color = (int(wfcol[0])<<16) | (int(wfcol[1])<<8) | int(wfcol[2]);
+ if(!color)
+ {
+ const bvec &wcol = getwatercolor(mat);
+ color = (int(wcol[0])<<16) | (int(wcol[1])<<8) | int(wcol[2]);
+ }
+ }
+ regularsplash(PART_WATER, color, 150, 4, 200, offsetvec(e.o, e.attr2, rnd(10)), 0.6f, 2);
+ break;
+ }
+ case 3: //fire ball - <size> <rgb>
+ newparticle(e.o, vec(0, 0, 1), 1, PART_EXPLOSION, colorfromattr(e.attr3), 4.0f)->val = 1+e.attr2;
+ break;
+ case 4: //tape - <dir> <length> <rgb>
+ case 7: //lightning
+ case 9: //steam
+ case 10: //water
+ case 13: //snow
+ {
+ static const int typemap[] = { PART_STREAK, -1, -1, PART_LIGHTNING, -1, PART_STEAM, PART_WATER, -1, -1, PART_SNOW };
+ static const float sizemap[] = { 0.28f, 0.0f, 0.0f, 1.0f, 0.0f, 2.4f, 0.60f, 0.0f, 0.0f, 0.5f };
+ static const int gravmap[] = { 0, 0, 0, 0, 0, -20, 2, 0, 0, 20 };
+ int type = typemap[e.attr1-4];
+ float size = sizemap[e.attr1-4];
+ int gravity = gravmap[e.attr1-4];
+ if(e.attr2 >= 256) regularshape(type, max(1+e.attr3, 1), colorfromattr(e.attr4), e.attr2-256, 5, e.attr5 > 0 ? min(int(e.attr5), 10000) : 200, e.o, size, gravity);
+ else newparticle(e.o, offsetvec(e.o, e.attr2, max(1+e.attr3, 0)), 1, type, colorfromattr(e.attr4), size, gravity);
+ break;
+ }
+ case 5: //meter, metervs - <percent> <rgb> <rgb2>
+ case 6:
+ {
+ particle *p = newparticle(e.o, vec(0, 0, 1), 1, e.attr1==5 ? PART_METER : PART_METER_VS, colorfromattr(e.attr3), 2.0f);
+ int color2 = colorfromattr(e.attr4);
+ p->color2[0] = color2>>16;
+ p->color2[1] = (color2>>8)&0xFF;
+ p->color2[2] = color2&0xFF;
+ p->progress = clamp(int(e.attr2), 0, 100);
+ break;
+ }
+ case 11: // flame <radius> <height> <rgb> - radius=100, height=100 is the classic size
+ regularflame(PART_FLAME, e.o, float(e.attr2)/100.0f, float(e.attr3)/100.0f, colorfromattr(e.attr4), 3, 2.0f);
+ break;
+ case 12: // smoke plume <radius> <height> <rgb>
+ regularflame(PART_SMOKE, e.o, float(e.attr2)/100.0f, float(e.attr3)/100.0f, colorfromattr(e.attr4), 1, 4.0f, 100.0f, 2000.0f, -20);
+ break;
+ case 32: //lens flares - plain/sparkle/sun/sparklesun <red> <green> <blue>
+ case 33:
+ case 34:
+ case 35:
+ flares.addflare(e.o, e.attr2, e.attr3, e.attr4, (e.attr1&0x02)!=0, (e.attr1&0x01)!=0);
+ break;
+ default:
+ if(!editmode)
+ {
+ defformatstring(ds, "particles %d?", e.attr1);
+ particle_textcopy(e.o, ds, PART_TEXT, 1, 0x6496FF, 2.0f);
+ }
+ break;
+ }
+}
+
+bool printparticles(extentity &e, char *buf, int len)
+{
+ switch(e.attr1)
+ {
+ case 0: case 4: case 7: case 8: case 9: case 10: case 11: case 12: case 13:
+ nformatstring(buf, len, "%s %d %d %d 0x%.3hX %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+ return true;
+ case 3:
+ nformatstring(buf, len, "%s %d %d 0x%.3hX %d %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+ return true;
+ case 5: case 6:
+ nformatstring(buf, len, "%s %d %d 0x%.3hX 0x%.3hX %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+ return true;
+ }
+ return false;
+}
+
+void seedparticles()
+{
+ renderprogress(0, "seeding particles");
+ addparticleemitters();
+ canemit = true;
+ loopv(emitters)
+ {
+ particleemitter &pe = emitters[i];
+ extentity &e = *pe.ent;
+ seedemitter = &pe;
+ for(int millis = 0; millis < seedmillis; millis += min(emitmillis, seedmillis/10))
+ makeparticles(e);
+ seedemitter = NULL;
+ pe.lastemit = -seedmillis;
+ pe.finalize();
+ }
+}
+
+void updateparticles()
+{
+ if(regenemitters) addparticleemitters();
+
+ if(minimized) { canemit = false; return; }
+
+ if(lastmillis - lastemitframe >= emitmillis)
+ {
+ canemit = true;
+ lastemitframe = lastmillis - (lastmillis%emitmillis);
+ }
+ else canemit = false;
+
+ flares.makelightflares();
+
+ if(!editmode || showparticles)
+ {
+ int emitted = 0, replayed = 0;
+ addedparticles = 0;
+ loopv(emitters)
+ {
+ particleemitter &pe = emitters[i];
+ extentity &e = *pe.ent;
+ if(e.o.dist(camera1->o) > maxparticledistance) { pe.lastemit = lastmillis; continue; }
+ if(cullparticles && pe.maxfade >= 0)
+ {
+ if(isfoggedsphere(pe.radius, pe.center)) { pe.lastcull = lastmillis; continue; }
+ if(pvsoccluded(pe.cullmin, pe.cullmax)) { pe.lastcull = lastmillis; continue; }
+ }
+ makeparticles(e);
+ emitted++;
+ if(replayparticles && pe.maxfade > 5 && pe.lastcull > pe.lastemit)
+ {
+ for(emitoffset = max(pe.lastemit + emitmillis - lastmillis, -pe.maxfade); emitoffset < 0; emitoffset += emitmillis)
+ {
+ makeparticles(e);
+ replayed++;
+ }
+ emitoffset = 0;
+ }
+ pe.lastemit = lastmillis;
+ }
+ if(dbgpcull && (canemit || replayed) && addedparticles) conoutf(CON_DEBUG, "%d emitters, %d particles", emitted, addedparticles);
+ }
+ if(editmode) // show sparkly thingies for map entities in edit mode
+ {
+ const vector<extentity *> &ents = entities::getents();
+ // note: order matters in this case as particles of the same type are drawn in the reverse order that they are added
+ loopv(entgroup)
+ {
+ entity &e = *ents[entgroup[i]];
+ particle_textcopy(e.o, entname(e), PART_TEXT, 1, 0xFF4B19, 2.0f);
+ }
+ loopv(ents)
+ {
+ entity &e = *ents[i];
+ if(e.type==ET_EMPTY) continue;
+ particle_textcopy(e.o, entname(e), PART_TEXT, 1, 0x1EC850, 2.0f);
+ regular_particle_splash(PART_EDIT, 2, 40, e.o, 0x3232FF, 0.32f*particlesize/100.0f);
+ }
+ }
+}
diff --git a/src/engine/rendersky.cpp b/src/engine/rendersky.cpp
new file mode 100644
index 0000000..32ca947
--- /dev/null
+++ b/src/engine/rendersky.cpp
@@ -0,0 +1,774 @@
+#include "engine.h"
+
+Texture *sky[6] = { 0, 0, 0, 0, 0, 0 }, *clouds[6] = { 0, 0, 0, 0, 0, 0 };
+
+void loadsky(const char *basename, Texture *texs[6])
+{
+ const char *wildcard = strchr(basename, '*');
+ loopi(6)
+ {
+ const char *side = cubemapsides[i].name;
+ string name;
+ copystring(name, makerelpath("packages", basename));
+ if(wildcard)
+ {
+ char *chop = strchr(name, '*');
+ if(chop) { *chop = '\0'; concatstring(name, side); concatstring(name, wildcard+1); }
+ texs[i] = textureload(name, 3, true, false);
+ }
+ else
+ {
+ defformatstring(ext, "_%s.jpg", side);
+ concatstring(name, ext);
+ if((texs[i] = textureload(name, 3, true, false))==notexture)
+ {
+ strcpy(name+strlen(name)-3, "png");
+ texs[i] = textureload(name, 3, true, false);
+ }
+ }
+ if(texs[i]==notexture) conoutf(CON_ERROR, "could not load side %s of sky texture %s", side, basename);
+ }
+}
+
+Texture *cloudoverlay = NULL;
+
+Texture *loadskyoverlay(const char *basename)
+{
+ const char *ext = strrchr(basename, '.');
+ string name;
+ copystring(name, makerelpath("packages", basename));
+ Texture *t = notexture;
+ if(ext) t = textureload(name, 0, true, false);
+ else
+ {
+ concatstring(name, ".jpg");
+ if((t = textureload(name, 0, true, false)) == notexture)
+ {
+ strcpy(name+strlen(name)-3, "png");
+ t = textureload(name, 0, true, false);
+ }
+ }
+ if(t==notexture) conoutf(CON_ERROR, "could not load sky overlay texture %s", basename);
+ return t;
+}
+
+SVARFR(skybox, "", { if(skybox[0]) loadsky(skybox, sky); });
+HVARR(skyboxcolour, 0, 0xFFFFFF, 0xFFFFFF);
+FVARR(spinsky, -720, 0, 720);
+VARR(yawsky, 0, 0, 360);
+SVARFR(cloudbox, "", { if(cloudbox[0]) loadsky(cloudbox, clouds); });
+HVARR(cloudboxcolour, 0, 0xFFFFFF, 0xFFFFFF);
+FVARR(cloudboxalpha, 0, 1, 1);
+FVARR(spinclouds, -720, 0, 720);
+VARR(yawclouds, 0, 0, 360);
+FVARR(cloudclip, 0, 0.5f, 1);
+SVARFR(cloudlayer, "", { if(cloudlayer[0]) cloudoverlay = loadskyoverlay(cloudlayer); });
+FVARR(cloudoffsetx, 0, 0, 1);
+FVARR(cloudoffsety, 0, 0, 1);
+FVARR(cloudscrollx, -16, 0, 16);
+FVARR(cloudscrolly, -16, 0, 16);
+FVARR(cloudscale, 0.001, 1, 64);
+FVARR(spincloudlayer, -720, 0, 720);
+VARR(yawcloudlayer, 0, 0, 360);
+FVARR(cloudheight, -1, 0.2f, 1);
+FVARR(cloudfade, 0, 0.2f, 1);
+FVARR(cloudalpha, 0, 1, 1);
+VARR(cloudsubdiv, 4, 16, 64);
+HVARR(cloudcolour, 0, 0xFFFFFF, 0xFFFFFF);
+
+void drawenvboxface(float s0, float t0, int x0, int y0, int z0,
+ float s1, float t1, int x1, int y1, int z1,
+ float s2, float t2, int x2, int y2, int z2,
+ float s3, float t3, int x3, int y3, int z3,
+ Texture *tex)
+{
+ glBindTexture(GL_TEXTURE_2D, (tex ? tex : notexture)->id);
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attribf(x3, y3, z3); gle::attribf(s3, t3);
+ gle::attribf(x2, y2, z2); gle::attribf(s2, t2);
+ gle::attribf(x0, y0, z0); gle::attribf(s0, t0);
+ gle::attribf(x1, y1, z1); gle::attribf(s1, t1);
+ xtraverts += gle::end();
+}
+
+void drawenvbox(int w, float z1clip = 0.0f, float z2clip = 1.0f, int faces = 0x3F, Texture **sky = NULL)
+{
+ if(z1clip >= z2clip) return;
+
+ float v1 = 1-z1clip, v2 = 1-z2clip;
+ int z1 = int(ceil(2*w*(z1clip-0.5f))), z2 = int(ceil(2*w*(z2clip-0.5f)));
+
+ gle::defvertex();
+ gle::deftexcoord0();
+
+ if(faces&0x01)
+ drawenvboxface(0.0f, v2, -w, -w, z2,
+ 1.0f, v2, -w, w, z2,
+ 1.0f, v1, -w, w, z1,
+ 0.0f, v1, -w, -w, z1, sky[0]);
+
+ if(faces&0x02)
+ drawenvboxface(1.0f, v1, w, -w, z1,
+ 0.0f, v1, w, w, z1,
+ 0.0f, v2, w, w, z2,
+ 1.0f, v2, w, -w, z2, sky[1]);
+
+ if(faces&0x04)
+ drawenvboxface(1.0f, v1, -w, -w, z1,
+ 0.0f, v1, w, -w, z1,
+ 0.0f, v2, w, -w, z2,
+ 1.0f, v2, -w, -w, z2, sky[2]);
+
+ if(faces&0x08)
+ drawenvboxface(1.0f, v1, w, w, z1,
+ 0.0f, v1, -w, w, z1,
+ 0.0f, v2, -w, w, z2,
+ 1.0f, v2, w, w, z2, sky[3]);
+
+ if(z1clip <= 0 && faces&0x10)
+ drawenvboxface(0.0f, 1.0f, -w, w, -w,
+ 0.0f, 0.0f, w, w, -w,
+ 1.0f, 0.0f, w, -w, -w,
+ 1.0f, 1.0f, -w, -w, -w, sky[4]);
+
+ if(z2clip >= 1 && faces&0x20)
+ drawenvboxface(0.0f, 1.0f, w, w, w,
+ 0.0f, 0.0f, -w, w, w,
+ 1.0f, 0.0f, -w, -w, w,
+ 1.0f, 1.0f, w, -w, w, sky[5]);
+}
+
+void drawenvoverlay(int w, Texture *overlay = NULL, float tx = 0, float ty = 0)
+{
+ float z = w*cloudheight, tsz = 0.5f*(1-cloudfade)/cloudscale, psz = w*(1-cloudfade);
+ glBindTexture(GL_TEXTURE_2D, overlay ? overlay->id : notexture->id);
+ vec color = vec::hexcolor(cloudcolour);
+ gle::color(color, cloudalpha);
+ gle::defvertex();
+ gle::deftexcoord0();
+ gle::begin(GL_TRIANGLE_FAN);
+ loopi(cloudsubdiv+1)
+ {
+ vec p(1, 1, 0);
+ p.rotate_around_z((-2.0f*M_PI*i)/cloudsubdiv);
+ gle::attribf(p.x*psz, p.y*psz, z);
+ gle::attribf(tx + p.x*tsz, ty + p.y*tsz);
+ }
+ xtraverts += gle::end();
+ float tsz2 = 0.5f/cloudscale;
+ gle::defvertex();
+ gle::deftexcoord0();
+ gle::defcolor(4);
+ gle::begin(GL_TRIANGLE_STRIP);
+ loopi(cloudsubdiv+1)
+ {
+ vec p(1, 1, 0);
+ p.rotate_around_z((-2.0f*M_PI*i)/cloudsubdiv);
+ gle::attribf(p.x*psz, p.y*psz, z);
+ gle::attribf(tx + p.x*tsz, ty + p.y*tsz);
+ gle::attrib(color, cloudalpha);
+ gle::attribf(p.x*w, p.y*w, z);
+ gle::attribf(tx + p.x*tsz2, ty + p.y*tsz2);
+ gle::attrib(color, 0.0f);
+ }
+ xtraverts += gle::end();
+}
+
+FVARR(fogdomeheight, -1, -0.5f, 1);
+FVARR(fogdomemin, 0, 0, 1);
+FVARR(fogdomemax, 0, 0, 1);
+VARR(fogdomecap, 0, 1, 1);
+FVARR(fogdomeclip, 0, 1, 1);
+bvec fogdomecolor(0, 0, 0);
+HVARFR(fogdomecolour, 0, 0, 0xFFFFFF,
+{
+ fogdomecolor = bvec((fogdomecolour>>16)&0xFF, (fogdomecolour>>8)&0xFF, fogdomecolour&0xFF);
+});
+VARR(fogdomeclouds, 0, 1, 1);
+
+namespace fogdome
+{
+ struct vert
+ {
+ vec pos;
+ bvec4 color;
+
+ vert() {}
+ vert(const vec &pos, const bvec &fcolor, float alpha) : pos(pos), color(fcolor, uchar(alpha*255))
+ {
+ }
+ vert(const vert &v0, const vert &v1) : pos(vec(v0.pos).add(v1.pos).normalize()), color(v0.color)
+ {
+ if(v0.pos.z != v1.pos.z) color.a += uchar((v1.color.a - v0.color.a) * (pos.z - v0.pos.z) / (v1.pos.z - v0.pos.z));
+ }
+ } *verts = NULL;
+ GLushort *indices = NULL;
+ int numverts = 0, numindices = 0, capindices = 0;
+ GLuint vbuf = 0, ebuf = 0;
+ bvec lastcolor(0, 0, 0);
+ float lastminalpha = 0, lastmaxalpha = 0, lastcapsize = -1, lastclipz = 1;
+
+ void subdivide(int depth, int face);
+
+ void genface(int depth, int i1, int i2, int i3)
+ {
+ int face = numindices; numindices += 3;
+ indices[face] = i3;
+ indices[face+1] = i2;
+ indices[face+2] = i1;
+ subdivide(depth, face);
+ }
+
+ void subdivide(int depth, int face)
+ {
+ if(depth-- <= 0) return;
+ int idx[6];
+ loopi(3) idx[i] = indices[face+2-i];
+ loopi(3)
+ {
+ int curvert = numverts++;
+ verts[curvert] = vert(verts[idx[i]], verts[idx[(i+1)%3]]); //push on to unit sphere
+ idx[3+i] = curvert;
+ indices[face+2-i] = curvert;
+ }
+ subdivide(depth, face);
+ loopi(3) genface(depth, idx[i], idx[3+i], idx[3+(i+2)%3]);
+ }
+
+ int sortcap(GLushort x, GLushort y)
+ {
+ const vec &xv = verts[x].pos, &yv = verts[y].pos;
+ return xv.y < 0 ? yv.y >= 0 || xv.x < yv.x : yv.y >= 0 && xv.x > yv.x;
+ }
+
+ void init(const bvec &color, float minalpha = 0.0f, float maxalpha = 1.0f, float capsize = -1, float clipz = 1, int hres = 16, int depth = 2)
+ {
+ const int tris = hres << (2*depth);
+ numverts = numindices = capindices = 0;
+ verts = new vert[tris+1 + (capsize >= 0 ? 1 : 0)];
+ indices = new GLushort[(tris + (capsize >= 0 ? hres<<depth : 0))*3];
+ if(clipz >= 1)
+ {
+ verts[numverts++] = vert(vec(0.0f, 0.0f, 1.0f), color, minalpha); //build initial 'hres' sided pyramid
+ loopi(hres) verts[numverts++] = vert(vec(sincos360[(360*i)/hres], 0.0f), color, maxalpha);
+ loopi(hres) genface(depth, 0, i+1, 1+(i+1)%hres);
+ }
+ else if(clipz <= 0)
+ {
+ loopi(hres<<depth) verts[numverts++] = vert(vec(sincos360[(360*i)/(hres<<depth)], 0.0f), color, maxalpha);
+ }
+ else
+ {
+ float clipxy = sqrtf(1 - clipz*clipz);
+ const vec2 &scm = sincos360[180/hres];
+ loopi(hres)
+ {
+ const vec2 &sc = sincos360[(360*i)/hres];
+ verts[numverts++] = vert(vec(sc.x*clipxy, sc.y*clipxy, clipz), color, minalpha);
+ verts[numverts++] = vert(vec(sc.x, sc.y, 0.0f), color, maxalpha);
+ verts[numverts++] = vert(vec(sc.x*scm.x - sc.y*scm.y, sc.y*scm.x + sc.x*scm.y, 0.0f), color, maxalpha);
+ }
+ loopi(hres)
+ {
+ genface(depth-1, 3*i, 3*i+1, 3*i+2);
+ genface(depth-1, 3*i, 3*i+2, 3*((i+1)%hres));
+ genface(depth-1, 3*i+2, 3*((i+1)%hres)+1, 3*((i+1)%hres));
+ }
+ }
+
+ if(capsize >= 0)
+ {
+ GLushort *cap = &indices[numindices];
+ int capverts = 0;
+ loopi(numverts) if(!verts[i].pos.z) cap[capverts++] = i;
+ verts[numverts++] = vert(vec(0.0f, 0.0f, -capsize), color, maxalpha);
+ quicksort(cap, capverts, sortcap);
+ loopi(capverts)
+ {
+ int n = capverts-1-i;
+ cap[n*3] = cap[n];
+ cap[n*3+1] = cap[(n+1)%capverts];
+ cap[n*3+2] = numverts-1;
+ capindices += 3;
+ }
+ }
+
+ if(!vbuf) glGenBuffers_(1, &vbuf);
+ gle::bindvbo(vbuf);
+ glBufferData_(GL_ARRAY_BUFFER, numverts*sizeof(vert), verts, GL_STATIC_DRAW);
+ DELETEA(verts);
+
+ if(!ebuf) glGenBuffers_(1, &ebuf);
+ gle::bindebo(ebuf);
+ glBufferData_(GL_ELEMENT_ARRAY_BUFFER, (numindices + capindices)*sizeof(GLushort), indices, GL_STATIC_DRAW);
+ DELETEA(indices);
+ }
+
+ void cleanup()
+ {
+ numverts = numindices = 0;
+ if(vbuf) { glDeleteBuffers_(1, &vbuf); vbuf = 0; }
+ if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
+ }
+
+ void draw()
+ {
+ float capsize = fogdomecap && fogdomeheight < 1 ? (1 + fogdomeheight) / (1 - fogdomeheight) : -1;
+ bvec color = fogdomecolour ? fogdomecolor : fogcolor;
+ if(!numverts || lastcolor != color || lastminalpha != fogdomemin || lastmaxalpha != fogdomemax || lastcapsize != capsize || lastclipz != fogdomeclip)
+ {
+ init(color, min(fogdomemin, fogdomemax), fogdomemax, capsize, fogdomeclip);
+ lastcolor = color;
+ lastminalpha = fogdomemin;
+ lastmaxalpha = fogdomemax;
+ lastcapsize = capsize;
+ lastclipz = fogdomeclip;
+ }
+
+ gle::bindvbo(vbuf);
+ gle::bindebo(ebuf);
+
+ gle::vertexpointer(sizeof(vert), &verts->pos);
+ gle::colorpointer(sizeof(vert), &verts->color);
+ gle::enablevertex();
+ gle::enablecolor();
+
+ glDrawRangeElements_(GL_TRIANGLES, 0, numverts-1, numindices + fogdomecap*capindices, GL_UNSIGNED_SHORT, indices);
+ xtraverts += numverts;
+ glde++;
+
+ gle::disablevertex();
+ gle::disablecolor();
+
+ gle::clearvbo();
+ gle::clearebo();
+ }
+}
+
+static void drawfogdome(int farplane)
+{
+ SETSHADER(skyfog);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ matrix4 skymatrix = cammatrix, skyprojmatrix;
+ skymatrix.settranslation(vec(cammatrix.c).mul(farplane*fogdomeheight*0.5f));
+ skymatrix.scale(farplane/2, farplane/2, farplane*(0.5f - fogdomeheight*0.5f));
+ skyprojmatrix.mul(projmatrix, skymatrix);
+ LOCALPARAM(skymatrix, skyprojmatrix);
+
+ fogdome::draw();
+
+ glDisable(GL_BLEND);
+}
+
+void cleanupsky()
+{
+ fogdome::cleanup();
+}
+
+extern int atmo;
+
+void preloadatmoshaders(bool force = false)
+{
+ static bool needatmo = false;
+ if(force) needatmo = true;
+ if(!atmo || !needatmo) return;
+
+ useshaderbyname("atmosphere");
+ useshaderbyname("atmosphereglare");
+}
+
+void setupsky()
+{
+ preloadatmoshaders(true);
+}
+
+VARFR(atmo, 0, 0, 1, preloadatmoshaders());
+FVARR(atmoplanetsize, 1e-3f, 1, 1e3f);
+FVARR(atmoheight, 1e-3f, 1, 1e3f);
+FVARR(atmobright, 0, 1, 16);
+bvec atmosunlightcolor(0, 0, 0);
+HVARFR(atmosunlight, 0, 0, 0xFFFFFF,
+{
+ if(atmosunlight <= 255) atmosunlight |= (atmosunlight<<8) | (atmosunlight<<16);
+ atmosunlightcolor = bvec((atmosunlight>>16)&0xFF, (atmosunlight>>8)&0xFF, atmosunlight&0xFF);
+});
+FVARR(atmosunlightscale, 0, 1, 16);
+bvec atmosundiskcolor(0, 0, 0);
+HVARFR(atmosundisk, 0, 0, 0xFFFFFF,
+{
+ if(atmosundisk <= 255) atmosundisk |= (atmosundisk<<8) | (atmosundisk<<16);
+ atmosundiskcolor = bvec((atmosundisk>>16)&0xFF, (atmosundisk>>8)&0xFF, atmosundisk&0xFF);
+});
+FVARR(atmosundisksize, 0, 12, 90);
+FVARR(atmosundiskcorona, 0, 0.4f, 1);
+FVARR(atmosundiskbright, 0, 1, 16);
+FVARR(atmohaze, 0, 0.1f, 16);
+FVARR(atmodensity, 0, 1, 16);
+FVARR(atmoozone, 0, 1, 16);
+FVARR(atmoalpha, 0, 1, 1);
+
+static void drawatmosphere(int w, float z1clip = 0.0f, float z2clip = 1.0f, int faces = 0x3F)
+{
+ if(z1clip >= z2clip) return;
+
+ if(glaring) SETSHADER(atmosphereglare);
+ else SETSHADER(atmosphere);
+
+ matrix4 skymatrix = cammatrix, skyprojmatrix;
+ skymatrix.settranslation(0, 0, 0);
+ skyprojmatrix.mul(projmatrix, skymatrix);
+ LOCALPARAM(skymatrix, skyprojmatrix);
+
+ // optical depth scales for 3 different shells of atmosphere - air, haze, ozone
+ const float earthradius = 6371e3f, earthairheight = 8.4e3f, earthhazeheight = 1.25e3f, earthozoneheight = 50e3f;
+ float planetradius = earthradius*atmoplanetsize;
+ vec atmoshells = vec(earthairheight, earthhazeheight, earthozoneheight).mul(atmoheight).add(planetradius).square().sub(planetradius*planetradius);
+ LOCALPARAM(opticaldepthparams, vec4(atmoshells, planetradius));
+
+ // Henyey-Greenstein approximation, 1/(4pi) * (1 - g^2)/(1 + g^2 - 2gcos)]^1.5
+ // Hoffman-Preetham variation uses (1-g)^2 instead of 1-g^2 which avoids excessive glare
+ // clamp values near 0 angle to avoid spotlight artifact inside sundisk
+ float gm = max(0.95f - 0.2f*atmohaze, 0.65f), miescale = pow((1-gm)*(1-gm)/(4*M_PI), -2.0f/3.0f);
+ LOCALPARAMF(mieparams, miescale*(1 + gm*gm), miescale*-2*gm, 1 - (1 - cosf(0.5f*atmosundisksize*(1 - atmosundiskcorona)*RAD)));
+
+ static const vec lambda(680e-9f, 550e-9f, 450e-9f),
+ k(0.686f, 0.678f, 0.666f),
+ ozone(3.426f, 8.298f, 0.356f);
+ vec betar = vec(lambda).square().square().recip().mul(1.241e-30f/M_LN2 * atmodensity),
+ betam = vec(lambda).recip().square().mul(k).mul(9.072e-17f/M_LN2 * atmohaze),
+ betao = vec(ozone).mul(1.5e-7f/M_LN2 * atmoozone);
+ LOCALPARAM(betarayleigh, betar);
+ LOCALPARAM(betamie, betam);
+ LOCALPARAM(betaozone, betao);
+
+ // extinction in direction of sun
+ float sunoffset = sunlightdir.z*planetradius;
+ vec sundepth = vec(atmoshells).add(sunoffset*sunoffset).sqrt().sub(sunoffset);
+ vec sunweight = vec(betar).mul(sundepth.x).madd(betam, sundepth.y).madd(betao, sundepth.z - sundepth.x);
+ vec sunextinction = vec(sunweight).neg().exp2();
+ vec suncolor = atmosunlight ? atmosunlightcolor.tocolor().mul(atmosunlightscale) : sunlightcolor.tocolor().mul(sunlightscale);
+ // assume sunlight color is gamma encoded, so decode to linear light, then apply extinction
+ vec sunscale = vec(suncolor).square().mul(atmobright * 16).mul(sunextinction);
+ float maxsunweight = max(max(sunweight.x, sunweight.y), sunweight.z);
+ if(maxsunweight > 127) sunweight.mul(127/maxsunweight);
+ sunweight.add(1e-4f);
+ LOCALPARAM(sunweight, sunweight);
+ LOCALPARAM(sunlight, vec4(sunscale, atmoalpha));
+ LOCALPARAM(sundir, sunlightdir);
+
+ // invert extinction at zenith to get an approximation of how bright the sun disk should be
+ vec zenithdepth = vec(atmoshells).add(planetradius*planetradius).sqrt().sub(planetradius);
+ vec zenithweight = vec(betar).mul(zenithdepth.x).madd(betam, zenithdepth.y).madd(betao, zenithdepth.z - zenithdepth.x);
+ vec zenithextinction = vec(zenithweight).sub(sunweight).exp2();
+ vec diskcolor = (atmosundisk ? atmosundiskcolor.tocolor() : suncolor).square().mul(zenithextinction).mul(atmosundiskbright * (glaring ? 1 : 1.5f)).min(1);
+ LOCALPARAM(sundiskcolor, diskcolor);
+
+ // convert from view cosine into mu^2 for limb darkening, where mu = sqrt(1 - sin^2) and sin^2 = 1 - cos^2, thus mu^2 = 1 - (1 - cos^2*scale)
+ // convert corona offset into scale for mu^2, where sin = (1-corona) and thus mu^2 = 1 - (1-corona^2)
+ float sundiskscale = sinf(0.5f*atmosundisksize*RAD);
+ float coronamu = 1 - (1-atmosundiskcorona)*(1-atmosundiskcorona);
+ if(sundiskscale > 0) LOCALPARAMF(sundiskparams, 1.0f/(sundiskscale*sundiskscale), 1.0f/max(coronamu, 1e-3f));
+ else LOCALPARAMF(sundiskparams, 0, 0);
+
+ float z1 = 2*w*(z1clip-0.5f), z2 = ceil(2*w*(z2clip-0.5f));
+
+ gle::defvertex();
+
+ if(glaring)
+ {
+ if(sundiskscale > 0 && sunlightdir.z*w + sundiskscale > z1 && sunlightdir.z*w - sundiskscale < z2)
+ {
+ gle::begin(GL_TRIANGLE_FAN);
+ vec spoke;
+ spoke.orthogonal(sunlightdir);
+ spoke.rescale(2*sundiskscale);
+ loopi(4) gle::attrib(vec(spoke).rotate(-2*M_PI*i/4.0f, sunlightdir).add(sunlightdir).mul(w));
+ xtraverts += gle::end();
+ }
+ return;
+ }
+
+ gle::begin(GL_QUADS);
+
+ if(faces&0x01)
+ {
+ gle::attribf(-w, -w, z1);
+ gle::attribf(-w, w, z1);
+ gle::attribf(-w, w, z2);
+ gle::attribf(-w, -w, z2);
+ }
+
+ if(faces&0x02)
+ {
+ gle::attribf(w, -w, z2);
+ gle::attribf(w, w, z2);
+ gle::attribf(w, w, z1);
+ gle::attribf(w, -w, z1);
+ }
+
+ if(faces&0x04)
+ {
+ gle::attribf(-w, -w, z2);
+ gle::attribf( w, -w, z2);
+ gle::attribf( w, -w, z1);
+ gle::attribf(-w, -w, z1);
+ }
+
+ if(faces&0x08)
+ {
+ gle::attribf( w, w, z2);
+ gle::attribf(-w, w, z2);
+ gle::attribf(-w, w, z1);
+ gle::attribf( w, w, z1);
+ }
+
+ if(z1clip <= 0 && faces&0x10)
+ {
+ gle::attribf(-w, -w, -w);
+ gle::attribf( w, -w, -w);
+ gle::attribf( w, w, -w);
+ gle::attribf(-w, w, -w);
+ }
+
+ if(z2clip >= 1 && faces&0x20)
+ {
+ gle::attribf( w, -w, w);
+ gle::attribf(-w, -w, w);
+ gle::attribf(-w, w, w);
+ gle::attribf( w, w, w);
+ }
+
+ xtraverts += gle::end();
+}
+
+VARP(sparklyfix, 0, 0, 1);
+VAR(showsky, 0, 1, 1);
+VAR(clipsky, 0, 1, 1);
+
+bool drawskylimits(bool explicitonly)
+{
+ nocolorshader->set();
+
+ glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+ bool rendered = rendersky(explicitonly);
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+ return rendered;
+}
+
+void drawskyoutline()
+{
+ notextureshader->set();
+
+ glDepthMask(GL_FALSE);
+ extern int wireframe;
+ if(!wireframe)
+ {
+ enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+ }
+ gle::colorf(0.5f, 0.0f, 0.5f);
+ rendersky(true);
+ if(!wireframe)
+ {
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+ disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+ }
+ glDepthMask(GL_TRUE);
+}
+
+VAR(clampsky, 0, 1, 1);
+
+static int yawskyfaces(int faces, int yaw, float spin = 0)
+{
+ if(spin || yaw%90) return faces&0x0F ? faces | 0x0F : faces;
+ static const int faceidxs[3][4] =
+ {
+ { 3, 2, 0, 1 },
+ { 1, 0, 3, 2 },
+ { 2, 3, 1, 0 }
+ };
+ yaw /= 90;
+ if(yaw < 1 || yaw > 3) return faces;
+ const int *idxs = faceidxs[yaw - 1];
+ return (faces & ~0x0F) | (((faces>>idxs[0])&1)<<0) | (((faces>>idxs[1])&1)<<1) | (((faces>>idxs[2])&1)<<2) | (((faces>>idxs[3])&1)<<3);
+}
+
+void drawskybox(int farplane, bool limited, bool force)
+{
+ extern int renderedskyfaces, renderedskyclip; // , renderedsky, renderedexplicitsky;
+ bool alwaysrender = editmode || !insideworld(camera1->o) || reflecting || force,
+ explicitonly = false;
+ if(limited)
+ {
+ explicitonly = alwaysrender || !sparklyfix || refracting;
+ if(!drawskylimits(explicitonly) && !alwaysrender) return;
+ extern int ati_skybox_bug;
+ if(!alwaysrender && !renderedskyfaces && !ati_skybox_bug) explicitonly = false;
+ }
+ else if(!alwaysrender)
+ {
+ renderedskyfaces = 0;
+ renderedskyclip = INT_MAX;
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(va->occluded >= OCCLUDE_BB && va->skyfaces&0x80) continue;
+ renderedskyfaces |= va->skyfaces&0x3F;
+ if(!(va->skyfaces&0x1F) || camera1->o.z < va->skyclip) renderedskyclip = min(renderedskyclip, va->skyclip);
+ else renderedskyclip = 0;
+ }
+ if(!renderedskyfaces) return;
+ }
+
+ if(alwaysrender)
+ {
+ renderedskyfaces = 0x3F;
+ renderedskyclip = 0;
+ }
+
+ float skyclip = clipsky ? max(renderedskyclip-1, 0) : 0, topclip = 1;
+ if(reflectz<worldsize)
+ {
+ if(refracting<0) topclip = 0.5f + 0.5f*(reflectz-camera1->o.z)/float(worldsize);
+ else if(reflectz>skyclip) skyclip = reflectz;
+ }
+ if(skyclip) skyclip = 0.5f + 0.5f*(skyclip-camera1->o.z)/float(worldsize);
+
+ if(limited)
+ {
+ if(explicitonly) glDisable(GL_DEPTH_TEST);
+ else glDepthFunc(GL_GEQUAL);
+ }
+ else glDepthFunc(GL_LEQUAL);
+
+ glDepthMask(GL_FALSE);
+
+ if(clampsky) glDepthRange(1, 1);
+
+ if(!atmo || (skybox[0] && atmoalpha < 1))
+ {
+ if(glaring) SETSHADER(skyboxglare);
+ else SETSHADER(skybox);
+
+ gle::color(vec::hexcolor(skyboxcolour));
+
+ matrix4 skymatrix = cammatrix, skyprojmatrix;
+ skymatrix.settranslation(0, 0, 0);
+ skymatrix.rotate_around_z((spinsky*lastmillis/1000.0f+yawsky)*-RAD);
+ skyprojmatrix.mul(projmatrix, skymatrix);
+ LOCALPARAM(skymatrix, skyprojmatrix);
+
+ drawenvbox(farplane/2, skyclip, topclip, yawskyfaces(renderedskyfaces, yawsky, spinsky), sky);
+ }
+
+ if(atmo)
+ {
+ if(atmoalpha < 1)
+ {
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+
+ drawatmosphere(farplane/2, skyclip, topclip, renderedskyfaces);
+
+ if(atmoalpha < 1) glDisable(GL_BLEND);
+ }
+
+ if(!glaring)
+ {
+ if(fogdomemax && !fogdomeclouds)
+ {
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+ drawfogdome(farplane);
+ }
+
+ if(cloudbox[0])
+ {
+ SETSHADER(skybox);
+
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ gle::color(vec::hexcolor(cloudboxcolour), cloudboxalpha);
+
+ matrix4 skymatrix = cammatrix, skyprojmatrix;
+ skymatrix.settranslation(0, 0, 0);
+ skymatrix.rotate_around_z((spinclouds*lastmillis/1000.0f+yawclouds)*-RAD);
+ skyprojmatrix.mul(projmatrix, skymatrix);
+ LOCALPARAM(skymatrix, skyprojmatrix);
+
+ drawenvbox(farplane/2, skyclip ? skyclip : cloudclip, topclip, yawskyfaces(renderedskyfaces, yawclouds, spinclouds), clouds);
+
+ glDisable(GL_BLEND);
+ }
+
+ if(cloudlayer[0] && cloudheight && renderedskyfaces&(cloudheight < 0 ? 0x1F : 0x2F))
+ {
+ SETSHADER(skybox);
+
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+ glDisable(GL_CULL_FACE);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ matrix4 skymatrix = cammatrix, skyprojmatrix;
+ skymatrix.settranslation(0, 0, 0);
+ skymatrix.rotate_around_z((spincloudlayer*lastmillis/1000.0f+yawcloudlayer)*-RAD);
+ skyprojmatrix.mul(projmatrix, skymatrix);
+ LOCALPARAM(skymatrix, skyprojmatrix);
+
+ drawenvoverlay(farplane/2, cloudoverlay, cloudoffsetx + cloudscrollx * lastmillis/1000.0f, cloudoffsety + cloudscrolly * lastmillis/1000.0f);
+
+ glDisable(GL_BLEND);
+
+ glEnable(GL_CULL_FACE);
+ }
+
+ if(fogdomemax && fogdomeclouds)
+ {
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+ drawfogdome(farplane);
+ }
+ }
+
+ if(clampsky) glDepthRange(0, 1);
+
+ glDepthMask(GL_TRUE);
+
+ if(limited)
+ {
+ if(explicitonly) glEnable(GL_DEPTH_TEST);
+ else glDepthFunc(GL_LESS);
+ if(!reflecting && !refracting && !drawtex && editmode && showsky) drawskyoutline();
+ }
+ else glDepthFunc(GL_LESS);
+}
+
+VARNR(skytexture, useskytexture, 0, 1, 1);
+
+int explicitsky = 0;
+double skyarea = 0;
+
+bool limitsky()
+{
+ return (explicitsky && (useskytexture || editmode)) || (sparklyfix && skyarea / (double(worldsize)*double(worldsize)*6) < 0.9);
+}
+
+bool shouldrenderskyenvmap()
+{
+ return atmo != 0;
+}
+
+bool shouldclearskyboxglare()
+{
+ return atmo && (!skybox[0] || atmoalpha >= 1);
+}
+
diff --git a/src/engine/rendertarget.h b/src/engine/rendertarget.h
new file mode 100644
index 0000000..3c93f18
--- /dev/null
+++ b/src/engine/rendertarget.h
@@ -0,0 +1,464 @@
+extern int rtsharefb, rtscissor, blurtile;
+
+struct rendertarget
+{
+ int texw, texh, vieww, viewh;
+ GLenum colorfmt, depthfmt;
+ GLuint rendertex, renderfb, renderdb, blurtex, blurfb, blurdb;
+ int blursize, blurysize;
+ float blursigma;
+ float blurweights[MAXBLURRADIUS+1], bluroffsets[MAXBLURRADIUS+1];
+ float bluryweights[MAXBLURRADIUS+1], bluryoffsets[MAXBLURRADIUS+1];
+
+ float scissorx1, scissory1, scissorx2, scissory2;
+#define BLURTILES 32
+#define BLURTILEMASK (0xFFFFFFFFU>>(32-BLURTILES))
+ uint blurtiles[BLURTILES+1];
+
+ bool initialized;
+
+ rendertarget() : texw(0), texh(0), vieww(0), viewh(0), colorfmt(GL_FALSE), depthfmt(GL_FALSE), rendertex(0), renderfb(0), renderdb(0), blurtex(0), blurfb(0), blurdb(0), blursize(0), blurysize(0), blursigma(0), initialized(false)
+ {
+ }
+
+ virtual ~rendertarget() {}
+
+ virtual GLenum attachment() const
+ {
+ return GL_COLOR_ATTACHMENT0;
+ }
+
+ virtual const GLenum *colorformats() const
+ {
+ static const GLenum colorfmts[] = { GL_RGB, GL_RGB8, GL_FALSE };
+ return colorfmts;
+ }
+
+ virtual const GLenum *depthformats() const
+ {
+ static const GLenum depthfmts[] = { GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT32, GL_FALSE };
+ return depthfmts;
+ }
+
+ virtual bool depthtest() const { return true; }
+
+ void cleanup(bool fullclean = false)
+ {
+ if(renderfb) { glDeleteFramebuffers_(1, &renderfb); renderfb = 0; }
+ if(renderdb) { glDeleteRenderbuffers_(1, &renderdb); renderdb = 0; }
+ if(rendertex) { glDeleteTextures(1, &rendertex); rendertex = 0; }
+ texw = texh = 0;
+ cleanupblur();
+
+ if(fullclean) colorfmt = depthfmt = GL_FALSE;
+ }
+
+ void cleanupblur()
+ {
+ if(blurfb) { glDeleteFramebuffers_(1, &blurfb); blurfb = 0; }
+ if(blurtex) { glDeleteTextures(1, &blurtex); blurtex = 0; }
+ if(blurdb) { glDeleteRenderbuffers_(1, &blurdb); blurdb = 0; }
+ blursize = blurysize = 0;
+ blursigma = 0.0f;
+ }
+
+ void setupblur()
+ {
+ if(!blurtex) glGenTextures(1, &blurtex);
+ createtexture(blurtex, texw, texh, NULL, 3, 1, colorfmt);
+
+ if(!swaptexs() || rtsharefb) return;
+ if(!blurfb) glGenFramebuffers_(1, &blurfb);
+ glBindFramebuffer_(GL_FRAMEBUFFER, blurfb);
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, blurtex, 0);
+ if(depthtest())
+ {
+ if(!blurdb) glGenRenderbuffers_(1, &blurdb);
+ glGenRenderbuffers_(1, &blurdb);
+ glBindRenderbuffer_(GL_RENDERBUFFER, blurdb);
+ glRenderbufferStorage_(GL_RENDERBUFFER, depthfmt, texw, texh);
+ glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, blurdb);
+ }
+ glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+ }
+
+ void setup(int w, int h)
+ {
+ if(!renderfb) glGenFramebuffers_(1, &renderfb);
+ glBindFramebuffer_(GL_FRAMEBUFFER, renderfb);
+ if(!rendertex) glGenTextures(1, &rendertex);
+
+ GLenum attach = attachment();
+ if(attach == GL_DEPTH_ATTACHMENT)
+ {
+ glDrawBuffer(GL_NONE);
+ glReadBuffer(GL_NONE);
+ }
+
+ const GLenum *colorfmts = colorformats();
+ int find = 0;
+ do
+ {
+ createtexture(rendertex, w, h, NULL, 3, filter() ? 1 : 0, colorfmt ? colorfmt : colorfmts[find]);
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, attach, GL_TEXTURE_2D, rendertex, 0);
+ if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break;
+ }
+ while(!colorfmt && colorfmts[++find]);
+ if(!colorfmt) colorfmt = colorfmts[find];
+
+ if(attach != GL_DEPTH_ATTACHMENT && depthtest())
+ {
+ if(!renderdb) { glGenRenderbuffers_(1, &renderdb); depthfmt = GL_FALSE; }
+ if(!depthfmt) glBindRenderbuffer_(GL_RENDERBUFFER, renderdb);
+ const GLenum *depthfmts = depthformats();
+ find = 0;
+ do
+ {
+ if(!depthfmt) glRenderbufferStorage_(GL_RENDERBUFFER, depthfmts[find], w, h);
+ glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderdb);
+ if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break;
+ }
+ while(!depthfmt && depthfmts[++find]);
+ if(!depthfmt) depthfmt = depthfmts[find];
+ }
+
+ glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+
+ texw = w;
+ texh = h;
+ initialized = false;
+ }
+
+ bool addblurtiles(float x1, float y1, float x2, float y2, float blurmargin = 0)
+ {
+ if(x1 >= 1 || y1 >= 1 || x2 <= -1 || y2 <= -1) return false;
+
+ scissorx1 = min(scissorx1, max(x1, -1.0f));
+ scissory1 = min(scissory1, max(y1, -1.0f));
+ scissorx2 = max(scissorx2, min(x2, 1.0f));
+ scissory2 = max(scissory2, min(y2, 1.0f));
+
+ float blurerror = 2.0f*float(2*blursize + blurmargin);
+ int tx1 = max(0, min(BLURTILES - 1, int((x1-blurerror/vieww + 1)/2 * BLURTILES))),
+ ty1 = max(0, min(BLURTILES - 1, int((y1-blurerror/viewh + 1)/2 * BLURTILES))),
+ tx2 = max(0, min(BLURTILES - 1, int((x2+blurerror/vieww + 1)/2 * BLURTILES))),
+ ty2 = max(0, min(BLURTILES - 1, int((y2+blurerror/viewh + 1)/2 * BLURTILES)));
+
+ uint mask = (BLURTILEMASK>>(BLURTILES - (tx2+1))) & (BLURTILEMASK<<tx1);
+ for(int y = ty1; y <= ty2; y++) blurtiles[y] |= mask;
+ return true;
+ }
+
+ bool checkblurtiles(float x1, float y1, float x2, float y2, float blurmargin = 0)
+ {
+ float blurerror = 2.0f*float(2*blursize + blurmargin);
+ if(x2+blurerror/vieww < scissorx1 || y2+blurerror/viewh < scissory1 ||
+ x1-blurerror/vieww > scissorx2 || y1-blurerror/viewh > scissory2)
+ return false;
+
+ if(!blurtile) return true;
+
+ int tx1 = max(0, min(BLURTILES - 1, int((x1 + 1)/2 * BLURTILES))),
+ ty1 = max(0, min(BLURTILES - 1, int((y1 + 1)/2 * BLURTILES))),
+ tx2 = max(0, min(BLURTILES - 1, int((x2 + 1)/2 * BLURTILES))),
+ ty2 = max(0, min(BLURTILES - 1, int((y2 + 1)/2 * BLURTILES)));
+
+ uint mask = (BLURTILEMASK>>(BLURTILES - (tx2+1))) & (BLURTILEMASK<<tx1);
+ for(int y = ty1; y <= ty2; y++) if(blurtiles[y] & mask) return true;
+
+ return false;
+ }
+
+ void rendertiles()
+ {
+ float wscale = vieww/float(texw), hscale = viewh/float(texh);
+ if(blurtile && scissorx1 < scissorx2 && scissory1 < scissory2)
+ {
+ uint tiles[sizeof(blurtiles)/sizeof(uint)];
+ memcpy(tiles, blurtiles, sizeof(blurtiles));
+
+ LOCALPARAMF(screentexcoord0, wscale*0.5f, hscale*0.5f, wscale*0.5f, hscale*0.5f);
+ gle::defvertex(2);
+ gle::begin(GL_QUADS);
+ float tsz = 1.0f/BLURTILES;
+ loop(y, BLURTILES+1)
+ {
+ uint mask = tiles[y];
+ int x = 0;
+ while(mask)
+ {
+ while(!(mask&0xFF)) { mask >>= 8; x += 8; }
+ while(!(mask&1)) { mask >>= 1; x++; }
+ int xstart = x;
+ do { mask >>= 1; x++; } while(mask&1);
+ uint strip = (BLURTILEMASK>>(BLURTILES - x)) & (BLURTILEMASK<<xstart);
+ int yend = y;
+ do { tiles[yend] &= ~strip; yend++; } while((tiles[yend] & strip) == strip);
+ float tx = xstart*tsz,
+ ty = y*tsz,
+ tw = (x-xstart)*tsz,
+ th = (yend-y)*tsz,
+ vx = 2*tx - 1, vy = 2*ty - 1, vw = tw*2, vh = th*2;
+ gle::attribf(vx, vy);
+ gle::attribf(vx+vw, vy);
+ gle::attribf(vx+vw, vy+vh);
+ gle::attribf(vx, vy+vh);
+ }
+ }
+ gle::end();
+ }
+ else
+ {
+ screenquad(wscale, hscale);
+ }
+ }
+
+ void blur(int wantsblursize, float wantsblursigma, int wantsblurysize, int x, int y, int w, int h, bool scissor)
+ {
+ if(!blurtex) setupblur();
+ if(blursize!=wantsblursize || blurysize != wantsblurysize || (wantsblursize && blursigma!=wantsblursigma))
+ {
+ setupblurkernel(wantsblursize, wantsblursigma, blurweights, bluroffsets);
+ if(wantsblurysize != wantsblursize) setupblurkernel(wantsblurysize, wantsblursigma, bluryweights, bluryoffsets);
+ blursize = wantsblursize;
+ blursigma = wantsblursigma;
+ blurysize = wantsblurysize;
+ }
+
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_CULL_FACE);
+
+ if(scissor)
+ {
+ glScissor(x, y, w, h);
+ glEnable(GL_SCISSOR_TEST);
+ }
+
+ loopi(2)
+ {
+ if(i && blurysize != blursize) setblurshader(i, texh, blurysize, bluryweights, bluryoffsets);
+ else setblurshader(i, i ? texh : texw, blursize, blurweights, bluroffsets);
+
+ if(!swaptexs() || rtsharefb) glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, i ? rendertex : blurtex, 0);
+ else glBindFramebuffer_(GL_FRAMEBUFFER, i ? renderfb : blurfb);
+ glBindTexture(GL_TEXTURE_2D, i ? blurtex : rendertex);
+
+ rendertiles();
+ }
+
+ if(scissor) glDisable(GL_SCISSOR_TEST);
+
+ glEnable(GL_DEPTH_TEST);
+ glEnable(GL_CULL_FACE);
+ }
+
+ virtual bool swaptexs() const { return false; }
+
+ virtual bool dorender() { return true; }
+
+ virtual bool shouldrender() { return true; }
+
+ virtual void doblur(int blursize, float blursigma, int blurysize)
+ {
+ int sx, sy, sw, sh;
+ bool scissoring = rtscissor && scissorblur(sx, sy, sw, sh) && sw > 0 && sh > 0;
+ if(!scissoring) { sx = sy = 0; sw = vieww; sh = viewh; }
+ blur(blursize, blursigma, blurysize, sx, sy, sw, sh, scissoring);
+ }
+
+ virtual bool scissorrender(int &x, int &y, int &w, int &h)
+ {
+ if(scissorx1 >= scissorx2 || scissory1 >= scissory2)
+ {
+ if(vieww < texw || viewh < texh)
+ {
+ x = y = 0;
+ w = vieww;
+ h = viewh;
+ return true;
+ }
+ return false;
+ }
+ x = max(int(floor((scissorx1+1)/2*vieww)) - 2*blursize, 0);
+ y = max(int(floor((scissory1+1)/2*viewh)) - 2*blursize, 0);
+ w = min(int(ceil((scissorx2+1)/2*vieww)) + 2*blursize, vieww) - x;
+ h = min(int(ceil((scissory2+1)/2*viewh)) + 2*blursize, viewh) - y;
+ return true;
+ }
+
+ virtual bool scissorblur(int &x, int &y, int &w, int &h)
+ {
+ if(scissorx1 >= scissorx2 || scissory1 >= scissory2)
+ {
+ if(vieww < texw || viewh < texh)
+ {
+ x = y = 0;
+ w = vieww;
+ h = viewh;
+ return true;
+ }
+ return false;
+ }
+ x = max(int(floor((scissorx1+1)/2*vieww)), 0);
+ y = max(int(floor((scissory1+1)/2*viewh)), 0);
+ w = min(int(ceil((scissorx2+1)/2*vieww)), vieww) - x;
+ h = min(int(ceil((scissory2+1)/2*viewh)), viewh) - y;
+ return true;
+ }
+
+ virtual void doclear() {}
+
+ virtual bool screenrect() const { return false; }
+ virtual bool filter() const { return true; }
+
+ void render(int w, int h, int blursize = 0, float blursigma = 0, int blurysize = 0)
+ {
+ w = min(w, hwtexsize);
+ h = min(h, hwtexsize);
+ if(screenrect())
+ {
+ if(w > screenw) w = screenw;
+ if(h > screenh) h = screenh;
+ }
+ vieww = w;
+ viewh = h;
+ if(w!=texw || h!=texh || (swaptexs() && !rtsharefb ? !blurfb : blurfb)) cleanup();
+
+ if(!filter())
+ {
+ if(blurtex) cleanupblur();
+ blursize = blurysize = 0;
+ }
+
+ if(!rendertex) setup(w, h);
+
+ scissorx2 = scissory2 = -1;
+ scissorx1 = scissory1 = 1;
+ memset(blurtiles, 0, sizeof(blurtiles));
+
+ if(!shouldrender()) return;
+
+ if(blursize && !blurtex) setupblur();
+ if(swaptexs() && blursize)
+ {
+ swap(rendertex, blurtex);
+ if(!rtsharefb)
+ {
+ swap(renderfb, blurfb);
+ swap(renderdb, blurdb);
+ }
+ }
+ glBindFramebuffer_(GL_FRAMEBUFFER, renderfb);
+ if(swaptexs() && blursize && rtsharefb)
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, attachment(), GL_TEXTURE_2D, rendertex, 0);
+ glViewport(0, 0, vieww, viewh);
+
+ doclear();
+
+ int sx, sy, sw, sh;
+ bool scissoring = rtscissor && scissorrender(sx, sy, sw, sh) && sw > 0 && sh > 0;
+ if(scissoring)
+ {
+ glScissor(sx, sy, sw, sh);
+ glEnable(GL_SCISSOR_TEST);
+ }
+ else
+ {
+ sx = sy = 0;
+ sw = vieww;
+ sh = viewh;
+ }
+
+ if(!depthtest()) glDisable(GL_DEPTH_TEST);
+
+ bool succeeded = dorender();
+
+ if(!depthtest()) glEnable(GL_DEPTH_TEST);
+
+ if(scissoring) glDisable(GL_SCISSOR_TEST);
+
+ if(succeeded)
+ {
+ initialized = true;
+
+ if(blursize) doblur(blursize, blursigma, blurysize ? blurysize : blursize);
+ }
+
+ glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+ glViewport(0, 0, screenw, screenh);
+ }
+
+ virtual void dodebug(int w, int h) {}
+ virtual bool flipdebug() const { return true; }
+
+ void debugscissor(int w, int h, bool lines = false)
+ {
+ if(!rtscissor || scissorx1 >= scissorx2 || scissory1 >= scissory2) return;
+ int sx = int(0.5f*(scissorx1 + 1)*w),
+ sy = int(0.5f*(scissory1 + 1)*h),
+ sw = int(0.5f*(scissorx2 - scissorx1)*w),
+ sh = int(0.5f*(scissory2 - scissory1)*h);
+ if(flipdebug()) { sy = h - sy; sh = -sh; }
+ gle::defvertex(2);
+ gle::begin(lines ? GL_LINE_LOOP : GL_TRIANGLE_STRIP);
+ gle::attribf(sx, sy);
+ gle::attribf(sx + sw, sy);
+ if(lines) gle::attribf(sx + sw, sy + sh);
+ gle::attribf(sx, sy + sh);
+ if(!lines) gle::attribf(sx + sw, sy + sh);
+ gle::end();
+ }
+
+ void debugblurtiles(int w, int h, bool lines = false)
+ {
+ if(!blurtile) return;
+ float vxsz = float(w)/BLURTILES, vysz = float(h)/BLURTILES;
+ gle::defvertex(2);
+ loop(y, BLURTILES+1)
+ {
+ uint mask = blurtiles[y];
+ int x = 0;
+ while(mask)
+ {
+ while(!(mask&0xFF)) { mask >>= 8; x += 8; }
+ while(!(mask&1)) { mask >>= 1; x++; }
+ int xstart = x;
+ do { mask >>= 1; x++; } while(mask&1);
+ uint strip = (BLURTILEMASK>>(BLURTILES - x)) & (BLURTILEMASK<<xstart);
+ int yend = y;
+ do { blurtiles[yend] &= ~strip; yend++; } while((blurtiles[yend] & strip) == strip);
+ float vx = xstart*vxsz,
+ vy = y*vysz,
+ vw = (x-xstart)*vxsz,
+ vh = (yend-y)*vysz;
+ if(flipdebug()) { vy = h - vy; vh = -vh; }
+ loopi(lines ? 1 : 2)
+ {
+ if(!lines) gle::colorf(1, 1, i ? 1.0f : 0.5f);
+ gle::begin(lines || i ? GL_LINE_LOOP : GL_TRIANGLE_STRIP);
+ gle::attribf(vx, vy);
+ gle::attribf(vx+vw, vy);
+ if(lines || i) gle::attribf(vx+vw, vy+vh);
+ gle::attribf(vx, vy+vh);
+ if(!lines && !i) gle::attribf(vx+vw, vy+vh);
+ gle::end();
+ }
+ }
+ }
+ }
+
+ void debug()
+ {
+ if(!rendertex) return;
+ int w = min(screenw, screenh)/2, h = (w*screenh)/screenw;
+ hudshader->set();
+ gle::colorf(1, 1, 1);
+ glBindTexture(GL_TEXTURE_2D, rendertex);
+ float tx1 = 0, tx2 = 1, ty1 = 0, ty2 = 1;
+ if(flipdebug()) swap(ty1, ty2);
+ hudquad(0, 0, w, h, tx1, ty1, tx2-tx1, ty2-ty1);
+ hudnotextureshader->set();
+ dodebug(w, h);
+ }
+};
+
diff --git a/src/engine/rendertext.cpp b/src/engine/rendertext.cpp
new file mode 100644
index 0000000..44ad136
--- /dev/null
+++ b/src/engine/rendertext.cpp
@@ -0,0 +1,392 @@
+#include "engine.h"
+
+static hashnameset<font> fonts;
+static font *fontdef = NULL;
+static int fontdeftex = 0;
+
+font *curfont = NULL;
+int curfonttex = 0;
+
+void newfont(char *name, char *tex, int *defaultw, int *defaulth)
+{
+ font *f = &fonts[name];
+ if(!f->name) f->name = newstring(name);
+ f->texs.shrink(0);
+ f->texs.add(textureload(tex));
+ f->chars.shrink(0);
+ f->charoffset = '!';
+ f->defaultw = *defaultw;
+ f->defaulth = *defaulth;
+ f->scale = f->defaulth;
+
+ fontdef = f;
+ fontdeftex = 0;
+}
+
+void fontoffset(char *c)
+{
+ if(!fontdef) return;
+
+ fontdef->charoffset = c[0];
+}
+
+void fontscale(int *scale)
+{
+ if(!fontdef) return;
+
+ fontdef->scale = *scale > 0 ? *scale : fontdef->defaulth;
+}
+
+void fonttex(char *s)
+{
+ if(!fontdef) return;
+
+ Texture *t = textureload(s);
+ loopv(fontdef->texs) if(fontdef->texs[i] == t) { fontdeftex = i; return; }
+ fontdeftex = fontdef->texs.length();
+ fontdef->texs.add(t);
+}
+
+void fontchar(int *x, int *y, int *w, int *h, int *offsetx, int *offsety, int *advance)
+{
+ if(!fontdef) return;
+
+ font::charinfo &c = fontdef->chars.add();
+ c.x = *x;
+ c.y = *y;
+ c.w = *w ? *w : fontdef->defaultw;
+ c.h = *h ? *h : fontdef->defaulth;
+ c.offsetx = *offsetx;
+ c.offsety = *offsety;
+ c.advance = *advance ? *advance : c.offsetx + c.w;
+ c.tex = fontdeftex;
+}
+
+void fontskip(int *n)
+{
+ if(!fontdef) return;
+ loopi(max(*n, 1))
+ {
+ font::charinfo &c = fontdef->chars.add();
+ c.x = c.y = c.w = c.h = c.offsetx = c.offsety = c.advance = c.tex = 0;
+ }
+}
+
+COMMANDN(font, newfont, "ssii");
+COMMAND(fontoffset, "s");
+COMMAND(fontscale, "i");
+COMMAND(fonttex, "s");
+COMMAND(fontchar, "iiiiiii");
+COMMAND(fontskip, "i");
+
+void fontalias(const char *dst, const char *src)
+{
+ font *s = fonts.access(src);
+ if(!s) return;
+ font *d = &fonts[dst];
+ if(!d->name) d->name = newstring(dst);
+ d->texs = s->texs;
+ d->chars = s->chars;
+ d->charoffset = s->charoffset;
+ d->defaultw = s->defaultw;
+ d->defaulth = s->defaulth;
+ d->scale = s->scale;
+
+ fontdef = d;
+ fontdeftex = d->texs.length()-1;
+}
+
+COMMAND(fontalias, "ss");
+
+bool setfont(const char *name)
+{
+ font *f = fonts.access(name);
+ if(!f) return false;
+ curfont = f;
+ return true;
+}
+
+static vector<font *> fontstack;
+
+void pushfont()
+{
+ fontstack.add(curfont);
+}
+
+bool popfont()
+{
+ if(fontstack.empty()) return false;
+ curfont = fontstack.pop();
+ return true;
+}
+
+void gettextres(int &w, int &h)
+{
+ if(w < MINRESW || h < MINRESH)
+ {
+ if(MINRESW > w*MINRESH/h)
+ {
+ h = h*MINRESW/w;
+ w = MINRESW;
+ }
+ else
+ {
+ w = w*MINRESH/h;
+ h = MINRESH;
+ }
+ }
+}
+
+float text_widthf(const char *str)
+{
+ float width, height;
+ text_boundsf(str, width, height);
+ return width;
+}
+
+#define FONTTAB (4*FONTW)
+#define TEXTTAB(x) ((int((x)/FONTTAB)+1.0f)*FONTTAB)
+
+void tabify(const char *str, int *numtabs)
+{
+ int tw = max(*numtabs, 0)*FONTTAB-1, tabs = 0;
+ for(float w = text_widthf(str); w <= tw; w = TEXTTAB(w)) ++tabs;
+ int len = strlen(str);
+ char *tstr = newstring(len + tabs);
+ memcpy(tstr, str, len);
+ memset(&tstr[len], '\t', tabs);
+ tstr[len+tabs] = '\0';
+ stringret(tstr);
+}
+
+COMMAND(tabify, "si");
+
+void draw_textf(const char *fstr, int left, int top, ...)
+{
+ defvformatstring(str, top, fstr);
+ draw_text(str, left, top);
+}
+
+const matrix4x3 *textmatrix = NULL;
+
+static float draw_char(Texture *&tex, int c, float x, float y, float scale)
+{
+ font::charinfo &info = curfont->chars[c-curfont->charoffset];
+ if(tex != curfont->texs[info.tex])
+ {
+ xtraverts += gle::end();
+ tex = curfont->texs[info.tex];
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+ }
+
+ float x1 = x + scale*info.offsetx,
+ y1 = y + scale*info.offsety,
+ x2 = x + scale*(info.offsetx + info.w),
+ y2 = y + scale*(info.offsety + info.h),
+ tx1 = info.x / float(tex->xs),
+ ty1 = info.y / float(tex->ys),
+ tx2 = (info.x + info.w) / float(tex->xs),
+ ty2 = (info.y + info.h) / float(tex->ys);
+
+ if(textmatrix)
+ {
+ gle::attrib(textmatrix->transform(vec2(x1, y1))); gle::attribf(tx1, ty1);
+ gle::attrib(textmatrix->transform(vec2(x2, y1))); gle::attribf(tx2, ty1);
+ gle::attrib(textmatrix->transform(vec2(x2, y2))); gle::attribf(tx2, ty2);
+ gle::attrib(textmatrix->transform(vec2(x1, y2))); gle::attribf(tx1, ty2);
+ }
+ else
+ {
+ gle::attribf(x1, y1); gle::attribf(tx1, ty1);
+ gle::attribf(x2, y1); gle::attribf(tx2, ty1);
+ gle::attribf(x2, y2); gle::attribf(tx2, ty2);
+ gle::attribf(x1, y2); gle::attribf(tx1, ty2);
+ }
+
+ return scale*info.advance;
+}
+
+//stack[sp] is current color index
+static void text_color(char c, char *stack, int size, int &sp, bvec color, int a)
+{
+ if(c=='s') // save color
+ {
+ c = stack[sp];
+ if(sp<size-1) stack[++sp] = c;
+ }
+ else
+ {
+ xtraverts += gle::end();
+ if(c=='r') { if(sp > 0) --sp; c = stack[sp]; } // restore color
+ else stack[sp] = c;
+ switch(c)
+ {
+ case '0': color = bvec( 64, 255, 128); break; // green: player talk
+ case '1': color = bvec( 96, 160, 255); break; // blue: "echo" command
+ case '2': color = bvec(255, 192, 64); break; // yellow: gameplay messages
+ case '3': color = bvec(255, 64, 64); break; // red: important errors
+ case '4': color = bvec(128, 128, 128); break; // gray
+ case '5': color = bvec(192, 64, 192); break; // magenta
+ case '6': color = bvec(255, 128, 0); break; // orange
+ case '7': color = bvec(255, 255, 255); break; // white
+ case '8': color = bvec( 96, 240, 255); break; // cyan
+ // provided color: everything else
+ }
+ gle::color(color, a);
+ }
+}
+
+#define TEXTSKELETON \
+ float y = 0, x = 0, scale = curfont->scale/float(curfont->defaulth);\
+ int i;\
+ for(i = 0; str[i]; i++)\
+ {\
+ TEXTINDEX(i)\
+ int c = uchar(str[i]);\
+ if(c=='\t') { x = TEXTTAB(x); TEXTWHITE(i) }\
+ else if(c==' ') { x += scale*curfont->defaultw; TEXTWHITE(i) }\
+ else if(c=='\n') { TEXTLINE(i) x = 0; y += FONTH; }\
+ else if(c=='\f') { if(str[i+1]) { i++; TEXTCOLOR(i) }}\
+ else if(curfont->chars.inrange(c-curfont->charoffset))\
+ {\
+ float cw = scale*curfont->chars[c-curfont->charoffset].advance;\
+ if(cw <= 0) continue;\
+ if(maxwidth != -1)\
+ {\
+ int j = i;\
+ float w = cw;\
+ for(; str[i+1]; i++)\
+ {\
+ int c = uchar(str[i+1]);\
+ if(c=='\f') { if(str[i+2]) i++; continue; }\
+ if(i-j > 16) break;\
+ if(!curfont->chars.inrange(c-curfont->charoffset)) break;\
+ float cw = scale*curfont->chars[c-curfont->charoffset].advance;\
+ if(cw <= 0 || w + cw > maxwidth) break;\
+ w += cw;\
+ }\
+ if(x + w > maxwidth && j!=0) { TEXTLINE(j-1) x = 0; y += FONTH; }\
+ TEXTWORD\
+ }\
+ else\
+ { TEXTCHAR(i) }\
+ }\
+ }
+
+//all the chars are guaranteed to be either drawable or color commands
+#define TEXTWORDSKELETON \
+ for(; j <= i; j++)\
+ {\
+ TEXTINDEX(j)\
+ int c = uchar(str[j]);\
+ if(c=='\f') { if(str[j+1]) { j++; TEXTCOLOR(j) }}\
+ else { float cw = scale*curfont->chars[c-curfont->charoffset].advance; TEXTCHAR(j) }\
+ }
+
+#define TEXTEND(cursor) if(cursor >= i) { do { TEXTINDEX(cursor); } while(0); }
+
+int text_visible(const char *str, float hitx, float hity, int maxwidth)
+{
+ #define TEXTINDEX(idx)
+ #define TEXTWHITE(idx) if(y+FONTH > hity && x >= hitx) return idx;
+ #define TEXTLINE(idx) if(y+FONTH > hity) return idx;
+ #define TEXTCOLOR(idx)
+ #define TEXTCHAR(idx) x += cw; TEXTWHITE(idx)
+ #define TEXTWORD TEXTWORDSKELETON
+ TEXTSKELETON
+ #undef TEXTINDEX
+ #undef TEXTWHITE
+ #undef TEXTLINE
+ #undef TEXTCOLOR
+ #undef TEXTCHAR
+ #undef TEXTWORD
+ return i;
+}
+
+//inverse of text_visible
+void text_posf(const char *str, int cursor, float &cx, float &cy, int maxwidth)
+{
+ #define TEXTINDEX(idx) if(idx == cursor) { cx = x; cy = y; break; }
+ #define TEXTWHITE(idx)
+ #define TEXTLINE(idx)
+ #define TEXTCOLOR(idx)
+ #define TEXTCHAR(idx) x += cw;
+ #define TEXTWORD TEXTWORDSKELETON if(i >= cursor) break;
+ cx = cy = 0;
+ TEXTSKELETON
+ TEXTEND(cursor)
+ #undef TEXTINDEX
+ #undef TEXTWHITE
+ #undef TEXTLINE
+ #undef TEXTCOLOR
+ #undef TEXTCHAR
+ #undef TEXTWORD
+}
+
+void text_boundsf(const char *str, float &width, float &height, int maxwidth)
+{
+ #define TEXTINDEX(idx)
+ #define TEXTWHITE(idx)
+ #define TEXTLINE(idx) if(x > width) width = x;
+ #define TEXTCOLOR(idx)
+ #define TEXTCHAR(idx) x += cw;
+ #define TEXTWORD x += w;
+ width = 0;
+ TEXTSKELETON
+ height = y + FONTH;
+ TEXTLINE(_)
+ #undef TEXTINDEX
+ #undef TEXTWHITE
+ #undef TEXTLINE
+ #undef TEXTCOLOR
+ #undef TEXTCHAR
+ #undef TEXTWORD
+}
+
+void draw_text(const char *str, int left, int top, int r, int g, int b, int a, int cursor, int maxwidth)
+{
+ #define TEXTINDEX(idx) if(idx == cursor) { cx = x; cy = y; }
+ #define TEXTWHITE(idx)
+ #define TEXTLINE(idx)
+ #define TEXTCOLOR(idx) if(usecolor) text_color(str[idx], colorstack, sizeof(colorstack), colorpos, color, a);
+ #define TEXTCHAR(idx) draw_char(tex, c, left+x, top+y, scale); x += cw;
+ #define TEXTWORD TEXTWORDSKELETON
+ char colorstack[10];
+ colorstack[0] = 'c'; //indicate user color
+ bvec color(r, g, b);
+ int colorpos = 0;
+ float cx = -FONTW, cy = 0;
+ bool usecolor = true;
+ if(a < 0) { usecolor = false; a = -a; }
+ Texture *tex = curfont->texs[0];
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glBindTexture(GL_TEXTURE_2D, tex->id);
+ gle::color(color, a);
+ gle::defvertex(textmatrix ? 3 : 2);
+ gle::deftexcoord0();
+ gle::begin(GL_QUADS);
+ TEXTSKELETON
+ TEXTEND(cursor)
+ xtraverts += gle::end();
+ if(cursor >= 0 && (totalmillis/250)&1)
+ {
+ gle::color(color, a);
+ if(maxwidth != -1 && cx >= maxwidth) { cx = 0; cy += FONTH; }
+ draw_char(tex, '_', left+cx, top+cy, scale);
+ xtraverts += gle::end();
+ }
+ #undef TEXTINDEX
+ #undef TEXTWHITE
+ #undef TEXTLINE
+ #undef TEXTCOLOR
+ #undef TEXTCHAR
+ #undef TEXTWORD
+}
+
+void reloadfonts()
+{
+ enumerate(fonts, font, f,
+ loopv(f.texs) if(!reloadtexture(*f.texs[i])) fatal("failed to reload font texture");
+ );
+}
+
diff --git a/src/engine/renderva.cpp b/src/engine/renderva.cpp
new file mode 100644
index 0000000..cf9d376
--- /dev/null
+++ b/src/engine/renderva.cpp
@@ -0,0 +1,1896 @@
+// renderva.cpp: handles the occlusion and rendering of vertex arrays
+
+#include "engine.h"
+
+static inline void drawtris(GLsizei numindices, const GLvoid *indices, ushort minvert, ushort maxvert)
+{
+ glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, numindices, GL_UNSIGNED_SHORT, indices);
+ glde++;
+}
+
+static inline void drawvatris(vtxarray *va, GLsizei numindices, const GLvoid *indices)
+{
+ drawtris(numindices, indices, va->minvert, va->maxvert);
+}
+
+///////// view frustrum culling ///////////////////////
+
+plane vfcP[5]; // perpindictular vectors to view frustrum bounding planes
+float vfcDfog; // far plane culling distance (fog limit).
+float vfcDnear[5], vfcDfar[5];
+
+vtxarray *visibleva;
+
+bool isfoggedsphere(float rad, const vec &cv)
+{
+ loopi(4) if(vfcP[i].dist(cv) < -rad) return true;
+ float dist = vfcP[4].dist(cv);
+ return dist < -rad || dist > vfcDfog + rad;
+}
+
+int isvisiblesphere(float rad, const vec &cv)
+{
+ int v = VFC_FULL_VISIBLE;
+ float dist;
+
+ loopi(5)
+ {
+ dist = vfcP[i].dist(cv);
+ if(dist < -rad) return VFC_NOT_VISIBLE;
+ if(dist < rad) v = VFC_PART_VISIBLE;
+ }
+
+ dist -= vfcDfog;
+ if(dist > rad) return VFC_FOGGED; //VFC_NOT_VISIBLE; // culling when fog is closer than size of world results in HOM
+ if(dist > -rad) v = VFC_PART_VISIBLE;
+
+ return v;
+}
+
+static inline int ishiddencube(const ivec &o, int size)
+{
+ loopi(5) if(o.dist(vfcP[i]) < -vfcDfar[i]*size) return true;
+ return false;
+}
+
+static inline int isfoggedcube(const ivec &o, int size)
+{
+ loopi(4) if(o.dist(vfcP[i]) < -vfcDfar[i]*size) return true;
+ float dist = o.dist(vfcP[4]);
+ return dist < -vfcDfar[4]*size || dist > vfcDfog - vfcDnear[4]*size;
+}
+
+int isvisiblecube(const ivec &o, int size)
+{
+ int v = VFC_FULL_VISIBLE;
+ float dist;
+
+ loopi(5)
+ {
+ dist = o.dist(vfcP[i]);
+ if(dist < -vfcDfar[i]*size) return VFC_NOT_VISIBLE;
+ if(dist < -vfcDnear[i]*size) v = VFC_PART_VISIBLE;
+ }
+
+ dist -= vfcDfog;
+ if(dist > -vfcDnear[4]*size) return VFC_FOGGED;
+ if(dist > -vfcDfar[4]*size) v = VFC_PART_VISIBLE;
+
+ return v;
+}
+
+float vadist(vtxarray *va, const vec &p)
+{
+ return p.dist_to_bb(va->bbmin, va->bbmax);
+}
+
+#define VASORTSIZE 64
+
+static vtxarray *vasort[VASORTSIZE];
+
+void addvisibleva(vtxarray *va)
+{
+ float dist = vadist(va, camera1->o);
+ va->distance = int(dist); /*cv.dist(camera1->o) - va->size*SQRT3/2*/
+
+ int hash = clamp(int(dist*VASORTSIZE/worldsize), 0, VASORTSIZE-1);
+ vtxarray **prev = &vasort[hash], *cur = vasort[hash];
+
+ while(cur && va->distance >= cur->distance)
+ {
+ prev = &cur->next;
+ cur = cur->next;
+ }
+
+ va->next = *prev;
+ *prev = va;
+}
+
+void sortvisiblevas()
+{
+ visibleva = NULL;
+ vtxarray **last = &visibleva;
+ loopi(VASORTSIZE) if(vasort[i])
+ {
+ vtxarray *va = vasort[i];
+ *last = va;
+ while(va->next) va = va->next;
+ last = &va->next;
+ }
+}
+
+void findvisiblevas(vector<vtxarray *> &vas, bool resetocclude = false)
+{
+ loopv(vas)
+ {
+ vtxarray &v = *vas[i];
+ int prevvfc = resetocclude ? VFC_NOT_VISIBLE : v.curvfc;
+ v.curvfc = isvisiblecube(v.o, v.size);
+ if(v.curvfc!=VFC_NOT_VISIBLE)
+ {
+ if(pvsoccluded(v.o, v.size))
+ {
+ v.curvfc += PVS_FULL_VISIBLE - VFC_FULL_VISIBLE;
+ continue;
+ }
+ addvisibleva(&v);
+ if(v.children.length()) findvisiblevas(v.children, prevvfc>=VFC_NOT_VISIBLE);
+ if(prevvfc>=VFC_NOT_VISIBLE)
+ {
+ v.occluded = !v.texs ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+ v.query = NULL;
+ }
+ }
+ }
+}
+
+void calcvfcD()
+{
+ loopi(5)
+ {
+ plane &p = vfcP[i];
+ vfcDnear[i] = vfcDfar[i] = 0;
+ loopk(3) if(p[k] > 0) vfcDfar[i] += p[k];
+ else vfcDnear[i] += p[k];
+ }
+}
+
+void setvfcP(float z, const vec &bbmin, const vec &bbmax)
+{
+ vec4 px = camprojmatrix.rowx(), py = camprojmatrix.rowy(), pz = camprojmatrix.rowz(), pw = camprojmatrix.roww();
+ vfcP[0] = plane(vec4(pw).mul(-bbmin.x).add(px)).normalize(); // left plane
+ vfcP[1] = plane(vec4(pw).mul(bbmax.x).sub(px)).normalize(); // right plane
+ vfcP[2] = plane(vec4(pw).mul(-bbmin.y).add(py)).normalize(); // bottom plane
+ vfcP[3] = plane(vec4(pw).mul(bbmax.y).sub(py)).normalize(); // top plane
+ vfcP[4] = plane(vec4(pw).add(pz)).normalize(); // near/far planes
+ if(z >= 0) loopi(5) vfcP[i].reflectz(z);
+
+ vfcDfog = fog;
+ calcvfcD();
+}
+
+plane oldvfcP[5];
+
+void savevfcP()
+{
+ memcpy(oldvfcP, vfcP, sizeof(vfcP));
+}
+
+void restorevfcP()
+{
+ memcpy(vfcP, oldvfcP, sizeof(vfcP));
+ calcvfcD();
+}
+
+void visiblecubes(bool cull)
+{
+ memclear(vasort);
+
+ if(cull)
+ {
+ setvfcP();
+ findvisiblevas(varoot);
+ sortvisiblevas();
+ }
+ else
+ {
+ memclear(vfcP);
+ vfcDfog = 1000000;
+ memclear(vfcDnear);
+ memclear(vfcDfar);
+ visibleva = NULL;
+ loopv(valist)
+ {
+ vtxarray *va = valist[i];
+ va->distance = 0;
+ va->curvfc = VFC_FULL_VISIBLE;
+ va->occluded = !va->texs ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+ va->query = NULL;
+ va->next = visibleva;
+ visibleva = va;
+ }
+ }
+}
+
+static inline bool insideva(const vtxarray *va, const vec &v, int margin = 2)
+{
+ int size = va->size + margin;
+ return v.x>=va->o.x-margin && v.y>=va->o.y-margin && v.z>=va->o.z-margin &&
+ v.x<=va->o.x+size && v.y<=va->o.y+size && v.z<=va->o.z+size;
+}
+
+///////// occlusion queries /////////////
+
+#define MAXQUERY 2048
+#define MAXQUERYFRAMES 2
+
+struct queryframe
+{
+ int cur, max;
+ occludequery queries[MAXQUERY];
+
+ queryframe() : cur(0), max(0) {}
+
+ void flip() { loopi(cur) queries[i].owner = NULL; cur = 0; }
+
+ occludequery *newquery(void *owner)
+ {
+ if(cur >= max)
+ {
+ if(max >= MAXQUERY) return NULL;
+ glGenQueries_(1, &queries[max++].id);
+ }
+ occludequery *query = &queries[cur++];
+ query->owner = owner;
+ query->fragments = -1;
+ return query;
+ }
+
+ void reset() { loopi(max) queries[i].owner = NULL; }
+
+ void cleanup()
+ {
+ loopi(max)
+ {
+ glDeleteQueries_(1, &queries[i].id);
+ queries[i].owner = NULL;
+ }
+ cur = max = 0;
+ }
+};
+
+static queryframe queryframes[MAXQUERYFRAMES];
+static uint flipquery = 0;
+
+int getnumqueries()
+{
+ return queryframes[flipquery].cur;
+}
+
+void flipqueries()
+{
+ flipquery = (flipquery + 1) % MAXQUERYFRAMES;
+ queryframes[flipquery].flip();
+}
+
+occludequery *newquery(void *owner)
+{
+ return queryframes[flipquery].newquery(owner);
+}
+
+void resetqueries()
+{
+ loopi(MAXQUERYFRAMES) queryframes[i].reset();
+}
+
+void clearqueries()
+{
+ loopi(MAXQUERYFRAMES) queryframes[i].cleanup();
+}
+
+VAR(oqfrags, 0, 8, 64);
+VAR(oqwait, 0, 1, 1);
+
+void startquery(occludequery *query)
+{
+ glBeginQuery_(GL_SAMPLES_PASSED, query->id);
+}
+
+void endquery(occludequery *query)
+{
+ glEndQuery_(GL_SAMPLES_PASSED);
+}
+
+bool checkquery(occludequery *query, bool nowait)
+{
+ GLuint fragments;
+ if(query->fragments >= 0) fragments = query->fragments;
+ else
+ {
+ if(nowait || !oqwait)
+ {
+ GLint avail;
+ glGetQueryObjectiv_(query->id, GL_QUERY_RESULT_AVAILABLE, &avail);
+ if(!avail) return false;
+ }
+ glGetQueryObjectuiv_(query->id, GL_QUERY_RESULT, &fragments);
+ query->fragments = fragments;
+ }
+ return fragments < uint(oqfrags);
+}
+
+static GLuint bbvbo = 0, bbebo = 0;
+
+static void setupbb()
+{
+ if(!bbvbo)
+ {
+ glGenBuffers_(1, &bbvbo);
+ gle::bindvbo(bbvbo);
+ vec verts[8];
+ loopi(8) verts[i] = vec(i&1, (i>>1)&1, (i>>2)&1);
+ glBufferData_(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
+ gle::clearvbo();
+ }
+ if(!bbebo)
+ {
+ glGenBuffers_(1, &bbebo);
+ gle::bindebo(bbebo);
+ GLushort tris[3*2*6];
+ #define GENFACEORIENT(orient, v0, v1, v2, v3) do { \
+ int offset = orient*3*2; \
+ tris[offset + 0] = v0; \
+ tris[offset + 1] = v1; \
+ tris[offset + 2] = v2; \
+ tris[offset + 3] = v0; \
+ tris[offset + 4] = v2; \
+ tris[offset + 5] = v3; \
+ } while(0);
+ #define GENFACEVERT(orient, vert, ox,oy,oz, rx,ry,rz) (ox | oy | oz)
+ GENFACEVERTS(0, 1, 0, 2, 0, 4, , , , , , )
+ #undef GENFACEORIENT
+ #undef GENFACEVERT
+ glBufferData_(GL_ELEMENT_ARRAY_BUFFER, sizeof(tris), tris, GL_STATIC_DRAW);
+ gle::clearebo();
+ }
+}
+
+static void cleanupbb()
+{
+ if(bbvbo) { glDeleteBuffers_(1, &bbvbo); bbvbo = 0; }
+ if(bbebo) { glDeleteBuffers_(1, &bbebo); bbebo = 0; }
+}
+
+void startbb(bool mask)
+{
+ setupbb();
+ gle::bindvbo(bbvbo);
+ gle::bindebo(bbebo);
+ gle::vertexpointer(sizeof(vec), (const vec *)0);
+ gle::enablevertex();
+ SETSHADER(bbquery);
+ if(mask)
+ {
+ glDepthMask(GL_FALSE);
+ glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+ }
+}
+
+void endbb(bool mask)
+{
+ gle::disablevertex();
+ gle::clearvbo();
+ gle::clearebo();
+ if(mask)
+ {
+ glDepthMask(GL_TRUE);
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ }
+}
+
+void drawbb(const ivec &bo, const ivec &br)
+{
+ LOCALPARAMF(bborigin, bo.x, bo.y, bo.z);
+ LOCALPARAMF(bbsize, br.x, br.y, br.z);
+ glDrawRangeElements_(GL_TRIANGLES, 0, 8-1, 3*2*6, GL_UNSIGNED_SHORT, (ushort *)0);
+ xtraverts += 8;
+}
+
+extern int octaentsize;
+
+static octaentities *visiblemms, **lastvisiblemms;
+
+static inline bool insideoe(const octaentities *oe, const vec &v, int margin = 1)
+{
+ return v.x>=oe->bbmin.x-margin && v.y>=oe->bbmin.y-margin && v.z>=oe->bbmin.z-margin &&
+ v.x<=oe->bbmax.x+margin && v.y<=oe->bbmax.y+margin && v.z<=oe->bbmax.z+margin;
+}
+
+void findvisiblemms(const vector<extentity *> &ents, bool doquery)
+{
+ visiblemms = NULL;
+ lastvisiblemms = &visiblemms;
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(va->mapmodels.empty() || va->curvfc >= VFC_FOGGED || va->occluded >= OCCLUDE_BB) continue;
+ loopv(va->mapmodels)
+ {
+ octaentities *oe = va->mapmodels[i];
+ if(isfoggedcube(oe->o, oe->size) || pvsoccluded(oe->bbmin, oe->bbmax)) continue;
+
+ bool occluded = doquery && oe->query && oe->query->owner == oe && checkquery(oe->query);
+ if(occluded)
+ {
+ oe->distance = -1;
+
+ oe->next = NULL;
+ *lastvisiblemms = oe;
+ lastvisiblemms = &oe->next;
+ }
+ else
+ {
+ int visible = 0;
+ loopv(oe->mapmodels)
+ {
+ extentity &e = *ents[oe->mapmodels[i]];
+ if(e.flags&EF_NOVIS) continue;
+ e.flags |= EF_RENDER;
+ ++visible;
+ }
+ if(!visible) continue;
+
+ oe->distance = int(camera1->o.dist_to_bb(oe->o, oe->size));
+
+ octaentities **prev = &visiblemms, *cur = visiblemms;
+ while(cur && cur->distance >= 0 && oe->distance > cur->distance)
+ {
+ prev = &cur->next;
+ cur = cur->next;
+ }
+
+ if(*prev == NULL) lastvisiblemms = &oe->next;
+ oe->next = *prev;
+ *prev = oe;
+ }
+ }
+ }
+}
+
+VAR(oqmm, 0, 4, 8);
+
+void rendermapmodel(extentity &e)
+{
+ int anim = ANIM_MAPMODEL|ANIM_LOOP, basetime = 0;
+ if(e.flags&EF_ANIM) entities::animatemapmodel(e, anim, basetime);
+ mapmodelinfo *mmi = getmminfo(e.attr2);
+ if(mmi) rendermodel(&e.light, mmi->name, anim, e.o, e.attr1, 0, MDL_CULL_VFC | MDL_CULL_DIST | MDL_DYNLIGHT, NULL, NULL, basetime);
+}
+
+vtxarray *reflectedva;
+
+void renderreflectedmapmodels()
+{
+ const vector<extentity *> &ents = entities::getents();
+
+ octaentities *mms = visiblemms;
+ if(reflecting)
+ {
+ octaentities **lastmms = &mms;
+ for(vtxarray *va = reflectedva; va; va = va->rnext)
+ {
+ if(va->mapmodels.empty() || va->distance > reflectdist) continue;
+ loopv(va->mapmodels)
+ {
+ octaentities *oe = va->mapmodels[i];
+ *lastmms = oe;
+ lastmms = &oe->rnext;
+ }
+ }
+ *lastmms = NULL;
+ }
+ for(octaentities *oe = mms; oe; oe = reflecting ? oe->rnext : oe->next) if(reflecting || oe->distance >= 0)
+ {
+ if(reflecting || refracting>0 ? oe->bbmax.z <= reflectz : oe->bbmin.z >= reflectz) continue;
+ if(isfoggedcube(oe->o, oe->size)) continue;
+ loopv(oe->mapmodels)
+ {
+ extentity &e = *ents[oe->mapmodels[i]];
+ if(e.flags&(EF_NOVIS | EF_RENDER)) continue;
+ e.flags |= EF_RENDER;
+ }
+ }
+ if(mms)
+ {
+ startmodelbatches();
+ for(octaentities *oe = mms; oe; oe = reflecting ? oe->rnext : oe->next)
+ {
+ loopv(oe->mapmodels)
+ {
+ extentity &e = *ents[oe->mapmodels[i]];
+ if(!(e.flags&EF_RENDER)) continue;
+ rendermapmodel(e);
+ e.flags &= ~EF_RENDER;
+ }
+ }
+ endmodelbatches();
+ }
+}
+
+void rendermapmodels()
+{
+ static int skipoq = 0;
+ bool doquery = !drawtex && oqfrags && oqmm;
+ const vector<extentity *> &ents = entities::getents();
+ findvisiblemms(ents, doquery);
+
+ startmodelbatches();
+ for(octaentities *oe = visiblemms; oe; oe = oe->next) if(oe->distance>=0)
+ {
+ bool rendered = false;
+ loopv(oe->mapmodels)
+ {
+ extentity &e = *ents[oe->mapmodels[i]];
+ if(!(e.flags&EF_RENDER)) continue;
+ if(!rendered)
+ {
+ rendered = true;
+ oe->query = doquery && oe->distance>0 && !(++skipoq%oqmm) ? newquery(oe) : NULL;
+ if(oe->query) startmodelquery(oe->query);
+ }
+ rendermapmodel(e);
+ e.flags &= ~EF_RENDER;
+ }
+ if(rendered && oe->query) endmodelquery();
+ }
+ endmodelbatches();
+
+ bool queried = true;
+ for(octaentities *oe = visiblemms; oe; oe = oe->next) if(oe->distance<0)
+ {
+ oe->query = doquery && !insideoe(oe, camera1->o) ? newquery(oe) : NULL;
+ if(!oe->query) continue;
+ if(queried)
+ {
+ startbb();
+ queried = false;
+ }
+ startquery(oe->query);
+ drawbb(oe->bbmin, ivec(oe->bbmax).sub(oe->bbmin));
+ endquery(oe->query);
+ }
+ if(!queried)
+ {
+ endbb();
+ }
+}
+
+static inline bool bbinsideva(const ivec &bo, const ivec &br, vtxarray *va)
+{
+ return bo.x >= va->bbmin.x && bo.y >= va->bbmin.y && bo.z >= va->bbmin.z &&
+ br.x <= va->bbmax.x && br.y <= va->bbmax.y && br.z <= va->bbmax.z;
+}
+
+static inline bool bboccluded(const ivec &bo, const ivec &br, cube *c, const ivec &o, int size)
+{
+ loopoctabox(o, size, bo, br)
+ {
+ ivec co(i, o, size);
+ if(c[i].ext && c[i].ext->va)
+ {
+ vtxarray *va = c[i].ext->va;
+ if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) continue;
+ }
+ if(c[i].children && bboccluded(bo, br, c[i].children, co, size>>1)) continue;
+ return false;
+ }
+ return true;
+}
+
+bool bboccluded(const ivec &bo, const ivec &br)
+{
+ int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z);
+ if(diff&~((1<<worldscale)-1)) return false;
+ int scale = worldscale-1;
+ if(diff&(1<<scale)) return bboccluded(bo, br, worldroot, ivec(0, 0, 0), 1<<scale);
+ cube *c = &worldroot[octastep(bo.x, bo.y, bo.z, scale)];
+ if(c->ext && c->ext->va)
+ {
+ vtxarray *va = c->ext->va;
+ if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) return true;
+ }
+ scale--;
+ while(c->children && !(diff&(1<<scale)))
+ {
+ c = &c->children[octastep(bo.x, bo.y, bo.z, scale)];
+ if(c->ext && c->ext->va)
+ {
+ vtxarray *va = c->ext->va;
+ if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) return true;
+ }
+ scale--;
+ }
+ if(c->children) return bboccluded(bo, br, c->children, ivec(bo).mask(~((2<<scale)-1)), 1<<scale);
+ return false;
+}
+
+VAR(outline, 0, 0, 1);
+HVARP(outlinecolour, 0, 0, 0xFFFFFF);
+VAR(dtoutline, 0, 1, 1);
+
+void renderoutline()
+{
+ notextureshader->set();
+
+ gle::enablevertex();
+
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+ gle::color(vec::hexcolor(outlinecolour));
+
+ enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+ if(!dtoutline) glDisable(GL_DEPTH_TEST);
+
+ vtxarray *prev = NULL;
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(va->occluded >= OCCLUDE_BB) continue;
+ if(!va->alphaback && !va->alphafront && (!va->texs || va->occluded >= OCCLUDE_GEOM)) continue;
+
+ if(!prev || va->vbuf != prev->vbuf)
+ {
+ gle::bindvbo(va->vbuf);
+ gle::bindebo(va->ebuf);
+ const vertex *ptr = 0;
+ gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+ }
+
+ if(va->texs && va->occluded < OCCLUDE_GEOM)
+ {
+ drawvatris(va, 3*va->tris, va->edata);
+ xtravertsva += va->verts;
+ }
+ if(va->alphatris)
+ {
+ drawvatris(va, 3*va->alphatris, &va->edata[3*(va->tris + va->blendtris)]);
+ xtravertsva += 3*va->alphatris;
+ }
+
+ prev = va;
+ }
+
+ if(!dtoutline) glEnable(GL_DEPTH_TEST);
+
+ disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+ gle::clearvbo();
+ gle::clearebo();
+ gle::disablevertex();
+}
+
+HVAR(blendbrushcolor, 0, 0x0000C0, 0xFFFFFF);
+
+void renderblendbrush(GLuint tex, float x, float y, float w, float h)
+{
+ SETSHADER(blendbrush);
+
+ gle::enablevertex();
+
+ glDepthFunc(GL_LEQUAL);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
+ glBindTexture(GL_TEXTURE_2D, tex);
+ gle::color(vec::hexcolor(blendbrushcolor), 0.25f);
+
+ LOCALPARAMF(texgenS, 1.0f/w, 0, 0, -x/w);
+ LOCALPARAMF(texgenT, 0, 1.0f/h, 0, -y/h);
+
+ vtxarray *prev = NULL;
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(!va->texs || va->occluded >= OCCLUDE_GEOM) continue;
+ if(va->o.x + va->size <= x || va->o.y + va->size <= y || va->o.x >= x + w || va->o.y >= y + h) continue;
+
+ if(!prev || va->vbuf != prev->vbuf)
+ {
+ gle::bindvbo(va->vbuf);
+ gle::bindebo(va->ebuf);
+ const vertex *ptr = 0;
+ gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+ }
+
+ drawvatris(va, 3*va->tris, va->edata);
+ xtravertsva += va->verts;
+
+ prev = va;
+ }
+
+ glDisable(GL_BLEND);
+
+ glDepthFunc(GL_LESS);
+
+ gle::clearvbo();
+ gle::clearebo();
+ gle::disablevertex();
+}
+
+void rendershadowmapreceivers()
+{
+ SETSHADER(shadowmapreceiver);
+
+ gle::enablevertex();
+
+ glCullFace(GL_FRONT);
+ glDepthMask(GL_FALSE);
+ glDepthFunc(GL_GREATER);
+
+ extern int ati_minmax_bug;
+ if(!ati_minmax_bug) glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_FALSE);
+
+ glEnable(GL_BLEND);
+ glBlendEquation_(GL_MAX);
+ glBlendFunc(GL_ONE, GL_ONE);
+
+ vtxarray *prev = NULL;
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(!va->texs || va->curvfc >= VFC_FOGGED || !isshadowmapreceiver(va)) continue;
+
+ if(!prev || va->vbuf != prev->vbuf)
+ {
+ gle::bindvbo(va->vbuf);
+ gle::bindebo(va->ebuf);
+ const vertex *ptr = 0;
+ gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+ }
+
+ drawvatris(va, 3*va->tris, va->edata);
+ xtravertsva += va->verts;
+
+ prev = va;
+ }
+
+ glDisable(GL_BLEND);
+ glBlendEquation_(GL_FUNC_ADD);
+
+ glCullFace(GL_BACK);
+ glDepthMask(GL_TRUE);
+ glDepthFunc(GL_LESS);
+
+ if(!ati_minmax_bug) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+ gle::clearvbo();
+ gle::clearebo();
+ gle::disablevertex();
+}
+
+void renderdepthobstacles(const vec &bbmin, const vec &bbmax, float scale, float *ranges, int numranges)
+{
+ float scales[4] = { 0, 0, 0, 0 }, offsets[4] = { 0, 0, 0, 0 };
+ if(numranges < 0)
+ {
+ SETSHADER(depthfxsplitworld);
+
+ loopi(-numranges)
+ {
+ if(!i) scales[i] = 1.0f/scale;
+ else scales[i] = scales[i-1]*256;
+ }
+ }
+ else
+ {
+ SETSHADER(depthfxworld);
+
+ if(!numranges) loopi(4) scales[i] = 1.0f/scale;
+ else loopi(numranges)
+ {
+ scales[i] = 1.0f/scale;
+ offsets[i] = -ranges[i]/scale;
+ }
+ }
+ LOCALPARAMF(depthscale, scales[0], scales[1], scales[2], scales[3]);
+ LOCALPARAMF(depthoffsets, offsets[0], offsets[1], offsets[2], offsets[3]);
+
+ gle::enablevertex();
+
+ vtxarray *prev = NULL;
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(!va->texs || va->occluded >= OCCLUDE_GEOM ||
+ va->o.x > bbmax.x || va->o.y > bbmax.y || va->o.z > bbmax.z ||
+ va->o.x + va->size < bbmin.x || va->o.y + va->size < bbmin.y || va->o.z + va->size < bbmin.z)
+ continue;
+
+ if(!prev || va->vbuf != prev->vbuf)
+ {
+ gle::bindvbo(va->vbuf);
+ gle::bindebo(va->ebuf);
+ const vertex *ptr = 0;
+ gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+ }
+
+ drawvatris(va, 3*va->tris, va->edata);
+ xtravertsva += va->verts;
+ if(va->alphatris > 0)
+ {
+ drawvatris(va, 3*va->alphatris, va->edata + 3*(va->tris + va->blendtris));
+ xtravertsva += 3*va->alphatris;
+ }
+
+ prev = va;
+ }
+
+ gle::clearvbo();
+ gle::clearebo();
+ gle::disablevertex();
+}
+
+VAR(oqdist, 0, 256, 1024);
+VAR(zpass, 0, 1, 1);
+VAR(envpass, 0, 1, 1);
+
+struct renderstate
+{
+ bool colormask, depthmask, blending;
+ int alphaing;
+ GLuint vbuf;
+ bool vattribs, vquery;
+ vec colorscale, lightcolor;
+ float alphascale;
+ GLuint textures[8];
+ Slot *slot, *texgenslot;
+ VSlot *vslot, *texgenvslot;
+ vec2 texgenscroll;
+ int texgendim;
+ int visibledynlights;
+ uint dynlightmask;
+
+ renderstate() : colormask(true), depthmask(true), blending(false), alphaing(0), vbuf(0), vattribs(false), vquery(false), colorscale(1, 1, 1), alphascale(0), slot(NULL), texgenslot(NULL), vslot(NULL), texgenvslot(NULL), texgenscroll(0, 0), texgendim(-1), visibledynlights(0), dynlightmask(0)
+ {
+ loopk(8) textures[k] = 0;
+ }
+};
+
+static inline void disablevbuf(renderstate &cur)
+{
+ gle::clearvbo();
+ gle::clearebo();
+ cur.vbuf = 0;
+}
+
+static inline void enablevquery(renderstate &cur)
+{
+ if(cur.colormask) { cur.colormask = false; glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); }
+ if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); }
+ startbb(false);
+ cur.vquery = true;
+}
+
+static inline void disablevquery(renderstate &cur)
+{
+ endbb(false);
+ cur.vquery = false;
+}
+
+static void renderquery(renderstate &cur, occludequery *query, vtxarray *va, bool full = true)
+{
+ if(!cur.vquery) enablevquery(cur);
+
+ startquery(query);
+
+ if(full) drawbb(ivec(va->bbmin).sub(1), ivec(va->bbmax).sub(va->bbmin).add(2));
+ else drawbb(va->geommin, ivec(va->geommax).sub(va->geommin));
+
+ endquery(query);
+}
+
+enum
+{
+ RENDERPASS_LIGHTMAP = 0,
+ RENDERPASS_Z,
+ RENDERPASS_CAUSTICS,
+ RENDERPASS_FOG,
+ RENDERPASS_LIGHTMAP_BLEND
+};
+
+struct geombatch
+{
+ const elementset &es;
+ VSlot &vslot;
+ ushort *edata;
+ vtxarray *va;
+ int next, batch;
+
+ geombatch(const elementset &es, ushort *edata, vtxarray *va)
+ : es(es), vslot(lookupvslot(es.texture)), edata(edata), va(va),
+ next(-1), batch(-1)
+ {}
+
+ int compare(const geombatch &b) const
+ {
+ if(va->vbuf < b.va->vbuf) return -1;
+ if(va->vbuf > b.va->vbuf) return 1;
+ if(va->dynlightmask < b.va->dynlightmask) return -1;
+ if(va->dynlightmask > b.va->dynlightmask) return 1;
+ if(vslot.slot->shader < b.vslot.slot->shader) return -1;
+ if(vslot.slot->shader > b.vslot.slot->shader) return 1;
+ if(vslot.slot->params.length() < b.vslot.slot->params.length()) return -1;
+ if(vslot.slot->params.length() > b.vslot.slot->params.length()) return 1;
+ if(es.texture < b.es.texture) return -1;
+ if(es.texture > b.es.texture) return 1;
+ if(es.lmid < b.es.lmid) return -1;
+ if(es.lmid > b.es.lmid) return 1;
+ if(es.envmap < b.es.envmap) return -1;
+ if(es.envmap > b.es.envmap) return 1;
+ if(es.dim < b.es.dim) return -1;
+ if(es.dim > b.es.dim) return 1;
+ return 0;
+ }
+};
+
+static vector<geombatch> geombatches;
+static int firstbatch = -1, numbatches = 0;
+
+static void mergetexs(renderstate &cur, vtxarray *va, elementset *texs = NULL, int numtexs = 0, ushort *edata = NULL)
+{
+ if(!texs)
+ {
+ texs = va->eslist;
+ numtexs = va->texs;
+ edata = va->edata;
+ if(cur.alphaing)
+ {
+ texs += va->texs + va->blends;
+ edata += 3*(va->tris + va->blendtris);
+ numtexs = va->alphaback;
+ if(cur.alphaing > 1) numtexs += va->alphafront;
+ }
+ }
+
+ if(firstbatch < 0)
+ {
+ firstbatch = geombatches.length();
+ numbatches = numtexs;
+ loopi(numtexs-1)
+ {
+ geombatches.add(geombatch(texs[i], edata, va)).next = i+1;
+ edata += texs[i].length[1];
+ }
+ geombatches.add(geombatch(texs[numtexs-1], edata, va));
+ return;
+ }
+
+ int prevbatch = -1, curbatch = firstbatch, curtex = 0;
+ do
+ {
+ geombatch &b = geombatches.add(geombatch(texs[curtex], edata, va));
+ edata += texs[curtex].length[1];
+ int dir = -1;
+ while(curbatch >= 0)
+ {
+ dir = b.compare(geombatches[curbatch]);
+ if(dir <= 0) break;
+ prevbatch = curbatch;
+ curbatch = geombatches[curbatch].next;
+ }
+ if(!dir)
+ {
+ int last = curbatch, next;
+ for(;;)
+ {
+ next = geombatches[last].batch;
+ if(next < 0) break;
+ last = next;
+ }
+ if(last==curbatch)
+ {
+ b.batch = curbatch;
+ b.next = geombatches[curbatch].next;
+ if(prevbatch < 0) firstbatch = geombatches.length()-1;
+ else geombatches[prevbatch].next = geombatches.length()-1;
+ curbatch = geombatches.length()-1;
+ }
+ else
+ {
+ b.batch = next;
+ geombatches[last].batch = geombatches.length()-1;
+ }
+ }
+ else
+ {
+ numbatches++;
+ b.next = curbatch;
+ if(prevbatch < 0) firstbatch = geombatches.length()-1;
+ else geombatches[prevbatch].next = geombatches.length()-1;
+ prevbatch = geombatches.length()-1;
+ }
+ }
+ while(++curtex < numtexs);
+}
+
+static inline void enablevattribs(renderstate &cur, bool all = true)
+{
+ gle::enablevertex();
+ if(all)
+ {
+ gle::enabletexcoord0();
+ gle::enabletexcoord1();
+ gle::enablenormal();
+ gle::enabletangent();
+ }
+ cur.vattribs = true;
+}
+
+static inline void disablevattribs(renderstate &cur, bool all = true)
+{
+ gle::disablevertex();
+ if(all)
+ {
+ gle::disabletexcoord0();
+ gle::disabletexcoord1();
+ gle::disablenormal();
+ gle::disabletangent();
+ }
+ cur.vattribs = false;
+}
+
+static void changevbuf(renderstate &cur, int pass, vtxarray *va)
+{
+ gle::bindvbo(va->vbuf);
+ gle::bindebo(va->ebuf);
+ cur.vbuf = va->vbuf;
+
+ vertex *vdata = (vertex *)0;
+ gle::vertexpointer(sizeof(vertex), vdata->pos.v);
+
+ if(pass==RENDERPASS_LIGHTMAP)
+ {
+ gle::normalpointer(sizeof(vertex), vdata->norm.v, GL_BYTE);
+ gle::texcoord0pointer(sizeof(vertex), vdata->tc.v);
+ gle::texcoord1pointer(sizeof(vertex), vdata->lm.v, GL_SHORT);
+ gle::tangentpointer(sizeof(vertex), vdata->tangent.v, GL_BYTE);
+ }
+}
+
+static void changebatchtmus(renderstate &cur, int pass, geombatch &b)
+{
+ bool changed = false;
+ extern bool brightengeom;
+ extern int fullbright;
+ int lmid = brightengeom && (b.es.lmid < LMID_RESERVED || (fullbright && editmode)) ? LMID_BRIGHT : b.es.lmid;
+ if(cur.textures[1]!=lightmaptexs[lmid].id)
+ {
+ glActiveTexture_(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, cur.textures[1] = lightmaptexs[lmid].id);
+ changed = true;
+ }
+ int tmu = 2;
+ if(b.vslot.slot->shader->type&SHADER_NORMALSLMS)
+ {
+ if(cur.textures[tmu]!=lightmaptexs[lmid+1].id)
+ {
+ glActiveTexture_(GL_TEXTURE0+tmu);
+ glBindTexture(GL_TEXTURE_2D, cur.textures[tmu] = lightmaptexs[lmid+1].id);
+ changed = true;
+ }
+ tmu++;
+ }
+ if(b.vslot.slot->shader->type&SHADER_ENVMAP && b.es.envmap!=EMID_CUSTOM)
+ {
+ GLuint emtex = lookupenvmap(b.es.envmap);
+ if(cur.textures[tmu]!=emtex)
+ {
+ glActiveTexture_(GL_TEXTURE0+tmu);
+ glBindTexture(GL_TEXTURE_CUBE_MAP, cur.textures[tmu] = emtex);
+ changed = true;
+ }
+ }
+ if(changed) glActiveTexture_(GL_TEXTURE0);
+
+ if(cur.dynlightmask != b.va->dynlightmask)
+ {
+ cur.visibledynlights = setdynlights(b.va);
+ cur.dynlightmask = b.va->dynlightmask;
+ }
+}
+
+static void changeslottmus(renderstate &cur, int pass, Slot &slot, VSlot &vslot)
+{
+ if(pass==RENDERPASS_LIGHTMAP)
+ {
+ GLuint diffusetex = slot.sts.empty() ? notexture->id : slot.sts[0].t->id;
+ if(cur.textures[0]!=diffusetex)
+ glBindTexture(GL_TEXTURE_2D, cur.textures[0] = diffusetex);
+ }
+
+ if(cur.alphaing)
+ {
+ float alpha = cur.alphaing > 1 ? vslot.alphafront : vslot.alphaback;
+ if(cur.colorscale != vslot.colorscale || cur.alphascale != alpha)
+ {
+ cur.colorscale = vslot.colorscale;
+ cur.alphascale = alpha;
+ GLOBALPARAMF(colorparams, 2*alpha*vslot.colorscale.x, 2*alpha*vslot.colorscale.y, 2*alpha*vslot.colorscale.z, alpha);
+ setfogcolor(vec(curfogcolor).mul(alpha));
+ }
+ }
+ else if(cur.colorscale != vslot.colorscale)
+ {
+ cur.colorscale = vslot.colorscale;
+ GLOBALPARAMF(colorparams, 2*vslot.colorscale.x, 2*vslot.colorscale.y, 2*vslot.colorscale.z, 1);
+ }
+ int tmu = 2, envmaptmu = -1;
+ if(slot.shader->type&SHADER_NORMALSLMS) tmu++;
+ if(slot.shader->type&SHADER_ENVMAP) envmaptmu = tmu++;
+ loopvj(slot.sts)
+ {
+ Slot::Tex &t = slot.sts[j];
+ if(t.type==TEX_DIFFUSE || t.combined>=0) continue;
+ if(t.type==TEX_ENVMAP)
+ {
+ if(envmaptmu>=0 && t.t && cur.textures[envmaptmu]!=t.t->id)
+ {
+ glActiveTexture_(GL_TEXTURE0+envmaptmu);
+ glBindTexture(GL_TEXTURE_CUBE_MAP, cur.textures[envmaptmu] = t.t->id);
+ }
+ }
+ else
+ {
+ if(cur.textures[tmu]!=t.t->id)
+ {
+ glActiveTexture_(GL_TEXTURE0+tmu);
+ glBindTexture(GL_TEXTURE_2D, cur.textures[tmu] = t.t->id);
+ }
+ if(++tmu >= 8) break;
+ }
+ }
+ glActiveTexture_(GL_TEXTURE0);
+
+ cur.slot = &slot;
+ cur.vslot = &vslot;
+}
+
+static void changeshader(renderstate &cur, Shader *s, Slot &slot, VSlot &vslot, bool shadowed)
+{
+ if(glaring)
+ {
+ static Shader *noglareshader = NULL, *noglareblendshader = NULL, *noglarealphashader = NULL;
+ Shader *fallback;
+ if(cur.blending) { if(!noglareblendshader) noglareblendshader = lookupshaderbyname("noglareblendworld"); fallback = noglareblendshader; }
+ else if(cur.alphaing) { if(!noglarealphashader) noglarealphashader = lookupshaderbyname("noglarealphaworld"); fallback = noglarealphashader; }
+ else { if(!noglareshader) noglareshader = lookupshaderbyname("noglareworld"); fallback = noglareshader; }
+ if(s->hasoption(4)) s->setvariant(cur.visibledynlights, 4, slot, vslot, fallback);
+ else s->setvariant(cur.blending ? 1 : 0, 4, slot, vslot, fallback);
+ }
+ else if(fading && !cur.blending && !cur.alphaing)
+ {
+ if(shadowed) s->setvariant(cur.visibledynlights, 3, slot, vslot);
+ else s->setvariant(cur.visibledynlights, 2, slot, vslot);
+ }
+ else if(shadowed) s->setvariant(cur.visibledynlights, 1, slot, vslot);
+ else if(!cur.visibledynlights) s->set(slot, vslot);
+ else s->setvariant(cur.visibledynlights-1, 0, slot, vslot);
+}
+
+static void changetexgen(renderstate &cur, int dim, Slot &slot, VSlot &vslot)
+{
+ if(cur.texgenslot != &slot || cur.texgenvslot != &vslot)
+ {
+ Texture *curtex = !cur.texgenslot || cur.texgenslot->sts.empty() ? notexture : cur.texgenslot->sts[0].t,
+ *tex = slot.sts.empty() ? notexture : slot.sts[0].t;
+ if(!cur.texgenvslot || slot.sts.empty() ||
+ (curtex->xs != tex->xs || curtex->ys != tex->ys ||
+ cur.texgenvslot->rotation != vslot.rotation || cur.texgenvslot->scale != vslot.scale ||
+ cur.texgenvslot->offset != vslot.offset || cur.texgenvslot->scroll != vslot.scroll))
+ {
+ const texrotation &r = texrotations[vslot.rotation];
+ float xs = r.flipx ? -tex->xs : tex->xs,
+ ys = r.flipy ? -tex->ys : tex->ys;
+ vec2 scroll(vslot.scroll);
+ if(r.swapxy) swap(scroll.x, scroll.y);
+ scroll.x *= lastmillis*tex->xs/xs;
+ scroll.y *= lastmillis*tex->ys/ys;
+ if(cur.texgenscroll != scroll)
+ {
+ cur.texgenscroll = scroll;
+ cur.texgendim = -1;
+ }
+ }
+ cur.texgenslot = &slot;
+ cur.texgenvslot = &vslot;
+ }
+
+ if(cur.texgendim == dim) return;
+ GLOBALPARAM(texgenscroll, cur.texgenscroll);
+ cur.texgendim = dim;
+}
+
+static void renderbatch(renderstate &cur, int pass, geombatch &b)
+{
+ geombatch *shadowed = NULL;
+ int rendered = -1;
+ for(geombatch *curbatch = &b;; curbatch = &geombatches[curbatch->batch])
+ {
+ ushort len = curbatch->es.length[curbatch->va->shadowed ? 0 : 1];
+ if(len)
+ {
+ if(rendered < 0)
+ {
+ changeshader(cur, b.vslot.slot->shader, *b.vslot.slot, b.vslot, false);
+ rendered = 0;
+ gbatches++;
+ }
+ ushort minvert = curbatch->es.minvert[0], maxvert = curbatch->es.maxvert[0];
+ if(!curbatch->va->shadowed) { minvert = min(minvert, curbatch->es.minvert[1]); maxvert = max(maxvert, curbatch->es.maxvert[1]); }
+ drawtris(len, curbatch->edata, minvert, maxvert);
+ vtris += len/3;
+ }
+ if(curbatch->es.length[1] > len && !shadowed) shadowed = curbatch;
+ if(curbatch->batch < 0) break;
+ }
+ if(shadowed) for(geombatch *curbatch = shadowed;; curbatch = &geombatches[curbatch->batch])
+ {
+ if(curbatch->va->shadowed && curbatch->es.length[1] > curbatch->es.length[0])
+ {
+ if(rendered < 1)
+ {
+ changeshader(cur, b.vslot.slot->shader, *b.vslot.slot, b.vslot, true);
+ rendered = 1;
+ gbatches++;
+ }
+ ushort len = curbatch->es.length[1] - curbatch->es.length[0];
+ drawtris(len, curbatch->edata + curbatch->es.length[0], curbatch->es.minvert[1], curbatch->es.maxvert[1]);
+ vtris += len/3;
+ }
+ if(curbatch->batch < 0) break;
+ }
+}
+
+static void resetbatches()
+{
+ geombatches.setsize(0);
+ firstbatch = -1;
+ numbatches = 0;
+}
+
+static void renderbatches(renderstate &cur, int pass)
+{
+ cur.slot = NULL;
+ cur.vslot = NULL;
+ int curbatch = firstbatch;
+ if(curbatch >= 0)
+ {
+ if(cur.alphaing)
+ {
+ if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); }
+ }
+ else if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); }
+ if(!cur.colormask) { cur.colormask = true; glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, cur.alphaing ? GL_FALSE : GL_TRUE); }
+ if(!cur.vattribs)
+ {
+ if(cur.vquery) disablevquery(cur);
+ enablevattribs(cur);
+ }
+ }
+ while(curbatch >= 0)
+ {
+ geombatch &b = geombatches[curbatch];
+ curbatch = b.next;
+
+ if(cur.vbuf != b.va->vbuf) changevbuf(cur, pass, b.va);
+ if(cur.vslot != &b.vslot)
+ {
+ changeslottmus(cur, pass, *b.vslot.slot, b.vslot);
+ if(cur.texgendim != b.es.dim || (cur.texgendim <= 2 && cur.texgenvslot != &b.vslot)) changetexgen(cur, b.es.dim, *b.vslot.slot, b.vslot);
+ }
+ else if(cur.texgendim != b.es.dim) changetexgen(cur, b.es.dim, *b.vslot.slot, b.vslot);
+ if(pass == RENDERPASS_LIGHTMAP) changebatchtmus(cur, pass, b);
+
+ renderbatch(cur, pass, b);
+ }
+
+ resetbatches();
+}
+
+void renderzpass(renderstate &cur, vtxarray *va)
+{
+ if(!cur.vattribs)
+ {
+ if(cur.vquery) disablevquery(cur);
+ enablevattribs(cur, false);
+ }
+ if(cur.vbuf!=va->vbuf) changevbuf(cur, RENDERPASS_Z, va);
+ if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); }
+ if(cur.colormask) { cur.colormask = false; glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); }
+ int firsttex = 0, numtris = va->tris;
+ ushort *edata = va->edata;
+ if(cur.alphaing)
+ {
+ firsttex += va->texs + va->blends;
+ edata += 3*(va->tris + va->blendtris);
+ numtris = va->alphatris;
+ xtravertsva += 3*numtris;
+ }
+ else xtravertsva += va->verts;
+ nocolorshader->set();
+ drawvatris(va, 3*numtris, edata);
+}
+
+vector<vtxarray *> foggedvas;
+
+#define startvaquery(va, flush) \
+ do { \
+ if(va->query) \
+ { \
+ flush; \
+ startquery(va->query); \
+ } \
+ } while(0)
+
+
+#define endvaquery(va, flush) \
+ do { \
+ if(va->query) \
+ { \
+ flush; \
+ endquery(va->query); \
+ } \
+ } while(0)
+
+void renderfoggedvas(renderstate &cur, bool doquery = false)
+{
+ static Shader *fogshader = NULL;
+ if(!fogshader) fogshader = lookupshaderbyname("fogworld");
+ if(fading) fogshader->setvariant(0, 2);
+ else fogshader->set();
+
+ if(!cur.vattribs) enablevattribs(cur, false);
+
+ loopv(foggedvas)
+ {
+ vtxarray *va = foggedvas[i];
+ if(cur.vbuf!=va->vbuf) changevbuf(cur, RENDERPASS_FOG, va);
+
+ if(doquery) startvaquery(va, );
+ drawvatris(va, 3*va->tris, va->edata);
+ vtris += va->tris;
+ if(doquery) endvaquery(va, );
+ }
+
+ foggedvas.setsize(0);
+}
+
+VAR(batchgeom, 0, 1, 1);
+
+void renderva(renderstate &cur, vtxarray *va, int pass = RENDERPASS_LIGHTMAP, bool fogpass = false, bool doquery = false)
+{
+ switch(pass)
+ {
+ case RENDERPASS_LIGHTMAP:
+ if(!cur.alphaing) vverts += va->verts;
+ va->shadowed = false;
+ va->dynlightmask = 0;
+ if(fogpass ? va->geommax.z<=reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED)
+ {
+ if(!cur.alphaing && !cur.blending) foggedvas.add(va);
+ break;
+ }
+ if(!drawtex && !glaring && !cur.alphaing)
+ {
+ va->shadowed = isshadowmapreceiver(va);
+ calcdynlightmask(va);
+ }
+ if(doquery) startvaquery(va, { if(geombatches.length()) renderbatches(cur, pass); });
+ mergetexs(cur, va);
+ if(doquery) endvaquery(va, { if(geombatches.length()) renderbatches(cur, pass); });
+ else if(!batchgeom && geombatches.length()) renderbatches(cur, pass);
+ break;
+
+ case RENDERPASS_LIGHTMAP_BLEND:
+ {
+ if(doquery) startvaquery(va, { if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); });
+ mergetexs(cur, va, &va->eslist[va->texs], va->blends, va->edata + 3*va->tris);
+ if(doquery) endvaquery(va, { if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); });
+ else if(!batchgeom && geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+ break;
+ }
+
+ case RENDERPASS_FOG:
+ if(cur.vbuf!=va->vbuf) changevbuf(cur, pass, va);
+ drawvatris(va, 3*va->tris, va->edata);
+ xtravertsva += va->verts;
+ break;
+
+ case RENDERPASS_CAUSTICS:
+ if(cur.vbuf!=va->vbuf) changevbuf(cur, pass, va);
+ drawvatris(va, 3*va->tris, va->edata);
+ xtravertsva += va->verts;
+ break;
+
+ case RENDERPASS_Z:
+ if(doquery) startvaquery(va, );
+ renderzpass(cur, va);
+ if(doquery) endvaquery(va, );
+ break;
+ }
+}
+
+#define NUMCAUSTICS 32
+
+static Texture *caustictex[NUMCAUSTICS] = { NULL };
+
+void loadcaustics(bool force)
+{
+ static bool needcaustics = false;
+ if(force) needcaustics = true;
+ if(!caustics || !needcaustics) return;
+ useshaderbyname("caustic");
+ if(caustictex[0]) return;
+ loopi(NUMCAUSTICS)
+ {
+ defformatstring(name, "<grey><noswizzle>packages/caustics/caust%.2d.png", i);
+ caustictex[i] = textureload(name);
+ }
+}
+
+void cleanupva()
+{
+ clearvas(worldroot);
+ clearqueries();
+ cleanupbb();
+ cleanupgrass();
+ loopi(NUMCAUSTICS) caustictex[i] = NULL;
+}
+
+VARR(causticscale, 0, 50, 10000);
+VARR(causticmillis, 0, 75, 1000);
+FVARR(causticcontrast, 0, 0.6f, 1);
+VARFP(caustics, 0, 1, 1, loadcaustics());
+
+void setupcaustics(float blend)
+{
+ if(!caustictex[0]) loadcaustics(true);
+
+ vec s = vec(0.011f, 0, 0.0066f).mul(100.0f/causticscale), t = vec(0, 0.011f, 0.0066f).mul(100.0f/causticscale);
+ int tex = (lastmillis/causticmillis)%NUMCAUSTICS;
+ float frac = float(lastmillis%causticmillis)/causticmillis;
+ loopi(2)
+ {
+ glActiveTexture_(GL_TEXTURE0+i);
+ glBindTexture(GL_TEXTURE_2D, caustictex[(tex+i)%NUMCAUSTICS]->id);
+ }
+ glActiveTexture_(GL_TEXTURE0);
+ SETSHADER(caustic);
+ LOCALPARAM(texgenS, s);
+ LOCALPARAM(texgenT, t);
+ blend *= causticcontrast;
+ LOCALPARAMF(frameblend, blend*(1-frac), blend*frac, blend, 1 - blend);
+}
+
+void setupgeom(renderstate &cur)
+{
+ GLOBALPARAMF(colorparams, 2, 2, 2, 1);
+ GLOBALPARAM(camera, camera1->o);
+ GLOBALPARAMF(ambient, ambientcolor.x/255.0f, ambientcolor.y/255.0f, ambientcolor.z/255.0f);
+ GLOBALPARAMF(millis, lastmillis/1000.0f);
+
+ glActiveTexture_(GL_TEXTURE0);
+}
+
+void cleanupgeom(renderstate &cur)
+{
+ if(cur.vattribs) disablevattribs(cur);
+ if(cur.vbuf) disablevbuf(cur);
+}
+
+#define FIRSTVA (reflecting ? reflectedva : visibleva)
+#define NEXTVA (reflecting ? va->rnext : va->next)
+
+static void rendergeommultipass(renderstate &cur, int pass, bool fogpass)
+{
+ if(cur.vbuf) disablevbuf(cur);
+ if(!cur.vattribs) enablevattribs(cur, false);
+ cur.texgendim = -1;
+ for(vtxarray *va = FIRSTVA; va; va = NEXTVA)
+ {
+ if(!va->texs) continue;
+ if(refracting)
+ {
+ if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_GEOM) continue;
+ if(ishiddencube(va->o, va->size)) continue;
+ }
+ else if(reflecting)
+ {
+ if(va->geommax.z <= reflectz) continue;
+ }
+ else if(va->occluded >= OCCLUDE_GEOM) continue;
+ if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue;
+ renderva(cur, va, pass, fogpass);
+ }
+ if(geombatches.length()) renderbatches(cur, pass);
+}
+
+VAR(oqgeom, 0, 1, 1);
+
+void rendergeom(float causticspass, bool fogpass)
+{
+ if(causticspass && (!causticscale || !causticmillis)) causticspass = 0;
+
+ bool mainpass = !reflecting && !refracting && !drawtex && !glaring,
+ doOQ = oqfrags && oqgeom && mainpass,
+ doZP = doOQ && zpass,
+ doSM = shadowmap && !drawtex && !glaring;
+ renderstate cur;
+ if(mainpass)
+ {
+ flipqueries();
+ vtris = vverts = 0;
+ }
+ if(!doZP)
+ {
+ if(shadowmap && mainpass) rendershadowmap();
+ setupgeom(cur);
+ if(doSM) pushshadowmap();
+ }
+
+ finddynlights();
+
+ resetbatches();
+
+ int blends = 0;
+ for(vtxarray *va = FIRSTVA; va; va = NEXTVA)
+ {
+ if(!va->texs) continue;
+ if(refracting)
+ {
+ if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_GEOM) continue;
+ if(ishiddencube(va->o, va->size)) continue;
+ }
+ else if(reflecting)
+ {
+ if(va->geommax.z <= reflectz) continue;
+ }
+ else if(doOQ && (zpass || va->distance > oqdist) && !insideva(va, camera1->o))
+ {
+ if(va->parent && va->parent->occluded >= OCCLUDE_BB)
+ {
+ va->query = NULL;
+ va->occluded = OCCLUDE_PARENT;
+ continue;
+ }
+ va->occluded = va->query && va->query->owner == va && checkquery(va->query) ? min(va->occluded+1, int(OCCLUDE_BB)) : OCCLUDE_NOTHING;
+ va->query = newquery(va);
+ if((!va->query && zpass) || !va->occluded)
+ va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+ if(va->occluded >= OCCLUDE_GEOM)
+ {
+ if(va->query)
+ {
+ if(!zpass && geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+ if(cur.vattribs) disablevattribs(cur, !doZP);
+ if(cur.vbuf) disablevbuf(cur);
+ renderquery(cur, va->query, va);
+ }
+ continue;
+ }
+ }
+ else
+ {
+ va->query = NULL;
+ va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+ if(va->occluded >= OCCLUDE_GEOM) continue;
+ }
+
+ if(!doZP) blends += va->blends;
+ renderva(cur, va, doZP ? RENDERPASS_Z : RENDERPASS_LIGHTMAP, fogpass, doOQ);
+ }
+
+ if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+
+ if(cur.vquery) disablevquery(cur);
+ if(cur.vattribs) disablevattribs(cur, !doZP);
+ if(cur.vbuf) disablevbuf(cur);
+
+ if(!cur.colormask) { cur.colormask = true; glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); }
+ if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); }
+
+ bool multipassing = false;
+
+ if(doZP)
+ {
+ glFlush();
+
+ if(shadowmap && mainpass) rendershadowmap();
+ setupgeom(cur);
+ if(doSM) pushshadowmap();
+
+ if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); }
+ cur.texgendim = -1;
+
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(!va->texs || va->occluded >= OCCLUDE_GEOM) continue;
+ blends += va->blends;
+ renderva(cur, va, RENDERPASS_LIGHTMAP, fogpass);
+ }
+ if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(!va->texs || va->occluded < OCCLUDE_GEOM) continue;
+ else if((va->parent && va->parent->occluded >= OCCLUDE_BB) ||
+ (va->query && checkquery(va->query)))
+ {
+ va->occluded = OCCLUDE_BB;
+ continue;
+ }
+ else
+ {
+ va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+ if(va->occluded >= OCCLUDE_GEOM) continue;
+ }
+
+ blends += va->blends;
+ renderva(cur, va, RENDERPASS_LIGHTMAP, fogpass);
+ }
+ if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+ }
+
+ if(blends)
+ {
+ if(cur.vbuf) disablevbuf(cur);
+
+ if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); }
+ glDepthMask(GL_FALSE);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+ cur.texgendim = -1;
+ cur.blending = true;
+ for(vtxarray *va = FIRSTVA; va; va = NEXTVA)
+ {
+ if(!va->blends) continue;
+ if(refracting)
+ {
+ if(refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) continue;
+ if(ishiddencube(va->o, va->size)) continue;
+ if(va->occluded >= OCCLUDE_GEOM) continue;
+ }
+ else if(reflecting)
+ {
+ if(va->geommax.z <= reflectz) continue;
+ }
+ else if(va->occluded >= OCCLUDE_GEOM) continue;
+ if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue;
+ renderva(cur, va, RENDERPASS_LIGHTMAP_BLEND, fogpass);
+ }
+ if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+ cur.blending = false;
+
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ glDisable(GL_BLEND);
+ glDepthMask(GL_TRUE);
+ }
+
+ if(doSM) popshadowmap();
+
+ if(cur.vattribs) disablevattribs(cur);
+
+ if(foggedvas.length()) renderfoggedvas(cur, doOQ && !zpass);
+
+ if(causticspass)
+ {
+ if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); }
+ glDepthMask(GL_FALSE);
+ glEnable(GL_BLEND);
+
+ setupcaustics(causticspass);
+ glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+ rendergeommultipass(cur, RENDERPASS_CAUSTICS, fogpass);
+ if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+ glDisable(GL_BLEND);
+ glDepthMask(GL_TRUE);
+ }
+
+ if(multipassing) glDepthFunc(GL_LESS);
+
+ cleanupgeom(cur);
+}
+
+void renderalphageom(bool fogpass)
+{
+ static vector<vtxarray *> alphavas;
+ alphavas.setsize(0);
+ bool hasback = false;
+ for(vtxarray *va = FIRSTVA; va; va = NEXTVA)
+ {
+ if(!va->alphatris) continue;
+ if(refracting)
+ {
+ if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_BB) continue;
+ if(ishiddencube(va->o, va->size)) continue;
+ if(va->occluded >= OCCLUDE_GEOM && pvsoccluded(va->geommin, va->geommax)) continue;
+ }
+ else if(reflecting)
+ {
+ if(va->geommax.z <= reflectz) continue;
+ }
+ else
+ {
+ if(va->occluded >= OCCLUDE_BB) continue;
+ if(va->occluded >= OCCLUDE_GEOM && pvsoccluded(va->geommin, va->geommax)) continue;
+ }
+ if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue;
+ alphavas.add(va);
+ if(va->alphabacktris) hasback = true;
+ }
+ if(alphavas.empty()) return;
+
+ resetbatches();
+
+ renderstate cur;
+ cur.alphaing = 1;
+
+ loop(front, 2) if(front || hasback)
+ {
+ cur.alphaing = front+1;
+ if(!front) glCullFace(GL_FRONT);
+ cur.vbuf = 0;
+ cur.texgendim = -1;
+ loopv(alphavas) renderva(cur, alphavas[i], RENDERPASS_Z);
+ if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); }
+ cur.colormask = true;
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+ if(cur.vattribs) disablevattribs(cur, false);
+ if(cur.vbuf) disablevbuf(cur);
+
+ setupgeom(cur);
+
+ glDepthFunc(GL_LEQUAL);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+ cur.vbuf = 0;
+ cur.texgendim = -1;
+ cur.colorscale = vec(1, 1, 1);
+ cur.alphascale = -1;
+ loopv(alphavas) if(front || alphavas[i]->alphabacktris) renderva(cur, alphavas[i], RENDERPASS_LIGHTMAP, fogpass);
+ if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+
+ cleanupgeom(cur);
+
+ resetfogcolor();
+ if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); }
+ glDisable(GL_BLEND);
+ glDepthFunc(GL_LESS);
+ if(!front) glCullFace(GL_BACK);
+ }
+
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, fading ? GL_FALSE : GL_TRUE);
+}
+
+void findreflectedvas(vector<vtxarray *> &vas, int prevvfc = VFC_PART_VISIBLE)
+{
+ loopv(vas)
+ {
+ vtxarray *va = vas[i];
+ if(prevvfc >= VFC_NOT_VISIBLE) va->curvfc = prevvfc;
+ if(va->curvfc == VFC_FOGGED || va->curvfc == PVS_FOGGED || va->o.z+va->size <= reflectz || isfoggedcube(va->o, va->size)) continue;
+ bool render = true;
+ if(va->curvfc == VFC_FULL_VISIBLE)
+ {
+ if(va->occluded >= OCCLUDE_BB) continue;
+ if(va->occluded >= OCCLUDE_GEOM) render = false;
+ }
+ else if(va->curvfc == PVS_FULL_VISIBLE) continue;
+ if(render)
+ {
+ if(va->curvfc >= VFC_NOT_VISIBLE) va->distance = (int)vadist(va, camera1->o);
+ vtxarray **vprev = &reflectedva, *vcur = reflectedva;
+ while(vcur && va->distance > vcur->distance)
+ {
+ vprev = &vcur->rnext;
+ vcur = vcur->rnext;
+ }
+ va->rnext = *vprev;
+ *vprev = va;
+ }
+ if(va->children.length()) findreflectedvas(va->children, va->curvfc);
+ }
+}
+
+void renderreflectedgeom(bool causticspass, bool fogpass)
+{
+ if(reflecting)
+ {
+ reflectedva = NULL;
+ findreflectedvas(varoot);
+ rendergeom(causticspass ? 1 : 0, fogpass);
+ }
+ else rendergeom(causticspass ? 1 : 0, fogpass);
+}
+
+static vtxarray *prevskyva = NULL;
+
+void renderskyva(vtxarray *va, bool explicitonly = false)
+{
+ if(!prevskyva || va->vbuf != prevskyva->vbuf)
+ {
+ gle::bindvbo(va->vbuf);
+ gle::bindebo(va->skybuf);
+ const vertex *ptr = 0;
+ gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+ if(!prevskyva) gle::enablevertex();
+ }
+
+ drawvatris(va, explicitonly ? va->explicitsky : va->sky+va->explicitsky, explicitonly ? va->skydata+va->sky : va->skydata);
+
+ if(!explicitonly) xtraverts += va->sky/3;
+ xtraverts += va->explicitsky/3;
+
+ prevskyva = va;
+}
+
+int renderedsky = 0, renderedexplicitsky = 0, renderedskyfaces = 0, renderedskyclip = INT_MAX;
+
+static inline void updateskystats(vtxarray *va)
+{
+ renderedsky += va->sky;
+ renderedexplicitsky += va->explicitsky;
+ renderedskyfaces |= va->skyfaces&0x3F;
+ if(!(va->skyfaces&0x1F) || camera1->o.z < va->skyclip) renderedskyclip = min(renderedskyclip, va->skyclip);
+ else renderedskyclip = 0;
+}
+
+void renderreflectedskyvas(vector<vtxarray *> &vas, int prevvfc = VFC_PART_VISIBLE)
+{
+ loopv(vas)
+ {
+ vtxarray *va = vas[i];
+ if(prevvfc >= VFC_NOT_VISIBLE) va->curvfc = prevvfc;
+ if((va->curvfc == VFC_FULL_VISIBLE && va->occluded >= OCCLUDE_BB) || va->curvfc==PVS_FULL_VISIBLE) continue;
+ if(va->o.z+va->size <= reflectz || ishiddencube(va->o, va->size)) continue;
+ if(va->sky+va->explicitsky)
+ {
+ updateskystats(va);
+ renderskyva(va);
+ }
+ if(va->children.length()) renderreflectedskyvas(va->children, va->curvfc);
+ }
+}
+
+bool rendersky(bool explicitonly)
+{
+ prevskyva = NULL;
+ renderedsky = renderedexplicitsky = renderedskyfaces = 0;
+ renderedskyclip = INT_MAX;
+
+ if(reflecting)
+ {
+ renderreflectedskyvas(varoot);
+ }
+ else for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if((va->occluded >= OCCLUDE_BB && va->skyfaces&0x80) || !(va->sky+va->explicitsky)) continue;
+
+ // count possibly visible sky even if not actually rendered
+ updateskystats(va);
+ if(explicitonly && !va->explicitsky) continue;
+ renderskyva(va, explicitonly);
+ }
+
+ if(prevskyva)
+ {
+ gle::disablevertex();
+ gle::clearvbo();
+ gle::clearebo();
+ }
+
+ return renderedsky+renderedexplicitsky > 0;
+}
+
diff --git a/src/engine/server.cpp b/src/engine/server.cpp
new file mode 100644
index 0000000..e76c67d
--- /dev/null
+++ b/src/engine/server.cpp
@@ -0,0 +1,1154 @@
+// server.cpp: little more than enhanced multicaster
+// runs dedicated or as client coroutine
+
+#include "engine.h"
+
+#define LOGSTRLEN 512
+
+static FILE *logfile = NULL;
+
+void closelogfile()
+{
+ if(logfile)
+ {
+ fclose(logfile);
+ logfile = NULL;
+ }
+}
+
+FILE *getlogfile()
+{
+#ifdef WIN32
+ return logfile;
+#else
+ return logfile ? logfile : stdout;
+#endif
+}
+
+void setlogfile(const char *fname)
+{
+ closelogfile();
+ if(fname && fname[0])
+ {
+ fname = findfile(fname, "w");
+ if(fname) logfile = fopen(fname, "w");
+ }
+ FILE *f = getlogfile();
+ if(f) setvbuf(f, NULL, _IOLBF, BUFSIZ);
+}
+
+void logoutf(const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ logoutfv(fmt, args);
+ va_end(args);
+}
+
+
+static void writelog(FILE *file, const char *buf)
+{
+ static uchar ubuf[512];
+ size_t len = strlen(buf), carry = 0;
+ while(carry < len)
+ {
+ size_t numu = encodeutf8(ubuf, sizeof(ubuf)-1, &((const uchar *)buf)[carry], len - carry, &carry);
+ if(carry >= len) ubuf[numu++] = '\n';
+ fwrite(ubuf, 1, numu, file);
+ }
+}
+
+static void writelogv(FILE *file, const char *fmt, va_list args)
+{
+ static char buf[LOGSTRLEN];
+ vformatstring(buf, fmt, args, sizeof(buf));
+ writelog(file, buf);
+}
+
+#ifdef STANDALONE
+void fatal(const char *fmt, ...)
+{
+ void cleanupserver();
+ cleanupserver();
+ defvformatstring(msg,fmt,fmt);
+ if(logfile) logoutf("%s", msg);
+#ifdef WIN32
+ MessageBox(NULL, msg, "Cube 2: Sauerbraten fatal error", MB_OK|MB_SYSTEMMODAL);
+#else
+ fprintf(stderr, "server error: %s\n", msg);
+#endif
+ closelogfile();
+ exit(EXIT_FAILURE);
+}
+
+void conoutfv(int type, const char *fmt, va_list args)
+{
+ logoutfv(fmt, args);
+}
+#endif
+
+#define DEFAULTCLIENTS 8
+
+enum { ST_EMPTY, ST_LOCAL, ST_TCPIP };
+
+struct client // server side version of "dynent" type
+{
+ int type;
+ int num;
+ ENetPeer *peer;
+ string hostname;
+ void *info;
+};
+
+vector<client *> clients;
+
+ENetHost *serverhost = NULL;
+int laststatus = 0;
+ENetSocket pongsock = ENET_SOCKET_NULL, lansock = ENET_SOCKET_NULL;
+
+int localclients = 0, nonlocalclients = 0;
+
+bool hasnonlocalclients() { return nonlocalclients!=0; }
+bool haslocalclients() { return localclients!=0; }
+
+client &addclient(int type)
+{
+ client *c = NULL;
+ loopv(clients) if(clients[i]->type==ST_EMPTY)
+ {
+ c = clients[i];
+ break;
+ }
+ if(!c)
+ {
+ c = new client;
+ c->num = clients.length();
+ clients.add(c);
+ }
+ c->info = server::newclientinfo();
+ c->type = type;
+ switch(type)
+ {
+ case ST_TCPIP: nonlocalclients++; break;
+ case ST_LOCAL: localclients++; break;
+ }
+ return *c;
+}
+
+void delclient(client *c)
+{
+ if(!c) return;
+ switch(c->type)
+ {
+ case ST_TCPIP: nonlocalclients--; if(c->peer) c->peer->data = NULL; break;
+ case ST_LOCAL: localclients--; break;
+ case ST_EMPTY: return;
+ }
+ c->type = ST_EMPTY;
+ c->peer = NULL;
+ if(c->info)
+ {
+ server::deleteclientinfo(c->info);
+ c->info = NULL;
+ }
+}
+
+void cleanupserver()
+{
+ if(serverhost) enet_host_destroy(serverhost);
+ serverhost = NULL;
+
+ if(pongsock != ENET_SOCKET_NULL) enet_socket_destroy(pongsock);
+ if(lansock != ENET_SOCKET_NULL) enet_socket_destroy(lansock);
+ pongsock = lansock = ENET_SOCKET_NULL;
+}
+
+VARF(maxclients, 0, DEFAULTCLIENTS, MAXCLIENTS, { if(!maxclients) maxclients = DEFAULTCLIENTS; });
+VARF(maxdupclients, 0, 0, MAXCLIENTS, { if(serverhost) serverhost->duplicatePeers = maxdupclients ? maxdupclients : MAXCLIENTS; });
+
+void process(ENetPacket *packet, int sender, int chan);
+//void disconnect_client(int n, int reason);
+
+int getservermtu() { return serverhost ? serverhost->mtu : -1; }
+void *getclientinfo(int i) { return !clients.inrange(i) || clients[i]->type==ST_EMPTY ? NULL : clients[i]->info; }
+ENetPeer *getclientpeer(int i) { return clients.inrange(i) && clients[i]->type==ST_TCPIP ? clients[i]->peer : NULL; }
+int getnumclients() { return clients.length(); }
+uint getclientip(int n) { return clients.inrange(n) && clients[n]->type==ST_TCPIP ? clients[n]->peer->address.host : 0; }
+
+void sendpacket(int n, int chan, ENetPacket *packet, int exclude)
+{
+ if(n<0)
+ {
+ server::recordpacket(chan, packet->data, packet->dataLength);
+ loopv(clients) if(i!=exclude && server::allowbroadcast(i)) sendpacket(i, chan, packet);
+ return;
+ }
+ switch(clients[n]->type)
+ {
+ case ST_TCPIP:
+ {
+ enet_peer_send(clients[n]->peer, chan, packet);
+ break;
+ }
+
+#ifndef STANDALONE
+ case ST_LOCAL:
+ localservertoclient(chan, packet);
+ break;
+#endif
+ }
+}
+
+ENetPacket *sendf(int cn, int chan, const char *format, ...)
+{
+ int exclude = -1;
+ bool reliable = false;
+ if(*format=='r') { reliable = true; ++format; }
+ packetbuf p(MAXTRANS, reliable ? ENET_PACKET_FLAG_RELIABLE : 0);
+ va_list args;
+ va_start(args, format);
+ while(*format) switch(*format++)
+ {
+ case 'x':
+ exclude = va_arg(args, int);
+ break;
+
+ case 'v':
+ {
+ int n = va_arg(args, int);
+ int *v = va_arg(args, int *);
+ loopi(n) putint(p, v[i]);
+ break;
+ }
+
+ case 'i':
+ {
+ int n = isdigit(*format) ? *format++-'0' : 1;
+ loopi(n) putint(p, va_arg(args, int));
+ break;
+ }
+ case 'f':
+ {
+ int n = isdigit(*format) ? *format++-'0' : 1;
+ loopi(n) putfloat(p, (float)va_arg(args, double));
+ break;
+ }
+ case 's': sendstring(va_arg(args, const char *), p); break;
+ case 'm':
+ {
+ int n = va_arg(args, int);
+ p.put(va_arg(args, uchar *), n);
+ break;
+ }
+ }
+ va_end(args);
+ ENetPacket *packet = p.finalize();
+ sendpacket(cn, chan, packet, exclude);
+ return packet->referenceCount > 0 ? packet : NULL;
+}
+
+ENetPacket *sendfile(int cn, int chan, stream *file, const char *format, ...)
+{
+ if(cn < 0)
+ {
+#ifdef STANDALONE
+ return NULL;
+#endif
+ }
+ else if(!clients.inrange(cn)) return NULL;
+
+ int len = (int)min(file->size(), stream::offset(INT_MAX));
+ if(len <= 0 || len > 16<<20) return NULL;
+
+ packetbuf p(MAXTRANS+len, ENET_PACKET_FLAG_RELIABLE);
+ va_list args;
+ va_start(args, format);
+ while(*format) switch(*format++)
+ {
+ case 'i':
+ {
+ int n = isdigit(*format) ? *format++-'0' : 1;
+ loopi(n) putint(p, va_arg(args, int));
+ break;
+ }
+ case 's': sendstring(va_arg(args, const char *), p); break;
+ case 'l': putint(p, len); break;
+ }
+ va_end(args);
+
+ file->seek(0, SEEK_SET);
+ file->read(p.subbuf(len).buf, len);
+
+ ENetPacket *packet = p.finalize();
+ if(cn >= 0) sendpacket(cn, chan, packet, -1);
+#ifndef STANDALONE
+ else sendclientpacket(packet, chan);
+#endif
+ return packet->referenceCount > 0 ? packet : NULL;
+}
+
+const char *disconnectreason(int reason)
+{
+ switch(reason)
+ {
+ case DISC_EOP: return "end of packet";
+ case DISC_LOCAL: return "server is in local mode";
+ case DISC_KICK: return "kicked/banned";
+ case DISC_MSGERR: return "message error";
+ case DISC_IPBAN: return "ip is banned";
+ case DISC_PRIVATE: return "server is in private mode";
+ case DISC_MAXCLIENTS: return "server FULL";
+ case DISC_TIMEOUT: return "connection timed out";
+ case DISC_OVERFLOW: return "overflow";
+ case DISC_PASSWORD: return "invalid password";
+ default: return NULL;
+ }
+}
+
+void disconnect_client(int n, int reason)
+{
+ if(!clients.inrange(n) || clients[n]->type!=ST_TCPIP) return;
+ enet_peer_disconnect(clients[n]->peer, reason);
+ server::clientdisconnect(n);
+ delclient(clients[n]);
+ const char *msg = disconnectreason(reason);
+ string s;
+ if(msg) formatstring(s, "client (%s) disconnected because: %s", clients[n]->hostname, msg);
+ else formatstring(s, "client (%s) disconnected", clients[n]->hostname);
+ logoutf("%s", s);
+ server::sendservmsg(s);
+}
+
+void kicknonlocalclients(int reason)
+{
+ loopv(clients) if(clients[i]->type==ST_TCPIP) disconnect_client(i, reason);
+}
+
+void process(ENetPacket *packet, int sender, int chan) // sender may be -1
+{
+ packetbuf p(packet);
+ server::parsepacket(sender, chan, p);
+ if(p.overread()) { disconnect_client(sender, DISC_EOP); return; }
+}
+
+void localclienttoserver(int chan, ENetPacket *packet)
+{
+ client *c = NULL;
+ loopv(clients) if(clients[i]->type==ST_LOCAL) { c = clients[i]; break; }
+ if(c) process(packet, c->num, chan);
+}
+
+#ifdef STANDALONE
+bool resolverwait(const char *name, ENetAddress *address)
+{
+ return enet_address_set_host(address, name) >= 0;
+}
+
+int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &remoteaddress)
+{
+ return enet_socket_connect(sock, &remoteaddress);
+}
+#endif
+
+ENetSocket mastersock = ENET_SOCKET_NULL;
+ENetAddress masteraddress = { ENET_HOST_ANY, ENET_PORT_ANY }, serveraddress = { ENET_HOST_ANY, ENET_PORT_ANY };
+int lastupdatemaster = 0, lastconnectmaster = 0, masterconnecting = 0, masterconnected = 0;
+vector<char> masterout, masterin;
+int masteroutpos = 0, masterinpos = 0;
+VARN(updatemaster, allowupdatemaster, 0, 1, 1);
+
+void disconnectmaster()
+{
+ if(mastersock != ENET_SOCKET_NULL)
+ {
+ server::masterdisconnected();
+ enet_socket_destroy(mastersock);
+ mastersock = ENET_SOCKET_NULL;
+ }
+
+ masterout.setsize(0);
+ masterin.setsize(0);
+ masteroutpos = masterinpos = 0;
+
+ masteraddress.host = ENET_HOST_ANY;
+ masteraddress.port = ENET_PORT_ANY;
+
+ lastupdatemaster = masterconnecting = masterconnected = 0;
+}
+
+SVARF(mastername, server::defaultmaster(), disconnectmaster());
+VARF(masterport, 1, server::masterport(), 0xFFFF, disconnectmaster());
+
+ENetSocket connectmaster(bool wait)
+{
+ if(!mastername[0]) return ENET_SOCKET_NULL;
+ if(masteraddress.host == ENET_HOST_ANY)
+ {
+ if(isdedicatedserver()) logoutf("looking up %s...", mastername);
+ masteraddress.port = masterport;
+ if(!resolverwait(mastername, &masteraddress)) return ENET_SOCKET_NULL;
+ }
+ ENetSocket sock = enet_socket_create(ENET_SOCKET_TYPE_STREAM);
+ if(sock == ENET_SOCKET_NULL)
+ {
+ if(isdedicatedserver()) logoutf("could not open master server socket");
+ return ENET_SOCKET_NULL;
+ }
+ if(wait || serveraddress.host == ENET_HOST_ANY || !enet_socket_bind(sock, &serveraddress))
+ {
+ enet_socket_set_option(sock, ENET_SOCKOPT_NONBLOCK, 1);
+ if(wait)
+ {
+ if(!connectwithtimeout(sock, mastername, masteraddress)) return sock;
+ }
+ else if(!enet_socket_connect(sock, &masteraddress)) return sock;
+ }
+ enet_socket_destroy(sock);
+ if(isdedicatedserver()) logoutf("could not connect to master server");
+ return ENET_SOCKET_NULL;
+}
+
+bool requestmaster(const char *req)
+{
+ if(mastersock == ENET_SOCKET_NULL)
+ {
+ mastersock = connectmaster(false);
+ if(mastersock == ENET_SOCKET_NULL) return false;
+ lastconnectmaster = masterconnecting = totalmillis ? totalmillis : 1;
+ }
+
+ if(masterout.length() >= 4096) return false;
+
+ masterout.put(req, strlen(req));
+ return true;
+}
+
+bool requestmasterf(const char *fmt, ...)
+{
+ defvformatstring(req, fmt, fmt);
+ return requestmaster(req);
+}
+
+void processmasterinput()
+{
+ if(masterinpos >= masterin.length()) return;
+
+ char *input = &masterin[masterinpos], *end = (char *)memchr(input, '\n', masterin.length() - masterinpos);
+ while(end)
+ {
+ *end = '\0';
+
+ const char *args = input;
+ while(args < end && !iscubespace(*args)) args++;
+ int cmdlen = args - input;
+ while(args < end && iscubespace(*args)) args++;
+
+ if(matchstring(input, cmdlen, "failreg"))
+ conoutf(CON_ERROR, "master server registration failed: %s", args);
+ else if(matchstring(input, cmdlen, "succreg"))
+ conoutf("master server registration succeeded");
+ else server::processmasterinput(input, cmdlen, args);
+
+ end++;
+ masterinpos = end - masterin.getbuf();
+ input = end;
+ end = (char *)memchr(input, '\n', masterin.length() - masterinpos);
+ }
+
+ if(masterinpos >= masterin.length())
+ {
+ masterin.setsize(0);
+ masterinpos = 0;
+ }
+}
+
+void flushmasteroutput()
+{
+ if(masterconnecting && totalmillis - masterconnecting >= 60000)
+ {
+ logoutf("could not connect to master server");
+ disconnectmaster();
+ }
+ if(masterout.empty() || !masterconnected) return;
+
+ ENetBuffer buf;
+ buf.data = &masterout[masteroutpos];
+ buf.dataLength = masterout.length() - masteroutpos;
+ int sent = enet_socket_send(mastersock, NULL, &buf, 1);
+ if(sent >= 0)
+ {
+ masteroutpos += sent;
+ if(masteroutpos >= masterout.length())
+ {
+ masterout.setsize(0);
+ masteroutpos = 0;
+ }
+ }
+ else disconnectmaster();
+}
+
+void flushmasterinput()
+{
+ if(masterin.length() >= masterin.capacity())
+ masterin.reserve(4096);
+
+ ENetBuffer buf;
+ buf.data = masterin.getbuf() + masterin.length();
+ buf.dataLength = masterin.capacity() - masterin.length();
+ int recv = enet_socket_receive(mastersock, NULL, &buf, 1);
+ if(recv > 0)
+ {
+ masterin.advance(recv);
+ processmasterinput();
+ }
+ else disconnectmaster();
+}
+
+static ENetAddress pongaddr;
+
+void sendserverinforeply(ucharbuf &p)
+{
+ ENetBuffer buf;
+ buf.data = p.buf;
+ buf.dataLength = p.length();
+ enet_socket_send(pongsock, &pongaddr, &buf, 1);
+}
+
+#define MAXPINGDATA 32
+
+void checkserversockets() // reply all server info requests
+{
+ static ENetSocketSet readset, writeset;
+ ENET_SOCKETSET_EMPTY(readset);
+ ENET_SOCKETSET_EMPTY(writeset);
+ ENetSocket maxsock = pongsock;
+ ENET_SOCKETSET_ADD(readset, pongsock);
+ if(mastersock != ENET_SOCKET_NULL)
+ {
+ maxsock = max(maxsock, mastersock);
+ ENET_SOCKETSET_ADD(readset, mastersock);
+ if(!masterconnected) ENET_SOCKETSET_ADD(writeset, mastersock);
+ }
+ if(lansock != ENET_SOCKET_NULL)
+ {
+ maxsock = max(maxsock, lansock);
+ ENET_SOCKETSET_ADD(readset, lansock);
+ }
+ if(enet_socketset_select(maxsock, &readset, &writeset, 0) <= 0) return;
+
+ ENetBuffer buf;
+ uchar pong[MAXTRANS];
+ loopi(2)
+ {
+ ENetSocket sock = i ? lansock : pongsock;
+ if(sock == ENET_SOCKET_NULL || !ENET_SOCKETSET_CHECK(readset, sock)) continue;
+
+ buf.data = pong;
+ buf.dataLength = sizeof(pong);
+ int len = enet_socket_receive(sock, &pongaddr, &buf, 1);
+ if(len < 0 || len > MAXPINGDATA) continue;
+ ucharbuf req(pong, len), p(pong, sizeof(pong));
+ p.len += len;
+ server::serverinforeply(req, p);
+ }
+
+ if(mastersock != ENET_SOCKET_NULL)
+ {
+ if(!masterconnected)
+ {
+ if(ENET_SOCKETSET_CHECK(readset, mastersock) || ENET_SOCKETSET_CHECK(writeset, mastersock))
+ {
+ int error = 0;
+ if(enet_socket_get_option(mastersock, ENET_SOCKOPT_ERROR, &error) < 0 || error)
+ {
+ logoutf("could not connect to master server");
+ disconnectmaster();
+ }
+ else
+ {
+ masterconnecting = 0;
+ masterconnected = totalmillis ? totalmillis : 1;
+ server::masterconnected();
+ }
+ }
+ }
+ if(mastersock != ENET_SOCKET_NULL && ENET_SOCKETSET_CHECK(readset, mastersock)) flushmasterinput();
+ }
+}
+
+VAR(serveruprate, 0, 0, INT_MAX);
+SVAR(serverip, "");
+VARF(serverport, 0, server::serverport(), 0xFFFF-1, { if(!serverport) serverport = server::serverport(); });
+
+#ifdef STANDALONE
+int curtime = 0, lastmillis = 0, elapsedtime = 0, totalmillis = 0;
+#endif
+
+void updatemasterserver()
+{
+ if(!masterconnected && lastconnectmaster && totalmillis-lastconnectmaster <= 5*60*1000) return;
+ if(mastername[0] && allowupdatemaster) requestmasterf("regserv %d\n", serverport);
+ lastupdatemaster = totalmillis ? totalmillis : 1;
+}
+
+uint totalsecs = 0;
+
+void updatetime()
+{
+ static int lastsec = 0;
+ if(totalmillis - lastsec >= 1000)
+ {
+ int cursecs = (totalmillis - lastsec) / 1000;
+ totalsecs += cursecs;
+ lastsec += cursecs * 1000;
+ }
+}
+
+void serverslice(bool dedicated, uint timeout) // main server update, called from main loop in sp, or from below in dedicated server
+{
+ if(!serverhost)
+ {
+ server::serverupdate();
+ server::sendpackets();
+ return;
+ }
+
+ // below is network only
+
+ if(dedicated)
+ {
+ int millis = (int)enet_time_get();
+ elapsedtime = millis - totalmillis;
+ static int timeerr = 0;
+ int scaledtime = server::scaletime(elapsedtime) + timeerr;
+ curtime = scaledtime/100;
+ timeerr = scaledtime%100;
+ if(server::ispaused()) curtime = 0;
+ lastmillis += curtime;
+ totalmillis = millis;
+ updatetime();
+ }
+ server::serverupdate();
+
+ flushmasteroutput();
+ checkserversockets();
+
+ if(!lastupdatemaster || totalmillis-lastupdatemaster>60*60*1000) // send alive signal to masterserver every hour of uptime
+ updatemasterserver();
+
+ if(totalmillis-laststatus>60*1000) // display bandwidth stats, useful for server ops
+ {
+ laststatus = totalmillis;
+ if(nonlocalclients || serverhost->totalSentData || serverhost->totalReceivedData) logoutf("status: %d remote clients, %.1f send, %.1f rec (K/sec)", nonlocalclients, serverhost->totalSentData/60.0f/1024, serverhost->totalReceivedData/60.0f/1024);
+ serverhost->totalSentData = serverhost->totalReceivedData = 0;
+ }
+
+ ENetEvent event;
+ bool serviced = false;
+ while(!serviced)
+ {
+ if(enet_host_check_events(serverhost, &event) <= 0)
+ {
+ if(enet_host_service(serverhost, &event, timeout) <= 0) break;
+ serviced = true;
+ }
+ switch(event.type)
+ {
+ case ENET_EVENT_TYPE_CONNECT:
+ {
+ client &c = addclient(ST_TCPIP);
+ c.peer = event.peer;
+ c.peer->data = &c;
+ string hn;
+ copystring(c.hostname, (enet_address_get_host_ip(&c.peer->address, hn, sizeof(hn))==0) ? hn : "unknown");
+ logoutf("client connected (%s)", c.hostname);
+ int reason = server::clientconnect(c.num, c.peer->address.host);
+ if(reason) disconnect_client(c.num, reason);
+ break;
+ }
+ case ENET_EVENT_TYPE_RECEIVE:
+ {
+ client *c = (client *)event.peer->data;
+ if(c) process(event.packet, c->num, event.channelID);
+ if(event.packet->referenceCount==0) enet_packet_destroy(event.packet);
+ break;
+ }
+ case ENET_EVENT_TYPE_DISCONNECT:
+ {
+ client *c = (client *)event.peer->data;
+ if(!c) break;
+ logoutf("disconnected client (%s)", c->hostname);
+ server::clientdisconnect(c->num);
+ delclient(c);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ if(server::sendpackets()) enet_host_flush(serverhost);
+}
+
+void flushserver(bool force)
+{
+ if(server::sendpackets(force) && serverhost) enet_host_flush(serverhost);
+}
+
+#ifndef STANDALONE
+void localdisconnect(bool cleanup)
+{
+ bool disconnected = false;
+ loopv(clients) if(clients[i]->type==ST_LOCAL)
+ {
+ server::localdisconnect(i);
+ delclient(clients[i]);
+ disconnected = true;
+ }
+ if(!disconnected) return;
+ game::gamedisconnect(cleanup);
+ mainmenu = 1;
+}
+
+void localconnect()
+{
+ if(initing) return;
+ client &c = addclient(ST_LOCAL);
+ copystring(c.hostname, "local");
+ game::gameconnect(false);
+ server::localconnect(c.num);
+}
+#endif
+
+#ifdef WIN32
+#include "shellapi.h"
+
+#define IDI_ICON1 1
+
+static string apptip = "";
+static HINSTANCE appinstance = NULL;
+static ATOM wndclass = 0;
+static HWND appwindow = NULL, conwindow = NULL;
+static HICON appicon = NULL;
+static HMENU appmenu = NULL;
+static HANDLE outhandle = NULL;
+static const int MAXLOGLINES = 200;
+struct logline { int len; char buf[LOGSTRLEN]; };
+static queue<logline, MAXLOGLINES> loglines;
+
+static void cleanupsystemtray()
+{
+ NOTIFYICONDATA nid;
+ memset(&nid, 0, sizeof(nid));
+ nid.cbSize = sizeof(nid);
+ nid.hWnd = appwindow;
+ nid.uID = IDI_ICON1;
+ Shell_NotifyIcon(NIM_DELETE, &nid);
+}
+
+static bool setupsystemtray(UINT uCallbackMessage)
+{
+ NOTIFYICONDATA nid;
+ memset(&nid, 0, sizeof(nid));
+ nid.cbSize = sizeof(nid);
+ nid.hWnd = appwindow;
+ nid.uID = IDI_ICON1;
+ nid.uCallbackMessage = uCallbackMessage;
+ nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
+ nid.hIcon = appicon;
+ strcpy(nid.szTip, apptip);
+ if(Shell_NotifyIcon(NIM_ADD, &nid) != TRUE)
+ return false;
+ atexit(cleanupsystemtray);
+ return true;
+}
+
+#if 0
+static bool modifysystemtray()
+{
+ NOTIFYICONDATA nid;
+ memset(&nid, 0, sizeof(nid));
+ nid.cbSize = sizeof(nid);
+ nid.hWnd = appwindow;
+ nid.uID = IDI_ICON1;
+ nid.uFlags = NIF_TIP;
+ strcpy(nid.szTip, apptip);
+ return Shell_NotifyIcon(NIM_MODIFY, &nid) == TRUE;
+}
+#endif
+
+static void cleanupwindow()
+{
+ if(!appinstance) return;
+ if(appmenu)
+ {
+ DestroyMenu(appmenu);
+ appmenu = NULL;
+ }
+ if(wndclass)
+ {
+ UnregisterClass(MAKEINTATOM(wndclass), appinstance);
+ wndclass = 0;
+ }
+}
+
+static BOOL WINAPI consolehandler(DWORD dwCtrlType)
+{
+ switch(dwCtrlType)
+ {
+ case CTRL_C_EVENT:
+ case CTRL_BREAK_EVENT:
+ case CTRL_CLOSE_EVENT:
+ exit(EXIT_SUCCESS);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void writeline(logline &line)
+{
+ static uchar ubuf[512];
+ size_t len = strlen(line.buf), carry = 0;
+ while(carry < len)
+ {
+ size_t numu = encodeutf8(ubuf, sizeof(ubuf), &((uchar *)line.buf)[carry], len - carry, &carry);
+ DWORD written = 0;
+ WriteConsole(outhandle, ubuf, numu, &written, NULL);
+ }
+}
+
+static void setupconsole()
+{
+ if(conwindow) return;
+ if(!AllocConsole()) return;
+ SetConsoleCtrlHandler(consolehandler, TRUE);
+ conwindow = GetConsoleWindow();
+ SetConsoleTitle(apptip);
+ //SendMessage(conwindow, WM_SETICON, ICON_SMALL, (LPARAM)appicon);
+ SendMessage(conwindow, WM_SETICON, ICON_BIG, (LPARAM)appicon);
+ outhandle = GetStdHandle(STD_OUTPUT_HANDLE);
+ CONSOLE_SCREEN_BUFFER_INFO coninfo;
+ GetConsoleScreenBufferInfo(outhandle, &coninfo);
+ coninfo.dwSize.Y = MAXLOGLINES;
+ SetConsoleScreenBufferSize(outhandle, coninfo.dwSize);
+ SetConsoleCP(CP_UTF8);
+ SetConsoleOutputCP(CP_UTF8);
+ loopv(loglines) writeline(loglines[i]);
+}
+
+enum
+{
+ MENU_OPENCONSOLE = 0,
+ MENU_SHOWCONSOLE,
+ MENU_HIDECONSOLE,
+ MENU_EXIT
+};
+
+static LRESULT CALLBACK handlemessages(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ switch(uMsg)
+ {
+ case WM_APP:
+ SetForegroundWindow(hWnd);
+ switch(lParam)
+ {
+ //case WM_MOUSEMOVE:
+ // break;
+ case WM_LBUTTONUP:
+ case WM_RBUTTONUP:
+ {
+ POINT pos;
+ GetCursorPos(&pos);
+ TrackPopupMenu(appmenu, TPM_CENTERALIGN|TPM_BOTTOMALIGN|TPM_RIGHTBUTTON, pos.x, pos.y, 0, hWnd, NULL);
+ PostMessage(hWnd, WM_NULL, 0, 0);
+ break;
+ }
+ }
+ return 0;
+ case WM_COMMAND:
+ switch(LOWORD(wParam))
+ {
+ case MENU_OPENCONSOLE:
+ setupconsole();
+ if(conwindow) ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_HIDECONSOLE, "Hide Console");
+ break;
+ case MENU_SHOWCONSOLE:
+ ShowWindow(conwindow, SW_SHOWNORMAL);
+ ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_HIDECONSOLE, "Hide Console");
+ break;
+ case MENU_HIDECONSOLE:
+ ShowWindow(conwindow, SW_HIDE);
+ ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_SHOWCONSOLE, "Show Console");
+ break;
+ case MENU_EXIT:
+ PostMessage(hWnd, WM_CLOSE, 0, 0);
+ break;
+ }
+ return 0;
+ case WM_CLOSE:
+ PostQuitMessage(0);
+ return 0;
+ }
+ return DefWindowProc(hWnd, uMsg, wParam, lParam);
+}
+
+static void setupwindow(const char *title)
+{
+ copystring(apptip, title);
+ //appinstance = GetModuleHandle(NULL);
+ if(!appinstance) fatal("failed getting application instance");
+ appicon = LoadIcon(appinstance, MAKEINTRESOURCE(IDI_ICON1));//(HICON)LoadImage(appinstance, MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE);
+ if(!appicon) fatal("failed loading icon");
+
+ appmenu = CreatePopupMenu();
+ if(!appmenu) fatal("failed creating popup menu");
+ AppendMenu(appmenu, MF_STRING, MENU_OPENCONSOLE, "Open Console");
+ AppendMenu(appmenu, MF_SEPARATOR, 0, NULL);
+ AppendMenu(appmenu, MF_STRING, MENU_EXIT, "Exit");
+ //SetMenuDefaultItem(appmenu, 0, FALSE);
+
+ WNDCLASS wc;
+ memset(&wc, 0, sizeof(wc));
+ wc.hCursor = NULL; //LoadCursor(NULL, IDC_ARROW);
+ wc.hIcon = appicon;
+ wc.lpszMenuName = NULL;
+ wc.lpszClassName = title;
+ wc.style = 0;
+ wc.hInstance = appinstance;
+ wc.lpfnWndProc = handlemessages;
+ wc.cbWndExtra = 0;
+ wc.cbClsExtra = 0;
+ wndclass = RegisterClass(&wc);
+ if(!wndclass) fatal("failed registering window class");
+
+ appwindow = CreateWindow(MAKEINTATOM(wndclass), title, 0, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, HWND_MESSAGE, NULL, appinstance, NULL);
+ if(!appwindow) fatal("failed creating window");
+
+ atexit(cleanupwindow);
+
+ if(!setupsystemtray(WM_APP)) fatal("failed adding to system tray");
+}
+
+static char *parsecommandline(const char *src, vector<char *> &args)
+{
+ char *buf = new char[strlen(src) + 1], *dst = buf;
+ for(;;)
+ {
+ while(isspace(*src)) src++;
+ if(!*src) break;
+ args.add(dst);
+ for(bool quoted = false; *src && (quoted || !isspace(*src)); src++)
+ {
+ if(*src != '"') *dst++ = *src;
+ else if(dst > buf && src[-1] == '\\') dst[-1] = '"';
+ else quoted = !quoted;
+ }
+ *dst++ = '\0';
+ }
+ args.add(NULL);
+ return buf;
+}
+
+
+int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int sw)
+{
+ vector<char *> args;
+ char *buf = parsecommandline(GetCommandLine(), args);
+ appinstance = hInst;
+#ifdef STANDALONE
+ int standalonemain(int argc, char **argv);
+ int status = standalonemain(args.length()-1, args.getbuf());
+ #define main standalonemain
+#else
+ SDL_SetMainReady();
+ int status = SDL_main(args.length()-1, args.getbuf());
+#endif
+ delete[] buf;
+ exit(status);
+ return 0;
+}
+
+void logoutfv(const char *fmt, va_list args)
+{
+ if(appwindow)
+ {
+ logline &line = loglines.add();
+ vformatstring(line.buf, fmt, args, sizeof(line.buf));
+ if(logfile) writelog(logfile, line.buf);
+ line.len = min(strlen(line.buf), sizeof(line.buf)-2);
+ line.buf[line.len++] = '\n';
+ line.buf[line.len] = '\0';
+ if(outhandle) writeline(line);
+ }
+ else if(logfile) writelogv(logfile, fmt, args);
+}
+
+#else
+
+void logoutfv(const char *fmt, va_list args)
+{
+ FILE *f = getlogfile();
+ if(f) writelogv(f, fmt, args);
+}
+
+#endif
+
+static bool dedicatedserver = false;
+
+bool isdedicatedserver() { return dedicatedserver; }
+
+void rundedicatedserver()
+{
+ dedicatedserver = true;
+ logoutf("dedicated server started, waiting for clients...");
+#ifdef WIN32
+ SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
+ for(;;)
+ {
+ MSG msg;
+ while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
+ {
+ if(msg.message == WM_QUIT) exit(EXIT_SUCCESS);
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ serverslice(true, 5);
+ }
+#else
+ for(;;) serverslice(true, 5);
+#endif
+ dedicatedserver = false;
+}
+
+bool servererror(bool dedicated, const char *desc)
+{
+#ifndef STANDALONE
+ if(!dedicated)
+ {
+ conoutf(CON_ERROR, "%s", desc);
+ cleanupserver();
+ }
+ else
+#endif
+ fatal("%s", desc);
+ return false;
+}
+
+bool setuplistenserver(bool dedicated)
+{
+ ENetAddress address = { ENET_HOST_ANY, enet_uint16(serverport <= 0 ? server::serverport() : serverport) };
+ if(*serverip)
+ {
+ if(enet_address_set_host(&address, serverip)<0) conoutf(CON_WARN, "WARNING: server ip not resolved");
+ else serveraddress.host = address.host;
+ }
+ serverhost = enet_host_create(&address, min(maxclients + server::reserveclients(), MAXCLIENTS), server::numchannels(), 0, serveruprate);
+ if(!serverhost) return servererror(dedicated, "could not create server host");
+ serverhost->duplicatePeers = maxdupclients ? maxdupclients : MAXCLIENTS;
+ address.port = server::serverinfoport(serverport > 0 ? serverport : -1);
+ pongsock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+ if(pongsock != ENET_SOCKET_NULL && enet_socket_bind(pongsock, &address) < 0)
+ {
+ enet_socket_destroy(pongsock);
+ pongsock = ENET_SOCKET_NULL;
+ }
+ if(pongsock == ENET_SOCKET_NULL) return servererror(dedicated, "could not create server info socket");
+ else enet_socket_set_option(pongsock, ENET_SOCKOPT_NONBLOCK, 1);
+ address.port = server::laninfoport();
+ lansock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+ if(lansock != ENET_SOCKET_NULL && (enet_socket_set_option(lansock, ENET_SOCKOPT_REUSEADDR, 1) < 0 || enet_socket_bind(lansock, &address) < 0))
+ {
+ enet_socket_destroy(lansock);
+ lansock = ENET_SOCKET_NULL;
+ }
+ if(lansock == ENET_SOCKET_NULL) conoutf(CON_WARN, "WARNING: could not create LAN server info socket");
+ else enet_socket_set_option(lansock, ENET_SOCKOPT_NONBLOCK, 1);
+ return true;
+}
+
+void initserver(bool listen, bool dedicated)
+{
+ if(dedicated)
+ {
+#ifdef WIN32
+ setupwindow("Cube 2: Sauerbraten server");
+#endif
+ }
+
+ execfile("server-init.cfg", false);
+
+ if(listen) setuplistenserver(dedicated);
+
+ server::serverinit();
+
+ if(listen)
+ {
+ dedicatedserver = dedicated;
+ updatemasterserver();
+ if(dedicated) rundedicatedserver(); // never returns
+#ifndef STANDALONE
+ else conoutf("listen server started");
+#endif
+ }
+}
+
+#ifndef STANDALONE
+void startlistenserver(int *usemaster)
+{
+ if(serverhost) { conoutf(CON_ERROR, "listen server is already running"); return; }
+
+ allowupdatemaster = *usemaster>0 ? 1 : 0;
+
+ if(!setuplistenserver(false)) return;
+
+ updatemasterserver();
+
+ conoutf("listen server started for %d clients%s", maxclients, allowupdatemaster ? " and listed with master server" : "");
+}
+COMMAND(startlistenserver, "i");
+
+void stoplistenserver()
+{
+ if(!serverhost) { conoutf(CON_ERROR, "listen server is not running"); return; }
+
+ kicknonlocalclients();
+ enet_host_flush(serverhost);
+ cleanupserver();
+
+ conoutf("listen server stopped");
+}
+COMMAND(stoplistenserver, "");
+#endif
+
+bool serveroption(char *opt)
+{
+ switch(opt[1])
+ {
+ case 'u': setvar("serveruprate", atoi(opt+2)); return true;
+ case 'c': setvar("maxclients", atoi(opt+2)); return true;
+ case 'i': setsvar("serverip", opt+2); return true;
+ case 'j': setvar("serverport", atoi(opt+2)); return true;
+ case 'm': setsvar("mastername", opt+2); setvar("updatemaster", mastername[0] ? 1 : 0); return true;
+#ifdef STANDALONE
+ case 'q': logoutf("Using home directory: %s", opt); sethomedir(opt+2); return true;
+ case 'k': logoutf("Adding package directory: %s", opt); addpackagedir(opt+2); return true;
+ case 'g': logoutf("Setting log file: %s", opt); setlogfile(opt+2); return true;
+#endif
+ default: return false;
+ }
+}
+
+vector<const char *> gameargs;
+
+#ifdef STANDALONE
+int main(int argc, char **argv)
+{
+ setlogfile(NULL);
+ if(enet_initialize()<0) fatal("Unable to initialise network module");
+ atexit(enet_deinitialize);
+ enet_time_set(0);
+ for(int i = 1; i<argc; i++) if(argv[i][0]!='-' || !serveroption(argv[i])) gameargs.add(argv[i]);
+ game::parseoptions(gameargs);
+ initserver(true, true);
+ return EXIT_SUCCESS;
+}
+#endif
diff --git a/src/engine/serverbrowser.cpp b/src/engine/serverbrowser.cpp
new file mode 100644
index 0000000..281b0dc
--- /dev/null
+++ b/src/engine/serverbrowser.cpp
@@ -0,0 +1,751 @@
+// serverbrowser.cpp: eihrul's concurrent resolver, and server browser window management
+
+#include "engine.h"
+
+struct resolverthread
+{
+ SDL_Thread *thread;
+ const char *query;
+ int starttime;
+};
+
+struct resolverresult
+{
+ const char *query;
+ ENetAddress address;
+};
+
+vector<resolverthread> resolverthreads;
+vector<const char *> resolverqueries;
+vector<resolverresult> resolverresults;
+SDL_mutex *resolvermutex;
+SDL_cond *querycond, *resultcond;
+
+#define RESOLVERTHREADS 2
+#define RESOLVERLIMIT 3000
+
+int resolverloop(void * data)
+{
+ resolverthread *rt = (resolverthread *)data;
+ SDL_LockMutex(resolvermutex);
+ SDL_Thread *thread = rt->thread;
+ SDL_UnlockMutex(resolvermutex);
+ if(!thread || SDL_GetThreadID(thread) != SDL_ThreadID())
+ return 0;
+ while(thread == rt->thread)
+ {
+ SDL_LockMutex(resolvermutex);
+ while(resolverqueries.empty()) SDL_CondWait(querycond, resolvermutex);
+ rt->query = resolverqueries.pop();
+ rt->starttime = totalmillis;
+ SDL_UnlockMutex(resolvermutex);
+
+ ENetAddress address = { ENET_HOST_ANY, ENET_PORT_ANY };
+ enet_address_set_host(&address, rt->query);
+
+ SDL_LockMutex(resolvermutex);
+ if(rt->query && thread == rt->thread)
+ {
+ resolverresult &rr = resolverresults.add();
+ rr.query = rt->query;
+ rr.address = address;
+ rt->query = NULL;
+ rt->starttime = 0;
+ SDL_CondSignal(resultcond);
+ }
+ SDL_UnlockMutex(resolvermutex);
+ }
+ return 0;
+}
+
+void resolverinit()
+{
+ resolvermutex = SDL_CreateMutex();
+ querycond = SDL_CreateCond();
+ resultcond = SDL_CreateCond();
+
+ SDL_LockMutex(resolvermutex);
+ loopi(RESOLVERTHREADS)
+ {
+ resolverthread &rt = resolverthreads.add();
+ rt.query = NULL;
+ rt.starttime = 0;
+ rt.thread = SDL_CreateThread(resolverloop, "resolver", &rt);
+ }
+ SDL_UnlockMutex(resolvermutex);
+}
+
+void resolverstop(resolverthread &rt)
+{
+ SDL_LockMutex(resolvermutex);
+ if(rt.query)
+ {
+#if SDL_VERSION_ATLEAST(2, 0, 2)
+ SDL_DetachThread(rt.thread);
+#endif
+ rt.thread = SDL_CreateThread(resolverloop, "resolver", &rt);
+ }
+ rt.query = NULL;
+ rt.starttime = 0;
+ SDL_UnlockMutex(resolvermutex);
+}
+
+void resolverclear()
+{
+ if(resolverthreads.empty()) return;
+
+ SDL_LockMutex(resolvermutex);
+ resolverqueries.shrink(0);
+ resolverresults.shrink(0);
+ loopv(resolverthreads)
+ {
+ resolverthread &rt = resolverthreads[i];
+ resolverstop(rt);
+ }
+ SDL_UnlockMutex(resolvermutex);
+}
+
+void resolverquery(const char *name)
+{
+ if(resolverthreads.empty()) resolverinit();
+
+ SDL_LockMutex(resolvermutex);
+ resolverqueries.add(name);
+ SDL_CondSignal(querycond);
+ SDL_UnlockMutex(resolvermutex);
+}
+
+bool resolvercheck(const char **name, ENetAddress *address)
+{
+ bool resolved = false;
+ SDL_LockMutex(resolvermutex);
+ if(!resolverresults.empty())
+ {
+ resolverresult &rr = resolverresults.pop();
+ *name = rr.query;
+ address->host = rr.address.host;
+ resolved = true;
+ }
+ else loopv(resolverthreads)
+ {
+ resolverthread &rt = resolverthreads[i];
+ if(rt.query && totalmillis - rt.starttime > RESOLVERLIMIT)
+ {
+ resolverstop(rt);
+ *name = rt.query;
+ resolved = true;
+ }
+ }
+ SDL_UnlockMutex(resolvermutex);
+ return resolved;
+}
+
+bool resolverwait(const char *name, ENetAddress *address)
+{
+ if(resolverthreads.empty()) resolverinit();
+
+ defformatstring(text, "resolving %s... (esc to abort)", name);
+ renderprogress(0, text);
+
+ SDL_LockMutex(resolvermutex);
+ resolverqueries.add(name);
+ SDL_CondSignal(querycond);
+ int starttime = SDL_GetTicks(), timeout = 0;
+ bool resolved = false;
+ for(;;)
+ {
+ SDL_CondWaitTimeout(resultcond, resolvermutex, 250);
+ loopv(resolverresults) if(resolverresults[i].query == name)
+ {
+ address->host = resolverresults[i].address.host;
+ resolverresults.remove(i);
+ resolved = true;
+ break;
+ }
+ if(resolved) break;
+
+ timeout = SDL_GetTicks() - starttime;
+ renderprogress(min(float(timeout)/RESOLVERLIMIT, 1.0f), text);
+ if(interceptkey(SDLK_ESCAPE)) timeout = RESOLVERLIMIT + 1;
+ if(timeout > RESOLVERLIMIT) break;
+ }
+ if(!resolved && timeout > RESOLVERLIMIT)
+ {
+ loopv(resolverthreads)
+ {
+ resolverthread &rt = resolverthreads[i];
+ if(rt.query == name) { resolverstop(rt); break; }
+ }
+ }
+ SDL_UnlockMutex(resolvermutex);
+ return resolved && address->host != ENET_HOST_ANY;
+}
+
+#define CONNLIMIT 20000
+
+int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &address)
+{
+ defformatstring(text, "connecting to %s... (esc to abort)", hostname);
+ renderprogress(0, text);
+
+ ENetSocketSet readset, writeset;
+ if(!enet_socket_connect(sock, &address)) for(int starttime = SDL_GetTicks(), timeout = 0; timeout <= CONNLIMIT;)
+ {
+ ENET_SOCKETSET_EMPTY(readset);
+ ENET_SOCKETSET_EMPTY(writeset);
+ ENET_SOCKETSET_ADD(readset, sock);
+ ENET_SOCKETSET_ADD(writeset, sock);
+ int result = enet_socketset_select(sock, &readset, &writeset, 250);
+ if(result < 0) break;
+ else if(result > 0)
+ {
+ if(ENET_SOCKETSET_CHECK(readset, sock) || ENET_SOCKETSET_CHECK(writeset, sock))
+ {
+ int error = 0;
+ if(enet_socket_get_option(sock, ENET_SOCKOPT_ERROR, &error) < 0 || error) break;
+ return 0;
+ }
+ }
+ timeout = SDL_GetTicks() - starttime;
+ renderprogress(min(float(timeout)/CONNLIMIT, 1.0f), text);
+ if(interceptkey(SDLK_ESCAPE)) break;
+ }
+
+ return -1;
+}
+
+struct pingattempts
+{
+ enum { MAXATTEMPTS = 2 };
+
+ int offset, attempts[MAXATTEMPTS];
+
+ pingattempts() : offset(0) { clearattempts(); }
+
+ void clearattempts() { memset(attempts, 0, sizeof(attempts)); }
+
+ void setoffset() { offset = 1 + rnd(0xFFFFFF); }
+
+ int encodeping(int millis)
+ {
+ millis += offset;
+ return millis ? millis : 1;
+ }
+
+ int decodeping(int val)
+ {
+ return val - offset;
+ }
+
+ int addattempt(int millis)
+ {
+ int val = encodeping(millis);
+ loopk(MAXATTEMPTS-1) attempts[k+1] = attempts[k];
+ attempts[0] = val;
+ return val;
+ }
+
+ bool checkattempt(int val, bool del = true)
+ {
+ if(val) loopk(MAXATTEMPTS) if(attempts[k] == val)
+ {
+ if(del) attempts[k] = 0;
+ return true;
+ }
+ return false;
+ }
+
+};
+
+enum { UNRESOLVED = 0, RESOLVING, RESOLVED };
+
+struct serverinfo : pingattempts
+{
+ enum
+ {
+ WAITING = INT_MAX,
+
+ MAXPINGS = 3
+ };
+
+ string name, map, sdesc;
+ int port, numplayers, resolved, ping, lastping, nextping;
+ int pings[MAXPINGS];
+ vector<int> attr;
+ ENetAddress address;
+ bool keep;
+ const char *password;
+
+ serverinfo()
+ : port(-1), numplayers(0), resolved(UNRESOLVED), keep(false), password(NULL)
+ {
+ name[0] = map[0] = sdesc[0] = '\0';
+ clearpings();
+ setoffset();
+ }
+
+ ~serverinfo()
+ {
+ DELETEA(password);
+ }
+
+ void clearpings()
+ {
+ ping = WAITING;
+ loopk(MAXPINGS) pings[k] = WAITING;
+ nextping = 0;
+ lastping = -1;
+ clearattempts();
+ }
+
+ void cleanup()
+ {
+ clearpings();
+ attr.setsize(0);
+ numplayers = 0;
+ }
+
+ void reset()
+ {
+ lastping = -1;
+ }
+
+ void checkdecay(int decay)
+ {
+ if(lastping >= 0 && totalmillis - lastping >= decay)
+ cleanup();
+ if(lastping < 0) lastping = totalmillis;
+ }
+
+ void calcping()
+ {
+ int numpings = 0, totalpings = 0;
+ loopk(MAXPINGS) if(pings[k] != WAITING) { totalpings += pings[k]; numpings++; }
+ ping = numpings ? totalpings/numpings : WAITING;
+ }
+
+ void addping(int rtt, int millis)
+ {
+ if(millis >= lastping) lastping = -1;
+ pings[nextping] = rtt;
+ nextping = (nextping+1)%MAXPINGS;
+ calcping();
+ }
+
+ static bool compare(serverinfo *a, serverinfo *b)
+ {
+ bool ac = server::servercompatible(a->name, a->sdesc, a->map, a->ping, a->attr, a->numplayers),
+ bc = server::servercompatible(b->name, b->sdesc, b->map, b->ping, b->attr, b->numplayers);
+ if(ac > bc) return true;
+ if(bc > ac) return false;
+ if(a->keep > b->keep) return true;
+ if(a->keep < b->keep) return false;
+ if(a->numplayers < b->numplayers) return false;
+ if(a->numplayers > b->numplayers) return true;
+ if(a->ping > b->ping) return false;
+ if(a->ping < b->ping) return true;
+ int cmp = strcmp(a->name, b->name);
+ if(cmp != 0) return cmp < 0;
+ if(a->port < b->port) return true;
+ if(a->port > b->port) return false;
+ return false;
+ }
+};
+
+vector<serverinfo *> servers;
+ENetSocket pingsock = ENET_SOCKET_NULL;
+int lastinfo = 0;
+
+static serverinfo *newserver(const char *name, int port, uint ip = ENET_HOST_ANY)
+{
+ serverinfo *si = new serverinfo;
+ si->address.host = ip;
+ si->address.port = server::serverinfoport(port);
+ if(ip!=ENET_HOST_ANY) si->resolved = RESOLVED;
+
+ si->port = port;
+ if(name) copystring(si->name, name);
+ else if(ip==ENET_HOST_ANY || enet_address_get_host_ip(&si->address, si->name, sizeof(si->name)) < 0)
+ {
+ delete si;
+ return NULL;
+
+ }
+
+ servers.add(si);
+
+ return si;
+}
+
+void addserver(const char *name, int port, const char *password, bool keep)
+{
+ if(port <= 0) port = server::serverport();
+ loopv(servers)
+ {
+ serverinfo *s = servers[i];
+ if(strcmp(s->name, name) || s->port != port) continue;
+ if(password && (!s->password || strcmp(s->password, password)))
+ {
+ DELETEA(s->password);
+ s->password = newstring(password);
+ }
+ if(keep && !s->keep) s->keep = true;
+ return;
+ }
+ serverinfo *s = newserver(name, port);
+ if(!s) return;
+ if(password) s->password = newstring(password);
+ s->keep = keep;
+}
+
+VARP(searchlan, 0, 0, 1);
+VARP(servpingrate, 1000, 5000, 60000);
+VARP(servpingdecay, 1000, 15000, 60000);
+VARP(maxservpings, 0, 10, 1000);
+
+pingattempts lanpings;
+
+template<size_t N> static inline void buildping(ENetBuffer &buf, uchar (&ping)[N], pingattempts &a)
+{
+ ucharbuf p(ping, N);
+ putint(p, a.addattempt(totalmillis));
+ buf.data = ping;
+ buf.dataLength = p.length();
+}
+
+void pingservers()
+{
+ if(pingsock == ENET_SOCKET_NULL)
+ {
+ pingsock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+ if(pingsock == ENET_SOCKET_NULL)
+ {
+ lastinfo = totalmillis;
+ return;
+ }
+ enet_socket_set_option(pingsock, ENET_SOCKOPT_NONBLOCK, 1);
+ enet_socket_set_option(pingsock, ENET_SOCKOPT_BROADCAST, 1);
+
+ lanpings.setoffset();
+ }
+
+ ENetBuffer buf;
+ uchar ping[MAXTRANS];
+
+ static int lastping = 0;
+ if(lastping >= servers.length()) lastping = 0;
+ loopi(maxservpings ? min(servers.length(), maxservpings) : servers.length())
+ {
+ serverinfo &si = *servers[lastping];
+ if(++lastping >= servers.length()) lastping = 0;
+ if(si.address.host == ENET_HOST_ANY) continue;
+ buildping(buf, ping, si);
+ enet_socket_send(pingsock, &si.address, &buf, 1);
+
+ si.checkdecay(servpingdecay);
+ }
+ if(searchlan)
+ {
+ ENetAddress address;
+ address.host = ENET_HOST_BROADCAST;
+ address.port = server::laninfoport();
+ buildping(buf, ping, lanpings);
+ enet_socket_send(pingsock, &address, &buf, 1);
+ }
+ lastinfo = totalmillis;
+}
+
+void checkresolver()
+{
+ int resolving = 0;
+ loopv(servers)
+ {
+ serverinfo &si = *servers[i];
+ if(si.resolved == RESOLVED) continue;
+ if(si.address.host == ENET_HOST_ANY)
+ {
+ if(si.resolved == UNRESOLVED) { si.resolved = RESOLVING; resolverquery(si.name); }
+ resolving++;
+ }
+ }
+ if(!resolving) return;
+
+ const char *name = NULL;
+ for(;;)
+ {
+ ENetAddress addr = { ENET_HOST_ANY, ENET_PORT_ANY };
+ if(!resolvercheck(&name, &addr)) break;
+ loopv(servers)
+ {
+ serverinfo &si = *servers[i];
+ if(name == si.name)
+ {
+ si.resolved = RESOLVED;
+ si.address.host = addr.host;
+ break;
+ }
+ }
+ }
+}
+
+static int lastreset = 0;
+
+void checkpings()
+{
+ if(pingsock==ENET_SOCKET_NULL) return;
+ enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE;
+ ENetBuffer buf;
+ ENetAddress addr;
+ uchar ping[MAXTRANS];
+ char text[MAXTRANS];
+ buf.data = ping;
+ buf.dataLength = sizeof(ping);
+ while(enet_socket_wait(pingsock, &events, 0) >= 0 && events)
+ {
+ int len = enet_socket_receive(pingsock, &addr, &buf, 1);
+ if(len <= 0) return;
+ ucharbuf p(ping, len);
+ int millis = getint(p);
+ serverinfo *si = NULL;
+ loopv(servers) if(addr.host == servers[i]->address.host && addr.port == servers[i]->address.port) { si = servers[i]; break; }
+ if(si)
+ {
+ if(!si->checkattempt(millis)) continue;
+ millis = si->decodeping(millis);
+ }
+ else if(!searchlan || !lanpings.checkattempt(millis, false)) continue;
+ else
+ {
+ si = newserver(NULL, server::serverport(addr.port), addr.host);
+ millis = lanpings.decodeping(millis);
+ }
+ int rtt = clamp(totalmillis - millis, 0, min(servpingdecay, totalmillis));
+ if(millis >= lastreset && rtt < servpingdecay) si->addping(rtt, millis);
+ si->numplayers = getint(p);
+ int numattr = getint(p);
+ si->attr.setsize(0);
+ loopj(numattr) { int attr = getint(p); if(p.overread()) break; si->attr.add(attr); }
+ getstring(text, p);
+ filtertext(si->map, text, false);
+ getstring(text, p);
+ filtertext(si->sdesc, text, true, true);
+ }
+}
+
+void sortservers()
+{
+ servers.sort(serverinfo::compare);
+}
+COMMAND(sortservers, "");
+
+VARP(autosortservers, 0, 1, 1);
+VARP(autoupdateservers, 0, 1, 1);
+
+void refreshservers()
+{
+ static int lastrefresh = 0;
+ if(lastrefresh==totalmillis) return;
+ if(totalmillis - lastrefresh > 1000)
+ {
+ loopv(servers) servers[i]->reset();
+ lastreset = totalmillis;
+ }
+ lastrefresh = totalmillis;
+
+ checkresolver();
+ checkpings();
+ if(totalmillis - lastinfo >= servpingrate/(maxservpings ? max(1, (servers.length() + maxservpings - 1) / maxservpings) : 1)) pingservers();
+ if(autosortservers) sortservers();
+}
+
+serverinfo *selectedserver = NULL;
+
+const char *showservers(g3d_gui *cgui, uint *header, int pagemin, int pagemax)
+{
+ refreshservers();
+ if(servers.empty())
+ {
+ if(header) execute(header);
+ return NULL;
+ }
+ serverinfo *sc = NULL;
+ for(int start = 0; start < servers.length();)
+ {
+ if(start > 0) cgui->tab();
+ if(header) execute(header);
+ int end = servers.length();
+ cgui->pushlist();
+ loopi(10)
+ {
+ if(!game::serverinfostartcolumn(cgui, i)) break;
+ for(int j = start; j < end; j++)
+ {
+ if(!i && j+1 - start >= pagemin && (j+1 - start >= pagemax || cgui->shouldtab())) { end = j; break; }
+ serverinfo &si = *servers[j];
+ const char *sdesc = si.sdesc;
+ if(si.address.host == ENET_HOST_ANY) sdesc = "[unknown host]";
+ else if(si.ping == serverinfo::WAITING) sdesc = "[waiting for response]";
+ if(game::serverinfoentry(cgui, i, si.name, si.port, sdesc, si.map, sdesc == si.sdesc ? si.ping : -1, si.attr, si.numplayers))
+ sc = &si;
+ }
+ game::serverinfoendcolumn(cgui, i);
+ }
+ cgui->poplist();
+ start = end;
+ }
+ if(selectedserver || !sc) return NULL;
+ selectedserver = sc;
+ return "connectselected";
+}
+
+void connectselected()
+{
+ if(!selectedserver) return;
+ connectserv(selectedserver->name, selectedserver->port, selectedserver->password);
+ selectedserver = NULL;
+}
+
+COMMAND(connectselected, "");
+
+void clearservers(bool full = false)
+{
+ resolverclear();
+ if(full) servers.deletecontents();
+ else loopvrev(servers) if(!servers[i]->keep) delete servers.remove(i);
+ selectedserver = NULL;
+}
+
+#define RETRIEVELIMIT 20000
+
+void retrieveservers(vector<char> &data)
+{
+ ENetSocket sock = connectmaster(true);
+ if(sock == ENET_SOCKET_NULL) return;
+
+ extern char *mastername;
+ defformatstring(text, "retrieving servers from %s... (esc to abort)", mastername);
+ renderprogress(0, text);
+
+ int starttime = SDL_GetTicks(), timeout = 0;
+ const char *req = "list\n";
+ int reqlen = strlen(req);
+ ENetBuffer buf;
+ while(reqlen > 0)
+ {
+ enet_uint32 events = ENET_SOCKET_WAIT_SEND;
+ if(enet_socket_wait(sock, &events, 250) >= 0 && events)
+ {
+ buf.data = (void *)req;
+ buf.dataLength = reqlen;
+ int sent = enet_socket_send(sock, NULL, &buf, 1);
+ if(sent < 0) break;
+ req += sent;
+ reqlen -= sent;
+ if(reqlen <= 0) break;
+ }
+ timeout = SDL_GetTicks() - starttime;
+ renderprogress(min(float(timeout)/RETRIEVELIMIT, 1.0f), text);
+ if(interceptkey(SDLK_ESCAPE)) timeout = RETRIEVELIMIT + 1;
+ if(timeout > RETRIEVELIMIT) break;
+ }
+
+ if(reqlen <= 0) for(;;)
+ {
+ enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE;
+ if(enet_socket_wait(sock, &events, 250) >= 0 && events)
+ {
+ if(data.length() >= data.capacity()) data.reserve(4096);
+ buf.data = data.getbuf() + data.length();
+ buf.dataLength = data.capacity() - data.length();
+ int recv = enet_socket_receive(sock, NULL, &buf, 1);
+ if(recv <= 0) break;
+ data.advance(recv);
+ }
+ timeout = SDL_GetTicks() - starttime;
+ renderprogress(min(float(timeout)/RETRIEVELIMIT, 1.0f), text);
+ if(interceptkey(SDLK_ESCAPE)) timeout = RETRIEVELIMIT + 1;
+ if(timeout > RETRIEVELIMIT) break;
+ }
+
+ if(data.length()) data.add('\0');
+ enet_socket_destroy(sock);
+}
+
+bool updatedservers = false;
+
+void updatefrommaster()
+{
+ vector<char> data;
+ retrieveservers(data);
+ if(data.empty()) conoutf(CON_ERROR, "master server not replying");
+ else
+ {
+ clearservers();
+ char *line = data.getbuf();
+ while(char *end = (char *)memchr(line, '\n', data.length() - (line - data.getbuf())))
+ {
+ *end = '\0';
+
+ const char *args = line;
+ while(args < end && !iscubespace(*args)) args++;
+ int cmdlen = args - line;
+ while(args < end && iscubespace(*args)) args++;
+
+ if(matchstring(line, cmdlen, "addserver"))
+ {
+ string ip;
+ int port;
+ if(sscanf(args, "%100s %d", ip, &port) == 2) addserver(ip, port);
+ }
+ else if(matchstring(line, cmdlen, "echo")) conoutf("\f1%s", args);
+
+ line = end + 1;
+ }
+ }
+ refreshservers();
+ updatedservers = true;
+}
+
+void initservers()
+{
+ selectedserver = NULL;
+ if(autoupdateservers && !updatedservers) updatefrommaster();
+}
+
+ICOMMAND(addserver, "sis", (const char *name, int *port, const char *password), addserver(name, *port, password[0] ? password : NULL));
+ICOMMAND(keepserver, "sis", (const char *name, int *port, const char *password), addserver(name, *port, password[0] ? password : NULL, true));
+ICOMMAND(clearservers, "i", (int *full), clearservers(*full!=0));
+COMMAND(updatefrommaster, "");
+COMMAND(initservers, "");
+
+void writeservercfg()
+{
+ if(!game::savedservers()) return;
+ stream *f = openutf8file(path(game::savedservers(), true), "w");
+ if(!f) return;
+ int kept = 0;
+ loopv(servers)
+ {
+ serverinfo *s = servers[i];
+ if(s->keep)
+ {
+ if(!kept) f->printf("// servers that should never be cleared from the server list\n\n");
+ if(s->password) f->printf("keepserver %s %d %s\n", escapeid(s->name), s->port, escapestring(s->password));
+ else f->printf("keepserver %s %d\n", escapeid(s->name), s->port);
+ kept++;
+ }
+ }
+ if(kept) f->printf("\n");
+ f->printf("// servers connected to are added here automatically\n\n");
+ loopv(servers)
+ {
+ serverinfo *s = servers[i];
+ if(!s->keep)
+ {
+ if(s->password) f->printf("addserver %s %d %s\n", escapeid(s->name), s->port, escapestring(s->password));
+ else f->printf("addserver %s %d\n", escapeid(s->name), s->port);
+ }
+ }
+ delete f;
+}
+
diff --git a/src/engine/shader.cpp b/src/engine/shader.cpp
new file mode 100644
index 0000000..da17d4f
--- /dev/null
+++ b/src/engine/shader.cpp
@@ -0,0 +1,1522 @@
+// shader.cpp: OpenGL GLSL shader management
+
+#include "engine.h"
+
+Shader *Shader::lastshader = NULL;
+
+Shader *nullshader = NULL, *hudshader = NULL, *hudnotextureshader = NULL, *textureshader = NULL, *notextureshader = NULL, *nocolorshader = NULL, *foggedshader = NULL, *foggednotextureshader = NULL, *stdworldshader = NULL;
+
+static hashnameset<GlobalShaderParamState> globalparams(256);
+static hashtable<const char *, int> localparams(256);
+static hashnameset<Shader> shaders(256);
+static Shader *slotshader = NULL;
+static vector<SlotShaderParam> slotparams;
+static bool standardshaders = false, forceshaders = true, loadedshaders = false;
+
+VAR(reservevpparams, 1, 16, 0);
+VAR(maxvsuniforms, 1, 0, 0);
+VAR(maxfsuniforms, 1, 0, 0);
+VAR(maxvaryings, 1, 0, 0);
+VAR(dbgshader, 0, 0, 2);
+
+void loadshaders()
+{
+ standardshaders = true;
+ execfile("data/glsl.cfg");
+ standardshaders = false;
+
+ nullshader = lookupshaderbyname("null");
+ hudshader = lookupshaderbyname("hud");
+ hudnotextureshader = lookupshaderbyname("hudnotexture");
+ stdworldshader = lookupshaderbyname("stdworld");
+ if(!nullshader || !hudshader || !hudnotextureshader || !stdworldshader) fatal("cannot find shader definitions");
+
+ dummyslot.shader = stdworldshader;
+
+ textureshader = lookupshaderbyname("texture");
+ notextureshader = lookupshaderbyname("notexture");
+ nocolorshader = lookupshaderbyname("nocolor");
+ foggedshader = lookupshaderbyname("fogged");
+ foggednotextureshader = lookupshaderbyname("foggednotexture");
+
+ nullshader->set();
+
+ loadedshaders = true;
+}
+
+Shader *lookupshaderbyname(const char *name)
+{
+ Shader *s = shaders.access(name);
+ return s && s->loaded() ? s : NULL;
+}
+
+Shader *generateshader(const char *name, const char *fmt, ...)
+{
+ if(!loadedshaders) return NULL;
+ Shader *s = name ? lookupshaderbyname(name) : NULL;
+ if(!s)
+ {
+ defvformatstring(cmd, fmt, fmt);
+ bool wasstandard = standardshaders;
+ standardshaders = true;
+ execute(cmd);
+ standardshaders = wasstandard;
+ s = name ? lookupshaderbyname(name) : NULL;
+ if(!s) s = nullshader;
+ }
+ return s;
+}
+
+static void showglslinfo(GLenum type, GLuint obj, const char *name, const char **parts = NULL, int numparts = 0)
+{
+ GLint length = 0;
+ if(type) glGetShaderiv_(obj, GL_INFO_LOG_LENGTH, &length);
+ else glGetProgramiv_(obj, GL_INFO_LOG_LENGTH, &length);
+ if(length > 1)
+ {
+ conoutf(CON_ERROR, "GLSL ERROR (%s:%s)", type == GL_VERTEX_SHADER ? "VS" : (type == GL_FRAGMENT_SHADER ? "FS" : "PROG"), name);
+ FILE *l = getlogfile();
+ if(l)
+ {
+ GLchar *log = new GLchar[length];
+ if(type) glGetShaderInfoLog_(obj, length, &length, log);
+ else glGetProgramInfoLog_(obj, length, &length, log);
+ fprintf(l, "%s\n", log);
+ bool partlines = log[0] != '0';
+ int line = 0;
+ loopi(numparts)
+ {
+ const char *part = parts[i];
+ int startline = line;
+ while(*part)
+ {
+ const char *next = strchr(part, '\n');
+ if(++line > 1000) goto done;
+ if(partlines) fprintf(l, "%d(%d): ", i, line - startline); else fprintf(l, "%d: ", line);
+ fwrite(part, 1, next ? next - part + 1 : strlen(part), l);
+ if(!next) { fputc('\n', l); break; }
+ part = next + 1;
+ }
+ }
+ done:
+ delete[] log;
+ }
+ }
+}
+
+static void compileglslshader(GLenum type, GLuint &obj, const char *def, const char *name, bool msg = true)
+{
+ const char *source = def + strspn(def, " \t\r\n");
+ const char *parts[16];
+ int numparts = 0;
+ static const struct { int version; const char * const header; } glslversions[] =
+ {
+ { 330, "#version 330\n" },
+ { 150, "#version 150\n" },
+ { 130, "#version 130\n" },
+ { 120, "#version 120\n" }
+ };
+ loopi(sizeof(glslversions)/sizeof(glslversions[0])) if(glslversion >= glslversions[i].version)
+ {
+ parts[numparts++] = glslversions[i].header;
+ break;
+ }
+ if(glslversion >= 130)
+ {
+ if(type == GL_VERTEX_SHADER) parts[numparts++] =
+ "#define attribute in\n"
+ "#define varying out\n";
+ else if(type == GL_FRAGMENT_SHADER)
+ {
+ parts[numparts++] = "#define varying in\n";
+ if(glslversion < 150) parts[numparts++] = "precision highp float;\n";
+ if(glversion >= 300) parts[numparts++] =
+ "out vec4 cube2_FragColor;\n"
+ "#define gl_FragColor cube2_FragColor\n";
+ }
+ parts[numparts++] =
+ "#define texture2D(sampler, coords) texture(sampler, coords)\n"
+ "#define texture2DProj(sampler, coords) textureProj(sampler, coords)\n"
+ "#define textureCube(sampler, coords) texture(sampler, coords)\n";
+ }
+ parts[numparts++] = source;
+
+ obj = glCreateShader_(type);
+ glShaderSource_(obj, numparts, (const GLchar **)parts, NULL);
+ glCompileShader_(obj);
+ GLint success;
+ glGetShaderiv_(obj, GL_COMPILE_STATUS, &success);
+ if(!success)
+ {
+ if(msg) showglslinfo(type, obj, name, parts, numparts);
+ glDeleteShader_(obj);
+ obj = 0;
+ }
+ else if(dbgshader > 1 && msg) showglslinfo(type, obj, name, parts, numparts);
+}
+
+VAR(dbgubo, 0, 0, 1);
+
+static void bindglsluniform(Shader &s, UniformLoc &u)
+{
+ u.loc = glGetUniformLocation_(s.program, u.name);
+ if(!u.blockname || !hasUBO) return;
+ GLuint bidx = glGetUniformBlockIndex_(s.program, u.blockname);
+ GLuint uidx = GL_INVALID_INDEX;
+ glGetUniformIndices_(s.program, 1, &u.name, &uidx);
+ if(bidx != GL_INVALID_INDEX && uidx != GL_INVALID_INDEX)
+ {
+ GLint sizeval = 0, offsetval = 0, strideval = 0;
+ glGetActiveUniformBlockiv_(s.program, bidx, GL_UNIFORM_BLOCK_DATA_SIZE, &sizeval);
+ if(sizeval <= 0) return;
+ glGetActiveUniformsiv_(s.program, 1, &uidx, GL_UNIFORM_OFFSET, &offsetval);
+ if(u.stride > 0)
+ {
+ glGetActiveUniformsiv_(s.program, 1, &uidx, GL_UNIFORM_ARRAY_STRIDE, &strideval);
+ if(strideval > u.stride) return;
+ }
+ u.offset = offsetval;
+ u.size = sizeval;
+ glUniformBlockBinding_(s.program, bidx, u.binding);
+ if(dbgubo) conoutf(CON_DEBUG, "UBO: %s:%s:%d, offset: %d, size: %d, stride: %d", u.name, u.blockname, u.binding, offsetval, sizeval, strideval);
+ }
+}
+
+static void linkglslprogram(Shader &s, bool msg = true)
+{
+ s.program = s.vsobj && s.psobj ? glCreateProgram_() : 0;
+ GLint success = 0;
+ if(s.program)
+ {
+ glAttachShader_(s.program, s.vsobj);
+ glAttachShader_(s.program, s.psobj);
+ uint attribs = 0;
+ loopv(s.attriblocs)
+ {
+ AttribLoc &a = s.attriblocs[i];
+ glBindAttribLocation_(s.program, a.loc, a.name);
+ attribs |= 1<<a.loc;
+ }
+ loopi(gle::MAXATTRIBS) if(!(attribs&(1<<i))) glBindAttribLocation_(s.program, i, gle::attribnames[i]);
+ if(glversion >= 300)
+ {
+ glBindFragDataLocation_(s.program, 0, "cube2_FragColor");
+ }
+ glLinkProgram_(s.program);
+ glGetProgramiv_(s.program, GL_LINK_STATUS, &success);
+ }
+ if(success)
+ {
+ glUseProgram_(s.program);
+ loopi(8)
+ {
+ static const char * const texnames[8] = { "tex0", "tex1", "tex2", "tex3", "tex4", "tex5", "tex6", "tex7" };
+ GLint loc = glGetUniformLocation_(s.program, texnames[i]);
+ if(loc != -1) glUniform1i_(loc, i);
+ }
+ loopv(s.defaultparams)
+ {
+ SlotShaderParamState &param = s.defaultparams[i];
+ param.loc = glGetUniformLocation_(s.program, param.name);
+ }
+ loopv(s.uniformlocs) bindglsluniform(s, s.uniformlocs[i]);
+ glUseProgram_(0);
+ }
+ else if(s.program)
+ {
+ if(msg) showglslinfo(GL_FALSE, s.program, s.name);
+ glDeleteProgram_(s.program);
+ s.program = 0;
+ }
+}
+
+int getlocalparam(const char *name)
+{
+ return localparams.access(name, int(localparams.numelems));
+}
+
+static int addlocalparam(Shader &s, const char *name, int loc, int size, GLenum format)
+{
+ int idx = getlocalparam(name);
+ if(idx >= s.localparamremap.length())
+ {
+ int n = idx + 1 - s.localparamremap.length();
+ memset(s.localparamremap.pad(n), 0xFF, n);
+ }
+ s.localparamremap[idx] = s.localparams.length();
+
+ LocalShaderParamState &l = s.localparams.add();
+ l.name = name;
+ l.loc = loc;
+ l.size = size;
+ l.format = format;
+ return idx;
+}
+
+GlobalShaderParamState *getglobalparam(const char *name)
+{
+ GlobalShaderParamState *param = globalparams.access(name);
+ if(!param)
+ {
+ param = &globalparams[name];
+ param->name = name;
+ memset(param->buf, -1, sizeof(param->buf));
+ param->version = -1;
+ }
+ return param;
+}
+
+static GlobalShaderParamUse *addglobalparam(Shader &s, GlobalShaderParamState *param, int loc, int size, GLenum format)
+{
+ GlobalShaderParamUse &g = s.globalparams.add();
+ g.param = param;
+ g.version = -2;
+ g.loc = loc;
+ g.size = size;
+ g.format = format;
+ return &g;
+}
+
+static void setglsluniformformat(Shader &s, const char *name, GLenum format, int size)
+{
+ switch(format)
+ {
+ case GL_FLOAT:
+ case GL_FLOAT_VEC2:
+ case GL_FLOAT_VEC3:
+ case GL_FLOAT_VEC4:
+ case GL_INT:
+ case GL_INT_VEC2:
+ case GL_INT_VEC3:
+ case GL_INT_VEC4:
+ case GL_BOOL:
+ case GL_BOOL_VEC2:
+ case GL_BOOL_VEC3:
+ case GL_BOOL_VEC4:
+ case GL_FLOAT_MAT2:
+ case GL_FLOAT_MAT3:
+ case GL_FLOAT_MAT4:
+ break;
+ default:
+ return;
+ }
+ if(!strncmp(name, "gl_", 3)) return;
+
+ int loc = glGetUniformLocation_(s.program, name);
+ if(loc < 0) return;
+ loopvj(s.defaultparams) if(s.defaultparams[j].loc == loc)
+ {
+ s.defaultparams[j].format = format;
+ return;
+ }
+ loopvj(s.uniformlocs) if(s.uniformlocs[j].loc == loc) return;
+ loopvj(s.globalparams) if(s.globalparams[j].loc == loc) return;
+ loopvj(s.localparams) if(s.localparams[j].loc == loc) return;
+
+ name = getshaderparamname(name);
+ GlobalShaderParamState *param = globalparams.access(name);
+ if(param) addglobalparam(s, param, loc, size, format);
+ else addlocalparam(s, name, loc, size, format);
+}
+
+static void allocglslactiveuniforms(Shader &s)
+{
+ GLint numactive = 0;
+ glGetProgramiv_(s.program, GL_ACTIVE_UNIFORMS, &numactive);
+ string name;
+ loopi(numactive)
+ {
+ GLsizei namelen = 0;
+ GLint size = 0;
+ GLenum format = GL_FLOAT_VEC4;
+ name[0] = '\0';
+ glGetActiveUniform_(s.program, i, sizeof(name)-1, &namelen, &size, &format, name);
+ if(namelen <= 0 || size <= 0) continue;
+ name[clamp(int(namelen), 0, (int)sizeof(name)-2)] = '\0';
+ char *brak = strchr(name, '[');
+ if(brak) *brak = '\0';
+ setglsluniformformat(s, name, format, size);
+ }
+}
+
+void Shader::allocparams(Slot *slot)
+{
+ if(slot)
+ {
+#define UNIFORMTEX(name, tmu) \
+ { \
+ loc = glGetUniformLocation_(program, name); \
+ int val = tmu; \
+ if(loc != -1) glUniform1i_(loc, val); \
+ }
+ int loc, tmu = 2;
+ if(type & SHADER_NORMALSLMS)
+ {
+ UNIFORMTEX("lmcolor", 1);
+ UNIFORMTEX("lmdir", 2);
+ tmu++;
+ }
+ else UNIFORMTEX("lightmap", 1);
+ if(type & SHADER_ENVMAP) UNIFORMTEX("envmap", tmu++);
+ UNIFORMTEX("shadowmap", 7);
+ int stex = 0;
+ loopv(slot->sts)
+ {
+ Slot::Tex &t = slot->sts[i];
+ switch(t.type)
+ {
+ case TEX_DIFFUSE: UNIFORMTEX("diffusemap", 0); break;
+ case TEX_NORMAL: UNIFORMTEX("normalmap", tmu++); break;
+ case TEX_GLOW: UNIFORMTEX("glowmap", tmu++); break;
+ case TEX_DECAL: UNIFORMTEX("decal", tmu++); break;
+ case TEX_SPEC: if(t.combined<0) UNIFORMTEX("specmap", tmu++); break;
+ case TEX_DEPTH: if(t.combined<0) UNIFORMTEX("depthmap", tmu++); break;
+ case TEX_ALPHA: if(t.combined<0) UNIFORMTEX("alphamap", tmu++); break;
+ case TEX_UNKNOWN:
+ {
+ defformatstring(sname, "stex%d", stex++);
+ UNIFORMTEX(sname, tmu++);
+ break;
+ }
+ }
+ }
+ }
+ allocglslactiveuniforms(*this);
+}
+
+int GlobalShaderParamState::nextversion = 0;
+
+void GlobalShaderParamState::resetversions()
+{
+ enumerate(shaders, Shader, s,
+ {
+ loopv(s.globalparams)
+ {
+ GlobalShaderParamUse &u = s.globalparams[i];
+ if(u.version != u.param->version) u.version = -2;
+ }
+ });
+ nextversion = 0;
+ enumerate(globalparams, GlobalShaderParamState, g, { g.version = ++nextversion; });
+ enumerate(shaders, Shader, s,
+ {
+ loopv(s.globalparams)
+ {
+ GlobalShaderParamUse &u = s.globalparams[i];
+ if(u.version >= 0) u.version = u.param->version;
+ }
+ });
+}
+
+static float *findslotparam(Slot &s, const char *name, float *noval = NULL)
+{
+ loopv(s.params)
+ {
+ SlotShaderParam &param = s.params[i];
+ if(name == param.name) return param.val;
+ }
+ loopv(s.shader->defaultparams)
+ {
+ SlotShaderParamState &param = s.shader->defaultparams[i];
+ if(name == param.name) return param.val;
+ }
+ return noval;
+}
+
+static float *findslotparam(VSlot &s, const char *name, float *noval = NULL)
+{
+ loopv(s.params)
+ {
+ SlotShaderParam &param = s.params[i];
+ if(name == param.name) return param.val;
+ }
+ return findslotparam(*s.slot, name, noval);
+}
+
+static inline void setslotparam(SlotShaderParamState &l, const float *val)
+{
+ switch(l.format)
+ {
+ case GL_BOOL:
+ case GL_FLOAT: glUniform1fv_(l.loc, 1, val); break;
+ case GL_BOOL_VEC2:
+ case GL_FLOAT_VEC2: glUniform2fv_(l.loc, 1, val); break;
+ case GL_BOOL_VEC3:
+ case GL_FLOAT_VEC3: glUniform3fv_(l.loc, 1, val); break;
+ case GL_BOOL_VEC4:
+ case GL_FLOAT_VEC4: glUniform4fv_(l.loc, 1, val); break;
+ case GL_INT: glUniform1i_(l.loc, int(val[0])); break;
+ case GL_INT_VEC2: glUniform2i_(l.loc, int(val[0]), int(val[1])); break;
+ case GL_INT_VEC3: glUniform3i_(l.loc, int(val[0]), int(val[1]), int(val[2])); break;
+ case GL_INT_VEC4: glUniform4i_(l.loc, int(val[0]), int(val[1]), int(val[2]), int(val[3])); break;
+ }
+}
+
+#define SETSLOTPARAM(l, mask, i, val) do { \
+ if(!(mask&(1<<i))) { \
+ mask |= 1<<i; \
+ setslotparam(l, val); \
+ } \
+} while(0)
+
+#define SETSLOTPARAMS(slotparams) \
+ loopv(slotparams) \
+ { \
+ SlotShaderParam &p = slotparams[i]; \
+ if(!defaultparams.inrange(p.loc)) continue; \
+ SlotShaderParamState &l = defaultparams[p.loc]; \
+ SETSLOTPARAM(l, unimask, p.loc, p.val); \
+ }
+#define SETDEFAULTPARAMS \
+ loopv(defaultparams) \
+ { \
+ SlotShaderParamState &l = defaultparams[i]; \
+ SETSLOTPARAM(l, unimask, i, l.val); \
+ }
+
+void Shader::setslotparams(Slot &slot)
+{
+ uint unimask = 0;
+ SETSLOTPARAMS(slot.params)
+ SETDEFAULTPARAMS
+}
+
+void Shader::setslotparams(Slot &slot, VSlot &vslot)
+{
+ uint unimask = 0;
+ if(vslot.slot == &slot)
+ {
+ SETSLOTPARAMS(vslot.params)
+ SETSLOTPARAMS(slot.params)
+ SETDEFAULTPARAMS
+ }
+ else
+ {
+ SETSLOTPARAMS(slot.params)
+ SETDEFAULTPARAMS
+ }
+}
+
+void Shader::bindprograms()
+{
+ if(this == lastshader || type&(SHADER_DEFERRED|SHADER_INVALID)) return;
+ glUseProgram_(program);
+ lastshader = this;
+}
+
+bool Shader::compile()
+{
+ if(!vsstr) vsobj = !reusevs || reusevs->invalid() ? 0 : reusevs->vsobj;
+ else compileglslshader(GL_VERTEX_SHADER, vsobj, vsstr, name, dbgshader || !variantshader);
+ if(!psstr) psobj = !reuseps || reuseps->invalid() ? 0 : reuseps->psobj;
+ else compileglslshader(GL_FRAGMENT_SHADER, psobj, psstr, name, dbgshader || !variantshader);
+ linkglslprogram(*this, !variantshader);
+ return program!=0;
+}
+
+void Shader::cleanup(bool invalid)
+{
+ detailshader = NULL;
+ used = false;
+ if(vsobj) { if(!reusevs) glDeleteShader_(vsobj); vsobj = 0; }
+ if(psobj) { if(!reuseps) glDeleteShader_(psobj); psobj = 0; }
+ if(program) { glDeleteProgram_(program); program = 0; }
+ localparams.setsize(0);
+ localparamremap.setsize(0);
+ globalparams.setsize(0);
+ if(standard || invalid)
+ {
+ type = SHADER_INVALID;
+ DELETEA(vsstr);
+ DELETEA(psstr);
+ DELETEA(defer);
+ variants.setsize(0);
+ DELETEA(variantrows);
+ defaultparams.setsize(0);
+ attriblocs.setsize(0);
+ uniformlocs.setsize(0);
+ altshader = NULL;
+ loopi(MAXSHADERDETAIL) fastshader[i] = this;
+ reusevs = reuseps = NULL;
+ }
+ else loopv(defaultparams) defaultparams[i].loc = -1;
+ owner = NULL;
+}
+
+bool Shader::isnull(const Shader *s) { return !s; }
+
+static void genattriblocs(Shader &s, const char *vs, const char *ps, Shader *reusevs, Shader *reuseps)
+{
+ static int len = strlen("//:attrib");
+ string name;
+ int loc;
+ if(reusevs) s.attriblocs = reusevs->attriblocs;
+ else while((vs = strstr(vs, "//:attrib")))
+ {
+ if(sscanf(vs, "//:attrib %100s %d", name, &loc) == 2)
+ s.attriblocs.add(AttribLoc(getshaderparamname(name), loc));
+ vs += len;
+ }
+}
+
+static void genuniformlocs(Shader &s, const char *vs, const char *ps, Shader *reusevs, Shader *reuseps)
+{
+ static int len = strlen("//:uniform");
+ string name, blockname;
+ int binding, stride;
+ if(reusevs) s.uniformlocs = reusevs->uniformlocs;
+ else while((vs = strstr(vs, "//:uniform")))
+ {
+ int numargs = sscanf(vs, "//:uniform %100s %100s %d %d", name, blockname, &binding, &stride);
+ if(numargs >= 3) s.uniformlocs.add(UniformLoc(getshaderparamname(name), getshaderparamname(blockname), binding, numargs >= 4 ? stride : 0));
+ else if(numargs >= 1) s.uniformlocs.add(UniformLoc(getshaderparamname(name)));
+ vs += len;
+ }
+}
+
+Shader *newshader(int type, const char *name, const char *vs, const char *ps, Shader *variant = NULL, int row = 0)
+{
+ if(Shader::lastshader)
+ {
+ glUseProgram_(0);
+ Shader::lastshader = NULL;
+ }
+
+ Shader *exists = shaders.access(name);
+ char *rname = exists ? exists->name : newstring(name);
+ Shader &s = shaders[rname];
+ s.name = rname;
+ s.vsstr = newstring(vs);
+ s.psstr = newstring(ps);
+ DELETEA(s.defer);
+ s.type = type;
+ s.variantshader = variant;
+ s.standard = standardshaders;
+ if(forceshaders) s.forced = true;
+ s.reusevs = s.reuseps = NULL;
+ if(variant)
+ {
+ int row = 0, col = 0;
+ if(!vs[0] || sscanf(vs, "%d , %d", &row, &col) >= 1)
+ {
+ DELETEA(s.vsstr);
+ s.reusevs = !vs[0] ? variant : variant->getvariant(col, row);
+ }
+ row = col = 0;
+ if(!ps[0] || sscanf(ps, "%d , %d", &row, &col) >= 1)
+ {
+ DELETEA(s.psstr);
+ s.reuseps = !ps[0] ? variant : variant->getvariant(col, row);
+ }
+ }
+ if(variant) loopv(variant->defaultparams) s.defaultparams.add(variant->defaultparams[i]);
+ else loopv(slotparams) s.defaultparams.add(slotparams[i]);
+ s.attriblocs.setsize(0);
+ s.uniformlocs.setsize(0);
+ genattriblocs(s, vs, ps, s.reusevs, s.reuseps);
+ genuniformlocs(s, vs, ps, s.reusevs, s.reuseps);
+ if(!s.compile())
+ {
+ s.cleanup(true);
+ if(variant) shaders.remove(rname);
+ return NULL;
+ }
+ if(variant) variant->addvariant(row, &s);
+ s.fixdetailshader();
+ return &s;
+}
+
+void setupshaders()
+{
+ GLint val;
+ glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS, &val);
+ maxvsuniforms = val/4;
+ glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, &val);
+ maxfsuniforms = val/4;
+ if(glversion < 300)
+ {
+ glGetIntegerv(GL_MAX_VARYING_COMPONENTS, &val);
+ maxvaryings = val;
+ }
+
+ standardshaders = true;
+ nullshader = newshader(0, "<init>null",
+ "attribute vec4 vvertex;\n"
+ "void main(void) {\n"
+ " gl_Position = vvertex;\n"
+ "}\n",
+ "void main(void) {\n"
+ " gl_FragColor = vec4(1.0, 0.0, 1.0, 0.0);\n"
+ "}\n");
+ hudshader = newshader(0, "<init>hud",
+ "attribute vec4 vvertex, vcolor;\n"
+ "attribute vec2 vtexcoord0;\n"
+ "uniform mat4 hudmatrix;\n"
+ "varying vec2 texcoord0;\n"
+ "varying vec4 color;\n"
+ "void main(void) {\n"
+ " gl_Position = hudmatrix * vvertex;\n"
+ " texcoord0 = vtexcoord0;\n"
+ " color = vcolor;\n"
+ "}\n",
+ "varying vec2 texcoord0;\n"
+ "varying vec4 color;\n"
+ "uniform sampler2D tex0;\n"
+ "void main(void) {\n"
+ " gl_FragColor = color * texture2D(tex0, texcoord0);\n"
+ "}\n");
+ hudnotextureshader = newshader(0, "<init>hudnotexture",
+ "attribute vec4 vvertex, vcolor;\n"
+ "uniform mat4 hudmatrix;\n"
+ "varying vec4 color;\n"
+ "void main(void) {\n"
+ " gl_Position = hudmatrix * vvertex;\n"
+ " color = vcolor;\n"
+ "}\n",
+ "varying vec4 color;\n"
+ "void main(void) {\n"
+ " gl_FragColor = color;\n"
+ "}\n");
+ standardshaders = false;
+
+ if(!nullshader || !hudshader || !hudnotextureshader) fatal("failed to setup shaders");
+
+ dummyslot.shader = nullshader;
+}
+
+static const char *findglslmain(const char *s)
+{
+ const char *main = strstr(s, "main");
+ if(!main) return NULL;
+ for(; main >= s; main--) switch(*main) { case '\r': case '\n': case ';': return main + 1; }
+ return s;
+}
+
+static void gengenericvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 0)
+{
+ int rowoffset = 0;
+ bool vschanged = false, pschanged = false;
+ vector<char> vsv, psv;
+ vsv.put(vs, strlen(vs)+1);
+ psv.put(ps, strlen(ps)+1);
+
+ static const int len = strlen("//:variant"), olen = strlen("override");
+ for(char *vspragma = vsv.getbuf();; vschanged = true)
+ {
+ vspragma = strstr(vspragma, "//:variant");
+ if(!vspragma) break;
+ if(sscanf(vspragma + len, "row %d", &rowoffset) == 1) continue;
+ memset(vspragma, ' ', len);
+ vspragma += len;
+ if(!strncmp(vspragma, "override", olen))
+ {
+ memset(vspragma, ' ', olen);
+ vspragma += olen;
+ char *end = vspragma + strcspn(vspragma, "\n\r");
+ end += strspn(end, "\n\r");
+ int endlen = strcspn(end, "\n\r");
+ memset(end, ' ', endlen);
+ }
+ }
+ for(char *pspragma = psv.getbuf();; pschanged = true)
+ {
+ pspragma = strstr(pspragma, "//:variant");
+ if(!pspragma) break;
+ if(sscanf(pspragma + len, "row %d", &rowoffset) == 1) continue;
+ memset(pspragma, ' ', len);
+ pspragma += len;
+ if(!strncmp(pspragma, "override", olen))
+ {
+ memset(pspragma, ' ', olen);
+ pspragma += olen;
+ char *end = pspragma + strcspn(pspragma, "\n\r");
+ end += strspn(end, "\n\r");
+ int endlen = strcspn(end, "\n\r");
+ memset(end, ' ', endlen);
+ }
+ }
+ row += rowoffset;
+ if(row < 0 || row >= MAXVARIANTROWS) return;
+ int col = s.numvariants(row);
+ defformatstring(varname, "<variant:%d,%d>%s", col, row, sname);
+ string reuse;
+ if(col) formatstring(reuse, "%d", row);
+ else copystring(reuse, "");
+ newshader(s.type, varname, vschanged ? vsv.getbuf() : reuse, pschanged ? psv.getbuf() : reuse, &s, row);
+}
+
+static bool genwatervariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 2)
+{
+ if(!strstr(vs, "//:water") && !strstr(ps, "//:water")) return false;
+
+ vector<char> vsw, psw;
+
+ const char *vsmain = findglslmain(vs), *vsend = strrchr(vs, '}');
+ if(!vsmain || !vsend) return false;
+ vsw.put(vs, vsmain - vs);
+ const char *fadeparams = "\nuniform vec4 waterfadeparams;\nvarying float fadedepth;\n";
+ vsw.put(fadeparams, strlen(fadeparams));
+ vsw.put(vsmain, vsend - vsmain);
+ const char *fadedef = "\nfadedepth = vvertex.z*waterfadeparams.x + waterfadeparams.y;\n";
+ vsw.put(fadedef, strlen(fadedef));
+ vsw.put(vsend, strlen(vsend)+1);
+
+ const char *psmain = findglslmain(ps), *psend = strrchr(ps, '}');
+ if(!psmain || !psend) return false;
+ psw.put(ps, psmain - ps);
+ const char *fadeinterp = "\nvarying float fadedepth;\n";
+ psw.put(fadeinterp, strlen(fadeinterp));
+ psw.put(psmain, psend - psmain);
+ const char *fade = "\ngl_FragColor.a = fadedepth;\n";
+ psw.put(fade, strlen(fade));
+ psw.put(psend, strlen(psend)+1);
+
+ defformatstring(name, "<water>%s", sname);
+ Shader *variant = newshader(s.type, name, vsw.getbuf(), psw.getbuf(), &s, row);
+ return variant!=NULL;
+}
+
+bool minimizedynlighttcusage() { return glversion < 300 && maxvaryings < 48; }
+
+static void gendynlightvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 0)
+{
+ int numlights = minimizedynlighttcusage() ? 1 : MAXDYNLIGHTS;
+
+ const char *vspragma = strstr(vs, "//:dynlight"), *pspragma = strstr(ps, "//:dynlight");
+ if(!vspragma || !pspragma) return;
+
+ string pslight;
+ vspragma += strcspn(vspragma, "\n");
+ if(*vspragma) vspragma++;
+
+ if(sscanf(pspragma, "//:dynlight %100s", pslight)!=1) return;
+
+ pspragma += strcspn(pspragma, "\n");
+ if(*pspragma) pspragma++;
+
+ const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps);
+ if(vsmain > vspragma) vsmain = vs;
+ if(psmain > pspragma) psmain = ps;
+
+ vector<char> vsdl, psdl;
+ loopi(MAXDYNLIGHTS)
+ {
+ vsdl.setsize(0);
+ psdl.setsize(0);
+ if(vsmain >= vs) vsdl.put(vs, vsmain - vs);
+ if(psmain >= ps) psdl.put(ps, psmain - ps);
+
+ defformatstring(pos, "uniform vec4 dynlightpos[%d];\n", i+1);
+ vsdl.put(pos, strlen(pos));
+ psdl.put(pos, strlen(pos));
+ defformatstring(color, "uniform vec3 dynlightcolor[%d];\n", i+1);
+ psdl.put(color, strlen(color));
+
+ loopk(min(i+1, numlights))
+ {
+ defformatstring(dir, "%sdynlight%ddir%s", !k ? "varying vec3 " : " ", k, k==i || k+1==numlights ? ";\n" : ",");
+ vsdl.put(dir, strlen(dir));
+ psdl.put(dir, strlen(dir));
+ }
+
+ vsdl.put(vsmain, vspragma-vsmain);
+ psdl.put(psmain, pspragma-psmain);
+
+ loopk(i+1)
+ {
+ defformatstring(tc,
+ k<numlights ?
+ "dynlight%ddir = vvertex.xyz*dynlightpos[%d].w + dynlightpos[%d].xyz;\n" :
+ "vec3 dynlight%ddir = dynlight0dir*dynlightpos[%d].w + dynlightpos[%d].xyz;\n",
+ k, k, k);
+ if(k < numlights) vsdl.put(tc, strlen(tc));
+ else psdl.put(tc, strlen(tc));
+
+ defformatstring(dl,
+ "%s.rgb += dynlightcolor[%d] * (1.0 - clamp(dot(dynlight%ddir, dynlight%ddir), 0.0, 1.0));\n",
+ pslight, k, k, k);
+ psdl.put(dl, strlen(dl));
+ }
+
+ vsdl.put(vspragma, strlen(vspragma)+1);
+ psdl.put(pspragma, strlen(pspragma)+1);
+
+ defformatstring(name, "<dynlight %d>%s", i+1, sname);
+ Shader *variant = newshader(s.type, name, vsdl.getbuf(), psdl.getbuf(), &s, row);
+ if(!variant) return;
+ if(row < 4) genwatervariant(s, name, vsdl.getbuf(), psdl.getbuf(), row+2);
+ }
+}
+
+static void genshadowmapvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 1)
+{
+ const char *vspragma = strstr(vs, "//:shadowmap"), *pspragma = strstr(ps, "//:shadowmap");
+ if(!vspragma || !pspragma) return;
+
+ string pslight;
+ vspragma += strcspn(vspragma, "\n");
+ if(*vspragma) vspragma++;
+
+ if(sscanf(pspragma, "//:shadowmap %100s", pslight)!=1) return;
+
+ pspragma += strcspn(pspragma, "\n");
+ if(*pspragma) pspragma++;
+
+ const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps);
+ if(vsmain > vspragma) vsmain = vs;
+ if(psmain > pspragma) psmain = ps;
+
+ vector<char> vssm, pssm;
+ if(vsmain >= vs) vssm.put(vs, vsmain - vs);
+ if(psmain >= ps) pssm.put(ps, psmain - ps);
+
+ const char *vsdecl =
+ "uniform mat4 shadowmapproject;\n"
+ "varying vec3 shadowmaptc;\n";
+ vssm.put(vsdecl, strlen(vsdecl));
+
+ const char *psdecl =
+ "uniform sampler2D shadowmap;\n"
+ "uniform vec4 shadowmapambient;\n"
+ "varying vec3 shadowmaptc;\n";
+ pssm.put(psdecl, strlen(psdecl));
+
+ vssm.put(vsmain, vspragma-vsmain);
+ pssm.put(psmain, pspragma-psmain);
+
+ extern int smoothshadowmappeel;
+ const char *tcgen =
+ "shadowmaptc = vec3(shadowmapproject * vvertex);\n";
+ vssm.put(tcgen, strlen(tcgen));
+ const char *sm =
+ smoothshadowmappeel ?
+ "vec4 smvals = texture2D(shadowmap, shadowmaptc.xy);\n"
+ "vec2 smdiff = clamp(smvals.xz - shadowmaptc.zz*smvals.y, 0.0, 1.0);\n"
+ "float shadowed = clamp((smdiff.x > 0.0 ? smvals.w : 0.0) - 8.0*smdiff.y, 0.0, 1.0);\n" :
+
+ "vec4 smvals = texture2D(shadowmap, shadowmaptc.xy);\n"
+ "float smtest = shadowmaptc.z*smvals.y;\n"
+ "float shadowed = smtest < smvals.x && smtest > smvals.z ? smvals.w : 0.0;\n";
+ pssm.put(sm, strlen(sm));
+ defformatstring(smlight,
+ "%s.rgb -= shadowed*clamp(%s.rgb - shadowmapambient.rgb, 0.0, 1.0);\n",
+ pslight, pslight);
+ pssm.put(smlight, strlen(smlight));
+
+ vssm.put(vspragma, strlen(vspragma)+1);
+ pssm.put(pspragma, strlen(pspragma)+1);
+
+ defformatstring(name, "<shadowmap>%s", sname);
+ Shader *variant = newshader(s.type, name, vssm.getbuf(), pssm.getbuf(), &s, row);
+ if(!variant) return;
+ genwatervariant(s, name, vssm.getbuf(), pssm.getbuf(), row+2);
+
+ if(strstr(vs, "//:dynlight")) gendynlightvariant(s, name, vssm.getbuf(), pssm.getbuf(), row);
+}
+
+static void genfogshader(vector<char> &vsbuf, vector<char> &psbuf, const char *vs, const char *ps)
+{
+ const char *vspragma = strstr(vs, "//:fog"), *pspragma = strstr(ps, "//:fog");
+ if(!vspragma && !pspragma) return;
+ static const int pragmalen = strlen("//:fog");
+ const char *vsmain = findglslmain(vs), *vsend = strrchr(vs, '}');
+ if(vsmain && vsend)
+ {
+ vsbuf.put(vs, vsmain - vs);
+ const char *fogparams = "\nuniform vec4 fogplane;\nvarying float fogcoord;\n";
+ vsbuf.put(fogparams, strlen(fogparams));
+ vsbuf.put(vsmain, vsend - vsmain);
+ const char *vsfog = "\nfogcoord = dot(fogplane, gl_Position);\n";
+ vsbuf.put(vsfog, strlen(vsfog));
+ vsbuf.put(vsend, strlen(vsend)+1);
+ }
+ const char *psmain = findglslmain(ps), *psend = strrchr(ps, '}');
+ if(psmain && psend)
+ {
+ psbuf.put(ps, psmain - ps);
+ const char *fogparams =
+ "\nuniform vec3 fogcolor;\n"
+ "uniform vec2 fogparams;\n"
+ "varying float fogcoord;\n";
+ psbuf.put(fogparams, strlen(fogparams));
+ psbuf.put(psmain, psend - psmain);
+ const char *psdef = "\n#define FOG_COLOR ";
+ const char *psfog =
+ pspragma && !strncmp(pspragma+pragmalen, "rgba", 4) ?
+ "\ngl_FragColor = mix((FOG_COLOR), gl_FragColor, clamp(fogcoord*fogparams.x + fogparams.y, 0.0, 1.0));\n" :
+ "\ngl_FragColor.rgb = mix((FOG_COLOR).rgb, gl_FragColor.rgb, clamp(fogcoord*fogparams.x + fogparams.y, 0.0, 1.0));\n";
+ int clen = 0;
+ if(pspragma)
+ {
+ pspragma += pragmalen;
+ while(iscubealpha(*pspragma)) pspragma++;
+ while(*pspragma && !iscubespace(*pspragma)) pspragma++;
+ pspragma += strspn(pspragma, " \t\v\f");
+ clen = strcspn(pspragma, "\r\n");
+ }
+ if(clen <= 0) { pspragma = "fogcolor"; clen = strlen(pspragma); }
+ psbuf.put(psdef, strlen(psdef));
+ psbuf.put(pspragma, clen);
+ psbuf.put(psfog, strlen(psfog));
+ psbuf.put(psend, strlen(psend)+1);
+ }
+}
+
+static void genuniformdefs(vector<char> &vsbuf, vector<char> &psbuf, const char *vs, const char *ps, Shader *variant = NULL)
+{
+ if(variant ? variant->defaultparams.empty() : slotparams.empty()) return;
+ const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps);
+ if(!vsmain || !psmain) return;
+ vsbuf.put(vs, vsmain - vs);
+ psbuf.put(ps, psmain - ps);
+ if(variant) loopv(variant->defaultparams)
+ {
+ defformatstring(uni, "\nuniform vec4 %s;\n", variant->defaultparams[i].name);
+ vsbuf.put(uni, strlen(uni));
+ psbuf.put(uni, strlen(uni));
+ }
+ else loopv(slotparams)
+ {
+ defformatstring(uni, "\nuniform vec4 %s;\n", slotparams[i].name);
+ vsbuf.put(uni, strlen(uni));
+ psbuf.put(uni, strlen(uni));
+ }
+ vsbuf.put(vsmain, strlen(vsmain)+1);
+ psbuf.put(psmain, strlen(psmain)+1);
+}
+
+VAR(defershaders, 0, 1, 1);
+
+void defershader(int *type, const char *name, const char *contents)
+{
+ Shader *exists = shaders.access(name);
+ if(exists && !exists->invalid()) return;
+ if(!defershaders) { execute(contents); return; }
+ char *rname = exists ? exists->name : newstring(name);
+ Shader &s = shaders[rname];
+ s.name = rname;
+ DELETEA(s.defer);
+ s.defer = newstring(contents);
+ s.type = SHADER_DEFERRED | *type;
+ s.standard = standardshaders;
+}
+
+void Shader::force()
+{
+ if(!deferred() || !defer) return;
+
+ char *cmd = defer;
+ defer = NULL;
+ bool wasstandard = standardshaders, wasforcing = forceshaders;
+ int oldflags = identflags;
+ standardshaders = standard;
+ forceshaders = false;
+ identflags &= ~IDF_PERSIST;
+ slotparams.shrink(0);
+ execute(cmd);
+ identflags = oldflags;
+ forceshaders = wasforcing;
+ standardshaders = wasstandard;
+ delete[] cmd;
+
+ if(deferred())
+ {
+ DELETEA(defer);
+ type = SHADER_INVALID;
+ }
+}
+
+void fixshaderdetail()
+{
+ // must null out separately because fixdetailshader can recursively set it
+ enumerate(shaders, Shader, s, { if(!s.forced) s.detailshader = NULL; });
+ enumerate(shaders, Shader, s, { if(s.forced) s.fixdetailshader(); });
+ linkslotshaders();
+}
+
+int Shader::uniformlocversion()
+{
+ static int version = 0;
+ if(++version >= 0) return version;
+ version = 0;
+ enumerate(shaders, Shader, s, { loopvj(s.uniformlocs) s.uniformlocs[j].version = -1; });
+ return version;
+}
+
+VARFP(shaderdetail, 0, MAXSHADERDETAIL, MAXSHADERDETAIL, fixshaderdetail());
+
+void Shader::fixdetailshader(bool shouldforce, bool recurse)
+{
+ Shader *alt = this;
+ detailshader = NULL;
+ do
+ {
+ Shader *cur = shaderdetail < MAXSHADERDETAIL ? alt->fastshader[shaderdetail] : alt;
+ if(cur->deferred() && shouldforce) cur->force();
+ if(!cur->invalid())
+ {
+ if(cur->deferred()) break;
+ detailshader = cur;
+ break;
+ }
+ alt = alt->altshader;
+ } while(alt && alt!=this);
+
+ if(recurse && detailshader) loopv(detailshader->variants) detailshader->variants[i]->fixdetailshader(shouldforce, false);
+}
+
+Shader *useshaderbyname(const char *name)
+{
+ Shader *s = shaders.access(name);
+ if(!s) return NULL;
+ if(!s->detailshader) s->fixdetailshader();
+ s->forced = true;
+ return s;
+}
+
+void shader(int *type, char *name, char *vs, char *ps)
+{
+ if(lookupshaderbyname(name)) return;
+
+ defformatstring(info, "shader %s", name);
+ renderprogress(loadprogress, info);
+
+ vector<char> vsbuf, psbuf, vsbak, psbak;
+#define GENSHADER(cond, body) \
+ if(cond) \
+ { \
+ if(vsbuf.length()) { vsbak.setsize(0); vsbak.put(vs, strlen(vs)+1); vs = vsbak.getbuf(); vsbuf.setsize(0); } \
+ if(psbuf.length()) { psbak.setsize(0); psbak.put(ps, strlen(ps)+1); ps = psbak.getbuf(); psbuf.setsize(0); } \
+ body; \
+ if(vsbuf.length()) vs = vsbuf.getbuf(); \
+ if(psbuf.length()) ps = psbuf.getbuf(); \
+ }
+ GENSHADER(slotparams.length(), genuniformdefs(vsbuf, psbuf, vs, ps));
+ GENSHADER(strstr(vs, "//:fog") || strstr(ps, "//:fog"), genfogshader(vsbuf, psbuf, vs, ps));
+ Shader *s = newshader(*type, name, vs, ps);
+ if(s)
+ {
+ if(strstr(vs, "//:water")) genwatervariant(*s, s->name, vs, ps);
+ if(strstr(vs, "//:shadowmap")) genshadowmapvariant(*s, s->name, vs, ps);
+ if(strstr(vs, "//:dynlight")) gendynlightvariant(*s, s->name, vs, ps);
+ }
+ slotparams.shrink(0);
+}
+
+void variantshader(int *type, char *name, int *row, char *vs, char *ps)
+{
+ if(*row < 0)
+ {
+ shader(type, name, vs, ps);
+ return;
+ }
+ else if(*row >= MAXVARIANTROWS) return;
+
+ Shader *s = lookupshaderbyname(name);
+ if(!s) return;
+
+ defformatstring(varname, "<variant:%d,%d>%s", s->numvariants(*row), *row, name);
+ //defformatstring(info, "shader %s", varname);
+ //renderprogress(loadprogress, info);
+ vector<char> vsbuf, psbuf, vsbak, psbak;
+ GENSHADER(s->defaultparams.length(), genuniformdefs(vsbuf, psbuf, vs, ps, s));
+ GENSHADER(strstr(vs, "//:fog") || strstr(ps, "//:fog"), genfogshader(vsbuf, psbuf, vs, ps));
+ Shader *v = newshader(*type, varname, vs, ps, s, *row);
+ if(v)
+ {
+ if(strstr(vs, "//:dynlight")) gendynlightvariant(*s, varname, vs, ps, *row);
+ if(strstr(ps, "//:variant") || strstr(vs, "//:variant")) gengenericvariant(*s, varname, vs, ps, *row);
+ }
+}
+
+void setshader(char *name)
+{
+ slotparams.shrink(0);
+ Shader *s = shaders.access(name);
+ if(!s)
+ {
+ conoutf(CON_ERROR, "no such shader: %s", name);
+ }
+ else slotshader = s;
+}
+
+void resetslotshader()
+{
+ slotshader = NULL;
+ slotparams.shrink(0);
+}
+
+void setslotshader(Slot &s)
+{
+ s.shader = slotshader;
+ if(!s.shader)
+ {
+ s.shader = stdworldshader;
+ return;
+ }
+ loopv(slotparams) s.params.add(slotparams[i]);
+}
+
+static void linkslotshaderparams(vector<SlotShaderParam> &params, Shader *sh, bool load)
+{
+ if(sh) loopv(params)
+ {
+ int loc = -1;
+ SlotShaderParam &param = params[i];
+ loopv(sh->defaultparams)
+ {
+ SlotShaderParamState &dparam = sh->defaultparams[i];
+ if(dparam.name==param.name)
+ {
+ if(memcmp(param.val, dparam.val, sizeof(param.val))) loc = i;
+ break;
+ }
+ }
+ param.loc = loc;
+ }
+ else if(load) loopv(params) params[i].loc = -1;
+}
+
+void linkslotshader(Slot &s, bool load)
+{
+ if(!s.shader) return;
+
+ if(load && !s.shader->detailshader) s.shader->fixdetailshader();
+
+ linkslotshaderparams(s.params, s.shader->detailshader, load);
+}
+
+void linkvslotshader(VSlot &s, bool load)
+{
+ if(!s.slot->shader) return;
+
+ Shader *sh = s.slot->shader->detailshader;
+ linkslotshaderparams(s.params, sh, load);
+
+ if(!sh) return;
+
+ if(s.slot->texmask&(1<<TEX_GLOW))
+ {
+ static const char *paramname = getshaderparamname("glowcolor");
+ const float *param = findslotparam(s, paramname);
+ if(param) s.glowcolor = vec(param).clamp(0, 1);
+ }
+}
+
+void altshader(char *origname, char *altname)
+{
+ Shader *orig = shaders.access(origname), *alt = shaders.access(altname);
+ if(!orig || !alt) return;
+ orig->altshader = alt;
+ orig->fixdetailshader(false);
+}
+
+void fastshader(char *nice, char *fast, int *detail)
+{
+ Shader *ns = shaders.access(nice), *fs = shaders.access(fast);
+ if(!ns || !fs) return;
+ loopi(min(*detail+1, MAXSHADERDETAIL)) ns->fastshader[i] = fs;
+ ns->fixdetailshader(false);
+}
+
+COMMAND(shader, "isss");
+COMMAND(variantshader, "isiss");
+COMMAND(setshader, "s");
+COMMAND(altshader, "ss");
+COMMAND(fastshader, "ssi");
+COMMAND(defershader, "iss");
+ICOMMAND(forceshader, "s", (const char *name), useshaderbyname(name));
+
+ICOMMAND(isshaderdefined, "s", (char *name), intret(lookupshaderbyname(name) ? 1 : 0));
+
+static hashset<const char *> shaderparamnames(256);
+
+const char *getshaderparamname(const char *name, bool insert)
+{
+ const char *exists = shaderparamnames.find(name, NULL);
+ if(exists || !insert) return exists;
+ return shaderparamnames.add(newstring(name));
+}
+
+void addslotparam(const char *name, float x, float y, float z, float w)
+{
+ if(name) name = getshaderparamname(name);
+ loopv(slotparams)
+ {
+ SlotShaderParam &param = slotparams[i];
+ if(param.name==name)
+ {
+ param.val[0] = x;
+ param.val[1] = y;
+ param.val[2] = z;
+ param.val[3] = w;
+ return;
+ }
+ }
+ SlotShaderParam param = {name, -1, {x, y, z, w}};
+ slotparams.add(param);
+}
+
+ICOMMAND(setuniformparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w));
+ICOMMAND(setshaderparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w));
+ICOMMAND(defuniformparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w));
+
+#define NUMPOSTFXBINDS 10
+
+struct postfxtex
+{
+ GLuint id;
+ int scale, used;
+
+ postfxtex() : id(0), scale(0), used(-1) {}
+};
+vector<postfxtex> postfxtexs;
+int postfxbinds[NUMPOSTFXBINDS];
+GLuint postfxfb = 0;
+int postfxw = 0, postfxh = 0;
+
+struct postfxpass
+{
+ Shader *shader;
+ vec4 params;
+ uint inputs, freeinputs;
+ int outputbind, outputscale;
+
+ postfxpass() : shader(NULL), inputs(1), freeinputs(1), outputbind(0), outputscale(0) {}
+};
+vector<postfxpass> postfxpasses;
+
+static int allocatepostfxtex(int scale)
+{
+ loopv(postfxtexs)
+ {
+ postfxtex &t = postfxtexs[i];
+ if(t.scale==scale && t.used < 0) return i;
+ }
+ postfxtex &t = postfxtexs.add();
+ t.scale = scale;
+ glGenTextures(1, &t.id);
+ createtexture(t.id, max(screenw>>scale, 1), max(screenh>>scale, 1), NULL, 3, 1, GL_RGB);
+ return postfxtexs.length()-1;
+}
+
+void cleanuppostfx(bool fullclean)
+{
+ if(fullclean && postfxfb)
+ {
+ glDeleteFramebuffers_(1, &postfxfb);
+ postfxfb = 0;
+ }
+
+ loopv(postfxtexs) glDeleteTextures(1, &postfxtexs[i].id);
+ postfxtexs.shrink(0);
+
+ postfxw = 0;
+ postfxh = 0;
+}
+
+void renderpostfx()
+{
+ if(postfxpasses.empty()) return;
+
+ if(postfxw != screenw || postfxh != screenh)
+ {
+ cleanuppostfx(false);
+ postfxw = screenw;
+ postfxh = screenh;
+ }
+
+ int binds[NUMPOSTFXBINDS];
+ loopi(NUMPOSTFXBINDS) binds[i] = -1;
+ loopv(postfxtexs) postfxtexs[i].used = -1;
+
+ binds[0] = allocatepostfxtex(0);
+ postfxtexs[binds[0]].used = 0;
+ glBindTexture(GL_TEXTURE_2D, postfxtexs[binds[0]].id);
+ glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh);
+
+ if(postfxpasses.length() > 1)
+ {
+ if(!postfxfb) glGenFramebuffers_(1, &postfxfb);
+ glBindFramebuffer_(GL_FRAMEBUFFER, postfxfb);
+ }
+
+ GLOBALPARAMF(millis, lastmillis/1000.0f);
+
+ loopv(postfxpasses)
+ {
+ postfxpass &p = postfxpasses[i];
+
+ int tex = -1;
+ if(!postfxpasses.inrange(i+1))
+ {
+ if(postfxpasses.length() > 1) glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+ }
+ else
+ {
+ tex = allocatepostfxtex(p.outputscale);
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, postfxtexs[tex].id, 0);
+ }
+
+ int w = tex >= 0 ? max(screenw>>postfxtexs[tex].scale, 1) : screenw,
+ h = tex >= 0 ? max(screenh>>postfxtexs[tex].scale, 1) : screenh;
+ glViewport(0, 0, w, h);
+ p.shader->set();
+ LOCALPARAM(params, p.params);
+ int tw = w, th = h, tmu = 0;
+ loopj(NUMPOSTFXBINDS) if(p.inputs&(1<<j) && binds[j] >= 0)
+ {
+ if(!tmu)
+ {
+ tw = max(screenw>>postfxtexs[binds[j]].scale, 1);
+ th = max(screenh>>postfxtexs[binds[j]].scale, 1);
+ }
+ else glActiveTexture_(GL_TEXTURE0 + tmu);
+ glBindTexture(GL_TEXTURE_2D, postfxtexs[binds[j]].id);
+ ++tmu;
+ }
+ if(tmu) glActiveTexture_(GL_TEXTURE0);
+ LOCALPARAMF(postfxscale, 1.0f/tw, 1.0f/th);
+ screenquad(1, 1);
+
+ loopj(NUMPOSTFXBINDS) if(p.freeinputs&(1<<j) && binds[j] >= 0)
+ {
+ postfxtexs[binds[j]].used = -1;
+ binds[j] = -1;
+ }
+ if(tex >= 0)
+ {
+ if(binds[p.outputbind] >= 0) postfxtexs[binds[p.outputbind]].used = -1;
+ binds[p.outputbind] = tex;
+ postfxtexs[tex].used = p.outputbind;
+ }
+ }
+}
+
+static bool addpostfx(const char *name, int outputbind, int outputscale, uint inputs, uint freeinputs, const vec4 &params)
+{
+ if(!*name) return false;
+ Shader *s = useshaderbyname(name);
+ if(!s)
+ {
+ conoutf(CON_ERROR, "no such postfx shader: %s", name);
+ return false;
+ }
+ postfxpass &p = postfxpasses.add();
+ p.shader = s;
+ p.outputbind = outputbind;
+ p.outputscale = outputscale;
+ p.inputs = inputs;
+ p.freeinputs = freeinputs;
+ p.params = params;
+ return true;
+}
+
+void clearpostfx()
+{
+ postfxpasses.shrink(0);
+ cleanuppostfx(false);
+}
+
+COMMAND(clearpostfx, "");
+
+ICOMMAND(addpostfx, "siisffff", (char *name, int *bind, int *scale, char *inputs, float *x, float *y, float *z, float *w),
+{
+ int inputmask = inputs[0] ? 0 : 1;
+ int freemask = inputs[0] ? 0 : 1;
+ bool freeinputs = true;
+ for(; *inputs; inputs++) if(isdigit(*inputs))
+ {
+ inputmask |= 1<<(*inputs-'0');
+ if(freeinputs) freemask |= 1<<(*inputs-'0');
+ }
+ else if(*inputs=='+') freeinputs = false;
+ else if(*inputs=='-') freeinputs = true;
+ inputmask &= (1<<NUMPOSTFXBINDS)-1;
+ freemask &= (1<<NUMPOSTFXBINDS)-1;
+ addpostfx(name, clamp(*bind, 0, NUMPOSTFXBINDS-1), max(*scale, 0), inputmask, freemask, vec4(*x, *y, *z, *w));
+});
+
+ICOMMAND(setpostfx, "sffff", (char *name, float *x, float *y, float *z, float *w),
+{
+ clearpostfx();
+ if(name[0]) addpostfx(name, 0, 0, 1, 1, vec4(*x, *y, *z, *w));
+});
+
+void cleanupshaders()
+{
+ cleanuppostfx(true);
+
+ loadedshaders = false;
+ nullshader = hudshader = hudnotextureshader = textureshader = notextureshader = nocolorshader = foggedshader = foggednotextureshader = stdworldshader = NULL;
+ enumerate(shaders, Shader, s, s.cleanup());
+ Shader::lastshader = NULL;
+ glUseProgram_(0);
+}
+
+void reloadshaders()
+{
+ identflags &= ~IDF_PERSIST;
+ loadshaders();
+ identflags |= IDF_PERSIST;
+ linkslotshaders();
+ enumerate(shaders, Shader, s,
+ {
+ if(!s.standard && !(s.type&(SHADER_DEFERRED|SHADER_INVALID)) && !s.variantshader)
+ {
+ defformatstring(info, "shader %s", s.name);
+ renderprogress(0.0, info);
+ if(!s.compile()) s.cleanup(true);
+ loopv(s.variants)
+ {
+ Shader *v = s.variants[i];
+ if((v->reusevs && v->reusevs->invalid()) ||
+ (v->reuseps && v->reuseps->invalid()) ||
+ !v->compile())
+ v->cleanup(true);
+ }
+ }
+ if(s.forced && !s.detailshader) s.fixdetailshader();
+ });
+}
+
+void setupblurkernel(int radius, float sigma, float *weights, float *offsets)
+{
+ if(radius<1 || radius>MAXBLURRADIUS) return;
+ sigma *= 2*radius;
+ float total = 1.0f/sigma;
+ weights[0] = total;
+ offsets[0] = 0;
+ // rely on bilinear filtering to sample 2 pixels at once
+ // transforms a*X + b*Y into (u+v)*[X*u/(u+v) + Y*(1 - u/(u+v))]
+ loopi(radius)
+ {
+ float weight1 = exp(-((2*i)*(2*i)) / (2*sigma*sigma)) / sigma,
+ weight2 = exp(-((2*i+1)*(2*i+1)) / (2*sigma*sigma)) / sigma,
+ scale = weight1 + weight2,
+ offset = 2*i+1 + weight2 / scale;
+ weights[i+1] = scale;
+ offsets[i+1] = offset;
+ total += 2*scale;
+ }
+ loopi(radius+1) weights[i] /= total;
+ for(int i = radius+1; i <= MAXBLURRADIUS; i++) weights[i] = offsets[i] = 0;
+}
+
+void setblurshader(int pass, int size, int radius, float *weights, float *offsets)
+{
+ if(radius<1 || radius>MAXBLURRADIUS) return;
+ static Shader *blurshader[7][2] = { { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL } };
+ Shader *&s = blurshader[radius-1][pass];
+ if(!s)
+ {
+ defformatstring(name, "blur%c%d", 'x'+pass, radius);
+ s = lookupshaderbyname(name);
+ }
+ s->set();
+ LOCALPARAMV(weights, weights, 8);
+ float scaledoffsets[8];
+ loopk(8) scaledoffsets[k] = offsets[k]/size;
+ LOCALPARAMV(offsets, scaledoffsets, 8);
+}
+
diff --git a/src/engine/shadowmap.cpp b/src/engine/shadowmap.cpp
new file mode 100644
index 0000000..4dafbd8
--- /dev/null
+++ b/src/engine/shadowmap.cpp
@@ -0,0 +1,329 @@
+#include "engine.h"
+#include "rendertarget.h"
+
+VARP(shadowmap, 0, 0, 1);
+
+extern void cleanshadowmap();
+VARFP(shadowmapsize, 7, 9, 11, cleanshadowmap());
+VARP(shadowmapradius, 64, 96, 256);
+VAR(shadowmapheight, 0, 32, 128);
+VARP(shadowmapdist, 128, 256, 512);
+VARFP(fpshadowmap, 0, 0, 1, cleanshadowmap());
+VARFP(shadowmapprecision, 0, 0, 1, cleanshadowmap());
+bvec shadowmapambientcolor(0, 0, 0);
+HVARFR(shadowmapambient, 0, 0, 0xFFFFFF,
+{
+ if(shadowmapambient <= 255) shadowmapambient |= (shadowmapambient<<8) | (shadowmapambient<<16);
+ shadowmapambientcolor = bvec((shadowmapambient>>16)&0xFF, (shadowmapambient>>8)&0xFF, shadowmapambient&0xFF);
+});
+VARP(shadowmapintensity, 0, 40, 100);
+
+VARP(blurshadowmap, 0, 1, 3);
+VARP(blursmsigma, 1, 100, 200);
+
+#define SHADOWSKEW 0.7071068f
+
+vec shadowoffset(0, 0, 0), shadowfocus(0, 0, 0), shadowdir(0, SHADOWSKEW, 1);
+VAR(shadowmapcasters, 1, 0, 0);
+float shadowmapmaxz = 0;
+
+void setshadowdir(int angle)
+{
+ shadowdir = vec(0, SHADOWSKEW, 1);
+ shadowdir.rotate_around_z(angle*RAD);
+}
+
+VARFR(shadowmapangle, 0, 0, 360, setshadowdir(shadowmapangle));
+
+void guessshadowdir()
+{
+ if(shadowmapangle) return;
+ vec dir;
+ if(!sunlightcolor.iszero()) dir = sunlightdir;
+ else
+ {
+ vec lightpos(0, 0, 0), casterpos(0, 0, 0);
+ int numlights = 0, numcasters = 0;
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents)
+ {
+ extentity &e = *ents[i];
+ switch(e.type)
+ {
+ case ET_LIGHT:
+ if(!e.attr1) { lightpos.add(e.o); numlights++; }
+ break;
+
+ case ET_MAPMODEL:
+ casterpos.add(e.o);
+ numcasters++;
+ break;
+
+ default:
+ if(e.type<ET_GAMESPECIFIC) break;
+ casterpos.add(e.o);
+ numcasters++;
+ break;
+ }
+ }
+ if(!numlights || !numcasters) return;
+ lightpos.div(numlights);
+ casterpos.div(numcasters);
+ dir = vec(lightpos).sub(casterpos);
+ }
+ dir.z = 0;
+ if(dir.iszero()) return;
+ dir.normalize();
+ dir.mul(SHADOWSKEW);
+ dir.z = 1;
+ shadowdir = dir;
+}
+
+bool shadowmapping = false;
+
+matrix4 shadowmatrix;
+
+VARP(shadowmapbias, 0, 5, 1024);
+VARP(shadowmappeelbias, 0, 20, 1024);
+VAR(smdepthpeel, 0, 1, 1);
+VAR(smoothshadowmappeel, 1, 0, 0);
+
+static struct shadowmaptexture : rendertarget
+{
+ const GLenum *colorformats() const
+ {
+ static const GLenum rgbafmts[] = { GL_RGBA16F, GL_RGBA16, GL_RGBA, GL_RGBA8, GL_FALSE };
+ return &rgbafmts[fpshadowmap && hasTF ? 0 : (shadowmapprecision ? 1 : 2)];
+ }
+
+ bool swaptexs() const { return true; }
+
+ bool scissorblur(int &x, int &y, int &w, int &h)
+ {
+ x = max(int(floor((scissorx1+1)/2*vieww)) - 2*blursize, 2);
+ y = max(int(floor((scissory1+1)/2*viewh)) - 2*blursize, 2);
+ w = min(int(ceil((scissorx2+1)/2*vieww)) + 2*blursize, vieww-2) - x;
+ h = min(int(ceil((scissory2+1)/2*viewh)) + 2*blursize, viewh-2) - y;
+ return true;
+ }
+
+ bool scissorrender(int &x, int &y, int &w, int &h)
+ {
+ x = y = 2;
+ w = vieww - 2*2;
+ h = viewh - 2*2;
+ return true;
+ }
+
+ void doclear()
+ {
+ glClearColor(0, 0, 0, 0);
+ glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
+ }
+
+ bool dorender()
+ {
+ vec skewdir(shadowdir);
+ skewdir.rotate_around_z(-camera1->yaw*RAD);
+
+ vec dir;
+ vecfromyawpitch(camera1->yaw, camera1->pitch, 1, 0, dir);
+ dir.z = 0;
+ dir.mul(shadowmapradius);
+
+ vec dirx, diry;
+ vecfromyawpitch(camera1->yaw, 0, 0, 1, dirx);
+ vecfromyawpitch(camera1->yaw, 0, 1, 0, diry);
+ shadowoffset.x = -fmod(dirx.dot(camera1->o) - skewdir.x*camera1->o.z, 2.0f*shadowmapradius/vieww);
+ shadowoffset.y = -fmod(diry.dot(camera1->o) - skewdir.y*camera1->o.z, 2.0f*shadowmapradius/viewh);
+
+ shadowmatrix.ortho(-shadowmapradius, shadowmapradius, -shadowmapradius, shadowmapradius, -shadowmapdist, shadowmapdist);
+ shadowmatrix.mul(matrix3(vec(1, 0, 0), vec(0, 1, 0), vec(skewdir.x, skewdir.y, 1)));
+ shadowmatrix.translate(skewdir.x*shadowmapheight + shadowoffset.x, skewdir.y*shadowmapheight + shadowoffset.y + dir.magnitude(), -shadowmapheight);
+ shadowmatrix.rotate_around_z((camera1->yaw+180)*-RAD);
+ shadowmatrix.translate(vec(camera1->o).neg());
+ GLOBALPARAM(shadowmatrix, shadowmatrix);
+
+ shadowfocus = camera1->o;
+ shadowfocus.add(dir);
+ shadowfocus.add(vec(shadowdir).mul(shadowmapheight));
+ shadowfocus.add(dirx.mul(shadowoffset.x));
+ shadowfocus.add(diry.mul(shadowoffset.y));
+
+ gle::colorf(0, 0, 0);
+
+ GLOBALPARAMF(shadowmapbias, -shadowmapbias/float(shadowmapdist), 1 - (shadowmapbias + (smoothshadowmappeel ? 0 : shadowmappeelbias))/float(shadowmapdist));
+
+ shadowmapcasters = 0;
+ shadowmapmaxz = shadowfocus.z - shadowmapdist;
+ shadowmapping = true;
+ rendergame();
+ shadowmapping = false;
+ shadowmapmaxz = min(shadowmapmaxz, shadowfocus.z);
+
+ if(shadowmapcasters && smdepthpeel)
+ {
+ int sx, sy, sw, sh;
+ bool scissoring = rtscissor && scissorblur(sx, sy, sw, sh) && sw > 0 && sh > 0;
+ if(scissoring) glScissor(sx, sy, sw, sh);
+ if(!rtscissor || scissoring) rendershadowmapreceivers();
+ }
+
+ return shadowmapcasters>0;
+ }
+
+ bool flipdebug() const { return false; }
+
+ void dodebug(int w, int h)
+ {
+ if(shadowmapcasters)
+ {
+ glColorMask(GL_TRUE, GL_FALSE, GL_FALSE, GL_FALSE);
+ debugscissor(w, h);
+ glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_FALSE);
+ debugblurtiles(w, h);
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ }
+ }
+} shadowmaptex;
+
+void cleanshadowmap()
+{
+ shadowmaptex.cleanup(true);
+}
+
+void calcshadowmapbb(const vec &o, float xyrad, float zrad, float &x1, float &y1, float &x2, float &y2)
+{
+ vec skewdir(shadowdir);
+ skewdir.rotate_around_z(-camera1->yaw*RAD);
+
+ vec ro(o);
+ ro.sub(camera1->o);
+ ro.rotate_around_z(-(camera1->yaw+180)*RAD);
+ ro.x += ro.z * skewdir.x + shadowoffset.x;
+ ro.y += ro.z * skewdir.y + shadowmapradius * cosf(camera1->pitch*RAD) + shadowoffset.y;
+
+ vec high(ro), low(ro);
+ high.x += zrad * skewdir.x;
+ high.y += zrad * skewdir.y;
+ low.x -= zrad * skewdir.x;
+ low.y -= zrad * skewdir.y;
+
+ x1 = (min(high.x, low.x) - xyrad) / shadowmapradius;
+ y1 = (min(high.y, low.y) - xyrad) / shadowmapradius;
+ x2 = (max(high.x, low.x) + xyrad) / shadowmapradius;
+ y2 = (max(high.y, low.y) + xyrad) / shadowmapradius;
+}
+
+bool addshadowmapcaster(const vec &o, float xyrad, float zrad)
+{
+ if(o.z + zrad <= shadowfocus.z - shadowmapdist || o.z - zrad >= shadowfocus.z) return false;
+
+ shadowmapmaxz = max(shadowmapmaxz, o.z + zrad);
+
+ float x1, y1, x2, y2;
+ calcshadowmapbb(o, xyrad, zrad, x1, y1, x2, y2);
+
+ if(!shadowmaptex.addblurtiles(x1, y1, x2, y2, 2)) return false;
+
+ shadowmapcasters++;
+ return true;
+}
+
+bool isshadowmapreceiver(vtxarray *va)
+{
+ if(!shadowmap || !shadowmapcasters) return false;
+
+ if(va->shadowmapmax.z <= shadowfocus.z - shadowmapdist || va->shadowmapmin.z >= shadowmapmaxz) return false;
+
+ float xyrad = SQRT2*0.5f*max(va->shadowmapmax.x-va->shadowmapmin.x, va->shadowmapmax.y-va->shadowmapmin.y),
+ zrad = 0.5f*(va->shadowmapmax.z-va->shadowmapmin.z),
+ x1, y1, x2, y2;
+ if(xyrad<0 || zrad<0) return false;
+
+ vec center = vec(va->shadowmapmin).add(vec(va->shadowmapmax)).mul(0.5f);
+ calcshadowmapbb(center, xyrad, zrad, x1, y1, x2, y2);
+
+ return shadowmaptex.checkblurtiles(x1, y1, x2, y2, 2);
+
+#if 0
+ // cheaper inexact test
+ float dz = va->o.z + va->size/2 - shadowfocus.z;
+ float cx = shadowfocus.x + dz*shadowdir.x, cy = shadowfocus.y + dz*shadowdir.y;
+ float skew = va->size/2*SHADOWSKEW;
+ if(!shadowmap || !shadowmaptex ||
+ va->o.z + va->size <= shadowfocus.z - shadowmapdist || va->o.z >= shadowmapmaxz ||
+ va->o.x + va->size <= cx - shadowmapradius-skew || va->o.x >= cx + shadowmapradius+skew ||
+ va->o.y + va->size <= cy - shadowmapradius-skew || va->o.y >= cy + shadowmapradius+skew)
+ return false;
+ return true;
+#endif
+}
+
+bool isshadowmapcaster(const vec &o, float rad)
+{
+ // cheaper inexact test
+ float dz = o.z - shadowfocus.z;
+ float cx = shadowfocus.x + dz*shadowdir.x, cy = shadowfocus.y + dz*shadowdir.y;
+ float skew = rad*SHADOWSKEW;
+ if(!shadowmapping ||
+ o.z + rad <= shadowfocus.z - shadowmapdist || o.z - rad >= shadowfocus.z ||
+ o.x + rad <= cx - shadowmapradius-skew || o.x - rad >= cx + shadowmapradius+skew ||
+ o.y + rad <= cy - shadowmapradius-skew || o.y - rad >= cy + shadowmapradius+skew)
+ return false;
+ return true;
+}
+
+void pushshadowmap()
+{
+ if(!shadowmap || !shadowmaptex.rendertex) return;
+
+ glActiveTexture_(GL_TEXTURE7);
+ glBindTexture(GL_TEXTURE_2D, shadowmaptex.rendertex);
+
+ matrix4 m = shadowmatrix;
+ m.projective(-1, 1-shadowmapbias/float(shadowmapdist));
+ GLOBALPARAM(shadowmapproject, m);
+
+ glActiveTexture_(GL_TEXTURE0);
+
+ float r, g, b;
+ if(!shadowmapambient)
+ {
+ if(skylightcolor[0] || skylightcolor[1] || skylightcolor[2])
+ {
+ r = max(25.0f, 0.4f*ambientcolor[0] + 0.6f*max(ambientcolor[0], skylightcolor[0]));
+ g = max(25.0f, 0.4f*ambientcolor[1] + 0.6f*max(ambientcolor[1], skylightcolor[1]));
+ b = max(25.0f, 0.4f*ambientcolor[2] + 0.6f*max(ambientcolor[2], skylightcolor[2]));
+ }
+ else
+ {
+ r = max(25.0f, 2.0f*ambientcolor[0]);
+ g = max(25.0f, 2.0f*ambientcolor[1]);
+ b = max(25.0f, 2.0f*ambientcolor[2]);
+ }
+ }
+ else { r = shadowmapambientcolor[0]; g = shadowmapambientcolor[1]; b = shadowmapambientcolor[2]; }
+ GLOBALPARAMF(shadowmapambient, r/255.0f, g/255.0f, b/255.0f);
+}
+
+void popshadowmap()
+{
+ if(!shadowmap || !shadowmaptex.rendertex) return;
+}
+
+void rendershadowmap()
+{
+ if(!shadowmap) return;
+
+ shadowmaptex.render(1<<shadowmapsize, 1<<shadowmapsize, blurshadowmap, blursmsigma/100.0f);
+}
+
+VAR(debugsm, 0, 0, 1);
+
+void viewshadowmap()
+{
+ if(!shadowmap) return;
+ shadowmaptex.debug();
+}
+
diff --git a/src/engine/skelmodel.h b/src/engine/skelmodel.h
new file mode 100644
index 0000000..6d36b35
--- /dev/null
+++ b/src/engine/skelmodel.h
@@ -0,0 +1,1861 @@
+VARP(gpuskel, 0, 1, 1);
+
+VAR(maxskelanimdata, 1, 192, 0);
+VAR(testtags, 0, 0, 1);
+
+#define BONEMASK_NOT 0x8000
+#define BONEMASK_END 0xFFFF
+#define BONEMASK_BONE 0x7FFF
+
+struct skelmodel : animmodel
+{
+ struct vert { vec pos, norm; vec2 tc; int blend, interpindex; };
+ struct vvert { vec pos; vec2 tc; };
+ struct vvertn : vvert { vec norm; };
+ struct vvertbump : vvert { squat tangent; };
+ struct vvertw { uchar weights[4]; uchar bones[4]; };
+ struct vvertnw : vvertn, vvertw {};
+ struct vvertbumpw : vvertbump, vvertw {};
+ struct bumpvert { quat tangent; };
+ struct tri { ushort vert[3]; };
+
+ struct blendcombo
+ {
+ int uses, interpindex;
+ float weights[4];
+ uchar bones[4], interpbones[4];
+
+ blendcombo() : uses(1)
+ {
+ }
+
+ bool operator==(const blendcombo &c) const
+ {
+ loopk(4) if(bones[k] != c.bones[k]) return false;
+ loopk(4) if(weights[k] != c.weights[k]) return false;
+ return true;
+ }
+
+ int size() const
+ {
+ int i = 1;
+ while(i < 4 && weights[i]) i++;
+ return i;
+ }
+
+ static bool sortcmp(const blendcombo &x, const blendcombo &y)
+ {
+ loopi(4)
+ {
+ if(x.weights[i])
+ {
+ if(!y.weights[i]) return true;
+ }
+ else if(y.weights[i]) return false;
+ else break;
+ }
+ return false;
+ }
+
+ int addweight(int sorted, float weight, int bone)
+ {
+ if(weight <= 1e-3f) return sorted;
+ loopk(sorted) if(weight > weights[k])
+ {
+ for(int l = min(sorted-1, 2); l >= k; l--)
+ {
+ weights[l+1] = weights[l];
+ bones[l+1] = bones[l];
+ }
+ weights[k] = weight;
+ bones[k] = bone;
+ return sorted<4 ? sorted+1 : sorted;
+ }
+ if(sorted>=4) return sorted;
+ weights[sorted] = weight;
+ bones[sorted] = bone;
+ return sorted+1;
+ }
+
+ void finalize(int sorted)
+ {
+ loopj(4-sorted) { weights[sorted+j] = 0; bones[sorted+j] = 0; }
+ if(sorted <= 0) return;
+ float total = 0;
+ loopj(sorted) total += weights[j];
+ total = 1.0f/total;
+ loopj(sorted) weights[j] *= total;
+ }
+
+ void serialize(vvertw &v)
+ {
+ if(interpindex >= 0)
+ {
+ v.weights[0] = 255;
+ loopk(3) v.weights[k+1] = 0;
+ v.bones[0] = 2*interpindex;
+ loopk(3) v.bones[k+1] = v.bones[0];
+ }
+ else
+ {
+ int total = 0;
+ loopk(4) total += (v.weights[k] = uchar(0.5f + weights[k]*255));
+ while(total > 255)
+ {
+ loopk(4) if(v.weights[k] > 0 && total > 255) { v.weights[k]--; total--; }
+ }
+ while(total < 255)
+ {
+ loopk(4) if(v.weights[k] < 255 && total < 255) { v.weights[k]++; total++; }
+ }
+ loopk(4) v.bones[k] = 2*interpbones[k];
+ }
+ }
+ };
+
+
+ struct animcacheentry
+ {
+ animstate as[MAXANIMPARTS];
+ float pitch;
+ int millis;
+ uchar *partmask;
+ ragdolldata *ragdoll;
+
+ animcacheentry() : ragdoll(NULL)
+ {
+ loopk(MAXANIMPARTS) as[k].cur.fr1 = as[k].prev.fr1 = -1;
+ }
+
+ bool operator==(const animcacheentry &c) const
+ {
+ loopi(MAXANIMPARTS) if(as[i]!=c.as[i]) return false;
+ return pitch==c.pitch && partmask==c.partmask && ragdoll==c.ragdoll && (!ragdoll || min(millis, c.millis) >= ragdoll->lastmove);
+ }
+ };
+
+ struct vbocacheentry : animcacheentry
+ {
+ GLuint vbuf;
+ int owner;
+
+ vbocacheentry() : vbuf(0), owner(-1) {}
+ };
+
+ struct skelcacheentry : animcacheentry
+ {
+ dualquat *bdata;
+ int version;
+ bool dirty;
+
+ skelcacheentry() : bdata(NULL), version(-1), dirty(false) {}
+
+ void nextversion()
+ {
+ version = Shader::uniformlocversion();
+ dirty = true;
+ }
+ };
+
+ struct blendcacheentry : skelcacheentry
+ {
+ int owner;
+
+ blendcacheentry() : owner(-1) {}
+ };
+
+ struct skelmeshgroup;
+
+ struct skelmesh : mesh
+ {
+ vert *verts;
+ bumpvert *bumpverts;
+ tri *tris;
+ int numverts, numtris, maxweights;
+
+ int voffset, eoffset, elen;
+ ushort minvert, maxvert;
+
+ skelmesh() : verts(NULL), bumpverts(NULL), tris(NULL), numverts(0), numtris(0), maxweights(0)
+ {
+ }
+
+ virtual ~skelmesh()
+ {
+ DELETEA(verts);
+ DELETEA(bumpverts);
+ DELETEA(tris);
+ }
+
+ int addblendcombo(const blendcombo &c)
+ {
+ maxweights = max(maxweights, c.size());
+ return ((skelmeshgroup *)group)->addblendcombo(c);
+ }
+
+ void smoothnorms(float limit = 0, bool areaweight = true)
+ {
+ mesh::smoothnorms(verts, numverts, tris, numtris, limit, areaweight);
+ }
+
+ void buildnorms(bool areaweight = true)
+ {
+ mesh::buildnorms(verts, numverts, tris, numtris, areaweight);
+ }
+
+ void calctangents(bool areaweight = true)
+ {
+ if(bumpverts) return;
+ bumpverts = new bumpvert[numverts];
+ mesh::calctangents(bumpverts, verts, verts, numverts, tris, numtris, areaweight);
+ }
+
+ void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
+ {
+ loopj(numverts)
+ {
+ vec v = m.transform(verts[j].pos);
+ loopi(3)
+ {
+ bbmin[i] = min(bbmin[i], v[i]);
+ bbmax[i] = max(bbmax[i], v[i]);
+ }
+ }
+ }
+
+ void genBIH(BIH::mesh &m)
+ {
+ m.tris = (const BIH::tri *)tris;
+ m.numtris = numtris;
+ m.pos = (const uchar *)&verts->pos;
+ m.posstride = sizeof(vert);
+ m.tc = (const uchar *)&verts->tc;
+ m.tcstride = sizeof(vert);
+ }
+
+ static inline void assignvert(vvertn &vv, int j, vert &v, blendcombo &c)
+ {
+ vv.pos = v.pos;
+ vv.norm = v.norm;
+ vv.tc = v.tc;
+ }
+
+ inline void assignvert(vvertbump &vv, int j, vert &v, blendcombo &c)
+ {
+ vv.pos = v.pos;
+ vv.tc = v.tc;
+ vv.tangent = bumpverts[j].tangent;
+ }
+
+ static inline void assignvert(vvertnw &vv, int j, vert &v, blendcombo &c)
+ {
+ vv.pos = v.pos;
+ vv.norm = v.norm;
+ vv.tc = v.tc;
+ c.serialize(vv);
+ }
+
+ inline void assignvert(vvertbumpw &vv, int j, vert &v, blendcombo &c)
+ {
+ vv.pos = v.pos;
+ vv.tc = v.tc;
+ vv.tangent = bumpverts[j].tangent;
+ c.serialize(vv);
+ }
+
+ template<class T>
+ int genvbo(vector<ushort> &idxs, int offset, vector<T> &vverts)
+ {
+ voffset = offset;
+ eoffset = idxs.length();
+ loopi(numverts)
+ {
+ vert &v = verts[i];
+ assignvert(vverts.add(), i, v, ((skelmeshgroup *)group)->blendcombos[v.blend]);
+ }
+ loopi(numtris) loopj(3) idxs.add(voffset + tris[i].vert[j]);
+ elen = idxs.length()-eoffset;
+ minvert = voffset;
+ maxvert = voffset + numverts-1;
+ return numverts;
+ }
+
+ template<class T>
+ int genvbo(vector<ushort> &idxs, int offset, vector<T> &vverts, int *htdata, int htlen)
+ {
+ voffset = offset;
+ eoffset = idxs.length();
+ minvert = 0xFFFF;
+ loopi(numtris)
+ {
+ tri &t = tris[i];
+ loopj(3)
+ {
+ int index = t.vert[j];
+ vert &v = verts[index];
+ T vv;
+ assignvert(vv, index, v, ((skelmeshgroup *)group)->blendcombos[v.blend]);
+ int htidx = hthash(v.pos)&(htlen-1);
+ loopk(htlen)
+ {
+ int &vidx = htdata[(htidx+k)&(htlen-1)];
+ if(vidx < 0) { vidx = idxs.add(ushort(vverts.length())); vverts.add(vv); break; }
+ else if(!memcmp(&vverts[vidx], &vv, sizeof(vv))) { minvert = min(minvert, idxs.add(ushort(vidx))); break; }
+ }
+ }
+ }
+ elen = idxs.length()-eoffset;
+ minvert = min(minvert, ushort(voffset));
+ maxvert = max(minvert, ushort(vverts.length()-1));
+ return vverts.length()-voffset;
+ }
+
+ int genvbo(vector<ushort> &idxs, int offset)
+ {
+ loopi(numverts) verts[i].interpindex = ((skelmeshgroup *)group)->remapblend(verts[i].blend);
+
+ voffset = offset;
+ eoffset = idxs.length();
+ loopi(numtris)
+ {
+ tri &t = tris[i];
+ loopj(3) idxs.add(voffset+t.vert[j]);
+ }
+ minvert = voffset;
+ maxvert = voffset + numverts-1;
+ elen = idxs.length()-eoffset;
+ return numverts;
+ }
+
+ template<class T>
+ static inline void fillvert(T &vv, int j, vert &v)
+ {
+ vv.tc = v.tc;
+ }
+
+ template<class T>
+ void fillverts(T *vdata)
+ {
+ vdata += voffset;
+ loopi(numverts) fillvert(vdata[i], i, verts[i]);
+ }
+
+ void interpverts(const dualquat * RESTRICT bdata1, const dualquat * RESTRICT bdata2, bool tangents, void * RESTRICT vdata, skin &s)
+ {
+ const int blendoffset = ((skelmeshgroup *)group)->skel->numgpubones;
+ bdata2 -= blendoffset;
+
+ #define IPLOOP(type, dosetup, dotransform) \
+ loopi(numverts) \
+ { \
+ const vert &src = verts[i]; \
+ type &dst = ((type * RESTRICT)vdata)[i]; \
+ dosetup; \
+ const dualquat &b = (src.interpindex < blendoffset ? bdata1 : bdata2)[src.interpindex]; \
+ dst.pos = b.transform(src.pos); \
+ dotransform; \
+ }
+
+ if(tangents)
+ {
+ IPLOOP(vvertbump, bumpvert &bsrc = bumpverts[i],
+ {
+ quat q = b.transform(bsrc.tangent);
+ fixqtangent(q, bsrc.tangent.w);
+ dst.tangent = q;
+ });
+ }
+ else
+ {
+ IPLOOP(vvertn, ,
+ {
+ dst.norm = b.transformnormal(src.norm);
+ });
+ }
+
+ #undef IPLOOP
+ }
+
+ void setshader(Shader *s)
+ {
+ skelmeshgroup *g = (skelmeshgroup *)group;
+ if(glaring)
+ {
+ if(!g->skel->usegpuskel) s->setvariant(0, 1);
+ else s->setvariant(min(maxweights, g->vweights), 1);
+ }
+ else if(!g->skel->usegpuskel) s->set();
+ else s->setvariant(min(maxweights, g->vweights)-1, 0);
+ }
+
+ void render(const animstate *as, skin &s, vbocacheentry &vc)
+ {
+ if(!Shader::lastshader) return;
+ glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, elen, GL_UNSIGNED_SHORT, &((skelmeshgroup *)group)->edata[eoffset]);
+ glde++;
+ xtravertsva += numverts;
+ }
+ };
+
+
+ struct tag
+ {
+ char *name;
+ int bone;
+ matrix4x3 matrix;
+
+ tag() : name(NULL) {}
+ ~tag() { DELETEA(name); }
+ };
+
+ struct skelanimspec
+ {
+ char *name;
+ int frame, range;
+
+ skelanimspec() : name(NULL), frame(0), range(0) {}
+ ~skelanimspec()
+ {
+ DELETEA(name);
+ }
+ };
+
+ struct boneinfo
+ {
+ const char *name;
+ int parent, children, next, group, scheduled, interpindex, interpparent, ragdollindex, correctindex;
+ float pitchscale, pitchoffset, pitchmin, pitchmax;
+ dualquat base, invbase;
+
+ boneinfo() : name(NULL), parent(-1), children(-1), next(-1), group(INT_MAX), scheduled(-1), interpindex(-1), interpparent(-1), ragdollindex(-1), correctindex(-1), pitchscale(0), pitchoffset(0), pitchmin(0), pitchmax(0) {}
+ ~boneinfo()
+ {
+ DELETEA(name);
+ }
+ };
+
+ struct antipode
+ {
+ int parent, child;
+
+ antipode(int parent, int child) : parent(parent), child(child) {}
+ };
+
+ struct pitchdep
+ {
+ int bone, parent;
+ dualquat pose;
+ };
+
+ struct pitchtarget
+ {
+ int bone, frame, corrects, deps;
+ float pitchmin, pitchmax, deviated;
+ dualquat pose;
+ };
+
+ struct pitchcorrect
+ {
+ int bone, target, parent;
+ float pitchmin, pitchmax, pitchscale, pitchangle, pitchtotal;
+
+ pitchcorrect() : parent(-1), pitchangle(0), pitchtotal(0) {}
+ };
+
+ struct skeleton
+ {
+ char *name;
+ int shared;
+ vector<skelmeshgroup *> users;
+ boneinfo *bones;
+ int numbones, numinterpbones, numgpubones, numframes;
+ dualquat *framebones;
+ vector<skelanimspec> skelanims;
+ vector<tag> tags;
+ vector<antipode> antipodes;
+ ragdollskel *ragdoll;
+ vector<pitchdep> pitchdeps;
+ vector<pitchtarget> pitchtargets;
+ vector<pitchcorrect> pitchcorrects;
+
+ bool usegpuskel;
+ vector<skelcacheentry> skelcache;
+ hashtable<GLuint, int> blendoffsets;
+
+ skeleton() : name(NULL), shared(0), bones(NULL), numbones(0), numinterpbones(0), numgpubones(0), numframes(0), framebones(NULL), ragdoll(NULL), usegpuskel(false), blendoffsets(32)
+ {
+ }
+
+ ~skeleton()
+ {
+ DELETEA(name);
+ DELETEA(bones);
+ DELETEA(framebones);
+ DELETEP(ragdoll);
+ loopv(skelcache)
+ {
+ DELETEA(skelcache[i].bdata);
+ }
+ }
+
+ skelanimspec *findskelanim(const char *name, char sep = '\0')
+ {
+ int len = sep ? strlen(name) : 0;
+ loopv(skelanims)
+ {
+ if(skelanims[i].name)
+ {
+ if(sep)
+ {
+ const char *end = strchr(skelanims[i].name, ':');
+ if(end && end - skelanims[i].name == len && !memcmp(name, skelanims[i].name, len)) return &skelanims[i];
+ }
+ if(!strcmp(name, skelanims[i].name)) return &skelanims[i];
+ }
+ }
+ return NULL;
+ }
+
+ skelanimspec &addskelanim(const char *name)
+ {
+ skelanimspec &sa = skelanims.add();
+ sa.name = name ? newstring(name) : NULL;
+ return sa;
+ }
+
+ int findbone(const char *name)
+ {
+ loopi(numbones) if(bones[i].name && !strcmp(bones[i].name, name)) return i;
+ return -1;
+ }
+
+ int findtag(const char *name)
+ {
+ loopv(tags) if(!strcmp(tags[i].name, name)) return i;
+ return -1;
+ }
+
+ bool addtag(const char *name, int bone, const matrix4x3 &matrix)
+ {
+ int idx = findtag(name);
+ if(idx >= 0)
+ {
+ if(!testtags) return false;
+ tag &t = tags[idx];
+ t.bone = bone;
+ t.matrix = matrix;
+ }
+ else
+ {
+ tag &t = tags.add();
+ t.name = newstring(name);
+ t.bone = bone;
+ t.matrix = matrix;
+ }
+ return true;
+ }
+
+ void calcantipodes()
+ {
+ antipodes.shrink(0);
+ vector<int> schedule;
+ loopi(numbones)
+ {
+ if(bones[i].group >= numbones)
+ {
+ bones[i].scheduled = schedule.length();
+ schedule.add(i);
+ }
+ else bones[i].scheduled = -1;
+ }
+ loopv(schedule)
+ {
+ int bone = schedule[i];
+ const boneinfo &info = bones[bone];
+ loopj(numbones) if(abs(bones[j].group) == bone && bones[j].scheduled < 0)
+ {
+ antipodes.add(antipode(info.interpindex, bones[j].interpindex));
+ bones[j].scheduled = schedule.length();
+ schedule.add(j);
+ }
+ if(i + 1 == schedule.length())
+ {
+ int conflict = INT_MAX;
+ loopj(numbones) if(bones[j].group < numbones && bones[j].scheduled < 0) conflict = min(conflict, abs(bones[j].group));
+ if(conflict < numbones)
+ {
+ bones[conflict].scheduled = schedule.length();
+ schedule.add(conflict);
+ }
+ }
+ }
+ }
+
+ void remapbones()
+ {
+ loopi(numbones)
+ {
+ boneinfo &info = bones[i];
+ info.interpindex = -1;
+ info.ragdollindex = -1;
+ }
+ numgpubones = 0;
+ loopv(users)
+ {
+ skelmeshgroup *group = users[i];
+ loopvj(group->blendcombos)
+ {
+ blendcombo &c = group->blendcombos[j];
+ loopk(4)
+ {
+ if(!c.weights[k]) { c.interpbones[k] = k > 0 ? c.interpbones[k-1] : 0; continue; }
+ boneinfo &info = bones[c.bones[k]];
+ if(info.interpindex < 0) info.interpindex = numgpubones++;
+ c.interpbones[k] = info.interpindex;
+ if(info.group < 0) continue;
+ loopl(4)
+ {
+ if(!c.weights[l]) break;
+ if(l == k) continue;
+ int parent = c.bones[l];
+ if(info.parent == parent || (info.parent >= 0 && info.parent == bones[parent].parent)) { info.group = -info.parent; break; }
+ if(info.group <= parent) continue;
+ int child = c.bones[k];
+ while(parent > child) parent = bones[parent].parent;
+ if(parent != child) info.group = c.bones[l];
+ }
+ }
+ }
+ }
+ numinterpbones = numgpubones;
+ loopv(tags)
+ {
+ boneinfo &info = bones[tags[i].bone];
+ if(info.interpindex < 0) info.interpindex = numinterpbones++;
+ }
+ if(ragdoll)
+ {
+ loopv(ragdoll->joints)
+ {
+ boneinfo &info = bones[ragdoll->joints[i].bone];
+ if(info.interpindex < 0) info.interpindex = numinterpbones++;
+ info.ragdollindex = i;
+ }
+ }
+ loopi(numbones)
+ {
+ boneinfo &info = bones[i];
+ if(info.interpindex < 0) continue;
+ for(int parent = info.parent; parent >= 0 && bones[parent].interpindex < 0; parent = bones[parent].parent)
+ bones[parent].interpindex = numinterpbones++;
+ }
+ loopi(numbones)
+ {
+ boneinfo &info = bones[i];
+ if(info.interpindex < 0) continue;
+ info.interpparent = info.parent >= 0 ? bones[info.parent].interpindex : -1;
+ }
+ if(ragdoll)
+ {
+ loopi(numbones)
+ {
+ boneinfo &info = bones[i];
+ if(info.interpindex < 0 || info.ragdollindex >= 0) continue;
+ for(int parent = info.parent; parent >= 0; parent = bones[parent].parent)
+ {
+ if(bones[parent].ragdollindex >= 0) { ragdoll->addreljoint(i, bones[parent].ragdollindex); break; }
+ }
+ }
+ }
+ calcantipodes();
+ }
+
+
+ void addpitchdep(int bone, int frame)
+ {
+ for(; bone >= 0; bone = bones[bone].parent)
+ {
+ int pos = pitchdeps.length();
+ loopvj(pitchdeps) if(bone <= pitchdeps[j].bone)
+ {
+ if(bone == pitchdeps[j].bone) goto nextbone;
+ pos = j;
+ break;
+ }
+ {
+ pitchdep d;
+ d.bone = bone;
+ d.parent = -1;
+ d.pose = framebones[frame*numbones + bone];
+ pitchdeps.insert(pos, d);
+ }
+ nextbone:;
+ }
+ }
+
+ int findpitchdep(int bone)
+ {
+ loopv(pitchdeps) if(bone <= pitchdeps[i].bone) return bone == pitchdeps[i].bone ? i : -1;
+ return -1;
+ }
+
+ int findpitchcorrect(int bone)
+ {
+ loopv(pitchcorrects) if(bone <= pitchcorrects[i].bone) return bone == pitchcorrects[i].bone ? i : -1;
+ return -1;
+ }
+
+ void initpitchdeps()
+ {
+ pitchdeps.setsize(0);
+ if(pitchtargets.empty()) return;
+ loopv(pitchtargets)
+ {
+ pitchtarget &t = pitchtargets[i];
+ t.deps = -1;
+ addpitchdep(t.bone, t.frame);
+ }
+ loopv(pitchdeps)
+ {
+ pitchdep &d = pitchdeps[i];
+ int parent = bones[d.bone].parent;
+ if(parent >= 0)
+ {
+ int j = findpitchdep(parent);
+ if(j >= 0)
+ {
+ d.parent = j;
+ d.pose.mul(pitchdeps[j].pose, dualquat(d.pose));
+ }
+ }
+ }
+ loopv(pitchtargets)
+ {
+ pitchtarget &t = pitchtargets[i];
+ int j = findpitchdep(t.bone);
+ if(j >= 0)
+ {
+ t.deps = j;
+ t.pose = pitchdeps[j].pose;
+ }
+ t.corrects = -1;
+ for(int parent = t.bone; parent >= 0; parent = bones[parent].parent)
+ {
+ t.corrects = findpitchcorrect(parent);
+ if(t.corrects >= 0) break;
+ }
+ }
+ loopv(pitchcorrects)
+ {
+ pitchcorrect &c = pitchcorrects[i];
+ bones[c.bone].correctindex = i;
+ c.parent = -1;
+ for(int parent = c.bone;;)
+ {
+ parent = bones[parent].parent;
+ if(parent < 0) break;
+ c.parent = findpitchcorrect(parent);
+ if(c.parent >= 0) break;
+ }
+ }
+ }
+
+ void optimize()
+ {
+ cleanup();
+ if(ragdoll) ragdoll->setup();
+ remapbones();
+ initpitchdeps();
+ }
+
+ void expandbonemask(uchar *expansion, int bone, int val)
+ {
+ expansion[bone] = val;
+ bone = bones[bone].children;
+ while(bone>=0) { expandbonemask(expansion, bone, val); bone = bones[bone].next; }
+ }
+
+ void applybonemask(ushort *mask, uchar *partmask, int partindex)
+ {
+ if(!mask || *mask==BONEMASK_END) return;
+ uchar *expansion = new uchar[numbones];
+ memset(expansion, *mask&BONEMASK_NOT ? 1 : 0, numbones);
+ while(*mask!=BONEMASK_END)
+ {
+ expandbonemask(expansion, *mask&BONEMASK_BONE, *mask&BONEMASK_NOT ? 0 : 1);
+ mask++;
+ }
+ loopi(numbones) if(expansion[i]) partmask[i] = partindex;
+ delete[] expansion;
+ }
+
+ void linkchildren()
+ {
+ loopi(numbones)
+ {
+ boneinfo &b = bones[i];
+ b.children = -1;
+ if(b.parent<0) b.next = -1;
+ else
+ {
+ b.next = bones[b.parent].children;
+ bones[b.parent].children = i;
+ }
+ }
+ }
+
+ int availgpubones() const { return min(maxvsuniforms - reservevpparams - 10, maxskelanimdata) / 2; }
+ bool gpuaccelerate() const { return numframes && gpuskel && numgpubones<=availgpubones(); }
+
+ float calcdeviation(const vec &axis, const vec &forward, const dualquat &pose1, const dualquat &pose2)
+ {
+ vec forward1 = pose1.transformnormal(forward).project(axis).normalize(),
+ forward2 = pose2.transformnormal(forward).project(axis).normalize(),
+ daxis = vec().cross(forward1, forward2);
+ float dx = clamp(forward1.dot(forward2), -1.0f, 1.0f), dy = clamp(daxis.magnitude(), -1.0f, 1.0f);
+ if(daxis.dot(axis) < 0) dy = -dy;
+ return atan2f(dy, dx)/RAD;
+ }
+
+ void calcpitchcorrects(float pitch, const vec &axis, const vec &forward)
+ {
+ loopv(pitchtargets)
+ {
+ pitchtarget &t = pitchtargets[i];
+ t.deviated = calcdeviation(axis, forward, t.pose, pitchdeps[t.deps].pose);
+ }
+ loopv(pitchcorrects)
+ {
+ pitchcorrect &c = pitchcorrects[i];
+ c.pitchangle = c.pitchtotal = 0;
+ }
+ loopvj(pitchtargets)
+ {
+ pitchtarget &t = pitchtargets[j];
+ float tpitch = pitch - t.deviated;
+ for(int parent = t.corrects; parent >= 0; parent = pitchcorrects[parent].parent)
+ tpitch -= pitchcorrects[parent].pitchangle;
+ if(t.pitchmin || t.pitchmax) tpitch = clamp(tpitch, t.pitchmin, t.pitchmax);
+ loopv(pitchcorrects)
+ {
+ pitchcorrect &c = pitchcorrects[i];
+ if(c.target != j) continue;
+ float total = c.parent >= 0 ? pitchcorrects[c.parent].pitchtotal : 0,
+ avail = tpitch - total,
+ used = tpitch*c.pitchscale;
+ if(c.pitchmin || c.pitchmax)
+ {
+ if(used < 0) used = clamp(c.pitchmin, used, 0.0f);
+ else used = clamp(c.pitchmax, 0.0f, used);
+ }
+ if(used < 0) used = clamp(avail, used, 0.0f);
+ else used = clamp(avail, 0.0f, used);
+ c.pitchangle = used;
+ c.pitchtotal = used + total;
+ }
+ }
+ }
+
+ #define INTERPBONE(bone) \
+ const animstate &s = as[partmask[bone]]; \
+ const framedata &f = partframes[partmask[bone]]; \
+ dualquat d; \
+ (d = f.fr1[bone]).mul((1-s.cur.t)*s.interp); \
+ d.accumulate(f.fr2[bone], s.cur.t*s.interp); \
+ if(s.interp<1) \
+ { \
+ d.accumulate(f.pfr1[bone], (1-s.prev.t)*(1-s.interp)); \
+ d.accumulate(f.pfr2[bone], s.prev.t*(1-s.interp)); \
+ }
+
+ void interpbones(const animstate *as, float pitch, const vec &axis, const vec &forward, int numanimparts, const uchar *partmask, skelcacheentry &sc)
+ {
+ if(!sc.bdata) sc.bdata = new dualquat[numinterpbones];
+ sc.nextversion();
+ struct framedata
+ {
+ const dualquat *fr1, *fr2, *pfr1, *pfr2;
+ } partframes[MAXANIMPARTS];
+ loopi(numanimparts)
+ {
+ partframes[i].fr1 = &framebones[as[i].cur.fr1*numbones];
+ partframes[i].fr2 = &framebones[as[i].cur.fr2*numbones];
+ if(as[i].interp<1)
+ {
+ partframes[i].pfr1 = &framebones[as[i].prev.fr1*numbones];
+ partframes[i].pfr2 = &framebones[as[i].prev.fr2*numbones];
+ }
+ }
+ loopv(pitchdeps)
+ {
+ pitchdep &p = pitchdeps[i];
+ INTERPBONE(p.bone);
+ d.normalize();
+ if(p.parent >= 0) p.pose.mul(pitchdeps[p.parent].pose, d);
+ else p.pose = d;
+ }
+ calcpitchcorrects(pitch, axis, forward);
+ loopi(numbones) if(bones[i].interpindex>=0)
+ {
+ INTERPBONE(i);
+ const boneinfo &b = bones[i];
+ d.normalize();
+ if(b.interpparent<0) sc.bdata[b.interpindex] = d;
+ else sc.bdata[b.interpindex].mul(sc.bdata[b.interpparent], d);
+ float angle;
+ if(b.pitchscale) { angle = b.pitchscale*pitch + b.pitchoffset; if(b.pitchmin || b.pitchmax) angle = clamp(angle, b.pitchmin, b.pitchmax); }
+ else if(b.correctindex >= 0) angle = pitchcorrects[b.correctindex].pitchangle;
+ else continue;
+ if(as->cur.anim&ANIM_NOPITCH || (as->interp < 1 && as->prev.anim&ANIM_NOPITCH))
+ angle *= (as->cur.anim&ANIM_NOPITCH ? 0 : as->interp) + (as->interp < 1 && as->prev.anim&ANIM_NOPITCH ? 0 : 1-as->interp);
+ sc.bdata[b.interpindex].mulorient(quat(axis, angle*RAD), b.base);
+ }
+ loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]);
+ }
+
+ void initragdoll(ragdolldata &d, skelcacheentry &sc, part *p)
+ {
+ const dualquat *bdata = sc.bdata;
+ loopv(ragdoll->joints)
+ {
+ const ragdollskel::joint &j = ragdoll->joints[i];
+ const boneinfo &b = bones[j.bone];
+ const dualquat &q = bdata[b.interpindex];
+ loopk(3) if(j.vert[k] >= 0)
+ {
+ ragdollskel::vert &v = ragdoll->verts[j.vert[k]];
+ ragdolldata::vert &dv = d.verts[j.vert[k]];
+ dv.pos.add(q.transform(v.pos).mul(v.weight));
+ }
+ }
+ if(ragdoll->animjoints) loopv(ragdoll->joints)
+ {
+ const ragdollskel::joint &j = ragdoll->joints[i];
+ const boneinfo &b = bones[j.bone];
+ const dualquat &q = bdata[b.interpindex];
+ d.calcanimjoint(i, matrix4x3(q));
+ }
+ loopv(ragdoll->verts)
+ {
+ ragdolldata::vert &dv = d.verts[i];
+ matrixstack[matrixpos].transform(vec(dv.pos).add(p->translate).mul(p->model->scale), dv.pos);
+ }
+ loopv(ragdoll->reljoints)
+ {
+ const ragdollskel::reljoint &r = ragdoll->reljoints[i];
+ const ragdollskel::joint &j = ragdoll->joints[r.parent];
+ const boneinfo &br = bones[r.bone], &bj = bones[j.bone];
+ d.reljoints[i].mul(dualquat(bdata[bj.interpindex]).invert(), bdata[br.interpindex]);
+ }
+ }
+
+ void genragdollbones(ragdolldata &d, skelcacheentry &sc, part *p)
+ {
+ if(!sc.bdata) sc.bdata = new dualquat[numinterpbones];
+ sc.nextversion();
+ loopv(ragdoll->joints)
+ {
+ const ragdollskel::joint &j = ragdoll->joints[i];
+ const boneinfo &b = bones[j.bone];
+ vec pos(0, 0, 0);
+ loopk(3) if(j.vert[k]>=0) pos.add(d.verts[j.vert[k]].pos);
+ pos.mul(j.weight/p->model->scale).sub(p->translate);
+ matrix4x3 m;
+ m.mul(d.tris[j.tri], pos, d.animjoints ? d.animjoints[i] : j.orient);
+ sc.bdata[b.interpindex] = dualquat(m);
+ }
+ loopv(ragdoll->reljoints)
+ {
+ const ragdollskel::reljoint &r = ragdoll->reljoints[i];
+ const ragdollskel::joint &j = ragdoll->joints[r.parent];
+ const boneinfo &br = bones[r.bone], &bj = bones[j.bone];
+ sc.bdata[br.interpindex].mul(sc.bdata[bj.interpindex], d.reljoints[i]);
+ }
+ loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]);
+ }
+
+ void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n)
+ {
+ matrix4x3 t;
+ t.mul(bones[tags[i].bone].base, tags[i].matrix);
+ t.posttranslate(p->translate, p->model->scale);
+ n.mul(m, t);
+ }
+
+ void calctags(part *p, skelcacheentry *sc = NULL)
+ {
+ loopv(p->links)
+ {
+ linkedpart &l = p->links[i];
+ tag &t = tags[l.tag];
+ dualquat q;
+ if(sc) q.mul(sc->bdata[bones[t.bone].interpindex], bones[t.bone].base);
+ else q = bones[t.bone].base;
+ matrix4x3 m;
+ m.mul(q, t.matrix);
+ m.d.add(p->translate).mul(p->model->scale);
+ l.matrix = m;
+ }
+ }
+
+ void cleanup(bool full = true)
+ {
+ loopv(skelcache)
+ {
+ skelcacheentry &sc = skelcache[i];
+ loopj(MAXANIMPARTS) sc.as[j].cur.fr1 = -1;
+ DELETEA(sc.bdata);
+ }
+ skelcache.setsize(0);
+ blendoffsets.clear();
+ if(full) loopv(users) users[i]->cleanup();
+ }
+
+ bool canpreload() { return !numframes || gpuaccelerate(); }
+
+ void preload()
+ {
+ if(!numframes) return;
+ if(skelcache.empty())
+ {
+ usegpuskel = gpuaccelerate();
+ }
+ }
+
+ skelcacheentry &checkskelcache(part *p, const animstate *as, float pitch, const vec &axis, const vec &forward, ragdolldata *rdata)
+ {
+ if(skelcache.empty())
+ {
+ usegpuskel = gpuaccelerate();
+ }
+
+ int numanimparts = ((skelpart *)as->owner)->numanimparts;
+ uchar *partmask = ((skelpart *)as->owner)->partmask;
+ skelcacheentry *sc = NULL;
+ bool match = false;
+ loopv(skelcache)
+ {
+ skelcacheentry &c = skelcache[i];
+ loopj(numanimparts) if(c.as[j]!=as[j]) goto mismatch;
+ if(c.pitch != pitch || c.partmask != partmask || c.ragdoll != rdata || (rdata && c.millis < rdata->lastmove)) goto mismatch;
+ match = true;
+ sc = &c;
+ break;
+ mismatch:
+ if(c.millis < lastmillis) { sc = &c; break; }
+ }
+ if(!sc) sc = &skelcache.add();
+ if(!match)
+ {
+ loopi(numanimparts) sc->as[i] = as[i];
+ sc->pitch = pitch;
+ sc->partmask = partmask;
+ sc->ragdoll = rdata;
+ if(rdata) genragdollbones(*rdata, *sc, p);
+ else interpbones(as, pitch, axis, forward, numanimparts, partmask, *sc);
+ }
+ sc->millis = lastmillis;
+ return *sc;
+ }
+
+ int getblendoffset(UniformLoc &u)
+ {
+ int &offset = blendoffsets.access(Shader::lastshader->program, -1);
+ if(offset < 0)
+ {
+ defformatstring(offsetname, "%s[%d]", u.name, 2*numgpubones);
+ offset = glGetUniformLocation_(Shader::lastshader->program, offsetname);
+ }
+ return offset;
+ }
+
+ void setglslbones(UniformLoc &u, skelcacheentry &sc, skelcacheentry &bc, int count)
+ {
+ if(u.version == bc.version && u.data == bc.bdata) return;
+ glUniform4fv_(u.loc, 2*numgpubones, sc.bdata[0].real.v);
+ if(count > 0)
+ {
+ int offset = getblendoffset(u);
+ if(offset >= 0) glUniform4fv_(offset, 2*count, bc.bdata[0].real.v);
+ }
+ u.version = bc.version;
+ u.data = bc.bdata;
+ }
+
+ void setgpubones(skelcacheentry &sc, blendcacheentry *bc, int count)
+ {
+ if(!Shader::lastshader) return;
+ if(Shader::lastshader->uniformlocs.length() < 1) return;
+ UniformLoc &u = Shader::lastshader->uniformlocs[0];
+ setglslbones(u, sc, bc ? *bc : sc, count);
+ }
+
+ bool shouldcleanup() const
+ {
+ return numframes && (skelcache.empty() || gpuaccelerate()!=usegpuskel);
+ }
+ };
+
+ struct skelmeshgroup : meshgroup
+ {
+ skeleton *skel;
+
+ vector<blendcombo> blendcombos;
+ int numblends[4];
+
+ static const int MAXBLENDCACHE = 16;
+ blendcacheentry blendcache[MAXBLENDCACHE];
+
+ static const int MAXVBOCACHE = 16;
+ vbocacheentry vbocache[MAXVBOCACHE];
+
+ ushort *edata;
+ GLuint ebuf;
+ bool vtangents;
+ int vlen, vertsize, vblends, vweights;
+ uchar *vdata;
+
+ skelmeshgroup() : skel(NULL), edata(NULL), ebuf(0), vtangents(false), vlen(0), vertsize(0), vblends(0), vweights(0), vdata(NULL)
+ {
+ memset(numblends, 0, sizeof(numblends));
+ }
+
+ virtual ~skelmeshgroup()
+ {
+ if(skel)
+ {
+ if(skel->shared) skel->users.removeobj(this);
+ else DELETEP(skel);
+ }
+ if(ebuf) glDeleteBuffers_(1, &ebuf);
+ loopi(MAXBLENDCACHE)
+ {
+ DELETEA(blendcache[i].bdata);
+ }
+ loopi(MAXVBOCACHE)
+ {
+ if(vbocache[i].vbuf) glDeleteBuffers_(1, &vbocache[i].vbuf);
+ }
+ DELETEA(vdata);
+ }
+
+ void shareskeleton(char *name)
+ {
+ if(!name)
+ {
+ skel = new skeleton;
+ skel->users.add(this);
+ return;
+ }
+
+ static hashnameset<skeleton *> skeletons;
+ if(skeletons.access(name)) skel = skeletons[name];
+ else
+ {
+ skel = new skeleton;
+ skel->name = newstring(name);
+ skeletons.add(skel);
+ }
+ skel->users.add(this);
+ skel->shared++;
+ }
+
+ int findtag(const char *name)
+ {
+ return skel->findtag(name);
+ }
+
+ void *animkey() { return skel; }
+ int totalframes() const { return max(skel->numframes, 1); }
+
+ virtual skelanimspec *loadanim(const char *filename) { return NULL; }
+
+ void genvbo(bool tangents, vbocacheentry &vc)
+ {
+ if(!vc.vbuf) glGenBuffers_(1, &vc.vbuf);
+ if(ebuf) return;
+
+ vector<ushort> idxs;
+
+ if(tangents) loopv(meshes) ((skelmesh *)meshes[i])->calctangents();
+
+ vtangents = tangents;
+ vlen = 0;
+ vblends = 0;
+ if(skel->numframes && !skel->usegpuskel)
+ {
+ vweights = 1;
+ loopv(blendcombos)
+ {
+ blendcombo &c = blendcombos[i];
+ c.interpindex = c.weights[1] ? skel->numgpubones + vblends++ : -1;
+ }
+
+ vertsize = tangents ? sizeof(vvertbump) : sizeof(vvertn);
+ loopv(meshes) vlen += ((skelmesh *)meshes[i])->genvbo(idxs, vlen);
+ DELETEA(vdata);
+ vdata = new uchar[vlen*vertsize];
+ #define FILLVDATA(type) do { \
+ loopv(meshes) ((skelmesh *)meshes[i])->fillverts((type *)vdata); \
+ } while(0)
+ if(tangents) FILLVDATA(vvertbump);
+ else FILLVDATA(vvertn);
+ #undef FILLVDATA
+ }
+ else
+ {
+ if(skel->numframes)
+ {
+ vweights = 4;
+ int availbones = skel->availgpubones() - skel->numgpubones;
+ while(vweights > 1 && availbones >= numblends[vweights-1]) availbones -= numblends[--vweights];
+ loopv(blendcombos)
+ {
+ blendcombo &c = blendcombos[i];
+ c.interpindex = c.size() > vweights ? skel->numgpubones + vblends++ : -1;
+ }
+ }
+ else
+ {
+ vweights = 0;
+ loopv(blendcombos) blendcombos[i].interpindex = -1;
+ }
+
+ gle::bindvbo(vc.vbuf);
+ #define GENVBO(type, args) do { \
+ vertsize = sizeof(type); \
+ vector<type> vverts; \
+ loopv(meshes) vlen += ((skelmesh *)meshes[i])->genvbo args; \
+ glBufferData_(GL_ARRAY_BUFFER, vverts.length()*sizeof(type), vverts.getbuf(), GL_STATIC_DRAW); \
+ } while(0)
+ #define GENVBOANIM(type) GENVBO(type, (idxs, vlen, vverts))
+ #define GENVBOSTAT(type) GENVBO(type, (idxs, vlen, vverts, htdata, htlen))
+ if(skel->numframes)
+ {
+ if(tangents) GENVBOANIM(vvertbumpw);
+ else GENVBOANIM(vvertnw);
+ }
+ else
+ {
+ int numverts = 0, htlen = 128;
+ loopv(meshes) numverts += ((skelmesh *)meshes[i])->numverts;
+ while(htlen < numverts) htlen *= 2;
+ if(numverts*4 > htlen*3) htlen *= 2;
+ int *htdata = new int[htlen];
+ memset(htdata, -1, htlen*sizeof(int));
+ if(tangents) GENVBOSTAT(vvertbump);
+ else GENVBOSTAT(vvertn);
+ delete[] htdata;
+ }
+ #undef GENVBO
+ #undef GENVBOANIM
+ #undef GENVBOSTAT
+ gle::clearvbo();
+ }
+
+ glGenBuffers_(1, &ebuf);
+ gle::bindebo(ebuf);
+ glBufferData_(GL_ELEMENT_ARRAY_BUFFER, idxs.length()*sizeof(ushort), idxs.getbuf(), GL_STATIC_DRAW);
+ gle::clearebo();
+ }
+
+ void bindvbo(const animstate *as, vbocacheentry &vc, skelcacheentry *sc = NULL, blendcacheentry *bc = NULL)
+ {
+ vvert *vverts = 0;
+ bindpos(ebuf, vc.vbuf, &vverts->pos, vertsize);
+ if(as->cur.anim&ANIM_NOSKIN)
+ {
+ if(enabletc) disabletc();
+ if(enablenormals) disablenormals();
+ if(enabletangents) disabletangents();
+ }
+ else
+ {
+ if(vtangents)
+ {
+ if(enablenormals) disablenormals();
+ vvertbump *vvertbumps = 0;
+ bindtangents(&vvertbumps->tangent, vertsize);
+ }
+ else
+ {
+ if(enabletangents) disabletangents();
+ vvertn *vvertns = 0;
+ bindnormals(&vvertns->norm, vertsize);
+ }
+
+ bindtc(&vverts->tc, vertsize);
+ }
+ if(!sc || !skel->usegpuskel)
+ {
+ if(enablebones) disablebones();
+ }
+ else
+ {
+ if(vtangents)
+ {
+ vvertbumpw *vvertbumpws = 0;
+ bindbones(vvertbumpws->weights, vvertbumpws->bones, vertsize);
+ }
+ else
+ {
+ vvertnw *vvertnws = 0;
+ bindbones(vvertnws->weights, vvertnws->bones, vertsize);
+ }
+ }
+ }
+
+ void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n)
+ {
+ skel->concattagtransform(p, i, m, n);
+ }
+
+ int addblendcombo(const blendcombo &c)
+ {
+ loopv(blendcombos) if(blendcombos[i]==c)
+ {
+ blendcombos[i].uses += c.uses;
+ return i;
+ }
+ numblends[c.size()-1]++;
+ blendcombo &a = blendcombos.add(c);
+ return a.interpindex = blendcombos.length()-1;
+ }
+
+ void sortblendcombos()
+ {
+ blendcombos.sort(blendcombo::sortcmp);
+ int *remap = new int[blendcombos.length()];
+ loopv(blendcombos) remap[blendcombos[i].interpindex] = i;
+ loopv(meshes)
+ {
+ skelmesh *m = (skelmesh *)meshes[i];
+ loopj(m->numverts)
+ {
+ vert &v = m->verts[j];
+ v.blend = remap[v.blend];
+ }
+ }
+ delete[] remap;
+ }
+
+ int remapblend(int blend)
+ {
+ const blendcombo &c = blendcombos[blend];
+ return c.weights[1] ? c.interpindex : c.interpbones[0];
+ }
+
+ static inline void blendbones(dualquat &d, const dualquat *bdata, const blendcombo &c)
+ {
+ d = bdata[c.interpbones[0]];
+ d.mul(c.weights[0]);
+ d.accumulate(bdata[c.interpbones[1]], c.weights[1]);
+ if(c.weights[2])
+ {
+ d.accumulate(bdata[c.interpbones[2]], c.weights[2]);
+ if(c.weights[3]) d.accumulate(bdata[c.interpbones[3]], c.weights[3]);
+ }
+ }
+
+ void blendbones(const skelcacheentry &sc, blendcacheentry &bc)
+ {
+ bc.nextversion();
+ if(!bc.bdata) bc.bdata = new dualquat[vblends];
+ dualquat *dst = bc.bdata - skel->numgpubones;
+ bool normalize = !skel->usegpuskel || vweights<=1;
+ loopv(blendcombos)
+ {
+ const blendcombo &c = blendcombos[i];
+ if(c.interpindex<0) break;
+ dualquat &d = dst[c.interpindex];
+ blendbones(d, sc.bdata, c);
+ if(normalize) d.normalize();
+ }
+ }
+
+ void cleanup()
+ {
+ loopi(MAXBLENDCACHE)
+ {
+ blendcacheentry &c = blendcache[i];
+ DELETEA(c.bdata);
+ c.owner = -1;
+ }
+ loopi(MAXVBOCACHE)
+ {
+ vbocacheentry &c = vbocache[i];
+ if(c.vbuf) { glDeleteBuffers_(1, &c.vbuf); c.vbuf = 0; }
+ c.owner = -1;
+ }
+ if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
+ if(skel) skel->cleanup(false);
+ }
+
+ #define SEARCHCACHE(cachesize, cacheentry, cache, reusecheck) \
+ loopi(cachesize) \
+ { \
+ cacheentry &c = cache[i]; \
+ if(c.owner==owner) \
+ { \
+ if(c==sc) return c; \
+ else c.owner = -1; \
+ break; \
+ } \
+ } \
+ loopi(cachesize-1) \
+ { \
+ cacheentry &c = cache[i]; \
+ if(reusecheck c.owner < 0 || c.millis < lastmillis) \
+ return c; \
+ } \
+ return cache[cachesize-1];
+
+ vbocacheentry &checkvbocache(skelcacheentry &sc, int owner)
+ {
+ SEARCHCACHE(MAXVBOCACHE, vbocacheentry, vbocache, !c.vbuf || );
+ }
+
+ blendcacheentry &checkblendcache(skelcacheentry &sc, int owner)
+ {
+ SEARCHCACHE(MAXBLENDCACHE, blendcacheentry, blendcache, )
+ }
+
+ void preload(part *p)
+ {
+ if(!skel->canpreload()) return;
+ bool tangents = false;
+ loopv(p->skins) if(p->skins[i].tangents()) tangents = true;
+ if(skel->shouldcleanup()) skel->cleanup();
+ else if(tangents!=vtangents) cleanup();
+ skel->preload();
+ if(!vbocache->vbuf) genvbo(tangents, *vbocache);
+ }
+
+ void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p)
+ {
+ bool tangents = false;
+ loopv(p->skins) if(p->skins[i].tangents()) tangents = true;
+ if(skel->shouldcleanup()) { skel->cleanup(); disablevbo(); }
+ else if(tangents!=vtangents) { cleanup(); disablevbo(); }
+
+ if(!skel->numframes)
+ {
+ if(!(as->cur.anim&ANIM_NORENDER))
+ {
+ if(!vbocache->vbuf) genvbo(tangents, *vbocache);
+ bindvbo(as, *vbocache);
+ loopv(meshes)
+ {
+ skelmesh *m = (skelmesh *)meshes[i];
+ p->skins[i].bind(m, as);
+ m->render(as, p->skins[i], *vbocache);
+ }
+ }
+ skel->calctags(p);
+ return;
+ }
+
+ skelcacheentry &sc = skel->checkskelcache(p, as, pitch, axis, forward, as->cur.anim&ANIM_RAGDOLL || !d || !d->ragdoll || d->ragdoll->skel != skel->ragdoll ? NULL : d->ragdoll);
+ if(!(as->cur.anim&ANIM_NORENDER))
+ {
+ int owner = &sc-&skel->skelcache[0];
+ vbocacheentry &vc = skel->usegpuskel ? *vbocache : checkvbocache(sc, owner);
+ vc.millis = lastmillis;
+ if(!vc.vbuf) genvbo(tangents, vc);
+ blendcacheentry *bc = NULL;
+ if(vblends)
+ {
+ bc = &checkblendcache(sc, owner);
+ bc->millis = lastmillis;
+ if(bc->owner!=owner)
+ {
+ bc->owner = owner;
+ *(animcacheentry *)bc = sc;
+ blendbones(sc, *bc);
+ }
+ }
+ if(!skel->usegpuskel && vc.owner!=owner)
+ {
+ vc.owner = owner;
+ (animcacheentry &)vc = sc;
+ loopv(meshes)
+ {
+ skelmesh &m = *(skelmesh *)meshes[i];
+ m.interpverts(sc.bdata, bc ? bc->bdata : NULL, tangents, vdata + m.voffset*vertsize, p->skins[i]);
+ }
+ gle::bindvbo(vc.vbuf);
+ glBufferData_(GL_ARRAY_BUFFER, vlen*vertsize, vdata, GL_STREAM_DRAW);
+ }
+
+ bindvbo(as, vc, &sc, bc);
+ loopv(meshes)
+ {
+ skelmesh *m = (skelmesh *)meshes[i];
+ p->skins[i].bind(m, as);
+ if(skel->usegpuskel) skel->setgpubones(sc, bc, vblends);
+ m->render(as, p->skins[i], vc);
+ }
+ }
+
+ skel->calctags(p, &sc);
+
+ if(as->cur.anim&ANIM_RAGDOLL && skel->ragdoll && !d->ragdoll)
+ {
+ d->ragdoll = new ragdolldata(skel->ragdoll, p->model->scale);
+ skel->initragdoll(*d->ragdoll, sc, p);
+ d->ragdoll->init(d);
+ }
+ }
+ };
+
+ struct animpartmask
+ {
+ animpartmask *next;
+ int numbones;
+ uchar bones[1];
+ };
+
+ struct skelpart : part
+ {
+ animpartmask *buildingpartmask;
+
+ uchar *partmask;
+
+ skelpart(animmodel *model, int index = 0) : part(model, index), buildingpartmask(NULL), partmask(NULL)
+ {
+ }
+
+ virtual ~skelpart()
+ {
+ DELETEA(buildingpartmask);
+ }
+
+ uchar *sharepartmask(animpartmask *o)
+ {
+ static animpartmask *partmasks = NULL;
+ animpartmask *p = partmasks;
+ for(; p; p = p->next) if(p->numbones==o->numbones && !memcmp(p->bones, o->bones, p->numbones))
+ {
+ delete[] (uchar *)o;
+ return p->bones;
+ }
+
+ o->next = p;
+ partmasks = o;
+ return o->bones;
+ }
+
+ animpartmask *newpartmask()
+ {
+ animpartmask *p = (animpartmask *)new uchar[sizeof(animpartmask) + ((skelmeshgroup *)meshes)->skel->numbones-1];
+ p->numbones = ((skelmeshgroup *)meshes)->skel->numbones;
+ memset(p->bones, 0, p->numbones);
+ return p;
+ }
+
+ void initanimparts()
+ {
+ DELETEA(buildingpartmask);
+ buildingpartmask = newpartmask();
+ }
+
+ bool addanimpart(ushort *bonemask)
+ {
+ if(!buildingpartmask || numanimparts>=MAXANIMPARTS) return false;
+ ((skelmeshgroup *)meshes)->skel->applybonemask(bonemask, buildingpartmask->bones, numanimparts);
+ numanimparts++;
+ return true;
+ }
+
+ void endanimparts()
+ {
+ if(buildingpartmask)
+ {
+ partmask = sharepartmask(buildingpartmask);
+ buildingpartmask = NULL;
+ }
+
+ ((skelmeshgroup *)meshes)->skel->optimize();
+ }
+
+ void loaded()
+ {
+ endanimparts();
+ part::loaded();
+ }
+ };
+
+ skelmodel(const char *name) : animmodel(name)
+ {
+ }
+
+ int linktype(animmodel *m) const
+ {
+ return type()==m->type() &&
+ ((skelmeshgroup *)parts[0]->meshes)->skel == ((skelmeshgroup *)m->parts[0]->meshes)->skel ?
+ LINK_REUSE :
+ LINK_TAG;
+ }
+
+ bool skeletal() const { return true; }
+
+ skelpart &addpart()
+ {
+ flushpart();
+ skelpart *p = new skelpart(this, parts.length());
+ parts.add(p);
+ return *p;
+ }
+};
+
+struct skeladjustment
+{
+ float yaw, pitch, roll;
+ vec translate;
+
+ skeladjustment(float yaw, float pitch, float roll, const vec &translate) : yaw(yaw), pitch(pitch), roll(roll), translate(translate) {}
+
+ void adjust(dualquat &dq)
+ {
+ if(yaw) dq.mulorient(quat(vec(0, 0, 1), yaw*RAD));
+ if(pitch) dq.mulorient(quat(vec(0, -1, 0), pitch*RAD));
+ if(roll) dq.mulorient(quat(vec(-1, 0, 0), roll*RAD));
+ if(!translate.iszero()) dq.translate(translate);
+ }
+};
+
+template<class MDL> struct skelloader : modelloader<MDL, skelmodel>
+{
+ static vector<skeladjustment> adjustments;
+
+ skelloader(const char *name) : modelloader<MDL, skelmodel>(name) {}
+
+ void flushpart()
+ {
+ adjustments.setsize(0);
+ }
+};
+
+template<class MDL> vector<skeladjustment> skelloader<MDL>::adjustments;
+
+template<class MDL> struct skelcommands : modelcommands<MDL, struct MDL::skelmesh>
+{
+ typedef modelcommands<MDL, struct MDL::skelmesh> commands;
+ typedef struct MDL::skeleton skeleton;
+ typedef struct MDL::skelmeshgroup meshgroup;
+ typedef struct MDL::skelpart part;
+ typedef struct MDL::skin skin;
+ typedef struct MDL::boneinfo boneinfo;
+ typedef struct MDL::skelanimspec animspec;
+ typedef struct MDL::pitchdep pitchdep;
+ typedef struct MDL::pitchtarget pitchtarget;
+ typedef struct MDL::pitchcorrect pitchcorrect;
+
+ static void loadpart(char *meshfile, char *skelname, float *smooth)
+ {
+ if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ defformatstring(filename, "%s/%s", MDL::dir, meshfile);
+ part &mdl = MDL::loading->addpart();
+ mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0;
+ mdl.meshes = MDL::loading->sharemeshes(path(filename), skelname[0] ? skelname : NULL, double(*smooth > 0 ? cos(clamp(*smooth, 0.0f, 180.0f)*RAD) : 2));
+ if(!mdl.meshes) conoutf(CON_ERROR, "could not load %s", filename);
+ else
+ {
+ mdl.initanimparts();
+ mdl.initskins();
+ }
+ }
+
+ static void settag(char *name, char *tagname, float *tx, float *ty, float *tz, float *rx, float *ry, float *rz)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ part &mdl = *(part *)MDL::loading->parts.last();
+ int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
+ if(i >= 0)
+ {
+ float cx = *rx ? cosf(*rx/2*RAD) : 1, sx = *rx ? sinf(*rx/2*RAD) : 0,
+ cy = *ry ? cosf(*ry/2*RAD) : 1, sy = *ry ? sinf(*ry/2*RAD) : 0,
+ cz = *rz ? cosf(*rz/2*RAD) : 1, sz = *rz ? sinf(*rz/2*RAD) : 0;
+ matrix4x3 m(matrix3(quat(sx*cy*cz - cx*sy*sz, cx*sy*cz + sx*cy*sz, cx*cy*sz - sx*sy*cz, cx*cy*cz + sx*sy*sz)),
+ vec(*tx, *ty, *tz));
+ ((meshgroup *)mdl.meshes)->skel->addtag(tagname, i, m);
+ return;
+ }
+ conoutf(CON_ERROR, "could not find bone %s for tag %s", name, tagname);
+ }
+
+ static void setpitch(char *name, float *pitchscale, float *pitchoffset, float *pitchmin, float *pitchmax)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ part &mdl = *(part *)MDL::loading->parts.last();
+
+ if(name[0])
+ {
+ int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
+ if(i>=0)
+ {
+ boneinfo &b = ((meshgroup *)mdl.meshes)->skel->bones[i];
+ b.pitchscale = *pitchscale;
+ b.pitchoffset = *pitchoffset;
+ if(*pitchmin || *pitchmax)
+ {
+ b.pitchmin = *pitchmin;
+ b.pitchmax = *pitchmax;
+ }
+ else
+ {
+ b.pitchmin = -360*fabs(b.pitchscale) + b.pitchoffset;
+ b.pitchmax = 360*fabs(b.pitchscale) + b.pitchoffset;
+ }
+ return;
+ }
+ conoutf(CON_ERROR, "could not find bone %s to pitch", name);
+ return;
+ }
+
+ mdl.pitchscale = *pitchscale;
+ mdl.pitchoffset = *pitchoffset;
+ if(*pitchmin || *pitchmax)
+ {
+ mdl.pitchmin = *pitchmin;
+ mdl.pitchmax = *pitchmax;
+ }
+ else
+ {
+ mdl.pitchmin = -360*fabs(mdl.pitchscale) + mdl.pitchoffset;
+ mdl.pitchmax = 360*fabs(mdl.pitchscale) + mdl.pitchoffset;
+ }
+ }
+
+ static void setpitchtarget(char *name, char *animfile, int *frameoffset, float *pitchmin, float *pitchmax)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ part &mdl = *(part *)MDL::loading->parts.last();
+ if(!mdl.meshes) return;
+ defformatstring(filename, "%s/%s", MDL::dir, animfile);
+ animspec *sa = ((meshgroup *)mdl.meshes)->loadanim(path(filename));
+ if(!sa) { conoutf(CON_ERROR, "could not load %s anim file %s", MDL::formatname(), filename); return; }
+ skeleton *skel = ((meshgroup *)mdl.meshes)->skel;
+ int bone = skel ? skel->findbone(name) : -1;
+ if(bone < 0)
+ {
+ conoutf(CON_ERROR, "could not find bone %s to pitch target", name);
+ return;
+ }
+ loopv(skel->pitchtargets) if(skel->pitchtargets[i].bone == bone) return;
+ pitchtarget &t = skel->pitchtargets.add();
+ t.bone = bone;
+ t.frame = sa->frame + clamp(*frameoffset, 0, sa->range-1);
+ t.pitchmin = *pitchmin;
+ t.pitchmax = *pitchmax;
+ }
+
+ static void setpitchcorrect(char *name, char *targetname, float *scale, float *pitchmin, float *pitchmax)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ part &mdl = *(part *)MDL::loading->parts.last();
+ if(!mdl.meshes) return;
+ skeleton *skel = ((meshgroup *)mdl.meshes)->skel;
+ int bone = skel ? skel->findbone(name) : -1;
+ if(bone < 0)
+ {
+ conoutf(CON_ERROR, "could not find bone %s to pitch correct", name);
+ return;
+ }
+ if(skel->findpitchcorrect(bone) >= 0) return;
+ int targetbone = skel->findbone(targetname), target = -1;
+ if(targetbone >= 0) loopv(skel->pitchtargets) if(skel->pitchtargets[i].bone == targetbone) { target = i; break; }
+ if(target < 0)
+ {
+ conoutf(CON_ERROR, "could not find pitch target %s to pitch correct %s", targetname, name);
+ return;
+ }
+ pitchcorrect c;
+ c.bone = bone;
+ c.target = target;
+ c.pitchmin = *pitchmin;
+ c.pitchmax = *pitchmax;
+ c.pitchscale = *scale;
+ int pos = skel->pitchcorrects.length();
+ loopv(skel->pitchcorrects) if(bone <= skel->pitchcorrects[i].bone) { pos = i; break; }
+ skel->pitchcorrects.insert(pos, c);
+ }
+
+ static void setanim(char *anim, char *animfile, float *speed, int *priority, int *startoffset, int *endoffset)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+
+ vector<int> anims;
+ findanims(anim, anims);
+ if(anims.empty()) conoutf(CON_ERROR, "could not find animation %s", anim);
+ else
+ {
+ part *p = (part *)MDL::loading->parts.last();
+ if(!p->meshes) return;
+ defformatstring(filename, "%s/%s", MDL::dir, animfile);
+ animspec *sa = ((meshgroup *)p->meshes)->loadanim(path(filename));
+ if(!sa) conoutf(CON_ERROR, "could not load %s anim file %s", MDL::formatname(), filename);
+ else loopv(anims)
+ {
+ int start = sa->frame, end = sa->range;
+ if(*startoffset > 0) start += min(*startoffset, end-1);
+ else if(*startoffset < 0) start += max(end + *startoffset, 0);
+ end -= start - sa->frame;
+ if(*endoffset > 0) end = min(end, *endoffset);
+ else if(*endoffset < 0) end = max(end + *endoffset, 1);
+ MDL::loading->parts.last()->setanim(p->numanimparts-1, anims[i], start, end, *speed, *priority);
+ }
+ }
+ }
+
+ static void setanimpart(char *maskstr)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+
+ part *p = (part *)MDL::loading->parts.last();
+
+ vector<char *> bonestrs;
+ explodelist(maskstr, bonestrs);
+ vector<ushort> bonemask;
+ loopv(bonestrs)
+ {
+ char *bonestr = bonestrs[i];
+ int bone = p->meshes ? ((meshgroup *)p->meshes)->skel->findbone(bonestr[0]=='!' ? bonestr+1 : bonestr) : -1;
+ if(bone<0) { conoutf(CON_ERROR, "could not find bone %s for anim part mask [%s]", bonestr, maskstr); bonestrs.deletearrays(); return; }
+ bonemask.add(bone | (bonestr[0]=='!' ? BONEMASK_NOT : 0));
+ }
+ bonestrs.deletearrays();
+ bonemask.sort();
+ if(bonemask.length()) bonemask.add(BONEMASK_END);
+
+ if(!p->addanimpart(bonemask.getbuf())) conoutf(CON_ERROR, "too many animation parts");
+ }
+
+ static void setadjust(char *name, float *yaw, float *pitch, float *roll, float *tx, float *ty, float *tz)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ part &mdl = *(part *)MDL::loading->parts.last();
+
+ if(!name[0]) return;
+ int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
+ if(i < 0) { conoutf(CON_ERROR, "could not find bone %s to adjust", name); return; }
+ while(!MDL::adjustments.inrange(i)) MDL::adjustments.add(skeladjustment(0, 0, 0, vec(0, 0, 0)));
+ MDL::adjustments[i] = skeladjustment(*yaw, *pitch, *roll, vec(*tx/4, *ty/4, *tz/4));
+ }
+
+ skelcommands()
+ {
+ if(MDL::multiparted()) this->modelcommand(loadpart, "load", "ssf");
+ this->modelcommand(settag, "tag", "ssffffff");
+ this->modelcommand(setpitch, "pitch", "sffff");
+ this->modelcommand(setpitchtarget, "pitchtarget", "ssiff");
+ this->modelcommand(setpitchcorrect, "pitchcorrect", "ssfff");
+ if(MDL::animated())
+ {
+ this->modelcommand(setanim, "anim", "ssfiii");
+ this->modelcommand(setanimpart, "animpart", "s");
+ this->modelcommand(setadjust, "adjust", "sffffff");
+ }
+ }
+};
+
diff --git a/src/engine/smd.h b/src/engine/smd.h
new file mode 100644
index 0000000..771ec9a
--- /dev/null
+++ b/src/engine/smd.h
@@ -0,0 +1,447 @@
+struct smd;
+
+struct smdbone
+{
+ string name;
+ int parent;
+ smdbone() : parent(-1) { name[0] = '\0'; }
+};
+
+struct smd : skelloader<smd>
+{
+ smd(const char *name) : skelloader(name) {}
+
+ static const char *formatname() { return "smd"; }
+ int type() const { return MDL_SMD; }
+
+ struct smdmesh : skelmesh
+ {
+ };
+
+ struct smdmeshgroup : skelmeshgroup
+ {
+ smdmeshgroup()
+ {
+ }
+
+ bool skipcomment(char *&curbuf)
+ {
+ while(*curbuf && isspace(*curbuf)) curbuf++;
+ switch(*curbuf)
+ {
+ case '#':
+ case ';':
+ case '\r':
+ case '\n':
+ case '\0':
+ return true;
+ case '/':
+ if(curbuf[1] == '/') return true;
+ break;
+ }
+ return false;
+ }
+
+ void skipsection(stream *f, char *buf, size_t bufsize)
+ {
+ while(f->getline(buf, bufsize))
+ {
+ char *curbuf = buf;
+ if(skipcomment(curbuf)) continue;
+ if(!strncmp(curbuf, "end", 3)) break;
+ }
+ }
+
+ void readname(char *&curbuf, char *name, size_t namesize)
+ {
+ char *curname = name;
+ while(*curbuf && isspace(*curbuf)) curbuf++;
+ bool allowspace = false;
+ if(*curbuf == '"') { curbuf++; allowspace = true; }
+ while(*curbuf)
+ {
+ char c = *curbuf++;
+ if(c == '"') break;
+ if(isspace(c) && !allowspace) break;
+ if(curname < &name[namesize-1]) *curname++ = c;
+ }
+ *curname = '\0';
+ }
+
+ void readnodes(stream *f, char *buf, size_t bufsize, vector<smdbone> &bones)
+ {
+ while(f->getline(buf, bufsize))
+ {
+ char *curbuf = buf;
+ if(skipcomment(curbuf)) continue;
+ if(!strncmp(curbuf, "end", 3)) break;
+ int id = strtol(curbuf, &curbuf, 10);
+ string name;
+ readname(curbuf, name, sizeof(name));
+ int parent = strtol(curbuf, &curbuf, 10);
+ if(id < 0 || id > 255 || parent > 255 || !name[0]) continue;
+ while(!bones.inrange(id)) bones.add();
+ smdbone &bone = bones[id];
+ copystring(bone.name, name);
+ bone.parent = parent;
+ }
+ }
+
+ void readmaterial(char *&curbuf, char *name, size_t namesize)
+ {
+ char *curname = name;
+ while(*curbuf && isspace(*curbuf)) curbuf++;
+ while(*curbuf)
+ {
+ char c = *curbuf++;
+ if(isspace(c)) break;
+ if(c == '.')
+ {
+ while(*curbuf && !isspace(*curbuf)) curbuf++;
+ break;
+ }
+ if(curname < &name[namesize-1]) *curname++ = c;
+ }
+ *curname = '\0';
+ }
+
+ struct smdmeshdata
+ {
+ smdmesh *mesh;
+ vector<vert> verts;
+ vector<tri> tris;
+
+ void finalize()
+ {
+ if(verts.empty() || tris.empty()) return;
+ vert *mverts = new vert[mesh->numverts + verts.length()];
+ if(mesh->numverts)
+ {
+ memcpy(mverts, mesh->verts, mesh->numverts*sizeof(vert));
+ delete[] mesh->verts;
+ }
+ memcpy(&mverts[mesh->numverts], verts.getbuf(), verts.length()*sizeof(vert));
+ mesh->numverts += verts.length();
+ mesh->verts = mverts;
+ tri *mtris = new tri[mesh->numtris + tris.length()];
+ if(mesh->numtris)
+ {
+ memcpy(mtris, mesh->tris, mesh->numtris*sizeof(tri));
+ delete[] mesh->tris;
+ }
+ memcpy(&mtris[mesh->numtris], tris.getbuf(), tris.length()*sizeof(tri));
+ mesh->numtris += tris.length();
+ mesh->tris = mtris;
+ }
+ };
+
+ struct smdvertkey : vert
+ {
+ smdmeshdata *mesh;
+
+ smdvertkey(smdmeshdata *mesh) : mesh(mesh) {}
+ };
+
+ void readtriangles(stream *f, char *buf, size_t bufsize)
+ {
+ smdmeshdata *curmesh = NULL;
+ hashtable<const char *, smdmeshdata> materials(1<<6);
+ hashset<int> verts(1<<12);
+ while(f->getline(buf, bufsize))
+ {
+ char *curbuf = buf;
+ if(skipcomment(curbuf)) continue;
+ if(!strncmp(curbuf, "end", 3)) break;
+ string material;
+ readmaterial(curbuf, material, sizeof(material));
+ if(!curmesh || strcmp(curmesh->mesh->name, material))
+ {
+ curmesh = materials.access(material);
+ if(!curmesh)
+ {
+ smdmesh *m = new smdmesh;
+ m->group = this;
+ m->name = newstring(material);
+ meshes.add(m);
+ curmesh = &materials[m->name];
+ curmesh->mesh = m;
+ }
+ }
+ tri curtri;
+ loopi(3)
+ {
+ char *curbuf;
+ do
+ {
+ if(!f->getline(buf, bufsize)) goto endsection;
+ curbuf = buf;
+ } while(skipcomment(curbuf));
+ smdvertkey key(curmesh);
+ int parent = -1, numlinks = 0, len = 0;
+ if(sscanf(curbuf, " %d %f %f %f %f %f %f %f %f %d%n", &parent, &key.pos.x, &key.pos.y, &key.pos.z, &key.norm.x, &key.norm.y, &key.norm.z, &key.tc.x, &key.tc.y, &numlinks, &len) < 9) goto endsection;
+ curbuf += len;
+ key.pos.y = -key.pos.y;
+ key.norm.y = -key.norm.y;
+ key.tc.y = 1 - key.tc.y;
+ blendcombo c;
+ int sorted = 0;
+ float pweight = 0, tweight = 0;
+ for(; numlinks > 0; numlinks--)
+ {
+ int bone = -1, len = 0;
+ float weight = 0;
+ if(sscanf(curbuf, " %d %f%n", &bone, &weight, &len) < 2) break;
+ curbuf += len;
+ tweight += weight;
+ if(bone == parent) pweight += weight;
+ else sorted = c.addweight(sorted, weight, bone);
+ }
+ if(tweight < 1) pweight += 1 - tweight;
+ if(pweight > 0) sorted = c.addweight(sorted, pweight, parent);
+ c.finalize(sorted);
+ key.blend = curmesh->mesh->addblendcombo(c);
+ int index = verts.access(key, curmesh->verts.length());
+ if(index == curmesh->verts.length()) curmesh->verts.add(key);
+ curtri.vert[2-i] = index;
+ }
+ curmesh->tris.add(curtri);
+ }
+ endsection:
+ enumerate(materials, smdmeshdata, data, data.finalize());
+ }
+
+ void readskeleton(stream *f, char *buf, size_t bufsize)
+ {
+ int frame = -1;
+ while(f->getline(buf, bufsize))
+ {
+ char *curbuf = buf;
+ if(skipcomment(curbuf)) continue;
+ if(sscanf(curbuf, " time %d", &frame) == 1) continue;
+ else if(!strncmp(curbuf, "end", 3)) break;
+ else if(frame != 0) continue;
+ int bone;
+ vec pos, rot;
+ if(sscanf(curbuf, " %d %f %f %f %f %f %f", &bone, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z) != 7)
+ continue;
+ if(bone < 0 || bone >= skel->numbones)
+ continue;
+ rot.x = -rot.x;
+ rot.z = -rot.z;
+ float cx = cosf(rot.x/2), sx = sinf(rot.x/2),
+ cy = cosf(rot.y/2), sy = sinf(rot.y/2),
+ cz = cosf(rot.z/2), sz = sinf(rot.z/2);
+ pos.y = -pos.y;
+ dualquat dq(quat(sx*cy*cz - cx*sy*sz,
+ cx*sy*cz + sx*cy*sz,
+ cx*cy*sz - sx*sy*cz,
+ cx*cy*cz + sx*sy*sz),
+ pos);
+ boneinfo &b = skel->bones[bone];
+ if(b.parent < 0) b.base = dq;
+ else b.base.mul(skel->bones[b.parent].base, dq);
+ (b.invbase = b.base).invert();
+ }
+ }
+
+ bool loadmesh(const char *filename)
+ {
+ stream *f = openfile(filename, "r");
+ if(!f) return false;
+
+ char buf[512];
+ int version = -1;
+ while(f->getline(buf, sizeof(buf)))
+ {
+ char *curbuf = buf;
+ if(skipcomment(curbuf)) continue;
+ if(sscanf(curbuf, " version %d", &version) == 1)
+ {
+ if(version != 1) { delete f; return false; }
+ }
+ else if(!strncmp(curbuf, "nodes", 5))
+ {
+ if(skel->numbones > 0) { skipsection(f, buf, sizeof(buf)); continue; }
+ vector<smdbone> bones;
+ readnodes(f, buf, sizeof(buf), bones);
+ if(bones.empty()) continue;
+ skel->numbones = bones.length();
+ skel->bones = new boneinfo[skel->numbones];
+ loopv(bones)
+ {
+ boneinfo &dst = skel->bones[i];
+ smdbone &src = bones[i];
+ dst.name = newstring(src.name);
+ dst.parent = src.parent;
+ }
+ skel->linkchildren();
+ }
+ else if(!strncmp(curbuf, "triangles", 9))
+ readtriangles(f, buf, sizeof(buf));
+ else if(!strncmp(curbuf, "skeleton", 8))
+ {
+ if(skel->shared > 1) skipsection(f, buf, sizeof(buf));
+ else readskeleton(f, buf, sizeof(buf));
+ }
+ else if(!strncmp(curbuf, "vertexanimation", 15))
+ skipsection(f, buf, sizeof(buf));
+ }
+
+ sortblendcombos();
+
+ delete f;
+ return true;
+ }
+
+ int readframes(stream *f, char *buf, size_t bufsize, vector<dualquat> &animbones)
+ {
+ int frame = -1, numframes = 0, lastbone = skel->numbones;
+ while(f->getline(buf, bufsize))
+ {
+ char *curbuf = buf;
+ if(skipcomment(curbuf)) continue;
+ int nextframe = -1;
+ if(sscanf(curbuf, " time %d", &nextframe) == 1)
+ {
+ for(; lastbone < skel->numbones; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone];
+ if(nextframe >= numframes)
+ {
+ databuf<dualquat> framebones = animbones.reserve(skel->numbones * (nextframe + 1 - numframes));
+ loopi(nextframe - numframes) framebones.put(animbones.getbuf(), skel->numbones);
+ animbones.addbuf(framebones);
+ animbones.advance(skel->numbones);
+ numframes = nextframe + 1;
+ }
+ frame = nextframe;
+ lastbone = 0;
+ continue;
+ }
+ else if(!strncmp(curbuf, "end", 3)) break;
+ int bone;
+ vec pos, rot;
+ if(sscanf(curbuf, " %d %f %f %f %f %f %f", &bone, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z) != 7)
+ continue;
+ if(bone < 0 || bone >= skel->numbones)
+ continue;
+ for(; lastbone < bone; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone];
+ lastbone++;
+ float cx = cosf(rot.x/2), sx = sinf(rot.x/2),
+ cy = cosf(rot.y/2), sy = sinf(rot.y/2),
+ cz = cosf(rot.z/2), sz = sinf(rot.z/2);
+ pos.y = -pos.y;
+ dualquat dq(quat(-(sx*cy*cz - cx*sy*sz),
+ cx*sy*cz + sx*cy*sz,
+ -(cx*cy*sz - sx*sy*cz),
+ cx*cy*cz + sx*sy*sz),
+ pos);
+ if(adjustments.inrange(bone)) adjustments[bone].adjust(dq);
+ dq.mul(skel->bones[bone].invbase);
+ dualquat &dst = animbones[frame*skel->numbones + bone];
+ if(skel->bones[bone].parent < 0) dst = dq;
+ else dst.mul(skel->bones[skel->bones[bone].parent].base, dq);
+ dst.fixantipodal(skel->numframes > 0 ? skel->framebones[bone] : animbones[bone]);
+ }
+ for(; lastbone < skel->numbones; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone];
+ return numframes;
+ }
+
+ skelanimspec *loadanim(const char *filename)
+ {
+ skelanimspec *sa = skel->findskelanim(filename);
+ if(sa || skel->numbones <= 0) return sa;
+
+ stream *f = openfile(filename, "r");
+ if(!f) return NULL;
+
+ char buf[512];
+ int version = -1;
+ vector<dualquat> animbones;
+ while(f->getline(buf, sizeof(buf)))
+ {
+ char *curbuf = buf;
+ if(skipcomment(curbuf)) continue;
+ if(sscanf(curbuf, " version %d", &version) == 1)
+ {
+ if(version != 1) { delete f; return NULL; }
+ }
+ else if(!strncmp(curbuf, "nodes", 5))
+ {
+ vector<smdbone> bones;
+ readnodes(f, buf, sizeof(buf), bones);
+ if(bones.length() != skel->numbones) { delete f; return NULL; }
+ }
+ else if(!strncmp(curbuf, "triangles", 9))
+ skipsection(f, buf, sizeof(buf));
+ else if(!strncmp(curbuf, "skeleton", 8))
+ readframes(f, buf, sizeof(buf), animbones);
+ else if(!strncmp(curbuf, "vertexanimation", 15))
+ skipsection(f, buf, sizeof(buf));
+ }
+ int numframes = animbones.length() / skel->numbones;
+ dualquat *framebones = new dualquat[(skel->numframes+numframes)*skel->numbones];
+ if(skel->framebones)
+ {
+ memcpy(framebones, skel->framebones, skel->numframes*skel->numbones*sizeof(dualquat));
+ delete[] skel->framebones;
+ }
+ memcpy(&framebones[skel->numframes*skel->numbones], animbones.getbuf(), numframes*skel->numbones*sizeof(dualquat));
+ skel->framebones = framebones;
+ sa = &skel->addskelanim(filename);
+ sa->frame = skel->numframes;
+ sa->range = numframes;
+ skel->numframes += numframes;
+
+ delete f;
+
+ return sa;
+ }
+
+ bool load(const char *meshfile)
+ {
+ name = newstring(meshfile);
+
+ if(!loadmesh(meshfile)) return false;
+
+ return true;
+ }
+ };
+
+ meshgroup *loadmeshes(const char *name, va_list args)
+ {
+ smdmeshgroup *group = new smdmeshgroup;
+ group->shareskeleton(va_arg(args, char *));
+ if(!group->load(name)) { delete group; return NULL; }
+ return group;
+ }
+
+ bool loaddefaultparts()
+ {
+ skelpart &mdl = addpart();
+ mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0;
+ adjustments.setsize(0);
+ const char *fname = name + strlen(name);
+ do --fname; while(fname >= name && *fname!='/' && *fname!='\\');
+ fname++;
+ defformatstring(meshname, "packages/models/%s/%s.smd", name, fname);
+ mdl.meshes = sharemeshes(path(meshname), NULL);
+ if(!mdl.meshes) return false;
+ mdl.initanimparts();
+ mdl.initskins();
+ return true;
+ }
+};
+
+static inline uint hthash(const smd::smdmeshgroup::smdvertkey &k)
+{
+ return hthash(k.pos);
+}
+
+static inline bool htcmp(const smd::smdmeshgroup::smdvertkey &k, int index)
+{
+ if(!k.mesh->verts.inrange(index)) return false;
+ const smd::vert &v = k.mesh->verts[index];
+ return k.pos == v.pos && k.norm == v.norm && k.tc == v.tc && k.blend == v.blend;
+}
+
+skelcommands<smd> smdcommands;
+
diff --git a/src/engine/sound.cpp b/src/engine/sound.cpp
new file mode 100644
index 0000000..38ff025
--- /dev/null
+++ b/src/engine/sound.cpp
@@ -0,0 +1,991 @@
+// sound.cpp: basic positional sound using sdl_mixer
+
+#include "engine.h"
+#include "SDL_mixer.h"
+
+bool nosound = true;
+
+struct soundsample
+{
+ char *name;
+ Mix_Chunk *chunk;
+
+ soundsample() : name(NULL), chunk(NULL) {}
+ ~soundsample() { DELETEA(name); }
+
+ void cleanup() { if(chunk) { Mix_FreeChunk(chunk); chunk = NULL; } }
+ bool load(bool msg = false);
+};
+
+struct soundslot
+{
+ soundsample *sample;
+ int volume;
+};
+
+struct soundconfig
+{
+ int slots, numslots;
+ int maxuses;
+
+ bool hasslot(const soundslot *p, const vector<soundslot> &v) const
+ {
+ return p >= v.getbuf() + slots && p < v.getbuf() + slots+numslots && slots+numslots < v.length();
+ }
+
+ int chooseslot(int flags) const
+ {
+ if(flags&SND_NO_ALT || numslots <= 1) return slots;
+ if(flags&SND_USE_ALT) return slots + 1 + rnd(numslots - 1);
+ return slots + rnd(numslots);
+ }
+};
+
+struct soundchannel
+{
+ int id;
+ bool inuse;
+ vec loc;
+ soundslot *slot;
+ extentity *ent;
+ int radius, volume, pan, flags;
+ bool dirty;
+
+ soundchannel(int id) : id(id) { reset(); }
+
+ bool hasloc() const { return loc.x >= -1e15f; }
+ void clearloc() { loc = vec(-1e16f, -1e16f, -1e16f); }
+
+ void reset()
+ {
+ inuse = false;
+ clearloc();
+ slot = NULL;
+ ent = NULL;
+ radius = 0;
+ volume = -1;
+ pan = -1;
+ flags = 0;
+ dirty = false;
+ }
+};
+vector<soundchannel> channels;
+int maxchannels = 0;
+
+soundchannel &newchannel(int n, soundslot *slot, const vec *loc = NULL, extentity *ent = NULL, int flags = 0, int radius = 0)
+{
+ if(ent)
+ {
+ loc = &ent->o;
+ ent->flags |= EF_SOUND;
+ }
+ while(!channels.inrange(n)) channels.add(channels.length());
+ soundchannel &chan = channels[n];
+ chan.reset();
+ chan.inuse = true;
+ if(loc) chan.loc = *loc;
+ chan.slot = slot;
+ chan.ent = ent;
+ chan.flags = 0;
+ chan.radius = radius;
+ return chan;
+}
+
+void freechannel(int n)
+{
+ if(!channels.inrange(n) || !channels[n].inuse) return;
+ soundchannel &chan = channels[n];
+ chan.inuse = false;
+ if(chan.ent) chan.ent->flags &= ~EF_SOUND;
+}
+
+void syncchannel(soundchannel &chan)
+{
+ if(!chan.dirty) return;
+ if(!Mix_FadingChannel(chan.id)) Mix_Volume(chan.id, chan.volume);
+ Mix_SetPanning(chan.id, 255-chan.pan, chan.pan);
+ chan.dirty = false;
+}
+
+void stopchannels()
+{
+ loopv(channels)
+ {
+ soundchannel &chan = channels[i];
+ if(!chan.inuse) continue;
+ Mix_HaltChannel(i);
+ freechannel(i);
+ }
+}
+
+void setmusicvol(int musicvol);
+extern int musicvol;
+static int curvol = 0;
+VARFP(soundvol, 0, 255, 255,
+{
+ if(!soundvol) { stopchannels(); setmusicvol(0); }
+ else if(!curvol) setmusicvol(musicvol);
+ curvol = soundvol;
+});
+VARFP(musicvol, 0, 128, 255, setmusicvol(soundvol ? musicvol : 0));
+
+char *musicfile = NULL, *musicdonecmd = NULL;
+
+Mix_Music *music = NULL;
+SDL_RWops *musicrw = NULL;
+stream *musicstream = NULL;
+
+void setmusicvol(int musicvol)
+{
+ if(nosound) return;
+ if(music) Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255);
+}
+
+void stopmusic()
+{
+ if(nosound) return;
+ DELETEA(musicfile);
+ DELETEA(musicdonecmd);
+ if(music)
+ {
+ Mix_HaltMusic();
+ Mix_FreeMusic(music);
+ music = NULL;
+ }
+ if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; }
+ DELETEP(musicstream);
+}
+
+#ifdef WIN32
+#define AUDIODRIVER "directsound winmm"
+#else
+#define AUDIODRIVER ""
+#endif
+bool shouldinitaudio = true;
+SVARF(audiodriver, AUDIODRIVER, { shouldinitaudio = true; initwarning("sound configuration", INIT_RESET, CHANGE_SOUND); });
+VARF(usesound, 0, 1, 1, { shouldinitaudio = true; initwarning("sound configuration", INIT_RESET, CHANGE_SOUND); });
+VARF(soundchans, 1, 32, 128, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
+VARF(soundfreq, 0, MIX_DEFAULT_FREQUENCY, 48000, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
+VARF(soundbufferlen, 128, 1024, 4096, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
+
+bool initaudio()
+{
+ static string fallback = "";
+ static bool initfallback = true;
+ static bool restorefallback = false;
+ if(initfallback)
+ {
+ initfallback = false;
+ if(char *env = SDL_getenv("SDL_AUDIODRIVER")) copystring(fallback, env);
+ }
+ if(!fallback[0] && audiodriver[0])
+ {
+ vector<char*> drivers;
+ explodelist(audiodriver, drivers);
+ loopv(drivers)
+ {
+ restorefallback = true;
+ SDL_setenv("SDL_AUDIODRIVER", drivers[i], 1);
+ if(SDL_InitSubSystem(SDL_INIT_AUDIO) >= 0)
+ {
+ drivers.deletearrays();
+ return true;
+ }
+ }
+ drivers.deletearrays();
+ }
+ if(restorefallback)
+ {
+ restorefallback = false;
+ #ifdef WIN32
+ SDL_setenv("SDL_AUDIODRIVER", fallback, 1);
+ #else
+ unsetenv("SDL_AUDIODRIVER");
+ #endif
+ }
+ if(SDL_InitSubSystem(SDL_INIT_AUDIO) >= 0) return true;
+ conoutf(CON_ERROR, "sound init failed: %s", SDL_GetError());
+ return false;
+}
+
+void initsound()
+{
+ SDL_version version;
+ SDL_GetVersion(&version);
+ if(version.major == 2 && version.minor == 0 && version.patch == 6)
+ {
+ nosound = true;
+ if(usesound) conoutf(CON_ERROR, "audio is broken in SDL 2.0.6");
+ return;
+ }
+
+ if(shouldinitaudio)
+ {
+ shouldinitaudio = false;
+ if(SDL_WasInit(SDL_INIT_AUDIO)) SDL_QuitSubSystem(SDL_INIT_AUDIO);
+ if(!usesound || !initaudio())
+ {
+ nosound = true;
+ return;
+ }
+ }
+
+ if(Mix_OpenAudio(soundfreq, MIX_DEFAULT_FORMAT, 2, soundbufferlen)<0)
+ {
+ nosound = true;
+ conoutf(CON_ERROR, "sound init failed (SDL_mixer): %s", Mix_GetError());
+ return;
+ }
+ Mix_AllocateChannels(soundchans);
+ maxchannels = soundchans;
+ nosound = false;
+}
+
+void musicdone()
+{
+ if(music) { Mix_HaltMusic(); Mix_FreeMusic(music); music = NULL; }
+ if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; }
+ DELETEP(musicstream);
+ DELETEA(musicfile);
+ if(!musicdonecmd) return;
+ char *cmd = musicdonecmd;
+ musicdonecmd = NULL;
+ execute(cmd);
+ delete[] cmd;
+}
+
+Mix_Music *loadmusic(const char *name)
+{
+ if(!musicstream) musicstream = openzipfile(name, "rb");
+ if(musicstream)
+ {
+ if(!musicrw) musicrw = musicstream->rwops();
+ if(!musicrw) DELETEP(musicstream);
+ }
+ if(musicrw) music = Mix_LoadMUSType_RW(musicrw, MUS_NONE, 0);
+ else music = Mix_LoadMUS(findfile(name, "rb"));
+ if(!music)
+ {
+ if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; }
+ DELETEP(musicstream);
+ }
+ return music;
+}
+
+void startmusic(char *name, char *cmd)
+{
+ if(nosound) return;
+ stopmusic();
+ if(soundvol && musicvol && *name)
+ {
+ defformatstring(file, "packages/%s", name);
+ path(file);
+ if(loadmusic(file))
+ {
+ DELETEA(musicfile);
+ DELETEA(musicdonecmd);
+ musicfile = newstring(file);
+ if(cmd[0]) musicdonecmd = newstring(cmd);
+ Mix_PlayMusic(music, cmd[0] ? 0 : -1);
+ Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255);
+ intret(1);
+ }
+ else
+ {
+ conoutf(CON_ERROR, "could not play music: %s", file);
+ intret(0);
+ }
+ }
+}
+
+COMMANDN(music, startmusic, "ss");
+
+static Mix_Chunk *loadwav(const char *name)
+{
+ Mix_Chunk *c = NULL;
+ stream *z = openzipfile(name, "rb");
+ if(z)
+ {
+ SDL_RWops *rw = z->rwops();
+ if(rw)
+ {
+ c = Mix_LoadWAV_RW(rw, 0);
+ SDL_FreeRW(rw);
+ }
+ delete z;
+ }
+ if(!c) c = Mix_LoadWAV(findfile(name, "rb"));
+ return c;
+}
+
+template<class T> static void scalewav(T* dst, T* src, size_t len, int scale)
+{
+ len /= sizeof(T);
+ const T* end = src + len;
+ if(scale==2) for(; src < end; src++, dst += scale)
+ {
+ T s = src[0];
+ dst[0] = s;
+ dst[1] = s;
+ }
+ else if(scale==4) for(; src < end; src++, dst += scale)
+ {
+ T s = src[0];
+ dst[0] = s;
+ dst[1] = s;
+ dst[2] = s;
+ dst[3] = s;
+ }
+ else for(; src < end; src++)
+ {
+ T s = src[0];
+ loopi(scale) *dst++ = s;
+ }
+}
+
+static Mix_Chunk *loadwavscaled(const char *name)
+{
+ int mixerfreq = 0;
+ Uint16 mixerformat = 0;
+ int mixerchannels = 0;
+ if(!Mix_QuerySpec(&mixerfreq, &mixerformat, &mixerchannels)) return NULL;
+
+ SDL_AudioSpec spec;
+ Uint8 *audiobuf = NULL;
+ Uint32 audiolen = 0;
+ stream *z = openzipfile(name, "rb");
+ if(z)
+ {
+ SDL_RWops *rw = z->rwops();
+ if(rw)
+ {
+ SDL_LoadWAV_RW(rw, 0, &spec, &audiobuf, &audiolen);
+ SDL_FreeRW(rw);
+ }
+ delete z;
+ }
+ if(!audiobuf) SDL_LoadWAV(findfile(name, "rb"), &spec, &audiobuf, &audiolen);
+ if(!audiobuf) return NULL;
+ int samplesize = ((spec.format&0xFF)/8) * spec.channels;
+ int scale = mixerfreq / spec.freq;
+ if(scale >= 2)
+ {
+ Uint8 *scalebuf = (Uint8*)SDL_malloc(audiolen * scale);
+ if(scalebuf)
+ {
+ switch(samplesize)
+ {
+ case 1: scalewav((uchar*)scalebuf, (uchar*)audiobuf, audiolen, scale); break;
+ case 2: scalewav((ushort*)scalebuf, (ushort*)audiobuf, audiolen, scale); break;
+ case 4: scalewav((uint*)scalebuf, (uint*)audiobuf, audiolen, scale); break;
+ case 8: scalewav((ullong*)scalebuf, (ullong*)audiobuf, audiolen, scale); break;
+ default: SDL_free(scalebuf); scalebuf = NULL; break;
+ }
+ if(scalebuf)
+ {
+ SDL_free(audiobuf);
+ audiobuf = scalebuf;
+ audiolen *= scale;
+ spec.freq *= scale;
+ }
+ }
+ }
+ if(spec.freq != mixerfreq || spec.format != mixerformat || spec.channels != mixerchannels)
+ {
+ SDL_AudioCVT cvt;
+ if(SDL_BuildAudioCVT(&cvt, spec.format, spec.channels, spec.freq, mixerformat, mixerchannels, mixerfreq) < 0)
+ {
+ SDL_free(audiobuf);
+ return NULL;
+ }
+ if(cvt.filters[0])
+ {
+ cvt.len = audiolen & ~(samplesize-1);
+ cvt.buf = (Uint8*)SDL_malloc(cvt.len * cvt.len_mult);
+ if(!cvt.buf) { SDL_free(audiobuf); return NULL; }
+ SDL_memcpy(cvt.buf, audiobuf, cvt.len);
+ SDL_free(audiobuf);
+ if(SDL_ConvertAudio(&cvt) < 0) { SDL_free(cvt.buf); return NULL; }
+ audiobuf = cvt.buf;
+ audiolen = cvt.len_cvt;
+ }
+ }
+ Mix_Chunk *c = Mix_QuickLoad_RAW(audiobuf, audiolen);
+ if(!c) { SDL_free(audiobuf); return NULL; }
+ c->allocated = 1;
+ return c;
+}
+
+VARFP(fixwav, 0, 1, 1, initwarning("sound configuration", INIT_LOAD, CHANGE_SOUND));
+
+bool soundsample::load(bool msg)
+{
+ if(chunk) return true;
+ if(!name[0]) return false;
+
+ static const char * const exts[] = { "", ".wav", ".ogg" };
+ string filename;
+ loopi(sizeof(exts)/sizeof(exts[0]))
+ {
+ formatstring(filename, "packages/sounds/%s%s", name, exts[i]);
+ if(msg && !i) renderprogress(0, filename);
+ path(filename);
+ if(fixwav)
+ {
+ size_t len = strlen(filename);
+ if(len >= 4 && !strcasecmp(filename + len - 4, ".wav"))
+ {
+ chunk = loadwavscaled(filename);
+ if(chunk) return true;
+ }
+ }
+ chunk = loadwav(filename);
+ if(chunk) return true;
+ }
+
+ conoutf(CON_ERROR, "failed to load sample: packages/sounds/%s", name);
+ return false;
+}
+
+static hashnameset<soundsample> samples;
+
+static void cleanupsamples()
+{
+ enumerate(samples, soundsample, s, s.cleanup());
+}
+
+static struct soundtype
+{
+ vector<soundslot> slots;
+ vector<soundconfig> configs;
+
+ int findsound(const char *name, int vol)
+ {
+ loopv(configs)
+ {
+ soundconfig &s = configs[i];
+ loopj(s.numslots)
+ {
+ soundslot &c = slots[s.slots+j];
+ if(!strcmp(c.sample->name, name) && (!vol || c.volume==vol)) return i;
+ }
+ }
+ return -1;
+ }
+
+ int addslot(const char *name, int vol)
+ {
+ soundsample *s = samples.access(name);
+ if(!s)
+ {
+ char *n = newstring(name);
+ s = &samples[n];
+ s->name = n;
+ s->chunk = NULL;
+ }
+ soundslot *oldslots = slots.getbuf();
+ int oldlen = slots.length();
+ soundslot &slot = slots.add();
+ // soundslots.add() may relocate slot pointers
+ if(slots.getbuf() != oldslots) loopv(channels)
+ {
+ soundchannel &chan = channels[i];
+ if(chan.inuse && chan.slot >= oldslots && chan.slot < &oldslots[oldlen])
+ chan.slot = &slots[chan.slot - oldslots];
+ }
+ slot.sample = s;
+ slot.volume = vol ? vol : 100;
+ return oldlen;
+ }
+
+ int addsound(const char *name, int vol, int maxuses = 0)
+ {
+ soundconfig &s = configs.add();
+ s.slots = addslot(name, vol);
+ s.numslots = 1;
+ s.maxuses = maxuses;
+ return configs.length()-1;
+ }
+
+ void addalt(const char *name, int vol)
+ {
+ if(configs.empty()) return;
+ addslot(name, vol);
+ configs.last().numslots++;
+ }
+
+ void clear()
+ {
+ slots.setsize(0);
+ configs.setsize(0);
+ }
+
+ void reset()
+ {
+ loopv(channels)
+ {
+ soundchannel &chan = channels[i];
+ if(chan.inuse && slots.inbuf(chan.slot))
+ {
+ Mix_HaltChannel(i);
+ freechannel(i);
+ }
+ }
+ clear();
+ }
+
+ void preloadsound(int n)
+ {
+ if(nosound || !configs.inrange(n)) return;
+ soundconfig &config = configs[n];
+ loopk(config.numslots) slots[config.slots+k].sample->load(true);
+ }
+
+ bool playing(const soundchannel &chan, const soundconfig &config) const
+ {
+ return chan.inuse && config.hasslot(chan.slot, slots);
+ }
+} gamesounds, mapsounds;
+
+void registersound(char *name, int *vol) { intret(gamesounds.addsound(name, *vol, 0)); }
+COMMAND(registersound, "si");
+
+void mapsound(char *name, int *vol, int *maxuses) { intret(mapsounds.addsound(name, *vol, *maxuses < 0 ? 0 : max(1, *maxuses))); }
+COMMAND(mapsound, "sii");
+
+void altsound(char *name, int *vol) { gamesounds.addalt(name, *vol); }
+COMMAND(altsound, "si");
+
+void altmapsound(char *name, int *vol) { mapsounds.addalt(name, *vol); }
+COMMAND(altmapsound, "si");
+
+ICOMMAND(numsounds, "", (), intret(gamesounds.configs.length()));
+ICOMMAND(nummapsounds, "", (), intret(mapsounds.configs.length()));
+
+void soundreset()
+{
+ gamesounds.reset();
+}
+COMMAND(soundreset, "");
+
+void mapsoundreset()
+{
+ mapsounds.reset();
+}
+COMMAND(mapsoundreset, "");
+
+void resetchannels()
+{
+ loopv(channels) if(channels[i].inuse) freechannel(i);
+ channels.shrink(0);
+}
+
+void clear_sound()
+{
+ closemumble();
+ if(nosound) return;
+ stopmusic();
+
+ cleanupsamples();
+ gamesounds.clear();
+ mapsounds.clear();
+ samples.clear();
+ Mix_CloseAudio();
+ resetchannels();
+}
+
+void stopmapsounds()
+{
+ loopv(channels) if(channels[i].inuse && channels[i].ent)
+ {
+ Mix_HaltChannel(i);
+ freechannel(i);
+ }
+}
+
+void clearmapsounds()
+{
+ stopmapsounds();
+ mapsounds.clear();
+}
+
+void stopmapsound(extentity *e)
+{
+ loopv(channels)
+ {
+ soundchannel &chan = channels[i];
+ if(chan.inuse && chan.ent == e)
+ {
+ Mix_HaltChannel(i);
+ freechannel(i);
+ }
+ }
+}
+
+void checkmapsounds()
+{
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents)
+ {
+ extentity &e = *ents[i];
+ if(e.type!=ET_SOUND) continue;
+ if(camera1->o.dist(e.o) < e.attr2)
+ {
+ if(!(e.flags&EF_SOUND)) playsound(e.attr1, NULL, &e, SND_MAP, -1);
+ }
+ else if(e.flags&EF_SOUND) stopmapsound(&e);
+ }
+}
+
+VAR(stereo, 0, 1, 1);
+
+bool updatechannel(soundchannel &chan)
+{
+ if(!chan.slot) return false;
+ int vol = soundvol, pan = 255/2;
+ if(chan.hasloc())
+ {
+ vec v;
+ float dist = chan.loc.dist(camera1->o, v);
+ int rad = 0;
+ if(chan.ent)
+ {
+ rad = chan.ent->attr2;
+ if(chan.ent->attr3)
+ {
+ rad -= chan.ent->attr3;
+ dist -= chan.ent->attr3;
+ }
+ }
+ else if(chan.radius > 0) rad = chan.radius;
+ if(rad > 0) vol -= int(clamp(dist/rad, 0.0f, 1.0f)*soundvol); // simple mono distance attenuation
+ if(stereo && (v.x != 0 || v.y != 0) && dist>0)
+ {
+ v.rotate_around_z(-camera1->yaw*RAD);
+ pan = int(255.9f*(0.5f - 0.5f*v.x/v.magnitude2())); // range is from 0 (left) to 255 (right)
+ }
+ }
+ vol = (vol*MIX_MAX_VOLUME*chan.slot->volume)/255/255;
+ vol = min(vol, MIX_MAX_VOLUME);
+ if(vol == chan.volume && pan == chan.pan) return false;
+ chan.volume = vol;
+ chan.pan = pan;
+ chan.dirty = true;
+ return true;
+}
+
+void reclaimchannels()
+{
+ loopv(channels)
+ {
+ soundchannel &chan = channels[i];
+ if(chan.inuse && !Mix_Playing(i)) freechannel(i);
+ }
+}
+
+void syncchannels()
+{
+ loopv(channels)
+ {
+ soundchannel &chan = channels[i];
+ if(chan.inuse && chan.hasloc() && updatechannel(chan)) syncchannel(chan);
+ }
+}
+
+VARP(minimizedsounds, 0, 0, 1);
+
+void updatesounds()
+{
+ updatemumble();
+ if(nosound) return;
+ if(minimized && !minimizedsounds) stopsounds();
+ else
+ {
+ reclaimchannels();
+ if(mainmenu) stopmapsounds();
+ else checkmapsounds();
+ syncchannels();
+ }
+ if(music)
+ {
+ if(!Mix_PlayingMusic()) musicdone();
+ else if(Mix_PausedMusic()) Mix_ResumeMusic();
+ }
+}
+
+VARP(maxsoundsatonce, 0, 7, 100);
+
+VAR(dbgsound, 0, 0, 1);
+
+void preloadsound(int n)
+{
+ gamesounds.preloadsound(n);
+}
+
+void preloadmapsound(int n)
+{
+ mapsounds.preloadsound(n);
+}
+
+void preloadmapsounds()
+{
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents)
+ {
+ extentity &e = *ents[i];
+ if(e.type==ET_SOUND) mapsounds.preloadsound(e.attr1);
+ }
+}
+
+int playsound(int n, const vec *loc, extentity *ent, int flags, int loops, int fade, int chanid, int radius, int expire)
+{
+ if(nosound || !soundvol || (minimized && !minimizedsounds)) return -1;
+
+ soundtype &sounds = ent || flags&SND_MAP ? mapsounds : gamesounds;
+ if(!sounds.configs.inrange(n)) { conoutf(CON_WARN, "unregistered sound: %d", n); return -1; }
+ soundconfig &config = sounds.configs[n];
+
+ if(loc)
+ {
+ // cull sounds that are unlikely to be heard
+ int maxrad = game::maxsoundradius(n);
+ if(radius <= 0 || maxrad < radius) radius = maxrad;
+ if(camera1->o.dist(*loc) > 1.5f*radius)
+ {
+ if(channels.inrange(chanid) && sounds.playing(channels[chanid], config))
+ {
+ Mix_HaltChannel(chanid);
+ freechannel(chanid);
+ }
+ return -1;
+ }
+ }
+
+ if(chanid < 0)
+ {
+ if(config.maxuses)
+ {
+ int uses = 0;
+ loopv(channels) if(sounds.playing(channels[i], config) && ++uses >= config.maxuses) return -1;
+ }
+
+ // avoid bursts of sounds with heavy packetloss and in sp
+ static int soundsatonce = 0, lastsoundmillis = 0;
+ if(totalmillis == lastsoundmillis) soundsatonce++; else soundsatonce = 1;
+ lastsoundmillis = totalmillis;
+ if(maxsoundsatonce && soundsatonce > maxsoundsatonce) return -1;
+ }
+
+ if(channels.inrange(chanid))
+ {
+ soundchannel &chan = channels[chanid];
+ if(sounds.playing(chan, config))
+ {
+ if(loc) chan.loc = *loc;
+ else if(chan.hasloc()) chan.clearloc();
+ return chanid;
+ }
+ }
+ if(fade < 0) return -1;
+
+ soundslot &slot = sounds.slots[config.chooseslot(flags)];
+ if(!slot.sample->chunk && !slot.sample->load()) return -1;
+
+ if(dbgsound) conoutf(CON_DEBUG, "sound: %s", slot.sample->name);
+
+ chanid = -1;
+ loopv(channels) if(!channels[i].inuse) { chanid = i; break; }
+ if(chanid < 0 && channels.length() < maxchannels) chanid = channels.length();
+ if(chanid < 0) loopv(channels) if(!channels[i].volume) { chanid = i; break; }
+ if(chanid < 0) return -1;
+
+ soundchannel &chan = newchannel(chanid, &slot, loc, ent, flags, radius);
+ updatechannel(chan);
+ int playing = -1;
+ if(fade)
+ {
+ Mix_Volume(chanid, chan.volume);
+ playing = expire >= 0 ? Mix_FadeInChannelTimed(chanid, slot.sample->chunk, loops, fade, expire) : Mix_FadeInChannel(chanid, slot.sample->chunk, loops, fade);
+ }
+ else playing = expire >= 0 ? Mix_PlayChannelTimed(chanid, slot.sample->chunk, loops, expire) : Mix_PlayChannel(chanid, slot.sample->chunk, loops);
+ if(playing >= 0) syncchannel(chan);
+ else freechannel(chanid);
+ return playing;
+}
+
+void stopsounds()
+{
+ loopv(channels) if(channels[i].inuse)
+ {
+ Mix_HaltChannel(i);
+ freechannel(i);
+ }
+}
+
+bool stopsound(int n, int chanid, int fade)
+{
+ if(!gamesounds.configs.inrange(n) || !channels.inrange(chanid) || !channels[chanid].inuse || !gamesounds.playing(channels[chanid], gamesounds.configs[n])) return false;
+ if(dbgsound) conoutf(CON_DEBUG, "stopsound: %s", channels[chanid].slot->sample->name);
+ if(!fade || !Mix_FadeOutChannel(chanid, fade))
+ {
+ Mix_HaltChannel(chanid);
+ freechannel(chanid);
+ }
+ return true;
+}
+
+int playsoundname(const char *s, const vec *loc, int vol, int flags, int loops, int fade, int chanid, int radius, int expire)
+{
+ if(!vol) vol = 100;
+ int id = gamesounds.findsound(s, vol);
+ if(id < 0) id = gamesounds.addsound(s, vol);
+ return playsound(id, loc, NULL, flags, loops, fade, chanid, radius, expire);
+}
+
+ICOMMAND(sound, "i", (int *n), playsound(*n));
+
+void resetsound()
+{
+ clearchanges(CHANGE_SOUND);
+ if(!nosound)
+ {
+ cleanupsamples();
+ if(music)
+ {
+ Mix_HaltMusic();
+ Mix_FreeMusic(music);
+ }
+ if(musicstream) musicstream->seek(0, SEEK_SET);
+ Mix_CloseAudio();
+ }
+ initsound();
+ resetchannels();
+ if(nosound)
+ {
+ DELETEA(musicfile);
+ DELETEA(musicdonecmd);
+ music = NULL;
+ cleanupsamples();
+ return;
+ }
+ if(music && loadmusic(musicfile))
+ {
+ Mix_PlayMusic(music, musicdonecmd ? 0 : -1);
+ Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255);
+ }
+ else
+ {
+ DELETEA(musicfile);
+ DELETEA(musicdonecmd);
+ }
+}
+
+COMMAND(resetsound, "");
+
+#ifdef WIN32
+
+#include <wchar.h>
+
+#else
+
+#include <unistd.h>
+
+#ifdef _POSIX_SHARED_MEMORY_OBJECTS
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <wchar.h>
+#endif
+
+#endif
+
+#if defined(WIN32) || defined(_POSIX_SHARED_MEMORY_OBJECTS)
+struct MumbleInfo
+{
+ int version, timestamp;
+ vec pos, front, top;
+ wchar_t name[256];
+};
+#endif
+
+#ifdef WIN32
+static HANDLE mumblelink = NULL;
+static MumbleInfo *mumbleinfo = NULL;
+#define VALID_MUMBLELINK (mumblelink && mumbleinfo)
+#elif defined(_POSIX_SHARED_MEMORY_OBJECTS)
+static int mumblelink = -1;
+static MumbleInfo *mumbleinfo = (MumbleInfo *)-1;
+#define VALID_MUMBLELINK (mumblelink >= 0 && mumbleinfo != (MumbleInfo *)-1)
+#endif
+
+#ifdef VALID_MUMBLELINK
+VARFP(mumble, 0, 1, 1, { if(mumble) initmumble(); else closemumble(); });
+#else
+VARFP(mumble, 0, 0, 1, { if(mumble) initmumble(); else closemumble(); });
+#endif
+
+void initmumble()
+{
+ if(!mumble) return;
+#ifdef VALID_MUMBLELINK
+ if(VALID_MUMBLELINK) return;
+
+ #ifdef WIN32
+ mumblelink = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "MumbleLink");
+ if(mumblelink)
+ {
+ mumbleinfo = (MumbleInfo *)MapViewOfFile(mumblelink, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(MumbleInfo));
+ if(mumbleinfo) wcsncpy(mumbleinfo->name, L"Sauerbraten", 256);
+ }
+ #elif defined(_POSIX_SHARED_MEMORY_OBJECTS)
+ defformatstring(shmname, "/MumbleLink.%d", getuid());
+ mumblelink = shm_open(shmname, O_RDWR, 0);
+ if(mumblelink >= 0)
+ {
+ mumbleinfo = (MumbleInfo *)mmap(NULL, sizeof(MumbleInfo), PROT_READ|PROT_WRITE, MAP_SHARED, mumblelink, 0);
+ if(mumbleinfo != (MumbleInfo *)-1) wcsncpy(mumbleinfo->name, L"Sauerbraten", 256);
+ }
+ #endif
+ if(!VALID_MUMBLELINK) closemumble();
+#else
+ conoutf(CON_ERROR, "Mumble positional audio is not available on this platform.");
+#endif
+}
+
+void closemumble()
+{
+#ifdef WIN32
+ if(mumbleinfo) { UnmapViewOfFile(mumbleinfo); mumbleinfo = NULL; }
+ if(mumblelink) { CloseHandle(mumblelink); mumblelink = NULL; }
+#elif defined(_POSIX_SHARED_MEMORY_OBJECTS)
+ if(mumbleinfo != (MumbleInfo *)-1) { munmap(mumbleinfo, sizeof(MumbleInfo)); mumbleinfo = (MumbleInfo *)-1; }
+ if(mumblelink >= 0) { close(mumblelink); mumblelink = -1; }
+#endif
+}
+
+static inline vec mumblevec(const vec &v, bool pos = false)
+{
+ // change from X left, Z up, Y forward to X right, Y up, Z forward
+ // 8 cube units = 1 meter
+ vec m(-v.x, v.z, v.y);
+ if(pos) m.div(8);
+ return m;
+}
+
+void updatemumble()
+{
+#ifdef VALID_MUMBLELINK
+ if(!VALID_MUMBLELINK) return;
+
+ static int timestamp = 0;
+
+ mumbleinfo->version = 1;
+ mumbleinfo->timestamp = ++timestamp;
+
+ mumbleinfo->pos = mumblevec(player->o, true);
+ mumbleinfo->front = mumblevec(vec(RAD*player->yaw, RAD*player->pitch));
+ mumbleinfo->top = mumblevec(vec(RAD*player->yaw, RAD*(player->pitch+90)));
+#endif
+}
+
diff --git a/src/engine/textedit.h b/src/engine/textedit.h
new file mode 100644
index 0000000..c273661
--- /dev/null
+++ b/src/engine/textedit.h
@@ -0,0 +1,770 @@
+
+struct editline
+{
+ enum { CHUNKSIZE = 256 };
+
+ char *text;
+ int len, maxlen;
+
+ editline() : text(NULL), len(0), maxlen(0) {}
+ editline(const char *init) : text(NULL), len(0), maxlen(0)
+ {
+ set(init);
+ }
+
+ bool empty() { return len <= 0; }
+
+ void clear()
+ {
+ DELETEA(text);
+ len = maxlen = 0;
+ }
+
+ bool grow(int total, const char *fmt = "", ...)
+ {
+ if(total + 1 <= maxlen) return false;
+ maxlen = (total + CHUNKSIZE) - total%CHUNKSIZE;
+ char *newtext = new char[maxlen];
+ if(fmt)
+ {
+ va_list args;
+ va_start(args, fmt);
+ vformatstring(newtext, fmt, args, maxlen);
+ va_end(args);
+ }
+ else newtext[0] = '\0';
+ DELETEA(text);
+ text = newtext;
+ return true;
+ }
+
+ void set(const char *str, int slen = -1)
+ {
+ if(slen < 0)
+ {
+ slen = strlen(str);
+ if(!grow(slen, "%s", str)) memcpy(text, str, slen + 1);
+ }
+ else
+ {
+ grow(slen);
+ memcpy(text, str, slen);
+ text[slen] = '\0';
+ }
+ len = slen;
+ }
+
+ void prepend(const char *str)
+ {
+ int slen = strlen(str);
+ if(!grow(slen + len, "%s%s", str, text ? text : ""))
+ {
+ memmove(&text[slen], text, len + 1);
+ memcpy(text, str, slen + 1);
+ }
+ len += slen;
+ }
+
+ void append(const char *str)
+ {
+ int slen = strlen(str);
+ if(!grow(len + slen, "%s%s", text ? text : "", str)) memcpy(&text[len], str, slen + 1);
+ len += slen;
+ }
+
+ bool read(stream *f, int chop = -1)
+ {
+ if(chop < 0) chop = INT_MAX; else chop++;
+ set("");
+ while(len + 1 < chop && f->getline(&text[len], min(maxlen, chop) - len))
+ {
+ len += strlen(&text[len]);
+ if(len > 0 && text[len-1] == '\n')
+ {
+ text[--len] = '\0';
+ return true;
+ }
+ if(len + 1 >= maxlen && len + 1 < chop) grow(len + CHUNKSIZE, "%s", text);
+ }
+ if(len + 1 >= chop)
+ {
+ char buf[CHUNKSIZE];
+ while(f->getline(buf, sizeof(buf)))
+ {
+ int blen = strlen(buf);
+ if(blen > 0 && buf[blen-1] == '\n') return true;
+ }
+ }
+ return len > 0;
+ }
+
+ void del(int start, int count)
+ {
+ if(!text) return;
+ if(start < 0) { count += start; start = 0; }
+ if(count <= 0 || start >= len) return;
+ if(start + count > len) count = len - start - 1;
+ memmove(&text[start], &text[start+count], len + 1 - (start + count));
+ len -= count;
+ }
+
+ void chop(int newlen)
+ {
+ if(!text) return;
+ len = clamp(newlen, 0, len);
+ text[len] = '\0';
+ }
+
+ void insert(char *str, int start, int count = 0)
+ {
+ if(count <= 0) count = strlen(str);
+ start = clamp(start, 0, len);
+ grow(len + count, "%s", text ? text : "");
+ memmove(&text[start + count], &text[start], len - start + 1);
+ memcpy(&text[start], str, count);
+ len += count;
+ }
+
+ void combinelines(vector<editline> &src)
+ {
+ if(src.empty()) set("");
+ else loopv(src)
+ {
+ if(i) append("\n");
+ if(!i) set(src[i].text, src[i].len);
+ else insert(src[i].text, len, src[i].len);
+ }
+ }
+};
+
+struct editor
+{
+ int mode; //editor mode - 1= keep while focused, 2= keep while used in gui, 3= keep forever (i.e. until mode changes)
+ bool active, rendered;
+ const char *name;
+ const char *filename;
+
+ int cx, cy; // cursor position - ensured to be valid after a region() or currentline()
+ int mx, my; // selection mark, mx=-1 if following cursor - avoid direct access, instead use region()
+ int maxx, maxy; // maxy=-1 if unlimited lines, 1 if single line editor
+
+ int scrolly; // vertical scroll offset
+
+ bool linewrap;
+ int pixelwidth; // required for up/down/hit/draw/bounds
+ int pixelheight; // -1 for variable sized, i.e. from bounds()
+
+ vector<editline> lines; // MUST always contain at least one line!
+
+ editor(const char *name, int mode, const char *initval) :
+ mode(mode), active(true), rendered(false), name(newstring(name)), filename(NULL),
+ cx(0), cy(0), mx(-1), maxx(-1), maxy(-1), scrolly(0), linewrap(false), pixelwidth(-1), pixelheight(-1)
+ {
+ //printf("editor %08x '%s'\n", this, name);
+ lines.add().set(initval ? initval : "");
+ }
+
+ ~editor()
+ {
+ //printf("~editor %08x '%s'\n", this, name);
+ DELETEA(name);
+ DELETEA(filename);
+ clear(NULL);
+ }
+
+ void clear(const char *init = "")
+ {
+ cx = cy = 0;
+ mark(false);
+ loopv(lines) lines[i].clear();
+ lines.shrink(0);
+ if(init) lines.add().set(init);
+ }
+
+ void setfile(const char *fname)
+ {
+ DELETEA(filename);
+ if(fname) filename = newstring(fname);
+ }
+
+ void load()
+ {
+ if(!filename) return;
+ clear(NULL);
+ stream *file = openutf8file(filename, "r");
+ if(file)
+ {
+ while(lines.add().read(file, maxx) && (maxy < 0 || lines.length() <= maxy));
+ lines.pop().clear();
+ delete file;
+ }
+ if(lines.empty()) lines.add().set("");
+ }
+
+ void save()
+ {
+ if(!filename) return;
+ stream *file = openutf8file(filename, "w");
+ if(!file) return;
+ loopv(lines) file->putline(lines[i].text);
+ delete file;
+ }
+
+ void mark(bool enable)
+ {
+ mx = (enable) ? cx : -1;
+ my = cy;
+ }
+
+ void selectall()
+ {
+ mx = my = INT_MAX;
+ cx = cy = 0;
+ }
+
+ // constrain results to within buffer - s=start, e=end, return true if a selection range
+ // also ensures that cy is always within lines[] and cx is valid
+ bool region(int &sx, int &sy, int &ex, int &ey)
+ {
+ int n = lines.length();
+ ASSERT(n != 0);
+ if(cy < 0) cy = 0; else if(cy >= n) cy = n-1;
+ int len = lines[cy].len;
+ if(cx < 0) cx = 0; else if(cx > len) cx = len;
+ if(mx >= 0)
+ {
+ if(my < 0) my = 0; else if(my >= n) my = n-1;
+ len = lines[my].len;
+ if(mx > len) mx = len;
+ }
+ sx = (mx >= 0) ? mx : cx;
+ sy = (mx >= 0) ? my : cy;
+ ex = cx;
+ ey = cy;
+ if(sy > ey) { swap(sy, ey); swap(sx, ex); }
+ else if(sy==ey && sx > ex) swap(sx, ex);
+ return (sx != ex) || (sy != ey);
+ }
+
+ bool region() { int sx, sy, ex, ey; return region(sx, sy, ex, ey); }
+
+ // also ensures that cy is always within lines[] and cx is valid
+ editline &currentline()
+ {
+ int n = lines.length();
+ ASSERT(n != 0);
+ if(cy < 0) cy = 0; else if(cy >= n) cy = n-1;
+ if(cx < 0) cx = 0; else if(cx > lines[cy].len) cx = lines[cy].len;
+ return lines[cy];
+ }
+
+ void copyselectionto(editor *b)
+ {
+ if(b==this) return;
+
+ b->clear(NULL);
+ int sx, sy, ex, ey;
+ region(sx, sy, ex, ey);
+ loopi(1+ey-sy)
+ {
+ if(b->maxy != -1 && b->lines.length() >= b->maxy) break;
+ int y = sy+i;
+ char *line = lines[y].text;
+ int len = lines[y].len;
+ if(y == sy && y == ey)
+ {
+ line += sx;
+ len = ex - sx;
+ }
+ else if(y == sy) line += sx;
+ else if(y == ey) len = ex;
+ b->lines.add().set(line, len);
+ }
+ if(b->lines.empty()) b->lines.add().set("");
+ }
+
+ char *tostring()
+ {
+ int len = 0;
+ loopv(lines) len += lines[i].len + 1;
+ char *str = newstring(len);
+ int offset = 0;
+ loopv(lines)
+ {
+ editline &l = lines[i];
+ memcpy(&str[offset], l.text, l.len);
+ offset += l.len;
+ str[offset++] = '\n';
+ }
+ str[offset] = '\0';
+ return str;
+ }
+
+ char *selectiontostring()
+ {
+ vector<char> buf;
+ int sx, sy, ex, ey;
+ region(sx, sy, ex, ey);
+ loopi(1+ey-sy)
+ {
+ int y = sy+i;
+ char *line = lines[y].text;
+ int len = lines[y].len;
+ if(y == sy && y == ey)
+ {
+ line += sx;
+ len = ex - sx;
+ }
+ else if(y == sy) line += sx;
+ else if(y == ey) len = ex;
+ buf.put(line, len);
+ buf.add('\n');
+ }
+ buf.add('\0');
+ return newstring(buf.getbuf(), buf.length()-1);
+ }
+
+ void removelines(int start, int count)
+ {
+ loopi(count) lines[start+i].clear();
+ lines.remove(start, count);
+ }
+
+ bool del() // removes the current selection (if any)
+ {
+ int sx, sy, ex, ey;
+ if(!region(sx, sy, ex, ey))
+ {
+ mark(false);
+ return false;
+ }
+ if(sy == ey)
+ {
+ if(sx == 0 && ex == lines[ey].len) removelines(sy, 1);
+ else lines[sy].del(sx, ex - sx);
+ }
+ else
+ {
+ if(ey > sy+1) { removelines(sy+1, ey-(sy+1)); ey = sy+1; }
+ if(ex == lines[ey].len) removelines(ey, 1); else lines[ey].del(0, ex);
+ if(sx == 0) removelines(sy, 1); else lines[sy].del(sx, lines[sy].len - sx);
+ }
+ if(lines.empty()) lines.add().set("");
+ mark(false);
+ cx = sx;
+ cy = sy;
+ editline &current = currentline();
+ if(cx >= current.len && cy < lines.length() - 1)
+ {
+ current.append(lines[cy+1].text);
+ removelines(cy + 1, 1);
+ }
+ return true;
+ }
+
+ void insert(char ch)
+ {
+ del();
+ editline &current = currentline();
+ if(ch == '\n')
+ {
+ if(maxy == -1 || cy < maxy-1)
+ {
+ editline newline(&current.text[cx]);
+ current.chop(cx);
+ cy = min(lines.length(), cy+1);
+ lines.insert(cy, newline);
+ }
+ else current.chop(cx);
+ cx = 0;
+ }
+ else
+ {
+ int len = current.len;
+ if(maxx >= 0 && len > maxx-1) len = maxx-1;
+ if(cx <= len) current.insert(&ch, cx++, 1);
+ }
+ }
+
+ void insert(const char *s)
+ {
+ while(*s) insert(*s++);
+ }
+
+ void insertallfrom(editor *b)
+ {
+ if(b==this) return;
+
+ del();
+
+ if(b->lines.length() == 1 || maxy == 1)
+ {
+ editline &current = currentline();
+ char *str = b->lines[0].text;
+ int slen = b->lines[0].len;
+ if(maxx >= 0 && b->lines[0].len + cx > maxx) slen = maxx-cx;
+ if(slen > 0)
+ {
+ int len = current.len;
+ if(maxx >= 0 && slen + cx + len > maxx) len = max(0, maxx-(cx+slen));
+ current.insert(str, cx, slen);
+ cx += slen;
+ }
+ }
+ else
+ {
+ loopv(b->lines)
+ {
+ if(!i)
+ {
+ lines[cy++].append(b->lines[i].text);
+ }
+ else if(i >= b->lines.length())
+ {
+ cx = b->lines[i].len;
+ lines[cy].prepend(b->lines[i].text);
+ }
+ else if(maxy < 0 || lines.length() < maxy) lines.insert(cy++, editline(b->lines[i].text));
+ }
+ }
+ }
+
+ void key(int code)
+ {
+ switch(code)
+ {
+ case SDLK_UP:
+ if(linewrap)
+ {
+ int x, y;
+ char *str = currentline().text;
+ text_pos(str, cx+1, x, y, pixelwidth);
+ if(y > 0) { cx = text_visible(str, x, y-FONTH, pixelwidth); break; }
+ }
+ cy--;
+ break;
+ case SDLK_DOWN:
+ if(linewrap)
+ {
+ int x, y, width, height;
+ char *str = currentline().text;
+ text_pos(str, cx, x, y, pixelwidth);
+ text_bounds(str, width, height, pixelwidth);
+ y += FONTH;
+ if(y < height) { cx = text_visible(str, x, y, pixelwidth); break; }
+ }
+ cy++;
+ break;
+ case -4:
+ cy--;
+ break;
+ case -5:
+ cy++;
+ break;
+ case SDLK_PAGEUP:
+ cy-=pixelheight/FONTH;
+ break;
+ case SDLK_PAGEDOWN:
+ cy+=pixelheight/FONTH;
+ break;
+ case SDLK_HOME:
+ cx = cy = 0;
+ break;
+ case SDLK_END:
+ cx = cy = INT_MAX;
+ break;
+ case SDLK_LEFT:
+ cx--;
+ break;
+ case SDLK_RIGHT:
+ cx++;
+ break;
+ case SDLK_DELETE:
+ if(!del())
+ {
+ editline &current = currentline();
+ if(cx < current.len) current.del(cx, 1);
+ else if(cy < lines.length()-1)
+ { //combine with next line
+ current.append(lines[cy+1].text);
+ removelines(cy+1, 1);
+ }
+ }
+ break;
+ case SDLK_BACKSPACE:
+ if(!del())
+ {
+ editline &current = currentline();
+ if(cx > 0) current.del(--cx, 1);
+ else if(cy > 0)
+ { //combine with previous line
+ cx = lines[cy-1].len;
+ lines[cy-1].append(current.text);
+ removelines(cy--, 1);
+ }
+ }
+ break;
+ case SDLK_LSHIFT:
+ case SDLK_RSHIFT:
+ break;
+ case SDLK_RETURN:
+ insert('\n');
+ break;
+ case SDLK_TAB:
+ insert('\t');
+ break;
+ }
+ }
+
+ void input(const char *str, int len)
+ {
+ loopi(len) insert(str[i]);
+ }
+
+ void hit(int hitx, int hity, bool dragged)
+ {
+ int maxwidth = linewrap?pixelwidth:-1;
+ int h = 0;
+ for(int i = scrolly; i < lines.length(); i++)
+ {
+ int width, height;
+ text_bounds(lines[i].text, width, height, maxwidth);
+ if(h + height > pixelheight) break;
+
+ if(hity >= h && hity <= h+height)
+ {
+ int x = text_visible(lines[i].text, hitx, hity-h, maxwidth);
+ if(dragged) { mx = x; my = i; } else { cx = x; cy = i; };
+ break;
+ }
+ h+=height;
+ }
+ }
+
+ int limitscrolly()
+ {
+ int maxwidth = linewrap?pixelwidth:-1;
+ int slines = lines.length();
+ for(int ph = pixelheight; slines > 0 && ph > 0;)
+ {
+ int width, height;
+ text_bounds(lines[slines-1].text, width, height, maxwidth);
+ if(height > ph) break;
+ ph -= height;
+ slines--;
+ }
+ return slines;
+ }
+
+ void draw(int x, int y, int color, bool hit)
+ {
+ int maxwidth = linewrap?pixelwidth:-1;
+
+ int sx, sy, ex, ey;
+ bool selection = region(sx, sy, ex, ey);
+
+ // fix scrolly so that <cx, cy> is always on screen
+ if(cy < scrolly) scrolly = cy;
+ else
+ {
+ if(scrolly < 0) scrolly = 0;
+ int h = 0;
+ for(int i = cy; i >= scrolly; i--)
+ {
+ int width, height;
+ text_bounds(lines[i].text, width, height, maxwidth);
+ if(h + height > pixelheight) { scrolly = i+1; break; }
+ h += height;
+ }
+ }
+
+ if(selection)
+ {
+ // convert from cursor coords into pixel coords
+ int psx, psy, pex, pey;
+ text_pos(lines[sy].text, sx, psx, psy, maxwidth);
+ text_pos(lines[ey].text, ex, pex, pey, maxwidth);
+ int maxy = lines.length();
+ int h = 0;
+ for(int i = scrolly; i < maxy; i++)
+ {
+ int width, height;
+ text_bounds(lines[i].text, width, height, maxwidth);
+ if(h + height > pixelheight) { maxy = i; break; }
+ if(i == sy) psy += h;
+ if(i == ey) { pey += h; break; }
+ h += height;
+ }
+ maxy--;
+
+ if(ey >= scrolly && sy <= maxy)
+ {
+ // crop top/bottom within window
+ if(sy < scrolly) { sy = scrolly; psy = 0; psx = 0; }
+ if(ey > maxy) { ey = maxy; pey = pixelheight - FONTH; pex = pixelwidth; }
+
+ hudnotextureshader->set();
+ gle::colorub(0xA0, 0x80, 0x80);
+ gle::defvertex(2);
+ gle::begin(GL_QUADS);
+ if(psy == pey)
+ {
+ gle::attribf(x+psx, y+psy);
+ gle::attribf(x+pex, y+psy);
+ gle::attribf(x+pex, y+pey+FONTH);
+ gle::attribf(x+psx, y+pey+FONTH);
+ }
+ else
+ { gle::attribf(x+psx, y+psy);
+ gle::attribf(x+psx, y+psy+FONTH);
+ gle::attribf(x+pixelwidth, y+psy+FONTH);
+ gle::attribf(x+pixelwidth, y+psy);
+ if(pey-psy > FONTH)
+ {
+ gle::attribf(x, y+psy+FONTH);
+ gle::attribf(x+pixelwidth, y+psy+FONTH);
+ gle::attribf(x+pixelwidth, y+pey);
+ gle::attribf(x, y+pey);
+ }
+ gle::attribf(x, y+pey);
+ gle::attribf(x, y+pey+FONTH);
+ gle::attribf(x+pex, y+pey+FONTH);
+ gle::attribf(x+pex, y+pey);
+ }
+ gle::end();
+ hudshader->set();
+ }
+ }
+
+ int h = 0;
+ for(int i = scrolly; i < lines.length(); i++)
+ {
+ int width, height;
+ text_bounds(lines[i].text, width, height, maxwidth);
+ if(h + height > pixelheight) break;
+
+ draw_text(lines[i].text, x, y+h, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, hit&&(cy==i)?cx:-1, maxwidth);
+ if(linewrap && height > FONTH) // line wrap indicator
+ {
+ hudnotextureshader->set();
+ gle::colorub(0x80, 0xA0, 0x80);
+ gle::defvertex(2);
+ gle::begin(GL_TRIANGLE_STRIP);
+ gle::attribf(x, y+h+FONTH);
+ gle::attribf(x, y+h+height);
+ gle::attribf(x-FONTW/2, y+h+FONTH);
+ gle::attribf(x-FONTW/2, y+h+height);
+ gle::end();
+ hudshader->set();
+ }
+ h+=height;
+ }
+ }
+};
+
+// a 'stack' where the last is the current focused editor
+static vector <editor*> editors;
+
+static editor *currentfocus() { return editors.length() ? editors.last() : NULL; }
+
+static void readyeditors()
+{
+ loopv(editors) editors[i]->active = (editors[i]->mode==EDITORFOREVER);
+}
+
+static void flusheditors()
+{
+ loopvrev(editors) if(!editors[i]->active)
+ {
+ editor *e = editors.remove(i);
+ DELETEP(e);
+ }
+}
+
+static editor *useeditor(const char *name, int mode, bool focus, const char *initval = NULL)
+{
+ loopv(editors) if(strcmp(editors[i]->name, name) == 0)
+ {
+ editor *e = editors[i];
+ if(focus) { editors.add(e); editors.remove(i); } // re-position as last
+ e->active = true;
+ return e;
+ }
+ editor *e = new editor(name, mode, initval);
+ if(focus) editors.add(e); else editors.insert(0, e);
+ return e;
+}
+
+
+#define TEXTCOMMAND(f, s, d, body) ICOMMAND(f, s, d,\
+ editor *top = currentfocus();\
+ if(!top || identflags&IDF_OVERRIDDEN) return;\
+ body\
+)
+
+ICOMMAND(textlist, "", (), // @DEBUG return list of all the editors
+ vector<char> s;
+ loopv(editors)
+ {
+ if(i > 0) s.put(", ", 2);
+ s.put(editors[i]->name, strlen(editors[i]->name));
+ }
+ s.add('\0');
+ result(s.getbuf());
+);
+TEXTCOMMAND(textshow, "", (), // @DEBUG return the start of the buffer
+ editline line;
+ line.combinelines(top->lines);
+ result(line.text);
+ line.clear();
+);
+ICOMMAND(textfocus, "si", (char *name, int *mode), // focus on a (or create a persistent) specific editor, else returns current name
+ if(*name) useeditor(name, *mode<=0 ? EDITORFOREVER : *mode, true);
+ else if(editors.length() > 0) result(editors.last()->name);
+);
+TEXTCOMMAND(textprev, "", (), editors.insert(0, top); editors.pop();); // return to the previous editor
+TEXTCOMMAND(textmode, "i", (int *m), // (1= keep while focused, 2= keep while used in gui, 3= keep forever (i.e. until mode changes)) topmost editor, return current setting if no args
+ if(*m) top->mode = *m;
+ else intret(top->mode);
+);
+TEXTCOMMAND(textsave, "s", (char *file), // saves the topmost (filename is optional)
+ if(*file) top->setfile(path(file, true));
+ top->save();
+);
+TEXTCOMMAND(textload, "s", (char *file), // loads into the topmost editor, returns filename if no args
+ if(*file)
+ {
+ top->setfile(path(file, true));
+ top->load();
+ }
+ else if(top->filename) result(top->filename);
+);
+TEXTCOMMAND(textinit, "sss", (char *name, char *file, char *initval), // loads into named editor if no file assigned and editor has been rendered
+{
+ editor *e = NULL;
+ loopv(editors) if(!strcmp(editors[i]->name, name)) { e = editors[i]; break; }
+ if(e && e->rendered && !e->filename && *file && (e->lines.empty() || (e->lines.length() == 1 && !strcmp(e->lines[0].text, initval))))
+ {
+ e->setfile(path(file, true));
+ e->load();
+ }
+});
+
+#define PASTEBUFFER "#pastebuffer"
+
+TEXTCOMMAND(textcopy, "", (), editor *b = useeditor(PASTEBUFFER, EDITORFOREVER, false); top->copyselectionto(b););
+TEXTCOMMAND(textpaste, "", (), editor *b = useeditor(PASTEBUFFER, EDITORFOREVER, false); top->insertallfrom(b););
+TEXTCOMMAND(textmark, "i", (int *m), // (1=mark, 2=unmark), return current mark setting if no args
+ if(*m) top->mark(*m==1);
+ else intret(top->region() ? 1 : 2);
+);
+TEXTCOMMAND(textselectall, "", (), top->selectall(););
+TEXTCOMMAND(textclear, "", (), top->clear(););
+TEXTCOMMAND(textcurrentline, "", (), result(top->currentline().text););
+
+TEXTCOMMAND(textexec, "i", (int *selected), // execute script commands from the buffer (0=all, 1=selected region only)
+ char *script = *selected ? top->selectiontostring() : top->tostring();
+ execute(script);
+ delete[] script;
+);
+
diff --git a/src/engine/texture.cpp b/src/engine/texture.cpp
new file mode 100644
index 0000000..964d39d
--- /dev/null
+++ b/src/engine/texture.cpp
@@ -0,0 +1,3644 @@
+// texture.cpp: texture slot management
+
+#include "engine.h"
+#include "SDL_image.h"
+
+#ifndef SDL_IMAGE_VERSION_ATLEAST
+#define SDL_IMAGE_VERSION_ATLEAST(X, Y, Z) \
+ (SDL_VERSIONNUM(SDL_IMAGE_MAJOR_VERSION, SDL_IMAGE_MINOR_VERSION, SDL_IMAGE_PATCHLEVEL) >= SDL_VERSIONNUM(X, Y, Z))
+#endif
+
+template<int BPP> static void halvetexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst)
+{
+ for(uchar *yend = &src[sh*stride]; src < yend;)
+ {
+ for(uchar *xend = &src[sw*BPP], *xsrc = src; xsrc < xend; xsrc += 2*BPP, dst += BPP)
+ {
+ loopi(BPP) dst[i] = (uint(xsrc[i]) + uint(xsrc[i+BPP]) + uint(xsrc[stride+i]) + uint(xsrc[stride+i+BPP]))>>2;
+ }
+ src += 2*stride;
+ }
+}
+
+template<int BPP> static void shifttexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst, uint dw, uint dh)
+{
+ uint wfrac = sw/dw, hfrac = sh/dh, wshift = 0, hshift = 0;
+ while(dw<<wshift < sw) wshift++;
+ while(dh<<hshift < sh) hshift++;
+ uint tshift = wshift + hshift;
+ for(uchar *yend = &src[sh*stride]; src < yend;)
+ {
+ for(uchar *xend = &src[sw*BPP], *xsrc = src; xsrc < xend; xsrc += wfrac*BPP, dst += BPP)
+ {
+ uint t[BPP] = {0};
+ for(uchar *ycur = xsrc, *xend = &ycur[wfrac*BPP], *yend = &src[hfrac*stride];
+ ycur < yend;
+ ycur += stride, xend += stride)
+ {
+ for(uchar *xcur = ycur; xcur < xend; xcur += BPP)
+ loopi(BPP) t[i] += xcur[i];
+ }
+ loopi(BPP) dst[i] = t[i] >> tshift;
+ }
+ src += hfrac*stride;
+ }
+}
+
+template<int BPP> static void scaletexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst, uint dw, uint dh)
+{
+ uint wfrac = (sw<<12)/dw, hfrac = (sh<<12)/dh, darea = dw*dh, sarea = sw*sh;
+ int over, under;
+ for(over = 0; (darea>>over) > sarea; over++);
+ for(under = 0; (darea<<under) < sarea; under++);
+ uint cscale = clamp(under, over - 12, 12),
+ ascale = clamp(12 + under - over, 0, 24),
+ dscale = ascale + 12 - cscale,
+ area = ((ullong)darea<<ascale)/sarea;
+ dw *= wfrac;
+ dh *= hfrac;
+ for(uint y = 0; y < dh; y += hfrac)
+ {
+ const uint yn = y + hfrac - 1, yi = y>>12, h = (yn>>12) - yi, ylow = ((yn|(-int(h)>>24))&0xFFFU) + 1 - (y&0xFFFU), yhigh = (yn&0xFFFU) + 1;
+ const uchar *ysrc = &src[yi*stride];
+ for(uint x = 0; x < dw; x += wfrac, dst += BPP)
+ {
+ const uint xn = x + wfrac - 1, xi = x>>12, w = (xn>>12) - xi, xlow = ((w+0xFFFU)&0x1000U) - (x&0xFFFU), xhigh = (xn&0xFFFU) + 1;
+ const uchar *xsrc = &ysrc[xi*BPP], *xend = &xsrc[w*BPP];
+ uint t[BPP] = {0};
+ for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP)
+ loopi(BPP) t[i] += xcur[i];
+ loopi(BPP) t[i] = (ylow*(t[i] + ((xsrc[i]*xlow + xend[i]*xhigh)>>12)))>>cscale;
+ if(h)
+ {
+ xsrc += stride;
+ xend += stride;
+ for(uint hcur = h; --hcur; xsrc += stride, xend += stride)
+ {
+ uint c[BPP] = {0};
+ for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP)
+ loopi(BPP) c[i] += xcur[i];
+ loopi(BPP) t[i] += ((c[i]<<12) + xsrc[i]*xlow + xend[i]*xhigh)>>cscale;
+ }
+ uint c[BPP] = {0};
+ for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP)
+ loopi(BPP) c[i] += xcur[i];
+ loopi(BPP) t[i] += (yhigh*(c[i] + ((xsrc[i]*xlow + xend[i]*xhigh)>>12)))>>cscale;
+ }
+ loopi(BPP) dst[i] = (t[i] * area)>>dscale;
+ }
+ }
+}
+
+static void scaletexture(uchar * RESTRICT src, uint sw, uint sh, uint bpp, uint pitch, uchar * RESTRICT dst, uint dw, uint dh)
+{
+ if(sw == dw*2 && sh == dh*2)
+ {
+ switch(bpp)
+ {
+ case 1: return halvetexture<1>(src, sw, sh, pitch, dst);
+ case 2: return halvetexture<2>(src, sw, sh, pitch, dst);
+ case 3: return halvetexture<3>(src, sw, sh, pitch, dst);
+ case 4: return halvetexture<4>(src, sw, sh, pitch, dst);
+ }
+ }
+ else if(sw < dw || sh < dh || sw&(sw-1) || sh&(sh-1) || dw&(dw-1) || dh&(dh-1))
+ {
+ switch(bpp)
+ {
+ case 1: return scaletexture<1>(src, sw, sh, pitch, dst, dw, dh);
+ case 2: return scaletexture<2>(src, sw, sh, pitch, dst, dw, dh);
+ case 3: return scaletexture<3>(src, sw, sh, pitch, dst, dw, dh);
+ case 4: return scaletexture<4>(src, sw, sh, pitch, dst, dw, dh);
+ }
+ }
+ else
+ {
+ switch(bpp)
+ {
+ case 1: return shifttexture<1>(src, sw, sh, pitch, dst, dw, dh);
+ case 2: return shifttexture<2>(src, sw, sh, pitch, dst, dw, dh);
+ case 3: return shifttexture<3>(src, sw, sh, pitch, dst, dw, dh);
+ case 4: return shifttexture<4>(src, sw, sh, pitch, dst, dw, dh);
+ }
+ }
+}
+
+static void reorientnormals(uchar * RESTRICT src, int sw, int sh, int bpp, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy)
+{
+ int stridex = bpp, stridey = bpp;
+ if(swapxy) stridex *= sh; else stridey *= sw;
+ if(flipx) { dst += (sw-1)*stridex; stridex = -stridex; }
+ if(flipy) { dst += (sh-1)*stridey; stridey = -stridey; }
+ uchar *srcrow = src;
+ loopi(sh)
+ {
+ for(uchar *curdst = dst, *src = srcrow, *end = &srcrow[sw*bpp]; src < end;)
+ {
+ uchar nx = *src++, ny = *src++;
+ if(flipx) nx = 255-nx;
+ if(flipy) ny = 255-ny;
+ if(swapxy) swap(nx, ny);
+ curdst[0] = nx;
+ curdst[1] = ny;
+ curdst[2] = *src++;
+ if(bpp > 3) curdst[3] = *src++;
+ curdst += stridex;
+ }
+ srcrow += stride;
+ dst += stridey;
+ }
+}
+
+template<int BPP>
+static inline void reorienttexture(uchar * RESTRICT src, int sw, int sh, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy)
+{
+ int stridex = BPP, stridey = BPP;
+ if(swapxy) stridex *= sh; else stridey *= sw;
+ if(flipx) { dst += (sw-1)*stridex; stridex = -stridex; }
+ if(flipy) { dst += (sh-1)*stridey; stridey = -stridey; }
+ uchar *srcrow = src;
+ loopi(sh)
+ {
+ for(uchar *curdst = dst, *src = srcrow, *end = &srcrow[sw*BPP]; src < end;)
+ {
+ loopk(BPP) curdst[k] = *src++;
+ curdst += stridex;
+ }
+ srcrow += stride;
+ dst += stridey;
+ }
+}
+
+static void reorienttexture(uchar * RESTRICT src, int sw, int sh, int bpp, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy)
+{
+ switch(bpp)
+ {
+ case 1: return reorienttexture<1>(src, sw, sh, stride, dst, flipx, flipy, swapxy);
+ case 2: return reorienttexture<2>(src, sw, sh, stride, dst, flipx, flipy, swapxy);
+ case 3: return reorienttexture<3>(src, sw, sh, stride, dst, flipx, flipy, swapxy);
+ case 4: return reorienttexture<4>(src, sw, sh, stride, dst, flipx, flipy, swapxy);
+ }
+}
+
+static void reorients3tc(GLenum format, int blocksize, int w, int h, uchar *src, uchar *dst, bool flipx, bool flipy, bool swapxy, bool normals = false)
+{
+ int bx1 = 0, by1 = 0, bx2 = min(w, 4), by2 = min(h, 4), bw = (w+3)/4, bh = (h+3)/4, stridex = blocksize, stridey = blocksize;
+ if(swapxy) stridex *= bw; else stridey *= bh;
+ if(flipx) { dst += (bw-1)*stridex; stridex = -stridex; bx1 += 4-bx2; bx2 = 4; }
+ if(flipy) { dst += (bh-1)*stridey; stridey = -stridey; by1 += 4-by2; by2 = 4; }
+ loopi(bh)
+ {
+ for(uchar *curdst = dst, *end = &src[bw*blocksize]; src < end; src += blocksize, curdst += stridex)
+ {
+ if(format == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT)
+ {
+ ullong salpha = lilswap(*(const ullong *)src), dalpha = 0;
+ uint xmask = flipx ? 15 : 0, ymask = flipy ? 15 : 0, xshift = 2, yshift = 4;
+ if(swapxy) swap(xshift, yshift);
+ for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
+ {
+ dalpha |= ((salpha&15) << (((xmask^x)<<xshift) + ((ymask^y)<<yshift)));
+ salpha >>= 4;
+ }
+ *(ullong *)curdst = lilswap(dalpha);
+ src += 8;
+ curdst += 8;
+ }
+ else if(format == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT)
+ {
+ uchar alpha1 = src[0], alpha2 = src[1];
+ ullong salpha = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16), dalpha = 0;
+ uint xmask = flipx ? 7 : 0, ymask = flipy ? 7 : 0, xshift = 0, yshift = 2;
+ if(swapxy) swap(xshift, yshift);
+ for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
+ {
+ dalpha |= ((salpha&7) << (3*((xmask^x)<<xshift) + ((ymask^y)<<yshift)));
+ salpha >>= 3;
+ }
+ curdst[0] = alpha1;
+ curdst[1] = alpha2;
+ *(ushort *)&curdst[2] = lilswap(ushort(dalpha));
+ *(ushort *)&curdst[4] = lilswap(ushort(dalpha>>16));
+ *(ushort *)&curdst[6] = lilswap(ushort(dalpha>>32));
+ src += 8;
+ curdst += 8;
+ }
+
+ ushort color1 = lilswap(*(const ushort *)src), color2 = lilswap(*(const ushort *)&src[2]);
+ uint sbits = lilswap(*(const uint *)&src[4]);
+ if(normals)
+ {
+ ushort ncolor1 = color1, ncolor2 = color2;
+ if(flipx)
+ {
+ ncolor1 = (ncolor1 & ~0xF800) | (0xF800 - (ncolor1 & 0xF800));
+ ncolor2 = (ncolor2 & ~0xF800) | (0xF800 - (ncolor2 & 0xF800));
+ }
+ if(flipy)
+ {
+ ncolor1 = (ncolor1 & ~0x7E0) | (0x7E0 - (ncolor1 & 0x7E0));
+ ncolor2 = (ncolor2 & ~0x7E0) | (0x7E0 - (ncolor2 & 0x7E0));
+ }
+ if(swapxy)
+ {
+ ncolor1 = (ncolor1 & 0x1F) | (((((ncolor1 >> 11) & 0x1F) * 0x3F) / 0x1F) << 5) | (((((ncolor1 >> 5) & 0x3F) * 0x1F) / 0x3F) << 11);
+ ncolor2 = (ncolor2 & 0x1F) | (((((ncolor2 >> 11) & 0x1F) * 0x3F) / 0x1F) << 5) | (((((ncolor2 >> 5) & 0x3F) * 0x1F) / 0x3F) << 11);
+ }
+ if(color1 <= color2 && ncolor1 > ncolor2) { color1 = ncolor2; color2 = ncolor1; }
+ else { color1 = ncolor1; color2 = ncolor2; }
+ }
+ uint dbits = 0, xmask = flipx ? 3 : 0, ymask = flipy ? 3 : 0, xshift = 1, yshift = 3;
+ if(swapxy) swap(xshift, yshift);
+ for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
+ {
+ dbits |= ((sbits&3) << (((xmask^x)<<xshift) + ((ymask^y)<<yshift)));
+ sbits >>= 2;
+ }
+ *(ushort *)curdst = lilswap(color1);
+ *(ushort *)&curdst[2] = lilswap(color2);
+ *(uint *)&curdst[4] = lilswap(dbits);
+
+ if(blocksize > 8) { src -= 8; curdst -= 8; }
+ }
+ dst += stridey;
+ }
+}
+
+static void reorientrgtc(GLenum format, int blocksize, int w, int h, uchar *src, uchar *dst, bool flipx, bool flipy, bool swapxy)
+{
+ int bx1 = 0, by1 = 0, bx2 = min(w, 4), by2 = min(h, 4), bw = (w+3)/4, bh = (h+3)/4, stridex = blocksize, stridey = blocksize;
+ if(swapxy) stridex *= bw; else stridey *= bh;
+ if(flipx) { dst += (bw-1)*stridex; stridex = -stridex; bx1 += 4-bx2; bx2 = 4; }
+ if(flipy) { dst += (bh-1)*stridey; stridey = -stridey; by1 += 4-by2; by2 = 4; }
+ stridex -= blocksize;
+ loopi(bh)
+ {
+ for(uchar *curdst = dst, *end = &src[bw*blocksize]; src < end; curdst += stridex)
+ {
+ loopj(blocksize/8)
+ {
+ uchar val1 = src[0], val2 = src[1];
+ ullong sval = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4] )<< 16), dval = 0;
+ uint xmask = flipx ? 7 : 0, ymask = flipy ? 7 : 0, xshift = 0, yshift = 2;
+ if(swapxy) swap(xshift, yshift);
+ for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
+ {
+ dval |= ((sval&7) << (3*((xmask^x)<<xshift) + ((ymask^y)<<yshift)));
+ sval >>= 3;
+ }
+ curdst[0] = val1;
+ curdst[1] = val2;
+ *(ushort *)&curdst[2] = lilswap(ushort(dval));
+ *(ushort *)&curdst[4] = lilswap(ushort(dval>>16));
+ *(ushort *)&curdst[6] = lilswap(ushort(dval>>32));
+ src += 8;
+ curdst += 8;
+ }
+ }
+ dst += stridey;
+ }
+}
+
+#define writetex(t, body) do \
+ { \
+ uchar *dstrow = t.data; \
+ loop(y, t.h) \
+ { \
+ for(uchar *dst = dstrow, *end = &dstrow[t.w*t.bpp]; dst < end; dst += t.bpp) \
+ { \
+ body; \
+ } \
+ dstrow += t.pitch; \
+ } \
+ } while(0)
+
+#define readwritetex(t, s, body) do \
+ { \
+ uchar *dstrow = t.data, *srcrow = s.data; \
+ loop(y, t.h) \
+ { \
+ for(uchar *dst = dstrow, *src = srcrow, *end = &srcrow[s.w*s.bpp]; src < end; dst += t.bpp, src += s.bpp) \
+ { \
+ body; \
+ } \
+ dstrow += t.pitch; \
+ srcrow += s.pitch; \
+ } \
+ } while(0)
+
+#define read2writetex(t, s1, src1, s2, src2, body) do \
+ { \
+ uchar *dstrow = t.data, *src1row = s1.data, *src2row = s2.data; \
+ loop(y, t.h) \
+ { \
+ for(uchar *dst = dstrow, *end = &dstrow[t.w*t.bpp], *src1 = src1row, *src2 = src2row; dst < end; dst += t.bpp, src1 += s1.bpp, src2 += s2.bpp) \
+ { \
+ body; \
+ } \
+ dstrow += t.pitch; \
+ src1row += s1.pitch; \
+ src2row += s2.pitch; \
+ } \
+ } while(0)
+
+#define readwritergbtex(t, s, body) \
+ { \
+ if(t.bpp >= 3) readwritetex(t, s, body); \
+ else \
+ { \
+ ImageData rgb(t.w, t.h, 3); \
+ read2writetex(rgb, t, orig, s, src, { dst[0] = dst[1] = dst[2] = orig[0]; body; }); \
+ t.replace(rgb); \
+ } \
+ }
+
+void forcergbimage(ImageData &s)
+{
+ if(s.bpp >= 3) return;
+ ImageData d(s.w, s.h, 3);
+ readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; });
+ s.replace(d);
+}
+
+#define readwritergbatex(t, s, body) \
+ { \
+ if(t.bpp >= 4) { readwritetex(t, s, body); } \
+ else \
+ { \
+ ImageData rgba(t.w, t.h, 4); \
+ if(t.bpp==3) read2writetex(rgba, t, orig, s, src, { dst[0] = orig[0]; dst[1] = orig[1]; dst[2] = orig[2]; body; }); \
+ else read2writetex(rgba, t, orig, s, src, { dst[0] = dst[1] = dst[2] = orig[0]; body; }); \
+ t.replace(rgba); \
+ } \
+ }
+
+void forcergbaimage(ImageData &s)
+{
+ if(s.bpp >= 4) return;
+ ImageData d(s.w, s.h, 4);
+ if(s.bpp==3) readwritetex(d, s, { dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; });
+ else readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; });
+ s.replace(d);
+}
+
+void swizzleimage(ImageData &s)
+{
+ if(s.bpp==2)
+ {
+ ImageData d(s.w, s.h, 4);
+ readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; dst[3] = src[1]; });
+ s.replace(d);
+ }
+ else if(s.bpp==1)
+ {
+ ImageData d(s.w, s.h, 3);
+ readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; });
+ s.replace(d);
+ }
+}
+
+void texreorient(ImageData &s, bool flipx, bool flipy, bool swapxy, int type = TEX_DIFFUSE)
+{
+ ImageData d(swapxy ? s.h : s.w, swapxy ? s.w : s.h, s.bpp, s.levels, s.align, s.compressed);
+ switch(s.compressed)
+ {
+ case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+ {
+ uchar *dst = d.data, *src = s.data;
+ loopi(s.levels)
+ {
+ reorients3tc(s.compressed, s.bpp, max(s.w>>i, 1), max(s.h>>i, 1), src, dst, flipx, flipy, swapxy, type==TEX_NORMAL);
+ src += s.calclevelsize(i);
+ dst += d.calclevelsize(i);
+ }
+ break;
+ }
+ case GL_COMPRESSED_RED_RGTC1:
+ case GL_COMPRESSED_RG_RGTC2:
+ case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+ case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+ {
+ uchar *dst = d.data, *src = s.data;
+ loopi(s.levels)
+ {
+ reorientrgtc(s.compressed, s.bpp, max(s.w>>i, 1), max(s.h>>i, 1), src, dst, flipx, flipy, swapxy);
+ src += s.calclevelsize(i);
+ dst += d.calclevelsize(i);
+ }
+ break;
+ }
+ default:
+ if(type==TEX_NORMAL && s.bpp >= 3) reorientnormals(s.data, s.w, s.h, s.bpp, s.pitch, d.data, flipx, flipy, swapxy);
+ else reorienttexture(s.data, s.w, s.h, s.bpp, s.pitch, d.data, flipx, flipy, swapxy);
+ break;
+ }
+ s.replace(d);
+}
+
+extern const texrotation texrotations[8] =
+{
+ { false, false, false }, // 0: default
+ { false, true, true }, // 1: 90 degrees
+ { true, true, false }, // 2: 180 degrees
+ { true, false, true }, // 3: 270 degrees
+ { true, false, false }, // 4: flip X
+ { false, true, false }, // 5: flip Y
+ { false, false, true }, // 6: transpose
+ { true, true, true }, // 7: flipped transpose
+};
+
+void texrotate(ImageData &s, int numrots, int type = TEX_DIFFUSE)
+{
+ if(numrots>=1 && numrots<=7)
+ {
+ const texrotation &r = texrotations[numrots];
+ texreorient(s, r.flipx, r.flipy, r.swapxy, type);
+ }
+}
+
+void texoffset(ImageData &s, int xoffset, int yoffset)
+{
+ xoffset = max(xoffset, 0);
+ xoffset %= s.w;
+ yoffset = max(yoffset, 0);
+ yoffset %= s.h;
+ if(!xoffset && !yoffset) return;
+ ImageData d(s.w, s.h, s.bpp);
+ uchar *src = s.data;
+ loop(y, s.h)
+ {
+ uchar *dst = (uchar *)d.data+((y+yoffset)%d.h)*d.pitch;
+ memcpy(dst+xoffset*s.bpp, src, (s.w-xoffset)*s.bpp);
+ memcpy(dst, src+(s.w-xoffset)*s.bpp, xoffset*s.bpp);
+ src += s.pitch;
+ }
+ s.replace(d);
+}
+
+void texmad(ImageData &s, const vec &mul, const vec &add)
+{
+ if(s.bpp < 3 && (mul.x != mul.y || mul.y != mul.z || add.x != add.y || add.y != add.z))
+ swizzleimage(s);
+ int maxk = min(int(s.bpp), 3);
+ writetex(s,
+ loopk(maxk) dst[k] = uchar(clamp(dst[k]*mul[k] + 255*add[k], 0.0f, 255.0f));
+ );
+}
+
+void texcolorify(ImageData &s, const vec &color, vec weights)
+{
+ if(s.bpp < 3) return;
+ if(weights.iszero()) weights = vec(0.21f, 0.72f, 0.07f);
+ writetex(s,
+ float lum = dst[0]*weights.x + dst[1]*weights.y + dst[2]*weights.z;
+ loopk(3) dst[k] = uchar(clamp(lum*color[k], 0.0f, 255.0f));
+ );
+}
+
+void texcolormask(ImageData &s, const vec &color1, const vec &color2)
+{
+ if(s.bpp < 4) return;
+ ImageData d(s.w, s.h, 3);
+ readwritetex(d, s,
+ vec color;
+ color.lerp(color2, color1, src[3]/255.0f);
+ loopk(3) dst[k] = uchar(clamp(color[k]*src[k], 0.0f, 255.0f));
+ );
+ s.replace(d);
+}
+
+void texdup(ImageData &s, int srcchan, int dstchan)
+{
+ if(srcchan==dstchan || max(srcchan, dstchan) >= s.bpp) return;
+ writetex(s, dst[dstchan] = dst[srcchan]);
+}
+
+void texmix(ImageData &s, int c1, int c2, int c3, int c4)
+{
+ int numchans = c1 < 0 ? 0 : (c2 < 0 ? 1 : (c3 < 0 ? 2 : (c4 < 0 ? 3 : 4)));
+ if(numchans <= 0) return;
+ ImageData d(s.w, s.h, numchans);
+ readwritetex(d, s,
+ switch(numchans)
+ {
+ case 4: dst[3] = src[c4];
+ case 3: dst[2] = src[c3];
+ case 2: dst[1] = src[c2];
+ case 1: dst[0] = src[c1];
+ }
+ );
+ s.replace(d);
+}
+
+void texgrey(ImageData &s)
+{
+ if(s.bpp <= 2) return;
+ ImageData d(s.w, s.h, s.bpp >= 4 ? 2 : 1);
+ if(s.bpp >= 4)
+ {
+ readwritetex(d, s,
+ dst[0] = src[0];
+ dst[1] = src[3];
+ );
+ }
+ else
+ {
+ readwritetex(d, s, dst[0] = src[0]);
+ }
+ s.replace(d);
+}
+
+void texpremul(ImageData &s)
+{
+ switch(s.bpp)
+ {
+ case 2:
+ writetex(s,
+ dst[0] = uchar((uint(dst[0])*uint(dst[1]))/255);
+ );
+ break;
+ case 4:
+ writetex(s,
+ uint alpha = dst[3];
+ dst[0] = uchar((uint(dst[0])*alpha)/255);
+ dst[1] = uchar((uint(dst[1])*alpha)/255);
+ dst[2] = uchar((uint(dst[2])*alpha)/255);
+ );
+ break;
+ }
+}
+
+void texagrad(ImageData &s, float x2, float y2, float x1, float y1)
+{
+ if(s.bpp != 2 && s.bpp != 4) return;
+ y1 = 1 - y1;
+ y2 = 1 - y2;
+ float minx = 1, miny = 1, maxx = 1, maxy = 1;
+ if(x1 != x2)
+ {
+ minx = (0 - x1) / (x2 - x1);
+ maxx = (1 - x1) / (x2 - x1);
+ }
+ if(y1 != y2)
+ {
+ miny = (0 - y1) / (y2 - y1);
+ maxy = (1 - y1) / (y2 - y1);
+ }
+ float dx = (maxx - minx)/max(s.w-1, 1),
+ dy = (maxy - miny)/max(s.h-1, 1),
+ cury = miny;
+ for(uchar *dstrow = s.data + s.bpp - 1, *endrow = dstrow + s.h*s.pitch; dstrow < endrow; dstrow += s.pitch)
+ {
+ float curx = minx;
+ for(uchar *dst = dstrow, *end = &dstrow[s.w*s.bpp]; dst < end; dst += s.bpp)
+ {
+ dst[0] = uchar(dst[0]*clamp(curx, 0.0f, 1.0f)*clamp(cury, 0.0f, 1.0f));
+ curx += dx;
+ }
+ cury += dy;
+ }
+}
+
+VAR(hwtexsize, 1, 0, 0);
+VAR(hwcubetexsize, 1, 0, 0);
+VAR(hwmaxaniso, 1, 0, 0);
+VARFP(maxtexsize, 0, 0, 1<<12, initwarning("texture quality", INIT_LOAD));
+VARFP(reducefilter, 0, 1, 1, initwarning("texture quality", INIT_LOAD));
+VARFP(texreduce, 0, 0, 12, initwarning("texture quality", INIT_LOAD));
+VARFP(texcompress, 0, 1<<10, 1<<12, initwarning("texture quality", INIT_LOAD));
+VARFP(texcompressquality, -1, -1, 1, setuptexcompress());
+VARFP(trilinear, 0, 1, 1, initwarning("texture filtering", INIT_LOAD));
+VARFP(bilinear, 0, 1, 1, initwarning("texture filtering", INIT_LOAD));
+VARFP(aniso, 0, 0, 16, initwarning("texture filtering", INIT_LOAD));
+
+extern int usetexcompress;
+
+void setuptexcompress()
+{
+ if(!usetexcompress) return;
+
+ GLenum hint = GL_DONT_CARE;
+ switch(texcompressquality)
+ {
+ case 1: hint = GL_NICEST; break;
+ case 0: hint = GL_FASTEST; break;
+ }
+ glHint(GL_TEXTURE_COMPRESSION_HINT, hint);
+}
+
+GLenum compressedformat(GLenum format, int w, int h, int force = 0)
+{
+ if(usetexcompress && texcompress && force >= 0 && (force || max(w, h) >= texcompress)) switch(format)
+ {
+ case GL_RGB5:
+ case GL_RGB8:
+ case GL_RGB: return usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB;
+ case GL_RGB5_A1: return usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGBA;
+ case GL_RGBA: return usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA;
+ case GL_RED:
+ case GL_R8: return hasRGTC ? (usetexcompress > 1 ? GL_COMPRESSED_RED_RGTC1 : GL_COMPRESSED_RED) : (usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB);
+ case GL_RG:
+ case GL_RG8: return hasRGTC ? (usetexcompress > 1 ? GL_COMPRESSED_RG_RGTC2 : GL_COMPRESSED_RG) : (usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA);
+ case GL_LUMINANCE:
+ case GL_LUMINANCE8: return hasLATC ? (usetexcompress > 1 ? GL_COMPRESSED_LUMINANCE_LATC1_EXT : GL_COMPRESSED_LUMINANCE) : (usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB);
+ case GL_LUMINANCE_ALPHA:
+ case GL_LUMINANCE8_ALPHA8: return hasLATC ? (usetexcompress > 1 ? GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT : GL_COMPRESSED_LUMINANCE_ALPHA) : (usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA);
+ }
+ return format;
+}
+
+GLenum uncompressedformat(GLenum format)
+{
+ switch(format)
+ {
+ case GL_COMPRESSED_ALPHA:
+ return GL_ALPHA;
+ case GL_COMPRESSED_LUMINANCE:
+ case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+ return GL_LUMINANCE;
+ case GL_COMPRESSED_LUMINANCE_ALPHA:
+ case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+ return GL_LUMINANCE_ALPHA;
+ case GL_COMPRESSED_RED:
+ case GL_COMPRESSED_RED_RGTC1:
+ return GL_RED;
+ case GL_COMPRESSED_RG:
+ case GL_COMPRESSED_RG_RGTC2:
+ return GL_RG;
+ case GL_COMPRESSED_RGB:
+ case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+ return GL_RGB;
+ case GL_COMPRESSED_RGBA:
+ case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+ return GL_RGBA;
+ }
+ return GL_FALSE;
+}
+
+int formatsize(GLenum format)
+{
+ switch(format)
+ {
+ case GL_RED:
+ case GL_LUMINANCE:
+ case GL_ALPHA: return 1;
+ case GL_RG:
+ case GL_LUMINANCE_ALPHA: return 2;
+ case GL_RGB: return 3;
+ case GL_RGBA: return 4;
+ default: return 4;
+ }
+}
+
+VARFP(usenp2, 0, 0, 1, initwarning("texture quality", INIT_LOAD));
+
+void resizetexture(int w, int h, bool mipmap, bool canreduce, GLenum target, int compress, int &tw, int &th)
+{
+ int hwlimit = target==GL_TEXTURE_CUBE_MAP ? hwcubetexsize : hwtexsize,
+ sizelimit = mipmap && maxtexsize ? min(maxtexsize, hwlimit) : hwlimit;
+ if(compress > 0 && !usetexcompress)
+ {
+ w = max(w/compress, 1);
+ h = max(h/compress, 1);
+ }
+ if(canreduce && texreduce)
+ {
+ w = max(w>>texreduce, 1);
+ h = max(h>>texreduce, 1);
+ }
+ w = min(w, sizelimit);
+ h = min(h, sizelimit);
+ if(!usenp2 && (w&(w-1) || h&(h-1)))
+ {
+ tw = th = 1;
+ while(tw < w) tw *= 2;
+ while(th < h) th *= 2;
+ if(w < tw - tw/4) tw /= 2;
+ if(h < th - th/4) th /= 2;
+ }
+ else
+ {
+ tw = w;
+ th = h;
+ }
+}
+
+static GLuint mipmapfbo[2] = { 0, 0 };
+
+void cleanupmipmaps()
+{
+ if(mipmapfbo[0]) { glDeleteFramebuffers_(2, mipmapfbo); memset(mipmapfbo, 0, sizeof(mipmapfbo)); }
+}
+
+VARFP(gpumipmap, 0, 0, 1, cleanupmipmaps());
+
+void uploadtexture(int tnum, GLenum target, GLenum internal, int tw, int th, GLenum format, GLenum type, void *pixels, int pw, int ph, int pitch, bool mipmap)
+{
+ int bpp = formatsize(format), row = 0, rowalign = 0;
+ if(!pitch) pitch = pw*bpp;
+ uchar *buf = NULL;
+ if(pw!=tw || ph!=th)
+ {
+ buf = new uchar[tw*th*bpp];
+ scaletexture((uchar *)pixels, pw, ph, bpp, pitch, buf, tw, th);
+ }
+ else if(tw*bpp != pitch)
+ {
+ row = pitch/bpp;
+ rowalign = texalign(pixels, pitch, 1);
+ while(rowalign > 0 && ((row*bpp + rowalign - 1)/rowalign)*rowalign != pitch) rowalign >>= 1;
+ if(!rowalign)
+ {
+ row = 0;
+ buf = new uchar[tw*th*bpp];
+ loopi(th) memcpy(&buf[i*tw*bpp], &((uchar *)pixels)[i*pitch], tw*bpp);
+ }
+ }
+ bool shouldgpumipmap = pixels && mipmap && max(tw, th) > 1 && gpumipmap && hasFBB && !uncompressedformat(internal);
+ for(int level = 0, align = 0, mw = tw, mh = th;; level++)
+ {
+ uchar *src = buf ? buf : (uchar *)pixels;
+ if(buf) pitch = mw*bpp;
+ int srcalign = row > 0 ? rowalign : texalign(src, pitch, 1);
+ if(align != srcalign) glPixelStorei(GL_UNPACK_ALIGNMENT, align = srcalign);
+ if(row > 0) glPixelStorei(GL_UNPACK_ROW_LENGTH, row);
+ glTexImage2D(target, level, internal, mw, mh, 0, format, type, src);
+ if(row > 0) glPixelStorei(GL_UNPACK_ROW_LENGTH, row = 0);
+ if(!mipmap || shouldgpumipmap || max(mw, mh) <= 1) break;
+ int srcw = mw, srch = mh;
+ if(mw > 1) mw /= 2;
+ if(mh > 1) mh /= 2;
+ if(src)
+ {
+ if(!buf) buf = new uchar[mw*mh*bpp];
+ scaletexture(src, srcw, srch, bpp, pitch, buf, mw, mh);
+ }
+ }
+ if(buf) delete[] buf;
+ if(shouldgpumipmap)
+ {
+ GLint fbo = 0;
+ if(!inbetweenframes || drawtex) glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);
+ for(int level = 1, mw = tw, mh = th; max(mw, mh) > 1; level++)
+ {
+ if(mw > 1) mw /= 2;
+ if(mh > 1) mh /= 2;
+ glTexImage2D(target, level, internal, mw, mh, 0, format, type, NULL);
+ }
+ if(!mipmapfbo[0]) glGenFramebuffers_(2, mipmapfbo);
+ glBindFramebuffer_(GL_READ_FRAMEBUFFER, mipmapfbo[0]);
+ glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, mipmapfbo[1]);
+ for(int level = 1, mw = tw, mh = th; max(mw, mh) > 1; level++)
+ {
+ int srcw = mw, srch = mh;
+ if(mw > 1) mw /= 2;
+ if(mh > 1) mh /= 2;
+ glFramebufferTexture2D_(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, tnum, level - 1);
+ glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, tnum, level);
+ glBlitFramebuffer_(0, 0, srcw, srch, 0, 0, mw, mh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
+ }
+ glFramebufferTexture2D_(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, 0, 0);
+ glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, 0, 0);
+ glBindFramebuffer_(GL_FRAMEBUFFER, fbo);
+ }
+}
+
+void uploadcompressedtexture(GLenum target, GLenum subtarget, GLenum format, int w, int h, uchar *data, int align, int blocksize, int levels, bool mipmap)
+{
+ int hwlimit = target==GL_TEXTURE_CUBE_MAP ? hwcubetexsize : hwtexsize,
+ sizelimit = levels > 1 && maxtexsize ? min(maxtexsize, hwlimit) : hwlimit;
+ int level = 0;
+ loopi(levels)
+ {
+ int size = ((w + align-1)/align) * ((h + align-1)/align) * blocksize;
+ if(w <= sizelimit && h <= sizelimit)
+ {
+ glCompressedTexImage2D_(subtarget, level, format, w, h, 0, size, data);
+ level++;
+ if(!mipmap) break;
+ }
+ if(max(w, h) <= 1) break;
+ if(w > 1) w /= 2;
+ if(h > 1) h /= 2;
+ data += size;
+ }
+}
+
+GLenum textarget(GLenum subtarget)
+{
+ switch(subtarget)
+ {
+ case GL_TEXTURE_CUBE_MAP_POSITIVE_X:
+ case GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
+ case GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
+ case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
+ case GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
+ case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
+ return GL_TEXTURE_CUBE_MAP;
+ }
+ return subtarget;
+}
+
+const GLint *swizzlemask(GLenum format)
+{
+ static const GLint luminance[4] = { GL_RED, GL_RED, GL_RED, GL_ONE };
+ static const GLint luminancealpha[4] = { GL_RED, GL_RED, GL_RED, GL_GREEN };
+ switch(format)
+ {
+ case GL_RED: return luminance;
+ case GL_RG: return luminancealpha;
+ }
+ return NULL;
+}
+
+void setuptexparameters(int tnum, void *pixels, int clamp, int filter, GLenum format, GLenum target, bool swizzle)
+{
+ glBindTexture(target, tnum);
+ glTexParameteri(target, GL_TEXTURE_WRAP_S, clamp&1 ? GL_CLAMP_TO_EDGE : (clamp&0x100 ? GL_MIRRORED_REPEAT : GL_REPEAT));
+ glTexParameteri(target, GL_TEXTURE_WRAP_T, clamp&2 ? GL_CLAMP_TO_EDGE : (clamp&0x200 ? GL_MIRRORED_REPEAT : GL_REPEAT));
+ if(target==GL_TEXTURE_2D && hasAF && min(aniso, hwmaxaniso) > 0 && filter > 1) glTexParameteri(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, min(aniso, hwmaxaniso));
+ glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filter && bilinear ? GL_LINEAR : GL_NEAREST);
+ glTexParameteri(target, GL_TEXTURE_MIN_FILTER,
+ filter > 1 ?
+ (trilinear ?
+ (bilinear ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_LINEAR) :
+ (bilinear ? GL_LINEAR_MIPMAP_NEAREST : GL_NEAREST_MIPMAP_NEAREST)) :
+ (filter && bilinear ? GL_LINEAR : GL_NEAREST));
+ if(swizzle && hasTRG && hasTSW)
+ {
+ const GLint *mask = swizzlemask(format);
+ if(mask) glTexParameteriv(target, GL_TEXTURE_SWIZZLE_RGBA, mask);
+ }
+}
+
+static GLenum textype(GLenum &component, GLenum &format)
+{
+ GLenum type = GL_UNSIGNED_BYTE;
+ switch(component)
+ {
+ case GL_R16F:
+ case GL_R32F:
+ if(!format) format = GL_RED;
+ type = GL_FLOAT;
+ break;
+
+ case GL_RG16F:
+ case GL_RG32F:
+ if(!format) format = GL_RG;
+ type = GL_FLOAT;
+ break;
+
+ case GL_RGB16F:
+ case GL_RGB32F:
+ if(!format) format = GL_RGB;
+ type = GL_FLOAT;
+ break;
+
+ case GL_RGBA16F:
+ case GL_RGBA32F:
+ if(!format) format = GL_RGBA;
+ type = GL_FLOAT;
+ break;
+
+ case GL_DEPTH_COMPONENT16:
+ case GL_DEPTH_COMPONENT24:
+ case GL_DEPTH_COMPONENT32:
+ if(!format) format = GL_DEPTH_COMPONENT;
+ break;
+
+ case GL_RGB5:
+ case GL_RGB8:
+ case GL_RGB10:
+ case GL_RGB16:
+ case GL_COMPRESSED_RGB:
+ case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+ if(!format) format = GL_RGB;
+ break;
+
+ case GL_RGB5_A1:
+ case GL_RGBA8:
+ case GL_RGB10_A2:
+ case GL_RGBA16:
+ case GL_COMPRESSED_RGBA:
+ case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+ if(!format) format = GL_RGBA;
+ break;
+
+ case GL_DEPTH_STENCIL:
+ case GL_DEPTH24_STENCIL8:
+ if(!format) format = GL_DEPTH_STENCIL;
+ type = GL_UNSIGNED_INT_24_8;
+ break;
+
+ case GL_R8:
+ case GL_R16:
+ case GL_COMPRESSED_RED:
+ case GL_COMPRESSED_RED_RGTC1:
+ if(!format) format = GL_RED;
+ break;
+
+ case GL_RG8:
+ case GL_RG16:
+ case GL_COMPRESSED_RG:
+ case GL_COMPRESSED_RG_RGTC2:
+ if(!format) format = GL_RG;
+ break;
+
+ case GL_LUMINANCE8:
+ case GL_LUMINANCE16:
+ case GL_COMPRESSED_LUMINANCE:
+ case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+ if(!format) format = GL_LUMINANCE;
+ break;
+
+ case GL_LUMINANCE8_ALPHA8:
+ case GL_LUMINANCE16_ALPHA16:
+ case GL_COMPRESSED_LUMINANCE_ALPHA:
+ case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+ if(!format) format = GL_LUMINANCE_ALPHA;
+ break;
+
+ case GL_ALPHA8:
+ case GL_ALPHA16:
+ case GL_COMPRESSED_ALPHA:
+ if(!format) format = GL_ALPHA;
+ break;
+ }
+ if(!format) format = component;
+ return type;
+}
+
+void createtexture(int tnum, int w, int h, void *pixels, int clamp, int filter, GLenum component, GLenum subtarget, int pw, int ph, int pitch, bool resize, GLenum format, bool swizzle)
+{
+ GLenum target = textarget(subtarget), type = textype(component, format);
+ if(filter >= 0 && clamp >= 0) setuptexparameters(tnum, pixels, clamp, filter, format, target, swizzle);
+ if(!pw) pw = w;
+ if(!ph) ph = h;
+ int tw = w, th = h;
+ bool mipmap = filter > 1 && pixels;
+ if(resize && pixels)
+ {
+ resizetexture(w, h, mipmap, false, target, 0, tw, th);
+ if(mipmap) component = compressedformat(component, tw, th);
+ }
+ uploadtexture(tnum, subtarget, component, tw, th, format, type, pixels, pw, ph, pitch, mipmap);
+}
+
+void createcompressedtexture(int tnum, int w, int h, uchar *data, int align, int blocksize, int levels, int clamp, int filter, GLenum format, GLenum subtarget, bool swizzle = false)
+{
+ GLenum target = textarget(subtarget);
+ if(filter >= 0 && clamp >= 0) setuptexparameters(tnum, data, clamp, filter, format, target);
+ uploadcompressedtexture(target, subtarget, format, w, h, data, align, blocksize, levels, filter > 1);
+}
+
+hashnameset<Texture> textures;
+
+Texture *notexture = NULL; // used as default, ensured to be loaded
+
+static GLenum texformat(int bpp, bool swizzle = false)
+{
+ switch(bpp)
+ {
+ case 1: return hasTRG && (hasTSW || !glcompat || !swizzle) ? GL_RED : GL_LUMINANCE;
+ case 2: return hasTRG && (hasTSW || !glcompat || !swizzle) ? GL_RG : GL_LUMINANCE_ALPHA;
+ case 3: return GL_RGB;
+ case 4: return GL_RGBA;
+ default: return 0;
+ }
+}
+
+static bool alphaformat(GLenum format)
+{
+ switch(format)
+ {
+ case GL_ALPHA:
+ case GL_LUMINANCE_ALPHA:
+ case GL_RG:
+ case GL_RGBA:
+ return true;
+ default:
+ return false;
+ }
+}
+
+int texalign(const void *data, int w, int bpp)
+{
+ int stride = w*bpp;
+ if(stride&1) return 1;
+ if(stride&2) return 2;
+ return 4;
+}
+
+static Texture *newtexture(Texture *t, const char *rname, ImageData &s, int clamp = 0, bool mipit = true, bool canreduce = false, bool transient = false, int compress = 0)
+{
+ if(!t)
+ {
+ char *key = newstring(rname);
+ t = &textures[key];
+ t->name = key;
+ }
+
+ t->clamp = clamp;
+ t->mipmap = mipit;
+ t->type = Texture::IMAGE;
+ if(transient) t->type |= Texture::TRANSIENT;
+ if(clamp&0x300) t->type |= Texture::MIRROR;
+ if(!s.data)
+ {
+ t->type |= Texture::STUB;
+ t->w = t->h = t->xs = t->ys = t->bpp = 0;
+ return t;
+ }
+
+ bool swizzle = !(clamp&0x10000);
+ GLenum format;
+ if(s.compressed)
+ {
+ format = uncompressedformat(s.compressed);
+ t->bpp = formatsize(format);
+ t->type |= Texture::COMPRESSED;
+ }
+ else
+ {
+ format = texformat(s.bpp, swizzle);
+ t->bpp = s.bpp;
+ if(swizzle && hasTRG && !hasTSW && swizzlemask(format))
+ {
+ swizzleimage(s);
+ format = texformat(s.bpp, swizzle);
+ t->bpp = s.bpp;
+ }
+ }
+ if(alphaformat(format)) t->type |= Texture::ALPHA;
+ t->w = t->xs = s.w;
+ t->h = t->ys = s.h;
+
+ int filter = !canreduce || reducefilter ? (mipit ? 2 : 1) : 0;
+ glGenTextures(1, &t->id);
+ if(s.compressed)
+ {
+ uchar *data = s.data;
+ int levels = s.levels, level = 0;
+ if(canreduce && texreduce) loopi(min(texreduce, s.levels-1))
+ {
+ data += s.calclevelsize(level++);
+ levels--;
+ if(t->w > 1) t->w /= 2;
+ if(t->h > 1) t->h /= 2;
+ }
+ int sizelimit = mipit && maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize;
+ while(t->w > sizelimit || t->h > sizelimit)
+ {
+ data += s.calclevelsize(level++);
+ levels--;
+ if(t->w > 1) t->w /= 2;
+ if(t->h > 1) t->h /= 2;
+ }
+ createcompressedtexture(t->id, t->w, t->h, data, s.align, s.bpp, levels, clamp, filter, s.compressed, GL_TEXTURE_2D, swizzle);
+ }
+ else
+ {
+ resizetexture(t->w, t->h, mipit, canreduce, GL_TEXTURE_2D, compress, t->w, t->h);
+ GLenum component = compressedformat(format, t->w, t->h, compress);
+ createtexture(t->id, t->w, t->h, s.data, clamp, filter, component, GL_TEXTURE_2D, t->xs, t->ys, s.pitch, false, format, swizzle);
+ }
+ return t;
+}
+
+#if SDL_BYTEORDER == SDL_BIG_ENDIAN
+#define RGBAMASKS 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff
+#define RGBMASKS 0xff0000, 0x00ff00, 0x0000ff, 0
+#else
+#define RGBAMASKS 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000
+#define RGBMASKS 0x0000ff, 0x00ff00, 0xff0000, 0
+#endif
+
+SDL_Surface *wrapsurface(void *data, int width, int height, int bpp)
+{
+ switch(bpp)
+ {
+ case 3: return SDL_CreateRGBSurfaceFrom(data, width, height, 8*bpp, bpp*width, RGBMASKS);
+ case 4: return SDL_CreateRGBSurfaceFrom(data, width, height, 8*bpp, bpp*width, RGBAMASKS);
+ }
+ return NULL;
+}
+
+SDL_Surface *creatergbsurface(SDL_Surface *os)
+{
+ SDL_Surface *ns = SDL_CreateRGBSurface(SDL_SWSURFACE, os->w, os->h, 24, RGBMASKS);
+ if(ns) SDL_BlitSurface(os, NULL, ns, NULL);
+ SDL_FreeSurface(os);
+ return ns;
+}
+
+SDL_Surface *creatergbasurface(SDL_Surface *os)
+{
+ SDL_Surface *ns = SDL_CreateRGBSurface(SDL_SWSURFACE, os->w, os->h, 32, RGBAMASKS);
+ if(ns)
+ {
+ SDL_SetSurfaceBlendMode(os, SDL_BLENDMODE_NONE);
+ SDL_BlitSurface(os, NULL, ns, NULL);
+ }
+ SDL_FreeSurface(os);
+ return ns;
+}
+
+bool checkgrayscale(SDL_Surface *s)
+{
+ // gray scale images have 256 levels, no colorkey, and the palette is a ramp
+ if(s->format->palette)
+ {
+ if(s->format->palette->ncolors != 256 || SDL_GetColorKey(s, NULL) >= 0) return false;
+ const SDL_Color *colors = s->format->palette->colors;
+ loopi(256) if(colors[i].r != i || colors[i].g != i || colors[i].b != i) return false;
+ }
+ return true;
+}
+
+SDL_Surface *fixsurfaceformat(SDL_Surface *s)
+{
+ if(!s) return NULL;
+ if(!s->pixels || min(s->w, s->h) <= 0 || s->format->BytesPerPixel <= 0)
+ {
+ SDL_FreeSurface(s);
+ return NULL;
+ }
+ static const uint rgbmasks[] = { RGBMASKS }, rgbamasks[] = { RGBAMASKS };
+ switch(s->format->BytesPerPixel)
+ {
+ case 1:
+ if(!checkgrayscale(s)) return SDL_GetColorKey(s, NULL) >= 0 ? creatergbasurface(s) : creatergbsurface(s);
+ break;
+ case 3:
+ if(s->format->Rmask != rgbmasks[0] || s->format->Gmask != rgbmasks[1] || s->format->Bmask != rgbmasks[2])
+ return creatergbsurface(s);
+ break;
+ case 4:
+ if(s->format->Rmask != rgbamasks[0] || s->format->Gmask != rgbamasks[1] || s->format->Bmask != rgbamasks[2] || s->format->Amask != rgbamasks[3])
+ return s->format->Amask ? creatergbasurface(s) : creatergbsurface(s);
+ break;
+ }
+ return s;
+}
+
+void texflip(ImageData &s)
+{
+ ImageData d(s.w, s.h, s.bpp);
+ uchar *dst = d.data, *src = &s.data[s.pitch*s.h];
+ loopi(s.h)
+ {
+ src -= s.pitch;
+ memcpy(dst, src, s.bpp*s.w);
+ dst += d.pitch;
+ }
+ s.replace(d);
+}
+
+void texnormal(ImageData &s, int emphasis)
+{
+ ImageData d(s.w, s.h, 3);
+ uchar *src = s.data, *dst = d.data;
+ loop(y, s.h) loop(x, s.w)
+ {
+ vec normal(0.0f, 0.0f, 255.0f/emphasis);
+ normal.x += src[y*s.pitch + ((x+s.w-1)%s.w)*s.bpp];
+ normal.x -= src[y*s.pitch + ((x+1)%s.w)*s.bpp];
+ normal.y += src[((y+s.h-1)%s.h)*s.pitch + x*s.bpp];
+ normal.y -= src[((y+1)%s.h)*s.pitch + x*s.bpp];
+ normal.normalize();
+ *dst++ = uchar(127.5f + normal.x*127.5f);
+ *dst++ = uchar(127.5f + normal.y*127.5f);
+ *dst++ = uchar(127.5f + normal.z*127.5f);
+ }
+ s.replace(d);
+}
+
+template<int n, int bpp, bool normals>
+static void blurtexture(int w, int h, uchar *dst, const uchar *src, int margin)
+{
+ static const int weights3x3[9] =
+ {
+ 0x10, 0x20, 0x10,
+ 0x20, 0x40, 0x20,
+ 0x10, 0x20, 0x10
+ };
+ static const int weights5x5[25] =
+ {
+ 0x05, 0x05, 0x09, 0x05, 0x05,
+ 0x05, 0x0A, 0x14, 0x0A, 0x05,
+ 0x09, 0x14, 0x28, 0x14, 0x09,
+ 0x05, 0x0A, 0x14, 0x0A, 0x05,
+ 0x05, 0x05, 0x09, 0x05, 0x05
+ };
+ const int *mat = n > 1 ? weights5x5 : weights3x3;
+ int mstride = 2*n + 1,
+ mstartoffset = n*(mstride + 1),
+ stride = bpp*w,
+ startoffset = n*bpp,
+ nextoffset1 = stride + mstride*bpp,
+ nextoffset2 = stride - mstride*bpp;
+ src += margin*(stride + bpp);
+ for(int y = margin; y < h-margin; y++)
+ {
+ for(int x = margin; x < w-margin; x++)
+ {
+ int dr = 0, dg = 0, db = 0;
+ const uchar *p = src - startoffset;
+ const int *m = mat + mstartoffset;
+ for(int t = y; t >= y-n; t--, p -= nextoffset1, m -= mstride)
+ {
+ if(t < 0) p += stride;
+ int a = 0;
+ if(n > 1) { a += m[-2]; if(x >= 2) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp; }
+ a += m[-1]; if(x >= 1) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp;
+ int cr = p[0], cg = p[1], cb = p[2]; a += m[0]; dr += cr * a; dg += cg * a; db += cb * a; p += bpp;
+ if(x+1 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[1]; dg += cg * m[1]; db += cb * m[1]; p += bpp;
+ if(n > 1) { if(x+2 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[2]; dg += cg * m[2]; db += cb * m[2]; p += bpp; }
+ }
+ p = src - startoffset + stride;
+ m = mat + mstartoffset + mstride;
+ for(int t = y+1; t <= y+n; t++, p += nextoffset2, m += mstride)
+ {
+ if(t >= h) p -= stride;
+ int a = 0;
+ if(n > 1) { a += m[-2]; if(x >= 2) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp; }
+ a += m[-1]; if(x >= 1) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp;
+ int cr = p[0], cg = p[1], cb = p[2]; a += m[0]; dr += cr * a; dg += cg * a; db += cb * a; p += bpp;
+ if(x+1 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[1]; dg += cg * m[1]; db += cb * m[1]; p += bpp;
+ if(n > 1) { if(x+2 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[2]; dg += cg * m[2]; db += cb * m[2]; p += bpp; }
+ }
+ if(normals)
+ {
+ vec v(dr-0x7F80, dg-0x7F80, db-0x7F80);
+ float mag = 127.5f/v.magnitude();
+ dst[0] = uchar(v.x*mag + 127.5f);
+ dst[1] = uchar(v.y*mag + 127.5f);
+ dst[2] = uchar(v.z*mag + 127.5f);
+ }
+ else
+ {
+ dst[0] = dr>>8;
+ dst[1] = dg>>8;
+ dst[2] = db>>8;
+ }
+ if(bpp > 3) dst[3] = src[3];
+ dst += bpp;
+ src += bpp;
+ }
+ src += 2*margin*bpp;
+ }
+}
+
+void blurtexture(int n, int bpp, int w, int h, uchar *dst, const uchar *src, int margin)
+{
+ switch((clamp(n, 1, 2)<<4) | bpp)
+ {
+ case 0x13: blurtexture<1, 3, false>(w, h, dst, src, margin); break;
+ case 0x23: blurtexture<2, 3, false>(w, h, dst, src, margin); break;
+ case 0x14: blurtexture<1, 4, false>(w, h, dst, src, margin); break;
+ case 0x24: blurtexture<2, 4, false>(w, h, dst, src, margin); break;
+ }
+}
+
+void blurnormals(int n, int w, int h, bvec *dst, const bvec *src, int margin)
+{
+ switch(clamp(n, 1, 2))
+ {
+ case 1: blurtexture<1, 3, true>(w, h, dst->v, src->v, margin); break;
+ case 2: blurtexture<2, 3, true>(w, h, dst->v, src->v, margin); break;
+ }
+}
+
+void texblur(ImageData &s, int n, int r)
+{
+ if(s.bpp < 3) return;
+ loopi(r)
+ {
+ ImageData d(s.w, s.h, s.bpp);
+ blurtexture(n, s.bpp, s.w, s.h, d.data, s.data);
+ s.replace(d);
+ }
+}
+
+void scaleimage(ImageData &s, int w, int h)
+{
+ ImageData d(w, h, s.bpp);
+ scaletexture(s.data, s.w, s.h, s.bpp, s.pitch, d.data, w, h);
+ s.replace(d);
+}
+
+bool canloadsurface(const char *name)
+{
+ stream *f = openfile(name, "rb");
+ if(!f) return false;
+ delete f;
+ return true;
+}
+
+SDL_Surface *loadsurface(const char *name)
+{
+ SDL_Surface *s = NULL;
+ stream *z = openzipfile(name, "rb");
+ if(z)
+ {
+ SDL_RWops *rw = z->rwops();
+ if(rw)
+ {
+ char *ext = (char *)strrchr(name, '.');
+ if(ext) ++ext;
+ s = IMG_LoadTyped_RW(rw, 0, ext);
+ SDL_FreeRW(rw);
+ }
+ delete z;
+ }
+ if(!s) s = IMG_Load(findfile(name, "rb"));
+ return fixsurfaceformat(s);
+}
+
+static vec parsevec(const char *arg)
+{
+ vec v(0, 0, 0);
+ int i = 0;
+ for(; arg[0] && (!i || arg[0]=='/') && i<3; arg += strcspn(arg, "/,><"), i++)
+ {
+ if(i) arg++;
+ v[i] = atof(arg);
+ }
+ if(i==1) v.y = v.z = v.x;
+ return v;
+}
+
+VAR(usedds, 0, 1, 1);
+VAR(dbgdds, 0, 0, 1);
+VAR(scaledds, 0, 2, 4);
+
+static bool texturedata(ImageData &d, const char *tname, Slot::Tex *tex = NULL, bool msg = true, int *compress = NULL, int *wrap = NULL)
+{
+ const char *cmds = NULL, *file = tname;
+
+ if(!tname)
+ {
+ if(!tex) return false;
+ if(tex->name[0]=='<')
+ {
+ cmds = tex->name;
+ file = strrchr(tex->name, '>');
+ if(!file) { if(msg) conoutf(CON_ERROR, "could not load texture packages/%s", tex->name); return false; }
+ file++;
+ }
+ else file = tex->name;
+
+ static string pname;
+ formatstring(pname, "packages/%s", file);
+ file = path(pname);
+ }
+ else if(tname[0]=='<')
+ {
+ cmds = tname;
+ file = strrchr(tname, '>');
+ if(!file) { if(msg) conoutf(CON_ERROR, "could not load texture %s", tname); return false; }
+ file++;
+ }
+
+ int flen = strlen(file);
+ bool raw = !usedds || !compress, dds = false, guess = false;
+ for(const char *pcmds = cmds; pcmds;)
+ {
+ #define PARSETEXCOMMANDS(cmds) \
+ const char *cmd = NULL, *end = NULL, *arg[4] = { NULL, NULL, NULL, NULL }; \
+ cmd = &cmds[1]; \
+ end = strchr(cmd, '>'); \
+ if(!end) break; \
+ cmds = strchr(cmd, '<'); \
+ size_t len = strcspn(cmd, ":,><"); \
+ loopi(4) \
+ { \
+ arg[i] = strchr(i ? arg[i-1] : cmd, i ? ',' : ':'); \
+ if(!arg[i] || arg[i] >= end) arg[i] = ""; \
+ else arg[i]++; \
+ }
+ PARSETEXCOMMANDS(pcmds);
+ if(matchstring(cmd, len, "dds")) dds = true;
+ else if(matchstring(cmd, len, "thumbnail"))
+ {
+ raw = true;
+ guess = flen >= 4 && !strchr(file+flen-4, '.');
+ }
+ else if(matchstring(cmd, len, "stub")) return canloadsurface(file);
+ }
+
+ if(msg) renderprogress(loadprogress, file);
+
+ if(flen >= 4 && (!strcasecmp(file + flen - 4, ".dds") || (dds && !raw)))
+ {
+ string dfile;
+ copystring(dfile, file);
+ memcpy(dfile + flen - 4, ".dds", 4);
+ if(!loaddds(dfile, d, raw ? 1 : (dds ? 0 : -1)) && (!dds || raw))
+ {
+ if(msg) conoutf(CON_ERROR, "could not load texture %s", dfile);
+ return false;
+ }
+ if(d.data && !d.compressed && !dds && compress) *compress = scaledds;
+ }
+
+ if(!d.data)
+ {
+ SDL_Surface *s = NULL;
+ if(guess)
+ {
+ static const char *exts[] = {".jpg", ".png"};
+ string ext;
+ loopi(sizeof(exts)/sizeof(exts[0]))
+ {
+ copystring(ext, file);
+ concatstring(ext, exts[i]);
+ s = loadsurface(ext);
+ if(s) break;
+ }
+ }
+ else s = loadsurface(file);
+ if(!s) { if(msg) conoutf(CON_ERROR, "could not load texture %s", file); return false; }
+ int bpp = s->format->BitsPerPixel;
+ if(bpp%8 || !texformat(bpp/8)) { SDL_FreeSurface(s); conoutf(CON_ERROR, "texture must be 8, 16, 24, or 32 bpp: %s", file); return false; }
+ if(max(s->w, s->h) > (1<<12)) { SDL_FreeSurface(s); conoutf(CON_ERROR, "texture size exceeded %dx%d pixels: %s", 1<<12, 1<<12, file); return false; }
+ d.wrap(s);
+ }
+
+ while(cmds)
+ {
+ PARSETEXCOMMANDS(cmds);
+ if(d.compressed) goto compressed;
+ if(matchstring(cmd, len, "mad")) texmad(d, parsevec(arg[0]), parsevec(arg[1]));
+ else if(matchstring(cmd, len, "colorify")) texcolorify(d, parsevec(arg[0]), parsevec(arg[1]));
+ else if(matchstring(cmd, len, "colormask")) texcolormask(d, parsevec(arg[0]), *arg[1] ? parsevec(arg[1]) : vec(1, 1, 1));
+ else if(matchstring(cmd, len, "normal"))
+ {
+ int emphasis = atoi(arg[0]);
+ texnormal(d, emphasis > 0 ? emphasis : 3);
+ }
+ else if(matchstring(cmd, len, "dup")) texdup(d, atoi(arg[0]), atoi(arg[1]));
+ else if(matchstring(cmd, len, "offset")) texoffset(d, atoi(arg[0]), atoi(arg[1]));
+ else if(matchstring(cmd, len, "rotate")) texrotate(d, atoi(arg[0]), tex ? tex->type : 0);
+ else if(matchstring(cmd, len, "reorient")) texreorient(d, atoi(arg[0])>0, atoi(arg[1])>0, atoi(arg[2])>0, tex ? tex->type : TEX_DIFFUSE);
+ else if(matchstring(cmd, len, "mix")) texmix(d, *arg[0] ? atoi(arg[0]) : -1, *arg[1] ? atoi(arg[1]) : -1, *arg[2] ? atoi(arg[2]) : -1, *arg[3] ? atoi(arg[3]) : -1);
+ else if(matchstring(cmd, len, "grey")) texgrey(d);
+ else if(matchstring(cmd, len, "blur"))
+ {
+ int emphasis = atoi(arg[0]), repeat = atoi(arg[1]);
+ texblur(d, emphasis > 0 ? clamp(emphasis, 1, 2) : 1, repeat > 0 ? repeat : 1);
+ }
+ else if(matchstring(cmd, len, "premul")) texpremul(d);
+ else if(matchstring(cmd, len, "agrad")) texagrad(d, atof(arg[0]), atof(arg[1]), atof(arg[2]), atof(arg[3]));
+ else if(matchstring(cmd, len, "compress") || matchstring(cmd, len, "dds"))
+ {
+ int scale = atoi(arg[0]);
+ if(scale <= 0) scale = scaledds;
+ if(compress) *compress = scale;
+ }
+ else if(matchstring(cmd, len, "nocompress"))
+ {
+ if(compress) *compress = -1;
+ }
+ else if(matchstring(cmd, len, "thumbnail"))
+ {
+ int w = atoi(arg[0]), h = atoi(arg[1]);
+ if(w <= 0 || w > (1<<12)) w = 64;
+ if(h <= 0 || h > (1<<12)) h = w;
+ if(d.w > w || d.h > h) scaleimage(d, w, h);
+ }
+ else
+ compressed:
+ if(matchstring(cmd, len, "mirror"))
+ {
+ if(wrap) *wrap |= 0x300;
+ }
+ else if(matchstring(cmd, len, "noswizzle"))
+ {
+ if(wrap) *wrap |= 0x10000;
+ }
+ }
+
+ return true;
+}
+
+uchar *loadalphamask(Texture *t)
+{
+ if(t->alphamask) return t->alphamask;
+ if(!(t->type&Texture::ALPHA)) return NULL;
+ ImageData s;
+ if(!texturedata(s, t->name, NULL, false) || !s.data || s.compressed) return NULL;
+ t->alphamask = new uchar[s.h * ((s.w+7)/8)];
+ uchar *srcrow = s.data, *dst = t->alphamask-1;
+ loop(y, s.h)
+ {
+ uchar *src = srcrow+s.bpp-1;
+ loop(x, s.w)
+ {
+ int offset = x%8;
+ if(!offset) *++dst = 0;
+ if(*src) *dst |= 1<<offset;
+ src += s.bpp;
+ }
+ srcrow += s.pitch;
+ }
+ return t->alphamask;
+}
+
+Texture *textureload(const char *name, int clamp, bool mipit, bool msg)
+{
+ string tname;
+ copystring(tname, name);
+ Texture *t = textures.access(path(tname));
+ if(t) return t;
+ int compress = 0;
+ ImageData s;
+ if(texturedata(s, tname, NULL, msg, &compress, &clamp)) return newtexture(NULL, tname, s, clamp, mipit, false, false, compress);
+ return notexture;
+}
+
+bool settexture(const char *name, int clamp)
+{
+ Texture *t = textureload(name, clamp, true, false);
+ glBindTexture(GL_TEXTURE_2D, t->id);
+ return t != notexture;
+}
+
+vector<VSlot *> vslots;
+vector<Slot *> slots;
+MSlot materialslots[(MATF_VOLUME|MATF_INDEX)+1];
+Slot dummyslot;
+VSlot dummyvslot(&dummyslot);
+
+void texturereset(int *n)
+{
+ if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return;
+ resetslotshader();
+ int limit = clamp(*n, 0, slots.length());
+ for(int i = limit; i < slots.length(); i++)
+ {
+ Slot *s = slots[i];
+ for(VSlot *vs = s->variants; vs; vs = vs->next) vs->slot = &dummyslot;
+ delete s;
+ }
+ slots.setsize(limit);
+ while(vslots.length())
+ {
+ VSlot *vs = vslots.last();
+ if(vs->slot != &dummyslot || vs->changed) break;
+ delete vslots.pop();
+ }
+}
+
+COMMAND(texturereset, "i");
+
+void materialreset()
+{
+ if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return;
+ loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].reset();
+}
+
+COMMAND(materialreset, "");
+
+static int compactedvslots = 0, compactvslotsprogress = 0, clonedvslots = 0;
+static bool markingvslots = false;
+
+void clearslots()
+{
+ resetslotshader();
+ slots.deletecontents();
+ vslots.deletecontents();
+ loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].reset();
+ clonedvslots = 0;
+}
+
+static void assignvslot(VSlot &vs);
+
+static inline void assignvslotlayer(VSlot &vs)
+{
+ if(vs.layer && vslots.inrange(vs.layer))
+ {
+ VSlot &layer = *vslots[vs.layer];
+ if(layer.index < 0) assignvslot(layer);
+ }
+}
+
+static void assignvslot(VSlot &vs)
+{
+ vs.index = compactedvslots++;
+ assignvslotlayer(vs);
+}
+
+void compactvslot(int &index)
+{
+ if(vslots.inrange(index))
+ {
+ VSlot &vs = *vslots[index];
+ if(vs.index < 0) assignvslot(vs);
+ if(!markingvslots) index = vs.index;
+ }
+}
+
+void compactvslot(VSlot &vs)
+{
+ if(vs.index < 0) assignvslot(vs);
+}
+
+void compactvslots(cube *c, int n)
+{
+ if((compactvslotsprogress++&0xFFF)==0) renderprogress(min(float(compactvslotsprogress)/allocnodes, 1.0f), markingvslots ? "marking slots..." : "compacting slots...");
+ loopi(n)
+ {
+ if(c[i].children) compactvslots(c[i].children);
+ else loopj(6) if(vslots.inrange(c[i].texture[j]))
+ {
+ VSlot &vs = *vslots[c[i].texture[j]];
+ if(vs.index < 0) assignvslot(vs);
+ if(!markingvslots) c[i].texture[j] = vs.index;
+ }
+ }
+}
+
+int compactvslots()
+{
+ clonedvslots = 0;
+ markingvslots = false;
+ compactedvslots = 0;
+ compactvslotsprogress = 0;
+ loopv(vslots) vslots[i]->index = -1;
+ loopv(slots) slots[i]->variants->index = compactedvslots++;
+ loopv(slots) assignvslotlayer(*slots[i]->variants);
+ loopv(vslots)
+ {
+ VSlot &vs = *vslots[i];
+ if(!vs.changed && vs.index < 0) { markingvslots = true; break; }
+ }
+ compactvslots(worldroot);
+ int total = compactedvslots;
+ compacteditvslots();
+ loopv(vslots)
+ {
+ VSlot *vs = vslots[i];
+ if(vs->changed) continue;
+ while(vs->next)
+ {
+ if(vs->next->index < 0) vs->next = vs->next->next;
+ else vs = vs->next;
+ }
+ }
+ if(markingvslots)
+ {
+ markingvslots = false;
+ compactedvslots = 0;
+ compactvslotsprogress = 0;
+ int lastdiscard = 0;
+ loopv(vslots)
+ {
+ VSlot &vs = *vslots[i];
+ if(vs.changed || (vs.index < 0 && !vs.next)) vs.index = -1;
+ else
+ {
+ while(lastdiscard < i)
+ {
+ VSlot &ds = *vslots[lastdiscard++];
+ if(!ds.changed && ds.index < 0) ds.index = compactedvslots++;
+ }
+ vs.index = compactedvslots++;
+ }
+ }
+ compactvslots(worldroot);
+ total = compactedvslots;
+ compacteditvslots();
+ }
+ compactmruvslots();
+ loopv(vslots)
+ {
+ VSlot &vs = *vslots[i];
+ if(vs.index >= 0 && vs.layer && vslots.inrange(vs.layer)) vs.layer = vslots[vs.layer]->index;
+ }
+ loopv(vslots)
+ {
+ while(vslots[i]->index >= 0 && vslots[i]->index != i)
+ swap(vslots[i], vslots[vslots[i]->index]);
+ }
+ for(int i = compactedvslots; i < vslots.length(); i++) delete vslots[i];
+ vslots.setsize(compactedvslots);
+ return total;
+}
+
+ICOMMAND(compactvslots, "", (),
+{
+ extern int nompedit;
+ if(nompedit && multiplayer()) return;
+ compactvslots();
+ allchanged();
+});
+
+static Slot &loadslot(Slot &s, bool forceload);
+
+static void clampvslotoffset(VSlot &dst, Slot *slot = NULL)
+{
+ if(!slot) slot = dst.slot;
+ if(slot && slot->sts.inrange(0))
+ {
+ if(!slot->loaded) loadslot(*slot, false);
+ Texture *t = slot->sts[0].t;
+ int xs = t->xs, ys = t->ys;
+ if(t->type & Texture::MIRROR) { xs *= 2; ys *= 2; }
+ if(texrotations[dst.rotation].swapxy) swap(xs, ys);
+ dst.offset.x %= xs; if(dst.offset.x < 0) dst.offset.x += xs;
+ dst.offset.y %= ys; if(dst.offset.y < 0) dst.offset.y += ys;
+ }
+ else dst.offset.max(0);
+}
+
+static void propagatevslot(VSlot &dst, const VSlot &src, int diff, bool edit = false)
+{
+ if(diff & (1<<VSLOT_SHPARAM)) loopv(src.params) dst.params.add(src.params[i]);
+ if(diff & (1<<VSLOT_SCALE)) dst.scale = src.scale;
+ if(diff & (1<<VSLOT_ROTATION))
+ {
+ dst.rotation = src.rotation;
+ if(edit && !dst.offset.iszero()) clampvslotoffset(dst);
+ }
+ if(diff & (1<<VSLOT_OFFSET))
+ {
+ dst.offset = src.offset;
+ if(edit) clampvslotoffset(dst);
+ }
+ if(diff & (1<<VSLOT_SCROLL)) dst.scroll = src.scroll;
+ if(diff & (1<<VSLOT_LAYER)) dst.layer = src.layer;
+ if(diff & (1<<VSLOT_ALPHA))
+ {
+ dst.alphafront = src.alphafront;
+ dst.alphaback = src.alphaback;
+ }
+ if(diff & (1<<VSLOT_COLOR)) dst.colorscale = src.colorscale;
+}
+
+static void propagatevslot(VSlot *root, int changed)
+{
+ for(VSlot *vs = root->next; vs; vs = vs->next)
+ {
+ int diff = changed & ~vs->changed;
+ if(diff) propagatevslot(*vs, *root, diff);
+ }
+}
+
+static void mergevslot(VSlot &dst, const VSlot &src, int diff, Slot *slot = NULL)
+{
+ if(diff & (1<<VSLOT_SHPARAM)) loopv(src.params)
+ {
+ const SlotShaderParam &sp = src.params[i];
+ loopvj(dst.params)
+ {
+ SlotShaderParam &dp = dst.params[j];
+ if(sp.name == dp.name)
+ {
+ memcpy(dp.val, sp.val, sizeof(dp.val));
+ goto nextparam;
+ }
+ }
+ dst.params.add(sp);
+ nextparam:;
+ }
+ if(diff & (1<<VSLOT_SCALE))
+ {
+ dst.scale = clamp(dst.scale*src.scale, 1/8.0f, 8.0f);
+ }
+ if(diff & (1<<VSLOT_ROTATION))
+ {
+ dst.rotation = clamp(dst.rotation + src.rotation, 0, 7);
+ if(!dst.offset.iszero()) clampvslotoffset(dst, slot);
+ }
+ if(diff & (1<<VSLOT_OFFSET))
+ {
+ dst.offset.add(src.offset);
+ clampvslotoffset(dst, slot);
+ }
+ if(diff & (1<<VSLOT_SCROLL)) dst.scroll.add(src.scroll);
+ if(diff & (1<<VSLOT_LAYER)) dst.layer = src.layer;
+ if(diff & (1<<VSLOT_ALPHA))
+ {
+ dst.alphafront = src.alphafront;
+ dst.alphaback = src.alphaback;
+ }
+ if(diff & (1<<VSLOT_COLOR)) dst.colorscale.mul(src.colorscale);
+}
+
+void mergevslot(VSlot &dst, const VSlot &src, const VSlot &delta)
+{
+ dst.changed = src.changed | delta.changed;
+ propagatevslot(dst, src, (1<<VSLOT_NUM)-1);
+ mergevslot(dst, delta, delta.changed, src.slot);
+}
+
+static VSlot *reassignvslot(Slot &owner, VSlot *vs)
+{
+ owner.variants = vs;
+ while(vs)
+ {
+ vs->slot = &owner;
+ vs->linked = false;
+ vs = vs->next;
+ }
+ return owner.variants;
+}
+
+static VSlot *emptyvslot(Slot &owner)
+{
+ int offset = 0;
+ loopvrev(slots) if(slots[i]->variants) { offset = slots[i]->variants->index + 1; break; }
+ for(int i = offset; i < vslots.length(); i++) if(!vslots[i]->changed) return reassignvslot(owner, vslots[i]);
+ return vslots.add(new VSlot(&owner, vslots.length()));
+}
+
+static bool comparevslot(const VSlot &dst, const VSlot &src, int diff)
+{
+ if(diff & (1<<VSLOT_SHPARAM))
+ {
+ if(src.params.length() != dst.params.length()) return false;
+ loopv(src.params)
+ {
+ const SlotShaderParam &sp = src.params[i], &dp = dst.params[i];
+ if(sp.name != dp.name || memcmp(sp.val, dp.val, sizeof(sp.val))) return false;
+ }
+ }
+ if(diff & (1<<VSLOT_SCALE) && dst.scale != src.scale) return false;
+ if(diff & (1<<VSLOT_ROTATION) && dst.rotation != src.rotation) return false;
+ if(diff & (1<<VSLOT_OFFSET) && dst.offset != src.offset) return false;
+ if(diff & (1<<VSLOT_SCROLL) && dst.scroll != src.scroll) return false;
+ if(diff & (1<<VSLOT_LAYER) && dst.layer != src.layer) return false;
+ if(diff & (1<<VSLOT_ALPHA) && (dst.alphafront != src.alphafront || dst.alphaback != src.alphaback)) return false;
+ if(diff & (1<<VSLOT_COLOR) && dst.colorscale != src.colorscale) return false;
+ return true;
+}
+
+void packvslot(vector<uchar> &buf, const VSlot &src)
+{
+ if(src.changed & (1<<VSLOT_SHPARAM))
+ {
+ loopv(src.params)
+ {
+ const SlotShaderParam &p = src.params[i];
+ buf.put(VSLOT_SHPARAM);
+ sendstring(p.name, buf);
+ loopj(4) putfloat(buf, p.val[j]);
+ }
+ }
+ if(src.changed & (1<<VSLOT_SCALE))
+ {
+ buf.put(VSLOT_SCALE);
+ putfloat(buf, src.scale);
+ }
+ if(src.changed & (1<<VSLOT_ROTATION))
+ {
+ buf.put(VSLOT_ROTATION);
+ putint(buf, src.rotation);
+ }
+ if(src.changed & (1<<VSLOT_OFFSET))
+ {
+ buf.put(VSLOT_OFFSET);
+ putint(buf, src.offset.x);
+ putint(buf, src.offset.y);
+ }
+ if(src.changed & (1<<VSLOT_SCROLL))
+ {
+ buf.put(VSLOT_SCROLL);
+ putfloat(buf, src.scroll.x);
+ putfloat(buf, src.scroll.y);
+ }
+ if(src.changed & (1<<VSLOT_LAYER))
+ {
+ buf.put(VSLOT_LAYER);
+ putuint(buf, vslots.inrange(src.layer) && !vslots[src.layer]->changed ? src.layer : 0);
+ }
+ if(src.changed & (1<<VSLOT_ALPHA))
+ {
+ buf.put(VSLOT_ALPHA);
+ putfloat(buf, src.alphafront);
+ putfloat(buf, src.alphaback);
+ }
+ if(src.changed & (1<<VSLOT_COLOR))
+ {
+ buf.put(VSLOT_COLOR);
+ putfloat(buf, src.colorscale.r);
+ putfloat(buf, src.colorscale.g);
+ putfloat(buf, src.colorscale.b);
+ }
+ buf.put(0xFF);
+}
+
+void packvslot(vector<uchar> &buf, int index)
+{
+ if(vslots.inrange(index)) packvslot(buf, *vslots[index]);
+ else buf.put(0xFF);
+}
+
+void packvslot(vector<uchar> &buf, const VSlot *vs)
+{
+ if(vs) packvslot(buf, *vs);
+ else buf.put(0xFF);
+}
+
+bool unpackvslot(ucharbuf &buf, VSlot &dst, bool delta)
+{
+ while(buf.remaining())
+ {
+ int changed = buf.get();
+ if(changed >= 0x80) break;
+ switch(changed)
+ {
+ case VSLOT_SHPARAM:
+ {
+ string name;
+ getstring(name, buf);
+ SlotShaderParam p = { name[0] ? getshaderparamname(name) : NULL, -1, { 0, 0, 0, 0 } };
+ loopi(4) p.val[i] = getfloat(buf);
+ if(p.name) dst.params.add(p);
+ break;
+ }
+ case VSLOT_SCALE:
+ dst.scale = getfloat(buf);
+ if(dst.scale <= 0) dst.scale = 1;
+ else if(!delta) dst.scale = clamp(dst.scale, 1/8.0f, 8.0f);
+ break;
+ case VSLOT_ROTATION:
+ dst.rotation = getint(buf);
+ if(!delta) dst.rotation = clamp(dst.rotation, 0, 7);
+ break;
+ case VSLOT_OFFSET:
+ dst.offset.x = getint(buf);
+ dst.offset.y = getint(buf);
+ if(!delta) dst.offset.max(0);
+ break;
+ case VSLOT_SCROLL:
+ dst.scroll.x = getfloat(buf);
+ dst.scroll.y = getfloat(buf);
+ break;
+ case VSLOT_LAYER:
+ {
+ int tex = getuint(buf);
+ dst.layer = vslots.inrange(tex) ? tex : 0;
+ break;
+ }
+ case VSLOT_ALPHA:
+ dst.alphafront = clamp(getfloat(buf), 0.0f, 1.0f);
+ dst.alphaback = clamp(getfloat(buf), 0.0f, 1.0f);
+ break;
+ case VSLOT_COLOR:
+ dst.colorscale.r = clamp(getfloat(buf), 0.0f, 2.0f);
+ dst.colorscale.g = clamp(getfloat(buf), 0.0f, 2.0f);
+ dst.colorscale.b = clamp(getfloat(buf), 0.0f, 2.0f);
+ break;
+ default:
+ return false;
+ }
+ dst.changed |= 1<<changed;
+ }
+ if(buf.overread()) return false;
+ return true;
+}
+
+VSlot *findvslot(Slot &slot, const VSlot &src, const VSlot &delta)
+{
+ for(VSlot *dst = slot.variants; dst; dst = dst->next)
+ {
+ if((!dst->changed || dst->changed == (src.changed | delta.changed)) &&
+ comparevslot(*dst, src, src.changed & ~delta.changed) &&
+ comparevslot(*dst, delta, delta.changed))
+ return dst;
+ }
+ return NULL;
+}
+
+static VSlot *clonevslot(const VSlot &src, const VSlot &delta)
+{
+ VSlot *dst = vslots.add(new VSlot(src.slot, vslots.length()));
+ dst->changed = src.changed | delta.changed;
+ propagatevslot(*dst, src, ((1<<VSLOT_NUM)-1) & ~delta.changed);
+ propagatevslot(*dst, delta, delta.changed, true);
+ return dst;
+}
+
+VARP(autocompactvslots, 0, 256, 0x10000);
+
+VSlot *editvslot(const VSlot &src, const VSlot &delta)
+{
+ VSlot *exists = findvslot(*src.slot, src, delta);
+ if(exists) return exists;
+ if(vslots.length()>=0x10000)
+ {
+ compactvslots();
+ allchanged();
+ if(vslots.length()>=0x10000) return NULL;
+ }
+ if(autocompactvslots && ++clonedvslots >= autocompactvslots)
+ {
+ compactvslots();
+ allchanged();
+ }
+ return clonevslot(src, delta);
+}
+
+static void fixinsidefaces(cube *c, const ivec &o, int size, int tex)
+{
+ loopi(8)
+ {
+ ivec co(i, o, size);
+ if(c[i].children) fixinsidefaces(c[i].children, co, size>>1, tex);
+ else loopj(6) if(!visibletris(c[i], j, co, size))
+ c[i].texture[j] = tex;
+ }
+}
+
+ICOMMAND(fixinsidefaces, "i", (int *tex),
+{
+ extern int nompedit;
+ if(noedit(true) || (nompedit && multiplayer())) return;
+ fixinsidefaces(worldroot, ivec(0, 0, 0), worldsize>>1, *tex && vslots.inrange(*tex) ? *tex : DEFAULT_GEOM);
+ allchanged();
+});
+
+const struct slottex
+{
+ const char *name;
+ int id;
+} slottexs[] =
+{
+ {"c", TEX_DIFFUSE},
+ {"u", TEX_UNKNOWN},
+ {"d", TEX_DECAL},
+ {"n", TEX_NORMAL},
+ {"g", TEX_GLOW},
+ {"s", TEX_SPEC},
+ {"z", TEX_DEPTH},
+ {"a", TEX_ALPHA},
+ {"e", TEX_ENVMAP}
+};
+
+int findslottex(const char *name)
+{
+ loopi(sizeof(slottexs)/sizeof(slottex))
+ {
+ if(!strcmp(slottexs[i].name, name)) return slottexs[i].id;
+ }
+ return -1;
+}
+
+void texture(char *type, char *name, int *rot, int *xoffset, int *yoffset, float *scale)
+{
+ if(slots.length()>=0x10000) return;
+ static int lastmatslot = -1;
+ int tnum = findslottex(type), matslot = findmaterial(type);
+ if(tnum<0) tnum = atoi(type);
+ if(tnum==TEX_DIFFUSE) lastmatslot = matslot;
+ else if(lastmatslot>=0) matslot = lastmatslot;
+ else if(slots.empty()) return;
+ Slot &s = matslot>=0 ? materialslots[matslot] : *(tnum!=TEX_DIFFUSE ? slots.last() : slots.add(new Slot(slots.length())));
+ s.loaded = false;
+ s.texmask |= 1<<tnum;
+ if(s.sts.length()>=8) conoutf(CON_WARN, "warning: too many textures in slot %d", slots.length()-1);
+ Slot::Tex &st = s.sts.add();
+ st.type = tnum;
+ st.combined = -1;
+ st.t = NULL;
+ copystring(st.name, name);
+ path(st.name);
+ if(tnum==TEX_DIFFUSE)
+ {
+ setslotshader(s);
+ VSlot &vs = matslot >= 0 ? materialslots[matslot] : *emptyvslot(s);
+ vs.reset();
+ vs.rotation = clamp(*rot, 0, 7);
+ vs.offset = ivec2(*xoffset, *yoffset).max(0);
+ vs.scale = *scale <= 0 ? 1 : *scale;
+ propagatevslot(&vs, (1<<VSLOT_NUM)-1);
+ }
+}
+
+COMMAND(texture, "ssiiif");
+
+void autograss(char *name)
+{
+ if(slots.empty()) return;
+ Slot &s = *slots.last();
+ DELETEA(s.autograss);
+ s.autograss = name[0] ? newstring(makerelpath("packages", name, NULL, "<premul>")) : NULL;
+}
+COMMAND(autograss, "s");
+
+void texscroll(float *scrollS, float *scrollT)
+{
+ if(slots.empty()) return;
+ Slot &s = *slots.last();
+ s.variants->scroll = vec2(*scrollS, *scrollT).div(1000.0f);
+ propagatevslot(s.variants, 1<<VSLOT_SCROLL);
+}
+COMMAND(texscroll, "ff");
+
+void texoffset_(int *xoffset, int *yoffset)
+{
+ if(slots.empty()) return;
+ Slot &s = *slots.last();
+ s.variants->offset = ivec2(*xoffset, *yoffset).max(0);
+ propagatevslot(s.variants, 1<<VSLOT_OFFSET);
+}
+COMMANDN(texoffset, texoffset_, "ii");
+
+void texrotate_(int *rot)
+{
+ if(slots.empty()) return;
+ Slot &s = *slots.last();
+ s.variants->rotation = clamp(*rot, 0, 7);
+ propagatevslot(s.variants, 1<<VSLOT_ROTATION);
+}
+COMMANDN(texrotate, texrotate_, "i");
+
+void texscale(float *scale)
+{
+ if(slots.empty()) return;
+ Slot &s = *slots.last();
+ s.variants->scale = *scale <= 0 ? 1 : *scale;
+ propagatevslot(s.variants, 1<<VSLOT_SCALE);
+}
+COMMAND(texscale, "f");
+
+void texlayer(int *layer, char *name, int *mode, float *scale)
+{
+ if(slots.empty()) return;
+ Slot &s = *slots.last();
+ s.variants->layer = *layer < 0 ? max(slots.length()-1+*layer, 0) : *layer;
+ s.layermaskname = name[0] ? newstring(path(makerelpath("packages", name))) : NULL;
+ s.layermaskmode = *mode;
+ s.layermaskscale = *scale <= 0 ? 1 : *scale;
+ propagatevslot(s.variants, 1<<VSLOT_LAYER);
+}
+COMMAND(texlayer, "isif");
+
+void texalpha(float *front, float *back)
+{
+ if(slots.empty()) return;
+ Slot &s = *slots.last();
+ s.variants->alphafront = clamp(*front, 0.0f, 1.0f);
+ s.variants->alphaback = clamp(*back, 0.0f, 1.0f);
+ propagatevslot(s.variants, 1<<VSLOT_ALPHA);
+}
+COMMAND(texalpha, "ff");
+
+void texcolor(float *r, float *g, float *b)
+{
+ if(slots.empty()) return;
+ Slot &s = *slots.last();
+ s.variants->colorscale = vec(clamp(*r, 0.0f, 1.0f), clamp(*g, 0.0f, 1.0f), clamp(*b, 0.0f, 1.0f));
+ propagatevslot(s.variants, 1<<VSLOT_COLOR);
+}
+COMMAND(texcolor, "fff");
+
+static int findtextype(Slot &s, int type, int last = -1)
+{
+ for(int i = last+1; i<s.sts.length(); i++) if((type&(1<<s.sts[i].type)) && s.sts[i].combined<0) return i;
+ return -1;
+}
+
+static void addglow(ImageData &c, ImageData &g, const vec &glowcolor)
+{
+ if(g.bpp < 3)
+ {
+ readwritergbtex(c, g,
+ loopk(3) dst[k] = clamp(int(dst[k]) + int(src[0]*glowcolor[k]), 0, 255);
+ );
+ }
+ else
+ {
+ readwritergbtex(c, g,
+ loopk(3) dst[k] = clamp(int(dst[k]) + int(src[k]*glowcolor[k]), 0, 255);
+ );
+ }
+}
+
+static void mergespec(ImageData &c, ImageData &s)
+{
+ if(s.bpp < 3)
+ {
+ readwritergbatex(c, s,
+ dst[3] = src[0];
+ );
+ }
+ else
+ {
+ readwritergbatex(c, s,
+ dst[3] = (int(src[0]) + int(src[1]) + int(src[2]))/3;
+ );
+ }
+}
+
+static void mergedepth(ImageData &c, ImageData &z)
+{
+ readwritergbatex(c, z,
+ dst[3] = src[0];
+ );
+}
+
+static void mergealpha(ImageData &c, ImageData &s)
+{
+ if(s.bpp < 3)
+ {
+ readwritergbatex(c, s,
+ dst[3] = src[0];
+ );
+ }
+ else if(s.bpp == 3)
+ {
+ readwritergbatex(c, s,
+ dst[3] = (int(src[0]) + int(src[1]) + int(src[2]))/3;
+ );
+ }
+ else
+ {
+ readwritergbatex(c, s,
+ dst[3] = src[3];
+ );
+ }
+}
+
+static void addname(vector<char> &key, Slot &slot, Slot::Tex &t, bool combined = false, const char *prefix = NULL)
+{
+ if(combined) key.add('&');
+ if(prefix) { while(*prefix) key.add(*prefix++); }
+ defformatstring(tname, "packages/%s", t.name);
+ for(const char *s = path(tname); *s; key.add(*s++));
+}
+
+static void texcombine(Slot &s, int index, Slot::Tex &t, bool forceload = false)
+{
+ vector<char> key;
+ addname(key, s, t);
+ int texmask = 0;
+ if(!forceload) switch(t.type)
+ {
+ case TEX_DIFFUSE:
+ case TEX_NORMAL:
+ {
+ int i = findtextype(s, t.type==TEX_DIFFUSE ? (s.texmask&(1<<TEX_SPEC) ? 1<<TEX_SPEC : 1<<TEX_ALPHA) : (s.texmask&(1<<TEX_DEPTH) ? 1<<TEX_DEPTH : 1<<TEX_ALPHA));
+ if(i<0) break;
+ texmask |= 1<<s.sts[i].type;
+ s.sts[i].combined = index;
+ addname(key, s, s.sts[i], true);
+ break;
+ }
+ }
+ key.add('\0');
+ t.t = textures.access(key.getbuf());
+ if(t.t) return;
+ int compress = 0, wrap = 0;
+ ImageData ts;
+ if(!texturedata(ts, NULL, &t, true, &compress, &wrap)) { t.t = notexture; return; }
+ if(!ts.compressed) switch(t.type)
+ {
+ case TEX_DIFFUSE:
+ case TEX_NORMAL:
+ loopv(s.sts)
+ {
+ Slot::Tex &a = s.sts[i];
+ if(a.combined!=index) continue;
+ ImageData as;
+ if(!texturedata(as, NULL, &a)) continue;
+ //if(ts.bpp!=4) forcergbaimage(ts);
+ if(as.w!=ts.w || as.h!=ts.h) scaleimage(as, ts.w, ts.h);
+ switch(a.type)
+ {
+ case TEX_SPEC: mergespec(ts, as); break;
+ case TEX_DEPTH: mergedepth(ts, as); break;
+ case TEX_ALPHA: mergealpha(ts, as); break;
+ }
+ break; // only one combination
+ }
+ break;
+ }
+ t.t = newtexture(NULL, key.getbuf(), ts, wrap, true, true, true, compress);
+}
+
+static Slot &loadslot(Slot &s, bool forceload)
+{
+ linkslotshader(s);
+ loopv(s.sts)
+ {
+ Slot::Tex &t = s.sts[i];
+ if(t.combined >= 0) continue;
+ switch(t.type)
+ {
+ case TEX_ENVMAP:
+ t.t = cubemapload(t.name);
+ break;
+
+ default:
+ texcombine(s, i, t, forceload);
+ break;
+ }
+ }
+ s.loaded = true;
+ return s;
+}
+
+MSlot &lookupmaterialslot(int index, bool load)
+{
+ if(materialslots[index].sts.empty() && index&MATF_INDEX) index &= ~MATF_INDEX;
+ MSlot &s = materialslots[index];
+ if(load && !s.linked)
+ {
+ if(!s.loaded) loadslot(s, true);
+ linkvslotshader(s);
+ s.linked = true;
+ }
+ return s;
+}
+
+Slot &lookupslot(int index, bool load)
+{
+ Slot &s = slots.inrange(index) ? *slots[index] : (slots.inrange(DEFAULT_GEOM) ? *slots[DEFAULT_GEOM] : dummyslot);
+ return s.loaded || !load ? s : loadslot(s, false);
+}
+
+VSlot &lookupvslot(int index, bool load)
+{
+ VSlot &s = vslots.inrange(index) && vslots[index]->slot ? *vslots[index] : (slots.inrange(DEFAULT_GEOM) && slots[DEFAULT_GEOM]->variants ? *slots[DEFAULT_GEOM]->variants : dummyvslot);
+ if(load && !s.linked)
+ {
+ if(!s.slot->loaded) loadslot(*s.slot, false);
+ linkvslotshader(s);
+ s.linked = true;
+ }
+ return s;
+}
+
+void linkslotshaders()
+{
+ loopv(slots) if(slots[i]->loaded) linkslotshader(*slots[i]);
+ loopv(vslots) if(vslots[i]->linked) linkvslotshader(*vslots[i]);
+ loopi((MATF_VOLUME|MATF_INDEX)+1) if(materialslots[i].loaded)
+ {
+ linkslotshader(materialslots[i]);
+ linkvslotshader(materialslots[i]);
+ }
+}
+
+Texture *loadthumbnail(Slot &slot)
+{
+ if(slot.thumbnail) return slot.thumbnail;
+ if(!slot.variants)
+ {
+ slot.thumbnail = notexture;
+ return slot.thumbnail;
+ }
+ VSlot &vslot = *slot.variants;
+ linkslotshader(slot, false);
+ linkvslotshader(vslot, false);
+ vector<char> name;
+ if(vslot.colorscale == vec(1, 1, 1)) addname(name, slot, slot.sts[0], false, "<thumbnail>");
+ else
+ {
+ defformatstring(prefix, "<thumbnail:%.2f/%.2f/%.2f>", vslot.colorscale.x, vslot.colorscale.y, vslot.colorscale.z);
+ addname(name, slot, slot.sts[0], false, prefix);
+ }
+ int glow = -1;
+ if(slot.texmask&(1<<TEX_GLOW))
+ {
+ loopvj(slot.sts) if(slot.sts[j].type==TEX_GLOW) { glow = j; break; }
+ if(glow >= 0)
+ {
+ defformatstring(prefix, "<glow:%.2f/%.2f/%.2f>", vslot.glowcolor.x, vslot.glowcolor.y, vslot.glowcolor.z);
+ addname(name, slot, slot.sts[glow], true, prefix);
+ }
+ }
+ VSlot *layer = vslot.layer ? &lookupvslot(vslot.layer, false) : NULL;
+ if(layer)
+ {
+ if(layer->colorscale == vec(1, 1, 1)) addname(name, *layer->slot, layer->slot->sts[0], true, "<layer>");
+ else
+ {
+ defformatstring(prefix, "<layer:%.2f/%.2f/%.2f>", vslot.colorscale.x, vslot.colorscale.y, vslot.colorscale.z);
+ addname(name, *layer->slot, layer->slot->sts[0], true, prefix);
+ }
+ }
+ name.add('\0');
+ Texture *t = textures.access(path(name.getbuf()));
+ if(t) slot.thumbnail = t;
+ else
+ {
+ ImageData s, g, l;
+ texturedata(s, NULL, &slot.sts[0], false);
+ if(glow >= 0) texturedata(g, NULL, &slot.sts[glow], false);
+ if(layer) texturedata(l, NULL, &layer->slot->sts[0], false);
+ if(!s.data) t = slot.thumbnail = notexture;
+ else
+ {
+ if(vslot.colorscale != vec(1, 1, 1)) texmad(s, vslot.colorscale, vec(0, 0, 0));
+ int xs = s.w, ys = s.h;
+ if(s.w > 64 || s.h > 64) scaleimage(s, min(s.w, 64), min(s.h, 64));
+ if(g.data)
+ {
+ if(g.w != s.w || g.h != s.h) scaleimage(g, s.w, s.h);
+ addglow(s, g, vslot.glowcolor);
+ }
+ if(l.data)
+ {
+ if(layer->colorscale != vec(1, 1, 1)) texmad(l, layer->colorscale, vec(0, 0, 0));
+ if(l.w != s.w/2 || l.h != s.h/2) scaleimage(l, s.w/2, s.h/2);
+ forcergbimage(s);
+ forcergbimage(l);
+ uchar *dstrow = &s.data[s.pitch*l.h + s.bpp*l.w], *srcrow = l.data;
+ loop(y, l.h)
+ {
+ for(uchar *dst = dstrow, *src = srcrow, *end = &srcrow[l.w*l.bpp]; src < end; dst += s.bpp, src += l.bpp)
+ loopk(3) dst[k] = src[k];
+ dstrow += s.pitch;
+ srcrow += l.pitch;
+ }
+ }
+ if(s.bpp < 3) forcergbimage(s);
+ t = newtexture(NULL, name.getbuf(), s, 0, false, false, true);
+ t->xs = xs;
+ t->ys = ys;
+ slot.thumbnail = t;
+ }
+ }
+ return t;
+}
+
+void loadlayermasks()
+{
+ loopv(slots)
+ {
+ Slot &slot = *slots[i];
+ if(slot.loaded && slot.layermaskname && !slot.layermask)
+ {
+ slot.layermask = new ImageData;
+ texturedata(*slot.layermask, slot.layermaskname);
+ if(!slot.layermask->data) DELETEP(slot.layermask);
+ }
+ }
+}
+
+// environment mapped reflections
+
+void forcecubemapload(GLuint tex)
+{
+ extern int ati_cubemap_bug;
+ if(!ati_cubemap_bug || !tex) return;
+
+ SETSHADER(cubemap);
+ GLenum depthtest = glIsEnabled(GL_DEPTH_TEST), blend = glIsEnabled(GL_BLEND);
+ if(depthtest) glDisable(GL_DEPTH_TEST);
+ glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
+ if(!blend) glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ gle::defvertex(2);
+ gle::deftexcoord0(3);
+ gle::defcolor(4);
+ gle::begin(GL_LINES);
+ loopi(2)
+ {
+ gle::attribf(i*1e-3f, 0);
+ gle::attribf(0, 0, 1);
+ gle::attribf(1, 1, 1, 0);
+ }
+ gle::end();
+ if(!blend) glDisable(GL_BLEND);
+ if(depthtest) glEnable(GL_DEPTH_TEST);
+}
+
+extern const cubemapside cubemapsides[6] =
+{
+ { GL_TEXTURE_CUBE_MAP_NEGATIVE_X, "lf", true, true, true },
+ { GL_TEXTURE_CUBE_MAP_POSITIVE_X, "rt", false, false, true },
+ { GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, "ft", true, false, false },
+ { GL_TEXTURE_CUBE_MAP_POSITIVE_Y, "bk", false, true, false },
+ { GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, "dn", false, false, true },
+ { GL_TEXTURE_CUBE_MAP_POSITIVE_Z, "up", false, false, true },
+};
+
+VARFP(envmapsize, 4, 7, 10, setupmaterials());
+
+Texture *cubemaploadwildcard(Texture *t, const char *name, bool mipit, bool msg, bool transient = false)
+{
+ string tname;
+ if(!name) copystring(tname, t->name);
+ else
+ {
+ copystring(tname, name);
+ t = textures.access(path(tname));
+ if(t)
+ {
+ if(!transient && t->type&Texture::TRANSIENT) t->type &= ~Texture::TRANSIENT;
+ return t;
+ }
+ }
+ char *wildcard = strchr(tname, '*');
+ ImageData surface[6];
+ string sname;
+ if(!wildcard) copystring(sname, tname);
+ int tsize = 0, compress = 0;
+ loopi(6)
+ {
+ if(wildcard)
+ {
+ copystring(sname, stringslice(tname, wildcard));
+ concatstring(sname, cubemapsides[i].name);
+ concatstring(sname, wildcard+1);
+ }
+ ImageData &s = surface[i];
+ texturedata(s, sname, NULL, msg, &compress);
+ if(!s.data) return NULL;
+ if(s.w != s.h)
+ {
+ if(msg) conoutf(CON_ERROR, "cubemap texture %s does not have square size", sname);
+ return NULL;
+ }
+ if(s.compressed ? s.compressed!=surface[0].compressed || s.w!=surface[0].w || s.h!=surface[0].h || s.levels!=surface[0].levels : surface[0].compressed || s.bpp!=surface[0].bpp)
+ {
+ if(msg) conoutf(CON_ERROR, "cubemap texture %s doesn't match other sides' format", sname);
+ return NULL;
+ }
+ tsize = max(tsize, max(s.w, s.h));
+ }
+ if(name)
+ {
+ char *key = newstring(tname);
+ t = &textures[key];
+ t->name = key;
+ }
+ t->type = Texture::CUBEMAP;
+ if(transient) t->type |= Texture::TRANSIENT;
+ GLenum format;
+ if(surface[0].compressed)
+ {
+ format = uncompressedformat(surface[0].compressed);
+ t->bpp = formatsize(format);
+ t->type |= Texture::COMPRESSED;
+ }
+ else
+ {
+ format = texformat(surface[0].bpp, true);
+ t->bpp = surface[0].bpp;
+ if(hasTRG && !hasTSW && swizzlemask(format))
+ {
+ loopi(6) swizzleimage(surface[i]);
+ format = texformat(surface[0].bpp, true);
+ t->bpp = surface[0].bpp;
+ }
+ }
+ if(alphaformat(format)) t->type |= Texture::ALPHA;
+ t->mipmap = mipit;
+ t->clamp = 3;
+ t->xs = t->ys = tsize;
+ t->w = t->h = min(1<<envmapsize, tsize);
+ resizetexture(t->w, t->h, mipit, false, GL_TEXTURE_CUBE_MAP, compress, t->w, t->h);
+ GLenum component = format;
+ if(!surface[0].compressed)
+ {
+ component = compressedformat(format, t->w, t->h, compress);
+ switch(component)
+ {
+ case GL_RGB: component = GL_RGB5; break;
+ }
+ }
+ glGenTextures(1, &t->id);
+ loopi(6)
+ {
+ ImageData &s = surface[i];
+ const cubemapside &side = cubemapsides[i];
+ texreorient(s, side.flipx, side.flipy, side.swapxy);
+ if(s.compressed)
+ {
+ int w = s.w, h = s.h, levels = s.levels, level = 0;
+ uchar *data = s.data;
+ while(levels > 1 && (w > t->w || h > t->h))
+ {
+ data += s.calclevelsize(level++);
+ levels--;
+ if(w > 1) w /= 2;
+ if(h > 1) h /= 2;
+ }
+ createcompressedtexture(t->id, w, h, data, s.align, s.bpp, levels, i ? -1 : 3, mipit ? 2 : 1, s.compressed, side.target, true);
+ }
+ else
+ {
+ createtexture(t->id, t->w, t->h, s.data, i ? -1 : 3, mipit ? 2 : 1, component, side.target, s.w, s.h, s.pitch, false, format, true);
+ }
+ }
+ forcecubemapload(t->id);
+ return t;
+}
+
+Texture *cubemapload(const char *name, bool mipit, bool msg, bool transient)
+{
+ string pname;
+ copystring(pname, makerelpath("packages", name));
+ path(pname);
+ Texture *t = NULL;
+ if(!strchr(pname, '*'))
+ {
+ defformatstring(jpgname, "%s_*.jpg", pname);
+ t = cubemaploadwildcard(NULL, jpgname, mipit, false, transient);
+ if(!t)
+ {
+ defformatstring(pngname, "%s_*.png", pname);
+ t = cubemaploadwildcard(NULL, pngname, mipit, false, transient);
+ if(!t && msg) conoutf(CON_ERROR, "could not load envmap %s", name);
+ }
+ }
+ else t = cubemaploadwildcard(NULL, pname, mipit, msg, transient);
+ return t;
+}
+
+VARR(envmapradius, 0, 128, 10000);
+VARR(envmapbb, 0, 0, 1);
+
+struct envmap
+{
+ int radius, size, blur;
+ vec o;
+ GLuint tex;
+
+ envmap() : radius(-1), size(0), blur(0), o(0, 0, 0), tex(0) {}
+
+ void clear()
+ {
+ if(tex) { glDeleteTextures(1, &tex); tex = 0; }
+ }
+};
+
+static vector<envmap> envmaps;
+static Texture *skyenvmap = NULL;
+
+void clearenvmaps()
+{
+ if(skyenvmap)
+ {
+ if(skyenvmap->type&Texture::TRANSIENT) cleanuptexture(skyenvmap);
+ skyenvmap = NULL;
+ }
+ loopv(envmaps) envmaps[i].clear();
+ envmaps.shrink(0);
+}
+
+VAR(aaenvmap, 0, 2, 4);
+
+GLuint genenvmap(const vec &o, int envmapsize, int blur, bool onlysky)
+{
+ int rendersize = 1<<(envmapsize+aaenvmap), sizelimit = min(hwcubetexsize, min(screenw, screenh));
+ if(maxtexsize) sizelimit = min(sizelimit, maxtexsize);
+ while(rendersize > sizelimit) rendersize /= 2;
+ int texsize = min(rendersize, 1<<envmapsize);
+ if(!aaenvmap) rendersize = texsize;
+ GLuint tex;
+ glGenTextures(1, &tex);
+ glViewport(0, 0, rendersize, rendersize);
+ float yaw = 0, pitch = 0;
+ uchar *pixels = new uchar[3*rendersize*rendersize*2];
+ glPixelStorei(GL_PACK_ALIGNMENT, texalign(pixels, rendersize, 3));
+ loopi(6)
+ {
+ const cubemapside &side = cubemapsides[i];
+ switch(side.target)
+ {
+ case GL_TEXTURE_CUBE_MAP_NEGATIVE_X: // lf
+ yaw = 90; pitch = 0; break;
+ case GL_TEXTURE_CUBE_MAP_POSITIVE_X: // rt
+ yaw = 270; pitch = 0; break;
+ case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: // ft
+ yaw = 180; pitch = 0; break;
+ case GL_TEXTURE_CUBE_MAP_POSITIVE_Y: // bk
+ yaw = 0; pitch = 0; break;
+ case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: // dn
+ yaw = 270; pitch = -90; break;
+ case GL_TEXTURE_CUBE_MAP_POSITIVE_Z: // up
+ yaw = 270; pitch = 90; break;
+ }
+ glFrontFace((side.flipx==side.flipy)!=side.swapxy ? GL_CW : GL_CCW);
+ drawcubemap(rendersize, o, yaw, pitch, side, onlysky);
+ uchar *src = pixels, *dst = &pixels[3*rendersize*rendersize];
+ glReadPixels(0, 0, rendersize, rendersize, GL_RGB, GL_UNSIGNED_BYTE, src);
+ if(rendersize > texsize)
+ {
+ scaletexture(src, rendersize, rendersize, 3, 3*rendersize, dst, texsize, texsize);
+ swap(src, dst);
+ }
+ if(blur > 0)
+ {
+ blurtexture(blur, 3, texsize, texsize, dst, src);
+ swap(src, dst);
+ }
+ createtexture(tex, texsize, texsize, src, 3, 2, GL_RGB5, side.target);
+ }
+ glFrontFace(GL_CW);
+ delete[] pixels;
+ glViewport(0, 0, screenw, screenh);
+ clientkeepalive();
+ forcecubemapload(tex);
+ return tex;
+}
+
+void initenvmaps()
+{
+ clearenvmaps();
+ skyenvmap = NULL;
+ if(shouldrenderskyenvmap()) envmaps.add().size = 1;
+ else if(skybox[0]) skyenvmap = cubemapload(skybox, true, false, true);
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents)
+ {
+ const extentity &ent = *ents[i];
+ if(ent.type != ET_ENVMAP) continue;
+ envmap &em = envmaps.add();
+ em.radius = ent.attr1 ? clamp(int(ent.attr1), 0, 10000) : envmapradius;
+ em.size = ent.attr2 ? clamp(int(ent.attr2), 4, 9) : 0;
+ em.blur = ent.attr3 ? clamp(int(ent.attr3), 1, 2) : 0;
+ em.o = ent.o;
+ }
+}
+
+void genenvmaps()
+{
+ if(envmaps.empty()) return;
+ renderprogress(0, "generating environment maps...");
+ int lastprogress = SDL_GetTicks();
+ loopv(envmaps)
+ {
+ envmap &em = envmaps[i];
+ em.tex = genenvmap(em.o, em.size ? min(em.size, envmapsize) : envmapsize, em.blur, em.radius < 0);
+ if(renderedframe) continue;
+ int millis = SDL_GetTicks();
+ if(millis - lastprogress >= 250)
+ {
+ renderprogress(float(i+1)/envmaps.length(), "generating environment maps...", 0, true);
+ lastprogress = millis;
+ }
+ }
+}
+
+ushort closestenvmap(const vec &o)
+{
+ ushort minemid = EMID_SKY;
+ float mindist = 1e16f;
+ loopv(envmaps)
+ {
+ envmap &em = envmaps[i];
+ float dist;
+ if(envmapbb)
+ {
+ if(!o.insidebb(vec(em.o).sub(em.radius), vec(em.o).add(em.radius))) continue;
+ dist = em.o.dist(o);
+ }
+ else
+ {
+ dist = em.o.dist(o);
+ if(dist > em.radius) continue;
+ }
+ if(dist < mindist)
+ {
+ minemid = EMID_RESERVED + i;
+ mindist = dist;
+ }
+ }
+ return minemid;
+}
+
+ushort closestenvmap(int orient, const ivec &co, int size)
+{
+ vec loc(co);
+ int dim = dimension(orient);
+ if(dimcoord(orient)) loc[dim] += size;
+ loc[R[dim]] += size/2;
+ loc[C[dim]] += size/2;
+ return closestenvmap(loc);
+}
+
+static inline GLuint lookupskyenvmap()
+{
+ return envmaps.length() && envmaps[0].radius < 0 ? envmaps[0].tex : (skyenvmap ? skyenvmap->id : 0);
+}
+
+GLuint lookupenvmap(Slot &slot)
+{
+ loopv(slot.sts) if(slot.sts[i].type==TEX_ENVMAP && slot.sts[i].t) return slot.sts[i].t->id;
+ return lookupskyenvmap();
+}
+
+GLuint lookupenvmap(ushort emid)
+{
+ if(emid==EMID_SKY || emid==EMID_CUSTOM) return skyenvmap ? skyenvmap->id : 0;
+ if(emid==EMID_NONE || !envmaps.inrange(emid-EMID_RESERVED)) return 0;
+ GLuint tex = envmaps[emid-EMID_RESERVED].tex;
+ return tex ? tex : lookupskyenvmap();
+}
+
+void cleanuptexture(Texture *t)
+{
+ DELETEA(t->alphamask);
+ if(t->id) { glDeleteTextures(1, &t->id); t->id = 0; }
+ if(t->type&Texture::TRANSIENT) textures.remove(t->name);
+}
+
+void cleanuptextures()
+{
+ cleanupmipmaps();
+ clearenvmaps();
+ loopv(slots) slots[i]->cleanup();
+ loopv(vslots) vslots[i]->cleanup();
+ loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].cleanup();
+ enumerate(textures, Texture, tex, cleanuptexture(&tex));
+}
+
+bool reloadtexture(const char *name)
+{
+ Texture *t = textures.access(path(name, true));
+ if(t) return reloadtexture(*t);
+ return true;
+}
+
+bool reloadtexture(Texture &tex)
+{
+ if(tex.id) return true;
+ switch(tex.type&Texture::TYPE)
+ {
+ case Texture::IMAGE:
+ {
+ int compress = 0;
+ ImageData s;
+ if(!texturedata(s, tex.name, NULL, true, &compress) || !newtexture(&tex, NULL, s, tex.clamp, tex.mipmap, false, false, compress)) return false;
+ break;
+ }
+
+ case Texture::CUBEMAP:
+ if(!cubemaploadwildcard(&tex, NULL, tex.mipmap, true)) return false;
+ break;
+ }
+ return true;
+}
+
+void reloadtex(char *name)
+{
+ Texture *t = textures.access(path(name, true));
+ if(!t) { conoutf(CON_ERROR, "texture %s is not loaded", name); return; }
+ if(t->type&Texture::TRANSIENT) { conoutf(CON_ERROR, "can't reload transient texture %s", name); return; }
+ DELETEA(t->alphamask);
+ Texture oldtex = *t;
+ t->id = 0;
+ if(!reloadtexture(*t))
+ {
+ if(t->id) glDeleteTextures(1, &t->id);
+ *t = oldtex;
+ conoutf(CON_ERROR, "failed to reload texture %s", name);
+ }
+}
+
+COMMAND(reloadtex, "s");
+
+void reloadtextures()
+{
+ int reloaded = 0;
+ enumerate(textures, Texture, tex,
+ {
+ loadprogress = float(++reloaded)/textures.numelems;
+ reloadtexture(tex);
+ });
+ loadprogress = 0;
+}
+
+enum
+{
+ DDSD_CAPS = 0x00000001,
+ DDSD_HEIGHT = 0x00000002,
+ DDSD_WIDTH = 0x00000004,
+ DDSD_PITCH = 0x00000008,
+ DDSD_PIXELFORMAT = 0x00001000,
+ DDSD_MIPMAPCOUNT = 0x00020000,
+ DDSD_LINEARSIZE = 0x00080000,
+ DDSD_BACKBUFFERCOUNT = 0x00800000,
+ DDPF_ALPHAPIXELS = 0x00000001,
+ DDPF_FOURCC = 0x00000004,
+ DDPF_INDEXED = 0x00000020,
+ DDPF_ALPHA = 0x00000002,
+ DDPF_RGB = 0x00000040,
+ DDPF_COMPRESSED = 0x00000080,
+ DDPF_LUMINANCE = 0x00020000,
+ DDSCAPS_COMPLEX = 0x00000008,
+ DDSCAPS_TEXTURE = 0x00001000,
+ DDSCAPS_MIPMAP = 0x00400000,
+ DDSCAPS2_CUBEMAP = 0x00000200,
+ DDSCAPS2_CUBEMAP_POSITIVEX = 0x00000400,
+ DDSCAPS2_CUBEMAP_NEGATIVEX = 0x00000800,
+ DDSCAPS2_CUBEMAP_POSITIVEY = 0x00001000,
+ DDSCAPS2_CUBEMAP_NEGATIVEY = 0x00002000,
+ DDSCAPS2_CUBEMAP_POSITIVEZ = 0x00004000,
+ DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x00008000,
+ DDSCAPS2_VOLUME = 0x00200000,
+ FOURCC_DXT1 = 0x31545844,
+ FOURCC_DXT2 = 0x32545844,
+ FOURCC_DXT3 = 0x33545844,
+ FOURCC_DXT4 = 0x34545844,
+ FOURCC_DXT5 = 0x35545844,
+ FOURCC_ATI1 = 0x31495441,
+ FOURCC_ATI2 = 0x32495441
+};
+
+struct DDCOLORKEY { uint dwColorSpaceLowValue, dwColorSpaceHighValue; };
+struct DDPIXELFORMAT
+{
+ uint dwSize, dwFlags, dwFourCC;
+ union { uint dwRGBBitCount, dwYUVBitCount, dwZBufferBitDepth, dwAlphaBitDepth, dwLuminanceBitCount, dwBumpBitCount, dwPrivateFormatBitCount; };
+ union { uint dwRBitMask, dwYBitMask, dwStencilBitDepth, dwLuminanceBitMask, dwBumpDuBitMask, dwOperations; };
+ union { uint dwGBitMask, dwUBitMask, dwZBitMask, dwBumpDvBitMask; struct { ushort wFlipMSTypes, wBltMSTypes; } MultiSampleCaps; };
+ union { uint dwBBitMask, dwVBitMask, dwStencilBitMask, dwBumpLuminanceBitMask; };
+ union { uint dwRGBAlphaBitMask, dwYUVAlphaBitMask, dwLuminanceAlphaBitMask, dwRGBZBitMask, dwYUVZBitMask; };
+
+};
+struct DDSCAPS2 { uint dwCaps, dwCaps2, dwCaps3, dwCaps4; };
+struct DDSURFACEDESC2
+{
+ uint dwSize, dwFlags, dwHeight, dwWidth;
+ union { int lPitch; uint dwLinearSize; };
+ uint dwBackBufferCount;
+ union { uint dwMipMapCount, dwRefreshRate, dwSrcVBHandle; };
+ uint dwAlphaBitDepth, dwReserved, lpSurface;
+ union { DDCOLORKEY ddckCKDestOverlay; uint dwEmptyFaceColor; };
+ DDCOLORKEY ddckCKDestBlt, ddckCKSrcOverlay, ddckCKSrcBlt;
+ union { DDPIXELFORMAT ddpfPixelFormat; uint dwFVF; };
+ DDSCAPS2 ddsCaps;
+ uint dwTextureStage;
+};
+
+#define DECODEDDS(name, dbpp, initblock, writeval, nextval) \
+static void name(ImageData &s) \
+{ \
+ ImageData d(s.w, s.h, dbpp); \
+ uchar *dst = d.data; \
+ const uchar *src = s.data; \
+ for(int by = 0; by < s.h; by += s.align) \
+ { \
+ for(int bx = 0; bx < s.w; bx += s.align, src += s.bpp) \
+ { \
+ int maxy = min(d.h - by, s.align), maxx = min(d.w - bx, s.align); \
+ initblock; \
+ loop(y, maxy) \
+ { \
+ int x; \
+ for(x = 0; x < maxx; ++x) \
+ { \
+ writeval; \
+ nextval; \
+ dst += d.bpp; \
+ } \
+ for(; x < s.align; ++x) { nextval; } \
+ dst += d.pitch - maxx*d.bpp; \
+ } \
+ dst += maxx*d.bpp - maxy*d.pitch; \
+ } \
+ dst += (s.align-1)*d.pitch; \
+ } \
+ s.replace(d); \
+}
+
+DECODEDDS(decodedxt1, s.compressed == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 4 : 3,
+ ushort color0 = lilswap(*(const ushort *)src);
+ ushort color1 = lilswap(*(const ushort *)&src[2]);
+ uint bits = lilswap(*(const uint *)&src[4]);
+ bvec4 rgba[4];
+ rgba[0] = bvec4(bvec::from565(color0), 0xFF);
+ rgba[1] = bvec4(bvec::from565(color1), 0xFF);
+ if(color0 > color1)
+ {
+ rgba[2].lerp(rgba[0], rgba[1], 2, 1, 3);
+ rgba[3].lerp(rgba[0], rgba[1], 1, 2, 3);
+ }
+ else
+ {
+ rgba[2].lerp(rgba[0], rgba[1], 1, 1, 2);
+ rgba[3] = bvec4(0, 0, 0, 0);
+ }
+,
+ memcpy(dst, rgba[bits&3].v, d.bpp);
+,
+ bits >>= 2;
+);
+
+DECODEDDS(decodedxt3, 4,
+ ullong alpha = lilswap(*(const ullong *)src);
+ ushort color0 = lilswap(*(const ushort *)&src[8]);
+ ushort color1 = lilswap(*(const ushort *)&src[10]);
+ uint bits = lilswap(*(const uint *)&src[12]);
+ bvec rgb[4];
+ rgb[0] = bvec::from565(color0);
+ rgb[1] = bvec::from565(color1);
+ rgb[2].lerp(rgb[0], rgb[1], 2, 1, 3);
+ rgb[3].lerp(rgb[0], rgb[1], 1, 2, 3);
+,
+ memcpy(dst, rgb[bits&3].v, 3);
+ dst[3] = ((alpha&0xF)*1088 + 32) >> 6;
+,
+ bits >>= 2;
+ alpha >>= 4;
+);
+
+static inline void decodealpha(uchar alpha0, uchar alpha1, uchar alpha[8])
+{
+ alpha[0] = alpha0;
+ alpha[1] = alpha1;
+ if(alpha0 > alpha1)
+ {
+ alpha[2] = (6*alpha0 + alpha1)/7;
+ alpha[3] = (5*alpha0 + 2*alpha1)/7;
+ alpha[4] = (4*alpha0 + 3*alpha1)/7;
+ alpha[5] = (3*alpha0 + 4*alpha1)/7;
+ alpha[6] = (2*alpha0 + 5*alpha1)/7;
+ alpha[7] = (alpha0 + 6*alpha1)/7;
+ }
+ else
+ {
+ alpha[2] = (4*alpha0 + alpha1)/5;
+ alpha[3] = (3*alpha0 + 2*alpha1)/5;
+ alpha[4] = (2*alpha0 + 3*alpha1)/5;
+ alpha[5] = (alpha0 + 4*alpha1)/5;
+ alpha[6] = 0;
+ alpha[7] = 0xFF;
+ }
+}
+
+DECODEDDS(decodedxt5, 4,
+ uchar alpha[8];
+ decodealpha(src[0], src[1], alpha);
+ ullong alphabits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16);
+ ushort color0 = lilswap(*(const ushort *)&src[8]);
+ ushort color1 = lilswap(*(const ushort *)&src[10]);
+ uint bits = lilswap(*(const uint *)&src[12]);
+ bvec rgb[4];
+ rgb[0] = bvec::from565(color0);
+ rgb[1] = bvec::from565(color1);
+ rgb[2].lerp(rgb[0], rgb[1], 2, 1, 3);
+ rgb[3].lerp(rgb[0], rgb[1], 1, 2, 3);
+,
+ memcpy(dst, rgb[bits&3].v, 3);
+ dst[3] = alpha[alphabits&7];
+,
+ bits >>= 2;
+ alphabits >>= 3;
+);
+
+DECODEDDS(decodergtc1, 1,
+ uchar red[8];
+ decodealpha(src[0], src[1], red);
+ ullong redbits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16);
+,
+ dst[0] = red[redbits&7];
+,
+ redbits >>= 3;
+);
+
+DECODEDDS(decodergtc2, 2,
+ uchar red[8];
+ decodealpha(src[0], src[1], red);
+ ullong redbits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16);
+ uchar green[8];
+ decodealpha(src[8], src[9], green);
+ ullong greenbits = lilswap(*(const ushort *)&src[10]) + ((ullong)lilswap(*(const uint *)&src[12]) << 16);
+,
+ dst[0] = red[redbits&7];
+ dst[1] = green[greenbits&7];
+,
+ redbits >>= 3;
+ greenbits >>= 3;
+);
+
+bool loaddds(const char *filename, ImageData &image, int force)
+{
+ stream *f = openfile(filename, "rb");
+ if(!f) return false;
+ GLenum format = GL_FALSE;
+ uchar magic[4];
+ if(f->read(magic, 4) != 4 || memcmp(magic, "DDS ", 4)) { delete f; return false; }
+ DDSURFACEDESC2 d;
+ if(f->read(&d, sizeof(d)) != sizeof(d)) { delete f; return false; }
+ lilswap((uint *)&d, sizeof(d)/sizeof(uint));
+ if(d.dwSize != sizeof(DDSURFACEDESC2) || d.ddpfPixelFormat.dwSize != sizeof(DDPIXELFORMAT)) { delete f; return false; }
+ bool supported = false;
+ if(d.ddpfPixelFormat.dwFlags & DDPF_FOURCC)
+ {
+ switch(d.ddpfPixelFormat.dwFourCC)
+ {
+ case FOURCC_DXT1:
+ if((supported = hasS3TC) || force) format = d.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
+ break;
+ case FOURCC_DXT2:
+ case FOURCC_DXT3:
+ if((supported = hasS3TC) || force) format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
+ break;
+ case FOURCC_DXT4:
+ case FOURCC_DXT5:
+ if((supported = hasS3TC) || force) format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
+ break;
+ case FOURCC_ATI1:
+ if((supported = hasRGTC) || force) format = GL_COMPRESSED_RED_RGTC1;
+ else if((supported = hasLATC)) format = GL_COMPRESSED_LUMINANCE_LATC1_EXT;
+ break;
+ case FOURCC_ATI2:
+ if((supported = hasRGTC) || force) format = GL_COMPRESSED_RG_RGTC2;
+ else if((supported = hasLATC)) format = GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT;
+ break;
+ }
+ }
+ if(!format || (!supported && !force)) { delete f; return false; }
+ if(dbgdds) conoutf(CON_DEBUG, "%s: format 0x%X, %d x %d, %d mipmaps", filename, format, d.dwWidth, d.dwHeight, d.dwMipMapCount);
+ int bpp = 0;
+ switch(format)
+ {
+ case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: bpp = 8; break;
+ case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: bpp = 16; break;
+ case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+ case GL_COMPRESSED_RED_RGTC1: bpp = 8; break;
+ case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+ case GL_COMPRESSED_RG_RGTC2: bpp = 16; break;
+ }
+ image.setdata(NULL, d.dwWidth, d.dwHeight, bpp, !supported || force > 0 ? 1 : d.dwMipMapCount, 4, format);
+ size_t size = image.calcsize();
+ if(f->read(image.data, size) != size) { delete f; image.cleanup(); return false; }
+ delete f;
+ if(!supported || force > 0) switch(format)
+ {
+ case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+ case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+ decodedxt1(image);
+ break;
+ case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+ decodedxt3(image);
+ break;
+ case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+ decodedxt5(image);
+ break;
+ case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+ case GL_COMPRESSED_RED_RGTC1:
+ decodergtc1(image);
+ break;
+ case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+ case GL_COMPRESSED_RG_RGTC2:
+ decodergtc2(image);
+ break;
+ }
+ return true;
+}
+
+void gendds(char *infile, char *outfile)
+{
+ if(!hasS3TC || usetexcompress <= 1) { conoutf(CON_ERROR, "OpenGL driver does not support S3TC texture compression"); return; }
+
+ glHint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST);
+
+ defformatstring(cfile, "<compress>%s", infile);
+ extern void reloadtex(char *name);
+ Texture *t = textures.access(path(cfile));
+ if(t) reloadtex(cfile);
+ t = textureload(cfile);
+ if(t==notexture) { conoutf(CON_ERROR, "failed loading %s", infile); return; }
+
+ glBindTexture(GL_TEXTURE_2D, t->id);
+ GLint compressed = 0, format = 0, width = 0, height = 0;
+ glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed);
+ glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format);
+ glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
+ glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);
+
+ if(!compressed) { conoutf(CON_ERROR, "failed compressing %s", infile); return; }
+ int fourcc = 0;
+ switch(format)
+ {
+ case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: fourcc = FOURCC_DXT1; conoutf("compressed as DXT1"); break;
+ case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: fourcc = FOURCC_DXT1; conoutf("compressed as DXT1a"); break;
+ case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: fourcc = FOURCC_DXT3; conoutf("compressed as DXT3"); break;
+ case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: fourcc = FOURCC_DXT5; conoutf("compressed as DXT5"); break;
+ case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+ case GL_COMPRESSED_RED_RGTC1: fourcc = FOURCC_ATI1; conoutf("compressed as ATI1"); break;
+ case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+ case GL_COMPRESSED_RG_RGTC2: fourcc = FOURCC_ATI2; conoutf("compressed as ATI2"); break;
+ default:
+ conoutf(CON_ERROR, "failed compressing %s: unknown format: 0x%X", infile, format); break;
+ return;
+ }
+
+ if(!outfile[0])
+ {
+ static string buf;
+ copystring(buf, infile);
+ int len = strlen(buf);
+ if(len > 4 && buf[len-4]=='.') memcpy(&buf[len-4], ".dds", 4);
+ else concatstring(buf, ".dds");
+ outfile = buf;
+ }
+
+ stream *f = openfile(path(outfile, true), "wb");
+ if(!f) { conoutf(CON_ERROR, "failed writing to %s", outfile); return; }
+
+ int csize = 0;
+ for(int lw = width, lh = height, level = 0;;)
+ {
+ GLint size = 0;
+ glGetTexLevelParameteriv(GL_TEXTURE_2D, level++, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &size);
+ csize += size;
+ if(max(lw, lh) <= 1) break;
+ if(lw > 1) lw /= 2;
+ if(lh > 1) lh /= 2;
+ }
+
+ DDSURFACEDESC2 d;
+ memset(&d, 0, sizeof(d));
+ d.dwSize = sizeof(DDSURFACEDESC2);
+ d.dwWidth = width;
+ d.dwHeight = height;
+ d.dwLinearSize = csize;
+ d.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE | DDSD_MIPMAPCOUNT;
+ d.ddsCaps.dwCaps = DDSCAPS_TEXTURE | DDSCAPS_COMPLEX | DDSCAPS_MIPMAP;
+ d.ddpfPixelFormat.dwSize = sizeof(DDPIXELFORMAT);
+ d.ddpfPixelFormat.dwFlags = DDPF_FOURCC | (alphaformat(uncompressedformat(format)) ? DDPF_ALPHAPIXELS : 0);
+ d.ddpfPixelFormat.dwFourCC = fourcc;
+
+ uchar *data = new uchar[csize], *dst = data;
+ for(int lw = width, lh = height;;)
+ {
+ GLint size;
+ glGetTexLevelParameteriv(GL_TEXTURE_2D, d.dwMipMapCount, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &size);
+ glGetCompressedTexImage_(GL_TEXTURE_2D, d.dwMipMapCount++, dst);
+ dst += size;
+ if(max(lw, lh) <= 1) break;
+ if(lw > 1) lw /= 2;
+ if(lh > 1) lh /= 2;
+ }
+
+ lilswap((uint *)&d, sizeof(d)/sizeof(uint));
+
+ f->write("DDS ", 4);
+ f->write(&d, sizeof(d));
+ f->write(data, csize);
+ delete f;
+
+ delete[] data;
+
+ conoutf("wrote DDS file %s", outfile);
+
+ setuptexcompress();
+}
+COMMAND(gendds, "ss");
+
+void writepngchunk(stream *f, const char *type, uchar *data = NULL, uint len = 0)
+{
+ f->putbig<uint>(len);
+ f->write(type, 4);
+ f->write(data, len);
+
+ uint crc = crc32(0, Z_NULL, 0);
+ crc = crc32(crc, (const Bytef *)type, 4);
+ if(data) crc = crc32(crc, data, len);
+ f->putbig<uint>(crc);
+}
+
+VARP(compresspng, 0, 9, 9);
+
+void savepng(const char *filename, ImageData &image, bool flip)
+{
+ uchar ctype = 0;
+ switch(image.bpp)
+ {
+ case 1: ctype = 0; break;
+ case 2: ctype = 4; break;
+ case 3: ctype = 2; break;
+ case 4: ctype = 6; break;
+ default: conoutf(CON_ERROR, "failed saving png to %s", filename); return;
+ }
+ stream *f = openfile(filename, "wb");
+ if(!f) { conoutf(CON_ERROR, "could not write to %s", filename); return; }
+
+ uchar signature[] = { 137, 80, 78, 71, 13, 10, 26, 10 };
+ f->write(signature, sizeof(signature));
+
+ struct pngihdr
+ {
+ uint width, height;
+ uchar bitdepth, colortype, compress, filter, interlace;
+ } ihdr = { bigswap<uint>(image.w), bigswap<uint>(image.h), 8, ctype, 0, 0, 0 };
+ writepngchunk(f, "IHDR", (uchar *)&ihdr, 13);
+
+ stream::offset idat = f->tell();
+ uint len = 0;
+ f->write("\0\0\0\0IDAT", 8);
+ uint crc = crc32(0, Z_NULL, 0);
+ crc = crc32(crc, (const Bytef *)"IDAT", 4);
+
+ z_stream z;
+ z.zalloc = NULL;
+ z.zfree = NULL;
+ z.opaque = NULL;
+
+ if(deflateInit(&z, compresspng) != Z_OK)
+ goto error;
+
+ uchar buf[1<<12];
+ z.next_out = (Bytef *)buf;
+ z.avail_out = sizeof(buf);
+
+ loopi(image.h)
+ {
+ uchar filter = 0;
+ loopj(2)
+ {
+ z.next_in = j ? (Bytef *)image.data + (flip ? image.h-i-1 : i)*image.pitch : (Bytef *)&filter;
+ z.avail_in = j ? image.w*image.bpp : 1;
+ while(z.avail_in > 0)
+ {
+ if(deflate(&z, Z_NO_FLUSH) != Z_OK) goto cleanuperror;
+ #define FLUSHZ do { \
+ int flush = sizeof(buf) - z.avail_out; \
+ crc = crc32(crc, buf, flush); \
+ len += flush; \
+ f->write(buf, flush); \
+ z.next_out = (Bytef *)buf; \
+ z.avail_out = sizeof(buf); \
+ } while(0)
+ FLUSHZ;
+ }
+ }
+ }
+
+ for(;;)
+ {
+ int err = deflate(&z, Z_FINISH);
+ if(err != Z_OK && err != Z_STREAM_END) goto cleanuperror;
+ FLUSHZ;
+ if(err == Z_STREAM_END) break;
+ }
+
+ deflateEnd(&z);
+
+ f->seek(idat, SEEK_SET);
+ f->putbig<uint>(len);
+ f->seek(0, SEEK_END);
+ f->putbig<uint>(crc);
+
+ writepngchunk(f, "IEND");
+
+ delete f;
+ return;
+
+cleanuperror:
+ deflateEnd(&z);
+
+error:
+ delete f;
+
+ conoutf(CON_ERROR, "failed saving png to %s", filename);
+}
+
+struct tgaheader
+{
+ uchar identsize;
+ uchar cmaptype;
+ uchar imagetype;
+ uchar cmaporigin[2];
+ uchar cmapsize[2];
+ uchar cmapentrysize;
+ uchar xorigin[2];
+ uchar yorigin[2];
+ uchar width[2];
+ uchar height[2];
+ uchar pixelsize;
+ uchar descbyte;
+};
+
+VARP(compresstga, 0, 1, 1);
+
+void savetga(const char *filename, ImageData &image, bool flip)
+{
+ switch(image.bpp)
+ {
+ case 3: case 4: break;
+ default: conoutf(CON_ERROR, "failed saving tga to %s", filename); return;
+ }
+
+ stream *f = openfile(filename, "wb");
+ if(!f) { conoutf(CON_ERROR, "could not write to %s", filename); return; }
+
+ tgaheader hdr;
+ memset(&hdr, 0, sizeof(hdr));
+ hdr.pixelsize = image.bpp*8;
+ hdr.width[0] = image.w&0xFF;
+ hdr.width[1] = (image.w>>8)&0xFF;
+ hdr.height[0] = image.h&0xFF;
+ hdr.height[1] = (image.h>>8)&0xFF;
+ hdr.imagetype = compresstga ? 10 : 2;
+ f->write(&hdr, sizeof(hdr));
+
+ uchar buf[128*4];
+ loopi(image.h)
+ {
+ uchar *src = image.data + (flip ? i : image.h - i - 1)*image.pitch;
+ for(int remaining = image.w; remaining > 0;)
+ {
+ int raw = 1;
+ if(compresstga)
+ {
+ int run = 1;
+ for(uchar *scan = src; run < min(remaining, 128); run++)
+ {
+ scan += image.bpp;
+ if(src[0]!=scan[0] || src[1]!=scan[1] || src[2]!=scan[2] || (image.bpp==4 && src[3]!=scan[3])) break;
+ }
+ if(run > 1)
+ {
+ f->putchar(0x80 | (run-1));
+ f->putchar(src[2]); f->putchar(src[1]); f->putchar(src[0]);
+ if(image.bpp==4) f->putchar(src[3]);
+ src += run*image.bpp;
+ remaining -= run;
+ if(remaining <= 0) break;
+ }
+ for(uchar *scan = src; raw < min(remaining, 128); raw++)
+ {
+ scan += image.bpp;
+ if(src[0]==scan[0] && src[1]==scan[1] && src[2]==scan[2] && (image.bpp!=4 || src[3]==scan[3])) break;
+ }
+ f->putchar(raw - 1);
+ }
+ else raw = min(remaining, 128);
+ uchar *dst = buf;
+ loopj(raw)
+ {
+ dst[0] = src[2];
+ dst[1] = src[1];
+ dst[2] = src[0];
+ if(image.bpp==4) dst[3] = src[3];
+ dst += image.bpp;
+ src += image.bpp;
+ }
+ f->write(buf, raw*image.bpp);
+ remaining -= raw;
+ }
+ }
+
+ delete f;
+}
+
+enum
+{
+ IMG_BMP = 0,
+ IMG_TGA = 1,
+ IMG_PNG = 2,
+ IMG_JPG = 3,
+ NUMIMG
+};
+
+VARP(screenshotquality, 0, 97, 100);
+VARP(screenshotformat, 0, IMG_PNG, NUMIMG-1);
+
+const char *imageexts[NUMIMG] = { ".bmp", ".tga", ".png", ".jpg" };
+
+int guessimageformat(const char *filename, int format = IMG_BMP)
+{
+ int len = strlen(filename);
+ loopi(NUMIMG)
+ {
+ int extlen = strlen(imageexts[i]);
+ if(len >= extlen && !strcasecmp(&filename[len-extlen], imageexts[i])) return i;
+ }
+ return format;
+}
+
+void saveimage(const char *filename, int format, ImageData &image, bool flip = false)
+{
+ switch(format)
+ {
+ case IMG_PNG: savepng(filename, image, flip); break;
+ case IMG_TGA: savetga(filename, image, flip); break;
+ default:
+ {
+ ImageData flipped(image.w, image.h, image.bpp, image.data);
+ if(flip) texflip(flipped);
+ SDL_Surface *s = wrapsurface(flipped.data, flipped.w, flipped.h, flipped.bpp);
+ if(!s) break;
+ stream *f = openfile(filename, "wb");
+ if(f)
+ {
+ switch(format) {
+ case IMG_JPG:
+#if SDL_IMAGE_VERSION_ATLEAST(2, 0, 2)
+ IMG_SaveJPG_RW(s, f->rwops(), 1, screenshotquality);
+#else
+ conoutf(CON_ERROR, "JPG screenshot support requires SDL_image 2.0.2");
+#endif
+ break;
+ default: SDL_SaveBMP_RW(s, f->rwops(), 1); break;
+ }
+ delete f;
+ }
+ SDL_FreeSurface(s);
+ break;
+ }
+ }
+}
+
+bool loadimage(const char *filename, ImageData &image)
+{
+ SDL_Surface *s = loadsurface(path(filename, true));
+ if(!s) return false;
+ image.wrap(s);
+ return true;
+}
+
+SVARP(screenshotdir, "screenshot");
+
+void screenshot(char *filename)
+{
+ static string buf;
+ int format = -1, dirlen = 0;
+ copystring(buf, screenshotdir);
+ if(screenshotdir[0])
+ {
+ dirlen = strlen(buf);
+ if(buf[dirlen] != '/' && buf[dirlen] != '\\' && dirlen+1 < (int)sizeof(buf)) { buf[dirlen++] = '/'; buf[dirlen] = '\0'; }
+ const char *dir = findfile(buf, "w");
+ if(!fileexists(dir, "w")) createdir(dir);
+ }
+ if(filename[0])
+ {
+ concatstring(buf, filename);
+ format = guessimageformat(buf, -1);
+ }
+ else
+ {
+ string sstime;
+ time_t t = time(NULL);
+ size_t len = strftime(sstime, sizeof(sstime), "%Y-%m-%d_%H.%M.%S", localtime(&t));
+ sstime[min(len, sizeof(sstime)-1)] = '\0';
+ concatstring(buf, sstime);
+
+ const char *map = game::getclientmap(), *ssinfo = game::getscreenshotinfo();
+ if(map && map[0])
+ {
+ concatstring(buf, "_");
+ concatstring(buf, map);
+ }
+ if(ssinfo && ssinfo[0])
+ {
+ concatstring(buf, "_");
+ concatstring(buf, ssinfo);
+ }
+
+ for(char *s = &buf[dirlen]; *s; s++) if(iscubespace(*s) || *s == '/' || *s == '\\') *s = '-';
+ }
+ if(format < 0)
+ {
+ format = screenshotformat;
+ concatstring(buf, imageexts[format]);
+ }
+
+ ImageData image(screenw, screenh, 3);
+ glPixelStorei(GL_PACK_ALIGNMENT, texalign(image.data, screenw, 3));
+ glReadPixels(0, 0, screenw, screenh, GL_RGB, GL_UNSIGNED_BYTE, image.data);
+ saveimage(path(buf), format, image, true);
+}
+
+COMMAND(screenshot, "s");
+
+void flipnormalmapy(char *destfile, char *normalfile) // jpg/png /tga-> tga
+{
+ ImageData ns;
+ if(!loadimage(normalfile, ns)) return;
+ ImageData d(ns.w, ns.h, 3);
+ readwritetex(d, ns,
+ dst[0] = src[0];
+ dst[1] = 255 - src[1];
+ dst[2] = src[2];
+ );
+ saveimage(destfile, guessimageformat(destfile, IMG_TGA), d);
+}
+
+void mergenormalmaps(char *heightfile, char *normalfile) // jpg/png/tga + tga -> tga
+{
+ ImageData hs, ns;
+ if(!loadimage(heightfile, hs) || !loadimage(normalfile, ns) || hs.w != ns.w || hs.h != ns.h) return;
+ ImageData d(ns.w, ns.h, 3);
+ read2writetex(d, hs, srch, ns, srcn,
+ *(bvec *)dst = bvec(((bvec *)srcn)->tonormal().mul(2).add(((bvec *)srch)->tonormal()).normalize());
+ );
+ saveimage(normalfile, guessimageformat(normalfile, IMG_TGA), d);
+}
+
+COMMAND(flipnormalmapy, "ss");
+COMMAND(mergenormalmaps, "ss");
+
diff --git a/src/engine/texture.h b/src/engine/texture.h
new file mode 100644
index 0000000..17bce5f
--- /dev/null
+++ b/src/engine/texture.h
@@ -0,0 +1,779 @@
+struct GlobalShaderParamState
+{
+ const char *name;
+ union
+ {
+ float fval[32];
+ int ival[32];
+ uint uval[32];
+ uchar buf[32*sizeof(float)];
+ };
+ int version;
+
+ static int nextversion;
+
+ void resetversions();
+
+ void changed()
+ {
+ if(++nextversion < 0) resetversions();
+ version = nextversion;
+ }
+};
+
+struct ShaderParamBinding
+{
+ int loc, size;
+ GLenum format;
+};
+
+struct GlobalShaderParamUse : ShaderParamBinding
+{
+
+ GlobalShaderParamState *param;
+ int version;
+
+ void flush()
+ {
+ if(version == param->version) return;
+ switch(format)
+ {
+ case GL_BOOL:
+ case GL_FLOAT: glUniform1fv_(loc, size, param->fval); break;
+ case GL_BOOL_VEC2:
+ case GL_FLOAT_VEC2: glUniform2fv_(loc, size, param->fval); break;
+ case GL_BOOL_VEC3:
+ case GL_FLOAT_VEC3: glUniform3fv_(loc, size, param->fval); break;
+ case GL_BOOL_VEC4:
+ case GL_FLOAT_VEC4: glUniform4fv_(loc, size, param->fval); break;
+ case GL_INT: glUniform1iv_(loc, size, param->ival); break;
+ case GL_INT_VEC2: glUniform2iv_(loc, size, param->ival); break;
+ case GL_INT_VEC3: glUniform3iv_(loc, size, param->ival); break;
+ case GL_INT_VEC4: glUniform4iv_(loc, size, param->ival); break;
+ case GL_FLOAT_MAT2: glUniformMatrix2fv_(loc, 1, GL_FALSE, param->fval); break;
+ case GL_FLOAT_MAT3: glUniformMatrix3fv_(loc, 1, GL_FALSE, param->fval); break;
+ case GL_FLOAT_MAT4: glUniformMatrix4fv_(loc, 1, GL_FALSE, param->fval); break;
+ }
+ version = param->version;
+ }
+};
+
+struct LocalShaderParamState : ShaderParamBinding
+{
+ const char *name;
+};
+
+struct SlotShaderParam
+{
+ const char *name;
+ int loc;
+ float val[4];
+};
+
+struct SlotShaderParamState : LocalShaderParamState
+{
+ float val[4];
+
+ SlotShaderParamState() {}
+ SlotShaderParamState(const SlotShaderParam &p)
+ {
+ name = p.name;
+ loc = -1;
+ size = 1;
+ format = GL_FLOAT_VEC4;
+ memcpy(val, p.val, sizeof(val));
+ }
+};
+
+enum
+{
+ SHADER_DEFAULT = 0,
+ SHADER_NORMALSLMS = 1<<0,
+ SHADER_ENVMAP = 1<<1,
+ SHADER_OPTION = 1<<3,
+
+ SHADER_INVALID = 1<<8,
+ SHADER_DEFERRED = 1<<9
+};
+
+#define MAXSHADERDETAIL 3
+#define MAXVARIANTROWS 5
+
+extern int shaderdetail;
+
+struct Slot;
+struct VSlot;
+
+struct UniformLoc
+{
+ const char *name, *blockname;
+ int loc, version, binding, stride, offset, size;
+ void *data;
+ UniformLoc(const char *name = NULL, const char *blockname = NULL, int binding = -1, int stride = -1) : name(name), blockname(blockname), loc(-1), version(-1), binding(binding), stride(stride), offset(-1), size(-1), data(NULL) {}
+};
+
+struct AttribLoc
+{
+ const char *name;
+ int loc;
+ AttribLoc(const char *name = NULL, int loc = -1) : name(name), loc(loc) {}
+};
+
+struct Shader
+{
+ static Shader *lastshader;
+
+ char *name, *vsstr, *psstr, *defer;
+ int type;
+ GLuint program, vsobj, psobj;
+ vector<SlotShaderParamState> defaultparams;
+ vector<GlobalShaderParamUse> globalparams;
+ vector<LocalShaderParamState> localparams;
+ vector<uchar> localparamremap;
+ Shader *detailshader, *variantshader, *altshader, *fastshader[MAXSHADERDETAIL];
+ vector<Shader *> variants;
+ ushort *variantrows;
+ bool standard, forced, used;
+ Shader *reusevs, *reuseps;
+ vector<UniformLoc> uniformlocs;
+ vector<AttribLoc> attriblocs;
+ const void *owner;
+
+ Shader() : name(NULL), vsstr(NULL), psstr(NULL), defer(NULL), type(SHADER_DEFAULT), program(0), vsobj(0), psobj(0), detailshader(NULL), variantshader(NULL), altshader(NULL), variantrows(NULL), standard(false), forced(false), used(false), reusevs(NULL), reuseps(NULL), owner(NULL)
+ {
+ loopi(MAXSHADERDETAIL) fastshader[i] = this;
+ }
+
+ ~Shader()
+ {
+ DELETEA(name);
+ DELETEA(vsstr);
+ DELETEA(psstr);
+ DELETEA(defer);
+ DELETEA(variantrows);
+ }
+
+ void fixdetailshader(bool force = true, bool recurse = true);
+ void allocparams(Slot *slot = NULL);
+ void setslotparams(Slot &slot);
+ void setslotparams(Slot &slot, VSlot &vslot);
+ void bindprograms();
+
+ void flushparams(Slot *slot = NULL)
+ {
+ if(!used) { allocparams(slot); used = true; }
+ loopv(globalparams) globalparams[i].flush();
+ }
+
+ void force();
+
+ bool invalid() const { return (type&SHADER_INVALID)!=0; }
+ bool deferred() const { return (type&SHADER_DEFERRED)!=0; }
+ bool loaded() const { return detailshader!=NULL; }
+
+ static bool isnull(const Shader *s);
+
+ bool isnull() const { return isnull(this); }
+
+ int numvariants(int row) const
+ {
+ if(row < 0 || row >= MAXVARIANTROWS || !variantrows) return 0;
+ return variantrows[row+1] - variantrows[row];
+ }
+
+ Shader *getvariant(int col, int row) const
+ {
+ if(row < 0 || row >= MAXVARIANTROWS || col < 0 || !variantrows) return NULL;
+ int start = variantrows[row], end = variantrows[row+1];
+ return col < end - start ? variants[start + col] : NULL;
+ }
+
+ bool hasoption(int row)
+ {
+ if(detailshader)
+ {
+ Shader *s = getvariant(0, row);
+ if(s) return (s->type&SHADER_OPTION)!=0;
+ }
+ return false;
+ }
+
+ void addvariant(int row, Shader *s)
+ {
+ if(row < 0 || row >= MAXVARIANTROWS || variants.length() >= USHRT_MAX) return;
+ if(!variantrows) { variantrows = new ushort[MAXVARIANTROWS+1]; memset(variantrows, 0, (MAXVARIANTROWS+1)*sizeof(ushort)); }
+ variants.insert(variantrows[row+1], s);
+ for(int i = row+1; i <= MAXVARIANTROWS; ++i) ++variantrows[i];
+ }
+
+ void setvariant_(int col, int row, Shader *fallbackshader)
+ {
+ Shader *s = fallbackshader;
+ if(detailshader->variantrows)
+ {
+ int start = detailshader->variantrows[row], end = detailshader->variantrows[row+1];
+ for(col = min(start + col, end-1); col >= start; --col)
+ {
+ if(!detailshader->variants[col]->invalid()) { s = detailshader->variants[col]; break; }
+ }
+ }
+ if(lastshader!=s) s->bindprograms();
+ }
+
+ void setvariant(int col, int row, Shader *fallbackshader)
+ {
+ if(isnull() || !loaded()) return;
+ setvariant_(col, row, fallbackshader);
+ lastshader->flushparams();
+ }
+
+ void setvariant(int col, int row)
+ {
+ if(isnull() || !loaded()) return;
+ setvariant_(col, row, detailshader);
+ lastshader->flushparams();
+ }
+
+ void setvariant(int col, int row, Slot &slot, VSlot &vslot, Shader *fallbackshader)
+ {
+ if(isnull() || !loaded()) return;
+ setvariant_(col, row, fallbackshader);
+ lastshader->flushparams(&slot);
+ lastshader->setslotparams(slot, vslot);
+ }
+
+ void setvariant(int col, int row, Slot &slot, VSlot &vslot)
+ {
+ if(isnull() || !loaded()) return;
+ setvariant_(col, row, detailshader);
+ lastshader->flushparams(&slot);
+ lastshader->setslotparams(slot, vslot);
+ }
+
+ void set_()
+ {
+ if(lastshader!=detailshader) detailshader->bindprograms();
+ }
+
+ void set()
+ {
+ if(isnull() || !loaded()) return;
+ set_();
+ lastshader->flushparams();
+ }
+
+ void set(Slot &slot, VSlot &vslot)
+ {
+ if(isnull() || !loaded()) return;
+ set_();
+ lastshader->flushparams(&slot);
+ lastshader->setslotparams(slot, vslot);
+ }
+
+ bool compile();
+ void cleanup(bool invalid = false);
+
+ static int uniformlocversion();
+};
+
+struct GlobalShaderParam
+{
+ const char *name;
+ GlobalShaderParamState *param;
+
+ GlobalShaderParam(const char *name) : name(name), param(NULL) {}
+
+ GlobalShaderParamState *resolve()
+ {
+ extern GlobalShaderParamState *getglobalparam(const char *name);
+ if(!param) param = getglobalparam(name);
+ param->changed();
+ return param;
+ }
+
+ void setf(float x = 0, float y = 0, float z = 0, float w = 0)
+ {
+ GlobalShaderParamState *g = resolve();
+ g->fval[0] = x;
+ g->fval[1] = y;
+ g->fval[2] = z;
+ g->fval[3] = w;
+ }
+ void set(const vec &v, float w = 0) { setf(v.x, v.y, v.z, w); }
+ void set(const vec2 &v, float z = 0, float w = 0) { setf(v.x, v.y, z, w); }
+ void set(const vec4 &v) { setf(v.x, v.y, v.z, v.w); }
+ void set(const plane &p) { setf(p.x, p.y, p.z, p.offset); }
+ void set(const matrix2 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); }
+ void set(const matrix3 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); }
+ void set(const matrix4 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); }
+
+ template<class T>
+ void setv(const T *v, int n = 1) { memcpy(resolve()->buf, v, n*sizeof(T)); }
+
+ void seti(int x = 0, int y = 0, int z = 0, int w = 0)
+ {
+ GlobalShaderParamState *g = resolve();
+ g->ival[0] = x;
+ g->ival[1] = y;
+ g->ival[2] = z;
+ g->ival[3] = w;
+ }
+ void set(const ivec &v, int w = 0) { seti(v.x, v.y, v.z, w); }
+ void set(const ivec2 &v, int z = 0, int w = 0) { seti(v.x, v.y, z, w); }
+ void set(const ivec4 &v) { seti(v.x, v.y, v.z, v.w); }
+
+ void setu(uint x = 0, uint y = 0, uint z = 0, uint w = 0)
+ {
+ GlobalShaderParamState *g = resolve();
+ g->uval[0] = x;
+ g->uval[1] = y;
+ g->uval[2] = z;
+ g->uval[3] = w;
+ }
+
+ template<class T>
+ T *reserve(int n = 1) { return (T *)resolve()->buf; }
+};
+
+struct LocalShaderParam
+{
+ const char *name;
+ int loc;
+
+ LocalShaderParam(const char *name) : name(name), loc(-1) {}
+
+ LocalShaderParamState *resolve()
+ {
+ Shader *s = Shader::lastshader;
+ if(!s) return NULL;
+ if(!s->localparamremap.inrange(loc))
+ {
+ extern int getlocalparam(const char *name);
+ if(loc == -1) loc = getlocalparam(name);
+ if(!s->localparamremap.inrange(loc)) return NULL;
+ }
+ uchar remap = s->localparamremap[loc];
+ return s->localparams.inrange(remap) ? &s->localparams[remap] : NULL;
+ }
+
+ void setf(float x = 0, float y = 0, float z = 0, float w = 0)
+ {
+ ShaderParamBinding *b = resolve();
+ if(b) switch(b->format)
+ {
+ case GL_BOOL:
+ case GL_FLOAT: glUniform1f_(b->loc, x); break;
+ case GL_BOOL_VEC2:
+ case GL_FLOAT_VEC2: glUniform2f_(b->loc, x, y); break;
+ case GL_BOOL_VEC3:
+ case GL_FLOAT_VEC3: glUniform3f_(b->loc, x, y, z); break;
+ case GL_BOOL_VEC4:
+ case GL_FLOAT_VEC4: glUniform4f_(b->loc, x, y, z, w); break;
+ case GL_INT: glUniform1i_(b->loc, int(x)); break;
+ case GL_INT_VEC2: glUniform2i_(b->loc, int(x), int(y)); break;
+ case GL_INT_VEC3: glUniform3i_(b->loc, int(x), int(y), int(z)); break;
+ case GL_INT_VEC4: glUniform4i_(b->loc, int(x), int(y), int(z), int(w)); break;
+ }
+ }
+ void set(const vec &v, float w = 0) { setf(v.x, v.y, v.z, w); }
+ void set(const vec2 &v, float z = 0, float w = 0) { setf(v.x, v.y, z, w); }
+ void set(const vec4 &v) { setf(v.x, v.y, v.z, v.w); }
+ void set(const plane &p) { setf(p.x, p.y, p.z, p.offset); }
+ void setv(const float *f, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform1fv_(b->loc, n, f); }
+ void setv(const vec *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform3fv_(b->loc, n, v->v); }
+ void setv(const vec2 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform2fv_(b->loc, n, v->v); }
+ void setv(const vec4 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4fv_(b->loc, n, v->v); }
+ void setv(const plane *p, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4fv_(b->loc, n, p->v); }
+ void setv(const matrix2 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix2fv_(b->loc, n, GL_FALSE, m->a.v); }
+ void setv(const matrix3 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix3fv_(b->loc, n, GL_FALSE, m->a.v); }
+ void setv(const matrix4 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix4fv_(b->loc, n, GL_FALSE, m->a.v); }
+ void set(const matrix2 &m) { setv(&m); }
+ void set(const matrix3 &m) { setv(&m); }
+ void set(const matrix4 &m) { setv(&m); }
+
+ template<class T>
+ void sett(T x, T y, T z, T w)
+ {
+ ShaderParamBinding *b = resolve();
+ if(b) switch(b->format)
+ {
+ case GL_FLOAT: glUniform1f_(b->loc, x); break;
+ case GL_FLOAT_VEC2: glUniform2f_(b->loc, x, y); break;
+ case GL_FLOAT_VEC3: glUniform3f_(b->loc, x, y, z); break;
+ case GL_FLOAT_VEC4: glUniform4f_(b->loc, x, y, z, w); break;
+ case GL_BOOL:
+ case GL_INT: glUniform1i_(b->loc, x); break;
+ case GL_BOOL_VEC2:
+ case GL_INT_VEC2: glUniform2i_(b->loc, x, y); break;
+ case GL_BOOL_VEC3:
+ case GL_INT_VEC3: glUniform3i_(b->loc, x, y, z); break;
+ case GL_BOOL_VEC4:
+ case GL_INT_VEC4: glUniform4i_(b->loc, x, y, z, w); break;
+ }
+ }
+ void seti(int x = 0, int y = 0, int z = 0, int w = 0) { sett<int>(x, y, z, w); }
+ void set(const ivec &v, int w = 0) { seti(v.x, v.y, v.z, w); }
+ void set(const ivec2 &v, int z = 0, int w = 0) { seti(v.x, v.y, z, w); }
+ void set(const ivec4 &v) { seti(v.x, v.y, v.z, v.w); }
+ void setv(const int *i, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform1iv_(b->loc, n, i); }
+ void setv(const ivec *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform3iv_(b->loc, n, v->v); }
+ void setv(const ivec2 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform2iv_(b->loc, n, v->v); }
+ void setv(const ivec4 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4iv_(b->loc, n, v->v); }
+};
+
+#define LOCALPARAM(name, vals) do { static LocalShaderParam param( #name ); param.set(vals); } while(0)
+#define LOCALPARAMF(name, ...) do { static LocalShaderParam param( #name ); param.setf(__VA_ARGS__); } while(0)
+#define LOCALPARAMI(name, ...) do { static LocalShaderParam param( #name ); param.seti(__VA_ARGS__); } while(0)
+#define LOCALPARAMV(name, vals, num) do { static LocalShaderParam param( #name ); param.setv(vals, num); } while(0)
+#define GLOBALPARAM(name, vals) do { static GlobalShaderParam param( #name ); param.set(vals); } while(0)
+#define GLOBALPARAMF(name, ...) do { static GlobalShaderParam param( #name ); param.setf(__VA_ARGS__); } while(0)
+#define GLOBALPARAMI(name, ...) do { static GlobalShaderParam param( #name ); param.seti(__VA_ARGS__); } while(0)
+#define GLOBALPARAMV(name, vals, num) do { static GlobalShaderParam param( #name ); param.setv(vals, num); } while(0)
+
+#define SETSHADER(name, ...) \
+ do { \
+ static Shader *name##shader = NULL; \
+ if(!name##shader) name##shader = lookupshaderbyname(#name); \
+ name##shader->set(__VA_ARGS__); \
+ } while(0)
+#define SETVARIANT(name, ...) \
+ do { \
+ static Shader *name##shader = NULL; \
+ if(!name##shader) name##shader = lookupshaderbyname(#name); \
+ name##shader->setvariant(__VA_ARGS__); \
+ } while(0)
+
+struct ImageData
+{
+ int w, h, bpp, levels, align, pitch;
+ GLenum compressed;
+ uchar *data;
+ void *owner;
+ void (*freefunc)(void *);
+
+ ImageData()
+ : data(NULL), owner(NULL), freefunc(NULL)
+ {}
+
+
+ ImageData(int nw, int nh, int nbpp, int nlevels = 1, int nalign = 0, GLenum ncompressed = GL_FALSE)
+ {
+ setdata(NULL, nw, nh, nbpp, nlevels, nalign, ncompressed);
+ }
+
+ ImageData(int nw, int nh, int nbpp, uchar *data)
+ : owner(NULL), freefunc(NULL)
+ {
+ setdata(data, nw, nh, nbpp);
+ }
+
+ ImageData(SDL_Surface *s) { wrap(s); }
+ ~ImageData() { cleanup(); }
+
+ void setdata(uchar *ndata, int nw, int nh, int nbpp, int nlevels = 1, int nalign = 0, GLenum ncompressed = GL_FALSE)
+ {
+ w = nw;
+ h = nh;
+ bpp = nbpp;
+ levels = nlevels;
+ align = nalign;
+ pitch = align ? 0 : w*bpp;
+ compressed = ncompressed;
+ data = ndata ? ndata : new uchar[calcsize()];
+ if(!ndata) { owner = this; freefunc = NULL; }
+ }
+
+ int calclevelsize(int level) const { return ((max(w>>level, 1)+align-1)/align)*((max(h>>level, 1)+align-1)/align)*bpp; }
+
+ int calcsize() const
+ {
+ if(!align) return w*h*bpp;
+ int lw = w, lh = h,
+ size = 0;
+ loopi(levels)
+ {
+ if(lw<=0) lw = 1;
+ if(lh<=0) lh = 1;
+ size += ((lw+align-1)/align)*((lh+align-1)/align)*bpp;
+ if(lw*lh==1) break;
+ lw >>= 1;
+ lh >>= 1;
+ }
+ return size;
+ }
+
+ void disown()
+ {
+ data = NULL;
+ owner = NULL;
+ freefunc = NULL;
+ }
+
+ void cleanup()
+ {
+ if(owner==this) delete[] data;
+ else if(freefunc) (*freefunc)(owner);
+ disown();
+ }
+
+ void replace(ImageData &d)
+ {
+ cleanup();
+ *this = d;
+ if(owner == &d) owner = this;
+ d.disown();
+ }
+
+ void wrap(SDL_Surface *s)
+ {
+ setdata((uchar *)s->pixels, s->w, s->h, s->format->BytesPerPixel);
+ pitch = s->pitch;
+ owner = s;
+ freefunc = (void (*)(void *))SDL_FreeSurface;
+ }
+};
+
+// management of texture slots
+// each texture slot can have multiple texture frames, of which currently only the first is used
+// additional frames can be used for various shaders
+
+struct Texture
+{
+ enum
+ {
+ IMAGE = 0,
+ CUBEMAP = 1,
+ TYPE = 0xFF,
+
+ STUB = 1<<8,
+ TRANSIENT = 1<<9,
+ COMPRESSED = 1<<10,
+ ALPHA = 1<<11,
+ MIRROR = 1<<12,
+ FLAGS = 0xFF00
+ };
+
+ char *name;
+ int type, w, h, xs, ys, bpp, clamp;
+ bool mipmap, canreduce;
+ GLuint id;
+ uchar *alphamask;
+
+ Texture() : alphamask(NULL) {}
+};
+
+enum
+{
+ TEX_DIFFUSE = 0,
+ TEX_UNKNOWN,
+ TEX_DECAL,
+ TEX_NORMAL,
+ TEX_GLOW,
+ TEX_SPEC,
+ TEX_DEPTH,
+ TEX_ALPHA,
+ TEX_ENVMAP
+};
+
+enum
+{
+ VSLOT_SHPARAM = 0,
+ VSLOT_SCALE,
+ VSLOT_ROTATION,
+ VSLOT_OFFSET,
+ VSLOT_SCROLL,
+ VSLOT_LAYER,
+ VSLOT_ALPHA,
+ VSLOT_COLOR,
+ VSLOT_NUM
+};
+
+struct VSlot
+{
+ Slot *slot;
+ VSlot *next;
+ int index, changed;
+ vector<SlotShaderParam> params;
+ bool linked;
+ float scale;
+ int rotation;
+ ivec2 offset;
+ vec2 scroll;
+ int layer;
+ float alphafront, alphaback;
+ vec colorscale;
+ vec glowcolor;
+
+ VSlot(Slot *slot = NULL, int index = -1) : slot(slot), next(NULL), index(index), changed(0)
+ {
+ reset();
+ if(slot) addvariant(slot);
+ }
+
+ void addvariant(Slot *slot);
+
+ void reset()
+ {
+ params.shrink(0);
+ linked = false;
+ scale = 1;
+ rotation = 0;
+ offset = ivec2(0, 0);
+ scroll = vec2(0, 0);
+ layer = 0;
+ alphafront = 0.5f;
+ alphaback = 0;
+ colorscale = vec(1, 1, 1);
+ glowcolor = vec(1, 1, 1);
+ }
+
+ void cleanup()
+ {
+ linked = false;
+ }
+};
+
+struct Slot
+{
+ struct Tex
+ {
+ int type;
+ Texture *t;
+ string name;
+ int combined;
+ };
+
+ int index;
+ vector<Tex> sts;
+ Shader *shader;
+ vector<SlotShaderParam> params;
+ VSlot *variants;
+ bool loaded;
+ uint texmask;
+ char *autograss;
+ Texture *grasstex, *thumbnail;
+ char *layermaskname;
+ int layermaskmode;
+ float layermaskscale;
+ ImageData *layermask;
+
+ Slot(int index = -1) : index(index), variants(NULL), autograss(NULL), layermaskname(NULL), layermask(NULL) { reset(); }
+
+ void reset()
+ {
+ sts.shrink(0);
+ shader = NULL;
+ params.shrink(0);
+ loaded = false;
+ texmask = 0;
+ DELETEA(autograss);
+ grasstex = NULL;
+ thumbnail = NULL;
+ DELETEA(layermaskname);
+ layermaskmode = 0;
+ layermaskscale = 1;
+ if(layermask) DELETEP(layermask);
+ }
+
+ void cleanup()
+ {
+ loaded = false;
+ grasstex = NULL;
+ thumbnail = NULL;
+ loopv(sts)
+ {
+ Tex &t = sts[i];
+ t.t = NULL;
+ t.combined = -1;
+ }
+ }
+};
+
+inline void VSlot::addvariant(Slot *slot)
+{
+ if(!slot->variants) slot->variants = this;
+ else
+ {
+ VSlot *prev = slot->variants;
+ while(prev->next) prev = prev->next;
+ prev->next = this;
+ }
+}
+
+struct MSlot : Slot, VSlot
+{
+ MSlot() : VSlot(this) {}
+
+ void reset()
+ {
+ Slot::reset();
+ VSlot::reset();
+ }
+
+ void cleanup()
+ {
+ Slot::cleanup();
+ VSlot::cleanup();
+ }
+};
+
+struct texrotation
+{
+ bool flipx, flipy, swapxy;
+};
+
+struct cubemapside
+{
+ GLenum target;
+ const char *name;
+ bool flipx, flipy, swapxy;
+};
+
+extern const texrotation texrotations[8];
+extern const cubemapside cubemapsides[6];
+extern Texture *notexture;
+extern Shader *nullshader, *hudshader, *hudnotextureshader, *textureshader, *notextureshader, *nocolorshader, *foggedshader, *foggednotextureshader, *stdworldshader;
+extern int reservevpparams, maxvsuniforms, maxfsuniforms;
+
+extern Shader *lookupshaderbyname(const char *name);
+extern Shader *useshaderbyname(const char *name);
+extern Shader *generateshader(const char *name, const char *cmd, ...);
+extern Texture *loadthumbnail(Slot &slot);
+extern void resetslotshader();
+extern void setslotshader(Slot &s);
+extern void linkslotshader(Slot &s, bool load = true);
+extern void linkvslotshader(VSlot &s, bool load = true);
+extern void linkslotshaders();
+extern const char *getshaderparamname(const char *name, bool insert = true);
+extern void setupshaders();
+extern void reloadshaders();
+extern void cleanupshaders();
+
+#define MAXDYNLIGHTS 5
+#define DYNLIGHTBITS 6
+#define DYNLIGHTMASK ((1<<DYNLIGHTBITS)-1)
+
+#define MAXBLURRADIUS 7
+
+extern void setupblurkernel(int radius, float sigma, float *weights, float *offsets);
+extern void setblurshader(int pass, int size, int radius, float *weights, float *offsets);
+
+extern void savepng(const char *filename, ImageData &image, bool flip = false);
+extern void savetga(const char *filename, ImageData &image, bool flip = false);
+extern bool loaddds(const char *filename, ImageData &image, int force = 0);
+extern bool loadimage(const char *filename, ImageData &image);
+
+extern MSlot &lookupmaterialslot(int slot, bool load = true);
+extern Slot &lookupslot(int slot, bool load = true);
+extern VSlot &lookupvslot(int slot, bool load = true);
+extern VSlot *findvslot(Slot &slot, const VSlot &src, const VSlot &delta);
+extern VSlot *editvslot(const VSlot &src, const VSlot &delta);
+extern void mergevslot(VSlot &dst, const VSlot &src, const VSlot &delta);
+extern void packvslot(vector<uchar> &buf, const VSlot &src);
+extern bool unpackvslot(ucharbuf &buf, VSlot &dst, bool delta);
+
+extern Slot dummyslot;
+extern VSlot dummyvslot;
+extern vector<Slot *> slots;
+extern vector<VSlot *> vslots;
+
diff --git a/src/engine/vertmodel.h b/src/engine/vertmodel.h
new file mode 100644
index 0000000..eb09001
--- /dev/null
+++ b/src/engine/vertmodel.h
@@ -0,0 +1,490 @@
+struct vertmodel : animmodel
+{
+ struct vert { vec pos, norm; };
+ struct vvert { vec pos; vec2 tc; };
+ struct vvertn : vvert { vec norm; };
+ struct vvertbump : vvert { squat tangent; };
+ struct tcvert { vec2 tc; };
+ struct bumpvert { vec4 tangent; };
+ struct tri { ushort vert[3]; };
+
+ struct vbocacheentry
+ {
+ GLuint vbuf;
+ animstate as;
+ int millis;
+
+ vbocacheentry() : vbuf(0) { as.cur.fr1 = as.prev.fr1 = -1; }
+ };
+
+ struct vertmesh : mesh
+ {
+ vert *verts;
+ tcvert *tcverts;
+ bumpvert *bumpverts;
+ tri *tris;
+ int numverts, numtris;
+
+ int voffset, eoffset, elen;
+ ushort minvert, maxvert;
+
+ vertmesh() : verts(0), tcverts(0), bumpverts(0), tris(0)
+ {
+ }
+
+ virtual ~vertmesh()
+ {
+ DELETEA(verts);
+ DELETEA(tcverts);
+ DELETEA(bumpverts);
+ DELETEA(tris);
+ }
+
+ void smoothnorms(float limit = 0, bool areaweight = true)
+ {
+ if(((vertmeshgroup *)group)->numframes == 1) mesh::smoothnorms(verts, numverts, tris, numtris, limit, areaweight);
+ else buildnorms(areaweight);
+ }
+
+ void buildnorms(bool areaweight = true)
+ {
+ mesh::buildnorms(verts, numverts, tris, numtris, areaweight, ((vertmeshgroup *)group)->numframes);
+ }
+
+ void calctangents(bool areaweight = true)
+ {
+ if(bumpverts) return;
+ bumpverts = new bumpvert[((vertmeshgroup *)group)->numframes*numverts];
+ mesh::calctangents(bumpverts, verts, tcverts, numverts, tris, numtris, areaweight, ((vertmeshgroup *)group)->numframes);
+ }
+
+ void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
+ {
+ loopj(numverts)
+ {
+ vec v = m.transform(verts[j].pos);
+ loopi(3)
+ {
+ bbmin[i] = min(bbmin[i], v[i]);
+ bbmax[i] = max(bbmax[i], v[i]);
+ }
+ }
+ }
+
+ void genBIH(BIH::mesh &m)
+ {
+ m.tris = (const BIH::tri *)tris;
+ m.numtris = numtris;
+ m.pos = (const uchar *)&verts->pos;
+ m.posstride = sizeof(vert);
+ m.tc = (const uchar *)&tcverts->tc;
+ m.tcstride = sizeof(tcvert);
+ }
+
+ static inline void assignvert(vvertn &vv, int j, tcvert &tc, vert &v)
+ {
+ vv.pos = v.pos;
+ vv.norm = v.norm;
+ vv.tc = tc.tc;
+ }
+
+ inline void assignvert(vvertbump &vv, int j, tcvert &tc, vert &v)
+ {
+ vv.pos = v.pos;
+ vv.tc = tc.tc;
+ vv.tangent = bumpverts[j].tangent;
+ }
+
+ template<class T>
+ int genvbo(vector<ushort> &idxs, int offset, vector<T> &vverts, int *htdata, int htlen)
+ {
+ voffset = offset;
+ eoffset = idxs.length();
+ minvert = 0xFFFF;
+ loopi(numtris)
+ {
+ tri &t = tris[i];
+ loopj(3)
+ {
+ int index = t.vert[j];
+ tcvert &tc = tcverts[index];
+ vert &v = verts[index];
+ T vv;
+ assignvert(vv, index, tc, v);
+ int htidx = hthash(v.pos)&(htlen-1);
+ loopk(htlen)
+ {
+ int &vidx = htdata[(htidx+k)&(htlen-1)];
+ if(vidx < 0) { vidx = idxs.add(ushort(vverts.length())); vverts.add(vv); break; }
+ else if(!memcmp(&vverts[vidx], &vv, sizeof(vv))) { minvert = min(minvert, idxs.add(ushort(vidx))); break; }
+ }
+ }
+ }
+ minvert = min(minvert, ushort(voffset));
+ maxvert = max(minvert, ushort(vverts.length()-1));
+ elen = idxs.length()-eoffset;
+ return vverts.length()-voffset;
+ }
+
+ int genvbo(vector<ushort> &idxs, int offset)
+ {
+ voffset = offset;
+ eoffset = idxs.length();
+ loopi(numtris)
+ {
+ tri &t = tris[i];
+ loopj(3) idxs.add(voffset+t.vert[j]);
+ }
+ minvert = voffset;
+ maxvert = voffset + numverts-1;
+ elen = idxs.length()-eoffset;
+ return numverts;
+ }
+
+ template<class T>
+ static inline void fillvert(T &vv, int j, tcvert &tc, vert &v)
+ {
+ vv.tc = tc.tc;
+ }
+
+ template<class T>
+ void fillverts(T *vdata)
+ {
+ vdata += voffset;
+ loopi(numverts) fillvert(vdata[i], i, tcverts[i], verts[i]);
+ }
+
+ void interpverts(const animstate &as, bool tangents, void * RESTRICT vdata, skin &s)
+ {
+ const vert * RESTRICT vert1 = &verts[as.cur.fr1 * numverts],
+ * RESTRICT vert2 = &verts[as.cur.fr2 * numverts],
+ * RESTRICT pvert1 = as.interp<1 ? &verts[as.prev.fr1 * numverts] : NULL,
+ * RESTRICT pvert2 = as.interp<1 ? &verts[as.prev.fr2 * numverts] : NULL;
+ #define ipvert(attrib) v.attrib.lerp(vert1[i].attrib, vert2[i].attrib, as.cur.t)
+ #define ipbvert(attrib, type) v.attrib.lerp(bvert1[i].attrib, bvert2[i].attrib, as.cur.t)
+ #define ipvertp(attrib) v.attrib.lerp(pvert1[i].attrib, pvert2[i].attrib, as.prev.t).lerp(vec().lerp(vert1[i].attrib, vert2[i].attrib, as.cur.t), as.interp)
+ #define ipbvertp(attrib, type) v.attrib.lerp(type().lerp(bpvert1[i].attrib, bpvert2[i].attrib, as.prev.t), type().lerp(bvert1[i].attrib, bvert2[i].attrib, as.cur.t), as.interp)
+ #define iploop(type, body) \
+ loopi(numverts) \
+ { \
+ type &v = ((type * RESTRICT)vdata)[i]; \
+ body; \
+ }
+ if(tangents)
+ {
+ const bumpvert * RESTRICT bvert1 = &bumpverts[as.cur.fr1 * numverts],
+ * RESTRICT bvert2 = &bumpverts[as.cur.fr2 * numverts],
+ * RESTRICT bpvert1 = as.interp<1 ? &bumpverts[as.prev.fr1 * numverts] : NULL,
+ * RESTRICT bpvert2 = as.interp<1 ? &bumpverts[as.prev.fr2 * numverts] : NULL;
+ if(as.interp<1) iploop(vvertbump, { ipvertp(pos); ipbvertp(tangent, vec4); })
+ else iploop(vvertbump, { ipvert(pos); ipbvert(tangent, vec4); })
+ }
+ else
+ {
+ if(as.interp<1) iploop(vvertn, { ipvertp(pos); ipvertp(norm); })
+ else iploop(vvertn, { ipvert(pos); ipvert(norm); })
+ }
+ #undef iploop
+ #undef ipvert
+ #undef ipbvert
+ #undef ipvertp
+ #undef ipbvertp
+ }
+
+ void render(const animstate *as, skin &s, vbocacheentry &vc)
+ {
+ if(!Shader::lastshader) return;
+ glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, elen, GL_UNSIGNED_SHORT, &((vertmeshgroup *)group)->edata[eoffset]);
+ glde++;
+ xtravertsva += numverts;
+ }
+ };
+
+ struct tag
+ {
+ char *name;
+ matrix4x3 transform;
+
+ tag() : name(NULL) {}
+ ~tag() { DELETEA(name); }
+ };
+
+ struct vertmeshgroup : meshgroup
+ {
+ int numframes;
+ tag *tags;
+ int numtags;
+
+ static const int MAXVBOCACHE = 16;
+ vbocacheentry vbocache[MAXVBOCACHE];
+
+ ushort *edata;
+ GLuint ebuf;
+ bool vtangents;
+ int vlen, vertsize;
+ uchar *vdata;
+
+ vertmeshgroup() : numframes(0), tags(NULL), numtags(0), edata(NULL), ebuf(0), vtangents(false), vlen(0), vertsize(0), vdata(NULL)
+ {
+ }
+
+ virtual ~vertmeshgroup()
+ {
+ DELETEA(tags);
+ if(ebuf) glDeleteBuffers_(1, &ebuf);
+ loopi(MAXVBOCACHE)
+ {
+ if(vbocache[i].vbuf) glDeleteBuffers_(1, &vbocache[i].vbuf);
+ }
+ DELETEA(vdata);
+ }
+
+ int findtag(const char *name)
+ {
+ loopi(numtags) if(!strcmp(tags[i].name, name)) return i;
+ return -1;
+ }
+
+ int totalframes() const { return numframes; }
+
+ void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n)
+ {
+ n.mul(m, tags[numtags + i].transform);
+ n.posttranslate(m.transformnormal(p->translate), p->model->scale);
+ }
+
+ void calctagmatrix(part *p, int i, const animstate &as, matrix4 &matrix)
+ {
+ const matrix4x3 &tag1 = tags[as.cur.fr1*numtags + i].transform,
+ &tag2 = tags[as.cur.fr2*numtags + i].transform;
+ matrix4x3 tag;
+ tag.lerp(tag1, tag2, as.cur.t);
+ if(as.interp<1)
+ {
+ const matrix4x3 &tag1p = tags[as.prev.fr1*numtags + i].transform,
+ &tag2p = tags[as.prev.fr2*numtags + i].transform;
+ matrix4x3 tagp;
+ tagp.lerp(tag1p, tag2p, as.prev.t);
+ tag.lerp(tagp, tag, as.interp);
+ }
+ tag.d.add(p->translate).mul(p->model->scale);
+ matrix = matrix4(tag);
+ }
+
+ void genvbo(bool tangents, vbocacheentry &vc)
+ {
+ if(!vc.vbuf) glGenBuffers_(1, &vc.vbuf);
+ if(ebuf) return;
+
+ vector<ushort> idxs;
+
+ if(tangents) loopv(meshes) ((vertmesh *)meshes[i])->calctangents();
+
+ vtangents = tangents;
+ vertsize = tangents ? sizeof(vvertbump) : sizeof(vvertn);
+ vlen = 0;
+ if(numframes>1)
+ {
+ loopv(meshes) vlen += ((vertmesh *)meshes[i])->genvbo(idxs, vlen);
+ DELETEA(vdata);
+ vdata = new uchar[vlen*vertsize];
+ #define FILLVDATA(type) do { \
+ loopv(meshes) ((vertmesh *)meshes[i])->fillverts((type *)vdata); \
+ } while(0)
+ if(tangents) FILLVDATA(vvertbump);
+ else FILLVDATA(vvertn);
+ #undef FILLVDATA
+ }
+ else
+ {
+ gle::bindvbo(vc.vbuf);
+ #define GENVBO(type) do { \
+ vector<type> vverts; \
+ loopv(meshes) vlen += ((vertmesh *)meshes[i])->genvbo(idxs, vlen, vverts, htdata, htlen); \
+ glBufferData_(GL_ARRAY_BUFFER, vverts.length()*sizeof(type), vverts.getbuf(), GL_STATIC_DRAW); \
+ } while(0)
+ int numverts = 0, htlen = 128;
+ loopv(meshes) numverts += ((vertmesh *)meshes[i])->numverts;
+ while(htlen < numverts) htlen *= 2;
+ if(numverts*4 > htlen*3) htlen *= 2;
+ int *htdata = new int[htlen];
+ memset(htdata, -1, htlen*sizeof(int));
+ if(tangents) GENVBO(vvertbump);
+ else GENVBO(vvertn);
+ delete[] htdata;
+ #undef GENVBO
+ gle::clearvbo();
+ }
+
+ glGenBuffers_(1, &ebuf);
+ gle::bindebo(ebuf);
+ glBufferData_(GL_ELEMENT_ARRAY_BUFFER, idxs.length()*sizeof(ushort), idxs.getbuf(), GL_STATIC_DRAW);
+ gle::clearebo();
+ }
+
+ void bindvbo(const animstate *as, vbocacheentry &vc)
+ {
+ vvert *vverts = 0;
+ bindpos(ebuf, vc.vbuf, &vverts->pos, vertsize);
+ if(as->cur.anim&ANIM_NOSKIN)
+ {
+ if(enabletc) disabletc();
+ if(enablenormals) disablenormals();
+ if(enabletangents) disabletangents();
+ }
+ else
+ {
+ if(vtangents)
+ {
+ if(enablenormals) disablenormals();
+ vvertbump *vvertbumps = 0;
+ bindtangents(&vvertbumps->tangent, vertsize);
+ }
+ else
+ {
+ if(enabletangents) disabletangents();
+ vvertn *vvertns = 0;
+ bindnormals(&vvertns->norm, vertsize);
+ }
+
+ bindtc(&vverts->tc, vertsize);
+ }
+ if(enablebones) disablebones();
+ }
+
+ void cleanup()
+ {
+ loopi(MAXVBOCACHE)
+ {
+ vbocacheentry &c = vbocache[i];
+ if(c.vbuf) { glDeleteBuffers_(1, &c.vbuf); c.vbuf = 0; }
+ c.as.cur.fr1 = -1;
+ }
+ if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
+ }
+
+ void preload(part *p)
+ {
+ if(numframes > 1) return;
+ bool tangents = p->tangents();
+ if(tangents!=vtangents) cleanup();
+ if(!vbocache->vbuf) genvbo(tangents, *vbocache);
+ }
+
+ void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p)
+ {
+ if(as->cur.anim&ANIM_NORENDER)
+ {
+ loopv(p->links) calctagmatrix(p, p->links[i].tag, *as, p->links[i].matrix);
+ return;
+ }
+
+ bool tangents = p->tangents();
+ if(tangents!=vtangents) { cleanup(); disablevbo(); }
+ vbocacheentry *vc = NULL;
+ if(numframes<=1) vc = vbocache;
+ else
+ {
+ loopi(MAXVBOCACHE)
+ {
+ vbocacheentry &c = vbocache[i];
+ if(!c.vbuf) continue;
+ if(c.as==*as) { vc = &c; break; }
+ }
+ if(!vc) loopi(MAXVBOCACHE) { vc = &vbocache[i]; if(!vc->vbuf || vc->millis < lastmillis) break; }
+ }
+ if(!vc->vbuf) genvbo(tangents, *vc);
+ if(numframes>1)
+ {
+ if(vc->as!=*as)
+ {
+ vc->as = *as;
+ vc->millis = lastmillis;
+ loopv(meshes)
+ {
+ vertmesh &m = *(vertmesh *)meshes[i];
+ m.interpverts(*as, tangents, vdata + m.voffset*vertsize, p->skins[i]);
+ }
+ gle::bindvbo(vc->vbuf);
+ glBufferData_(GL_ARRAY_BUFFER, vlen*vertsize, vdata, GL_STREAM_DRAW);
+ }
+ vc->millis = lastmillis;
+ }
+
+ bindvbo(as, *vc);
+ loopv(meshes)
+ {
+ vertmesh *m = (vertmesh *)meshes[i];
+ p->skins[i].bind(m, as);
+ m->render(as, p->skins[i], *vc);
+ }
+
+ loopv(p->links) calctagmatrix(p, p->links[i].tag, *as, p->links[i].matrix);
+ }
+ };
+
+ vertmodel(const char *name) : animmodel(name)
+ {
+ }
+};
+
+template<class MDL> struct vertloader : modelloader<MDL, vertmodel>
+{
+ vertloader(const char *name) : modelloader<MDL, vertmodel>(name) {}
+};
+
+template<class MDL> struct vertcommands : modelcommands<MDL, struct MDL::vertmesh>
+{
+ typedef struct MDL::part part;
+ typedef struct MDL::skin skin;
+
+ static void loadpart(char *model, float *smooth)
+ {
+ if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ defformatstring(filename, "%s/%s", MDL::dir, model);
+ part &mdl = MDL::loading->addpart();
+ if(mdl.index) mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0;
+ mdl.meshes = MDL::loading->sharemeshes(path(filename), double(*smooth > 0 ? cos(clamp(*smooth, 0.0f, 180.0f)*RAD) : 2));
+ if(!mdl.meshes) conoutf(CON_ERROR, "could not load %s", filename);
+ else mdl.initskins();
+ }
+
+ static void setpitch(float *pitchscale, float *pitchoffset, float *pitchmin, float *pitchmax)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ part &mdl = *MDL::loading->parts.last();
+
+ mdl.pitchscale = *pitchscale;
+ mdl.pitchoffset = *pitchoffset;
+ if(*pitchmin || *pitchmax)
+ {
+ mdl.pitchmin = *pitchmin;
+ mdl.pitchmax = *pitchmax;
+ }
+ else
+ {
+ mdl.pitchmin = -360*fabs(mdl.pitchscale) + mdl.pitchoffset;
+ mdl.pitchmax = 360*fabs(mdl.pitchscale) + mdl.pitchoffset;
+ }
+ }
+
+ static void setanim(char *anim, int *frame, int *range, float *speed, int *priority)
+ {
+ if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+ vector<int> anims;
+ findanims(anim, anims);
+ if(anims.empty()) conoutf(CON_ERROR, "could not find animation %s", anim);
+ else loopv(anims)
+ {
+ MDL::loading->parts.last()->setanim(0, anims[i], *frame, *range, *speed, *priority);
+ }
+ }
+
+ vertcommands()
+ {
+ if(MDL::multiparted()) this->modelcommand(loadpart, "load", "sf");
+ this->modelcommand(setpitch, "pitch", "ffff");
+ if(MDL::animated()) this->modelcommand(setanim, "anim", "siiff");
+ }
+};
+
diff --git a/src/engine/water.cpp b/src/engine/water.cpp
new file mode 100644
index 0000000..07c24c2
--- /dev/null
+++ b/src/engine/water.cpp
@@ -0,0 +1,1061 @@
+#include "engine.h"
+
+VARFP(waterreflect, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
+VARFP(waterrefract, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
+VARFP(waterenvmap, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
+VARFP(waterfallrefract, 0, 0, 1, { cleanreflections(); preloadwatershaders(); });
+
+/* vertex water */
+VARP(watersubdiv, 0, 2, 3);
+VARP(waterlod, 0, 1, 3);
+
+static int wx1, wy1, wx2, wy2, wz, wsize, wsubdiv;
+static float whoffset, whphase;
+
+static inline float vertwangle(int v1, int v2)
+{
+ static const float whscale = 59.0f/23.0f/(2*M_PI);
+ v1 &= wsize-1;
+ v2 &= wsize-1;
+ return v1*v2*whscale+whoffset;
+}
+
+static inline float vertwphase(float angle)
+{
+ float s = angle - int(angle) - 0.5f;
+ s *= 8 - fabs(s)*16;
+ return WATER_AMPLITUDE*s-WATER_OFFSET;
+}
+
+static inline void vertw(int v1, int v2, int v3)
+{
+ float h = vertwphase(vertwangle(v1, v2));
+ gle::attribf(v1, v2, v3+h);
+}
+
+static inline void vertwq(float v1, float v2, float v3)
+{
+ gle::attribf(v1, v2, v3+whphase);
+}
+
+static inline void vertwn(float v1, float v2, float v3)
+{
+ float h = -WATER_OFFSET;
+ gle::attribf(v1, v2, v3+h);
+}
+
+struct waterstrip
+{
+ int x1, y1, x2, y2, z;
+ ushort size, subdiv;
+
+ int numverts() const { return 2*((y2-y1)/subdiv + 1)*((x2-x1)/subdiv); }
+
+ void save()
+ {
+ x1 = wx1;
+ y1 = wy1;
+ x2 = wx2;
+ y2 = wy2;
+ z = wz;
+ size = wsize;
+ subdiv = wsubdiv;
+ }
+
+ void restore()
+ {
+ wx1 = x1;
+ wy1 = y1;
+ wx2 = x2;
+ wy2 = y2;
+ wz = z;
+ wsize = size;
+ wsubdiv = subdiv;
+ }
+};
+vector<waterstrip> waterstrips;
+
+void flushwaterstrips()
+{
+ if(gle::attribbuf.length()) xtraverts += gle::end();
+ gle::defvertex();
+ int numverts = 0;
+ loopv(waterstrips) numverts += waterstrips[i].numverts();
+ gle::begin(GL_TRIANGLE_STRIP, numverts);
+ loopv(waterstrips)
+ {
+ waterstrips[i].restore();
+ for(int x = wx1; x < wx2; x += wsubdiv)
+ {
+ for(int y = wy1; y <= wy2; y += wsubdiv)
+ {
+ vertw(x, y, wz);
+ vertw(x+wsubdiv, y, wz);
+ }
+ x += wsubdiv;
+ if(x >= wx2) break;
+ for(int y = wy2; y >= wy1; y -= wsubdiv)
+ {
+ vertw(x, y, wz);
+ vertw(x+wsubdiv, y, wz);
+ }
+ }
+ gle::multidraw();
+ }
+ waterstrips.setsize(0);
+ wsize = 0;
+ xtraverts += gle::end();
+}
+
+void flushwater(int mat = MAT_WATER, bool force = true)
+{
+ if(wsize)
+ {
+ if(wsubdiv >= wsize)
+ {
+ if(gle::attribbuf.empty()) { gle::defvertex(); gle::begin(GL_QUADS); }
+ vertwq(wx1, wy1, wz);
+ vertwq(wx2, wy1, wz);
+ vertwq(wx2, wy2, wz);
+ vertwq(wx1, wy2, wz);
+ }
+ else waterstrips.add().save();
+ wsize = 0;
+ }
+
+ if(force)
+ {
+ if(gle::attribbuf.length()) xtraverts += gle::end();
+ if(waterstrips.length()) flushwaterstrips();
+ }
+}
+
+void rendervertwater(int subdiv, int xo, int yo, int z, int size, int mat)
+{
+ if(wsize == size && wsubdiv == subdiv && wz == z)
+ {
+ if(wx2 == xo)
+ {
+ if(wy1 == yo && wy2 == yo + size) { wx2 += size; return; }
+ }
+ else if(wy2 == yo && wx1 == xo && wx2 == xo + size) { wy2 += size; return; }
+ }
+
+ flushwater(mat, false);
+
+ wx1 = xo;
+ wy1 = yo;
+ wx2 = xo + size,
+ wy2 = yo + size;
+ wz = z;
+ wsize = size;
+ wsubdiv = subdiv;
+
+ ASSERT((wx1 & (subdiv - 1)) == 0);
+ ASSERT((wy1 & (subdiv - 1)) == 0);
+}
+
+int calcwatersubdiv(int x, int y, int z, int size)
+{
+ float dist;
+ if(camera1->o.x >= x && camera1->o.x < x + size &&
+ camera1->o.y >= y && camera1->o.y < y + size)
+ dist = fabs(camera1->o.z - float(z));
+ else
+ dist = vec(x + size/2, y + size/2, z + size/2).dist(camera1->o) - size*1.42f/2;
+ int subdiv = watersubdiv + int(dist) / (32 << waterlod);
+ return subdiv >= 31 ? INT_MAX : 1<<subdiv;
+}
+
+int renderwaterlod(int x, int y, int z, int size, int mat)
+{
+ if(size <= (32 << waterlod))
+ {
+ int subdiv = calcwatersubdiv(x, y, z, size);
+ if(subdiv < size * 2) rendervertwater(min(subdiv, size), x, y, z, size, mat);
+ return subdiv;
+ }
+ else
+ {
+ int subdiv = calcwatersubdiv(x, y, z, size);
+ if(subdiv >= size)
+ {
+ if(subdiv < size * 2) rendervertwater(size, x, y, z, size, mat);
+ return subdiv;
+ }
+ int childsize = size / 2,
+ subdiv1 = renderwaterlod(x, y, z, childsize, mat),
+ subdiv2 = renderwaterlod(x + childsize, y, z, childsize, mat),
+ subdiv3 = renderwaterlod(x + childsize, y + childsize, z, childsize, mat),
+ subdiv4 = renderwaterlod(x, y + childsize, z, childsize, mat),
+ minsubdiv = subdiv1;
+ minsubdiv = min(minsubdiv, subdiv2);
+ minsubdiv = min(minsubdiv, subdiv3);
+ minsubdiv = min(minsubdiv, subdiv4);
+ if(minsubdiv < size * 2)
+ {
+ if(minsubdiv >= size) rendervertwater(size, x, y, z, size, mat);
+ else
+ {
+ if(subdiv1 >= size) rendervertwater(childsize, x, y, z, childsize, mat);
+ if(subdiv2 >= size) rendervertwater(childsize, x + childsize, y, z, childsize, mat);
+ if(subdiv3 >= size) rendervertwater(childsize, x + childsize, y + childsize, z, childsize, mat);
+ if(subdiv4 >= size) rendervertwater(childsize, x, y + childsize, z, childsize, mat);
+ }
+ }
+ return minsubdiv;
+ }
+}
+
+void renderflatwater(int x, int y, int z, int rsize, int csize, int mat)
+{
+ if(gle::attribbuf.empty()) { gle::defvertex(); gle::begin(GL_QUADS); }
+ vertwn(x, y, z);
+ vertwn(x+rsize, y, z);
+ vertwn(x+rsize, y+csize, z);
+ vertwn(x, y+csize, z);
+}
+
+VARFP(vertwater, 0, 1, 1, allchanged());
+
+static inline void renderwater(const materialsurface &m, int mat = MAT_WATER)
+{
+ if(!vertwater || drawtex == DRAWTEX_MINIMAP) renderflatwater(m.o.x, m.o.y, m.o.z, m.rsize, m.csize, mat);
+ else if(renderwaterlod(m.o.x, m.o.y, m.o.z, m.csize, mat) >= int(m.csize) * 2)
+ rendervertwater(m.csize, m.o.x, m.o.y, m.o.z, m.csize, mat);
+}
+
+void setuplava(Texture *tex, float scale)
+{
+ float xk = TEX_SCALE/(tex->xs*scale);
+ float yk = TEX_SCALE/(tex->ys*scale);
+ float scroll = lastmillis/1000.0f;
+ LOCALPARAMF(lavatexgen, xk, yk, scroll, scroll);
+ gle::normal(vec(0, 0, 1));
+ whoffset = fmod(float(lastmillis/2000.0f/(2*M_PI)), 1.0f);
+ whphase = vertwphase(whoffset);
+}
+
+void renderlava(const materialsurface &m)
+{
+ renderwater(m, MAT_LAVA);
+}
+
+void flushlava()
+{
+ flushwater(MAT_LAVA);
+}
+
+/* reflective/refractive water */
+
+#define MAXREFLECTIONS 16
+
+struct Reflection
+{
+ GLuint tex, refracttex;
+ int material, height, depth, age;
+ bool init;
+ matrix4 projmat;
+ occludequery *query, *prevquery;
+ vector<materialsurface *> matsurfs;
+
+ Reflection() : tex(0), refracttex(0), material(-1), height(-1), depth(0), age(0), init(false), query(NULL), prevquery(NULL)
+ {}
+};
+
+VARP(reflectdist, 0, 2000, 10000);
+
+#define WATERVARS(name) \
+ bvec name##color(0x14, 0x46, 0x50), name##fallcolor(0, 0, 0); \
+ HVARFR(name##colour, 0, 0x144650, 0xFFFFFF, \
+ { \
+ if(!name##colour) name##colour = 0x144650; \
+ name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \
+ }); \
+ VARR(name##fog, 0, 150, 10000); \
+ VARR(name##spec, 0, 150, 1000); \
+ HVARFR(name##fallcolour, 0, 0, 0xFFFFFF, \
+ { \
+ name##fallcolor = bvec((name##fallcolour>>16)&0xFF, (name##fallcolour>>8)&0xFF, name##fallcolour&0xFF); \
+ });
+
+WATERVARS(water)
+WATERVARS(water2)
+WATERVARS(water3)
+WATERVARS(water4)
+
+GETMATIDXVAR(water, colour, int)
+GETMATIDXVAR(water, color, const bvec &)
+GETMATIDXVAR(water, fallcolour, int)
+GETMATIDXVAR(water, fallcolor, const bvec &)
+GETMATIDXVAR(water, fog, int)
+GETMATIDXVAR(water, spec, int)
+
+#define LAVAVARS(name) \
+ bvec name##color(0xFF, 0x40, 0x00); \
+ HVARFR(name##colour, 0, 0xFF4000, 0xFFFFFF, \
+ { \
+ if(!name##colour) name##colour = 0xFF4000; \
+ name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \
+ }); \
+ VARR(name##fog, 0, 50, 10000);
+
+LAVAVARS(lava)
+LAVAVARS(lava2)
+LAVAVARS(lava3)
+LAVAVARS(lava4)
+
+GETMATIDXVAR(lava, colour, int)
+GETMATIDXVAR(lava, color, const bvec &)
+GETMATIDXVAR(lava, fog, int)
+
+void setprojtexmatrix(Reflection &ref)
+{
+ if(ref.init)
+ {
+ ref.init = false;
+ (ref.projmat = camprojmatrix).projective();
+ }
+
+ LOCALPARAM(watermatrix, ref.projmat);
+}
+
+Reflection reflections[MAXREFLECTIONS];
+Reflection waterfallrefraction;
+GLuint reflectionfb = 0, reflectiondb = 0;
+
+GLuint getwaterfalltex() { return waterfallrefraction.refracttex ? waterfallrefraction.refracttex : notexture->id; }
+
+VAR(oqwater, 0, 2, 2);
+VARFP(waterfade, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
+
+void preloadwatershaders(bool force)
+{
+ static bool needwater = false;
+ if(force) needwater = true;
+ if(!needwater) return;
+
+ useshaderbyname("waterglare");
+
+ if(waterenvmap && !waterreflect)
+ useshaderbyname(waterrefract ? (waterfade ? "waterenvfade" : "waterenvrefract") : "waterenv");
+ else useshaderbyname(waterrefract ? (waterfade ? "waterfade" : "waterrefract") : (waterreflect ? "waterreflect" : "water"));
+
+ useshaderbyname(waterrefract ? (waterfade ? "underwaterfade" : "underwaterrefract") : "underwater");
+
+ extern int waterfallenv;
+ useshaderbyname(waterfallenv ? "waterfallenv" : "waterfall");
+ if(waterfallrefract) useshaderbyname(waterfallenv ? "waterfallenvrefract" : "waterfallrefract");
+}
+
+void renderwater()
+{
+ if(editmode && showmat && !drawtex) return;
+ if(!rplanes) return;
+
+ glDisable(GL_CULL_FACE);
+
+ if(!glaring && drawtex != DRAWTEX_MINIMAP)
+ {
+ if(waterrefract)
+ {
+ if(waterfade)
+ {
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ }
+ }
+ else
+ {
+ glDepthMask(GL_FALSE);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_SRC_ALPHA);
+ }
+ }
+
+ GLOBALPARAM(camera, camera1->o);
+ GLOBALPARAMF(millis, lastmillis/1000.0f);
+
+ #define SETWATERSHADER(which, name) \
+ do { \
+ static Shader *name##shader = NULL; \
+ if(!name##shader) name##shader = lookupshaderbyname(#name); \
+ which##shader = name##shader; \
+ } while(0)
+
+ Shader *aboveshader = NULL;
+ if(glaring) SETWATERSHADER(above, waterglare);
+ else if(drawtex == DRAWTEX_MINIMAP) aboveshader = notextureshader;
+ else if(waterenvmap && !waterreflect)
+ {
+ if(waterrefract)
+ {
+ if(waterfade) SETWATERSHADER(above, waterenvfade);
+ else SETWATERSHADER(above, waterenvrefract);
+ }
+ else SETWATERSHADER(above, waterenv);
+ }
+ else if(waterrefract)
+ {
+ if(waterfade) SETWATERSHADER(above, waterfade);
+ else SETWATERSHADER(above, waterrefract);
+ }
+ else if(waterreflect) SETWATERSHADER(above, waterreflect);
+ else SETWATERSHADER(above, water);
+
+ Shader *belowshader = NULL;
+ if(!glaring && drawtex != DRAWTEX_MINIMAP)
+ {
+ if(waterrefract)
+ {
+ if(waterfade) SETWATERSHADER(below, underwaterfade);
+ else SETWATERSHADER(below, underwaterrefract);
+ }
+ else SETWATERSHADER(below, underwater);
+ }
+
+ vec ambient(max(skylightcolor[0], ambientcolor[0]), max(skylightcolor[1], ambientcolor[1]), max(skylightcolor[2], ambientcolor[2]));
+ float offset = -WATER_OFFSET;
+ loopi(MAXREFLECTIONS)
+ {
+ Reflection &ref = reflections[i];
+ if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue;
+ if(!glaring && oqfrags && oqwater && ref.query && ref.query->owner==&ref)
+ {
+ if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
+ {
+ if(checkquery(ref.query)) continue;
+ }
+ }
+
+ bool below = camera1->o.z < ref.height+offset;
+ if(below)
+ {
+ if(!belowshader) continue;
+ belowshader->set();
+ }
+ else aboveshader->set();
+
+ if(!glaring && drawtex != DRAWTEX_MINIMAP)
+ {
+ if(waterreflect || waterrefract)
+ {
+ if(waterreflect || !waterenvmap) glBindTexture(GL_TEXTURE_2D, waterreflect ? ref.tex : ref.refracttex);
+ setprojtexmatrix(ref);
+ }
+
+ if(waterrefract)
+ {
+ glActiveTexture_(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_2D, ref.refracttex);
+ if(waterfade)
+ {
+ float fadeheight = ref.height+offset+(below ? -2 : 2);
+ LOCALPARAMF(waterheight, fadeheight);
+ }
+ }
+ }
+
+ MSlot &mslot = lookupmaterialslot(ref.material);
+ glActiveTexture_(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, mslot.sts.inrange(2) ? mslot.sts[2].t->id : notexture->id);
+ glActiveTexture_(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, mslot.sts.inrange(3) ? mslot.sts[3].t->id : notexture->id);
+ glActiveTexture_(GL_TEXTURE0);
+ if(!glaring && waterenvmap && !waterreflect && drawtex != DRAWTEX_MINIMAP)
+ {
+ glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(mslot));
+ }
+
+ whoffset = fmod(float(lastmillis/600.0f/(2*M_PI)), 1.0f);
+ whphase = vertwphase(whoffset);
+
+ gle::color(getwatercolor(ref.material));
+ int wfog = getwaterfog(ref.material), wspec = getwaterspec(ref.material);
+
+ const entity *lastlight = (const entity *)-1;
+ int lastdepth = -1;
+ loopvj(ref.matsurfs)
+ {
+ materialsurface &m = *ref.matsurfs[j];
+
+ entity *light = (m.light && m.light->type==ET_LIGHT ? m.light : NULL);
+ if(light!=lastlight)
+ {
+ flushwater();
+ vec lightpos = light ? light->o : vec(worldsize/2, worldsize/2, worldsize);
+ float lightrad = light && light->attr1 ? light->attr1 : worldsize*8.0f;
+ vec lightcol = (light ? vec(light->attr2, light->attr3, light->attr4) : vec(ambient)).div(255.0f).mul(wspec/100.0f);
+ LOCALPARAM(lightpos, lightpos);
+ LOCALPARAM(lightcolor, lightcol);
+ LOCALPARAMF(lightradius, lightrad);
+ lastlight = light;
+ }
+
+ if(!glaring && !waterrefract && m.depth!=lastdepth)
+ {
+ flushwater();
+ float depth = !wfog ? 1.0f : min(0.75f*m.depth/wfog, 0.95f);
+ depth = max(depth, !below && (waterreflect || waterenvmap) ? 0.3f : 0.6f);
+ LOCALPARAMF(depth, depth, 1.0f-depth);
+ lastdepth = m.depth;
+ }
+
+ renderwater(m);
+ }
+ flushwater();
+ }
+
+ if(!glaring && drawtex != DRAWTEX_MINIMAP)
+ {
+ if(waterrefract)
+ {
+ if(waterfade) glDisable(GL_BLEND);
+ }
+ else
+ {
+ glDepthMask(GL_TRUE);
+ glDisable(GL_BLEND);
+ }
+ }
+
+ glEnable(GL_CULL_FACE);
+}
+
+void setupwaterfallrefract()
+{
+ glBindTexture(GL_TEXTURE_2D, waterfallrefraction.refracttex ? waterfallrefraction.refracttex : notexture->id);
+ setprojtexmatrix(waterfallrefraction);
+}
+
+void cleanreflection(Reflection &ref)
+{
+ ref.material = -1;
+ ref.height = -1;
+ ref.init = false;
+ ref.query = ref.prevquery = NULL;
+ ref.matsurfs.setsize(0);
+ if(ref.tex)
+ {
+ glDeleteTextures(1, &ref.tex);
+ ref.tex = 0;
+ }
+ if(ref.refracttex)
+ {
+ glDeleteTextures(1, &ref.refracttex);
+ ref.refracttex = 0;
+ }
+}
+
+void cleanreflections()
+{
+ loopi(MAXREFLECTIONS) cleanreflection(reflections[i]);
+ cleanreflection(waterfallrefraction);
+ if(reflectionfb)
+ {
+ glDeleteFramebuffers_(1, &reflectionfb);
+ reflectionfb = 0;
+ }
+ if(reflectiondb)
+ {
+ glDeleteRenderbuffers_(1, &reflectiondb);
+ reflectiondb = 0;
+ }
+}
+
+VARFP(reflectsize, 6, 8, 11, cleanreflections());
+
+void genwatertex(GLuint &tex, GLuint &fb, GLuint &db, bool refract = false)
+{
+ static const GLenum colorfmts[] = { GL_RGBA, GL_RGBA8, GL_RGB, GL_RGB8, GL_FALSE },
+ depthfmts[] = { GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT32, GL_FALSE };
+ static GLenum reflectfmt = GL_FALSE, refractfmt = GL_FALSE, depthfmt = GL_FALSE;
+ static bool usingalpha = false;
+ bool needsalpha = refract && waterrefract && waterfade;
+ if(refract && usingalpha!=needsalpha)
+ {
+ usingalpha = needsalpha;
+ refractfmt = GL_FALSE;
+ }
+ int size = 1<<reflectsize;
+ while(size>hwtexsize) size /= 2;
+
+ glGenTextures(1, &tex);
+ char *buf = new char[size*size*4];
+ memset(buf, 0, size*size*4);
+
+ GLenum &colorfmt = refract ? refractfmt : reflectfmt;
+ if(colorfmt && fb && db)
+ {
+ createtexture(tex, size, size, buf, 3, 1, colorfmt);
+ delete[] buf;
+ return;
+ }
+
+ if(!fb) glGenFramebuffers_(1, &fb);
+ int find = needsalpha ? 0 : 2;
+ do
+ {
+ createtexture(tex, size, size, buf, 3, 1, colorfmt ? colorfmt : colorfmts[find]);
+ glBindFramebuffer_(GL_FRAMEBUFFER, fb);
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break;
+ }
+ while(!colorfmt && colorfmts[++find]);
+ if(!colorfmt) colorfmt = colorfmts[find];
+
+ delete[] buf;
+
+ if(!db) { glGenRenderbuffers_(1, &db); depthfmt = GL_FALSE; }
+ if(!depthfmt) glBindRenderbuffer_(GL_RENDERBUFFER, db);
+ find = 0;
+ do
+ {
+ if(!depthfmt) glRenderbufferStorage_(GL_RENDERBUFFER, depthfmts[find], size, size);
+ glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, db);
+ if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break;
+ }
+ while(!depthfmt && depthfmts[++find]);
+ if(!depthfmt)
+ {
+ glBindRenderbuffer_(GL_RENDERBUFFER, 0);
+ depthfmt = depthfmts[find];
+ }
+
+ glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+}
+
+void addwaterfallrefraction(materialsurface &m)
+{
+ Reflection &ref = waterfallrefraction;
+ if(ref.age>=0)
+ {
+ ref.age = -1;
+ ref.init = false;
+ ref.matsurfs.setsize(0);
+ ref.material = MAT_WATER;
+ ref.height = INT_MAX;
+ }
+ ref.matsurfs.add(&m);
+
+ if(!ref.refracttex) genwatertex(ref.refracttex, reflectionfb, reflectiondb);
+}
+
+void addreflection(materialsurface &m)
+{
+ int mat = m.material, height = m.o.z;
+ Reflection *ref = NULL, *oldest = NULL;
+ loopi(MAXREFLECTIONS)
+ {
+ Reflection &r = reflections[i];
+ if(r.height<0)
+ {
+ if(!ref) ref = &r;
+ }
+ else if(r.height==height && r.material==mat)
+ {
+ r.matsurfs.add(&m);
+ r.depth = max(r.depth, int(m.depth));
+ if(r.age<0) return;
+ ref = &r;
+ break;
+ }
+ else if(!oldest || r.age>oldest->age) oldest = &r;
+ }
+ if(!ref)
+ {
+ if(!oldest || oldest->age<0) return;
+ ref = oldest;
+ }
+ if(ref->height!=height || ref->material!=mat)
+ {
+ ref->material = mat;
+ ref->height = height;
+ ref->prevquery = NULL;
+ }
+ rplanes++;
+ ref->age = -1;
+ ref->init = false;
+ ref->matsurfs.setsize(0);
+ ref->matsurfs.add(&m);
+ ref->depth = m.depth;
+ if(drawtex == DRAWTEX_MINIMAP) return;
+
+ if(waterreflect && !ref->tex) genwatertex(ref->tex, reflectionfb, reflectiondb);
+ if(waterrefract && !ref->refracttex) genwatertex(ref->refracttex, reflectionfb, reflectiondb, true);
+}
+
+static void drawmaterialquery(const materialsurface &m, float offset, float border = 0, float reflect = -1)
+{
+ if(gle::attribbuf.empty())
+ {
+ gle::defvertex();
+ gle::begin(GL_QUADS);
+ }
+ float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize + border, rsize = m.rsize + border;
+ if(reflect >= 0) z = 2*reflect - z;
+ switch(m.orient)
+ {
+#define GENFACEORIENT(orient, v0, v1, v2, v3) \
+ case orient: v0 v1 v2 v3 break;
+#define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
+ gle::attribf(mx sx, my sy, mz sz);
+ GENFACEVERTS(x, x, y, y, z, z, - border, + csize, - border, + rsize, + offset, - offset)
+#undef GENFACEORIENT
+#undef GENFACEVERT
+ }
+}
+
+extern void drawreflection(float z, bool refract, int fogdepth = -1, const bvec &col = bvec(0, 0, 0));
+
+int rplanes = 0;
+
+void queryreflection(Reflection &ref, bool init)
+{
+ if(init)
+ {
+ nocolorshader->set();
+ glDepthMask(GL_FALSE);
+ glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+ glDisable(GL_CULL_FACE);
+ }
+ startquery(ref.query);
+ loopvj(ref.matsurfs)
+ {
+ materialsurface &m = *ref.matsurfs[j];
+ float offset = 0.1f;
+ if(m.orient==O_TOP)
+ {
+ offset = WATER_OFFSET +
+ (vertwater ? WATER_AMPLITUDE*(camera1->pitch > 0 || m.depth < WATER_AMPLITUDE+0.5f ? -1 : 1) : 0);
+ if(fabs(m.o.z-offset - camera1->o.z) < 0.5f && m.depth > WATER_AMPLITUDE+1.5f)
+ offset += camera1->pitch > 0 ? -1 : 1;
+ }
+ drawmaterialquery(m, offset);
+ }
+ xtraverts += gle::end();
+ endquery(ref.query);
+}
+
+void queryreflections()
+{
+ rplanes = 0;
+
+ static int lastsize = 0;
+ int size = 1<<reflectsize;
+ while(size>hwtexsize) size /= 2;
+ if(size!=lastsize) { if(lastsize) cleanreflections(); lastsize = size; }
+
+ for(vtxarray *va = visibleva; va; va = va->next)
+ {
+ if(!va->matsurfs || va->occluded >= OCCLUDE_BB || va->curvfc >= VFC_FOGGED) continue;
+ int lastmat = -1;
+ loopi(va->matsurfs)
+ {
+ materialsurface &m = va->matbuf[i];
+ if(m.material != lastmat)
+ {
+ if((m.material&MATF_VOLUME) != MAT_WATER || m.orient == O_BOTTOM) { i += m.skip; continue; }
+ if(m.orient != O_TOP)
+ {
+ if(!waterfallrefract || !getwaterfog(m.material)) { i += m.skip; continue; }
+ }
+ lastmat = m.material;
+ }
+ if(m.orient==O_TOP) addreflection(m);
+ else addwaterfallrefraction(m);
+ }
+ }
+
+ loopi(MAXREFLECTIONS)
+ {
+ Reflection &ref = reflections[i];
+ ++ref.age;
+ if(ref.height>=0 && !ref.age && ref.matsurfs.length())
+ {
+ if(waterpvsoccluded(ref.height)) ref.matsurfs.setsize(0);
+ }
+ }
+ if(waterfallrefract)
+ {
+ Reflection &ref = waterfallrefraction;
+ ++ref.age;
+ if(ref.height>=0 && !ref.age && ref.matsurfs.length())
+ {
+ if(waterpvsoccluded(-1)) ref.matsurfs.setsize(0);
+ }
+ }
+
+ if((editmode && showmat && !drawtex) || !oqfrags || !oqwater || drawtex == DRAWTEX_MINIMAP) return;
+
+ int refs = 0;
+ if(waterreflect || waterrefract) loopi(MAXREFLECTIONS)
+ {
+ Reflection &ref = reflections[i];
+ ref.prevquery = oqwater > 1 ? ref.query : NULL;
+ ref.query = ref.height>=0 && !ref.age && ref.matsurfs.length() ? newquery(&ref) : NULL;
+ if(ref.query) queryreflection(ref, !refs++);
+ }
+ if(waterfallrefract)
+ {
+ Reflection &ref = waterfallrefraction;
+ ref.prevquery = oqwater > 1 ? ref.query : NULL;
+ ref.query = ref.height>=0 && !ref.age && ref.matsurfs.length() ? newquery(&ref) : NULL;
+ if(ref.query) queryreflection(ref, !refs++);
+ }
+
+ if(refs)
+ {
+ glDepthMask(GL_TRUE);
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ glEnable(GL_CULL_FACE);
+ }
+
+ glFlush();
+}
+
+VARP(maxreflect, 1, 2, 8);
+
+int refracting = 0, refractfog = 0;
+bvec refractcolor(0, 0, 0);
+bool reflecting = false, fading = false, fogging = false;
+float reflectz = 1e16f;
+
+VAR(maskreflect, 0, 2, 16);
+
+void maskreflection(Reflection &ref, float offset, bool reflect, bool clear = false)
+{
+ const bvec &wcol = getwatercolor(ref.material);
+ vec color = wcol.tocolor();
+ if(!maskreflect)
+ {
+ if(clear) glClearColor(color.r, color.g, color.b, 1);
+ glClear(GL_DEPTH_BUFFER_BIT | (clear ? GL_COLOR_BUFFER_BIT : 0));
+ return;
+ }
+ glClearDepth(0);
+ glClear(GL_DEPTH_BUFFER_BIT);
+ glClearDepth(1);
+ glDepthRange(1, 1);
+ glDepthFunc(GL_ALWAYS);
+ glDisable(GL_CULL_FACE);
+ if(clear)
+ {
+ notextureshader->set();
+ gle::color(color);
+ }
+ else
+ {
+ nocolorshader->set();
+ glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+ }
+ float reflectheight = reflect ? ref.height + offset : -1;
+ loopv(ref.matsurfs)
+ {
+ materialsurface &m = *ref.matsurfs[i];
+ drawmaterialquery(m, -offset, maskreflect, reflectheight);
+ }
+ xtraverts += gle::end();
+ if(!clear) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ glEnable(GL_CULL_FACE);
+ glDepthFunc(GL_LESS);
+ glDepthRange(0, 1);
+}
+
+VAR(reflectscissor, 0, 1, 1);
+VAR(reflectvfc, 0, 1, 1);
+
+static bool calcscissorbox(Reflection &ref, int size, vec &clipmin, vec &clipmax, int &sx, int &sy, int &sw, int &sh)
+{
+ materialsurface &m0 = *ref.matsurfs[0];
+ int dim = dimension(m0.orient), r = R[dim], c = C[dim];
+ ivec bbmin = m0.o, bbmax = bbmin;
+ bbmax[r] += m0.rsize;
+ bbmax[c] += m0.csize;
+ loopvj(ref.matsurfs)
+ {
+ materialsurface &m = *ref.matsurfs[j];
+ bbmin[r] = min(bbmin[r], m.o[r]);
+ bbmin[c] = min(bbmin[c], m.o[c]);
+ bbmax[r] = max(bbmax[r], m.o[r] + m.rsize);
+ bbmax[c] = max(bbmax[c], m.o[c] + m.csize);
+ bbmin[dim] = min(bbmin[dim], m.o[dim]);
+ bbmax[dim] = max(bbmax[dim], m.o[dim]);
+ }
+
+ vec4 v[8];
+ float sx1 = 1, sy1 = 1, sx2 = -1, sy2 = -1;
+ loopi(8)
+ {
+ vec4 &p = v[i];
+ camprojmatrix.transform(vec(i&1 ? bbmax.x : bbmin.x, i&2 ? bbmax.y : bbmin.y, (i&4 ? bbmax.z + WATER_AMPLITUDE : bbmin.z - WATER_AMPLITUDE) - WATER_OFFSET), p);
+ if(p.z >= -p.w)
+ {
+ float x = p.x / p.w, y = p.y / p.w;
+ sx1 = min(sx1, x);
+ sy1 = min(sy1, y);
+ sx2 = max(sx2, x);
+ sy2 = max(sy2, y);
+ }
+ }
+ if(sx1 >= sx2 || sy1 >= sy2) return false;
+ loopi(8)
+ {
+ const vec4 &p = v[i];
+ if(p.z >= -p.w) continue;
+ loopj(3)
+ {
+ const vec4 &o = v[i^(1<<j)];
+ if(o.z <= -o.w) continue;
+ float t = (p.z + p.w)/(p.z + p.w - o.z - o.w),
+ w = p.w + t*(o.w - p.w),
+ x = (p.x + t*(o.x - p.x))/w,
+ y = (p.y + t*(o.y - p.y))/w;
+ sx1 = min(sx1, x);
+ sy1 = min(sy1, y);
+ sx2 = max(sx2, x);
+ sy2 = max(sy2, y);
+ }
+ }
+ if(sx1 <= -1 && sy1 <= -1 && sx2 >= 1 && sy2 >= 1) return false;
+ sx1 = max(sx1, -1.0f);
+ sy1 = max(sy1, -1.0f);
+ sx2 = min(sx2, 1.0f);
+ sy2 = min(sy2, 1.0f);
+ if(reflectvfc)
+ {
+ clipmin.x = clamp(clipmin.x, sx1, sx2);
+ clipmin.y = clamp(clipmin.y, sy1, sy2);
+ clipmax.x = clamp(clipmax.x, sx1, sx2);
+ clipmax.y = clamp(clipmax.y, sy1, sy2);
+ }
+ sx = int(floor((sx1+1)*0.5f*size));
+ sy = int(floor((sy1+1)*0.5f*size));
+ sw = max(int(ceil((sx2+1)*0.5f*size)) - sx, 0);
+ sh = max(int(ceil((sy2+1)*0.5f*size)) - sy, 0);
+ return true;
+}
+
+VARR(refractclear, 0, 0, 1);
+
+void drawreflections()
+{
+ if((editmode && showmat && !drawtex) || drawtex == DRAWTEX_MINIMAP) return;
+
+ static int lastdrawn = 0;
+ int refs = 0, n = lastdrawn;
+ float offset = -WATER_OFFSET;
+ int size = 1<<reflectsize;
+ while(size>hwtexsize) size /= 2;
+
+ if(waterreflect || waterrefract) loopi(MAXREFLECTIONS)
+ {
+ Reflection &ref = reflections[++n%MAXREFLECTIONS];
+ if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue;
+ if(oqfrags && oqwater && ref.query && ref.query->owner==&ref)
+ {
+ if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
+ {
+ if(checkquery(ref.query)) continue;
+ }
+ }
+
+ if(!refs)
+ {
+ glViewport(0, 0, size, size);
+ glBindFramebuffer_(GL_FRAMEBUFFER, reflectionfb);
+ }
+ refs++;
+ ref.init = true;
+ lastdrawn = n;
+
+ vec clipmin(-1, -1, -1), clipmax(1, 1, 1);
+ int sx, sy, sw, sh;
+ bool scissor = reflectscissor && calcscissorbox(ref, size, clipmin, clipmax, sx, sy, sw, sh);
+ if(scissor) glScissor(sx, sy, sw, sh);
+ else
+ {
+ sx = sy = 0;
+ sw = sh = size;
+ }
+
+ const bvec &wcol = getwatercolor(ref.material);
+ int wfog = getwaterfog(ref.material);
+
+ if(waterreflect && ref.tex && camera1->o.z >= ref.height+offset)
+ {
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.tex, 0);
+ if(scissor) glEnable(GL_SCISSOR_TEST);
+ maskreflection(ref, offset, true);
+ savevfcP();
+ setvfcP(ref.height+offset, clipmin, clipmax);
+ drawreflection(ref.height+offset, false);
+ restorevfcP();
+ if(scissor) glDisable(GL_SCISSOR_TEST);
+ }
+
+ if(waterrefract && ref.refracttex)
+ {
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.refracttex, 0);
+ if(scissor) glEnable(GL_SCISSOR_TEST);
+ maskreflection(ref, offset, false, refractclear || !wfog || (ref.depth>=10000 && camera1->o.z >= ref.height + offset));
+ if(wfog || waterfade)
+ {
+ savevfcP();
+ setvfcP(-1, clipmin, clipmax);
+ drawreflection(ref.height+offset, true, wfog, wcol);
+ restorevfcP();
+ }
+ if(scissor) glDisable(GL_SCISSOR_TEST);
+ }
+
+ if(refs>=maxreflect) break;
+ }
+
+ if(waterfallrefract && waterfallrefraction.refracttex)
+ {
+ Reflection &ref = waterfallrefraction;
+
+ if(ref.height<0 || ref.age || ref.matsurfs.empty()) goto nowaterfall;
+ if(oqfrags && oqwater && ref.query && ref.query->owner==&ref)
+ {
+ if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
+ {
+ if(checkquery(ref.query)) goto nowaterfall;
+ }
+ }
+
+ if(!refs)
+ {
+ glViewport(0, 0, size, size);
+ glBindFramebuffer_(GL_FRAMEBUFFER, reflectionfb);
+ }
+ refs++;
+ ref.init = true;
+
+ vec clipmin(-1, -1, -1), clipmax(1, 1, 1);
+ int sx, sy, sw, sh;
+ bool scissor = reflectscissor && calcscissorbox(ref, size, clipmin, clipmax, sx, sy, sw, sh);
+ if(scissor) glScissor(sx, sy, sw, sh);
+ else
+ {
+ sx = sy = 0;
+ sw = sh = size;
+ }
+
+ glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.refracttex, 0);
+ if(scissor) glEnable(GL_SCISSOR_TEST);
+ maskreflection(ref, -0.1f, false);
+ savevfcP();
+ setvfcP(-1, clipmin, clipmax);
+ drawreflection(-1, true);
+ restorevfcP();
+ if(scissor) glDisable(GL_SCISSOR_TEST);
+ }
+nowaterfall:
+
+ if(!refs) return;
+ glViewport(0, 0, screenw, screenh);
+ glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+}
+
diff --git a/src/engine/world.cpp b/src/engine/world.cpp
new file mode 100644
index 0000000..fcf64c2
--- /dev/null
+++ b/src/engine/world.cpp
@@ -0,0 +1,1391 @@
+// world.cpp: core map management stuff
+
+#include "engine.h"
+
+VARR(mapversion, 1, MAPVERSION, 0);
+VARNR(mapscale, worldscale, 1, 0, 0);
+VARNR(mapsize, worldsize, 1, 0, 0);
+SVARR(maptitle, "Untitled Map by Unknown");
+
+VAR(octaentsize, 0, 64, 1024);
+VAR(entselradius, 0, 2, 10);
+
+static inline void mmboundbox(const entity &e, model *m, vec &center, vec &radius)
+{
+ m->boundbox(center, radius);
+ rotatebb(center, radius, e.attr1);
+}
+
+static inline void mmcollisionbox(const entity &e, model *m, vec &center, vec &radius)
+{
+ m->collisionbox(center, radius);
+ rotatebb(center, radius, e.attr1);
+}
+
+bool getentboundingbox(const extentity &e, ivec &o, ivec &r)
+{
+ switch(e.type)
+ {
+ case ET_EMPTY:
+ return false;
+ case ET_MAPMODEL:
+ {
+ model *m = loadmapmodel(e.attr2);
+ if(m)
+ {
+ vec center, radius;
+ mmboundbox(e, m, center, radius);
+ center.add(e.o);
+ radius.max(entselradius);
+ o = ivec(vec(center).sub(radius));
+ r = ivec(vec(center).add(radius).add(1));
+ break;
+ }
+ }
+ // invisible mapmodels use entselradius
+ default:
+ o = ivec(vec(e.o).sub(entselradius));
+ r = ivec(vec(e.o).add(entselradius+1));
+ break;
+ }
+ return true;
+}
+
+enum
+{
+ MODOE_ADD = 1<<0,
+ MODOE_UPDATEBB = 1<<1,
+ MODOE_LIGHTENT = 1<<2
+};
+
+void modifyoctaentity(int flags, int id, extentity &e, cube *c, const ivec &cor, int size, const ivec &bo, const ivec &br, int leafsize, vtxarray *lastva = NULL)
+{
+ loopoctabox(cor, size, bo, br)
+ {
+ ivec o(i, cor, size);
+ vtxarray *va = c[i].ext && c[i].ext->va ? c[i].ext->va : lastva;
+ if(c[i].children != NULL && size > leafsize)
+ modifyoctaentity(flags, id, e, c[i].children, o, size>>1, bo, br, leafsize, va);
+ else if(flags&MODOE_ADD)
+ {
+ if(!c[i].ext || !c[i].ext->ents) ext(c[i]).ents = new octaentities(o, size);
+ octaentities &oe = *c[i].ext->ents;
+ switch(e.type)
+ {
+ case ET_MAPMODEL:
+ if(loadmapmodel(e.attr2))
+ {
+ if(va)
+ {
+ va->bbmin.x = -1;
+ if(oe.mapmodels.empty()) va->mapmodels.add(&oe);
+ }
+ oe.mapmodels.add(id);
+ oe.bbmin.min(bo).max(oe.o);
+ oe.bbmax.max(br).min(ivec(oe.o).add(oe.size));
+ break;
+ }
+ // invisible mapmodel
+ default:
+ oe.other.add(id);
+ break;
+ }
+
+ }
+ else if(c[i].ext && c[i].ext->ents)
+ {
+ octaentities &oe = *c[i].ext->ents;
+ switch(e.type)
+ {
+ case ET_MAPMODEL:
+ if(loadmapmodel(e.attr2))
+ {
+ oe.mapmodels.removeobj(id);
+ if(va)
+ {
+ va->bbmin.x = -1;
+ if(oe.mapmodels.empty()) va->mapmodels.removeobj(&oe);
+ }
+ oe.bbmin = oe.bbmax = oe.o;
+ oe.bbmin.add(oe.size);
+ loopvj(oe.mapmodels)
+ {
+ extentity &e = *entities::getents()[oe.mapmodels[j]];
+ ivec eo, er;
+ if(getentboundingbox(e, eo, er))
+ {
+ oe.bbmin.min(eo);
+ oe.bbmax.max(er);
+ }
+ }
+ oe.bbmin.max(oe.o);
+ oe.bbmax.min(ivec(oe.o).add(oe.size));
+ break;
+ }
+ // invisible mapmodel
+ default:
+ oe.other.removeobj(id);
+ break;
+ }
+ if(oe.mapmodels.empty() && oe.other.empty())
+ freeoctaentities(c[i]);
+ }
+ if(c[i].ext && c[i].ext->ents) c[i].ext->ents->query = NULL;
+ if(va && va!=lastva)
+ {
+ if(lastva)
+ {
+ if(va->bbmin.x < 0) lastva->bbmin.x = -1;
+ }
+ else if(flags&MODOE_UPDATEBB) updatevabb(va);
+ }
+ }
+}
+
+vector<int> outsideents;
+
+static bool modifyoctaent(int flags, int id, extentity &e)
+{
+ if(flags&MODOE_ADD ? e.flags&EF_OCTA : !(e.flags&EF_OCTA)) return false;
+
+ ivec o, r;
+ if(!getentboundingbox(e, o, r)) return false;
+
+ if(!insideworld(e.o))
+ {
+ int idx = outsideents.find(id);
+ if(flags&MODOE_ADD)
+ {
+ if(idx < 0) outsideents.add(id);
+ }
+ else if(idx >= 0) outsideents.removeunordered(idx);
+ }
+ else
+ {
+ int leafsize = octaentsize, limit = max(r.x - o.x, max(r.y - o.y, r.z - o.z));
+ while(leafsize < limit) leafsize *= 2;
+ int diff = ~(leafsize-1) & ((o.x^r.x)|(o.y^r.y)|(o.z^r.z));
+ if(diff && (limit > octaentsize/2 || diff < leafsize*2)) leafsize *= 2;
+ modifyoctaentity(flags, id, e, worldroot, ivec(0, 0, 0), worldsize>>1, o, r, leafsize);
+ }
+ e.flags ^= EF_OCTA;
+ if(e.type == ET_LIGHT) clearlightcache(id);
+ else if(e.type == ET_PARTICLES) clearparticleemitters();
+ else if(flags&MODOE_LIGHTENT) lightent(e);
+ return true;
+}
+
+static inline bool modifyoctaent(int flags, int id)
+{
+ vector<extentity *> &ents = entities::getents();
+ return ents.inrange(id) && modifyoctaent(flags, id, *ents[id]);
+}
+
+static inline void addentity(int id) { modifyoctaent(MODOE_ADD|MODOE_UPDATEBB|MODOE_LIGHTENT, id); }
+static inline void removeentity(int id) { modifyoctaent(MODOE_UPDATEBB, id); }
+
+void freeoctaentities(cube &c)
+{
+ if(!c.ext) return;
+ if(entities::getents().length())
+ {
+ while(c.ext->ents && !c.ext->ents->mapmodels.empty()) removeentity(c.ext->ents->mapmodels.pop());
+ while(c.ext->ents && !c.ext->ents->other.empty()) removeentity(c.ext->ents->other.pop());
+ }
+ if(c.ext->ents)
+ {
+ delete c.ext->ents;
+ c.ext->ents = NULL;
+ }
+}
+
+void entitiesinoctanodes()
+{
+ vector<extentity *> &ents = entities::getents();
+ loopv(ents) modifyoctaent(MODOE_ADD, i, *ents[i]);
+}
+
+static inline void findents(octaentities &oe, int low, int high, bool notspawned, const vec &pos, const vec &invradius, vector<int> &found)
+{
+ vector<extentity *> &ents = entities::getents();
+ loopv(oe.other)
+ {
+ int id = oe.other[i];
+ extentity &e = *ents[id];
+ if(e.type >= low && e.type <= high && (e.spawned() || notspawned) && vec(e.o).sub(pos).mul(invradius).squaredlen() <= 1) found.add(id);
+ }
+}
+
+static inline void findents(cube *c, const ivec &o, int size, const ivec &bo, const ivec &br, int low, int high, bool notspawned, const vec &pos, const vec &invradius, vector<int> &found)
+{
+ loopoctabox(o, size, bo, br)
+ {
+ if(c[i].ext && c[i].ext->ents) findents(*c[i].ext->ents, low, high, notspawned, pos, invradius, found);
+ if(c[i].children && size > octaentsize)
+ {
+ ivec co(i, o, size);
+ findents(c[i].children, co, size>>1, bo, br, low, high, notspawned, pos, invradius, found);
+ }
+ }
+}
+
+void findents(int low, int high, bool notspawned, const vec &pos, const vec &radius, vector<int> &found)
+{
+ vec invradius(1/radius.x, 1/radius.y, 1/radius.z);
+ ivec bo(vec(pos).sub(radius).sub(1)),
+ br(vec(pos).add(radius).add(1));
+ int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z) | octaentsize,
+ scale = worldscale-1;
+ if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|br.x|br.y|br.z) >= uint(worldsize))
+ {
+ findents(worldroot, ivec(0, 0, 0), 1<<scale, bo, br, low, high, notspawned, pos, invradius, found);
+ return;
+ }
+ cube *c = &worldroot[octastep(bo.x, bo.y, bo.z, scale)];
+ if(c->ext && c->ext->ents) findents(*c->ext->ents, low, high, notspawned, pos, invradius, found);
+ scale--;
+ while(c->children && !(diff&(1<<scale)))
+ {
+ c = &c->children[octastep(bo.x, bo.y, bo.z, scale)];
+ if(c->ext && c->ext->ents) findents(*c->ext->ents, low, high, notspawned, pos, invradius, found);
+ scale--;
+ }
+ if(c->children && 1<<scale >= octaentsize) findents(c->children, ivec(bo).mask(~((2<<scale)-1)), 1<<scale, bo, br, low, high, notspawned, pos, invradius, found);
+}
+
+char *entname(entity &e)
+{
+ static string fullentname;
+ copystring(fullentname, entities::entname(e.type));
+ const char *einfo = entities::entnameinfo(e);
+ if(*einfo)
+ {
+ concatstring(fullentname, ": ");
+ concatstring(fullentname, einfo);
+ }
+ return fullentname;
+}
+
+extern selinfo sel;
+extern bool havesel;
+int entlooplevel = 0;
+int efocus = -1, enthover = -1, entorient = -1, oldhover = -1;
+bool undonext = true;
+
+VARF(entediting, 0, 0, 1, { if(!entediting) { entcancel(); efocus = enthover = -1; } });
+
+bool noentedit()
+{
+ if(!editmode) { conoutf(CON_ERROR, "operation only allowed in edit mode"); return true; }
+ return !entediting;
+}
+
+bool pointinsel(const selinfo &sel, const vec &o)
+{
+ return(o.x <= sel.o.x+sel.s.x*sel.grid
+ && o.x >= sel.o.x
+ && o.y <= sel.o.y+sel.s.y*sel.grid
+ && o.y >= sel.o.y
+ && o.z <= sel.o.z+sel.s.z*sel.grid
+ && o.z >= sel.o.z);
+}
+
+vector<int> entgroup;
+
+bool haveselent()
+{
+ return entgroup.length() > 0;
+}
+
+void entcancel()
+{
+ entgroup.shrink(0);
+}
+
+void entadd(int id)
+{
+ undonext = true;
+ entgroup.add(id);
+}
+
+undoblock *newundoent()
+{
+ int numents = entgroup.length();
+ if(numents <= 0) return NULL;
+ undoblock *u = (undoblock *)new uchar[sizeof(undoblock) + numents*sizeof(undoent)];
+ u->numents = numents;
+ undoent *e = (undoent *)(u + 1);
+ loopv(entgroup)
+ {
+ e->i = entgroup[i];
+ e->e = *entities::getents()[entgroup[i]];
+ e++;
+ }
+ return u;
+}
+
+void makeundoent()
+{
+ if(!undonext) return;
+ undonext = false;
+ oldhover = enthover;
+ undoblock *u = newundoent();
+ if(u) addundo(u);
+}
+
+void detachentity(extentity &e)
+{
+ if(!e.attached) return;
+ e.attached->attached = NULL;
+ e.attached = NULL;
+}
+
+VAR(attachradius, 1, 100, 1000);
+
+void attachentity(extentity &e)
+{
+ switch(e.type)
+ {
+ case ET_SPOTLIGHT:
+ break;
+
+ default:
+ if(e.type<ET_GAMESPECIFIC || !entities::mayattach(e)) return;
+ break;
+ }
+
+ detachentity(e);
+
+ vector<extentity *> &ents = entities::getents();
+ int closest = -1;
+ float closedist = 1e10f;
+ loopv(ents)
+ {
+ extentity *a = ents[i];
+ if(a->attached) continue;
+ switch(e.type)
+ {
+ case ET_SPOTLIGHT:
+ if(a->type!=ET_LIGHT) continue;
+ break;
+
+ default:
+ if(e.type<ET_GAMESPECIFIC || !entities::attachent(e, *a)) continue;
+ break;
+ }
+ float dist = e.o.dist(a->o);
+ if(dist < closedist)
+ {
+ closest = i;
+ closedist = dist;
+ }
+ }
+ if(closedist>attachradius) return;
+ e.attached = ents[closest];
+ ents[closest]->attached = &e;
+}
+
+void attachentities()
+{
+ vector<extentity *> &ents = entities::getents();
+ loopv(ents) attachentity(*ents[i]);
+}
+
+// convenience macros implicitly define:
+// e entity, currently edited ent
+// n int, index to currently edited ent
+#define addimplicit(f) { if(entgroup.empty() && enthover>=0) { entadd(enthover); undonext = (enthover != oldhover); f; entgroup.drop(); } else f; }
+#define entfocusv(i, f, v){ int n = efocus = (i); if(n>=0) { extentity &e = *v[n]; f; } }
+#define entfocus(i, f) entfocusv(i, f, entities::getents())
+#define enteditv(i, f, v) \
+{ \
+ entfocusv(i, \
+ { \
+ int oldtype = e.type; \
+ removeentity(n); \
+ f; \
+ if(oldtype!=e.type) detachentity(e); \
+ if(e.type!=ET_EMPTY) { addentity(n); if(oldtype!=e.type) attachentity(e); } \
+ entities::editent(n, true); \
+ }, v); \
+}
+#define entedit(i, f) enteditv(i, f, entities::getents())
+#define addgroup(exp) { vector<extentity *> &ents = entities::getents(); loopv(ents) entfocusv(i, if(exp) entadd(n), ents); }
+#define setgroup(exp) { entcancel(); addgroup(exp); }
+#define groupeditloop(f){ vector<extentity *> &ents = entities::getents(); entlooplevel++; int _ = efocus; loopv(entgroup) enteditv(entgroup[i], f, ents); efocus = _; entlooplevel--; }
+#define groupeditpure(f){ if(entlooplevel>0) { entedit(efocus, f); } else groupeditloop(f); }
+#define groupeditundo(f){ makeundoent(); groupeditpure(f); }
+#define groupedit(f) { addimplicit(groupeditundo(f)); }
+
+vec getselpos()
+{
+ vector<extentity *> &ents = entities::getents();
+ if(entgroup.length() && ents.inrange(entgroup[0])) return ents[entgroup[0]]->o;
+ if(ents.inrange(enthover)) return ents[enthover]->o;
+ return vec(sel.o);
+}
+
+undoblock *copyundoents(undoblock *u)
+{
+ entcancel();
+ undoent *e = u->ents();
+ loopi(u->numents)
+ entadd(e[i].i);
+ undoblock *c = newundoent();
+ loopi(u->numents) if(e[i].e.type==ET_EMPTY)
+ entgroup.removeobj(e[i].i);
+ return c;
+}
+
+void pasteundoent(int idx, const entity &ue)
+{
+ if(idx < 0 || idx >= MAXENTS) return;
+ vector<extentity *> &ents = entities::getents();
+ while(ents.length() < idx) ents.add(entities::newentity())->type = ET_EMPTY;
+ int efocus = -1;
+ entedit(idx, (entity &)e = ue);
+}
+
+void pasteundoents(undoblock *u)
+{
+ undoent *ue = u->ents();
+ loopi(u->numents)
+ entedit(ue[i].i, (entity &)e = ue[i].e);
+}
+
+void entflip()
+{
+ if(noentedit()) return;
+ int d = dimension(sel.orient);
+ float mid = sel.s[d]*sel.grid/2+sel.o[d];
+ groupeditundo(e.o[d] -= (e.o[d]-mid)*2);
+}
+
+void entrotate(int *cw)
+{
+ if(noentedit()) return;
+ int d = dimension(sel.orient);
+ int dd = (*cw<0) == dimcoord(sel.orient) ? R[d] : C[d];
+ float mid = sel.s[dd]*sel.grid/2+sel.o[dd];
+ vec s(sel.o.v);
+ groupeditundo(
+ e.o[dd] -= (e.o[dd]-mid)*2;
+ e.o.sub(s);
+ swap(e.o[R[d]], e.o[C[d]]);
+ e.o.add(s);
+ );
+}
+
+void entselectionbox(const entity &e, vec &eo, vec &es)
+{
+ model *m = NULL;
+ const char *mname = entities::entmodel(e);
+ if(mname && (m = loadmodel(mname)))
+ {
+ m->collisionbox(eo, es);
+ if(es.x > es.y) es.y = es.x; else es.x = es.y; // square
+ es.z = (es.z + eo.z + 1 + entselradius)/2; // enclose ent radius box and model box
+ eo.x += e.o.x;
+ eo.y += e.o.y;
+ eo.z = e.o.z - entselradius + es.z;
+ }
+ else if(e.type == ET_MAPMODEL && (m = loadmapmodel(e.attr2)))
+ {
+ mmcollisionbox(e, m, eo, es);
+ es.max(entselradius);
+ eo.add(e.o);
+ }
+ else
+ {
+ es = vec(entselradius);
+ eo = e.o;
+ }
+ eo.sub(es);
+ es.mul(2);
+}
+
+VAR(entselsnap, 0, 0, 1);
+VAR(entmovingshadow, 0, 1, 1);
+
+extern void boxs(int orient, vec o, const vec &s, float size);
+extern void boxs(int orient, vec o, const vec &s);
+extern void boxs3D(const vec &o, vec s, int g);
+extern bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first);
+
+int entmoving = 0;
+
+void entdrag(const vec &ray)
+{
+ if(noentedit() || !haveselent()) return;
+
+ float r = 0, c = 0;
+ static vec v, handle;
+ vec eo, es;
+ int d = dimension(entorient),
+ dc= dimcoord(entorient);
+
+ entfocus(entgroup.last(),
+ entselectionbox(e, eo, es);
+
+ if(!editmoveplane(e.o, ray, d, eo[d] + (dc ? es[d] : 0), handle, v, entmoving==1))
+ return;
+
+ ivec g(v);
+ int z = g[d]&(~(sel.grid-1));
+ g.add(sel.grid/2).mask(~(sel.grid-1));
+ g[d] = z;
+
+ r = (entselsnap ? g[R[d]] : v[R[d]]) - e.o[R[d]];
+ c = (entselsnap ? g[C[d]] : v[C[d]]) - e.o[C[d]];
+ );
+
+ if(entmoving==1) makeundoent();
+ groupeditpure(e.o[R[d]] += r; e.o[C[d]] += c);
+ entmoving = 2;
+}
+
+VAR(showentradius, 0, 1, 1);
+
+void renderentring(const extentity &e, float radius, int axis)
+{
+ if(radius <= 0) return;
+ gle::defvertex();
+ gle::begin(GL_LINE_LOOP);
+ loopi(15)
+ {
+ vec p(e.o);
+ const vec2 &sc = sincos360[i*(360/15)];
+ p[axis>=2 ? 1 : 0] += radius*sc.x;
+ p[axis>=1 ? 2 : 1] += radius*sc.y;
+ gle::attrib(p);
+ }
+ xtraverts += gle::end();
+}
+
+void renderentsphere(const extentity &e, float radius)
+{
+ if(radius <= 0) return;
+ loopk(3) renderentring(e, radius, k);
+}
+
+void renderentattachment(const extentity &e)
+{
+ if(!e.attached) return;
+ gle::defvertex();
+ gle::begin(GL_LINES);
+ gle::attrib(e.o);
+ gle::attrib(e.attached->o);
+ xtraverts += gle::end();
+}
+
+void renderentarrow(const extentity &e, const vec &dir, float radius)
+{
+ if(radius <= 0) return;
+ float arrowsize = min(radius/8, 0.5f);
+ vec target = vec(dir).mul(radius).add(e.o), arrowbase = vec(dir).mul(radius - arrowsize).add(e.o), spoke;
+ spoke.orthogonal(dir);
+ spoke.normalize();
+ spoke.mul(arrowsize);
+
+ gle::defvertex();
+
+ gle::begin(GL_LINES);
+ gle::attrib(e.o);
+ gle::attrib(target);
+ xtraverts += gle::end();
+
+ gle::begin(GL_TRIANGLE_FAN);
+ gle::attrib(target);
+ loopi(5) gle::attrib(vec(spoke).rotate(2*M_PI*i/4.0f, dir).add(arrowbase));
+ xtraverts += gle::end();
+}
+
+void renderentcone(const extentity &e, const vec &dir, float radius, float angle)
+{
+ if(radius <= 0) return;
+ vec spot = vec(dir).mul(radius*cosf(angle*RAD)).add(e.o), spoke;
+ spoke.orthogonal(dir);
+ spoke.normalize();
+ spoke.mul(radius*sinf(angle*RAD));
+
+ gle::defvertex();
+
+ gle::begin(GL_LINES);
+ loopi(8)
+ {
+ gle::attrib(e.o);
+ gle::attrib(vec(spoke).rotate(2*M_PI*i/8.0f, dir).add(spot));
+ }
+ xtraverts += gle::end();
+
+ gle::begin(GL_LINE_LOOP);
+ loopi(8) gle::attrib(vec(spoke).rotate(2*M_PI*i/8.0f, dir).add(spot));
+ xtraverts += gle::end();
+}
+
+void renderentradius(extentity &e, bool color)
+{
+ switch(e.type)
+ {
+ case ET_LIGHT:
+ if(color) gle::colorf(e.attr2/255.0f, e.attr3/255.0f, e.attr4/255.0f);
+ renderentsphere(e, e.attr1);
+ break;
+
+ case ET_SPOTLIGHT:
+ if(e.attached)
+ {
+ if(color) gle::colorf(0, 1, 1);
+ float radius = e.attached->attr1;
+ if(!radius) radius = 2*e.o.dist(e.attached->o);
+ vec dir = vec(e.o).sub(e.attached->o).normalize();
+ float angle = clamp(int(e.attr1), 1, 89);
+ renderentattachment(e);
+ renderentcone(*e.attached, dir, radius, angle);
+ }
+ break;
+
+ case ET_SOUND:
+ if(color) gle::colorf(0, 1, 1);
+ renderentsphere(e, e.attr2);
+ break;
+
+ case ET_ENVMAP:
+ {
+ extern int envmapradius;
+ if(color) gle::colorf(0, 1, 1);
+ renderentsphere(e, e.attr1 ? max(0, min(10000, int(e.attr1))) : envmapradius);
+ break;
+ }
+
+ case ET_MAPMODEL:
+ case ET_PLAYERSTART:
+ {
+ if(color) gle::colorf(0, 1, 1);
+ entities::entradius(e, color);
+ vec dir;
+ vecfromyawpitch(e.attr1, 0, 1, 0, dir);
+ renderentarrow(e, dir, 4);
+ break;
+ }
+
+ default:
+ if(e.type>=ET_GAMESPECIFIC)
+ {
+ if(color) gle::colorf(0, 1, 1);
+ entities::entradius(e, color);
+ }
+ break;
+ }
+}
+
+static void renderentbox(const vec &eo, vec es)
+{
+ es.add(eo);
+
+ // bottom quad
+ gle::attrib(eo.x, eo.y, eo.z); gle::attrib(es.x, eo.y, eo.z);
+ gle::attrib(es.x, eo.y, eo.z); gle::attrib(es.x, es.y, eo.z);
+ gle::attrib(es.x, es.y, eo.z); gle::attrib(eo.x, es.y, eo.z);
+ gle::attrib(eo.x, es.y, eo.z); gle::attrib(eo.x, eo.y, eo.z);
+
+ // top quad
+ gle::attrib(eo.x, eo.y, es.z); gle::attrib(es.x, eo.y, es.z);
+ gle::attrib(es.x, eo.y, es.z); gle::attrib(es.x, es.y, es.z);
+ gle::attrib(es.x, es.y, es.z); gle::attrib(eo.x, es.y, es.z);
+ gle::attrib(eo.x, es.y, es.z); gle::attrib(eo.x, eo.y, es.z);
+
+ // sides
+ gle::attrib(eo.x, eo.y, eo.z); gle::attrib(eo.x, eo.y, es.z);
+ gle::attrib(es.x, eo.y, eo.z); gle::attrib(es.x, eo.y, es.z);
+ gle::attrib(es.x, es.y, eo.z); gle::attrib(es.x, es.y, es.z);
+ gle::attrib(eo.x, es.y, eo.z); gle::attrib(eo.x, es.y, es.z);
+}
+
+void renderentselection(const vec &o, const vec &ray, bool entmoving)
+{
+ if(noentedit()) return;
+ vec eo, es;
+
+ if(entgroup.length())
+ {
+ gle::colorub(0, 40, 0);
+ gle::defvertex();
+ gle::begin(GL_LINES, entgroup.length()*24);
+ loopv(entgroup) entfocus(entgroup[i],
+ entselectionbox(e, eo, es);
+ renderentbox(eo, es);
+ );
+ xtraverts += gle::end();
+ }
+
+ if(enthover >= 0)
+ {
+ gle::colorub(0, 40, 0);
+ entfocus(enthover, entselectionbox(e, eo, es)); // also ensures enthover is back in focus
+ boxs3D(eo, es, 1);
+ if(entmoving && entmovingshadow==1)
+ {
+ vec a, b;
+ gle::colorub(20, 20, 20);
+ (a = eo).x = eo.x - fmod(eo.x, worldsize); (b = es).x = a.x + worldsize; boxs3D(a, b, 1);
+ (a = eo).y = eo.y - fmod(eo.y, worldsize); (b = es).y = a.x + worldsize; boxs3D(a, b, 1);
+ (a = eo).z = eo.z - fmod(eo.z, worldsize); (b = es).z = a.x + worldsize; boxs3D(a, b, 1);
+ }
+ gle::colorub(150,0,0);
+ boxs(entorient, eo, es);
+ boxs(entorient, eo, es, clamp(0.015f*camera1->o.dist(eo)*tan(fovy*0.5f*RAD), 0.1f, 1.0f));
+ }
+
+ if(showentradius && (entgroup.length() || enthover >= 0))
+ {
+ glDepthFunc(GL_GREATER);
+ gle::colorf(0.25f, 0.25f, 0.25f);
+ loopv(entgroup) entfocus(entgroup[i], renderentradius(e, false));
+ if(enthover>=0) entfocus(enthover, renderentradius(e, false));
+ glDepthFunc(GL_LESS);
+ loopv(entgroup) entfocus(entgroup[i], renderentradius(e, true));
+ if(enthover>=0) entfocus(enthover, renderentradius(e, true));
+ }
+}
+
+bool enttoggle(int id)
+{
+ undonext = true;
+ int i = entgroup.find(id);
+ if(i < 0)
+ entadd(id);
+ else
+ entgroup.remove(i);
+ return i < 0;
+}
+
+bool hoveringonent(int ent, int orient)
+{
+ if(noentedit()) return false;
+ entorient = orient;
+ if((efocus = enthover = ent) >= 0)
+ return true;
+ efocus = entgroup.empty() ? -1 : entgroup.last();
+ enthover = -1;
+ return false;
+}
+
+VAR(entitysurf, 0, 0, 1);
+
+ICOMMAND(entadd, "", (),
+{
+ if(enthover >= 0 && !noentedit())
+ {
+ if(entgroup.find(enthover) < 0) entadd(enthover);
+ if(entmoving > 1) entmoving = 1;
+ }
+});
+
+ICOMMAND(enttoggle, "", (),
+{
+ if(enthover < 0 || noentedit() || !enttoggle(enthover)) { entmoving = 0; intret(0); }
+ else { if(entmoving > 1) entmoving = 1; intret(1); }
+});
+
+ICOMMAND(entmoving, "b", (int *n),
+{
+ if(*n >= 0)
+ {
+ if(!*n || enthover < 0 || noentedit()) entmoving = 0;
+ else
+ {
+ if(entgroup.find(enthover) < 0) { entadd(enthover); entmoving = 1; }
+ else if(!entmoving) entmoving = 1;
+ }
+ }
+ intret(entmoving);
+});
+
+void entpush(int *dir)
+{
+ if(noentedit()) return;
+ int d = dimension(entorient);
+ int s = dimcoord(entorient) ? -*dir : *dir;
+ if(entmoving)
+ {
+ groupeditpure(e.o[d] += float(s*sel.grid)); // editdrag supplies the undo
+ }
+ else
+ groupedit(e.o[d] += float(s*sel.grid));
+ if(entitysurf==1)
+ {
+ player->o[d] += float(s*sel.grid);
+ player->resetinterp();
+ }
+}
+
+VAR(entautoviewdist, 0, 25, 100);
+void entautoview(int *dir)
+{
+ if(!haveselent()) return;
+ static int s = 0;
+ vec v(player->o);
+ v.sub(worldpos);
+ v.normalize();
+ v.mul(entautoviewdist);
+ int t = s + *dir;
+ s = abs(t) % entgroup.length();
+ if(t<0 && s>0) s = entgroup.length() - s;
+ entfocus(entgroup[s],
+ v.add(e.o);
+ player->o = v;
+ player->resetinterp();
+ );
+}
+
+COMMAND(entautoview, "i");
+COMMAND(entflip, "");
+COMMAND(entrotate, "i");
+COMMAND(entpush, "i");
+
+void delent()
+{
+ if(noentedit()) return;
+ groupedit(e.type = ET_EMPTY;);
+ entcancel();
+}
+
+int findtype(char *what)
+{
+ for(int i = 0; *entities::entname(i); i++) if(strcmp(what, entities::entname(i))==0) return i;
+ conoutf(CON_ERROR, "unknown entity type \"%s\"", what);
+ return ET_EMPTY;
+}
+
+VAR(entdrop, 0, 2, 3);
+
+bool dropentity(entity &e, int drop = -1)
+{
+ vec radius(4.0f, 4.0f, 4.0f);
+ if(drop<0) drop = entdrop;
+ if(e.type == ET_MAPMODEL)
+ {
+ model *m = loadmapmodel(e.attr2);
+ if(m)
+ {
+ vec center;
+ mmboundbox(e, m, center, radius);
+ radius.x += fabs(center.x);
+ radius.y += fabs(center.y);
+ }
+ radius.z = 0.0f;
+ }
+ switch(drop)
+ {
+ case 1:
+ if(e.type != ET_LIGHT && e.type != ET_SPOTLIGHT)
+ dropenttofloor(&e);
+ break;
+ case 2:
+ case 3:
+ int cx = 0, cy = 0;
+ if(sel.cxs == 1 && sel.cys == 1)
+ {
+ cx = (sel.cx ? 1 : -1) * sel.grid / 2;
+ cy = (sel.cy ? 1 : -1) * sel.grid / 2;
+ }
+ e.o = vec(sel.o);
+ int d = dimension(sel.orient), dc = dimcoord(sel.orient);
+ e.o[R[d]] += sel.grid / 2 + cx;
+ e.o[C[d]] += sel.grid / 2 + cy;
+ if(!dc)
+ e.o[D[d]] -= radius[D[d]];
+ else
+ e.o[D[d]] += sel.grid + radius[D[d]];
+
+ if(drop == 3)
+ dropenttofloor(&e);
+ break;
+ }
+ return true;
+}
+
+void dropent()
+{
+ if(noentedit()) return;
+ groupedit(dropentity(e));
+}
+
+void attachent()
+{
+ if(noentedit()) return;
+ groupedit(attachentity(e));
+}
+
+COMMAND(attachent, "");
+
+VARP(entcamdir, 0, 1, 1);
+
+static int keepents = 0;
+
+extentity *newentity(bool local, const vec &o, int type, int v1, int v2, int v3, int v4, int v5, int &idx)
+{
+ vector<extentity *> &ents = entities::getents();
+ if(local)
+ {
+ idx = -1;
+ for(int i = keepents; i < ents.length(); i++) if(ents[i]->type == ET_EMPTY) { idx = i; break; }
+ if(idx < 0 && ents.length() >= MAXENTS) { conoutf(CON_ERROR, "too many entities"); return NULL; }
+ }
+ else while(ents.length() < idx) ents.add(entities::newentity())->type = ET_EMPTY;
+ extentity &e = *entities::newentity();
+ e.o = o;
+ e.attr1 = v1;
+ e.attr2 = v2;
+ e.attr3 = v3;
+ e.attr4 = v4;
+ e.attr5 = v5;
+ e.type = type;
+ e.reserved = 0;
+ e.light.color = vec(1, 1, 1);
+ e.light.dir = vec(0, 0, 1);
+ if(local)
+ {
+ if(entcamdir) switch(type)
+ {
+ case ET_MAPMODEL:
+ case ET_PLAYERSTART:
+ e.attr5 = e.attr4;
+ e.attr4 = e.attr3;
+ e.attr3 = e.attr2;
+ e.attr2 = e.attr1;
+ e.attr1 = (int)camera1->yaw;
+ break;
+ }
+ entities::fixentity(e);
+ }
+ if(ents.inrange(idx)) { entities::deleteentity(ents[idx]); ents[idx] = &e; }
+ else { idx = ents.length(); ents.add(&e); }
+ return &e;
+}
+
+void newentity(int type, int a1, int a2, int a3, int a4, int a5)
+{
+ int idx;
+ extentity *t = newentity(true, player->o, type, a1, a2, a3, a4, a5, idx);
+ if(!t) return;
+ dropentity(*t);
+ t->type = ET_EMPTY;
+ enttoggle(idx);
+ makeundoent();
+ entedit(idx, e.type = type);
+}
+
+void newent(char *what, int *a1, int *a2, int *a3, int *a4, int *a5)
+{
+ if(noentedit()) return;
+ int type = findtype(what);
+ if(type != ET_EMPTY)
+ newentity(type, *a1, *a2, *a3, *a4, *a5);
+}
+
+int entcopygrid;
+vector<entity> entcopybuf;
+
+void entcopy()
+{
+ if(noentedit()) return;
+ entcopygrid = sel.grid;
+ entcopybuf.shrink(0);
+ loopv(entgroup)
+ entfocus(entgroup[i], entcopybuf.add(e).o.sub(vec(sel.o)));
+}
+
+void entpaste()
+{
+ if(noentedit()) return;
+ if(entcopybuf.length()==0) return;
+ entcancel();
+ float m = float(sel.grid)/float(entcopygrid);
+ loopv(entcopybuf)
+ {
+ entity &c = entcopybuf[i];
+ vec o(c.o);
+ o.mul(m).add(vec(sel.o));
+ int idx;
+ extentity *e = newentity(true, o, ET_EMPTY, c.attr1, c.attr2, c.attr3, c.attr4, c.attr5, idx);
+ if(!e) continue;
+ entadd(idx);
+ keepents = max(keepents, idx+1);
+ }
+ keepents = 0;
+ int j = 0;
+ groupeditundo(e.type = entcopybuf[j++].type;);
+}
+
+COMMAND(newent, "siiiii");
+COMMAND(delent, "");
+COMMAND(dropent, "");
+COMMAND(entcopy, "");
+COMMAND(entpaste, "");
+
+void entset(char *what, int *a1, int *a2, int *a3, int *a4, int *a5)
+{
+ if(noentedit()) return;
+ int type = findtype(what);
+ if(type != ET_EMPTY)
+ groupedit(e.type=type;
+ e.attr1=*a1;
+ e.attr2=*a2;
+ e.attr3=*a3;
+ e.attr4=*a4;
+ e.attr5=*a5);
+}
+
+void printent(extentity &e, char *buf, int len)
+{
+ switch(e.type)
+ {
+ case ET_PARTICLES:
+ if(printparticles(e, buf, len)) return;
+ break;
+
+ default:
+ if(e.type >= ET_GAMESPECIFIC && entities::printent(e, buf, len)) return;
+ break;
+ }
+ nformatstring(buf, len, "%s %d %d %d %d %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+}
+
+void nearestent()
+{
+ if(noentedit()) return;
+ int closest = -1;
+ float closedist = 1e16f;
+ vector<extentity *> &ents = entities::getents();
+ loopv(ents)
+ {
+ extentity &e = *ents[i];
+ if(e.type == ET_EMPTY) continue;
+ float dist = e.o.dist(player->o);
+ if(dist < closedist)
+ {
+ closest = i;
+ closedist = dist;
+ }
+ }
+ if(closest >= 0 && entgroup.find(closest) < 0) entadd(closest);
+}
+
+ICOMMAND(enthavesel,"", (), addimplicit(intret(entgroup.length())));
+ICOMMAND(entselect, "e", (uint *body), if(!noentedit()) addgroup(e.type != ET_EMPTY && entgroup.find(n)<0 && executebool(body)));
+ICOMMAND(entloop, "e", (uint *body), if(!noentedit()) addimplicit(groupeditloop(((void)e, execute(body)))));
+ICOMMAND(insel, "", (), entfocus(efocus, intret(pointinsel(sel, e.o))));
+ICOMMAND(entget, "", (), entfocus(efocus, string s; printent(e, s, sizeof(s)); result(s)));
+ICOMMAND(entindex, "", (), intret(efocus));
+COMMAND(entset, "siiiii");
+COMMAND(nearestent, "");
+
+void enttype(char *type, int *numargs)
+{
+ if(*numargs >= 1)
+ {
+ int typeidx = findtype(type);
+ if(typeidx != ET_EMPTY) groupedit(e.type = typeidx);
+ }
+ else entfocus(efocus,
+ {
+ result(entities::entname(e.type));
+ })
+}
+
+void entattr(int *attr, int *val, int *numargs)
+{
+ if(*numargs >= 2)
+ {
+ if(*attr >= 0 && *attr <= 4)
+ groupedit(
+ switch(*attr)
+ {
+ case 0: e.attr1 = *val; break;
+ case 1: e.attr2 = *val; break;
+ case 2: e.attr3 = *val; break;
+ case 3: e.attr4 = *val; break;
+ case 4: e.attr5 = *val; break;
+ }
+ );
+ }
+ else entfocus(efocus,
+ {
+ switch(*attr)
+ {
+ case 0: intret(e.attr1); break;
+ case 1: intret(e.attr2); break;
+ case 2: intret(e.attr3); break;
+ case 3: intret(e.attr4); break;
+ case 4: intret(e.attr5); break;
+ }
+ });
+}
+
+COMMAND(enttype, "sN");
+COMMAND(entattr, "iiN");
+
+int findentity(int type, int index, int attr1, int attr2)
+{
+ const vector<extentity *> &ents = entities::getents();
+ if(index > ents.length()) index = ents.length();
+ else for(int i = index; i<ents.length(); i++)
+ {
+ extentity &e = *ents[i];
+ if(e.type==type && (attr1<0 || e.attr1==attr1) && (attr2<0 || e.attr2==attr2))
+ return i;
+ }
+ loopj(index)
+ {
+ extentity &e = *ents[j];
+ if(e.type==type && (attr1<0 || e.attr1==attr1) && (attr2<0 || e.attr2==attr2))
+ return j;
+ }
+ return -1;
+}
+
+struct spawninfo { const extentity *e; float weight; };
+
+// Compiles a vector of available playerstarts, each with a non-zero weight
+// which serves as a measure of its desirability for a spawning player.
+float gatherspawninfos(dynent *d, int tag, vector<spawninfo> &spawninfos)
+{
+ const vector<extentity *> &ents = entities::getents();
+ float total = 0.0f;
+ loopv(ents)
+ {
+ const extentity &e = *ents[i];
+ if(e.type != ET_PLAYERSTART || e.attr2 != tag) continue;
+ spawninfo &s = spawninfos.add();
+ s.e = &e;
+ s.weight = game::ratespawn(d, e);
+ total += s.weight;
+ }
+ return total;
+}
+
+// Randomly picks a weighted spawn from the provided vector and removes it.
+// The probability of a given spawn being picked is proportional to its weight.
+// If all weights are zero, the index is picked uniformly.
+static const extentity *poprandomspawn(vector<spawninfo> &spawninfos, float &total)
+{
+ if(spawninfos.empty()) return NULL;
+ int index = 0;
+ if(total > 0.0f)
+ {
+ float x = rndscale(total);
+ do x -= spawninfos[index].weight; while(x > 0 && ++index < spawninfos.length()-1);
+ }
+ else index = rnd(spawninfos.length());
+ spawninfo s = spawninfos.removeunordered(index);
+ total -= s.weight;
+ return s.e;
+}
+
+static inline bool tryspawn(dynent *d, const extentity &e)
+{
+ d->o = e.o;
+ d->yaw = e.attr1;
+ return entinmap(d, true);
+}
+
+void findplayerspawn(dynent *d, int forceent, int tag)
+{
+ const vector<extentity *> &ents = entities::getents();
+ d->pitch = 0;
+ d->roll = 0;
+ if(ents.inrange(forceent) && tryspawn(d, *ents[forceent])) return;
+ vector<spawninfo> spawninfos;
+ float total = gatherspawninfos(d, tag, spawninfos);
+ while(const extentity *e = poprandomspawn(spawninfos, total)) if(tryspawn(d, *e)) return;
+ d->o = vec(0.5f * worldsize).addz(1);
+ d->yaw = 0;
+ entinmap(d);
+}
+
+void splitocta(cube *c, int size)
+{
+ if(size <= 0x1000) return;
+ loopi(8)
+ {
+ if(!c[i].children) c[i].children = newcubes(isempty(c[i]) ? F_EMPTY : F_SOLID);
+ splitocta(c[i].children, size>>1);
+ }
+}
+
+void resetmap()
+{
+ clearoverrides();
+ clearmapsounds();
+ cleanreflections();
+ resetblendmap();
+ resetlightmaps();
+ clearpvs();
+ clearslots();
+ clearparticles();
+ cleardecals();
+ cleardamagescreen();
+ clearsleep();
+ cancelsel();
+ pruneundos();
+ clearmapcrc();
+
+ entities::clearents();
+ outsideents.setsize(0);
+}
+
+void startmap(const char *name)
+{
+ game::startmap(name);
+}
+
+bool emptymap(int scale, bool force, const char *mname, bool usecfg) // main empty world creation routine
+{
+ if(!force && !editmode)
+ {
+ conoutf(CON_ERROR, "newmap only allowed in edit mode");
+ return false;
+ }
+
+ resetmap();
+
+ setvar("mapscale", scale<10 ? 10 : (scale>16 ? 16 : scale), true, false);
+ setvar("mapsize", 1<<worldscale, true, false);
+
+ texmru.shrink(0);
+ freeocta(worldroot);
+ worldroot = newcubes(F_EMPTY);
+ loopi(4) solidfaces(worldroot[i]);
+
+ if(worldsize > 0x1000) splitocta(worldroot, worldsize>>1);
+
+ clearmainmenu();
+
+ if(usecfg)
+ {
+ identflags |= IDF_OVERRIDDEN;
+ execfile("data/default_map_settings.cfg", false);
+ identflags &= ~IDF_OVERRIDDEN;
+ }
+
+ initlights();
+ allchanged(true);
+
+ startmap(mname);
+
+ return true;
+}
+
+bool enlargemap(bool force)
+{
+ if(!force && !editmode)
+ {
+ conoutf(CON_ERROR, "mapenlarge only allowed in edit mode");
+ return false;
+ }
+ if(worldsize >= 1<<16) return false;
+
+ while(outsideents.length()) removeentity(outsideents.pop());
+
+ worldscale++;
+ worldsize *= 2;
+ cube *c = newcubes(F_EMPTY);
+ c[0].children = worldroot;
+ loopi(3) solidfaces(c[i+1]);
+ worldroot = c;
+
+ if(worldsize > 0x1000) splitocta(worldroot, worldsize>>1);
+
+ enlargeblendmap();
+
+ allchanged();
+
+ return true;
+}
+
+static bool isallempty(cube &c)
+{
+ if(!c.children) return isempty(c);
+ loopi(8) if(!isallempty(c.children[i])) return false;
+ return true;
+}
+
+void shrinkmap()
+{
+ extern int nompedit;
+ if(noedit(true) || (nompedit && multiplayer())) return;
+ if(worldsize <= 1<<10) return;
+
+ int octant = -1;
+ loopi(8) if(!isallempty(worldroot[i]))
+ {
+ if(octant >= 0) return;
+ octant = i;
+ }
+ if(octant < 0) return;
+
+ while(outsideents.length()) removeentity(outsideents.pop());
+
+ if(!worldroot[octant].children) subdividecube(worldroot[octant], false, false);
+ cube *root = worldroot[octant].children;
+ worldroot[octant].children = NULL;
+ freeocta(worldroot);
+ worldroot = root;
+ worldscale--;
+ worldsize /= 2;
+
+ ivec offset(octant, ivec(0, 0, 0), worldsize);
+ vector<extentity *> &ents = entities::getents();
+ loopv(ents) ents[i]->o.sub(vec(offset));
+
+ shrinkblendmap(octant);
+
+ allchanged();
+
+ conoutf("shrunk map to size %d", worldscale);
+}
+
+void newmap(int *i) { bool force = !isconnected(); if(force) game::forceedit(""); if(emptymap(*i, force, NULL)) game::newmap(max(*i, 0)); }
+void mapenlarge() { if(enlargemap(false)) game::newmap(-1); }
+COMMAND(newmap, "i");
+COMMAND(mapenlarge, "");
+COMMAND(shrinkmap, "");
+
+void mapname()
+{
+ result(game::getclientmap());
+}
+
+COMMAND(mapname, "");
+
+void mpeditent(int i, const vec &o, int type, int attr1, int attr2, int attr3, int attr4, int attr5, bool local)
+{
+ if(i < 0 || i >= MAXENTS) return;
+ vector<extentity *> &ents = entities::getents();
+ if(ents.length()<=i)
+ {
+ extentity *e = newentity(local, o, type, attr1, attr2, attr3, attr4, attr5, i);
+ if(!e) return;
+ addentity(i);
+ attachentity(*e);
+ }
+ else
+ {
+ extentity &e = *ents[i];
+ removeentity(i);
+ int oldtype = e.type;
+ if(oldtype!=type) detachentity(e);
+ e.type = type;
+ e.o = o;
+ e.attr1 = attr1; e.attr2 = attr2; e.attr3 = attr3; e.attr4 = attr4; e.attr5 = attr5;
+ addentity(i);
+ if(oldtype!=type) attachentity(e);
+ }
+ entities::editent(i, local);
+}
+
+int getworldsize() { return worldsize; }
+int getmapversion() { return mapversion; }
+
diff --git a/src/engine/world.h b/src/engine/world.h
new file mode 100644
index 0000000..9c5be78
--- /dev/null
+++ b/src/engine/world.h
@@ -0,0 +1,59 @@
+
+enum // hardcoded texture numbers
+{
+ DEFAULT_SKY = 0,
+ DEFAULT_GEOM
+};
+
+#define MAPVERSION 33 // bump if map format changes, see worldio.cpp
+
+struct octaheader
+{
+ char magic[4]; // "OCTA"
+ int version; // any >8bit quantity is little endian
+ int headersize; // sizeof(header)
+ int worldsize;
+ int numents;
+ int numpvs;
+ int lightmaps;
+ int blendmap;
+ int numvars;
+ int numvslots;
+};
+
+struct compatheader // map file format header
+{
+ char magic[4]; // "OCTA"
+ int version; // any >8bit quantity is little endian
+ int headersize; // sizeof(header)
+ int worldsize;
+ int numents;
+ int numpvs;
+ int lightmaps;
+ int lightprecision, lighterror, lightlod;
+ uchar ambient;
+ uchar watercolour[3];
+ uchar blendmap;
+ uchar lerpangle, lerpsubdiv, lerpsubdivsize;
+ uchar bumperror;
+ uchar skylight[3];
+ uchar lavacolour[3];
+ uchar waterfallcolour[3];
+ uchar reserved[10];
+ char maptitle[128];
+};
+
+#define WATER_AMPLITUDE 0.4f
+#define WATER_OFFSET 1.1f
+
+enum
+{
+ MATSURF_NOT_VISIBLE = 0,
+ MATSURF_VISIBLE,
+ MATSURF_EDIT_ONLY
+};
+
+#define TEX_SCALE 8.0f
+
+struct vertex { vec pos; bvec4 norm; vec2 tc; svec2 lm; bvec4 tangent; };
+
diff --git a/src/engine/worldio.cpp b/src/engine/worldio.cpp
new file mode 100644
index 0000000..514b45e
--- /dev/null
+++ b/src/engine/worldio.cpp
@@ -0,0 +1,1388 @@
+// worldio.cpp: loading & saving of maps and savegames
+
+#include "engine.h"
+
+void validmapname(char *dst, const char *src, const char *prefix = NULL, const char *alt = "untitled", size_t maxlen = 100)
+{
+ if(prefix) while(*prefix) *dst++ = *prefix++;
+ const char *start = dst;
+ if(src) loopi(maxlen)
+ {
+ char c = *src++;
+ if(iscubealnum(c) || c == '_' || c == '-' || c == '/' || c == '\\') *dst++ = c;
+ else break;
+ }
+ if(dst > start) *dst = '\0';
+ else if(dst != alt) copystring(dst, alt, maxlen);
+}
+
+void fixmapname(char *name)
+{
+ validmapname(name, name, NULL, "");
+}
+
+void getmapfilenames(const char *fname, const char *cname, char *pakname, char *mapname, char *cfgname)
+{
+ if(!cname) cname = fname;
+ string name;
+ validmapname(name, cname);
+ char *slash = strpbrk(name, "/\\");
+ if(slash)
+ {
+ copystring(pakname, name, slash-name+1);
+ copystring(cfgname, slash+1, MAXSTRLEN);
+ }
+ else
+ {
+ copystring(pakname, "base", MAXSTRLEN);
+ copystring(cfgname, name, MAXSTRLEN);
+ }
+ validmapname(mapname, fname, strpbrk(fname, "/\\") ? NULL : "base/");
+}
+
+static void fixent(entity &e, int version)
+{
+ if(version <= 10 && e.type >= 7) e.type++;
+ if(version <= 12 && e.type >= 8) e.type++;
+ if(version <= 14 && e.type >= ET_MAPMODEL && e.type <= 16)
+ {
+ if(e.type == 16) e.type = ET_MAPMODEL;
+ else e.type++;
+ }
+ if(version <= 20 && e.type >= ET_ENVMAP) e.type++;
+ if(version <= 21 && e.type >= ET_PARTICLES) e.type++;
+ if(version <= 22 && e.type >= ET_SOUND) e.type++;
+ if(version <= 23 && e.type >= ET_SPOTLIGHT) e.type++;
+ if(version <= 30 && (e.type == ET_MAPMODEL || e.type == ET_PLAYERSTART)) e.attr1 = (int(e.attr1)+180)%360;
+ if(version <= 31 && e.type == ET_MAPMODEL) { int yaw = (int(e.attr1)%360 + 360)%360 + 7; e.attr1 = yaw - yaw%15; }
+}
+
+bool loadents(const char *fname, vector<entity> &ents, uint *crc)
+{
+ string pakname, mapname, mcfgname, ogzname;
+ getmapfilenames(fname, NULL, pakname, mapname, mcfgname);
+ formatstring(ogzname, "packages/%s.ogz", mapname);
+ path(ogzname);
+ stream *f = opengzfile(ogzname, "rb");
+ if(!f) return false;
+ octaheader hdr;
+ if(f->read(&hdr, 7*sizeof(int)) != 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+ lilswap(&hdr.version, 6);
+ if(memcmp(hdr.magic, "OCTA", 4) || hdr.worldsize <= 0|| hdr.numents < 0) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+ if(hdr.version>MAPVERSION) { conoutf(CON_ERROR, "map %s requires a newer version of Cube 2: Sauerbraten", ogzname); delete f; return false; }
+ compatheader chdr;
+ if(hdr.version <= 28)
+ {
+ if(f->read(&chdr.lightprecision, sizeof(chdr) - 7*sizeof(int)) != sizeof(chdr) - 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+ }
+ else
+ {
+ int extra = 0;
+ if(hdr.version <= 29) extra++;
+ if(f->read(&hdr.blendmap, sizeof(hdr) - (7+extra)*sizeof(int)) != sizeof(hdr) - (7+extra)*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+ }
+
+ if(hdr.version <= 28)
+ {
+ lilswap(&chdr.lightprecision, 3);
+ hdr.blendmap = chdr.blendmap;
+ hdr.numvars = 0;
+ hdr.numvslots = 0;
+ }
+ else
+ {
+ lilswap(&hdr.blendmap, 2);
+ if(hdr.version <= 29) hdr.numvslots = 0;
+ else lilswap(&hdr.numvslots, 1);
+ }
+
+ loopi(hdr.numvars)
+ {
+ int type = f->getchar(), ilen = f->getlil<ushort>();
+ f->seek(ilen, SEEK_CUR);
+ switch(type)
+ {
+ case ID_VAR: f->getlil<int>(); break;
+ case ID_FVAR: f->getlil<float>(); break;
+ case ID_SVAR: { int slen = f->getlil<ushort>(); f->seek(slen, SEEK_CUR); break; }
+ }
+ }
+
+ string gametype;
+ copystring(gametype, "fps");
+ bool samegame = true;
+ int eif = 0;
+ if(hdr.version>=16)
+ {
+ int len = f->getchar();
+ f->read(gametype, len+1);
+ }
+ if(strcmp(gametype, game::gameident()))
+ {
+ samegame = false;
+ conoutf(CON_WARN, "WARNING: loading map from %s game, ignoring entities except for lights/mapmodels", gametype);
+ }
+ if(hdr.version>=16)
+ {
+ eif = f->getlil<ushort>();
+ int extrasize = f->getlil<ushort>();
+ f->seek(extrasize, SEEK_CUR);
+ }
+
+ if(hdr.version<14)
+ {
+ f->seek(256, SEEK_CUR);
+ }
+ else
+ {
+ ushort nummru = f->getlil<ushort>();
+ f->seek(nummru*sizeof(ushort), SEEK_CUR);
+ }
+
+ loopi(min(hdr.numents, MAXENTS))
+ {
+ entity &e = ents.add();
+ f->read(&e, sizeof(entity));
+ lilswap(&e.o.x, 3);
+ lilswap(&e.attr1, 5);
+ fixent(e, hdr.version);
+ if(eif > 0) f->seek(eif, SEEK_CUR);
+ if(samegame)
+ {
+ entities::readent(e, NULL, hdr.version);
+ }
+ else if(e.type>=ET_GAMESPECIFIC || hdr.version<=14)
+ {
+ ents.pop();
+ continue;
+ }
+ }
+
+ if(crc)
+ {
+ f->seek(0, SEEK_END);
+ *crc = f->getcrc();
+ }
+
+ delete f;
+
+ return true;
+}
+
+#ifndef STANDALONE
+string ogzname, bakname, cfgname, picname;
+
+VARP(savebak, 0, 2, 2);
+
+void setmapfilenames(const char *fname, const char *cname = NULL)
+{
+ string pakname, mapname, mcfgname;
+ getmapfilenames(fname, cname, pakname, mapname, mcfgname);
+
+ formatstring(ogzname, "packages/%s.ogz", mapname);
+ if(savebak==1) formatstring(bakname, "packages/%s.BAK", mapname);
+ else formatstring(bakname, "packages/%s_%d.BAK", mapname, totalmillis);
+ formatstring(cfgname, "packages/%s/%s.cfg", pakname, mcfgname);
+ formatstring(picname, "packages/%s.jpg", mapname);
+
+ path(ogzname);
+ path(bakname);
+ path(cfgname);
+ path(picname);
+}
+
+void mapcfgname()
+{
+ const char *mname = game::getclientmap();
+ string pakname, mapname, mcfgname;
+ getmapfilenames(mname, NULL, pakname, mapname, mcfgname);
+ defformatstring(cfgname, "packages/%s/%s.cfg", pakname, mcfgname);
+ path(cfgname);
+ result(cfgname);
+}
+
+COMMAND(mapcfgname, "");
+
+void backup(char *name, char *backupname)
+{
+ string backupfile;
+ copystring(backupfile, findfile(backupname, "wb"));
+ remove(backupfile);
+ rename(findfile(name, "wb"), backupfile);
+}
+
+enum { OCTSAV_CHILDREN = 0, OCTSAV_EMPTY, OCTSAV_SOLID, OCTSAV_NORMAL, OCTSAV_LODCUBE };
+
+static int savemapprogress = 0;
+
+void savec(cube *c, const ivec &o, int size, stream *f, bool nolms)
+{
+ if((savemapprogress++&0xFFF)==0) renderprogress(float(savemapprogress)/allocnodes, "saving octree...");
+
+ loopi(8)
+ {
+ ivec co(i, o, size);
+ if(c[i].children)
+ {
+ f->putchar(OCTSAV_CHILDREN);
+ savec(c[i].children, co, size>>1, f, nolms);
+ }
+ else
+ {
+ int oflags = 0, surfmask = 0, totalverts = 0;
+ if(c[i].material!=MAT_AIR) oflags |= 0x40;
+ if(isempty(c[i])) f->putchar(oflags | OCTSAV_EMPTY);
+ else
+ {
+ if(!nolms)
+ {
+ if(c[i].merged) oflags |= 0x80;
+ if(c[i].ext) loopj(6)
+ {
+ const surfaceinfo &surf = c[i].ext->surfaces[j];
+ if(!surf.used()) continue;
+ oflags |= 0x20;
+ surfmask |= 1<<j;
+ totalverts += surf.totalverts();
+ }
+ }
+
+ if(isentirelysolid(c[i])) f->putchar(oflags | OCTSAV_SOLID);
+ else
+ {
+ f->putchar(oflags | OCTSAV_NORMAL);
+ f->write(c[i].edges, 12);
+ }
+ }
+
+ loopj(6) f->putlil<ushort>(c[i].texture[j]);
+
+ if(oflags&0x40) f->putlil<ushort>(c[i].material);
+ if(oflags&0x80) f->putchar(c[i].merged);
+ if(oflags&0x20)
+ {
+ f->putchar(surfmask);
+ f->putchar(totalverts);
+ loopj(6) if(surfmask&(1<<j))
+ {
+ surfaceinfo surf = c[i].ext->surfaces[j];
+ vertinfo *verts = c[i].ext->verts() + surf.verts;
+ int layerverts = surf.numverts&MAXFACEVERTS, numverts = surf.totalverts(),
+ vertmask = 0, vertorder = 0, uvorder = 0,
+ dim = dimension(j), vc = C[dim], vr = R[dim];
+ if(numverts)
+ {
+ if(c[i].merged&(1<<j))
+ {
+ vertmask |= 0x04;
+ if(layerverts == 4)
+ {
+ ivec v[4] = { verts[0].getxyz(), verts[1].getxyz(), verts[2].getxyz(), verts[3].getxyz() };
+ loopk(4)
+ {
+ const ivec &v0 = v[k], &v1 = v[(k+1)&3], &v2 = v[(k+2)&3], &v3 = v[(k+3)&3];
+ if(v1[vc] == v0[vc] && v1[vr] == v2[vr] && v3[vc] == v2[vc] && v3[vr] == v0[vr])
+ {
+ vertmask |= 0x01;
+ vertorder = k;
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ int vis = visibletris(c[i], j, co, size);
+ if(vis&4 || faceconvexity(c[i], j) < 0) vertmask |= 0x01;
+ if(layerverts < 4 && vis&2) vertmask |= 0x02;
+ }
+ bool matchnorm = true;
+ loopk(numverts)
+ {
+ const vertinfo &v = verts[k];
+ if(v.u || v.v) vertmask |= 0x40;
+ if(v.norm) { vertmask |= 0x80; if(v.norm != verts[0].norm) matchnorm = false; }
+ }
+ if(matchnorm) vertmask |= 0x08;
+ if(vertmask&0x40 && layerverts == 4)
+ {
+ loopk(4)
+ {
+ const vertinfo &v0 = verts[k], &v1 = verts[(k+1)&3], &v2 = verts[(k+2)&3], &v3 = verts[(k+3)&3];
+ if(v1.u == v0.u && v1.v == v2.v && v3.u == v2.u && v3.v == v0.v)
+ {
+ if(surf.numverts&LAYER_DUP)
+ {
+ const vertinfo &b0 = verts[4+k], &b1 = verts[4+((k+1)&3)], &b2 = verts[4+((k+2)&3)], &b3 = verts[4+((k+3)&3)];
+ if(b1.u != b0.u || b1.v != b2.v || b3.u != b2.u || b3.v != b0.v)
+ continue;
+ }
+ uvorder = k;
+ vertmask |= 0x02 | (((k+4-vertorder)&3)<<4);
+ break;
+ }
+ }
+ }
+ }
+ surf.verts = vertmask;
+ f->write(&surf, sizeof(surfaceinfo));
+ bool hasxyz = (vertmask&0x04)!=0, hasuv = (vertmask&0x40)!=0, hasnorm = (vertmask&0x80)!=0;
+ if(layerverts == 4)
+ {
+ if(hasxyz && vertmask&0x01)
+ {
+ ivec v0 = verts[vertorder].getxyz(), v2 = verts[(vertorder+2)&3].getxyz();
+ f->putlil<ushort>(v0[vc]); f->putlil<ushort>(v0[vr]);
+ f->putlil<ushort>(v2[vc]); f->putlil<ushort>(v2[vr]);
+ hasxyz = false;
+ }
+ if(hasuv && vertmask&0x02)
+ {
+ const vertinfo &v0 = verts[uvorder], &v2 = verts[(uvorder+2)&3];
+ f->putlil<ushort>(v0.u); f->putlil<ushort>(v0.v);
+ f->putlil<ushort>(v2.u); f->putlil<ushort>(v2.v);
+ if(surf.numverts&LAYER_DUP)
+ {
+ const vertinfo &b0 = verts[4+uvorder], &b2 = verts[4+((uvorder+2)&3)];
+ f->putlil<ushort>(b0.u); f->putlil<ushort>(b0.v);
+ f->putlil<ushort>(b2.u); f->putlil<ushort>(b2.v);
+ }
+ hasuv = false;
+ }
+ }
+ if(hasnorm && vertmask&0x08) { f->putlil<ushort>(verts[0].norm); hasnorm = false; }
+ if(hasxyz || hasuv || hasnorm) loopk(layerverts)
+ {
+ const vertinfo &v = verts[(k+vertorder)%layerverts];
+ if(hasxyz)
+ {
+ ivec xyz = v.getxyz();
+ f->putlil<ushort>(xyz[vc]); f->putlil<ushort>(xyz[vr]);
+ }
+ if(hasuv) { f->putlil<ushort>(v.u); f->putlil<ushort>(v.v); }
+ if(hasnorm) f->putlil<ushort>(v.norm);
+ }
+ if(surf.numverts&LAYER_DUP) loopk(layerverts)
+ {
+ const vertinfo &v = verts[layerverts + (k+vertorder)%layerverts];
+ if(hasuv) { f->putlil<ushort>(v.u); f->putlil<ushort>(v.v); }
+ }
+ }
+ }
+ }
+ }
+}
+
+struct surfacecompat
+{
+ uchar texcoords[8];
+ uchar w, h;
+ ushort x, y;
+ uchar lmid, layer;
+};
+
+struct normalscompat
+{
+ bvec normals[4];
+};
+
+struct mergecompat
+{
+ ushort u1, u2, v1, v2;
+};
+
+cube *loadchildren(stream *f, const ivec &co, int size, bool &failed);
+
+void convertoldsurfaces(cube &c, const ivec &co, int size, surfacecompat *srcsurfs, int hassurfs, normalscompat *normals, int hasnorms, mergecompat *merges, int hasmerges)
+{
+ surfaceinfo dstsurfs[6];
+ vertinfo verts[6*2*MAXFACEVERTS];
+ int totalverts = 0, numsurfs = 6;
+ memset(dstsurfs, 0, sizeof(dstsurfs));
+ loopi(6) if((hassurfs|hasnorms|hasmerges)&(1<<i))
+ {
+ surfaceinfo &dst = dstsurfs[i];
+ vertinfo *curverts = NULL;
+ int numverts = 0;
+ surfacecompat *src = NULL, *blend = NULL;
+ if(hassurfs&(1<<i))
+ {
+ src = &srcsurfs[i];
+ if(src->layer&2)
+ {
+ blend = &srcsurfs[numsurfs++];
+ dst.lmid[0] = src->lmid;
+ dst.lmid[1] = blend->lmid;
+ dst.numverts |= LAYER_BLEND;
+ if(blend->lmid >= LMID_RESERVED && (src->x != blend->x || src->y != blend->y || src->w != blend->w || src->h != blend->h || memcmp(src->texcoords, blend->texcoords, sizeof(src->texcoords))))
+ dst.numverts |= LAYER_DUP;
+ }
+ else if(src->layer == 1) { dst.lmid[1] = src->lmid; dst.numverts |= LAYER_BOTTOM; }
+ else { dst.lmid[0] = src->lmid; dst.numverts |= LAYER_TOP; }
+ }
+ else dst.numverts |= LAYER_TOP;
+ bool uselms = hassurfs&(1<<i) && (dst.lmid[0] >= LMID_RESERVED || dst.lmid[1] >= LMID_RESERVED || dst.numverts&~LAYER_TOP),
+ usemerges = hasmerges&(1<<i) && merges[i].u1 < merges[i].u2 && merges[i].v1 < merges[i].v2,
+ usenorms = hasnorms&(1<<i) && normals[i].normals[0] != bvec(128, 128, 128);
+ if(uselms || usemerges || usenorms)
+ {
+ ivec v[4], pos[4], e1, e2, e3, n, vo = ivec(co).mask(0xFFF).shl(3);
+ genfaceverts(c, i, v);
+ n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));
+ if(usemerges)
+ {
+ const mergecompat &m = merges[i];
+ int offset = -n.dot(v[0].mul(size).add(vo)),
+ dim = dimension(i), vc = C[dim], vr = R[dim];
+ loopk(4)
+ {
+ const ivec &coords = facecoords[i][k];
+ int cc = coords[vc] ? m.u2 : m.u1,
+ rc = coords[vr] ? m.v2 : m.v1,
+ dc = n[dim] ? -(offset + n[vc]*cc + n[vr]*rc)/n[dim] : vo[dim];
+ ivec &mv = pos[k];
+ mv[vc] = cc;
+ mv[vr] = rc;
+ mv[dim] = dc;
+ }
+ }
+ else
+ {
+ int convex = (e3 = v[0]).sub(v[3]).dot(n), vis = 3;
+ if(!convex)
+ {
+ if(ivec().cross(e3, e2).iszero()) { if(!n.iszero()) vis = 1; }
+ else if(n.iszero()) vis = 2;
+ }
+ int order = convex < 0 ? 1 : 0;
+ pos[0] = v[order].mul(size).add(vo);
+ pos[1] = vis&1 ? v[order+1].mul(size).add(vo) : pos[0];
+ pos[2] = v[order+2].mul(size).add(vo);
+ pos[3] = vis&2 ? v[(order+3)&3].mul(size).add(vo) : pos[0];
+ }
+ curverts = verts + totalverts;
+ loopk(4)
+ {
+ if(k > 0 && (pos[k] == pos[0] || pos[k] == pos[k-1])) continue;
+ vertinfo &dv = curverts[numverts++];
+ dv.setxyz(pos[k]);
+ if(uselms)
+ {
+ float u = src->x + (src->texcoords[k*2] / 255.0f) * (src->w - 1),
+ v = src->y + (src->texcoords[k*2+1] / 255.0f) * (src->h - 1);
+ dv.u = ushort(floor(clamp((u) * float(USHRT_MAX+1)/LM_PACKW + 0.5f, 0.0f, float(USHRT_MAX))));
+ dv.v = ushort(floor(clamp((v) * float(USHRT_MAX+1)/LM_PACKH + 0.5f, 0.0f, float(USHRT_MAX))));
+ }
+ else dv.u = dv.v = 0;
+ dv.norm = usenorms && normals[i].normals[k] != bvec(128, 128, 128) ? encodenormal(normals[i].normals[k].tonormal().normalize()) : 0;
+ }
+ dst.verts = totalverts;
+ dst.numverts |= numverts;
+ totalverts += numverts;
+ if(dst.numverts&LAYER_DUP) loopk(4)
+ {
+ if(k > 0 && (pos[k] == pos[0] || pos[k] == pos[k-1])) continue;
+ vertinfo &bv = verts[totalverts++];
+ bv.setxyz(pos[k]);
+ bv.u = ushort(floor(clamp((blend->x + (blend->texcoords[k*2] / 255.0f) * (blend->w - 1)) * float(USHRT_MAX+1)/LM_PACKW, 0.0f, float(USHRT_MAX))));
+ bv.v = ushort(floor(clamp((blend->y + (blend->texcoords[k*2+1] / 255.0f) * (blend->h - 1)) * float(USHRT_MAX+1)/LM_PACKH, 0.0f, float(USHRT_MAX))));
+ bv.norm = usenorms && normals[i].normals[k] != bvec(128, 128, 128) ? encodenormal(normals[i].normals[k].tonormal().normalize()) : 0;
+ }
+ }
+ }
+ setsurfaces(c, dstsurfs, verts, totalverts);
+}
+
+static inline int convertoldmaterial(int mat)
+{
+ return ((mat&7)<<MATF_VOLUME_SHIFT) | (((mat>>3)&3)<<MATF_CLIP_SHIFT) | (((mat>>5)&7)<<MATF_FLAG_SHIFT);
+}
+
+void loadc(stream *f, cube &c, const ivec &co, int size, bool &failed)
+{
+ bool haschildren = false;
+ int octsav = f->getchar();
+ switch(octsav&0x7)
+ {
+ case OCTSAV_CHILDREN:
+ c.children = loadchildren(f, co, size>>1, failed);
+ return;
+
+ case OCTSAV_LODCUBE: haschildren = true; break;
+ case OCTSAV_EMPTY: emptyfaces(c); break;
+ case OCTSAV_SOLID: solidfaces(c); break;
+ case OCTSAV_NORMAL: f->read(c.edges, 12); break;
+ default: failed = true; return;
+ }
+ loopi(6) c.texture[i] = mapversion<14 ? f->getchar() : f->getlil<ushort>();
+ if(mapversion < 7) f->seek(3, SEEK_CUR);
+ else if(mapversion <= 31)
+ {
+ uchar mask = f->getchar();
+ if(mask & 0x80)
+ {
+ int mat = f->getchar();
+ if(mapversion < 27)
+ {
+ static const ushort matconv[] = { MAT_AIR, MAT_WATER, MAT_CLIP, MAT_GLASS|MAT_CLIP, MAT_NOCLIP, MAT_LAVA|MAT_DEATH, MAT_GAMECLIP, MAT_DEATH };
+ c.material = size_t(mat) < sizeof(matconv)/sizeof(matconv[0]) ? matconv[mat] : MAT_AIR;
+ }
+ else c.material = convertoldmaterial(mat);
+ }
+ surfacecompat surfaces[12];
+ normalscompat normals[6];
+ mergecompat merges[6];
+ int hassurfs = 0, hasnorms = 0, hasmerges = 0;
+ if(mask & 0x3F)
+ {
+ int numsurfs = 6;
+ loopi(numsurfs)
+ {
+ if(i >= 6 || mask & (1 << i))
+ {
+ f->read(&surfaces[i], sizeof(surfacecompat));
+ lilswap(&surfaces[i].x, 2);
+ if(mapversion < 10) ++surfaces[i].lmid;
+ if(mapversion < 18)
+ {
+ if(surfaces[i].lmid >= LMID_AMBIENT1) ++surfaces[i].lmid;
+ if(surfaces[i].lmid >= LMID_BRIGHT1) ++surfaces[i].lmid;
+ }
+ if(mapversion < 19)
+ {
+ if(surfaces[i].lmid >= LMID_DARK) surfaces[i].lmid += 2;
+ }
+ if(i < 6)
+ {
+ if(mask & 0x40) { hasnorms |= 1<<i; f->read(&normals[i], sizeof(normalscompat)); }
+ if(surfaces[i].layer != 0 || surfaces[i].lmid != LMID_AMBIENT)
+ hassurfs |= 1<<i;
+ if(surfaces[i].layer&2) numsurfs++;
+ }
+ }
+ }
+ }
+ if(mapversion <= 8) edgespan2vectorcube(c);
+ if(mapversion <= 11)
+ {
+ swap(c.faces[0], c.faces[2]);
+ swap(c.texture[0], c.texture[4]);
+ swap(c.texture[1], c.texture[5]);
+ if(hassurfs&0x33)
+ {
+ swap(surfaces[0], surfaces[4]);
+ swap(surfaces[1], surfaces[5]);
+ hassurfs = (hassurfs&~0x33) | ((hassurfs&0x30)>>4) | ((hassurfs&0x03)<<4);
+ }
+ }
+ if(mapversion >= 20)
+ {
+ if(octsav&0x80)
+ {
+ int merged = f->getchar();
+ c.merged = merged&0x3F;
+ if(merged&0x80)
+ {
+ int mask = f->getchar();
+ if(mask)
+ {
+ hasmerges = mask&0x3F;
+ loopi(6) if(mask&(1<<i))
+ {
+ mergecompat *m = &merges[i];
+ f->read(m, sizeof(mergecompat));
+ lilswap(&m->u1, 4);
+ if(mapversion <= 25)
+ {
+ int uorigin = m->u1 & 0xE000, vorigin = m->v1 & 0xE000;
+ m->u1 = (m->u1 - uorigin) << 2;
+ m->u2 = (m->u2 - uorigin) << 2;
+ m->v1 = (m->v1 - vorigin) << 2;
+ m->v2 = (m->v2 - vorigin) << 2;
+ }
+ }
+ }
+ }
+ }
+ }
+ if(hassurfs || hasnorms || hasmerges)
+ convertoldsurfaces(c, co, size, surfaces, hassurfs, normals, hasnorms, merges, hasmerges);
+ }
+ else
+ {
+ if(octsav&0x40)
+ {
+ if(mapversion <= 32)
+ {
+ int mat = f->getchar();
+ c.material = convertoldmaterial(mat);
+ }
+ else c.material = f->getlil<ushort>();
+ }
+ if(octsav&0x80) c.merged = f->getchar();
+ if(octsav&0x20)
+ {
+ int surfmask, totalverts;
+ surfmask = f->getchar();
+ totalverts = max(f->getchar(), 0);
+ newcubeext(c, totalverts, false);
+ memset(c.ext->surfaces, 0, sizeof(c.ext->surfaces));
+ memset(c.ext->verts(), 0, totalverts*sizeof(vertinfo));
+ int offset = 0;
+ loopi(6) if(surfmask&(1<<i))
+ {
+ surfaceinfo &surf = c.ext->surfaces[i];
+ f->read(&surf, sizeof(surfaceinfo));
+ int vertmask = surf.verts, numverts = surf.totalverts();
+ if(!numverts) { surf.verts = 0; continue; }
+ surf.verts = offset;
+ vertinfo *verts = c.ext->verts() + offset;
+ offset += numverts;
+ ivec v[4], n, vo = ivec(co).mask(0xFFF).shl(3);
+ int layerverts = surf.numverts&MAXFACEVERTS, dim = dimension(i), vc = C[dim], vr = R[dim], bias = 0;
+ genfaceverts(c, i, v);
+ bool hasxyz = (vertmask&0x04)!=0, hasuv = (vertmask&0x40)!=0, hasnorm = (vertmask&0x80)!=0;
+ if(hasxyz)
+ {
+ ivec e1, e2, e3;
+ n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));
+ if(n.iszero()) n.cross(e2, (e3 = v[3]).sub(v[0]));
+ bias = -n.dot(ivec(v[0]).mul(size).add(vo));
+ }
+ else
+ {
+ int vis = layerverts < 4 ? (vertmask&0x02 ? 2 : 1) : 3, order = vertmask&0x01 ? 1 : 0, k = 0;
+ verts[k++].setxyz(v[order].mul(size).add(vo));
+ if(vis&1) verts[k++].setxyz(v[order+1].mul(size).add(vo));
+ verts[k++].setxyz(v[order+2].mul(size).add(vo));
+ if(vis&2) verts[k++].setxyz(v[(order+3)&3].mul(size).add(vo));
+ }
+ if(layerverts == 4)
+ {
+ if(hasxyz && vertmask&0x01)
+ {
+ ushort c1 = f->getlil<ushort>(), r1 = f->getlil<ushort>(), c2 = f->getlil<ushort>(), r2 = f->getlil<ushort>();
+ ivec xyz;
+ xyz[vc] = c1; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+ verts[0].setxyz(xyz);
+ xyz[vc] = c1; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+ verts[1].setxyz(xyz);
+ xyz[vc] = c2; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+ verts[2].setxyz(xyz);
+ xyz[vc] = c2; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+ verts[3].setxyz(xyz);
+ hasxyz = false;
+ }
+ if(hasuv && vertmask&0x02)
+ {
+ int uvorder = (vertmask&0x30)>>4;
+ vertinfo &v0 = verts[uvorder], &v1 = verts[(uvorder+1)&3], &v2 = verts[(uvorder+2)&3], &v3 = verts[(uvorder+3)&3];
+ v0.u = f->getlil<ushort>(); v0.v = f->getlil<ushort>();
+ v2.u = f->getlil<ushort>(); v2.v = f->getlil<ushort>();
+ v1.u = v0.u; v1.v = v2.v;
+ v3.u = v2.u; v3.v = v0.v;
+ if(surf.numverts&LAYER_DUP)
+ {
+ vertinfo &b0 = verts[4+uvorder], &b1 = verts[4+((uvorder+1)&3)], &b2 = verts[4+((uvorder+2)&3)], &b3 = verts[4+((uvorder+3)&3)];
+ b0.u = f->getlil<ushort>(); b0.v = f->getlil<ushort>();
+ b2.u = f->getlil<ushort>(); b2.v = f->getlil<ushort>();
+ b1.u = b0.u; b1.v = b2.v;
+ b3.u = b2.u; b3.v = b0.v;
+ }
+ hasuv = false;
+ }
+ }
+ if(hasnorm && vertmask&0x08)
+ {
+ ushort norm = f->getlil<ushort>();
+ loopk(layerverts) verts[k].norm = norm;
+ hasnorm = false;
+ }
+ if(hasxyz || hasuv || hasnorm) loopk(layerverts)
+ {
+ vertinfo &v = verts[k];
+ if(hasxyz)
+ {
+ ivec xyz;
+ xyz[vc] = f->getlil<ushort>(); xyz[vr] = f->getlil<ushort>();
+ xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+ v.setxyz(xyz);
+ }
+ if(hasuv) { v.u = f->getlil<ushort>(); v.v = f->getlil<ushort>(); }
+ if(hasnorm) v.norm = f->getlil<ushort>();
+ }
+ if(surf.numverts&LAYER_DUP) loopk(layerverts)
+ {
+ vertinfo &v = verts[k+layerverts], &t = verts[k];
+ v.setxyz(t.x, t.y, t.z);
+ if(hasuv) { v.u = f->getlil<ushort>(); v.v = f->getlil<ushort>(); }
+ v.norm = t.norm;
+ }
+ }
+ }
+ }
+
+ c.children = (haschildren ? loadchildren(f, co, size>>1, failed) : NULL);
+}
+
+cube *loadchildren(stream *f, const ivec &co, int size, bool &failed)
+{
+ cube *c = newcubes();
+ loopi(8)
+ {
+ loadc(f, c[i], ivec(i, co, size), size, failed);
+ if(failed) break;
+ }
+ return c;
+}
+
+VAR(dbgvars, 0, 0, 1);
+
+void savevslot(stream *f, VSlot &vs, int prev)
+{
+ f->putlil<int>(vs.changed);
+ f->putlil<int>(prev);
+ if(vs.changed & (1<<VSLOT_SHPARAM))
+ {
+ f->putlil<ushort>(vs.params.length());
+ loopv(vs.params)
+ {
+ SlotShaderParam &p = vs.params[i];
+ f->putlil<ushort>(strlen(p.name));
+ f->write(p.name, strlen(p.name));
+ loopk(4) f->putlil<float>(p.val[k]);
+ }
+ }
+ if(vs.changed & (1<<VSLOT_SCALE)) f->putlil<float>(vs.scale);
+ if(vs.changed & (1<<VSLOT_ROTATION)) f->putlil<int>(vs.rotation);
+ if(vs.changed & (1<<VSLOT_OFFSET))
+ {
+ f->putlil<int>(vs.offset.x);
+ f->putlil<int>(vs.offset.y);
+ }
+ if(vs.changed & (1<<VSLOT_SCROLL))
+ {
+ f->putlil<float>(vs.scroll.x);
+ f->putlil<float>(vs.scroll.y);
+ }
+ if(vs.changed & (1<<VSLOT_LAYER)) f->putlil<int>(vs.layer);
+ if(vs.changed & (1<<VSLOT_ALPHA))
+ {
+ f->putlil<float>(vs.alphafront);
+ f->putlil<float>(vs.alphaback);
+ }
+ if(vs.changed & (1<<VSLOT_COLOR))
+ {
+ loopk(3) f->putlil<float>(vs.colorscale[k]);
+ }
+}
+
+void savevslots(stream *f, int numvslots)
+{
+ if(vslots.empty()) return;
+ int *prev = new int[numvslots];
+ memset(prev, -1, numvslots*sizeof(int));
+ loopi(numvslots)
+ {
+ VSlot *vs = vslots[i];
+ if(vs->changed) continue;
+ for(;;)
+ {
+ VSlot *cur = vs;
+ do vs = vs->next; while(vs && vs->index >= numvslots);
+ if(!vs) break;
+ prev[vs->index] = cur->index;
+ }
+ }
+ int lastroot = 0;
+ loopi(numvslots)
+ {
+ VSlot &vs = *vslots[i];
+ if(!vs.changed) continue;
+ if(lastroot < i) f->putlil<int>(-(i - lastroot));
+ savevslot(f, vs, prev[i]);
+ lastroot = i+1;
+ }
+ if(lastroot < numvslots) f->putlil<int>(-(numvslots - lastroot));
+ delete[] prev;
+}
+
+void loadvslot(stream *f, VSlot &vs, int changed)
+{
+ vs.changed = changed;
+ if(vs.changed & (1<<VSLOT_SHPARAM))
+ {
+ int numparams = f->getlil<ushort>();
+ string name;
+ loopi(numparams)
+ {
+ SlotShaderParam &p = vs.params.add();
+ int nlen = f->getlil<ushort>();
+ f->read(name, min(nlen, MAXSTRLEN-1));
+ name[min(nlen, MAXSTRLEN-1)] = '\0';
+ if(nlen >= MAXSTRLEN) f->seek(nlen - (MAXSTRLEN-1), SEEK_CUR);
+ p.name = getshaderparamname(name);
+ p.loc = -1;
+ loopk(4) p.val[k] = f->getlil<float>();
+ }
+ }
+ if(vs.changed & (1<<VSLOT_SCALE)) vs.scale = f->getlil<float>();
+ if(vs.changed & (1<<VSLOT_ROTATION)) vs.rotation = clamp(f->getlil<int>(), 0, 7);
+ if(vs.changed & (1<<VSLOT_OFFSET))
+ {
+ vs.offset.x = f->getlil<int>();
+ vs.offset.y = f->getlil<int>();
+ }
+ if(vs.changed & (1<<VSLOT_SCROLL))
+ {
+ vs.scroll.x = f->getlil<float>();
+ vs.scroll.y = f->getlil<float>();
+ }
+ if(vs.changed & (1<<VSLOT_LAYER)) vs.layer = f->getlil<int>();
+ if(vs.changed & (1<<VSLOT_ALPHA))
+ {
+ vs.alphafront = f->getlil<float>();
+ vs.alphaback = f->getlil<float>();
+ }
+ if(vs.changed & (1<<VSLOT_COLOR))
+ {
+ loopk(3) vs.colorscale[k] = f->getlil<float>();
+ }
+}
+
+void loadvslots(stream *f, int numvslots)
+{
+ int *prev = new (false) int[numvslots];
+ if(!prev) return;
+ memset(prev, -1, numvslots*sizeof(int));
+ while(numvslots > 0)
+ {
+ int changed = f->getlil<int>();
+ if(changed < 0)
+ {
+ loopi(-changed) vslots.add(new VSlot(NULL, vslots.length()));
+ numvslots += changed;
+ }
+ else
+ {
+ prev[vslots.length()] = f->getlil<int>();
+ loadvslot(f, *vslots.add(new VSlot(NULL, vslots.length())), changed);
+ numvslots--;
+ }
+ }
+ loopv(vslots) if(vslots.inrange(prev[i])) vslots[prev[i]]->next = vslots[i];
+ delete[] prev;
+}
+
+bool save_world(const char *mname, bool nolms)
+{
+ if(!*mname) mname = game::getclientmap();
+ setmapfilenames(mname);
+ if(savebak) backup(ogzname, bakname);
+ stream *f = opengzfile(ogzname, "wb");
+ if(!f) { conoutf(CON_WARN, "could not write map to %s", ogzname); return false; }
+
+ int numvslots = vslots.length();
+ if(!nolms && !multiplayer(false))
+ {
+ numvslots = compactvslots();
+ allchanged();
+ }
+
+ savemapprogress = 0;
+ renderprogress(0, "saving map...");
+
+ octaheader hdr;
+ memcpy(hdr.magic, "OCTA", 4);
+ hdr.version = MAPVERSION;
+ hdr.headersize = sizeof(hdr);
+ hdr.worldsize = worldsize;
+ hdr.numents = 0;
+ const vector<extentity *> &ents = entities::getents();
+ loopv(ents) if(ents[i]->type!=ET_EMPTY || nolms) hdr.numents++;
+ hdr.numpvs = nolms ? 0 : getnumviewcells();
+ hdr.lightmaps = nolms ? 0 : lightmaps.length();
+ hdr.blendmap = shouldsaveblendmap();
+ hdr.numvars = 0;
+ hdr.numvslots = numvslots;
+ enumerate(idents, ident, id,
+ {
+ if((id.type == ID_VAR || id.type == ID_FVAR || id.type == ID_SVAR) && id.flags&IDF_OVERRIDE && !(id.flags&IDF_READONLY) && id.flags&IDF_OVERRIDDEN) hdr.numvars++;
+ });
+ lilswap(&hdr.version, 9);
+ f->write(&hdr, sizeof(hdr));
+
+ enumerate(idents, ident, id,
+ {
+ if((id.type!=ID_VAR && id.type!=ID_FVAR && id.type!=ID_SVAR) || !(id.flags&IDF_OVERRIDE) || id.flags&IDF_READONLY || !(id.flags&IDF_OVERRIDDEN)) continue;
+ f->putchar(id.type);
+ f->putlil<ushort>(strlen(id.name));
+ f->write(id.name, strlen(id.name));
+ switch(id.type)
+ {
+ case ID_VAR:
+ if(dbgvars) conoutf(CON_DEBUG, "wrote var %s: %d", id.name, *id.storage.i);
+ f->putlil<int>(*id.storage.i);
+ break;
+
+ case ID_FVAR:
+ if(dbgvars) conoutf(CON_DEBUG, "wrote fvar %s: %f", id.name, *id.storage.f);
+ f->putlil<float>(*id.storage.f);
+ break;
+
+ case ID_SVAR:
+ if(dbgvars) conoutf(CON_DEBUG, "wrote svar %s: %s", id.name, *id.storage.s);
+ f->putlil<ushort>(strlen(*id.storage.s));
+ f->write(*id.storage.s, strlen(*id.storage.s));
+ break;
+ }
+ });
+
+ if(dbgvars) conoutf(CON_DEBUG, "wrote %d vars", hdr.numvars);
+
+ f->putchar((int)strlen(game::gameident()));
+ f->write(game::gameident(), (int)strlen(game::gameident())+1);
+ f->putlil<ushort>(entities::extraentinfosize());
+ vector<char> extras;
+ game::writegamedata(extras);
+ f->putlil<ushort>(extras.length());
+ f->write(extras.getbuf(), extras.length());
+
+ f->putlil<ushort>(texmru.length());
+ loopv(texmru) f->putlil<ushort>(texmru[i]);
+ char *ebuf = new char[entities::extraentinfosize()];
+ loopv(ents)
+ {
+ if(ents[i]->type!=ET_EMPTY || nolms)
+ {
+ entity tmp = *ents[i];
+ lilswap(&tmp.o.x, 3);
+ lilswap(&tmp.attr1, 5);
+ f->write(&tmp, sizeof(entity));
+ entities::writeent(*ents[i], ebuf);
+ if(entities::extraentinfosize()) f->write(ebuf, entities::extraentinfosize());
+ }
+ }
+ delete[] ebuf;
+
+ savevslots(f, numvslots);
+
+ renderprogress(0, "saving octree...");
+ savec(worldroot, ivec(0, 0, 0), worldsize>>1, f, nolms);
+
+ if(!nolms)
+ {
+ if(lightmaps.length()) renderprogress(0, "saving lightmaps...");
+ loopv(lightmaps)
+ {
+ LightMap &lm = lightmaps[i];
+ f->putchar(lm.type | (lm.unlitx>=0 ? 0x80 : 0));
+ if(lm.unlitx>=0)
+ {
+ f->putlil<ushort>(ushort(lm.unlitx));
+ f->putlil<ushort>(ushort(lm.unlity));
+ }
+ f->write(lm.data, lm.bpp*LM_PACKW*LM_PACKH);
+ renderprogress(float(i+1)/lightmaps.length(), "saving lightmaps...");
+ }
+ if(getnumviewcells()>0) { renderprogress(0, "saving pvs..."); savepvs(f); }
+ }
+ if(shouldsaveblendmap()) { renderprogress(0, "saving blendmap..."); saveblendmap(f); }
+
+ delete f;
+ conoutf("wrote map file %s", ogzname);
+ return true;
+}
+
+static uint mapcrc = 0;
+
+uint getmapcrc() { return mapcrc; }
+void clearmapcrc() { mapcrc = 0; }
+
+bool load_world(const char *mname, const char *cname) // still supports all map formats that have existed since the earliest cube betas!
+{
+ int loadingstart = SDL_GetTicks();
+ setmapfilenames(mname, cname);
+ stream *f = opengzfile(ogzname, "rb");
+ if(!f) { conoutf(CON_ERROR, "could not read map %s", ogzname); return false; }
+ octaheader hdr;
+ if(f->read(&hdr, 7*sizeof(int)) != 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+ lilswap(&hdr.version, 6);
+ if(memcmp(hdr.magic, "OCTA", 4) || hdr.worldsize <= 0|| hdr.numents < 0) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+ if(hdr.version>MAPVERSION) { conoutf(CON_ERROR, "map %s requires a newer version of Cube 2: Sauerbraten", ogzname); delete f; return false; }
+ compatheader chdr;
+ if(hdr.version <= 28)
+ {
+ if(f->read(&chdr.lightprecision, sizeof(chdr) - 7*sizeof(int)) != sizeof(chdr) - 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+ }
+ else
+ {
+ int extra = 0;
+ if(hdr.version <= 29) extra++;
+ if(f->read(&hdr.blendmap, sizeof(hdr) - (7+extra)*sizeof(int)) != sizeof(hdr) - (7+extra)*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+ }
+
+ resetmap();
+
+ Texture *mapshot = textureload(picname, 3, true, false);
+ renderbackground("loading...", mapshot, mname, game::getmapinfo());
+
+ game::loadingmap(cname ? cname : mname);
+
+ setvar("mapversion", hdr.version, true, false);
+
+ if(hdr.version <= 28)
+ {
+ lilswap(&chdr.lightprecision, 3);
+ if(chdr.lightprecision) setvar("lightprecision", chdr.lightprecision);
+ if(chdr.lighterror) setvar("lighterror", chdr.lighterror);
+ if(chdr.bumperror) setvar("bumperror", chdr.bumperror);
+ setvar("lightlod", chdr.lightlod);
+ if(chdr.ambient) setvar("ambient", chdr.ambient);
+ setvar("skylight", (int(chdr.skylight[0])<<16) | (int(chdr.skylight[1])<<8) | int(chdr.skylight[2]));
+ setvar("watercolour", (int(chdr.watercolour[0])<<16) | (int(chdr.watercolour[1])<<8) | int(chdr.watercolour[2]), true);
+ setvar("waterfallcolour", (int(chdr.waterfallcolour[0])<<16) | (int(chdr.waterfallcolour[1])<<8) | int(chdr.waterfallcolour[2]));
+ setvar("lavacolour", (int(chdr.lavacolour[0])<<16) | (int(chdr.lavacolour[1])<<8) | int(chdr.lavacolour[2]));
+ setvar("fullbright", 0, true);
+ if(chdr.lerpsubdivsize || chdr.lerpangle) setvar("lerpangle", chdr.lerpangle);
+ if(chdr.lerpsubdivsize)
+ {
+ setvar("lerpsubdiv", chdr.lerpsubdiv);
+ setvar("lerpsubdivsize", chdr.lerpsubdivsize);
+ }
+ setsvar("maptitle", chdr.maptitle);
+ hdr.blendmap = chdr.blendmap;
+ hdr.numvars = 0;
+ hdr.numvslots = 0;
+ }
+ else
+ {
+ lilswap(&hdr.blendmap, 2);
+ if(hdr.version <= 29) hdr.numvslots = 0;
+ else lilswap(&hdr.numvslots, 1);
+ }
+
+ renderprogress(0, "clearing world...");
+
+ freeocta(worldroot);
+ worldroot = NULL;
+
+ int worldscale = 0;
+ while(1<<worldscale < hdr.worldsize) worldscale++;
+ setvar("mapsize", 1<<worldscale, true, false);
+ setvar("mapscale", worldscale, true, false);
+
+ renderprogress(0, "loading vars...");
+
+ loopi(hdr.numvars)
+ {
+ int type = f->getchar(), ilen = f->getlil<ushort>();
+ string name;
+ f->read(name, min(ilen, MAXSTRLEN-1));
+ name[min(ilen, MAXSTRLEN-1)] = '\0';
+ if(ilen >= MAXSTRLEN) f->seek(ilen - (MAXSTRLEN-1), SEEK_CUR);
+ ident *id = getident(name);
+ bool exists = id && id->type == type && id->flags&IDF_OVERRIDE;
+ switch(type)
+ {
+ case ID_VAR:
+ {
+ int val = f->getlil<int>();
+ if(exists && id->minval <= id->maxval) setvar(name, val);
+ if(dbgvars) conoutf(CON_DEBUG, "read var %s: %d", name, val);
+ break;
+ }
+
+ case ID_FVAR:
+ {
+ float val = f->getlil<float>();
+ if(exists && id->minvalf <= id->maxvalf) setfvar(name, val);
+ if(dbgvars) conoutf(CON_DEBUG, "read fvar %s: %f", name, val);
+ break;
+ }
+
+ case ID_SVAR:
+ {
+ int slen = f->getlil<ushort>();
+ string val;
+ f->read(val, min(slen, MAXSTRLEN-1));
+ val[min(slen, MAXSTRLEN-1)] = '\0';
+ if(slen >= MAXSTRLEN) f->seek(slen - (MAXSTRLEN-1), SEEK_CUR);
+ if(exists) setsvar(name, val);
+ if(dbgvars) conoutf(CON_DEBUG, "read svar %s: %s", name, val);
+ break;
+ }
+ }
+ }
+ if(dbgvars) conoutf(CON_DEBUG, "read %d vars", hdr.numvars);
+
+ string gametype;
+ copystring(gametype, "fps");
+ bool samegame = true;
+ int eif = 0;
+ if(hdr.version>=16)
+ {
+ int len = f->getchar();
+ f->read(gametype, len+1);
+ }
+ if(strcmp(gametype, game::gameident())!=0)
+ {
+ samegame = false;
+ conoutf(CON_WARN, "WARNING: loading map from %s game, ignoring entities except for lights/mapmodels", gametype);
+ }
+ if(hdr.version>=16)
+ {
+ eif = f->getlil<ushort>();
+ int extrasize = f->getlil<ushort>();
+ vector<char> extras;
+ f->read(extras.pad(extrasize), extrasize);
+ if(samegame) game::readgamedata(extras);
+ }
+
+ texmru.shrink(0);
+ if(hdr.version<14)
+ {
+ uchar oldtl[256];
+ f->read(oldtl, sizeof(oldtl));
+ loopi(256) texmru.add(oldtl[i]);
+ }
+ else
+ {
+ ushort nummru = f->getlil<ushort>();
+ loopi(nummru) texmru.add(f->getlil<ushort>());
+ }
+
+ renderprogress(0, "loading entities...");
+
+ vector<extentity *> &ents = entities::getents();
+ int einfosize = entities::extraentinfosize();
+ char *ebuf = einfosize > 0 ? new char[einfosize] : NULL;
+ loopi(min(hdr.numents, MAXENTS))
+ {
+ extentity &e = *entities::newentity();
+ ents.add(&e);
+ f->read(&e, sizeof(entity));
+ lilswap(&e.o.x, 3);
+ lilswap(&e.attr1, 5);
+ fixent(e, hdr.version);
+ if(samegame)
+ {
+ if(einfosize > 0) f->read(ebuf, einfosize);
+ entities::readent(e, ebuf, mapversion);
+ }
+ else
+ {
+ if(eif > 0) f->seek(eif, SEEK_CUR);
+ if(e.type>=ET_GAMESPECIFIC || hdr.version<=14)
+ {
+ entities::deleteentity(ents.pop());
+ continue;
+ }
+ }
+ if(!insideworld(e.o))
+ {
+ if(e.type != ET_LIGHT && e.type != ET_SPOTLIGHT)
+ {
+ conoutf(CON_WARN, "warning: ent outside of world: enttype[%s] index %d (%f, %f, %f)", entities::entname(e.type), i, e.o.x, e.o.y, e.o.z);
+ }
+ }
+ if(hdr.version <= 14 && e.type == ET_MAPMODEL)
+ {
+ e.o.z += e.attr3;
+ if(e.attr4) conoutf(CON_WARN, "warning: mapmodel ent (index %d) uses texture slot %d", i, e.attr4);
+ e.attr3 = e.attr4 = 0;
+ }
+ }
+ if(ebuf) delete[] ebuf;
+
+ if(hdr.numents > MAXENTS)
+ {
+ conoutf(CON_WARN, "warning: map has %d entities", hdr.numents);
+ f->seek((hdr.numents-MAXENTS)*(samegame ? sizeof(entity) + einfosize : eif), SEEK_CUR);
+ }
+
+ renderprogress(0, "loading slots...");
+ loadvslots(f, hdr.numvslots);
+
+ renderprogress(0, "loading octree...");
+ bool failed = false;
+ worldroot = loadchildren(f, ivec(0, 0, 0), hdr.worldsize>>1, failed);
+ if(failed) conoutf(CON_ERROR, "garbage in map");
+
+ renderprogress(0, "validating...");
+ validatec(worldroot, hdr.worldsize>>1);
+
+ if(!failed)
+ {
+ if(hdr.version >= 7) loopi(hdr.lightmaps)
+ {
+ renderprogress(i/(float)hdr.lightmaps, "loading lightmaps...");
+ LightMap &lm = lightmaps.add();
+ if(hdr.version >= 17)
+ {
+ int type = f->getchar();
+ lm.type = type&0x7F;
+ if(hdr.version >= 20 && type&0x80)
+ {
+ lm.unlitx = f->getlil<ushort>();
+ lm.unlity = f->getlil<ushort>();
+ }
+ }
+ if(lm.type&LM_ALPHA && (lm.type&LM_TYPE)!=LM_BUMPMAP1) lm.bpp = 4;
+ lm.data = new uchar[lm.bpp*LM_PACKW*LM_PACKH];
+ f->read(lm.data, lm.bpp * LM_PACKW * LM_PACKH);
+ lm.finalize();
+ }
+
+ if(hdr.version >= 25 && hdr.numpvs > 0) loadpvs(f, hdr.numpvs);
+ if(hdr.version >= 28 && hdr.blendmap) loadblendmap(f, hdr.blendmap);
+ }
+
+ mapcrc = f->getcrc();
+ delete f;
+
+ conoutf("read map %s (%.1f seconds)", ogzname, (SDL_GetTicks()-loadingstart)/1000.0f);
+
+ clearmainmenu();
+
+ identflags |= IDF_OVERRIDDEN;
+ execfile("data/default_map_settings.cfg", false);
+ execfile(cfgname, false);
+ identflags &= ~IDF_OVERRIDDEN;
+
+ extern void fixlightmapnormals();
+ if(hdr.version <= 25) fixlightmapnormals();
+ extern void fixrotatedlightmaps();
+ if(hdr.version <= 31) fixrotatedlightmaps();
+
+ preloadusedmapmodels(true);
+
+ game::preload();
+ flushpreloadedmodels();
+
+ preloadmapsounds();
+
+ entitiesinoctanodes();
+ attachentities();
+ initlights();
+ allchanged(true);
+
+ renderbackground("loading...", mapshot, mname, game::getmapinfo());
+
+ if(maptitle[0] && strcmp(maptitle, "Untitled Map by Unknown")) conoutf(CON_ECHO, "%s", maptitle);
+
+ startmap(cname ? cname : mname);
+
+ return true;
+}
+
+void savecurrentmap() { save_world(game::getclientmap()); }
+void savemap(char *mname) { save_world(mname); }
+
+COMMAND(savemap, "s");
+COMMAND(savecurrentmap, "");
+
+void writeobj(char *name)
+{
+ defformatstring(fname, "%s.obj", name);
+ stream *f = openfile(path(fname), "w");
+ if(!f) return;
+ f->printf("# obj file of Cube 2 level\n\n");
+ defformatstring(mtlname, "%s.mtl", name);
+ path(mtlname);
+ f->printf("mtllib %s\n\n", mtlname);
+ vector<vec> verts;
+ vector<vec2> texcoords;
+ hashtable<vec, int> shareverts(1<<16);
+ hashtable<vec2, int> sharetc(1<<16);
+ hashtable<int, vector<ivec2> > mtls(1<<8);
+ vector<int> usedmtl;
+ vec bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f);
+ loopv(valist)
+ {
+ vtxarray &va = *valist[i];
+ ushort *edata = NULL;
+ vertex *vdata = NULL;
+ if(!readva(&va, edata, vdata)) continue;
+ ushort *idx = edata;
+ loopj(va.texs)
+ {
+ elementset &es = va.eslist[j];
+ if(usedmtl.find(es.texture) < 0) usedmtl.add(es.texture);
+ vector<ivec2> &keys = mtls[es.texture];
+ loopk(es.length[1])
+ {
+ int n = idx[k] - va.voffset;
+ const vertex &v = vdata[n];
+ const vec &pos = v.pos;
+ const vec2 &tc = v.tc;
+ ivec2 &key = keys.add();
+ key.x = shareverts.access(pos, verts.length());
+ if(key.x == verts.length())
+ {
+ verts.add(pos);
+ loopl(3)
+ {
+ bbmin[l] = min(bbmin[l], pos[l]);
+ bbmax[l] = max(bbmax[l], pos[l]);
+ }
+ }
+ key.y = sharetc.access(tc, texcoords.length());
+ if(key.y == texcoords.length()) texcoords.add(tc);
+ }
+ idx += es.length[1];
+ }
+ delete[] edata;
+ delete[] vdata;
+ }
+
+ vec center(-(bbmax.x + bbmin.x)/2, -(bbmax.y + bbmin.y)/2, -bbmin.z);
+ loopv(verts)
+ {
+ vec v = verts[i];
+ v.add(center);
+ if(v.y != floor(v.y)) f->printf("v %.3f ", -v.y); else f->printf("v %d ", int(-v.y));
+ if(v.z != floor(v.z)) f->printf("%.3f ", v.z); else f->printf("%d ", int(v.z));
+ if(v.x != floor(v.x)) f->printf("%.3f\n", v.x); else f->printf("%d\n", int(v.x));
+ }
+ f->printf("\n");
+ loopv(texcoords)
+ {
+ const vec2 &tc = texcoords[i];
+ f->printf("vt %.6f %.6f\n", tc.x, 1-tc.y);
+ }
+ f->printf("\n");
+
+ usedmtl.sort();
+ loopv(usedmtl)
+ {
+ vector<ivec2> &keys = mtls[usedmtl[i]];
+ f->printf("g slot%d\n", usedmtl[i]);
+ f->printf("usemtl slot%d\n\n", usedmtl[i]);
+ for(int i = 0; i < keys.length(); i += 3)
+ {
+ f->printf("f");
+ loopk(3) f->printf(" %d/%d", keys[i+2-k].x+1, keys[i+2-k].y+1);
+ f->printf("\n");
+ }
+ f->printf("\n");
+ }
+ delete f;
+
+ f = openfile(mtlname, "w");
+ if(!f) return;
+ f->printf("# mtl file of Cube 2 level\n\n");
+ loopv(usedmtl)
+ {
+ VSlot &vslot = lookupvslot(usedmtl[i], false);
+ f->printf("newmtl slot%d\n", usedmtl[i]);
+ f->printf("map_Kd %s\n", vslot.slot->sts.empty() ? notexture->name : path(makerelpath("packages", vslot.slot->sts[0].name)));
+ f->printf("\n");
+ }
+ delete f;
+}
+
+COMMAND(writeobj, "s");
+
+#endif
+