[SCM] Packaging for Red Eclipse branch, master-svn-ppa, updated. debian/1.2-2-52-g97e4152

Martin Erik Werner martinerikwerner at gmail.com
Tue Aug 7 19:30:22 UTC 2012


The following commit has been merged in the master-svn-ppa branch:
commit 0cee4de23e3e000634aa6ed47e2d63edab11771d
Author: Martin Erik Werner <martinerikwerner at gmail.com>
Date:   Tue Aug 7 03:17:22 2012 +0200

    Imported Upstream version 1.2+svn3822

diff --git a/changelog.txt b/changelog.txt
index 5d5ee44..144f5b1 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -19,6 +19,7 @@ Maps:
 
 Modes & Mutators:
  * Hover mutator removed
+ * Added "gamespeedlock" to independently allow servers to unlock "gamespeed" (still checks varslock though)
 
 Modding:
  * Brush selection now mapped to K+wheel (unbound in 1.2, J+wheel in 1.1)
@@ -33,23 +34,30 @@ Misc:
  * Enet 1.3.5
  * 64bit builds for Windows
  * Use APP/app* variables for name abstraction (simplifies forking/install renaming)
- * *nix icons stored as PNG, no ImageMagick needed in system-install process
+ * *nix icons stored as PNG/XPM, no ImageMagick needed in system-install process
+ * Show player count on server browser
+ * Allow players to cast votes for ones made by an admin which conflict with a lock control
 
 Additional Fixed Bugs:
- * #103 [minor] Revenge-seeking remains after team-change
- * #112 Fix server browser sorting full servers to the bottom
- * #115 When joining mid-match, bomber-ball affinities aren't enabled
- * #128 Buff state not properly synchronised
- * #129 Spawn rotation problems
- * #136 Arena-expert and submodes are bugged (rocket & grenade select issues)
- * #132 venus geom errors
- * #135 ghost geom errors
- * #117 Team-duel problems (force cycle team member after 3 wins)
- * #113 Botbalance isues (breaks if teambalance = 2 or team imbalance is > 1)
- * #154 Fix darkness omega bb-goal to be at same position as alpha
- * #158 Possibility to miss first checkpoint on testchamber
- * #161 Include waypoints and text file with sendmap
+ * #103 - [minor] Revenge-seeking remains after team-change
+ * #112 - Fix server browser sorting full servers to the bottom
+ * #115 - When joining mid-match, bomber-ball affinities aren't enabled
+ * #128 - Buff state not properly synchronised
+ * #129 - Spawn rotation problems
+ * #136 - Arena-expert and submodes are bugged (rocket & grenade select issues)
+ * #132 - venus geom errors
+ * #135 - ghost geom errors
+ * #117 - Team-duel problems (force cycle team member after 3 wins)
+ * #113 - Botbalance isues (breaks if teambalance = 2 or team imbalance is > 1)
+ * #154 - Fix darkness omega bb-goal to be at same position as alpha
+ * #158 - Possibility to miss first checkpoint on testchamber
+ * #161 - Include waypoints and text file with sendmap
  * #164 - synchronise death count in resume/frag messages
+ * #165 - ghost clipping (disable odd sniper spot)
+ * #163 - added ability to lock spec/kick/ban to specific access levels, applied same logic to other lock variables
+ * #148 - replaced tabs on texture gui with scrolling pages
+ * #141 - can select a team while in spectator to join the game on a specific team
+ * #166 - ability to save and automatically use arena loadouts
 
 = Red Eclipse 1.2 =
 Gameplay:
@@ -109,26 +117,26 @@ Misc:
  * system-install make target for packaging convenience
 
 Additional Fixed Bugs:
- * #8 AI performance issues
- * #30 Use RE_DIR=$(dirname $0) in launch script
- * #31 Changing weapon name breaks the entry in Variables GUI
- * #32 Floating/Infinite Flight Bug
- * #33 random weapon selection in arena not working
- * #35 /firstpersondist (exploit)
- * #37 Player color (skew towards team color)
- * #38 Tweaking hit sounds for shotgun/flak bleed
- * #39 teampersist crashes server
- * #40 dedicated server demo recording is broken
- * #42 Plasma Blue-Ball-of-Death sticking to Ragdoll
- * #43 Create lower poly weapon models for item and vwep version
- * #44 Entity Radius Broken in Edit Mode
- * #47 Create "botoffset" variable to replace INSERT/DELETE behaviour
- * #50 Use "aiclip" on common areas where bots can't travel
- * #53 can't drop flag in ctf-protect
- * #60 Live Support does not work correctly
- * #65 Clicking the Red Eclipse icon on map selection without selecting a map produces an error message
- * #66 Winning a conquer defend-the-flag match yields an insanely high score
- * #69 Edit mode segfault
- * #70 Temporarily missing sound for Flamer primary fire after reload
- * #74 better Link line colors
- * #81 Killing the opponent team in Survivor CTF/BB does not yield you a point
+ * #8 - AI performance issues
+ * #30 - Use RE_DIR=$(dirname $0) in launch script
+ * #31 - Changing weapon name breaks the entry in Variables GUI
+ * #32 - Floating/Infinite Flight Bug
+ * #33 - random weapon selection in arena not working
+ * #35 - /firstpersondist (exploit)
+ * #37 - Player color (skew towards team color)
+ * #38 - Tweaking hit sounds for shotgun/flak bleed
+ * #39 - teampersist crashes server
+ * #40 - dedicated server demo recording is broken
+ * #42 - Plasma Blue-Ball-of-Death sticking to Ragdoll
+ * #43 - Create lower poly weapon models for item and vwep version
+ * #44 - Entity Radius Broken in Edit Mode
+ * #47 - Create "botoffset" variable to replace INSERT/DELETE behaviour
+ * #50 - Use "aiclip" on common areas where bots can't travel
+ * #53 - can't drop flag in ctf-protect
+ * #60 - Live Support does not work correctly
+ * #65 - Clicking the Red Eclipse icon on map selection without selecting a map produces an error message
+ * #66 - Winning a conquer defend-the-flag match yields an insanely high score
+ * #69 - Edit mode segfault
+ * #70 - Temporarily missing sound for Flamer primary fire after reload
+ * #74 - better Link line colors
+ * #81 - Killing the opponent team in Survivor CTF/BB does not yield you a point
diff --git a/src/engine/irc.cpp b/src/engine/irc.cpp
index 1135c86..bb152e7 100644
--- a/src/engine/irc.cpp
+++ b/src/engine/irc.cpp
@@ -80,7 +80,7 @@ VAR(0, ircfilter, 0, 1, 2);
 void converttext(char *dst, const char *src)
 {
     int colorpos = 0; char colorstack[10];
-    loopi(10) colorstack[i] = 'u'; //indicate user color
+    memset(colorstack, 'u', sizeof(colorstack)); //indicate user color
     for(int c = *src; c; c = *++src)
     {
         if(c == '\f')
@@ -96,8 +96,8 @@ void converttext(char *dst, const char *src)
                 const char *end = strchr(src, c == '[' ? ']' : ')');
                 src += end ? end-src : strlen(src);
             }
-            else if(c == 's') { colorpos++; continue; }
-            else if(c == 'S') { c = colorstack[--colorpos]; }
+            else if(c == 's') { if(colorpos < (int)sizeof(colorstack)-1) colorpos++; continue; }
+            else if(c == 'S') { if(colorpos > 0) --colorpos; c = colorstack[colorpos]; }
             int oldcolor = colorstack[colorpos]; colorstack[colorpos] = c;
             switch(c)
             {
diff --git a/src/engine/main.cpp b/src/engine/main.cpp
index 66f86ea..757f85f 100644
--- a/src/engine/main.cpp
+++ b/src/engine/main.cpp
@@ -789,13 +789,9 @@ SVAR(0, progresstext, "");
 FVAR(0, progressamt, 0, 0, 1);
 FVAR(0, progresspart, 0, 0, 1);
 
-int lastprogress = 0;
-
 void progress(float bar1, const char *text1, float bar2, const char *text2)
 {
-    if(progressing || !inbetweenframes) return;
-    if(!lastprogress) { lastprogress = totalmillis; return; }
-    else if(totalmillis-lastprogress > 0) return;
+    if(progressing || !inbetweenframes || envmapping) return;
     clientkeepalive();
 
     #ifdef __APPLE__
@@ -1021,7 +1017,6 @@ int main(int argc, char **argv)
             if(!minimized)
             {
                 inbetweenframes = renderedframe = false;
-                lastprogress = 0;
                 gl_drawframe(screen->w, screen->h);
                 renderedframe = true;
                 swapbuffers();
diff --git a/src/engine/octaedit.cpp b/src/engine/octaedit.cpp
index 1e71015..bd2041b 100644
--- a/src/engine/octaedit.cpp
+++ b/src/engine/octaedit.cpp
@@ -940,7 +940,7 @@ static hashset<octabrush> octabrushes;
 void delbrush(char *name)
 {
     if(octabrushes.remove(name))
-        conoutf("deleted brush %s", name); 
+        conoutf("deleted brush %s", name);
 }
 COMMAND(0, delbrush, "s");
 
@@ -966,7 +966,7 @@ void savebrush(char *name)
     lilswap(&hdr.version, 1);
     f->write(&hdr, sizeof(hdr));
     streambuf<uchar> s(f);
-    if(!packblock(*b->copy, s)) { delete f; conoutf("\frcould not pack brush %s", filename); return; } 
+    if(!packblock(*b->copy, s)) { delete f; conoutf("\frcould not pack brush %s", filename); return; }
     delete f;
     conoutf("wrote brush file %s", filename);
 }
@@ -1007,7 +1007,7 @@ void pastebrush(char *name)
     pasteblock(*b->copy, sel);
 }
 COMMAND(0, pastebrush, "s");
- 
+
 void mpcopy(editinfo *&e, selinfo &sel, bool local)
 {
     if(local) client::edittrigger(sel, EDIT_COPY);
@@ -2178,104 +2178,98 @@ static int lastthumbnail = 0;
 struct texturegui : guicb
 {
     bool menuon;
-    int menustart, menutab, menutex;
+    int menustart, menupage, menutex;
 
     texturegui() : menustart(-1), menutex(-1) {}
 
     void gui(guient &g, bool firstpass)
     {
         extern VSlot dummyvslot;
-        int origtab = menutab, nextslot = menutex,
-            numtabs = max((texmru.length() + thumbwidth*thumbheight - 1)/(thumbwidth*thumbheight), 1);;
-        g.start(menustart, menuscale, &menutab, true);
-        loopi(numtabs)
+        int nextslot = menutex, numpages = max((texmru.length() + thumbwidth*thumbheight - 1)/(thumbwidth*thumbheight), 1)-1;
+        if(menupage > numpages) menupage = numpages;
+        else if(menupage < 0) menupage = 0;
+        g.start(menustart, menuscale, NULL, true);
+        g.pushlist();
+        g.space(2);
+        if(g.button("\fgauto apply", 0xFFFFFF, autoapplytexgui ? "checkboxon" : "checkbox", 0xFFFFFF, autoapplytexgui ? false : true)&GUI_UP)
+            autoapplytexgui = autoapplytexgui ? 0 : 1;
+        g.space(2);
+        if(g.button("\fgauto close", 0xFFFFFF, autoclosetexgui ? (autoclosetexgui > 1 ? "checkboxtwo" : "checkboxon") : "checkbox", 0xFFFFFF, autoclosetexgui ? false : true)&GUI_UP)
+            autoclosetexgui = autoclosetexgui ? (autoclosetexgui > 1 ? 0 : 2) : 1;
+        g.poplist();
+        g.space(1);
+        g.pushlist();
+        if(texmru.inrange(menutex))
         {
-            g.tab(!i ? "textures" : NULL);
-            if(i+1 != origtab) continue; //don't load textures on non-visible tabs!
-            g.pushlist();
-            g.pushlist();
-            g.pushlist();
-            g.space(2);
-            if(g.button("\fgauto apply", 0xFFFFFF, autoapplytexgui ? "checkboxon" : "checkbox", 0xFFFFFF, autoapplytexgui ? false : true)&GUI_UP)
-                autoapplytexgui = autoapplytexgui ? 0 : 1;
-            g.space(2);
-            if(g.button("\fgauto close", 0xFFFFFF, autoclosetexgui ? (autoclosetexgui > 1 ? "checkboxtwo" : "checkboxon") : "checkbox", 0xFFFFFF, autoclosetexgui ? false : true)&GUI_UP)
-                autoclosetexgui = autoclosetexgui ? (autoclosetexgui > 1 ? 0 : 2) : 1;
-            g.poplist();
-            g.space(1);
-            g.pushlist();
-            if(texmru.inrange(menutex))
+            VSlot &v = lookupvslot(texmru[menutex], false);
+            if(v.slot->sts.empty()) g.texture(dummyvslot, thumbheight*thumbsize, false);
+            else if(!v.slot->loaded && !v.slot->thumbnail)
             {
-                VSlot &v = lookupvslot(texmru[menutex], false);
-                if(v.slot->sts.empty()) continue;
-                else if(!v.slot->loaded && !v.slot->thumbnail)
-                {
-                    if(totalmillis-lastthumbnail<thumbtime)
-                    {
-                        g.texture(dummyvslot, thumbheight*thumbsize, false); //create an empty space
-                        continue;
-                    }
-                    loadthumbnail(*v.slot);
-                    lastthumbnail = totalmillis;
-                }
-                if(g.texture(v, thumbheight*thumbsize, true)&GUI_UP)
-                {
-                    edittex(texmru[menutex]);
-                    if(autoclosetexgui) menuon = false;
-                }
+                if(totalmillis-lastthumbnail<thumbtime)
+                    g.texture(dummyvslot, thumbheight*thumbsize, false); //create an empty space
+                loadthumbnail(*v.slot);
+                lastthumbnail = totalmillis;
             }
-            else g.image(textureload("textures/nothumb", 3), thumbheight*thumbsize, true);
-            g.space(1);
+            if(g.texture(v, thumbheight*thumbsize, true)&GUI_UP)
+            {
+                edittex(texmru[menutex]);
+                if(autoclosetexgui) menuon = false;
+            }
+        }
+        else g.image(textureload("textures/nothumb", 3), thumbheight*thumbsize, true);
+        g.space(1);
+        g.pushlist();
+        g.pushlist();
+        g.pushlist();
+        loop(h, thumbheight)
+        {
             g.pushlist();
-            loop(h, thumbheight)
+            loop(w, thumbwidth)
             {
-                g.pushlist();
-                loop(w, thumbwidth)
+                int ti = (menupage*thumbheight+h)*thumbwidth+w;
+                if(ti<texmru.length())
                 {
-                    int ti = (i*thumbheight+h)*thumbwidth+w;
-                    if(ti<texmru.length())
+                    VSlot &v = lookupvslot(texmru[ti], false);
+                    if(v.slot->sts.empty()) continue;
+                    else if(!v.slot->loaded && !v.slot->thumbnail)
                     {
-                        VSlot &v = lookupvslot(texmru[ti], false);
-                        if(v.slot->sts.empty()) continue;
-                        else if(!v.slot->loaded && !v.slot->thumbnail)
+                        if(totalmillis-lastthumbnail<thumbtime)
                         {
-                            if(totalmillis-lastthumbnail<thumbtime)
-                            {
-                                g.texture(dummyvslot, thumbsize, false); //create an empty space
-                                continue;
-                            }
-                            loadthumbnail(*v.slot);
-                            lastthumbnail = totalmillis;
+                            g.texture(dummyvslot, thumbsize, false); //create an empty space
+                            continue;
                         }
-                        if(g.texture(v, thumbsize, true)&GUI_UP && (v.slot->loaded || v.slot->thumbnail!=notexture))
+                        loadthumbnail(*v.slot);
+                        lastthumbnail = totalmillis;
+                    }
+                    if(g.texture(v, thumbsize, true)&GUI_UP && (v.slot->loaded || v.slot->thumbnail!=notexture))
+                    {
+                        nextslot = ti;
+                        if(autoapplytexgui)
                         {
-                            nextslot = ti;
-                            if(autoapplytexgui)
-                            {
-                                edittex(texmru[ti]);
-                                if(autoclosetexgui > 1) menuon = false;
-                            }
+                            edittex(texmru[ti]);
+                            if(autoclosetexgui > 1) menuon = false;
                         }
                     }
-                    else g.texture(dummyvslot, thumbsize, false); //create an empty space
                 }
-                g.poplist();
-            }
-            g.poplist();
-            g.poplist();
-            g.space(1);
-            g.pushlist();
-            g.space(1);
-            if(texmru.inrange(menutex))
-            {
-                VSlot &v = lookupvslot(texmru[menutex]);
-                g.textf("#%-3d \fa%s", 0xFFFFFF, NULL, 0, texmru[menutex], v.slot->sts.empty() ? "<unknown texture>" : v.slot->sts[0].name);
+                else g.texture(dummyvslot, thumbsize, false); //create an empty space
             }
-            else g.textf("no texture selected", 0x888888);
-            g.poplist();
-            g.poplist();
             g.poplist();
         }
+        g.poplist();
+        g.slider(menupage, 0, numpages, 0xFFFFFF, NULL, true, true);
+        g.poplist();
+        g.poplist();
+        g.poplist();
+        g.space(1);
+        g.pushlist();
+        g.space(1);
+        if(texmru.inrange(menutex))
+        {
+            VSlot &v = lookupvslot(texmru[menutex]);
+            g.textf("#%-3d \fa%s", 0xFFFFFF, NULL, 0, texmru[menutex], v.slot->sts.empty() ? "<unknown texture>" : v.slot->sts[0].name);
+        }
+        else g.textf("no texture selected", 0x888888);
+        g.poplist();
         menutex = nextslot;
         g.end();
     }
@@ -2284,11 +2278,15 @@ struct texturegui : guicb
     {
         if(on != menuon && (menuon = on))
         {
-            if(menustart <= lasttexmillis)
-                menutab = 1+clamp(texmru.find(lasttex), 0, texmru.length()-1)/(thumbwidth*thumbheight);
             menustart = starttime();
             cube &c = lookupcube(sel.o.x, sel.o.y, sel.o.z, -sel.grid);
-            menutex = !isempty(c) ? texmru.find(c.texture[sel.orient]) : 0;
+            if(texmru.length() > 0)
+            {
+                if(!texmru.inrange(menutex = !isempty(c) ? texmru.find(c.texture[sel.orient]) : texmru.find(lasttex)))
+                    menutex = 0;
+                menupage = clamp(menutex, 0, texmru.length()-1)/(thumbwidth*thumbheight);
+            }
+            else menutex = menupage = 0;
         }
     }
 
diff --git a/src/engine/rendertext.cpp b/src/engine/rendertext.cpp
index 7fe5899..f23bed5 100644
--- a/src/engine/rendertext.cpp
+++ b/src/engine/rendertext.cpp
@@ -1,6 +1,6 @@
 #include "engine.h"
 
-VAR(IDF_PERSIST, blinkingtext, 0, 1, 1);
+VAR(IDF_PERSIST, blinkingtext, 0, 250, VAR_MAX);
 
 static inline bool htcmp(const char *key, const font &f) { return !strcmp(key, f.name); }
 
@@ -269,7 +269,7 @@ static float icon_width(const char *name, float scale)
     if(g[h] == 'z' && g[h+1]) \
     { \
         h++; \
-        bool alt = blinkingtext && totalmillis%500 > 250; \
+        bool alt = blinkingtext && totalmillis%(blinkingtext*2) > blinkingtext; \
         TEXTCOLOR(h); \
         if(g[h+1]) \
         { \
diff --git a/src/engine/texture.cpp b/src/engine/texture.cpp
index 415ba1e..b6ca131 100644
--- a/src/engine/texture.cpp
+++ b/src/engine/texture.cpp
@@ -134,9 +134,9 @@ static void reorients3tc(GLenum format, int blocksize, int w, int h, uchar *src,
             if(normals)
             {
                 ushort ncolor1 = color1, ncolor2 = color2;
-                if(flipx) 
-                { 
-                    ncolor1 = (ncolor1 & ~0xF800) | (0xF800 - (ncolor1 & 0xF800)); 
+                if(flipx)
+                {
+                    ncolor1 = (ncolor1 & ~0xF800) | (0xF800 - (ncolor1 & 0xF800));
                     ncolor2 = (ncolor2 & ~0xF800) | (0xF800 - (ncolor2 & 0xF800));
                 }
                 if(flipy)
@@ -1358,19 +1358,20 @@ MSlot materialslots[MATF_VOLUME+1];
 Slot dummyslot;
 VSlot dummyvslot(&dummyslot);
 
-void resettextures()
+void resettextures(int n)
 {
     resetslotshader();
-    loopv(slots)
+    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.shrink(0);
+    slots.setsize(limit);
 }
 
-ICOMMAND(0, texturereset, "", (void), if(editmode || identflags&IDF_WORLD) resettextures(););
+ICOMMAND(0, texturereset, "i", (int *n), if(editmode || identflags&IDF_WORLD) resettextures(*n););
 
 void resetmaterials()
 {
@@ -1440,7 +1441,15 @@ int compactvslots(bool cull)
     compactedvslots = 0;
     compactvslotsprogress = 0;
     loopv(vslots) vslots[i]->index = -1;
-    if(!cull)
+    if(cull)
+    {
+        if(slots.inrange(DEFAULT_SKY))
+        {
+            slots[DEFAULT_SKY]->variants->index = compactedvslots++;
+            assignvslotlayer(*slots[DEFAULT_SKY]->variants); 
+        }
+    }
+    else
     {
         loopv(slots) slots[i]->variants->index = compactedvslots++;
         loopv(slots) assignvslotlayer(*slots[i]->variants);
@@ -2425,7 +2434,7 @@ Texture *cubemaploadwildcard(Texture *t, const char *name, bool mipit, bool msg,
         t->bpp = formatsize(format);
         t->type |= Texture::COMPRESSED;
     }
-    else 
+    else
     {
         format = texformat(surface[0].bpp);
         t->bpp = surface[0].bpp;
diff --git a/src/engine/texture.h b/src/engine/texture.h
index ffde4a1..4375b18 100644
--- a/src/engine/texture.h
+++ b/src/engine/texture.h
@@ -563,8 +563,8 @@ struct VSlot
     }
 
     vec getcolorscale() const { return palette || palindex ? vec(colorscale).mul(game::getpalette(palette, palindex)) : colorscale; }
-    vec getglowcolor() const 
-    { 
+    vec getglowcolor() const
+    {
         if(glowcolor)
         {
             vec c(glowcolor->val);
@@ -573,8 +573,8 @@ struct VSlot
         }
         return vec(1, 1, 1);
     }
-    vec getpulseglowcolor() const 
-    { 
+    vec getpulseglowcolor() const
+    {
         if(pulseglowcolor)
         {
             vec c(pulseglowcolor->val);
@@ -704,7 +704,7 @@ extern bool loadimage(const char *name, ImageData &image);
 extern bool loaddds(const char *filename, ImageData &image);
 
 extern void resetmaterials();
-extern void resettextures();
+extern void resettextures(int n = 0);
 extern void setshader(char *name);
 extern void setshaderparam(const char *name, int type, int n, float x, float y, float z, float w);
 extern int findtexturetype(char *name, bool tryint = false);
diff --git a/src/game/auth.h b/src/game/auth.h
index a60fd03..5741126 100644
--- a/src/game/auth.h
+++ b/src/game/auth.h
@@ -79,7 +79,7 @@ namespace auth
         if(paused)
         {
             int others = 0;
-            loopv(clients) if(clients[i]->privilege >= (GAME(varslock) >= 2 ? PRIV_ADMIN : PRIV_MASTER) || clients[i]->local) others++;
+            loopv(clients) if(clients[i]->privilege >= PRIV_ADMIN || clients[i]->local) others++;
             if(!others) setpause(false);
         }
     }
diff --git a/src/game/bomber.cpp b/src/game/bomber.cpp
index 01ac7ef..91a2c25 100644
--- a/src/game/bomber.cpp
+++ b/src/game/bomber.cpp
@@ -355,6 +355,7 @@ namespace bomber
                     loopk(3) inertia[k] = getint(p)/DMF;
                 }
             }
+            if(p.overread()) break;
             if(st.flags.inrange(i))
             {
                 bomberstate::flag &f = st.flags[i];
@@ -423,10 +424,14 @@ namespace bomber
         if(f.enabled && value)
         {
             destroyaffinity(f.pos());
-            if(value == 2 && isbomberaffinity(f))
+            if(isbomberaffinity(f))
             {
-                affinityeffect(i, TEAM_NEUTRAL, f.pos(), f.spawnloc, 3, "RESET");
-                game::announcef(S_V_BOMBRESET, CON_INFO, NULL, "\fathe \fs\fwbomb\fS has been reset");
+                if(value == 2)
+                {
+                    affinityeffect(i, TEAM_NEUTRAL, f.pos(), f.spawnloc, 3, "RESET");
+                    game::announcef(S_V_BOMBRESET, CON_INFO, NULL, "\fathe \fs\fwbomb\fS has been reset");
+                }
+                entities::execlink(NULL, f.ent, false);
             }
         }
         st.returnaffinity(i, lastmillis, value!=0);
@@ -438,6 +443,8 @@ namespace bomber
         bomberstate::flag &f = st.flags[relay], &g = st.flags[goal];
         affinityeffect(goal, d->team, g.spawnloc, f.spawnloc, 3, "DESTROYED");
         destroyaffinity(g.spawnloc);
+        entities::execlink(NULL, f.ent, false);
+        entities::execlink(NULL, g.ent, false);
         hud::teamscore(d->team).total = score;
         game::announcef(S_V_BOMBSCORE, d == game::focus ? CON_SELF : CON_INFO, d, "\fa%s destroyed the \fs\f[%d]%s\fS base for team \fs\f[%d]%s\fS (score: \fs\fc%d\fS, time taken: \fs\fc%s\fS)", game::colorname(d), TEAM(g.team, colour), TEAM(g.team, name), TEAM(d->team, colour), TEAM(d->team, name), score, hud::timetostr(lastmillis-f.inittime));
         st.returnaffinity(relay, lastmillis, false);
@@ -454,6 +461,7 @@ namespace bomber
         {
             affinityeffect(i, d->team, d->feetpos(), f.pos(), 1, "TAKEN");
             game::announcef(S_V_BOMBPICKUP, d == game::focus ? CON_SELF : CON_INFO, d, "\fa%s picked up the \fs\fwbomb\fS", game::colorname(d));
+            entities::execlink(NULL, f.ent, false);
         }
         st.takeaffinity(i, d, lastmillis);
     }
diff --git a/src/game/capture.cpp b/src/game/capture.cpp
index 85a9e1f..bea33ed 100644
--- a/src/game/capture.cpp
+++ b/src/game/capture.cpp
@@ -368,6 +368,7 @@ namespace capture
                     loopk(3) inertia[k] = getint(p)/DMF;
                 }
             }
+            if(p.overread()) break;
             if(st.flags.inrange(i))
             {
                 capturestate::flag &f = st.flags[i];
@@ -424,6 +425,7 @@ namespace capture
         capturestate::flag &f = st.flags[i];
         affinityeffect(i, d->team, d->feetpos(), f.spawnloc, m_gsp(game::gamemode, game::mutators) ? 2 : 3, "RETURNED");
         game::announcef(S_V_FLAGRETURN, d == game::focus ? CON_SELF : CON_INFO, d, "\fa%s returned the \fs\f[%d]%s\fS flag (time taken: \fs\fc%s\fS)", game::colorname(d), TEAM(f.team, colour), TEAM(f.team, name), hud::timetostr(lastmillis-(m_gsp1(game::gamemode, game::mutators) || m_gsp3(game::gamemode, game::mutators) ? f.taketime : f.droptime)));
+        entities::execlink(NULL, f.ent, false);
         st.returnaffinity(i, lastmillis);
     }
 
@@ -436,6 +438,7 @@ namespace capture
             affinityeffect(i, TEAM_NEUTRAL, f.droploc, f.spawnloc, 3, "RESET");
             game::announcef(S_V_FLAGRESET, CON_INFO, NULL, "\fathe \fs\f[%d]%s\fS flag has been reset", TEAM(f.team, colour), TEAM(f.team, name));
         }
+        entities::execlink(NULL, f.ent, false);
         st.returnaffinity(i, lastmillis);
     }
 
@@ -447,8 +450,10 @@ namespace capture
         {
             capturestate::flag &g = st.flags[goal];
             affinityeffect(goal, d->team, g.spawnloc, f.spawnloc, 3, "CAPTURED");
+            entities::execlink(NULL, g.ent, false);
         }
         else affinityeffect(goal, d->team, f.pos(), f.spawnloc, 3, "CAPTURED");
+        entities::execlink(NULL, f.ent, false);
         hud::teamscore(d->team).total = score;
         game::announcef(S_V_FLAGSCORE, d == game::focus ? CON_SELF : CON_INFO, d, "\fa%s captured the \fs\f[%d]%s\fS flag for team \fs\f[%d]%s\fS (score: \fs\fc%d\fS, time taken: \fs\fc%s\fS)", game::colorname(d), TEAM(f.team, colour), TEAM(f.team, name), TEAM(d->team, colour), TEAM(d->team, name), score, hud::timetostr(lastmillis-f.taketime));
         st.returnaffinity(relay, lastmillis);
@@ -463,6 +468,7 @@ namespace capture
         playsound(S_CATCH, d->o, d);
         affinityeffect(i, d->team, d->feetpos(), f.pos(), 1, f.team == d->team ? "SECURED" : "TAKEN");
         game::announcef(f.team == d->team ? S_V_FLAGSECURED : S_V_FLAGPICKUP, d == game::focus ? CON_SELF : CON_INFO, d, "\fa%s %s the \fs\f[%d]%s\fS flag", game::colorname(d), f.team == d->team ? "secured" : (f.droptime ? "picked up" : "stole"), TEAM(f.team, colour), TEAM(f.team, name));
+        entities::execlink(NULL, f.ent, false);
         st.takeaffinity(i, d, lastmillis);
     }
 
diff --git a/src/game/client.cpp b/src/game/client.cpp
index 11a2924..22c64c4 100644
--- a/src/game/client.cpp
+++ b/src/game/client.cpp
@@ -259,7 +259,7 @@ namespace client
     {
         if(team[0])
         {
-            if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators) && game::player1->state != CS_SPECTATOR && game::player1->state != CS_EDITING)
+            if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
             {
                 int t = teamname(team);
                 if(t != game::player1->team) addmsg(N_SWITCHTEAM, "ri", t);
@@ -316,6 +316,13 @@ namespace client
     }
     ICOMMAND(0, ismaster, "i", (int *cn), intret(ismaster(*cn) ? 1 : 0));
 
+    bool isauth(int cn)
+    {
+        gameent *d = game::getclient(cn);
+        return d && d->privilege >= PRIV_AUTH;
+    }
+    ICOMMAND(0, isauth, "i", (int *cn), intret(isauth(*cn) ? 1 : 0));
+
     bool isadmin(int cn)
     {
         gameent *d = game::getclient(cn);
@@ -1558,6 +1565,8 @@ namespace client
                             regularshape(PART_SPARK, f->height*2, colour, 53, 50, 350, f->headpos(-f->height/2), 1.5f, 1, 1, 0, 35);
                         }
                     }
+                    if(f->aitype <= AI_BOT && entities::ents.inrange(ent) && entities::ents[ent]->type == PLAYERSTART)
+                        entities::execlink(f, ent, false);
                     ai::spawned(f, ent);
                     if(f == game::focus) game::resetcamera();
                     f->setscale(game::rescale(f), 0, true, game::gamemode, game::mutators);
@@ -2078,19 +2087,24 @@ namespace client
 
                 case N_CHECKPOINT:
                 {
-                    int tn = getint(p), laptime = getint(p), besttime = getint(p);
+                    int tn = getint(p), ent = getint(p), laptime = getint(p), besttime = getint(p);
                     gameent *t = game::getclient(tn);
                     if(!t) break;
-                    if(laptime >= 0)
+                    if(ent >= 0)
                     {
-                        t->cplast = laptime;
-                        t->cptime = besttime;
-                        t->cpmillis = t->impulse[IM_METER] = 0;
-                        if(showlaptimes > (t != game::focus ? (t->aitype > AI_NONE ? 2 : 1) : 0))
+                        if(laptime >= 0)
                         {
-                            defformatstring(best)("%s", hud::timetostr(besttime));
-                            conoutft(t != game::focus ? CON_INFO : CON_SELF, "%s lap time: \fs\fg%s\fS (best: \fs\fy%s\fS)", game::colorname(t), hud::timetostr(laptime), best);
+                            t->cplast = laptime;
+                            t->cptime = besttime;
+                            t->cpmillis = t->impulse[IM_METER] = 0;
+                            if(showlaptimes > (t != game::focus ? (t->aitype > AI_NONE ? 2 : 1) : 0))
+                            {
+                                defformatstring(best)("%s", hud::timetostr(besttime));
+                                conoutft(t != game::focus ? CON_INFO : CON_SELF, "%s lap time: \fs\fg%s\fS (best: \fs\fy%s\fS)", game::colorname(t), hud::timetostr(laptime), best);
+                            }
                         }
+                        if(entities::ents.inrange(ent) && entities::ents[ent]->type == CHECKPOINT)
+                            entities::execlink(t, ent, false);
                     }
                     else
                     {
diff --git a/src/game/defend.cpp b/src/game/defend.cpp
index 688adf7..f1cd747 100644
--- a/src/game/defend.cpp
+++ b/src/game/defend.cpp
@@ -257,6 +257,7 @@ namespace defend
         loopi(numflags)
         {
             int kin = getint(p), converted = getint(p), owner = getint(p), enemy = getint(p);
+            if(p.overread()) break;
             st.initaffinity(i, kin, owner, enemy, converted);
         }
     }
@@ -276,6 +277,7 @@ namespace defend
                 game::announcef(S_V_FLAGSECURED, d == game::focus ? CON_SELF : CON_INFO, d, "\fateam \fs\f[%d]%s\fS secured \fw%s", TEAM(owner, colour), TEAM(owner, name), b.name);
                 part_textcopy(vec(b.o).add(vec(0, 0, enttype[AFFINITY].radius)), "<super>\fzZeSECURED", PART_TEXT, game::eventiconfade, TEAM(owner, colour), 3, 1, -10);
                 if(game::dynlighteffects) adddynlight(vec(b.o).add(vec(0, 0, enttype[AFFINITY].radius)), enttype[AFFINITY].radius*2, vec::hexcolor(TEAM(owner, colour)).mul(2.f), 500, 250);
+                entities::execlink(NULL, b.ent, false);
             }
         }
         else if(b.owner)
@@ -287,6 +289,7 @@ namespace defend
             game::announcef(S_V_FLAGOVERTHROWN, d == game::focus ? CON_SELF : CON_INFO, d, "\fateam \fs\f[%d]%s\fS overthrew \fw%s", TEAM(enemy, colour), TEAM(enemy, name), b.name);
             part_textcopy(vec(b.o).add(vec(0, 0, enttype[AFFINITY].radius)), "<super>\fzZeOVERTHROWN", PART_TEXT, game::eventiconfade, TEAM(enemy, colour), 3, 1, -10);
             if(game::dynlighteffects) adddynlight(vec(b.o).add(vec(0, 0, enttype[AFFINITY].radius)), enttype[AFFINITY].radius*2, vec::hexcolor(TEAM(enemy, colour)).mul(2.f), 500, 250);
+            entities::execlink(NULL, b.ent, false);
         }
         b.owner = owner;
         b.enemy = enemy;
diff --git a/src/game/game.cpp b/src/game/game.cpp
index af5bff0..898cf16 100644
--- a/src/game/game.cpp
+++ b/src/game/game.cpp
@@ -156,6 +156,10 @@ namespace game
     FVAR(IDF_PERSIST, playerblend, 0, 1, 1);
     VAR(IDF_PERSIST, forceplayermodel, 0, 0, NUMPLAYERMODELS);
 
+    VAR(IDF_PERSIST, autoloadweap, 0, 0, 1); // 0 = off, 1 = auto-set loadout weapons
+    VAR(IDF_PERSIST, favloadweap1, -1, -1, WEAP_MAX-1);
+    VAR(IDF_PERSIST, favloadweap2, -1, -1, WEAP_MAX-1);
+
     ICOMMAND(0, gamemode, "", (), intret(gamemode));
     ICOMMAND(0, mutators, "", (), intret(mutators));
 
@@ -392,33 +396,6 @@ namespace game
         return true;
     }
 
-    void chooseloadweap(gameent *d, const char *a, const char *b)
-    {
-        if(m_arena(gamemode, mutators))
-        {
-            loopj(2)
-            {
-                const char *s = j ? b : a;
-                if(*s >= '0' && *s <= '9') d->loadweap[j] = parseint(s);
-                else loopi(WEAP_MAX) if(!strcasecmp(WEAP(i, name), s))
-                {
-                    d->loadweap[j] = i;
-                    break;
-                }
-                if(d->loadweap[j] < WEAP_OFFSET || d->loadweap[j] >= WEAP_ITEM) d->loadweap[j] = WEAP_MELEE;
-            }
-            client::addmsg(N_LOADWEAP, "ri3", d->clientnum, d->loadweap[0], d->loadweap[1]);
-            conoutft(CON_SELF, "weapon selection is now: \fs\f[%d]\f(%s)%s\fS and \fs\f[%d]\f(%s)%s\fS",
-                WEAP(d->loadweap[0] != WEAP_MELEE ? d->loadweap[0] : WEAP_MELEE, colour), (d->loadweap[0] != WEAP_MELEE ? hud::itemtex(WEAPON, d->loadweap[0]) : hud::questiontex), (d->loadweap[0] != WEAP_MELEE ? WEAP(d->loadweap[0], name) : "random"),
-                WEAP(d->loadweap[1] != WEAP_MELEE ? d->loadweap[1] : WEAP_MELEE, colour), (d->loadweap[1] != WEAP_MELEE ? hud::itemtex(WEAPON, d->loadweap[1]) : hud::questiontex), (d->loadweap[1] != WEAP_MELEE ? WEAP(d->loadweap[1], name) : "random")
-            );
-        }
-        else conoutft(CON_MESG, "\foweapon selection is only available in arena");
-    }
-    ICOMMAND(0, loadweap, "ss", (char *a, char *b), chooseloadweap(player1, a, b));
-    ICOMMAND(0, getloadweap, "i", (int *n), intret(player1->loadweap[*n!=0 ? 1 : 0]));
-    ICOMMAND(0, allowedweap, "i", (int *n), intret(isweap(*n) && WEAP(*n, allowed) >= (m_duke(gamemode, mutators) ? 2 : 1) ? 1 : 0));
-
     void respawn(gameent *d)
     {
         if(d->state == CS_DEAD && d->respawned < 0 && (!d->lastdeath || lastmillis-d->lastdeath >= 500))
@@ -1365,6 +1342,35 @@ namespace game
         if(!empty) smartmusic(true, false);
     }
 
+    int lookupweap(const char *a)
+    {
+        if(*a >= '0' && *a <= '9') return parseint(a);
+        else loopi(WEAP_MAX) if(!strcasecmp(WEAP(i, name), a)) return i;
+        return -1;
+    }
+
+    void chooseloadweap(gameent *d, int a, int b, bool saved = false)
+    {
+        if(m_arena(gamemode, mutators))
+        {
+            loopj(2)
+            {
+                d->loadweap[j] = (j ? b : a);
+                if(d->loadweap[j] < WEAP_OFFSET || d->loadweap[j] >= WEAP_ITEM) d->loadweap[j] = WEAP_MELEE;
+                if(d == game::player1) (j ? favloadweap2 : favloadweap1) = d->loadweap[j];
+            }
+            client::addmsg(N_LOADWEAP, "ri3", d->clientnum, d->loadweap[0], d->loadweap[1]);
+            conoutft(CON_SELF, "weapon selection is now: \fs\f[%d]\f(%s)%s\fS and \fs\f[%d]\f(%s)%s\fS",
+                WEAP(d->loadweap[0] != WEAP_MELEE ? d->loadweap[0] : WEAP_MELEE, colour), (d->loadweap[0] != WEAP_MELEE ? hud::itemtex(WEAPON, d->loadweap[0]) : hud::questiontex), (d->loadweap[0] != WEAP_MELEE ? WEAP(d->loadweap[0], name) : "random"),
+                WEAP(d->loadweap[1] != WEAP_MELEE ? d->loadweap[1] : WEAP_MELEE, colour), (d->loadweap[1] != WEAP_MELEE ? hud::itemtex(WEAPON, d->loadweap[1]) : hud::questiontex), (d->loadweap[1] != WEAP_MELEE ? WEAP(d->loadweap[1], name) : "random")
+            );
+        }
+        else conoutft(CON_MESG, "\foweapon selection is only available in arena");
+    }
+    ICOMMAND(0, loadweap, "ssi", (char *a, char *b, int *n), chooseloadweap(player1, lookupweap(a), lookupweap(b), *n!=0));
+    ICOMMAND(0, getloadweap, "i", (int *n), intret(player1->loadweap[*n!=0 ? 1 : 0]));
+    ICOMMAND(0, allowedweap, "i", (int *n), intret(isweap(*n) && WEAP(*n, allowed) >= (m_duke(gamemode, mutators) ? 2 : 1) ? 1 : 0));
+
     void startmap(const char *name, const char *reqname, bool empty)    // called just after a map load
     {
         ai::startmap(name, reqname, empty);
@@ -1385,6 +1391,8 @@ namespace game
         int numdyns = numdynents();
         loopi(numdyns) if((d = (gameent *)iterdynents(i)) && (d->type == ENT_PLAYER || d->type == ENT_AI))
             d->mapchange(lastmillis, m_health(gamemode, mutators));
+        if(m_arena(gamemode, mutators) && autoloadweap && favloadweap1 >= 0 && favloadweap2 >= 0)
+            chooseloadweap(game::player1, favloadweap1, favloadweap2);
         entities::spawnplayer(player1, -1, false); // prevent the player from being in the middle of nowhere
         resetcamera();
         if(!empty) client::sendinfo = client::sendcrc = true;
diff --git a/src/game/game.h b/src/game/game.h
index e506dac..fc45056 100644
--- a/src/game/game.h
+++ b/src/game/game.h
@@ -78,7 +78,8 @@ enttypes enttype[] = {
     },
     {
         PLAYERSTART,    1,          59,     0,      EU_NONE,    6,
-            0, 0,
+            (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
+            (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             false,  true,  false,      false,      false,
                 "playerstart",  { "team",   "yaw",      "pitch",    "modes",    "muts",     "id" }
     },
@@ -90,22 +91,22 @@ enttypes enttype[] = {
     },
     {
         PARTICLES,      1,          59,     0,      EU_NONE,    11,
-            (1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER),
-            (1<<TRIGGER)|(1<<PUSHER),
+            (1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
+            (1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
             false,  false,  false,      false,      false,
                 "particles",    { "type",   "a",        "b",        "c",        "d",        "e",        "f",        "g",        "i",        "j",        "k" }
     },
     {
         MAPSOUND,       1,          58,     0,      EU_NONE,    5,
-            (1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER),
-            (1<<TRIGGER)|(1<<PUSHER),
+            (1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
+            (1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
             false,  false,  false,      false,      false,
                 "sound",        { "type",   "maxrad",   "minrad",   "volume",   "flags" }
     },
     {
         LIGHTFX,        1,          1,      0,      EU_NONE,    5,
-            (1<<LIGHT)|(1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER),
-            (1<<LIGHT)|(1<<TRIGGER)|(1<<PUSHER),
+            (1<<LIGHT)|(1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
+            (1<<LIGHT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
             false,  false,  false,      false,      false,
                 "lightfx",      { "type",   "mod",      "min",      "max",      "flags" }
     },
@@ -150,13 +151,15 @@ enttypes enttype[] = {
     },
     {
         AFFINITY,       1,          48,     32,     EU_NONE,    6,
-            0, 0,
+            (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
+            (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             false,  false,  false,      false,      false,
                 "affinity",     { "team",   "yaw",      "pitch",    "modes",    "muts",     "id" }
     },
     {
         CHECKPOINT,     1,          48,     16,     EU_AUTO,    7,
-            0, 0,
+            (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
+            (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             false,  true,   false,      false,      false,
                 "checkpoint",   { "radius", "yaw",      "pitch",    "modes",    "muts",     "id",       "type" }
     },
diff --git a/src/game/gamemode.h b/src/game/gamemode.h
index db85bd7..9eee4c1 100644
--- a/src/game/gamemode.h
+++ b/src/game/gamemode.h
@@ -1,293 +1,337 @@
-enum
-{
-    G_DEMO = 0, G_EDITMODE, G_CAMPAIGN, G_DEATHMATCH, G_CAPTURE, G_DEFEND, G_BOMBER, G_TRIAL, G_MAX,
-    G_START = G_EDITMODE, G_PLAY = G_CAMPAIGN, G_FIGHT = G_DEATHMATCH, G_RAND = G_BOMBER-G_DEATHMATCH+1,
-    G_NEVER = (1<<G_DEMO)|(1<<G_EDITMODE),
-    G_LIMIT = (1<<G_DEATHMATCH)|(1<<G_CAPTURE)|(1<<G_DEFEND)|(1<<G_BOMBER),
-    G_ALL = (1<<G_DEMO)|(1<<G_EDITMODE)|(1<<G_CAMPAIGN)|(1<<G_DEATHMATCH)|(1<<G_CAPTURE)|(1<<G_DEFEND)|(1<<G_BOMBER)|(1<<G_TRIAL)
-};
-enum
-{
-    G_M_NONE = 0,
-    G_M_MULTI = 1<<0, G_M_TEAM = 1<<1, G_M_INSTA = 1<<2, G_M_MEDIEVAL = 1<<3, G_M_BALLISTIC = 1<<4,
-    G_M_DUEL = 1<<5, G_M_SURVIVOR = 1<<6, G_M_ARENA = 1<<7, G_M_ONSLAUGHT = 1<<8,
-    G_M_JETPACK = 1<<9, G_M_VAMPIRE = 1<<10, G_M_EXPERT = 1<<11, G_M_RESIZE = 1<<12,
-    G_M_GSP1 = 1<<13, G_M_GSP2 = 1<<14, G_M_GSP3 = 1<<15,
-    G_M_ALL = G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-    G_M_FILTER = G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_SURVIVOR|G_M_ARENA|G_M_JETPACK|G_M_VAMPIRE|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-    G_M_GSN = 3, G_M_GSP = 13, G_M_NUM = 16, G_M_IGN = 2 // instagib number for instagibfilter
-};
-
-struct gametypes
-{
-    int type,           implied,
-        mutators[G_M_GSN+1];
-    const char *name,                       *gsp[G_M_GSN],
-        *desc,                              *gsd[G_M_GSN];
-};
-struct mutstypes
-{
-    int type,           implied,            mutators;
-    const char *name,
-        *desc;
-};
-#ifdef GAMESERVER
-gametypes gametype[] = {
-    {
-        G_DEMO,         G_M_NONE,
-        {
-            G_M_NONE, G_M_NONE, G_M_NONE, G_M_NONE
-        },
-        "demo",                             { "", "", "" },
-        "",                                 { "", "", "" },
-    },
-    {
-        G_EDITMODE,     G_M_NONE,
-        {
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE,
-            G_M_NONE, G_M_NONE, G_M_NONE
-        },
-        "editing",                          { "", "", "" },
-        "create and edit existing maps",    { "", "", "" },
-    },
-    {
-        G_CAMPAIGN,     G_M_TEAM,
-        {
-            G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE,
-            G_M_NONE, G_M_NONE, G_M_NONE
-        },
-        "campaign",                         { "", "", "" },
-        "make your way through a maze of monsters", { "", "", "" },
-    },
-    {
-        G_DEATHMATCH,   G_M_NONE,
-        {
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE,
-            G_M_NONE, G_M_NONE, G_M_NONE
-        },
-        "deathmatch",                       { "", "", "" },
-        "shoot to kill and earn points by fragging", { "", "", "" },
-    },
-    {
-        G_CAPTURE,      G_M_TEAM,
-        {
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1,
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP2,
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP3
-        },
-        "capture-the-flag",                 { "return", "defend", "protect" },
-        "take the enemy flag and return it to the base", { "dropped flags must be carried back to base", "dropped flags must be defended until they reset", "protect the flag and hold the enemy flag to score" },
-    },
-    {
-        G_DEFEND,       G_M_TEAM,
-        {
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2,
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2,
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2,
-            G_M_NONE
-        },
-        "defend-the-flag",                  { "quick", "conquer", "" },
-        "secure and defend flags to earn points", { "flags secure quicker than normal", "match ends when all flags are secured", "" },
-    },
-    {
-        G_BOMBER,       G_M_NONE,
-        {
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2,
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1,
-            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP2,
-            G_M_NONE
-        },
-        "bomber-ball",                      { "basket", "hold", "" },
-        "take the bomb to the enemy goal before it blows up", { "the bomb may be thrown into the goal", "hold the bomb as long as possible to score points", "" },
-    },
-    {
-        G_TRIAL,        G_M_NONE,
-        {
-            G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE,
-            G_M_NONE, G_M_NONE, G_M_NONE
-        },
-        "time-trial",                       { "", "", "" },
-        "compete for the fastest time completing a lap", { "", "", "" },
-    },
-};
-mutstypes mutstype[] = {
-    {
-        G_M_MULTI,      G_M_MULTI,          G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "multi",
-        "four teams fight to determine the winning side"
-    },
-    {
-        G_M_TEAM,       G_M_TEAM,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "team",
-        "two teams fight to determine the winning side"
-    },
-    {
-        G_M_INSTA,      G_M_INSTA,          G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "instagib",
-        "one shot, one kill"
-    },
-    {
-        G_M_MEDIEVAL,   G_M_MEDIEVAL,       G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_MEDIEVAL|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "medieval",
-        "everyone spawns only with swords"
-    },
-    {
-        G_M_BALLISTIC,  G_M_BALLISTIC,      G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "ballistic",
-        "everyone spawns only with rockets"
-    },
-    {
-        G_M_DUEL,       G_M_DUEL,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "duel",
-        "one on one battles to determine the winner"
-    },
-    {
-        G_M_SURVIVOR,   G_M_SURVIVOR,       G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "survivor",
-        "everyone battles to determine the winner"
-    },
-    {
-        G_M_ARENA,      G_M_ARENA,          G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "arena",
-        "spawn with a choice of weaponry"
-    },
-    {
-        G_M_ONSLAUGHT,  G_M_ONSLAUGHT,      G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "onslaught",
-        "waves of enemies fill the battle arena"
-    },
-    {
-        G_M_JETPACK,    G_M_JETPACK,        G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "jetpack",
-        "everyone comes equipped with a jetpack"
-    },
-    {
-        G_M_VAMPIRE,    G_M_VAMPIRE,        G_M_MULTI|G_M_TEAM|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "vampire",
-        "deal damage to regenerate health"
-    },
-    {
-        G_M_EXPERT,     G_M_EXPERT,         G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "expert",
-        "headshot damage only"
-    },
-    {
-        G_M_RESIZE,     G_M_RESIZE,         G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "resize",
-        "everyone changes size depending on their health"
-    },
-    {
-        G_M_GSP1,       G_M_GSP1,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "gsp1",
-        ""
-    },
-    {
-        G_M_GSP2,       G_M_GSP2,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "gsp2",
-        ""
-    },
-    {
-        G_M_GSP3,       G_M_GSP3,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
-        "gsp3",
-        ""
-    },
-};
-#else
-extern gametypes gametype[];
-extern mutstypes mutstype[];
-#endif
-
-#define m_game(a)           (a > -1 && a < G_MAX)
-#define m_check(a,b,c,d)    ((!a || (a < 0 ? !((0-a)&(1<<(c-G_PLAY))) : a&(1<<(c-G_PLAY)))) && (!b || (b < 0 ? !((0-b)&d) : b&d)))
-#define m_local(a)          (a == G_DEMO)
-
-#define m_demo(a)           (a == G_DEMO)
-#define m_edit(a)           (a == G_EDITMODE)
-#define m_campaign(a)       (a == G_CAMPAIGN)
-#define m_dm(a)             (a == G_DEATHMATCH)
-#define m_capture(a)        (a == G_CAPTURE)
-#define m_defend(a)         (a == G_DEFEND)
-#define m_bomber(a)         (a == G_BOMBER)
-#define m_trial(a)          (a == G_TRIAL)
-
-#define m_play(a)           (a >= G_PLAY)
-#define m_affinity(a)       (m_capture(a) || m_defend(a) || m_bomber(a))
-#define m_fight(a)          (a >= G_FIGHT)
-
-#define m_implied(a,b)      (gametype[a].implied|((b&G_M_MULTI) || (a == G_BOMBER && !((b|gametype[a].implied)&G_M_GSP2)) ? G_M_TEAM : G_M_NONE))
-#define m_doimply(a,b,c)    (gametype[a].implied|mutstype[c].implied|((b&G_M_MULTI) || (a == G_BOMBER && !((b|gametype[a].implied|mutstype[c].implied)&G_M_GSP2)) ? G_M_TEAM : G_M_NONE))
-
-#define m_multi(a,b)        ((b&G_M_MULTI) || (m_implied(a,b)&G_M_MULTI))
-#define m_team(a,b)         ((b&G_M_TEAM) || (m_implied(a,b)&G_M_TEAM))
-#define m_insta(a,b)        ((b&G_M_INSTA) || (m_implied(a,b)&G_M_INSTA))
-#define m_medieval(a,b)     ((b&G_M_MEDIEVAL) || (m_implied(a,b)&G_M_MEDIEVAL))
-#define m_ballistic(a,b)    ((b&G_M_BALLISTIC) || (m_implied(a,b)&G_M_BALLISTIC))
-#define m_duel(a,b)         ((b&G_M_DUEL) || (m_implied(a,b)&G_M_DUEL))
-#define m_survivor(a,b)     ((b&G_M_SURVIVOR) || (m_implied(a,b)&G_M_SURVIVOR))
-#define m_arena(a,b)        ((b&G_M_ARENA) || (m_implied(a,b)&G_M_ARENA))
-#define m_onslaught(a,b)    ((b&G_M_ONSLAUGHT) || (m_implied(a,b)&G_M_ONSLAUGHT))
-#define m_jetpack(a,b)      ((b&G_M_JETPACK) || (m_implied(a,b)&G_M_JETPACK))
-#define m_vampire(a,b)      ((b&G_M_VAMPIRE) || (m_implied(a,b)&G_M_VAMPIRE))
-#define m_expert(a,b)       ((b&G_M_EXPERT) || (m_implied(a,b)&G_M_EXPERT))
-#define m_resize(a,b)       ((b&G_M_RESIZE) || (m_implied(a,b)&G_M_RESIZE))
-
-#define m_gsp1(a,b)         ((b&G_M_GSP1) || (m_implied(a,b)&G_M_GSP1))
-#define m_gsp2(a,b)         ((b&G_M_GSP2) || (m_implied(a,b)&G_M_GSP2))
-#define m_gsp3(a,b)         ((b&G_M_GSP3) || (m_implied(a,b)&G_M_GSP3))
-#define m_gsp(a,b)          (m_gsp1(a,b) || m_gsp2(a,b) || m_gsp3(a,b))
-
-#define m_limited(a,b)      (m_insta(a, b) || m_medieval(a, b) || m_ballistic(a, b))
-#define m_special(a,b)      (m_arena(a, b) || m_insta(a, b) || m_medieval(a, b) || m_ballistic(a, b))
-#define m_duke(a,b)         (m_duel(a, b) || m_survivor(a, b))
-#define m_regen(a,b)        (!m_duke(a, b) && !m_insta(a, b))
-#define m_enemies(a,b)      (m_campaign(a) || m_onslaught(a, b))
-#define m_scores(a)         (m_dm(a))
-#define m_checkpoint(a)     (m_campaign(a) || m_trial(a))
-#define m_sweaps(a,b)       (m_medieval(a, b) || m_ballistic(a, b) || m_arena(a, b))
-
-#define m_weapon(a,b)       (m_arena(a,b) ? -WEAP_ITEM : (m_medieval(a,b) ? WEAP_SWORD : (m_ballistic(a,b) ? WEAP_ROCKET : (m_insta(a,b) ? GAME(instaweapon) : (m_trial(a) ? GAME(trialweapon) : GAME(spawnweapon))))))
-#define m_delay(a,b)        (m_play(a) && !m_duke(a,b) ? (m_trial(a) ? GAME(trialdelay) : (m_bomber(a) ? GAME(bomberdelay) : (m_insta(a, b) ? GAME(instadelay) : GAME(spawndelay)))) : 0)
-#define m_protect(a,b)      (m_duke(a,b) ? GAME(duelprotect) : (m_insta(a, b) ? GAME(instaprotect) : GAME(spawnprotect)))
-#define m_noitems(a,b)      (m_trial(a) || GAME(itemsallowed) < (m_limited(a,b) ? 2 : 1))
-#define m_health(a,b)       (m_insta(a,b) ? 1 : GAME(spawnhealth))
-
-#define w_reload(w1,w2)     (w1 != WEAP_MELEE && ((isweap(w2) ? w1 == w2 : w1 < -w2) || (isweap(w1) && WEAP(w1, reloads))))
-#define w_carry(w1,w2)      (w1 > WEAP_MELEE && (isweap(w2) ? w1 != w2 : w1 >= -w2) && (isweap(w1) && WEAP(w1, carried)))
-#define w_attr(a,w1,w2)     (m_edit(a) || (w1 >= WEAP_OFFSET && w1 != w2) ? w1 : (w2 == WEAP_GRENADE ? WEAP_ROCKET : WEAP_GRENADE))
-#define w_spawn(weap)       int(ceilf(GAME(itemspawntime)*WEAP(weap, frequency)))
-
-#define mapshrink(a,b,c) if((a) && (b) && (c) && *(c)) \
-{ \
-    char *p = shrinklist(b, c, 1); \
-    if(p) \
-    { \
-        DELETEA(b); \
-        b = p; \
-    } \
-}
-
-#define mapcull(a,b,c,d) \
-{ \
-    mapshrink(m_multi(b, c) && (m_capture(b) || (m_bomber(b) && !m_gsp2(b, c))), a, GAME(multimaps)); \
-    mapshrink(m_duel(b, c), a, GAME(duelmaps)); \
-    mapshrink(m_jetpack(b, c), a, GAME(jetpackmaps)); \
-    if(d > 0 && GAME(mapsfilter) >= 2 && m_fight(b) && !m_duel(b, c)) \
-    { \
-        mapshrink(GAME(smallmapmax) && d <= GAME(smallmapmax), a, GAME(smallmaps)) \
-        else mapshrink(GAME(mediummapmax) && d <= GAME(mediummapmax), a, GAME(mediummaps)) \
-        else mapshrink(GAME(mediummapmax) && d > GAME(mediummapmax), a, GAME(largemaps)) \
-    } \
-}
-
-#define maplist(a,b,c,d) \
-{ \
-    if(m_campaign(b)) a = newstring(GAME(campaignmaps)); \
-    else if(m_capture(b)) a = newstring(GAME(capturemaps)); \
-    else if(m_defend(b)) a = newstring(GAME(defendmaps)); \
-    else if(m_bomber(b)) a = newstring(m_gsp2(b, c) ? GAME(holdmaps) : GAME(bombermaps)); \
-    else if(m_trial(b)) a = newstring(GAME(trialmaps)); \
-    else if(m_fight(b)) a = newstring(GAME(mainmaps)); \
-    else a = newstring(GAME(allowmaps)); \
-    if(GAME(mapsfilter)) mapcull(a, b, c, d); \
-}
+enum
+{
+    G_DEMO = 0, G_EDITMODE, G_CAMPAIGN, G_DEATHMATCH, G_CAPTURE, G_DEFEND, G_BOMBER, G_TRIAL, G_MAX,
+    G_START = G_EDITMODE, G_PLAY = G_CAMPAIGN, G_FIGHT = G_DEATHMATCH, G_RAND = G_BOMBER-G_DEATHMATCH+1,
+    G_NEVER = (1<<G_DEMO)|(1<<G_EDITMODE),
+    G_LIMIT = (1<<G_DEATHMATCH)|(1<<G_CAPTURE)|(1<<G_DEFEND)|(1<<G_BOMBER),
+    G_ALL = (1<<G_DEMO)|(1<<G_EDITMODE)|(1<<G_CAMPAIGN)|(1<<G_DEATHMATCH)|(1<<G_CAPTURE)|(1<<G_DEFEND)|(1<<G_BOMBER)|(1<<G_TRIAL)
+};
+enum
+{
+    G_M_NONE = 0,
+    G_M_MULTI = 1<<0, G_M_TEAM = 1<<1, G_M_INSTA = 1<<2, G_M_MEDIEVAL = 1<<3, G_M_BALLISTIC = 1<<4,
+    G_M_DUEL = 1<<5, G_M_SURVIVOR = 1<<6, G_M_ARENA = 1<<7, G_M_ONSLAUGHT = 1<<8,
+    G_M_JETPACK = 1<<9, G_M_VAMPIRE = 1<<10, G_M_EXPERT = 1<<11, G_M_RESIZE = 1<<12,
+    G_M_GSP1 = 1<<13, G_M_GSP2 = 1<<14, G_M_GSP3 = 1<<15,
+    G_M_ALL = G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+    G_M_FILTER = G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_SURVIVOR|G_M_ARENA|G_M_JETPACK|G_M_VAMPIRE|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+    G_M_GSN = 3, G_M_GSP = 13, G_M_NUM = 16, G_M_IGN = 2 // instagib number for instagibfilter
+};
+
+struct gametypes
+{
+    int type,           implied,
+        mutators[G_M_GSN+1];
+    const char *name,                       *gsp[G_M_GSN],
+        *desc,                              *gsd[G_M_GSN];
+};
+struct mutstypes
+{
+    int type,           implied,            mutators;
+    const char *name,
+        *desc;
+};
+#ifdef GAMESERVER
+gametypes gametype[] = {
+    {
+        G_DEMO,         G_M_NONE,
+        {
+            G_M_NONE, G_M_NONE, G_M_NONE, G_M_NONE
+        },
+        "demo",                             { "", "", "" },
+        "",                                 { "", "", "" },
+    },
+    {
+        G_EDITMODE,     G_M_NONE,
+        {
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE,
+            G_M_NONE, G_M_NONE, G_M_NONE
+        },
+        "editing",                          { "", "", "" },
+        "create and edit existing maps",    { "", "", "" },
+    },
+    {
+        G_CAMPAIGN,     G_M_TEAM,
+        {
+            G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE,
+            G_M_NONE, G_M_NONE, G_M_NONE
+        },
+        "campaign",                         { "", "", "" },
+        "make your way through a maze of monsters", { "", "", "" },
+    },
+    {
+        G_DEATHMATCH,   G_M_NONE,
+        {
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE,
+            G_M_NONE, G_M_NONE, G_M_NONE
+        },
+        "deathmatch",                       { "", "", "" },
+        "shoot to kill and earn points by fragging", { "", "", "" },
+    },
+    {
+        G_CAPTURE,      G_M_TEAM,
+        {
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1,
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP2,
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP3
+        },
+        "capture-the-flag",                 { "return", "defend", "protect" },
+        "take the enemy flag and return it to the base", { "dropped flags must be carried back to base", "dropped flags must be defended until they reset", "protect the flag and hold the enemy flag to score" },
+    },
+    {
+        G_DEFEND,       G_M_TEAM,
+        {
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2,
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2,
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2,
+            G_M_NONE
+        },
+        "defend-the-flag",                  { "quick", "conquer", "" },
+        "secure and defend flags to earn points", { "flags secure quicker than normal", "match ends when all flags are secured", "" },
+    },
+    {
+        G_BOMBER,       G_M_NONE,
+        {
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2,
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1,
+            G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP2,
+            G_M_NONE
+        },
+        "bomber-ball",                      { "basket", "hold", "" },
+        "take the bomb to the enemy goal before it blows up", { "the bomb may be thrown into the goal", "hold the bomb as long as possible to score points", "" },
+    },
+    {
+        G_TRIAL,        G_M_NONE,
+        {
+            G_M_INSTA|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE,
+            G_M_NONE, G_M_NONE, G_M_NONE
+        },
+        "time-trial",                       { "", "", "" },
+        "compete for the fastest time completing a lap", { "", "", "" },
+    },
+};
+mutstypes mutstype[] = {
+    {
+        G_M_MULTI,      G_M_MULTI,          G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "multi",
+        "four teams fight to determine the winning side"
+    },
+    {
+        G_M_TEAM,       G_M_TEAM,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "team",
+        "two teams fight to determine the winning side"
+    },
+    {
+        G_M_INSTA,      G_M_INSTA,          G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "instagib",
+        "one shot, one kill"
+    },
+    {
+        G_M_MEDIEVAL,   G_M_MEDIEVAL,       G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_MEDIEVAL|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "medieval",
+        "everyone spawns only with swords"
+    },
+    {
+        G_M_BALLISTIC,  G_M_BALLISTIC,      G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "ballistic",
+        "everyone spawns only with rockets"
+    },
+    {
+        G_M_DUEL,       G_M_DUEL,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "duel",
+        "one on one battles to determine the winner"
+    },
+    {
+        G_M_SURVIVOR,   G_M_SURVIVOR,       G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_SURVIVOR|G_M_ARENA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "survivor",
+        "everyone battles to determine the winner"
+    },
+    {
+        G_M_ARENA,      G_M_ARENA,          G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "arena",
+        "spawn with a choice of weaponry"
+    },
+    {
+        G_M_ONSLAUGHT,  G_M_ONSLAUGHT,      G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "onslaught",
+        "waves of enemies fill the battle arena"
+    },
+    {
+        G_M_JETPACK,    G_M_JETPACK,        G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "jetpack",
+        "everyone comes equipped with a jetpack"
+    },
+    {
+        G_M_VAMPIRE,    G_M_VAMPIRE,        G_M_MULTI|G_M_TEAM|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "vampire",
+        "deal damage to regenerate health"
+    },
+    {
+        G_M_EXPERT,     G_M_EXPERT,         G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "expert",
+        "headshot damage only"
+    },
+    {
+        G_M_RESIZE,     G_M_RESIZE,         G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "resize",
+        "everyone changes size depending on their health"
+    },
+    {
+        G_M_GSP1,       G_M_GSP1,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "gsp1",
+        ""
+    },
+    {
+        G_M_GSP2,       G_M_GSP2,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "gsp2",
+        ""
+    },
+    {
+        G_M_GSP3,       G_M_GSP3,           G_M_MULTI|G_M_TEAM|G_M_INSTA|G_M_MEDIEVAL|G_M_BALLISTIC|G_M_DUEL|G_M_SURVIVOR|G_M_ARENA|G_M_ONSLAUGHT|G_M_JETPACK|G_M_VAMPIRE|G_M_EXPERT|G_M_RESIZE|G_M_GSP1|G_M_GSP2|G_M_GSP3,
+        "gsp3",
+        ""
+    },
+};
+#else
+extern gametypes gametype[];
+extern mutstypes mutstype[];
+#endif
+
+#define m_game(a)           (a > -1 && a < G_MAX)
+#define m_check(a,b,c,d)    ((!a || (a < 0 ? !((0-a)&(1<<(c-G_PLAY))) : a&(1<<(c-G_PLAY)))) && (!b || (b < 0 ? !((0-b)&d) : b&d)))
+#define m_local(a)          (a == G_DEMO)
+
+#define m_demo(a)           (a == G_DEMO)
+#define m_edit(a)           (a == G_EDITMODE)
+#define m_campaign(a)       (a == G_CAMPAIGN)
+#define m_dm(a)             (a == G_DEATHMATCH)
+#define m_capture(a)        (a == G_CAPTURE)
+#define m_defend(a)         (a == G_DEFEND)
+#define m_bomber(a)         (a == G_BOMBER)
+#define m_trial(a)          (a == G_TRIAL)
+
+#define m_play(a)           (a >= G_PLAY)
+#define m_affinity(a)       (m_capture(a) || m_defend(a) || m_bomber(a))
+#define m_fight(a)          (a >= G_FIGHT)
+
+#define m_implied(a,b)      (gametype[a].implied|((b&G_M_MULTI) || (a == G_BOMBER && !((b|gametype[a].implied)&G_M_GSP2)) ? G_M_TEAM : G_M_NONE))
+#define m_doimply(a,b,c)    (gametype[a].implied|mutstype[c].implied|((b&G_M_MULTI) || (a == G_BOMBER && !((b|gametype[a].implied|mutstype[c].implied)&G_M_GSP2)) ? G_M_TEAM : G_M_NONE))
+
+#define m_multi(a,b)        ((b&G_M_MULTI) || (m_implied(a,b)&G_M_MULTI))
+#define m_team(a,b)         ((b&G_M_TEAM) || (m_implied(a,b)&G_M_TEAM))
+#define m_insta(a,b)        ((b&G_M_INSTA) || (m_implied(a,b)&G_M_INSTA))
+#define m_medieval(a,b)     ((b&G_M_MEDIEVAL) || (m_implied(a,b)&G_M_MEDIEVAL))
+#define m_ballistic(a,b)    ((b&G_M_BALLISTIC) || (m_implied(a,b)&G_M_BALLISTIC))
+#define m_duel(a,b)         ((b&G_M_DUEL) || (m_implied(a,b)&G_M_DUEL))
+#define m_survivor(a,b)     ((b&G_M_SURVIVOR) || (m_implied(a,b)&G_M_SURVIVOR))
+#define m_arena(a,b)        ((b&G_M_ARENA) || (m_implied(a,b)&G_M_ARENA))
+#define m_onslaught(a,b)    ((b&G_M_ONSLAUGHT) || (m_implied(a,b)&G_M_ONSLAUGHT))
+#define m_jetpack(a,b)      ((b&G_M_JETPACK) || (m_implied(a,b)&G_M_JETPACK))
+#define m_vampire(a,b)      ((b&G_M_VAMPIRE) || (m_implied(a,b)&G_M_VAMPIRE))
+#define m_expert(a,b)       ((b&G_M_EXPERT) || (m_implied(a,b)&G_M_EXPERT))
+#define m_resize(a,b)       ((b&G_M_RESIZE) || (m_implied(a,b)&G_M_RESIZE))
+
+#define m_gsp1(a,b)         ((b&G_M_GSP1) || (m_implied(a,b)&G_M_GSP1))
+#define m_gsp2(a,b)         ((b&G_M_GSP2) || (m_implied(a,b)&G_M_GSP2))
+#define m_gsp3(a,b)         ((b&G_M_GSP3) || (m_implied(a,b)&G_M_GSP3))
+#define m_gsp(a,b)          (m_gsp1(a,b) || m_gsp2(a,b) || m_gsp3(a,b))
+
+#define m_limited(a,b)      (m_insta(a, b) || m_medieval(a, b) || m_ballistic(a, b))
+#define m_special(a,b)      (m_arena(a, b) || m_insta(a, b) || m_medieval(a, b) || m_ballistic(a, b))
+#define m_duke(a,b)         (m_duel(a, b) || m_survivor(a, b))
+#define m_regen(a,b)        (!m_duke(a, b) && !m_insta(a, b))
+#define m_enemies(a,b)      (m_campaign(a) || m_onslaught(a, b))
+#define m_scores(a)         (m_dm(a))
+#define m_checkpoint(a)     (m_campaign(a) || m_trial(a))
+#define m_sweaps(a,b)       (m_medieval(a, b) || m_ballistic(a, b) || m_arena(a, b))
+
+#define m_weapon(a,b)       (m_arena(a,b) ? -WEAP_ITEM : (m_medieval(a,b) ? WEAP_SWORD : (m_ballistic(a,b) ? WEAP_ROCKET : (m_insta(a,b) ? GAME(instaweapon) : (m_trial(a) ? GAME(trialweapon) : GAME(spawnweapon))))))
+#define m_delay(a,b)        (m_play(a) && !m_duke(a,b) ? (m_trial(a) ? GAME(trialdelay) : (m_bomber(a) ? GAME(bomberdelay) : (m_insta(a, b) ? GAME(instadelay) : GAME(spawndelay)))) : 0)
+#define m_protect(a,b)      (m_duke(a,b) ? GAME(duelprotect) : (m_insta(a, b) ? GAME(instaprotect) : GAME(spawnprotect)))
+#define m_noitems(a,b)      (m_trial(a) || GAME(itemsallowed) < (m_limited(a,b) ? 2 : 1))
+#define m_health(a,b)       (m_insta(a,b) ? 1 : GAME(spawnhealth))
+
+#define w_reload(w1,w2)     (w1 != WEAP_MELEE && ((isweap(w2) ? w1 == w2 : w1 < -w2) || (isweap(w1) && WEAP(w1, reloads))))
+#define w_carry(w1,w2)      (w1 > WEAP_MELEE && (isweap(w2) ? w1 != w2 : w1 >= -w2) && (isweap(w1) && WEAP(w1, carried)))
+#define w_attr(a,w1,w2)     (m_edit(a) || (w1 >= WEAP_OFFSET && w1 != w2) ? w1 : (w2 == WEAP_GRENADE ? WEAP_ROCKET : WEAP_GRENADE))
+#define w_spawn(weap)       int(ceilf(GAME(itemspawntime)*WEAP(weap, frequency)))
+
+#define mapshrink(a,b,c) if((a) && (b) && (c) && *(c)) \
+{ \
+    char *p = shrinklist(b, c, 1); \
+    if(p) \
+    { \
+        DELETEA(b); \
+        b = p; \
+    } \
+}
+
+#define mapcull(a,b,c,d) \
+{ \
+    mapshrink(m_multi(b, c) && (m_capture(b) || (m_bomber(b) && !m_gsp2(b, c))), a, GAME(multimaps)); \
+    mapshrink(m_duel(b, c), a, GAME(duelmaps)); \
+    mapshrink(m_jetpack(b, c), a, GAME(jetpackmaps)); \
+    if(d > 0 && GAME(mapsfilter) >= 2 && m_fight(b) && !m_duel(b, c)) \
+    { \
+        mapshrink(GAME(smallmapmax) && d <= GAME(smallmapmax), a, GAME(smallmaps)) \
+        else mapshrink(GAME(mediummapmax) && d <= GAME(mediummapmax), a, GAME(mediummaps)) \
+        else mapshrink(GAME(mediummapmax) && d > GAME(mediummapmax), a, GAME(largemaps)) \
+    } \
+}
+
+#define maplist(a,b,c,d) \
+{ \
+    if(m_campaign(b)) a = newstring(GAME(campaignmaps)); \
+    else if(m_capture(b)) a = newstring(GAME(capturemaps)); \
+    else if(m_defend(b)) a = newstring(GAME(defendmaps)); \
+    else if(m_bomber(b)) a = newstring(m_gsp2(b, c) ? GAME(holdmaps) : GAME(bombermaps)); \
+    else if(m_trial(b)) a = newstring(GAME(trialmaps)); \
+    else if(m_fight(b)) a = newstring(GAME(mainmaps)); \
+    else a = newstring(GAME(allowmaps)); \
+    if(GAME(mapsfilter)) mapcull(a, b, c, d); \
+}
+
+#ifdef GAMESERVER
+SVAR(0, modename, "demo editing campaign deathmatch capture-the-flag defend-the-flag bomber-ball time-trial");
+SVAR(0, modeidxname, "demo editing campaign deathmatch capture defend bomber trial");
+VAR(0, modeidxdemo, -1, G_DEMO, 1);
+VAR(0, modeidxediting, -1, G_EDITMODE, 1);
+VAR(0, modeidxcampaign, -1, G_CAMPAIGN, 1);
+VAR(0, modeidxdeathmatch, -1, G_DEATHMATCH, 1);
+VAR(0, modeidxcapture, -1, G_CAPTURE, 1);
+VAR(0, modeidxdefend, -1, G_DEFEND, 1);
+VAR(0, modeidxbomber, -1, G_BOMBER, 1);
+VAR(0, modeidxtrial, -1, G_TRIAL, 1);
+VAR(0, modeidxstart, -1, G_START, 1);
+VAR(0, modeidxplay, -1, G_PLAY, 1);
+VAR(0, modeidxfight, -1, G_FIGHT, 1);
+VAR(0, modeidxrand, -1, G_RAND, 1);
+VAR(0, modeidxnever, -1, G_NEVER, 1);
+VAR(0, modeidxlimit, -1, G_LIMIT, 1);
+VAR(0, modeidxall, -1, G_ALL, 1);
+VAR(0, modeidxnum, -1, G_MAX, 1);
+SVAR(0, mutsname, "multi teamplay instagib medieval ballistic duel survivor arena onslaught jetpack vampire expert resize");
+SVAR(0, mutsidxname, "multi team instagib medieval ballistic duel survivor arena onslaught jetpack vampire expert resize");
+VAR(0, mutsidxmulti, -1, G_M_MULTI, 1);
+VAR(0, mutsidxteam, -1, G_M_TEAM, 1);
+VAR(0, mutsidxinstagib, -1, G_M_INSTA, 1);
+VAR(0, mutsidxmedieval, -1, G_M_MEDIEVAL, 1);
+VAR(0, mutsidxballistic, -1, G_M_BALLISTIC, 1);
+VAR(0, mutsidxduel, -1, G_M_DUEL, 1);
+VAR(0, mutsidxsurvivor, -1, G_M_SURVIVOR, 1);
+VAR(0, mutsidxarena, -1, G_M_ARENA, 1);
+VAR(0, mutsidxonslaught, -1, G_M_ONSLAUGHT, 1);
+VAR(0, mutsidxjetpack, -1, G_M_JETPACK, 1);
+VAR(0, mutsidxvampire, -1, G_M_VAMPIRE, 1);
+VAR(0, mutsidxexpert, -1, G_M_EXPERT, 1);
+VAR(0, mutsidxresize, -1, G_M_RESIZE, 1);
+VAR(0, mutsidxgsp1, -1, G_M_GSP1, 1);
+VAR(0, mutsidxgsp2, -1, G_M_GSP2, 1);
+VAR(0, mutsidxgsp3, -1, G_M_GSP3, 1);
+VAR(0, mutsidxall, -1, G_M_ALL, 1);
+VAR(0, mutsidxfilter, -1, G_M_FILTER, 1);
+VAR(0, mutsidxgsn, -1, G_M_GSN, 1);
+VAR(0, mutsidxgsp, -1, G_M_GSP, 1);
+VAR(0, mutsidxnum, -1, G_M_NUM, 1);
+#endif
diff --git a/src/game/hud.cpp b/src/game/hud.cpp
index 687ce08..fbd90b0 100644
--- a/src/game/hud.cpp
+++ b/src/game/hud.cpp
@@ -1150,6 +1150,11 @@ namespace hud
             SEARCHBINDCACHE(speconkey)("spectator 0", 1);
             pushfont("little");
             ty += draw_textx("Press \fs\fc%s\fS to join the game", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, speconkey);
+            if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators) && shownotices >= 2)
+            {
+                SEARCHBINDCACHE(teamkey)("showgui team", 0);
+                ty += draw_textx("Press \fs\fc%s\fS to join a team", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, teamkey);
+            }
             if(!m_edit(game::gamemode) && shownotices >= 2)
             {
                 SEARCHBINDCACHE(specmodekey)("specmodeswitch", 1);
diff --git a/src/game/server.cpp b/src/game/server.cpp
index e78e43f..2beeff7 100644
--- a/src/game/server.cpp
+++ b/src/game/server.cpp
@@ -801,7 +801,11 @@ namespace server
         changemap();
     }
 
-    void start() { cleanup(true); }
+    void start()
+    {
+        cleanup(true);
+    }
+
     void shutdown()
     {
         srvmsgft(-1, CON_EVENT, "\fyserver shutdown in progress..");
@@ -852,7 +856,8 @@ namespace server
         switch(type)
         {
             case PRIV_ADMIN: return "admin";
-            case PRIV_MASTER: case PRIV_AUTH: return "master";
+            case PRIV_AUTH: return "auth";
+            case PRIV_MASTER: return "master";
             case PRIV_USER: return "user";
             case PRIV_MAX: return "local";
             default: return "alone";
@@ -918,6 +923,7 @@ namespace server
             case 0: return RE_VERSION;
             case 1: return GAMEVERSION;
             case 2: case 3: return version[n%2];
+            default: break;
         }
         return 0;
     }
@@ -975,7 +981,6 @@ namespace server
         return mdname;
     }
     ICOMMAND(0, modedesc, "iii", (int *g, int *m, int *c), result(modedesc(*g, *m, *c)));
-    VAR(0, maxmodes, G_MAX-1, G_MAX-1, -G_MAX-1);
 
     const char *mutsdesc(int mode, int muts, int type)
     {
@@ -1000,7 +1005,6 @@ namespace server
         return mtname;
     }
     ICOMMAND(0, mutsdesc, "iii", (int *g, int *m, int *c), result(mutsdesc(*g, *m, *c)));
-    VAR(0, maxmuts, G_M_NUM, G_M_NUM, -G_M_NUM);
 
     void changemode(int &mode, int &muts)
     {
@@ -1832,6 +1836,11 @@ namespace server
     {
         setpause(false);
         if(demorecord) enddemorecord();
+        if(sv_botoffset != 0)
+        {
+            setvar("sv_botoffset", 0, true);
+            sendf(-1, 1, "ri2ss", N_COMMAND, -1, "botoffset", "0");
+        }
         if(GAME(resetmmonend) >= 2) { mastermode = MM_OPEN; resetallows(); }
         if(GAME(resetvarsonend) >= 2) resetgamevars(true);
         if(GAME(resetbansonend) >= 2) resetbans();
@@ -1912,60 +1921,77 @@ namespace server
 
     void vote(const char *reqmap, int &reqmode, int &reqmuts, int sender)
     {
-        clientinfo *ci = (clientinfo *)getinfo(sender); modecheck(reqmode, reqmuts);
+        clientinfo *ci = (clientinfo *)getinfo(sender);
+        modecheck(reqmode, reqmuts);
         if(!ci || !m_game(reqmode) || !reqmap || !*reqmap) return;
-        if(GAME(modelock) == 5 && GAME(mapslock) == 5 && !haspriv(ci, PRIV_MAX, "vote for a new game")) return;
-        else switch(GAME(votelock))
-        {
-            case 1: case 2: if(!m_edit(reqmode) && !strcmp(reqmap, smapname) && !haspriv(ci, GAME(votelock) == 1 ? PRIV_MASTER : PRIV_ADMIN, "vote for the same map again")) return; break;
-            case 3: case 4: if(!haspriv(ci, GAME(votelock) == 3 ? PRIV_MASTER : PRIV_ADMIN, "vote for a new game")) return; break;
-            case 5: if(!haspriv(ci, PRIV_MAX, "vote for a new game")) return; break;
-        }
-        bool hasveto = haspriv(ci, PRIV_MASTER) && (mastermode >= MM_VETO || !numclients(ci->clientnum));
+        bool hasvote = false, hasveto = haspriv(ci, PRIV_MASTER) && (mastermode >= MM_VETO || !numclients(ci->clientnum));
         if(!hasveto)
         {
             if(ci->lastvote && totalmillis-ci->lastvote <= GAME(votewait)) return;
             if(ci->modevote == reqmode && ci->mutsvote == reqmuts && !strcmp(ci->mapvote, reqmap)) return;
         }
-        if(m_local(reqmode) && !ci->local)
-        {
-            srvmsgft(ci->clientnum, CON_EVENT, "\fraccess denied, you must be a local client to start a %s game", gametype[reqmode].name);
-            return;
-        }
-        switch(GAME(modelock))
+        loopv(clients)
         {
-            case 1: case 2: if(!haspriv(ci, GAME(modelock) == 1 ? PRIV_MASTER : PRIV_ADMIN, "change game modes")) return; break;
-            case 3: case 4: if((!((1<<reqmode)&GAME(modelockfilter)) || !mutscmp(reqmuts, GAME(mutslockfilter))) && !haspriv(ci, GAME(modelock) == 3 ? PRIV_MASTER : PRIV_ADMIN, "change to a locked game mode")) return; break;
-            case 5: if(!haspriv(ci, PRIV_MAX, "change game modes")) return; break;
-            case 0: default: break;
+            clientinfo *oi = clients[i];
+            if(oi->state.aitype > AI_NONE || !oi->mapvote[0] || ci == oi) continue;
+            if(!strcmp(oi->mapvote, reqmap) && oi->modevote == reqmode && oi->mutsvote == reqmuts)
+            {
+                hasvote = true;
+                break;
+            }
         }
-        if(reqmode != G_EDITMODE && GAME(mapslock))
+        if(!hasvote)
         {
-            char *list = NULL;
-            switch(GAME(mapslock))
+            if(GAME(modelock) == 7 && GAME(mapslock) == 7 && !haspriv(ci, PRIV_MAX, "vote for a new game")) return;
+            else switch(GAME(votelock))
             {
-                case 1: case 2:
-                {
-                    list = newstring(GAME(allowmaps));
-                    mapcull(list, reqmode, reqmuts, numclients());
-                    break;
-                }
-                case 3: case 4:
-                {
-                    maplist(list, reqmode, reqmuts, numclients());
-                    break;
-                }
-                case 5: if(!haspriv(ci, PRIV_MAX, "select a map to play")) return; break;
+                case 1: case 2: case 3: if(!m_edit(reqmode) && !strcmp(reqmap, smapname) && !haspriv(ci, GAME(votelock)-1+PRIV_MASTER, "vote for the same map again")) return; break;
+                case 4: case 5: case 6: if(!haspriv(ci, GAME(votelock)-4+PRIV_MASTER, "vote for a new game")) return; break;
+                case 7: if(!haspriv(ci, PRIV_MAX, "vote for a new game")) return; break;
                 case 0: default: break;
             }
-            if(list)
+            if(m_local(reqmode) && !ci->local)
             {
-                if(listincludes(list, reqmap, strlen(reqmap)) < 0 && !haspriv(ci, GAME(mapslock)%2 ? PRIV_MASTER : PRIV_ADMIN, "select maps not in the rotation"))
+                srvmsgft(ci->clientnum, CON_EVENT, "\fraccess denied, you must be a local client to start a %s game", gametype[reqmode].name);
+                return;
+            }
+            switch(GAME(modelock))
+            {
+                case 1: case 2: case 3: if(!haspriv(ci, GAME(modelock)-1+PRIV_MASTER, "change game modes")) return; break;
+                case 4: case 5: case 6: if((!((1<<reqmode)&GAME(modelockfilter)) || !mutscmp(reqmuts, GAME(mutslockfilter))) && !haspriv(ci, GAME(modelock)-4+PRIV_MASTER, "change to a locked game mode")) return; break;
+                case 7: if(!haspriv(ci, PRIV_MAX, "change game modes")) return; break;
+                case 0: default: break;
+            }
+            if(reqmode != G_EDITMODE && GAME(mapslock))
+            {
+                char *list = NULL;
+                int level = GAME(mapslock);
+                switch(GAME(mapslock))
                 {
+                    case 1: case 2: case 3:
+                    {
+                        list = newstring(GAME(allowmaps));
+                        mapcull(list, reqmode, reqmuts, numclients());
+                        break;
+                    }
+                    case 4: case 5: case 6:
+                    {
+                        level -= 3;
+                        maplist(list, reqmode, reqmuts, numclients());
+                        break;
+                    }
+                    case 7: if(!haspriv(ci, PRIV_MAX, "select a map to play")) return; level -= 6; break;
+                    case 0: default: break;
+                }
+                if(list)
+                {
+                    if(listincludes(list, reqmap, strlen(reqmap)) < 0 && !haspriv(ci, level-1+PRIV_MASTER, "select maps not in the rotation"))
+                    {
+                        DELETEA(list);
+                        return;
+                    }
                     DELETEA(list);
-                    return;
                 }
-                DELETEA(list);
             }
         }
         copystring(ci->mapvote, reqmap);
@@ -2382,12 +2408,12 @@ namespace server
                 case ID_COMMAND:
                 {
                     string s;
-                    if(nargs <= 1 || !arg) formatstring(s)("%s", cmd);
-                    else formatstring(s)("%s %s", cmd, arg);
+                    if(nargs <= 1 || !arg) formatstring(s)("%s", id->name);
+                    else formatstring(s)("%s %s", id->name, arg);
                     char *ret = executestr(s);
                     if(ret)
                     {
-                        if(*ret) conoutft(CON_MESG, "\fc%s returned %s", cmd, ret);
+                        if(*ret) conoutft(CON_MESG, "\fc%s returned %s", id->name, ret);
                         delete[] ret;
                     }
                     return true;
@@ -2396,12 +2422,12 @@ namespace server
                 {
                     if(nargs <= 1 || !arg)
                     {
-                        conoutft(CON_MESG, id->flags&IDF_HEX && *id->storage.i >= 0 ? (id->maxval==0xFFFFFF ? "\fc%s = 0x%.6X" : "\fc%s = 0x%X") : "\fc%s = %d", cmd, *id->storage.i);
+                        conoutft(CON_MESG, id->flags&IDF_HEX && *id->storage.i >= 0 ? (id->maxval==0xFFFFFF ? "\fc%s = 0x%.6X" : "\fc%s = 0x%X") : "\fc%s = %d", id->name, *id->storage.i);
                         return true;
                     }
                     if(id->maxval < id->minval)
                     {
-                        conoutft(CON_MESG, "\frcannot override variable: %s", cmd);
+                        conoutft(CON_MESG, "\frcannot override variable: %s", id->name);
                         return true;
                     }
                     int ret = parseint(arg);
@@ -2410,7 +2436,7 @@ namespace server
                         conoutft(CON_MESG,
                             id->flags&IDF_HEX ?
                                     (id->minval <= 255 ? "\frvalid range for %s is %d..0x%X" : "\frvalid range for %s is 0x%X..0x%X") :
-                                    "\frvalid range for %s is %d..%d", cmd, id->minval, id->maxval);
+                                    "\frvalid range for %s is %d..%d", id->name, id->minval, id->maxval);
                         return true;
                     }
                     checkvar(id, arg);
@@ -2423,13 +2449,13 @@ namespace server
                 {
                     if(nargs <= 1 || !arg)
                     {
-                        conoutft(CON_MESG, "\fc%s = %s", cmd, floatstr(*id->storage.f));
+                        conoutft(CON_MESG, "\fc%s = %s", id->name, floatstr(*id->storage.f));
                         return true;
                     }
                     float ret = parsefloat(arg);
                     if(ret < id->minvalf || ret > id->maxvalf)
                     {
-                        conoutft(CON_MESG, "\frvalid range for %s is %s..%s", cmd, floatstr(id->minvalf), floatstr(id->maxvalf));
+                        conoutft(CON_MESG, "\frvalid range for %s is %s..%s", id->name, floatstr(id->minvalf), floatstr(id->maxvalf));
                         return true;
                     }
                     checkvar(id, arg);
@@ -2442,7 +2468,7 @@ namespace server
                 {
                     if(nargs <= 1 || !arg)
                     {
-                        conoutft(CON_MESG, strchr(*id->storage.s, '"') ? "\fc%s = [%s]" : "\fc%s = \"%s\"", cmd, *id->storage.s);
+                        conoutft(CON_MESG, strchr(*id->storage.s, '"') ? "\fc%s = [%s]" : "\fc%s = \"%s\"", id->name, *id->storage.s);
                         return true;
                     }
                     checkvar(id, arg);
@@ -2467,19 +2493,21 @@ namespace server
         ident *id = idents.access(cmdname);
         if(id && id->flags&IDF_SERVER)
         {
+            const char *name = &id->name[3];
             mkstring(val);
-            int locked = max(id->flags&IDF_ADMIN ? 2 : 0, GAME(varslock));
+            int locked = max(id->flags&IDF_ADMIN ? 3 : 0, GAME(varslock));
+            if(!strcmp(id->name, "sv_gamespeed") && GAME(gamespeedlock) > locked) locked = GAME(gamespeedlock);
             switch(id->type)
             {
                 case ID_COMMAND:
                 {
-                    if(locked && !haspriv(ci, locked >= 3 ? PRIV_MAX : (locked >= 2 ? PRIV_ADMIN : PRIV_MASTER), "execute commands")) return;
+                    if(locked && !haspriv(ci, locked-1+PRIV_MASTER, "execute that command")) return;
                     string s;
-                    if(nargs <= 1 || !arg) formatstring(s)("sv_%s", cmd);
-                    else formatstring(s)("sv_%s %s", cmd, arg);
+                    if(nargs <= 1 || !arg) formatstring(s)("%s", id->name);
+                    else formatstring(s)("%s %s", id->name, arg);
                     char *ret = executestr(s);
-                    if(ret && *ret) srvoutf(-3, "\fc%s executed %s (returned: %s)", colorname(ci), cmd, ret);
-                    else srvoutf(-3, "\fc%s executed %s", colorname(ci), cmd);
+                    if(ret && *ret) srvoutf(-3, "\fc%s executed %s (returned: %s)", colorname(ci), name, ret);
+                    else srvoutf(-3, "\fc%s executed %s", colorname(ci), name);
                     if(ret) delete[] ret;
                     return;
                 }
@@ -2487,18 +2515,18 @@ namespace server
                 {
                     if(nargs <= 1 || !arg)
                     {
-                        srvmsgf(ci->clientnum, id->flags&IDF_HEX && *id->storage.i >= 0 ? (id->maxval==0xFFFFFF ? "\fc%s = 0x%.6X" : "\fc%s = 0x%X") : "\fc%s = %d", cmd, *id->storage.i);
+                        srvmsgf(ci->clientnum, id->flags&IDF_HEX && *id->storage.i >= 0 ? (id->maxval==0xFFFFFF ? "\fc%s = 0x%.6X" : "\fc%s = 0x%X") : "\fc%s = %d", name, *id->storage.i);
                         return;
                     }
-                    else if(locked && !haspriv(ci, locked >= 3 ? PRIV_MAX : (locked >= 2 ? PRIV_ADMIN : PRIV_MASTER), "change variables"))
+                    else if(locked && !haspriv(ci, locked-1+PRIV_MASTER, "change that variable"))
                     {
                         formatstring(val)(id->flags&IDF_HEX && *id->storage.i >= 0 ? (id->maxval==0xFFFFFF ? "0x%.6X" : "0x%X") : "%d", *id->storage.i);
-                        sendf(ci->clientnum, 1, "ri2ss", N_COMMAND, -1, &id->name[3], val);
+                        sendf(ci->clientnum, 1, "ri2ss", N_COMMAND, -1, name, val);
                         return;
                     }
                     if(id->maxval < id->minval)
                     {
-                        srvmsgf(ci->clientnum, "\frcannot override variable: %s", cmd);
+                        srvmsgf(ci->clientnum, "\frcannot override variable: %s", name);
                         return;
                     }
                     int ret = parseint(arg);
@@ -2507,7 +2535,7 @@ namespace server
                         srvmsgf(ci->clientnum,
                             id->flags&IDF_HEX ?
                                 (id->minval <= 255 ? "\frvalid range for %s is %d..0x%X" : "\frvalid range for %s is 0x%X..0x%X") :
-                                "\frvalid range for %s is %d..%d", cmd, id->minval, id->maxval);
+                                "\frvalid range for %s is %d..%d", name, id->minval, id->maxval);
                         return;
                     }
                     checkvar(id, arg);
@@ -2520,19 +2548,19 @@ namespace server
                 {
                     if(nargs <= 1 || !arg)
                     {
-                        srvmsgf(ci->clientnum, "\fc%s = %s", cmd, floatstr(*id->storage.f));
+                        srvmsgf(ci->clientnum, "\fc%s = %s", name, floatstr(*id->storage.f));
                         return;
                     }
-                    else if(locked && !haspriv(ci, locked >= 3 ? PRIV_MAX : (locked >= 2 ? PRIV_ADMIN : PRIV_MASTER), "change variables"))
+                    else if(locked && !haspriv(ci, locked-1+PRIV_MASTER, "change that variable"))
                     {
                         formatstring(val)("%s", floatstr(*id->storage.f));
-                        sendf(ci->clientnum, 1, "ri2ss", N_COMMAND, -1, &id->name[3], val);
+                        sendf(ci->clientnum, 1, "ri2ss", N_COMMAND, -1, name, val);
                         return;
                     }
                     float ret = parsefloat(arg);
                     if(ret < id->minvalf || ret > id->maxvalf)
                     {
-                        srvmsgf(ci->clientnum, "\frvalid range for %s is %s..%s", cmd, floatstr(id->minvalf), floatstr(id->maxvalf));
+                        srvmsgf(ci->clientnum, "\frvalid range for %s is %s..%s", name, floatstr(id->minvalf), floatstr(id->maxvalf));
                         return;
                     }
                     checkvar(id, arg);
@@ -2545,13 +2573,13 @@ namespace server
                 {
                     if(nargs <= 1 || !arg)
                     {
-                        srvmsgf(ci->clientnum, strchr(*id->storage.s, '"') ? "\fc%s = [%s]" : "\fc%s = \"%s\"", cmd, *id->storage.s);
+                        srvmsgf(ci->clientnum, strchr(*id->storage.s, '"') ? "\fc%s = [%s]" : "\fc%s = \"%s\"", name, *id->storage.s);
                         return;
                     }
-                    else if(locked && !haspriv(ci, locked >= 3 ? PRIV_MAX : (locked >= 2 ? PRIV_ADMIN : PRIV_MASTER), "change variables"))
+                    else if(locked && !haspriv(ci, locked-1+PRIV_MASTER, "change that variable"))
                     {
                         formatstring(val)("%s", *id->storage.s);
-                        sendf(ci->clientnum, 1, "ri2ss", N_COMMAND, -1, &id->name[3], val);
+                        sendf(ci->clientnum, 1, "ri2ss", N_COMMAND, -1, name, val);
                         return;
                     }
                     checkvar(id, arg);
@@ -2563,8 +2591,8 @@ namespace server
                 }
                 default: return;
             }
-            sendf(-1, 1, "ri2ss", N_COMMAND, ci->clientnum, &id->name[3], val);
-            relayf(3, "\fc%s set %s to %s", colorname(ci), &id->name[3], val);
+            sendf(-1, 1, "ri2ss", N_COMMAND, ci->clientnum, name, val);
+            relayf(3, "\fc%s set %s to %s", colorname(ci), name, val);
         }
         else srvmsgf(ci->clientnum, "\frunknown command: %s", cmd);
     }
@@ -3040,7 +3068,7 @@ namespace server
             {
                 ci->state.cpmillis = 0;
                 ci->state.cpnodes.shrink(0);
-                sendf(-1, 1, "ri4", N_CHECKPOINT, ci->clientnum, -1, 0);
+                sendf(-1, 1, "ri5", N_CHECKPOINT, ci->clientnum, -1, -1, 0);
             }
         }
         else if(!m_duke(gamemode, mutators)) givepoints(ci, smode ? smode->points(ci, ci) : -1);
@@ -3690,7 +3718,7 @@ namespace server
 
             if(interm && totalmillis - interm >= 0) // wait then call for next map
             {
-                if(GAME(votelimit) && !maprequest && GAME(votelock) != 5 && (GAME(modelock) != 5 || GAME(mapslock) != 5))
+                if(GAME(votelimit) && !maprequest && GAME(votelock) != 7 && GAME(modelock) != 7 && GAME(mapslock) != 7)
                 { // if they can't vote, no point in waiting for them to do so
                     if(demorecord) enddemorecord();
                     sendf(-1, 1, "ri", N_NEWGAME);
@@ -4440,7 +4468,7 @@ namespace server
                                             cp->state.cptime = laptime;
                                             if(sents[ent].attrs[6] == CP_FINISH) { cp->state.cpmillis = -gamemillis; waiting(cp); }
                                         }
-                                        sendf(-1, 1, "ri4", N_CHECKPOINT, cp->clientnum, laptime, cp->state.cptime);
+                                        sendf(-1, 1, "ri5", N_CHECKPOINT, cp->clientnum, ent, laptime, cp->state.cptime);
                                         if(m_team(gamemode, mutators))
                                         {
                                             score &ts = teamscore(cp->team);
@@ -4454,6 +4482,7 @@ namespace server
                                     case CP_RESPAWN: if(sents[ent].attrs[6] == CP_RESPAWN && cp->state.cpmillis) break;
                                     case CP_START:
                                     {
+                                        sendf(-1, 1, "ri5", N_CHECKPOINT, cp->clientnum, ent, -1, 0);
                                         cp->state.cpmillis = gamemillis;
                                         cp->state.cpnodes.shrink(0);
                                     }
@@ -4570,9 +4599,16 @@ namespace server
                 case N_SWITCHTEAM:
                 {
                     int team = getint(p);
-                    if(((ci->state.state == CS_SPECTATOR || ci->state.state == CS_EDITING) && team != TEAM_NEUTRAL) || !isteam(gamemode, mutators, team, TEAM_FIRST) || ci->state.aitype >= AI_START)
-                        team = chooseteam(ci, team);
-                    if(ci->team != team) setteam(ci, team, true, true);
+                    if(!isteam(gamemode, mutators, team, TEAM_FIRST) || ci->state.aitype >= AI_START || team == ci->team) break;
+                    bool reset = true;
+                    if(ci->state.state == CS_SPECTATOR)
+                    {
+                        if(!allowstate(ci, ALST_TRY) && !haspriv(ci, GAME(speclock)+PRIV_MASTER, "exit spectator"))
+                            break;
+                        spectate(ci, false);
+                        reset = false;
+                    }
+                    setteam(ci, team, reset, true);
                     break;
                 }
 
@@ -4772,15 +4808,15 @@ namespace server
                 {
                     int victim = getint(p);
                     bool ban = getint(p) != 0;
-                    if(haspriv(ci, PRIV_MASTER, "kick/ban people") && victim >= 0 && ci->clientnum != victim)
+                    if(haspriv(ci, (ban ? GAME(banlock) : GAME(kicklock))+PRIV_MASTER, ban ? "ban people" : "kick people") && victim >= 0 && ci->clientnum != victim)
                     {
                         uint ip = getclientip(victim);
                         if(!ip) break;
                         clientinfo *cp = (clientinfo *)getinfo(victim);
-                        if(!cp || cp->state.ownernum >= 0 || !cmppriv(ci, cp, "kick/ban")) break;
+                        if(!cp || cp->state.ownernum >= 0 || !cmppriv(ci, cp, ban ? "ban" : "kick")) break;
                         if(checkipinfo(allows, ip))
                         {
-                            if(!haspriv(ci, PRIV_ADMIN, "kick/ban protected people")) break;
+                            if(!haspriv(ci, PRIV_ADMIN, ban ? "ban protected people" : "kick protected people")) break;
                             else if(ban) loopvrev(allows) if((ip & allows[i].mask) == allows[i].ip) allows.remove(i);
                         }
                         if(ban)
@@ -4801,7 +4837,7 @@ namespace server
                     int sn = getint(p), val = getint(p);
                     clientinfo *cp = (clientinfo *)getinfo(sn);
                     if(!cp || cp->state.aitype > AI_NONE) break;
-                    if((sn != sender || !allowstate(cp, val ? ALST_SPEC : ALST_TRY)) && !haspriv(ci, PRIV_MASTER, sn != sender ? "control other players" : (val ? "enter spectator" : "exit spectator")))
+                    if((sn != sender || !allowstate(cp, val ? ALST_SPEC : ALST_TRY)) && !haspriv(ci, GAME(speclock)+PRIV_MASTER, sn != sender ? "control other players" : (val ? "enter spectator" : "exit spectator")))
                         break;
                     spectate(cp, val);
                     break;
@@ -4812,8 +4848,8 @@ namespace server
                     int who = getint(p), team = getint(p);
                     if(who<0 || who>=getnumclients() || !haspriv(ci, PRIV_MASTER, "change the team of others")) break;
                     clientinfo *cp = (clientinfo *)getinfo(who);
-                    if(!cp || !m_team(gamemode, mutators) || !m_fight(gamemode) || cp->state.aitype >= AI_START) break;
-                    if(cp->state.state == CS_SPECTATOR || cp->state.state == CS_EDITING || !isteam(gamemode, mutators, team, TEAM_FIRST)) break;
+                    if(!cp || !m_team(gamemode, mutators) || m_local(gamemode) || cp->state.aitype >= AI_START) break;
+                    if(cp->state.state == CS_SPECTATOR || !isteam(gamemode, mutators, team, TEAM_FIRST)) break;
                     setteam(cp, team, true, true);
                     break;
                 }
diff --git a/src/game/team.h b/src/game/team.h
index 0a3eacf..2a1a29c 100644
--- a/src/game/team.h
+++ b/src/game/team.h
@@ -1,47 +1,46 @@
-enum
-{
-    TEAM_NEUTRAL = 0, TEAM_ALPHA, TEAM_OMEGA, TEAM_KAPPA, TEAM_SIGMA, TEAM_ENEMY, TEAM_MAX,
-    TEAM_FIRST = TEAM_ALPHA, TEAM_LAST = TEAM_OMEGA, TEAM_MULTI = TEAM_SIGMA,
-    TEAM_COUNT = TEAM_LAST+1, TEAM_ALL = TEAM_MULTI+1,
-    TEAM_NUM = (TEAM_LAST-TEAM_FIRST)+1,
-    TEAM_TOTAL = (TEAM_MULTI-TEAM_FIRST)+1
-};
-
-#define TEAMS(a,b) \
-    GSVAR(0, team##a##name, #a); \
-    GVAR(IDF_HEX, team##a##colour, 0, b, 0xFFFFFF);
-
-TEAMS(neutral, 0x90A090);
-TEAMS(alpha, 0x5F66FF);
-TEAMS(omega, 0xFF4F44);
-TEAMS(kappa, 0xFFD022);
-TEAMS(sigma, 0x22FF22);
-TEAMS(enemy, 0x999999);
-
-#ifdef GAMESERVER
-#define TEAMDEF(proto,name)     proto *sv_team_stat_##name[] = { &sv_teamneutral##name, &sv_teamalpha##name, &sv_teamomega##name, &sv_teamkappa##name, &sv_teamsigma##name, &sv_teamenemy##name };
-#define TEAM(id,name)           (*sv_team_stat_##name[id])
-#else
-#ifdef GAMEWORLD
-#define TEAMDEF(proto,name)     proto *team_stat_##name[] = { &teamneutral##name, &teamalpha##name, &teamomega##name, &teamkappa##name, &teamsigma##name, &teamenemy##name };
-#else
-#define TEAMDEF(proto,name)     extern proto *team_stat_##name[];
-#endif
-#define TEAM(id,name)           (*team_stat_##name[id])
-#endif
-TEAMDEF(char *, name);
-TEAMDEF(int, colour);
-
-struct score
-{
-    int team, total;
-    score() {}
-    score(int s, int n) : team(s), total(n) {}
-};
-enum { BASE_NONE = 0, BASE_HOME = 1<<0, BASE_FLAG = 1<<1, BASE_BOTH = BASE_HOME|BASE_FLAG };
-
-#define numteams(a,b)   (m_fight(a) && m_team(a,b) ? (m_multi(a,b) ? TEAM_TOTAL : TEAM_NUM) : 1)
-#define teamcount(a,b)  (m_fight(a) && m_team(a,b) ? (m_multi(a,b) ? TEAM_ALL : TEAM_COUNT) : 1)
-#define isteam(a,b,c,d) (m_fight(a) && m_team(a,b) ? (c >= d && c <= numteams(a,b)) : c == TEAM_NEUTRAL)
-#define valteam(a,b)    (a >= b && a <= TEAM_TOTAL)
-
+enum
+{
+    TEAM_NEUTRAL = 0, TEAM_ALPHA, TEAM_OMEGA, TEAM_KAPPA, TEAM_SIGMA, TEAM_ENEMY, TEAM_MAX,
+    TEAM_FIRST = TEAM_ALPHA, TEAM_LAST = TEAM_OMEGA, TEAM_MULTI = TEAM_SIGMA,
+    TEAM_COUNT = TEAM_LAST+1, TEAM_ALL = TEAM_MULTI+1,
+    TEAM_NUM = (TEAM_LAST-TEAM_FIRST)+1,
+    TEAM_TOTAL = (TEAM_MULTI-TEAM_FIRST)+1
+};
+
+#define TEAMS(a,b) \
+    GSVAR(0, team##a##name, #a); \
+    GVAR(IDF_HEX, team##a##colour, 0, b, 0xFFFFFF);
+
+TEAMS(neutral, 0x90A090);
+TEAMS(alpha, 0x5F66FF);
+TEAMS(omega, 0xFF4F44);
+TEAMS(kappa, 0xFFD022);
+TEAMS(sigma, 0x22FF22);
+TEAMS(enemy, 0x999999);
+
+#ifdef GAMESERVER
+#define TEAMDEF(proto,name)     proto *sv_team_stat_##name[] = { &sv_teamneutral##name, &sv_teamalpha##name, &sv_teamomega##name, &sv_teamkappa##name, &sv_teamsigma##name, &sv_teamenemy##name };
+#define TEAM(id,name)           (*sv_team_stat_##name[id])
+#else
+#ifdef GAMEWORLD
+#define TEAMDEF(proto,name)     proto *team_stat_##name[] = { &teamneutral##name, &teamalpha##name, &teamomega##name, &teamkappa##name, &teamsigma##name, &teamenemy##name };
+#else
+#define TEAMDEF(proto,name)     extern proto *team_stat_##name[];
+#endif
+#define TEAM(id,name)           (*team_stat_##name[id])
+#endif
+TEAMDEF(char *, name);
+TEAMDEF(int, colour);
+
+struct score
+{
+    int team, total;
+    score() {}
+    score(int s, int n) : team(s), total(n) {}
+};
+
+#define numteams(a,b)   (m_fight(a) && m_team(a,b) ? (m_multi(a,b) ? TEAM_TOTAL : TEAM_NUM) : 1)
+#define teamcount(a,b)  (m_fight(a) && m_team(a,b) ? (m_multi(a,b) ? TEAM_ALL : TEAM_COUNT) : 1)
+#define isteam(a,b,c,d) (m_fight(a) && m_team(a,b) ? (c >= d && c <= numteams(a,b)) : c == TEAM_NEUTRAL)
+#define valteam(a,b)    (a >= b && a <= TEAM_TOTAL)
+
diff --git a/src/game/vars.h b/src/game/vars.h
index 8a0d03b..b3c2adf 100644
--- a/src/game/vars.h
+++ b/src/game/vars.h
@@ -3,7 +3,11 @@ GVAR(IDF_ADMIN, serverclients, 1, 16, MAXCLIENTS);
 GVAR(IDF_ADMIN, serveropen, 0, 3, 3);
 GSVAR(IDF_ADMIN, serverdesc, "");
 GSVAR(IDF_ADMIN, servermotd, "");
+
 GVAR(IDF_ADMIN, automaster, 0, 0, 1);
+GVAR(IDF_ADMIN, speclock, 0, 1, 3); // 0 = master, 1 = auth, 2 = admin, 3 = nobody
+GVAR(IDF_ADMIN, kicklock, 0, 1, 3); // 0 = master, 1 = auth, 2 = admin, 3 = nobody
+GVAR(IDF_ADMIN, banlock, 0, 1, 3); // 0 = master, 1 = auth, 2 = admin, 3 = nobody
 
 GVAR(IDF_ADMIN, autospectate, 0, 1, 1); // auto spectate if idle, 1 = auto spectate when remaining dead for autospecdelay
 GVAR(IDF_ADMIN, autospecdelay, 0, 60000, VAR_MAX);
@@ -12,7 +16,8 @@ GVAR(IDF_ADMIN, resetbansonend, 0, 1, 2); // reset bans on end (1: just when emp
 GVAR(IDF_ADMIN, resetvarsonend, 0, 1, 2); // reset variables on end (1: just when empty, 2: when matches end)
 GVAR(IDF_ADMIN, resetmmonend, 0, 2, 2); // reset mastermode on end (1: just when empty, 2: when matches end)
 
-GVARF(IDF_ADMIN, gamespeed, 1, 100, 10000, timescale = sv_gamespeed, timescale = gamespeed);
+GVARF(0, gamespeed, 1, 100, 10000, timescale = sv_gamespeed, timescale = gamespeed);
+GVAR(IDF_ADMIN, gamespeedlock, 0, 3, 4); // 0 = off, 1 = master, 2 = auth, 3 = admin, 4 = nobody
 GVARF(IDF_ADMIN, gamepaused, 0, 0, 1, paused = sv_gamepaused, paused = gamepaused);
 
 GSVAR(IDF_ADMIN, defaultmap, "");
@@ -24,11 +29,11 @@ GVAR(IDF_ADMIN, rotatemuts, 0, 3, VAR_MAX); // any more than one decreases the c
 GVAR(IDF_ADMIN, rotatemutsfilter, 0, G_M_FILTER, G_M_ALL); // mutators not in this array are filtered out
 GVAR(IDF_ADMIN, campaignplayers, 1, 4, MAXPLAYERS);
 
-GSVAR(IDF_ADMIN, allowmaps, "alphacampaign ares bath biolytic blink cargo center colony conflict darkness dawn deadsimple deathtrap deli depot dropzone dutility echo error facility forge foundation fourplex futuresport ghost hinder industrial isolation keystone lab linear longestyard mist nova panic processing purge spacetech starlibido stone testchamber tower tranquility tribal ubik venus warp wet");
+GSVAR(IDF_ADMIN, allowmaps, "alphacampaign ares bath biolytic blink cargo center colony conflict darkness dawn deadsimple deathtrap deli depot dropzone dutility echo error facility forge foundation fourplex futuresport ghost hinder industrial institute isolation keystone lab linear longestyard mist nova panic processing purge spacetech starlibido stone testchamber tower tranquility tribal ubik venus warp wet");
 
-GSVAR(IDF_ADMIN, mainmaps, "ares bath biolytic blink cargo center colony conflict darkness deadsimple deathtrap deli depot dropzone dutility echo error facility forge foundation fourplex futuresport ghost industrial isolation keystone lab linear longestyard mist nova panic processing spacetech starlibido stone tower tranquility tribal ubik venus warp wet");
-GSVAR(IDF_ADMIN, capturemaps, "ares bath biolytic cargo center colony conflict darkness deadsimple deli depot dropzone dutility echo facility forge foundation fourplex futuresport ghost industrial isolation keystone linear mist nova panic stone tranquility tribal venus warp wet");
-GSVAR(IDF_ADMIN, defendmaps, "ares bath biolytic cargo center colony conflict darkness deadsimple deli depot dropzone dutility echo facility forge foundation fourplex futuresport ghost industrial isolation keystone lab linear mist nova panic processing stone tower tranquility tribal ubik venus warp wet");
+GSVAR(IDF_ADMIN, mainmaps, "ares bath biolytic blink cargo center colony conflict darkness deadsimple deathtrap deli depot dropzone dutility echo error facility forge foundation fourplex futuresport ghost industrial institute isolation keystone lab linear longestyard mist nova panic processing spacetech starlibido stone tower tranquility tribal ubik venus warp wet");
+GSVAR(IDF_ADMIN, capturemaps, "ares bath biolytic cargo center colony conflict darkness deadsimple deli depot dropzone dutility echo facility forge foundation fourplex futuresport ghost industrial institute isolation keystone linear mist nova panic stone tranquility tribal venus warp wet");
+GSVAR(IDF_ADMIN, defendmaps, "ares bath biolytic cargo center colony conflict darkness deadsimple deli depot dropzone dutility echo facility forge foundation fourplex futuresport ghost industrial institute isolation keystone lab linear mist nova panic processing stone tower tranquility tribal ubik venus warp wet");
 GSVAR(IDF_ADMIN, bombermaps, "ares bath biolytic cargo center colony conflict darkness deadsimple deli depot dropzone dutility echo forge foundation futuresport fourplex ghost industrial isolation linear mist nova stone tower tranquility tribal venus warp wet");
 GSVAR(IDF_ADMIN, holdmaps, "ares bath biolytic cargo center colony conflict darkness deadsimple deli depot dropzone dutility echo facility forge foundation fourplex futuresport ghost industrial isolation keystone lab linear mist nova panic processing stone tower tranquility tribal ubik venus warp wet");
 GSVAR(IDF_ADMIN, trialmaps, "hinder purge testchamber");
@@ -39,20 +44,20 @@ GSVAR(IDF_ADMIN, duelmaps, "bath darkness deadsimple dutility echo fourplex ghos
 GSVAR(IDF_ADMIN, jetpackmaps, "alphacampaign ares biolytic cargo center colony conflict darkness dawn deadsimple deathtrap deli depot dropzone dutility echo error forge foundation fourplex futuresport ghost isolation keystone linear longestyard mist nova spacetech starlibido testchamber tower tranquility tribal ubik venus warp");
 
 GSVAR(IDF_ADMIN, smallmaps, "bath darkness deadsimple dutility echo fourplex ghost longestyard starlibido stone panic wet");
-GSVAR(IDF_ADMIN, mediummaps, "ares biolytic blink cargo center colony conflict darkness deadsimple deathtrap deli dropzone echo error facility forge foundation fourplex futuresport ghost industrial isolation keystone lab linear mist nova panic processing spacetech starlibido stone tower tranquility tribal ubik venus warp wet");
+GSVAR(IDF_ADMIN, mediummaps, "ares biolytic blink cargo center colony conflict darkness deadsimple deathtrap deli dropzone echo error facility forge foundation fourplex futuresport ghost industrial institute isolation keystone lab linear mist nova panic processing spacetech starlibido stone tower tranquility tribal ubik venus warp wet");
 GSVAR(IDF_ADMIN, largemaps, "ares biolytic blink cargo center colony dawn deadsimple deathtrap deli depot error facility forge foundation futuresport ghost industrial isolation lab linear mist nova processing spacetech tower tranquility tribal ubik venus warp");
 
 
-GVAR(IDF_ADMIN, modelock, 0, 3, 5); // 0 = off, 1 = master only (+1 admin only), 3 = master can only set limited mode and higher (+1 admin), 5 = no mode selection
+GVAR(IDF_ADMIN, modelock, 0, 5, 7); // 0 = off, 1-3 = master/auth/admin only, 4-6 = master/auth/admin can only set limited mode and higher, 7 = no mode selection
 GVAR(IDF_ADMIN, modelockfilter, 0, G_LIMIT, G_ALL);
 GVAR(IDF_ADMIN, mutslockfilter, 0, G_M_ALL, G_M_ALL);
 GVARF(IDF_ADMIN, instagibfilter, 0, mutstype[G_M_IGN].mutators&~G_M_ARENA, mutstype[G_M_IGN].mutators, sv_instagibfilter &= ~G_M_VAMPIRE; sv_instagibfilter |= G_M_INSTA, instagibfilter &= ~G_M_VAMPIRE; instagibfilter |= G_M_INSTA);
 
 GVAR(IDF_ADMIN, maprotate, 0, 2, 2); // 0 = off, 1 = sequence, 2 = random
 GVAR(IDF_ADMIN, mapsfilter, 0, 1, 2); // 0 = off, 1 = filter based on mutators, 2 = also filter based on players
-GVAR(IDF_ADMIN, mapslock, 0, 3, 5); // 0 = off, 1 = master can select non-allow maps (+1 admin), 3 = master can select non-rotation maps (+1 admin), 5 = no map selection
-GVAR(IDF_ADMIN, varslock, 0, 1, 3); // 0 = off, 1 = master, 2 = admin only, 3 = nobody
-GVAR(IDF_ADMIN, votelock, 0, 1, 5); // 0 = off, 1 = master can select same game (+1 admin), 3 = master only can vote (+1 admin), 5 = no voting
+GVAR(IDF_ADMIN, mapslock, 0, 5, 7); // 0 = off, 1-3 = master/auth/admin can select non-allow maps, 4-6 = master/auth/admin can select non-rotation maps, 7 = no map selection
+GVAR(IDF_ADMIN, varslock, 0, 2, 4); // 0 = off, 1 = master, 2 = auth, 3 = admin, 4 = nobody
+GVAR(IDF_ADMIN, votelock, 0, 2, 7); // 0 = off, 1-3 = master/auth/admin can select same game, 4 = master/auth/admin only can vote, 7 = no voting
 GVAR(IDF_ADMIN, votewait, 0, 2500, VAR_MAX);
 GFVAR(IDF_ADMIN, votethreshold, 0, 0.5f, 1); // auto-pass votes when this many agree
 
diff --git a/src/game/waypoint.cpp b/src/game/waypoint.cpp
index 7a650bd..f6f6bb0 100644
--- a/src/game/waypoint.cpp
+++ b/src/game/waypoint.cpp
@@ -1,813 +1,813 @@
-#include "game.h"
-
-extern selinfo sel;
-
-namespace ai
-{
-    using namespace game;
-
-    vector<waypoint> waypoints;
-    vector<oldwaypoint> oldwaypoints;
-
-    bool clipped(const vec &o)
-    {
-        int material = lookupmaterial(o), clipmat = material&MATF_CLIP;
-        return clipmat == MAT_CLIP || clipmat == MAT_AICLIP || material&MAT_DEATH || (material&MATF_VOLUME) == MAT_LAVA;
-    }
-
-    int getweight(const vec &o)
-    {
-        vec pos = o; pos.z += JUMPMIN;
-        if(!insideworld(vec(pos.x, pos.y, min(pos.z, getworldsize() - 1e-3f)))) return -2;
-        float dist = raycube(pos, vec(0, 0, -1), 0, RAY_CLIPMAT);
-        int posmat = lookupmaterial(pos), weight = 1;
-        if(isliquid(posmat&MATF_VOLUME)) weight *= 5;
-        if(dist >= 0)
-        {
-            weight = int(dist/JUMPMIN);
-            pos.z -= clamp(dist-8.0f, 0.0f, pos.z);
-            int trgmat = lookupmaterial(pos);
-            if(trgmat&MAT_DEATH || (trgmat&MATF_VOLUME) == MAT_LAVA) weight *= 10;
-            else if(isliquid(trgmat&MATF_VOLUME)) weight *= 2;
-        }
-        return weight;
-    }
-
-    enum
-    {
-        WPCACHE_STATIC = 0,
-        WPCACHE_DYNAMIC,
-        NUMWPCACHES
-    };
-
-    struct wpcachenode
-    {
-        float split[2];
-        uint child[2];
-
-        int axis() const { return child[0]>>30; }
-        int childindex(int which) const { return child[which]&0x3FFFFFFF; }
-        bool isleaf(int which) const { return (child[1]&(1<<(30+which)))!=0; }
-    };
-
-    struct wpcache
-    {
-        vector<wpcachenode> nodes;
-        int firstwp, lastwp, maxdepth;
-        vec bbmin, bbmax;
-
-        wpcache() { clear(); }
-
-        void clear()
-        {
-            nodes.setsize(0);
-            firstwp = lastwp = -1;
-            maxdepth = -1;
-            bbmin = vec(1e16f, 1e16f, 1e16f);
-            bbmax = vec(-1e16f, -1e16f, -1e16f);
-        }
-
-        void build(int first = -1, int last = -1)
-        {
-            if(last < 0) last = waypoints.length();
-            vector<int> indices;
-            for(int i = first; i < last; i++)
-            {
-                waypoint &w = waypoints[i];
-                indices.add(i);
-                if(firstwp < 0) firstwp = i;
-                float radius = WAYPOINTRADIUS;
-                bbmin.min(vec(w.o).sub(radius));
-                bbmax.max(vec(w.o).add(radius));
-            }
-            build(indices.getbuf(), indices.length(), bbmin, bbmax);
-        }
-
-        void build(int *indices, int numindices, const vec &vmin, const vec &vmax, int depth = 1)
-        {
-            int axis = 2;
-            loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k;
-
-            vec leftmin(1e16f, 1e16f, 1e16f), leftmax(-1e16f, -1e16f, -1e16f), rightmin(1e16f, 1e16f, 1e16f), rightmax(-1e16f, -1e16f, -1e16f);
-            float split = 0.5f*(vmax[axis] + vmin[axis]), splitleft = -1e16f, splitright = 1e16f;
-            int left, right;
-            for(left = 0, right = numindices; left < right;)
-            {
-                waypoint &w = waypoints[indices[left]];
-                float radius = WAYPOINTRADIUS;
-                if(max(split - (w.o[axis]-radius), 0.0f) > max((w.o[axis]+radius) - split, 0.0f))
-                {
-                    ++left;
-                    splitleft = max(splitleft, w.o[axis]+radius);
-                    leftmin.min(vec(w.o).sub(radius));
-                    leftmax.max(vec(w.o).add(radius));
-                }
-                else
-                {
-                    --right;
-                    swap(indices[left], indices[right]);
-                    splitright = min(splitright, w.o[axis]-radius);
-                    rightmin.min(vec(w.o).sub(radius));
-                    rightmax.max(vec(w.o).add(radius));
-                }
-            }
-
-            if(!left || right==numindices)
-            {
-                leftmin = rightmin = vec(1e16f, 1e16f, 1e16f);
-                leftmax = rightmax = vec(-1e16f, -1e16f, -1e16f);
-                left = right = numindices/2;
-                splitleft = -1e16f;
-                splitright = 1e16f;
-                loopi(numindices)
-                {
-                    waypoint &w = waypoints[indices[i]];
-                    float radius = WAYPOINTRADIUS;
-                    if(i < left)
-                    {
-                        splitleft = max(splitleft, w.o[axis]+radius);
-                        leftmin.min(vec(w.o).sub(radius));
-                        leftmax.max(vec(w.o).add(radius));
-                    }
-                    else
-                    {
-                        splitright = min(splitright, w.o[axis]-radius);
-                        rightmin.min(vec(w.o).sub(radius));
-                        rightmax.max(vec(w.o).add(radius));
-                    }
-                }
-            }
-
-            int node = nodes.length();
-            nodes.add();
-            nodes[node].split[0] = splitleft;
-            nodes[node].split[1] = splitright;
-
-            if(left<=1) nodes[node].child[0] = (axis<<30) | (left>0 ? indices[0] : 0x3FFFFFFF);
-            else
-            {
-                nodes[node].child[0] = (axis<<30) | (nodes.length()-node);
-                if(left) build(indices, left, leftmin, leftmax, depth+1);
-            }
-
-            if(numindices-right<=1) nodes[node].child[1] = (1<<31) | (left<=1 ? 1<<30 : 0) | (numindices-right>0 ? indices[right] : 0x3FFFFFFF);
-            else
-            {
-                nodes[node].child[1] = (left<=1 ? 1<<30 : 0) | (nodes.length()-node);
-                if(numindices-right) build(&indices[right], numindices-right, rightmin, rightmax, depth+1);
-            }
-
-            maxdepth = max(maxdepth, depth);
-        }
-    } wpcaches[NUMWPCACHES];
-
-    static int invalidatedwpcaches = 0, clearedwpcaches = (1<<NUMWPCACHES)-1, numinvalidatewpcaches = 0, lastwpcache = 0;
-
-    static inline void invalidatewpcache(int wp)
-    {
-        if(++numinvalidatewpcaches >= 1000) { numinvalidatewpcaches = 0; invalidatedwpcaches = (1<<NUMWPCACHES)-1; }
-        else loopi(NUMWPCACHES) if((wp >= wpcaches[i].firstwp && wp <= wpcaches[i].lastwp) || i+1 >= NUMWPCACHES) { invalidatedwpcaches |= 1<<i; break; }
-    }
-
-    void clearwpcache(bool full = true)
-	{
-        loopi(NUMWPCACHES) if(full || invalidatedwpcaches&(1<<i)) { wpcaches[i].clear(); clearedwpcaches |= 1<<i; }
-        if(full || invalidatedwpcaches == (1<<NUMWPCACHES)-1)
-        {
-            numinvalidatewpcaches = 0;
-            lastwpcache = 0;
-        }
-        invalidatedwpcaches = 0;
-	}
-    ICOMMAND(0, clearwpcache, "", (), clearwpcache());
-
-    void buildwpcache()
-    {
-        loopi(NUMWPCACHES) if(wpcaches[i].maxdepth < 0)
-            wpcaches[i].build(i > 0 ? wpcaches[i-1].lastwp+1 : 0, i+1 >= NUMWPCACHES || wpcaches[i+1].maxdepth < 0 ? -1 : wpcaches[i+1].firstwp);
-        clearedwpcaches = 0;
-        lastwpcache = waypoints.length();
-
-		wpavoid.clear();
-		loopv(waypoints) if(waypoints[i].weight < 0) wpavoid.avoidnear(NULL, waypoints[i].o.z + WAYPOINTRADIUS, waypoints[i].o, WAYPOINTRADIUS);
-    }
-
-    struct wpcachestack
-    {
-        wpcachenode *node;
-        float tmin, tmax;
-    };
-
-    vector<wpcachenode *> wpcachestack;
-
-    int closestwaypoint(const vec &pos, float mindist, bool links)
-    {
-        if(waypoints.empty()) return -1;
-        if(clearedwpcaches) buildwpcache();
-
-        #define CHECKCLOSEST(index) do { \
-            int n = (index); \
-            waypoint &w = waypoints[n]; \
-            if(!links || w.haslinks()) \
-            { \
-                float dist = w.o.squaredist(pos); \
-                if(dist < mindist*mindist) { closest = n; mindist = sqrtf(dist); } \
-            } \
-        } while(0)
-        int closest = -1;
-        wpcachenode *curnode;
-        loop(which, NUMWPCACHES) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
-        {
-            int axis = curnode->axis();
-            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
-            if(dist1 >= mindist)
-            {
-                if(dist2 < mindist)
-                {
-                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
-                    CHECKCLOSEST(curnode->childindex(1));
-                }
-            }
-            else if(curnode->isleaf(0))
-            {
-                CHECKCLOSEST(curnode->childindex(0));
-                if(dist2 < mindist)
-                {
-                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
-                    CHECKCLOSEST(curnode->childindex(1));
-                }
-            }
-            else
-            {
-                if(dist2 < mindist)
-                {
-                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
-                    else CHECKCLOSEST(curnode->childindex(1));
-                }
-                curnode += curnode->childindex(0);
-                continue;
-            }
-            if(wpcachestack.empty()) break;
-            curnode = wpcachestack.pop();
-        }
-        for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKCLOSEST(i); }
-        return closest;
-    }
-
-    void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results)
-    {
-        if(waypoints.empty()) return;
-        if(clearedwpcaches) buildwpcache();
-
-        float mindist2 = mindist*mindist, maxdist2 = maxdist*maxdist;
-        #define CHECKWITHIN(index) do { \
-            int n = (index); \
-            const waypoint &w = waypoints[n]; \
-            float dist = w.o.squaredist(pos); \
-            if(dist > mindist2 && dist < maxdist2) results.add(n); \
-        } while(0)
-        wpcachenode *curnode;
-        loop(which, NUMWPCACHES) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
-        {
-            int axis = curnode->axis();
-            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
-            if(dist1 >= maxdist)
-            {
-                if(dist2 < maxdist)
-                {
-                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
-                    CHECKWITHIN(curnode->childindex(1));
-                }
-            }
-            else if(curnode->isleaf(0))
-            {
-                CHECKWITHIN(curnode->childindex(0));
-                if(dist2 < maxdist)
-                {
-                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
-                    CHECKWITHIN(curnode->childindex(1));
-                }
-            }
-            else
-            {
-                if(dist2 < maxdist)
-                {
-                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
-                    else CHECKWITHIN(curnode->childindex(1));
-                }
-                curnode += curnode->childindex(0);
-                continue;
-            }
-            if(wpcachestack.empty()) break;
-            curnode = wpcachestack.pop();
-        }
-        for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKWITHIN(i); }
-    }
-
-    void avoidset::avoidnear(void *owner, float above, const vec &pos, float limit)
-    {
-        if(ai::waypoints.empty()) return;
-        if(clearedwpcaches) buildwpcache();
-
-        float limit2 = limit*limit;
-        #define CHECKNEAR(index) do { \
-            int n = (index); \
-            const waypoint &w = ai::waypoints[n]; \
-            if(w.o.squaredist(pos) < limit2) add(owner, above, n); \
-        } while(0)
-        wpcachenode *curnode;
-        loop(which, NUMWPCACHES) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
-        {
-            int axis = curnode->axis();
-            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
-            if(dist1 >= limit)
-            {
-                if(dist2 < limit)
-                {
-                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
-                    CHECKNEAR(curnode->childindex(1));
-                }
-            }
-            else if(curnode->isleaf(0))
-            {
-                CHECKNEAR(curnode->childindex(0));
-                if(dist2 < limit)
-                {
-                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
-                    CHECKNEAR(curnode->childindex(1));
-                }
-            }
-            else
-            {
-                if(dist2 < limit)
-                {
-                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
-                    else CHECKNEAR(curnode->childindex(1));
-                }
-                curnode += curnode->childindex(0);
-                continue;
-            }
-            if(wpcachestack.empty()) break;
-            curnode = wpcachestack.pop();
-        }
-        for(int i = lastwpcache; i < ai::waypoints.length(); i++) { CHECKNEAR(i); }
-    }
-
-    int avoidset::remap(gameent *d, int n, vec &pos, bool retry)
-    {
-        if(!obstacles.empty())
-        {
-            int cur = 0;
-            loopv(obstacles)
-            {
-                obstacle &ob = obstacles[i];
-                int next = cur + ob.numwaypoints;
-                if(ob.owner != d)
-                {
-                    for(; cur < next; cur++) if(waypoints[cur] == n)
-                    {
-                        if(ob.above < 0) return retry ? n : -1;
-                        vec above(pos.x, pos.y, ob.above);
-                        if(above.z-d->o.z >= ai::JUMPMAX)
-                            return retry ? n : -1; // too much scotty
-                        int node = closestwaypoint(above, ai::CLOSEDIST, true);
-                        if(ai::iswaypoint(node) && node != n)
-                        { // try to reroute above their head?
-                            if(!find(node, d))
-                            {
-                                pos = ai::waypoints[node].o;
-                                return node;
-                            }
-                            else return retry ? n : -1;
-                        }
-                        else
-                        {
-                            vec old = d->o;
-                            d->o = vec(above).add(vec(0, 0, d->height));
-                            bool col = collide(d, vec(0, 0, 1));
-                            d->o = old;
-                            if(col)
-                            {
-                                pos = above;
-                                return n;
-                            }
-                            else return retry ? n : -1;
-                        }
-                    }
-                }
-                cur = next;
-            }
-        }
-        return n;
-    }
-
-    static inline float heapscore(waypoint *q) { return q->score(); }
-
-    bool route(gameent *d, int node, int goal, vector<int> &route, const avoidset &obstacles, int retries)
-    {
-        if(waypoints.empty() || !iswaypoint(node) || !iswaypoint(goal) || goal == node || !waypoints[node].haslinks())
-            return false;
-
-        static ushort routeid = 1;
-        static vector<waypoint *> queue;
-
-        if(!routeid)
-        {
-            loopv(waypoints) waypoints[i].route = 0;
-            routeid = 1;
-        }
-
-        if(d)
-        {
-            if(retries <= 1 && d->ai) loopi(NUMPREVNODES) if(d->ai->prevnodes[i] != node && iswaypoint(d->ai->prevnodes[i]))
-            {
-                waypoints[d->ai->prevnodes[i]].route = routeid;
-                waypoints[d->ai->prevnodes[i]].curscore = -1;
-                waypoints[d->ai->prevnodes[i]].estscore = 0;
-            }
-			if(retries <= 0)
-			{
-				loopavoid(obstacles, d,
-				{
-					if(iswaypoint(wp) && wp != node && wp != goal && waypoints[node].find(wp) < 0 && waypoints[goal].find(wp) < 0)
-					{
-						waypoints[wp].route = routeid;
-						waypoints[wp].curscore = -1;
-						waypoints[wp].estscore = 0;
-					}
-				});
-			}
-        }
-
-        waypoints[node].route = routeid;
-        waypoints[node].curscore = waypoints[node].estscore = 0;
-        waypoints[node].prev = 0;
-        queue.setsize(0);
-        queue.add(&waypoints[node]);
-        route.setsize(0);
-
-        int lowest = -1;
-        while(!queue.empty())
-        {
-            waypoint &m = *queue.removeheap();
-            float prevscore = m.curscore;
-            m.curscore = -1;
-            loopi(MAXWAYPOINTLINKS)
-            {
-                int link = m.links[i];
-                if(!link) break;
-                if(iswaypoint(link) && (link == node || link == goal || waypoints[link].haslinks()))
-                {
-                    waypoint &n = waypoints[link];
-                    int weight = max(n.weight, 1);
-                    float curscore = prevscore + n.o.dist(m.o)*weight;
-                    if(n.route == routeid && curscore >= n.curscore) continue;
-                    n.curscore = curscore;
-                    n.prev = ushort(&m - &waypoints[0]);
-                    if(n.route != routeid)
-                    {
-                        n.estscore = n.o.dist(waypoints[goal].o)*weight;
-                        if(n.estscore <= WAYPOINTRADIUS*4 && (lowest < 0 || n.estscore <= waypoints[lowest].estscore))
-                            lowest = link;
-                        n.route = routeid;
-                        if(link == goal) goto foundgoal;
-                        queue.addheap(&n);
-                    }
-                    else loopvj(queue) if(queue[j] == &n) { queue.upheap(j); break; }
-                }
-            }
-        }
-        foundgoal:
-
-        routeid++;
-
-        if(lowest >= 0) // otherwise nothing got there
-        {
-            for(waypoint *m = &waypoints[lowest]; m > &waypoints[0]; m = &waypoints[m->prev])
-                route.add(m - &waypoints[0]); // just keep it stored backward
-        }
-
-        return !route.empty();
-    }
-
-    string loadedwaypoints = "";
-    VARF(0, dropwaypoints, 0, 0, 1, if(dropwaypoints) getwaypoints());
-
-    int addwaypoint(const vec &o, int weight = -1)
-    {
-        if(waypoints.length() > MAXWAYPOINTS) return -1;
-        int n = waypoints.length();
-        waypoints.add(waypoint(o, weight >= 0 ? weight : getweight(o)));
-        return n;
-    }
-
-    void linkwaypoint(waypoint &a, int n)
-    {
-        loopi(MAXWAYPOINTLINKS)
-        {
-            if(a.links[i] == n) return;
-            if(!a.links[i]) { a.links[i] = n; return; }
-        }
-        a.links[rnd(MAXWAYPOINTLINKS)] = n;
-    }
-
-    static inline bool shouldnavigate()
-    {
-        if(dropwaypoints) return true;
-        loopvrev(players) if(players[i] && players[i]->aitype != AI_NONE) return true;
-        return false;
-    }
-
-    static inline bool shoulddrop(gameent *d)
-    {
-        return !d->ai && (dropwaypoints || !loadedwaypoints[0]);
-    }
-
-    void inferwaypoints(gameent *d, const vec &o, const vec &v, float mindist)
-    {
-        if(!shouldnavigate()) return;
-    	if(shoulddrop(d) && !clipped(o) && !clipped(v))
-    	{
-			int from = closestwaypoint(o, mindist, false), to = closestwaypoint(v, mindist, false);
-			if(!iswaypoint(from)) from = addwaypoint(o);
-			if(!iswaypoint(to)) to = addwaypoint(v);
-			if(d->lastnode != from && iswaypoint(d->lastnode) && iswaypoint(from))
-				linkwaypoint(waypoints[d->lastnode], from);
-			if(iswaypoint(to))
-			{
-				if(from != to && iswaypoint(from) && iswaypoint(to))
-					linkwaypoint(waypoints[from], to);
-				d->lastnode = to;
-			}
-		}
-		else d->lastnode = closestwaypoint(v, CLOSEDIST, false);
-    }
-
-    void navigate(gameent *d)
-    {
-        if(d->state != CS_ALIVE) { d->lastnode = -1; return; }
-        vec v(d->feetpos());
-        bool dropping = shoulddrop(d) && !clipped(v);
-        float dist = dropping ? WAYPOINTRADIUS : CLOSEDIST;
-        int curnode = closestwaypoint(v, dist, false), prevnode = d->lastnode;
-        if(!iswaypoint(curnode) && dropping) curnode = addwaypoint(v);
-        if(iswaypoint(curnode))
-        {
-            if(dropping && d->lastnode != curnode && iswaypoint(d->lastnode))
-            {
-                linkwaypoint(waypoints[d->lastnode], curnode);
-                if(!d->timeinair) linkwaypoint(waypoints[curnode], d->lastnode);
-            }
-            d->lastnode = curnode;
-            if(d->ai && iswaypoint(prevnode) && d->lastnode != prevnode) d->ai->addprevnode(prevnode);
-        }
-        else if(!iswaypoint(d->lastnode) || waypoints[d->lastnode].o.squaredist(v) > dist*dist)
-        {
-            dist = physics::jetpack(d) ? JETDIST : RETRYDIST; // workaround
-			d->lastnode = closestwaypoint(v, dist, false);
-        }
-    }
-
-    void navigate()
-    {
-    	if(shouldnavigate())
-    	{
-    	    navigate(game::player1);
-    	    loopv(players) if(players[i]) navigate(players[i]);
-    	}
-        if(invalidatedwpcaches) clearwpcache(false);
-    }
-
-    void clearwaypoints(bool full)
-    {
-        waypoints.setsize(0);
-        clearwpcache();
-        if(full) loadedwaypoints[0] = '\0';
-    }
-    ICOMMAND(0, clearwaypoints, "", (), clearwaypoints());
-
-    void remapwaypoints()
-    {
-        vector<ushort> remap;
-        int total = 0;
-        loopv(waypoints) remap.add(waypoints[i].links[1] == 0xFFFF ? 0 : total++);
-        total = 0;
-        loopvj(waypoints)
-        {
-            if(waypoints[j].links[1] == 0xFFFF) continue;
-            waypoint &w = waypoints[total];
-            if(j != total) w = waypoints[j];
-            int k = 0;
-            loopi(MAXWAYPOINTLINKS)
-            {
-                int link = w.links[i];
-                if(!link) break;
-                if((w.links[k] = remap[link])) k++;
-            }
-            if(k < MAXWAYPOINTLINKS) w.links[k] = 0;
-            total++;
-        }
-        waypoints.setsize(total);
-    }
-
-    bool checkteleport(const vec &o, const vec &v)
-    {
-        if(o.dist(v) > CLOSEDIST)
-        {
-            loopi(entities::lastenttype[TELEPORT]) if(entities::ents[i]->type == TELEPORT)
-            {
-                gameentity &e = *(gameentity *)entities::ents[i];
-                if(o.dist(e.o) < (e.attrs[3] ? e.attrs[3] : enttype[e.type].radius)+CLOSEDIST)
-                {
-                    loopvj(e.links) if(entities::ents.inrange(e.links[j]) && entities::ents[e.links[j]]->type == TELEPORT)
-                    {
-                        gameentity &f = *(gameentity *)entities::ents[e.links[j]];
-                        if(v.dist(f.o) < (f.attrs[3] ? f.attrs[3] : enttype[f.type].radius)+CLOSEDIST) return true;
-                    }
-                }
-            }
-            return false;
-        }
-        return true;
-    }
-
-    bool cleanwaypoints()
-    {
-        int cleared = 0;
-        loopv(waypoints)
-        {
-            waypoint &w = waypoints[i];
-            if(clipped(w.o))
-            {
-                w.links[0] = 0;
-                w.links[1] = 0xFFFF;
-                cleared++;
-            }
-            else loopk(MAXWAYPOINTLINKS)
-            {
-                int link = w.links[k];
-                if(!link) continue;
-                waypoint &v = waypoints[link];
-                if(!checkteleport(w.o, v.o))
-                {
-                    int highest = MAXWAYPOINTLINKS-1;
-                    loopj(MAXWAYPOINTLINKS) if(!w.links[j]) { highest = j-1; break; }
-                    w.links[k] = w.links[highest];
-                    w.links[highest] = 0;
-                    k--;
-                }
-            }
-        }
-        if(cleared)
-        {
-            player1->lastnode = -1;
-            loopv(players) if(players[i]) players[i]->lastnode = -1;
-            remapwaypoints();
-            clearwpcache();
-            return true;
-        }
-        return false;
-    }
-
-    bool getwaypointfile(const char *mname, char *wptname)
-    {
-        if(!mname || !*mname) mname = mapname;
-        if(!*mname) return false;
-        formatstring(wptname)("%s.wpt", mname);
-        path(wptname);
-        return true;
-    }
-
-    bool loadwaypoints(bool force, const char *mname)
-    {
-        string wptname;
-        if(!getwaypointfile(mname, wptname)) return false;
-        if(!force && (waypoints.length() || !strcmp(loadedwaypoints, wptname))) return true;
-
-        stream *f = opengzfile(wptname, "rb");
-        if(!f) return false;
-        char magic[4];
-        if(f->read(magic, 4) < 4 || memcmp(magic, "OWPT", 4)) { delete f; return false; }
-
-        copystring(loadedwaypoints, wptname);
-
-        waypoints.setsize(0);
-        waypoints.add(vec(0, 0, 0));
-        ushort numwp = f->getlil<ushort>();
-        loopi(numwp)
-        {
-            if(f->end()) break;
-            vec o;
-            o.x = f->getlil<float>();
-            o.y = f->getlil<float>();
-            o.z = f->getlil<float>();
-            waypoint &w = waypoints.add(waypoint(o, getweight(o)));
-            int numlinks = f->getchar(), k = 0;
-            loopi(numlinks)
-            {
-                if((w.links[k] = f->getlil<ushort>()))
-                {
-                    if(++k >= MAXWAYPOINTLINKS) break;
-                }
-            }
-        }
-
-        delete f;
-        conoutf("loaded %d waypoints from %s", numwp, wptname);
-
-        if(!cleanwaypoints()) clearwpcache();
-        return true;
-    }
-    ICOMMAND(0, loadwaypoints, "s", (char *mname), getwaypoints(true, mname));
-
-    void savewaypoints(bool force, const char *mname)
-    {
-        if((!dropwaypoints && !force) || waypoints.empty()) return;
-
-        string wptname;
-        if(!getwaypointfile(mname, wptname)) return;
-
-        stream *f = opengzfile(wptname, "wb");
-        if(!f) return;
-        f->write("OWPT", 4);
-        f->putlil<ushort>(waypoints.length()-1);
-        for(int i = 1; i < waypoints.length(); i++)
-        {
-            waypoint &w = waypoints[i];
-            f->putlil<float>(w.o.x);
-            f->putlil<float>(w.o.y);
-            f->putlil<float>(w.o.z);
-            int numlinks = 0;
-            loopj(MAXWAYPOINTLINKS) { if(!w.links[j]) break; numlinks++; }
-            f->putchar(numlinks);
-            loopj(numlinks) f->putlil<ushort>(w.links[j]);
-        }
-
-        delete f;
-        conoutf("saved %d waypoints to %s", waypoints.length()-1, wptname);
-    }
-
-    ICOMMAND(0, savewaypoints, "s", (char *mname), savewaypoints(true, mname));
-
-    bool importwaypoints()
-    {
-        if(oldwaypoints.empty()) return false;
-        string wptname;
-        if(getwaypointfile(mapname, wptname)) copystring(loadedwaypoints, wptname);
-        waypoints.setsize(0);
-        waypoints.add(vec(0, 0, 0));
-        loopv(oldwaypoints)
-        {
-            oldwaypoint &v = oldwaypoints[i];
-            loopvj(v.links) loopvk(oldwaypoints) if(v.links[j] == oldwaypoints[k].ent)
-            {
-                v.links[j] = k+1;
-                break;
-            }
-            waypoint &w = waypoints.add(waypoint(v.o, getweight(v.o)));
-            int k = 0;
-            loopvj(v.links)
-            {
-                if((w.links[k] = v.links[j]))
-                {
-                    if(++k >= MAXWAYPOINTLINKS) break;
-                }
-            }
-        }
-        conoutf("imported %d waypoints from the map file", oldwaypoints.length());
-        oldwaypoints.setsize(0);
-        if(!cleanwaypoints()) clearwpcache();
-        return true;
-    }
-
-    bool getwaypoints(bool force, const char *mname, bool check)
-    {
-        if(check && loadedwaypoints[0]) return false;
-        return loadwaypoints(force, mname) || importwaypoints();
-    }
-
-    void delselwaypoints()
-    {
-        if(noedit(true)) return;
-        vec o = sel.o.tovec().sub(0.1f), s = sel.s.tovec().mul(sel.grid).add(o).add(0.1f);
-        int cleared = 0;
-        loopv(waypoints)
-        {
-            waypoint &w = waypoints[i];
-            if(w.o.x >= o.x && w.o.x <= s.x && w.o.y >= o.y && w.o.y <= s.y && w.o.z >= o.z && w.o.z <= s.z)
-            {
-                w.links[0] = 0;
-                w.links[1] = 0xFFFF;
-                cleared++;
-            }
-        }
-        if(cleared)
-        {
-            player1->lastnode = -1;
-            remapwaypoints();
-            clearwpcache();
-        }
-    }
-    COMMAND(0, delselwaypoints, "");
-}
-
+#include "game.h"
+
+extern selinfo sel;
+
+namespace ai
+{
+    using namespace game;
+
+    vector<waypoint> waypoints;
+    vector<oldwaypoint> oldwaypoints;
+
+    bool clipped(const vec &o)
+    {
+        int material = lookupmaterial(o), clipmat = material&MATF_CLIP;
+        return clipmat == MAT_CLIP || clipmat == MAT_AICLIP || material&MAT_DEATH || (material&MATF_VOLUME) == MAT_LAVA;
+    }
+
+    int getweight(const vec &o)
+    {
+        vec pos = o; pos.z += JUMPMIN;
+        if(!insideworld(vec(pos.x, pos.y, min(pos.z, getworldsize() - 1e-3f)))) return -2;
+        float dist = raycube(pos, vec(0, 0, -1), 0, RAY_CLIPMAT);
+        int posmat = lookupmaterial(pos), weight = 1;
+        if(isliquid(posmat&MATF_VOLUME)) weight *= 5;
+        if(dist >= 0)
+        {
+            weight = int(dist/JUMPMIN);
+            pos.z -= clamp(dist-8.0f, 0.0f, pos.z);
+            int trgmat = lookupmaterial(pos);
+            if(trgmat&MAT_DEATH || (trgmat&MATF_VOLUME) == MAT_LAVA) weight *= 10;
+            else if(isliquid(trgmat&MATF_VOLUME)) weight *= 2;
+        }
+        return weight;
+    }
+
+    enum
+    {
+        WPCACHE_STATIC = 0,
+        WPCACHE_DYNAMIC,
+        NUMWPCACHES
+    };
+
+    struct wpcachenode
+    {
+        float split[2];
+        uint child[2];
+
+        int axis() const { return child[0]>>30; }
+        int childindex(int which) const { return child[which]&0x3FFFFFFF; }
+        bool isleaf(int which) const { return (child[1]&(1<<(30+which)))!=0; }
+    };
+
+    struct wpcache
+    {
+        vector<wpcachenode> nodes;
+        int firstwp, lastwp, maxdepth;
+        vec bbmin, bbmax;
+
+        wpcache() { clear(); }
+
+        void clear()
+        {
+            nodes.setsize(0);
+            firstwp = lastwp = -1;
+            maxdepth = -1;
+            bbmin = vec(1e16f, 1e16f, 1e16f);
+            bbmax = vec(-1e16f, -1e16f, -1e16f);
+        }
+
+        void build(int first = -1, int last = -1)
+        {
+            if(last < 0) last = waypoints.length();
+            vector<int> indices;
+            for(int i = first; i < last; i++)
+            {
+                waypoint &w = waypoints[i];
+                indices.add(i);
+                if(firstwp < 0) firstwp = i;
+                float radius = WAYPOINTRADIUS;
+                bbmin.min(vec(w.o).sub(radius));
+                bbmax.max(vec(w.o).add(radius));
+            }
+            build(indices.getbuf(), indices.length(), bbmin, bbmax);
+        }
+
+        void build(int *indices, int numindices, const vec &vmin, const vec &vmax, int depth = 1)
+        {
+            int axis = 2;
+            loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k;
+
+            vec leftmin(1e16f, 1e16f, 1e16f), leftmax(-1e16f, -1e16f, -1e16f), rightmin(1e16f, 1e16f, 1e16f), rightmax(-1e16f, -1e16f, -1e16f);
+            float split = 0.5f*(vmax[axis] + vmin[axis]), splitleft = -1e16f, splitright = 1e16f;
+            int left, right;
+            for(left = 0, right = numindices; left < right;)
+            {
+                waypoint &w = waypoints[indices[left]];
+                float radius = WAYPOINTRADIUS;
+                if(max(split - (w.o[axis]-radius), 0.0f) > max((w.o[axis]+radius) - split, 0.0f))
+                {
+                    ++left;
+                    splitleft = max(splitleft, w.o[axis]+radius);
+                    leftmin.min(vec(w.o).sub(radius));
+                    leftmax.max(vec(w.o).add(radius));
+                }
+                else
+                {
+                    --right;
+                    swap(indices[left], indices[right]);
+                    splitright = min(splitright, w.o[axis]-radius);
+                    rightmin.min(vec(w.o).sub(radius));
+                    rightmax.max(vec(w.o).add(radius));
+                }
+            }
+
+            if(!left || right==numindices)
+            {
+                leftmin = rightmin = vec(1e16f, 1e16f, 1e16f);
+                leftmax = rightmax = vec(-1e16f, -1e16f, -1e16f);
+                left = right = numindices/2;
+                splitleft = -1e16f;
+                splitright = 1e16f;
+                loopi(numindices)
+                {
+                    waypoint &w = waypoints[indices[i]];
+                    float radius = WAYPOINTRADIUS;
+                    if(i < left)
+                    {
+                        splitleft = max(splitleft, w.o[axis]+radius);
+                        leftmin.min(vec(w.o).sub(radius));
+                        leftmax.max(vec(w.o).add(radius));
+                    }
+                    else
+                    {
+                        splitright = min(splitright, w.o[axis]-radius);
+                        rightmin.min(vec(w.o).sub(radius));
+                        rightmax.max(vec(w.o).add(radius));
+                    }
+                }
+            }
+
+            int node = nodes.length();
+            nodes.add();
+            nodes[node].split[0] = splitleft;
+            nodes[node].split[1] = splitright;
+
+            if(left<=1) nodes[node].child[0] = (axis<<30) | (left>0 ? indices[0] : 0x3FFFFFFF);
+            else
+            {
+                nodes[node].child[0] = (axis<<30) | (nodes.length()-node);
+                if(left) build(indices, left, leftmin, leftmax, depth+1);
+            }
+
+            if(numindices-right<=1) nodes[node].child[1] = (1<<31) | (left<=1 ? 1<<30 : 0) | (numindices-right>0 ? indices[right] : 0x3FFFFFFF);
+            else
+            {
+                nodes[node].child[1] = (left<=1 ? 1<<30 : 0) | (nodes.length()-node);
+                if(numindices-right) build(&indices[right], numindices-right, rightmin, rightmax, depth+1);
+            }
+
+            maxdepth = max(maxdepth, depth);
+        }
+    } wpcaches[NUMWPCACHES];
+
+    static int invalidatedwpcaches = 0, clearedwpcaches = (1<<NUMWPCACHES)-1, numinvalidatewpcaches = 0, lastwpcache = 0;
+
+    static inline void invalidatewpcache(int wp)
+    {
+        if(++numinvalidatewpcaches >= 1000) { numinvalidatewpcaches = 0; invalidatedwpcaches = (1<<NUMWPCACHES)-1; }
+        else loopi(NUMWPCACHES) if((wp >= wpcaches[i].firstwp && wp <= wpcaches[i].lastwp) || i+1 >= NUMWPCACHES) { invalidatedwpcaches |= 1<<i; break; }
+    }
+
+    void clearwpcache(bool full = true)
+	{
+        loopi(NUMWPCACHES) if(full || invalidatedwpcaches&(1<<i)) { wpcaches[i].clear(); clearedwpcaches |= 1<<i; }
+        if(full || invalidatedwpcaches == (1<<NUMWPCACHES)-1)
+        {
+            numinvalidatewpcaches = 0;
+            lastwpcache = 0;
+        }
+        invalidatedwpcaches = 0;
+	}
+    ICOMMAND(0, clearwpcache, "", (), clearwpcache());
+
+    void buildwpcache()
+    {
+        loopi(NUMWPCACHES) if(wpcaches[i].maxdepth < 0)
+            wpcaches[i].build(i > 0 ? wpcaches[i-1].lastwp+1 : 0, i+1 >= NUMWPCACHES || wpcaches[i+1].maxdepth < 0 ? -1 : wpcaches[i+1].firstwp);
+        clearedwpcaches = 0;
+        lastwpcache = waypoints.length();
+
+		wpavoid.clear();
+		loopv(waypoints) if(waypoints[i].weight < 0) wpavoid.avoidnear(NULL, waypoints[i].o.z + WAYPOINTRADIUS, waypoints[i].o, WAYPOINTRADIUS);
+    }
+
+    struct wpcachestack
+    {
+        wpcachenode *node;
+        float tmin, tmax;
+    };
+
+    vector<wpcachenode *> wpcachestack;
+
+    int closestwaypoint(const vec &pos, float mindist, bool links)
+    {
+        if(waypoints.empty()) return -1;
+        if(clearedwpcaches) buildwpcache();
+
+        #define CHECKCLOSEST(index) do { \
+            int n = (index); \
+            waypoint &w = waypoints[n]; \
+            if(!links || w.haslinks()) \
+            { \
+                float dist = w.o.squaredist(pos); \
+                if(dist < mindist*mindist) { closest = n; mindist = sqrtf(dist); } \
+            } \
+        } while(0)
+        int closest = -1;
+        wpcachenode *curnode;
+        loop(which, NUMWPCACHES) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
+        {
+            int axis = curnode->axis();
+            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
+            if(dist1 >= mindist)
+            {
+                if(dist2 < mindist)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKCLOSEST(curnode->childindex(1));
+                }
+            }
+            else if(curnode->isleaf(0))
+            {
+                CHECKCLOSEST(curnode->childindex(0));
+                if(dist2 < mindist)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKCLOSEST(curnode->childindex(1));
+                }
+            }
+            else
+            {
+                if(dist2 < mindist)
+                {
+                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
+                    else CHECKCLOSEST(curnode->childindex(1));
+                }
+                curnode += curnode->childindex(0);
+                continue;
+            }
+            if(wpcachestack.empty()) break;
+            curnode = wpcachestack.pop();
+        }
+        for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKCLOSEST(i); }
+        return closest;
+    }
+
+    void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results)
+    {
+        if(waypoints.empty()) return;
+        if(clearedwpcaches) buildwpcache();
+
+        float mindist2 = mindist*mindist, maxdist2 = maxdist*maxdist;
+        #define CHECKWITHIN(index) do { \
+            int n = (index); \
+            const waypoint &w = waypoints[n]; \
+            float dist = w.o.squaredist(pos); \
+            if(dist > mindist2 && dist < maxdist2) results.add(n); \
+        } while(0)
+        wpcachenode *curnode;
+        loop(which, NUMWPCACHES) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
+        {
+            int axis = curnode->axis();
+            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
+            if(dist1 >= maxdist)
+            {
+                if(dist2 < maxdist)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKWITHIN(curnode->childindex(1));
+                }
+            }
+            else if(curnode->isleaf(0))
+            {
+                CHECKWITHIN(curnode->childindex(0));
+                if(dist2 < maxdist)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKWITHIN(curnode->childindex(1));
+                }
+            }
+            else
+            {
+                if(dist2 < maxdist)
+                {
+                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
+                    else CHECKWITHIN(curnode->childindex(1));
+                }
+                curnode += curnode->childindex(0);
+                continue;
+            }
+            if(wpcachestack.empty()) break;
+            curnode = wpcachestack.pop();
+        }
+        for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKWITHIN(i); }
+    }
+
+    void avoidset::avoidnear(void *owner, float above, const vec &pos, float limit)
+    {
+        if(ai::waypoints.empty()) return;
+        if(clearedwpcaches) buildwpcache();
+
+        float limit2 = limit*limit;
+        #define CHECKNEAR(index) do { \
+            int n = (index); \
+            const waypoint &w = ai::waypoints[n]; \
+            if(w.o.squaredist(pos) < limit2) add(owner, above, n); \
+        } while(0)
+        wpcachenode *curnode;
+        loop(which, NUMWPCACHES) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
+        {
+            int axis = curnode->axis();
+            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
+            if(dist1 >= limit)
+            {
+                if(dist2 < limit)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKNEAR(curnode->childindex(1));
+                }
+            }
+            else if(curnode->isleaf(0))
+            {
+                CHECKNEAR(curnode->childindex(0));
+                if(dist2 < limit)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKNEAR(curnode->childindex(1));
+                }
+            }
+            else
+            {
+                if(dist2 < limit)
+                {
+                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
+                    else CHECKNEAR(curnode->childindex(1));
+                }
+                curnode += curnode->childindex(0);
+                continue;
+            }
+            if(wpcachestack.empty()) break;
+            curnode = wpcachestack.pop();
+        }
+        for(int i = lastwpcache; i < ai::waypoints.length(); i++) { CHECKNEAR(i); }
+    }
+
+    int avoidset::remap(gameent *d, int n, vec &pos, bool retry)
+    {
+        if(!obstacles.empty())
+        {
+            int cur = 0;
+            loopv(obstacles)
+            {
+                obstacle &ob = obstacles[i];
+                int next = cur + ob.numwaypoints;
+                if(ob.owner != d)
+                {
+                    for(; cur < next; cur++) if(waypoints[cur] == n)
+                    {
+                        if(ob.above < 0) return retry ? n : -1;
+                        vec above(pos.x, pos.y, ob.above);
+                        if(above.z-d->o.z >= ai::JUMPMAX)
+                            return retry ? n : -1; // too much scotty
+                        int node = closestwaypoint(above, ai::CLOSEDIST, true);
+                        if(ai::iswaypoint(node) && node != n)
+                        { // try to reroute above their head?
+                            if(!find(node, d))
+                            {
+                                pos = ai::waypoints[node].o;
+                                return node;
+                            }
+                            else return retry ? n : -1;
+                        }
+                        else
+                        {
+                            vec old = d->o;
+                            d->o = vec(above).add(vec(0, 0, d->height));
+                            bool col = collide(d, vec(0, 0, 1));
+                            d->o = old;
+                            if(col)
+                            {
+                                pos = above;
+                                return n;
+                            }
+                            else return retry ? n : -1;
+                        }
+                    }
+                }
+                cur = next;
+            }
+        }
+        return n;
+    }
+
+    static inline float heapscore(waypoint *q) { return q->score(); }
+
+    bool route(gameent *d, int node, int goal, vector<int> &route, const avoidset &obstacles, int retries)
+    {
+        if(waypoints.empty() || !iswaypoint(node) || !iswaypoint(goal) || goal == node || !waypoints[node].haslinks())
+            return false;
+
+        static ushort routeid = 1;
+        static vector<waypoint *> queue;
+
+        if(!routeid)
+        {
+            loopv(waypoints) waypoints[i].route = 0;
+            routeid = 1;
+        }
+
+        if(d)
+        {
+            if(retries <= 1 && d->ai) loopi(NUMPREVNODES) if(d->ai->prevnodes[i] != node && iswaypoint(d->ai->prevnodes[i]))
+            {
+                waypoints[d->ai->prevnodes[i]].route = routeid;
+                waypoints[d->ai->prevnodes[i]].curscore = -1;
+                waypoints[d->ai->prevnodes[i]].estscore = 0;
+            }
+			if(retries <= 0)
+			{
+				loopavoid(obstacles, d,
+				{
+					if(iswaypoint(wp) && wp != node && wp != goal && waypoints[node].find(wp) < 0 && waypoints[goal].find(wp) < 0)
+					{
+						waypoints[wp].route = routeid;
+						waypoints[wp].curscore = -1;
+						waypoints[wp].estscore = 0;
+					}
+				});
+			}
+        }
+
+        waypoints[node].route = routeid;
+        waypoints[node].curscore = waypoints[node].estscore = 0;
+        waypoints[node].prev = 0;
+        queue.setsize(0);
+        queue.add(&waypoints[node]);
+        route.setsize(0);
+
+        int lowest = -1;
+        while(!queue.empty())
+        {
+            waypoint &m = *queue.removeheap();
+            float prevscore = m.curscore;
+            m.curscore = -1;
+            loopi(MAXWAYPOINTLINKS)
+            {
+                int link = m.links[i];
+                if(!link) break;
+                if(iswaypoint(link) && (link == node || link == goal || waypoints[link].haslinks()))
+                {
+                    waypoint &n = waypoints[link];
+                    int weight = max(n.weight, 1);
+                    float curscore = prevscore + n.o.dist(m.o)*weight;
+                    if(n.route == routeid && curscore >= n.curscore) continue;
+                    n.curscore = curscore;
+                    n.prev = ushort(&m - &waypoints[0]);
+                    if(n.route != routeid)
+                    {
+                        n.estscore = n.o.dist(waypoints[goal].o)*weight;
+                        if(n.estscore <= WAYPOINTRADIUS*4 && (lowest < 0 || n.estscore <= waypoints[lowest].estscore))
+                            lowest = link;
+                        n.route = routeid;
+                        if(link == goal) goto foundgoal;
+                        queue.addheap(&n);
+                    }
+                    else loopvj(queue) if(queue[j] == &n) { queue.upheap(j); break; }
+                }
+            }
+        }
+        foundgoal:
+
+        routeid++;
+
+        if(lowest >= 0) // otherwise nothing got there
+        {
+            for(waypoint *m = &waypoints[lowest]; m > &waypoints[0]; m = &waypoints[m->prev])
+                route.add(m - &waypoints[0]); // just keep it stored backward
+        }
+
+        return !route.empty();
+    }
+
+    string loadedwaypoints = "";
+    VARF(0, dropwaypoints, 0, 0, 1, if(dropwaypoints) getwaypoints());
+
+    int addwaypoint(const vec &o, int weight = -1)
+    {
+        if(waypoints.length() > MAXWAYPOINTS) return -1;
+        int n = waypoints.length();
+        waypoints.add(waypoint(o, weight >= 0 ? weight : getweight(o)));
+        return n;
+    }
+
+    void linkwaypoint(waypoint &a, int n)
+    {
+        loopi(MAXWAYPOINTLINKS)
+        {
+            if(a.links[i] == n) return;
+            if(!a.links[i]) { a.links[i] = n; return; }
+        }
+        a.links[rnd(MAXWAYPOINTLINKS)] = n;
+    }
+
+    static inline bool shouldnavigate()
+    {
+        if(dropwaypoints) return true;
+        loopvrev(players) if(players[i] && players[i]->aitype != AI_NONE) return true;
+        return false;
+    }
+
+    static inline bool shoulddrop(gameent *d)
+    {
+        return !d->ai && (dropwaypoints || !loadedwaypoints[0]);
+    }
+
+    void inferwaypoints(gameent *d, const vec &o, const vec &v, float mindist)
+    {
+        if(!shouldnavigate()) return;
+    	if(shoulddrop(d) && !clipped(o) && !clipped(v))
+    	{
+			int from = closestwaypoint(o, mindist, false), to = closestwaypoint(v, mindist, false);
+			if(!iswaypoint(from)) from = addwaypoint(o);
+			if(!iswaypoint(to)) to = addwaypoint(v);
+			if(d->lastnode != from && iswaypoint(d->lastnode) && iswaypoint(from))
+				linkwaypoint(waypoints[d->lastnode], from);
+			if(iswaypoint(to))
+			{
+				if(from != to && iswaypoint(from) && iswaypoint(to))
+					linkwaypoint(waypoints[from], to);
+				d->lastnode = to;
+			}
+		}
+		else d->lastnode = closestwaypoint(v, CLOSEDIST, false);
+    }
+
+    void navigate(gameent *d)
+    {
+        if(d->state != CS_ALIVE) { d->lastnode = -1; return; }
+        vec v(d->feetpos());
+        bool dropping = shoulddrop(d) && !clipped(v);
+        float dist = dropping ? WAYPOINTRADIUS : CLOSEDIST;
+        int curnode = closestwaypoint(v, dist, false), prevnode = d->lastnode;
+        if(!iswaypoint(curnode) && dropping) curnode = addwaypoint(v);
+        if(iswaypoint(curnode))
+        {
+            if(dropping && d->lastnode != curnode && iswaypoint(d->lastnode))
+            {
+                linkwaypoint(waypoints[d->lastnode], curnode);
+                if(!d->timeinair) linkwaypoint(waypoints[curnode], d->lastnode);
+            }
+            d->lastnode = curnode;
+            if(d->ai && iswaypoint(prevnode) && d->lastnode != prevnode) d->ai->addprevnode(prevnode);
+        }
+        else if(!iswaypoint(d->lastnode) || waypoints[d->lastnode].o.squaredist(v) > dist*dist)
+        {
+            dist = physics::jetpack(d) ? JETDIST : RETRYDIST; // workaround
+			d->lastnode = closestwaypoint(v, dist, false);
+        }
+    }
+
+    void navigate()
+    {
+    	if(shouldnavigate())
+    	{
+    	    navigate(game::player1);
+    	    loopv(players) if(players[i]) navigate(players[i]);
+    	}
+        if(invalidatedwpcaches) clearwpcache(false);
+    }
+
+    void clearwaypoints(bool full)
+    {
+        waypoints.setsize(0);
+        clearwpcache();
+        if(full) loadedwaypoints[0] = '\0';
+    }
+    ICOMMAND(0, clearwaypoints, "", (), clearwaypoints());
+
+    void remapwaypoints()
+    {
+        vector<ushort> remap;
+        int total = 0;
+        loopv(waypoints) remap.add(waypoints[i].links[1] == 0xFFFF ? 0 : total++);
+        total = 0;
+        loopvj(waypoints)
+        {
+            if(waypoints[j].links[1] == 0xFFFF) continue;
+            waypoint &w = waypoints[total];
+            if(j != total) w = waypoints[j];
+            int k = 0;
+            loopi(MAXWAYPOINTLINKS)
+            {
+                int link = w.links[i];
+                if(!link) break;
+                if((w.links[k] = remap[link])) k++;
+            }
+            if(k < MAXWAYPOINTLINKS) w.links[k] = 0;
+            total++;
+        }
+        waypoints.setsize(total);
+    }
+
+    bool checkteleport(const vec &o, const vec &v)
+    {
+        if(o.dist(v) > CLOSEDIST)
+        {
+            loopi(entities::lastenttype[TELEPORT]) if(entities::ents[i]->type == TELEPORT)
+            {
+                gameentity &e = *(gameentity *)entities::ents[i];
+                if(o.dist(e.o) < (e.attrs[3] ? e.attrs[3] : enttype[e.type].radius)+CLOSEDIST)
+                {
+                    loopvj(e.links) if(entities::ents.inrange(e.links[j]) && entities::ents[e.links[j]]->type == TELEPORT)
+                    {
+                        gameentity &f = *(gameentity *)entities::ents[e.links[j]];
+                        if(v.dist(f.o) < (f.attrs[3] ? f.attrs[3] : enttype[f.type].radius)+CLOSEDIST) return true;
+                    }
+                }
+            }
+            return false;
+        }
+        return true;
+    }
+
+    bool cleanwaypoints()
+    {
+        int cleared = 0;
+        loopv(waypoints)
+        {
+            waypoint &w = waypoints[i];
+            if(clipped(w.o))
+            {
+                w.links[0] = 0;
+                w.links[1] = 0xFFFF;
+                cleared++;
+            }
+            else loopk(MAXWAYPOINTLINKS)
+            {
+                int link = w.links[k];
+                if(!link) continue;
+                waypoint &v = waypoints[link];
+                if(!checkteleport(w.o, v.o))
+                {
+                    int highest = MAXWAYPOINTLINKS-1;
+                    loopj(MAXWAYPOINTLINKS) if(!w.links[j]) { highest = j-1; break; }
+                    w.links[k] = w.links[highest];
+                    w.links[highest] = 0;
+                    k--;
+                }
+            }
+        }
+        if(cleared)
+        {
+            player1->lastnode = -1;
+            loopv(players) if(players[i]) players[i]->lastnode = -1;
+            remapwaypoints();
+            clearwpcache();
+            return true;
+        }
+        return false;
+    }
+
+    bool getwaypointfile(const char *mname, char *wptname)
+    {
+        if(!mname || !*mname) mname = mapname;
+        if(!*mname) return false;
+        formatstring(wptname)("%s.wpt", mname);
+        path(wptname);
+        return true;
+    }
+
+    bool loadwaypoints(bool force, const char *mname)
+    {
+        string wptname;
+        if(!getwaypointfile(mname, wptname)) return false;
+        if(!force && (waypoints.length() || !strcmp(loadedwaypoints, wptname))) return true;
+
+        stream *f = opengzfile(wptname, "rb");
+        if(!f) return false;
+        char magic[4];
+        if(f->read(magic, 4) < 4 || memcmp(magic, "OWPT", 4)) { delete f; return false; }
+
+        copystring(loadedwaypoints, wptname);
+
+        waypoints.setsize(0);
+        waypoints.add(vec(0, 0, 0));
+        ushort numwp = f->getlil<ushort>();
+        loopi(numwp)
+        {
+            if(f->end()) break;
+            vec o;
+            o.x = f->getlil<float>();
+            o.y = f->getlil<float>();
+            o.z = f->getlil<float>();
+            waypoint &w = waypoints.add(waypoint(o, getweight(o)));
+            int numlinks = f->getchar(), k = 0;
+            loopi(numlinks)
+            {
+                if((w.links[k] = f->getlil<ushort>()))
+                {
+                    if(++k >= MAXWAYPOINTLINKS) break;
+                }
+            }
+        }
+
+        delete f;
+        conoutf("loaded %d waypoints from %s", numwp, wptname);
+
+        if(!cleanwaypoints()) clearwpcache();
+        return true;
+    }
+    ICOMMAND(0, loadwaypoints, "s", (char *mname), getwaypoints(true, mname));
+
+    void savewaypoints(bool force, const char *mname)
+    {
+        if((!dropwaypoints && !force) || waypoints.empty()) return;
+
+        string wptname;
+        if(!getwaypointfile(mname, wptname)) return;
+
+        stream *f = opengzfile(wptname, "wb");
+        if(!f) return;
+        f->write("OWPT", 4);
+        f->putlil<ushort>(waypoints.length()-1);
+        for(int i = 1; i < waypoints.length(); i++)
+        {
+            waypoint &w = waypoints[i];
+            f->putlil<float>(w.o.x);
+            f->putlil<float>(w.o.y);
+            f->putlil<float>(w.o.z);
+            int numlinks = 0;
+            loopj(MAXWAYPOINTLINKS) { if(!w.links[j]) break; numlinks++; }
+            f->putchar(numlinks);
+            loopj(numlinks) f->putlil<ushort>(w.links[j]);
+        }
+
+        delete f;
+        conoutf("saved %d waypoints to %s", waypoints.length()-1, wptname);
+    }
+
+    ICOMMAND(0, savewaypoints, "s", (char *mname), savewaypoints(true, mname));
+
+    bool importwaypoints()
+    {
+        if(oldwaypoints.empty()) return false;
+        string wptname;
+        if(getwaypointfile(mapname, wptname)) copystring(loadedwaypoints, wptname);
+        waypoints.setsize(0);
+        waypoints.add(vec(0, 0, 0));
+        loopv(oldwaypoints)
+        {
+            oldwaypoint &v = oldwaypoints[i];
+            loopvj(v.links) loopvk(oldwaypoints) if(v.links[j] == oldwaypoints[k].ent)
+            {
+                v.links[j] = k+1;
+                break;
+            }
+            waypoint &w = waypoints.add(waypoint(v.o, getweight(v.o)));
+            int k = 0;
+            loopvj(v.links)
+            {
+                if((w.links[k] = v.links[j]))
+                {
+                    if(++k >= MAXWAYPOINTLINKS) break;
+                }
+            }
+        }
+        conoutf("imported %d waypoints from the map file", oldwaypoints.length());
+        oldwaypoints.setsize(0);
+        if(!cleanwaypoints()) clearwpcache();
+        return true;
+    }
+
+    bool getwaypoints(bool force, const char *mname, bool check)
+    {
+        if(check && loadedwaypoints[0]) return false;
+        return loadwaypoints(force, mname) || importwaypoints();
+    }
+
+    void delselwaypoints()
+    {
+        if(noedit(true)) return;
+        vec o = sel.o.tovec().sub(0.1f), s = sel.s.tovec().mul(sel.grid).add(o).add(0.1f);
+        int cleared = 0;
+        loopv(waypoints)
+        {
+            waypoint &w = waypoints[i];
+            if(w.o.x >= o.x && w.o.x <= s.x && w.o.y >= o.y && w.o.y <= s.y && w.o.z >= o.z && w.o.z <= s.z)
+            {
+                w.links[0] = 0;
+                w.links[1] = 0xFFFF;
+                cleared++;
+            }
+        }
+        if(cleared)
+        {
+            player1->lastnode = -1;
+            remapwaypoints();
+            clearwpcache();
+        }
+    }
+    COMMAND(0, delselwaypoints, "");
+}
+
diff --git a/src/redeclipse.cbp b/src/redeclipse.cbp
index f9c616c..d6c10d4 100644
--- a/src/redeclipse.cbp
+++ b/src/redeclipse.cbp
@@ -15,7 +15,7 @@
 				<Option deps_output=".deps\client" />
 				<Option type="0" />
 				<Option compiler="gcc" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600" />
+				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
 				<Option projectIncludeDirsRelation="1" />
@@ -61,7 +61,7 @@
 				<Option deps_output=".deps\debug" />
 				<Option type="0" />
 				<Option compiler="gcc" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600" />
+				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
 				<Option projectIncludeDirsRelation="1" />
@@ -104,7 +104,7 @@
 				<Option deps_output=".deps\debug" />
 				<Option type="0" />
 				<Option compiler="gcc" />
-				<Option parameters="-hhome -rinit.cfg -du0 -df0 -dw640 -dh600" />
+				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
 				<Option projectIncludeDirsRelation="1" />
@@ -228,7 +228,7 @@
 				<Option deps_output=".deps\client" />
 				<Option type="0" />
 				<Option compiler="gnu_gcc_compiler_for_64bit" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600" />
+				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
 				<Option projectIncludeDirsRelation="1" />
@@ -274,7 +274,7 @@
 				<Option deps_output=".deps\debug" />
 				<Option type="0" />
 				<Option compiler="gnu_gcc_compiler_for_64bit" />
-				<Option parameters="-hhome -rinit.cfg -du0 -df0 -dw640 -dh600" />
+				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
 				<Option projectIncludeDirsRelation="1" />
@@ -318,7 +318,7 @@
 				<Option deps_output=".deps\debug" />
 				<Option type="0" />
 				<Option compiler="gnu_gcc_compiler_for_64bit" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600" />
+				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
 				<Option projectIncludeDirsRelation="1" />
@@ -527,9 +527,9 @@
 			</Target>
 		</Build>
 		<VirtualTargets>
-			<Add alias="All" targets="client32;server32;client64;server64;" />
 			<Add alias="32 bit" targets="client32;server32;" />
 			<Add alias="64 bit" targets="client64;server64;" />
+			<Add alias="All" targets="client32;server32;client64;server64;" />
 			<Add alias="Tools" targets="cube2font;genkey;" />
 		</VirtualTargets>
 		<Unit filename="engine\animmodel.h">
diff --git a/src/shared/cube2font.c b/src/shared/cube2font.c
index b6f00ed..cb62df2 100644
--- a/src/shared/cube2font.c
+++ b/src/shared/cube2font.c
@@ -1,555 +1,555 @@
-#include <stdlib.h>
-#include <string.h>
-#include <stdio.h>
-#include <stdarg.h>
-#include <limits.h>
-#include <zlib.h>
-#include <ft2build.h>
-#include FT_FREETYPE_H
-#include FT_STROKER_H
-#include FT_GLYPH_H
-
-typedef unsigned char uchar;
-typedef unsigned short ushort;
-typedef unsigned int uint;
-
-int imin(int a, int b) { return a < b ? a : b; }
-int imax(int a, int b) { return a > b ? a : b; }
-
-void fatal(const char *fmt, ...)
-{
-    va_list v;
-    va_start(v, fmt);
-    vfprintf(stderr, fmt, v);
-    va_end(v);
-    fputc('\n', stderr);
-
-    exit(EXIT_FAILURE);
-}
-
-uint bigswap(uint n)
-{
-    const int islittleendian = 1;
-    return *(const uchar *)&islittleendian ? (n<<24) | (n>>24) | ((n>>8)&0xFF00) | ((n<<8)&0xFF0000) : n;
-}
-
-size_t writebig(FILE *f, uint n)
-{
-    n = bigswap(n);
-    return fwrite(&n, 1, sizeof(n), f);
-}
-
-void writepngchunk(FILE *f, const char *type, uchar *data, uint len)
-{
-    uint crc;
-    writebig(f, len);
-    fwrite(type, 1, 4, f);
-    fwrite(data, 1, len, f);
-
-    crc = crc32(0, Z_NULL, 0);
-    crc = crc32(crc, (const Bytef *)type, 4);
-    if(data) crc = crc32(crc, data, len);
-    writebig(f, crc);
-}
-
-struct pngihdr
-{
-    uint width, height;
-    uchar bitdepth, colortype, compress, filter, interlace;
-};
-
-void savepng(const char *filename, uchar *data, int w, int h, int bpp, int flip)
-{
-    const uchar signature[] = { 137, 80, 78, 71, 13, 10, 26, 10 };
-    struct pngihdr ihdr;
-    FILE *f;
-    long idat;
-    uint len, crc;
-    z_stream z;
-    uchar buf[1<<12];
-    int i, j;
-
-    memset(&ihdr, 0, sizeof(ihdr));
-    ihdr.width = bigswap(w);
-    ihdr.height = bigswap(h);
-    ihdr.bitdepth = 8;
-    switch(bpp)
-    {
-        case 1: ihdr.colortype = 0; break;
-        case 2: ihdr.colortype = 4; break;
-        case 3: ihdr.colortype = 2; break;
-        case 4: ihdr.colortype = 6; break;
-        default: fatal("cube2font: invalid PNG bpp"); return;
-    }
-    f = fopen(filename, "wb");
-    if(!f) { fatal("cube2font: could not write to %s", filename); return; }
-
-    fwrite(signature, 1, sizeof(signature), f);
-
-    writepngchunk(f, "IHDR", (uchar *)&ihdr, 13);
-
-    idat = ftell(f);
-    len = 0;
-    fwrite("\0\0\0\0IDAT", 1, 8, f);
-    crc = crc32(0, Z_NULL, 0);
-    crc = crc32(crc, (const Bytef *)"IDAT", 4);
-
-    z.zalloc = NULL;
-    z.zfree = NULL;
-    z.opaque = NULL;
-
-    if(deflateInit(&z, Z_BEST_COMPRESSION) != Z_OK)
-        goto error;
-
-    z.next_out = (Bytef *)buf;
-    z.avail_out = sizeof(buf);
-
-    for(i = 0; i < h; i++)
-    {
-        uchar filter = 0;
-        for(j = 0; j < 2; j++)
-        {
-            z.next_in = j ? (Bytef *)data + (flip ? h-i-1 : i)*w*bpp : (Bytef *)&filter;
-            z.avail_in = j ? w*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; \
-                    fwrite(buf, 1, flush, f); \
-                    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);
-
-    fseek(f, idat, SEEK_SET);
-    writebig(f, len);
-    fseek(f, 0, SEEK_END);
-    writebig(f, crc);
-
-    writepngchunk(f, "IEND", NULL, 0);
-
-    fclose(f);
-    return;
-
-cleanuperror:
-    deflateEnd(&z);
-
-error:
-    fclose(f);
-
-    fatal("cube2font: failed saving PNG to %s", filename);
-}
-
-enum
-{
-    CT_PRINT   = 1<<0,
-    CT_SPACE   = 1<<1,
-    CT_DIGIT   = 1<<2,
-    CT_ALPHA   = 1<<3,
-    CT_LOWER   = 1<<4,
-    CT_UPPER   = 1<<5,
-    CT_UNICODE = 1<<6
-};
-#define CUBECTYPE(s, p, d, a, A, u, U) \
-    0, U, U, U, U, U, U, U, U, s, s, s, s, s, U, U, \
-    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, \
-    s, p, p, p, p, p, p, p, p, p, p, p, p, p, p, p, \
-    d, d, d, d, d, d, d, d, d, d, p, p, p, p, p, p, \
-    p, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, \
-    A, A, A, A, A, A, A, A, A, A, A, p, p, p, p, p, \
-    p, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, \
-    a, a, a, a, a, a, a, a, a, a, a, p, p, p, p, U, \
-    U, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, \
-    u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, U, \
-    u, U, u, U, u, U, u, U, u, U, u, U, u, U, u, U, \
-    u, U, u, U, u, U, u, U, u, U, u, U, u, U, u, U, \
-    u, U, u, U, u, U, u, U, U, u, U, u, U, u, U, U, \
-    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, \
-    U, U, U, U, u, u, u, u, u, u, u, u, u, u, u, u, \
-    u, u, u, u, u, u, u, u, u, u, u, u, u, u, U, u
-const uchar cubectype[256] =
-{
-    CUBECTYPE(CT_SPACE,
-              CT_PRINT,
-              CT_PRINT|CT_DIGIT,
-              CT_PRINT|CT_ALPHA|CT_LOWER,
-              CT_PRINT|CT_ALPHA|CT_UPPER,
-              CT_PRINT|CT_UNICODE|CT_ALPHA|CT_LOWER,
-              CT_PRINT|CT_UNICODE|CT_ALPHA|CT_UPPER)
-};
-int iscubeprint(uchar c) { return cubectype[c]&CT_PRINT; }
-int iscubespace(uchar c) { return cubectype[c]&CT_SPACE; }
-int iscubealpha(uchar c) { return cubectype[c]&CT_ALPHA; }
-int iscubealnum(uchar c) { return cubectype[c]&(CT_ALPHA|CT_DIGIT); }
-int iscubelower(uchar c) { return cubectype[c]&CT_LOWER; }
-int iscubeupper(uchar c) { return cubectype[c]&CT_UPPER; }
-const int cube2unichars[256] =
-{
-    0, 192, 193, 194, 195, 196, 197, 198, 199, 9, 10, 11, 12, 13, 200, 201,
-    202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 213, 214, 216, 217, 218, 219,
-    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
-    48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
-    64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
-    80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
-    96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
-    112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 220,
-    221, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237,
-    238, 239, 241, 242, 243, 244, 245, 246, 248, 249, 250, 251, 252, 253, 255, 0x104,
-    0x105, 0x106, 0x107, 0x10C, 0x10D, 0x10E, 0x10F, 0x118, 0x119, 0x11A, 0x11B, 0x11E, 0x11F, 0x130, 0x131, 0x141,
-    0x142, 0x143, 0x144, 0x147, 0x148, 0x150, 0x151, 0x152, 0x153, 0x158, 0x159, 0x15A, 0x15B, 0x15E, 0x15F, 0x160,
-    0x161, 0x164, 0x165, 0x16E, 0x16F, 0x170, 0x171, 0x178, 0x179, 0x17A, 0x17B, 0x17C, 0x17D, 0x17E, 0x404, 0x411,
-    0x413, 0x414, 0x416, 0x417, 0x418, 0x419, 0x41B, 0x41F, 0x423, 0x424, 0x426, 0x427, 0x428, 0x429, 0x42A, 0x42B,
-    0x42C, 0x42D, 0x42E, 0x42F, 0x431, 0x432, 0x433, 0x434, 0x436, 0x437, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D,
-    0x43F, 0x442, 0x444, 0x446, 0x447, 0x448, 0x449, 0x44A, 0x44B, 0x44C, 0x44D, 0x44E, 0x44F, 0x454, 0x490, 0x491
-};
-int cube2uni(uchar c)
-{
-    return cube2unichars[c];
-}
-
-const char *encodeutf8(int uni)
-{
-    static char buf[7];
-    char *dst = buf;
-    if(uni <= 0x7F) { *dst++ = uni; goto uni1; }
-    else if(uni <= 0x7FF) { *dst++ = 0xC0 | (uni>>6); goto uni2; }
-    else if(uni <= 0xFFFF) { *dst++ = 0xE0 | (uni>>12); goto uni3; }
-    else if(uni <= 0x1FFFFF) { *dst++ = 0xF0 | (uni>>18); goto uni4; }
-    else if(uni <= 0x3FFFFFF) { *dst++ = 0xF8 | (uni>>24); goto uni5; }
-    else if(uni <= 0x7FFFFFFF) { *dst++ = 0xFC | (uni>>30); goto uni6; }
-    else goto uni1;
-uni6: *dst++ = 0x80 | ((uni>>24)&0x3F);
-uni5: *dst++ = 0x80 | ((uni>>18)&0x3F);
-uni4: *dst++ = 0x80 | ((uni>>12)&0x3F);
-uni3: *dst++ = 0x80 | ((uni>>6)&0x3F);
-uni2: *dst++ = 0x80 | (uni&0x3F);
-uni1: *dst++ = '\0';
-    return buf;
-}
-
-struct fontchar { int code, uni, tex, x, y, w, h, offx, offy, offset, advance; FT_BitmapGlyph color, alpha; };
-
-const char *texdir = "";
-
-const char *texfilename(const char *name, int texnum)
-{
-    static char file[256];
-    snprintf(file, sizeof(file), "%s%d.png", name, texnum);
-    return file;
-}
-
-const char *texname(const char *name, int texnum)
-{
-    static char file[512];
-    snprintf(file, sizeof(file), "<grey>%s%s", texdir, texfilename(name, texnum));
-    return file;
-}
-
-void writetexs(const char *name, struct fontchar *chars, int numchars, int numtexs, int tw, int th)
-{
-    int tex;
-    uchar *pixels = (uchar *)malloc(tw*th*2);
-    if(!pixels) fatal("cube2font: failed allocating textures");
-    for(tex = 0; tex < numtexs; tex++)
-    {
-        const char *file = texfilename(name, tex);
-        int texchars = 0, i;
-        uchar *dst, *src;
-        memset(pixels, 0, tw*th*2);
-        for(i = 0; i < numchars; i++)
-        {
-            struct fontchar *c = &chars[i];
-            int x, y;
-            if(c->tex != tex) continue;
-            texchars++;
-            dst = &pixels[2*((c->y + c->offy - c->color->top)*tw + c->x + c->color->left - c->offx)];
-            src = (uchar *)c->color->bitmap.buffer;
-            for(y = 0; y < c->color->bitmap.rows; y++)
-            {
-                for(x = 0; x < c->color->bitmap.width; x++)
-                    dst[2*x] = src[x];
-                src += c->color->bitmap.pitch;
-                dst += 2*tw;
-            }
-            dst = &pixels[2*((c->y + c->offy - c->alpha->top)*tw + c->x + c->alpha->left - c->offx)];
-            src = (uchar *)c->alpha->bitmap.buffer;
-            for(y = 0; y < c->alpha->bitmap.rows; y++)
-            {
-                for(x = 0; x < c->alpha->bitmap.width; x++)
-                    dst[2*x+1] = src[x];
-                src += c->alpha->bitmap.pitch;
-                dst += 2*tw;
-            }
-        }
-        printf("cube2font: writing %d chars to %s\n", texchars, file);
-        savepng(file, pixels, tw, th, 2, 0);
-   }
-   free(pixels);
-}
-
-void writecfg(const char *name, struct fontchar *chars, int numchars, int x1, int y1, int x2, int y2, int sw, int sh, int argc, char **argv)
-{
-    FILE *f;
-    char file[256];
-    int i, lastcode = 0, lasttex = 0;
-    snprintf(file, sizeof(file), "%s.cfg", name);
-    f = fopen(file, "w");
-    if(!f) fatal("cube2font: failed writing %s", file);
-    printf("cube2font: writing %d chars to %s\n", numchars, file);
-    fprintf(f, "//");
-    for(i = 1; i < argc; i++)
-        fprintf(f, " %s", argv[i]);
-    fprintf(f, "\n");
-    fprintf(f, "font \"%s\" \"%s\" %d %d\n", name, texname(name, 0), sw, sh);
-    for(i = 0; i < numchars; i++)
-    {
-        struct fontchar *c = &chars[i];
-        if(!lastcode && lastcode < c->code)
-        {
-            fprintf(f, "fontoffset \"%s\"\n", encodeutf8(c->uni));
-            lastcode = c->code;
-        }
-        else if(lastcode < c->code)
-        {
-            if(lastcode + 1 == c->code)
-                fprintf(f, "fontskip // %d\n", lastcode);
-            else
-                fprintf(f, "fontskip %d // %d .. %d\n", c->code - lastcode, lastcode, c->code-1);
-            lastcode = c->code;
-        }
-        if(lasttex != c->tex)
-        {
-            fprintf(f, "\nfonttex \"%s\"\n", texname(name, c->tex));
-            lasttex = c->tex;
-        }
-        if(c->code != c->uni)
-            fprintf(f, "fontchar %d %d %d %d %d %d %d // %s (%d -> 0x%X)\n", c->x, c->y, c->w, c->h, c->offx+c->offset, y2-c->offy, c->advance, encodeutf8(c->uni), c->code, c->uni);
-        else
-            fprintf(f, "fontchar %d %d %d %d %d %d %d // %s (%d)\n", c->x, c->y, c->w, c->h, c->offx+c->offset, y2-c->offy, c->advance, encodeutf8(c->uni), c->code);
-        lastcode++;
-    }
-    fclose(f);
-}
-
-int groupchar(int c)
-{
-    switch(c)
-    {
-    case 0x152: case 0x153: case 0x178: return 1;
-    }
-    if(c < 127 || c >= 0x2000) return 0;
-    if(c < 0x100) return 1;
-    if(c < 0x400) return 2;
-    return 3;
-}
-
-int sortchars(const void *x, const void *y)
-{
-    const struct fontchar *xc = *(const struct fontchar **)x, *yc = *(const struct fontchar **)y;
-    int xg = groupchar(xc->uni), yg = groupchar(yc->uni);
-    if(xg < yg) return -1;
-    if(xg > yg) return 1;
-    if(xc->h != yc->h) return yc->h - xc->h;
-    if(xc->w != yc->w) return yc->w - xc->w;
-    return yc->uni - xc->uni;
-}
-
-int scorechar(struct fontchar *f, int pad, int tw, int th, int rw, int rh, int ry)
-{
-    int score = 0;
-    if(rw + f->w > tw) { ry += rh + pad; score = 1; }
-    if(ry + f->h > th) score = 2;
-    return score;
-}
-
-int main(int argc, char **argv)
-{
-    FT_Library l;
-    FT_Face f;
-    FT_Stroker s, s2;
-    int i, pad, offset, advance, w, h, tw, th, c, trial = -2, rw = 0, rh = 0, ry = 0, x1 = INT_MAX, x2 = INT_MIN, y1 = INT_MAX, y2 = INT_MIN, w2 = 0, h2 = 0, sw = 0, sh = 0;
-    float outborder = 0, inborder = 0;
-    struct fontchar chars[256];
-    struct fontchar *order[256];
-    int numchars = 0, numtex = 0;
-    if(argc < 11)
-        fatal("Usage: cube2font infile outfile outborder[:inborder] pad offset advance charwidth charheight texwidth texheight [spacewidth spaceheight texdir]");
-    sscanf(argv[3], "%f:%f", &outborder, &inborder);
-    pad = atoi(argv[4]);
-    offset = atoi(argv[5]);
-    advance = atoi(argv[6]);
-    w = atoi(argv[7]);
-    h = atoi(argv[8]);
-    tw = atoi(argv[9]);
-    th = atoi(argv[10]);
-    if(argc > 11) sw = atoi(argv[11]);
-    if(argc > 12) sh = atoi(argv[12]);
-    if(argc > 13) texdir = argv[13];
-    if(FT_Init_FreeType(&l))
-        fatal("cube2font: failed initing freetype");
-    if(FT_New_Face(l, argv[1], 0, &f) ||
-       FT_Set_Charmap(f, f->charmaps[0]) ||
-       FT_Set_Pixel_Sizes(f, w, h) ||
-       FT_Stroker_New(l, &s) ||
-       FT_Stroker_New(l, &s2))
-        fatal("cube2font: failed loading font %s", argv[1]);
-    if(outborder > 0) FT_Stroker_Set(s, (FT_Fixed)(outborder * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
-    if(inborder > 0) FT_Stroker_Set(s2, (FT_Fixed)(inborder * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
-    for(c = 0; c < 256; c++) if(iscubeprint(c))
-    {
-        FT_Glyph p, p2;
-        FT_BitmapGlyph b, b2;
-        struct fontchar *dst = &chars[numchars];
-        dst->code = c;
-        dst->uni = cube2uni(c);
-        if(FT_Load_Char(f, dst->uni, FT_LOAD_DEFAULT))
-            fatal("cube2font: failed loading character %s", encodeutf8(dst->uni));
-        FT_Get_Glyph(f->glyph, &p);
-        p2 = p;
-        if(outborder > 0) FT_Glyph_StrokeBorder(&p, s, 0, 0);
-        if(inborder > 0) FT_Glyph_StrokeBorder(&p2, s2, 1, 0);
-        FT_Glyph_To_Bitmap(&p, FT_RENDER_MODE_NORMAL, 0, 1);
-        FT_Glyph_To_Bitmap(&p2, FT_RENDER_MODE_NORMAL, 0, 1);
-        b = (FT_BitmapGlyph)p;
-        b2 = (FT_BitmapGlyph)p2;
-        dst->tex = -1;
-        dst->x = INT_MIN;
-        dst->y = INT_MIN;
-        dst->offx = imin(b->left, b2->left);
-        dst->offy = imax(b->top, b2->top);
-        dst->offset = offset;
-        dst->advance = offset + ((p->advance.x+0xFFFF)>>16) + advance;
-        dst->w = imax(b->left + b->bitmap.width, b2->left + b2->bitmap.width) - dst->offx;
-        dst->h = dst->offy - imin(b->top - b->bitmap.rows, b2->top - b2->bitmap.rows);
-        dst->alpha = b;
-        dst->color = b2;
-        order[numchars++] = dst;
-    }
-    qsort(order, numchars, sizeof(order[0]), sortchars);
-    for(i = 0; i < numchars;)
-    {
-        struct fontchar *dst;
-        int j, k, trial0, prevscore, dstscore, fitscore;
-        for(trial0 = trial, prevscore = -1; (trial -= 2) >= trial0-512;)
-        {
-            int g, fw = rw, fh = rh, fy = ry, curscore = 0, reused = 0;
-            for(j = i; j < numchars; j++)
-            {
-                dst = order[j];
-                if(dst->tex >= 0 || dst->tex <= trial) continue;
-                g = groupchar(dst->uni);
-                dstscore = scorechar(dst, pad, tw, th, fw, fh, fy);
-                for(k = j; k < numchars; k++)
-                {
-                    struct fontchar *fit = order[k];
-                    if(fit->tex >= 0 || fit->tex <= trial) continue;
-                    if(fit->tex >= trial0 && groupchar(fit->uni) != g) break;
-                    fitscore = scorechar(fit, pad, tw, th, fw, fh, fy);
-                    if(fitscore < dstscore || (fitscore == dstscore && fit->h > dst->h))
-                    {
-                        dst = fit;
-                        dstscore = fitscore;
-                    }
-                }
-                if(fw + dst->w > tw)
-                {
-                    fy += fh + pad;
-                    fw = fh = 0;
-                }
-                if(fy + dst->h > th)
-                {
-                    fy = fw = fh = 0;
-                    if(curscore > 0) break;
-                }
-                if(dst->tex >= trial+1 && dst->tex <= trial+2) { dst->tex = trial; reused++; }
-                else dst->tex = trial;
-                fw += dst->w + pad;
-                fh = imax(fh, dst->h);
-                if(dst != order[j]) --j;
-                curscore++;
-            }
-            if(reused < prevscore || curscore <= prevscore) break;
-            prevscore = curscore;
-        }
-        for(; i < numchars; i++)
-        {
-            dst = order[i];
-            if(dst->tex >= 0) continue;
-            dstscore = scorechar(dst, pad, tw, th, rw, rh, ry);
-            for(j = i; j < numchars; j++)
-            {
-                struct fontchar *fit = order[j];
-                if(fit->tex < trial || fit->tex > trial+2) continue;
-                fitscore = scorechar(fit, pad, tw, th, rw, rh, ry);
-                if(fitscore < dstscore || (fitscore == dstscore && fit->h > dst->h))
-                {
-                    dst = fit;
-                    dstscore = fitscore;
-                }
-            }
-            if(dst->tex < trial || dst->tex > trial+2) break;
-            if(rw + dst->w > tw)
-            {
-                ry += rh + pad;
-                rw = rh = 0;
-            }
-            if(ry + dst->h > th)
-            {
-                ry = rw = rh = 0;
-                numtex++;
-            }
-            dst->tex = numtex;
-            dst->x = rw;
-            dst->y = ry;
-            rw += dst->w + pad;
-            rh = imax(rh, dst->h);
-            y1 = imin(y1, dst->offy - dst->h);
-            y2 = imax(y2, dst->offy);
-            x1 = imin(x1, dst->offx);
-            x2 = imax(x2, dst->offx + dst->w);
-            w2 = imax(w2, dst->w);
-            h2 = imax(h2, dst->h);
-            if(dst != order[i]) --i;
-        }
-    }
-    if(rh > 0) numtex++;
-#if 0
-    if(sw <= 0)
-    {
-        if(FT_Load_Char(f, ' ', FT_LOAD_DEFAULT))
-            fatal("cube2font: failed loading space character");
-        sw = (f->glyph->advance.x+0x3F)>>6;
-    }
-#endif
-    if(sh <= 0) sh = y2 - y1; 
-    if(sw <= 0) sw = sh/3;
-    writetexs(argv[2], chars, numchars, numtex, tw, th);
-    writecfg(argv[2], chars, numchars, x1, y1, x2, y2, sw, sh, argc, argv);
-    for(i = 0; i < numchars; i++)
-    {
-        FT_Done_Glyph((FT_Glyph)chars[i].alpha);
-        FT_Done_Glyph((FT_Glyph)chars[i].color);
-    }
-    FT_Stroker_Done(s);
-    FT_Stroker_Done(s2);
-    FT_Done_FreeType(l);
-    printf("cube2font: (%d, %d) .. (%d, %d) = (%d, %d) / (%d, %d), %d texs\n", x1, y1, x2, y2, x2 - x1, y2 - y1, w2, h2, numtex);
-    return EXIT_SUCCESS;
-}
-
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <limits.h>
+#include <zlib.h>
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include FT_STROKER_H
+#include FT_GLYPH_H
+
+typedef unsigned char uchar;
+typedef unsigned short ushort;
+typedef unsigned int uint;
+
+int imin(int a, int b) { return a < b ? a : b; }
+int imax(int a, int b) { return a > b ? a : b; }
+
+void fatal(const char *fmt, ...)
+{
+    va_list v;
+    va_start(v, fmt);
+    vfprintf(stderr, fmt, v);
+    va_end(v);
+    fputc('\n', stderr);
+
+    exit(EXIT_FAILURE);
+}
+
+uint bigswap(uint n)
+{
+    const int islittleendian = 1;
+    return *(const uchar *)&islittleendian ? (n<<24) | (n>>24) | ((n>>8)&0xFF00) | ((n<<8)&0xFF0000) : n;
+}
+
+size_t writebig(FILE *f, uint n)
+{
+    n = bigswap(n);
+    return fwrite(&n, 1, sizeof(n), f);
+}
+
+void writepngchunk(FILE *f, const char *type, uchar *data, uint len)
+{
+    uint crc;
+    writebig(f, len);
+    fwrite(type, 1, 4, f);
+    fwrite(data, 1, len, f);
+
+    crc = crc32(0, Z_NULL, 0);
+    crc = crc32(crc, (const Bytef *)type, 4);
+    if(data) crc = crc32(crc, data, len);
+    writebig(f, crc);
+}
+
+struct pngihdr
+{
+    uint width, height;
+    uchar bitdepth, colortype, compress, filter, interlace;
+};
+
+void savepng(const char *filename, uchar *data, int w, int h, int bpp, int flip)
+{
+    const uchar signature[] = { 137, 80, 78, 71, 13, 10, 26, 10 };
+    struct pngihdr ihdr;
+    FILE *f;
+    long idat;
+    uint len, crc;
+    z_stream z;
+    uchar buf[1<<12];
+    int i, j;
+
+    memset(&ihdr, 0, sizeof(ihdr));
+    ihdr.width = bigswap(w);
+    ihdr.height = bigswap(h);
+    ihdr.bitdepth = 8;
+    switch(bpp)
+    {
+        case 1: ihdr.colortype = 0; break;
+        case 2: ihdr.colortype = 4; break;
+        case 3: ihdr.colortype = 2; break;
+        case 4: ihdr.colortype = 6; break;
+        default: fatal("cube2font: invalid PNG bpp"); return;
+    }
+    f = fopen(filename, "wb");
+    if(!f) { fatal("cube2font: could not write to %s", filename); return; }
+
+    fwrite(signature, 1, sizeof(signature), f);
+
+    writepngchunk(f, "IHDR", (uchar *)&ihdr, 13);
+
+    idat = ftell(f);
+    len = 0;
+    fwrite("\0\0\0\0IDAT", 1, 8, f);
+    crc = crc32(0, Z_NULL, 0);
+    crc = crc32(crc, (const Bytef *)"IDAT", 4);
+
+    z.zalloc = NULL;
+    z.zfree = NULL;
+    z.opaque = NULL;
+
+    if(deflateInit(&z, Z_BEST_COMPRESSION) != Z_OK)
+        goto error;
+
+    z.next_out = (Bytef *)buf;
+    z.avail_out = sizeof(buf);
+
+    for(i = 0; i < h; i++)
+    {
+        uchar filter = 0;
+        for(j = 0; j < 2; j++)
+        {
+            z.next_in = j ? (Bytef *)data + (flip ? h-i-1 : i)*w*bpp : (Bytef *)&filter;
+            z.avail_in = j ? w*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; \
+                    fwrite(buf, 1, flush, f); \
+                    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);
+
+    fseek(f, idat, SEEK_SET);
+    writebig(f, len);
+    fseek(f, 0, SEEK_END);
+    writebig(f, crc);
+
+    writepngchunk(f, "IEND", NULL, 0);
+
+    fclose(f);
+    return;
+
+cleanuperror:
+    deflateEnd(&z);
+
+error:
+    fclose(f);
+
+    fatal("cube2font: failed saving PNG to %s", filename);
+}
+
+enum
+{
+    CT_PRINT   = 1<<0,
+    CT_SPACE   = 1<<1,
+    CT_DIGIT   = 1<<2,
+    CT_ALPHA   = 1<<3,
+    CT_LOWER   = 1<<4,
+    CT_UPPER   = 1<<5,
+    CT_UNICODE = 1<<6
+};
+#define CUBECTYPE(s, p, d, a, A, u, U) \
+    0, U, U, U, U, U, U, U, U, s, s, s, s, s, U, U, \
+    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, \
+    s, p, p, p, p, p, p, p, p, p, p, p, p, p, p, p, \
+    d, d, d, d, d, d, d, d, d, d, p, p, p, p, p, p, \
+    p, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, \
+    A, A, A, A, A, A, A, A, A, A, A, p, p, p, p, p, \
+    p, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, \
+    a, a, a, a, a, a, a, a, a, a, a, p, p, p, p, U, \
+    U, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, \
+    u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, U, \
+    u, U, u, U, u, U, u, U, u, U, u, U, u, U, u, U, \
+    u, U, u, U, u, U, u, U, u, U, u, U, u, U, u, U, \
+    u, U, u, U, u, U, u, U, U, u, U, u, U, u, U, U, \
+    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, \
+    U, U, U, U, u, u, u, u, u, u, u, u, u, u, u, u, \
+    u, u, u, u, u, u, u, u, u, u, u, u, u, u, U, u
+const uchar cubectype[256] =
+{
+    CUBECTYPE(CT_SPACE,
+              CT_PRINT,
+              CT_PRINT|CT_DIGIT,
+              CT_PRINT|CT_ALPHA|CT_LOWER,
+              CT_PRINT|CT_ALPHA|CT_UPPER,
+              CT_PRINT|CT_UNICODE|CT_ALPHA|CT_LOWER,
+              CT_PRINT|CT_UNICODE|CT_ALPHA|CT_UPPER)
+};
+int iscubeprint(uchar c) { return cubectype[c]&CT_PRINT; }
+int iscubespace(uchar c) { return cubectype[c]&CT_SPACE; }
+int iscubealpha(uchar c) { return cubectype[c]&CT_ALPHA; }
+int iscubealnum(uchar c) { return cubectype[c]&(CT_ALPHA|CT_DIGIT); }
+int iscubelower(uchar c) { return cubectype[c]&CT_LOWER; }
+int iscubeupper(uchar c) { return cubectype[c]&CT_UPPER; }
+const int cube2unichars[256] =
+{
+    0, 192, 193, 194, 195, 196, 197, 198, 199, 9, 10, 11, 12, 13, 200, 201,
+    202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 213, 214, 216, 217, 218, 219,
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
+    48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
+    64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
+    80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
+    96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
+    112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 220,
+    221, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237,
+    238, 239, 241, 242, 243, 244, 245, 246, 248, 249, 250, 251, 252, 253, 255, 0x104,
+    0x105, 0x106, 0x107, 0x10C, 0x10D, 0x10E, 0x10F, 0x118, 0x119, 0x11A, 0x11B, 0x11E, 0x11F, 0x130, 0x131, 0x141,
+    0x142, 0x143, 0x144, 0x147, 0x148, 0x150, 0x151, 0x152, 0x153, 0x158, 0x159, 0x15A, 0x15B, 0x15E, 0x15F, 0x160,
+    0x161, 0x164, 0x165, 0x16E, 0x16F, 0x170, 0x171, 0x178, 0x179, 0x17A, 0x17B, 0x17C, 0x17D, 0x17E, 0x404, 0x411,
+    0x413, 0x414, 0x416, 0x417, 0x418, 0x419, 0x41B, 0x41F, 0x423, 0x424, 0x426, 0x427, 0x428, 0x429, 0x42A, 0x42B,
+    0x42C, 0x42D, 0x42E, 0x42F, 0x431, 0x432, 0x433, 0x434, 0x436, 0x437, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D,
+    0x43F, 0x442, 0x444, 0x446, 0x447, 0x448, 0x449, 0x44A, 0x44B, 0x44C, 0x44D, 0x44E, 0x44F, 0x454, 0x490, 0x491
+};
+int cube2uni(uchar c)
+{
+    return cube2unichars[c];
+}
+
+const char *encodeutf8(int uni)
+{
+    static char buf[7];
+    char *dst = buf;
+    if(uni <= 0x7F) { *dst++ = uni; goto uni1; }
+    else if(uni <= 0x7FF) { *dst++ = 0xC0 | (uni>>6); goto uni2; }
+    else if(uni <= 0xFFFF) { *dst++ = 0xE0 | (uni>>12); goto uni3; }
+    else if(uni <= 0x1FFFFF) { *dst++ = 0xF0 | (uni>>18); goto uni4; }
+    else if(uni <= 0x3FFFFFF) { *dst++ = 0xF8 | (uni>>24); goto uni5; }
+    else if(uni <= 0x7FFFFFFF) { *dst++ = 0xFC | (uni>>30); goto uni6; }
+    else goto uni1;
+uni6: *dst++ = 0x80 | ((uni>>24)&0x3F);
+uni5: *dst++ = 0x80 | ((uni>>18)&0x3F);
+uni4: *dst++ = 0x80 | ((uni>>12)&0x3F);
+uni3: *dst++ = 0x80 | ((uni>>6)&0x3F);
+uni2: *dst++ = 0x80 | (uni&0x3F);
+uni1: *dst++ = '\0';
+    return buf;
+}
+
+struct fontchar { int code, uni, tex, x, y, w, h, offx, offy, offset, advance; FT_BitmapGlyph color, alpha; };
+
+const char *texdir = "";
+
+const char *texfilename(const char *name, int texnum)
+{
+    static char file[256];
+    snprintf(file, sizeof(file), "%s%d.png", name, texnum);
+    return file;
+}
+
+const char *texname(const char *name, int texnum)
+{
+    static char file[512];
+    snprintf(file, sizeof(file), "<grey>%s%s", texdir, texfilename(name, texnum));
+    return file;
+}
+
+void writetexs(const char *name, struct fontchar *chars, int numchars, int numtexs, int tw, int th)
+{
+    int tex;
+    uchar *pixels = (uchar *)malloc(tw*th*2);
+    if(!pixels) fatal("cube2font: failed allocating textures");
+    for(tex = 0; tex < numtexs; tex++)
+    {
+        const char *file = texfilename(name, tex);
+        int texchars = 0, i;
+        uchar *dst, *src;
+        memset(pixels, 0, tw*th*2);
+        for(i = 0; i < numchars; i++)
+        {
+            struct fontchar *c = &chars[i];
+            int x, y;
+            if(c->tex != tex) continue;
+            texchars++;
+            dst = &pixels[2*((c->y + c->offy - c->color->top)*tw + c->x + c->color->left - c->offx)];
+            src = (uchar *)c->color->bitmap.buffer;
+            for(y = 0; y < c->color->bitmap.rows; y++)
+            {
+                for(x = 0; x < c->color->bitmap.width; x++)
+                    dst[2*x] = src[x];
+                src += c->color->bitmap.pitch;
+                dst += 2*tw;
+            }
+            dst = &pixels[2*((c->y + c->offy - c->alpha->top)*tw + c->x + c->alpha->left - c->offx)];
+            src = (uchar *)c->alpha->bitmap.buffer;
+            for(y = 0; y < c->alpha->bitmap.rows; y++)
+            {
+                for(x = 0; x < c->alpha->bitmap.width; x++)
+                    dst[2*x+1] = src[x];
+                src += c->alpha->bitmap.pitch;
+                dst += 2*tw;
+            }
+        }
+        printf("cube2font: writing %d chars to %s\n", texchars, file);
+        savepng(file, pixels, tw, th, 2, 0);
+   }
+   free(pixels);
+}
+
+void writecfg(const char *name, struct fontchar *chars, int numchars, int x1, int y1, int x2, int y2, int sw, int sh, int argc, char **argv)
+{
+    FILE *f;
+    char file[256];
+    int i, lastcode = 0, lasttex = 0;
+    snprintf(file, sizeof(file), "%s.cfg", name);
+    f = fopen(file, "w");
+    if(!f) fatal("cube2font: failed writing %s", file);
+    printf("cube2font: writing %d chars to %s\n", numchars, file);
+    fprintf(f, "//");
+    for(i = 1; i < argc; i++)
+        fprintf(f, " %s", argv[i]);
+    fprintf(f, "\n");
+    fprintf(f, "font \"%s\" \"%s\" %d %d\n", name, texname(name, 0), sw, sh);
+    for(i = 0; i < numchars; i++)
+    {
+        struct fontchar *c = &chars[i];
+        if(!lastcode && lastcode < c->code)
+        {
+            fprintf(f, "fontoffset \"%s\"\n", encodeutf8(c->uni));
+            lastcode = c->code;
+        }
+        else if(lastcode < c->code)
+        {
+            if(lastcode + 1 == c->code)
+                fprintf(f, "fontskip // %d\n", lastcode);
+            else
+                fprintf(f, "fontskip %d // %d .. %d\n", c->code - lastcode, lastcode, c->code-1);
+            lastcode = c->code;
+        }
+        if(lasttex != c->tex)
+        {
+            fprintf(f, "\nfonttex \"%s\"\n", texname(name, c->tex));
+            lasttex = c->tex;
+        }
+        if(c->code != c->uni)
+            fprintf(f, "fontchar %d %d %d %d %d %d %d // %s (%d -> 0x%X)\n", c->x, c->y, c->w, c->h, c->offx+c->offset, y2-c->offy, c->advance, encodeutf8(c->uni), c->code, c->uni);
+        else
+            fprintf(f, "fontchar %d %d %d %d %d %d %d // %s (%d)\n", c->x, c->y, c->w, c->h, c->offx+c->offset, y2-c->offy, c->advance, encodeutf8(c->uni), c->code);
+        lastcode++;
+    }
+    fclose(f);
+}
+
+int groupchar(int c)
+{
+    switch(c)
+    {
+    case 0x152: case 0x153: case 0x178: return 1;
+    }
+    if(c < 127 || c >= 0x2000) return 0;
+    if(c < 0x100) return 1;
+    if(c < 0x400) return 2;
+    return 3;
+}
+
+int sortchars(const void *x, const void *y)
+{
+    const struct fontchar *xc = *(const struct fontchar **)x, *yc = *(const struct fontchar **)y;
+    int xg = groupchar(xc->uni), yg = groupchar(yc->uni);
+    if(xg < yg) return -1;
+    if(xg > yg) return 1;
+    if(xc->h != yc->h) return yc->h - xc->h;
+    if(xc->w != yc->w) return yc->w - xc->w;
+    return yc->uni - xc->uni;
+}
+
+int scorechar(struct fontchar *f, int pad, int tw, int th, int rw, int rh, int ry)
+{
+    int score = 0;
+    if(rw + f->w > tw) { ry += rh + pad; score = 1; }
+    if(ry + f->h > th) score = 2;
+    return score;
+}
+
+int main(int argc, char **argv)
+{
+    FT_Library l;
+    FT_Face f;
+    FT_Stroker s, s2;
+    int i, pad, offset, advance, w, h, tw, th, c, trial = -2, rw = 0, rh = 0, ry = 0, x1 = INT_MAX, x2 = INT_MIN, y1 = INT_MAX, y2 = INT_MIN, w2 = 0, h2 = 0, sw = 0, sh = 0;
+    float outborder = 0, inborder = 0;
+    struct fontchar chars[256];
+    struct fontchar *order[256];
+    int numchars = 0, numtex = 0;
+    if(argc < 11)
+        fatal("Usage: cube2font infile outfile outborder[:inborder] pad offset advance charwidth charheight texwidth texheight [spacewidth spaceheight texdir]");
+    sscanf(argv[3], "%f:%f", &outborder, &inborder);
+    pad = atoi(argv[4]);
+    offset = atoi(argv[5]);
+    advance = atoi(argv[6]);
+    w = atoi(argv[7]);
+    h = atoi(argv[8]);
+    tw = atoi(argv[9]);
+    th = atoi(argv[10]);
+    if(argc > 11) sw = atoi(argv[11]);
+    if(argc > 12) sh = atoi(argv[12]);
+    if(argc > 13) texdir = argv[13];
+    if(FT_Init_FreeType(&l))
+        fatal("cube2font: failed initing freetype");
+    if(FT_New_Face(l, argv[1], 0, &f) ||
+       FT_Set_Charmap(f, f->charmaps[0]) ||
+       FT_Set_Pixel_Sizes(f, w, h) ||
+       FT_Stroker_New(l, &s) ||
+       FT_Stroker_New(l, &s2))
+        fatal("cube2font: failed loading font %s", argv[1]);
+    if(outborder > 0) FT_Stroker_Set(s, (FT_Fixed)(outborder * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
+    if(inborder > 0) FT_Stroker_Set(s2, (FT_Fixed)(inborder * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
+    for(c = 0; c < 256; c++) if(iscubeprint(c))
+    {
+        FT_Glyph p, p2;
+        FT_BitmapGlyph b, b2;
+        struct fontchar *dst = &chars[numchars];
+        dst->code = c;
+        dst->uni = cube2uni(c);
+        if(FT_Load_Char(f, dst->uni, FT_LOAD_DEFAULT))
+            fatal("cube2font: failed loading character %s", encodeutf8(dst->uni));
+        FT_Get_Glyph(f->glyph, &p);
+        p2 = p;
+        if(outborder > 0) FT_Glyph_StrokeBorder(&p, s, 0, 0);
+        if(inborder > 0) FT_Glyph_StrokeBorder(&p2, s2, 1, 0);
+        FT_Glyph_To_Bitmap(&p, FT_RENDER_MODE_NORMAL, 0, 1);
+        FT_Glyph_To_Bitmap(&p2, FT_RENDER_MODE_NORMAL, 0, 1);
+        b = (FT_BitmapGlyph)p;
+        b2 = (FT_BitmapGlyph)p2;
+        dst->tex = -1;
+        dst->x = INT_MIN;
+        dst->y = INT_MIN;
+        dst->offx = imin(b->left, b2->left);
+        dst->offy = imax(b->top, b2->top);
+        dst->offset = offset;
+        dst->advance = offset + ((p->advance.x+0xFFFF)>>16) + advance;
+        dst->w = imax(b->left + b->bitmap.width, b2->left + b2->bitmap.width) - dst->offx;
+        dst->h = dst->offy - imin(b->top - b->bitmap.rows, b2->top - b2->bitmap.rows);
+        dst->alpha = b;
+        dst->color = b2;
+        order[numchars++] = dst;
+    }
+    qsort(order, numchars, sizeof(order[0]), sortchars);
+    for(i = 0; i < numchars;)
+    {
+        struct fontchar *dst;
+        int j, k, trial0, prevscore, dstscore, fitscore;
+        for(trial0 = trial, prevscore = -1; (trial -= 2) >= trial0-512;)
+        {
+            int g, fw = rw, fh = rh, fy = ry, curscore = 0, reused = 0;
+            for(j = i; j < numchars; j++)
+            {
+                dst = order[j];
+                if(dst->tex >= 0 || dst->tex <= trial) continue;
+                g = groupchar(dst->uni);
+                dstscore = scorechar(dst, pad, tw, th, fw, fh, fy);
+                for(k = j; k < numchars; k++)
+                {
+                    struct fontchar *fit = order[k];
+                    if(fit->tex >= 0 || fit->tex <= trial) continue;
+                    if(fit->tex >= trial0 && groupchar(fit->uni) != g) break;
+                    fitscore = scorechar(fit, pad, tw, th, fw, fh, fy);
+                    if(fitscore < dstscore || (fitscore == dstscore && fit->h > dst->h))
+                    {
+                        dst = fit;
+                        dstscore = fitscore;
+                    }
+                }
+                if(fw + dst->w > tw)
+                {
+                    fy += fh + pad;
+                    fw = fh = 0;
+                }
+                if(fy + dst->h > th)
+                {
+                    fy = fw = fh = 0;
+                    if(curscore > 0) break;
+                }
+                if(dst->tex >= trial+1 && dst->tex <= trial+2) { dst->tex = trial; reused++; }
+                else dst->tex = trial;
+                fw += dst->w + pad;
+                fh = imax(fh, dst->h);
+                if(dst != order[j]) --j;
+                curscore++;
+            }
+            if(reused < prevscore || curscore <= prevscore) break;
+            prevscore = curscore;
+        }
+        for(; i < numchars; i++)
+        {
+            dst = order[i];
+            if(dst->tex >= 0) continue;
+            dstscore = scorechar(dst, pad, tw, th, rw, rh, ry);
+            for(j = i; j < numchars; j++)
+            {
+                struct fontchar *fit = order[j];
+                if(fit->tex < trial || fit->tex > trial+2) continue;
+                fitscore = scorechar(fit, pad, tw, th, rw, rh, ry);
+                if(fitscore < dstscore || (fitscore == dstscore && fit->h > dst->h))
+                {
+                    dst = fit;
+                    dstscore = fitscore;
+                }
+            }
+            if(dst->tex < trial || dst->tex > trial+2) break;
+            if(rw + dst->w > tw)
+            {
+                ry += rh + pad;
+                rw = rh = 0;
+            }
+            if(ry + dst->h > th)
+            {
+                ry = rw = rh = 0;
+                numtex++;
+            }
+            dst->tex = numtex;
+            dst->x = rw;
+            dst->y = ry;
+            rw += dst->w + pad;
+            rh = imax(rh, dst->h);
+            y1 = imin(y1, dst->offy - dst->h);
+            y2 = imax(y2, dst->offy);
+            x1 = imin(x1, dst->offx);
+            x2 = imax(x2, dst->offx + dst->w);
+            w2 = imax(w2, dst->w);
+            h2 = imax(h2, dst->h);
+            if(dst != order[i]) --i;
+        }
+    }
+    if(rh > 0) numtex++;
+#if 0
+    if(sw <= 0)
+    {
+        if(FT_Load_Char(f, ' ', FT_LOAD_DEFAULT))
+            fatal("cube2font: failed loading space character");
+        sw = (f->glyph->advance.x+0x3F)>>6;
+    }
+#endif
+    if(sh <= 0) sh = y2 - y1; 
+    if(sw <= 0) sw = sh/3;
+    writetexs(argv[2], chars, numchars, numtex, tw, th);
+    writecfg(argv[2], chars, numchars, x1, y1, x2, y2, sw, sh, argc, argv);
+    for(i = 0; i < numchars; i++)
+    {
+        FT_Done_Glyph((FT_Glyph)chars[i].alpha);
+        FT_Done_Glyph((FT_Glyph)chars[i].color);
+    }
+    FT_Stroker_Done(s);
+    FT_Stroker_Done(s2);
+    FT_Done_FreeType(l);
+    printf("cube2font: (%d, %d) .. (%d, %d) = (%d, %d) / (%d, %d), %d texs\n", x1, y1, x2, y2, x2 - x1, y2 - y1, w2, h2, numtex);
+    return EXIT_SUCCESS;
+}
+

-- 
Packaging for Red Eclipse



More information about the Pkg-games-commits mailing list