[redeclipse] 14/23: Imported Upstream version 1.5.1

Martin Werner arand-guest at moszumanska.debian.org
Sat Apr 4 00:47:48 UTC 2015


This is an automated email from the git hooks/post-receive script.

arand-guest pushed a commit to branch next
in repository redeclipse.

commit f2c9e9f25c3e9de87909a4e67d69749e1505652c
Author: Martin Erik Werner <martinerikwerner at gmail.com>
Date:   Fri Apr 3 17:34:50 2015 +0200

    Imported Upstream version 1.5.1
---
 config/brush.cfg                                   |  140 +
 config/compass.cfg                                 |   43 +
 config/defaults.cfg                                |   24 +
 config/engine.cfg                                  |  289 ++
 config/fonts/command.cfg                           |    3 +
 config/fonts/console.cfg                           |    3 +
 config/fonts/consub.cfg                            |    3 +
 config/fonts/default.cfg                           |    3 +
 config/fonts/emphasis.cfg                          |    3 +
 config/fonts/huge.cfg                              |    3 +
 config/fonts/little.cfg                            |    3 +
 config/fonts/play.cfg                              |  326 +++
 config/fonts/reduced.cfg                           |    3 +
 config/fonts/super.cfg                             |    3 +
 config/fonts/tiny.cfg                              |    3 +
 config/glsl.cfg                                    | 2395 ++++++++++++++++
 config/keymap.cfg                                  |  486 ++++
 config/legacy.cfg                                  |   41 +
 config/map/default.cfg                             |   74 +
 config/map/models.cfg                              |    7 +
 config/map/octa.cfg                                |   66 +
 config/map/sounds.cfg                              |    2 +
 config/map/textures.cfg                            |   10 +
 config/menus/editing.cfg                           |  966 +++++++
 config/menus/game.cfg                              |   42 +
 config/menus/glue.cfg                              |  213 ++
 config/menus/help.cfg                              | 1062 ++++++++
 config/menus/irc.cfg                               |   68 +
 config/menus/main.cfg                              |  116 +
 config/menus/maps.cfg                              |  558 ++++
 config/menus/options.cfg                           | 1133 ++++++++
 config/menus/package.cfg                           |   12 +
 config/menus/profile.cfg                           |  238 ++
 config/menus/scratch.cfg                           |   85 +
 config/menus/servers.cfg                           |  382 +++
 config/menus/vars.cfg                              |  257 ++
 config/setup.cfg                                   |  311 +++
 config/sounds/announcer.cfg                        |   37 +
 config/sounds/interface.cfg                        |    3 +
 config/sounds/package.cfg                          |    5 +
 config/sounds/player.cfg                           |    7 +
 config/sounds/sfx.cfg                              |   27 +
 config/sounds/weapons.cfg                          |  260 ++
 config/stdlib.cfg                                  |   37 +
 config/stdshader.cfg                               | 2848 ++++++++++++++++++++
 config/tips.cfg                                    |   41 +
 config/usage.cfg                                   |  809 ++++++
 config/vanities.cfg                                |   46 +
 config/version.cfg                                 |   23 +
 config/voice.cfg                                   |   40 +
 doc/all-licenses.txt                               |  515 ++--
 doc/announce.txt                                   |   13 +
 doc/changelog.txt                                  |  211 +-
 doc/examples/servinit.cfg                          |  290 +-
 doc/guidelines.txt                                 |   14 +-
 doc/irc.txt                                        |    4 +-
 doc/license.txt                                    |    6 +-
 doc/man/redeclipse.6.am                            |    7 -
 doc/media.txt                                      |   77 +
 doc/trademark.txt                                  |   10 +-
 game/fps/version.cfg                               |   34 -
 readme.md                                          |    1 +
 readme.txt                                         |  221 +-
 redeclipse.sh                                      |  220 +-
 redeclipse_server.sh                               |   72 +-
 src/Makefile                                       |  657 ++++-
 src/core.mk                                        |  618 -----
 src/dist.mk                                        |  267 +-
 src/dpiaware.manifest                              |   32 +-
 src/engine/animmodel.h                             |   66 +-
 src/engine/bih.cpp                                 |   10 +-
 src/engine/blend.cpp                               |    6 +-
 src/engine/blob.cpp                                |    8 +-
 src/engine/client.cpp                              |   49 +-
 src/engine/command.cpp                             |  624 ++++-
 src/engine/console.cpp                             |  219 +-
 src/engine/decal.cpp                               |   60 +-
 src/engine/dynlight.cpp                            |    5 +-
 src/engine/engine.h                                |  231 +-
 src/engine/explosion.h                             |   29 +-
 src/engine/glexts.h                                |  293 ++
 src/engine/grass.cpp                               |    8 +-
 src/engine/iqm.h                                   |   74 +-
 src/engine/irc.cpp                                 |  283 +-
 src/engine/irc.h                                   |   28 +-
 src/engine/lensflare.h                             |   35 +-
 src/engine/lightmap.cpp                            |    4 +-
 src/engine/lightmap.h                              |    3 +-
 src/engine/lightning.h                             |    6 +-
 src/engine/main.cpp                                |  139 +-
 src/engine/master.cpp                              |  129 +-
 src/engine/material.cpp                            |   39 +-
 src/engine/md5.h                                   |    4 +-
 src/engine/menus.cpp                               |  310 ++-
 src/engine/model.h                                 |   54 +-
 src/engine/movie.cpp                               |   16 +-
 src/engine/mpr.h                                   |  277 +-
 src/engine/octa.cpp                                |    5 +-
 src/engine/octa.h                                  |   23 +-
 src/engine/octaedit.cpp                            |  232 +-
 src/engine/octarender.cpp                          |   25 +-
 src/engine/physics.cpp                             |  649 +++--
 src/engine/pvs.cpp                                 |   48 +-
 src/engine/ragdoll.h                               |   45 +-
 src/engine/rendergl.cpp                            |  187 +-
 src/engine/rendermodel.cpp                         |   37 +-
 src/engine/renderparticles.cpp                     |   46 +-
 src/engine/rendersky.cpp                           |   11 +-
 src/engine/rendertext.cpp                          |  150 +-
 src/engine/renderva.cpp                            |   94 +-
 src/engine/scale.h                                 |   12 +-
 src/engine/server.cpp                              |  470 ++--
 src/engine/serverbrowser.cpp                       |  145 +-
 src/engine/shader.cpp                              |    8 +-
 src/engine/shadowmap.cpp                           |    2 +-
 src/engine/skelmodel.h                             |   14 +-
 src/engine/sound.cpp                               |  132 +-
 src/engine/sound.h                                 |    4 +-
 src/engine/textedit.h                              |  141 +-
 src/engine/texture.cpp                             |  288 +-
 src/engine/texture.h                               |  222 +-
 src/engine/ui.cpp                                  |  860 ++++--
 src/engine/vertmodel.h                             |   18 +-
 src/engine/water.cpp                               |  120 +-
 src/engine/world.cpp                               |  218 +-
 src/engine/world.h                                 |    4 +-
 src/engine/worldio.cpp                             |  144 +-
 src/game/ai.cpp                                    | 1272 +++++----
 src/game/ai.h                                      |  106 +-
 src/game/aiman.h                                   |  223 +-
 src/game/auth.h                                    |  209 +-
 src/game/bomber.cpp                                |  445 +--
 src/game/bomber.h                                  |   62 +-
 src/game/bombermode.h                              |  201 +-
 src/game/capture.cpp                               |  441 +--
 src/game/capture.h                                 |   50 +-
 src/game/capturemode.h                             |  121 +-
 src/game/client.cpp                                |  975 ++++---
 src/game/compass.h                                 |   21 +-
 src/game/defend.cpp                                |  191 +-
 src/game/defend.h                                  |   70 +-
 src/game/defendmode.h                              |   59 +-
 src/game/duelmut.h                                 |  235 +-
 src/game/entities.cpp                              |  857 +++---
 src/game/game.cpp                                  | 1811 +++++++------
 src/game/game.h                                    |  874 +++---
 src/game/gamemode.h                                |  305 +--
 src/game/hud.cpp                                   | 1480 +++++-----
 src/game/physics.cpp                               |  623 ++---
 src/game/player.h                                  |  180 +-
 src/game/playerdef.h                               |   58 +
 src/game/projs.cpp                                 |  759 +++---
 src/game/scoreboard.cpp                            | 1050 +++++---
 src/game/server.cpp                                | 2845 +++++++++++--------
 src/game/teamdef.h                                 |  129 +
 src/game/vars.h                                    |  329 ++-
 src/game/waypoint.cpp                              |  143 +-
 src/game/weapdef.h                                 |  233 +-
 src/game/weapons.cpp                               |  227 +-
 src/game/weapons.h                                 |  877 +++---
 src/install/nix/redeclipse.appdata.xml             |   24 +
 src/install/nix/redeclipse.desktop.am              |   26 +-
 src/redeclipse.cbp                                 | 1575 +----------
 src/redeclipse.ico                                 |  Bin 90022 -> 490479 bytes
 src/redeclipse.mk                                  |    9 -
 src/redeclipse.rc                                  |   40 +-
 src/scripts/check-wiki-all-result                  |   13 -
 .../{generate-cube2font-txt => cube2font-txt}      |    0
 src/scripts/generate-wiki-aliases                  |   30 -
 src/scripts/generate-wiki-all-vars-commands        |   25 -
 src/scripts/generate-wiki-commands                 |   40 -
 src/scripts/generate-wiki-engine-vars              |   80 -
 src/scripts/generate-wiki-game-vars                |   60 -
 src/scripts/generate-wiki-weapon-vars              |   65 -
 ...{update-servinit-comments => servinit-comments} |    4 -
 ...{update-servinit-defaults => servinit-defaults} |    0
 src/scripts/wiki-common                            |  132 -
 ...enerate-wiki-contributors => wiki-contributors} |    8 +-
 src/scripts/wiki-convert                           |  179 ++
 .../{generate-wiki-guidelines => wiki-guidelines}  |    0
 src/semaphore.sh                                   |  170 ++
 src/semdeploy.sh                                   |   25 +
 src/shared/command.h                               |  110 +-
 src/shared/crypto.cpp                              |  211 +-
 src/shared/cube.h                                  |   11 +-
 src/shared/cube2font.c                             |    5 +-
 src/shared/ents.h                                  |   34 +-
 src/shared/geom.cpp                                |   16 +-
 src/shared/geom.h                                  |  267 +-
 src/shared/iengine.h                               |   86 +-
 src/shared/igame.h                                 |   26 +-
 src/shared/stream.cpp                              |  208 +-
 src/shared/tools.cpp                               |   20 +-
 src/shared/tools.h                                 |  277 +-
 src/shared/zip.cpp                                 |   95 +-
 src/system-install.mk                              |   85 +-
 src/version.h                                      |   10 +
 src/wiki.mk                                        |  107 +
 198 files changed, 31247 insertions(+), 14478 deletions(-)

diff --git a/config/brush.cfg b/config/brush.cfg
new file mode 100644
index 0000000..16df63e
--- /dev/null
+++ b/config/brush.cfg
@@ -0,0 +1,140 @@
+// this config defines various heightmap brushes
+
+// a brush has two parts: the brushmap and the brushhandle
+
+// the brushmap is a 2D field that defines how the brush affects a heightmap
+
+// the brushhandle is a 2D vertex on the brushmap.
+// this handle is a reference point on the brushmap which represents
+// where the editing cursor is pointed at.
+
+// clearbrush                   : resets the brushmap
+// brushvert <x> <y> <value>    : set a point on the brushmap
+// brushx <x>                   : the x coordinate of the brushhandle
+// brushy <y>                   : the y coordinate of the brushhandle
+
+brushhandle = [
+    brushx $arg1
+    brushy $arg2
+]
+
+brushindex = -1
+brushmax = -1                // make sure to bump this up if you add more brushes
+
+selectbrush = [
+    brushindex = ( + $brushindex $arg1 )
+    if (< $brushindex 0) [ brushindex = $brushmax ]
+    if (> $brushindex $brushmax) [ brushindex = 0 ]
+    do [brush_ at brushindex]
+    echo $brushname
+]; setcomplete "selectbrush" 1
+
+brushverts = [
+    loop y (listlen $arg1) [
+        bv = (at $arg1 $y)
+        loop x (listlen $bv) [
+            brushvert $x $y (at $bv $x)
+        ]
+    ]
+]
+
+newbrush = [
+    brushmax = (+ $brushmax 1)
+    do [
+        brush_ at brushmax = [
+            brushname = [@@@arg1]
+            clearbrush
+            @@(if (> $numargs 1) [result [brushhandle @arg2 @arg3; brushverts [@@arg4]]])
+        ]
+    ]
+]
+
+newbrush "Circle 1-0 Brush" 0 0 [1]
+newbrush "Circle 2-1 Brush" 2 2 [
+    ""
+    "0 0 1 "
+    "0 1 2 1 "
+    "0 0 1 "
+]
+newbrush "Circle 4-2-1 Brush" 2 2 [
+    "0 0 1 "
+    "0 1 2 1 "
+    "1 2 4 2 1 "
+    "0 1 2 1 "
+    "0 0 1 "
+]
+newbrush "Square 3x3 brush" 1 1 [
+    "1 1 1 "
+    "1 1 1 "
+    "1 1 1 "
+]
+newbrush "Square 5x5 brush" 2 2 [
+    "1 1 1 1 1 "
+    "1 1 1 1 1 "
+    "1 1 1 1 1 "
+    "1 1 1 1 1 "
+    "1 1 1 1 1 "
+]
+newbrush "Square 7x7 brush" 3 3 [
+    "1 1 1 1 1 1 1 "
+    "1 1 1 1 1 1 1 "
+    "1 1 1 1 1 1 1 "
+    "1 1 1 1 1 1 1 "
+    "1 1 1 1 1 1 1 "
+    "1 1 1 1 1 1 1 "
+    "1 1 1 1 1 1 1 "
+]
+
+newbrush "Smooth 3x3 brush" 1 1 [
+    "0 0 0 "
+    "0 "
+    "0 "
+]
+newbrush "Smooth 5x5 brush" 2 2 [
+    "0 0 0 0 0 "
+    "0 "
+    "0 "
+    "0 "
+    "0 "
+]
+newbrush "Smooth 7x7 brush" 3 3 [
+    "0 0 0 0 0 0 0"
+    "0 "
+    "0 "
+    "0 "
+    "0 "
+    "0 "
+    "0 "
+]
+
+newbrush "Noise 25x25 Brush" 12 12 [
+    "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 "
+    ""
+    "0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 1 1 0 1 "
+    "0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 2 2 "
+    "0 0 0 0 1 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 1 "
+    "0 0 0 0 0 1 0 0 0 0 0 2 0 0 0 0 1 0 0 0 1 1 0 0 1 "
+    "0 0 1 0 0 0 1 0 1 1 0 0 0 0 1 0 0 1 0 0 0 0 2 "
+    "0 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 2 "
+    "0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 1 1 "
+    "0 0 0 0 1 1 1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 1 "
+    "0 1 0 2 0 1 1 1 1 0 0 1 0 0 0 0 1 "
+    "0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 "
+    "1 0 1 0 0 0 0 0 1 0 0 0 1 0 1 "
+    "0 0 0 0 0 0 0 1 1 0 1 1 0 0 1 0 0 1 0 0 0 0 1 0 0 1 "
+    "0 1 1 1 0 3 0 2 0 0 0 1 1 0 0 0 1 1 "
+    "0 0 1 0 0 1 0 0 1 0 1 1 0 1 0 0 0 0 0 1 "
+    "0 0 1 1 0 0 0 0 2 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 1 "
+    "0 1 1 0 1 0 0 1 0 0 0 0 0 1 0 0 1 1 0 0 0 0 1 "
+    "1 0 0 0 0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 1 "
+    "0 0 0 1 0 0 1 0 1 1 0 0 0 0 0 0 0 1 "
+    "0 0 0 0 0 0 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 1 "
+    "0 0 0 0 1 0 1 1 0 2 0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 "
+    "0 0 0 0 0 0 0 0 0 1 0 1 "
+    "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 "
+]
+
+do [ brush_2 ] // 421
+
+//blendmap brushes; not heightmap related, but whatever :P
+loopfiles curbrush blendbrush png [ addblendbrush $curbrush (concatword "blendbrush/" $curbrush ".png") ]
diff --git a/config/compass.cfg b/config/compass.cfg
new file mode 100644
index 0000000..f50eede
--- /dev/null
+++ b/config/compass.cfg
@@ -0,0 +1,43 @@
+newcompass main "textures/menu" [
+    compass "menus" [showcompass menus]
+    compass "voice" [showcompass voice]
+    compass "team" [showcompass team]
+    compass "clear" [clearcompass]
+]
+
+newcompass menus "textures/menu" [
+    compass "main" [showgui main]
+    compass "maps" [showgui maps 1]
+    compass "vote" [showgui maps 2]
+    compass "servers" [showgui servers]
+    compass "team" [showgui team]
+    if (&& [> (mutators) 0] [& (mutators) 0x0020]) [compass "loadout" [showgui profile 2]]
+    compass "options" [showgui options]
+]
+
+newcompass voice "textures/hud/voices" [
+    compass "argh!" [say "argh!"]
+    compass "lucky shot" [say "lucky shot"]
+    compass "nice shot" [say "nice shot"]
+    compass "boom!" [say "boom!"]
+    compass "damn it!" [say "damn it!"]
+    compass "haha" [say "haha"]
+    compass "suck it" [say "suck it"]
+    compass "pzap!" [say "pzap!"]
+    compass "team voice actions" "V" [showcompass team]
+]
+
+newcompass team "textures/hud/voices" [
+    compass "yes" [sayteam "yes"]
+    compass "sorry" [sayteam "sorry"]
+    compass "no problem" [sayteam "no problem"]
+    compass "thanks" [sayteam "thanks"]
+    compass "no" [sayteam "no"]
+    compass "go go go!" [sayteam "go go go!"]
+    compass "hang on" [sayteam "hang on"]
+    compass "pzap!" [sayteam "pzap!"]
+    compass "common voice actions" "V" [showcompass voice]
+]
+
+bind V [showcompass voice]
+bind X [showcompass team]
diff --git a/config/defaults.cfg b/config/defaults.cfg
new file mode 100644
index 0000000..408dd7a
--- /dev/null
+++ b/config/defaults.cfg
@@ -0,0 +1,24 @@
+loopfiles curmod . zip [ addzip $curmod ]
+loopfiles curmod maps zip [ addzip (concatword "maps/" $curmod) ]
+loopfiles curmod mods zip [
+    mntmod = (concatword "mods/" $curmod)
+    addzip $mntmod $mntmod
+    exec (concatword "mods/" $curmod "/package.cfg")
+]
+
+exec "config/keymap.cfg"
+
+exec "config/engine.cfg"
+exec "config/brush.cfg"
+exec "config/setup.cfg"
+exec "config/legacy.cfg"
+
+exec "config/sounds/package.cfg"
+exec "config/vanities.cfg"
+
+exec "config/usage.cfg"
+exec "config/tips.cfg"
+
+exec "config/menus/package.cfg" // keep this here, it uses some of the stuff above
+exec "config/voice.cfg"
+exec "config/compass.cfg"
diff --git a/config/engine.cfg b/config/engine.cfg
new file mode 100644
index 0000000..67b8694
--- /dev/null
+++ b/config/engine.cfg
@@ -0,0 +1,289 @@
+defaultmodifier = 0
+modifier = $defaultmodifier
+domodifier = [ modifier = $arg1; onrelease [ modifier = $defaultmodifier ] ]
+
+deltastates = ["game" "dead" "edit" "spec" "wait"]
+universaldelta = [
+    delta = (format "delta_%1_%2" (at $deltastates (getplayerstate)) $modifier)
+    if (stringcmp (getalias $delta) "") [
+        delta = (format "delta_%1_%2" (at $deltastates (getplayerstate)) $defaultmodifier)
+        if (stringcmp (getalias $delta) "") [
+            delta = (format "delta_%1_%2" (at $deltastates 0) $modifier)
+            if (stringcmp (getalias $delta) "") [
+                delta = (format "delta_%1_%2" (at $deltastates 0) $defaultmodifier)
+            ]
+        ]
+    ]
+    if (getalias $delta) [ do [@delta @arg1] ]
+]; setcomplete universaldelta 1
+
+listcomplete editmat "air water clip glass noclip lava death aiclip ladder alpha"
+air = [ editmat air $arg1 ]; setcomplete air 1
+loop i 4 [
+    [water@(? $i (+ $i 1))] = [ editmat water@(? $i (+ $i 1)) $arg1 ]; setcomplete [water@(? $i (+ $i 1))] 1
+    [lava@(? $i (+ $i 1))] = [ editmat lava@(? $i (+ $i 1)) $arg1 ]; setcomplete [lava@(? $i (+ $i 1))] 1
+    [glass@(? $i (+ $i 1))] = [ editmat glass@(? $i (+ $i 1)) $arg1 ]; setcomplete [glass@(? $i (+ $i 1))] 1
+]
+clip = [ editmat clip $arg1 ]; setcomplete clip 1
+noclip = [ editmat noclip $arg1 ]; setcomplete noclip 1
+death = [ editmat death $arg1 ]; setcomplete death 1
+aiclip = [ editmat aiclip $arg1 ]; setcomplete aiclip 1
+ladder = [ editmat ladder $arg1 ]; setcomplete ladder 1
+alpha = [ editmat alpha $arg1 ]; setcomplete alpha 1
+
+//////// Entity Editing ///////////////
+
+=enttype = [
+    || [stringcmp * $arg1] [stringcmp (enttype) $arg1]
+]
+
+=entattr = [
+    || [stringcmp * $arg2] [= (entattr $arg1) $arg2]
+]
+
+// clear ents of given type
+clearents = [
+    if $editing [
+        entcancel
+        entselect [ =enttype $arg1 ];
+        echo Deleted (enthavesel) $arg1 entities;
+        delent
+    ]
+]; setcomplete clearents 1
+
+// replace all ents that match current selection
+// with the values given
+replaceents = [
+    if $editing [
+        do [
+            entfind @(entget)
+            entset @(loopconcat i $numargs [result $[arg@(+ $i 1)]])
+        ]
+        echo Replaced (enthavesel) entities
+    ]
+]; setcomplete replaceents 1
+
+selentedit      = [ saycommand ( concatword "/entset " (entget) ) ]; setcomplete selentedit 1
+selreplaceents  = [ saycommand ( concatword "/replaceents " (entget) ) ]; setcomplete selreplaceents 1
+selentfindall   = [ do [ entfind @(entget) ] ]; setcomplete selentfindall 1
+
+// modify given attribute of ent by a given amount
+// arg1 attribute
+// arg2 value
+entmodify = [
+    entattr $arg1 (+ (entattr $arg1) $arg2)
+]; setcomplete entmodify 1
+
+entproperty = [
+    entprop $arg1 $arg2
+]; setcomplete entproperty 1
+
+//////// Copy and Paste //////////////
+
+// 3 types of copying and pasting
+// 1. select only cubes      -> paste only cubes
+// 2. select cubes and ents  -> paste cubes and ents. same relative positions
+// 3. select only ents       -> paste last selected ent. if ents are selected, replace attrs as paste
+
+opaquepaste = 1
+entcopybuf = ""
+
+entreplace   = [
+    do [
+        if (enthavesel) [] [ newent @entcopybuf ]
+        entset @entcopybuf
+    ]
+]; setcomplete entreplace 1
+
+editcopy   = [
+    if (|| [havesel] [! (enthavesel)]) [
+        entcopybuf = ""
+        entcopy
+        copy
+    ] [
+        entcopybuf = (entget)
+    ]
+]; setcomplete editcopy 1
+
+editpaste  = [
+    cancelpaste = (! (|| [enthavesel] [havesel]));
+    if (stringcmp "" $entcopybuf) [
+        pastehilight
+        onrelease [
+            if $opaquepaste delcube
+            paste
+            entpaste
+            if $cancelpaste [ cancelsel ]
+        ]
+    ] [
+        entreplace
+        if $cancelpaste [ cancelsel ]
+    ]
+]; setcomplete editpaste 1
+
+/////// Selection ///////////////
+
+// select ents with given properties
+// '*' is wildcard
+entfind    = [
+    if (= $numargs 0) [
+        entselect 1
+    ] [
+        entselect (concat [ && [=enttype @@arg1] ] (loopconcat i (- $numargs 1) [
+            result [ [=entattr @@i @@[arg@(+ $i 2)]] ]
+        ]))
+    ]
+]; setcomplete entfind 1
+
+entfindinsel = [
+    if (= $numargs 0) [
+        entselect [ insel ]
+    ] [
+        entselect (concat [ && [insel] [=enttype @@arg1] ] (loopconcat i (- $numargs 1) [
+            result [ [=entattr @@i @@[arg@(+ $i 2)]] ]
+        ]))
+    ]
+]; setcomplete entfindinsel 1
+
+lse        = [
+    line = ""
+    count = 0
+    entloop [
+        line  = ( concatword $line (entget) "       " )
+        count = ( + $count 1 )
+        if (> $count 4) [
+            echo $line
+            line = ""
+            count = 0
+        ]
+    ]
+    if (> $count 0 ) [ echo $line ]
+    echo (enthavesel) entities selected
+]; setcomplete lse 1
+
+grabbing = 0
+
+drag       = [ dragging 1; onrelease [ dragging 0 ] ]
+corners    = [ selectcorners 1; dragging 1; onrelease [ selectcorners 0; dragging 0 ] ]
+editmove   = [ moving 1; onrelease [ moving 0 ]; result $moving ]
+entdrag    = [ entmoving 1; onrelease [entmoving 0]; result $entmoving ]
+
+editdrag   = [ cancelsel; || [entdrag] [ drag ] ]; setcomplete editdrag 1
+selcorners = [ if $hmapedit [ hmapselect ] [ cancelsel; || [entdrag] [ corners ] ] ]; setcomplete selcorners 1
+editextend = [ || [entdrag] [ selextend; reorient; editmove ] ]; setcomplete editextend 1
+
+editmovewith    = [
+    if (havesel) [
+        || [editmove] [ @arg1 ]
+        onrelease [ moving 0; dragging 0 ]
+    ] [
+        @arg1
+    ]
+]
+
+editmovecorner = [ editmovewith selcorners ]; setcomplete editmovecorner 1
+editmovedrag   = [ editmovewith editdrag ]; setcomplete editmovedrag 1
+
+////// Other Editing commands /////////
+
+editfacewentpush = [
+    if (|| [havesel] [! (enthavesel)] ) [
+        if $moving [
+            pushsel $arg1
+        ] [
+            entcancel
+            editface $arg1 $arg2
+        ]
+    ] [
+        if $entmoving [ entpush $arg1 ]
+    ]
+]; setcomplete editfacewentpush 1
+
+entswithdirection = ["playerstart 0" "mapmodel 1" "teledest 0" "camera 0"]
+
+editdel    = [ if (= (enthavesel) 0) [ delcube ] [ delent ] ]; setcomplete editdel 1
+editflip   = [ if (= (enthavesel) 0) [ flip ] [ entflip ] ]; setcomplete editflip 1
+
+editrotate = [
+    rotate $arg1
+    entrotate $arg1
+]; setcomplete editrotate 1
+
+editcut    = [  
+  if (moving 1) [
+    if (= $moving 1) [selsave]
+    onrelease [ 
+      moving 0 
+      if (selmoved) [
+        selswap
+        copy; entcopy
+        delcube; delent
+        selrestore
+        paste; entpaste
+      ]
+    ] 
+  ] 
+]; setcomplete editcut 1
+
+selectents = [ entselect [&& [insel] [=enttype @@arg1]] ]; setcomplete selectents 1
+
+noautosave = 0
+autosaveinterval = 10; setpersist autosaveinterval 1; setcomplete autosaveinterval 1
+autosave = [
+    if (= $noautosave 0) [
+        savemap
+        sleep (* $autosaveinterval 60000) [ autosave ]
+    ] [ noautosave = 0 ]
+]; setcomplete autosave 1
+stopautosave = [ noautosave = 1 ]; setcomplete stopautosave 1
+
+togglesound = [ if (> $mastervol 0) [ mastervol 0; echo "sound off" ] [ mastervol 255; echo "sound on" ] ]; setcomplete togglesound 1
+
+changeoutline = [
+    outlinestatus = (mod (+ $arg1 $outlinestatus) (listlen $outlinecolours))
+    if (< $outlinestatus 0) [
+        outlinestatus = (+ (listlen $outlinecolours) $outlinestatus ) //just making sure it'll fit
+    ]
+
+    if (=s (at $outlinecolours $outlinestatus) "OFF") [
+        echo "Outline OFF"
+        outline 0
+    ] [
+        echo (concat "Outline" (at $outlinecolours $outlinestatus))
+        outline 1
+        outlinecolour @(at $outlinecolours $outlinestatus)
+    ]
+]; setcomplete changeoutline 1
+outlinecolours = ["255 255 255" "0 0 0" "80 80 255" "0 127 255" "255 80 80" "255 0 255" "80 255 80" "255 255 80" "OFF"]
+outlinestatus = 0
+
+enttypelist = ""
+do [
+    en = (getentinfo)
+    loop q $en [
+        es = (getentinfo $q)
+        enttypelist = (? $q (concat $enttypelist $es) $es)
+    ]
+]
+entcomplete = [ listcomplete $arg1 $enttypelist ]
+
+// entcomplete gets defined by game
+entcomplete newent
+entcomplete entfind
+
+enttypeselect = [
+    enttypelength = (listlen $enttypelist)
+    next = (mod (+ (indexof $enttypelist (enttype)) $arg1) $enttypelength)
+    if (< $next 0) [ next = (+ $next $enttypelength) ]
+    do [entset @(listsplice (entget) (at $enttypelist $next) 0 1)]
+]
+
+slime = [
+    editmat water
+    editmat death
+    obitwater "sucked on slime"
+    watercolour 0x182200
+    waterfallcolour 0x002200
+    waterfog 0
+    waterspec 10
+]
diff --git a/config/fonts/command.cfg b/config/fonts/command.cfg
new file mode 100644
index 0000000..ccb8586
--- /dev/null
+++ b/config/fonts/command.cfg
@@ -0,0 +1,3 @@
+fontalias "command" "default"
+fontscale 56
+
diff --git a/config/fonts/console.cfg b/config/fonts/console.cfg
new file mode 100644
index 0000000..b21c6e6
--- /dev/null
+++ b/config/fonts/console.cfg
@@ -0,0 +1,3 @@
+fontalias "console" "default"
+fontscale 42
+
diff --git a/config/fonts/consub.cfg b/config/fonts/consub.cfg
new file mode 100644
index 0000000..aad5625
--- /dev/null
+++ b/config/fonts/consub.cfg
@@ -0,0 +1,3 @@
+fontalias "consub" "default"
+fontscale 36
+
diff --git a/config/fonts/default.cfg b/config/fonts/default.cfg
new file mode 100644
index 0000000..e228122
--- /dev/null
+++ b/config/fonts/default.cfg
@@ -0,0 +1,3 @@
+fontalias "default" "play"
+fontscale 48
+
diff --git a/config/fonts/emphasis.cfg b/config/fonts/emphasis.cfg
new file mode 100644
index 0000000..4f9057d
--- /dev/null
+++ b/config/fonts/emphasis.cfg
@@ -0,0 +1,3 @@
+fontalias "emphasis" "default"
+fontscale 68
+
diff --git a/config/fonts/huge.cfg b/config/fonts/huge.cfg
new file mode 100644
index 0000000..3710dd8
--- /dev/null
+++ b/config/fonts/huge.cfg
@@ -0,0 +1,3 @@
+fontalias "huge" "default"
+fontscale 80
+
diff --git a/config/fonts/little.cfg b/config/fonts/little.cfg
new file mode 100644
index 0000000..6452948
--- /dev/null
+++ b/config/fonts/little.cfg
@@ -0,0 +1,3 @@
+fontalias "little" "default"
+fontscale 38
+
diff --git a/config/fonts/play.cfg b/config/fonts/play.cfg
new file mode 100644
index 0000000..8abc26b
--- /dev/null
+++ b/config/fonts/play.cfg
@@ -0,0 +1,326 @@
+// Play-Bold.ttf play 4 15 1 1 54 54 512 512 0 0 fonts/
+font "play" "<grey>fonts/play0.png" 23 71
+fontoffset "À"
+
+fonttex "<grey>fonts/play1.png"
+fontchar 348 215 45 53 -3 6 38 // À (1 -> 0xC0)
+fontchar 288 215 45 53 -3 6 38 // Á (2 -> 0xC1)
+fontchar 228 215 45 53 -3 6 38 // Â (3 -> 0xC2)
+fontchar 168 215 45 53 -3 6 39 // Ã (4 -> 0xC3)
+fontchar 108 215 45 53 -4 6 38 // Ä (5 -> 0xC4)
+fontchar 408 215 45 51 -4 8 37 // Å (6 -> 0xC5)
+fontchar 220 408 60 43 -4 16 55 // Æ (7 -> 0xC6)
+fontchar 320 0 36 56 0 15 34 // Ç (8 -> 0xC7)
+fontskip 5 // 9 .. 13
+fontchar 474 145 36 54 1 5 35 // È (14 -> 0xC8)
+fontchar 423 145 36 54 1 5 35 // É (15 -> 0xC9)
+fontchar 372 145 36 54 1 5 35 // Ê (16 -> 0xCA)
+fontchar 321 145 36 54 1 5 35 // Ë (17 -> 0xCB)
+fontchar 40 215 22 54 -2 5 18 // Ì (18 -> 0xCC)
+fontchar 489 75 22 54 -2 5 18 // Í (19 -> 0xCD)
+fontchar 0 215 25 54 -3 5 18 // Î (20 -> 0xCE)
+fontchar 478 0 25 54 -3 5 19 // Ï (21 -> 0xCF)
+fontchar 212 0 41 56 1 3 43 // Ñ (22 -> 0xD1)
+fontchar 112 75 41 55 0 5 41 // Ò (23 -> 0xD2)
+fontchar 56 75 41 55 0 5 41 // Ó (24 -> 0xD3)
+fontchar 0 75 41 55 0 5 41 // Ô (25 -> 0xD4)
+fontchar 103 0 41 57 0 3 41 // Õ (26 -> 0xD5)
+fontchar 422 0 41 55 0 5 41 // Ö (27 -> 0xD6)
+fontchar 48 408 41 45 0 15 41 // Ø (28 -> 0xD8)
+fontchar 327 75 38 55 1 5 40 // Ù (29 -> 0xD9)
+fontchar 274 75 38 55 1 5 40 // Ú (30 -> 0xDA)
+fontchar 221 75 38 55 1 5 40 // Û (31 -> 0xDB)
+fontskip // 32
+
+fonttex "<grey>fonts/play0.png"
+fontchar 41 370 18 43 0 16 18 // ! (33)
+fontchar 0 478 24 22 1 16 26 // " (34)
+fontchar 404 312 33 43 0 16 33 // # (35)
+fontchar 196 0 37 54 -1 13 35 // $ (36)
+fontchar 230 74 50 45 -1 15 48 // % (37)
+fontchar 471 74 41 45 0 15 38 // & (38)
+fontchar 73 478 14 22 1 16 16 // ' (39)
+fontchar 287 0 24 52 1 14 21 // ( (40)
+fontchar 248 0 24 52 -4 14 21 // ) (41)
+fontchar 433 428 29 27 -1 16 26 // * (42)
+fontchar 339 428 32 33 1 21 34 // + (43)
+fontchar 39 478 19 22 -2 43 16 // , (44)
+fontchar 259 478 25 14 -1 32 23 // - (45)
+fontchar 189 478 16 16 0 43 16 // . (46)
+fontchar 406 135 31 45 -3 15 25 // / (47)
+fontchar 309 135 34 45 0 15 33 // 0 (48)
+fontchar 0 370 26 43 1 16 33 // 1 (49)
+fontchar 53 195 33 44 0 15 33 // 2 (50)
+fontchar 195 195 32 44 0 16 33 // 3 (51)
+fontchar 256 312 35 43 -2 16 33 // 4 (52)
+fontchar 148 195 32 44 0 16 33 // 5 (53)
+fontchar 478 135 33 44 0 16 33 // 6 (54)
+fontchar 355 312 34 43 -1 16 33 // 7 (55)
+fontchar 210 135 35 45 -1 15 33 // 8 (56)
+fontchar 260 135 34 45 0 15 33 // 9 (57)
+fontchar 261 428 16 34 0 25 16 // : (58)
+fontchar 74 370 19 41 -2 25 16 // ; (59)
+fontchar 292 428 32 33 1 21 34 // < (60)
+fontchar 477 428 32 24 1 26 34 // = (61)
+fontchar 386 428 32 32 1 22 34 // > (62)
+fontchar 101 195 32 44 -1 15 30 // ? (63)
+fontchar 355 74 45 45 0 15 45 // @ (64)
+fontchar 60 254 45 43 -3 16 38 // A (65)
+fontchar 205 312 36 43 1 16 36 // B (66)
+fontchar 159 135 36 45 0 15 34 // C (67)
+fontchar 401 254 39 43 1 16 40 // D (68)
+fontchar 154 312 36 43 1 16 35 // E (69)
+fontchar 103 312 36 43 1 16 35 // F (70)
+fontchar 0 135 40 45 0 15 40 // G (71)
+fontchar 346 254 40 43 1 16 42 // H (72)
+fontchar 495 312 16 43 1 16 18 // I (73)
+fontchar 242 195 27 44 -2 16 26 // J (74)
+fontchar 178 254 42 43 1 16 39 // K (75)
+fontchar 306 312 34 43 1 16 33 // L (76)
+fontchar 392 195 49 43 1 16 51 // M (77)
+fontchar 235 254 41 43 1 16 43 // N (78)
+fontchar 415 74 41 45 0 15 41 // O (79)
+fontchar 52 312 36 43 1 16 36 // P (80)
+fontchar 295 74 45 45 0 15 42 // Q (81)
+fontchar 291 254 40 43 1 16 38 // R (82)
+fontchar 108 135 36 45 -1 15 34 // S (83)
+fontchar 455 254 38 43 -2 16 34 // T (84)
+fontchar 0 195 38 44 1 16 40 // U (85)
+fontchar 0 254 45 43 -4 16 38 // V (86)
+fontchar 315 195 62 43 -4 16 55 // W (87)
+fontchar 456 195 45 43 -3 16 39 // X (88)
+fontchar 120 254 43 43 -4 16 36 // Y (89)
+fontchar 0 312 37 43 -1 16 35 // Z (90)
+fontchar 122 0 22 59 2 10 22 // [ (91)
+fontchar 358 135 33 45 -4 15 25 // \ (92)
+fontchar 84 0 23 59 -3 10 22 // ] (93)
+fontchar 102 478 25 20 -2 14 20 // ^ (94)
+fontchar 299 478 32 13 1 55 34 // _ (95)
+fontchar 220 478 24 15 0 14 24 // ` (96)
+fontchar 206 370 33 36 -1 24 32 // a (97)
+fontchar 426 0 35 47 0 13 35 // b (98)
+fontchar 301 370 31 36 0 24 28 // c (99)
+fontchar 376 0 35 47 0 13 35 // d (100)
+fontchar 158 370 33 36 0 24 33 // e (101)
+fontchar 476 0 29 47 -2 12 24 // f (102)
+fontchar 326 0 35 47 0 24 35 // g (103)
+fontchar 151 74 33 46 0 13 33 // h (104)
+fontchar 284 195 16 44 1 15 18 // i (105)
+fontchar 159 0 22 56 -5 15 17 // j (106)
+fontchar 0 74 36 46 0 13 31 // k (107)
+fontchar 199 74 16 46 1 13 18 // l (108)
+fontchar 347 370 49 35 0 24 49 // m (109)
+fontchar 459 370 33 35 0 24 33 // n (110)
+fontchar 108 370 35 36 0 24 35 // o (111)
+fontchar 101 74 35 46 0 24 35 // p (112)
+fontchar 51 74 35 46 0 24 35 // q (113)
+fontchar 0 428 25 35 0 24 23 // r (114)
+fontchar 254 370 32 36 -1 24 30 // s (115)
+fontchar 452 312 28 43 -2 17 24 // t (116)
+fontchar 411 370 33 35 0 25 33 // u (117)
+fontchar 162 428 37 34 -4 25 30 // v (118)
+fontchar 40 428 53 34 -4 25 46 // w (119)
+fontchar 108 428 39 34 -3 25 33 // x (120)
+fontchar 55 135 38 45 -4 25 31 // y (121)
+fontchar 214 428 32 34 -1 25 30 // z (122)
+fontchar 42 0 27 59 -2 10 23 // { (123)
+fontchar 452 135 11 45 3 15 17 // | (124)
+fontchar 0 0 27 59 -2 10 22 // } (125)
+fontchar 142 478 32 18 1 30 34 // ~ (126)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 168 75 38 55 1 5 40 // Ü (127 -> 0xDC)
+fontchar 263 145 43 54 -4 5 36 // Ý (128 -> 0xDD)
+fontchar 98 284 36 47 0 12 36 // ß (129 -> 0xDF)
+fontchar 0 408 33 46 -1 14 32 // à (130 -> 0xE0)
+fontchar 432 347 33 46 -1 14 32 // á (131 -> 0xE1)
+fontchar 384 347 33 46 -1 14 32 // â (132 -> 0xE2)
+fontchar 50 284 33 48 -1 12 32 // ã (133 -> 0xE3)
+fontchar 336 347 33 46 -1 14 32 // ä (134 -> 0xE4)
+fontchar 468 215 33 51 -1 9 32 // å (135 -> 0xE5)
+fontchar 432 408 49 36 -1 24 48 // æ (136 -> 0xE6)
+fontchar 197 284 31 47 0 24 28 // ç (137 -> 0xE7)
+fontchar 288 347 33 46 0 14 33 // è (138 -> 0xE8)
+fontchar 240 347 33 46 0 14 33 // é (139 -> 0xE9)
+fontchar 192 347 33 46 0 14 33 // ê (140 -> 0xEA)
+fontchar 144 347 33 46 0 14 33 // ë (141 -> 0xEB)
+fontchar 144 408 24 45 -3 14 18 // ì (142 -> 0xEC)
+fontchar 183 408 22 45 -2 14 18 // í (143 -> 0xED)
+fontchar 480 347 26 45 -4 14 18 // î (144 -> 0xEE)
+fontchar 104 408 25 45 -4 14 17 // ï (145 -> 0xEF)
+fontchar 149 284 33 47 0 12 33 // ñ (146 -> 0xF1)
+fontchar 393 284 35 46 0 14 35 // ò (147 -> 0xF2)
+fontchar 343 284 35 46 0 14 35 // ó (148 -> 0xF3)
+fontchar 293 284 35 46 0 14 35 // ô (149 -> 0xF4)
+fontchar 0 284 35 48 0 12 35 // õ (150 -> 0xF5)
+fontchar 243 284 35 46 0 14 35 // ö (151 -> 0xF6)
+fontchar 0 469 35 36 0 24 35 // ø (152 -> 0xF8)
+fontchar 96 347 33 46 0 14 33 // ù (153 -> 0xF9)
+fontchar 48 347 33 46 0 14 33 // ú (154 -> 0xFA)
+fontchar 0 347 33 46 0 14 33 // û (155 -> 0xFB)
+fontchar 443 284 33 46 0 14 33 // ü (156 -> 0xFC)
+fontchar 159 0 38 57 -4 13 31 // ý (157 -> 0xFD)
+fontchar 268 0 37 56 -4 14 30 // ÿ (158 -> 0xFF)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 107 72 45 53 -3 16 38 // Ą (159 -> 0x104)
+fontchar 92 207 34 45 -1 24 33 // ą (160 -> 0x105)
+fontchar 67 0 36 55 0 5 34 // Ć (161 -> 0x106)
+fontchar 46 207 31 46 0 14 28 // ć (162 -> 0x107)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 154 145 36 55 0 5 34 // Č (163 -> 0x10C)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 0 207 31 46 0 14 28 // č (164 -> 0x10D)
+fontchar 229 0 39 54 1 5 40 // Ď (165 -> 0x10E)
+fontchar 93 141 46 47 0 13 40 // ď (166 -> 0x10F)
+fontchar 223 72 36 53 1 16 35 // Ę (167 -> 0x118)
+fontchar 237 207 33 45 0 24 33 // ę (168 -> 0x119)
+fontchar 0 72 36 54 1 5 35 // Ě (169 -> 0x11A)
+fontchar 347 141 33 46 0 14 33 // ě (170 -> 0x11B)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 0 145 37 55 0 5 37 // Ğ (171 -> 0x11E)
+fontchar 53 0 35 58 0 13 35 // ğ (172 -> 0x11F)
+fontchar 77 215 16 54 1 5 18 // İ (173 -> 0x130)
+
+fonttex "<grey>fonts/play0.png"
+fontchar 346 478 16 34 1 25 18 // ı (174 -> 0x131)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 50 469 37 43 -2 16 33 // Ł (175 -> 0x141)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 489 141 22 46 -2 13 18 // ł (176 -> 0x142)
+fontchar 118 0 41 54 1 5 43 // Ń (177 -> 0x143)
+fontchar 189 207 33 45 0 14 33 // ń (178 -> 0x144)
+fontchar 167 72 41 53 1 6 43 // Ň (179 -> 0x147)
+fontchar 141 207 33 45 0 14 33 // ň (180 -> 0x148)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 380 75 41 55 0 5 41 // Ő (181 -> 0x150)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 249 141 35 46 0 14 35 // ő (182 -> 0x151)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 295 408 56 43 0 16 55 // Π(183 -> 0x152)
+fontchar 366 408 51 36 0 24 50 // œ (184 -> 0x153)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 174 0 40 54 3 5 37 // Ř (185 -> 0x158)
+fontchar 426 207 27 45 -2 14 23 // ř (186 -> 0x159)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 103 145 36 55 -1 5 34 // Ś (187 -> 0x15A)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 442 141 32 46 -1 14 30 // ś (188 -> 0x15B)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 371 0 36 56 -1 15 34 // Ş (189 -> 0x15E)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 154 141 32 47 -1 24 30 // ş (190 -> 0x15F)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 52 145 36 55 -1 5 34 // Š (191 -> 0x160)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 395 141 32 46 -1 14 30 // š (192 -> 0x161)
+fontchar 283 0 38 54 -2 5 34 // Ť (193 -> 0x164)
+fontchar 46 141 32 49 -2 11 24 // ť (194 -> 0x165)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 0 0 38 60 1 0 40 // Ů (195 -> 0x16E)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 458 72 33 51 0 9 33 // ů (196 -> 0x16F)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 436 75 38 55 1 5 40 // Ű (197 -> 0x170)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 299 141 33 46 0 14 33 // ű (198 -> 0x171)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 205 145 43 54 -3 5 37 // Ÿ (199 -> 0x178)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 440 0 37 54 -1 5 35 // Ź (200 -> 0x179)
+fontchar 379 207 32 45 -1 14 30 // ź (201 -> 0x17A)
+fontchar 388 0 37 54 -1 5 35 // Ż (202 -> 0x17B)
+fontchar 332 207 32 45 -1 14 30 // ż (203 -> 0x17C)
+fontchar 336 0 37 54 -1 5 35 // Ž (204 -> 0x17D)
+fontchar 285 207 32 45 -1 14 30 // ž (205 -> 0x17E)
+fontchar 70 268 36 45 0 15 34 // Є (206 -> 0x404)
+fontchar 53 328 38 43 1 16 38 // Б (207 -> 0x411)
+fontchar 106 328 31 43 1 16 30 // Г (208 -> 0x413)
+fontchar 400 72 43 52 -2 16 40 // Д (209 -> 0x414)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 102 469 65 43 -3 16 58 // Ж (210 -> 0x416)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 288 268 34 44 -1 16 33 // З (211 -> 0x417)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 441 469 41 43 1 16 43 // И (212 -> 0x418)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 51 72 41 54 1 5 43 // Й (213 -> 0x419)
+fontchar 171 268 44 44 -2 16 43 // Л (214 -> 0x41B)
+fontchar 392 268 39 43 1 16 41 // П (215 -> 0x41F)
+fontchar 230 268 43 44 -3 16 36 // У (216 -> 0x423)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 383 469 43 43 0 16 43 // Ф (217 -> 0x424)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 345 72 40 53 1 16 42 // Ц (218 -> 0x426)
+fontchar 0 328 38 43 0 16 39 // Ч (219 -> 0x427)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 182 469 56 43 1 16 58 // Ш (220 -> 0x428)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 274 72 56 53 1 16 58 // Щ (221 -> 0x429)
+
+fonttex "<grey>fonts/play1.png"
+fontchar 319 469 49 43 -2 16 46 // Ъ (222 -> 0x42A)
+fontchar 253 469 51 43 1 16 53 // Ы (223 -> 0x42B)
+
+fonttex "<grey>fonts/play2.png"
+fontchar 446 268 38 43 1 16 38 // Ь (224 -> 0x42C)
+fontchar 121 268 35 45 -1 15 34 // Э (225 -> 0x42D)
+fontchar 0 268 55 45 1 15 56 // Ю (226 -> 0x42E)
+fontchar 337 268 40 43 -3 16 38 // Я (227 -> 0x42F)
+fontchar 201 141 33 47 0 13 33 // б (228 -> 0x431)
+fontchar 247 437 34 34 0 25 33 // в (229 -> 0x432)
+fontchar 343 437 26 34 0 25 23 // г (230 -> 0x433)
+fontchar 214 328 37 42 -2 25 34 // д (231 -> 0x434)
+fontchar 98 386 56 34 -5 25 46 // ж (232 -> 0x436)
+fontchar 417 328 33 36 -2 24 30 // з (233 -> 0x437)
+fontchar 0 437 36 34 0 25 36 // и (234 -> 0x438)
+fontchar 468 207 36 45 0 14 36 // й (235 -> 0x439)
+fontchar 457 386 36 34 0 25 31 // к (236 -> 0x43A)
+fontchar 46 386 37 35 -3 25 34 // л (237 -> 0x43B)
+fontchar 349 386 41 34 0 25 41 // м (238 -> 0x43C)
+fontchar 198 437 34 34 0 25 34 // н (239 -> 0x43D)
+fontchar 149 437 34 34 0 25 34 // п (240 -> 0x43F)
+fontchar 296 437 32 34 -1 25 30 // т (241 -> 0x442)
+fontchar 0 0 52 57 0 13 52 // ф (242 -> 0x444)
+fontchar 266 328 33 42 0 25 33 // ц (243 -> 0x446)
+fontchar 100 437 34 34 0 25 34 // ч (244 -> 0x447)
+fontchar 169 386 47 34 0 25 47 // ш (245 -> 0x448)
+fontchar 152 328 47 42 0 25 47 // щ (246 -> 0x449)
+fontchar 292 386 42 34 -1 25 30 // ъ (247 -> 0x44A)
+fontchar 231 386 46 34 0 25 46 // ы (248 -> 0x44B)
+fontchar 51 437 34 34 0 25 33 // ь (249 -> 0x44C)
+fontchar 0 386 31 36 -1 24 30 // э (250 -> 0x44D)
+fontchar 354 328 48 36 0 24 48 // ю (251 -> 0x44E)
+fontchar 405 386 37 34 -4 25 33 // я (252 -> 0x44F)
+fontchar 465 328 31 36 0 24 28 // є (253 -> 0x454)
+fontchar 0 141 31 51 1 8 30 // Ґ (254 -> 0x490)
+fontchar 314 328 25 41 0 18 23 // ґ (255 -> 0x491)
diff --git a/config/fonts/reduced.cfg b/config/fonts/reduced.cfg
new file mode 100644
index 0000000..23c8430
--- /dev/null
+++ b/config/fonts/reduced.cfg
@@ -0,0 +1,3 @@
+fontalias "reduced" "default"
+fontscale 48
+
diff --git a/config/fonts/super.cfg b/config/fonts/super.cfg
new file mode 100644
index 0000000..c4d7bf1
--- /dev/null
+++ b/config/fonts/super.cfg
@@ -0,0 +1,3 @@
+fontalias "super" "default"
+fontscale 68
+
diff --git a/config/fonts/tiny.cfg b/config/fonts/tiny.cfg
new file mode 100644
index 0000000..5ab54bb
--- /dev/null
+++ b/config/fonts/tiny.cfg
@@ -0,0 +1,3 @@
+fontalias "tiny" "default"
+fontscale 30
+
diff --git a/config/glsl.cfg b/config/glsl.cfg
new file mode 100644
index 0000000..cc4e37c
--- /dev/null
+++ b/config/glsl.cfg
@@ -0,0 +1,2395 @@
+// standard shader definitions
+
+lazyshader = [
+    defershader $arg1 $arg2 [
+        shader @arg1 @arg2 [@@arg3] [@@arg4]
+    ]
+]
+
+lmcoordscale = (divf 1 32767)
+
+///////////////////////////////////////////////////
+//
+// used for any textured polys that don't have a shader set
+//
+///////////////////////////////////////////////////
+
+shader 4 "default" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = gl_MultiTexCoord0;
+        gl_FrontColor = gl_Color;
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        gl_FragColor = gl_Color * texture2D(tex0, gl_TexCoord[0].xy);
+    }
+]
+
+shader 4 "rect" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = gl_MultiTexCoord0;
+        gl_FrontColor = gl_Color;
+    }
+] [
+    #extension GL_ARB_texture_rectangle : enable
+    uniform sampler2DRect tex0;
+    void main(void)
+    {
+        gl_FragColor = gl_Color * texture2DRect(tex0, gl_TexCoord[0].xy);
+    }
+]
+
+shader 4 "cubemap" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = gl_MultiTexCoord0;
+        gl_FrontColor = gl_Color;
+    }
+] [
+    uniform samplerCube tex0;
+    void main(void)
+    {
+        gl_FragColor = gl_Color * textureCube(tex0, gl_TexCoord[0].xyz);
+    }
+]
+
+shader 4 "rgbonly" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = gl_MultiTexCoord0;
+        gl_FrontColor = gl_Color;
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        gl_FragColor.rgb = gl_Color.rgb * texture2D(tex0, gl_TexCoord[0].xy).rgb;
+        gl_FragColor.a   = gl_Color.a;
+    }
+]
+
+//////////////////////////////////////////////////////////////////////
+//
+// same, but now without texture sampling (some HUD stuff needs this)
+//
+//////////////////////////////////////////////////////////////////////
+
+shader 4 "notexture" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_Color;
+    }
+]
+
+shader 4 "notextureglsl" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_Color;
+    }
+]
+
+//////////////////////////////////////////////////////////////////////
+//
+// fogged variants of default shaders
+//
+//////////////////////////////////////////////////////////////////////
+
+shader 4 "fogged" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = gl_MultiTexCoord0;
+        gl_FrontColor = gl_Color;
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        gl_FragColor = gl_Color * texture2D(tex0, gl_TexCoord[0].xy);
+    }
+]
+
+shader 4 "foggednotexture" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_Color;
+    }
+]
+
+shader 4 "foggednotextureglsl" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_Color;
+    }
+]
+
+//////////////////////////////////////////////////////////////////////
+//
+// for filling the z-buffer only (i.e. multi-pass rendering, OQ)
+//
+//////////////////////////////////////////////////////////////////////
+
+shader 4 "nocolor" [
+    void main() { gl_Position = ftransform(); }
+] [
+    void main() {}
+]
+
+shader 4 "nocolorglsl" [
+    void main() { gl_Position = ftransform(); }
+] [
+    void main() {}
+]
+
+////////////////////////////////////////////////////////
+//
+// default lightmapped world shader.. does texcoord gen
+//
+///////////////////////////////////////////////////////
+
+worldshader = [
+    stype = 4
+    if (>= (stringstr $arg1 "env") 0) [stype = (+ $stype 2)]
+    shader $stype $arg1 [
+        #pragma CUBE2_fog
+        @(if (>= $numargs 5) [result $arg5])
+        uniform vec4 texgenscroll;
+        void main(void)
+        {
+            gl_Position = ftransform();
+            gl_TexCoord[0].xy = gl_MultiTexCoord0.xy + texgenscroll.xy;
+            gl_TexCoord[1].xy = gl_MultiTexCoord1.xy * @lmcoordscale;
+
+            @arg2
+
+            #pragma CUBE2_shadowmap
+            #pragma CUBE2_dynlight
+            #pragma CUBE2_water
+        }
+    ] [
+        @(if (>= $numargs 5) [result $arg5])
+        @(if (>= $numargs 6) [result $arg6])
+        uniform vec4 colorparams;
+        uniform sampler2D diffusemap, lightmap;
+        void main(void)
+        {
+            vec4 diffuse = texture2D(diffusemap, gl_TexCoord[0].xy);
+            vec4 lm = texture2D(lightmap, gl_TexCoord[1].xy);
+
+            #pragma CUBE2_shadowmap lm
+            #pragma CUBE2_dynlight lm
+
+            @arg3
+
+            diffuse *= colorparams;
+            @(if (|| (< $numargs 4) [=s $arg4 []]) [result [gl_FragColor = diffuse * lm;]] [result $arg4])
+
+            #pragma CUBE2_water
+        }
+    ]
+]
+
+glareworldshader = [
+    variantshader (if (< (stringstr $arg1 "env") 0) 4 6) $arg1 4 [
+        #pragma CUBE2_fog
+        @(if (>= $numargs 4) [result $arg4])
+        uniform vec4 texgenscroll;
+        void main(void)
+        {
+            gl_Position = ftransform();
+            gl_TexCoord[0].xy = gl_MultiTexCoord0.xy + texgenscroll.xy;
+            gl_TexCoord[1].xy = gl_MultiTexCoord1.xy * @lmcoordscale;
+
+            @arg2
+        }
+    ] [
+        @(if (>= $numargs 4) [result $arg4])
+        @(if (>= $numargs 5) [result $arg5])
+        uniform vec4 colorparams;
+        void main(void)
+        {
+            @arg3
+        }
+    ]
+]
+
+worldshader "stdworld" [] []
+
+defershader 4 "decalworld" [
+    worldshader "decalworld" [] [
+        vec4 diffuse2 = texture2D(decal, gl_TexCoord[0].xy);
+        diffuse.rgb = mix(diffuse.rgb, diffuse2.rgb, diffuse2.a);
+    ] [] [] [uniform sampler2D decal;]
+]
+
+defershader 4 "glowworld" [
+    defuniformparam "glowcolor" 1 1 1 // glow color
+    worldshader "glowworld" [] [] [
+        vec3 glow = texture2D(glowmap, gl_TexCoord[0].xy).rgb;
+        glow *= glowcolor.rgb;
+        gl_FragColor = diffuse*lm + vec4(glow, 0.0);
+    ] [] [uniform sampler2D glowmap;]
+    glareworldshader "glowworld" [] [
+        vec3 glow = texture2D(glowmap, gl_TexCoord[0].xy).rgb;
+        glow *= glowcolor.rgb;
+        float k = max(glow.r, max(glow.g, glow.b));
+        gl_FragColor.rgb = min(k*k*32.0, 1.0) * glow;
+        #pragma CUBE2_variantoverride gl_FragColor.a = texture2D(lightmap, gl_TexCoord[1].xy).a;
+        gl_FragColor.a = colorparams.a;
+    ] [] [
+        uniform sampler2D glowmap;
+        #pragma CUBE2_variant uniform sampler2D lightmap;
+    ]
+]
+
+defershader 4 "pulseworld" [
+    defuniformparam "pulsespeed" 1 // pulse frequency (Hz)
+    worldshader "pulseworld" [
+        pulse = abs(fract(millis.x * pulsespeed.x)*2.0 - 1.0);
+    ] [
+        vec3 diffuse2 = texture2D(decal, gl_TexCoord[0].xy).rgb;
+        diffuse.rgb = mix(diffuse.rgb, diffuse2, pulse);
+    ] [] [uniform vec4 millis; varying float pulse;] [uniform sampler2D decal;]
+]
+
+defershader 4 "pulseglowworld" [
+    defuniformparam "glowcolor" 1 1 1 // glow color
+    defuniformparam "pulseglowspeed" 1 // pulse frequency (Hz)
+    defuniformparam "pulseglowcolor" 0 0 0 // pulse glow color
+    worldshader "pulseglowworld" [
+        pulse = mix(glowcolor.rgb, pulseglowcolor.rgb, abs(fract(millis.x * pulseglowspeed.x)*2.0 - 1.0));
+    ] [] [
+        vec3 glow = texture2D(glowmap, gl_TexCoord[0].xy).rgb;
+        gl_FragColor = diffuse*lm + vec4(glow*pulse, 0.0);
+    ] [uniform vec4 millis; varying vec3 pulse;] [uniform sampler2D glowmap;]
+    glareworldshader "pulseglowworld" [
+        pulse = mix(glowcolor.rgb, pulseglowcolor.rgb, abs(fract(millis.x * pulseglowspeed.x)*2.0 - 1.0));
+    ] [
+        vec3 glow = texture2D(glowmap, gl_TexCoord[0].xy).rgb;
+        glow *= pulse;
+        float k = max(glow.r, max(glow.g, glow.b));
+        gl_FragColor.rgb = min(k*k*32.0, 1.0) * glow;
+        #pragma CUBE2_variantoverride gl_FragColor.a = texture2D(lightmap, gl_TexCoord[1].xy).a;
+        gl_FragColor.a = colorparams.a;
+    ] [uniform vec4 millis; varying vec3 pulse;] [
+        uniform sampler2D glowmap;
+        #pragma CUBE2_variant uniform sampler2D lightmap;
+    ]
+]
+
+shader 4 "fogworld" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        #pragma CUBE2_water
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_Fog.color;
+        #pragma CUBE2_water
+    }
+]
+
+shader 4 "noglareworld" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = vec4(0.0);
+    }
+]
+
+shader 4 "noglareblendworld" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0].xy = gl_MultiTexCoord1.xy * @lmcoordscale;
+    }
+] [
+    uniform sampler2D lightmap;
+    void main(void)
+    {
+        gl_FragColor.rgb = vec3(0.0);
+        gl_FragColor.a = texture2D(lightmap, gl_TexCoord[0].xy).a;
+    }
+]
+
+shader 4 "noglarealphaworld" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+    }
+] [
+    uniform vec4 colorparams;
+    uniform sampler2D lightmap;
+    void main(void)
+    {
+        gl_FragColor.rgb = vec3(0.0);
+        gl_FragColor.a = colorparams.a;
+    }
+]
+
+defershader 6 "envworld" [
+    defuniformparam "envscale" 0.2 0.2 0.2 // reflectivity
+    worldshader "envworld" [
+        normal = gl_Normal;
+        camvec = camera.xyz - gl_Vertex.xyz;
+    ] [
+        vec3 reflect = textureCube(envmap, 2.0*normal*dot(camvec, normal) - camvec).rgb;
+    ] [
+        diffuse *= lm;
+        gl_FragColor.rgb = mix(diffuse.rgb, reflect, envscale.rgb);
+        gl_FragColor.a = diffuse.a;
+    ] [uniform vec4 camera; varying vec3 normal, camvec;] [uniform samplerCube envmap;]
+
+    defuniformparam "envscale" 0.2 0.2 0.2 // reflectivity
+    worldshader "envworldfast" [
+        vec3 camvec = camera.xyz - gl_Vertex.xyz;
+        rvec = 2.0*gl_Normal*dot(camvec, gl_Normal) - camvec;
+    ] [
+        vec3 reflect = textureCube(envmap, rvec).rgb;
+    ] [
+        diffuse *= lm;
+        gl_FragColor.rgb = mix(diffuse.rgb, reflect, envscale.rgb);
+        gl_FragColor.a = diffuse.a;
+    ] [uniform vec4 camera; varying vec3 rvec;] [uniform samplerCube envmap;]
+
+    defuniformparam "envscale" 0.2 0.2 0.2 // reflectivity
+    worldshader "envworldalt" [] []
+
+    altshader envworld envworldalt
+    fastshader envworld envworldfast 2
+    fastshader envworld envworldalt 1
+]
+
+shader 4 "depthfxworld" [
+    uniform vec4 depthscale, depthoffsets;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = depthoffsets - (gl_ModelViewMatrix * gl_Vertex).z*depthscale;
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_TexCoord[0];
+    }
+]
+
+shader 4 depthfxsplitworld [
+    uniform vec4 depthscale, depthoffsets;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = depthoffsets - (gl_ModelViewMatrix * gl_Vertex).z*depthscale;
+    }
+] [
+    void main(void)
+    {
+        vec4 ranges = vec4(gl_TexCoord[0].x, fract(gl_TexCoord[0].yzw));
+        ranges.xy -= ranges.yz*vec2(0.00390625, 0.00390625);
+        gl_FragColor = ranges;
+    }
+]
+
+// bumptype:
+//    e -> reserve envmap texture slot
+//    o -> orthonormalize
+//    t -> tangent space cam
+//    r -> envmap reflection
+//    R -> modulate envmap reflection with spec map
+//    s -> spec
+//    S -> spec map
+//    p -> parallax
+//    P -> steep parallax (7 steps)
+//    g -> glow
+//    G -> pulse glow
+//    i -> glare intensity
+
+btopt = [ >= (stringstr $bumptype $arg1) 0 ]
+
+bumpvariantshader = [
+    bumptype = $arg2
+    stype = (? (btopt "e") 7 5)
+    if (! (btopt "i")) [
+        if (btopt "G") [
+            defuniformparam "glowcolor" 1 1 1 // glow color
+            defuniformparam "pulseglowspeed" 1     // pulse frequency (Hz)
+            defuniformparam "pulseglowcolor" 0 0 0 // pulse glow color
+        ] [if (btopt "g") [
+            defuniformparam "glowcolor" 1 1 1  // glow color
+        ]]
+        if (btopt "S") [
+            defuniformparam "specscale" 6 6 6 // spec map multiplier
+        ] [if (btopt "s") [
+            defuniformparam "specscale" 1 1 1 // spec multiplier
+        ]]
+        if (|| (btopt "p") (btopt "P")) [
+            defuniformparam "parallaxscale" 0.06 -0.03 // parallax scaling
+        ]
+        if (btopt "R") [
+            defuniformparam "envscale" 1 1 1 // reflectivity map multiplier
+        ] [if (btopt "r") [
+            defuniformparam "envscale" 0.2 0.2 0.2 // reflectivity
+        ]]
+    ] [
+        if (btopt "s") [stype = (+ $stype 8)]
+    ]
+    variantshader $stype $arg1 (? (btopt "i") 4 -1) [
+        #pragma CUBE2_fog
+        uniform vec4 texgenscroll;
+        @(if (|| (btopt "t") (btopt "r")) [result [uniform vec4 camera; varying vec3 camvec;]])
+        @(if (btopt "G") [result [uniform vec4 millis; varying float pulse;]])
+        @(if (btopt "r") [result [varying mat3 world;]])
+        void main(void)
+        {
+            gl_Position = ftransform();
+            gl_TexCoord[0].xy = gl_MultiTexCoord0.xy + texgenscroll.xy;
+            // need to store these in Z/W to keep texcoords < 6, otherwise kills performance on Radeons
+            // but slows lightmap access in fragment shader a bit, so avoid when possible
+            @(if (|| $minimizetcusage (btopt "r")) [result [
+                gl_TexCoord[0].zw = gl_MultiTexCoord1.yx * @lmcoordscale;
+            ]] [result [
+                gl_TexCoord[1].xy = gl_MultiTexCoord1.xy * @lmcoordscale;
+            ]])
+
+            @(if (btopt "o") [result [
+                vec4 tangent = gl_Color*2.0 - 1.0;
+                vec3 bitangent = cross(gl_Normal, tangent.xyz) * tangent.w;
+                @@(if (btopt "t") [result [
+                    // trans eye vector into TS
+                    vec3 camobj = camera.xyz - gl_Vertex.xyz;
+                    camvec = vec3(dot(camobj, tangent.xyz), dot(camobj, bitangent), dot(camobj, gl_Normal));
+                ]])
+                @@(if (btopt "r") [result [
+                    @@(if (! (btopt "t")) [result [
+                        camvec = camera.xyz - gl_Vertex.xyz;
+                    ]])
+                    // calculate tangent -> world transform
+                    world = mat3(tangent.xyz, bitangent, gl_Normal);
+                ]])
+            ]])
+
+            @(if (btopt "G") [result [
+                pulse = abs(fract(millis.x*pulseglowspeed.x)*2.0 - 1.0);
+            ]])
+
+            @(if (|| (! (btopt "i")) (btopt "s")) [result [
+                #pragma CUBE2_dynlight
+            ]])
+            @(if (! (btopt "i")) [result [
+                #pragma CUBE2_shadowmap
+                #pragma CUBE2_water
+            ]])
+        }
+    ] [
+        uniform vec4 colorparams;
+        uniform sampler2D diffusemap, lmcolor, lmdir;
+        @(if (|| (! (btopt "i")) (btopt "s") (btopt "p") (btopt "P")) [result [uniform sampler2D normalmap;]])
+        @(if (|| (btopt "t") (btopt "r")) [result [varying vec3 camvec;]])
+        @(if (btopt "g") [result [uniform sampler2D glowmap;]])
+        @(if (btopt "G") [result [varying float pulse;]])
+        @(if (btopt "r") [result [uniform samplerCube envmap; varying mat3 world;]])
+        @(if (|| (! (btopt "i")) (btopt "s")) [result [uniform vec4 ambient;]])
+        void main(void)
+        {
+            #define lmtc @(? (|| $minimizetcusage (btopt "r")) "gl_TexCoord[0].wz" "gl_TexCoord[1].xy")
+            @(if (|| (! (btopt "i")) (btopt "s")) [result [
+                vec4 lmc = texture2D(lmcolor, lmtc);
+                gl_FragColor.a = colorparams.a * lmc.a;
+                vec3 lmlv = texture2D(lmdir, lmtc).rgb*2.0 - 1.0;
+            ]])
+            @(if (btopt "t") [result [vec3 camdir = normalize(camvec);]])
+            @(if (btopt "p") [result [
+                float height = texture2D(normalmap, gl_TexCoord[0].xy).a;
+                vec2 dtc = gl_TexCoord[0].xy + camdir.xy*(height*parallaxscale.x + parallaxscale.y);
+            ]])
+            @(if (btopt "P") [result [
+                const float step = -1.0/7.0;
+                vec3 duv = vec3((step*parallaxscale.x/camdir.z)*camdir.xy, step);
+                vec3 htc = vec3(gl_TexCoord[0].xy + duv.xy*parallaxscale.y, 1.0);
+                vec4 height = texture2D(normalmap, htc.xy);
+                @@(loopconcat i 7 [concatword [
+                    htc += height.w < htc.z ? duv : vec3(0.0);
+                    height = texture2D(normalmap, htc.xy);
+                ]])
+                #define dtc htc.xy
+                #define bump height.xyz
+            ]])
+            @(if (|| (btopt "p") (btopt "P")) [] [result [#define dtc gl_TexCoord[0].xy]])
+
+            @(if (|| (! (btopt "i")) (btopt "S")) [result [
+                vec4 diffuse = texture2D(diffusemap, dtc);
+            ]])
+            @(if (! (btopt "i")) [result [
+                diffuse.rgb *= colorparams.rgb;
+            ]])
+
+            @(if (|| (! (btopt "i")) (btopt "s")) [result [
+                @(if (! (btopt "P")) [result [vec3 bump = texture2D(normalmap, dtc).rgb;]])
+                bump = bump*2.0 - 1.0;
+            ]])
+
+            @(if (btopt "s") [result [
+                vec3 halfangle = normalize(camdir + lmlv);
+                float spec = pow(clamp(dot(halfangle, bump), 0.0, 1.0), @(? (btopt "i") "128.0" "32.0"));
+                @(if (btopt "i") [result [spec = min(spec*64.0, 1.0);]])
+                @(if (btopt "S") [result [spec *= diffuse.a;]])
+                @(if (btopt "i") [result [
+                    @(? (btopt "S") "diffuse.rgb" "vec3 diffuse") = specscale.xyz*spec;
+                ]] [result [
+                    diffuse.rgb += specscale.xyz*spec;
+                ]])
+            ]])
+
+            @(if (|| (! (btopt "i")) (btopt "s")) [result [
+                lmc.rgb = max(lmc.rgb*clamp(dot(lmlv, bump), 0.0, 1.0), ambient.xyz);
+                @(if (btopt "i") [result [
+                    #pragma CUBE2_dynlight lmc
+
+                    @(? (btopt "g") "diffuse.rgb" "gl_FragColor.rgb") = diffuse.rgb * lmc.rgb;
+                ]] [result [
+                    #pragma CUBE2_shadowmap lmc
+                    #pragma CUBE2_dynlight lmc
+
+                    @(? (|| (btopt "g") (btopt "r")) "diffuse.rgb" "gl_FragColor.rgb") = diffuse.rgb * lmc.rgb;
+                ]])
+            ]])
+
+            @(if (btopt "r") [result [
+                vec3 rvec;
+                @(if (btopt "t") [result [
+                    vec3 rvects = 2.0*bump*dot(camvec, bump) - camvec;
+                    rvec = world * rvects;
+                ]] [result [
+                    vec3 bumpw = world * bump;
+                    rvec = 2.0*bumpw*dot(camvec, bumpw) - camvec;
+                ]])
+                vec3 reflect = textureCube(envmap, rvec).rgb;
+                @@(if (btopt "R") [result [
+                    vec3 rmod = envscale.xyz*diffuse.a;
+                ]] [result [
+                    #define rmod envscale.xyz
+                ]])
+                @(? (btopt "g") "diffuse.rgb" "gl_FragColor.rgb") = mix(diffuse.rgb, reflect, rmod);
+            ]])
+
+            @(if (btopt "g") [result [
+                vec3 glow = texture2D(glowmap, dtc).rgb;
+                @@(if (btopt "G") [result [
+                    vec3 pulsecol = mix(glowcolor.xyz, pulseglowcolor.xyz, pulse);
+                ]])
+                @@(if (btopt "i") [result [
+                    glow *= @(? (btopt "G") "pulsecol" "glowcolor.xyz");
+                    float k = max(glow.r, max(glow.g, glow.b));
+                    k = min(k*k*32.0, 1.0);
+                    @(if (btopt "s") [result [
+                        gl_FragColor.rgb = glow*k + diffuse.rgb;
+                    ]] [result [
+                        gl_FragColor.rgb = glow*k;
+                        #pragma CUBE2_variantoverride gl_FragColor.a = texture2D(lmcolor, lmtc).a;
+                        gl_FragColor.a = colorparams.a;
+                    ]])
+                ]] [result [
+                    gl_FragColor.rgb = glow * @(? (btopt "G") "pulsecol" "glowcolor.xyz") + diffuse.rgb;
+                ]])
+            ]])
+
+            @(if (! (btopt "i")) [result [
+                #pragma CUBE2_water
+            ]])
+        }
+    ]
+]
+
+bumpshader = [
+    defershader (if (>= (stringstr $arg2 "e") 0) [result 7] [result 5]) $arg1 [
+        bumpvariantshader @arg1 @arg2
+        if (|| (btopt "g") (btopt "s")) [
+            bumpvariantshader @@arg1 (stringreplace (concatword @@arg2 "i") "r")
+        ]
+    ]
+]
+
+bumpshader "bumpworld" ""
+bumpshader "bumpspecworld" "ots"
+fastshader bumpspecworld bumpworld 2
+altshader bumpspecworld bumpworld
+bumpshader "bumpspecmapworld" "otsS"
+fastshader bumpspecmapworld bumpworld 2
+altshader bumpspecmapworld bumpworld
+
+bumpshader "bumpglowworld" "g"
+bumpshader "bumpspecglowworld" "otsg"
+altshader bumpspecglowworld bumpglowworld
+bumpshader "bumpspecmapglowworld" "otsSg"
+fastshader bumpspecmapglowworld bumpglowworld 2
+altshader bumpspecmapglowworld bumpglowworld
+
+bumpshader "bumppulseglowworld" "gG"
+bumpshader "bumpspecpulseglowworld" "otsgG"
+altshader bumpspecpulseglowworld bumppulseglowworld
+bumpshader "bumpspecmappulseglowworld" "otsSgG"
+fastshader bumpspecmappulseglowworld bumppulseglowworld 2
+altshader bumpspecmappulseglowworld bumppulseglowworld
+
+bumpshader "bumpparallaxworld" "pot"
+fastshader bumpparallaxworld bumpworld 1
+altshader bumpparallaxworld bumpworld
+bumpshader "bumpspecparallaxworld" "pots"
+fastshader bumpspecparallaxworld bumpparallaxworld 2
+fastshader bumpspecparallaxworld bumpworld 1
+altshader bumpspecparallaxworld bumpworld
+bumpshader "bumpspecmapparallaxworld" "potsS"
+fastshader bumpspecmapparallaxworld bumpparallaxworld 2
+fastshader bumpspecmapparallaxworld bumpworld 1
+altshader bumpspecmapparallaxworld bumpworld
+
+bumpshader "bumpparallaxglowworld" "potg"
+fastshader bumpparallaxglowworld bumpglowworld 1
+altshader bumpparallaxglowworld bumpglowworld
+bumpshader "bumpspecparallaxglowworld" "potsg"
+fastshader bumpspecparallaxglowworld bumpparallaxglowworld 2
+fastshader bumpspecparallaxglowworld bumpglowworld 1
+altshader bumpspecparallaxglowworld bumpglowworld
+bumpshader "bumpspecmapparallaxglowworld" "potsSg"
+fastshader bumpspecmapparallaxglowworld bumpparallaxglowworld 2
+fastshader bumpspecmapparallaxglowworld bumpglowworld 1
+altshader bumpspecmapparallaxglowworld bumpglowworld
+
+bumpshader "bumpparallaxpulseglowworld" "potgG"
+fastshader bumpparallaxpulseglowworld bumppulseglowworld 1
+altshader bumpparallaxpulseglowworld bumppulseglowworld
+bumpshader "bumpspecparallaxpulseglowworld" "potsgG"
+fastshader bumpspecparallaxpulseglowworld bumpparallaxpulseglowworld 2
+fastshader bumpspecparallaxpulseglowworld bumppulseglowworld 1
+altshader bumpspecparallaxpulseglowworld bumppulseglowworld
+bumpshader "bumpspecmapparallaxpulseglowworld" "potsSgG"
+fastshader bumpspecmapparallaxpulseglowworld bumpparallaxpulseglowworld 2
+fastshader bumpspecmapparallaxpulseglowworld bumppulseglowworld 1
+altshader bumpspecmapparallaxpulseglowworld bumppulseglowworld
+
+bumpshader "bumpenvworldalt" "e"
+bumpshader "bumpenvworld" "eor"
+altshader bumpenvworld bumpenvworldalt
+fastshader bumpenvworld bumpenvworldalt 2
+bumpshader "bumpenvspecworld" "eotsr"
+altshader bumpenvspecworld bumpenvworldalt
+fastshader bumpenvspecworld bumpenvworldalt 2
+bumpshader "bumpenvspecmapworld" "eotsSrR"
+altshader bumpenvspecmapworld bumpenvworldalt
+fastshader bumpenvspecmapworld bumpenvworldalt 2
+
+bumpshader "bumpenvglowworldalt" "eg"
+bumpshader "bumpenvglowworld" "eorg"
+altshader bumpenvglowworld bumpenvglowworldalt
+fastshader bumpenvglowworld bumpenvglowworldalt 2
+bumpshader "bumpenvspecglowworld" "eotsrg"
+altshader bumpenvspecglowworld bumpenvglowworldalt
+fastshader bumpenvspecglowworld bumpenvglowworldalt 2
+bumpshader "bumpenvspecmapglowworld" "eotsSrRg"
+altshader bumpenvspecmapglowworld bumpenvglowworldalt
+fastshader bumpenvspecmapglowworld bumpenvglowworldalt 2
+
+bumpshader "bumpenvpulseglowworldalt" "egG"
+bumpshader "bumpenvpulseglowworld" "eorgG"
+altshader bumpenvpulseglowworld bumpenvpulseglowworldalt
+fastshader bumpenvpulseglowworld bumpenvpulseglowworldalt 2
+bumpshader "bumpenvspecpulseglowworld" "eotsrgG"
+altshader bumpenvspecpulseglowworld bumpenvpulseglowworldalt
+fastshader bumpenvspecpulseglowworld bumpenvpulseglowworldalt 2
+bumpshader "bumpenvspecmappulseglowworld" "eotsSrRgG"
+altshader bumpenvspecmappulseglowworld bumpenvpulseglowworldalt
+fastshader bumpenvspecmappulseglowworld bumpenvpulseglowworldalt 2
+
+bumpshader "bumpenvparallaxworldalt" "epot"
+altshader bumpenvparallaxworldalt bumpenvworldalt
+bumpshader "bumpenvparallaxworld" "epotr"
+altshader bumpenvparallaxworld bumpenvparallaxworldalt
+fastshader bumpenvparallaxworld bumpenvparallaxworldalt 2
+fastshader bumpenvparallaxworld bumpenvworldalt 1
+bumpshader "bumpenvspecparallaxworld" "epotsr"
+altshader bumpenvspecparallaxworld bumpenvparallaxworldalt
+fastshader bumpenvspecparallaxworld bumpenvparallaxworldalt 2
+fastshader bumpenvspecparallaxworld bumpenvworldalt 1
+bumpshader "bumpenvspecmapparallaxworld" "epotsSrR"
+altshader bumpenvspecmapparallaxworld bumpenvparallaxworldalt
+fastshader bumpenvspecmapparallaxworld bumpenvparallaxworldalt 2
+fastshader bumpenvspecmapparallaxworld bumpenvworldalt 1
+
+bumpshader "bumpenvparallaxglowworldalt" "epotg"
+altshader bumpenvparallaxglowworldalt bumpenvglowworldalt
+bumpshader "bumpenvparallaxglowworld" "epotrg"
+altshader bumpenvparallaxglowworld bumpenvparallaxglowworldalt
+fastshader bumpenvparallaxglowworld bumpenvparallaxglowworldalt 2
+fastshader bumpenvparallaxglowworld bumpenvglowworldalt 1
+bumpshader "bumpenvspecparallaxglowworld" "epotsrg"
+altshader bumpenvspecparallaxglowworld bumpenvparallaxglowworldalt
+fastshader bumpenvspecparallaxglowworld bumpenvparallaxglowworldalt 2
+fastshader bumpenvspecparallaxglowworld bumpenvglowworldalt 1
+bumpshader "bumpenvspecmapparallaxglowworld" "epotsSrRg"
+altshader bumpenvspecmapparallaxglowworld bumpenvparallaxglowworldalt
+fastshader bumpenvspecmapparallaxglowworld bumpenvparallaxglowworldalt 2
+fastshader bumpenvspecmapparallaxglowworld bumpenvglowworldalt 1
+
+bumpshader "bumpenvparallaxpulseglowworldalt" "epotgG"
+altshader bumpenvparallaxpulseglowworldalt bumpenvpulseglowworldalt
+bumpshader "bumpenvparallaxpulseglowworld" "epotrgG"
+altshader bumpenvparallaxpulseglowworld bumpenvparallaxpulseglowpulseglowworldalt
+fastshader bumpenvparallaxpulseglowworld bumpenvparallaxpulseglowpulseglowworldalt 2
+fastshader bumpenvparallaxpulseglowworld bumpenvpulseglowworldalt 1
+bumpshader "bumpenvspecparallaxpulseglowworld" "epotsrgG"
+altshader bumpenvspecparallaxpulseglowworld bumpenvparallaxpulseglowworldalt
+fastshader bumpenvspecparallaxpulseglowworld bumpenvparallaxpulseglowworldalt 2
+fastshader bumpenvspecparallaxpulseglowworld bumpenvpulseglowworldalt 1
+bumpshader "bumpenvspecmapparallaxpulseglowworld" "epotsSrRgG"
+altshader bumpenvspecmapparallaxpulseglowworld bumpenvparallaxpulseglowworldalt
+fastshader bumpenvspecmapparallaxpulseglowworld bumpenvparallaxpulseglowworldalt 2
+fastshader bumpenvspecmapparallaxpulseglowworld bumpenvpulseglowworldalt 1
+
+//bumpshader "steepworld" "Pot"
+
+////////////////////////////////////////////////
+//
+// phong lighting model shader
+//
+////////////////////////////////////////////////
+
+// skeletal animation for matrices and dual quaternions
+
+skelanimdefs = [
+    skelanimlength = (min (- $maxvsuniforms (+ $reservevpparams 10)) $maxskelanimdata)
+    result [
+        #pragma CUBE2_attrib vweights 6
+        #pragma CUBE2_attrib vbones 7
+        attribute vec4 vweights;
+        attribute vec4 vbones;
+        #pragma CUBE2_uniform animdata
+        uniform vec4 animdata[@@skelanimlength];
+    ]
+]
+
+skelmatanim = [
+    result [
+        int index = int(vbones.x);
+        @(if (= $arg1 1) [result [
+            vec4 mx = animdata[index];
+            vec4 my = animdata[index+1];
+            vec4 mz = animdata[index+2];
+        ]] [result [
+            vec4 mx = animdata[index] * vweights.x;
+            vec4 my = animdata[index+1] * vweights.x;
+            vec4 mz = animdata[index+2] * vweights.x;
+            index = int(vbones.y);
+            mx += animdata[index] * vweights.y;
+            my += animdata[index+1] * vweights.y;
+            mz += animdata[index+2] * vweights.y;
+        ]])
+        @(if (>= $arg1 3) [result [
+            index = int(vbones.z);
+            mx += animdata[index] * vweights.z;
+            my += animdata[index+1] * vweights.z;
+            mz += animdata[index+2] * vweights.z;
+        ]])
+        @(if (>= $arg1 4) [result [
+            index = int(vbones.w);
+            mx += animdata[index] * vweights.w;
+            my += animdata[index+1] * vweights.w;
+            mz += animdata[index+2] * vweights.w;
+        ]])
+
+        vec4 opos = vec4(dot(mx, gl_Vertex), dot(my, gl_Vertex), dot(mz, gl_Vertex), gl_Vertex.w);
+
+        @(if $arg2 [result [
+            vec3 onormal = vec3(dot(mx.xyz, gl_Normal), dot(my.xyz, gl_Normal), dot(mz.xyz, gl_Normal));
+        ]])
+
+        @(if $arg3 [result [
+            vec3 otangent = vec3(dot(mx.xyz, vtangent.xyz), dot(my.xyz, vtangent.xyz), dot(mz.xyz, vtangent.xyz));
+        ]])
+    ]
+]
+
+skelquatanim = [
+    result [
+        int index = int(vbones.x);
+        @(if (= $arg1 1) [result [
+            vec4 dqreal = animdata[index];
+            vec4 dqdual = animdata[index+1];
+        ]] [result [
+            vec4 dqreal = animdata[index] * vweights.x;
+            vec4 dqdual = animdata[index+1] * vweights.x;
+            index = int(vbones.y);
+            dqreal += animdata[index] * vweights.y;
+            dqdual += animdata[index+1] * vweights.y;
+            @(if (>= $arg1 3) [result [
+                index = int(vbones.z);
+                dqreal += animdata[index] * vweights.z;
+                dqdual += animdata[index+1] * vweights.z;
+            ]])
+            @(if (>= $arg1 4) [result [
+                index = int(vbones.w);
+                dqreal += animdata[index] * vweights.w;
+                dqdual += animdata[index+1] * vweights.w;
+            ]])
+            float len = length(dqreal);
+            dqreal /= len;
+            dqdual /= len;
+        ]])
+
+        vec4 opos = vec4((cross(dqreal.xyz, cross(dqreal.xyz, gl_Vertex.xyz) + gl_Vertex.xyz*dqreal.w + dqdual.xyz) + dqdual.xyz*dqreal.w - dqreal.xyz*dqdual.w)*2.0 + gl_Vertex.xyz, gl_Vertex.w);
+
+        @(if $arg2 [result [
+            vec3 onormal = cross(dqreal.xyz, cross(dqreal.xyz, gl_Normal) + gl_Normal*dqreal.w)*2.0 + gl_Normal;
+        ]])
+
+        @(if $arg3 [result [
+            vec3 otangent = cross(dqreal.xyz, cross(dqreal.xyz, vtangent.xyz) + vtangent.xyz*dqreal.w)*2.0 + vtangent.xyz;
+        ]])
+    ]
+]
+
+// model shadowmapping
+
+shadowmapcastervertexshader = [
+    result [
+        @(if (>= $numargs 2) [result $arg1])
+            uniform vec4 shadowintensity;
+        @(if (>= $numargs 2) [result [
+            void main(void)
+            {
+                @arg2
+                gl_Position = gl_ModelViewProjectionMatrix * opos;
+        ]] [result [
+            void main(void)
+            {
+                gl_Position = ftransform();
+        ]])
+                gl_TexCoord[0] = vec4(1.0 - gl_Position.z, 1.0, 0.0, shadowintensity.x);
+            }
+    ]
+]
+
+shader 4 shadowmapcaster (shadowmapcastervertexshader) [
+    void main(void)
+    {
+        gl_FragColor = gl_TexCoord[0];
+    }
+]
+loop i 4 [
+    variantshader 4 shadowmapcaster 0 (shadowmapcastervertexshader (skelanimdefs (+ $i 1) 0 0) (skelmatanim (+ $i 1) 0 0)) []
+    variantshader 4 shadowmapcaster 1 (shadowmapcastervertexshader (skelanimdefs (+ $i 1) 0 0) (skelquatanim (+ $i 1) 0 0)) []
+]
+
+shader 4 "shadowmapreceiver" [
+    uniform vec4 shadowmapbias;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = vec4(0.0, 0.0, shadowmapbias.y - gl_Position.z, 0.0);
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_TexCoord[0];
+    }
+]
+
+// model stencil shadows
+
+notexturemodelvertexshader = [
+    result [
+        @(if (>= $numargs 2) [result [
+            @arg1
+            #pragma CUBE2_fog opos
+            void main(void)
+            {
+                @arg2
+                gl_Position = gl_ModelViewProjectionMatrix * opos;
+        ]] [result [
+            #pragma CUBE2_fog
+            void main(void)
+            {
+                gl_Position = ftransform();
+        ]])
+                gl_FrontColor = gl_Color;
+            }
+    ]
+]
+
+shader 4 notexturemodel (notexturemodelvertexshader) [
+    void main(void)
+    {
+        gl_FragColor = gl_Color;
+    }
+]
+loop i 4 [
+    variantshader 4 notexturemodel 0 (notexturemodelvertexshader (skelanimdefs (+ $i 1) 0 0) (skelmatanim (+ $i 1) 0 0)) []
+    variantshader 4 notexturemodel 1 (notexturemodelvertexshader (skelanimdefs (+ $i 1) 0 0) (skelquatanim (+ $i 1) 0 0)) []
+]
+
+// mdltype:
+//    e -> envmap
+//    n -> normalmap
+//    s -> spec
+//    m -> masks
+//    B -> matrix skeletal animation
+//    b -> dual-quat skeletal animation
+//    i -> glare intensity
+
+mdlopt = [ >= (stringstr $modeltype $arg1) 0 ]
+
+modelvertexshader = [
+    modeltype = $arg1
+    result [
+        @(if (|| (mdlopt "b") (mdlopt "B")) [skelanimdefs $arg2 1 (mdlopt "n")])
+        #pragma CUBE2_fog opos
+        @(if (mdlopt "n") [result [
+            #pragma CUBE2_attrib vtangent 1
+            attribute vec4 vtangent;
+        ]])
+        uniform vec4 camera, lightdir, lightscale, texscroll;
+        @(if (mdlopt "n") [result [
+            @(if (mdlopt "e") [result [
+                varying vec3 camvec;
+                varying mat3 world;
+            ]] [result [
+                varying vec3 lightvec, halfangle;
+            ]])
+        ]] [result [
+            @(if (mdlopt "s") [result [
+                varying vec3 nvec, halfangle;
+            ]])
+            @(if (mdlopt "e") [result [
+                uniform vec4 envmapscale;
+                varying vec3 rvec;
+                varying float rmod;
+            ]])
+        ]])
+        void main(void)
+        {
+            @(if (mdlopt "B") [skelmatanim $arg2 1 (mdlopt "n")])
+            @(if (mdlopt "b") [skelquatanim $arg2 1 (mdlopt "n")])
+            @(if (|| (mdlopt "b") (mdlopt "B")) [result [
+                gl_Position = gl_ModelViewProjectionMatrix * opos;
+            ]] [result [
+                gl_Position = ftransform();
+                #define opos gl_Vertex
+                #define onormal gl_Normal
+                #define otangent vtangent.xyz
+            ]])
+
+            @(if (|| (mdlopt "n") (mdlopt "s") (mdlopt "i")) [result [
+                gl_FrontColor = gl_Color;
+            ]])
+            gl_TexCoord[0].xy = gl_MultiTexCoord0.xy + texscroll.yz;
+
+            @(if (|| (mdlopt "e") (mdlopt "s")) [result [
+                vec3 camdir = normalize(camera.xyz - opos.xyz);
+            ]])
+
+            @(if (mdlopt "n") [
+                if (mdlopt "e") [result [
+                    camvec = mat3(gl_TextureMatrix[0][0].xyz, gl_TextureMatrix[0][1].xyz, gl_TextureMatrix[0][2].xyz) * camdir;
+                    // composition of tangent -> object and object -> world transforms
+                    //   becomes tangent -> world
+                    vec3 wnormal = mat3(gl_TextureMatrix[0][0].xyz, gl_TextureMatrix[0][1].xyz, gl_TextureMatrix[0][2].xyz) * onormal;
+                    vec3 wtangent = mat3(gl_TextureMatrix[0][0].xyz, gl_TextureMatrix[0][1].xyz, gl_TextureMatrix[0][2].xyz) * otangent;
+                    world = mat3(wtangent, cross(wnormal, wtangent) * vtangent.w, wnormal);
+                ]] [result [
+                    vec3 obitangent = cross(onormal, otangent) * vtangent.w;
+                    lightvec = vec3(dot(lightdir.xyz, otangent), dot(lightdir.xyz, obitangent), dot(lightdir.xyz, onormal));
+                    @(if (mdlopt "s") [result [
+                        vec3 halfdir = lightdir.xyz + camdir;
+                        halfangle = vec3(dot(halfdir, otangent), dot(halfdir, obitangent), dot(halfdir, onormal));
+                    ]])
+                ]]
+            ] [result [
+                @(if (mdlopt "s") [result [
+                    nvec = onormal;
+                    halfangle = lightdir.xyz + camdir;
+                ]] [if (! (mdlopt "i")) [result [
+                    float intensity = dot(onormal, lightdir.xyz);
+                    gl_FrontColor = vec4(gl_Color.rgb*clamp(intensity*(intensity*lightscale.x + lightscale.y) + lightscale.z, 0.0, 1.0), gl_Color.a);
+                ]]])
+                @(if (mdlopt "e") [result [
+                    float invfresnel = dot(camdir, onormal);
+                    rvec = mat3(gl_TextureMatrix[0][0].xyz, gl_TextureMatrix[0][1].xyz, gl_TextureMatrix[0][2].xyz) * (2.0*invfresnel*onormal - camdir);
+                    rmod = envmapscale.x*max(invfresnel, 0.0) + envmapscale.y;
+                ]])
+            ]])
+        }
+    ]
+]
+
+modelfragmentshader = [
+    modeltype = $arg1
+    result [
+        @(if (mdlopt "n") [result [
+            @(if (mdlopt "e") [result [
+                #define lightvec lightdirworld.xyz
+                uniform vec4 lightdirworld, envmapscale;
+                varying vec3 camvec;
+                varying mat3 world;
+            ]] [result [
+                varying vec3 lightvec, halfangle;
+            ]])
+        ]] [result [
+            @(if (mdlopt "s") [result [
+                #define lightvec lightdir.xyz
+                uniform vec4 lightdir;
+                varying vec3 nvec, halfangle;
+            ]])
+            @(if (mdlopt "e") [result [
+                varying vec3 rvec;
+                varying float rmod;
+            ]])
+        ]])
+        uniform vec4 lightmaterial;
+        @(if (mdlopt "m") [result [uniform vec4 lightmaterial2;]])
+        @(if (&& (|| (mdlopt "s") (mdlopt "n")) (! (mdlopt "i"))) [result [uniform vec4 lightscale;]])
+        @(if (|| (mdlopt "s") (mdlopt "m")) [result [uniform vec4 maskscale;]])
+        uniform sampler2D tex0;
+        @(if (mdlopt "m") [result [uniform sampler2D tex1;]])
+        @(if (mdlopt "e") [result [uniform samplerCube tex2;]])
+        @(if (mdlopt "n") [result [uniform sampler2D tex3;]])
+        void main(void)
+        {
+            vec4 light = texture2D(tex0, gl_TexCoord[0].xy);
+
+            @(if (mdlopt "m") [result [
+                vec4 masks = texture2D(tex1, gl_TexCoord[0].xy);
+                light.rgb *= mix(lightmaterial2.rgb, lightmaterial.rgb, masks.a); // material color mask in alpha channel
+                vec3 glow = light.rgb * maskscale.y;
+            ]] [result [
+                light.rgb *= lightmaterial.rgb;
+            ]])
+
+            @(if (mdlopt "n") [result [
+                vec3 normal = texture2D(tex3, gl_TexCoord[0].xy).rgb - 0.5;
+                @(if (mdlopt "e") [result [
+                    normal = world * normal;
+                ]])
+                normal = normalize(normal);
+            ]])
+
+            @(if (mdlopt "s") [result [
+                @(if (mdlopt "n") [
+                    if (mdlopt "e") [result [
+                        vec3 halfangle = lightvec + camvec;
+                    ]]
+                ] [result [
+                    vec3 normal = normalize(nvec);
+                ]])
+                float spec = maskscale.x * pow(clamp(dot(normalize(halfangle), normal), 0.0, 1.0), @(? (mdlopt "i") "256.0" "128.0"));
+                @(if (mdlopt "m") [result [spec *= masks.r;]])   // specmap in red channel
+            ]])
+
+            @(if (mdlopt "i") [
+                if (mdlopt "s") [result [
+                    spec *= maskscale.z;
+                    @(? (mdlopt "m") "light.rgb" "gl_FragColor.rgb") = spec * gl_Color.rgb;
+                ]] [
+                    if (! (mdlopt "m")) [result [gl_FragColor.rgb = vec3(0.0);]]
+                ]
+            ] [result [
+                @(if (|| (mdlopt "s") (mdlopt "n")) [result [
+                    float intensity = dot(normal, lightvec);
+                    light.rgb *= clamp(intensity*(intensity*lightscale.x + lightscale.y) + lightscale.z, 0.0, 1.0);
+                ]])
+                @(if (mdlopt "s") [result [
+                    light.rgb += spec;
+                ]])
+                @(if (mdlopt "m") [result [
+                    light.rgb *= gl_Color.rgb;
+                ]] [result [
+                    gl_FragColor = light * gl_Color;
+                ]])
+            ]])
+
+            @(if (mdlopt "m") [result [
+                @(if (mdlopt "e") [result [
+                    light.rgb = mix(light.rgb, glow, masks.g); // glow mask in green channel
+
+                    @(if (mdlopt "n") [result [
+                        vec3 camn = normalize(camvec);
+                        float invfresnel = dot(camn, normal);
+                        vec3 rvec = 2.0*invfresnel*normal - camn;
+                        float rmod = envmapscale.x*max(invfresnel, 0.0) + envmapscale.y;
+                    ]])
+                    vec3 reflect = textureCube(tex2, rvec).rgb;
+                    gl_FragColor.rgb = mix(light.rgb, reflect, rmod*masks.b); // envmap mask in blue channel
+                ]] [if (mdlopt "i") [result [
+                    float k = min(masks.g*masks.g*maskscale.w, 1.0); // glow mask in green channel
+                    gl_FragColor.rgb = @(? (mdlopt "s") "glow*k + light.rgb" "glow*k");
+                ]] [result [
+                    gl_FragColor.rgb = mix(light.rgb, glow, masks.g); // glow mask in green channel
+                ]]])
+            ]])
+
+            @(if (|| (mdlopt "i") (mdlopt "m")) [result [
+                gl_FragColor.a = light.a * gl_Color.a;
+            ]])
+        }
+    ]
+]
+
+modelanimshader = [
+    fraganimshader = (? (> $arg2 0) $arg2)
+    variantshader 4 $arg1 $arg2 (modelvertexshader (concatword "B" $arg3) $arg4) $fraganimshader
+    variantshader 4 $arg1 (+ $arg2 1) (modelvertexshader (concatword "b" $arg3) $arg4) $fraganimshader
+]
+
+modelshader = [
+    defershader 4 $arg1 [
+        basemodeltype = [@@arg2]
+        shader 4 @arg1 (modelvertexshader $basemodeltype) (modelfragmentshader $basemodeltype)
+        loop i 4 [
+            modelanimshader @@arg1 0 $basemodeltype (+ $i 1)
+        ]
+        glaremodeltype = (stringreplace (concatword $basemodeltype "i") "e")
+        if (< (stringstr $glaremodeltype "s") 0) [glaremodeltype = (stringreplace $glaremodeltype "n")]
+        variantshader 4 @arg1 2 (modelvertexshader $glaremodeltype) (modelfragmentshader $glaremodeltype)
+        loop i 4 [
+            modelanimshader @@arg1 2 $glaremodeltype (+ $i 1)
+        ]
+    ]
+]
+
+////////////////////////////////////////////////
+//
+// gourad lighting model shader: cheaper, non-specular version for vegetation etc. gets used when spec==0
+//
+////////////////////////////////////////////////
+
+modelshader "nospecmodel" ""
+modelshader "masksnospecmodel" "m"
+modelshader "envmapnospecmodel" "me"
+altshader envmapnospecmodel masksnospecmodel
+
+modelshader "bumpnospecmodel" "n"
+modelshader "bumpmasksnospecmodel" "nm"
+modelshader "bumpenvmapnospecmodel" "nme"
+altshader bumpenvmapnospecmodel bumpmasksnospecmodel
+
+////////////////////////////////////////////////
+//
+// phong lighting model shader
+//
+////////////////////////////////////////////////
+
+modelshader "stdmodel" "s"
+fastshader stdmodel nospecmodel 1
+modelshader "masksmodel" "sm"
+fastshader masksmodel masksnospecmodel 1
+modelshader "envmapmodel" "sme"
+altshader envmapmodel masksmodel
+fastshader envmapmodel envmapnospecmodel 1
+
+modelshader "bumpmodel" "ns"
+fastshader bumpmodel bumpnospecmodel 1
+modelshader "bumpmasksmodel" "nsm"
+fastshader bumpmasksmodel bumpmasksnospecmodel 1
+modelshader "bumpenvmapmodel" "nsme"
+altshader bumpenvmapmodel bumpmasksmodel
+fastshader bumpenvmapmodel bumpenvmapnospecmodel 1
+
+////////////////////////////////////////////////
+//
+// separable blur with up to 7 taps
+//
+////////////////////////////////////////////////
+
+blurshader = [
+    shader 4 $arg1 [
+        uniform vec4 offsets;
+        void main(void)
+        {
+            gl_Position = gl_Vertex;
+            gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+            vec2 tc1 = gl_MultiTexCoord0.xy + offsets.xy;
+            vec2 tc2 = gl_MultiTexCoord0.xy - offsets.xy;
+            gl_TexCoord[1].xy = tc1;
+            gl_TexCoord[2].xy = tc2;
+            @(loopconcat i (min (- $arg2 1) 2) [concatword [
+                tc1.@@arg3 += offsets.@(at "z w" $i);
+                tc2.@@arg3 -= offsets.@(at "z w" $i);
+                gl_TexCoord[@@(+ (* $i 2) 3)].xy = tc1;
+                gl_TexCoord[@@(+ (* $i 2) 4)].xy = tc2;
+            ]])
+        }
+    ] [
+        @(if (=s $arg4 "2DRect") [result [
+            #extension GL_ARB_texture_rectangle : enable
+        ]])
+        uniform vec4 weights, weights2, offset4, offset5, offset6, offset7;
+        uniform @(concatword "sampler" $arg4) tex0;
+        void main(void)
+        {
+            #define texval(coords) @(concatword "texture" $arg4)(tex0, (coords))
+            vec4 val = texval(gl_TexCoord[0].xy) * weights.x;
+            @(loopconcat i $arg2 [concatword [
+                @(if (< $i 3) [result [
+                    val += weights.@(at "y z w" $i) * (texval(gl_TexCoord[@@(+ (* $i 2) 1)].xy) + texval(gl_TexCoord[@@(+ (* $i 2) 2)].xy));
+                ]] [result [
+                    val += weights2.@(at "x y z w" (- $i 3)) *
+                                (texval(gl_TexCoord[0].xy + @(at "offset4 offset5 offset6 offset7" (- $i 3)).xy) +
+                                 texval(gl_TexCoord[0].xy - @(at "offset4 offset5 offset6 offset7" (- $i 3)).xy));
+                ]])
+            ]])
+            gl_FragColor = val;
+        }
+    ]
+]
+
+loop i 7 [
+    blurshader (format "blurx%1" (+ $i 1)) (+ $i 1) x 2D
+    blurshader (format "blury%1" (+ $i 1)) (+ $i 1) y 2D
+    if (> $i 0) [
+        altshader (format "blurx%1" (+ $i 1)) (format "blurx%1" $i)
+        altshader (format "blury%1" (+ $i 1)) (format "blury%1" $i)
+    ]
+    if $usetexrect [
+        blurshader (format "blurx%1rect" (+ $i 1)) (+ $i 1) x 2DRect
+        blurshader (format "blury%1rect" (+ $i 1)) (+ $i 1) y 2DRect
+        if (> $i 0) [
+            altshader (format "blurx%1rect" (+ $i 1)) (format "blurx%1rect" $i)
+            altshader (format "blury%1rect" (+ $i 1)) (format "blury%1rect" $i)
+        ]
+    ]
+]
+
+////////////////////////////////////////////////
+//
+// full screen shaders:
+//
+////////////////////////////////////////////////
+
+fsvs = [
+    void main(void)
+    {
+        gl_Position = gl_Vertex;   // woohoo, no mvp :)
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+]
+
+fsps = [
+    #extension GL_ARB_texture_rectangle : enable
+    uniform sampler2DRect tex0;
+    void main(void)
+    {
+        vec4 sample = texture2DRect(tex0, gl_TexCoord[0].xy);
+]
+
+setup4corners = [
+    gl_TexCoord[1].xy = gl_MultiTexCoord0.xy + vec2(-1.5, -1.5);
+    gl_TexCoord[2].xy = gl_MultiTexCoord0.xy + vec2( 1.5, -1.5);
+    gl_TexCoord[3].xy = gl_MultiTexCoord0.xy + vec2(-1.5,  1.5);
+    gl_TexCoord[4].xy = gl_MultiTexCoord0.xy + vec2( 1.5,  1.5);
+]
+
+sample4corners = [
+    vec4 s00 = texture2DRect(tex0, gl_TexCoord[1].xy);
+    vec4 s02 = texture2DRect(tex0, gl_TexCoord[2].xy);
+    vec4 s20 = texture2DRect(tex0, gl_TexCoord[3].xy);
+    vec4 s22 = texture2DRect(tex0, gl_TexCoord[4].xy);
+]
+
+// some simple ones that just do an effect on the RGB value...
+
+lazyshader 4 "invert" [ @fsvs } ] [ @fsps gl_FragColor = 1.0 - sample; } ]
+lazyshader 4 "gbr"    [ @fsvs } ] [ @fsps gl_FragColor = sample.yzxw; } ]
+lazyshader 4 "bw"     [ @fsvs } ] [ @fsps gl_FragColor = vec4(dot(sample.xyz, vec3(0.333))); } ]
+
+// sobel
+
+lazyshader 4 "sobel" [ @fsvs @setup4corners } ] [
+    @fsps
+    @sample4corners
+
+        vec4 t = s00 + s20 - s02 - s22;
+        vec4 u = s00 + s02 - s20 - s22;
+        gl_FragColor = sample + t*t + u*u;
+    }
+]
+
+// rotoscope
+
+lazyshader 4 "rotoscope" [
+    uniform vec4 params;
+    void main(void)
+    {
+        gl_Position = gl_Vertex;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+
+        // stuff two sets of texture coordinates into each one to get around hardware attribute limits
+        gl_TexCoord[1] = vec4(-1.0, -1.0,  1.0, 0.0)*params.x + gl_MultiTexCoord0.xyyx;
+        gl_TexCoord[2] = vec4(-1.0,  0.0, -1.0, 1.0)*params.x + gl_MultiTexCoord0.xyyx;
+        gl_TexCoord[3] = vec4(-1.0,  1.0,  0.0, 1.0)*params.x + gl_MultiTexCoord0.xyyx;
+        gl_TexCoord[4] = vec4( 0.0, -1.0,  1.0, 1.0)*params.x + gl_MultiTexCoord0.xyyx;
+    }
+] [
+    #extension GL_ARB_texture_rectangle : enable
+    uniform sampler2DRect tex0;
+    void main(void)
+    {
+        #define t11 gl_TexCoord[0]
+        #define t00_12 gl_TexCoord[1]
+        #define t01_20 gl_TexCoord[2]
+        #define t02_21 gl_TexCoord[3]
+        #define t10_22 gl_TexCoord[4]
+        vec4 c00 = texture2DRect(tex0, t00_12.xy);
+        vec4 c01 = texture2DRect(tex0, t01_20.xy);
+        vec4 c02 = texture2DRect(tex0, t02_21.xy);
+        vec4 c10 = texture2DRect(tex0, t10_22.xy);
+        vec4 c11 = texture2DRect(tex0, t11.xy);
+        vec4 c12 = texture2DRect(tex0, t00_12.wz);
+        vec4 c20 = texture2DRect(tex0, t01_20.wz);
+        vec4 c21 = texture2DRect(tex0, t02_21.wz);
+        vec4 c22 = texture2DRect(tex0, t10_22.wz);
+
+        vec4 diag1 = c00 - c22;
+        vec4 diag2 = c02 - c20;
+        vec4 xedge = (c01 - c21)*2.0 + diag1 + diag2;
+        vec4 yedge = (c10 - c12)*2.0 + diag1 - diag2;
+        xedge *= xedge;
+        yedge *= yedge;
+
+        vec4 xyedge = xedge + yedge;
+        float sobel = step(max(xyedge.x, max(xyedge.y, xyedge.z)), 0.1);
+
+        float hue = dot(c11.xyz, vec3(1.0));
+        c11 /= hue;
+        vec3 cc = step(vec3(0.2, 0.8, 1.5), vec3(hue));
+        c11 *= dot(cc, vec3(0.5, 0.5, 1.5));
+
+        gl_FragColor = c11 * max(cc.z, sobel);
+
+    }
+]
+
+blur3shader = [
+    lazyshader 4 $arg1 [
+        void main(void)
+        {
+            gl_Position = gl_Vertex;
+            gl_TexCoord[0].xy = gl_MultiTexCoord0.xy + vec2(@(if $arg2 -0.5 0.0), @(if $arg3 -0.5 0.0));
+            gl_TexCoord[1].xy = gl_MultiTexCoord0.xy + vec2(@(if $arg2  0.5 0.0), @(if $arg3  0.5 0.0));
+        }
+    ] [
+        #extension GL_ARB_texture_rectangle : enable
+        uniform sampler2DRect tex0;
+        void main(void)
+        {
+            gl_FragColor = 0.5*(texture2DRect(tex0, gl_TexCoord[0].xy) + texture2DRect(tex0, gl_TexCoord[1].xy));
+        }
+    ]
+]
+blur3shader hblur3 1 0
+blur3shader vblur3 0 1
+
+blur5shader = [
+    lazyshader 4 $arg1 [
+        @fsvs
+            gl_TexCoord[1].xy = gl_MultiTexCoord0.xy + vec2(@(if $arg2 -1.333 0.0), @(if $arg3 -1.333 0.0));
+            gl_TexCoord[2].xy = gl_MultiTexCoord0.xy + vec2(@(if $arg2  1.333 0.0), @(if $arg3  1.333 0.0));
+        }
+    ] [
+        #extension GL_ARB_texture_rectangle : enable
+        uniform sampler2DRect tex0;
+        void main(void)
+        {
+            gl_FragColor = 0.4*texture2DRect(tex0, gl_TexCoord[0].xy) + 0.3*(texture2DRect(tex0, gl_TexCoord[1].xy) + texture2DRect(tex0, gl_TexCoord[2].xy));
+        }
+    ]
+]
+blur5shader hblur5 1 0
+blur5shader vblur5 0 1
+
+rotoscope = [
+    clearpostfx
+    if (>= $numargs 1) [addpostfx rotoscope 0 0 0 $arg1]
+    if (>= $numargs 2) [
+        if (= $arg2 1) [addpostfx hblur3; addpostfx vblur3]
+        if (= $arg2 2) [addpostfx hblur5; addpostfx vblur5]
+    ]
+]
+
+// bloom-ish
+
+shader 4 "glare" [
+    void main(void)
+    {
+        gl_Position = gl_Vertex;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+    }
+] [
+    uniform vec4 glarescale;
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        gl_FragColor = texture2D(tex0, gl_TexCoord[0].xy) * glarescale;
+    }
+]
+
+lazyshader 4 "bloom_scale" [ @fsvs @setup4corners } ] [
+    @fsps
+    @sample4corners
+        gl_FragColor = 0.2 * (s02 + s00 + s22 + s20 + sample);
+    }
+]
+
+lazyshader 4 "bloom_init" [ @fsvs } ] [
+    @fsps
+        float t = max(sample.r, max(sample.g, sample.b));
+        gl_FragColor = t*t*sample;
+    }
+]
+
+bloomshader = [
+    defershader 4 $arg1 [
+        forceshader "bloom_scale"
+        forceshader "bloom_init"
+        shader 4 @arg1 [
+            void main(void)
+            {
+                gl_Position = gl_Vertex;
+                gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+                vec2 tc = gl_MultiTexCoord0.xy;
+                @@(loopconcat i $arg2 [concat [
+                    tc *= 0.5;
+                    gl_TexCoord[@@(+ $i 1)].xy = tc;
+                ]])
+            }
+        ] [
+            #extension GL_ARB_texture_rectangle : enable
+            uniform vec4 params;
+            uniform sampler2DRect tex0 @@(loopconcat i $arg2 [format ", tex%1" (+ $i 1)]);
+            void main(void)
+            {
+                vec4 sample = texture2DRect(tex0, gl_TexCoord[0].xy);
+                @@(loopconcat i $arg2 [
+                    format [
+                        @(? (> $i 0) "bloom +=" "vec4 bloom =") texture2DRect(tex%1, gl_TexCoord[%1].xy);
+                    ] (+ $i 1)
+                ])
+                gl_FragColor = bloom*params.x + sample;
+            }
+        ]
+    ]
+]
+
+bloomshader bloom1 1
+bloomshader bloom2 2
+bloomshader bloom3 3
+bloomshader bloom4 4
+bloomshader bloom5 5
+bloomshader bloom6 6
+
+setupbloom = [
+    addpostfx bloom_init 1 1 "+0"
+    loop i (- $arg1 1) [
+        addpostfx bloom_scale (+ $i 2) (+ $i 2) (concatword "+" (+ $i 1))
+    ]
+    addpostfx (concatword bloom $arg1) 0 0 (loopconcat i (+ $arg1 1) [result $i]) $arg2
+]
+
+bloom = [
+    clearpostfx
+    if (>= $numargs 1) [setupbloom 6 $arg1]
+]
+
+setupfinecorners = [
+    gl_TexCoord[1].xy = gl_MultiTexCoord0.xy + vec2(-1.0, 0.0);
+    gl_TexCoord[2].xy = gl_MultiTexCoord0.xy + vec2( 1.0, 0.0);
+    gl_TexCoord[3].xy = gl_MultiTexCoord0.xy + vec2( 0.0, -1.0);
+    gl_TexCoord[4].xy = gl_MultiTexCoord0.xy + vec2( 0.0,  1.0);
+]
+
+lazyshader 4 "sharpen" [ @fsvs @setupfinecorners } ] [
+    @fsps
+    @sample4corners
+    gl_FragColor = vec4(sample.xyz*1.95 - 0.25*(s00.xyz + s02.xyz + s20.xyz + s22.xyz), sample.w);
+    }
+]
+
+////////////////////////////////////////////////
+//
+// miscellaneous effect shaders:
+//
+////////////////////////////////////////////////
+
+// wobbles the vertices of an explosion sphere
+// and generates all texcoords
+// and blends the edge color
+// and modulates the texture
+explosionshader = [
+    shader 4 $arg1 [
+        #pragma CUBE2_fog
+        uniform vec4 center, animstate;
+        @(if (>= (stringstr $arg1 "3d") 0) [result [uniform vec4 texgenS, texgenT;]])
+        @(if (>= (stringstr $arg1 "soft") 0) [result [uniform vec4 depthfxparams, depthfxview;]])
+        void main(void)
+        {
+            vec4 wobble = vec4(gl_Vertex.xyz*(1.0 + 0.5*abs(fract(dot(gl_Vertex.xyz, center.xyz) + animstate.w*0.002) - 0.5)), gl_Vertex.w);
+            @(if (>= (stringstr $arg1 "soft") 0) [result [
+                vec4 projtc = gl_ModelViewProjectionMatrix * wobble;
+                gl_Position = projtc;
+
+                gl_TexCoord[3] = vec4((projtc.xy + projtc.w)*depthfxview.xy, projtc.w, depthfxparams.y - (gl_ModelViewMatrix * wobble).z*depthfxparams.x);
+            ]] [result [
+                gl_Position = gl_ModelViewProjectionMatrix * wobble;
+            ]])
+
+            gl_FrontColor = gl_Color;
+
+            @arg2
+        }
+    ] [
+        @(if (>= (stringstr $arg1 "rect") 0) [result [
+            #extension GL_ARB_texture_rectangle : enable
+            uniform sampler2DRect tex2;
+        ]] [result [
+            uniform sampler2D tex2;
+        ]])
+        uniform sampler2D tex0, tex1;
+        @(if (>= (stringstr $arg1 "soft") 0) [result [uniform vec4 depthfxparams;]])
+        @(if (>= (stringstr $arg1 "soft8") 0) [result [uniform vec4 depthfxselect;]])
+        void main(void)
+        {
+            vec2 dtc = gl_TexCoord[0].xy + texture2D(tex0, @arg3.xy).xy*0.1; // use color texture as noise to distort texcoords
+            vec4 diffuse = texture2D(tex0, dtc);
+            vec4 blend = texture2D(tex1, gl_TexCoord[1].xy); // get blend factors from modulation texture
+            @(if (>= (stringstr $arg1 "glare") 0) [result [
+                float k = blend.a*blend.a;
+                diffuse.rgb *= k*8.0;
+                diffuse.a *= k;
+                diffuse.b += k*k;
+            ]] [result [
+                diffuse *= blend.a*4.0; // dup alpha into RGB channels + intensify and over saturate
+                diffuse.b += 0.5 - blend.a*0.5; // blue tint
+            ]])
+
+            @(if (>= (stringstr $arg1 "soft") 0) [result [
+                gl_FragColor.rgb = diffuse.rgb * gl_Color.rgb;
+
+                #define depthvals @(format "texture%1Proj" (? (>= (stringstr $arg1 "rect") 0) "2DRect" "2D"))(tex2, gl_TexCoord[3].xyz)
+                @(if (>= (stringstr $arg1 "soft8") 0) [result [
+                    float depth = dot(depthvals, depthfxselect);
+                ]] [result [
+                    float depth = depthvals.x*depthfxparams.z;
+                ]])
+                gl_FragColor.a = diffuse.a * max(clamp(depth - gl_TexCoord[3].w, 0.0, 1.0) * gl_Color.a, depthfxparams.w);
+            ]] [result [
+                gl_FragColor = diffuse * gl_Color;
+            ]])
+        }
+    ]
+]
+
+loop i (if $usetexrect 6 4) [
+    explosionshader (concatword "explosion2d" (at ["" "glare" "soft" "soft8" "softrect" "soft8rect"] $i)) [
+        //blow up the tex coords
+        float dtc = 1.768 - animstate.x*1.414; // -2, 2.5; -> -2*sqrt(0.5), 2.5*sqrt(0.5);
+        dtc *= dtc;
+        gl_TexCoord[0].xy = animstate.w*0.0004 + dtc*gl_Vertex.xy;
+        gl_TexCoord[1].xy = gl_Vertex.xy*0.5 + 0.5; //using wobble makes it look too spherical at a distance
+    ] "gl_TexCoord[1]"
+    explosionshader (concatword "explosion3d" (at ["" "glare" "soft" "soft8" "softrect" "soft8rect"] $i)) [
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+        vec2 texgen = vec2(dot(texgenS, gl_Vertex), dot(texgenT, gl_Vertex));
+        gl_TexCoord[1].xy = texgen;
+        gl_TexCoord[2].xy = texgen - animstate.w*0.0005;
+    ] "gl_TexCoord[2]"
+]
+
+shader 4 "particlenotexture" [
+    #pragma CUBE2_fog
+    uniform vec4 colorscale;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0] = gl_Color * colorscale;
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_TexCoord[0];
+    }
+]
+
+particleshader = [
+    shader 4 $arg1 [
+        #pragma CUBE2_fog
+        uniform vec4 colorscale;
+        @(if (>= (stringstr $arg1 "soft") 0) [result [uniform vec4 depthfxparams, depthfxview;]])
+        void main(void)
+        {
+            gl_Position = ftransform();
+            gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+            gl_TexCoord[1] = gl_Color * colorscale;
+
+            @(if (>= (stringstr $arg1 "soft") 0) [result [
+                vec4 projtc = gl_ModelViewProjectionMatrix * gl_Vertex;
+                gl_TexCoord[2].xyz = vec3((projtc.xy + projtc.w) * depthfxview.xy, projtc.w);
+
+                vec2 offset = gl_MultiTexCoord0.xy*2.82842712474619 - 1.4142135623731;
+                gl_TexCoord[3].xyz = vec3(offset, 1.0);
+                gl_TexCoord[4].xyz = vec3(offset, depthfxparams.y - (gl_ModelViewMatrix * gl_Vertex).z*depthfxparams.x);
+            ]])
+        }
+    ] [
+        @(if (>= (stringstr $arg1 "soft") 0) [
+            if (>= (stringstr $arg1 "rect") 0) [result [
+                #extension GL_ARB_texture_rectangle : enable
+                uniform sampler2DRect tex2;
+            ]] [result [
+                uniform sampler2D tex2;
+            ]]
+        ])
+        uniform sampler2D tex0;
+        @(if (>= (stringstr $arg1 "soft") 0) [result [uniform vec4 depthfxparams;]])
+        @(if (>= (stringstr $arg1 "soft8") 0) [result [uniform vec4 depthfxselect;]])
+        void main(void)
+        {
+            vec4 diffuse = texture2D(tex0, gl_TexCoord[0].xy);
+
+            @(if (>= (stringstr $arg1 "soft") 0) [result [
+                #define depthvals @(format "texture%1Proj" (? (>= (stringstr $arg1 "rect") 0) "2DRect" "2D"))(tex2, gl_TexCoord[2].xyz)
+                @(if (>= (stringstr $arg1 "soft8") 0) [result [
+                    float depth = dot(depthvals, depthfxselect);
+                ]] [result [
+                    float depth = depthvals.x*depthfxparams.z;
+                ]])
+                diffuse.a *= clamp(depth - dot(gl_TexCoord[3].xyz, gl_TexCoord[4].xyz), 0.0, 1.0);
+            ]])
+
+            gl_FragColor = diffuse * gl_TexCoord[1];
+        }
+    ]
+]
+
+loop i (if $usetexrect 5 3) [
+    particleshader (concatword "particle" (at ["" "soft" "soft8" "softrect" "soft8rect"] $i))
+]
+
+shader 4 "blendbrush" [
+    uniform vec4 texgenS, texgenT;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = vec2(dot(texgenS, gl_Vertex), dot(texgenT, gl_Vertex));
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        gl_FragColor = texture2D(tex0, gl_TexCoord[0].xy) * gl_Color;
+    }
+]
+
+lazyshader 4 "moviergb" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+    }
+] [
+    #extension GL_ARB_texture_rectangle : enable
+    uniform sampler2DRect tex0;
+    void main(void)
+    {
+        gl_FragColor = texture2DRect(tex0, gl_TexCoord[0].xy);
+    }
+]
+
+lazyshader 4 "movieyuv" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+    }
+] [
+    #extension GL_ARB_texture_rectangle : enable
+    uniform sampler2DRect tex0;
+    void main(void)
+    {
+        vec3 sample = texture2DRect(tex0, gl_TexCoord[0].xy).rgb;
+        gl_FragColor = vec4(dot(sample, vec3(0.439216, -0.367788, -0.071427)) + 0.501961,
+                            dot(sample, vec3(-0.148224, -0.290992, 0.439216)) + 0.501961,
+                            dot(sample, vec3(0.256788, 0.504125, 0.097905)) + 0.062745,
+                            0.0);
+    }
+]
+
+lazyshader 4 "moviey" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy + vec2(-1.5, 0.0);
+        gl_TexCoord[1].xy = gl_MultiTexCoord0.xy + vec2(-0.5, 0.0);
+        gl_TexCoord[2].xy = gl_MultiTexCoord0.xy + vec2( 0.5, 0.0);
+        gl_TexCoord[3].xy = gl_MultiTexCoord0.xy + vec2( 1.5, 0.0);
+    }
+] [
+    #extension GL_ARB_texture_rectangle : enable
+    uniform sampler2DRect tex0;
+    void main(void)
+    {
+        vec3 sample1 = texture2DRect(tex0, gl_TexCoord[0].xy).rgb;
+        vec3 sample2 = texture2DRect(tex0, gl_TexCoord[1].xy).rgb;
+        vec3 sample3 = texture2DRect(tex0, gl_TexCoord[2].xy).rgb;
+        vec3 sample4 = texture2DRect(tex0, gl_TexCoord[3].xy).rgb;
+        gl_FragColor = vec4(dot(sample3, vec3(0.256788, 0.504125, 0.097905)) + 0.062745,
+                            dot(sample2, vec3(0.256788, 0.504125, 0.097905)) + 0.062745,
+                            dot(sample1, vec3(0.256788, 0.504125, 0.097905)) + 0.062745,
+                            dot(sample4, vec3(0.256788, 0.504125, 0.097905)) + 0.062745);
+    }
+]
+
+lazyshader 4 "movieu" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy + vec2(-3.0, 0.0);
+        gl_TexCoord[1].xy = gl_MultiTexCoord0.xy + vec2(-1.0, 0.0);
+        gl_TexCoord[2].xy = gl_MultiTexCoord0.xy + vec2( 1.0, 0.0);
+        gl_TexCoord[3].xy = gl_MultiTexCoord0.xy + vec2( 3.0, 0.0);
+    }
+] [
+    #extension GL_ARB_texture_rectangle : enable
+    uniform sampler2DRect tex0;
+    void main(void)
+    {
+        vec3 sample1 = texture2DRect(tex0, gl_TexCoord[0].xy).rgb;
+        vec3 sample2 = texture2DRect(tex0, gl_TexCoord[1].xy).rgb;
+        vec3 sample3 = texture2DRect(tex0, gl_TexCoord[2].xy).rgb;
+        vec3 sample4 = texture2DRect(tex0, gl_TexCoord[3].xy).rgb;
+        gl_FragColor = vec4(dot(sample3, vec3(-0.148224, -0.290992, 0.43921)) + 0.501961,
+                            dot(sample2, vec3(-0.148224, -0.290992, 0.43921)) + 0.501961,
+                            dot(sample1, vec3(-0.148224, -0.290992, 0.43921)) + 0.501961,
+                            dot(sample4, vec3(-0.148224, -0.290992, 0.43921)) + 0.501961);
+    }
+]
+
+lazyshader 4 "moviev" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy + vec2(-3.0, 0.0);
+        gl_TexCoord[1].xy = gl_MultiTexCoord0.xy + vec2(-1.0, 0.0);
+        gl_TexCoord[2].xy = gl_MultiTexCoord0.xy + vec2( 1.0, 0.0);
+        gl_TexCoord[3].xy = gl_MultiTexCoord0.xy + vec2( 3.0, 0.0);
+    }
+] [
+    #extension GL_ARB_texture_rectangle : enable
+    uniform sampler2DRect tex0;
+    void main(void)
+    {
+        vec3 sample1 = texture2DRect(tex0, gl_TexCoord[0].xy).rgb;
+        vec3 sample2 = texture2DRect(tex0, gl_TexCoord[1].xy).rgb;
+        vec3 sample3 = texture2DRect(tex0, gl_TexCoord[2].xy).rgb;
+        vec3 sample4 = texture2DRect(tex0, gl_TexCoord[3].xy).rgb;
+        gl_FragColor = vec4(dot(sample3, vec3(0.439216, -0.367788, -0.071427)) + 0.501961,
+                            dot(sample2, vec3(0.439216, -0.367788, -0.071427)) + 0.501961,
+                            dot(sample1, vec3(0.439216, -0.367788, -0.071427)) + 0.501961,
+                            dot(sample4, vec3(0.439216, -0.367788, -0.071427)) + 0.501961);
+    }
+]
+
+///////////////////////////////////////////////////
+//
+// reflective/refractive water shaders:
+//
+///////////////////////////////////////////////////
+
+watershader = [
+    specular = $arg2
+    rgbfog = $arg3
+    distort = $arg4
+    combine = $arg5
+    lazyshader 4 $arg1 [
+        uniform vec4 camera, millis, waterheight;
+        @(if $specular [result [uniform vec4 lightpos; varying vec3 lightdir;]])
+        varying vec3 camdir;
+        void main(void)
+        {
+            gl_Position = ftransform();
+            gl_FrontColor = gl_Color;
+            gl_TexCoord[0] = gl_TextureMatrix[0] * gl_Vertex;
+            @(if (>= (stringstr $arg1 "underwater") 0) [result [
+                gl_TexCoord[0].z = waterheight.x - gl_Vertex.z;
+            ]] [result [
+                gl_TexCoord[0].z = gl_Vertex.z - waterheight.x;
+            ]])
+            vec2 tc = gl_MultiTexCoord0.xy * 0.1;
+            gl_TexCoord[1].xy = tc + millis.x*0.04;
+            gl_TexCoord[2].xy = tc - millis.x*0.02;
+            camdir = camera.xyz - gl_Vertex.xyz;
+            @(if $specular [result [
+                lightdir = lightpos.xyz - gl_Vertex.xyz;
+            ]])
+        }
+    ] [
+        @(if $rgbfog [result [
+            #pragma CUBE2_fog
+        ]] [result [
+            #pragma CUBE2_fogrgba vec4(0.0, 0.0, 0.0, 1.0)
+        ]])
+        uniform vec4 depth;
+        @(if $specular [result [uniform vec4 lightcolor, lightradius; varying vec3 lightdir;]])
+        varying vec3 camdir;
+        @(if (>= (stringstr $arg1 "env") 0) [result [
+            uniform samplerCube tex0;
+        ]] [result [
+            uniform sampler2D tex0;
+        ]])
+        uniform sampler2D tex1, tex2, tex3;
+        void main(void)
+        {
+            vec3 camvec = normalize(camdir);
+            @(if $specular [result [
+                vec3 lightvec = normalize(lightdir);
+                vec3 halfangle = normalize(camvec + lightvec);
+            ]])
+
+            vec2 dudv = texture2D(tex2, gl_TexCoord[1].xy).xy*2.0 - 1.0;
+
+            @distort
+
+            @(if $specular [result [
+                float spec = pow(clamp(dot(halfangle, bump), 0.0, 1.0), 96.0);
+                vec3 light = lightcolor.xyz * (1.0 - clamp(length(lightdir)/lightradius.x, 0.0, 1.0));
+            ]])
+
+            @combine
+        }
+    ]
+]
+
+watershader "waterglare" 1 1 [
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+] [
+    gl_FragColor = vec4(light*spec*spec*32.0, 0.0);
+]
+lazyshader 4 "waterglarefast" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = vec4(0.0);
+    }
+]
+fastshader waterglare waterglarefast 2
+altshader waterglare waterglarefast
+
+lazyshader 4 "underwater" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+    }
+] [
+    #pragma CUBE2_fogrgba vec4(0.0, 0.0, 0.0, 1.0)
+    uniform vec4 depth;
+    void main(void)
+    {
+        gl_FragColor.rgb = 0.8*depth.x*gl_Color.rgb;
+        gl_FragColor.a = 0.5*depth.y;
+    }
+]
+
+watershader "underwaterrefract" 0 1 [
+    dudv = texture2D(tex2, gl_TexCoord[2].xy + 0.025*dudv).xy*2.0 - 1.0;
+
+    gl_FragColor = texture2D(tex3, gl_TexCoord[0].xy/gl_TexCoord[0].w + 0.01*dudv);
+] []
+watershader "underwaterrefractfast" 0 1 [
+    gl_FragColor = texture2DProj(tex3, gl_TexCoord[0] + vec4(0.4*dudv, 0.0, 0.0));
+] []
+fastshader underwaterrefract underwaterrefractfast 2
+altshader underwaterrefract underwaterrefractfast
+
+watershader "underwaterfade" 0 1 [
+    dudv = texture2D(tex2, gl_TexCoord[2].xy + 0.025*dudv).xy*2.0 - 1.0;
+
+    vec2 projtc = gl_TexCoord[0].xy/gl_TexCoord[0].w;
+    float fade = gl_TexCoord[0].z + 4.0*texture2D(tex3, projtc).a;
+    gl_FragColor.a = fade * clamp(gl_FragCoord.z, 0.0, 1.0);
+    gl_FragColor.rgb = texture2D(tex3, projtc + 0.01*dudv).rgb;
+] []
+watershader "underwaterfadefast" 0 1 [
+    gl_FragColor.rgb = texture2DProj(tex3, gl_TexCoord[0] + vec4(0.4*dudv, 0.0, 0.0)).rgb;
+    gl_FragColor.a = gl_TexCoord[0].z + 4.0*texture2DProj(tex3, gl_TexCoord[0]).a;
+] []
+fastshader underwaterfade underwaterfadefast 2
+altshader underwaterfade underwaterfadefast
+
+watershader "water" 1 0 [
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+] [
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    gl_FragColor.rgb = gl_Color.rgb*depth.x*mix(0.6, 1.0, invfresnel*0.5+0.5) + spec*light;
+    gl_FragColor.a = invfresnel*depth.y;
+]
+watershader "waterfast" 0 0 [
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+] [
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    gl_FragColor.rgb = gl_Color.rgb*depth.x*mix(0.6, 1.0, invfresnel*0.5+0.5);
+    gl_FragColor.a = invfresnel*depth.y;
+]
+fastshader water waterfast 1
+altshader water waterfast
+
+watershader "waterreflect" 1 0 [
+    vec3 reflect = texture2DProj(tex0, gl_TexCoord[0] + vec4(0.4*dudv, 0.0, 0.0)).rgb;
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+] [
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    gl_FragColor.rgb = mix(reflect, gl_Color.rgb*depth.x, invfresnel*0.5+0.5) + spec*light;
+    gl_FragColor.a = invfresnel*depth.y;
+]
+watershader "waterreflectfast" 0 0 [
+    vec3 reflect = texture2DProj(tex0, gl_TexCoord[0] + vec4(0.4*dudv, 0.0, 0.0)).rgb;
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+] [
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    gl_FragColor.rgb = mix(reflect, gl_Color.rgb*depth.x, invfresnel*0.5+0.5);
+    gl_FragColor.a = invfresnel*depth.y;
+]
+fastshader waterreflect waterreflectfast 2
+altshader waterreflect waterreflectfast
+
+watershader "waterrefract" 1 1 [
+    vec2 dtc = gl_TexCoord[2].xy + 0.025*dudv;
+    vec3 bump = texture2D(tex1, dtc).rgb*2.0 - 1.0;
+    dudv = texture2D(tex2, dtc).xy*2.0 - 1.0;
+
+    vec2 rtc = gl_TexCoord[0].xy/gl_TexCoord[0].w + 0.01*dudv;
+    vec3 reflect = texture2D(tex0, rtc).rgb;
+    vec3 refract = texture2D(tex3, rtc).rgb;
+] [
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    gl_FragColor = vec4(mix(reflect, refract, invfresnel*0.5+0.5) + spec*light, 0.0);
+]
+watershader "waterrefractfast" 0 1 [
+    vec4 rtc = gl_TexCoord[0] + vec4(0.4*dudv, 0.0, 0.0);
+    vec3 reflect = texture2DProj(tex0, rtc).rgb;
+    vec3 refract = texture2DProj(tex3, rtc).rgb;
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+] [
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    gl_FragColor = vec4(mix(reflect, refract, invfresnel*0.5+0.5), 0.0);
+]
+fastshader waterrefract waterrefractfast 2
+altshader waterrefract waterrefractfast
+
+watershader "waterfade" 1 1 [
+    vec2 dtc = gl_TexCoord[2].xy + 0.025*dudv;
+    vec3 bump = texture2D(tex1, dtc).rgb*2.0 - 1.0;
+    dudv = texture2D(tex2, dtc).xy*2.0 - 1.0;
+
+    vec2 projtc = gl_TexCoord[0].xy/gl_TexCoord[0].w;
+    vec2 rtc = projtc + 0.01*dudv;
+    vec3 reflect = texture2D(tex0, rtc).rgb;
+    vec3 refract = texture2D(tex3, rtc).rgb;
+    float fade = gl_TexCoord[0].z + 4.0*texture2D(tex3, projtc).a;
+    gl_FragColor.a = fade * clamp(gl_FragCoord.z, 0.0, 1.0);
+] [
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    gl_FragColor.rgb = mix(reflect, refract, invfresnel*0.5+0.5) + spec*light;
+]
+watershader "waterfadefast" 0 1 [
+    vec4 rtc = gl_TexCoord[0] + vec4(0.4*dudv, 0.0, 0.0);
+    vec3 reflect = texture2DProj(tex0, rtc).rgb;
+    vec3 refract = texture2DProj(tex3, rtc).rgb;
+    gl_FragColor.a = gl_TexCoord[0].z + 4.0*texture2DProj(tex3, gl_TexCoord[0]).a;
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+] [
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    gl_FragColor.rgb = mix(reflect, refract, invfresnel*0.5+0.5);
+]
+fastshader waterfade waterfadefast 2
+altshader waterfade waterrefract
+
+watershader "waterenv" 1 0 [
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    vec3 reflect = textureCube(tex0, camvec - 2.0*invfresnel*bump).rgb;
+] [
+    gl_FragColor.rgb = mix(reflect, gl_Color.rgb*depth.x, invfresnel*0.5+0.5) + spec*light;
+    gl_FragColor.a = invfresnel*depth.y;
+]
+watershader "waterenvfast" 0 0 [
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    vec3 reflect = textureCube(tex0, camvec - 2.0*invfresnel*bump).rgb;
+] [
+    gl_FragColor.rgb = mix(reflect, gl_Color.rgb*depth.x, invfresnel*0.5+0.5);
+    gl_FragColor.a = invfresnel*depth.y;
+]
+fastshader waterenv waterenvfast 2
+altshader waterenv waterenvfast
+
+watershader "waterenvrefract" 1 1 [
+    vec2 dtc = gl_TexCoord[2].xy + 0.025*dudv;
+    vec3 bump = texture2D(tex1, dtc).rgb*2.0 - 1.0;
+    dudv = texture2D(tex2, dtc).xy*2.0 - 1.0;
+
+    vec3 refract = texture2D(tex3, gl_TexCoord[0].xy/gl_TexCoord[0].w + 0.01*dudv).rgb;
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    vec3 reflect = textureCube(tex0, camvec - 2.0*invfresnel*bump).rgb;
+] [
+    gl_FragColor = vec4(mix(reflect, refract, invfresnel*0.5+0.5) + spec*light, 0.0);
+]
+watershader "waterenvrefractfast" 0 1 [
+    vec3 refract = texture2DProj(tex3, gl_TexCoord[0] + vec4(0.4*dudv, 0.0, 0.0)).rgb;
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    vec3 reflect = textureCube(tex0, camvec - 2.0*invfresnel*bump).rgb;
+] [
+    gl_FragColor = vec4(mix(reflect, refract, invfresnel*0.5+0.5), 0.0);
+]
+fastshader waterenvrefract waterenvrefractfast 2
+altshader waterenvrefract waterenvrefractfast
+
+watershader "waterenvfade" 1 1 [
+    vec2 dtc = gl_TexCoord[2].xy + 0.025*dudv;
+    vec3 bump = texture2D(tex1, dtc).rgb*2.0 - 1.0;
+    dudv = texture2D(tex2, dtc).xy*2.0 - 1.0;
+
+    vec2 projtc = gl_TexCoord[0].xy/gl_TexCoord[0].w;
+    vec3 refract = texture2D(tex3, projtc + 0.01*dudv).rgb;
+    float fade = gl_TexCoord[0].z + 4.0*texture2D(tex3, projtc).a;
+    gl_FragColor.a = fade * clamp(gl_FragCoord.z, 0.0, 1.0);
+
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    vec3 reflect = textureCube(tex0, camvec - 2.0*invfresnel*bump).rgb;
+] [
+    gl_FragColor.rgb = mix(reflect, refract, invfresnel*0.5+0.5) + spec*light;
+]
+watershader "waterenvfadefast" 0 1 [
+    vec3 refract = texture2DProj(tex3, gl_TexCoord[0] + vec4(0.4*dudv, 0.0, 0.0)).rgb;
+    gl_FragColor.a = gl_TexCoord[0].z + 4.0*texture2DProj(tex3, gl_TexCoord[0]).a;
+    vec3 bump = texture2D(tex1, gl_TexCoord[2].xy + 0.025*dudv).rgb*2.0 - 1.0;
+    float invfresnel = clamp(dot(camvec, bump), 0.0, 1.0);
+    vec3 reflect = textureCube(tex0, camvec - 2.0*invfresnel*bump).rgb;
+] [
+    gl_FragColor.rgb = mix(reflect, refract, invfresnel*0.5+0.5);
+]
+fastshader waterenvfade waterenvfadefast 2
+altshader waterenvfade waterenvrefract
+
+causticshader = [
+    lazyshader 4 $arg1 [
+        #pragma CUBE2_fog
+        uniform vec4 texgenS, texgenT;
+        void main(void)
+        {
+            gl_Position = ftransform();
+            gl_TexCoord[0].xy = vec2(dot(texgenS.xyz, gl_Vertex.xyz), dot(texgenT.xyz, gl_Vertex.xyz));
+        }
+    ] [
+        uniform vec4 frameoffset;
+        uniform sampler2D tex0, tex1;
+        void main(void)
+        {
+            @arg2
+        }
+    ]
+]
+causticshader caustic [
+    gl_FragColor = frameoffset.x*texture2D(tex0, gl_TexCoord[0].xy) + frameoffset.y*texture2D(tex1, gl_TexCoord[0].xy);
+]
+causticshader causticfast [
+    gl_FragColor = frameoffset.z*texture2D(tex0, gl_TexCoord[0].xy);
+]
+fastshader caustic causticfast 2
+
+lazyshader 4 "lava" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        gl_FragColor = gl_Color * texture2D(tex0, gl_TexCoord[0].xy) * 2.0;
+    }
+]
+
+lazyshader 4 "lavaglare" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color*2.0 - 1.0;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        vec4 glow = texture2D(tex0, gl_TexCoord[0].xy) * gl_Color;
+        float k = max(glow.r, max(glow.g, glow.b));
+        gl_FragColor = glow*k*k*32.0;
+    }
+]
+
+lazyshader 4 "waterfallrefract" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+        gl_TexCoord[1] = gl_TextureMatrix[0] * gl_Vertex;
+    }
+] [
+    uniform vec4 dudvoffset;
+    uniform sampler2D tex0, tex2, tex4;
+    void main(void)
+    {
+        vec4 diffuse = texture2D(tex0, gl_TexCoord[0].xy);
+        vec2 dudv = texture2D(tex2, gl_TexCoord[0].xy + 0.2*diffuse.xy + dudvoffset.xy).xy;
+        vec4 refract = texture2DProj(tex4, gl_TexCoord[1] + vec4(4.0*dudv, 0.0, 0.0));
+        gl_FragColor = mix(refract, gl_Color, diffuse);
+    }
+]
+
+lazyshader 4 "waterfallenvrefract" [
+    #pragma CUBE2_fog
+    uniform vec4 camera;
+    varying vec3 camdir;
+    varying mat3 world;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+        gl_TexCoord[1] = gl_TextureMatrix[0] * gl_Vertex;
+        camdir = camera.xyz - gl_Vertex.xyz;
+        vec3 absnorm = abs(gl_Normal);
+        world = mat3(absnorm.yzx, -absnorm.zxy, gl_Normal);
+    }
+] [
+    uniform vec4 dudvoffset;
+    uniform sampler2D tex0, tex1, tex2, tex4;
+    uniform samplerCube tex3;
+    varying vec3 camdir;
+    varying mat3 world;
+    void main(void)
+    {
+        vec4 diffuse = texture2D(tex0, gl_TexCoord[0].xy);
+        vec2 dudv = texture2D(tex2, gl_TexCoord[0].xy + 0.2*diffuse.xy + dudvoffset.xy).xy;
+        vec3 normal = world * (texture2D(tex1, gl_TexCoord[0].xy + 0.1*dudv).rgb*2.0 - 1.0);
+        vec4 refract = texture2DProj(tex4, gl_TexCoord[1] + vec4(4.0*dudv, 0.0, 0.0));
+        vec3 camvec = normalize(camdir);
+        float invfresnel = dot(normal, camvec);
+        vec4 reflect = textureCube(tex3, 2.0*invfresnel*normal - camvec);
+        gl_FragColor = mix(mix(reflect, refract, 1.0 - 0.4*step(0.0, invfresnel)), gl_Color, diffuse);
+    }
+]
+altshader waterfallenvrefract waterfallrefract
+
+lazyshader 4 "waterfallenv" [
+    #pragma CUBE2_fog
+    uniform vec4 camera;
+    varying vec3 camdir;
+    varying mat3 world;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+        camdir = camera.xyz - gl_Vertex.xyz;
+        vec3 absnorm = abs(gl_Normal);
+        world = mat3(absnorm.yzx, -absnorm.zxy, gl_Normal);
+    }
+] [
+    uniform vec4 dudvoffset;
+    uniform sampler2D tex0, tex1, tex2;
+    uniform samplerCube tex3;
+    varying vec3 camdir;
+    varying mat3 world;
+    void main(void)
+    {
+        vec4 diffuse = texture2D(tex0, gl_TexCoord[0].xy);
+        vec2 dudv = texture2D(tex2, gl_TexCoord[0].xy + 0.2*diffuse.xy + dudvoffset.xy).xy;
+        vec3 normal = world * (texture2D(tex1, gl_TexCoord[0].xy + 0.1*dudv).rgb*2.0 - 1.0);
+        vec3 camvec = normalize(camdir);
+        vec4 reflect = textureCube(tex3, 2.0*dot(normal, camvec)*normal - camvec);
+        gl_FragColor.rgb = mix(reflect.rgb, gl_Color.rgb, diffuse.rgb);
+        gl_FragColor.a = 0.25 + 0.75*diffuse.r;
+    }
+]
+
+lazyshader 4 "glass" [
+    uniform vec4 camera;
+    varying vec3 rvec, camdir, normal;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        rvec = gl_MultiTexCoord0.xyz;
+        camdir = camera.xyz - gl_Vertex.xyz;
+        normal = gl_Normal;
+    }
+] [
+    #pragma CUBE2_fogrgba vec4(0.0, 0.0, 0.0, 1.0)
+    uniform samplerCube tex0;
+    varying vec3 rvec, camdir, normal;
+    void main(void)
+    {
+        vec3 camvec = normalize(camdir);
+        vec3 reflect = textureCube(tex0, rvec).rgb;
+
+        float invfresnel = max(dot(camvec, normal), 0.70);
+        gl_FragColor.rgb = mix(reflect, gl_Color.rgb*0.05, invfresnel);
+        gl_FragColor.a = invfresnel * 0.95;
+    }
+]
+lazyshader 4 "glassfast" [
+    varying vec3 rvec;
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        rvec = gl_MultiTexCoord0.xyz;
+    }
+] [
+    #pragma CUBE2_fogrgba vec4(0.0, 0.0, 0.0, 1.0)
+    uniform samplerCube tex0;
+    varying vec3 rvec;
+    void main(void)
+    {
+        vec3 reflect = textureCube(tex0, rvec).rgb;
+        const float invfresnel = 0.75;
+        gl_FragColor.rgb = mix(reflect, gl_Color.rgb*0.05, invfresnel);
+        gl_FragColor.a = invfresnel * 0.95;
+    }
+]
+fastshader glass glassfast 2
+altshader glass glassfast
+
+lazyshader 4 "grass" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+        gl_TexCoord[1].xy = gl_MultiTexCoord1.xy;
+    }
+] [
+    #pragma CUBE2_fogrgba vec4(0.0, 0.0, 0.0, 0.0)
+    uniform sampler2D tex0, tex1;
+    void main(void)
+    {
+        vec4 diffuse = texture2D(tex0, gl_TexCoord[0].xy);
+        diffuse.rgb *= 2.0;
+        vec4 lm = texture2D(tex1, gl_TexCoord[1].xy) * gl_Color;
+        lm.rgb *= lm.a;
+        gl_FragColor = diffuse * lm;
+    }
+]
+
+shader 4 "overbrightdecal" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        vec4 diffuse = texture2D(tex0, gl_TexCoord[0].xy);
+        gl_FragColor = mix(gl_Color, diffuse, gl_Color.a);
+    }
+]
+
+shader 4 "saturatedecal" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        vec4 diffuse = texture2D(tex0, gl_TexCoord[0].xy);
+        diffuse.rgb *= 2.0;
+        gl_FragColor = diffuse * gl_Color;
+    }
+]
+
+shader 4 "skyboxglare" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+        gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
+    }
+] [
+    uniform sampler2D tex0;
+    void main(void)
+    {
+        vec4 glare = texture2D(tex0, gl_TexCoord[0].xy) * gl_Color;
+        gl_FragColor.rgb = vec3(dot(glare.rgb, vec3(10.56, 10.88, 10.56)) - 30.4);
+        gl_FragColor.a = glare.a;
+    }
+]
+
diff --git a/config/keymap.cfg b/config/keymap.cfg
new file mode 100644
index 0000000..ed79611
--- /dev/null
+++ b/config/keymap.cfg
@@ -0,0 +1,486 @@
+keymap -255 MOUSE255
+keymap -254 MOUSE254
+keymap -253 MOUSE253
+keymap -252 MOUSE252
+keymap -251 MOUSE251
+keymap -250 MOUSE250
+keymap -249 MOUSE249
+keymap -248 MOUSE248
+keymap -247 MOUSE247
+keymap -246 MOUSE246
+keymap -245 MOUSE245
+keymap -244 MOUSE244
+keymap -243 MOUSE243
+keymap -242 MOUSE242
+keymap -241 MOUSE241
+keymap -240 MOUSE240
+keymap -239 MOUSE239
+keymap -238 MOUSE238
+keymap -237 MOUSE237
+keymap -236 MOUSE236
+keymap -235 MOUSE235
+keymap -234 MOUSE234
+keymap -233 MOUSE233
+keymap -232 MOUSE232
+keymap -231 MOUSE231
+keymap -230 MOUSE230
+keymap -229 MOUSE229
+keymap -228 MOUSE228
+keymap -227 MOUSE227
+keymap -226 MOUSE226
+keymap -225 MOUSE225
+keymap -224 MOUSE224
+keymap -223 MOUSE223
+keymap -222 MOUSE222
+keymap -221 MOUSE221
+keymap -220 MOUSE220
+keymap -219 MOUSE219
+keymap -218 MOUSE218
+keymap -217 MOUSE217
+keymap -216 MOUSE216
+keymap -215 MOUSE215
+keymap -214 MOUSE214
+keymap -213 MOUSE213
+keymap -212 MOUSE212
+keymap -211 MOUSE211
+keymap -210 MOUSE210
+keymap -209 MOUSE209
+keymap -208 MOUSE208
+keymap -207 MOUSE207
+keymap -206 MOUSE206
+keymap -205 MOUSE205
+keymap -204 MOUSE204
+keymap -203 MOUSE203
+keymap -202 MOUSE202
+keymap -201 MOUSE201
+keymap -200 MOUSE200
+keymap -199 MOUSE199
+keymap -198 MOUSE198
+keymap -197 MOUSE197
+keymap -196 MOUSE196
+keymap -195 MOUSE195
+keymap -194 MOUSE194
+keymap -193 MOUSE193
+keymap -192 MOUSE192
+keymap -191 MOUSE191
+keymap -190 MOUSE190
+keymap -189 MOUSE189
+keymap -188 MOUSE188
+keymap -187 MOUSE187
+keymap -186 MOUSE186
+keymap -185 MOUSE185
+keymap -184 MOUSE184
+keymap -183 MOUSE183
+keymap -182 MOUSE182
+keymap -181 MOUSE181
+keymap -180 MOUSE180
+keymap -179 MOUSE179
+keymap -178 MOUSE178
+keymap -177 MOUSE177
+keymap -176 MOUSE176
+keymap -175 MOUSE175
+keymap -174 MOUSE174
+keymap -173 MOUSE173
+keymap -172 MOUSE172
+keymap -171 MOUSE171
+keymap -170 MOUSE170
+keymap -169 MOUSE169
+keymap -168 MOUSE168
+keymap -167 MOUSE167
+keymap -166 MOUSE166
+keymap -165 MOUSE165
+keymap -164 MOUSE164
+keymap -163 MOUSE163
+keymap -162 MOUSE162
+keymap -161 MOUSE161
+keymap -160 MOUSE160
+keymap -159 MOUSE159
+keymap -158 MOUSE158
+keymap -157 MOUSE157
+keymap -156 MOUSE156
+keymap -155 MOUSE155
+keymap -154 MOUSE154
+keymap -153 MOUSE153
+keymap -152 MOUSE152
+keymap -151 MOUSE151
+keymap -150 MOUSE150
+keymap -149 MOUSE149
+keymap -148 MOUSE148
+keymap -147 MOUSE147
+keymap -146 MOUSE146
+keymap -145 MOUSE145
+keymap -144 MOUSE144
+keymap -143 MOUSE143
+keymap -142 MOUSE142
+keymap -141 MOUSE141
+keymap -140 MOUSE140
+keymap -139 MOUSE139
+keymap -138 MOUSE138
+keymap -137 MOUSE137
+keymap -136 MOUSE136
+keymap -135 MOUSE135
+keymap -134 MOUSE134
+keymap -133 MOUSE133
+keymap -132 MOUSE132
+keymap -131 MOUSE131
+keymap -130 MOUSE130
+keymap -129 MOUSE129
+keymap -128 MOUSE128
+keymap -127 MOUSE127
+keymap -126 MOUSE126
+keymap -125 MOUSE125
+keymap -124 MOUSE124
+keymap -123 MOUSE123
+keymap -122 MOUSE122
+keymap -121 MOUSE121
+keymap -120 MOUSE120
+keymap -119 MOUSE119
+keymap -118 MOUSE118
+keymap -117 MOUSE117
+keymap -116 MOUSE116
+keymap -115 MOUSE115
+keymap -114 MOUSE114
+keymap -113 MOUSE113
+keymap -112 MOUSE112
+keymap -111 MOUSE111
+keymap -110 MOUSE110
+keymap -109 MOUSE109
+keymap -108 MOUSE108
+keymap -107 MOUSE107
+keymap -106 MOUSE106
+keymap -105 MOUSE105
+keymap -104 MOUSE104
+keymap -103 MOUSE103
+keymap -102 MOUSE102
+keymap -101 MOUSE101
+keymap -100 MOUSE100
+keymap -99 MOUSE99
+keymap -98 MOUSE98
+keymap -97 MOUSE97
+keymap -96 MOUSE96
+keymap -95 MOUSE95
+keymap -94 MOUSE94
+keymap -93 MOUSE93
+keymap -92 MOUSE92
+keymap -91 MOUSE91
+keymap -90 MOUSE90
+keymap -89 MOUSE89
+keymap -88 MOUSE88
+keymap -87 MOUSE87
+keymap -86 MOUSE86
+keymap -85 MOUSE85
+keymap -84 MOUSE84
+keymap -83 MOUSE83
+keymap -82 MOUSE82
+keymap -81 MOUSE81
+keymap -80 MOUSE80
+keymap -79 MOUSE79
+keymap -78 MOUSE78
+keymap -77 MOUSE77
+keymap -76 MOUSE76
+keymap -75 MOUSE75
+keymap -74 MOUSE74
+keymap -73 MOUSE73
+keymap -72 MOUSE72
+keymap -71 MOUSE71
+keymap -70 MOUSE70
+keymap -69 MOUSE69
+keymap -68 MOUSE68
+keymap -67 MOUSE67
+keymap -66 MOUSE66
+keymap -65 MOUSE65
+keymap -64 MOUSE64
+keymap -63 MOUSE63
+keymap -62 MOUSE62
+keymap -61 MOUSE61
+keymap -60 MOUSE60
+keymap -59 MOUSE59
+keymap -58 MOUSE58
+keymap -57 MOUSE57
+keymap -56 MOUSE56
+keymap -55 MOUSE55
+keymap -54 MOUSE54
+keymap -53 MOUSE53
+keymap -52 MOUSE52
+keymap -51 MOUSE51
+keymap -50 MOUSE50
+keymap -49 MOUSE49
+keymap -48 MOUSE48
+keymap -47 MOUSE47
+keymap -46 MOUSE46
+keymap -45 MOUSE45
+keymap -44 MOUSE44
+keymap -43 MOUSE43
+keymap -42 MOUSE42
+keymap -41 MOUSE41
+keymap -40 MOUSE40
+keymap -39 MOUSE39
+keymap -38 MOUSE38
+keymap -37 MOUSE37
+keymap -36 MOUSE36
+keymap -35 MOUSE35
+keymap -34 MOUSE34
+keymap -33 MOUSE33
+keymap -32 MOUSE32
+keymap -31 MOUSE31
+keymap -30 MOUSE30
+keymap -29 MOUSE29
+keymap -28 MOUSE28
+keymap -27 MOUSE27
+keymap -26 MOUSE26
+keymap -25 MOUSE25
+keymap -24 MOUSE24
+keymap -23 MOUSE23
+keymap -22 MOUSE22
+keymap -21 MOUSE21
+keymap -20 MOUSE20
+keymap -19 MOUSE19
+keymap -18 MOUSE18
+keymap -17 MOUSE17
+keymap -16 MOUSE16
+keymap -15 MOUSE15
+keymap -14 MOUSE14
+keymap -13 MOUSE13
+keymap -12 MOUSE12
+keymap -11 MOUSE11
+keymap -10 MOUSE10
+keymap -9 MOUSE9
+keymap -8 MOUSE8
+keymap -7 MOUSE7
+keymap -6 MOUSE6
+keymap -5 MOUSE5
+keymap -4 MOUSE4
+keymap -3 MOUSE2
+keymap -2 MOUSE3
+keymap -1 MOUSE1
+keymap 8 BACKSPACE
+keymap 9 TAB
+keymap 12 CLEAR
+keymap 13 RETURN
+keymap 19 PAUSE
+keymap 27 ESCAPE
+keymap 32 SPACE
+keymap 33 EXCLAIM
+keymap 34 QUOTEDBL
+keymap 35 HASH
+keymap 36 DOLLAR
+keymap 38 AMPERSAND
+keymap 39 QUOTE
+keymap 40 LEFTPAREN
+keymap 41 RIGHTPAREN
+keymap 42 ASTERISK
+keymap 43 PLUS
+keymap 44 COMMA
+keymap 45 MINUS
+keymap 46 PERIOD
+keymap 47 SLASH
+keymap 48 0
+keymap 49 1
+keymap 50 2
+keymap 51 3
+keymap 52 4
+keymap 53 5
+keymap 54 6
+keymap 55 7
+keymap 56 8
+keymap 57 9
+keymap 58 COLON
+keymap 59 SEMICOLON
+keymap 60 LESS
+keymap 61 EQUALS
+keymap 62 GREATER
+keymap 63 QUESTION
+keymap 64 AT
+keymap 91 LEFTBRACKET
+keymap 92 BACKSLASH
+keymap 93 RIGHTBRACKET
+keymap 94 CARET
+keymap 95 UNDERSCORE
+keymap 96 BACKQUOTE
+keymap 97 A
+keymap 98 B
+keymap 99 C
+keymap 100 D
+keymap 101 E
+keymap 102 F
+keymap 103 G
+keymap 104 H
+keymap 105 I
+keymap 106 J
+keymap 107 K
+keymap 108 L
+keymap 109 M
+keymap 110 N
+keymap 111 O
+keymap 112 P
+keymap 113 Q
+keymap 114 R
+keymap 115 S
+keymap 116 T
+keymap 117 U
+keymap 118 V
+keymap 119 W
+keymap 120 X
+keymap 121 Y
+keymap 122 Z
+keymap 127 DELETE
+keymap 160 WORLD_0
+keymap 161 WORLD_1
+keymap 162 WORLD_2
+keymap 163 WORLD_3
+keymap 164 WORLD_4
+keymap 165 WORLD_5
+keymap 166 WORLD_6
+keymap 167 WORLD_7
+keymap 168 WORLD_8
+keymap 169 WORLD_9
+keymap 170 WORLD_10
+keymap 171 WORLD_11
+keymap 172 WORLD_12
+keymap 173 WORLD_13
+keymap 174 WORLD_14
+keymap 175 WORLD_15
+keymap 176 WORLD_16
+keymap 177 WORLD_17
+keymap 178 WORLD_18
+keymap 179 WORLD_19
+keymap 180 WORLD_20
+keymap 181 WORLD_21
+keymap 182 WORLD_22
+keymap 183 WORLD_23
+keymap 184 WORLD_24
+keymap 185 WORLD_25
+keymap 186 WORLD_26
+keymap 187 WORLD_27
+keymap 188 WORLD_28
+keymap 189 WORLD_29
+keymap 190 WORLD_30
+keymap 191 WORLD_31
+keymap 192 WORLD_32
+keymap 193 WORLD_33
+keymap 194 WORLD_34
+keymap 195 WORLD_35
+keymap 196 WORLD_36
+keymap 197 WORLD_37
+keymap 198 WORLD_38
+keymap 199 WORLD_39
+keymap 200 WORLD_40
+keymap 201 WORLD_41
+keymap 202 WORLD_42
+keymap 203 WORLD_43
+keymap 204 WORLD_44
+keymap 205 WORLD_45
+keymap 206 WORLD_46
+keymap 207 WORLD_47
+keymap 208 WORLD_48
+keymap 209 WORLD_49
+keymap 210 WORLD_50
+keymap 211 WORLD_51
+keymap 212 WORLD_52
+keymap 213 WORLD_53
+keymap 214 WORLD_54
+keymap 215 WORLD_55
+keymap 216 WORLD_56
+keymap 217 WORLD_57
+keymap 218 WORLD_58
+keymap 219 WORLD_59
+keymap 220 WORLD_60
+keymap 221 WORLD_61
+keymap 222 WORLD_62
+keymap 223 WORLD_63
+keymap 224 WORLD_64
+keymap 225 WORLD_65
+keymap 226 WORLD_66
+keymap 227 WORLD_67
+keymap 228 WORLD_68
+keymap 229 WORLD_69
+keymap 230 WORLD_70
+keymap 231 WORLD_71
+keymap 232 WORLD_72
+keymap 233 WORLD_73
+keymap 234 WORLD_74
+keymap 235 WORLD_75
+keymap 236 WORLD_76
+keymap 237 WORLD_77
+keymap 238 WORLD_78
+keymap 239 WORLD_79
+keymap 240 WORLD_80
+keymap 241 WORLD_81
+keymap 242 WORLD_82
+keymap 243 WORLD_83
+keymap 244 WORLD_84
+keymap 245 WORLD_85
+keymap 246 WORLD_86
+keymap 247 WORLD_87
+keymap 248 WORLD_88
+keymap 249 WORLD_89
+keymap 250 WORLD_90
+keymap 251 WORLD_91
+keymap 252 WORLD_92
+keymap 253 WORLD_93
+keymap 254 WORLD_94
+keymap 255 WORLD_95
+keymap 256 KP0
+keymap 257 KP1
+keymap 258 KP2
+keymap 259 KP3
+keymap 260 KP4
+keymap 261 KP5
+keymap 262 KP6
+keymap 263 KP7
+keymap 264 KP8
+keymap 265 KP9
+keymap 266 KP_PERIOD
+keymap 267 KP_DIVIDE
+keymap 268 KP_MULTIPLY
+keymap 269 KP_MINUS
+keymap 270 KP_PLUS
+keymap 271 KP_ENTER
+keymap 272 KP_EQUALS
+keymap 273 UP
+keymap 274 DOWN
+keymap 275 RIGHT
+keymap 276 LEFT
+keymap 277 INSERT
+keymap 278 HOME
+keymap 279 END
+keymap 280 PAGEUP
+keymap 281 PAGEDOWN
+keymap 282 F1
+keymap 283 F2
+keymap 284 F3
+keymap 285 F4
+keymap 286 F5
+keymap 287 F6
+keymap 288 F7
+keymap 289 F8
+keymap 290 F9
+keymap 291 F10
+keymap 292 F11
+keymap 293 F12
+keymap 294 F13
+keymap 295 F14
+keymap 296 F15
+keymap 300 NUMLOCK
+keymap 301 CAPSLOCK
+keymap 302 SCROLLOCK
+keymap 303 RSHIFT
+keymap 304 LSHIFT
+keymap 305 RCTRL
+keymap 306 LCTRL
+keymap 307 RALT
+keymap 308 LALT
+keymap 309 RMETA
+keymap 310 LMETA
+keymap 311 LSUPER
+keymap 312 RSUPER
+keymap 313 MODE
+keymap 314 COMPOSE
+keymap 315 HELP
+keymap 316 PRINT
+keymap 317 SYSREQ
+keymap 318 BREAK
+keymap 319 MENU
+keymap 320 POWER
+keymap 321 EURO
+keymap 322 UNDO
diff --git a/config/legacy.cfg b/config/legacy.cfg
new file mode 100644
index 0000000..92ae207
--- /dev/null
+++ b/config/legacy.cfg
@@ -0,0 +1,41 @@
+loadsky = [
+    skybox $arg1
+    if (> $numargs 1) [spinsky $arg2]
+    if (> $numargs 2) [yawsky $arg3]
+]; setcomplete loadsky 1
+
+mapmsg = [maptitle $arg1]; setcomplete mapmsg 1
+cloudalpha = [cloudblend $arg1]
+savecurrentmap = [ savemap ]
+
+// people have complained /fov was removed
+fov = [do [ @(? (isthirdperson) third first)personfov @arg1 ]]; setcomplete fov 1
+texturecull = [ compactvslots 1 ]; setcomplete texturecull 1
+name = [playername $arg1]; setcomplete name 1
+colour = [playercolour $arg1]; setcomplete colour 1
+vanity = [playervanity $arg1]; setcomplete vanity 1
+setinfo = [playername $arg1; playercolour $arg2; playermodel $arg3; playervanity $arg4; playerloadweap $arg5]; setcomplete setinfo 1
+setmaster = [ setpriv $arg1 ]; setcomplete setmaster 1
+vcolor = [ vcolour $arg1 $arg2 $arg3 ]
+// random stuff
+privcreatortex = $privfoundertex
+
+takescreenshot = [
+	sleep 0 [
+		showhud @showhud
+		showconsole @showconsole
+		screenshot "screenshot"
+	]
+	showhud 0
+	showconsole 0
+]; setcomplete takescreenshot 1
+
+// sauer rotation aliases
+alias octamaps [
+    allowmaps "complex douze ot academy metl2 metl3 nmp8 refuge tartech kalking1 dock turbine fanatic_quake oddworld wake5 aard3c curvedm fragplaza pgdm kffa neondevastation hog2 memento neonpanic lostinspace DM_BS1 shindou sdm1 shinmei1 stemple powerplant phosgene oasis island metl4 ruby frozen ksauer1 killfactory corruption deathtek aqueducts orbe roughinery shadowed torment moonlite darkdeath orion nmp10 katrez_d thor frostbyte ogrosupply kmap5 thetowers guacamole tejen hades paradigm [...]
+    mainmaps "complex douze ot academy metl2 metl3 nmp8 refuge tartech kalking1 dock turbine fanatic_quake oddworld wake5 aard3c curvedm fragplaza pgdm kffa neondevastation hog2 memento neonpanic lostinspace DM_BS1 shindou sdm1 shinmei1 stemple powerplant phosgene oasis island metl4 ruby frozen ksauer1 killfactory corruption deathtek aqueducts orbe roughinery shadowed torment moonlite darkdeath orion nmp10 katrez_d thor frostbyte ogrosupply kmap5 thetowers guacamole tejen hades paradigm  [...]
+    capturemaps "hallo reissen flagstone face-capture shipwreck dust2 urban_c berlin_wall akroseum valhalla damnation mach2 redemption tejen europium capture_night l_ctf forge campo wdcd sacrifice core_transfer recovery"
+    defendmaps "urban_c nevil_c fb_capture nmp9 c_valley lostinspace fc3 face-capture nmp4 nmp8 hallo monastery ph-capture hades fc4 relic frostbyte venice paradigm corruption asteroids ogrosupply reissen akroseum duomo capture_night c_egypt tejen dust2 campo killcore3 damnation arabic cwcastle river_c serenity"
+    bombermaps "hallo reissen flagstone face-capture shipwreck dust2 urban_c berlin_wall akroseum valhalla damnation mach2 redemption tejen europium capture_night l_ctf forge campo wdcd sacrifice core_transfer recovery"
+    holdmaps "complex douze ot academy metl2 metl3 nmp8 refuge tartech kalking1 dock turbine fanatic_quake oddworld wake5 aard3c curvedm fragplaza pgdm kffa neondevastation hog2 memento neonpanic lostinspace DM_BS1 shindou sdm1 shinmei1 stemple powerplant phosgene oasis island metl4 ruby frozen ksauer1 killfactory corruption deathtek aqueducts orbe roughinery shadowed torment moonlite darkdeath orion nmp10 katrez_d thor frostbyte ogrosupply kmap5 thetowers guacamole tejen hades paradigm  [...]
+]
diff --git a/config/map/default.cfg b/config/map/default.cfg
new file mode 100644
index 0000000..bbabce9
--- /dev/null
+++ b/config/map/default.cfg
@@ -0,0 +1,74 @@
+// Red Eclipse: Default Map Settings
+// This file is executed when you create a new map, or if a map does not supply its own config file.
+
+loop i 4 [
+    texture [water@(+ $i 1)] "textures/waterfall.jpg"
+    texture 1 "textures/waterfall.jpg"
+    texture 1 "textures/watern.jpg"
+    texture 1 "textures/waterdudv.jpg"
+    texture 1 "textures/waterfalln.jpg"
+    texture 1 "textures/waterfalldudv.jpg"
+
+    texture [lava@(+ $i 1)] "textures/lava.jpg" 0 0 0 1
+    texture 1 "textures/lava.jpg"
+
+    texture [glass@(+ $i 1)] "textures/glass.jpg" 0 0 0 8
+    texture 1 "textures/watern.jpg"
+    texture 1 "textures/waterdudv.jpg"
+]
+
+setshader stdworld
+texture 0 "textures/sky.png"
+texture 0 "textures/default.png"
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 1
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 2
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.75 0.25 0.125
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 3
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.25 0.75 0.125
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 4
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.125 0.25 0.75
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 5
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.75 0.75 0.25
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 6
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.75 0.25 0.75
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 7
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.9 0.9 0.9
+
+exec "config/map/textures.cfg"
+exec "config/map/models.cfg"
+exec "config/map/sounds.cfg"
diff --git a/config/map/models.cfg b/config/map/models.cfg
new file mode 100644
index 0000000..68183d0
--- /dev/null
+++ b/config/map/models.cfg
@@ -0,0 +1,7 @@
+exec "models/jojo/package.cfg"
+exec "models/luckystrike/package.cfg"
+exec "models/vegetation/package.cfg"
+exec "models/mikeplus64/package.cfg"
+exec "models/john/package.cfg"
+exec "models/unnamed/package.cfg"
+exec "models/acerspyro/package.cfg"
\ No newline at end of file
diff --git a/config/map/octa.cfg b/config/map/octa.cfg
new file mode 100644
index 0000000..012fe48
--- /dev/null
+++ b/config/map/octa.cfg
@@ -0,0 +1,66 @@
+// Red Eclipse: OCTA Map Import Settings
+
+playasong = []
+cloudalpha = [cloudblend $arg1]
+skyboxcolour = [skycolour $arg1 $arg2 $arg3]
+cloudboxcolour = [cloudcolour $arg1 $arg2 $arg3]
+
+texture water "textures/waterfall.jpg" // water surface
+texture 1 "textures/waterfall.jpg" // waterfall
+texture 1 "textures/watern.jpg" // water normals
+texture 1 "textures/waterdudv.jpg" // water distortion
+texture 1 "textures/waterfalln.jpg" // waterfall normals
+texture 1 "textures/waterfalldudv.jpg" // waterfall distortion
+
+texture lava "textures/lava.jpg" 0 0 0 2 // lava surface
+texture 1 "textures/lava.jpg" 0 0 0 2 // falling lava
+
+setshader stdworld
+texture 0 "skyboxes/clouds02" // sky
+texture 0 "textures/waterfall" // water
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 2
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.75 0.25 0.125
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 3
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.25 0.75 0.125
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 4
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.125 0.25 0.75
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 5
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.75 0.75 0.25
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 6
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.75 0.25 0.75
+
+setshader bumpspecmapworld
+setshaderparam "specscale" 0.25 0.25 0.25
+texture c "trak/trak6/tile3.jpg" 0 0 0 0.250000 // 7
+texture n "trak/trak6/tile3_nm.png"
+texture s "trak/trak6/tile3_gloss.jpg"
+texcolor 0.9 0.9 0.9
+
+setshader stdworld
+exec "data/default_map_settings.cfg"
+
diff --git a/config/map/sounds.cfg b/config/map/sounds.cfg
new file mode 100644
index 0000000..d7f73db
--- /dev/null
+++ b/config/map/sounds.cfg
@@ -0,0 +1,2 @@
+exec "sounds/ambience/package.cfg"
+exec "sounds/ambience/morph/package.cfg"
diff --git a/config/map/textures.cfg b/config/map/textures.cfg
new file mode 100644
index 0000000..1c0264a
--- /dev/null
+++ b/config/map/textures.cfg
@@ -0,0 +1,10 @@
+exec "trak/package.cfg"
+exec "philipk/package.cfg"
+exec "appleflap/package.cfg"
+exec "luckystrike/package.cfg"
+exec "wicked/package.cfg"
+exec "torley/package.cfg"
+exec "jojo/package.cfg"
+exec "mikeplus64/package.cfg"
+exec "misc/package.cfg"
+exec "unnamed/package.cfg"
\ No newline at end of file
diff --git a/config/menus/editing.cfg b/config/menus/editing.cfg
new file mode 100644
index 0000000..b60aedd
--- /dev/null
+++ b/config/menus/editing.cfg
@@ -0,0 +1,966 @@
+    efuiinsel = 0
+
+    efuitype = 0
+    efuiattr0 = 0
+    efuiattr1 = 0
+    efuiattr2 = 0
+    efuiattr3 = 0
+    efuiattr4 = 0
+
+    efuidotype = 1
+    efuidoattr0 = 1
+    efuidoattr1 = 1
+    efuidoattr2 = 1
+    efuidoattr3 = 1
+    efuidoattr4 = 1
+
+    entupdate = [ entset $tmpt $tmp0 $tmp1 $tmp2 $tmp3 ]
+
+    initentgui = [
+        tmpt = (enttype)
+        @(loopconcat i 4 [result [
+            tmp at i = (entattr @i)
+        ]])
+    ]
+
+    genentattributes = [
+        entattributes = ""
+        n = (listlen $arg2)
+        loop i $n [
+            entattributes = (concat $entattributes [
+                guitext @(at $arg2 $i)
+                guislider tmp at i @(at $arg3 (* 2 $i)) @(at $arg3 (+ 1 (* 2 $i))) entupdate
+            ])
+        ]
+    ]
+
+    newgui entfind [
+        guitext "set the attributes below to search for"
+        guistrut 1
+        guilist [
+            guicheckbox "type   " efuidotype
+            guistrut 1
+            efuitypename = (at $enttypelist $efuitype)
+            guifield efuitypename 11 [efuitypeval = (indexof $enttypelist $efuitypename)
+            if (>= $efuitypeval 0) [efuitype = $efuitypeval]]
+        ]
+        loop i 5 [
+            guilist [
+                guicheckbox [attr @(+ $i 1)] [efuidoattr at i]
+                guistrut 1
+                guifield [efuiattr at i] 11 [ efuiattr at i = (+ $efuiattr at i 0)]
+            ]
+        ]
+
+        guilist [
+            guiradio "off" efuiinsel 0
+            guistrut 1
+            guiradio "inside selection" efuiinsel 1
+            guistrut 1
+            guiradio "outside selection" efuiinsel 2
+        ]
+
+        guibar
+        guistayopen [
+            guibutton "copy attributes from currently selected entity" [
+                loop i 5 [
+                    [efuiattr at i] = (+ (entattr $i) 0)
+                    echo [efuiattr at i] = (+ (entattr $i) 0)
+                ]
+                efuitypeval = (indexof $enttypelist (enttype))
+                if (>= $efuitypeval 0) [efuitype = $efuitypeval]
+            ]
+            guibutton "select all valid entities" [entselect [efuimatch]]
+        ]
+
+        guitab type
+        guilist [
+            loop i (+ (div (listlen $enttypelist) 10) 1) [
+                guilist [
+                    loop j (min (- (listlen $enttypelist) (* 10 $i)) 10) [
+                        guiradio (at $enttypelist (+ $j (* $i 10))) efuitype (+ $j (* $i 10))
+                    ]
+                ]
+            ]
+        ]
+    ]
+
+    newmapsize = 12
+    savemap_name = temp
+
+    newgui edit [
+        guilist [
+            guilist [
+                guibutton   "toggle walk / edit mode"            "edittoggle"
+                guitext " "
+                guibutton   "open textures menu.."               "showtexgui"
+                guibutton   "open this edit menu.."               " "
+                guibutton   "open materials menu.."               "showgui materials"
+                guibutton   "open console.."                  "saycommand /"
+                guitext " "
+                guibutton    "optimize geometry"                  "remip"
+                guibutton   "patch existing lightmap (patch)"      [fullbright 0; patchlight]
+                guibutton   "quick calculate lights (preview)"      [fullbright 0; calclight -1]
+                guibutton    "full calculate lights (full)"         [fullbright 0; calclight 1]
+                guitext " "
+                guibutton   "environment settings.."            "showgui environment"
+                guitext " "
+                guibutton   "claim privileges.."               "showgui serverpriv"
+                guitext " "
+                guibutton    "send map to server"               "sendmap"
+                guibutton   "get map from server"               "getmap"
+                guitext " "
+                guibutton   "take screenshot"                  [ cleargui; sleep 0 [ screenshot ] ]
+            ]
+            guispring
+            guilist [
+                guitext (format "key %1" (searcheditbinds "edittoggle"))
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "showtexgui"))
+                guitext (format "key %1" (searcheditbinds "showgui edit"))
+                guitext (format "key %1" (searcheditbinds "showgui materials"))
+                guitext (format "key %1" (searcheditbinds "saycommand /" 0 "" "" " or "))
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "remip"))
+                guitext (format "key %1" (searcheditbinds [fullbright 0; patchlight]))
+                guitext (format "key %1" (searcheditbinds [fullbright 0; calclight -1]))
+                guitext (format "key %1" (searcheditbinds [fullbright 0; calclight 1]))
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "sendmap"))
+                guitext (format "key %1" (searcheditbinds "getmap"))
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "screenshot"))
+            ]
+            guispring
+        ]
+
+
+
+        guitab file
+        guitext "set and change file properties of the currently loaded map:"
+        guistrut 0.5
+        guilist [
+            guilist [
+                guistrut 0.5
+                guitext    "set map title:"
+                guistrut 0.75
+                guitext    "set map author:"
+                guistrut 0.75
+                guitext    "set map filename:"
+                guistrut 1.25
+                guibutton   "^fs^fgSAVE^fS map with these properties"            "savemap"
+                guistrut 0.5
+                guibutton    "increase mapsize (2x)"                    "mapenlarge"
+                guistrut 0.5
+                guibutton   "start new map"                        "showgui newmapgui"
+            ]
+            guispring
+            guilist [
+                guistrut 0.5
+                guifield                                     maptitle 40 [ maptitle $maptitle ]
+                guistrut 0.5
+                guifield                                      mapauthor 40 [ mapauthor $mapauthor ]
+                guistrut 0.5
+                guifield                                      savemap_name 40 [ savemap $savemap_name ]
+                guistrut 1
+                guitext (format "(shortcut key %1)" (searcheditbinds "savemap"))
+                guistrut 4
+            ]
+            guispring
+        ]
+
+        guitab ents
+        guilist [
+            guilist [
+                guibutton   "find ents"            "showgui entfind"
+                guitext " "
+                guibutton   "link selected ents (teleport, sound, trigger)"   "showgui entlink"
+                guitext " "
+                guibutton   "focus on selected ent"                           "entautoview"
+                guibutton   "deselect all ents"                              "entcancel"
+                guibutton   "edit selected ent"                              "selentedit"
+                guitext " "
+                guibutton   "select all ents inside selection area"               "entselect insel"
+                guibutton   "select all ents matching selected"                  "selentfindall"
+                guitext " "
+                guitext " "
+                guibutton   "create weapon.."                              "showgui createweapon"
+                guibutton   "create playerstart"                           "saycommand [/newent playerstart 0 0 0 0 0]"
+                guibutton   "create light.."                              "resetlight;showgui newlight"
+                guibutton   "create lightfx.."                              "saycommand [/newent lightfx 0 0 0 0 0]"
+                guibutton   "create teleport.."                              "showgui newteleport"
+                guibutton   "create mapmodel"                              "saycommand [/newent mapmodel 0 0 0 0 0]"
+                guibutton   "create sound"                                 "saycommand [/newent sound 0 0 0 0 0]"
+                guibutton   "create pusher"                                 "saycommand [/newent pusher 0 90 200 0 0 0]"
+                guibutton   "create affinity (flag, bomb)"                     "saycommand [/newent affinity 0 0 0 0 0]"
+                guibutton   "create actor"                                 "saycommand [/newent actor 0 0 0 0 0 0 0 0 0]"
+                guibutton   "create trigger"                              "saycommand [/newent trigger 0 0 0 0 0 0 ]"
+                guibutton   "create checkpoint"                              "saycommand [/newent checkpoint 0 0 0 0 0 0 ]"
+            ]
+            guispring
+            guilist [
+                guitext " "
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "entlink"))
+                guitext " "
+                guitext (format "key COMMA %1 + scroll" (searcheditbinds [ domodifier 10; onrelease entautoview ]))
+                guitext (format "key %1" (searcheditbinds "entcancel"))
+                guitext (format "key %1" (searcheditbinds "selentedit"))
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "entselect insel"))
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+            ]
+            guispring
+        ]
+
+        guitab ops
+        guilist [
+            guilist [
+                guibutton   "copy"                                    "copy"
+                guibutton   "paste"                                    "paste"
+                guitext " "
+                guibutton   "mirror geometry per selection orientation"         "flip"
+                guibutton   "delete selected ents / geometry"               "
+                guibutton   "grab texture from selected face"               "gettex"
+                guibutton   "repeat last texture change across entire map"      [allfaces 0; replace]
+                guitext " "
+                guibutton   "toggle heightmap mode"                        "hmapedit 1"
+                guitext " "
+                guibutton   "materials.."                              [showgui materials]
+                guitext " "
+                guibutton   "waypoints.."                              [showgui waypoints]
+                guitext " "
+                guibutton   "undo"                                    "undo"
+                guibutton   "redo"                                    "redo"
+                guitext      "undo cache size (default: 5 MB)"
+         ]
+            
+            guispring
+            guilist [
+                guitext (format "key %1" (searcheditbinds "editcopy"))
+                guitext (format "key %1" (searcheditbinds "editpaste"))
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "editflip"))
+                guitext (format "key %1" (searcheditbinds "editdel"))
+                guitext (format "key %1" (searcheditbinds "gettex"))
+                guitext " "
+                guitext " "
+                guitext (format "key H%1" (searcheditbinds " hmapedit (! $hmapedit); blendpaintmode 0 "))
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "showgui materials"))
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext (format "key Z%1" (searcheditbinds [undo; passthroughsel 0 ]))
+                guitext (format "key %1" (searcheditbinds "redo"))
+                guislider                                          "undomegs" 1 10"
+                guitext " "
+            ]
+            guispring
+        ]
+
+        guitab vtex
+        guilist [
+            guilist [
+                guitext "create new version of the selected face texture:"
+                guitext ""
+                guibutton "color.."                                       "showgui vcolour"
+                guitext " "
+                guibutton "scale.."                                       "showgui vscale"
+                guibutton "rotate / flip.."                                 "showgui vrotate"
+                guibutton "offset texture up or down.."                        "showgui voffset"
+                guitext " "
+                guibutton "texture scrolling.."                              "showgui vscroll"
+                guitext " "
+                guibutton "glow color.."                                 "showgui glowcolor"
+                guibutton "specularity (gloss).."                           "showgui specscale"
+                guibutton "heightmap intensity.."                           "showgui parallaxscale"
+                guitext " "
+                guibutton "texture blend painting.."                        "showgui blendmap"
+                guitext " "
+                guibutton "^foRESET ^fwselected textures to default"            "vreset"
+                guitext " "
+                guibutton "^foRESET ^fwall textures in map"                     "showgui texreset"
+            ]
+        ]
+
+        guitab helpers
+        guilist [
+            guilist [
+                guicheckbox   "toggle edit radar"                  [showeditradar]
+                guicheckbox   "show material volumes"               "showmat"
+                guitext " "
+                guicheckbox   "toggle fullbright"                  "fullbright"
+                guicheckbox   "toggle wireframe"                  "wireframe"
+                guicheckbox   "toggle blank geometry"               "blankgeom"
+                guicheckbox   "toggle outline"                  "outline"
+                guibutton   "set outline color.."               "showgui outlinecolor"
+                guitext " "
+                guicheckbox   "toggle paste grid"                  "showpastegrid"
+                guicheckbox   "toggle cursor grid"               "showcursorgrid"
+                guicheckbox   "toggle selection grid"               "showselgrid"
+                guicheckbox   "snap ents to grid"                  "entselsnap"
+                guitext " "
+                guicheckbox   "toggle allfaces texture mode"         "allfaces"
+                guitext " "
+                guitext    "adjust your float speed (0-1000)"
+                guifield                                   floatspeed 10 [ floatspeed $floatspeed ]
+            ]
+            guispring
+            guilist [
+                guitext   "toggle edit radar"
+                guitext (format "key PLUS%1" (searcheditbinds "showmat")) //?
+                guitext " "
+                guitext (format "key B%1" (searcheditbinds "fullbright")) //?
+                guitext " "
+                guitext " "
+                guitext (format "key EQUALS%1" (searcheditbinds "outline")) //?
+                guitext " "
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "togglegrid"))
+                guitext (format "key %1" (searcheditbinds "togglegrid"))
+                guitext (format "key %1" (searcheditbinds "togglegrid"))
+                guitext " "
+                guitext " "
+                guitext (format "key MINUS%1" (searcheditbinds "allfaces")) //?
+            ]
+            guispring
+        ]
+
+
+        guitab lightmap
+        guilist [
+            guilist [
+                guitext "lightprecision (default: 32)"
+                guitext "^fr(Notice!)^fw Increasing the lightprecision creates a smaller mapfile"
+                guitext " "
+                guitext "lighterror (default: 8)"
+                guitext " "
+                guitext "lightthreads (CPU threads/cores) (default: 0)"
+                guitext " "
+                guibutton "set min light level / shadow color (ambient)..."                   "showgui environment 2"
+                guitext " "
+                guibutton "optimize geometry"                                    "remip"
+                guibutton "patch existing lightmap (patch)"                           [fullbright 0; patchlight]
+                guibutton "quick calculate lights (preview)"                        [fullbright 0; calclight -1]
+                guibutton "full calculate lights (full)"                           [fullbright 0; calclight 1]
+            ]
+            guispring
+            guilist [
+                guilistslider lightprecision "8 16 32 48 64 128 256"
+                guitext " "
+                guitext " "
+                guislider lighterror
+                guitext " "
+                guislider lightthreads
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext (format "key %1" (searcheditbinds "remip"))
+                guitext (format "key %1" (searcheditbinds [fullbright 0; patchlight]))
+                guitext (format "key %1" (searcheditbinds [fullbright 0; calclight -1]))
+                guitext (format "key %1" (searcheditbinds [fullbright 0; calclight 1]))
+            ]
+            guispring
+        ]
+
+        guitab keyref
+        guilist [
+            guilist [
+                guitext "change gridsize"
+                guitext "select"
+                guitext "extend selection / select additional ents"
+                guitext   "move selection area / move selected ents"
+                guitext   "deselect all"
+                guitext   "orient selection face"
+                guitext   "create /destroy cubes"
+                guitext   "push / pull selected faces"
+                guitext   "push / pull corner"
+                guitext "select multiple corners"
+                guitext   "move selected geometry"
+                guitext   "copy"
+                guitext   "paste"
+                guitext   "rotate geometry per selection orientation"
+                guitext   "mirror geometry per selection orientation"
+                guitext   "quick texture selected faces"
+                guitext   "grab texture from selected faces"
+                guitext "change properties of selected ents"
+                guitext   "cycle link modes for selected ents"
+                guitext   "change brush type"
+            ]
+            guispring
+            guilist [
+                guitext (format "hold key %1 + scroll wheel" (searcheditbinds "domodifier 1"))
+                guitext (format "drag left mouse button")
+                guitext (format "right mouse button")
+                guitext (format "drag right mouse button")
+                guitext (format "key %1 " (searcheditbinds cancelsel))
+                guitext (format "right mouse button")
+                guitext (format "scroll mouse wheel")
+                guitext (format "hold key %1 + scroll mouse wheel" (searcheditbinds "domodifier 2"))
+                guitext (format "hold key %1 + scroll wheel" (searcheditbinds "domodifier 3"))
+                guitext (format "drag middle mouse button")
+                guitext (format "hold key %1 + drag right mouse button" (searcheditbinds editcut))
+                guitext (format "key %1" (searcheditbinds editcopy))
+                guitext (format "key %2" (searcheditbinds editpaste))
+                guitext (format "hold key %1 + scroll mouse wheel" (searcheditbinds "domodifier 4"))
+                guitext (format "key %1" (searcheditbinds editflip))
+                guitext (format "hold key %1 + scroll mouse wheel" (searcheditbinds "domodifier 6"))
+                guitext (format "key %1" (searcheditbinds gettex))
+                guitext (format "hold number keys 1-9 + scroll mouse wheel")
+                guitext (format "key %1" (searcheditbinds entlink))
+                guitext (format "hold key %1 + scroll mouse wheel" (searcheditbinds "domodifier 9"))
+            ]
+            guispring
+        ]
+
+        guitab cfg
+        guitext [@[mapname].cfg]
+        showfileeditor [@[mapname].cfg] -80 20
+    ]
+
+
+
+
+
+
+
+
+
+    newgui serverpriv [
+        guistayopen [
+            guiradio   "veto server"               openservertype 0   [serveropen 3]
+            guiradio   "open server"               openservertype 1   [serveropen 1]
+            guiradio   "coop server"               openservertype 2   [serveropen 2]
+            guitext " "
+            guibutton   "claim privileges"            "setpriv 1"
+            guibutton   "relinquish privileges"         "setpriv 0"
+            guitext " "
+            guibutton   "administrator password"      "saycommand [/adminpass ]"
+        ]
+    ]
+
+    newgui waypoints [
+            guicheckbox   "show waypoints"                     "showwaypoints"
+            guicheckbox   "create waypoints"                     "dropwaypoints"
+            guiradio   "gamespeed 100% (default)"               gamespeedtype 0   [gamespeed 100]
+            guiradio   "gamespeed 200% (faster waypointing)"      gamespeedtype 1   [gamespeed 200]
+            guiradio   "gamespeed 75% (slower waypointing)"      gamespeedtype 2   [gamespeed 75]
+            guitext " "
+            guibutton   "add bot"                           "addbot"
+            guibutton   "remove bot"                        "delbot"
+            guitext " "
+            guibutton   "^foSAVE ^fwwaypoints"                  "savewaypoints"
+            guibutton   "^foLOAD ^fwwaypoints"                  "saycommand [/loadwaypoints ]"
+            guibutton   "^foSAVE ^fwmap"                     "savemap"
+            guibutton   "^foCLEAR ^fwwaypoints in selection area"   "saycommand [/delselwaypoints]"
+            guibutton   "^foCLEAR ^fwall waypoints"               "saycommand [/clearwaypoints]"
+    ]
+    newgui createweapon [
+            guilist [
+                guilist [
+                guitext   "^fo/newent weapon ^fy#"
+                guibutton "sword"         "newent weapon 2"
+                guibutton "shotgun"         "newent weapon 3"
+                guibutton "smg"            "newent weapon 4"
+                guibutton "flamer"         "newent weapon 5"
+                guibutton "plasma"         "newent weapon 6"
+                guibutton "taser"         "newent weapon 7"
+                guibutton "rifle"         "newent weapon 8"
+                guibutton "grenade"         "newent weapon 9"
+                guibutton "mine"         "newent weapon 10"
+                guibutton "rocket"         "newent weapon 11"
+                ]
+                guilist [
+                guitext " "
+                guitext "weapon 2"
+                guitext "weapon 3"
+                guitext "weapon 4"
+                guitext "weapon 5"
+                guitext "weapon 6"
+                guitext "weapon 7"
+                guitext "weapon 8"
+                guitext "weapon 9"
+                guitext "weapon 10"
+                ]
+            ]
+    ]
+
+    newgui newmapgui [
+            guibutton    "^foREVERT ^fwto last saved version of @savemap_name"      "map $savemap_name"
+            guitext " "
+            guibutton   "^foOPEN ^fwan existing map"                        "showgui maps 1"
+            guitext " "
+            guibutton    "^foCLEAR MAP ^fwand begin a new one ^fr(Warning!)"         "newmap $newmapsize"
+            guitext      "size of new map"
+            guislider    "newmapsize" 10 16
+    ]
+
+    newgui outlinecolor [
+                guiradio   "white"               outlinetype 1 [outlinecolour 255 255 255]
+                guiradio   "black"               outlinetype 2 [outlinecolour 0 0 0]
+                guiradio   "blue"               outlinetype 3 [outlinecolour 80 80 255]
+                guiradio   "light blue"               outlinetype 4 [outlinecolour 0 127 255]
+                guiradio   "red"               outlinetype 5 [outlinecolour 255 80 80]
+                guiradio   "purple"               outlinetype 6 [outlinecolour 255 80 255]
+                guiradio   "green"               outlinetype 7 [outlinecolour 80 255 80]
+                guiradio   "yellow"            outlinetype 8 [outlinecolour 255 255 80]
+                guitext " "
+                guicheckbox   "toggle outline"      "outline"
+    ]
+
+
+    newgui entlink [
+        guistayopen [
+        guitext "^fo/entlink ^fw cycles link modes of selected ents"
+        guitext " "
+        guibutton    "cycle linkmode +         key L"               "entlink"
+        guitext " "
+        guitext    "link modes:"
+        guitext      "1. A     B"
+        guitext    "2. ^frA >> B"
+        guitext      "3. ^fyA <> B"
+        guitext    "4. ^frA << B"
+        guitext " "
+        guibutton   "create new teleport"                            "saycommand [/newent teleport -1 0 0 0 655 0]"
+        guibutton   "create new sound"                              "saycommand [/newent sound 0 0 0 0 0]"
+        guibutton   "create new trigger"                           "saycommand [/newent trigger 0 0 0 0 0 0 0]"
+        ]
+
+
+    ]
+
+
+    newgui vcolour [
+        guistayopen [
+            guitext "^fo/vcolour ^fr%R ^fg%G ^fb%B"
+            guitext " "
+            guibutton "red"            "saycommand [/vcolour 1.0 0.3 0.3]"
+            guibutton "green"         "saycommand [/vcolour 0.3 1.0 0.3]"
+            guibutton "blue"         "saycommand [/vcolour 0.3 0.3 1.0]"
+            guibutton "orange"         "saycommand [/vcolour 1.0 0.6 0.2]"
+            guibutton "yellow"         "saycommand [/vcolour 1.0 0.8 0.3]"
+            guibutton "purple"         "saycommand [/vcolour 0.5 0.2 0.7]"
+            guibutton "turquoise"      "saycommand [/vcolour 0.1 0.8 0.8]"
+            guibutton "dark"         "saycommand [/vcolour 0.3 0.3 0.3]"
+            guibutton "white"         "saycommand [/vcolour 1.0 1.0 1.0]"
+        ]
+    ]
+
+    newgui vscale [
+        guistayopen [
+        guitext "^fo/vscale ^fy% ^fw(0.125 - 8.0)"
+        guitext " "
+        guibutton "12.5%"      "saycommand [/vscale .125]"
+        guibutton "25%"         "saycommand [/vscale .25]"
+        guibutton "50%"         "saycommand [/vscale .5]"
+        guibutton "75%"         "saycommand [/vscale .75]"
+        guibutton "100%"      "saycommand [/vscale 1]"
+        guibutton "150%"      "saycommand [/vscale 1.5]"
+        guibutton "400%"      "saycommand [/vscale 4]"
+        guibutton "800%"      "saycommand [/vscale 8]"
+        ]
+    ]
+
+    newgui vrotate [
+        guistayopen [
+            guitext "^fo/vrotate ^fy# ^fy(1-3 rotate, 4-5 flip)"
+            guitext " "
+            guibutton "rotation/flip +"               "vdelta vrotate 1"
+            guibutton "rotation/flip -"               "vdelta vrotate -1"
+            guitext " "
+            guibutton "rotate 90"                  "saycommand [/vrotate 1]"
+            guibutton "rotate 180"                  "saycommand [/vrotate 2]"
+            guibutton "rotate 270"                  "saycommand [/vrotate 3]"
+            guitext " "
+            guibutton "flip vertical"               "saycommand [/vrotate 4]"
+            guibutton "flip horizontal"               "saycommand [/vrotate 5]"
+            guitext " "
+            guibutton "^foRESET ^fwto 0"            "saycommand [/vrotate 0]"
+        ]
+    ]
+
+    newgui specscale [
+        guitext "^fo/vdelta vshaderparam specscale ^fr%R ^fg%G ^fb%B"
+        guitext " "
+        guibutton "red gloss"            "saycommand [/vdelta vshaderparam specscale 1.0 0.3 0.3]"
+        guibutton "green gloss"            "saycommand [/vdelta vshaderparam specscale 0.3 1.0 0.3]"
+        guibutton "blue gloss"            "saycommand [/vdelta vshaderparam specscale 0.3 0.3 1.0]"
+        guibutton "orange gloss"         "saycommand [/vdelta vshaderparam specscale 1.0 0.6 0.2]"
+        guibutton "yellow gloss"         "saycommand [/vdelta vshaderparam specscale 1.0 0.8 0.3]"
+        guibutton "purple gloss"         "saycommand [/vdelta vshaderparam specscale 0.5 0.2 0.7]"
+        guibutton "turquoise gloss"         "saycommand [/vdelta vshaderparam specscale 0.1 0.8 0.8]"
+        guibutton "50% gloss (subtle)"      "saycommand [/vdelta vshaderparam specscale 0.5 0.5 0.5]"
+        guibutton "no gloss (matte)"      "saycommand [/vdelta vshaderparam specscale 0.0 0.0 0.0]"
+        guitext " "
+        guibutton "^foRESET ^fwto 100%"      "saycommand [/vdelta vshaderparam specscale 1.0 1.0 1.0]"
+    ]
+
+    newgui glowcolor [
+        guitext "^fo/vdelta vshaderparam glowcolor ^fr%R ^fg%G ^fb%B"
+        guitext " "
+        guibutton "red"                  "saycommand [/vdelta vshaderparam glowcolor 1.0 0.3 0.3]"
+        guibutton "green"               "saycommand [/vdelta vshaderparam glowcolor 0.3 1.0 0.3]"
+        guibutton "blue"               "saycommand [/vdelta vshaderparam glowcolor 0.3 0.3 1.0]"
+        guibutton "orange"               "saycommand [/vdelta vshaderparam glowcolor 1.0 0.6 0.2]"
+        guibutton "yellow"               "saycommand [/vdelta vshaderparam glowcolor 1.0 0.8 0.3]"
+        guibutton "purple"               "saycommand [/vdelta vshaderparam glowcolor 0.5 0.2 0.7]"
+        guibutton "turquoise"            "saycommand [/vdelta vshaderparam glowcolor 0.1 0.8 0.8]"
+        guitext " "
+        guibutton "^foRESET ^fwto 100%"      "saycommand [/vdelta vshaderparam glowcolor 1.0 1.0 1.0]"
+    ]
+
+    newgui parallaxscale [
+        guitext "^fo/vdelta vshaderparam parallaxscale ^fr%R ^fg%G ^fb%B"
+        guitext " "
+        guibutton "flat"                        "saycommand [/vdelta vshaderparam parallaxscale 0.0 0.0 0.0]"
+        guibutton "extreme"                        "saycommand [/vdelta vshaderparam parallaxscale 0.0025 0.0025 0.0025]"
+        guibutton "another dimension"               "saycommand [/vdelta vshaderparam parallaxscale 1.0 1.0 1.0]"
+        guitext " "
+        guibutton "^foRESET ^fwto default"            "saycommand [/vdelta vshaderparam parallaxscale 0.0000025 0.0000025 0.0000025]"
+    ]
+
+    newgui vscroll [
+        guistayopen [
+            guitext    "^fo/vscroll ^fyX Y"
+            guitext " "
+            guibutton    "scroll Y +"         "vdelta vscroll 0 +0.1"
+            guibutton    "scroll Y -"         "vdelta vscroll 0 -0.1"
+            guitext " "
+            guibutton    "scroll X +"         "vdelta vscroll +0.1 0"
+            guibutton    "scroll X -"         "vdelta vscroll -0.1 0"
+            guitext " "
+            guibutton    "^foRESET ^fwto 0"         "saycommand [/vscroll 0 0]"
+        ]
+    ]
+
+    newgui voffset [
+        guistayopen [
+            guitext "^fo/voffset ^fyXpx Ypx"
+            guitext " "
+            guibutton    "offset Y +"         "vdelta voffset 0 +16"
+            guibutton    "offset Y -"         "vdelta voffset 0 -16"
+            guitext " "
+            guibutton    "offset X +"         "vdelta voffset +16 0"
+            guibutton    "offset X -"         "vdelta voffset -16 0"
+            guitext " "
+            guibutton    "^foRESET ^fwto 0"         "saycommand [/voffset 0 0]"
+        ]
+    ]
+
+    newgui blendmap [
+        guilist [
+            guilist [
+                guilist [
+                    guilist [
+                        guistayopen [
+                            guitext "assign a blend layer to selected texture:"
+                            guitext "^fo/vlayer ^fy# ^fw(slot number)"
+                            guitext " "
+                            guilist [
+                                guistrut 0
+                                guitext "texture slot number: "
+                                guifield slotnum -8
+                            ]
+                            guibutton "^foADD ^fwabove brush" [vlayer $slotnum]
+                            guibutton "^foREMOVE ^fwabove brush" [vlayer ""]
+                        ]
+                    ]
+                ]
+                guibutton "open textures menu.." [showtexgui]
+                guitext " "
+                guistayopen [
+                    guibutton "cycle blend paint modes" [
+                        hmapedit 0
+                        blendpaintmode (? (= $blendpaintmode 5) 0 (+ $blendpaintmode 1))
+                        echo (concat "^fgblend mode:^fw" (at $paintmodes $blendpaintmode))
+                    ]
+                    guibutton "blend mode off" [blendpaintmode 0]
+                    guilist [
+                        guibutton "cycle paint brush: " [nextblendbrush -1]
+                        guitext (getblendbrushname (curblendbrush))
+                    ]
+                    guitext " "
+                    guibutton "generate preview" showblendmap
+                    guitext " "
+                    guibutton "^foCLEAR ^fwBlendmap" clearblendmap
+                ]
+            ]
+            guilist [
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext " "
+                guitext "   key F2"
+                guitext " "
+                guitext "   key P"
+                guitext   "   key Alt"
+                guitext   "    scroll mouse wheel"
+                guitext " "
+                guitext " "
+                guitext " "
+            ]
+        ]
+    ]
+
+    newgui texreset [
+        guilist [
+            guilist [
+            guibutton "^foCLEAR ^fwall unused textures from map cfg" [saycommand /texturecull]
+            guibutton "^foCLEAR ^fwall textures from map cfg" [saycommand /texturereset]
+            guibutton "^foLOAD ^fwdefault texture package to map cfg (reload map required)" [saycommand /exec data/textures.cfg]
+            ]
+        ]
+    ]
+
+    newgui environment [
+        guistayopen [
+        guitext         "quick global lighting:"
+        guitext " "
+        guibutton      "night"                              [ambient 40 40 40; skylight 70 80 90; skybox skyboxes/stars; skycolour 255 255 255; cloudlayer skyboxes/clouds01; cloudlayercolour 50 60 100; cloudlayerblend 0.3; cloudscrollx 0.005; fogcolour 12 10 13; fog 2000; calclight -1]
+        guibutton      "morning"                           [ambient 60 35 40; skylight 170 140 145; skybox penguins/harmony; skycolour 255 160 180; cloudlayer skyboxes/clouds03; cloudlayercolour 255 125 170; cloudlayerblend 0.2; cloudscrollx 0.01; fogcolour 129 79 59; fog 2000; calclight -1]
+        guibutton      "evening"                           [ambient 50 30 10; skylight 180 100 70; skybox skyboxes/sunsetflat; skycolour 255 255 255; cloudlayer skyboxes/clouds01; cloudlayercolour 255 100 50; cloudlayerblend 0.2; cloudscrollx 0.005; fogcolour 40 20 5; fog 2000; calclight -1]
+        guitext " "
+        guibutton      "^foRESET ^fwenvironment to default"      [ambient 0 0 0; skylight 0 0 0; skybox ""; skycolour 255 255 255; cloudlayer ""; cloudlayercolour 255 255 255; cloudlayerblend 1.0; cloudscrollx 0; fogcolour 128 153 179; fog 4000; calclight -1]
+
+        guitab ambient
+        guitext         "^fo/ambient ^frR ^fgG ^fbB"
+        guibutton      "grey shadows"                        "saycommand [/ambient 40 40 40]"
+        guibutton      "purple shadows"                     "saycommand [/ambient 60 35 40]"
+        guibutton      "brown shadows"                        "saycommand [/ambient 50 30 10]"
+        guitext " "
+        guibutton      "^foRESET ^fwambient to default"         "saycommand [/ambient 0 0 0]"
+
+        guitab skylight
+        guitext         "^fo/skylight ^frR ^fgG ^fbB"
+        guibutton      "cool skylight"                        "saycommand [/skylight 70 80 90]"
+        guibutton      "soft skylight"                        "saycommand [/skylight 170 140 145]"
+        guibutton      "warm skylight"                        "saycommand [/skylight 180 100 70]"
+        guitext " "
+        guibutton      "^foRESET ^fwskylight to default"         "saycommand [/skylight 0 0 0]"
+
+
+        guitab fog
+            guitext    "^fo/fog ^fy#"
+            guibutton    "fog distance far"            "saycommand [/fog 1500]"
+            guibutton    "fog distance distant"         "saycommand [/fog 800]"
+            guibutton    "fog distance near"            "saycommand [/fog 300]"
+            guibutton    "^foRESET ^fwfog distance"      "saycommand [/fog 4000]"
+            guitext " "
+            guitext " "
+            guitext    "^fo/fogcolour ^frR ^fgG ^fbB"
+            guibutton    "black fog (darkness)"            "saycommand [/fogcolour 0 0 0]"
+            guibutton    "grey fog (haze)"               "saycommand [/fogcolour 50 50 50]"
+            guibutton    "green fog (gas)"               "saycommand [/fogcolour 40 60 45]"
+            guibutton    "^foRESET ^fwfog color to default"   "saycommand [/fogcolour 128 153 179]"
+
+        guitab skybox
+            guitext    "^fo/skybox ^fyfolder/filename"
+            guibutton    "set skybox to skyhigh"         "saycommand [/skybox skyboxes/skyhigh]"
+            guibutton    "set skybox to sunsetflat"      "saycommand [/skybox skyboxes/sunsetflat]"
+            guibutton   "set skybox to gradient"      "saycommand [/skybox skyboxes/gradient]"
+            guibutton    "^foREMOVE ^fwskybox"           [saycommand "/skybox ^"^""]
+            guitext " "
+            guitext " "
+            guitext      "^fo/skycolour ^frR ^fgG ^fbB"
+            guibutton   "set sky color to orange"      "saycommand [/skycolour 255 150 20]"
+            guibutton   "set sky color to yellow"      "saycommand [/skycolour 150 150 80]"
+            guibutton   "set sky color to red"         "saycommand [/skycolour 180 80 80]"
+            guibutton   "set sky color to blue"         "saycommand [/skycolour 80 80 180]"
+            guibutton   "set sky color to black"      "saycommand [/skycolour 80 80 80]"
+            guibutton    "^foRESET ^fwsky color"         "saycommand [/skycolour 255 255 255]"
+
+        guitab clouds
+            guitext    "^fo/cloudlayer ^fyfolder/filename"
+            guibutton    "set clouds to clouds01"         "saycommand [/cloudlayer skyboxes/clouds01]"
+            guibutton    "set clouds to clouds02"         "saycommand [/cloudlayer skyboxes/clouds02]"
+            guibutton    "set clouds to clouds03"         "saycommand [/cloudlayer skyboxes/clouds03]"
+            guibutton    "^foREMOVE ^fwcloudlayer"          [saycommand "/cloudlayer ^"^""]
+            guitext " "
+            guitext " "
+            guitext      "^fo/cloudlayercolour ^frR ^fgG ^fbB"
+            guibutton   "set cloud color to orange"      "saycommand [/cloudlayercolour 255 150 20]"
+            guibutton   "set cloud color to yellow"      "saycommand [/cloudlayercolour 150 150 80]"
+            guibutton   "set cloud color to red"      "saycommand [/cloudlayercolour 180 80 80]"
+            guibutton   "set cloud color to blue"      "saycommand [/cloudlayercolour 80 80 180]"
+            guibutton   "set cloud color to black"      "saycommand [/cloudlayercolour 80 80 80]"
+            guibutton    "^foRESET ^fwcloud color"      "saycommand [/skycolour 255 255 255]"
+
+            guitab vars
+            guitext   "^fo/gravity ^fy%"
+            guibutton   "none"                        "saycommand   [/gravity 5.0]"
+            guibutton   "low"                        "saycommand [/gravity 15.0]"
+            guibutton   "default"                     "saycommand [/gravity 50.0]"
+            guitext " "
+
+            guitext "^fo/spawnweapon ^fy#"
+            guilist [
+                guilist [
+                guibutton "sword"         "saycommand   [/spawnweapon 2]"
+                guibutton "shotgun"         "saycommand   [/spawnweapon 3]"
+                guibutton "smg"            "saycommand   [/spawnweapon 4]"
+                guibutton "flamer"         "saycommand   [/spawnweapon 5]"
+                guibutton "taser"           "saycommand   [/spawnweapon 6]"
+                ]
+
+                guilist [
+                guibutton "         plasma"             "saycommand   [/spawnweapon 7]"
+                guibutton "         rifle"            "saycommand   [/spawnweapon 8]"
+                guibutton "         grenade"         "saycommand   [/spawnweapon 9]"
+                guibutton "         mine"            "saycommand   [/spawnweapon 10]"
+                guibutton "         rocket"            "saycommand   [/spawnweapon 11]"
+                ]
+            ]
+                guitext " "
+                guitext   "^foplayer vars"
+                guibutton "maximum inventory (default 2)"      "saycommand [/maxcarry 9]"
+                guibutton "jumpspeed (default 100)"          "saycommand [/jumpspeed 200]"
+                guibutton "impulse freestyle (default 1)"      "saycommand [/impulsestyle 3]"
+
+        guibutton "show all variables.."         "showgui vars"
+
+        ]
+    ]
+    resetlight = [
+        lightcolour = 0
+        lightrgb = "255 255 255"
+        lightbright = 1
+        lightradius = 128
+    ]
+
+    lightcmd = [
+        lightr = (at $lightrgb 0)
+        lightg = (at $lightrgb 1)
+        lightb = (at $lightrgb 2)
+        if (! $lightbright) [
+            if (=s $lightrgb "255 255 255") [
+                lightr = 192; lightg = 192; lightb = 192
+            ] [
+                lightr = (div $lightr 2); lightg = (div $lightg 2); lightb = (div $lightb 2)
+            ]
+        ]
+        result (concat newent light $lightradius $lightr $lightg $lightb)
+    ]
+
+    newgui newlight [
+        guibutton "create a sunlight entity          " "saycommand [/newent sunlight 45 320 100 70 50]"
+        guitext " "
+        guitext "light radius:"
+        guislider lightradius 0 800
+        guiradio "white"                         lightcolour 0 [lightrgb = "150 150 150"]
+        guiradio "blue"                        lightcolour 1 [lightrgb = "80 80 180"]
+        guiradio "red"                         lightcolour 2 [lightrgb = "180 80 80"]
+        guiradio "green"                       lightcolour 3 [lightrgb = "80 180 80"]
+        guiradio "yellow"                      lightcolour 4 [lightrgb = "150 150 80"]
+        guiradio "purple"                      lightcolour 5 [lightrgb = "150 80 150"]
+        guiradio "turquoise"                   lightcolour 6 [lightrgb = "80 150 150"]
+        guitext " "
+        guicheckbox "bright"                   lightbright
+        guitext " "
+        guibutton "^foCREATE" (lightcmd)
+        guibutton (lightcmd)
+    ]
+
+
+    newgui newteleport [
+       guitext "^fo/newent teleport ^fwlink teleports with ^fo/entlink ^fw(key L)"
+        guitext " "
+       guibutton "create a new teleport"          "saycommand [/newent teleport -1 0 0 0 655 0]"
+    ]
+
+    quickeditmenu = [
+        guitext "Quick Commands:"
+        guibar
+        guifield  savemap_name 10 [ savemap $savemap_name ]
+        guibutton quicklight          "calclight -1"
+        guibutton "optimize map"      "remip"
+        guibutton "new entity"        "newent light"
+        guibar
+        guibutton newmap
+        guibar
+        guibutton help "showgui edit"
+    ]
+
+    matmenu = [
+        guicheckbox   "show material volumes"               "showmat"
+        guitext " "
+        guibutton   "air                     key Numpad 1"               "editmat air"
+        guibutton   "alpha                  key Numpad 2"                  "editmat alpha"
+        guibutton   "water                  key Numpad 3"                  "editmat water"
+        guibutton   "lava                     key Numpad 4"               "editmat lava"
+        guibutton   "clip                     key Numpad 5"               "editmat clip"
+        guibutton   "noclip                  key Numpad 6"                  "editmat noclip"
+        guibutton   "aiclip                  key Numpad 7"                  "editmat aiclip"
+        guibutton   "death                  key Numpad 8"                  "editmat death"
+        guibutton   "ladder                  key Numpad 9"                  "editmat ladder"
+
+    ]
+
+    newentgui = [
+        genentattributes [@arg1] [@arg2] [@arg3]
+        newgui $arg1 [
+            guitext $tmpt
+            guibar
+            @entattributes
+            guitab type
+            guilistsplit n 2 $enttypelist [
+                guibutton $n [ entset @n ]
+            ]
+        ]
+    ]
+
+    looplist i $enttypelist [
+        newentgui $i "" ""
+    ]
+
+    newgui materials [
+        @matmenu
+
+    ]
+
+    newgui quickedit [
+        @quickeditmenu
+        guitab materials
+        @matmenu
+    ]
+
+    newentgui light "radius red green blue" "0 400 0 255 0 255 0 255"
+    newentgui spotlight "radius" "0 200"
+    newentgui playerstart "yaw team id" "0 360"
+    newentgui teleport "tag" "0 20"
+    newentgui teledest "yaw tag" "0 360 0 20"
+    newentgui monster "yaw type" "0 360 0 7"
+    newentgui mapmodel "model yaw type tag flags" "360 0 0 100 0 29 0 20"
+    newentgui envmap "radius" "0 400"
+    newentgui pusher "Z Y X" "0 200 0 200 0 200"
+    newentgui carrot "tag type" "0 50 0 29"
+    newentgui sound "type radius size" "0 20 0 500 0 500"
+    newentgui particles "type" "0 3"
+
+    alias showmapmodel [
+        mapmodelpath = (mapmodelindex $arg1)
+        mapmodelshot = (concatword "models/" $mapmodelpath "/thumb")
+        mapmodelcmd = (concat newent mapmodel $arg1)
+        guilistx 2 [
+            guiimage $mapmodelshot $mapmodelcmd 2 1 "textures/nothumb"
+            guibutton $mapmodelpath $mapmodelcmd
+        ]
+    ]
+
+    newgui mapmodels [
+        mapmodelnum = (mapmodelindex)
+        guilistloop 4 2 $mapmodelnum [ showmapmodel $i ]
+    ]
diff --git a/config/menus/game.cfg b/config/menus/game.cfg
new file mode 100644
index 0000000..d6a13f9
--- /dev/null
+++ b/config/menus/game.cfg
@@ -0,0 +1,42 @@
+newgui team [
+    guione [
+        guifont "default" [ guicenter [ guitext "choose team" ] ]
+        guistrut 1
+        if (isspectator (getclientnum)) [
+            guicenter [ guibutton (format "^fw^f(%1) unspectate" $playertex) [spectator 0] ]
+        ] [
+            guicenter [ guibutton (format "^fw^f(%1) spectate" $spectatortex) [spectator 1] ]
+        ]
+        if (&& [>= (gamemode) $modeidxstart] [! (& (mutators) $mutsbitcoop)] (|| [! (& (mutators) $mutsbitffa)] [& (mutators) $mutsbitmulti])) [
+            guistrut 1
+            loop t (? (& (mutators) $mutsbitmulti) 4 2) [
+                push t (at "alpha omega kappa sigma" $t) [
+                    guicenter [ guibutton (format "^f[%1]^f(%2) %3" $[team@[t]colour] $[team@[t]tex] $[team@[t]name]) [team @t] ]
+                ]
+            ]
+        ]
+        guistrut 1
+        guifont "default" [ guicenter [ guibutton "^focancel" [cleargui 1] [] "action" ] ]
+    ]
+    guitip (format "press %1 to open this menu at any time" (dobindsearch "showgui team"))
+]
+
+clientguitarget = -1
+newgui client [
+    clempty = (! (getclientpresence $clientguitarget))
+    clname = (? $clempty "empty" (getclientname $clientguitarget))
+    clcname = (? $clempty "empty" (getclientname $clientguitarget 1))
+    clmodel = (? $clempty 0 (getclientmodel $clientguitarget))
+    clcolour = (? $clempty 0 (getclientcolour $clientguitarget))
+    clteam = (? $clempty 0 (getclientteam $clientguitarget))
+    clweap = (? $clempty 0 (getclientweapselect $clientguitarget))
+    clvanity = (? $clempty "" (getclientvanity $clientguitarget))
+    cltab = (format "\f[%1]%2" (? $clempty 0x111111 $clcolour) $clientguitarget)
+    guibox [guiplayerpreview $clmodel $clcolour $clteam $clweap $clvanity [cleargui 1] 7.5 1 1] [
+        guicenter [ guifont "emphasis" [ guitext "player name" ] ]
+        guicenter [ guitext $clcname ]
+        //if (! $clempty) [
+        guistrut 1
+        guicenter [ guitext "quin got bored at this point" ]
+    ]
+] [ clientguitarget = $scoretarget ]
\ No newline at end of file
diff --git a/config/menus/glue.cfg b/config/menus/glue.cfg
new file mode 100644
index 0000000..b665617
--- /dev/null
+++ b/config/menus/glue.cfg
@@ -0,0 +1,213 @@
+vardef =  [guitext $arg2 ; [@[arg1]val] = $$arg1]
+varhdef = [guitext $arg2 ; [@[arg1]val] = (hexcolour $$arg1)]
+varfld =  [varname = [@[arg1]val] ; guifield $varname $arg2 [@arg1 $@varname]]
+varbit =  [varname = [@[arg1]val] ; guibitfield "" $arg2 [@arg1 $@varname]]
+
+vardef2 = [
+    guitext $arg2
+    [@[arg1]val1] = $[@[arg1]1]
+    [@[arg1]val2] = $[@[arg1]2]
+]
+varhdef2 = [
+    guitext $arg2
+    [@[arg1]val1] = (hexcolour $[@[arg1]1])
+    [@[arg1]val2] = (hexcolour $[@[arg1]2])
+]
+varfld2 = [guilist [
+    varname1 = [@[arg1]val1]
+    guifield $varname1 $arg2 [[@@[arg1]1] $@varname1]
+    varname2 = [@[arg1]val2]
+    guifield $varname2 $arg2 [[@@[arg1]2] $@varname2]
+]]
+varbit2 = [guilist [
+    varname1 = [@[arg1]val1]
+    guibitfield "" $varname1 $arg2 [[@@[arg1]1] $@varname1]
+    varname2 = [@[arg1]val2]
+    guibitfield "" $varname2 $arg2 [[@@[arg1]2] $@varname2]
+]]
+varincr = [guilist [
+    varname1 = [@[arg1]val1]
+    guibutton "^fy-" [[@@[arg1]1] (- $@varname1 1)]
+    guifield $varname1 $arg2 [[@@[arg1]1] $@varname1]
+    guibutton "^fg+" [[@@[arg1]1] (+ $@varname1 1)]
+    varname2 = [@[arg1]val2]
+    guifield $varname2 $arg2 [[@@[arg1]2] $@varname2]
+]]
+
+guilistx = [
+    if (> $arg1 0) [ guilist [ guilistx (- $arg1 1) $arg2 ] ] [ @arg2 ]
+]
+
+guione = [
+    guispring 1
+    guilist [
+        guispring 1
+        guilist [
+            //guistrut 80 1
+            arg1
+        ]
+        guispring 1
+    ]
+    guispring 1
+]
+
+guibox = [
+    guispring 1
+    guilist [
+        guilist [
+            guispring 1
+            guilist [
+                guispring 1
+                arg1
+                guispring 1
+            ]
+            guispring 1
+        ]
+        guistrut 2
+        guilist [
+            //guistrut 50 1
+            guispring 1
+            arg2
+            guispring 1
+            arg3
+        ]
+    ]
+    guispring 1
+]
+
+guimerge = [ guibody [guilist [guilist [guistrut $arg1]; guilist [arg2]]] $arg3 $arg4 $arg5 ]
+guiarea = [
+    guilist [
+        if (> $numargs 9) arg10
+        guilist [
+            [@[arg1]count] = $arg2
+            [@[arg1]disp] = 0
+            [@[arg1]list] = 0
+            guilist [
+                guistrut 0.5
+                guistrut $arg3 1
+                if (arg5) [
+                    [@[arg1]num] = $arg6
+                    if (> $[@[arg1]num] 0) [
+                        [@[arg1]index] = (min (max 0 (- $[@[arg1]num] $[@[arg1]count])) $[@[arg1]index]) //safeguard
+                        loop i $[@[arg1]num] [
+                            arg7
+                            if (arg8) [
+                                if (&& (>= $i $[@[arg1]index]) (< $[@[arg1]list] $[@[arg1]count])) [
+                                    if (> $numargs 10) arg11
+                                    [@[arg1]list] = (+ $[@[arg1]list] 1)
+                                ]
+                                [@[arg1]disp] = (+ $[@[arg1]disp] 1)
+                            ]
+                        ]
+                    ] [guistrut (-f (*f $arg4 $arg2) 1); arg9; [@[arg1]list] = $arg2]
+                ] [guistrut (-f (*f $arg4 $arg2) 1); arg9; [@[arg1]list] = $arg2]
+                [@[arg1]alts] = (- $[@[arg1]count] $[@[arg1]list])
+                if (> $[@[arg1]alts] 0) [guistrut (*f $[@[arg1]alts] $arg4)]
+            ]
+            guistrut 2
+            guislider [@[arg1]index] 0 (max (- $[@[arg1]disp] $[@[arg1]count]) 0) [] 1 1
+        ]
+        arg12
+    ]
+]
+guipage = [ guistayopen [guiarea $arg1 $arg2 $arg3 $arg4 $arg5 $arg6 $arg7 $arg8 $arg9 $arg10 $arg11 $arg12] ]
+guicontainer = [ guilist [ guilist [ if (arg1) [arg2] [arg3] ] ] ]
+guiright = [ guilist [ guispring 1; arg1 ] ]
+guicenter = [ guilist [ guispring 1; arg1; guispring 1 ] ]
+guicenterz = [ guilist [ guispring 1; guilist [ arg1 ]; guispring 1 ] ]
+guicenterx = [ guispring 1; arg1; guispring 1 ]
+
+guitip = [ guistatus (format "TIP: ^fa%1" (? (> $numargs 0) [@arg1] (showtip))) ]
+
+guilistsplit = [
+    guilist [
+        gli = 0
+        gll = (listlen $arg3)
+        glz = (div (+ $gll (- $arg2 1)) $arg2)
+        loop gla $arg2 [
+            guilist [
+                glt = (min (+ $gli $glz) $gll)
+                while [< $gli $glt] [
+                    $arg1 = (at $arg3 $gli)
+                    arg4
+                    gli = (+ $gli 1)
+                ]
+            ]
+            if (< (+ $gla 1) $arg2) [arg5]
+        ]
+    ]
+]
+
+guiloopsplit = [
+    guilist [
+        gli = 0
+        glz = (div (+ $arg3 (- $arg2 1)) $arg2)
+        loop gla $arg2 [
+            guilist [
+                glt = (min (+ $gli $glz) $arg3)
+                while [< $gli $glt] [
+                    $arg1 = $gli
+                    arg4
+                    gli = (+ $gli 1)
+                ]
+            ]
+            if (< (+ $gla 1) $arg2) [arg5]
+        ]
+    ]
+]
+
+guilistloop = [
+    i = 0
+    j = (+ (div $arg3 (* $arg1 $arg2)) 1)
+    loop a $j [
+        if (> $a 0) [ guitab $a ]
+        guilist [
+            loop b $arg1 [
+                guilist [
+                    loop c $arg2 [
+                        if (< $i $arg3) [
+                            arg4
+                            i = (+ $i 1)
+                            if (< $c (- $arg2 1)) [ guibar ]
+                        ]
+                    ]
+                ]
+                if (&& (< $i $arg3) (< $b (- $arg1 1))) [ guibar ]
+            ]
+        ]
+    ]
+]
+
+guicheckbox2 = [
+    guistayopen [ guibutton $arg1 [case $@arg2 0 [@@arg2 1] 1 (? @arg3 [@@arg2 2] [@@arg2 0]) 2 [@@arg2 0]] [] (case $$arg2 0 [result "checkbox"] 1 [result "checkboxon"] 2 [result "checkboxtwo"]) ]
+]
+
+guiradio2 = [
+    guistayopen [ guibutton $arg1 [@arg2 @arg3] [] (? (= $$arg2 $arg3) "radioboxon" "radiobox") ]
+]
+
+savewarncmd = ""
+newgui savewarn [
+    guifont "super" [ guicenter [ guitext "confirm action" ] ]
+    guifont "default" [ guicenter [ guitext (format "^"^fs^fa%1^fS^"" $savewarncmd) ] ]
+    guistrut 1
+    guicenter [ guitext "the current map has unsaved changes" ]
+    guicenter [ guitext "would you like to save it now?" ]
+    guistrut 1
+    guilist [
+        guifont "emphasis" [
+            guispring 1
+            guibutton "^fgyes" [savemap; @@savewarncmd]
+            guispring 1
+            guibutton "^fono" [@@savewarncmd]
+            guispring 1
+            guibutton "^frabort" [cleargui 1]
+            guispring 1
+        ]
+    ]
+]
+
+savewarnchk = [
+    if (&& (isconnected) (= (gamemode) $modeidxediting) $totalundos) [ savewarncmd = $arg1; showgui savewarn ] [ arg1 ]
+]
diff --git a/config/menus/help.cfg b/config/menus/help.cfg
new file mode 100644
index 0000000..8fd1b4e
--- /dev/null
+++ b/config/menus/help.cfg
@@ -0,0 +1,1062 @@
+newgui help [
+    guilist [
+        if ($isconnected) [
+            guilist [
+                guione [
+                    guicenter [
+                        guifont "emphasis" [
+                            guitext "Current ^fs^fcmode^fS"
+                            if ($mutators) [
+                                guitext " and ^fs^fymutators^fS:"
+                            ] [
+                                guitext ":"
+                            ]
+                        ]
+                    ]
+                    guilist [
+                        guispring
+                        looplist i (modetexlist $gamemode $mutators 1) [
+                            guinohitfx [ guiimage $i [showgui (substring @i 21)] 2 ]
+                        ]
+                        guispring
+                    ]
+                    guicenter [ guitext ( concatword  "^fc" (modedesc $gamemode 1 3) ) ]
+                    loop i $mutsidxnum [
+                        if (& (<< 1 $i) $mutators) [  
+                            guicenter [ guitext ( concatword  "^fy" (mutsdesc $gamemode (<< 1 $i) 3) ) ]
+                        ]
+                    ]
+                    if (= $gamemode $modeidxrace) [
+                            guistrut 0.5
+                            guicenter [
+                                guibutton "indicate route: " [saycommand "/routecolour "] [] "" $routecolour
+                                guiradio "off " routeid -1
+                                guiradio "basic " routeid 0
+                                guiradio "advanced " routeid 1
+
+                            ]
+                    ]
+                ]
+            ]
+            guistrut 0.5
+            guibar
+            guistrut 0.5
+        ]
+        guione [
+            guilist [
+                guifont "emphasis" [
+                    guicenter [ guibutton "about" "showgui about" ]
+                    guicenter [ guibutton "accounts" "showgui accounts" ]
+                    guicenter [ guibutton "live support" "showgui support" ]
+                    guicenter [ guibutton "modes & mutators" "showgui modes-mutators" ]
+                    guicenter [ guibutton "parkour" "showgui parkour" ]
+                    guicenter [ guibutton "rules" "showgui rules" ]
+                    guicenter [ guibutton "scoring" "showgui scoring" ]
+                ]
+            ]
+        ]
+    ]
+    guivisible [ guitip (format "press %1 to open this menu at any time" (dobindsearch "showgui help")) ]
+]
+
+newgui about [
+    guione [
+        guicenter [ guitext "Red Eclipse Engine" ]
+        guifont "little" [
+            guicenter [ guitext "(C) 2010-2015 Quinton Reeves, Lee Salzman" ]
+            guicenter [ guitext "^fs^fghttp://www.redeclipse.net/^fS" ]
+        ]
+        guistrut 1
+        guicenter [ guitext "Red Eclipse Data" ]
+        guifont "little" [
+            guicenter [ guitext "(C) 2010-2015 Red Eclipse Team" ]
+            guicenter [ guitext "^fs^fghttp://www.redeclipse.net/^fS" ]
+        ]
+        guistrut 1
+        guicenter [ guitext "Based on Cube Engine 2" ]
+        guifont "little" [
+            guicenter [ guitext "(C) 2001-2015 Wouter van Oortmerssen, Lee Salzman" ]
+            guicenter [ guitext "Mike Dysart, Robert Pointon, and Quinton Reeves" ]
+            guicenter [ guitext "^fs^fghttp://sauerbraten.org/^fS" ]
+        ]
+    ]
+]
+
+livesupport = 0
+newgui support [
+    guiheader "live support"
+    if $livesupport [
+        ircgui freenode
+    ] [
+        guifont "emphasis" [ guicenter [ guitext "ONLINE LIVE SUPPORT" ] ]
+        guistrut 0.5
+        guicenter [ guitext "You are about to enter the main project chat room." ]
+        guistrut 0.25
+        guifont "little" [
+            guicenter [ guitext "From there, you can ask questions or talk to the community." ]
+            guicenter [ guitext "Please be patient, as you may not get a response immediately." ]
+        ]
+        guistrut 0.5
+        guicenter [ guitext "Are you sure you wish to continue?" ]
+        guistrut 0.5
+        guicenter [ guistayopen [
+            guibutton "^fgYES, continue" [livesupport = 1; ircaddclient freenode irc.freenode.net 6667 (getplayername); ircaddchan freenode #redeclipse ]
+            guispring 1
+            guibutton "^foNO, go back" [cleargui 1]
+        ] ]
+    ]
+]
+
+newgui modes-mutators [
+    guiheader "modes & mutators"
+    guifont "emphasis" [ guicenter [ guitext "GAME MODES & MODE MUTATORS" ] ]
+    guistrut 0.5
+    guicenter [ guitext "Red Eclipse has a variety of different game modes to choose from," ]
+    guicenter [ guitext "with mutators to further tweak the experience." ]
+    guistrut 0.25
+    guifont "little" [
+        guicenter [ guitext "(select a ^fs^fcmode^fS or ^fs^fymutator^fS to learn more)" ]
+    ]
+    guistrut 1
+    guicenter [
+        guilist [
+            guilist [
+                guilist [
+                    guicenter [ guifont "emphasis" [ guitext "Modes" ] ]
+                    guibar
+                    loop i $modeidxnum [
+                        guicenter [ guibutton (at $modename $i) [showgui (at $modeidxname @i) 1] [] "" 0x00FFFF ]
+                    ]
+                ]
+                guistrut 0.5
+                guibar
+                guistrut 0.5
+                guilist [
+                    guilist [
+                        guistrut 2
+                        guicenter [ guifont "emphasis" [ guitext "Mutators" ] ]
+                        guistrut 2
+                    ]
+                    guibar
+                    looplist i $mutsidxname [
+                        guicenter [ guibutton $i [showgui @i 1] [] "" 0xFFFF00 ]
+                    ]
+                ]
+            ]
+        ]
+    ]
+]
+
+newgui demo [
+    guiheader "demo mode"
+    guifont "emphasis" [ guicenter [ guitext "DEMO MODE" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/spectator" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Spectate a recorded match from any player's perspective. Speed up or slow down the action," ]
+    guicenter [ guitext "and view every key that any player presses to learn new strategy and see new opportunities!" ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & SHORTCUTS" ] ]
+    guistrut 0.25
+    guitext "By default, public game servers record a demo file for every new match as well as store the demo files of the last ten matches. These files can be listed by entering the command ^fs^fg/listdemos^fS into the console, showing a numbered list of files including game modes and timestamps." point -1 -1 2100
+    guistrut 0.25
+    guitext "Demo files can be downloaded and saved locally by entering the command ^fs^fg/getdemo <number>^fS. A demo file of a match that has just ended becomes available at the end of intermission, when voting starts." point -1 -1 2100
+    guistrut 0.25
+    guitext "If no number is specified using ^fs^fg/getdemo^fS, the last recorded demo from the server IP address will be fetched. If multiple servers share the same IP address (but use different ports), they should be set up with different home folders to avoid overlap and confusion." point -1 -1 2100
+    guistrut 0.25
+    guitext "The commands to save demo files when playing offline are identical to when playing online." point -1 -1 2100
+    guistrut 0.25
+    guitext "When viewing a demo, game speed can be changed with the variable ^fs^fg/sv_gamespeed^fS. Set up a key binding with this to quickly slow down or speed up the action during demo playback. You can do this in the console with: ^fs^fg/specbind KEY [ sv_gamespeed 500; onrelease [ sv_gamespeed 100 ] ]^fS" point -1 -1 2100
+    guistrut 0.25
+    guitext "Demo files save by default to a 'demos' folder within the home folder. They can be renamed from there and viewed in-game by selecting Demo from offline game modes (or by typing ^fs^fg/demo <name>^fS into the console)." point -1 -1 2100
+]
+
+newgui editing [
+    guiheader "edit mode"
+    guifont "emphasis" [ guicenter [ guitext "EDITING MODE" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/editing" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Create your own custom maps or edit an existing map using a wide variety of textures, models," ]
+    guicenter [ guitext "entities and world variables. If your creation is worthy it could be included in Red Eclipse!" ]
+    guicenter [
+        guinohitfx [guiimage "textures/images/007" [] 10]
+        guistrut 2.5
+        guinohitfx [guiimage "textures/images/008" [] 10]
+    ]
+    guistrut -3
+    guitab "tips & further reading"
+    guifont "emphasis" [ guicenter [ guitext "TIPS & FURTHER READING" ] ]
+    guistrut 0.5
+    guitext (format "In an editing game, press %1 to toggle between editing mode and testing mode." (dobindsearch edittoggle)) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "While in editing mode, different key bindings are used. Press %1 in editing mode to see an overview of these shortcuts." (dobindsearch "showgui edit" edit)) point -1 -1 2100
+    guistrut 0.25
+    guitext "To evaluate how parkour plays a role in your map without the constraints of limited impulse, load your creation in a Freestyle Editing match." point -1 -1 2100
+    guistrut 0.25
+    guitext "Some advanced editing commands with arguments and parameters can be executed via the console." point -1 -1 2100
+    guistrut 0.25
+    guitext "To submit your completed map creation for judgement by the community, visit the Maps and Mods section of the Red Eclipse forum at: ^fs^fghttp://redeclipse.net/forum^fS" point -1 -1 2100
+    guistrut 0.25
+    guitext "For more editing resources and guides, visit the Red Eclipse wiki at: ^fs^fghttp://redeclipse.net/wiki^fS" point -1 -1 2100
+]
+
+newgui deathmatch [
+    guiheader "deathmatch mode"
+    guifont "emphasis" [ guicenter [ guitext "DEATHMATCH MODE" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/deathmatch" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Attack enemy players gathering frags, assists, sprees and other bonuses to score points." ]
+    guicenter [ guitext "The team or individual with the most points at the end wins!" ]
+    guicenter [
+        guinohitfx [guiimage "textures/images/001" [] 10]
+        guistrut 2.5
+        guinohitfx [guiimage "textures/images/006" [] 10]
+    ]
+    guistrut -3
+    guitab "tips & strategy"
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.5
+    guitext "In a team match, working together is the key to success. Scoring an assist means a higher overall score for your team, and multiple players can overtake an enemy much faster." point -1 -1 2100
+    guistrut 0.25
+    guitext "In a Free-for-All match (also popular with the duel & survivor mutators), target groups of players close to you with high-spread ammo weapons, or target solitary players using weapons that require greater accuracy. To lose focus means to lose points!" point -1 -1 2100
+    guistrut 0.25
+    guitext "Regardless of what mutators are active, always avoid team kills. You and/or your team will lose points quickly if you neglect to avoid friendly fire, especially when it comes to explosives." point -1 -1 2100
+    guistrut 0.25
+    guitext "Using the flamer's secondary fire you can put out flames on yourself by pointing at the ground, and on others by pointing at them (which causes no harm to teammates)." point -1 -1 2100
+    guistrut 0.25
+    guitext "In Survivor Deathmatch, remember that to survive is everything, while the total amount of frags matters far less. Maintain a low profile and target the most difficult opponents first." point -1 -1 2100
+    guistrut 0.25
+    guitext "In Classic Deathmatch, map control can be the difference between winning and losing. Learn the locations of your favorite weapon spawns and plan accordingly." point -1 -1 2100
+    guistrut 0.25
+    guitext "In Duel Deathmatch, awareness of the location of your opponent at all times is very important. Choose a radar style that suits you from the options menu, or when radar is disabled (in the Hard mutator for example) turn music volume down to hear footsteps." point -1 -1 2100
+]
+
+newgui capture [
+    guiheader "capture-the-flag mode"
+    guifont "emphasis" [ guicenter [ guitext "CAPTURE-THE-FLAG MODE" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/capture" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Capture the enemy team's flag and return it to your base while defending your own flag from capture." ]
+    guicenter [ guitext "Secure both flags at your base to score a point, the team with the most points at the end wins!" ]
+    guicenter [
+        guinohitfx [guiimage "textures/images/002" [] 10]
+        guistrut 2.5
+        guinohitfx [guiimage "textures/images/009" [] 10]
+    ]
+    guistrut -3
+    guitab "tips & strategy"
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.5
+    guitext (format "Secure your own team's flag by pressing %1 to make stealing it more difficult and defending it simpler." (dobindsearch affinity)) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "You can drop a flag at any time by pressing %1. Drop your team's flag into lava or an open abyss to reset it back to the base." (dobindsearch affinity)) point -1 -1 2100
+    guistrut 0.25
+    guitext "Remember, defending your own flag is as important as stealing the flag from the enemy. Choose which role you will play and try to stick to it for at least half the match." point -1 -1 2100
+    guistrut 0.25
+    guitext "Beware of players who hold grenades in defending positions near a flag. Draw them away with your shots or bring a flamer to put out flames." point -1 -1 2100
+    guistrut 0.25
+    guitext "Being in close proximity of your team's flag will add to both your attack and defense, as well as buff your max health and health regeneration." point -1 -1 2100
+    guistrut 0.25
+    guitext "Use impulse boosts and parkour tricks to steal the flag and make your escape more efficiently." point -1 -1 2100
+    guistrut 0.25
+    guitext "In Multi Capture-the-Flag make your target the weakest team, remembering you can carry up to 4 flags!" point -1 -1 2100
+    guistrut 0.25
+    guitext (format "In Survivor Capture-the-Flag, if you are the last player alive on your team and at a disadvantage, press %1 to suicide and respawn your teammates." (dobindsearch suicide)) point -1 -1 2100
+    guistrut 0.25
+    guitext "In Multi-Survivor Capture-the-Flag, up to three points may be gained per round. Try to gather several flags and capture them all at once!" point -1 -1 2100
+    guistrut 0.25
+    guitext "In Defend Capture-the-Flag, lure the opponent holding your team's flag into an easily defendable location to successfully reset the flag." point -1 -1 2100
+    guistrut 0.25
+    guitext (format "In Protect Capture-the-Flag, press %1 to use cunning parkour maneuvers and evade the enemy team long enough while holding their flag." (dobindsearch special)) point -1 -1 2100
+    guitab "mode-specific mutators"
+    guifont "emphasis" [ guicenter [ guitext "QUICK MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/capturequick" [] 2 ] ]
+    guicenter [ guitext "Touching your own team's loose flag will instantly teleport it back to your base," ]
+    guicenter [ guitext "instead of having to escort it there on foot." ]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "DEFEND MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/capturedefend" [] 2 ] ]
+    guicenter [ guitext "When your flag is taken from your base, you cannot pick it up." ]
+    guicenter [ guitext "You must protect it from where it's dropped until the timer resets." ]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "PROTECT MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/captureprotect" [] 2 ] ]
+    guicenter [ guitext "Instead of scoring for returning an enemy flag to your base, you simply must" ]
+    guicenter [ guitext "pick it up and hold onto it for 15 seconds to score. You may also carry your own" ]
+    guicenter [ guitext "flag around while seeking out others." ]
+]
+
+newgui defend [
+    guiheader "defend-and-control mode"
+    guifont "emphasis" [ guicenter [ guitext "DEFEND-AND-CONTROL MODE" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/defend" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Secure and defend control points scattered around the map by standing near them for a set period of time." ]
+    guicenter [ guitext "Overthrow enemy control points then secure them for your own team to gain an advantage. Every secured" ]
+    guicenter [ guitext "control point provides a steady stream of points, the team with the most points at the end wins!" ]
+    guicenter [
+        guinohitfx [guiimage "textures/images/003" [] 10]
+        guistrut 2.5
+        guinohitfx [guiimage "textures/images/010" [] 10]
+    ]
+    guistrut -3
+    guitab "tips & strategy"
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.5
+    guitext "Secured control points provide a powerful buff when in close proximity that enhances attack, defense, health regeneration and max health." point -1 -1 2100
+    guistrut 0.25
+    guitext "If your team is ahead and controls several more points than the enemy, switch focus to defense of the points you control." point -1 -1 2100
+    guistrut 0.25
+    guitext "In Defend-and-Control mode the right side of the screen holds a quick reference to see which teams control which points, and which are neutral." point -1 -1 2100
+    guistrut 0.25
+    guitext "Controlling the enemy base point for long is very difficult and rarely worth it, as enemy spawn points are often right next to this point. Concentrate first on the other points in the map unless the opportunity is too perfect to pass up." point -1 -1 2100
+    guitab "mode-specific mutators"
+    guifont "emphasis" [ guicenter [ guitext "KING MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/defendking" [] 2 ] ]
+    guicenter [ guitext "There is only one control point in this mutator, located near the center of the map." ]
+    guicenter [ guitext "To make things harder, points are only given when players are in proximity to it." ]
+    guicenter [ guitext "The objective: make your team king of the hill." ]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "QUICK MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/defendquick" [] 2 ] ]
+    guicenter [ guitext "Control points are secured much more quickly in this mutator. When capturing enemy control" ]
+    guicenter [ guitext "points there is no neutral state, the point is captured immediately for your team." ]
+]
+
+newgui bomber [
+    guiheader "bomber-ball mode"
+    guifont "emphasis" [ guicenter [ guitext "BOMBER-BALL MODE" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/bomber" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Secure the bomber-ball and deliver it to the enemy base to destroy it, but watch the timer!" ]
+    guicenter [ guitext (format "If the timer runs out, the ball will explode and you with it. Press %1 to throw it before this" (dobindsearch "affinity") ) ]
+    guicenter [ guitext (format "happens, or hold %1 and lock target on a teammate to pass the ball, resetting the timer." (dobindsearch "affinity") ) ]
+    guicenter [
+        guinohitfx [guiimage "textures/images/004" [] 10]
+        guistrut 2.5
+        guinohitfx [guiimage "textures/images/011" [] 10]
+    ]
+    guistrut -3
+    guitab "tips & strategy"
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.5
+    guitext "Just as in Capture-the-Flag, being close to your team base provides a powerful buff that enhances attack, defense, health regeneration and max health." point -1 -1 2100
+    guistrut 0.25
+    guitext (format "One of the most important keys in Bomber-Ball is %1. With it you can throw the ball before it explodes, throw it inside an enemey goal or pass it to teammates." (dobindsearch affinity)) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "Holding %1 will lock target on the nearest teammate in the center of your sight, and releasing will pass the ball directly to them instead of a normal throw (regardless of how far away they are from you)." (dobindsearch affinity)) point -1 -1 2100
+    guistrut 0.25
+    guitext "Pointing directly up in the air and throwing the ball can often allow you to catch it easily again, while defending the point where it will fall. This is especially useful in Free-for-All Hold Bomber-Ball." point -1 -1 2100
+    guitab "mode-specific mutators"
+    guifont "emphasis" [ guicenter [ guitext "HOLD MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/bomberhold" [] 2 ] ]
+    guicenter [ guitext "Instead of bringing the ball to a goal, you earn points in this mutator by holding the ball as long as" ]
+    guicenter [ guitext "possible. Killing an enemy will reset the explode timer. The ball will still explode when the timer runs" ]
+    guicenter [ guitext "out however, resulting in drastic point loss, so throw it away or pass it to a teammate before this happens!" ]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "BASKET MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/bomberbasket" [] 2 ] ]
+    guicenter [ guitext "Instead of bringing the ball to the goal on foot, you absolutely must throw it in to score." ]
+    guicenter [ guitext "You also must be far enough away from the goal before attempting a shot!" ]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "ATTACK MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/bomberattack" [] 2 ] ]
+    guicenter [ guitext "One team spawns with the ball at their goal while all others try to prevent the" ]
+    guicenter [ guitext "team from scoring at another goal. Afer a set time, the roles are switched." ]
+]
+
+newgui race [
+    guiheader "race mode"
+    guifont "emphasis" [ guicenter [ guitext "RACE MODE" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/race" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Race across maps set up as a course and complete more finished laps than opponents, carefully" ]
+    guicenter [ guitext (format "managing your limited impulse and executing parkour moves with the %1 button. Reaching a" (dobindsearch "special") ) ]
+    guicenter [ guitext "checkpoint will allow you to respawn from that spot." ]
+    guicenter [
+        guinohitfx [guiimage "textures/images/005" [] 10]
+        guistrut 2.5
+        guinohitfx [guiimage "textures/images/012" [] 10]
+    ]
+    guistrut -3
+    guitab "tips & strategy"
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.5
+    guitext "During a Race, players do not spawn with a weapon. Melee attacks are also not effective against opponents unless the Gauntlet mutator is active. The only objective is to finish laps." point -1 -1 2100
+    guistrut 0.25
+    guitext "Race mode offers an excellent opportunity to learn and practice the more advanced parkour moves in Red Eclipse, and can be an essential tool to performing your best in other modes." point -1 -1 2100
+    guistrut 0.25
+    guitext (format "When competing with the Timed mutator, if you wish to begin the lap again instead of starting from a checkpoint, press %1 to suicide (there is no penalty)." (dobindsearch suicide)) point -1 -1 2100
+    guistrut 0.25
+    guitext "Remember, in Race completing laps is more important than the individual times of each lap. Suiciding or dying from a map hazard is very counter-productive to winning!" point -1 -1 2100
+    guistrut 0.25
+    guitext "The non-combative nature of Race matches can make it a less hectic or competitive mode. Just run some laps and relax a little!" point  -1 -1 2100
+    guistrut 0.25
+    guitext "There are many unofficial Race maps to explore, created by the Red Eclipse community. You can find them on the forum at ^fs^fghttp://redeclipse.net/forum^fS" point -1 -1 2100
+    guitab "mode-specific mutators"
+    guifont "emphasis" [ guicenter [ guitext "TIMED MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/racetimed" [] 2 ] ]
+    guicenter [ guitext "Instead of competing for the most number of laps, players" ]
+    guicenter [ guitext "and teams compete for the fastest overall lap time." ]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "ENDURANCE MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/raceendurance" [] 2 ] ]
+    guicenter [ guitext "Your impulse meter is not restored upon death or checkpoint respawn." ]
+    guicenter [ guitext "Impulse management is essential here, so plan your route carefully!" ]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "GAUNTLET MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/racegauntlet" [] 2 ] ]
+    guicenter [ guitext "One team races to reach the goal and score points, while the other teams try to" ]
+    guicenter [ guitext "attack them before they can reach the end. Afer a set time, the roles are switched." ]
+]
+
+newgui multi [
+    guiheader "multi mutator"
+    guifont "emphasis" [ guicenter [ guitext "MULTI MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/multi" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Play with four teams instead of two (by default Kappa & Sigma, in addition to Alpha & Omega)." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "Multi matches are best suited to large groups of players on symmetrical maps." point -1 -1 2100
+    guistrut 0.25
+    guitext "Not all maps support the Multi mutator, though some maps are created with Multi specifically in mind" point -1 -1 2100
+    guistrut 0.25
+    guitext "Similar to the Free-for-All mutator, there are many different opponents to target in Multi. Try to concentrate on either the weakest team or the greatest threat." point -1 -1 2100
+    guistrut 0.25
+    guitext "As with all team modes, remember the presence of your teammates and try at all costs to avoid team kills (the point loss can be detrimental in Multi)." point -1 -1 2100
+]
+
+newgui ffa [
+    guiheader "free-for-all mutator"
+    guifont "emphasis" [ guicenter [ guitext "FREE-FOR-ALL MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/ffa" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "There are no teams, just every player for themselves in a Free-for-All showdown!" ]
+    guicenter [ guitext "This mutator is not available in Capture-the-Flag or Defend-and-Control modes." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "The most important thing to remember in Free-for-All: no target or enemy is safe from your fire! Don't hesitate to let loose in a frenzy at everyone and everything you see." point -1 -1 2100
+    guistrut 0.25
+    guitext "Unlike team modes, explosive items cannot possibly count against you in Free-for-All unless you hit yourself. Grab every single grenade, mine & rocket you can hold then use them quickly and strategically." point -1 -1 2100
+    guistrut 0.25
+    guitext "The Free-for-All mutator is often used in Race matches and forced in Edit mode." point -1 -1 2100
+    guistrut 0.25
+    guitext "Any modes, mutators or combinations thereof with team objectives are incompatible with the Free-for-All mutator." point -1 -1 2100
+    guistrut 0.25
+    guitext "Free-for-All can be used in Bomber-Ball matches if the Hold mutator is selected." point -1 -1 2100
+    guistrut 0.25
+    guitext "Player coloring is different when the Free-for-All mutator is selected, and these settings can be controlled from the profile menu." point -1 -1 2100
+]
+
+newgui coop [
+    guiheader "co-op mutator"
+    guifont "emphasis" [ guicenter [ guitext "CO-OP MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/coop" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "All human players are forced onto the same team, battling against a team of" ]
+    guicenter [ guitext "bots... and there are more of them than there are of you!" ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "Note that there are separate vars for the balancing and skill range of bots in Co-op games. For an extra challenge, raise ^fs^fg/botskill^fS or ^fs^fg/botlimit^fS." point -1 -1 2100
+    guistrut 0.25
+    guitext "In addition to A.I. skill, each weapon has a skew variable according to it's name that affects A.I. accuracy. For example, ^fs^fg/shotgunaiskew1^fS." point -1 -1 2100
+    guistrut 0.25
+    guitext "Bots may be formidable opponents in Deathmatch games, but the A.I. has problems keeping up with players in more complex modes." point -1 -1 2100
+    guistrut 0.25
+    guitext "To make a Co-op match more interesting, enable the Survivor and/or Vampire Mutators." point -1 -1 2100
+    guistrut 0.25
+    guitext "Bots are programmed in a specific way and some of their actions can be predictable, so use this to your advantage." point -1 -1 2100
+    guistrut 0.25
+    guitext "A.I. movement is assisted by a large collection of invisible points on the map called waypoints. Bots use these waypoints to calculate their fastest or least dangerous routes." point -1 -1 2100
+]
+
+newgui instagib [
+    guiheader "instagib mutator"
+    guifont "emphasis" [ guicenter [ guitext "INSTAGIB MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/instagib" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Players spawn with only 1 HP, so any hit is lethal. Unless the Medieval or Kaboom mutators are" ]
+    guicenter [ guitext "also selected, players only spawn with the rifle. All rifle spawns on the map become grenade spawns." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "Instagib matches are very fast-paced and hectic. While it may be easier to frag opponents, you are also fragged much more often yourself (the respawn time is shortened to reflect this)." point -1 -1 2100
+    guistrut 0.25
+    guitext "Rifle shots normally inflict splash damage, but this is disabled in Instagib games. So aim carefully!" point -1 -1 2100
+    guistrut 0.25
+    guitext "Remember, you can also attack opponents with melee kicks or certain other parkour moves in Instagib." point -1 -1 2100
+    guistrut 0.25
+    guitext "Instagib is a very effective way to build your skill quickly with the Rifle's primary and secondary fire." point -1 -1 2100
+    guistrut 0.25
+    guitext "To mix up an Instagib match, change the spawn weapon using ^fs^fg/instaweapon^fS." point -1 -1 2100
+]
+
+newgui medieval [
+    guiheader "medieval mutator"
+    guifont "emphasis" [ guicenter [ guitext "MEDIEVAL MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/medieval" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Players only spawn with swords. All sword spawns on the map become grenade spawns." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "Grenades still spawn in the Medieval mutator, but mines are disabled by default." point -1 -1 2100
+    guistrut 0.25
+    guitext "When attacking with the sword, impulse is almost as valuable as health. Spend your limited meter wisely, and remember to crouch to regenerate impulse faster." point -1 -1 2100
+    guistrut 0.25
+    guitext "As with any weapon, always aim for the head! A headshot is sure to inflict bleed damage and drain opponents' health quicker." point -1 -1 2100
+    guistrut 0.25
+    guitext "Medieval is a very effective way to build your skill quickly with the Sword's primary and secondary attacks." point -1 -1 2100
+    guistrut 0.25
+    guitext "When the Medieval mutator is voted with Instagib, it only takes one swipe to frag opponents!" point -1 -1 2100
+]
+
+newgui kaboom [
+    guiheader "kaboom mutator"
+    guifont "emphasis" [ guicenter [ guitext "KABOOM MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/kaboom" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Players only spawn with grenades and mines. Grenades and mines reload in" ]
+    guicenter [ guitext "this mutator like a normal loadout weapon, though your carry capacity remains default." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "On some maps, there are additional rockets that spawn only in Kaboom games." point -1 -1 2100
+    guistrut 0.25
+    guitext "Kaboom matches on small and complicated maps can be very... explosive. Evasion is almost as important as your offense." point -1 -1 2100
+    guistrut 0.25
+    guitext "Remember, you spawn with both grenades and mines! Switch between them often to maximize your combat effectiveness." point -1 -1 2100
+    guistrut 0.25
+    guitext "Make use of the unique functionality of grenade and mine primary and secondary attacks. Learn their differences to boost your advantage." point -1 -1 2100
+    guistrut 0.25
+    guitext "In Kaboom (especially Free-for-All), mines often cover lower surfaces. So, traverse using Wall Runs and Wall Jumps whenever you can." point -1 -1 2100
+]
+
+newgui duel [
+    guiheader "duel mutator"
+    guifont "emphasis" [ guicenter [ guitext "DUEL MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/duel" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Only two players spawn at a time, the rest are placed in a queue. The winning team or individual" ]
+    guicenter [ guitext "continues to the next round, while the loser always gets placed at the end of the queue." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "For obvious reasons, the rocket does not spawn in the Duel mutator (though other explosives do by default)." point -1 -1 2100
+    guistrut 0.25
+    guitext "Duel matches are often very frustrating between two players of different skill level. However, there is much to be learned from other players when spectating! Don't let the length of a queue distract you from learning new strategy." point -1 -1 2100
+    guistrut 0.25
+    guitext "Duels are typically carried out on smaller and more compact maps, which is why there is a separate map list for this mutator." point -1 -1 2100
+    guistrut 0.25
+    guitext "Without health regeneration, every action toward opponents becomes a calculated risk. Be as cautious in your offense as your defense." point -1 -1 2100
+    guistrut 0.25
+    guitext "If desired, game music can be silenced using ^fs^fg/musicvol 0^fS. This will allow you to hear your opponent's footsteps and parkour moves!" point -1 -1 2100
+    guistrut 0.25
+    guitext "For an extra challenge, combine Duel with the Hard mutator for disabled radar." point -1 -1 2100
+]
+
+newgui survivor [
+    guiheader "survivor mutator"
+    guifont "emphasis" [ guicenter [ guitext "SURVIVOR MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/survivor" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "All players spawn as normal, but there is no health regeneration. The last player or team" ]
+    guicenter [ guitext "standing wins the round. Players that die are placed in a queue to respawn in the next round." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "With no health regeneration, it is wiser to form a more cautious strategy than rushing into the heat of battle. Remeber, the only goal in this mutator is survival to the end." point -1 -1 2100
+    guistrut 0.25
+    guitext "Explosives and their recursive damage statuses are more dangerous than normal. Be on your guard for hidden mines and opponents wielding a grenade. By default the rocket does not spawn in Survivor." point -1 -1 2100
+    guistrut 0.25
+    guitext "Snipers that camp behind cover are commonplace in Survivor. If possible, avoid routes with wide open spaces and stick to corridors or plenty of cover." point -1 -1 2100
+    guistrut 0.25
+    guitext "Players that have just joined a Survivor match must wait in queue until the next round starts to spawn." point -1 -1 2100
+    guistrut 0.25
+    guitext "In Free-for-All Survivor Deathmatch, taking cover in a hidden spot until the worst of the combat is over is not a poor strategy!" point -1 -1 2100
+]
+
+newgui classic [
+    guiheader "classic mutator"
+    guifont "emphasis" [ guicenter [ guitext "CLASSIC MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/classic" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Everyone spawns with only one spawn weapon (by default, the pistol). The rest of" ]
+    guicenter [ guitext "the weapons can be found at spawn points, scattered around the map." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "In addition to weapon spawn points, fragged players drop their weapons upon death to be picked up by other players (but will disappear after a few seconds)." point -1 -1 2100
+    guistrut 0.25
+    guitext "Once a weapon has spawned, it will not respawn until it has been dropped and disappears or is lost in a pit." point -1 -1 2100
+    guistrut 0.25
+    guitext "Learn the locations of your favorite weapon spawns per map, for better control of your combat and to put yourself at a greater advantage." point -1 -1 2100
+    guistrut 0.25
+    guitext "Playing Classic is an excellent way to develop your skill with every weapon and learn the effectiveness of new strategies, as well as determine what loadout you are most comfortable with when the choice is presented to you." point -1 -1 2100
+    guistrut 0.25
+    guitext "Fun Fact: Classic used to be the default playstyle for Deathmatch mode, while choice of weapon loadout was a mutator called Arena." point -1 -1 2100
+]
+
+newgui onslaught [
+    guiheader "onslaught mutator"
+    guifont "emphasis" [ guicenter [ guitext "ONSLAUGHT MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/onslaught" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Enemy drones and sentry turrets spawn in the middle of the action." ]
+    guicenter [ guitext "If one of them kills you, you lose points!" ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "The addition of drones and turrets is intended to complicate the action and add to the chaos of a match. It is especially overwhelming in Defend-and-Control, where you must hold the same spot for a set period of time." point -1 -1 2100
+    guistrut 0.25
+    guitext "On some maps there are enemy gun turrets than spawn only when the Onslaught mutator is enabled." point -1 -1 2100
+    guistrut 0.25
+    guitext "More targets to hit make weapons with a wider spread more rewarding to use. Now, where is that rocket?" point -1 -1 2100
+    guistrut 0.25
+    guitext "When critically wounded during Onslaught, keep your distance from annoying drones to avoid being picked off quickly." point -1 -1 2100
+    guistrut 0.25
+    guitext "Drone enemies travel at a slower default speed than normal bots, and utilize only 1 weapon (with added A.I. stupidity)." point -1 -1 2100
+]
+
+newgui freestyle [
+    guiheader "freestyle mutator"
+    guifont "emphasis" [ guicenter [ guitext "FREESTYLE MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/freestyle" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "There are no impulse limits, the impulse meter is" ]
+    guicenter [ guitext "never depleted so get your parkour on!" ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "The Freestyle mutator also disables the impulse count rule, so you can keep on performing parkour moves to your heart's content." point -1 -1 2100
+    guistrut 0.25
+    guitext "Especially on larger maps with wide open areas, the Freesyle mutator can lead to chaotic and very fast-paced gameplay." point -1 -1 2100
+    guistrut 0.25
+    guitext "There are no limits to creativity with parkour during Freestyle. Explore new shortcuts, reach new heights and cheat death!" point -1 -1 2100
+    guistrut 0.25
+    guitext "One very effective strategy for quick movement during Freestyle is a continuous chain of Impulse Slides." point -1 -1 2100
+    guistrut 0.25
+    guitext "Freestyle is a sword wielder's heavensend. With no need to stop offense to regenerate impulse, all players should beware the mighty sword." point -1 -1 2100
+]
+
+newgui vampire [
+    guiheader "vampire mutator"
+    guifont "emphasis" [ guicenter [ guitext "VAMPIRE MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/vampire" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Damage dealt to enemies causes you to regenerate the same amount of health." ]
+    guicenter [ guitext "The default cap in this mutator is 300 health." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "Because of the leech effect when players exchange health, combat during vampire matches can last for quite a while (especially when Survivor or Duel is also selected)." point -1 -1 2100
+    guistrut 0.25
+    guitext "Even with a large amount of health, a few direct hits will bring you back to reality quickly. Don't let your guard down." point -1 -1 2100
+    guistrut 0.25
+    guitext "When the Resize mutator is also selected, players can grow larger than their normal size, proportional to the health increase." point -1 -1 2100
+    guistrut 0.25
+    guitext "Obviously the most effective strategy in this mode is to deal as much damage as possible to as many opponents as possible. The Flamer and Zapper are excellent ways to cause residual damage which will conversely add to your health." point -1 -1 2100
+    guistrut 0.25
+    guitext "During Vampire Capture-the-Flag it is advantageous to target a few weaker opponents to build up your health before making a run for the flag." point -1 -1 2100
+]
+
+newgui resize [
+    guiheader "resize mutator"
+    guifont "emphasis" [ guicenter [ guitext "RESIZE MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/resize" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "As your health decreases, you become smaller and your speed decreases." ]
+    guicenter [ guitext "The opposite is true when you gain health." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "When combined with the Instagib mutator, you become larger every time you kill an enemy in Resize (until you die and return to normal size)." point -1 -1 2100
+    guistrut 0.25
+    guitext "A small target may be harder to hit, but also moves at a slower pace. Cover more width using a weapon with a larger spread." point -1 -1 2100
+    guistrut 0.25
+    guitext "In a way, being far smaller or far larger than the default sized player has disadvantages. Aim for a delicate balance to negate the effect of this mutator." point -1 -1 2100
+    guistrut 0.25
+    guitext "You may grow so large in Resize as to find yourself unable to fit in certain areas of maps. A little of your own weapon flak will shave off some of that extra mass." point -1 -1 2100
+    guistrut 0.25
+    guitext "When smaller in size than your opponent, try to remain right beneath their field of view by jumping toward their feet." point -1 -1 2100
+]
+
+newgui hard [
+    guiheader "hard mutator"
+    guifont "emphasis" [ guicenter [ guitext "HARD MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/hard" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "Players spawn without a radar." ]
+    guicenter [ guitext "There is no health regeneration in this mutator." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "As the name implies, the purpose of this mutator is to provide an added layer of difficulty to a match through handicaps." point -1 -1 2100
+    guistrut 0.25
+    guitext "As with Survivor or Duel, there is no health regeneration in Hard matches. Tread carefully and make your shots count." point -1 -1 2100
+    guistrut 0.25
+    guitext "No matter what radar style you have chosen, it is completely disabled during a Hard match. The element of suprise rests in the hands of every player." point -1 -1 2100
+    guistrut 0.25
+    guitext "Remember, not only have you lost track of players and teammates, you also won't see explosives (including the rocket) spawn on your radar. Check their spawn points frequently." point -1 -1 2100
+    guistrut 0.25
+    guitext "For a truly brutal Red Eclipse, try a Free-for-All Freestyle Onslaught Hard Deathmatch on a large map." point -1 -1 2100
+]
+
+newgui basic [
+    guiheader "basic mutator"
+    guifont "emphasis" [ guicenter [ guitext "BASIC MUTATOR" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage "textures/modes/basic" [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext "No collectable items will spawn in the arena." ]
+    guistrut 2
+    guifont "emphasis" [ guicenter [ guitext "TIPS & STRATEGY" ] ]
+    guistrut 0.25
+    guitext "If you want a match without grenades, mines and rockets, try the basic mutator." point -1 -1 2100
+    guistrut 0.25
+    guitext "Forget about map control: Focus on your enemies and team objective!" point -1 -1 2100
+    guistrut 0.25
+    guitext "For a showdown with pistols and parkour only, combine the classic and basic mutators." point -1 -1 2100
+]
+
+newgui capturedefend [
+    cleargui 1 ; showgui capture 3
+]
+
+newgui captureprotect [
+    cleargui 1 ; showgui capture 3
+]
+
+newgui capturequick [
+    cleargui 1 ; showgui capture 3
+]
+
+newgui defendking [
+    cleargui 1 ; showgui defend 3
+]
+
+newgui defendquick [
+    cleargui 1 ; showgui defend 3
+]
+
+newgui bomberattack [
+    cleargui 1 ; showgui bomber 3
+]
+
+newgui bomberbasket [
+    cleargui 1 ; showgui bomber 3
+]
+
+newgui bomberhold [
+    cleargui 1 ; showgui bomber 3
+]
+
+newgui raceendurance [
+    cleargui 1 ; showgui race 3
+]
+
+newgui racegauntlet [
+    cleargui 1 ; showgui race 3
+]
+
+newgui racemarathon [
+    cleargui 1 ; showgui race 3
+]
+
+newgui accounts [
+    guiheader "player accounts"
+    guifont "emphasis" [ guicenter [ guitext "PLAYER ACCOUNTS" ] ]
+    guistrut 0.5
+    guifont "emphasis" [
+        guicenter [ guibutton "obtain an account" "showgui obtain-account" ]
+        guicenter [ guibutton "account auth levels" "showgui account-levels" ]
+    ]
+    if (&& (stringlen $accountname) (stringlen $accountpass) ) [
+        guistrut 1
+        guicenter [
+            guitext (format "logged in as ^fs^fc%1^fS" $accountname)
+            guifont "little" [
+                guicenter [ guibutton " ^fs^fy(change)^fS" [ showgui profile 4 ] ]
+            ]
+        ]
+    ]
+]
+
+newgui obtain-account [
+    guiheader "obtain an account"
+    guifont "emphasis" [ guicenter [ guitext "OBTAIN AN ACCOUNT" ] ]
+    guistrut 0.5
+    guicenter [ guitext "You may apply for an account by filling out the form at the following URL:" ]
+    guicenter [ guitext "^fs^fghttp://redeclipse.net/apply^fS" ]
+    guistrut 0.5
+    guicenter [ guitext "Accounts are processed manually, please allow 1-2 weeks for processing."]
+    guicenter [ guitext "Global Moderator applications are closed, you will need a referral"]
+    guicenter [ guitext "from an existing team member at this time."]
+]
+
+newgui account-levels [
+    guiheader "account auth levels"
+    guifont "emphasis" [ guicenter [ guitext "ACCOUNT AUTH LEVELS" ] ]
+    guistrut 0.5
+    guicenter [ guitext "There are two types of auth accounts in Red Eclipse, ^fs^fyglobal auth^fS and ^fs^fclocal auth^fS. The former are assigned by the Red Eclipse team and are functional globally across all servers, the latter are assigned by server owners and local only to that server. Auth accounts are represented by small icons shown next to player names, each with different privileges and responsibilities. If a player has a higher local auth than their global auth, the local auth [...]
+    guistrut 1
+    guitext "The following signify ^fs^fyglobal^fS privileges across all servers:"
+    guistrut 0.5
+    guilist [
+        guilist [
+            looplist i [player supporter moderator operator administrator developer founder] [
+                guitext  $i (format "privs/%1" $i) 0xFFFF00
+            ]
+        ]
+        guistrut 1
+        guilist [
+            guitext "Registered players with regular auth accounts."
+            guitext "These are players who contributed to Red Eclipse in some way, be it financially or creatively."
+            guitext "These are regular moderators, capable of muting, kicking and putting players in quarantine."
+            guitext "These are advanced moderators, capable of banning and changing many server variables."
+            guitext "These are normal administrators, having full control of server settings, variables and connected players."
+            guitext "These are advanced administrators and core developers of the Red Eclipse project."
+            guitext "These are the founders of the Red Eclipse project and creators of the game."
+        ]
+    ]
+    guistrut 1
+    guitext "Furthermore, some ^fs^fclocal^fS privileges can be granted on individual servers:"
+    guistrut 0.5
+    guilist [
+        guilist [
+            looplist i [supporter moderator operator administrator] [
+                guitext  $i (format "privs/local%1" $i) 0x00FFFF
+            ]
+        ]
+        guistrut 1
+        guilist [
+            guitext "These are players who contributed to the server in some way or received special recognition."
+            guitext "These are regular moderators, local to the server and capable of all the same as global moderators"
+            guitext "These are advanced moderators, local to the server and capable of all the same as global operators."
+            guitext "These are normal administrators, local to the server and capable of all the same as global administrators."
+        ]
+    ]
+    guistrut 1
+    guilist [
+        guitext "Last and also least, the following signify those with "
+        guitext "no auth" "" 0xFF0000
+        guitext " at all:"
+    ]
+    guistrut 0.5
+    guilist [
+        guilist [
+            looplist i [bot none] [
+                guitext  $i (format "privs/%1" $i) 0xFF0000
+            ]
+        ]
+        guistrut 1
+        guilist [
+            guitext "These are A.I. controlled players, bots that do not speak and exist to balance matches."
+            guitext "These are default players with no account and no extra privileges."
+        ]
+    ]
+]
+
+newgui parkour [
+    guiheader "parkour"
+    guifont "emphasis" [ guicenter [ guitext "PARKOUR" ] ]
+    guistrut 0.25
+    guicenter [ guitext "In Red Eclipse, the art of parkour replaces your player's normal fatigue and damage from falling." ]
+    guicenter [ guitext (format "Parkour or impulse moves greatly enhance your mobility, but deplete your impulse meter. Press %1 to crouch" (dobindsearch crouch) ) ]
+    guicenter [ guitext (format "and refill the meter more quickly. You can do at most %1 parkour moves before you must touch the ground." $impulsecount ) ]
+    guistrut 0.5
+    guitext (format "^fs^fyMelee^fS: Jump at an enemy and hold %1 to attack with a kick (always aim for the head). You can also inflict collision damage with a ^fs^fySlide^fS or ^fs^fyWall Run^fS, as described below." (dobindsearch special) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyBoost^fS: Press %1 while jumping or falling. This propels you in the direction you are facing and redirects momentum, such as falling speed. Jumps and ^fs^fyBoosts^fS can be chained together to build up speed quickly." (dobindsearch jump) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyImpulse Jump^fS: When airborne, stop moving before you ^fs^fyBoost^fS to get an upwards push that redirects all horizontal momentum. This is useful to land on small platforms." (dobindsearch jump) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyWall Run^fS: Jump at a wall and press %1 at any time while touching it to run across it. This move is a fast and easy way to cover wide distances, especially with obstacles in the way. It can be followed by other parkour moves to build momentum." (dobindsearch special) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyWall Jump^fS: Press %1 during a ^fs^fyWall Run^fS. In essence this allows you to perform two consecutive parkour jumps, meaning you can still ^fs^fyBoost^fS after jumping off of the wall!" (dobindsearch jump) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyVault^fS: Hold %1 to bounce over ledges or any obstacle below your eye level. This also works while on solid ground." (dobindsearch special) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyWall Kick^fS: Jump at a wall and press %1 to bounce off it. Look either up or down to adjust the height of your momentum." (dobindsearch special) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyWall Climb^fS: Jump at a wall, look directly up or down vertically and hold %1. If you bounce off instead, you are facing the wall instead of a vertical angle." (dobindsearch special) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyDash^fS: Double-tap a movement key quickly to perform an evasive maneuver (also works side-to-side for a strafing effect, or even backwards)." (dobindsearch jump) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fySlide^fS: Hold %1 to crouch and then do a ^fs^fyDash^fS. Alternately, jump forward and press %1 when you land (or whenever you hit solid ground after being airborne)." (dobindsearch crouch) ) point -1 -1 2100
+    guistrut 0.25
+    guitext (format "^fs^fyLaunch^fS: During a ^fs^fySlide^fS, look directly up (important) and press %1. Reaching the full potential height of a ^fs^fyLaunch^fS requires very precise timing. You cannot ^fs^fyBoost^fS after a ^fs^fyLaunch^fS, unless you do a ^fs^fyWall Kick^fS or ^fs^fyVault^fS." (dobindsearch jump) ) point -1 -1 2100
+    guistrut -0.75
+    guitext (format "^fs^fyMulti Slide^fS: During a ^fs^fySlide^fS, look down and press %1. This allows you to chain ^fs^fySlides^fS and build momentum, that can be used to ^fs^fyLaunch^fS even higher or move across flat terrain very quickly. " (dobindsearch jump) ) point -1 -1 2100
+]
+
+newgui rules [
+    guiheader "rules"
+    guifont "emphasis" [ guicenter [ guitext "RED ECLIPSE MULTIPLAYER GUIDELINES" ] ]
+    guistrut 0.5
+    guicenter [ guitext "In order to ensure a better experience for all players, the Red Eclipse" ]
+    guicenter [ guitext "team asks that you comply with some simple guidelines:" ]
+    guistrut 0.5
+    guicenter [
+        guilist [
+            guitext "^fs^fcRespect^fS the rules of individual ^fs^fcservers^fS at all times." point
+            guitext "^fs^fcRespect^fS other ^fs^fcusers^fS when communicating and by playing fairly." point
+            guitext "^fs^fcObey^fS the requests of global ^fs^fcmoderators^fS without argument." point
+        ]
+    ]
+    guistrut 0.5
+    guicenter [ guitext "The following are the full multiplayer guidelines for connecting to the Red Eclipse Master Server:" ]
+    guistrut 0.5
+    guicenter [
+        guieditor doc/guidelines.txt -80 15
+        textinit doc/guidelines.txt doc/guidelines.txt
+        textmode 4
+    ]
+    guistrut 1
+]
+
+newgui scoring [
+    guiheader "scoring"
+    guifont "emphasis" [ guicenter [ guitext "SCORING SYSTEM" ] ]
+    guistrut 0.5
+    guicenter [ guitext "Scoring in Red Eclipse is handled using a very intricate Deathmatch Scoring System, with normal point gain and loss as well as bonus points attainable through combo streaks or sprees. While team scores in other game modes are objective based, the system described here still applies to scores of individual players." "" -1 -1 2100 ]
+    guitext ""
+    guicenter [ guitext "Choose a specific topic to learn more:" ]
+    guistrut 1
+    guifont "emphasis" [
+        guicenter [ guibutton "point gain" "showgui point-gain" ]
+        guicenter [ guibutton "point penalties" "showgui point-penalties" ]
+        guicenter [ guibutton "combos and sprees" "showgui combos" ]
+        guicenter [ guibutton "dominating and revenge" "showgui dominating" ]
+        guicenter [ guibutton "capture, defend and bomber" "showgui objective-scores" ]
+        guicenter [ guibutton "duel, survivor and race" "showgui various-scores" ]
+    ]
+]
+
+newgui point-gain [
+    guiheader "point gain"
+    guifont "emphasis" [ guicenter [ guitext "FRAGS & ASSISTS" ] ]
+    guistrut 0.25
+    guicenter [ guinohitfx [ guiimage textures/dead [] 2 ] ]
+    guistrut 0.25
+    guicenter [ guitext (format "The core objective of the entire game, land the killing shot on an enemy to earn a frag and ^fs^fg%1^fS points. If you did not kill an opponent, but chipped in with a hit over the previous ^fs^fg%2^fS seconds before death, your name is mentioned in the obituary line for an assist, and you receive one point. Multiple players can receive assists on the same kill, and in a team game this can result in multiple points from assists for the same team. Also note [...]
+    guistrut 1
+    //guifont "emphasis" [ guicenter [ guitext "HEADSHOTS" ] ]
+    guistrut -1.75
+    guicenter [ guiimage textures/rewards/headshot [] 3.5 ]
+    guistrut -1.5
+    guicenter [ guitext (format "If your killing shot hits the head, you get a ^fs^fg%1^fS-point bonus for this. Unique among all bonuses, Head Shot is not listed in the obituary line, only as a visual indicator on the screen of the player who scored it (or a pop-up shown above their head, from the other players' perspectives). You can also get a Head Shot message on a team kill, but in this case the message results in no additional points, positive or negative." $headshotpoints) "" -1 - [...]
+    guistrut 1
+    //guifont "emphasis" [ guicenter [ guitext "FIRST BLOOD" ] ]
+    guistrut -1.5
+    guicenter [ guiimage textures/rewards/firstblood [] 3 ]
+    guistrut -1.5
+    guicenter [ guitext (format "This bonus can only be awarded once in a match, providing a ^fs^fg%1^fS-point bonus to the first human player who scores a frag. Frags made by bot players are currently not taken into account for handing out First Blood, because bots (who don't have to load the graphical textures and models for a map) often have a several-second head start on the humans in getting set up." $firstbloodpoints) "" -1 -1 2100 ]
+]
+
+newgui point-penalties [
+    guiheader "point penalties"
+    guifont "emphasis" [ guicenter [ guitext "TEAM KILLS" ] ]
+    guicenter [ guinohitfx [ guiimage textures/warning [] 2 ] 0 "" [] 0xff0000 ]
+    guicenter [ guitext (format "In team games, you have teammates for a reason, so don't kill them! Doing so brings a harsh penalty: ^fs^fr%1^fS points are lost for each team kill. There is only one exception: No points are lost for kills attributed to residual damage (bleeding, electric shock, or burn). Avoid team kills at all costs, as too many will get you automatically kicked and banned from the server!" (* $teamkillpenalty $fragbonus) ) "" -1 -1 2100 ]
+    guistrut 0.25
+    guifont "emphasis" [ guicenter [ guitext "SUICIDE" ] ]
+    guicenter [ guinohitfx [ guiimage textures/dead [] 2 ] ]
+    guicenter [ guitext (format "If you hit yourself with your own weapon and run out of health (this includes exploding yourself with grenades or hitting your own mines), this counts as a team kill and is subject to the above scoring. However, there are other ways to die both accidental and on purpose: jumping into an abyss or off the edge of the map, a lava pit or other map hazard (causing you to collide with invisible death material). Also, you can press %1 at any time to immediately  [...]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "ENEMIES" ] ]
+    guicenter [ guinohitfx [ guiimage textures/modes/onslaught [] 2 ] ]
+    guicenter [ guitext (format "If the Onslaught mutator is selected, and if the chosen map provides for it by placing the appropriate spawn points, a neutral army of grunts, drones, and/or turrets will swarm the arena. These forces (collectively and internally known as 'enemies') do not have a place on the scoreboard, but if one of them administers a frag, the unfortunate victim loses ^fs^fr%1^fS points from their score. Enemies can also be listed for assists, but these are totally inc [...]
+]
+
+newgui combos [
+    guiheader "Combos and killing sprees"
+    guistrut -2
+    guicenter [ guiimage textures/rewards/double [] 4; guiimage textures/rewards/triple [] 4; guiimage textures/rewards/multi [] 4 ]
+    guistrut -2
+    guicenter [ guitext (format "Killing 2, 3 or even 4 players in a time window of ^fs^fg%1^fS seconds is awarded with a double, triple or multi kill bonus for a total of ^fs^fg%2^fS, ^fs^fg%3^fS or ^fs^fg%4^fS points, respectively, in addition to the usual ^fs^fg%5^fS points per frag. Though there are no bonus points awarded for a combination of more than 4 kills in a row, these streak bonuses can be earned multiple times." (*f 0.001 $multikilldelay) $multikillpoints (* $multikillpoint [...]
+    //guifont "emphasis" [ guicenter [ guitext "KILLING SPREES" ] ]
+    guistrut -1.5
+    guicenter [ guiimage textures/rewards/carnage [] 4; guiimage textures/rewards/slaughter [] 4; guiimage textures/rewards/massacre [] 4; guiimage textures/rewards/bloodbath [] 4 ]
+    guistrut -1.5
+    guicenter [ guitext (format "If you manage to get ^fs^fg%1^fS frags in a row (regardless of how long they take) without dying or committing any team kills in between, this will give a message saying that it was 'in total carnage', and you receive a ^fs^fg%2^fS-point bonus. Further spree awards for higher multiples of ^fs^fg%1^fS frags in a row are slaugher, massacre and bloodbath. Every killing spree bonus can only be earned once per player per match. For example, once you get a carn [...]
+    guistrut 1
+    //guifont "emphasis" [ guicenter [ guitext "SPREE BREAKER" ] ]
+    guistrut -1.5
+    guicenter [ guiimage textures/rewards/breaker [] 4 ]
+    guistrut -1.5
+    guicenter [ guitext (format "If you frag an opponent who has an active killing spree, you will earn the Spree Breaker bonus and ^fs^fg%1^fS extra point. Note that it's possible for the player on a killing spree to get team-killed, commit a team-kill, or commit suicide, in which case their spree will come to an end without allowing anyone to score the Spree Breaker." $spreebreaker ) "" -1 -1 2100 ]
+]
+
+newgui dominating [
+    guiheader "dominating and revenge"
+    //guifont "emphasis" [ guicenter [ guitext "DOMINATING" ] ]
+    guistrut -1.5
+    guicenter [ guiimage textures/rewards/dominate [] 4 ]
+    guistrut -1.5
+    guicenter [ guitext (format "If you frag the same opponent ^fs^fg%1^fS times, possibly interspersed with any number of kills on other players, or even with your own deaths (as long as none of them were caused by that opponent), the fifth head-to-head kill in a row will give you a ^fs^fg%2^fS point Dominating bonus. You can have active Dominating streaks on any number of other opponents, getting a point for each one, but there are no further bonuses for any consecutive head-to-head ki [...]
+    guistrut 0.5
+    //guifont "emphasis" [ guicenter [ guitext "REVENGE" ] ]
+    guistrut -1.5
+    guicenter [ guiimage textures/rewards/revenge [] 4 ]
+    guistrut -1.5
+    guicenter [ guitext (format "If an opponent has a Dominating streak against you, track them down and frag them once to earn Revenge and a ^fs^fg%1^fS point bonus, which also causes you to lose your constant sight of them on the radar. After a streak has been broken with Revenge, it's possible to start up a new streak against the same player and score Dominating again. Or maybe the vengeful player will find their fortunes have turned around and they can start up a Dominating streak of [...]
+]
+
+newgui objective-scores [
+    guiheader "Player scores in objective based games"
+    guitext "In objective based team games, frags and bonus elements of the Deathmatch Scoring System have no impact on the team scores. However, this scoring system is still applied to individual player scores, while certain actions related to the team objective give additional bonus points." "" -1 -1 2100
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "CAPTURE-THE-FLAG" ] ]
+    guitext (format "Fragging an opponent who holds your flag: ^fs^fg%1^fS points (including the awarded for the frag). Picking up an opponent's flag: ^fs^fg%2^fS points. Returning your flag to your base: ^fs^fg5^fS points. If the Quick Mutator is active, you can score this simply by touching the flag; note that there is no bonus for picking up your own flag. Capturing an opponent's flag: ^fs^fg%3^fS points. Killing a team mate who carries a flag: ^fs^fr-%4^fS points. " (* 2 $fragbonus)  [...]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "DEFEND-AND-CONTROL" ] ]
+    guitext (format "Fragging an opponent who is defending one of their control points: ^fs^fg%1^fS points (including the awarded for the frag). Standing near a control point of your own color: ^fs^fg%2^fS point every ^fs^fg2.5^fS seconds, the same rate at which it adds to your team score. Killing a team mate that defends a secured control point: ^fs^fr-%3^fS points. In non-King games, you can leave a control point and it will continue to provide points for your team as long as it remain [...]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "BOMBER-BALL" ] ]
+    guitext (format "Fragging an opponent who holds the ball: ^fs^fg%1^fS points (on top of the points already awarded for the frag). Picking up the ball: ^fs^fg%2^fS points. Scoring a goal: ^fs^fg%3^fS points. Scoring an own goal: ^fs^fr-5^fS points, and ^fs^fr-1^fS point to the team score.  Killing a team mate who holds the ball: ^fs^fr-%4^fS points (including the team kill penalty). Note that simply carrying the ball over your base does not count as an own goal; you have to throw the  [...]
+]
+
+newgui various-scores [
+    guiheader "Scoring in Duel, Survivor and Race games"
+    guifont "emphasis" [ guicenter [ guitext "DUEL & SURVIVOR" ] ]
+    guitext "If either one of these modes are selected, regardless of game type, players' scores are replaced by a different system that tracks the number of rounds they've scored in, and thus their contribution to the team score if it's a team game. In a survivor deathmatch, the winning team only receives ^fs^fg1^fS point per round regardless of how many players are still alive, but each surviving player at the end gets a point to their own credit. The one exception is that multi-surviv [...]
+    guistrut 0.5
+    guifont "emphasis" [ guicenter [ guitext "RACE" ] ]
+    guitext "The scoring system in Race games is usually limited to the lap count of each player and team - or the fastest lap time when the Timed mutator is active. Even though deathmatch scoring awards can be earned in Gauntlet Race, they have no effect on a players score." "" -1 -1 2100
+]
diff --git a/config/menus/irc.cfg b/config/menus/irc.cfg
new file mode 100644
index 0000000..e662cb5
--- /dev/null
+++ b/config/menus/irc.cfg
@@ -0,0 +1,68 @@
+ircident = 0
+enterlobby = 0
+
+newgui lobby [
+    guiheader "lobby"
+    if $enterlobby [
+        ircgui freenode
+    ] [
+        guifont "emphasis" [ guicenter [ guitext "RED ECLIPSE CHAT LOBBY" ] ]
+        guistrut 0.5
+        guicenter [ guitext "You are about to enter the main project chat room." ]
+        guistrut 0.5
+        guicenter [ guitext "From there, you can talk with other connected players or ask questions." ]
+        guicenter [ guitext  (format "To access the lobby again at any time, press %1" (dobindsearch "showgui lobby"))]
+        guistrut 0.5
+        guicenter [ guitext "Are you sure you wish to continue?" ]
+        guistrut 0.5
+        guicenter [ guistayopen [
+            guibutton "^fgYES, continue" [enterlobby = 1; ircaddclient freenode irc.freenode.net 6667 (getplayername); ircaddchan freenode #redeclipse ]
+            guispring 1
+            guibutton "^foNO, go back" [cleargui 1]
+        ] ]
+    ]
+]
+
+newgui irc [
+    guistayopen [
+        guitext "saved identities"
+        guislider ircident 0 10
+        guilist [
+            guilist [
+                guitext "network  "
+                guitext "address  "
+                guitext "port     "
+                guitext "nickname "
+                guitext "channel  "
+            ]
+            guilist [
+                guilist [
+                    guifield ircname at ircident -40
+                    guistrut 1
+                    guibutton "connect" [
+                        ircaddclient $[ircname at ircident] $[irchost at ircident] $[ircport at ircident] $[ircnick at ircident]
+                        ircaddchan $[ircname at ircident] $[ircchan at ircident]
+                    ]
+                ]
+                guifield [irchost at ircident] -40
+                guifield [ircport at ircident] -40
+                guifield [ircnick at ircident] -40
+                guilist [
+                    guifield [ircchan at ircident] -40
+                    guistrut 1
+                    guibutton "join" [ ircaddchan $[ircname at ircident] $[ircchan at ircident] ]
+                ]
+            ]
+        ]
+    ]
+    guibar
+    ircgui
+] [
+    if (= $guipasses 0) [
+        ircname0 = freenode
+        irchost0 = irc.freenode.net
+        ircport0 = 6667
+        ircnick0 = (filter (getplayername) 1 1 0)
+        ircchan0 = #redeclipse
+    ]
+]
diff --git a/config/menus/main.cfg b/config/menus/main.cfg
new file mode 100644
index 0000000..5cc70a9
--- /dev/null
+++ b/config/menus/main.cfg
@@ -0,0 +1,116 @@
+bind ESCAPE [if (cleargui 1) [] [showgui main]]    // it all starts here
+
+showmainplayerprev = 1
+setpersist showmainplayerprev 1
+setcomplete showmainplayerprev 1
+
+newgui main [
+    guibox [
+        if $showmainplayerprev [ guicenter [
+            guicenter [ guiplayerpreview (getplayermodel) (getplayercolour -1) (getplayerteam 1) (weapselect) (getplayervanity) [showgui profile] 7.5 1 1]
+            guicenter [ guitext (getplayername) ]
+        ] ]
+    ] [
+        guilist [ guistrut 30 ]
+        guicenter [ guilist [ guifont "emphasis" [
+            guicenter [ guibutton "profile" "showgui profile" ]
+            guistrut 0.5
+            if (&& (isconnected) (> (getvote) 0)) [
+                guicenter [ guibutton "show votes" "showgui maps 2" ]
+                guistrut 0.5
+            ]
+            guicenter [ guibutton (? (isonline) "find servers" "play online") "showservers" ]
+            guicenter [ guibutton (? (isconnected) "vote map/mode" "offline practice") "showgui maps 1" ]
+            guistrut 0.5
+            if (&& (isconnected) (= (gamemode) $modeidxediting)) [
+                guicenter [ guibutton "editing" "showgui edit" ]
+                guistrut 0.5
+            ]
+            guicenter [ guibutton "options" "showgui options" ]
+            guicenter [ guibutton "chat lobby" "showgui lobby" ]
+            guicenter [ guibutton "variables" "showgui vars" ]
+            guicenter [ guibutton "help" "showgui help" ]
+            guistrut 0.5
+            if (isconnected) [
+                guicenter [ guibutton "disconnect" "savewarnchk disconnect" ]
+                guistrut 0.5
+            ]
+            guicenter [ guibutton "quit" "savewarnchk quit" ]
+        ] ] ]
+    ]
+    cases $guirolloveraction "showservers" [
+        guitooltip "display the server browser for online matches"
+    ] "showgui profile" [
+        guitooltip "edit your name, colour, loadout and model type"
+    ] "showgui edit" [
+        guitooltip "access commands to use while editing a map"
+    ] "showgui lobby" [
+        guitooltip "connect to our irc channel or show open irc tabs"
+    ]  "savewarnchk disconnect" [
+        guitooltip "disconnect your current connection"
+    ] "showgui maps 2" [
+        guitooltip "display the current votes"
+    ] "showgui maps 1" [
+        guitooltip (format "select a map/mode combination to %1" (? (isonline) "vote for" "practice on"))
+    ] "showgui options" [
+        guitooltip "edit your configuration options"
+    ] "showgui vars" [
+        guitooltip "search for variables, commands or aliases and edit a plethora of game and client settings."
+    ] "showgui help" [
+        guitooltip "learn more about many aspects of the game"
+    ] "savewarnchk quit" [
+        guitooltip "close Red Eclipse"
+    ]
+    guitip
+]
+
+guimodes = [
+    guicenter [ guitext (modedesc @arg1 @arg2 5) ]
+    if (> $arg2 0) [
+        loop i $mutsidxnum [
+            mut = (<< 1 $i)
+            if (&& [& $arg2 $mut] [! (& (mutsimplied $arg1) $mut)]) [
+                muttxt = (mutsdesc $arg1 $mut 4)
+                if (stringlen $muttxt) [ guicenter [ guitext (concatword "^fa" $muttxt) ] ]
+            ]
+        ]
+    ]
+]
+
+newgui loading [
+    guistayopen [ guinohitfx [
+        guione [
+            guifont "huge" [ guicenter [ guitext (? (&& (isloadingmap) (stringlen $maptitle)) $maptitle "loading") ] ]
+            guifont "default" [ guicenter [ guitext (? (&& (isloadingmap) (stringlen $maptitle) (stringlen $mapauthor)) (concat "by" $mapauthor) "please wait..") ] ]
+            if (&& (isloadingmap) (isconnected)) [
+                if (= (isloadingmap) 2) [
+                    guicenter [ guitext "^fs^fylightmapping^fS" ]
+                ] [
+                    gname = (gamename (gamemode) (mutators) 0 32)
+                    guicenter [ guitext (format "^fs^fy%1^fS" $gname) ]
+                ]
+            ]
+            case (isloadingmap) 2 [
+                guistrut 0.5
+                guicenter [ guiimage "" [] 7 0 ]
+            ] 1 [
+                if (isconnected) [
+                    guistrut 0.5
+                    guifont "little" [
+                        guicenter [
+                            modetexs = (modetexlist (gamemode) (mutators))
+                            looplist modetex $modetexs [ guiimage $modetex [] 1 0 "textures/blank" ]
+                        ]
+                        guimodes (gamemode) (mutators)
+                    ]
+                    guistrut 0.5
+                    if (stringlen $connectname) [
+                        guicenter [ guitext (format "^faon: ^fw%1:[%2]" $connectname $connectport) ]
+                        if (&& (isconnected) (stringlen $serverdesc)) [ guifont "little" [ guicenter [ guitext (format "^"^fs%1^fS^"" $serverdesc) ] ] ]
+                    ]
+                ]
+            ]
+        ]
+        guitip
+    ] ]
+]
diff --git a/config/menus/maps.cfg b/config/menus/maps.cfg
new file mode 100644
index 0000000..b2cf496
--- /dev/null
+++ b/config/menus/maps.cfg
@@ -0,0 +1,558 @@
+resetmapgui = 0
+mapselected = 0
+mutsselected = 0
+modeselected = -1
+lastmode = -1
+lastmuts = 0
+
+mapindex = 0
+lastmode = -1
+lastmuts = 0
+mapnum = -1
+mapcur = ""
+mappth = ""
+maplist = ""
+mappath = ""
+mapextra = ""
+mapimg = ""
+mapocta = 0
+mapfavs = ""
+setpersist mapfavs 1
+setcomplete mapfavs 1
+demofavs = ""
+setpersist demofavs 1
+setcomplete demofavs 1
+resetmapgui = 1
+mapscount = 20
+searchfilter = 0
+
+mapsmenuinit = [
+    modeselected = -1
+    mutsselected = 0
+    mapindex = 0
+    lastmode = -1
+    lastmuts = 0
+    mapnum = -1
+    mapcur = ""
+    mappth = ""
+    maplist = ""
+    mappath = ""
+    mapextra = ""
+    mapimg = ""
+    mapocta = 0
+    resetmapgui = 1
+    nummodes = 0
+    curmode = 0
+    loop g (- $modeidxnum 1) [
+        q = (+ $g 1)
+        if (! (ismodelocked $q)) [ nummodes = (+ $nummodes 1); curmode = $q ]
+    ]
+    if (= $nummodes 1) [ modeselected = $curmode ]
+]
+
+mapsmenuiter = [
+    if (|| [!= $lastmode $modeselected] [!= $lastmuts $mutsselected]) [ resetmapgui = 1 ]
+    if (= $resetmapgui 1) [
+        maprot = 0
+        maplist = ""
+        mappath = ""
+        if (= $modeselected $modeidxdemo) [
+            mutsselected = 0
+            demofavs = (sortlist $demofavs a b [<s $a $b])
+            filelist = (sortlist (listfiles demos dmo) a b [<s $a $b])
+            loop q 2 [
+                looplist lcurmap $filelist [
+                    if (? $q (< (listfind xcurmap $demofavs [stringcmp $xcurmap $lcurmap]) 0) (>= (listfind xcurmap $demofavs [stringcmp $xcurmap $lcurmap]) 0)) [
+                        append maplist $lcurmap
+                        append mappath [demos/@lcurmap]
+                    ]
+                ]
+            ]
+            maprot = (listlen $maplist)
+            append maplist "?"
+            append mappath "?"
+        ] [
+            mutsselected = (mutscheck $modeselected $mutsselected)
+            mapfavs = (sortlist $mapfavs a b [<s $a $b])
+            curlist = (sortlist (? (>= $modeselected $modeidxplay) (getmaplist $modeselected $mutsselected (? (isonline) (listlen (listclients 1)) 0)) $allowmaps) a b [<s $a $b])
+            curprev = (sortlist (sublist $previousmaps 0 $maphistory) a b [<s $a $b])
+            loop q 2 [
+                looplist lcurmap $curlist [
+                    if (< (listfind pcurmap $curprev [stringcmp $pcurmap $lcurmap]) 0) [
+                        if (? $q (< (listfind xcurmap $mapfavs [stringcmp $xcurmap $lcurmap]) 0) (>= (listfind xcurmap $mapfavs [stringcmp $xcurmap $lcurmap]) 0)) [
+                            append maplist $lcurmap
+                            append mappath [maps/@lcurmap]
+                        ]
+                    ]
+                ]
+            ]
+            maprot = (listlen $maplist)
+            wantlist = 1
+            looplist lcurmap $curprev [
+                if $wantlist [
+                    append maplist "~"
+                    append mappath "~"
+                    wantlist = 0
+                ]
+                append maplist $lcurmap
+                append mappath [maps/@lcurmap]
+            ]
+            wantlist = 1
+            filelist = (sortlist (listfiles maps mpz) a b [<s $a $b])
+            loop q 2 [
+                looplist lcurmap $filelist [
+                    if (< (listfind mcurmap $maplist [stringcmp $mcurmap $lcurmap]) 0) [
+                        if (? $q (< (listfind xcurmap $mapfavs [stringcmp $xcurmap $lcurmap]) 0) (>= (listfind xcurmap $mapfavs [stringcmp $xcurmap $lcurmap]) 0)) [
+                            if $wantlist [
+                                append maplist "*"
+                                append mappath "*"
+                                wantlist = 0
+                            ]
+                            append maplist $lcurmap
+                            append mappath [maps/@lcurmap]
+                        ]
+                    ]
+                ]
+            ]
+            if (&& [> $mapocta 0] [> $hasoctapaks 0]) [
+                wantlist = 1
+                filelist = (sortlist (listfiles base ogz) a b [<s $a $b])
+                loop q 2 [
+                    looplist lcurmap $filelist [
+                        if (? $q (< (listfind xcurmap $mapfavs [stringcmp $xcurmap $lcurmap]) 0) (>= (listfind xcurmap $mapfavs [stringcmp $xcurmap $lcurmap]) 0)) [
+                            if $wantlist [
+                                append maplist "."
+                                append mappath "."
+                                wantlist = 0
+                            ]
+                            append maplist $lcurmap
+                            append mappath [base/@lcurmap]
+                        ]
+                    ]
+                ]
+            ]
+            if (&& (= 1 $searchfilter) (> (stringlen $mapextra) 0)) [
+                // apply the search filter and keep special tags $t
+                t = "~*.?"
+                maplist = (listfilter xcurmap $maplist [ (|| (> (stringstr $xcurmap $mapextra) -1) (> (stringstr $t $xcurmap) -1))])
+                mappath = (listfilter xcurmap $mappath [ (|| (> (stringstr $xcurmap $mapextra) -1) (> (stringstr $t $xcurmap) -1))])
+                // if there are no maps between two $t tags, remove the first tag
+                // if no matching maps are available to vote, begin with a "-" tag
+                if (> (stringstr $t (at $maplist 0)) -1) [
+                    xcurmap = "-"
+                    pcurmap = "-"
+                ] [
+                    xcurmap = ""
+                    pcurmap = ""
+                ] 
+                loop i (listlen $maplist) [
+                    if (|| (= (stringstr $t (at $maplist $i)) -1) (= (stringstr $t (at $maplist (+ 1 $i))) -1)) [
+                        append xcurmap (at $maplist $i) 
+                        append pcurmap (at $mappath $i) 
+                    ]
+                ]
+                maplist = $xcurmap
+                mappath = $pcurmap
+            ]
+            append maplist "?"
+            append mappath "?"
+            mapnum = (listfind curmap $maplist [
+                || [=s $curmap $mapcur] [=s [maps/@curmap] $mapcur] [
+                    && [> $hasoctapaks 0] [> $mapocta 0] [=s [base/@curmap] $mapcur]
+                ]
+            ])
+        ]
+        mapcount = (listlen $maplist)
+        if (! $maprot) [ maprot = $mapcount ]
+        lastmode = $modeselected
+        lastmuts = $mutsselected
+        mapindex = 0
+        resetmapgui = 0
+    ]
+    if (|| [< $mapnum 0] [>= $mapnum $mapcount]) [
+        mapnum = -1
+        mapcur = ""
+        mappth = ""
+        mapdmo = ""
+        mapimg = ""
+    ] [
+    if (=s (at $maplist $mapnum) "?") [
+        mapcur = $mapextra
+        if (= $modeselected $modeidxdemo) [
+            mappth = [demos/@mapcur]
+            mapdmo = (demoscan (format "%1.dmo" $mappth))
+            mapimg = (? (>= $mapdmo 0) (format "maps/%1" (demoinfo $mapdmo 1)) "textures/emblem")
+        ] [
+            mappth = $mapextra
+            mapimg = $mapextra
+        ]
+    ] [
+        mapcur = (at $maplist $mapnum)
+        mappth = (at $mappath $mapnum)
+        if (= $modeselected $modeidxdemo) [
+            mapdmo = (demoscan (format "%1.dmo" $mappth))
+            mapimg = (? (>= $mapdmo 0) (format "maps/%1" (demoinfo $mapdmo 1)) "textures/emblem")
+        ] [
+            mapimg = (at $mappath $mapnum)
+        ]
+    ] ]
+]
+
+mutsvar = [
+    local g m s t
+    guistayopen [ guilist [
+        g = $$arg2
+        m = $$arg3
+        s = $$arg5
+        if (< $g 0) [ guinohitfx [ guibutton $arg1 [ disabled = @arg4 ] [] "checkdisable" ] ] [
+            if (|| (& (mutsimplied $g $m) $arg4) (& $mutslockforce $arg4)) [
+                guibutton $arg1 [ implied = @arg4 ] [] "checkboxtwo"
+            ] [
+                if (ismodelocked $g (| $m $arg4) $arg4 $s) [ guinohitfx [ guibutton $arg1 [ disabled = @arg4 ] [] "checkdisable" ] ] [
+                    t = (& $m $arg4)
+                    guibutton $arg1 [
+                        mutate = @arg4
+                        if @t [@@arg3 = @@(mutscheck $g (- $m $arg4))] [
+                            @@arg3 = @@(mutscheck $g (| $m $arg4) $arg4)
+                        ]
+                    ] [] (? $t "checkboxon" "checkbox")
+                ]
+            ]
+        ]
+    ] ]
+]
+
+modevar = [
+    local g m s
+    guistayopen [ guilist [
+        g = $arg4
+        m = $$arg3
+        s = $$arg5
+        if (ismodelocked $g $m 0 $s) [ guinohitfx [ guibutton $arg1 [ disabled = @arg4 ] [] "checkdisable" ] ] [
+            guibutton $arg1 [@arg2 = @arg4] [] (? (= $$arg2 $arg4) "checkboxon" "checkbox")
+        ]
+    ] ]
+]
+
+mapsexec = [
+    sleep 1 [
+        if (isconnected) [ showgui maps 2 ]
+        start @arg1 @arg2 @arg3
+    ]
+]
+
+mapsmenu = [
+    guilist [
+        guistrut 70 1
+        guilist [
+            guilist [
+                guilist [
+                    guistrut 3 1
+                    guilist [
+                        guistrut 38 1
+                        if (< $lastmode 0) [
+                            guifont "emphasis" [ guitext "game select" ]
+                            guitext "please select a mode and map to continue"
+                        ] [
+                            gname = (gamename $modeselected $mutsselected 0 32)
+                            guilist [
+                                guifont "emphasis" [ guitext $gname ]
+                                if (&& (stringlen $mapcur) (= $modeselected $modeidxdemo)) [
+                                    dinfo = (demoscan (format "%1.dmo" $mappth))
+                                    dmode = (demoinfo $dinfo 2)
+                                    dmuts = (demoinfo $dinfo 3)
+                                    dname = (gamename $dmode $dmuts 0 32)
+                                    guicenter [ guifont "little" [ guitext (format " (^fs^fa%1^fS)" $dname) ] ]
+                                ]
+                            ]
+                            guilist [
+                                if (stringlen $mapcur) [
+                                    guitext " ^faselected on ";
+                                    guitext $mapcur
+                                    if (= $modeselected $modeidxdemo) [
+                                        dinfo = (demoscan (format "%1.dmo" $mappth))
+                                        dmapname = (demoinfo $dinfo 1)
+                                        guicenter [ guifont "little" [ guitext (format " (^fs^fa%1^fS)" $dmapname) ] ]
+                                    ]
+                                ] [ guitext "please select a map to continue" ]
+                            ]
+                        ]
+                        guistrut 0.125
+                        guilist [
+                            guispring
+                            guistayopen [ guibutton "^fvpick random" [
+                                pickrandom = 1
+                                nummodes = 0
+                                cntmodes = (- $modeidxnum $modeidxplay)
+                                modeselected = (+ (rnd $cntmodes) $modeidxplay)
+                                while [&& [ismodelocked $modeselected] [<= $nummodes $cntmodes]] [
+                                    modeselected = (+ $modeselected 1)
+                                    if (>= $modeselected $modeidxnum) [ modeselected = $modeidxplay ]
+                                    nummodes = (+ $nummodes 1)
+                                ]
+                                mutsselected = (rnd (+ $mutsbitall 1))
+                                loop g $mutsidxnum [
+                                    q = (<< 1 $g)
+                                    if (ismodelocked $modeselected $q) [ mutsselected = (&~ $mutsselected $q) ]
+                                ]
+                            ] ]
+                            guispring
+                            if (isconnected) [
+                                guistayopen [
+                                    guibutton "^focopy current" [
+                                        copycurrent = 1
+                                        modeselected = (gamemode)
+                                        if (< (mutators) 0) [ mutsselected = 0 ] [ mutsselected = (mutscheck $modeselected (mutators)) ]
+                                    ]
+                                ]
+                                guispring
+                            ]
+                            guistayopen [ guibutton "^fyreset selection" [
+                                resetselection = 1
+                                modeselected = -1
+                                mutsselected = 0
+                                mapnum = -1
+                                mapcur = ""
+                                mappth = ""
+                                mapimg = ""
+                                mapdmo = ""
+                            ] ]
+                            guispring
+                        ]
+                        guistrut 0.125
+                    ]
+                ]
+                guilist [
+                    guilist [
+                        guifont "emphasis" [ guitext "mode" ]
+                        guistrut 0.25
+                        loop z $modeidxnum [
+                            modevar (at $modename $z) modeselected mutsselected $z mapcur
+                        ]
+                    ]
+                    guistrut 3
+                    guilist [
+                        guistayopen [
+                            guiimage $mapimg [if (stringlen $mapcur) [ mapsexec @mapcur @modeselected @mutsselected ] [ conout 1 "you have not selected a map" ] ] 5 1 "textures/emblem"
+                        ]
+                    ]
+                ]
+                guifont "emphasis" [ guitext "mutators" ]
+                guistrut 0.25
+                cnt = (- $mutsidxnum $mutsidxgsn)
+                guiloopsplit n 3 $cnt [
+                    mutsvar (at $mutsname $n) modeselected mutsselected (<< 1 $n) mapcur
+                ] [ guistrut 3 ]
+                if (> (stringlen (gspmutname $modeselected 0)) 0) [
+                    guistrut 0.25
+                    guifont "emphasis" [ guitext "mode specific" ]
+                    guistrut 0.25
+                    guiloopsplit n 3 $mutsidxgsn [
+                        mut = (gspmutname $modeselected $n)
+                        if (stringlen $mut) [
+                            mutsvar $mut modeselected mutsselected (<< 1 (+ $mutsidxgsp $n)) mapcur
+                        ]
+                    ] [ guistrut 3 ]
+                ]
+            ]
+            guistrut 1
+            guilist [
+                guilist [
+                    guitext "available maps:"
+                    if (> $hasoctapaks 0) [
+                        guispring
+                        guistayopen [
+                            guibutton "show sauer maps" [
+                                mapocta = (! $mapocta)
+                                resetmapgui = 1
+                            ] [] (? (> $mapocta 0) "checkboxon" "checkbox")
+                        ]
+                        guispring 1
+                        guitext "fav"
+                        guistrut 3
+                    ]
+                ]
+                guistrut 0.25
+                guilist [
+                    guicontainer [1] [
+                        nummaps = (- (listlen $maplist) 1)
+                        guilist [
+                            guilist [
+                                guistrut $mapscount 1
+                                mapindex = (min (max 0 (- $nummaps $mapscount)) $mapindex) //safeguard
+                                mapnum = (min $mapnum $nummaps)
+                                guilist [
+                                    guistrut 37 1
+                                    break = 0
+                                    loopwhile i $mapscount [= $break 0] [
+                                        q = (+ $mapindex $i)
+                                        curmap = (at $maplist $q)
+                                        cases $curmap "*" [
+                                            guitext "maps not in the rotation:"
+                                        ] "~" [
+                                            guitext "maps played recently:"
+                                        ] "." [
+                                            guitext "maps from sauerbraten:"
+                                        ] "-" [
+                                            guitext "^fanone for this search filter" "textures/info"
+                                        ] "?" [
+                                            break = 1
+                                        ] () [
+                                            guilist [
+                                                guiradio (stringreplace $curmap $mapextra (format "^fs^fy%1^fS" $mapextra)) mapnum $q
+                                                guispring 1
+                                                guistayopen [
+                                                    hasmap = (>= (indexof (? (= $modeselected $modeidxdemo) $demofavs $mapfavs) $curmap) 0)
+                                                    guiimage (? $hasmap "textures/checkboxon" "textures/checkbox") [
+                                                        if (= $modeselected $modeidxdemo) [
+                                                            demofavs = (? @@hasmap (listdel $demofavs @@curmap) (concat (listdel $demofavs @@curmap) @@curmap))
+                                                        ] [
+                                                            mapfavs = (? @@hasmap (listdel $mapfavs @@curmap) (concat (listdel $mapfavs @@curmap) @@curmap))
+                                                        ]
+                                                        resetmapgui = 1
+                                                    ] 0.5 0 "textures/blank"
+                                                ]
+                                            ]
+                                        ]
+                                    ]
+                                ]
+                            ]
+                            guilist [
+                                guitext "custom map or " 
+                                guicheckbox "search filter" searchfilter 1 0 [resetmapgui = 1]
+                            ]
+                            guilist [
+                                mapextraval = $mapextra
+                                mapextranum = $nummaps
+                                guiradio "" mapnum $mapextranum
+                                guifield mapextraval 34 [mapextra = $mapextraval; mapnum = $mapextranum; resetmapgui = 1] -1 0 "" 0 "^fd<enter map name>" $searchfilter
+                            ]
+                        ]
+                        guislider mapindex 0 (max (- $nummaps $mapscount) 0) [] 1 1
+                    ] [
+                        guistrut 40.25
+                        guistrut (+f $mapscount 2) 1
+                    ]
+                ]
+            ]
+        ]
+        guistrut 0.25
+        guilist [
+            if (! (isonline)) [
+                guitext "server type:"; guistrut 0.5
+                guiradio "offline" servertype 0; guistrut 0.5
+                guiradio "private" servertype 1; guistrut 0.5
+                guiradio "public" servertype 2
+            ]
+            guispring 1
+            if (>= $lastmode 0) [
+                if (stringlen $mapcur) [
+                    guifont "default" [
+                        guistayopen [ guibutton (? (isonline) "^fgsubmit this vote" "^fgstart local game") [ mapsexec $mapcur $modeselected $mutsselected ] ]
+                    ]
+                    guistrut 14
+                ] [ guifont "default" [ guitext "^fy.. pick a map to continue .." ]; guistrut 10 ]
+            ] [ guifont "default" [ guitext "^fy.. pick a mode and map to continue .." ]; guistrut 6 ]
+        ]
+        guivisible [
+            cases (at $guirolloveraction 0) "modeselected" [
+                guitooltip (modedesc (at $guirolloveraction 2) $mutsselected 3)
+            ] "mutate" [
+                guitooltip (mutsdesc $modeselected (at $guirolloveraction 2) 3)
+            ] "implied" [
+                guitooltip "this is forced on by the current configuration"
+            ] "disabled" [
+                guitooltip "this is disabled by the current configuration"
+            ]
+            guitip (format "press %1 to open this menu at any time" (dobindsearch "showgui maps 1"))
+        ]
+    ]
+]
+
+voteindex = 0
+votenum = 0
+votemenuinit = [ voteindex = 0 ]
+
+votemenu = [
+    guipage vote 8 74 3.2 [1] (getvote) [
+        voteplayers = (getvote $i 0)
+        votemode = (getvote $i 1)
+        votemuts = (getvote $i 2)
+        votemap = (getvote $i 3)
+        voteself = 0
+        loop j $voteplayers [ if (= (getclientnum) (getvote $i 0 $j)) [ voteself = 1 ] ]
+    ] [1] [
+        guibutton "^fwThere are no votes currently pending, ^fgsubmit ^fwone yourself" [showgui maps 1] [] "chat"
+    ] [
+        guilist [
+            guicheckbox "^fcdynamic sort" sortvotes; guistrut 1.5
+            guicheckbox "^focleanup list" cleanvotes; guistrut 1.5
+            guibutton "^fyclear vote" clearvote
+            guispring
+            guilist [
+                guibackground $guifieldbgcolour $guifieldbgblend $guifieldbordercolour $guifieldborderblend
+                guilist [
+                    guistrut 1
+                    guitext (format "^fc%1 ^fwvote%2" $votenum (? (!= $votenum 1) "s"))
+                    guistrut 1
+                ]
+            ]
+        ]
+        guistrut 0.25
+    ] [
+        guimerge 72 [
+            guicenter [
+                guistrut 4 1
+                votecolour = (? $voteself "^fy" "^fw")
+                guicenter [guifont "default" [guibutton (concatword $votecolour $voteplayers)]]
+                guicenter [guifont "little" [guibutton (format "%1vote%2" $votecolour (? (!= $voteplayers 1) "s"))]]
+            ]
+            voteimage = "textures/chat"
+            votepath = (listfind curmap $maplist [|| [=s $curmap $votemap] [=s [maps/@curmap] $votemap] [
+                && [> $mapocta 0] [> $hasoctapaks 0] [=s [base/@curmap] $votemap]
+            ]])
+            if (> $votepath -1) [ voteimage = (at $mappath $votepath) ]
+            guistrut 1
+            guiimage $voteimage "" 1.5 1 "textures/chat"
+            guistrut 1
+            guicenter [
+                gname = (gamename $votemode $votemuts 0 32)
+                guibutton (format "^fy%1 ^favoted for on ^fo%2" $gname $votemap)
+                guilist [
+                    if (> $voteplayers 0) [
+                        guibutton "^fwby "
+                        guifont "little" [ 
+                            pname = ""
+                            pmore = 0
+                            loop j $voteplayers [
+                                if (|| $pmore (> (guitextwidth $pname) 1220)) [ pmore = (+ $pmore 1) ] [
+                                    append pname (format ["%1"] (getclientname (getvote $i 0 $j) 1))
+                                ]
+                            ]
+                            guibutton (concat (prettylist $pname) (? $pmore (concat "and^fy" $pmore "^fwmore")))
+                        ]
+                    ] [ guibutton "^fano current votes" ]
+                ]
+            ]
+        ] [
+            if (= @voteself 1) [ clearvote ] [ start @@votemap @@votemode @@votemuts ]
+        ]
+    ] [
+        guivisible [ guitip (format "press %1 to open this menu at any time" (dobindsearch "showgui maps 2")) ]
+    ]
+]
+
+newgui maps [
+    octapaks [
+        mapsmenuiter
+        guilist [ mapsmenu ]
+        if (|| (getclientcount) (getvote)) [
+            guitab "votes"
+            guilist [ votemenu ]
+        ]
+    ]
+] [
+    if (= $guipasses 0) [
+        octapaks [ mapsmenuinit; votemenuinit ]
+    ]
+]
diff --git a/config/menus/options.cfg b/config/menus/options.cfg
new file mode 100644
index 0000000..84a90c2
--- /dev/null
+++ b/config/menus/options.cfg
@@ -0,0 +1,1133 @@
+macro resbutton [
+    guibutton "%1x%2" "screenres %1 %2" [] (? (&& [= $scr_w %1] [= $scr_h %2]) "radioboxon" "radiobox")
+]
+
+checkboxdesc = [
+    if (= $arg2 1) [
+        guitext (format "^fa^f(textures/checkboxtwo) %1" $arg1)
+    ] [
+        guitext (format "^fa^f(textures/checkboxon) %1  ^f(textures/checkboxtwo) %2" $arg1 $arg2)
+    ]
+]
+
+newgui options [
+    guione [
+        guifont "emphasis" [
+            guicenter [ guibutton "graphics" [ showgui options_graphics ] ]
+            guicenter [ guibutton "display" [ showgui options_display ] ]
+            guicenter [ guibutton "user interface" [ showgui options_ui ] ]
+            guicenter [ guibutton "sound" [ showgui options_sound ] ]
+            guicenter [ guibutton "controls" [ showgui options_controls ] ]
+            guistrut 0.5
+            guicenter [ guibutton "autoexec" [ showgui options_autoexec ] ]
+        ]
+    ]
+    cases $guirollovername "graphics" [
+        guitooltip "configure graphical quality settings"
+    ] "display" [
+        guitooltip "configure display options and resolution"
+    ] "user interface" [
+        guitooltip "configure user interface and in-game hud settings"
+    ] "sound" [
+        guitooltip "configure sound and music settings"
+    ] "controls" [
+        guitooltip "configure controls, keyboard and mouse settings"
+    ] "autoexec" [
+        guitooltip "modify your autoexec.cfg, which is executed every launch"
+    ]
+    guistatus "select an option to edit your configuration options"
+]
+
+newgui options_graphics [
+    guiheader "graphics"
+    guistrut 67 1
+    guitext "performance key: ^fgfast^f~, ^fymoderate^f~, ^foslow and pretty^f~"
+    guibar
+    guilist [
+        guilist [
+            if (&& [< $shaders 0] [= $renderpath 0]) [
+                guicheckbox     "^fyshaders"            shaders 1 -1
+            ] [
+                guicheckbox     "^fyshaders"            shaders
+            ]
+            guitext "water" blank
+            guitext (? (> $renderpath 0) (result "waterfalls") (result "^fAwaterfalls")) blank
+            if (= $renderpath 0) [
+                guicheckbox     "^foshadow maps"        shadowmap
+                if (>= $maxtmus 3) [
+                    guicheckbox "^fydynamic lights"     ffdynlights 5 0
+                ] [ guinohitfx [ 
+                    guitext "^fAdynamic lights" checkdisable
+                ] ]
+            ] [
+                guicheckbox     "^fosoft shadows"       shadowmap
+                if $glare [
+                    glarepreset = 0
+                    if (= $glarescale 1) [
+                        if (= $blurglare 4) [glarepreset = 1]
+                        if (= $blurglare 7) [glarepreset = 3]
+                    ]
+                    if (= $glarescale 2) [
+                        if (= $blurglare 3) [glarepreset = 2]
+                        if (= $blurglare 7) [glarepreset = 4]
+                    ]
+                    guicheckbox "^foglare"              glare
+                ] [
+                    guicheckbox "^foglare"              glare
+                ]
+            ]
+            if $usetexrect [
+                guicheckbox "^fomotion blur"            motionblur
+            ] [ guinohitfx [ 
+                guitext "^fAmotion blur" checkdisable
+            ] ]
+            guicheckbox "^fograss"                      grass
+            if (> $renderpath 0) [
+                guicheckbox "^fgdynamic lights"         maxdynlights 3 0
+                guicheckbox "^fysoft particles"         depthfx
+            ]
+            guicheckbox "^fgglass reflection"           glassenv
+            guicheckbox "^fgdecals"                     decals
+            guicheckbox "^fgfix t-joints (world sparklies)" filltjoints
+            guitext "textures" blank
+            guitext "models" blank
+            guitext "animation" blank
+        ]
+        guispring 1
+        guibar
+        guispring 1
+        guilist [
+            guilist [   // shaders
+                if $shaders [
+                    guiradio "^fglow detail" shaderdetail 1
+                    guistrut 1
+                    guiradio "^fyhigh detail" shaderdetail 3
+                    //if $hasglsl [
+                    //    guicheckbox "^foGLSL only" forceglsl
+                    //]
+                ] [ guinohitfx [ 
+                    guitext "^fAlow detail" radiodisable
+                    guistrut 1
+                    guitext "^fAhigh detail" radiodisable
+                    //if $hasglsl [
+                    //    guitext "^fAGLSL only" checkdisable
+                    //]
+                ] ]
+            ]
+            guilist [   // water
+                guicheckbox "^fyrefraction" waterrefract
+                guistrut 1
+                guicheckbox "^foreflection" waterreflect
+                guistrut 1
+                guicheckbox "^fgcaustics" caustics
+            ]
+            guilist [   // waterfalls
+                if (> $renderpath 0) [
+                    guicheckbox "^fyrefraction" waterfallrefract
+                    guistrut 1
+                    guicheckbox "^fgreflection" waterfallenv
+                ] [ guinohitfx [ 
+                    guitext "^fArefraction" checkdisable
+                    guistrut 1
+                    guitext "^fAreflection" checkdisable
+                ] ]
+            ]
+            guilist [   // shadows
+                if (= $renderpath 0) [
+                    if $shadowmap [
+                        guiradio "^fymedium quality" shadowmapsize 9 [blurshadowmap 1]
+                        guistrut 1
+                        guiradio "^fohigh quality" shadowmapsize 10 [blurshadowmap 2]
+                    ] [
+                        guicheckbox "^fgblob shadows" blobs
+                    ]
+                ] [
+                    if $shadowmap [
+                        guiradio "^fymedium quality" shadowmapsize 9 [blurshadowmap 1]
+                        guistrut 1
+                        guiradio "^fohigh quality" shadowmapsize 10 [blurshadowmap 2]
+                    ] [
+                        guicheckbox "^fgblob shadows" blobs
+                    ]
+                ]
+            ]
+            guilist [   // glare
+                if (!= $renderpath 0) [
+                    if $glare [
+                        guiradio "^fysubtle" glarepreset 1 [blurglare 4; glarescale 1]
+                        guistrut 1
+                        guiradio "^fyglowy" glarepreset 2 [blurglare 3; glarescale 2]
+                        guistrut 1
+                        guiradio "^fosoft" glarepreset 3 [blurglare 7; glarescale 1]
+                        guistrut 1
+                        guiradio "^fointense" glarepreset 4 [blurglare 7; glarescale 2]
+                    ] [ guinohitfx [ 
+                        guitext "^fAsubtle" radiodisable
+                        guistrut 1
+                        guitext "^fAglowy" radiodisable
+                        guistrut 1
+                        guitext "^fAsoft" radiodisable
+                        guistrut 1
+                        guitext "^fAintense" radiodisable
+                    ] ]
+                ] [ guitext " " ] // spacer
+            ]
+            guilist [   // motion blur
+                if (&& $motionblur $usetexrect) [
+                    guiradio "^fosubtle" motionblurscale 0.5
+                    guistrut 1
+                    guiradio "^fomoderate" motionblurscale 1
+                ] [ guinohitfx [ 
+                    guitext "^fAsubtle" radiodisable
+                    guistrut 1
+                    guitext "^fAmoderate" radiodisable
+                ] ]
+            ]
+            guilist [   // grass
+                if $grass [
+                    guiradio "^fyquick fade" grassdist 128
+                    guistrut 1
+                    guiradio "^fymoderate fade" grassdist 256
+                    guistrut 1
+                    guiradio "^foslow fade" grassdist 512
+                ] [ guinohitfx [ 
+                    guitext "^fAquick fade" radiodisable
+                    guistrut 1
+                    guitext "^fAmoderate fade" radiodisable
+                    guistrut 1
+                    guitext "^fAslow fade" radiodisable
+                ] ]
+            ]
+            guilist [   // dynamic lights
+                if (> $renderpath 0) [
+                    if $maxdynlights [
+                        guiradio "^fgmedium quality" maxdynlights 3
+                        guistrut 1
+                        guiradio "^fyhigh quality" maxdynlights 5
+                    ] [ guinohitfx [ 
+                        guitext "^fAmedium quality" radiodisable
+                        guistrut 1
+                        guitext "^fAhigh quality" radiodisable
+                    ] ]
+                ]
+            ]
+            guilist [   // soft particles
+                if $depthfx [
+                    guiradio "^fglow quality" depthfxsize 7 [depthfxrect 0; depthfxfilter 1; blurdepthfx 1]
+                    guistrut 1
+                    guiradio "^fymedium quality" depthfxsize 10 [depthfxrect 1; depthfxfilter 0; blurdepthfx 0]
+                    guistrut 1
+                    guiradio "^fohigh quality" depthfxsize 12 [depthfxrect 1; depthfxfilter 0; blurdepthfx 0]
+                ] [ guinohitfx [ 
+                    guitext "^fAlow quality" radiodisable
+                    guistrut 1
+                    guitext "^fAmedium quality" radiodisable
+                    guistrut 1
+                    guitext "^fAhigh quality" radiodisable
+                ] ]
+            ]
+            guitext " "  // spacer for glass reflection
+            guilist [   // decals
+                if $decals [
+                    guiradio "^fgmedium quality" maxdecaltris 1024
+                    guistrut 1
+                    guiradio "^fyhigh quality" maxdecaltris 4096
+                ] [ guinohitfx [ 
+                    guitext "^fAmedium quality" radiodisable
+                    guistrut 1
+                    guitext "^fAhigh quality" radiodisable
+                ] ]
+            ]
+            guitext " " // spacer for t-joints
+            guilist [   // textures
+                guiradio "^fglow quality" maxtexsize 256
+                guistrut 1
+                guiradio "^fgmedium quality" maxtexsize 512
+                guistrut 1
+                guiradio "^fyhigh quality" maxtexsize 0
+            ]
+            guilist [   // models
+                guicheckbox "^fglighting" lightmodels
+                guistrut 1
+                guicheckbox "^fgreflection" envmapmodels
+                guistrut 1
+                guicheckbox "^fgglow" glowmodels
+                if (> $renderpath 0) [
+                    guicheckbox "^fybumpmap" bumpmodels
+                ] [ guinohitfx [ 
+                    guitext "^fAbumpmap" checkdisable
+                ] ]
+            ]
+            guilist [   // animation
+                guiradio "^fgmedium quality" matskel 1
+                guistrut 1
+                guiradio "^fghigh quality" matskel 0
+            ]
+        ]
+    ]
+
+    guitab postfx
+    guistayopen [
+        guibutton "(effect OFF)" "clearpostfx"
+        guibutton "bloom (subtle: 30%)" "bloom 0.3"
+        guibutton "bloom (bright: 55%)" "bloom 0.55"
+        guibutton "bloom (intense: 80%)" "bloom 0.8"
+        guibutton "rotoscope" "rotoscope 1"
+        guibutton "rotoscope + blur3" "rotoscope 1 1"
+        guibutton "rotoscope + blur5" "rotoscope 1 2"
+        guibutton "sharpen" "setpostfx sharpen"
+        guibutton "sobel" "setpostfx sobel"
+        guibutton "invert" "setpostfx invert"
+        guibutton "gbr" "setpostfx gbr"
+        guibutton "bw" "setpostfx bw"
+        guibutton "blur3" "setpostfx hblur3; addpostfx vblur3"
+        guibutton "blur5" "setpostfx hblur5; addpostfx vblur5"
+    ]
+]
+
+newgui options_display [
+    guiheader "display"
+    guistrut 50 1
+    guilist [
+        guitext "v-sync: "
+        guiradio "on " vsync 1
+        guiradio "off " vsync 0
+        guiradio "set by display driver" vsync -1
+    ]
+    guilist [
+        guitext "fullscreen: "
+        guiradio "on " fullscreen 1
+        guiradio "off " fullscreen 0
+    ]
+    guistrut 1
+    guitext "gamma (default: 100)"
+    guislider gamma
+    guitext "full-scene anti-aliasing (default: -1)"
+    guilistslider fsaa "-1 0 2 4 8 16"
+    guitext "color depth (default: 0)"
+    guilistslider colorbits "0 8 16 24 32"
+    guitext "z-buffer depth (default: 0)"
+    guilistslider depthbits "0 8 16 24 32"
+    guitext "stencil bits (default: 0)"
+    guislider stencilbits
+    guitext "anisotropic filtering (default: 0)"
+    guilistslider aniso "0 2 4 8 16"
+
+    guitab "resolution"
+    guistayopen [
+        guilist [
+            guilist [
+                guifont "emphasis" [ guitext "^fa4:3" ]
+                @(resbutton 320 240)
+                @(resbutton 640 480)
+                @(resbutton 800 600)
+                @(resbutton 1024 768)
+                @(resbutton 1152 864)
+                @(resbutton 1280 960)
+                @(resbutton 1400 1050)
+                @(resbutton 1600 1200)
+                @(resbutton 1792 1344)
+                @(resbutton 1856 1392)
+                @(resbutton 1920 1440)
+                @(resbutton 2048 1536)
+                @(resbutton 2800 2100)
+                @(resbutton 3200 2400)
+            ]
+            guistrut 0.5
+            guibar
+            guistrut 0.5
+            guilist [
+                guifont "emphasis" [ guitext "^fa16:10" ]
+                @(resbutton 320 200)
+                @(resbutton 640 400)
+                @(resbutton 1024 640)
+                @(resbutton 1280 800)
+                @(resbutton 1440 900)
+                @(resbutton 1600 1000)
+                @(resbutton 1680 1050)
+                @(resbutton 1920 1200)
+                @(resbutton 2048 1280)
+                @(resbutton 2560 1600)
+                @(resbutton 2880 1800)
+                @(resbutton 3200 2000)
+                @(resbutton 3840 2400)
+            ]
+            guistrut 0.5
+            guibar
+            guistrut 0.5
+            guilist [
+                guifont "emphasis" [ guitext "^fa16:9" ]
+                @(resbutton 1024 600)
+                @(resbutton 1280 720)
+                @(resbutton 1366 768)
+                @(resbutton 1600 900)
+                @(resbutton 1920 1080)
+                @(resbutton 2048 1152)
+                @(resbutton 2560 1440)
+                @(resbutton 2880 1620)
+                @(resbutton 3200 1800)
+                @(resbutton 3840 2160)
+            ]
+            guistrut 0.5
+            guibar
+            guistrut 0.5
+            guilist [
+                guifont "emphasis" [ guitext "^fa5:4" ]
+                @(resbutton 600 480)
+                @(resbutton 1280 1024)
+                @(resbutton 1600 1280)
+                @(resbutton 2560 2048)
+            ]
+            guistrut 0.5
+            guibar
+            guistrut 0.5
+            guilist [
+                guifont "emphasis" [ guitext "^fa5:3" ]
+                @(resbutton 800 480)
+                @(resbutton 1280 768)
+                guibar
+                guitext "Custom"
+                guilist [
+                    customw = $scr_w
+                    customh = $scr_h
+                    guifield customw 4 [scr_w $customw]
+                    guifield customh 4 [scr_h $customh]
+                ]
+            ]
+        ]
+    ]
+]
+
+newgui options_ui [
+    guiheader "hud"
+    guistayopen [
+        guistrut 40 1
+        guifont "emphasis" [ guitext "^faradar" ]
+        guibar
+        guistrut 0.5
+        guilist [
+            guilist [
+                guicheckbox2 "show radar" showradar 1
+                guicheckbox2 "show items" radaritems 1
+                guicheckbox2 "show players" radarplayers 1
+            ]
+            guistrut 2
+            guilist [
+                guicheckbox2 "show affinities" radaraffinity 1
+                guicheckbox2 "show item names" radaritemnames 1
+                guicheckbox2 "show player names" radarplayernames 1
+            ]
+        ]
+        guistrut 0.3
+        guifont "little" [ guitext "^t^fakey:  ^f(textures/checkboxon) not visible in SpecTV   ^f(textures/checkboxtwo) visible always" ]
+        guistrut 0.5
+        guitext "radar style ^fa(default: compass-distance)"
+        guilist [
+            guiradio2 "compass"             radarstyle 0
+            guistrut 1
+            guiradio2 "compass-distance"    radarstyle 1
+            guistrut 1
+            guiradio2 "full-hud"            radarstyle 2
+            guistrut 1
+            guiradio2 "corner"              radarstyle 3
+        ]
+        guistrut 0.5
+        guilist [
+            guitext "radar opacity:"
+            guispring 1
+            guilist [
+                guistrut 25 1
+                radarblendstorage = (*f $radarblend 100)
+                guislider radarblendstorage 0 100 [ radarblend (divf $radarblendstorage 100) ]
+            ]
+        ]
+        guifont "little" [ guitext "^fadefault: 100%" ]
+        guistrut 1
+        guifont "emphasis" [ guitext "^facrosshair" ]
+        guibar
+        guistrut 0.5
+        guilist [
+            guilist [
+                guicheckbox2 "show crosshair" showcrosshair 1
+                guicheckbox2 "weapon crosshairs" crosshairweapons 1
+                guicheckbox2 "critical flash" crosshairflash
+        guistrut 0.5
+        guitext "weapon ammo/clip surrounding crosshair:"
+        guilist [
+            guiradio2 "no visible clip"             showclips 0
+            guistrut 1
+            guiradio2 "solid bar of clip"    showclips 1
+            guistrut 1
+            guiradio2 "individual clip"            showclips 2
+        ]
+        guistrut 0.5
+            ]
+            guispring 1
+            guilist [
+                guilist [
+                    guispring 1
+                    guiimage $crosshairtex [showgui options_xhair] 2 1 [] [crosshairtex "crosshairs/cross-01"]
+                ]
+                guilist [
+                    guispring 1
+                    guifont "little" [ guitext (stringreplace $crosshairtex "crosshairs/" "") ]
+                ]
+            ]
+        ]
+        guifont "emphasis" [ guitext "^fastats" ]
+        guibar
+        guistrut 0.5
+        guilist [
+            guilist [
+                guicheckbox2 "names above head" aboveheadnames
+                guicheckbox2 "team above head" aboveheadteam
+                guicheckbox2 "damage above head" aboveheaddamage
+            ]
+            guistrut 2
+            guilist [
+                guicheckbox2 "impulse meter" inventoryimpulse 1
+                guicheckbox2 "velocity meter" inventoryvelocity 1
+            ]
+            guistrut 2
+            guilist [
+                guibitfield "spectator input" inventoryinput 2
+                guibitfield "own input" inventoryinput 1
+            ]
+        ]
+        guistrut 0.5
+        guilist [
+            guitext "top scores:"
+            guispring 1
+            guilist [
+                guistrut 25 1
+                guislider inventoryscore 1 8
+            ]
+        ]
+        guifont "little" [ guitext "^fadefault: 1" ]
+        guistrut 1
+        guifont "little" [
+            cases $guirollovername "show radar" [
+                guitext "toggles visibility of the radar completely"
+            ] "show affinities" [
+                guitext "toggles visibility of affinities (bomb, flag)"
+            ] "show items" [
+                guitext "toggles visibility of items"
+            ] "show item names" [
+                guitext "toggles visibility of item names"
+            ] "show players" [
+                guitext "toggles visibility of players"
+            ] "show player names" [
+                guitext "toggles visibility of player names"
+            ] "compass" [
+                guitext "compass-like radar showing direction of targets (classic style)"
+            ] "compass-distance" [
+                guitext "compass-like radar showing direction and relative distance of targets"
+            ] "full-hud" [
+                guitext "compass-distance style radar with affinities overlaid onto hud like beacons"
+            ] "corner" [
+                guitext "top-down view of map with targets' actual positions"
+            ] "show crosshair" [
+                guitext "toggles visibility of crosshair"
+                checkboxdesc "blends crosshair based on accuracy" 1
+            ] "weapon crosshairs" [
+                guitext "uses weapon-specific crosshairs
+                checkboxdesc "apply weapon color to crosshair" 1
+            ] "critical flash" [
+                guitext "crosshair flashes red at critical health"
+            ] "show clips" [
+                guitext "toggles visibility of weapon clip around crosshair"
+            ] "showgui options_xhair" [
+                guitext "click to change crosshair texture, opacity and sizing"
+            ] "impulse meter" [
+                guitext "toggles visibility of impulse meter"
+                checkboxdesc "textual percentage" "graphical bar"
+            ] "velocity meter" [
+                guitext "toggles visibility of velocity meter"
+                checkboxdesc "visible only in time race" "always visible"
+            ] "spectator input" [
+                guitext "toggles visibility of keyboard input when watching other players"
+            ] "own input" [
+                guitext "toggles visibility of your own keyboard input when playing"
+            ] "damage above head" [
+                guitext "toggles visibility of damage received above a player's head"
+            ] "names above head" [
+                guitext "toggles visibility of player names above head"
+            ] "team above head" [
+                guitext "toggles visibility of player team above head"
+            ] () [
+                guitext "hover over an item for a description"
+                guitext " "
+            ]
+        ]
+    ]
+
+    guitab "scoreboard"
+    guistayopen [
+        guifont "emphasis" [ guitext "^facustomize scoreboard layout" ]
+        guibar
+        guistrut 0.5
+        guilist [
+            guilist [
+                guicheckbox2 "show frags" scorefrags 1
+                guicheckbox2 "show points" scorepoints
+                guicheckbox2 "show status icons" scoreicons
+                guicheckbox2 "show spectators" scorespectators
+                guicheckbox2 "show account handle" scorehandles
+            ]
+            guistrut 2
+            guilist [
+                guicheckbox2 "show client numbers" scoreclientnum
+                guicheckbox2 "show host info" scorehostinfo
+                guicheckbox2 "show ping" scoreping
+                guicheckbox2 "show packet jump" scorepj
+                guicheckbox2 "show bot info" scorebotinfo
+            ]
+        ]
+        guistrut 1
+        guifont "emphasis" [ guitext "^faname hilight color" ]
+        guibar
+        guistrut 0.5
+        guilist [
+            scolr = (& (>> $scolrval 16) 0xFF)
+            scolg = (& (>> $scolrval 8) 0xFF)
+            scolb = (& $scolrval 0xFF)
+            guilist [
+                guibackground $scolrval
+                guistrut 6 1
+                guistrut 3
+            ]
+            guistrut 1
+            guilistsplit n 3 "0 1 2" [
+                guilist [
+                    guistrut 30 1
+                    guislider scol@(at "r g b" $n) 0 255 [
+                        scolrval = (| (| (<< $scolr 16) (<< $scolg 8)) $scolb)
+                        scorehilight $scolrval
+                    ] 0 1 0x@(at "FF0000 00FF00 0000FF" $n)
+                ]
+            ]
+        ]
+        guistrut 1
+        guibutton "reset default" [ resetvar scorehilight; scolrval = $scorehilight ]
+        guistrut 2
+        guifont "little" [
+            cases $guirollovername "show frags" [
+                guitext "toggles visibility of frag count"
+                checkboxdesc "only visible in deathmatch" "visible in all modes"
+            ] "show points" [
+                guitext "toggles visibility of total points"
+            ] "show laps" [
+                guitext "toggles visibilty of lap count"
+                checkboxdesc "only visible in lap modes" "visible in race"
+            ] "show status icons" [
+                guitext "toggles visibility of status icons (dead, alive, queued)"
+            ] "show spectators" [
+                guitext "toggles visibility of spectating players"
+            ] "show account handle" [
+                guitext "toggles visibility of player account handles"
+            ] "show client numbers" [
+                guitext "toggles visibility of player client numbers (cn)"
+            ] "show host info" [
+                guitext "toggles visibility of player hostnames"
+            ] "show ping" [
+                guitext "toggles visibility of player latency (ping)"
+            ] "show packet jump" [
+                guitext "toggles visibility of packet jump measurement"
+            ] "show bot info" [
+                guitext "toggles visibility of bot skill levels and their owner"
+            ]
+        ]
+    ]
+
+    guitab "console"
+    guicheckbox "centered console" concenter
+    guistrut 0.5
+    guilist [
+        guilist [
+            guilist [
+                guitext "passive lines:"
+                guistrut 1
+                guifield consize 3
+            ]
+        ]
+        guistrut 2
+        guilist [
+            guilist [
+                guitext "overflow lines:"
+                guistrut 1
+                guifield conoverflow 3
+            ]
+        ]
+    ]
+    guistrut 0.5
+    guilist [
+        guitext "font scale:"
+        guistrut 1
+        guifield conscale 5
+    ]
+    guistrut 0.5
+    guilist [
+        guitext "console opacity:"
+        guispring 1
+        guilist [
+            guistrut 25 1
+            conblendstorage = (*f $conblend 100)
+            guislider conblendstorage 0 100 [ conblend (divf $conblendstorage 100) ]
+        ]
+    ]
+    guifont "little" [ guitext "^fadefault: 60%" ]
+    guistrut 0.5
+    guitext "obituaries"
+    guilist [
+        guilist [
+            guiradio2 "off"                                 showobituaries 0
+            guiradio2 "only me"                             showobituaries 1
+            guiradio2 "me + announcements"                  showobituaries 2
+            guiradio2 "announcements; exclude dying bots"   showobituaries 3
+            guiradio2 "announcements; exclude bot vs. bot"  showobituaries 4
+            guiradio2 "everything"                          showobituaries 5
+        ]
+    ]
+    guistrut 1
+    guicheckbox "show obituary distances" showobitdists
+    guicheckbox "show obituary health left" showobithpleft
+    guistrut 1
+    guifont "emphasis" [ guitext "^fachat console" ]
+    guibar
+    guistrut 0.5
+    guilist [
+        guistrut 0.5
+        guicheckbox "show chat colors" colourchat [ON]
+    ]
+    guistrut 0.5
+    guilist [
+        guilist [
+            guilist [
+                guitext "passive lines:"
+                guistrut 1
+                guifield chatconsize 3
+            ]
+        ]
+        guistrut 2
+        guilist [
+            guilist [
+                guitext "overflow lines:"
+                guistrut 1
+                guifield chatconoverflow 3
+            ]
+        ]
+    ]
+    guistrut 0.5
+    guilist [
+        guitext "font scale:"
+        guistrut 1
+        guifield chatconscale 5
+    ]
+    guistrut 0.5
+    guilist [
+        guitext "console opacity:"
+        guispring 1
+        guilist [
+            guistrut 25 1
+            chatconblendstorage = (*f $chatconblend 100)
+            guislider chatconblendstorage 0 100 [ chatconblend (divf $chatconblendstorage 100) ]
+        ]
+    ]
+    guifont "little" [ guitext "^fadefault: 100%" ]
+    guistrut 0.5
+    guilist [
+        guitext "text blink rate (ms):"
+        guispring 1
+        guilist [
+            guistrut 25 1
+            guilistslider textblinking "0 50 100 150 200 250 300 350 400 450 500 550 600 650 700 750 800 850 900 950 1000"
+        ]
+    ]
+    guifont "little" [ guitext "^fadefault: 350ms; 0 = off" ]
+    guistrut 0.5
+    guilist [
+        guitext "text intensity:"
+        guispring 1
+        guilist [
+            guistrut 25 1
+            guislider textminintensity
+        ]
+    ]
+    guifont "little" [ guitext "^fadefault: 32" ]
+
+    guitab "misc"
+    guifont "emphasis" [ guitext "^faborders" ]
+    guibar
+    guilist [
+        guitext "playing"
+        guispring 1
+        guilist [
+            guistrut 5 1
+            guislider playborder 0 3
+        ]
+        guistrut 1
+        guitext "size:"
+        guistrut 0.5
+        guilist [
+            guistrut 22 1
+            _playbordersize = (*f $playbordersize 20)
+            guislider _playbordersize 0 20 [ playbordersize (divf $_playbordersize 20) ]
+        ]
+    ]
+    guilist [
+        guitext "editing"
+        guispring 1
+        guilist [
+            guistrut 5 1
+            guislider editborder 0 3
+        ]
+        guistrut 1
+        guitext "size:"
+        guistrut 0.5
+        guilist [
+            guistrut 22 1
+            _editbordersize = (*f $editbordersize 20)
+            guislider _editbordersize 0 20 [ editbordersize (divf $_editbordersize 20) ]
+        ]
+    ]
+    guilist [
+        guitext "spectator"
+        guispring 1
+        guilist [
+            guistrut 5 1
+            guislider specborder 0 3
+        ]
+        guistrut 1
+        guitext "size:"
+        guistrut 0.5
+        guilist [
+            guistrut 22 1
+            _specbordersize = (*f $specbordersize 20)
+            guislider _specbordersize 0 20 [ specbordersize (divf $_specbordersize 20) ]
+        ]
+    ]
+    guilist [
+        guitext "waiting"
+        guispring 1
+        guilist [
+            guistrut 5 1
+            guislider waitborder 0 3
+        ]
+        guistrut 1
+        guitext "size:"
+        guistrut 0.5
+        guilist [
+            guistrut 22 1
+            _waitbordersize = (*f $waitbordersize 20)
+            guislider _waitbordersize 0 20 [ waitbordersize (divf $_waitbordersize 20) ]
+        ]
+    ]
+    guilist [
+        guitext "background"
+        guispring 1
+        guilist [
+            guistrut 5 1
+            guislider backgroundborder 0 3
+        ]
+        guistrut 1
+        guitext "size:"
+        guistrut 0.5
+        guilist [
+            guistrut 22 1
+            _backgroundbordersize = (*f $backgroundbordersize 20)
+            guislider _backgroundbordersize 0 20 [ backgroundbordersize (divf $_backgroundbordersize 20) ]
+        ]
+    ]
+    guistrut 1
+    guifont "emphasis" [ guitext "^fatext info" ]
+    guibar
+    guilist [
+        guilist [
+            guitext "fps display:"
+            guiradio2 "off" showfps 0
+            guiradio2 "number only" showfps 1
+            guiradio2 "verbose" showfps 2
+            guiradio2 "more verbose" showfps 3
+        ]
+        guistrut 3
+        guilist [
+            guitext "notice messages:"
+            guiradio2 "off" shownotices 0
+            guiradio2 "basic notices" shownotices 1
+            guiradio2 "level 2" shownotices 2
+            guiradio2 "level 3" shownotices 3
+            guiradio2 "everything" shownotices 4
+        ]
+    ]
+    guistrut 1
+    guifont "emphasis" [ guitext "^famenus" ]
+    guibar
+            guicheckbox "show tooltips" guitooltips [ON]
+            guicheckbox "show game hints" guistatusline [ON]
+            guicheckbox "show client GPU info" showloadinggpu [ON]
+            guicheckbox "show versioning info" showloadingversion [ON]
+            guicheckbox "show official site link" showloadingurl [ON]
+] [
+    if (= $guipasses 0) [
+        scolrval = $scorehilight
+    ]
+]
+
+newgui options_xhair [
+    guiheader "crosshair"
+    guistayopen [
+        guicenter [
+            guibutton "main" [ xhairprefix = "crosshair" ] [] (? (=s $xhairprefix "crosshair") "radioboxon" "radiobox")
+            guistrut 1
+            guibutton "hit"  [ xhairprefix = "hithair" ] [] (? (=s $xhairprefix "hithair") "radioboxon" "radiobox")
+            guistrut 1
+            guibutton "zoom" [ xhairprefix = "zoomcrosshair" ] [] (? (=s $xhairprefix "zoomcrosshair") "radioboxon" "radiobox")
+            guistrut 1
+            guibutton "team" [ xhairprefix = "teamcrosshair" ] [] (? (=s $xhairprefix "teamcrosshair") "radioboxon" "radiobox")
+        ]
+        xhairtex = (concatword $xhairprefix "tex")
+        xhairblend = (concatword $xhairprefix "blend")
+        xhairblendstorage = (concatword $xhairprefix "blendstorage")
+        xhairsize = (concatword $xhairprefix "size")
+
+        guistrut 0.5
+        guilist [
+            guistrut $crosshairsboxlen 1
+            numcrosshairs = (listlen $crosshairlist)
+            crosshairindex = (min (max 0 (- $numcrosshairs $crosshairsboxlen)) $crosshairindex)
+            guilist [
+                guistrut 35 1
+                loop i $crosshairsboxlen [
+                    q = (+ $crosshairindex $i)
+                    curcrosshair = (at $crosshairlist $q)
+                    crosshairname = (concatword "crosshairs/" $curcrosshair)
+                    guibody [
+                        guilist [
+                            guistrut 35 1
+                            guistrut 0.3
+                            guilist [
+                                guiimage $crosshairname [] 1.5 1
+                                guistrut 1
+                                guilist [
+                                    guistrut 1
+                                    guibutton (format "%1%2" (? (=s $$xhairtex $crosshairname) "^fy" "^fa") $curcrosshair)
+                                ]
+                            ]
+                            guistrut 0.3
+                        ]
+                    ] [ ($xhairtex @crosshairname) ]
+                ]
+            ]
+            guislider crosshairindex 0 (max 0 (- $numcrosshairs $crosshairsboxlen)) [] 1 1
+        ]
+        guistrut 1
+        if (&& (!=s $xhairprefix "hithair") (!=s $xhairprefix "teamcrosshair")) [
+            guilist [
+                guitext "crosshair opacity:"
+                guispring 1
+                guilist [
+                    guistrut 20 1
+                    $xhairblendstorage = (*f $$xhairblend 100)
+                    guislider $xhairblendstorage 0 100 [ ($xhairblend (divf $$xhairblendstorage 100)) ]
+                ]
+            ]
+            guifont "little" [ guitext "^fadefault: 100%" ]
+            guistrut 0.5
+            guilist [
+                guitext "crosshair size:"
+                guistrut 1
+                guifield $xhairsize 6
+            ]
+            guifont "little" [ guitext "^fadefault: 0.04" ]
+        ] [ guistrut 4.5 ]
+    ]
+] [
+    if (= $guipasses 0) [
+        crosshairlist = ""
+        crosshairsboxlen = 5
+        xhairprefix = "crosshair"
+        xhairtex = (concatword $xhairprefix "tex")
+        xhairblend = (concatword $xhairprefix "blend")
+        xhairblendstorage = (concatword $xhairprefix "blendstorage")
+        xhairsize = (concatword $xhairprefix "size")
+
+        loopfiles crosshairs crosshairs png [
+            crosshairlist = (concat $crosshairlist $crosshairs)
+        ]
+        crosshairlist = (sortlist $crosshairlist a b [<s $a $b])
+        crosshairindex = (max 0 (listfind mcurcrosshair $crosshairlist [stringcmp (concatword "crosshairs/" $mcurcrosshair) $$xhairtex]))
+    ]
+]
+
+newgui options_sound [
+    guiheader   "sound"
+    guilist [
+        guilist [
+            guistrut 70 1
+            guitext "master volume"
+            guislider mastervol
+
+            guitext "sound volume"
+            guislider soundvol
+            guitext "music volume"
+            guislider musicvol
+            guilist [ guitext "allocated sound channels"; guifont "little" [ guitext "(allocation, NOT the number of speakers)" ] ]
+            guislider soundachans
+            guitext "sound frequency"
+            guilistslider soundfreq "11025 22050 44100"
+            guitext "sound buffer length"
+            guislider soundbufferlen
+        ]
+    ]
+    guistrut 1
+    guilist [
+        guilist [
+            guitext "in-game music:"
+            guitext "edit mode music:"
+        ]
+        guistrut 2
+        guilist [
+            guilist [
+                guiradio "off" 	    musictype 0
+                guistrut 1
+                guiradio "mapmusic" musictype 1
+                guistrut 1
+                guiradio "random"   musictype 2
+            ]
+            guilist [
+                guiradio "off" 	    musicedit 0
+                guistrut 1
+                guiradio "mapmusic" musicedit 1
+                guistrut 1
+                guiradio "random"   musicedit 2
+            ]
+        ]
+    ]
+    guistrut 1
+    guicheckbox "mumble positional audio" mumble
+    guicheckbox "play map-specific sounds" soundenvvol [1.0 0]
+    guistrut 1
+    guibutton "^fyopen sound debugger" [showgui cursounds]
+]
+
+newgui options_autoexec [
+    guiheader "autoexec"
+    guitext "autoexec.cfg"
+    showfileeditor "autoexec.cfg" -80 20
+]
+
+bindtypes = [ default spectator editing waiting ]
+bindcalls = [ "" spec edit wait ]
+
+bindactions = [
+    forward backward left right "universaldelta 1" "universaldelta -1" "spectator 1" "spectator 0"
+    "primary" "secondary" "reload" "use" "jump" "walk" "crouch" "special" "drop" "affinity"
+    "weapon 1" "weapon 2" "weapon 3" "weapon 4" "weapon 5" "weapon 6" "weapon 7" "weapon 8" "weapon 9" "weapon 10"
+    "saycommand /" "saytextcommand (getsaycolour)" "sayteamcommand (getsaycolour)" toggleconsole edittoggle thirdpersonswitch screenshot
+    addbot delbot "showgui maps 1" "showgui maps 2" "setpriv 1" "showgui profile 2" "showgui team"
+    "showcompass voice" "showcompass team"
+]
+bindtitles = [
+    forward backward left right "scroll up" "scroll down" "enter spectator" "exit spectator"
+    "primary" "secondary" "reload" "use" "jump" "walk" "crouch" "parkour/kick" "drop" "flag/bomb"
+    "weap slot 1" "weap slot 2" "weap slot 3" "weap slot 4" "weap slot 5" "weap slot 6" "weap slot 7" "weap slot 8" "weap slot 9" "weap slot 10"
+    "cmd input" "all chat" "team chat" "toggle console" "toggle editing" "toggle thirdperson" "screenshot"
+    "add bot" "delete bot" "maps menu" "maps voting" "claim privileges" "loadout menu" "team menu"
+    "voice compass" "team compass"
+]
+
+specbindactions = [
+    forward backward left right "universaldelta 1" "universaldelta -1" "spectator 1" "spectator 0"
+    "saycommand /" "saytextcommand (getsaycolour)" "sayteamcommand (getsaycolour)" toggleconsole edittoggle thirdpersonswitch screenshot
+    addbot delbot "showgui maps 1" "showgui maps 2" "setpriv 1" "showgui profile 2" "showgui team"
+    "showcompass voice" "showcompass team"
+]
+specbindtitles = [
+    forward backward left right "scroll up" "scroll down" "enter spectator" "exit spectator"
+    "cmd input" "all chat" "team chat" "toggle console" "toggle editing" "toggle thirdperson" "screenshot"
+    "add bot" "delete bot" "maps menu" "maps voting" "claim privileges" "loadout menu" "team menu"
+    "voice compass" "team compass"
+]
+
+waitbindactions = [
+    forward backward left right "universaldelta 1" "universaldelta -1" "spectator 1" "spectator 0"
+    "saycommand /" "saytextcommand (getsaycolour)" "sayteamcommand (getsaycolour)" toggleconsole edittoggle thirdpersonswitch screenshot
+    addbot delbot "showgui maps 1" "showgui maps 2" "setpriv 1" "showgui profile 2" "showgui team"
+    "showcompass voice" "showcompass team"
+]
+waitbindtitles = [
+    forward backward left right "scroll up" "scroll down" "enter spectator" "exit spectator"
+    "cmd input" "all chat" "team chat" "toggle console" "toggle editing" "toggle thirdperson" "screenshot"
+    "add bot" "delete bot" "maps menu" "maps voting" "claim privileges" "loadout menu" "team menu"
+    "voice compass" "team compass"
+]
+
+editbindactions = [
+    forward backward left right "universaldelta 1" "universaldelta -1" "spectator 1" "spectator 0"
+    "saycommand /" "saytextcommand (getsaycolour)" "sayteamcommand (getsaycolour)" toggleconsole edittoggle screenshot
+    "showtexgui" "showgui editing" "remip" "fullbright 0; patchlight" "fullbright 0; calclight -1" "fullbright 0; calclight 1"
+    savemap "changeoutline 1"
+]
+editbindtitles = [
+    forward backward left right "scroll up" "scroll down" "enter spectator" "exit spectator"
+    "cmd input" "all chat" "team chat" "toggle console" "toggle editing" "screenshot"
+    "texture menu" "editing menu" "remip" "patch lights" "quick calclight" "full calclight"
+    "save map" "change outline"
+]
+
+curbindtype = 0
+curbindshow = 0
+
+newgui options_controls [
+    guiheader  "keys"
+    guilist [
+        guilist [ guistrut 28 ]
+        guilist [
+            guitext "basic keybinds, for anything more use the 'bind' command"
+            guitext "select action to bind and press desired keys (ESC when done):"
+            guistrut 1
+            guilist [
+                guitext "state:"
+                guistrut 1
+                loop i 4 [ guiradio (at $bindtypes $i) curbindtype $i; guistrut 1 ]
+                guicheckbox "show default bind if not found" curbindshow
+            ]
+            guistrut 1
+            curbindsign = (at $bindcalls $curbindtype)
+            curbindcall = (concatword $curbindsign bind)
+            curbindacts = (concatword $curbindsign bindactions)
+            curbindtits = (concatword $curbindsign bindtitles)
+            curbindsrch = (concatword search $curbindsign binds)
+            guilistsplit n 2 $[@curbindacts] [
+                guilist [
+                    guitext (tabify (concatword (at $[@curbindtits] $gli) " ") 5)
+                    [newbinds at curbindsign@gli] = ([@curbindsrch] $n 0 "" "" " " " " @curbindshow)
+                    guikeyfield [newbinds at curbindsign@gli] -24 [
+                        oldbinds = ([@@curbindsrch] @n 0 "" "" " " " "  @@curbindshow)
+                        looplist j $oldbinds [echo @@curbindcall " " $j; @@curbindcall $j ""]
+                        looplist j $[newbinds@@curbindsign@@gli] [echo @@curbindcall $j " " @@n; @@curbindcall $j [@@@n]]
+                    ]
+                ]
+            ] [ guistrut 2 ]
+        ]
+    ]
+
+    guitab  "mouse"
+    guilist [
+        guilist [
+            guitext "mouse overall sensitivity  "
+            guitext "mouse yaw sensitivity"
+            guitext "mouse pitch sensitivity"
+            guistrut 1
+            guicheckbox "invert y-axis" mouseinvert
+        ]
+        guilist [
+            guistrut 50 1
+            guislider sensitivity 1 100
+            guislider yawsensitivity 1 100
+            guislider pitchsensitivity 1 100
+        ]
+    ]
+] [
+    if (= $guipasses 0) [ curbindtype = 0 ]
+]
diff --git a/config/menus/package.cfg b/config/menus/package.cfg
new file mode 100644
index 0000000..837dbe0
--- /dev/null
+++ b/config/menus/package.cfg
@@ -0,0 +1,12 @@
+exec "config/menus/glue.cfg"
+exec "config/menus/scratch.cfg"
+exec "config/menus/irc.cfg"
+exec "config/menus/main.cfg"
+exec "config/menus/profile.cfg"
+exec "config/menus/servers.cfg"
+exec "config/menus/game.cfg"
+exec "config/menus/maps.cfg"
+exec "config/menus/options.cfg"
+exec "config/menus/vars.cfg"
+exec "config/menus/editing.cfg"
+exec "config/menus/help.cfg"
\ No newline at end of file
diff --git a/config/menus/profile.cfg b/config/menus/profile.cfg
new file mode 100644
index 0000000..0e49818
--- /dev/null
+++ b/config/menus/profile.cfg
@@ -0,0 +1,238 @@
+profilebuttons = [
+    guicenter [ guifont "emphasis" [
+        guibutton "^fgok" [
+            setinfo $playerprevname $playerprevcolour $playerprevmodel $playerprevvanity $playerprevlweap
+            cleargui 1
+        ] []
+        guistrut 0.5
+        guispring 1
+        guistrut 0.5
+        if (needname) [
+            guibutton "^fomain menu" [showgui main]
+        ] [
+            guibutton "^focancel" [cleargui 1]
+        ]
+    ] ]
+]
+
+profilepreview = [
+    guiplayerpreview $playerprevmodel $playerprevcolour $playerprevteam $playerprevweap $playerprevvanity [playerprevinherit = 1; showgui playerprev] 7.5 1 1
+]
+
+playerprevinherit = 0
+playerprevdisinherit = 0
+
+newgui playerprev [
+    guiheader "player preview"
+    guicenter [ guiplayerpreview $playerprevmodel $playerprevcolour $playerprevteam $playerprevweap $playerprevvanity [cleargui 1] 15 1 1 ]
+    guistrut 0.25
+    guicenter [
+        guilistx 2 [
+            guitext "team: "
+            teamneutraltex = $teamtex
+            loop t 5 [
+                push t (at "neutral alpha omega kappa sigma" $t) [
+                    guistayopen [ guifont "default" [ guibutton (format "^f[%1]^f(%2)" $[team@[t]colour] $[team@[t]tex]) [playerprevteam = @@@@t] ] ]
+                ]
+            ]
+            guistrut 1
+            guitext "weapon: "
+            loop w $weapidxnum [
+                push w (at $weapname $w) [
+                    guistayopen [ guifont "emphasis" [ guibutton (format "^f[%1]^f(%2)" $[@[w]colour] [textures/weapons/@w]) [playerprevweap = @@@@w] ] ]
+                ]
+            ]
+        ]
+    ]
+] [
+    if (= $guipasses 0) [
+        if $playerprevinherit [ playerprevinherit = 0; playerprevdisinherit = 1 ] [
+            playerprevteam = (getplayerteam 1)
+            playerprevweap = (weapselect)
+            playerprevname = (getplayername)
+            playerprevcolour = (getplayercolour -1)
+            playerprevmodel = (getplayermodel)
+            playerprevvanity = (getplayervanity)
+        ]
+    ]
+]
+
+loadweaps = [
+    lwa = ""
+    lwl = (listlen $playerprevlweap)
+    lwo = (? (> $lwl $arg1) (at $playerprevlweap $arg1) -1)
+    loop lw2 $weapidxloadout [
+        if (= $lw2 $arg1) [
+            lwa = (? $lw2 (concat $lwa $arg2) $arg2)
+        ] [
+            lw3 = (? (> $lwl $lw2) (at $playerprevlweap $lw2) -1)
+            if (&& $lw3 (= $arg2 $lw3)) [ lw3 = $lwo ]
+            lwa = (? $lw2 (concat $lwa $lw3) $lw3)
+        ]
+    ]
+    if (listlen $lwa) [ playerprevlweap = $lwa ]
+]
+
+newgui profile [
+    guibox [ profilepreview ] [
+        guicenter [ guifont "emphasis" [ guitext "player name" ] ]
+        guicenter [ guifield playerprevname 24 [] -1 0 "" 0 "^fzad<enter name>" ]
+        guispring 1
+        guicenter [ guifont "emphasis" [ guitext "colour tone" ] ]
+        guicenter [
+            colr = (& (>> $playerprevcolour 16) 0xFF)
+            colg = (& (>> $playerprevcolour 8) 0xFF)
+            colb = (& $playerprevcolour 0xFF)
+            guilist [
+                guibackground $playerprevcolour 1 0xFFFFFF 1 1
+                guistrut 6 1
+                guistrut 3
+            ]
+            guistrut 1
+            guilist [
+                loop i 3 [
+                    n = [col@(at "r g b" $i)]
+                    guilist [
+                        guilist [
+                            guistrut 30 1
+                            guislider $n 0 255 [
+                                playerprevcolour = (+ (<< $colr 0x10) (<< $colg 0x8) $colb)
+                            ] 0 1 (at [0xFF0000 0x00FF00 0x0000FF] $i) 1 (<< $$n (* (- 2 $i) 8))
+                        ]
+                        guitext (concatword "^f[" (at [0xFF0000 0x00FF00 0x0000FF] $i) "]" (at "R G B" $i))
+                    ]
+                ]
+            ]
+        ]
+        guispring 1
+        guicenter [ guifont "emphasis" [ guitext "gender" ] ]
+        guicenter [
+            guiradio "male" playerprevmodel 0
+            guistrut 1
+            guiradio "female" playerprevmodel 1
+        ]
+    ] [ profilebuttons ]
+    guitab "loadout"
+    guibox [ profilepreview ] [
+        guilist [
+            guilist [ 
+                guibackground
+                guicenter [
+                    guistayopen [
+                        gdw = 0
+                        gdl = (listlen $playerprevlweap)
+                        loop w2 $weapidxloadout [
+                            w3 = (? (> $gdl $w2) (at $playerprevlweap $w2) -1)
+                            hi = (mod $w2 2)
+                            al = (|| (= $w3 0) (allowedweap $w3))
+                            guilist [
+                                guibackground (? (&& $al (< $gdw $maxcarry)) 0x303030 0x000000)
+                                guistrut 0.25
+                                guicenter [ guitext (format "%1%2" (? $hi "^fd" "^fw") (+ $w2 1)) ]
+                                guistrut 0.5
+                                guilist [
+                                    if (= $w3 0) [ guibackground 0xFFFFFF 0.01 0xFFFFFF 1 1 ]
+                                    guistrut 0.125
+                                    guiimage [textures/question] [loadweaps @w2 0] 0.75 0 [textures/blank] [] (? (= $w3 0) 0xFFFFFF 0x808080)
+                                    guistrut 0.125
+                                ] 
+                                loop w1 $weapidxloadout [
+                                    w4 = (+ $w1 $weapidxoffset)
+                                    w5 = (at $weapname $w4)
+                                    guilist [
+                                        if (= $w3 $w4) [ guibackground 0xFFFFFF 0.01 $[@[w5]colour] 1 1 ]
+                                        guistrut 0.125
+                                        guiimage (? (allowedweap $w4) [textures/weapons/@w5] [textures/warning]) [loadweaps @w2 @w4] 0.75 0 [textures/blank] [] (? (= $w3 $w4) $[@[w5]colour] 0x808080)
+                                        guistrut 0.125
+                                    ] 
+                                ]
+                            ]
+                            if $al [ gdw = (+ $gdw 1) ]
+                        ]
+                    ]
+                ]
+            ]
+            guistrut 2
+            guilist [
+                guispring 1
+                guicenter [ guitext "select favourite weapons" ]
+                guistrut 0.5
+                guifont "little" [
+                    guicenter [ guitext (format "you can carry ^fs^fc%1^fS %2" $maxcarry (? (= $maxcarry 1) "weapon" "weapons")) ]
+                ]
+                guistrut 0.5
+                guifont "little" [
+                    guicenter [ guitext "^faif a weapon is not available, the" ]
+                    guicenter [ guitext "^fanext one in the list will be used" ]
+                ]
+                guistrut 2
+                guifont "little" [
+                    guicenter [ guicheckbox "pick a loadout every new game" showloadoutmenu ]
+                ]
+                guispring 1
+            ]
+        ]
+    ] [ profilebuttons ]
+    guitab "vanity items"
+    guibox [ profilepreview ] [
+        guicenter [
+            guilist [
+                vanityset = (listlen $playerprevvanity)
+                loop z $vanityset [
+                    vaint = (at $playerprevvanity $z)
+                    vainn = (findvanity $vaint)
+                    if (>= $vainn 0) [
+                        guistayopen [ guicenter [ guibutton (format "^fy%1" (getvanity $vainn 2)) [ playerprevvanity = (listdel $playerprevvanity @(escape $vaint)) ] ] ]
+                    ]
+                ]
+            ]
+            guistrut 1
+            guispring 1
+            guistrut 1
+            guilist [
+                guifont "little" [
+                    vanitynum = (getvanity)
+                    guiloopsplit z 3 $vanitynum [
+                        vaint = (getvanity $z 1)
+                        vainn = (indexof $playerprevvanity $vaint)
+                        if (< $vainn 0) [
+                            guistayopen [ guicenter [ guibutton (format "^fa%1" (getvanity $z 2)) [ playerprevvanity = (concat $playerprevvanity @(escape $vaint)) ] ] ]
+                        ]
+                    ] [ guistrut 2 ]
+                ]
+            ]
+        ]
+    ] [ profilebuttons ]
+    guitab "user account"
+    guibox [ profilepreview ] [
+        guicenter [ guitext "username " ]
+        guicenter [ guifield accountname 48 [] ]
+        guistrut 0.5
+        guicenter [ guitext "private key " ]
+        guicenter [ guifield accountpass 48 [] ]
+        guistrut 1
+        guicenter [ guicheckbox "identify on connect" authconnect ]
+        guicenter [ guicheckbox "identify when playing offline" quickauthchecks ]
+        guistrut 1
+        guicenter [ guitext "^fyaccount applications are available at" ]
+        guicenter [ guitext "^fchttp://redeclipse.net/apply" ]
+        guistrut 1
+    ] [ profilebuttons ]
+] [
+    if (= $guipasses 0) [
+        if $playerprevdisinherit [ playerprevdisinherit = 0 ] [
+            playerprevteam = (getplayerteam 1)
+            playerprevweap = (weapselect)
+            playerprevname = (getplayername)
+            playerprevcolour = (getplayercolour -1)
+            playerprevmodel = (getplayermodel)
+            playerprevvanity = (getplayervanity)
+        ]
+        playerprevlweap = ""
+        break = 0
+        loopwhile i $weapidxloadout [= $break 0] [
+            q = (getloadweap $i)
+            if (< $q 0) [ break = 1 ] [ playerprevlweap = (? $i (concat $playerprevlweap $q) $q) ]
+        ]
+    ]
+]
\ No newline at end of file
diff --git a/config/menus/scratch.cfg b/config/menus/scratch.cfg
new file mode 100644
index 0000000..11b2c3e
--- /dev/null
+++ b/config/menus/scratch.cfg
@@ -0,0 +1,85 @@
+showfileeditor = [
+    guieditor $arg1 $arg2 $arg3
+    textinit $arg1 $arg1
+    guistayopen [
+        guilist [
+            guibutton "load" [textfocus @arg1; textload @arg1]
+            guibar
+            guibutton "save" [textfocus @arg1; textsave @arg1]
+            guibar
+            guibutton "exec" [textfocus @arg1; textexec]
+            guibar
+            guibutton "copy" [textfocus @arg1; textcopy]
+            guibar
+            guibutton "paste" [textfocus @arg1; textpaste]
+            guibar
+            guibutton "select" [textfocus @arg1; textselectall]
+            guibar
+            guibutton "clear" [textfocus @arg1; textclear]
+        ]
+    ]
+]
+
+notepadfile = "untitled.txt"
+
+newgui notepad [
+    guifield notepadfile -30
+    showfileeditor $notepadfile -80 20
+]
+
+notepad = [
+    if (> $numargs 0) [notepadfile = $arg1]
+    showgui notepad
+]
+
+newgui pastebuffer [
+    guieditor "#pastebuffer" -80 20
+    guistayopen [
+        guilist [
+            guibutton "exec" [textfocus "#pastebuffer"; textexec]
+            guibar
+            guibutton "clear" [textfocus "#pastebuffer"; textclear]
+        ]
+    ]
+]
+
+pastebuffer = [showgui pastebuffer]
+
+newgui scratchpad [
+    guieditor "#scratchpad" -80 20
+    guistayopen [
+        guilist [
+            guibutton "exec" [textfocus "#scratchpad"; textexec]
+            guibar
+            guibutton "copy" [textfocus "#scratchpad"; textcopy]
+            guibar
+            guibutton "paste" [textfocus "#scratchpad"; textpaste]
+            guibar
+            guibutton "select" [textfocus "#scratchpad"; textselectall]
+            guibar
+            guibutton "clear" [textfocus "#scratchpad"; textclear]
+        ]
+    ]
+]
+
+scratchpad = [showgui scratchpad]
+
+efuimatch = [
+    match = 1
+    if (&& $efuiinsel $match) [
+        if (= $efuiinsel 1) [
+            match = (insel)
+        ] [
+            match = (! (insel))
+        ]
+    ]
+    if (&& $efuidotype $match) [
+        match = (stringcmp (enttype) (at $enttypelist $efuitype))
+    ]
+    loop i 5 [
+        if (&& $[efuidoattr at i] $match) [
+            match = (= (entattr $i) $[efuiattr at i])
+        ]
+    ]
+    result $match
+]
diff --git a/config/menus/servers.cfg b/config/menus/servers.cfg
new file mode 100644
index 0000000..764bcaa
--- /dev/null
+++ b/config/menus/servers.cfg
@@ -0,0 +1,382 @@
+shownservergui = 0
+sinfoindex = 0
+sinfowait = 0
+sinforetry = ""
+sinfopass = ""
+sinfonum = 0
+sinfodisp = 0
+sinfoplayers = 0
+sinfoservers = 0
+sinfonumsrv = 0
+sinfouitime = 0
+
+sinfotypes = [ "" "status" "name" "port" "qport" "desc" "mode" "muts" "map" "time" "players" "maxplayers" "ping" ]
+sinfomodify = [
+    sinfotlist = $arg1
+    sinfotabsl = (? (> $sinfotlist 0) $sinfotlist (- 0 $sinfotlist))
+    loop i (listlen $serversort) [
+        sinfostype = (at $serversort $i)
+        if $sinfostype [
+            sinfosabsl = (? (> $sinfostype 0) $sinfostype (- 0 $sinfostype))
+            if (!= $sinfotabsl $sinfosabsl) [ append sinfotlist $sinfostype ]
+        ]
+    ]
+    serversort $sinfotlist
+]
+servermenuinit = [
+    sinfoindex = 0
+    sinfowait = 0
+    sinforetry = ""
+    sinfopass = ""
+    sinfoplayers = 0
+    sinfoservers = 0
+    sinfonumsrv = 0
+    sinfouitime = (getmillis 1)
+    shownservergui = 0
+    updateservergui = 0
+    searchfilter = 0
+    searchstr = "" 
+]
+servermenuiter = [
+    if (! $shownservergui) [
+        maplist = ""
+        mappath = ""
+        loopfiles lcurmap maps mpz [
+            if (< (listfind mcurmap $maplist [=s $mcurmap $lcurmap]) 0) [
+                append maplist $lcurmap
+                append mappath [maps/@lcurmap]
+            ]
+        ]
+        if (> $hasoctapaks 0) [
+            loopfiles lcurmap base ogz [
+                append maplist $lcurmap
+                append mappath [base/@lcurmap]
+            ]
+        ]
+        shownservergui = 1
+    ]
+]
+servermenu = [
+    sinfopause = 0
+    sinfotimer = (- (getmillis 1) $sinfouitime)
+    guipage sinfo 5 96 4 [getserver] (getserver) [
+        sinfostat = (getserver $i 0 0)
+        sinfoname = (getserver $i 0 1)
+        sinfoport = (getserver $i 0 2)
+        sinfonpid = [@[sinfoname]:[@@sinfoport]]
+        sinfodesc = (getserver $i 0 3)
+        sinfomapn = (getserver $i 0 4)
+        sinfonump = (getserver $i 0 5)
+        sinfoplayers = (? $i (+ $sinfoplayers $sinfonump) $sinfonump)
+        sinfoservers = (? $i (? $sinfonump (+ $sinfoservers 1) $sinfoservers) (? $sinfonump 1 0))
+        sinfoping = (getserver $i 0 6)
+        sinfolast = (getserver $i 0 7)
+        // attrs
+        sinfoattr = (getserver $i 1 -1)
+        sinfogver = (getserver $i 1 0)
+        sinfomode = (getserver $i 1 1)
+        sinfomuts = (getserver $i 1 2)
+        sinfotime = (getserver $i 1 3)
+        sinfomaxp = (getserver $i 1 4)
+        sinfomstr = (getserver $i 1 5)
+        sinfovars = (getserver $i 1 6)
+        sinfomods = (getserver $i 1 7)
+        sinfoverm = (getserver $i 1 8)
+        sinfovern = (getserver $i 1 9)
+        sinfoverp = (getserver $i 1 10)
+        sinfovers = (getserver $i 1 11)
+        sinfovera = (getserver $i 1 12)
+        sinfogmst = (getserver $i 1 13)
+        sinfogmtl = (getserver $i 1 14)
+        // other
+        sinfoofft = (? (>= $sinfolast 0) (div (max (- (getmillis 1) (- $sinfolast (div $sinfoping 2))) 0) 1000) 0)
+        sinfoactive = (? (< $sinfoping $serverwaiting) 1 0)
+        sinfonumsrv = (? $i (+ $sinfonumsrv $sinfoactive) $sinfoactive)
+        searchbuffer = (concat $sinfodesc $sinfonpid $sinfomapn (gamename $sinfomode $sinfomuts 0))
+        if (> $sinfonump 0 ) [
+            loop j $sinfonump [
+                append searchbuffer (getserver $i 2 $j) // the player name
+                append searchbuffer (getserver $i 3 $j) // and handle
+            ]
+        ]
+    ] [(|| (= $searchfilter 0)  (> (stringcasestr $searchbuffer $searchstr) -1))] [ 
+        if $updateservergui [
+            guibutton "^fwThere are no servers to display, maybe ^fgupdate ^fwthe list?" updatefrommaster [] "info"
+        ] [
+            sleep 1 [ updateservergui = 1; updatefrommaster; sinfouitime = (getmillis) ]
+        ]
+    ] [
+        if (getserver) [ sleep 1 [ updateservers ] ]
+        guilist [
+            guibutton "^fgupdate" updatefrommaster
+            guistrut 1.5
+            guibutton "^frreset" clearservers
+            guistrut 1.5
+            guibutton "^fodisconnect" "savewarnchk disconnect"
+            guistrut 1.5
+            guibutton "^fmlan connect" "savewarnchk lanconnect"
+            guistrut 1.5
+            guicheckbox "^fcsearch lan" searchlan
+            guistrut 1.5
+            guicheckbox (? $pausesortservers "^fdauto sort" "^fbauto sort") autosortservers
+            guistrut 1.5
+            guibutton "^fysort now" sortservers
+            guispring 1
+            guilist [
+                guibackground $guifieldbgcolour $guifieldbgblend $guifieldbordercolour $guifieldborderblend
+                guilist [
+                    guistrut 1
+                    guitext (format "^fc%1 ^fwplayers on ^fc%2 ^fwof ^fc%3^fa/^fc%4 ^fwserver%5" $sinfoplayers $sinfoservers $sinfonumsrv $sinfonum (? (!= $sinfonum 1) "s"))
+                    guistrut 1
+                ]
+            ]
+        ]
+        guistrut 0.25
+        if (> (getversion 3) (getversion 1)) [
+            guilist [ guibutton "^fzoynew version released! ^fwget it now from: ^fcwww.redeclipse.net" "" ]
+            guistrut 0.25
+        ]
+    ] [
+        guimerge 88 [
+            guicenter [
+                guistrut 8 1
+                if $sinfoactive [
+                    guicenter [
+                        guifont "huge" [ guibutton (format "^fw%1" $sinfonump) ]
+                        guicenter [ guibutton (format "^fd/^fa%1" $sinfomaxp) ]
+                    ]
+                    guistrut 0.125
+                    guicenter [
+                        guibutton (format "^f%1%2" (? (< $sinfoping 200) "g" (? (< $sinfoping 400) "y" "r")) $sinfoping)
+                        guifont "little" [ guibutton " ^fams" ]
+                    ]
+                    guistrut 0.125
+                    guicenter [
+                        sinfosecs = (? (>= $sinfolast 0) (div (- (getmillis 1) $sinfolast) 1000) -1)
+                        guifont "little" [ guibutton (? (>= $sinfosecs 0) (format "^fa%1 ^fds ago" $sinfosecs) "^fdwaiting..") ]
+                    ]
+                ] [ guicenter [ guifont "huge" [ guicenterx [ guibutton "?" ] ] ] ]
+            ]
+            sinfoimage = "textures/emblem"
+            if (&& [< $sinfoping $serverwaiting] [= $sinfogver (getversion 1)]) [
+                sinfopath = (listfind curmap $maplist [|| [=s $curmap $sinfomapn] [=s [base/@curmap] $sinfomapn] [
+                    && [> $hasoctapaks 0] [=s [base/@curmap] $sinfomapn]
+                ]])
+                if (> $sinfopath -1) [ sinfoimage = (at $mappath $sinfopath) ]
+            ]
+            guiimage $sinfoimage "" 2 1 "textures/emblem"
+            guistrut 1
+            guicenter [
+                guilist [
+                    guibutton (format "^fw%1 " $sinfodesc)
+                    guifont "little" [ guicenter [ guibutton (format "^fd(^fa%1^fd)" $sinfonpid) ] ]
+                ]
+                guispring 1
+                if $sinfoactive [
+                    guilist [
+                        guistrut 0.25
+                        if (= $sinfogver (getversion 1)) [
+                            guinohitfx [ guifont "default" [
+                                case $sinfostat 0 [
+                                    guibutton "^fgopen" [] [] "textures/servers/default"
+                                ] 1 [
+                                    guibutton "^fylock" [] [] "textures/servers/locked"
+                                ] 2 [
+                                    guibutton "^fmpriv" [] [] "textures/servers/private"
+                                ] 3 [
+                                    guibutton "^frfull" [] [] "textures/servers/full"
+                                ] () [
+                                    guibutton "^founknown" [] [] "textures/servers/unknown"
+                                ]
+                            ] ]
+                            if (!= $sinfomode $modeidxediting) [
+                                guibutton (format " ^fd[^fc%1^fd:^fw%2^fd]" (at $gamestatename $sinfogmst) (timestr (* (? $sinfonump (max (- $sinfogmtl $sinfoofft) 0) $sinfotime) 1000) 3))
+                            ]
+                            gname = (gamename $sinfomode $sinfomuts 0 32)
+                            guifont "little" [ guicenter [ guilist [
+                                guibutton (format " ^fy%1 ^faon ^fo%2" $gname $sinfomapn)
+                                guibutton (format " ^fa(%1modified^fa)" (? $sinfomods (format "^fc%1%% " (precf (*f (divf $sinfomods $sinfovars) 100) 2)) "^fgun"))
+                                if (>= $sinfoattr 13) [ guibutton (format " ^fa[^fc%1.%2.%3-%4%5^fa]" $sinfoverm $sinfovern $sinfoverp (platname $sinfovers) $sinfovera) ]
+                            ] ] ]
+                        ] [
+                            guinohitfx [ guifont "default" [ guibutton "^foincompatible" [] [] "textures/servers/failed" ] ]
+                            guibutton (concat " ^faserver is using" (? (> $sinfogver (getversion 1)) "a ^fwnewer" "an ^fdolder") "protocol")
+                        ]
+                    ]
+                    guispring 1
+                    guilist [
+                        if (=s $sinfonpid $sinforetry) [
+                            if (= $sinfostat 3) [ guibutton "^fd[ ^fwwaiting for slot ^fd] " ]
+                            guibutton "^fwpassword ^fd= "
+                            sinfopassval = $sinfopass
+                            guifield sinfopassval 20 [sinfopass = $sinfopassval]
+                        ] [
+                            if (> $sinfonump 0) [
+                                guistrut 0.25
+                                sinfopnum = (getserver $i 2)
+                                if (> $sinfopnum 0) [
+                                    guifont "little" [
+                                        pname = ""
+                                        plist = ""
+                                        pmore = 0
+                                        loop j $sinfopnum [
+                                            if (|| $pmore (>= (guitextwidth $plist) 1400)) [ pmore = (+ $pmore 1) ] [
+                                                append pname (format ["%1"] (getserver $i 2 $j))
+                                                plist = (prettylist $pname)
+                                            ]
+                                        ]
+                                        guibutton (concat $plist (? $pmore (concat "and^fy" $pmore "^fwmore")))
+                                        //[] [] "" -1 -1 1900
+                                    ]
+                                ] [ guibutton "^faplayer info not available" ]
+                            ] [ guibutton "^fano players online" ]
+                        ]
+                    ]
+                ] [
+                    guilist [
+                        guistrut 0.25
+                        guifont "default" [ guibutton "^founresponsive" [] [] "textures/servers/failed" ]
+                        guibutton " ^faserver is not replying to queries"
+                    ]
+                ]
+            ]
+        ] [
+            sinfopass = ""
+            sinforetry = @(escape $sinfonpid)
+            sinfowait = (! (|| [hasauthkey 1] [!= @@sinfomstr 4]))
+        ] [
+            sinfopass = ""
+            sinforetry = @(escape $sinfonpid)
+            sinfowait = (! (hasauthkey 1))
+        ] [
+            sinfopause = 1
+            if $sinfoactive [
+                sinfopnum = (getserver $i 2)
+                if (> $sinfopnum 0) [
+                    phover = ""
+                    loop j $sinfopnum [
+                        if $j [ append phover "^n" ]
+                        phandle = (getserver $i 3 $j)
+                        if (stringlen $phandle) [ 
+                            append phover (format "%1 (%2)" (getserver $i 2 $j) $phandle)
+                        ] [
+                            append phover (format "%1" (getserver $i 2 $j))
+                        ]
+                    ]
+                    guitooltip $phover
+                ] [ guitooltip "^fano players online" ]
+            ] [ guitooltip "^fano information available" ]
+        ]
+        if (=s $sinfonpid $sinforetry) [
+            if (= $sinfowait 1) [
+                if (stringlen $sinfopass) [
+                    savewarnchk [connect $sinfoname $sinfoport $sinfopass]
+                    sinfopass = ""
+                    sinforetry = ""
+                    sinfowait = 0
+                ]
+            ] [
+                if (|| [hasauthkey 1] [!= $sinfostat 3]) [
+                    savewarnchk [connect $sinfoname $sinfoport]
+                    sinfopass = ""
+                    sinforetry = ""
+                    sinfowait = 0
+                ]
+            ]
+        ]
+    ] [
+        guistrut 0.5
+        guilist [
+            guicenterz [ guitext "update interval:" ]
+            guistrut 0.5
+            guilist [
+                guistrut 87.5 1
+                guilistslider serverupdateinterval "1 2 3 4 5 10 15 20 25 30 35 40 45 50 55 60"
+            ]
+        ]
+        guistrut 0.5
+        guilist [
+            guicenterz [ guicheckbox "filter:" searchfilter ]
+            if (=s $guirollovername "filter:") [
+                guitooltip "^faFilter by player names or handles, current map, mode or mutators, server description, IP address or port." 
+            ]
+            guistrut 0.5
+            guifield searchstrval 18 [searchstr = $searchstrval; searchfilter = 1] -1 0 "" 0 "^fd <enter search terms>" 1
+            guispring 1
+            guicenterz [
+                guitext "sort:"
+                guistrut 0.5
+                loop i (listlen $serversort) [
+                    sinfostype = (at $serversort $i)
+                    if $sinfostype [
+                        sinfosabsl = (? (> $sinfostype 0) $sinfostype (- 0 $sinfostype))
+                        sinfosname = (at $sinfotypes $sinfosabsl)
+                        guistrut 0.5
+                        guibutton (? (> $sinfostype 0) $sinfosname [- at sinfosname]) [
+                            sinfomodify @sinfostype
+                        ] [
+                            sinfomodify (- 0 @sinfostype)
+                        ] "" 0x00FFFF
+                    ]
+                ]
+            ]
+            guispring 1
+            guicenterz [
+                guifont "little" [
+                    loop i (listlen $sinfotypes) [
+                        if (&& $i [< (listfind sinfoctype $serversort [= $i (? (> $sinfoctype 0) $sinfoctype (- 0 $sinfoctype))]) 0]) [
+                            sinfosname = (at $sinfotypes $i)
+                            guistrut 1
+                            guibutton $sinfosname [
+                                sinfomodify @i
+                            ] [
+                                sinfomodify (- 0 @i)
+                            ] "" 0x888888
+                        ]
+                    ]
+                ]
+            ]
+            guispring 1
+            guicenterz [ guibutton "reset" [serversortreset] [serversortreset] "" 0xFF8888 ]
+            guistrut 1
+        ]
+        guistrut 0.5
+        if (hasauthkey) [
+            guilist [
+                guicenterz [ guitext "user account:" ]
+                guispring 1
+                guicenterz [ guicheckbox (format "identify as ^fs^fc%1^fS on connect" $accountname) authconnect ]
+                guispring 1
+                guicenterz [ guifont "little" [ guibutton "(^fs^fredit account^fS)" [showgui profile 4] ] ]
+            ]
+        ]
+    ]
+    if (&& [! $guilayoutpass] [|| (> $sinfotimer 30000) (> $sinfonumsrv (div (* $sinfonum 3) 4))]) [ if $sinfopause [ if (! $pausesortservers) [ pausesortservers 1 ] ] [ if $pausesortservers [ pausesortservers 0 ] ] ]
+    guitip (format "press %1 to open this menu at any time" (dobindsearch "showservers"))
+]
+
+newgui servers [
+    servermenuiter
+    guilist [ servermenu ]
+] [
+    if (= $guipasses 0) [
+        servermenuinit
+    ]
+]
+
+newgui guidelines [
+    guieditor doc/guidelines.txt -80 22
+    textinit doc/guidelines.txt doc/guidelines.txt
+    textmode 4
+    guistrut 0.5
+    guifont "emphasis" [
+        guilist [
+            guibutton "[ ^fs^foi disagree, stay offline^fS ]" [cleargui 1]
+            guispring 1
+            guibutton "[ ^fs^fgi agree, play online^fS ]" [connectguidelines 1; @guidelinesaction; guidelinesaction ""]
+            guistrut 3
+        ]
+    ]
+    guistatus "you must read and agree to these guidelines before playing online"
+] [ if $connectguidelines [cleargui 1] ]
+showservers = [ guidelinesaction "showgui servers"; showgui (? $connectguidelines servers guidelines) ]
diff --git a/config/menus/vars.cfg b/config/menus/vars.cfg
new file mode 100644
index 0000000..617d8b3
--- /dev/null
+++ b/config/menus/vars.cfg
@@ -0,0 +1,257 @@
+varsearchstr = ""
+varscount = 30
+varindex = 0
+varnum = -1
+vartypes = 7
+varnotypes = 0
+varflags = 16
+varnoflags = 128
+
+newgui vars [
+    //  guilist [
+    //      guitext "varflags:  "
+    //      loop i 8 [ guibitfield $i varflags (<< 1 $i) ]
+    //      guitext "  varnoflags:  "
+    //      loop i 8 [ guibitfield $i varnoflags (<< 1 $i) ]
+    //  ] 
+    //  guibar
+    guiheader "variables and commands"
+    numvars = (getvarinfo -1 $vartypes $varnotypes $varflags $varnoflags $varsearchstr)
+    if (>= $varnum $numvars) [ varnum = -1 ]
+    guilist [
+        guistrut 1
+        guifield varsearchstrval 32 [varsearchstr = $varsearchstrval; varnum = -1; varindex = 0] -1 0 "" 0 "^fd<enter search terms>" 1
+        guistrut 1
+        guilist [
+            guibackground $guifieldbgcolour $guifieldbgblend $guifieldbordercolour $guifieldborderblend 1
+            guistrut 0.15
+            guilist [
+                guistrut 0.5
+                guibitfield "integer" vartypes (<< 1 0) [varnum = -1] 
+                guistrut 1
+                guibitfield "float" vartypes (<< 1 1) [varnum = -1] 
+                guistrut 1
+                guibitfield "string" vartypes (<< 1 2) [varnum = -1] 
+                guistrut 1
+                guibitfield "command" vartypes (<< 1 3) [varnum = -1] 
+                guistrut 1
+                guibitfield "alias" vartypes (<< 1 4) [varnum = -1] 
+                guistrut 0.5
+            ]
+            guistrut 0.15
+        ]
+        guispring 1
+        guilist [
+            guibackground $guifieldbgcolour $guifieldbgblend $guifieldbordercolour $guifieldborderblend 1
+            guistrut 0.15
+            guilist [
+                guistrut 1
+                guitext (format "^fg%1 ^fa%2 found" $numvars (? (= $numvars 1) "match" "matches"))
+                guistrut 1
+            ]
+            guistrut 0.15
+        ]
+    ]
+    guistrut 0.5
+    guilist [
+        guilistx 2 [
+            guifont "little" [
+                guicontainer [1] [
+                    guilist [
+                        guilist [
+                            //guistrut $varscount 1
+                            varindex = (min (max 0 (- $numvars $varscount)) $varindex) //safeguard
+                            varnum = (min $varnum $numvars)
+                            guilist [
+                                guistrut 39 1
+                                break = 0
+                                loopwhile i $varscount [= $break 0] [
+                                    q = (+ $varindex $i)
+                                    if (< $q $numvars) [
+                                        curvar = (getvarinfo $q $vartypes $varnotypes $varflags $varnoflags $varsearchstr)
+                                        hilvar = (stringreplace $curvar $varsearchstr (format "^fs^fy%1^fS" $varsearchstr))
+                                        guilist [ guiradio $hilvar varnum $q ]
+                                    ] [ guistrut 1 ]
+                                ]
+                            ]
+                        ]
+                    ]
+                    guislider varindex 0 (max (- $numvars $varscount) 0) [] 1 1
+                ] [
+                    guistrut 43.25
+                    //guistrut (+f $varscount 1) 1
+                ]
+            ]
+        ]
+        guistrut 1
+        guilist [
+            guistrut 59 1
+            if (&& (>= $varnum 0) (< $varnum $numvars) (> $numvars 0)) [
+                scurvar = (getvarinfo $varnum $vartypes $varnotypes $varflags $varnoflags $varsearchstr)
+                scurvartype = (getvartype $scurvar)
+                guifont "emphasis" [ guitext $scurvar ]
+                guilist [
+                    case $scurvartype 0 [
+                        guitext "^fainteger"
+                        guispring 1
+                        guitext (format "^famin: ^fw%1" (getvarmin $scurvar))
+                        guispring 1
+                        guitext (format "^famax: ^fw%1" (getvarmax $scurvar))
+                        guispring 1
+                        guitext (format "^fadefault: ^fw%1" (getvardef $scurvar 1))
+                    ] 1 [
+                        guitext "^fafloat"
+                        guispring 1
+                        guitext (format "^famin: ^fw%1" (getfvarmin $scurvar))
+                        guispring 1
+                        guitext (format "^famax: ^fw%1" (getfvarmax $scurvar))
+                        guispring 1
+                        guitext (format "^fadefault: ^fw%1" (getfvardef $scurvar 1))
+                    ] 2 [
+                        guitext "^fastring"
+                    ] 3 [
+                        guitext "^facommand"
+                    ] 4 [
+                        guitext "^faalias"
+                    ] 5 [
+                        guitext "^falocal"
+                    ] () [
+                        guitext "^fa???"
+                    ]
+                ]
+                if (= $scurvartype 2) [
+                    guistrut 0.25
+                    guilist [
+                        guitext "^fadefault"
+                        guispring 1
+                        guieditor [@[scurvar]_vardef] -58 8 4 -1 0 "" (getsvardef $scurvar 1)
+                    ]
+                ]
+                guistrut 0.25
+                if (> $scurvartype 2) [
+                    guistrut 65 1
+                    guilist [
+                        guitext "^faconsole"
+                        guistrut 1
+                        guilist [
+                            guibackground $guifieldbgcolour $guifieldbgblend $guifieldbordercolour $guifieldborderblend 1
+                            guistrut  0.2
+                            guistayopen [ guibutton (format "/%1 ...   " $scurvar) [saycommand (format "/%1 " $scurvar)] [] "textures/menu" ]
+                            guistrut  0.2
+                        ]
+                        guistrut 1
+                        guilist [
+                            guibackground $guifieldbgcolour $guifieldbgblend $guifieldbordercolour $guifieldborderblend 1
+                            guistrut  0.2
+                            guistayopen [ guibutton (format "/echo (%1 ...   " $scurvar) [saycommand (format "/echo (%1 " $scurvar)] [] "textures/menu" ]
+                            guistrut  0.2
+                        ]
+                    ]
+                ] [    
+                    guistrut 65 1
+                    guilist [
+                        guitext "value"
+                        guistrut 1
+                        [@[scurvar]_varval] = $$scurvar
+                        guifield [@[scurvar]_varval] -58 [@scurvar $[@@[scurvar]_varval]] -1 0 ""
+                    ]
+                ]
+                if (= $scurvartype 0) [
+                    if (= 0xffffff (getvarmax $scurvar)) [
+                        guistrut 0.25
+                        guilist [
+                            guitext (format "^fahex value: ^fw%1" (hexcolour $$scurvar))
+                            guispring 1
+                            guitext "^fa preview:"
+                            guistrut 1
+                            guiimage "textures/guiskin.png" [] 1.5 0 "" [] $$scurvar
+                        ]
+                    ] 
+                ] 
+                scurusage = (getvarusage $scurvar)
+                if (stringlen $scurusage) [
+                    guistrut 0.25
+                    guilist [
+                        guitext "^fausage"
+                        guispring 1
+                        guitext (concat $scurvar $scurusage) "" -1 -1 1220
+                    ]
+                ]
+                scurdesc = (getvardesc $scurvar)
+                if (stringlen $scurdesc) [
+                    guistrut 0.25
+                    guilist [
+                        guitext "^fainfo"
+                        guispring 1
+                        guitext $scurdesc "" -1 -1 1220
+                    ]
+                ]
+            ] [
+                guistrut 65 1
+                guifont "emphasis" [ guitext "no variable or command selected" ]
+                guifont "little" [
+                    guitext "^fause the list on the left to pick a variable" point
+                    guitext "^fause the search field at the top to narrow results" point
+                ]
+            ]
+        ]
+    ]
+] [
+    if (= $guipasses 0) [
+        varindex = 0
+        varnum = -1
+    ]
+]
+
+cursoundnames = [ "xv" "cv" "cp" "fl" "xr" "mr" "" "" "" "ss" "" "pos" "" "" "" "m" "" "c" "cn" ]
+cursoundwidth = [ 4 4 4 4 4 4 0 0 0 3 0 16 0 0 0 2 0 2 3 ]
+newgui cursounds [
+    cursoundnum = (getcursound)
+    guilist [
+        guilist [ guistrut 3 1 ]
+        cursoundprop = (listlen $cursoundnames)
+        loop j $cursoundprop [
+            cursoundw = (at $cursoundwidth $j)
+            if $cursoundw [
+                cursoundval = (at $cursoundnames $j)
+                guilist [
+                    guistrut $cursoundw 1
+                    guicenter [ guitext (format "^fy%1" $cursoundval) ]
+                ]
+            ]
+        ]
+        guilist [ guistrut 30 1 ]
+    ]
+    loop i $cursoundnum [
+        guilist [
+            guilist [
+                guistrut 3 1
+                guilist [ guitext (format "^fg%1." $i) ]
+            ]
+            cursoundplay = (getcursound $i 14)
+            if $cursoundplay [ 
+                cursoundprop = (getcursound $i)
+                loop j $cursoundprop [
+                    cursoundw = (at $cursoundwidth $j)
+                    if $cursoundw [
+                        cursoundval = (getcursound $i $j)
+                        guilist [
+                            guistrut $cursoundw 1
+                            guicenter [ guitext $cursoundval ]
+                        ]
+                    ]
+                ]
+                guilist [
+                    guistrut 30 1
+                    guilist [
+                        guispring
+                        guitext (getsound (getcursound $i 15) (getcursound $i 9) 3)
+                    ]
+                ]
+            ] [
+                guispring
+                guitext "-"
+            ]
+        ]
+    ]
+]
diff --git a/config/setup.cfg b/config/setup.cfg
new file mode 100644
index 0000000..3bdda19
--- /dev/null
+++ b/config/setup.cfg
@@ -0,0 +1,311 @@
+complete exec .
+
+mapcomplete     = [ setcomplete $arg1 1; complete $arg1 maps mpz ]; mapcomplete map
+start           = [ mode $arg2 $arg3; map $arg1 ]; mapcomplete start
+
+demo            = [ stopdemo; start $arg1 0 0 ]; complete demo demos dmo
+edit            = [ start $arg1 $modeidxediting (+ $mutsbitffa $mutsbitclassic $arg2) ]; mapcomplete edit
+deathmatch      = [ start $arg1 $modeidxdeathmatch (+ 0 $arg2) ]; mapcomplete deathmatch; dm = [ deathmatch $arg1 $arg2 ]; mapcomplete dm
+teamdm          = [ start $arg1 $modeidxdeathmatch (+ 0 $arg2) ]; mapcomplete teamdm; tdm = [ teamdm $arg1 $arg2 ]; mapcomplete tdm
+ffa             = [ start $arg1 $modeidxdeathmatch (+ $mutsbitffa $arg2) ]; mapcomplete ffa; fdm = [ ffa $arg1 $arg2 ]; mapcomplete fdm
+multidm         = [ start $arg1 $modeidxdeathmatch (+ $mutsbitmulti $arg2) ]; mapcomplete multidm; mdm = [ multidm $arg1 $arg2 ]; mapcomplete mdm
+coop            = [ start $arg1 $modeidxdeathmatch (+ $mutsbitcoop $arg2) ]; mapcomplete coop; cdm = [ coop $arg1 $arg2 ]; mapcomplete cdm
+capture         = [ start $arg1 $modeidxcapture (+ 0 $arg2) ]; mapcomplete capture; ctf = [ capture $arg1 $arg2 ]; mapcomplete ctf
+defend          = [ start $arg1 $modeidxdefend (+ 0 $arg2) ]; mapcomplete defend; dnc = [ defend $arg1 $arg2 ]; mapcomplete dnc; dac = [defend $arg1 $arg2 ] ; mapcomplete dac
+bomber          = [ start $arg1 $modeidxbomber (+ 0 $arg2) ]; mapcomplete bomber; bb = [ bomber $arg1 $arg2 ]; mapcomplete bbr
+race            = [ start $arg1 $modeidxrace (+ 0 $arg2) ]; mapcomplete race
+instagib        = [ start $arg1 $modeidxdeathmatch (+ $mutsbitinstagib $arg2) ]; mapcomplete instagib; insta = [ instagib $arg1 $arg2 ]; mapcomplete insta
+medieval        = [ start $arg1 $modeidxdeathmatch (+ $mutsbitmedieval $arg2) ]; mapcomplete medieval
+kaboom          = [ start $arg1 $modeidxdeathmatch (+ $mutsbitkaboom $arg2) ]; mapcomplete kaboom
+duel            = [ start $arg1 $modeidxdeathmatch (+ $mutsbitduel $arg2) ]; mapcomplete duel
+survivor        = [ start $arg1 $modeidxdeathmatch (+ $mutsbitsurvivor $arg2) ]; mapcomplete survivor; lms = [ survivor $arg1 $arg2 ]; mapcomplete lms
+classic         = [ start $arg1 $modeidxdeathmatch (+ $mutsbitclassic $arg2) ]; mapcomplete classic
+quickcapture    = [ start $arg1 $modeidxcapture (+ $mutsbitgsp1 $arg2) ]; mapcomplete quickcapture
+defendcapture   = [ start $arg1 $modeidxcapture (+ $mutsbitgsp2 $arg2) ]; mapcomplete defendcapture
+protectcapture  = [ start $arg1 $modeidxcapture (+ $mutsbitgsp3 $arg2) ]; mapcomplete protectcapture
+quickdefend     = [ start $arg1 $modeidxdefend (+ $mutsbitgsp1 $arg2) ]; mapcomplete quickdefend
+kingdefend      = [ start $arg1 $modeidxdefend (+ $mutsbitgsp2 $arg2) ]; mapcomplete kingdefend; koth = [ kingdefend $arg1 $arg2 ]; mapcomplete koth
+holdbomber      = [ start $arg1 $modeidxbomber (+ $mutsbitgsp1 $arg2) ]; mapcomplete holdbomber
+basketbomber    = [ start $arg1 $modeidxbomber (+ $mutsbitgsp2 $arg2) ]; mapcomplete basketbomber
+attackbomber    = [ start $arg1 $modeidxbomber (+ $mutsbitgsp3 $arg2) ]; mapcomplete attackbomber
+timedrace       = [ start $arg1 $modeidxrace (+ $mutsbitgsp1 $arg2) ]; mapcomplete timedrace; trial = [timedrace $arg1 $arg2 ]; mapcomplete trial
+endurancerace   = [ start $arg1 $modeidxrace (+ $mutsbitgsp2 $arg2) ]; mapcomplete endurancerace
+gauntletrace    = [ start $arg1 $modeidxrace (+ $mutsbitgsp3 $arg2) ]; mapcomplete gauntletrace
+
+
+delta_game_0 = [ if (iszooming) [ setzoom $arg1 ] [ weapon -1 $arg1 ] ]
+delta_spec_0 = [ followdelta $arg1 ]
+delta_wait_0 = [ followdelta $arg1 ]
+
+delta_edit_0 = [
+    if $blendpaintmode [
+        nextblendbrush $arg1
+        echo (concatword "^fgblend brush:^fw " (getblendbrushname (curblendbrush)))
+    ] [
+        editfacewentpush $arg1 1
+    ]
+]
+
+delta_edit_1    = [ nodebug [ gridpower (+ $arg1 $gridpower) ] ]
+delta_edit_2    = [ editfacewentpush $arg1 0 ] // push face/corners selected
+delta_edit_3    = [ editfacewentpush $arg1 2 ] // push corner pointed at by cursor
+delta_edit_4    = [ editrotate $arg1 ] // rotate 90 degrees
+delta_edit_5    = [ entproperty 0 $arg1 ] // and the others
+delta_edit_6    = [ edittex $arg1 ] // change textures
+delta_edit_9    = [ selectbrush $arg1 ] // change heightmap brushes
+delta_edit_10   = [ entautoview $arg1 ]
+delta_edit_11   = [ entproperty 0 $arg1 ]
+delta_edit_12   = [ entproperty 1 $arg1 ]
+delta_edit_13   = [ entproperty 2 $arg1 ]
+delta_edit_14   = [ entproperty 3 $arg1 ]
+delta_edit_15   = [ entproperty 4 $arg1 ]
+delta_edit_16   = [ entproperty 5 $arg1 ]
+delta_edit_17   = [ entproperty 6 $arg1 ]
+delta_edit_18   = [ entproperty 7 $arg1 ]
+delta_edit_19   = [ entproperty 8 $arg1 ]
+delta_edit_20   = [ entproperty 9 $arg1 ]
+
+shiftfloatspeed = 500
+setcomplete shiftfloatspeed 1
+setpersist shiftfloatspeed 1
+shiftmodscale = 10
+setcomplete shiftmodscale 1
+setpersist shiftmodscale 1
+oldfloatspeed = $floatspeed
+shiftmod = [
+    if $arg1 [
+        oldfloatspeed = $floatspeed
+        floatspeed $shiftfloatspeed
+        modscale = $shiftmodscale
+    ] [
+        floatspeed $oldfloatspeed
+        modscale = 1
+    ]
+]
+nullbind = []
+
+bind MOUSE1         [ primary ] // primary fire
+bind MOUSE2         [ secondary ] // secondary fire
+bind MOUSE3         [ reload ] // reload
+bind MOUSE4         [ universaldelta 1 ]    // also used for editing, see below
+bind MOUSE5         [ universaldelta -1 ]
+bind LEFTBRACKET    [ universaldelta 1 ]
+bind RIGHTBRACKET   [ universaldelta -1 ]
+
+specbind LSHIFT     [ shiftmod 1; onrelease [ shiftmod 0 ] ]
+specbind E          [ spectator 0 ]
+specbind MOUSE1     [ nullbind ]
+specbind MOUSE2     [ spectator 0 ]
+specbind HOME       [ specmodeswitch ]
+specbind R          [ specmodeswitch ]
+specbind MOUSE3     [ specmodeswitch ]
+
+waitbind LSHIFT     [ shiftmod 1; onrelease [ shiftmod 0 ] ]
+waitbind MOUSE1     [ nullbind ]
+waitbind MOUSE2     [ nullbind ]
+waitbind END        [ waitmodeswitch ]
+waitbind R          [ waitmodeswitch ]
+waitbind MOUSE3     [ waitmodeswitch ]
+
+bind END            [ suicide ]
+
+bind W              [ forward ]
+bind A              [ left ]
+bind S              [ backward ]
+bind D              [ right ]
+
+bind UP             [ forward ]
+bind DOWN           [ backward ]
+bind RIGHT          [ right ]
+bind LEFT           [ left ]
+
+bind SPACE          [ jump ] // jump
+bind LCTRL          [ walk ] // walk
+bind LSHIFT         [ crouch ] // crouch
+bind E              [ use ] // use
+bind R              [ reload ] // reload
+bind Q              [ special ] // special
+bind Z              [ drop ] // drop
+bind F              [ affinity ] // affinity
+bind K              [ suicide ]
+
+bind TAB            [ showscores ]
+
+saytextcolour = 0; setpersist saytextcolour 1; setcomplete saytextcolour 1
+getsaycolour = [
+    sc = $saytextcolour
+    if (< $sc 0) [ sc = (getplayercolour 1) ]
+    if (> $sc 0) [ result (format "^f[%1]" $sc) ] [ result "" ]
+]
+
+saytextcommand =    [inputcommand $arg1 [say $commandbuffer] "textures/chat" 0 "s"]
+sayteamcommand =    [inputcommand $arg1 [sayteam $commandbuffer] (getplayerteamicon) (getplayerteamcolour) 0 "s"]
+
+bind T              [ saytextcommand (getsaycolour) ]
+bind RETURN			[ saytextcommand (getsaycolour) ]
+bind BACKQUOTE      "saycommand /"
+bind SLASH          "saycommand /"
+bind Y              [ sayteamcommand (getsaycolour) ]
+bind I              [ showgui lobby ]
+
+bind KP_MINUS       [ mastervol (max (- $mastervol 5) 0) ]
+bind KP_PLUS        [ mastervol (min (+ $mastervol 5) 255) ]
+bind KP_DIVIDE      [ togglesound ]
+
+bind PAUSE          [ gamepaused (! $gamepaused) ]
+bind PAGEDOWN       [ conskip (- 0 $consize) ]
+bind PAGEUP         [ conskip $consize ]
+
+addbot =            [ botoffset (+ $botoffset 1) ]
+delbot =            [ botoffset (- $botoffset 1) ]
+
+bind INSERT         [ addbot ]
+bind DELETE         [ delbot ]
+bind HOME           [ spectator 1 ]
+bind END            [ spectator 0 ]
+
+bind F1             [ showgui help ]
+bind F2             [ edittoggle ]
+bind F3             [ showgui maps 1 ]
+bind F4             [ showgui maps 2 ]
+bind F5             [ showservers ]
+bind F6             [ showgui profile 2 ]
+bind F7             [ showgui team ]
+bind F8             [ setpriv 1 ]
+bind F9             [ thirdpersonswitch ]
+bind F10            [ grabinput (! $grabinput) ]
+bind F11            [ toggleconsole ]
+bind F12            [ screenshot ]
+bind COMMA          [ showgui profile 2 ]
+bind PERIOD         [ showgui team ]
+
+editbind F1         [ showtexgui ]
+editbind F2         [ edittoggle ]
+editbind F3         [ showgui edit ]
+editbind F4         [ remip ]
+editbind F5         [ fullbright 0; patchlight]
+editbind F6         [ fullbright 0; calclight -1 ]
+editbind F7         [ fullbright 0; calclight 1 ]
+editbind F8         [ savemap ]
+editbind F9         [ changeoutline 1 ]
+editbind BACKSLASH  [ readpixel [printpixel] ]
+
+bind 1              [ weapon 1 ]
+bind 2              [ weapon 2 ]
+bind 3              [ weapon 3 ]
+bind 4              [ weapon 4 ]
+bind 5              [ weapon 5 ]
+bind 6              [ weapon 6 ]
+bind 7              [ weapon 7 ]
+bind 8              [ weapon 8 ]
+bind 9              [ weapon 9 ]
+bind 0              [ weapon 10 ]
+
+editbind 1          [ domodifier 11 ]
+editbind 2          [ domodifier 12 ]
+editbind 3          [ domodifier 13 ]
+editbind 4          [ domodifier 14 ]
+editbind 5          [ domodifier 15 ]
+editbind 6          [ domodifier 16 ]
+editbind 7          [ domodifier 17 ]
+editbind 8          [ domodifier 18 ]
+editbind 9          [ domodifier 19 ]
+editbind 0          [ domodifier 20 ]
+
+editbind K          [ domodifier 9 ]
+
+editbindvar MINUS   allfaces
+editbindvar PLUS    showmat
+editbindvar EQUALS  outline
+
+
+editbind INSERT     [ sendmap ]
+editbind DELETE     [ getmap ]
+editbind LSHIFT     [ shiftmod 1; onrelease [ shiftmod 0 ] ]
+editbind END	    [ entcancel ]
+
+oldpassthroughsel = 0
+oldpassthroughcube = 0
+passthrough = [
+    if (= $arg1 1) [
+        oldpassthroughsel = $passthroughsel
+        oldpassthroughcube = $passthroughcube
+        passthroughsel 1; passthroughcube 1
+    ] [
+        passthroughsel $oldpassthroughsel
+        passthroughcube $oldpassthroughcube
+    ]
+    entcancel
+]; setcomplete passthrough 1
+editbind LCTRL      [ passthrough 1; onrelease [ passthrough 0 ] ]
+editbind LALT       [ hmapedit 0; blendpaintmode 0 ]
+
+gridbindswitch = 0
+togglegrid = [
+    case $gridbindswitch 3 [
+        showpastegrid 0; showcursorgrid 0; showselgrid 0; gridbindswitch = 0
+    ] 2 [
+        showpastegrid 0; showcursorgrid 0; showselgrid 1; gridbindswitch = 3
+    ] 1 [
+        showpastegrid 0; showcursorgrid 1; showselgrid 0; gridbindswitch = 2
+    ] 0 [
+        showpastegrid 1; showcursorgrid 0; showselgrid 0; gridbindswitch = 1
+    ]
+]; setcomplete togglegrid 1
+
+editbind TAB        [ togglegrid ]
+
+editbind SPACE      [ cancelsel ]
+editbind MOUSE1     [ if $blendpaintmode paintblendmap editdrag ]
+editbind MOUSE3     [ selcorners ]
+editbind MOUSE2     [ if $blendpaintmode rotateblendbrush editextend ]
+
+editbind N          [ entselect insel ]
+do [brush_2] //421
+
+editbind L          [ entlink ]
+editbind X          [ editflip ]
+editbind C          [ editcopy ]
+editbind V          [ editpaste ]
+editbind U          [ editcut ]
+editbind Z          [ undo; passthroughsel 0 ]
+editbind O          [ redo ]
+editbind H          [ hmapedit (! $hmapedit); blendpaintmode 0 ]
+
+editbind BACKSPACE  [ editdel ]
+
+editbind G          [ domodifier 1 ] // domodifier 1 -> executes delta_edit_1
+editbind F          [ domodifier 2 ] // etc...
+editbind Q          [ domodifier 3 ]
+editbind R          [ domodifier 4 ]
+editbind Y          [ domodifier 6 ]
+editbind J          [ gettex ]
+editbind COMMA      [ domodifier 10; onrelease entautoview ]
+
+editbind PERIOD     selentedit
+
+paintmodes = ["off" "overwrite blendmap" "merge blendmap" "max opacity to brush's" "inverted merge" "opacity multiplier (erasing)"]
+
+editbind P [
+    hmapedit 0;
+    blendpaintmode (? (= $blendpaintmode (getvarmax blendpaintmode)) 0 (+ $blendpaintmode 1))
+    echo (concatword "^fgblend mode:^fw " (at $paintmodes $blendpaintmode) " (^fc" $blendpaintmode "^fw)")
+]
+editbindvar B       fullbright
+
+editbind KP0        [ showgui materials ]
+editbind KP1        [ editmat air ]
+editbind KP2        [ editmat alpha ]
+editbind KP3        [ editmat water ]
+editbind KP4        [ editmat lava ]
+editbind KP5        [ editmat clip ]
+editbind KP6        [ editmat noclip]
+editbind KP7        [ editmat aiclip ]
+editbind KP8        [ editmat death ]
+editbind KP9        [ editmat ladder ]
+
+texturerehash = [ compactvslots 1; exec "config/map/textures.cfg" ]
+
+dobindsearch = [
+    [search@[arg2]binds] $arg1 5 "^f{" "}" (? $textkeyseps (? $textkeybg "|" ", ") (? $textkeybg "" " ")) (? $textkeyseps (? $textkeybg "|" " or ") (? $textkeybg "" " "))
+]
diff --git a/config/sounds/announcer.cfg b/config/sounds/announcer.cfg
new file mode 100644
index 0000000..b505298
--- /dev/null
+++ b/config/sounds/announcer.cfg
@@ -0,0 +1,37 @@
+registersound   "sounds/announcer/secured"              150 //  S_V_FLAGSECURED
+registersound   "sounds/announcer/overthrown"           150 //  S_V_FLAGOVERTHROWN
+registersound   "sounds/announcer/alert"                150 //  S_V_FLAGPICKUP
+registersound   "sounds/announcer/warning"              150 //  S_V_FLAGDROP
+registersound   "sounds/announcer/returned"             150 //  S_V_FLAGRETURN
+registersound   "sounds/announcer/captured"             150 //  S_V_FLAGSCORE
+registersound   "sounds/announcer/reset"                150 //  S_V_FLAGRESET
+registersound   "sounds/announcer/fight"                150 //  S_V_BOMBSTART
+registersound   "sounds/announcer/alert"                150 //  S_V_BOMBDUEL
+registersound   "sounds/announcer/secured"              150 //  S_V_BOMBPICKUP
+registersound   "sounds/announcer/score"                150 //  S_V_BOMBSCORE
+registersound   "sounds/announcer/reset"                150 //  S_V_BOMBRESET
+registersound   "sounds/interface/back"                 224 //  S_V_NOTIFY
+registersound   "sounds/announcer/fight"                150 //  S_V_FIGHT
+registersound   "sounds/announcer/fight"                128 //  S_V_START
+registersound   "sounds/announcer/checkpoint"           128 //  S_V_CHECKPOINT
+registersound   "sounds/announcer/score"                192 //  S_V_COMPLETE
+registersound   "sounds/announcer/overtime"             150 //  S_V_OVERTIME
+registersound   "sounds/announcer/oneminute"            150 //  S_V_ONEMINUTE
+registersound   "sounds/announcer/headshot"             150 //  S_V_HEADSHOT
+registersound   "sounds/announcer/spree"                180 //  S_V_SPREE1
+registersound   "sounds/announcer/spree"                180 //  S_V_SPREE2
+registersound   "sounds/announcer/spree"                180 //  S_V_SPREE3
+registersound   "sounds/announcer/spreex"               180 //  S_V_SPREE4
+registersound   "sounds/announcer/multikill"            180 //  S_V_MKILL1
+registersound   "sounds/announcer/multikill2"           180 //  S_V_MKILL2
+registersound   "sounds/announcer/multikill3"           180 //  S_V_MKILL3
+registersound   "sounds/announcer/revenge"              180 //  S_V_REVENGE
+registersound   "sounds/announcer/dominating"           180 //  S_V_DOMINATE
+registersound   "sounds/announcer/firstblood"           180 //  S_V_FIRSTBLOOD
+registersound   "sounds/announcer/breaker"              180 //  S_V_BREAKER
+registersound   "sounds/announcer/youwin"               150 //  S_V_YOUWIN
+registersound   "sounds/announcer/youlose"              150 //  S_V_YOULOSE
+registersound   "sounds/announcer/draw"                 150 //  S_V_DRAW
+registersound   "sounds/announcer/fragged"              80  //  S_V_FRAGGED
+registersound   "sounds/announcer/warning"              150 //  S_V_BALWARN
+registersound   "sounds/announcer/alert"                150 //  S_V_BALALERT
diff --git a/config/sounds/interface.cfg b/config/sounds/interface.cfg
new file mode 100644
index 0000000..40b4938
--- /dev/null
+++ b/config/sounds/interface.cfg
@@ -0,0 +1,3 @@
+registersound   "sounds/interface/press"    255 //  S_GUIPRESS (gui button pressed)
+registersound   "sounds/interface/back"     255 //  S_GUIBACK (gui back one level)
+registersound   "sounds/interface/change"   255 //  S_GUIACT (gui up one level)
diff --git a/config/sounds/package.cfg b/config/sounds/package.cfg
new file mode 100644
index 0000000..ca65f09
--- /dev/null
+++ b/config/sounds/package.cfg
@@ -0,0 +1,5 @@
+exec "config/sounds/interface.cfg"
+exec "config/sounds/player.cfg"
+exec "config/sounds/sfx.cfg"
+exec "config/sounds/announcer.cfg"
+exec "config/sounds/weapons.cfg"
diff --git a/config/sounds/player.cfg b/config/sounds/player.cfg
new file mode 100644
index 0000000..a3e22c5
--- /dev/null
+++ b/config/sounds/player.cfg
@@ -0,0 +1,7 @@
+registersound   "sounds/player/jump"            64  100     0   0   //  S_JUMP (player jump)
+registersound   "sounds/player/impulse"         64  100     0   3   //  S_IMPULSE (player impulse)
+registersound   "sounds/player/land"            128 100     0   3   //  S_LAND (player landed with high velocity)
+registersound   "sounds/player/footstep"        248 300     0   5   //  S_FOOTSTEP (default footsteps)
+registersound   "sounds/player/swimstep"        248 200     0   4   //  S_SWIMSTEP (liquid footsteps)
+registersound   "sounds/player/pain"            130 200     0   6   //  S_PAIN (player damage)
+registersound   "sounds/player/death"           200 270     0   5   //  S_DEATH (player die)
diff --git a/config/sounds/sfx.cfg b/config/sounds/sfx.cfg
new file mode 100644
index 0000000..3e8d7e6
--- /dev/null
+++ b/config/sounds/sfx.cfg
@@ -0,0 +1,27 @@
+registersound   "sounds/sfx/splashin"           128 100     0   3   //  S_SPLASH1 (entering water)
+registersound   "sounds/sfx/splashout"          128 100     0   3   //  S_SPLASH2 (leaving water)
+registersound   "sounds/sfx/splosh"             128 160     0   3   //  S_SPLOSH (gibs bounce)
+registersound   "sounds/sfx/debris"             128 100     0   3   //  S_DEBRIS (debris bounce)
+registersound   "sounds/sfx/burning"            224 100     0   0   //  S_BURNLAVA (set on fire by lava)
+registersound   "sounds/sfx/extinguish"         156 200     0   3   //  S_EXTINGUISH (extinguish fire)
+registersound   "sounds/sfx/shell"              64  100     0   6   //  S_SHELL (shell ejection bounce)
+registersound   "sounds/sfx/itemuse"            128 100     0   0   //  S_ITEMUSE (non-weapon item used)
+registersound   "sounds/sfx/itemspawn"          192 300     0   0   //  S_ITEMSPAWN (non-weapon item spawned)
+registersound   "sounds/sfx/regenerate"         210 200     0   0   //  S_REGEN (health regenerating)
+registersound   "sounds/sfx/damage"             128 100     0   0   //  S_DAMAGE (damage dealt 0-9)
+registersound   "sounds/sfx/damage2"            128 100     0   0   //  S_DAMAGE2 (damage dealt 10-24)
+registersound   "sounds/sfx/damage3"            128 100     0   0   //  S_DAMAGE3 (damage dealt 25-49)
+registersound   "sounds/sfx/damage4"            128 100     0   0   //  S_DAMAGE4 (damage dealt 50-74)
+registersound   "sounds/sfx/damage5"            128 100     0   0   //  S_DAMAGE5 (damage dealt 75-99)
+registersound   "sounds/sfx/damage6"            128 100     0   0   //  S_DAMAGE6 (damage dealt 100-149)
+registersound   "sounds/sfx/damage7"            128 100     0   0   //  S_DAMAGE7 (damage dealt 150-199)
+registersound   "sounds/sfx/damage8"            128 100     0   0   //  S_DAMAGE8 (damage dealt 200+)
+registersound   "sounds/sfx/burndamage"         50  100     0   0   //  S_BURNED (damage from residual burning)
+registersound   "sounds/sfx/bleeddamage"        50  100     0   0   //  S_BLEED (damage from residual bleeding)
+registersound   "sounds/sfx/shockdamage"        50  100     0   0   //  S_SHOCK (damage from residual shocking)
+registersound   "sounds/sfx/respawn"            156 200     0   0   //  S_RESPAWN (player respawned)
+registersound   "sounds/sfx/chat"               254 200     0   0   //  S_CHAT (chat beep)
+registersound   "sounds/sfx/error"              230 200     0   0   //  S_ERROR (disallowed action)
+registersound   "sounds/sfx/alarm"              180 200     0   0   //  S_ALARM (hurt team member)
+registersound   "sounds/sfx/catch"              230 280     0   0   //  S_CATCH (catch/pickup affinity)
+registersound   "sounds/sfx/bounce"             248 400     0   3   //  S_BOUNCE (bouncing affinity)
diff --git a/config/sounds/weapons.cfg b/config/sounds/weapons.cfg
new file mode 100644
index 0000000..87dc2f7
--- /dev/null
+++ b/config/sounds/weapons.cfg
@@ -0,0 +1,260 @@
+registersound   <none>                                  0   0       0   0   //   primary fire
+registersound   <none>                                  0   0       0   0   // melee secondary fire
+registersound   <none>                                  0   0       0   0   // melee primary powerup/cook
+registersound   <none>                                  0   0       0   0   // melee secondary powerup/cook
+registersound   <none>                                  0   0       0   0   // melee zoom
+registersound   "sounds/weapons/switch"                 156 100     0   0   // melee switch to weapon
+registersound   "sounds/weapons/reload"                 192 100     0   0   // melee reload weapon
+registersound   <none>                                  0   0       0   0   // melee notify reload complete
+registersound   <none>                                  0   0       0   0   // melee primary projectile destroy (explode)
+registersound   <none>                                  0   0       0   0   // melee secondary projectile destroy (explode)
+registersound   <none>                                  0   0       0   0   // melee primary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // melee secondary projectile destroy (non-explode)
+registersound   "sounds/weapons/thud"                   192 120     0   3   // melee primary projectile impact
+registersound   "sounds/weapons/thud"                   224 160     0   3   // melee secondary projectile impact
+registersound   <none>                                  0   0       0   0   // melee primary projectile extinguished
+registersound   <none>                                  0   0       0   0   // melee secondary projectile extinguished
+registersound   <none>                                  0   0       0   0   // melee primary projectile in transit
+registersound   <none>                                  0   0       0   0   // melee secondary projectile in transit
+registersound   "sounds/weapons/ricochet"               36  80      0   0   // melee primary projectile bounce/ricochet
+registersound   "sounds/weapons/ricochet"               36  80      0   0   // melee secondary projectile bounce/ricochet
+registersound   "sounds/weapons/pistol/primary"         150 180     0   3   // pistol primary fire
+registersound   "sounds/weapons/pistol/secondary"       150 180     0   2   // pistol secondary fire
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // pistol primary powerup/cook
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // pistol secondary powerup/cook
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // pistol zoom
+registersound   "sounds/weapons/pistol/notify"          170 80      0   0   // pistol switch to weapon
+registersound   "sounds/weapons/pistol/reload"          170 80      0   0   // pistol reload weapon
+registersound   "sounds/weapons/pistol/notify"          170 80      0   0   // pistol notify reload complete
+registersound   "sounds/sfx/shell"                      100 50      0   6   // pistol primary projectile destroy (explode)
+registersound   "sounds/sfx/shell"                      100 50      0   6   // pistol secondary projectile destroy (explode)
+registersound   "sounds/sfx/shell"                      100 50      0   6   // pistol primary projectile destroy (non-explode)
+registersound   "sounds/sfx/shell"                      100 50      0   6   // pistol secondary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // pistol primary projectile impact
+registersound   <none>                                  0   0       0   0   // pistol secondary projectile impact
+registersound   "sounds/sfx/shell"                      100 50      0   6   // pistol primary projectile extinguished
+registersound   "sounds/sfx/shell"                      10  50      0   6   // pistol secondary projectile extinguished
+registersound   <none>                                  0   0       0   0   // pistol primary projectile in transit
+registersound   <none>                                  0   0       0   0   // pistol secondary projectile in transit
+registersound   "sounds/weapons/ricochet"               36  80      0   0   // pistol primary projectile bounce/ricochet
+registersound   "sounds/weapons/ricochet"               36  80      0   0   // pistol secondary projectile bounce/ricochet
+registersound   "sounds/weapons/sword/primary"          205 150     0   0   // sword primary fire
+registersound   "sounds/weapons/sword/secondary"        205 150     0   0   // sword secondary fire
+registersound   "sounds/weapons/sword/power"            160 100     0   0   // sword primary powerup/cook
+registersound   "sounds/weapons/sword/power"            160 100     0   0   // sword secondary powerup/cook
+registersound   "sounds/weapons/sword/power"            160 100     0   0   // sword zoom
+registersound   "sounds/weapons/sword/switch"           185 100     0   0   // sword switch to weapon
+registersound   "sounds/weapons/reload"                 185 100     0   0   // sword reload weapon
+registersound   "sounds/weapons/notify"                 185 100     0   0   // sword notify reload complete
+registersound   "sounds/weapons/explode"                32  200     0   3   // sword primary projectile destroy (explode)
+registersound   "sounds/weapons/explode"                32  200     0   3   // sword secondary projectile destroy (explode)
+registersound   <none>                                  0   0       0   0   // sword primary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // sword secondary projectile destroy (non-explode)
+registersound   "sounds/weapons/thwack"                 192 120     0   3   // sword primary projectile impact
+registersound   "sounds/weapons/thwack"                 224 160     0   3   // sword secondary projectile impact
+registersound   <none>                                  0   0       0   0   // sword primary projectile extinguished
+registersound   <none>                                  0   0       0   0   // sword secondary projectile extinguished
+registersound   <none>                                  0   0       0   0   // sword primary projectile in transit
+registersound   <none>                                  0   0       0   0   // sword secondary projectile in transit
+registersound   "sounds/weapons/tink"                   12  50      0   0   // sword primary projectile bounce/ricochet
+registersound   "sounds/weapons/tink"                   12  50      0   0   // sword secondary projectile bounce/ricochet
+registersound   "sounds/weapons/sword/switch"           185 100     0   0   // sword item used
+registersound   "sounds/sfx/itemspawn"                  192 150     0   0   // sword item spawned
+registersound   "sounds/weapons/shotgun/primary"        200 300     0   2   // shotgun primary fire
+registersound   "sounds/weapons/shotgun/secondary"      190 300     0   0   // shotgun secondary fire
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // shotgun primary powerup/cook
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // shotgun secondary powerup/cook
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // shotgun zoom
+registersound   "sounds/weapons/shotgun/notify"         130 100     0   0   // shotgun switch to weapon
+registersound   "sounds/weapons/shotgun/reload"         150 100     0   0   // shotgun reload weapon
+registersound   "sounds/weapons/shotgun/notify"         110 100     0   0   // shotgun notify reload complete
+registersound   "sounds/sfx/shell"                      80  50      0   6   // shotgun primary projectile destroy (explode)
+registersound   "sounds/weapons/shotgun/explode"        100 100     0   2   // shotgun secondary projectile destroy (explode)
+registersound   "sounds/sfx/shell"                      80  50      0   6   // shotgun primary projectile destroy (non-explode)
+registersound   "sounds/weapons/shotgun/explode"        100 100     0   2   // shotgun secondary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // shotgun primary projectile impact
+registersound   <none>                                  0   0       0   0   // shotgun secondary projectile impact
+registersound   "sounds/sfx/shell"                      40	50      0   6   // shotgun primary projectile extinguished
+registersound   "sounds/sfx/shell"                      80	50      0   6   // shotgun secondary projectile extinguished
+registersound   "sounds/weapons/whizz"                  25  40      0   0   // shotgun primary projectile in transit
+registersound   <none>                                  0   0       0   0   // shotgun secondary projectile in transit
+registersound   "sounds/sfx/shell"                      170 50      0   6   // shotgun primary projectile bounce/ricochet
+registersound   "sounds/sfx/shell"                      170 50      0   6   // shotgun secondary projectile bounce/ricochet
+registersound   "sounds/weapons/shotgun/notify"         130 100     0   0   // shotgun item used
+registersound   "sounds/sfx/itemspawn"                  192 150     0   0   // shotgun item spawned
+registersound   "sounds/weapons/smg/primary"            210 270     0   0   // smg primary fire
+registersound   "sounds/weapons/smg/secondary"          200 270     0   3   // smg secondary fire
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // smg primary powerup/cook
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // smg secondary powerup/cook
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // smg zoom
+registersound   "sounds/weapons/smg/notify"             180 100     0   0   // smg switch to weapon
+registersound   "sounds/weapons/smg/reload"             130 100     0   0   // smg reload weapon
+registersound   "sounds/weapons/smg/notify"             224 100     0   0   // smg notify reload complete
+registersound   "sounds/weapons/shotgun/explode"        120 200     0   2   // smg primary projectile destroy (explode)
+registersound   "sounds/weapons/shotgun/explode"        120 200     0   2   // smg secondary projectile destroy (explode)
+registersound   "sounds/weapons/bzap"                   48  100     0   0   // smg primary projectile destroy (non-explode)
+registersound   "sounds/weapons/bzap"                   48  100     0   0   // smg secondary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // smg primary projectile impact
+registersound   <none>                                  0   0       0   0   // smg secondary projectile impact
+registersound   "sounds/sfx/shell"                      170 50      0   6   // smg primary projectile extinguished
+registersound   "sounds/sfx/shell"                      170 50      0   6   // smg secondary projectile extinguished
+registersound   "sounds/weapons/whizz"                  40  50      0   0   // smg primary projectile in transit
+registersound   "sounds/weapons/whizz"                  30  50      0   0   // smg secondary projectile in transit
+registersound   "sounds/sfx/shell"                      170 50      0   6   // smg primary projectile bounce/ricochet
+registersound   "sounds/sfx/shell"                      170 50      0   6   // smg secondary projectile bounce/ricochet
+registersound   "sounds/weapons/smg/notify"             180 100     0   0   // smg item used
+registersound   "sounds/sfx/itemspawn"                  120 150     0   0   // smg item spawned
+registersound   "sounds/weapons/flamer/primary"         230 150     0   0   // flamer primary fire
+registersound   "sounds/weapons/flamer/secondary"       250 170     0   0   // flamer secondary fire
+registersound   "sounds/weapons/flamer/power"           220 100     0   0   // flamer primary powerup/cook
+registersound   "sounds/weapons/flamer/power"           220 100     0   0   // flamer secondary powerup/cook
+registersound   "sounds/weapons/flamer/power"           220 100     0   0   // flamer zoom
+registersound   "sounds/weapons/flamer/notify"          180 100     0   0   // flamer switch to weapon
+registersound   "sounds/weapons/flamer/reload"          240 100     0   0   // flamer reload weapon
+registersound   "sounds/weapons/flamer/notify"          180 100     0   0   // flamer notify reload complete
+registersound   <none>                                  0   0       0   0   // flamer primary projectile destroy (explode)
+registersound   <none>                                  0   0       0   0   // flamer secondary projectile destroy (explode)
+registersound   <none>                                  0   0       0   0   // flamer primary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // flamer secondary projectile destroy (non-explode)
+registersound   "sounds/weapons/singe"                  156 150     0   3   // flamer primary projectile impact
+registersound   "sounds/weapons/singe"                  98  100     0   3   // flamer secondary projectile impact
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // flamer primary projectile extinguished
+registersound   "sounds/weapons/bzzt"                   128 100     0   0   // flamer secondary projectile extinguished
+registersound   <none>                                  0   0       0   0   // flamer primary projectile in transit
+registersound   <none>                                  0   0       0   0   // flamer secondary projectile in transit
+registersound   <none>                                  0   0       0   0   // flamer primary projectile bounce/ricochet
+registersound   <none>                                  0   0       0   0   // flamer secondary projectile bounce/ricochet
+registersound   "sounds/weapons/flamer/notify"          180 100     0   0   // flamer item used
+registersound   "sounds/sfx/itemspawn"                  192 100     0   0   // flamer item spawned
+registersound   "sounds/weapons/plasma/primary"         240 280     0   3   // plasma primary fire
+registersound   "sounds/weapons/plasma/secondary"       200 500     0   0   // plasma secondary fire
+registersound   "sounds/weapons/plasma/power"           170 100     0   0   // plasma primary powerup/cook
+registersound   "sounds/weapons/plasma/power"           170 100     0   0   // plasma secondary powerup/cook
+registersound   "sounds/weapons/plasma/power"           170 100     0   0   // plasma zoom
+registersound   "sounds/weapons/plasma/notify"          200 100     0   0   // plasma switch to weapon
+registersound   "sounds/weapons/plasma/reload"          170 100     0   0   // plasma reload weapon
+registersound   "sounds/weapons/plasma/notify"          200 100     0   0   // plasma notify reload complete
+registersound   "sounds/weapons/energy"                 128 100     0   0   // plasma primary projectile destroy (explode)
+registersound   "sounds/weapons/energy"                 128 100     0   0   // plasma secondary projectile destroy (explode)
+registersound   "sounds/weapons/energy"                 128 100     0   0   // plasma primary projectile destroy (non-explode)
+registersound   "sounds/weapons/energy"                 128 100     0   0   // plasma secondary projectile destroy (non-explode)
+registersound   "sounds/weapons/singe"                  128 100     0   3   // plasma primary projectile impact
+registersound   <none>                                  0   0       0   0   // plasma secondary projectile impact
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // plasma primary projectile extinguished
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // plasma secondary projectile extinguished
+registersound   "sounds/weapons/plasma/transit"         50  50      0   0   // plasma primary projectile in transit
+registersound   "sounds/weapons/plasma/transit"         128 150     0   0   // plasma secondary projectile in transit
+registersound   "sounds/weapons/bzzt"                   128 100     0   0   // plasma primary projectile bounce/ricochet
+registersound   "sounds/weapons/bzzt"                   128 100     0   0   // plasma secondary projectile bounce/ricochet
+registersound   "sounds/weapons/plasma/notify"          200 100     0   0   // plasma item used
+registersound   "sounds/sfx/itemspawn"                  192 100     0   0   // plasma item spawned
+registersound   "sounds/weapons/zapper/primary"         200 250     0   4   // zapper primary fire
+registersound   "sounds/weapons/zapper/secondary"       200 350     0   0   // zapper secondary fire
+registersound   "sounds/weapons/zapper/power"           170 100     0   0   // zapper primary powerup/cook
+registersound   "sounds/weapons/zapper/power"           170 100     0   0   // zapper secondary powerup/cook
+registersound   "sounds/weapons/zapper/power"           170 100     0   0   // zapper zoom
+registersound   "sounds/weapons/zapper/notify"          200 100     0   0   // zapper switch to weapon
+registersound   "sounds/weapons/plasma/reload"          170 100     0   0   // zapper reload weapon
+registersound   "sounds/weapons/zapper/notify"          240 100     0   0   // zapper notify reload complete
+registersound   "sounds/weapons/energy"                 64  100     0   0   // zapper primary projectile destroy (explode)
+registersound   "sounds/weapons/energy"                 64  100     0   0   // zapper secondary projectile destroy (explode)
+registersound   <none>                                  0   0       0   0   // zapper primary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // zapper secondary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // zapper primary impact
+registersound   <none>                                  0   0       0   0   // zapper secondary impact
+registersound   "sounds/sfx/extinguish"                 64  200     0   0   // zapper primary projectile extinguished
+registersound   "sounds/sfx/extinguish"                 64  200     0   0   // zapper secondary projectile extinguished
+registersound   <none>                                  0   0       0   0   // zapper primary projectile in transit
+registersound   <none>                                  0   0       0   0   // zapper secondary projectile in transit
+registersound   "sounds/weapons/bzzt"                   128 100     0   0   // zapper primary projectile bounce/ricochet
+registersound   "sounds/weapons/bzzt"                   128 100     0   0   // zapper secondary projectile bounce/ricochet
+registersound   "sounds/weapons/zapper/notify"          240 100     0   0   // zapper item used
+registersound   "sounds/sfx/itemspawn"                  192 100     0   0   // zapper item spawned
+registersound   "sounds/weapons/rifle/primary"          245 300     0   3   // rifle primary fire
+registersound   "sounds/weapons/rifle/secondary"        220 400     0   0   // rifle secondary fire
+registersound   "sounds/weapons/plasma/power"           224 100     0   0   // rifle primary powerup/cook
+registersound   "sounds/weapons/plasma/power"           224 100     0   0   // rifle secondary powerup/cook
+registersound   "sounds/weapons/plasma/power"           224 100     0   0   // rifle zoom
+registersound   "sounds/weapons/rifle/notify"           220 100     0   0   // rifle switch to weapon
+registersound   "sounds/weapons/rifle/reload"           125 100     0   0   // rifle reload weapon
+registersound   "sounds/weapons/rifle/notify"           190 100     0   0   // rifle notify reload complete
+registersound   "sounds/weapons/rifle/explode"          200 350     0   3   // rifle primary projectile destroy (explode)
+registersound   "sounds/weapons/rifle/explode"          200 350     0   3   // rifle secondary projectile destroy (explode)
+registersound   "sounds/weapons/energy"                 128 100     0   0   // rifle primary projectile destroy (non-explode)
+registersound   "sounds/weapons/energy"                 128 100     0   0   // rifle secondary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // rifle primary projectile impact
+registersound   <none>                                  0   0       0   0   // rifle secondary projectile impact
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // rifle primary projectile extinguished
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // rifle secondary projectile extinguished
+registersound   "sounds/weapons/rifle/transit"          156 500     0   0   // rifle primary projectile in transit
+registersound   "sounds/weapons/rifle/transit"          156 500     0   0   // rifle secondary projectile in transit
+registersound   "sounds/weapons/bzzt"                   64  100     0   0   // rifle primary projectile bounce/ricochet
+registersound   "sounds/weapons/bzzt"                   64  100     0   0   // rifle secondary projectile bounce/ricochet
+registersound   "sounds/weapons/rifle/notify"           220 100     0   0   // rifle item used
+registersound   "sounds/sfx/itemspawn"                  192 150     0   0   // rifle item spawned
+registersound   "sounds/weapons/grenade/primary"        128 200     0   0   // grenade primary fire
+registersound   "sounds/weapons/grenade/primary"        128 200     0   0   // grenade secondary fire
+registersound   "sounds/weapons/grenade/beep"           192 100     0   0   // grenade primary powerup/cook
+registersound   "sounds/weapons/grenade/beep"           192 100     0   0   // grenade secondary powerup/cook
+registersound   "sounds/weapons/grenade/beep"           192 100     0   0   // grenade zoom
+registersound   "sounds/weapons/grenade/switch"         105 100     0   0   // grenade switch to weapon
+registersound   "sounds/weapons/reload"                 192 100     0   0   // grenade reload weapon
+registersound   "sounds/weapons/grenade/switch"         105 100     0   0   // grenade notify reload complete
+registersound   "sounds/weapons/explode"                224 600     0   3   // grenade primary projectile destroy (explode)
+registersound   "sounds/weapons/explode"                224 600     0   3   // grenade secondary projectile destroy (explode)
+registersound   "sounds/weapons/explode"                224 600     0   3   // grenade primary projectile destroy (non-explode)
+registersound   "sounds/weapons/explode"                224 600     0   3   // grenade secondary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // grenade primary projectile impact
+registersound   <none>                                  0   0       0   0   // grenade secondary projectile impact
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // grenade primary projectile extinguished
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // grenade secondary projectile extinguished
+registersound   "sounds/weapons/grenade/beep"           192 200     0   0   // grenade primary projectile in transit
+registersound   "sounds/weapons/grenade/beep"           192 200     0   0   // grenade secondary projectile in transit
+registersound   "sounds/weapons/tink"                   128 200     0   0   // grenade primary projectile bounce/ricochet
+registersound   "sounds/weapons/tink"                   128 200     0   0   // grenade secondary projectile bounce/ricochet
+registersound   "sounds/weapons/grenade/switch"         105 100     0   0   // grenade item used
+registersound   "sounds/sfx/itemspawn"                  192 150     0   0   // grenade item spawned
+registersound   "sounds/weapons/grenade/primary"        128 200     0   0   // mine primary fire
+registersound   "sounds/weapons/grenade/primary"        128 200     0   0   // mine secondary fire
+registersound   "sounds/weapons/mine/beep"              156 100     0   0   // mine primary powerup/cook
+registersound   "sounds/weapons/mine/beep"              156 100     0   0   // mine secondary powerup/cook
+registersound   "sounds/weapons/mine/beep"              156 100     0   0   // mine zoom
+registersound   "sounds/weapons/grenade/switch"         105 100     0   0   // mine switch to weapon
+registersound   "sounds/weapons/reload"                 192 100     0   0   // mine reload weapon
+registersound   "sounds/weapons/grenade/switch"         105 100     0   0   // mine notify reload complete
+registersound   "sounds/weapons/mine/explode"           224 600     0   3   // mine primary projectile destroy (explode)
+registersound   "sounds/weapons/mine/explode"           224 600     0   3   // mine secondary projectile destroy (explode)
+registersound   "sounds/weapons/mine/explode"           224 600     0   3   // mine primary projectile destroy (non-explode)
+registersound   "sounds/weapons/mine/explode"           224 600     0   3   // mine secondary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // mine primary projectile impact
+registersound   <none>                                  0   0       0   0   // mine secondary projectile impact
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // mine primary projectile extinguished
+registersound   "sounds/sfx/extinguish"                 156 200     0   0   // mine secondary projectile extinguished
+registersound   "sounds/weapons/mine/beep"              156 100     0   0   // mine primary projectile in transit
+registersound   "sounds/weapons/mine/beep"              156 100     0   0   // mine secondary projectile in transit
+registersound   "sounds/weapons/tink"                   128 200     0   0   // mine primary projectile bounce/ricochet
+registersound   "sounds/weapons/tink"                   128 200     0   0   // mine secondary projectile bounce/ricochet
+registersound   "sounds/weapons/grenade/switch"         105 100     0   0   // mine item used
+registersound   "sounds/sfx/itemspawn"                  192 150     0   0   // mine item spawned
+registersound   "sounds/weapons/rocket/primary"         255 400     0   3   // rocket primary fire
+registersound   "sounds/weapons/rocket/primary"         255 400     0   3   // rocket secondary fire
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // rocket primary powerup/cook
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // rocket secondary powerup/cook
+registersound   "sounds/weapons/rocket/power"           225 100     0   0   // rocket zoom
+registersound   "sounds/weapons/rocket/reload"          200 100     0   0   // rocket switch to weapon
+registersound   "sounds/weapons/rocket/reload"          250 100     0   0   // rocket reload weapon
+registersound   "sounds/weapons/rocket/notify"          255 100     0   0   // rocket notify reload complete
+registersound   "sounds/weapons/explode"                224 800     0   3   // rocket primary projectile destroy (explode)
+registersound   "sounds/weapons/explode"                224 800     0   3   // rocket secondary projectile destroy (explode)
+registersound   "sounds/weapons/explode"                224 800     0   3   // rocket primary projectile destroy (non-explode)
+registersound   "sounds/weapons/explode"                224 800     0   3   // rocket secondary projectile destroy (non-explode)
+registersound   <none>                                  0   0       0   0   // rocket primary projectile impact
+registersound   <none>                                  0   0       0   0   // rocket secondary projectile impact
+registersound   "sounds/sfx/extinguish"                 155 200     0   0   // rocket primary projectile extinguished
+registersound   "sounds/sfx/extinguish"                 155 200     0   0   // rocket secondary projectile extinguished
+registersound   "sounds/weapons/rocket/transit"         255 300     0   0   // rocket primary projectile in transit
+registersound   "sounds/weapons/rocket/transit"         255 300     0   0   // rocket secondary projectile in transit
+registersound   "sounds/weapons/tink"                   128 200     0   0   // rocket primary projectile bounce/ricochet
+registersound   "sounds/weapons/tink"                   128 200     0   0   // rocket secondary projectile bounce/ricochet
+registersound   "sounds/weapons/rocket/notify"          250 100     0   0   // rocket item used
+registersound   "sounds/sfx/itemspawn"                  192 150     0   0   // rocket item spawned
diff --git a/config/stdlib.cfg b/config/stdlib.cfg
new file mode 100644
index 0000000..c43086b
--- /dev/null
+++ b/config/stdlib.cfg
@@ -0,0 +1,37 @@
+// console language standard library
+
+// creates a macro whose body is a format str
+// i.e. macro greet [ say Hi, %1! ]
+macro = [$arg1 = (concat [format [@@arg2]] (loopconcat i $numargs [result [$arg@(+ $i 1)]]))]
+
+append = [$arg1 = (concat (getalias $arg1) $arg2)]
+
+swapval = [
+    local tmp
+    tmp = $$arg1
+    $arg1 = $$arg2
+    $arg2 = $tmp
+]
+
+// binds a key so that it will toggle a variable
+// i.e. bindvar 9 thirdperson
+bindvar = [
+    bind $arg1 [@arg2 (= $@arg2 0); if (= $@arg2 0) [echo @@arg2 OFF] [ echo @@arg2 ON]]
+]; setcomplete bindvar 1
+
+// same as above, but only binds for edit mode
+editbindvar = [
+    editbind $arg1 [@arg2 (= $@arg2 0); if (= $@arg2 0) [echo @@arg2 OFF] [ echo @@arg2 ON]]
+]; setcomplete editbindvar 1
+
+// binds a key so that it will set a modifier while held down
+bindmod = [
+    bind $arg1 [@arg2 1; onrelease [@@arg2 0]]
+]; setcomplete bindmod 1
+
+// same as above, but only binds for edit mode
+editbindmod = [
+    editbind $arg1 [@arg2 1; onrelease [@@arg2 0]]
+]; setcomplete editbindmod 1
+
+quine = [ echo (format "quine = [%1]" $quine) ]; setcomplete quine 1
diff --git a/config/stdshader.cfg b/config/stdshader.cfg
new file mode 100644
index 0000000..09f553c
--- /dev/null
+++ b/config/stdshader.cfg
@@ -0,0 +1,2848 @@
+// standard shader definitions
+
+fpopts = [
+    @(if (= $shaderprecision 0) [result "OPTION ARB_precision_hint_fastest;"])
+    @(if (= $shaderprecision 2) [result "OPTION ARB_precision_hint_nicest;"])
+]
+
+vpstart = [
+    !!ARBvp1.0
+    @(if $apple_ff_bug [result [
+        ATTRIB opos = vertex.position;
+        DP4 result.position.x, state.matrix.mvp.row[0], opos;
+        DP4 result.position.y, state.matrix.mvp.row[1], opos;
+        DP4 result.position.z, state.matrix.mvp.row[2], opos;
+        DP4 result.position.w, state.matrix.mvp.row[3], opos;
+    ]] [result [
+        OPTION ARB_position_invariant;
+        ATTRIB opos = vertex.position;
+    ]])
+]
+
+fpstart = [
+    !!ARBfp1.0
+    @fpopts
+]
+
+macro normalize [
+    DP3 %1.w, %2, %2;
+    RSQ %1.w, %1.w;
+    MUL %1.xyz, %1.w, %2;
+]
+
+fogcoord = [
+    DP4 result.fogcoord, -opos, state.matrix.modelview.row[2];
+]
+
+macro rgbafog [
+    TEMP fog;
+    SUB fog.x, state.fog.params.z, fragment.fogcoord.x;
+    MUL_SAT fog.x, fog.x, state.fog.params.w;
+    LRP result.color, fog.x, %1, %2;
+]
+
+lazyshader = [
+    defershader $arg1 $arg2 [
+        shader @arg1 @arg2 [@@arg3] [@@arg4]
+    ]
+]
+
+vertexparam0 = "program.env[16]"
+vertexparam1 = "program.env[17]"
+vertexparam2 = "program.env[18]"
+vertexparam3 = "program.env[19]"
+vertexparam4 = "program.env[20]"
+vertexparam5 = "program.env[21]"
+vertexparam6 = "program.env[22]"
+vertexparam7 = "program.env[23]"
+
+pixelparam0 = "program.env[16]"
+pixelparam1 = "program.env[17]"
+pixelparam2 = "program.env[18]"
+pixelparam3 = "program.env[19]"
+pixelparam4 = "program.env[20]"
+pixelparam5 = "program.env[21]"
+pixelparam6 = "program.env[22]"
+pixelparam7 = "program.env[23]"
+
+lmcoordscale = (divf 1 32767)
+
+///////////////////////////////////////////////////
+//
+// used for any textured polys that don't have a shader set
+//
+///////////////////////////////////////////////////
+
+shader 0 "default" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    END
+] [
+    @fpstart
+    TEMP diffuse;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MUL result.color, fragment.color, diffuse;
+    END
+]
+
+shader 0 "rect" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    END
+] [
+    @fpstart
+    TEMP diffuse;
+    TEX diffuse, fragment.texcoord[0], texture[0], RECT;
+    MUL result.color, fragment.color, diffuse;
+    END
+]
+
+shader 0 "cubemap" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    END
+] [
+    @fpstart
+    TEMP diffuse;
+    TEX diffuse, fragment.texcoord[0], texture[0], CUBE;
+    MUL result.color, fragment.color, diffuse;
+    END
+]
+
+shader 0 "rgbonly" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    END
+] [
+    @fpstart
+    TEMP diffuse;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MUL result.color.xyz, fragment.color, diffuse;
+    MOV result.color.w, fragment.color;
+    END
+]
+
+//////////////////////////////////////////////////////////////////////
+//
+// same, but now without texture sampling (some HUD stuff needs this)
+//
+//////////////////////////////////////////////////////////////////////
+
+shader 0 "notexture" [
+    @vpstart
+    MOV result.color, vertex.color;
+    END
+] [
+    @fpstart
+    MOV result.color, fragment.color;
+    END
+]
+
+shader 4 "notextureglsl" [
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_Color;
+    }
+]
+
+//////////////////////////////////////////////////////////////////////
+//
+// fogged variants of default shaders
+//
+//////////////////////////////////////////////////////////////////////
+
+shader 0 "fogged" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+    TEMP diffuse;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MUL result.color, fragment.color, diffuse;
+    END
+]
+
+shader 0 "foggednotexture" [
+    @vpstart
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+    MOV result.color, fragment.color;
+    END
+]
+
+shader 4 "foggednotextureglsl" [
+    #pragma CUBE2_fog
+    void main(void)
+    {
+        gl_Position = ftransform();
+        gl_FrontColor = gl_Color;
+    }
+] [
+    void main(void)
+    {
+        gl_FragColor = gl_Color;
+    }
+]
+
+//////////////////////////////////////////////////////////////////////
+//
+// for filling the z-buffer only (i.e. multi-pass rendering, OQ)
+//
+//////////////////////////////////////////////////////////////////////
+
+shader 0 "nocolor" [
+    @vpstart
+    END
+] [
+    @fpstart
+    END
+]
+
+// some OpenGL implementations don't have consistent depth computation between assembly and GLSL shaders
+shader 4 "nocolorglsl" [
+    void main() { gl_Position = ftransform(); }
+] [
+    void main() {}
+]
+
+////////////////////////////////////////////////////////
+//
+// default lightmapped world shader.. does texcoord gen
+//
+///////////////////////////////////////////////////////
+
+worldshader = [
+    stype = 0
+    if (>= (stringstr $arg1 "env") 0) [stype = (+ $stype 2)]
+    shader $stype $arg1 [
+        @vpstart
+        ADD result.texcoord[0].xy, vertex.texcoord[0], program.env[0];
+        MUL result.texcoord[1].xy, vertex.texcoord[1], @lmcoordscale;
+
+        @arg2
+
+        #pragma CUBE2_shadowmap
+        #pragma CUBE2_dynlight
+        @fogcoord
+        #pragma CUBE2_water
+
+        END
+    ] [
+        @fpstart
+        OPTION ARB_fog_linear;
+        TEMP diffuse, lm;
+        TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+        TEX lm,      fragment.texcoord[1], texture[1], 2D;
+
+        #pragma CUBE2_shadowmap lm
+        #pragma CUBE2_dynlight lm
+
+        @arg3
+
+        MUL diffuse, diffuse, program.env[6];
+        @(if (< $numargs 4) [result [MUL result.color, diffuse, lm;]] [result $arg4])
+
+        #pragma CUBE2_water
+
+        END
+    ]
+]
+
+glareworldshader = [
+    variantshader (if (< (stringstr $arg1 "env") 0) 0 2) $arg1 4 [
+        @vpstart
+        ADD result.texcoord[0].xy, vertex.texcoord[0], program.env[0];
+        MUL result.texcoord[1].xy, vertex.texcoord[1], @lmcoordscale;
+
+        @arg2
+
+        @fogcoord
+
+        END
+    ] [
+        @fpstart
+        OPTION ARB_fog_linear;
+
+        @arg3
+
+        END
+    ]
+]
+
+worldshader "stdworld" [] []
+
+defershader 0 "decalworld" [
+    worldshader "decalworld" [] [
+        TEMP decal;
+        TEX decal, fragment.texcoord[0], texture[2], 2D;
+        LRP diffuse.rgb, decal.w, decal, diffuse;
+    ]
+]
+
+defershader 0 "glowworld" [
+    defpixelparam "glowcolor" 0 1 1 1 // glow color
+    worldshader "glowworld" [] [] [
+        TEMP glow;
+        TEX glow, fragment.texcoord[0], texture[2], 2D;
+        MUL glow.rgb, glow, @pixelparam0;
+        MAD result.color.rgb, lm, diffuse, glow;
+        MUL result.color.a, lm.a, program.env[6];
+    ]
+    glareworldshader "glowworld" [] [
+        TEMP glow, k;
+        TEX glow, fragment.texcoord[0], texture[2], 2D;
+        MUL glow.rgb, glow, @pixelparam0;
+        MAX k.x, glow.r, glow.g;
+        MAX k.x, k.x, glow.b;
+        MUL k.x, k.x, k.x;
+        MUL_SAT k.x, k.x, 32;
+        MUL result.color.rgb, k.x, glow;
+        #pragma CUBE2_variantoverride TEX result.color.a, fragment.texcoord[1], texture[1], 2D;
+        MOV result.color.a, program.env[6];
+    ]
+]
+
+defershader 0 "pulseworld" [
+    defvertexparam "pulsespeed" 1 1 // pulse frequency (Hz)
+    worldshader "pulseworld" [
+        TEMP k;
+        MUL k.x, program.env[6], @vertexparam1.x;
+        FRC k.x, k.x;
+        MAD k.x, k.x, 2, -1;
+        ABS result.texcoord[2], k.x;
+    ] [
+        TEMP pulse;
+        TEX pulse, fragment.texcoord[0], texture[2], 2D;
+        LRP diffuse.rgb, fragment.texcoord[2].x, pulse, diffuse;
+    ]
+]
+
+defershader 0 "pulseglowworld" [
+    defvertexparam "glowcolor" 0 1 1 1 // glow color
+    defvertexparam "pulseglowspeed" 1 1 // pulse frequency (Hz)
+    defvertexparam "pulseglowcolor" 2 0 0 0 // pulse glow color
+    worldshader "pulseglowworld" [
+        TEMP k, col;
+        MUL k.x, program.env[6], @vertexparam1.x;
+        FRC k.x, k.x;
+        MAD k.x, k.x, 2, -1;
+        ABS k.x, k.x;
+        SUB col.xyz, @vertexparam2, @vertexparam0;
+        MAD result.texcoord[2].xyz, k.x, col, @vertexparam0;
+    ] [] [
+        TEMP glow;
+        TEX glow, fragment.texcoord[0], texture[2], 2D;
+        MUL glow.rgb, glow, fragment.texcoord[2];
+        MAD result.color.rgb, lm, diffuse, glow;
+        MUL result.color.a, lm.a, program.env[6];
+    ]
+    glareworldshader "pulseglowworld" [
+        TEMP k, col;
+        MUL k.x, program.env[6], @vertexparam1.x;
+        FRC k.x, k.x;
+        MAD k.x, k.x, 2, -1;
+        ABS k.x, k.x;
+        SUB col.xyz, @vertexparam2, @vertexparam0;
+        MAD result.texcoord[2].xyz, k.x, col, @vertexparam0;
+    ] [
+        TEMP glow, k;
+        TEX glow, fragment.texcoord[0], texture[2], 2D;
+        MUL glow.rgb, glow, fragment.texcoord[2];
+        MAX k.x, glow.r, glow.g;
+        MAX k.x, k.x, glow.b;
+        MUL k.x, k.x, k.x;
+        MUL_SAT k.x, k.x, 32;
+        MUL result.color.rgb, k.x, glow;
+        #pragma CUBE2_variantoverride TEX result.color.a, fragment.texcoord[1], texture[1], 2D;
+        MOV result.color.a, program.env[6];
+    ]
+]
+
+shader 0 "fogworld" [
+    @vpstart
+    #pragma CUBE2_water
+    END
+] [
+    @fpstart
+    MOV result.color, state.fog.color;
+    #pragma CUBE2_water
+    END
+]
+
+shader 0 "noglareworld" [
+    @vpstart
+    END
+] [
+    @fpstart
+    MOV result.color, 0;
+    END
+]
+
+shader 0 "noglareblendworld" [
+    @vpstart
+    MUL result.texcoord[0].xy, vertex.texcoord[1], @lmcoordscale;
+    @fogcoord
+    END
+] [
+    @fpstart
+    MOV result.color.rgb, 0;
+    TEX result.color.a, fragment.texcoord[0], texture[1], 2D;
+    END
+]
+
+shader 0 "noglarealphaworld" [
+    @vpstart
+    END
+] [
+    @fpstart
+    MOV result.color.rgb, 0;
+    MOV result.color.a, program.env[6];
+    END
+]
+
+defershader 2 "envworld" [
+    defpixelparam "envscale" 0 0.2 0.2 0.2 // reflectivity
+    worldshader "envworld" [
+        MOV result.texcoord[2].xyz, vertex.normal;
+        SUB result.texcoord[3].xyz, program.env[4], opos;
+    ] [
+        ATTRIB normal = fragment.texcoord[2];
+        ATTRIB camvec = fragment.texcoord[3];
+        TEMP rvec;
+        DP3 rvec.w, camvec, normal;
+        MUL rvec.xyz, rvec.w, normal;
+        MAD rvec.xyz, rvec, 2, -camvec;
+
+        TEMP reflect;
+        TEX reflect, rvec, texture[2], CUBE;
+    ] [
+        MUL diffuse, diffuse, lm;
+        LRP result.color.rgb, @pixelparam0, reflect, diffuse;
+        MOV result.color.a, diffuse.a;
+    ]
+
+    defpixelparam "envscale" 0 0.2 0.2 0.2 // reflectivity
+    worldshader "envworldfast" [
+        TEMP camvec, rvec;
+        SUB camvec.xyz, program.env[4], opos;
+        DP3 rvec.w, camvec, vertex.normal;
+        MUL rvec.xyz, rvec.w, vertex.normal;
+        MAD result.texcoord[2].xyz, rvec, 2, -camvec;
+    ] [
+        TEMP reflect;
+        TEX reflect, fragment.texcoord[2], texture[2], CUBE;
+    ] [
+        MUL diffuse, diffuse, lm;
+        LRP result.color.rgb, @pixelparam0, reflect, diffuse;
+        MOV result.color.a, diffuse.a;
+    ]
+
+    defpixelparam "envscale" 0 0.2 0.2 0.2 // reflectivity
+    worldshader "envworldalt" [] []
+
+    altshader envworld envworldalt
+    fastshader envworld envworldfast 2
+    fastshader envworld envworldalt 1
+]
+
+shader 0 depthfxworld [
+    @vpstart
+    TEMP z;
+    DP4 z.x, -opos, state.matrix.modelview.row[2];
+    MAD result.texcoord[0], z.x, program.env[0], program.env[1];
+    END
+] [
+    @fpstart
+    MOV result.color, fragment.texcoord[0];
+    END
+]
+
+shader 0 depthfxsplitworld [
+    @vpstart
+    TEMP z;
+    DP4 z.x, -opos, state.matrix.modelview.row[2];
+    MAD result.texcoord[0], z.x, program.env[0], program.env[1];
+    END
+] [
+    @fpstart
+    TEMP ranges;
+    MOV ranges.x, fragment.texcoord[0];
+    FRC ranges.yzw, fragment.texcoord[0];
+    MAD result.color, ranges.yzxw, { -0.00390625, -0.00390625, 0, 0 }, ranges;
+    END
+]
+
+// bumptype:
+//    e -> reserve envmap texture slot
+//    o -> orthonormalize
+//    t -> tangent space cam
+//    r -> envmap reflection
+//    R -> modulate envmap reflection with spec map
+//    s -> spec
+//    S -> spec map
+//    p -> parallax
+//    P -> steep parallax (7 steps)
+//    g -> glow
+//    G -> pulse glow
+//    i -> glare intensity
+
+btopt = [ >= (stringstr $bumptype $arg1) 0 ]
+
+bumpvariantshader = [
+    bumptype = $arg2
+    normtex = (if (btopt "e") [result "texture[4]"] [result "texture[3]"])
+    glowtex = (if (btopt "e") [result "texture[5]"] [result "texture[4]"])
+    stype = (if (btopt "e") [result 3] [result 1])
+    if (! (btopt "i")) [
+        if (btopt "G") [
+            defpixelparam "glowcolor" 0 1 1 1 // glow color
+            defvertexparam "pulseglowspeed" 4 1     // pulse frequency (Hz)
+            defpixelparam "pulseglowcolor" 5 0 0 0 // pulse glow color
+        ] [if (btopt "g") [
+            defpixelparam "glowcolor" 0 1 1 1  // glow color
+        ]]
+        if (btopt "S") [
+            defpixelparam "specscale" 1 6 6 6 // spec map multiplier
+        ] [if (btopt "s") [
+            defpixelparam "specscale" 1 1 1 1 // spec multiplier
+        ]]
+        if (|| (btopt "p") (btopt "P")) [
+            defpixelparam "parallaxscale" 2 0.06 -0.03 // parallax scaling
+        ]
+        if (btopt "R") [
+            defpixelparam "envscale" 3 1 1 1 // reflectivity map multiplier
+        ] [if (btopt "r") [
+            defpixelparam "envscale" 3 0.2 0.2 0.2 // reflectivity
+        ]]
+    ] [
+        if (btopt "s") [stype = (+ $stype 8)]
+    ]
+    variantshader $stype $arg1 (if (btopt "i") [result 4] [result -1]) [
+        @vpstart
+        ADD result.texcoord[0].xy, vertex.texcoord[0], program.env[0];
+        // need to store these in Z/W to keep texcoords < 6, otherwise kills performance on Radeons
+        // but slows lightmap access in fragment shader a bit, so avoid when possible
+        @(if (|| $minimizetcusage (btopt "r")) [result [
+            MUL result.texcoord[0].zw, vertex.texcoord[1].wzyx, @lmcoordscale;
+        ]] [result [
+            MUL result.texcoord[1].xy, vertex.texcoord[1], @lmcoordscale;
+        ]])
+        @fogcoord
+
+        @(if (btopt "o") [result [
+            ATTRIB normal = vertex.normal;
+            TEMP camv, tangent, bitangent;
+            MAD tangent, vertex.color, 2, -1;
+            XPD bitangent.xyz, normal, tangent;
+            MUL bitangent.xyz, bitangent, tangent.w;
+
+            @@(if (btopt "t") [result [
+                // trans eye vector into TS
+                SUB camv.xyz, program.env[4], opos;
+                DP3 @(if (btopt "r") [result "result.texcoord[1].x"] [result "result.texcoord[2].x"]), camv, tangent;
+                DP3 @(if (btopt "r") [result "result.texcoord[1].y"] [result "result.texcoord[2].y"]), camv, bitangent;
+                DP3 @(if (btopt "r") [result "result.texcoord[1].z"] [result "result.texcoord[2].z"]), camv, normal;
+            ]])
+            @@(if (btopt "r") [result [
+                @@(if (! (btopt "t")) [result [
+                    SUB result.texcoord[1].xyz, program.env[4], opos;
+                ]])
+
+                // calculate tangent -> world transform
+                MOV result.texcoord[2].xyz, tangent;
+                MOV result.texcoord[3].xyz, bitangent;
+                MOV result.texcoord[4].xyz, normal;
+            ]])
+        ]])
+
+        @(if (btopt "G") [result [
+            TEMP pulse, pulsecol;
+            MUL pulse, program.env[6], @vertexparam4.x;
+            FRC pulse, pulse;
+            MAD pulse, pulse, 2, -1;
+            ABS result.texcoord[1].w, pulse.x;
+        ]])
+
+        @(if (|| (! (btopt "i")) (btopt "s")) [result [
+            #pragma CUBE2_dynlight
+        ]])
+        @(if (! (btopt "i")) [result [
+            #pragma CUBE2_shadowmap
+            #pragma CUBE2_water
+        ]])
+
+        END
+    ] [
+        @fpstart
+        OPTION ARB_fog_linear;
+        ATTRIB @(if (|| (btopt "p") (btopt "P")) [result "htc"] [result "dtc"]) = fragment.texcoord[0];
+        ATTRIB lmtc = @(if (|| $minimizetcusage (btopt "r")) [result "fragment.texcoord[0]"] [result "fragment.texcoord[1]"]);
+        @(if (btopt "r") [result [
+            ATTRIB cam = fragment.texcoord[1];
+        ]] [if (btopt "t") [result [
+            ATTRIB cam = fragment.texcoord[2];
+        ]]])
+        TEMP diffuse, lmc, lmlv, bump;
+
+        @(if (|| (! (btopt "i")) (btopt "s")) [result [
+            TEX lmc,  @(if (|| $minimizetcusage (btopt "r")) [result "lmtc.wzyx"] [result "lmtc"]), texture[1], 2D;
+            MUL result.color.a, lmc, program.env[6];
+            TEX lmlv, @(if (|| $minimizetcusage (btopt "r")) [result "lmtc.wzyx"] [result "lmtc"]), texture[2], 2D;
+            MAD lmlv.xyz, lmlv, 2, -1;
+        ]])
+
+        @(if (btopt "t") [result [
+            TEMP camvts;
+            @(normalize camvts cam)
+        ]])
+
+        @(if (btopt "p") [result [
+            TEMP height;
+            TEX height, htc, @@normtex, 2D;
+            MAD height.w, height.w, @pixelparam2.x, @pixelparam2.y;
+            TEMP dtc;
+            MAD dtc.xy, height.w, camvts, htc;
+        ]])
+
+        @(if (btopt "P") [result [
+            PARAM step = -0.142857142857143; // 1 / 7
+            TEMP duv, dtc, cc;
+            RCP duv.w, camvts.z;
+            MUL duv.xyz, duv.w, camvts;
+            MUL duv.xyz, duv, step;
+            MUL duv.xy, duv, @pixelparam2.x;
+
+            MAD dtc.xy, duv, @pixelparam2.y, htc;
+            MOV dtc.z, 1.0;
+            TEX bump, dtc, @@normtex, 2D;
+
+            @@(loopconcat i 7 [concatword [
+                SLT cc.x, bump.w, dtc.z;
+                MAD dtc.xyz, duv, cc.x, dtc;
+                TEX bump, dtc, @@normtex, 2D;
+            ]])
+        ]])
+
+        @(if (|| (! (btopt "i")) (btopt "S")) [result [
+            TEX @(if (btopt "i") [result "diffuse.a"] [result "diffuse"]), dtc, texture[0], 2D;
+        ]])
+        @(if (! (btopt "i")) [result [
+            MUL diffuse.rgb, diffuse, program.env[6];
+        ]])
+
+        @(if (|| (! (btopt "i")) (btopt "s")) [result [
+            @(if (! (btopt "P")) [result [TEX bump, dtc, @normtex, 2D;]])
+            MAD bump.xyz, bump, 2, -1;
+        ]])
+
+        @(if (btopt "s") [result [
+            PARAM specfactor = @(if (btopt "i") 128 32);
+            TEMP he;
+            ADD he.xyz, camvts, lmlv;
+            @(normalize he he)
+            DP3_SAT he.w, he, bump;
+            POW he.w, he.w, specfactor.x;
+            @(if (btopt "i") [result [MUL_SAT he.w, he, 64;]])
+            @(if (btopt "S") [result [MUL he.w, he, diffuse;]])
+            @(if (btopt "i") [result [
+                MUL diffuse.rgb, he.w, @pixelparam1;
+            ]] [result [
+                MAD diffuse.rgb, he.w, @pixelparam1, diffuse;
+            ]])
+        ]])
+
+        @(if (|| (! (btopt "i")) (btopt "s")) [result [
+            DP3_SAT lmlv.w, bump, lmlv;
+            MUL lmc.rgb, lmc, lmlv.w;
+            MAX lmc.rgb, lmc, program.env[5];
+
+            @(if (btopt "i") [result [
+                #pragma CUBE2_dynlight lmc
+
+                MUL @(if (btopt "g") [result "diffuse.rgb"] [result "result.color.rgb"]), diffuse, lmc;
+            ]] [result [
+                #pragma CUBE2_shadowmap lmc
+                #pragma CUBE2_dynlight lmc
+
+                MUL @(if (|| (btopt "g") (btopt "r")) [result "diffuse.rgb"] [result "result.color.rgb"]), diffuse, lmc;
+            ]])
+        ]])
+
+        @(if (btopt "r") [result [
+            TEMP rvec;
+            @(if (btopt "t") [result [
+                TEMP rvects;
+                DP3 rvects.w, cam, bump;
+                MUL rvects.xyz, rvects.w, bump;
+                MAD rvects.xyz, rvects, 2, -cam;
+
+                MUL rvec.xyz, rvects.x, fragment.texcoord[2];
+                MAD rvec.xyz, rvects.y, fragment.texcoord[3], rvec;
+                MAD rvec.xyz, rvects.z, fragment.texcoord[4], rvec;
+            ]] [result [
+                TEMP bumpw;
+                MUL bumpw.xyz, bump.x, fragment.texcoord[2];
+                MAD bumpw.xyz, bump.y, fragment.texcoord[3], bumpw;
+                MAD bumpw.xyz, bump.z, fragment.texcoord[4], bumpw;
+
+                DP3 rvec.w, cam, bumpw;
+                MUL rvec.xyz, rvec.w, bumpw;
+                MAD rvec.xyz, rvec, 2, -cam;
+            ]])
+
+            TEMP reflect;
+            TEX reflect, rvec, texture[3], CUBE;
+            @@(if (btopt "R") [result [
+                TEMP rmod;
+                MUL rmod.rgb, diffuse.w, @pixelparam3;
+            ]] [result [
+                PARAM rmod = @pixelparam3;
+            ]])
+            LRP @(if (btopt "g") [result "diffuse.rgb"] [result "result.color.rgb"]), rmod, reflect, diffuse;
+        ]])
+
+        @(if (btopt "g") [result [
+            TEMP glow;
+            TEX glow, dtc, @@glowtex, 2D;
+            @@(if (btopt "G") [result [
+                TEMP pulsecol;
+                LRP pulsecol.rgb, fragment.texcoord[1].w, @pixelparam5, @pixelparam0;
+            ]])
+            @@(if (btopt "i") [result [
+                TEMP k;
+                MUL glow.rgb, glow, @(if (btopt "G") [result "pulsecol"] [result $pixelparam0]);
+                MAX k.x, glow.r, glow.g;
+                MAX k.x, k.x, glow.b;
+                MUL k.x, k.x, k.x;
+                MUL_SAT k.x, k.x, 32;
+                @(if (btopt "s") [result [
+                    MAD result.color.rgb, k.x, glow, diffuse;
+                ]] [result [
+                    MUL result.color.rgb, k.x, glow;
+                    #pragma CUBE2_variantoverride TEX result.color.a, @(if $minimizetcusage [result "lmtc.wzyx"] [result "lmtc"]), texture[1], 2D;
+                    MOV result.color.a, program.env[6];
+                ]])
+            ]] [result [
+                MAD result.color.rgb, glow, @(if (btopt "G") [result "pulsecol"] [result $pixelparam0]), diffuse;
+            ]])
+        ]])
+
+        @(if (! (btopt "i")) [result [
+            #pragma CUBE2_water
+        ]])
+
+        END
+    ]
+]
+
+bumpshader = [
+    defershader (if (>= (stringstr $arg2 "e") 0) [result 3] [result 1]) $arg1 [
+        bumpvariantshader @arg1 @arg2
+        if (|| (btopt "g") (btopt "s")) [
+            bumpvariantshader @@arg1 (stringreplace (concatword @@arg2 "i") "r")
+        ]
+    ]
+]
+
+bumpshader "bumpworld" ""
+bumpshader "bumpspecworld" "ots"
+fastshader bumpspecworld bumpworld 2
+altshader bumpspecworld bumpworld
+bumpshader "bumpspecmapworld" "otsS"
+fastshader bumpspecmapworld bumpworld 2
+altshader bumpspecmapworld bumpworld
+
+bumpshader "bumpglowworld" "g"
+bumpshader "bumpspecglowworld" "otsg"
+altshader bumpspecglowworld bumpglowworld
+bumpshader "bumpspecmapglowworld" "otsSg"
+fastshader bumpspecmapglowworld bumpglowworld 2
+altshader bumpspecmapglowworld bumpglowworld
+
+bumpshader "bumppulseglowworld" "gG"
+bumpshader "bumpspecpulseglowworld" "otsgG"
+altshader bumpspecpulseglowworld bumppulseglowworld
+bumpshader "bumpspecmappulseglowworld" "otsSgG"
+fastshader bumpspecmappulseglowworld bumppulseglowworld 2
+altshader bumpspecmappulseglowworld bumppulseglowworld
+
+bumpshader "bumpparallaxworld" "pot"
+fastshader bumpparallaxworld bumpworld 1
+altshader bumpparallaxworld bumpworld
+bumpshader "bumpspecparallaxworld" "pots"
+fastshader bumpspecparallaxworld bumpparallaxworld 2
+fastshader bumpspecparallaxworld bumpworld 1
+altshader bumpspecparallaxworld bumpworld
+bumpshader "bumpspecmapparallaxworld" "potsS"
+fastshader bumpspecmapparallaxworld bumpparallaxworld 2
+fastshader bumpspecmapparallaxworld bumpworld 1
+altshader bumpspecmapparallaxworld bumpworld
+
+bumpshader "bumpparallaxglowworld" "potg"
+fastshader bumpparallaxglowworld bumpglowworld 1
+altshader bumpparallaxglowworld bumpglowworld
+bumpshader "bumpspecparallaxglowworld" "potsg"
+fastshader bumpspecparallaxglowworld bumpparallaxglowworld 2
+fastshader bumpspecparallaxglowworld bumpglowworld 1
+altshader bumpspecparallaxglowworld bumpglowworld
+bumpshader "bumpspecmapparallaxglowworld" "potsSg"
+fastshader bumpspecmapparallaxglowworld bumpparallaxglowworld 2
+fastshader bumpspecmapparallaxglowworld bumpglowworld 1
+altshader bumpspecmapparallaxglowworld bumpglowworld
+
+bumpshader "bumpparallaxpulseglowworld" "potgG"
+fastshader bumpparallaxpulseglowworld bumppulseglowworld 1
+altshader bumpparallaxpulseglowworld bumppulseglowworld
+bumpshader "bumpspecparallaxpulseglowworld" "potsgG"
+fastshader bumpspecparallaxpulseglowworld bumpparallaxpulseglowworld 2
+fastshader bumpspecparallaxpulseglowworld bumppulseglowworld 1
+altshader bumpspecparallaxpulseglowworld bumppulseglowworld
+bumpshader "bumpspecmapparallaxpulseglowworld" "potsSgG"
+fastshader bumpspecmapparallaxpulseglowworld bumpparallaxpulseglowworld 2
+fastshader bumpspecmapparallaxpulseglowworld bumppulseglowworld 1
+altshader bumpspecmapparallaxpulseglowworld bumppulseglowworld
+
+bumpshader "bumpenvworldalt" "e"
+bumpshader "bumpenvworld" "eor"
+altshader bumpenvworld bumpenvworldalt
+fastshader bumpenvworld bumpenvworldalt 2
+bumpshader "bumpenvspecworld" "eotsr"
+altshader bumpenvspecworld bumpenvworldalt
+fastshader bumpenvspecworld bumpenvworldalt 2
+bumpshader "bumpenvspecmapworld" "eotsSrR"
+altshader bumpenvspecmapworld bumpenvworldalt
+fastshader bumpenvspecmapworld bumpenvworldalt 2
+
+bumpshader "bumpenvglowworldalt" "eg"
+bumpshader "bumpenvglowworld" "eorg"
+altshader bumpenvglowworld bumpenvglowworldalt
+fastshader bumpenvglowworld bumpenvglowworldalt 2
+bumpshader "bumpenvspecglowworld" "eotsrg"
+altshader bumpenvspecglowworld bumpenvglowworldalt
+fastshader bumpenvspecglowworld bumpenvglowworldalt 2
+bumpshader "bumpenvspecmapglowworld" "eotsSrRg"
+altshader bumpenvspecmapglowworld bumpenvglowworldalt
+fastshader bumpenvspecmapglowworld bumpenvglowworldalt 2
+
+bumpshader "bumpenvpulseglowworldalt" "egG"
+bumpshader "bumpenvpulseglowworld" "eorgG"
+altshader bumpenvpulseglowworld bumpenvpulseglowworldalt
+fastshader bumpenvpulseglowworld bumpenvpulseglowworldalt 2
+bumpshader "bumpenvspecpulseglowworld" "eotsrgG"
+altshader bumpenvspecpulseglowworld bumpenvpulseglowworldalt
+fastshader bumpenvspecpulseglowworld bumpenvpulseglowworldalt 2
+bumpshader "bumpenvspecmappulseglowworld" "eotsSrRgG"
+altshader bumpenvspecmappulseglowworld bumpenvpulseglowworldalt
+fastshader bumpenvspecmappulseglowworld bumpenvpulseglowworldalt 2
+
+bumpshader "bumpenvparallaxworldalt" "epot"
+altshader bumpenvparallaxworldalt bumpenvworldalt
+bumpshader "bumpenvparallaxworld" "epotr"
+altshader bumpenvparallaxworld bumpenvparallaxworldalt
+fastshader bumpenvparallaxworld bumpenvparallaxworldalt 2
+fastshader bumpenvparallaxworld bumpenvworldalt 1
+bumpshader "bumpenvspecparallaxworld" "epotsr"
+altshader bumpenvspecparallaxworld bumpenvparallaxworldalt
+fastshader bumpenvspecparallaxworld bumpenvparallaxworldalt 2
+fastshader bumpenvspecparallaxworld bumpenvworldalt 1
+bumpshader "bumpenvspecmapparallaxworld" "epotsSrR"
+altshader bumpenvspecmapparallaxworld bumpenvparallaxworldalt
+fastshader bumpenvspecmapparallaxworld bumpenvparallaxworldalt 2
+fastshader bumpenvspecmapparallaxworld bumpenvworldalt 1
+
+bumpshader "bumpenvparallaxglowworldalt" "epotg"
+altshader bumpenvparallaxglowworldalt bumpenvglowworldalt
+bumpshader "bumpenvparallaxglowworld" "epotrg"
+altshader bumpenvparallaxglowworld bumpenvparallaxglowworldalt
+fastshader bumpenvparallaxglowworld bumpenvparallaxglowworldalt 2
+fastshader bumpenvparallaxglowworld bumpenvglowworldalt 1
+bumpshader "bumpenvspecparallaxglowworld" "epotsrg"
+altshader bumpenvspecparallaxglowworld bumpenvparallaxglowworldalt
+fastshader bumpenvspecparallaxglowworld bumpenvparallaxglowworldalt 2
+fastshader bumpenvspecparallaxglowworld bumpenvglowworldalt 1
+bumpshader "bumpenvspecmapparallaxglowworld" "epotsSrRg"
+altshader bumpenvspecmapparallaxglowworld bumpenvparallaxglowworldalt
+fastshader bumpenvspecmapparallaxglowworld bumpenvparallaxglowworldalt 2
+fastshader bumpenvspecmapparallaxglowworld bumpenvglowworldalt 1
+
+bumpshader "bumpenvparallaxpulseglowworldalt" "epotgG"
+altshader bumpenvparallaxpulseglowworldalt bumpenvpulseglowworldalt
+bumpshader "bumpenvparallaxpulseglowworld" "epotrgG"
+altshader bumpenvparallaxpulseglowworld bumpenvparallaxpulseglowpulseglowworldalt
+fastshader bumpenvparallaxpulseglowworld bumpenvparallaxpulseglowpulseglowworldalt 2
+fastshader bumpenvparallaxpulseglowworld bumpenvpulseglowworldalt 1
+bumpshader "bumpenvspecparallaxpulseglowworld" "epotsrgG"
+altshader bumpenvspecparallaxpulseglowworld bumpenvparallaxpulseglowworldalt
+fastshader bumpenvspecparallaxpulseglowworld bumpenvparallaxpulseglowworldalt 2
+fastshader bumpenvspecparallaxpulseglowworld bumpenvpulseglowworldalt 1
+bumpshader "bumpenvspecmapparallaxpulseglowworld" "epotsSrRgG"
+altshader bumpenvspecmapparallaxpulseglowworld bumpenvparallaxpulseglowworldalt
+fastshader bumpenvspecmapparallaxpulseglowworld bumpenvparallaxpulseglowworldalt 2
+fastshader bumpenvspecmapparallaxpulseglowworld bumpenvpulseglowworldalt 1
+
+//bumpshader "steepworld" "Pot"
+
+////////////////////////////////////////////////
+//
+// phong lighting model shader
+//
+////////////////////////////////////////////////
+
+// skeletal animation for matrices and dual quaternions
+
+skelmatanim = [
+    skelanimlength = (min (- $maxvpenvparams (+ $reservevpparams 10)) $maxskelanimdata)
+    result [
+        PARAM mats[] = { program.env[ 10 .. @@(- (+ 10 $skelanimlength) 1) ] };
+        @(if (> $arg1 1) [result "ATTRIB weights = vertex.attrib[6];"])
+        ATTRIB bones = vertex.attrib[7];
+        ADDRESS bone;
+        TEMP mx, my, mz;
+
+        ARL bone.x, bones.x;
+        @(if (= $arg1 1) [result [
+            MOV mx, mats[bone.x];
+            MOV my, mats[bone.x+1];
+            MOV mz, mats[bone.x+2];
+        ]] [result [
+            MUL mx, weights.x, mats[bone.x];
+            MUL my, weights.x, mats[bone.x+1];
+            MUL mz, weights.x, mats[bone.x+2];
+            ARL bone.x, bones.y;
+            MAD mx, weights.y, mats[bone.x], mx;
+            MAD my, weights.y, mats[bone.x+1], my;
+            MAD mz, weights.y, mats[bone.x+2], mz;
+        ]])
+        @(if (>= $arg1 3) [result [
+            ARL bone.x, bones.z;
+            MAD mx, weights.z, mats[bone.x], mx;
+            MAD my, weights.z, mats[bone.x+1], my;
+            MAD mz, weights.z, mats[bone.x+2], mz;
+        ]])
+        @(if (>= $arg1 4) [result [
+            ARL bone.x, bones.w;
+            MAD mx, weights.w, mats[bone.x], mx;
+            MAD my, weights.w, mats[bone.x+1], my;
+            MAD mz, weights.w, mats[bone.x+2], mz;
+        ]])
+
+        TEMP opos;
+        DP4 opos.x, mx, spos;
+        DP4 opos.y, my, spos;
+        DP4 opos.z, mz, spos;
+        MOV opos.w, spos.w;
+
+        @(if $arg2 [result [
+            TEMP onormal;
+            DP3 onormal.x, mx, snormal;
+            DP3 onormal.y, my, snormal;
+            DP3 onormal.z, mz, snormal;
+        ]])
+
+        @(if $arg3 [result [
+            TEMP otangent;
+            DP3 otangent.x, mx, stangent;
+            DP3 otangent.y, my, stangent;
+            DP3 otangent.z, mz, stangent;
+        ]])
+    ]
+]
+
+skelquatanim = [
+    skelanimlength = (min (- $maxvpenvparams (+ $reservevpparams 10)) $maxskelanimdata)
+    result [
+        PARAM quats[] = { program.env[ 10 .. @@(- (+ 10 $skelanimlength) 1) ] };
+        @(if (> $arg1 1) [result "ATTRIB weights = vertex.attrib[6];"])
+        ATTRIB bones = vertex.attrib[7];
+        ADDRESS bone;
+        TEMP dqreal, dqdual, dir;
+
+        ARL bone.x, bones.x;
+        @(if (= $arg1 1) [result [
+            MOV dqreal, quats[bone.x];
+            MOV dqdual, quats[bone.x+1];
+        ]] [result [
+            MUL dqreal, weights.x, quats[bone.x];
+            MUL dqdual, weights.x, quats[bone.x+1];
+            ARL bone.x, bones.y;
+            MAD dqreal, weights.y, quats[bone.x], dqreal;
+            MAD dqdual, weights.y, quats[bone.x+1], dqdual;
+            @(if (>= $arg1 3) [result [
+                ARL bone.x, bones.z;
+                MAD dqreal, weights.z, quats[bone.x], dqreal;
+                MAD dqdual, weights.z, quats[bone.x+1], dqdual;
+            ]])
+            @(if (>= $arg1 4) [result [
+                ARL bone.x, bones.w;
+                MAD dqreal, weights.w, quats[bone.x], dqreal;
+                MAD dqdual, weights.w, quats[bone.x+1], dqdual;
+            ]])
+
+            TEMP len;
+            DP4 len.x, dqreal, dqreal;
+            RSQ len.x, len.x;
+            MUL dqreal, dqreal, len.x;
+            MUL dqdual, dqdual, len.x;
+        ]])
+
+        TEMP opos;
+        XPD opos.xyz, dqreal, spos;
+        MAD opos.xyz, spos, dqreal.w, opos;
+        ADD opos.xyz, opos, dqdual;
+        XPD opos.xyz, dqreal, opos;
+        MAD opos.xyz, dqdual, dqreal.w, opos;
+        MAD opos.xyz, dqreal, -dqdual.w, opos;
+        MAD opos.xyz, 2, opos, spos;
+        MOV opos.w, spos.w;
+
+        @(if $arg2 [result [
+            TEMP onormal;
+            XPD onormal.xyz, dqreal, snormal;
+            MAD onormal.xyz, dqreal.w, snormal, onormal;
+            XPD onormal.xyz, dqreal, onormal;
+            MAD onormal.xyz, 2, onormal, snormal;
+        ]])
+
+        @(if $arg3 [result [
+            TEMP otangent;
+            XPD otangent.xyz, dqreal, stangent;
+            MAD otangent.xyz, dqreal.w, stangent, otangent;
+            XPD otangent.xyz, dqreal, otangent;
+            MAD otangent.xyz, 2, otangent, stangent;
+        ]])
+    ]
+]
+
+// model shadowmapping
+
+shadowmapcastervertexshader = [
+    result [
+        @(if (< $numargs 1) [result $vpstart] [result [
+            !!ARBvp1.0
+            ATTRIB spos = vertex.position;
+            @arg1
+            DP4 result.position.x, opos, state.matrix.mvp.row[0];
+            DP4 result.position.y, opos, state.matrix.mvp.row[1];
+            DP4 result.position.w, opos, state.matrix.mvp.row[3];
+        ]])
+
+        TEMP z;
+        DP4 z.x, opos, state.matrix.mvp.row[2];
+
+        @(if (>= $numargs 1) [result "MOV result.position.z, z.x;"])
+
+        SUB result.texcoord[0].x, 1, z.x;
+        MOV result.texcoord[0].y, 1;
+        MOV result.texcoord[0].z, 0;
+        MOV result.texcoord[0].w, program.env[1].x;
+
+        END
+    ]
+]
+
+shader 0 shadowmapcaster (shadowmapcastervertexshader) [
+    @fpstart
+    MOV result.color, fragment.texcoord[0];
+    END
+]
+loop i 4 [
+    variantshader 0 shadowmapcaster 0 (shadowmapcastervertexshader (skelmatanim (+ $i 1) 0 0)) []
+    variantshader 0 shadowmapcaster 1 (shadowmapcastervertexshader (skelquatanim (+ $i 1) 0 0)) []
+]
+
+shader 0 "shadowmapreceiver" [
+    @vpstart
+    TEMP z;
+    DP4 z, state.matrix.mvp.row[2], opos;
+    @(if $ati_minmax_bug [result [
+        MOV result.texcoord[0].xyw, 0;
+        SUB result.texcoord[0].z, program.env[0].y, z;
+    ]] [result [
+        SUB result.texcoord[0], program.env[0].y, z;
+    ]])
+    END
+] [
+    @fpstart
+    MOV result.color, fragment.texcoord[0];
+    END
+]
+
+// model stencil shadows
+
+notexturemodelvertexshader = [
+    result [
+        @(if (< $numargs 1) [result $vpstart] [result [
+            !!ARBvp1.0
+            ATTRIB spos = vertex.position;
+            @arg1
+            DP4 result.position.x, opos, state.matrix.mvp.row[0];
+            DP4 result.position.y, opos, state.matrix.mvp.row[1];
+            DP4 result.position.z, opos, state.matrix.mvp.row[2];
+            DP4 result.position.w, opos, state.matrix.mvp.row[3];
+        ]])
+
+        MOV result.color, vertex.color;
+        @fogcoord
+        END
+    ]
+]
+
+shader 0 notexturemodel (notexturemodelvertexshader) [
+    @fpstart
+    OPTION ARB_fog_linear;
+    MOV result.color, fragment.color;
+    END
+]
+loop i 4 [
+    variantshader 0 notexturemodel 0 (notexturemodelvertexshader (skelmatanim (+ $i 1) 0 0)) []
+    variantshader 0 notexturemodel 1 (notexturemodelvertexshader (skelquatanim (+ $i 1) 0 0)) []
+]
+
+// mdltype:
+//    e -> envmap
+//    n -> normalmap
+//    s -> spec
+//    m -> masks
+//    B -> matrix skeletal animation
+//    b -> dual-quat skeletal animation
+//    i -> glare intensity
+
+mdlopt = [ >= (stringstr $modeltype $arg1) 0 ]
+
+modelvertexshader = [
+    modeltype = $arg1
+    result [
+        @(if (|| (mdlopt "b") (mdlopt "B")) [result [
+            !!ARBvp1.0
+            ATTRIB spos = vertex.position;
+            ATTRIB snormal = vertex.normal;
+            @(if (mdlopt "n") [result "ATTRIB stangent = vertex.attrib[1];"])
+        ]] [result [
+            @vpstart
+            ATTRIB onormal = vertex.normal;
+            @(if (mdlopt "n") [result "ATTRIB otangent = vertex.attrib[1];"])
+        ]])
+        PARAM ocampos = program.env[1];
+        PARAM lightdir = program.env[0];
+        PARAM lightscale = program.env[2];
+
+        @(if (mdlopt "B") [skelmatanim $arg2 1 (mdlopt "n")])
+        @(if (mdlopt "b") [skelquatanim $arg2 1 (mdlopt "n")])
+        @(if (|| (mdlopt "b") (mdlopt "B")) [result [
+            DP4 result.position.x, opos, state.matrix.mvp.row[0];
+            DP4 result.position.y, opos, state.matrix.mvp.row[1];
+            DP4 result.position.z, opos, state.matrix.mvp.row[2];
+            DP4 result.position.w, opos, state.matrix.mvp.row[3];
+        ]])
+
+        @(if (|| (mdlopt "n") (mdlopt "s") (mdlopt "i")) [result [
+            MOV result.color, vertex.color;
+        ]])
+        ADD result.texcoord[0].xy, vertex.texcoord[0], program.env[5].yzww;
+
+        @(if (|| (mdlopt "e") (mdlopt "s")) [result [
+            TEMP camvec;
+            SUB camvec, ocampos, opos;
+            @(normalize camvec camvec)
+        ]])
+
+        @(if (mdlopt "n") [
+            if (mdlopt "e") [result [
+                DP3 result.texcoord[1].x, state.matrix.texture.row[0], camvec;
+                DP3 result.texcoord[1].y, state.matrix.texture.row[1], camvec;
+                DP3 result.texcoord[1].z, state.matrix.texture.row[2], camvec;
+
+                // composition of tangent -> object and object -> world transforms
+                //   becomes tangent -> world
+                TEMP wnormal, wtangent, wbitangent;
+                DP3 wnormal.x, state.matrix.texture.row[0], onormal;
+                DP3 wnormal.y, state.matrix.texture.row[1], onormal;
+                DP3 wnormal.z, state.matrix.texture.row[2], onormal;
+                MOV result.texcoord[4].xyz, wnormal;
+                DP3 wtangent.x, state.matrix.texture.row[0], otangent;
+                DP3 wtangent.y, state.matrix.texture.row[1], otangent;
+                DP3 wtangent.z, state.matrix.texture.row[2], otangent;
+                MOV result.texcoord[2].xyz, wtangent;
+                XPD wbitangent.xyz, wnormal, wtangent;
+                MUL result.texcoord[3].xyz, wbitangent, @(if (|| (mdlopt "b") (mdlopt "B")) [result "stangent.w"] [result "otangent.w"]);
+            ]] [result [
+                TEMP obitangent;
+                XPD obitangent.xyz, onormal, otangent;
+                MUL obitangent.xyz, obitangent, @(if (|| (mdlopt "b") (mdlopt "B")) [result "stangent.w"] [result "otangent.w"]);
+
+                DP3 result.texcoord[1].x, lightdir, otangent;
+                DP3 result.texcoord[1].y, lightdir, obitangent;
+                DP3 result.texcoord[1].z, lightdir, onormal;
+
+                @(if (mdlopt "s") [result [
+                    TEMP halfangle;
+                    ADD halfangle.xyz, lightdir, camvec;
+                    DP3 result.texcoord[2].x, halfangle, otangent;
+                    DP3 result.texcoord[2].y, halfangle, obitangent;
+                    DP3 result.texcoord[2].z, halfangle, onormal;
+                ]])
+            ]]
+        ] [result [
+            @(if (mdlopt "s") [result [
+                MOV result.texcoord[1].xyz, onormal;
+                ADD result.texcoord[2].xyz, lightdir, camvec;
+            ]] [if (! (mdlopt "i")) [result [
+                TEMP light;
+                DP3 light.y, onormal, lightdir;
+                MAD light.x, light.y, lightscale.x, lightscale.y;
+                MAD light.x, light.y, light.x, lightscale.z;
+                MIN light.x, light.x, 1;
+                MUL result.color.xyz, light.x, vertex.color;
+                MOV result.color.w, vertex.color.w;
+            ]]])
+            @(if (mdlopt "e") [result [
+                TEMP rvec, invfresnel;
+                DP3 invfresnel.x, camvec, onormal;
+                MUL rvec.xyz, invfresnel.x, onormal;
+                MAD rvec.xyz, rvec, 2, -camvec;
+                DP3 result.texcoord[3].x, state.matrix.texture.row[0], rvec;
+                DP3 result.texcoord[3].y, state.matrix.texture.row[1], rvec;
+                DP3 result.texcoord[3].z, state.matrix.texture.row[2], rvec;
+                MAX invfresnel.x, invfresnel.x, 0;
+                MAD result.texcoord[3].w, program.env[3].x, invfresnel.x, program.env[3].y;
+            ]])
+        ]])
+
+        @fogcoord
+
+        END
+    ]
+]
+
+modelfragmentshader = [
+    modeltype = $arg1
+    result [
+        @fpstart
+        OPTION ARB_fog_linear;
+        ATTRIB dtc = fragment.texcoord[0];
+        @(if (mdlopt "n") [if (mdlopt "e") [result [
+            PARAM lightdir = program.env[1];
+        ]] [result [
+            ATTRIB lightdir = fragment.texcoord[1];
+        ]]])
+        @(if (mdlopt "s") [result [
+            @(if (&& (! (mdlopt "n")) (! (mdlopt "i"))) [result [
+                PARAM lightdir = program.env[0];
+            ]])
+            PARAM specfactor = @(if (mdlopt "i") 256 128);
+        ]])
+        @(if (&& (|| (mdlopt "s") (mdlopt "n")) (! (mdlopt "i"))) [result "PARAM lightscale = program.env[2];"])
+        @(if (|| (mdlopt "s") (mdlopt "m")) [result "PARAM maskscale = program.env[4];"])
+        PARAM lightmaterial = program.env[6];
+
+        TEMP light;
+        TEX light, dtc, texture[0], 2D;
+
+        @(if (mdlopt "m") [result [
+            PARAM lightmaterial2 = program.env[7];
+            TEMP masks, mat, glow;
+            TEX masks, dtc, texture[1], 2D;
+            LRP mat.rgb, masks.a, lightmaterial, lightmaterial2; // material color mask in alpha channel
+            MUL light.rgb, light, mat;
+            MUL glow.rgb, light, maskscale.y;
+        ]] [result [
+            MUL light.rgb, light, lightmaterial;
+        ]])
+
+        @(if (mdlopt "n") [if (mdlopt "e") [result [
+            TEMP normal, normts;
+            TEX normts, dtc, texture[3], 2D;
+            SUB normts.xyz, normts, 0.5;
+            MUL normal.xyz, normts.x, fragment.texcoord[2];
+            MAD normal.xyz, normts.y, fragment.texcoord[3], normal;
+            MAD normal.xyz, normts.z, fragment.texcoord[4], normal;
+            @(normalize normal normal)
+        ]] [result [
+            TEMP normal;
+            TEX normal, dtc, texture[3], 2D;
+            SUB normal.xyz, normal, 0.5;
+            @(normalize normal normal)
+        ]]])
+
+        @(if (mdlopt "s") [result [
+            TEMP spec, halfangle;
+            @(if (mdlopt "n") [
+                if (mdlopt "e") [result [
+                    ADD halfangle, lightdir, fragment.texcoord[1];
+                    @(normalize halfangle halfangle)
+                ]] [result [
+                    @(normalize halfangle fragment.texcoord[2])
+                ]]
+            ] [result [
+                TEMP normal;
+                @(normalize normal fragment.texcoord[1])
+                @(normalize halfangle fragment.texcoord[2])
+            ]])
+            DP3_SAT spec.x, halfangle, normal;
+            POW spec.x, spec.x, specfactor.x;
+            MUL spec.x, spec.x, maskscale.x;
+            @(if (mdlopt "m") [result "MUL spec.x, spec.x, masks.r;"])   // specmap in red channel
+        ]])
+
+        @(if (mdlopt "i") [
+            if (mdlopt "s") [result [
+                MUL spec.x, spec.x, maskscale.z;
+                MUL @(if (mdlopt "m") [result "light.rgb"] [result "result.color.rgb"]), spec.x, fragment.color;
+            ]] [
+                if (! (mdlopt "m")) [result "MOV result.color.rgb, 0;"]
+            ]
+        ] [result [
+            @(if (|| (mdlopt "s") (mdlopt "n")) [result [
+                TEMP intensity;
+                DP3 intensity.y, normal, lightdir;
+                MAD intensity.x, intensity.y, lightscale.x, lightscale.y;
+                MAD_SAT intensity.x, intensity.y, intensity.x, lightscale.z;
+            ]])
+            @(if (mdlopt "s") [result [
+                MAD light.rgb, intensity.x, light, spec.x;
+            ]] [if (mdlopt "n") [result [
+                MUL light.rgb, intensity.x, light;
+            ]]])
+            MUL @(if (mdlopt "m") [result "light.rgb"] [result "result.color"]), light, fragment.color;
+        ]])
+
+        @(if (mdlopt "m") [result [
+            @(if (mdlopt "e") [result [
+                LRP light.rgb, masks.g, glow, light;
+
+                TEMP reflect;
+                @(if (mdlopt "n") [result [
+                    TEMP camvec, invfresnel, rvec;
+                    @(normalize camvec fragment.texcoord[1])
+                    DP3 invfresnel.x, camvec, normal;
+                    MUL rvec.xyz, invfresnel.x, normal;
+                    MAD rvec.xyz, rvec, 2, -camvec;
+
+                    MAX invfresnel.x, invfresnel, 0;
+                    MAD invfresnel.x, program.env[3].x, invfresnel.x, program.env[3].y;
+
+                    TEX reflect, rvec, texture[2], CUBE;
+                    MUL masks.b, masks.b, invfresnel.x; // envmap mask in blue channel
+                ]] [result [
+                    TEX reflect, fragment.texcoord[3], texture[2], CUBE;
+                    MUL masks.b, masks.b, fragment.texcoord[3].w; // envmap mask in blue channel
+                ]])
+                LRP result.color.rgb, masks.b, reflect, light;
+            ]] [if (mdlopt "i") [result [
+                TEMP k;
+                MUL k.x, masks.g, masks.g;
+                MUL_SAT k.x, k.x, maskscale.w;
+                @(if (mdlopt "s") [result [
+                    MAD result.color.rgb, k.x, glow, light;
+                ]] [result [
+                    MUL result.color.rgb, k.x, glow;
+                ]])
+            ]] [result [
+                LRP result.color.rgb, masks.g, glow, light;
+            ]]])
+        ]])
+
+        @(if (|| (mdlopt "m") || (mdlopt "i")) [result [
+            MUL result.color.a, light.a, fragment.color.a;
+        ]])
+
+        END
+    ]
+]
+
+modelshader = [
+    defershader 0 $arg1 [
+        basemodeltype = [@@arg2]
+        shader 0 @arg1 (modelvertexshader $basemodeltype) (modelfragmentshader $basemodeltype)
+        loop i 4 [
+            variantshader 0 @@arg1 0 (modelvertexshader (concatword "B" $basemodeltype) (+ $i 1)) []
+            variantshader 0 @@arg1 1 (modelvertexshader (concatword "b" $basemodeltype) (+ $i 1)) []
+        ]
+        glaremodeltype = (stringreplace (concatword $basemodeltype "i") "e")
+        if (< (stringstr $glaremodeltype "s") 0) [glaremodeltype = (stringreplace $glaremodeltype "n")]
+        variantshader 0 @arg1 2 (modelvertexshader $glaremodeltype) (modelfragmentshader $glaremodeltype)
+        loop i 4 [
+            variantshader 0 @@arg1 2 (modelvertexshader (concatword "B" $glaremodeltype) (+ $i 1)) 2
+            variantshader 0 @@arg1 3 (modelvertexshader (concatword "b" $glaremodeltype) (+ $i 1)) 2
+        ]
+    ]
+]
+
+////////////////////////////////////////////////
+//
+// gourad lighting model shader: cheaper, non-specular version for vegetation etc. gets used when spec==0
+//
+////////////////////////////////////////////////
+
+modelshader "nospecmodel" ""
+modelshader "masksnospecmodel" "m"
+modelshader "envmapnospecmodel" "me"
+altshader envmapnospecmodel masksnospecmodel
+
+modelshader "bumpnospecmodel" "n"
+modelshader "bumpmasksnospecmodel" "nm"
+modelshader "bumpenvmapnospecmodel" "nme"
+altshader bumpenvmapnospecmodel bumpmasksnospecmodel
+
+////////////////////////////////////////////////
+//
+// phong lighting model shader
+//
+////////////////////////////////////////////////
+
+modelshader "stdmodel" "s"
+fastshader stdmodel nospecmodel 1
+modelshader "masksmodel" "sm"
+fastshader masksmodel masksnospecmodel 1
+modelshader "envmapmodel" "sme"
+altshader envmapmodel masksmodel
+fastshader envmapmodel envmapnospecmodel 1
+
+modelshader "bumpmodel" "ns"
+fastshader bumpmodel bumpnospecmodel 1
+modelshader "bumpmasksmodel" "nsm"
+fastshader bumpmasksmodel bumpmasksnospecmodel 1
+modelshader "bumpenvmapmodel" "nsme"
+altshader bumpenvmapmodel bumpmasksmodel
+fastshader bumpenvmapmodel bumpenvmapnospecmodel 1
+
+////////////////////////////////////////////////
+//
+// separable blur with up to 7 taps
+//
+////////////////////////////////////////////////
+
+blurshader = [
+    shader 0 $arg1 [
+        !!ARBvp1.0
+        MOV result.position, vertex.position;
+        MOV result.texcoord[0], vertex.texcoord[0];
+        TEMP tc1, tc2;
+        MAD tc1, program.env[1], { 1, 1, 0, 0 }, vertex.texcoord[0];
+        MAD tc2, program.env[1], { -1, -1, 0, 0 }, vertex.texcoord[0];
+        MOV result.texcoord[1], tc1;
+        MOV result.texcoord[2], tc2;
+        @(loopconcat i (min (- $arg2 1) 2) [concatword [
+            ADD tc1.@@arg3, tc1, program.env[1].@(at "z w" $i);
+            SUB tc2.@@arg3, tc2, program.env[1].@(at "z w" $i);
+            MOV result.texcoord[@@(+ (* $i 2) 3)], tc1;
+            MOV result.texcoord[@@(+ (* $i 2) 4)], tc2;
+        ]])
+        END
+    ] [
+        @fpstart
+        TEMP val, blur1, blur2;
+        TEX val, fragment.texcoord[0], texture[0], @arg4;
+        MUL val, val, program.env[0].x;
+        @(if (> $arg2 3) [result [
+            TEMP tc1, tc2;
+        ]])
+        @(loopconcat i $arg2 [concatword [
+            @(if (< $i 3) [result [
+                TEX blur1, fragment.texcoord[@@(+ (* $i 2) 1)], texture[0], @@@@arg4;
+                TEX blur2, fragment.texcoord[@@(+ (* $i 2) 2)], texture[0], @@@@arg4;
+            ]] [result [
+                ADD tc1, fragment.texcoord[0], program.env[@@(+ $i 0)];
+                SUB tc2, fragment.texcoord[0], program.env[@@(+ $i 0)];
+                TEX blur1, tc1, texture[0], @@@@arg4;
+                TEX blur2, tc2, texture[0], @@@@arg4;
+            ]])
+            ADD blur1, blur1, blur2;
+            @(if (< $i 3) [result [
+                MAD @(if (= (+ $i 1) $arg2) [result "result.color"] [result "val"]), blur1, program.env[0].@(at "y z w" $i), val;
+            ]] [result [
+                MAD @(if (= (+ $i 1) $arg2) [result "result.color"] [result "val"]), blur1, program.env[2].@(at "x y z w" (- $i 3)), val;
+            ]])
+        ]])
+        END
+    ]
+]
+
+loop i 7 [
+    blurshader (format "blurx%1" (+ $i 1)) (+ $i 1) x 2D
+    blurshader (format "blury%1" (+ $i 1)) (+ $i 1) y 2D
+    if (> $i 0) [
+        altshader (format "blurx%1" (+ $i 1)) (format "blurx%1" $i)
+        altshader (format "blury%1" (+ $i 1)) (format "blury%1" $i)
+    ]
+    if $usetexrect [
+        blurshader (format "blurx%1rect" (+ $i 1)) (+ $i 1) x RECT
+        blurshader (format "blury%1rect" (+ $i 1)) (+ $i 1) y RECT
+        if (> $i 0) [
+            altshader (format "blurx%1rect" (+ $i 1)) (format "blurx%1rect" $i)
+            altshader (format "blury%1rect" (+ $i 1)) (format "blury%1rect" $i)
+        ]
+    ]
+]
+
+////////////////////////////////////////////////
+//
+// full screen shaders:
+//
+////////////////////////////////////////////////
+
+fsvs = [
+    !!ARBvp1.0
+    MOV result.position, vertex.position;   // woohoo, no mvp :)
+    MOV result.texcoord[0], vertex.texcoord[0];
+]
+
+fsps = [
+    @fpstart
+    TEMP sample;
+    TEX sample, fragment.texcoord[0], texture[0], RECT;
+]
+
+setup4corners = [
+    ADD result.texcoord[1], vertex.texcoord[0], { -1.5, -1.5, 0, 0 };
+    ADD result.texcoord[2], vertex.texcoord[0], {  1.5, -1.5, 0, 0 };
+    ADD result.texcoord[3], vertex.texcoord[0], { -1.5,  1.5, 0, 0 };
+    ADD result.texcoord[4], vertex.texcoord[0], {  1.5,  1.5, 0, 0 };
+]
+
+sample4corners = [
+    TEMP s00, s02, s20, s22;
+    TEX s00, fragment.texcoord[1], texture[0], RECT;
+    TEX s02, fragment.texcoord[2], texture[0], RECT;
+    TEX s20, fragment.texcoord[3], texture[0], RECT;
+    TEX s22, fragment.texcoord[4], texture[0], RECT;
+]
+
+// some simple ones that just do an effect on the RGB value...
+
+lazyshader 0 "invert" [ @fsvs END ] [ @fsps SUB result.color, 1, sample;   END ]
+lazyshader 0 "gbr"    [ @fsvs END ] [ @fsps MOV result.color, sample.yzxw; END ]
+lazyshader 0 "bw"     [ @fsvs END ] [ @fsps DP3 result.color, sample, 0.333; END ]
+
+// sobel
+
+lazyshader 0 "sobel" [ @fsvs @setup4corners END ] [
+    @fsps
+    @sample4corners
+
+    TEMP t, u;
+
+    ADD t, s00, s20;
+    SUB t, t, s02;
+    SUB t, t, s22;
+    MUL t, t, t;
+
+    ADD u, s00, s02;
+    SUB u, u, s20;
+    SUB u, u, s22;
+    MUL u, u, u;
+
+    ADD t, t, u;
+
+    ADD result.color, sample, t;
+    END
+]
+
+// rotoscope
+
+lazyshader 0 "rotoscope" [
+    @fsvs
+    PARAM scale = program.env[0];
+    // stuff two sets of texture coordinates into each one to get around hardware attribute limits
+    MAD result.texcoord[1], { -1.0, -1.0,  1.0, 0.0 }, scale.x, vertex.texcoord[0].xyyx;
+    MAD result.texcoord[2], { -1.0,  0.0, -1.0, 1.0 }, scale.x, vertex.texcoord[0].xyyx;
+    MAD result.texcoord[3], { -1.0,  1.0,  0.0, 1.0 }, scale.x, vertex.texcoord[0].xyyx;
+    MAD result.texcoord[4], {  0.0, -1.0,  1.0, 1.0 }, scale.x, vertex.texcoord[0].xyyx;
+    END
+] [
+    @fpstart
+    ATTRIB t11 = fragment.texcoord[0];
+    ATTRIB t00_12 = fragment.texcoord[1];
+    ATTRIB t01_20 = fragment.texcoord[2];
+    ATTRIB t02_21 = fragment.texcoord[3];
+    ATTRIB t10_22 = fragment.texcoord[4];
+    TEMP c00, c01, c02, c10, c11, c12, c20, c21, c22;
+
+    TEX c00, t00_12, texture[0], RECT;
+    TEX c01, t01_20, texture[0], RECT;
+    TEX c02, t02_21, texture[0], RECT;
+    TEX c10, t10_22, texture[0], RECT;
+    TEX c11, t11, texture[0], RECT;
+    TEX c12, t00_12.wzyx, texture[0], RECT;
+    TEX c20, t01_20.wzyx, texture[0], RECT;
+    TEX c21, t02_21.wzyx, texture[0], RECT;
+    TEX c22, t10_22.wzyx, texture[0], RECT;
+
+    TEMP diag1, diag2, xedge, yedge;
+    SUB diag1, c00, c22;
+    SUB diag2, c02, c20;
+    SUB xedge, c01, c21;
+    MAD xedge, xedge, 2.0, diag1;
+    ADD xedge, xedge, diag2;
+    SUB yedge, c10, c12;
+    MAD yedge, yedge, 2.0, diag1;
+    SUB yedge, yedge, diag2;
+    MUL xedge, xedge, xedge;
+    MUL yedge, yedge, yedge;
+
+    TEMP sobel;
+    ADD sobel, xedge, yedge;
+    MAX sobel.x, sobel.x, sobel.y;
+    MAX sobel.x, sobel.x, sobel.z;
+    SLT sobel.x, sobel.x, 0.1;
+
+    TEMP hue;
+    DP3 hue.x, c11, 1;
+    RCP hue.w, hue.x;
+    MUL c11, c11, hue.w;
+    SGE hue.xyz, hue.x, { 0.2, 0.8, 1.5, 0.0 };
+    DP3 hue.x, hue, { 0.5, 0.5, 1.5, 0.0 };
+    MUL c11, c11, hue.x;
+
+    MAX sobel.x, sobel.x, hue.z;
+    MUL result.color, c11, sobel.x;
+
+    END
+]
+
+blur3shader = [
+    lazyshader 0 $arg1 [
+        !!ARBvp1.0
+        MOV result.position, vertex.position;
+        ADD result.texcoord[0], vertex.texcoord[0], { @(if $arg2 -0.5 0), @(if $arg3 -0.5 0), 0, 0 };
+        ADD result.texcoord[1], vertex.texcoord[0], { @(if $arg2 0.5 0), @(if $arg3 0.5 0), 0, 0 };
+        END
+    ] [
+        @fpstart
+        TEMP c1, c2;
+        TEX c1, fragment.texcoord[0], texture[0], RECT;
+        TEX c2, fragment.texcoord[1], texture[0], RECT;
+        ADD c1, c1, c2;
+        MUL result.color, c1, 0.5;
+        END
+    ]
+]
+blur3shader hblur3 1 0
+blur3shader vblur3 0 1
+
+blur5shader = [
+    lazyshader 0 $arg1 [
+        @fsvs
+        ADD result.texcoord[1], vertex.texcoord[0], { @(if $arg2 -1.333 0), @(if $arg3 -1.333 0), 0, 0 };
+        ADD result.texcoord[2], vertex.texcoord[0], { @(if $arg2 1.333 0), @(if $arg3 1.333 0), 0, 0 };
+        END
+    ] [
+        @fpstart
+        TEMP c0, c1, c2;
+        TEX c0, fragment.texcoord[0], texture[0], RECT;
+        TEX c1, fragment.texcoord[1], texture[0], RECT;
+        TEX c2, fragment.texcoord[2], texture[0], RECT;
+        ADD c1, c1, c2;
+        MUL c0, c0, 0.4;
+        MAD result.color, c1, 0.3, c0;
+        END
+    ]
+]
+blur5shader hblur5 1 0
+blur5shader vblur5 0 1
+
+rotoscope = [
+    clearpostfx
+    if (>= $numargs 1) [addpostfx rotoscope 0 0 0 $arg1]
+    if (>= $numargs 2) [
+        if (= $arg2 1) [addpostfx hblur3; addpostfx vblur3]
+        if (= $arg2 2) [addpostfx hblur5; addpostfx vblur5]
+    ]
+]
+
+// bloom-ish
+
+shader 0 "glare" [
+    !!ARBvp1.0
+    MOV result.position, vertex.position;
+    MOV result.texcoord[0], vertex.texcoord[0];
+    END
+] [
+    @fpstart
+    TEMP glare;
+    TEX glare, fragment.texcoord[0], texture[0], 2D;
+    MUL result.color, glare, program.env[0];
+    END
+]
+
+lazyshader 0 "bloom_scale" [ @fsvs @setup4corners END ] [
+    @fsps
+    @sample4corners
+    TEMP t;
+    ADD t, s02, s00;
+    ADD t, t, s22;
+    ADD t, t, s20;
+    ADD t, t, sample;
+    MUL result.color, t, 0.2;
+    END
+]
+
+lazyshader 0 "bloom_init" [ @fsvs END ] [
+    @fsps
+    TEMP t;
+    MAX t, sample.r, sample.g;
+    MAX t, t, sample.b;
+    MUL t, t, t;
+    MUL result.color, t, sample;
+    END
+]
+
+bloomshader = [
+    defershader 0 $arg1 [
+        forceshader "bloom_scale"
+        forceshader "bloom_init"
+        shader 0 @arg1 [
+            @fsvs
+            TEMP tc;
+            MOV tc, vertex.texcoord[0];
+            @@(loopconcat i $arg2 [concat "MUL tc, tc, 0.5; MOV result.texcoord[" (+ $i 1) "], tc;"])
+            END
+        ] [
+            @fsps
+            TEMP scaled, bloom;
+            @@(loopconcat i $arg2 [
+                format [
+                    TEX @(if (> $i 0) [result "scaled"] [result "bloom"]), fragment.texcoord[%1], texture[%1], RECT;
+                    @(if (> $i 0) [result [
+                        ADD bloom, bloom, scaled;
+                    ]])
+                ] (+ $i 1)
+            ])
+            MAD result.color, bloom, program.env[0].x, sample;
+            END
+        ]
+    ]
+]
+
+bloomshader bloom1 1
+bloomshader bloom2 2
+bloomshader bloom3 3
+bloomshader bloom4 4
+bloomshader bloom5 5
+bloomshader bloom6 6
+
+setupbloom = [
+    addpostfx bloom_init 1 1 "+0"
+    loop i (- $arg1 1) [
+        addpostfx bloom_scale (+ $i 2) (+ $i 2) (concatword "+" (+ $i 1))
+    ]
+    addpostfx (concatword bloom $arg1) 0 0 (loopconcat i (+ $arg1 1) [result $i]) $arg2
+]
+
+bloom = [
+    clearpostfx
+    if (>= $numargs 1) [setupbloom 6 $arg1]
+]
+
+setupfinecorners = [
+    ADD result.texcoord[1], vertex.texcoord[0], { -1,  0, 0, 0 };
+    ADD result.texcoord[2], vertex.texcoord[0], {  1,  0, 0, 0 };
+    ADD result.texcoord[3], vertex.texcoord[0], {  0, -1, 0, 0 };
+    ADD result.texcoord[4], vertex.texcoord[0], {  0,  1, 0, 0 };
+]
+
+lazyshader 0 "sharpen" [ @fsvs @setupfinecorners END ] [
+    @fsps
+    @sample4corners
+    ADD s00.xyz, s00, s02;
+    ADD s00.xyz, s00, s20;
+    ADD s00.xyz, s00, s22;
+    MUL sample.xyz, sample, 1.95;
+    MAD result.color.rgb, s00, -0.25, sample;
+    MOV result.color.a, sample.a;
+    END
+]
+
+////////////////////////////////////////////////
+//
+// miscellaneous effect shaders:
+//
+////////////////////////////////////////////////
+
+// wobbles the vertices of an explosion sphere
+// and generates all texcoords
+// and blends the edge color
+// and modulates the texture
+explosionshader = [
+    shader 0 $arg1 [
+        !!ARBvp1.0
+        ATTRIB opos = vertex.position;
+        OUTPUT spos = result.position;
+
+        TEMP wobble; // uses a simple linear oscillation instead of more expensive sinusoidal
+        DP3 wobble.w, opos, program.env[0]; // generate wobble offset based off vertex normal and sphere center
+        MAD wobble.w, program.env[1].w, 0.002, wobble.w; // wobble frequency
+        FRC wobble.w, wobble.w; // 0..1
+        SUB wobble.w, wobble.w, 0.5; // -0.5..0.5
+        ABS wobble.w, wobble.w; // now oscillates up and down between 0..0.5
+        MUL wobble.w, wobble.w, 0.5; // wobble amplitude
+
+        MAD wobble.xyz, wobble.w, opos, opos;
+        MOV wobble.w, opos.w;
+
+        @(if (>= (stringstr $arg1 "soft") 0) [result [
+            TEMP projtc;
+            DP4 projtc.x, state.matrix.mvp.row[0], wobble;
+            DP4 projtc.y, state.matrix.mvp.row[1], wobble;
+            DP4 projtc.z, state.matrix.mvp.row[2], wobble;
+            DP4 projtc.w, state.matrix.mvp.row[3], wobble;
+            MOV spos, projtc;
+
+            DP4 projtc.z, state.matrix.modelview.row[2], -wobble;
+            MAD projtc.z, projtc, program.env[5].x, program.env[5].y;
+            ADD projtc.xy, projtc, projtc.w;
+            MUL projtc.xy, projtc, program.env[6];
+            MOV result.texcoord[3], projtc;
+        ]] [result [
+            DP4 spos.x, state.matrix.mvp.row[0], wobble;
+            DP4 spos.y, state.matrix.mvp.row[1], wobble;
+            DP4 spos.z, state.matrix.mvp.row[2], wobble;
+            DP4 spos.w, state.matrix.mvp.row[3], wobble;
+        ]])
+
+        MOV result.color, vertex.color;
+
+        @arg2
+
+        @fogcoord
+        END
+    ] [
+        @fpstart
+        OPTION ARB_fog_linear;
+        TEMP dtc, diffuse, blend;
+
+        TEX dtc, @arg3, texture[0], 2D;
+        MAD dtc.xy, dtc, 0.1, fragment.texcoord[0]; // use color texture as noise to distort texcoords
+        TEX diffuse, dtc, texture[0], 2D;
+
+        TEX blend, fragment.texcoord[1], texture[1], 2D; // get blend factors from modulation texture
+
+        @(if (>= (stringstr $arg1 "glare") 0) [result [
+            TEMP k;
+            MUL k.x, blend.a, blend.a;
+            MUL diffuse.rgb, diffuse, 8;
+            ADD diffuse.b, diffuse, k.x;
+            MUL diffuse, diffuse, k.x;
+        ]] [result [
+            MAD diffuse, diffuse, 4, { 0, 0, -0.5, 0 }; // intensify and over saturate + blue tint
+            MAD diffuse, diffuse, blend.a, { 0, 0, 0.5, 0 }; // dup alpha into RGB channels + blue bias
+        ]])
+
+        @(if (>= (stringstr $arg1 "soft") 0) [result [
+            MUL result.color.rgb, diffuse, fragment.color;
+
+            TEMP depth;
+            TXP depth, fragment.texcoord[3], texture[2], @(if (>= (stringstr $arg1 "rect") 0) [result "RECT"] [result "2D"]);
+            @(if (>= (stringstr $arg1 "soft8") 0) [result [
+                DP4 depth.x, depth, program.env[6];
+                SUB_SAT depth.x, depth.x, fragment.texcoord[3].z;
+            ]] [result [
+                MAD_SAT depth.x, depth.x, program.env[5].z, -fragment.texcoord[3].z;
+            ]])
+            MUL depth.x, depth.x, fragment.color.a;
+            MAX depth.x, depth.x, program.env[5].w;
+            MUL result.color.a, diffuse.a, depth.x;
+        ]] [result [
+            MUL result.color, diffuse, fragment.color;
+        ]])
+
+        END
+    ]
+]
+
+loop i (if $usetexrect 6 4) [
+    explosionshader (concatword "explosion2d" (at ["" "glare" "soft" "soft8" "softrect" "soft8rect"] $i)) [
+        TEMP dtc; //blow up the tex coords
+        MAD dtc.x, program.env[1].x, -1.414, 1.768; // -2, 2.5; -> -2*sqrt(0.5), 2.5*sqrt(0.5);
+        MUL dtc.x, dtc.x, dtc.x;
+        MUL dtc.xy, opos, dtc.x;
+        //MAD dtc, dtc, 0.5, 0.5; centering at 0.5 not really needed for color sample since texture isn't clamped, so can just fold in the multiplication as above
+        MAD result.texcoord[0].xy, program.env[1].w, 0.0004, dtc;
+        MAD result.texcoord[1].xy, opos, 0.5, 0.5; //using wobble makes it look too spherical at a distance
+    ] "fragment.texcoord[1]"
+    explosionshader (concatword "explosion3d" (at ["" "glare" "soft" "soft8" "softrect" "soft8rect"] $i)) [
+        MOV result.texcoord[0], vertex.texcoord[0];
+
+        TEMP texgen;
+        DP4 texgen.x, opos, program.env[2];
+        DP4 texgen.y, opos, program.env[3];
+        MOV result.texcoord[1].xy, texgen;
+        MAD result.texcoord[2].xy, program.env[1].w, -0.0005, texgen;
+    ] "fragment.texcoord[2]"
+]
+
+shader 0 "particlenotexture" [
+    @vpstart
+    MUL result.texcoord[0], vertex.color, program.env[4];
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+    MOV result.color, fragment.texcoord[0];
+    END
+]
+
+particleshader = [
+    shader 0 $arg1 [
+        @vpstart
+        MOV result.texcoord[0], vertex.texcoord[0];
+        MUL result.texcoord[1], vertex.color, program.env[4];
+
+        @(if (>= (stringstr $arg1 "soft") 0) [result [
+            TEMP projtc;
+            DP4 projtc.x, state.matrix.mvp.row[0], opos;
+            DP4 projtc.y, state.matrix.mvp.row[1], opos;
+            DP4 projtc.w, state.matrix.mvp.row[3], opos;
+
+            ADD projtc.xy, projtc, projtc.w;
+            MUL projtc.xy, projtc, program.env[6];
+            MOV result.texcoord[2].xyw, projtc;
+
+            TEMP offset;
+            MAD offset.xyz, vertex.texcoord[0], { 2.82842712474619, 2.82842712474619, 0, 0 }, { -1.4142135623731, -1.4142135623731, 1, 0 };
+            MOV result.texcoord[3].xyz, offset;
+
+            DP4 offset.z, state.matrix.modelview.row[2], -opos;
+            MAD offset.z, offset.z, program.env[5].x, program.env[5].y;
+            MOV result.texcoord[4].xyz, offset;
+        ]])
+
+        @fogcoord
+        END
+    ] [
+        @fpstart
+        OPTION ARB_fog_linear;
+        TEMP diffuse;
+        TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+        @(if (>= (stringstr $arg1 "soft") 0) [result [
+            TEMP depth, offset;
+            TXP depth, fragment.texcoord[2], texture[2], @(if (>= (stringstr $arg1 "rect") 0) [result "RECT"] [result "2D"]);
+            DP3 offset.x, fragment.texcoord[3], fragment.texcoord[4];
+            @(if (>= (stringstr $arg1 "soft8") 0) [result [
+                DP4 depth.x, depth, program.env[6];
+                SUB_SAT depth.x, depth.x, offset.x;
+            ]] [result [
+                MAD_SAT depth.x, depth.x, program.env[5].z, -offset.x;
+            ]])
+            MUL diffuse.a, diffuse.a, depth.x;
+        ]])
+        MUL result.color, fragment.texcoord[1], diffuse;
+        END
+    ]
+]
+
+loop i (if $usetexrect 5 3) [
+    particleshader (concatword "particle" (at ["" "soft" "soft8" "softrect" "soft8rect"] $i))
+]
+
+shader 0 "blendbrush" [
+    @vpstart
+    DP4 result.texcoord[0].x, opos, program.env[0];
+    DP4 result.texcoord[0].y, opos, program.env[1];
+    MOV result.color, vertex.color;
+    END
+] [
+    @fpstart
+    TEMP brush;
+    TEX brush, fragment.texcoord[0], texture[0], 2D;
+    MUL result.color, fragment.color, brush;
+    END
+]
+
+lazyshader 0 "moviergb" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    END
+] [
+    @fpstart
+    TEX result.color, fragment.texcoord[0], texture[0], RECT;
+    END
+]
+
+lazyshader 0 "movieyuv" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    END
+] [
+    @fpstart
+    TEMP sample;
+    TEX sample, fragment.texcoord[0], texture[0], RECT;
+    DP4 result.color.b, sample, { 0.256788, 0.504125, 0.097905, 0.062745 };
+    DP4 result.color.g, sample, { -0.148224, -0.290992, 0.439216, 0.501961 };
+    DP4 result.color.r, sample, { 0.439216, -0.367788, -0.071427, 0.501961 };
+    END
+]
+
+lazyshader 0 "moviey" [
+    @vpstart
+    ADD result.texcoord[0], vertex.texcoord[0], { -1.5, 0, 0, 0 };
+    ADD result.texcoord[1], vertex.texcoord[0], { -0.5, 0, 0, 0 };
+    ADD result.texcoord[2], vertex.texcoord[0], {  0.5, 0, 0, 0 };
+    ADD result.texcoord[3], vertex.texcoord[0], {  1.5, 0, 0, 0 };
+    END
+] [
+    @fpstart
+    TEMP sample1, sample2, sample3, sample4;
+    TEX sample1, fragment.texcoord[0], texture[0], RECT;
+    TEX sample2, fragment.texcoord[1], texture[0], RECT;
+    TEX sample3, fragment.texcoord[2], texture[0], RECT;
+    TEX sample4, fragment.texcoord[3], texture[0], RECT;
+    DP4 result.color.b, sample1, { 0.256788, 0.504125, 0.097905, 0.062745 };
+    DP4 result.color.g, sample2, { 0.256788, 0.504125, 0.097905, 0.062745 };
+    DP4 result.color.r, sample3, { 0.256788, 0.504125, 0.097905, 0.062745 };
+    DP4 result.color.a, sample4, { 0.256788, 0.504125, 0.097905, 0.062745 };
+    END
+]
+
+lazyshader 0 "movieu" [
+    @vpstart
+    ADD result.texcoord[0], vertex.texcoord[0], { -3, 0, 0, 0 };
+    ADD result.texcoord[1], vertex.texcoord[0], { -1, 0, 0, 0 };
+    ADD result.texcoord[2], vertex.texcoord[0], {  1, 0, 0, 0 };
+    ADD result.texcoord[3], vertex.texcoord[0], {  3, 0, 0, 0 };
+    END
+] [
+    @fpstart
+    TEMP sample1, sample2, sample3, sample4;
+    TEX sample1, fragment.texcoord[0], texture[0], RECT;
+    TEX sample2, fragment.texcoord[1], texture[0], RECT;
+    TEX sample3, fragment.texcoord[2], texture[0], RECT;
+    TEX sample4, fragment.texcoord[3], texture[0], RECT;
+    DP4 result.color.b, sample1, { -0.148224, -0.290992, 0.439216, 0.501961  };
+    DP4 result.color.g, sample2, { -0.148224, -0.290992, 0.439216, 0.501961  };
+    DP4 result.color.r, sample3, { -0.148224, -0.290992, 0.439216, 0.501961  };
+    DP4 result.color.a, sample4, { -0.148224, -0.290992, 0.439216, 0.501961  };
+    END
+]
+
+lazyshader 0 "moviev" [
+    @vpstart
+    ADD result.texcoord[0], vertex.texcoord[0], { -3, 0, 0, 0 };
+    ADD result.texcoord[1], vertex.texcoord[0], { -1, 0, 0, 0 };
+    ADD result.texcoord[2], vertex.texcoord[0], {  1, 0, 0, 0 };
+    ADD result.texcoord[3], vertex.texcoord[0], {  3, 0, 0, 0 };
+    END
+] [
+    @fpstart
+    TEMP sample1, sample2, sample3, sample4;
+    TEX sample1, fragment.texcoord[0], texture[0], RECT;
+    TEX sample2, fragment.texcoord[1], texture[0], RECT;
+    TEX sample3, fragment.texcoord[2], texture[0], RECT;
+    TEX sample4, fragment.texcoord[3], texture[0], RECT;
+    DP4 result.color.b, sample1, { 0.439216, -0.367788, -0.071427, 0.501961 };
+    DP4 result.color.g, sample2, { 0.439216, -0.367788, -0.071427, 0.501961 };
+    DP4 result.color.r, sample3, { 0.439216, -0.367788, -0.071427, 0.501961 };
+    DP4 result.color.a, sample4, { 0.439216, -0.367788, -0.071427, 0.501961 };
+    END
+]
+
+///////////////////////////////////////////////////
+//
+// reflective/refractive water shaders:
+//
+///////////////////////////////////////////////////
+
+watershader = [
+    specular = $arg2
+    rgbfog = $arg3
+    distort = $arg4
+    combine = $arg5
+    lazyshader 0 $arg1 [
+        @vpstart
+        TEMP tc;
+        PARAM campos = program.env[0];
+        PARAM seconds = program.env[1];
+        @(if $specular [result "PARAM lightpos = program.env[2];"])
+
+        DP4 result.texcoord[0].x, state.matrix.texture.row[0], opos;
+        DP4 result.texcoord[0].y, state.matrix.texture.row[1], opos;
+        @(if (>= (stringstr $arg1 "underwater") 0) [result [
+            SUB result.texcoord[0].z, program.env[7], opos.z;
+        ]] [result [
+            SUB result.texcoord[0].z, opos.z, program.env[7];
+        ]])
+        DP4 result.texcoord[0].w, state.matrix.texture.row[3], opos;
+        MUL tc, vertex.texcoord[0], 0.1;
+        MAD result.texcoord[1].xy, seconds, 0.04, tc;
+        MAD result.texcoord[2].xy, seconds, -0.02, tc;
+        SUB result.texcoord[3].xyz, campos, opos;
+        @(if $specular [result "SUB result.texcoord[4].xyz, lightpos, opos;"])
+
+        MOV result.color, vertex.color;
+
+        @fogcoord
+
+        END
+    ] [
+        @fpstart
+        @(if $rgbfog [result "OPTION ARB_fog_linear;"])
+        TEMP he, light, cam, bump, invfresnel, temp, dudv;
+
+        ATTRIB projtc = fragment.texcoord[0];
+        ATTRIB tc1 = fragment.texcoord[1];
+        ATTRIB tc2 = fragment.texcoord[2];
+        ATTRIB camts = fragment.texcoord[3];
+        @(if $specular [result "ATTRIB lightts = fragment.texcoord[4];"])
+
+        @(normalize cam camts)
+        @(if $specular [result [
+            @(normalize light lightts)
+            ADD he.xyz, cam, light;
+            @(normalize he he)
+        ]])
+
+        TEX dudv, tc1, texture[2], 2D;
+        MAD dudv.xy, dudv, 2, -1;
+
+        @distort
+
+        @(if $specular [result [
+            PARAM specfactor = 96;
+
+            DP3_SAT he.x, he, bump;
+            POW he.x, he.x, specfactor.w;
+
+            MUL light.w, program.env[4].x, light.w;
+            RCP_SAT light.w, light.w;
+            MAD light.xyz, light.w, -program.env[3], program.env[3];
+        ]])
+
+        @combine
+
+        END
+    ]
+]
+
+watershader "waterglare" 1 1 [
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+] [
+    MUL he.x, he.x, he.x;
+    MUL he.x, he.x, 32;
+    MUL result.color.rgb, he.x, light;
+    MOV result.color.a, 0;
+]
+lazyshader 0 "waterglarefast" [
+    @vpstart
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+    MOV result.color, 0;
+    END
+]
+fastshader waterglare waterglarefast 2
+altshader waterglare waterglarefast
+
+lazyshader 0 "underwater" [
+    @vpstart
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    TEMP col;
+    MUL col.rgb, fragment.color, program.env[5].x;
+    MUL col.rgb, col, 0.8;
+    MUL col.a, 0.5, program.env[5].y;
+    @(rgbafog col "{0, 0, 0, 1}")
+    END
+]
+
+watershader "underwaterrefract" 0 1 [
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX dudv, temp, texture[2], 2D;
+    MAD dudv.xy, dudv, 2, -1;
+
+    RCP temp.w, projtc.w;
+    MUL temp.xy, projtc, temp.w;
+    MAD temp.xy, dudv, 0.01, temp;
+
+    TEX result.color, temp, texture[3], 2D;
+] []
+watershader "underwaterrefractfast" 0 1 [
+    MAD temp.xy, dudv, 0.4, projtc;
+    MOV temp.zw, projtc;
+    TXP result.color, temp, texture[3], 2D;
+] []
+fastshader underwaterrefract underwaterrefractfast 2
+altshader underwaterrefract underwaterrefractfast
+
+watershader "underwaterfade" 0 1 [
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX dudv, temp, texture[2], 2D;
+    MAD dudv.xy, dudv, 2, -1;
+
+    RCP temp.w, projtc.w;
+    MUL temp.xy, projtc, temp.w;
+
+    TEMP fade;
+    TEX fade.a, temp, texture[3], 2D;
+    MAD result.color.a, fade.a, 4, projtc.z;
+
+    MAD temp.xy, dudv, 0.01, temp;
+    TEX result.color.rgb, temp, texture[3], 2D;
+] []
+watershader "underwaterfadefast" 0 1 [
+    MAD temp.xy, dudv, 0.4, projtc;
+    MOV temp.zw, projtc;
+    TXP result.color.rgb, temp, texture[3], 2D;
+
+    TEMP fade;
+    TXP fade.a, projtc, texture[3], 2D;
+    MAD result.color.a, fade.a, 4, projtc.z;
+] []
+fastshader underwaterfade underwaterfadefast 2
+altshader underwaterfade underwaterfadefast
+
+watershader "water" 1 0 [
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+] [
+    DP3_SAT invfresnel.x, cam, bump;
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    MAD temp.x, invfresnel.x, 0.4, 0.6;
+    MUL temp.x, temp.x, program.env[5].x;
+
+    TEMP col;
+    MUL col.rgb, temp.x, fragment.color;
+    MAD col.rgb, he.x, light, col;
+    MUL col.a, invfresnel.x, program.env[5].y;
+    @(rgbafog col "{0, 0, 0, 1}")
+]
+watershader "waterfast" 0 0 [
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+] [
+    DP3_SAT invfresnel.x, cam, bump;
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    MAD temp.x, invfresnel.x, 0.4, 0.6;
+    MUL temp.x, temp.x, program.env[5].x;
+
+    TEMP col;
+    MUL col.rgb, temp.x, fragment.color;
+    MUL col.a, invfresnel.x, program.env[5].y;
+    @(rgbafog col "{0, 0, 0, 1}")
+]
+fastshader water waterfast 1
+altshader water waterfast
+
+watershader "waterreflect" 1 0 [
+    TEMP reflect;
+
+    MAD temp.xy, dudv, 0.4, projtc;
+    MOV temp.zw, projtc;
+    TXP reflect, temp, texture[0], 2D;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+] [
+    DP3_SAT invfresnel.x, cam, bump;
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    MUL temp.rgb, fragment.color, program.env[5].x;
+    LRP temp.rgb, invfresnel.x, temp, reflect;
+
+    TEMP col;
+    MAD col.rgb, he.x, light, temp;
+    MUL col.a, invfresnel.x, program.env[5].y;
+    @(rgbafog col "{0, 0, 0, 1}")
+]
+watershader "waterreflectfast" 0 0 [
+    TEMP reflect;
+
+    MAD temp.xy, dudv, 0.4, projtc;
+    MOV temp.zw, projtc;
+    TXP reflect, temp, texture[0], 2D;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+] [
+    DP3_SAT invfresnel.x, cam, bump;
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    MUL temp.rgb, fragment.color, program.env[5].x;
+
+    TEMP col;
+    LRP col.rgb, invfresnel.x, temp, reflect;
+    MUL col.a, invfresnel.x, program.env[5].y;
+    @(rgbafog col "{0, 0, 0, 1}")
+]
+fastshader waterreflect waterreflectfast 2
+altshader waterreflect waterreflectfast
+
+watershader "waterrefract" 1 1 [
+    TEMP reflect, refract;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+    TEX dudv, temp, texture[2], 2D;
+    MAD dudv.xy, dudv, 2, -1;
+
+    RCP temp.w, projtc.w;
+    MUL temp.xy, projtc, temp.w;
+    MAD temp.xy, dudv, 0.01, temp;
+
+    TEX reflect, temp, texture[0], 2D;
+    TEX refract, temp, texture[3], 2D;
+] [
+    DP3_SAT invfresnel.x, cam, bump;
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    LRP temp.rgb, invfresnel.x, refract, reflect;
+    MAD result.color.rgb, he.x, light, temp;
+    MOV result.color.a, 0;
+]
+watershader "waterrefractfast" 0 1 [
+    TEMP reflect, refract;
+
+    MAD temp.xy, dudv, 0.4, projtc;
+    MOV temp.zw, projtc;
+    TXP reflect, temp, texture[0], 2D;
+    TXP refract, temp, texture[3], 2D;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+] [
+    DP3_SAT invfresnel.x, cam, bump;
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    LRP result.color, invfresnel.x, refract, reflect;
+]
+fastshader waterrefract waterrefractfast 2
+altshader waterrefract waterrefractfast
+
+watershader "waterfade" 1 1 [
+    TEMP distort, reflect, refract, fade;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+    TEX dudv, temp, texture[2], 2D;
+    MAD dudv.xy, dudv, 2, -1;
+
+    RCP temp.w, projtc.w;
+    MUL temp.xy, projtc, temp.w;
+    MAD distort.xy, dudv, 0.01, temp;
+
+    TEX reflect, distort, texture[0], 2D;
+    TEX refract, distort, texture[3], 2D;
+    TEX fade.a, temp, texture[3], 2D;
+    MAD result.color.a, fade.a, 4, projtc.z;
+] [
+    DP3_SAT invfresnel.x, cam, bump;
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    LRP temp.rgb, invfresnel.x, refract, reflect;
+    MAD result.color.rgb, he.x, light, temp;
+]
+watershader "waterfadefast" 0 1 [
+    TEMP reflect, refract, fade;
+
+    MAD temp.xy, dudv, 0.4, projtc;
+    MOV temp.zw, projtc;
+    TXP reflect, temp, texture[0], 2D;
+    TXP refract, temp, texture[3], 2D;
+    TXP fade.a, projtc, texture[3], 2D;
+    MAD result.color.a, fade.a, 4, projtc.z;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+] [
+    DP3_SAT invfresnel.x, cam, bump;
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    LRP result.color.rgb, invfresnel.x, refract, reflect;
+]
+fastshader waterfade waterfadefast 2
+altshader waterfade waterrefract
+
+watershader "waterenv" 1 0 [
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+
+    TEMP rvec, reflect;
+    DP3_SAT invfresnel.x, cam, bump;
+    MUL rvec.xyz, invfresnel.x, bump;
+    MAD rvec.xyz, rvec, 2, -cam;
+    TEX reflect, rvec, texture[0], CUBE;
+] [
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    MUL temp.rgb, fragment.color, program.env[5].x;
+    LRP temp.rgb, invfresnel.x, temp, reflect;
+
+    TEMP col;
+    MAD col.rgb, he.x, light, temp;
+    MUL col.a, invfresnel.x, program.env[5].y;
+    @(rgbafog col "{0, 0, 0, 1}")
+]
+watershader "waterenvfast" 0 0 [
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+
+    TEMP rvec, reflect;
+    DP3_SAT invfresnel.x, cam, bump;
+    MUL rvec.xyz, invfresnel.x, bump;
+    MAD rvec.xyz, rvec, 2, -cam;
+    TEX reflect, rvec, texture[0], CUBE;
+] [
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    MUL temp.rgb, fragment.color, program.env[5].x;
+
+    TEMP col;
+    LRP col.rgb, invfresnel.x, temp, reflect;
+    MUL col.a, invfresnel.x, program.env[5].y;
+    @(rgbafog col "{0, 0, 0, 1}")
+]
+fastshader waterenv waterenvfast 2
+altshader waterenv waterenvfast
+
+watershader "waterenvrefract" 1 1 [
+    TEMP distort, reflect, refract;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+    TEX dudv, temp, texture[2], 2D;
+    MAD dudv.xy, dudv, 2, -1;
+
+    RCP temp.w, projtc.w;
+    MUL temp.xy, projtc, temp.w;
+    MAD distort.xy, dudv, 0.01, temp;
+
+    TEX refract, distort, texture[3], 2D;
+
+    TEMP rvec;
+    DP3_SAT invfresnel.x, cam, bump;
+    MUL rvec.xyz, invfresnel.x, bump;
+    MAD rvec.xyz, rvec, 2, -cam;
+    TEX reflect, rvec, texture[0], CUBE;
+] [
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    LRP temp.rgb, invfresnel.x, refract, reflect;
+    MAD result.color.rgb, he.x, light, temp;
+]
+watershader "waterenvrefractfast" 0 1 [
+    TEMP reflect, refract;
+
+    MAD temp.xy, dudv, 0.4, projtc;
+    MOV temp.zw, projtc;
+    TXP refract, temp, texture[3], 2D;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+
+    TEMP rvec;
+    DP3_SAT invfresnel.x, cam, bump;
+    MUL rvec.xyz, invfresnel.x, bump;
+    MAD rvec.xyz, rvec, 2, -cam;
+    TEX reflect, rvec, texture[0], CUBE;
+] [
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    LRP result.color.rgb, invfresnel.x, refract, reflect;
+]
+fastshader waterenvrefract waterenvrefractfast 2
+altshader waterenvrefract waterenvrefractfast
+
+watershader "waterenvfade" 1 1 [
+    TEMP distort, reflect, refract, fade;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+    TEX dudv, temp, texture[2], 2D;
+    MAD dudv.xy, dudv, 2, -1;
+
+    RCP temp.w, projtc.w;
+    MUL temp.xy, projtc, temp.w;
+    MAD distort.xy, dudv, 0.01, temp;
+
+    TEX refract, distort, texture[3], 2D;
+    TEX fade.a, temp, texture[3], 2D;
+    MAD result.color.a, fade.a, 4, projtc.z;
+
+    TEMP rvec;
+    DP3_SAT invfresnel.x, cam, bump;
+    MUL rvec.xyz, invfresnel.x, bump;
+    MAD rvec.xyz, rvec, 2, -cam;
+    TEX reflect, rvec, texture[0], CUBE;
+] [
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    LRP temp.rgb, invfresnel.x, refract, reflect;
+    MAD result.color.rgb, he.x, light, temp;
+]
+watershader "waterenvfadefast" 0 1 [
+    TEMP reflect, refract, fade;
+
+    MAD temp.xy, dudv, 0.4, projtc;
+    MOV temp.zw, projtc;
+    TXP refract, temp, texture[3], 2D;
+    TXP fade.a, projtc, texture[3], 2D;
+    MAD result.color.a, fade.a, 4, projtc.z;
+
+    MAD temp.xy, dudv, 0.025, tc2;
+    TEX bump, temp, texture[1], 2D;
+    MAD bump.xyz, bump, 2, -1;
+
+    TEMP rvec;
+    DP3_SAT invfresnel.x, cam, bump;
+    MUL rvec.xyz, invfresnel.x, bump;
+    MAD rvec.xyz, rvec, 2, -cam;
+    TEX reflect, rvec, texture[0], CUBE;
+] [
+    MAD invfresnel.x, invfresnel.x, 0.5, 0.5;
+    LRP result.color.rgb, invfresnel.x, refract, reflect;
+]
+fastshader waterenvfade waterenvfadefast 2
+altshader waterenvfade waterenvrefract
+
+causticshader = [
+    lazyshader 0 $arg1 [
+        @vpstart
+        DP3 result.texcoord[0].x, opos, program.env[0];
+        DP3 result.texcoord[0].y, opos, program.env[1];
+        @fogcoord
+        END
+    ] [
+        @fpstart
+        OPTION ARB_fog_linear;
+        @arg2
+        END
+    ]
+]
+causticshader caustic [
+    TEMP caustic, caustic2;
+    TEX caustic, fragment.texcoord[0], texture[0], 2D;
+    TEX caustic2, fragment.texcoord[0], texture[1], 2D;
+    MUL caustic, caustic, program.env[0].x;
+    MAD result.color, caustic2, program.env[0].y, caustic;
+]
+causticshader causticfast [
+  TEMP caustic;
+  TEX caustic, fragment.texcoord[0], texture[0], 2D;
+  MUL result.color, caustic, program.env[0].z;
+]
+fastshader caustic causticfast 2
+
+lazyshader 0 "lava" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+    TEMP diffuse;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MUL diffuse, diffuse, 2;
+    MUL result.color, fragment.color, diffuse;
+    END
+]
+
+lazyshader 0 "lavaglare" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MAD result.color, vertex.color, 2, -1;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+    TEMP glow, k;
+    TEX glow, fragment.texcoord[0], texture[0], 2D;
+    MUL glow, glow, fragment.color;
+    MAX k.x, glow.r, glow.g;
+    MAX k.x, k.x, glow.b;
+    MUL k.x, k.x, k.x;
+    MUL k.x, k.x, 32;
+    MUL result.color, k.x, glow;
+    END
+]
+
+lazyshader 0 "waterfallrefract" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    DP4 result.texcoord[1].x, state.matrix.texture.row[0], opos;
+    DP4 result.texcoord[1].y, state.matrix.texture.row[1], opos;
+    DP4 result.texcoord[1].w, state.matrix.texture.row[3], opos;
+
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+
+    TEMP diffuse, dudv;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MAD dudv.xy, diffuse, 0.2, fragment.texcoord[0];
+    ADD dudv.xy, dudv, program.env[1];
+    TEX dudv, dudv, texture[2], 2D;
+
+    TEMP projtc, refract;
+    MAD projtc.xy, dudv, 4, fragment.texcoord[1];
+    MOV projtc.zw, fragment.texcoord[1];
+    TXP refract, projtc, texture[4], 2D;
+
+    LRP result.color, diffuse, fragment.color, refract;
+
+    END
+]
+
+lazyshader 0 "waterfallenvrefract" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    SUB result.texcoord[1].xyz, program.env[0], vertex.position;
+
+    TEMP absnormal;
+    ABS absnormal.xyz, vertex.normal;
+
+    MOV result.texcoord[2].xyz, absnormal.yzxw;
+    MOV result.texcoord[3].xyz, -absnormal.zxyw;
+    MOV result.texcoord[4].xyz, vertex.normal;
+
+    DP4 result.texcoord[5].x, state.matrix.texture.row[0], opos;
+    DP4 result.texcoord[5].y, state.matrix.texture.row[1], opos;
+    DP4 result.texcoord[5].w, state.matrix.texture.row[3], opos;
+
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+
+    TEMP diffuse, dudv;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MAD dudv.xy, diffuse, 0.2, fragment.texcoord[0];
+    ADD dudv.xy, dudv, program.env[1];
+    TEX dudv, dudv, texture[2], 2D;
+
+    TEMP ntc, normal, wnormal;
+    MAD ntc.xy, dudv, 0.1, fragment.texcoord[0];
+    TEX normal, ntc, texture[1], 2D;
+    MAD normal.xyz, normal, 2, -1;
+    MUL wnormal.xyz, normal.x, fragment.texcoord[2];
+    MAD wnormal.xyz, normal.y, fragment.texcoord[3], wnormal;
+    MAD wnormal.xyz, normal.z, fragment.texcoord[4], wnormal;
+
+    TEMP projtc, refract;
+    MAD projtc.xy, dudv, 4, fragment.texcoord[5];
+    MOV projtc.zw, fragment.texcoord[5];
+    TXP refract, projtc, texture[4], 2D;
+
+    TEMP cam, invfresnel, rvec, reflect;
+    @(normalize cam fragment.texcoord[1])
+    DP3 invfresnel.x, wnormal, cam;
+    MUL rvec.xyz, invfresnel.x, wnormal;
+    MAD rvec.xyz, rvec, 2, -cam;
+    TEX reflect, rvec, texture[3], CUBE;
+    CMP invfresnel.x, invfresnel.x, 1, 0;
+    MAD_SAT invfresnel.x, invfresnel.x, 0.4, 0.6;
+    LRP refract, invfresnel.x, refract, reflect;
+    LRP result.color, diffuse, fragment.color, refract;
+
+    END
+]
+altshader waterfallenvrefract waterfallrefract
+
+lazyshader 0 "waterfallenv" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    SUB result.texcoord[1].xyz, program.env[0], vertex.position;
+
+    TEMP absnormal;
+    ABS absnormal.xyz, vertex.normal;
+
+    MOV result.texcoord[2].xyz, absnormal.yzxw;
+    MOV result.texcoord[3].xyz, -absnormal.zxyw;
+    MOV result.texcoord[4].xyz, vertex.normal;
+
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+
+    TEMP diffuse, dudv;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MAD dudv.xy, diffuse, 0.2, fragment.texcoord[0];
+    ADD dudv.xy, dudv, program.env[1];
+    TEX dudv, dudv, texture[2], 2D;
+    MAD dudv.xy, dudv, 0.1, fragment.texcoord[0];
+
+    TEMP normal, wnormal;
+    TEX normal, dudv, texture[1], 2D;
+    MAD normal.xyz, normal, 2, -1;
+    MUL wnormal.xyz, normal.x, fragment.texcoord[2];
+    MAD wnormal.xyz, normal.y, fragment.texcoord[3], wnormal;
+    MAD wnormal.xyz, normal.z, fragment.texcoord[4], wnormal;
+
+    TEMP cam, rvec, reflect;
+    @(normalize cam fragment.texcoord[1])
+    DP3 rvec.w, wnormal, cam;
+    MUL rvec.xyz, rvec.w, wnormal;
+    MAD rvec.xyz, rvec, 2, -cam;
+    TEX reflect, rvec, texture[3], CUBE;
+    MAD result.color.a, diffuse.r, 0.75, 0.25;
+    LRP result.color.rgb, diffuse, fragment.color, reflect;
+
+    END
+]
+
+lazyshader 0 "glass" [
+    @vpstart
+    PARAM campos = program.env[0];
+    ATTRIB normal = vertex.normal;
+    MOV result.texcoord[0], vertex.texcoord[0];
+    SUB result.texcoord[1].xyz, campos, opos;
+    MOV result.texcoord[2].xyz, normal;
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    ATTRIB rvec = fragment.texcoord[0];
+    ATTRIB camvec = fragment.texcoord[1];
+    ATTRIB normal = fragment.texcoord[2];
+
+    TEMP reflect;
+    TEX reflect, rvec, texture[0], CUBE;
+
+    TEMP invfresnel;
+    @(normalize invfresnel camvec)
+    DP3 invfresnel.x, invfresnel, normal;
+    MAX invfresnel.x, invfresnel.x, 0.70;
+
+    TEMP col;
+    MUL col.rgb, fragment.color, 0.05;
+    LRP col.rgb, invfresnel.x, col, reflect;
+    MUL col.a, invfresnel.x, 0.95;
+
+    @(rgbafog col "{0, 0, 0, 1}")
+    END
+]
+lazyshader 0 "glassfast" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    ATTRIB rvec = fragment.texcoord[0];
+
+    TEMP reflect;
+    TEX reflect, rvec, texture[0], CUBE;
+
+    PARAM invfresnel = 0.75;
+    TEMP col;
+    MUL col.rgb, fragment.color, 0.05;
+    LRP col.rgb, invfresnel, col, reflect;
+    MUL col.a, invfresnel, 0.95;
+
+    @(rgbafog col "{0, 0, 0, 1}")
+    END
+]
+fastshader glass glassfast 2
+altshader glass glassfast
+
+lazyshader 0 "grass" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.texcoord[1], vertex.texcoord[1];
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    TEMP diffuse, lm;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MUL diffuse.rgb, diffuse, 2;
+    TEX lm, fragment.texcoord[1], texture[1], 2D;
+    MUL lm, lm, fragment.color;
+    MUL lm.rgb, lm, lm.a;
+    MUL diffuse, diffuse, lm;
+    @(rgbafog diffuse "{0, 0, 0, 0}")
+    END
+]
+
+shader 0 "overbrightdecal" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+    TEMP diffuse;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    LRP result.color, fragment.color.a, diffuse, fragment.color;
+    END
+]
+
+shader 0 "saturatedecal" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    @fogcoord
+    END
+] [
+    @fpstart
+    OPTION ARB_fog_linear;
+    TEMP diffuse;
+    TEX diffuse, fragment.texcoord[0], texture[0], 2D;
+    MUL diffuse.xyz, diffuse, 2;
+    MUL result.color, fragment.color, diffuse;
+    END
+]
+
+shader 0 "skyboxglare" [
+    @vpstart
+    MOV result.texcoord[0], vertex.texcoord[0];
+    MOV result.color, vertex.color;
+    END
+] [
+    @fpstart
+    TEMP glare;
+    TEX glare, fragment.texcoord[0], texture[0], 2D;
+    MUL glare, glare, fragment.color;
+    //DP3 intensity, glare, { 0.33, 0.34, 0.33 };
+    //SUB_SAT intensity, intensity, 0.95;
+    //MUL intensity, intensity, 32;
+    //MUL glare, glare, intensity;
+    // ^ all folded into single dot product below
+    //DP4 result.color, glare, { 10.56, 10.88, 10.56, -30.4 };
+    DPH result.color.rgb, glare, { 10.56, 10.88, 10.56, -30.4 };
+    MOV result.color.a, glare.a;
+    END
+]
+
diff --git a/config/tips.cfg b/config/tips.cfg
new file mode 100644
index 0000000..c11dcec
--- /dev/null
+++ b/config/tips.cfg
@@ -0,0 +1,41 @@
+tips = 0
+addtip = [
+    tips = (+ $tips 1)
+    [tip at tips] = $arg1
+]
+
+resettips = [
+    tips = 0
+    addtip (format "press %1 to ^fs^fyjump^fS and again in mid-air to ^fs^fyimpulse boost^fS" (dobindsearch "jump"))
+    addtip (format "press %1 to ^fs^fycrouch^fS, doing so while landing will perform an ^fs^fyimpulse slide^fS" (dobindsearch "crouch"))
+    addtip (format "press %1 and %2 to use your primary and secondary weapon fire modes" (dobindsearch "primary") (dobindsearch "secondary"))
+    addtip (format "press %1 to ^fs^fyreload^fS your weapon, timing this can be crucial to survival" (dobindsearch "reload"))
+    addtip (format "press %1 to ^fs^fyuse items^fS and ^fs^fytriggers^fS" (dobindsearch "use"))
+    addtip (format "press %1 to ^fs^fywall run^fS, ^fs^fywall kick^fS, or ^fs^fymelee^fS" (dobindsearch "special"))
+    addtip (format "press %1 to ^fs^fytalk^fS and %2 to only speak to ^fs^fyteammates^fS" (dobindsearch "saytextcommand (getsaycolour)") (dobindsearch "sayteamcommand (getsaycolour)"))
+    addtip (format "press %1 while in the air to ^fs^fyfly-kick^fS enemies" (dobindsearch "special"))
+    addtip (format "press %1 to ^fs^fysuicide^fS, this will reset you in ^fs^fyrace^fS" (dobindsearch "suicide"))
+    addtip (format "press %1 to crouch when landing to perform an ^fs^fyimpulse slide^fS" (dobindsearch "crouch"))
+    addtip (format "press %1 during an ^fs^fyimpulse slide^fS to perform an ^fs^fyimpulse launch^fS" (dobindsearch "jump"))
+    addtip (format "press %1 to open the ^fs^fyhelp menu^fS at any time" (dobindsearch "showgui help"))
+    addtip (format "press %1 to make a ^fs^fymap selection^fS" (dobindsearch "showgui maps 1"))
+    addtip (format "press %1 to show the ^fs^fyserver list^fS" (dobindsearch "showservers"))
+    addtip (format "press %1 to change your ^fs^fyloadout weapons^fS" (dobindsearch "showgui profile 2"))
+    addtip (format "press %1 to ^fs^fychange teams^fS" (dobindsearch "showgui team"))
+    addtip "when you're ^fs^foon fire^fS you can ^fs^fcjump in water^fS to put yourself out, crouch if necessary"
+    addtip "you're ^fs^fyless accurate^fS when ^fs^fyjumping^fS and ^fs^fymoving^fS, stop for a perfect shot"
+    addtip "you can chat with the community and developers in ^fs^fc#redeclipse^fS on ^fs^fcirc.freenode.net^fS"
+    addtip "share your own tips with the developers in ^fs^fc#redeclipse^fS on ^fs^fcirc.freenode.net^fS"
+    addtip "tips are ^fs^fccool^fS, you should ^fs^fyread them more often^fS"
+]
+
+lasttip = 0
+showtip = [
+    if (|| [= $lasttip 0] [> (- (getmillis) $lasttip) 30000]) [
+        resettips
+        curtip = $[tip@(+ (rnd $tips) 1)]
+        lasttip = (getmillis)
+    ]
+    result $curtip
+]
+curtip = (showtip)
diff --git a/config/usage.cfg b/config/usage.cfg
new file mode 100644
index 0000000..817593a
--- /dev/null
+++ b/config/usage.cfg
@@ -0,0 +1,809 @@
+// sets descriptions for commands/variables/etc
+// usage: setdesc <id> <description> "usage"
+setdesc "kick" "kicks a player from the server with an optional reason" "<clientnum> [<text>]"
+setdesc "allow" "sets a ban exemption for a player on the server" "<clientnum>"
+setdesc "ban" "bans a player from the server with an optional reason" "<clientnum> [<text>]"
+setdesc "mute" "mutes a player on the server with an optional reason" "<clientnum> [<text>]"
+setdesc "limit" "limits team changing for a player on the server" "<clientnum>"
+setdesc "spectator" "put a player to spectator mode;^n0 = join/unquarantine, 1 = spectate, 2 = spectate and quarantine" "<mode> [<cn>]"
+setdesc "quit" "quits the game"
+setdesc "version" "game version for scripts" "<version>"
+setdesc "servertype" "type of server, 1 = private (does not register with masterserver), 2 = public, 3 = dedicated" "<value>"
+setdesc "hasoctapaks" "mega hack;^ntry to find Cube 2, done after our own data so as to not clobber stuff" "<value>"
+setdesc "autosavebackups" "make backups;^n0 = off, 1 = single backup, 2 = named backup, 3/4 = same as 1/2 with move to ^"backups/^"" "<value>"
+setdesc "autoreloading" "0 = never, 1 = when empty, 2 = weapons that don't add a full clip, 3 = always (+1 zooming weaps too)" "<value>"
+setdesc "skipspawnweapon" "skip spawnweapon;^n0 = never, 1 = if numweaps > 1 (+1), 3 = if carry > 0 (+2), 6 = always" "<value>"
+setdesc "skipmelee" "skip melee;^n0 = never, 1 = if numweaps > 1 (+2), 4 = if carry > 0 (+2), 7 = if carry > 0 and is offset (+2), 10 = always" "<value>"
+setdesc "skippistol" "skip pistol;^n0 = never, 1 = if numweaps > 1 (+2), 4 = if carry > 0 (+2), 7 = if carry > 0 and is offset (+2), 10 = always" "<value>"
+setdesc "skipgrenade" "skip grenade;^n0 = never, 1 = if numweaps > 1 (+2), 4 = if carry > 0 (+2), 7 = if carry > 0 and is offset (+2), 10 = always" "<value>"
+setdesc "weapselectslot" "determines how weapons are selected using the number keys;^n0 = by global weapon numbers, 1 = by temporary pickup slot numbers" "<bool>"
+setdesc "showaiinfo" "determines how much info is shown for bots;^n0 = hide bot info, 1 = show bot joins/parts, 2 = show more verbose info" "<value>"
+setdesc "demolock" "determines who may record demos;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "democount" "determines the maximum amount of demo files that may be saved simultaneously on the server (deletes old demos if exceeded)" "<value>"
+setdesc "demomaxsize" "determines the maximum size of individual demo files that may be saved on the server" "<kilobytes>"
+setdesc "demoautorec" "determines if demos are automatically recorded each match" "<value>"
+setdesc "speclock" "determines who may force players to spectate;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "kicklock" "determines who may kick players;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "allowlock" "determines who may allow players;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "banlock" "determines who may ban players;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "mutelock" "determines who may mute players;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "limitlock" "determines who may limit players;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "vetolock" "determines who may force match votes;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "floodlock" "enables flood protection for everyone below a specific privilege level;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "floodmute" "automatically mute player when warned this many times" "<value>"
+setdesc "floodtime" "time span to check for floody messages" "<value>"
+setdesc "floodlines" "number of lines in floodtime span before too many" "<value>"
+setdesc "autospectate" "determines when the game switches automatically to spectate mode;^n0 = when idle, 1 = when remaining dead for autospecdelay" "<bool>"
+setdesc "resetallowsonend" "determines when the allow list is reset;^n0 = off, 1 = just when empty, 2 = when matches end" "<value>"
+setdesc "resetbansonend" "determines when the ban list is reset;^n0 = off, 1 = just when empty, 2 = when matches end" "<value>"
+setdesc "resetmutesonend" "determines when the mute list is reset;^n0 = off, 1 = just when empty, 2 = when matches end" "<value>"
+setdesc "resetlimitsonend" "determines when the limit list is reset;^n0 = off, 1 = just when empty, 2 = when matches end" "<value>"
+setdesc "resetvarsonend" "determines when these game variables are reset;^n0 = off, 1 = just when empty, 2 = when matches end" "<value>"
+setdesc "resetmmonend" "determines when privilege mode changes are reset;^n0 = off, 1 = just when empty, 2 = when matches end" "<value>"
+setdesc "gamespeedlock" "determines if gamespeed is locked (also limited by varslock);^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "rotatemodefilter" "determines the modes which can be selected when the server selects the next map,^nconvenient to set using a sum of $modebit* vars (available: editing, deathmatch, capture, defend, bomber, race),^nexample: (+ $modebitediting $modebitdeathmatch)" "<sum>"
+setdesc "rotatemuts" "determines if mutators rotate when the server selects the next map;^n0 = never rotate mutators, 1 = always rotate mutators, >1 = decrease chances the larger this value" "<value>"
+setdesc "rotatemutsfilter" "determines the mutators which can be selected when the server selects the next map,^nconvenient to set using a sum of $mutsbit* vars (available: multi, ffa, coop, instagib, medieval, kaboom, duel, survivor, classic, onslaught, freestyle, vampire, hard, resize, gsp1 = first mode variation {ctf-quick, dac-quick, bb-hold, tt-marathon}, gsp2 = second mode variation {ctf-defend, dac-king, bb-basket, tt-endurance}, gsp3 = third mode variation {ctf-protect, bb-attack [...]
+setdesc "kingmaps" "king-of-the-hill maps" "<list>"
+setdesc "multimaps" "maps allowed for modes which *require* multi spawns (ctf/bb)" "<list>"
+setdesc "modelock" "determines at which privilege level modes are locked, according to modelocktype;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "modelocktype" "determines the mode locking type;^n0 = off, 1 = lock level only, 2 = lock level can set limited mode and higher" "<value>"
+setdesc "mapsfilter" "0 = off, 1 = filter based on mutators, 2 = also filter based on players" "<value>"
+setdesc "mapslock" "determines at which privilege level maps are locked, according to mapslocktype;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "mapslocktype" "determines the maps locking type;^n0 = off, 1 = lock level only, 2 = lock level can select non-rotation" "<value>"
+setdesc "varslock" "determines if vars are locked;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "votelock" "determines at which privilege level votes are locked, according to votelocktype;^n0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = founder" "<value>"
+setdesc "votelocktype" "determines the vote locking type;^n0 = off, 1 = lock level only, 2 = lock level can select prevmaps" "<value>"
+setdesc "votewait" "time in milliseconds before a player may cast another vote (to avoid flooding)" "<milliseconds>"
+setdesc "votestyle" "determines how mid-match votes are handled;^n0 = votes don't pass mid-match, 1 = passes if votethreshold is met, 2 = passes if unanimous" "<milliseconds>"
+setdesc "votethreshold" "auto-pass votes when this many agree" "<value>"
+setdesc "voteinterm" "0 = must wait entire time, 1 = passes if votethreshold is met, 2 = passes if unanimous" "<value>"
+setdesc "smallmapmax" "maximum number of players for a small map" "<value>"
+setdesc "mediummapmax" "maximum number of players for a medium map" "<value>"
+setdesc "maxalive" "only allow this*numplayers (for current map) to be alive at once" "<value>"
+setdesc "maxalivequeue" "toggle queue system (used if number of players exceeds maxalive*numplayers for current map);^n0 = prohibits leaving spectator, 1 = players enter queue" "<value>"
+setdesc "maxaliveminimum" "only enables maxalive limit if the number of players is more than or equal to this" "<value>"
+setdesc "maxalivethreshold" "only enables maxalive limit if the number of players is more than or equal to this*numplayers (for current map)" "<value>"
+setdesc "spawnrotate" "spawn point rotation;^n0 = let client decide, 1 = sequence, 2 = random" "<value>"
+setdesc "spawngrenades" "spawn with grenades;^n0 = never, 1 = all but instagib/race, 2 = always" "<value>"
+setdesc "spawnmines" "spawn with mines;^n0 = never, 1 = all but instagib/race, 2 = always" "<value>"
+setdesc "spawndelay" "time in milliseconds before players can respawn in most modes" "<millisecond>"
+setdesc "instadelay" "time in milliseconds before players can respawn in instagib mutated modes" "<milliseconds>"
+setdesc "racedelay" "time in milliseconds before players can respawn in race mode" "<milliseconds>"
+setdesc "racedelayex" "time in milliseconds before defenders can respawn in gauntlet mode" "<milliseconds>"
+setdesc "bomberdelay" "delay before spawning in bomber" "<millisecond>"
+setdesc "spawnprotect" "time in milliseconds after spawning players cannot be damaged" "<milliseconds>"
+setdesc "duelprotect" "time in milliseconds after spawning players cannot be damaged in duel/survivor matches" "<milliseconds>"
+setdesc "instaprotect" "time in milliseconds after spawning players cannot be damaged in instagib matches" "<milliseconds>"
+setdesc "instaresizeamt" "each kill adds this much size in insta-resize" "<value>"
+setdesc "regendelay" "time in milliseconds after being damage before normal regeneration resumes" "<milliseconds>"
+setdesc "regentime" "time in milliseconds for which regenerate gives health" "<milliseconds>"
+setdesc "regenhealth" "amount of health regeneration gives" "<value>"
+setdesc "regendecay" "if over maxhealth, decay this amount each regen" "<value>"
+setdesc "kamikaze" "determines the level of kamikaze events;^n0 = never, 1 = holding grenade, 2 = have grenade, 3 = always" "<value>"
+setdesc "itemsallowed" "determines if items are present in the level;^n0 = never, 1 = all but instagib, 2 = always" "<value>"
+setdesc "itemspawntime" "time in milliseconds before items (re)spawn" "<milliseconds>"
+setdesc "itemspawndelay" "time in milliseconds after map start items first spawn" "<milliseconds>"
+setdesc "itemspawnstyle" "determines the timing of item spawning at map start;^n0 = all at once, 1 = staggered, 2 = random, 3 = randomise between both" "<value>"
+setdesc "itemthreshold" "if numitems/(players*maxcarry) is less than this, spawn one of this type" "<value>"
+setdesc "timelimit" "time in minutes before round ends;^n0 = off" "<minutes>"
+setdesc "overtimeallow" "if scores are equal, go into overtime" "<value>"
+setdesc "overtimelimit" "maximum time overtime may last, 0 = forever" "<minutes>"
+setdesc "intermlimit" "time in milliseconds intermission lasts" "<milliseconds>"
+setdesc "votelimit" "time in milliseconds intermission voting lasts" "<milliseconds>"
+setdesc "duelreset" "determines if duel/survivor winner should be sent back to respawn for next round" "<value>"
+setdesc "duelclear" "determines if items are reset at the end of each round" "<value>"
+setdesc "duelcooloff" "cool off period before duel goes to next round" "<milliseconds>"
+setdesc "duelcycle" "determines if players are force-cycled after a certain number of wins (0 = off, 1 = non-team games, 2 = team games, 3 = both)" "<value>"
+setdesc "duelcycles" "maximum wins in a row before force-cycling (0 = num team/total players)" "<value>"
+setdesc "damageself" "determines if the player can damage themselves;^n0 = off, 1 = either hurt self or use damageteam rules" "<value>"
+setdesc "damageselfscale" "determines how much self-damage is dealt;^n0 = off, anything else = scale for damage" "<value>"
+setdesc "damageteam" "determines if the player can damage team members;^n0 = off, 1 = non-bots damage team, 2 = all players damage team" "<value>"
+setdesc "damageteamscale" "determines how much team damage is dealt;^n0 = off, anything else = scale for damage" "<value>"
+setdesc "teambalance" "determines the method of team balancing;^n0 = off, 1 = by number then rank, 2 = by rank then number" "<value>"
+setdesc "teampersist" "when a player leaves and rejoins the game, automatically put them on the same team they had before;^n0 = off, 1 = only attempt, 2 = forced" "<value>"
+setdesc "pointlimit" "number of points required to end the round (and win) in deathmatch modes" "<value>"
+setdesc "capturelimit" "number of captures required to end the round (and win) in ctf" "<value>"
+setdesc "capturepoints" "points added to score for capturing flag" "<value>"
+setdesc "capturepickuppoints" "points added to score for picking up enemy flag" "<value>"
+setdesc "capturethreshold" "automatically drop the flag if the flag-carrier ^"warps^" more than this distance" "<distance>"
+setdesc "capturebuffing" "buff circumstances;^n0 = off, &1 = near own flag at its base, &2 = near own loose flag, &4 = holding own flag, &8 = near teammate holding own flag, &16 = holding enemy flag, &32 = near teammate holding enemy flag" "<value>"
+setdesc "capturebuffdelay" "buffed when guarding, and for this long after" "<milliseconds>"
+setdesc "capturebuffarea" "multiply affinity radius by this much for buff" "<value>"
+setdesc "capturebuffdamage" "multiply outgoing damage by this much when buffed" "<value>"
+setdesc "capturebuffshield" "divide incoming damage by this much when buffed" "<value>"
+setdesc "captureregenbuff" "0 = off, 1 = modify regeneration when buffed" "<value>"
+setdesc "captureregendelay" "regen this often when buffed" "<milliseconds>"
+setdesc "captureregenextra" "add this to regen when buffed" "<value>"
+setdesc "defendlimit" "determines the style of dac play;^nnumber of points required to end the round (and win) in dac" "<value>"
+setdesc "defendinterval" "time in milliseconds to add one tick to the secure gauge" "<milliseconds>"
+setdesc "defendpoints" "number of points given in dac" "<value>"
+setdesc "defendoccupy" "ticks needed to overthrow or secure a control area" "<value>"
+setdesc "defendking" "ticks needed to secure in king-of-the-hill" "<value>"
+setdesc "defendhold" "ticks an area must remain secured in order to score" "<value>"
+setdesc "defendflags" "flags to init and how;^n0 = init all (neutral), 1 = init neutral and team only, 2 = init team only, 3 = init all (team + neutral + converted)" "<value>"
+setdesc "defendbuffing" "buff circumstances;^n0 = off, &1 = when guarding, &2 = when securing, &4 = even when enemies are present" "<value>"
+setdesc "defendbuffoccupy" "if defendbuffing = 4, must be occupied this much before passing" "<milliseconds>"
+setdesc "defendbuffdelay" "buffed for this long after leaving" "<milliseconds>"
+setdesc "defendbuffarea" "multiply affinity radius by this much for buff" "<value>"
+setdesc "defendbuffdamage" "multiply outgoing damage by this much when buffed" "<value>"
+setdesc "defendbuffshield" "divide incoming damage by this much when buffed" "<value>"
+setdesc "defendregenbuff" "0 = off, 1 = modify regeneration when buffed" "<value>"
+setdesc "defendregendelay" "regen this often when buffed" "<milliseconds>"
+setdesc "defendregenextra" "add this to regen when buffed" "<value>"
+setdesc "bomberlimit" "finish when score is this or more (non-hold)" "<value>"
+setdesc "bomberholdlimit" "finish when score is this or more (hold)" "<value>"
+setdesc "bomberpoints" "points added to score for a goal" "<value>"
+setdesc "bomberpenalty" "points taken from score for throwing the bomb into your own goal" "<value>"
+setdesc "bomberpickuppoints" "points added to score for picking up the bomb" "<value>"
+setdesc "bomberholdpoints" "points added to score for each interval (hold)" "<value>"
+setdesc "bomberholdpenalty" "points subtracted for having the bomb fuse run out (hold)" "<value>"
+setdesc "bomberthreshold" "automatically drop the bomb if the bomb-carrier ^"warps^" more than this distance" "<distance>"
+setdesc "bomberbuffing" "buff circumstances;^n0 = off, &1 = when near own base, &2 = when holding bomb, &4 = when holding bomb as the defenders (attack only)" "<value>"
+setdesc "bomberbuffdelay" "buffed for this long after leaving" "<milliseconds>"
+setdesc "bomberbuffarea" "multiply affinity radius by this much for buff" "<value>"
+setdesc "bomberbuffdamage" "multiply outgoing damage by this much when buffed" "<value>"
+setdesc "bomberbuffshield" "divide incoming damage by this much when buffed" "<value>"
+setdesc "bomberregenbuff" "0 = off, 1 = modify regeneration when buffed" "<value>"
+setdesc "bomberregendelay" "regen this often when buffed" "<milliseconds>"
+setdesc "bomberregenextra" "add this to regen when buffed" "<value>"
+setdesc "bombercarryspeed" "scales the movement speed of the player carrying the bomber ball by this value" "<value>"
+setdesc "bombercarrytime" "time in milliseconds a player can carry the bomber ball before it explodes" "<milliseconds>"
+setdesc "bomberholdinterval" "time in milliseconds a player needs to hold the ball in hold bomber-ball to get a point" "<milliseconds>"
+setdesc "bomberpickupdelay" "time in milliseconds a player needs to wait to be able to pick up the bomber ball again after dropping it" "<milliseconds>"
+setdesc "bomberresetdelay" "time in milliseconds before the bomber ball resets after being dropped" "<milliseconds>"
+setdesc "bomberspeed" "speed at which the bomber ball moves when thrown or dropped;^nfor reference, 125 is the speed the player moves at by default" "<value>"
+setdesc "botbalance" "determines bot balancing method;^n-1 = always use numplayers, 0 = don't balance, 1 or more = fill only with this*numteams" "<value>"
+setdesc "enemyspawntime" "determine length of time before enemies respawn" "<milliseconds>"
+setdesc "enemyspawndelay" "determine length of time after map start enemies first spawn" "<milliseconds>"
+setdesc "enemyspawnstyle" "determines enemy spawning style, 0 = all at once, 1 = staggered, 2 = random, 3 = randomise between both" "<value>"
+setdesc "enemystrength" "scale enemy health values by this much" "<value>"
+setdesc "movespeed" "baseline movement speed" "<value>"
+setdesc "movecrawl" "multiplier of speed when crawling" "<value>"
+setdesc "movepacing" "multiplier of speed when pacing" "<value>"
+setdesc "movestraight" "multiplier of speed when only moving forward" "<value>"
+setdesc "movestrafe" "multiplier of speed when strafing" "<value>"
+setdesc "moveinair" "multiplier of speed when in-air" "<value>"
+setdesc "movestepup" "multiplier of speed when stepping up" "<value>"
+setdesc "movestepdown" "multiplier of speed when stepping down" "<value>"
+setdesc "jumpspeed" "extra velocity to add when jumping" "<value>"
+setdesc "impulsespeed" "extra velocity to add when impulsing" "<value>"
+setdesc "impulselimit" "maximum impulse speed" "<value>"
+setdesc "impulseboost" "multiplier of impulse when just boosting" "<value>"
+setdesc "impulsepower" "power jump modifier" "<value>"
+setdesc "impulsedash" "multiplier of impulse when dashing" "<value>"
+setdesc "impulsejump" "multiplier of impulse when jumping" "<value>"
+setdesc "impulseparkour" "multiplier of impulse when doing other parkour" "<value>"
+setdesc "impulseparkourkick" "parkour kick modifier" "<value>"
+setdesc "impulseparkourvault" "parkour vault modifier" "<value>"
+setdesc "impulseparkournorm" "minimum parkour surface z normal" "<value>"
+setdesc "impulseallowed" "determines which impulse actions are allowed (bitwise OR);^n0 = off, 1 = dash, 2 = boost, 4 = parkour" "<sum>"
+setdesc "impulsestyle" "impulse style;^n0 = off, 1 = touch and count, 2 = count only, 3 = freestyle" "<value>"
+setdesc "impulsecount" "number of impulse actions per air transit" "<value>"
+setdesc "impulseslip" "time before floor friction kicks back in" "<milliseconds>"
+setdesc "impulseslide" "time before powerslides end" "<milliseconds>"
+setdesc "impulsedelay" "minimum time between boosts" "<milliseconds>"
+setdesc "impulsedashdelay" "minimum time between dashes/powerslides" "<milliseconds>"
+setdesc "impulsekickdelay" "minimum time between wall kicks/climbs" "<milliseconds>"
+setdesc "impulsevaultmin" "minimum percentage of height for vault" "<value>"
+setdesc "impulsevaultmax" "maximum percentage of height for vault" "<value>"
+setdesc "impulsemelee" "multiplier of impulse when using melee" "<value>"
+setdesc "impulsemeter" "impulse dash length;^n0 = unlimited, anything else = timer" "<value>"
+setdesc "impulsecost" "cost of impulse jump" "<value>"
+setdesc "impulseskate" "length of time a run along a wall can last" "<value>"
+setdesc "impulsepacing" "pacing counts toward impulse;^n0 = off, anything else = multiplier of time" "<value>"
+setdesc "impulseregen" "impulse regen multiplier" "<value>"
+setdesc "impulseregencrouch" "impulse regen modifier when crouching" "<value>"
+setdesc "impulseregenpacing" "impulse regen modifier when pacing" "<value>"
+setdesc "impulseregenmove" "impulse regen modifier when moving" "<value>"
+setdesc "impulseregeninair" "impulse regen modifier when in air" "<value>"
+setdesc "impulseregendelay" "delay before impulse regens" "<value>"
+setdesc "zoomlock" "0 = unrestricted, 1 = must be on floor, 2 = also must not be moving, 3 = also must be on flat floor, 4 = must also be crouched" "<value>"
+setdesc "zoomlocktime" "time before zoomlock kicks in when in the air" "<milliseconds>"
+setdesc "autoscores" "1 = when dead, 2 = also in spectv, 3 = and in waittv too" "<value>"
+setdesc "scoresdelay" "otherwise use respawn delay" "<milliseconds>"
+setdesc "showlaptimes" "0 = off, 1 = only player, 2 = +humans, 3 = +bots" "<value>"
+setdesc "muzzleflash" "0 = off, 1 = only other players, 2 = only thirdperson, 3 = all" "<value>"
+setdesc "muzzleflare" "0 = off, 1 = only other players, 2 = only thirdperson, 3 = all" "<value>"
+setdesc "underlaydisplay" "0 = only firstperson and alive, 1 = only when alive, 2 = always" "<value>"
+setdesc "overlaydisplay" "0 = only firstperson and alive, 1 = only when alive, 2 = always" "<value>"
+setdesc "showdamage" "1 shows just damage texture, 2 blends as well" "<value>"
+setdesc "showcrosshair" "0 = off, 1 = on, 2 = blend depending on current accuracy level" "<value>"
+setdesc "crosshairweapons" "0 = off, 1 = crosshair-specific weapons, 2 = also appy colour" "<value>"
+setdesc "cursorstyle" "0 = top left tracking, 1 = center" "<value>"
+setdesc "inventorybgskew" "skew items inside by this much" "<value>"
+setdesc "inventorybgspace" "for aligning diagonals" "<value>"
+setdesc "inventoryhealth" "0 = off, 1 = text, 2 = bar, 3 = bar + text" "<value>"
+setdesc "inventoryhealthbartop" "starts from this offset" "<value>"
+setdesc "inventoryhealthbarbottom" "ends at this offset" "<value>"
+setdesc "inventoryimpulsebartop" "starts from this offset" "<value>"
+setdesc "inventoryimpulsebarbottom" "ends at this offset" "<value>"
+setdesc "inventorystatus" "0 = off, 1 = text, 2 = icon, 3 = icon + tex" "<value>"
+setdesc "radarstyle" "0 = compass-sectional, 1 = compass-distance, 2 = screen-space, 3 = right-corner-positional" "<value>"
+setdesc "radaraspect" "0 = off, else = (for radarstyle 0/1) radar forms an ellipse" "<value>"
+setdesc "radardist" "0 = use world size" "<value>"
+setdesc "radarcornerdist" "0 = use world size" "<value>"
+setdesc "radarplayerfilter" "0 = off, 1 = non-team, 2 = team, 3 = only in duel/survivor/edit" "<value>"
+setdesc "radardamage" "0 = off, 1 = basic damage, 2 = with killer announce (+1 killer track, +2 and bots), 5 = verbose" "<value>"
+setdesc "editradarstyle" "0 = compass-sectional, 1 = compass-distance, 2 = screen-space, 3 = right-corner-positional" "<value>"
+setdesc "editradardist" "0 = use world size" "<value>"
+setdesc "animationinterpolationtime" "sets the duration in milliseconds of animation blending" "<value>"
+setdesc "addpostfx" "adds this effect to the post-processing stack" "<value>"
+setdesc "setpostfx" "replaces all the current post-processing effects with this one" "<value>"
+setdesc "clearpostfx" "clears all post-processing effects"
+setdesc "motionblurfx" "0 = off, 1 = on, 2 = override" "<value>"
+setdesc "motionblurmin" "minimum" "<value>"
+setdesc "motionblurmax" "maximum" "<value>"
+setdesc "motionbluramt" "used for override" "<value>"
+setdesc "musictype" "0 = no in-game music, 1 = map music (or random if none), 2 = always random, 3 = map music (silence if none), 4-5 = same as 1-2 but pick new tracks when done" "<value>"
+setdesc "musicedit" "0 = no editing music, 1 = map music (or random if none), 2 = always random, 3 = map music (silence if none), 4-5 = same as 1-2 but pick new tracks when done" "<value>"
+setdesc "followdead" "0 = never, 1 = in all but duel/survivor, 2 = always" "<value>"
+setdesc "specmode" "0 = float, 1 = tv" "<value>"
+setdesc "waitmode" "0 = float, 1 = tv in duel/survivor, 2 = tv always" "<value>"
+setdesc "spectvrotate" "rotate style, < 0 = absolute angle, 0 = scaled, > 0 = scaled with max angle" "<value>"
+setdesc "spectvdead" "0 = never, 1 = in all but duel/survivor, 2 = always" "<value>"
+setdesc "deathcamstyle" "0 = no follow, 1 = follow attacker, 2 = follow self" "<value>"
+setdesc "zoomdefault" "0 = last used, else defines default level" "<value>"
+setdesc "zoomscroll" "0 = stop at min/max, 1 = go to opposite end" "<value>"
+setdesc "showobituaries" "0 = off, 1 = only me, 2 = 1 + announcements, 3 = 2 + but dying bots, 4 = 3 + but bot vs bot, 5 = all" "<value>"
+setdesc "obitannounce" "0 = off, 1 = only focus, 2 = everyone" "<value>"
+setdesc "obitverbose" "0 = extremely simple, 1 = simplified per-weapon, 2 = regular messages" "<value>"
+setdesc "obitstyles" " 0 = no obituary styles, 1 = show sprees/dominations/etc" "<value>"
+setdesc "showpresence" "0 = never show join/leave, 1 = show only during game, 2 = show when connecting/disconnecting" "<value>"
+setdesc "showteamchange" "0 = never show, 1 = show only when switching between, 2 = show when entering match too" "<value>"
+setdesc "deathanim" "0 = hide player when dead, 1 = old death animation, 2 = ragdolls" "<value>"
+setdesc "deathfade" "0 = don't fade out dead players, 1 = fade them out" "<value>"
+setdesc "deathscale" "0 = don't scale out dead players, 1 = scale them out" "<value>"
+setdesc "gravity" "gravity" "<value>"
+setdesc "impulsekick" "determines the minimum angle to switch between wall kick and run" "<angle>"
+setdesc "impulsemethod" "determines which impulse method to use, 0 = none, 1 = power jump, 2 = power slide, 3 = both" "<value>"
+setdesc "impulseaction" "determines how impulse action works, 0 = off, 1 = impulse jump, 2 = impulse dash, 3 = both" "<value>"
+setdesc "impulseroll" "determines the camera angle tilt when wallrunning" "<angle>"
+setdesc "dashstyle" "0 = only with impulse, 1 = double tap" "<value>"
+setdesc "crouchstyle" "0 = press and hold, 1 = double-tap toggle, 2 = toggle" "<value>"
+setdesc "pacingstyle" "0 = press and hold, 1 = double-tap toggle, 2 = toggle, 3-5 = same, but auto engage if impulsepacing == 0" "<value>"
+setdesc "showentdir" "0 = off, 1 = only selected, 2 = always when editing, 3 = always in editmode" "<value>"
+setdesc "aboveheadblend" "determines the opacity of all abovehead* vars" "<value>"
+setdesc "aboveheaddamage" "shows amount of damage done to a player above their head" "<value>"
+setdesc "aboveheadeventsize" "determines the size of event icons above players' heads" "<value>"
+setdesc "aboveheadicons" "detemines what type of actions displays icons above player (bitwise OR for affinities and weapons);^n0 = none, 1+ = damage & kill events, 2 = affinity pickups, 4 = weapons pickups" "<value>"
+setdesc "aboveheadiconssize" "determines the size of icons handled by aboveheadicons" "<value>"
+setdesc "aboveheadnames" "toggles the display of player names above their heads" "<value>"
+setdesc "aboveheadnamessize" "determines the size of names for aboveheadnames" "<value>"
+setdesc "aboveheadstatus" "toggles display of player status above players' heads, player status is dominating, dominated, death" "<value>"
+setdesc "aboveheadstatussize" "determines the size of player status controlled by aboveheadstatus" "<value>"
+setdesc "aboveheadteam" "determines how team icons are displayed above player heads;^n0 = do not show, 1 = only teammates, 2 = all teams" "<value>"
+setdesc "bloodsize" "sets the size of blood splatter" "<value>"
+setdesc "chatconsize" "sets the amount of lines to show in the chat console, this does not include overflow lines" "<value>"
+setdesc "chatconblend" "sets the opacity of the chat console" "<value>"
+setdesc "chatconscale" "determines the scaling of the chat console text" "<value>"
+setdesc "commandscale" "determines the scaling of the command and chat input text" "<value>"
+setdesc "conblend" "sets the opacity of the console, this does not include obituaries and affinity events! (flag, bomb)" "<value>"
+setdesc "concenter" "toggles alignment of console;^n0 = console is in top left, 1 = console is centered" "<value>"
+setdesc "consize" "sets the amount of lines to show in the console, this does not include overflow lines" "<value>"
+setdesc "conscale" "determines the scaling of the console text" "<value>"
+setdesc "dominatetex" "determines what image is used for the ^"dominating!^" event icon" "<value>"
+setdesc "dominatedtex" "determines what image is used for the icon that appears over a player's head while dominating" "<value>"
+setdesc "dominatingtex" "determines what image is used for the icon that appears over a player's head while being dominated" "<value>"
+setdesc "eventblend" "sets the opacity of event icons, (first blood, 2x, 3x, headshot, etc.)" "<value>"
+setdesc "eventiconfade" "sets how long event icons should stay on screen" "<value>"
+setdesc "eventoffset" "sets the offset for the event icons in your hud, negative values move it downward, positive values move it upward" "<value>"
+setdesc "eventscale" "sets the scale of event icons that appear in your hud" "<value>"
+setdesc "fullconblend" "sets the opacity of the ^"full^" console, this is when the console is open, and includes obituaries and affinity events. (flag, bomb) excluding those two elements, conblend takes precedence over this variable, yet only affects everything else" "<value>"
+setdesc "healthbgtex" "determines what image is used for the background of the healthbar" "<value>"
+setdesc "hudblend" "sets the opacity of the entire hud as a whole, this includes both consoles, mouse pointer, and inventory" "<value>"
+setdesc "hudsize" "sets the size of the entire hud as a whole, this includes both consoles, menus, inventory, etc" "<value>"
+setdesc "inventoryblend" "sets the opacity of the inventory" "<value>"
+setdesc "inventorycolour" "toggles the colour of the weapon icons in the weapon bar;^n0 = no colour at all, 1 = only weapon icon is coloured, 2 = both weapon icon and slot number are coloured" "<value>"
+setdesc "inventoryglow" "sets the size of the ^"glow^" in the inventory, the ^"glow^" is the splatter/explosion image behind inventory items" "<value>"
+setdesc "inventorybgblend" "sets the opacity of the inventory background" "<value>"
+setdesc "inventoryimpulse" "sets how the impulse meter is displayed;^n0 = do not show at all, 1 = show textual percentage, 2 = show a bar meter" "<value>"
+setdesc "impulsetex" "determines what image is used for the impulse meter" "<value>"
+setdesc "inventoryleft" "sets the size of the inventory on the left" "<value>"
+setdesc "inventoryright" "sets the size of the inventory on the right" "<value>"
+setdesc "inventorytone" "sets the tone of various inventory elements;^n0 = no colouring at all, 1 = uses your profile colour, 2 = uses team colour for team games and grey when neutral, 3 = uses team colour for team games and your profile colour when neutral, 4 = uses profile colour for team games and grey when neutral" "<value>"
+setdesc "inventoryweapids" "sets how to display the slot number of the weapons in the weapon bar;^n0 = do not show, 1 = show for active weapon only, 2 = show for all weapons" "<value>"
+setdesc "noticescale" "sets the size of the notices" "<value>"
+setdesc "zoomsensitivity" "determines aiming/look sensitivity while zoomed in with the rifle" "<value>"
+setdesc "zoomtex" "determines what image is used for the scope on the rifle" "<value>"
+setdesc "actorscale" "defines the size of all actors (players, bots, grunts, etc.)" "<value>"
+setdesc "adminpass" "sets server admin password, if no password given, it will return the current password" "<value>"
+setdesc "aiforcegun" "forces bots' weapon preference, if -1, bots have randomized weapon preferences" "<value>"
+setdesc "aipassive" "sets ai to be passive, ie, non-hostile;^n0 = hostile, 1 = hostile to other AI, 2 = hostile to all" "<value>"
+setdesc "captureresetdelay" "time in milliseconds before a dropped flag automatically resets" "<milliseconds>"
+setdesc "maxhealth" "spawnhealth * maxhealth defines the maximum amount of health that can be reached (e.g. standing next to a friendly goal)" "<value>"
+setdesc "maxhealthvampire" "spawnhealth * maxhealthvampire defines the maximum amount of health that can be reached by damaging other players in vampire" "<value>"
+setdesc "minresizescale" "defines the smallest scaled size a player can become in a match with the resize mutator enabled" "<value>"
+setdesc "maxresizescale" "defines the largest scaled size a player can become in a match with the resize mutator enabled" "<value>"
+setdesc "maxcarry" "maximum number of weapons a player can carry, plus pistol and grenades" "<value>"
+setdesc "spawnhealth" "defines the amount of health you spawn with, and the maximum amount of health to which you will regenerate when damaged, assigning this variable a value of 0 or 1 will cause you to die in one hit" "<value>"
+setdesc "zoomtime" "determines how long it takes for the rifle scope to zoom in and out, the rifle uses the primary fire mode unless the scope is fully zoomed in" "<value>"
+setdesc "editmat" "modifies the material properties for selected cubes;^n<type> is the material to be applied {water, clip, ladder, etc.},^n<filter> limits the command to only affect cubes whose material match this, if given as empty string ^"^" it matches all,^n<style> limits the command to only affect cubes whose geometry match this {0 = normal, 1 = non-empty, 2 = empty, 3 = not entirely solid, 4 = entirely solid},^nexample: editmat air water 2 would set all empty cubes with water with [...]
+setdesc "showmat" "toggles the visibility of material volumes" "<value>"
+setdesc "air" "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+loop i 4 [
+    setdesc [water@(? $i (+ $i 1))] "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+    setdesc [glass@(? $i (+ $i 1))] "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+    setdesc [lava@(? $i (+ $i 1))] "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+]
+setdesc "clip" "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+setdesc "noclip" "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+setdesc "death" "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+setdesc "aiclip" "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+setdesc "ladder" "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+setdesc "alpha" "modifies the material properties for the selected cubes;^n<filter> limits the command to only affect cubes whose material match this {water, clip, ladder, etc.}" "[<filter>]"
+setdesc "checkmaps" "reports if clients are using a modified map"
+setdesc "serverclients" "maximum number of allowed clients" "<value>"
+setdesc "serverdesc" "server description" "<text>"
+setdesc "servermotd" "server message of the day" "<text>"
+setdesc "serveropen" "determines server openness for public use;^n0 = allow ^"setpriv 1^" and locked/private, 1 = allow ^"setpriv 1^" but no privileged mode, no locked/private, 2 = allow ^"setpriv 1^" but disallows private privileged mode (for public coop-editing), 3 = privilege only by moderator or above" "<value>"
+setdesc "autoadmin" "determines if authorities claim status by default" "<value>"
+setdesc "airefreshdelay" "delay imposed before the AI manager reorganises their setup" "<milliseconds>"
+setdesc "modelockfilter" "determines the modes which are allowed to be used as dictated by modelock,^nconvenient to set using a sum of $modebit* vars (available: editing, deathmatch, capture, defend, bomber, race),^nexample: (+ $modebitediting $modebitdeathmatch)" "<sum>"
+setdesc "mutslockfilter" "determines the mutators which are allowed to be used as dictated by modelock,^nconvenient to set using a sum of $mutsbit* vars (available: multi, ffa, coop, instagib, medieval, kaboom, duel, survivor, classic, onslaught, freestyle, vampire, hard, resize, gsp1 = first mode variation {ctf-quick, dac-quick, bb-hold, tt-marathon}, gsp2 = second mode variation {ctf-defend, dac-king, bb-basket, tt-endurance}, gsp3 = third mode variation {ctf-protect, bb-attack, tt-gau [...]
+setdesc "mutslockfilter" "determines the mutators which must always be on,^nconvenient to set using a sum of $mutsbit* vars (available: multi, ffa, coop, instagib, medieval, kaboom, duel, survivor, classic, onslaught, freestyle, vampire, hard, resize, gsp1 = first mode variation {ctf-quick, dac-quick, bb-hold, tt-marathon}, gsp2 = second mode variation {ctf-defend, dac-king, bb-basket, tt-endurance}, gsp3 = third mode variation {ctf-protect, bb-attack, tt-gauntlet}),^nexample: (+ $mutsbi [...]
+setdesc "gamespeed" "percentage of default gameplay speed" "<value>"
+setdesc "gamepaused" "pauses the game, automatically unset by server" "<value>"
+setdesc "defaultmap" "default map, ^"^" = random" "<map>"
+setdesc "defaultmode" "default game mode;^n1 = editing, 2 = deathmatch, 3 = ctf, 4 = dac, 5 = bomber, 6 = race" "<value>"
+setdesc "defaultmuts" "default mutators, convenient to set using a sum of $mutsbit* vars (available: multi, ffa, coop, instagib, medieval, kaboom, duel, survivor, classic, onslaught, freestyle, vampire, hard, resize, gsp1 = first mode variation {ctf-quick, dac-quick, bb-hold, tt-marathon}, gsp2 = second mode variation {ctf-defend, dac-king, bb-basket, tt-endurance}, gsp3 = third mode variation {ctf-protect, bb-attack, tt-gauntlet})" "<sum>"
+setdesc "rotatemaps" "determines behaviour of map rotation;^n0 = off, 1 = sequence, 2 = random" "<value>"
+setdesc "rotatemapsfilter" "0 = off, 1 = filter based on mutators, 2 = also filter based on players" "<value>"
+setdesc "rotatemode" "determines if modes rotate when the server selects the next map" "<value>"
+setdesc "allowmaps" "determines which maps are allowed to be chosen without elevated privileges" "<list>"
+setdesc "mainmaps" "deathmatch maps" "<list>"
+setdesc "capturemaps" "capture-the-flag maps" "<list>"
+setdesc "defendmaps" "defend-the-flag maps" "<list>"
+setdesc "bombermaps" "bomber-ball maps" "<list>"
+setdesc "holdmaps" "hold bomber-ball maps" "<list>"
+setdesc "racemaps" "race maps" "<list>"
+setdesc "duelmaps" "duel map filter (extra filter on top of mode filter)" "<list>"
+setdesc "spawnweapon" "weapon players spawn with, defaults to pistol (1)" "<value>"
+setdesc "instaweapon" "weapon players spawn with in instagib, defaults to rifle (8)" "<value>"
+setdesc "raceweapon" "weapon players spawn with in race, defaults to melee only (0)" "<value>"
+setdesc "burntime" "time in milliseconds fire burns for, try to allow an extra 500ms breathing room for sync" "<milliseconds>"
+setdesc "burndelay" "time in milliseconds for which fire burning deals damage" "<milliseconds>"
+setdesc "burndamage" "amount of damage fire burning deals" "<value>"
+setdesc "bleedtime" "time in milliseconds bleeding lasts for, try to allow an extra 500ms breathing room for sync" "<milliseconds>"
+setdesc "bleeddelay" "time in milliseconds for which bleeding deals damage" "<value>"
+setdesc "bleeddamage" "amount of damage bleeding deals" "<value>"
+setdesc "bleedtime" "time in milliseconds shocking lasts for, try to allow an extra 500ms breathing room for sync" "<milliseconds>"
+setdesc "bleeddelay" "time in milliseconds for which shocking deals damage" "<value>"
+setdesc "bleeddamage" "amount of damage shocking deals" "<value>"
+setdesc "botskillmin" "minimum randomly assigned AI skill level" "<value>"
+setdesc "botskillmax" "maximum randomly assigned AI skill level" "<value>"
+setdesc "coopskillmin" "minimum randomly assigned AI skill level in coop" "<value>"
+setdesc "coopskillmax" "maximum randomly assigned AI skill level in coop" "<value>"
+setdesc "enemyskillmin" "minimum randomly assigned skill level for neutral actors in onslaught" "<value>"
+setdesc "enemyskillmax" "maximum randomly assigned skill level for neutral actors in onslaught" "<value>"
+setdesc "botscale" "scale the 'numplayers' world variable which determines bot auto population" "<value>"
+setdesc "botlimit" "maximum number of bots allowed, regardless of any other variable/setting" "<value>"
+setdesc "enemybalance" "multiply number of enemy spawns by this much" "<value>"
+setdesc "gravityscale" "multiply gravity by this much" "<value>"
+setdesc "liquidspeedscale" "multiply liquidspeed by this much" "<value>"
+setdesc "liquidcoastscale" "multiply liquidcoast by this much" "<value>"
+setdesc "floorcoastscale" "multiply floorcoast by this much" "<value>"
+setdesc "aircoastscale" "multiply aircoast by this much" "<value>"
+setdesc "slidecoastscale" "multiply slidecoast by this much" "<value>"
+setdesc "stillspread" "multiply projectile spread by this much when standing still" "<value>"
+setdesc "movespread" "multiply projectile spread by this much when moving" "<value>"
+setdesc "inairspread" "multiply projectile spread by this much when jumping/in-air" "<value>"
+setdesc "impulsespread" "multiply projectile spread by this much when impulsing/running" "<value>"
+setdesc "radialscale" "multiply explosion radius by this amount" "<value>"
+setdesc "radiallimited" "multiply explosion radius by this amount in limited situations (eg. instagib)" "<value>"
+setdesc "damagescale" "scale damage by this amount" "<value>"
+setdesc "hitpushscale" "scale hit pushes by this amount" "<value>"
+setdesc "hitstunscale" "multiply ^"stun target on hit^" by this amount" "<value>"
+setdesc "deadpushscale" "scale hit pushes by this amount when it results in a frag" "<value>"
+setdesc "wavepushscale" "scale of the hitpush force used in a wavepush" "<value>"
+setdesc "kickpushscale" "multiply kick pushes from weapons by this much" "<value>"
+setdesc "kickpushcrouch" "multiply kick pushes from weapons by this much when crouching" "<value>"
+setdesc "kickpushsway" "multiply kick push effect on hud gun sway by this much" "<value>"
+setdesc "kickpushzoom" "multiply kick pushes from weapons by this much when zoomed" "<value>"
+setdesc "multikilldelay" "time in milliseconds multiple kills in a row must occur" "<value>"
+setdesc "spreecount" "number of consecutive frags for each spree level" "<value>"
+setdesc "dominatecount" "number of frags on a single player without revenge before it is considered domination" "<value>"
+setdesc "firstpersonfov" "determines the size of the field of view in first person mode" "<angle>"
+setdesc "fov" "determines the size of the field of view in first or third person mode, depending on the current camera mode" "<angle>"
+setdesc "getclientcolour" "gets the colour of a player" "<clientnum>"
+setdesc "getclientmodel" "gets the model number of a player" "<clientnum>"
+setdesc "getmodelname" "gets the model name from a model number, the second argument determines which bits of the path to the model file is printed, defaults to 2;^n0 = path to model directory, 1 = path to model/hwep directory, name of model directory" "<modelnumber> <value>"
+setdesc "newent" "create a new entity (available: light, mapmodel, playerstart, envmap, particles, sound, lightfx, sunlight, weapon, teleport, actor, trigger, pusher, affinity, checkpoint, dummy1, dummy2)" "<type> <properties...>"
+setdesc "entset" "set type and properties of the currently selected entity" "<type> <properties...>"
+setdesc "skylight" "set colour and brightness of a uniform downwards-facing lightsource spanning the whole sky" "<value>"
+setdesc "ambient" "set colour and brightness of a uniform ambient light throughout the whole map" "<value>"
+setdesc "entfind" "selects all entities of a specific type and optionally with specific properties, using ^"1^" as type selects all entities" "<type> <properties...>"
+setdesc "entfindinsel" "selects all entities, within the selected area, of a specific type and optionally with specific properties, using ^"1^" as type selects all entities" "<type> <properties...>"
+setdesc "entcancel" "deselects all entities while leaving area selected"
+setdesc "edit" "starts editing on a new map file named ^"mapname.mpz^" or loads mapname.mpz if it already exists" "<map>"
+setdesc "savemap" "saves the current map as ^"mapname.mpz^" in the homedir" "<map>"
+setdesc "maptitle" "sets the title message of the map" "<text>"
+setdesc "mapauthor" "sets the author message of the map" "<text>"
+setdesc "mapenlarge" "enlarges the map to twice the size" "<text>"
+setdesc "shrinkmap" "shrinks the map size to smallest cubed area, only empty space outside of the map will be removed"
+setdesc "sendmap" "manually sends map to the server"
+setdesc "getmap" "manually retrieves map from the server"
+setdesc "gettex" "picks up the texture from the face currently selected, note that allfaces must be turned off"
+setdesc "vscale" "replaces selected textures with scaled versions, minimum is 0.125, maximum is 8" "<value>"
+setdesc "vrotate" "replaces selected textures with clockwise rotated or flipped versions;^n0 = default, 1-3 = rotate 90*this degrees, 4 = flipped left-to-right, 5 = flipped top-to-bottom" "<value>"
+setdesc "vcolour" "replaces selected textures with colour-tinted versions,^ne.g. 1 0.2 0.2 will create red versions, default is 1 1 1" "<red> <green> <blue>"
+setdesc "vpalette" "applies the colour specified by <set> and <id> from the builtin palette;^nset 0 = pulse colours {0 = turn off all palette effects, 1 = dark fire pulse colours, 2 = bright fire pulse colours, 3 = disco pulse colours (bomber-ball)},^nset 1 = team colours {0-3 = only enforced in teamplay games 4-7 = enforced at all times},^nset 2 = weapon colours {0-9 = only enforced when that weapon is able to spawn in the current game configuration 10-19 = enforced at all times}" "<set> <id>"
+setdesc "vreset" "resets a vtexture to its original settings"
+setdesc "replace" "repeats the last texture change across the entire map, note that allfaces must be turned off"
+setdesc "vdelta" "combines with texture v*-commands to adjust rather than overwrite the setting;^nexample: vdelta [vcolour 1 .5 .5]^nexample: vdelta [voffset 0 +16]" "<command> <settings...>"
+setdesc "vshaderparam" "set shader parameters of textures;^nindependent of vdelta, only one shader parameter may be overridden per texture, previous modifications are lost^nspecscale <red> <green> <blue> = specularity colour and brightness of a texture {example: vshaderparam specscale 0.2 0.2 1.1 creates a blue shine, specscale 0.2 0.2 0.2 resets},^nparallaxscale <low> <high> = adjusts the 3d heightmap effect of a texture,^nglowcolor <red> <green> <blue> = adjusts the 3d glow colour of a [...]
+setdesc "vlayer" "Sets the bottom texture layer for all textures in the current selection;^n<texture_id> is the number of the texture as seen in the texture browser" "<texture_id>"
+setdesc "texturecull" "removes all textures not used in the map from the map config file"
+setdesc "setgrass" "designates the currently selected texture as a ^"grass texture^" throughout the whole map, and uses <file> as the vertical grass image, e.g textures/grass.png" "<file>"
+setdesc "grasscolour" "sets the colour of grass" "<red> <green> <blue>"
+setdesc "grassheight" "sets the height of grass" "<value>"
+setdesc "grassblend" "sets the opacity of grass" "<value>"
+setdesc "grassdist" "sets the distance at which grass becomes visible" "<value>"
+setdesc "calclight" "create and apply lightmap for all textures"
+setdesc "lightprecision" "set precision of lightmap, higher precision reduces quality and speeds up calclight"
+setdesc "fullbright" "hides lightmap for all textures if enabled"
+setdesc "selectbrush" "select a different brush with relative index to the current one;^n-1 = previous brush, 1 = next brush" "<index>"
+setdesc "skybox" "loads a set of 6 images associated by name from the specified basepath;^ne.g. ^"skyboxes/gradient^" will load gradient_{up,dn,lf,rt,bk,ft}.{png,jpg}" "<basepath>"
+setdesc "skycolour" "changes the colour of the skybox" "<red> <green> <blue>"
+setdesc "yawsky" "determines the placement of the skybox (move the sun's placement)"
+setdesc "spinsky" "controls the speed at which the skybox turns"
+setdesc "cloudlayer" "loads a specified image to tile as a cloud layer" "<file>"
+setdesc "cloudheight" "set cloud placement;^n-1 = bottom of the skybox, 1 = top of the skybox"
+setdesc "cloudscale" "controls the scale of the cloud layer" "<value>"
+setdesc "cloudscrollx" "controls the movement speed of the cloud layer on the x axis" "<value>"
+setdesc "cloudscrolly" "controls the movement speed of the cloud layer on the y axis" "<value>"
+setdesc "cloudlayercolour" "controls the colour of the cloud layer" "<red> <green> <blue>"
+setdesc "cloudfade" "controls how the cloud layer fades around the edges" "<value>"
+setdesc "dropwaypoints" "determines if waypoints are dropped by players in game, defaults to 1;^n0 = no waypoints are dropped, 1 = limited number of waypoints are dropped" "<value>"
+setdesc "showwaypoints" "toggles the visibility of the path that connects waypoints, waypoints are always visible if they are being dropped" "<value>"
+setdesc "savewaypoints" "saves a .wpt file containing the current maps waypoints, if no argument is given it uses the current map's name" "[<name>]"
+setdesc "loadwaypoints" "loads a .wpt file containing waypoints, if no argument is given it loads a waypoint file matching the current map's name" "[<name>]"
+setdesc "delselwaypoints" "delete the waypoints in the currently selected area"
+setdesc "followaiming" "determines aim direction when following a player^n0 = don't aim, &1 = aim in thirdperson, &2 = aim in first person" "<value>"
+setdesc "spectvfirstperson" "determines aim direction of spectv;^n0 = aim in direction followed player is facing, 1 = aim in direction determined by spectv when dead, 2 = always aim in direction" "<value>"
+setdesc "spectvthirdperson" "determines aim direction of spectv;^n0 = aim in direction followed player is facing, 1 = aim in direction determined by spectv when dead, 2 = always aim in direction" "<value>"
+setdesc "previousmaps" "list of previously played maps"
+setdesc "maphistory" "remember this many maps that can't be voted again if votelock is set" "<value>"
+setdesc "intermmode" "determines camera style in intermission;^n0 = float, 1 = tv" "<value>"
+setdesc "coopbalance" "in coop, bot team gets this many players for each human"
+setdesc "coopmultibalance" "in multi-coop, each bot team gets this many players for each human"
+setdesc "setpersist" "toggles an alias as persistent, that is, determines if it will be added to config.cfg;^n<bool>: 0 = not persistent, 1 = persistent" "<alias> <bool>"
+setdesc "local" "declares a set of aliases as local to the alias (function) it was declared in"
+setdesc "newgui" "creates a new menu;^n<name> refers to the menu's variable name,^n<content> refers to the actual content of the gui itself,^n[<initscript>] is optional, and is run before the menu is created (used with ^"if (= $guipasses 0)^" to initialize variables for the menu)" "<name> <content> [<initscript>]"
+setdesc "guiheader" "sets the header title of the menu, replacing the first argument of newgui in the title bar." "<value>"
+setdesc "guistayopen" "Usually, clicking a button or image to trigger an action will close the menu.^nThis rule does not apply to buttons or images in a guistayopen block." "<content>"
+setdesc "guitab" "creates a new tab for the current menu. Note: The first tab of each menu is already impied by newgui." "<name>"
+setdesc "showgui" "shows a menu created via newgui;^n<name> refers to the menu's variable name,^n[<tab>] is the index for the tab to show (and not the tab's title)" "<name> [<tab>]"
+setdesc "guicount" "Returns the number of menus in the current gui stack." ""
+setdesc "cleargui" "Closes all open menus; If an integer argument is given, it goes back <value> steps in the stack of open menus." "[<value>]"
+setdesc "guitext" "creates a text element;^n<text> can be a raw string or a variable,^n[<icon>] is the path to an image, example: ^"textures/bomb^",^n[<colour>] is a hexadecimal colour code, example: 0xFF0000, ^n[<iconcolour>] acts on the <icon> just like <colour> acts on the <text>,^n[<wrap>] specifies a width limit, which allows the text to expand over several lines" "<text> [<icon>] [<colour>]  [<iconcolour>] [<wrap>]"
+setdesc "guibutton" "creates a text button;^n<name> refers to the button's variable name,^n<action> defines what the button does,^n[<alt-act>] defines what the button does when right-clicked,^n[<icon>] is the path to an image, example: ^"textures/bomb^",^n[<colour>] is a hexadecimal colour code, example: 0xFF0000" "<name> <action> [<alt-act>] [<icon>] [<colour>]"
+setdesc "guiimage" "creates a clickable image;^n<path> is the path to an image, example: ^"textures/bomb^",^n[<action>] defines what clicking the image does,^n[<scale>] defines the scale of the image,^n[<overlaid>] whether or not the image is overlaid with guioverlaytex (true/false),^n[<alt-path>] alternate image to use if <path> cannot be loaded,^n[<alt-act>] defines what the image does when right clicked,^n[<colour>] is a hexadecimal colour code - useful to colorize icons, e.g. for wea [...]
+setdesc "guicheckbox" "creates a checkbox;^n<name> a text string shown to the right of the checkbox,^n<variable> refers to the alias/var that the checkbox controls,^n[<on>] is the value given to <variable> when the checkbox is on, and vice-versa for [<off>], if these are not specified, 1/0 is assumed,^n[<onchange>] is the action taken whenever the checkbox is toggled,^n[<colour>] is a hexadecimal colour code, example: 0xFF0000" "<name> <var> [<on>] [<off>] [<onchange>] [<colour>]"
+setdesc "guiradio" "creates a radio button;^n<name> a text string shown to the right of the radio button,^n<var> refers to the alias/var that the radio button controls,^n<value> is the value given to <var> when selected,^n[<onchange>] is the action taken whenever the button is toggled,^n[<colour>] is a hexadecimal colour code, example: 0xFF0000" "<name> <var> <value> [<onchange>] [<colour>]"
+setdesc "guilist" "adds <content> to a list. Guilists can be nested to arrange gui elements horizontally and vertically.^nThis also allows to structure the layout in order to use guibars, guisliders and coloured guibackgrounds." "<content>"
+setdesc "guibody" "allows a list of gui elements to act like a single button;^n<content> is a block of code inside an implied guilist,^n<action> is the action taken when clicking any element of <content>^n[<alt-act>] allows to define a different right-click action,^n[<onhover>] is executed when the mouse moves over the boundary of the guibody - useful for guitooltip" "<content> <action> [<alt-act>] [<onhover>]"
+setdesc "guispring" "adds weight to a menu for pushing elements more/less to one side;^nexample: guilist [ guispring ; guitext ^"centered^"; guispring ]" "[<value>]"
+setdesc "guistrut" "adds spacing to a menu;^nif [<bool>] is 1, it wraps the guistrut in a guilist (adds spacing in the alternate direction),^n^"guistrut 5 1^" is the same as ^"guilist [ guistrut 5 ]^"" "<amount> [<bool>]"
+setdesc "guibar" "draws either a vertical or a horizontal line, depending on the nesting of guilist"
+setdesc "guifont" "sets the text font to be used in a block;^n<font> is a keyword, e.g. emphasis or small^n<block> is a block of gui elements for which <font> is used^nexample: newgui font-preview [loopfiles i config/fonts cfg [guifont $i [guitext $i]]]^nnote: This will preview all of the available <font> options." "<font> <block>"
+setdesc "guibackground" "creates a colored background for the current guilist;^n<colour> is in hexadecimal, example: 0xFF0000,^n[<blend>] is the opacity of the colour, ranges from 0 (invisible) to 1 (opaque),^n[<bordercolour>] is the colour of the border,^n[<borderblend>] is the opacity of the border,^n[<border>] determines if the border is drawn (0 or 1),^n[<levels>] specifies how many guilist levels to go back" "<colour> [<blend>] [<bordercolour>] [<borderblend>] [<border>]  [<levels>]"
+setdesc "guifill" "creates a coloured background for the current guilist, but without the skin settings of guibackground.^nThis implies sharp corners, no borders and a blend value of 0.5;^n<colour> is in hexadecimal, example: 0xFF0000,^n[<levels>] specifies how many guilist levels to go back" "<colour> [<levels>]"
+setdesc "guifield" "creates a text field;^n<var> is the variable the guifield controls,^n<maxlength> is the width in characters, negative values allow the field to expand downward,^n[<onchange>] is the action taken when the value changes,^n[<colour>] is a hex colour, e.g. 0xFF0000, ^n[<focus>] (1) will keep the input field in focus,^n[<parent>] allows to pass the input,^n[<height>] is the number of lines to show with a scroll bar,^n[<promt>] is a text shown when <var> is empty,^n[<immedi [...]
+setdesc "guieditor" "creates a guifield with content from a text file;^n<name> is a filename,^n[<maxlength>] is the width of the field, negative values imply text wrap,^n[<height>] is the number of lines shown with a scroll bar,^n[<mode>] (4) implies read-only,^n[<colour>] is a hex code for the entire editor,^n[<focus>] (1) keeps the editor field in focus^n[<parent>] allows to pass input,^n[<str>] allows to do a textinit using <name>,^n[<prompt>] is shown when the field is empty." "<name [...]
+setdesc "guikeyfield" "creates a key field (each key being a separate element);^n<var> refers to the alias/variable the guikeyfield controls,^n<maxlength> defines the maximum number of characters/length of the field, negative values allow the field to expand downward as needed,^n[<onchangge>] is the action taken when the guikeyfield value changes,^n[<colour>] is a hexadecimal colour code example: 0xFF0000" "<var> <maxlength> [<onchange>] [<colour>]"
+setdesc "guibitfield" "creates a checkbox for the bitwise and of <var> and <bit>;^n<name> a text string shown to the right of the checkbox,^n<variable> refers to an alias/var that holds bitwise information,^n<bit> is the bit of <var> that the checkbox controls, given as a power of 2,^n[<onchange>] is the action taken whenever the checkbox is toggled,^n[<colour>] is a hexadecimal colour code, example: 0xFF0000.^nexample: guibitfield ^"allow votes for race games^" modelockfilter $modebitra [...]
+setdesc "guislider" "creates a slider, depending on the use of guilist;^n<var> is an integer that the slider controls,^n<min> and <max> give the valid range for <var>,^n[<onchange>] is the action taken when the slider is moved,^n[<reverse>] (1) flips the slider,^n[<scroll>] (1) enables scrolling also on the parent guilist,^n[<colour>] is a hex colour (hover text),^n[<style>] (1) draws a bar instead of a point,^n[<slidercolour>] is a hex colour (bar/point).^nexample: guislider raceweapon  [...]
+setdesc "guilistslider" "creates a slider, depending on the use of guilist;^n<var> is an integer that the slider controls,^n<list> holds the values for <var>,^n[<onchange>] is the action taken when the slider is moved,^n[<reverse>] (1) flips the slider,^n[<scroll>] (1) enables scrolling also on the parent guilist,^n[<colour>] is a hex colour (hover text),^n[<style>] (1) draws a bar instead of a point,^n[<slidercolour>] is a hex colour (bar/point).^nexample: guilistslider raceweapon [0 1  [...]
+setdesc "guinameslider" "creates a slider, depending on the use of guilist;^n<var> is an integer that the slider controls,^n<names> is a list of hover texts shown for each value,^n<list> is a list of valid values for <var>,^n[<onchange>] is the action taken when the slider is moved,^n[<reverse>] (1) flips the slider,^n[<scroll>] (1) enables scrolling also on the parent guilist,^n[<colour>] is a hex colour (hover text),^n[<style>] (1) draws a bar instead of a point,^n[<slidercolour>] is a [...]
+setdesc "guimodelpreview" "shows a preview of a mapmodel^n<model> is a subdirectory of data/models^n[<animspec>] is the name of an associated animation,^n[<action>] allows to use the preview as a button,^n[<scale>] is the preview image size, ^n[<overlaid>] (1) draws a frame (guioverlaytex),^n[<size>] is a scaling or zoom factor,^n[<blend>] is for opacity,^n[<alt-act>] defines an action for right-clicking"  "<model> [<animspec>] [<action>] [<scale>] [<overlaid>] [<size>] [<blend>] [<altact>]"
+setdesc "guiplayerpreview" "shows a preview of a player model;^n[<model>] is 0 (male) or 1 (female),^n[<colour>] is the player colour,^n[<team>] is the team index (0-4),^n[<weap>] is a weapon id (0-11),^n[<vanity>] is a list of vanity item names,^n[<action>] allows to use the preview as a button,^n[<scale>] is the preview image size, ^n[<overlaid>] (1) draws a frame (guioverlaytex),^n[<size>] is a scaling or zoom factor,^n[<blend>] is for opacity,^n[<alt-act>] defines an action for right [...]
+setdesc "guiprogress" "draws an animated progress icon, as shown when loading a map;^n<value> is converted to a percentage and used as an overlay text,^n[<size>] is the image size for the progress icon" "<value> [<size>]"
+setdesc "guislice" "creates a slice (pie-chart) of an image with overlaid text;^n[<path>] is the path to an image, example: ^"textures/bomb^" ,^n[<action>] is executed when clicking the image,^n[<scale>] is the size of the image,^n[<start>] and [<end>] give the range of the slice. For a full rotation, use 0 1,^n[<text>] is an overlay text,^n[<altpath>] is used when <path> cannot be loaded,^n[<altact>] allows to specify a different right-click action" "[<path>] [<action>] [<scale>] [<star [...]
+setdesc "guinohitfx" "Hover effects are disabled for all buttons or images in a guinohitfx block.^nThis is useful for images that do not act as buttons." "<content>"
+setdesc "guitooltip" "creates a text box attached to the mouse pointer;^n<text> is the hover text,^n[<width>] adjusts the width of the box,^nexample: if (=s $guirolloveraction ^"disconnect^") [guitooltip ^"leave the current game (server)^"]" "<text> [<width>]"
+setdesc "guirollovername" "Returns the text label or image path of the gui element at the current mouse position." ""
+setdesc "guirolloveraction" "Returns the <action> of a button or image or the <var> of a checkbox or radio at the current mouse position." ""
+setdesc "guirollovertype" "Returns the type of the gui element at the current mouse position, e.g. text, image or checkbox." ""
+setdesc "inputcommand" "prepares input to the command line;^n<init> is the input string,^n[<action>] is an optional command to execute after inputcommand,^n[<icon>] is an optional icon to use next to the input (example: ^"textures/bomb^"),^n[<colour>] is an optional color of <icon>,^n[<flags>] are optional command flags to pass {c = CF_COMPLETE, x = CF_EXECUTE, s = CF_COMPLETE|CF_EXECUTE (default)}" "<init> [<action>] [<icon>] [<colour>] [<flags>]"
+setdesc "alias" "defines a new alias (variable or command block);^n example: alias pi 3.14159265359^nis equivalent to: pi = 3.14159265359" "<name> <contents>"
+setdesc "getalias" "looks up the contents of an alias;^nexample: echo (getalias dm)^nis equivalent to: echo $dm" "<name>"
+setdesc "setcomplete" "Enables (or disables) tab-completion of a command;^n<name> is the name of a command or alias,^n[<bool>] (1) enables completion of <name> (the dafault is 0, disabled)." "<name> [<bool>]"
+setdesc "listcomplete" "Enables tab-completion of a command's first argument;^n<name> is a command or alias,^n<list> is a list of valid arguments to be completed" "<name> <list>"
+setdesc "complete" "Enables tab-completion for files as argument of a command;^n<name> is a command or alias,^n[<path>] is a directory to look for the files, note the <path> is not contained in the auto-completed argument,^n<extension> allows to pick only a certain file type, and will omit the extension when completing file names.^nexample: complete guifont config/fonts cfg" "<name> [<path>] [<extension>]"
+setdesc "bind" "binds an action to a key. WARNING: This overrides existing binds for that key;^n<key> is a key, for example KP1 or RETURN,^n<action> is a command to be executed when the key is pressed,^nexample: bind O [echo O was pressed ... ; onrelease [echo ... and released]]" "<key> <action>"
+setdesc "editbind" "binds an edit mode action to a key. WARNING: This overrides existing edit binds for that key;^n<key> is a key, for example KP1 or RETURN,^n<action> is a command to be executed when the key is pressed in edit mode." "<key> <action>"
+setdesc "specbind" "binds a spectator mode action to a key. WARNING: This overrides existing spectator binds for that key;^n<key> is a key, for example KP1 or RETURN,^n<action> is a command to be executed when the key is pressed in spectator mode." "<key> <action>"
+setdesc "getbind" "returns the command associated with a given key bind;^n<key> is a key, for example KP1 or RETURN." "<key>"
+setdesc "geteditbind" "returns the command associated with a given key bind for edit mode;^n<key> is a key, for example KP1 or RETURN." "<key>"
+setdesc "getspecbind" "returns the command associated with a given key bind for spectator mode;^n<key> is a key, for example KP1 or RETURN." "<key>"
+setdesc "searchbinds" "finds key binds for a given action;^nthis is used by the dobindsearch alias" "<action> [<formatting options>]"
+setdesc "searcheditbinds" "finds edit key binds for a given action;^nthis is used by the dobindsearch alias" "<action> [<formatting options>]"
+setdesc "searchspecbinds" "finds spectator key binds for a given action;^nthis is used by the dobindsearch alias" "<action> [<formatting options>]"
+setdesc "dobindsearch" "prints out a nicely formatted string with the key binds for a given action;^n<action> is the action to search for,^n[<mode>] can be edit or spec to search for the corresponding mode specific binds." "<key> [<mode>]"
+setdesc "loop" "executes <body> with <var> incremented from 0 to <count>-1;^nexample: loop i 10 [ echo $i ]" "<var> <count> <body>"
+setdesc "loopconcat" "returns a string with the result of the <body> concatenated with <var> incremented from 0 to <count>-1;^nexample: loopconcat i 10 [ result $i ]" "<var> <count> <body>"
+setdesc "loopconcatword" "returns a string with the result of the <body> concatword-ed with <var> incremented from 0 to <count>-1;^nexample: loopconcatword i 10 [ result $i ]" "<var> <count> <body>"
+setdesc "loopfiles" "executes <body> with <var> set to each file in <directory> with the given <extension> (^"^" for extension is all files);^nexample: loopfiles f data cfg [ echo $f ]" "<var> <directory> <extension> <body>"
+setdesc "looplist" "executes <body> with <var> set to each item in <list>;^nexample: looplist i ^"this is a list^" [ echo $i ]" "<var> <list> <body>"
+setdesc "loopwhile" "executes <body> with <var> incremented from 0 to <count>-1 while <condition> is true;^nexample: loopwhile i 10 [ < $i 5 ] [ echo $i ]" "<var> <count> <condition> <body>"
+setdesc "while" "executes <body> while <cond> is true;^nexample: i = 0; while [ < $i 10 ] [ echo $i; i = (+ $i 1) ]" "<cond> <body>"
+setdesc "indexof" "returns the index in <list> of <data>;^nexample: indexof ^"this is a list^" list" "<list> <data>"
+setdesc "listdel" "returns the <list1> with all occurrences of each element in <list2> removed, it is equivalent to set difference;^nexample: listdel ^"this list has duplicates^" ^"this duplicates^", will return ^"list has^"" "<list1> <list2>"
+setdesc "listfind" "returns the index in <list> where the <body> returns true, similar to looplist;^nexample: listfind i ^"this is a list^" [ result (>= (stringstr $i l) 0) ]" "<var> <list> <body>"
+setdesc "listlen" "returns the length of the <list>;^nexample: listlen ^"this is a list^"" "<list>"
+setdesc "listsplice" "returns a list where the elements between the <start> index and the end or for <count> elements is replaced with <value>;^nexample: listsplice ^"1 2 3 4 5 6^" insert 2 3" "<list> <value> <start> <count>"
+setdesc "prettylist" "returns a recombined <list> with a comma placed between each list element and <conjunctor> placed between the last two elements (in addition to the comma);^nexample: prettylist ^"1 2 3 4^" and" "<list> <conjunctor>"
+setdesc "shrinklist" "returns a list of all element that exists in both <list1> and <list2>;^nif there are no results and <failover> is 1 or 2, the list that failover points to is returned,^nwithout <failover>, shrinklist is equivalent to set symmetric difference;^nexample: shrinklist ^"this is a list^" ^"this is another list^", will return ^"this is list^"" "<list1> <list2> [<failover>]"
+setdesc "sublist" "returns a sublist of <list> starting at <start> and ending at the end or for <count> elements;^nexample: sublist ^"this is a list^" 1 2" "<list> <start> [<count>]"
+setdesc "&" "returns the bit-wise AND of <int1> and <int2>" "<int1> <int2>"
+setdesc "&~" "returns the bit-wise NOT of the bit-wise AND of <int1> and <int2>" "<int1> <int2>"
+setdesc "<<" "returns <int1> left shifted <int2> times" "<int1> <int2>"
+setdesc ">>" "returns <int1> right shifted <int2> times" "<int1> <int2>"
+setdesc "^^" "returns the bit-wise XOR of <int1> and <int2>" "<int1> <int2>"
+setdesc "^^~" "returns the bit-wise NOT of the bit-wise XOR of <int1> and <int2>" "<int1> <int2>"
+setdesc "|" "returns the bit-wise OR of <int1> and <int2>" "<int1> <int2>"
+setdesc "|~" "returns the bit-wise NOT of the bit-wise OR of <int1> and <int2>" "<int1> <int2>"
+setdesc "~" "returns the bit-wise NOT of <int>" "<int>"
+setdesc "!" "returns the logical not of <condition>" "<condition>"
+setdesc "&&" "returns the logical and of all given conditions" "<condition...>"
+setdesc "?" "returns <true_body> when <condition> is true, otherwise <false_body>" "<condition> <true_body> <false_body>"
+setdesc "||" "returns the logical or of all given conditions" "<condition...>"
+setdesc "!=" "returns true when <int1> is not equal to <int2>" "<int1> <int2>"
+setdesc "!=f" "returns true when <float1> is not equal to <float2>" "<float1> <float2>"
+setdesc "!=s" "returns true when <string1> is not equal to <string2> on the ascii table" "<string1> <string2>"
+setdesc "<=" "returns true when <int1> is less than or equal to <int2>" "<int1> <int2>"
+setdesc "<=f" "returns true when <float1> is less than or equal to <float2>" "<float1> <float2>"
+setdesc "<=s" "returns true when <string1> is less than or equal to <string2> on the ascii table" "<string1> <string2>"
+setdesc "<" "returns true when <int1> is less than <int2>" "<int1> <int2>"
+setdesc "<f" "returns true when <float1> is less than <float2>" "<float1> <float2>"
+setdesc "<s" "returns true when <string1> is less than <string2> on the ascii table" "<string1> <string2>"
+setdesc "=" "returns true when <int1> is equal to <int2>" "<int1> <int2>"
+setdesc "=f" "returns true when <float1> is equal to <float2>" "<float1> <float2>"
+setdesc "=s" "returns true when <string1> is equal to <string2> on the ascii table" "<string1> <string2>"
+setdesc ">=" "returns true when <int1> is greater than or equal to <int2>" "<int1> <int2>"
+setdesc ">=f" "returns true when <float1> is greater than or equal to <float2>" "<float1> <float2>"
+setdesc ">=s" "returns true when <string1> is greater than or equal to <string2> on the ascii table" "<string1> <string2>"
+setdesc ">" "returns true when <int1> is greater than <int2>" "<int1> <int2>"
+setdesc ">f" "returns true when <float1> is greater than <float2>" "<float1> <float2>"
+setdesc ">s" "returns true when <string1> is greater than <string2> on the ascii table" "<string1> <string2>"
+setdesc "*" "returns all arguments multiplied together as integers" "<int...>"
+setdesc "*f" "returns all arguments multiplied together as floats" "<float...>"
+setdesc "+" "returns all arguments added together as integers" "<int...>"
+setdesc "+f" "returns all arguments added together as floats" "<float...>"
+setdesc "-" "returns all other arguments subtracted from the first argument as integers" "<int...>"
+setdesc "-f" "returns all other arguments subtracted from the first argument as floats" "<float...>"
+setdesc "div" "returns all other arguments divided from the first argument as integers" "<int...>"
+setdesc "divf" "returns all other arguments divided from the first argument as floats" "<float...>"
+setdesc "mod" "returns the modulus of <int1> and <int2> (that is, the remainder of <int1>/<int2>)" "<int1> <int2>"
+setdesc "modf" "returns the modulus of <float1> and <float2> (that is, the remainder of <float1>/<float2>)" "<float1> <float2>"
+setdesc "stringcasecmp" "returns true when <string1> is equal to <string2> ignoring case;^nexample: stringcasecmp ^"str^" ^"StR^"" "<string1> <string2>"
+setdesc "stringcmp" "returns true when <string1> is equal to <string2>, equivalent to =s;^nexample: stringcmp ^"str^" ^"str^"" "<string1> <string2>"
+setdesc "stringlen" "returns the length of <string>;^nexample: stringlen ^"four^"" "<string>"
+setdesc "stringncasecmp" "returns true when the first <count> characters of <string1> and <string2> are equal ignoring case;^nexample: stringncasecmp ^"str^" ^"StRiNg^" 3" "<string1> <string2> <count>"
+setdesc "stringncmp" "returns true when the first <count> characters of <string1> and <string2> are equal;^nexample: stringncmp ^"str^" ^"string^" 3" "<string1> <string2> <count>"
+setdesc "stringreplace" "returns <string> with all occurrences of <search> replaced with <replace>;^nexample: stringreplace ^"misspelled sring^" ^"sring^" ^"string^"" "<string> <search> <replace>"
+setdesc "stringstr" "returns the index of <search> in <string>;^nexample: stringstr ^"long string^" ^"str^"" "<string> <search>"
+setdesc "substring" "returns a substringing of <string> starting at <start> and continuing to the end or for <count> characters;^nexample: substring ^"string^" 2 3" "<string> <start> [<count>]"
+setdesc "concat" "returns all <value>s concatenated with a space between each argument;^nexample: concat hello (getname)" "<value...>"
+setdesc "concatword" "returns all <value>s concatenated without a space between each argument;^nexample: concatword squished string" "<value...>"
+setdesc "clearsleep" "removes all pending sleeps, if <clearworlds> is set, it will only clear sleeps generated in a map's config file;^nexample: clearsleep" "[<clearworlds>]"
+setdesc "do" "executes <body>;^nexample: do [ echo hi ]" "<body>"
+setdesc "format" "returns a formatted string with all %1 - %9 replaced with the respective <value> argument, %% inserts a % sign;^nexample: format ^"Hello, %1, welcome to Red Eclipse v. %2^" (getname) $version" "<format_string> <value...>"
+setdesc "if" "execute <true_body> when <condition> is true, otherwise executes <false_body>;^nexample: if (< $var 5) [ echo less than 5 ] [ echo greater than 5 ]" "<condition> <true_body> <false_body>"
+setdesc "push" "pushes <value> into <var>'s stack and executes <body> then pops <value> off of <var> so that <var> is untouched after execution of <body>;^nexample: var = 5; push var 10 [ echo $var ]; echo $var" "<var> <value> <body>"
+setdesc "result" "sets the return value for the currently executing code to <value>, note that this does not stop the code execution like return would;^nexample: result 1" "<value>"
+setdesc "rnd" "returns a random number between 0 or <min> and <max>;^nexample: rnd 50 25" "<max> [<min>]"
+setdesc "sleep" "executes <body> after waiting <milliseconds>;^nexample: sleep 2000 [ echo waited 2 seconds ]" "<milliseconds> <body>"
+
+setdesc "at" "returns the element at <index> in <list>, subsequent indexes are at'd with the previous result;^nexample: at [this has a [nested list]] 3 1" "<list> <index> [<nested_index>...]"
+setdesc "filter" "filters a string by stripping of any or all of the following: newlines, color, and whitespace, each of these values are boolean, and true by default if omitted;^n[<newlines>] - if true, replaces newline characters with spaces,^n[<color>] - if true, any color formatting will be removed,^n[<whitespace>] - if true, whitespace is kept; if false, whitespace is removed" "<string> [<newlines>] [<color>] [<whitespace>]"
+setdesc "goto" "jumps the spectator camera to the position of the specified client" "<cn>"
+setdesc "hexcolour" "converts a decimal color value to a ^"pretty-printed^" hexadecimal color value;^nexample: (hexcolour 342344) will give 0x093548" "<color>"
+setdesc "precf" "returns the value <float> truncated to <accuracy> number of decimals;^nexample: (precf 0.14986 2) will give 0.14" "<float> <accuracy>"
+setdesc "writevars" "writes the current server's variables to a config file named <file>;^nif [<all>] is true, every variable will be written with default values commented out; if false, only changed variables will be written (default: false)^nif [<sv_>] is true, variables will be prefixed with ^"sv_^" (default: false)" "<file> [<all>] [<sv_>]"
+setdesc "case"  "executes the first <body> where the corresponding <test_value> is equivalent to <integer> using integer comparison;^nfor the 'default' case (always true), use () for the <test_value>,^nexample: case (rnd 3) 0 [ echo 0 ] 1 [ echo 1 ] () [ echo default ]" "<integer> [<test_value> <body>] [<test_value> <body>] [...]"
+setdesc "casef" "executes the first <body> where the corresponding <test_value> is equivalent to <float> using float comparison;^nfor the 'default' case (always true) use () for the <test_value>,^nexample: case (+f (rnd 3) .1) 0.1 [ echo 0.1 ] 1.1 [ echo 1.1 ] () [ echo default ]" "<float> [<test_value> <body>] [<test_value> <body>] [...]"
+setdesc "cases" "executes the first <body> where the corresponding <test_value> is equivalent to <string> using string comparison;^nfor the 'default' case (always true) use () for the <test_value>,^nexample: case (substring ^"abc^" (rnd 3) 1) a [ echo a ] b [ echo b ] () [ echo default ]" "<string> [<test_value> <body>] [<test_value> <body>] [...]"
+setdesc "cond"  "executes the first <body> where the corresponding <condition> is true;^nexample: i = (rnd 3); cond [ = $i 0 ] [ echo 0 ] [ = $i 1 ] [ echo 1 ] 1 [ echo default ]" "[<condition> <body>] [<condition> <body>] [...]"
+setdesc "getvarmin" "returns the minimum value of <varname>, a built-in integer variable;^nexample: getvarmin firstpersonfov" "<varname>"
+setdesc "getvarmax" "returns the maximum value of <varname>, a built-in integer variable;^nexample: getvarmax firstpersonfov" "<varname>"
+setdesc "getfvarmin" "returns the minimum value of <varname>, a built-in float variable;^nexample: getfvarmin movespeed" "<varname>"
+setdesc "getfvarmax" "returns the maximum value of <varname>, a built-in float variable;^nexample: getfvarmax movespeed" "<varname>"
+setdesc "escape" "returns a quoted string with all newline, tab, form feed, ^^, and ^" characters converted to the cubescript escape sequence (^^n, ^^t, ^^f, ^^^^, ^^^");^nexample: escape [line1^^nline2]" "<text>"
+setdesc "unescape" "returns a string with all cubescript escape sequences (^^n, ^^t, ^^f, ^^^^, ^^^") converted back into newline, tab, form feed, ^^, and ^" respectively;^nexample: unescape [line1^^^^nline2]" "<text>"
+setdesc "max" "returns the maximum <integer> value;^nexample: max 1 3 -9" "<integer...>"
+setdesc "maxf" "returns the maximum <float> value;^nexample: maxf 1.1, 2.9, -9.5" "<float...>"
+setdesc "min" "returns the minimum <integer> value;^nexample: min 1 3 -9" "<integer...>"
+setdesc "minf" "returns the minimum <float> value;^nexample: minf 1.1, 2.9, -9.5" "<float...>"
+setdesc "cos" "returns the cosine of <float>;^nexample: cos 3.1415" "<float>"
+setdesc "sin" "returns the sine of <float>;^nexample: sin 3.1415" "<float>"
+setdesc "tan" "returns the tangent of <float>;^nexample: tan 3.1415" "<float>"
+setdesc "acos" "returns the arc-cosine of <float>;^nexample: acos 1" "<float>"
+setdesc "asin" "returns the arc-sine of <float>;^nexample: asin 1" "<float>"
+setdesc "atan" "returns the arc-tangent of <float>;^nexample: atan 1" "<float>"
+setdesc "abs" "returns the absolute power of <integer>;^nexample: abs -5" "<integer>"
+setdesc "absf" "returns the absolute power of <float>;^nexample: absf -8.5" "<float>"
+setdesc "exp" "returns e (2.71828) raised to the <float> power;^nexample: exp 5.6" "<float>"
+setdesc "pow" "returns <base> raised to the power of <exponent>;^nexample: pow 2.1 8.1" "<base> <exponent>"
+setdesc "sqrt" "returns the square root of <float>;^nexample: sqrt 4" "<float>"
+setdesc "log10" "return the logarithm of <float> with base 10;^nexample: log10 1000" "<float>"
+setdesc "log2" "return the logarithm of <float> with base 2;^nexample: log2 8" "<float>"
+setdesc "loge" "return the logarithm of <float> with base e;^nexample: loge 20.08" "<float>"
+setdesc "sortlist" "returns a sorted list (quicksort) based on the result of <body> applied onto <list>;^n if <body> returns true the two elements <varname_1> and <varname_2> will be swapped, unchanged otherwise,^nexample: sortlist ^"cat ape bear boar^" a b [<s $a $b], will return ^"ape bear boar cat^" (<s is ascii-alphabetic comparison)" "<list> <varname_1> <varname_2> <body>"
+setdesc "stripcolors" "returns the <string> without any colors;^nexample: stripcolors ^"^^frhello ^^f[6252287]^"" "<string>"
+setdesc "addlocalop" "elevates the privilege of a user to a specified level locally;^nthis allows server administrators to ^"upgrade^" the access of people as they identify, on that particular server,^n<user_handle> is the authkey handle of the user,^n<flag> is the privilege level {a = administrator m = moderator o = operator s = supporter}" "<user_handle> <flag>"
+setdesc "resetlocalop" "removes all temporary elevated local privileges;^nthis allows server administrators to reset the access given by addlocalop for all users, on that particular server"
+setdesc "ircaddchan" "joins a channel on a given IRC instance with automatic rejoin;^n<name> - name of the IRC instance,^n<channel> - channel to join,^n[<friendly>] - optional friendly name of <channel>,^n[<passkey>] - optional passkey required for mode +k channels,^n[<relay>] - optional relay level (verbosity of output)" "<name> <channel> [<friendly>] [<passkey>] [<relay>]"
+setdesc "ircaddclient" "creates an IRC instance of type 'client';^n<name> - name of the created IRC instance,^n<host> - host to connect to,^n<port> - port of <host> to connect on,^n<nick> - nickname to use for the connection,^n[<ip>] - optional address to bind to (leave blank for any),^n[<passkey>] - optional password required to connect to <host>" "<name> <host> <port> <nick> [<ip>] [<passkey>]"
+setdesc "ircaddrelay" "creates an IRC instance of type 'relay';^n<name> - name of the created IRC instance,^n<host> - host to connect to,^n<port> - port of <host> to connect on,^n<nick> - nickname to use for the connection,^n[<ip>] - optional address to bind to (leave blank for any),^n[passkey] - optional password required to connect to <host>" "<name> <host> <port> <nick> [<ip>] [<passkey>]"
+setdesc "ircjoinchan" "joins a channel on a given IRC instance without automatic rejoin;^n<name> - name of the IRC instance,^n<channel> - channel to join,^n[<friendly>] - optional friendly name of <channel>,^n[<passkey>] - optional passkey required for mode +k channels^n[<relay>] - optional relay level (verbosity of output)" "<name> <channel> [<friendly>] [<passkey>] [<relay>]"
+setdesc "ircfilter" "sets how IRC messages are filtered;^n0 = send string as is, color escapes not converted, other escapes not filtered,^n1 = convert Cube2-style color escapes to IRC color escapes,^n2 = filter out all Cube2-style escapes" "<value>"
+setdesc "ircbind" "sets the address that an IRC instance binds to;^nwhen used without [<ip>], returns the address that the IRC instance '<name>' is currently bound to,^n<name> - name of the IRC instance,^n[<ip>] - optional ip address for <name> to bind to" "<name> [<ip>]"
+setdesc "ircconnect" "attempts to establish a connection to the IRC instance '<name>'" "<name>"
+setdesc "ircconns" "returns the number of active IRC connections"
+setdesc "ircfriendlychan" "sets the friendly name of an IRC channel;^nif the [<friendly>] parameter is not specified, the current friendly name of the channel is returned,^n<name> - name of an IRC instance,^n<channel> - the IRC channel,^n[<friendly>] - optional new friendly name to use" "<name> <channel> [<friendly>]"
+setdesc "ircnick" "sets the nickname of an IRC instance;^nif the [<nick>] parameter is not specified, the current nickname is returned,^n<name> - name of the IRC instance,^n[<nick>] - optional new nickname" "<name> [<nick>]"
+setdesc "ircpass" "sets the server passkey used by an IRC instance;^nif the [<passkey>] parameter is not specified, ^"<set>^" or ^"<not set>^" will be returned accordingly,^n<name> - name of the IRC instance,^n[<passkey>] - optional new passkey" "<name> [<passkey>]"
+setdesc "ircpasschan" "sets the passkey for a channel on an IRC instance;^nif the [<passkey>] parameter is not specified, ^"<set>^" or ^"<not set>^" will be returned accordingly,^n<name> - name of the IRC instance^n<channel> - the IRC channel,^n[<passkey>] - optional new passkey" "<name> [<passkey>]"
+setdesc "ircrelaychan" "sets the relay level for a channel on an IRC instance;^nif the [<relay>] parameter is not specified, the current relay level will be returned,^n<name> - name of the IRC instance,^n<channel> - the IRC channel,^n[<relay>] - optional new relay level (verbosity of output)" "<name> <channel> [<relay>]"
+setdesc "ircport" "sets the server port used by an IRC instance;^nif the [<port>] parameter is not specified, the current server port is returned,^n<name> - name of an IRC instance,^n[<port>] - optional new port" "<name> [<port>]"
+setdesc "ircserv" "sets the host to connect to for a given IRC instance;^nif the [<host>] parameter is not specified, the current host is returned,^n<name> - name of an IRC instance,^n[<host>] - optional new hostname" "<name> [<host>]"
+setdesc "ircauth" "sends a message to a specified target when succefully connected to an an IRC server;^ncan be used for setting up automatic authentication,^nif only <name> is specified, [<target>] is returned along with ^"<set>^" or ^"<not set>^" for [<message>],^n<name> - name of the IRC instance,^n[<target>] - target for the message (user, service, channel),^n[<message>] - message sent to [<target>],^nexample: ircauth myrelay NickServ ^"IDENTIFY password^"" "<name> [<target>] [<message>]"
+setdesc "botoffset" "increases or decreases the amount of bots starting from the numplayers value of the current map" "<value>"
+setdesc "mapcomplete" "enables map completion for a given command or alias;^nthis enables tab-completion for the first argument of an already existing command or alias,^nthe completion uses files that ends with 'mpz' in the 'maps' folders of the game's homedir, and its data dir(s)" "<command>"
+setdesc "map" "requests a map change to a given map, with no change of mode or mutators;^ndepending on privileges, this will force or vote for the map change" "<map>"
+setdesc "mode" "sets the mode and mutator values for the next map change request;^n<mode> sets the mode type,^n<muts> sets the mutators according to a bitwise sum of mutator values,^nconveniently set using $modeidx* vars and sums of $mutsbit* vars,^nexample: mode $modeidxdeathmatch (+ $mutsbitinstagib $mutsbitmedieval)" "<mode> <muts>"
+setdesc "start" "requests a map change to a given map with a specific mode and mutators;^ndepending on privileges, this will force or vote for the map change,^n<mode> sets the mode type,^n<muts> sets the mutators according to a bitwise sum of mutator values,^nconveniently set using $modeidx* vars and sums of $mutsbit* vars,^nexample: start bath $modeidxdeathmatch (+ $mutsbitinstagib $mutsbitmedieval)" "<map> <mode> <muts>"
+setdesc "demo" "starts playback of a given demo" "<demo>"
+setdesc "deathmatch" "requests a map change to deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values, conveniently set using a sum of $mutsbit* vars,^nexample: deathmatch bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "dm" "requests a map change to deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values, conveniently set using a sum of $mutsbit* vars,^nexample: dm bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "teamdm" "requests a map change to team deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: teamdm bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "tdm" "requests a map change to team deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: tdm bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "multidm" "requests a map change to multi deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: multidm bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "mdm" "requests a map change to multi deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: mdm bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "coop" "requests a map change to coop deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: coop bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "cdm" "requests a map change to coop deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: cdm bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "capture" "requests a map change to capture-the-flag on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: capture bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "ctf" "requests a map change to capture-the-flag on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: ctf bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "defend" "requests a map change to defend-and-control on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: defend bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "dac" "requests a map change to defend-and-control on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: dac bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "dnc" "requests a map change to defend-and-control on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: dac bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "bomber" "requests a map change to bomber-ball on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: bomber futuresport (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "bb" "requests a map change to bomber-ball on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: bb futuresport (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "race" "requests a map change to race on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: race purge (+ $mutsbitfreestyle $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "instagib" "requests a map change to instagib deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: instagib bath (+ $mutsbitduel $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "insta" "requests a map change to instagib deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: insta bath (+ $mutsbitduel $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "medieval" "requests a map change to medieval deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: medieval bath (+ $mutsbitinstagib $mutsbitfreestyle)" "<map> [<muts>]"
+setdesc "kaboom" "requests a map change to kaboom deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: kaboom bath (+ $mutsbitinstagib $mutsbitfreestyle)" "<map> [<muts>]"
+setdesc "duel" "requests a map change to duel deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: duel bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "survivor" "requests a map change to survivor deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: survivor bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "lms" "requests a map change to survivor deathmatch (last man standing) on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: lms bath (+ $mutsbitinstagib $mutsbitmedieval)" "<map> [<muts>]"
+setdesc "classic" "requests a map change to classic deathmatch on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "quickcapture" "requests a map change to quick capture-the-flag on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "defendcapture" "requests a map change to defend capture-the-flag on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "protectcapture" "requests a map change to protect capture-the-flag on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "quickdefend" "requests a map change to quick defend-and-control on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "kingdefend" "requests a map change to king defend-and-control on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "koth" "requests a map change to king defend-and-control (king of the hill) on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "holdbomber" "requests a map change to hold bomber-ball on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "basketbomber" "requests a map change to basket bomber-ball on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "attackbomber" "requests a map change to attack bomber-ball on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "timedrace" "requests a map change to timed race on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "trial" "requests a map change to timed race on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "endurancerace" "requests a map change to endurance race on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "gauntletrace" "requests a map change to gauntlet race on a given map;^n[<muts>] optionally adds extra mutators according to a bitwise mutator values,^nconveniently set using a sum of $mutsbit* vars,^nexample: classic bath (+ $mutsbitinstagib $mutsbitduel)" "<map> [<muts>]"
+setdesc "writevarsinfo" "writes raw information about all existing vars, commands and aliases to a file;^nfor each item the following fields are printed: NAME TYPE FLAGS ARGS VALTYPE VALUE MIN MAX DESC USAGE,^nfields are separated by tabs, and empty if nonexistent" "<file>"
+setdesc "simpleitems" "sets how items are shown in the level;^n0 = items are models, 1 = items are icons, 2 = items are off and only halos appear" "<level>"
+setdesc "simpleitemsize" "sets the size of simple items" "<size>"
+setdesc "simpleitemblend" "sets the blend level of simple items" "<blend>"
+setdesc "simpleitemhalo" "adjusts the halo blend when displaying simple items as icon" "<blend>"
+setdesc "autospecdelay" "determines the delay after player joins spectators after being fragged" "<milliseconds>"
+setdesc "bomberlockondelay" "determines the amount of time needed to hold throw button to achieve assisted pass to another team member" "<milliseconds>"
+setdesc "botspeed" "multiplier for the movement speed of bots" "<value>"
+setdesc "capturecarryspeed" "multiplier for the movement speed of flag-carrying players;^nacts as an added multiplier on top of the normal movespeed multiplier" "<value>"
+setdesc "capturepickupdelay" "time a player needs to wait to be able to pick up a flag again after losing it" "<milliseconds>"
+setdesc "coopskillmax" "determines the maximum skill level a bot can have in coop" "<skill level>"
+setdesc "coopskillmin" "determines the minimum skill level a bot can have in coop" "<skill level>"
+setdesc "fragbonus" "determines the amount of points a player gets for each frag" "<value>"
+setdesc "enemybonus" "determines the amount of points a player gets for frags against neutral enemies in onslaught" "<value>"
+setdesc "headshotpoints" "determines the amount of bonus points a player gets when a frag is caused by a headshot" "<value>"
+setdesc "multikillpoints" "determines the amount of bonus points a player gets for a double, triple, or multi kill" "<value>"
+setdesc "multikillbonus" "determines if multikillpoints should be multiplied by 2, 3, or 4 as appropriate" "<value>"
+setdesc "spreepoints" "determines the amount of points a player gets for achieving each level of killing spree for the first time" "<value>"
+setdesc "spreebreaker" "determines the amount of points a player gets for ending an opponent's killing spree" "<value>"
+setdesc "dominatepoints" "determines the amount of points a player gets when he achieves domination on another player" "<value>"
+setdesc "revengepoints" "determines the amount of points a player gets when he gets revenge against a dominating player" "<value>"
+setdesc "firstbloodpoints" "determines the amount of points a player gets when he makes the first kill of a match" "<value>"
+setdesc "mapbalance" "determines if teams are switched on asymmetrical maps after half of the time is over;^n0 = no balance, 1 = balance only in ctf/dac/bb, 2 = balance in all modes" "<value>"
+setdesc "teamkillban" "automatically ban player when warned this many times for teamkill" "<value>"
+setdesc "teamkillkick" "automatically kick player when warned this many times for teamkill" "<value>"
+setdesc "teamkillpenalty" "subtract this*fragbonus from the players points when teamkilling" "<value>"
+setdesc "teamkillrestore" "restore the team score as if the teamkiller was never there if the point loss of the team was greater than this" "<value>"
+setdesc "teamkilltime" "time threshold in which teamkills are counted" "<minutes>"
+setdesc "teamkillwarn" "the limit of teamkills after which the player receives a warning" "<value>"
+setdesc "saycommand" "write a chat message or command but waits for your affirmation to send it" "<text>"
+setdesc "say" "send a message to the global chat" "<text>"
+setdesc "me" "send a message to the global chat in the form of an action^nex: * foobar demonstrates something" "<text>"
+setdesc "sayteam" "send a message to the team chat" "<text>"
+setdesc "meteam" "send a message to the team chat in the form of an action^nex: * foobar (to team ...) demonstrates something" "<text>"
+setdesc "whisper" "send a message directly to another player on the server^n^frnote: messages are ^fRnot ^frprivate and are visible in demos" "<cn> <text>"
+setdesc "mewhisper" "send a message directly to another player on the server in the form of an action^nex: * foobar (whispers to barfoo) demonstrates something^n^frnote: messages are ^fRnot ^frprivate and are visible in demos" "<cn> <text>"
+setdesc "waitforplayers" "wait this long for players to load the map" "<milliseconds>"
+setdesc "waitforplayerannounce" "update everyone on the progress every this often" "<milliseconds>"
+
+looplist w $weapname [
+    setdesc (concatword $w ammoadd) "the amount added when picking up the weapon or reloading it" "<value>"
+    setdesc (concatword $w ammomax) "the maximum amount the clip of this weapon can hold, can never be higher than default" "<value>"
+    setdesc (concatword $w delayreload) "the time it takes for the weapon to reload one 'add' unit" "<milliseconds>"
+    setdesc (concatword $w reloads) "determines if this weapon can reload" "<bool>"
+    setdesc (concatword $w laser) "determines if this weapon has a laser pointer which is projected to the point where the player is aiming" "<value>"
+    setdesc (concatword $w frequency) "determines the multiplier of itemspawntime in which items of this type respawn in" "<value>"
+    setdesc (concatword $w colour) "determines the weapon main colour;^naffects the light around the weapon, the ammo and the icon in the weaponlist" "<colour>"
+    setdesc (concatword $w modes) "determines the modes in which this weapon is allowed or disallowed;^nconveniently set as a sum of $modebit* vars,^npositive sum: allow this weapon if one of the declared modes is selected,^nnegative sum: disallow this weapon if one if the declared modes is selected" "<sum>"
+    setdesc (concatword $w muts) "determines the mutators in which this weapon is allowed or disallowed;^nconveniently set as a sum of $mutsbit* vars,^npositive sum: allow this weapon if one of the declared mutators is selected,^nnegative sum: disallow this weapon if one of the declared mutators is selected" "<sum>"
+
+    loop n 2 [
+        m = (+ $n 1)
+        setdesc (concatword $w ammosub $m) "the amount taken from the clip for each firing action, set to zero for unlimited ammo" "<value>"
+        setdesc (concatword $w delayattack $m) "the time it takes after each firing action for the weapon to be ready again" "<milliseconds>"
+        setdesc (concatword $w damage $m) "the amount of damage a projectile from each firing action does" "<value>"
+        setdesc (concatword $w speed $m) "the speed of a projectile from each firing action" "<value>"
+        setdesc (concatword $w cooktime $m) "when more than zero, determines that the weapon can be 'cooked' this long before firing" "<value>"
+        setdesc (concatword $w time $m) "the maximum lifetime of a projectile for each firing action" "<milliseconds>"
+        setdesc (concatword $w projdelay $m) "when more than zero, projectiles from this weapon will be delayed this long" "<milliseconds>"
+        setdesc (concatword $w guideddelay $m) "when more than zero, projectiles from this weapon will delay guided settings by this long" "<milliseconds>"
+        setdesc (concatword $w escapedelay $m) "when more than zero, projectiles from this weapon will not be able to hurt its owner for this long" "<milliseconds>"
+        setdesc (concatword $w explode $m) "determines the explosion radius for a particle of this firing action" "<value>"
+        setdesc (concatword $w rays $m) "the amount of projectiles spawned from one shot of each firing action" "<value>"
+        setdesc (concatword $w spread $m) "determines the amount a projectile from each firing action skews off-center" "<value>"
+        setdesc (concatword $w spreadz $m) "when zero, keeps spread projectiles aligned horizontally, else divide the z axis this much" "<value>"
+        setdesc (concatword $w aiskew $m) "determines 'added stupidity' for each weapon for AI counterparts" "<value>"
+        setdesc (concatword $w fragweap $m) "when projectiles from this firing action are destroyed, create projectiles from this kind of weapon (+11 = alt fire for the weapon)" "<value>"
+        setdesc (concatword $w flakdamage $m) "flak of this type deals this much damage" "<value>"
+        setdesc (concatword $w fragrays $m) "when creating flak, create this many projectiles for it" "<value>"
+        setdesc (concatword $w fragtime $m) "flak projectiles from this weapon last this long" "<milliseconds>"
+        setdesc (concatword $w fragspeed $m) "flak projectiles from this weapon start with this much speed (may be influenced by fragspread/fragskew and fragrel)" "<value>"
+        setdesc (concatword $w collide $m) "determines collision properties for a projectile from each firing action (bitwise OR);^nIMPACT_GEOM = 1, BOUNCE_GEOM = 2, IMPACT_PLAYER = 4, BOUNCE_PLAYER = 8, RADIAL_PLAYER = 16, COLLIDE_TRACE = 32, COLLIDE_OWNER = 64, COLLIDE_CONT = 128, COLLIDE_STICK = 256" "<sum>"
+        setdesc (concatword $w extinguish $m) "determines if a projectile from each firing action is extinguished by water" "<value>"
+        setdesc (concatword $w cooked $m) "determines cooking style for a projectile, &1 = more damage, &2 = less damage, &4 = longer lifetime, &8 = shorter lifetime, &16 = faster speed, &32 = slower speed, &64 = extra rays, &128 = zoom view" "<value>"
+        setdesc (concatword $w guided $m) "determines guided style for a projectile, 0 = off, 1 = follow crosshair direction, 2 = home crosshair target (+1 only first target), 4 = home projectile target (+1 only first target), 6 = direct toward first crosshair direction" "<value>"
+        setdesc (concatword $w radial $m) "determines the time between ticks of ^"continuous^" radial damage, starts counting from first ^"radiation^"" "<milliseconds>"
+
+        setdesc (concatword $w residual $m) "determines if a projectile from each firing action has a residual effect;^n0 = off, 1 = burns, 2 = bleeds, 4 = shocks" "<bits>"
+        setdesc (concatword $w residualundo $m) "determines if a projectile from each firing action will extinguish a residual effect;^n0 = off, 1 = burns, 2 = bleeds, 4 = shocks" "<bits>"
+        setdesc (concatword $w fullauto $m) "determines if each firing action is fully automatic (click-and-hold) or not (click-and-click)" "<bool>"
+        setdesc (concatword $w taperin $m) "determines the fraction of the lifetime from the start until the projectile reaches its full size" "<value>"
+        setdesc (concatword $w taperout $m) "determines the fraction of the lifetime from the end where the projectile size ^"fades out^"" "<value>"
+        setdesc (concatword $w elasticity $m) "multiplier of velocity for a projectile of each firing action when doing a bounce event" "<value>"
+        setdesc (concatword $w reflectivity $m) "guard angle for a projectile of each firing action when doing a bounce event" "<angle>"
+        setdesc (concatword $w relativity $m) "multiplier of player velocity added to a projectile of each firing action" "<value>"
+        setdesc (concatword $w liquidcoast $m) "multiplier of velocity for a projectile of each firing action when in liquid" "<value>"
+        setdesc (concatword $w weight $m) "relative weight for a projectile of each firing action" "<value>"
+        setdesc (concatword $w radius $m) "determines the size for a projectile of each firing action" "<value>"
+        setdesc (concatword $w kickpush $m) "determines the amount of pushback from shooting each firing action" "<value>"
+        setdesc (concatword $w hitpush $m) "determines the amount of pushback from getting hit by this shot" "<value>"
+        setdesc (concatword $w aidist $m) "determines the 'maximum distance' a weapon should be shot at, used by AI to determine weapon effectiveness ranges" "<distance>"
+        setdesc (concatword $w partsize $m) "determines the maximum particle size of a projectile from each firing action" "<value>"
+        setdesc (concatword $w partlen $m) "determines the maximum tape particle length of a projectile from each firing action" "<value>"
+        setdesc (concatword $w delta $m) "determines the amount by which each firing action is guided" "<value>"
+        setdesc (concatword $w trace $m) "determines the multiplier of length to apply to traced weapons" "<value>"
+        setdesc (concatword $w damagetorso $m) "determines the multiplier of damage for torso shots" "<value>"
+        setdesc (concatword $w damagelegs $m) "determines the multiplier of damage for leg shots" "<value>"
+        setdesc (concatword $w damagehead $m) "determines the multiplier of damage for head shots" "<value>"
+        setdesc (concatword $w fragscale $m) "flak created by this firing action is scaled by this much" "<value>"
+        setdesc (concatword $w fragspread $m) "flak created by this firing action spreads its direction randomly by this much if it doesn't impact a player" "<value>"
+        setdesc (concatword $w fragrel $m) "flak created by this firing action retains this much of its parent relative momentum" "<value>"
+        setdesc (concatword $w fragoffset $m) "flak created by this firing action is offset by this distance if it impacts a player before being created" "<value>"
+        setdesc (concatword $w fragskew $m) "flak created by this firing action spreads its direction randomly by this much when it impacts a player" "<value>"
+        setdesc (concatword $w wavepush $m) "determines the multiplier of explode radius this weapon pushes in" "<value>"
+        setdesc (concatword $w explcol $m) "determines the colour of the explosion of a projectile from this weapon" "<colour>"
+        setdesc (concatword $w flakcollide $m) "determines collision properties for flak of this type from each firing action (bitwise OR);^nIMPACT_GEOM = 1, BOUNCE_GEOM = 2, IMPACT_PLAYER = 4, BOUNCE_PLAYER = 8, RADIAL_PLAYER = 16, COLLIDE_TRACE = 32, COLLIDE_OWNER = 64, COLLIDE_CONT = 128, COLLIDE_STICK = 256" "<sum>"
+        setdesc (concatword $w flakexplcol $m) "determines the colour of the explosion of flak of this type" "<colour>"
+        setdesc (concatword $w flakpartcol $m) "determines the colour of flak of this type" "<colour>"
+        setdesc (concatword $w flakresidual $m) "determines the residual effects for flak of this type from each firing action (bitwise OR);^n0 = off, 1 = burns, 2 = bleeds, 4 = shocks" "<sum>"
+        setdesc (concatword $w partcol $m) "determines the colour of a projectile from this weapon" "<colour>"
+        setdesc (concatword $w parttype $m) "determines the type of projectiles this weapon shoots" "<value>"
+        setdesc (concatword $w residual $m) "determines the residual effects of a projectile from each firing action (bitwise OR);^n0 = off, 1 = burns, 2 = bleeds, 4 = shocks" "<sum>"
+    ]
+]
diff --git a/config/vanities.cfg b/config/vanities.cfg
new file mode 100644
index 0000000..1b7aaff
--- /dev/null
+++ b/config/vanities.cfg
@@ -0,0 +1,46 @@
+resetvanity
+// zone:   unique definition that can each only hold one vanity
+// model:  vanity model folder
+// name:   friendly name for viewing
+// tag:    tag on player where vanity attaches
+// cond:   bitwise: 1 = not on first person body, 2 = not on headless model
+// style:  1 = add folder for player priv level
+//     zone model                   name                tag              cond   style
+addvanity 0 "badge"                 "badge"             "tag_badge"         1   1       // chest
+addvanity 0 "chestplate"			"chestplate"        "tag_badge"			1           // chest
+addvanity 1 "monocle/classy"        "classy monocle"    "tag_eyepiece"      3           // eyes
+addvanity 1 "monocle/bionic"        "bionic monocle"    "tag_eyepiece"      3           // eyes
+addvanity 1 "eye"                   "eye"               "tag_eyepiece"      3           // eyes
+addvanity 1 "microeye"			    "microeye"			"tag_glasses"		3           // eyes
+addvanity 1 "nightvision"           "nightvision"       "tag_glasses"       3           // eyes
+addvanity 2 "tophat/felt"           "felt tophat"       "tag_headtop"       3           // head
+addvanity 2 "tophat/metal"          "metal tophat"      "tag_headtop"       3           // head
+addvanity 2 "crown"                 "crown"             "tag_headtop"       3           // head
+addvanity 2 "horns"                 "horns"             "tag_headtop"       3           // head
+addvanity 2 "brain"                 "brain"             "tag_headtop"       3           // head
+addvanity 2 "foxears"               "fox ears"          "tag_headtop"       3           // head
+addvanity 2 "afro"                  "afro"              "tag_headtop"       3           // head
+addvanity 2 "prophat"               "propeller"         "tag_headtop"       3           // head
+addvanity 2 "antenna/right"         "antenna right"     "tag_headtop"       3           // head
+addvanity 2 "antenna/left"          "antenna left"      "tag_headtop"       3           // head
+addvanity 3 "mask"			        "mask"			    "tag_headtop"		3           // face
+addvanity 3 "moustache"             "moustache"         "tag_moustache"     3           // face
+addvanity 4 "wings"                 "wings"             "tag_wings"         1           // back
+addvanity 4 "guitar"                "guitar"            "tag_back"          1           // back
+addvanity 4 "blade"                 "blade"             "tag_back"          1           // back
+addvanity 4 "blade/twin"            "twin blade"        "tag_back"          1           // back
+addvanity 4 "prongs"                "prongs"            "tag_wings"         1           // back
+addvanity 4 "disc"                  "disc"              "tag_wings"         1           // back
+addvanity 5 "boots/right"           "boots right"       "tag_rtoe"                      // right foot
+addvanity 5 "boots2/right"          "boots2 right"      "tag_rtoe"                      // right foot
+addvanity 5 "raptor/right"          "raptor foot right" "tag_rtoe"                      // right foot
+addvanity 6 "boots/left"            "boots left"        "tag_ltoe"                      // left foot
+addvanity 6 "boots2/left"           "boots2 left"       "tag_ltoe"                      // left foot
+addvanity 6 "raptor/left"           "raptor foot left"  "tag_ltoe"                      // left foot
+addvanity 7 "panda"                 "shoulder panda"    "tag_rshoulder"     1           // right shoulder
+addvanity 7 "shoulder/right"        "shoulder right"    "tag_rshoulder"     1           // right shoulder
+addvanity 7 "shoulder2/right"       "shoulder2 right"   "tag_rshoulder"     1           // right shoulder
+addvanity 7 "shoulder3/right" 		"shoulder3 right"	"tag_rshoulder"     1           // right shoulder
+addvanity 8 "shoulder/left"         "shoulder left"     "tag_lshoulder"     1           // left shoulder
+addvanity 8 "shoulder2/left"        "shoulder2 left"    "tag_lshoulder"     1           // left shoulder
+addvanity 8 "shoulder3/left"   		"shoulder3 left" 	"tag_lshoulder"     1           // left shoulder
diff --git a/config/version.cfg b/config/version.cfg
new file mode 100644
index 0000000..6154762
--- /dev/null
+++ b/config/version.cfg
@@ -0,0 +1,23 @@
+servermaster "play.redeclipse.net"
+
+sv_botmalenames "moe larry curly jerry ted phil bob billy joe john james gunner dante bob lorenzo chuck zeus alexander benjamin daniel ethan fernando gabriel hunter isaac jacob kevin logan michael noah owen parker ryan samuel tyler uriel victor william xavier yahir zachary abram avaz abraham agap agapit agathon adam azamat azat aidar airat akim alan alexander alex ali alikhan diamond albert anatoly angel andrew anton anfim aram arkady arman armen arseniy arslan artem artemia arthur askha [...]
+sv_botfemalenames "sue debbie luanne brandy angel kitty jane ava brianna chloe destiny emma faith grace hannah julia kaylee lily madison natalie olivia peyton riley sophia taylor unique victoria ximena yaretzi zoe avdotya aurora agatha agafya agnes ada adelaide adeline adele aksinya alevtina alain alina alice aliyah alla alsou alba albina alfia alya amalia amin amir anahit anastasia angelina angela angelica anisa anna antonina anfisa arina bela bertha valentine valeria varvara vasilisa v [...]
+sv_botmalevanities "badge"
+sv_botfemalevanities "badge"
+
+sv_allowmaps "absorption abuse affluence ares bath battlefield biolytic bloodlust campgrounds canals canyon cargo castle center colony conflict condensation convolution cutec cyanide darkness deadsimple deathtrap decay decomposition deli depot dropzone dutility echo enyo erosion error escape fortitude futuresport ghost hinder institute keystone2k linear livefire longestyard mist neodrive nova octavus oneiroi panic processing pumpstation purge relax rooftop spacetech steelrat stone suspen [...]
+
+sv_mainmaps "abuse affluence ares bath battlefield biolytic bloodlust campgrounds canals canyon cargo castle center colony conflict condensation convolution cutec darkness deadsimple deathtrap decay deli depot dropzone dutility echo enyo erosion error fortitude futuresport ghost institute keystone2k linear livefire longestyard mist nova octavus oneiroi panic processing pumpstation rooftop spacetech stone suspended tower tribal ubik vault venus wet"
+sv_capturemaps "affluence ares bath battlefield biolytic canals canyon center colony conflict condensation convolution darkness deadsimple deli depot dropzone dutility echo enyo erosion fortitude futuresport institute keystone2k linear mist nova octavus panic pumpstation rooftop stone suspended tribal vault venus wet"
+sv_defendmaps "affluence ares bath battlefield biolytic campgrounds canals canyon cargo center colony conflict condensation convolution cutec darkness deadsimple deathtrap deli depot dropzone dutility echo enyo erosion fortitude futuresport ghost institute keystone2k linear livefire mist nova octavus panic processing rooftop stone suspended tower tribal ubik vault venus wet"
+sv_kingmaps "ares bath battlefield biolytic campgrounds canals canyon cargo center colony conflict condensation darkness depot dropzone dutility echo enyo linear livefire octavus processing pumpstation rooftop stone suspended tribal ubik vault venus"
+sv_bombermaps "affluence ares battlefield canals canyon cargo center conflict condensation convolution darkness deathtrap deli depot dropzone dutility echo enyo erosion fortitude futuresport linear mist nova octavus pumpstation rooftop stone suspended tower tribal vault venus wet"
+sv_holdmaps "affluence ares bath battlefield biolytic campgrounds canals canyon cargo center colony conflict condensation convolution cutec darkness deadsimple deathtrap decay deli depot dropzone dutility echo enyo erosion futuresport ghost keystone2k linear mist nova octavus panic pumpstation stone suspended tower tribal ubik vault venus wet"
+sv_racemaps "absorption cyanide decomposition escape hinder neodrive purge relax steelrat testchamber tonatiuh wardepot"
+
+sv_multimaps "canals condensation convolution deadsimple depot keystone2k suspended"
+sv_duelmaps "abuse bath bloodlust campgrounds canyon cargo castle darkness deadsimple dutility echo error ghost livefire longestyard mist panic stone vault wet"
+
+sv_smallmaps "abuse affluence bath bloodlust campgrounds canyon cargo castle darkness deadsimple deathtrap dutility echo error ghost keystone2k livefire longestyard mist panic processing stone vault wet"
+sv_mediummaps "affluence ares battlefield biolytic bloodlust campgrounds canyon cargo center conflict cutec darkness deadsimple deathtrap deli dropzone dutility echo enyo erosion error fortitude futuresport ghost institute keystone2k linear livefire mist nova octavus oneiroi processing pumpstation rooftop spacetech suspended stone tower ubik venus wet"
+sv_largemaps "battlefield biolytic canals center colony condensation convolution cutec decay deli depot dropzone enyo erosion fortitude futuresport linear nova octavus rooftop spacetech suspended tower tribal ubik"
diff --git a/config/voice.cfg b/config/voice.cfg
new file mode 100644
index 0000000..cf10d75
--- /dev/null
+++ b/config/voice.cfg
@@ -0,0 +1,40 @@
+// compass/voice actions
+
+voices = 0
+addvoice = [
+    voices = (+ $voices 1)
+    [voice@[voices]str] = $arg1
+    [voice@[voices]snd] = (registersound $arg2 255 512 8 $arg3)
+]
+
+addvoice "argh"         "voice/argh"        2
+addvoice "lucky shot"   "voice/luckyshot"   2
+addvoice "nice shot"    "voice/niceshot"    2
+addvoice "ns~"          "voice/niceshot"    1 // reuse
+addvoice "boom"         "voice/boom"        2
+addvoice "damn"         "voice/damnit"      2
+addvoice "haha"         "voice/haha"        2
+addvoice "suck"         "voice/suckit"      2
+addvoice "pzap"         "voice/pzap"        2
+addvoice "yes~"         "voice/yes"         2
+addvoice "sorry"        "voice/sorry"       2
+addvoice "sry~"         "voice/sorry"       1 // reuse
+addvoice "no problem"   "voice/noproblem"   2
+addvoice "np~"          "voice/noproblem"   1 // reuse
+addvoice "no prob"      "voice/noproblem"   1 // reuse
+addvoice "no~"          "voice/no"          2
+addvoice "go go go"     "voice/gogogo"      2
+addvoice "gogogo"       "voice/gogogo"      1 // reuse
+addvoice "hang on"      "voice/hangon"      2
+addvoice "thanks"       "voice/thanks"      2
+addvoice "ty~"          "voice/thanks"      1 // reuse
+
+on_text = [
+    id = 0
+    reg = [@(filter $arg4)~] // the ~ allows absolute matching, like in yes/no/etc
+    loopwhile i $voices [= $id 0] [
+        str = $[voice@(+ $i 1)str]
+        if (stringncasecmp $str $reg (stringlen $str)) [ id = (+ $i 1) ]
+    ]
+    result (getalias [voice@[id]snd])
+]
diff --git a/doc/all-licenses.txt b/doc/all-licenses.txt
index 3851edf..f697f54 100644
--- a/doc/all-licenses.txt
+++ b/doc/all-licenses.txt
@@ -1,19 +1,17 @@
-Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
 
 Files: bin/* src/include/* src/lib/* src/enet/* src/site/* src/xcode/*
-       game/mek/*
 Copyright: Omitted
 License: Omitted
 
 Files: *
-Copyright: 2009-2013, Red Eclipse Team
+Copyright: 2009-2015, Red Eclipse Team
 License: CC-BY-SA-3.0+
 
 Files: src/*
-Copyright: 2001-2013, Wouter van Oortmerssen, Lee Salzman, Mike Dysart,
+Copyright: 2001-2015, Wouter van Oortmerssen, Lee Salzman, Mike Dysart,
            Robert Pointon, Quinton Reeves
-           2009-2013, Quinton Reeves, Lee Salzman
-
+           2009-2015, Quinton Reeves, Lee Salzman
 License: Zlib
 
 Files: doc/trademark.txt
@@ -32,7 +30,7 @@ Copyright: 2011-2012, Red Eclipse Team
 License: Zlib
 
 Files: doc/examples/*
-Copyright: 2011-2013, Red Eclipse Team
+Copyright: 2011-2015, Red Eclipse Team
 License: Zlib
 
 Files: doc/man/cube2font.1 doc/man/redeclipse.6.am
@@ -40,76 +38,76 @@ Files: doc/man/cube2font.1 doc/man/redeclipse.6.am
 Copyright: 2011-2012, Martin Erik "arand" Werner <martinerikwerner at gmail.com>
 License: ZLib
 
-Files: game/fps/maps/biolytic* game/fps/maps/darkness* game/fps/maps/mist*
-       game/fps/maps/nova* game/fps/maps/panic* game/fps/maps/oneiroi*
-       game/fps/maps/processing* game/fps/maps/tranquility*
-       game/fps/maps/vault*
+Files: data/maps/biolytic* data/maps/darkness* data/maps/mist* data/maps/nova*
+       data/maps/panic* data/maps/oneiroi* data/maps/processing*
+       data/maps/tranquility* data/maps/vault*
 Copyright: 2009-2013, Derek "favorito" Ponicki <favorito.redeclipse at gmail.com>
 License: Zlib
 
-Files: game/fps/maps/deadsimple*
+Files: data/maps/deadsimple*
 Copyright: Jonathan 'Wicked' Roels
            2013 Ulukai <jonathan.denil at gmail.com>
 License: CC-BY-SA-3.0
 
-Files: game/fps/maps/ares* game/fps/maps/canals* game/fps/maps/conflict*
+Files: data/maps/ares* data/maps/canals* data/maps/conflict* data/maps/enyo*
+       data/ulukai/*
 Copyright: 2011-2013, Ulukai <jonathan.denil at gmail.com>
 License: CC-BY-SA-3.0
 
-Files: game/fps/maps/keystone2k* game/fps/maps/livefire*
+Files: data/maps/keystone2k* data/maps/livefire*
 Copyright: 2012-2013, John_III <snaginneb at yahoo.com>
 License: CC-BY-SA-3.0
 
-Files: game/fps/maps/fourplex* game/fps/maps/pumpstation*
-       game/fps/maps/starlibido*
-Copyright: 2012-2013 D.a.M.i.E.n. <daymeenn at gmail.com>
-License: CC-BY-SA-3.0
-
-Files: game/fps/maps/neodrive* game/fps/maps/hawk*
+Files: data/maps/neodrive* data/maps/wardepot*
 Copyright: 2012-2013, Kirill "TristamK" Kolesnikov <tristamk at bk.ru>
 License: CC-BY-SA-3.0
 
-Files: game/fps/maps/cutec*
+Files: data/maps/cutec*
 Copyright: 2012-2013, Raiden
 License: CC-BY-SA-3.0
 
-Files: game/fps/maps/erosion* game/fps/maps/suspended*
+Files: data/maps/erosion* data/maps/suspended*
 Copyright: 2012-2013 Matt "greasepirate" Kalt
 License: CC-BY-3.0
 
-Files: game/fps/maps/battlefield*
+Files: data/maps/battlefield*
 Copyright: 2012-2013, Quinton Reeves, Ulukai <jonathan.denil at gmail.com>
 License: CC-BY-SA-3.0+
 
-Files: game/fps/maps/cyanide*
+Files: data/maps/cyanide* data/maps/decay* data/maps/octavus*
 Copyright: 2013 Ricky Thomson "unixfreak" <punkalert at gmail.com>
 License: CC-BY-SA-3.0
 
-Files: game/fps/maps/steelrat*
+Files: data/maps/steelrat*
 Copyright: 2012 Kirill "TristamK" Kolesnikov <tristamk at bk.ru>
            2012-2013 D.a.M.i.E.n. <daymeenn at gmail.com>
 License: CC-BY-SA-3.0
 
-Files: game/fps/maps/venus*
+Files: data/maps/venus*
 Copyright: 2011-2013, Red Eclipse Team
            2013, Ulukai <jonathan.denil at gmail.com>
 License: CC-BY-SA-3.0
 
-Files: game/fps/maps/bloodlust*
+Files: data/maps/bloodlust*
 Copyright: 2013 Joseph "ballist1c" Calabria
            2013 Matt "greaserpirate" Kalt
 License: CC-BY-SA-3.0
 
-Files: data/misc/light2*
-Copyright: 2009 Georges "TRaK" Grondin <drognin at gmail.com>
-           2013 D.a.M.i.E.n. <daymeenn at gmail.com>
-License: Expat
+Files: data/maps/abuse*
+Copyright: 2013, Joseph "ballist1c" Calabria
+License: CC-BY-SA-3.0
 
-Files: data/misc/starllogo*
-Copyright: 2009 Georges "TRaK" Grondin <drognin at gmail.com>
-           2012 Julia Petretta <julia.petretta at googlemail.com>
-           2013 D.a.M.i.E.n. <daymeenn at gmail.com>
-License: Expat
+Files: data/maps/castle* data/maps/rooftop*
+Copyright: 2014, Fatony
+License: CC0-1.0
+
+Files: data/maps/fortitude*
+Copyright: 2014, James "Max Payne" Tonjes <tonjesjames at gmail.com>
+License: CC-BY-SA-3.0
+
+Files: data/maps/relax* data/maps/tonatiuh*
+Copyright: 2013-2014, Bonifarz <bonifarz at gmx.ch>
+License: CC-BY-SA-3.0
 
 Files: data/jwin/*
 Copyright: JWindecker <jacobwindecker at hotmail.com>
@@ -120,11 +118,7 @@ Copyright: Jeroen "appleflap" Boukens
 License: CC-BY-SA-3.0
 
 Files: data/penguins/*
-Copyright: 2009, 2012, skiingpenguins <roy153roy153 at yahoo.com>
-License: CC-BY-SA-3.0
-
-Files: data/ulukai/*
-Copyright: 2013 Ulukai <jonathan.denil at gmail.com>
+Copyright: 2009, 2012, Zachery "skiingpenguins" Slocum <freezurbern at gmail.com>
 License: CC-BY-SA-3.0
 
 Files: data/ulukai/redeclipse*
@@ -140,6 +134,10 @@ Files: data/crosshairs/*
 Copyright: 2011, fluxord <fluxord> at <yahoo.com>
 License: CC-BY-SA-3.0
 
+Files: data/crosshairs/fed* data/crosshairs/trident*
+Copyright: 2014, pyccna
+License: CC-BY-SA-3.0
+
 Files: data/skyboxes/gradient* data/skyboxes/stars*
 Copyright: Quinton Reeves <qreeves at gmail.com>
 License: CC-BY-SA-3.0+
@@ -169,12 +167,20 @@ License: CC-BY-3.0
 
 Files: data/mayhem/*
 Copyright: Mayhem
-License: CC-BY-SA (version unclear, probably not 3.0)
+License: CC-BY-SA-3.0
 
 Files: data/philipk/
 Copyright: Philip Klevestav <philipk at philipk.net>
 License: CC-BY-3.0
 
+Files: data/particles/bullet.png
+       data/particles/energy.png
+       data/particles/scorch.png
+       data/particles/blob.png
+Copyright: public-domain
+License: public-domain
+ public-domain sources
+
 Files: data/particles/smoke.png data/particles/explosion.png
 Copyright: RaZgRiZ
 License:
@@ -183,6 +189,7 @@ License:
  author is credited for the original work.
 
 Files: data/particles/blood.png
+       data/particles/fire.png
 Copyright: FischKopF
 License: public-domain
  Created by FischKopF, source: http://www.quadropolis.us/node/2693
@@ -198,26 +205,45 @@ Copyright: 2009, Georges 'TRaK' Grondin
            http://trak.mercenariesguild.net/
 License: Expat
 
-Files: data/models/weapons/plasma/* data/models/weapons/flamer/*
-       data/models/actors/turret/*
+Files: data/models/weapons/flamer/* data/models/actors/turret/*
 Copyright: Mike Ledger ( mikeplus[i] )
 License: CC-BY-SA-3.0-AU
 
+Files: data/models/weapons/plasma/* data/models/weapons/zapper/*
+Copyright: Unnamed <Viktor.Hahn at web.de>
+License: CC-BY-SA-3.0-AU
+
 Files: data/models/john/*
 Copyright: John_III
 License: CC-BY-SA-3.0
 
+Files: data/models/acerspyro/*
+Copyright: Maxim "acerspyro" Therrien
+License: CC-BY-SA-3.0
+
 Files: data/mayhem/*
 Copyright: Mayhem
 License: CC-BY-SA-3.0
 
+Files: data/models/vanities/antenna/* data/models/vanities/blade/*
+       data/models/vanities/boots* data/models/vanities/chestplate/*
+       data/models/vanities/disc/* data/models/vanities/mask/*
+       data/models/vanities/microeye/* data/models/vanities/prongs/*
+       data/models/vanities/shoulder2/* data/models/vanities/shoulder3/*
+Copyright: Unnamed <Viktor.Hahn at web.de>
+License: CC-BY-SA-3.0
+
+Files: data/nobiax/*
+Copyright: Nobiax
+License: CC0-1.0
+
 Files: data/fonts/Play*.ttf
 Copyright: 2011, Jonas Hecksher, Playtypes, e-types AS (e-types.com)
 License: OFL-1.1
 Comment:
  Reserved Font Names "Play", "Playtype", "Playtype Sans".
 
-Files: data/fonts/*.png
+Files: data/fonts/play*.png
 Copyright: 2011, Jonas Hecksher, Playtypes, e-types AS (e-types.com)
            2011, Red Eclipse Team
 License: OFL-1.1
@@ -226,6 +252,91 @@ Comment:
  .
  These font image grids are generated from the "Play" font via cube2font.
 
+Files: data/fonts/akashi*.ttf
+Copyright: 2007-2013 Ten by Twenty
+License: OFL-1.1
+Comment:
+ I, Martin Erik Werner ("arand") have had a longer email discussion with Ed
+ Merrit (the Author of Akashi) regarding the license of the Akashi font
+ available from Ten By Twenty, and the conclusion is that the Akashi font is
+ distibuted under the SIL Open Font License (OFL) 1.1
+ Copyright © 2007 - 2013 Ten by Twenty.
+ .
+ The concluding bits from the email conversation are reproduced below:
+ .
+ Message-ID: <517F948C.8040909 at edmerritt.com>
+ Date: Tue, 30 Apr 2013 10:53:16 +0100
+ From: Ed Merritt <ed at edmerritt.com>
+ To: Martin Erik Werner <martinerikwerner at gmail.com>
+ Subject: Re: Fwd: akashi license
+ References: <4EB54D50.30708 at gmail.com> <4EB54DF3.30105 at edmerritt.com>
+           <1364335792.6577.62.camel at mas> <1367096373.20993.29.camel at mas>
+ In-Reply-To: <1367096373.20993.29.camel at mas>
+ .
+ Hi Martin,
+ I've updated the footer of the site to state that all fonts are
+ distributed under the SIL Open Font License.
+ .
+ Ed
+ .
+ .
+ .
+ On 27/04/2013 21:59, Martin Erik Werner wrote:
+ > Hi,
+ >
+ > I know I'm nagging a bit :) But could you have a look at the questions
+ > in my last email below?
+ >
+ > We'd like to know where to go with our usage of Akashi and things kind
+ > of depend on whether or not we can get a clarification of the license.
+ >
+ > Pretty please? :)
+ .
+ Message-ID: <1364335792.6577.62.camel at mas>
+ Subject: Re: Fwd: akashi license
+ From: Martin Erik Werner <martinerikwerner at gmail.com>
+ To: Ed Merritt <ed at edmerritt.com>
+ Date: Tue, 26 Mar 2013 23:09:52 +0100
+ In-Reply-To: <4EB54DF3.30105 at edmerritt.com>
+ References: <4EB54D50.30708 at gmail.com> <4EB54DF3.30105 at edmerritt.com>
+ .
+ Hi Ed,
+ .
+ (I was using the alias "Arand Nash" with the ienorand at gmail.com email
+ previously. I've since switched over to using the birth name)
+ .
+ We are currently using your typeface "Akashi" for several pieces of
+ "word art" and in the logo of the Free and Open Source FPS game Red
+ Eclipse <http://redeclipse.net>.
+ .
+ It's an awesome font which we really like! :)
+ .
+ I previously contacted you regarding a clarification of the font
+ license, since we tend to be a bit picky with our licenses in order to
+ remain fully open source, the clarification I got at that point we
+ deemed fine.
+ .
+ However, it seems that some distributions, into which we would very much
+ like Red Eclipse to be accepted, wants a more formal license for things
+ (explicitly allowing use, modification, and redistribution with or
+ without modification).
+ .
+ (The discussion in question is on a bug report for the game package in
+ the linux distribution Fedora:
+ https://bugzilla.redhat.com/show_bug.cgi?id=895947)
+ .
+ Would it be possible to get a more formal clarification of the license
+ for the Akashi font covering those bits, or more preferably, would it be
+ possible to use the Akashi font under a well-known license like the
+ OFL-1.1, since this would make sure to avoid all the misinterpretation
+ pitfalls of a custom-written license.
+ .
+ Thanks!
+ --
+ Martin Erik Werner "arand" <martinerikwerner at gmail.com>
+ .
+ <removed previous quoted email conversation>
+
 Files: data/textures/lava.jpg
 Copyright: RaZgRiZ
 License:
@@ -233,19 +344,10 @@ License:
  Licensed publicly for everyone to use, modify and distribute as long as the
  author is credited for the original work.
 
-Files: data/textures/rewards/*
-Copyright: Ed Merrit, Ten by Twenty, <http://www.tenbytwenty.com/>
-           2011, Red Eclipse Team
-License: Akashi-Font and CC-BY-SA-3.0+
-Comment:
- The Akashi font is used as a basis for the word art in these images.
-
 Files: data/textures/logo.png data/luckystrike/pub2* data/luckystrike/pub1*
 Copyright: Ed Merrit, Ten by Twenty, <http://www.tenbytwenty.com/>
            2011, Red Eclipse Team
-License: Akashi-Font and CC-BY-SA-3.0+ and Mark-Policy
-Comment:
- The Akashi font is used as a basis for the word art in this image.
+License: CC-BY-SA-3.0+ and Mark-Policy
 
 Files: data/textures/emblem.png
 Copyright: 2011, Red Eclipse Team
@@ -256,6 +358,10 @@ Copyright: 2007-2008, Torley Linden
            2010, Red Eclipse Team
 License: CC-BY-SA-3.0
 
+Files: data/unnamed/*
+Copyright: 2013-2014, Unnamed <Viktor.Hahn at web.de>
+License: CC-BY-SA-3.0
+
 Files: data/sounds/ambience/*
 Copyright: Quinton "quin" Reeves, Alex "blackshoe" Anguix
 License: CC-BY-SA-3.0
@@ -375,6 +481,17 @@ Files: data/sounds/ambience/vapour.ogg
 Copyright: 2013 D.a.M.i.E.n. <daymeenn at gmail.com>
 License: CC-BY-SA-3.0
 
+Files: data/sounds/music/track_01.ogg to data/sounds/music/track_12.ogg
+Copyright: Copyright Derek "JoJo" Stegall
+License: CC-BY-SA-3.0
+
+Files: data/sounds/music/track_13.ogg data/maps/tonatiuh.ogg
+Copyright: Copyright yd
+License: CC0-1.0
+Comment:
+ This track originates from:
+ http://opengameart.org/content/galactic-temple
+
 License: Zlib
  This software is provided 'as-is', without any express or implied
  warranty. In no event will the authors be held liable for any damages
@@ -414,84 +531,6 @@ License: Expat
  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  DEALINGS IN THE SOFTWARE.
 
-License: Akashi-Font
- Basically, you can use the fonts for whatever you like, the only
- restriction being that:
- If the font is a "paid" one, rather than a free one, you should not make
- the .ttf file available to the public.
- .
- Other than that I'm happy for you use the font however you like.
- .
- Note:
- The "akashi" typeface (available from http://www.tenbytwenty.com/akashi.php) is
- made available for free by the author.
- .
- Background:
- I ("Arand") contacted Ed Merrit (of Ten by Twenty) via email, requesting a
- clarification for the font license, here is a copy of the response:
- .
- Date: Sat, 05 Nov 2011 14:53:39 +0000
- From: Ed Merritt <ed at edmerritt.com>
- To: Arand Nash <ienorand at gmail.com>
- Subject: Re: Fwd: akashi license
- .
- Hi Arand,
- .
- Basically, you can use the fonts for whatever you like, the only
- restriction being that:
- If the font is a "paid" one, rather than a free one, you should not make
- the .ttf file available to the public.
- .
- Other than that I'm happy for you use the font however you like.
- .
- Ed
- .
- .
- .
- .
- On 05/11/2011 14:50, Arand Nash wrote:
- > Hello, sorry, but would you have time to have a look at these questions?
- >
- > I noticed that you've released the typeface "Jura" under a SIL license,
- > would it be at all feasible, to use the Akashi font under a similar license?
- > I completely understand if this is not possible.
- >
- > I'm still looking for an answer regarding the current license, though:
- >
- > -------- Original Message --------
- > Subject: akashi license
- > Date: Fri, 14 Oct 2011 01:59:44 +0200
- > From: Arand Nash<ienorand at gmail.com>
- > To: ed at edmerritt.com
- >
- > Hello Ed Merrit,
- >
- > I've got a few questions about the license, set for the Ten by twenty
- > products, in particular for the akashi.ttf font:
- > ###
- > By downloading any product from Ten by Twenty, you agree to the following:
- >
- > -All products remain property of Ten by Twenty.
- > -Products may be used by the licensee in any personal or commercial
- > projects (royalty-free).
- > -Products may not be resold or redistributed.
- > ###
- > It is somewhat unclear in what way it may, and may not be used.
- >
- > What is meant by the "used by licensee" wording in this context?
- > Does it mean strictly personal, "off-line" use only?
- > Or displaying (publicly) of the font only (with no access by viewer to
- > .ttf file, somehow)?
- >
- > The "may not be (...) redistributed" seems to imply that one may only
- > receive the .ttf file from your site and that any other propagation of
- > the font must be in some hardcoded-into-image format?
- >
- > Is my interpretation of the main intent of your license correct?
- >
- > --
- > Martin Werner ("arand")
-
 License: OFL-1.1
  -----------------------------------------------------------
  SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
@@ -1455,214 +1494,6 @@ License: CC-BY-SA-3.0-AU
  The construction, validity and performance of this Licence shall be governed by
  the laws in force in the Australian Capital Territory, Australia.
 
-License: CC-BY-3.0-US
- THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
- COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
- COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
- AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
- .
- BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE
- BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE
- CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE
- IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
- .
- 1. Definitions
- .
-  a. "Collective Work" means a work, such as a periodical issue, anthology or
-     encyclopedia, in which the Work in its entirety in unmodified form, along
-     with one or more other contributions, constituting separate and independent
-     works in themselves, are assembled into a collective whole. A work that
-     constitutes a Collective Work will not be considered a Derivative Work (as
-     defined below) for the purposes of this License.
-  b. "Derivative Work" means a work based upon the Work or upon the Work and
-     other pre-existing works, such as a translation, musical arrangement,
-     dramatization, fictionalization, motion picture version, sound recording,
-     art reproduction, abridgment, condensation, or any other form in which the
-     Work may be recast, transformed, or adapted, except that a work that
-     constitutes a Collective Work will not be considered a Derivative Work for
-     the purpose of this License. For the avoidance of doubt, where the Work is
-     a musical composition or sound recording, the synchronization of the Work
-     in timed-relation with a moving image ("synching") will be considered a
-     Derivative Work for the purpose of this License.
-  c. "Licensor" means the individual, individuals, entity or entities that
-     offers the Work under the terms of this License.
-  d. "Original Author" means the individual, individuals, entity or entities who
-     created the Work.
-  e. "Work" means the copyrightable work of authorship offered under the terms
-     of this License.
-  f. "You" means an individual or entity exercising rights under this License
-     who has not previously violated the terms of this License with respect to
-     the Work, or who has received express permission from the Licensor to
-     exercise rights under this License despite a previous violation.
- .
- 2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or
- restrict any rights arising from fair use, first sale or other limitations on
- the exclusive rights of the copyright owner under copyright law or other
- applicable laws.
- .
- 3. License Grant. Subject to the terms and conditions of this License, Licensor
- hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the
- duration of the applicable copyright) license to exercise the rights in the
- Work as stated below:
- .
-  a. to reproduce the Work, to incorporate the Work into one or more Collective
-     Works, and to reproduce the Work as incorporated in the Collective Works;
-  b. to create and reproduce Derivative Works provided that any such Derivative
-     Work, including any translation in any medium, takes reasonable steps to
-     clearly label, demarcate or otherwise identify that changes were made to
-     the original Work. For example, a translation could be marked "The original
-     work was translated from English to Spanish," or a modification could
-     indicate "The original work has been modified.";;
-  c. to distribute copies or phonorecords of, display publicly, perform
-     publicly, and perform publicly by means of a digital audio transmission the
-     Work including as incorporated in Collective Works;
-  d. to distribute copies or phonorecords of, display publicly, perform
-     publicly, and perform publicly by means of a digital audio transmission
-     Derivative Works.
-  e. For the avoidance of doubt, where the Work is a musical composition:
- .
-      i. Performance Royalties Under Blanket Licenses. Licensor waives the
-         exclusive right to collect, whether individually or, in the event that
-         Licensor is a member of a performance rights society (e.g. ASCAP, BMI,
-         SESAC), via that society, royalties for the public performance or
-         public digital performance (e.g. webcast) of the Work.
-     ii. Mechanical Rights and Statutory Royalties. Licensor waives the
-         exclusive right to collect, whether individually or via a music rights
-         agency or designated agent (e.g. Harry Fox Agency), royalties for any
-         phonorecord You create from the Work ("cover version") and distribute,
-         subject to the compulsory license created by 17 USC Section 115 of the
-         US Copyright Act (or the equivalent in other jurisdictions).
-  f. Webcasting Rights and Statutory Royalties. For the avoidance of doubt,
-     where the Work is a sound recording, Licensor waives the exclusive right to
-     collect, whether individually or via a performance-rights society (e.g.
-     SoundExchange), royalties for the public digital performance (e.g. webcast)
-     of the Work, subject to the compulsory license created by 17 USC Section
-     114 of the US Copyright Act (or the equivalent in other jurisdictions).
- .
- The above rights may be exercised in all media and formats whether now known or
- hereafter devised. The above rights include the right to make such
- modifications as are technically necessary to exercise the rights in other
- media and formats. All rights not expressly granted by Licensor are hereby
- reserved.
- .
- 4. Restrictions. The license granted in Section 3 above is expressly made
- subject to and limited by the following restrictions:
- .
-  a. You may distribute, publicly display, publicly perform, or publicly
-     digitally perform the Work only under the terms of this License, and You
-     must include a copy of, or the Uniform Resource Identifier for, this
-     License with every copy or phonorecord of the Work You distribute, publicly
-     display, publicly perform, or publicly digitally perform. You may not offer
-     or impose any terms on the Work that restrict the terms of this License or
-     the ability of a recipient of the Work to exercise the rights granted to
-     that recipient under the terms of the License. You may not sublicense the
-     Work. You must keep intact all notices that refer to this License and to
-     the disclaimer of warranties. When You distribute, publicly display,
-     publicly perform, or publicly digitally perform the Work, You may not
-     impose any technological measures on the Work that restrict the ability of
-     a recipient of the Work from You to exercise the rights granted to that
-     recipient under the terms of the License. This Section 4(a) applies to the
-     Work as incorporated in a Collective Work, but this does not require the
-     Collective Work apart from the Work itself to be made subject to the terms
-     of this License. If You create a Collective Work, upon notice from any
-     Licensor You must, to the extent practicable, remove from the Collective
-     Work any credit as required by Section 4(b), as requested. If You create a
-     Derivative Work, upon notice from any Licensor You must, to the extent
-     practicable, remove from the Derivative Work any credit as required by
-     Section 4(b), as requested.
-  b. If You distribute, publicly display, publicly perform, or publicly
-     digitally perform the Work (as defined in Section 1 above) or any
-     Derivative Works (as defined in Section 1 above) or Collective Works (as
-     defined in Section 1 above), You must, unless a request has been made
-     pursuant to Section 4(a), keep intact all copyright notices for the Work
-     and provide, reasonable to the medium or means You are utilizing: (i) the
-     name of the Original Author (or pseudonym, if applicable) if supplied, and/
-     or (ii) if the Original Author and/or Licensor designate another party or
-     parties (e.g. a sponsor institute, publishing entity, journal) for
-     attribution ("Attribution Parties") in Licensor's copyright notice, terms
-     of service or by other reasonable means, the name of such party or parties;
-     the title of the Work if supplied; to the extent reasonably practicable,
-     the Uniform Resource Identifier, if any, that Licensor specifies to be
-     associated with the Work, unless such URI does not refer to the copyright
-     notice or licensing information for the Work; and, consistent with Section
-     3(b) in the case of a Derivative Work, a credit identifying the use of the
-     Work in the Derivative Work (e.g., "French translation of the Work by
-     Original Author," or "Screenplay based on original Work by Original
-     Author"). The credit required by this Section 4(b) may be implemented in
-     any reasonable manner; provided, however, that in the case of a Derivative
-     Work or Collective Work, at a minimum such credit will appear, if a credit
-     for all contributing authors of the Derivative Work or Collective Work
-     appears, then as part of these credits and in a manner at least as
-     prominent as the credits for the other contributing authors. For the
-     avoidance of doubt, You may only use the credit required by this Section
-     for the purpose of attribution in the manner set out above and, by
-     exercising Your rights under this License, You may not implicitly or
-     explicitly assert or imply any connection with, sponsorship or endorsement
-     by the Original Author, Licensor and/or Attribution Parties, as
-     appropriate, of You or Your use of the Work, without the separate, express
-     prior written permission of the Original Author, Licensor and/or
-     Attribution Parties.
- .
- 5. Representations, Warranties and Disclaimer
- .
- UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS
- THE WORK AS-IS AND ONLY TO THE EXTENT OF ANY RIGHTS HELD IN THE LICENSED WORK
- BY THE LICENSOR. THE LICENSOR MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
- KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING,
- WITHOUT LIMITATION, WARRANTIES OF TITLE, MARKETABILITY, MERCHANTIBILITY,
- FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR
- OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT
- DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED
- WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
- .
- 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN
- NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL,
- INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS
- LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE
- POSSIBILITY OF SUCH DAMAGES.
- .
- 7. Termination
- .
-  a. This License and the rights granted hereunder will terminate automatically
-     upon any breach by You of the terms of this License. Individuals or
-     entities who have received Derivative Works (as defined in Section 1 above)
-     or Collective Works (as defined in Section 1 above) from You under this
-     License, however, will not have their licenses terminated provided such
-     individuals or entities remain in full compliance with those licenses.
-     Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
-  b. Subject to the above terms and conditions, the license granted here is
-     perpetual (for the duration of the applicable copyright in the Work).
-     Notwithstanding the above, Licensor reserves the right to release the Work
-     under different license terms or to stop distributing the Work at any time;
-     provided, however that any such election will not serve to withdraw this
-     License (or any other license that has been, or is required to be, granted
-     under the terms of this License), and this License will continue in full
-     force and effect unless terminated as stated above.
- .
- 8. Miscellaneous
- .
-  a. Each time You distribute or publicly digitally perform the Work (as defined
-     in Section 1 above) or a Collective Work (as defined in Section 1 above),
-     the Licensor offers to the recipient a license to the Work on the same
-     terms and conditions as the license granted to You under this License.
-  b. Each time You distribute or publicly digitally perform a Derivative Work,
-     Licensor offers to the recipient a license to the original Work on the same
-     terms and conditions as the license granted to You under this License.
-  c. If any provision of this License is invalid or unenforceable under
-     applicable law, it shall not affect the validity or enforceability of the
-     remainder of the terms of this License, and without further action by the
-     parties to this agreement, such provision shall be reformed to the minimum
-     extent necessary to make such provision valid and enforceable.
-  d. No term or provision of this License shall be deemed waived and no breach
-     consented to unless such waiver or consent shall be in writing and signed
-     by the party to be charged with such waiver or consent.
-  e. This License constitutes the entire agreement between the parties with
-     respect to the Work licensed here. There are no understandings, agreements
-     or representations with respect to the Work not specified here. Licensor
-     shall not be bound by any additional provisions that may appear in any
-     communication from You. This License may not be modified without the mutual
-     written agreement of the Licensor and You.
-
 License: Mark-Policy
  THE RED ECLIPSE MARK POLICY
  .
@@ -1760,7 +1591,7 @@ License: Mark-Policy
  2. Modify this policy to preserve the rights of Red Eclipse, the public, the
     community, and the users.
     The latest version of the Red Eclipse Mark Policy should be available at
-    <http://www.redeclipse.net/wiki/Trademark_Policy>
+    <http://redeclipse.net/wiki/Trademark_Policy>
  .
  3. Grant exceptions to this policy, of any kind and for any reason whatsoever,
     other clauses notwithstanding.
diff --git a/doc/announce.txt b/doc/announce.txt
new file mode 100644
index 0000000..44ff917
--- /dev/null
+++ b/doc/announce.txt
@@ -0,0 +1,13 @@
+Red Eclipse, a fun-filled new take on the casual first person arena shooter, makes an impressive return with the release of version 1.5, codenamed "Aurora Edition". The solid dedication of the Red Eclipse team and its thriving community has led to several new ideas to enrich gameplay; 
+
+* New weapon: electrocute your foes and reduce their agility with the all-purpose zapper.
+* New models: accessorize with new vanity items and test a more refined and colorful arsenal of weaponry.
+* Freestyle: explore different areas of maps and reach new heights with the freestyle mutator; experience the thrill unlimited impulse can offer.
+* Race mode: test your parkour experience on a variety of new courses in the revamped Race mode including new mutators; finish more laps than opponents or race against the clock.
+* New deathmatch maps: with more levels to explore than ever before, discover unfamiliar terrain as well as some remixed and improved favorites.
+* Further improved menus, GUIs, variables and gameplay options: navigate quickly and take control of all aspects of gameplay or get help from the community when needed.
+* Improved weapon balance, team balance, bugs fixed, and much more.
+
+With an ever-growing player base and a devotion to the open source community it was born from, Red Eclipse is both the casual arena shooter enthusiasts will love and the fun dynamic environment for the avid indie gamer. As a constantly evolving project with contributions from all across the globe, any community member could potentially become an imperative part of that growth. The Red Eclipse team wishes to thank all its supporters, old and new, for their contributions that keep this ven [...]
+
+As always, you can download Red Eclipse from http://redeclipse.net/
\ No newline at end of file
diff --git a/doc/changelog.txt b/doc/changelog.txt
index e0294a9..682a97c 100644
--- a/doc/changelog.txt
+++ b/doc/changelog.txt
@@ -1,3 +1,213 @@
+The development version changelog is an ongoing work and should be reasonably
+complete covering changes made up to revision:
+
+6554 (05/30/2014)
+
+If you update the changelog with changes made beyond this point, and consider
+it to resonably cover all previous changes, please update the above revision
+number.
+
+* Only add items which you think are interesting to others, ignore those which
+  you think might not be
+
+* Please do create, update, remove and improve any entries and section titles
+  as needed
+
+* In general The ordering of sections is intented to keep items which are
+  interesting to players at the top, followed by modders, server admins, etc.
+  - The ordering of the entries themselves is arbitrary, loosely chronological
+
+* The "Additional fixed bugs" section is intended to cover all items (however
+  small) which have a ticket number that have been closed and
+  - Are bugs which existed in the previous release which have now been fixed
+  or
+  - Are small enough to not warrant a note in any of the other sections
+
+* For purely numerical changes to variables it can be convenient to use the
+  instructions given at http://redeclipse.net/wiki/Vars_Changes to get a
+  before-after view
+
+* This and all the above changelog hints should be removed prior to release
+
+
+= Red Eclipse Development version =
+Gameplay:
+ * Ragdolls now twitch when they're under the influence of shock residual
+ * Show player distance when fragged
+ * Show throwing distance in Bomber-Ball when scored
+ * Bomber-Ball throwing originates from head for more consistent throwing
+ * Added /playerhint variable to make players have a team colored glow
+   over them to make it more clear to which team they belong
+ * Improved AI for CTF and DAC games, making bots more evasive when endangered
+ * Support for giving AI orders ("defend", "attack", "base", "goal", "ball", "forget")
+ * Better physics and bounce sounds for affinities
+ * Enable interactions with affinities
+ * Footstep sounds
+ * Fixed spawn queue in Time-Trial
+ * Auto team balancing
+
+Weapons:
+ * Sword damage and secondary attack speed increased,
+   now uses negative kickpush to propel the player forward slightly
+ * Decrease pistol firing rate to disable benefits of auto-fire scripts
+ * Rifle has faster reload time, slightly increased wavepush,
+   slightly higher delay between shots
+ * Flamer has higher ammo capacity, secondary fire changed to airblast,
+   used to damage enemies and put out teammates that are on fire
+ * Grenades have smaller explode radius, back to v1.3 behaviour
+ * Ability to cancel a cook event (e.g. grenades) by pressing reload
+ * Set all weapon selfdamage to 100%
+ * Increased weapon spread when in air / impulsing
+
+Modes & Mutators:
+ * Rebranded Defend-The-Flag to Defend-And-Control
+ * Disable debris in Kaboom
+ * Always pick one spawn for alpha team in Gauntlet
+ * Resize more in Resize
+ * Steal more life in Vampire
+ * Increased buffing timeout tolerances when defending affinities
+ * Changed jetpack mutator into freestyle mutator
+ * Changed expert mutator into tourney mutator
+
+Maps:
+ * Time-trial
+   - Absorption - new time-trial map by Unnamed
+   - Amplification - new time-trial map by Unnamed
+   - Relax - new time-trial map by Bonifarz
+   - Steel Rat - added to time-trial rotation
+   - Testchamber - retextured by Quinton Reeves
+   - War Depot - new time-trial map by TristamK
+ * Other game modes
+   - Abuse - new map by ballist1c
+   - Campgrounds - new map by ballist1c and Greaserpirate
+   - Canyon - new map by restcoser
+   - Condensation - new map by Unnamed
+   - Decay by Ricky Thomson
+   - Echo2k - remix of Echo by Ulukai
+   - Enyo - new map by Ulukai
+   - Octavus - new map by Ricky Thomson
+ * Out of rotation
+   - Pumpstation
+   - Star Libido
+   - Echo
+ * Maps for review (WIP): 
+   - Affluence by Greaserpirate
+
+Interface & Menus:
+ * Added /spectvfollow for aiming camera to certain player whilst
+   maintaining normal spectv camera selections
+ * Added variables for showing distance and health left when killed
+   (/showobitdists /showobithpleft)
+ * Added /nogore variable to disable gore effects
+ * Added third person key to controls menu
+ * Added higher resolutions to display menu
+ * Better inventory score
+ * Added borders on screen while waiting / spectating,
+   these can be controlled from options menu
+ * Fancy keyboard / mouse button rendering
+ * Ability to cancel a swapteam request
+ * Slicker menus, tips and rollover help text
+ * Scoreboard redesign and better colouring
+ * New loading screen
+ * Map preview size bumped to 512x512
+ * Bomber-Ball in inventory and its itembar flash red
+ * Added "Sharpen" shader to postfx menu
+ * New variable menu with search capabilities
+
+Modding & Mapmaking:
+ * Updated edit menu
+ * When selecting an entity, its properties are now numbered
+ * Added /moveselwaypoints to move selected waypoints around
+ * Specscale of pk02 textures corrected
+ * Glow colours inherit texture palette if not defined in shader parameters
+ * Cubescript commands for retrieving entity information
+ * 0 + mousewheel = entproperty 9
+ * Ability to read the color of the pixel under crosshair by pressing "\"
+
+Data Assets:
+ * New vanity items
+   - Blade
+   - Boots
+   - Chestplate
+   - Crown
+   - Disc
+   - Fox Ears
+   - Guitar
+   - Mask
+   - Microeye
+   - Panda
+   - Prongs
+   - Shoulder armor
+ * New wakawaka sound
+ * New model for control points used in defend-and-control and Bomber-Ball
+ * New Rocket projectile by Unnamed
+ * Added texture set by Unnamed
+ * Added lava texture by Nobiax, applied in maps Hinder, Nova, Tower
+ * New mapmodels by Nobiax
+   - Plastic barrel, cardboard, pipes, trash can
+   - Ivy, weeds, grasses, bushes, tree, snowpine, cactus, rocks
+   - Fuel can, gaz tank
+   - Concrete barriers
+
+Multiplayer & Servers:
+ * Show time since last server ping reply
+ * Added chat filter
+ * Wait for players to be ready before starting the game
+ * Players can now request a username for use in the profile menu
+   to prevent other players impersonating you,
+   see http://forum.freegamedev.net/viewtopic.php?f=72&t=4340
+ * Pause server sorting while hovering over items in the server list
+ * Added a "server type" field to the maps menu when offline to change
+   servertype to simplify setting up local games
+ * Players will have to accept guidelines before playing online
+
+Core & Install:
+ * Fixed desktop entry icon field
+ * Improve IRC connection stability
+ * ENet updated to v1.3.12
+ * Removed Mekarcade from the project
+
+Additional noteworthy fixed bugs:
+ * #449 - Ability to make current settings default (/savevars)
+ * #505 - Picking up an item instantly completes reload cycle
+ * #506 - Garbled output when selecting weapon loadout
+ * #509 - Users with account info are denied access to password-protected servers
+ * #510 - server dependency on data
+ * #520 - Start menu icon has the wrong .exe selected
+ * #524 - redeclipse.net shortlink for files on trac
+ * #540 - On maps with too little goals for bomber-ball
+          switching to hold fails when touchdown is selected
+ * #545 - Time-trial "dnf" ranks ahead of any completed time in final standings
+ * #549 - Allow remaining time to be shown on HUD
+ * #556 - Time-trial time not displayed in top-right of screen
+ * #558 - Scoring anomalies
+ * #563 - Duel displays a comma too much in match info
+ * #564 - Correct initialising of affinities at game start
+ * #569 - Incorrect grenade collision
+ * #574 - Scoring events can take place after the match is over
+ * #575 - GUIs can be used to cheat
+ * #576 - After dying in resize mode, camera move speed is affected by player size
+ * #577 - Disconnecting while alive in a survivor game leads to excessive scores
+ * #581 - vpalette correction for multi team palettes in 2-team games
+ * #589 - Nickname is overlayed by time in ffa-timetrial
+ * #590 - Sort players in player list by scoreboard standings
+ * #591 - Time overlays time in time-trial
+ * #597 - Recently played or out-of-rotation maps cannot be voted
+ * #599 - Multiple teamkill adjustments by the same player in the same match
+          are overcompensated
+ * #601 - Plasma particle spawns too close to screen
+ * #610 - Spectate in time-trial without losing checkpoints
+ * #613 - Bug when selecting "demo" mode during or after another game
+ * #615 - Remove "with 1 health left" in instagib duel
+ * #617 - When a shot hits multiple players, only one pop-up number appears
+ * #618 - Standing on a teleport exit causes death
+ * #620 - Local mod privileges with normal registered user keys
+ * #628 - Various entity properties lost when added to clipboard in editmode
+ * #636 - Survivor first match bots only
+ * #640 - Mines can be thrown inside walls to frag everyone on the map
+ * #648 - onslaught enemies are handled inconsistently under weapon-forcing mutators
+
+
 = Red Eclipse 1.4 =
 Gameplay:
  * Radar now always tracks players who are dominating you, and shows everyone
@@ -171,7 +381,6 @@ Core & Install:
  * Added IRC guidelines
  * Client now uses log.txt in the homedir by default
  * Moved binary files to bin/{amd64,x86}/ subdirectories
- * MekArcade source & data tree merged in
  * Enet 1.3.7
  * Workaround for Intel/Apple occlusion bug
  * Network encoding functions moved into tools
diff --git a/doc/examples/servinit.cfg b/doc/examples/servinit.cfg
index 267a56e..5a51d25 100644
--- a/doc/examples/servinit.cfg
+++ b/doc/examples/servinit.cfg
@@ -1,8 +1,22 @@
 // This file controls server side variables which control the operation of the server.
 // To change a setting, remove the "// " in front of it and change the value.
 //
-// serverpass "p4ssw0rd" // server password required to successfully connect
-// adminpass "4dm1np4ss" // server password for administrators (/setmaster password)
+// serverpass "p4ssw0rd" // server password required to successfully connect, used for private servers
+//
+// There are two ways of setting local privileged access:
+//
+// [1]
+// adminpass "4dm1np4ss" // server password for administrators (change the password to your own, of course)
+//       to gain access this way, go in game, connect to your server, and use /setmaster 4dm1np4ss
+//
+// [2]
+// addlocalop qreeves a
+//       to set access this way, change "qreeves" to the auth name of the player you want to give access to
+//       and possibly change the last letter "a" depending on the access level:
+//             a - administrator - can change security settings and anything else
+//             o - operator - can change game variables
+//             m - moderator - can kick and ban
+//             s - supporter - no special privileges, but gets a special icon to identify them as friends, teammates, etc.
 //
 // addban ip.address // adds to the ban list
 // addallow ip.address // adds to the allow list
@@ -13,7 +27,7 @@ if (= $rehashing 0) [
     // servertype 3 // type of server, 1 = private (does not register with masterserver), 2 = public, 3 = dedicated
     // serveruprate 0 // maximum upload speed; cmdline: -suN
     // serverip "127.0.0.1" // host which server binds to; cmdline: -siN
-    // servermaster "lifremont.mncs.info" // host server tries to use as master by default; cmdline: -smS
+    // servermaster "play.redeclipse.net" // host server tries to use as master by default; cmdline: -smS
     // serverport 28801 // port which server binds to (you must open this port [UDP] and this plus one, default 28801 and 28802); cmdline: -spN
     // servermasterport 28800 // master server port which server *connects* to; cmdline: -saN
     //
@@ -42,8 +56,6 @@ if (= $rehashing 0) [
     // masterport 28800 // port which master server binds to; cmdline: -mpN
     //
 ]
-// addauth handle [a/m/u] publickey // adds a user to /auth list for the master server, keys may be generated with regenkey
-//
 //
 // These are server side variables which influence gameplay (which must be prefixed with 'sv_')
 // Vars set here acts as defaults which are applied on start, and whenever 'resetvarsonend' tells it to
@@ -54,12 +66,39 @@ if (= $rehashing 0) [
 // sv_serveropen 3 // determines server openness for public use; 0 = allow "setpriv 1" and locked/private, 1 = allow "setpriv 1" but no privileged mode, no locked/private, 2 = allow "setpriv 1" but disallows private privileged mode (for public coop-editing), 3 = privilege only by moderator or above
 // sv_autoadmin 0 // determines if authorities claim status by default
 // sv_airefreshdelay 1000 // delay imposed before the AI manager reorganises their setup
-// sv_modelockfilter 188 // determines the modes which are allowed to be used as dictated by modelock, convenient to set using a sum of $modebit* vars (available: editing, deathmatch, capture, defend, bomber, trial), example: (+ $modebitediting $modebitdeathmatch)
-// sv_mutslockfilter 126975 // determines the mutators which are allowed to be used as dictated by modelock, convenient to set using a sum of $mutsbit* vars (available: multi, team, coop, instagib, medieval, kaboom, duel, survivor, classic, onslaught, jetpack, vampire, expert, resize, gsp1 = first mode variation {ctf-return, dtf-quick, bomber-hold}, gsp2 = second mode variation {ctf-defend, dtf-conquer}, gsp3 = third mode variation {ctf-protect, dtf-king-of-the-hill}), example: (+ $mutsb [...]
-// sv_modelock 4 // determines at which privilege level modes are locked, according to modelocktype; 0 = off, 1 = helper, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = nobody
-// sv_mapslock 4 // determines at which privilege level maps are locked, accordigng to mapslocktype; 0 = off, 1 = helper, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = nobody
-// sv_varslock 4 // determines if vars are locked; 0 = off, 1 = helper, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = nobody
-// sv_votelock 4 // determines at which privilege level votes are locked, accordigng to votelocktype; 0 = off, 1-6 = supporter/helper/moderator/operator/admin/creator can select recently played maps, 7-13 = support/helper/moderator/operator/admin/creator only can vote, 14 = no voting
+// sv_mutslockfilter 131071 // determines the mutators which are allowed to be used
+// sv_mutslockforce 0 // determines the mutators which must be added to all games
+// sv_defaultmuts 0 // determines the default mutators to use when server is idle
+// sv_rotatemutsfilter 258 // determines the mutators which are allowed to be generated when there are no votes and the server creates a random game
+//   mutators are set as a bitmask, where each numbered bit has a corresponding alias (for example, 258 can be expressed as " (+ $mutsbitffa $mutsbitclassic) "):
+//        1 $mutsbitmulti         128 $mutsbitsurvivor
+//        2 $mutsbitffa           256 $mutsbitclassic
+//        4 $mutsbitcoop          512 $mutsbitonslaught
+//        8 $mutsbitinstagib     1024 $mutsbitfreestyle
+//       16 $mutsbitmedieval     2048 $mutsbitvampire
+//       32 $mutsbitkaboom       4096 $mutsbithard
+//       64 $mutsbitduel         8192 $mutsbitresize
+//    16384 $mutsbitgsp1 (quick capture the flag, quick defend and control, hold bomber ball, marathon time trial)
+//    32768 $mutsbitgsp2 (defend capture the flag, king defend and control, basket bomber ball, endurance time trial)
+//    65536 $mutsbitgsp3 (protect capture the flag, attack bomber ball, gauntlet time trial)
+//   131071 $mutsbitall (all of the bits added together)
+//
+// sv_modelockfilter 60 // determines the game modes which are allowed to be used
+// sv_rotatemodefilter 60 // determines the game modes which are allowed to be chosen when there are no votes and the server picks a random game
+//   modes are also set as a bitmask, with their own aliases:
+//      1 $modebitdemo    (impossible to use online, even if the modelock allows it)
+//      2 $modebitediting
+//      4 $modebitdeathmatch
+//      8 $modebitcapture (capture the flag)
+//     16 $modebitdefend  (defend and control)
+//     32 $modebitbomber  (bomber ball)
+//     64 $modebittrial   (time trial)
+//    127 $modebitall     (all of the mode bits added together)
+//
+// sv_modelock 4 // determines at which privilege level modes are locked, according to modelocktype; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = nobody
+// sv_mapslock 4 // determines at which privilege level maps are locked, accordigng to mapslocktype; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = nobody
+// sv_varslock 4 // determines if vars are locked; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = nobody
+// sv_votelock 4 // determines at which privilege level votes are locked, according to votelocktype; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = nobody
 // sv_votewait 2500 // time in milliseconds before a player may cast another vote (to avoid flooding)
 // sv_votestyle 2 // determines how mid-match votes are handled; 0 = votes don't pass mid-match, 1 = passes if votethreshold is met, 2 = passes if unanimous
 // sv_voteinterm 2 // 0 = must wait entire time, 1 = passes if votethreshold is met, 2 = passes if unanimous
@@ -69,100 +108,160 @@ if (= $rehashing 0) [
 // sv_resetmutesonend 1 // determines when the mute list is reset; 0 = off, 1 = just when empty, 2 = when matches end
 // sv_resetlimitsonend 1 // determines when the limit list is reset; 0 = off, 1 = just when empty, 2 = when matches end
 // sv_resetvarsonend 1 // determines when these game variables are reset; 0 = off, 1 = just when empty, 2 = when matches end
-// sv_demolock 4 // determines who may record demos; 0 = helper, 1 = supporter, 2 = moderator, 3 = operator, 4 = administrator, 5 = developer, 6 = creator, 7 = nobody
+// sv_demolock 4 // determines who may record demos; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
 // sv_democount 5 // determines the maximum amount of demo files that may be saved simultaneously on the server (deletes old demos if exceeded)
 // sv_demomaxsize 16 // determines the maximum size of individual demo files that may be saved on the server
 // sv_demoautorec 1 // determines if demos are automatically recorded each match
-// sv_speclock 3 // determines who may force players to spectate; 0 = helper, 1 = supporter, 2 = moderator, 3 = operator, 4 = administrator, 5 = developer, 6 = creator, 7 = nobody
-// sv_kicklock 3 // determines who may kick players; 0 = helper, 1 = supporter, 2 = moderator, 3 = operator, 4 = administrator, 5 = developer, 6 = creator, 7 = nobody
-// sv_allowlock 4 // determines who may allow players; 0 = helper, 1 = supporter, 2 = moderator, 3 = operator, 4 = administrator, 5 = developer, 6 = creator, 7 = nobody
-// sv_banlock 4 // determines who may ban players; 0 = helper, 1 = supporter, 2 = moderator, 3 = operator, 4 = administrator, 5 = developer, 6 = creator, 7 = nobody
-// sv_mutelock 3 // determines who may mute players; 0 = helper, 1 = supporter, 2 = moderator, 3 = operator, 4 = administrator, 5 = developer, 6 = creator, 7 = nobody
-// sv_limitlock 3 // determines who may limit players; 0 = helper, 1 = supporter, 2 = moderator, 3 = operator, 4 = administrator, 5 = developer, 6 = creator, 7 = nobody
-// sv_vetolock 4 // determines who may force match votes; 0 = helper, 1 = supporter, 2 = moderator, 3 = operator, 4 = administrator, 5 = developer, 6 = creator, 7 = nobody
-// sv_floodlock 4 // enables flood protection for everyone below a specific privilege level; 0 = disable flood protection, 1 = helper, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = everyone
+// sv_speclock 3 // determines who may force players to spectate; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
+// sv_kicklock 3 // determines who may kick players; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
+// sv_allowlock 4 // determines who may allow players; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
+// sv_banlock 4 // determines who may ban players; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
+// sv_mutelock 3 // determines who may mute players; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
+// sv_limitlock 3 // determines who may limit players; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
+// sv_vetolock 4 // determines who may force match votes; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
+// sv_floodlock 4 // enables flood protection for everyone below a specific privilege level; 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
 // sv_floodmute 3 // automatically mute player when warned this many times
 // sv_floodtime 10000 // time span to check for floody messages
 // sv_floodlines 5 // number of lines in floodtime span before too many
-// sv_gamespeedlock 5 // determines if gamespeed is locked (also limited by varslock); 0 = off, 1 = helper, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator, 8 = nobody
+// sv_gamespeedlock 5 // determines if gamespeed is locked (also limited by varslock); 0 = off, 1 = player, 2 = supporter, 3 = moderator, 4 = operator, 5 = administrator, 6 = developer, 7 = creator
 // sv_gamespeed 100 // override gameplay speed
 // sv_gamepaused 0 // pauses the game, automatically unset by server
 // sv_defaultmap "" // default map, "" = random
 // sv_defaultmode 2 // default game mode; 1 = editing, 2 = deathmatch, 3 = ctf, 4 = dtf, 5 = bomber, 6 = trial
-// sv_defaultmuts 0 // default mutators, convenient to set using a sum of $mutsbit* vars (available: multi, team, coop, instagib, medieval, ballistic, duel, survivor, arena, onslaught, jetpack, vampire, expert, resize, gsp1 = first mode variation {ctf-return, dtf-quick, bomber-hold}, gsp2 = second mode variation {ctf-defend, dtf-conquer}, gsp3 = third mode variation {ctf-protect, dtf-king-of-the-hill}
 // sv_rotatemode 1 // determines if modes rotate when the server selects the next map
-// sv_rotatemuts 3 // determines if mutators rotate when the server selects the next map (more than 1 decreases chances)
-// sv_rotatemodefilter 60 // determines the modes which can be selected when the server selects the next map, convenient to set using a sum of $modebit* vars (available: editing, deathmatch, capture, defend, bomber, trial), example: (+ $modebitediting $modebitdeathmatch)
-// sv_rotatemutsfilter 126399 // determines the mutators which can be selected when the server selects the next map, convenient to set using a sum of $mutsbit* vars (available: multi, team, coop, instagib, medieval, kaboom, duel, survivor, classic, onslaught, jetpack, vampire, expert, resize, gsp1 = first mode variation {ctf-return, dtf-quick, bomber-hold}, gsp2 = second mode variation {ctf-defend, dtf-conquer}, gsp3 = third mode variation {ctf-protect, dtf-king-of-the-hill}, example: (+ [...]
-// sv_allowmaps "ares bath battlefield biolytic bloodlust canals cargo center colony conflict cutec cyanide darkness deadsimple deathtrap deli depot dropzone dutility echo erosion error forge foundation fourplex futuresport ghost hawk hinder institute keystone2k linear livefire longestyard mist neodrive nova oneiroi panic processing pumpstation purge spacetech starlibido stone suspended testchamber tower tranquility tribal ubik vault venus warp wet" // determines which maps are allowed t [...]
-// sv_mainmaps "ares bath battlefield biolytic bloodlust canals cargo center colony conflict cutec darkness deadsimple deathtrap deli depot dropzone dutility echo erosion error foundation fourplex futuresport ghost institute keystone2k linear livefire longestyard mist nova oneiroi panic processing pumpstation spacetech starlibido stone suspended tower tribal ubik vault venus warp wet" // deathmatch maps
-// sv_capturemaps "ares bath battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deli depot dropzone dutility echo erosion foundation fourplex futuresport ghost institute keystone2k linear mist nova panic pumpstation stone suspended tribal vault venus warp wet" // capture-the-flag maps
-// sv_defendmaps "ares bath battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deli depot dropzone dutility echo erosion foundation fourplex futuresport ghost institute keystone2k linear livefire mist nova panic processing pumpstation stone suspended tower tribal ubik vault venus warp wet" // defend-the-flag maps
-// sv_kingmaps "ares bath battlefield biolytic canals cargo center colony conflict darkness deadsimple depot dutility echo fourplex linear livefire processing stone suspended tower tribal ubik vault venus" // king-of-the-hill maps
-// sv_bombermaps "ares battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deli depot dropzone dutility echo erosion foundation fourplex futuresport ghost linear mist nova pumpstation stone suspended tower tribal vault venus warp wet" // bomber-ball maps
-// sv_holdmaps "ares bath battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deli depot dropzone dutility echo erosion foundation fourplex futuresport ghost keystone2k linear mist nova panic processing pumpstation stone suspended tower tribal ubik vault venus warp wet" // hold bomber-ball maps
-// sv_trialmaps "cyanide hawk hinder neodrive purge testchamber" // time-trial maps
-// sv_multimaps "canals deadsimple depot keystone2k warp fourplex" // maps allowed for modes which *require* multi spawns (ctf/bb)
-// sv_duelmaps "bath bloodlust darkness deadsimple dutility echo fourplex ghost longestyard livefire stone panic vault wet" // duel map filter (extra filter on top of mode filter)
-// sv_jetpackmaps "ares battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deathtrap deli depot dropzone dutility echo erosion error forge fourplex futuresport ghost linear longestyard mist nova oneiroi pumpstation spacetech starlibido suspended testchamber tower tranquility tribal ubik venus warp" // jetpack map filter (extra filter on top of mode filter)
+// sv_rotatemuts 3 // determines if mutators rotate when the server selects the next map; 0 = never rotate mutators, 1 = always rotate mutators, >1 = decrease chances the larger this value
+// sv_allowmaps "absorption abuse affluence ares bath battlefield biolytic bloodlust campgrounds canals canyon cargo castle center colony conflict condensation convolution cutec cyanide darkness deadsimple deathtrap decay decomposition deli depot dropzone dutility echo enyo erosion error escape futuresport ghost hinder institute keystone2k linear livefire longestyard mist neodrive nova octavus oneiroi panic processing pumpstation purge relax rooftop spacetech steelrat stone suspended tes [...]
+// sv_mainmaps "abuse affluence ares bath battlefield biolytic bloodlust campgrounds canals canyon cargo castle center colony conflict condensation convolution cutec darkness deadsimple deathtrap decay deli depot dropzone dutility echo enyo erosion error futuresport ghost institute keystone2k linear livefire longestyard mist nova octavus oneiroi panic processing pumpstation rooftop spacetech stone suspended tower tribal ubik vault venus warp wet" // deathmatch maps
+// sv_capturemaps "affluence ares bath battlefield biolytic canals canyon center colony conflict condensation convolution darkness deadsimple deli depot dropzone dutility echo enyo erosion futuresport institute keystone2k linear mist nova octavus panic pumpstation stone suspended tribal vault venus warp wet" // capture-the-flag maps
+// sv_defendmaps "affluence ares bath battlefield biolytic campgrounds canals canyon cargo center colony conflict condensation convolution cutec darkness deadsimple deathtrap decay deli depot dropzone dutility echo enyo erosion futuresport ghost institute keystone2k linear livefire mist nova octavus panic processing rooftop stone suspended tower tribal ubik vault venus warp wet" // defend-and-control maps
+// sv_kingmaps "ares bath battlefield biolytic campgrounds canals canyon cargo center colony conflict condensation darkness depot dropzone dutility echo enyo linear livefire octavus processing pumpstation rooftop stone suspended tribal ubik vault venus" // king-of-the-hill maps
+// sv_bombermaps "affluence ares battlefield canals canyon cargo center conflict condensation convolution darkness deathtrap deli depot dropzone dutility echo enyo erosion futuresport linear mist nova octavus pumpstation stone suspended tower tribal vault venus warp wet" // bomber-ball maps
+// sv_holdmaps "affluence ares bath battlefield biolytic campgrounds canals canyon cargo center colony conflict condensation convolution cutec darkness deadsimple deathtrap decay deli depot dropzone dutility echo enyo erosion futuresport ghost keystone2k linear mist nova octavus panic pumpstation stone suspended tower tribal ubik vault venus warp wet" // hold bomber-ball maps
+// sv_trialmaps "absorption cyanide decomposition escape hinder neodrive purge relax steelrat testchamber tonatiuh wardepot" // time-trial maps
+// sv_multimaps "canals condensation convolution deadsimple depot keystone2k suspended warp" // maps allowed for modes which *require* multi spawns (ctf/bb)
+// sv_duelmaps "abuse bath bloodlust campgrounds canyon cargo castle darkness deadsimple dutility echo error ghost livefire longestyard mist panic stone vault wet" // duel map filter (extra filter on top of mode filter)
 // sv_rotatemaps 2 // determines behaviour of map rotation; 0 = off, 1 = sequence, 2 = random
 // sv_rotatemapsfilter 2 // 0 = off, 1 = filter based on mutators, 2 = also filter based on players
 // sv_maphistory 5 // remember this many maps that can't be voted again if votelock is set
-// sv_maxcarry 2 // maximum number of weapons a player can carry, plus pistol and grenades
+// sv_maxcarry 2 // maximum number of weapons a player can carry, plus pistol, grenades, and mines
 // sv_spawnrotate 2 // spawn point rotation; 0 = let client decide, 1 = sequence, 2 = random
 // sv_spawnweapon 1 // weapon players spawn with, defaults to pistol (1)
-// sv_instaweapon 7 // weapon players spawn with in instagib, defaults to rifle (7)
+// sv_instaweapon 8 // weapon players spawn with in instagib, defaults to rifle (8)
 // sv_trialweapon 0 // weapon players spawn with in trial, defaults to melee only (0)
 // sv_spawngrenades 0 // spawn with grenades; 0 = never, 1 = all but instagib/time-trial, 2 = always
+// sv_spawnmines 0 // spawn with mines; 0 = never, 1 = all but instagib/time-trial, 2 = always
 // sv_spawndelay 5000 // time in milliseconds before players can respawn in most modes
 // sv_instadelay 3000 // time in milliseconds before players can respawn in instagib mutated modes
 // sv_trialdelay 500 // time in milliseconds before players can respawn in trial mode
+// sv_trialdelayex 3000 // time in milliseconds before defenders can respawn in gauntlet trial mode
 // sv_spawnprotect 3000 // time in milliseconds after spawning players cannot be damaged
 // sv_duelprotect 5000 // time in milliseconds after spawning players cannot be damaged in duel/survivor matches
 // sv_instaprotect 3000 // time in milliseconds after spawning players cannot be damaged in instagib matches
-// sv_maxhealth 1.5 // spawnhealth * maxhealth defines the maximum amount of health that can be reached (e.g. standing next to a friendly goal)
-// sv_maxhealthvampire 2.0 // spawnhealth * maxhealthvampire defines the maximum amount of health that can be reached by damaging other players in vampire
+// sv_spawnhealth 100 // how much health players spawn with
+// sv_maxhealth 1.5 // spawnhealth * maxhealth defines the maximum amount of health that can be reached (e.g. standing next to your base in capture the flag)
+// sv_maxhealthvampire 3.0 // spawnhealth * maxhealthvampire defines the maximum amount of health that can be reached by damaging other players in vampire
 // sv_burntime 5500 // time in milliseconds fire burns for, try to allow an extra 500ms breathing room for sync
 // sv_burndelay 1000 // time in milliseconds for which fire burning deals damage
-// sv_burndamage 3 // amount of damage fire burning deals
+// sv_burndamage 3 // amount of damage fire burning deals at each interval
 // sv_bleedtime 5500 // time in milliseconds bleeding lasts for, try to allow an extra 500ms breathing room for sync
 // sv_bleeddelay 1000 // time in milliseconds for which bleeding deals damage
-// sv_bleeddamage 3 // amount of damage bleeding deals
+// sv_bleeddamage 3 // amount of damage bleeding deals at each interval
+// sv_shocktime 5500 // time in milliseconds shock lasts for, try to allow an extra 500ms breathing room for sync
+// sv_shockdelay 1000 // time in milliseconds for which shock deals damage
+// sv_shockdamage 2 // amount of damage shock deals at each interval
 // sv_regendelay 3000 // time in milliseconds after being damage before normal regeneration resumes
-// sv_regentime 1000 // time in milliseconds for which regenerate gives health
-// sv_regenhealth 5 // amount of health regneration gives
+// sv_regentime 1000 // time in milliseconds between regeneration intervals
+// sv_regenhealth 5 // amount of health gained per regeneration interval
+// sv_capturebuffing 9 // scenarios in which a player gets health and damage bonuses for capture the flag (bitmask):
+//      1 - standing near own flag at its base
+//      2 - standing near own loose flag (always on during defend mutator)
+//      4 - holding own flag
+//      8 - standing near teammate who is holding own flag
+//     16 - holding enemy flag
+//     32 - standing near teammate who is holding enemy flag
 // sv_captureregenbuff 1 // 0 = off, 1 = modify regeneration when buffed
 // sv_captureregendelay 1000 // regen this often when buffed
 // sv_captureregenextra 2 // add this to regen when buffed
-// sv_capturebuffdelay 1000 // buffed when guarding, and for this long after
-// sv_capturebuffdamage 1.5 // multiply outgoing damage by this much when buffed
-// sv_capturebuffshield 1.5 // divide incoming damage by this much when buffed
+// sv_capturebuffdelay 3000 // buffed when guarding, and for this long after
+// sv_capturebuffdamage 1.25 // multiply outgoing damage by this much when buffed
+// sv_capturebuffshield 1.25 // divide incoming damage by this much when buffed
+// sv_defendbuffing 1 // scenarios in which a player gets health and damage bonuses for defend and control (bitmask):
+//      1 - standing near a controlled, undisturbed area
+//      2 - standing near a neutral area your team is in the process of securing
+//      4 - standing near a controlled area, even if other teams are nearby
+// sv_defendregenbuff 1 // 0 = off, 1 = modify regeneration when buffed
+// sv_defendregendelay 1000 // regen this often when buffed
+// sv_defendregenextra 2 // add this to regen when buffed
+// sv_defendbuffdelay 1000 // buffed when guarding, and for this long after
+// sv_defendbuffdamage 1.25 // multiply outgoing damage by this much when buffed
+// sv_defendbuffshield 1.25 // divide incoming damage by this much when buffed
+// sv_bomberbuffing 1 // scenarios in which a player gets health and damage bonuses for bomber ball (bitmask):
+//      1 - standing near own base
+//      2 - holding the bomb
+//      4 - holding the bomb (defending team only, attack mutator only, and only if bomberattackreset is 0)
+// sv_bomberregenbuff 1 // 0 = off, 1 = modify regeneration when buffed
+// sv_bomberregendelay 1000 // regen this often when buffed
+// sv_bomberregenextra 2 // add this to regen when buffed
+// sv_bomberbuffdelay 1000 // buffed when guarding, and for this long after
+// sv_bomberbuffdamage 1.25 // multiply outgoing damage by this much when buffed
+// sv_bomberbuffshield 1.25 // divide incoming damage by this much when buffed
+//
 // sv_itemsallowed 2 // determines if items are present in the level; 0 = never, 1 = all but instagib, 2 = always
-// sv_itemspawntime 15000 // time in milliseconds before items (re)spawn
+// sv_itemspawntime 15000 // time in milliseconds before items (re)spawn after being picked up
 // sv_itemspawndelay 1000 // time in milliseconds after map start items first spawn
 // sv_itemthreshold 2.0 // if numitems/(players*maxcarry) is less than this, spawn one of this type
 // sv_itemspawnstyle 1 // determines the timing of item spawning at map start; 0 = all at once, 1 = staggered, 2 = random, 3 = randomise between both
-// sv_kamikaze 1 // determines the level of kamikaze events; 0 = never, 1 = holding grenade, 2 = have grenade, 3 = always
+// sv_kamikaze 1 // determines the level of kamikaze events; 0 = never, 1 = wielding grenade, 2 = have grenade in inventory, 3 = always
 // sv_timelimit 10 // time in minutes before round ends; 0 = off
+// sv_overtimeallow 1 // determines if an overtime period should be played at the end of a tied game
+// sv_overtimelimit 5 // maximum time in minutes before overtime ends; 0 = no time limit; overtime also ends immediately if the tie is ever broken
+// sv_rotatecycle 10 // time in minutes before server randomly picks a new game type when there are no players connected; 0 = do not rotate
 // sv_intermlimit 15000 // time in milliseconds intermission lasts
 // sv_votelimit 45000 // time in milliseconds intermission voting lasts
-// sv_duellimit 5000 // time in milliseconds before next round in duel/survivor
+// sv_duelcooloff 5000 // time in milliseconds before next round in duel/survivor
 // sv_duelclear 1 // determines if items are reset at the end of each round
-// sv_selfdamage 1 // determines if the player can damage themselves; 0 = off, 1 = either hurt self or use teamdamage rules
-// sv_teamdamage 1 // determines if the player can damage team members; 0 = off, 1 = non-bots damage team, 2 = all players damage team
+// sv_duelreset 1 // determines if winner of a duel round is forced to respawn (if 0, they can roam freely during the intermission between rounds)
+// sv_duelcycle 2 // determines if winner of a duel round can be forced back into the respawn queue along with the loser: 0 = no, 1 = FFA only, 2 = team games only, 3 = both
+// sv_duelcycles 2 // number of consecutive wins needed by the same player before duelcycle applies
+// sv_damageself 1 // determines if the player can damage themselves
+// sv_damageteam 1 // determines if the player can damage team members; 0 = off (and forces damageself off as well), 1 = non-bots damage team, 2 = all players damage team
 // sv_teambalance 1 // determines the method of team balancing; 0 = off, 1 = by number then rank, 2 = by rank then number
-// sv_coopmultibalance 2.0 // multiply number of players in bot teams by this much
-// sv_pointlimit 0 // number of points required to end the round (and win) in deathmatch modes
-// sv_capturelimit 0 // number of captures required to end the round (and win) in ctf
+// sv_coopbalance 1.5 // multiply number of human players by this much (rounded up) to get size of bot team in coop
+// sv_coopmultibalance 2.0 // multiply number of human players by this much to get size of each bot team in multi-coop
+// sv_pointlimit 0 // number of points required to end the round (and win) in deathmatch (0 = no limit)
+// sv_capturelimit 0 // number of captures required to end the round (and win) in ctf (0 = no limit)
 // sv_captureresetdelay 30000 // time in milliseconds before a dropped flag automatically resets
-// sv_defendlimit 0 // determines the style of dtf play; number of points required to end the round (and win) in dtf
-// sv_defendpoints 1 // number of points given in dtf
-// sv_defendoccupy 100 // points needed to occupy in regular games
-// sv_defendking 25 // points needed to occupy in king-of-the-hill
-// sv_defendflags 3 // flags to init and how; 0 = init all (neutral), 1 = init neutral and team only, 2 = init team only, 3 = init all (team + neutral + converted)
+// sv_capturedefenddelay 15000 // time in milliseconds before a dropped flag automatically resets with the defend mutator
+// sv_captureprotectdelay 15000 // time in milliseconds required to hold onto an enemy flag with the protect mutator in order to score
+// sv_capturepickupdelay 2500 // minimum time in milliseconds after picking up own flag that it can be returned to base
+// sv_captureresetpenalty 3500 // time in milliseconds that a team is not allowed to score or pick up their own flag after it resets
+// sv_captureteampenalty 7500 // same as captureresetpenalty, but applies when the flag was last held by a member of its own team
+// sv_capturecarryspeed 0.9 // multiplier of movement speed for a player who is holding any flag
+// sv_defendlimit 0 // number of points required to end the round (and win) in defend and control (0 = no limit)
+// sv_defendpoints 1 // number of points given for controlling an area in defend and control
+// sv_defendinterval 50 // time in milliseconds between "ticks" for a player near a control area
+// sv_defendoccupy 100 // number of "ticks" needed to secure or overthrow an area
+// sv_defendking 100 // number of "ticks" needed to secure in king of the hill
+// sv_defendhold 100 // number of "ticks" a control area needs to be undisturbed in order to score
+// sv_bomberlimit 0 // number of points required to end the round (and win) in bomber ball (0 = no limit)
+// sv_bomberholdlimit 0 // number of points required to end the round (and win) in hold bomber ball (0 = no limit)
+// sv_bomberbasketonly 1 // determines if touchdowns are disallowed with the basket mutator
+// sv_bomberbasketmindist 48 // if touchdowns are disallowed, also disallow throws from closer than this (8 units = 1 meter)
+// sv_bomberattackreset 1 // if 1, defenders in attack bomber ball cannot hold the bomb, and it will reset immediately if they pick it up
+// sv_bomberresetdelay 15000 // time in milliseconds before a loose bomb resets
+// sv_bomberpickupdelay 5000 // time in milliseconds before a loose bomb can be picked up by the same player who dropped it
+// sv_bombercarrytime 15000 // fuse length of the bomb in milliseconds
+// sv_bomberholdtime 15000 // fuse length of the bomb in milliseconds for hold bomber ball
+// sv_bomberholdinterval 1000 // time in milliseconds between each point in hold bomber ball
+// sv_bomberholdpenalty 10 // points lost for holding onto the bomb so long that the fuse runs out
+// sv_bombercarryspeed 0.9 // multiplier of movement speed for a player who is holding the bomb
 // sv_botskillmin 60 // minimum randomly assigned AI skill level
 // sv_botskillmax 75 // maximum randomly assigned AI skill level
-// sv_botbalance -1 // determines bot balancing method; -1 = always use numplayers, 0 = don't balance, 1 or more = fill only with this*numteams
+// sv_coopskillmin 75 // minimum randomly assigned AI skill level in coop
+// sv_coopskillmax 85 // maximum randomly assigned AI skill level in coop
+// sv_enemyskillmin 65 // minimum skill level of drones and grunts in onslaught
+// sv_enemyskillmax 80 // maximum skill level of drones and grunts in onslaught
+// sv_botbalance -1 // determines bot balancing method; -1 = always use map's numplayers value, 0 = don't balance, 1 or more = fill only with this*numteams
 // sv_botscale 1.0 // scale the 'numplayers' world variable which determines bot auto population
 // sv_botlimit 32 // maximum number of bots allowed, regardless of any other variable/setting
 // sv_enemybalance 1 // multiply number of enemy spawns by this much
@@ -175,41 +274,40 @@ if (= $rehashing 0) [
 // sv_floorcoastscale 1.0 // multiply floorcoast by this much
 // sv_aircoastscale 1.0 // multiply aircoast by this much
 // sv_slidecoastscale 1.0 // multiply slidecoast by this much
-// sv_movespeed 100.0 // baseline movement speed
+// sv_movespeed 125.0 // baseline movement speed
 // sv_movecrawl 0.6 // multiplier of speed when crawling
-// sv_movepacing 1.6 // multiplier of speed when pacing
-// sv_movejet 1.6 // multiplier of speed when using jetpack
+// sv_moverun 1.3 // multiplier of speed when running
 // sv_movestraight 1.2 // multiplier of speed when only moving forward
 // sv_movestrafe 1.0 // multiplier of speed when strafing
 // sv_moveinair 0.9 // multiplier of speed when in-air
-// sv_movestepup 0.95 // multiplier of speed when stepping up
-// sv_movestepdown 1.15 // multiplier of speed when stepping down
+// sv_movestepup 0.95 // multiplier of speed when going up a slope or stairs
+// sv_movestepdown 1.15 // multiplier of speed when going down a slope or stairs
 // sv_jumpspeed 110.0 // extra velocity to add when jumping
 // sv_impulsespeed 90.0 // extra velocity to add when impulsing
 // sv_impulselimit 0.0 // maximum impulse speed
 // sv_impulseboost 1.0 // multiplier of impulse when just boosting
-// sv_impulsedash 1.2 // multiplier of impulse when dashing
+// sv_impulsedash 1.3 // multiplier of impulse when dashing
 // sv_impulsejump 1.1 // multiplier of impulse when jumping
 // sv_impulsemelee 0.75 // multiplier of impulse when using melee
 // sv_impulseparkour 1.0 // multiplier of impulse when doing other parkour
 // sv_impulseallowed 15 // determines which impulse actions are allowed (bitwise OR); 0 = off, 1 = dash, 2 = boost, 4 = pacing, 8 = parkour
 // sv_impulsestyle 1 // impulse style; 0 = off, 1 = touch and count, 2 = count only, 3 = freestyle
-// sv_impulsemeter 30000 // impulse dash length; 0 = unlimited, anything else = timer
 // sv_impulsecount 6 // number of impulse actions per air transit
 // sv_impulsedelay 250 // minimum time between boosts
 // sv_impulseslide 1000 // time before powerslides end
-// sv_impulsecost 5000 // cost of impulse jump
 // sv_impulseskate 1000 // length of time a run along a wall can last
-// sv_impulsepacing 0.0 // pacing counts toward impulse; 0 = off, anything else = multiplier of time
+// sv_impulsemeter 30000 // amount of fuel in the impulse meter
+// sv_impulsecost 5000 // fuel cost to use impulse moves
 // sv_impulseregen 5.0 // impulse regen multiplier
-// sv_impulseregencrouch 2.5 // impulse regen modifier when crouching
-// sv_impulseregenpacing 0.75 // impulse regen modifier when pacing
-// sv_impulseregenmove 1.0 // impulse regen modifier when moving
-// sv_impulseregeninair 0.75 // impulse regen modifier when in air
+// sv_impulseregencrouch 2.5 // additional impulse regen modifier when crouching
+// sv_impulseregenpacing 0.75 // additional impulse regen modifier when pacing
+// sv_impulseregenmove 1.0 // additional impulse regen modifier when moving
+// sv_impulseregeninair 0.75 // additional impulse regen modifier when in air
+// sv_impulseregenslide 0.0 // additional impulse regen modifier when sliding
 // sv_stillspread 0.0 // multiply projectile spread by this much when standing still
 // sv_movespread 1.0 // multiply projectile spread by this much when moving
 // sv_inairspread 2.0 // multiply projectile spread by this much when jumping/in-air
-// sv_impulsespread 1.0 // multiply projectile spread by this much when impulsing/pacing/jetpacking
+// sv_impulsespread 1.0 // multiply projectile spread by this much when impulsing/pacing
 // sv_radialscale 1.0 // multiply explosion radius by this amount
 // sv_radiallimited 0.75 // multiply explosion radius by this amount in limited situations (eg. instagib)
 // sv_damagescale 1.0 // scale damage by this amount
@@ -228,22 +326,25 @@ if (= $rehashing 0) [
 // weapon variables are in the form of sv_<weapon><attribute>[1|2], where weapon is the name of the weapon
 //  attribute is one of the following attributes, and 1|2 applies if the attribute differs between primary and secondary
 //
-//  add: the amount added when picking up the weapon or reloading it
-//  max: the maximum amount the clip of this weapon can hold, can never be higher than default
-//  sub[1|2]: the amount taken from the clip for each firing action, set to zero for unlimited ammo
-//  adelay[1|2]: the time it takes after each firing action for the weapon to be ready again
-//  rdelay: the time it takes for the weapon to reload one 'add' unit
+//  name: displayed name for the weapon in-game
+//  modes: bitmask of game modes in which this weapon is allowed (if positive), or disallowed (if negative), see $modebit list above
+//  muts: bitmask of mutators in which this weapon is allowed (if positive), or disallowed (if negative), see $mutsbit list above
+//  ammoadd: the amount added when picking up the weapon or reloading it
+//  ammomax: the maximum amount the clip of this weapon can hold
+//  ammosub[1|2]: the amount taken from the clip for each firing action, set to zero for unlimited ammo
+//  delayattack[1|2]: the time it takes after each firing action for the weapon to be ready again
+//  delayreload: the time it takes for the weapon to reload one 'add' unit
 //  damage[1|2]: the amount of damage a projectile from each firing action does
 //  speed[1|2]: the speed of a projectile from each firing action
 //  power[1|2]: when more than zero, determines that the weapon can be 'cooked' this long before firing
 //  time[1|2]: the maximum lifetime of a projectile for each firing action
-//  pdelay[1|2]: when more than zero, projectiles from this weapon will be delayed this long
-//  gdelay[1|2]: when more than zero, projectiles from this weapon will delay guided settings by this long
-//  edelay[1|2]: when more than zero, projectiles from this weapon will not be able to hurt its owner for this long
+//  proxdelay[1|2]: when more than zero, projectiles from this weapon will be delayed this long
+//  guideddelay[1|2]: when more than zero, projectiles from this weapon will delay guided settings by this long
+//  escapedelay[1|2]: when more than zero, projectiles from this weapon will not be able to hurt its owner for this long
 //  explode[1|2]: if more than zero, a projectile from this firing action will explode at the end of its life or based on collision settings
 //  rays[1|2]: the amount of projectiles spawned from one shot of each firing action
 //  spread[1|2]: determines the amount a projectile from each firing action skews off-center
-//  zdiv[1|2]: when zero, keeps spread projectiles aligned horizontally, else divide the z axis this much
+//  spreadz[1|2]: when zero, keeps spread projectiles aligned horizontally, else divide the z axis this much
 //  aiskew[1|2]: determines 'added stupidity' for each weapon for AI counterparts
 //  fragweap[1|2]: when projectiles from this firing action are destroyed, create projectiles from this kind of weapon (+10 = alt fire for the weapon)
 //  flakdamage[1|2]: flak of this type deals this much damage
@@ -253,14 +354,13 @@ if (= $rehashing 0) [
 //  collide[1|2]: bitwise value which determines collision properties for a projectile from each firing action
 //      IMPACT_GEOM = 1     BOUNCE_GEOM = 2     IMPACT_PLAYER = 4   BOUNCE_PLAYER = 8   RADIAL_PLAYER = 16  COLLIDE_TRACE = 32  COLLIDE_OWNER = 64  COLLIDE_CONT = 128  COLLIDE_STICK = 256
 //  extinguish[1|2]: determines if a projectile from each firing action is extinguished by water
-//  cooked[1|2]: determines cooking style for a projectile, 0 = off, 1 = scale size, 2 = shorten life (+1 no scale), 4 = lengthen life (+1 no scale)
+//  cooked[1|2]: determines what happens the longer a weapon is cooked (bitmask): 0 = cannot be cooked, 1 = projectile becomes bigger, 2 = projectile becomes smaller, 4 = projectile lasts longer, 8 = projectile disappears sooner, 16 = projectile moves faster, 32 = projectile moves slower, 64 = create extra projectiles, 128 = switch to scope view before shooting (this last one applies to secondary fire only)
 //  guided[1|2]: determines guided style for a projectile, 0 = off, 1 = follow crosshair, 2 = home crosshair target (+1 only first target), 4 = home projectile target (+1 only first target)
 //  radial[1|2]: boolean 0 or 1, sets a projectile for each firing action to do continuous radial damage inside the 'explode' radius during transit
-//  residual[1|2]: boolean 0 or 1, determines if a projectile from each firing action has a residual effect, 0 = off, 1 = burns, 2 = bleeds
-//  reloads: boolean 0 or 1, determines if this weapon can reload
-//  zooms: boolean 0 or 1, determines if the secondary action on this weapon zooms
+//  residual[1|2]: determines if a projectile from each firing action has one or more residual effects, 0 = off, 1 = burns, 2 = bleeds, 4 = shocks
+//  residualundo[1|2]: determines if a projectile can put out the residual effects above
 //  fullauto[1|2]: boolean 0 or 1, determines if each firing action is fully automatic (click-and-hold) or not (click-and-click)
-//  allowed: determines if this weapon type is allowed to spawn at all, 0 = off, 1 = all but insta/duel, 2 = all but insta, 3 = always
+//  disabled: determines if this weapon type is allowed to be used at all
 //  laser: determines if this weapon has a laser pointer which is projected to the point where the player is aiming
 //  taperin[1|2]: determines the maximum amount a projectile from each firing action is allowed to 'taper in' to over its lifetime
 //  taperout[1|2]: determines the maximum amount a projectile from each firing action is allowed to 'taper out' to over its lifetime
@@ -271,17 +371,21 @@ if (= $rehashing 0) [
 //  weight[1|2]: relative weight for a projectile of each firing action
 //  radius[1|2]: determines the size for a projectile of each firing action
 //  kickpush[1|2]: determines the amount of pushback from shooting each firing action
-//  hitpush[1|2]: multiplier of damage for a projectile or explosion of each firing action impacting a player
+//  hitpush[1|2]: determines the amount of pushback from getting hit by this projectile
 //  slow[1|2]: slow target hit with a projectile from this by this ammount
 //  aidist[1|2]: determines the 'maximum distance' a weapon can be shot at, used by AI to determine weapon effectiveness ranges
 //  partsize[1|2]: determines the maximum particle size of a projectile from each firing action
 //  partlen[1|2]: determines the maximum tape particle length of a projectile from each firing action
 //  frequency: determines the multiplier of itemspawntime in which items of this type respawn in
-//  pusharea: determines the multiplier of explode radius this weapon pushes in
+//  wavepush[1|2]: determines the multiplier of explode radius this weapon pushes in
 //  delta[1|2]: determines the amount by which each firing action is guided
 //  trace[1|2]: determines the multiplier of length to apply to traced weapons
-//  torsodmg[1|2]: determines the multiplier of damage for torso shots
-//  legsdmg[1|2]: determines the multiplier of damage for leg shots
+//  damagehead[1|2]: determines the multiplier of damage for head shots
+//  damagetorso[1|2]: determines the multiplier of damage for torso shots
+//  damagelegs[1|2]: determines the multiplier of damage for leg shots
+//  damageself[1|2]: determines whether this weapon can hit the player who fired it
+//  damageteam[1|2]: determines whether this weapon can hit teammates of the player who fired it
+//  damagepenalty[1|2]: determines whether team-kills caused by this weapon should result in point loss
 //  fragscale[1|2]: flak created by this firing action is scaled by this much
 //  fragspread[1|2]: flak created by this firing action spreads its direction randomly by this much if it doesn't impact a player
 //  fragrel[1|2]: flak created by this firing action retains this much of its parent relative momentum
diff --git a/doc/guidelines.txt b/doc/guidelines.txt
index 4b0632a..0f554c8 100644
--- a/doc/guidelines.txt
+++ b/doc/guidelines.txt
@@ -26,14 +26,20 @@ basis.
 
 (See RE Mark Policy for definitions of "substantially (un)modified")
 
-* The gameplay of the server must not grant an undue or unfair advantage to any
-  party.
+* The gameplay of the server must not grant an unfavorable advantage or
+  disservice to any party.
   - "Humans vs Zombies" is fine, as long as it rotates roles in a fair manner.
   - Giving one player double the health of other players permanently is not
     permitted.
+  - Game variables may not be set in such a way that makes it impossible to
+    avoid harming yourself or teammates (i.e. no suicide weapons).
 
 * If the gameplay of the server is modified substantially this must be
   indicated clearly, preferably through the use of "servermotd".
+  - If you wish to disable one or more weapons on the server, use the
+    "sv_<weapon>disabled" variable. Letting players wield a
+    weapon, but changing variables so that weapon doesn't actually work (such
+    as by setting its damage output to 0), is not allowed.
 
 * If the source code of the server is modified substantially you must contact
   the Red Eclipse Team to check that the changes are permitted.
@@ -65,7 +71,7 @@ basis.
     designated as "family friendly" and provides a clear definition of what
     kind of language is not permitted.
   - Intentionally killing team mates or scoring for an opposing team is not
-    permitted.
+    permitted. This includes "kamikaze" tactics that result in negative scores.
   - Cheating, flooding and/or spamming is not permitted.
 
 * The owner of a server may outline their own rules, specific to their server.
@@ -101,6 +107,6 @@ basis.
 
     Attribution
 
-This text is Copyright (C) 2011-2012, the Red Eclipse Team
+This text is Copyright (C) 2011-2015, the Red Eclipse Team
 and is available under a Creative Commons Attribution-ShareAlike 3.0 Unported
 License <http://creativecommons.org/licenses/by-sa/3.0/>
diff --git a/doc/irc.txt b/doc/irc.txt
index 6a844b5..843228c 100644
--- a/doc/irc.txt
+++ b/doc/irc.txt
@@ -3,14 +3,14 @@
 The Red Eclipse IRC (Internet Relay Chat) channel is located in #redeclipse on
 the Freenode Network (irc.freenode.net). If you don't have an IRC client
 installed, you may access the channel from your web browser at
-http://www.redeclipse.net/chat
+http://redeclipse.net/chat
 
 The channel is primarily a development and social environment for those
 who are working on or with others on the project. Please be mindful of how
 you approach the people in there, as for most of us this is our home and you
 are our guest. You may ask help and support questions, but the preferred
 mechanism for getting assistance is our forums, located at:
-http://www.redeclipse.net/forum
+http://redeclipse.net/forum
 
 Please be patient when asking questions or seeking other support, not everyone
 on the channel is able to answer your question, and others may not actually
diff --git a/doc/license.txt b/doc/license.txt
index ee9e96f..3a9614b 100644
--- a/doc/license.txt
+++ b/doc/license.txt
@@ -3,8 +3,8 @@ THE RED ECLIPSE LICENSE
 Red Eclipse is based on Cube Engine 2, both of which are covered under the ZLIB
 license, you may use the source code so long as you obey this license.
 
-    Red Eclipse, Copyright (C) 2009-2013 Quinton Reeves, Lee Salzman
-    Cube Engine 2, Copyright (C) 2001-2013 Wouter van Oortmerssen, Lee Salzman,
+    Red Eclipse, Copyright (C) 2009-2015 Quinton Reeves, Lee Salzman
+    Cube Engine 2, Copyright (C) 2001-2015 Wouter van Oortmerssen, Lee Salzman,
         Mike Dysart, Robert Pointon, and Quinton Reeves
     http://www.opensource.org/licenses/zlib-license.php
 
@@ -36,7 +36,7 @@ the CC-BY-SA license, either version 3.0 or (at your option) any later version,
 you may use the content in Red Eclipse so long as you obey individual
 licensing criteria.
 
-    Red Eclipse, Copyright (C) 2009-2013 Red Eclipse Team
+    Red Eclipse, Copyright (C) 2009-2015 Red Eclipse Team
     Creative Commons Attribution ShareAlike 3.0+ License (CC-BY-SA)
     See cc-by-sa.txt or http://creativecommons.org/licenses/by-sa/3.0/
 
diff --git a/doc/man/redeclipse.6.am b/doc/man/redeclipse.6.am
index 1f946b5..3722f85 100644
--- a/doc/man/redeclipse.6.am
+++ b/doc/man/redeclipse.6.am
@@ -28,7 +28,6 @@
 
 '\" General options
 .OP \-x\fR'command(s)'
-.OP \-k\fR{0|1}
 .OP \-v\fR{0..4}
 .OP \-\-help
 
@@ -178,12 +177,6 @@ This configuration is stored in init.cfg
 Executes a list of commands once Red Eclipse has started up.
 
 .TP
-.BR \-k {0|1}
-Enables or disables kid mode, which reduces blood and death effects somewhat. 1 enables reduced gore, defaults to 0.
-.IP
-This configuration is stored in init.cfg
-
-.TP
 .BR \-v {0..4}
 Sets verbosity. This affects how much information is printed to the console. 0 is the least verbose, 4 is the most verbose, defaults to 0.
 .IP
diff --git a/doc/media.txt b/doc/media.txt
new file mode 100644
index 0000000..b960d04
--- /dev/null
+++ b/doc/media.txt
@@ -0,0 +1,77 @@
+The following is a summary list of Red Eclipse in the media and on the
+internet; it is not intended to be a comprehensive guide. If you are
+looking to collect bits and pieces for use in your own media, it is
+appreciated if you focus on what makes Red Eclipse unique, rather than
+using any comparisons to other games.
+
+Regards,
+Quinton "quin" Reeves
+Lead Developer, Red Eclipse
+http://redeclipse.net/
+
+
+= Press and Blogs =
+
+[2013-08-07] TuxArena | Red Eclipse Review – Free Shooter Game in Futuristic Environments
+"The good things about Red Eclipse are its unique features and game style. Since it’s actively developed and pretty complete, Red Eclipse has a lot of potential."
+> http://www.tuxarena.com/2013/10/red-eclipse-review-free-shooter-game-in-futuristic-environments/
+
+[2013-02-14] LAN Party Mania | Red Eclipse
+"Red Eclipse is a free competitive shooter similar to Quake and Unreal Tournament... but on crack."
+> http://lanpartymania.com/?p=464
+
+[2012-10-22] GameSpot Users: TheNineRings | Red Eclipse and Assault Cube. Oh yeah, and me too.
+"What makes this FPS distinctive is that you are extremely mobile. Dodges, double jumping, wall running, midair and ground based dashes, a diving kick [..] and jetpacks."
+> http://au.gamespot.com/users/TheNineRings/show_blog_entry.php?topic_id=m-100-25996813
+
+[2012-09-09] eGamer | Indie Review: Red Eclipse
+"Red Eclipse is a blast to play, it's awesome fun, and it's free. It's one of the best indie games of the year."
+> http://egamer.co.za/2012/09/indie-review-red-eclipse/
+
+[2012-08-20] Super Budget Brothers Podcast | Red Eclipse
+> http://superbudgetbrothers.com/2012/08/20/red-eclipse/
+> http://superbudgetbrothers.files.wordpress.com/2012/08/episode-034-red-eclipse.mp3
+
+[2012-03-03] MakeUseOf | Red Eclipse – A Pulse-Pounding First Person Shooter Game
+"Will Red Eclipse become my new number one shooter? Quite possibly." --Danny Stieben
+> http://www.makeuseof.com/tag/red-eclipse-pulsepounding-person-shooter-game/
+
+[2012-01-24] IndieGameReviewer.com | Review: Red Eclipse - An Open Source Fast-Paced Classic Shooter
+"The gameplay is nothing short of stupendous, allowing the player to run on walls, double jump and turn jumps into flying kicks that usually translate into instant kills."
+> http://www.indiegamereviewer.com/red-eclipse-an-open-source-fast-paced-classic-shooter/
+
+[2011-03-18] Joystiq | Free, open source FPS 'Red Eclipse' hits version 1.0
+"Red Eclipse is a full-featured, Quake 3 Arena-style first person shooter with, as you can see in the trailer after the break, some nice touches, including a wallrunning system, lots of bright colors exploding around and what looks like pretty good ragdoll physics."
+> http://www.joystiq.com/2011/03/18/free-open-source-fps-red-eclipse-hits-version-1-0/
+
+[Date Unknown] About.com Open Source | 5 Open Source First-Person Shooter Video Games (Page 3)
+"Red Eclipse is a fairly textbook FPS -- weapons, enemies, fight! -- but its parkour-style physics allow players to perform unusual acrobatics and its mode/mutator system offers an unusually wide range of gameplay."
+> http://opensource.about.com/od/desktopapps/ss/5-Open-Source-First-Person-Shooter-Video-Games_3.htm
+
+
+= Honourable Re-mentions =
+
+Usually, this just means all they did was give a decent re-mention, usually release announcements.
+
+[2013-03-26] WEB UPD8 | Open Source FPS Game 'Red Eclipse' 1.4 Brings Graphical and Gameplay Enhancements
+"Red Eclipse is very fun to play with your friends or with the growing online community"
+> http://www.webupd8.org/2013/03/open-source-fps-game-red-eclipse-14.html
+
+[2013-01-10] Free Gamer | Red Eclipse, STK feature in Microsoft Promo
+> http://freegamer.blogspot.com.au/2013/01/red-eclipse-stk-feature-in-micrsoft.html
+
+Others
+
+> http://www.ubuntuvibes.com/2011/07/first-person-shooter-red-eclipse.html
+> http://www.phoronix.com/scan.php?page=news_item&px=OTcyOQ
+> http://www.phoronix.com/scan.php?page=news_item&px=MTAzODM
+> http://www.thegametalk.net/2012/10/open-source-games-red-eclipse.html
+
+
+= Misc Stuff =
+
+SourceForge.net Reviews
+> http://sourceforge.net/projects/redeclipse/reviews/
+
+Desura Reviews
+> http://www.desura.com/games/red-eclipse/reviews
\ No newline at end of file
diff --git a/doc/trademark.txt b/doc/trademark.txt
index 1b79594..dfd8e35 100644
--- a/doc/trademark.txt
+++ b/doc/trademark.txt
@@ -81,6 +81,12 @@ You may not use the marks in the following ways:
    distribution, platform, product, etc. over another except where explicitly
    indicated in writing by the Red Eclipse project.
 
+5. In any way that promotes the use of third party services over those provided
+   by Red Eclipse.
+
+6. In any way that could be deemed harmful or disruptive to the Red Eclipse
+   community and/or project.
+
 Thus uses of the marks in a domain name or company name without explicit written
 permission from the Red Eclipse project are prohibited.
 
@@ -94,7 +100,7 @@ The Red Eclipse project reserves the sole right to:
 2. Modify this policy to preserve the rights of Red Eclipse, the public, the
    community, and the users.
    The latest version of the Red Eclipse Mark Policy should be available at
-   <http://www.redeclipse.net/wiki/Trademark_Policy>
+   <http://redeclipse.net/wiki/Trademark_Policy>
 
 3. Grant exceptions to this policy, of any kind and for any reason whatsoever,
    other clauses notwithstanding.
@@ -121,7 +127,7 @@ as described above.
 
   Attribution
 
-This text is Copyright (C) 2011-2012, the Red Eclipse Team
+This text is Copyright (C) 2011-2015, the Red Eclipse Team
 and is available under a Creative Commons Attribution-ShareAlike 3.0 Unported
 License <http://creativecommons.org/licenses/by-sa/3.0/>
 
diff --git a/game/fps/version.cfg b/game/fps/version.cfg
deleted file mode 100644
index 7611b69..0000000
--- a/game/fps/version.cfg
+++ /dev/null
@@ -1,34 +0,0 @@
-versionmajor 1
-versionminor 4
-versionpatch 0
-versionstring "1.4"
-versionname "Red Eclipse"
-versionuname "redeclipse"
-versionrelease "Elara"
-versionurl "www.redeclipse.net"
-
-servermaster "play.redeclipse.net"
-
-sv_botmalenames "moe larry curly jerry ted phil bob billy joe john james gunner dante dick lorenzo chuck zeus alexander benjamin daniel ethan fernando gabriel hunter isaac jacob kevin logan michael noah owen parker ryan samuel tyler uriel victor william xavier yahir zachary abram avaz abraham agap agapit agathon adam adrian azamat azat aidar airat akim alan alexander alex ali alikhan diamond albert anatoly angel andrew anton anfim aram arkady arman armen arseniy arslan artem artemia arth [...]
-sv_botfemalenames "sue debbie luanne brandy angel kitty jane ava brianna chloe destiny emma faith grace hannah julia kaylee lily madison natalie olivia peyton riley sophia taylor unique victoria ximena yaretzi zoe avdotya aurora agatha agafya agnes ada adelaide adeline adele adriana aksinya alevtina alain alina alice aliyah alla alsou alba albina alfia alya amalia amin amir anahit anastasia angelina angela angelica anisa anna antonina anfisa arina bela bertha valentine valeria varvara va [...]
-sv_botmalevanities "badge"
-sv_botfemalevanities "badge"
-
-sv_allowmaps "ares bath battlefield biolytic bloodlust canals cargo center colony conflict cutec cyanide darkness deadsimple deathtrap deli depot dropzone dutility echo erosion error forge foundation fourplex futuresport ghost hawk hinder institute keystone2k linear livefire longestyard mist neodrive nova oneiroi panic processing pumpstation purge spacetech starlibido stone suspended testchamber tower tranquility tribal ubik vault venus warp wet"
-
-sv_mainmaps "ares bath battlefield biolytic bloodlust canals cargo center colony conflict cutec darkness deadsimple deathtrap deli depot dropzone dutility echo erosion error foundation fourplex futuresport ghost institute keystone2k linear livefire longestyard mist nova oneiroi panic processing pumpstation spacetech starlibido stone suspended tower tribal ubik vault venus warp wet"
-sv_capturemaps "ares bath battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deli depot dropzone dutility echo erosion foundation fourplex futuresport ghost institute keystone2k linear mist nova panic pumpstation stone suspended tribal vault venus warp wet"
-sv_defendmaps "ares bath battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deli depot dropzone dutility echo erosion foundation fourplex futuresport ghost institute keystone2k linear livefire mist nova panic processing pumpstation stone suspended tower tribal ubik vault venus warp wet"
-sv_kingmaps "ares bath battlefield biolytic canals cargo center colony conflict darkness deadsimple depot dutility echo fourplex linear livefire processing stone suspended tower tribal ubik vault venus"
-sv_bombermaps "ares battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deli depot dropzone dutility echo erosion foundation fourplex futuresport ghost linear mist nova pumpstation stone suspended tower tribal vault venus warp wet"
-sv_holdmaps "ares bath battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deli depot dropzone dutility echo erosion foundation fourplex futuresport ghost keystone2k linear mist nova panic processing pumpstation stone suspended tower tribal ubik vault venus warp wet"
-sv_trialmaps "cyanide hawk hinder neodrive purge testchamber"
-sv_gauntletmaps "ares bath battlefield biolytic canals cargo center colony conflict deli depot dropzone erosion foundation futuresport ghost linear nova stone suspended tribal venus wet"
-
-sv_multimaps "canals deadsimple depot keystone2k warp fourplex" 
-sv_duelmaps "bath bloodlust darkness deadsimple dutility echo fourplex ghost longestyard livefire stone panic vault wet"
-sv_jetpackmaps "ares battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deathtrap deli depot dropzone dutility echo erosion error forge fourplex futuresport ghost linear longestyard mist nova oneiroi pumpstation spacetech starlibido suspended testchamber tower tranquility tribal ubik venus warp"
-
-sv_smallmaps "bath bloodlust darkness deadsimple dutility echo error fourplex ghost longestyard livefire stone panic vault wet"
-sv_mediummaps "ares battlefield biolytic canals cargo center colony conflict cutec darkness deadsimple deathtrap deli dropzone echo erosion error forge foundation fourplex futuresport ghost institute keystone2k linear livefire mist nova oneiroi panic processing pumpstation spacetech suspended starlibido stone tower tranquility tribal ubik venus warp wet"
-sv_largemaps "ares battlefield biolytic canals cargo center colony cutec deadsimple deathtrap deli depot erosion forge foundation futuresport ghost linear mist nova processing pumpstation spacetech suspended tower tranquility tribal ubik venus warp"
diff --git a/readme.md b/readme.md
new file mode 120000
index 0000000..0d79d56
--- /dev/null
+++ b/readme.md
@@ -0,0 +1 @@
+readme.txt
\ No newline at end of file
diff --git a/readme.txt b/readme.txt
index 0fadfa7..20563e4 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,164 +1,109 @@
-= Red Eclipse =
-
-* A Free, Casual Arena Shooter; released as Open Source
-* Available for Windows, GNU/Linux and BSD
-* Parkour, impulse boosts, dashing, and other tricks
+# Red Eclipse
+* A free, casual arena shooter; released as Open Source
+* Available for Windows, GNU/Linux, BSD, and OS X
+* Features parkour, impulse boosts, dashing, and other tricks
 * Favourite gamemodes with an array of mutators and variables
 * Builtin editor lets you create your own maps cooperatively online
-* Free Download from http://www.redeclipse.net/
-
-= About =
-
-Red Eclipse is a fun-filled new take on the casual first person arena
-shooter, built as a total conversion of Cube Engine 2, which lends itself
-toward a balanced gameplay, with a general theme of agility in a variety
-of environments. For more information, please see our Wiki or Forum.
-
-Wiki: http://www.redeclipse.net/wiki
-Forum: http://www.redeclipse.net/forum
-
-The project is a Free and Open Source game, built on Cube Engine 2 using SDL
-and OpenGL which allows it to be ported to many platforms; you can download
-a package for Windows, GNU/Linux, BSD, or grab a development copy from
-our Subversion repository and live on the bleeding edge.
+* Free download from http://redeclipse.net/
 
-Download: http://www.redeclipse.net/download
-Subversion: http://www.redeclipse.net/devel
+## About
+Red Eclipse is a fun-filled new take on the casual first person arena shooter, built as a total conversion of Cube Engine 2, which lends itself toward a balanced gameplay, with a general theme of agility in a variety of environments. For more information, please see our Wiki or Forum.
 
-In a true open source by the people for the people nature, we try to work
-closely with the gaming and open source communities to provide a better
-overall experience, aiming to create a game environment that is fun and easy
-to play, while still having elements to master.
+[Wiki](http://redeclipse.net/wiki)
 
-If you think you might have something to contribute to the game or
-community, please feel free to drop by our Chat or Forums and talk to us
-directly. We try to maintain a standard of friendly behaviour in our
-community, so don't be afraid to speak up and have your say in building this
-game for us all :)
+[Forum](http://redeclipse.net/forum)
 
-= Support Us =
+The project is a Free and Open Source game, built on Cube Engine 2 using SDL and OpenGL which allows it to be ported to many platforms; you can download a package for Windows, GNU/Linux, BSD, and Mac OS X; or grab a development copy from our Git repository and live on the bleeding edge.
 
-Red Eclipse is developed by volunteers, and you get it free of charge; your
-donations go toward ongoing costs which keep this project alive.
+[Download](http://redeclipse.net/download)
 
-Donate: http://www.redeclipse.net/donate
+[Git](http://redeclipse.net/devel)
 
-= Get Involved =
+In a true open source by the people for the people nature, we try to work closely with the gaming and open source communities to provide a better overall experience, aiming to create a game environment that is fun and easy to play, while still having elements to master.
 
-You're encouraged to help the project by joining in with the rest of the
-community to make a better project! You can do this by participating
-in our Community, or using the SVN (Subversion) repository, and reporting
-any issues, ideas, suggestions, or comments you might have. If you are
-feeling particularly generous, you can donate and help support our
-development efforts!
+If you think you might have something to contribute to the game or community, please feel free to drop by our Chat or Forums and talk to us directly. We try to maintain a standard of friendly behaviour in our community, so don't be afraid to speak up and have your say in building this game for us all :)
 
-We chat via IRC in the channel #redeclipse on irc.freenode.net,
-if you don't have an IRC client you can use http://www.redeclipse.net/chat
-You can use this service to talk directly to the Red Eclipse developers
-and supporters, whether you just want to have a chat, report a problem, or
-make a suggestion, this is the best place to do it.
+## Get Involved
+You're encouraged to help the project by joining in with the rest of the community to make a better project! You can do this by participating in our Community, or using the development version, and reporting any issues, ideas, suggestions, or comments you might have.
 
-= Open Source =
+We chat via IRC in the channel #redeclipse on irc.freenode.net, if you don't have an IRC client you can use http://redeclipse.net/chat - You can use this service to talk directly to the Red Eclipse developers, contributors, and supporters, whether you just want to have a chat, report a problem, or make a suggestion, this is the best place to do it.
 
-The project is Free and Open Source meaning that you can both use it for
-free, and you can be apart of it by contributing in whatever way you can.
-These are people who have helped shape Red Eclipse into what you see today,
-and they are regular people just like you who volunteer their time or
-donate in the spirit of making a really cool game, your name could be here
-too.
-
-== Developers ==
+## Open Source
+The project is Free and Open Source meaning that you can both use it for free, and you can be a part of it by contributing in whatever way you can. These are people who have helped shape Red Eclipse into what you see today, and they are regular people just like you who volunteer their time or donate in the spirit of making a really cool game, your name could be here too.
 
+### Founders
 * Quinton "Quin" Reeves - Lead Developer
-    - Gameplay and AI Code/Design, Community Management
+    - Gameplay and AI Design, Community Management
 
 * Lee "Eihrul" Salzman - Lead Programmer / Advisor
-    - Cube 2 Backports, Support, Speed Improvements, Encouragement
-
-* Kevin "Hirato" Meyer
-    - Development Assistance, Design and Testing
-
-* Joshua L. "Verbalshadow" Blocher
-    - Some source artwork, Design and Testing
-
-* R�mi "LuckystrikeRx" Clouet d'Orval
-    - Maps, Textures, Models, Weapons, Design and Testing, Website, Trailer
-
-* Jonathan "W!ck3d" Roels
-    - Maps, Textures, Design and Testing
-
-* Derek "Favorito" Ponicki
-    - Maps, Textures, Design and Testing
-
-* Jeroen "appleflap" Boukens
-    - Textures, Design and Testing
-
-* Martin "arand" Werner
-    - Documentation, Legal Drafting, Cross-Distribution Support,
-        Open Source Compliance Checks, and much more.
-
-* Mike "mikeplus64" Ledger
-    - Maps, Models, Weapons, Textures, HUD, Design and Testing
-
-* Derek "JoJo" Stegall
-    - Maps, Models, Textures, HUD, Sound FX, Design and Testing
-
-* Jeff "Architect" Cope
-    - Design and Testing
-
-* Jonathan "Ulukai" De Nil
-    - Maps, Testing, Support
-
-* Ruben "TheLastProject" van Os
-    - Design, Testing and Support
+    - Cube 2 Engine and Backports/Support, Speed Improvements
 
-* Jacinta "cinta" Hall
-    - Design, Testing, Support, and making babies.
-
-* Stefan Norman and Peter McInerney
-    - Master and Game server hosting
-
-== Contributors ==
-
-* Alex "ZeroKnight" George - Design, Testing and Support
+### Contributors, Developers and Supporters
+* Adam "lycanfox" Hobson
+* Alex "ZeroKnight" George - Design/Testing, Support
+* Anup "DOS_WARRIOR" Debnath
+* Cameron "Rhubarb" Dawdy
+* Christopher "Dratz-_C" Dratz
+* Christopher "paroneayea" Webber
+* Corey "c0rdawg" Maher
+* Daniel "Imerion" Eriksson
+* David "srbs" Forrest
+* Derek "Favorito" Ponicki - Maps, Textures, Design/Testing
+* Derek "JoJo" Stegall - Maps, Models, Textures, HUD, Sound FX, Design/Testing
+* Eddie "skedz4u" Webb
+* Henrik "ahven" Pihl
+* Jacinta "cinta" Hall - Design/Testing, Support, and making babies.
+* Jeff "Architect" Cope - Design/Testing
+* Jeroen "appleflap" Boukens - Textures, Design/Testing
+* Jonathan "Ulukai" De Nil - Maps, Testing, Support
+* Jonathan "W!ck3d" Roels - Maps, Textures, Design/Testing
 * Joseph "ballist1c" Calabria - Maps
-* "John_III" - Maps and Models
-* "D.a.M.i.E.n" - Maps and Textures
-* Matt "greasepirate" Kalt - Maps
-* Ricky Thomson "unixfreak" - Maps
-* "Raiden" - Maps
-* "Korsi" - Maps
-* "4rson" - Maps and Support
-* Slawek "VibrantWave" Blauciak - Ambient Sounds
-* "SkiingPenguins" - Skyboxes
-* "Fleeky" - Models
-* "fluxord" - Crosshairs
+* Joshua "JDWhyte" Dwight
+* Joshua L. "Verbalshadow" Blocher - Some source artwork, Design/Testing
+* Kevin "Hirato" Meyer - Development Assistance, Design/Testing
+* Kirill "TristamK" Kolesnikov - Maps
 * Kurt "Kurtis84" Kessler - Textures
+* Ludwig "Sniper-goth" Boscolo
+* Luke "syreal" Jones
+* Mark "xtort-" Doodeman
+* Martin "arand" Werner - Documentation, Legal Drafting, Cross-Dist Support, Open Source Checks, and much more.
+* Matt "greaserpirate" Kalt - Maps
+* Maxim "acerspyro" Therrien - UI elements, Models, Support
+* Mike "mikeplus64" Ledger - Maps, Models, Weapons, Textures, HUD, Design/Testing
+* Mikhail "Agustis" Kashin
+* Nick "Fatal_Glory" Watts
+* Petri Lukkarinen
+* Ricky "unixfreak" Thomson - Maps
 * Riidom Li - Models
-* RaZgRiZ - Textures and Cubescript maintenance
-* Kirill "TristamK" Kolesnikov - Maps
-
-== Supporters ==
-
-* "w00p|dazza"
-* Corey "c0rdawg" Maher
 * Rob "Lloir" Shannon
-* Henrik "ahven" Pihl
 * Robert "Homicidal" Crane
-* Wayne Bennett
-* Cameron "Rhubarb" Dawdy
-* Christopher "paroneayea" Webber
-* Mark "xtort-" Doodeman
-* Vadim Peretokin
+* Robert Winkler
+* Ryan "icculus" Gordon / icculus.org - Master server and website hosting
+* R�mi "LuckystrikeRx" Clouet d'Orval - Maps, Textures, Models, Weapons, Design/Testing, Website
+* Slawek "VibrantWave" Blauciak - Ambient Sounds
+* Stefan Norman and Peter McInerney - Game server and domain name hosting
+* S�bastien "sinma" Chauvel
 * Taiyo Rawle
+* Vadim Peretokin
+* Viktor "Unnamed" Hahn - Maps, Models, and other improvements
+* Wayne Bennett
+* Zachery "skiingpenguins" Slocum - Skyboxes
+* "Gaming Tilt"
+* "4rson" - Maps and Support
+* "bonifarz"
+* "Boognish" - Maps
 * "CD Xbow"
-* "FearFighter"
-* Daniel "Imerion" Eriksson
-* Furor
-* Gaming Tilt
-* Nick "Fatal_Glory" Watts
-* Robert Winkler
+* "D.a.M.i.E.n" - Maps and Textures
+* "FaTony"
 * "fbt"
-* Eddie "skedz4u" Webb
-* Christopher "Dratz-_C" Dratz
-* Petri Lukkarinen
+* "FearFighter"
+* "Fleeky" - Models
+* "fluxord" - Crosshairs
+* "Furor"
+* "John_III" - Maps and Models
+* "Korsi" - Maps
+* "littlebabyjesus"
+* "Raiden" - Maps
+* "RaZgRiZ" - Textures and Cubescript maintenance
+* "TheLastProject" - Design, Testing and Support
+* "w00p|dazza"
diff --git a/redeclipse.sh b/redeclipse.sh
index 81937cc..a736654 100755
--- a/redeclipse.sh
+++ b/redeclipse.sh
@@ -1,70 +1,170 @@
 #!/bin/sh
-# APP_PATH should refer to the directory in which Red Eclipse data files are placed.
-#APP_PATH=~/redeclipse
-#APP_PATH=/usr/local/redeclipse
-#APP_PATH=.
-APP_PATH="$(cd "$(dirname "$0")" && pwd)"
+if [ "${REDECLIPSE_CALLED}" = "true" ]; then REDECLIPSE_EXITR="return"; else REDECLIPSE_EXITR="exit"; fi
+REDECLIPSE_SCRIPT="$0"
 
-# APP_OPTIONS contains any command line options you would like to start Red Eclipse with.
-APP_OPTIONS=""
+redeclipse_path() {
+    if [ -z "${REDECLIPSE_PATH+isset}" ]; then REDECLIPSE_PATH="$(cd "$(dirname "$0")" && pwd)"; fi
+}
 
-# SYSTEM_NAME should be set to the name of your operating system.
-#SYSTEM_NAME=Linux
-SYSTEM_NAME="$(uname -s)"
+redeclipse_init() {
+    if [ -z "${REDECLIPSE_BINARY+isset}" ]; then REDECLIPSE_BINARY="redeclipse"; fi
+    REDECLIPSE_SUFFIX=""
+    REDECLIPSE_MAKE="make"
+}
 
-# MACHINE_NAME should be set to the name of your processor.
-#MACHINE_NAME=i686
-MACHINE_NAME="$(uname -m)"
+redeclipse_setup() {
+    if [ -z "${REDECLIPSE_TARGET+isset}" ]; then
+        REDECLIPSE_SYSTEM="$(uname -s)"
+        REDECLIPSE_MACHINE="$(uname -m)"
+        case "${REDECLIPSE_SYSTEM}" in
+            Linux)
+                REDECLIPSE_SUFFIX="_linux"
+                REDECLIPSE_TARGET="linux"
+                ;;
+            FreeBSD)
+                REDECLIPSE_SUFFIX="_bsd"
+                REDECLIPSE_TARGET="bsd"
+                REDECLIPSE_BRANCH="source" # we don't have binaries for bsd yet sorry
+                ;;
+            MINGW*)
+                REDECLIPSE_SUFFIX=".exe"
+                REDECLIPSE_TARGET="windows"
+                REDECLIPSE_MAKE="mingw32-make"
+                if [ -n "${PROCESSOR_ARCHITEW6432+isset}" ]; then
+                    REDECLIPSE_MACHINE="${PROCESSOR_ARCHITEW6432}"
+                else
+                    REDECLIPSE_MACHINE="${PROCESSOR_ARCHITECTURE}"
+                fi
+                ;;
+            *)
+                echo "Unsupported system: ${REDECLIPSE_SYSTEM}"
+                return 1
+                ;;
+        esac
+    fi
+    if [ -z "${REDECLIPSE_ARCH+isset}" ]; then
+        case "${REDECLIPSE_MACHINE}" in
+            i486|i586|i686|x86)
+                REDECLIPSE_ARCH="x86"
+                ;;
+            x86_64|[Aa][Mm][Dd]64)
+                REDECLIPSE_ARCH="amd64"
+                ;;
+            arm|armv*)
+                REDECLIPSE_ARCH="arm"
+                ;;
+            *)
+                REDECLIPSE_ARCH="native"
+                ;;
+        esac
+    fi
+    if [ -e "${REDECLIPSE_PATH}/branch.txt" ]; then REDECLIPSE_BRANCH_CURRENT=`cat "${REDECLIPSE_PATH}/branch.txt"`; fi
+    if [ -z "${REDECLIPSE_BRANCH+isset}" ]; then
+        if [ -n "${REDECLIPSE_BRANCH_CURRENT+isset}" ]; then
+            REDECLIPSE_BRANCH="${REDECLIPSE_BRANCH_CURRENT}"
+        elif [ -e ".git" ]; then
+            REDECLIPSE_BRANCH="devel"
+        else
+            REDECLIPSE_BRANCH="stable"
+        fi
+    fi
+    if [ -z "${REDECLIPSE_HOME+isset}" ] && [ "${REDECLIPSE_BRANCH}" != "stable" ] && [ "${REDECLIPSE_BRANCH}" != "inplace" ]; then REDECLIPSE_HOME="home"; fi
+    if [ -n "${REDECLIPSE_HOME+isset}" ]; then REDECLIPSE_OPTIONS="-h${REDECLIPSE_HOME} ${REDECLIPSE_OPTIONS}"; fi
+    redeclipse_check
+    return $?
+}
 
-if [ -x "${APP_PATH}/bin/redeclipse_native" ]
-then
-    SYSTEM_SUFFIX="_native"
-    APP_ARCH=""
-else
-    case "$SYSTEM_NAME" in
-    Linux)
-        SYSTEM_SUFFIX="_linux"
-        ;;
-    FreeBSD)
-        SYSTEM_SUFFIX="_freebsd"
-        ;;
-    *)
-        SYSTEM_SUFFIX="_unknown"
-        ;;
-    esac
+redeclipse_check() {
+    if [ "${REDECLIPSE_BRANCH}" = "source" ]; then
+        echo ""
+        echo "Rebuilding \"${REDECLIPSE_BRANCH}\". To disable set: REDECLIPSE_BRANCH=\"inplace\""
+        echo ""
+        ${REDECLIPSE_MAKE} -C src all install
+        return $?
+    elif [ "${REDECLIPSE_BRANCH}" != "inplace" ]; then
+        echo ""
+        echo "Checking for updates to \"${REDECLIPSE_BRANCH}\". To disable set: REDECLIPSE_BRANCH=\"inplace\""
+        echo ""
+        redeclipse_begin
+        return $?
+    fi
+    redeclipse_runit
+    return $?
+}
 
-    case "$MACHINE_NAME" in
-    i486|i586|i686)
-        APP_ARCH="x86/"
-        ;;
-    x86_64|amd64)
-        APP_ARCH="amd64/"
-        ;;
-    *)
-        SYSTEM_SUFFIX="_native"
-        APP_ARCH=""
-        ;;
-    esac
-fi
+redeclipse_begin() {
+    REDECLIPSE_RETRY="false"
+    redeclipse_update
+    return $?
+}
 
-if [ -x "${APP_PATH}/bin/${APP_ARCH}redeclipse${SYSTEM_SUFFIX}" ]
-then
-    cd "$APP_PATH" || exit 1
-    exec "${APP_PATH}/bin/${APP_ARCH}redeclipse${SYSTEM_SUFFIX}" $APP_OPTIONS "$@"
-else
-    echo "Your platform does not have a pre-compiled Red Eclipse client."
-    echo -n "Would you like to build one now? [Yn] "
-    read CC
-    if [ "$CC" != "n" ]; then
-        cd "${APP_PATH}/src" || exit 1
-        make clean install-client
-        echo "Build complete, please try running the script again."
+redeclipse_retry() {
+    if [ "${REDECLIPSE_RETRY}" != "true" ]; then
+        REDECLIPSE_RETRY="true"
+        echo "Retrying..."
+        redeclipse_update
+        return $?
+    fi
+    redeclipse_runit
+    return $?
+}
+
+redeclipse_update() {
+    chmod +x "${REDECLIPSE_PATH}/bin/update.sh"
+    REDECLIPSE_CALLED="true"
+    . "${REDECLIPSE_PATH}/bin/update.sh"
+    if [ $? -eq 0 ]; then
+        redeclipse_runit
+        return $?
     else
-        echo "Please follow the following steps to build:"
-        echo "1) Ensure you have the SDL, SDL image, SDL mixer, zlib, and OpenGL *DEVELOPMENT* libraries installed."
-        echo "2) Change directory to src/ and type \"make clean install\"."
-        echo "3) If the build succeeds, return to this directory and run this script again."
-        exit 1
+        redeclipse_retry
+        return $?
     fi
-fi
+    return 0
+}
+
+redeclipse_runit() {
+    if [ -e "${REDECLIPSE_PATH}/bin/${REDECLIPSE_ARCH}/${REDECLIPSE_BINARY}${REDECLIPSE_SUFFIX}" ]; then
+        REDECLIPSE_PWD=`pwd`
+        cd "${REDECLIPSE_PATH}" || return 1
+        exec "${REDECLIPSE_PATH}/bin/${REDECLIPSE_ARCH}/${REDECLIPSE_BINARY}${REDECLIPSE_SUFFIX}" ${REDECLIPSE_OPTIONS} "$@" || (
+            cd "${REDECLIPSE_PWD}"
+            return 1
+        )
+        cd "${REDECLIPSE_PWD}"
+        return 0
+    else
+        if [ "${REDECLIPSE_BRANCH}" = "source" ]; then
+            ${REDECLIPSE_MAKE} -C src all install && ( redeclipse_runit; return $? )
+            REDECLIPSE_BRANCH="devel"
+        fi
+        if [ "${REDECLIPSE_BRANCH}" != "inplace" ] && [ "${REDECLIPSE_TRYUPDATE}" != "true" ]; then
+            REDECLIPSE_TRYUPDATE="true"
+            redeclipse_begin
+            return $?
+        fi
+        if [ "${REDECLIPSE_ARCH}" = "amd64" ]; then
+            REDECLIPSE_ARCH="x86"
+            redeclipse_runit
+            return $?
+        elif [ "${REDECLIPSE_ARCH}" = "x86" ]; then
+            REDECLIPSE_ARCH="native"
+            redeclipse_runit
+            return $?
+        fi
+        echo "Unable to find a working binary."
+    fi
+    return 1
+}
+
+redeclipse_path
+redeclipse_init
+redeclipse_setup
 
+if [ $? -ne 0 ]; then
+    echo ""
+    echo "There was an error running Red Eclipse."
+    ${REDECLIPSE_EXITR} 1
+else
+    ${REDECLIPSE_EXITR} 0
+fi
diff --git a/redeclipse_server.sh b/redeclipse_server.sh
index 32f0b6f..524e05b 100755
--- a/redeclipse_server.sh
+++ b/redeclipse_server.sh
@@ -1,69 +1,5 @@
 #!/bin/sh
-# APP_PATH should refer to the directory in which Red Eclipse data files are placed.
-#APP_PATH=~/redeclipse
-#APP_PATH=/usr/local/redeclipse
-APP_PATH="$(cd "$(dirname "$0")" && pwd)"
-
-# APP_OPTIONS contains any command line options you would like to start Red Eclipse with.
-APP_OPTIONS=""
-
-# SYSTEM_NAME should be set to the name of your operating system.
-#SYSTEM_NAME=Linux
-SYSTEM_NAME="$(uname -s)"
-
-# MACHINE_NAME should be set to the name of your processor.
-#MACHINE_NAME=i686
-MACHINE_NAME="$(uname -m)"
-
-if [ -x "${APP_PATH}/bin/redeclipse_server_native" ]
-then
-    SYSTEM_SUFFIX="_native"
-    APP_ARCH=""
-else
-    case "$SYSTEM_NAME" in
-    Linux)
-        SYSTEM_SUFFIX="_linux"
-        ;;
-    FreeBSD)
-        SYSTEM_SUFFIX="_freebsd"
-        ;;
-    *)
-        SYSTEM_SUFFIX="_unknown"
-        ;;
-    esac
-
-    case "$MACHINE_NAME" in
-    i486|i586|i686)
-        APP_ARCH="x86/"
-        ;;
-    x86_64|amd64)
-        APP_ARCH="amd64/"
-        ;;
-    *)
-        SYSTEM_SUFFIX="_native"
-        APP_ARCH=""
-        ;;
-    esac
-fi
-
-if [ -x "${APP_PATH}/bin/${APP_ARCH}redeclipse_server${SYSTEM_SUFFIX}" ]
-then
-    cd "$APP_PATH" || exit 1
-    exec "${APP_PATH}/bin/${APP_ARCH}redeclipse_server${SYSTEM_SUFFIX}" $APP_OPTIONS "$@"
-else
-    echo "Your platform does not have a pre-compiled Red Eclipse server."
-    echo -n "Would you like to build one now? [Yn] "
-    read CC
-    if [ "$CC" != "n" ]; then
-        cd "${APP_PATH}/src" || exit 1
-        make clean install-server
-        echo "Build complete, please try running the script again."
-    else
-        echo "Please follow the following steps to build:"
-        echo "1) Ensure you have the zlib *DEVELOPMENT* libraries installed."
-        echo "2) Change directory to src/ and type \"make clean install-server\"."
-        echo "3) If the build succeeds, return to this directory and run this script again."
-        exit 1
-    fi
-fi
-
+if [ -z "${REDECLIPSE_PATH+isset}" ]; then REDECLIPSE_PATH="$(cd "$(dirname "$0")" && pwd)"; fi
+REDECLIPSE_BINARY="redeclipse_server"
+REDECLIPSE_CALLED="true"
+. "${REDECLIPSE_PATH}/redeclipse.sh"
diff --git a/src/Makefile b/src/Makefile
index 4a23b1d..da23382 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,7 +1,660 @@
 APPNAME=redeclipse
+APPCLIENT=$(APPNAME)
+APPSERVER=$(APPNAME)_server
+
+#CXXFLAGS= -ggdb3
+CXXFLAGS= -O3 -fomit-frame-pointer -ffast-math
+override CXXFLAGS+= -Wall -fsigned-char -fno-exceptions -fno-rtti
+
+PLATFORM= $(shell gcc -dumpmachine)
+
+ifeq (,$(PLATFORM_BIN))
+ifneq (,$(findstring arm,$(PLATFORM)))
+PLATFORM_BIN=arm
+else
+ifneq (,$(findstring 64,$(PLATFORM)))
+PLATFORM_BIN=amd64
+else
+PLATFORM_BIN=x86
+endif
+endif
+endif
+
+ifeq (,$(INSTDIR))
+INSTDIR=../bin/$(PLATFORM_BIN)
+endif
+
+TOOLSET_PREFIX=
+ifneq (,$(findstring cross,$(PLATFORM)))
+ifneq (,$(findstring 64,$(PLATFORM)))
+TOOLSET_PREFIX=x86_64-w64-mingw32-
+else
+TOOLSET_PREFIX=i686-w64-mingw32-
+endif
+CXX=g++
+endif
+
+CXX_TEMP:=$(CXX)
+override CXX=$(TOOLSET_PREFIX)$(CXX_TEMP)
+
+INCLUDES= -I. -Ishared -Iengine -Igame -Ienet/include
+
+
+STRIP=
+ifeq (,$(findstring -g,$(CXXFLAGS)))
+ifeq (,$(findstring -pg,$(CXXFLAGS)))
+	STRIP=strip
+endif
+endif
+STRIP_TEMP:=$(STRIP)
+override STRIP=$(TOOLSET_PREFIX)$(STRIP_TEMP)
+
+MV=mv
+CP=cp
+MKDIR=mkdir -p
+
+ifneq (,$(findstring mingw,$(PLATFORM)))
+APPMODIFIER=_windows
+BIN_SUFFIX=.exe
+WINDRES=windres
+WINDRES_TEMP:=$(WINDRES)
+WINLIB=lib/$(PLATFORM_BIN)
+WINDLL=../bin/$(PLATFORM_BIN)
+override WINDRES=$(TOOLSET_PREFIX)$(WINDRES_TEMP)
+ifneq (,$(findstring 64,$(PLATFORM)))
+override WINDRES+= -F pe-x86-64
+else
+override WINDRES+= -F pe-i386
+endif
+STD_LIBS= -static-libgcc -static-libstdc++
+CLIENT_INCLUDES= $(INCLUDES) -Iinclude
+CLIENT_LIBS= -mwindows $(STD_LIBS) -L$(WINDLL) -L$(WINLIB) -lSDL -lSDL_image -lSDL_mixer -lzlib1 -lopengl32 -lenet -lws2_32 -lwinmm
+else
+APPMODIFIER=
+ifneq (,$(findstring linux,$(PLATFORM)))
+BIN_SUFFIX=_linux
+else
+ifneq (,$(findstring bsd,$(PLATFORM)))
+BIN_SUFFIX=_bsd
+else
+BIN_SUFFIX=_native
+endif
+endif
+CLIENT_INCLUDES= $(INCLUDES) -I/usr/X11R6/include `sdl-config --cflags`
+CLIENT_LIBS= -Lenet -lenet -L/usr/X11R6/lib -lX11 `sdl-config --libs` -lSDL_image -lSDL_mixer -lz -lGL
+endif
+ifneq (,$(findstring linux,$(PLATFORM)))
+CLIENT_LIBS+= -lrt
+else
+ifneq (,$(findstring gnu,$(PLATFORM)))
+CLIENT_LIBS+= -lrt
+endif
+endif
+CLIENT_OBJS= \
+	shared/crypto.o \
+	shared/geom.o \
+	shared/stream.o \
+	shared/tools.o \
+	shared/zip.o \
+	engine/bih.o \
+	engine/blend.o \
+	engine/blob.o \
+	engine/client.o \
+	engine/command.o \
+	engine/console.o \
+	engine/decal.o \
+	engine/dynlight.o \
+	engine/glare.o \
+	engine/grass.o \
+	engine/irc.o	\
+	engine/lightmap.o \
+	engine/main.o \
+	engine/material.o \
+	engine/menus.o \
+	engine/movie.o \
+	engine/normal.o \
+	engine/octa.o \
+	engine/octaedit.o \
+	engine/octarender.o \
+	engine/physics.o \
+	engine/pvs.o \
+	engine/rendergl.o \
+	engine/rendermodel.o \
+	engine/renderparticles.o \
+	engine/rendersky.o \
+	engine/rendertext.o \
+	engine/renderva.o \
+	engine/server.o \
+	engine/serverbrowser.o \
+	engine/shader.o \
+	engine/shadowmap.o \
+	engine/sound.o \
+	engine/texture.o \
+	engine/ui.o \
+	engine/water.o \
+	engine/world.o \
+	engine/worldio.o \
+	game/ai.o \
+	game/client.o \
+	game/capture.o \
+	game/defend.o \
+	game/bomber.o \
+	game/entities.o \
+	game/game.o \
+	game/hud.o \
+	game/physics.o \
+	game/projs.o \
+	game/scoreboard.o \
+	game/server.o \
+	game/waypoint.o \
+	game/weapons.o
+
+CLIENT_PCH= shared/cube.h.gch engine/engine.h.gch
+
+ifneq (,$(findstring mingw,$(PLATFORM)))
+SERVER_INCLUDES= -DSTANDALONE $(INCLUDES) -Iinclude
+SERVER_LIBS= -mwindows $(STD_LIBS) -L$(WINDLL) -L$(WINLIB) -lzlib1 -lenet -lws2_32 -lwinmm
+else
+SERVER_INCLUDES= -DSTANDALONE $(INCLUDES)
+SERVER_LIBS= -Lenet -lenet -lz
+endif
+SERVER_OBJS= \
+	shared/crypto-standalone.o \
+	shared/geom-standalone.o \
+	shared/stream-standalone.o \
+	shared/tools-standalone.o \
+	shared/zip-standalone.o \
+	engine/command-standalone.o \
+	engine/irc-standalone.o \
+	engine/master-standalone.o \
+	engine/server-standalone.o \
+	game/server-standalone.o
+
+LIBENET= enet/libenet.a
 
 all:
 
-include redeclipse.mk
+default: all
+
+clean-enet:
+	$(MAKE) -C enet clean
+
+clean-client:
+	@rm -fv $(CLIENT_PCH) $(CLIENT_OBJS) $(APPCLIENT)$(BIN_SUFFIX)
+
+clean-server:
+	@rm -fv $(SERVER_OBJS) $(APPSERVER)$(BIN_SUFFIX)
+
+clean: clean-enet clean-client clean-server
+
+%.h.gch: %.h
+	$(CXX) $(CXXFLAGS) -x c++-header -o $(subst .h.gch,.tmp.h.gch,$@) $(subst .h.gch,.h,$@)
+	$(MV) $(subst .h.gch,.tmp.h.gch,$@) $@
+
+%-standalone.o: %.cpp
+	$(CXX) $(CXXFLAGS) -c -o $@ $(subst -standalone.o,.cpp,$@)
+
+$(CLIENT_OBJS): CXXFLAGS += $(CLIENT_INCLUDES)
+$(filter shared/%,$(CLIENT_OBJS)): $(filter shared/%,$(CLIENT_PCH))
+$(filter engine/%,$(CLIENT_OBJS)): $(filter engine/%,$(CLIENT_PCH))
+$(filter game/%,$(CLIENT_OBJS)): $(filter game/%,$(CLIENT_PCH))
+
+$(SERVER_OBJS): CXXFLAGS += $(SERVER_INCLUDES)
+
+$(APPCLIENT)_windows$(BIN_SUFFIX): $(CLIENT_OBJS)
+	$(WINDRES) -i $(APPNAME).rc -J rc -o $(APPNAME).res -O coff
+	$(CXX) $(CXXFLAGS) -o $@ $(APPNAME).res $(CLIENT_OBJS) $(CLIENT_LIBS)
+
+$(APPSERVER)_windows$(BIN_SUFFIX): $(SERVER_OBJS)
+	$(WINDRES) -i $(APPNAME).rc -J rc -o $(APPNAME).res -O coff
+	$(CXX) $(CXXFLAGS) -o $@ $(APPNAME).res $(SERVER_OBJS) $(SERVER_LIBS)
+
+$(APPCLIENT)$(BIN_SUFFIX): $(LIBENET) $(CLIENT_OBJS)
+	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(CLIENT_OBJS) $(CLIENT_LIBS)
+
+$(APPSERVER)$(BIN_SUFFIX): $(LIBENET) $(SERVER_OBJS)
+	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(SERVER_OBJS) $(SERVER_LIBS)
+
+client: $(APPCLIENT)$(APPMODIFIER)$(BIN_SUFFIX)
+
+server: $(APPSERVER)$(APPMODIFIER)$(BIN_SUFFIX)
+
+$(INSTDIR)/%$(BIN_SUFFIX): %$(APPMODIFIER)$(BIN_SUFFIX)
+	$(MKDIR) $(INSTDIR)
+	$(CP) $< $@
+ifneq (,$(STRIP))
+	$(STRIP) $@
+endif
+
+install-client: $(INSTDIR)/$(APPCLIENT)$(BIN_SUFFIX)
+
+install-server: $(INSTDIR)/$(APPSERVER)$(BIN_SUFFIX)
+
+ifeq (,$(findstring mingw,$(PLATFORM)))
+shared/cube2font.o: shared/cube2font.c
+	$(CXX) $(CXXFLAGS) -c -o $@ $< `freetype-config --cflags`
+
+cube2font: shared/cube2font.o
+	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $< `freetype-config --libs` -lz
+
+install-cube2font: cube2font
+	$(MKDIR) $(INSTDIR)
+	install -m 755 $< $(INSTDIR)/$<
+endif
+
+install: install-client install-server
+
+$(LIBENET):
+	$(MAKE) -C enet
+
+depend:
+	makedepend -Y -I. -Ishared -Iengine -Igame $(subst .o,.cpp,$(CLIENT_OBJS))
+	makedepend -a -o-standalone.o -Y -I. -Ishared -Iengine -Igame -DSTANDALONE $(subst -standalone.o,.cpp,$(SERVER_OBJS))
+	makedepend -a -o.h.gch -Y -I. -Ishared -Iengine -Igame $(subst .h.gch,.h,$(CLIENT_PCH))
+
+all: client server
+
+include system-install.mk
+include dist.mk
+include wiki.mk
+
+engine/engine.h.gch: version.h shared/cube.h.gch
+
+# DO NOT DELETE
+
+shared/crypto.o: version.h shared/cube.h shared/tools.h shared/command.h shared/geom.h
+shared/crypto.o: shared/ents.h shared/iengine.h shared/igame.h
+shared/geom.o: version.h shared/cube.h shared/tools.h shared/command.h shared/geom.h
+shared/geom.o: shared/ents.h shared/iengine.h shared/igame.h
+shared/stream.o: version.h shared/cube.h shared/tools.h shared/command.h shared/geom.h
+shared/stream.o: shared/ents.h shared/iengine.h shared/igame.h
+shared/tools.o: version.h shared/cube.h shared/tools.h shared/command.h shared/geom.h
+shared/tools.o: shared/ents.h shared/iengine.h shared/igame.h
+shared/zip.o: version.h shared/cube.h shared/tools.h shared/command.h shared/geom.h
+shared/zip.o: shared/ents.h shared/iengine.h shared/igame.h
+engine/bih.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/bih.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/bih.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/bih.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/bih.o: engine/model.h engine/varray.h
+engine/blend.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/blend.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/blend.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/blend.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/blend.o: engine/model.h engine/varray.h
+engine/blob.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/blob.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/blob.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/blob.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/blob.o: engine/model.h engine/varray.h
+engine/client.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/client.o: shared/command.h shared/geom.h shared/ents.h
+engine/client.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/client.o: engine/world.h engine/glexts.h engine/octa.h
+engine/client.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/client.o: engine/model.h engine/varray.h
+engine/command.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/command.o: shared/command.h shared/geom.h shared/ents.h
+engine/command.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/command.o: engine/world.h engine/glexts.h engine/octa.h
+engine/command.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/command.o: engine/model.h engine/varray.h
+engine/console.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/console.o: shared/command.h shared/geom.h shared/ents.h
+engine/console.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/console.o: engine/world.h engine/glexts.h engine/octa.h
+engine/console.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/console.o: engine/model.h engine/varray.h
+engine/decal.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/decal.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/decal.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/decal.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/decal.o: engine/model.h engine/varray.h
+engine/dynlight.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/dynlight.o: shared/command.h shared/geom.h shared/ents.h
+engine/dynlight.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/dynlight.o: engine/sound.h engine/world.h engine/glexts.h
+engine/dynlight.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/dynlight.o: engine/texture.h engine/model.h engine/varray.h
+engine/glare.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/glare.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/glare.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/glare.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/glare.o: engine/model.h engine/varray.h engine/rendertarget.h
+engine/grass.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/grass.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/grass.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/grass.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/grass.o: engine/model.h engine/varray.h
+engine/irc.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/irc.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/irc.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/irc.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/irc.o: engine/model.h engine/varray.h
+engine/lightmap.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/lightmap.o: shared/command.h shared/geom.h shared/ents.h
+engine/lightmap.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/lightmap.o: engine/sound.h engine/world.h engine/glexts.h
+engine/lightmap.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/lightmap.o: engine/texture.h engine/model.h engine/varray.h
+engine/main.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/main.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/main.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/main.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/main.o: engine/model.h engine/varray.h
+engine/material.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/material.o: shared/command.h shared/geom.h shared/ents.h
+engine/material.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/material.o: engine/sound.h engine/world.h engine/glexts.h
+engine/material.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/material.o: engine/texture.h engine/model.h engine/varray.h
+engine/menus.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/menus.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/menus.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/menus.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/menus.o: engine/model.h engine/varray.h
+engine/movie.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/movie.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/movie.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/movie.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/movie.o: engine/model.h engine/varray.h
+engine/normal.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/normal.o: shared/command.h shared/geom.h shared/ents.h
+engine/normal.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/normal.o: engine/world.h engine/glexts.h engine/octa.h
+engine/normal.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/normal.o: engine/model.h engine/varray.h
+engine/octa.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/octa.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/octa.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/octa.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/octa.o: engine/model.h engine/varray.h
+engine/octaedit.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/octaedit.o: shared/command.h shared/geom.h shared/ents.h
+engine/octaedit.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/octaedit.o: engine/sound.h engine/world.h engine/glexts.h
+engine/octaedit.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/octaedit.o: engine/texture.h engine/model.h engine/varray.h
+engine/octarender.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/octarender.o: shared/command.h shared/geom.h shared/ents.h
+engine/octarender.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/octarender.o: engine/sound.h engine/world.h engine/glexts.h
+engine/octarender.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/octarender.o: engine/texture.h engine/model.h engine/varray.h
+engine/physics.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/physics.o: shared/command.h shared/geom.h shared/ents.h
+engine/physics.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/physics.o: engine/world.h engine/glexts.h engine/octa.h
+engine/physics.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/physics.o: engine/model.h engine/varray.h engine/mpr.h
+engine/pvs.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/pvs.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/pvs.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/pvs.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/pvs.o: engine/model.h engine/varray.h
+engine/rendergl.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/rendergl.o: shared/command.h shared/geom.h shared/ents.h
+engine/rendergl.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/rendergl.o: engine/sound.h engine/world.h engine/glexts.h
+engine/rendergl.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/rendergl.o: engine/texture.h engine/model.h engine/varray.h
+engine/rendermodel.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/rendermodel.o: shared/command.h shared/geom.h shared/ents.h
+engine/rendermodel.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/rendermodel.o: engine/sound.h engine/world.h engine/glexts.h
+engine/rendermodel.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/rendermodel.o: engine/texture.h engine/model.h engine/varray.h
+engine/rendermodel.o: engine/ragdoll.h engine/animmodel.h engine/vertmodel.h
+engine/rendermodel.o: engine/skelmodel.h engine/md2.h engine/md3.h
+engine/rendermodel.o: engine/md5.h engine/obj.h engine/smd.h engine/iqm.h
+engine/renderparticles.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/renderparticles.o: shared/command.h shared/geom.h shared/ents.h
+engine/renderparticles.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/renderparticles.o: engine/sound.h engine/world.h engine/glexts.h
+engine/renderparticles.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/renderparticles.o: engine/texture.h engine/model.h engine/varray.h
+engine/renderparticles.o: engine/rendertarget.h engine/depthfx.h
+engine/renderparticles.o: engine/lensflare.h engine/explosion.h
+engine/renderparticles.o: engine/lightning.h
+engine/rendersky.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/rendersky.o: shared/command.h shared/geom.h shared/ents.h
+engine/rendersky.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/rendersky.o: engine/sound.h engine/world.h engine/glexts.h
+engine/rendersky.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/rendersky.o: engine/texture.h engine/model.h engine/varray.h
+engine/rendertext.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/rendertext.o: shared/command.h shared/geom.h shared/ents.h
+engine/rendertext.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/rendertext.o: engine/sound.h engine/world.h engine/glexts.h
+engine/rendertext.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/rendertext.o: engine/texture.h engine/model.h engine/varray.h
+engine/renderva.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/renderva.o: shared/command.h shared/geom.h shared/ents.h
+engine/renderva.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/renderva.o: engine/sound.h engine/world.h engine/glexts.h
+engine/renderva.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/renderva.o: engine/texture.h engine/model.h engine/varray.h
+engine/server.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/server.o: shared/command.h shared/geom.h shared/ents.h
+engine/server.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/server.o: engine/world.h engine/glexts.h engine/octa.h
+engine/server.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/server.o: engine/model.h engine/varray.h
+engine/serverbrowser.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/serverbrowser.o: shared/command.h shared/geom.h shared/ents.h
+engine/serverbrowser.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/serverbrowser.o: engine/sound.h engine/world.h engine/glexts.h
+engine/serverbrowser.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/serverbrowser.o: engine/texture.h engine/model.h engine/varray.h
+engine/shader.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/shader.o: shared/command.h shared/geom.h shared/ents.h
+engine/shader.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/shader.o: engine/world.h engine/glexts.h engine/octa.h
+engine/shader.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/shader.o: engine/model.h engine/varray.h
+engine/shadowmap.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/shadowmap.o: shared/command.h shared/geom.h shared/ents.h
+engine/shadowmap.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/shadowmap.o: engine/sound.h engine/world.h engine/glexts.h
+engine/shadowmap.o: engine/octa.h engine/lightmap.h engine/bih.h
+engine/shadowmap.o: engine/texture.h engine/model.h engine/varray.h
+engine/shadowmap.o: engine/rendertarget.h
+engine/sound.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/sound.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/sound.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/sound.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/sound.o: engine/model.h engine/varray.h
+engine/texture.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/texture.o: shared/command.h shared/geom.h shared/ents.h
+engine/texture.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/texture.o: engine/world.h engine/glexts.h engine/octa.h
+engine/texture.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/texture.o: engine/model.h engine/varray.h engine/scale.h
+engine/ui.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/ui.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/ui.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/ui.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/ui.o: engine/model.h engine/varray.h engine/textedit.h
+engine/water.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/water.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/water.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/water.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/water.o: engine/model.h engine/varray.h
+engine/world.o: engine/engine.h version.h shared/cube.h shared/tools.h shared/command.h
+engine/world.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
+engine/world.o: engine/irc.h engine/sound.h engine/world.h engine/glexts.h
+engine/world.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
+engine/world.o: engine/model.h engine/varray.h
+engine/worldio.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/worldio.o: shared/command.h shared/geom.h shared/ents.h
+engine/worldio.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+engine/worldio.o: engine/world.h engine/glexts.h engine/octa.h
+engine/worldio.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/worldio.o: engine/model.h engine/varray.h
+game/ai.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/ai.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/ai.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/ai.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/ai.o: engine/texture.h engine/model.h engine/varray.h game/gamemode.h
+game/ai.o: game/weapons.h game/weapdef.h game/player.h game/teamdef.h
+game/ai.o: game/playerdef.h game/ai.h game/vars.h game/capture.h
+game/ai.o: game/defend.h game/bomber.h
+game/client.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/client.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/client.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/client.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/client.o: engine/texture.h engine/model.h engine/varray.h
+game/client.o: game/gamemode.h game/weapons.h game/weapdef.h game/player.h
+game/client.o: game/teamdef.h game/playerdef.h game/ai.h game/vars.h
+game/client.o: game/capture.h game/defend.h game/bomber.h
+game/capture.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/capture.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/capture.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/capture.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/capture.o: engine/texture.h engine/model.h engine/varray.h
+game/capture.o: game/gamemode.h game/weapons.h game/weapdef.h game/player.h
+game/capture.o: game/teamdef.h game/playerdef.h game/ai.h game/vars.h
+game/capture.o: game/capture.h game/defend.h game/bomber.h
+game/defend.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/defend.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/defend.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/defend.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/defend.o: engine/texture.h engine/model.h engine/varray.h
+game/defend.o: game/gamemode.h game/weapons.h game/weapdef.h game/player.h
+game/defend.o: game/teamdef.h game/playerdef.h game/ai.h game/vars.h
+game/defend.o: game/capture.h game/defend.h game/bomber.h
+game/bomber.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/bomber.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/bomber.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/bomber.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/bomber.o: engine/texture.h engine/model.h engine/varray.h
+game/bomber.o: game/gamemode.h game/weapons.h game/weapdef.h game/player.h
+game/bomber.o: game/teamdef.h game/playerdef.h game/ai.h game/vars.h
+game/bomber.o: game/capture.h game/defend.h game/bomber.h
+game/entities.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/entities.o: shared/command.h shared/geom.h shared/ents.h
+game/entities.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+game/entities.o: engine/world.h engine/glexts.h engine/octa.h
+game/entities.o: engine/lightmap.h engine/bih.h engine/texture.h
+game/entities.o: engine/model.h engine/varray.h game/gamemode.h
+game/entities.o: game/weapons.h game/weapdef.h game/player.h game/teamdef.h
+game/entities.o: game/playerdef.h game/ai.h game/vars.h game/capture.h
+game/entities.o: game/defend.h game/bomber.h
+game/game.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/game.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/game.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/game.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/game.o: engine/texture.h engine/model.h engine/varray.h game/gamemode.h
+game/game.o: game/weapons.h game/weapdef.h game/player.h game/teamdef.h
+game/game.o: game/playerdef.h game/ai.h game/vars.h game/capture.h
+game/game.o: game/defend.h game/bomber.h
+game/hud.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/hud.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/hud.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/hud.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/hud.o: engine/texture.h engine/model.h engine/varray.h game/gamemode.h
+game/hud.o: game/weapons.h game/weapdef.h game/player.h game/teamdef.h
+game/hud.o: game/playerdef.h game/ai.h game/vars.h game/capture.h
+game/hud.o: game/defend.h game/bomber.h game/compass.h
+game/physics.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/physics.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/physics.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/physics.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/physics.o: engine/texture.h engine/model.h engine/varray.h
+game/physics.o: game/gamemode.h game/weapons.h game/weapdef.h game/player.h
+game/physics.o: game/teamdef.h game/playerdef.h game/ai.h game/vars.h
+game/physics.o: game/capture.h game/defend.h game/bomber.h
+game/projs.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/projs.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/projs.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/projs.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/projs.o: engine/texture.h engine/model.h engine/varray.h game/gamemode.h
+game/projs.o: game/weapons.h game/weapdef.h game/player.h game/teamdef.h
+game/projs.o: game/playerdef.h game/ai.h game/vars.h game/capture.h
+game/projs.o: game/defend.h game/bomber.h
+game/scoreboard.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/scoreboard.o: shared/command.h shared/geom.h shared/ents.h
+game/scoreboard.o: shared/iengine.h shared/igame.h engine/irc.h
+game/scoreboard.o: engine/sound.h engine/world.h engine/glexts.h
+game/scoreboard.o: engine/octa.h engine/lightmap.h engine/bih.h
+game/scoreboard.o: engine/texture.h engine/model.h engine/varray.h
+game/scoreboard.o: game/gamemode.h game/weapons.h game/weapdef.h
+game/scoreboard.o: game/player.h game/teamdef.h game/playerdef.h game/ai.h
+game/scoreboard.o: game/vars.h game/capture.h game/defend.h game/bomber.h
+game/server.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/server.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/server.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/server.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/server.o: engine/texture.h engine/model.h engine/varray.h
+game/server.o: game/gamemode.h game/weapons.h game/weapdef.h game/player.h
+game/server.o: game/teamdef.h game/playerdef.h game/ai.h game/vars.h
+game/server.o: game/capture.h game/defend.h game/bomber.h game/auth.h
+game/server.o: game/capturemode.h game/defendmode.h game/bombermode.h
+game/server.o: game/duelmut.h game/aiman.h
+game/waypoint.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/waypoint.o: shared/command.h shared/geom.h shared/ents.h
+game/waypoint.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
+game/waypoint.o: engine/world.h engine/glexts.h engine/octa.h
+game/waypoint.o: engine/lightmap.h engine/bih.h engine/texture.h
+game/waypoint.o: engine/model.h engine/varray.h game/gamemode.h
+game/waypoint.o: game/weapons.h game/weapdef.h game/player.h game/teamdef.h
+game/waypoint.o: game/playerdef.h game/ai.h game/vars.h game/capture.h
+game/waypoint.o: game/defend.h game/bomber.h
+game/weapons.o: game/game.h engine/engine.h version.h shared/cube.h shared/tools.h
+game/weapons.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
+game/weapons.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
+game/weapons.o: engine/glexts.h engine/octa.h engine/lightmap.h engine/bih.h
+game/weapons.o: engine/texture.h engine/model.h engine/varray.h
+game/weapons.o: game/gamemode.h game/weapons.h game/weapdef.h game/player.h
+game/weapons.o: game/teamdef.h game/playerdef.h game/ai.h game/vars.h
+game/weapons.o: game/capture.h game/defend.h game/bomber.h
+
+shared/crypto-standalone.o: version.h shared/cube.h shared/tools.h shared/command.h
+shared/crypto-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
+shared/crypto-standalone.o: shared/igame.h
+shared/geom-standalone.o: version.h shared/cube.h shared/tools.h shared/command.h
+shared/geom-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
+shared/geom-standalone.o: shared/igame.h
+shared/stream-standalone.o: version.h shared/cube.h shared/tools.h shared/command.h
+shared/stream-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
+shared/stream-standalone.o: shared/igame.h
+shared/tools-standalone.o: version.h shared/cube.h shared/tools.h shared/command.h
+shared/tools-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
+shared/tools-standalone.o: shared/igame.h
+shared/zip-standalone.o: version.h shared/cube.h shared/tools.h shared/command.h
+shared/zip-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
+shared/zip-standalone.o: shared/igame.h
+engine/command-standalone.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/command-standalone.o: shared/command.h shared/geom.h shared/ents.h
+engine/command-standalone.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/command-standalone.o: engine/sound.h
+engine/irc-standalone.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/irc-standalone.o: shared/command.h shared/geom.h shared/ents.h
+engine/irc-standalone.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/irc-standalone.o: engine/sound.h
+engine/master-standalone.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/master-standalone.o: shared/command.h shared/geom.h shared/ents.h
+engine/master-standalone.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/master-standalone.o: engine/sound.h
+engine/server-standalone.o: engine/engine.h version.h shared/cube.h shared/tools.h
+engine/server-standalone.o: shared/command.h shared/geom.h shared/ents.h
+engine/server-standalone.o: shared/iengine.h shared/igame.h engine/irc.h
+engine/server-standalone.o: engine/sound.h
+game/server-standalone.o: game/game.h engine/engine.h version.h shared/cube.h
+game/server-standalone.o: shared/tools.h shared/command.h shared/geom.h
+game/server-standalone.o: shared/ents.h shared/iengine.h shared/igame.h
+game/server-standalone.o: engine/irc.h engine/sound.h game/gamemode.h
+game/server-standalone.o: game/weapons.h game/weapdef.h game/player.h
+game/server-standalone.o: game/teamdef.h game/playerdef.h game/vars.h
+game/server-standalone.o: game/capture.h game/defend.h game/bomber.h
+game/server-standalone.o: game/auth.h game/capturemode.h game/defendmode.h
+game/server-standalone.o: game/bombermode.h game/duelmut.h game/aiman.h
 
-include core.mk
+shared/cube.h.gch: shared/tools.h shared/command.h shared/geom.h
+shared/cube.h.gch: shared/ents.h shared/iengine.h shared/igame.h
+engine/engine.h.gch: version.h shared/cube.h shared/tools.h shared/command.h
+engine/engine.h.gch: shared/geom.h shared/ents.h shared/iengine.h
+engine/engine.h.gch: shared/igame.h engine/irc.h engine/sound.h
+engine/engine.h.gch: engine/world.h engine/glexts.h engine/octa.h
+engine/engine.h.gch: engine/lightmap.h engine/bih.h engine/texture.h
+engine/engine.h.gch: engine/model.h engine/varray.h
diff --git a/src/core.mk b/src/core.mk
deleted file mode 100644
index 5c900f0..0000000
--- a/src/core.mk
+++ /dev/null
@@ -1,618 +0,0 @@
-APPNAME?=appname
-APPCLIENT=$(APPNAME)
-APPSERVER=$(APPNAME)_server
-
-#CXXFLAGS= -ggdb3
-CXXFLAGS= -O3 -fomit-frame-pointer
-override CXXFLAGS+= -Wall -fsigned-char -fno-exceptions -fno-rtti
-
-PLATFORM= $(shell uname -s)
-PLATFORM_SUFFIX=_native
-PLATFORM_BIN=
-
-ifneq (,$(PLATFORM_BIN))
-INSTDIR=../bin/$(PLATFORM_BIN)/
-else
-INSTDIR=../bin/
-endif
-
-TOOLSET_PREFIX=
-ifneq (,$(findstring CROSS,$(PLATFORM)))
-ifneq (,$(findstring 64,$(PLATFORM)))
-TOOLSET_PREFIX=x86_64-w64-mingw32-
-else
-TOOLSET_PREFIX=i686-w64-mingw32-
-endif
-CXX=g++
-endif
-CXX_TEMP:=$(CXX)
-override CXX=$(TOOLSET_PREFIX)$(CXX_TEMP)
-
-INCLUDES= -Ishared -Iengine -Igame -Ienet/include $(APPFLAGS)
-
-
-STRIP=
-ifeq (,$(findstring -g,$(CXXFLAGS)))
-ifeq (,$(findstring -pg,$(CXXFLAGS)))
-	STRIP=strip
-endif
-endif
-STRIP_TEMP:=$(STRIP)
-override STRIP=$(TOOLSET_PREFIX)$(STRIP_TEMP)
-
-MV=mv
-
-ifneq (,$(findstring MINGW,$(PLATFORM)))
-WINDRES=windres
-WINDRES_TEMP:=$(WINDRES)
-override WINDRES=$(TOOLSET_PREFIX)$(WINDRES_TEMP)
-ifneq (,$(findstring 64,$(PLATFORM)))
-WINLIB=lib/amd64
-WINBIN=../bin/amd64
-override CXX+= -m64
-override WINDRES+= -F pe-x86-64
-else
-WINLIB=lib/x86
-WINBIN=../bin/x86
-override CXX+= -m32
-override WINDRES+= -F pe-i386
-endif
-ifneq (,$(findstring TDM,$(PLATFORM)))
-STD_LIBS=
-else
-STD_LIBS= -static-libgcc -static-libstdc++
-endif
-CLIENT_INCLUDES= $(INCLUDES) -Iinclude
-CLIENT_LIBS= -mwindows $(STD_LIBS) -L$(WINBIN) -L$(WINLIB) -lSDL -lSDL_image -lSDL_mixer -lzlib1 -lopengl32 -lenet -lws2_32 -lwinmm
-else
-CLIENT_INCLUDES= $(INCLUDES) -I/usr/X11R6/include `sdl-config --cflags`
-CLIENT_LIBS= -Lenet/.libs -lenet -L/usr/X11R6/lib -lX11 `sdl-config --libs` -lSDL_image -lSDL_mixer -lz -lGL
-endif
-ifeq ($(PLATFORM),Linux)
-CLIENT_LIBS+= -lrt
-endif
-CLIENT_OBJS= \
-	shared/crypto.o \
-	shared/geom.o \
-	shared/stream.o \
-	shared/tools.o \
-	shared/zip.o \
-	engine/bih.o \
-	engine/blend.o \
-	engine/blob.o \
-	engine/client.o \
-	engine/command.o \
-	engine/console.o \
-	engine/decal.o \
-	engine/dynlight.o \
-	engine/glare.o \
-	engine/grass.o \
-	engine/irc.o	\
-	engine/lightmap.o \
-	engine/main.o \
-	engine/material.o \
-	engine/menus.o \
-	engine/movie.o \
-	engine/normal.o \
-	engine/octa.o \
-	engine/octaedit.o \
-	engine/octarender.o \
-	engine/physics.o \
-	engine/pvs.o \
-	engine/rendergl.o \
-	engine/rendermodel.o \
-	engine/renderparticles.o \
-	engine/rendersky.o \
-	engine/rendertext.o \
-	engine/renderva.o \
-	engine/server.o \
-	engine/serverbrowser.o \
-	engine/shader.o \
-	engine/shadowmap.o \
-	engine/sound.o \
-	engine/texture.o \
-	engine/ui.o \
-	engine/water.o \
-	engine/world.o \
-	engine/worldio.o \
-	game/ai.o \
-	game/client.o \
-	game/capture.o \
-	game/defend.o \
-	game/bomber.o \
-	game/entities.o \
-	game/game.o \
-	game/hud.o \
-	game/physics.o \
-	game/projs.o \
-	game/scoreboard.o \
-	game/server.o \
-	game/waypoint.o \
-	game/weapons.o
-
-CLIENT_PCH= shared/cube.h.gch engine/engine.h.gch
-
-ifneq (,$(findstring MINGW,$(PLATFORM)))
-SERVER_INCLUDES= -DSTANDALONE $(INCLUDES) -Iinclude
-SERVER_LIBS= -mwindows $(STD_LIBS) -L$(WINBIN) -L$(WINLIB) -lzlib1 -lenet -lws2_32 -lwinmm
-else
-SERVER_INCLUDES= -DSTANDALONE $(INCLUDES)
-SERVER_LIBS= -Lenet/.libs -lenet -lz
-endif
-SERVER_OBJS= \
-	shared/crypto-standalone.o \
-	shared/geom-standalone.o \
-	shared/stream-standalone.o \
-	shared/tools-standalone.o \
-	engine/command-standalone.o \
-	engine/irc-standalone.o \
-	engine/master-standalone.o \
-	engine/server-standalone.o \
-	game/server-standalone.o
-
-default: all
-
-enet/Makefile:
-	cd enet; ./configure --enable-shared=no --enable-static=yes
-
-libenet: enet/Makefile
-	$(MAKE) -C enet/ all
-
-clean-enet: enet/Makefile
-	$(MAKE) -C enet/ clean
-
-clean: clean-client clean-server
-
-clean-client:
-	@rm -fv $(CLIENT_PCH) $(CLIENT_OBJS) $(APPCLIENT)
-
-clean-server:
-	@rm -fv $(SERVER_OBJS) $(APPSERVER)
-
-%.h.gch: %.h
-	$(CXX) $(CXXFLAGS) -o $(subst .h.gch,.tmp.h.gch,$@) $(subst .h.gch,.h,$@)
-	$(MV) $(subst .h.gch,.tmp.h.gch,$@) $@
-
-%-standalone.o: %.cpp
-	$(CXX) $(CXXFLAGS) -c -o $@ $(subst -standalone.o,.cpp,$@)
-
-$(CLIENT_OBJS): CXXFLAGS += $(CLIENT_INCLUDES)
-$(filter shared/%,$(CLIENT_OBJS)): $(filter shared/%,$(CLIENT_PCH))
-$(filter engine/%,$(CLIENT_OBJS)): $(filter engine/%,$(CLIENT_PCH))
-$(filter game/%,$(CLIENT_OBJS)): $(filter game/%,$(CLIENT_PCH))
-
-$(SERVER_OBJS): CXXFLAGS += $(SERVER_INCLUDES)
-
-ifneq (,$(findstring MINGW,$(PLATFORM)))
-client: $(CLIENT_OBJS)
-	$(WINDRES) -i $(APPNAME).rc -J rc -o $(APPNAME).res -O coff
-	$(CXX) $(CXXFLAGS) -o $(WINBIN)/$(APPCLIENT).exe $(APPNAME).res $(CLIENT_OBJS) $(CLIENT_LIBS)
-
-server: $(SERVER_OBJS)
-	$(WINDRES) -i $(APPNAME).rc -J rc -o $(APPNAME).res -O coff
-	$(CXX) $(CXXFLAGS) -o $(WINBIN)/$(APPSERVER).exe $(APPNAME).res $(SERVER_OBJS) $(SERVER_LIBS)
-
-install-client: client
-ifneq (,$(STRIP))
-	$(STRIP) $(WINBIN)/$(APPCLIENT).exe
-endif
-
-install-server: server
-ifneq (,$(STRIP))
-	$(STRIP) $(WINBIN)/$(APPSERVER).exe
-endif
-else
-client: libenet $(CLIENT_OBJS)
-	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $(APPCLIENT) $(CLIENT_OBJS) $(CLIENT_LIBS)
-ifneq (,$(STRIP))
-	$(STRIP) $(APPCLIENT)
-endif
-
-server: libenet $(SERVER_OBJS)
-	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $(APPSERVER) $(SERVER_OBJS) $(SERVER_LIBS)
-ifneq (,$(STRIP))
-	$(STRIP) $(APPSERVER)
-endif
-
-install-client: client
-	install -d $(INSTDIR)
-	install -m 755 $(APPCLIENT) $(INSTDIR)$(APPCLIENT)$(PLATFORM_SUFFIX)
-
-install-server: server
-	install -d $(INSTDIR)
-	install -m 755 $(APPSERVER) $(INSTDIR)$(APPSERVER)$(PLATFORM_SUFFIX)
-
-shared/cube2font.o: shared/cube2font.c
-	$(CXX) $(CXXFLAGS) -c -o $@ $< `freetype-config --cflags`
-
-cube2font: shared/cube2font.o
-	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o cube2font shared/cube2font.o `freetype-config --libs` -lz
-
-install-cube2font: cube2font
-	install -d ../bin
-	install -m 755 cube2font ../bin/cube2font
-endif
-
-install: install-client install-server
-
-depend:
-	makedepend -Y -Ishared -Iengine -Igame $(subst .o,.cpp,$(CLIENT_OBJS))
-	makedepend -a -o-standalone.o -Y -Ishared -Iengine -Igame -DSTANDALONE $(subst -standalone.o,.cpp,$(SERVER_OBJS))
-	makedepend -a -o.h.gch -Y -Ishared -Iengine -Igame $(subst .h.gch,.h,$(CLIENT_PCH))
-
-all: client server
-
-include system-install.mk
-include dist.mk
-
-engine/engine.h.gch: shared/cube.h.gch
-
-# DO NOT DELETE
-
-shared/crypto.o: shared/cube.h shared/tools.h shared/command.h shared/geom.h
-shared/crypto.o: shared/ents.h shared/iengine.h shared/igame.h
-shared/geom.o: shared/cube.h shared/tools.h shared/command.h shared/geom.h
-shared/geom.o: shared/ents.h shared/iengine.h shared/igame.h
-shared/stream.o: shared/cube.h shared/tools.h shared/command.h shared/geom.h
-shared/stream.o: shared/ents.h shared/iengine.h shared/igame.h
-shared/tools.o: shared/cube.h shared/tools.h shared/command.h shared/geom.h
-shared/tools.o: shared/ents.h shared/iengine.h shared/igame.h
-shared/zip.o: shared/cube.h shared/tools.h shared/command.h shared/geom.h
-shared/zip.o: shared/ents.h shared/iengine.h shared/igame.h
-engine/bih.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/bih.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/bih.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/bih.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
-engine/bih.o: engine/varray.h
-engine/blend.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/blend.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/blend.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/blend.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/blend.o: engine/model.h engine/varray.h
-engine/blob.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/blob.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/blob.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/blob.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
-engine/blob.o: engine/varray.h
-engine/client.o: engine/engine.h shared/cube.h shared/tools.h
-engine/client.o: shared/command.h shared/geom.h shared/ents.h
-engine/client.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/client.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/client.o: engine/texture.h engine/model.h engine/varray.h
-engine/command.o: engine/engine.h shared/cube.h shared/tools.h
-engine/command.o: shared/command.h shared/geom.h shared/ents.h
-engine/command.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/command.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/command.o: engine/texture.h engine/model.h engine/varray.h
-engine/console.o: engine/engine.h shared/cube.h shared/tools.h
-engine/console.o: shared/command.h shared/geom.h shared/ents.h
-engine/console.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/console.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/console.o: engine/texture.h engine/model.h engine/varray.h
-engine/decal.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/decal.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/decal.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/decal.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/decal.o: engine/model.h engine/varray.h
-engine/dynlight.o: engine/engine.h shared/cube.h shared/tools.h
-engine/dynlight.o: shared/command.h shared/geom.h shared/ents.h
-engine/dynlight.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/dynlight.o: engine/sound.h engine/world.h engine/octa.h
-engine/dynlight.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/dynlight.o: engine/model.h engine/varray.h
-engine/glare.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/glare.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/glare.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/glare.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/glare.o: engine/model.h engine/varray.h engine/rendertarget.h
-engine/grass.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/grass.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/grass.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/grass.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/grass.o: engine/model.h engine/varray.h
-engine/irc.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/irc.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/irc.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/irc.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
-engine/irc.o: engine/varray.h
-engine/lightmap.o: engine/engine.h shared/cube.h shared/tools.h
-engine/lightmap.o: shared/command.h shared/geom.h shared/ents.h
-engine/lightmap.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/lightmap.o: engine/sound.h engine/world.h engine/octa.h
-engine/lightmap.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/lightmap.o: engine/model.h engine/varray.h
-engine/main.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/main.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/main.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/main.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
-engine/main.o: engine/varray.h
-engine/material.o: engine/engine.h shared/cube.h shared/tools.h
-engine/material.o: shared/command.h shared/geom.h shared/ents.h
-engine/material.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/material.o: engine/sound.h engine/world.h engine/octa.h
-engine/material.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/material.o: engine/model.h engine/varray.h
-engine/menus.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/menus.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/menus.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/menus.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/menus.o: engine/model.h engine/varray.h
-engine/movie.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/movie.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/movie.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/movie.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/movie.o: engine/model.h engine/varray.h
-engine/normal.o: engine/engine.h shared/cube.h shared/tools.h
-engine/normal.o: shared/command.h shared/geom.h shared/ents.h
-engine/normal.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/normal.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/normal.o: engine/texture.h engine/model.h engine/varray.h
-engine/octa.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/octa.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/octa.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/octa.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
-engine/octa.o: engine/varray.h
-engine/octaedit.o: engine/engine.h shared/cube.h shared/tools.h
-engine/octaedit.o: shared/command.h shared/geom.h shared/ents.h
-engine/octaedit.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/octaedit.o: engine/sound.h engine/world.h engine/octa.h
-engine/octaedit.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/octaedit.o: engine/model.h engine/varray.h
-engine/octarender.o: engine/engine.h shared/cube.h shared/tools.h
-engine/octarender.o: shared/command.h shared/geom.h shared/ents.h
-engine/octarender.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/octarender.o: engine/sound.h engine/world.h engine/octa.h
-engine/octarender.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/octarender.o: engine/model.h engine/varray.h
-engine/physics.o: engine/engine.h shared/cube.h shared/tools.h
-engine/physics.o: shared/command.h shared/geom.h shared/ents.h
-engine/physics.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/physics.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/physics.o: engine/texture.h engine/model.h engine/varray.h
-engine/physics.o: engine/mpr.h
-engine/pvs.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/pvs.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/pvs.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/pvs.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
-engine/pvs.o: engine/varray.h
-engine/rendergl.o: engine/engine.h shared/cube.h shared/tools.h
-engine/rendergl.o: shared/command.h shared/geom.h shared/ents.h
-engine/rendergl.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/rendergl.o: engine/sound.h engine/world.h engine/octa.h
-engine/rendergl.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/rendergl.o: engine/model.h engine/varray.h
-engine/rendermodel.o: engine/engine.h shared/cube.h shared/tools.h
-engine/rendermodel.o: shared/command.h shared/geom.h shared/ents.h
-engine/rendermodel.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/rendermodel.o: engine/sound.h engine/world.h engine/octa.h
-engine/rendermodel.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/rendermodel.o: engine/model.h engine/varray.h engine/ragdoll.h
-engine/rendermodel.o: engine/animmodel.h engine/vertmodel.h
-engine/rendermodel.o: engine/skelmodel.h engine/md2.h engine/md3.h
-engine/rendermodel.o: engine/md5.h engine/obj.h engine/smd.h engine/iqm.h
-engine/renderparticles.o: engine/engine.h shared/cube.h shared/tools.h
-engine/renderparticles.o: shared/command.h shared/geom.h shared/ents.h
-engine/renderparticles.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/renderparticles.o: engine/sound.h engine/world.h engine/octa.h
-engine/renderparticles.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/renderparticles.o: engine/model.h engine/varray.h
-engine/renderparticles.o: engine/rendertarget.h engine/depthfx.h
-engine/renderparticles.o: engine/lensflare.h engine/explosion.h
-engine/renderparticles.o: engine/lightning.h
-engine/rendersky.o: engine/engine.h shared/cube.h shared/tools.h
-engine/rendersky.o: shared/command.h shared/geom.h shared/ents.h
-engine/rendersky.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/rendersky.o: engine/sound.h engine/world.h engine/octa.h
-engine/rendersky.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/rendersky.o: engine/model.h engine/varray.h
-engine/rendertext.o: engine/engine.h shared/cube.h shared/tools.h
-engine/rendertext.o: shared/command.h shared/geom.h shared/ents.h
-engine/rendertext.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/rendertext.o: engine/sound.h engine/world.h engine/octa.h
-engine/rendertext.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/rendertext.o: engine/model.h engine/varray.h
-engine/renderva.o: engine/engine.h shared/cube.h shared/tools.h
-engine/renderva.o: shared/command.h shared/geom.h shared/ents.h
-engine/renderva.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/renderva.o: engine/sound.h engine/world.h engine/octa.h
-engine/renderva.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/renderva.o: engine/model.h engine/varray.h
-engine/server.o: engine/engine.h shared/cube.h shared/tools.h
-engine/server.o: shared/command.h shared/geom.h shared/ents.h
-engine/server.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/server.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/server.o: engine/texture.h engine/model.h engine/varray.h
-engine/serverbrowser.o: engine/engine.h shared/cube.h shared/tools.h
-engine/serverbrowser.o: shared/command.h shared/geom.h shared/ents.h
-engine/serverbrowser.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/serverbrowser.o: engine/sound.h engine/world.h engine/octa.h
-engine/serverbrowser.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/serverbrowser.o: engine/model.h engine/varray.h
-engine/shader.o: engine/engine.h shared/cube.h shared/tools.h
-engine/shader.o: shared/command.h shared/geom.h shared/ents.h
-engine/shader.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/shader.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/shader.o: engine/texture.h engine/model.h engine/varray.h
-engine/shadowmap.o: engine/engine.h shared/cube.h shared/tools.h
-engine/shadowmap.o: shared/command.h shared/geom.h shared/ents.h
-engine/shadowmap.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/shadowmap.o: engine/sound.h engine/world.h engine/octa.h
-engine/shadowmap.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/shadowmap.o: engine/model.h engine/varray.h engine/rendertarget.h
-engine/sound.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/sound.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/sound.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/sound.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/sound.o: engine/model.h engine/varray.h
-engine/texture.o: engine/engine.h shared/cube.h shared/tools.h
-engine/texture.o: shared/command.h shared/geom.h shared/ents.h
-engine/texture.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/texture.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/texture.o: engine/texture.h engine/model.h engine/varray.h
-engine/texture.o: engine/scale.h
-engine/ui.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/ui.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/ui.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/ui.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
-engine/ui.o: engine/varray.h engine/textedit.h
-engine/water.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/water.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/water.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/water.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/water.o: engine/model.h engine/varray.h
-engine/world.o: engine/engine.h shared/cube.h shared/tools.h shared/command.h
-engine/world.o: shared/geom.h shared/ents.h shared/iengine.h shared/igame.h
-engine/world.o: engine/irc.h engine/sound.h engine/world.h engine/octa.h
-engine/world.o: engine/lightmap.h engine/bih.h engine/texture.h
-engine/world.o: engine/model.h engine/varray.h
-engine/worldio.o: engine/engine.h shared/cube.h shared/tools.h
-engine/worldio.o: shared/command.h shared/geom.h shared/ents.h
-engine/worldio.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-engine/worldio.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-engine/worldio.o: engine/texture.h engine/model.h engine/varray.h
-game/ai.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/ai.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/ai.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/ai.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/ai.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/ai.o: game/player.h game/ai.h game/vars.h game/capture.h game/defend.h
-game/ai.o: game/bomber.h
-game/client.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/client.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/client.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/client.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/client.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/client.o: game/player.h game/ai.h game/vars.h game/capture.h game/defend.h
-game/client.o: game/bomber.h
-game/capture.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/capture.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/capture.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/capture.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/capture.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/capture.o: game/player.h game/ai.h game/vars.h game/capture.h
-game/capture.o: game/defend.h game/bomber.h
-game/defend.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/defend.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/defend.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/defend.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/defend.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/defend.o: game/player.h game/ai.h game/vars.h game/capture.h game/defend.h
-game/defend.o: game/bomber.h
-game/bomber.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/bomber.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/bomber.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/bomber.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/bomber.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/bomber.o: game/player.h game/ai.h game/vars.h game/capture.h game/defend.h
-game/bomber.o: game/bomber.h
-game/entities.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/entities.o: shared/command.h shared/geom.h shared/ents.h
-game/entities.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-game/entities.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-game/entities.o: engine/texture.h engine/model.h engine/varray.h
-game/entities.o: game/weapons.h game/weapdef.h game/gamemode.h game/player.h game/ai.h
-game/entities.o: game/vars.h game/capture.h game/defend.h game/bomber.h
-game/game.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/game.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/game.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/game.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/game.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/game.o: game/player.h game/ai.h game/vars.h game/capture.h game/defend.h
-game/game.o: game/bomber.h
-game/hud.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/hud.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/hud.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/hud.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/hud.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/hud.o: game/player.h game/ai.h game/vars.h game/capture.h game/defend.h
-game/hud.o: game/bomber.h game/compass.h
-game/physics.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/physics.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/physics.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/physics.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/physics.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/physics.o: game/player.h game/ai.h game/vars.h game/capture.h
-game/physics.o: game/defend.h game/bomber.h
-game/projs.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/projs.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/projs.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/projs.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/projs.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/projs.o: game/player.h game/ai.h game/vars.h game/capture.h game/defend.h
-game/projs.o: game/bomber.h
-game/scoreboard.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/scoreboard.o: shared/command.h shared/geom.h shared/ents.h
-game/scoreboard.o: shared/iengine.h shared/igame.h engine/irc.h
-game/scoreboard.o: engine/sound.h engine/world.h engine/octa.h
-game/scoreboard.o: engine/lightmap.h engine/bih.h engine/texture.h
-game/scoreboard.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h
-game/scoreboard.o: game/gamemode.h game/player.h game/ai.h game/vars.h
-game/scoreboard.o: game/capture.h game/defend.h game/bomber.h
-game/server.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/server.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/server.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/server.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/server.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/server.o: game/player.h game/ai.h game/vars.h game/capture.h game/defend.h
-game/server.o: game/bomber.h game/auth.h game/capturemode.h game/defendmode.h
-game/server.o: game/bombermode.h game/duelmut.h game/aiman.h
-game/waypoint.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/waypoint.o: shared/command.h shared/geom.h shared/ents.h
-game/waypoint.o: shared/iengine.h shared/igame.h engine/irc.h engine/sound.h
-game/waypoint.o: engine/world.h engine/octa.h engine/lightmap.h engine/bih.h
-game/waypoint.o: engine/texture.h engine/model.h engine/varray.h
-game/waypoint.o: game/weapons.h game/weapdef.h game/gamemode.h game/player.h game/ai.h
-game/waypoint.o: game/vars.h game/capture.h game/defend.h game/bomber.h
-game/weapons.o: game/game.h engine/engine.h shared/cube.h shared/tools.h
-game/weapons.o: shared/command.h shared/geom.h shared/ents.h shared/iengine.h
-game/weapons.o: shared/igame.h engine/irc.h engine/sound.h engine/world.h
-game/weapons.o: engine/octa.h engine/lightmap.h engine/bih.h engine/texture.h
-game/weapons.o: engine/model.h engine/varray.h game/weapons.h game/weapdef.h game/gamemode.h
-game/weapons.o: game/player.h game/ai.h game/vars.h game/capture.h
-game/weapons.o: game/defend.h game/bomber.h
-
-shared/crypto-standalone.o: shared/cube.h shared/tools.h shared/command.h
-shared/crypto-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
-shared/crypto-standalone.o: shared/igame.h
-shared/geom-standalone.o: shared/cube.h shared/tools.h shared/command.h
-shared/geom-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
-shared/geom-standalone.o: shared/igame.h
-shared/stream-standalone.o: shared/cube.h shared/tools.h shared/command.h
-shared/stream-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
-shared/stream-standalone.o: shared/igame.h
-shared/tools-standalone.o: shared/cube.h shared/tools.h shared/command.h
-shared/tools-standalone.o: shared/geom.h shared/ents.h shared/iengine.h
-shared/tools-standalone.o: shared/igame.h
-engine/command-standalone.o: engine/engine.h shared/cube.h shared/tools.h
-engine/command-standalone.o: shared/command.h shared/geom.h shared/ents.h
-engine/command-standalone.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/command-standalone.o: engine/sound.h
-engine/irc-standalone.o: engine/engine.h shared/cube.h shared/tools.h
-engine/irc-standalone.o: shared/command.h shared/geom.h shared/ents.h
-engine/irc-standalone.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/irc-standalone.o: engine/sound.h
-engine/master-standalone.o: engine/engine.h shared/cube.h shared/tools.h
-engine/master-standalone.o: shared/command.h shared/geom.h shared/ents.h
-engine/master-standalone.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/master-standalone.o: engine/sound.h
-engine/server-standalone.o: engine/engine.h shared/cube.h shared/tools.h
-engine/server-standalone.o: shared/command.h shared/geom.h shared/ents.h
-engine/server-standalone.o: shared/iengine.h shared/igame.h engine/irc.h
-engine/server-standalone.o: engine/sound.h
-game/server-standalone.o: game/game.h engine/engine.h shared/cube.h
-game/server-standalone.o: shared/tools.h shared/command.h shared/geom.h
-game/server-standalone.o: shared/ents.h shared/iengine.h shared/igame.h
-game/server-standalone.o: engine/irc.h engine/sound.h game/weapons.h game/weapdef.h
-game/server-standalone.o: game/gamemode.h game/player.h game/ai.h game/vars.h
-game/server-standalone.o: game/capture.h game/defend.h game/bomber.h
-game/server-standalone.o: game/auth.h game/capturemode.h game/defendmode.h
-game/server-standalone.o: game/bombermode.h game/duelmut.h game/aiman.h
-
-shared/cube.h.gch: shared/tools.h shared/command.h shared/geom.h
-shared/cube.h.gch: shared/ents.h shared/iengine.h shared/igame.h
-engine/engine.h.gch: shared/cube.h shared/tools.h shared/command.h
-engine/engine.h.gch: shared/geom.h shared/ents.h shared/iengine.h
-engine/engine.h.gch: shared/igame.h engine/irc.h engine/sound.h
-engine/engine.h.gch: engine/world.h engine/octa.h engine/lightmap.h
-engine/engine.h.gch: engine/bih.h engine/texture.h engine/model.h
-engine/engine.h.gch: engine/varray.h
diff --git a/src/dist.mk b/src/dist.mk
index 90da3e2..f6643a5 100644
--- a/src/dist.mk
+++ b/src/dist.mk
@@ -1,156 +1,61 @@
 appname=$(APPNAME)
-appnamefull:=$(shell sed -n 's/versionname *"\([^"]*\)"/\1/p' ../game/$(APPSHORTNAME)/version.cfg)
-appversion:=$(shell sed -n 's/versionstring *"\([^"]*\)"/\1/p' ../game/$(APPSHORTNAME)/version.cfg)
+appnamefull=$(shell sed -n 's/.define VERSION_NAME *"\([^"]*\)"/\1/p' version.h)
+appversion=$(shell sed -n 's/.define VERSION_STRING *"\([^"]*\)"/\1/p' version.h)
 
 dirname=$(appname)-$(appversion)
-dirname-osx=$(appname).app
 dirname-win=$(dirname)-win
 
-tmpdir-osx:=$(shell cd ../ && DIR=$$(mktemp -d $(dirname)-osx_XXXXX); rmdir $$DIR; echo $$DIR)
 exename=$(appname)_$(appversion)_win.exe
 
-tarname=$(appname)_$(appversion)_nix_bsd.tar
-tarname-all=$(appname)_$(appversion)_all.tar
-tarname-osx=$(appname)_$(appversion)_osx.tar
+tarname=$(appname)_$(appversion)_nix.tar
+tarname-combined=$(appname)_$(appversion)_combined.tar
 
 torrent-trackers-url="udp://tracker.openbittorrent.com:80,udp://tracker.publicbt.com:80,udp://tracker.ccc.de:80,udp://tracker.istole.it:80"
-torrent-webseed-baseurl="http://downloads.sourceforge.net/redeclipse"
-
-FILES+= \
-	$(APPCLIENT).bat \
-	$(APPCLIENT).sh \
-	$(APPSERVER).bat \
-	$(APPSERVER).sh
-
-SRC_DIRS= \
-	src/enet \
-	src/engine \
-	src/game \
-	src/include \
-	src/lib \
-	src/scripts \
-	src/shared
-
-SRC_FILES:= \
-	src/core.mk \
-	src/dist.mk \
-	src/dpiaware.manifest \
-	src/system-install.mk \
-	src/$(APPNAME).* \
-	src/install/nix/$(APPNAME)* \
-	src/install/win/*
-
-SRC_XCODE:= \
-	src/xcode/*.h \
-	src/xcode/*.m \
-	src/xcode/*.mm \
-	src/xcode/*.lproj \
-	src/xcode/$(APPNAME)* \
+torrent-webseed-baseurl="http://redeclipse.net/files/releases"
 
 OSX_APP=
 ifeq ($(APPNAME),redeclipse)
 OSX_APP=bin/$(APPNAME).app
 endif
 
-BIN_FILES:= \
-	bin/amd64/*.txt \
-	bin/amd64/*.dll \
-	bin/amd64/$(APPCLIENT)* \
-	bin/amd64/$(APPSERVER)* \
-	bin/x86/*.txt \
-	bin/x86/*.dll \
-	bin/x86/$(APPCLIENT)* \
-	bin/x86/$(APPSERVER)* \
-	$(OSX_APP)
-
-DOC_FILES:= \
-	doc/all-licenses.txt \
-	doc/cc-by-sa.txt \
-	doc/changelog.txt \
-	doc/cube2font.txt \
-	doc/examples \
-	doc/guidelines.txt \
-	doc/irc.txt \
-	doc/license.txt \
-	doc/man/cube2font* \
-	doc/man/$(APPNAME)* \
-	doc/trademark.txt
-
-DISTFILES:= \
-	$(FILES) \
-	$(BIN_FILES) \
-	data \
-	game/$(APPSHORTNAME) \
-	$(DOC_FILES) \
-	$(shell cd ../ && find $(SRC_DIRS) -not -iname *.lo -not -iname *.gch -not -iname *.o) \
-	$(SRC_FILES) \
-	$(SRC_XCODE)
+DISTFILES=$(shell cd ../ && find . -not -iname *.lo -not -iname *.gch -not -iname *.o || echo "")
 
 ../$(dirname):
 	rm -rf $@
-	# Transform relative to src/ dir
-	tar -cf - $(DISTFILES:%=../%) | (mkdir $@/; cd $@/ ; tar -xpf -)
-	# create dedicated Makefile
-	echo "APPNAME=$(APPNAME)" >$@/src/Makefile
-ifneq ($(APPNAME),$(appname))
-	echo "appname=$(appname)" >>$@/src/Makefile
-endif
-	echo >>$@/src/Makefile
-	echo "all:" >>$@/src/Makefile
-	echo >>$@/src/Makefile
-	echo "include $(APPNAME).mk" >>$@/src/Makefile
-	echo >>$@/src/Makefile
-	echo "include core.mk" >>$@/src/Makefile
+	# exclude VCS and transform relative to src/ dir
+	tar --exclude=.git --exclude=$(dirname) \
+		-cf - $(DISTFILES:%=../%) | (mkdir $@/; cd $@/ ; tar -xpf -)
 	$(MAKE) -C $@/src clean
-	-$(MAKE) -C $@/src/enet distclean
-	rm -rf $@/src/enet/autom4te.cache/
+	$(MAKE) -C $@/src/enet clean
+	echo "stable" > $@/branch.txt
+	curl --location --insecure --fail http://redeclipse.net/files/stable/base.txt --output $@/version.txt
+	curl --location --insecure --fail http://redeclipse.net/files/stable/bins.txt --output $@/bin/version.txt
+	curl --location --insecure --fail http://redeclipse.net/files/stable/data.txt --output $@/data/version.txt
+	curl --location --insecure --fail http://redeclipse.net/files/stable/linux.tar.gz --output linux.tar.gz
+	tar --gzip --extract --verbose --overwrite --file=linux.tar.gz --directory=$@
+	rm -f linux.tar.gz
+	curl --location --insecure --fail http://redeclipse.net/files/stable/windows.zip --output windows.zip
+	unzip -o windows.zip -d $@
+	rm -f windows.zip
 
 distdir: ../$(dirname)
 
 ../$(tarname): ../$(dirname)
 	tar \
-		--exclude='$</bin/*.app*' \
 		--exclude='$</bin/*/*.exe' \
-		--exclude='$</bin/*/*.dll' \
-		--exclude='$</bin/*/*.txt' \
-		--exclude='$</*.bat' \
 		-cf $@ $<
 
 dist-tar: ../$(tarname)
 
-../$(tarname-all): ../$(dirname)
+../$(tarname-combined): ../$(dirname)
 	tar -cf $@ $<
 
-dist-tar-all: ../$(tarname-all)
-
-../$(tarname-osx): ../$(dirname)
-	tar -cf $@ -C $</bin $(dirname-osx)
-	if [ -z $(tmpdir-osx) ]; then exit 1; fi
-	rm -rf ../$(tmpdir-osx)/
-	mkdir ../$(tmpdir-osx)
-	mkdir ../$(tmpdir-osx)/$(dirname-osx)
-	mkdir ../$(tmpdir-osx)/$(dirname-osx)/Contents
-	mkdir ../$(tmpdir-osx)/$(dirname-osx)/Contents/Resources
-	# Use links with tar dereference to change directory paths
-	ln -s ../../../$</data/ ../$(tmpdir-osx)/$(dirname-osx)/Contents/Resources/data
-	ln -s ../../../$</doc/ ../$(tmpdir-osx)/$(dirname-osx)/Contents/Resources/doc
-	ln -s ../../../$</game/ ../$(tmpdir-osx)/$(dirname-osx)/Contents/Resources/game
-	ln -s ../../../$</src/ ../$(tmpdir-osx)/$(dirname-osx)/Contents/Resources/src
-ifeq ($(APPNAME),redeclipse)
-	ln -s ../../../$</readme.txt ../$(tmpdir-osx)/$(dirname-osx)/Contents/Resources/readme.txt
-endif
-	tar \
-		-hrf $@ -C ../$(tmpdir-osx) $(dirname-osx)
-	rm -rf ../$(tmpdir-osx)/
-
-dist-tar-osx: ../$(tarname-osx)
+dist-tar-combined: ../$(tarname-combined)
 
 ../$(dirname-win): ../$(dirname)
 	cp -r $< $@
-	rm -rf $@/bin/*.app/
 	rm -rf $@/bin/*/*linux*
-	rm -rf $@/bin/*/*freebsd*
-	rm -f $@/*.sh
+	rm -rf $@/bin/*/*bsd*
 
 distdir-win: ../$(dirname-win)
 
@@ -171,39 +76,22 @@ dist-nix: ../$(tarname).bz2
 
 dist-xz: ../$(tarname).xz
 
-../$(tarname-all).gz: ../$(tarname-all)
+../$(tarname-combined).gz: ../$(tarname-combined)
 	gzip -c < $< > $@
 
-dist-gz-all: ../$(tarname-all).gz
+dist-gz-combined: ../$(tarname-combined).gz
 
-../$(tarname-all).bz2: ../$(tarname-all)
+../$(tarname-combined).bz2: ../$(tarname-combined)
 	bzip2 -c < $< > $@
 
-dist-bz2-all: ../$(tarname-all).bz2
+dist-bz2-combined: ../$(tarname-combined).bz2
 
-dist-all: ../$(tarname-all).bz2
+dist-combined: ../$(tarname-combined).bz2
 
-../$(tarname-all).xz: ../$(tarname-all)
+../$(tarname-combined).xz: ../$(tarname-combined)
 	xz -c < $< > $@
 
-dist-xz-all: ../$(tarname-all).xz
-
-../$(tarname-osx).gz: ../$(tarname-osx)
-	gzip -c < $< > $@
-
-dist-gz-osx: ../$(tarname-osx).gz
-
-../$(tarname-osx).bz2: ../$(tarname-osx)
-	bzip2 -c < $< > $@
-
-dist-bz2-osx: ../$(tarname-osx).bz2
-
-dist-osx: ../$(tarname-osx).bz2
-
-../$(tarname-osx).xz: ../$(tarname-osx)
-	xz -c < $< > $@
-
-dist-xz-osx: ../$(tarname-osx).xz
+dist-xz-combined: ../$(tarname-combined).xz
 
 ../$(exename): ../$(dirname-win)
 	makensis $</src/install/win/$(APPNAME).nsi
@@ -211,13 +99,7 @@ dist-xz-osx: ../$(tarname-osx).xz
 
 dist-win: ../$(exename)
 
-ifeq ($(APPNAME),redeclipse)
-dist: dist-bz2 dist-bz2-all dist-bz2-osx dist-win
-else
-ifeq ($(APPNAME),mekarcade)
-dist: dist-bz2 dist-bz2-all
-endif
-endif
+dist: dist-clean dist-bz2 dist-bz2-combined dist-win
 
 ../$(tarname).bz2.torrent: ../$(tarname).bz2
 	rm -f $@
@@ -231,101 +113,40 @@ endif
 
 dist-torrent: ../$(tarname).bz2.torrent
 
-../$(tarname-all).bz2.torrent: ../$(tarname-all).bz2
+../$(tarname-combined).bz2.torrent: ../$(tarname-combined).bz2
 	rm -f $@
 	cd ../ &&\
 		mktorrent \
 		-a $(torrent-trackers-url) \
-		-w $(torrent-webseed-baseurl)/$(tarname-all).bz2 \
-		-n $(tarname-all).bz2 \
-		-c "$(appnamefull) $(appversion) for All Platforms" \
-		$(tarname-all).bz2
+		-w $(torrent-webseed-baseurl)/$(tarname-combined).bz2 \
+		-n $(tarname-combined).bz2 \
+		-c "$(appnamefull) $(appversion) Combined Platforms" \
+		$(tarname-combined).bz2
 
-dist-torrent-all: ../$(tarname-all).bz2.torrent
-
-../$(tarname-osx).bz2.torrent: ../$(tarname-osx).bz2
-	rm -f $@
-	cd ../ &&\
-		mktorrent \
-		-a $(torrent-trackers-url) \
-		-w $(torrent-webseed-baseurl)/$(tarname-osx).bz2 \
-		-n $(tarname-osx).bz2 \
-		-c "$(appnamefull) $(appversion) for OSX" \
-		$(tarname-osx).bz2
-
-dist-torrent-osx: ../$(tarname-osx).bz2.torrent
-
-../$(exename).torrent: ../$(exename)
-	rm -f $@
-	cd ../ &&\
-		mktorrent \
-		-a $(torrent-trackers-url) \
-		-w $(torrent-webseed-baseurl)/$(exename) \
-		-n $(exename) \
-		-c "$(appnamefull) $(appversion) for Windows" \
-		$(exename)
+dist-torrent-combined: ../$(tarname-combined).bz2.torrent
 
 dist-torrent-win: ../$(exename).torrent
 
-ifeq ($(APPNAME),redeclipse)
-dist-torrents: dist-torrent dist-torrent-all dist-torrent-osx dist-torrent-win
-else
-ifeq ($(APPNAME),mekarcade)
-dist-torrents: dist-torrent dist-torrent-all
-endif
-endif
+dist-torrents: dist-torrent dist-torrent-combined dist-torrent-win
 
 dist-mostlyclean:
 	rm -rf ../$(dirname)
 	rm -rf ../$(dirname-win)
 	rm -f ../$(tarname)
-	rm -f ../$(tarname-all)
-	rm -f ../$(tarname-osx)
+	rm -f ../$(tarname-combined)
 
 dist-clean: dist-mostlyclean
 	rm -f ../$(tarname)*
-	rm -f ../$(tarname-all)*
-	rm -f ../$(tarname-osx)*
+	rm -f ../$(tarname-combined)*
 	rm -f ../$(exename)*
 
 ../doc/cube2font.txt: ../doc/man/cube2font.1
-	scripts/generate-cube2font-txt $< $@
+	scripts/cube2font-txt $< $@
 
 cube2font-txt: ../doc/cube2font.txt
 
-../doc/examples/servinit.cfg: ../data/config/usage.cfg install-server
-	scripts/update-servinit-defaults $@
-	scripts/update-servinit-comments $< $@
+../doc/examples/servinit.cfg: ../config/usage.cfg install-server
+	scripts/servinit-defaults $@
+	scripts/servinit-comments $< $@
 
 update-servinit: ../doc/examples/servinit.cfg
-
-../doc/wiki-contributors.txt: ../readme.txt
-	scripts/generate-wiki-contributors $< $@
-
-wiki-contributors: ../doc/wiki-contributors.txt
-
-../doc/wiki-guidelines.txt: ../doc/guidelines.txt
-	scripts/generate-wiki-guidelines $< $@
-
-wiki-guidelines: ../doc/wiki-guidelines.txt
-
-../doc/wiki-%.txt: ../data/config/usage.cfg scripts/wiki-common
-	scripts/generate-wiki-$* $< $@
-
-../doc/wiki-all-vars-commands.txt: ../doc/wiki-game-vars.txt ../doc/wiki-engine-vars.txt ../doc/wiki-weapon-vars.txt ../doc/wiki-commands.txt ../doc/wiki-aliases.txt
-	scripts/generate-wiki-all-vars-commands $^ $@
-
-wiki-game-vars: ../doc/wiki-game-vars.txt
-
-wiki-engine-vars: ../doc/wiki-engine-vars.txt
-
-wiki-weapon-vars: ../doc/wiki-weapon-vars.txt
-
-wiki-commands: ../doc/wiki-commands.txt
-
-wiki-aliases: ../doc/wiki-aliases.txt
-
-wiki-all-vars-commands: ../doc/wiki-all-vars-commands.txt
-
-wiki-check-all-result: ../doc/wiki-all-vars-commands.txt
-	scripts/check-wiki-all-result ../data/config/usage.cfg ../doc/wiki-all-vars-commands.txt
diff --git a/src/dpiaware.manifest b/src/dpiaware.manifest
index 7e87cdd..fce25a2 100644
--- a/src/dpiaware.manifest
+++ b/src/dpiaware.manifest
@@ -1,16 +1,16 @@
-<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" 
-          xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" 
-          xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
-  <asmv3:application>
-    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
-      <dpiAware>true</dpiAware>
-    </asmv3:windowsSettings>
-  </asmv3:application>
-  <asmv2:trustInfo>
-    <asmv2:security>
-      <asmv3:requestedPrivileges>
-        <asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false"/>
-      </asmv3:requestedPrivileges>
-    </asmv2:security>
-  </asmv2:trustInfo>
-</assembly>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" 
+          xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" 
+          xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
+  <asmv3:application>
+    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
+      <dpiAware>true</dpiAware>
+    </asmv3:windowsSettings>
+  </asmv3:application>
+  <asmv2:trustInfo>
+    <asmv2:security>
+      <asmv3:requestedPrivileges>
+        <asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false"/>
+      </asmv3:requestedPrivileges>
+    </asmv2:security>
+  </asmv2:trustInfo>
+</assembly>
diff --git a/src/engine/animmodel.h b/src/engine/animmodel.h
index c9362ba..844d2d4 100644
--- a/src/engine/animmodel.h
+++ b/src/engine/animmodel.h
@@ -376,8 +376,8 @@ struct animmodel : model
             DELETEA(name);
         }
 
-        virtual void calcbb(int frame, vec &bbmin, vec &bbmax, const matrix3x4 &m) {}
-        virtual void gentris(int frame, Texture *tex, vector<BIH::tri> *out, const matrix3x4 &m) {}
+        virtual void calcbb(vec &bbmin, vec &bbmax, const matrix3x4 &m) {}
+        virtual void gentris(Texture *tex, vector<BIH::tri> *out, const matrix3x4 &m) {}
 
         virtual void setshader(Shader *s)
         {
@@ -520,16 +520,16 @@ struct animmodel : model
         }
 
         virtual int findtag(const char *name) { return -1; }
-        virtual void concattagtransform(part *p, int frame, int i, const matrix3x4 &m, matrix3x4 &n) {}
+        virtual void concattagtransform(part *p, int i, const matrix3x4 &m, matrix3x4 &n) {}
 
-        void calcbb(int frame, vec &bbmin, vec &bbmax, const matrix3x4 &m)
+        void calcbb(vec &bbmin, vec &bbmax, const matrix3x4 &m)
         {
-            loopv(meshes) meshes[i]->calcbb(frame, bbmin, bbmax, m);
+            loopv(meshes) meshes[i]->calcbb(bbmin, bbmax, m);
         }
 
-        void gentris(int frame, vector<skin> &skins, vector<BIH::tri> *tris, const matrix3x4 &m)
+        void gentris(vector<skin> &skins, vector<BIH::tri> *tris, const matrix3x4 &m)
         {
-            loopv(meshes) meshes[i]->gentris(frame, skins[i].tex && skins[i].tex->type&Texture::ALPHA ? skins[i].tex : NULL, tris, m);
+            loopv(meshes) meshes[i]->gentris(skins[i].tex && skins[i].tex->type&Texture::ALPHA ? skins[i].tex : NULL, tris, m);
         }
 
         virtual void *animkey() { return this; }
@@ -597,33 +597,33 @@ struct animmodel : model
             if(meshes) meshes->cleanup();
         }
 
-        void calcbb(int frame, vec &bbmin, vec &bbmax, const matrix3x4 &m)
+        void calcbb(vec &bbmin, vec &bbmax, const matrix3x4 &m)
         {
             matrix3x4 t = m;
             t.translate(translate);
             t.scale(model->scale);
-            meshes->calcbb(frame, bbmin, bbmax, t);
+            meshes->calcbb(bbmin, bbmax, t);
             loopv(links)
             {
                 matrix3x4 n;
-                meshes->concattagtransform(this, frame, links[i].tag, m, n);
+                meshes->concattagtransform(this, links[i].tag, m, n);
                 n.transformedtranslate(links[i].translate, model->scale);
-                links[i].p->calcbb(frame, bbmin, bbmax, n);
+                links[i].p->calcbb(bbmin, bbmax, n);
             }
         }
 
-        void gentris(int frame, vector<BIH::tri> *tris, const matrix3x4 &m)
+        void gentris(vector<BIH::tri> *tris, const matrix3x4 &m)
         {
             matrix3x4 t = m;
             t.translate(translate);
             t.scale(model->scale);
-            meshes->gentris(frame, skins, tris, t);
+            meshes->gentris(skins, tris, t);
             loopv(links)
             {
                 matrix3x4 n;
-                meshes->concattagtransform(this, frame, links[i].tag, m, n);
+                meshes->concattagtransform(this, links[i].tag, m, n);
                 n.transformedtranslate(links[i].translate, model->scale);
-                links[i].p->gentris(frame, tris, n);
+                links[i].p->gentris(tris, n);
             }
         }
 
@@ -707,19 +707,27 @@ struct animmodel : model
                 animspec *spec = NULL;
                 if(anims[animpart])
                 {
-                    vector<animspec> &primary = anims[animpart][anim&ANIM_INDEX];
-                    if(&primary < &anims[animpart][game::numanims()] && primary.length()) spec = &primary[uint(varseed + basetime)%primary.length()];
+                    int primaryidx = anim&ANIM_INDEX;
+                    if(primaryidx < game::numanims())
+                    {
+                        vector<animspec> &primary = anims[animpart][primaryidx];
+                        if(primary.length()) spec = &primary[uint(varseed + basetime)%primary.length()];
+                    }
                     if((anim>>ANIM_SECONDARY)&(ANIM_INDEX|ANIM_DIR))
                     {
-                        vector<animspec> &secondary = anims[animpart][(anim>>ANIM_SECONDARY)&ANIM_INDEX];
-                        if(&secondary < &anims[animpart][game::numanims()] && secondary.length())
+                        int secondaryidx = (anim>>ANIM_SECONDARY)&ANIM_INDEX;
+                        if(secondaryidx < game::numanims())
                         {
-                            animspec &spec2 = secondary[uint(varseed + basetime2)%secondary.length()];
-                            if(!spec || spec2.priority > spec->priority)
+                            vector<animspec> &secondary = anims[animpart][secondaryidx];
+                            if(secondary.length())
                             {
-                                spec = &spec2;
-                                info.anim >>= ANIM_SECONDARY;
-                                info.basetime = basetime2;
+                                animspec &spec2 = secondary[uint(varseed + basetime2)%secondary.length()];
+                                if(!spec || spec2.priority > spec->priority)
+                                {
+                                    spec = &spec2;
+                                    info.anim >>= ANIM_SECONDARY;
+                                    info.basetime = basetime2;
+                                }
                             }
                         }
                     }
@@ -1132,12 +1140,12 @@ struct animmodel : model
         if(offsetpitch) m.rotate_around_y(offsetpitch*RAD);
     }
 
-    void gentris(int frame, vector<BIH::tri> *tris)
+    void gentris(vector<BIH::tri> *tris)
     {
         if(parts.empty()) return;
         matrix3x4 m;
         initmatrix(m);
-        parts[0]->gentris(frame, tris, m);
+        parts[0]->gentris(tris, m);
     }
 
     void preloadBIH()
@@ -1150,7 +1158,7 @@ struct animmodel : model
     {
         if(bih) return bih;
         vector<BIH::tri> tris[2];
-        gentris(0, tris);
+        gentris(tris);
         bih = new BIH(tris);
         return bih;
     }
@@ -1279,13 +1287,13 @@ struct animmodel : model
         }
     }
 
-    void calcbb(int frame, vec &center, vec &radius)
+    void calcbb(vec &center, vec &radius)
     {
         if(parts.empty()) return;
         vec bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f);
         matrix3x4 m;
         initmatrix(m);
-        parts[0]->calcbb(frame, bbmin, bbmax, m);
+        parts[0]->calcbb(bbmin, bbmax, m);
         radius = bbmax;
         radius.sub(bbmin);
         radius.mul(0.5f);
diff --git a/src/engine/bih.cpp b/src/engine/bih.cpp
index ea75598..f2958d3 100644
--- a/src/engine/bih.cpp
+++ b/src/engine/bih.cpp
@@ -283,14 +283,8 @@ BIH::BIH(vector<tri> *t)
 
 bool mmintersect(const extentity &e, const vec &o, const vec &ray, float maxdist, int mode, float &dist)
 {
-    extern vector<mapmodelinfo> mapmodels;
-    if(!mapmodels.inrange(e.attrs[0])) return false;
-    model *m = mapmodels[e.attrs[0]].m;
-    if(!m)
-    {
-        m = loadmodel(NULL, e.attrs[0]);
-        if(!m) return false;
-    }
+    model *m = loadmapmodel(e.attrs[0]);
+    if(!m) return false;
     if(mode&RAY_SHADOW)
     {
         if(!m->shadow || e.attrs[6]&MMT_NOSHADOW) return false;
diff --git a/src/engine/blend.cpp b/src/engine/blend.cpp
index bbbe795..b8aa32b 100644
--- a/src/engine/blend.cpp
+++ b/src/engine/blend.cpp
@@ -645,7 +645,7 @@ void paintblendmap(bool msg)
         y = (int)floor(clamp(worldpos.y, 0.0f, float(hdr.worldsize))/(1<<BM_SCALE) - 0.5f*brush->h);
     blitblendmap(brush->data, x, y, brush->w, brush->h);
     previewblends(ivec((x-1)<<BM_SCALE, (y-1)<<BM_SCALE, 0),
-                  ivec((brush->w+2)<<BM_SCALE, (brush->h+2)<<BM_SCALE, hdr.worldsize));
+                  ivec((x+brush->w+1)<<BM_SCALE, (y+brush->h+1)<<BM_SCALE, hdr.worldsize));
 }
 
 VAR(0, paintblendmapdelay, 1, 500, 3000);
@@ -689,7 +689,7 @@ void clearblendmapsel()
         y2 = (sel.o.y+sel.s.y*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE;
     fillblendmap(x1, y1, x2-x1, y2-y1, 0xFF);
     previewblends(ivec(x1<<BM_SCALE, y1<<BM_SCALE, 0),
-                  ivec((x2-x1)<<BM_SCALE, (y2-y1)<<BM_SCALE, hdr.worldsize));
+                  ivec(x2<<BM_SCALE, y2<<BM_SCALE, hdr.worldsize));
 }
 
 COMMAND(0, clearblendmapsel, "");
@@ -702,7 +702,7 @@ void invertblendmapsel()
         y2 = (sel.o.y+sel.s.y*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE;
     invertblendmap(x1, y1, x2-x1, y2-y1);
     previewblends(ivec(x1<<BM_SCALE, y1<<BM_SCALE, 0),
-                  ivec((x2-x1)<<BM_SCALE, (y2-y1)<<BM_SCALE, hdr.worldsize));
+                  ivec(x2<<BM_SCALE, y2<<BM_SCALE, hdr.worldsize));
 }
 
 COMMAND(0, invertblendmapsel, "");
diff --git a/src/engine/blob.cpp b/src/engine/blob.cpp
index e913daf..530fe2a 100644
--- a/src/engine/blob.cpp
+++ b/src/engine/blob.cpp
@@ -42,7 +42,7 @@ struct blobrenderer
     blobinfo *lastblob, *flushblob;
 
     vec blobmin, blobmax;
-    ivec bborigin, bbsize;
+    ivec bbmin, bbmax;
     float blobalphalow, blobalphahigh;
     uchar blobalpha;
 
@@ -406,7 +406,7 @@ struct blobrenderer
 
     void gentris(cube *cu, const ivec &o, int size, int escaped = 0)
     {
-        int overlap = octantrectangleoverlap(o, size, bborigin, bbsize);
+        int overlap = octaboxoverlap(o, size, bbmin, bbmax);
         loopi(8)
         {
             if(overlap&(1<<i))
@@ -449,8 +449,8 @@ struct blobrenderer
         blobmax.x += radius;
         blobmax.y += radius;
         blobmax.z += blobfadehigh;
-        (bborigin = blobmin).sub(2);
-        (bbsize = blobmax).sub(blobmin).add(4);
+        (bbmin = blobmin).sub(2);
+        (bbmax = blobmax).add(2);
         float scale =  fade*blobintensity*255/100.0f;
         blobalphalow = scale / blobfadelow;
         blobalphahigh = scale / blobfadehigh;
diff --git a/src/engine/client.cpp b/src/engine/client.cpp
index 7454763..434d3b2 100644
--- a/src/engine/client.cpp
+++ b/src/engine/client.cpp
@@ -52,6 +52,13 @@ ICOMMAND(0, connectedip, "", (),
     result(address && enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0 ? hostname : "");
 });
 
+ICOMMAND(0, connectedhost, "", (),
+{
+    const ENetAddress *address = connectedpeer();
+    string hostname;
+    result(address && enet_address_get_host(address, hostname, sizeof(hostname)) >= 0 ? hostname : (address && enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0 ? hostname : ""));
+});
+
 ICOMMAND(0, connectedport, "", (),
 {
     const ENetAddress *address = connectedpeer();
@@ -89,6 +96,8 @@ void trydisconnect()
 
 SVAR(0, connectname, "");
 VAR(0, connectport, 0, 0, VAR_MAX);
+VAR(IDF_PERSIST, connectguidelines, 0, 0, 1);
+SVAR(0, guidelinesaction, "");
 
 void connectserv(const char *name, int port, const char *password)
 {
@@ -102,6 +111,13 @@ void connectserv(const char *name, int port, const char *password)
     setvar("connectport", 0);
     if(name && *name)
     {
+        if(!connectguidelines)
+        {
+            defformatstring(s)("connect %s %d %s", name, port, password && *password ? password : "");
+            setsvar("guidelinesaction", s);
+            showgui("guidelines");
+            return;
+        }
         addserver(name, port);
         conoutft(CON_MESG, "\faattempting to connect to %s:[%d]", name, port);
         if(!resolverwait(name, &address))
@@ -120,22 +136,23 @@ void connectserv(const char *name, int port, const char *password)
     }
 
     if(!clienthost)
-        clienthost = enet_host_create(NULL, 2, server::numchannels(), rate*1024, rate*1024);
-
-    if(clienthost)
     {
-        connpeer = enet_host_connect(clienthost, &address, server::numchannels(), 0);
-        enet_host_flush(clienthost);
-        connmillis = totalmillis;
-        connattempts = 0;
-        client::connectattempt(name ? name : "", port, password ? password : "", address);
-        conoutft(CON_MESG, "\fgconnecting to %s:[%d]", name != NULL ? name : "local server", port);
-    }
-    else
-    {
-        conoutft(CON_MESG, "\frfailed creating client socket");
-        connectfail();
+        clienthost = enet_host_create(NULL, 2, server::numchannels(), rate*1024, rate*1024);
+        if(!clienthost)
+        {
+            conoutft(CON_MESG, "\frfailed creating client socket");
+            connectfail();
+            return;
+        }
+        clienthost->duplicatePeers = 0;
     }
+
+    connpeer = enet_host_connect(clienthost, &address, server::numchannels(), 0);
+    enet_host_flush(clienthost);
+    connmillis = totalmillis;
+    connattempts = 0;
+    client::connectattempt(name ? name : "", port, password ? password : "", address);
+    conoutft(CON_MESG, "\fgconnecting to %s:[%d]", name != NULL ? name : "local server", port);
 }
 
 void disconnect(int onlyclean, int async)
@@ -187,7 +204,7 @@ ICOMMAND(0, isconnected, "ii", (int *a, int *b), intret(connected(*a==0, *b==0)
 void reconnect(const char *pass)
 {
     int port = 0;
-    mkstring(addr);
+    string addr = "";
     if(*connectname) copystring(addr, connectname);
     if(connectport) port = connectport;
     disconnect(1);
@@ -271,7 +288,7 @@ void gets2c()           // get updates from the server
             }
             else
             {
-                if(!discmillis || event.data) conoutft(CON_MESG, "\frserver network error, disconnecting (%s) ...", disc_reasons[event.data]);
+                if(!discmillis || event.data) conoutft(CON_MESG, "\frserver disconnected (%s) ...", disc_reasons[event.data]);
                 disconnect();
             }
             return;
diff --git a/src/engine/command.cpp b/src/engine/command.cpp
index 9faffc7..a7b1dc6 100644
--- a/src/engine/command.cpp
+++ b/src/engine/command.cpp
@@ -219,7 +219,7 @@ static bool initidents()
     }
     return true;
 }
-static bool forceinitidents = initidents();
+UNUSED static bool forceinitidents = initidents();
 
 static const char *sourcefile = NULL, *sourcestr = NULL;
 
@@ -234,11 +234,11 @@ static const char *debugline(const char *p, const char *fmt)
         if(!end) end = line + strlen(line);
         if(p >= line && p <= end)
         {
-            static string buf;
+            static bigstring buf;
             char color[] = { '\0', '\0', '\0' };
-            if(fmt[0] == '\f') { strncpy(color, fmt, 2); fmt += strlen(color); }
-            if(sourcefile) formatstring(buf)("%s%s:%d: %s", color, sourcefile, num, fmt);
-            else formatstring(buf)("%s%d: %s", color, num, fmt);
+            if(fmt[0] == '\f') { memcpy(color, fmt, 2); fmt += strlen(color); }
+            if(sourcefile) formatbigstring(buf)("%s%s:%d: %s", color, sourcefile, num, fmt);
+            else formatbigstring(buf)("%s%d: %s", color, num, fmt);
             return buf;
         }
         if(!*end) break;
@@ -280,7 +280,7 @@ static void debugcode(const char *fmt, ...)
 {
     if(nodebug) return;
 
-    defvformatstring(msg, fmt, fmt);
+    defvformatbigstring(msg, fmt, fmt);
     conoutft(CON_MESG, "%s", msg);
 
     debugalias();
@@ -292,7 +292,7 @@ static void debugcodeline(const char *p, const char *fmt, ...)
 {
     if(nodebug) return;
 
-    defvformatstring(msg, fmt, debugline(p, fmt));
+    defvformatbigstring(msg, fmt, debugline(p, fmt));
     conoutft(CON_MESG, "%s", msg);
 
     debugalias();
@@ -401,7 +401,8 @@ void resetvar(char *name)
 {
     ident *id = idents.access(name);
     if(!id) return;
-    if(id->flags&IDF_READONLY) debugcode("\frvariable %s is read-only", id->name);
+    if(id->flags&IDF_READONLY || id->flags&IDF_CLIENT || id->flags&IDF_SERVER) debugcode("\frvariable %s is read-only", id->name);
+    else if(id->flags&IDF_WORLD) debugcode("\frvariable %s is only directly modifiable in editmode", id->name);
     else clearoverride(*id);
 }
 
@@ -514,7 +515,11 @@ void setvar(const char *name, int i, bool dofunc, bool def)
 {
     GETVAR(id, ID_VAR, name, );
     *id->storage.i = clamp(i, id->minval, id->maxval);
-    if(def || versioning) id->def.i = i;
+    if(def || versioning)
+    {
+        id->def.i = i;
+        if(versioning == 2) id->bin.i = i;
+    }
     if(dofunc) id->changed();
 #ifndef STANDALONE
     if(versioning && id->flags&IDF_SERVER) setvar(&id->name[3], i, dofunc, def);
@@ -524,7 +529,11 @@ void setfvar(const char *name, float f, bool dofunc, bool def)
 {
     GETVAR(id, ID_FVAR, name, );
     *id->storage.f = clamp(f, id->minvalf, id->maxvalf);
-    if(def || versioning) id->def.f = f;
+    if(def || versioning)
+    {
+        id->def.f = f;
+        if(versioning == 2) id->bin.f = f;
+    }
     if(dofunc) id->changed();
 #ifndef STANDALONE
     if(versioning && id->flags&IDF_SERVER) setfvar(&id->name[3], f, dofunc, def);
@@ -539,6 +548,11 @@ void setsvar(const char *name, const char *str, bool dofunc, bool def)
     {
         delete[] id->def.s;
         id->def.s = newstring(str);
+        if(versioning == 2)
+        {
+            delete[] id->bin.s;
+            id->bin.s = newstring(str);
+        }
     }
     if(dofunc) id->changed();
 #ifndef STANDALONE
@@ -612,21 +626,65 @@ float getfvarmax(const char *name)
     }
     return 0;
 }
-int getvardef(const char *name)
+int getvardef(const char *name, bool rb)
 {
     ident *id = getident(name);
     if(!id) return 0;
     switch(id->type)
     {
-        case ID_VAR: return id->def.i;
-        case ID_FVAR: return int(id->def.f);
-        case ID_SVAR: return atoi(id->def.s);
+        case ID_VAR: return (rb ? id->bin : id->def).i;
+        case ID_FVAR: return int((rb ? id->bin : id->def).f);
+        case ID_SVAR: return atoi((rb ? id->bin : id->def).s);
         case ID_ALIAS: return id->getint();
         default: break;
     }
     return 0;
 }
 
+float getfvardef(const char *name, bool rb)
+{
+    ident *id = getident(name);
+    if(!id) return 0.f;
+    switch(id->type)
+    {
+        case ID_VAR: return float((rb ? id->bin : id->def).i);
+        case ID_FVAR: return (rb ? id->bin : id->def).f;
+        case ID_SVAR: return atof((rb ? id->bin : id->def).s);
+        case ID_ALIAS: return id->getfloat();
+        default: break;
+    }
+    return 0.f;
+}
+
+const char *getsvardef(const char *name, bool rb)
+{
+    ident *id = getident(name);
+    if(!id) return "";
+    switch(id->type)
+    {
+        case ID_VAR: return intstr((rb ? id->bin : id->def).i);
+        case ID_FVAR: return floatstr((rb ? id->bin : id->def).f);
+        case ID_SVAR: return (rb ? id->bin : id->def).s;
+        case ID_ALIAS: return id->getstr();
+        default: break;
+    }
+    return "";
+}
+
+const char *getvardesc(const char *name)
+{
+    ident *id = getident(name);
+    if(!id || !id->desc) return "";
+    return id->desc;
+}
+
+const char *getvarusage(const char *name)
+{
+    ident *id = getident(name);
+    if(!id || !id->usage) return "";
+    return id->usage;
+}
+
 ICOMMAND(0, getvar, "s", (char *n), intret(getvar(n)));
 ICOMMAND(0, getvartype, "s", (char *n), intret(getvartype(n)));
 ICOMMAND(0, getvarflags, "s", (char *n), intret(getvarflags(n)));
@@ -634,7 +692,11 @@ ICOMMAND(0, getvarmin, "s", (char *n), intret(getvarmin(n)));
 ICOMMAND(0, getvarmax, "s", (char *n), intret(getvarmax(n)));
 ICOMMAND(0, getfvarmin, "s", (char *s), floatret(getfvarmin(s)));
 ICOMMAND(0, getfvarmax, "s", (char *s), floatret(getfvarmax(s)));
-ICOMMAND(0, getvardef, "s", (char *n), intret(getvardef(n)));
+ICOMMAND(0, getvardef, "si", (char *n, int *b), intret(getvardef(n, *b!=0)));
+ICOMMAND(0, getfvardef, "si", (char *n, int *b), floatret(getfvardef(n, *b!=0)));
+ICOMMAND(0, getsvardef, "si", (char *n, int *b), result(getsvardef(n, *b!=0)));
+ICOMMAND(0, getvardesc, "s", (char *n), result(getvardesc(n)));
+ICOMMAND(0, getvarusage, "s", (char *n), result(getvarusage(n)));
 
 bool identexists(const char *name) { return idents.access(name)!=NULL; }
 ident *getident(const char *name) { return idents.access(name); }
@@ -658,12 +720,18 @@ const char *getalias(const char *name)
     return i && i->type==ID_ALIAS && (i->index >= MAXARGS || aliasstack->usedargs&(1<<i->index)) ? i->getstr() : "";
 }
 
+ICOMMAND(0, getalias, "s", (char *s), result(getalias(s)));
+
 #ifndef STANDALONE
-#define WORLDVAR \
-    if(!(identflags&IDF_WORLD) && !editmode && id->flags&IDF_WORLD && !(id->flags&IDF_REWRITE)) \
+#define CHECKVAR(argstr) \
+    if(!versioning) \
     { \
-        debugcode("\frcannot set world variable %s outside editmode", id->name); \
-        return; \
+        if(!(identflags&IDF_WORLD) && !editmode && id->flags&IDF_WORLD && !(id->flags&IDF_REWRITE)) \
+        { \
+            debugcode("\frcannot set world variable %s outside editmode", id->name); \
+            return; \
+        } \
+        if(id->flags&IDF_CLIENT && (!(id->flags&IDF_WORLD) || !(identflags&IDF_WORLD)) && client::sendcmd(2, id->name, argstr)) return; \
     }
 #endif
 
@@ -673,7 +741,7 @@ void setvarchecked(ident *id, int val)
     else
     {
 #ifndef STANDALONE
-        WORLDVAR
+        CHECKVAR(intstr(val))
 #endif
         if(val<id->minval || val>id->maxval)
         {
@@ -685,7 +753,11 @@ void setvarchecked(ident *id, int val)
                 id->name, id->minval, id->maxval);
         }
         *id->storage.i = val;
-        if(versioning) id->def.i = val;
+        if(versioning)
+        {
+            id->def.i = val;
+            if(versioning == 2) id->bin.i = val;
+        }
         id->changed();                                             // call trigger function if available
 #ifndef STANDALONE
         client::editvar(id, interactive && !(identflags&IDF_WORLD));
@@ -700,7 +772,7 @@ void setfvarchecked(ident *id, float val)
     else
     {
 #ifndef STANDALONE
-        WORLDVAR
+        CHECKVAR(floatstr(val))
 #endif
         if(val<id->minvalf || val>id->maxvalf)
         {
@@ -708,7 +780,11 @@ void setfvarchecked(ident *id, float val)
             debugcode("\frvalid range for %s is %s..%s", id->name, floatstr(id->minvalf), floatstr(id->maxvalf));
         }
         *id->storage.f = val;
-        if(versioning) id->def.f = val;
+        if(versioning)
+        {
+            id->def.f = val;
+            if(versioning == 2) id->bin.f = val;
+        }
         id->changed();
 #ifndef STANDALONE
         client::editvar(id, interactive && !(identflags&IDF_WORLD));
@@ -723,7 +799,7 @@ void setsvarchecked(ident *id, const char *val)
     else
     {
 #ifndef STANDALONE
-        WORLDVAR
+        CHECKVAR(val)
 #endif
         delete[] *id->storage.s;
         *id->storage.s = newstring(val);
@@ -731,6 +807,11 @@ void setsvarchecked(ident *id, const char *val)
         {
             delete[] id->def.s;
             id->def.s = newstring(val);
+            if(versioning == 2)
+            {
+                delete[] id->bin.s;
+                id->bin.s = newstring(val);
+            }
         }
         id->changed();
 #ifndef STANDALONE
@@ -747,20 +828,20 @@ bool addcommand(const char *name, identfun fun, const char *args, int flags)
     bool limit = true;
     for(const char *fmt = args; *fmt; fmt++) switch(*fmt)
     {
-        case 'i': case 'b': case 'f': case 't': case 'N': case 'D': if(numargs < MAXARGS) numargs++; break;
+        case 'i': case 'b': case 'f': case 'g': case 't': case 'N': case 'D': if(numargs < MAXARGS) numargs++; break;
         case 's': case 'e': case 'r': case '$': if(numargs < MAXARGS) { argmask |= 1<<numargs; numargs++; } break;
         case '1': case '2': case '3': case '4': if(numargs < MAXARGS) fmt -= *fmt-'0'+1; break;
         case 'C': case 'V': limit = false; break;
         default: fatal("builtin %s declared with illegal type: %s", name, args); break;
     }
     if(limit && numargs > 16) fatal("builtin %s declared with too many args: %d", name, numargs);
-    addident(ident(ID_COMMAND, name, args, argmask, (void *)fun, flags));
+    addident(ident(ID_COMMAND, name, args, argmask, numargs, (void *)fun, flags));
     return false;
 }
 
 bool addkeyword(int type, const char *name, int flags)
 {
-    addident(ident(type, name, "", 0, NULL, flags));
+    addident(ident(type, name, "", 0, 0, NULL, flags));
     return true;
 }
 
@@ -830,29 +911,55 @@ static char *conc(vector<char> &buf, tagval *v, int n, bool space, const char *p
     return buf.getbuf();
 }
 
-char *conc(tagval *v, int n, bool space, const char *prefix)
+char *conc(tagval *v, int n, bool space, const char *prefix, int prefixlen)
 {
-    int len = space ? max(prefix ? n : n-1, 0) : 0, prefixlen = 0;
-    if(prefix) { prefixlen = strlen(prefix); len += prefixlen; }
-    loopi(n) switch(v[i].type)
+    static int vlen[MAXARGS];
+    static char numbuf[3*MAXSTRLEN];
+    int len = prefixlen, numlen = 0, i = 0;
+    for(; i < n; i++) switch(v[i].type)
     {
-        case VAL_MACRO: len += v[i].code[-1]>>8; break;
-        case VAL_STR: len += int(strlen(v[i].s)); break;
-        default:
+        case VAL_MACRO: len += (vlen[i] = v[i].code[-1]>>8); break;
+        case VAL_STR: len += (vlen[i] = int(strlen(v[i].s))); break;
+        case VAL_INT:
+            if(numlen + MAXSTRLEN > int(sizeof(numbuf))) goto overflow;
+            intformat(&numbuf[numlen], v[i].i);
+            numlen += (vlen[i] = strlen(&numbuf[numlen]));
+            break;
+        case VAL_FLOAT:
+            if(numlen + MAXSTRLEN > int(sizeof(numbuf))) goto overflow;
+            floatformat(&numbuf[numlen], v[i].f);
+            numlen += (vlen[i] = strlen(&numbuf[numlen]));
+            break;
+        default: vlen[i] = 0; break;
+    }
+overflow:
+    if(space) len += max(prefix ? i : i-1, 0);
+    char *buf = newstring(len + numlen);
+    int offset = 0, numoffset = 0;
+    if(prefix)
+    {
+        memcpy(buf, prefix, prefixlen);
+        offset += prefixlen;
+        if(space && i) buf[offset++] = ' ';
+    }
+    loopj(i)
+    {
+        if(v[j].type == VAL_INT || v[j].type == VAL_FLOAT)
         {
-            vector<char> buf;
-            buf.reserve(len);
-            conc(buf, v, n, space, prefix, prefixlen);
-            return newstring(buf.getbuf(), buf.length()-1);
+            memcpy(&buf[offset], &numbuf[numoffset], vlen[j]);
+            numoffset += vlen[j];
         }
+        else if(vlen[j]) memcpy(&buf[offset], v[j].s, vlen[j]);
+        offset += vlen[j];
+        if(j==i-1) break;
+        if(space) buf[offset++] = ' ';
     }
-    char *buf = newstring(prefix ? prefix : "", len);
-    if(prefix && space && n) { buf[prefixlen] = ' '; buf[prefixlen] = '\0'; }
-    loopi(n)
+    buf[offset] = '\0';
+    if(i < n)
     {
-        strcat(buf, v[i].s);
-        if(i==n-1) break;
-        if(space) strcat(buf, " ");
+        char *morebuf = conc(&v[i], n-i, space, buf, offset);
+        delete[] buf;
+        return morebuf;
     }
     return buf;
 }
@@ -1078,6 +1185,7 @@ static void compilelookup(vector<uint> &code, const char *&p, int ltype)
                         case 'i': compileint(code); numargs++; break;
                         case 'b': compileint(code, INT_MIN); numargs++; break;
                         case 'f': compilefloat(code); numargs++; break;
+                        case 'g': compilefloat(code, -FLT_MAX); numargs++; break;
                         case 't': compilenull(code); numargs++; break;
                         case 'e': compileblock(code); numargs++; break;
                         case 'r': compileident(code); numargs++; break;
@@ -1381,7 +1489,7 @@ static void compilestatements(vector<uint> &code, const char *&p, int rettype, i
             {
                 if(!checknumber(idname)) { compilestr(code, idname, idlen); delete[] idname; goto noid; }
                 char *end = idname;
-                int val = int(strtol(idname, &end, 0));
+                int val = int(strtoul(idname, &end, 0));
                 if(*end) compilestr(code, idname, idlen);
                 else compileint(code, val);
                 code.add(CODE_RESULT);
@@ -1390,7 +1498,7 @@ static void compilestatements(vector<uint> &code, const char *&p, int rettype, i
             {
                 case ID_ALIAS:
                     while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++;
-                    code.add(CODE_CALL|(id->index<<8));
+                    code.add((id->index < MAXARGS ? CODE_CALLARG : CODE_CALL)|(id->index<<8));
                     break;
                 case ID_COMMAND:
                 {
@@ -1417,6 +1525,7 @@ static void compilestatements(vector<uint> &code, const char *&p, int rettype, i
                     case 'i': if(more) more = compilearg(code, p, VAL_INT); if(!more) { if(rep) break; compileint(code); fakeargs++; } numargs++; break;
                     case 'b': if(more) more = compilearg(code, p, VAL_INT); if(!more) { if(rep) break; compileint(code, INT_MIN); fakeargs++; } numargs++; break;
                     case 'f': if(more) more = compilearg(code, p, VAL_FLOAT); if(!more) { if(rep) break; compilefloat(code); fakeargs++; } numargs++; break;
+                    case 'g': if(more) more = compilearg(code, p, VAL_FLOAT); if(!more) { if(rep) break; compilefloat(code, -FLT_MAX); fakeargs++; } numargs++; break;
                     case 't': if(more) more = compilearg(code, p, VAL_ANY); if(!more) { if(rep) break; compilenull(code); fakeargs++; } numargs++; break;
                     case 'e': if(more) more = compilearg(code, p, VAL_CODE); if(!more) { if(rep) break; compileblock(code); fakeargs++; } numargs++; break;
                     case 'r': if(more) more = compilearg(code, p, VAL_IDENT); if(!more) { if(rep) break; compileident(code); fakeargs++; } numargs++; break;
@@ -1635,6 +1744,7 @@ static inline void callcommand(ident *id, tagval *args, int numargs, bool lookup
         case 'i': if(++i >= numargs) { if(rep) break; args[i].setint(0); fakeargs++; } else forceint(args[i]); break;
         case 'b': if(++i >= numargs) { if(rep) break; args[i].setint(INT_MIN); fakeargs++; } else forceint(args[i]); break;
         case 'f': if(++i >= numargs) { if(rep) break; args[i].setfloat(0.0f); fakeargs++; } else forcefloat(args[i]); break;
+        case 'g': if(++i >= numargs) { if(rep) break; args[i].setfloat(-FLT_MAX); fakeargs++; } else forcefloat(args[i]); break;
         case 's': if(++i >= numargs) { if(rep) break; args[i].setstr(newstring("")); fakeargs++; } else forcestr(args[i]); break;
         case 't': if(++i >= numargs) { if(rep) break; args[i].setnull(); fakeargs++; } break;
         case 'e':
@@ -1962,6 +2072,7 @@ static const uint *runcode(const uint *code, tagval &result)
                 forcenull(result);
                 {
                     vector<char> buf;
+                    buf.reserve(MAXSTRLEN);
                     ((comfun1)id->fun)(conc(buf, args, numargs, true));
                 }
                 goto forceresult;
@@ -2186,7 +2297,7 @@ static inline bool getbool(const char *s)
         case '0':
         {
             char *end;
-            int val = strtol((char *)s, &end, 0);
+            int val = int(strtoul((char *)s, &end, 0));
             if(val) return true;
             switch(*end)
             {
@@ -2240,9 +2351,9 @@ bool execfile(const char *cfgfile, bool msg, int flags)
         return false;
     }
     int oldflags = identflags;
-    bool oldversion = versioning;
+    int oldversion = versioning;
     if(flags&EXEC_NOWORLD) identflags &= ~IDF_WORLD;
-    if(flags&EXEC_VERSION) versioning = true;
+    if(flags&EXEC_VERSION) versioning = flags&EXEC_BUILTIN ? 2 : 1;
     const char *oldsourcefile = sourcefile, *oldsourcestr = sourcestr;
     sourcefile = cfgfile;
     sourcestr = buf;
@@ -2255,6 +2366,7 @@ bool execfile(const char *cfgfile, bool msg, int flags)
     if(verbose >= 2) conoutf("\faloaded script %s", cfgfile);
     return true;
 }
+ICOMMAND(0, exec, "sib", (char *file, int *flags, int *msg), intret(execfile(file, *msg != 0, *flags) ? 1 : 0));
 
 const char *escapestring(const char *s)
 {
@@ -2277,6 +2389,15 @@ const char *escapestring(const char *s)
     return buf.getbuf();
 }
 
+ICOMMAND(0, escape, "s", (char *s), result(escapestring(s)));
+ICOMMAND(0, unescape, "s", (char *s),
+{
+    int len = strlen(s);
+    char *d = newstring(len);
+    d[unescapestring(d, s, &s[len])] = '\0';
+    stringret(d);
+});
+
 const char *escapeid(const char *s)
 {
     const char *end = s + strcspn(s, "\"/;()[]@ \f\t\r\n\0");
@@ -2303,19 +2424,19 @@ bool validateblock(const char *s)
 // below the commands that implement a small imperative language. thanks to the semantics of
 // () and [] expressions, any control construct can be defined trivially.
 
-static string retbuf[3];
+static string retbuf[4];
 static int retidx = 0;
 
 const char *intstr(int v)
 {
-    retidx = (retidx + 1)%3;
-    formatstring(retbuf[retidx])("%d", v);
+    retidx = (retidx + 1)%4;
+    intformat(retbuf[retidx], v);
     return retbuf[retidx];
 }
 
 const char *intstr(ident *id)
 {
-    retidx = (retidx + 1)%3;
+    retidx = (retidx + 1)%4;
     formatstring(retbuf[retidx])(id->flags&IDF_HEX && *id->storage.i >= 0 ? (id->maxval==0xFFFFFF ? "0x%.6X" : "0x%X") : "%d", *id->storage.i);
     return retbuf[retidx];
 }
@@ -2327,8 +2448,8 @@ void intret(int v)
 
 const char *floatstr(float v)
 {
-    retidx = (retidx + 1)%3;
-    formatstring(retbuf[retidx])(v==int(v) ? "%.1f" : "%.7g", v);
+    retidx = (retidx + 1)%4;
+    floatformat(retbuf[retidx], v);
     return retbuf[retidx];
 }
 
@@ -2344,9 +2465,43 @@ ICOMMAND(0, do, "e", (uint *body), executeret(body, *commandret));
 ICOMMAND(0, if, "tee", (tagval *cond, uint *t, uint *f), executeret(getbool(*cond) ? t : f, *commandret));
 ICOMMAND(0, ?, "ttt", (tagval *cond, tagval *t, tagval *f), result(*(getbool(*cond) ? t : f)));
 
+ICOMMAND(0, pushif, "rte", (ident *id, tagval *v, uint *code),
+{
+    if(id->type != ID_ALIAS || id->index < MAXARGS) return;
+    if(getbool(*v))
+    {
+        identstack stack;
+        pusharg(*id, *v, stack);
+        v->type = VAL_NULL;
+        id->flags &= ~IDF_UNKNOWN;
+        executeret(code, *commandret);
+        poparg(*id);
+    }
+});
+
+void loopiter(ident *id, identstack &stack, const tagval &v)
+{
+    if(id->stack != &stack)
+    {
+        pusharg(*id, v, stack);
+        id->flags &= ~IDF_UNKNOWN;
+    }
+    else
+    {
+        if(id->valtype == VAL_STR) delete[] id->val.s;
+        cleancode(*id);
+        id->setval(v);
+    }
+}
+
+void loopend(ident *id, identstack &stack)
+{
+    if(id->stack == &stack) poparg(*id);
+}
+
 static inline void setiter(ident &id, int i, identstack &stack)
 {
-    if(i)
+    if(id.stack == &stack)
     {
         if(id.valtype != VAL_INT)
         {
@@ -2358,9 +2513,9 @@ static inline void setiter(ident &id, int i, identstack &stack)
     }
     else
     {
-        tagval zero;
-        zero.setint(0);
-        pusharg(id, zero, stack);
+        tagval t;
+        t.setint(i);
+        pusharg(id, t, stack);
         id.flags &= ~IDF_UNKNOWN;
     }
 }
@@ -2404,7 +2559,7 @@ char *loopconc(ident *id, int n, uint *body, bool space)
         s.put(vstr, len);
         freearg(v);
     }
-    poparg(*id);
+    if(n > 0) poparg(*id);
     s.add('\0');
     return newstring(s.getbuf(), s.length()-1);
 }
@@ -2423,11 +2578,13 @@ void concat(tagval *v, int n)
 {
     commandret->setstr(conc(v, n, true));
 }
+COMMAND(0, concat, "V");
 
 void concatword(tagval *v, int n)
 {
     commandret->setstr(conc(v, n, false));
 }
+COMMAND(0, concatword, "V");
 
 void result(tagval &v)
 {
@@ -2445,6 +2602,12 @@ void result(const char *s)
     commandret->setstr(newstring(s));
 }
 
+ICOMMAND(0, result, "t", (tagval *v),
+{
+    *commandret = *v;
+    v->type = VAL_NULL;
+});
+
 void format(tagval *args, int numargs)
 {
     vector<char> s;
@@ -2468,15 +2631,6 @@ void format(tagval *args, int numargs)
     s.add('\0');
     result(s.getbuf());
 }
-
-ICOMMAND(0, result, "t", (tagval *v),
-{
-    *commandret = *v;
-    v->type = VAL_NULL;
-});
-
-COMMAND(0, concat, "V");
-COMMAND(0, concatword, "V");
 COMMAND(0, format, "V");
 
 static const char *liststart = NULL, *listend = NULL, *listquotestart = NULL, *listquoteend = NULL;
@@ -2546,6 +2700,7 @@ int listlen(const char *s)
     while(parselist(s)) n++;
     return n;
 }
+ICOMMAND(0, listlen, "s", (char *s), intret(listlen(s)));
 
 const char *indexlist(const char *s, int pos, int &len)
 {
@@ -2569,12 +2724,14 @@ void at(tagval *args, int numargs)
     }
     commandret->setstr(newstring(start, end-start));
 }
+COMMAND(0, at, "si1V");
 
-void substr(char *s, int *start, int *count, int *numargs)
+void substring(char *s, int *start, int *count, int *numargs)
 {
     int len = strlen(s), offset = clamp(*start, 0, len);
     commandret->setstr(newstring(&s[offset], *numargs >= 3 ? clamp(*count, 0, len - offset) : len - offset));
 }
+COMMAND(0, substring, "siiN");
 
 void sublist(const char *s, int *skip, int *count, int *numargs)
 {
@@ -2585,62 +2742,116 @@ void sublist(const char *s, int *skip, int *count, int *numargs)
     if(len > 0 && parselist(s, start, end, list, qend)) while(--len > 0 && parselist(s, start, end, qstart, qend));
     commandret->setstr(newstring(list, qend - list));
 }
+COMMAND(0, sublist, "siiN");
 
-void getalias_(char *s)
-{
-    result(getalias(s));
-}
-
-ICOMMAND(0, exec, "si", (char *file, int *n), execfile(file, true, *n));
-COMMAND(0, at, "si1V");
-ICOMMAND(0, escape, "s", (char *s), result(escapestring(s)));
-ICOMMAND(0, unescape, "s", (char *s),
-{
-    int len = strlen(s);
-    char *d = newstring(len);
-    d[unescapestring(d, s, &s[len])] = '\0';
-    stringret(d);
-});
 ICOMMAND(0, stripcolors, "s", (char *s),
 {
-    int len = strlen(s);
+    size_t len = strlen(s);
     char *d = newstring(len);
-    filtertext(d, s, false, true, true, len);
+    filterstring(d, s, false, true, true, false, len);
     stringret(d);
 });
-COMMAND(0, substr, "siiN");
-COMMAND(0, sublist, "siiN");
-ICOMMAND(0, listlen, "s", (char *s), intret(listlen(s)));
-ICOMMAND(0, indexof, "ss", (char *list, char *elem), intret(listincludes(list, elem, strlen(elem))));
-COMMANDN(0, getalias, getalias_, "s");
 
-void looplist(ident *id, const char *list, const uint *body, bool search)
+static inline void setiter(ident &id, char *val, identstack &stack)
+{
+    if(id.stack == &stack)
+    {
+        if(id.valtype == VAL_STR) delete[] id.val.s;
+        else id.valtype = VAL_STR;
+        cleancode(id);
+        id.val.s = val;
+    }
+    else
+    {
+        tagval t;
+        t.setstr(val);
+        pusharg(id, t, stack);
+        id.flags &= ~IDF_UNKNOWN;
+    }
+}
+
+void listfind(ident *id, const char *list, const uint *body)
 {
-    if(id->type!=ID_ALIAS) { if(search) intret(-1); return; }
+    if(id->type!=ID_ALIAS) { intret(-1); return; }
     identstack stack;
-    int n = 0;
+    int n = -1;
     for(const char *s = list, *start, *end; parselist(s, start, end);)
     {
+        ++n;
         char *val = newstring(start, end-start);
-        if(n++)
-        {
-            if(id->valtype == VAL_STR) delete[] id->val.s;
-            else id->valtype = VAL_STR;
-            cleancode(*id);
-            id->val.s = val;
-        }
-        else
+        setiter(*id, val, stack);
+        if(executebool(body)) { intret(n); goto found; }
+    }
+    intret(-1);
+found:
+    if(n >= 0) poparg(*id);
+}
+COMMAND(0, listfind, "rse");
+
+void looplist(ident *id, const char *list, const uint *body)
+{
+    if(id->type!=ID_ALIAS) return;
+    identstack stack;
+    int n = 0;
+    for(const char *s = list, *start, *end; parselist(s, start, end); n++)
+    {
+        char *val = newstring(start, end-start);
+        setiter(*id, val, stack);
+        execute(body);
+    }
+    if(n) poparg(*id);
+}
+COMMAND(0, looplist, "rse");
+
+void looplistconc(ident *id, const char *list, const uint *body, bool space)
+{
+    if(id->type!=ID_ALIAS) return;
+    identstack stack;
+    vector<char> r;
+    int n = 0;
+    for(const char *s = list, *start, *end; parselist(s, start, end); n++)
+    {
+        char *val = newstring(start, end-start);
+        setiter(*id, val, stack);
+
+        if(n && space) r.add(' ');
+
+        tagval v;
+        executeret(body, v);
+        const char *vstr = v.getstr();
+        int len = strlen(vstr);
+        r.put(vstr, len);
+        freearg(v);
+    }
+    if(n) poparg(*id);
+    r.add('\0');
+    commandret->setstr(newstring(r.getbuf(), r.length()-1));
+}
+ICOMMAND(0, looplistconcat, "rse", (ident *id, char *list, uint *body), looplistconc(id, list, body, true));
+ICOMMAND(0, looplistconcatword, "rse", (ident *id, char *list, uint *body), looplistconc(id, list, body, false));
+
+void listfilter(ident *id, const char *list, const uint *body)
+{
+    if(id->type!=ID_ALIAS) return;
+    identstack stack;
+    vector<char> r;
+    int n = 0;
+    for(const char *s = list, *start, *end, *quotestart, *quoteend; parselist(s, start, end, quotestart, quoteend); n++)
+    {
+        char *val = newstring(start, end-start);
+        setiter(*id, val, stack);
+
+        if(executebool(body))
         {
-            tagval t;
-            t.setstr(val);
-            pusharg(*id, t, stack);
-            id->flags &= ~IDF_UNKNOWN;
+            if(r.length()) r.add(' ');
+            r.put(quotestart, quoteend-quotestart);
         }
-        if(executebool(body) && search) { intret(n-1); search = false; break; }
     }
-    if(search) intret(-1);
     if(n) poparg(*id);
+    r.add('\0');
+    commandret->setstr(newstring(r.getbuf(), r.length()-1));
 }
+COMMAND(0, listfilter, "rse");
 
 void prettylist(const char *s, const char *conj)
 {
@@ -2676,6 +2887,7 @@ int listincludes(const char *list, const char *needle, int needlelen)
     }
     return -1;
 }
+ICOMMAND(0, indexof, "ss", (char *list, char *elem), intret(listincludes(list, elem, strlen(elem))));
 
 char *listdel(const char *s, const char *del)
 {
@@ -2691,6 +2903,7 @@ char *listdel(const char *s, const char *del)
     p.add('\0');
     return newstring(p.getbuf(), p.length()-1);
 }
+ICOMMAND(0, listdel, "ss", (char *list, char *del), commandret->setstr(listdel(list, del)));
 
 char *shrinklist(const char *list, const char *limit, int failover, bool invert)
 {
@@ -2717,10 +2930,11 @@ char *shrinklist(const char *list, const char *limit, int failover, bool invert)
     p.add('\0');
     return newstring(p.getbuf(), p.length()-1);
 }
+ICOMMAND(0, shrinklist, "ssii", (char *s, char *t, int *n, int *v), commandret->setstr(shrinklist(s, t, *n, *v!=0)));
 
-void listsplice(const char *s, const char *vals, int *skip, int *count, int *numargs)
+void listsplice(const char *s, const char *vals, int *skip, int *count)
 {
-    int offset = max(*skip, 0), len = *numargs >= 4 ? max(*count, 0) : -1;
+    int offset = max(*skip, 0), len = max(*count, 0);
     const char *list = s, *start, *end, *qstart, *qend = s;
     loopi(offset) if(!parselist(s, start, end, qstart, qend)) break;
     vector<char> p;
@@ -2730,7 +2944,7 @@ void listsplice(const char *s, const char *vals, int *skip, int *count, int *num
         if(!p.empty()) p.add(' ');
         p.put(vals, strlen(vals));
     }
-    while(len-- > 0) if(!parselist(s)) break;
+    loopi(len) if(!parselist(s)) break;
     skiplist(s);
     switch(*s)
     {
@@ -2743,24 +2957,38 @@ void listsplice(const char *s, const char *vals, int *skip, int *count, int *num
     p.add('\0');
     commandret->setstr(newstring(p.getbuf(), p.length()-1));
 }
-COMMAND(0, listsplice, "ssiiN");
+COMMAND(0, listsplice, "ssii");
+
+ICOMMAND(0, listfiles, "ss", (char *dir, char *ext),
+{
+    vector<char *> files;
+    listfiles(dir, ext[0] ? ext : NULL, files);
+    vector<char> p;
+    loopv(files)
+    {
+        if(i) p.put(' ');
+        p.put(files[i], strlen(files[i]));
+    }
+    p.add('\0');
+    commandret->setstr(newstring(p.getbuf(), p.length()-1));
+});
 
-ICOMMAND(0, listdel, "ss", (char *list, char *del), commandret->setstr(listdel(list, del)));
-ICOMMAND(0, shrinklist, "ssii", (char *s, char *t, int *n, int *v), commandret->setstr(shrinklist(s, t, *n, *v!=0)));
-ICOMMAND(0, listfind, "rse", (ident *id, char *list, uint *body), looplist(id, list, body, true));
-ICOMMAND(0, looplist, "rse", (ident *id, char *list, uint *body), looplist(id, list, body, false));
 ICOMMAND(0, loopfiles, "rsse", (ident *id, char *dir, char *ext, uint *body),
 {
     if(id->type!=ID_ALIAS) return;
     identstack stack;
     vector<char *> files;
     listfiles(dir, ext[0] ? ext : NULL, files);
+//    loopvrev(files)
+//    {
+//        char *file = files[i];
+//        bool redundant = false;
+//        loopj(i) if(!strcmp(files[j], file)) { redundant = true; break; }
+//        if(redundant) delete[] files.removeunordered(i);
+//    }
     loopv(files)
     {
         char *file = files[i];
-//        bool redundant = false;
-//        loopj(i) if(!strcmp(files[j], file)) { redundant = true; break; }
-//        if(redundant) { delete[] file; continue; }
         if(i)
         {
             if(id->valtype == VAL_STR) delete[] id->val.s;
@@ -2779,6 +3007,14 @@ ICOMMAND(0, loopfiles, "rsse", (ident *id, char *dir, char *ext, uint *body),
     if(files.length()) poparg(*id);
 });
 
+ICOMMAND(0, findfile, "s", (char *name),
+{
+    string fname;
+    copystring(fname, name);
+    path(fname);
+    intret(findzipfile(fname) || fileexists(fname, "e") || findfile(fname, "e") ? 1 : 0);
+});
+
 struct sortitem
 {
     const char *str, *quotestart, *quoteend;
@@ -2922,8 +3158,8 @@ ICOMMAND(0, ~, "i", (int *a), intret(~*a));
 ICOMMAND(0, ^~, "ii", (int *a, int *b), intret(*a ^ ~*b));
 ICOMMAND(0, &~, "ii", (int *a, int *b), intret(*a & ~*b));
 ICOMMAND(0, |~, "ii", (int *a, int *b), intret(*a | ~*b));
-ICOMMAND(0, <<, "ii", (int *a, int *b), intret(*a << *b));
-ICOMMAND(0, >>, "ii", (int *a, int *b), intret(*a >> *b));
+ICOMMAND(0, <<, "ii", (int *a, int *b), intret(*b < 32 ? *a << max(*b, 0) : 0));
+ICOMMAND(0, >>, "ii", (int *a, int *b), intret(*a >> clamp(*b, 0, 31)));
 ICOMMAND(0, &&, "e1V", (tagval *args, int numargs),
 {
     if(!numargs) intret(1);
@@ -2953,6 +3189,7 @@ ICOMMAND(0, tan, "f", (float *a), floatret(tan(*a*RAD)));
 ICOMMAND(0, asin, "f", (float *a), floatret(asin(*a)/RAD));
 ICOMMAND(0, acos, "f", (float *a), floatret(acos(*a)/RAD));
 ICOMMAND(0, atan, "f", (float *a), floatret(atan(*a)/RAD));
+ICOMMAND(0, atan2, "ff", (float *y, float *x), floatret(atan2(*y, *x)/RAD));
 ICOMMAND(0, sqrt, "f", (float *a), floatret(sqrt(*a)));
 ICOMMAND(0, pow, "ff", (float *a, float *b), floatret(pow(*a, *b)));
 ICOMMAND(0, loge, "f", (float *a), floatret(log(*a)));
@@ -3030,20 +3267,92 @@ CASECOMMAND(casef, "f", float, args[0].getfloat(), args[i].type == VAL_NULL || a
 CASECOMMAND(cases, "s", const char *, args[0].getstr(), args[i].type == VAL_NULL || !strcmp(args[i].getstr(), val));
 
 ICOMMAND(0, rnd, "ii", (int *a, int *b), intret(*a - *b > 0 ? rnd(*a - *b) + *b : *b));
-ICOMMAND(0, strcmp, "ss", (char *a, char *b), intret(strcmp(a,b)==0));
+ICOMMAND(0, stringcmp, "ss", (char *a, char *b), intret(strcmp(a,b)==0));
 ICOMMAND(0, =s, "ss", (char *a, char *b), intret(strcmp(a,b)==0));
 ICOMMAND(0, !=s, "ss", (char *a, char *b), intret(strcmp(a,b)!=0));
 ICOMMAND(0, <s, "ss", (char *a, char *b), intret(strcmp(a,b)<0));
 ICOMMAND(0, >s, "ss", (char *a, char *b), intret(strcmp(a,b)>0));
 ICOMMAND(0, <=s, "ss", (char *a, char *b), intret(strcmp(a,b)<=0));
 ICOMMAND(0, >=s, "ss", (char *a, char *b), intret(strcmp(a,b)>=0));
-ICOMMAND(0, strcasecmp, "ss", (char *a, char *b), intret(strcasecmp(a,b)==0));
-ICOMMAND(0, strncmp, "ssi", (char *a, char *b, int *n), intret(strncmp(a,b,*n)==0));
-ICOMMAND(0, strncasecmp, "ssi", (char *a, char *b, int *n), intret(strncasecmp(a,b,*n)==0));
 ICOMMAND(0, echo, "C", (char *s), conoutft(CON_MESG, "%s", s));
 ICOMMAND(0, error, "C", (char *s), conoutft(CON_DEBUG, "\fr%s", s));
-ICOMMAND(0, strstr, "ss", (char *a, char *b), { char *s = strstr(a, b); intret(s ? s-a : -1); });
-ICOMMAND(0, strlen, "s", (char *s), intret(strlen(s)));
+ICOMMAND(0, stringlen, "s", (char *s), intret(strlen(s)));
+ICOMMAND(0, stringcode, "si", (char *s, int *i), intret(*i > 0 ? (memchr(s, 0, *i) ? 0 : uchar(s[*i])) : uchar(s[0])));
+ICOMMAND(0, codestring, "i", (int *i), { char *s = newstring(1); s[0] = char(*i); s[1] = '\0'; stringret(s); });
+ICOMMAND(0, stringuni, "si", (char *s, int *i), intret(*i > 0 ? (memchr(s, 0, *i) ? 0 : cube2uni(s[*i])) : cube2uni(s[0])));
+ICOMMAND(0, unistring, "i", (int *i), { char *s = newstring(1); s[0] = uni2cube(*i); s[1] = '\0'; stringret(s); });
+
+// some dirty hacks to get around macro expansion issues
+int rigstr(char *s, char *n)
+{
+    char *v = strstr(s, n);
+    return v ? v-s : -1;
+}
+ICOMMAND(0, stringstr, "ss", (char *a, char *b), intret(rigstr(a, b)));
+
+int rigcasecmp(char *s, char *n)
+{
+    return strcasecmp(s, n);
+}
+ICOMMAND(0, stringcasecmp, "ss", (char *a, char *b), intret(rigcasecmp(a,b)==0));
+
+int rigncmp(char *s, char *n, int len)
+{
+    return strncmp(s, n, len);
+}
+ICOMMAND(0, stringncmp, "ssi", (char *a, char *b, int *n), intret(rigncmp(a,b,*n)==0));
+
+int rigncasecmp(char *s, char *n, int len)
+{
+    return strncasecmp(s, n, len);
+}
+ICOMMAND(0, stringncasecmp, "ssi", (char *a, char *b, int *n), intret(rigncasecmp(a,b,*n)==0));
+
+#define STRMAPCOMMAND(name, map) \
+    ICOMMAND(0, name, "s", (char *s), \
+    { \
+        int len = strlen(s); \
+        char *m = newstring(len); \
+        loopi(len) m[i] = map(s[i]); \
+        m[len] = '\0'; \
+        stringret(m); \
+    })
+
+STRMAPCOMMAND(strlower, cubelower);
+STRMAPCOMMAND(strupper, cubeupper);
+
+char *rigcasestr(const char *s, const char *n)
+{
+    bool passed = true;
+    char *start = newstring(s), *needle = newstring(n), *a = start, *b = needle, *ret = NULL;
+    while(*a && *b)
+    {
+        *a = cubelower(*a);
+        *b = cubelower(*b);
+        if(passed && *a != *b) passed = false;
+        a++;
+        b++;
+    }
+    if(!*b)
+    {
+        if(passed) ret = (char *)s;
+        else
+        {
+            while(*a)
+            {
+                *a = cubelower(*a);
+                a++;
+            }
+            char *p = strstr(start, needle);
+            if(p) ret = (char *)(s+(p-start));
+        }
+    }
+    delete[] start;
+    delete[] needle;
+    return ret;
+}
+
+ICOMMAND(0, stringcasestr, "ss", (char *a, char *b), { char *s = rigcasestr(a, b); intret(s ? s-a : -1); });
 
 char *strreplace(const char *s, const char *oldval, const char *newval)
 {
@@ -3069,16 +3378,21 @@ char *strreplace(const char *s, const char *oldval, const char *newval)
     }
 }
 
-ICOMMAND(0, strreplace, "sss", (char *s, char *o, char *n), commandret->setstr(strreplace(s, o, n)));
+ICOMMAND(0, stringreplace, "sss", (char *s, char *o, char *n), commandret->setstr(strreplace(s, o, n)));
 
-void nonworld(uint *contents)
+void stringsplice(const char *s, const char *vals, int *skip, int *count)
 {
-    int oldflags = identflags;
-    identflags &= ~IDF_WORLD;
-    execute(contents);
-    identflags = oldflags;
+    int slen = strlen(s), vlen = strlen(vals),
+        offset = clamp(*skip, 0, slen),
+        len = clamp(*count, 0, slen - offset);
+    char *p = newstring(slen - len + vlen);
+    if(offset) memcpy(p, s, offset);
+    if(vlen) memcpy(&p[offset], vals, vlen);
+    if(offset + len < slen) memcpy(&p[offset + vlen], &s[offset + len], slen - (offset + len));
+    p[slen - len + vlen] = '\0';
+    commandret->setstr(p);
 }
-COMMAND(0, nonworld, "e");
+COMMAND(0, stringsplice, "ssii");
 
 struct sleepcmd
 {
@@ -3136,7 +3450,7 @@ ICOMMAND(0, getmillis, "i", (int *total), intret(*total ? totalmillis : lastmill
 
 void getvariable(int num)
 {
-    mkstring(text);
+    string text = "";
     num--;
     static vector<ident *> ids;
     static int lastupdate = 0;
@@ -3156,6 +3470,40 @@ void getvariable(int num)
 }
 ICOMMAND(0, getvariable, "i", (int *n), getvariable(*n));
 
+void getvarinfo(int n, int types, int notypes, int flags, int noflags, char *str)
+{
+    static vector<ident *> ids[2];
+    static int lastupdate = 0, lasttypes = 0, lastnotypes = 0, lastflags = 0, lastnoflags = 0, curids = 0;
+    if(ids[0].empty() || !lastupdate || types != lasttypes || notypes != lastnotypes || flags != lastflags || noflags != lastnoflags || totalmillis-lastupdate >= 60000)
+    {
+        loopi(2) ids[i].setsize(0);
+        enumerate(idents, ident, id, if((!types || (1<<id.type)&types) && (!notypes || !((1<<id.type)&notypes)) && (!flags || id.flags&flags) && (!noflags || !(id.flags&noflags))) ids[0].add(&id));
+        lastupdate = totalmillis;
+        lasttypes = types;
+        lastnotypes = notypes;
+        lastflags = flags;
+        lastnoflags = noflags;
+        ids[0].sort(ident::compare);
+    }
+    if(str && *str)
+    {
+        static char *laststr = NULL;
+        if(ids[1].empty() || !laststr || strcmp(str, laststr))
+        {
+            ids[1].setsize(0);
+            loopv(ids[0]) if(rigcasestr(ids[0][i]->name, str)) ids[1].add(ids[0][i]);
+            if(laststr) DELETEA(laststr);
+            laststr = newstring(str);
+        }
+        curids = 1;
+    }
+    else curids = 0;
+    if(n < 0) intret(ids[curids].length());
+    else if(ids[curids].inrange(n)) result(ids[curids][n]->name);
+}
+
+ICOMMAND(0, getvarinfo, "biiiis", (int *n, int *w, int *x, int *t, int *o, char *s), getvarinfo(*n, *w, *x, *t, *o, s));
+
 void hexcolour(int *n)
 {
     defformatstring(s)(*n >= 0 && *n <= 0xFFFFFF ? "0x%.6X" : "%d", *n);
@@ -3168,7 +3516,7 @@ void genkey(char *s)
 {
     vector<char> privkey, pubkey;
     genprivkey(s, privkey, pubkey);
-    conoutft(CON_MESG, "private key: %s", privkey.getbuf());
-    conoutft(CON_MESG, "public key: %s", pubkey.getbuf());
+    defformatstring(keybuf)("%s %s", privkey.getbuf(), pubkey.getbuf());
+    result(keybuf);
 }
 COMMAND(0, genkey, "s");
diff --git a/src/engine/console.cpp b/src/engine/console.cpp
index a627217..75fc707 100644
--- a/src/engine/console.cpp
+++ b/src/engine/console.cpp
@@ -2,46 +2,38 @@
 
 #include "engine.h"
 
-vector<cline> conlines;
+reversequeue<cline, MAXCONLINES> conlines;
+
 int commandmillis = -1;
-string commandbuf;
+bigstring commandbuf;
 char *commandaction = NULL, *commandicon = NULL;
 enum { CF_COMPLETE = 1<<0, CF_EXECUTE = 1<<1, CF_MESSAGE = 1<<2 };
 int commandflags = 0, commandpos = -1, commandcolour = 0;
 
 void conline(int type, const char *sf, int n)
 {
-    cline cl;
+    char *buf = conlines.length() >= MAXCONLINES ? conlines.remove().cref : newstring("", BIGSTRLEN-1);
+    cline &cl = conlines.add();
     cl.type = type;
-    cl.cref = conlines.length() > MAXSTRLEN ? conlines.pop().cref : newstringbuf("");
+    cl.cref = buf;
     cl.reftime = cl.outtime = totalmillis;
-    conlines.insert(n, cl);
-
-    int c = 0;
-    #define addcref(d) { cl.cref[c] = d; c++; }
-    if(n)
-    {
-        addcref(' ');
-        addcref(' ');
-        concatstring(cl.cref, sf);
-    }
-    addcref(0);
-    concatstring(cl.cref, sf);
+    cl.realtime = clocktime;
 
     if(n)
     {
-        string sd;
+        copybigstring(cl.cref, "  ");
+        concatbigstring(cl.cref, sf);
         loopj(2)
         {
             int off = n+j;
             if(conlines.inrange(off))
             {
-                if(j) formatstring(sd)("%s\fs", conlines[off].cref);
-                else formatstring(sd)("\fS%s", conlines[off].cref);
-                copystring(conlines[off].cref, sd);
+                if(j) concatbigstring(conlines[off].cref, "\fs");
+                else prependbigstring(conlines[off].cref, "\fS");
             }
         }
     }
+    else copybigstring(cl.cref, sf);
 }
 
 // keymap is defined externally in keymap.cfg
@@ -87,32 +79,14 @@ const char *getkeyname(int code)
     return km ? km->name : NULL;
 }
 
-#define ADDSEP(comma, conj) do { \
-    if(pretty && *pretty) \
-    { \
-        names.put("\fs", 2); \
-        names.put(pretty, strlen(pretty)); \
-        if(comma) names.add(comma); \
-        names.add(' '); \
-        if((conj)[0]) \
-        { \
-            names.put(conj, strlen(conj)); \
-            names.add(' '); \
-        } \
-        names.put("\fS", 2); \
-    } \
-    else if(sep && *sep) names.put(sep, strlen(sep)); \
-    else names.add(' '); \
-} while(0)
-
-void searchbindlist(const char *action, int type, int limit, const char *sep, const char *pretty, vector<char> &names)
+void searchbindlist(const char *action, int type, int limit, const char *s1, const char *s2, const char *sep1, const char *sep2, vector<char> &names, bool force)
 {
     const char *name1 = NULL, *name2 = NULL, *lastname = NULL;
     int found = 0;
     enumerate(keyms, keym, km,
     {
-        char *act = ((!km.actions[type] || !*km.actions[type]) && type ? km.actions[keym::ACTION_DEFAULT] : km.actions[type]);
-        if(!strcmp(act, action))
+        char *act = type && force && (!km.actions[type] || !*km.actions[type]) ? km.actions[keym::ACTION_DEFAULT] : km.actions[type];
+        if(act && !strcmp(act, action))
         {
             if(!name1) name1 = km.name;
             else if(!name2) name2 = km.name;
@@ -120,14 +94,20 @@ void searchbindlist(const char *action, int type, int limit, const char *sep, co
             {
                 if(lastname)
                 {
-                    ADDSEP(',', "");
+                    if(sep1 && *sep1) names.put(sep1, strlen(sep1));
+                    if(s1 && *s1) names.put(s1, strlen(s1));
                     names.put(lastname, strlen(lastname));
+                    if(s2 && *s2) names.put(s2, strlen(s2));
                 }
                 else
                 {
+                    if(s1 && *s1) names.put(s1, strlen(s1));
                     names.put(name1, strlen(name1));
-                    ADDSEP(',', "");
+                    if(s2 && *s2) names.put(s2, strlen(s2));
+                    if(sep1 && *sep1) names.put(sep1, strlen(sep1));
+                    if(s1 && *s1) names.put(s1, strlen(s1));
                     names.put(name2, strlen(name2));
+                    if(s2 && *s2) names.put(s2, strlen(s2));
                 }
                 lastname = km.name;
             }
@@ -137,16 +117,27 @@ void searchbindlist(const char *action, int type, int limit, const char *sep, co
     });
     if(lastname)
     {
-        ADDSEP(',', sep);
+        if(sep2 && *sep2) names.put(sep2, strlen(sep2));
+        else if(sep1 && *sep1) names.put(sep1, strlen(sep1));
+        if(s1 && *s1) names.put(s1, strlen(s1));
         names.put(lastname, strlen(lastname));
+        if(s2 && *s2) names.put(s2, strlen(s2));
     }
     else
     {
-        if(name1) names.put(name1, strlen(name1));
+        if(name1)
+        {
+            if(s1 && *s1) names.put(s1, strlen(s1));
+            names.put(name1, strlen(name1));
+            if(s2 && *s2) names.put(s2, strlen(s2));
+        }
         if(name2)
         {
-            ADDSEP('\0', sep);
+            if(sep2 && *sep2) names.put(sep2, strlen(sep2));
+            else if(sep1 && *sep1) names.put(sep1, strlen(sep1));
+            if(s1 && *s1) names.put(s1, strlen(s1));
             names.put(name2, strlen(name2));
+            if(s2 && *s2) names.put(s2, strlen(s2));
         }
     }
     names.add('\0');
@@ -162,7 +153,7 @@ const char *searchbind(const char *action, int type)
     return NULL;
 }
 
-void getkeypressed(int limit, const char *sep, const char *pretty, vector<char> &names)
+void getkeypressed(int limit, const char *s1, const char *s2, const char *sep1, const char *sep2, vector<char> &names)
 {
     const char *name1 = NULL, *name2 = NULL, *lastname = NULL;
     int found = 0;
@@ -176,14 +167,20 @@ void getkeypressed(int limit, const char *sep, const char *pretty, vector<char>
             {
                 if(lastname)
                 {
-                    ADDSEP(',', "");
+                    if(sep1 && *sep1) names.put(sep1, strlen(sep1));
+                    if(s1 && *s1) names.put(s1, strlen(s1));
                     names.put(lastname, strlen(lastname));
+                    if(s2 && *s2) names.put(s2, strlen(s2));
                 }
                 else
                 {
+                    if(s1 && *s1) names.put(s1, strlen(s1));
                     names.put(name1, strlen(name1));
-                    ADDSEP(',', "");
+                    if(s2 && *s2) names.put(s2, strlen(s2));
+                    if(sep1 && *sep1) names.put(sep1, strlen(sep1));
+                    if(s1 && *s1) names.put(s1, strlen(s1));
                     names.put(name2, strlen(name2));
+                    if(s2 && *s2) names.put(s2, strlen(s2));
                 }
                 lastname = km.name;
             }
@@ -193,16 +190,27 @@ void getkeypressed(int limit, const char *sep, const char *pretty, vector<char>
     });
     if(lastname)
     {
-        ADDSEP(',', sep);
+        if(sep2 && *sep2) names.put(sep2, strlen(sep2));
+        else if(sep1 && *sep1) names.put(sep1, strlen(sep1));
+        if(s1 && *s1) names.put(s1, strlen(s1));
         names.put(lastname, strlen(lastname));
+        if(s2 && *s2) names.put(s2, strlen(s2));
     }
     else
     {
-        if(name1) names.put(name1, strlen(name1));
+        if(name1)
+        {
+            if(s1 && *s1) names.put(s1, strlen(s1));
+            names.put(name1, strlen(name1));
+            if(s2 && *s2) names.put(s2, strlen(s2));
+        }
         if(name2)
         {
-            ADDSEP('\0', sep);
+            if(sep2 && *sep2) names.put(sep2, strlen(sep2));
+            else if(sep1 && *sep1) names.put(sep1, strlen(sep1));
+            if(s1 && *s1) names.put(s1, strlen(s1));
             names.put(name2, strlen(name2));
+            if(s2 && *s2) names.put(s2, strlen(s2));
         }
     }
     names.add('\0');
@@ -259,19 +267,19 @@ ICOMMAND(0, getbind,     "s", (char *key), getbind(key, keym::ACTION_DEFAULT));
 ICOMMAND(0, getspecbind, "s", (char *key), getbind(key, keym::ACTION_SPECTATOR));
 ICOMMAND(0, geteditbind, "s", (char *key), getbind(key, keym::ACTION_EDITING));
 ICOMMAND(0, getwaitbind, "s", (char *key), getbind(key, keym::ACTION_WAITING));
-ICOMMAND(0, searchbinds,     "siss", (char *action, int *limit, char *sep, char *pretty), { vector<char> list; searchbindlist(action, keym::ACTION_DEFAULT, max(*limit, 0), sep, pretty, list); result(list.getbuf()); });
-ICOMMAND(0, searchspecbinds, "siss", (char *action, int *limit, char *sep, char *pretty), { vector<char> list; searchbindlist(action, keym::ACTION_SPECTATOR, max(*limit, 0), sep, pretty, list); result(list.getbuf()); });
-ICOMMAND(0, searcheditbinds, "siss", (char *action, int *limit, char *sep, char *pretty), { vector<char> list; searchbindlist(action, keym::ACTION_EDITING, max(*limit, 0), sep, pretty, list); result(list.getbuf()); });
-ICOMMAND(0, searchwaitbinds, "siss", (char *action, int *limit, char *sep, char *pretty), { vector<char> list; searchbindlist(action, keym::ACTION_WAITING, max(*limit, 0), sep, pretty, list); result(list.getbuf()); });
+ICOMMAND(0, searchbinds,     "sissssb", (char *action, int *limit, char *s1, char *s2, char *sep1, char *sep2, int *force), { vector<char> list; searchbindlist(action, keym::ACTION_DEFAULT, max(*limit, 0), s1, s2, sep1, sep2, list, *force!=0); result(list.getbuf()); });
+ICOMMAND(0, searchspecbinds, "sissssb", (char *action, int *limit, char *s1, char *s2, char *sep1, char *sep2, int *force), { vector<char> list; searchbindlist(action, keym::ACTION_SPECTATOR, max(*limit, 0), s1, s2, sep1, sep2, list, *force!=0); result(list.getbuf()); });
+ICOMMAND(0, searcheditbinds, "sissssb", (char *action, int *limit, char *s1, char *s2, char *sep1, char *sep2, int *force), { vector<char> list; searchbindlist(action, keym::ACTION_EDITING, max(*limit, 0), s1, s2, sep1, sep2, list, *force!=0); result(list.getbuf()); });
+ICOMMAND(0, searchwaitbinds, "sissssb", (char *action, int *limit, char *s1, char *s2, char *sep1, char *sep2, int *force), { vector<char> list; searchbindlist(action, keym::ACTION_WAITING, max(*limit, 0), s1, s2, sep1, sep2, list, *force!=0); result(list.getbuf()); });
 
-ICOMMAND(0, keyspressed, "iss", (int *limit, char *sep, char *pretty), { vector<char> list; getkeypressed(max(*limit, 0), sep, pretty, list); result(list.getbuf()); });
+ICOMMAND(0, keyspressed, "issss", (int *limit, char *s1, char *s2, char *sep1, char *sep2), { vector<char> list; getkeypressed(max(*limit, 0), s1, s2, sep1, sep2, list); result(list.getbuf()); });
 
 void inputcommand(char *init, char *action = NULL, char *icon = NULL, int colour = 0, char *flags = NULL) // turns input to the command line on or off
 {
     commandmillis = init ? totalmillis : -totalmillis;
     SDL_EnableUNICODE(commandmillis > 0 ? 1 : 0);
     keyrepeat(commandmillis > 0);
-    copystring(commandbuf, init ? init : "");
+    copybigstring(commandbuf, init ? init : "");
     DELETEA(commandaction);
     DELETEA(commandicon);
     commandpos = -1;
@@ -295,7 +303,6 @@ ICOMMAND(0, inputcommand, "sssis", (char *init, char *action, char *icon, int *c
 #if !defined(WIN32) && !defined(__APPLE__)
 #include <X11/Xlib.h>
 #endif
-
 void pasteconsole()
 {
 #ifdef WIN32
@@ -307,17 +314,16 @@ void pasteconsole()
     }
     if(!OpenClipboard(NULL)) return;
     HANDLE h = GetClipboardData(fmt);
-    size_t commandlen = strlen(commandbuf);
-    int cblen = int(GlobalSize(h)), decoded = 0;
+    size_t commandlen = strlen(commandbuf), cblen = GlobalSize(h), decoded = 0;
     ushort *cb = (ushort *)GlobalLock(h);
     switch(fmt)
     {
         case CF_UNICODETEXT:
-            decoded = min(int(sizeof(commandbuf)-1-commandlen), cblen/2);
+            decoded = min(sizeof(commandbuf)-1-commandlen, cblen/2);
             loopi(decoded) commandbuf[commandlen++] = uni2cube(cb[i]);
             break;
         case CF_TEXT:
-            decoded = min(int(sizeof(commandbuf)-1-commandlen), cblen);
+            decoded = min(sizeof(commandbuf)-1-commandlen, cblen);
             memcpy(&commandbuf[commandlen], cb, decoded);
             break;
     }
@@ -325,28 +331,28 @@ void pasteconsole()
     GlobalUnlock(cb);
     CloseClipboard();
 #elif defined(__APPLE__)
-    extern char *mac_pasteconsole(int *cblen);
-    int cblen = 0;
+    extern char *mac_pasteconsole(size_t *cblen);
+    size_t cblen = 0;
     uchar *cb = (uchar *)mac_pasteconsole(&cblen);
     if(!cb) return;
     size_t commandlen = strlen(commandbuf);
-    int decoded = decodeutf8((uchar *)&commandbuf[commandlen], int(sizeof(commandbuf)-1-commandlen), cb, cblen);
+           decoded = decodeutf8((uchar *)&commandbuf[commandlen], sizeof(commandbuf)-1-commandlen, cb, cblen);
     commandbuf[commandlen + decoded] = '\0';
     free(cb);
-    #else
+#else
     SDL_SysWMinfo wminfo;
     SDL_VERSION(&wminfo.version);
     wminfo.subsystem = SDL_SYSWM_X11;
     if(!SDL_GetWMInfo(&wminfo)) return;
     int cbsize;
     uchar *cb = (uchar *)XFetchBytes(wminfo.info.x11.display, &cbsize);
-    if(!cb || !cbsize) return;
+    if(!cb || cbsize <= 0) return;
     size_t commandlen = strlen(commandbuf);
     for(uchar *cbline = cb, *cbend; commandlen + 1 < sizeof(commandbuf) && cbline < &cb[cbsize]; cbline = cbend + 1)
     {
         cbend = (uchar *)memchr(cbline, '\0', &cb[cbsize] - cbline);
         if(!cbend) cbend = &cb[cbsize];
-        int cblen = int(cbend-cbline), commandmax = int(sizeof(commandbuf)-1-commandlen);
+        size_t cblen = cbend-cbline, commandmax = sizeof(commandbuf)-1-commandlen;
         loopi(cblen) if((cbline[i]&0xC0) == 0x80)
         {
             commandlen += decodeutf8((uchar *)&commandbuf[commandlen], commandmax, cbline, cblen);
@@ -380,7 +386,7 @@ struct hline
 
     void restore()
     {
-        copystring(commandbuf, buf);
+        copybigstring(commandbuf, buf);
         if(commandpos >= (int)strlen(commandbuf)) commandpos = -1;
         DELETEA(commandaction);
         DELETEA(commandicon);
@@ -446,7 +452,7 @@ vector<releaseaction> releaseactions;
 
 const char *addreleaseaction(char *s)
 {
-    if(!keypressed) return NULL;
+    if(!keypressed) { delete[] s; return NULL; }
     releaseaction &ra = releaseactions.add();
     ra.key = keypressed;
     ra.action = s;
@@ -661,6 +667,12 @@ void keypress(int code, bool isdown, int cooked)
         case SDLK_TAB:
             keyintercept(iconify, SDL_WM_IconifyWindow());
             break;
+        case SDLK_CAPSLOCK:
+            if(!isdown) capslockon = capslocked();
+            break;
+        case SDLK_NUMLOCK:
+            if(!isdown) numlockon = numlocked();
+            break;
         default: break;
     }
     keym *haskey = keyms.access(code);
@@ -686,7 +698,7 @@ static inline bool sortbinds(keym *x, keym *y)
 
 void writebinds(stream *f)
 {
-    static const char *cmds[4] = { "bind", "specbind", "editbind", "waitbind" };
+    static const char * const cmds[4] = { "bind", "specbind", "editbind", "waitbind" };
     vector<keym *> binds;
     enumerate(keyms, keym, km, binds.add(&km));
     binds.sort(sortbinds);
@@ -758,7 +770,7 @@ static hashtable<fileskey, filesval *> completefiles;
 static hashtable<char *, filesval *> completions;
 
 int completeoffset = -1, completesize = 0;
-string lastcomplete;
+bigstring lastcomplete;
 
 void resetcomplete() { completesize = 0; }
 
@@ -820,8 +832,8 @@ void complete(char *s, const char *cmdprefix)
         int cmdlen = strlen(cmdprefix);
         if(strncmp(s, cmdprefix, cmdlen))
         {
-            defformatstring(cmd)("%s%s", cmdprefix, start);
-            copystring(s, cmd);
+            defformatbigstring(cmd)("%s%s", cmdprefix, start);
+            copybigstring(s, cmd);
         }
         start = &s[cmdlen];
     }
@@ -850,18 +862,18 @@ void complete(char *s, const char *cmdprefix)
         char *end = strchr(start, ' ');
         if(end)
         {
-            string command;
-            copystring(command, start, min(size_t(end-start+1), sizeof(command)));
+            bigstring command;
+            copybigstring(command, start, min(size_t(end-start+1), sizeof(command)));
             filesval **hasfiles = completions.access(command);
             if(hasfiles) f = *hasfiles;
         }
     }
     const char *nextcomplete = NULL;
-    string prefix;
+    bigstring prefix;
     if(f) // complete using filenames
     {
         int commandsize = strchr(start, ' ')+1-start;
-        copystring(prefix, s, min(size_t(commandsize+1+(start-s)), sizeof(prefix)));
+        copybigstring(prefix, s, min(size_t(commandsize+1+(start-s)), sizeof(prefix)));
         f->update();
         loopv(f->files)
         {
@@ -872,7 +884,7 @@ void complete(char *s, const char *cmdprefix)
     }
     else // complete using command names
     {
-        copystring(prefix, s, min(size_t(1+(start-s)), sizeof(prefix)));
+        copybigstring(prefix, s, min(size_t(1+(start-s)), sizeof(prefix)));
         enumerate(idents, ident, id,
             if((variable ? id.type == ID_VAR || id.type == ID_SVAR || id.type == ID_FVAR || id.type == ID_ALIAS: id.flags&IDF_COMPLETE) && strncmp(id.name, start, completesize)==0 &&
                strcmp(id.name, lastcomplete) > 0 && (!nextcomplete || strcmp(id.name, nextcomplete) < 0))
@@ -881,8 +893,8 @@ void complete(char *s, const char *cmdprefix)
     }
     if(nextcomplete)
     {
-        formatstring(s)("%s%s", prefix, nextcomplete);
-        copystring(lastcomplete, nextcomplete);
+        formatbigstring(s)("%s%s", prefix, nextcomplete);
+        copybigstring(lastcomplete, nextcomplete);
     }
     else
     {
@@ -950,3 +962,46 @@ void writecompletions(stream *f)
     }
 }
 
+bool capslockon = false, numlockon = false;
+#if !defined(WIN32) && !defined(__APPLE__)
+#include <X11/XKBlib.h>
+#endif
+bool capslocked()
+{
+    #ifdef WIN32
+    if(GetKeyState(VK_CAPITAL)) return true;
+    #elif defined(__APPLE__)
+    if(mac_capslock()) return true;
+    #else
+    Display *d = XOpenDisplay((char*)0);
+    if(d)
+    {
+        uint n = 0;
+        XkbGetIndicatorState(d, XkbUseCoreKbd, &n);
+        XCloseDisplay(d);
+        return (n&0x01)!=0;
+    }
+    #endif
+    return false;
+}
+ICOMMAND(0, getcapslock, "", (), intret(capslockon ? 1 : 0));
+
+bool numlocked()
+{
+    #ifdef WIN32
+    if(GetKeyState(VK_NUMLOCK)) return true;
+    #elif defined(__APPLE__)
+    if(mac_numlock()) return true;
+    #else
+    Display *d = XOpenDisplay((char*)0);
+    if(d)
+    {
+        uint n = 0;
+        XkbGetIndicatorState(d, XkbUseCoreKbd, &n);
+        XCloseDisplay(d);
+        return (n&0x02)!=0;
+    }
+    #endif
+    return false;
+}
+ICOMMAND(0, getnumlock, "", (), intret(numlockon ? 1 : 0));
diff --git a/src/engine/decal.cpp b/src/engine/decal.cpp
index 84d65e3..81744b3 100644
--- a/src/engine/decal.cpp
+++ b/src/engine/decal.cpp
@@ -4,8 +4,7 @@ struct decalvert
 {
     vec pos;
     float u, v;
-    bvec color;
-    uchar alpha;
+    bvec4 color;
 };
 
 struct decalinfo
@@ -37,14 +36,14 @@ struct decalrenderer
     decalinfo *decals;
     int maxdecals, startdecal, enddecal;
     decalvert *verts;
-    int maxverts, startvert, endvert, availverts;
+    int maxverts, startvert, endvert, lastvert, availverts;
 
     decalrenderer(const char *texname, int flags = 0, int fadeintime = 0, int fadeouttime = 1000, int timetolive = -1)
         : texname(texname), flags(flags),
           fadeintime(fadeintime), fadeouttime(fadeouttime), timetolive(timetolive),
           tex(NULL),
           decals(NULL), maxdecals(0), startdecal(0), enddecal(0),
-          verts(NULL), maxverts(0), startvert(0), endvert(0), availverts(0),
+          verts(NULL), maxverts(0), startvert(0), endvert(0), lastvert(0), availverts(0),
           decalu(0), decalv(0)
     {
     }
@@ -59,7 +58,7 @@ struct decalrenderer
         if(verts)
         {
             DELETEA(verts);
-            maxverts = startvert = endvert = availverts = 0;
+            maxverts = startvert = endvert = lastvert = availverts = 0;
         }
         decals = new decalinfo[tris];
         maxdecals = tris;
@@ -77,7 +76,7 @@ struct decalrenderer
     void cleardecals()
     {
         startdecal = enddecal = 0;
-        startvert = endvert = 0;
+        startvert = endvert = lastvert = 0;
         availverts = maxverts - 3;
     }
 
@@ -91,7 +90,7 @@ struct decalrenderer
 
         int removed = d.endvert < d.startvert ? maxverts - (d.startvert - d.endvert) : d.endvert - d.startvert;
         startvert = d.endvert;
-        if(startvert==endvert) startvert = endvert = 0;
+        if(startvert==endvert) startvert = endvert = lastvert = 0;
         availverts += removed;
 
         return removed;
@@ -99,24 +98,24 @@ struct decalrenderer
 
     void fadedecal(decalinfo &d, uchar alpha)
     {
-        bvec color;
+        bvec rgb;
         if(flags&DF_OVERBRIGHT)
         {
-            if(renderpath!=R_FIXEDFUNCTION || hasTE) color = bvec(128, 128, 128);
-            else color = bvec(alpha, alpha, alpha);
+            if(renderpath!=R_FIXEDFUNCTION || hasTE) rgb = bvec(128, 128, 128);
+            else rgb = bvec(alpha, alpha, alpha);
         }
         else
         {
-            color = d.color;
-            if(flags&(DF_ADD|DF_INVMOD)) loopk(3) color[k] = uchar((int(color[k])*int(alpha))>>8);
+            rgb = d.color;
+            if(flags&(DF_ADD|DF_INVMOD)) rgb.scale(alpha, 255);
         }
+        bvec4 color(rgb, alpha);
 
         decalvert *vert = &verts[d.startvert],
                   *end = &verts[d.endvert < d.startvert ? maxverts : d.endvert];
         while(vert < end)
         {
             vert->color = color;
-            vert->alpha = alpha;
             vert++;
         }
         if(d.endvert < d.startvert)
@@ -126,7 +125,6 @@ struct decalrenderer
             while(vert < end)
             {
                 vert->color = color;
-                vert->alpha = alpha;
                 vert++;
             }
         }
@@ -146,7 +144,7 @@ struct decalrenderer
         }
         startdecal = d - decals;
         if(startdecal!=enddecal) startvert = decals[startdecal].startvert;
-        else startvert = endvert = 0;
+        else startvert = endvert = lastvert = 0;
         availverts = endvert < startvert ? startvert - endvert - 3 : maxverts - 3 - (endvert - startvert);
     }
 
@@ -298,18 +296,18 @@ struct decalrenderer
         return d;
     }
 
-    ivec bborigin, bbsize;
+    ivec bbmin, bbmax;
     vec decalcenter, decalnormal, decaltangent, decalbitangent;
     float decalradius, decalu, decalv;
-    bvec decalcolor;
+    bvec4 decalcolor;
 
     void adddecal(const vec &center, const vec &dir, float radius, const bvec &color, int info)
     {
-        int isz = int(ceil(radius));
-        bborigin = ivec(center).sub(isz);
-        bbsize = ivec(isz*2, isz*2, isz*2);
+        int bbradius = int(ceil(radius));
+        bbmin = ivec(center).sub(bbradius);
+        bbmax = ivec(center).add(bbradius);
 
-        decalcolor = color;
+        decalcolor = bvec4(color, 255);
         decalcenter = center;
         decalradius = radius;
         decalnormal = dir;
@@ -328,19 +326,19 @@ struct decalrenderer
             decalv = 0.5f*((info>>1)&1);
         }
 
-        ushort dstart = endvert;
+        lastvert = endvert;
         gentris(worldroot, ivec(0, 0, 0), hdr.worldsize>>1);
         if(dbgdec)
         {
-            int nverts = endvert < dstart ? endvert + maxverts - dstart : endvert - dstart;
+            int nverts = endvert < lastvert ? endvert + maxverts - lastvert : endvert - lastvert;
             conoutf("\fatris = %d, verts = %d, total tris = %d", nverts/3, nverts, (maxverts - 3 - availverts)/3);
         }
-        if(endvert==dstart) return;
+        if(endvert==lastvert) return;
 
         decalinfo &d = newdecal();
         d.color = color;
         d.millis = lastmillis;
-        d.startvert = dstart;
+        d.startvert = lastvert;
         d.endvert = endvert;
     }
 
@@ -473,8 +471,8 @@ struct decalrenderer
             float tsz = flags&DF_RND4 ? 0.5f : 1.0f, scale = tsz*0.5f/decalradius,
                   tu = decalu + tsz*0.5f - ptc*scale, tv = decalv + tsz*0.5f - pbc*scale;
             pt.mul(scale); pb.mul(scale);
-            decalvert dv1 = { v2[0], pt.dot(v2[0]) + tu, pb.dot(v2[0]) + tv, decalcolor, 255 },
-                      dv2 = { v2[1], pt.dot(v2[1]) + tu, pb.dot(v2[1]) + tv, decalcolor, 255 };
+            decalvert dv1 = { v2[0], pt.dot(v2[0]) + tu, pb.dot(v2[0]) + tv, decalcolor },
+                      dv2 = { v2[1], pt.dot(v2[1]) + tu, pb.dot(v2[1]) + tv, decalcolor };
             int totalverts = 3*(numv-2);
             if(totalverts > maxverts-3) return;
             while(availverts < totalverts)
@@ -509,9 +507,9 @@ struct decalrenderer
             for(;;)
             {
                 materialsurface &m = matbuf[i];
-                if(m.o[dim] >= bborigin[dim] && m.o[dim] <= bborigin[dim] + bbsize[dim] &&
-                   m.o[c] + m.csize >= bborigin[c] && m.o[c] <= bborigin[c] + bbsize[c] &&
-                   m.o[r] + m.rsize >= bborigin[r] && m.o[r] <= bborigin[r] + bbsize[r])
+                if(m.o[dim] >= bbmin[dim] && m.o[dim] <= bbmax[dim] &&
+                   m.o[c] + m.csize >= bbmin[c] && m.o[c] <= bbmax[c] &&
+                   m.o[r] + m.rsize >= bbmin[r] && m.o[r] <= bbmax[r])
                 {
                     static cube dummy;
                     gentris(dummy, m.orient, m.o, max(m.csize, m.rsize), &m);
@@ -543,7 +541,7 @@ struct decalrenderer
 
     void gentris(cube *cu, const ivec &o, int size, int escaped = 0)
     {
-        int overlap = octantrectangleoverlap(o, size, bborigin, bbsize);
+        int overlap = octaboxoverlap(o, size, bbmin, bbmax);
         loopi(8)
         {
             if(overlap&(1<<i))
diff --git a/src/engine/dynlight.cpp b/src/engine/dynlight.cpp
index 371379a..13c021c 100644
--- a/src/engine/dynlight.cpp
+++ b/src/engine/dynlight.cpp
@@ -99,13 +99,12 @@ int finddynlights()
     if(renderpath==R_FIXEDFUNCTION ? !ffdynlights || maxtmus<3 : !maxdynlights) return 0;
     physent e;
     e.type = ENT_CAMERA;
-    e.collidetype = COLLIDE_AABB;
     loopvj(dynlights)
     {
         dynlight &d = dynlights[j];
         if(d.curradius <= 0) continue;
         d.dist = camera1->o.dist(d.o) - d.curradius;
-        if(d.dist > dynlightdist || isfoggedsphere(d.curradius, d.o) || pvsoccluded(d.o, 2*int(d.curradius+1)))
+        if(d.dist > dynlightdist || isfoggedsphere(d.curradius, d.o) || pvsoccludedsphere(d.o, d.curradius))
             continue;
         if(reflecting || refracting > 0)
         {
@@ -114,7 +113,7 @@ int finddynlights()
         else if(refracting < 0 && d.o.z - d.curradius > reflectz) continue;
         e.o = d.o;
         e.radius = e.xradius = e.yradius = e.height = e.aboveeye = d.curradius;
-        if(collide(&e, vec(0, 0, 0), 0, false)) continue;
+        if(!collide(&e, vec(0, 0, 0), 0, false)) continue;
 
         int insert = 0;
         loopvrev(closedynlights) if(d.dist >= closedynlights[i]->dist && (!(d.flags&DL_KEEP) || (closedynlights[i]->flags&DL_KEEP))) { insert = i+1; break; }
diff --git a/src/engine/engine.h b/src/engine/engine.h
index 4068fb9..6ee79cd 100644
--- a/src/engine/engine.h
+++ b/src/engine/engine.h
@@ -1,27 +1,38 @@
 #ifndef __ENGINE_H__
 #define __ENGINE_H__
 
+#include "version.h"
 #include "cube.h"
 
-#define LAN_PORT         28799
-#define MASTER_PORT      28800
-#define SERVER_PORT      28801
+#define LAN_PORT 28799
+#define MASTER_PORT 28800
+#define SERVER_PORT 28801
 
-extern bool versioning;
-extern int versionmajor, versionminor, versionpatch;
-extern char *versionstring, *versionname, *versionuname, *versionrelease, *versionurl, *versionmaster;
-#define CUR_VER_MAKE(a,b,c)  (((a)<<16) | ((b)<<8) | (c))
-#define CUR_VER              CUR_VER_MAKE(versionmajor, versionminor, versionpatch)
-#define CUR_VERSION          (versionmajor*100)+(versionminor*10)+versionpatch
+extern int versioning, versionmajor, versionminor, versionpatch, versionplatform, versionarch, versionisserver;
+extern char *versionstring, *versionname, *versionuname, *versionrelease, *versionurl, *versionmaster, *versionplatname, *versionplatlongname;
+extern uint versioncrc;
+#define CUR_VER_MAKE(a,b,c) (((a)<<16) | ((b)<<8) | (c))
+#define CUR_VER CUR_VER_MAKE(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
+#define CUR_VERSION (VERSION_MAJOR*100)+(VERSION_MINOR*10)+VERSION_PATCH
 
 #ifdef WIN32
-#define CUR_PLATFORM         "win"
+#define CUR_PLATFORM 0
+#define CUR_PLATID
 #elif defined(__APPLE__)
-#define CUR_PLATFORM         "mac"
+#define CUR_PLATFORM 1
 #else
-#define CUR_PLATFORM         "nix"
+#define CUR_PLATFORM 2
 #endif
-#define CUR_ARCH             (int(8*sizeof(void *)))
+#define CUR_ARCH (int(8*sizeof(void *)))
+
+#define MAX_PLATFORMS 3
+
+#define sup_platform(a) (a >= 0 && a < MAX_PLATFORMS)
+#define sup_arch(a) (a == 32 || a == 64)
+
+extern const char *platnames[MAX_PLATFORMS], *platlongnames[MAX_PLATFORMS];
+#define plat_name(a) (sup_platform(a) ? platnames[a] : "unk")
+#define plat_longname(a) (sup_platform(a) ? platlongnames[a] : "unknown")
 
 #ifdef STANDALONE
 extern void setupmaster();
@@ -33,6 +44,8 @@ extern char *masterip;
 extern int nextcontrolversion();
 #endif
 
+extern void setcrc(const char *bin);
+
 #include "irc.h"
 #include "sound.h"
 
@@ -42,128 +55,34 @@ enum { PACKAGEDIR_OCTA = 1<<0 };
 extern const char * const disc_reasons[];
 struct ipinfo
 {
-    enum { ALLOW = 0, BAN, MUTE, LIMIT, MAXTYPES };
+    enum { ALLOW = 0, BAN, MUTE, LIMIT, EXCEPT, MAXTYPES };
     enum { TEMPORARY = 0, LOCAL, GLOBAL };
     enet_uint32 ip, mask;
     int type, flag, time, version;
+    char *reason;
 
-    ipinfo() : ip(0), mask(0), type(-1), flag(TEMPORARY), time(-1), version(-1) {}
-    ~ipinfo() {}
+    ipinfo() : ip(0), mask(0), type(-1), flag(TEMPORARY), time(-1), version(-1), reason(NULL) {}
+    ~ipinfo()
+    {
+        if(reason) delete[] reason;
+    }
 };
 extern vector<ipinfo> control;
 extern const char *ipinfotypes[ipinfo::MAXTYPES];
-extern void addipinfo(vector<ipinfo> &info, int type, const char *name);
+extern void addipinfo(vector<ipinfo> &info, int type, const char *name, const char *reason = NULL);
 extern char *printipinfo(const ipinfo &info, char *buf = NULL);
-extern bool checkipinfo(vector<ipinfo> &info, int type, enet_uint32 ip);
+extern ipinfo *checkipinfo(vector<ipinfo> &info, int type, enet_uint32 ip);
 extern void writecfg();
 extern void rehash(bool reload = true);
 
-#define NZT(t) (t != 0 ? t : 1)
-
 #ifndef STANDALONE
 #include "world.h"
+#include "glexts.h"
 #include "octa.h"
 #include "lightmap.h"
 #include "bih.h"
 #include "texture.h"
 #include "model.h"
-
-// GL_ARB_multitexture
-extern PFNGLACTIVETEXTUREARBPROC        glActiveTexture_;
-extern PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTexture_;
-extern PFNGLMULTITEXCOORD2FARBPROC   glMultiTexCoord2f_;
-extern PFNGLMULTITEXCOORD3FARBPROC   glMultiTexCoord3f_;
-extern PFNGLMULTITEXCOORD4FARBPROC   glMultiTexCoord4f_;
-
-// GL_ARB_vertex_buffer_object
-extern PFNGLGENBUFFERSARBPROC       glGenBuffers_;
-extern PFNGLBINDBUFFERARBPROC       glBindBuffer_;
-extern PFNGLMAPBUFFERARBPROC        glMapBuffer_;
-extern PFNGLUNMAPBUFFERARBPROC      glUnmapBuffer_;
-extern PFNGLBUFFERDATAARBPROC       glBufferData_;
-extern PFNGLBUFFERSUBDATAARBPROC    glBufferSubData_;
-extern PFNGLDELETEBUFFERSARBPROC    glDeleteBuffers_;
-extern PFNGLGETBUFFERSUBDATAARBPROC glGetBufferSubData_;
-
-// GL_ARB_occlusion_query
-extern PFNGLGENQUERIESARBPROC       glGenQueries_;
-extern PFNGLDELETEQUERIESARBPROC     glDeleteQueries_;
-extern PFNGLBEGINQUERYARBPROC       glBeginQuery_;
-extern PFNGLENDQUERYARBPROC       glEndQuery_;
-extern PFNGLGETQUERYIVARBPROC       glGetQueryiv_;
-extern PFNGLGETQUERYOBJECTIVARBPROC  glGetQueryObjectiv_;
-extern PFNGLGETQUERYOBJECTUIVARBPROC glGetQueryObjectuiv_;
-
-// GL_EXT_framebuffer_object
-extern PFNGLBINDRENDERBUFFEREXTPROC     glBindRenderbuffer_;
-extern PFNGLDELETERENDERBUFFERSEXTPROC   glDeleteRenderbuffers_;
-extern PFNGLGENFRAMEBUFFERSEXTPROC       glGenRenderbuffers_;
-extern PFNGLRENDERBUFFERSTORAGEEXTPROC   glRenderbufferStorage_;
-extern PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC  glCheckFramebufferStatus_;
-extern PFNGLBINDFRAMEBUFFEREXTPROC       glBindFramebuffer_;
-extern PFNGLDELETEFRAMEBUFFERSEXTPROC     glDeleteFramebuffers_;
-extern PFNGLGENFRAMEBUFFERSEXTPROC       glGenFramebuffers_;
-extern PFNGLFRAMEBUFFERTEXTURE2DEXTPROC glFramebufferTexture2D_;
-extern PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC glFramebufferRenderbuffer_;
-extern PFNGLGENERATEMIPMAPEXTPROC         glGenerateMipmap_;
-
-// GL_EXT_framebuffer_blit
-#ifndef GL_EXT_framebuffer_blit
-#define GL_READ_FRAMEBUFFER_EXT           0x8CA8
-#define GL_DRAW_FRAMEBUFFER_EXT           0x8CA9
-#define GL_DRAW_FRAMEBUFFER_BINDING_EXT   0x8CA6
-#define GL_READ_FRAMEBUFFER_BINDING_EXT   0x8CAA
-typedef void (APIENTRYP PFNGLBLITFRAMEBUFFEREXTPROC) (GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
-#endif
-extern PFNGLBLITFRAMEBUFFEREXTPROC         glBlitFramebuffer_;
-
-// GL_EXT_draw_range_elements
-extern PFNGLDRAWRANGEELEMENTSEXTPROC glDrawRangeElements_;
-
-// GL_EXT_blend_minmax
-extern PFNGLBLENDEQUATIONEXTPROC glBlendEquation_;
-
-// GL_EXT_blend_color
-extern PFNGLBLENDCOLOREXTPROC glBlendColor_;
-
-// GL_EXT_multi_draw_arrays
-extern PFNGLMULTIDRAWARRAYSEXTPROC   glMultiDrawArrays_;
-extern PFNGLMULTIDRAWELEMENTSEXTPROC glMultiDrawElements_;
-
-// GL_EXT_packed_depth_stencil
-#ifndef GL_DEPTH_STENCIL_EXT
-#define GL_DEPTH_STENCIL_EXT 0x84F9
-#endif
-#ifndef GL_DEPTH24_STENCIL8_EXT
-#define GL_DEPTH24_STENCIL8_EXT 0x88F0
-#endif
-
-// GL_ARB_texture_compression
-extern PFNGLCOMPRESSEDTEXIMAGE3DARBPROC    glCompressedTexImage3D_;
-extern PFNGLCOMPRESSEDTEXIMAGE2DARBPROC    glCompressedTexImage2D_;
-extern PFNGLCOMPRESSEDTEXIMAGE1DARBPROC    glCompressedTexImage1D_;
-extern PFNGLCOMPRESSEDTEXSUBIMAGE3DARBPROC glCompressedTexSubImage3D_;
-extern PFNGLCOMPRESSEDTEXSUBIMAGE2DARBPROC glCompressedTexSubImage2D_;
-extern PFNGLCOMPRESSEDTEXSUBIMAGE1DARBPROC glCompressedTexSubImage1D_;
-extern PFNGLGETCOMPRESSEDTEXIMAGEARBPROC   glGetCompressedTexImage_;
-
-// GL_EXT_fog_coord
-extern PFNGLFOGCOORDPOINTEREXTPROC glFogCoordPointer_;
-
-// GL_ARB_map_buffer_range
-#ifndef GL_ARB_map_buffer_range
-#define GL_MAP_READ_BIT                   0x0001
-#define GL_MAP_WRITE_BIT                  0x0002
-#define GL_MAP_INVALIDATE_RANGE_BIT       0x0004
-#define GL_MAP_INVALIDATE_BUFFER_BIT      0x0008
-#define GL_MAP_FLUSH_EXPLICIT_BIT         0x0010
-#define GL_MAP_UNSYNCHRONIZED_BIT         0x0020
-typedef GLvoid* (APIENTRYP PFNGLMAPBUFFERRANGEPROC) (GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
-typedef void (APIENTRYP PFNGLFLUSHMAPPEDBUFFERRANGEPROC) (GLenum target, GLintptr offset, GLsizeiptr length);
-#endif
-extern PFNGLMAPBUFFERRANGEPROC         glMapBufferRange_;
-extern PFNGLFLUSHMAPPEDBUFFERRANGEPROC glFlushMappedBufferRange_;
-
 #include "varray.h"
 
 extern physent *camera1, camera;
@@ -206,7 +125,7 @@ extern float textscale;
 extern font *curfont;
 
 // texture
-extern int hwtexsize, hwcubetexsize, hwmaxaniso, maxtexsize, aniso, envmapradius;
+extern int hwtexsize, hwcubetexsize, hwmaxanisotropy, maxtexsize, anisotropy, envmapradius;
 
 extern Texture *textureload(const char *name, int clamp = 0, bool mipit = true, bool msg = true);
 extern int texalign(void *data, int w, int bpp);
@@ -258,7 +177,8 @@ extern void guessshadowdir();
 
 // pvs
 extern void clearpvs();
-extern bool pvsoccluded(const ivec &bborigin, const ivec &bbsize);
+extern bool pvsoccluded(const ivec &bbmin, const ivec &bbmax);
+extern bool pvsoccludedsphere(const vec &center, float radius);
 extern bool waterpvsoccluded(int height);
 extern void setviewcell(const vec &p);
 extern void savepvs(stream *f);
@@ -267,13 +187,14 @@ extern int getnumviewcells();
 
 static inline bool pvsoccluded(const ivec &bborigin, int size)
 {
-    return pvsoccluded(bborigin, ivec(size, size, size));
+    return pvsoccluded(bborigin, ivec(bborigin).add(size));
 }
 
 // rendergl
 extern bool hasVBO, hasDRE, hasOQ, hasTR, hasFBO, hasDS, hasTF, hasBE, hasBC, hasCM, hasNP2, hasTC, hasS3TC, hasFXT1, hasTE, hasMT, hasD3, hasAF, hasVP2, hasVP3, hasPP, hasMDA, hasTE3, hasTE4, hasVP, hasFP, hasGLSL, hasGM, hasNVFB, hasSGIDT, hasSGISH, hasDT, hasSH, hasNVPCF, hasRN, hasPBO, hasFBB, hasUBO, hasBUE, hasMBR, hasFC, hasTEX;
 extern int hasstencil;
 extern int glversion, glslversion;
+extern char *gfxvendor, *gfxexts, *gfxrenderer, *gfxversion;
 
 extern bool envmapping, minimapping, renderedgame, modelpreviewing;
 extern const glmatrixf viewmatrix;
@@ -380,28 +301,14 @@ extern void calcmerges();
 extern int mergefaces(int orient, facebounds *m, int sz);
 extern void mincubeface(const cube &cu, int orient, const ivec &o, int size, const facebounds &orig, facebounds &cf, ushort nmat = MAT_AIR, ushort matmask = MATF_VOLUME);
 
-static inline uchar octantrectangleoverlap(const ivec &c, int size, const ivec &o, const ivec &s)
+static inline bool insideworld(const vec &o, bool zup = true)
 {
-    uchar p = 0xFF; // bitmask of possible collisions with octants. 0 bit = 0 octant, etc
-    ivec v(c);
-    v.add(size);
-    if(v.z <= o.z)     p &= 0xF0; // not in a -ve Z octant
-    else if(v.z >= o.z+s.z) p &= 0x0F; // not in a +ve Z octant
-    if(v.y <= o.y)     p &= 0xCC; // not in a -ve Y octant
-    else if(v.y >= o.y+s.y) p &= 0x33; // etc..
-    if(v.x <= o.x)     p &= 0xAA;
-    else if(v.x >= o.x+s.x) p &= 0x55;
-    return p;
+    return o.x>=0 && o.x<hdr.worldsize && o.y>=0 && o.y<hdr.worldsize && o.z>=0 && (!zup || o.z<hdr.worldsize);
 }
 
-static inline bool insideworld(const vec &o)
+static inline bool insideworld(const ivec &o, bool zup = true)
 {
-    return o.x>=0 && o.x<hdr.worldsize && o.y>=0 && o.y<hdr.worldsize && o.z>=0 && o.z<hdr.worldsize;
-}
-
-static inline bool insideworld(const ivec &o)
-{
-    return uint(o.x)<uint(hdr.worldsize) && uint(o.y)<uint(hdr.worldsize) && uint(o.z)<uint(hdr.worldsize);
+    return uint(o.x)<uint(hdr.worldsize) && uint(o.y)<uint(hdr.worldsize) && (!zup || uint(o.z)<uint(hdr.worldsize));
 }
 
 static inline cubeext &ext(cube &c)
@@ -555,7 +462,7 @@ extern void localconnect(bool force = true);
 extern void localdisconnect();
 
 // serverbrowser
-extern void addserver(const char *name, int port, const char *desc = NULL);
+extern void addserver(const char *name, int port, int priority = 0, const char *desc = NULL);
 
 // client
 extern char *connectname;
@@ -586,7 +493,7 @@ extern const char *getkeyname(int code);
 extern int findkeycode(char *key);
 
 extern int uimillis, commandmillis,  commandpos, commandcolour, completeoffset, completesize;
-extern string commandbuf;
+extern bigstring commandbuf;
 extern char *commandaction, *commandicon;
 extern bool fullconsole;
 // main
@@ -600,7 +507,7 @@ enum
 };
 extern int initing, fullscreen, numcpus;
 void setfullscreen(bool enable, bool force = false);
-extern bool progressing;
+extern bool progressing, pixeling;
 extern float loadprogress, progresspart, progressamt;
 extern char *progresstitle, *progresstext;
 extern void progress(float bar1 = 0, const char *text1 = NULL, float bar2 = 0, const char *text2 = NULL);
@@ -612,13 +519,16 @@ enum
     CHANGE_SOUND = 1<<1
 };
 extern bool initwarning(const char *desc, int level = INIT_RESET, int type = CHANGE_GFX, bool force = false);
+
+extern bool minimized;
+
 extern void resetcursor(bool warp = true, bool reset = true);
 extern int compresslevel, imageformat;
 
 extern void pushevent(const SDL_Event &e);
 extern bool interceptkey(int sym, int mod = 0);
 extern void getfps(int &fps, int &bestdiff, int &worstdiff);
-extern void swapbuffers();
+extern void swapbuffers(bool overlay = true);
 extern int getclockmillis();
 
 // menu
@@ -653,7 +563,7 @@ extern void entcancel();
 extern void entitiesinoctanodes();
 extern void attachentities();
 extern void freeoctaentities(cube &c);
-extern bool pointinsel(selinfo &sel, vec &o);
+extern bool pointinsel(const selinfo &sel, const vec &o);
 
 extern void clearworldvars(bool msg = false);
 extern void resetmap(bool empty);
@@ -675,11 +585,21 @@ extern void preloadmodel(const char *name);
 extern void flushpreloadedmodels(bool msg = true);
 extern void preloadusedmapmodels(bool msg = false, bool bih = false);
 
+static inline model *loadmapmodel(int n)
+{
+    extern vector<mapmodelinfo> mapmodels;
+    if(mapmodels.inrange(n))
+    {
+        model *m = mapmodels[n].m;
+        return m ? m : loadmodel(NULL, n);
+    }
+    return NULL;
+}
+
 // renderparticles
 extern void particleinit();
 extern void clearparticles();
 extern void makeparticle(const vec &o, attrvector &attr);
-extern void makeparticles(extentity &e);
 extern void updateparticles();
 extern void renderparticles(bool mainpass = false);
 
@@ -712,13 +632,19 @@ extern void drawskybox(int farplane, bool limited);
 extern bool limitsky();
 
 // gui
-extern int mouseaction[2], guibound[2];
+extern int mouseaction[2];
+extern bool guiactionon;
+
+extern int guishadow, guiclicktab, guitextblend, guitextfade, guisepsize, guiscaletime,
+    guibgcolour, guibordercolour, guihovercolour, guitooltips, guitooltiptime,
+    guitooltipcolour, guitooltipbordercolour, guifieldbgcolour, guifieldbordercolour, guifieldactivecolour, guiactivecolour;
+extern float guibgblend, guiborderblend, guihoverscale, guihoverblend, guitooltipblend, guitooltipborderblend, guifieldbgblend, guifieldborderblend, guifieldactiveblend, guiactiveblend;
 
 extern void progressmenu();
 extern void mainmenu();
 extern void texturemenu();
 extern bool menuactive();
-extern int cleargui(int n = 0);
+extern int cleargui(int n = 0, bool skip = true);
 
 #define uipad(parent,count,body) { (parent).space(count); body; (parent).space(count); }
 #define uifont(parent,font,body) { (parent).pushfont(font); body; (parent).popfont(); }
@@ -740,7 +666,7 @@ extern void replacetexcube(cube &c, int oldtex, int newtex);
 extern void loadsky(char *basename);
 
 // main
-extern void setcaption(const char *text);
+extern void setcaption(const char *text = "", const char *text2 = "");
 extern int grabinput, colorpos, curfps, bestfps, worstfps, bestfpsdiff, worstfpsdiff, maxfps;
 
 // editing
@@ -748,15 +674,16 @@ extern int getmatvec(vec v);
 extern int fullbright, fullbrightlevel;
 extern vector<int> entgroup;
 
-extern void newentity(int type, const attrvector &attrs);
-extern void newentity(const vec &v, int type, const attrvector &attrs);
+extern int newentity(int type, const attrvector &attrs);
+extern int newentity(const vec &v, int type, const attrvector &attrs);
 
 // menu
 enum { MN_BACK = 0, MN_INPUT, MN_MAX };
 
 // console
-struct cline { char *cref; int type, reftime, outtime; };
-extern vector<cline> conlines;
+#define MAXCONLINES 1000
+struct cline { char *cref; int type, reftime, outtime, realtime; };
+extern reversequeue<cline, MAXCONLINES> conlines;
 extern void conline(int type, const char *sf, int n);
 
 // rendergl
@@ -808,7 +735,7 @@ extern uchar shouldsaveblendmap();
 namespace recorder
 {
     extern void stop();
-    extern void capture();
+    extern void capture(bool overlay = true);
     extern void cleanup();
 }
 #endif // STANDALONE
diff --git a/src/engine/explosion.h b/src/engine/explosion.h
index 6777f6e..1c9a73e 100644
--- a/src/engine/explosion.h
+++ b/src/engine/explosion.h
@@ -142,12 +142,12 @@ static void initsphere(int slices, int stacks)
     float ds = 1.0f/slices, dt = 1.0f/stacks, t = 1.0f;
     loopi(stacks+1)
     {
-        float rho = M_PI*(1-t), s = 0.0f;
+        float rho = M_PI*(1-t), s = 0.0f, sinrho = i && i < stacks ? sin(rho) : 0, cosrho = !i ? 1 : (i < stacks ? cos(rho) : -1);
         loopj(slices+1)
         {
             float theta = j==slices ? 0 : 2*M_PI*s;
             spherevert &v = sphereverts[i*(slices+1) + j];
-            v.pos = vec(-sin(theta)*sin(rho), cos(theta)*sin(rho), cos(rho));
+            v.pos = vec(-sin(theta)*sinrho, cos(theta)*sinrho, cosrho);
             v.s = s;
             v.t = t;
             s += ds;
@@ -156,7 +156,7 @@ static void initsphere(int slices, int stacks)
     }
 
     DELETEA(sphereindices);
-    spherenumindices = stacks*slices*3*2;
+    spherenumindices = (stacks-1)*slices*3*2;
     sphereindices = new ushort[spherenumindices];
     GLushort *curindex = sphereindices;
     loopi(stacks)
@@ -164,14 +164,18 @@ static void initsphere(int slices, int stacks)
         loopk(slices)
         {
             int j = i%2 ? slices-k-1 : k;
-
-            *curindex++ = i*(slices+1)+j;
-            *curindex++ = (i+1)*(slices+1)+j;
-            *curindex++ = i*(slices+1)+j+1;
-
-            *curindex++ = i*(slices+1)+j+1;
-            *curindex++ = (i+1)*(slices+1)+j;
-            *curindex++ = (i+1)*(slices+1)+j+1;
+            if(i)
+            {
+                *curindex++ = i*(slices+1)+j;
+                *curindex++ = (i+1)*(slices+1)+j;
+                *curindex++ = i*(slices+1)+j+1;
+            }
+            if(i+1 < stacks)
+            {
+                *curindex++ = i*(slices+1)+j+1;
+                *curindex++ = (i+1)*(slices+1)+j;
+                *curindex++ = (i+1)*(slices+1)+j+1;
+            }
         }
     }
 
@@ -415,7 +419,6 @@ struct explosionrenderer : sharedlistrenderer
             explosionent()
             {
                 type = ENT_CAMERA;
-                collidetype = COLLIDE_AABB;
             }
         } e;
 
@@ -431,7 +434,7 @@ struct explosionrenderer : sharedlistrenderer
 
             e.o = p->o;
             e.radius = e.xradius = e.yradius = e.height = e.aboveeye = psize;
-            if(::collide(&e, vec(0, 0, 0), 0, false)) continue;
+            if(!::collide(&e, vec(0, 0, 0), 0, false)) continue;
 
             if(depthfxscissor==2 && !depthfxtex.addscissorbox(p->o, psize)) continue;
 
diff --git a/src/engine/glexts.h b/src/engine/glexts.h
new file mode 100644
index 0000000..98ca379
--- /dev/null
+++ b/src/engine/glexts.h
@@ -0,0 +1,293 @@
+// GL_ARB_multitexture
+extern PFNGLACTIVETEXTUREARBPROC       glActiveTexture_;
+extern PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTexture_;
+extern PFNGLMULTITEXCOORD2FARBPROC     glMultiTexCoord2f_;
+extern PFNGLMULTITEXCOORD3FARBPROC     glMultiTexCoord3f_;
+extern PFNGLMULTITEXCOORD4FARBPROC     glMultiTexCoord4f_;
+
+// GL_ARB_vertex_buffer_object
+extern PFNGLGENBUFFERSARBPROC       glGenBuffers_;
+extern PFNGLBINDBUFFERARBPROC       glBindBuffer_;
+extern PFNGLMAPBUFFERARBPROC        glMapBuffer_;
+extern PFNGLUNMAPBUFFERARBPROC      glUnmapBuffer_;
+extern PFNGLBUFFERDATAARBPROC       glBufferData_;
+extern PFNGLBUFFERSUBDATAARBPROC    glBufferSubData_;
+extern PFNGLDELETEBUFFERSARBPROC    glDeleteBuffers_;
+extern PFNGLGETBUFFERSUBDATAARBPROC glGetBufferSubData_;
+
+// GL_ARB_occlusion_query
+extern PFNGLGENQUERIESARBPROC        glGenQueries_;
+extern PFNGLDELETEQUERIESARBPROC     glDeleteQueries_;
+extern PFNGLBEGINQUERYARBPROC        glBeginQuery_;
+extern PFNGLENDQUERYARBPROC          glEndQuery_;
+extern PFNGLGETQUERYIVARBPROC        glGetQueryiv_;
+extern PFNGLGETQUERYOBJECTIVARBPROC  glGetQueryObjectiv_;
+extern PFNGLGETQUERYOBJECTUIVARBPROC glGetQueryObjectuiv_;
+
+// GL_EXT_framebuffer_object
+extern PFNGLBINDRENDERBUFFEREXTPROC        glBindRenderbuffer_;
+extern PFNGLDELETERENDERBUFFERSEXTPROC     glDeleteRenderbuffers_;
+extern PFNGLGENFRAMEBUFFERSEXTPROC         glGenRenderbuffers_;
+extern PFNGLRENDERBUFFERSTORAGEEXTPROC     glRenderbufferStorage_;
+extern PFNGLCHECKFRAMEBUFFERSTATUSEXTPROC  glCheckFramebufferStatus_;
+extern PFNGLBINDFRAMEBUFFEREXTPROC         glBindFramebuffer_;
+extern PFNGLDELETEFRAMEBUFFERSEXTPROC      glDeleteFramebuffers_;
+extern PFNGLGENFRAMEBUFFERSEXTPROC         glGenFramebuffers_;
+extern PFNGLFRAMEBUFFERTEXTURE2DEXTPROC    glFramebufferTexture2D_;
+extern PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC glFramebufferRenderbuffer_;
+extern PFNGLGENERATEMIPMAPEXTPROC          glGenerateMipmap_;
+
+// GL_EXT_framebuffer_blit
+#ifndef GL_EXT_framebuffer_blit
+#define GL_READ_FRAMEBUFFER_EXT           0x8CA8
+#define GL_DRAW_FRAMEBUFFER_EXT           0x8CA9
+#define GL_DRAW_FRAMEBUFFER_BINDING_EXT   0x8CA6
+#define GL_READ_FRAMEBUFFER_BINDING_EXT   0x8CAA
+typedef void (APIENTRYP PFNGLBLITFRAMEBUFFEREXTPROC) (GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
+#endif
+extern PFNGLBLITFRAMEBUFFEREXTPROC         glBlitFramebuffer_;
+
+// GL_EXT_draw_range_elements
+extern PFNGLDRAWRANGEELEMENTSEXTPROC glDrawRangeElements_;
+
+// GL_EXT_blend_minmax
+extern PFNGLBLENDEQUATIONEXTPROC glBlendEquation_;
+
+// GL_EXT_blend_color
+extern PFNGLBLENDCOLOREXTPROC glBlendColor_;
+
+// GL_EXT_multi_draw_arrays
+extern PFNGLMULTIDRAWARRAYSEXTPROC   glMultiDrawArrays_;
+extern PFNGLMULTIDRAWELEMENTSEXTPROC glMultiDrawElements_;
+
+// GL_EXT_packed_depth_stencil
+#ifndef GL_DEPTH_STENCIL_EXT
+#define GL_DEPTH_STENCIL_EXT 0x84F9
+#endif
+#ifndef GL_DEPTH24_STENCIL8_EXT
+#define GL_DEPTH24_STENCIL8_EXT 0x88F0
+#endif
+
+// GL_ARB_texture_compression
+extern PFNGLCOMPRESSEDTEXIMAGE3DARBPROC    glCompressedTexImage3D_;
+extern PFNGLCOMPRESSEDTEXIMAGE2DARBPROC    glCompressedTexImage2D_;
+extern PFNGLCOMPRESSEDTEXIMAGE1DARBPROC    glCompressedTexImage1D_;
+extern PFNGLCOMPRESSEDTEXSUBIMAGE3DARBPROC glCompressedTexSubImage3D_;
+extern PFNGLCOMPRESSEDTEXSUBIMAGE2DARBPROC glCompressedTexSubImage2D_;
+extern PFNGLCOMPRESSEDTEXSUBIMAGE1DARBPROC glCompressedTexSubImage1D_;
+extern PFNGLGETCOMPRESSEDTEXIMAGEARBPROC   glGetCompressedTexImage_;
+
+// GL_EXT_fog_coord
+extern PFNGLFOGCOORDPOINTEREXTPROC glFogCoordPointer_;
+
+// GL_ARB_map_buffer_range
+#ifndef GL_ARB_map_buffer_range
+#define GL_MAP_READ_BIT                   0x0001
+#define GL_MAP_WRITE_BIT                  0x0002
+#define GL_MAP_INVALIDATE_RANGE_BIT       0x0004
+#define GL_MAP_INVALIDATE_BUFFER_BIT      0x0008
+#define GL_MAP_FLUSH_EXPLICIT_BIT         0x0010
+#define GL_MAP_UNSYNCHRONIZED_BIT         0x0020
+typedef GLvoid* (APIENTRYP PFNGLMAPBUFFERRANGEPROC) (GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
+typedef void (APIENTRYP PFNGLFLUSHMAPPEDBUFFERRANGEPROC) (GLenum target, GLintptr offset, GLsizeiptr length);
+#endif
+extern PFNGLMAPBUFFERRANGEPROC         glMapBufferRange_;
+extern PFNGLFLUSHMAPPEDBUFFERRANGEPROC glFlushMappedBufferRange_;
+
+// GL_ARB_vertex_program, GL_ARB_fragment_program
+extern PFNGLGENPROGRAMSARBPROC              glGenProgramsARB_;
+extern PFNGLDELETEPROGRAMSARBPROC           glDeleteProgramsARB_;
+extern PFNGLBINDPROGRAMARBPROC              glBindProgramARB_;
+extern PFNGLPROGRAMSTRINGARBPROC            glProgramStringARB_;
+extern PFNGLGETPROGRAMIVARBPROC             glGetProgramivARB_;
+extern PFNGLPROGRAMENVPARAMETER4FARBPROC    glProgramEnvParameter4fARB_;
+extern PFNGLPROGRAMENVPARAMETER4FVARBPROC   glProgramEnvParameter4fvARB_;
+
+// GL_EXT_gpu_program_parameters
+#ifndef GL_EXT_gpu_program_parameters
+#define GL_EXT_gpu_program_parameters 1
+typedef void (APIENTRYP PFNGLPROGRAMENVPARAMETERS4FVEXTPROC) (GLenum target, GLuint index, GLsizei count, const GLfloat *params);
+typedef void (APIENTRYP PFNGLPROGRAMLOCALPARAMETERS4FVEXTPROC) (GLenum target, GLuint index, GLsizei count, const GLfloat *params);
+#endif
+
+extern PFNGLPROGRAMENVPARAMETERS4FVEXTPROC   glProgramEnvParameters4fv_;
+extern PFNGLPROGRAMLOCALPARAMETERS4FVEXTPROC glProgramLocalParameters4fv_;
+
+#ifndef GL_VERSION_2_1
+#define GL_VERSION_2_1 1
+#define GL_FLOAT_MAT2x3                   0x8B65
+#define GL_FLOAT_MAT2x4                   0x8B66
+#define GL_FLOAT_MAT3x2                   0x8B67
+#define GL_FLOAT_MAT3x4                   0x8B68
+#define GL_FLOAT_MAT4x2                   0x8B69
+#define GL_FLOAT_MAT4x3                   0x8B6A
+typedef void (APIENTRYP PFNGLUNIFORMMATRIX2X3FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
+typedef void (APIENTRYP PFNGLUNIFORMMATRIX3X2FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
+typedef void (APIENTRYP PFNGLUNIFORMMATRIX2X4FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
+typedef void (APIENTRYP PFNGLUNIFORMMATRIX4X2FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
+typedef void (APIENTRYP PFNGLUNIFORMMATRIX3X4FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
+typedef void (APIENTRYP PFNGLUNIFORMMATRIX4X3FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
+#endif
+
+// OpenGL 2.0: GL_ARB_shading_language_100, GL_ARB_shader_objects, GL_ARB_fragment_shader, GL_ARB_vertex_shader
+#ifdef __APPLE__
+#define glCreateProgram_ glCreateProgram
+#define glDeleteProgram_ glDeleteProgram
+#define glUseProgram_ glUseProgram
+#define glCreateShader_ glCreateShader
+#define glDeleteShader_ glDeleteShader
+#define glShaderSource_ glShaderSource
+#define glCompileShader_ glCompileShader
+#define glGetShaderiv_ glGetShaderiv
+#define glGetProgramiv_ glGetProgramiv
+#define glAttachShader_ glAttachShader
+#define glGetProgramInfoLog_ glGetProgramInfoLog
+#define glGetShaderInfoLog_ glGetShaderInfoLog
+#define glLinkProgram_ glLinkProgram
+#define glGetUniformLocation_ glGetUniformLocation
+#define glUniform1f_ glUniform1f
+#define glUniform2f_ glUniform2f
+#define glUniform3f_ glUniform3f
+#define glUniform4f_ glUniform4f
+#define glUniform1fv_ glUniform1fv
+#define glUniform2fv_ glUniform2fv
+#define glUniform3fv_ glUniform3fv
+#define glUniform4fv_ glUniform4fv
+#define glUniform1i_ glUniform1i
+#define glUniformMatrix2fv_ glUniformMatrix2fv
+#define glUniformMatrix3fv_ glUniformMatrix3fv
+#define glUniformMatrix4fv_ glUniformMatrix4fv
+#define glBindAttribLocation_ glBindAttribLocation
+#define glGetActiveUniform_ glGetActiveUniform
+#define glEnableVertexAttribArray_ glEnableVertexAttribArray
+#define glDisableVertexAttribArray_ glDisableVertexAttribArray
+#define glVertexAttribPointer_ glVertexAttribPointer
+
+#define glUniformMatrix2x3fv_ glUniformMatrix2x3fv
+#define glUniformMatrix3x2fv_ glUniformMatrix3x2fv
+#define glUniformMatrix2x4fv_ glUniformMatrix2x4fv
+#define glUniformMatrix4x2fv_ glUniformMatrix4x2fv
+#define glUniformMatrix3x4fv_ glUniformMatrix3x4fv
+#define glUniformMatrix4x3fv_ glUniformMatrix4x3fv
+#else
+extern PFNGLCREATEPROGRAMPROC            glCreateProgram_;
+extern PFNGLDELETEPROGRAMPROC            glDeleteProgram_;
+extern PFNGLUSEPROGRAMPROC               glUseProgram_;
+extern PFNGLCREATESHADERPROC             glCreateShader_;
+extern PFNGLDELETESHADERPROC             glDeleteShader_;
+extern PFNGLSHADERSOURCEPROC             glShaderSource_;
+extern PFNGLCOMPILESHADERPROC            glCompileShader_;
+extern PFNGLGETSHADERIVPROC              glGetShaderiv_;
+extern PFNGLGETPROGRAMIVPROC             glGetProgramiv_;
+extern PFNGLATTACHSHADERPROC             glAttachShader_;
+extern PFNGLGETPROGRAMINFOLOGPROC        glGetProgramInfoLog_;
+extern PFNGLGETSHADERINFOLOGPROC         glGetShaderInfoLog_;
+extern PFNGLLINKPROGRAMPROC              glLinkProgram_;
+extern PFNGLGETUNIFORMLOCATIONPROC       glGetUniformLocation_;
+extern PFNGLUNIFORM1FPROC                glUniform1f_;
+extern PFNGLUNIFORM2FPROC                glUniform2f_;
+extern PFNGLUNIFORM3FPROC                glUniform3f_;
+extern PFNGLUNIFORM4FPROC                glUniform4f_;
+extern PFNGLUNIFORM1FVPROC               glUniform1fv_;
+extern PFNGLUNIFORM2FVPROC               glUniform2fv_;
+extern PFNGLUNIFORM3FVPROC               glUniform3fv_;
+extern PFNGLUNIFORM4FVPROC               glUniform4fv_;
+extern PFNGLUNIFORM1IPROC                glUniform1i_;
+extern PFNGLBINDATTRIBLOCATIONPROC       glBindAttribLocation_;
+extern PFNGLGETACTIVEUNIFORMPROC         glGetActiveUniform_;
+extern PFNGLENABLEVERTEXATTRIBARRAYPROC  glEnableVertexAttribArray_;
+extern PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray_;
+extern PFNGLVERTEXATTRIBPOINTERPROC      glVertexAttribPointer_;
+
+extern PFNGLUNIFORMMATRIX2X3FVPROC       glUniformMatrix2x3fv_;
+extern PFNGLUNIFORMMATRIX3X2FVPROC       glUniformMatrix3x2fv_;
+extern PFNGLUNIFORMMATRIX2X4FVPROC       glUniformMatrix2x4fv_;
+extern PFNGLUNIFORMMATRIX4X2FVPROC       glUniformMatrix4x2fv_;
+extern PFNGLUNIFORMMATRIX3X4FVPROC       glUniformMatrix3x4fv_;
+extern PFNGLUNIFORMMATRIX4X3FVPROC       glUniformMatrix4x3fv_;
+#endif
+
+#ifndef GL_ARB_uniform_buffer_object
+#define GL_ARB_uniform_buffer_object 1
+#define GL_UNIFORM_BUFFER                 0x8A11
+#define GL_UNIFORM_BUFFER_BINDING         0x8A28
+#define GL_UNIFORM_BUFFER_START           0x8A29
+#define GL_UNIFORM_BUFFER_SIZE            0x8A2A
+#define GL_MAX_VERTEX_UNIFORM_BLOCKS      0x8A2B
+#define GL_MAX_GEOMETRY_UNIFORM_BLOCKS    0x8A2C
+#define GL_MAX_FRAGMENT_UNIFORM_BLOCKS    0x8A2D
+#define GL_MAX_COMBINED_UNIFORM_BLOCKS    0x8A2E
+#define GL_MAX_UNIFORM_BUFFER_BINDINGS    0x8A2F
+#define GL_MAX_UNIFORM_BLOCK_SIZE         0x8A30
+#define GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS 0x8A31
+#define GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS 0x8A32
+#define GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS 0x8A33
+#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34
+#define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35
+#define GL_ACTIVE_UNIFORM_BLOCKS          0x8A36
+#define GL_UNIFORM_TYPE                   0x8A37
+#define GL_UNIFORM_SIZE                   0x8A38
+#define GL_UNIFORM_NAME_LENGTH            0x8A39
+#define GL_UNIFORM_BLOCK_INDEX            0x8A3A
+#define GL_UNIFORM_OFFSET                 0x8A3B
+#define GL_UNIFORM_ARRAY_STRIDE           0x8A3C
+#define GL_UNIFORM_MATRIX_STRIDE          0x8A3D
+#define GL_UNIFORM_IS_ROW_MAJOR           0x8A3E
+#define GL_UNIFORM_BLOCK_BINDING          0x8A3F
+#define GL_UNIFORM_BLOCK_DATA_SIZE        0x8A40
+#define GL_UNIFORM_BLOCK_NAME_LENGTH      0x8A41
+#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS  0x8A42
+#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES 0x8A43
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER 0x8A44
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER 0x8A45
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER 0x8A46
+#define GL_INVALID_INDEX                  0xFFFFFFFFu
+
+typedef void (APIENTRYP PFNGLGETUNIFORMINDICESPROC) (GLuint program, GLsizei uniformCount, const GLchar* *uniformNames, GLuint *uniformIndices);
+typedef void (APIENTRYP PFNGLGETACTIVEUNIFORMSIVPROC) (GLuint program, GLsizei uniformCount, const GLuint *uniformIndices, GLenum pname, GLint *params);
+typedef GLuint (APIENTRYP PFNGLGETUNIFORMBLOCKINDEXPROC) (GLuint program, const GLchar *uniformBlockName);
+typedef void (APIENTRYP PFNGLGETACTIVEUNIFORMBLOCKIVPROC) (GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint *params);
+typedef void (APIENTRYP PFNGLUNIFORMBLOCKBINDINGPROC) (GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
+#endif
+
+#ifndef GL_INVALID_INDEX
+#define GL_INVALID_INDEX                  0xFFFFFFFFu
+#endif
+
+#ifndef GL_VERSION_3_0
+#define GL_VERSION_3_0 1
+typedef void (APIENTRYP PFNGLBINDBUFFERRANGEPROC) (GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
+typedef void (APIENTRYP PFNGLBINDBUFFERBASEPROC) (GLenum target, GLuint index, GLuint buffer);
+#elif GL_GLEXT_VERSION < 43
+typedef void (APIENTRYP PFNGLBINDBUFFERRANGEPROC) (GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
+typedef void (APIENTRYP PFNGLBINDBUFFERBASEPROC) (GLenum target, GLuint index, GLuint buffer);
+#endif
+
+// GL_ARB_uniform_buffer_object
+extern PFNGLGETUNIFORMINDICESPROC       glGetUniformIndices_;
+extern PFNGLGETACTIVEUNIFORMSIVPROC     glGetActiveUniformsiv_;
+extern PFNGLGETUNIFORMBLOCKINDEXPROC    glGetUniformBlockIndex_;
+extern PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv_;
+extern PFNGLUNIFORMBLOCKBINDINGPROC     glUniformBlockBinding_;
+extern PFNGLBINDBUFFERBASEPROC          glBindBufferBase_;
+extern PFNGLBINDBUFFERRANGEPROC         glBindBufferRange_;
+
+#ifndef GL_EXT_bindable_uniform
+#define GL_EXT_bindable_uniform 1
+#define GL_MAX_VERTEX_BINDABLE_UNIFORMS_EXT 0x8DE2
+#define GL_MAX_FRAGMENT_BINDABLE_UNIFORMS_EXT 0x8DE3
+#define GL_MAX_GEOMETRY_BINDABLE_UNIFORMS_EXT 0x8DE4
+#define GL_MAX_BINDABLE_UNIFORM_SIZE_EXT  0x8DED
+#define GL_UNIFORM_BUFFER_EXT             0x8DEE
+#define GL_UNIFORM_BUFFER_BINDING_EXT     0x8DEF
+
+typedef void (APIENTRYP PFNGLUNIFORMBUFFEREXTPROC) (GLuint program, GLint location, GLuint buffer);
+typedef GLint (APIENTRYP PFNGLGETUNIFORMBUFFERSIZEEXTPROC) (GLuint program, GLint location);
+typedef GLintptr (APIENTRYP PFNGLGETUNIFORMOFFSETEXTPROC) (GLuint program, GLint location);
+#endif
+
+// GL_EXT_bindable_uniform
+extern PFNGLUNIFORMBUFFEREXTPROC        glUniformBuffer_;
+extern PFNGLGETUNIFORMBUFFERSIZEEXTPROC glGetUniformBufferSize_;
+extern PFNGLGETUNIFORMOFFSETEXTPROC     glGetUniformOffset_;
+
diff --git a/src/engine/grass.cpp b/src/engine/grass.cpp
index e4a9754..64e9cc1 100644
--- a/src/engine/grass.cpp
+++ b/src/engine/grass.cpp
@@ -36,7 +36,7 @@ VARFN(IDF_PERSIST, grasswedges, numgrasswedges, 8, 8, 1024, resetgrasswedges(num
 struct grassvert
 {
     vec pos;
-    uchar color[4];
+    bvec4 color;
     float u, v, lmu, lmv;
 };
 
@@ -203,12 +203,12 @@ static void gengrassquads(grassgroup *&group, const grasswedge &w, const grasstr
               lm2u = g.tcu.dot(p2), lm2v = g.tcv.dot(p2),
               fade = dist - t > taperdist ? (grassdist - (dist - t))*taperscale : 1,
               height = gh * fade;
-        uchar color[4] = { gcol.x, gcol.y, gcol.z, uchar(fade*blend*255) };
+        bvec4 color(gcol, uchar(fade*blend*255));
 
         #define GRASSVERT(n, tcv, modify) { \
             grassvert &gv = grassverts.add(); \
             gv.pos = p##n; \
-            memcpy(gv.color, color, sizeof(color)); \
+            gv.color = color; \
             gv.u = tc##n; gv.v = tcv; \
             gv.lmu = lm##n##u; gv.lmv = lm##n##v; \
             modify; \
@@ -296,7 +296,7 @@ void rendergrass()
     glVertexPointer(3, GL_FLOAT, sizeof(grassvert), grassverts[0].pos.v);
 
     glEnableClientState(GL_COLOR_ARRAY);
-    glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(grassvert), grassverts[0].color);
+    glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(grassvert), grassverts[0].color.v);
 
     glEnableClientState(GL_TEXTURE_COORD_ARRAY);
     glTexCoordPointer(2, GL_FLOAT, sizeof(grassvert), &grassverts[0].u);
diff --git a/src/engine/iqm.h b/src/engine/iqm.h
index 816642b..75650d6 100644
--- a/src/engine/iqm.h
+++ b/src/engine/iqm.h
@@ -131,6 +131,8 @@ struct iqm : skelmodel, skelloader<iqm>
                     case IQM_BLENDWEIGHTS: if(va.format != IQM_UBYTE || va.size != 4) return false; vweight = (uchar *)&buf[va.offset]; break;
                 }
             }
+            if(!vpos) return false;
+
             iqmtriangle *tris = (iqmtriangle *)&buf[hdr.ofs_triangles];
             iqmmesh *imeshes = (iqmmesh *)&buf[hdr.ofs_meshes];
             iqmjoint *joints = (iqmjoint *)&buf[hdr.ofs_joints];
@@ -172,43 +174,71 @@ struct iqm : skelmodel, skelloader<iqm>
                 meshes.add(m);
                 m->name = newstring(&str[im.name]);
                 m->numverts = im.num_vertexes;
-                if(m->numverts) 
+                int noblend = -1;
+                if(m->numverts)
                 {
                     m->verts = new vert[m->numverts];
                     if(vtan) m->bumpverts = new bumpvert[m->numverts];
+                    if(!vindex || !vweight)
+                    {
+                        blendcombo c;
+                        c.finalize(0);
+                        noblend = m->addblendcombo(c);
+                    }
                 }
+                int fv = im.first_vertex;
+                float *mpos = vpos + 3*fv,
+                      *mnorm = vnorm ? vnorm + 3*fv : NULL,
+                      *mtan = vtan ? vtan + 4*fv : NULL,
+                      *mtc = vtc ? vtc + 2*fv : NULL;
+                uchar *mindex = vindex ? vindex + 4*fv : NULL, *mweight = vweight ? vweight + 4*fv : NULL;
                 loopj(im.num_vertexes)
                 {
-                    int fj = j + im.first_vertex;
                     vert &v = m->verts[j];
-                    loopk(3) v.pos[k] = vpos[3*fj + k];    
-                    v.pos.y = -v.pos.y;
-                    v.u = vtc[2*fj + 0];
-                    v.v = vtc[2*fj + 1];
-                    if(vnorm) 
+                    v.pos = vec(mpos[0], -mpos[1], mpos[2]);
+                    mpos += 3;
+                    if(mtc)
+                    {
+                        v.u = mtc[0];
+                        v.v = mtc[1];
+                        mtc += 2;
+                    }
+                    else v.u = v.v = 0;
+                    if(mnorm)
                     {
-                        loopk(3) v.norm[k] = vnorm[3*fj + k];
-                        v.norm.y = -v.norm.y;
-                        if(vtan)
+                        v.norm = vec(mnorm[0], -mnorm[1], mnorm[2]);
+                        mnorm += 3;
+                        if(mtan)
                         {
                             bumpvert &bv = m->bumpverts[j];
-                            loopk(3) bv.tangent[k] = vtan[4*fj + k];
-                            bv.tangent.y = -bv.tangent.y;
-                            bv.bitangent = vtan[4*fj + 3];
+                            bv.tangent = vec(mtan[0], -mtan[1], mtan[2]);
+                            bv.bitangent = mtan[3];
+                            mtan += 4;
                         }
-                    } 
-                    blendcombo c;
-                    int sorted = 0;
-                    if(vindex && vweight) loopk(4) sorted = c.addweight(sorted, vweight[4*fj + k], vindex[4*fj + k]);
-                    c.finalize(sorted);
-                    v.blend = m->addblendcombo(c);
+                    }
+                    else v.norm = vec(0, 0, 0);
+                    if(noblend < 0)
+                    {
+                        blendcombo c;
+                        int sorted = 0;
+                        loopk(4) sorted = c.addweight(sorted, mweight[k], mindex[k]);
+                        mweight += 4;
+                        mindex += 4;
+                        c.finalize(sorted);
+                        v.blend = m->addblendcombo(c);
+                    }
+                    else v.blend = noblend;
                 }
                 m->numtris = im.num_triangles;
                 if(m->numtris) m->tris = new tri[m->numtris]; 
+                iqmtriangle *mtris = tris + im.first_triangle;
                 loopj(im.num_triangles)
                 {
-                    int fj = j + im.first_triangle;
-                    loopk(3) m->tris[j].vert[k] = tris[fj].vertex[k] - im.first_vertex;
+                    tri &t = m->tris[j];
+                    t.vert[0] = mtris->vertex[0] - fv;
+                    t.vert[1] = mtris->vertex[1] - fv;
+                    t.vert[2] = mtris->vertex[2] - fv;
+                    ++mtris;
                 }
                 if(!m->numtris || !m->numverts)
                 {
@@ -302,7 +332,7 @@ struct iqm : skelmodel, skelloader<iqm>
             if(hdr.version != 2) goto error;
             if(hdr.filesize > (16<<20)) goto error; // sanity check... don't load files bigger than 16 MB
             buf = new uchar[hdr.filesize];
-            if(f->read(buf + sizeof(hdr), hdr.filesize - sizeof(hdr)) != int(hdr.filesize - sizeof(hdr))) goto error;
+            if(f->read(buf + sizeof(hdr), hdr.filesize - sizeof(hdr)) != hdr.filesize - sizeof(hdr)) goto error;
 
             if(doloadmesh && !loadiqmmeshes(filename, hdr, buf)) goto error;
             if(doloadanim && !loadiqmanims(filename, hdr, buf)) goto error;
diff --git a/src/engine/irc.cpp b/src/engine/irc.cpp
index 86711d6..cf36296 100644
--- a/src/engine/irc.cpp
+++ b/src/engine/irc.cpp
@@ -1,5 +1,8 @@
 #include "engine.h"
 
+VAR(0, ircfilter, 0, 2, 2);
+VAR(0, ircverbose, 0, 0, 2);
+
 vector<ircnet *> ircnets;
 
 ircnet *ircfind(const char *name)
@@ -13,8 +16,8 @@ ircnet *ircfind(const char *name)
 
 void ircprintf(ircnet *n, int relay, const char *target, const char *msg, ...)
 {
-    defvformatstring(str, msg, msg);
-    string s;
+    defvformatbigstring(str, msg, msg);
+    string s = "";
     if(target && *target && strcasecmp(target, n->nick))
     {
         ircchan *c = ircfindchan(n, target);
@@ -45,13 +48,14 @@ void ircprintf(ircnet *n, int relay, const char *target, const char *msg, ...)
         n->updated |= IRCUP_MSG;
         #endif
     }
-    console(0, "%s %s", s, str); // console is not used to relay
+    if(ircverbose) console(0, "%s %s", s, str); // console is not used to relay
 }
 
 void ircestablish(ircnet *n)
 {
     if(!n) return;
-    n->lastattempt = clocktime;
+    n->lastattempt = n->lastactivity = clocktime;
+    n->lastping = 0;
     if(n->address.host == ENET_HOST_ANY)
     {
         ircprintf(n, 4, NULL, "looking up %s:[%d]...", n->serv, n->port);
@@ -71,7 +75,8 @@ void ircestablish(ircnet *n)
         ircprintf(n, 4, NULL, "failed to bind connection socket: %s", n->ip);
         address.host = ENET_HOST_ANY;
     }
-    if(n->sock == ENET_SOCKET_NULL || connectwithtimeout(n->sock, n->serv, n->address) < 0)
+    if(n->sock != ENET_SOCKET_NULL) enet_socket_set_option(n->sock, ENET_SOCKOPT_NONBLOCK, 1);
+    if(n->sock == ENET_SOCKET_NULL || enet_socket_connect(n->sock, &n->address) < 0)
     {
         ircprintf(n, 4, NULL, n->sock == ENET_SOCKET_NULL ? "could not open socket to %s:[%d]" : "could not connect to %s:[%d]", n->serv, n->port);
         if(n->sock != ENET_SOCKET_NULL)
@@ -82,7 +87,7 @@ void ircestablish(ircnet *n)
         n->state = IRC_DISC;
         return;
     }
-    n->state = IRC_ATTEMPT;
+    n->state = IRC_WAIT;
     ircprintf(n, 4, NULL, "connecting to %s:[%d]...", n->serv, n->port);
 }
 
@@ -90,20 +95,21 @@ void ircsend(ircnet *n, const char *msg, ...)
 {
     if(!n) return;
     defvformatstring(str, msg, msg);
-    if(n->sock == ENET_SOCKET_NULL) return;
-    if(verbose >= 2) console(0, "[%s] >>> %s", n->name, str);
+    if(n->sock == ENET_SOCKET_NULL || !*msg) return; // don't spew \n
+    if(ircverbose >= 2) console(0, "[%s] >>> %s", n->name, str);
     concatstring(str, "\n");
     ENetBuffer buf;
     uchar ubuf[512];
-    int len = strlen(str), carry = 0;
+    size_t len = strlen(str), carry = 0;
     while(carry < len)
     {
-        int numu = encodeutf8(ubuf, sizeof(ubuf)-1, &((uchar *)str)[carry], len - carry, &carry);
+        size_t numu = encodeutf8(ubuf, sizeof(ubuf)-1, &((uchar *)str)[carry], len - carry, &carry);
         if(carry >= len) ubuf[numu++] = '\n';
         loopi(numu) switch(ubuf[i])
         {
             case '\v': ubuf[i] = '\x01'; break;
-            case '\f': ubuf[i] = '\x03'; break;
+            case '\f': ubuf[i] = '\x03'; break; // color code
+            case '\r': ubuf[i] = '\x0F'; break; // end color formatting
         }
         buf.data = ubuf;
         buf.dataLength = numu;
@@ -111,9 +117,7 @@ void ircsend(ircnet *n, const char *msg, ...)
     }
 }
 
-VAR(0, ircfilter, 0, 2, 2);
-
-void converttext(char *dst, const char *src)
+void cube2irc(char *dst, const char *src)
 {
     int colorpos = 0; char colorstack[10];
     memset(colorstack, 'u', sizeof(colorstack)); //indicate user color
@@ -127,9 +131,9 @@ void converttext(char *dst, const char *src)
                 c = *++src;
                 if(c) ++src;
             }
-            else if(c == '[' || c == '(')
+            else if(c == '[' || c == '(' || c == '{')
             {
-                const char *end = strchr(src, c == '[' ? ']' : ')');
+                const char *end = strchr(src, c == '[' ? ']' : (c == '(' ? ')' : '}'));
                 src += end ? end-src : strlen(src);
             }
             else if(c == 's') { if(colorpos < (int)sizeof(colorstack)-1) colorpos++; continue; }
@@ -137,18 +141,21 @@ void converttext(char *dst, const char *src)
             int oldcolor = colorstack[colorpos]; colorstack[colorpos] = c;
             switch(c)
             {
-                case 'g': case '0': case 'G': *dst++ = '\f'; *dst++ = '0'; *dst++ = '3'; break; // green
-                case 'b': case '1': case 'B': *dst++ = '\f'; *dst++ = '1'; *dst++ = '2'; break; // blue
-                case 'y': case '2': case 'Y': *dst++ = '\f'; *dst++ = '0'; *dst++ = '3'; break; // yellow
-                case 'r': case '3': case 'R': *dst++ = '\f'; *dst++ = '0'; *dst++ = '4'; break; // red
-                case 'a': case '4': *dst++ = '\f'; *dst++ = '1'; *dst++ = '4'; break; // grey
-                case 'm': case '5': case 'M': *dst++ = '\f'; *dst++ = '1'; *dst++ = '3'; break; // magenta
-                case 'o': case '6': case 'O': *dst++ = '\f'; *dst++ = '0'; *dst++ = '7'; break; // orange
-                case 'c': case '9': case 'C': *dst++ = '\f'; *dst++ = '1'; *dst++ = '0'; break; // cyan
-                case 'v': *dst++ = '\f'; *dst++ = '0'; *dst++ = '6'; break; // violet
-                case 'p': *dst++ = '\f'; *dst++ = '0'; *dst++ = '6'; break; // purple
+                case 'B': *dst++ = '\f'; *dst++ = '0'; *dst++ = '2'; break; // dark blue
+                case 'G': *dst++ = '\f'; *dst++ = '0'; *dst++ = '3'; break; // dark green
+                case 'r': case '3': *dst++ = '\f'; *dst++ = '0'; *dst++ = '4'; break; // red
                 case 'n': *dst++ = '\f'; *dst++ = '0'; *dst++ = '5'; break; // brown
-                case 'u': case 'w': case '7': case 'k': case '8': case 'd': case 'A': *dst++ = '\f'; *dst++ = '0'; *dst++ = '1'; break; // dark grey
+                case 'p': case 'v': *dst++ = '\f'; *dst++ = '0'; *dst++ = '6'; break; // purple
+                case 'o': case '6': case 'O': *dst++ = '\f'; *dst++ = '0'; *dst++ = '7'; break; // orange
+                case 'y': case '2': case 'Y': *dst++ = '\f'; *dst++ = '0'; *dst++ = '8'; break; // yellow
+                case 'g': case '0': *dst++ = '\f'; *dst++ = '0'; *dst++ = '9'; break; // green
+                case 'C': *dst++ = '\f'; *dst++ = '1'; *dst++ = '0'; break; // dark cyan
+                case 'c': case '9': *dst++ = '\f'; *dst++ = '1'; *dst++ = '1'; break; // cyan
+                case 'b': case '1': *dst++ = '\f'; *dst++ = '1'; *dst++ = '2'; break; // blue
+                case 'm': case '5': case 'M': *dst++ = '\f'; *dst++ = '1'; *dst++ = '3'; break; // magenta
+                case 'k': case '8': case 'd': case 'A': *dst++ = '\f'; *dst++ = '1'; *dst++ = '4'; break; // dark grey
+                case 'a': case '4': *dst++ = '\f'; *dst++ = '1'; *dst++ = '5'; break; // grey
+                case 'u': case 'w': case '7': *dst++ = '\r'; break; // no color
                 default: colorstack[colorpos] = oldcolor; break;
             }
             continue;
@@ -158,21 +165,77 @@ void converttext(char *dst, const char *src)
     *dst = '\0';
 }
 
+void irc2cube(char *dst, const char *src)
+{
+    for(int c = *src; c; c = *++src)
+    {
+        if(c == '\f')
+        {
+            c = *++src;
+            switch(c)
+            {
+                case '0':
+                    c = *++src;
+                    switch(c)
+                    {
+                        case '0': *dst++ = '\f'; *dst++ = 'w'; break; // white
+                        case '1': *dst++ = '\f'; *dst++ = 'A'; break; // dark grey (black too hard to see)
+                        case '2': *dst++ = '\f'; *dst++ = 'B'; break; // dark blue
+                        case '3': *dst++ = '\f'; *dst++ = 'G'; break; // dark green
+                        case '4': *dst++ = '\f'; *dst++ = 'r'; break; // red
+                        case '5': *dst++ = '\f'; *dst++ = 'n'; break; // brown
+                        case '6': *dst++ = '\f'; *dst++ = 'p'; break; // purple
+                        case '7': *dst++ = '\f'; *dst++ = 'o'; break; // orange
+                        case '8': *dst++ = '\f'; *dst++ = 'y'; break; // yellow
+                        case '9': *dst++ = '\f'; *dst++ = 'g'; break; // green
+                        default: *dst++ = '\f'; *dst++ = 'w'; c = *--src; break;
+                    }
+                    break;
+                case '1':
+                    c = *++src;
+                    switch(c)
+                    {
+                        case '0': *dst++ = '\f'; *dst++ = 'C'; break; // dark cyan
+                        case '1': *dst++ = '\f'; *dst++ = 'c'; break; // cyan
+                        case '2': *dst++ = '\f'; *dst++ = 'b'; break; // blue
+                        case '3': *dst++ = '\f'; *dst++ = 'm'; break; // magenta
+                        case '4': *dst++ = '\f'; *dst++ = 'A'; break; // dark grey
+                        case '5': *dst++ = '\f'; *dst++ = 'a'; break; // grey
+                        default: *dst++ = '\f'; *dst++ = 'A'; c = *--src; break;
+                    }
+                    break;
+                case '2': *dst++ = '\f'; *dst++ = 'B'; break; // dark blue
+                case '3': *dst++ = '\f'; *dst++ = 'G'; break; // dark green
+                case '4': *dst++ = '\f'; *dst++ = 'r'; break; // red
+                case '5': *dst++ = '\f'; *dst++ = 'n'; break; // brown
+                case '6': *dst++ = '\f'; *dst++ = 'p'; break; // purple
+                case '7': *dst++ = '\f'; *dst++ = 'o'; break; // orange
+                case '8': *dst++ = '\f'; *dst++ = 'y'; break; // yellow
+                case '9': *dst++ = '\f'; *dst++ = 'g'; break; // green
+                default: *dst++ = '\f'; *dst++ = 'w'; c = *--src; break;
+            }
+            continue;
+        }
+        if(iscubeprint(c) || iscubespace(c)) *dst++ = c;
+    }
+    *dst = '\0';
+}
+
 void ircoutf(int relay, const char *msg, ...)
 {
     defvformatstring(src, msg, msg);
-    mkstring(str);
+    string str = "";
     switch(ircfilter)
     {
-        case 2: filtertext(str, src); break;
-        case 1: converttext(str, src); break;
+        case 2: filterstring(str, src); break;
+        case 1: cube2irc(str, src); break;
         case 0: default: copystring(str, src); break;
     }
     loopv(ircnets) if(ircnets[i]->sock != ENET_SOCKET_NULL && ircnets[i]->type == IRCT_RELAY && ircnets[i]->state == IRC_ONLINE)
     {
         ircnet *n = ircnets[i];
 #if 0 // workaround for freenode's crappy dropping all but the first target of multi-target messages even though they don't state MAXTARGETS=1 in 005 string..
-        mkstring(s);
+        string s = "";
         loopvj(n->channels) if(n->channels[j].state == IRCC_JOINED && n->channels[j].relay >= relay)
         {
             ircchan *c = &n->channels[j];
@@ -192,8 +255,7 @@ int ircrecv(ircnet *n)
     if(!n) return -1;
     if(n->sock == ENET_SOCKET_NULL) return -2;
     int total = 0;
-    enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE;
-    while(enet_socket_wait(n->sock, &events, 0) >= 0 && events)
+    for(;;)
     {
         ENetBuffer buf;
         buf.data = n->input + n->inputlen;
@@ -208,7 +270,7 @@ int ircrecv(ircnet *n)
             case '\v': case '\f': n->input[n->inputlen+i] = ' '; break;
         }
         n->inputlen += len;
-        int carry = 0, decoded = decodeutf8(&n->input[n->inputcarry], n->inputlen - n->inputcarry, &n->input[n->inputcarry], n->inputlen - n->inputcarry, &carry);
+        size_t carry = 0, decoded = decodeutf8(&n->input[n->inputcarry], n->inputlen - n->inputcarry, &n->input[n->inputcarry], n->inputlen - n->inputcarry, &carry);
         if(carry > decoded)
         {
             memmove(&n->input[n->inputcarry + decoded], &n->input[n->inputcarry + carry], n->inputlen - (n->inputcarry + carry));
@@ -226,19 +288,12 @@ void ircnewnet(int type, const char *name, const char *serv, int port, const cha
     ircnet *m = ircfind(name);
     if(m)
     {
-        if(m->state != IRC_DISC) conoutf("ircnet %s already exists", m->name);
-        else ircestablish(m);
+        conoutf("ircnet %s already exists", m->name);
         return;
     }
     ircnet &n = *ircnets.add(new ircnet);
     n.type = type;
-    n.state = IRC_NEW;
-    n.sock = ENET_SOCKET_NULL;
     n.port = port;
-    n.lastattempt = 0;
-    #ifndef STANDALONE
-    n.updated = IRCUP_NEW;
-    #endif
     copystring(n.name, name);
     copystring(n.serv, serv);
     copystring(n.nick, nick);
@@ -289,14 +344,14 @@ ICOMMAND(0, ircpass, "ss", (const char *name, const char *s), {
 ICOMMAND(0, ircauth, "sss", (const char *name, const char *s, const char *t), {
     ircnet *n = ircfind(name);
     if(!n) { conoutf("no such ircnet: %s", name); return; }
-    if(!s || !*s || !t || !*t) { ircprintf(n, 4, NULL, "current authority is: %s (%s)", n->authname, n->authpass && *n->authpass ? "<set>" : "<not set>"); return; }
+    if(!s || !*s || !t || !*t) { ircprintf(n, 4, NULL, "current auth details are: %s (%s)", n->authname, n->authpass && *n->authpass ? "<set>" : "<not set>"); return; }
     copystring(n->authname, s);
     copystring(n->authpass, t);
 });
 ICOMMAND(0, ircconnect, "s", (const char *name), {
     ircnet *n = ircfind(name);
     if(!n) { conoutf("no such ircnet: %s", name); return; }
-    if(n->state != IRC_DISC) { ircprintf(n, 4, NULL, "network already already active"); return; }
+    if(n->state > IRC_DISC) { ircprintf(n, 4, NULL, "network already already active"); return; }
     ircestablish(n);
 });
 
@@ -421,13 +476,17 @@ void ircprocess(ircnet *n, char *user[3], int g, int numargs, char *w[])
                     if(ismsg)
                     {
                         if(!strcasecmp(q, "ACTION"))
-                            ircprintf(n, 1, g ? w[g+1] : NULL, "\fv* %s %s", user[0], r);
+                        {
+                            string str = "";
+                            irc2cube(str, r);
+                            ircprintf(n, 1, g ? w[g+1] : NULL, "\fv* %s %s", user[0], str);
+                        }
                         else
                         {
                             ircprintf(n, 4, g ? w[g+1] : NULL, "\fr%s requests: %s %s", user[0], q, r);
 
                             if(!strcasecmp(q, "VERSION"))
-                                ircsend(n, "NOTICE %s :\vVERSION %s v%s-%s %d bit (%s), %s\v", user[0], versionname, versionstring, CUR_PLATFORM, CUR_ARCH, versionrelease, versionurl);
+                                ircsend(n, "NOTICE %s :\vVERSION %s v%s-%s%d (%s)%s%s\v", user[0], VERSION_NAME, VERSION_STRING, versionplatname, versionarch, VERSION_RELEASE, *VERSION_URL ? ", " : "", VERSION_URL);
                             else if(!strcasecmp(q, "PING")) // eh, echo back
                                 ircsend(n, "NOTICE %s :\vPING %s\v", user[0], r);
                         }
@@ -438,13 +497,22 @@ void ircprocess(ircnet *n, char *user[3], int g, int numargs, char *w[])
             }
             else if(ismsg)
             {
+                string str = "";
                 if(n->type == IRCT_RELAY && g && strcasecmp(w[g+1], n->nick) && !strncasecmp(w[g+2], n->nick, strlen(n->nick)))
                 {
                     const char *p = &w[g+2][strlen(n->nick)];
                     while(p && (*p == ':' || *p == ';' || *p == ',' || *p == '.' || *p == ' ' || *p == '\t')) p++;
-                    if(p && *p) ircprintf(n, 0, w[g+1], "\fa<\fw%s\fa>\fw %s", user[0], p);
+                    if(p && *p)
+                    {
+                        irc2cube(str, p);
+                        ircprintf(n, 0, w[g+1], "\fa<\fw%s\fa>\fw %s", user[0], str);
+                    }
+                }
+                else
+                {
+                    irc2cube(str, w[g+2]);
+                    ircprintf(n, 1, g ? w[g+1] : NULL, "\fa<\fw%s\fa>\fw %s", user[0], str);
                 }
-                else ircprintf(n, 1, g ? w[g+1] : NULL, "\fa<\fw%s\fa>\fw %s", user[0], w[g+2]);
             }
             else ircprintf(n, 2, g ? w[g+1] : NULL, "\fo-%s- %s", user[0], w[g+2]);
         }
@@ -510,7 +578,7 @@ void ircprocess(ircnet *n, char *user[3], int g, int numargs, char *w[])
     {
         if(numargs > g+2)
         {
-            mkstring(modestr);
+            string modestr = "";
             loopi(numargs-g-2)
             {
                 if(i) concatstring(modestr, " ");
@@ -534,10 +602,16 @@ void ircprocess(ircnet *n, char *user[3], int g, int numargs, char *w[])
             ircsend(n, "PONG %d", clocktime);
         }
     }
+    else if(!strcasecmp(w[g], "ERROR"))
+    {
+        if(numargs > g+1) ircprintf(n, 4, NULL, "%s ERROR %s", user[0], w[g+1]);
+        else ircprintf(n, 4, NULL, "%s ERROR", user[0]);
+        n->state = IRC_QUIT;
+    }
     else
     {
         int numeric = *w[g] && *w[g] >= '0' && *w[g] <= '9' ? atoi(w[g]) : 0, off = 0;
-        mkstring(s);
+        string s = "";
         #define irctarget(a) (!strcasecmp(n->nick, a) || *a == '#' || ircfindchan(n, a))
         char *targ = numargs > g+1 && irctarget(w[g+1]) ? w[g+1] : NULL;
         if(numeric)
@@ -663,7 +737,7 @@ void ircparse(ircnet *n)
             loopi(3) DELETEA(user[i]);
         }
     cleanup:
-        loopi(MAXWORDS) DELETEA(w[i]);
+        loopi(numargs) DELETEA(w[i]);
     }
     int parsed = start - (char *)n->input;
     if(parsed > 0)
@@ -677,12 +751,14 @@ void ircparse(ircnet *n)
 void ircdiscon(ircnet *n, const char *msg = NULL)
 {
     if(!n) return;
-    if(msg) ircprintf(n, 4, NULL, "disconnected from %s (%s:[%d]): %s", n->name, n->serv, n->port, msg);
+    if(n->state == IRC_WAIT) ircprintf(n, 4, NULL, "could not connect to %s:[%d]", n->serv, n->port);
+    else if(msg) ircprintf(n, 4, NULL, "disconnected from %s (%s:[%d]): %s", n->name, n->serv, n->port, msg);
     else ircprintf(n, 4, NULL, "disconnected from %s (%s:[%d])", n->name, n->serv, n->port);
     enet_socket_destroy(n->sock);
     n->state = IRC_DISC;
     n->sock = ENET_SOCKET_NULL;
     n->lastattempt = clocktime;
+    n->lastactivity = n->lastping = 0;
 }
 
 void irccleanup()
@@ -690,18 +766,74 @@ void irccleanup()
     loopv(ircnets) if(ircnets[i]->sock != ENET_SOCKET_NULL)
     {
         ircnet *n = ircnets[i];
-        ircsend(n, "QUIT :%s, %s", versionname, versionurl);
+        ircsend(n, "QUIT :%s%s%s", VERSION_NAME, *VERSION_URL ? ", " : "", VERSION_URL);
         ircdiscon(n, "shutdown");
     }
 }
 
+bool ircaddsockets(ENetSocket &maxsock, ENetSocketSet &readset, ENetSocketSet &writeset)
+{
+    int numsocks = 0;
+    loopv(ircnets)
+    {
+        ircnet *n = ircnets[i];
+        if(n->sock != ENET_SOCKET_NULL && n->state > IRC_DISC) switch(n->state)
+        {
+            case IRC_WAIT:
+                ENET_SOCKETSET_ADD(writeset, n->sock);
+                // fall-through
+            case IRC_ONLINE: case IRC_CONN: case IRC_QUIT:
+                maxsock = maxsock == ENET_SOCKET_NULL ? n->sock : max(maxsock, n->sock);
+                ENET_SOCKETSET_ADD(readset, n->sock);
+                numsocks++;
+                break;
+        }
+    }
+    return numsocks > 0;
+}
+
+void ircchecksockets(ENetSocketSet &readset, ENetSocketSet &writeset)
+{
+    loopv(ircnets)
+    {
+        ircnet *n = ircnets[i];
+        if(n->sock != ENET_SOCKET_NULL && n->state > IRC_DISC) switch(n->state)
+        {
+            case IRC_WAIT:
+                if(ENET_SOCKETSET_CHECK(readset, n->sock) || ENET_SOCKETSET_CHECK(writeset, n->sock))
+                {
+                    int error = 0;
+                    if(enet_socket_get_option(n->sock, ENET_SOCKOPT_ERROR, &error) < 0 || error) ircdiscon(n);
+                    else n->state = IRC_ATTEMPT;
+                }
+                break;
+            case IRC_ONLINE: case IRC_CONN: case IRC_QUIT:
+                if(ENET_SOCKETSET_CHECK(readset, n->sock)) switch(ircrecv(n))
+                {
+                    case -3: ircdiscon(n, "read error"); break;
+                    case -2: ircdiscon(n, "connection reset"); break;
+                    case -1: ircdiscon(n, "invalid connection"); break;
+                    case 0: break;
+                    default:
+                    {
+                        ircparse(n);
+                        n->lastactivity = clocktime;
+                        n->lastping = 0;
+                        break;
+                    }
+                }
+                break;
+         }
+    }
+}
+
 void ircslice()
 {
     #ifndef STANDALONE
     loopvrev(ircnets)
     {
         ircnet *n = ircnets[i];
-        if((n->sock == ENET_SOCKET_NULL || n->state == IRC_DISC) && n->updated&IRCUP_LEAVE)
+        if((n->sock == ENET_SOCKET_NULL || n->state <= IRC_DISC) && n->updated&IRCUP_LEAVE)
         {
             delete n;
             ircnets.remove(i);
@@ -722,11 +854,16 @@ void ircslice()
         {
             switch(n->state)
             {
+                case IRC_WAIT:
+                {
+                    if(!n->lastattempt || clocktime-n->lastattempt >= 60) ircdiscon(n);
+                    break;
+                }
                 case IRC_ATTEMPT:
                 {
                     if(*n->passkey) ircsend(n, "PASS %s", n->passkey);
                     ircsend(n, "NICK %s", n->nick);
-                    ircsend(n, "USER %s +iw %s :%s v%s-%s %d bit (%s)", versionuname, versionuname, versionname, versionstring, CUR_PLATFORM, CUR_ARCH, versionrelease);
+                    ircsend(n, "USER %s +iw %s :%s v%s-%s%d (%s)", VERSION_UNAME, VERSION_UNAME, VERSION_NAME, VERSION_STRING, versionplatname, versionarch, VERSION_RELEASE);
                     n->state = IRC_CONN;
                     loopvj(n->channels)
                     {
@@ -749,22 +886,26 @@ void ircslice()
                 }
                 case IRC_CONN:
                 {
-                    if(n->state == IRC_CONN && clocktime-n->lastattempt >= 60) ircdiscon(n, "connection attempt timed out");
-                    else switch(ircrecv(n))
+                    if(n->state == IRC_CONN && (!n->lastattempt || clocktime-n->lastattempt >= 60))
+                        ircdiscon(n, "connection attempt timed out");
+                    if(!n->lastactivity)
                     {
-                        case -3: ircdiscon(n, "read error"); break;
-                        case -2: ircdiscon(n, "connection reset"); break;
-                        case -1: ircdiscon(n, "invalid connection"); break;
-                        case 0: break;
-                        default: ircparse(n); break;
+                        n->lastactivity = clocktime;
+                        n->lastping = 0;
+                    }
+                    else if(clocktime-n->lastactivity >= 120)
+                    {
+                        if(!n->lastping)
+                        {
+                            ircsend(n, "PING %d", clocktime);
+                            n->lastping = clocktime;
+                        }
+                        else if(clocktime-n->lastping >= 120) ircdiscon(n, "connection timed out");
                     }
                     break;
                 }
-                default:
-                {
-                    ircdiscon(n, "encountered unknown connection state");
-                    break;
-                }
+                case IRC_QUIT: ircdiscon(n, "closing link"); break;
+                default: ircdiscon(n, "encountered unknown connection state"); break;
             }
         }
         else if(!n->lastattempt || clocktime-n->lastattempt >= 60) ircestablish(n);
@@ -894,7 +1035,7 @@ bool ircnetgui(guient *g, ircnet *n, bool tab)
     return true;
 }
 
-static const char *ircstates[IRC_MAX] = { "\froffline", "\foconnecting", "\fynegotiating", "\fgonline" };
+static const char * const ircstates[IRC_MAX] = { "\fowaiting", "\froffline", "\foconnecting", "\fynegotiating", "\fgonline", "\foquitting" };
 bool ircgui(guient *g, const char *s)
 {
     g->strut(94);
@@ -905,7 +1046,7 @@ bool ircgui(guient *g, const char *s)
         {
             if(!ircnetgui(g, n, false)) return false;
         }
-        else g->textf("not currently connected to %s", 0xFFFFFF, NULL, 0, s);
+        else g->textf("not currently connected to %s", 0xFFFFFF, NULL, 0, -1, s);
     }
     else
     {
@@ -914,9 +1055,9 @@ bool ircgui(guient *g, const char *s)
         {
             ircnet *n = ircnets[i];
             uilist(*g, {
-                g->buttonf("%s via %s:[%d]", 0xFFFFFF, NULL, 0, true, n->name, n->serv, n->port);
+                g->buttonf("%s via %s:[%d]", 0xFFFFFF, NULL, 0, -1, true, n->name, n->serv, n->port);
                 g->space(1);
-                g->buttonf("\fs%s\fS as %s", 0xFFFFFF, NULL, 0, true, ircstates[n->state], n->nick);
+                g->buttonf("\fs%s\fS as %s", 0xFFFFFF, NULL, 0, -1, true, ircstates[n->state], n->nick);
             });
             nets++;
         }
diff --git a/src/engine/irc.h b/src/engine/irc.h
index 5e9b683..1be98f5 100644
--- a/src/engine/irc.h
+++ b/src/engine/irc.h
@@ -54,10 +54,10 @@ struct ircchan
     }
 };
 enum { IRCT_NONE = 0, IRCT_CLIENT, IRCT_RELAY, IRCT_MAX };
-enum { IRC_NEW = 0, IRC_DISC, IRC_ATTEMPT, IRC_CONN, IRC_ONLINE, IRC_MAX };
+enum { IRC_NEW = 0, IRC_DISC, IRC_WAIT, IRC_ATTEMPT, IRC_CONN, IRC_ONLINE, IRC_QUIT, IRC_MAX };
 struct ircnet
 {
-    int type, state, port, lastattempt, inputcarry, inputlen;
+    int type, state, port, lastattempt, lastactivity, lastping, inputcarry, inputlen;
     string name, serv, nick, ip, passkey, authname, authpass;
     ENetAddress address;
     ENetSocket sock;
@@ -68,19 +68,26 @@ struct ircnet
     ircbuf buffer;
 #endif
 
-    ircnet() { reset(); }
-    ~ircnet() { reset(); }
+    ircnet() { reset(true); }
+    ~ircnet() { reset(true); }
 
-    void reset()
+    void reset(bool start = false)
     {
-        type = IRCT_NONE;
-        state = IRC_DISC;
+        if(start)
+        {
+            type = IRCT_NONE;
+            state = IRC_NEW;
+            sock = ENET_SOCKET_NULL;
+            address.host = ENET_HOST_ANY;
+            address.port = 6667;
+        }
+        else state = IRC_DISC;
         inputcarry = inputlen = 0;
-        port = lastattempt = 0;
+        port = lastattempt = lastactivity = lastping = 0;
         name[0] = serv[0] = nick[0] = ip[0] = passkey[0] = authname[0] = authpass[0] = 0;
         channels.shrink(0);
 #ifndef STANDALONE
-        updated = 0;
+        updated = IRCUP_NEW;
         buffer.reset();
 #endif
     }
@@ -99,6 +106,7 @@ extern bool ircjoin(ircnet *n, ircchan *c);
 extern bool ircenterchan(ircnet *n, const char *name);
 extern bool ircnewchan(int type, const char *name, const char *channel, const char *friendly = "", const char *passkey = "", int relay = 0);
 extern void ircparse(ircnet *n);
-extern void ircdiscon(ircnet *n);
 extern void irccleanup();
+extern bool ircaddsockets(ENetSocket &maxsock, ENetSocketSet &readset, ENetSocketSet &writeset);
+extern void ircchecksockets(ENetSocketSet &readset, ENetSocketSet &writeset);
 extern void ircslice();
diff --git a/src/engine/lensflare.h b/src/engine/lensflare.h
index 8bc71d7..7d0e1e8 100644
--- a/src/engine/lensflare.h
+++ b/src/engine/lensflare.h
@@ -24,7 +24,7 @@ struct flare
 {
     vec o, center;
     float size;
-    uchar color[3];
+    bvec color;
     int sparkle; // 0 = off, 1 = sparkles and flares, 2 = only sparkles
 };
 
@@ -43,10 +43,15 @@ struct flarerenderer : partrenderer
     flare *flares;
 
     flarerenderer(const char *texname, int maxflares)
-        : partrenderer(texname, 3, PT_FLARE), maxflares(maxflares), shinetime(0)
+        : partrenderer(texname, 3, PT_FLARE), maxflares(maxflares), numflares(0), shinetime(0)
     {
         flares = new flare[maxflares];
     }
+    ~flarerenderer()
+    {
+        delete[] flares;
+    }
+
     void reset()
     {
         numflares = 0;
@@ -170,13 +175,13 @@ struct flarerenderer : partrenderer
         glBegin(GL_QUADS);
         loopi(numflares)
         {
-            flare *f = flares+i;
+            const flare &f = flares[i];
             float blend = flareblend;
-            vec center = f->center, axis = vec(f->o).sub(center);
+            vec center = f.center, axis = vec(f.o).sub(center);
             if(flareadjust > 0)
             {
                 float yaw, pitch;
-                vec dir = vec(f->o).sub(camera1->o).normalize();
+                vec dir = vec(f.o).sub(camera1->o).normalize();
                 vectoyawpitch(dir, yaw, pitch);
                 yaw -= camera1->yaw;
                 while(yaw < -180.0f) yaw += 360.0f;
@@ -189,24 +194,24 @@ struct flarerenderer : partrenderer
                 if(pitch < 0) pitch = -pitch;
                 blend *= 1-min(pitch/(fovy*0.5f)*flareadjust, 1.f);
             }
-            uchar color[4] = {f->color[0], f->color[1], f->color[2], 255};
-            loopj(f->sparkle ? (f->sparkle != 2 ? 12 : 3) : 9)
+            bvec4 color(f.color, 255);
+            loopj(f.sparkle ? (f.sparkle != 2 ? 12 : 3) : 9)
             {
-                int q = f->sparkle != 2 ? j : j+9;
+                int q = f.sparkle != 2 ? j : j+9;
                 const flaretype &ft = flaretypes[q];
                 vec o = vec(axis).mul(ft.loc).add(center);
-                float sz = ft.scale*f->size;
+                float sz = ft.scale*f.size;
                 int tex = ft.type;
                 if(ft.type < 0) //sparkles - always done last
                 {
                     tex = 6+((shinetime+1)%10);
-                    color[0] = 0;
-                    color[1] = 0;
-                    color[2] = 0;
-                    color[-ft.type-1] = f->color[-ft.type-1]; //only want a single channel
+                    color.r = 0;
+                    color.g = 0;
+                    color.b = 0;
+                    color[-ft.type-1] = f.color[-ft.type-1]; //only want a single channel
                 }
-                color[3] = uchar(ceilf(ft.alpha*blend));
-                glColor4ubv(color);
+                color.a = uchar(ceilf(ft.alpha*blend));
+                glColor4ubv(color.v);
                 const float tsz = 0.25f; //flares are aranged in 4x4 grid
                 float tx = tsz*(tex&0x03);
                 float ty = tsz*((tex>>2)&0x03);
diff --git a/src/engine/lightmap.cpp b/src/engine/lightmap.cpp
index 8e20a0d..9543789 100644
--- a/src/engine/lightmap.cpp
+++ b/src/engine/lightmap.cpp
@@ -547,9 +547,9 @@ static uint generatelumel(lightmapworker *w, const float tolerance, uint lightma
             if(avgray.iszero()) break;
             // transform to tangent space
             extern vec orientation_tangent[6][3];
-            extern vec orientation_binormal[6][3];
+            extern vec orientation_bitangent[6][3];
             vec S(orientation_tangent[w->rotate][dimension(w->orient)]),
-                T(orientation_binormal[w->rotate][dimension(w->orient)]);
+                T(orientation_bitangent[w->rotate][dimension(w->orient)]);
             normal.orthonormalize(S, T);
             avgray.normalize();
             w->raydata[y*w->w+x].add(vec(S.dot(avgray)/S.magnitude(), T.dot(avgray)/T.magnitude(), normal.dot(avgray)));
diff --git a/src/engine/lightmap.h b/src/engine/lightmap.h
index dfad432..c471ab1 100644
--- a/src/engine/lightmap.h
+++ b/src/engine/lightmap.h
@@ -70,6 +70,7 @@ struct LightMap
 };
 
 extern vector<LightMap> lightmaps;
+extern GLuint lmprogtex;
 
 struct LightMapTexture
 {
@@ -139,6 +140,6 @@ extern volatile bool check_calclight_lmprog;
 extern void check_calclight_canceled();
 extern void fixlightmapnormals();
 extern void fixrotatedlightmaps();
- 
+
 extern int lightmapping;
 
diff --git a/src/engine/lightning.h b/src/engine/lightning.h
index bf9c845..85ae5fb 100644
--- a/src/engine/lightning.h
+++ b/src/engine/lightning.h
@@ -89,8 +89,8 @@ static void renderlightning(Texture *tex, const vec &o, const vec &d, float sz,
 
 struct lightningrenderer : sharedlistrenderer
 {
-    lightningrenderer()
-        : sharedlistrenderer("<grey>particles/lightning", 2, PT_LIGHTNING|PT_GLARE)
+    lightningrenderer(const char *texname)
+        : sharedlistrenderer(texname, 2, PT_LIGHTNING|PT_GLARE)
     {}
 
     void startrender()
@@ -129,4 +129,4 @@ struct lightningrenderer : sharedlistrenderer
         renderlightning(tex, p->o, p->d, size, midcol, endcol);
     }
 };
-static lightningrenderer lightnings;
+static lightningrenderer lightnings("<grey>particles/lightning"), lightzaps("<grey>particles/lightzap");
diff --git a/src/engine/main.cpp b/src/engine/main.cpp
index 473bb5c..39cdc04 100644
--- a/src/engine/main.cpp
+++ b/src/engine/main.cpp
@@ -1,5 +1,4 @@
 // main.cpp: initialisation & main loop
-// main.cpp: initialisation & main loop
 
 #include "engine.h"
 #include <signal.h>
@@ -36,13 +35,14 @@ void showcursor(bool show)
 #endif
 }
 
-void setcaption(const char *text)
+void setcaption(const char *text, const char *text2)
 {
-    static string caption = "";
-    defformatstring(newcaption)("%s v%s-%s %d bit (%s)%s%s", versionname, versionstring, CUR_PLATFORM, CUR_ARCH, versionrelease, text ? ": " : "", text ? text : "");
-    if(strcmp(caption, newcaption))
+    static string prevtext = "", prevtext2 = "";
+    if(strcmp(text, prevtext) || strcmp(text2, prevtext2))
     {
-        copystring(caption, newcaption);
+        copystring(prevtext, text);
+        copystring(prevtext2, text2);
+        defformatstring(caption)("%s v%s-%s%d (%s)%s%s%s%s", VERSION_NAME, VERSION_STRING, versionplatname, versionarch, VERSION_RELEASE, text[0] ? ": " : "", text, text2[0] ? " - " : "", text2);
         SDL_WM_SetCaption(caption, NULL);
     }
 }
@@ -65,22 +65,12 @@ void inputgrab(bool on)
 VARF(0, grabinput, 0, 0, 1, inputgrab(grabinput!=0));
 VAR(IDF_PERSIST, autograbinput, 0, 1, 1);
 
-void setscreensaver(bool active)
-{
-#ifdef WIN32
-    SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, active ? 1 : 0, NULL, 0);
-    SystemParametersInfo(SPI_SETLOWPOWERACTIVE, active ? 1 : 0, NULL, 0);
-    SystemParametersInfo(SPI_SETPOWEROFFACTIVE, active ? 1 : 0, NULL, 0);
-#endif
-}
-
 extern void cleargamma();
 
 void cleanup()
 {
     recorder::stop();
     cleanupserver();
-    setscreensaver(true);
     showcursor(true);
 //#ifdef FAKESHOWCURSOR
 //    if(scursor) SDL_FreeCursor(scursor);
@@ -116,14 +106,13 @@ void fatal(const char *s, ...)    // failure exit
 {
     if(++errors <= 2) // print up to one extra recursive error
     {
-        defvformatstring(msg, s, s);
+        defvformatbigstring(msg, s, s);
         if(logfile) logoutf("%s", msg);
         #ifndef WIN32
         fprintf(stderr, "%s\n", msg);
         #endif
         if(errors <= 1) // avoid recursion
         {
-            setscreensaver(true);
             if(SDL_WasInit(SDL_INIT_VIDEO))
             {
                 showcursor(true);
@@ -131,7 +120,7 @@ void fatal(const char *s, ...)    // failure exit
                 cleargamma();
             }
             #ifdef WIN32
-            defformatstring(cap)("%s: Error", versionname);
+            defformatstring(cap)("%s: Error", VERSION_NAME);
             MessageBox(NULL, msg, cap, MB_OK|MB_SYSTEMMODAL);
             #endif
             SDL_Quit();
@@ -217,10 +206,10 @@ void writeinitcfg()
     f->printf("shaders %d\n", useshaders);
     f->printf("shaderprecision %d\n", shaderprecision);
     f->printf("forceglsl %d\n", forceglsl);
-    extern int soundmono, soundchans, soundbufferlen, soundfreq;
+    extern int soundmono, soundmixchans, soundbuflen, soundfreq;
     f->printf("soundmono %d\n", soundmono);
-    f->printf("soundchans %d\n", soundchans);
-    f->printf("soundbufferlen %d\n", soundbufferlen);
+    f->printf("soundmixchans %d\n", soundmixchans);
+    f->printf("soundbuflen %d\n", soundbuflen);
     f->printf("soundfreq %d\n", soundfreq);
     f->printf("verbose %d\n", verbose);
     delete f;
@@ -236,12 +225,12 @@ void screenshot(char *sname)
     glReadPixels(0, 0, screen->w, screen->h, GL_RGB, GL_UNSIGNED_BYTE, image.data);
     string fname;
     if(sname && *sname) copystring(fname, sname);
-    else formatstring(fname)("screenshots/%s", *filetimeformat ? gettime(clocktime, filetimeformat) : (*mapname ? mapname : "screen"));
+    else formatstring(fname)("screenshots/%s", *filetimeformat ? gettime(filetimelocal ? currenttime : clocktime, filetimeformat) : (*mapname ? mapname : "screen"));
     saveimage(fname, image, imageformat, compresslevel, true);
 }
 
-COMMAND(0, screenshot, "s");
-COMMAND(0, quit, "");
+ICOMMAND(0, screenshot, "s", (char *s), if(!(identflags&IDF_WORLD)) screenshot(s));
+ICOMMAND(0, quit, "", (void), if(!(identflags&IDF_WORLD)) quit());
 
 void setfullscreen(bool enable, bool force)
 {
@@ -258,7 +247,7 @@ void setfullscreen(bool enable, bool force)
 #endif
 }
 
-VARF(0, fullscreen, 0, 1, 1, setfullscreen(fullscreen!=0));
+VARF(0, fullscreen, 0, 1, 1, if(!(identflags&IDF_WORLD)) setfullscreen(fullscreen!=0));
 
 void screenres(int *w, int *h)
 {
@@ -476,7 +465,7 @@ void resetgl()
     extern void reloadshaders();
 
     inbetweenframes = false;
-    if(!reloadtexture("textures/notexture") || !reloadtexture("textures/blank") || !reloadtexture("textures/logo") || !reloadtexture("textures/cube2badge"))
+    if(!reloadtexture(notexturetex) || !reloadtexture(blanktex) || !reloadtexture(logotex) || !reloadtexture(badgetex))
         fatal("failed to reload core textures");
     reloadfonts();
     inbetweenframes = true;
@@ -488,7 +477,7 @@ void resetgl()
     allchanged(true);
 }
 
-COMMAND(0, resetgl, "");
+ICOMMAND(0, resetgl, "", (void), if(!(identflags&IDF_WORLD)) resetgl());
 
 bool activewindow = true, warping = false, minimized = false;
 
@@ -655,9 +644,9 @@ void checkinput()
     }
 }
 
-void swapbuffers()
+void swapbuffers(bool overlay)
 {
-    recorder::capture();
+    recorder::capture(overlay);
     SDL_GL_SwapBuffers();
 }
 
@@ -693,14 +682,14 @@ void stackdumper(unsigned int type, EXCEPTION_POINTERS *ep)
     if(!ep) fatal("unknown type");
     EXCEPTION_RECORD *er = ep->ExceptionRecord;
     CONTEXT *context = ep->ContextRecord;
-    string out, t;
-    formatstring(out)("%s Win32 Exception: 0x%x [0x%x]\n\n", versionname, er->ExceptionCode, er->ExceptionCode==EXCEPTION_ACCESS_VIOLATION ? er->ExceptionInformation[1] : -1);
+    bigstring out, t;
+    formatbigstring(out)("%s Win32 Exception: 0x%x [0x%x]\n\n", VERSION_NAME, er->ExceptionCode, er->ExceptionCode==EXCEPTION_ACCESS_VIOLATION ? er->ExceptionInformation[1] : -1);
     SymInitialize(GetCurrentProcess(), NULL, TRUE);
 #ifdef _AMD64_
     STACKFRAME64 sf = {{context->Rip, 0, AddrModeFlat}, {}, {context->Rbp, 0, AddrModeFlat}, {context->Rsp, 0, AddrModeFlat}, 0};
     while(::StackWalk64(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(), GetCurrentThread(), &sf, context, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL))
     {
-        union { IMAGEHLP_SYMBOL64 sym; char symext[sizeof(IMAGEHLP_SYMBOL64) + sizeof(string)]; };
+        union { IMAGEHLP_SYMBOL64 sym; char symext[sizeof(IMAGEHLP_SYMBOL64) + sizeof(bigstring)]; };
         sym.SizeOfStruct = sizeof(sym);
         sym.MaxNameLength = sizeof(symext) - sizeof(sym);
         IMAGEHLP_LINE64 line;
@@ -712,7 +701,7 @@ void stackdumper(unsigned int type, EXCEPTION_POINTERS *ep)
     STACKFRAME sf = {{context->Eip, 0, AddrModeFlat}, {}, {context->Ebp, 0, AddrModeFlat}, {context->Esp, 0, AddrModeFlat}, 0};
     while(::StackWalk(IMAGE_FILE_MACHINE_I386, GetCurrentProcess(), GetCurrentThread(), &sf, context, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL))
     {
-        union { IMAGEHLP_SYMBOL sym; char symext[sizeof(IMAGEHLP_SYMBOL) + sizeof(string)]; };
+        union { IMAGEHLP_SYMBOL sym; char symext[sizeof(IMAGEHLP_SYMBOL) + sizeof(bigstring)]; };
         sym.SizeOfStruct = sizeof(sym);
         sym.MaxNameLength = sizeof(symext) - sizeof(sym);
         IMAGEHLP_LINE line;
@@ -722,8 +711,8 @@ void stackdumper(unsigned int type, EXCEPTION_POINTERS *ep)
 #endif
         {
             char *del = strrchr(line.FileName, '\\');
-            formatstring(t)("%s - %s [%d]\n", sym.Name, del ? del + 1 : line.FileName, line.LineNumber);
-            concatstring(out, t);
+            formatbigstring(t)("%s - %s [%d]\n", sym.Name, del ? del + 1 : line.FileName, line.LineNumber);
+            concatbigstring(out, t);
         }
     }
     fatal(out);
@@ -819,7 +808,7 @@ void progress(float bar1, const char *text1, float bar2, const char *text2)
     interceptkey(SDLK_UNKNOWN); // keep the event queue awake to avoid 'beachball' cursor
     #endif
 
-    setsvar("progresstitle", text1 ? text1 : "loading..");
+    setsvar("progresstitle", text1 ? text1 : "please wait..");
     setfvar("progressamt", bar1);
     setsvar("progresstext", text2 ? text2 : "");
     setfvar("progresspart", bar2);
@@ -831,10 +820,36 @@ void progress(float bar1, const char *text1, float bar2, const char *text2)
     }
 
     progressing = true;
-    loopi(2) { drawnoview(); swapbuffers(); }
+    loopi(2) { drawnoview(); swapbuffers(false); }
     progressing = false;
 }
 
+
+bool pixeling = false;
+bvec pixel(0, 0, 0);
+char *pixelact = NULL;
+
+ICOMMAND(0, printpixel, "", (void), conoutft(CON_SELF, "pixel = 0x%.6X (%d, %d, %d)", pixel.tohexcolor(), pixel.r, pixel.g, pixel.b));
+ICOMMAND(0, getpixel, "i", (int *n), {
+    switch(*n)
+    {
+        case 1: intret(pixel.r); break;
+        case 2: intret(pixel.g); break;
+        case 3: intret(pixel.b); break;
+        case 0: default: intret(pixel.tohexcolor()); break;
+    }
+});
+
+void readpixel(char *act)
+{
+    if(pixeling) return;
+    if(!editmode) { conoutf("\froperation only allowed in edit mode"); return; }
+    if(pixelact) delete[] pixelact;
+    pixelact = act && *act ? newstring(act) : NULL;
+    pixeling = true;
+}
+ICOMMAND(0, readpixel, "s", (char *act), readpixel(act));
+
 VAR(0, numcpus, 1, 1, 16);
 
 int main(int argc, char **argv)
@@ -848,7 +863,9 @@ int main(int argc, char **argv)
     #endif
     #endif
 
-    clocktime = time(NULL); // initialise
+    currenttime = time(NULL); // initialise
+    clocktime = mktime(gmtime(&currenttime));
+    clockoffset = currenttime-clocktime;
 
     setlogfile(NULL);
     setlocations(true);
@@ -863,6 +880,7 @@ int main(int argc, char **argv)
         }
     }
     setlogfile("log.txt");
+    setcrc(argv[0]);
     execfile("init.cfg", false);
     for(int i = 1; i<argc; i++)
     {
@@ -923,6 +941,13 @@ int main(int argc, char **argv)
     initgame();
 
     conoutf("loading sdl..");
+    #ifdef WIN32
+    SetEnvironmentVariable("SDL_DISABLE_LOCK_KEYS", "1");
+    SetEnvironmentVariable("SDL_VIDEO_ALLOW_SCREENSAVER", "0");
+    #else
+    setenv("SDL_DISABLE_LOCK_KEYS", "1", 1);
+    setenv("SDL_VIDEO_ALLOW_SCREENSAVER", "0", 1);
+    #endif
     int par = 0;
     #ifdef _DEBUG
     par = SDL_INIT_NOPARACHUTE;
@@ -936,9 +961,9 @@ int main(int argc, char **argv)
     //#endif
 
     par |= SDL_INIT_TIMER|SDL_INIT_VIDEO|SDL_INIT_NOPARACHUTE;
-    if(SDL_Init(par) < 0) fatal("Unable to initialize SDL: %s", SDL_GetError());
+    if(SDL_Init(par) < 0) fatal("error initialising SDL: %s", SDL_GetError());
 
-    conoutf("loading video mode..");
+    conoutf("loading video..");
     const SDL_VideoInfo *video = SDL_GetVideoInfo();
     if(video)
     {
@@ -948,9 +973,7 @@ int main(int argc, char **argv)
     int usedcolorbits = 0, useddepthbits = 0, usedfsaa = 0;
     setupscreen(usedcolorbits, useddepthbits, usedfsaa);
 
-    conoutf("loading video misc..");
     showcursor(false);
-    setscreensaver(false);
     keyrepeat(false);
     setcaption("please wait..");
 
@@ -971,15 +994,15 @@ int main(int argc, char **argv)
     conoutf("loading gl..");
     gl_checkextensions();
     gl_init(scr_w, scr_h, usedcolorbits, useddepthbits, usedfsaa);
-    if(!(notexture = textureload("textures/notexture")) ||
-        !(blanktexture = textureload("textures/blank")))
+    if(!(notexture = textureload(notexturetex)) || !(blanktexture = textureload(blanktex)))
         fatal("could not find core textures");
 
     conoutf("loading sound..");
     initsound();
 
-    conoutf("loading defaults..");
+    game::start();
 
+    conoutf("loading defaults..");
     if(!execfile("config/stdlib.cfg", false)) fatal("cannot find data files");
     if(!setfont("default")) fatal("no default font specified");
     inbetweenframes = true;
@@ -1015,11 +1038,12 @@ int main(int argc, char **argv)
     if(SDL_GetWMInfo(&wminfo)) ShowWindow(wminfo.window, SW_SHOW);
 #endif
     if(autograbinput) setvar("grabinput", 1, true);
+    capslockon = capslocked();
+    numlockon = numlocked();
     ignoremousemotion();
 
     localconnect(false);
     resetfps();
-    UI::setup();
 
     for(int frameloops = 0; ; frameloops = frameloops >= INT_MAX-1 ? MAXFPSHISTORY+1 : frameloops+1)
     {
@@ -1054,8 +1078,25 @@ int main(int argc, char **argv)
                 swapbuffers();
                 inbetweenframes = true;
             }
-            defformatstring(cap)("%s - %s", game::gametitle(), game::gametext());
-            setcaption(cap);
+            if(pixeling)
+            {
+                if(editmode)
+                {
+                    glReadPixels(screen->w/2, screen->h/2, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &pixel.v[0]);
+                    if(pixelact) execute(pixelact);
+                }
+                if(pixelact) delete[] pixelact;
+                pixelact = NULL;
+                pixeling = false;
+            }
+            if(*progresstitle || progressamt > 0)
+            {
+                setsvar("progresstitle", "");
+                setsvar("progresstext", "");
+                setfvar("progressamt", 0.f);
+                setfvar("progresspart", 0.f);
+            }
+            setcaption(game::gametitle(), game::gametext());
         }
     }
 
diff --git a/src/engine/master.cpp b/src/engine/master.cpp
index 0f6fc9a..04d32de 100644
--- a/src/engine/master.cpp
+++ b/src/engine/master.cpp
@@ -1,3 +1,11 @@
+#ifdef WIN32
+#define FD_SETSIZE 4096
+#else
+#include <sys/types.h>
+#undef __FD_SETSIZE
+#define __FD_SETSIZE 4096
+#endif
+
 #include "engine.h"
 #include <enet/time.h>
 
@@ -6,8 +14,6 @@
 #define SERVER_TIME (35*60*1000)
 #define AUTH_TIME (30*1000)
 #define DUP_LIMIT 16
-#define PING_TIME 3000
-#define PING_RETRY 5
 
 VAR(0, masterserver, 0, 0, 1);
 VAR(0, masterport, 1, MASTER_PORT, VAR_MAX);
@@ -15,9 +21,12 @@ SVAR(0, masterip, "");
 SVAR(0, masterscriptclient, "");
 SVAR(0, masterscriptserver, "");
 
+VAR(0, masterpingdelay, 1000, 3000, VAR_MAX);
+VAR(0, masterpingtries, 1, 5, VAR_MAX);
+
 struct authuser
 {
-    char *name, *flags;
+    char *name, *flags, *email;
     void *pubkey;
 };
 
@@ -27,6 +36,7 @@ struct authreq
     uint id;
     void *answer;
     authuser *user;
+    string hostname;
 };
 
 struct masterclient
@@ -62,7 +72,7 @@ void setupmaster()
 {
     if(masterserver)
     {
-        conoutf("init: master (%s:%d)", *masterip ? masterip : "*", masterport);
+        conoutf("loading master (%s:%d)..", *masterip ? masterip : "*", masterport);
         ENetAddress address = { ENET_HOST_ANY, enet_uint16(masterport) };
         if(*masterip && enet_address_set_host(&address, masterip) < 0) fatal("failed to resolve master address: %s", masterip);
         if((mastersocket = enet_socket_create(ENET_SOCKET_TYPE_STREAM)) == ENET_SOCKET_NULL) fatal("failed to create master server socket");
@@ -72,8 +82,6 @@ void setupmaster()
         if(enet_socket_set_option(mastersocket, ENET_SOCKOPT_NONBLOCK, 1) < 0) fatal("failed to make master server socket non-blocking");
         if(!setuppingsocket(&address)) fatal("failed to create ping socket");
         starttime = clocktime;
-        char *ct = ctime(&starttime);
-        if(strchr(ct, '\n')) *strchr(ct, '\n') = '\0';
         conoutf("master server started on %s:[%d]", *masterip ? masterip : "localhost", masterport);
     }
 }
@@ -86,29 +94,37 @@ void masterout(masterclient &c, const char *msg, int len = 0)
 
 void masteroutf(masterclient &c, const char *fmt, ...)
 {
-    string msg;
+    bigstring msg;
     va_list args;
     va_start(args, fmt);
-    vformatstring(msg, fmt, args);
+    vformatbigstring(msg, fmt, args);
     va_end(args);
     masterout(c, msg);
 }
 
 static hashtable<char *, authuser> authusers;
 
-void addauth(char *name, char *flags, char *pubkey)
+void addauth(char *name, char *flags, char *pubkey, char *email)
 {
+    string authname;
+    if(filterstring(authname, name, true, true, true, true, 100)) name = authname;
+    if(authusers.access(name))
+    {
+        conoutf("auth handle '%s' already exists, skipping (%s)", name, email);
+        return;
+    }
     name = newstring(name);
     authuser &u = authusers[name];
     u.name = name;
     u.flags = newstring(flags);
     u.pubkey = parsepubkey(pubkey);
+    u.email = newstring(email);
 }
-COMMAND(0, addauth, "sss");
+COMMAND(0, addauth, "ssss");
 
 void clearauth()
 {
-    enumerate(authusers, authuser, u, { delete[] u.name; delete[] u.flags; freepubkey(u.pubkey); });
+    enumerate(authusers, authuser, u, { delete[] u.name; delete[] u.flags; delete[] u.email; freepubkey(u.pubkey); });
     authusers.clear();
 }
 COMMAND(0, clearauth, "");
@@ -129,31 +145,28 @@ void purgeauths(masterclient &c)
     if(expired > 0) c.authreqs.remove(0, expired);
 }
 
-void reqauth(masterclient &c, uint id, char *name)
+void reqauth(masterclient &c, uint id, char *name, char *hostname)
 {
     purgeauths(c);
 
-    char *ct = ctime(&clocktime);
-    if(ct)
-    {
-        char *newline = strchr(ct, '\n');
-        if(newline) *newline = '\0';
-    }
-    string ip;
+    string ip, host;
     if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-");
-    conoutf("%s: attempting \"%s\" as %u from %s\n", ct ? ct : "-", name, id, ip);
+    copystring(host, hostname && *hostname ? hostname : "-");
 
     authuser *u = authusers.access(name);
     if(!u)
     {
         masteroutf(c, "failauth %u\n", id);
+        conoutf("failed '%s' (%u) from %s on server %s (NOTFOUND)\n", name, id, host, ip);
         return;
     }
+    conoutf("attempting '%s' (%u) from %s on server %s\n", name, id, host, ip);
 
     authreq &a = c.authreqs.add();
     a.user = u;
     a.reqtime = totalmillis;
     a.id = id;
+    copystring(a.hostname, host);
     uint seed[3] = { uint(starttime), uint(totalmillis), randomMT() };
     static vector<char> buf;
     buf.setsize(0);
@@ -165,20 +178,19 @@ void reqauth(masterclient &c, uint id, char *name)
 void confauth(masterclient &c, uint id, const char *val)
 {
     purgeauths(c);
-
+    string ip;
+    if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-");
     loopv(c.authreqs) if(c.authreqs[i].id == id)
     {
-        string ip;
-        if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-");
         if(checkchallenge(val, c.authreqs[i].answer))
         {
             masteroutf(c, "succauth %u \"%s\" \"%s\"\n", id, c.authreqs[i].user->name, c.authreqs[i].user->flags);
-            conoutf("succeeded %u (%s [%s]) from %s\n", id, c.authreqs[i].user->name, c.authreqs[i].user->flags, ip);
+            conoutf("succeeded '%s' [%s] (%u) from %s on server %s\n", c.authreqs[i].user->name, c.authreqs[i].user->flags, id, c.authreqs[i].hostname, ip);
         }
         else
         {
             masteroutf(c, "failauth %u\n", id);
-            conoutf("failed %u (%s) from %s\n", id, c.authreqs[i].user->name, ip);
+            conoutf("failed '%s' (%u) from %s on server %s (BADKEY)\n", c.authreqs[i].user->name, id, c.authreqs[i].hostname, ip);
         }
         freechallenge(c.authreqs[i].answer);
         c.authreqs.remove(i--);
@@ -191,7 +203,7 @@ void purgemasterclient(int n)
 {
     masterclient &c = *masterclients[n];
     enet_socket_destroy(c.socket);
-    conoutf("master peer %s disconnected", c.name);
+    if(verbose) conoutf("master peer %s disconnected", c.name);
     delete masterclients[n];
     masterclients.remove(n);
 }
@@ -215,6 +227,8 @@ void checkmasterpongs()
                 c.lastpong = totalmillis ? totalmillis : 1;
                 c.listserver = true;
                 c.shouldping = false;
+                masteroutf(c, "echo \"ping reply confirmed (on port %d), server is now listed\"\n", addr.port);
+                conoutf("master peer %s responded to ping request on port %d successfully",  c.name, addr.port);
                 break;
             }
         }
@@ -285,7 +299,7 @@ bool checkmasterclientinput(masterclient &c)
         bool found = false, server = !strcmp(w[0], "server");
         if((server || !strcmp(w[0], "quick")) && !c.ishttp)
         {
-            c.port = MASTER_PORT;
+            c.port = SERVER_PORT;
             c.lastactivity = totalmillis ? totalmillis : 1;
             if(!server)
             {
@@ -295,24 +309,33 @@ bool checkmasterclientinput(masterclient &c)
             }
             else
             {
-                if(w[1]) c.port = clamp(atoi(w[1]), 1, VAR_MAX);
-                c.shouldping = true;
-                c.numpings = 0;
-                c.lastcontrol = controlversion;
-                loopv(control) if(control[i].flag == ipinfo::LOCAL)
-                    masteroutf(c, "%s %u %u\n", ipinfotypes[control[i].type], control[i].ip, control[i].mask);
-                if(c.isserver)
+                if(w[1] && *w[1]) c.port = clamp(atoi(w[1]), 1, VAR_MAX);
+                ENetAddress address = { ENET_HOST_ANY, enet_uint16(c.port) };
+                if(w[2] && *w[2] && strcmp(w[2], "*") && (enet_address_set_host(&address, w[2]) < 0 || address.host != c.address.host))
                 {
-                    masteroutf(c, "echo \"server updated, sending ping request\"\n");
-                    conoutf("master peer %s updated server info",  c.name);
+                    c.listserver = c.shouldping = false;
+                    masteroutf(c, "echo \"server IP '%s' does not match origin '%s', server will not be listed\n", w[2], c.name);
                 }
                 else
                 {
-                    if(*masterscriptserver) masteroutf(c, "%s\n", masterscriptserver);
-                    masteroutf(c, "echo \"server registered, sending ping request\"\n");
-                    conoutf("master peer %s registered as a server",  c.name);
+                    c.shouldping = true;
+                    c.numpings = 0;
+                    c.lastcontrol = controlversion;
+                    loopv(control) if(control[i].flag == ipinfo::LOCAL)
+                        masteroutf(c, "%s %u %u \"%s\"\n", ipinfotypes[control[i].type], control[i].ip, control[i].mask, control[i].reason);
+                    if(c.isserver)
+                    {
+                        masteroutf(c, "echo \"server updated (port %d), sending ping request (on port %d)\"\n", c.port, c.port+1);
+                        conoutf("master peer %s updated server info (%d)",  c.name, c.port);
+                    }
+                    else
+                    {
+                        if(*masterscriptserver) masteroutf(c, "%s\n", masterscriptserver);
+                        masteroutf(c, "echo \"server registered (port %d), sending ping request (on port %d)\"\n", c.port, c.port+1);
+                        conoutf("master peer %s registered as a server (%d)", c.name, c.port);
+                    }
+                    c.isserver = true;
                 }
-                c.isserver = true;
             }
             found = true;
         }
@@ -340,15 +363,15 @@ bool checkmasterclientinput(masterclient &c)
         }
         if(c.isserver || c.isquick)
         {
-            if(!strcmp(w[0], "reqauth")) { reqauth(c, uint(atoi(w[1])), w[2]); found = true; }
+            if(!strcmp(w[0], "reqauth")) { reqauth(c, uint(atoi(w[1])), w[2], w[3]); found = true; }
             if(!strcmp(w[0], "confauth")) { confauth(c, uint(atoi(w[1])), w[2]); found = true; }
         }
-        if(w[0] && !found)
+        if(w[0] && *w[0] && !found)
         {
             masteroutf(c, "error \"unknown command %s\"\n", w[0]);
             conoutf("master peer %s (client) sent unknown command: %s",  c.name, w[0]);
         }
-        loopj(numargs) if(w[j]) delete[] w[j];
+        loopi(numargs) DELETEA(w[i]);
     }
     c.inputpos = &c.input[c.inputpos] - p;
     memmove(c.input, p, c.inputpos);
@@ -369,9 +392,9 @@ void checkmaster()
     {
         masterclient &c = *masterclients[i];
         if(c.authreqs.length()) purgeauths(c);
-        if(c.shouldping && (!c.lastping || ((!c.lastpong || ENET_TIME_GREATER(c.lastping, c.lastpong)) && ENET_TIME_DIFFERENCE(totalmillis, c.lastping) > PING_TIME)))
+        if(c.shouldping && (!c.lastping || ((!c.lastpong || ENET_TIME_GREATER(c.lastping, c.lastpong)) && ENET_TIME_DIFFERENCE(totalmillis, c.lastping) > uint(masterpingdelay))))
         {
-            if(c.numpings < PING_RETRY)
+            if(c.numpings < masterpingtries)
             {
                 static const uchar ping[] = { 1 };
                 ENetBuffer buf;
@@ -384,15 +407,14 @@ void checkmaster()
             }
             else
             {
-                c.listserver = false;
-                c.shouldping = false;
-                masteroutf(c, "echo \"ping attempts failed, your server will not be listed\n");
+                c.listserver = c.shouldping = false;
+                masteroutf(c, "error \"ping attempts failed (tried %d times on port %d), server will not be listed\"\n", c.numpings, c.port+1);
             }
         }
         if(c.isserver && c.lastcontrol < controlversion)
         {
             loopv(control) if(control[i].flag == ipinfo::LOCAL && control[i].version > c.lastcontrol)
-                masteroutf(c, "%s %u %u\n", ipinfotypes[control[i].type], control[i].ip, control[i].mask);
+                masteroutf(c, "%s %u %u %s\n", ipinfotypes[control[i].type], control[i].ip, control[i].mask, control[i].reason);
             c.lastcontrol = controlversion;
         }
         if(c.outputpos < c.output.length()) ENET_SOCKETSET_ADD(writeset, c.socket);
@@ -407,7 +429,7 @@ void checkmaster()
     {
         ENetAddress address;
         ENetSocket masterclientsocket = enet_socket_accept(mastersocket, &address);
-        if(masterclients.length() >= MASTER_LIMIT || (checkipinfo(control, ipinfo::BAN, address.host) && !checkipinfo(control, ipinfo::ALLOW, address.host)))
+        if(masterclients.length() >= MASTER_LIMIT || (checkipinfo(control, ipinfo::BAN, address.host) && !checkipinfo(control, ipinfo::EXCEPT, address.host)))
             enet_socket_destroy(masterclientsocket);
         else if(masterclientsocket!=ENET_SOCKET_NULL)
         {
@@ -423,8 +445,8 @@ void checkmaster()
             c->socket = masterclientsocket;
             c->lastactivity = totalmillis ? totalmillis : 1;
             masterclients.add(c);
-            enet_address_get_host_ip(&c->address, c->name, sizeof(c->name));
-            conoutf("master peer %s connected", c->name);
+            if(enet_address_get_host_ip(&c->address, c->name, sizeof(c->name)) < 0) copystring(c->name, "unknown");
+            if(verbose) conoutf("master peer %s connected", c->name);
         }
     }
 
@@ -464,7 +486,7 @@ void checkmaster()
             else { purgemasterclient(i--); continue; }
         }
         /* if(c.output.length() > OUTPUT_LIMIT) { purgemasterclient(i--); continue; } */
-        if(ENET_TIME_DIFFERENCE(totalmillis, c.lastactivity) >= (c.isserver ? SERVER_TIME : CLIENT_TIME) || (checkipinfo(control, ipinfo::BAN, c.address.host) && !checkipinfo(control, ipinfo::ALLOW, c.address.host)))
+        if(ENET_TIME_DIFFERENCE(totalmillis, c.lastactivity) >= (c.isserver ? SERVER_TIME : CLIENT_TIME) || (checkipinfo(control, ipinfo::BAN, c.address.host) && !checkipinfo(control, ipinfo::EXCEPT, c.address.host)))
         {
             purgemasterclient(i--);
             continue;
@@ -481,4 +503,3 @@ void reloadmaster()
 {
     clearauth();
 }
-
diff --git a/src/engine/material.cpp b/src/engine/material.cpp
index a18e921..7ef0bd7 100644
--- a/src/engine/material.cpp
+++ b/src/engine/material.cpp
@@ -45,12 +45,12 @@ struct QuadNode
         }
     }
 
-    void genmatsurf(ushort mat, uchar orient, uchar flags, int x, int y, int z, int size, materialsurface *&matbuf)
+    void genmatsurf(ushort mat, uchar orient, uchar visible, int x, int y, int z, int size, materialsurface *&matbuf)
     {
         materialsurface &m = *matbuf++;
         m.material = mat;
         m.orient = orient;
-        m.flags = flags;
+        m.visible = visible;
         m.csize = size;
         m.rsize = size;
         int dim = dimension(orient);
@@ -106,9 +106,9 @@ static void renderwaterfall(const materialsurface &m, float offset, const vec *n
                 varray::attrib<float>(wfxscale*v.x, wfyscale*(v.z+wfscroll)); \
             }
 #define GENFACENORMAL varray::attrib<float>(n.x, n.y, n.z);
-    if(normal) 
-    { 
-        vec n = *normal; 
+    if(normal)
+    {
+        vec n = *normal;
         switch(m.orient) { GENFACEVERTSXY(x, x, y, y, zmin, zmax, /**/, + csize, /**/, + rsize, + offset, - offset) }
     }
 #undef GENFACENORMAL
@@ -135,7 +135,7 @@ static void drawmaterial(const materialsurface &m, float offset)
 #define GENFACEORIENT(orient, v0, v1, v2, v3) \
         case orient: v0 v1 v2 v3 break;
 #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
-            varray::attrib<float>(mx sx, my sy, mz sz); 
+            varray::attrib<float>(mx sx, my sy, mz sz);
         GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset)
 #undef GENFACEORIENT
 #undef GENFACEVERT
@@ -174,10 +174,10 @@ const char *getmaterialdesc(int mat, const char *prefix)
     static string desc;
     desc[0] = '\0';
     loopi(sizeof(matmasks)/sizeof(matmasks[0])) if(mat&matmasks[i])
-    {               
+    {
         const char *matname = findmaterialname(mat&matmasks[i]);
-        if(matname)     
-        {               
+        if(matname)
+        {
             concatstring(desc, desc[0] ? ", " : prefix);
             concatstring(desc, matname);
         }
@@ -226,7 +226,7 @@ void genmatsurfs(const cube &c, int cx, int cy, int cz, int size, vector<materia
                 materialsurface m;
                 m.material = c.material&matmask;
                 m.orient = i;
-                m.flags = vis == MATSURF_EDIT_ONLY ? materialsurface::F_EDIT : 0;
+                m.visible = vis;
                 m.o = ivec(cx, cy, cz);
                 m.csize = m.rsize = size;
                 if(dimcoord(i)) m.o[dimension(i)] += size;
@@ -323,7 +323,7 @@ int optimizematsurfs(materialsurface *matbuf, int matsurfs)
          while(cur < end &&
                cur->material == start->material &&
                cur->orient == start->orient &&
-               cur->flags == start->flags &&
+               cur->visible == start->visible &&
                cur->o[dim] == start->o[dim])
             ++cur;
          if(!isliquid(start->material&MATF_VOLUME) || start->orient != O_TOP || !vertwater)
@@ -335,7 +335,7 @@ int optimizematsurfs(materialsurface *matbuf, int matsurfs)
          {
             QuadNode vmats(0, 0, hdr.worldsize);
             loopi(cur-start) vmats.insert(start[i].o[C[dim]], start[i].o[R[dim]], start[i].csize);
-            vmats.genmatsurfs(start->material, start->orient, start->flags, start->o[dim], matbuf);
+            vmats.genmatsurfs(start->material, start->orient, start->visible, start->o[dim], matbuf);
          }
          else
          {
@@ -434,7 +434,7 @@ void setupmaterials(int start, int len)
             m.skip = 0;
             if(skip && m.material == skip->material && m.orient == skip->orient && skip->skip < 0xFFFF)
                 skip->skip++;
-            else 
+            else
                 skip = &m;
         }
     }
@@ -514,8 +514,7 @@ void sortmaterials(vector<materialsurface *> &vismats)
 {
     sortorigin = ivec(camera1->o);
     if(reflecting) sortorigin.z = int(reflectz - (camera1->o.z - reflectz));
-    vec dir;
-    vecfromyawpitch(camera1->yaw, reflecting ? -camera1->pitch : camera1->pitch, 1, 0, dir);
+    vec dir(camera1->yaw*RAD, reflecting ? -camera1->pitch : camera1->pitch);
     loopi(3) { dir[i] = fabs(dir[i]); sortdim[i] = i; }
     if(dir[sortdim[2]] > dir[sortdim[1]]) swap(sortdim[2], sortdim[1]);
     if(dir[sortdim[1]] > dir[sortdim[0]]) swap(sortdim[1], sortdim[0]);
@@ -532,7 +531,7 @@ void sortmaterials(vector<materialsurface *> &vismats)
             {
                 int matvol = m.material&MATF_VOLUME;
                 if(matvol==MAT_WATER && (m.orient==O_TOP || (refracting<0 && reflectz>hdr.worldsize))) { i += m.skip; continue; }
-                if(m.flags&materialsurface::F_EDIT) { i += m.skip; continue; }
+                if(m.visible == MATSURF_EDIT_ONLY) { i += m.skip; continue; }
                 if(glaring && matvol!=MAT_LAVA) { i += m.skip; continue; }
             }
             else if(glaring) continue;
@@ -579,7 +578,7 @@ void rendermatgrid(vector<materialsurface *> &vismats)
     bvec name##col(0x20, 0x80, 0xC0); \
     VARF(IDF_HEX|IDF_WORLD, name##colour, 0, 0x2080C0, 0xFFFFFF, \
     { \
-        if(!name##colour) name##colour = 0x2080FF; \
+        if(!name##colour) name##colour = 0x2080C0; \
         name##col = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \
     });
 
@@ -618,7 +617,7 @@ static void drawglass(const materialsurface &m, float offset, const vec *normal
     if(normal)
     {
         vec n = *normal;
-        switch(m.orient) { GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset) }     
+        switch(m.orient) { GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset) }
     }
     #undef GENFACENORMAL
     #define GENFACENORMAL
@@ -663,7 +662,7 @@ void rendermaterials()
     if(editmode && showmat && !envmapping)
     {
         glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
-        glEnable(GL_BLEND); blended = true; 
+        glEnable(GL_BLEND); blended = true;
         glDisable(GL_TEXTURE_2D); textured = 0;
         foggednotextureshader->set();
         glFogfv(GL_FOG_COLOR, zerofog); lastfogtype = 0;
@@ -709,7 +708,7 @@ void rendermaterials()
                     {
                         xtraverts += varray::end();
                         glBindTexture(GL_TEXTURE_2D, mslot->sts[1].t->id);
-                        float angle = fmod(float(lastmillis/(renderpath!=R_FIXEDFUNCTION ? 600.0f : 300.0f)/(2*M_PI)), 1.0f), 
+                        float angle = fmod(float(lastmillis/(renderpath!=R_FIXEDFUNCTION ? 600.0f : 300.0f)/(2*M_PI)), 1.0f),
                               s = angle - int(angle) - 0.5f;
                         s *= 8 - fabs(s)*16;
                         wfwave = vertwater ? WATER_AMPLITUDE*s-WATER_OFFSET : -WATER_OFFSET;
diff --git a/src/engine/md5.h b/src/engine/md5.h
index 2d75c93..ef96016 100644
--- a/src/engine/md5.h
+++ b/src/engine/md5.h
@@ -322,7 +322,7 @@ struct md5 : skelmodel, skelloader<md5>
                             basejoints.add(j);
                         }
                     }
-                    if(basejoints.length()!=skel->numbones) { delete f; return NULL; }
+                    if(basejoints.length()!=skel->numbones) { delete f; if(animdata) delete[] animdata; return NULL; }
                     animbones = new dualquat[(skel->numframes+animframes)*skel->numbones];
                     if(skel->framebones)
                     {
@@ -373,7 +373,7 @@ struct md5 : skelmodel, skelloader<md5>
                 }
             }
 
-            DELETEA(animdata);
+            if(animdata) delete[] animdata;
             delete f;
 
             return sa;
diff --git a/src/engine/menus.cpp b/src/engine/menus.cpp
index f8af9b9..c19c0aa 100644
--- a/src/engine/menus.cpp
+++ b/src/engine/menus.cpp
@@ -15,33 +15,28 @@ struct menu : guicb
     char *name, *header;
     uint *contents, *initscript;
     int passes, menutab, menustart;
-    bool world, useinput, usetitle;
+    bool world, useinput, usetitle, usebgfx, builtin, *keep;
 
-    menu() : name(NULL), header(NULL), contents(NULL), initscript(NULL), passes(0), menutab(0), menustart(0), world(false), useinput(true), usetitle(true) {}
+    menu() : name(NULL), header(NULL), contents(NULL), initscript(NULL), passes(0), menutab(0), menustart(0), world(false), useinput(true), usetitle(true), usebgfx(true), builtin(false), keep(NULL) {}
 
     void gui(guient &g, bool firstpass)
     {
         cgui = &g;
         cmenu = this;
         guipasses = passes;
-        if(!passes) world = (identflags&IDF_WORLD)!=0;
-        if(initscript)
-        {
-            if(world && passes) { WITHWORLD(execute(initscript)); }
-            else execute(initscript);
-        }
-        cgui->start(menustart, menuscale, &menutab, useinput, usetitle);
+        if(!passes) guiactionon = false;
+        int oldflags = identflags;
+        if(world && !builtin) identflags |= IDF_WORLD;
+        if(initscript) execute(initscript);
+        cgui->start(menustart, menuscale, &menutab, useinput, usetitle, usebgfx);
         cgui->tab(header ? header : name);
-        if(contents)
-        {
-            if(world && passes) { WITHWORLD(execute(contents)); }
-            else execute(contents);
-        }
+        if(contents) execute(contents);
         cgui->end();
+        identflags = oldflags;
         guipasses = -1;
         cmenu = NULL;
         cgui = NULL;
-        passes++;
+        if((++passes) <= 0) passes = 1;
     }
 
     virtual void clear() {}
@@ -63,13 +58,14 @@ struct delayedupdate
         float f;
         char *s;
     } val;
+    bool world;
     delayedupdate() : type(ACTION), id(NULL) { val.s = NULL; }
     ~delayedupdate() { if(type == STRING || type == ACTION) DELETEA(val.s); }
 
-    void schedule(const char *s) { type = ACTION; val.s = newstring(s); }
-    void schedule(ident *var, int i) { type = INT; id = var; val.i = i; }
-    void schedule(ident *var, float f) { type = FLOAT; id = var; val.f = f; }
-    void schedule(ident *var, char *s) { type = STRING; id = var; val.s = newstring(s); }
+    void schedule(const char *s) { type = ACTION; val.s = newstring(s); world = (identflags&IDF_WORLD)!=0; }
+    void schedule(ident *var, int i) { type = INT; id = var; val.i = i; world = (identflags&IDF_WORLD)!=0; }
+    void schedule(ident *var, float f) { type = FLOAT; id = var; val.f = f; world = (identflags&IDF_WORLD)!=0; }
+    void schedule(ident *var, char *s) { type = STRING; id = var; val.s = newstring(s); world = (identflags&IDF_WORLD)!=0; }
 
     int getint() const
     {
@@ -106,6 +102,8 @@ struct delayedupdate
 
     void run()
     {
+        int _oldflags = identflags;
+        if(world) identflags |= IDF_WORLD;
         if(type == ACTION) { if(val.s) execute(val.s); }
         else if(id) switch(id->type)
         {
@@ -114,6 +112,7 @@ struct delayedupdate
             case ID_SVAR: setsvarchecked(id, getstring()); break;
             case ID_ALIAS: alias(id->name, getstring()); break;
         }
+        identflags = _oldflags;
     }
 };
 
@@ -122,25 +121,30 @@ static vector<menu *> menustack;
 static vector<delayedupdate> updatelater;
 static bool shouldclearmenu = true, clearlater = false;
 
-void popgui()
+bool popgui(bool skip = true)
 {
-    menu *m = menustack.pop();
+    menu *m = menustack.last();
+    if(skip && m->keep && *m->keep) return false;
+    menustack.pop();
     m->passes = 0;
+    if(m->keep) *m->keep = false;
     m->clear();
+    return true;
 }
 
 void removegui(menu *m)
 {
-    loopv(menustack) if(menustack[i]==m)
+    loopv(menustack) if(menustack[i] == m)
     {
         menustack.remove(i);
         m->passes = 0;
+        if(m->keep) *m->keep = false;
         m->clear();
         return;
     }
 }
 
-void pushgui(menu *m, int pos = -1, int tab = 0)
+void pushgui(menu *m, int pos = -1, int tab = 0, bool *keep = NULL)
 {
     if(menustack.empty()) resetcursor();
     if(pos < 0) menustack.add(m);
@@ -151,10 +155,12 @@ void pushgui(menu *m, int pos = -1, int tab = 0)
         m->menustart = totalmillis;
         if(tab > 0) m->menutab = tab;
         m->usetitle = tab >= 0 ? true : false;
+        m->world = (identflags&IDF_WORLD)!=0;
+        m->keep = keep;
     }
 }
 
-void restoregui(int pos, int tab = 0)
+void restoregui(int pos, int tab = 0, bool *keep = NULL)
 {
     int clear = menustack.length()-pos-1;
     loopi(clear) popgui();
@@ -164,33 +170,42 @@ void restoregui(int pos, int tab = 0)
         m->passes = 0;
         m->menustart = totalmillis;
         if(tab > 0) m->menutab = tab;
+        m->world = (identflags&IDF_WORLD)!=0;
+        m->keep = keep;
     }
 }
 
-void showgui(const char *name, int tab)
+void showgui(const char *name, int tab, bool *keep)
 {
     menu *m = menus.access(name);
     if(!m) return;
     int pos = menustack.find(m);
-    if(pos<0) pushgui(m, -1, tab);
-    else restoregui(pos, tab);
+    if(pos<0) pushgui(m, -1, tab, keep);
+    else if(pos < menustack.length()-1) restoregui(pos, tab, keep);
+    else
+    {
+        m->world = (identflags&IDF_WORLD)!=0;
+        m->menutab = tab;
+        m->keep = keep;
+        return;
+    }
     playsound(S_GUIPRESS, camera1->o, camera1, SND_FORCED);
 }
 
 extern bool closetexgui();
-int cleargui(int n)
+int cleargui(int n, bool skip)
 {
     if(closetexgui()) n--;
     int clear = menustack.length();
     if(n>0) clear = min(clear, n);
-    loopi(clear) popgui();
+    loopi(clear) if(!popgui(skip)) break;
     if(!menustack.empty()) restoregui(menustack.length()-1);
     return clear;
 }
 
 void cleargui_(int *n)
 {
-    intret(cleargui(*n));
+    intret(cleargui(*n, false));
 }
 
 void guishowtitle(int *n)
@@ -199,6 +214,12 @@ void guishowtitle(int *n)
     cmenu->usetitle = *n ? true : false;
 }
 
+void guishowbgfx(int *n)
+{
+    if(!cmenu) return;
+    cmenu->usebgfx = *n ? true : false;
+}
+
 void guistayopen(uint *contents)
 {
     bool oldclearmenu = shouldclearmenu;
@@ -215,14 +236,14 @@ void guinohitfx(uint *contents)
     cgui->allowhitfx(true);
 }
 
-//@DOC name and icon are optional
 SVAR(0, guirollovername, "");
 SVAR(0, guirolloveraction, "");
+SVAR(0, guirollovertype, "");
 
-void guibutton(char *name, char *action, char *altact, char *icon, int *colour)
+void guibutton(char *name, char *action, char *altact, char *icon, int *colour, int *icolour, int *wrap)
 {
     if(!cgui) return;
-    int ret = cgui->button(name, *colour ? *colour : 0xFFFFFF, *icon ? icon : NULL);
+    int ret = cgui->button(name, *colour >= 0 ? *colour : 0xFFFFFF, *icon ? icon : NULL, *icolour >= 0 ? *icolour : 0xFFFFFF, *wrap > 0 ? *wrap : -1);
     if(ret&GUI_UP)
     {
         char *act = NULL;
@@ -238,13 +259,11 @@ void guibutton(char *name, char *action, char *altact, char *icon, int *colour)
     {
         setsvar("guirollovername", name, true);
         setsvar("guirolloveraction", action, true);
+        setsvar("guirollovertype", "button", true);
     }
 }
 
-SVAR(0, guirolloverimgpath, "");
-SVAR(0, guirolloverimgaction, "");
-
-void guiimage(char *path, char *action, float *scale, int *overlaid, char *altpath, char *altact)
+void guiimage(char *path, char *action, float *scale, int *overlaid, char *altpath, char *altact, int *colour)
 {
     if(!cgui) return;
     Texture *t = path[0] ? textureload(path, 0, true, false) : NULL;
@@ -253,7 +272,7 @@ void guiimage(char *path, char *action, float *scale, int *overlaid, char *altpa
         if(*altpath) t = textureload(altpath, 0, true, false);
         if(t == notexture) return;
     }
-    int ret = cgui->image(t, *scale, *overlaid!=0);
+    int ret = cgui->image(t, *scale, *overlaid!=0, *colour >= 0 ? *colour : 0xFFFFFF);
     if(ret&GUI_UP)
     {
         char *act = NULL;
@@ -267,8 +286,9 @@ void guiimage(char *path, char *action, float *scale, int *overlaid, char *altpa
     }
     else if(ret&GUI_ROLLOVER)
     {
-        setsvar("guirolloverimgpath", path, true);
-        setsvar("guirolloverimgaction", action, true);
+        setsvar("guirollovername", path, true);
+        setsvar("guirolloveraction", action, true);
+        setsvar("guirollovertype", "image", true);
     }
 }
 
@@ -295,24 +315,30 @@ void guislice(char *path, char *action, float *scale, float *start, float *end,
     }
     else if(ret&GUI_ROLLOVER)
     {
-        setsvar("guirolloverimgpath", path, true);
-        setsvar("guirolloverimgaction", action, true);
+        setsvar("guirollovername", path, true);
+        setsvar("guirolloveraction", action, true);
+        setsvar("guirollovertype", "image", true);
     }
 }
 
-void guitext(char *name, char *icon, int *colour, int *icolour)
+void guitext(char *name, char *icon, int *colour, int *icolour, int *wrap)
 {
-    if(cgui) cgui->text(name, *colour ? *colour : 0xFFFFFF, icon[0] ? icon : NULL, *icolour ? *icolour : 0xFFFFFF);
+    if(cgui) cgui->text(name, *colour >= 0 ? *colour : 0xFFFFFF, icon[0] ? icon : NULL, *icolour >= 0 ? *icolour : 0xFFFFFF, *wrap > 0 ? *wrap : -1);
 }
 
-void guititle(char *name)
+void guitab(char *name)
 {
-    if(cgui) cgui->title(name);
+    if(cgui) cgui->tab(name);
 }
 
-void guitab(char *name)
+void guistatus(char *str, int *width)
 {
-    if(cgui) cgui->tab(name);
+    if(cgui) cgui->setstatus("%s", *width, str);
+}
+
+void guitooltip(char *str, int *width)
+{
+    if(cgui) cgui->settooltip("%s", *width, str);
 }
 
 void guibar()
@@ -320,16 +346,27 @@ void guibar()
     if(cgui) cgui->separator();
 }
 
-void guibackground(int *colour, int *levels)
+void guifill(int *colour, int *levels)
+{
+    if(cgui) cgui->fill(*colour, *levels);
+}
+
+void guioutline(int *colour, int *levels, int *offsetx, int *offsety)
+{
+    if(cgui) cgui->outline(*colour, *levels, 0, *offsetx, *offsety);
+}
+
+void guibackground(int *colour1, float *blend1, int *colour2, float *blend2, int *skinborder, int *levels)
 {
-    if(cgui) cgui->background(*colour, *levels);
+    if(cgui) cgui->background(*colour1, *blend1, *colour2, *blend2, *skinborder!=0, *levels);
 }
 
 void guistrut(float *strut, int *alt)
 {
     if(cgui)
     {
-        if(*alt) cgui->strut(*strut); else cgui->space(*strut);
+        if(*alt) cgui->strut(*strut);
+        else cgui->space(*strut);
     }
 }
 
@@ -338,6 +375,16 @@ void guispring(int *weight)
     if(cgui) cgui->spring(max(*weight, 1));
 }
 
+void guivisible(uint *body)
+{
+    if(cgui && cgui->visible()) execute(body);
+}
+
+void guivisibletab(uint *body)
+{
+    if(cgui && cgui->visibletab()) execute(body);
+}
+
 void guifont(char *font, uint *body)
 {
     if(cgui)
@@ -348,6 +395,40 @@ void guifont(char *font, uint *body)
     }
 }
 
+int guifontwidth(char *font)
+{
+    if(font && *font) pushfont(font);
+    int width = FONTW;
+    if(font && *font) popfont();
+    return width;
+}
+
+int guifontheight(char *font)
+{
+    if(font && *font) pushfont(font);
+    int height = FONTH;
+    if(font && *font) popfont();
+    return height;
+}
+
+int guitextwidth(char *text, char *font, int wrap)
+{
+    if(font && *font) pushfont(font);
+    int width = 0, height = 0;
+    text_bounds(text, width, height, wrap > 0 ? wrap : -1, TEXT_NO_INDENT);
+    if(font && *font) popfont();
+    return width;
+}
+
+int guitextheight(char *text, char *font, int wrap)
+{
+    if(font && *font) pushfont(font);
+    int width = 0, height = 0;
+    text_bounds(text, width, height, wrap > 0 ? wrap : -1, TEXT_NO_INDENT);
+    if(font && *font) popfont();
+    return height;
+}
+
 template<class T> static void updateval(char *var, T val, char *onchange)
 {
     ident *id = writeident(var);
@@ -403,7 +484,7 @@ void guiprogress(float *percent, float *scale)
     cgui->progress(*percent, *scale);
 }
 
-void guislider(char *var, int *min, int *max, char *onchange, int *reverse, int *scroll, int *colour)
+void guislider(char *var, int *min, int *max, char *onchange, int *reverse, int *scroll, int *colour, int *style, int *scolour)
 {
     if(!cgui || !var || !*var) return;
     int oldval = getval(var), val = oldval, vmin = *max > INT_MIN ? *min : getvarmin(var), vmax = *max > INT_MIN ? *max : getvarmax(var);
@@ -412,11 +493,11 @@ void guislider(char *var, int *min, int *max, char *onchange, int *reverse, int
         int vdef = getvardef(var);
         vmax = vdef > vmin ? vdef*3 : vmin*4;
     }
-    cgui->slider(val, vmin, vmax, *colour ? *colour : 0xFFFFFF, NULL, *reverse ? true : false, *scroll ? true : false);
+    cgui->slider(val, vmin, vmax, *colour >= 0 ? *colour : 0xFFFFFF, NULL, *reverse ? true : false, *scroll ? true : false, *style, *scolour);
     if(val != oldval) updateval(var, val, onchange);
 }
 
-void guilistslider(char *var, char *list, char *onchange, int *reverse, int *scroll, int *colour)
+void guilistslider(char *var, char *list, char *onchange, int *reverse, int *scroll, int *colour, int *style, int *scolour)
 {
     if(!cgui) return;
     vector<int> vals;
@@ -430,11 +511,11 @@ void guilistslider(char *var, char *list, char *onchange, int *reverse, int *scr
     if(vals.empty()) return;
     int val = getval(var), oldoffset = vals.length()-1, offset = oldoffset;
     loopv(vals) if(val <= vals[i]) { oldoffset = offset = i; break; }
-    cgui->slider(offset, 0, vals.length()-1, *colour ? *colour : 0xFFFFFF, intstr(val), *reverse ? true : false, *scroll ? true : false);
+    cgui->slider(offset, 0, vals.length()-1, *colour >= 0 ? *colour : 0xFFFFFF, intstr(val), *reverse ? true : false, *scroll ? true : false, *style, *scolour);
     if(offset != oldoffset) updateval(var, vals[offset], onchange);
 }
 
-void guinameslider(char *var, char *names, char *list, char *onchange, int *reverse, int *scroll, int *colour)
+void guinameslider(char *var, char *names, char *list, char *onchange, int *reverse, int *scroll, int *colour, int *style, int *scolour)
 {
     if(!cgui) return;
     vector<int> vals;
@@ -449,68 +530,86 @@ void guinameslider(char *var, char *names, char *list, char *onchange, int *reve
     int val = getval(var), oldoffset = vals.length()-1, offset = oldoffset;
     loopv(vals) if(val <= vals[i]) { oldoffset = offset = i; break; }
     char *label = indexlist(names, offset);
-    cgui->slider(offset, 0, vals.length()-1, *colour ? *colour : 0xFFFFFF, label, *reverse ? true : false, *scroll ? true : false);
+    cgui->slider(offset, 0, vals.length()-1, *colour >= 0 ? *colour : 0xFFFFFF, label, *reverse ? true : false, *scroll ? true : false, *style, *scolour);
     if(offset != oldoffset) updateval(var, vals[offset], onchange);
     delete[] label;
 }
 
 void guicheckbox(char *name, char *var, float *on, float *off, char *onchange, int *colour)
 {
+    if(!cgui) return;
     bool enabled = getfval(var) != *off, two = getfvarmax(var) == 2, next = two && getfval(var) == 1.0f;
-    if(cgui && cgui->button(name, *colour ? *colour : 0xFFFFFF, enabled ? (two && !next ? "checkboxtwo" : "checkboxon") : "checkbox", 0xFFFFFF, enabled ? false : true)&GUI_UP)
+    int ret = cgui->button(name, *colour >= 0 ? *colour : 0xFFFFFF, enabled ? (two && !next ? "checkboxtwo" : "checkboxon") : "checkbox", 0xFFFFFF);
+    if(ret&GUI_UP) updateval(var, enabled ? (two && next ? 2.0f : *off) : (*on || *off ? *on : 1.0f), onchange);
+    else if(ret&GUI_ROLLOVER)
     {
-        updateval(var, enabled ? (two && next ? 2.0f : *off) : (*on || *off ? *on : 1.0f), onchange);
+        setsvar("guirollovername", name, true);
+        setsvar("guirolloveraction", var, true);
+        setsvar("guirollovertype", "checkbox", true);
     }
 }
 
 void guiradio(char *name, char *var, float *n, char *onchange, int *colour)
 {
+    if(!cgui) return;
     bool enabled = getfval(var)==*n;
-    if(cgui && cgui->button(name, *colour ? *colour : 0xFFFFFF, enabled ? "radioboxon" : "radiobox", *colour ? *colour : 0xFFFFFF, enabled ? false : true)&GUI_UP)
+    int ret = cgui->button(name, *colour >= 0 ? *colour : 0xFFFFFF, enabled ? "radioboxon" : "radiobox", *colour >= 0 ? *colour : 0xFFFFFF);
+    if(ret&GUI_UP)
     {
         if(!enabled) updateval(var, *n, onchange);
     }
+    else if(ret&GUI_ROLLOVER)
+    {
+        setsvar("guirollovername", name, true);
+        setsvar("guirolloveraction", var, true);
+        setsvar("guirollovertype", "radio", true);
+    }
 }
 
 void guibitfield(char *name, char *var, int *mask, char *onchange, int *colour)
 {
+    if(!cgui) return;
     int val = getval(var);
     bool enabled = (val & *mask) != 0;
-    if(cgui && cgui->button(name, *colour ? *colour : 0xFFFFFF, enabled ? "checkboxon" : "checkbox", *colour ? *colour : 0xFFFFFF, enabled ? false : true)&GUI_UP)
+    int ret = cgui->button(name, *colour >= 0 ? *colour : 0xFFFFFF, enabled ? "checkboxon" : "checkbox", *colour >= 0 ? *colour : 0xFFFFFF);
+    if(ret&GUI_UP) updateval(var, enabled ? val & ~*mask : val | *mask, onchange);
+    else if(ret&GUI_ROLLOVER)
     {
-        updateval(var, enabled ? val & ~*mask : val | *mask, onchange);
+        setsvar("guirollovername", name, true);
+        setsvar("guirolloveraction", var, true);
+        setsvar("guirollovertype", "bitfield", true);
     }
 }
 
 //-ve length indicates a wrapped text field of any (approx 260 chars) length, |length| is the field width
-void guifield(char *var, int *maxlength, char *onchange, int *colour, int *focus, char *parent)
+void guifield(char *var, int *maxlength, char *onchange, int *colour, int *focus, char *parent, int *height, char *prompt, int *immediate)
 {
     if(!cgui) return;
     const char *initval = getsval(var);
-    char *result = cgui->field(var, *colour ? *colour : 0xFFFFFF, *maxlength ? *maxlength : 12, 0, initval, EDITORFOCUSED, *focus!=0, parent);
-    if(result) updateval(var, result, onchange);
+    char *result = cgui->field(var, *colour >= 0 ? *colour : 0xFFFFFF, *maxlength ? *maxlength : 12, *height, initval, EDITORFOCUSED, *focus!=0, parent, prompt, *immediate!=0);
+    if(result && (!*immediate || strcmp(initval, result))) updateval(var, result, onchange);
 }
 
 //-ve maxlength indicates a wrapped text field of any (approx 260 chars) length, |maxlength| is the field width
-void guieditor(char *name, int *maxlength, int *height, int *mode, int *colour, int *focus, char *parent)
+void guieditor(char *name, int *maxlength, int *height, int *mode, int *colour, int *focus, char *parent, char *str, char *prompt)
 {
     if(!cgui) return;
-    cgui->field(name, *colour ? *colour : 0xFFFFFF, *maxlength ? *maxlength : 12, *height, NULL, *mode<=0 ? EDITORFOREVER : *mode, *focus!=0, parent);
+    cgui->field(name, *colour >= 0 ? *colour : 0xFFFFFF, *maxlength ? *maxlength : 12, *height, str && *str ? str : NULL, *mode<=0 ? EDITORFOREVER : *mode, *focus!=0, parent, prompt);
     //returns a non-NULL pointer (the currentline) when the user commits, could then manipulate via text* commands
 }
 
 //-ve length indicates a wrapped text field of any (approx 260 chars) length, |length| is the field width
-void guikeyfield(char *var, int *maxlength, char *onchange, int *colour, int *focus, char *parent)
+void guikeyfield(char *var, int *maxlength, char *onchange, int *colour, int *focus, char *parent, char *prompt, int *immediate)
 {
     if(!cgui) return;
     const char *initval = getsval(var);
-    char *result = cgui->keyfield(var, *colour ? *colour : 0xFFFFFF, *maxlength ? *maxlength : -8, 0, initval, EDITORFOCUSED, *focus!=0, parent);
-    if(result) updateval(var, result, onchange);
+    char *result = cgui->keyfield(var, *colour >= 0 ? *colour : 0xFFFFFF, *maxlength ? *maxlength : -8, 0, initval, EDITORFOCUSED, *focus!=0, parent, prompt, *immediate!=0);
+    if(result && (!*immediate || strcmp(initval, result))) updateval(var, result, onchange);
 }
 
 //use text<action> to do more...
 
-void guibody(uint *contents, char *action, char *altact)
+void guibody(uint *contents, char *action, char *altact, uint *onhover)
 {
     if(!cgui) return;
     cgui->pushlist(action[0] ? true : false);
@@ -527,6 +626,7 @@ void guibody(uint *contents, char *action, char *altact)
             if(shouldclearmenu) clearlater = true;
         }
     }
+    else if(ret&GUI_ROLLOVER) execute(onhover);
 }
 
 void guilist(uint *contents)
@@ -574,42 +674,52 @@ void guimodify(char *name, char *contents)
 COMMAND(0, newgui, "sss");
 COMMAND(0, guiheader, "s");
 COMMAND(0, guimodify, "ss");
-COMMAND(0, guibutton, "ssssi");
-COMMAND(0, guitext, "ssii");
+COMMAND(0, guibutton, "ssssbbb");
+COMMAND(0, guitext, "ssbbb");
 COMMANDN(0, cleargui, cleargui_, "i");
 ICOMMAND(0, showgui, "si", (const char *s, int *n), showgui(s, *n));
 COMMAND(0, guishowtitle, "i");
+COMMAND(0, guishowbgfx, "i");
 COMMAND(0, guistayopen, "e");
 COMMAND(0, guinohitfx, "e");
 
-ICOMMAND(0, guicount, "", (), intret(menustack.length()));
-
 COMMAND(0, guilist, "e");
-COMMAND(0, guibody, "ess");
-COMMAND(0, guititle, "s");
+COMMAND(0, guibody, "esse");
 COMMAND(0, guibar, "");
-COMMAND(0, guibackground, "ii");
+COMMAND(0, guifill, "ii");
+COMMAND(0, guioutline, "iiii");
+COMMAND(0, guibackground, "bgbgii");
 COMMAND(0, guistrut,"fi");
 COMMAND(0, guispring, "i");
+COMMAND(0, guivisible, "e");
+COMMAND(0, guivisibletab, "e");
 COMMAND(0, guifont,"se");
-COMMAND(0, guiimage,"ssfiss");
+COMMAND(0, guiimage,"ssfissb");
 COMMAND(0, guislice,"ssfffsss");
 COMMAND(0, guiprogress,"ff");
-COMMAND(0, guislider,"sbbsiii");
-COMMAND(0, guilistslider, "sssiii");
-COMMAND(0, guinameslider, "ssssiii");
-COMMAND(0, guiradio,"ssfsi");
-COMMAND(0, guibitfield, "ssisi");
-COMMAND(0, guicheckbox, "ssffsi");
+COMMAND(0, guislider,"sbbsiibib");
+COMMAND(0, guilistslider, "sssiibib");
+COMMAND(0, guinameslider, "ssssiibib");
+COMMAND(0, guiradio,"ssfsb");
+COMMAND(0, guibitfield, "ssisb");
+COMMAND(0, guicheckbox, "ssffsb");
 COMMAND(0, guitab, "s");
-COMMAND(0, guifield, "sisiis");
-COMMAND(0, guikeyfield, "sisiis");
-COMMAND(0, guieditor, "siiiiis");
+COMMAND(0, guistatus, "si");
+COMMAND(0, guitooltip, "si");
+COMMAND(0, guifield, "sisbisisi");
+COMMAND(0, guikeyfield, "sisbissi");
+COMMAND(0, guieditor, "siiibisss");
+
+ICOMMAND(0, guicount, "", (), intret(menustack.length()));
+ICOMMAND(0, guifontwidth, "s", (char *font), intret(guifontwidth(font)));
+ICOMMAND(0, guifontheight, "s", (char *font), intret(guifontheight(font)));
+ICOMMAND(0, guitextwidth, "ssb", (char *text, char *font, int *wrap), intret(guitextwidth(text, font, *wrap)));
+ICOMMAND(0, guitextheight, "ssb", (char *text, char *font, int *wrap), intret(guitextheight(text, font, *wrap)));
 
 void guiplayerpreview(int *model, int *color, int *team, int *weap, char *vanity, char *action, float *scale, int *overlaid, float *size, float *blend, char *altact)
 {
     if(!cgui) return;
-    int ret = cgui->playerpreview(*model, *color, *team, *weap, vanity, *scale, *overlaid!=0, *size!=0 ? *size : 1.f, *blend!=0 ? *blend : 1.f);
+    int ret = cgui->playerpreview(*model, *color, *team, *weap, vanity, *scale, *overlaid!=0, *size!=0 ? *size : 1.f, *blend >= 0 ? *blend : 1.f);
     if(ret&GUI_UP)
     {
         char *act = NULL;
@@ -621,8 +731,15 @@ void guiplayerpreview(int *model, int *color, int *team, int *weap, char *vanity
             if(shouldclearmenu) clearlater = true;
         }
     }
+    else if(ret&GUI_ROLLOVER)
+    {
+        defformatstring(str)("%d %d %d %d %s", *model, *color, *team, *weap, vanity);
+        setsvar("guirollovername", str, true);
+        setsvar("guirolloveraction", action, true);
+        setsvar("guirollovertype", "player", true);
+    }
 }
-COMMAND(0, guiplayerpreview, "iiiissfiffs");
+COMMAND(0, guiplayerpreview, "iiiissfifgs");
 
 void guimodelpreview(char *model, char *animspec, char *action, float *scale, int *overlaid, float *size, float *blend, char *altact)
 {
@@ -655,6 +772,13 @@ void guimodelpreview(char *model, char *animspec, char *action, float *scale, in
             if(shouldclearmenu) clearlater = true;
         }
     }
+    else if(ret&GUI_ROLLOVER)
+    {
+        defformatstring(str)("%d %s", *model, animspec);
+        setsvar("guirollovername", str, true);
+        setsvar("guirolloveraction", action, true);
+        setsvar("guirollovertype", "model", true);
+    }
 }
 COMMAND(0, guimodelpreview, "sssfiffs");
 
@@ -752,11 +876,11 @@ void progressmenu()
 {
     menu *m = menus.access("loading");
     if(m)
-    {
-        m->usetitle = m->useinput = false;
+    { // it doesn't need to exist
+        m->usetitle = m->useinput = m->world = false;
+        m->builtin = true;
         UI::addcb(m);
     }
-    else conoutf("cannot find menu 'loading'");
 }
 
 void mainmenu()
diff --git a/src/engine/model.h b/src/engine/model.h
index e17384b..d05338b 100644
--- a/src/engine/model.h
+++ b/src/engine/model.h
@@ -7,13 +7,13 @@ struct model
     float scale;
     vec translate;
     BIH *bih;
-    vec bbcenter, bbradius, bbextend;
-    float height, collideradius, collideheight;
+    vec bbcenter, bbradius, bbextend, collidecenter, collideradius;
+    float rejectradius, height, collidexyradius, collideheight;
     int batch;
 
-    model() : spinyaw(0), spinpitch(0), spinroll(0), offsetyaw(0), offsetpitch(0), offsetroll(0), collide(true), ellipsecollide(false), shadow(true), alphadepth(true), depthoffset(false), scale(1.0f), translate(0, 0, 0), bih(0), bbcenter(0, 0, 0), bbradius(0, 0, 0), bbextend(0, 0, 0), height(0.45f), collideradius(0), collideheight(0), batch(-1) {}
+    model() : spinyaw(0), spinpitch(0), spinroll(0), offsetyaw(0), offsetpitch(0), offsetroll(0), collide(true), ellipsecollide(false), shadow(true), alphadepth(true), depthoffset(false), scale(1.0f), translate(0, 0, 0), bih(0), bbcenter(0, 0, 0), bbradius(-1, -1, -1), bbextend(0, 0, 0), collidecenter(0, 0, 0), collideradius(-1, -1, -1), rejectradius(-1), height(0.45f), collidexyradius(0), collideheight(0), batch(-1) {}
     virtual ~model() { DELETEP(bih); }
-    virtual void calcbb(int frame, vec &center, vec &radius) = 0;
+    virtual void calcbb(vec &center, vec &radius) = 0;
     virtual void render(int anim, int basetime, int basetime2, const vec &o, float yaw, float pitch, float roll, dynent *d, modelattach *a = NULL, const vec &color = vec(0, 0, 0), const vec &dir = vec(0, 0, 0), const bvec *material = NULL, float transparent = 1, float size = 1) = 0;
     virtual bool load() = 0;
     virtual const char *name() const = 0;
@@ -42,43 +42,49 @@ struct model
     virtual void startrender() {}
     virtual void endrender() {}
 
-    void boundbox(int frame, vec &center, vec &radius)
+    void boundbox(vec &center, vec &radius)
     {
-        if(frame) calcbb(frame, center, radius);
-        else
+        if(bbradius.x < 0)
         {
-            if(bbradius.iszero()) calcbb(0, bbcenter, bbradius);
-            center = bbcenter;
-            radius = bbradius;
+            calcbb(bbcenter, bbradius);
+            bbradius.add(bbextend);
         }
-        radius.add(bbextend);
+        center = bbcenter;
+        radius = bbradius;
     }
 
-    void collisionbox(int frame, vec &center, vec &radius)
+    float collisionbox(vec &center, vec &radius)
     {
-        boundbox(frame, center, radius);
-        if(collideradius)
+        if(collideradius.x < 0)
         {
-            center[0] = center[1] = 0;
-            radius[0] = radius[1] = collideradius;
-        }
-        if(collideheight)
-        {
-            center[2] = radius[2] = collideheight/2;
+            boundbox(collidecenter, collideradius);
+            if(collidexyradius)
+            {
+                collidecenter.x = collidecenter.y = 0;
+                collideradius.x = collideradius.y = collidexyradius;
+            }
+            if(collideheight)
+            {
+                collidecenter.z = collideradius.z = collideheight/2;
+            }
+            rejectradius = collideradius.magnitude();
         }
+        center = collidecenter;
+        radius = collideradius;
+        return rejectradius;
     }
 
-    float boundsphere(int frame, vec &center)
+    float boundsphere(vec &center)
     {
         vec radius;
-        boundbox(frame, center, radius);
+        boundbox(center, radius);
         return radius.magnitude();
     }
 
-    float above(int frame = 0)
+    float above()
     {
         vec center, radius;
-        boundbox(frame, center, radius);
+        boundbox(center, radius);
         return center.z+radius.z;
     }
 };
diff --git a/src/engine/movie.cpp b/src/engine/movie.cpp
index 689144f..dfee128 100644
--- a/src/engine/movie.cpp
+++ b/src/engine/movie.cpp
@@ -373,7 +373,7 @@ struct aviwriter
         endlistchunk(); // LIST odml
 
         listchunk("LIST", "INFO");
-        const char *software = versionname;
+        const char *software = VERSION_NAME;
         writechunk("ISFT", software, strlen(software)+1);
         endlistchunk(); // LIST INFO
 
@@ -825,7 +825,7 @@ namespace recorder
         return 1.0f - float(dps)/float(dps+file->videofps); // strictly speaking should lock to read dps - 1.0=perfect, 0.5=half of frames are beingdropped
     }
 
-    int gettime()
+    int gettimet()
     {
         return inbetweenframes ? getclockmillis() : totalmillis;
     }
@@ -888,7 +888,7 @@ namespace recorder
         }
         else if(state == REC_OK)
         {
-            uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000;
+            uint nextframe = (max(gettimet() - starttime, 0)*file->videofps)/1000;
             soundbuffer &s = soundbuffers.add();
             s.load((uchar *)stream, len, nextframe);
         }
@@ -921,7 +921,7 @@ namespace recorder
         }
         conoutf("movie recording to: %s %dx%d @ %dfps%s", file->filename, file->videow, file->videoh, file->videofps, (file->soundfrequency>0)?" + sound":"");
 
-        starttime = gettime();
+        starttime = gettimet();
         loopi(file->videofps) stats[i] = 0;
         statsindex = 0;
         dps = 0;
@@ -980,7 +980,7 @@ namespace recorder
         shouldencode = shouldread = NULL;
         thread = NULL;
 
-        static const char *mesgs[] = { "ok", "stopped", "computer too slow", "file error"};
+        static const char * const mesgs[] = { "ok", "stopped", "computer too slow", "file error"};
         conoutf("movie recording halted: %s, %d frames", mesgs[state], file->videoframes);
 
         DELETEP(file);
@@ -1121,7 +1121,7 @@ namespace recorder
         }
         SDL_LockMutex(videolock);
         if(moviesync && videobuffers.full()) SDL_CondWait(shouldread, videolock);
-        uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000;
+        uint nextframe = (max(gettimet() - starttime, 0)*file->videofps)/1000;
         if(!videobuffers.full() && (lastframe == ~0U || nextframe > lastframe))
         {
             videobuffer &m = videobuffers.adding();
@@ -1169,9 +1169,9 @@ namespace recorder
         glDisable(GL_BLEND);
     }
 
-    void capture()
+    void capture(bool overlay)
     {
-        if(readbuffer()) drawhud();
+        if(readbuffer() && overlay) drawhud();
     }
 }
 
diff --git a/src/engine/mpr.h b/src/engine/mpr.h
index 9f2a6a3..12ccf58 100644
--- a/src/engine/mpr.h
+++ b/src/engine/mpr.h
@@ -5,11 +5,11 @@ namespace mpr
     struct CubePlanes
     {
         const clipplanes &p;
-    
+
         CubePlanes(const clipplanes &p) : p(p) {}
-    
+
         vec center() const { return p.o; }
-    
+
         vec supportpoint(const vec &n) const
         {
             int besti = 7;
@@ -22,77 +22,50 @@ namespace mpr
             return p.v[besti];
         }
     };
-    
+
     struct SolidCube
     {
         vec o;
         int size;
-    
+
         SolidCube(float x, float y, float z, int size) : o(x, y, z), size(size) {}
         SolidCube(const vec &o, int size) : o(o), size(size) {}
         SolidCube(const ivec &o, int size) : o(o.tovec()), size(size) {}
- 
+
         vec center() const { return vec(o).add(size/2); }
-    
+
         vec supportpoint(const vec &n) const
         {
             vec p(o);
             if(n.x > 0) p.x += size;
             if(n.y > 0) p.y += size;
-            if(n.z > 0) p.z += size; 
+            if(n.z > 0) p.z += size;
             return p;
         }
     };
-    
-    struct EntAABB
+
+    struct Ent
     {
         physent *ent;
 
-        EntAABB(physent *ent) : ent(ent) {}
-
-        vec center() const { vec o(ent->o); o.z += (ent->aboveeye - ent->height)/2; return o; }
-
-        vec contactface(const vec &n, const vec &dir) const
-        {
-            vec an(n.x*dir.x < 0 ? fabs(n.x)/ent->xradius : 0, n.y*dir.y < 0 ? fabs(n.y)/ent->yradius : 0, n.z*dir.z < 0 ? fabs(n.z)*2/(ent->aboveeye + ent->height) : 0),
-                fn(0, 0, 0);
-            if(an.x > an.y)
-            {
-                if(an.x > an.z) fn.x = n.x > 0 ? 1 : -1;
-                else if(an.z > 0) fn.z = n.z > 0 ? 1 : -1;
-            }
-            else if(an.y > an.z) fn.y = n.y > 0 ? 1 : -1;
-            else if(an.z > 0) fn.z = n.z > 0 ? 1 : -1;
-            return fn;
-        }
+        Ent(physent *ent) : ent(ent) {}
 
-        vec supportpoint(const vec &n) const
-        {
-            vec p(ent->o);
-            if(n.x > 0) p.x += ent->xradius;
-            else p.x -= ent->xradius;
-            if(n.y > 0) p.y += ent->yradius;
-            else p.y -= ent->yradius;
-            if(n.z > 0) p.z += ent->aboveeye;
-            else p.z -= ent->height;
-            return p;
-        }
+        vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->height)/2); }
     };
 
-    struct EntOBB
+    struct EntOBB : Ent
     {
-        physent *ent;
-        quat orient;
-        float zmargin;
-
-        EntOBB(physent *ent, float zmargin = 0) : ent(ent), orient(vec(0, 0, 1), ent->yaw*RAD), zmargin(zmargin) {}
+        matrix3x3 orient;
 
-        vec center() const { vec o(ent->o); o.z += (ent->aboveeye - ent->height - zmargin)/2; return o; }
+        EntOBB(physent *ent) : Ent(ent)
+        {
+            orient.setyaw(ent->yaw*RAD);
+        }
 
         vec contactface(const vec &wn, const vec &wdir) const
         {
-            vec n = orient.invertedrotate(wn).div(vec(ent->xradius, ent->yradius, (ent->aboveeye + ent->height + zmargin)/2)), 
-                dir = orient.invertedrotate(wdir), 
+            vec n = orient.transform(wn).div(vec(ent->xradius, ent->yradius, (ent->aboveeye + ent->height)/2)),
+                dir = orient.transform(wdir),
                 an(fabs(n.x), fabs(n.y), dir.z ? fabs(n.z) : 0),
                 fn(0, 0, 0);
             if(an.x > an.y)
@@ -102,37 +75,60 @@ namespace mpr
             }
             else if(an.y > an.z) fn.y = n.y*dir.y < 0 ? (n.y > 0 ? 1 : -1) : 0;
             else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
-            return orient.rotate(fn);
+            return orient.transposedtransform(fn);
+        }
+
+        vec localsupportpoint(const vec &ln) const
+        {
+            return vec(ln.x > 0 ? ent->xradius : -ent->xradius,
+                       ln.y > 0 ? ent->yradius : -ent->yradius,
+                       ln.z > 0 ? ent->aboveeye : -ent->height);
         }
 
         vec supportpoint(const vec &n) const
         {
-            vec ln = orient.invertedrotate(n), p(0, 0, 0);
-            if(ln.x > 0) p.x += ent->xradius;
-            else p.x -= ent->xradius;
-            if(ln.y > 0) p.y += ent->yradius;
-            else p.y -= ent->yradius;
-            if(ln.z > 0) p.z += ent->aboveeye;
-            else p.z -= ent->height + zmargin;
-            return orient.rotate(p).add(ent->o);
+            return orient.transposedtransform(localsupportpoint(orient.transform(n))).add(ent->o);
+        }
+
+        float supportcoordneg(float a, float b, float c) const
+        {
+            return localsupportpoint(vec(-a, -b, -c)).dot(vec(a, b, c));
         }
+        float supportcoord(float a, float b, float c) const
+        {
+            return localsupportpoint(vec(a, b, c)).dot(vec(a, b, c));
+        }
+
+        float left() const { return supportcoordneg(orient.a.x, orient.b.x, orient.c.x) + ent->o.x; }
+        float right() const { return supportcoord(orient.a.x, orient.b.x, orient.c.x) + ent->o.x; }
+        float back() const { return supportcoordneg(orient.a.y, orient.b.y, orient.c.y) + ent->o.y; }
+        float front() const { return supportcoord(orient.a.y, orient.b.y, orient.c.y) + ent->o.y; }
+        float bottom() const { return ent->o.z - ent->height; }
+        float top() const { return ent->o.z + ent->aboveeye; }
     };
 
-    struct EntCylinder
+    struct EntFuzzy : Ent
     {
-        physent *ent;
-        float zmargin;
- 
-        EntCylinder(physent *ent, float zmargin = 0) : ent(ent), zmargin(zmargin) {}
-    
-        vec center() const { vec o(ent->o); o.z += (ent->aboveeye - ent->height - zmargin)/2; return o; }
-   
+        EntFuzzy(physent *ent) : Ent(ent) {}
+
+        float left() const { return ent->o.x - ent->radius; }
+        float right() const { return ent->o.x + ent->radius; }
+        float back() const { return ent->o.y - ent->radius; }
+        float front() const { return ent->o.y + ent->radius; }
+        float bottom() const { return ent->o.z - ent->height; }
+        float top() const { return ent->o.z + ent->aboveeye; }
+    };
+
+    struct EntCylinder : EntFuzzy
+    {
+        EntCylinder(physent *ent) : EntFuzzy(ent) {}
+
         vec contactface(const vec &n, const vec &dir) const
         {
-            float dxy = n.dot2(n)/(ent->radius*ent->radius), dz = n.z*n.z*4/(ent->aboveeye + ent->height + zmargin);
+            float dxy = n.dot2(n)/(ent->radius*ent->radius), dz = n.z*n.z*4/(ent->aboveeye + ent->height);
             vec fn(0, 0, 0);
             if(dz > dxy && dir.z) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
-            else if(n.dot2(dir) < 0) 
+            else if(n.dot2(dir) < 0)
             {
                 fn.x = n.x;
                 fn.y = n.y;
@@ -140,12 +136,12 @@ namespace mpr
             }
             return fn;
         }
- 
+
         vec supportpoint(const vec &n) const
         {
             vec p(ent->o);
             if(n.z > 0) p.z += ent->aboveeye;
-            else p.z -= ent->height + zmargin;
+            else p.z -= ent->height;
             if(n.x || n.y)
             {
                 float r = ent->radius / n.magnitude2();
@@ -156,13 +152,9 @@ namespace mpr
         }
     };
 
-    struct EntCapsule
+    struct EntCapsule : EntFuzzy
     {
-        physent *ent;
-
-        EntCapsule(physent *ent) : ent(ent) {}
-
-        vec center() const { vec o(ent->o); o.z += (ent->aboveeye - ent->height)/2; return o; }
+        EntCapsule(physent *ent) : EntFuzzy(ent) {}
 
         vec supportpoint(const vec &n) const
         {
@@ -174,40 +166,46 @@ namespace mpr
         }
     };
 
-    struct EntEllipsoid
+    struct EntEllipsoid : EntFuzzy
     {
-        physent *ent;
-
-        EntEllipsoid(physent *ent) : ent(ent) {}
-
-        vec center() const { vec o(ent->o); o.z += (ent->aboveeye - ent->height)/2; return o; }
+        EntEllipsoid(physent *ent) : EntFuzzy(ent) {}
 
         vec supportpoint(const vec &dir) const
         {
             vec p(ent->o), n = vec(dir).normalize();
             p.x += ent->radius*n.x;
-            p.y += ent->radius*n.y; 
+            p.y += ent->radius*n.y;
             p.z += (ent->aboveeye + ent->height)/2*(1 + n.z) - ent->height;
             return p;
         }
     };
 
-    struct ModelOBB
+    struct Model
     {
         vec o, radius;
-        quat orient;
+        matrix3x3 orient;
 
-        ModelOBB(const vec &ent, const vec &center, const vec &radius, int yaw, int roll) : o(ent), radius(radius), orient(vec(0, 0, 1), yaw*RAD) 
+        Model(const vec &ent, const vec &center, const vec &radius, int yaw, int pitch, int roll) : o(ent), radius(radius)
         {
-            if(roll) orient.mul(quat(vec(-1, 0, 0), roll*RAD), quat(orient));
-            o.add(orient.rotate(center));
+            orient.identity();
+            if(pitch) orient.rotate_around_y(sincosmod360(pitch));
+            if(roll) orient.rotate_around_x(sincosmod360(roll));
+            if(yaw) orient.rotate_around_z(sincosmod360(-yaw));
+            o.add(orient.transposedtransform(center));
         }
 
         vec center() const { return o; }
+    };
+
+    struct ModelOBB : Model
+    {
+        ModelOBB(const vec &ent, const vec &center, const vec &radius, int yaw, int pitch, int roll) :
+            Model(ent, center, radius, yaw, pitch, roll)
+        {}
 
         vec contactface(const vec &wn, const vec &wdir) const
         {
-            vec n = orient.invertedrotate(wn).div(radius), dir = orient.invertedrotate(wdir),
+            vec n = orient.transform(wn).div(radius), dir = orient.transform(wdir),
                 an(fabs(n.x), fabs(n.y), dir.z ? fabs(n.z) : 0),
                 fn(0, 0, 0);
             if(an.x > an.y)
@@ -217,104 +215,97 @@ namespace mpr
             }
             else if(an.y > an.z) fn.y = n.y*dir.y < 0 ? (n.y > 0 ? 1 : -1) : 0;
             else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
-            return orient.rotate(fn);
+            return orient.transposedtransform(fn);
         }
 
         vec supportpoint(const vec &n) const
         {
-            vec ln = orient.invertedrotate(n), p(0, 0, 0);
+            vec ln = orient.transform(n), p(0, 0, 0);
             if(ln.x > 0) p.x += radius.x;
             else p.x -= radius.x;
             if(ln.y > 0) p.y += radius.y;
             else p.y -= radius.y;
             if(ln.z > 0) p.z += radius.z;
             else p.z -= radius.z;
-            return orient.rotate(p).add(o);
+            return orient.transposedtransform(p).add(o);
         }
     };
 
-    struct ModelEllipse
+    struct ModelEllipse : Model
     {
-        vec o, radius;
-        quat orient;
-
-        ModelEllipse(const vec &ent, const vec &center, const vec &radius, int yaw, int roll) : o(ent), radius(radius), orient(vec(0, 0, 1), yaw*RAD) 
-        {
-            if(roll) orient.mul(quat(vec(-1, 0, 0), roll*RAD), quat(orient));
-            o.add(orient.rotate(center));
-        }
-
-        vec center() const { return o; }
+        ModelEllipse(const vec &ent, const vec &center, const vec &radius, int yaw, int pitch, int roll) :
+            Model(ent, center, radius, yaw, pitch, roll)
+        {}
 
         vec contactface(const vec &wn, const vec &wdir) const
         {
-            vec n = orient.invertedrotate(wn).div(radius), dir = orient.invertedrotate(wdir);
+            vec n = orient.transform(wn).div(radius), dir = orient.transform(wdir);
             float dxy = n.dot2(n), dz = n.z*n.z;
             vec fn(0, 0, 0);
             if(dz > dxy && dir.z) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
-            else if(n.dot2(dir) < 0) 
+            else if(n.dot2(dir) < 0)
             {
-                fn.x = n.x*radius.x;
-                fn.y = n.y*radius.y;
+                fn.x = n.x*radius.y;
+                fn.y = n.y*radius.x;
                 fn.normalize();
             }
-            return orient.rotate(fn);
+            return orient.transposedtransform(fn);
         }
 
         vec supportpoint(const vec &n) const
         {
-            vec ln = orient.invertedrotate(n), p(0, 0, 0);
+            vec ln = orient.transform(n), p(0, 0, 0);
             if(ln.z > 0) p.z += radius.z;
             else p.z -= radius.z;
             if(ln.x || ln.y)
             {
-                float r = n.magnitude2();
+                float r = ln.magnitude2();
                 p.x += ln.x*radius.x/r;
                 p.y += ln.y*radius.y/r;
             }
-            return orient.rotate(p).add(o);
+            return orient.transposedtransform(p).add(o);
         }
     };
- 
+
     const float boundarytolerance = 1e-3f;
-        
+
     template<class T, class U>
     bool collide(const T &p1, const U &p2)
     {
         // v0 = center of Minkowski difference
         vec v0 = p2.center().sub(p1.center());
         if(v0.iszero()) return true;  // v0 and origin overlap ==> hit
-    
+
         // v1 = support in direction of origin
         vec n = vec(v0).neg();
         vec v1 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
         if(v1.dot(n) <= 0) return false;  // origin outside v1 support plane ==> miss
-    
+
         // v2 = support perpendicular to plane containing origin, v0 and v1
         n.cross(v1, v0);
         if(n.iszero()) return true;   // v0, v1 and origin colinear (and origin inside v1 support plane) == > hit
         vec v2 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
         if(v2.dot(n) <= 0) return false;  // origin outside v2 support plane ==> miss
-    
+
         // v3 = support perpendicular to plane containing v0, v1 and v2
         n.cross(v0, v1, v2);
-    
+
         // If the origin is on the - side of the plane, reverse the direction of the plane
         if(n.dot(v0) > 0)
         {
             swap(v1, v2);
             n.neg();
         }
-    
+
         ///
         // Phase One: Find a valid portal
-    
+
         loopi(100)
         {
             // Obtain the next support point
             vec v3 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
             if(v3.dot(n) <= 0) return false;  // origin outside v3 support plane ==> miss
-    
+
             // If origin is outside (v1,v0,v3), then portal is invalid -- eliminate v2 and find new support outside face
             vec v3xv0;
             v3xv0.cross(v3, v0);
@@ -324,7 +315,7 @@ namespace mpr
                 n.cross(v0, v1, v3);
                 continue;
             }
-    
+
             // If origin is outside (v3,v0,v2), then portal is invalid -- eliminate v1 and find new support outside face
             if(v2.dot(v3xv0) > 0)
             {
@@ -332,23 +323,23 @@ namespace mpr
                 n.cross(v0, v3, v2);
                 continue;
             }
-    
+
             ///
             // Phase Two: Refine the portal
-    
+
             for(int j = 0;; j++)
             {
                 // Compute outward facing normal of the portal
                 n.cross(v1, v2, v3);
-    
+
                 // If the origin is inside the portal, we have a hit
                 if(n.dot(v1) >= 0) return true;
-    
+
                 n.normalize();
 
                 // Find the support point in the direction of the portal's normal
                 vec v4 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
-    
+
                 // If the origin is outside the support plane or the boundary is thin enough, we have a miss
                 if(v4.dot(n) <= 0 || vec(v4).sub(v3).dot(n) <= boundarytolerance || j > 100) return false;
 
@@ -381,10 +372,10 @@ namespace mpr
         vec v01 = p1.center();
         vec v02 = p2.center();
         vec v0 = vec(v02).sub(v01);
-    
+
         // Avoid case where centers overlap -- any direction is fine in this case
         if(v0.iszero()) v0 = vec(0, 0, 1e-5f);
-    
+
         // v1 = support in direction of origin
         vec n = vec(v0).neg();
         vec v11 = p1.supportpoint(vec(n).neg());
@@ -395,7 +386,7 @@ namespace mpr
             if(contactnormal) *contactnormal = n;
             return false;
         }
-    
+
         // v2 - support perpendicular to v1,v0
         n.cross(v1, v0);
         if(n.iszero())
@@ -415,7 +406,7 @@ namespace mpr
             if(contactnormal) *contactnormal = n;
             return false;
         }
-    
+
         // Determine whether origin is on + or - side of plane (v1,v0,v2)
         n.cross(v0, v1, v2);
         ASSERT( !n.iszero() );
@@ -427,10 +418,10 @@ namespace mpr
             swap(v12, v22);
             n.neg();
         }
-    
+
         ///
         // Phase One: Identify a portal
-    
+
         loopi(100)
         {
             // Obtain the support point in a direction perpendicular to the existing plane
@@ -443,7 +434,7 @@ namespace mpr
                 if(contactnormal) *contactnormal = n;
                 return false;
             }
-    
+
             // If origin is outside (v1,v0,v3), then eliminate v2 and loop
             vec v3xv0;
             v3xv0.cross(v3, v0);
@@ -455,7 +446,7 @@ namespace mpr
                 n.cross(v0, v1, v3);
                 continue;
             }
-    
+
             // If origin is outside (v3,v0,v2), then eliminate v1 and loop
             if(v2.dot(v3xv0) > 0)
             {
@@ -465,32 +456,32 @@ namespace mpr
                 n.cross(v0, v3, v2);
                 continue;
             }
-    
+
             bool hit = false;
-    
+
             ///
             // Phase Two: Refine the portal
-    
+
             // We are now inside of a wedge...
             for(int j = 0;; j++)
             {
                 // Compute normal of the wedge face
                 n.cross(v1, v2, v3);
-    
+
                 // Can this happen???  Can it be handled more cleanly?
                 if(n.iszero())
                 {
                     ASSERT(0);
                     return true;
                 }
-    
+
                 n.normalize();
-    
+
                 // If the origin is inside the wedge, we have a hit
                 if(n.dot(v1) >= 0 && !hit)
                 {
                     if(contactnormal) *contactnormal = n;
-    
+
                     // Compute the barycentric coordinates of the origin
                     if(contactpoint1 || contactpoint2)
                     {
@@ -512,23 +503,23 @@ namespace mpr
                         if(contactpoint2)
                             *contactpoint2 = (vec(v02).mul(b0).add(vec(v12).mul(b1)).add(vec(v22).mul(b2)).add(vec(v32).mul(b3))).mul(1.0f/sum);
                     }
-    
+
                     // HIT!!!
                     hit = true;
                 }
-    
+
                 // Find the support point in the direction of the wedge face
                 vec v41 = p1.supportpoint(vec(n).neg());
                 vec v42 = p2.supportpoint(n);
                 vec v4 = vec(v42).sub(v41);
-    
+
                 // If the boundary is thin enough or the origin is outside the support plane for the newly discovered vertex, then we can terminate
                 if(v4.dot(n) <= 0 || vec(v4).sub(v3).dot(n) <= boundarytolerance || j > 100)
                 {
                     if(contactnormal) *contactnormal = n;
                     return hit;
                 }
-  
+
                 // Test origin against the three planes that separate the new portal candidates: (v1,v4,v0) (v2,v4,v0) (v3,v4,v0)
                 // Note:  We're taking advantage of the triple product identities here as an optimization
                 //        (v1 % v4) * v0 == v1 * (v4 % v0)    > 0 if origin inside (v1, v4, v0)
diff --git a/src/engine/octa.cpp b/src/engine/octa.cpp
index a6b2b20..915f1e5 100644
--- a/src/engine/octa.cpp
+++ b/src/engine/octa.cpp
@@ -659,6 +659,7 @@ static inline void gencubevert(const cube &c, int i, T &v)
 {
     switch(i)
     {
+        default:
 #define GENCUBEVERT(n, x, y, z) \
         case n: \
             v = T(edgeget(cubeedge(c, 0, y, z), x), \
@@ -672,9 +673,9 @@ static inline void gencubevert(const cube &c, int i, T &v)
 
 void genfaceverts(const cube &c, int orient, ivec v[4])
 {
-
     switch(orient)
     {
+        default:
 #define GENFACEORIENT(o, v0, v1, v2, v3) \
         case o: v0 v1 v2 v3 break;
 #define GENFACEVERT(o, n, x,y,z, xv,yv,zv) \
@@ -1114,7 +1115,7 @@ int visibletris(const cube &c, int orient, int x, int y, int z, int size, ushort
     int convex = (e3 = v[0]).sub(v[3]).dot(n);
     if(!convex)
     {
-        if(ivec().cross(e3, e2).iszero()) { if(n.iszero()) return 0; vis = 1; touching = 0xF&~(1<<3); }
+        if(ivec().cross(e3, e2).iszero() || v[1] == v[3]) { if(n.iszero()) return 0; vis = 1; touching = 0xF&~(1<<3); }
         else if(n.iszero()) { vis = 2; touching = 0xF&~(1<<1); }
     }
 
diff --git a/src/engine/octa.h b/src/engine/octa.h
index b90f8bb..fcc8241 100644
--- a/src/engine/octa.h
+++ b/src/engine/octa.h
@@ -17,15 +17,10 @@ enum
 
 struct materialsurface
 {
-    enum
-    {
-        F_EDIT = 1<<0
-    };
-
     ivec o;
     ushort csize, rsize;
     ushort material, skip;
-    uchar orient, flags;
+    uchar orient, visible;
     union
     {
         short index;
@@ -274,7 +269,21 @@ const uint F_SOLID = 0x80808080;    // all edges in the range (0,8)
 #define octaindex(d,x,y,z)  (((z)<<D[d])+((y)<<C[d])+((x)<<R[d]))
 #define octastep(x, y, z, scale) (((((z)>>(scale))&1)<<2) | ((((y)>>(scale))&1)<<1) | (((x)>>(scale))&1))
 
-#define loopoctabox(c, size, o, s) uchar possible = octantrectangleoverlap(c, size, o, s); loopi(8) if(possible&(1<<i))
+static inline uchar octaboxoverlap(const ivec &o, int size, const ivec &bbmin, const ivec &bbmax)
+{
+    uchar p = 0xFF; // bitmask of possible collisions with octants. 0 bit = 0 octant, etc
+    ivec mid = ivec(o).add(size);
+    if(mid.z <= bbmin.z)      p &= 0xF0; // not in a -ve Z octant
+    else if(mid.z >= bbmax.z) p &= 0x0F; // not in a +ve Z octant
+    if(mid.y <= bbmin.y)      p &= 0xCC; // not in a -ve Y octant
+    else if(mid.y >= bbmax.y) p &= 0x33; // etc..
+    if(mid.x <= bbmin.x)      p &= 0xAA;
+    else if(mid.x >= bbmax.x) p &= 0x55;
+    return p;
+}
+
+#define loopoctabox(o, size, bbmin, bbmax) uchar possible = octaboxoverlap(o, size, bbmin, bbmax); loopi(8) if(possible&(1<<i))
+#define loopoctaboxsize(o, size, bborigin, bbsize) uchar possible = octaboxoverlap(o, size, bborigin, ivec(bborigin).add(bbsize)); loopi(8) if(possible&(1<<i))
 
 enum
 {
diff --git a/src/engine/octaedit.cpp b/src/engine/octaedit.cpp
index ba38c28..5991cf9 100644
--- a/src/engine/octaedit.cpp
+++ b/src/engine/octaedit.cpp
@@ -6,13 +6,13 @@ VAR(0, showpastegrid, 0, 0, 1);
 VAR(0, showcursorgrid, 0, 0, 1);
 VAR(0, showselgrid, 0, 0, 1);
 
+bool boxoutline = false;
+
 void boxs(int orient, vec o, const vec &s)
 {
-    int d = dimension(orient),
-          dc= dimcoord(orient);
-
-    float f = !outline && !(fullbright && blankgeom) ? 0 : (dc>0 ? 0.2f : -0.2f);
-    o[D[d]] += float(dc) * s[D[d]] + f,
+    int d = dimension(orient), dc = dimcoord(orient);
+    float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0;
+    o[D[d]] += dc * s[D[d]] + f;
 
     glBegin(GL_LINE_LOOP);
 
@@ -34,13 +34,12 @@ void boxs3D(const vec &o, vec s, int g)
 
 void boxsgrid(int orient, vec o, vec s, int g)
 {
-    int d = dimension(orient),
-          dc= dimcoord(orient);
+    int d = dimension(orient), dc = dimcoord(orient);
     float ox = o[R[d]],
           oy = o[C[d]],
           xs = s[R[d]],
           ys = s[C[d]],
-          f = !outline && !(fullbright && blankgeom) ? 0 : (dc>0 ? 0.2f : -0.2f);
+          f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0;
 
     o[D[d]] += dc * s[D[d]]*g + f;
 
@@ -63,7 +62,7 @@ void boxsgrid(int orient, vec o, vec s, int g)
     xtraverts += 2*int(xs+ys);
 }
 
-selinfo sel, lastsel;
+selinfo sel, lastsel, savedsel;
 
 int orient = 0;
 int gridsize = 8;
@@ -86,12 +85,16 @@ VARF(0, dragging, 0, 0, 1,
     sel.orient = orient;
 );
 
-VARF(0, moving, 0, 0, 1,
-    if(!moving) return;
-    vec v(cur.v); v.add(1);
-    moving = pointinsel(sel, v);
-    if(moving) havesel = false; // tell cursorupdate to create handle
-);
+int moving = 0;
+ICOMMAND(0, moving, "b", (int *n),
+{
+    if(*n >= 0)
+    {
+        if(!*n || (moving<=1 && !pointinsel(sel, cur.tovec().add(1)))) moving = 0;
+        else if(!moving) moving = 1;
+    }
+    intret(moving);
+});
 
 VARF(0, gridpower, 0, 3, 12,
 {
@@ -108,11 +111,14 @@ VARF(0, hmapedit, 0, 0, 1, horient = sel.orient);
 
 void forcenextundo() { lastsel.orient = -1; }
 
+extern void hmapcancel();
+
 void cubecancel()
 {
     havesel = false;
     moving = dragging = hmapedit = passthroughsel = 0;
     forcenextundo();
+    hmapcancel();
 }
 
 void cancelsel()
@@ -179,6 +185,11 @@ COMMAND(0, cancelsel, "");
 COMMAND(0, reorient, "");
 COMMAND(0, selextend, "");
 
+ICOMMAND(0, selmoved, "", (), { if(noedit(true)) return; intret(sel.o != savedsel.o ? 1 : 0); });
+ICOMMAND(0, selsave, "", (), { if(noedit(true)) return; savedsel = sel; });
+ICOMMAND(0, selrestore, "", (), { if(noedit(true)) return; sel = savedsel; });
+ICOMMAND(0, selswap, "", (), { if(noedit(true)) return; swap(sel, savedsel); });
+
 ///////// selection support /////////////
 
 cube &blockcube(int x, int y, int z, const block3 &b, int rgrid) // looks up a world cube, based on coordinates mapped by the block
@@ -203,9 +214,8 @@ ICOMMAND(0, havesel, "", (), intret(havesel ? selchildcount : 0));
 
 void countselchild(cube *c, const ivec &cor, int size)
 {
-    ivec ss(sel.s);
-    ss.mul(sel.grid);
-    loopoctabox(cor, size, sel.o, ss)
+    ivec ss = ivec(sel.s).mul(sel.grid);
+    loopoctaboxsize(cor, size, sel.o, ss)
     {
         ivec o(i, cor.x, cor.y, cor.z, size);
         if(c[i].children) countselchild(c[i].children, o, size/2);
@@ -248,25 +258,19 @@ void updateselection()
     sel.s.z = abs(lastcur.z-cur.z)/sel.grid+1;
 }
 
-void editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first)
+bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first)
 {
     plane pl(d, off);
     float dist = 0.0f;
-
     physent *player = (physent *)game::focusedent(true);
     if(!player) player = camera1;
-    if(pl.rayintersect(player->o, ray, dist))
-    {
-        dest = ray;
-        dest.mul(dist);
-        dest.add(player->o);
-        if(first)
-        {
-            handle = dest;
-            handle.sub(o);
-        }
-        dest.sub(handle);
-    }
+    if(!pl.rayintersect(player->o, ray, dist))
+        return false;
+
+    dest = vec(ray).mul(dist).add(player->o);
+    if(first) handle = vec(dest).sub(o);
+    dest.sub(handle);
+    return true;
 }
 
 inline bool isheightmap(int orient, int d, bool empty, cube *c);
@@ -296,19 +300,20 @@ void rendereditcursor()
 
     if(moving)
     {
-        ivec e;
-        static vec v, handle;
-        editmoveplane(sel.o.tovec(), ray, od, sel.o[D[od]]+odc*sel.grid*sel.s[D[od]], handle, v, !havesel);
-        if(!havesel)
+        static vec dest, handle;
+        if(editmoveplane(sel.o.tovec(), camdir, od, sel.o[D[od]]+odc*sel.grid*sel.s[D[od]], handle, dest, moving==1))
         {
-            v.add(handle);
-            (e = handle).mask(~(sel.grid-1));
-            v.sub(handle = e.tovec());
-            havesel = true;
+            if(moving==1)
+            {
+                dest.add(handle);
+                handle = ivec(handle).mask(~(sel.grid-1)).tovec();
+                dest.sub(handle);
+                moving = 2;
+            }
+            ivec o = ivec(dest).mask(~(sel.grid-1));
+            sel.o[R[od]] = o[R[od]];
+            sel.o[C[od]] = o[C[od]];
         }
-        (e = v).mask(~(sel.grid-1));
-        sel.o[R[od]] = e[R[od]];
-        sel.o[C[od]] = e[C[od]];
     }
     else if(entmoving)
     {
@@ -327,7 +332,7 @@ void rendereditcursor()
                                             | (passthroughcube==1 ? RAY_PASS : 0), gridsize, entorient, ent);
 
         if((havesel || dragging) && !passthroughsel && !hmapedit)     // now try selecting the selection
-            if(rayrectintersect(sel.o.tovec(), vec(sel.s.tovec()).mul(sel.grid), player->o, ray, sdist, orient))
+            if(rayboxintersect(sel.o.tovec(), vec(sel.s.tovec()).mul(sel.grid), player->o, ray, sdist, orient))
             {   // and choose the nearest of the two
                 if(sdist < wdist)
                 {
@@ -362,7 +367,7 @@ void rendereditcursor()
             if(gridlookup && !dragging && !moving && !havesel && hmapedit!=1) gridsize = lusize;
             int mag = gridsize && lusize ? lusize / gridsize : 0;
             normalizelookupcube(int(w.x), int(w.y), int(w.z));
-            if(sdist == 0 || sdist > wdist) rayrectintersect(lu.tovec(), vec(gridsize), player->o, ray, t=0, orient); // just getting orient
+            if(sdist == 0 || sdist > wdist) rayboxintersect(lu.tovec(), vec(gridsize), player->o, ray, t=0, orient); // just getting orient
             cur = lu;
             cor = vec(w).mul(2).div(gridsize);
             od = dimension(orient);
@@ -434,6 +439,8 @@ void rendereditcursor()
 
     renderentselection(player->o, ray, entmoving!=0);
 
+    boxoutline = outline || (fullbright && blankgeom);
+
     enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
 
     #define planargrid(q,r,s) \
@@ -460,7 +467,7 @@ void rendereditcursor()
     }
 
     // selections
-    if(havesel)
+    if(havesel || moving)
     {
         d = dimension(sel.orient);
         glColor3ub(120, 120, 120);  // grid
@@ -487,12 +494,14 @@ void rendereditcursor()
 
     if(showpastegrid && localedit && localedit->copy)
     {
-        glColor3ub(30, 0, 30);
+        glColor3ub(0, 192, 192);
         boxs3D(havesel ? sel.o.tovec() : lu.tovec(), localedit->copy->s.tovec(), havesel ? sel.grid : gridsize);
     }
 
     disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
 
+    boxoutline = false;
+
     notextureshader->set();
 
     glDisable(GL_BLEND);
@@ -502,9 +511,9 @@ void rendereditcursor()
 
 static bool haschanged = false;
 
-void readychanges(block3 &b, cube *c, const ivec &cor, int size)
+void readychanges(const ivec &bbmin, const ivec &bbmax, cube *c, const ivec &cor, int size)
 {
-    loopoctabox(cor, size, b.o, b.s)
+    loopoctabox(cor, size, bbmin, bbmax)
     {
         ivec o(i, cor.x, cor.y, cor.z, size);
         if(c[i].ext)
@@ -527,7 +536,7 @@ void readychanges(block3 &b, cube *c, const ivec &cor, int size)
                 discardchildren(c[i], true);
                 brightencube(c[i]);
             }
-            else readychanges(b, c[i].children, o, size/2);
+            else readychanges(bbmin, bbmax, c[i].children, o, size/2);
         }
         else brightencube(c[i]);
     }
@@ -554,17 +563,7 @@ void commitchanges(bool force)
 void changed(const block3 &sel, bool commit = true)
 {
     if(sel.s.iszero()) return;
-    block3 b = sel;
-    loopi(3) b.s[i] *= b.grid;
-    b.grid = 1;
-    loopi(3)                    // the changed blocks are the selected cubes
-    {
-        b.o[i] -= 1;
-        b.s[i] += 2;
-        readychanges(b, worldroot, ivec(0, 0, 0), hdr.worldsize/2);
-        b.o[i] += 1;
-        b.s[i] -= 2;
-    }
+    readychanges(ivec(sel.o).sub(1), ivec(sel.s).mul(sel.grid).add(sel.o).add(1), worldroot, ivec(0, 0, 0), hdr.worldsize/2);
     haschanged = true;
 
     if(commit) commitchanges();
@@ -696,7 +695,7 @@ struct undolist
 
 undolist undos, redos;
 VAR(IDF_PERSIST, undomegs, 0, 8, 100);                              // bounded by n megs
-int totalundos = 0;
+VAR(IDF_READONLY, totalundos, 1, 0, -1);
 
 void pruneundos(int maxremain)                          // bound memory
 {
@@ -937,58 +936,58 @@ void freeeditinfo(editinfo *&e)
     e = NULL;
 }
 
-struct octabrushheader
+struct prefabheader
 {
     char magic[4];
     int version;
 };
 
-struct octabrush : editinfo
+struct prefab : editinfo
 {
     char *name;
 
-    octabrush() : name(NULL) {}
-    ~octabrush() { DELETEA(name); if(copy) freeblock(copy); }
+    prefab() : name(NULL) {}
+    ~prefab() { DELETEA(name); if(copy) freeblock(copy); }
 };
 
-static inline bool htcmp(const char *key, const octabrush &b) { return !strcmp(key, b.name); }
+static inline bool htcmp(const char *key, const prefab &b) { return !strcmp(key, b.name); }
 
-static hashset<octabrush> octabrushes;
+static hashset<prefab> prefabs;
 
-void delbrush(char *name)
+void delprefab(char *name)
 {
-    if(octabrushes.remove(name))
-        conoutf("deleted brush %s", name);
+    if(prefabs.remove(name))
+        conoutf("deleted prefab %s", name);
 }
-COMMAND(0, delbrush, "s");
+COMMAND(0, delprefab, "s");
 
-void savebrush(char *name)
+void saveprefab(char *name)
 {
     if(!name[0] || noedit(true) || multiplayer()) return;
-    octabrush *b = octabrushes.access(name);
+    prefab *b = prefabs.access(name);
     if(!b)
     {
-        b = &octabrushes[name];
+        b = &prefabs[name];
         b->name = newstring(name);
     }
     if(b->copy) freeblock(b->copy);
     protectsel(b->copy = blockcopy(block3(sel), sel.grid));
     changed(sel);
-    defformatstring(filename)(strpbrk(name, "/\\") ? "%s.obr" : "brush/%s.obr", name);
+    defformatstring(filename)(strpbrk(name, "/\\") ? "%s.obr" : "prefab/%s.obr", name);
     path(filename);
     stream *f = opengzfile(filename, "wb");
-    if(!f) { conoutf("\frcould not write brush to %s", filename); return; }
-    octabrushheader hdr;
+    if(!f) { conoutf("\frcould not write prefab to %s", filename); return; }
+    prefabheader hdr;
     memcpy(hdr.magic, "OEBR", 4);
     hdr.version = 0;
     lilswap(&hdr.version, 1);
     f->write(&hdr, sizeof(hdr));
     streambuf<uchar> s(f);
-    if(!packblock(*b->copy, s)) { delete f; conoutf("\frcould not pack brush %s", filename); return; }
+    if(!packblock(*b->copy, s)) { delete f; conoutf("\frcould not pack prefab %s", filename); return; }
     delete f;
-    conoutf("wrote brush file %s", filename);
+    conoutf("wrote prefab file %s", filename);
 }
-COMMAND(0, savebrush, "s");
+ICOMMAND(0, saveprefab, "s", (char *s), if(!(identflags&IDF_WORLD)) saveprefab(s));
 
 void pasteblock(block3 &b, selinfo &sel, bool local)
 {
@@ -1000,31 +999,31 @@ void pasteblock(block3 &b, selinfo &sel, bool local)
     sel.orient = o;
 }
 
-void pastebrush(char *name)
+void pasteprefab(char *name)
 {
     if(!name[0] || noedit() || multiplayer()) return;
-    octabrush *b = octabrushes.access(name);
+    prefab *b = prefabs.access(name);
     if(!b)
     {
-        defformatstring(filename)(strpbrk(name, "/\\") ? "%s.obr" : "brush/%s.obr", name);
+        defformatstring(filename)(strpbrk(name, "/\\") ? "%s.obr" : "prefab/%s.obr", name);
         path(filename);
         stream *f = opengzfile(filename, "rb");
-        if(!f) { conoutf("\frcould not read brush %s", filename); return; }
-        octabrushheader hdr;
-        if(f->read(&hdr, sizeof(hdr)) != sizeof(octabrushheader) || memcmp(hdr.magic, "OEBR", 4)) { delete f; conoutf("\frbrush %s has malformatted header", filename); return; }
+        if(!f) { conoutf("\frcould not read prefab %s", filename); return; }
+        prefabheader hdr;
+        if(f->read(&hdr, sizeof(hdr)) != sizeof(prefabheader) || memcmp(hdr.magic, "OEBR", 4)) { delete f; conoutf("\frprefab %s has malformatted header", filename); return; }
         lilswap(&hdr.version, 1);
-        if(hdr.version != 0) { delete f; conoutf("\frbrush %s uses unsupported version", filename); return; }
+        if(hdr.version != 0) { delete f; conoutf("\frprefab %s uses unsupported version", filename); return; }
         streambuf<uchar> s(f);
         block3 *copy = NULL;
-        if(!unpackblock(copy, s)) { delete f; conoutf("\frcould not unpack brush %s", filename); return; }
+        if(!unpackblock(copy, s)) { delete f; conoutf("\frcould not unpack prefab %s", filename); return; }
         delete f;
-        b = &octabrushes[name];
+        b = &prefabs[name];
         b->name = newstring(name);
         b->copy = copy;
     }
     pasteblock(*b->copy, sel, true);
 }
-COMMAND(0, pastebrush, "s");
+COMMAND(0, pasteprefab, "s");
 
 void mpcopy(editinfo *&e, selinfo &sel, bool local)
 {
@@ -1052,8 +1051,7 @@ void copy()
 void pasteclear()
 {
     if(!localedit) return;
-    if(localedit->copy) freeblock(localedit->copy);
-    localedit->copy = NULL;
+    freeeditinfo(localedit);
 }
 
 void pastehilight()
@@ -1066,7 +1064,7 @@ void pastehilight()
 
 void paste()
 {
-    if(noedit()) return;
+    if(noedit() || !localedit) return;
     mppaste(localedit, sel, true);
 }
 
@@ -1171,7 +1169,8 @@ vector<int> htextures;
 
 COMMAND(0, clearbrush, "");
 COMMAND(0, brushvert, "iii");
-ICOMMAND(0, hmapcancel, "", (), htextures.setsize(0); );
+void hmapcancel() { htextures.setsize(0); }
+COMMAND(0, hmapcancel, "");
 ICOMMAND(0, hmapselect, "", (),
     int t = lookupcube(cur.x, cur.y, cur.z).texture[orient];
     int i = htextures.find(t);
@@ -1181,21 +1180,12 @@ ICOMMAND(0, hmapselect, "", (),
         htextures.remove(i);
 );
 
-inline bool ishtexture(int t)
-{
-    loopv(htextures)
-        if(t == htextures[i])
-            return false;
-    return true;
-}
-
-VAR(0, bypassheightmapcheck, 0, 0, 1);    // temp
-
 inline bool isheightmap(int o, int d, bool empty, cube *c)
 {
     return havesel ||
            (empty && isempty(*c)) ||
-           ishtexture(c->texture[o]);
+           htextures.empty() ||
+           htextures.find(c->texture[o]) >= 0;
 }
 
 namespace hmap
@@ -1663,7 +1653,7 @@ vector<ushort> texmru;
 void tofronttex()                                       // maintain most recently used of the texture lists when applying texture
 {
     int c = curtexindex;
-    if(c>=0)
+    if(texmru.inrange(c))
     {
         texmru.insert(0, texmru.remove(c));
         curtexindex = -1;
@@ -1854,7 +1844,7 @@ void valpha(float *front, float *back)
 }
 COMMAND(0, valpha, "ff");
 
-void vcolor(float *r, float *g, float *b)
+void vcolour(float *r, float *g, float *b)
 {
     if(noedit() || multiplayer()) return;
     VSlot ds;
@@ -1862,7 +1852,7 @@ void vcolor(float *r, float *g, float *b)
     ds.colorscale = vec(max(*r, 0.0f), max(*g, 0.0f), max(*b, 0.0f));
     mpeditvslot(ds, allfaces, sel, true);
 }
-COMMAND(0, vcolor, "fff");
+COMMAND(0, vcolour, "fff");
 
 void vpalette(int *p, int *x)
 {
@@ -1883,7 +1873,7 @@ void vcoastscale(float *value)
     ds.coastscale = clamp(*value, 0.f, 1000.f);
     mpeditvslot(ds, allfaces, sel, true);
 }
-COMMAND(0, vcoastscale, "fff");
+COMMAND(0, vcoastscale, "f");
 
 void vreset()
 {
@@ -1977,6 +1967,7 @@ void edittex_(int *dir)
 {
     if(noedit()) return;
     filltexlist();
+    if(texmru.empty()) return;
     texpaneltimer = texpaneltime;
     if(!(lastsel==sel)) tofronttex();
     curtexindex = clamp(curtexindex<0 ? 0 : curtexindex+*dir, 0, texmru.length()-1);
@@ -2157,15 +2148,12 @@ void flip()
 
 void mprotate(int cw, selinfo &sel, bool local)
 {
-    if(local)
-    {
-        client::edittrigger(sel, EDIT_ROTATE, cw);
-        makeundo();
-    }
+    if(local) client::edittrigger(sel, EDIT_ROTATE, cw);
     int d = dimension(sel.orient);
     if(!dimcoord(sel.orient)) cw = -cw;
     int m = sel.s[C[d]] < sel.s[R[d]] ? C[d] : R[d];
     int ss = sel.s[m] = max(sel.s[R[d]], sel.s[C[d]]);
+    if(local) makeundo();
     loop(z,sel.s[D[d]]) loopi(cw>0 ? 1 : 3)
     {
         loopxy(sel) rotatecube(selcube(x,y,z), d);
@@ -2286,7 +2274,6 @@ VAR(IDF_PERSIST, thumbtime, 0, 25, 1000);
 FVAR(IDF_PERSIST, thumbsize, 0, 2, 25);
 
 static int lastthumbnail = 0;
-extern bool layoutpass;
 struct texturegui : guicb
 {
     bool menuon;
@@ -2299,19 +2286,18 @@ struct texturegui : guicb
         extern VSlot dummyvslot;
         int curtex = menutex, numpages = max((texmru.length() + thumbwidth*thumbheight - 1)/(thumbwidth*thumbheight), 1)-1;
         if(autopreviewtexgui && texmru.inrange(rolltex)) curtex = rolltex;
-        if(!layoutpass) rolltex = -1;
         if(menupage > numpages) menupage = numpages;
         else if(menupage < 0) menupage = 0;
         g.start(menustart, menuscale, NULL, true);
         uilist(g, {
             g.space(2);
-            if(g.button("\fgauto apply", 0xFFFFFF, autoapplytexgui ? "checkboxon" : "checkbox", 0xFFFFFF, autoapplytexgui ? false : true)&GUI_UP)
+            if(g.button("\fgauto apply", 0xFFFFFF, autoapplytexgui ? "checkboxon" : "checkbox", 0xFFFFFF)&GUI_UP)
                 autoapplytexgui = autoapplytexgui ? 0 : 1;
             g.space(2);
-            if(g.button("\fgauto preview", 0xFFFFFF, autopreviewtexgui ? "checkboxon" : "checkbox", 0xFFFFFF, autopreviewtexgui ? false : true)&GUI_UP)
+            if(g.button("\fgauto preview", 0xFFFFFF, autopreviewtexgui ? "checkboxon" : "checkbox", 0xFFFFFF)&GUI_UP)
                 autopreviewtexgui = autopreviewtexgui ? 0 : 1;
             g.space(2);
-            if(g.button("\fgauto close", 0xFFFFFF, autoclosetexgui ? (autoclosetexgui > 1 ? "checkboxtwo" : "checkboxon") : "checkbox", 0xFFFFFF, autoclosetexgui ? false : true)&GUI_UP)
+            if(g.button("\fgauto close", 0xFFFFFF, autoclosetexgui ? (autoclosetexgui > 1 ? "checkboxtwo" : "checkboxon") : "checkbox", 0xFFFFFF)&GUI_UP)
                 autoclosetexgui = autoclosetexgui ? (autoclosetexgui > 1 ? 0 : 2) : 1;
             g.space(2);
             if(g.button("\foreset order", 0xFFFFFF, NULL)&GUI_UP)
@@ -2341,7 +2327,7 @@ struct texturegui : guicb
                     if(autoclosetexgui) menuon = false;
                 }
             }
-            else g.image(textureload("textures/nothumb", 3), thumbheight*thumbsize, true);
+            else g.image(textureload(nothumbtex, 3), thumbheight*thumbsize, true);
             g.space(1);
             uilistv(g, 2, {
                 uilist(g, loop(h, thumbheight)
@@ -2387,7 +2373,7 @@ struct texturegui : guicb
             if(texmru.inrange(curtex))
             {
                 VSlot &v = lookupvslot(texmru[curtex]);
-                g.textf("#%-3d \fa%s", 0xFFFFFF, NULL, 0, texmru[curtex], v.slot->sts.empty() ? "<unknown texture>" : v.slot->sts[0].name);
+                g.textf("#%-3d \fa%s", 0xFFFFFF, NULL, 0, -1, texmru[curtex], v.slot->sts.empty() ? "<unknown texture>" : v.slot->sts[0].name);
             }
             else g.textf("no texture selected", 0x888888);
         });
@@ -2445,7 +2431,7 @@ void rendertexturepanel(int w, int h)
         loopi(7)
         {
             int s = (i == 3 ? 285 : 220), ti = curtexindex+i-3;
-            if(ti>=0 && ti<texmru.length())
+            if(texmru.inrange(ti))
             {
                 VSlot &vslot = lookupvslot(texmru[ti]), *layer = NULL;
                 Slot &slot = *vslot.slot;
diff --git a/src/engine/octarender.cpp b/src/engine/octarender.cpp
index 402c210..585c9ee 100644
--- a/src/engine/octarender.cpp
+++ b/src/engine/octarender.cpp
@@ -167,7 +167,7 @@ struct verthash
         for(int i = table[h]; i>=0; i = chain[i])
         {
             const vertex &c = verts[i];
-            if(c.pos==v.pos && c.u==v.u && c.v==v.v && c.norm==v.norm && c.tangent==v.tangent && c.bitangent==v.bitangent)
+            if(c.pos==v.pos && c.u==v.u && c.v==v.v && c.norm==v.norm && c.tangent==v.tangent)
             {
                  if(!v.lmu && !v.lmv) return i;
                  if(c.lmu==v.lmu && c.lmv==v.lmv) return i;
@@ -179,7 +179,7 @@ struct verthash
         return table[h] = verts.length()-1;
     }
 
-    int addvert(const vec &pos, float u = 0, float v = 0, short lmu = 0, short lmv = 0, const bvec &norm = bvec(128, 128, 128), const bvec &tangent = bvec(128, 128, 128), uchar bitangent = 128)
+    int addvert(const vec &pos, float u = 0, float v = 0, short lmu = 0, short lmv = 0, const bvec &norm = bvec(128, 128, 128), const bvec4 &tangent = bvec4(128, 128, 128, 128))
     {
         vertex vtx;
         vtx.pos = pos;
@@ -188,9 +188,7 @@ struct verthash
         vtx.lmu = lmu;
         vtx.lmv = lmv;
         vtx.norm = norm;
-        vtx.reserved = 0;
         vtx.tangent = tangent;
-        vtx.bitangent = bitangent;
         return addvert(vtx);
     }
 };
@@ -427,7 +425,7 @@ struct vacollect : verthash
             f++; \
         } \
     } while(0)
-#define GENVERTSPOSNORMUV(type, ptr, body) GENVERTS(type, ptr, { f->pos = v.pos; f->norm = v.norm; f->norm.flip(); f->reserved = 0; f->u = v.u; f->v = v.v; body; })
+#define GENVERTSPOSNORMUV(type, ptr, body) GENVERTS(type, ptr, { f->pos = v.pos; f->norm = v.norm; f->norm.flip(); f->u = v.u; f->v = v.v; body; })
 
     void genverts(void *buf)
     {
@@ -612,7 +610,7 @@ struct texcoords
 };
 
 // [rotation][dimension]
-vec orientation_tangent [6][3] =
+vec orientation_tangent[6][3] =
 {
     { vec(0,  1,  0), vec( 1, 0,  0), vec( 1,  0, 0) },
     { vec(0,  0, -1), vec( 0, 0, -1), vec( 0,  1, 0) },
@@ -621,7 +619,7 @@ vec orientation_tangent [6][3] =
     { vec(0, -1,  0), vec(-1, 0,  0), vec(-1,  0, 0) },
     { vec(0,  1,  0), vec( 1, 0,  0), vec( 1,  0, 0) },
 };
-vec orientation_binormal[6][3] =
+vec orientation_bitangent[6][3] =
 {
     { vec(0,  0, -1), vec( 0, 0, -1), vec( 0,  1, 0) },
     { vec(0, -1,  0), vec(-1, 0,  0), vec(-1,  0, 0) },
@@ -695,14 +693,12 @@ void addtris(const sortkey &key, int orient, vertex *verts, int *index, int numv
                     float offset = (t.offset - offset1) * doffset;
                     vertex vt;
                     vt.pos = d.tovec().mul(t.offset/8.0f).add(o);
-                    vt.reserved = 0;
                     vt.u = v1.u + (v2.u-v1.u)*offset;
                     vt.v = v1.v + (v2.v-v1.v)*offset;
                     vt.lmu = short(v1.lmu + (v2.lmu-v1.lmu)*offset),
                     vt.lmv = short(v1.lmv + (v2.lmv-v1.lmv)*offset);
                     vt.norm.lerp(v1.norm, v2.norm, offset);
                     vt.tangent.lerp(v1.tangent, v2.tangent, offset);
-                    vt.bitangent = v1.bitangent;
                     int i2 = vc.addvert(vt);
                     if(i2 < 0) return;
                     if(i1 >= 0)
@@ -852,7 +848,6 @@ void addcubeverts(VSlot &vslot, int orient, int size, vec *pos, int convex, usho
     {
         vertex &v = verts[k];
         v.pos = pos[k];
-        v.reserved = 0;
         v.u = sgen.dot(v.pos);
         v.v = tgen.dot(v.pos);
         if(lmtex)
@@ -864,16 +859,14 @@ void addcubeverts(VSlot &vslot, int orient, int size, vec *pos, int convex, usho
         if(renderpath!=R_FIXEDFUNCTION && vinfo && vinfo[k].norm)
         {
             vec n = decodenormal(vinfo[k].norm), t = orientation_tangent[vslot.rotation][dim];
-            t.sub(vec(n).mul(n.dot(t))).normalize();
+            t.project(n).normalize();
             v.norm = bvec(n);
-            v.tangent = bvec(t);
-            v.bitangent = vec().cross(n, t).dot(orientation_binormal[vslot.rotation][dim]) < 0 ? 0 : 255;
+            v.tangent = bvec4(bvec(t), orientation_bitangent[vslot.rotation][dim].scalartriple(n, t) < 0 ? 0 : 255);
         }
         else
         {
             v.norm = vinfo && vinfo[k].norm && envmap != EMID_NONE ? bvec(decodenormal(vinfo[k].norm)) : bvec(128, 128, 255);
-            v.tangent = bvec(255, 128, 128);
-            v.bitangent = 255;
+            v.tangent = bvec4(255, 128, 128, 255);
         }
         index[k] = vc.addvert(v);
         if(index[k] < 0) return;
@@ -1309,6 +1302,8 @@ void updatevabb(vtxarray *va, bool force)
         va->bbmin.min(oe->bbmin);
         va->bbmax.max(oe->bbmax);
     }
+    va->bbmin.max(va->o);
+    va->bbmax.min(ivec(va->o).add(va->size));
 
     if(va->skyfaces)
     {
diff --git a/src/engine/physics.cpp b/src/engine/physics.cpp
index 8616c8c..23d13d8 100644
--- a/src/engine/physics.cpp
+++ b/src/engine/physics.cpp
@@ -117,7 +117,6 @@ static inline bool raycubeintersect(const clipplanes &p, const cube &c, const ve
 }
 
 extern void entselectionbox(const extentity &e, vec &eo, vec &es);
-extern int entselradius;
 float hitentdist;
 int hitent, hitorient;
 
@@ -128,63 +127,58 @@ int hitent, hitorient;
             { \
                 if(e.attrs[6]&MMT_HIDE) \
                 { \
-                    if(e.spawned) continue; \
+                    if(e.spawned()) continue; \
                 } \
                 else if(e.lastemit > 0) \
                 { \
                     int millis = lastmillis-e.lastemit, delay = entities::triggertime(e); \
-                    if(!e.spawned && millis < delay/2) continue; \
-                    if(e.spawned && millis > delay/2) continue; \
+                    if(!e.spawned() && millis < delay/2) continue; \
+                    if(e.spawned() && millis > delay/2) continue; \
                 } \
-                else if(e.spawned) continue; \
+                else if(e.spawned()) continue; \
             } \
     }
 
 
-static float disttoent(octaentities *oc, octaentities *last, const vec &o, const vec &ray, float radius, int mode, extentity *t)
+static float disttoent(octaentities *oc, const vec &o, const vec &ray, float radius, int mode, extentity *t)
 {
     vec eo, es;
-    int orient;
-    float dist = 1e16f, f = 0.0f;
-    if(oc == last) return dist;
+    int orient = -1;
+    float dist = radius, f = 0.0f;
     const vector<extentity *> &ents = entities::getents();
 
     #define entintersect(mask, type, func) {\
-        if((mode&(mask))==(mask)) \
+        if((mode&(mask))==(mask)) loopv(oc->type) \
         { \
-            loopv(oc->type) \
-                if(!last || last->type.find(oc->type[i])<0) \
-                { \
-                    int n = oc->type[i]; \
-                    extentity &e = *ents[n]; \
-                    if(!e.inoctanode || &e==t) continue; \
-                    func; \
-                    if(f<dist && f>0) { \
-                        hitentdist = dist = f; \
-                        hitent = oc->type[i]; \
-                        hitorient = orient; \
-                    } \
-                } \
+            int n = oc->type[i]; \
+            extentity &e = *ents[n]; \
+            if(!(e.flags&EF_OCTA) || &e==t) continue; \
+            func; \
+            if(f<dist && f>0 && vec(ray).mul(f).add(o).insidebb(oc->o, oc->size)) \
+            { \
+                hitentdist = dist = f; \
+                hitent = n; \
+                hitorient = orient; \
+            } \
         } \
     }
 
     entintersect(RAY_POLY, mapmodels, {
-        if((mode&RAY_ENTS)!=RAY_ENTS) mapmodelskip;
-        if((mode&RAY_ENTS)==RAY_ENTS && !entities::cansee(n)) continue;
-        orient = 0; // FIXME, not set
+        if((mode&RAY_ENTS)!=RAY_ENTS) mapmodelskip
+        else if(!entities::cansee(n)) continue;
         if(!mmintersect(e, o, ray, radius, mode, f)) continue;
     });
 
     entintersect(RAY_ENTS, other,
         if((mode&RAY_ENTS)==RAY_ENTS && !entities::cansee(n)) continue;
         entselectionbox(e, eo, es);
-        if(!rayrectintersect(eo, es, o, ray, f, orient)) continue;
+        if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
     );
 
     entintersect(RAY_ENTS, mapmodels,
         if((mode&RAY_ENTS)==RAY_ENTS && !entities::cansee(n)) continue;
         entselectionbox(e, eo, es);
-        if(!rayrectintersect(eo, es, o, ray, f, orient)) continue;
+        if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
     );
 
     return dist;
@@ -194,14 +188,14 @@ static float disttooutsideent(const vec &o, const vec &ray, float radius, int mo
 {
     vec eo, es;
     int orient;
-    float dist = 1e16f, f = 0.0f;
+    float dist = radius, f = 0.0f;
     const vector<extentity *> &ents = entities::getents();
     loopv(outsideents)
     {
         extentity &e = *ents[outsideents[i]];
-        if(!e.inoctanode || &e == t) continue;
+        if(!(e.flags&EF_OCTA) || &e == t) continue;
         entselectionbox(e, eo, es);
-        if(!rayrectintersect(eo, es, o, ray, f, orient)) continue;
+        if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
         if(f<dist && f>0)
         {
             hitentdist = dist = f;
@@ -213,15 +207,14 @@ static float disttooutsideent(const vec &o, const vec &ray, float radius, int mo
 }
 
 // optimized shadow version
-static float shadowent(octaentities *oc, octaentities *last, const vec &o, const vec &ray, float radius, int mode, extentity *t)
+static float shadowent(octaentities *oc, const vec &o, const vec &ray, float radius, int mode, extentity *t)
 {
-    float dist = 1e16f, f = 0.0f;
-    if(oc == last) return dist;
+    float dist = radius, f = 0.0f;
     const vector<extentity *> &ents = entities::getents();
-    loopv(oc->mapmodels) if(!last || last->mapmodels.find(oc->mapmodels[i])<0)
+    loopv(oc->mapmodels)
     {
         extentity &e = *ents[oc->mapmodels[i]];
-        if(!e.inoctanode || &e==t) continue;
+        if(!(e.flags&EF_OCTA) || &e==t) continue;
         if(e.attrs[6]&MMT_NOSHADOW) continue;
         if(!mmintersect(e, o, ray, radius, mode, f)) continue;
         if(f>0 && f<dist) dist = f;
@@ -230,12 +223,11 @@ static float shadowent(octaentities *oc, octaentities *last, const vec &o, const
 }
 
 #define INITRAYCUBE \
-    octaentities *oclast = NULL; \
-    float dist = 0, dent = mode&RAY_BB ? 1e16f : 1e14f; \
+    float dist = 0, dent = radius > 0 ? radius : 1e16f; \
     vec v(o), invray(ray.x ? 1/ray.x : 1e16f, ray.y ? 1/ray.y : 1e16f, ray.z ? 1/ray.z : 1e16f); \
     cube *levels[20]; \
     levels[worldscale] = worldroot; \
-    int lshift = worldscale, elvl = worldscale; \
+    int lshift = worldscale, elvl = mode&RAY_BB ? worldscale : 0; \
     ivec lsizemask(invray.x>0 ? 1 : 0, invray.y>0 ? 1 : 0, invray.z>0 ? 1 : 0); \
 
 #define CHECKINSIDEWORLD \
@@ -267,14 +259,13 @@ static float shadowent(octaentities *oc, octaentities *last, const vec &o, const
             lc += octastep(x, y, z, lshift); \
             if(lc->ext && lc->ext->ents && lshift < elvl) \
             { \
-                float edist = disttoent(lc->ext->ents, oclast, o, ray, radius, mode, t); \
-                if(edist < 1e15f) \
+                float edist = disttoent(lc->ext->ents, o, ray, dent, mode, t); \
+                if(edist < dent) \
                 { \
-                    if(earlyexit) return min(edist, dist); \
+                    earlyexit return min(edist, dist); \
                     elvl = lshift; \
                     dent = min(dent, edist); \
                 } \
-                oclast = lc->ext->ents; \
             } \
             if(lc->children==NULL) break; \
             lc = lc->children; \
@@ -317,7 +308,7 @@ float raycube(const vec &o, const vec &ray, float radius, int mode, int size, ex
     int closest = -1, x = int(v.x), y = int(v.y), z = int(v.z);
     for(;;)
     {
-        DOWNOCTREE(disttoent, mode&RAY_SHADOW);
+        DOWNOCTREE(disttoent, if(mode&RAY_SHADOW));
 
         int lsize = 1<<lshift;
 
@@ -372,7 +363,7 @@ float shadowray(const vec &o, const vec &ray, float radius, int mode, extentity
     int side = O_BOTTOM, x = int(v.x), y = int(v.y), z = int(v.z);
     for(;;)
     {
-        DOWNOCTREE(shadowent, true);
+        DOWNOCTREE(shadowent, );
 
         cube &c = *lc;
         ivec lo(x&(~0<<lshift), y&(~0<<lshift), z&(~0<<lshift));
@@ -427,7 +418,7 @@ float shadowray(ShadowRayCache *cache, const vec &o, const vec &ray, float radiu
     int side = O_BOTTOM, x = int(v.x), y = int(v.y), z = int(v.z);
     for(;;)
     {
-        DOWNOCTREE(shadowent, true);
+        DOWNOCTREE(shadowent, );
 
         cube &c = *lc;
         ivec lo(x&(~0<<lshift), y&(~0<<lshift), z&(~0<<lshift));
@@ -455,6 +446,7 @@ float rayent(const vec &o, const vec &ray, float radius, int mode, int size, int
 {
     hitent = -1;
     hitentdist = radius;
+    hitorient = -1;
     float dist = raycube(o, ray, radius, mode, size);
     if((mode&RAY_ENTS) == RAY_ENTS)
     {
@@ -498,16 +490,16 @@ float rayfloor(const vec &o, vec &floor, int mode, float radius)
 /////////////////////////  entity collision  ///////////////////////////////////////////////
 
 // info about collisions
-bool inside; // whether an internal collision happened
+bool collideinside; // whether an internal collision happened
 physent *hitplayer; // whether the collection hit a player
 int hitflags = HITFLAG_NONE;
-vec wall; // just the normal vectors.
+vec collidewall; // just the normal vectors.
 
-bool ellipserectcollide(physent *d, const vec &dir, const vec &o, const vec &center, float yaw, float xr, float yr, float hi, float lo)
+bool ellipseboxcollide(physent *d, const vec &dir, const vec &o, const vec &center, float yaw, float xr, float yr, float hi, float lo)
 {
     float below = (o.z+center.z-lo) - (d->o.z+d->aboveeye),
           above = (d->o.z-d->height) - (o.z+center.z+hi);
-    if(below>=0 || above>=0) return true;
+    if(below>=0 || above>=0) return false;
 
     vec yo(d->o);
     yo.sub(o);
@@ -528,41 +520,41 @@ bool ellipserectcollide(physent *d, const vec &dir, const vec &o, const vec &cen
             {
                 if(dir.iszero() || sx*ydir.x < -1e-6f)
                 {
-                    wall = vec(sx, 0, 0);
-                    wall.rotate_around_z(yaw*RAD);
-                    return false;
+                    collidewall = vec(sx, 0, 0);
+                    collidewall.rotate_around_z(yaw*RAD);
+                    return true;
                 }
             }
             else if(dir.iszero() || sy*ydir.y < -1e-6f)
             {
-                wall = vec(0, sy, 0);
-                wall.rotate_around_z(yaw*RAD);
-                return false;
+                collidewall = vec(0, sy, 0);
+                collidewall.rotate_around_z(yaw*RAD);
+                return true;
             }
         }
         if(yo.z < 0)
         {
             if(dir.iszero() || (dir.z > 0 && (d->type>=ENT_INANIMATE || below >= d->zmargin-(d->height+d->aboveeye)/4.0f)))
             {
-                wall = vec(0, 0, -1);
-                return false;
+                collidewall = vec(0, 0, -1);
+                return true;
             }
         }
         else if(dir.iszero() || (dir.z < 0 && (d->type>=ENT_INANIMATE || above >= d->zmargin-(d->height+d->aboveeye)/3.0f)))
         {
-            wall = vec(0, 0, 1);
-            return false;
+            collidewall = vec(0, 0, 1);
+            return true;
         }
-        inside = true;
+        collideinside = true;
     }
-    return true;
+    return false;
 }
 
 bool ellipsecollide(physent *d, const vec &dir, const vec &o, const vec &center, float yaw, float xr, float yr, float hi, float lo)
 {
     float below = (o.z+center.z-lo) - (d->o.z+d->aboveeye),
           above = (d->o.z-d->height) - (o.z+center.z+hi);
-    if(below>=0 || above>=0) return true;
+    if(below>=0 || above>=0) return false;
     vec yo(center);
     yo.rotate_around_z(yaw*RAD);
     yo.add(o);
@@ -575,53 +567,26 @@ bool ellipsecollide(physent *d, const vec &dir, const vec &o, const vec &center,
     {
         if(dist > (d->o.z < yo.z ? below : above) && (dir.iszero() || x*dir.x + y*dir.y > 0))
         {
-            wall = vec(-x, -y, 0);
-            if(!wall.iszero()) wall.normalize();
-            return false;
+            collidewall = vec(-x, -y, 0);
+            if(!collidewall.iszero()) collidewall.normalize();
+            return true;
         }
         if(d->o.z < yo.z)
         {
             if(dir.iszero() || (dir.z > 0 && (d->type>=ENT_INANIMATE || below >= d->zmargin-(d->height+d->aboveeye)/4.0f)))
             {
-                wall = vec(0, 0, -1);
-                return false;
+                collidewall = vec(0, 0, -1);
+                return true;
             }
         }
         else if(dir.iszero() || (dir.z < 0 && (d->type>=ENT_INANIMATE || above >= d->zmargin-(d->height+d->aboveeye)/3.0f)))
         {
-            wall = vec(0, 0, 1);
-            return false;
+            collidewall = vec(0, 0, 1);
+            return true;
         }
-        inside = true;
+        collideinside = true;
     }
-    return true;
-}
-
-bool rectcollide(physent *d, const vec &dir, const vec &o, float xr, float yr, float hi, float lo, uchar visible)
-{
-    vec s(d->o);
-    s.sub(o);
-    float dxr = d->collidetype==COLLIDE_AABB ? d->xradius : d->radius, dyr = d->collidetype==COLLIDE_AABB ? d->yradius : d->radius;
-    xr += dxr;
-    yr += dyr;
-    float zr = s.z>0 ? d->height+hi : d->aboveeye+lo;
-    float ax = fabs(s.x)-xr;
-    float ay = fabs(s.y)-yr;
-    float az = fabs(s.z)-zr;
-    if(ax>0 || ay>0 || az>0) return true;
-    wall.x = wall.y = wall.z = 0;
-#define TRYCOLLIDE(dim, ON, OP, N, P) \
-    { \
-        if(s.dim<0) { if(visible&(1<<ON) && (dir.iszero() || (dir.dim>0 && (d->type>=ENT_INANIMATE || (N))))) { wall.dim = -1; return false; } } \
-        else if(visible&(1<<OP) && (dir.iszero() || (dir.dim<0 && (d->type>=ENT_INANIMATE || (P))))) { wall.dim = 1; return false; } \
-    }
-    if(ax>ay && ax>az) TRYCOLLIDE(x, O_LEFT, O_RIGHT, ax > -dxr, ax > -dxr);
-    if(ay>az) TRYCOLLIDE(y, O_BACK, O_FRONT, ay > -dyr, ay > -dyr);
-    TRYCOLLIDE(z, O_BOTTOM, O_TOP,
-         az >= d->zmargin-(d->height+d->aboveeye)/4.0f,
-         az >= d->zmargin-(d->height+d->aboveeye)/3.0f);
-    inside = true;
-    return true;
+    return false;
 }
 
 #define DYNENTCACHESIZE 1024
@@ -708,33 +673,32 @@ static inline bool plcollide(physent *d, const vec &dir, physent *o)
     if(mpr::collide(entvol, obvol, NULL, NULL, &cp))
     {
         vec wn = vec(cp).sub(obvol.center());
-        wall = obvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir);
-        if(!wall.iszero()) return false;
-        inside = true;
+        collidewall = obvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir);
+        if(!collidewall.iszero()) return true;
+        collideinside = true;
     }
-    return true;
+    return false;
 }
 
 bool plcollide(physent *d, const vec &dir, physent *o)
 {
-    if(d->o.reject(o->o, d->radius + o->radius)) return true;
+    if(d->o.reject(o->o, d->radius + o->radius)) return false;
     switch(d->collidetype)
     {
         case COLLIDE_ELLIPSE:
-            if(o->collidetype == COLLIDE_ELLIPSE) return ellipsecollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->height);
-            else return ellipserectcollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->height);
+        case COLLIDE_ELLIPSE_PRECISE:
+            if(o->collidetype == COLLIDE_OBB) return ellipseboxcollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->height);
+            else return ellipsecollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->height);
         case COLLIDE_OBB:
-            if(o->collidetype == COLLIDE_ELLIPSE) return plcollide<mpr::EntOBB, mpr::EntCylinder>(d, dir, o);
-            else return plcollide<mpr::EntOBB, mpr::EntOBB>(d, dir, o);
-        case COLLIDE_AABB:
-        default:
-            return rectcollide(d, dir, o->o, o->collidetype == COLLIDE_AABB ? o->xradius : o->radius, o->collidetype == COLLIDE_AABB ? o->yradius : o->radius, o->aboveeye, o->height);
+            if(o->collidetype == COLLIDE_OBB) return plcollide<mpr::EntOBB, mpr::EntOBB>(d, dir, o);
+            else return plcollide<mpr::EntOBB, mpr::EntCylinder>(d, dir, o);
+        default: return false;
     }
 }
 
 bool plcollide(physent *d, const vec &dir)  // collide with player or monster
 {
-    if(d->type==ENT_CAMERA || d->state!=CS_ALIVE) return true;
+    if(d->type==ENT_CAMERA || d->state!=CS_ALIVE) return false;
     loopdynentcache(x, y, d->o, d->radius)
     {
         const vector<physent *> &dynents = checkdynentcache(x, y);
@@ -742,108 +706,200 @@ bool plcollide(physent *d, const vec &dir)  // collide with player or monster
         {
             physent *o = dynents[i];
             if(o==d || !physics::issolid(o, d)) continue;
-            if(!physics::xcollide(d, dir, o))
+            if(physics::xcollide(d, dir, o))
             {
                 hitplayer = o;
-                return false;
+                return true;
             }
         }
     }
-    return true;
+    return false;
 }
 
 void rotatebb(vec &center, vec &radius, int yaw, int pitch, int roll)
 {
-    if(pitch)
+    matrix3x3 orient;
+    orient.identity();
+    if(yaw) orient.rotate_around_z(sincosmod360(yaw));
+    if(roll) orient.rotate_around_x(sincosmod360(-roll));
+    if(pitch) orient.rotate_around_y(sincosmod360(-pitch));
+    center = orient.transform(center);
+    radius = orient.abstransform(radius);
+}
+
+template<class E, class M>
+static inline bool mmcollide(physent *d, const vec &dir, const extentity &e, const vec &center, const vec &radius, int yaw, int pitch, int roll)
+{
+    E entvol(d);
+    M mdlvol(e.o, center, radius, yaw, pitch, roll);
+    vec cp(0, 0, 0);
+    if(mpr::collide(entvol, mdlvol, NULL, NULL, &cp))
     {
-        if(pitch < 0) pitch = 360 + pitch%360;
-        else if(pitch >= 360) pitch %= 360;
-        const vec2 &rot = sincos360[pitch];
-        vec2 oldcenter(center.x, center.z), oldradius(radius.x, radius.z);
-        center.x = oldcenter.x*rot.x - oldcenter.y*rot.y;
-        center.z = oldcenter.y*rot.x + oldcenter.x*rot.y;
-        radius.x = fabs(oldradius.x*rot.x) + fabs(oldradius.y*rot.y);
-        radius.z = fabs(oldradius.y*rot.x) + fabs(oldradius.x*rot.y);
+        vec wn = vec(cp).sub(mdlvol.center());
+        collidewall = mdlvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir);
+        if(!collidewall.iszero()) return true;
+        collideinside = true;
     }
-    if(roll)
+    return false;
+}
+
+template<class E>
+static bool fuzzycollidebox(physent *d, const vec &dir, float cutoff, const vec &o, const vec &center, const vec &radius, int yaw, int pitch, int roll)
+{
+    mpr::ModelOBB mdlvol(o, center, radius, yaw, pitch, roll);
+    vec bbradius = mdlvol.orient.abstransposedtransform(radius);
+
+    if(fabs(d->o.x - mdlvol.o.x) > bbradius.x + d->radius || fabs(d->o.y - mdlvol.o.y) > bbradius.y + d->radius ||
+       d->o.z + d->aboveeye < mdlvol.o.z - bbradius.z || d->o.z - d->height > mdlvol.o.z + bbradius.z)
+        return false;
+
+    E entvol(d);
+    collidewall = vec(0, 0, 0);
+    float bestdist = -1e10f;
+    loopi(6)
     {
-        if(roll < 0) roll = 360 + roll%360;
-        else if(roll >= 360) roll %= 360;
-        const vec2 &rot = sincos360[roll];
-        vec2 oldcenter(center.y, center.z), oldradius(radius.y, radius.z);
-        center.y = oldcenter.x*rot.x + oldcenter.y*rot.y;
-        center.z = oldcenter.y*rot.x - oldcenter.x*rot.y;
-        radius.y = fabs(oldradius.x*rot.x) + fabs(oldradius.y*rot.y);
-        radius.z = fabs(oldradius.y*rot.x) + fabs(oldradius.x*rot.y);
+        vec w;
+        float dist;
+        switch(i)
+        {
+            default:
+            case 0: w = vec(mdlvol.orient.a).neg(); dist = -radius.x; break;
+            case 1: w = mdlvol.orient.a; dist = -radius.x; break;
+            case 2: w = vec(mdlvol.orient.b).neg(); dist = -radius.y; break;
+            case 3: w = mdlvol.orient.b; dist = -radius.y; break;
+            case 4: w = vec(mdlvol.orient.c).neg(); dist = -radius.z; break;
+            case 5: w = mdlvol.orient.c; dist = -radius.z; break;
+        }
+        vec pw = entvol.supportpoint(vec(w).neg());
+        dist += w.dot(vec(pw).sub(mdlvol.o));
+        if(dist >= 0) return false;
+        if(dist <= bestdist) continue;
+        collidewall = vec(0, 0, 0);
+        bestdist = dist;
+        if(!dir.iszero())
+        {
+            if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
+            if(d->type<ENT_CAMERA &&
+                dist < (dir.z*w.z < 0 ?
+                    d->zmargin-(d->height+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
+                    (dir.x*w.x < 0 || dir.y*w.y < 0 ? -d->radius : 0)))
+                continue;
+        }
+        collidewall = w;
     }
-    if(yaw)
+    if(collidewall.iszero())
     {
-        if(yaw < 0) yaw = 360 + yaw%360;
-        else if(yaw >= 360) yaw %= 360;
-        const vec2 &rot = sincos360[yaw];
-        vec2 oldcenter(center), oldradius(radius);
-        center.x = oldcenter.x*rot.x - oldcenter.y*rot.y;
-        center.y = oldcenter.y*rot.x + oldcenter.x*rot.y;
-        radius.x = fabs(oldradius.x*rot.x) + fabs(oldradius.y*rot.y);
-        radius.y = fabs(oldradius.y*rot.x) + fabs(oldradius.x*rot.y);
+        collideinside = true;
+        return false;
     }
+    return true;
 }
 
-template<class E, class M>
-static inline bool mmcollide(physent *d, const vec &dir, const extentity &e, const vec &center, const vec &radius, float yaw, float roll)
+template<class E>
+static bool fuzzycollideellipse(physent *d, const vec &dir, float cutoff, const vec &o, const vec &center, const vec &radius, int yaw, int pitch, int roll)
 {
+    mpr::ModelEllipse mdlvol(o, center, radius, yaw, pitch, roll);
+    vec bbradius = mdlvol.orient.abstransposedtransform(radius);
+
+    if(fabs(d->o.x - mdlvol.o.x) > bbradius.x + d->radius || fabs(d->o.y - mdlvol.o.y) > bbradius.y + d->radius ||
+       d->o.z + d->aboveeye < mdlvol.o.z - bbradius.z || d->o.z - d->height > mdlvol.o.z + bbradius.z)
+        return false;
+
     E entvol(d);
-    M mdlvol(e.o, center, radius, yaw, roll);
-    vec cp(0, 0, 0);
-    if(mpr::collide(entvol, mdlvol, NULL, NULL, &cp))
+    collidewall = vec(0, 0, 0);
+    float bestdist = -1e10f;
+    loopi(3)
     {
-        vec wn = vec(cp).sub(mdlvol.center());
-        wall = mdlvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir);
-        if(!wall.iszero()) return false;
-        inside = true;
+        vec w;
+        float dist;
+        switch(i)
+        {
+            default:
+            case 0: w = mdlvol.orient.c; dist = -radius.z; break;
+            case 1: w = vec(mdlvol.orient.c).neg(); dist = -radius.z; break;
+            case 2:
+            {
+                vec2 ln(mdlvol.orient.transform(entvol.center().sub(mdlvol.o)));
+                float r = ln.magnitude();
+                if(r < 1e-6f) continue;
+                vec2 lw = vec2(ln.x*radius.y, ln.y*radius.x).normalize();
+                w = mdlvol.orient.transposedtransform(lw);
+                dist = -vec2(ln.x*radius.x, ln.y*radius.y).dot(lw)/r;
+                break;
+            }
+        }
+        vec pw = entvol.supportpoint(vec(w).neg());
+        dist += w.dot(vec(pw).sub(mdlvol.o));
+        if(dist >= 0) return false;
+        if(dist <= bestdist) continue;
+        collidewall = vec(0, 0, 0);
+        bestdist = dist;
+        if(!dir.iszero())
+        {
+            if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
+            if(d->type<ENT_CAMERA &&
+                dist < (dir.z*w.z < 0 ?
+                    d->zmargin-(d->height+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
+                    (dir.x*w.x < 0 || dir.y*w.y < 0 ? -d->radius : 0)))
+                continue;
+        }
+        collidewall = w;
+    }
+    if(collidewall.iszero())
+    {
+        collideinside = true;
+        return false;
     }
     return true;
 }
 
-bool mmcollide(physent *d, const vec &dir, octaentities &oc)               // collide with a mapmodel
+bool mmcollide(physent *d, const vec &dir, float cutoff, octaentities &oc) // collide with a mapmodel
 {
-    if(d->type == ENT_CAMERA) return true;
+    if(d->type == ENT_CAMERA) return false;
     const vector<extentity *> &ents = entities::getents();
     loopv(oc.mapmodels)
     {
         extentity &e = *ents[oc.mapmodels[i]];
         mapmodelskip;
-        model *m = loadmodel(NULL, e.attrs[0]);
+        model *m = loadmapmodel(e.attrs[0]);
         if(!m || !m->collide) continue;
+
         vec center, radius;
-        m->collisionbox(0, center, radius);
-        if(e.attrs[5]) { float scale = max(e.attrs[5]/100.f, 1e-3f); center.mul(scale); radius.mul(scale); }
+        float rejectradius = m->collisionbox(center, radius), scale = e.attrs[5]  ? max(e.attrs[5]/100.0f, 1e-3f) : 1;
+        center.mul(scale);
+        if(d->o.reject(vec(e.o).add(center), d->radius + rejectradius*scale)) continue;
+
+        radius.mul(scale);
         int yaw = e.attrs[1], pitch = e.attrs[2], roll = e.attrs[3];
         switch(d->collidetype)
         {
             case COLLIDE_ELLIPSE:
-                if(pitch || roll) rotatebb(center, radius, 0, pitch, roll);
+            case COLLIDE_ELLIPSE_PRECISE:
                 if(m->ellipsecollide)
                 {
-                    if(!ellipsecollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return false;
+                    if(pitch || roll)
+                    {
+                        if(fuzzycollideellipse<mpr::EntCapsule>(d, dir, cutoff, e.o, center, radius, yaw, pitch, roll)) return true;
+                    }
+                    else if(ellipsecollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return true;
                 }
-                else if(!ellipserectcollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return false;
+                else if(pitch || roll)
+                {
+                    if(fuzzycollidebox<mpr::EntCapsule>(d, dir, cutoff, e.o, center, radius, yaw, pitch, roll)) return true;
+                }
+                else if(ellipseboxcollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return true;
                 break;
             case COLLIDE_OBB:
                 if(m->ellipsecollide)
                 {
-                    if(!mmcollide<mpr::EntOBB, mpr::ModelEllipse>(d, dir, e, center, radius, yaw, roll)) return false;
+                    if(mmcollide<mpr::EntOBB, mpr::ModelEllipse>(d, dir, e, center, radius, yaw, pitch, roll)) return true;
                 }
-                else if(!mmcollide<mpr::EntOBB, mpr::ModelOBB>(d, dir, e, center, radius, yaw, roll)) return false;
-                break;
-            case COLLIDE_AABB:
-            default:
-                rotatebb(center, radius, yaw, pitch, roll);
-                if(!rectcollide(d, dir, center.add(e.o), radius.x, radius.y, radius.z, radius.z)) return false;
+                else if(mmcollide<mpr::EntOBB, mpr::ModelOBB>(d, dir, e, center, radius, yaw, pitch, roll)) return true;
                 break;
+            default: continue;
         }
     }
-    return true;
+    return false;
 }
 
 template<class E>
@@ -852,39 +908,38 @@ static bool fuzzycollidesolid(physent *d, const vec &dir, float cutoff, const cu
     int crad = size/2;
     if(fabs(d->o.x - co.x - crad) > d->radius + crad || fabs(d->o.y - co.y - crad) > d->radius + crad ||
        d->o.z + d->aboveeye < co.z || d->o.z - d->height > co.z + size)
-        return true;
+        return false;
 
     E entvol(d);
-    wall = vec(0, 0, 0);
+    collidewall = vec(0, 0, 0);
     float bestdist = -1e10f;
     int visible = isentirelysolid(c) ? c.visible : 0xFF;
-    loopi(6) if(visible&(1<<i))
-    {
-        int dim = dimension(i), dc = dimcoord(i), dimdir = 2*dc - 1;
-        plane w(0, 0, 0, -(dimdir*co[dim] + dc*size));
-        w[dim] = dimdir;
-        vec pw = entvol.supportpoint(vec(w).neg());
-        float dist = w.dist(pw);
-        if(dist > 0) return true;
-        if(dist <= bestdist) continue;
-        if(!dir.iszero())
-        {
-            if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
-            if(d->type<ENT_CAMERA &&
-                dist < (dir.z*w.z < 0 ?
-                    d->zmargin-(d->height+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
-                    ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0)))
-                continue;
-        }
-        wall = w;
-        bestdist = dist;
-    }
-    if(wall.iszero())
+    #define CHECKSIDE(side, distval, dotval, margin, normal) if(visible&(1<<side)) do \
+    { \
+        float dist = distval; \
+        if(dist > 0) return false; \
+        if(dist <= bestdist) continue; \
+        if(!dir.iszero()) \
+        { \
+            if(dotval >= -cutoff*dir.magnitude()) continue; \
+            if(d->type<ENT_CAMERA && dotval < 0 && dist < margin) continue; \
+        } \
+        collidewall = normal; \
+        bestdist = dist; \
+    } while(0)
+    CHECKSIDE(O_LEFT, co.x - (d->o.x + d->radius), -dir.x, -d->radius, vec(-1, 0, 0));
+    CHECKSIDE(O_RIGHT, d->o.x - d->radius - (co.x + size), dir.x, -d->radius, vec(1, 0, 0));
+    CHECKSIDE(O_BACK, co.y - (d->o.y + d->radius), -dir.y, -d->radius, vec(0, -1, 0));
+    CHECKSIDE(O_FRONT, d->o.y - d->radius - (co.y + size), dir.y, -d->radius, vec(0, 1, 0));
+    CHECKSIDE(O_BOTTOM, co.z - (d->o.z + d->aboveeye), -dir.z, d->zmargin-(d->height+d->aboveeye)/4.0f, vec(0, 0, -1));
+    CHECKSIDE(O_TOP, d->o.z - d->height - (co.z + size), dir.z, d->zmargin-(d->height+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+    if(collidewall.iszero())
     {
-        inside = true;
-        return true;
+        collideinside = true;
+        return false;
     }
-    return false;
+    return true;
 }
 
 template<class E>
@@ -921,39 +976,26 @@ static bool fuzzycollideplanes(physent *d, const vec &dir, float cutoff, const c
 
     if(fabs(d->o.x - p.o.x) > p.r.x + d->radius || fabs(d->o.y - p.o.y) > p.r.y + d->radius ||
        d->o.z + d->aboveeye < p.o.z - p.r.z || d->o.z - d->height > p.o.z + p.r.z)
-        return true;
+        return false;
 
-    E entvol(d);
-    wall = vec(0, 0, 0);
+    collidewall = vec(0, 0, 0);
     float bestdist = -1e10f;
-    loopi(6) if(p.visible&(1<<i))
-    {
-        int dim = dimension(i), dimdir = 2*dimcoord(i) - 1;
-        plane w(0, 0, 0, -(dimdir*p.o[dim] + p.r[dim]));
-        w[dim] = dimdir;
-        vec pw = entvol.supportpoint(vec(w).neg());
-        float dist = w.dist(pw);
-        if(dist >= 0) return true;
-        if(dist <= bestdist) continue;
-        if(!dir.iszero())
-        {
-            if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
-            if(d->type<ENT_CAMERA &&
-                dist < (dir.z*w.z < 0 ?
-                    d->zmargin-(d->height+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
-                    ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0)))
-                continue;
-        }
-        wall = w;
-        bestdist = dist;
-    }
+    int visible = p.visible;
+    CHECKSIDE(O_LEFT, p.o.x - p.r.x - (d->o.x + d->radius), -dir.x, -d->radius, vec(-1, 0, 0));
+    CHECKSIDE(O_RIGHT, d->o.x - d->radius - (p.o.x + p.r.x), dir.x, -d->radius, vec(1, 0, 0));
+    CHECKSIDE(O_BACK, p.o.y - p.r.y - (d->o.y + d->radius), -dir.y, -d->radius, vec(0, -1, 0));
+    CHECKSIDE(O_FRONT, d->o.y - d->radius - (p.o.y + p.r.y), dir.y, -d->radius, vec(0, 1, 0));
+    CHECKSIDE(O_BOTTOM, p.o.z - p.r.z - (d->o.z + d->aboveeye), -dir.z, d->zmargin-(d->height+d->aboveeye)/4.0f, vec(0, 0, -1));
+    CHECKSIDE(O_TOP, d->o.z - d->height - (p.o.z + p.r.z), dir.z, d->zmargin-(d->height+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+    E entvol(d);
     int bestplane = -1;
     loopi(p.size)
     {
         const plane &w = p.p[i];
         vec pw = entvol.supportpoint(vec(w).neg());
         float dist = w.dist(pw);
-        if(dist >= 0) return true;
+        if(dist >= 0) return false;
         if(dist <= bestdist) continue;
         bestplane = -1;
         bestdist = dist;
@@ -969,13 +1011,13 @@ static bool fuzzycollideplanes(physent *d, const vec &dir, float cutoff, const c
         if(clampcollide(p, entvol, w, pw)) continue;
         bestplane = i;
     }
-    if(bestplane >= 0) wall = p.p[bestplane];
-    else if(wall.iszero())
+    if(bestplane >= 0) collidewall = p.p[bestplane];
+    else if(collidewall.iszero())
     {
-        inside = true;
-        return true;
+        collideinside = true;
+        return false;
     }
-    return false;
+    return true;
 }
 
 template<class E>
@@ -984,41 +1026,28 @@ static bool cubecollidesolid(physent *d, const vec &dir, float cutoff, const cub
     int crad = size/2;
     if(fabs(d->o.x - co.x - crad) > d->radius + crad || fabs(d->o.y - co.y - crad) > d->radius + crad ||
        d->o.z + d->aboveeye < co.z || d->o.z - d->height > co.z + size)
-        return true;
+        return false;
 
     E entvol(d);
     bool collided = mpr::collide(mpr::SolidCube(co, size), entvol);
-    if(!collided) return true;
+    if(!collided) return false;
 
-    wall = vec(0, 0, 0);
+    collidewall = vec(0, 0, 0);
     float bestdist = -1e10f;
     int visible = isentirelysolid(c) ? c.visible : 0xFF;
-    loopi(6) if(visible&(1<<i))
+    CHECKSIDE(O_LEFT, co.x - entvol.right(), -dir.x, -d->radius, vec(-1, 0, 0));
+    CHECKSIDE(O_RIGHT, entvol.left() - (co.x + size), dir.x, -d->radius, vec(1, 0, 0));
+    CHECKSIDE(O_BACK, co.y - entvol.front(), -dir.y, -d->radius, vec(0, -1, 0));
+    CHECKSIDE(O_FRONT, entvol.back() - (co.y + size), dir.y, -d->radius, vec(0, 1, 0));
+    CHECKSIDE(O_BOTTOM, co.z - entvol.top(), -dir.z, d->zmargin-(d->height+d->aboveeye)/4.0f, vec(0, 0, -1));
+    CHECKSIDE(O_TOP, entvol.bottom() - (co.z + size), dir.z, d->zmargin-(d->height+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+    if(collidewall.iszero())
     {
-        int dim = dimension(i), dc = dimcoord(i), dimdir = 2*dc - 1;
-        plane w(0, 0, 0, -(dimdir*co[dim] + dc*size));
-        w[dim] = dimdir;
-        vec pw = entvol.supportpoint(vec(w).neg());
-        float dist = w.dist(pw);
-        if(dist <= bestdist) continue;
-        if(!dir.iszero())
-        {
-            if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
-            if(d->type<ENT_CAMERA &&
-                dist < (dir.z*w.z < 0 ?
-                    d->zmargin-(d->height+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
-                    ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0)))
-                continue;
-        }
-        wall = w;
-        bestdist = dist;
-    }
-    if(wall.iszero())
-    {
-        inside = true;
-        return true;
+        collideinside = true;
+        return false;
     }
-    return false;
+    return true;
 }
 
 template<class E>
@@ -1028,34 +1057,22 @@ static bool cubecollideplanes(physent *d, const vec &dir, float cutoff, const cu
 
     if(fabs(d->o.x - p.o.x) > p.r.x + d->radius || fabs(d->o.y - p.o.y) > p.r.y + d->radius ||
        d->o.z + d->aboveeye < p.o.z - p.r.z || d->o.z - d->height > p.o.z + p.r.z)
-        return true;
+        return false;
 
     E entvol(d);
     bool collided = mpr::collide(mpr::CubePlanes(p), entvol);
-    if(!collided) return true;
+    if(!collided) return false;
 
-    wall = vec(0, 0, 0);
+    collidewall = vec(0, 0, 0);
     float bestdist = -1e10f;
-    loopi(6) if(p.visible&(1<<i))
-    {
-        int dim = dimension(i), dimdir = 2*dimcoord(i) - 1;
-        plane w(0, 0, 0, -(dimdir*p.o[dim] + p.r[dim]));
-        w[dim] = dimdir;
-        vec pw = entvol.supportpoint(vec(w).neg());
-        float dist = w.dist(pw);
-        if(dist <= bestdist) continue;
-        if(!dir.iszero())
-        {
-            if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
-            if(d->type<ENT_CAMERA &&
-                dist < (dir.z*w.z < 0 ?
-                    d->zmargin-(d->height+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
-                    ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0)))
-                continue;
-        }
-        wall = w;
-        bestdist = dist;
-    }
+    int visible = p.visible;
+    CHECKSIDE(O_LEFT, p.o.x - p.r.x - entvol.right(), -dir.x, -d->radius, vec(-1, 0, 0));
+    CHECKSIDE(O_RIGHT, entvol.left() - (p.o.x + p.r.x), dir.x, -d->radius, vec(1, 0, 0));
+    CHECKSIDE(O_BACK, p.o.y - p.r.y - entvol.front(), -dir.y, -d->radius, vec(0, -1, 0));
+    CHECKSIDE(O_FRONT, entvol.back() - (p.o.y + p.r.y), dir.y, -d->radius, vec(0, 1, 0));
+    CHECKSIDE(O_BOTTOM, p.o.z - p.r.z - entvol.top(), -dir.z, d->zmargin-(d->height+d->aboveeye)/4.0f, vec(0, 0, -1));
+    CHECKSIDE(O_TOP, entvol.bottom() - (p.o.z + p.r.z), dir.z, d->zmargin-(d->height+d->aboveeye)/3.0f, vec(0, 0, 1));
+
     int bestplane = -1;
     loopi(p.size)
     {
@@ -1077,58 +1094,29 @@ static bool cubecollideplanes(physent *d, const vec &dir, float cutoff, const cu
         if(clampcollide(p, entvol, w, pw)) continue;
         bestplane = i;
     }
-    if(bestplane >= 0) wall = p.p[bestplane];
-    else if(wall.iszero())
+    if(bestplane >= 0) collidewall = p.p[bestplane];
+    else if(collidewall.iszero())
     {
-        inside = true;
-        return true;
+        collideinside = true;
+        return false;
     }
-    return false;
+    return true;
 }
 
 static inline bool cubecollide(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size, bool solid)
 {
     switch(d->collidetype)
     {
-    case COLLIDE_AABB:
-        if(isentirelysolid(c) || solid)
-        {
-            if(cutoff <= 0)
-            {
-                int crad = size/2;
-                return rectcollide(d, dir, vec(co.x + crad, co.y + crad, co.z), crad, crad, size, 0, isentirelysolid(c) ? c.visible : 0xFF);
-            }
-#if 0
-            else return cubecollidesolid<mpr::EntAABB>(d, dir, cutoff, c, co, size);
-#else
-            else return fuzzycollidesolid<mpr::EntAABB>(d, dir, cutoff, c, co, size);
-#endif
-        }
-        else
-        {
-#if 0
-            if(cutoff <= 0)
-            {
-                const clipplanes &p = getclipplanes(c, co, size);
-                if(!p.size) return rectcollide(d, dir, p.o, p.r.x, p.r.y, p.r.z, p.r.z, p.visible);
-            }
-            return cubecollideplanes<mpr::EntAABB>(d, dir, cutoff, c, co, size);
-#else
-            return fuzzycollideplanes<mpr::EntAABB>(d, dir, cutoff, c, co, size);
-#endif
-        }
     case COLLIDE_OBB:
         if(isentirelysolid(c) || solid) return cubecollidesolid<mpr::EntOBB>(d, dir, cutoff, c, co, size);
         else return cubecollideplanes<mpr::EntOBB>(d, dir, cutoff, c, co, size);
     case COLLIDE_ELLIPSE:
-    default:
-        if(d->type < ENT_CAMERA)
-        {
-            if(isentirelysolid(c) || solid) return fuzzycollidesolid<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
-            else return fuzzycollideplanes<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
-        }
-        else if(isentirelysolid(c) || solid) return cubecollidesolid<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+        if(isentirelysolid(c) || solid) return fuzzycollidesolid<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+        else return fuzzycollideplanes<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+    case COLLIDE_ELLIPSE_PRECISE:
+        if(isentirelysolid(c) || solid) return cubecollidesolid<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
         else return cubecollideplanes<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+    default: return false;
     }
 }
 
@@ -1136,11 +1124,11 @@ static inline bool octacollide(physent *d, const vec &dir, float cutoff, const i
 {
     loopoctabox(cor, size, bo, bs)
     {
-        if(c[i].ext && c[i].ext->ents) if(!mmcollide(d, dir, *c[i].ext->ents)) return false;
+        if(c[i].ext && c[i].ext->ents) if(mmcollide(d, dir, cutoff, *c[i].ext->ents)) return true;
         ivec o(i, cor.x, cor.y, cor.z, size);
         if(c[i].children)
         {
-            if(!octacollide(d, dir, cutoff, bo, bs, c[i].children, o, size>>1)) return false;
+            if(octacollide(d, dir, cutoff, bo, bs, c[i].children, o, size>>1)) return true;
         }
         else
         {
@@ -1151,35 +1139,35 @@ static inline bool octacollide(physent *d, const vec &dir, float cutoff, const i
                 case MAT_CLIP: if(isclipped(c[i].material&MATF_VOLUME) || d->type<ENT_CAMERA) solid = true; break;
             }
             if(!solid && isempty(c[i])) continue;
-            if(!cubecollide(d, dir, cutoff, c[i], o, size, solid)) return false;
+            if(cubecollide(d, dir, cutoff, c[i], o, size, solid)) return true;
         }
     }
-    return true;
+    return false;
 }
 
 static inline bool octacollide(physent *d, const vec &dir, float cutoff, const ivec &bo, const ivec &bs)
 {
-    int diff = (bo.x^(bo.x+bs.x)) | (bo.y^(bo.y+bs.y)) | (bo.z^(bo.z+bs.z)),
+    int diff = (bo.x^bs.x) | (bo.y^bs.y) | (bo.z^bs.z),
         scale = worldscale-1;
-    if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|(bo.x+bs.x)|(bo.y+bs.y)|(bo.z+bs.z)) >= uint(hdr.worldsize))
+    if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|bs.x|bs.y|bs.z) >= uint(hdr.worldsize))
        return octacollide(d, dir, cutoff, bo, bs, worldroot, ivec(0, 0, 0), hdr.worldsize>>1);
     const cube *c = &worldroot[octastep(bo.x, bo.y, bo.z, scale)];
-    if(c->ext && c->ext->ents && !mmcollide(d, dir, *c->ext->ents)) return false;
+    if(c->ext && c->ext->ents && mmcollide(d, dir, cutoff, *c->ext->ents)) return true;
     scale--;
     while(c->children && !(diff&(1<<scale)))
     {
         c = &c->children[octastep(bo.x, bo.y, bo.z, scale)];
-        if(c->ext && c->ext->ents && !mmcollide(d, dir, *c->ext->ents)) return false;
+        if(c->ext && c->ext->ents && mmcollide(d, dir, cutoff, *c->ext->ents)) return true;
         scale--;
     }
     if(c->children) return octacollide(d, dir, cutoff, bo, bs, c->children, ivec(bo).mask(~((2<<scale)-1)), 1<<scale);
     bool solid = false;
     switch(c->material&MATF_CLIP)
     {
-        case MAT_NOCLIP: return true;
+        case MAT_NOCLIP: return false;
         case MAT_CLIP: if(isclipped(c->material&MATF_VOLUME) || d->type<ENT_CAMERA) solid = true; break;
     }
-    if(!solid && isempty(*c)) return true;
+    if(!solid && isempty(*c)) return false;
     int csize = 2<<scale, cmask = ~(csize-1);
     return cubecollide(d, dir, cutoff, *c, ivec(bo).mask(cmask), csize, solid);
 }
@@ -1187,15 +1175,14 @@ static inline bool octacollide(physent *d, const vec &dir, float cutoff, const i
 // all collision happens here
 bool collide(physent *d, const vec &dir, float cutoff, bool playercol)
 {
-    inside = false;
+    collideinside = false;
     hitplayer = NULL;
     hitflags = HITFLAG_NONE;
-    wall.x = wall.y = wall.z = 0;
+    collidewall = vec(0, 0, 0);
     ivec bo(int(d->o.x-d->radius), int(d->o.y-d->radius), int(d->o.z-d->height)),
-         bs(int(d->radius*2), int(d->radius*2), int(d->height+d->aboveeye));
-    bs.add(2);  // guard space for rounding errors
-    if(!octacollide(d, dir, cutoff, bo, bs)) return false;//, worldroot, ivec(0, 0, 0), hdr.worldsize>>1)) return false; // collide with world
-    return !playercol || plcollide(d, dir);
+         bs(int(d->o.x+d->radius), int(d->o.y+d->radius), int(d->o.z+d->aboveeye));
+    bs.add(1);  // guard space for rounding errors
+    return octacollide(d, dir, cutoff, bo, bs) || (playercol && plcollide(d, dir));
 }
 
 float pltracecollide(physent *d, const vec &from, const vec &ray, float maxdist)
@@ -1212,7 +1199,7 @@ float pltracecollide(physent *d, const vec &from, const vec &ray, float maxdist)
             physent *o = dynents[i];
             if(!physics::issolid(o, d)) continue;
             float dist = 1e16f;
-            if(!physics::xtracecollide(d, from, to, x1, x2, y1, y2, maxdist, dist, o) && dist < bestdist)
+            if(physics::xtracecollide(d, from, to, x1, x2, y1, y2, maxdist, dist, o) && dist < bestdist)
             {
                 bestdist = dist;
                 if(dist <= maxdist) { hitplayer = o; bestflags = hitflags; }
@@ -1239,7 +1226,7 @@ float tracecollide(physent *d, const vec &o, const vec &ray, float maxdist, int
 
 void phystest()
 {
-    static const char *states[] = {"float", "fall", "slide", "slope", "floor", "step up", "step down", "bounce"};
+    static const char * const states[] = {"float", "fall", "slide", "slope", "floor", "step up", "step down", "bounce"};
     physent *player = (physent *)game::focusedent();
     conoutf("PHYS(pl): %s, air %d, mat: %d, ladder: %s, floor: (%f, %f, %f), vel: (%f, %f, %f), g: (%f, %f, %f)", states[player->physstate], lastmillis-player->airmillis, player->inmaterial, player->onladder ? "yes" : "no", player->floor.x, player->floor.y, player->floor.z, player->vel.x, player->vel.y, player->vel.z, player->falling.x, player->falling.y, player->falling.z);
 }
diff --git a/src/engine/pvs.cpp b/src/engine/pvs.cpp
index aaf8ce2..1965063 100644
--- a/src/engine/pvs.cpp
+++ b/src/engine/pvs.cpp
@@ -421,7 +421,7 @@ struct pvsworker
         if(s.inside(geom)) p.flags |= PVS_HIDE_GEOM;
     }
 
-    ringbuf<shaftbb, 32> prevblockers;
+    queue<shaftbb, 32> prevblockers;
 
     struct cullorder
     {
@@ -717,14 +717,14 @@ struct pvsworker
         return true;
     }
 
-    bool materialoccluded(pvsnode &p, const ivec &co, int size, const ivec &bborigin, const ivec &bbsize)
+    bool materialoccluded(pvsnode &p, const ivec &co, int size, const ivec &bbmin, const ivec &bbmax)
     {
         pvsnode *children = &pvsnodes[p.children];
-        loopoctabox(co, size, bborigin, bbsize)
+        loopoctabox(co, size, bbmin, bbmax)
         {
             ivec o(i, co.x, co.y, co.z, size);
             if(children[i].flags & PVS_HIDE_BB) continue;
-            if(!children[i].children || !materialoccluded(children[i], o, size/2, bborigin, bbsize)) return false;
+            if(!children[i].children || !materialoccluded(children[i], o, size/2, bbmin, bbmax)) return false;
         }
         return true;
     }
@@ -736,13 +736,12 @@ struct pvsworker
         loopv(matsurfs)
         {
             materialsurface &m = *matsurfs[i];
-            ivec bborigin(m.o), bbsize(0, 0, 0);
+            ivec bbmin(m.o), bbmax(m.o);
             int dim = dimension(m.orient);
-            bbsize[C[dim]] = m.csize;
-            bbsize[R[dim]] = m.rsize;
-            bborigin[dim] -= 2;
-            bbsize[dim] = 2;
-            if(!materialoccluded(pvsnodes[0], vec(0, 0, 0), hdr.worldsize/2, bborigin, bbsize)) return false;
+            bbmin[dim] += dimcoord(m.orient) ? -2 : 2;
+            bbmax[C[dim]] += m.csize;
+            bbmax[R[dim]] += m.rsize;
+            if(!materialoccluded(pvsnodes[0], vec(0, 0, 0), hdr.worldsize/2, bbmin, bbmax)) return false;
         }
         return true;
     }
@@ -1202,46 +1201,53 @@ void pvsstats()
 
 COMMAND(0, pvsstats, "");
 
-static inline bool pvsoccluded(uchar *buf, const ivec &co, int size, const ivec &bborigin, const ivec &bbsize)
+static inline bool pvsoccluded(uchar *buf, const ivec &co, int size, const ivec &bbmin, const ivec &bbmax)
 {
     uchar leafmask = buf[0];
-    loopoctabox(co, size, bborigin, bbsize)
+    loopoctabox(co, size, bbmin, bbmax)
     {
         ivec o(i, co.x, co.y, co.z, size);
         if(leafmask&(1<<i))
         {
             uchar leafvalues = buf[1+i];
-            if(!leafvalues || (leafvalues!=0xFF && octantrectangleoverlap(o, size>>1, bborigin, bbsize)&~leafvalues))
+            if(!leafvalues || (leafvalues!=0xFF && octaboxoverlap(o, size>>1, bbmin, bbmax)&~leafvalues))
                 return false;
         }
-        else if(!pvsoccluded(buf+9*buf[1+i], o, size>>1, bborigin, bbsize)) return false;
+        else if(!pvsoccluded(buf+9*buf[1+i], o, size>>1, bbmin, bbmax)) return false;
     }
     return true;
 }
 
-static inline bool pvsoccluded(uchar *buf, const ivec &bborigin, const ivec &bbsize)
+static inline bool pvsoccluded(uchar *buf, const ivec &bbmin, const ivec &bbmax)
 {
-    int diff = (bborigin.x^(bborigin.x+bbsize.x)) | (bborigin.y^(bborigin.y+bbsize.y)) | (bborigin.z^(bborigin.z+bbsize.z));
+    int diff = (bbmin.x^bbmax.x) | (bbmin.y^bbmax.y) | (bbmin.z^bbmax.z);
     if(diff&~((1<<worldscale)-1)) return false;
     int scale = worldscale-1;
     while(!(diff&(1<<scale)))
     {
-        int i = octastep(bborigin.x, bborigin.y, bborigin.z, scale);
+        int i = octastep(bbmin.x, bbmin.y, bbmin.z, scale);
         scale--;
         uchar leafmask = buf[0];
         if(leafmask&(1<<i))
         {
             uchar leafvalues = buf[1+i];
-            return leafvalues && (leafvalues==0xFF || !(octantrectangleoverlap(ivec(bborigin).mask(~((2<<scale)-1)), 1<<scale, bborigin, bbsize)&~leafvalues));
+            return leafvalues && (leafvalues==0xFF || !(octaboxoverlap(ivec(bbmin).mask(~((2<<scale)-1)), 1<<scale, bbmin, bbmax)&~leafvalues));
         }
         buf += 9*buf[1+i];
     }
-    return pvsoccluded(buf, ivec(bborigin).mask(~((2<<scale)-1)), 1<<scale, bborigin, bbsize);
+    return pvsoccluded(buf, ivec(bbmin).mask(~((2<<scale)-1)), 1<<scale, bbmin, bbmax);
 }
 
-bool pvsoccluded(const ivec &bborigin, const ivec &bbsize)
+bool pvsoccluded(const ivec &bbmin, const ivec &bbmax)
 {
-    return curpvs!=NULL && pvsoccluded(curpvs, bborigin, bbsize);
+    return curpvs!=NULL && pvsoccluded(curpvs, bbmin, bbmax);
+}
+
+bool pvsoccludedsphere(const vec &center, float radius)
+{
+    if(curpvs==NULL) return false;
+    ivec bbmin = vec(center).sub(radius), bbmax = vec(center).add(radius+1);
+    return pvsoccluded(curpvs, bbmin, bbmax);
 }
 
 bool waterpvsoccluded(int height)
diff --git a/src/engine/ragdoll.h b/src/engine/ragdoll.h
index 274a537..c73ed36 100644
--- a/src/engine/ragdoll.h
+++ b/src/engine/ragdoll.h
@@ -1,3 +1,8 @@
+VAR(0, ragdolltimestepmin, 1, 5, 50);
+VAR(0, ragdolltimestepmax, 1, 10, 50);
+FVAR(0, ragdollrotfric, 0, 0.85f, 1);
+FVAR(0, ragdollrotfricstop, 0, 0.1f, 1);
+
 struct ragdollskel
 {
     struct vert
@@ -215,7 +220,6 @@ struct ragdolldata
 
     void init(dynent *d)
     {
-        extern int ragdolltimestepmin;
         float ts = ragdolltimestepmin/1000.0f;
         loopv(skel->verts) (verts[i].oldpos = verts[i].pos).sub(vec(d->vel).add(d->falling).mul(ts));
         timestep = ts;
@@ -237,6 +241,7 @@ struct ragdolldata
     void applyrotfriction(float ts);
     void tryunstick(float speed);
     void warppos(const vec &vel, const vec &offset);
+    void twitch(float vel);
 
     static inline bool collidevert(const vec &pos, const vec &dir, float radius)
     {
@@ -245,7 +250,6 @@ struct ragdolldata
             vertent()
             {
                 type = ENT_RAGDOLL;
-                collidetype = COLLIDE_AABB;
                 radius = xradius = yradius = height = aboveeye = 1;
             }
         } v;
@@ -334,11 +338,6 @@ void ragdolldata::constrainrot()
     }
 }
 
-VAR(0, ragdolltimestepmin, 1, 5, 50);
-VAR(0, ragdolltimestepmax, 1, 10, 50);
-FVAR(0, ragdollrotfric, 0, 0.85f, 1);
-FVAR(0, ragdollrotfricstop, 0, 0.1f, 1);
-
 void ragdolldata::calcrotfriction()
 {
     loopv(skel->rotfrictions)
@@ -385,7 +384,7 @@ void ragdolldata::tryunstick(float speed)
         vert &v = verts[i];
         if(v.stuck)
         {
-            if(!collidevert(v.pos, vec(0, 0, 0), skel->verts[i].radius)) { stuck++; continue; }
+            if(collidevert(v.pos, vec(0, 0, 0), skel->verts[i].radius)) { stuck++; continue; }
             v.stuck = false;
         }
         unstuck.add(v.pos);
@@ -406,7 +405,6 @@ void ragdolldata::tryunstick(float speed)
 
 void ragdolldata::warppos(const vec &vel, const vec &offset)
 {
-    extern int ragdolltimestepmin;
     float ts = ragdolltimestepmin/1000.0f;
     vec frame = vec(vel).mul(ts);
     loopv(skel->verts)
@@ -418,7 +416,16 @@ void ragdolldata::warppos(const vec &vel, const vec &offset)
     collidemillis = 0;
 }
 
-extern vec wall;
+void ragdolldata::twitch(float vel)
+{
+    float ts = ragdolltimestepmin/1000.0f;
+    loopv(skel->verts)
+    {
+        vert &v = verts[i];
+        v.oldpos.add(vec(rnd(201)-100, rnd(201)-100, rnd(201)-100).div(100.f).normalize().mul(vel).mul(ts));
+    }
+    collidemillis = 0;
+}
 
 void ragdolldata::updatepos()
 {
@@ -428,11 +435,11 @@ void ragdolldata::updatepos()
         if(v.weight)
         {
             v.newpos.div(v.weight);
-            if(collidevert(v.newpos, vec(v.newpos).sub(v.pos), skel->verts[i].radius)) v.pos = v.newpos;
+            if(!collidevert(v.newpos, vec(v.newpos).sub(v.pos), skel->verts[i].radius)) v.pos = v.newpos;
             else
             {
                 vec dir = vec(v.newpos).sub(v.oldpos);
-                if(dir.dot(wall) < 0) v.oldpos = vec(v.pos).sub(dir.reflect(wall));
+                if(dir.dot(collidewall) < 0) v.oldpos = vec(v.pos).sub(dir.reflect(collidewall));
                 v.collided = true;
             }
         }
@@ -495,11 +502,11 @@ void ragdolldata::move(dynent *pl, float ts)
         vert &v = verts[i];
         //if(v.pos.z < 0) { v.pos.z = 0; v.oldpos = v.pos; collisions++; }
         vec dir = vec(v.pos).sub(v.oldpos);
-        v.collided = !collidevert(v.pos, dir, skel->verts[i].radius);
+        v.collided = collidevert(v.pos, dir, skel->verts[i].radius);
         if(v.collided)
         {
             v.pos = v.oldpos;
-            v.oldpos.sub(dir.reflect(wall).mul(ragdollelasticity));
+            v.oldpos.sub(dir.reflect(collidewall).mul(ragdollelasticity));
             collisions++;
         }
     }
@@ -529,7 +536,7 @@ bool validragdoll(dynent *d, int millis)
 
 void moveragdoll(dynent *d, bool smooth)
 {
-    if(!curtime || !d->ragdoll) return;
+    if(!curtime || !d->ragdoll || (d->state != CS_DEAD && d->state != CS_WAITING)) return;
 
     if(!d->ragdoll->collidemillis || lastmillis < d->ragdoll->collidemillis)
     {
@@ -563,10 +570,16 @@ void cleanragdoll(dynent *d)
 
 void warpragdoll(dynent *d, const vec &vel, const vec &offset)
 {
-    if(!d->ragdoll) return;
+    if(!d->ragdoll || (d->state != CS_DEAD && d->state != CS_WAITING)) return;
     d->ragdoll->warppos(vel, offset);
 }
 
+void twitchragdoll(dynent *d, float vel)
+{
+    if(!d->ragdoll || (d->state != CS_DEAD && d->state != CS_WAITING)) return;
+    d->ragdoll->twitch(vel);
+}
+
 vec rdabove(dynent *d, float offset)
 {
     if((d->type != ENT_PLAYER && d->type != ENT_AI) || !d->ragdoll || (d->state != CS_DEAD && d->state != CS_WAITING)) return d->physent::abovehead(offset);
diff --git a/src/engine/rendergl.cpp b/src/engine/rendergl.cpp
index b734c27..2c8b9b2 100644
--- a/src/engine/rendergl.cpp
+++ b/src/engine/rendergl.cpp
@@ -5,9 +5,9 @@
 bool hasVBO = false, hasDRE = false, hasOQ = false, hasTR = false, hasFBO = false, hasDS = false, hasTF = false, hasBE = false, hasBC = false, hasCM = false, hasNP2 = false, hasTC = false, hasS3TC = false, hasFXT1 = false, hasTE = false, hasMT = false, hasD3 = false, hasAF = false, hasVP2 = false, hasVP3 = false, hasPP = false, hasMDA = false, hasTE3 = false, hasTE4 = false, hasVP = false, hasFP = false, hasGLSL = false, hasGM = false, hasNVFB = false, hasSGIDT = false, hasSGISH = false, [...]
 int hasstencil = 0;
 
-VAR(0, renderpath, 1, 0, 0);
-VAR(0, glversion, 1, 0, 0);
-VAR(0, glslversion, 1, 0, 0);
+VAR(IDF_READONLY, renderpath, 1, 0, 0);
+VAR(IDF_READONLY, glversion, 1, 0, 0);
+VAR(IDF_READONLY, glslversion, 1, 0, 0);
 
 // GL_ARB_vertex_buffer_object, GL_ARB_pixel_buffer_object
 PFNGLGENBUFFERSARBPROC       glGenBuffers_       = NULL;
@@ -201,14 +201,20 @@ bool hasext(const char *exts, const char *ext)
     return false;
 }
 
+SVAR(IDF_READONLY, gfxvendor, "");
+SVAR(IDF_READONLY, gfxexts, "");
+SVAR(IDF_READONLY, gfxrenderer, "");
+SVAR(IDF_READONLY, gfxversion, "");
+
 void gl_checkextensions()
 {
-    const char *vendor = (const char *)glGetString(GL_VENDOR);
-    const char *exts = (const char *)glGetString(GL_EXTENSIONS);
-    const char *renderer = (const char *)glGetString(GL_RENDERER);
-    const char *version = (const char *)glGetString(GL_VERSION);
-    conoutf("renderer: %s (%s)", renderer, vendor);
-    conoutf("driver: %s", version);
+    setsvar("gfxvendor", (const char *)glGetString(GL_VENDOR));
+    setsvar("gfxexts", (const char *)glGetString(GL_EXTENSIONS));
+    setsvar("gfxrenderer", (const char *)glGetString(GL_RENDERER));
+    setsvar("gfxversion", (const char *)glGetString(GL_VERSION));
+
+    conoutf("renderer: %s (%s)", gfxrenderer, gfxvendor);
+    conoutf("driver: %s", gfxversion);
 
 #ifdef __APPLE__
     extern int mac_osversion();
@@ -217,43 +223,43 @@ void gl_checkextensions()
 #endif
 
     bool mesa = false, intel = false, ati = false, nvidia = false;
-    if(strstr(renderer, "Mesa") || strstr(version, "Mesa"))
+    if(strstr(gfxrenderer, "Mesa") || strstr(gfxversion, "Mesa"))
     {
         mesa = true;
-        if(strstr(renderer, "Intel")) intel = true;
+        if(strstr(gfxrenderer, "Intel")) intel = true;
     }
-    else if(strstr(vendor, "NVIDIA"))
+    else if(strstr(gfxvendor, "NVIDIA"))
         nvidia = true;
-    else if(strstr(vendor, "ATI") || strstr(vendor, "Advanced Micro Devices"))
+    else if(strstr(gfxvendor, "ATI") || strstr(gfxvendor, "Advanced Micro Devices"))
         ati = true;
-    else if(strstr(vendor, "Intel"))
+    else if(strstr(gfxvendor, "Intel"))
         intel = true;
 
     uint glmajorversion, glminorversion;
-    if(sscanf(version, " %u.%u", &glmajorversion, &glminorversion) != 2) glversion = 100;
+    if(sscanf(gfxversion, " %u.%u", &glmajorversion, &glminorversion) != 2) glversion = 100;
     else glversion = glmajorversion*100 + glminorversion*10;
 
     //extern int shaderprecision;
     // default to low precision shaders on certain cards, can be overridden with -f3
     // char *weakcards[] = { "GeForce FX", "Quadro FX", "6200", "9500", "9550", "9600", "9700", "9800", "X300", "X600", "FireGL", "Intel", "Chrome", NULL }
-    // if(shaderprecision==2) for(char **wc = weakcards; *wc; wc++) if(strstr(renderer, *wc)) shaderprecision = 1;
+    // if(shaderprecision==2) for(char **wc = weakcards; *wc; wc++) if(strstr(gfxrenderer, *wc)) shaderprecision = 1;
 
     GLint val;
     glGetIntegerv(GL_MAX_TEXTURE_SIZE, &val);
     hwtexsize = val;
 
-    if(hasext(exts, "GL_EXT_texture_env_combine") || hasext(exts, "GL_ARB_texture_env_combine"))
+    if(hasext(gfxexts, "GL_EXT_texture_env_combine") || hasext(gfxexts, "GL_ARB_texture_env_combine"))
     {
         hasTE = true;
-        if(hasext(exts, "GL_ARB_texture_env_crossbar")) hasTEX = true;
-        if(hasext(exts, "GL_ATI_texture_env_combine3")) hasTE3 = true;
-        if(hasext(exts, "GL_NV_texture_env_combine4")) hasTE4 = true;
-        if(hasext(exts, "GL_EXT_texture_env_dot3") || hasext(exts, "GL_ARB_texture_env_dot3")) hasD3 = true;
+        if(hasext(gfxexts, "GL_ARB_texture_env_crossbar")) hasTEX = true;
+        if(hasext(gfxexts, "GL_ATI_texture_env_combine3")) hasTE3 = true;
+        if(hasext(gfxexts, "GL_NV_texture_env_combine4")) hasTE4 = true;
+        if(hasext(gfxexts, "GL_EXT_texture_env_dot3") || hasext(gfxexts, "GL_ARB_texture_env_dot3")) hasD3 = true;
         if(dbgexts) conoutf("\frUsing GL_ARB_texture_env_combine extension.");
     }
     else conoutf("\frWARNING: No texture_env_combine extension! (your video card is WAY too old)");
 
-    if(hasext(exts, "GL_ARB_multitexture"))
+    if(hasext(gfxexts, "GL_ARB_multitexture"))
     {
         glActiveTexture_       = (PFNGLACTIVETEXTUREARBPROC)      getprocaddress("glActiveTextureARB");
         glClientActiveTexture_ = (PFNGLCLIENTACTIVETEXTUREARBPROC)getprocaddress("glClientActiveTextureARB");
@@ -265,7 +271,7 @@ void gl_checkextensions()
     }
     else conoutf("\frWARNING: No multitexture extension");
 
-    if(hasext(exts, "GL_ARB_vertex_buffer_object"))
+    if(hasext(gfxexts, "GL_ARB_vertex_buffer_object"))
     {
         hasVBO = true;
         if(dbgexts) conoutf("\frUsing GL_ARB_vertex_buffer_object extension.");
@@ -277,7 +283,7 @@ void gl_checkextensions()
     if(osversion < 0x0A0600) maxvbosize = min(maxvbosize, 8192);
 #endif
 
-    if(hasext(exts, "GL_ARB_pixel_buffer_object"))
+    if(hasext(gfxexts, "GL_ARB_pixel_buffer_object"))
     {
         hasPBO = true;
         if(dbgexts) conoutf("\frUsing GL_ARB_pixel_buffer_object extension.");
@@ -295,14 +301,14 @@ void gl_checkextensions()
         glGetBufferSubData_ = (PFNGLGETBUFFERSUBDATAARBPROC)getprocaddress("glGetBufferSubDataARB");
     }
 
-    if(hasext(exts, "GL_EXT_draw_range_elements"))
+    if(hasext(gfxexts, "GL_EXT_draw_range_elements"))
     {
         glDrawRangeElements_ = (PFNGLDRAWRANGEELEMENTSEXTPROC)getprocaddress("glDrawRangeElementsEXT");
         hasDRE = true;
         if(dbgexts) conoutf("\frUsing GL_EXT_draw_range_elements extension.");
     }
 
-    if(hasext(exts, "GL_EXT_multi_draw_arrays"))
+    if(hasext(gfxexts, "GL_EXT_multi_draw_arrays"))
     {
         glMultiDrawArrays_   = (PFNGLMULTIDRAWARRAYSEXTPROC)  getprocaddress("glMultiDrawArraysEXT");
         glMultiDrawElements_ = (PFNGLMULTIDRAWELEMENTSEXTPROC)getprocaddress("glMultiDrawElementsEXT");
@@ -314,7 +320,7 @@ void gl_checkextensions()
     // floating point FBOs not fully supported until 10.5
     if(osversion>=0x0A0500)
 #endif
-    if(hasext(exts, "GL_ARB_texture_float") || hasext(exts, "GL_ATI_texture_float"))
+    if(hasext(gfxexts, "GL_ARB_texture_float") || hasext(gfxexts, "GL_ATI_texture_float"))
     {
         hasTF = true;
         if(dbgexts) conoutf("\frUsing GL_ARB_texture_float extension");
@@ -322,13 +328,13 @@ void gl_checkextensions()
         setvar("smoothshadowmappeel", 1, false, true);
     }
 
-    if(hasext(exts, "GL_NV_float_buffer"))
+    if(hasext(gfxexts, "GL_NV_float_buffer"))
     {
         hasNVFB = true;
         if(dbgexts) conoutf("\frUsing GL_NV_float_buffer extension.");
     }
 
-    if(hasext(exts, "GL_EXT_framebuffer_object"))
+    if(hasext(gfxexts, "GL_EXT_framebuffer_object"))
     {
         glBindRenderbuffer_        = (PFNGLBINDRENDERBUFFEREXTPROC)       getprocaddress("glBindRenderbufferEXT");
         glDeleteRenderbuffers_     = (PFNGLDELETERENDERBUFFERSEXTPROC)    getprocaddress("glDeleteRenderbuffersEXT");
@@ -344,7 +350,7 @@ void gl_checkextensions()
         hasFBO = true;
         if(dbgexts) conoutf("\frUsing GL_EXT_framebuffer_object extension.");
 
-        if(hasext(exts, "GL_EXT_framebuffer_blit"))
+        if(hasext(gfxexts, "GL_EXT_framebuffer_blit"))
         {
             glBlitFramebuffer_     = (PFNGLBLITFRAMEBUFFEREXTPROC)        getprocaddress("glBlitFramebufferEXT");
             hasFBB = true;
@@ -353,7 +359,7 @@ void gl_checkextensions()
     }
     else conoutf("\frWARNING: No framebuffer object support. (reflective water may be slow)");
 
-    if(hasext(exts, "GL_ARB_occlusion_query"))
+    if(hasext(gfxexts, "GL_ARB_occlusion_query"))
     {
         GLint bits;
         glGetQueryiv_ = (PFNGLGETQUERYIVARBPROC)getprocaddress("glGetQueryivARB");
@@ -381,7 +387,7 @@ void gl_checkextensions()
         setvar("waterreflect", 0, false, true);
     }
 
-    if(hasext(exts, "GL_ARB_vertex_program") && hasext(exts, "GL_ARB_fragment_program"))
+    if(hasext(gfxexts, "GL_ARB_vertex_program") && hasext(gfxexts, "GL_ARB_fragment_program"))
     {
         hasVP = hasFP = true;
         glGenProgramsARB_ =            (PFNGLGENPROGRAMSARBPROC)            getprocaddress("glGenProgramsARB");
@@ -490,10 +496,10 @@ void gl_checkextensions()
     {
         reservevpparams = 10;
         rtsharefb = 0; // work-around for strange driver stalls involving when using many FBOs
-        if(!hasext(exts, "GL_EXT_gpu_shader4")) setvar("filltjoints", 0, false, true); // DX9 or less NV cards seem to not cause many sparklies
+        if(!hasext(gfxexts, "GL_EXT_gpu_shader4")) setvar("filltjoints", 0, false, true); // DX9 or less NV cards seem to not cause many sparklies
 
         if(hasFBO && !hasTF) nvidia_scissor_bug = 1; // 5200 bug, clearing with scissor on an FBO messes up on reflections, may affect lesser cards too
-        if(hasTF && (!strstr(renderer, "GeForce") || !checkseries(renderer, 6000, 6600)))
+        if(hasTF && (!strstr(gfxrenderer, "GeForce") || !checkseries(gfxrenderer, 6000, 6600)))
             setvar("fpdepthfx", 1, false, true); // FP filtering causes software fallback on 6200?
     }
     else
@@ -535,10 +541,10 @@ void gl_checkextensions()
         }
     }
 
-    if(hasext(exts, "GL_NV_vertex_program2_option")) { usevp2 = 1; hasVP2 = true; }
-    if(hasext(exts, "GL_NV_vertex_program3")) { usevp3 = 1; hasVP3 = true; }
+    if(hasext(gfxexts, "GL_NV_vertex_program2_option")) { usevp2 = 1; hasVP2 = true; }
+    if(hasext(gfxexts, "GL_NV_vertex_program3")) { usevp3 = 1; hasVP3 = true; }
 
-    if(hasext(exts, "GL_EXT_gpu_program_parameters"))
+    if(hasext(gfxexts, "GL_EXT_gpu_program_parameters"))
     {
         glProgramEnvParameters4fv_   = (PFNGLPROGRAMENVPARAMETERS4FVEXTPROC)  getprocaddress("glProgramEnvParameters4fvEXT");
         glProgramLocalParameters4fv_ = (PFNGLPROGRAMLOCALPARAMETERS4FVEXTPROC)getprocaddress("glProgramLocalParameters4fvEXT");
@@ -546,7 +552,7 @@ void gl_checkextensions()
         if(dbgexts) conoutf("\frUsing GL_EXT_gpu_program_parameters extension.");
     }
 
-    if(hasext(exts, "GL_ARB_map_buffer_range"))
+    if(hasext(gfxexts, "GL_ARB_map_buffer_range"))
     {
         glMapBufferRange_         = (PFNGLMAPBUFFERRANGEPROC)        getprocaddress("glMapBufferRange");
         glFlushMappedBufferRange_ = (PFNGLFLUSHMAPPEDBUFFERRANGEPROC)getprocaddress("glFlushMappedBufferRange");
@@ -554,7 +560,7 @@ void gl_checkextensions()
         if(dbgexts) conoutf("\frUsing GL_ARB_map_buffer_range.");
     }
 
-    if(hasext(exts, "GL_ARB_uniform_buffer_object"))
+    if(hasext(gfxexts, "GL_ARB_uniform_buffer_object"))
     {
         glGetUniformIndices_       = (PFNGLGETUNIFORMINDICESPROC)      getprocaddress("glGetUniformIndices");
         glGetActiveUniformsiv_     = (PFNGLGETACTIVEUNIFORMSIVPROC)    getprocaddress("glGetActiveUniformsiv");
@@ -569,7 +575,7 @@ void gl_checkextensions()
         if(ati) ati_ubo_bug = 1;
         if(dbgexts) conoutf("\frUsing GL_ARB_uniform_buffer_object extension.");
     }
-    else if(hasext(exts, "GL_EXT_bindable_uniform"))
+    else if(hasext(gfxexts, "GL_EXT_bindable_uniform"))
     {
         glUniformBuffer_        = (PFNGLUNIFORMBUFFEREXTPROC)       getprocaddress("glUniformBufferEXT");
         glGetUniformBufferSize_ = (PFNGLGETUNIFORMBUFFERSIZEEXTPROC)getprocaddress("glGetUniformBufferSizeEXT");
@@ -581,7 +587,7 @@ void gl_checkextensions()
         if(dbgexts) conoutf("\frUsing GL_EXT_bindable_uniform extension.");
     }
 
-    if(hasext(exts, "GL_EXT_texture_rectangle") || hasext(exts, "GL_ARB_texture_rectangle"))
+    if(hasext(gfxexts, "GL_EXT_texture_rectangle") || hasext(gfxexts, "GL_ARB_texture_rectangle"))
     {
         usetexrect = 1;
         hasTR = true;
@@ -589,13 +595,13 @@ void gl_checkextensions()
     }
     else if(hasMT && hasshaders) conoutf("\frWARNING: No texture rectangle support. (no full screen shaders)");
 
-    if(hasext(exts, "GL_EXT_packed_depth_stencil") || hasext(exts, "GL_NV_packed_depth_stencil"))
+    if(hasext(gfxexts, "GL_EXT_packed_depth_stencil") || hasext(gfxexts, "GL_NV_packed_depth_stencil"))
     {
         hasDS = true;
         if(dbgexts) conoutf("\frUsing GL_EXT_packed_depth_stencil extension.");
     }
 
-    if(hasext(exts, "GL_EXT_blend_minmax"))
+    if(hasext(gfxexts, "GL_EXT_blend_minmax"))
     {
         glBlendEquation_ = (PFNGLBLENDEQUATIONEXTPROC) getprocaddress("glBlendEquationEXT");
         hasBE = true;
@@ -603,21 +609,21 @@ void gl_checkextensions()
         if(dbgexts) conoutf("\frUsing GL_EXT_blend_minmax extension.");
     }
 
-    if(hasext(exts, "GL_EXT_blend_color"))
+    if(hasext(gfxexts, "GL_EXT_blend_color"))
     {
         glBlendColor_ = (PFNGLBLENDCOLOREXTPROC) getprocaddress("glBlendColorEXT");
         hasBC = true;
         if(dbgexts) conoutf("\frUsing GL_EXT_blend_color extension.");
     }
 
-    if(hasext(exts, "GL_EXT_fog_coord"))
+    if(hasext(gfxexts, "GL_EXT_fog_coord"))
     {
         glFogCoordPointer_ = (PFNGLFOGCOORDPOINTEREXTPROC) getprocaddress("glFogCoordPointerEXT");
         hasFC = true;
         if(dbgexts) conoutf("\frUsing GL_EXT_fog_coord extension.");
     }
 
-    if(hasext(exts, "GL_ARB_texture_cube_map"))
+    if(hasext(gfxexts, "GL_ARB_texture_cube_map"))
     {
         GLint val;
         glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE_ARB, &val);
@@ -630,14 +636,14 @@ void gl_checkextensions()
     else conoutf("\frWARNING: No cube map texture support. (no reflective glass)");
 
     extern int usenp2;
-    if(hasext(exts, "GL_ARB_texture_non_power_of_two"))
+    if(hasext(gfxexts, "GL_ARB_texture_non_power_of_two"))
     {
         hasNP2 = true;
         if(dbgexts) conoutf("\frUsing GL_ARB_texture_non_power_of_two extension.");
     }
     else if(usenp2) conoutf("\frWARNING: Non-power-of-two textures not supported");
 
-    if(hasext(exts, "GL_ARB_texture_compression"))
+    if(hasext(gfxexts, "GL_ARB_texture_compression"))
     {
         glCompressedTexImage3D_ =    (PFNGLCOMPRESSEDTEXIMAGE3DARBPROC)   getprocaddress("glCompressedTexImage3DARB");
         glCompressedTexImage2D_ =    (PFNGLCOMPRESSEDTEXIMAGE2DARBPROC)   getprocaddress("glCompressedTexImage2DARB");
@@ -650,7 +656,7 @@ void gl_checkextensions()
         hasTC = true;
         if(dbgexts) conoutf("\frUsing GL_ARB_texture_compression.");
 
-        if(hasext(exts, "GL_EXT_texture_compression_s3tc"))
+        if(hasext(gfxexts, "GL_EXT_texture_compression_s3tc"))
         {
             hasS3TC = true;
 #ifdef __APPLE__
@@ -660,12 +666,12 @@ void gl_checkextensions()
 #endif
             if(dbgexts) conoutf("\frUsing GL_EXT_texture_compression_s3tc extension.");
         }
-        else if(hasext(exts, "GL_EXT_texture_compression_dxt1") && hasext(exts, "GL_ANGLE_texture_compression_dxt3") && hasext(exts, "GL_ANGLE_texture_compression_dxt5"))
+        else if(hasext(gfxexts, "GL_EXT_texture_compression_dxt1") && hasext(gfxexts, "GL_ANGLE_texture_compression_dxt3") && hasext(gfxexts, "GL_ANGLE_texture_compression_dxt5"))
         {
             hasS3TC = true;
             if(dbgexts) conoutf("\frUsing GL_EXT_texture_compression_dxt1 extension.");
         }
-        if(hasext(exts, "GL_3DFX_texture_compression_FXT1"))
+        if(hasext(gfxexts, "GL_3DFX_texture_compression_FXT1"))
         {
             hasFXT1 = true;
             if(mesa) usetexcompress = max(usetexcompress, 1);
@@ -673,45 +679,45 @@ void gl_checkextensions()
         }
     }
 
-    if(hasext(exts, "GL_EXT_texture_filter_anisotropic"))
+    if(hasext(gfxexts, "GL_EXT_texture_filter_anisotropic"))
     {
        GLint val;
        glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &val);
-       hwmaxaniso = val;
+       hwmaxanisotropy = val;
        hasAF = true;
        if(dbgexts) conoutf("\frUsing GL_EXT_texture_filter_anisotropic extension.");
     }
 
-    if(hasext(exts, "GL_SGIS_generate_mipmap"))
+    if(hasext(gfxexts, "GL_SGIS_generate_mipmap"))
     {
         hasGM = true;
         if(dbgexts) conoutf("\frUsing GL_SGIS_generate_mipmap extension.");
     }
 
-    if(hasext(exts, "GL_ARB_depth_texture"))
+    if(hasext(gfxexts, "GL_ARB_depth_texture"))
     {
         hasSGIDT = hasDT = true;
         if(dbgexts) conoutf("\frUsing GL_ARB_depth_texture extension.");
     }
-    else if(hasext(exts, "GL_SGIX_depth_texture"))
+    else if(hasext(gfxexts, "GL_SGIX_depth_texture"))
     {
         hasSGIDT = true;
         if(dbgexts) conoutf("\frUsing GL_SGIX_depth_texture extension.");
     }
 
-    if(hasext(exts, "GL_ARB_shadow"))
+    if(hasext(gfxexts, "GL_ARB_shadow"))
     {
         hasSGISH = hasSH = true;
-        if(nvidia || (ati && strstr(renderer, "Radeon HD"))) hasNVPCF = true;
+        if(nvidia || (ati && strstr(gfxrenderer, "Radeon HD"))) hasNVPCF = true;
         if(dbgexts) conoutf("\frUsing GL_ARB_shadow extension.");
     }
-    else if(hasext(exts, "GL_SGIX_shadow"))
+    else if(hasext(gfxexts, "GL_SGIX_shadow"))
     {
         hasSGISH = true;
         if(dbgexts) conoutf("\frUsing GL_SGIX_shadow extension.");
     }
 
-    if(hasext(exts, "GL_EXT_rescale_normal"))
+    if(hasext(gfxexts, "GL_EXT_rescale_normal"))
     {
         hasRN = true;
         if(dbgexts) conoutf("\frUsing GL_EXT_rescale_normal extension.");
@@ -719,7 +725,7 @@ void gl_checkextensions()
 
     if(!hasSGIDT && !hasSGISH) setvar("shadowmap", 0, false, true);
 
-    if(hasext(exts, "GL_EXT_gpu_shader4") && !avoidshaders)
+    if(hasext(gfxexts, "GL_EXT_gpu_shader4") && !avoidshaders)
     {
         // on DX10 or above class cards (i.e. GF8 or RadeonHD) enable expensive features
         setvar("grass", 1, false, true);
@@ -742,8 +748,8 @@ void gl_checkextensions()
 
 void glext(char *ext)
 {
-    const char *exts = (const char *)glGetString(GL_EXTENSIONS);
-    intret(hasext(exts, ext) ? 1 : 0);
+    if(!*gfxexts) setsvar("gfxexts", (const char *)glGetString(GL_EXTENSIONS));
+    intret(hasext(gfxexts, ext) ? 1 : 0);
 }
 COMMAND(0, glext, "s");
 
@@ -832,8 +838,7 @@ vec worldpos, camdir, camright, camup;
 
 void findorientation(vec &o, float yaw, float pitch, vec &pos)
 {
-    vec dir;
-    vecfromyawpitch(yaw, pitch, 1, 0, dir);
+    vec dir(yaw*RAD, pitch*RAD);
     if(raycubepos(o, dir, pos, 0, RAY_CLIPMAT|RAY_SKIPFIRST) == -1)
         pos = dir.mul(2*hdr.worldsize).add(o); //otherwise gui won't work when outside of map
 }
@@ -1184,7 +1189,6 @@ bool deferdrawtextures = false;
 
 void drawtextures()
 {
-    extern bool minimized;
     if(minimized) { deferdrawtextures = true; return; }
     deferdrawtextures = false;
     genenvmaps();
@@ -2382,7 +2386,7 @@ void drawviewtype(int targtype)
 
     glDisable(GL_TEXTURE_2D);
     notextureshader->set();
-    if(editmode)
+    if(editmode && !pixeling)
     {
         glEnable(GL_DEPTH_TEST);
         glDepthMask(GL_FALSE);
@@ -2404,33 +2408,36 @@ void drawviewtype(int targtype)
     glOrtho(0, w, h, 0, -1, 1);
     glColor3f(1, 1, 1);
 
-    extern int debugsm;
-    if(debugsm)
+    if(!pixeling || !editmode)
     {
-        extern void viewshadowmap();
-        viewshadowmap();
-    }
+        extern int debugsm;
+        if(debugsm)
+        {
+            extern void viewshadowmap();
+            viewshadowmap();
+        }
 
-    extern int debugglare;
-    if(debugglare)
-    {
-        extern void viewglaretex();
-        viewglaretex();
-    }
+        extern int debugglare;
+        if(debugglare)
+        {
+            extern void viewglaretex();
+            viewglaretex();
+        }
 
-    extern int debugdepthfx;
-    if(debugdepthfx)
-    {
-        extern void viewdepthfxtex();
-        viewdepthfxtex();
-    }
+        extern int debugdepthfx;
+        if(debugdepthfx)
+        {
+            extern void viewdepthfxtex();
+            viewdepthfxtex();
+        }
 
-    glEnable(GL_TEXTURE_2D);
-    defaultshader->set();
-    hud::drawhud();
-    rendertexturepanel(w, h);
-    hud::drawlast();
-    glDisable(GL_TEXTURE_2D);
+        glEnable(GL_TEXTURE_2D);
+        defaultshader->set();
+        hud::drawhud();
+        rendertexturepanel(w, h);
+        hud::drawlast();
+        glDisable(GL_TEXTURE_2D);
+    }
 
     renderedgame = false;
 
diff --git a/src/engine/rendermodel.cpp b/src/engine/rendermodel.cpp
index 12899ce..d13b183 100644
--- a/src/engine/rendermodel.cpp
+++ b/src/engine/rendermodel.cpp
@@ -1,7 +1,7 @@
 #include "engine.h"
 
-VAR(IDF_PERSIST, oqdynent, 0, 1, 1);
-VAR(IDF_PERSIST, animationinterpolationtime, 0, 150, 1000);
+VAR(0, oqdynent, 0, 1, 1);
+VAR(0, animationinterpolationtime, 0, 200, 1000);
 
 model *loadingmodel = NULL;
 
@@ -23,7 +23,7 @@ static model *__loadmodel__##modelclass(const char *filename) \
 { \
     return new modelclass(filename); \
 } \
-static int __dummy__##modelclass = addmodeltype((modeltype), __loadmodel__##modelclass);
+UNUSED static int __dummy__##modelclass = addmodeltype((modeltype), __loadmodel__##modelclass);
 
 #include "md2.h"
 #include "md3.h"
@@ -184,9 +184,8 @@ COMMAND(0, mdlspin, "fff");
 void mdlscale(int *percent)
 {
     checkmdl;
-    float scale = 0.3f;
+    float scale = 1.0f;
     if(*percent>0) scale = *percent/100.0f;
-    else if(*percent<0) scale = 0.0f;
     loadingmodel->scale = scale;
 }
 
@@ -243,7 +242,7 @@ COMMAND(0, mdlshadow, "i");
 void mdlbb(float *rad, float *h, float *height)
 {
     checkmdl;
-    loadingmodel->collideradius = *rad;
+    loadingmodel->collidexyradius = *rad;
     loadingmodel->collideheight = *h;
     loadingmodel->height = *height;
 }
@@ -269,6 +268,7 @@ COMMAND(0, mdlname, "");
 #define checkragdoll \
     if(!loadingmodel->skeletal()) { conoutf("\frnot loading a skeletal model"); return; } \
     skelmodel *m = (skelmodel *)loadingmodel; \
+    if(m->parts.empty()) return; \
     skelmodel::skelmeshgroup *meshes = (skelmodel::skelmeshgroup *)m->parts.last()->meshes; \
     if(!meshes) return; \
     skelmodel::skeleton *skel = meshes->skel; \
@@ -501,9 +501,8 @@ COMMAND(0, clearmodel, "s");
 
 bool modeloccluded(const vec &center, float radius)
 {
-    int br = int(radius*2)+1;
-    return pvsoccluded(ivec(int(center.x-radius), int(center.y-radius), int(center.z-radius)), ivec(br, br, br)) ||
-           bboccluded(ivec(int(center.x-radius), int(center.y-radius), int(center.z-radius)), ivec(br, br, br));
+    ivec bbmin = vec(center).sub(radius), bbmax = vec(center).add(radius+1);
+    return pvsoccluded(bbmin, bbmax) || bboccluded(bbmin, bbmax);
 }
 
 VAR(0, showboundingbox, 0, 0, 2);
@@ -637,7 +636,7 @@ void endmodelbatches()
         if(b.flags&(MDL_SHADOW|MDL_DYNSHADOW))
         {
             vec center, bbradius;
-            b.m->boundbox(0/*frame*/, center, bbradius); // FIXME
+            b.m->boundbox(center, bbradius);
             loopvj(b.batched)
             {
                 batchedmodel &bm = b.batched[j];
@@ -741,7 +740,7 @@ void endmodelquery()
     modelattached.setsize(minattached);
 }
 
-VAR(IDF_PERSIST, maxmodelradiusdistance, 10, 200, 1000);
+VAR(0, maxmodelradiusdistance, 10, 200, 1000);
 
 void rendermodelquery(model *m, dynent *d, const vec &center, float radius)
 {
@@ -805,7 +804,7 @@ void rendermodel(entitylight *light, const char *mdl, int anim, const vec &o, fl
          doOQ = flags&MDL_CULL_QUERY && hasOQ && oqfrags && oqdynent;
     if(flags&(MDL_CULL_VFC|MDL_CULL_DIST|MDL_CULL_OCCLUDED|MDL_CULL_QUERY|MDL_SHADOW|MDL_DYNSHADOW))
     {
-        m->boundbox(0/*frame*/, center, bbradius); // FIXME
+        m->boundbox(center, bbradius);
         radius = bbradius.magnitude();
         if(d && d->ragdoll)
         {
@@ -878,7 +877,7 @@ void rendermodel(entitylight *light, const char *mdl, int anim, const vec &o, fl
         glEnable(GL_BLEND);
         glBlendFunc(GL_ONE, GL_ONE);
 
-        if(d && showboundingbox==1)
+        if(d && showboundingbox==1 && (d->type == ENT_PLAYER || d->type == ENT_AI))
         {
             if(d->ragdoll && (d->state == CS_DEAD || d->state == CS_WAITING))
             {
@@ -897,8 +896,8 @@ void rendermodel(entitylight *light, const char *mdl, int anim, const vec &o, fl
         else
         {
             vec center, radius;
-            if(showboundingbox==1) m->collisionbox(0, center, radius);
-            else m->boundbox(0, center, radius);
+            if(showboundingbox==1) m->collisionbox(center, radius);
+            else m->boundbox(center, radius);
             center.mul(size);
             radius.mul(size);
             glTranslatef(o.x, o.y, o.z);
@@ -1031,7 +1030,7 @@ void abovemodel(vec &o, const char *mdl)
 {
     model *m = loadmodel(mdl);
     if(!m) return;
-    o.z += m->above(0/*frame*/);
+    o.z += m->above();
 }
 
 bool matchanim(const char *name, const char *pattern)
@@ -1093,13 +1092,9 @@ void setbbfrommodel(dynent *d, const char *mdl, float size)
     model *m = loadmodel(mdl);
     if(!m) return;
     vec center, radius;
-    m->collisionbox(0, center, radius);
+    m->collisionbox(center, radius);
     if(d->type==ENT_INANIMATE && !m->ellipsecollide)
-    {
         d->collidetype = COLLIDE_OBB;
-        //d->collidetype = COLLIDE_AABB;
-        //rotatebb(center, radius, int(d->yaw));
-    }
     d->xradius  = (radius.x + fabs(center.x))*size;
     d->yradius  = (radius.y + fabs(center.y))*size;
     d->radius   = d->collidetype==COLLIDE_OBB ? sqrtf(d->xradius*d->xradius + d->yradius*d->yradius) : max(d->xradius, d->yradius);
diff --git a/src/engine/renderparticles.cpp b/src/engine/renderparticles.cpp
index 801eb54..07d005c 100644
--- a/src/engine/renderparticles.cpp
+++ b/src/engine/renderparticles.cpp
@@ -14,13 +14,13 @@ VAR(IDF_PERSIST, particletext, 0, 1, 1);
 VAR(IDF_PERSIST, particleglare, 0, 1, 100);
 VAR(0, debugparticles, 0, 0, 1);
 
-// Check emit_particles() to limit the rate that paricles can be emitted for models/sparklies
+// Check canemitparticles() to limit the rate that paricles can be emitted for models/sparklies
 // Automatically stops particles being emitted when paused or in reflective drawing
 VAR(IDF_PERSIST, emitmillis, 1, 15, VAR_MAX);
 static int lastemitframe = 0;
 static bool emit = false;
 
-static bool emit_particles()
+static bool canemitparticles()
 {
     if(reflecting || refracting) return false;
     return emit;
@@ -224,7 +224,7 @@ struct listrenderer : partrenderer
         p->blend = blend;
         p->grav = grav;
         p->collide = collide;
-        if((p->owner = pl) != NULL && (p->owner->type == ENT_PLAYER || p->owner->type == ENT_AI)) switch(type&0xFF)
+        if((p->owner = pl) != NULL && (p->owner->type == ENT_PLAYER || p->owner->type == ENT_AI)) switch(type&PT_TYPE)
         {
             case PT_TEXT: case PT_ICON: p->m.add(vec(p->o).sub(p->owner->abovehead())); break;
             default: break;
@@ -322,7 +322,7 @@ struct textrenderer : sharedlistrenderer
         {
             const char *start = text;
             while(*text && *text != '>') text++;
-            if(*text) { int len = text-(start+1); strncpy(font, start+1, len); font[len] = 0; text++; }
+            if(*text) { int len = text-(start+1); memcpy(font, start+1, len); font[len] = 0; text++; }
             else text = start;
         }
         float xoff = -text_width(text)/2;
@@ -1004,7 +1004,7 @@ static coneprimitiverenderer coneprimitives(PT_CONE|PT_LERP), coneontopprimitive
 
 static partrenderer *parts[] =
 {
-    new portalrenderer("<grey>textures/teleport"), &icons,
+    new portalrenderer("<grey>particles/teleport"), &icons,
     &lineprimitives, &lineontopprimitives, &trisprimitives, &trisontopprimitives,
     &loopprimitives, &loopontopprimitives, &coneprimitives, &coneontopprimitives,
     new softquadrenderer("<grey>particles/fire", PT_PART|PT_GLARE|PT_RND4|PT_FLIP|PT_LERP|PT_SHRINK),
@@ -1033,14 +1033,17 @@ static partrenderer *parts[] =
     new quadrenderer("<grey>particles/plasma", PT_PART|PT_GLARE|PT_FLIP|PT_SHRINK),
     new softquadrenderer("<grey>particles/electric", PT_PART|PT_GLARE|PT_RND4|PT_FLIP|PT_SHRINK),
     new quadrenderer("<grey>particles/electric", PT_PART|PT_GLARE|PT_RND4|PT_FLIP|PT_SHRINK),
+    new softquadrenderer("<grey>particles/eleczap", PT_PART|PT_GLARE|PT_RND4|PT_FLIP|PT_SHRINK),
+    new quadrenderer("<grey>particles/eleczap", PT_PART|PT_GLARE|PT_RND4|PT_FLIP|PT_SHRINK),
     new quadrenderer("<grey>particles/fire", PT_PART|PT_GLARE|PT_FLIP|PT_RND4|PT_GLARE|PT_SHRINK),
     new taperenderer("<grey>particles/sflare", PT_TAPE|PT_GLARE),
     new taperenderer("<grey>particles/mflare", PT_TAPE|PT_GLARE|PT_RND4|PT_VFLIP|PT_GLARE),
     new taperenderer("<grey>particles/lightning", PT_TAPE|PT_GLARE|PT_HFLIP|PT_VFLIP, 2), // uses same clamp setting as normal lightning to avoid conflict
+    new taperenderer("<grey>particles/lightzap", PT_TAPE|PT_GLARE|PT_HFLIP|PT_VFLIP, 2),
     new quadrenderer("<grey>particles/muzzle", PT_PART|PT_GLARE|PT_RND4|PT_FLIP),
     new quadrenderer("<grey>particles/snow", PT_PART|PT_GLARE|PT_FLIP),
     &texts, &textontop,
-    &explosions, &shockwaves, &shockballs, &lightnings,
+    &explosions, &shockwaves, &shockballs, &lightnings, &lightzaps,
     &flares // must be done last!
 };
 
@@ -1121,7 +1124,7 @@ void renderparticles(bool mainpass)
         {
             int type = parts[i]->type;
             const char *title = parts[i]->texname ? strrchr(parts[i]->texname, '/')+1 : NULL;
-            mkstring(info);
+            string info = "";
             if(type&PT_GLARE) concatstring(info, "g,");
             if(type&PT_SOFT) concatstring(info, "s,");
             if(type&PT_LERP) concatstring(info, "l,");
@@ -1129,7 +1132,7 @@ void renderparticles(bool mainpass)
             if(type&PT_RND4) concatstring(info, "r,");
             if(type&PT_FLIP) concatstring(info, "f,");
             if(type&PT_ONTOP) concatstring(info, "o,");
-            defformatstring(ds)("%d\t%s: %s %s", parts[i]->count(), partnames[type&0xFF], info, (title?title:""));
+            defformatstring(ds)("%d\t%s: %s %s", parts[i]->count(), partnames[type&PT_TYPE], info, (title?title:""));
             draw_text(ds, FONTH, (i+n/2)*FONTH);
         }
         glDisable(GL_BLEND);
@@ -1264,7 +1267,7 @@ void create(int type, int color, int fade, const vec &p, float size, float blend
 
 void regularcreate(int type, int color, int fade, const vec &p, float size, float blend, int grav, int collide, physent *pl, int delay)
 {
-    if(!emit_particles() || (delay > 0 && rnd(delay) != 0)) return;
+    if(!canemitparticles() || (delay > 0 && rnd(delay) != 0)) return;
     create(type, color, fade, p, size, blend, grav, collide, pl);
 }
 
@@ -1287,13 +1290,13 @@ void splash(int type, int color, float radius, int num, int fade, const vec &p,
 
 void regularsplash(int type, int color, float radius, int num, int fade, const vec &p, float size, float blend, int grav, int collide, float vel, int delay)
 {
-    if(!emit_particles() || (delay > 0 && rnd(delay) != 0)) return;
+    if(!canemitparticles() || (delay > 0 && rnd(delay) != 0)) return;
     splash(type, color, radius, num, fade, p, size, blend, grav, collide, vel);
 }
 
 bool canaddparticles()
 {
-    return !renderedgame && !shadowmapping;
+    return !renderedgame && !shadowmapping && !minimized;
 }
 
 void regular_part_create(int type, int fade, const vec &p, int color, float size, float blend, int grav, int collide, physent *pl, int delay)
@@ -1369,7 +1372,7 @@ void part_explosion(const vec &dest, float maxsize, int type, int fade, int colo
 
 void regular_part_explosion(const vec &dest, float maxsize, int type, int fade, int color, float size, float blend, int grav, int collide)
 {
-    if(!canaddparticles() || !emit_particles()) return;
+    if(!canaddparticles() || !canemitparticles()) return;
     part_explosion(dest, maxsize, type, fade, color, size, blend, grav, collide);
 }
 
@@ -1431,7 +1434,7 @@ void part_dir(const vec &o, float yaw, float pitch, float length, float size, fl
 {
     if(!canaddparticles()) return;
 
-    vec v; vecfromyawpitch(yaw, pitch, 1, 0, v); v.normalize();
+    vec v(yaw*RAD, pitch*RAD);
     part_line(o, vec(v).mul(length).add(o), size, blend, fade, color);
     if(interval)
     {
@@ -1509,9 +1512,9 @@ static inline vec offsetvec(vec o, int dir, int dist)
  */
 void regularshape(int type, float radius, int color, int dir, int num, int fade, const vec &p, float size, float blend, int grav, int collide, float vel)
 {
-    if(!emit_particles()) return;
+    if(!canemitparticles()) return;
 
-    int basetype = parts[type]->type&0xFF;
+    int basetype = parts[type]->type&PT_TYPE;
     bool flare = (basetype == PT_TAPE) || (basetype == PT_LIGHTNING),
          inv = (dir&0x20)!=0, taper = (dir&0x40)!=0;
     dir &= 0x1F;
@@ -1607,7 +1610,7 @@ void regularshape(int type, float radius, int color, int dir, int num, int fade,
 
 void regularflame(int type, const vec &p, float radius, float height, int color, int density, int fade, float size, float blend, int grav, int collide, float vel)
 {
-    if(!emit_particles()) return;
+    if(!canemitparticles()) return;
 
     float s = size*min(radius, height);
     vec v(0, 0, min(1.0f, height)*vel);
@@ -1632,6 +1635,8 @@ static int partcolour(int c, int p, int x)
 }
 void makeparticle(const vec &o, attrvector &attr)
 {
+    bool oldemit = emit;
+    if(attr[11]) emit = true;
     switch(attr[0])
     {
         case 0: //fire
@@ -1709,10 +1714,9 @@ void makeparticle(const vec &o, attrvector &attr)
         case 5:
         {
             float length = clamp(attr[1], 0, 100)/100.f;
-            Texture *t = textureload(hud::progresstex, 3);
             int colour = partcolour(attr[2], attr[4], attr[5]);
-            part_icon(o, t, 2, 1, 0, 0, 1, colour, 0, length);
-            part_icon(o, t, 3, 1, 0, 0, 1, colour, (totalmillis%1000)/1000.f, 0.1f);
+            part_icon(o, textureload(hud::progringtex, 3), 3, 1, 0, 0, 1, colour, (totalmillis%1000)/1000.f, 0.1f);
+            part_icon(o, textureload(hud::progresstex, 3), 3, 1, 0, 0, 1, colour, 0, length);
             break;
         }
         case 32: //lens flares - plain/sparkle/sun/sparklesun <red> <green> <blue>
@@ -1726,11 +1730,13 @@ void makeparticle(const vec &o, attrvector &attr)
             part_textcopy(o, ds);
             break;
     }
+    emit = oldemit;
 }
-void makeparticles(extentity &e) { makeparticle(e.o, e.attrs); }
 
 void updateparticles()
 {
+    if(minimized) { emit = false; return; }
+
     if(lastmillis-lastemitframe >= emitmillis)
     {
         emit = true;
diff --git a/src/engine/rendersky.cpp b/src/engine/rendersky.cpp
index 3d625c5..e88be6b 100644
--- a/src/engine/rendersky.cpp
+++ b/src/engine/rendersky.cpp
@@ -203,18 +203,15 @@ void draw_env_overlay(int w, float height, int subdiv, float fade, float scale,
 static struct domevert
 {
     vec pos;
-    uchar color[4];
+    bvec4 color;
 
     domevert() {}
-    domevert(const vec &pos, const bvec &fcolor, float alpha) : pos(pos)
+    domevert(const vec &pos, const bvec &fcolor, float alpha) : pos(pos), color(fcolor, uchar(alpha*255))
     {
-        memcpy(color, fcolor.v, 3);
-        color[3] = uchar(alpha*255);
     }
-    domevert(const domevert &v0, const domevert &v1) : pos(vec(v0.pos).add(v1.pos).normalize())
+    domevert(const domevert &v0, const domevert &v1) : pos(vec(v0.pos).add(v1.pos).normalize()), color(v0.color)
     {
-        memcpy(color, v0.color, 4);
-        if(v0.pos.z != v1.pos.z) color[3] += uchar((v1.color[3] - v0.color[3]) * (pos.z - v0.pos.z) / (v1.pos.z - v0.pos.z));
+        if(v0.pos.z != v1.pos.z) color.a += uchar((v1.color.a - v0.color.a) * (pos.z - v0.pos.z) / (v1.pos.z - v0.pos.z));
     }
 } *domeverts = NULL;
 static GLushort *domeindices = NULL;
diff --git a/src/engine/rendertext.cpp b/src/engine/rendertext.cpp
index 14b2214..d881842 100644
--- a/src/engine/rendertext.cpp
+++ b/src/engine/rendertext.cpp
@@ -1,9 +1,16 @@
 #include "engine.h"
 
-VAR(IDF_PERSIST, textblinking, 0, 350, VAR_MAX);
-FVARF(IDF_PERSIST, textscale, FVAR_NONZERO, 1, FVAR_MAX, UI::setup());
+VAR(IDF_PERSIST, textblinking, 0, 250, VAR_MAX);
+FVAR(IDF_PERSIST, textscale, FVAR_NONZERO, 1, FVAR_MAX);
 VAR(IDF_PERSIST, textfaded, 0, 1, 1);
 VAR(IDF_PERSIST, textminintensity, 0, 32, 255);
+VARF(IDF_PERSIST, textkeybg, 0, 1, 1, changedkeys = totalmillis);
+VARF(IDF_PERSIST, textkeyseps, 0, 1, 1, changedkeys = totalmillis);
+VAR(IDF_PERSIST|IDF_HEX, textkeybgcolour, 0x000000, 0xFFFFFF, 0xFFFFFF);
+VAR(IDF_PERSIST|IDF_HEX, textkeyfgcolour, 0x000000, 0x00FFFF, 0xFFFFFF);
+FVAR(IDF_PERSIST, textkeybgblend, 0, 0.25f, 1);
+FVAR(IDF_PERSIST, textkeyfgblend, 0, 1, 1);
+TVAR(IDF_PERSIST|IDF_PRELOAD, textkeybgtex, "textures/textkeybg", 3);
 
 static inline bool htcmp(const char *key, const font &f) { return !strcmp(key, f.name); }
 
@@ -21,6 +28,7 @@ void newfont(char *name, char *tex, int *defaultw, int *defaulth)
     f->texs.shrink(0);
     f->texs.add(textureload(tex));
     f->chars.shrink(0);
+
     f->charoffset = '!';
     f->maxw = f->defaultw = *defaultw;
     f->maxh = f->defaulth = f->scale = *defaulth;
@@ -92,7 +100,7 @@ font *loadfont(const char *name)
     font *f = fonts.access(name);
     if(!f)
     {
-        defformatstring(n)("fonts/%s.cfg", name);
+        defformatstring(n)("config/fonts/%s.cfg", name);
         if(execfile(n, false)) f = fonts.access(name);
     }
     return f;
@@ -171,7 +179,7 @@ COMMAND(0, tabify, "si");
 
 int draw_textf(const char *fstr, int left, int top, ...)
 {
-    defvformatstring(str, top, fstr);
+    defvformatbigstring(str, top, fstr);
     return draw_text(str, left, top);
 }
 
@@ -241,7 +249,8 @@ static void text_color(char c, cvec *stack, int size, int &sp, cvec &color, int
         }
         case 'S': // restore
         {
-            color = stack[sp > 0 ? --sp : sp];
+            if(sp > 0) --sp;
+            color = stack[sp];
             break;
         }
         default: color = stack[sp]; break; // everything else
@@ -250,7 +259,6 @@ static void text_color(char c, cvec *stack, int size, int &sp, cvec &color, int
     glColor4ub((uchar)color.r, (uchar)color.g, (uchar)color.b, (uchar)color.a);
 }
 
-#define FONTX int(curfont->maxh*textscale)
 static const char *gettexvar(const char *var)
 {
     ident *id = getident(var);
@@ -269,8 +277,8 @@ static float draw_icon(Texture *&tex, const char *name, float x, float y, float
     if(!name && !*name) return 0;
     const char *file = name;
     if(*file == '$') file = gettexvar(++file);
-    if(!*file) { conoutf("invalid texture: %s (%s)", name, file); return 0; }
-    Texture *t = textureload(file, 3);
+    if(!*file) return 0;
+    Texture *t = textureload(file, 3, true, false);
     if(!t) return 0;
     if(tex != t)
     {
@@ -278,7 +286,7 @@ static float draw_icon(Texture *&tex, const char *name, float x, float y, float
         tex = t;
         glBindTexture(GL_TEXTURE_2D, tex->id);
     }
-    float h = scale*FONTX, w = (t->w*h)/float(t->h);
+    float h = curfont->maxh*scale, w = (t->w*h)/float(t->h);
     varray::attrib<float>(x,     y    ); varray::attrib<float>(0, 0);
     varray::attrib<float>(x + w, y    ); varray::attrib<float>(1, 0);
     varray::attrib<float>(x + w, y + h); varray::attrib<float>(1, 1);
@@ -291,10 +299,10 @@ static float icon_width(const char *name, float scale)
     if(!name && !*name) return 0;
     const char *file = name;
     if(*file == '$') file = gettexvar(++file);
-    if(!*file) { conoutf("invalid texture: %s (%s)", name, file); return 0; }
-    Texture *t = textureload(file, 3);
+    if(!*file) return 0;
+    Texture *t = textureload(file, 3, true, false);
     if(!t) return 0;
-    return (t->w*scale*FONTX)/t->h;
+    return (t->w*curfont->maxh*scale)/t->h;
 }
 
 #define TEXTCOLORIZE(h,s) \
@@ -331,14 +339,31 @@ static float icon_width(const char *name, float scale)
         { \
             if(s && end > start) \
             { \
-                string value; \
-                copystring(value, start, min(size_t(end - start + 1), sizeof(value))); \
+                bigstring value; \
+                copybigstring(value, start, min(size_t(end - start + 1), sizeof(value))); \
                 TEXTICON(value); \
             } \
             h += end-start; \
         } \
         else break; \
     } \
+    else if(str[h] == '{') \
+    { \
+        h++; \
+        const char *start = &str[h]; \
+        const char *end = strchr(start, '}'); \
+        if(end) \
+        { \
+            if(s && end > start) \
+            { \
+                bigstring value; \
+                copybigstring(value, start, min(size_t(end - start + 1), sizeof(value))); \
+                TEXTKEY(value); \
+            } \
+            h += end-start; \
+        } \
+        else break; \
+    } \
     else if(s) TEXTCOLOR(h); \
 }
 #define TEXTALIGN \
@@ -355,7 +380,7 @@ static float icon_width(const char *name, float scale)
         int c = uchar(str[i]);\
         TEXTINDEX(i)\
         if(c == '\t')      { x = TEXTTAB(x); TEXTWHITE(i) }\
-        else if(c == ' ')  { x += scale*curfont->defaultw*textscale; TEXTWHITE(i) }\
+        else if(c == ' ')  { x += scale*curfont->defaultw; TEXTWHITE(i) }\
         else if(c == '\n') { TEXTLINE(i) TEXTALIGN }\
         else if(c == '\f') { if(str[i+1]) { i++; TEXTCOLORIZE(i, true); } }\
         else if(curfont->chars.inrange(c-curfont->charoffset))\
@@ -403,6 +428,7 @@ int text_visible(const char *str, float hitx, float hity, int maxwidth, int flag
     #define TEXTCOLOR(idx)
     #define TEXTHEXCOLOR(ret)
     #define TEXTICON(ret) x += icon_width(ret, scale);
+    #define TEXTKEY(ret) x += (textkeybg ? icon_width(textkeybgtex, scale)*0.6f : 0.f)+text_widthf(ret, flags);
     #define TEXTCHAR(idx) x += cw; TEXTWHITE(idx)
     #define TEXTWORD TEXTWORDSKELETON
     TEXTSKELETON
@@ -412,6 +438,7 @@ int text_visible(const char *str, float hitx, float hity, int maxwidth, int flag
     #undef TEXTCOLOR
     #undef TEXTHEXCOLOR
     #undef TEXTICON
+    #undef TEXTKEY
     #undef TEXTCHAR
     #undef TEXTWORD
     return i;
@@ -425,7 +452,8 @@ void text_posf(const char *str, int cursor, float &cx, float &cy, int maxwidth,
     #define TEXTLINE(idx)
     #define TEXTCOLOR(idx)
     #define TEXTHEXCOLOR(ret)
-    #define TEXTICON(ret) x += icon_width(ret, scale)+2;
+    #define TEXTICON(ret) x += icon_width(ret, scale);
+    #define TEXTKEY(ret) x += (textkeybg ? icon_width(textkeybgtex, scale)*0.6f : 0.f)+text_widthf(ret, flags);
     #define TEXTCHAR(idx) x += cw;
     #define TEXTWORD TEXTWORDSKELETON if(i >= cursor) break;
     cx = cy = 0;
@@ -437,6 +465,7 @@ void text_posf(const char *str, int cursor, float &cx, float &cy, int maxwidth,
     #undef TEXTCOLOR
     #undef TEXTHEXCOLOR
     #undef TEXTICON
+    #undef TEXTKEY
     #undef TEXTCHAR
     #undef TEXTWORD
 }
@@ -448,7 +477,8 @@ void text_boundsf(const char *str, float &width, float &height, int maxwidth, in
     #define TEXTLINE(idx) if(x > width) width = x;
     #define TEXTCOLOR(idx)
     #define TEXTHEXCOLOR(ret)
-    #define TEXTICON(ret) x += icon_width(ret, scale)+2;
+    #define TEXTICON(ret) x += icon_width(ret, scale);
+    #define TEXTKEY(ret) x += (textkeybg ? icon_width(textkeybgtex, scale)*0.6f : 0.f)+text_widthf(ret, flags);
     #define TEXTCHAR(idx) x += cw;
     #define TEXTWORD TEXTWORDSKELETON
     width = 0;
@@ -461,8 +491,88 @@ void text_boundsf(const char *str, float &width, float &height, int maxwidth, in
     #undef TEXTCOLOR
     #undef TEXTHEXCOLOR
     #undef TEXTICON
+    #undef TEXTKEY
+    #undef TEXTCHAR
+    #undef TEXTWORD
+}
+
+int draw_key(Texture *&tex, const char *str, float sx, float sy, float sc, cvec &cl, int flags)
+{
+    float swidth = text_widthf(str, flags), ss = 0, sp = 0;
+    if(textkeybg)
+    {
+        Texture *t = textureload(textkeybgtex, 3, true, false);
+        if(tex != t)
+        {
+            xtraverts += varray::end();
+            tex = t;
+            glBindTexture(GL_TEXTURE_2D, tex->id);
+        }
+
+        glColor4ub(uchar((textkeybgcolour>>16)&0xFF), uchar((textkeybgcolour>>8)&0xFF), uchar(textkeybgcolour&0xFF), uchar(textkeybgblend*cl.a));
+
+        float sh = curfont->maxh*sc, sw = (t->w*sh)/float(t->h), w1 = sw*0.25f, w2 = sw*0.5f, amt = swidth/w2;
+        int count = int(floorf(amt));
+        varray::attrib<float>(sx + ss,     sy    ); varray::attrib<float>(0, 0);
+        varray::attrib<float>(sx + ss + w1, sy    ); varray::attrib<float>(0.25f, 0);
+        varray::attrib<float>(sx + ss + w1, sy + sh); varray::attrib<float>(0.25f, 1);
+        varray::attrib<float>(sx + ss,     sy + sh); varray::attrib<float>(0, 1);
+        sp = (ss += w1);
+        loopi(count)
+        {
+            varray::attrib<float>(sx + ss,     sy    ); varray::attrib<float>(0.25f, 0);
+            varray::attrib<float>(sx + ss + w2, sy    ); varray::attrib<float>(0.75f, 0);
+            varray::attrib<float>(sx + ss + w2, sy + sh); varray::attrib<float>(0.75f, 1);
+            varray::attrib<float>(sx + ss,     sy + sh); varray::attrib<float>(0.25f, 1);
+            ss += w2;
+        }
+        float w3 = amt-float(count), w4 = w1 + w2*w3, w5 = 0.75f - 0.5f*w3;
+        varray::attrib<float>(sx + ss,     sy    ); varray::attrib<float>(w5, 0);
+        varray::attrib<float>(sx + ss + w4, sy    ); varray::attrib<float>(1, 0);
+        varray::attrib<float>(sx + ss + w4, sy + sh); varray::attrib<float>(1, 1);
+        varray::attrib<float>(sx + ss,     sy + sh); varray::attrib<float>(w5, 1);
+        ss += w4;
+    }
+    else ss = swidth;
+    xtraverts += varray::end();
+
+    #define TEXTINDEX(idx)
+    #define TEXTWHITE(idx)
+    #define TEXTLINE(idx) ly += FONTH;
+    #define TEXTCOLOR(idx) text_color(str[idx], colorstack, sizeof(colorstack), colorpos, color, r, g, b, fade);
+    #define TEXTHEXCOLOR(ret) \
+        if(usecolor) \
+        { \
+            int alpha = colorstack[colorpos].a; \
+            color = TVECA(ret, alpha); \
+            colorstack[colorpos] = color; \
+            xtraverts += varray::end(); \
+            glColor4ub((uchar)color.r, (uchar)color.g, (uchar)color.b, (char)color.a); \
+        }
+    #define TEXTICON(ret) x += draw_icon(tex, ret, left+x, top+y, scale);
+    #define TEXTKEY(ret) x += draw_key(tex, ret, left+x, top+y, scale, color, flags);
+    #define TEXTCHAR(idx) { draw_char(tex, c, left+x, top+y, scale); x += cw; }
+    #define TEXTWORD TEXTWORDSKELETON
+    bool usecolor = true;
+    int fade = textkeyfgblend*cl.a, r = (textkeyfgcolour>>16)&0xFF, g = (textkeyfgcolour>>8)&0xFF, b = textkeyfgcolour&0xFF,
+        colorpos = 1, ly = 0, left = sx + sp, top = sy, cursor = -1, maxwidth = -1;
+    cvec colorstack[16], color = TVECX(r, g, b, fade);
+    loopi(16) colorstack[i] = color;
+    glColor4ub((uchar)color.r, (uchar)color.g, (uchar)color.b, (uchar)color.a);
+    TEXTSKELETON
+    TEXTEND(cursor)
+    xtraverts += varray::end();
+
+    glColor4ub((uchar)cl.r, (uchar)cl.g, (uchar)cl.b, (uchar)cl.a);
+
+    #undef TEXTINDEX
+    #undef TEXTWHITE
+    #undef TEXTLINE
+    #undef TEXTCOLOR
+    #undef TEXTHEXCOLOR
     #undef TEXTCHAR
     #undef TEXTWORD
+    return ss;
 }
 
 int draw_text(const char *str, int rleft, int rtop, int r, int g, int b, int a, int flags, int cursor, int maxwidth)
@@ -491,6 +601,7 @@ int draw_text(const char *str, int rleft, int rtop, int r, int g, int b, int a,
             glColor4ub((uchar)color.r, (uchar)color.g, (uchar)color.b, (char)color.a); \
         }
     #define TEXTICON(ret) x += draw_icon(tex, ret, left+x, top+y, scale);
+    #define TEXTKEY(ret) x += draw_key(tex, ret, left+x, top+y, scale, color, flags);
     #define TEXTCHAR(idx) { draw_char(tex, c, left+x, top+y, scale); x += cw; }
     #define TEXTWORD TEXTWORDSKELETON
     glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -538,7 +649,7 @@ void reloadfonts()
 
 int draw_textx(const char *fstr, int left, int top, int r, int g, int b, int a, int flags, int cursor, int maxwidth, ...)
 {
-    defvformatstring(str, maxwidth, fstr);
+    defvformatbigstring(str, maxwidth, fstr);
 
     int width = 0, height = 0;
     text_bounds(str, width, height, maxwidth, flags);
@@ -548,7 +659,8 @@ int draw_textx(const char *fstr, int left, int top, int r, int g, int b, int a,
         case TEXT_RIGHT_JUSTIFY: left -= width; break;
         default: break;
     }
-    if(flags&TEXT_UPWARD) top -= height;
+    if(flags&TEXT_BALLOON) top -= height/2;
+    else if(flags&TEXT_UPWARD) top -= height;
     if(flags&TEXT_SHADOW) draw_text(str, left-2, top-2, 0, 0, 0, a, flags, cursor, maxwidth);
     return draw_text(str, left, top, r, g, b, a, flags, cursor, maxwidth);
 }
diff --git a/src/engine/renderva.cpp b/src/engine/renderva.cpp
index 2331f9f..5b0a13a 100644
--- a/src/engine/renderva.cpp
+++ b/src/engine/renderva.cpp
@@ -226,14 +226,44 @@ static inline bool insideva(const vtxarray *va, const vec &v, int margin = 2)
 ///////// occlusion queries /////////////
 
 #define MAXQUERY 2048
+#define MAXQUERYFRAMES 2
 
 struct queryframe
 {
     int cur, max;
     occludequery queries[MAXQUERY];
+
+    queryframe() : cur(0), max(0) {}
+
+    void flip() { loopi(cur) queries[i].owner = NULL; cur = 0; }
+
+    occludequery *newquery(void *owner)
+    {
+        if(cur >= max)
+        {
+            if(max >= MAXQUERY) return NULL;
+            glGenQueries_(1, &queries[max++].id);
+        }
+        occludequery *query = &queries[cur++];
+        query->owner = owner;
+        query->fragments = -1;
+        return query;
+    }
+
+    void reset() { loopi(max) queries[i].owner = NULL; }
+
+    void cleanup()
+    {
+        loopi(max)
+        {
+            glDeleteQueries_(1, &queries[i].id);
+            queries[i].owner = NULL;
+        }
+        cur = max = 0;
+    }
 };
 
-static queryframe queryframes[2] = {{0, 0}, {0, 0}};
+static queryframe queryframes[MAXQUERYFRAMES];
 static uint flipquery = 0;
 
 int getnumqueries()
@@ -243,43 +273,23 @@ int getnumqueries()
 
 void flipqueries()
 {
-    flipquery = (flipquery + 1) % 2;
-    queryframe &qf = queryframes[flipquery];
-    loopi(qf.cur) qf.queries[i].owner = NULL;
-    qf.cur = 0;
+    flipquery = (flipquery + 1) % MAXQUERYFRAMES;
+    queryframes[flipquery].flip();
 }
 
 occludequery *newquery(void *owner)
 {
-    queryframe &qf = queryframes[flipquery];
-    if(qf.cur >= qf.max)
-    {
-        if(qf.max >= MAXQUERY) return NULL;
-        glGenQueries_(1, &qf.queries[qf.max++].id);
-    }
-    occludequery *query = &qf.queries[qf.cur++];
-    query->owner = owner;
-    query->fragments = -1;
-    return query;
+    return queryframes[flipquery].newquery(owner);
 }
 
 void resetqueries()
 {
-    loopi(2) loopj(queryframes[i].max) queryframes[i].queries[j].owner = NULL;
+    loopi(MAXQUERYFRAMES) queryframes[i].reset();
 }
 
 void clearqueries()
 {
-    loopi(2)
-    {
-        queryframe &qf = queryframes[i];
-        loopj(qf.max)
-        {
-            glDeleteQueries_(1, &qf.queries[j].id);
-            qf.queries[j].owner = NULL;
-        }
-        qf.cur = qf.max = 0;
-    }
+    loopi(MAXQUERYFRAMES) queryframes[i].cleanup();
 }
 
 VAR(0, oqfrags, 0, 8, 64);
@@ -344,7 +354,7 @@ void findvisiblemms(const vector<extentity *> &ents)
         loopv(va->mapmodels)
         {
             octaentities *oe = va->mapmodels[i];
-            if(isfoggedcube(oe->o, oe->size) || pvsoccluded(oe->bbmin, ivec(oe->bbmax).sub(oe->bbmin))) continue;
+            if(isfoggedcube(oe->o, oe->size) || pvsoccluded(oe->bbmin, oe->bbmax)) continue;
 
             bool occluded = oe->query && oe->query->owner == oe && checkquery(oe->query);
             if(occluded)
@@ -361,8 +371,8 @@ void findvisiblemms(const vector<extentity *> &ents)
                 loopv(oe->mapmodels)
                 {
                     extentity &e = *ents[oe->mapmodels[i]];
-                    if(e.lastemit && e.spawned && e.attrs[6]&MMT_HIDE) continue;
-                    e.visible = true;
+                    if(e.lastemit && e.spawned() && e.attrs[6]&MMT_HIDE) continue;
+                    e.flags |= EF_RENDER;
                     ++visible;
                 }
                 if(!visible) continue;
@@ -386,8 +396,6 @@ void findvisiblemms(const vector<extentity *> &ents)
 
 VAR(0, oqmm, 0, 4, 8);
 
-extern bool getentboundingbox(extentity &e, ivec &o, ivec &r);
-
 VAR(0, mmanimoverride, -1, 0, ANIM_ALL);
 
 void rendermapmodel(extentity &e)
@@ -395,8 +403,8 @@ void rendermapmodel(extentity &e)
     int anim = ANIM_MAPMODEL|ANIM_LOOP, basetime = 0, flags = MDL_CULL_VFC|MDL_CULL_DIST|MDL_DYNLIGHT;
     if(e.lastemit)
     {
-        if(e.attrs[6]&MMT_HIDE && e.spawned) return;
-        anim = e.spawned ? ANIM_TRIGGER_ON : ANIM_TRIGGER_OFF;
+        if(e.attrs[6]&MMT_HIDE && e.spawned()) return;
+        anim = e.spawned() ? ANIM_TRIGGER_ON : ANIM_TRIGGER_OFF;
         if(e.lastemit > 0 && lastmillis-e.lastemit < entities::triggertime(e)) basetime = e.lastemit;
         else anim |= ANIM_END;
     }
@@ -451,8 +459,8 @@ void renderreflectedmapmodels()
         loopv(oe->mapmodels)
         {
            extentity &e = *ents[oe->mapmodels[i]];
-           if(e.visible || (e.lastemit && e.spawned && e.attrs[6]&MMT_HIDE)) continue;
-           e.visible = true;
+           if(e.flags&EF_RENDER || (e.lastemit && e.spawned() && e.attrs[6]&MMT_HIDE)) continue;
+           e.flags |= EF_RENDER;
         }
     }
     if(mms)
@@ -463,9 +471,9 @@ void renderreflectedmapmodels()
             loopv(oe->mapmodels)
             {
                 extentity &e = *ents[oe->mapmodels[i]];
-                if(!e.visible) continue;
+                if(!(e.flags&EF_RENDER)) continue;
                 rendermapmodel(e);
-                e.visible = false;
+                e.flags &= ~EF_RENDER;
             }
         }
         endmodelbatches();
@@ -490,7 +498,7 @@ void rendermapmodels()
         loopv(oe->mapmodels)
         {
             extentity &e = *ents[oe->mapmodels[i]];
-            if(!e.visible) continue;
+            if(!(e.flags&EF_RENDER)) continue;
             if(!rendered)
             {
                 rendered = true;
@@ -498,7 +506,7 @@ void rendermapmodels()
                 if(oe->query) startmodelquery(oe->query);
             }
             rendermapmodel(e);
-            e.visible = false;
+            e.flags &= ~EF_RENDER;
         }
         if(rendered && oe->query) endmodelquery();
     }
@@ -530,7 +538,7 @@ void rendermapmodels()
 static inline bool bbinsideva(const ivec &bo, const ivec &br, vtxarray *va)
 {
     return bo.x >= va->bbmin.x && bo.y >= va->bbmin.y && va->o.z >= va->bbmin.z &&
-        bo.x + br.x <= va->bbmax.x && bo.y + br.y <= va->bbmax.y && bo.z + br.z <= va->bbmax.z;
+        br.x <= va->bbmax.x && br.y <= va->bbmax.y && br.z <= va->bbmax.z;
 }
 
 static inline bool bboccluded(const ivec &bo, const ivec &br, cube *c, const ivec &o, int size)
@@ -551,7 +559,7 @@ static inline bool bboccluded(const ivec &bo, const ivec &br, cube *c, const ive
 
 bool bboccluded(const ivec &bo, const ivec &br)
 {
-    int diff = (bo.x^(bo.x+br.x)) | (bo.y^(bo.y+br.y)) | (bo.z^(bo.z+br.z));
+    int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z);
     if(diff&~((1<<worldscale)-1)) return false;
     int scale = worldscale-1;
     if(diff&(1<<scale)) return bboccluded(bo, br, worldroot, ivec(0, 0, 0), 1<<scale);
@@ -2011,8 +2019,8 @@ void cleanupva()
     loopi(NUMCAUSTICS) caustictex[i] = NULL;
 }
 
-VAR(IDF_WORLD, causticscale, 0, 50, 10000);
-VAR(IDF_WORLD, causticmillis, 0, 75, 1000);
+VAR(IDF_WORLD, causticscale, 1, 50, 10000);
+VAR(IDF_WORLD, causticmillis, 1, 75, 1000);
 VARF(IDF_PERSIST, caustics, 0, 1, 1, loadcaustics());
 
 void setupcaustics(int tmu, float blend, GLfloat *color = NULL)
diff --git a/src/engine/scale.h b/src/engine/scale.h
index 36722eb..2f415f9 100644
--- a/src/engine/scale.h
+++ b/src/engine/scale.h
@@ -2,13 +2,13 @@ static void FUNCNAME(halvetexture)(uchar *src, uint sw, uint sh, uint stride, uc
 {
     for(uchar *yend = &src[sh*stride]; src < yend;)
     {
-        for(uchar *xend = &src[stride]; src < xend; src += 2*BPP, dst += BPP)
+        for(uchar *xend = &src[sw*BPP], *xsrc = src; xsrc < xend; xsrc += 2*BPP, dst += BPP)
         {
-            #define OP(c, n) dst[n] = (uint(src[n]) + uint(src[n+BPP]) + uint(src[stride+n]) + uint(src[stride+n+BPP]))>>2
+            #define OP(c, n) dst[n] = (uint(xsrc[n]) + uint(xsrc[n+BPP]) + uint(xsrc[stride+n]) + uint(xsrc[stride+n+BPP]))>>2
             PIXELOP
             #undef OP
         }
-        src += stride;
+        src += 2*stride;
     }
 }
 
@@ -20,12 +20,12 @@ static void FUNCNAME(shifttexture)(uchar *src, uint sw, uint sh, uint stride, uc
     uint tshift = wshift + hshift;
     for(uchar *yend = &src[sh*stride]; src < yend;)
     {
-        for(uchar *xend = &src[stride]; src < xend; src += wfrac*BPP, dst += BPP)
+        for(uchar *xend = &src[sw*BPP], *xsrc = src; xsrc < xend; xsrc += wfrac*BPP, dst += BPP)
         {        
             #define OP(c, n) c##t = 0
             DEFPIXEL
             #undef OP
-            for(uchar *ycur = src, *xend = &ycur[wfrac*BPP], *yend = &src[hfrac*stride]; 
+            for(uchar *ycur = xsrc, *xend = &ycur[wfrac*BPP], *yend = &src[hfrac*stride]; 
                 ycur < yend; 
                 ycur += stride, xend += stride)
             {
@@ -40,7 +40,7 @@ static void FUNCNAME(shifttexture)(uchar *src, uint sw, uint sh, uint stride, uc
             PIXELOP
             #undef OP
         }
-        src += (hfrac-1)*stride; 
+        src += hfrac*stride; 
     }
 }
 
diff --git a/src/engine/server.cpp b/src/engine/server.cpp
index 57a2a8b..6a558f4 100644
--- a/src/engine/server.cpp
+++ b/src/engine/server.cpp
@@ -6,27 +6,49 @@
 #include <shlobj.h>
 #endif
 
-bool versioning = false;
-VAR(0, version, 1, 0, -1);
-VAR(0, versionmajor, 0, 0, VAR_MAX);
-VAR(0, versionminor, 0, 0, VAR_MAX);
-VAR(0, versionpatch, 0, 0, VAR_MAX);
-SVAR(0, versionstring, "0.0");
-SVAR(0, versionname, "");
-SVAR(0, versionuname, "");
-SVAR(0, versionrelease, "");
-SVAR(0, versionurl, "");
+const char *platnames[MAX_PLATFORMS] = {
+    "win", "osx", "nix"
+}, *platlongnames[MAX_PLATFORMS] = {
+    "windows", "macosx", "linux/bsd"
+};
+
+VAR(0, versioning, 1, 0, -1);
+
+VAR(IDF_READONLY, version, 1, CUR_VERSION, -1);
+VAR(IDF_READONLY, versionmajor, 1, VERSION_MAJOR, -1);
+VAR(IDF_READONLY, versionminor, 1, VERSION_MINOR, -1);
+VAR(IDF_READONLY, versionpatch, 1, VERSION_PATCH, -1);
+SVAR(IDF_READONLY, versionstring, VERSION_STRING);
+SVAR(IDF_READONLY, versionname, VERSION_NAME);
+SVAR(IDF_READONLY, versionuname, VERSION_UNAME);
+SVAR(IDF_READONLY, versionrelease, VERSION_RELEASE);
+SVAR(IDF_READONLY, versionurl, VERSION_URL);
+SVAR(IDF_READONLY, versioncopy, VERSION_COPY);
+SVAR(IDF_READONLY, versiondesc, VERSION_DESC);
+SVAR(IDF_READONLY, versionplatname, plat_name(CUR_PLATFORM));
+SVAR(IDF_READONLY, versionplatlongname, plat_longname(CUR_PLATFORM));
+VAR(IDF_READONLY, versionplatform, 0, CUR_PLATFORM, VAR_MAX);
+VAR(IDF_READONLY, versionarch, 0, CUR_ARCH, VAR_MAX);
+#ifdef STANDALONE
+VAR(IDF_READONLY, versionisserver, 0, 1, 1);
+#else
+VAR(IDF_READONLY, versionisserver, 0, 0, 1);
+#endif
+uint versioncrc = 0;
+ICOMMAND(0, platname, "ii", (int *p, int *g), result(*p >= 0 && *p < MAX_PLATFORMS ? (*g!=0 ? plat_longname(*p) : plat_name(*p)) : ""));
 
 VAR(0, rehashing, 1, 0, -1);
 
-extern const char * const disc_reasons[] = { "normal", "end of packet", "client num", "user was kicked", "message error", "address is banned", "server is in private mode", "server is full", "connection timed out", "packet overflow", "server shutting down" };
+const char * const disc_reasons[] = { "normal", "end of packet", "client num", "user was kicked", "message error", "address is banned", "server is in private mode", "server is password protected", "server requires pure official builds", "server is at maximum capacity", "server and client are incompatible", "connection timed out", "packet overflow", "server shutting down" };
 
-SVAR(IDF_PERSIST, logtimeformat, "%c");
+SVAR(IDF_PERSIST, logtimeformat, "%Y-%m-%d %H:%M.%S");
 SVAR(IDF_PERSIST, filetimeformat, "%Y%m%d%H%M%S");
+VAR(IDF_PERSIST, filetimelocal, 0, 1, 1); // use clockoffset to localise
+
 const char *gettime(time_t ctime, const char *format)
 {
     static string buf;
-    if(!ctime) ctime = clocktime;
+    if(!ctime) ctime = currenttime;
     struct tm *t = localtime(&ctime);
     if(!strftime(buf, sizeof(buf), format && *format ? format : logtimeformat, t)) buf[0] = '\0';
     return buf;
@@ -37,13 +59,12 @@ const char *timestr(int dur, int style)
 {
     static string buf; buf[0] = 0;
     int tm = dur, ms = 0, ss = 0, mn = 0;
-    if(style < 2 && tm > 0)
+    if(tm > 0)
     {
         ms = tm%1000;
         tm = (tm-ms)/1000;
     }
-    if(style < 0 && tm > 0) ss = tm;
-    else if(style < 4 && tm > 0)
+    if(style > 0 && tm > 0)
     {
         ss = tm%60;
         tm = (tm-ss)/60;
@@ -51,11 +72,11 @@ const char *timestr(int dur, int style)
     }
     switch(style)
     {
-        case -1: formatstring(buf)("%d.%d", ss, ms/100); break;
-        case 0: formatstring(buf)("%d:%02d.%03d", mn, ss, ms); break;
-        case 1: formatstring(buf)("%d:%02d.%d", mn, ss, ms/100); break;
-        case 2: formatstring(buf)("%d:%02d", mn, ss); break;
-        case 3:
+        case 0: formatstring(buf)("%d.%d", tm, ms/100); break;
+        case 1: formatstring(buf)("%d:%02d.%03d", mn, ss, ms); break;
+        case 2: formatstring(buf)("%d:%02d.%d", mn, ss, ms/100); break;
+        case 3: formatstring(buf)("%d:%02d", mn, ss); break;
+        case 4:
         {
             if(mn > 0)
             {
@@ -71,7 +92,7 @@ const char *timestr(int dur, int style)
 ICOMMAND(0, timestr, "ii", (int *d, int *s), result(timestr(*d, *s)));
 
 vector<ipinfo> control;
-void addipinfo(vector<ipinfo> &info, int type, const char *name)
+void addipinfo(vector<ipinfo> &info, int type, const char *name, const char *reason)
 {
     union { uchar b[sizeof(enet_uint32)]; enet_uint32 i; } ip, mask;
     ip.i = 0;
@@ -94,12 +115,13 @@ void addipinfo(vector<ipinfo> &info, int type, const char *name)
 #ifdef STANDALONE
     p.version = nextcontrolversion();
 #endif
+    if(reason && *reason) p.reason = newstring(reason);
     server::updatecontrols = true;
 }
-ICOMMAND(0, addallow, "s", (char *name), addipinfo(control, ipinfo::ALLOW, name));
-ICOMMAND(0, addban, "s", (char *name), addipinfo(control, ipinfo::BAN, name));
-ICOMMAND(0, addmute, "s", (char *name), addipinfo(control, ipinfo::MUTE, name));
-ICOMMAND(0, addlimit, "s", (char *name), addipinfo(control, ipinfo::LIMIT, name));
+ICOMMAND(0, addallow, "ss", (char *name, char *reason), addipinfo(control, ipinfo::ALLOW, name, reason));
+ICOMMAND(0, addban, "ss", (char *name, char *reason), addipinfo(control, ipinfo::BAN, name, reason));
+ICOMMAND(0, addmute, "ss", (char *name, char *reason), addipinfo(control, ipinfo::MUTE, name, reason));
+ICOMMAND(0, addlimit, "ss", (char *name, char *reason), addipinfo(control, ipinfo::LIMIT, name, reason));
 
 const char *ipinfotypes[ipinfo::MAXTYPES] = { "allow", "ban", "mute", "limit" };
 char *printipinfo(const ipinfo &info, char *buf)
@@ -120,10 +142,10 @@ char *printipinfo(const ipinfo &info, char *buf)
     return str;
 }
 
-bool checkipinfo(vector<ipinfo> &info, int type, enet_uint32 ip)
+ipinfo *checkipinfo(vector<ipinfo> &info, int type, enet_uint32 ip)
 {
-    loopv(info) if(info[i].type == type && (ip & info[i].mask) == info[i].ip) return true;
-    return false;
+    loopv(info) if(info[i].type == type && (ip & info[i].mask) == info[i].ip) return &info[i];
+    return NULL;
 }
 
 #define LOGSTRLEN 512
@@ -171,10 +193,10 @@ void logoutf(const char *fmt, ...)
 
 void console(int type, const char *s, ...)
 {
-    defvformatstring(sf, s, s);
-    string osf;
-    filtertext(osf, sf);
-    if(*logtimeformat) logoutf("%s %s", gettime(clocktime, logtimeformat), osf);
+    defvformatbigstring(sf, s, s);
+    bigstring osf;
+    filterbigstring(osf, sf);
+    if(*logtimeformat) logoutf("%s %s", gettime(currenttime, logtimeformat), osf);
     else logoutf("%s", osf);
 #ifndef STANDALONE
     conline(type, sf, 0);
@@ -183,14 +205,14 @@ void console(int type, const char *s, ...)
 
 void conoutft(int type, const char *s, ...)
 {
-    defvformatstring(sf, s, s);
+    defvformatbigstring(sf, s, s);
     console(type, "%s", sf);
     ircoutf(5, "%s", sf);
 }
 
 void conoutf(const char *s, ...)
 {
-    defvformatstring(sf, s, s);
+    defvformatbigstring(sf, s, s);
     conoutft(0, "%s", sf);
 }
 
@@ -198,11 +220,11 @@ VAR(0, verbose, 0, 0, 6);
 
 static void writelog(FILE *file, const char *buf)
 {
-    static uchar ubuf[512];
-    int len = strlen(buf), carry = 0;
+    static uchar ubuf[LOGSTRLEN];
+    size_t len = strlen(buf), carry = 0;
     while(carry < len)
     {
-        int numu = encodeutf8(ubuf, sizeof(ubuf)-1, &((const uchar *)buf)[carry], len - carry, &carry);
+        size_t numu = encodeutf8(ubuf, sizeof(ubuf)-1, &((const uchar *)buf)[carry], len - carry, &carry);
         if(carry >= len) ubuf[numu++] = '\n';
         fwrite(ubuf, 1, numu, file);
     }
@@ -218,28 +240,65 @@ static void writelogv(FILE *file, const char *fmt, va_list args)
 #ifdef STANDALONE
 void localservertoclient(int chan, ENetPacket *packet) {}
 VAR(0, servertype, 1, 3, 3); // 1: private, 2: public, 3: dedicated
+VAR(0, standalone, 1, 1, -1);
 #else
 VARF(0, servertype, 0, 0, 3, changeservertype()); // 0: local only, 1: private, 2: public, 3: dedicated
+VAR(0, standalone, 1, 0, -1);
 #endif
 VAR(0, serveruprate, 0, 0, VAR_MAX);
 VAR(0, serverport, 1, SERVER_PORT, VAR_MAX);
+VAR(0, serverlanport, 0, LAN_PORT, VAR_MAX);
 SVAR(0, serverip, "");
 
 int curtime = 0, totalmillis = 1, lastmillis = 1, timescale = 100, paused = 0, timeerr = 0;
-time_t clocktime = 0;
+time_t clocktime = 0, currenttime = 0, clockoffset = 0;
 uint totalsecs = 0;
 const char *load = NULL;
 vector<char *> gameargs;
 
-bool filtertext(char *dst, const char *src, bool newline, bool colour, bool whitespace, int len)
+bool filterword(char *src, const char *list)
+{
+    bool filtered = false;
+    int len = listlen(list);
+    loopi(len)
+    {
+        char *word = indexlist(list, i);
+        if(word)
+        {
+            if(*word)
+            {
+                char *t = src;
+                int count = strlen(word);
+                while((t = strstr(t, word)) != NULL)
+                {
+                    loopj(count) if(*t) *t++ = '*';
+                    filtered = true;
+                }
+            }
+            delete[] word;
+        }
+    }
+    return filtered;
+}
+
+ICOMMAND(0, filterword, "ss", (char *s, char *t),
+{
+    char *d = newstring(s);
+    filterstring(d, d, t);
+    stringret(d);
+});
+
+
+bool filterstring(char *dst, const char *src, bool newline, bool colour, bool whitespace, bool wsstrip, size_t len)
 {
     bool filtered = false;
-    for(int c = uchar(*src); c; c = uchar(*++src))
+    size_t n = 0;
+    for(int c = uchar(*src); c && n < len; c = uchar(*++src))
     {
         if(newline && (c=='\n' || c=='\r')) c = ' ';
         if(c=='\f')
         {
-            if(!colour) *dst++ = c;
+            if(!colour) dst[n++] = c;
             else
             {
                 filtered = true;
@@ -251,29 +310,32 @@ bool filtertext(char *dst, const char *src, bool newline, bool colour, bool whit
                     if(c) c = *++src;
                     if(!c) break;
                 }
-                else if(c == '[' || c == '(')
+                else if(c == '[' || c == '(' || c == '{')
                 {
-                    const char *end = strchr(src, c == '[' ? ']' : ')');
+                    const char *end = strchr(src, c == '[' ? ']' : (c == '(' ? ')' : '}'));
                     src += end ? end-src : strlen(src);
                 }
 
             }
             continue;
         }
-        if(iscubeprint(c) || (iscubespace(c) && whitespace))
-        {
-            *dst++ = c;
-            if(!--len) break;
-        }
+        if(iscubeprint(c) || (iscubespace(c) && whitespace && (!wsstrip || n)))
+            dst[n++] = c;
         else filtered = true;
     }
-    *dst = '\0';
+    if(whitespace && wsstrip && n) while(iscubespace(dst[n-1])) dst[--n] = 0;
+    dst[n < len ? n : len-1] = 0;
     return filtered;
 }
-ICOMMAND(0, filter, "siiiN", (char *s, int *a, int *b, int *c, int *numargs),
+bool filterbigstring(char *dst, const char *src, bool newline, bool colour, bool whitespace, bool wsstrip, size_t len)
 {
-    char *d = newstring(s);
-    filtertext(d, s, *numargs >= 2 ? *a>0 : true, *numargs >= 3 ? *b>0 : true, *numargs >= 4 ? *c>0 : true);
+    return filterstring(dst, src, newline, colour, whitespace, wsstrip, len);
+}
+ICOMMAND(0, filter, "siiiiN", (char *s, int *a, int *b, int *c, int *d, int *numargs),
+{
+    size_t len = strlen(s);
+    char *d = newstring(len);
+    filterstring(d, s, *numargs >= 2 ? *a>0 : true, *numargs >= 3 ? *b>0 : true, *numargs >= 4 ? *c>0 : true, *numargs >= 5 ? *d>0 : false, len);
     stringret(d);
 });
 
@@ -359,15 +421,17 @@ void process(ENetPacket *packet, int sender, int chan);
 int getservermtu() { return serverhost ? serverhost->mtu : -1; }
 void *getinfo(int i)            { return !clients.inrange(i) || clients[i]->type==ST_EMPTY ? NULL : clients[i]->info; }
 const char *gethostname(int i)  { int o = server::peerowner(i); return !clients.inrange(o) || clients[o]->type==ST_EMPTY ? "unknown" : clients[o]->hostname; }
+const char *gethostip(int i)    { int o = server::peerowner(i); return !clients.inrange(o) || clients[o]->type==ST_EMPTY ? "0.0.0.0" : clients[o]->hostip; }
 int getnumclients()             { return clients.length(); }
 uint getclientip(int n)         { int o = server::peerowner(n); return clients.inrange(o) && clients[o]->type==ST_TCPIP ? clients[o]->peer->address.host : 0; }
 
 void sendpacket(int n, int chan, ENetPacket *packet, int exclude)
 {
+    if(n < 0 || chan < 0) server::recordpacket(abs(chan), packet->data, packet->dataLength);
+    if(chan < 0) return; // was just a record packet
     if(n < 0)
     {
-        server::recordpacket(abs(chan), packet->data, packet->dataLength);
-        if(chan >= 0) loopv(clients) if(i != server::peerowner(exclude) && server::allowbroadcast(i)) sendpacket(i, chan, packet, exclude);
+        loopv(clients) if(i != server::peerowner(exclude) && server::allowbroadcast(i)) sendpacket(i, chan, packet, exclude);
         return;
     }
     switch(clients[n]->type)
@@ -376,10 +440,7 @@ void sendpacket(int n, int chan, ENetPacket *packet, int exclude)
         {
             int owner = server::peerowner(n);
             if(owner >= 0 && clients.inrange(owner) && owner != n && owner != server::peerowner(exclude))
-            {
-                //conoutf("redirect %d packet to %d [%d:%d]", n, owner, exclude, server::peerowner(exclude));
                 sendpacket(owner, chan, packet, exclude);
-            }
             break;
         }
         case ST_TCPIP:
@@ -486,10 +547,15 @@ void sendfile(int cn, int chan, stream *file, const char *format, ...)
 
 void disconnect_client(int n, int reason)
 {
-    if(clients[n]->type!=ST_TCPIP) return;
-    enet_peer_disconnect(clients[n]->peer, reason);
-    server::clientdisconnect(n, false, reason);
+    if(clients[n]->type==ST_TCPIP) enet_peer_disconnect(clients[n]->peer, reason);
+    server::clientdisconnect(n, clients[n]->type==ST_LOCAL, reason);
     delclient(n);
+    if(clients[n]->type==ST_LOCAL) loopv(clients) if(i != n && clients[i] && clients[i]->type==ST_LOCAL)
+    {
+        clientdata &c = *clients[i];
+        server::clientdisconnect(c.num, true);
+        delclient(c.num);
+    }
 }
 
 void kicknonlocalclients(int reason)
@@ -524,6 +590,7 @@ void localconnect(bool force)
         clientdata &c = *clients[cn];
         c.peer = NULL;
         copystring(c.hostname, "localhost");
+        copystring(c.hostip, "127.0.0.1");
         conoutf("\fglocal client %d connected", c.num);
         client::gameconnect(false);
         server::clientconnect(c.num, 0, true);
@@ -547,17 +614,16 @@ bool resolverwait(const char *name, ENetAddress *address)
     return enet_address_set_host(address, name) >= 0;
 }
 
-int connectwithtimeout(ENetSocket sock, const char *hostname, ENetAddress &remoteaddress)
+int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &remoteaddress)
 {
-    int result = enet_socket_connect(sock, &remoteaddress);
-    if(result<0) enet_socket_destroy(sock);
-    return result;
+    return enet_socket_connect(sock, &remoteaddress);
 }
 #endif
 
 static ENetSocket mastersock = ENET_SOCKET_NULL;
 ENetAddress masteraddress = { ENET_HOST_ANY, ENET_PORT_ANY };
 static ENetAddress serveraddress = { ENET_HOST_ANY, ENET_PORT_ANY };
+static int masterconnecting = 0, masterconnected = 0;
 static vector<char> masterout, masterin;
 static int masteroutpos = 0, masterinpos = 0;
 
@@ -565,10 +631,10 @@ void disconnectmaster()
 {
     if(mastersock != ENET_SOCKET_NULL)
     {
-        server::disconnectedmaster();
+        server::masterdisconnected();
         enet_socket_destroy(mastersock);
         mastersock = ENET_SOCKET_NULL;
-        if(servertype >= 2) conoutf("disconnected from master server");
+        if(servertype >= 2 && masterconnected) conoutf("disconnected from master server");
     }
 
     masterout.setsize(0);
@@ -577,6 +643,7 @@ void disconnectmaster()
 
     masteraddress.host = ENET_HOST_ANY;
     masteraddress.port = ENET_PORT_ANY;
+    masterconnecting = masterconnected = 0;
 }
 
 VARF(0, servermasterport, 1, MASTER_PORT, INT_MAX-1, disconnectmaster());
@@ -586,7 +653,6 @@ ENetSocket connectmaster(bool reuse)
 {
     if(reuse && mastersock != ENET_SOCKET_NULL) return mastersock;
     if(!servermaster[0]) return ENET_SOCKET_NULL;
-
     if(masteraddress.host == ENET_HOST_ANY)
     {
         if(servertype >= 2) conoutf("\falooking up %s:[%d]...", servermaster, servermasterport);
@@ -598,20 +664,28 @@ ENetSocket connectmaster(bool reuse)
         }
     }
     ENetSocket sock = enet_socket_create(ENET_SOCKET_TYPE_STREAM);
-    if(sock != ENET_SOCKET_NULL && serveraddress.host != ENET_HOST_ANY && enet_socket_bind(sock, &serveraddress) < 0)
+    if(sock == ENET_SOCKET_NULL)
     {
-        enet_socket_destroy(sock);
-        sock = ENET_SOCKET_NULL;
+        conoutf("\frcould not open master server socket");
+        return ENET_SOCKET_NULL;
     }
-    if(sock == ENET_SOCKET_NULL || connectwithtimeout(sock, servermaster, masteraddress) < 0)
+    if(!reuse || serveraddress.host == ENET_HOST_ANY || !enet_socket_bind(sock, &serveraddress))
     {
-        conoutf(sock==ENET_SOCKET_NULL ? "\frcould not open socket to connect to master server" : "\frcould not connect to master server");
-        return ENET_SOCKET_NULL;
+        enet_socket_set_option(sock, ENET_SOCKOPT_NONBLOCK, 1);
+        if(!reuse)
+        {
+            if(!connectwithtimeout(sock, servermaster, masteraddress)) return sock;
+        }
+        else if(!enet_socket_connect(sock, &masteraddress))
+        {
+            masterconnecting = totalmillis ? totalmillis : 1;
+            mastersock = sock;
+            return sock;
+        }
     }
-
-    enet_socket_set_option(sock, ENET_SOCKOPT_NONBLOCK, 1);
-    if(reuse) mastersock = sock;
-    return sock;
+    enet_socket_destroy(sock);
+    conoutf("\frcould not connect to master server");
+    return ENET_SOCKET_NULL;
 }
 
 bool connectedmaster() { return mastersock != ENET_SOCKET_NULL; }
@@ -624,6 +698,8 @@ bool requestmaster(const char *req)
     //    mastersock = connectmaster();
         if(mastersock == ENET_SOCKET_NULL) return false;
     //}
+    if(masterout.length() >= 4096) return false;
+
     masterout.put(req, strlen(req));
     return true;
 }
@@ -664,7 +740,12 @@ void processmasterinput()
 
 void flushmasteroutput()
 {
-    if(masterout.empty()) return;
+    if(masterconnecting && totalmillis - masterconnecting >= 60000)
+    {
+        conoutf("\frcould not connect to master server");
+        disconnectmaster();
+    }
+    if(masterout.empty() || !masterconnected) return;
 
     ENetBuffer buf;
     buf.data = &masterout[masteroutpos];
@@ -709,25 +790,26 @@ void sendqueryreply(ucharbuf &p)
     enet_socket_send(pongsock, &pongaddr, &buf, 1);
 }
 
+#define MAXPINGDATA 32
+
 void checkserversockets()        // reply all server info requests
 {
-    static ENetSocketSet sockset;
+    static ENetSocketSet readset, writeset;
+    ENET_SOCKETSET_EMPTY(readset);
+    ENET_SOCKETSET_EMPTY(writeset);
     ENetSocket maxsock = ENET_SOCKET_NULL;
-#define CHECKSOCKET(sock) \
+#define ADDSOCKET(sock, write) \
     if(sock != ENET_SOCKET_NULL) \
     { \
-        if(maxsock == ENET_SOCKET_NULL) \
-        { \
-            ENET_SOCKETSET_EMPTY(sockset); \
-            maxsock = sock; \
-        }  \
-        else maxsock = max(maxsock, sock); \
-        ENET_SOCKETSET_ADD(sockset, sock); \
-    }
-    CHECKSOCKET(pongsock);
-    CHECKSOCKET(mastersock);
-    CHECKSOCKET(lansock);
-    if(maxsock == ENET_SOCKET_NULL || enet_socketset_select(maxsock, &sockset, NULL, 0) <= 0) return;
+        maxsock = maxsock == ENET_SOCKET_NULL ? sock : max(maxsock, sock); \
+        ENET_SOCKETSET_ADD(readset, sock); \
+        if(write) ENET_SOCKETSET_ADD(writeset, sock); \
+    }
+    ADDSOCKET(pongsock, false);
+    ADDSOCKET(mastersock, true);
+    ADDSOCKET(lansock, false);
+    bool ircsocks = ircaddsockets(maxsock, readset, writeset);
+    if(maxsock == ENET_SOCKET_NULL || enet_socketset_select(maxsock, &readset, &writeset, 0) <= 0) return;
 
     if(serverhost)
     {
@@ -736,18 +818,38 @@ void checkserversockets()        // reply all server info requests
         loopi(2)
         {
             ENetSocket sock = i ? lansock : pongsock;
-            if(sock == ENET_SOCKET_NULL || !ENET_SOCKETSET_CHECK(sockset, sock)) continue;
+            if(sock == ENET_SOCKET_NULL || !ENET_SOCKETSET_CHECK(readset, sock)) continue;
 
             buf.data = pong;
             buf.dataLength = sizeof(pong);
             int len = enet_socket_receive(sock, &pongaddr, &buf, 1);
-            if(len < 0) return;
+            if(len < 0 || len > MAXPINGDATA) continue;
             ucharbuf req(pong, len), p(pong, sizeof(pong));
             p.len += len;
             server::queryreply(req, p);
         }
     }
-    if(mastersock != ENET_SOCKET_NULL && ENET_SOCKETSET_CHECK(sockset, mastersock)) flushmasterinput();
+
+    if(mastersock != ENET_SOCKET_NULL)
+    {
+        if(!masterconnected)
+        {
+            if(ENET_SOCKETSET_CHECK(readset, mastersock) || ENET_SOCKETSET_CHECK(writeset, mastersock))
+            {
+                int error = 0;
+                if(enet_socket_get_option(mastersock, ENET_SOCKOPT_ERROR, &error) < 0 || error) disconnectmaster();
+                else
+                {
+                    masterconnecting = 0;
+                    masterconnected = totalmillis ? totalmillis : 1;
+                    server::masterconnected();
+                }
+            }
+        }
+        if(mastersock != ENET_SOCKET_NULL && ENET_SOCKETSET_CHECK(readset, mastersock)) flushmasterinput();
+    }
+
+    if(ircsocks) ircchecksockets(readset, writeset);
 }
 
 void serverslice(uint timeout)  // main server update, called from main loop in sp, or from below in dedicated server
@@ -788,8 +890,22 @@ void serverslice(uint timeout)  // main server update, called from main loop in
                 clientdata &c = *clients[cn];
                 c.peer = event.peer;
                 c.peer->data = &c;
-                static string hostn = "";
-                copystring(c.hostname, (enet_address_get_host_ip(&c.peer->address, hostn, sizeof(hostn))==0) ? hostn : "unknown");
+                if(enet_address_get_host_ip(&c.peer->address, c.hostip, sizeof(c.hostip)) >= 0)
+                {
+                    if(enet_address_get_host(&c.peer->address, c.hostname, sizeof(c.hostname)) >= 0)
+                    {
+                        ENetAddress address;
+                        string hostname;
+                        if(enet_address_set_host(&address, c.hostname) < 0 || enet_address_get_host_ip(&address, hostname, sizeof(hostname)) < 0 || strcmp(hostname, c.hostname))
+                            copystring(c.hostname, c.hostip);
+                    }
+                    else copystring(c.hostname, c.hostip);
+                }
+                else
+                {
+                    copystring(c.hostname, "unknown");
+                    copystring(c.hostip, "0.0.0.0");
+                }
                 int reason = server::clientconnect(c.num, c.peer->address.host);
                 if(reason) disconnect_client(c.num, reason);
                 break;
@@ -842,7 +958,10 @@ int getclockmillis()
 
 int updatetimer(bool limit)
 {
-    clocktime = time(NULL);
+    currenttime = time(NULL);
+    clocktime = mktime(gmtime(&currenttime));
+    clockoffset = currenttime-clocktime;
+
     int millis = getclockmillis();
 #ifndef STANDALONE
     if(limit) limitfps(millis, totalmillis);
@@ -889,7 +1008,7 @@ static HMENU appmenu = NULL;
 static HANDLE outhandle = NULL;
 static const int MAXLOGLINES = 200;
 struct logline { int len; char buf[LOGSTRLEN]; };
-static ringbuf<logline, MAXLOGLINES> loglines;
+static queue<logline, MAXLOGLINES> loglines;
 
 static void cleanupsystemtray()
 {
@@ -963,10 +1082,10 @@ static BOOL WINAPI consolehandler(DWORD dwCtrlType)
 static void writeline(logline &line)
 {
     static uchar ubuf[512];
-    int len = strlen(line.buf), carry = 0;
+    size_t len = strlen(line.buf), carry = 0;
     while(carry < len)
     {
-        int numu = encodeutf8(ubuf, sizeof(ubuf), &((uchar *)line.buf)[carry], len - carry, &carry);
+        size_t numu = encodeutf8(ubuf, sizeof(ubuf), &((uchar *)line.buf)[carry], len - carry, &carry);
         DWORD written = 0;
         WriteConsole(outhandle, ubuf, numu, &written, NULL);
     }
@@ -1082,6 +1201,7 @@ static void setupwindow(const char *title)
     atexit(cleanupwindow);
 
     if(!setupsystemtray(WM_APP)) fatal("failed adding to system tray");
+    conoutf("identity: v%s-%s%d %s (%s) [0x%x]", VERSION_STRING, versionplatname, versionarch, versionisserver ? "server" : "client", VERSION_RELEASE, versioncrc);
 }
 
 static char *parsecommandline(const char *src, vector<char *> &args)
@@ -1151,7 +1271,7 @@ void logoutfv(const char *fmt, va_list args)
 void serverloop()
 {
 #ifdef WIN32
-    defformatstring(cap)("%s server", versionname);
+    defformatstring(cap)("%s server", VERSION_NAME);
     setupwindow(cap);
     SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
 #endif
@@ -1180,6 +1300,13 @@ void serverloop()
     exit(EXIT_SUCCESS);
 }
 
+void limitdupclients()
+{
+    if(!serverhost) return;
+    int dupclients = server::dupclients();
+    serverhost->duplicatePeers = dupclients ? dupclients : serverhost->peerCount;
+}
+
 int setupserversockets()
 {
     if(!servertype || (serverhost && pongsock != ENET_SOCKET_NULL)) return servertype;
@@ -1187,7 +1314,11 @@ int setupserversockets()
     ENetAddress address = { ENET_HOST_ANY, enet_uint16(serverport) };
     if(*serverip)
     {
-        if(enet_address_set_host(&address, serverip) < 0) conoutf("\frWARNING: server address not resolved");
+        if(enet_address_set_host(&address, serverip) < 0)
+        {
+            setsvar("serverip", "");
+            conoutf("\frWARNING: server address not resolved");
+        }
         else serveraddress.host = address.host;
     }
 
@@ -1196,13 +1327,15 @@ int setupserversockets()
         serverhost = enet_host_create(&address, server::reserveclients(), server::numchannels(), 0, serveruprate);
         if(!serverhost)
         {
-            conoutf("\frcould not create server socket");
-#ifndef STANDALONE
+#ifdef STANDALONE
+            fatal("could not create server socket on port %d", serverport);
+#else
+            conoutf("\frcould not create server socket on port %d", serverport);
             setvar("servertype", 0);
 #endif
             return servertype;
         }
-        loopi(server::reserveclients()) serverhost->peers[i].data = NULL;
+        limitdupclients();
     }
 
     if(pongsock == ENET_SOCKET_NULL)
@@ -1216,23 +1349,27 @@ int setupserversockets()
         }
         if(pongsock == ENET_SOCKET_NULL)
         {
-            conoutf("\frcould not create server info socket, publicity disabled");
-#ifndef STANDALONE
+#ifdef STANDALONE
+            fatal("could not create server info socket on port %d", serverport+1);
+#else
+            conoutf("\frcould not create server info socket on port %d, publicity disabled", serverport+1);
             setvar("servertype", 1);
 #endif
             return servertype;
         }
         enet_socket_set_option(pongsock, ENET_SOCKOPT_NONBLOCK, 1);
-
-        address.port = LAN_PORT;
-        lansock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
-        if(lansock != ENET_SOCKET_NULL && (enet_socket_set_option(lansock, ENET_SOCKOPT_REUSEADDR, 1) < 0 || enet_socket_bind(lansock, &address) < 0))
+        if(serverlanport)
         {
-            enet_socket_destroy(lansock);
-            lansock = ENET_SOCKET_NULL;
+            address.port = LAN_PORT;
+            lansock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+            if(lansock != ENET_SOCKET_NULL && (enet_socket_set_option(lansock, ENET_SOCKOPT_REUSEADDR, 1) < 0 || enet_socket_bind(lansock, &address) < 0))
+            {
+                enet_socket_destroy(lansock);
+                lansock = ENET_SOCKET_NULL;
+            }
+            if(lansock == ENET_SOCKET_NULL) conoutf("\frcould not create LAN server info socket");
+            else enet_socket_set_option(lansock, ENET_SOCKOPT_NONBLOCK, 1);
         }
-        if(lansock == ENET_SOCKET_NULL) conoutf("\frcould not create LAN server info socket");
-        else enet_socket_set_option(lansock, ENET_SOCKOPT_NONBLOCK, 1);
     }
 
     return servertype;
@@ -1255,8 +1392,8 @@ void setupserver()
 #ifdef STANDALONE
     setupmaster();
 #endif
-    conoutf("init: server (%s:%d)", *serverip ? serverip : "*", serverport);
-    if(setupserversockets() && verbose) conoutf("\fggame server started");
+    conoutf("loading server (%s:%d)..", *serverip ? serverip : "*", serverport);
+    if(setupserversockets() && verbose) conoutf("game server started");
 #ifndef STANDALONE
     if(servertype >= 3) serverloop();
 #endif
@@ -1264,6 +1401,7 @@ void setupserver()
 
 void initgame()
 {
+    conoutf("identity: v%s-%s%d %s (%s) [0x%x]", VERSION_STRING, versionplatname, versionarch, versionisserver ? "server" : "client", VERSION_RELEASE, versioncrc);
     server::start();
     loopv(gameargs)
     {
@@ -1334,7 +1472,7 @@ bool serveroption(char *opt)
 
 bool findoctadir(const char *name, bool fallback)
 {
-    mkstring(s);
+    string s = "";
     copystring(s, name);
     path(s);
     defformatstring(octadefs)("%s/data/default_map_settings.cfg", s);
@@ -1365,13 +1503,18 @@ void trytofindocta(bool fallback)
 {
     if(!octadir || !*octadir)
     {
-        const char *dir = getenv("OCTA_DIR");
+        const char *dir = getenv("OCTA_DATA");
         if(dir && *dir) setsvar("octadir", dir, false);
+        else
+        {
+            dir = getenv("OCTA_DIR"); // backward compat
+            if(dir && *dir) setsvar("octadir", dir, false);
+        }
     }
     if((!octadir || !*octadir || !findoctadir(octadir, false)) && fallback)
     { // user hasn't specifically set it, try some common locations alongside our folder
 #if defined(WIN32)
-        mkstring(dir);
+        string dir = "";
         if(SHGetFolderPath(NULL, CSIDL_PROGRAM_FILESX86, NULL, 0, dir) == S_OK)
         {
             defformatstring(s)("%s\\Sauerbraten", dir);
@@ -1401,28 +1544,30 @@ void setlocations(bool wanthome)
 #if defined(__APPLE__)
     extern const char *mac_resourcedir();
     const char *dir = mac_resourcedir(); // ./blah.app/Contents/Resources
-    if(dir && *dir)
-    {
+    if(dir && *dir && !chdir(dir))
         conoutf("attempting to use resources in: %s", dir);
-        chdir(dir);
-    }
 #endif
-    loopi(3) if(!fileexists(findfile("data/config/keymap.cfg", "r"), "r"))
-    {
-        if(i != 2) chdir("..");
-        else fatal("could not find data directory");
+    loopirev(3) if(!fileexists(findfile("config/version.cfg", "r"), "r"))
+    { // standalone solution to this is: pebkac
+        if(!i || chdir("..") < 0) fatal("could not find config directory");
     }
-    addpackagedir("data");
-    defformatstring(gamedata)("game/%s", server::gameid());
-    addpackagedir(gamedata);
-    execfile("version.cfg", false, EXEC_VERSION);
+    if(!execfile("config/version.cfg", false, EXEC_VERSION|EXEC_BUILTIN)) fatal("cannot exec 'config/version.cfg'");
+    // pseudo directory with game content
+    const char *dir = getenv("GAME_DATA");
+    if(dir && *dir) addpackagedir(dir);
+    else addpackagedir("data");
+#ifndef STANDALONE
+    if(!fileexists(findfile("textures/logo.png", "r"), "r")) fatal("could not find game data");
+#endif
+    //defformatstring(gamedata)("game/%s", server::gameid());
+    //addpackagedir(gamedata);
     if(wanthome)
     {
 #if defined(WIN32)
-        mkstring(dir);
+        string dir = "";
         if(SHGetFolderPath(NULL, CSIDL_PERSONAL, NULL, 0, dir) == S_OK)
         {
-            defformatstring(s)("%s\\My Games\\%s", dir, versionname);
+            defformatstring(s)("%s\\My Games\\%s", dir, VERSION_NAME);
             sethomedir(s);
         }
 #elif defined(__APPLE__)
@@ -1430,14 +1575,14 @@ void setlocations(bool wanthome)
         const char *dir = mac_personaldir(); // typically  /Users/<name>/Application Support/
         if(dir && *dir)
         {
-            defformatstring(s)("%s/%s", dir, versionuname);
+            defformatstring(s)("%s/%s", dir, VERSION_UNAME);
             sethomedir(s);
         }
 #else
         const char *dir = getenv("HOME");
         if(dir && *dir)
         {
-            defformatstring(s)("%s/.%s", dir, versionuname);
+            defformatstring(s)("%s/.%s", dir, VERSION_UNAME);
             sethomedir(s);
         }
 #endif
@@ -1457,17 +1602,11 @@ void writecfg()
     loopv(ids)
     {
         ident &id = *ids[i];
-        bool saved = false;
         if(id.flags&IDF_PERSIST) switch(id.type)
         {
-            case ID_VAR: if(*id.storage.i != id.def.i) { found = saved = true; f->printf((id.flags&IDF_HEX && *id.storage.i >= 0 ? (id.maxval==0xFFFFFF ? "%s 0x%.6X" : "%s 0x%X") : "%s %d"), escapeid(id), *id.storage.i); } break;
-            case ID_FVAR: if(*id.storage.f != id.def.f) { found = saved = true; f->printf("%s %s", escapeid(id), floatstr(*id.storage.f)); } break;
-            case ID_SVAR: if(strcmp(*id.storage.s, id.def.s)) { found = saved = true; f->printf("%s %s", escapeid(id), escapestring(*id.storage.s)); } break;
-        }
-        if(saved)
-        {
-            if(!(id.flags&IDF_COMPLETE)) f->printf("; setcomplete %s 0", escapeid(id));
-            f->printf("\n");
+            case ID_VAR: if(*id.storage.i != id.def.i) { found = true; f->printf("%s %s\n", escapeid(id), intstr(&id)); } break;
+            case ID_FVAR: if(*id.storage.f != id.def.f) { found = true; f->printf("%s %s\n", escapeid(id), floatstr(*id.storage.f)); } break;
+            case ID_SVAR: if(strcmp(*id.storage.s, id.def.s)) { found = true; f->printf("%s %s\n", escapeid(id), escapestring(*id.storage.s)); } break;
         }
     }
     if(found) f->printf("\n");
@@ -1475,27 +1614,17 @@ void writecfg()
     loopv(ids)
     {
         ident &id = *ids[i];
-        bool saved = false;
         if(id.flags&IDF_PERSIST) switch(id.type)
         {
             case ID_ALIAS:
             {
                 const char *str = id.getstr();
-                //if(str[0])
-                {
-                    found = saved = true;
-                    if(validateblock(str)) f->printf("%s = [%s]", escapeid(id), str);
-                    else f->printf("%s = %s", escapeid(id), escapestring(str));
-                }
+                found = true;
+                if(validateblock(str)) f->printf("%s = [%s]\n", escapeid(id), str);
+                else f->printf("%s = %s\n", escapeid(id), escapestring(str));
             }
             break;
         }
-        if(saved)
-        {
-            f->printf("; setpersist \"%s\" 1", id.name);
-            if(id.flags&IDF_COMPLETE) f->printf("; setcomplete \"%s\" 1", id.name);
-            f->printf("\n");
-        }
     }
     if(found) f->printf("\n");
     writebinds(f);
@@ -1528,11 +1657,21 @@ void rehash(bool reload)
     interactive = false;
     initing = NOT_INITING;
 #endif
-    conoutf("\fwconfiguration reloaded");
+    conoutf("\fwconfiguration %s", reload ? "reloaded" : "loaded");
     rehashing = 0;
 }
 ICOMMAND(0, rehash, "i", (int *nosave), if(!(identflags&IDF_WORLD)) rehash(*nosave ? false : true));
 
+void setcrc(const char *bin)
+{
+    if(!bin || !*bin) return;
+    size_t len = 0;
+    char *buf = loadfile(bin, &len, false);
+    if(!buf) return;
+    versioncrc = crc32(0, (const Bytef *)buf, len);
+    delete[] buf;
+}
+
 #ifdef STANDALONE
 #include <signal.h>
 volatile int errors = 0;
@@ -1540,7 +1679,7 @@ void fatal(const char *s, ...)    // failure exit
 {
     if(++errors <= 2) // print up to one extra recursive error
     {
-        defvformatstring(msg, s, s);
+        defvformatbigstring(msg, s, s);
         if(logfile) logoutf("%s", msg);
 #ifndef WIN32
         fprintf(stderr, "Exiting: %s\n", msg);
@@ -1553,7 +1692,7 @@ void fatal(const char *s, ...)    // failure exit
 #endif
             enet_deinitialize();
 #ifdef WIN32
-            defformatstring(cap)("%s: Error", versionname);
+            defformatstring(cap)("%s: Error", VERSION_NAME);
             MessageBox(NULL, msg, cap, MB_OK|MB_SYSTEMMODAL);
 #endif
         }
@@ -1578,10 +1717,13 @@ void reloadsignal(int signum)
 
 int main(int argc, char **argv)
 {
-    clocktime = time(NULL); // initialise
+    currenttime = time(NULL); // initialise
+    clocktime = mktime(gmtime(&currenttime));
+    clockoffset = currenttime-clocktime;
 
     setlogfile(NULL);
     setlocations(true);
+    setcrc(argv[0]);
 
     char *initscript = NULL;
     for(int i = 1; i<argc; i++)
diff --git a/src/engine/serverbrowser.cpp b/src/engine/serverbrowser.cpp
index 6933cfa..6842674 100644
--- a/src/engine/serverbrowser.cpp
+++ b/src/engine/serverbrowser.cpp
@@ -182,80 +182,37 @@ bool resolverwait(const char *name, ENetAddress *address)
     return resolved;
 }
 
-SDL_Thread *connthread = NULL;
-SDL_mutex *connmutex = NULL;
-SDL_cond *conncond = NULL;
-
-struct connectdata
-{
-    ENetSocket sock;
-    ENetAddress address;
-    int result;
-};
-
-// do this in a thread to prevent timeouts
-// could set timeouts on sockets, but this is more reliable and gives more control
-int connectthread(void *data)
-{
-    SDL_LockMutex(connmutex);
-    if(!connthread || SDL_GetThreadID(connthread) != SDL_ThreadID())
-    {
-        SDL_UnlockMutex(connmutex);
-        return 0;
-    }
-    connectdata cd = *(connectdata *)data;
-    SDL_UnlockMutex(connmutex);
-
-    int result = enet_socket_connect(cd.sock, &cd.address);
-
-    SDL_LockMutex(connmutex);
-    if(!connthread || SDL_GetThreadID(connthread) != SDL_ThreadID())
-    {
-        enet_socket_destroy(cd.sock);
-        SDL_UnlockMutex(connmutex);
-        return 0;
-    }
-    ((connectdata *)data)->result = result;
-    SDL_CondSignal(conncond);
-    SDL_UnlockMutex(connmutex);
-
-    return 0;
-}
-
 #define CONNLIMIT 20000
 
-int connectwithtimeout(ENetSocket sock, const char *hostname, ENetAddress &address)
+int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &address)
 {
     defformatstring(text)("connecting to %s:[%d]...", hostname != NULL ? hostname : "local server", address.port);
     progress(0, text);
 
-    if(!connmutex) connmutex = SDL_CreateMutex();
-    if(!conncond) conncond = SDL_CreateCond();
-    SDL_LockMutex(connmutex);
-    connectdata cd = { sock, address, -1 };
-    connthread = SDL_CreateThread(connectthread, &cd);
-
-    int starttime = SDL_GetTicks(), timeout = 0;
-    for(;;)
+    ENetSocketSet readset, writeset;
+    if(!enet_socket_connect(sock, &address)) for(int starttime = SDL_GetTicks(), timeout = 0; timeout <= CONNLIMIT;)
     {
-        if(!SDL_CondWaitTimeout(conncond, connmutex, 250))
+        ENET_SOCKETSET_EMPTY(readset);
+        ENET_SOCKETSET_EMPTY(writeset);
+        ENET_SOCKETSET_ADD(readset, sock);
+        ENET_SOCKETSET_ADD(writeset, sock);
+        int result = enet_socketset_select(sock, &readset, &writeset, 250);
+        if(result < 0) break;
+        else if(result > 0)
         {
-            if(cd.result<0) enet_socket_destroy(sock);
-            break;
+            if(ENET_SOCKETSET_CHECK(readset, sock) || ENET_SOCKETSET_CHECK(writeset, sock))
+            {
+                int error = 0;
+                if(enet_socket_get_option(sock, ENET_SOCKOPT_ERROR, &error) < 0 || error) break;
+                return 0;
+            }
         }
         timeout = SDL_GetTicks() - starttime;
         progress(min(float(timeout)/CONNLIMIT, 1.0f), text);
-        if(interceptkey(SDLK_ESCAPE)) timeout = CONNLIMIT + 1;
-        if(timeout > CONNLIMIT) break;
+        if(interceptkey(SDLK_ESCAPE)) break;
     }
 
-    /* thread will actually timeout eventually if its still trying to connect
-     * so just leave it (and let it destroy socket) instead of causing problems on some platforms by killing it
-     */
-    connthread = NULL;
-    SDL_UnlockMutex(connmutex);
-
-    return cd.result;
+    return -1;
 }
 
 vector<serverinfo *> servers;
@@ -263,9 +220,9 @@ bool sortedservers = true;
 ENetSocket pingsock = ENET_SOCKET_NULL;
 int lastinfo = 0;
 
-static serverinfo *newserver(const char *name, int port = SERVER_PORT, const char *desc = NULL, int priority = 0, uint ip = ENET_HOST_ANY)
+static serverinfo *newserver(const char *name, int port = SERVER_PORT, int priority = 0, const char *desc = NULL, uint ip = ENET_HOST_ANY)
 {
-    serverinfo *si = new serverinfo(ip, port);
+    serverinfo *si = new serverinfo(ip, port, priority);
 
     if(name) copystring(si->name, name);
     else if(ip==ENET_HOST_ANY || enet_address_get_host_ip(&si->address, si->name, sizeof(si->name)) < 0)
@@ -281,13 +238,14 @@ static serverinfo *newserver(const char *name, int port = SERVER_PORT, const cha
     return si;
 }
 
-void addserver(const char *name, int port, const char *desc)
+void addserver(const char *name, int port, int priority, const char *desc)
 {
     loopv(servers) if(!strcmp(servers[i]->name, name) && servers[i]->port == port) return;
-    if(newserver(name, port, desc) && verbose >= 2)
+    if(newserver(name, port, priority, desc) && verbose >= 2)
         conoutf("added server %s (%d) [%s]", name, port, desc);
 }
-ICOMMAND(0, addserver, "sis", (char *n, int *p, char *d), addserver(n, *p > 0 ? *p : SERVER_PORT, d));
+ICOMMAND(0, addserver, "siis", (char *n, int *p, int *r, char *d), addserver(n, *p > 0 ? *p : SERVER_PORT, *r >= 0 ? *r : 0, d));
+
 VAR(0, searchlan, 0, 0, 1);
 VAR(IDF_PERSIST, maxservpings, 0, 10, 1000);
 VAR(IDF_PERSIST, serverupdateinterval, 0, 10, VAR_MAX);
@@ -310,7 +268,7 @@ void pingservers()
     ENetBuffer buf;
     uchar ping[MAXTRANS];
     ucharbuf p(ping, sizeof(ping));
-    putint(p, totalmillis);
+    putint(p, totalmillis ? totalmillis : 1);
 
     static int lastping = 0;
     if(lastping >= servers.length()) lastping = 0;
@@ -326,11 +284,11 @@ void pingservers()
         si.checkdecay(serverdecay*1000);
     }
 
-    if(searchlan)
+    if(searchlan && serverlanport)
     {
         ENetAddress address;
         address.host = ENET_HOST_BROADCAST;
-        address.port = LAN_PORT;
+        address.port = serverlanport;
         buf.data = ping;
         buf.dataLength = p.length();
         enet_socket_send(pingsock, &address, &buf, 1);
@@ -389,7 +347,7 @@ void checkpings()
         if(len <= 0) return;
         serverinfo *si = NULL;
         loopv(servers) if(addr.host == servers[i]->address.host && addr.port == servers[i]->address.port) { si = servers[i]; break; }
-        if(!si && searchlan) si = newserver(NULL, addr.port-1, NULL, 1, addr.host);
+        if(!si && searchlan) si = newserver(NULL, addr.port-1, 1, NULL, addr.host);
         if(!si) continue;
         ucharbuf p(ping, len);
         int millis = getint(p), rtt = clamp(totalmillis - millis, 0, min(serverdecay*1000, totalmillis));
@@ -400,14 +358,22 @@ void checkpings()
         si->attr.shrink(0);
         loopj(numattr) si->attr.add(getint(p));
         getstring(text, p);
-        filtertext(si->map, text, false);
+        filterstring(si->map, text, false);
         getstring(text, p);
-        filtertext(si->sdesc, text);
+        filterstring(si->sdesc, text);
         si->players.deletearrays();
+        si->handles.deletearrays();
+        loopi(si->numplayers)
+        {
+            if(p.overread()) break;
+            getstring(text, p);
+            si->players.add(newstring(text));
+        }
         loopi(si->numplayers)
         {
+            if(p.overread()) break;
             getstring(text, p);
-            if(text[0]) si->players.add(newstring(text));
+            si->handles.add(newstring(text));
         }
         sortedservers = false;
     }
@@ -498,8 +464,22 @@ void retrieveservers(vector<char> &data)
     enet_socket_destroy(sock);
 }
 
+void sortservers()
+{
+    if(!sortedservers)
+    {
+        servers.sort(serverinfocompare);
+        sortedservers = true;
+    }
+}
+COMMAND(0, sortservers, "");
+
+VAR(IDF_PERSIST, autosortservers, 0, 1, 1);
+VAR(0, pausesortservers, 0, 0, 1);
+
 void updatefrommaster()
 {
+    pausesortservers = 0;
     vector<char> data;
     retrieveservers(data);
     if(data.length() && data[0])
@@ -515,36 +495,25 @@ void updatefrommaster()
 }
 COMMAND(0, updatefrommaster, "");
 
-void sortservers()
-{
-    if(!sortedservers)
-    {
-        servers.sort(serverinfocompare);
-        sortedservers = true;
-    }
-}
-COMMAND(0, sortservers, "");
-
-VAR(IDF_PERSIST, autosortservers, 0, 1, 1);
-
 void updateservers()
 {
     if(!reqmaster) updatefrommaster();
     refreshservers();
-    if(autosortservers) sortservers();
+    if(autosortservers && !pausesortservers) sortservers();
     intret(servers.length());
 }
 COMMAND(0, updateservers, "");
 
 void writeservercfg()
 {
+    if(servers.empty()) return;
     stream *f = openutf8file("servers.cfg", "w");
     if(!f) return;
-    f->printf("// servers connected to are added here automatically\n\n");
+    f->printf("// servers which are connected to or queried get added here automatically\n\n");
     loopv(servers)
     {
         serverinfo *s = servers[i];
-        f->printf("addserver %s %d %s\n", escapeid(s->name), s->port, escapeid(s->sdesc[0] ? s->sdesc : s->name));
+        f->printf("addserver %s %d %d %s\n", escapeid(s->name), s->port, s->priority, escapeid(s->sdesc[0] ? s->sdesc : s->name));
     }
     delete f;
 }
diff --git a/src/engine/shader.cpp b/src/engine/shader.cpp
index 66c22aa..80165ad 100644
--- a/src/engine/shader.cpp
+++ b/src/engine/shader.cpp
@@ -1135,11 +1135,11 @@ static bool genwatervariant(Shader &s, const char *sname, vector<char> &vs, vect
     if(*pspragma) pspragma++;
     if(s.type & SHADER_GLSLANG)
     {
-        const char *fadedef = "waterfade = gl_Vertex.z*waterfadeparams.x + waterfadeparams.y;\n";
+        const char *fadedef = "fadedepth = gl_Vertex.z*waterfadeparams.x + waterfadeparams.y;\n";
         vs.insert(vspragma-vs.getbuf(), fadedef, strlen(fadedef));
-        const char *fadeuse = "gl_FragColor.a = waterfade;\n";
+        const char *fadeuse = "gl_FragColor.a = fadedepth;\n";
         ps.insert(pspragma-ps.getbuf(), fadeuse, strlen(fadeuse));
-        const char *fadedecl = "uniform vec4 waterfadeparams; varying float waterfade;\n";
+        const char *fadedecl = "uniform vec4 waterfadeparams; varying float fadedepth;\n";
         const char *vsmain = findglslmain(vs.getbuf()), *psmain = findglslmain(ps.getbuf());
         vs.insert(vsmain ? vsmain - vs.getbuf() : 0, fadedecl, strlen(fadedecl));
         ps.insert(psmain ? psmain - ps.getbuf() : 0, fadedecl, strlen(fadedecl));
@@ -2352,7 +2352,7 @@ void setblurshader(int pass, int size, int radius, float *weights, float *offset
         (offsets[3] - offsets[2])/size);
     loopk(4)
     {
-        static const char *names[4] = { "offset4", "offset5", "offset6", "offset7" };
+        static const char * const names[4] = { "offset4", "offset5", "offset6", "offset7" };
         setlocalparamf(names[k], SHPARAM_PIXEL, 3+k,
             pass==0 ? offsets[4+k]/size : offsets[0]/size,
             pass==1 ? offsets[4+k]/size : offsets[0]/size,
diff --git a/src/engine/shadowmap.cpp b/src/engine/shadowmap.cpp
index 8ff4a60..50dec4c 100644
--- a/src/engine/shadowmap.cpp
+++ b/src/engine/shadowmap.cpp
@@ -58,7 +58,7 @@ void guessshadowdir()
             }
             case ET_SUNLIGHT:
             {
-                vec dir; vecfromyawpitch(e.attrs[0], e.attrs[1], 1, 0, dir); dir.normalize().mul(hdr.worldsize);
+                vec dir = vec(e.attrs[0]*RAD, e.attrs[1]*RAD).mul(getworldsize());
                 lightpos.add(vec(1, 1, 1).mul(hdr.worldsize/2).add(dir));
                 numlights++;
                 break;
diff --git a/src/engine/skelmodel.h b/src/engine/skelmodel.h
index 6e9b8a3..d27cc45 100644
--- a/src/engine/skelmodel.h
+++ b/src/engine/skelmodel.h
@@ -99,7 +99,7 @@ struct skelmodel : animmodel
             else
             {
                 int total = 0;
-                loopk(4) total += (v.weights[k] = uchar(weights[k]*255));
+                loopk(4) total += (v.weights[k] = uchar(0.5f + weights[k]*255));
                 while(total > 255)
                 {
                     loopk(4) if(v.weights[k] > 0 && total > 255) { v.weights[k]--; total--; }
@@ -212,7 +212,7 @@ struct skelmodel : animmodel
             mesh::calctangents(bumpverts, verts, verts, numverts, tris, numtris, areaweight);
         }
 
-        void calcbb(int frame, vec &bbmin, vec &bbmax, const matrix3x4 &m)
+        void calcbb(vec &bbmin, vec &bbmax, const matrix3x4 &m)
         {
             loopj(numverts)
             {
@@ -225,7 +225,7 @@ struct skelmodel : animmodel
             }
         }
 
-        void gentris(int frame, Texture *tex, vector<BIH::tri> *out, const matrix3x4 &m)
+        void gentris(Texture *tex, vector<BIH::tri> *out, const matrix3x4 &m)
         {
             loopj(numtris)
             {
@@ -603,6 +603,8 @@ struct skelmodel : animmodel
     {
         int bone, target, parent;
         float pitchmin, pitchmax, pitchscale, pitchangle, pitchtotal;
+
+        pitchcorrect() : parent(-1), pitchangle(0), pitchtotal(0) {}
     };
 
     struct skeleton
@@ -1193,7 +1195,7 @@ struct skelmodel : animmodel
             loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]);
         }
 
-        void concattagtransform(part *p, int frame, int i, const matrix3x4 &m, matrix3x4 &n)
+        void concattagtransform(part *p, int i, const matrix3x4 &m, matrix3x4 &n)
         {
             matrix3x4 t;
             t.mul(bones[tags[i].bone].base, tags[i].matrix);
@@ -1639,9 +1641,9 @@ struct skelmodel : animmodel
             }
         }
 
-        void concattagtransform(part *p, int frame, int i, const matrix3x4 &m, matrix3x4 &n)
+        void concattagtransform(part *p, int i, const matrix3x4 &m, matrix3x4 &n)
         {
-            skel->concattagtransform(p, frame, i, m, n);
+            skel->concattagtransform(p, i, m, n);
         }
 
         int addblendcombo(const blendcombo &c)
diff --git a/src/engine/sound.cpp b/src/engine/sound.cpp
index 7d31097..e9e057e 100644
--- a/src/engine/sound.cpp
+++ b/src/engine/sound.cpp
@@ -21,7 +21,7 @@ soundslot::~soundslot() { DELETEA(name); }
 
 sound::sound() : hook(NULL) { reset(); }
 sound::~sound() {}
-bool sound::playing() { return chan >= 0 && Mix_Playing(chan); }
+bool sound::playing() { return chan >= 0 && (Mix_Playing(chan) || Mix_Paused(chan)); }
 void sound::reset()
 {
     pos = oldpos = vec(-1, -1, -1);
@@ -45,20 +45,21 @@ Mix_Music *music = NULL;
 SDL_RWops *musicrw = NULL;
 stream *musicstream = NULL;
 char *musicfile = NULL, *musicdonecmd = NULL;
-int soundsatonce = 0, lastsoundmillis = 0, musictime = -1, musicdonetime = -1;
+int musictime = -1, musicdonetime = -1;
 
 VARF(IDF_PERSIST, mastervol, 0, 255, 255, changedvol = true);
 VAR(IDF_PERSIST, soundvol, 0, 255, 255);
 VARF(0, soundmono, 0, 0, 1, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
-VARF(0, soundchans, 1, 32, 128, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
+VARF(0, soundmixchans, 16, 32, 1024, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
 VARF(0, soundfreq, 0, 44100, 48000, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
-VARF(0, soundbufferlen, 128, 1024, VAR_MAX, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
+VARF(0, soundbuflen, 128, 4096, VAR_MAX, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
 VAR(IDF_PERSIST, soundmaxrad, 0, 512, VAR_MAX);
 VAR(IDF_PERSIST, soundminrad, 0, 0, VAR_MAX);
 FVAR(IDF_PERSIST, soundevtvol, 0, 1, FVAR_MAX);
 FVAR(IDF_PERSIST, soundevtscale, 0, 1, FVAR_MAX);
 FVAR(IDF_PERSIST, soundenvvol, 0, 1, FVAR_MAX);
 FVAR(IDF_PERSIST, soundenvscale, 0, 1, FVAR_MAX);
+VAR(IDF_PERSIST, soundcull, 0, 1, 1);
 
 VARF(IDF_PERSIST, musicvol, 0, 64, 255, changedvol = true);
 VAR(IDF_PERSIST, musicfadein, 0, 1000, VAR_MAX);
@@ -69,13 +70,13 @@ void initsound()
 {
     if(nosound)
     {
-        if(Mix_OpenAudio(soundfreq, MIX_DEFAULT_FORMAT, soundmono ? 1 : 2, soundbufferlen) == -1)
+        if(Mix_OpenAudio(soundfreq, MIX_DEFAULT_FORMAT, soundmono ? 1 : 2, soundbuflen) == -1)
         {
             conoutf("\frsound initialisation failed: %s", Mix_GetError());
             return;
         }
-
-        Mix_AllocateChannels(soundchans);
+        int chans = Mix_AllocateChannels(soundmixchans);
+        conoutf("allocated %d of %d sound channels", chans, soundmixchans);
         nosound = false;
     }
     initmumble();
@@ -139,6 +140,58 @@ void clearsound()
     mapsounds.setsize(0);
 }
 
+void getsounds(bool mapsnd, int idx, int prop)
+{
+    vector<soundslot> &soundset = mapsnd ? mapsounds : gamesounds;
+    if(idx < 0) intret(soundset.length());
+    else if(soundset.inrange(idx))
+    {
+        if(prop < 0) intret(4);
+        else switch(prop)
+        {
+            case 0: intret(soundset[idx].vol); break;
+            case 1: intret(soundset[idx].maxrad); break;
+            case 2: intret(soundset[idx].minrad); break;
+            case 3: result(soundset[idx].name); break;
+            default: break;
+        }
+    }
+}
+ICOMMAND(0, getsound, "ibb", (int *n, int *v, int *p), getsounds(*n!=0, *v, *p));
+
+void getcursounds(int idx, int prop)
+{
+    if(idx < 0) intret(sounds.length());
+    else if(sounds.inrange(idx))
+    {
+        if(prop < 0) intret(19);
+        else switch(prop)
+        {
+            case 0: intret(sounds[idx].vol); break;
+            case 1: intret(sounds[idx].curvol); break;
+            case 2: intret(sounds[idx].curpan); break;
+            case 3: intret(sounds[idx].flags); break;
+            case 4: intret(sounds[idx].maxrad); break;
+            case 5: intret(sounds[idx].minrad); break;
+            case 6: intret(sounds[idx].material); break;
+            case 7: intret(sounds[idx].millis); break;
+            case 8: intret(sounds[idx].ends); break;
+            case 9: intret(sounds[idx].slotnum); break;
+            case 10: intret(sounds[idx].chan); break;
+            case 11: defformatstring(pos)("%.f %.f %.f", sounds[idx].pos.x, sounds[idx].pos.y, sounds[idx].pos.z); result(pos); break;
+            case 12: defformatstring(oldpos)("%.f %.f %.f", sounds[idx].oldpos.x, sounds[idx].oldpos.y, sounds[idx].oldpos.z); result(oldpos); break;
+            case 13: intret(sounds[idx].valid() ? 1 : 0); break;
+            case 14: intret(sounds[idx].playing() ? 1 : 0); break;
+            case 15: intret(sounds[idx].flags&SND_MAP ? 1 : 0); break;
+            case 16: intret(sounds[idx].owner!=NULL ? 1 : 0); break;
+            case 17: intret(sounds[idx].owner==camera1 ? 1 : 0); break;
+            case 18: intret(client::getcn(sounds[idx].owner)); break;
+            default: break;
+        }
+    }
+}
+ICOMMAND(0, getcursound, "bb", (int *n, int *p), getcursounds(*n, *p));
+
 Mix_Music *loadmusic(const char *name)
 {
     if(!musicstream) musicstream = openzipfile(name, "rb");
@@ -229,9 +282,9 @@ void smartmusic(bool cond, bool autooff)
 }
 ICOMMAND(0, smartmusic, "ii", (int *a, int *b), smartmusic(*a, *b));
 
-int findsound(const char *name, int vol, vector<soundslot> &sounds)
+int findsound(const char *name, int vol, vector<soundslot> &soundset)
 {
-    loopv(sounds) if(!strcmp(sounds[i].name, name) && (!vol || sounds[i].vol == vol)) return i;
+    loopv(soundset) if(!strcmp(soundset[i].name, name) && (!vol || soundset[i].vol == vol)) return i;
     return -1;
 }
 
@@ -253,20 +306,28 @@ static Mix_Chunk *loadwav(const char *name)
     return c;
 }
 
-int addsound(const char *name, int vol, int maxrad, int minrad, int value, vector<soundslot> &sounds)
+int addsound(const char *name, int vol, int maxrad, int minrad, int value, vector<soundslot> &soundset)
 {
     if(vol <= 0 || vol >= 255) vol = 255;
     if(maxrad <= 0) maxrad = -1;
     if(minrad < 0) minrad = -1;
     if(value == 1)
     {
-        loopv(sounds)
+        loopv(soundset)
         {
-            soundslot &slot = sounds[i];
+            soundslot &slot = soundset[i];
             if(slot.vol == vol && slot.maxrad == maxrad && slot.minrad == minrad && !strcmp(slot.name, name))
                 return i;
         }
     }
+    if(!strcmp(name, "<none>"))
+    {
+        soundslot &slot = soundset.add();
+        slot.name = newstring(name);
+        slot.vol = 0;
+        slot.maxrad = slot.minrad = -1;
+        return soundset.length()-1;
+    }
     soundsample *sample = NULL;
     #define loadsound(req) \
     { \
@@ -306,7 +367,7 @@ int addsound(const char *name, int vol, int maxrad, int minrad, int value, vecto
         }
         else break;
     }
-    soundslot &slot = sounds.add();
+    soundslot &slot = soundset.add();
     slot.name = newstring(name);
     slot.vol = vol;
     slot.maxrad = maxrad; // use these values if none are supplied when playing
@@ -319,7 +380,7 @@ int addsound(const char *name, int vol, int maxrad, int minrad, int value, vecto
         if(!sample->sound) conoutf("\frfailed to load sample: %s", sam);
         else slot.samples.add(sample);
     }
-    return sounds.length()-1;
+    return soundset.length()-1;
 }
 
 ICOMMAND(0, registersound, "sissi", (char *n, int *v, char *w, char *x, int *u), intret(addsound(n, *v, *w ? parseint(w) : -1, *x ? parseint(x) : -1, *u, gamesounds)));
@@ -327,7 +388,9 @@ ICOMMAND(0, mapsound, "sissi", (char *n, int *v, char *w, char *x, int *u), intr
 
 void calcvol(int flags, int vol, int slotvol, int maxrad, int minrad, const vec &pos, int *curvol, int *curpan, bool liquid)
 {
-    int svol = flags&SND_CLAMPED ? 255 : clamp(vol, 0, 255), span = 127; vec v; float dist = pos.dist(camera1->o, v);
+    vec v;
+    float dist = pos.dist(camera1->o, v);
+    int svol = flags&SND_CLAMPED ? 255 : clamp(vol, 0, 255), span = 127;
     if(!(flags&SND_NOATTEN) && dist > 0)
     {
         if(!(flags&SND_NOPAN) && !soundmono && (v.x != 0 || v.y != 0))
@@ -356,7 +419,7 @@ void updatesound(int chan)
 {
     sound &s = sounds[chan];
     bool waiting = (!(s.flags&SND_NODELAY) && Mix_Paused(chan));
-    if((s.flags&SND_NOCULL) || s.curvol > 0 || (s.owner && game::camerapos(s.owner).dist(camera1->o) == 0))
+    if((s.flags&SND_NOCULL) || !soundcull || s.curvol > 0 || s.pos.dist(camera1->o) <= 0)
     {
         if(waiting)
         { // delay the sound based on average physical constants
@@ -419,44 +482,48 @@ void updatesounds()
 
 int playsound(int n, const vec &pos, physent *d, int flags, int vol, int maxrad, int minrad, int *hook, int ends, int *oldhook)
 {
-    if(nosound || !mastervol || !soundvol) return -1;
-
+    if(nosound || !mastervol || !soundvol || ((flags&SND_MAP || n >= S_GAMESPECIFIC) && client::waiting(true)) || (!d && !insideworld(pos))) return -1;
     vector<soundslot> &soundset = flags&SND_MAP ? mapsounds : gamesounds;
 
-    if(soundset.inrange(n) && !soundset[n].samples.empty())
+    if(soundset.inrange(n))
     {
+        if(soundset[n].samples.empty() || !soundset[n].vol)
+        {
+            if(oldhook && issound(*oldhook)) removesound(*oldhook);
+            return -1;
+        }
         soundslot *slot = &soundset[n];
         if(!oldhook || !issound(*oldhook) || (n != sounds[*oldhook].slotnum && strcmp(slot->name, gamesounds[sounds[*oldhook].slotnum].name)))
             oldhook = NULL;
 
-        int cvol = 0, cpan = 0, v = vol > 0 && vol < 256 ? vol : (flags&SND_CLAMPED ? 64 : 255),
+        vec o = d ? game::camerapos(d) : pos;
+        int cvol = 0, cpan = 0, v = clamp(vol >= 0 ? vol : 255, flags&SND_CLAMPED ? 64 : 0, 255),
             x = maxrad > 0 ? maxrad : (flags&SND_CLAMPED ? getworldsize() : (slot->maxrad > 0 ? slot->maxrad : soundmaxrad)),
             y = minrad >= 0 ? minrad : (flags&SND_CLAMPED ? 32 : (slot->minrad >= 0 ? slot->minrad : soundminrad)),
-            mat = lookupmaterial(pos);
+            mat = lookupmaterial(o);
 
         bool liquid = isliquid(lookupmaterial(camera1->o)&MATF_VOLUME);
-        calcvol(flags, v, slot->vol, x, y, pos, &cvol, &cpan, liquid || isliquid(mat&MATF_VOLUME));
-        bool nocull = flags&SND_NOCULL || (d && game::camerapos(d).dist(camera1->o) == 0);
+        calcvol(flags, v, slot->vol, x, y, o, &cvol, &cpan, liquid || isliquid(mat&MATF_VOLUME));
+        bool nocull = flags&SND_NOCULL || o.dist(camera1->o) <= 0;
 
-        if(nocull || cvol > 0)
+        if(nocull || !soundcull || cvol > 0)
         {
             int chan = -1;
             if(oldhook) chan = *oldhook;
             else
             {
                 oldhook = NULL;
-                soundsample *sample = soundset[n].samples[soundset[n].samples.length() > 1 ? rnd(soundset[n].samples.length()) : 0];
+                soundsample *sample = soundset[n].samples[rnd(soundset[n].samples.length())];
                 if((chan = Mix_PlayChannel(-1, sample->sound, flags&SND_LOOP ? -1 : 0)) < 0)
                 {
                     int lowest = -1;
-                    loopv(sounds) if(sounds[i].chan >= 0 && !(sounds[i].flags&SND_NOCULL) && !(sounds[i].flags&SND_MAP) && (sounds[i].owner && game::camerapos(sounds[i].owner).dist(camera1->o) > 0))
-                        if((nocull || sounds[i].curvol < cvol) && (!sounds.inrange(lowest) || sounds[i].curvol < sounds[lowest].curvol))
-                            lowest = i;
+                    loopv(sounds) if(sounds[i].chan >= 0 && (!(sounds[i].flags&SND_MAP) || flags&SND_MAP) && sounds[i].curvol < cvol && (lowest < 0 || sounds[i].curvol < sounds[lowest].curvol) && (nocull || (!(sounds[i].flags&SND_NOCULL) && sounds[i].pos.dist(camera1->o) > 0)))
+                        lowest = i;
                     if(sounds.inrange(lowest))
                     {
+                        if(verbose >= 4) conoutf("culled channel %d (%d)", lowest, sounds[lowest].curvol);
                         removesound(lowest);
                         chan = Mix_PlayChannel(-1, sample->sound, flags&SND_LOOP ? -1 : 0);
-                        if(verbose >= 4) conoutf("culled channel %d (%d)", lowest, sounds[lowest].curvol);
                     }
                 }
             }
@@ -477,7 +544,7 @@ int playsound(int n, const vec &pos, physent *d, int flags, int vol, int maxrad,
                 s.ends = ends;
                 s.slotnum = n;
                 s.owner = d;
-                s.pos = s.oldpos = pos;
+                s.pos = s.oldpos = o;
                 s.curvol = cvol;
                 s.curpan = cpan;
                 s.chan = chan;
@@ -548,6 +615,7 @@ void resetsound()
         soundsamples.clear();
         return;
     }
+    rehash(true);
     if(music && loadmusic(musicfile))
     {
         if(musicfadein) Mix_FadeInMusic(music, musicdonecmd ? 0 : -1, musicfadein);
@@ -618,7 +686,7 @@ void initmumble()
         if(mumblelink)
         {
             mumbleinfo = (MumbleInfo *)MapViewOfFile(mumblelink, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(MumbleInfo));
-            if(mumbleinfo) wcsncpy(mumbleinfo->name, (const wchar_t *)versionuname, 256);
+            if(mumbleinfo) wcsncpy(mumbleinfo->name, (const wchar_t *)VERSION_UNAME, 256);
         }
     #elif defined(_POSIX_SHARED_MEMORY_OBJECTS)
         defformatstring(shmname)("/MumbleLink.%d", getuid());
@@ -626,7 +694,7 @@ void initmumble()
         if(mumblelink >= 0)
         {
             mumbleinfo = (MumbleInfo *)mmap(NULL, sizeof(MumbleInfo), PROT_READ|PROT_WRITE, MAP_SHARED, mumblelink, 0);
-            if(mumbleinfo != (MumbleInfo *)-1) wcsncpy(mumbleinfo->name, (const wchar_t *)versionuname, 256);
+            if(mumbleinfo != (MumbleInfo *)-1) wcsncpy(mumbleinfo->name, (const wchar_t *)VERSION_UNAME, 256);
         }
     #endif
     if(!VALID_MUMBLELINK) closemumble();
diff --git a/src/engine/sound.h b/src/engine/sound.h
index 49a28ca..e73cc3b 100644
--- a/src/engine/sound.h
+++ b/src/engine/sound.h
@@ -17,7 +17,7 @@ enum
     SND_LOOP    = 1<<7,
     SND_MAP     = 1<<8,
     SND_IMPORT  = SND_NODELAY|SND_NOCULL|SND_NOQUIET,
-    SND_FORCED  = SND_IMPORT|SND_NOATTEN|SND_CLAMPED,
+    SND_FORCED  = SND_IMPORT|SND_NOATTEN|SND_NODIST,
     SND_DIRECT  = SND_IMPORT|SND_CLAMPED,
     SND_MASKF   = SND_LOOP|SND_MAP,
     SND_LAST    = 7
@@ -70,7 +70,7 @@ extern bool playingmusic(bool check = true);
 extern void smartmusic(bool cond, bool autooff);
 extern void musicdone(bool docmd);
 extern void updatesounds();
-extern int addsound(const char *name, int vol, int maxrad, int minrad, int value, vector<soundslot> &sounds);
+extern int addsound(const char *name, int vol, int maxrad, int minrad, int value, vector<soundslot> &soundset);
 extern void removesound(int c);
 extern void clearsound();
 extern int playsound(int n, const vec &pos, physent *d = NULL, int flags = 0, int vol = -1, int maxrad = -1, int minrad = -1, int *hook = NULL, int ends = 0, int *oldhook = NULL);
diff --git a/src/engine/textedit.h b/src/engine/textedit.h
index 4786396..face903 100644
--- a/src/engine/textedit.h
+++ b/src/engine/textedit.h
@@ -29,7 +29,7 @@ struct editline
         {
             va_list args;
             va_start(args, fmt);
-            _vsnprintf(newtext, maxlen, fmt, args);
+            vformatstring(newtext, fmt, args, maxlen);
             va_end(args);
         }
         DELETEA(text);
@@ -215,7 +215,7 @@ struct editor
 
     void mark(bool enable)
     {
-        mx = (enable) ? cx : -1;
+        mx = enable ? cx : -1;
         my = cy;
     }
 
@@ -230,13 +230,20 @@ struct editor
     bool region(int &sx, int &sy, int &ex, int &ey)
     {
         int n = lines.length();
-        assert(n != 0);
-        if(cy < 0) cy = 0; else if(cy >= n) cy = n-1;
+        if(!n)
+        {
+            lines.add().set("");
+            n = 1;
+        }
+        if(cy < 0) cy = 0;
+        else if(cy >= n) cy = n-1;
         int len = lines[cy].len;
-        if(cx < 0) cx = 0; else if(cx > len) cx = len;
+        if(cx < 0) cx = 0;
+        else if(cx > len) cx = len;
         if(mx >= 0)
         {
-            if(my < 0) my = 0; else if(my >= n) my = n-1;
+            if(my < 0) my = 0;
+            else if(my >= n) my = n-1;
             len = lines[my].len;
             if(mx > len) mx = len;
         }
@@ -245,7 +252,7 @@ struct editor
         ex = cx;
         ey = cy;
         if(sy > ey) { swap(sy, ey); swap(sx, ex); }
-        else if(sy==ey && sx > ex) swap(sx, ex);
+        else if(sy == ey && sx > ex) swap(sx, ex);
         return (sx != ex) || (sy != ey);
     }
 
@@ -255,15 +262,21 @@ struct editor
     editline &currentline()
     {
         int n = lines.length();
-        assert(n != 0);
-        if(cy < 0) cy = 0; else if(cy >= n) cy = n-1;
-        if(cx < 0) cx = 0; else if(cx > lines[cy].len) cx = lines[cy].len;
+        if(!n)
+        {
+            lines.add().set("");
+            n = 1;
+        }
+        if(cy < 0) cy = 0;
+        else if(cy >= n) cy = n-1;
+        if(cx < 0) cx = 0;
+        else if(cx > lines[cy].len) cx = lines[cy].len;
         return lines[cy];
     }
 
     void copyselectionto(editor *b)
     {
-        if(b==this) return;
+        if(b == this) return;
 
         b->clear(NULL);
         int sx, sy, ex, ey;
@@ -396,7 +409,7 @@ struct editor
 
     void insertallfrom(editor *b)
     {
-        if(b==this) return;
+        if(b == this) return;
 
         del();
 
@@ -521,8 +534,7 @@ struct editor
 
     void hit(int hitx, int hity, bool dragged)
     {
-        int maxwidth = linewrap?pixelwidth:-1;
-        int h = 0;
+        int maxwidth = linewrap ? pixelwidth : -1, h = 0;
         for(int i = scrolly; i < lines.length(); i++)
         {
             int width, height;
@@ -535,14 +547,13 @@ struct editor
                 if(dragged) { mx = x; my = i; } else { cx = x; cy = i; };
                 break;
             }
-           h+=height;
+           h += height;
         }
     }
 
     int limitscrolly()
     {
-        int maxwidth = linewrap?pixelwidth:-1;
-        int slines = lines.length();
+        int maxwidth = linewrap ? pixelwidth : -1, slines = lines.length();
         for(int ph = pixelheight; slines > 0 && ph > 0;)
         {
             int width, height;
@@ -554,16 +565,27 @@ struct editor
         return slines;
     }
 
-    void draw(int x, int y, int color, bool hit)
+    void draw(int x, int y, int color, bool hit, const char *prompt = NULL)
     {
-        int maxwidth = linewrap?pixelwidth:-1;
-
-        int sx, sy, ex, ey;
+        int h = 0, maxwidth = linewrap ? pixelwidth : -1;
+        if(lines.empty() || !lines[0].text[0])
+        {
+            if(!hit && prompt && *prompt)
+            {
+                int width = 0, height = 0;
+                text_bounds(prompt, width, height, maxwidth, TEXT_NO_INDENT);
+                if(h+height <= pixelheight)
+                    draw_text(prompt, x, y+h, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, TEXT_NO_INDENT, -1, maxwidth);
+            }
+            return;
+        }
+        int starty = scrolly, sx = 0, sy = 0, ex = 0, ey = 0;
         bool selection = region(sx, sy, ex, ey);
-
-        // fix scrolly so that <cx, cy> is always on screen
-        int starty = scrolly;
-        if(starty==SCROLLEND) { cy = lines.length()-1; starty = 0; }
+        if(starty == SCROLLEND) // fix scrolly so that <cx, cy> is always on screen
+        {
+            cy = lines.length()-1;
+            starty = 0;
+        }
         if(cy < starty) starty = cy;
         else
         {
@@ -573,14 +595,13 @@ struct editor
                 int width, height;
                 text_bounds(lines[i].text, width, height, maxwidth, TEXT_NO_INDENT);
                 h += height;
-                if(h > pixelheight) { starty = i + 1; break; }
+                if(h > pixelheight) { starty = i+1; break; }
             }
         }
 
         if(selection)
         {
-            // convert from cursor coords into pixel coords
-            int psx, psy, pex, pey;
+            int psx, psy, pex, pey; // convert from cursor coords into pixel coords
             text_pos(lines[sy].text, sx, psx, psy, maxwidth, TEXT_NO_INDENT);
             text_pos(lines[ey].text, ex, pex, pey, maxwidth, TEXT_NO_INDENT);
             int maxy = lines.length();
@@ -589,7 +610,7 @@ struct editor
             {
                 int width, height;
                 text_bounds(lines[i].text, width, height, maxwidth, TEXT_NO_INDENT);
-                if(h + height > pixelheight) { maxy = i; break; }
+                if(h+height > pixelheight) { maxy = i; break; }
                 if(i == sy) psy += h;
                 if(i == ey) { pey += h; break; }
                 h += height;
@@ -598,10 +619,18 @@ struct editor
 
             if(ey >= starty && sy <= maxy)
             {
-                // crop top/bottom within window
-                if(sy < starty) { sy = starty; psy = 0; psx = 0; }
-                if(ey > maxy) { ey = maxy; pey = pixelheight - FONTH; pex = pixelwidth; }
-
+                if(sy < starty) // crop top/bottom within window
+                {
+                    sy = starty;
+                    psy = 0;
+                    psx = 0;
+                }
+                if(ey > maxy)
+                {
+                    ey = maxy;
+                    pey = pixelheight-FONTH;
+                    pex = pixelwidth;
+                }
                 notextureshader->set();
                 glDisable(GL_TEXTURE_2D);
                 glColor3f(0.25f, 0.25f, 0.75f);
@@ -614,19 +643,19 @@ struct editor
                     glVertex2f(x+psx, y+pey+FONTH);
                 }
                 else
-                {   glVertex2f(x+psx,        y+psy);
-                    glVertex2f(x+psx,        y+psy+FONTH);
+                {   glVertex2f(x+psx, y+psy);
+                    glVertex2f(x+psx, y+psy+FONTH);
                     glVertex2f(x+pixelwidth, y+psy+FONTH);
                     glVertex2f(x+pixelwidth, y+psy);
                     if(pey-psy > FONTH)
                     {
-                        glVertex2f(x,            y+psy+FONTH);
+                        glVertex2f(x, y+psy+FONTH);
                         glVertex2f(x+pixelwidth, y+psy+FONTH);
                         glVertex2f(x+pixelwidth, y+pey);
-                        glVertex2f(x,            y+pey);
+                        glVertex2f(x, y+pey);
                     }
-                    glVertex2f(x,     y+pey);
-                    glVertex2f(x,     y+pey+FONTH);
+                    glVertex2f(x, y+pey);
+                    glVertex2f(x, y+pey+FONTH);
                     glVertex2f(x+pex, y+pey+FONTH);
                     glVertex2f(x+pex, y+pey);
                 }
@@ -636,29 +665,27 @@ struct editor
             }
         }
 
-        int h = 0;
         for(int i = starty; i < lines.length(); i++)
         {
             int width, height;
             text_bounds(lines[i].text, width, height, maxwidth, TEXT_NO_INDENT);
-            if(h + height > pixelheight) break;
-
-            draw_text(lines[i].text, x, y+h, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, TEXT_NO_INDENT, hit&&(cy==i)?cx:-1, maxwidth);
+            if(h+height > pixelheight) break;
+            draw_text(lines[i].text, x, y+h, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, TEXT_NO_INDENT, hit && (cy == i) ? cx : -1, maxwidth);
             if(linewrap && height > FONTH) // line wrap indicator
             {
                 notextureshader->set();
                 glDisable(GL_TEXTURE_2D);
-                glColor3f(0.5f, 0.5f, 0.5f);
+                glColor4f((guifieldbordercolour>>16)/255.f, ((guifieldbordercolour>>8)&0xFF)/255.f, (guifieldbordercolour&0xFF)/255.f, guifieldborderblend);
                 glBegin(GL_TRIANGLE_STRIP);
-                glVertex2f(x,         y+h+FONTH);
-                glVertex2f(x,         y+h+height);
-                glVertex2f(x-FONTW/2, y+h+FONTH);
-                glVertex2f(x-FONTW/2, y+h+height);
+                glVertex2f(x, y+h+FONTH);
+                glVertex2f(x, y+h+height);
+                glVertex2f(x-FONTW/4, y+h+FONTH);
+                glVertex2f(x-FONTW/4, y+h+height);
                 glEnd();
                 glEnable(GL_TEXTURE_2D);
                 defaultshader->set();
             }
-            h+=height;
+            h += height;
         }
     }
 };
@@ -692,7 +719,8 @@ static editor *useeditor(const char *name, int mode, bool focus, const char *ini
         return e;
     }
     editor *e = new editor(name, mode, initval, parent);
-    if(focus) editors.add(e); else editors.insert(0, e);
+    if(focus) editors.add(e);
+    else editors.insert(0, e);
     return e;
 }
 
@@ -734,10 +762,12 @@ TEXTCOMMAND(textmode, "i", (int *m), // (1= keep while focused, 2= keep while us
     else intret(top->mode);
 );
 TEXTCOMMAND(textsave, "s", (char *file),  // saves the topmost (filename is optional)
+    if(identflags&IDF_WORLD) return;
     if(*file) top->setfile(copypath(file));
     top->save();
 );
 TEXTCOMMAND(textload, "s", (char *file), // loads into the topmost editor, returns filename if no args
+    if(identflags&IDF_WORLD) return;
     if(*file)
     {
         top->setfile(copypath(file));
@@ -747,11 +777,12 @@ TEXTCOMMAND(textload, "s", (char *file), // loads into the topmost editor, retur
 );
 TEXTCOMMAND(textinit, "sss", (char *name, char *file, char *initval), // loads into named editor if no file assigned and editor has been rendered
 {
+    char *f = identflags&IDF_WORLD ? NULL : file;
     editor *e = NULL;
     loopv(editors) if(!strcmp(editors[i]->name, name)) { e = editors[i]; break; }
-    if(e && e->rendered && !e->filename && *file && (e->lines.empty() || (e->lines.length() == 1 && !strcmp(e->lines[0].text, initval))))
+    if(e && e->rendered && !e->filename && f && *f && (e->lines.empty() || (e->lines.length() == 1 && !strcmp(e->lines[0].text, initval))))
     {
-        e->setfile(copypath(file));
+        e->setfile(copypath(f));
         e->load();
     }
 });
@@ -774,3 +805,9 @@ TEXTCOMMAND(textexec, "i", (int *selected), // execute script commands from the
     delete[] script;
 );
 
+TEXTCOMMAND(textadd, "ss", (char *name, char *str), // loads into named editor if no file assigned and editor has been rendered
+{
+    editor *e = NULL;
+    loopv(editors) if(!strcmp(editors[i]->name, name)) { e = editors[i]; break; }
+    if(e && e->rendered) e->insert(str);
+});
diff --git a/src/engine/texture.cpp b/src/engine/texture.cpp
index 8d6c57b..4cf1bc2 100644
--- a/src/engine/texture.cpp
+++ b/src/engine/texture.cpp
@@ -1,6 +1,7 @@
 // texture.cpp: texture slot management
 
 #include "engine.h"
+#include "SDL_image.h"
 
 #define FUNCNAME(name) name##1
 #define DEFPIXEL uint OP(r, 0);
@@ -26,6 +27,14 @@
 #define BPP 4
 #include "scale.h"
 
+TVAR(IDF_PERSIST|IDF_PRELOAD, notexturetex, "textures/notexture", 3);
+TVAR(IDF_PERSIST|IDF_PRELOAD, blanktex, "textures/blank", 3);
+TVAR(IDF_PERSIST|IDF_PRELOAD, logotex, "textures/logo", 3);
+TVAR(IDF_PERSIST|IDF_PRELOAD, badgetex, "textures/cube2badge", 3);
+TVAR(IDF_PERSIST|IDF_PRELOAD, emblemtex, "textures/emblem", 3);
+TVAR(IDF_PERSIST|IDF_PRELOAD, nothumbtex, "textures/nothumb", 3);
+TVAR(IDF_PERSIST|IDF_PRELOAD, backgroundtex, "textures/background", 3);
+
 static void scaletexture(uchar *src, uint sw, uint sh, uint bpp, uint pitch, uchar *dst, uint dw, uint dh)
 {
     if(sw == dw*2 && sh == dh*2)
@@ -38,7 +47,7 @@ static void scaletexture(uchar *src, uint sw, uint sh, uint bpp, uint pitch, uch
             case 4: return halvetexture4(src, sw, sh, pitch, dst);
         }
     }
-    else if(sw < dw || sh < dh || sw&(sw-1) || sh&(sh-1))
+    else if(sw < dw || sh < dh || sw&(sw-1) || sh&(sh-1) || dw&(dw-1) || dh&(dh-1))
     {
         switch(bpp)
         {
@@ -97,7 +106,7 @@ static void reorients3tc(GLenum format, int blocksize, int w, int h, uchar *src,
         {
             if(format == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT)
             {
-                ullong salpha = lilswap(*(ullong *)src), dalpha = 0;
+                ullong salpha = lilswap(*(const ullong *)src), dalpha = 0;
                 uint xmask = flipx ? 15 : 0, ymask = flipy ? 15 : 0, xshift = 2, yshift = 4;
                 if(swapxy) swap(xshift, yshift);
                 for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
@@ -112,7 +121,7 @@ static void reorients3tc(GLenum format, int blocksize, int w, int h, uchar *src,
             else if(format == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT)
             {
                 uchar alpha1 = src[0], alpha2 = src[1];
-                ullong salpha = lilswap(*(ushort *)&src[2]) + ((ullong)lilswap(*(ushort *)&src[4])<<16) + ((ullong)lilswap(*(ushort *)&src[6])<<32), dalpha = 0;
+                ullong salpha = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16), dalpha = 0;
                 uint xmask = flipx ? 7 : 0, ymask = flipy ? 7 : 0, xshift = 0, yshift = 2;
                 if(swapxy) swap(xshift, yshift);
                 for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
@@ -129,8 +138,8 @@ static void reorients3tc(GLenum format, int blocksize, int w, int h, uchar *src,
                 curdst += 8;
             }
 
-            ushort color1 = lilswap(*(ushort *)src), color2 = lilswap(*(ushort *)&src[2]);
-            uint sbits = lilswap(*(uint *)&src[4]);
+            ushort color1 = lilswap(*(const ushort *)src), color2 = lilswap(*(const ushort *)&src[2]);
+            uint sbits = lilswap(*(const uint *)&src[4]);
             if(normals)
             {
                 ushort ncolor1 = color1, ncolor2 = color2;
@@ -517,14 +526,14 @@ void texagrad(ImageData &s, float x2, float y2, float x1, float y1)
 
 VAR(0, hwtexsize, 1, 0, 0);
 VAR(0, hwcubetexsize, 1, 0, 0);
-VAR(0, hwmaxaniso, 1, 0, 0);
+VAR(0, hwmaxanisotropy, 1, 0, 0);
 VARF(IDF_PERSIST, maxtexsize, 0, 0, 1<<12, initwarning("texture quality", INIT_LOAD));
 VARF(IDF_PERSIST, texreduce, 0, 0, 12, initwarning("texture quality", INIT_LOAD));
 VARF(IDF_PERSIST, texcompress, 0, 1<<10, 1<<12, initwarning("texture quality", INIT_LOAD));
 VARF(IDF_PERSIST, texcompressquality, -1, -1, 1, setuptexcompress());
 VARF(IDF_PERSIST, trilinear, 0, 1, 1, initwarning("texture filtering", INIT_LOAD));
 VARF(IDF_PERSIST, bilinear, 0, 1, 1, initwarning("texture filtering", INIT_LOAD));
-VARF(IDF_PERSIST, aniso, 0, 0, 16, initwarning("texture filtering", INIT_LOAD));
+VARF(IDF_PERSIST, anisotropy, 0, 0, 16, initwarning("texture filtering", INIT_LOAD));
 
 extern int usetexcompress;
 
@@ -702,7 +711,7 @@ void setuptexparameters(int tnum, void *pixels, int clamp, int filter, GLenum fo
     glBindTexture(target, tnum);
     glTexParameteri(target, GL_TEXTURE_WRAP_S, clamp&1 ? GL_CLAMP_TO_EDGE : GL_REPEAT);
     if(target!=GL_TEXTURE_1D) glTexParameteri(target, GL_TEXTURE_WRAP_T, clamp&2 ? GL_CLAMP_TO_EDGE : GL_REPEAT);
-    if(target==GL_TEXTURE_2D && hasAF && min(aniso, hwmaxaniso) > 0 && filter > 1) glTexParameteri(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, min(aniso, hwmaxaniso));
+    if(target==GL_TEXTURE_2D && hasAF && min(anisotropy, hwmaxanisotropy) > 0 && filter > 1) glTexParameteri(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, min(anisotropy, hwmaxanisotropy));
     glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filter && bilinear ? GL_LINEAR : GL_NEAREST);
     glTexParameteri(target, GL_TEXTURE_MIN_FILTER,
         filter > 1 ?
@@ -785,15 +794,14 @@ Texture *notexture = NULL, *blanktexture = NULL; // used as default, ensured to
 static void updatetexture(Texture *t)
 {
     if(t->frames.length() <= 1 || t->delay <= 0) return;
-    int elapsed = lastmillis - t->last;
+    int elapsed = lastmillis-t->last;
     if(elapsed < t->delay) return;
     int animlen = t->throb ? (t->frames.length()-1)*2 : t->frames.length();
     t->frame += elapsed/t->delay;
     t->frame %= animlen;
-    t->last = lastmillis - lastmillis%t->delay;
-    t->id = t->frames[t->throb && t->frame >= t->frames.length() ? animlen - t->frame : t->frame];
-    if(t->id <= 0)
-         t->id = t != notexture ? notexture->id : 0;
+    t->last = lastmillis-(lastmillis%t->delay);
+    int frame = t->throb && t->frame >= t->frames.length() ? animlen-t->frame : t->frame;
+    t->id = t->frames.inrange(frame) ? t->frames[frame] : 0;
 }
 
 void updatetextures()
@@ -1179,6 +1187,7 @@ static vec parsevec(const char *arg)
 }
 
 VAR(0, usedds, 0, 1, 1);
+VAR(0, scaledds, 0, 2, 4);
 
 static bool texturedata(ImageData &d, const char *tname, Slot::Tex *tex = NULL, bool msg = true, int *compress = NULL, TextureAnim *anim = NULL)
 {
@@ -1192,7 +1201,7 @@ static bool texturedata(ImageData &d, const char *tname, Slot::Tex *tex = NULL,
         else file++;
     }
 
-    if(!file) { if(msg) conoutf("\frcould not load texture: %s", tname); return NULL; }
+    if(!file) { if(msg) conoutf("\frcould not load texture: %s", tname); return false; }
 
     bool raw = !usedds || !compress, dds = false;
     for(const char *pcmds = cmds; pcmds;)
@@ -1211,121 +1220,130 @@ static bool texturedata(ImageData &d, const char *tname, Slot::Tex *tex = NULL,
                 else arg[i]++; \
             }
         PARSETEXCOMMANDS(pcmds);
-        if(!strncmp(cmd, "noff", len))
+        if(matchstring(cmd, len, "noff"))
         {
             if(renderpath==R_FIXEDFUNCTION) return true;
         }
-        else if(!strncmp(cmd, "ffmask", len) || !strncmp(cmd, "ffskip", len))
+        else if(matchstring(cmd, len, "ffmask") || matchstring(cmd, len, "ffskip"))
         {
             if(renderpath==R_FIXEDFUNCTION) raw = true;
         }
-        else if(!strncmp(cmd, "decal", len))
+        else if(matchstring(cmd, len, "decal"))
         {
             if(renderpath==R_FIXEDFUNCTION && !hasTE) raw = true;
         }
-        else if(!strncmp(cmd, "dds", len)) dds = true;
-        else if(!strncmp(cmd, "thumbnail", len)) raw = true;
-        else if(!strncmp(cmd, "stub", len)) return loadsurface(file, true)!=NULL;
+        else if(matchstring(cmd, len, "dds")) dds = true;
+        else if(matchstring(cmd, len, "thumbnail")) raw = true;
+        else if(matchstring(cmd, len, "stub")) return loadsurface(file, true)!=NULL;
     }
 
     if(msg) progress(loadprogress, file);
 
     int flen = strlen(file);
-    if((flen >= 4 && !strcasecmp(file + flen - 4, ".dds")) || dds)
+    if((flen >= 4 && !strcasecmp(file + flen - 4, ".dds")) || (dds && !raw))
     {
         string dfile;
         copystring(dfile, file);
         if(flen >= 4 && dfile[flen-4]=='.') memcpy(dfile + flen - 4, ".dds", 4);
         else concatstring(dfile, ".dds");
-        if(!raw && hasS3TC && loaddds(dfile, d)) return true;
-        if(!dds) { if(msg) conoutf("\frcould not load texture %s", dfile); return false; }
+        if(!loaddds(dfile, d, raw ? 1 : (dds ? 0 : -1)) && (!dds || raw))
+        {
+            if(msg) conoutf("\frcould not load texture %s", dfile);
+            return false;
+        }
+        if(d.data && !d.compressed && !dds && compress) *compress = scaledds;
     }
 
-    SDL_Surface *s = loadsurface(file);
-    if(!s) { if(msg) conoutf("\frcould not load texture %s", file); return false; }
-    int bpp = s->format->BitsPerPixel;
-    if(bpp%8 || !texformat(bpp/8)) { SDL_FreeSurface(s); conoutf("\frtexture must be 8, 16, 24, or 32 bpp: %s", file); return false; }
-    d.wrap(s);
+    if(!d.data)
+    {
+        SDL_Surface *s = loadsurface(file);
+        if(!s) { if(msg) conoutf("\frcould not load texture %s", file); return false; }
+        int bpp = s->format->BitsPerPixel;
+        if(bpp%8 || !texformat(bpp/8)) { SDL_FreeSurface(s); conoutf("\frtexture must be 8, 16, 24, or 32 bpp: %s", file); return false; }
+        d.wrap(s);
+    }
 
-    while(cmds)
+    if(!d.compressed) while(cmds)
     {
         PARSETEXCOMMANDS(cmds);
-        if(!strncmp(cmd, "mad", len)) texmad(d, parsevec(arg[0]), parsevec(arg[1]));
-        else if(!strncmp(cmd, "colorify", len)) texcolorify(d, parsevec(arg[0]), parsevec(arg[1]));
-        else if(!strncmp(cmd, "colormask", len)) texcolormask(d, parsevec(arg[0]), *arg[1] ? parsevec(arg[1]) : vec(1, 1, 1));
+        if(matchstring(cmd, len, "mad")) texmad(d, parsevec(arg[0]), parsevec(arg[1]));
+        else if(matchstring(cmd, len, "colorify")) texcolorify(d, parsevec(arg[0]), parsevec(arg[1]));
+        else if(matchstring(cmd, len, "colormask")) texcolormask(d, parsevec(arg[0]), *arg[1] ? parsevec(arg[1]) : vec(1, 1, 1));
 
-        else if(!strncmp(cmd, "ffmask", len))
+        else if(matchstring(cmd, len, "ffmask"))
         {
             texffmask(d, atof(arg[0]), atof(arg[1]));
             if(!d.data) break;
         }
-        else if(!strncmp(cmd, "normal", len))
+        else if(matchstring(cmd, len, "normal"))
         {
             int emphasis = atoi(arg[0]);
             texnormal(d, emphasis > 0 ? emphasis : 3);
         }
-        else if(!strncmp(cmd, "dup", len)) texdup(d, atoi(arg[0]), atoi(arg[1]));
-        else if(!strncmp(cmd, "decal", len)) texdecal(d);
-        else if(!strncmp(cmd, "offset", len)) texoffset(d, atoi(arg[0]), atoi(arg[1]));
-        else if(!strncmp(cmd, "rotate", len)) texrotate(d, atoi(arg[0]), tex ? tex->type : 0);
-        else if(!strncmp(cmd, "reorient", len)) texreorient(d, atoi(arg[0])>0, atoi(arg[1])>0, atoi(arg[2])>0, tex ? tex->type : TEX_DIFFUSE);
-        else if(!strncmp(cmd, "crop", len)) texcrop(d, atoi(arg[0]), atoi(arg[1]), atoi(arg[2]), atoi(arg[3]));
-        else if(!strncmp(cmd, "mix", len)) texmix(d, *arg[0] ? atoi(arg[0]) : -1, *arg[1] ? atoi(arg[1]) : -1, *arg[2] ? atoi(arg[2]) : -1, *arg[3] ? atoi(arg[3]) : -1);
-        else if(!strncmp(cmd, "grey", len)) texgrey(d);
-        else if(!strncmp(cmd, "blur", len))
+        else if(matchstring(cmd, len, "dup")) texdup(d, atoi(arg[0]), atoi(arg[1]));
+        else if(matchstring(cmd, len, "decal")) texdecal(d);
+        else if(matchstring(cmd, len, "offset")) texoffset(d, atoi(arg[0]), atoi(arg[1]));
+        else if(matchstring(cmd, len, "rotate")) texrotate(d, atoi(arg[0]), tex ? tex->type : 0);
+        else if(matchstring(cmd, len, "reorient")) texreorient(d, atoi(arg[0])>0, atoi(arg[1])>0, atoi(arg[2])>0, tex ? tex->type : TEX_DIFFUSE);
+        else if(matchstring(cmd, len, "crop")) texcrop(d, atoi(arg[0]), atoi(arg[1]), atoi(arg[2]), atoi(arg[3]));
+        else if(matchstring(cmd, len, "mix")) texmix(d, *arg[0] ? atoi(arg[0]) : -1, *arg[1] ? atoi(arg[1]) : -1, *arg[2] ? atoi(arg[2]) : -1, *arg[3] ? atoi(arg[3]) : -1);
+        else if(matchstring(cmd, len, "grey")) texgrey(d);
+        else if(matchstring(cmd, len, "blur"))
         {
             int emphasis = atoi(arg[0]), repeat = atoi(arg[1]);
             texblur(d, emphasis > 0 ? clamp(emphasis, 1, 2) : 1, repeat > 0 ? repeat : 1);
         }
-        else if(!strncmp(cmd, "premul", len)) texpremul(d);
-        else if(!strncmp(cmd, "agrad", len)) texagrad(d, atof(arg[0]), atof(arg[1]), atof(arg[2]), atof(arg[3]));
-        else if(!strncmp(cmd, "compress", len) || !strncmp(cmd, "dds", len))
+        else if(matchstring(cmd, len, "premul")) texpremul(d);
+        else if(matchstring(cmd, len, "agrad")) texagrad(d, atof(arg[0]), atof(arg[1]), atof(arg[2]), atof(arg[3]));
+        else if(matchstring(cmd, len, "compress") || matchstring(cmd, len, "dds"))
         {
             int scale = atoi(arg[0]);
-            if(scale <= 0) scale = 2;
+            if(scale <= 0) scale = scaledds;
             if(compress) *compress = scale;
         }
-        else if(!strncmp(cmd, "nocompress", len))
+        else if(matchstring(cmd, len, "nocompress"))
         {
             if(compress) *compress = -1;
         }
-        else if(!strncmp(cmd, "anim", len))
+        else if(matchstring(cmd, len, "anim"))
         {
             if(anim)
             {
                 anim->delay = arg[0] ? atoi(arg[0]) : 50;
-                anim->x = arg[1] ? atoi(arg[1]) : 1;
-                anim->y = arg[2] ? atoi(arg[2]) : 2;
+                anim->x = min(1, arg[1] ? atoi(arg[1]) : 1);
+                anim->y = min(1, arg[2] ? atoi(arg[2]) : 2);
                 anim->throb = arg[3] && atoi(arg[3]);
                 anim->w = d.w/anim->x;
                 anim->h = d.h/anim->y;
                 anim->count = anim->x*anim->y;
             }
         }
-        else if(!strncmp(cmd, "thumbnail", len))
+        else if(matchstring(cmd, len, "thumbnail"))
         {
             int w = atoi(arg[0]), h = atoi(arg[1]);
             if(w <= 0 || w > (1<<12)) w = 64;
             if(h <= 0 || h > (1<<12)) h = w;
             if(d.w > w || d.h > h) scaleimage(d, w, h);
         }
-        else if(!strncmp(cmd, "ffskip", len))
+        else if(matchstring(cmd, len, "ffskip"))
         {
             if(renderpath==R_FIXEDFUNCTION) break;
         }
     }
+
     if((anim && anim->count ? max(anim->w, anim->h) : max(d.w, d.h)) > (1<<12))
     {
         d.cleanup();
         conoutf("\frtexture size exceeded %dx%d: %s", 1<<12, 1<<12, file);
         return false;
     }
+
     return true;
 }
 
 void loadalphamask(Texture *t)
 {
-    if(t->alphamask || (t->type&(Texture::ALPHA|Texture::COMPRESSED)) != Texture::ALPHA) return;
+    if(t->alphamask || !(t->type&Texture::ALPHA)) return;
     ImageData s;
     if(!texturedata(s, t->name, NULL, false) || !s.data || s.compressed) return;
     t->alphamask = new uchar[s.h * ((s.w+7)/8)];
@@ -1385,6 +1403,12 @@ void resettextures(int n)
         delete s;
     }
     slots.setsize(limit);
+    while(vslots.length())
+    {
+        VSlot *vs = vslots.last();
+        if(vs->slot != &dummyslot || vs->changed) break;
+        delete vslots.pop();
+    }
 }
 
 ICOMMAND(0, texturereset, "i", (int *n), if(editmode || identflags&IDF_WORLD) resettextures(*n););
@@ -2593,7 +2617,7 @@ GLuint genenvmap(const vec &o, int envmapsize, int blur)
         }
         if(blur > 0)
         {
-            blurtexture(blur, 3, texsize, texsize, src, dst);
+            blurtexture(blur, 3, texsize, texsize, dst, src);
             swap(src, dst);
         }
         createtexture(tex, texsize, texsize, src, 3, 2, GL_RGB5, side.target);
@@ -2839,7 +2863,123 @@ struct DDSURFACEDESC2
 
 VAR(0, dbgdds, 0, 0, 1);
 
-bool loaddds(const char *filename, ImageData &image)
+#define DECODEDDS(name, dbpp, initblock, writeval, nextval) \
+static void name(ImageData &s) \
+{ \
+    ImageData d(s.w, s.h, dbpp); \
+    uchar *dst = d.data; \
+    const uchar *src = s.data; \
+    for(int by = 0; by < s.h; by += s.align) \
+    { \
+        for(int bx = 0; bx < s.w; bx += s.align, src += s.bpp) \
+        { \
+            int maxy = min(d.h - by, s.align), maxx = min(d.w - bx, s.align); \
+            initblock; \
+            loop(y, maxy) \
+            { \
+                int x; \
+                for(x = 0; x < maxx; ++x) \
+                { \
+                    writeval; \
+                    nextval; \
+                    dst += d.bpp; \
+                }  \
+                for(; x < s.align; ++x) { nextval; } \
+                dst += d.pitch - maxx*d.bpp; \
+            } \
+            dst += maxx*d.bpp - maxy*d.pitch; \
+        } \
+        dst += (s.align-1)*d.pitch; \
+    } \
+    s.replace(d); \
+}
+
+DECODEDDS(decodedxt1, s.compressed == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 4 : 3,
+    ushort color0 = lilswap(*(const ushort *)src);
+    ushort color1 = lilswap(*(const ushort *)&src[2]);
+    uint bits = lilswap(*(const uint *)&src[4]);
+    bvec4 rgba[4];
+    rgba[0] = bvec4(bvec::from565(color0), 0xFF);
+    rgba[1] = bvec4(bvec::from565(color1), 0xFF);
+    if(color0 > color1)
+    {
+        rgba[2].lerp(rgba[0], rgba[1], 2, 1, 3);
+        rgba[3].lerp(rgba[0], rgba[1], 1, 2, 3);
+    }
+    else
+    {
+        rgba[2].lerp(rgba[0], rgba[1], 1, 1, 2);
+        rgba[3] = bvec4(0, 0, 0, 0);
+    }
+,
+    memcpy(dst, rgba[bits&3].v, d.bpp);
+,
+    bits >>= 2;
+);
+
+DECODEDDS(decodedxt3, 4,
+    ullong alpha = lilswap(*(const ullong *)src);
+    ushort color0 = lilswap(*(const ushort *)&src[8]);
+    ushort color1 = lilswap(*(const ushort *)&src[10]);
+    uint bits = lilswap(*(const uint *)&src[12]);
+    bvec rgb[4];
+    rgb[0] = bvec::from565(color0);
+    rgb[1] = bvec::from565(color1);
+    rgb[2].lerp(rgb[0], rgb[1], 2, 1, 3);
+    rgb[3].lerp(rgb[0], rgb[1], 1, 2, 3);
+,
+    memcpy(dst, rgb[bits&3].v, 3);
+    dst[3] = ((alpha&0xF)*1088 + 32) >> 6;
+,
+    bits >>= 2;
+    alpha >>= 4;
+);
+
+static inline void decodealpha(uchar alpha0, uchar alpha1, uchar alpha[8])
+{
+    alpha[0] = alpha0;
+    alpha[1] = alpha1;
+    if(alpha0 > alpha1)
+    {
+        alpha[2] = (6*alpha0 + alpha1)/7;
+        alpha[3] = (5*alpha0 + 2*alpha1)/7;
+        alpha[4] = (4*alpha0 + 3*alpha1)/7;
+        alpha[5] = (3*alpha0 + 4*alpha1)/7;
+        alpha[6] = (2*alpha0 + 5*alpha1)/7;
+        alpha[7] = (alpha0 + 6*alpha1)/7;
+    }
+    else
+    {
+        alpha[2] = (4*alpha0 + alpha1)/5;
+        alpha[3] = (3*alpha0 + 2*alpha1)/5;
+        alpha[4] = (2*alpha0 + 3*alpha1)/5;
+        alpha[5] = (alpha0 + 4*alpha1)/5;
+        alpha[6] = 0;
+        alpha[7] = 0xFF;
+    }
+}
+
+DECODEDDS(decodedxt5, 4,
+    uchar alpha[8];
+    decodealpha(src[0], src[1], alpha);
+    ullong alphabits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16);
+    ushort color0 = lilswap(*(const ushort *)&src[8]);
+    ushort color1 = lilswap(*(const ushort *)&src[10]);
+    uint bits = lilswap(*(const uint *)&src[12]);
+    bvec rgb[4];
+    rgb[0] = bvec::from565(color0);
+    rgb[1] = bvec::from565(color1);
+    rgb[2].lerp(rgb[0], rgb[1], 2, 1, 3);
+    rgb[3].lerp(rgb[0], rgb[1], 1, 2, 3);
+,
+    memcpy(dst, rgb[bits&3].v, 3);
+    dst[3] = alpha[alphabits&7];
+,
+    bits >>= 2;
+    alphabits >>= 3;
+);
+
+bool loaddds(const char *filename, ImageData &image, int force)
 {
     stream *f = openfile(filename, "rb");
     if(!f) return false;
@@ -2850,18 +2990,25 @@ bool loaddds(const char *filename, ImageData &image)
     if(f->read(&d, sizeof(d)) != sizeof(d)) { delete f; return false; }
     lilswap((uint *)&d, sizeof(d)/sizeof(uint));
     if(d.dwSize != sizeof(DDSURFACEDESC2) || d.ddpfPixelFormat.dwSize != sizeof(DDPIXELFORMAT)) { delete f; return false; }
+    bool supported = false;
     if(d.ddpfPixelFormat.dwFlags & DDPF_FOURCC)
     {
         switch(d.ddpfPixelFormat.dwFourCC)
         {
-            case FOURCC_DXT1: format = d.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT; break;
+            case FOURCC_DXT1:
+                if((supported = hasS3TC) || force) format = d.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
+                break;
             case FOURCC_DXT2:
-            case FOURCC_DXT3: format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; break;
+            case FOURCC_DXT3:
+                if((supported = hasS3TC) || force) format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
+                break;
             case FOURCC_DXT4:
-            case FOURCC_DXT5: format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break;
+            case FOURCC_DXT5:
+                if((supported = hasS3TC) || force) format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
+                break;
         }
     }
-    if(!format) { delete f; return false; }
+    if(!format || (!supported && !force)) { delete f; return false; }
     if(dbgdds) conoutf("%s: format 0x%X, %d x %d, %d mipmaps", filename, format, d.dwWidth, d.dwHeight, d.dwMipMapCount);
     int bpp = 0;
     switch(format)
@@ -2871,10 +3018,23 @@ bool loaddds(const char *filename, ImageData &image)
         case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
         case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: bpp = 16; break;
     }
-    image.setdata(NULL, d.dwWidth, d.dwHeight, bpp, d.dwMipMapCount, 4, format);
-    int size = image.calcsize();
+    image.setdata(NULL, d.dwWidth, d.dwHeight, bpp, !supported || force > 0 ? 1 : d.dwMipMapCount, 4, format);
+    size_t size = image.calcsize();
     if(f->read(image.data, size) != size) { delete f; image.cleanup(); return false; }
     delete f;
+    if(!supported || force > 0) switch(format)
+    {
+        case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+        case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+            decodedxt1(image);
+            break;
+        case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+           decodedxt3(image);
+            break;
+        case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+            decodedxt5(image);
+            break;
+    }
     return true;
 }
 
@@ -3215,7 +3375,9 @@ SDL_Surface *loadsurface(const char *name, bool noload)
             SDL_RWops *rw = z->rwops();
             if(rw)
             {
-                s = IMG_Load_RW(rw, 0);
+                char *ext = (char *)strrchr(name, '.');
+                if(ext) ++ext;
+                s = IMG_LoadTyped_RW(rw, 0, ext);
                 SDL_FreeRW(rw);
             }
             delete z;
diff --git a/src/engine/texture.h b/src/engine/texture.h
index 7302f65..5cee7c5 100644
--- a/src/engine/texture.h
+++ b/src/engine/texture.h
@@ -1,200 +1,3 @@
-// GL_ARB_vertex_program, GL_ARB_fragment_program
-extern PFNGLGENPROGRAMSARBPROC              glGenProgramsARB_;
-extern PFNGLDELETEPROGRAMSARBPROC           glDeleteProgramsARB_;
-extern PFNGLBINDPROGRAMARBPROC              glBindProgramARB_;
-extern PFNGLPROGRAMSTRINGARBPROC            glProgramStringARB_;
-extern PFNGLGETPROGRAMIVARBPROC             glGetProgramivARB_;
-extern PFNGLPROGRAMENVPARAMETER4FARBPROC    glProgramEnvParameter4fARB_;
-extern PFNGLPROGRAMENVPARAMETER4FVARBPROC   glProgramEnvParameter4fvARB_;
-
-// GL_EXT_gpu_program_parameters
-#ifndef GL_EXT_gpu_program_parameters
-#define GL_EXT_gpu_program_parameters 1
-typedef void (APIENTRYP PFNGLPROGRAMENVPARAMETERS4FVEXTPROC) (GLenum target, GLuint index, GLsizei count, const GLfloat *params);
-typedef void (APIENTRYP PFNGLPROGRAMLOCALPARAMETERS4FVEXTPROC) (GLenum target, GLuint index, GLsizei count, const GLfloat *params);
-#endif
-
-extern PFNGLPROGRAMENVPARAMETERS4FVEXTPROC   glProgramEnvParameters4fv_;
-extern PFNGLPROGRAMLOCALPARAMETERS4FVEXTPROC glProgramLocalParameters4fv_;
-
-#ifndef GL_VERSION_2_1
-#define GL_VERSION_2_1 1
-#define GL_FLOAT_MAT2x3                   0x8B65
-#define GL_FLOAT_MAT2x4                   0x8B66
-#define GL_FLOAT_MAT3x2                   0x8B67
-#define GL_FLOAT_MAT3x4                   0x8B68
-#define GL_FLOAT_MAT4x2                   0x8B69
-#define GL_FLOAT_MAT4x3                   0x8B6A
-typedef void (APIENTRYP PFNGLUNIFORMMATRIX2X3FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
-typedef void (APIENTRYP PFNGLUNIFORMMATRIX3X2FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
-typedef void (APIENTRYP PFNGLUNIFORMMATRIX2X4FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
-typedef void (APIENTRYP PFNGLUNIFORMMATRIX4X2FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
-typedef void (APIENTRYP PFNGLUNIFORMMATRIX3X4FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
-typedef void (APIENTRYP PFNGLUNIFORMMATRIX4X3FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
-#endif
-
-// OpenGL 2.0: GL_ARB_shading_language_100, GL_ARB_shader_objects, GL_ARB_fragment_shader, GL_ARB_vertex_shader
-#ifdef __APPLE__
-#define glCreateProgram_ glCreateProgram
-#define glDeleteProgram_ glDeleteProgram
-#define glUseProgram_ glUseProgram
-#define glCreateShader_ glCreateShader
-#define glDeleteShader_ glDeleteShader
-#define glShaderSource_ glShaderSource
-#define glCompileShader_ glCompileShader
-#define glGetShaderiv_ glGetShaderiv
-#define glGetProgramiv_ glGetProgramiv
-#define glAttachShader_ glAttachShader
-#define glGetProgramInfoLog_ glGetProgramInfoLog
-#define glGetShaderInfoLog_ glGetShaderInfoLog
-#define glLinkProgram_ glLinkProgram
-#define glGetUniformLocation_ glGetUniformLocation
-#define glUniform1f_ glUniform1f
-#define glUniform2f_ glUniform2f
-#define glUniform3f_ glUniform3f
-#define glUniform4f_ glUniform4f
-#define glUniform1fv_ glUniform1fv
-#define glUniform2fv_ glUniform2fv
-#define glUniform3fv_ glUniform3fv
-#define glUniform4fv_ glUniform4fv
-#define glUniform1i_ glUniform1i
-#define glUniformMatrix2fv_ glUniformMatrix2fv
-#define glUniformMatrix3fv_ glUniformMatrix3fv
-#define glUniformMatrix4fv_ glUniformMatrix4fv
-#define glBindAttribLocation_ glBindAttribLocation
-#define glGetActiveUniform_ glGetActiveUniform
-#define glEnableVertexAttribArray_ glEnableVertexAttribArray
-#define glDisableVertexAttribArray_ glDisableVertexAttribArray
-#define glVertexAttribPointer_ glVertexAttribPointer
-
-#define glUniformMatrix2x3fv_ glUniformMatrix2x3fv
-#define glUniformMatrix3x2fv_ glUniformMatrix3x2fv
-#define glUniformMatrix2x4fv_ glUniformMatrix2x4fv
-#define glUniformMatrix4x2fv_ glUniformMatrix4x2fv
-#define glUniformMatrix3x4fv_ glUniformMatrix3x4fv
-#define glUniformMatrix4x3fv_ glUniformMatrix4x3fv
-#else
-extern PFNGLCREATEPROGRAMPROC            glCreateProgram_;
-extern PFNGLDELETEPROGRAMPROC            glDeleteProgram_;
-extern PFNGLUSEPROGRAMPROC               glUseProgram_;
-extern PFNGLCREATESHADERPROC             glCreateShader_;
-extern PFNGLDELETESHADERPROC             glDeleteShader_;
-extern PFNGLSHADERSOURCEPROC             glShaderSource_;
-extern PFNGLCOMPILESHADERPROC            glCompileShader_;
-extern PFNGLGETSHADERIVPROC              glGetShaderiv_;
-extern PFNGLGETPROGRAMIVPROC             glGetProgramiv_;
-extern PFNGLATTACHSHADERPROC             glAttachShader_;
-extern PFNGLGETPROGRAMINFOLOGPROC        glGetProgramInfoLog_;
-extern PFNGLGETSHADERINFOLOGPROC         glGetShaderInfoLog_;
-extern PFNGLLINKPROGRAMPROC              glLinkProgram_;
-extern PFNGLGETUNIFORMLOCATIONPROC       glGetUniformLocation_;
-extern PFNGLUNIFORM1FPROC                glUniform1f_;
-extern PFNGLUNIFORM2FPROC                glUniform2f_;
-extern PFNGLUNIFORM3FPROC                glUniform3f_;
-extern PFNGLUNIFORM4FPROC                glUniform4f_;
-extern PFNGLUNIFORM1FVPROC               glUniform1fv_;
-extern PFNGLUNIFORM2FVPROC               glUniform2fv_;
-extern PFNGLUNIFORM3FVPROC               glUniform3fv_;
-extern PFNGLUNIFORM4FVPROC               glUniform4fv_;
-extern PFNGLUNIFORM1IPROC                glUniform1i_;
-extern PFNGLBINDATTRIBLOCATIONPROC       glBindAttribLocation_;
-extern PFNGLGETACTIVEUNIFORMPROC         glGetActiveUniform_;
-extern PFNGLENABLEVERTEXATTRIBARRAYPROC  glEnableVertexAttribArray_;
-extern PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray_;
-extern PFNGLVERTEXATTRIBPOINTERPROC      glVertexAttribPointer_;
-
-extern PFNGLUNIFORMMATRIX2X3FVPROC       glUniformMatrix2x3fv_;
-extern PFNGLUNIFORMMATRIX3X2FVPROC       glUniformMatrix3x2fv_;
-extern PFNGLUNIFORMMATRIX2X4FVPROC       glUniformMatrix2x4fv_;
-extern PFNGLUNIFORMMATRIX4X2FVPROC       glUniformMatrix4x2fv_;
-extern PFNGLUNIFORMMATRIX3X4FVPROC       glUniformMatrix3x4fv_;
-extern PFNGLUNIFORMMATRIX4X3FVPROC       glUniformMatrix4x3fv_;
-#endif
-
-#ifndef GL_ARB_uniform_buffer_object
-#define GL_ARB_uniform_buffer_object 1
-#define GL_UNIFORM_BUFFER                 0x8A11
-#define GL_UNIFORM_BUFFER_BINDING         0x8A28
-#define GL_UNIFORM_BUFFER_START           0x8A29
-#define GL_UNIFORM_BUFFER_SIZE            0x8A2A
-#define GL_MAX_VERTEX_UNIFORM_BLOCKS      0x8A2B
-#define GL_MAX_GEOMETRY_UNIFORM_BLOCKS    0x8A2C
-#define GL_MAX_FRAGMENT_UNIFORM_BLOCKS    0x8A2D
-#define GL_MAX_COMBINED_UNIFORM_BLOCKS    0x8A2E
-#define GL_MAX_UNIFORM_BUFFER_BINDINGS    0x8A2F
-#define GL_MAX_UNIFORM_BLOCK_SIZE         0x8A30
-#define GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS 0x8A31
-#define GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS 0x8A32
-#define GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS 0x8A33
-#define GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT 0x8A34
-#define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35
-#define GL_ACTIVE_UNIFORM_BLOCKS          0x8A36
-#define GL_UNIFORM_TYPE                   0x8A37
-#define GL_UNIFORM_SIZE                   0x8A38
-#define GL_UNIFORM_NAME_LENGTH            0x8A39
-#define GL_UNIFORM_BLOCK_INDEX            0x8A3A
-#define GL_UNIFORM_OFFSET                 0x8A3B
-#define GL_UNIFORM_ARRAY_STRIDE           0x8A3C
-#define GL_UNIFORM_MATRIX_STRIDE          0x8A3D
-#define GL_UNIFORM_IS_ROW_MAJOR           0x8A3E
-#define GL_UNIFORM_BLOCK_BINDING          0x8A3F
-#define GL_UNIFORM_BLOCK_DATA_SIZE        0x8A40
-#define GL_UNIFORM_BLOCK_NAME_LENGTH      0x8A41
-#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS  0x8A42
-#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES 0x8A43
-#define GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER 0x8A44
-#define GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER 0x8A45
-#define GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER 0x8A46
-#define GL_INVALID_INDEX                  0xFFFFFFFFu
-
-typedef void (APIENTRYP PFNGLGETUNIFORMINDICESPROC) (GLuint program, GLsizei uniformCount, const GLchar* *uniformNames, GLuint *uniformIndices);
-typedef void (APIENTRYP PFNGLGETACTIVEUNIFORMSIVPROC) (GLuint program, GLsizei uniformCount, const GLuint *uniformIndices, GLenum pname, GLint *params);
-typedef GLuint (APIENTRYP PFNGLGETUNIFORMBLOCKINDEXPROC) (GLuint program, const GLchar *uniformBlockName);
-typedef void (APIENTRYP PFNGLGETACTIVEUNIFORMBLOCKIVPROC) (GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint *params);
-typedef void (APIENTRYP PFNGLUNIFORMBLOCKBINDINGPROC) (GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
-#endif
-
-#ifndef GL_INVALID_INDEX
-#define GL_INVALID_INDEX                  0xFFFFFFFFu
-#endif
-
-#ifndef GL_VERSION_3_0
-#define GL_VERSION_3_0 1
-typedef void (APIENTRYP PFNGLBINDBUFFERRANGEPROC) (GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
-typedef void (APIENTRYP PFNGLBINDBUFFERBASEPROC) (GLenum target, GLuint index, GLuint buffer);
-#elif GL_GLEXT_VERSION < 43
-typedef void (APIENTRYP PFNGLBINDBUFFERRANGEPROC) (GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
-typedef void (APIENTRYP PFNGLBINDBUFFERBASEPROC) (GLenum target, GLuint index, GLuint buffer);
-#endif
-
-// GL_ARB_uniform_buffer_object
-extern PFNGLGETUNIFORMINDICESPROC       glGetUniformIndices_;
-extern PFNGLGETACTIVEUNIFORMSIVPROC     glGetActiveUniformsiv_;
-extern PFNGLGETUNIFORMBLOCKINDEXPROC    glGetUniformBlockIndex_;
-extern PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv_;
-extern PFNGLUNIFORMBLOCKBINDINGPROC     glUniformBlockBinding_;
-extern PFNGLBINDBUFFERBASEPROC          glBindBufferBase_;
-extern PFNGLBINDBUFFERRANGEPROC         glBindBufferRange_;
-
-#ifndef GL_EXT_bindable_uniform
-#define GL_EXT_bindable_uniform 1
-#define GL_MAX_VERTEX_BINDABLE_UNIFORMS_EXT 0x8DE2
-#define GL_MAX_FRAGMENT_BINDABLE_UNIFORMS_EXT 0x8DE3
-#define GL_MAX_GEOMETRY_BINDABLE_UNIFORMS_EXT 0x8DE4
-#define GL_MAX_BINDABLE_UNIFORM_SIZE_EXT  0x8DED
-#define GL_UNIFORM_BUFFER_EXT             0x8DEE
-#define GL_UNIFORM_BUFFER_BINDING_EXT     0x8DEF
-
-typedef void (APIENTRYP PFNGLUNIFORMBUFFEREXTPROC) (GLuint program, GLint location, GLuint buffer);
-typedef GLint (APIENTRYP PFNGLGETUNIFORMBUFFERSIZEEXTPROC) (GLuint program, GLint location);
-typedef GLintptr (APIENTRYP PFNGLGETUNIFORMOFFSETEXTPROC) (GLuint program, GLint location);
-#endif
-
-// GL_EXT_bindable_uniform
-extern PFNGLUNIFORMBUFFEREXTPROC        glUniformBuffer_;
-extern PFNGLGETUNIFORMBUFFERSIZEEXTPROC glGetUniformBufferSize_;
-extern PFNGLGETUNIFORMOFFSETEXTPROC     glGetUniformOffset_;
-
 extern int renderpath;
 
 enum { R_FIXEDFUNCTION = 0, R_ASMSHADER, R_GLSLANG, R_ASMGLSLANG };
@@ -329,6 +132,10 @@ struct Shader
         return (detailshader->variants[row][0]->type&SHADER_OPTION)!=0;
     }
 
+    static inline bool isnull(const Shader *s) { return !s; }
+
+    bool isnull() const { return isnull(this); }
+
     void setvariant_(int col, int row, Shader *fallbackshader)
     {
         Shader *s = fallbackshader;
@@ -343,21 +150,21 @@ struct Shader
 
     void setvariant(int col, int row, Shader *fallbackshader)
     {
-        if(!this || !detailshader || renderpath==R_FIXEDFUNCTION) return;
+        if(isnull() || !detailshader || renderpath==R_FIXEDFUNCTION) return;
         setvariant_(col, row, fallbackshader);
         lastshader->flushenvparams();
     }
 
     void setvariant(int col, int row)
     {
-        if(!this || !detailshader || renderpath==R_FIXEDFUNCTION) return;
+        if(isnull() || !detailshader || renderpath==R_FIXEDFUNCTION) return;
         setvariant_(col, row, detailshader);
         lastshader->flushenvparams();
     }
 
     void setvariant(int col, int row, Slot &slot, VSlot &vslot, Shader *fallbackshader)
     {
-        if(!this || !detailshader || renderpath==R_FIXEDFUNCTION) return;
+        if(isnull() || !detailshader || renderpath==R_FIXEDFUNCTION) return;
         setvariant_(col, row, fallbackshader);
         lastshader->flushenvparams(&slot);
         lastshader->setslotparams(slot, vslot);
@@ -365,7 +172,7 @@ struct Shader
 
     void setvariant(int col, int row, Slot &slot, VSlot &vslot)
     {
-        if(!this || !detailshader || renderpath==R_FIXEDFUNCTION) return;
+        if(isnull() || !detailshader || renderpath==R_FIXEDFUNCTION) return;
         setvariant_(col, row, detailshader);
         lastshader->flushenvparams(&slot);
         lastshader->setslotparams(slot, vslot);
@@ -378,14 +185,14 @@ struct Shader
 
     void set()
     {
-        if(!this || !detailshader || renderpath==R_FIXEDFUNCTION) return;
+        if(isnull() || !detailshader || renderpath==R_FIXEDFUNCTION) return;
         set_();
         lastshader->flushenvparams();
     }
 
     void set(Slot &slot, VSlot &vslot)
     {
-        if(!this || !detailshader || renderpath==R_FIXEDFUNCTION) return;
+        if(isnull() || !detailshader || renderpath==R_FIXEDFUNCTION) return;
         set_();
         lastshader->flushenvparams(&slot);
         lastshader->setslotparams(slot, vslot);
@@ -551,7 +358,7 @@ struct Texture
     GLuint retframe(int cur, int total)
     {
         if(!frames.empty())
-            return frames[::clamp((frames.length()-1)*cur/total, 0, frames.length()-1)];
+            return frames[::clamp((frames.length()-1)*cur/min(1, total), 0, frames.length()-1)];
         return id;
     }
 };
@@ -639,8 +446,10 @@ struct VSlot
         {
             vec c(glowcolor->val);
             if(glowcolor->palette || glowcolor->palindex) c.mul(game::getpalette(glowcolor->palette, glowcolor->palindex));
+            else if(palette || palindex) c.mul(game::getpalette(palette, palindex));
             return c.clamp(0.0f, 1.0f);
         }
+        if(palette || palindex) return game::getpalette(palette, palindex);
         return vec(1, 1, 1);
     }
     vec getpulseglowcolor() const
@@ -649,8 +458,10 @@ struct VSlot
         {
             vec c(pulseglowcolor->val);
             if(pulseglowcolor->palette || pulseglowcolor->palindex) c.mul(game::getpalette(pulseglowcolor->palette, pulseglowcolor->palindex));
+            else if(palette || palindex) c.mul(game::getpalette(palette, palindex));
             return c.clamp(0.0f, 1.0f);
         }
+        //if(palette || palindex) return vec(1, 1, 1).sub(game::getpalette(palette, palindex));
         return vec(0, 0, 0);
     }
 
@@ -764,6 +575,7 @@ enum
     IFMT_MAX,
 };
 extern const char *ifmtexts[IFMT_MAX];
+extern char *notexturetex, *blanktex, *logotex, *badgetex, *emblemtex, *nothumbtex, *backgroundtex;
 extern int imageformat;
 
 extern void savepng(const char *filename, ImageData &image, int compress = 9, bool flip = false);
@@ -771,7 +583,7 @@ extern void savetga(const char *filename, ImageData &image, int compress = 1, bo
 extern void saveimage(const char *name, ImageData &image, int format = IFMT_NONE, int compress = 9, bool flip = false, bool skip = false);
 extern SDL_Surface *loadsurface(const char *name, bool noload = false);
 extern bool loadimage(const char *name, ImageData &image);
-extern bool loaddds(const char *filename, ImageData &image);
+extern bool loaddds(const char *filename, ImageData &image, int force = 0);
 
 extern void resetmaterials();
 extern void resettextures(int n = 0);
diff --git a/src/engine/ui.cpp b/src/engine/ui.cpp
index 1d17100..308cdc0 100644
--- a/src/engine/ui.cpp
+++ b/src/engine/ui.cpp
@@ -2,9 +2,9 @@
 
 int uimillis = -1;
 
-bool layoutpass = false;
-static bool actionon = false;
-int mouseaction[2] = {0}, guibound[2] = {0};
+VAR(IDF_READONLY, guilayoutpass, 1, 0, -1);
+bool guiactionon = false;
+int mouseaction[2] = {0};
 
 static float firstx, firsty;
 
@@ -14,14 +14,65 @@ static bool fieldsactive = false;
 
 VAR(IDF_PERSIST, guishadow, 0, 2, 8);
 VAR(IDF_PERSIST, guiclicktab, 0, 1, 1);
-VAR(IDF_PERSIST, guiblend, 1, 255, 255);
-VAR(IDF_PERSIST, guilinesize, 1, 36, 128);
-VAR(IDF_PERSIST, guisepsize, 1, 10, 128);
+VAR(IDF_PERSIST, guitabborder, 0, 1, 2);
+VAR(IDF_PERSIST, guitextblend, 1, 255, 255);
+VAR(IDF_PERSIST, guitextfade, 1, 200, 255);
+VAR(IDF_PERSIST, guisepsize, 1, 2, 128);
+VAR(IDF_PERSIST, guispacesize, 1, 48, 128);
 VAR(IDF_PERSIST, guiscaletime, 0, 250, VAR_MAX);
-VAR(IDF_PERSIST|IDF_HEX, guibgcolour, -1, 0x888888, 0xFFFFFF);
-VAR(IDF_PERSIST|IDF_HEX, guibordercolour, -1, 0x181818, 0xFFFFFF);
 
-static bool needsinput = false, hastitle = true;
+VAR(IDF_PERSIST, guitooltipwidth, -1, 768, VAR_MAX);
+VAR(IDF_PERSIST, guistatuswidth, -1, 2048, VAR_MAX);
+
+VAR(IDF_PERSIST, guiskinned, 0, 3, 3); // 0 = no backgrounds, 1 = drawn backgrounds, 2 = skinned backgrounds, 3 = skinned with overlay border
+VARF(IDF_PERSIST, guiskinsize, 0, 0, VAR_MAX, if(guiskinsize) { int off = guiskinsize%4; if(off) guiskinsize += guiskinsize-off; }); // 0 = texture size, otherwise = size in pixels for skin scaling
+
+VAR(IDF_PERSIST|IDF_HEX, guibgcolour, -1, 0x000000, 0xFFFFFF);
+FVAR(IDF_PERSIST, guibgblend, 0, 0.7f, 1);
+VAR(IDF_PERSIST|IDF_HEX, guibordercolour, -1, 0x000000, 0xFFFFFF);
+FVAR(IDF_PERSIST, guiborderblend, 0, 1.f, 1);
+
+VAR(IDF_PERSIST|IDF_HEX, guihovercolour, -1, 0xF0A0A0, 0xFFFFFF);
+FVAR(IDF_PERSIST, guihoverscale, 0, 0.3f, 1);
+FVAR(IDF_PERSIST, guihoverblend, 0, 0.9f, 1);
+
+VAR(IDF_PERSIST, guistatusline, 0, 1, 1);
+VAR(IDF_PERSIST, guitooltips, 0, 1, 1);
+VAR(IDF_PERSIST, guitooltiptime, 0, 500, VAR_MAX);
+VAR(IDF_PERSIST, guitooltipfade, 0, 500, VAR_MAX);
+VAR(IDF_PERSIST|IDF_HEX, guitooltipcolour, -1, 0x000000, 0xFFFFFF);
+FVAR(IDF_PERSIST, guitooltipblend, 0, 0.9f, 1);
+VAR(IDF_PERSIST|IDF_HEX, guitooltipbordercolour, -1, 0x808080, 0xFFFFFF);
+FVAR(IDF_PERSIST, guitooltipborderblend, 0, 1.f, 1);
+VAR(IDF_PERSIST, guitooltipborderskin, 0, 1, 1);
+
+VAR(IDF_PERSIST|IDF_HEX, guifieldbgcolour, -1, 0x404040, 0xFFFFFF);
+FVAR(IDF_PERSIST, guifieldbgblend, 0, 0.7f, 1);
+VAR(IDF_PERSIST|IDF_HEX, guifieldbordercolour, -1, 0xC0C0C0, 0xFFFFFF);
+FVAR(IDF_PERSIST, guifieldborderblend, 0, 1.f, 1);
+VAR(IDF_PERSIST|IDF_HEX, guifieldactivecolour, -1, 0xF04040, 0xFFFFFF);
+FVAR(IDF_PERSIST, guifieldactiveblend, 0, 1.f, 1);
+
+VAR(IDF_PERSIST, guislidersize, 1, 48, 128);
+VAR(IDF_PERSIST|IDF_HEX, guislidercolour, -1, 0x000000, 0xFFFFFF);
+FVAR(IDF_PERSIST, guisliderblend, 0, 0.3f, 1);
+VAR(IDF_PERSIST|IDF_HEX, guisliderbordercolour, -1, 0xC0C0C0, 0xFFFFFF);
+FVAR(IDF_PERSIST, guisliderborderblend, 0, 1.f, 1);
+VAR(IDF_PERSIST, guisliderborderskin, 0, 2, 2);
+VAR(IDF_PERSIST|IDF_HEX, guislidermarkcolour, -1, 0x808080, 0xFFFFFF);
+FVAR(IDF_PERSIST, guislidermarkblend, 0, 1.f, 1);
+VAR(IDF_PERSIST|IDF_HEX, guislidermarkbordercolour, -1, 0xC0C0C0, 0xFFFFFF);
+FVAR(IDF_PERSIST, guislidermarkborderblend, 0, 1.f, 1);
+VAR(IDF_PERSIST, guislidermarkborderskin, 0, 0, 2);
+VAR(IDF_PERSIST|IDF_HEX, guislideractivecolour, -1, 0xF04040, 0xFFFFFF);
+FVAR(IDF_PERSIST, guislideractiveblend, 0, 1.f, 1);
+
+VAR(IDF_PERSIST|IDF_HEX, guiactivecolour, -1, 0xF02020, 0xFFFFFF);
+FVAR(IDF_PERSIST, guiactiveblend, 0, 1.f, 1);
+
+static bool needsinput = false, hastitle = true, hasbgfx = true, tooltipforce = false;
+static char *statusstr = NULL, *tooltipstr = NULL, *tooltip = NULL;
+static int lasttooltip = 0, statuswidth = 0, tooltipwidth = 0;
 
 #include "textedit.h"
 struct gui : guient
@@ -34,13 +85,160 @@ struct gui : guient
     static int curdepth, curlist, xsize, ysize, curx, cury, fontdepth, mergelist, mergedepth;
     static bool hitfx;
 
-    static void reset() { lists.shrink(0); mergelist = mergedepth = -1; }
+    static void reset()
+    {
+        if(statusstr) DELETEA(statusstr);
+        if(tooltipstr) DELETEA(tooltipstr);
+        lists.shrink(0);
+        statuswidth = tooltipwidth = 0;
+        mergelist = mergedepth = -1;
+        tooltipforce = false;
+    }
 
     static int ty, tx, tpos, *tcurrent, tcolor; //tracking tab size and position since uses different layout method...
 
     void allowhitfx(bool on) { hitfx = on; }
-    static inline bool visibletab() { return !tcurrent || tpos == *tcurrent; }
-    bool visible() { return !layoutpass && visibletab(); }
+    bool visibletab() { return !tcurrent || tpos == *tcurrent; }
+    bool visible() { return !guilayoutpass && visibletab(); }
+
+    void skin(int x1, int y1, int x2, int y2, int c1 = -1, float b1 = -1.f, int c2 = -1, float b2 = -1.f, bool skinborder = false)
+    {
+        int colour1 = c1 >= 0 ? c1 : (guibgcolour >= 0 ? guibgcolour : (c2 >= 0 ? c2 : 0x000000)),
+            colour2 = c2 >= 0 ? c2 : (guibordercolour >= 0 ? guibordercolour : 0x808080);
+        float blend1 = b1 >= 0 ? b1 : guibgblend, blend2 = b2 >= 0 ? b2 : guiborderblend;
+        switch(guiskinned)
+        {
+            case 2: case 3:
+            {
+                bool drawn = false;
+                loopk(guiskinned == 3 || skinborder ? 2 : 1)
+                {
+                    int colour = colour1;
+                    float blend = blend1;
+                    Texture *t = NULL;
+                    switch(k)
+                    {
+                        case 1:
+                            colour = colour2;
+                            blend = blend2;
+                            if(!skinbordertex) skinbordertex = textureload(guiskinbordertex, 3, true, false);
+                            if(skinbordertex && skinbordertex != notexture) t = skinbordertex;
+                            break;
+                        case 0: default:
+                            if(!skintex) skintex = textureload(guiskintex, 3, true, false);
+                            if(skintex && skintex != notexture) t = skintex;
+                            break;
+                    }
+                    if(!t) break;
+                    int w = x2-x1, h = y2-y1, tw = guiskinsize ? guiskinsize : t->w, th = guiskinsize ? guiskinsize : t->h;
+                    float pw = tw*0.25f, ph = th*0.25f, qw = tw*0.5f, qh = th*0.5f, px = 0, py = 0, tx = 0, ty = 0;
+                    int cw = max(int(floorf(w/qw))-1, 0), ch = max(int(floorf(h/qh))+1, 2);
+
+                    glBindTexture(GL_TEXTURE_2D, t->id);
+                    glColor4f((colour>>16)/255.f, ((colour>>8)&0xFF)/255.f, (colour&0xFF)/255.f, blend);
+                    glBegin(GL_QUADS);
+
+                    loopi(ch)
+                    {
+                        bool cond = !i || i == ch-1;
+                        float vph = cond ? ph : qh, vth = cond ? 0.25f : 0.5f;
+                        if(i && cond)
+                        {
+                            float off = h-py;
+                            if(off > vph)
+                            {
+                                float part = off/vph;
+                                vph *= part;
+                                vth *= part;
+                            }
+                            ty = 1-vth;
+                        }
+                        loopj(3) switch(j)
+                        {
+                            case 0: case 2:
+                            {
+                                glTexCoord2f(tx, ty); glVertex2f(x1+px, y1+py);
+                                glTexCoord2f(tx+0.25f, ty); glVertex2f(x1+px+pw, y1+py);
+                                glTexCoord2f(tx+0.25f, ty+vth); glVertex2f(x1+px+pw, y1+py+vph);
+                                glTexCoord2f(tx, ty+vth); glVertex2f(x1+px, y1+py+vph);
+                                tx += 0.25f;
+                                px += pw;
+                                xtraverts += 4;
+                                break;
+                            }
+                            case 1:
+                            {
+                                for(int xx = 0; xx < cw; xx++)
+                                {
+                                    glTexCoord2f(tx, ty); glVertex2f(x1+px, y1+py);
+                                    glTexCoord2f(tx+0.5f, ty); glVertex2f(x1+px+qw, y1+py);
+                                    glTexCoord2f(tx+0.5f, ty+vth); glVertex2f(x1+px+qw, y1+py+vph);
+                                    glTexCoord2f(tx, ty+vth); glVertex2f(x1+px, y1+py+vph);
+                                    px += qw;
+                                    xtraverts += 4;
+                                }
+                                float want = w-pw, off = want-px;
+                                if(off > 0)
+                                {
+                                    float part = 0.5f*off/qw;
+                                    glTexCoord2f(tx, ty); glVertex2f(x1+px, y1+py);
+                                    glTexCoord2f(tx+part, ty); glVertex2f(x1+want, y1+py);
+                                    glTexCoord2f(tx+part, ty+vth); glVertex2f(x1+want, y1+py+vph);
+                                    glTexCoord2f(tx, ty+vth); glVertex2f(x1+px, y1+py+vph);
+                                    px = want;
+                                }
+                                tx += 0.5f;
+                                break;
+                            }
+                            default: break;
+                        }
+                        px = tx = 0;
+                        py += vph;
+                        if(!i) ty += vth;
+                    }
+                    glEnd();
+                    drawn = true;
+                }
+                if(drawn) break; // otherwise fallback
+            }
+            case 1:
+            {
+                x1++; y1++; x2--; y2--; // offset these slightly like a skin
+                if(colour1 >= 0)
+                {
+                    notextureshader->set();
+                    glDisable(GL_TEXTURE_2D);
+                    glColor4f((colour1>>16)/255.f, ((colour1>>8)&0xFF)/255.f, (colour1&0xFF)/255.f, blend1);
+                    glBegin(GL_TRIANGLE_STRIP);
+                    glVertex2f(x1, y1);
+                    glVertex2f(x2, y1);
+                    glVertex2f(x1, y2);
+                    glVertex2f(x2, y2);
+                    xtraverts += 4;
+                    glEnd();
+                    defaultshader->set();
+                    glEnable(GL_TEXTURE_2D);
+                }
+                if(skinborder && colour2 >= 0)
+                {
+                    lineshader->set();
+                    glDisable(GL_TEXTURE_2D);
+                    glColor4f((colour2>>16)/255.f, ((colour2>>8)&0xFF)/255.f, (colour2&0xFF)/255.f, blend2);
+                    glBegin(GL_LINE_LOOP);
+                    glVertex2f(x1, y1);
+                    glVertex2f(x2, y1);
+                    glVertex2f(x2, y2);
+                    glVertex2f(x1, y2);
+                    xtraverts += 4;
+                    glEnd();
+                    defaultshader->set();
+                    glEnable(GL_TEXTURE_2D);
+                }
+                break;
+            }
+            case 0: default: break;
+        }
+    }
 
     //tab is always at top of page
     void tab(const char *name, int color, bool front)
@@ -50,7 +248,7 @@ struct gui : guient
         if(front && tcurrent && *tcurrent != tpos) *tcurrent = tpos;
         if(!hastitle)
         {
-            if(layoutpass)
+            if(guilayoutpass)
             {
                 ty = max(ty, ysize);
                 ysize = 0;
@@ -60,11 +258,10 @@ struct gui : guient
         }
         if(color) tcolor = color;
         if(!name) name = intstr(tpos);
-        defformatstring(tabtitle)("\fs\fd[\fS%s%s%s\fs\fd]\fS", visible() ? " " : "", name, visible() ? " " : "");
-        bool vis = visibletab();
-        gui::pushfont(vis ? "super" : "default");
-        int w = text_width(tabtitle);
-        if(layoutpass)
+        gui::pushfont("super");
+        int width = 0, height = 0;
+        text_bounds(name, width, height);
+        if(guilayoutpass)
         {
             ty = max(ty, ysize);
             ysize = 0;
@@ -72,35 +269,60 @@ struct gui : guient
         else
         {
             cury = -ysize;
-            int x1 = curx + tx + guibound[0], x2 = x1 + w, y1 = cury - guibound[1]*2, y2 = cury - guibound[1]*2 + FONTH, alpha = guiblend;
-            if(vis) { tcolor = 0xFFFFFF; alpha = 255; }
-            else if(tcurrent && hitx>=x1 && hity>=y1 && hitx<x2 && hity<y2)
+            int x1 = curx+tx, x2 = x1+width+guispacesize, y1 = cury-guispacesize-height, y2 = cury-guispacesize*3/4, alpha = guitextblend, border = -1;
+            if(!visibletab())
             {
-                if(!guiclicktab || mouseaction[0]&GUI_UP) *tcurrent = tpos; // switch tab
-                tcolor = 0xFF4444;
-                alpha = max(guiblend, 200);
+                if(tcurrent && hitx>=x1 && hity>=y1 && hitx<x2 && hity<y2)
+                {
+                    if(!guiclicktab || mouseaction[0]&GUI_UP) *tcurrent = tpos; // switch tab
+                    tcolor = guiactivecolour;
+                    alpha = max(alpha, guitextfade);
+                    if(guitabborder) border = tcolor;
+                }
+                else
+                {
+                    tcolor = vec::hexcolor(tcolor).mul(0.25f).tohexcolor();
+                    if(guitabborder == 2) border = tcolor;
+                }
             }
-            text_(tabtitle, x1, y1, tcolor, alpha, visible());
+            else if(guitabborder == 2) border = guibordercolour;
+            if(hasbgfx) skin(x1, y1, x2, y2, guibgcolour, guibgblend, border >= 0 ? border : guibordercolour, guiborderblend, border >= 0);
+            text_(name, x1+guispacesize/2, y1+guispacesize/8, tcolor, alpha, visible());
         }
-        tx += w + guibound[0]*2;
+        tx += width+guispacesize*3/2;
         gui::popfont();
     }
 
     void uibuttons()
     {
-        int x = curx+max(xsize-guibound[1]*2/3, tx), y = -ysize-guibound[1]*2;
-        #define uibtn(a,b) \
-        { \
-            bool hit = false; \
-            if(hitx>=x && hity>=y && hitx<x+guibound[1] && hity<y+guibound[1]) \
+        gui::pushfont("super");
+        tx += FONTH+guispacesize*2; // acts like a tab
+        if(!guilayoutpass)
+        {
+            cury = -ysize;
+            int x1 = curx+(xsize-FONTH+guispacesize/4), x2 = x1+FONTH+guispacesize/4, y1 = cury-guispacesize-FONTH, y2 = cury-guispacesize*3/4;
+            //int x1 = curx+(xsize-FONTH), x2 = x1+FONTH*3/2, y1 = cury-FONTH*225/100, y2 = cury-FONTH*3/4;
+            #define uibtn(a,b) \
             { \
-                if(mouseaction[0]&GUI_UP) { b; } \
-                hit = true; \
-            } \
-            icon_(textureload(a, 3, true, false), false, x, y, guibound[1], !hit, hit ? 0xFF0000 : 0xFFFFFF); \
-            y += guibound[1]*3/2; \
+                int border = -1; \
+                bool hit = false; \
+                if(hitx>=x1 && hity>=y1 && hitx<x2 && hity<y2) \
+                { \
+                    if(mouseaction[0]&GUI_UP) { b; } \
+                    hit = true; \
+                    if(guitabborder) border = guiactivecolour; \
+                } \
+                else if(guitabborder == 2) border = vec::hexcolor(guibordercolour).mul(0.25f).tohexcolor(); \
+                if(hasbgfx) skin(x1, y1, x2, y2, guibgcolour, guibgblend, border >= 0 ? border : guibordercolour, guiborderblend, border >= 0); \
+                x1 += guispacesize/8; \
+                y1 += guispacesize/8; \
+                icon_(a, false, x1, y1, FONTH, hit, 0xFFFFFF); \
+                y1 += FONTH*3/2; \
+            }
+            if(!exittex) exittex = textureload(guiexittex, 3, true, false); \
+            uibtn(exittex, cleargui(1));
         }
-        uibtn("textures/exit", cleargui(1));
+        gui::popfont();
     }
 
     bool ishorizontal() const { return curdepth&1; }
@@ -108,7 +330,7 @@ struct gui : guient
 
     void pushlist(bool merge)
     {
-        if(layoutpass)
+        if(guilayoutpass)
         {
             if(curlist>=0)
             {
@@ -144,7 +366,7 @@ struct gui : guient
             }
         }
         curdepth++;
-        if(!layoutpass && visible() && ishit(xsize, ysize)) loopi(2) lists[curlist].mouse[i] = mouseaction[i]|GUI_ROLLOVER;
+        if(!guilayoutpass && visible() && ishit(xsize, ysize)) loopi(2) lists[curlist].mouse[i] = mouseaction[i]|GUI_ROLLOVER;
         if(merge)
         {
             mergelist = curlist;
@@ -156,7 +378,7 @@ struct gui : guient
     {
         if(!lists.inrange(curlist)) return 0;
         list &l = lists[curlist];
-        if(layoutpass)
+        if(guilayoutpass)
         {
             l.w = xsize;
             l.h = ysize;
@@ -171,7 +393,7 @@ struct gui : guient
             list &p = lists[curlist];
             xsize = p.w;
             ysize = p.h;
-            if(!layoutpass && p.springs > 0)
+            if(!guilayoutpass && p.springs > 0)
             {
                 list &s = lists[p.parent];
                 if(ishorizontal()) xsize = s.w; else ysize = s.h;
@@ -181,25 +403,47 @@ struct gui : guient
         return 0;
     }
 
-    int text  (const char *text, int color, const char *icon, int icolor) { return button_(text, color, icon, icolor, false, false); }
-    int button(const char *text, int color, const char *icon, int icolor, bool faded) { return button_(text, color, icon, icolor, true, faded); }
-    int title (const char *text, int color, const char *icon, int icolor) { return button_(text, color, icon, icolor, false, false, "emphasis"); }
+    void setstatus(const char *fmt, int width, ...)
+    {
+        if(statusstr) DELETEA(statusstr);
+        defvformatstring(str, width, fmt);
+        statusstr = newstring(str);
+        if(width) statuswidth = width;
+    }
+
+    void settooltip(const char *fmt, int width, ...)
+    {
+        if(tooltipforce) return; // overridden in code
+        if(tooltipstr) DELETEA(tooltipstr);
+        defvformatstring(str, width, fmt);
+        tooltipstr = newstring(str);
+        if(width) tooltipwidth = width;
+    }
+
+    void pushfont(const char *font) { ::pushfont(font); fontdepth++; }
+    void popfont() { if(fontdepth) { ::popfont(); fontdepth--; } }
+
+    int text(const char *text, int color, const char *icon, int icolor, int wrap)
+    {
+        return button_(text, color, icon, icolor, false, wrap, false);
+    }
+    int button(const char *text, int color, const char *icon, int icolor, int wrap, bool faded)
+    {
+        return button_(text, color, icon, icolor, true, wrap, faded);
+    }
 
     void separator() { line_(guisepsize); }
 
     //use to set min size (useful when you have progress bars)
-    void strut(float size) { layout(isvertical() ? int(size*guibound[0]) : 0, isvertical() ? 0 : int(size*guibound[1])); }
+    void strut(float size) { layout(isvertical() ? int(size*FONTW) : 0, isvertical() ? 0 : int(size*FONTH)); }
     //add space between list items
-    void space(float size) { layout(isvertical() ? 0 : size*guibound[0], isvertical() ? size*guibound[1] : 0); }
-
-    void pushfont(const char *font) { ::pushfont(font); fontdepth++; }
-    void popfont() { if(fontdepth) { ::popfont(); fontdepth--; } }
+    void space(float size) { layout(isvertical() ? 0 : int(size*FONTW), isvertical() ? int(size*FONTH) : 0); }
 
     void spring(int weight)
     {
         if(curlist < 0) return;
         list &l = lists[curlist];
-        if(layoutpass) { if(l.parent >= 0) l.springs += weight; return; }
+        if(guilayoutpass) { if(l.parent >= 0) l.springs += weight; return; }
         int nextspring = min(l.curspring + weight, l.springs);
         if(nextspring <= l.curspring) return;
         if(ishorizontal())
@@ -217,7 +461,7 @@ struct gui : guient
 
     int layout(int w, int h)
     {
-        if(layoutpass)
+        if(guilayoutpass)
         {
             if(ishorizontal())
             {
@@ -252,7 +496,7 @@ struct gui : guient
     int image(Texture *t, float scale, bool overlaid, int icolor)
     {
         if(scale == 0) scale = 1;
-        int size = (int)(scale*2*guibound[1])-guishadow;
+        int size = (int)(scale*2*FONTH)-guishadow;
         if(visible()) icon_(t, overlaid, curx, cury, size, ishit(size+guishadow, size+guishadow), icolor);
         return layout(size+guishadow, size+guishadow);
     }
@@ -260,7 +504,7 @@ struct gui : guient
     int texture(VSlot &vslot, float scale, bool overlaid)
     {
         if(scale==0) scale = 1;
-        int size = (int)(scale*2*guibound[1])-guishadow;
+        int size = (int)(scale*2*FONTH)-guishadow;
         if(visible()) previewslot(vslot, overlaid, curx, cury, size, ishit(size+guishadow, size+guishadow));
         return layout(size+guishadow, size+guishadow);
     }
@@ -268,7 +512,7 @@ struct gui : guient
     int playerpreview(int model, int color, int team, int weap, const char *vanity, float sizescale, bool overlaid, float scale, float blend)
     {
         if(sizescale==0) sizescale = 1;
-        int size = (int)(sizescale*2*guibound[1])-guishadow;
+        int size = (int)(sizescale*2*FONTH)-guishadow;
         if(visible())
         {
             bool hit = ishit(size+guishadow, size+guishadow);
@@ -298,7 +542,7 @@ struct gui : guient
             if(overlaid)
             {
                 if(!overlaytex) overlaytex = textureload(guioverlaytex, 3, true, false);
-                const vec &ocolor = hit && hitfx ? vec(1, 0.25f, 0.25f) : vec(1, 1, 1);
+                const vec &ocolor = hit && hitfx ? vec::hexcolor(guiactivecolour) : vec(1, 1, 1);
                 glColor3fv(ocolor.v);
                 glBindTexture(GL_TEXTURE_2D, overlaytex->id);
                 rect_(xi - xpad, yi - ypad, xs + 2*xpad, ys + 2*ypad, 0);
@@ -310,7 +554,7 @@ struct gui : guient
     int modelpreview(const char *name, int anim, float sizescale, bool overlaid, float scale, float blend)
     {
         if(sizescale==0) sizescale = 1;
-        int size = (int)(sizescale*2*guibound[1])-guishadow;
+        int size = (int)(sizescale*2*FONTH)-guishadow;
         if(visible())
         {
             bool hit = ishit(size+guishadow, size+guishadow);
@@ -338,7 +582,7 @@ struct gui : guient
                 light.color = vec(1, 1, 1);
                 light.dir = vec(0, -1, 2).normalize();
                 vec center, radius;
-                m->boundbox(0, center, radius);
+                m->boundbox(center, radius);
                 float dist =  2.0f*max(radius.magnitude2(), 1.1f*radius.z),
                       yaw = fmod(lastmillis/10000.0f*360.0f, 360.0f);
                 vec o(-center.x, dist - center.y, -0.1f*dist - center.z);
@@ -352,7 +596,7 @@ struct gui : guient
             if(overlaid)
             {
                 if(!overlaytex) overlaytex = textureload(guioverlaytex, 3, true, false);
-                const vec &ocolor = hit && hitfx ? vec(1, 0.25f, 0.25f) : vec(1, 1, 1);
+                const vec &ocolor = hit && hitfx ? vec::hexcolor(guiactivecolour) : vec(1, 1, 1);
                 glColor3fv(ocolor.v);
                 glBindTexture(GL_TEXTURE_2D, overlaytex->id);
                 rect_(xi - xpad, yi - ypad, xs + 2*xpad, ys + 2*ypad, 0);
@@ -364,60 +608,39 @@ struct gui : guient
     int slice(Texture *t, float scale, float start, float end, const char *text)
     {
         if(scale == 0) scale = 1;
-        int size = (int)(scale*2*guibound[1]);
+        int size = (int)(scale*2*FONTH);
         if(t!=notexture && visible()) slice_(t, curx, cury, size, start, end, text);
         return layout(size, size);
     }
 
     void progress(float percent, float scale)
     {
-        Texture *t = textureload(hud::progresstex, 3, true, false);
         if(scale == 0) scale = 1;
-        int size = (int)(scale*2*guibound[1]), part = size*2/3;
-        slice_(t, curx+part/8, cury+part/8, part, 0, percent);
+        int size = (int)(scale*2*FONTH);
+        slice_(textureload(hud::progringtex, 3, true, false), curx, cury, size, (SDL_GetTicks()%1000)/1000.f, 0.1f);
         string s; if(percent > 0) formatstring(s)("\fg%d%%", int(percent*100)); else formatstring(s)("\fg...");
-        slice_(t, curx, cury, size, (SDL_GetTicks()%1000)/1000.f, 0.1f, s);
+        slice_(textureload(hud::progresstex, 3, true, false), curx, cury, size, 0, percent, s);
         layout(size, size);
     }
 
-    void slider(int &val, int vmin, int vmax, int color, const char *label, bool reverse, bool scroll)
+    int slider(int &val, int vmin, int vmax, int colour, const char *label, bool reverse, bool scroll, int style, int scolour)
     {
-        int x = curx;
-        int y = cury;
-        int space = line_(guilinesize, 1.0f, ishorizontal() ? guibound[0]*3 : guibound[1]);
+        int x = curx, y = cury;
+        float percent = (val-vmin)/float(max(vmax-vmin, 1));
+        bool hit = false;
+        int space = slider_(guislidersize, percent, ishorizontal() ? FONTW*3 : FONTH, hit, style, scolour);
         if(visible())
         {
-            pushfont("emphasis");
-            if(!label) label = intstr(val);
-            int w = text_width(label);
-
-            bool hit = false, forcecolor = false;
-            int px, py;
-            if(ishorizontal())
-            {
-                hit = ishit(guilinesize, ysize, x + space/2 - guilinesize/2, y);
-                px = x + space/2 - w/2;
-                py = ((ysize-guibound[1])*(val-vmin))/max(vmax-vmin, 1);
-                if(reverse) py += y; //vmin at top
-                else py = y + (ysize-guibound[1]) - py; //vmin at bottom
-            }
-            else
-            {
-                hit = ishit(xsize, guilinesize, x, y + space/2 - guilinesize/2);
-                px = ((xsize-w)*(val-vmin))/max(vmax-vmin, 1);
-                if(reverse) px = x + (xsize-guibound[0]/2-w/2) - px; //vmin at right
-                else px += x + guibound[0]/2 - w/2; //vmin at left
-                py = y + space/2 - FONTH/2;
-            }
-            if(hit && hitfx) { forcecolor = true; color = 0xFF4444; }
-            text_(label, px, py, color, hit && hitfx ? 255 : guiblend, hit && mouseaction[0]&GUI_DOWN, forcecolor);
             if(hit)
             {
+                if(!label) label = intstr(val);
+                settooltip("\f[%d]%s", -1, colour, label);
+                tooltipforce = true;
                 if(mouseaction[0]&GUI_PRESSED)
                 {
                     int vnew = vmax-vmin+1;
-                    if(ishorizontal()) vnew = int((vnew*(reverse ? hity-y-guibound[1]/2 : y+ysize-guibound[1]/2-hity))/(ysize-guibound[1]));
-                    else vnew = int((vnew*(reverse ? x+xsize-guibound[0]/2-hitx : hitx-x-guibound[0]/2))/(xsize-w));
+                    if(ishorizontal()) vnew = int((vnew*(reverse ? hity-y-guislidersize/2 : y+ysize-guislidersize/2-hity))/(ysize-guislidersize));
+                    else vnew = int((vnew*(reverse ? x+xsize-guislidersize/2-hitx : hitx-x-guislidersize/2))/(xsize-guislidersize));
                     vnew += vmin;
                     vnew = clamp(vnew, vmin, vmax);
                     if(vnew != val) val = vnew;
@@ -435,46 +658,40 @@ struct gui : guient
                     vnew = clamp(vval, vmin, vmax);
                 if(vnew != val) val = vnew;
             }
-            popfont();
         }
+        return space;
     }
 
-    char *field(const char *name, int color, int length, int height, const char *initval, int initmode, bool focus, const char *parent)
+    char *field(const char *name, int color, int length, int height, const char *initval, int initmode, bool focus, const char *parent, const char *prompt, bool immediate)
     {
-        return field_(name, color, length, height, initval, initmode, FIELDEDIT, focus, parent, "console");
+        return field_(name, color, length, height, initval, initmode, FIELDEDIT, focus, parent, prompt, immediate);
     }
 
-    char *keyfield(const char *name, int color, int length, int height, const char *initval, int initmode, bool focus, const char *parent)
+    char *keyfield(const char *name, int color, int length, int height, const char *initval, int initmode, bool focus, const char *parent, const char *prompt, bool immediate)
     {
-        return field_(name, color, length, height, initval, initmode, FIELDKEY, focus, parent, "console");
+        return field_(name, color, length, height, initval, initmode, FIELDKEY, focus, parent, prompt, immediate);
     }
 
-    char *field_(const char *name, int color, int length, int height, const char *initval, int initmode, int fieldtype = FIELDEDIT, bool focus = false, const char *parent = NULL, const char *font = "")
+    char *field_(const char *name, int color, int length, int height, const char *initval, int initmode, int fieldtype = FIELDEDIT, bool focus = false, const char *parent = NULL, const char *prompt = NULL, bool immediate = false)
     {
-        if(font && *font) gui::pushfont(font);
         editor *e = useeditor(name, initmode, false, initval, parent); // generate a new editor if necessary
-        if(layoutpass)
+        if(guilayoutpass)
         {
-            if(initval && e->mode==EDITORFOCUSED && (e!=currentfocus() || fieldmode == FIELDSHOW))
-            {
-                if(strcmp(e->lines[0].text, initval)) e->clear(initval);
-            }
-            e->linewrap = (length<0);
-            e->maxx = (e->linewrap) ? -1 : length;
-            e->maxy = (height<=0)?1:-1;
+            if(initval && (e->lines.empty() || (e->mode == EDITORFOCUSED && (e != currentfocus() || fieldmode == FIELDSHOW) && strcmp(e->lines[0].text, initval))))
+                e->clear(initval);
+            e->linewrap = (length < 0);
+            e->maxx = e->linewrap ? -1 : length;
+            e->maxy = (height <= 0) ? 1 : -1;
             e->pixelwidth = abs(length)*FONTW;
-            if(e->linewrap && e->maxy==1)
+            if(e->linewrap && e->maxy == 1)
             {
-                int temp;
+                int temp = 0;
                 text_bounds(e->lines[0].text, temp, e->pixelheight, e->pixelwidth); //only single line editors can have variable height
             }
-            else
-                e->pixelheight = FONTH*max(height, 1);
+            else e->pixelheight = FONTH*max(height, 1);
         }
-        int h = e->pixelheight, hpad = 0, w = e->pixelwidth, wpad = guibound[0];
-        if((h+hpad)%guibound[1]) hpad += guibound[1] - (h+hpad)%guibound[1];
+        int h = e->pixelheight, hpad = FONTH/4, w = e->pixelwidth, wpad = FONTW;
         h += hpad;
-        if((w+wpad)%guibound[0]) wpad += guibound[0] - (w+wpad)%guibound[0];
         w += wpad;
 
         bool wasvertical = isvertical();
@@ -485,46 +702,41 @@ struct gui : guient
         {
             e->rendered = true;
             if(focus && e->unfocus) focus = false;
-            bool hit = ishit(w, h) && e->mode!=EDITORREADONLY;
-            bool editing = (fieldmode != FIELDSHOW) && e==currentfocus() && e->mode!=EDITORREADONLY;
+            bool hit = ishit(w, h) && e->mode != EDITORREADONLY, clrs = fieldtype == FIELDKEY,
+                 editing = (fieldmode != FIELDSHOW) && e == currentfocus() && e->mode != EDITORREADONLY;
             if(mouseaction[0]&GUI_UP && mergedepth >= 0 && hit) mouseaction[0] &= ~GUI_UP;
             if(mouseaction[0]&GUI_DOWN) //mouse request focus
             {
                 if(hit)
                 {
                     focus = true;
+                    if(mouseaction[0]&GUI_ALT) clrs = true;
                     if(e->unfocus) e->unfocus = false;
                 }
                 else if(editing) fieldmode = FIELDCOMMIT;
             }
             if(focus)
             {
-                if(fieldtype == FIELDKEY) e->clear();
+                if(clrs) e->clear();
                 useeditor(e->name, initmode, true, initval, parent);
                 e->mark(false);
-                fieldmode = fieldtype;
+                if(fieldmode != FIELDCOMMIT && fieldmode != FIELDABORT) fieldmode = fieldtype;
             }
-            if(hit && editing && (mouseaction[0]&GUI_PRESSED)!=0 && fieldtype==FIELDEDIT)
+            if(hit && editing && (mouseaction[0]&GUI_PRESSED) && fieldtype == FIELDEDIT)
                 e->hit(int(floor(hitx-(curx+wpad/2))), int(floor(hity-(cury+hpad/2))), (mouseaction[0]&GUI_DRAGGED)!=0); //mouse request position
-            if(editing && (fieldmode==FIELDCOMMIT || fieldmode==FIELDABORT)) // commit field if user pressed enter
+            if(editing && (fieldmode == FIELDCOMMIT || fieldmode == FIELDABORT)) // commit field if user pressed enter
             {
-                if(fieldmode==FIELDCOMMIT) result = e->currentline().text;
-                e->active = (e->mode!=EDITORFOCUSED);
+                if(fieldmode == FIELDCOMMIT) result = e->currentline().text;
+                e->active = (e->mode != EDITORFOCUSED);
                 fieldmode = FIELDSHOW;
             }
-            else fieldsactive = true;
-
-            e->draw(curx+wpad/2, cury+hpad/2, color, editing);
-
-            lineshader->set();
-            glDisable(GL_TEXTURE_2D);
-            glDisable(GL_BLEND);
-            if(editing) glColor3f(0.75f, 0.25f, 0.25f);
-            else glColor3ub(color>>16, (color>>8)&0xFF, color&0xFF);
-            rect_(curx, cury, w, h, -1, true);
-            glEnable(GL_TEXTURE_2D);
-            glEnable(GL_BLEND);
-            defaultshader->set();
+            else
+            {
+                if(editing && immediate) result = e->currentline().text;
+                fieldsactive = true;
+            }
+            skin(curx, cury, curx+w, cury+h, guifieldbgcolour, guifieldbgblend, editing ? guifieldactivecolour : guifieldbordercolour, editing ? guifieldactiveblend : guifieldborderblend, true);
+            e->draw(curx+wpad/2, cury+hpad/2, color, editing, prompt);
         }
         else if(e->unfocus) e->unfocus = false;
         layout(w, h);
@@ -534,7 +746,7 @@ struct gui : guient
             if(slines > 0)
             {
                 int oldpos = e->scrolly == editor::SCROLLEND ? slines : e->scrolly, newpos = oldpos;
-                slider(newpos, 0, slines, color, NULL, true, true);
+                slider(newpos, 0, slines, color, NULL, true, true, 0, -1);
                 if(oldpos != newpos)
                 {
                     e->cy = newpos;
@@ -543,44 +755,40 @@ struct gui : guient
             }
             if(wasvertical) poplist();
         }
-        if(font && *font) gui::popfont();
         return result;
     }
 
-    void rect_(float x, float y, float w, float h, int usetc = -1, bool lines = false)
+    void rect_(float x, float y, float w, float h, bool lines = false)
     {
         glBegin(lines ? GL_LINE_LOOP : GL_TRIANGLE_STRIP);
-        static const GLfloat tc[4][2] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
-        if(usetc>=0) glTexCoord2fv(tc[usetc]);
         glVertex2f(x, y);
-        if(usetc>=0) glTexCoord2fv(tc[(usetc+1)%4]);
         glVertex2f(x + w, y);
-        if(lines)
-        {
-            if(usetc>=0) glTexCoord2fv(tc[(usetc+2)%4]);
-            glVertex2f(x + w, y + h);
-        }
-        if(usetc>=0) glTexCoord2fv(tc[(usetc+3)%4]);
+        if(lines) glVertex2f(x + w, y + h);
         glVertex2f(x, y + h);
-        if(!lines)
-        {
-            if(usetc>=0) glTexCoord2fv(tc[(usetc+2)%4]);
-            glVertex2f(x + w, y + h);
-        }
+        if(!lines) glVertex2f(x + w, y + h);
         glEnd();
         xtraverts += 4;
+    }
 
+    void rect_(float x, float y, float w, float h, int usetc)
+    {
+        glBegin(GL_TRIANGLE_STRIP);
+        static const GLfloat tc[5][2] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}};
+        glTexCoord2fv(tc[usetc]); glVertex2f(x, y);
+        glTexCoord2fv(tc[usetc+1]); glVertex2f(x + w, y);
+        glTexCoord2fv(tc[usetc+3]); glVertex2f(x, y + h);
+        glTexCoord2fv(tc[usetc+2]); glVertex2f(x + w, y + h);
+        glEnd();
+        xtraverts += 4;
     }
 
-    void text_(const char *text, int x, int y, int color, int alpha, bool shadow, bool force = false)
+    void text_(const char *text, int x, int y, int color, int alpha, bool shadow, bool force = false, int wrap = -1)
     {
-        if(FONTH < guibound[1]) y += (guibound[1]-FONTH)/2;
-        else if(FONTH > guibound[1]) y -= (FONTH-guibound[1])/2;
-        if(shadow) draw_text(text, x+guishadow, y+guishadow, 0x00, 0x00, 0x00, -0xC0*alpha/255);
-        draw_text(text, x, y, color>>16, (color>>8)&0xFF, color&0xFF, force ? -alpha : alpha);
+        if(shadow) draw_text(text, x+guishadow, y+guishadow, 0x00, 0x00, 0x00, -0xC0*alpha/255, TEXT_NO_INDENT, -1, wrap > 0 ? wrap : -1);
+        draw_text(text, x, y, color>>16, (color>>8)&0xFF, color&0xFF, force ? -alpha : alpha, TEXT_NO_INDENT, -1, wrap > 0 ? wrap : -1);
     }
 
-    void background(int color, int inheritw, int inherith)
+    void fill(int color, int inheritw, int inherith)
     {
         if(!visible()) return;
         glDisable(GL_TEXTURE_2D);
@@ -603,14 +811,67 @@ struct gui : guient
             list &p = lists[parenth];
             h = p.springs > 0 && !((curdepth-parentdepth)&1) ? lists[p.parent].h : p.h;
         }
-        rect_(curx, cury, w, h);
+        rect_(curx, cury, w, h, false);
         glEnable(GL_TEXTURE_2D);
         defaultshader->set();
     }
 
+    void outline(int color, int inheritw, int inherith, int offsetx, int offsety)
+    {
+        if(!visible()) return;
+        glDisable(GL_TEXTURE_2D);
+        lineshader->set();
+        glColor4ub(color>>16, (color>>8)&0xFF, color&0xFF, 0x80);
+        int w = xsize, h = ysize;
+        if(inheritw>0)
+        {
+            int parentw = curlist, parentdepth = 0;
+            for(;parentdepth < inheritw && lists[parentw].parent>=0; parentdepth++)
+                parentw = lists[parentw].parent;
+            list &p = lists[parentw];
+            w = p.springs > 0 && (curdepth-parentdepth)&1 ? lists[p.parent].w : p.w;
+        }
+        if(inherith>0)
+        {
+            int parenth = curlist, parentdepth = 0;
+            for(;parentdepth < inherith && lists[parenth].parent>=0; parentdepth++)
+                parenth = lists[parenth].parent;
+            list &p = lists[parenth];
+            h = p.springs > 0 && !((curdepth-parentdepth)&1) ? lists[p.parent].h : p.h;
+        }
+        rect_(curx+offsetx, cury+offsety, w-offsetx*2, h-offsety*2, true);
+        glEnable(GL_TEXTURE_2D);
+        defaultshader->set();
+    }
+
+    void background(int colour1, float blend1, int colour2, float blend2, bool skinborder, int inheritw, int inherith)
+    {
+        if(!visible()) return;
+        int w = xsize, h = ysize;
+        if(inheritw>0)
+        {
+            int parentw = curlist, parentdepth = 0;
+            for(;parentdepth < inheritw && lists[parentw].parent>=0; parentdepth++)
+                parentw = lists[parentw].parent;
+            list &p = lists[parentw];
+            w = p.springs > 0 && (curdepth-parentdepth)&1 ? lists[p.parent].w : p.w;
+        }
+        if(inherith>0)
+        {
+            int parenth = curlist, parentdepth = 0;
+            for(;parentdepth < inherith && lists[parenth].parent>=0; parentdepth++)
+                parenth = lists[parenth].parent;
+            list &p = lists[parenth];
+            h = p.springs > 0 && !((curdepth-parentdepth)&1) ? lists[p.parent].h : p.h;
+        }
+        skin(curx, cury, curx+w, cury+h, colour1, blend1, colour2, blend2, skinborder);
+    }
+
     void icon_(Texture *t, bool overlaid, int x, int y, int size, bool hit, int icolor)
     {
+        static const float tc[4][2] = { { 0, 0 }, { 1, 0 }, { 1, 1 }, { 0, 1 } };
         float xs = 0, ys = 0;
+        int textureid = -1;
         if(t)
         {
             float scale = float(size)/max(t->xs, t->ys); //scale and preserve aspect ratio
@@ -618,28 +879,27 @@ struct gui : guient
             ys = t->ys*scale;
             x += int((size-xs)/2);
             y += int((size-ys)/2);
-            glBindTexture(GL_TEXTURE_2D, t->id);
+            textureid = t->id;
         }
         else
         {
-            extern GLuint lmprogtex;
-            if(lmprogtex)
+            if(lightmapping && lmprogtex)
             {
                 float scale = float(size)/256; //scale and preserve aspect ratio
                 xs = 256*scale; ys = 256*scale;
                 x += int((size-xs)/2);
                 y += int((size-ys)/2);
-                glBindTexture(GL_TEXTURE_2D, lmprogtex);
+                textureid = lmprogtex;
             }
             else
             {
                 defformatstring(texname)("%s", mapname);
-                if((t = textureload(texname, 3, true, false)) == notexture) t = textureload("textures/emblem", 3, true, false);
+                if((t = textureload(texname, 3, true, false)) == notexture) t = textureload(emblemtex, 3, true, false);
                 float scale = float(size)/max(t->xs, t->ys); //scale and preserve aspect ratio
                 xs = t->xs*scale; ys = t->ys*scale;
                 x += int((size-xs)/2);
                 y += int((size-ys)/2);
-                glBindTexture(GL_TEXTURE_2D, t->id);
+                textureid = t->id;
             }
         }
         float xi = x, yi = y, xpad = 0, ypad = 0;
@@ -652,9 +912,22 @@ struct gui : guient
             xs -= 2*xpad;
             ys -= 2*ypad;
         }
-        static const float tc[4][2] = { { 0, 0 }, { 1, 0 }, { 1, 1 }, { 0, 1 } };
+        if(hit && hitfx)
+        {
+            float offx = xs*guihoverscale, offy = ys*guihoverscale;
+            if(!hovertex) hovertex = textureload(guihovertex, 3, true, false);
+            glBindTexture(GL_TEXTURE_2D, hovertex->id);
+            glColor4f((guihovercolour>>16)/255.f, ((guihovercolour>>8)&0xFF)/255.f, (guihovercolour&0xFF)/255.f, guihoverblend);
+            glBegin(GL_TRIANGLE_STRIP);
+            glTexCoord2fv(tc[0]); glVertex2f(xi-offx,    yi-offy);
+            glTexCoord2fv(tc[1]); glVertex2f(xi+xs+offx, yi-offy);
+            glTexCoord2fv(tc[3]); glVertex2f(xi-offx,    yi+ys+offy);
+            glTexCoord2fv(tc[2]); glVertex2f(xi+xs+offx, yi+ys+offy);
+            glEnd();
+        }
+        glBindTexture(GL_TEXTURE_2D, textureid);
         vec color = vec::hexcolor(icolor);
-        if(hit && hitfx && !overlaid) color.div(2);
+        //if(hit && hitfx && !overlaid) color.div(2);
         glColor3fv(color.v);
         glBegin(GL_TRIANGLE_STRIP);
         glTexCoord2fv(tc[0]); glVertex2f(xi,    yi);
@@ -665,7 +938,7 @@ struct gui : guient
         if(overlaid)
         {
             if(!overlaytex) overlaytex = textureload(guioverlaytex, 3, true, false);
-            const vec &ocolor = hit && hitfx ? vec(1, 0.25f, 0.25f) : vec(1, 1, 1);
+            const vec &ocolor = hit && hitfx ? vec::hexcolor(guiactivecolour) : vec(1, 1, 1);
             glColor3fv(ocolor.v);
             glBindTexture(GL_TEXTURE_2D, overlaytex->id);
             rect_(xi - xpad, yi - ypad, xs + 2*xpad, ys + 2*ypad, 0);
@@ -759,7 +1032,7 @@ struct gui : guient
         if(overlaid)
         {
             if(!overlaytex) overlaytex = textureload(guioverlaytex, 3, true, false);
-            const vec &ocolor = hit && hitfx ? vec(1.f, 0.25f, 0.25f) : vec(1, 1, 1);
+            const vec &ocolor = hit && hitfx ? vec::hexcolor(guiactivecolour) : vec(1, 1, 1);
             glColor3fv(ocolor.v);
             glBindTexture(GL_TEXTURE_2D, overlaytex->id);
             rect_(xi - xpad, yi - ypad, xs + 2*xpad, ys + 2*ypad, 0);
@@ -777,62 +1050,118 @@ struct gui : guient
         if(text && *text)
         {
             int w = text_width(text);
-            text_(text, x+s/2-w/2, y+s/2-FONTH/2, 0xFFFFFF, guiblend, false);
+            text_(text, x+s/2-w/2, y+s/2-FONTH/2, 0xFFFFFF, guitextblend, false);
         }
     }
 
-    int line_(int size, float percent = 1.0f, int space = 0)
+    int slider_(int size, float percent, int space, bool &hit, int style, int colour)
     {
-        space = max(max(space, guibound[0]), size);
+        space = max(max(space, FONTW), size);
         if(visible())
         {
-            if(!slidertex) slidertex = textureload(guislidertex, 3, true, false);
-            glBindTexture(GL_TEXTURE_2D, slidertex->id);
-            if(percent < 0.99f)
+            int x = ishorizontal() ? curx+space/2-size/2 : curx, w = ishorizontal() ? size : xsize,
+                y = ishorizontal() ? cury : cury+space/2-size/2, h = ishorizontal() ? ysize : size;
+            hit = ishit(w, h, x, y);
+            skin(x, y, x+w, y+h, guislidercolour, guisliderblend, hit ? guislideractivecolour : guisliderbordercolour, hit ? guislideractiveblend : guisliderborderblend, guisliderborderskin >= (hit ? 1 : 2));
+            if(percent >= 0 && percent <= 1)
             {
-                glColor4f(0.5f, 0.5f, 0.5f, 0.375f);
-                if(ishorizontal())
-                    rect_(curx + space/2 - size/2, cury, size, ysize, 0);
-                else
-                    rect_(curx, cury + space/2 - size/2, xsize, size, 1);
+                int px = x+size/8, py = y+size/8, pw = size*3/4, ph = size*3/4;
+                switch(style)
+                {
+                    case 1:
+                        if(ishorizontal()) ph = py+((h-size)*percent);
+                        else pw = px+((w-size)*percent);
+                        break;
+                    case 0: default:
+                        if(ishorizontal()) py += (h-size)*percent;
+                        else px += (w-size)*percent;
+                        break;
+                }
+                skin(px, py, px+pw, py+ph, colour >= 0 ? colour : guislidermarkcolour, guislidermarkblend, hit ? guislideractivecolour : guislidermarkbordercolour, hit ? guislideractiveblend : guislidermarkborderblend, guislidermarkborderskin >= (hit ? 1 : 2));
             }
-            glColor3f(0.5f, 0.5f, 0.5f);
-            if(ishorizontal())
-                rect_(curx + space/2 - size/2, cury + ysize*(1-percent), size, ysize*percent, 0);
-            else
-                rect_(curx, cury + space/2 - size/2, xsize*percent, size, 1);
         }
         layout(ishorizontal() ? space : 0, ishorizontal() ? 0 : space);
         return space;
     }
 
-    int button_(const char *text, int color, const char *icon, int icolor, bool clickable, bool faded, const char *font = "")
+    int line_(int size, int space = 0)
     {
-        if(font && *font) gui::pushfont(font);
-        int w = 0, h = max((int)FONTH, guibound[1]);
-        if(icon) w += guibound[1];
-        if(icon && text) w += 8;
-        if(text) w += text_width(text);
+        space = max(max(space, FONTW), size);
+        if(visible())
+        {
+            int colour1 = guibgcolour >= 0 ? guibgcolour : (guibordercolour >= 0 ? guibordercolour : 0x000000),
+                colour2 = guibordercolour >= 0 ? guibordercolour : 0x808080,
+                x1 = ishorizontal() ? curx+space/2-size/2 : curx, x2 = x1+(ishorizontal() ? size : xsize),
+                y1 = ishorizontal() ? cury : cury+space/2-size/2, y2 = y1+(ishorizontal() ? ysize : size);
+            if(colour1 >= 0)
+            {
+                notextureshader->set();
+                glDisable(GL_TEXTURE_2D);
+                glColor4f((colour1>>16)/255.f, ((colour1>>8)&0xFF)/255.f, (colour1&0xFF)/255.f, guibgblend);
+                glBegin(GL_TRIANGLE_STRIP);
+                glVertex2f(x1, y1);
+                glVertex2f(x2, y1);
+                glVertex2f(x1, y2);
+                glVertex2f(x2, y2);
+                xtraverts += 4;
+                glEnd();
+                defaultshader->set();
+                glEnable(GL_TEXTURE_2D);
+            }
+            if(colour2 >= 0)
+            {
+                lineshader->set();
+                glDisable(GL_TEXTURE_2D);
+                glColor4f((colour2>>16)/255.f, ((colour2>>8)&0xFF)/255.f, (colour2&0xFF)/255.f, guiborderblend);
+                glBegin(GL_LINE_LOOP);
+                glVertex2f(x1, y1);
+                glVertex2f(x2, y1);
+                glVertex2f(x2, y2);
+                glVertex2f(x1, y2);
+                xtraverts += 4;
+                glEnd();
+                defaultshader->set();
+                glEnable(GL_TEXTURE_2D);
+            }
+        }
+        layout(ishorizontal() ? space : 0, ishorizontal() ? 0 : space);
+        return space;
+    }
+
+    int button_(const char *text, int color, const char *icon, int icolor, bool clickable, int wrap = -1, bool faded = true)
+    {
+        int w = 0, h = 0;
+        if(icon && *icon)
+        {
+            w += FONTH;
+            if(text && *text) w += 8;
+        }
+        if(text && *text)
+        {
+            int tw = 0, th = 0;
+            text_bounds(text, tw, th, wrap > 0 ? wrap : -1);
+            w += tw;
+            h += th;
+        }
 
         if(visible())
         {
-            bool hit = ishit(w, FONTH), forcecolor = false;
-            if(hit && hitfx && clickable) { forcecolor = true; color = 0xFF4444; }
+            bool hit = ishit(w, h), forcecolor = false;
+            if(hit && hitfx && clickable) { forcecolor = true; color = guiactivecolour; }
             int x = curx;
-            if(icon)
+            if(icon && *icon)
             {
                 const char *tname = strstr(icon, "textures/") ? icon : makerelpath("textures", icon);
-                icon_(textureload(tname, 3, true, false), false, x, cury, guibound[1], faded && clickable && !hit, icolor);
-                x += guibound[1];
+                icon_(textureload(tname, 3, true, false), false, x, cury, FONTH, clickable && hit, icolor);
+                x += FONTH;
+                if(text && *text) x += 8;
             }
-            if(icon && text) x += 8;
-            if(text) text_(text, x, cury, color, (hit && hitfx) || !faded || !clickable ? 255 : guiblend, hit && clickable, forcecolor);
+            if(text && *text) text_(text, x, cury, color, (hit && hitfx) || !faded || !clickable ? guitextblend : guitextfade, hit && clickable, forcecolor, wrap > 0 ? wrap : -1);
         }
-        if(font && *font) gui::popfont();
         return layout(w, h);
     }
 
-    static Texture *overlaytex, *slidertex;
+    static Texture *skintex, *skinbordertex, *overlaytex, *exittex, *hovertex;
 
     vec uiorigin, uiscale;
     guicb *cb;
@@ -842,32 +1171,33 @@ struct gui : guient
 
     void adjustscale()
     {
-        int w = xsize + guibound[0]*4, h = ysize + guibound[1]*2;
+        int w = xsize + FONTW*8, h = ysize + FONTH*6;
         float aspect = forceaspect ? 1.0f/forceaspect : float(screen->h)/float(screen->w), fit = 1.0f;
         if(w*aspect*basescale>1.0f) fit = 1.0f/(w*aspect*basescale);
         if(h*basescale*fit>maxscale) fit *= maxscale/(h*basescale*fit);
         uiscale = vec(aspect*uiscale.x*fit, uiscale.y*fit, 1);
-        uiorigin = vec(0.5f - ((w-xsize)/2 - guibound[0])*uiscale.x, 0.5f + (0.5f*h-guibound[1])*uiscale.y, 0);
-        //uiorigin = vec(0.5f - (guibound[0]*2)*uiscale.x, 0.5f + (h-guibound[1]*2)*uiscale.y, 0);
+        uiorigin = vec(0.5f - ((w-xsize)/2 - (FONTW*4))*uiscale.x, 0.5f + (0.5f*h-(FONTH*2))*uiscale.y, 0);
     }
 
-    void start(int starttime, float initscale, int *tab, bool allowinput, bool wantstitle)
+    void start(int starttime, float initscale, int *tab, bool allowinput, bool wantstitle, bool wantsbgfx)
     {
+        fontdepth = 0;
+        gui::pushfont("reduced");
         initscale *= 0.025f;
         basescale = initscale;
-        if(layoutpass)
+        if(guilayoutpass)
             uiscale.x = uiscale.y = uiscale.z = guiscaletime ? min(basescale*(totalmillis-starttime)/float(guiscaletime), basescale) : basescale;
         needsinput = allowinput;
         hastitle = wantstitle;
+        hasbgfx = wantsbgfx;
         passthrough = !allowinput;
-        fontdepth = 0;
-        gui::pushfont("reduced");
         curdepth = curlist = mergedepth = mergelist = -1;
-        tpos = tx = ty = 0;
+        tpos = ty = 0;
+        tx = -FONTW;
         tcurrent = tab;
         tcolor = 0xFFFFFF;
         pushlist(false);
-        if(layoutpass) nextlist = curlist;
+        if(guilayoutpass) nextlist = curlist;
         else
         {
             if(tcurrent && !*tcurrent) tcurrent = NULL;
@@ -877,46 +1207,22 @@ struct gui : guient
             glPushMatrix();
             glTranslatef(uiorigin.x, uiorigin.y, uiorigin.z);
             glScalef(uiscale.x, uiscale.y, uiscale.z);
-            int x = curx-guibound[0]*2, y = cury-guibound[1], w = xsize+guibound[0]*4, h = ysize+guibound[1]*2;
-            if(hastitle)
-            {
-                y -= guibound[1]*3/2;
-                h += guibound[1]*3/2;
-            }
-            if(guibgcolour >= 0)
-            {
-                notextureshader->set();
-                glDisable(GL_TEXTURE_2D);
-                hud::drawblend(x, y, w, h, (guibgcolour>>16)/255.f, ((guibgcolour>>8)&0xFF)/255.f, (guibgcolour&0xFF)/255.f, true);
-                defaultshader->set();
-                glEnable(GL_TEXTURE_2D);
-            }
-            if(guibordercolour >= 0)
+            if(hasbgfx)
             {
-                lineshader->set();
-                glDisable(GL_TEXTURE_2D);
-                glDisable(GL_BLEND);
-                glColor3ub(guibordercolour>>16, (guibordercolour>>8)&0xFF, guibordercolour&0xFF);
-                glBegin(GL_LINE_LOOP);
-                glVertex2f(x, y);
-                glVertex2f(x+w, y);
-                glVertex2f(x+w, y+h);
-                glVertex2f(x, y+h);
-                glEnd();
-                defaultshader->set();
-                glEnable(GL_TEXTURE_2D);
-                glEnable(GL_BLEND);
+                int x1 = curx-FONTW, y1 = cury-FONTH/2, x2 = x1+xsize+FONTW*2, y2 = y1+ysize+FONTH;
+                skin(x1, y1, x2, y2, guibgcolour, guibgblend, guibordercolour, guiborderblend);
             }
         }
     }
 
     void end()
     {
-        if(layoutpass)
+        if(guilayoutpass)
         {
+            if(needsinput) uibuttons();
             xsize = max(tx, xsize);
             ysize = max(ty, ysize);
-            ysize = max(ysize, guibound[1]);
+            ysize = max(ysize, FONTH);
 
             if(tcurrent) *tcurrent = max(1, min(*tcurrent, tpos));
             adjustscale();
@@ -930,7 +1236,43 @@ struct gui : guient
         }
         else
         {
-            if(needsinput && hastitle) uibuttons();
+            if(guistatusline && statusstr && *statusstr)
+            {
+                gui::pushfont("little");
+                int width = 0, height = 0, tw = min(statuswidth ? statuswidth : (guistatuswidth ? guistatuswidth : -1), int(screen->w*(1/uiscale.y)));
+                text_bounds(statusstr, width, height, tw, TEXT_CENTERED|TEXT_NO_INDENT);
+                int w = width+FONTW*2, h = FONTH/2+height, x1 = -w/2, y1 = guispacesize*3/2, x2 = x1+w, y2 = y1+h;
+                if(hasbgfx) skin(x1, y1, x2, y2, guibgcolour, guibgblend, guibordercolour, guiborderblend);
+                draw_text(statusstr, x1+FONTW, y1+FONTH/4, 255, 255, 255, 255, TEXT_CENTERED|TEXT_NO_INDENT, -1, tw);
+                gui::popfont();
+            }
+            if(needsinput) uibuttons();
+            if((guitooltips || tooltipforce) && tooltipstr && *tooltipstr)
+            {
+                if(!tooltip || !lasttooltip || strcmp(tooltip, tooltipstr))
+                {
+                    if(tooltip) DELETEA(tooltip);
+                    tooltip = newstring(tooltipstr);
+                    lasttooltip = totalmillis;
+                }
+                if(tooltipforce || totalmillis-lasttooltip >= guitooltiptime)
+                {
+                    gui::pushfont("little");
+                    int width, height, tw = min(tooltipwidth ? tooltipwidth : (guitooltipwidth ? guitooltipwidth : -1), int(screen->w*(1/uiscale.y)));
+                    text_bounds(tooltipstr, width, height, tw, TEXT_NO_INDENT);
+                    int w = width+FONTW*2, h = FONTH/2+height, x1 = hitx, y1 = hity-height-FONTH/2, x2 = x1+w, y2 = y1+h,
+                        offset = totalmillis-lasttooltip-guitooltiptime;
+                    float blend = tooltipforce ? 1.f : (offset > 0 ? (offset < guitooltipfade ? offset/float(guitooltipfade) : 1.f) : 0.f);
+                    skin(x1, y1, x2, y2, guitooltipcolour, guitooltipblend*blend, guitooltipbordercolour, guitooltipborderblend*blend, guitooltipborderskin!=0);
+                    draw_text(tooltip, x1+FONTW, y1+FONTH/4, 255, 255, 255, int(255*blend), TEXT_NO_INDENT, -1, tw);
+                    gui::popfont();
+                }
+            }
+            else
+            {
+                if(tooltip) DELETEA(tooltip);
+                lasttooltip = 0;
+            }
             glPopMatrix();
         }
         poplist();
@@ -938,9 +1280,12 @@ struct gui : guient
     }
 };
 
-Texture *gui::overlaytex = NULL, *gui::slidertex = NULL;
+Texture *gui::skintex = NULL, *gui::skinbordertex, *gui::overlaytex = NULL, *gui::exittex = NULL, *gui::hovertex = NULL;
+TVARN(IDF_PERSIST|IDF_PRELOAD, guiskintex, "textures/guiskin", gui::skintex, 0);
+TVARN(IDF_PERSIST|IDF_PRELOAD, guiskinbordertex, "textures/guiskinborder", gui::skinbordertex, 0);
 TVARN(IDF_PERSIST|IDF_PRELOAD, guioverlaytex, "textures/guioverlay", gui::overlaytex, 0);
-TVARN(IDF_PERSIST|IDF_PRELOAD, guislidertex, "textures/guislider", gui::slidertex, 0);
+TVARN(IDF_PERSIST|IDF_PRELOAD, guiexittex, "textures/guiexit", gui::exittex, 0);
+TVARN(IDF_PERSIST|IDF_PRELOAD, guihovertex, "textures/guihover", gui::hovertex, 0);
 
 vector<gui::list> gui::lists;
 float gui::basescale, gui::maxscale = 1, gui::hitx, gui::hity;
@@ -951,15 +1296,7 @@ static vector<gui> guis;
 
 namespace UI
 {
-    bool isopen = false, ready = false;
-
-    void setup()
-    {
-        pushfont("reduced");
-        loopk(2) guibound[k] = (k ? FONTH : FONTW);
-        popfont();
-        ready = true;
-    }
+    bool isopen = false;
 
     bool keypress(int code, bool isdown, int cooked)
     {
@@ -969,7 +1306,11 @@ namespace UI
             switch(code)
             {
                 case SDLK_ESCAPE:
-                    if(isdown) fieldmode = FIELDCOMMIT;
+                    if(isdown)
+                    {
+                        fieldmode = FIELDCOMMIT;
+                        e->unfocus = true;
+                    }
                     return true;
             }
             const char *keyname = getkeyname(code);
@@ -988,7 +1329,7 @@ namespace UI
                 if(active()) return true;
                 break;
             case -3: mouseaction[0] |= GUI_ALT;
-            case -1: mouseaction[0] |= (actionon=isdown) ? GUI_DOWN : GUI_UP;
+            case -1: mouseaction[0] |= (guiactionon=isdown) ? GUI_DOWN : GUI_UP;
                 if(isdown) { firstx = gui::hitx; firsty = gui::hity; }
                 if(active()) return true;
                 break;
@@ -1060,13 +1401,12 @@ namespace UI
         if(isopen != p) uimillis = (isopen = p) ? totalmillis : -totalmillis;
         setsvar("guirollovername", "", true);
         setsvar("guirolloveraction", "", true);
-        setsvar("guirolloverimgpath", "", true);
-        setsvar("guirolloverimgaction", "", true);
+        setsvar("guirollovertype", "", true);
     }
 
     void render()
     {
-        if(actionon) mouseaction[0] |= GUI_PRESSED;
+        if(guiactionon) mouseaction[0] |= GUI_PRESSED;
 
         gui::reset();
         guis.shrink(0);
@@ -1085,14 +1425,14 @@ namespace UI
         fieldsactive = false;
 
         needsinput = false;
-        hastitle = true;
+        hastitle = hasbgfx = true;
 
         if(!guis.empty())
         {
-            layoutpass = true;
+            guilayoutpass = 1;
             //loopv(guis) guis[i].cb->gui(guis[i], true);
             guis.last().cb->gui(guis.last(), true);
-            layoutpass = false;
+            guilayoutpass = 0;
 
             glEnable(GL_BLEND);
             glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
diff --git a/src/engine/vertmodel.h b/src/engine/vertmodel.h
index 3b51562..ad6d71e 100644
--- a/src/engine/vertmodel.h
+++ b/src/engine/vertmodel.h
@@ -65,12 +65,11 @@ struct vertmodel : animmodel
                 mesh::calctangents(&bumpverts[k*numverts], &verts[k*numverts], tcverts, numverts, tris, numtris, areaweight);
         }
 
-        void calcbb(int frame, vec &bbmin, vec &bbmax, const matrix3x4 &m)
+        void calcbb(vec &bbmin, vec &bbmax, const matrix3x4 &m)
         {
-            vert *fverts = &verts[frame*numverts];
             loopj(numverts)
             {
-                vec v = m.transform(fverts[j].pos);
+                vec v = m.transform(verts[j].pos);
                 loopi(3)
                 {
                     bbmin[i] = min(bbmin[i], v[i]);
@@ -79,16 +78,15 @@ struct vertmodel : animmodel
             }
         }
 
-        void gentris(int frame, Texture *tex, vector<BIH::tri> *out, const matrix3x4 &m)
+        void gentris(Texture *tex, vector<BIH::tri> *out, const matrix3x4 &m)
         {
-            vert *fverts = &verts[frame*numverts];
             loopj(numtris)
             {
                 BIH::tri &t = out[noclip ? 1 : 0].add();
                 t.tex = tex;
-                t.a = m.transform(fverts[tris[j].vert[0]].pos);
-                t.b = m.transform(fverts[tris[j].vert[1]].pos);
-                t.c = m.transform(fverts[tris[j].vert[2]].pos);
+                t.a = m.transform(verts[tris[j].vert[0]].pos);
+                t.b = m.transform(verts[tris[j].vert[1]].pos);
+                t.c = m.transform(verts[tris[j].vert[2]].pos);
                 tcvert &av = tcverts[tris[j].vert[0]],
                        &bv = tcverts[tris[j].vert[1]],
                        &cv = tcverts[tris[j].vert[2]];
@@ -359,9 +357,9 @@ struct vertmodel : animmodel
 
         int totalframes() const { return numframes; }
 
-        void concattagtransform(part *p, int frame, int i, const matrix3x4 &m, matrix3x4 &n)
+        void concattagtransform(part *p, int i, const matrix3x4 &m, matrix3x4 &n)
         {
-            n.mul(m, tags[frame*numtags + i].transform);
+            n.mul(m, tags[numtags + i].transform);
             n.translate(m.transformnormal(p->translate).mul(p->model->scale));
         }
 
diff --git a/src/engine/water.cpp b/src/engine/water.cpp
index 0916f95..bc4f701 100644
--- a/src/engine/water.cpp
+++ b/src/engine/water.cpp
@@ -5,8 +5,8 @@ VARF(IDF_PERSIST, waterrefract, 0, 1, 1, { cleanreflections(); preloadwatershade
 VARF(IDF_PERSIST, waterenvmap, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
 VARF(IDF_PERSIST, waterfallrefract, 0, 0, 1, { cleanreflections(); preloadwatershaders(); });
 
-VAR(IDF_WORLD, watersubdiv, 0, 3, 3);
-VAR(IDF_WORLD, waterlod, 0, 1, 3);
+VAR(IDF_WORLD, watersubdiv, 0, 2, 3);
+VAR(IDF_WORLD, waterlod, 0, 2, 3);
 
 static int wx1, wy1, wx2, wy2, wsize;
 static float whscale, whoffset;
@@ -20,7 +20,7 @@ static uchar wcol[4];
     } \
     static inline void vertw(float v1, float v2, float v3) \
     { \
-        float angle = float((v1-wx1)*(v2-wy1))*float((v1-wx2)*(v2-wy2))*whscale+whoffset; \
+        float angle = (v1-wx1)*(v2-wy1)*(v1-wx2)*(v2-wy2)*whscale+whoffset; \
         float s = angle - int(angle) - 0.5f; \
         s *= 8 - fabs(s)*16; \
         float h = WATER_AMPLITUDE*s-WATER_OFFSET; \
@@ -157,7 +157,7 @@ VERTWN(vertln, {
     } \
 }
 
-void rendervertwater(uint subdiv, int xo, int yo, int z, uint size, int mat)
+void rendervertwater(int subdiv, int xo, int yo, int z, int size, int mat)
 {
     wx1 = xo;
     wy1 = yo;
@@ -201,47 +201,40 @@ void rendervertwater(uint subdiv, int xo, int yo, int z, uint size, int mat)
     }
 }
 
-uint calcwatersubdiv(int x, int y, int z, uint size)
+int calcwatersubdiv(int x, int y, int z, int size)
 {
     float dist;
     if(camera1->o.x >= x && camera1->o.x < x + size &&
-        camera1->o.y >= y && camera1->o.y < y + size)
+       camera1->o.y >= y && camera1->o.y < y + size)
         dist = fabs(camera1->o.z - float(z));
     else
-    {
-        vec t(x + size/2, y + size/2, z + size/2);
-        dist = t.dist(camera1->o) - size*1.42f/2;
-    }
-    uint subdiv = watersubdiv + int(dist) / (32 << waterlod);
-    if(subdiv >= 8*sizeof(subdiv))
-        subdiv = ~0;
-    else
-        subdiv = 1 << subdiv;
-    return subdiv;
+        dist = vec(x + size/2, y + size/2, z + size/2).dist(camera1->o) - size*1.42f/2;
+    int subdiv = watersubdiv + int(dist) / (32 << waterlod);
+    return subdiv >= 31 ? INT_MAX : 1<<subdiv;
 }
 
-uint renderwaterlod(int x, int y, int z, uint size, int mat)
+int renderwaterlod(int x, int y, int z, int size, int mat)
 {
-    if(size <= (uint)(32 << waterlod))
+    if(size <= (32 << waterlod))
     {
-        uint subdiv = calcwatersubdiv(x, y, z, size);
+        int subdiv = calcwatersubdiv(x, y, z, size);
         if(subdiv < size * 2) rendervertwater(min(subdiv, size), x, y, z, size, mat);
         return subdiv;
     }
     else
     {
-        uint subdiv = calcwatersubdiv(x, y, z, size);
+        int subdiv = calcwatersubdiv(x, y, z, size);
         if(subdiv >= size)
         {
             if(subdiv < size * 2) rendervertwater(size, x, y, z, size, mat);
             return subdiv;
         }
-        uint childsize = size / 2,
-             subdiv1 = renderwaterlod(x, y, z, childsize, mat),
-             subdiv2 = renderwaterlod(x + childsize, y, z, childsize, mat),
-             subdiv3 = renderwaterlod(x + childsize, y + childsize, z, childsize, mat),
-             subdiv4 = renderwaterlod(x, y + childsize, z, childsize, mat),
-             minsubdiv = subdiv1;
+        int childsize = size / 2,
+            subdiv1 = renderwaterlod(x, y, z, childsize, mat),
+            subdiv2 = renderwaterlod(x + childsize, y, z, childsize, mat),
+            subdiv3 = renderwaterlod(x + childsize, y + childsize, z, childsize, mat),
+            subdiv4 = renderwaterlod(x, y + childsize, z, childsize, mat),
+            minsubdiv = subdiv1;
         minsubdiv = min(minsubdiv, subdiv2);
         minsubdiv = min(minsubdiv, subdiv3);
         minsubdiv = min(minsubdiv, subdiv4);
@@ -270,7 +263,7 @@ uint renderwaterlod(int x, int y, int z, uint size, int mat)
         xtraverts += 4; \
     }
 
-void renderflatwater(int x, int y, int z, uint rsize, uint csize, int mat)
+void renderflatwater(int x, int y, int z, int rsize, int csize, int mat)
 {
     switch(mat)
     {
@@ -303,7 +296,7 @@ VARF(IDF_WORLD, vertwater, 0, 1, 1, if(!(identflags&IDF_WORLD)) allchanged());
 static inline void renderwater(const materialsurface &m, int mat = MAT_WATER)
 {
     if(!vertwater || minimapping) renderflatwater(m.o.x, m.o.y, m.o.z, m.rsize, m.csize, mat);
-    else if(renderwaterlod(m.o.x, m.o.y, m.o.z, m.csize, mat) >= (uint)m.csize * 2)
+    else if(renderwaterlod(m.o.x, m.o.y, m.o.z, m.csize, mat) >= int(m.csize) * 2)
         rendervertwater(m.csize, m.o.x, m.o.y, m.o.z, m.csize, mat);
 }
 
@@ -322,12 +315,13 @@ void renderlava(const materialsurface &m, Texture *tex, float scale)
 struct Reflection
 {
     GLuint tex, refracttex;
-    int material, height, depth, lastupdate, lastused;
+    int material, height, depth, age;
+    bool init;
     glmatrixf projmat;
     occludequery *query, *prevquery;
     vector<materialsurface *> matsurfs;
 
-    Reflection() : tex(0), refracttex(0), material(-1), height(-1), depth(0), lastused(0), query(NULL), prevquery(NULL)
+    Reflection() : tex(0), refracttex(0), material(-1), height(-1), depth(0), age(0), init(false), query(NULL), prevquery(NULL)
     {}
 };
 
@@ -375,9 +369,13 @@ GETMATIDXVAR(lava, colour, int)
 GETMATIDXVAR(lava, col, const bvec &)
 GETMATIDXVAR(lava, fog, int)
 
-void setprojtexmatrix(Reflection &ref, bool init = true)
+void setprojtexmatrix(Reflection &ref)
 {
-    if(init && ref.lastupdate==totalmillis) (ref.projmat = mvpmatrix).projective();
+    if(ref.init)
+    {
+        ref.init = false;
+        (ref.projmat = mvpmatrix).projective();
+    }
 
     glLoadMatrixf(ref.projmat.v);
 }
@@ -474,7 +472,7 @@ void renderwaterff()
     loopi(MAXREFLECTIONS)
     {
         Reflection &ref = reflections[i];
-        if(ref.height<0 || ref.lastused<totalmillis || ref.matsurfs.empty()) continue;
+        if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue;
 
         bool below = camera1->o.z < ref.height + offset;
         if(!nowater && (waterrefract || waterreflect || (waterenvmap && hasCM)) && !minimapping)
@@ -487,7 +485,6 @@ void renderwaterff()
                 }
             }
 
-            bool projtex = false;
             if(waterreflect || (waterenvmap && hasCM))
             {
                 bool tmu1 = waterrefract && (!below || !wasbelow);
@@ -504,9 +501,8 @@ void renderwaterff()
                     {
                         glBindTexture(GL_TEXTURE_2D, ref.tex);
                         setprojtexmatrix(ref);
-                        projtex = true;
                     }
-                    else 
+                    else
                     {
                         glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, lookupenvmap(lookupmaterialslot(ref.material)));
                     }
@@ -522,7 +518,7 @@ void renderwaterff()
             if(waterrefract)
             {
                 glBindTexture(GL_TEXTURE_2D, ref.refracttex);
-                setprojtexmatrix(ref, !projtex);
+                setprojtexmatrix(ref);
             }
         }
 
@@ -688,7 +684,7 @@ void renderwater()
     loopi(MAXREFLECTIONS)
     {
         Reflection &ref = reflections[i];
-        if(ref.height<0 || ref.lastused<totalmillis || ref.matsurfs.empty()) continue;
+        if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue;
         if(!glaring && hasOQ && oqfrags && oqwater && ref.query && ref.query->owner==&ref)
         {
             if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
@@ -824,7 +820,7 @@ void cleanreflection(Reflection &ref)
 {
     ref.material = -1;
     ref.height = -1;
-    ref.lastupdate = 0;
+    ref.init = false;
     ref.query = ref.prevquery = NULL;
     ref.matsurfs.setsize(0);
     if(ref.tex)
@@ -935,9 +931,10 @@ void genwatertex(GLuint &tex, GLuint &fb, GLuint &db, bool refract = false)
 void addwaterfallrefraction(materialsurface &m)
 {
     Reflection &ref = waterfallrefraction;
-    if(ref.lastused!=totalmillis)
+    if(ref.age>=0)
     {
-        ref.lastused = totalmillis;
+        ref.age = -1;
+        ref.init = false;
         ref.matsurfs.setsize(0);
         ref.material = MAT_WATER;
         ref.height = INT_MAX;
@@ -962,25 +959,26 @@ void addreflection(materialsurface &m)
         {
             r.matsurfs.add(&m);
             r.depth = max(r.depth, int(m.depth));
-            if(r.lastused==totalmillis) return;
+            if(r.age<0) return;
             ref = &r;
             break;
         }
-        else if(!oldest || r.lastused<oldest->lastused) oldest = &r;
+        else if(!oldest || r.age>oldest->age) oldest = &r;
     }
     if(!ref)
     {
-        if(!oldest || oldest->lastused==totalmillis) return;
+        if(!oldest || oldest->age<0) return;
         ref = oldest;
     }
-    if(ref->height!=height || ref->material!=mat) 
+    if(ref->height!=height || ref->material!=mat)
     {
         ref->material = mat;
         ref->height = height;
         ref->prevquery = NULL;
     }
     rplanes++;
-    ref->lastused = totalmillis;
+    ref->age = -1;
+    ref->init = false;
     ref->matsurfs.setsize(0);
     ref->matsurfs.add(&m);
     ref->depth = m.depth;
@@ -1003,7 +1001,7 @@ static void drawmaterialquery(const materialsurface &m, float offset, float bord
 #define GENFACEORIENT(orient, v0, v1, v2, v3) \
         case orient: v0 v1 v2 v3 break;
 #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
-            varray::attrib<float>(mx sx, my sy, mz sz); 
+            varray::attrib<float>(mx sx, my sy, mz sz);
         GENFACEVERTS(x, x, y, y, z, z, - border, + csize, - border, + rsize, + offset, - offset)
 #undef GENFACEORIENT
 #undef GENFACEVERT
@@ -1015,8 +1013,6 @@ extern void drawreflection(float z, bool refract, int fogdepth = -1, const bvec
 
 int rplanes = 0;
 
-static int lastquery = 0;
-
 void queryreflection(Reflection &ref, bool init)
 {
     if(init)
@@ -1079,7 +1075,8 @@ void queryreflections()
     loopi(MAXREFLECTIONS)
     {
         Reflection &ref = reflections[i];
-        if(ref.height>=0 && ref.lastused>=totalmillis && ref.matsurfs.length())
+        ++ref.age;
+        if(ref.height>=0 && !ref.age && ref.matsurfs.length())
         {
             if(waterpvsoccluded(ref.height)) ref.matsurfs.setsize(0);
         }
@@ -1087,14 +1084,13 @@ void queryreflections()
     if(renderpath!=R_FIXEDFUNCTION && waterfallrefract)
     {
         Reflection &ref = waterfallrefraction;
-        if(ref.height>=0 && ref.lastused>=totalmillis && ref.matsurfs.length())
+        ++ref.age;
+        if(ref.height>=0 && !ref.age && ref.matsurfs.length())
         {
             if(waterpvsoccluded(-1)) ref.matsurfs.setsize(0);
         }
     }
 
-    lastquery = totalmillis;
-
     if((editmode && showmat && !envmapping) || !hasOQ || !oqfrags || !oqwater || nowater || minimapping) return;
 
     varray::enable();
@@ -1104,14 +1100,14 @@ void queryreflections()
     {
         Reflection &ref = reflections[i];
         ref.prevquery = oqwater > 1 ? ref.query : NULL;
-        ref.query = ref.height>=0 && ref.lastused>=totalmillis && ref.matsurfs.length() ? newquery(&ref) : NULL;
+        ref.query = ref.height>=0 && !ref.age && ref.matsurfs.length() ? newquery(&ref) : NULL;
         if(ref.query) queryreflection(ref, !refs++);
     }
     if(renderpath!=R_FIXEDFUNCTION && waterfallrefract)
     {
         Reflection &ref = waterfallrefraction;
         ref.prevquery = oqwater > 1 ? ref.query : NULL;
-        ref.query = ref.height>=0 && ref.lastused>=totalmillis && ref.matsurfs.length() ? newquery(&ref) : NULL;
+        ref.query = ref.height>=0 && !ref.age && ref.matsurfs.length() ? newquery(&ref) : NULL;
         if(ref.query) queryreflection(ref, !refs++);
     }
 
@@ -1128,7 +1124,7 @@ void queryreflections()
     glFlush();
 }
 
-VAR(IDF_PERSIST, maxreflect, 1, 1, 8);
+VAR(IDF_PERSIST, maxreflect, 1, 2, 8);
 
 int refracting = 0, refractfog = 0;
 bvec refractcolor(0, 0, 0);
@@ -1280,9 +1276,9 @@ void drawreflections()
     if(waterreflect || waterrefract) loopi(MAXREFLECTIONS)
     {
         Reflection &ref = reflections[++n%MAXREFLECTIONS];
-        if(ref.height<0 || ref.lastused<lastquery || ref.matsurfs.empty()) continue;
+        if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue;
         if(hasOQ && oqfrags && oqwater && ref.query && ref.query->owner==&ref)
-        { 
+        {
             if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
             {
                 if(checkquery(ref.query)) continue;
@@ -1295,7 +1291,7 @@ void drawreflections()
             if(hasFBO) glBindFramebuffer_(GL_FRAMEBUFFER_EXT, reflectionfb);
         }
         refs++;
-        ref.lastupdate = totalmillis;
+        ref.init = true;
         lastdrawn = n;
 
         vec clipmin(-1, -1, -1), clipmax(1, 1, 1);
@@ -1319,7 +1315,7 @@ void drawreflections()
             maskreflection(ref, offset, true);
             if(scissor && nvidia_scissor_bug) glEnable(GL_SCISSOR_TEST);
             savevfcP();
-            setvfcP(ref.height+offset, clipmin, clipmax); 
+            setvfcP(ref.height+offset, clipmin, clipmax);
             drawreflection(ref.height+offset, false);
             restorevfcP();
             if(scissor) glDisable(GL_SCISSOR_TEST);
@@ -1358,7 +1354,7 @@ void drawreflections()
     {
         Reflection &ref = waterfallrefraction;
 
-        if(ref.height<0 || ref.lastused<lastquery || ref.matsurfs.empty()) goto nowaterfall;
+        if(ref.height<0 || ref.age || ref.matsurfs.empty()) goto nowaterfall;
         if(hasOQ && oqfrags && oqwater && ref.query && ref.query->owner==&ref)
         {
             if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
@@ -1373,7 +1369,7 @@ void drawreflections()
             if(hasFBO) glBindFramebuffer_(GL_FRAMEBUFFER_EXT, reflectionfb);
         }
         refs++;
-        ref.lastupdate = totalmillis;
+        ref.init = true;
 
         vec clipmin(-1, -1, -1), clipmax(1, 1, 1);
         int sx, sy, sw, sh;
diff --git a/src/engine/world.cpp b/src/engine/world.cpp
index e88751c..fc9eab9 100644
--- a/src/engine/world.cpp
+++ b/src/engine/world.cpp
@@ -5,10 +5,34 @@
 mapz hdr;
 int worldscale;
 
-VAR(0, octaentsize, 0, 128, 1024);
+VAR(0, octaentsize, 0, 64, 1024);
 VAR(0, entselradius, 0, 2, 10);
 
-bool getentboundingbox(extentity &e, ivec &o, ivec &r)
+static inline void mmboundbox(const entity &e, model *m, vec &center, vec &radius)
+{
+    m->boundbox(center, radius);
+    if(e.attrs[5])
+    {
+       float scale = max(e.attrs[5]/100.0f, 1e-3f);
+       center.mul(scale);
+       radius.mul(scale);
+    }
+    rotatebb(center, radius, e.attrs[1], e.attrs[2], e.attrs[3]);
+}
+
+static inline void mmcollisionbox(const entity &e, model *m, vec &center, vec &radius)
+{
+    m->collisionbox(center, radius);
+    if(e.attrs[5])
+    {
+       float scale = max(e.attrs[5]/100.0f, 1e-3f);
+       center.mul(scale);
+       radius.mul(scale);
+    }
+    rotatebb(center, radius, e.attrs[1], e.attrs[2], e.attrs[3]);
+}
+
+bool getentboundingbox(const extentity &e, ivec &o, ivec &r)
 {
     switch(e.type)
     {
@@ -16,30 +40,22 @@ bool getentboundingbox(extentity &e, ivec &o, ivec &r)
             return false;
         case ET_MAPMODEL:
         {
-            model *m = loadmodel(NULL, e.attrs[0]);
+            model *m = loadmapmodel(e.attrs[0]);
             if(m)
             {
                 vec center, radius;
-                m->boundbox(0, center, radius);
-                if(e.attrs[5])
-                {
-                    float scale = max(e.attrs[5]/100.0f, 1e-3f);
-                    center.mul(scale);
-                    radius.mul(scale);
-                }
-                rotatebb(center, radius, e.attrs[1], e.attrs[2], e.attrs[3]);
-                o = vec(center).add(e.o);
-                r = vec(radius).add(1);
-                o.sub(r);
-                r.mul(2);
+                mmboundbox(e, m, center, radius);
+                center.add(e.o);
+                radius.max(entselradius);
+                o = vec(center).sub(radius);
+                r = vec(center).add(radius).add(1);
                 break;
             }
         }
         // invisible mapmodels use entselradius
         default:
-            o = e.o;
-            o.sub(entselradius);
-            r.x = r.y = r.z = entselradius*2;
+            o = vec(e.o).sub(entselradius);
+            r = vec(e.o).add(entselradius+1);
             break;
     }
     return true;
@@ -67,7 +83,7 @@ void modifyoctaentity(int flags, int id, extentity &e, cube *c, const ivec &cor,
             switch(e.type)
             {
                 case ET_MAPMODEL:
-                    if(loadmodel(NULL, e.attrs[0]))
+                    if(loadmapmodel(e.attrs[0]))
                     {
                         if(va)
                         {
@@ -75,11 +91,8 @@ void modifyoctaentity(int flags, int id, extentity &e, cube *c, const ivec &cor,
                             if(oe.mapmodels.empty()) va->mapmodels.add(&oe);
                         }
                         oe.mapmodels.add(id);
-                        loopk(3)
-                        {
-                            oe.bbmin[k] = min(oe.bbmin[k], max(oe.o[k], bo[k]));
-                            oe.bbmax[k] = max(oe.bbmax[k], min(oe.o[k]+size, bo[k]+br[k]));
-                        }
+                        oe.bbmin.min(bo).max(oe.o);
+                        oe.bbmax.max(br).min(ivec(oe.o).add(oe.size));
                         break;
                     }
                     // invisible mapmodel
@@ -95,7 +108,7 @@ void modifyoctaentity(int flags, int id, extentity &e, cube *c, const ivec &cor,
             switch(e.type)
             {
                 case ET_MAPMODEL:
-                    if(loadmodel(NULL, e.attrs[0]))
+                    if(loadmapmodel(e.attrs[0]))
                     {
                         oe.mapmodels.removeobj(id);
                         if(va)
@@ -109,17 +122,14 @@ void modifyoctaentity(int flags, int id, extentity &e, cube *c, const ivec &cor,
                         {
                             extentity &e = *entities::getents()[oe.mapmodels[j]];
                             ivec eo, er;
-                            if(getentboundingbox(e, eo, er)) loopk(3)
+                            if(getentboundingbox(e, eo, er))
                             {
-                                oe.bbmin[k] = min(oe.bbmin[k], eo[k]);
-                                oe.bbmax[k] = max(oe.bbmax[k], eo[k]+er[k]);
+                                oe.bbmin.min(eo);
+                                oe.bbmax.max(er);
                             }
                         }
-                        loopk(3)
-                        {
-                            oe.bbmin[k] = max(oe.bbmin[k], oe.o[k]);
-                            oe.bbmax[k] = min(oe.bbmax[k], oe.o[k]+size);
-                        }
+                        oe.bbmin.max(oe.o);
+                        oe.bbmax.min(ivec(oe.o).add(oe.size));
                         break;
                     }
                     // invisible mapmodel
@@ -146,7 +156,7 @@ vector<int> outsideents;
 
 static bool modifyoctaent(int flags, int id, extentity &e)
 {
-    if(flags&MODOE_ADD ? e.inoctanode : !e.inoctanode) return false;
+    if(flags&MODOE_ADD ? e.flags&EF_OCTA : !(e.flags&EF_OCTA)) return false;
 
     ivec o, r;
     if(!getentboundingbox(e, o, r)) return false;
@@ -162,13 +172,13 @@ static bool modifyoctaent(int flags, int id, extentity &e)
     }
     else
     {
-        int leafsize = octaentsize, limit = max(r.x, max(r.y, r.z));
+        int leafsize = octaentsize, limit = max(r.x - o.x, max(r.y - o.y, r.z - o.z));
         while(leafsize < limit) leafsize *= 2;
-        int diff = ~(leafsize-1) & ((o.x^(o.x+r.x))|(o.y^(o.y+r.y))|(o.z^(o.z+r.z)));
+        int diff = ~(leafsize-1) & ((o.x^r.x)|(o.y^r.y)|(o.z^r.z));
         if(diff && (limit > octaentsize/2 || diff < leafsize*2)) leafsize *= 2;
         modifyoctaentity(flags, id, e, worldroot, ivec(0, 0, 0), hdr.worldsize>>1, o, r, leafsize);
     }
-    e.inoctanode = flags&MODOE_ADD ? 1 : 0;
+    e.flags ^= EF_OCTA;
     if(e.type == ET_LIGHT || e.type == ET_SUNLIGHT) clearlightcache(id);
     else if(flags&MODOE_LIGHTENT) lightent(e);
     return true;
@@ -204,26 +214,26 @@ void entitiesinoctanodes()
     loopv(ents) modifyoctaent(MODOE_ADD, i, *ents[i]);
 }
 
-static inline void findents(octaentities &oe, int low, int high, bool notspawned, const vec &pos, const vec &radius, vector<int> &found)
+static inline void findents(octaentities &oe, int low, int high, bool notspawned, const vec &pos, const vec &invradius, vector<int> &found)
 {
     vector<extentity *> &ents = entities::getents();
     loopv(oe.other)
     {
         int id = oe.other[i];
         extentity &e = *ents[id];
-        if(e.type >= low && e.type <= high && (e.spawned || notspawned) && vec(e.o).mul(radius).squaredlen() <= 1) found.add(id);
+        if(e.type >= low && e.type <= high && (e.spawned() || notspawned) && vec(e.o).sub(pos).mul(invradius).squaredlen() <= 1) found.add(id);
     }
 }
 
-static inline void findents(cube *c, const ivec &o, int size, const ivec &bo, const ivec &br, int low, int high, bool notspawned, const vec &pos, const vec &radius, vector<int> &found)
+static inline void findents(cube *c, const ivec &o, int size, const ivec &bo, const ivec &br, int low, int high, bool notspawned, const vec &pos, const vec &invradius, vector<int> &found)
 {
     loopoctabox(o, size, bo, br)
     {
-        if(c[i].ext && c[i].ext->ents) findents(*c[i].ext->ents, low, high, notspawned, pos, radius, found);
+        if(c[i].ext && c[i].ext->ents) findents(*c[i].ext->ents, low, high, notspawned, pos, invradius, found);
         if(c[i].children && size > octaentsize)
         {
             ivec co(i, o.x, o.y, o.z, size);
-            findents(c[i].children, co, size>>1, bo, br, low, high, notspawned, pos, radius, found);
+            findents(c[i].children, co, size>>1, bo, br, low, high, notspawned, pos, invradius, found);
         }
     }
 }
@@ -232,10 +242,10 @@ void findents(int low, int high, bool notspawned, const vec &pos, const vec &rad
 {
     vec invradius(1/radius.x, 1/radius.y, 1/radius.z);
     ivec bo = vec(pos).sub(radius).sub(1),
-         br = vec(radius).add(1).mul(2);
-    int diff = (bo.x^(bo.x+br.x)) | (bo.y^(bo.y+br.y)) | (bo.z^(bo.z+br.z)) | octaentsize,
+         br = vec(pos).add(radius).add(1);
+    int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z) | octaentsize,
         scale = worldscale-1;
-    if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|(bo.x+br.x)|(bo.y+br.y)|(bo.z+br.z)) >= uint(hdr.worldsize))
+    if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|br.x|br.y|br.z) >= uint(hdr.worldsize))
     {
         findents(worldroot, ivec(0, 0, 0), 1<<scale, bo, br, low, high, notspawned, pos, invradius, found);
         return;
@@ -265,7 +275,7 @@ bool noentedit()
     return !entediting;
 }
 
-bool pointinsel(selinfo &sel, vec &o)
+bool pointinsel(const selinfo &sel, const vec &o)
 {
     return(o.x <= sel.o.x+sel.s.x*sel.grid
         && o.x >= sel.o.x
@@ -331,17 +341,20 @@ void makeundoent()
 // e         entity, currently edited ent
 // n         int,   index to currently edited ent
 #define addimplicit(f)  { if(entgroup.empty() && enthover>=0) { entadd(enthover); undonext = (enthover != oldhover); f; entgroup.drop(); } else f; }
-#define entedit(i, f) \
+#define enteditv(i, f, v) \
 { \
-    entfocus(i, \
-    removeentity(n);  \
-    f; \
-    if(e.type!=ET_EMPTY) { addentity(n); } \
-    entities::editent(n)); \
+    entfocusv(i, \
+    { \
+        removeentity(n);  \
+        f; \
+        if(e.type!=ET_EMPTY) { addentity(n); } \
+        entities::editent(n); \
+    }, v); \
 }
-#define addgroup(exp)   { loopv(entities::getents()) entfocus(i, if(exp) entadd(n)); }
+#define entedit(i, f)   enteditv(i, f, entities::getents())
+#define addgroup(exp)   { vector<extentity *> &ents = entities::getents(); loopv(ents) entfocusv(i, if(exp) entadd(n), ents); }
 #define setgroup(exp)   { entcancel(); addgroup(exp); }
-#define groupeditloop(f){ entlooplevel++; int _ = efocus; loopv(entgroup) entedit(entgroup[i], f); efocus = _; entlooplevel--; }
+#define groupeditloop(f){ vector<extentity *> &ents = entities::getents(); entlooplevel++; int _ = efocus; loopv(entgroup) enteditv(entgroup[i], f, ents); efocus = _; entlooplevel--; }
 #define groupeditpure(f){ if(entlooplevel>0) { entedit(efocus, f); } else groupeditloop(f); }
 #define groupeditundo(f){ makeundoent(); groupeditpure(f); }
 #define groupedit(f)    { addimplicit(groupeditundo(f)); }
@@ -399,15 +412,10 @@ void entrotate(int *cw)
 void entselectionbox(const extentity &e, vec &eo, vec &es)
 {
     model *m = NULL;
-    if(e.type == ET_MAPMODEL && (m = loadmodel(NULL, e.attrs[0])))
+    if(e.type == ET_MAPMODEL && (m = loadmapmodel(e.attrs[0])))
     {
-        m->collisionbox(0, eo, es);
-        if(e.attrs[5]) { float scale = max(e.attrs[5]/100.f, 1e-3f); eo.mul(scale); es.mul(scale); }
-        rotatebb(eo, es, e.attrs[1], e.attrs[2], e.attrs[3]);
-        if(m->collide)
-            eo.z -= camera1->aboveeye; // wacky but true. see physics collide
-        else
-            es.div(2);  // cause the usual bb is too big...
+        mmcollisionbox(e, m, eo, es);
+        es.max(entselradius);
         eo.add(e.o);
     }
     else
@@ -424,9 +432,9 @@ VAR(0, entmovingshadow, 0, 1, 1);
 
 extern void boxs(int orient, vec o, const vec &s);
 extern void boxs3D(const vec &o, vec s, int g);
-extern void editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first);
+extern bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first);
 
-bool initentdragging = true;
+int entmoving = 0;
 
 void entdrag(const vec &ray)
 {
@@ -441,7 +449,8 @@ void entdrag(const vec &ray)
     entfocus(entgroup.last(),
         entselectionbox(e, eo, es);
 
-        editmoveplane(e.o, ray, d, eo[d] + (dc ? es[d] : 0), handle, v, initentdragging);
+        if(!editmoveplane(e.o, ray, d, eo[d] + (dc ? es[d] : 0), handle, v, entmoving==1))
+            return;
 
         ivec g(v);
         int z = g[d]&(~(sel.grid-1));
@@ -452,9 +461,9 @@ void entdrag(const vec &ray)
         c = (entselsnap ? g[C[d]] : v[C[d]]) - e.o[C[d]];
     );
 
-    if(initentdragging) makeundoent();
+    if(entmoving==1) makeundoent();
     groupeditpure(e.o[R[d]] += r; e.o[C[d]] += c);
-    initentdragging = false;
+    entmoving = 2;
 }
 
 void renderentselection(const vec &o, const vec &ray, bool entmoving)
@@ -510,16 +519,35 @@ bool hoveringonent(int ent, int orient)
 }
 
 VAR(0, entitysurf, 0, 0, 1);
-VARF(0, entmoving, 0, 0, 2,
-    if(enthover < 0 || noentedit())
-        entmoving = 0;
-    else if(entmoving == 1)
-        entmoving = enttoggle(enthover);
-    else if(entmoving == 2 && entgroup.find(enthover) < 0)
-        entadd(enthover);
-    if(entmoving > 0)
-        initentdragging = true;
-);
+
+ICOMMAND(0, entadd, "", (),
+{
+    if(enthover >= 0 && !noentedit())
+    {
+        if(entgroup.find(enthover) < 0) entadd(enthover);
+        if(entmoving > 1) entmoving = 1;
+    }
+});
+
+ICOMMAND(0, enttoggle, "", (),
+{
+    if(enthover < 0 || noentedit() || !enttoggle(enthover)) { entmoving = 0; intret(0); }
+    else { if(entmoving > 1) entmoving = 1; intret(1); }
+});
+
+ICOMMAND(0, entmoving, "b", (int *n),
+{
+    if(*n >= 0)
+    {
+        if(!*n || enthover < 0 || noentedit()) entmoving = 0;
+        else
+        {
+            if(entgroup.find(enthover) < 0) { entadd(enthover); entmoving = 1; }
+            else if(!entmoving) entmoving = 1;
+        }
+    }
+    intret(entmoving);
+});
 
 void entpush(int *dir)
 {
@@ -599,13 +627,11 @@ bool dropentity(extentity &e, int drop = -1)
     if(drop<0) drop = entdrop;
     if(e.type == ET_MAPMODEL)
     {
-        model *m = loadmodel(NULL, e.attrs[0]);
+        model *m = loadmapmodel(e.attrs[0]);
         if(m)
         {
             vec center;
-            m->boundbox(0, center, radius);
-            if(e.attrs[5]) { float scale = max(e.attrs[5]/100.f, 1e-3f); center.mul(scale); radius.mul(scale); }
-            rotatebb(center, radius, e.attrs[1], e.attrs[2], e.attrs[3]);
+            mmboundbox(e, m, center, radius);
             radius.x += fabs(center.x);
             radius.y += fabs(center.y);
         }
@@ -648,7 +674,7 @@ void dropent()
 
 static int keepents = 0;
 
-extentity *newentity(bool local, const vec &o, int type, const attrvector &attrs, int &idx)
+extentity *newentity(bool local, const vec &o, int type, const attrvector &attrs, int &idx, bool fix = true)
 {
     vector<extentity *> &ents = entities::getents();
     if(local)
@@ -660,40 +686,40 @@ extentity *newentity(bool local, const vec &o, int type, const attrvector &attrs
     else while(ents.length() < idx) ents.add(entities::newent())->type = ET_EMPTY;
     extentity &e = *entities::newent();
     e.o = o;
-    e.attrs.add(0, clamp(attrs.length(), entities::numattrs(type), MAXENTATTRS) - e.attrs.length());
+    e.attrs.add(0, min(attrs.length(), MAXENTATTRS) - e.attrs.length());
     loopi(min(attrs.length(), e.attrs.length())) e.attrs[i] = attrs[i];
     e.type = type;
-    e.spawned = false;
-    e.inoctanode = false;
     e.light.color = vec(1, 1, 1);
     e.light.dir = vec(0, 0, 1);
     if(ents.inrange(idx)) { entities::deleteent(ents[idx]); ents[idx] = &e; }
     else { idx = ents.length(); ents.add(&e); }
-    if(local) entities::fixentity(idx, true, true);
+    if(local && fix) entities::fixentity(idx, true, true);
     return &e;
 }
 
-void newentity(const vec &v, int type, const attrvector &attrs)
+int newentity(const vec &v, int type, const attrvector &attrs)
 {
-    int idx;
+    int idx = -1;
     extentity *t = newentity(true, v, type, attrs, idx);
-    if(!t) return;
+    if(!t) return -1;
     t->type = ET_EMPTY;
     enttoggle(idx);
     makeundoent();
     entedit(idx, e.type = type);
+    return idx;
 }
 
-void newentity(int type, const attrvector &attrs)
+int newentity(int type, const attrvector &attrs)
 {
-    int idx;
+    int idx = -1;
     extentity *t = newentity(true, camera1->o, type, attrs, idx);
-    if(!t) return;
+    if(!t) return -1;
     dropentity(*t);
     t->type = ET_EMPTY;
     enttoggle(idx);
     makeundoent();
     entedit(idx, e.type = type);
+    return idx;
 }
 
 void entattrs(const char *str, attrvector &attrs)
@@ -739,7 +765,7 @@ void entpaste()
         vec o(c.o);
         o.mul(m).add(sel.o.tovec());
         int idx;
-        extentity *e = newentity(true, o, ET_EMPTY, c.attrs, idx);
+        extentity *e = newentity(true, o, ET_EMPTY, c.attrs, idx, false);
         if(!e) continue;
         loopvk(c.links) e->links.add(c.links[k]);
         entadd(idx);
@@ -967,7 +993,7 @@ bool emptymap(int scale, bool force, char *mname, bool nocfg)   // main empty wo
     clearworldvars();
     resetmap(nocfg);
     setnames(mname, MAP_MAPZ);
-    strncpy(hdr.head, "MAPZ", 4);
+    memcpy(hdr.head, "MAPZ", 4);
 
     hdr.version = MAPVERSION;
     hdr.gamever = server::getver(1);
@@ -997,7 +1023,7 @@ bool emptymap(int scale, bool force, char *mname, bool nocfg)   // main empty wo
 
     initlights();
     allchanged(true);
-
+    entities::initents(MAP_MAPZ, hdr.version, hdr.gameid, hdr.gamever);
     game::startmap(nocfg ? "" : "maps/untitled", NULL, true);
     return true;
 }
@@ -1070,7 +1096,7 @@ void shrinkmap()
     conoutf("shrunk map to size %d", worldscale);
 }
 
-ICOMMAND(0, newmap, "is", (int *i), if(emptymap(*i, false)) game::newmap(::max(*i, 0)));
+ICOMMAND(0, newmap, "is", (int *i, char *n), if(emptymap(*i, false, n)) game::newmap(::max(*i, 0)));
 ICOMMAND(0, mapenlarge, "", (), if(enlargemap(false)) game::newmap(-1));
 COMMAND(0, shrinkmap, "");
 ICOMMAND(0, mapsize, "", (void),
diff --git a/src/engine/world.h b/src/engine/world.h
index 58e4f8c..26ca76a 100644
--- a/src/engine/world.h
+++ b/src/engine/world.h
@@ -134,8 +134,8 @@ enum
 
 #define TEX_SCALE 8.0f
 
-struct vertexff { vec pos; bvec norm; uchar reserved; float u, v; float lmu, lmv; };
-struct vertex { vec pos; bvec norm; uchar reserved; float u, v; short lmu, lmv; bvec tangent; uchar bitangent; };
+struct vertexff { vec pos; bvec4 norm; float u, v; float lmu, lmv; };
+struct vertex { vec pos; bvec4 norm; float u, v; short lmu, lmv; bvec4 tangent; };
 
 #define VTXSIZE (renderpath==R_FIXEDFUNCTION ? sizeof(vertexff) : sizeof(vertex))
 
diff --git a/src/engine/worldio.cpp b/src/engine/worldio.cpp
index e97d599..0c86c02 100644
--- a/src/engine/worldio.cpp
+++ b/src/engine/worldio.cpp
@@ -24,7 +24,7 @@ VAR(IDF_PERSIST, autosavemapshot, 0, 1, 1);
 void fixmaptitle()
 {
     string s; // remove colour from these things in RE
-    if(filtertext(s, maptitle)) setsvar("maptitle", s, false);
+    if(filterstring(s, maptitle)) setsvar("maptitle", s, false);
     const char *title = maptitle, *author = strstr(title, " by ");
     if(author && *author)
     {
@@ -33,7 +33,7 @@ void fixmaptitle()
         {
             if(*t)
             {
-                loopi(4) if(*author) author += 1;
+                loopi(4) if(*author) author++;
                 if(*author) setsvar("mapauthor", author, true);
                 setsvar("maptitle", t, false);
             }
@@ -43,7 +43,7 @@ void fixmaptitle()
 }
 
 SVARF(IDF_WORLD, maptitle, "", fixmaptitle());
-SVARF(IDF_WORLD, mapauthor, "", { string s; if(filtertext(s, mapauthor)) setsvar("mapauthor", s, false); });
+SVARF(IDF_WORLD, mapauthor, "", { string s; if(filterstring(s, mapauthor)) setsvar("mapauthor", s, false); });
 
 void setnames(const char *fname, int type)
 {
@@ -81,26 +81,28 @@ void savec(cube *c, const ivec &o, int size, stream *f, bool nolms)
         {
             int oflags = 0, surfmask = 0, totalverts = 0;
             if(c[i].material!=MAT_AIR) oflags |= 0x40;
-            if(!nolms)
+            if(isempty(c[i])) f->putchar(oflags | OCTSAV_EMPTY);
+            else
             {
-                if(c[i].merged) oflags |= 0x80;
-                if(c[i].ext) loopj(6)
+                if(!nolms)
                 {
-                    const surfaceinfo &surf = c[i].ext->surfaces[j];
-                    if(!surf.used()) continue;
-                    oflags |= 0x20;
-                    surfmask |= 1<<j;
-                    totalverts += surf.totalverts();
+                    if(c[i].merged) oflags |= 0x80;
+                    if(c[i].ext) loopj(6)
+                    {
+                        const surfaceinfo &surf = c[i].ext->surfaces[j];
+                        if(!surf.used()) continue;
+                        oflags |= 0x20;
+                        surfmask |= 1<<j;
+                        totalverts += surf.totalverts();
+                    }
                 }
-            }
 
-            if(c[i].children) f->putchar(oflags | OCTSAV_LODCUBE);
-            else if(isempty(c[i])) f->putchar(oflags | OCTSAV_EMPTY);
-            else if(isentirelysolid(c[i])) f->putchar(oflags | OCTSAV_SOLID);
-            else
-            {
-                f->putchar(oflags | OCTSAV_NORMAL);
-                f->write(c[i].edges, 12);
+                if(isentirelysolid(c[i])) f->putchar(oflags | OCTSAV_SOLID);
+                else
+                {
+                    f->putchar(oflags | OCTSAV_NORMAL);
+                    f->write(c[i].edges, 12);
+                }
             }
 
             loopj(6) f->putlil<ushort>(c[i].texture[j]);
@@ -217,8 +219,6 @@ void savec(cube *c, const ivec &o, int size, stream *f, bool nolms)
                     }
                 }
             }
-
-            if(c[i].children) savec(c[i].children, co, size>>1, f, nolms);
         }
     }
 }
@@ -289,7 +289,7 @@ void convertoldsurfaces(cube &c, const ivec &co, int size, surfacecompat *srcsur
                     const ivec &coords = facecoords[i][k];
                     int cc = coords[vc] ? m.u2 : m.u1,
                         rc = coords[vr] ? m.v2 : m.v1,
-                        dc = -(offset + n[vc]*cc + n[vr]*rc)/n[dim];
+                        dc = n[dim] ? -(offset + n[vc]*cc + n[vr]*rc)/n[dim] : vo[dim];
                     ivec &mv = pos[k];
                     mv[vc] = cc;
                     mv[vr] = rc;
@@ -489,7 +489,7 @@ void loadc(stream *f, cube &c, const ivec &co, int size, bool &failed)
                 surf.verts = offset;
                 vertinfo *verts = c.ext->verts() + offset;
                 offset += numverts;
-                ivec v[4], n;
+                ivec v[4], n, vo = ivec(co).mask(0xFFF).shl(3);
                 int layerverts = surf.numverts&MAXFACEVERTS, dim = dimension(i), vc = C[dim], vr = R[dim], bias = 0;
                 genfaceverts(c, i, v);
                 bool hasxyz = (vertmask&0x04)!=0, hasuv = (vertmask&0x40)!=0, hasnorm = (vertmask&0x80)!=0;
@@ -498,12 +498,11 @@ void loadc(stream *f, cube &c, const ivec &co, int size, bool &failed)
                     ivec e1, e2, e3;
                     n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));
                     if(n.iszero()) n.cross(e2, (e3 = v[3]).sub(v[0]));
-                    bias = -n.dot(ivec(v[0]).mul(size).add(ivec(co).mask(0xFFF).shl(3)));
+                    bias = -n.dot(ivec(v[0]).mul(size).add(vo));
                 }
                 else
                 {
                     int vis = layerverts < 4 ? (vertmask&0x02 ? 2 : 1) : 3, order = vertmask&0x01 ? 1 : 0, k = 0;
-                    ivec vo = ivec(co).mask(0xFFF).shl(3);
                     verts[k++].setxyz(v[order].mul(size).add(vo));
                     if(vis&1) verts[k++].setxyz(v[order+1].mul(size).add(vo));
                     verts[k++].setxyz(v[order+2].mul(size).add(vo));
@@ -515,13 +514,13 @@ void loadc(stream *f, cube &c, const ivec &co, int size, bool &failed)
                     {
                         ushort c1 = f->getlil<ushort>(), r1 = f->getlil<ushort>(), c2 = f->getlil<ushort>(), r2 = f->getlil<ushort>();
                         ivec xyz;
-                        xyz[vc] = c1; xyz[vr] = r1; xyz[dim] = -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim];
+                        xyz[vc] = c1; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
                         verts[0].setxyz(xyz);
-                        xyz[vc] = c1; xyz[vr] = r2; xyz[dim] = -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim];
+                        xyz[vc] = c1; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
                         verts[1].setxyz(xyz);
-                        xyz[vc] = c2; xyz[vr] = r2; xyz[dim] = -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim];
+                        xyz[vc] = c2; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
                         verts[2].setxyz(xyz);
-                        xyz[vc] = c2; xyz[vr] = r1; xyz[dim] = -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim];
+                        xyz[vc] = c2; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
                         verts[3].setxyz(xyz);
                         hasxyz = false;
                     }
@@ -557,7 +556,7 @@ void loadc(stream *f, cube &c, const ivec &co, int size, bool &failed)
                     {
                         ivec xyz;
                         xyz[vc] = f->getlil<ushort>(); xyz[vr] = f->getlil<ushort>();
-                        xyz[dim] = -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim];
+                        xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
                         v.setxyz(xyz);
                     }
                     if(hasuv) { v.u = f->getlil<ushort>(); v.v = f->getlil<ushort>(); }
@@ -812,12 +811,12 @@ void save_config(char *mname)
     if(!h) { conoutf("\frcould not write config to %s", fname); return; }
 
     string title, author;
-    if(*maptitle) filtertext(title, maptitle);
+    if(*maptitle) filterstring(title, maptitle);
     else copystring(title, maptitle);
-    if(*mapauthor) filtertext(author, mapauthor);
+    if(*mapauthor) filterstring(author, mapauthor);
     else copystring(author, mapauthor);
     // config
-    h->printf("// %s by %s (%s)\n// Config generated by %s\n\n", title, author, mapname, versionname);
+    h->printf("// %s by %s (%s)\n// Config generated by %s\n\n", title, author, mapname, VERSION_NAME);
 
     int vars = 0;
     h->printf("// Variables stored in map file, may be uncommented here, or changed from editmode.\n");
@@ -829,14 +828,14 @@ void save_config(char *mname)
         ident &id = *ids[i];
         if(id.flags&IDF_WORLD && !(id.flags&IDF_SERVER)) switch(id.type)
         {
-            case ID_VAR: h->printf((id.flags&IDF_HEX && *id.storage.i >= 0 ? (id.maxval==0xFFFFFF ? "// %s 0x%.6X\n" : "// %s 0x%X\n") : "// %s %d\n"), escapeid(id), *id.storage.i); vars++;break;
+            case ID_VAR: h->printf("// %s %s\n", escapeid(id), intstr(&id)); vars++;break;
             case ID_FVAR: h->printf("// %s %s\n", escapeid(id), floatstr(*id.storage.f)); vars++; break;
             case ID_SVAR: h->printf("// %s %s\n", escapeid(id), escapestring(*id.storage.s)); vars++; break;
             default: break;
         }
     }
     if(vars) h->printf("\n");
-    if(verbose >= 2) conoutf("\fawrote %d variable values", vars);
+    if(verbose >= 2) conoutf("wrote %d variable values", vars);
 
     int aliases = 0;
     loopv(ids)
@@ -854,7 +853,7 @@ void save_config(char *mname)
         }
     }
     if(aliases) h->printf("\n");
-    if(verbose >= 2) conoutf("\fasaved %d aliases", aliases);
+    if(verbose >= 2) conoutf("saved %d aliases", aliases);
 
     // texture slots
     int nummats = sizeof(materialslots)/sizeof(materialslots[0]);
@@ -869,14 +868,14 @@ void save_config(char *mname)
                 break;
         }
     }
-    if(verbose) conoutf("\fasaved %d material slots", nummats);
+    if(verbose) conoutf("saved %d material slots", nummats);
 
     loopv(slots)
     {
         progress(i/float(slots.length()), "saving texture slots...");
         saveslotconfig(h, *slots[i], i);
     }
-    if(verbose) conoutf("\fasaved %d texture slots", slots.length());
+    if(verbose) conoutf("saved %d texture slots", slots.length());
 
     loopv(mapmodels)
     {
@@ -884,7 +883,7 @@ void save_config(char *mname)
         h->printf("mmodel %s\n", escapestring(mapmodels[i].name));
     }
     if(mapmodels.length()) h->printf("\n");
-    if(verbose) conoutf("\fasaved %d mapmodel slots", mapmodels.length());
+    if(verbose) conoutf("saved %d mapmodel slots", mapmodels.length());
 
     loopv(mapsounds)
     {
@@ -897,14 +896,14 @@ void save_config(char *mname)
         h->printf("\n");
     }
     if(mapsounds.length()) h->printf("\n");
-    if(verbose) conoutf("\fasaved %d mapsound slots", mapsounds.length());
+    if(verbose) conoutf("saved %d mapsound slots", mapsounds.length());
 
     delete h;
-    if(verbose) conoutf("\fasaved config %s", fname);
+    if(verbose) conoutf("saved config %s", fname);
 }
-ICOMMAND(0, savemapconfig, "s", (char *mname), save_config(*mname ? mname : mapname));
+ICOMMAND(0, savemapconfig, "s", (char *mname), if(!(identflags&IDF_WORLD)) save_config(*mname ? mname : mapname));
 
-VARF(IDF_PERSIST, mapshotsize, 0, 256, INT_MAX-1, mapshotsize -= mapshotsize%2);
+VARF(IDF_PERSIST, mapshotsize, 0, 512, INT_MAX-1, mapshotsize -= mapshotsize%2);
 
 void save_mapshot(char *mname)
 {
@@ -926,7 +925,7 @@ void save_mapshot(char *mname)
     defformatstring(texname)("%s", mname);
     reloadtexture(texname);
 }
-ICOMMAND(0, savemapshot, "s", (char *mname), save_mapshot(*mname ? mname : mapname));
+ICOMMAND(0, savemapshot, "s", (char *mname), if(!(identflags&IDF_WORLD)) save_mapshot(*mname ? mname : mapname));
 
 #define istempname(n) (!strncmp(n, "temp/", 5) || !strncmp(n, "temp\\", 5))
 
@@ -952,14 +951,16 @@ void save_world(const char *mname, bool nodata, bool forcesave)
 
     savemapprogress = 0;
     progress(0, "saving map..");
-    strncpy(hdr.head, "MAPZ", 4);
+    memcpy(hdr.head, "MAPZ", 4);
     hdr.version = MAPVERSION;
     hdr.headersize = sizeof(mapz);
     hdr.gamever = server::getver(1);
     hdr.numents = 0;
     hdr.numvslots = numvslots;
     hdr.revision++;
-    strncpy(hdr.gameid, server::gameid(), 4);
+    string gameid;
+    copystring(gameid, server::gameid());
+    memcpy(hdr.gameid, gameid, 4);
 
     const vector<extentity *> &ents = entities::getents();
     loopv(ents)
@@ -1009,7 +1010,7 @@ void save_world(const char *mname, bool nodata, bool forcesave)
         }
     });
 
-    if(verbose) conoutf("\fasaved %d variables", vars);
+    if(verbose) conoutf("saved %d variables", vars);
 
     // texture slots
     f->putlil<ushort>(texmru.length());
@@ -1050,15 +1051,15 @@ void save_world(const char *mname, bool nodata, bool forcesave)
 
                 f->putlil<int>(links.length());
                 loopvj(links) f->putlil<int>(links[j]); // aligned index
-                if(verbose >= 2) conoutf("\faentity %s (%d) saved %d links", entities::findname(e.type), i, links.length());
+                if(verbose >= 2) conoutf("entity %s (%d) saved %d links", entities::findname(e.type), i, links.length());
             }
             count++;
         }
     }
-    if(verbose) conoutf("\fasaved %d entities", count);
+    if(verbose) conoutf("saved %d entities", count);
 
     savevslots(f, numvslots);
-    if(verbose) conoutf("\fasaved %d vslots", numvslots);
+    if(verbose) conoutf("saved %d vslots", numvslots);
 
     progress(0, "saving octree...");
     savec(worldroot, ivec(0, 0, 0), hdr.worldsize>>1, f, nodata);
@@ -1077,26 +1078,26 @@ void save_world(const char *mname, bool nodata, bool forcesave)
             }
             f->write(lm.data, lm.bpp*LM_PACKW*LM_PACKH);
         }
-        if(verbose) conoutf("\fasaved %d lightmaps", lightmaps.length());
+        if(verbose) conoutf("saved %d lightmaps", lightmaps.length());
         if(getnumviewcells()>0)
         {
             progress(0, "saving PVS...");
             savepvs(f);
-            if(verbose) conoutf("\fasaved %d PVS view cells", getnumviewcells());
+            if(verbose) conoutf("saved %d PVS view cells", getnumviewcells());
         }
         if(shouldsaveblendmap())
         {
             progress(0, "saving blendmap...");
             saveblendmap(f);
-            if(verbose) conoutf("\fasaved blendmap");
+            if(verbose) conoutf("saved blendmap");
         }
     }
     delete f;
-    conoutf("\fasaved map %s v.%d:%d (r%d) in %.1f secs", mapname, hdr.version, hdr.gamever, hdr.revision, (SDL_GetTicks()-savingstart)/1000.0f);
+    conoutf("saved %s (\fs%s\fS by \fs%s\fS) v.%d:%d (r%d) in %.1f secs", mapname, *maptitle ? maptitle : "Untitled", *mapauthor ? mapauthor : "Unknown", hdr.version, hdr.gamever, hdr.revision, (SDL_GetTicks()-savingstart)/1000.0f);
     if(istempname(mapname)) setnames(&mapname[5], MAP_MAPZ);
 }
 
-ICOMMAND(0, savemap, "s", (char *mname), save_world(*mname ? (istempname(mname) ? &mname[5] : mname) : (istempname(mapname) ? &mapname[5] : mapname)));
+ICOMMAND(0, savemap, "s", (char *mname), if(!(identflags&IDF_WORLD)) save_world(*mname ? (istempname(mname) ? &mname[5] : mname) : (istempname(mapname) ? &mapname[5] : mapname)));
 
 static uint mapcrc = 0;
 
@@ -1133,6 +1134,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
             stream *f = opengzfile(mapfile, "rb");
             if(!f)
             {
+                conoutf("\frnot found: %s", mapfile);
                 maskpackagedirs(mask);
                 continue;
             }
@@ -1153,11 +1155,11 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
             clearworldvars();
             if(memcmp(newhdr.head, "MAPZ", 4) == 0 || memcmp(newhdr.head, "BFGZ", 4) == 0)
             {
-                // this check removed below due to breakage: (size_t)newhdr.headersize > sizeof(chdr) || f->read(&chdr.worldsize, newhdr.headersize-sizeof(binary))!=newhdr.headersize-(int)sizeof(binary)
+                // this check removed below due to breakage: size_t(newhdr.headersize) > sizeof(chdr) || f->read(&chdr.worldsize, newhdr.headersize-sizeof(binary))!=size_t(newhdr.headersize)-sizeof(binary)
                 #define MAPZCOMPAT(ver) \
                     mapzcompat##ver chdr; \
                     memcpy(&chdr, &newhdr, sizeof(binary)); \
-                    if(f->read(&chdr.worldsize, sizeof(chdr)-sizeof(binary))!=int(sizeof(chdr)-sizeof(binary))) \
+                    if(f->read(&chdr.worldsize, sizeof(chdr)-sizeof(binary))!=sizeof(chdr)-sizeof(binary)) \
                     { \
                         conoutf("\frerror loading %s: malformatted mapz v%d[%d] header", mapname, newhdr.version, ver); \
                         delete f; \
@@ -1210,7 +1212,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                 }
                 else
                 {
-                    if((size_t)newhdr.headersize > sizeof(newhdr) || f->read(&newhdr.worldsize, newhdr.headersize-sizeof(binary))!=newhdr.headersize-(int)sizeof(binary))
+                    if(size_t(newhdr.headersize) > sizeof(newhdr) || f->read(&newhdr.worldsize, newhdr.headersize-sizeof(binary))!=size_t(newhdr.headersize)-sizeof(binary))
                     {
                         conoutf("\frerror loading %s: malformatted mapz v%d header", mapname, newhdr.version);
                         delete f;
@@ -1223,7 +1225,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
 
                 if(newhdr.version > MAPVERSION)
                 {
-                    conoutf("\frerror loading %s: requires a newer version of %s", mapname, versionname);
+                    conoutf("\frerror loading %s: requires a newer version of %s", mapname, VERSION_NAME);
                     delete f;
                     maploading = 0;
                     maskpackagedirs(mask);
@@ -1236,7 +1238,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                 maptype = MAP_MAPZ;
 
                 if(hdr.version <= 24) copystring(hdr.gameid, "bfa", 4); // all previous maps were bfa-fps
-                if(verbose) conoutf("\faloading v%d map from %s game v%d", hdr.version, hdr.gameid, hdr.gamever);
+                if(verbose) conoutf("loading v%d map from %s game v%d", hdr.version, hdr.gameid, hdr.gamever);
 
                 if(hdr.version >= 25 || (hdr.version == 24 && hdr.gamever >= 44))
                 {
@@ -1330,7 +1332,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                         }
                     }
                     identflags &= ~IDF_WORLD;
-                    if(verbose) conoutf("\faloaded %d variables", vars);
+                    if(verbose) conoutf("loaded %d variables", vars);
                 }
                 sanevars();
 
@@ -1349,7 +1351,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                 #define OCTACOMPAT(ver) \
                     octacompat##ver chdr; \
                     memcpy(&chdr, &ohdr, sizeof(binary)); \
-                    if(f->read(&chdr.worldsize, sizeof(chdr)-sizeof(binary))!=int(sizeof(chdr)-sizeof(binary))) \
+                    if(f->read(&chdr.worldsize, sizeof(chdr)-sizeof(binary))!=sizeof(chdr)-sizeof(binary)) \
                     { \
                         conoutf("\frerror loading %s: malformatted octa v%d[%d] header", mapname, ver, ohdr.version); \
                         delete f; \
@@ -1431,7 +1433,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                 progress(0, "please wait..");
                 maptype = MAP_OCTA;
 
-                strncpy(hdr.head, ohdr.head, 4);
+                memcpy(hdr.head, ohdr.head, 4);
                 hdr.gamever = 0; // sauer has no gamever
                 hdr.worldsize = ohdr.worldsize;
                 if(hdr.worldsize > 1<<18) hdr.worldsize = 1<<18;
@@ -1494,14 +1496,14 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                     f->read(gameid, len+1);
                 }
                 else copystring(gameid, "fps");
-                strncpy(hdr.gameid, gameid, 4);
+                memcpy(hdr.gameid, gameid, 4);
 
                 if(!server::canload(hdr.gameid))
                 {
                     if(verbose) conoutf("\frWARNING: loading OCTA v%d map from %s game, ignoring game specific data", hdr.version, hdr.gameid);
                     samegame = false;
                 }
-                else if(verbose) conoutf("\faloading OCTA v%d map from %s game", hdr.version, hdr.gameid);
+                else if(verbose) conoutf("loading OCTA v%d map from %s game", hdr.version, hdr.gameid);
 
                 if(hdr.version>=16)
                 {
@@ -1601,7 +1603,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                     int links = f->getlil<int>();
                     e.links.add(0, links);
                     loopk(links) e.links[k] = f->getlil<int>();
-                    if(verbose >= 2) conoutf("\faentity %s (%d) loaded %d link(s)", entities::findname(e.type), i, links);
+                    if(verbose >= 2) conoutf("entity %s (%d) loaded %d link(s)", entities::findname(e.type), i, links);
                 }
 
                 if(maptype == MAP_OCTA && e.type == ET_PARTICLES && e.attrs[0] >= 11)
@@ -1646,7 +1648,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                 if(verbose && !insideworld(e.o) && e.type != ET_LIGHT && e.type != ET_LIGHTFX && e.type != ET_SUNLIGHT)
                     conoutf("\frWARNING: ent outside of world: enttype[%s] index %d (%f, %f, %f)", entities::findname(e.type), i, e.o.x, e.o.y, e.o.z);
             }
-            if(verbose) conoutf("\faloaded %d entities", hdr.numents);
+            if(verbose) conoutf("loaded %d entities", hdr.numents);
             if(maptype == MAP_OCTA && sunlight)
             {
                 extentity &e = *ents.add(entities::newent());
@@ -1700,7 +1702,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                 if(hdr.numpvs > 0) loadpvs(f);
                 if(hdr.blendmap) loadblendmap(f);
 
-                if(verbose) conoutf("\faloaded %d lightmaps", hdr.lightmaps);
+                if(verbose) conoutf("loaded %d lightmaps", hdr.lightmaps);
             }
 
             progress(0, "initialising entities...");
@@ -1731,7 +1733,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
                     }
                 }
             }
-            entities::initents(f, maptype, hdr.version, hdr.gameid, hdr.gamever);
+            entities::initents(maptype, hdr.version, hdr.gameid, hdr.gamever);
 
             progress(0, "initialising config...");
             mapcrc = f->getcrc();
@@ -1749,7 +1751,7 @@ bool load_world(const char *mname, bool temp)       // still supports all map fo
             preloadusedmapmodels(true);
 
             delete f;
-            conoutf("\faloaded map %s v.%d:%d (r%d) in %.1f secs", mapname, hdr.version, hdr.gamever, hdr.revision, (SDL_GetTicks()-loadingstart)/1000.0f);
+            conoutf("loaded %s (\fs%s\fS by \fs%s\fS) v.%d:%d(r%d) [%.1fs]", mapname, *maptitle ? maptitle : "Untitled", *mapauthor ? mapauthor : "Unknown", hdr.version, hdr.gamever, hdr.revision, (SDL_GetTicks()-loadingstart)/1000.0f);
 
             progress(0, "checking world...");
             if((maptype == MAP_OCTA && hdr.version <= 25) || (maptype == MAP_MAPZ && hdr.version <= 26))
@@ -1878,7 +1880,7 @@ void writeobj(char *name)
     delete f;
 }
 
-COMMAND(0, writeobj, "s");
+ICOMMAND(0, writeobj, "s", (char *s), if(!(identflags&IDF_WORLD)) writeobj(s));
 
 int getworldsize() { return hdr.worldsize; }
 int getmapversion() { return hdr.version; }
diff --git a/src/game/ai.cpp b/src/game/ai.cpp
index 2c74b13..de9e27d 100644
--- a/src/game/ai.cpp
+++ b/src/game/ai.cpp
@@ -7,20 +7,28 @@ namespace ai
 
     VAR(0, aidebug, 0, 0, 7);
     VAR(0, aidebugfocus, 0, 1, 2);
-    VAR(0, aiforcegun, -1, -1, W_MAX-1);
-#ifdef CAMPAIGN
-    VAR(0, aicampaign, 0, 0, 1);
-#endif
-    VAR(0, aipassive, 0, 0, 1);
+    VAR(0, aipassive, 0, 0, 2); // 0 = off, 1 = passive to humans, 2 = completely passive
 
     VARF(0, showwaypoints, 0, 0, 1, if(showwaypoints) getwaypoints());
-    VAR(0, showwaypointsdrop, 0, 1, 1);
-    VAR(0, showwaypointsradius, 0, 256, VAR_MAX);
+
+    VAR(IDF_PERSIST, showwaypointsdrop, 0, 1, 1);
+    VAR(IDF_PERSIST, showwaypointsradius, 0, 256, VAR_MAX);
+    VAR(IDF_PERSIST|IDF_HEX, showwaypointscolour, 0, 0xFF00FF, 0xFFFFFF);
 
     VAR(IDF_PERSIST, aideadfade, 0, 10000, VAR_MAX);
     VAR(IDF_PERSIST, showaiinfo, 0, 0, 2); // 0/1 = shows/hides bot join/parts, 2 = show more verbose info
 
-    bool passive() { return aipassive || m_edit(game::gamemode); }
+    bool passive(gameent *d = NULL)
+    {
+        if(m_edit(game::gamemode)) return true;
+        switch(aipassive)
+        {
+            case 2: return true;
+            case 1: if(d && d->actortype == A_PLAYER) return true;
+            case 0: default: break;
+        }
+        return false;
+    }
     bool dbgfocus(gameent *d)  { return d->ai && (!aidebugfocus || d == game::focus || (aidebugfocus != 2 && !game::focus->ai)); }
 
     void startmap(const char *name, const char *reqname, bool empty)    // called just after a map load
@@ -34,30 +42,18 @@ namespace ai
     float viewfieldx(int x) { return x <= 100 ? clamp((VIEWMIN+(VIEWMAX-VIEWMIN))/100.f*float(x), float(VIEWMIN), float(VIEWMAX)) : float(VIEWMAX); }
     float viewfieldy(int x) { return viewfieldx(x)*3.f/4.f; }
 
-    int owner(gameent *d)
-    {
-        if(!d) return -1;
-        if(d->aitype >= AI_START)
-        {
-            if(m_gauntlet(game::gamemode)) return T_OMEGA;
-            else if(entities::ents.inrange(d->aientity))
-            {
-                if(m_capture(game::gamemode)) return capture::aiowner(d);
-                else if(m_defend(game::gamemode)) return defend::aiowner(d);
-                else if(m_bomber(game::gamemode)) return bomber::aiowner(d);
-            }
-        }
-        return d->team;
-    }
-
     float weapmindist(int weap, bool alt)
     {
-        return WX(false, weap, explode, alt, game::gamemode, game::mutators, 1.f) > 0 && W2(weap, collide, alt)&COLLIDE_OWNER ? WX(false, weap, explode, alt, game::gamemode, game::mutators, 1.f) : 2;
+        if(weaptype[weap].melee) return 0.f;
+        if(WX(false, weap, explode, alt, game::gamemode, game::mutators, 1.f) > 0) return WX(false, weap, explode, alt, game::gamemode, game::mutators, 1.f);
+        return CLOSEDIST;
     }
 
     float weapmaxdist(int weap, bool alt)
     {
-        return W2(weap, aidist, alt) ? W2(weap, aidist, alt) : hdr.worldsize;
+        if(weaptype[weap].melee) return CLOSEDIST;
+        if(W2(weap, aidist, alt) > 0) return W2(weap, aidist, alt);
+        return getworldsize();
     }
 
     bool weaprange(gameent *d, int weap, bool alt, float dist)
@@ -69,11 +65,10 @@ namespace ai
 
     bool targetable(gameent *d, gameent *e, bool solid)
     {
-        if(d && e && d != e && !passive() && e->state == CS_ALIVE && (!solid || physics::issolid(e, d)))
+        if(d && e && d != e && !passive(e) && e->state == CS_ALIVE && (!solid || physics::issolid(e, d)))
         {
-            int dt = owner(d), et = owner(e);
-            if(dt == T_ENEMY && et == T_ENEMY) return false;
-            if(!m_team(game::gamemode, game::mutators) || dt != et) return true;
+            if(d->team == T_ENEMY && e->team == T_ENEMY) return false;
+            if(!m_team(game::gamemode, game::mutators) || d->team != e->team) return true;
         }
         return false;
     }
@@ -95,7 +90,7 @@ namespace ai
         if(isweap(d->weapselect) && weaprange(d, d->weapselect, alt, e->o.squaredist(d->o)))
         {
             int prot = m_protect(game::gamemode, game::mutators);
-            if((d->aitype >= AI_START || !d->protect(lastmillis, prot)) && targetable(d, e, true))
+            if((d->actortype >= A_ENEMY || !d->protect(lastmillis, prot)) && targetable(d, e, true))
                 return d->canshoot(d->weapselect, alt ? HIT_ALT : 0, m_weapon(game::gamemode, game::mutators), lastmillis, (1<<W_S_RELOAD));
         }
         return false;
@@ -103,9 +98,9 @@ namespace ai
 
     bool altfire(gameent *d, gameent *e)
     {
-        if(e && !W(d->weapselect, zooms) && canshoot(d, e, true))
+        if(e && !(W2(d->weapselect, cooked, true)&W_C_ZOOM) && canshoot(d, e, true))
         {
-            if(d->weapstate[d->weapselect] == W_S_POWER)
+            if(d->weapstate[d->weapselect] == W_S_POWER || d->weapstate[d->weapselect] == W_S_ZOOM)
             {
                 if(d->action[AC_SECONDARY] && (!d->action[AC_PRIMARY] || d->actiontime[AC_SECONDARY] > d->actiontime[AC_PRIMARY]))
                     return true;
@@ -114,7 +109,7 @@ namespace ai
             {
                 case W_PISTOL: return true; break;
                 case W_MELEE: case W_ROCKET: default: return false; break;
-                case W_SWORD: case W_SHOTGUN: case W_SMG: case W_PLASMA: case W_GRENADE: case W_MINE:
+                case W_SWORD: case W_SHOTGUN: case W_SMG: case W_FLAMER: case W_PLASMA: case W_ZAPPER: case W_GRENADE: case W_MINE:
                     if(rnd(d->skill*3) <= d->skill) return false;
                     break;
                 case W_RIFLE: if(weaprange(d, d->weapselect, false, e->o.squaredist(d->o))) return false; break;
@@ -124,12 +119,12 @@ namespace ai
         return false;
     }
 
-    bool hastarget(gameent *d, aistate &b, gameent *e, bool alt, float yaw, float pitch, float dist)
+    bool hastarget(gameent *d, aistate &b, gameent *e, bool alt, bool insight, float yaw, float pitch)
     { // add margins of error
-        if(weaprange(d, d->weapselect, alt, dist) || (d->skill <= 100 && !rnd(d->skill)))
+        if((insight && weaprange(d, d->weapselect, alt, e->o.squaredist(d->o))) || (d->skill <= 100 && !rnd(d->skill)))
         {
             if(weaptype[d->weapselect].melee) return true;
-            float skew = clamp(float(lastmillis-d->ai->enemymillis)/float((d->skill*W(d->weapselect, reloaddelay)/5000.f)+(d->skill*W2(d->weapselect, attackdelay, alt)/500.f)), 0.f, weaptype[d->weapselect].thrown ? 0.25f : 1e16f),
+            float skew = clamp(float(lastmillis-d->ai->enemymillis)/float((d->skill*W(d->weapselect, delayreload)/5000.f)+(d->skill*W2(d->weapselect, delayattack, alt)/500.f)), 0.f, weaptype[d->weapselect].thrown ? 0.25f : 1e16f),
                 offy = yaw-d->yaw, offp = pitch-d->pitch;
             if(offy > 180) offy -= 360;
             else if(offy < -180) offy += 360;
@@ -140,34 +135,40 @@ namespace ai
 
     vec getaimpos(gameent *d, gameent *e, bool alt)
     {
-        vec o = e->headpos();
-        //if(W2(d->weapselect, radial, alt)) o.z -= e->height;
+        vec o = e->o;
         if(d->skill <= 100)
         {
             if(lastmillis >= d->ai->lastaimrnd)
             {
-                #define rndaioffset(r) ((rnd(int(r*W2(d->weapselect, aiskew, alt)*2)+1)-(r*W2(d->weapselect, aiskew, alt)))/float(max(d->skill, 1)))
-                loopk(3) d->ai->aimrnd[k] = rndaioffset(e->radius);
+                int radius = ceilf(e->radius*W2(d->weapselect, aiskew, alt));
+                float speed = clamp(e->vel.magnitude()/movespeed, 0.f, 1.f), scale = speed+((1-speed)*((101-d->skill)/100.f));
+                loopk(3) d->ai->aimrnd[k] = (rnd((radius*2)+1)-radius)*scale;
                 int dur = (d->skill+10)*10;
                 d->ai->lastaimrnd = lastmillis+dur+rnd(dur);
             }
-            loopk(3) o[k] += d->ai->aimrnd[k];
+            o.add(d->ai->aimrnd);
         }
         return o;
     }
 
+    int weappref(gameent *d)
+    {
+        if(d->loadweap.length()) return d->loadweap[0];
+        return m_weapon(game::gamemode, game::mutators);
+    }
+
     bool hasweap(gameent *d, int weap)
     {
         if(!isweap(weap)) return false;
         if(w_carry(weap, m_weapon(game::gamemode, game::mutators)))
             return d->hasweap(weap, m_weapon(game::gamemode, game::mutators));
-        return d->ammo[weap] >= W(weap, max);
+        return d->ammo[weap] >= W(weap, ammomax);
     }
 
     bool wantsweap(gameent *d, int weap)
     {
         if(!isweap(weap) || hasweap(d, weap)) return false;
-        if(d->carry(m_weapon(game::gamemode, game::mutators)) >= maxcarry && (hasweap(d, d->ai->weappref) || weap != d->ai->weappref))
+        if(d->carry(m_weapon(game::gamemode, game::mutators)) >= maxcarry && (hasweap(d, weappref(d)) || weap != weappref(d)))
             return false;
         return true;
     }
@@ -187,23 +188,32 @@ namespace ai
         if(d->ai) DELETEP(d->ai);
     }
 
-    void init(gameent *d, int at, int et, int on, int sk, int bn, char *name, int tm, int cl, int md, const char *vn)
+    void checkinfo(gameent *d)
+    {
+        gameent *o = game::getclient(d->ownernum);
+        if(o)
+        {
+            copystring(d->hostname, o->hostname);
+            copystring(d->hostip, o->hostip);
+            d->version.grab(o->version);
+        }
+    }
+
+    void init(gameent *d, int at, int et, int on, int sk, int bn, char *name, int tm, int cl, int md, const char *vn, vector<int> &lweaps)
     {
         getwaypoints();
 
         gameent *o = game::newclient(on);
+        if(!o) return;
         bool resetthisguy = false;
-
         string m;
-        if(o) copystring(m, game::colourname(o));
-        else formatstring(m)("\fs\faunknown [\fs\fr%d\fS]\fS", on);
-
+        copystring(m, game::colourname(o));
         if(!d->name[0])
         {
-            if(showaiinfo && client::showpresence >= (client::waiting(false) ? 2 : 1))
+            if(at == A_BOT && showaiinfo && client::showpresence >= (client::waiting(false) ? 2 : 1))
             {
                 if(showaiinfo > 1) conoutft(CON_EVENT, "\fg%s assigned to %s at skill %d", game::colourname(d, name), m, sk);
-                else conoutft(CON_EVENT, "\fg%s joined the game", game::colourname(d, name));//, m, sk);
+                else conoutft(CON_EVENT, "\fg%s was added to the game", game::colourname(d, name));//, m, sk);
             }
             game::specreset(d);
             resetthisguy = true;
@@ -212,17 +222,22 @@ namespace ai
         {
             if(d->ownernum != on)
             {
-                if(showaiinfo && client::showpresence >= (client::waiting(false) ? 2 : 1))
+                if(at == A_BOT && showaiinfo && client::showpresence >= (client::waiting(false) ? 2 : 1))
                     conoutft(CON_EVENT, "\fg%s reassigned to %s", game::colourname(d, name), m);
                 resetthisguy = true;
             }
-            if(d->skill != sk && showaiinfo > 1 && client::showpresence >= (client::waiting(false) ? 2 : 1))
+            if(at == A_BOT && d->skill != sk && showaiinfo > 1 && client::showpresence >= (client::waiting(false) ? 2 : 1))
                 conoutft(CON_EVENT, "\fg%s changed skill to %d", game::colourname(d, name), sk);
         }
 
-        if((d->aitype = at) >= AI_START) d->type = ENT_AI;
+        if((d->actortype = at) >= A_ENEMY) d->type = ENT_AI;
+        else
+        {
+            d->loadweap.shrink(0);
+            loopv(lweaps) d->loadweap.add(lweaps[i]);
+        }
         d->setname(name);
-        d->aientity = et;
+        d->spawnpoint = et;
         d->ownernum = on;
         d->skill = sk;
         d->team = tm;
@@ -230,8 +245,6 @@ namespace ai
         d->model = md;
         d->setvanity(vn);
 
-        formatstring(d->hostname)("bot#%d", d->ownernum);
-
         if(resetthisguy) projs::remove(d);
         if(d->ownernum >= 0 && game::player1->clientnum == d->ownernum)
         {
@@ -244,11 +257,15 @@ namespace ai
             }
         }
         else if(d->ai) destroy(d);
+        checkinfo(d);
     }
 
     void update()
     {
-        if(game::intermission) { loopv(game::players) if(game::players[i] && game::players[i]->ai) game::players[i]->stopmoving(true); }
+        if(!gs_playing(game::gamestate))
+        {
+            loopv(game::players) if(game::players[i] && game::players[i]->ai) game::players[i]->stopmoving(true);
+        }
         else // fixed rate logic done out-of-sequence at 1 frame per second for each ai
         {
             if(totalmillis-updatemillis > 100) avoid();
@@ -256,12 +273,15 @@ namespace ai
             {
                 iteration = 1;
                 itermillis = totalmillis;
-                if(multiplayer(false)) { aiforcegun = -1; aipassive = 0; }
+                if(multiplayer(false)) aipassive = 0;
                 updatemillis = totalmillis;
             }
             int c = 0;
             loopv(game::players) if(game::players[i] && game::players[i]->ai)
+            {
+                checkinfo(game::players[i]);
                 think(game::players[i], ++c == iteration ? true : false);
+            }
             if(c && ++iteration > c) iteration = 0;
         }
     }
@@ -274,16 +294,9 @@ namespace ai
         loopi(numdyns) if((e = (gameent *)game::iterdynents(i)))
         {
             if(targets.find(e->clientnum) >= 0) continue;
-            if(teams)
-            {
-                int dt = owner(d), et = owner(e);
-                if(dt != T_ENEMY || et != T_ENEMY)
-                {
-                    if(m_team(game::gamemode, game::mutators) && dt != et) continue;
-                }
-            }
+            if(teams && (d->team != T_ENEMY || e->team != T_ENEMY) && m_team(game::gamemode, game::mutators) && d->team != e->team) continue;
             if(members) (*members)++;
-            if(e == d || !e->ai || e->state != CS_ALIVE || e->aitype != d->aitype) continue;
+            if(e == d || !e->ai || e->state != CS_ALIVE || e->actortype != d->actortype) continue;
             aistate &b = e->ai->getstate();
             if(state >= 0 && b.type != state) continue;
             if(target >= 0 && b.target != target) continue;
@@ -331,36 +344,35 @@ namespace ai
         return randomnode(d, b, d->feetpos(), guard, wander);
     }
 
-    bool enemy(gameent *d, aistate &b, const vec &pos, float guard, int pursue, bool force)
+    bool enemy(gameent *d, aistate &b, const vec &pos, float guard, int pursue, bool force, bool retry = false)
     {
         if(passive() || (d->ai->enemy >= 0 && lastmillis-d->ai->enemymillis >= (111-d->skill)*50)) return false;
         gameent *t = NULL, *e = NULL;
-        vec dp = d->headpos();
         float mindist = guard*guard, bestdist = 1e16f;
         int numdyns = game::numdynents();
-        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e != d && targetable(d, e))
+        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && targetable(d, e))
         {
-            vec ep = getaimpos(d, e, altfire(d, e));
-            float dist = ep.squaredist(dp);
-            if(dist < bestdist && (cansee(d, dp, ep, d->aitype >= AI_START) || dist <= mindist))
+            float dist = d->o.squaredist(e->o);
+            if(dist < bestdist && (force || dist <= mindist || cansee(d, d->o, e->o, d->actortype >= A_ENEMY)))
             {
                 t = e;
                 bestdist = dist;
             }
         }
         if(t && violence(d, b, t, pursue)) return true;
+        if(retry && !force) return enemy(d, b, pos, guard, pursue, true, false);
         return false;
     }
 
     bool patrol(gameent *d, aistate &b, const vec &pos, float guard, float wander, int walk, bool retry)
     {
-        if(aistyle[d->aitype].canmove)
+        if(actor[d->actortype].canmove)
         {
             vec feet = d->feetpos();
             float dist = feet.squaredist(pos);
             if(walk == 2 || b.override || (walk && dist <= guard*guard) || !makeroute(d, b, pos))
             { // run away and back to keep ourselves busy
-                if(!b.override && randomnode(d, b, pos, guard, wander))
+                if(!b.override && wander > 0 && randomnode(d, b, pos, guard, wander))
                 {
                     b.override = true;
                     return true;
@@ -369,9 +381,15 @@ namespace ai
                 {
                     b.override = false;
                     if(!retry) return patrol(d, b, pos, guard, wander, walk, true);
-                    return false;
+                    if(wander > 0) return false;
+                }
+                if(wander <= 0)
+                {
+                    b.acttype = AI_A_IDLE;
+                    return true;
                 }
             }
+            if(!b.override && dist > guard*guard) b.acttype = AI_A_HASTE;
         }
         b.override = false;
         return true;
@@ -379,16 +397,16 @@ namespace ai
 
     bool defense(gameent *d, aistate &b, const vec &pos, float guard, float wander, int walk)
     {
-        if(!aistyle[d->aitype].canmove)
+        if(!actor[d->actortype].canmove)
         {
-            b.idle = enemy(d, b, pos, wander, weaptype[d->weapselect].melee ? 1 : 0, false) ? 2 : 1;
+            b.acttype = enemy(d, b, pos, wander, weaptype[d->weapselect].melee ? 1 : 0, false, true) ? AI_A_PROTECT : AI_A_IDLE;
             return true;
         }
         if(!walk)
         {
-            if(pos.squaredist(d->feetpos()) <= guard*guard)
+            if(d->feetpos().squaredist(pos) <= guard*guard)
             {
-                b.idle = enemy(d, b, pos, wander, weaptype[d->weapselect].melee ? 1 : 0, false) ? 2 : 1;
+                b.acttype = enemy(d, b, pos, d->ai->views[2], weaptype[d->weapselect].melee ? 1 : 0, false, true) ? AI_A_PROTECT : AI_A_IDLE;
                 return true;
             }
             walk++;
@@ -398,77 +416,68 @@ namespace ai
 
     bool violence(gameent *d, aistate &b, gameent *e, int pursue)
     {
-        if(passive() || (d->ai->enemy >= 0 && lastmillis-d->ai->enemymillis >= (111-d->skill)*50)) return false;
-        if(e && targetable(d, e))
+        if(passive() || !targetable(d, e)) return false;
+        if(d->ai->enemy != e->clientnum)
         {
-            if(pursue)
-            {
-                if((b.targtype != AI_T_AFFINITY || !(pursue%2)) && makeroute(d, b, e->lastnode))
-                    d->ai->switchstate(b, AI_S_PURSUE, AI_T_ACTOR, e->clientnum);
-                else if(pursue >= 3) return false; // can't pursue
-            }
-            if(d->ai->enemy != e->clientnum)
-            {
-                d->ai->enemyseen = d->ai->enemymillis = lastmillis;
-                d->ai->enemy = e->clientnum;
-            }
-            return true;
+            gameent *f = game::getclient(d->ai->enemy);
+            if(f && (d->o.squaredist(e->o) < d->o.squaredist(f->o) || (d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*10)+1000))) return false;
         }
-        return false;
+        if(pursue)
+        {
+            if((b.targtype != AI_T_AFFINITY || !(pursue%2)) && makeroute(d, b, e->lastnode))
+                d->ai->switchstate(b, AI_S_PURSUE, AI_T_ACTOR, e->clientnum, b.targtype != AI_T_AFFINITY ? AI_A_NORMAL : AI_A_HASTE);
+            else if(pursue >= 3) return false; // can't pursue
+        }
+        if(d->ai->enemy != e->clientnum)
+        {
+            d->ai->enemyseen = d->ai->enemymillis = lastmillis;
+            d->ai->enemy = e->clientnum;
+        }
+        return true;
     }
 
 
     struct targcache
     {
         gameent *d;
-        vec pos;
-        bool targ, see, tried;
+        bool dominated, visible;
         float dist;
 
-        targcache() : d(NULL), pos(0, 0, 0), targ(false), see(false), tried(false), dist(0) {}
+        targcache() : d(NULL), dominated(false), visible(false), dist(0) {}
         ~targcache() {}
+
+        static bool tcsort(targcache &a,  targcache &b)
+        {
+            if(a.dominated && !b.dominated) return true;
+            if(!a.dominated && b.dominated) return false;
+            if(a.visible && !b.visible) return true;
+            if(!a.visible && b.visible) return false;
+            if(a.dist < b.dist) return true;
+            if(a.dist > b.dist) return false;
+            return true;
+        }
     };
-    bool target(gameent *d, aistate &b, int pursue = 0, bool force = false, float mindist = 0.f)
+
+    bool target(gameent *d, aistate &b, int pursue = 0, bool force = false)
     {
         if(passive()) return false;
-        static vector<targcache> targets; targets.setsize(0);
-        vec dp = d->headpos();
+        static vector<targcache> targets;
+        targets.setsize(0);
+        gameent *e = NULL;
         int numdyns = game::numdynents();
-        while(true)
-        {
-            targcache *t = NULL;
-            if(targets.empty())
-            {
-                gameent *e = NULL;
-                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) != NULL && e != d)
-                {
-                    targcache &c = targets.add();
-                    c.d = e;
-                    if(!(c.targ = targetable(d, e))) continue;
-                    c.pos = getaimpos(d, e, altfire(d, e));
-                    c.dist = c.pos.squaredist(dp);
-                    if(d->dominating.find(c.d) >= 0) c.dist *= 0.5f; // REVENGE
-                    if((!t || c.dist < t->dist) && (mindist <= 0 || c.dist <= mindist))
-                    {
-                        if(!(c.see = force || cansee(d, dp, c.pos, d->aitype >= AI_START))) continue;
-                        t = &c;
-                    }
-                }
-            }
-            else loopv(targets) if(!targets[i].tried && targets[i].targ && targets[i].see)
-            {
-                targcache &c = targets[i];
-                if((!t || c.dist < t->dist) && (mindist <= 0 || c.dist <= mindist)) t = &c;
-            }
-            if(t)
-            {
-                d->ai->enemy = -1;
-                d->ai->enemymillis = d->ai->enemyseen = 0;
-                if(violence(d, b, t->d, pursue)) return true;
-                t->tried = true;
-            }
-            else break;
-        }
+        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && targetable(d, e))
+        {
+            targcache &c = targets.add();
+            c.d = e;
+            c.dist = d->o.squaredist(e->o);
+            if(d->dominating.find(c.d) >= 0) c.dominated = true;
+            c.visible = force || cansee(d, d->o, e->o, d->actortype >= A_ENEMY);
+        }
+        if(targets.empty()) return false;
+        targets.sort(targcache::tcsort);
+        d->ai->enemy = -1;
+        d->ai->enemymillis = d->ai->enemyseen = 0;
+        loopv(targets) if(violence(d, b, targets[i].d, pursue || targets[i].dominated ? 1 : 0)) return true;
         return false;
     }
 
@@ -476,16 +485,17 @@ namespace ai
     {
         gameent *e = NULL;
         int numdyns = game::numdynents();
-        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e != d && (all || e->aitype == AI_NONE) && owner(d) == owner(e))
+        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e != d && (all || e->actortype == A_PLAYER) && d->team == e->team)
         {
             interest &n = interests.add();
             n.state = AI_S_DEFEND;
             n.node = e->lastnode;
             n.target = e->clientnum;
             n.targtype = AI_T_ACTOR;
-            n.score = e->o.squaredist(d->o)/(force ? 1e8f : (hasweap(d, d->ai->weappref) ? 1.f : 0.5f));
+            n.score = e->o.squaredist(d->o)/(force ? 1e8f : (hasweap(d, weappref(d)) ? 1.f : 0.5f));
             n.tolerance = 0.25f;
             n.team = true;
+            n.acttype = AI_A_PROTECT;
         }
     }
 
@@ -493,54 +503,39 @@ namespace ai
     {
         vec pos = d->feetpos();
         int sweap = m_weapon(game::gamemode, game::mutators);
-        loopj(entities::lastusetype[EU_ITEM])
+        loopj(entities::lastuse(EU_ITEM))
         {
             gameentity &e = *(gameentity *)entities::ents[j];
-            if(enttype[e.type].usetype != EU_ITEM) continue;
-            switch(e.type)
-            {
-                case WEAPON:
-                {
-                    int attr = w_attr(game::gamemode, game::mutators, e.attrs[0], sweap);
-                    if(e.spawned && isweap(attr) && wantsweap(d, attr))
-                    { // go get a weapon upgrade
-                        interest &n = interests.add();
-                        n.state = AI_S_INTEREST;
-                        n.node = closestwaypoint(e.o, CLOSEDIST, true);
-                        n.target = j;
-                        n.targtype = AI_T_ENTITY;
-                        n.score =  pos.squaredist(e.o)/(attr == d->ai->weappref ? 1e8f : (force ? 1e4f : 1.f));
-                        n.tolerance = 0;
-                    }
-                    break;
-                }
-                default: break;
+            if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON) continue;
+            int attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
+            if(e.spawned() && isweap(attr) && wantsweap(d, attr))
+            { // go get a weapon upgrade
+                interest &n = interests.add();
+                n.state = AI_S_INTEREST;
+                n.node = closestwaypoint(e.o, CLOSEDIST, true);
+                n.target = j;
+                n.targtype = AI_T_ENTITY;
+                n.score =  pos.squaredist(e.o)/(attr == weappref(d) ? 1e8f : (force ? 1e4f : 1.f));
+                n.tolerance = 0;
             }
         }
 
         loopvj(projs::projs) if(projs::projs[j]->projtype == PRJ_ENT && projs::projs[j]->ready())
         {
             projent &proj = *projs::projs[j];
-            if(!entities::ents.inrange(proj.id) || enttype[entities::ents[proj.id]->type].usetype != EU_ITEM) continue;
+            if(!entities::ents.inrange(proj.id)) continue;
             gameentity &e = *(gameentity *)entities::ents[proj.id];
-            switch(e.type)
-            {
-                case WEAPON:
-                {
-                    int attr = w_attr(game::gamemode, game::mutators, e.attrs[0], sweap);
-                    if(isweap(attr) && wantsweap(d, attr) && proj.owner != d)
-                    { // go get a weapon upgrade
-                        interest &n = interests.add();
-                        n.state = AI_S_INTEREST;
-                        n.node = closestwaypoint(proj.o, CLOSEDIST, true);
-                        n.target = proj.id;
-                        n.targtype = AI_T_DROP;
-                        n.score = pos.squaredist(proj.o)/(attr == d->ai->weappref ? 1e8f : (force ? 1e4f : 1.f));
-                        n.tolerance = 0;
-                    }
-                    break;
-                }
-                default: break;
+            if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON) continue;
+            int attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
+            if(isweap(attr) && wantsweap(d, attr) && proj.owner != d)
+            { // go get a weapon upgrade
+                interest &n = interests.add();
+                n.state = AI_S_INTEREST;
+                n.node = closestwaypoint(proj.o, CLOSEDIST, true);
+                n.target = proj.id;
+                n.targtype = AI_T_DROP;
+                n.score = pos.squaredist(proj.o)/(attr == weappref(d) ? 1e8f : (force ? 1e4f : 1.f));
+                n.tolerance = 0;
             }
         }
     }
@@ -548,70 +543,31 @@ namespace ai
     bool find(gameent *d, aistate &b)
     {
         static vector<interest> interests; interests.setsize(0);
-        if(d->aitype == AI_BOT)
+        if(d->actortype == A_BOT)
         {
             if(!passive())
             {
                 int sweap = m_weapon(game::gamemode, game::mutators);
-                if(!hasweap(d, d->ai->weappref) || d->carry(sweap) == 0) items(d, b, interests, d->carry(sweap) == 0);
+                if(!hasweap(d, weappref(d)) || d->carry(sweap) == 0) items(d, b, interests, d->carry(sweap) == 0);
                 if(m_team(game::gamemode, game::mutators) && !m_duke(game::gamemode, game::mutators))
-#ifdef CAMPAIGN
-                    assist(d, b, interests, false, m_campaign(game::gamemode) || m_gauntlet(game::gamemode));
-#else
-                    assist(d, b, interests, false, m_gauntlet(game::gamemode));
-#endif
+                    assist(d, b, interests, false, false);
             }
             if(m_fight(game::gamemode))
             {
                 if(m_capture(game::gamemode)) capture::aifind(d, b, interests);
                 else if(m_defend(game::gamemode)) defend::aifind(d, b, interests);
                 else if(m_bomber(game::gamemode)) bomber::aifind(d, b, interests);
-                else if(m_gauntlet(game::gamemode))
-                {
-                    int numents = entities::lastenttype[CHECKPOINT];
-                    loopi(numents) if(entities::ents[i]->type == CHECKPOINT && (entities::ents[i]->attrs[6] == CP_LAST || entities::ents[i]->attrs[6] == CP_FINISH))
-                    { // defend the flag
-                        interest &n = interests.add();
-                        if(d->team != T_ALPHA)
-                        {
-                            n.state = AI_S_DEFEND;
-                            n.target = n.node = randomnode(d, b, entities::ents[i]->o, CLOSEDIST, FARDIST);
-                            n.score = entities::ents[i]->o.squaredist(d->feetpos())/100.f;
-                        }
-                        else
-                        {
-                            n.state = AI_S_PURSUE;
-                            n.target = n.node = closestwaypoint(entities::ents[i]->o, CLOSEDIST, true);
-                            n.score = entities::ents[i]->o.squaredist(d->feetpos())/100.f;
-                        }
-                        n.targtype = AI_T_NODE;
-                        n.tolerance = 0.5f;
-                        n.team = true;
-                    }
-                }
-            }
-#ifdef CAMPAIGN
-            if(m_campaign(game::gamemode) && aicampaign)
-            {
-                loopi(entities::lastent(TRIGGER)) if(entities::ents[i]->type == TRIGGER && entities::ents[i]->attrs[1] == TR_EXIT)
-                {
-                    interest &n = interests.add();
-                    n.state = AI_S_PURSUE;
-                    n.target = i;
-                    n.node = closestwaypoint(entities::ents[i]->o, CLOSEDIST, true);
-                    n.targtype = AI_T_AFFINITY;
-                    n.score = -1;
-                    n.tolerance = 1;
-                }
             }
-#endif
         }
-        else if(entities::ents.inrange(d->aientity)) loopv(entities::ents[d->aientity]->links)
+        else if(entities::ents.inrange(d->spawnpoint)) loopv(entities::ents[d->spawnpoint]->links)
         {
+            int t = entities::ents[d->spawnpoint]->links[i];
+            if(!entities::ents.inrange(t)) continue;
             interest &n = interests.add();
             n.state = AI_S_DEFEND;
-            n.target = n.node = entities::ents[d->aientity]->links[i];
-            n.targtype = AI_T_NODE;
+            n.target = t;
+            n.node = closestwaypoint(entities::ents[t]->o, CLOSEDIST, true);
+            n.targtype = AI_T_ENTITY;
             n.score = -1;
             n.tolerance = 1;
         }
@@ -620,17 +576,17 @@ namespace ai
             int q = interests.length()-1;
             loopi(interests.length()-1) if(interests[i].score < interests[q].score) q = i;
             interest n = interests.removeunordered(q);
-            if(d->aitype == AI_BOT && m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
+            if(d->actortype == A_BOT && m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
             {
                 int members = 0;
                 static vector<int> targets; targets.setsize(0);
                 int others = checkothers(targets, d, n.state, n.targtype, n.target, n.team, &members);
-                if(d->aitype == AI_BOT && n.state == AI_S_DEFEND && members == 1) continue;
+                if(d->actortype == A_BOT && n.state == AI_S_DEFEND && members == 1) continue;
                 if(others >= int(ceilf(members*n.tolerance))) continue;
             }
-            if(!aistyle[d->aitype].canmove || makeroute(d, b, n.node))
+            if(!actor[d->actortype].canmove || makeroute(d, b, n.node))
             {
-                d->ai->switchstate(b, n.state, n.targtype, n.target);
+                d->ai->switchstate(b, n.state, n.targtype, n.target, n.acttype);
                 return true;
             }
         }
@@ -641,17 +597,17 @@ namespace ai
     {
         if(d != e)
         {
-            if(d->ai && (d->aitype >= AI_START || hithurts(flags) || d->ai->enemy < 0)) // see if this ai is interested in a grudge
+            if(d->ai && (d->actortype >= A_ENEMY || (hithurts(flags) && damage > 0) || d->ai->enemy < 0 || d->dominating.find(e))) // see if this ai is interested in a grudge
             {
                 aistate &b = d->ai->getstate();
-                violence(d, b, e, d->aitype != AI_BOT || weaptype[d->weapselect].melee ? 1 : 0);
+                violence(d, b, e, d->actortype != A_BOT || weaptype[d->weapselect].melee ? 1 : 0);
             }
             static vector<int> targets; // check if one of our ai is defending them
             targets.setsize(0);
             if(checkothers(targets, d, AI_S_DEFEND, AI_T_ACTOR, d->clientnum, true))
             {
                 gameent *t;
-                loopv(targets) if((t = game::getclient(targets[i])) && t->ai && t->aitype == AI_BOT && (hithurts(flags) || !game::getclient(t->ai->enemy)))
+                loopv(targets) if((t = game::getclient(targets[i])) && t->ai && t->actortype == A_BOT && ((hithurts(flags) && damage > 0) || t->ai->enemy < 0 || t->dominating.find(e)))
                 {
                     aistate &c = t->ai->getstate();
                     violence(t, c, e, weaptype[d->weapselect].melee ? 1 : 0);
@@ -662,27 +618,13 @@ namespace ai
 
     void setup(gameent *d, int ent = -1)
     {
-        d->aientity = ent;
+        d->spawnpoint = ent;
         if(d->ai)
         {
             d->ai->clean();
             d->ai->reset(true);
             d->ai->lastrun = lastmillis;
-            if(d->aitype >= AI_START)
-            {
-                if(entities::ents.inrange(d->aientity) && entities::ents[d->aientity]->type == ACTOR && entities::ents[d->aientity]->attrs[6] > 0)
-                    d->ai->weappref = entities::ents[d->aientity]->attrs[6]-1;
-                else d->ai->weappref = aistyle[d->aitype].weap;
-                if(!isweap(d->ai->weappref)) d->ai->weappref = rnd(W_MAX);
-            }
-            else
-            {
-                if(m_sweaps(game::gamemode, game::mutators)) d->ai->weappref = m_weapon(game::gamemode, game::mutators);
-                else if(aiforcegun >= 0 && aiforcegun < W_MAX) d->ai->weappref = aiforcegun;
-                else d->ai->weappref = rnd(W_LOADOUT)+W_OFFSET;
-            }
-            vec dp = d->headpos();
-            findorientation(dp, d->yaw, d->pitch, d->ai->target);
+            findorientation(d->o, d->yaw, d->pitch, d->ai->target);
         }
     }
 
@@ -700,20 +642,20 @@ namespace ai
     {
         if(!passive() && m_fight(game::gamemode) && entities::ents.inrange(ent) && entities::ents[ent]->type == WEAPON && spawned > 0)
         {
-            int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, entities::ents[ent]->attrs[0], sweap);
-            loopv(game::players) if(game::players[i] && game::players[i]->ai && game::players[i]->aitype == AI_BOT && game::players[i]->state == CS_ALIVE && iswaypoint(game::players[i]->lastnode))
+            int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, entities::ents[ent]->type, entities::ents[ent]->attrs[0], sweap);
+            loopv(game::players) if(game::players[i] && game::players[i]->ai && game::players[i]->actortype == A_BOT && game::players[i]->state == CS_ALIVE && iswaypoint(game::players[i]->lastnode))
             {
                 gameent *d = game::players[i];
                 aistate &b = d->ai->getstate();
                 if(b.targtype == AI_T_AFFINITY) continue; // don't override any affinity states
-                if(!hasweap(d, attr) && (!hasweap(d, d->ai->weappref) || d->carry(sweap) == 0) && wantsweap(d, attr))
+                if(!hasweap(d, attr) && (!hasweap(d, weappref(d)) || d->carry(sweap) == 0) && wantsweap(d, attr))
                 {
                     if(b.type == AI_S_INTEREST && (b.targtype == AI_T_ENTITY || b.targtype == AI_T_DROP))
                     {
                         if(entities::ents.inrange(b.target))
                         {
-                            int weap = w_attr(game::gamemode, game::mutators, entities::ents[b.target]->attrs[0], sweap);
-                            if((attr == d->ai->weappref && weap != d->ai->weappref) || d->o.squaredist(entities::ents[ent]->o) < d->o.squaredist(entities::ents[b.target]->o))
+                            int weap = w_attr(game::gamemode, game::mutators, entities::ents[ent]->type, entities::ents[b.target]->attrs[0], sweap);
+                            if((attr == weappref(d) && weap != weappref(d)) || d->o.squaredist(entities::ents[ent]->o) < d->o.squaredist(entities::ents[b.target]->o))
                                 d->ai->switchstate(b, AI_S_INTEREST, AI_T_ENTITY, ent);
                         }
                         continue;
@@ -726,7 +668,7 @@ namespace ai
 
     bool check(gameent *d, aistate &b)
     {
-        if(d->aitype == AI_BOT)
+        if(d->actortype == A_BOT)
         {
             if(m_capture(game::gamemode) && capture::aicheck(d, b)) return true;
             else if(m_defend(game::gamemode) && defend::aicheck(d, b)) return true;
@@ -735,102 +677,79 @@ namespace ai
         return false;
     }
 
-    int dowait(gameent *d, aistate &b)
+    bool dowait(gameent *d, aistate &b)
     {
         //d->ai->clear(true); // ensure they're clean
-        if(check(d, b) || find(d, b)) return 1;
-        if(target(d, b, 4, false)) return 1;
-        if(target(d, b, 4, true)) return 1;
-        if(aistyle[d->aitype].canmove && randomnode(d, b, CLOSEDIST, 1e16f))
+        if(check(d, b) || find(d, b)) return true;
+        if(!passive())
+        {
+            if(target(d, b, 4, false)) return true;
+            if(target(d, b, 4, true)) return true;
+        }
+        if(actor[d->actortype].canmove && randomnode(d, b, CLOSEDIST, 1e16f))
         {
             d->ai->switchstate(b, AI_S_INTEREST, AI_T_NODE, d->ai->route[0]);
-            return 1;
+            return true;
         }
-        return 0; // but don't pop the state
+        return false; // but don't pop the state
     }
 
-    int dodefense(gameent *d, aistate &b)
+    bool dodefense(gameent *d, aistate &b)
     {
-        if(d->state != CS_ALIVE) return 0;
+        if(d->state != CS_ALIVE) return false;
         switch(b.targtype)
         {
-            case AI_T_NODE:
+            case AI_T_ENTITY:
             {
-                if(check(d, b)) return 1;
-                if(iswaypoint(b.target))
-                    return defense(d, b, waypoints[b.target].o) ? 1 : 0;
+                if(check(d, b)) return true;
+                if(entities::ents.inrange(b.target)) return defense(d, b, entities::ents[b.target]->o);
                 break;
             }
-            case AI_T_ENTITY:
+            case AI_T_AFFINITY:
             {
-                if(check(d, b)) return 1;
-                if(entities::ents.inrange(b.target))
-                {
-                    gameentity &e = *(gameentity *)entities::ents[b.target];
-                    return defense(d, b, e.o) ? 1 : 0;
-                }
+                if(m_capture(game::gamemode)) return capture::aidefense(d, b);
+                else if(m_defend(game::gamemode)) return defend::aidefense(d, b);
+                else if(m_bomber(game::gamemode)) return bomber::aidefense(d, b);
                 break;
             }
-            case AI_T_AFFINITY:
+            case AI_T_ACTOR:
             {
-#ifdef CAMPAIGN
-                if(m_campaign(game::gamemode))
+                if(check(d, b)) return true;
+                gameent *e = game::getclient(b.target);
+                if(e && d->team == e->team)
                 {
-                    if(aicampaign && entities::ents.inrange(b.target)) return defense(d, b, entities::ents[b.target]->o) ? 1 : 0;
+                    if(e->state == CS_ALIVE) return defense(d, b, e->feetpos());
+                    if(b.owner >= 0) return patrol(d, b, d->feetpos());
                 }
-                else
-#endif
-                if(m_capture(game::gamemode)) return capture::aidefense(d, b) ? 1 : 0;
-                else if(m_defend(game::gamemode)) return defend::aidefense(d, b) ? 1 : 0;
-                else if(m_bomber(game::gamemode)) return bomber::aidefense(d, b) ? 1 : 0;
                 break;
             }
-            case AI_T_ACTOR:
+            default:
             {
-                if(check(d, b)) return 1;
-                gameent *e = game::getclient(b.target);
-                if(e && e->state == CS_ALIVE) return defense(d, b, e->feetpos()) ? 1 : 0;
+                if(check(d, b)) return true;
+                if(iswaypoint(b.target)) return defense(d, b, waypoints[b.target].o, CLOSEDIST, 0, 0);
                 break;
             }
-            default: break;
         }
-        return 0;
+        return false;
     }
 
-    int dointerest(gameent *d, aistate &b)
+    bool dointerest(gameent *d, aistate &b)
     {
-        if(d->state != CS_ALIVE || !aistyle[d->aitype].canmove) return 0;
+        if(d->state != CS_ALIVE || !actor[d->actortype].canmove) return false;
         switch(b.targtype)
         {
-            case AI_T_NODE: // this is like a wait state without sitting still..
-            {
-                if(check(d, b) || find(d, b)) return 1;
-                if(target(d, b, 4, true)) return 1;
-                if(iswaypoint(b.target) && vec(waypoints[b.target].o).sub(d->feetpos()).magnitude() > CLOSEDIST)
-                    return makeroute(d, b, waypoints[b.target].o) ? 1 : 0;
-                break;
-            }
             case AI_T_ENTITY:
             {
                 if(entities::ents.inrange(b.target))
                 {
                     gameentity &e = *(gameentity *)entities::ents[b.target];
-                    if(enttype[e.type].usetype != EU_ITEM) return 0;
-                    int sweap = m_weapon(game::gamemode, game::mutators),
-                        attr = w_attr(game::gamemode, game::mutators, e.attrs[0], sweap);
-                    switch(e.type)
-                    {
-                        case WEAPON:
-                        {
-                            if(!e.spawned || !wantsweap(d, attr)) return 0;
-                            //float guard = enttype[e.type].radius;
-                            //if(d->feetpos().squaredist(e.o) <= guard*guard)
-                            //    b.idle = enemy(d, b, e.o, guard*4, weaptype[d->weapselect].melee ? 1 : 0, false) ? 2 : 1;
-                            break;
-                        }
-                        default: break;
-                    }
-                    return makeroute(d, b, e.o) ? 1 : 0;
+                    if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON) return false;
+                    int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
+                    if(!isweap(attr) || !e.spawned() || !wantsweap(d, attr)) return false;
+                    //float guard = enttype[e.type].radius;
+                    //if(d->feetpos().squaredist(e.o) <= guard*guard)
+                    //    b.acttype = enemy(d, b, e.o, guard*4, weaptype[d->weapselect].melee ? 1 : 0, false) ? AI_A_PROTECT : AI_A_IDLE;
+                    return makeroute(d, b, e.o);
                 }
                 break;
             }
@@ -839,93 +758,79 @@ namespace ai
                 loopvj(projs::projs) if(projs::projs[j]->projtype == PRJ_ENT && projs::projs[j]->ready() && projs::projs[j]->id == b.target)
                 {
                     projent &proj = *projs::projs[j];
-                    if(!entities::ents.inrange(proj.id) || enttype[entities::ents[proj.id]->type].usetype != EU_ITEM) return 0;
+                    if(!entities::ents.inrange(proj.id) || proj.owner == d) return false;
                     gameentity &e = *(gameentity *)entities::ents[proj.id];
-                    int sweap = m_weapon(game::gamemode, game::mutators),
-                        attr = w_attr(game::gamemode, game::mutators, e.attrs[0], sweap);
-                    switch(e.type)
-                    {
-                        case WEAPON:
-                        {
-                            if(!wantsweap(d, attr) || proj.owner == d) return 0;
-                            //float guard = enttype[e.type].radius;
-                            //if(d->feetpos().squaredist(e.o) <= guard*guard)
-                            //    b.idle = enemy(d, b, e.o, guard*4, weaptype[d->weapselect].melee ? 1 : 0, false) ? 2 : 1;
-                            break;
-                        }
-                        default: break;
-                    }
-                    return makeroute(d, b, proj.o) ? 1 : 0;
-                    break;
+                    if(enttype[entities::ents[proj.id]->type].usetype != EU_ITEM || e.type != WEAPON) return false;
+                    int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
+                    if(!isweap(attr) || !wantsweap(d, attr)) return false;
+                    //float guard = enttype[e.type].radius;
+                    //if(d->feetpos().squaredist(e.o) <= guard*guard)
+                    //    b.acttype = enemy(d, b, e.o, guard*4, weaptype[d->weapselect].melee ? 1 : 0, false) ? AI_A_PROTECT : AI_A_IDLE;
+                    return makeroute(d, b, proj.o);
                 }
                 break;
             }
-            default: break;
+            default: // this is like a wait state without sitting still..
+            {
+                if(check(d, b) || (b.owner < 0 && find(d, b))) return true;
+                if(target(d, b, b.owner < 0 ? 0 : 4, true)) return true;
+                if(iswaypoint(b.target) && (b.owner >= 0 || d->lastnode != b.target))
+                    return defense(d, b, waypoints[b.target].o, CLOSEDIST, b.owner >= 0 ? 0.f : FARDIST, b.owner >= 0 ? 0 : 2);
+                break;
+            }
         }
-        return 0;
+        return false;
     }
 
-    int dopursue(gameent *d, aistate &b)
+    bool dopursue(gameent *d, aistate &b)
     {
-        if(d->state != CS_ALIVE) return 0;
+        if(d->state != CS_ALIVE) return false;
         switch(b.targtype)
         {
-            case AI_T_NODE:
-            {
-                if(check(d, b)) return 1;
-                if(iswaypoint(b.target))
-                    return defense(d, b, waypoints[b.target].o) ? 1 : 0;
-                break;
-            }
             case AI_T_AFFINITY:
             {
-#ifdef CAMPAIGN
-                if(m_campaign(game::gamemode))
-                {
-                    if(aicampaign && entities::ents.inrange(b.target)) return defense(d, b, entities::ents[b.target]->o) ? 1 : 0;
-                }
-                else
-#endif
-                if(m_capture(game::gamemode)) return capture::aipursue(d, b) ? 1 : 0;
-                else if(m_defend(game::gamemode)) return defend::aipursue(d, b) ? 1 : 0;
-                else if(m_bomber(game::gamemode)) return bomber::aipursue(d, b) ? 1 : 0;
+                if(m_capture(game::gamemode)) return capture::aipursue(d, b);
+                else if(m_defend(game::gamemode)) return defend::aipursue(d, b);
+                else if(m_bomber(game::gamemode)) return bomber::aipursue(d, b);
                 break;
             }
 
             case AI_T_ACTOR:
             {
-                if(passive()) return 0;
-                //if(check(d, b)) return 1;
+                //if(check(d, b)) return true;
                 gameent *e = game::getclient(b.target);
-                if(e && e->state == CS_ALIVE)
+                if(e && targetable(d, e))
                 {
-                    bool alt = altfire(d, e);
-                    if(aistyle[d->aitype].canmove)
-                    {
-                        float mindist = weapmindist(d->weapselect, alt);
-                        if(!weaptype[d->weapselect].melee) mindist = min(mindist, CLOSEDIST);
-                        return patrol(d, b, e->feetpos(), mindist, d->ai->views[2]) ? 1 : 0;
-                    }
-                    else
+                    if(e->state == CS_ALIVE)
                     {
-                        vec dp = d->headpos(), ep = getaimpos(d, e, alt);
-                        if(cansee(d, dp, ep, d->aitype >= AI_START) || (e->clientnum == d->ai->enemy && d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*10)+1000))
-                            return 1;
-                        return 0;
+                        bool alt = altfire(d, e);
+                        if(!actor[d->actortype].canmove)
+                        {
+                            if(cansee(d, d->o, e->o, d->actortype >= A_ENEMY) || (e->clientnum == d->ai->enemy && d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*30)+1000))
+                                return true;
+                            return false;
+                        }
+                        return patrol(d, b, e->feetpos(), weapmindist(d->weapselect, alt), weapmaxdist(d->weapselect, alt));
                     }
+                    if(b.owner >= 0) return patrol(d, b, d->feetpos());
                 }
                 break;
             }
-            default: break;
+            default:
+            {
+                if(check(d, b)) return true;
+                if(iswaypoint(b.target)) return defense(d, b, waypoints[b.target].o);
+                break;
+            }
         }
-        return 0;
+        return false;
     }
 
     int closenode(gameent *d)
     {
         vec pos = d->feetpos();
         int node1 = -1, node2 = -1, node3 = -1;
-        float mindist1 = CLOSEDIST*CLOSEDIST, mindist2 = physics::jetpack(d) ? JETDIST*JETDIST : RETRYDIST*RETRYDIST, mindist3 = mindist2;
+        float mindist1 = CLOSEDIST*CLOSEDIST, mindist2 = RETRYDIST*RETRYDIST, mindist3 = mindist2;
         loopv(d->ai->route) if(iswaypoint(d->ai->route[i]))
         {
             vec epos = waypoints[d->ai->route[i]].o;
@@ -955,7 +860,9 @@ namespace ai
             if(iswaypoint(entid))
             {
                 vec feet = d->feetpos();
-                if(!aistyle[d->aitype].canjump && epos.z-d->feetpos().z >= JUMPMIN) epos.z = feet.z;
+                float zoff = epos.z-d->feetpos().z;
+                if(!actor[d->actortype].canjump && zoff >= JUMPMIN) epos.z = feet.z;
+                else if(actor[d->actortype].canjump && d->airtime(lastmillis) >= 25 && zoff <= -JUMPMIN) epos.z = feet.z;
                 d->ai->spot = epos;
                 d->ai->targnode = entid;
                 return !check || feet.squaredist(epos) > MINWPDIST*MINWPDIST ? 1 : 2;
@@ -1067,24 +974,23 @@ namespace ai
         return anynode(d, b);
     }
 
-    void jumpto(gameent *d, aistate &b, const vec &pos, bool locked)
+    void jumpto(gameent *d, aistate &b, const vec &pos)
     {
         vec off = vec(pos).sub(d->feetpos());
         int airtime = d->airtime(lastmillis);
         bool sequenced = d->ai->blockseq || d->ai->targseq, offground = airtime && !physics::liquidcheck(d) && !d->onladder,
-             jet = airtime > 250 && !d->turnside && off.z >= JUMPMIN && physics::canjet(d),
-             impulse = airtime > 500 && !d->turnside && off.z >= JUMPMIN && physics::canimpulse(d, IM_A_BOOST, false) && !physics::jetpack(d),
-             jumper = !offground && (locked || sequenced || off.z >= JUMPMIN || (d->aitype == AI_BOT && lastmillis >= d->ai->jumprand)),
-             jump = (impulse || jet || jumper) && (jet || lastmillis >= d->ai->jumpseed);
+             impulse = airtime > (b.acttype >= AI_A_LOCKON ? 100 : 250) && !d->turnside && (b.acttype >= AI_A_LOCKON || off.z >= JUMPMIN) && physics::canimpulse(d, IM_A_BOOST, false) && (m_freestyle(game::gamemode, game::mutators) || impulsemeter-d->impulse[IM_METER] >= impulsecost),
+             jumper = !offground && (b.acttype == AI_A_LOCKON || sequenced || off.z >= JUMPMIN || (d->actortype == A_BOT && lastmillis >= d->ai->jumprand)),
+             jump = (impulse || jumper) && lastmillis >= d->ai->jumpseed;
         if(jump)
         {
             vec old = d->o;
             d->o = vec(pos).add(vec(0, 0, d->height));
-            if(!collide(d, vec(0, 0, 1))) jump = false;
+            if(collide(d, vec(0, 0, 1))) jump = false;
             d->o = old;
             if(jump)
             {
-                loopi(entities::lastenttype[PUSHER]) if(entities::ents[i]->type == PUSHER)
+                loopi(entities::lastent(PUSHER)) if(entities::ents[i]->type == PUSHER)
                 {
                     gameentity &e = *(gameentity *)entities::ents[i];
                     float radius = (e.attrs[3] ? e.attrs[3] : enttype[e.type].radius)*1.5f; radius *= radius;
@@ -1099,14 +1005,14 @@ namespace ai
         }
         if(jumper && d->action[AC_JUMP])
         {
-            int seed = (111-d->skill)*(d->onladder || d->inliquid ? 3 : 5);
+            int seed = (111-d->skill)*(b.acttype == AI_A_LOCKON || b.acttype == AI_A_HASTE ? 2 : (d->onladder || d->inliquid || b.acttype == AI_A_PROTECT ? 4 : 8));
             d->ai->jumpseed = lastmillis+seed+rnd(seed);
-            seed *= b.idle == 1 ? 1000 : 100;
+            seed *= b.acttype == AI_A_IDLE ? 500 : 100;
             d->ai->jumprand = lastmillis+seed+rnd(seed);
         }
         if(!sequenced && !d->onladder && airtime)
         {
-            if(airtime > 300 && !d->turnside && (d->skill >= 100 || !rnd(101-d->skill)) && physics::canimpulse(d, IM_A_PARKOUR, true))
+            if(airtime > (b.acttype >= AI_A_LOCKON ? 250 : 500) && !d->turnside && (d->skill >= 100 || !rnd(101-d->skill)) && physics::canimpulse(d, IM_A_PARKOUR, true))
                 d->action[AC_SPECIAL] = true;
             else if(!passive() && lastmillis-d->ai->lastmelee >= (201-d->skill)*5 && d->canmelee(m_weapon(game::gamemode, game::mutators), lastmillis))
             {
@@ -1118,7 +1024,7 @@ namespace ai
 
     bool lockon(gameent *d, gameent *e, float maxdist, bool check)
     {
-        if(!passive() && check && !d->blocked)
+        if(!passive() && check && !d->blocked && (d->inmaterial&MATF_CLIP) != MAT_AICLIP)
         {
             vec dir = vec(e->o).sub(d->o);
             float xydist = dir.x*dir.x+dir.y*dir.y, zdist = dir.z*dir.z, mdist = maxdist*maxdist, ddist = d->radius*d->radius+e->radius*e->radius;
@@ -1127,23 +1033,29 @@ namespace ai
         return false;
     }
 
-    int process(gameent *d, aistate &b)
+    void process(gameent *d, aistate &b, bool &occupied, bool &firing, bool &enemyok)
     {
-        int result = 0, skmod = max(101-d->skill, 1);
-        float frame = d->skill <= 100 ? float(lastmillis-d->ai->lastrun)/float(skmod*10) : 1;
-        if(!aistyle[d->aitype].canstrafe && d->skill <= 100) frame *= 2;
-        vec dp = d->headpos();
+        int skmod = max(101-d->skill, 1);
+        float frame = d->skill <= 100 ? ((lastmillis-d->ai->lastrun)*(100.f/gamespeed))/float(skmod*10) : 1;
+        if(!actor[d->actortype].canstrafe && d->skill <= 100) frame *= 2;
+        if(d->dominating.length()) frame *= 1+d->dominating.length();
 
         d->action[AC_SPECIAL] = d->ai->dontmove = false;
-        if(b.idle == 1 || !aistyle[d->aitype].canmove)
+        if(b.acttype == AI_A_IDLE || !actor[d->actortype].canmove)
         {
+            frame *= 10;
             d->ai->dontmove = true;
-            d->ai->spot = vec(0, 0, 0);
+            d->ai->spot = d->feetpos();
+        }
+        else if(hunt(d, b))
+        {
+            vec fp = d->feetpos();
+            game::getyawpitch(fp, d->ai->spot, d->ai->targyaw, d->ai->targpitch);
+            if(d->ai->route.length() <= 1 && d->ai->spot.squaredist(fp) <= MINWPDIST*MINWPDIST) d->ai->dontmove = true;
         }
-        else if(hunt(d, b)) game::getyawpitch(d->feetpos(), d->ai->spot, d->ai->targyaw, d->ai->targpitch);
         else
         {
-            if(d->blocked && (!d->ai->lastturn || lastmillis-d->ai->lastturn >= 1000))
+            if((d->blocked || (d->inmaterial&MATF_CLIP) == MAT_AICLIP) && (!d->ai->lastturn || lastmillis-d->ai->lastturn >= 1000))
             {
                 d->ai->targyaw += 90+rnd(180);
                 d->ai->lastturn = lastmillis;
@@ -1154,116 +1066,91 @@ namespace ai
             d->ai->targnode = -1;
         }
 
-        bool enemyok = false, locked = false;
         gameent *e = game::getclient(d->ai->enemy);
-        if(!passive())
+        if(passive()) enemyok = false;
+        else if(!(enemyok = (e && targetable(d, e, true))) || d->skill >= 50 || d->ai->dontmove)
         {
-            if(!(enemyok = e && targetable(d, e, true)) || d->skill >= 50)
+            gameent *f = game::intersectclosest(d->o, d->ai->target, d);
+            if(f)
             {
-                gameent *f = game::intersectclosest(dp, d->ai->target, d);
-                if(f)
+                if(targetable(d, f, true))
                 {
-                    if(targetable(d, f, true))
-                    {
-                        if(!enemyok) violence(d, b, f, weaptype[d->weapselect].melee ? 1 : 0);
-                        enemyok = true;
-                        e = f;
-                    }
-                    else enemyok = false;
+                    if(!enemyok) violence(d, b, f, weaptype[d->weapselect].melee ? 1 : 0);
+                    enemyok = true;
+                    e = f;
                 }
-                else if(!enemyok && target(d, b, weaptype[d->weapselect].melee ? 1 : 0))
-                    enemyok = (e = game::getclient(d->ai->enemy)) != NULL;
-            }
-            if(enemyok)
-            {
-                bool alt = altfire(d, e);
-                vec ep = getaimpos(d, e, alt);
-                float yaw, pitch;
-                game::getyawpitch(dp, ep, yaw, pitch);
-                game::fixrange(yaw, pitch);
-                bool insight = cansee(d, dp, ep), hasseen = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*10)+3000,
-                    quick = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (W2(d->weapselect, fullauto, alt) ? W2(d->weapselect, attackdelay, alt)*3 : skmod)+30;
-                if(insight) d->ai->enemyseen = lastmillis;
-                if(b.idle || insight || hasseen || quick)
+                else enemyok = false; // would hit non-targetable person
+            }
+            else if((!enemyok || d->ai->dontmove) && target(d, b, weaptype[d->weapselect].melee ? 1 : 0, d->ai->dontmove))
+                enemyok = (e = game::getclient(d->ai->enemy)) != NULL;
+        }
+        if(enemyok)
+        {
+            bool alt = altfire(d, e);
+            vec ep = getaimpos(d, e, alt);
+            float yaw, pitch;
+            game::getyawpitch(d->o, ep, yaw, pitch);
+            game::fixrange(yaw, pitch);
+            bool insight = cansee(d, d->o, e->o), hasseen = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*10)+1000,
+                 quick = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (W2(d->weapselect, fullauto, alt) ? W2(d->weapselect, delayattack, alt)*3 : skmod*3)+skmod*3;
+            if(insight) d->ai->enemyseen = lastmillis;
+            if(d->ai->dontmove || insight || hasseen || quick)
+            {
+                frame *= insight || d->skill > 100 ? 1.5f : (hasseen || quick ? 1.25f : 1.f);
+                if(lockon(d, e, actor[d->actortype].canstrafe ? 32 : 16, weaptype[d->weapselect].melee))
                 {
-                    float sskew = insight || d->skill > 100 ? 1.5f : (hasseen ? 1.f : 0.5f);
-                    if(insight && lockon(d, e, aistyle[d->aitype].canstrafe ? 32 : 16, weaptype[d->weapselect].melee))
-                    {
-                        d->ai->targyaw = yaw;
-                        d->ai->targpitch = pitch;
-                        frame *= 2;
-                        locked = true;
-                    }
-                    game::scaleyawpitch(d->yaw, d->pitch, yaw, pitch, frame, frame*sskew);
-                    if(insight || quick)
-                    {
-                        bool shoot = canshoot(d, e, alt);
-                        if(d->action[alt ? AC_SECONDARY : AC_PRIMARY] && W2(d->weapselect, power, alt) && W2(d->weapselect, cooked, alt))
-                        { // TODO: make AI more aware of what they're shooting
-                            int cooked = W2(d->weapselect, cooked, alt);
-                            if(cooked&8) shoot = false; // inverted life
-                        }
-                        if(shoot && hastarget(d, b, e, alt, yaw, pitch, dp.squaredist(ep)))
-                        {
-                            d->action[alt ? AC_SECONDARY : AC_PRIMARY] = true;
-                            d->actiontime[alt ? AC_SECONDARY : AC_PRIMARY] = lastmillis;
-                            result = 3;
-                        }
-                        else result = 2;
-                    }
-                    else result = 1;
+                    frame *= 2.f;
+                    b.acttype = AI_A_LOCKON;
+                    d->ai->dontmove = false;
+                    d->ai->targyaw = yaw;
+                    d->ai->targpitch = pitch;
+                    d->ai->spot = e->feetpos();
                 }
-                else
-                {
-                    if(!d->ai->enemyseen || lastmillis-d->ai->enemyseen > (d->skill*50)+3000)
-                    {
-                        d->ai->enemy = -1;
-                        d->ai->enemyseen = d->ai->enemymillis = 0;
-                    }
-                    enemyok = false;
-                    result = 0;
+                game::scaleyawpitch(d->yaw, d->pitch, yaw, pitch, frame, frame*0.75f);
+                bool shoot = canshoot(d, e, alt);
+                if(d->action[alt ? AC_SECONDARY : AC_PRIMARY] && W2(d->weapselect, cooktime, alt) && W2(d->weapselect, cooked, alt))
+                { // TODO: make AI more aware of what they're shooting
+                    int cooked = W2(d->weapselect, cooked, alt);
+                    if(cooked&8) shoot = false; // inverted life
                 }
-            }
-            else
-            {
-                if(!enemyok)
+                if(shoot && hastarget(d, b, e, alt, insight || (!d->ai->dontmove && quick), yaw, pitch))
                 {
-                    d->ai->enemy = -1;
-                    d->ai->enemyseen = d->ai->enemymillis = 0;
+                    d->action[alt ? AC_SECONDARY : AC_PRIMARY] = true;
+                    d->actiontime[alt ? AC_SECONDARY : AC_PRIMARY] = lastmillis;
+                    firing = true;
                 }
-                enemyok = false;
-                result = 0;
+                occupied = true;
             }
+            else enemyok = false;
         }
-        else
+        if(!enemyok)
         {
             d->ai->enemy = -1;
             d->ai->enemyseen = d->ai->enemymillis = 0;
-            result = 0;
         }
-        if(result < 3) d->action[AC_PRIMARY] = d->action[AC_SECONDARY] = false;
+        if(!firing) d->action[AC_PRIMARY] = d->action[AC_SECONDARY] = false;
 
         game::fixrange(d->ai->targyaw, d->ai->targpitch);
-        if(!result) game::scaleyawpitch(d->yaw, d->pitch, d->ai->targyaw, d->ai->targpitch, frame, frame*0.5f);
-
-        if(aistyle[d->aitype].canjump && (!d->ai->dontmove || b.idle)) jumpto(d, b, d->ai->spot, locked);
-        if(d->aitype == AI_BOT || d->aitype == AI_GRUNT)
+        if(!occupied)
         {
-            bool wantsrun = false;
-            if(physics::allowimpulse(d, IM_A_SPRINT))
+            if(!m_insta(game::gamemode, game::mutators))
             {
-                if(!impulsemeter || impulsepacing == 0 || impulseregenpacing > 0) wantsrun = true;
-                //else if(b.idle == -1 && !d->ai->dontmove)
-                //    wantsrun = (d->action[AC_PACING] || !d->actiontime[AC_PACING] || lastmillis-d->actiontime[AC_PACING] > PHYSMILLIS*2);
+                if(b.acttype == AI_A_NORMAL && (d->health <= m_health(game::gamemode, game::mutators, d->model)/3 || (iswaypoint(d->ai->targnode) && obstacles.find(d->ai->targnode, d))))
+                    b.acttype = AI_A_HASTE;
+                if(b.acttype == AI_A_HASTE) frame *= 1+(max(m_health(game::gamemode, game::mutators, d->model)/3, 1)/float(max(d->health, 1)));
             }
-            if(d->action[AC_PACING] != wantsrun)
-                if((d->action[AC_PACING] = !d->action[AC_PACING]) == true) d->actiontime[AC_PACING] = lastmillis;
+            else frame *= 2;
+            game::scaleyawpitch(d->yaw, d->pitch, d->ai->targyaw, d->ai->targpitch, frame, frame*0.5f);
         }
 
-        if(d->ai->dontmove || (d->aitype >= AI_START && lastmillis-d->lastpain <= PHYSMILLIS/3)) d->move = d->strafe = 0;
-        else if(!aistyle[d->aitype].canmove || !aistyle[d->aitype].canstrafe)
+        if(actor[d->actortype].canjump) jumpto(d, b, d->ai->spot);
+        if((d->actortype == A_BOT || d->actortype == A_GRUNT) && d->action[AC_CROUCH] != d->ai->dontmove)
+            if((d->action[AC_CROUCH] = !d->action[AC_CROUCH]) == true) d->actiontime[AC_CROUCH] = lastmillis;
+
+        if(d->ai->dontmove || (d->actortype >= A_ENEMY && lastmillis-d->lastpain <= PHYSMILLIS/3)) d->move = d->strafe = 0;
+        else if(!actor[d->actortype].canmove || !actor[d->actortype].canstrafe)
         {
-            d->move = aistyle[d->aitype].canmove ? 1 : 0;
+            d->move = actor[d->actortype].canmove ? 1 : 0;
             d->strafe = 0;
         }
         else
@@ -1286,35 +1173,32 @@ namespace ai
             d->move = ad.move;
             d->strafe = ad.strafe;
         }
-        if(!aistyle[d->aitype].canstrafe && d->move && enemyok && lockon(d, e, 8, weaptype[d->weapselect].melee)) d->move = 0;
-        findorientation(dp, d->yaw, d->pitch, d->ai->target);
-        return result;
+        if(!actor[d->actortype].canstrafe && d->move && enemyok && lockon(d, e, 8, weaptype[d->weapselect].melee)) d->move = 0;
+        findorientation(d->o, d->yaw, d->pitch, d->ai->target);
     }
 
     bool hasrange(gameent *d, gameent *e, int weap)
     {
-        if(!e) return true;
-        if(targetable(d, e))
+        if(!targetable(d, e)) return false;
+        loopk(2)
         {
-            loopk(2)
-            {
-                vec ep = getaimpos(d, e, k!=0);
-                float dist = ep.squaredist(d->headpos());
-                if(weaprange(d, weap, k!=0, dist)) return true;
-            }
+            float dist = e->o.squaredist(d->o);
+            if(weaprange(d, weap, k!=0, dist)) return true;
         }
         return false;
     }
 
     bool request(gameent *d, aistate &b)
     {
-        int busy = process(d, b), sweap = m_weapon(game::gamemode, game::mutators);
-        bool haswaited = d->weapwaited(d->weapselect, lastmillis, (1<<W_S_RELOAD));
-        if(d->aitype == AI_BOT)
+        int sweap = m_weapon(game::gamemode, game::mutators);
+        bool occupied = false, firing = false, enemyok = false,
+             haswaited = d->weapwaited(d->weapselect, lastmillis, (1<<W_S_RELOAD));
+        process(d, b, occupied, firing, enemyok);
+        if(d->actortype == A_BOT)
         {
-            if(b.idle == 1 && busy <= 1 && d->carry(sweap, 1) > 1 && d->weapstate[d->weapselect] != W_S_WAIT)
+            if(d->ai->dontmove && haswaited && !firing && d->carry(sweap, 1) > 1)
             {
-                loopirev(W_ITEM) if(i != d->ai->weappref && d->candrop(i, sweap, lastmillis, G(weaponinterrupts)))
+                loopirev(W_ITEM) if(i != weappref(d) && d->candrop(i, sweap, lastmillis, m_loadout(game::gamemode, game::mutators), (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
                 {
                     client::addmsg(N_DROP, "ri3", d->clientnum, lastmillis-game::maptime, i);
                     d->setweapstate(d->weapselect, W_S_WAIT, weaponswitchdelay, lastmillis);
@@ -1322,11 +1206,13 @@ namespace ai
                     return true;
                 }
             }
-            if(busy <= 2 && !d->action[AC_USE] && haswaited)
+            if(haswaited && !firing && !d->action[AC_USE])
             {
                 static vector<actitem> actitems;
                 actitems.setsize(0);
-                if(entities::collateitems(d, actitems))
+                vec pos = d->center();
+                float radius = max(d->height*0.5f, max(d->xradius, d->yradius));
+                if(entities::collateitems(d, pos, radius, actitems))
                 {
                     while(!actitems.empty())
                     {
@@ -1338,7 +1224,7 @@ namespace ai
                             {
                                 if(!entities::ents.inrange(t.target)) break;
                                 extentity &e = *entities::ents[t.target];
-                                if(enttype[e.type].usetype != EU_ITEM) break;
+                                if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON) break;
                                 ent = t.target;
                                 break;
                             }
@@ -1348,7 +1234,7 @@ namespace ai
                                 projent &proj = *projs::projs[t.target];
                                 if(!entities::ents.inrange(proj.id)) break;
                                 extentity &e = *entities::ents[proj.id];
-                                if(enttype[e.type].usetype != EU_ITEM || proj.owner == d) break;
+                                if(enttype[e.type].usetype != EU_ITEM || e.type != WEAPON || proj.owner == d) break;
                                 ent = proj.id;
                                 break;
                             }
@@ -1357,17 +1243,13 @@ namespace ai
                         if(entities::ents.inrange(ent))
                         {
                             extentity &e = *entities::ents[ent];
-                            int attr = e.type == WEAPON ? w_attr(game::gamemode, game::mutators, e.attrs[0], sweap) : e.attrs[0];
-                            if(d->canuse(e.type, attr, e.attrs, sweap, lastmillis, G(weaponinterrupts))) switch(e.type)
+                            int attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
+                            if(d->canuse(e.type, attr, e.attrs, sweap, lastmillis, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
                             {
-                                case WEAPON:
-                                {
-                                    if(!wantsweap(d, attr)) break;
-                                    d->action[AC_USE] = true;
-                                    d->ai->lastaction = d->actiontime[AC_USE] = lastmillis;
-                                    return true;
-                                }
-                                default: break;
+                                if(!wantsweap(d, attr)) break;
+                                d->action[AC_USE] = true;
+                                d->ai->lastaction = d->actiontime[AC_USE] = lastmillis;
+                                return true;
                             }
                         }
                         actitems.pop();
@@ -1376,39 +1258,41 @@ namespace ai
             }
         }
 
-        bool timepassed = d->weapstate[d->weapselect] == W_S_IDLE && (!d->ammo[d->weapselect] || lastmillis-d->weaplast[d->weapselect] >= max(6000-(d->skill*50), weaponswitchdelay));
-        if(busy <= 2 && haswaited && timepassed)
+        bool timepassed = d->weapstate[d->weapselect] == W_S_IDLE && (d->ammo[d->weapselect] <= 0 || lastmillis-d->weaplast[d->weapselect] >= max(6000-(d->skill*50), weaponswitchdelay));
+
+        if(!firing && (!occupied || d->ammo[d->weapselect] <= 0) && timepassed && d->hasweap(d->weapselect, sweap) && weapons::weapreload(d, d->weapselect))
         {
-            int weap = d->ai->weappref;
-            gameent *e = game::getclient(d->ai->enemy);
-            if(!isweap(weap) || !d->hasweap(weap, sweap) || !hasrange(d, e, weap))
-            {
-                loopirev(W_MAX) if(i >= W_MELEE && d->hasweap(i, sweap) && hasrange(d, e, i)) { weap = i; break; }
-            }
-            if(isweap(weap) && weap != d->weapselect && weapons::weapselect(d, weap, G(weaponinterrupts)))
-            {
-                d->ai->lastaction = lastmillis;
-                return true;
-            }
+            d->ai->lastaction = lastmillis;
+            return true;
         }
 
-        if(d->hasweap(d->weapselect, sweap) && busy <= (!d->ammo[d->weapselect] ? 2 : 0) && timepassed)
+        if(!firing && timepassed)
         {
-            if(weapons::weapreload(d, d->weapselect))
+            int weap = weappref(d);
+            gameent *e = game::getclient(d->ai->enemy);
+            if(!isweap(weap) || !d->hasweap(weap, sweap) || (e && !hasrange(d, e, weap)))
+            {
+                loopirev(W_MAX) if(i >= W_MELEE && d->hasweap(i, sweap) && (!e || hasrange(d, e, i)))
+                {
+                    weap = i;
+                    break;
+                }
+            }
+            if(isweap(weap) && weap != d->weapselect && weapons::weapselect(d, weap, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
             {
                 d->ai->lastaction = lastmillis;
                 return true;
             }
         }
 
-        return busy >= 1;
+        return occupied;
     }
 
     bool transport(gameent *d, int find = 0)
     {
         vec pos = d->feetpos();
         static vector<int> candidates; candidates.setsize(0);
-        if(find) findwaypointswithin(pos, WAYPOINTRADIUS, (physics::jetpack(d) ? JETDIST : RETRYDIST)*find, candidates);
+        if(find) findwaypointswithin(pos, WAYPOINTRADIUS, RETRYDIST*find, candidates);
         if(find ? !candidates.empty() : !d->ai->route.empty()) loopk(2)
         {
             int best = -1;
@@ -1440,18 +1324,23 @@ namespace ai
 
     void timeouts(gameent *d, aistate &b)
     {
-        if(d->blocked)
+        if(d->blocked || (d->inmaterial&MATF_CLIP) == MAT_AICLIP)
         {
             d->ai->blocktime += lastmillis-d->ai->lastrun;
-            if(d->ai->blocktime > (d->ai->blockseq+1)*1000)
+            if(d->ai->blocktime > (d->ai->blockseq+1)*500)
             {
                 d->ai->blockseq++;
                 switch(d->ai->blockseq)
                 {
-                    case 1: case 2:
-                        d->ai->clear(d->ai->blockseq!=1);
-                        if(iswaypoint(d->ai->targnode) && !d->ai->hasprevnode(d->ai->targnode))
-                            d->ai->addprevnode(d->ai->targnode);
+                    case 1: break;
+                    case 2:
+                        d->ai->clear(d->ai->blockseq != 1);
+                        if(d->ai->blockseq != 1 && iswaypoint(d->ai->targnode))
+                        {
+                            if(!d->ai->hasprevnode(d->ai->targnode)) d->ai->addprevnode(d->ai->targnode);
+                            waypoints[d->ai->targnode].weight = -1;
+                            wpavoid.avoidnear(NULL, waypoints[d->ai->targnode].o.z + WAYPOINTRADIUS, waypoints[d->ai->targnode].o, WAYPOINTRADIUS);
+                        }
                         break;
                     case 3: if(!transport(d)) d->ai->reset(false); break;
                     case 4: default:
@@ -1459,7 +1348,7 @@ namespace ai
                         else d->ai->blockseq = 0; // waiting, so just try again..
                         break;
                 }
-                if(aidebug >= 6 && dbgfocus(d))
+                if(aidebug >= 7 && dbgfocus(d))
                     conoutf("%s blocked %dms sequence %d", game::colourname(d), d->ai->blocktime, d->ai->blockseq);
             }
         }
@@ -1468,13 +1357,14 @@ namespace ai
         if(iswaypoint(d->ai->targnode) && (d->ai->targnode == d->ai->targlast || d->ai->hasprevnode(d->ai->targnode)))
         {
             d->ai->targtime += lastmillis-d->ai->lastrun;
-            if(d->ai->targtime > (d->ai->targseq+1)*1000)
+            if(d->ai->targtime > (d->ai->targseq+1)*500)
             {
                 d->ai->targseq++;
                 switch(d->ai->targseq)
                 {
-                    case 1: case 2:
-                        d->ai->clear(d->ai->targseq!=1);
+                    case 1: break;
+                    case 2:
+                        d->ai->clear(d->ai->targseq != 1);
                         if(iswaypoint(d->ai->targnode) && !d->ai->hasprevnode(d->ai->targnode))
                             d->ai->addprevnode(d->ai->targnode);
                         break;
@@ -1484,7 +1374,7 @@ namespace ai
                         else d->ai->blockseq = 0; // waiting, so just try again..
                         break;
                 }
-                if(aidebug >= 6 && dbgfocus(d))
+                if(aidebug >= 7 && dbgfocus(d))
                     conoutf("%s targeted %d too long %dms sequence %d", game::colourname(d), d->ai->targnode, d->ai->targtime, d->ai->targseq);
             }
         }
@@ -1515,12 +1405,12 @@ namespace ai
         else
         {
             if(d->ragdoll) cleanragdoll(d);
-            if(d->state == CS_ALIVE && !game::intermission)
+            if(d->state == CS_ALIVE && gs_playing(game::gamestate))
             {
                 if(d->speedscale != 0)
                 {
                     physics::move(d, 1, true);
-                    if(aistyle[d->aitype].canmove && !b.idle) timeouts(d, b);
+                    if(actor[d->actortype].canmove && !d->ai->dontmove) timeouts(d, b);
                 }
                 else
                 {
@@ -1529,21 +1419,21 @@ namespace ai
                 }
             }
         }
-        if(!game::intermission && (d->state == CS_ALIVE || d->state == CS_DEAD || d->state == CS_WAITING))
+        if(gs_playing(game::gamestate) && (d->state == CS_ALIVE || d->state == CS_DEAD || d->state == CS_WAITING))
             entities::checkitems(d);
     }
 
     void avoid()
     {
-        float guessradius = max(aistyle[AI_NONE].xradius, aistyle[AI_NONE].yradius);
+        float guessradius = max(actor[A_PLAYER].xradius, actor[A_PLAYER].yradius);
         obstacles.clear();
         int numdyns = game::numdynents();
         loopi(numdyns)
         {
             gameent *d = (gameent *)game::iterdynents(i);
-            if(!d) continue; // || d->aitype >= AI_START) continue;
+            if(!d) continue; // || d->actortype >= A_ENEMY) continue;
             if(d->state != CS_ALIVE || !physics::issolid(d)) continue;
-            obstacles.avoidnear(d, d->o.z + d->aboveeye + 1, d->feetpos(), guessradius + d->radius);
+            obstacles.avoidnear(d, d->o.z + d->aboveeye + 1, d->feetpos(), guessradius + d->radius + 1);
         }
         obstacles.add(wpavoid);
         loopv(projs::projs)
@@ -1552,20 +1442,33 @@ namespace ai
             if(p && p->state == CS_ALIVE && p->projtype == PRJ_SHOT)
             {
                 float expl = WX(WK(p->flags), p->weap, explode, WS(p->flags), game::gamemode, game::mutators, p->curscale);
-                if(expl > 0) obstacles.avoidnear(p, p->o.z + expl, p->o, guessradius + expl);
+                if(expl > 0) obstacles.avoidnear(p, p->o.z + expl, p->o, guessradius + expl + 1);
             }
         }
-        loopi(entities::lastenttype[MAPMODEL]) if(entities::ents[i]->type == MAPMODEL && !entities::ents[i]->links.empty() && !entities::ents[i]->spawned)
+        loopi(entities::lastent(MAPMODEL)) if(entities::ents[i]->type == MAPMODEL && !entities::ents[i]->spawned())
         {
-            mapmodelinfo *mmi = getmminfo(entities::ents[i]->attrs[0]);
-            if(!mmi) continue;
+            gameentity &e = *(gameentity *)entities::ents[i];
+            bool skip = false;
+            loopvk(e.links) if(entities::ents.inrange(e.links[k]))
+            {
+                gameentity &f = *(gameentity *)entities::ents[e.links[k]];
+                if(f.type != TRIGGER || !m_check(f.attrs[5], f.attrs[6], game::gamemode, game::mutators)) continue;
+                if(f.attrs[2] != TA_AUTO || (f.attrs[1] != TR_TOGGLE && f.attrs[1] != TR_LINK)) continue;
+                skip = true;
+                break;
+            }
+            if(skip) continue;
+            mapmodelinfo *mmi = getmminfo(e.attrs[0]);
+            if(!mmi || !mmi->m) continue;
             vec center, radius;
-            mmi->m->collisionbox(0, center, radius);
-            if(entities::ents[i]->attrs[5]) { center.mul(entities::ents[i]->attrs[5]/100.f); radius.mul(entities::ents[i]->attrs[5]/100.f); }
-            if(!mmi->m->ellipsecollide) rotatebb(center, radius, int(entities::ents[i]->attrs[1]), int(entities::ents[i]->attrs[2]));
-            float limit = WAYPOINTRADIUS+(max(radius.x, max(radius.y, radius.z))*mmi->m->height);
-            vec pos = entities::ents[i]->o; pos.z += limit*0.5f;
-            obstacles.avoidnear(NULL, pos.z + limit*0.5f, pos, limit);
+            mmi->m->collisionbox(center, radius);
+            if(e.attrs[5])
+            {
+                center.mul(e.attrs[5]/100.f);
+                radius.mul(e.attrs[5]/100.f);
+            }
+            if(!mmi->m->ellipsecollide) rotatebb(center, radius, int(e.attrs[1]), int(e.attrs[2]));
+            obstacles.avoidnear(NULL, e.o.z + radius.z, e.o, max(radius.x, max(radius.y, radius.z)) + WAYPOINTRADIUS);
         }
     }
 
@@ -1579,6 +1482,11 @@ namespace ai
         loopvrev(d->ai->state)
         {
             aistate &c = d->ai->state[i];
+            if(c.owner >= 0)
+            {
+                gameent *e = game::getclient(c.owner);
+                if(!e || e->team != d->team) c.owner = -1;
+            }
             if(cleannext)
             {
                 c.millis = lastmillis;
@@ -1587,23 +1495,16 @@ namespace ai
             }
             if(d->state == CS_DEAD)
             {
-                if(d->respawned < 0 && (!d->lastdeath || lastmillis-d->lastdeath > (d->aitype == AI_BOT ? 500 : enemyspawntime)))
+                if(d->respawned < 0 && (!d->lastdeath || lastmillis-d->lastdeath > (d->actortype == A_BOT ? 500 : enemyspawntime)))
                 {
-                    if(d->aitype == AI_BOT && m_loadout(game::gamemode, game::mutators))
-                    {
-                        d->loadweap.shrink(0);
-                        d->loadweap.add(d->ai->weappref);
-                        if(maxcarry > 1) loopj(maxcarry-1) d->loadweap.add(0);
-                        client::addmsg(N_LOADW, "ri3v", d->clientnum, 1, d->loadweap.length(), d->loadweap.length(), d->loadweap.getbuf());
-                    }
                     client::addmsg(N_TRYSPAWN, "ri", d->clientnum);
                     d->respawned = lastmillis;
                 }
             }
             else if(d->state == CS_ALIVE && run)
             {
-                int result = 0;
-                c.idle = 0;
+                bool result = false;
+                c.acttype = m_insta(game::gamemode, game::mutators) ? AI_A_HASTE : AI_A_NORMAL;
                 switch(c.type)
                 {
                     case AI_S_WAIT: result = dowait(d, c); break;
@@ -1612,14 +1513,11 @@ namespace ai
                     case AI_S_INTEREST: result = dointerest(d, c); break;
                     default: result = 0; break;
                 }
-                if(result <= 0 && c.type != AI_S_WAIT)
+                if(!result && c.type != AI_S_WAIT)
                 {
-                    switch(result)
-                    {
-                        case 0: default: d->ai->removestate(i); cleannext = true; break;
-                        case -1: i = d->ai->state.length()-1; break;
-                    }
-                    continue; // shouldn't interfere
+                    d->ai->removestate(i);
+                    cleannext = true;
+                    continue; // logic is run on working states
                 }
             }
             logic(d, c);
@@ -1665,6 +1563,8 @@ namespace ai
         "wait", "defend", "pursue", "interest"
     }, *sttypes[AI_T_MAX+1] = {
         "none", "node", "actor", "affinity", "entity", "drop"
+    }, *attypes[AI_A_MAX] = {
+        "normal", "idle", "lockon", "protect", "haste"
     };
     void render()
     {
@@ -1678,7 +1578,7 @@ namespace ai
                 vec pos = d->abovehead();
                 pos.z += 3;
                 alive++;
-                if(aidebug >= 4 && aistyle[d->aitype].canmove) drawroute(d, 4.f*(float(alive)/float(total)));
+                if(aidebug >= 4 && actor[d->actortype].canmove) drawroute(d, 4.f*(float(alive)/float(total)));
                 if(aidebug >= 3)
                 {
                     defformatstring(q)("node: %d route: %d (%d)",
@@ -1693,11 +1593,14 @@ namespace ai
                 loopvrev(d->ai->state)
                 {
                     aistate &b = d->ai->state[i];
-                    defformatstring(s)("%s%s (%s) %s:%d",
-                        top ? "<default>\fg" : "<sub>\fy",
+                    gameent *e = b.owner >= 0 ? game::getclient(b.owner) : NULL;
+                    defformatstring(s)("%s%s (%s) %s:%d (\fs%s%s\fS%s%s%s)",
+                        top ? "<default>\fg" : "<sub>\fa",
                         stnames[b.type],
                         timestr(lastmillis-b.millis),
-                        sttypes[b.targtype+1], b.target
+                        sttypes[b.targtype+1], b.target,
+                        top ? "\fc" : "\fw", attypes[b.acttype],
+                        e ? " [" : "", e ? game::colourname(e) : "", e ? "]" : ""
                     );
                     part_textcopy(pos, s);
                     pos.z += 2;
@@ -1709,9 +1612,9 @@ namespace ai
                 }
                 if(aidebug >= 3)
                 {
-                    if(isweap(d->ai->weappref))
+                    if(isweap(weappref(d)))
                     {
-                        part_textcopy(pos, W(d->ai->weappref, name));
+                        part_textcopy(pos, W(weappref(d), name));
                         pos.z += 2;
                     }
                     gameent *e = game::getclient(d->ai->enemy);
@@ -1733,7 +1636,7 @@ namespace ai
                     {
                         int ent = obstacles.waypoints[cur];
                         if(iswaypoint(ent))
-                            part_create(PART_EDIT, 1, waypoints[ent].o, 0xFF6600, 1.5f);
+                            part_create(PART_EDIT, 1, waypoints[ent].o, 0xFF6600, 2);
                     }
                     cur = next;
                 }
@@ -1742,7 +1645,7 @@ namespace ai
         if(showwaypoints || (dropwaypoints && showwaypointsdrop) || aidebug >= 7)
         {
             vector<int> close;
-            int len = waypoints.length();
+            int len = waypoints.length(), col = vec::hexcolor(showwaypointscolour).mul(0.5f).tohexcolor();
             if(showwaypointsradius)
             {
                 findwaypointswithin(camera1->o, 0, showwaypointsradius, close);
@@ -1752,7 +1655,7 @@ namespace ai
             {
                 int idx = showwaypointsradius ? close[i] : i;
                 waypoint &w = waypoints[idx];
-                if(!w.haslinks()) part_create(PART_EDIT, 1, w.o, 0xFFFF00, 1.f);
+                if(!w.haslinks()) part_create(PART_EDIT, 1, w.o, showwaypointscolour, 1.f);
                 else loopj(MAXWAYPOINTLINKS)
                 {
                      int link = w.links[j];
@@ -1760,7 +1663,7 @@ namespace ai
                      waypoint &v = waypoints[link];
                      bool both = false;
                      loopk(MAXWAYPOINTLINKS) if(v.links[k] == idx) { both = true; break; }
-                     part_trace(w.o, v.o, 1, 1, 1, both ? 0xAA44CC : 0x660088);
+                     part_trace(w.o, v.o, 1, 1, 1, both ? showwaypointscolour : col);
                 }
             }
             if(game::player1->state == CS_ALIVE && iswaypoint(game::player1->lastnode))
@@ -1770,6 +1673,213 @@ namespace ai
 
     void preload()
     {
-        loopi(AI_TOTAL) loopk(3) preloadmodel(aistyle[i+AI_START].playermodel[1]);
+        loopi(A_TOTAL) loopk(3) preloadmodel(actor[i+A_ENEMY].playermodel[1]);
+    }
+
+    void botsay(gameent *d, gameent *t, const char *fmt, ...)
+    {
+        if(!d || !t) return;
+        defvformatbigstring(msg, fmt, fmt);
+        client::addmsg(N_TEXT, "ri3s", d->clientnum, t->clientnum, SAY_WHISPER, msg);
+    }
+
+    void scanchat(gameent *d, gameent *t, int flags, const char *text)
+    {
+        if((!m_edit(game::gamemode) && !m_team(game::gamemode, game::mutators)) || flags&SAY_ACTION || d->actortype != A_PLAYER) return;
+        bigstring msg;
+        filterbigstring(msg, text, true, true, true, true);
+        const int MAXWORDS = 8;
+        int numargs = MAXWORDS;
+        char *w[MAXWORDS];
+        const char *p = (const char *)msg;
+        loopi(MAXWORDS)
+        {
+            w[i] = (char *)"";
+            if(i > numargs) continue;
+            char *s = parsetext(p);
+            if(s) w[i] = s;
+            else numargs = i;
+        }
+        if(*w[0]) loopvj(game::players) if(game::players[j] && game::players[j]->actortype == A_BOT && game::players[j]->ai)
+        {
+            gameent *e = game::players[j];
+            if(!m_edit(game::gamemode) && d->team != e->team) continue;
+            int pos = 0;
+            if(!(flags&SAY_WHISPER))
+            {
+                if(!strncmp(w[0], "bots", 4)) pos = 1;
+                else
+                {
+                    size_t len = strlen(e->name);
+                    if(!len || strncasecmp(w[0], e->name, len)) continue;
+                    switch(w[0][len])
+                    {
+                        case 0: break;
+                        case ':': case ',': case ';': len++; break;
+                        default: continue;
+                    }
+                    if(w[0][len] != 0) continue;
+                    pos = 1;
+                }
+            }
+            const char *affirm[4] = { "roger", "okay", "will do", "i'm on it" };
+            if(!strcasecmp(w[pos], "defend"))
+            {
+                pos++;
+                if(!strcasecmp(w[pos], "me"))
+                {
+                    e->ai->clear();
+                    e->ai->addstate(AI_S_DEFEND, AI_T_ACTOR, d->clientnum, AI_A_PROTECT, d->clientnum);
+                    botsay(e, d, "%s, defending you", affirm[rnd(4)]);
+                }
+                else if(!strcasecmp(w[pos], "here"))
+                {
+                    e->ai->clear();
+                    e->ai->addstate(AI_S_DEFEND, AI_T_NODE, e->lastnode, AI_A_PROTECT, d->clientnum);
+                    botsay(e, d, "%s, defending your position", affirm[rnd(4)]);
+                }
+                else
+                {
+                    bool defend = false;
+                    gameent *f = NULL;
+                    int numdyns = game::numdynents();
+                    loopi(numdyns) if((f = (gameent *)game::iterdynents(i)) && f != e && f->team == e->team && !strcmp(w[pos], f->name))
+                    {
+                        e->ai->clear();
+                        e->ai->addstate(AI_S_DEFEND, AI_T_ACTOR, f->clientnum, AI_A_PROTECT, d->clientnum);
+                        botsay(e, d, "%s, defending %s", affirm[rnd(4)], f->name);
+                        defend = true;
+                        break;
+                    }
+                    if(!defend)
+                    {
+                        if(!strcasecmp(w[pos], "the")) pos++;
+                        switch(game::gamemode)
+                        {
+                            case G_CAPTURE:
+                            {
+                                if(!strcasecmp(w[pos], "flag"))
+                                {
+                                    loopv(capture::st.flags) if(capture::st.flags[i].team == e->team)
+                                    {
+                                        e->ai->clear();
+                                        e->ai->addstate(AI_S_DEFEND, AI_T_AFFINITY, i, AI_A_PROTECT, d->clientnum);
+                                        botsay(e, d, "%s, defending the flag", affirm[rnd(4)]);
+                                        break;
+                                    }
+                                }
+                                #if 0
+                                else if(!strcasecmp(w[pos], "base"))
+                                {
+                                    loopv(capture::st.flags) if(capture::st.flags[i].team == e->team)
+                                    {
+                                        e->ai->clear();
+                                        e->ai->addstate(AI_S_DEFEND, AI_T_ENTITY, capture::st.flags[i].ent, AI_A_PROTECT, d->clientnum);
+                                        botsay(e, d, "%s, defending base for the flag", affirm[rnd(4)]);
+                                        break;
+                                    }
+                                }
+                                #endif
+                                else botsay(e, d, "'me', 'here', or 'flag'");
+                                break;
+                            }
+                            case G_BOMBER:
+                            {
+                                if(!strcasecmp(w[pos], "goal") || !strcasecmp(w[pos], "base"))
+                                {
+                                    loopv(bomber::st.flags) if(!isbomberaffinity(bomber::st.flags[i]) && bomber::st.flags[i].team == e->team)
+                                    {
+                                        e->ai->clear();
+                                        e->ai->addstate(AI_S_DEFEND, AI_T_AFFINITY, i, AI_A_PROTECT, d->clientnum);
+                                        botsay(e, d, "%s, defending the goal", affirm[rnd(4)]);
+                                        break;
+                                    }
+                                }
+                                else botsay(e, d, "'me', 'here', or 'goal'");
+                                break;
+                            }
+                            default: botsay(e, d, "'me', or 'here'"); break;
+                        }
+                    }
+                }
+            }
+            else if(!strcasecmp(w[pos], "attack"))
+            {
+                pos++;
+                if(!strcasecmp(w[pos], "the")) pos++;
+                switch(game::gamemode)
+                {
+                    case G_CAPTURE:
+                    {
+                        if(!strcasecmp(w[pos], "flag"))
+                        {
+                            loopv(capture::st.flags) if(capture::st.flags[i].team != e->team)
+                            {
+                                e->ai->clear();
+                                e->ai->addstate(AI_S_PURSUE, AI_T_AFFINITY, i, AI_A_HASTE, d->clientnum);
+                                botsay(e, d, "%s, attacking the flag", affirm[rnd(4)]);
+                                break;
+                            }
+                        }
+                        #if 0
+                        else if(!strcasecmp(w[pos], "base"))
+                        {
+                            loopv(capture::st.flags) if(capture::st.flags[i].team != e->team)
+                            {
+                                e->ai->clear();
+                                e->ai->addstate(AI_S_DEFEND, AI_T_ENTITY, capture::st.flags[i].ent, AI_A_HASTE, d->clientnum);
+                                botsay(e, d, "%s, attacking the flag", affirm[rnd(4)]);
+                                break;
+                            }
+                        }
+                        #endif
+                        else botsay(e, d, "'flag' is the only option");
+
+                        break;
+                    }
+                    case G_BOMBER:
+                    {
+                        if(!strcasecmp(w[pos], "goal") || !strcasecmp(w[pos], "base"))
+                        {
+                            loopv(bomber::st.flags) if(!isbomberaffinity(bomber::st.flags[i]) && bomber::st.flags[i].team != e->team)
+                            {
+                                e->ai->clear();
+                                e->ai->addstate(AI_S_PURSUE, AI_T_AFFINITY, i, AI_A_HASTE, d->clientnum);
+                                botsay(e, d, "%s, attacking the goal", affirm[rnd(4)]);
+                                break;
+                            }
+                        }
+                        else if(!strcasecmp(w[pos], "ball"))
+                        {
+                            loopv(bomber::st.flags) if(isbomberaffinity(bomber::st.flags[i]))
+                            {
+                                e->ai->clear();
+                                e->ai->addstate(AI_S_PURSUE, AI_T_AFFINITY, i, AI_A_HASTE, d->clientnum);
+                                botsay(e, d, "%s, attacking the ball", affirm[rnd(4)]);
+                                break;
+                            }
+                        }
+                        else botsay(e, d, "'goal', or 'ball'");
+                        break;
+                    }
+                    default: botsay(e, d, "sorry, no attack directions in this game"); break;
+                }
+            }
+            else if(!strcasecmp(w[pos], "forget"))
+            {
+                loopvrev(e->ai->state) if(e->ai->state[i].owner == d->clientnum) e->ai->state.remove(i);
+                const char *quip[4] = { "back to what i was doing then", "resuming previous operations", "i am no longer your slave", "jolly good show then" };
+                botsay(e, d, "%s, %s", affirm[rnd(4)], quip[rnd(4)]);
+            }
+            else if(!strcasecmp(w[pos], "reset"))
+            {
+                e->ai->reset(true, false);
+                const char *quip[4] = { "what was i doing again?", "duh... off i go..", "who were you again?", "ummmm... wtf do i do now?" };
+                botsay(e, d, "%s, %s", affirm[rnd(4)], quip[rnd(4)]);
+            }
+            else botsay(e, d, "'defend', 'attack', 'forget', or 'reset'");
+        }
+        loopi(numargs) DELETEA(w[i]);
     }
+
 }
diff --git a/src/game/ai.h b/src/game/ai.h
index f7f3dee..88d6fc3 100644
--- a/src/game/ai.h
+++ b/src/game/ai.h
@@ -1,49 +1,5 @@
-struct aistyles
-{
-    int type,           weap,           health;
-    float   xradius,    yradius,    height,     weight,     speed,      scale;
-    bool    canmove,    canstrafe,  canjump,    cancrouch,  useweap,    living,     hitbox;
-    const char  *name,      *playermodel[4];
-};
-
-enum { AI_NONE = 0, AI_BOT, AI_TURRET, AI_GRUNT, AI_DRONE, AI_MAX, AI_START = AI_TURRET, AI_TOTAL = AI_MAX-AI_START };
-#ifdef GAMESERVER
-aistyles aistyle[] = {
-    {
-        AI_NONE,         -1,             0,
-            3,          3,          14,         200,        50,         1,
-            true,       true,       true,       true,       true,       true,       true,
-                "player",   { "actors/player/male/hwep",      "actors/player/male",     "actors/player/male/body",      "actors/player/male/headless" }
-    },
-    {
-        AI_BOT,         -1,             0,
-            3,          3,          14,         200,        50,         1,
-            true,       true,       true,       true,       true,       true,       true,
-                "bot",      { "actors/player/male/hwep",      "actors/player/male",     "actors/player/male/body",      "actors/player/male/headless" }
-    },
-    {
-        AI_TURRET,      W_SMG,       100,
-            4.75,       4.75,       8.75,       150,        1,          1,
-            false,      false,      false,      false,      false,      false,      false,
-                "turret",   { "actors/player/male/hwep",      "actors/turret",          "actors/player/male/body",      "actors/turret" }
-    },
-    {
-        AI_GRUNT,       W_PISTOL,   50,
-            3,          3,          14,         200,        50,         1,
-            true,       true,       true,       true,       true,       true,       true,
-                "grunt",   { "actors/player/male/hwep",      "actors/player/male",      "actors/player/male/body",      "actors/player/male/headless" }
-    },
-    {
-        AI_DRONE,       W_MELEE,     50,
-            3,          3,          14,         150,        40,         1,
-            true,       true,       true,       true,       true,       true,       true,
-                "drone",    { "actors/player/male/hwep",      "actors/drone",           "actors/player/male/body",      "actors/drone" }
-    },
-};
-#endif
-#ifndef GAMESERVER
 struct gameent;
-extern aistyles aistyle[];
+extern actors actor[];
 
 namespace ai
 {
@@ -54,7 +10,6 @@ namespace ai
     const float MINWPDIST       = 4.f;     // is on top of
     const float CLOSEDIST       = 32.f;    // is close
     const float RETRYDIST       = 64.f;    // is close when retrying
-    const float JETDIST         = 128.f;   // close when jetting
     const float FARDIST         = 256.f;   // too far to remap close
     const float JUMPMIN         = 2.f;     // decides to jump
     const float JUMPMAX         = 32.f;    // max jump
@@ -102,9 +57,10 @@ namespace ai
     extern vector<oldwaypoint> oldwaypoints;
 
     extern int showwaypoints, dropwaypoints;
-    extern int closestwaypoint(const vec &pos, float mindist, bool links);
+    extern int closestwaypoint(const vec &pos, float mindist = CLOSEDIST, bool links = true);
     extern void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results);
 	extern void inferwaypoints(gameent *d, const vec &o, const vec &v, float mindist = ai::CLOSEDIST);
+	extern void delwaypoint(int n = -1);
 
     struct avoidset
     {
@@ -208,21 +164,31 @@ namespace ai
         AI_T_MAX
     };
 
+    enum
+    {
+        AI_A_NORMAL = 0,
+        AI_A_IDLE,
+        AI_A_LOCKON,
+        AI_A_PROTECT,
+        AI_A_HASTE,
+        AI_A_MAX
+    };
+
     struct interest
     {
-        int state, node, target, targtype;
+        int state, node, target, targtype, acttype;
         float score, tolerance;
         bool team;
-        interest() : state(-1), node(-1), target(-1), targtype(-1), score(0.f), tolerance(1.f), team(false) {}
+        interest() : state(-1), node(-1), target(-1), targtype(-1), acttype(AI_A_NORMAL), score(0.f), tolerance(1.f), team(false) {}
         ~interest() {}
     };
 
     struct aistate
     {
-        int type, millis, targtype, target, idle;
+        int type, millis, targtype, target, acttype, owner;
         bool override;
 
-        aistate(int m, int t, int r = -1, int v = -1) : type(t), millis(m), targtype(r), target(v)
+        aistate(int m, int t, int r = -1, int v = -1, int a = AI_A_NORMAL, int o = -1) : type(t), millis(m), targtype(r), target(v), acttype(a), owner(o)
         {
             reset();
         }
@@ -230,7 +196,7 @@ namespace ai
 
         void reset()
         {
-            idle = 0;
+            acttype = AI_A_NORMAL;
             override = false;
         }
     };
@@ -241,10 +207,10 @@ namespace ai
     {
         vector<aistate> state;
         vector<int> route;
-        vec target, spot;
-        int weappref, enemy, enemyseen, enemymillis, prevnodes[NUMPREVNODES], targnode, targlast, targtime, targseq,
+        vec target, spot, views, aimrnd;
+        int enemy, enemyseen, enemymillis, prevnodes[NUMPREVNODES], targnode, targlast, targtime, targseq,
             lastrun, lastaction, lastcheck, jumpseed, jumprand, blocktime, blockseq, lastaimrnd, lastmelee, lastturn;
-        float targyaw, targpitch, views[3], aimrnd[3];
+        float targyaw, targpitch;
         bool dontmove, tryreset;
 
         aiinfo()
@@ -261,7 +227,7 @@ namespace ai
             lastaction = lastcheck = enemyseen = enemymillis = blocktime = blockseq = targtime = targseq = lastaimrnd = lastmelee = lastturn = 0;
             lastrun = jumpseed = lastmillis;
             jumprand = lastmillis+5000;
-            weappref = targnode = targlast = enemy = -1;
+            targnode = targlast = enemy = -1;
             targyaw = targpitch = 0;
         }
 
@@ -272,11 +238,14 @@ namespace ai
             lastcheck = 0;
         }
 
-        void reset(bool tryit = false)
+        void reset(bool tryit = false, bool keepowner = true)
         {
             clear(tryit);
+            vector<aistate> keep;
+            if(keepowner) loopv(state) if(state[i].owner >= 0) keep.add(state[i]);
             state.setsize(0);
             addstate(AI_S_WAIT);
+            if(keepowner) loopv(keep) state.add(keep[i]);
             tryreset = false;
         }
 
@@ -296,9 +265,9 @@ namespace ai
             }
         }
 
-        aistate &addstate(int t, int r = -1, int v = -1)
+        aistate &addstate(int t, int r = -1, int v = -1, int a = AI_A_NORMAL, int o = -1)
         {
-            return state.add(aistate(lastmillis, t, r, v));
+            return state.add(aistate(lastmillis, t, r, v, a, o));
         }
 
         void removestate(int index = -1)
@@ -314,16 +283,18 @@ namespace ai
             return state.last();
         }
 
-        aistate &switchstate(aistate &b, int t, int r = -1, int v = -1)
+        aistate &switchstate(aistate &b, int t, int r = -1, int v = -1, int a = AI_A_NORMAL, int o = -1)
         {
-            if((b.type == t && b.targtype == r) || (b.type == AI_S_INTEREST && b.targtype == AI_T_NODE))
+            if(((b.type == t && b.targtype == r) || (b.type == AI_S_INTEREST && b.targtype == AI_T_NODE)) && b.owner == o)
             {
                 b.millis = lastmillis;
                 b.target = v;
+                b.acttype = a;
+                b.owner = o;
                 b.reset();
                 return b;
             }
-            return addstate(t, r, v);
+            return addstate(t, r, v, a);
         }
     };
 
@@ -340,9 +311,9 @@ namespace ai
     extern bool targetable(gameent *d, gameent *e, bool solid = false);
     extern bool cansee(gameent *d, vec &x, vec &y, bool force = false, vec &targ = aitarget);
     extern bool altfire(gameent *d, gameent *e);
-    extern int owner(gameent *d);
+    extern int weappref(gameent *d);
 
-    extern void init(gameent *d, int at, int et, int on, int sk, int bn, char *name, int tm, int cl, int md, const char *vn = "");
+    extern void init(gameent *d, int at, int et, int on, int sk, int bn, char *name, int tm, int cl, int md, const char *vn, vector<int> &lweaps);
 
     extern bool badhealth(gameent *d);
     extern int checkothers(vector<int> &targets, gameent *d = NULL, int state = -1, int targtype = -1, int target = -1, bool teams = false, int *members = NULL);
@@ -351,8 +322,8 @@ namespace ai
     extern bool randomnode(gameent *d, aistate &b, const vec &pos, float guard = ALERTMIN, float wander = ALERTMAX);
     extern bool randomnode(gameent *d, aistate &b, float guard = ALERTMIN, float wander = ALERTMAX);
     extern bool violence(gameent *d, aistate &b, gameent *e, int pursue = 0);
-    extern bool patrol(gameent *d, aistate &b, const vec &pos, float guard = ALERTMIN, float wander = ALERTMAX, int walk = 1, bool retry = false);
-    extern bool defense(gameent *d, aistate &b, const vec &pos, float guard = ALERTMIN, float wander = ALERTMAX, int walk = 0);
+    extern bool patrol(gameent *d, aistate &b, const vec &pos, float guard = CLOSEDIST, float wander = FARDIST, int walk = 1, bool retry = false);
+    extern bool defense(gameent *d, aistate &b, const vec &pos, float guard = CLOSEDIST, float wander = FARDIST, int walk = 0);
 
     extern void respawned(gameent *d, bool local, int ent = -1);
     extern void damaged(gameent *d, gameent *e, int weap, int flags, int damage);
@@ -363,5 +334,6 @@ namespace ai
     extern void think(gameent *d, bool run);
     extern void render();
     extern void preload();
+
+    extern void scanchat(gameent *d, gameent *t, int flags, const char *text);
 };
-#endif
diff --git a/src/game/aiman.h b/src/game/aiman.h
index 8029c8b..552531b 100644
--- a/src/game/aiman.h
+++ b/src/game/aiman.h
@@ -1,7 +1,7 @@
 // server-side ai manager
 namespace aiman
 {
-    int oldbotskillmin = -1, oldbotskillmax = -1, oldcoopskillmin = -1, oldcoopskillmax = -1, oldenemyskillmin = -1, oldenemyskillmax = -1,
+    int dorefresh = 0, oldbotskillmin = -1, oldbotskillmax = -1, oldcoopskillmin = -1, oldcoopskillmax = -1, oldenemyskillmin = -1, oldenemyskillmax = -1,
         oldbotbalance = -2, oldnumplayers = -1, oldbotlimit = -1, oldbotoffset = 0, oldenemylimit = -1;
     float oldcoopbalance = -1, oldcoopmultibalance = -1;
 
@@ -11,26 +11,61 @@ namespace aiman
         loopv(clients)
         {
             clientinfo *ci = clients[i];
-            if(ci->clientnum < 0 || ci->state.aitype > AI_NONE || !ci->ready || ci == exclude) continue;
+            if(ci->clientnum < 0 || ci->state.actortype > A_PLAYER || !ci->ready || ci == exclude) continue;
             if(!least || ci->bots.length() < least->bots.length()) least = ci;
         }
         return least;
     }
 
-    void getskillrange(int type, int &m, int &n)
+    int getlimit(int type = A_BOT)
+    {
+        if(type >= A_ENEMY) return G(enemylimit);
+        if(m_coop(gamemode, mutators))
+        {
+            int people = numclients(-1, true, -1), numt = numteams(gamemode, mutators)-1;
+            return min(int(ceilf(people*numt*(m_multi(gamemode, mutators) ? G(coopmultibalance) : G(coopbalance)))), MAXAI);
+        }
+        return G(botlimit);
+    }
+
+    void getskillrange(int type, int &n, int &m, int frags = -1, int deaths = -1)
     {
         switch(type)
         {
-            case AI_BOT:
+            case A_BOT:
                 if(m_coop(gamemode, mutators))
                 {
                     m = max(G(coopskillmax), G(coopskillmin));
                     n = min(G(coopskillmin), m);
+                    if(deaths > 0 && G(coopskilldeaths) > 0)
+                    {
+                        int amt = G(coopskilldeaths)*deaths;
+                        m += amt;
+                        n += amt;
+                    }
+                    if(frags > 0 && G(coopskillfrags) > 0)
+                    {
+                        int amt = G(coopskillfrags)*frags;
+                        m -= amt;
+                        n -= amt;
+                    }
                 }
                 else
                 {
                     m = max(G(botskillmax), G(botskillmin));
                     n = min(G(botskillmin), m);
+                    if(deaths > 0 && G(botskilldeaths) > 0)
+                    {
+                        int amt = G(botskilldeaths)*deaths;
+                        m += amt;
+                        n += amt;
+                    }
+                    if(frags > 0 && G(botskillfrags) > 0)
+                    {
+                        int amt = G(botskillfrags)*frags;
+                        m -= amt;
+                        n -= amt;
+                    }
                 }
                 break;
             default:
@@ -38,59 +73,66 @@ namespace aiman
                 n = min(G(enemyskillmin), m);
                 break;
         }
+        m = min(m, 101);
+        n = min(n, m);
+    }
+
+    void setskill(clientinfo *ci)
+    {
+        int n = 1, m = 100;
+        getskillrange(ci->state.actortype, n, m, ci->state.frags, ci->state.deaths);
+        if(ci->state.skill > m || ci->state.skill < n)
+        { // needs re-skilling
+            ci->state.skill = (m != n ? rnd(m-n) + n + 1 : m);
+            if(!ci->state.aireinit) ci->state.aireinit = 1;
+        }
     }
 
     bool addai(int type, int ent, int skill)
     {
-        int numbots = 0, numenemies = 0;
-        loopv(clients)
+        int count = 0, limit = getlimit(type);
+        if(!limit) return false;
+        loopv(clients) if(clients[i]->state.actortype == type)
         {
-            if(type == AI_BOT && numbots >= G(botlimit)) return false;
-            if(type >= AI_START && numenemies >= G(enemylimit)) return false;
-            if(clients[i]->state.aitype == type)
-            {
-                clientinfo *ci = clients[i];
-                if(ci->state.ownernum < 0)
-                { // reuse a slot that was going to removed
-                    clientinfo *owner = findaiclient();
-                    if(!owner) return false;
-                    ci->state.ownernum = owner->clientnum;
-                    owner->bots.add(ci);
-                    ci->state.aireinit = 1;
-                    ci->state.aitype = type;
-                    ci->state.aientity = ent;
-                    return true;
-                }
-                if(type == AI_BOT) numbots++;
-                if(type >= AI_START) numenemies++;
+            clientinfo *ci = clients[i];
+            if(ci->state.ownernum < 0)
+            { // reuse a slot that was going to removed
+                clientinfo *owner = findaiclient();
+                if(!owner) return false;
+                ci->state.ownernum = owner->clientnum;
+                owner->bots.add(ci);
+                ci->state.aireinit = 1;
+                ci->state.actortype = type;
+                ci->state.spawnpoint = ent;
+                return true;
             }
+            if(++count >= limit) return false;
         }
-        if(type == AI_BOT && numbots >= G(botlimit)) return false;
-        if(type >= AI_START && numenemies >= G(enemylimit)) return false;
         int cn = addclient(ST_REMOTE);
         if(cn >= 0)
         {
             clientinfo *ci = (clientinfo *)getinfo(cn);
             if(ci)
             {
-                int s = skill, m = 100, n = 1;
-                getskillrange(type, m, n);
+                int s = skill, n = 1, m = 100;
+                getskillrange(type, n, m);
                 if(skill > m || skill < n) s = (m != n ? rnd(m-n) + n + 1 : m);
                 ci->clientnum = cn;
                 clientinfo *owner = findaiclient();
                 ci->state.ownernum = owner ? owner->clientnum : -1;
                 if(owner) owner->bots.add(ci);
                 ci->state.aireinit = 2;
-                ci->state.aitype = type;
-                ci->state.aientity = ent;
+                ci->state.actortype = type;
+                ci->state.spawnpoint = ent;
                 ci->state.skill = clamp(s, 1, 101);
                 clients.add(ci);
-                ci->state.lasttimeplayed = lastmillis;
+                ci->state.lasttimeplayed = totalmillis;
                 ci->state.colour = rnd(0xFFFFFF);
                 ci->state.model = rnd(PLAYERTYPES);
                 ci->state.setvanity(ci->state.model ? G(botfemalevanities) : G(botmalevanities)); // the first slot is special
-                copystring(ci->name, aistyle[ci->state.aitype].name, MAXNAMELEN);
-                if(ci->state.aitype == AI_BOT)
+                copystring(ci->name, actor[ci->state.actortype].name, MAXNAMELEN);
+                ci->state.loadweap.shrink(0);
+                if(ci->state.actortype == A_BOT)
                 {
                     const char *list = ci->state.model ? G(botfemalenames) : G(botmalenames);
                     int len = listlen(list);
@@ -104,9 +146,10 @@ namespace aiman
                             delete[] name;
                         }
                     }
+                    ci->state.loadweap.add(rnd(W_LOADOUT)+W_OFFSET);
                 }
                 ci->state.state = CS_DEAD;
-                ci->team = type == AI_BOT ? T_NEUTRAL : T_ENEMY;
+                ci->team = type == A_BOT ? T_NEUTRAL : T_ENEMY;
                 ci->online = ci->connected = ci->ready = true;
                 return true;
             }
@@ -117,7 +160,7 @@ namespace aiman
 
     void deleteai(clientinfo *ci)
     {
-        if(ci->state.aitype == AI_NONE) return;
+        if(ci->state.actortype == A_PLAYER) return;
         int cn = ci->clientnum;
         loopv(clients) if(clients[i] != ci)
         {
@@ -126,7 +169,6 @@ namespace aiman
         }
         if(smode) smode->leavegame(ci, true);
         mutate(smuts, mut->leavegame(ci, true));
-        ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed;
         savescore(ci);
         sendf(-1, 1, "ri3", N_DISCONNECT, cn, DISC_NONE);
         clientinfo *owner = (clientinfo *)getinfo(ci->state.ownernum);
@@ -139,7 +181,7 @@ namespace aiman
     bool delai(int type, bool skip)
     {
         bool retry = false;
-        loopvrev(clients) if(clients[i]->state.aitype == type && clients[i]->state.ownernum >= 0)
+        loopvrev(clients) if(clients[i]->state.actortype == type && clients[i]->state.ownernum >= 0)
         {
             if(!skip || clients[i]->state.state == CS_DEAD || clients[i]->state.state == CS_WAITING)
             {
@@ -154,12 +196,12 @@ namespace aiman
 
     void reinitai(clientinfo *ci)
     {
-        if(ci->state.aitype == AI_NONE) return;
+        if(ci->state.actortype == A_PLAYER) return;
         if(ci->state.ownernum < 0) deleteai(ci);
         else if(ci->state.aireinit >= 1)
         {
             if(ci->state.aireinit == 2) loopk(W_MAX) loopj(2) ci->state.weapshots[k][j].reset();
-            sendf(-1, 1, "ri6si3s", N_INITAI, ci->clientnum, ci->state.ownernum, ci->state.aitype, ci->state.aientity, ci->state.skill, ci->name, ci->team, ci->state.colour, ci->state.model, ci->state.vanity);
+            sendf(-1, 1, "ri6si3siv", N_INITAI, ci->clientnum, ci->state.ownernum, ci->state.actortype, ci->state.spawnpoint, ci->state.skill, ci->name, ci->team, ci->state.colour, ci->state.model, ci->state.vanity, ci->state.loadweap.length(), ci->state.loadweap.length(), ci->state.loadweap.getbuf());
             if(ci->state.aireinit == 2)
             {
                 waiting(ci, DROP_RESET);
@@ -189,7 +231,7 @@ namespace aiman
         loopv(clients)
         {
             clientinfo *ci = clients[i];
-            if(ci->clientnum < 0 || ci->state.aitype > AI_NONE || !ci->ready || ci == exclude)
+            if(ci->clientnum < 0 || ci->state.actortype > A_PLAYER || !ci->ready || ci == exclude)
                 continue;
             if(!lo || ci->bots.length() < lo->bots.length()) lo = ci;
             if(!hi || hi->bots.length() > hi->bots.length()) hi = ci;
@@ -207,42 +249,34 @@ namespace aiman
 
     void checksetup()
     {
-        int m = 100, n = 1, numbots = 0, numenemies = 0;
-        loopv(clients) if(clients[i]->state.aitype > AI_NONE && clients[i]->state.ownernum >= 0)
+        int numbots = 0, numenemies = 0, blimit = getlimit(A_BOT), elimit = getlimit(A_ENEMY);
+        loopv(clients) if(clients[i]->state.actortype > A_PLAYER && clients[i]->state.ownernum >= 0)
         {
             clientinfo *ci = clients[i];
-            getskillrange(clients[i]->state.aitype, m, n);
-            if(ci->state.skill > m || ci->state.skill < n)
-            { // needs re-skilling
-                ci->state.skill = (m != n ? rnd(m-n) + n + 1 : m);
-                if(!ci->state.aireinit) ci->state.aireinit = 1;
-            }
-            if(ci->state.aitype == AI_BOT && ++numbots >= G(botlimit)) shiftai(ci, NULL);
-            if(ci->state.aitype >= AI_START && ++numenemies >= G(enemylimit)) shiftai(ci, NULL);
+            if(ci->state.actortype == A_BOT && ++numbots >= blimit) { shiftai(ci, NULL); continue; }
+            if(ci->state.actortype >= A_ENEMY && ++numenemies >= elimit) { shiftai(ci, NULL); continue; }
+            setskill(ci);
         }
 
         int balance = 0, people = numclients(-1, true, -1), numt = numteams(gamemode, mutators);
-#ifdef CAMPAIGN
-        if(m_campaign(gamemode)) balance = G(campaignplayers); // campaigns strictly obeys player setting
-        else
-#endif
         if(m_coop(gamemode, mutators))
         {
             numt--; // filter out the human team
             balance = people+int(ceilf(people*numt*(m_multi(gamemode, mutators) ? G(coopmultibalance) : G(coopbalance))));
         }
-        else if(m_bots(gamemode) && G(botlimit) > 0)
+        else if(m_bots(gamemode) && blimit > 0)
         {
-            switch(G(botbalance))
+            int bb = m_botbal(gamemode, mutators);
+            switch(bb)
             {
-                case -1: balance = max(people, m_duel(gamemode, mutators) ? 2 : G(numplayers)); break; // use distributed map players
+                case -1: balance = max(people, G(numplayers)); break; // use distributed map players
                 case  0: balance = 0; break; // no bots
-                default: balance = max(people, m_duel(gamemode, mutators) ? 2 : G(botbalance)); break; // balance to at least this
+                default: balance = max(people, bb); break; // balance to at least this
             }
             if(m_team(gamemode, mutators) && balance > 0)
             { // skew this if teams are unbalanced
                 int plrs[T_TOTAL] = {0}, highest = -1; // we do this because humans can unbalance in odd ways
-                loopv(clients) if(clients[i]->state.aitype == AI_NONE && clients[i]->team >= T_FIRST && isteam(gamemode, mutators, clients[i]->team, T_FIRST))
+                loopv(clients) if(clients[i]->state.actortype == A_PLAYER && clients[i]->team >= T_FIRST && isteam(gamemode, mutators, clients[i]->team, T_FIRST))
                 {
                     int team = clients[i]->team-T_FIRST;
                     plrs[team]++;
@@ -261,18 +295,18 @@ namespace aiman
         }
         balance += G(botoffset)*numt;
         int bots = balance-people;
-        if(bots > G(botlimit)) balance -= bots-G(botlimit);
+        if(bots > blimit) balance -= bots-blimit;
         if(balance > 0)
         {
-            while(numclients(-1, true, AI_BOT) < balance) if(!addai(AI_BOT)) break;
-            while(numclients(-1, true, AI_BOT) > balance) if(!delai(AI_BOT)) break;
+            while(numclients(-1, true, A_BOT) < balance) if(!addai(A_BOT)) break;
+            while(numclients(-1, true, A_BOT) > balance) if(!delai(A_BOT)) break;
             if(m_team(gamemode, mutators)) loopvrev(clients)
             {
                 clientinfo *ci = clients[i];
-                if(ci->state.aitype == AI_BOT && ci->state.ownernum >= 0)
+                if(ci->state.actortype == A_BOT && ci->state.ownernum >= 0)
                 {
                     int teamb = chooseteam(ci, ci->team);
-                    if(ci->team != teamb) setteam(ci, teamb, TT_DFINFO);
+                    if(ci->team != teamb) setteam(ci, teamb, TT_RESETX);
                 }
             }
         }
@@ -281,36 +315,30 @@ namespace aiman
 
     void checkenemies()
     {
-        if(m_enemies(gamemode, mutators))
+        if(m_onslaught(gamemode, mutators))
         {
-            loopvj(sents) if(sents[j].type == ACTOR && sents[j].attrs[0] >= 0 && sents[j].attrs[0] < AI_TOTAL && gamemillis >= sents[j].millis && (sents[j].attrs[5] == triggerid || !sents[j].attrs[5]) && m_check(sents[j].attrs[3], sents[j].attrs[4], gamemode, mutators))
+            loopvj(sents) if(sents[j].type == ACTOR && sents[j].attrs[0] >= 0 && sents[j].attrs[0] < A_TOTAL && gamemillis >= sents[j].millis && (sents[j].attrs[5] == triggerid || !sents[j].attrs[5]) && m_check(sents[j].attrs[3], sents[j].attrs[4], gamemode, mutators))
             {
-                bool allow = true;
-                loopv(clients) if(clients[i]->state.aitype < AI_START)
-                {
-                    float dist = clients[i]->state.o.dist(sents[j].o);
-                    if(dist > G(enemyspawndistmax) || dist < G(enemyspawndistmin))
-                    {
-                        allow = false;
-                        break;
-                    }
-                }
-                if(!allow) continue;
                 int count = 0, numenemies = 0;
-                loopvrev(clients) if(clients[i]->state.aitype >= AI_START)
+                loopvrev(clients) if(clients[i]->state.actortype >= A_ENEMY)
                 {
-                    if(clients[i]->state.aientity == j && ++count > G(enemybalance))
+                    if(clients[i]->state.spawnpoint == j)
                     {
-                        deleteai(clients[i]);
-                        count--;
-                        continue;
+                        count++;
+                        if(count > G(enemybalance))
+                        {
+                            deleteai(clients[i]);
+                            count--;
+                            continue;
+                        }
                     }
                     numenemies++;
                 }
                 if(numenemies < G(enemylimit) && count < G(enemybalance))
                 {
                     int amt = min(G(enemybalance)-count, G(enemylimit)-numenemies);
-                    loopk(amt) addai(sents[j].attrs[0]+AI_START, j);
+                    loopk(amt) addai(sents[j].attrs[0]+A_ENEMY, j);
+                    sents[j].millis = gamemillis+G(enemyspawntime);
                 }
             }
         }
@@ -319,20 +347,25 @@ namespace aiman
 
     void clearai(int type)
     { // clear and remove all ai immediately
-        loopvrev(clients) if(!type || (type == 2 ? clients[i]->state.aitype >= AI_START : clients[i]->state.aitype == AI_BOT))
+        loopvrev(clients) if(!type || (type == 2 ? clients[i]->state.actortype >= A_ENEMY : clients[i]->state.actortype == A_BOT))
             deleteai(clients[i]);
     }
 
+    void poke()
+    {
+        dorefresh = max(dorefresh, G(airefreshdelay));
+    }
+
     void checkai()
     {
         if(!m_demo(gamemode) && numclients())
         {
-            if(hasgameinfo && !interm)
+            if(canplay())
             {
                 if(!dorefresh)
                 {
-                    #define checkold(n) if(old##n != G(n)) { dorefresh = 1; old##n = G(n); }
-                    if(m_enemies(gamemode, mutators))
+                    #define checkold(n) if(old##n != G(n)) { dorefresh = -1; old##n = G(n); }
+                    if(m_onslaught(gamemode, mutators))
                     {
                         checkold(enemyskillmin);
                         checkold(enemyskillmax);
@@ -354,14 +387,24 @@ namespace aiman
                     checkold(botoffset);
                     checkold(enemylimit);
                     checkold(numplayers);
+                    int bb = m_botbal(gamemode, mutators);
+                    if(oldbotbalance != bb) { dorefresh = 1; oldbotbalance = bb; }
                 }
                 if(dorefresh)
                 {
-                    dorefresh -= curtime;
-                    if(dorefresh <= 0) { dorefresh = 0; checksetup(); }
+                    if(dorefresh > 0) dorefresh -= curtime;
+                    if(dorefresh <= 0)
+                    {
+                        if(canbalancenow())
+                        {
+                            dorefresh = 0;
+                            checksetup();
+                        }
+                        else dorefresh = -1;
+                    }
                 }
                 checkenemies();
-                loopvrev(clients) if(clients[i]->state.aitype > AI_NONE) reinitai(clients[i]);
+                loopvrev(clients) if(clients[i]->state.actortype > A_PLAYER) reinitai(clients[i]);
                 while(true) if(!reassignai()) break;
             }
         }
diff --git a/src/game/auth.h b/src/game/auth.h
index a96ccad..170cd06 100644
--- a/src/game/auth.h
+++ b/src/game/auth.h
@@ -1,15 +1,41 @@
+// WARNING: Before modifying this file, please read our Guidelines
+// This file can be found in the distribution under: ./docs/guidelines.txt
+// Or at: http://redeclipse.net/wiki/Multiplayer_Guidelines
+//
+// The Red Eclipse Team provides the play.redeclipse.net master server for the
+// benefit of the Red Eclipse Community. We impose a general set of guidelines
+// for any server/user which connects to the play.redeclipse.net master server.
+// The team reserves the right to block any attempt to connect to the master
+// server at their discretion. Access to services provided by the project are
+// considered to be a privilege, not a right.
+
+// These guidelines are imposed to ensure the integrity of both the Red Eclipse
+// game, and its community. If you do not agree to these terms, you should not
+// connect to the play.redeclipse.net master server, or any servers which are
+// connected to it. These guidelines are not designed to limit your opinion or
+// freedoms granted to you by the open source licenses employed by the project,
+// nor do they cover usage of the game in either offline play or on servers
+// which are not connected to the Red Eclipse master.
+
+// If you have questions or comments regarding these guidelines please contact
+// the Red Eclipse Team. Any person seeking to modify their game or server for
+// use on the master server should first seek permission from the Red Eclipse
+// Team, each modification must be approved and will be done on a case-by-case
+// basis.
+
+
 void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen)
 {
-    char buf[2*sizeof(string)];
+    char buf[2*MAXSTRLEN];
     formatstring(buf)("%d %d ", cn, sessionid);
-    copystring(&buf[strlen(buf)], pwd);
+    concatstring(buf, pwd, sizeof(buf));
     if(!hashstring(buf, result, maxlen)) *result = '\0';
 }
 
 bool checkpassword(clientinfo *ci, const char *wanted, const char *given)
 {
     string hash;
-    hashpassword(ci->clientnum, ci->sessionid, wanted, hash, sizeof(string));
+    hashpassword(ci->clientnum, ci->sessionid, wanted, hash, MAXSTRLEN);
     return !strcmp(hash, given);
 }
 
@@ -36,6 +62,11 @@ ICOMMAND(0, resetlocalop, "", (), localopreset());
 void localopadd(const char *name, const char *flags)
 {
     if(!name || !flags) return;
+    loopv(localops) if(!strcmp(name, localops[i].name))
+    {
+        conoutf("local operator %s already exists with flags %s", localops[i].name, localops[i].flags);
+        return;
+    }
     localop &o = localops.add();
     o.name = newstring(name);
     o.flags = newstring(flags);
@@ -45,9 +76,8 @@ ICOMMAND(0, addlocalop, "ss", (char *n, char *f), localopadd(n, f));
 VAR(IDF_PERSIST, quickauthchecks, 0, 0, 1);
 namespace auth
 {
-    int lastconnect = 0, lastactivity = 0;
+    int lastconnect = 0, lastregister = 0, quickcheck = 0;
     uint nextauthreq = 1;
-    bool quickcheck = false;
 
     clientinfo *findauth(uint id)
     {
@@ -62,15 +92,21 @@ namespace auth
         ci->authreq = nextauthreq++;
         if(!connectedmaster())
         {
-            if(quickauthchecks) quickcheck = true;
+            if(quickauthchecks)
+            {
+                if(!ci->connectauth)
+                    srvmsgftforce(ci->clientnum, CON_EVENT, "\fyplease wait, connecting to master server for a quick match..");
+                quickcheck = totalmillis ? totalmillis : 1;
+            }
+            else if(!ci->local) srvmsgftforce(ci->clientnum, CON_EVENT, "\founable to verify, not connected to master server");
             return;
         }
-        if(!ci->connectauth) srvmsgftforce(ci->clientnum, CON_EVENT, "\fyplease wait, requesting credential match..");
-        requestmasterf("reqauth %u %s\n", ci->authreq, ci->authname);
-        lastactivity = totalmillis;
+        if(!ci->connectauth)
+            srvmsgftforce(ci->clientnum, CON_EVENT, "\fyplease wait, requesting credential match from master server..");
+        requestmasterf("reqauth %u %s %s\n", ci->authreq, ci->authname, gethostname(ci->clientnum));
     }
 
-    bool tryauth(clientinfo *ci, const char *user)
+    bool tryauth(clientinfo *ci, const char *authname = "")
     {
         if(!ci) return false;
         if(!connectedmaster() && !quickauthchecks)
@@ -83,57 +119,60 @@ namespace auth
             srvmsgftforce(ci->clientnum, CON_EVENT, "\foplease wait, still processing previous attempt..");
             return true;
         }
-        filtertext(ci->authname, user, true, true, false, 100);
+        copystring(ci->authname, authname);
         reqauth(ci);
         return true;
     }
 
-    void setprivilege(clientinfo *ci, int val, int flags = 0, bool authed = false, bool local = true)
+    void setprivilege(clientinfo *ci, int val, int flags = 0, bool authed = false)
     {
-        int privilege = ci->privilege;
-        mkstring(msg);
+        string msg = "";
         if(val > 0)
         {
-            if(ci->privilege >= flags) return;
-            privilege = ci->privilege = flags;
+            if((ci->privilege&PRIV_TYPE) >= (flags&PRIV_TYPE)) return;
+            ci->privilege = flags;
             if(authed)
             {
                 formatstring(msg)("\fy%s identified as \fs\fc%s\fS", colourname(ci), ci->authname);
-                if(ci->privilege > PRIV_PLAYER)
+                if((ci->privilege&PRIV_TYPE) > PRIV_PLAYER)
                 {
-                    defformatstring(msgx)(" (\fs\fc%s\fS)", privname(privilege));
+                    defformatstring(msgx)(" (\fs\fc%s\fS)", privname(ci->privilege));
                     concatstring(msg, msgx);
                 }
                 copystring(ci->handle, ci->authname);
             }
-            else formatstring(msg)("\fy%s elevated to \fs\fc%s\fS", colourname(ci), privname(privilege, true));
-            if(local) concatstring(msg, " [\fs\falocal\fS]");
+            else formatstring(msg)("\fy%s elevated to \fs\fc%s\fS", colourname(ci), privname(ci->privilege));
         }
         else
         {
-            if(!ci->privilege) return;
+            if(!(ci->privilege&PRIV_TYPE)) return;
+            int privilege = ci->privilege;
             ci->privilege = PRIV_NONE;
             ci->handle[0] = 0;
             int others = 0;
-            loopv(clients) if(clients[i]->privilege >= PRIV_MODERATOR || clients[i]->local) others++;
+            loopv(clients) if((clients[i]->privilege&PRIV_TYPE) >= PRIV_MODERATOR || clients[i]->local) others++;
             if(!others) mastermode = MM_OPEN;
-            if(!val && privilege >= PRIV_ELEVATED)
-                formatstring(msg)("\fy%s is no longer \fs\fc%s\fS", colourname(ci), privname(privilege, true));
+            if(!val && (privilege&PRIV_TYPE) >= PRIV_ELEVATED)
+                formatstring(msg)("\fy%s relinquished \fs\fc%s\fS status", colourname(ci), privname(privilege));
         }
         if(val >= 0)
         {
-            if(ci->connected && *msg) srvoutforce(ci, -2, "%s", msg);
+            if(*msg)
+            {
+                if(ci->connected) srvoutforce(ci, -2, "%s", msg);
+                else sendf(ci->clientnum, 1, "ri2s", N_SERVMSG, CON_EVENT, msg);
+            }
             sendf(ci->connected ? -1 : ci->clientnum, 1, "ri3s", N_CURRENTPRIV, ci->clientnum, ci->privilege, ci->handle);
         }
         if(paused)
         {
             int others = 0;
-            loopv(clients) if(clients[i]->privilege >= PRIV_ADMINISTRATOR || clients[i]->local) others++;
+            loopv(clients) if((clients[i]->privilege&PRIV_TYPE) >= PRIV_ADMINISTRATOR || clients[i]->local) others++;
             if(!others) setpause(false);
         }
     }
 
-    bool tryident(clientinfo *ci, bool connecting = true, const char *pwd = "", const char *authname = "")
+    bool tryident(clientinfo *ci, const char *authname = "", const char *pwd = "")
     {
         if(*authname)
         {
@@ -148,7 +187,7 @@ namespace auth
         {
             if(adminpass[0] && checkpassword(ci, adminpass, pwd))
             {
-                if(G(autoadmin) || G(connectlock)) setprivilege(ci, 1, PRIV_ADMINISTRATOR);
+                if(G(autoadmin) || G(connectlock)) setprivilege(ci, 1, PRIV_ADMINISTRATOR|PRIV_LOCAL);
                 return true;
             }
             if(serverpass[0] && checkpassword(ci, serverpass, pwd)) return true;
@@ -158,37 +197,47 @@ namespace auth
         return false;
     }
 
-    int allowconnect(clientinfo *ci, bool connecting = true, const char *pwd = "", const char *authname = "")
+    int allowconnect(clientinfo *ci, const char *authname = "", const char *pwd = "")
     {
-        if(ci->local) { tryident(ci, connecting, pwd, authname); return DISC_NONE; }
+        if(ci->local) { tryident(ci, authname, pwd); return DISC_NONE; }
+        if(ci->state.version.game != VERSION_GAME) return DISC_INCOMPATIBLE;
         if(m_local(gamemode)) return DISC_PRIVATE;
-        if(tryident(ci, connecting, pwd, authname)) return DISC_NONE;
+        if(tryident(ci, authname, pwd)) return DISC_NONE;
         // above here are short circuits
         if(numclients() >= G(serverclients)) return DISC_MAXCLIENTS;
         uint ip = getclientip(ci->clientnum);
-        if(!ip || !checkipinfo(control, ipinfo::ALLOW, ip))
+        if(!ip || !checkipinfo(control, ipinfo::EXCEPT, ip))
         {
-            if(mastermode >= MM_PRIVATE || serverpass[0] || (G(connectlock) && !haspriv(ci, G(connectlock)))) return DISC_PRIVATE;
-            if(checkipinfo(control, ipinfo::BAN, ip)) return DISC_IPBAN;
+            if(mastermode >= MM_PRIVATE || serverpass[0] || (G(connectlock) && !haspriv(ci, G(connectlock)))) return DISC_PASSWORD;
+            ipinfo *info = checkipinfo(control, ipinfo::BAN, ip);
+            if(info)
+            {
+                srvmsgftforce(ci->clientnum, CON_EVENT, "\foyou are \fs\fcbanned\fS: \fy%s", info->reason && *info->reason ? info->reason : "no reason specified");
+                return DISC_IPBAN;
+            }
         }
         return DISC_NONE;
     }
 
-    void authfailed(uint id)
+    void authfailed(clientinfo *ci)
     {
-        clientinfo *ci = findauth(id);
         if(!ci) return;
         ci->authreq = ci->authname[0] = 0;
-        srvmsgftforce(ci->clientnum, CON_EVENT, "\foauthority request failed, please check your credentials");
+        srvmsgftforce(ci->clientnum, CON_EVENT, "\foidentity verification failed, please check your credentials");
         if(ci->connectauth)
         {
             ci->connectauth = false;
-            int disc = allowconnect(ci, false);
+            int disc = allowconnect(ci);
             if(disc) { disconnect_client(ci->clientnum, disc); return; }
             connected(ci);
         }
     }
 
+    void authfailed(uint id)
+    {
+        authfailed(findauth(id));
+    }
+
     void authsucceeded(uint id, const char *name, const char *flags)
     {
         clientinfo *ci = findauth(id);
@@ -215,6 +264,7 @@ namespace auth
                 case 'a': case 'A': o = PRIV_ADMINISTRATOR; break;
                 case 'o': case 'O': o = PRIV_OPERATOR; break;
                 case 'm': case 'M': o = PRIV_MODERATOR; break;
+                case 's': case 'S': o = PRIV_SUPPORTER; break;
                 default: break;
             }
             if(o > n)
@@ -223,12 +273,12 @@ namespace auth
                 local = true;
             }
         }
-        if(n > PRIV_NONE) setprivilege(ci, 1, n, true, local);
+        if(n > PRIV_NONE) setprivilege(ci, 1, n|(local ? PRIV_LOCAL : 0), true);
         else ci->authname[0] = 0;
         if(ci->connectauth)
         {
             ci->connectauth = false;
-            int disc = allowconnect(ci, false);
+            int disc = allowconnect(ci);
             if(disc) { disconnect_client(ci->clientnum, disc); return; }
             connected(ci);
         }
@@ -241,15 +291,16 @@ namespace auth
         sendf(ci->clientnum, 1, "riis", N_AUTHCHAL, id, val);
     }
 
-    void answerchallenge(clientinfo *ci, uint id, char *val)
+    bool answerchallenge(clientinfo *ci, uint id, char *val)
     {
-        if(ci->authreq != id) return;
+        if(ci->authreq != id) return false;
         for(char *s = val; *s; s++)
         {
             if(!isxdigit(*s)) { *s = '\0'; break; }
         }
+        //srvmsgftforce(ci->clientnum, CON_EVENT, "\fyconfirming identity with master server..");
         requestmasterf("confauth %u %s\n", id, val);
-        lastactivity = totalmillis;
+        return true;
     }
 
     void processinput(const char *p)
@@ -270,14 +321,22 @@ namespace auth
         else if(!strcmp(w[0], "failauth")) authfailed((uint)(atoi(w[1])));
         else if(!strcmp(w[0], "succauth")) authsucceeded((uint)(atoi(w[1])), w[2], w[3]);
         else if(!strcmp(w[0], "chalauth")) authchallenged((uint)(atoi(w[1])), w[2]);
+        else if(!strcmp(w[0], "sync"))
+        {
+            int oldversion = versioning;
+            versioning = 2;
+            if(servcmd(2, w[1], w[2])) conoutf("master server variable synced: %s", w[1]);
+            versioning = oldversion;
+        }
         else loopj(ipinfo::MAXTYPES) if(!strcmp(w[0], ipinfotypes[j]))
         {
-            ipinfo &p = control.add();
-            p.ip = uint(atoi(w[1]));
-            p.mask = uint(atoi(w[2]));
-            p.type = j;
-            p.flag = ipinfo::GLOBAL; // master info
-            p.time = totalmillis ? totalmillis : 1;
+            ipinfo &c = control.add();
+            c.ip = uint(atoi(w[1]));
+            c.mask = uint(atoi(w[2]));
+            c.type = j;
+            c.flag = ipinfo::GLOBAL; // master info
+            c.time = totalmillis ? totalmillis : 1;
+            if(w[3] && *w[3]) c.reason = newstring(w[3]);
             updatecontrols = true;
             break;
         }
@@ -287,40 +346,64 @@ namespace auth
     void regserver()
     {
         loopvrev(control) if(control[i].flag == ipinfo::GLOBAL) control.remove(i);
-        if(quickcheck) requestmasterf("quick\n");
+        lastregister = totalmillis ? totalmillis : 1;
+        if(quickcheck)
+        {
+            quickcheck = lastregister;
+            requestmasterf("quick\n");
+        }
         else
         {
             conoutf("updating master server");
-            requestmasterf("server %d\n", serverport);
+            requestmasterf("server %d %s\n", serverport, *serverip ? serverip : "*");
         }
-        lastactivity = totalmillis;
     }
 
     void update()
     {
-        if(servertype < 2 && !quickcheck)
+        if(servertype < 2 && (!quickcheck || totalmillis-quickcheck >= 60*1000))
         {
             if(connectedmaster()) disconnectmaster();
             return;
         }
-        if(!connectedmaster() && (!lastconnect || totalmillis-lastconnect > 60*1000))
+        if(connectedmaster())
+        {
+            if(!quickcheck && totalmillis-lastregister >= G(masterinterval)) regserver();
+        }
+        else if(!lastconnect || totalmillis-lastconnect >= 60*1000)
         {
-            lastconnect = totalmillis;
+            lastconnect = totalmillis ? totalmillis : 1;
             if(connectmaster() == ENET_SOCKET_NULL) return;
-            regserver();
-            loopv(clients) if(clients[i]->authreq) reqauth(clients[i]);
-            loopv(connects) if(connects[i]->authreq) reqauth(connects[i]);
         }
-        if(!quickcheck && totalmillis-lastactivity > 30*60*1000) regserver();
     }
-}
 
-void disconnectedmaster()
-{
-    auth::quickcheck = false;
+    void masterconnected()
+    {
+        regserver();
+        loopv(clients) if(clients[i]->authreq) reqauth(clients[i]);
+        loopv(connects) if(connects[i]->authreq) reqauth(connects[i]);
+    }
+
+    void masterdisconnected()
+    {
+        quickcheck = 0;
+        loopv(clients) if(clients[i]->authreq) authfailed(clients[i]);
+        loopv(connects) if(connects[i]->authreq) authfailed(connects[i]);
+    }
 }
 
 void processmasterinput(const char *cmd, int cmdlen, const char *args)
 {
     auth::processinput(cmd);
 }
+
+void masterconnected()
+{
+    auth::masterconnected();
+}
+
+void masterdisconnected()
+{
+    auth::masterdisconnected();
+}
+
diff --git a/src/game/bomber.cpp b/src/game/bomber.cpp
index 0ee7e17..348728f 100644
--- a/src/game/bomber.cpp
+++ b/src/game/bomber.cpp
@@ -3,11 +3,11 @@ namespace bomber
 {
     bomberstate st;
 
-    void killed(gameent *d, gameent *actor)
+    void killed(gameent *d, gameent *v)
     {
-        if(actor && m_gsp1(game::gamemode, game::mutators) && (!m_team(game::gamemode, game::mutators) || d->team != actor->team))
+        if(v && m_gsp1(game::gamemode, game::mutators) && (!m_team(game::gamemode, game::mutators) || d->team != v->team))
         {
-            loopv(st.flags) if(isbomberaffinity(st.flags[i]) && st.flags[i].owner == actor)
+            loopv(st.flags) if(isbomberaffinity(st.flags[i]) && st.flags[i].owner == v)
                 st.flags[i].taketime = lastmillis;
         }
     }
@@ -43,16 +43,16 @@ namespace bomber
         float bestangle = 1e16f, bestdist = 1e16f;
         int best = -1;
         int numdyns = game::numdynents();
-        loopk(d->aitype != AI_NONE ? 4 : 2)
+        loopk(d->actortype != A_PLAYER ? 4 : 2)
         {
             if(bombertargetintersect)
             {
                 findorientation(d->o, d->yaw, d->pitch, dest);
-                if((e = game::intersectclosest(d->o, dest, d)) && e->team == d->team && e->state == CS_ALIVE && (k%2 || e->aitype != AI_BOT))
+                if((e = game::intersectclosest(d->o, dest, d)) && e->team == d->team && e->state == CS_ALIVE && (k%2 || e->actortype != A_BOT))
                     return e->clientnum;
             }
             float fx = k >= 2 ? 360 : (d->ai ? d->ai->views[0] : curfov), fy = k >= 2 ? 360 : (d->ai ? d->ai->views[1] : fovy);
-            loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e->team == d->team && e->state == CS_ALIVE && (k%2 || e->aitype != AI_BOT))
+            loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e->team == d->team && e->state == CS_ALIVE && (k%2 || e->actortype != A_BOT))
             {
                 if(getsight(d->o, d->yaw, d->pitch, e->o, dest, 1e16f, fx, fy))
                 {
@@ -80,9 +80,7 @@ namespace bomber
         if(carryaffinity(d) && (d->action[AC_AFFINITY] || d->actiontime[AC_AFFINITY] > 0))
         {
             if(d->action[AC_AFFINITY]) return true;
-            vec o = d->feetpos(1), inertia;
-            vecfromyawpitch(d->yaw, d->pitch, 1, 0, inertia);
-            inertia.normalize().mul(bomberspeed).add(vec(d->vel).add(d->falling).mul(bomberrelativity));
+            vec o = d->headpos(), inertia = vec(d->yaw*RAD, d->pitch*RAD).mul(bomberspeed).add(vec(d->vel).add(d->falling).mul(bomberrelativity));
             bool guided = m_team(game::gamemode, game::mutators) && bomberlockondelay && lastmillis-d->actiontime[AC_AFFINITY] >= bomberlockondelay;
             client::addmsg(N_DROPAFFIN, "ri8", d->clientnum, guided ? findtarget(d) : -1, int(o.x*DMF), int(o.y*DMF), int(o.z*DMF), int(inertia.x*DMF), int(inertia.y*DMF), int(inertia.z*DMF));
             d->action[AC_AFFINITY] = false;
@@ -110,8 +108,8 @@ namespace bomber
         loopv(st.flags)
         {
             bomberstate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent) || hasbombs.find(i) >= 0 || !f.enabled) continue;
-            vec pos = f.pos(true), dir = vec(pos).sub(camera1->o), colour = isbomberaffinity(f) ? pulsecolour() : vec::hexcolor(TEAM(f.team, colour));
+            if(hasbombs.find(i) >= 0 || !f.enabled) continue;
+            vec pos = f.pos(true), colour = isbomberaffinity(f) ? pulsecolour() : vec::hexcolor(TEAM(f.team, colour));
             float area = 3, size = hud::radaraffinitysize;
             if(isbomberaffinity(f))
             {
@@ -127,7 +125,7 @@ namespace bomber
                 area = 3;
                 if(isbombertarg(f, game::focus->team) && !hasbombs.empty()) size *= 1.25f;
             }
-            hud::drawblip(isbomberaffinity(f) ? hud::bombtex : (isbombertarg(f, game::focus->team) ? hud::arrowtex : hud::flagtex), area, w, h, size, blend*hud::radaraffinityblend, (isbombertarg(f, game::focus->team) ? -1-hud::radarstyle : hud::radarstyle), (isbombertarg(f, game::focus->team) ? dir : pos), colour);
+            hud::drawblip(isbomberaffinity(f) ? hud::bombtex : (isbombertarg(f, game::focus->team) ? hud::arrowtex : hud::pointtex), area, w, h, size, blend*hud::radaraffinityblend, isbombertarg(f, game::focus->team) ? 0 : -1, pos, colour);
         }
     }
 
@@ -151,11 +149,12 @@ namespace bomber
                     pushfont("emphasis");
                     ty += draw_textx("Holding: \fs\f[%d]\f(%s)bomb\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, pulsecols[PULSE_DISCO][clamp((lastmillis/100)%PULSECOLOURS, 0, PULSECOLOURS-1)], hud::bombtex)*hud::noticescale;
                     popfont();
+                    bool important = false;
                     if(carrytime)
                     {
                         int delay = carrytime-(lastmillis-f.taketime);
                         pushfont("default");
-                        ty += draw_textx("Explodes in \fs\fzgy%s\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, timestr(delay, -1))*hud::noticescale;
+                        ty += draw_textx("Explodes in \fs\fzgy%s\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, timestr(delay))*hud::noticescale;
                         popfont();
                         if(m_gsp1(game::gamemode, game::mutators))
                         {
@@ -163,11 +162,15 @@ namespace bomber
                             ty += draw_textx("Killing enemies resets fuse timer", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED)*hud::noticescale;
                             popfont();
                         }
+                        if(delay <= carrytime/4) important = true;
+                    }
+                    if(game::focus == game::player1)
+                    {
+                        SEARCHBINDCACHE(altkey)("affinity", 0, "\f{\fs\fzuy", "\fS}");
+                        pushfont(important ? "emphasis" : "reduced");
+                        ty += draw_textx(important ? "\fs\fzuyPress %s to throw\fS" : "Press %s to throw", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, altkey)*hud::noticescale;
+                        popfont();
                     }
-                    SEARCHBINDCACHE(altkey)("affinity", 0);
-                    pushfont("reduced");
-                    ty += draw_textx("Press \fs\fc%s\fS to throw", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, altkey)*hud::noticescale;
-                    popfont();
                     break;
                 }
             }
@@ -176,15 +179,22 @@ namespace bomber
 
     int drawinventory(int x, int y, int s, int m, float blend)
     {
-        int sy = 0;
+        int sy = 0, numflags = st.flags.length(), estsize = numflags*s, fitsize = y-m, size = s;
+        if(estsize > fitsize) size = int((fitsize/float(estsize))*s);
         loopv(st.flags) if(isbomberaffinity(st.flags[i]))
         {
-            if(y-sy-s < m) break;
+            if(y-sy-size < m) break;
             bomberstate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent) || !f.enabled) continue;
+            if(!f.enabled) continue;
             int millis = lastmillis-f.displaytime;
             vec colour = pulsecolour();
-            float skew = hud::inventoryskew;
+            float skew = hud::inventoryskew, wait = f.droptime ? clamp((lastmillis-f.droptime)/float(bomberresetdelay), 0.f, 1.f) : (f.owner ? clamp((lastmillis-f.taketime)/float(carrytime), 0.f, 1.f) : 1.f);
+            if(gs_playing(game::gamestate) && (f.droptime || (f.owner && carrytime)) && wait > 0.5f)
+            {
+                int delay = wait > 0.7f ? (wait > 0.85f ? 150 : 300) : 600, millis = lastmillis%(delay*2);
+                float amt = (millis <= delay ? millis/float(delay) : 1.f-((millis-delay)/float(delay)));
+                flashcolour(colour.r, colour.g, colour.b, 1.f, 0.f, 0.f, amt);
+            }
             if(f.owner || f.droptime)
             {
                 if(f.owner == game::focus)
@@ -196,29 +206,19 @@ namespace bomber
                 else skew = 1;
             }
             else if(millis <= 1000) skew += ((1.f-skew)-(clamp(float(millis)/1000.f, 0.f, 1.f)*(1.f-skew)));
-            float wait = f.droptime ? clamp((lastmillis-f.droptime)/float(bomberresetdelay), 0.f, 1.f) : (f.owner ? clamp((lastmillis-f.taketime)/float(carrytime), 0.f, 1.f) : 1.f);
             int oldy = y-sy;
-            if(!game::intermission && (f.owner || f.droptime))
-                sy += hud::drawitem(hud::bombtex, x, oldy, s, 0, true, false, colour.x, colour.y, colour.z, blend, skew, "super", "%d%%", int(wait*100.f));
-            else sy += hud::drawitem(hud::bombtex, x, oldy, s, 0, true, false, colour.x, colour.y, colour.z, blend, skew);
+            if(gs_playing(game::gamestate) && (f.owner || f.droptime))
+                sy += hud::drawitem(hud::bombtex, x, oldy, size, 0, true, false, colour.x, colour.y, colour.z, blend, skew, "super", "%d%%", int(wait*100.f));
+            else sy += hud::drawitem(hud::bombtex, x, oldy, size, 0, true, false, colour.x, colour.y, colour.z, blend, skew);
             if(f.owner)
             {
                 vec c2 = vec::hexcolor(TEAM(f.owner->team, colour));
-                hud::drawitem(hud::bombtakentex, x, oldy, s, 0.5f, true, false, c2.r, c2.g, c2.b, blend, skew);
+                hud::drawitem(hud::bombtakentex, x, oldy, size, 0.5f, true, false, c2.r, c2.g, c2.b, blend, skew);
             }
-            else if(f.droptime) hud::drawitem(hud::bombdroptex, x, oldy, s, 0.5f, true, false, 0.25f, 1.f, 1.f, blend, skew);
-            if(!game::intermission)
+            else if(f.droptime) hud::drawitem(hud::bombdroptex, x, oldy, size, 0.5f, true, false, 0.25f, 1.f, 1.f, blend, skew);
+            if(gs_playing(game::gamestate))
             {
-                if(f.droptime || (f.owner && carrytime))
-                {
-                    if(wait > 0.5f)
-                    {
-                        int delay = wait > 0.7f ? (wait > 0.85f ? 150 : 300) : 600, millis = lastmillis%(delay*2);
-                        float amt = (millis <= delay ? millis/float(delay) : 1.f-((millis-delay)/float(delay)));
-                        flashcolour(colour.r, colour.g, colour.b, 1.f, 0.f, 0.f, amt);
-                    }
-                    hud::drawitembar(x, oldy, s, false, colour.r, colour.g, colour.b, blend, skew, wait);
-                }
+                if(f.droptime || (f.owner && carrytime)) hud::drawitembar(x, oldy, size, false, colour.r, colour.g, colour.b, blend, skew, wait);
                 if(f.owner == game::focus && m_team(game::gamemode, game::mutators) && bomberlockondelay && f.owner->action[AC_AFFINITY] && lastmillis-f.owner->actiontime[AC_AFFINITY] >= bomberlockondelay)
                 {
                     gameent *e = game::getclient(findtarget(f.owner));
@@ -230,7 +230,7 @@ namespace bomber
                               sp = interval >= 250 ? (500-interval)/250.f : interval/250.f,
                               sq = max(sp, 0.5f);
                         hud::colourskew(rp, gp, bp, sp);
-                        int sx = int(cx*hud::hudwidth-s*sq), sy = int(cy*hud::hudsize-s*sq), ss = int(s*2*sq);
+                        int sx = int(cx*hud::hudwidth-size*sq), sy = int(cy*hud::hudsize-size*sq), ss = int(size*2*sq);
                         Texture *t = textureload(hud::indicatortex, 3);
                         if(t && t != notexture)
                         {
@@ -257,12 +257,12 @@ namespace bomber
         loopv(st.flags) // flags/bases
         {
             bomberstate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent)) continue;
             cament *c = cameras.add(new cament);
             c->o = f.pos(true);
             c->o.z += enttype[AFFINITY].radius/2;
             c->type = cament::AFFINITY;
             c->id = i;
+            c->player = f.owner;
         }
     }
 
@@ -277,7 +277,7 @@ namespace bomber
                     bomberstate::flag &f = st.flags[c->id];
                     c->o = f.pos(true);
                     c->o.z += enttype[AFFINITY].radius/2;
-                    if(f.owner) c->player = f.owner;
+                    c->player = f.owner;
                 }
                 break;
             }
@@ -289,57 +289,57 @@ namespace bomber
         loopv(st.flags) // flags/bases
         {
             bomberstate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent) || !f.enabled || (f.owner == game::focus && !game::thirdpersonview(true) && rendernormally)) continue;
-            vec above(f.pos(true, true));
-            float trans = isbomberaffinity(f) ? 1.f : 0.5f;
-            if(!isbomberaffinity(f) || !f.interptime) // || (!f.droptime && !f.owner))
-            {
-                int millis = lastmillis-f.displaytime;
-                if(millis <= 1000) trans *= float(millis)/1000.f;
-            }
-            if(trans > 0)
+            float trans = 1;
+            int millis = lastmillis-f.displaytime;
+            if(millis <= 1000) trans *= float(millis)/1000.f;
+            if(!f.enabled) f.baselight.material[0] = f.light.material[0] = bvec(0, 0, 0);
+            else if(isbomberaffinity(f))
             {
-                if(isbomberaffinity(f))
+                vec above(f.pos(true, true));
+                if(!f.owner && !f.droptime) above.z += enttype[AFFINITY].radius/4*trans;
+                float size = trans, yaw = !f.owner && f.proj ? f.proj->yaw : (lastmillis/4)%360, pitch = !f.owner && f.proj ? f.proj->pitch : 0, roll = !f.owner && f.proj ? f.proj->roll : 0,
+                      wait = f.droptime ? clamp((lastmillis-f.droptime)/float(bomberresetdelay), 0.f, 1.f) : ((f.owner && carrytime) ? clamp((lastmillis-f.taketime)/float(carrytime), 0.f, 1.f) : 0.f);
+                int interval = lastmillis%1000;
+                vec effect = pulsecolour();
+                if(wait > 0.5f)
                 {
-                    if(!f.owner && !f.droptime) above.z += enttype[AFFINITY].radius/4*trans;
-                    entitylight *light = &entities::ents[f.ent]->light;
-                    float size = trans, yaw = !f.owner && f.proj ? f.proj->yaw : (lastmillis/4)%360, pitch = !f.owner && f.proj ? f.proj->pitch : 0, roll = !f.owner && f.proj ? f.proj->roll : 0,
-                          wait = f.droptime ? clamp((lastmillis-f.droptime)/float(bomberresetdelay), 0.f, 1.f) : ((f.owner && carrytime) ? clamp((lastmillis-f.taketime)/float(carrytime), 0.f, 1.f) : 0.f);
-                    int interval = lastmillis%1000;
-                    vec effect = pulsecolour();
-                    if(wait > 0.5f)
-                    {
-                        int delay = wait > 0.7f ? (wait > 0.85f ? 150 : 300) : 600, millis = lastmillis%(delay*2);
-                        float amt = (millis <= delay ? millis/float(delay) : 1.f-((millis-delay)/float(delay)));
-                        flashcolour(effect.r, effect.g, effect.b, 1.f, 0.f, 0.f, amt);
-                    }
-                    light->material[0] = bvec::fromcolor(effect);
-                    if(f.owner == game::focus && game::thirdpersonview(true)) trans *= 0.5f;
-                    rendermodel(light, "props/ball", ANIM_MAPMODEL|ANIM_LOOP, above, yaw, pitch, roll, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHTFX, NULL, NULL, 0, 0, trans, size);
+                    int delay = wait > 0.7f ? (wait > 0.85f ? 150 : 300) : 600, millis = lastmillis%(delay*2);
+                    float amt = (millis <= delay ? millis/float(delay) : 1.f-((millis-delay)/float(delay)));
+                    flashcolour(effect.r, effect.g, effect.b, 1.f, 0.f, 0.f, amt);
+                }
+                f.baselight.material[0] = f.light.material[0] = bvec::fromcolor(effect);
+                if(f.owner != game::focus || game::thirdpersonview(true) || !rendernormally)
+                {
+                    if(f.owner == game::focus) trans *= 0.25f;
+                    rendermodel(&f.light, "props/ball", ANIM_MAPMODEL|ANIM_LOOP, above, yaw, pitch, roll, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHTFX, NULL, NULL, 0, 0, trans, size);
                     float fluc = interval >= 500 ? (1500-interval)/1000.f : (500+interval)/1000.f;
-                    int pcolour = (int(light->material[0].x)<<16)|(int(light->material[0].y)<<8)|int(light->material[0].z);
+                    int pcolour = effect.tohexcolor();
                     part_create(PART_HINT_SOFT, 1, above, pcolour, enttype[AFFINITY].radius/4*trans+(2*fluc), fluc*trans);
-                    if(!game::intermission && f.droptime)
+                    if(gs_playing(game::gamestate) && f.droptime)
                     {
-                        above.z += enttype[AFFINITY].radius/4*trans+2.5f;
-                        part_icon(above, textureload(hud::progresstex, 3), 3*trans, 1, 0, 0, 1, pcolour, (lastmillis%1000)/1000.f, 0.1f);
-                        part_icon(above, textureload(hud::progresstex, 3), 2*trans, 0.25f, 0, 0, 1, pcolour);
-                        part_icon(above, textureload(hud::progresstex, 3), 2*trans, 1, 0, 0, 1, pcolour, 0, wait);
-                        above.z += 1.f;
-                        defformatstring(str)("<huge>%d%%", int(wait*100.f)); part_textcopy(above, str, PART_TEXT, 1, pcolour, 2, 1);
+                        above.z += enttype[AFFINITY].radius/4*trans+1.5f;
+                        part_icon(above, textureload(hud::progringtex, 3), 4*trans, 1, 0, 0, 1, pcolour, (lastmillis%1000)/1000.f, 0.1f);
+                        part_icon(above, textureload(hud::progresstex, 3), 4*trans, 0.25f, 0, 0, 1, pcolour);
+                        part_icon(above, textureload(hud::progresstex, 3), 4*trans, 1, 0, 0, 1, pcolour, 0, wait);
                     }
                 }
-                else if(!m_gsp1(game::gamemode, game::mutators))
-                {
-                    part_explosion(above, enttype[AFFINITY].radius*trans, PART_SHOCKWAVE, 1, TEAM(f.team, colour), 1.f, trans*0.25f);
-                    part_explosion(above, enttype[AFFINITY].radius/3*trans, PART_SHOCKBALL, 1, TEAM(f.team, colour), 1.f, trans*0.5f);
-                    above.z += enttype[AFFINITY].radius*trans+2.5f;
-                    defformatstring(info)("<super>%s base", TEAM(f.team, name));
-                    part_textcopy(above, info, PART_TEXT, 1, TEAM(f.team, colour), 2, 1);
-                    above.z += 2.5f;
-                    part_icon(above, textureload(hud::teamtexname(f.team), 3), 2, 1, 0, 0, 1, TEAM(f.team, colour));
-                }
             }
+            else if(!m_gsp1(game::gamemode, game::mutators))
+            {
+                vec above = f.above;
+                float blend = clamp(camera1->o.dist(above)/enttype[AFFINITY].radius, 0.f, 1.f);
+                vec effect = vec::hexcolor(TEAM(f.team, colour)).mul(trans);
+                f.baselight.material[0] = f.light.material[0] = bvec::fromcolor(effect);
+                int pcolour = effect.tohexcolor();
+                part_explosion(above, enttype[AFFINITY].radius/4*trans, PART_SHOCKBALL, 1, pcolour, 1.f, trans*blend*0.25f);
+                above.z += enttype[AFFINITY].radius/4*trans;
+                defformatstring(info)("<super>%s base", TEAM(f.team, name));
+                part_textcopy(above, info, PART_TEXT, 1, TEAM(f.team, colour), 2, trans*blend);
+                above.z += 2.5f;
+                part_icon(above, textureload(hud::teamtexname(f.team), 3), 2, trans*blend, 0, 0, 1, TEAM(f.team, colour));
+            }
+            if(!m_gsp1(game::gamemode, game::mutators))
+                rendermodel(&f.baselight, "props/point", ANIM_MAPMODEL|ANIM_LOOP, f.render, f.yaw, 0, 0, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED, NULL, NULL, 0, 0, 1);
         }
     }
 
@@ -348,7 +348,7 @@ namespace bomber
         loopv(st.flags)
         {
             bomberstate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent) || !f.enabled) continue;
+            if(!f.enabled) continue;
             float trans = 1.f;
             int millis = lastmillis-f.displaytime;
             if(millis <= 1000) trans = float(millis)/1000.f;
@@ -369,7 +369,15 @@ namespace bomber
             gameentity &e = *(gameentity *)entities::ents[i];
             if(!m_check(e.attrs[3], e.attrs[4], game::gamemode, game::mutators) || !isteam(game::gamemode, game::mutators, e.attrs[0], T_NEUTRAL))
                 continue;
-            st.addaffinity(e.o, e.attrs[0], i);
+            int team = e.attrs[0];
+            if(m_gsp3(game::gamemode, game::mutators)) switch(team)
+            { // attack
+                case T_ALPHA: break; // goal
+                case T_OMEGA: team = T_NEUTRAL; break; // ball
+                default: continue; // remove
+            }
+            st.addaffinity(e.o, team, e.attrs[1], e.attrs[2]);
+            if(st.flags.length() >= MAXPARAMS) break;
         }
     }
 
@@ -381,7 +389,8 @@ namespace bomber
         {
             bomberstate::flag &f = st.flags[i];
             putint(p, f.team);
-            putint(p, f.ent);
+            putint(p, f.yaw);
+            putint(p, f.pitch);
             loopj(3) putint(p, int(f.spawnloc[j]*DMF));
         }
     }
@@ -397,7 +406,7 @@ namespace bomber
         while(st.flags.length() > numflags) st.flags.pop();
         loopi(numflags)
         {
-            int team = getint(p), ent = getint(p), enabled = getint(p), owner = getint(p), dropped = 0;
+            int team = getint(p), yaw = getint(p), pitch = getint(p), enabled = getint(p), owner = getint(p), dropped = 0;
             vec spawnloc(0, 0, 0), droploc(0, 0, 0), inertia(0, 0, 0);
             loopj(3) spawnloc[j] = getint(p)/DMF;
             if(owner < 0)
@@ -410,13 +419,19 @@ namespace bomber
                 }
             }
             if(p.overread()) break;
+            if(i >= MAXPARAMS) continue;
             while(!st.flags.inrange(i)) st.flags.add();
             bomberstate::flag &f = st.flags[i];
+            f.reset();
             f.team = team;
-            f.ent = ent;
-            f.enabled = enabled ? 1 : 0;
-            f.spawnloc = spawnloc;
-            if(owner >= 0) st.takeaffinity(i, game::getclient(owner), lastmillis);
+            f.yaw = yaw;
+            f.pitch = pitch;
+            f.enabled = enabled != 0;
+            f.spawnloc = f.render = f.above = spawnloc;
+            f.render.z += 2;
+            physics::droptofloor(f.render);
+            if(f.render.z >= f.above.z-1) f.above.z += f.render.z-(f.above.z-1);
+            if(owner >= 0) st.takeaffinity(i, game::newclient(owner), lastmillis);
             else if(dropped) st.dropaffinity(i, droploc, inertia, lastmillis);
         }
     }
@@ -432,7 +447,7 @@ namespace bomber
         loopv(st.flags) if(st.flags[i].owner == d)
         {
             bomberstate::flag &f = st.flags[i];
-            st.dropaffinity(i, f.owner->feetpos(1), f.owner->vel, lastmillis);
+            st.dropaffinity(i, f.owner->feetpos(bomberdropheight), f.owner->vel, lastmillis);
         }
     }
 
@@ -465,44 +480,46 @@ namespace bomber
         part_explosion(o, enttype[AFFINITY].radius, PART_EXPLOSION, 500, 0xAA4400, 1.f, 0.5f);
         part_explosion(o, enttype[AFFINITY].radius*2, PART_SHOCKWAVE, 250, 0xAA4400, 1.f, 0.1f);
         part_create(PART_SMOKE_LERP_SOFT, 500, o, 0x333333, enttype[AFFINITY].radius*0.75f, 0.5f, -15);
-        int debris = rnd(5)+5, amt = int((rnd(debris)+debris+1)*game::debrisscale);
-        loopi(amt) projs::create(o, o, true, NULL, PRJ_DEBRIS, rnd(game::debrisfade)+game::debrisfade, 0, rnd(501), rnd(101)+50);
+        if(!m_kaboom(game::gamemode, game::mutators) && game::nogore != 2 && game::debrisscale > 0)
+        {
+            int debris = rnd(5)+5, amt = int((rnd(debris)+debris+1)*game::debrisscale);
+            loopi(amt) projs::create(o, o, true, NULL, PRJ_DEBRIS, rnd(game::debrisfade)+game::debrisfade, 0, rnd(501), rnd(101)+50);
+        }
         playsound(WSND2(W_GRENADE, false, S_W_EXPLODE), o, NULL, 0, 255);
     }
 
-    void resetaffinity(int i, int value)
+    void resetaffinity(int i, int enabled)
     {
         if(!st.flags.inrange(i)) return;
         bomberstate::flag &f = st.flags[i];
-        bool isreset = false;
-        if(f.enabled && value)
+        if(f.enabled && !enabled)
         {
             destroyaffinity(f.pos(true, true));
             if(isbomberaffinity(f))
             {
-                if(value == 2)
-                {
-                    affinityeffect(i, T_NEUTRAL, f.pos(true, true), f.spawnloc, 3, "RESET");
-                    game::announcef(S_V_BOMBRESET, CON_INFO, NULL, true, "\fathe \fs\fwbomb\fS has been reset");
-                    isreset = true;
-                }
-                entities::execlink(NULL, f.ent, false);
+                affinityeffect(i, T_NEUTRAL, f.pos(true, true), f.spawnloc, 3, "RESET");
+                game::announcef(S_V_BOMBRESET, CON_SELF, NULL, true, "\fathe \fs\fzwvbomb\fS has been reset");
             }
         }
-        st.returnaffinity(i, lastmillis, value!=0, isreset);
+        st.returnaffinity(i, lastmillis, enabled!=0);
     }
 
+    VAR(IDF_PERSIST, showbomberdists, 0, 2, 2); // 0 = off, 1 = self only, 2 = all
     void scoreaffinity(gameent *d, int relay, int goal, int score)
     {
         if(!st.flags.inrange(relay) || !st.flags.inrange(goal)) return;
         bomberstate::flag &f = st.flags[relay], &g = st.flags[goal];
+        string extra; extra[0] = 0;
+        if(m_gsp2(game::gamemode, game::mutators) && showbomberdists >= (d != game::player1 ? 2 : 1))
+        {
+            if(f.droptime) formatstring(extra)(" from \fs\fy%.2f\fom\fS", f.droppos.dist(g.spawnloc)/8.f);
+            else copystring(extra, " with a \fs\fytouchdown\fS");
+        }
         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;
         defformatstring(gteam)("%s", game::colourteam(g.team, "bombtex"));
-        game::announcef(S_V_BOMBSCORE, CON_INFO, d, true, "\fa%s destroyed the %s base for team %s (score: \fs\fc%d\fS, time taken: \fs\fc%s\fS)", game::colourname(d), gteam, game::colourteam(d->team), score, timestr(lastmillis-f.inittime));
+        game::announcef(S_V_BOMBSCORE, CON_SELF, d, true, "\fa%s destroyed the %s base for team %s%s (score: \fs\fc%d\fS, time taken: \fs\fc%s\fS)", game::colourname(d), gteam, game::colourteam(d->team), extra, score, timestr(lastmillis-f.inittime, 1));
         st.returnaffinity(relay, lastmillis, false);
     }
 
@@ -510,117 +527,120 @@ namespace bomber
     {
         if(!st.flags.inrange(i)) return;
         bomberstate::flag &f = st.flags[i];
-        d->action[AC_AFFINITY] = false;
-        d->actiontime[AC_AFFINITY] = 0;
         playsound(S_CATCH, d->o, d);
         if(!f.droptime)
         {
             affinityeffect(i, d->team, d->feetpos(), f.pos(true, true), 1, "TAKEN");
-            game::announcef(S_V_BOMBPICKUP, CON_INFO, d, true, "\fa%s picked up the \fs\fzwv\f($bombtex)bomb\fS", game::colourname(d));
-            entities::execlink(NULL, f.ent, false);
+            game::announcef(S_V_BOMBPICKUP, CON_SELF, d, true, "\fa%s picked up the \fs\fzwv\f($bombtex)bomb\fS", game::colourname(d));
         }
         st.takeaffinity(i, d, lastmillis);
+        if(d->ai) aihomerun(d, d->ai->state.last());
     }
 
-    void checkaffinity(dynent *e)
+    void checkaffinity(gameent *d, int i)
     {
-        if(e->state != CS_ALIVE || !gameent::is(e)) return;
-        gameent *d = (gameent *)e;
         vec o = d->feetpos();
-        loopv(st.flags)
+        bomberstate::flag &f = st.flags[i];
+        if(f.owner)
         {
-            bomberstate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent) || !f.enabled || !isbomberaffinity(f)) continue;
-            if(f.owner)
+            if(!d->ai || f.owner != d) return;
+            bool forever = m_ffa(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model)/3 || findtarget(d) < 0;
+            if(!carrytime && forever) return;
+            int takemillis = lastmillis-f.taketime, length = forever ? carrytime-550-bomberlockondelay : min(carrytime, 1000);
+            if(takemillis >= length)
             {
-                int takemillis = lastmillis-f.taketime;
-                if(carrytime && d->ai && f.owner == d && takemillis >= carrytime-550-bomberlockondelay)
+                if(d->action[AC_AFFINITY])
                 {
-                    if(d->action[AC_AFFINITY])
-                    {
-                        if(takemillis >= carrytime-500 || lastmillis-d->actiontime[AC_AFFINITY] >= bomberlockondelay)
-                            d->action[AC_AFFINITY] = false;
-                    }
-                    else
-                    {
-                        d->action[AC_AFFINITY] = true;
-                        d->actiontime[AC_AFFINITY] = lastmillis;
-                    }
+                    if((carrytime && takemillis >= carrytime-500) || lastmillis-d->actiontime[AC_AFFINITY] >= bomberlockondelay)
+                        d->action[AC_AFFINITY] = false;
+                }
+                else
+                {
+                    d->action[AC_AFFINITY] = true;
+                    d->actiontime[AC_AFFINITY] = lastmillis;
                 }
-                continue;
             }
-            else if(f.droptime)
+            return;
+        }
+        if(!f.droptime && m_gsp3(game::gamemode, game::mutators) && d->team == T_ALPHA && bomberattackreset) return;
+        if(f.pickuptime && lastmillis-f.pickuptime <= 1000) return;
+        if(f.lastowner == d && f.droptime && lastmillis-f.droptime <= bomberpickupdelay) return;
+        if(o.dist(f.pos()) <= enttype[AFFINITY].radius/2)
+        {
+            client::addmsg(N_TAKEAFFIN, "ri2", d->clientnum, i);
+            f.pickuptime = lastmillis;
+        }
+    }
+
+    void update()
+    {
+        gameent *d = NULL;
+        int numdyn = game::numdynents();
+        loopj(numdyn) if(((d = (gameent *)game::iterdynents(j))) && d->state == CS_ALIVE && (d == game::player1 || d->ai)) dropaffinity(d);
+        loopv(st.flags)
+        {
+            bomberstate::flag &f = st.flags[i];
+            if(!f.enabled || !isbomberaffinity(f)) continue;
+            if(f.droptime)
             {
-                f.droploc = f.pos();
+                vec pos = f.pos();
+                f.distance += f.droploc.dist(pos);
+                f.droploc = pos;
                 if(f.lastowner && (f.lastowner == game::player1 || f.lastowner->ai) && f.proj && (!f.movetime || totalmillis-f.movetime >= 40))
                 {
                     f.inertia = f.proj->vel;
-                    f.movetime = totalmillis;
+                    f.movetime = totalmillis-(totalmillis%40);
                     client::addmsg(N_MOVEAFFIN, "ri8", f.lastowner->clientnum, i, int(f.droploc.x*DMF), int(f.droploc.y*DMF), int(f.droploc.z*DMF), int(f.inertia.x*DMF), int(f.inertia.y*DMF), int(f.inertia.z*DMF));
                 }
             }
-            if(f.pickuptime && lastmillis-f.pickuptime <= 1000) continue;
-            if(f.lastowner == d && f.droptime && (bomberpickupdelay < 0 || lastmillis-f.droptime <= bomberpickupdelay)) continue;
-            if(o.dist(f.pos()) <= enttype[AFFINITY].radius/2)
-            {
-                client::addmsg(N_TAKEAFFIN, "ri2", d->clientnum, i);
-                f.pickuptime = lastmillis;
-            }
+            loopj(numdyn) if(((d = (gameent *)game::iterdynents(j))) && d->state == CS_ALIVE && (d == game::player1 || d->ai)) checkaffinity(d, i);
         }
-        dropaffinity(d);
     }
 
     bool aihomerun(gameent *d, ai::aistate &b)
     {
         vec pos = d->feetpos();
-        if(m_team(game::gamemode, game::mutators) && !m_gsp1(game::gamemode, game::mutators))
+        if(m_team(game::gamemode, game::mutators) && !m_gsp1(game::gamemode, game::mutators) && (!m_gsp3(game::gamemode, game::mutators) || d->team != T_ALPHA))
         {
             int goal = -1;
             loopv(st.flags)
             {
                 bomberstate::flag &g = st.flags[i];
-                if(isbombertarg(g, ai::owner(d)) && (!st.flags.inrange(goal) || g.pos().squaredist(pos) < st.flags[goal].pos().squaredist(pos)))
+                if(isbombertarg(g, d->team) && (goal < 0 || g.pos().squaredist(pos) < st.flags[goal].pos().squaredist(pos)))
                     goal = i;
             }
             if(st.flags.inrange(goal) && ai::makeroute(d, b, st.flags[goal].pos()))
             {
-                d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_AFFINITY, goal);
+                d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_AFFINITY, goal, ai::AI_A_HASTE);
                 return true;
             }
         }
 	    if(b.type == ai::AI_S_PURSUE && b.targtype == ai::AI_T_NODE) return true; // we already did this..
 		if(ai::randomnode(d, b, ai::ALERTMIN, 1e16f))
 		{
-            d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_NODE, d->ai->route[0]);
+            d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_NODE, d->ai->route[0], ai::AI_A_HASTE);
             return true;
 		}
         return false;
     }
 
-    int aiowner(gameent *d)
-    {
-        loopv(st.flags) if(entities::ents.inrange(st.flags[i].ent) && entities::ents[d->aientity]->links.find(st.flags[i].ent) >= 0)
-            return st.flags[i].team;
-        return d->team;
-    }
-
     bool aicheck(gameent *d, ai::aistate &b)
     {
-        if(d->aitype == AI_BOT)
+        if(d->actortype == A_BOT)
         {
             static vector<int> taken; taken.setsize(0);
             loopv(st.flags)
             {
                 bomberstate::flag &g = st.flags[i];
                 if(g.owner == d) return aihomerun(d, b);
-                else if((g.owner && ai::owner(g.owner) != ai::owner(d)) || g.droptime) taken.add(i);
+                else if((g.owner && g.owner->team != d->team) || g.droptime) taken.add(i);
             }
             if(!ai::badhealth(d)) while(!taken.empty())
             {
                 int flag = taken.length() > 2 ? rnd(taken.length()) : 0;
                 if(ai::makeroute(d, b, st.flags[taken[flag]].pos()))
                 {
-                    d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_AFFINITY, taken[flag]);
+                    d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_AFFINITY, taken[flag], ai::AI_A_HASTE);
                     return true;
                 }
                 else taken.remove(flag);
@@ -635,20 +655,19 @@ namespace bomber
         loopvj(st.flags)
         {
             bomberstate::flag &f = st.flags[j];
-            if(!entities::ents.inrange(f.ent) || !f.enabled) continue;
-            int owner = ai::owner(d);
-            bool home = isbomberhome(f, owner) || isbombertarg(f, owner);
-            if(d->aitype == AI_BOT && m_duke(game::gamemode, game::mutators) && home) continue;
+            if(!f.enabled) continue;
+            bool home = isbomberhome(f, d->team) || isbombertarg(f, d->team);
+            if(d->actortype == A_BOT && m_duke(game::gamemode, game::mutators) && home) continue;
             static vector<int> targets; // build a list of others who are interested in this
             targets.setsize(0);
-            bool regen = d->aitype != AI_BOT || f.team != owner || !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
-            ai::checkothers(targets, d, home || d->aitype != AI_BOT ? ai::AI_S_DEFEND : ai::AI_S_PURSUE, ai::AI_T_AFFINITY, j, true);
-            if(d->aitype == AI_BOT)
+            bool regen = d->actortype != A_BOT || f.team != d->team || !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
+            ai::checkothers(targets, d, home || d->actortype != A_BOT ? ai::AI_S_DEFEND : ai::AI_S_PURSUE, ai::AI_T_AFFINITY, j, true);
+            if(d->actortype == A_BOT)
             {
                 gameent *e = NULL;
                 int numdyns = game::numdynents();
                 float mindist = enttype[AFFINITY].radius*4; mindist *= mindist;
-                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && owner == ai::owner(e))
+                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && d->team == e->team)
                 {
                     if(targets.find(e->clientnum) < 0 && (f.owner == e || e->feetpos().squaredist(f.pos()) <= mindist))
                         targets.add(e->clientnum);
@@ -658,12 +677,12 @@ namespace bomber
             {
                 bool guard = false;
                 if(f.owner || f.droptime || targets.empty()) guard = true;
-                else if(d->hasweap(d->ai->weappref, m_weapon(game::gamemode, game::mutators)))
+                else if(d->hasweap(ai::weappref(d), m_weapon(game::gamemode, game::mutators)))
                 { // see if we can relieve someone who only has a piece of crap
                     gameent *t;
                     loopvk(targets) if((t = game::getclient(targets[k])))
                     {
-                        if((t->ai && !t->hasweap(t->ai->weappref, m_weapon(game::gamemode, game::mutators))) || (!t->ai && t->weapselect < W_OFFSET))
+                        if((t->ai && !t->hasweap(ai::weappref(t), m_weapon(game::gamemode, game::mutators))) || (!t->ai && t->weapselect < W_OFFSET))
                         {
                             guard = true;
                             break;
@@ -678,8 +697,9 @@ namespace bomber
                     n.target = j;
                     n.targtype = ai::AI_T_AFFINITY;
                     n.score = pos.squaredist(f.pos())/(!regen ? 100.f : 1.f);
-                    n.tolerance = f.team != owner ? 0.5f : 0.25f;
+                    n.tolerance = f.team != d->team ? 0.5f : 0.25f;
                     n.team = true;
+                    n.acttype = ai::AI_A_PROTECT;
                 }
             }
             else if(isbomberaffinity(f))
@@ -687,7 +707,7 @@ namespace bomber
                 if(targets.empty())
                 { // attack the flag
                     ai::interest &n = interests.add();
-                    n.state = d->aitype == AI_BOT ? ai::AI_S_PURSUE : ai::AI_S_DEFEND;
+                    n.state = d->actortype == A_BOT ? ai::AI_S_PURSUE : ai::AI_S_DEFEND;
                     n.node = ai::closestwaypoint(f.pos(), ai::CLOSEDIST, true);
                     n.target = j;
                     n.targtype = ai::AI_T_AFFINITY;
@@ -701,8 +721,8 @@ namespace bomber
                     loopvk(targets) if((t = game::getclient(targets[k])))
                     {
                         ai::interest &n = interests.add();
-                        bool team = owner == ai::owner(t);
-                        if(d->aitype == AI_BOT && m_duke(game::gamemode, game::mutators) && team) continue;
+                        bool team = d->team == t->team;
+                        if(d->actortype == A_BOT && m_duke(game::gamemode, game::mutators) && team) continue;
                         n.state = team ? ai::AI_S_DEFEND : ai::AI_S_PURSUE;
                         n.node = t->lastnode;
                         n.target = t->clientnum;
@@ -710,6 +730,7 @@ namespace bomber
                         n.score = d->o.squaredist(t->o);
                         n.tolerance = 0.5f;
                         n.team = team;
+                        if(team) n.acttype = ai::AI_A_PROTECT;
                     }
                 }
             }
@@ -718,37 +739,43 @@ namespace bomber
 
     bool aidefense(gameent *d, ai::aistate &b)
     {
-        if(d->aitype == AI_BOT)
+        if(d->actortype == A_BOT)
         {
             loopv(st.flags) if(st.flags[i].owner == d) return aihomerun(d, b);
-            if(m_duke(game::gamemode, game::mutators)) return false;
+            if(m_duke(game::gamemode, game::mutators) && b.owner < 0) return false;
         }
         if(st.flags.inrange(b.target))
         {
             bomberstate::flag &f = st.flags[b.target];
-            if(isbomberaffinity(f) && f.owner && ai::owner(d) != ai::owner(f.owner))
-                return ai::violence(d, b, f.owner, 4);
-            int walk = f.owner && ai::owner(f.owner) != ai::owner(d) ? 1 : 0;
-            if(d->aitype == AI_BOT)
+            if(isbomberaffinity(f) && f.owner && d->team != f.owner->team && ai::violence(d, b, f.owner, 4)) return true;
+            int walk = f.owner && f.owner->team != d->team ? 1 : 0;
+            if(d->actortype == A_BOT)
             {
-                bool regen = !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
-                if(regen && lastmillis-b.millis >= (201-d->skill)*33)
+                if((!m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model)) && lastmillis-b.millis >= (201-d->skill)*33)
                 {
-                    static vector<int> targets; // build a list of others who are interested in this
-                    targets.setsize(0);
-                    ai::checkothers(targets, d, ai::AI_S_DEFEND, ai::AI_T_AFFINITY, b.target, true);
-                    gameent *e = NULL;
-                    int numdyns = game::numdynents();
-                    float mindist = enttype[AFFINITY].radius*4; mindist *= mindist;
-                    loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && ai::owner(d) == ai::owner(e))
+                    if(b.owner < 0)
                     {
-                        if(targets.find(e->clientnum) < 0 && (f.owner == e || e->feetpos().squaredist(f.pos()) <= mindist))
-                            targets.add(e->clientnum);
-                    }
-                    if(!targets.empty())
-                    {
-                        d->ai->tryreset = true; // re-evaluate so as not to herd
-                        return true;
+                        static vector<int> targets; // build a list of others who are interested in this
+                        targets.setsize(0);
+                        ai::checkothers(targets, d, ai::AI_S_DEFEND, ai::AI_T_AFFINITY, b.target, true);
+                        gameent *e = NULL;
+                        int numdyns = game::numdynents();
+                        float mindist = enttype[AFFINITY].radius*4; mindist *= mindist;
+                        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && d->team == e->team)
+                        {
+                            if(targets.find(e->clientnum) < 0 && (f.owner == e || e->feetpos().squaredist(f.pos()) <= mindist))
+                                targets.add(e->clientnum);
+                        }
+                        if(!targets.empty())
+                        {
+                            d->ai->tryreset = true; // re-evaluate so as not to herd
+                            return true;
+                        }
+                        else
+                        {
+                            walk = 2;
+                            b.millis = lastmillis;
+                        }
                     }
                     else
                     {
@@ -756,41 +783,37 @@ namespace bomber
                         b.millis = lastmillis;
                     }
                 }
-                vec pos = d->feetpos();
-                float mindist = enttype[AFFINITY].radius*8; mindist *= mindist;
-                loopv(st.flags)
-                { // get out of the way of the returnee!
-                    bomberstate::flag &g = st.flags[i];
-                    if(pos.squaredist(g.pos()) <= mindist)
-                    {
-                        if(g.owner && ai::owner(g.owner) == ai::owner(d) && !walk) walk = 1;
-                        if(g.droptime && ai::makeroute(d, b, g.pos())) return true;
-                    }
-                }
             }
-            return ai::defense(d, b, f.pos(), f.owner ? ai::CLOSEDIST : float(enttype[AFFINITY].radius), f.owner ? ai::ALERTMIN : float(enttype[AFFINITY].radius*walk*16), walk);
+            return ai::defense(d, b, f.pos(), enttype[AFFINITY].radius, enttype[AFFINITY].radius*walk*8, walk);
         }
         return false;
     }
 
     bool aipursue(gameent *d, ai::aistate &b)
     {
-        if(st.flags.inrange(b.target) && d->aitype == AI_BOT)
+        if(st.flags.inrange(b.target) && d->actortype == A_BOT)
         {
             bomberstate::flag &f = st.flags[b.target];
-            if(!entities::ents.inrange(f.ent) || !f.enabled) return false;
+            if(!f.enabled) return false;
             if(isbomberaffinity(f))
             {
                 if(f.owner)
                 {
                     if(d == f.owner) return aihomerun(d, b);
-                    else if(ai::owner(d) != ai::owner(f.owner)) return ai::violence(d, b, f.owner, 4);
+                    else if(d->team != f.owner->team) return ai::violence(d, b, f.owner, 4);
                     else return ai::defense(d, b, f.pos());
                 }
                 return ai::makeroute(d, b, f.pos());
             }
-            else if(isbombertarg(f, ai::owner(d)))
-                loopv(st.flags) if(st.flags[i].owner == d) return ai::makeroute(d, b, f.pos());
+            if(isbombertarg(f, d->team))
+            {
+                loopv(st.flags) if(st.flags[i].owner == d && ai::makeroute(d, b, f.pos()))
+                {
+                    b.acttype = ai::AI_A_HASTE;
+                    return true;
+                }
+            }
+            if(b.owner >= 0) return ai::makeroute(d, b, f.pos());
         }
         return false;
     }
diff --git a/src/game/bomber.h b/src/game/bomber.h
index 4547421..c57fc38 100644
--- a/src/game/bomber.h
+++ b/src/game/bomber.h
@@ -12,9 +12,10 @@ struct bomberstate
 {
     struct flag
     {
-        vec droploc, inertia, spawnloc;
-        int team, ent, droptime, taketime;
+        vec droploc, droppos, inertia, spawnloc;
+        int team, yaw, pitch, droptime, taketime;
         bool enabled;
+        float distance;
 #ifdef GAMESERVER
         int owner, lastowner;
         vector<int> votes;
@@ -22,31 +23,29 @@ struct bomberstate
         gameent *owner, *lastowner;
         projent *proj;
         int displaytime, pickuptime, movetime, inittime, viewtime, rendertime, interptime;
-        vec viewpos, renderpos, interppos;
+        vec viewpos, renderpos, interppos, render, above;
+        entitylight light, baselight;
 #endif
 
-        flag()
-#ifndef GAMESERVER
-          : ent(-1)
-#endif
-        { reset(); }
+        flag() { reset(); }
 
         void reset()
         {
             inertia = vec(0, 0, 0);
-            droploc = spawnloc = vec(-1, -1, -1);
+            droploc = droppos = spawnloc = vec(-1, -1, -1);
 #ifdef GAMESERVER
             owner = lastowner = -1;
             votes.shrink(0);
 #else
             owner = lastowner = NULL;
             proj = NULL;
-            displaytime = pickuptime = movetime = inittime = viewtime = rendertime = 0;
+            displaytime = pickuptime = movetime = inittime = viewtime = rendertime = interptime = 0;
             viewpos = renderpos = vec(-1, -1, -1);
 #endif
             team = T_NEUTRAL;
-            taketime = droptime = 0;
+            yaw = pitch = taketime = droptime = 0;
             enabled = false;
+            distance = 0;
         }
 
 #ifndef GAMESERVER
@@ -61,8 +60,7 @@ struct bomberstate
                         if(totalmillis != rendertime)
                         {
                             float yaw = 360-((lastmillis/2)%360), off = (lastmillis%1000)/500.f;
-                            vecfromyawpitch(yaw, 0, 1, 0, renderpos);
-                            renderpos.normalize().mul(owner->radius+4).add(owner->center());
+                            renderpos = vec(yaw*RAD, 0.f).mul(owner->radius+4).add(owner->center());
                             renderpos.z += owner->height*(off > 1 ?  2-off : off);
                             rendertime = totalmillis;
                         }
@@ -72,7 +70,7 @@ struct bomberstate
                 }
                 if(droptime) return proj ? proj->o : droploc;
             }
-            return spawnloc;
+            return above;
         }
 
         vec &pos(bool view = false, bool render = false)
@@ -93,6 +91,12 @@ struct bomberstate
             return position(render);
         }
 #endif
+        bool travel(const vec &o, float dist)
+        {
+            if(!droptime) return false;
+            if(distance > 0 && distance >= dist) return true;
+            return droppos.dist(o) >= dist;
+        }
     };
     vector<flag> flags;
 
@@ -101,13 +105,20 @@ struct bomberstate
         flags.shrink(0);
     }
 
-    void addaffinity(const vec &o, int team, int ent)
+    void addaffinity(const vec &o, int team, int yaw, int pitch)
     {
         flag &f = flags.add();
         f.reset();
         f.team = team;
         f.spawnloc = o;
-        f.ent = ent;
+        f.yaw = yaw;
+        f.pitch = pitch;
+#ifndef GAMESERVER
+        f.render = f.above = o;
+        f.render.z += 2;
+        physics::droptofloor(f.render);
+        if(f.render.z >= f.above.z-1) f.above.z += f.render.z-(f.above.z-1);
+#endif
     }
 
 #ifndef GAMESERVER
@@ -155,7 +166,8 @@ struct bomberstate
 #else
         f.pickuptime = f.movetime = 0;
         if(!f.inittime) f.inittime = t;
-        (f.lastowner = owner)->addicon(eventicon::AFFINITY, t, game::eventiconfade, f.team);
+        owner->addicon(eventicon::AFFINITY, t, game::eventiconfade, f.team);
+        f.lastowner = owner;
         destroy(i);
 #endif
     }
@@ -166,16 +178,16 @@ struct bomberstate
 #ifndef GAMESERVER
         interp(i, t);
 #endif
-        f.droploc = o;
+        f.droploc = f.droppos = o;
         f.inertia = p;
         f.droptime = t;
         f.taketime = 0;
+        f.distance = 0;
 #ifdef GAMESERVER
         f.owner = -1;
         f.votes.shrink(0);
 #else
-        f.pickuptime = 0;
-        f.movetime = t;
+        f.pickuptime = f.movetime = 0;
         if(!f.inittime) f.inittime = t;
         f.owner = NULL;
         destroy(i);
@@ -183,12 +195,11 @@ struct bomberstate
 #endif
     }
 
-    void returnaffinity(int i, int t, bool enabled, bool isreset = false)
+    void returnaffinity(int i, int t, bool enabled)
     {
         flag &f = flags[i];
 #ifndef GAMESERVER
         interp(i, t);
-        if(!isreset) f.interptime = 0;
 #endif
         f.droptime = f.taketime = 0;
         f.enabled = enabled;
@@ -214,12 +225,12 @@ namespace bomber
     extern void dropaffinity(gameent *d, int i, const vec &droploc, const vec &inertia, int target = -1);
     extern void scoreaffinity(gameent *d, int relay, int goal, int score);
     extern void takeaffinity(gameent *d, int i);
-    extern void resetaffinity(int i, int value);
+    extern void resetaffinity(int i, int enabled);
     extern void reset();
     extern void setup();
     extern void setscore(int team, int total);
-    extern void checkaffinity(dynent *e);
-    extern void killed(gameent *d, gameent *actor);
+    extern void update();
+    extern void killed(gameent *d, gameent *v);
     extern void drawnotices(int w, int h, int &tx, int &ty, float blend);
     extern void drawblips(int w, int h, float blend);
     extern int drawinventory(int x, int y, int s, int m, float blend);
@@ -227,6 +238,7 @@ namespace bomber
     extern void render();
     extern void adddynlights();
     extern int aiowner(gameent *d);
+    extern bool aihomerun(gameent *d, ai::aistate &b);
     extern void aifind(gameent *d, ai::aistate &b, vector<ai::interest> &interests);
     extern bool aicheck(gameent *d, ai::aistate &b);
     extern bool aidefense(gameent *d, ai::aistate &b);
diff --git a/src/game/bombermode.h b/src/game/bombermode.h
index f189f87..90c16ad 100644
--- a/src/game/bombermode.h
+++ b/src/game/bombermode.h
@@ -5,7 +5,7 @@ struct bomberservmode : bomberstate, servmode
 
     bomberservmode() : hasflaginfo(false), bombertime(-1) {}
 
-    void reset(bool empty)
+    void reset()
     {
         bomberstate::reset();
         hasflaginfo = false;
@@ -14,25 +14,25 @@ struct bomberservmode : bomberstate, servmode
 
     void dropaffinity(clientinfo *ci, const vec &o, const vec &inertia = vec(0, 0, 0), int target = -1)
     {
-        if(!hasflaginfo || ci->state.aitype >= AI_START) return;
-        vec n = inertia.iszero() ? vec(0, 0, G(bomberspeed)/10.f) : inertia, v = o; v.z += 1;
+        if(!canplay(hasflaginfo) || ci->state.actortype >= A_ENEMY) return;
+        vec n = inertia.iszero() ? vec(0, 0, G(bomberspeed)/10.f) : inertia;
         loopv(flags) if(flags[i].owner == ci->clientnum)
         {
-            ivec p(vec(v).mul(DMF)), q(vec(n).mul(DMF));
+            ivec p(vec(o).mul(DMF)), q(vec(n).mul(DMF));
             sendf(-1, 1, "ri3i7", N_DROPAFFIN, ci->clientnum, target, i, p.x, p.y, p.z, q.x, q.y, q.z);
-            bomberstate::dropaffinity(i, v, n, gamemillis);
+            bomberstate::dropaffinity(i, o, n, gamemillis);
         }
     }
 
     void leavegame(clientinfo *ci, bool disconnecting = false)
     {
-        if(!hasflaginfo) return;
-        dropaffinity(ci, ci->state.o, vec(ci->state.vel).add(ci->state.falling));
+        if(!canplay(hasflaginfo)) return;
+        dropaffinity(ci, ci->state.feetpos(G(bomberdropheight)), vec(ci->state.vel).add(ci->state.falling));
     }
 
-    void dodamage(clientinfo *target, clientinfo *actor, int &damage, int &hurt, int &weap, int &flags, int &material, const ivec &hitpush)
+    void dodamage(clientinfo *m, clientinfo *v, int &damage, int &hurt, int &weap, int &flags, int &material, const ivec &hitpush, const ivec &hitvel, float dist)
     {
-        //if(weaptype[weap].melee) dropaffinity(target, target->state.o);
+        //if(weaptype[weap].melee) dropaffinity(m, m->state.o);
     }
 
     void spawned(clientinfo *ci)
@@ -54,13 +54,13 @@ struct bomberservmode : bomberstate, servmode
         loopvj(sents) if(enttype[sents[j].type].usetype == EU_ITEM) setspawn(j, hasitem(j), true, true);
     }
 
-    void died(clientinfo *ci, clientinfo *actor)
+    void died(clientinfo *ci, clientinfo *v)
     {
-        if(!hasflaginfo) return;
-        dropaffinity(ci, ci->state.o, vec(ci->state.vel).add(ci->state.falling));
-        if(actor && m_gsp1(gamemode, mutators) && (!m_team(gamemode, mutators) || ci->team != actor->team))
+        if(!canplay(hasflaginfo)) return;
+        dropaffinity(ci, ci->state.feetpos(G(bomberdropheight)), vec(ci->state.vel).add(ci->state.falling));
+        if(v && m_gsp1(gamemode, mutators) && (!m_team(gamemode, mutators) || ci->team != v->team))
         {
-            loopv(flags) if(isbomberaffinity(flags[i]) && flags[i].owner == actor->clientnum)
+            loopv(flags) if(isbomberaffinity(flags[i]) && flags[i].owner == v->clientnum)
                 flags[i].taketime = gamemillis;
         }
     }
@@ -74,21 +74,22 @@ struct bomberservmode : bomberstate, servmode
 
     void scorebomb(clientinfo *ci, int relay, int goal)
     {
+        if(!canplay(hasflaginfo)) return;
         flag g = flags[goal];
         if(!g.enabled) return;
-        int score = 0;
+        int total = 0;
         if(g.team != ci->team)
         {
-            givepoints(ci, G(bomberpoints));
-            score = addscore(ci->team, 1);
+            if(!m_nopoints(gamemode, mutators)) givepoints(ci, G(bomberpoints));
+            total = addscore(ci->team, 1);
         }
         else
         {
-            givepoints(ci, -G(bomberpenalty));
-            score = addscore(ci->team, -1);
+            if(!m_nopoints(gamemode, mutators)) givepoints(ci, -G(bomberpenalty));
+            total = addscore(ci->team, -1);
         }
         bomberstate::returnaffinity(relay, gamemillis, false);
-        sendf(-1, 1, "ri5", N_SCOREAFFIN, ci->clientnum, relay, goal, score);
+        sendf(-1, 1, "ri5", N_SCOREAFFIN, ci->clientnum, relay, goal, total);
         mutate(smuts, mut->scoreaffinity(ci, g.team != ci->team));
         bombertime = m_duke(gamemode, mutators) ? -1 : gamemillis+G(bomberdelay);
         loopvj(flags) if(flags[j].enabled)
@@ -96,87 +97,108 @@ struct bomberservmode : bomberstate, servmode
             bomberstate::returnaffinity(j, gamemillis, false);
             sendf(-1, 1, "ri3", N_RESETAFFIN, j, 0);
         }
-        if(!m_balance(gamemode, mutators) && G(bomberlimit) && score >= G(bomberlimit))
+        if(!m_balance(gamemode, mutators, teamspawns) && G(bomberlimit) && total >= G(bomberlimit))
         {
             ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fyscore limit has been reached");
             startintermission();
         }
-    }
-
-    void scoreaffinity(clientinfo *ci, int relay, int goal)
-    {
-        if(!m_team(gamemode, mutators) || m_gsp2(gamemode, mutators) || !flags.inrange(relay) || !flags.inrange(goal) || flags[relay].lastowner != ci->clientnum || !flags[relay].droptime) return;
-        scorebomb(ci, relay, goal);
+        if(m_gsp3(gamemode, mutators) && G(bomberattackwinner) && !m_multi(gamemode, mutators))
+        {
+            int numt = numteams(gamemode, mutators);
+            if(curbalance == numt-1)
+            {
+                bool found = false;
+                loopi(numt)
+                {
+                    int t = i+T_FIRST, s = teamscore(t).total;
+                    if(t != ci->team && s >= total)
+                    {
+                        found = true;
+                        break;
+                    }
+                }
+                if(!found)
+                {
+                    ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fybest score has been reached");
+                    startintermission();
+                }
+            }
+        }
     }
 
     void moved(clientinfo *ci, const vec &oldpos, const vec &newpos)
     {
-        if(!hasflaginfo || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || ci->state.actortype >= A_ENEMY) return;
         if(G(bomberthreshold) > 0 && oldpos.dist(newpos) >= G(bomberthreshold))
             dropaffinity(ci, oldpos, vec(ci->state.vel).add(ci->state.falling));
-        if(!m_team(gamemode, mutators) || m_gsp1(gamemode, mutators)) return;
-        loopv(flags) if(isbomberaffinity(flags[i]) && flags[i].owner == ci->clientnum)
+        if(m_gsp1(gamemode, mutators) || (G(bomberbasketonly) && m_gsp2(gamemode, mutators))) return;
+        loopv(flags) if(isbomberaffinity(flags[i]) && flags[i].owner == ci->clientnum) loopvk(flags)
+            if(isbombertarg(flags[k], ci->team) && newpos.dist(flags[k].spawnloc) <= enttype[AFFINITY].radius/2) scorebomb(ci, i, k);
+    }
+
+    void returnaffinity(int i, bool enabled)
+    {
+        flag &f = flags[i];
+        bool wasenabled = isbomberaffinity(f) && f.enabled;
+        bomberstate::returnaffinity(i, gamemillis, enabled);
+        sendf(-1, 1, "ri3", N_RESETAFFIN, i, f.enabled ? 1 : 0);
+        if(wasenabled && !f.enabled)
         {
-            loopvk(flags)
-            {
-                flag &f = flags[k];
-                if(isbombertarg(f, ci->team) && newpos.dist(f.spawnloc) <= enttype[AFFINITY].radius/2) scorebomb(ci, i, k);
-            }
+            loopvj(flags) if(i != j && flags[j].enabled) returnaffinity(j, false);
+            if(bombertime >= 0) bombertime = gamemillis+G(bomberdelay);
         }
     }
 
     void takeaffinity(clientinfo *ci, int i)
     {
-        if(!hasflaginfo || !flags.inrange(i) || ci->state.state!=CS_ALIVE || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || !flags.inrange(i) || ci->state.state!=CS_ALIVE || ci->state.actortype >= A_ENEMY) return;
         flag &f = flags[i];
         if(!isbomberaffinity(f) || f.owner >= 0 || !f.enabled) return;
-        if(f.lastowner == ci->clientnum && f.droptime && (G(bomberpickupdelay) < 0 || lastmillis-f.droptime <= G(bomberpickupdelay))) return;
-        bomberstate::takeaffinity(i, ci->clientnum, gamemillis);
-        if(!m_nopoints(gamemode, mutators) && (!f.droptime || f.lastowner != ci->clientnum)) givepoints(ci, G(bomberpickuppoints));
-        sendf(-1, 1, "ri3", N_TAKEAFFIN, ci->clientnum, i);
-    }
-
-    void returnaffinity(int i, int v) // 0 = disable, 1 = return and wait, 2 = return instantly
-    {
-        bomberstate::returnaffinity(i, gamemillis, v == 2);
-        sendf(-1, 1, "ri3", N_RESETAFFIN, i, flags[i].enabled ? v : 0);
-        if(v && !flags[i].enabled)
+        if(f.lastowner == ci->clientnum && f.droptime && gamemillis-f.droptime <= G(bomberpickupdelay)) return;
+        if(m_gsp3(gamemode, mutators) && ci->team == T_ALPHA && G(bomberattackreset))
         {
-            loopvj(flags) if(flags[j].enabled) returnaffinity(j, 0);
-            if(bombertime >= 0) bombertime = gamemillis+G(bomberdelay);
+            if(!f.droptime) return;
+            if(!m_nopoints(gamemode, mutators) && (!f.droptime || f.lastowner != ci->clientnum)) givepoints(ci, G(bomberpickuppoints));
+            returnaffinity(i, false);
+        }
+        else
+        {
+            bomberstate::takeaffinity(i, ci->clientnum, gamemillis);
+            if(!m_nopoints(gamemode, mutators) && (!f.droptime || f.lastowner != ci->clientnum)) givepoints(ci, G(bomberpickuppoints));
+            sendf(-1, 1, "ri3", N_TAKEAFFIN, ci->clientnum, i);
         }
     }
 
-    void resetaffinity(clientinfo *ci, int i, bool force = false)
+    void resetaffinity(clientinfo *ci, int i)
     {
-        if(!hasflaginfo || !flags.inrange(i) || ci->state.ownernum >= 0) return;
+        if(!canplay(hasflaginfo) || !flags.inrange(i) || ci->state.ownernum >= 0) return;
         flag &f = flags[i];
         if(!isbomberaffinity(f) || f.owner >= 0 || !f.droptime || f.votes.find(ci->clientnum) >= 0 || !f.enabled) return;
         f.votes.add(ci->clientnum);
-        if(f.votes.length() >= numclients()/2) returnaffinity(i, 2);
+        if(f.votes.length() >= int(floorf(numclients()*0.5f))) returnaffinity(i, false);
     }
 
     void layout()
     {
-        if(!hasflaginfo) return;
+        if(!canplay(hasflaginfo)) return;
         bombertime = -1;
-        loopv(flags) if(flags[i].owner >= 0 || flags[i].droptime) returnaffinity(i, 0);
+        loopv(flags) if(flags[i].owner >= 0 || flags[i].droptime) returnaffinity(i, false);
         bombertime = gamemillis+G(bomberdelay);
     }
 
     void update()
     {
-        if(!hasflaginfo || bombertime < 0) return;
+        if(!canplay(hasflaginfo) || bombertime < 0) return;
         if(bombertime)
         {
             if(gamemillis < bombertime) return;
             int hasaffinity = 0;
             vector<int> candidates[T_MAX];
             loopv(flags) candidates[flags[i].team].add(i);
-            int wants = m_gsp1(gamemode, mutators) ? 1 : teamcount(gamemode, mutators);
-            loopi(wants)
+            int wants = m_gsp1(gamemode, mutators) ? 1 : (m_gsp3(gamemode, mutators) ? 2 : teamcount(gamemode, mutators));
+            loopi(wants) if(!candidates[i].empty())
             {
-                int c = candidates[i].length(), r = c > 1 ? rnd(c) : 0;
+                int c = candidates[i].length(), r = rnd(c);
                 if(candidates[i].inrange(r) && flags.inrange(candidates[i][r]) && isteam(gamemode, mutators, flags[candidates[i][r]].team, T_NEUTRAL))
                 {
                     bomberstate::returnaffinity(candidates[i][r], gamemillis, true);
@@ -188,16 +210,19 @@ struct bomberservmode : bomberstate, servmode
             {
                 if(!candidates[T_NEUTRAL].empty() && !m_gsp1(gamemode, mutators))
                 {
+                    int muts = mutators;
+                    if(muts&(1<<G_M_GSP2)) muts &= ~(1<<G_M_GSP2);
+                    muts |= (1<<G_M_GSP1);
                     srvmsgf(-1, "\fzoythis map does have enough goals, switching on hold mutator");
-                    sendf(-1, 1, "risi3", N_MAPCHANGE, smapname, 0, gamemode, mutators|(1<<G_M_GSP1));
-                    changemap(smapname, gamemode, mutators|(1<<G_M_GSP1));
+                    sendf(-1, 1, "risi3", N_MAPCHANGE, smapname, 0, gamemode, muts);
+                    changemap(smapname, gamemode, muts);
                     return;
                 }
                 hasflaginfo = false;
                 loopv(flags) sendf(-1, 1, "ri3", N_RESETAFFIN, i, 0);
                 srvmsgf(-1, "\fs\fzoythis map is not playable in:\fS %s", gamename(gamemode, mutators));
             }
-            else ancmsgft(-1, m_duke(gamemode, mutators) ? S_V_BOMBDUEL : S_V_BOMBSTART, CON_INFO, "\fathe \fs\fwbomb\fS has been spawned");
+            else ancmsgft(-1, m_duke(gamemode, mutators) ? S_V_BOMBDUEL : S_V_BOMBSTART, CON_INFO, "\fathe \fs\fzwvbomb\fS has been spawned");
             bombertime = 0;
         }
         int t = (gamemillis/G(bomberholdinterval))-((gamemillis-(curtime+scoresec))/G(bomberholdinterval));
@@ -209,7 +234,7 @@ struct bomberservmode : bomberstate, servmode
             if(f.owner >= 0)
             {
                 clientinfo *ci = (clientinfo *)getinfo(f.owner);
-                if((!m_team(gamemode, mutators) || m_gsp1(gamemode, mutators)) && t > 0)
+                if(m_gsp1(gamemode, mutators) && t > 0)
                 {
                     int score = G(bomberholdpoints)*t;
                     if(score)
@@ -221,7 +246,7 @@ struct bomberservmode : bomberstate, servmode
                             total = addscore(ci->team, score);
                             sendf(-1, 1, "ri3", N_SCORE, ci->team, total);
                         }
-                        if(!m_balance(gamemode, mutators) && G(bomberholdlimit) && total >= G(bomberholdlimit))
+                        if(!m_balance(gamemode, mutators, teamspawns) && G(bomberholdlimit) && total >= G(bomberholdlimit))
                         {
                             ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fyscore limit has been reached");
                             startintermission();
@@ -231,12 +256,12 @@ struct bomberservmode : bomberstate, servmode
                 if(ci && carrytime && gamemillis-f.taketime >= carrytime)
                 {
                     ci->state.weapshots[W_GRENADE][0].add(1);
-                    sendf(-1, 1, "ri8", N_DROP, ci->clientnum, -1, 1, W_GRENADE, -1, -1, -1);
-                    dropaffinity(ci, ci->state.o, vec(ci->state.vel).add(ci->state.falling));
-                    if((!m_team(gamemode, mutators) || m_gsp1(gamemode, mutators)) && G(bomberholdpenalty))
+                    sendf(-1, 1, "ri7", N_DROP, ci->clientnum, -1, 1, W_GRENADE, -1, -1);
+                    dropaffinity(ci, ci->state.feetpos(G(bomberdropheight)), vec(ci->state.vel).add(ci->state.falling));
+                    if(m_gsp1(gamemode, mutators) && G(bomberholdpenalty))
                     {
                         givepoints(ci, -G(bomberholdpenalty));
-                        if(m_team(gamemode, mutators) && m_gsp1(gamemode, mutators))
+                        if(m_team(gamemode, mutators))
                         {
                             int total = addscore(ci->team, -G(bomberholdpenalty));
                             sendf(-1, 1, "ri3", N_SCORE, ci->team, total);
@@ -245,7 +270,7 @@ struct bomberservmode : bomberstate, servmode
                 }
                 continue;
             }
-            if(f.droptime && gamemillis-f.droptime >= G(bomberresetdelay)) returnaffinity(i, 2);
+            if(f.droptime && gamemillis-f.droptime >= G(bomberresetdelay)) returnaffinity(i, false);
         }
     }
 
@@ -265,7 +290,8 @@ struct bomberservmode : bomberstate, servmode
         {
             flag &f = flags[i];
             putint(p, f.team);
-            putint(p, f.ent);
+            putint(p, f.yaw);
+            putint(p, f.pitch);
             putint(p, f.enabled ? 1 : 0);
             putint(p, f.owner);
             loopj(3) putint(p, int(f.spawnloc[j]*DMF));
@@ -292,7 +318,7 @@ struct bomberservmode : bomberstate, servmode
 
     void regen(clientinfo *ci, int &total, int &amt, int &delay)
     {
-        if(!hasflaginfo || !G(bomberregenbuff) || !ci->state.lastbuff) return;
+        if(!canplay(hasflaginfo) || !G(bomberregenbuff) || !ci->state.lastbuff) return;
         if(G(maxhealth)) total = max(m_maxhealth(gamemode, mutators, ci->state.model), total);
         if(ci->state.lastregen && G(bomberregendelay)) delay = G(bomberregendelay);
         if(G(bomberregenextra)) amt += G(bomberregenextra);
@@ -300,9 +326,9 @@ struct bomberservmode : bomberstate, servmode
 
     void checkclient(clientinfo *ci)
     {
-        if(!hasflaginfo || ci->state.state != CS_ALIVE || m_insta(gamemode, mutators)) return;
-        #define bomberbuff1 (G(bomberbuffing)&1 && isbomberhome(f, ci->team) && ci->state.o.dist(f.spawnloc) <= G(bomberbuffarea))
-        #define bomberbuff2 (G(bomberbuffing)&2 && isbomberaffinity(f) && f.owner == ci->clientnum)
+        if(!canplay(hasflaginfo) || ci->state.state != CS_ALIVE || m_insta(gamemode, mutators)) return;
+        #define bomberbuff1 (G(bomberbuffing)&1 && isbomberhome(f, ci->team) && (G(bomberbuffarea) > 0 ? ci->state.o.dist(f.spawnloc) <= G(bomberbuffarea) : true))
+        #define bomberbuff2 ((G(bomberbuffing)&2 || (G(bomberbuffing)&4 && m_gsp3(gamemode, mutators) && ci->team == T_ALPHA)) && isbomberaffinity(f) && f.owner == ci->clientnum)
         if(G(bomberbuffing)) loopv(flags)
         {
             flag &f = flags[i];
@@ -313,7 +339,7 @@ struct bomberservmode : bomberstate, servmode
                 return;
             }
         }
-        if(ci->state.lastbuff && (!G(bomberbuffing) || gamemillis-ci->state.lastbuff > G(bomberbuffdelay)))
+        if(ci->state.lastbuff && (!G(bomberbuffing) || gamemillis-ci->state.lastbuff >= G(bomberbuffdelay)))
         {
             ci->state.lastbuff = 0;
             sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 0);
@@ -322,13 +348,19 @@ struct bomberservmode : bomberstate, servmode
 
     void moveaffinity(clientinfo *ci, int cn, int id, const vec &o, const vec &inertia = vec(0, 0, 0))
     {
-        if(!flags.inrange(id)) return;
+        if(!canplay(hasflaginfo) || !flags.inrange(id)) return;
         flag &f = flags[id];
-        if(!f.droptime || f.owner >= 0 || !isbomberaffinity(f)) return;
-        clientinfo *co = f.lastowner >= 0 ? (clientinfo *)getinfo(f.lastowner) : NULL;
-        if(!co || co->clientnum != ci->clientnum) return;
+        if(!f.droptime || f.owner >= 0 || !isbomberaffinity(f) || f.lastowner != ci->clientnum) return;
+        f.distance += f.droploc.dist(o);
         f.droploc = o;
         f.inertia = inertia;
+        if(!m_gsp1(gamemode, mutators) && m_gsp2(gamemode, mutators)) loopv(flags)
+        {
+            if(isbomberaffinity(flags[i]) || f.droploc.dist(flags[i].spawnloc) > enttype[AFFINITY].radius/2) continue;
+            if(G(bomberbasketmindist) > 0 && !flags[id].travel(flags[i].spawnloc, G(bomberbasketmindist))) continue;
+            scorebomb(ci, id, i);
+            break;
+        }
         //sendf(-1, 1, "ri9", N_MOVEAFFIN, ci->clientnum, id, int(f.droploc.x*DMF), int(f.droploc.y*DMF), int(f.droploc.z*DMF), int(f.inertia.x*DMF), int(f.inertia.y*DMF), int(f.inertia.z*DMF));
     }
 
@@ -339,10 +371,11 @@ struct bomberservmode : bomberstate, servmode
         {
             loopi(numflags)
             {
-                int team = getint(p), ent = getint(p);
+                int team = getint(p), yaw = getint(p), pitch = getint(p);
                 vec o;
                 loopj(3) o[j] = getint(p)/DMF;
-                if(!hasflaginfo) addaffinity(o, team, ent);
+                if(p.overread()) break;
+                if(!hasflaginfo && i < MAXPARAMS) addaffinity(o, team, yaw, pitch);
             }
             if(!hasflaginfo)
             {
@@ -353,11 +386,11 @@ struct bomberservmode : bomberstate, servmode
         }
     }
 
-    int points(clientinfo *victim, clientinfo *actor)
+    int points(clientinfo *m, clientinfo *v)
     {
-        bool isteam = victim==actor || (m_team(gamemode, mutators) && victim->team == actor->team);
-        int p = isteam ? -1 : (m_team(gamemode, mutators) ? 1 : 0), v = p;
-        if(p) { loopv(flags) if(flags[i].owner == victim->clientnum) p += v; }
+        bool isteam = m==v || (m_team(gamemode, mutators) && m->team == v->team);
+        int p = isteam ? -1 : (m_team(gamemode, mutators) ? 1 : 0), q = p;
+        if(p) { loopv(flags) if(flags[i].owner == m->clientnum) p += q; }
         return p;
     }
 } bombermode;
diff --git a/src/game/capture.cpp b/src/game/capture.cpp
index f4b1ed7..415e7d2 100644
--- a/src/game/capture.cpp
+++ b/src/game/capture.cpp
@@ -11,33 +11,53 @@ namespace capture
 
     bool dropaffinity(gameent *d)
     {
-        if(m_capture(game::gamemode) && carryaffinity(d) && d->action[AC_AFFINITY])
+        if(carryaffinity(d) && d->action[AC_AFFINITY])
         {
-            vec o = d->feetpos(1), inertia = vec(d->vel).add(d->falling);
+            vec o = d->feetpos(capturedropheight), inertia = vec(d->vel).add(d->falling);
             client::addmsg(N_DROPAFFIN, "ri8", d->clientnum, -1, int(o.x*DMF), int(o.y*DMF), int(o.z*DMF), int(inertia.x*DMF), int(inertia.y*DMF), int(inertia.z*DMF));
             d->action[AC_AFFINITY] = false;
-            d->actiontime[AC_AFFINITY] = 0;
             return true;
         }
         return false;
     }
 
+    bool canpickup(gameent *d, int n, bool check = false)
+    {
+        if(!st.flags.inrange(n)) return false;
+        capturestate::flag &f = st.flags[n];
+        if(f.owner) return false;
+        if(f.pickuptime && lastmillis-f.pickuptime <= 1000) return false;
+        if(f.team == d->team)
+        {
+            if(m_gsp2(game::gamemode, game::mutators)) return false;
+            if(!f.droptime)
+            {
+                if(m_gsp1(game::gamemode, game::mutators)) return false;
+                if(!check && !d->action[AC_AFFINITY]) return false;
+            }
+        }
+        if(f.lastowner == d && f.droptime && lastmillis-f.droptime <= capturepickupdelay)
+            return false;
+        if((f.pos()).dist(d->feetpos()) > enttype[AFFINITY].radius*2/3) return false;
+        return true;
+    }
+
     void preload()
     {
+        preloadmodel("props/point");
         preloadmodel("props/flag");
     }
 
     void drawblips(int w, int h, float blend)
     {
         static vector<int> hasflags; hasflags.setsize(0);
-        loopv(st.flags) if(entities::ents.inrange(st.flags[i].ent) && st.flags[i].owner == game::focus) hasflags.add(i);
+        loopv(st.flags) if(st.flags[i].owner == game::focus) hasflags.add(i);
         loopv(st.flags)
         {
             capturestate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent)) continue;
             loopk(2)
             {
-                vec dir, pos, colour = vec::hexcolor(TEAM(f.team, colour));
+                vec pos, colour = vec::hexcolor(TEAM(f.team, colour));
                 const char *tex = hud::flagtex;
                 bool arrow = false;
                 float fade = blend*hud::radaraffinityblend, size = hud::radaraffinitysize;
@@ -48,7 +68,6 @@ namespace capture
                 {
                     if(f.owner == game::focus || (!f.owner && !f.droptime)) break;
                     pos = f.pos(true);
-                    dir = vec(pos).sub(camera1->o);
                     int interval = lastmillis%500;
                     if(interval >= 300 || interval <= 200)
                         fade *= clamp(interval >= 300 ? 1.f-((interval-300)/200.f) : interval/200.f, 0.f, 1.f);
@@ -56,7 +75,6 @@ namespace capture
                 else
                 {
                     pos = f.spawnloc;
-                    dir = vec(pos).sub(camera1->o);
                     if(f.team == game::focus->team && !m_gsp3(game::gamemode, game::mutators) && !hasflags.empty())
                     {
                         size *= 1.25f;
@@ -65,8 +83,8 @@ namespace capture
                     }
                     else if(f.owner || f.droptime) tex = hud::alerttex;
                 }
-                if(hud::radaraffinitynames > (arrow ? 0 : 1)) hud::drawblip(tex, arrow ? 3 : 2, w, h, size, fade, arrow ? -1-hud::radarstyle : hud::radarstyle, arrow ? dir : pos, colour, "little", "\f[%d]%s", TEAM(f.team, colour), k ? "flag" : "base");
-                else hud::drawblip(tex, arrow ? 3 : 2, w, h, hud::radaraffinitysize, fade, arrow ? -1-hud::radarstyle : hud::radarstyle, arrow ? dir : pos, colour);
+                if(hud::radaraffinitynames > (arrow ? 0 : 1)) hud::drawblip(tex, arrow ? 3 : 2, w, h, size, fade, arrow ? 0 : -1, pos, colour, "little", "\f[%d]%s", TEAM(f.team, colour), k ? "flag" : "base");
+                else hud::drawblip(tex, arrow ? 3 : 2, w, h, hud::radaraffinitysize, fade, arrow ? 0 : -1, pos, colour);
             }
         }
     }
@@ -94,13 +112,19 @@ namespace capture
                 else ty += draw_textx("Buffing: \fs\fo%d%%\fS damage, \fs\fg%d%%\fS shield", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, int(capturebuffdamage*100), int(capturebuffshield*100))*hud::noticescale;
                 popfont();
             }
-            static vector<int> hasflags, taken, droppedflags;
-            hasflags.setsize(0); taken.setsize(0); droppedflags.setsize(0);
+            bool ownflag = false;
+            static vector<int> pickup, hasflags, taken, droppedflags;
+            pickup.setsize(0); hasflags.setsize(0); taken.setsize(0); droppedflags.setsize(0);
             loopv(st.flags)
             {
                 capturestate::flag &f = st.flags[i];
-                if(f.owner == game::focus) hasflags.add(i);
-                else if(f.team == game::focus->team)
+                if(f.owner == game::focus)
+                {
+                    hasflags.add(i);
+                    if(f.team == game::focus->team) ownflag = true;
+                }
+                if(canpickup(game::focus, i, true)) pickup.add(i);
+                if(f.team == game::focus->team)
                 {
                     if(f.owner && f.owner->team != game::focus->team) taken.add(i);
                     else if(f.droptime) droppedflags.add(i);
@@ -108,36 +132,56 @@ namespace capture
             }
             if(!hasflags.empty())
             {
+                if(capturebuffing&(ownflag ? 8 : 32))
+                {
+                    pushfont("reduced");
+                    if(capturebuffarea > 0) ty += draw_textx("Buffing team-mates within \fs\fy%.2f\fom\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, capturebuffarea/8.f)*hud::noticescale;
+                    else ty += draw_textx("Buffing \fs\fyALL\fS team-mates", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1)*hud::noticescale;
+                    popfont();
+                }
                 pushfont("emphasis");
                 char *str = buildflagstr(hasflags, hasflags.length() <= 3);
                 ty += draw_textx("Holding: \fs%s\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, str)*hud::noticescale;
                 popfont();
-                SEARCHBINDCACHE(altkey)("affinity", 0);
+            }
+            if(!pickup.empty())
+            {
+                pushfont("emphasis");
+                char *str = buildflagstr(pickup, pickup.length() <= 3);
+                ty += draw_textx("Nearby: \fs%s\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, str)*hud::noticescale;
+                popfont();
+            }
+            if(game::focus == game::player1 && (!hasflags.empty() || !pickup.empty()))
+            {
+                SEARCHBINDCACHE(altkey)("affinity", 0, "\f{\fs\fzuy", "\fS}");
                 pushfont("reduced");
-                ty += draw_textx("Press \fs\fc%s\fS to drop", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, altkey)*hud::noticescale;
+                ty += draw_textx("Press %s to %s", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, altkey, !hasflags.empty() ? "drop" : "pick up")*hud::noticescale;
                 popfont();
             }
-            pushfont("default");
             if(!taken.empty())
             {
+                pushfont("default");
                 char *str = buildflagstr(taken, taken.length() <= 3);
                 ty += draw_textx("%s taken: \fs%s\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, taken.length() == 1 ? "Flag" : "Flags", str)*hud::noticescale;
+                popfont();
             }
             if(!droppedflags.empty())
             {
+                pushfont("default");
                 char *str = buildflagstr(droppedflags, droppedflags.length() <= 3);
                 ty += draw_textx("%s dropped: \fs%s\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, droppedflags.length() == 1 ? "Flag" : "Flags", str)*hud::noticescale;
+                popfont();
             }
-            popfont();
         }
     }
 
     int drawinventory(int x, int y, int s, int m, float blend)
     {
-        int sy = 0;
+        int sy = 0, numflags = st.flags.length(), estsize = numflags*s, fitsize = y-m, size = s;
+        if(estsize > fitsize) size = int((fitsize/float(estsize))*s);
         loopv(st.flags)
         {
-            if(y-sy-s < m) break;
+            if(y-sy-size < m) break;
             capturestate::flag &f = st.flags[i];
             bool headsup = hud::chkcond(hud::inventorygame, game::player1->state == CS_SPECTATOR || f.team == T_NEUTRAL || f.team == game::focus->team);
             if(headsup || f.lastowner == game::focus)
@@ -157,24 +201,24 @@ namespace capture
                 }
                 else if(millis <= 1000) skew += (1.f-skew)-(clamp(float(millis)/1000.f, 0.f, 1.f)*(1.f-skew));
                 int oldy = y-sy;
-                sy += hud::drawitem(hud::flagtex, x, oldy, s, 0, true, false, c.r, c.g, c.b, blend, skew);
+                sy += hud::drawitem(hud::flagtex, x, oldy, size, 0, true, false, c.r, c.g, c.b, blend, skew);
                 if(f.owner)
                 {
                     vec c2 = vec::hexcolor(TEAM(f.owner->team, colour));
-                    hud::drawitem(hud::flagtakentex, x, oldy, s, 0.5f, true, false, c2.r, c2.g, c2.b, blend, skew);
+                    hud::drawitem(hud::flagtakentex, x, oldy, size, 0.5f, true, false, c2.r, c2.g, c2.b, blend, skew);
                 }
-                else if(f.droptime) hud::drawitem(hud::flagdroptex, x, oldy, s, 0.5f, true, false, 0.25f, 1.f, 1.f, blend, skew);
-                else hud::drawitem(hud::teamtexname(f.team), x, oldy, s, 0.5f, true, false, c.r, c.g, c.b, blend, skew);
-                if(!game::intermission && (f.droptime || (m_gsp3(game::gamemode, game::mutators) && f.taketime && f.owner && f.owner->team != f.team)))
+                else if(f.droptime) hud::drawitem(hud::flagdroptex, x, oldy, size, 0.5f, true, false, 0.25f, 1.f, 1.f, blend, skew);
+                else hud::drawitem(hud::teamtexname(f.team), x, oldy, size, 0.5f, true, false, c.r, c.g, c.b, blend, skew);
+                if(gs_playing(game::gamestate) && (f.droptime || (m_gsp3(game::gamemode, game::mutators) && f.taketime && f.owner && f.owner->team != f.team)))
                 {
-                    float wait = f.droptime ? clamp((lastmillis-f.droptime)/float(capturedelay), 0.f, 1.f) : clamp((lastmillis-f.taketime)/float(captureprotectdelay), 0.f, 1.f);
+                    float wait = f.droptime ? clamp(f.dropleft(lastmillis, capturestore)/float(capturedelay), 0.f, 1.f) : clamp((lastmillis-f.taketime)/float(captureprotectdelay), 0.f, 1.f);
                     if(wait > 0.5f)
                     {
                         int delay = wait > 0.7f ? (wait > 0.85f ? 150 : 300) : 600, millis = lastmillis%(delay*2);
                         float amt = (millis <= delay ? millis/float(delay) : 1.f-((millis-delay)/float(delay)));
                         flashcolour(c.r, c.g, c.b, 0.65f, 0.65f, 0.65f, amt);
                     }
-                    hud::drawitembar(x, oldy, s, false, c.r, c.g, c.b, blend, skew, wait);
+                    hud::drawitembar(x, oldy, size, false, c.r, c.g, c.b, blend, skew, wait);
                 }
             }
         }
@@ -186,12 +230,12 @@ namespace capture
         loopv(st.flags) // flags/bases
         {
             capturestate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent)) continue;
             cament *c = cameras.add(new cament);
             c->o = f.pos(true);
             c->o.z += enttype[AFFINITY].radius*2/3;
             c->type = cament::AFFINITY;
             c->id = i;
+            c->player = f.owner;
         }
     }
 
@@ -206,7 +250,7 @@ namespace capture
                     capturestate::flag &f = st.flags[c->id];
                     c->o = f.pos(true);
                     c->o.z += enttype[AFFINITY].radius*2/3;
-                    if(f.owner) c->player = f.owner;
+                    c->player = f.owner;
                 }
                 break;
             }
@@ -214,6 +258,11 @@ namespace capture
         }
     }
 
+    FVAR(IDF_PERSIST, followflagblend, 0, 0.5f, 1);
+    FVAR(IDF_PERSIST, thirdflagblend, 0, 0.5f, 1);
+    FVAR(IDF_PERSIST, firstflagblend, 0, 1, 1);
+    FVAR(IDF_PERSIST, freeflagblend, 0, 1, 1);
+
     void render()
     {
         static vector<int> numflags, iterflags; // dropped/owned
@@ -221,79 +270,80 @@ namespace capture
         loopv(st.flags)
         {
             capturestate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent) || !f.owner) continue;
-            while(numflags.length() <= f.owner->clientnum) { numflags.add(0); iterflags.add(0); }
+            if(!f.owner) continue;
+            while(numflags.length() <= f.owner->clientnum)
+            {
+                numflags.add(0);
+                iterflags.add(0);
+            }
             numflags[f.owner->clientnum]++;
         }
         loopv(st.flags) // flags/bases
         {
             capturestate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent)) continue;
-            float wait = f.droptime ? clamp((lastmillis-f.droptime)/float(capturedelay), 0.f, 1.f) : ((m_gsp3(game::gamemode, game::mutators) && f.taketime && f.owner && f.owner->team != f.team) ? clamp((lastmillis-f.taketime)/float(captureprotectdelay), 0.f, 1.f) : 0.f);
-            entitylight *light = &entities::ents[f.ent]->light;
+            vec pos = f.pos(true);
+            float wait = f.droptime ? clamp(f.dropleft(lastmillis, capturestore)/float(capturedelay), 0.f, 1.f) : ((m_gsp3(game::gamemode, game::mutators) && f.taketime && f.owner && f.owner->team != f.team) ? clamp((lastmillis-f.taketime)/float(captureprotectdelay), 0.f, 1.f) : 0.f),
+                  blend = (!f.owner && (!f.droptime || m_gsp2(game::gamemode, game::mutators)) && f.team == game::focus->team ? camera1->o.distrange(pos, enttype[AFFINITY].radius, enttype[AFFINITY].radius/8) : 1.f)*(f.owner && f.owner == game::focus ? (game::thirdpersonview(true) ? (f.owner != game::player1 ? followflagblend : thirdflagblend) : firstflagblend) : freeflagblend);
             vec effect = vec::hexcolor(TEAM(f.team, colour));
+            int colour = effect.tohexcolor();
             if(wait > 0.5f)
             {
                 int delay = wait > 0.7f ? (wait > 0.85f ? 150 : 300) : 600, millis = lastmillis%(delay*2);
                 float amt = (millis <= delay ? millis/float(delay) : 1.f-((millis-delay)/float(delay)));
                 flashcolour(effect.r, effect.g, effect.b, 0.65f, 0.65f, 0.65f, amt);
             }
-            light->material[0] = bvec::fromcolor(effect);
-            int pcolour = (int(light->material[0].x)<<16)|(int(light->material[0].y)<<8)|int(light->material[0].z);
+            f.baselight.material[0] = f.light.material[0] = bvec::fromcolor(effect);
             if(!f.owner && !f.droptime)
-                rendermodel(light, "props/flag", ANIM_MAPMODEL|ANIM_LOOP, f.pos(true), entities::ents[f.ent]->attrs[1], entities::ents[f.ent]->attrs[2], 0, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED, NULL, NULL, 0, 0, 1);
+                rendermodel(&f.light, "props/flag", ANIM_MAPMODEL|ANIM_LOOP, pos, f.yaw, f.pitch, 0, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED, NULL, NULL, 0, 0, blend);
             else if(!f.owner || f.owner != game::focus || game::thirdpersonview(true) || !(rendernormally))
             {
-                vec lac(f.pos(true));
+                vec flagpos = pos;
                 float yaw = 0, pitch = 0, roll = 0;
-                if(f.owner)
-                {
-                    yaw = f.owner->yaw-45.f+(90/float(numflags[f.owner->clientnum]+1)*(iterflags[f.owner->clientnum]+1));
-                    //pitch = f.owner->pitch; // broken by mdlyaw 270
-                    //roll = f.owner->roll;
-                }
+                if(f.owner) yaw = f.owner->yaw-45.f+(90/float(numflags[f.owner->clientnum]+1)*(iterflags[f.owner->clientnum]+1));
                 else
                 {
                     yaw = ((lastmillis/8)+(360/st.flags.length()*i))%360;
-                    if(f.proj) lac.z -= f.proj->height;
+                    if(f.proj) flagpos.z -= f.proj->height;
                 }
                 while(yaw >= 360.f) yaw -= 360.f;
-                float trans = f.owner == game::focus && game::thirdpersonview(true) ? 0.5f : 1.f;
-                rendermodel(light, "props/flag", ANIM_MAPMODEL|ANIM_LOOP, lac, yaw, pitch, roll, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHTFX, NULL, NULL, 0, 0, trans);
-                lac.z += enttype[AFFINITY].radius*2/3;
-                if(f.owner) { lac.z += iterflags[f.owner->clientnum]*2; iterflags[f.owner->clientnum]++; }
-                defformatstring(info)("<super>%s flag", TEAM(f.team, name));
-                part_textcopy(lac, info, PART_TEXT, 1, TEAM(f.team, colour), 2, 1);
-                lac.z += 2.5f;
-                if(!game::intermission && (f.droptime || (m_gsp3(game::gamemode, game::mutators) && f.taketime && f.owner && f.owner->team != f.team)))
+                rendermodel(&f.light, "props/flag", ANIM_MAPMODEL|ANIM_LOOP, flagpos, yaw, pitch, roll, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHTFX, NULL, NULL, 0, 0, blend);
+                flagpos.z += enttype[AFFINITY].radius*2/3;
+                if(f.owner)
+                {
+                    flagpos.z += iterflags[f.owner->clientnum]*2;
+                    iterflags[f.owner->clientnum]++;
+                }
+                //defformatstring(info)("<super>%s flag", TEAM(f.team, name));
+                //part_textcopy(flagpos, info, PART_TEXT, 1, TEAM(f.team, colour), 2, blend);
+                //flagpos.z += 2.5f;
+                if(gs_playing(game::gamestate) && (f.droptime || (m_gsp3(game::gamemode, game::mutators) && f.taketime && f.owner && f.owner->team != f.team)))
                 {
-                    float wait = f.droptime ? clamp((lastmillis-f.droptime)/float(capturedelay), 0.f, 1.f) : clamp((lastmillis-f.taketime)/float(captureprotectdelay), 0.f, 1.f);
-                    part_icon(lac, textureload(hud::progresstex, 3), 3, 1, 0, 0, 1, pcolour, (lastmillis%1000)/1000.f, 0.1f);
-                    part_icon(lac, textureload(hud::progresstex, 3), 2, 0.25f, 0, 0, 1, pcolour);
-                    part_icon(lac, textureload(hud::progresstex, 3), 2, 1, 0, 0, 1, pcolour, 0, wait);
-                    lac.z += 0.5f;
-                    defformatstring(str)("<huge>%d%%", int(wait*100.f)); part_textcopy(lac, str, PART_TEXT, 1, 0xFFFFFF, 2, 1);
-                    lac.z += 2.5f;
+                    float wait = f.droptime ? clamp(f.dropleft(lastmillis, capturestore)/float(capturedelay), 0.f, 1.f) : clamp((lastmillis-f.taketime)/float(captureprotectdelay), 0.f, 1.f);
+                    part_icon(flagpos, textureload(hud::progringtex, 3), 4, blend, 0, 0, 1, colour, (lastmillis%1000)/1000.f, 0.1f);
+                    part_icon(flagpos, textureload(hud::progresstex, 3), 4, 0.25f*blend, 0, 0, 1, colour);
+                    part_icon(flagpos, textureload(hud::progresstex, 3), 4, blend, 0, 0, 1, colour, 0, wait);
+                    //flagpos.z += 0.5f;
+                    //defformatstring(str)("<huge>%d%%", int(wait*100.f)); part_textcopy(flagpos, str, PART_TEXT, 1, 0xFFFFFF, 2, 1);
+                    //flagpos.z += 3;
                 }
             }
-            vec loc(f.spawnloc);
-            loc.z += enttype[AFFINITY].radius*2/3;
-            defformatstring(info)("<super>%s %s", TEAM(f.team, name), !f.owner && !f.droptime ? "flag" : "base");
-            part_textcopy(loc, info, PART_TEXT, 1, TEAM(f.team, colour), 2, 1);
-            loc.z += 2.5f;
-            if(f.owner) part_icon(loc, textureload(hud::flagtakentex, 3), 2, 1, 0, 0, 1, TEAM(f.owner->team, colour));
-            else if(f.droptime) part_icon(loc, textureload(hud::flagdroptex, 3), 2, 1, 0, 0, 1, 0x28FFFF);
-            else part_icon(loc, textureload(hud::teamtexname(f.team), 3), 2, 1, 0, 0, 1, TEAM(f.team, colour));
-            loc.z += 2.5f;
-            if(!game::intermission && (f.droptime || (m_gsp3(game::gamemode, game::mutators) && f.taketime && f.owner && f.owner->team != f.team)))
+            rendermodel(&f.baselight, "props/point", ANIM_MAPMODEL|ANIM_LOOP, f.render, f.yaw, 0, 0, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED, NULL, NULL, 0, 0, 1);
+            vec above = f.above;
+            above.z += !f.owner && !f.droptime ? enttype[AFFINITY].radius*2/3 : 3;
+            blend = camera1->o.distrange(above, enttype[AFFINITY].radius, enttype[AFFINITY].radius/8);
+            defformatstring(info)("<super>%s base", TEAM(f.team, name));
+            part_textcopy(above, info, PART_TEXT, 1, TEAM(f.team, colour), 2, blend);
+            above.z += 2.5f;
+            if(gs_playing(game::gamestate) && (f.droptime || (m_gsp3(game::gamemode, game::mutators) && f.taketime && f.owner && f.owner->team != f.team)))
             {
-                part_icon(loc, textureload(hud::progresstex, 3), 3, 1, 0, 0, 1, pcolour, (lastmillis%1000)/1000.f, 0.1f);
-                part_icon(loc, textureload(hud::progresstex, 3), 2, 0.25f, 0, 0, 1, pcolour);
-                part_icon(loc, textureload(hud::progresstex, 3), 2, 1, 0, 0, 1, pcolour, 0, wait);
-                loc.z += 1.f;
-                defformatstring(str)("<huge>%d%%", int(wait*100.f)); part_textcopy(loc, str, PART_TEXT, 1, 0xFFFFFF, 2, 1);
-                loc.z += 1.5f;
+                part_icon(above, textureload(hud::progringtex, 3), 4, blend, 0, 0, 1, colour, (lastmillis%1000)/1000.f, 0.1f);
+                part_icon(above, textureload(hud::progresstex, 3), 4, 0.25f*blend, 0, 0, 1, colour);
+                part_icon(above, textureload(hud::progresstex, 3), 4, blend, 0, 0, 1, colour, 0, wait);
+                above.z += 3;
             }
+            if(f.owner) part_icon(above, textureload(hud::flagtakentex, 3), 2, blend, 0, 0, 1, TEAM(f.owner->team, colour));
+            else if(f.droptime) part_icon(above, textureload(hud::flagdroptex, 3), 2, blend, 0, 0, 1, 0x28FFFF);
+            else part_icon(above, textureload(hud::teamtexname(f.team), 3), 2, blend, 0, 0, 1, TEAM(f.team, colour));
         }
     }
 
@@ -302,9 +352,8 @@ namespace capture
         loopv(st.flags)
         {
             capturestate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent)) continue;
             if(f.owner || f.droptime)
-                adddynlight(vec(f.spawnloc).add(vec(0, 0, enttype[AFFINITY].radius/2)), enttype[AFFINITY].radius, vec::hexcolor(TEAM(f.team, colour)), 0, 0, DL_KEEP);
+                adddynlight(vec(f.above).add(vec(0, 0, enttype[AFFINITY].radius/2)), enttype[AFFINITY].radius, vec::hexcolor(TEAM(f.team, colour)), 0, 0, DL_KEEP);
             adddynlight(vec(f.pos(true)).add(vec(0, 0, enttype[AFFINITY].radius/2)), enttype[AFFINITY].radius, vec::hexcolor(TEAM(f.team, colour)), 0, 0, DL_KEEP);
         }
     }
@@ -321,7 +370,8 @@ namespace capture
             gameentity &e = *(gameentity *)entities::ents[i];
             if(!m_check(e.attrs[3], e.attrs[4], game::gamemode, game::mutators) || !isteam(game::gamemode, game::mutators, e.attrs[0], T_FIRST))
                 continue;
-            st.addaffinity(e.o, e.attrs[0], i);
+            st.addaffinity(e.o, e.attrs[0], e.attrs[1], e.attrs[2]);
+            if(st.flags.length() >= MAXPARAMS) break;
         }
     }
 
@@ -333,7 +383,8 @@ namespace capture
         {
             capturestate::flag &f = st.flags[i];
             putint(p, f.team);
-            putint(p, f.ent);
+            putint(p, f.yaw);
+            putint(p, f.pitch);
             loopj(3) putint(p, int(f.spawnloc[j]*DMF));
         }
     }
@@ -349,7 +400,7 @@ namespace capture
         while(st.flags.length() > numflags) st.flags.pop();
         loopi(numflags)
         {
-            int team = getint(p), ent = getint(p), owner = getint(p), dropped = 0;
+            int team = getint(p), yaw = getint(p), pitch = getint(p), owner = getint(p), dropped = 0, dropoffset = 0;
             vec spawnloc(0, 0, 0), droploc(0, 0, 0), inertia(0, 0, 0);
             loopj(3) spawnloc[j] = getint(p)/DMF;
             if(owner < 0)
@@ -357,18 +408,25 @@ namespace capture
                 dropped = getint(p);
                 if(dropped)
                 {
+                    dropoffset = getint(p);
                     loopj(3) droploc[j] = getint(p)/DMF;
                     loopj(3) inertia[j] = getint(p)/DMF;
                 }
             }
             if(p.overread()) break;
+            if(i >= MAXPARAMS) continue;
             while(!st.flags.inrange(i)) st.flags.add();
             capturestate::flag &f = st.flags[i];
+            f.reset();
             f.team = team;
-            f.ent = ent;
-            f.spawnloc = spawnloc;
-            if(owner >= 0) st.takeaffinity(i, game::getclient(owner), lastmillis);
-            else if(dropped) st.dropaffinity(i, droploc, inertia, lastmillis);
+            f.yaw = yaw;
+            f.pitch = pitch;
+            f.spawnloc = f.render = f.above = spawnloc;
+            f.render.z += 2;
+            physics::droptofloor(f.render);
+            if(f.render.z >= f.above.z-1) f.above.z += f.render.z-(f.above.z-1);
+            if(owner >= 0) st.takeaffinity(i, game::newclient(owner), lastmillis);
+            else if(dropped) st.dropaffinity(i, droploc, inertia, lastmillis, dropoffset);
         }
     }
 
@@ -376,8 +434,8 @@ namespace capture
     {
         if(!st.flags.inrange(i)) return;
         capturestate::flag &f = st.flags[i];
-        game::announcef(S_V_FLAGDROP, CON_INFO, d, true, "\fa%s dropped the the %s flag", game::colourname(d), game::colourteam(f.team, "flagtex"));
-        st.dropaffinity(i, droploc, inertia, lastmillis);
+        game::announcef(S_V_FLAGDROP, CON_SELF, d, true, "\fa%s dropped the the %s flag", game::colourname(d), game::colourteam(f.team, "flagtex"));
+        st.dropaffinity(i, droploc, inertia, lastmillis, target);
     }
 
     void removeplayer(gameent *d)
@@ -385,7 +443,7 @@ namespace capture
         loopv(st.flags) if(st.flags[i].owner == d)
         {
             capturestate::flag &f = st.flags[i];
-            st.dropaffinity(i, f.owner->feetpos(1), f.owner->vel, lastmillis);
+            st.dropaffinity(i, f.owner->feetpos(capturedropheight), f.owner->vel, lastmillis);
         }
     }
 
@@ -416,9 +474,8 @@ namespace capture
     {
         if(!st.flags.inrange(i)) return;
         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, CON_INFO, d, true, "\fa%s returned the %s flag (time taken: \fs\fc%s\fS)", game::colourname(d), game::colourteam(f.team, "flagtex"), timestr(lastmillis-(m_gsp1(game::gamemode, game::mutators) ? f.droptime : f.taketime)));
-        entities::execlink(NULL, f.ent, false);
+        affinityeffect(i, d->team, d->feetpos(), f.above, m_gsp(game::gamemode, game::mutators) ? 3 : 2, "RETURNED");
+        game::announcef(S_V_FLAGRETURN, CON_SELF, d, true, "\fa%s returned the %s flag (time taken: \fs\fc%s\fS)", game::colourname(d), game::colourteam(f.team, "flagtex"), timestr(m_gsp1(game::gamemode, game::mutators) ? f.dropleft(lastmillis, capturestore) : lastmillis-f.taketime, 1));
         st.returnaffinity(i, lastmillis);
     }
 
@@ -428,10 +485,9 @@ namespace capture
         capturestate::flag &f = st.flags[i];
         if(value > 0)
         {
-            affinityeffect(i, T_NEUTRAL, f.droploc, f.spawnloc, 3, "RESET");
-            game::announcef(S_V_FLAGRESET, CON_INFO, NULL, true, "\fathe %s flag has been reset", game::colourteam(f.team, "flagtex"));
+            affinityeffect(i, T_NEUTRAL, f.droploc, f.above, 3, "RESET");
+            game::announcef(S_V_FLAGRESET, CON_SELF, NULL, true, "\fathe %s flag has been reset", game::colourteam(f.team, "flagtex"));
         }
-        entities::execlink(NULL, f.ent, false);
         st.returnaffinity(i, lastmillis);
     }
 
@@ -442,14 +498,12 @@ namespace capture
         if(st.flags.inrange(goal))
         {
             capturestate::flag &g = st.flags[goal];
-            affinityeffect(goal, d->team, g.spawnloc, f.spawnloc, 3, "CAPTURED");
-            entities::execlink(NULL, g.ent, false);
+            affinityeffect(goal, d->team, g.above, f.above, 3, "CAPTURED");
         }
-        else affinityeffect(goal, d->team, f.pos(true), f.spawnloc, 3, "CAPTURED");
-        entities::execlink(NULL, f.ent, false);
+        else affinityeffect(goal, d->team, f.pos(true), f.above, 3, "CAPTURED");
         hud::teamscore(d->team).total = score;
         defformatstring(fteam)("%s", game::colourteam(f.team, "flagtex"));
-        game::announcef(S_V_FLAGSCORE, CON_INFO, d, true, "\fa%s captured the %s flag for team %s (score: \fs\fc%d\fS, time taken: \fs\fc%s\fS)", game::colourname(d), fteam, game::colourteam(d->team), score, timestr(lastmillis-f.taketime));
+        game::announcef(S_V_FLAGSCORE, CON_SELF, d, true, "\fa%s captured the %s flag for team %s (score: \fs\fc%d\fS, time taken: \fs\fc%s\fS)", game::colourname(d), fteam, game::colourteam(d->team), score, timestr(lastmillis-f.taketime, 1));
         st.returnaffinity(relay, lastmillis);
     }
 
@@ -457,45 +511,50 @@ namespace capture
     {
         if(!st.flags.inrange(i)) return;
         capturestate::flag &f = st.flags[i];
-        d->action[AC_AFFINITY] = false;
-        d->actiontime[AC_AFFINITY] = 0;
         playsound(S_CATCH, d->o, d);
         affinityeffect(i, d->team, d->feetpos(), f.pos(true), 1, f.team == d->team ? "SECURED" : "TAKEN");
-        game::announcef(f.team == d->team ? S_V_FLAGSECURED : S_V_FLAGPICKUP, CON_INFO, d, true, "\fa%s %s the %s flag", game::colourname(d), f.team == d->team ? "secured" : (f.droptime ? "picked up" : "stole"), game::colourteam(f.team, "flagtex"));
-        entities::execlink(NULL, f.ent, false);
+        game::announcef(f.team == d->team ? S_V_FLAGSECURED : S_V_FLAGPICKUP, CON_SELF, d, true, "\fa%s %s the %s flag", game::colourname(d), f.team == d->team ? "secured" : (f.droptime ? "picked up" : "stole"), game::colourteam(f.team, "flagtex"));
         st.takeaffinity(i, d, lastmillis);
+        if(d->ai) aihomerun(d, d->ai->state.last());
     }
 
-    void checkaffinity(dynent *e)
+    void checkaffinity(gameent *d, int i)
     {
-        if(e->state != CS_ALIVE || !gameent::is(e)) return;
-        gameent *d = (gameent *)e;
-        vec o = d->feetpos();
+        if(canpickup(d, i))
+        {
+            client::addmsg(N_TAKEAFFIN, "ri2", d->clientnum, i);
+            st.flags[i].pickuptime = lastmillis;
+            d->action[AC_AFFINITY] = false;
+        }
+    }
+
+    void update()
+    {
+        gameent *d = NULL;
+        int numdyn = game::numdynents();
+        loopj(numdyn) if(((d = (gameent *)game::iterdynents(j))) && d->state == CS_ALIVE && (d == game::player1 || d->ai)) dropaffinity(d);
         loopv(st.flags)
         {
             capturestate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent) || f.owner) continue;
+            if(f.owner) continue;
             if(f.droptime)
             {
                 f.droploc = f.pos();
                 if(f.lastowner && (f.lastowner == game::player1 || f.lastowner->ai) && f.proj && (!f.movetime || totalmillis-f.movetime >= 40))
                 {
                     f.inertia = f.proj->vel;
-                    f.movetime = totalmillis;
+                    f.movetime = totalmillis-(totalmillis%40);
                     client::addmsg(N_MOVEAFFIN, "ri8", f.lastowner->clientnum, i, int(f.droploc.x*DMF), int(f.droploc.y*DMF), int(f.droploc.z*DMF), int(f.inertia.x*DMF), int(f.inertia.y*DMF), int(f.inertia.z*DMF));
                 }
             }
-            if(f.pickuptime && lastmillis-f.pickuptime <= 1000) continue;
-            if(f.team == d->team && !m_gsp3(game::gamemode, game::mutators) && (m_gsp2(game::gamemode, game::mutators) || !f.droptime)) continue;
-            if(f.lastowner == d && f.droptime && (capturepickupdelay < 0 || lastmillis-f.droptime <= capturepickupdelay))
-                continue;
-            if(o.dist(f.pos()) <= enttype[AFFINITY].radius*2/3)
-            {
-                client::addmsg(N_TAKEAFFIN, "ri2", d->clientnum, i);
-                f.pickuptime = lastmillis;
-            }
+            loopj(numdyn) if(((d = (gameent *)game::iterdynents(j))) && d->state == CS_ALIVE && (d == game::player1 || d->ai)) checkaffinity(d, i);
         }
-        dropaffinity(d);
+    }
+
+    vec &aiflagpos(gameent *d, capturestate::flag &f)
+    {
+        if(f.droptime || f.owner != d) return f.pos();
+        return f.spawnloc;
     }
 
     bool aihomerun(gameent *d, ai::aistate &b)
@@ -505,19 +564,24 @@ namespace capture
             vec pos = d->feetpos();
             loopk(2)
             {
-                int goal = -1;
+                int closest = -1;
+                float closedist = 1e16f;
                 loopv(st.flags)
                 {
-                    capturestate::flag &g = st.flags[i];
-                    if(g.team == ai::owner(d) && (k || ((!g.owner || g.owner == d) && !g.droptime)) &&
-                        (!st.flags.inrange(goal) || g.pos().squaredist(pos) < st.flags[goal].pos().squaredist(pos)))
+                    capturestate::flag &f = st.flags[i];
+                    if(f.team == d->team && (k || ((!f.owner || f.owner == d) && !f.droptime)))
                     {
-                        goal = i;
+                        float dist = aiflagpos(d, f).squaredist(pos);
+                        if(closest < 0 || dist < closedist)
+                        {
+                            closest = i;
+                            closedist = dist;
+                        }
                     }
                 }
-                if(st.flags.inrange(goal) && ai::makeroute(d, b, st.flags[goal].pos()))
+                if(st.flags.inrange(closest) && ai::makeroute(d, b, aiflagpos(d, st.flags[closest])))
                 {
-                    d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_AFFINITY, goal);
+                    d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_AFFINITY, closest, ai::AI_A_HASTE);
                     return true;
                 }
             }
@@ -525,22 +589,15 @@ namespace capture
 	    if(b.type == ai::AI_S_PURSUE && b.targtype == ai::AI_T_NODE) return true; // we already did this..
 		if(ai::randomnode(d, b, ai::ALERTMIN, 1e16f))
 		{
-            d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_NODE, d->ai->route[0]);
+            d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_NODE, d->ai->route[0], ai::AI_A_HASTE);
             return true;
 		}
         return false;
     }
 
-    int aiowner(gameent *d)
-    {
-        loopv(st.flags) if(entities::ents.inrange(st.flags[i].ent) && entities::ents[d->aientity]->links.find(st.flags[i].ent) >= 0)
-            return st.flags[i].team;
-        return d->team;
-    }
-
     bool aicheck(gameent *d, ai::aistate &b)
     {
-        if(d->aitype == AI_BOT)
+        if(d->actortype == A_BOT)
         {
             static vector<int> taken; taken.setsize(0);
             loopv(st.flags)
@@ -550,15 +607,15 @@ namespace capture
                 {
                     if(!m_gsp3(game::gamemode, game::mutators)) return aihomerun(d, b);
                 }
-                else if(g.team == ai::owner(d) && (m_gsp3(game::gamemode, game::mutators) || (g.owner && ai::owner(g.owner) != ai::owner(d)) || g.droptime))
+                else if(g.team == d->team && (m_gsp3(game::gamemode, game::mutators) || (g.owner && g.owner->team != d->team) || g.droptime))
                     taken.add(i);
             }
             if(!ai::badhealth(d)) while(!taken.empty())
             {
                 int flag = taken.length() > 2 ? rnd(taken.length()) : 0;
-                if(ai::makeroute(d, b, st.flags[taken[flag]].pos()))
+                if(ai::makeroute(d, b, aiflagpos(d, st.flags[taken[flag]])))
                 {
-                    d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_AFFINITY, taken[flag]);
+                    d->ai->switchstate(b, ai::AI_S_PURSUE, ai::AI_T_AFFINITY, taken[flag], ai::AI_A_HASTE);
                     return true;
                 }
                 else taken.remove(flag);
@@ -573,20 +630,20 @@ namespace capture
         loopvj(st.flags)
         {
             capturestate::flag &f = st.flags[j];
-            bool home = f.team == ai::owner(d);
-            if(d->aitype == AI_BOT && m_duke(game::gamemode, game::mutators) && home) continue;
+            bool home = f.team == d->team;
+            if(d->actortype == A_BOT && m_duke(game::gamemode, game::mutators) && home) continue;
             static vector<int> targets; // build a list of others who are interested in this
             targets.setsize(0);
-            bool regen = d->aitype != AI_BOT || f.team == T_NEUTRAL || m_gsp3(game::gamemode, game::mutators) || !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
-            ai::checkothers(targets, d, home || d->aitype != AI_BOT ? ai::AI_S_DEFEND : ai::AI_S_PURSUE, ai::AI_T_AFFINITY, j, true);
-            if(d->aitype == AI_BOT)
+            bool regen = d->actortype != A_BOT || f.team == T_NEUTRAL || m_gsp3(game::gamemode, game::mutators) || !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
+            ai::checkothers(targets, d, home || d->actortype != A_BOT ? ai::AI_S_DEFEND : ai::AI_S_PURSUE, ai::AI_T_AFFINITY, j, true);
+            if(d->actortype == A_BOT)
             {
                 gameent *e = NULL;
                 int numdyns = game::numdynents();
                 float mindist = enttype[AFFINITY].radius*4; mindist *= mindist;
-                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && ai::owner(d) == ai::owner(e))
+                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && d->team == e->team)
                 {
-                    if(targets.find(e->clientnum) < 0 && (f.owner == e || e->feetpos().squaredist(f.pos()) <= mindist))
+                    if(targets.find(e->clientnum) < 0 && (f.owner == e || e->feetpos().squaredist(aiflagpos(d, f)) <= mindist))
                         targets.add(e->clientnum);
                 }
             }
@@ -594,12 +651,12 @@ namespace capture
             {
                 bool guard = false;
                 if(f.owner || f.droptime || targets.empty()) guard = true;
-                else if(d->hasweap(d->ai->weappref, m_weapon(game::gamemode, game::mutators)))
+                else if(d->hasweap(ai::weappref(d), m_weapon(game::gamemode, game::mutators)))
                 { // see if we can relieve someone who only has a piece of crap
                     gameent *t;
                     loopvk(targets) if((t = game::getclient(targets[k])))
                     {
-                        if((t->ai && !t->hasweap(t->ai->weappref, m_weapon(game::gamemode, game::mutators))) || (!t->ai && t->weapselect < W_OFFSET))
+                        if((t->ai && !t->hasweap(ai::weappref(t), m_weapon(game::gamemode, game::mutators))) || (!t->ai && t->weapselect < W_OFFSET))
                         {
                             guard = true;
                             break;
@@ -610,12 +667,13 @@ namespace capture
                 { // defend the flag
                     ai::interest &n = interests.add();
                     n.state = ai::AI_S_DEFEND;
-                    n.node = ai::closestwaypoint(f.pos(), ai::CLOSEDIST, true);
+                    n.node = ai::closestwaypoint(aiflagpos(d, f), ai::CLOSEDIST, true);
                     n.target = j;
                     n.targtype = ai::AI_T_AFFINITY;
-                    n.score = pos.squaredist(f.pos())/(!regen ? 100.f : 1.f);
+                    n.score = pos.squaredist(aiflagpos(d, f))/(!regen ? 100.f : 1.f);
                     n.tolerance = 0.25f;
                     n.team = true;
+                    n.acttype = ai::AI_A_PROTECT;
                 }
             }
             else
@@ -623,11 +681,11 @@ namespace capture
                 if(targets.empty())
                 { // attack the flag
                     ai::interest &n = interests.add();
-                    n.state = d->aitype == AI_BOT ? ai::AI_S_PURSUE : ai::AI_S_DEFEND;
-                    n.node = ai::closestwaypoint(f.pos(), ai::CLOSEDIST, true);
+                    n.state = d->actortype == A_BOT ? ai::AI_S_PURSUE : ai::AI_S_DEFEND;
+                    n.node = ai::closestwaypoint(aiflagpos(d, f), ai::CLOSEDIST, true);
                     n.target = j;
                     n.targtype = ai::AI_T_AFFINITY;
-                    n.score = pos.squaredist(f.pos());
+                    n.score = pos.squaredist(aiflagpos(d, f));
                     n.tolerance = 0.25f;
                     n.team = true;
                 }
@@ -637,7 +695,7 @@ namespace capture
                     loopvk(targets) if((t = game::getclient(targets[k])))
                     {
                         ai::interest &n = interests.add();
-                        bool team = ai::owner(d) == ai::owner(t);
+                        bool team = d->team == t->team;
                         n.state = team ? ai::AI_S_DEFEND : ai::AI_S_PURSUE;
                         n.node = t->lastnode;
                         n.target = t->clientnum;
@@ -645,6 +703,7 @@ namespace capture
                         n.score = d->o.squaredist(t->o);
                         n.tolerance = 0.25f;
                         n.team = team;
+                        if(team) n.acttype = ai::AI_A_PROTECT;
                     }
                 }
             }
@@ -653,37 +712,43 @@ namespace capture
 
     bool aidefense(gameent *d, ai::aistate &b)
     {
-        if(d->aitype == AI_BOT)
+        if(d->actortype == A_BOT)
         {
             if(!m_gsp3(game::gamemode, game::mutators)) loopv(st.flags) if(st.flags[i].owner == d) return aihomerun(d, b);
-            if(d->aitype == AI_BOT && m_duke(game::gamemode, game::mutators)) return false;
+            if(d->actortype == A_BOT && m_duke(game::gamemode, game::mutators) && b.owner < 0) return false;
         }
         if(st.flags.inrange(b.target))
         {
             capturestate::flag &f = st.flags[b.target];
-            if(f.team == ai::owner(d) && f.owner && ai::owner(f.owner) != ai::owner(d))
-                return ai::violence(d, b, f.owner, 4);
-            int walk = f.owner && ai::owner(f.owner) != ai::owner(d) ? 1 : 0;
-            if(d->aitype == AI_BOT)
+            if(f.team == d->team && f.owner && f.owner->team != d->team && ai::violence(d, b, f.owner, 4)) return true;
+            int walk = f.owner && f.owner->team != d->team ? 1 : 0;
+            if(d->actortype == A_BOT)
             {
-                bool regen = !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
-                if(regen && lastmillis-b.millis >= (201-d->skill)*33)
+                if((!m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model)) && lastmillis-b.millis >= (201-d->skill)*33)
                 {
-                    static vector<int> targets; // build a list of others who are interested in this
-                    targets.setsize(0);
-                    ai::checkothers(targets, d, ai::AI_S_DEFEND, ai::AI_T_AFFINITY, b.target, true);
-                    gameent *e = NULL;
-                    int numdyns = game::numdynents();
-                    float mindist = enttype[AFFINITY].radius*4; mindist *= mindist;
-                    loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && ai::owner(d) == ai::owner(e))
+                    if(b.owner < 0)
                     {
-                        if(targets.find(e->clientnum) < 0 && (f.owner == e || e->feetpos().squaredist(f.pos()) <= mindist))
-                            targets.add(e->clientnum);
-                    }
-                    if(!targets.empty())
-                    {
-                        d->ai->tryreset = true; // re-evaluate so as not to herd
-                        return true;
+                        static vector<int> targets; // build a list of others who are interested in this
+                        targets.setsize(0);
+                        ai::checkothers(targets, d, ai::AI_S_DEFEND, ai::AI_T_AFFINITY, b.target, true);
+                        gameent *e = NULL;
+                        int numdyns = game::numdynents();
+                        float mindist = enttype[AFFINITY].radius*4; mindist *= mindist;
+                        loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && d->team == e->team)
+                        {
+                            if(targets.find(e->clientnum) < 0 && (f.owner == e || e->feetpos().squaredist(aiflagpos(d, f)) <= mindist))
+                                targets.add(e->clientnum);
+                        }
+                        if(!targets.empty())
+                        {
+                            d->ai->tryreset = true; // re-evaluate so as not to herd
+                            return true;
+                        }
+                        else
+                        {
+                            walk = 2;
+                            b.millis = lastmillis;
+                        }
                     }
                     else
                     {
@@ -696,35 +761,39 @@ namespace capture
                 loopv(st.flags)
                 { // get out of the way of the returnee!
                     capturestate::flag &g = st.flags[i];
-                    if(pos.squaredist(g.pos()) <= mindist)
+                    if(pos.squaredist(aiflagpos(d, g)) <= mindist)
                     {
-                        if(g.owner && ai::owner(g.owner) == ai::owner(d) && !walk) walk = 1;
-                        if(g.droptime && ai::makeroute(d, b, g.pos())) return true;
+                        if(g.owner && g.owner->team == d->team && !walk) walk = 1;
+                        if(g.droptime && ai::makeroute(d, b, aiflagpos(d, g))) return true;
                     }
                 }
             }
-            return ai::defense(d, b, f.pos(), f.owner ? ai::CLOSEDIST : float(enttype[AFFINITY].radius), f.owner ? ai::ALERTMIN : float(enttype[AFFINITY].radius*walk*16), walk);
+            return ai::defense(d, b, aiflagpos(d, f), enttype[AFFINITY].radius, enttype[AFFINITY].radius*walk*8, walk);
         }
         return false;
     }
 
     bool aipursue(gameent *d, ai::aistate &b)
     {
-        if(st.flags.inrange(b.target) && d->aitype == AI_BOT)
+        if(st.flags.inrange(b.target) && d->actortype == A_BOT)
         {
             capturestate::flag &f = st.flags[b.target];
-            if(f.team != ai::owner(d))
+            if(f.team != d->team)
             {
                 if(f.owner)
                 {
                     if(d == f.owner) return aihomerun(d, b);
-                    else if(ai::owner(d) != ai::owner(f.owner)) return ai::violence(d, b, f.owner, 4);
-                    else return ai::defense(d, b, f.pos());
+                    else if(d->team != f.owner->team) return ai::violence(d, b, f.owner, 4);
+                    else return ai::defense(d, b, aiflagpos(d, f));
                 }
-                return ai::makeroute(d, b, f.pos());
+                return ai::makeroute(d, b, aiflagpos(d, f));
+            }
+            else loopv(st.flags) if(st.flags[i].owner == d && ai::makeroute(d, b, aiflagpos(d, f)))
+            {
+                b.acttype = ai::AI_A_HASTE;
+                return true;
             }
-            else if(!m_gsp3(game::gamemode, game::mutators))
-                loopv(st.flags) if(st.flags[i].owner == d) return ai::makeroute(d, b, f.owner == d ? f.spawnloc : f.pos());
+            else if(b.owner >= 0) return ai::makeroute(d, b, aiflagpos(d, f));
         }
         return false;
     }
diff --git a/src/game/capture.h b/src/game/capture.h
index 89d9a10..21fbc6b 100644
--- a/src/game/capture.h
+++ b/src/game/capture.h
@@ -1,45 +1,45 @@
 #ifdef GAMESERVER
 #define capturedelay (m_gsp2(gamemode, mutators) ? G(capturedefenddelay) : G(captureresetdelay))
+#define capturestore (G(captureresetstore)&((m_gsp1(gamemode, mutators) ? 1 : 0)|(m_gsp2(gamemode, mutators) ? 2 : 0)|(m_gsp3(gamemode, mutators) ? 4 : 0)|(!m_gsp1(gamemode, mutators) && !m_gsp2(gamemode, mutators) && !m_gsp3(gamemode, mutators) ? 8 : 0)))
 #define capturestate captureservstate
 #else
 #define capturedelay (m_gsp2(game::gamemode, game::mutators) ? G(capturedefenddelay) : G(captureresetdelay))
+#define capturestore (G(captureresetstore)&((m_gsp1(game::gamemode, game::mutators) ? 1 : 0)|(m_gsp2(game::gamemode, game::mutators) ? 2 : 0)|(m_gsp3(game::gamemode, game::mutators) ? 4 : 0)|(!m_gsp1(game::gamemode, game::mutators) && !m_gsp2(game::gamemode, game::mutators) && !m_gsp3(game::gamemode, game::mutators) ? 8 : 0)))
 #endif
 struct capturestate
 {
     struct flag
     {
         vec droploc, inertia, spawnloc;
-        int team, ent, droptime, taketime;
+        int team, yaw, pitch, droptime, taketime, dropoffset;
 #ifdef GAMESERVER
-        int owner, lastowner;
+        int owner, lastowner, nextreset;
         vector<int> votes;
 #else
         gameent *owner, *lastowner;
         projent *proj;
         int displaytime, pickuptime, movetime, viewtime, interptime;
-        vec viewpos, interppos;
+        vec viewpos, interppos, render, above;
+        entitylight light, baselight;
 #endif
 
-        flag()
-#ifndef GAMESERVER
-          : ent(-1)
-#endif
-        { reset(); }
+        flag() { reset(); }
 
         void reset()
         {
             inertia = vec(0, 0, 0);
             droploc = spawnloc = vec(-1, -1, -1);
 #ifdef GAMESERVER
+            nextreset = 0;
             owner = lastowner = -1;
             votes.shrink(0);
 #else
             owner = lastowner = NULL;
             proj = NULL;
-            displaytime = pickuptime = movetime = 0;
+            displaytime = pickuptime = movetime = viewtime = interptime = 0;
 #endif
             team = T_NEUTRAL;
-            taketime = droptime = 0;
+            yaw = pitch = taketime = droptime = dropoffset = 0;
         }
 
 #ifndef GAMESERVER
@@ -47,7 +47,7 @@ struct capturestate
         {
             if(owner) return owner->waist;
             if(droptime) return proj ? proj->o : droploc;
-            return spawnloc;
+            return above;
         }
 
         vec &pos(bool view = false)
@@ -68,6 +68,11 @@ struct capturestate
             return position();
         }
 #endif
+
+        int dropleft(int t, bool b)
+        {
+            return (t-droptime)+(b ? dropoffset : 0);
+        }
     };
     vector<flag> flags;
 
@@ -76,13 +81,20 @@ struct capturestate
         flags.shrink(0);
     }
 
-    void addaffinity(const vec &o, int team, int ent)
+    void addaffinity(const vec &o, int team, int yaw, int pitch)
     {
         flag &f = flags.add();
         f.reset();
         f.team = team;
         f.spawnloc = o;
-        f.ent = ent;
+        f.yaw = yaw;
+        f.pitch = pitch;
+#ifndef GAMESERVER
+        f.render = f.above = o;
+        f.render.z += 2;
+        physics::droptofloor(f.render);
+        if(f.render.z >= f.above.z-1) f.above.z += f.render.z-(f.above.z-1);
+#endif
     }
 
 #ifndef GAMESERVER
@@ -118,6 +130,7 @@ struct capturestate
 #endif
     {
         flag &f = flags[i];
+        if(f.droptime) f.dropoffset += t-f.droptime;
 #ifndef GAMESERVER
         interp(i, t);
 #endif
@@ -139,9 +152,10 @@ struct capturestate
 #endif
     }
 
-    void dropaffinity(int i, const vec &o, const vec &p, int t)
+    void dropaffinity(int i, const vec &o, const vec &p, int t, int offset = -1)
     {
         flag &f = flags[i];
+        if(offset >= 0) f.dropoffset = offset;
 #ifndef GAMESERVER
         interp(i, t);
 #endif
@@ -153,8 +167,7 @@ struct capturestate
         f.owner = -1;
         f.votes.shrink(0);
 #else
-        f.pickuptime = 0;
-        f.movetime = t;
+        f.pickuptime =  f.movetime = 0;
         f.owner = NULL;
         destroy(i);
         create(i);
@@ -167,7 +180,7 @@ struct capturestate
 #ifndef GAMESERVER
         interp(i, t);
 #endif
-        f.droptime = f.taketime = 0;
+        f.droptime = f.taketime = f.dropoffset = 0;
 #ifdef GAMESERVER
         f.owner = -1;
         f.votes.shrink(0);
@@ -195,7 +208,7 @@ namespace capture
     extern void reset();
     extern void setup();
     extern void setscore(int team, int total);
-    extern void checkaffinity(dynent *e);
+    extern void update();
     extern void drawnotices(int w, int h, int &tx, int &ty, float blend);
     extern void drawblips(int w, int h, float blend);
     extern int drawinventory(int x, int y, int s, int m, float blend);
@@ -203,6 +216,7 @@ namespace capture
     extern void render();
     extern void adddynlights();
     extern int aiowner(gameent *d);
+    extern bool aihomerun(gameent *d, ai::aistate &b);
     extern void aifind(gameent *d, ai::aistate &b, vector<ai::interest> &interests);
     extern bool aicheck(gameent *d, ai::aistate &b);
     extern bool aidefense(gameent *d, ai::aistate &b);
diff --git a/src/game/capturemode.h b/src/game/capturemode.h
index b44d8c1..361f32d 100644
--- a/src/game/capturemode.h
+++ b/src/game/capturemode.h
@@ -4,7 +4,7 @@ struct captureservmode : capturestate, servmode
 
     captureservmode() : hasflaginfo(false) {}
 
-    void reset(bool empty)
+    void reset()
     {
         capturestate::reset();
         hasflaginfo = false;
@@ -12,10 +12,10 @@ struct captureservmode : capturestate, servmode
 
     void dropaffinity(clientinfo *ci, const vec &o, const vec &inertia = vec(0, 0, 0), int target = -1)
     {
-        if(!hasflaginfo || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || ci->state.actortype >= A_ENEMY) return;
         int numflags = 0, iterflags = 0;
         loopv(flags) if(flags[i].owner == ci->clientnum) numflags++;
-        vec dir = inertia, olddir = dir, v = o; v.z += 1;
+        vec dir = inertia, olddir = dir;
         if(numflags > 1 && dir.iszero())
         {
             dir.x = -sinf(RAD*ci->state.yaw);
@@ -31,27 +31,27 @@ struct captureservmode : capturestate, servmode
                 dir = vec(olddir).rotate_around_z(yaw*RAD);
                 iterflags++;
             }
-            ivec p(vec(v).mul(DMF)), q(vec(dir).mul(DMF));
-            sendf(-1, 1, "ri3i7", N_DROPAFFIN, ci->clientnum, -1, i, p.x, p.y, p.z, q.x, q.y, q.z);
-            capturestate::dropaffinity(i, v, dir, gamemillis);
+            ivec p(vec(o).mul(DMF)), q(vec(dir).mul(DMF));
+            sendf(-1, 1, "ri3i7", N_DROPAFFIN, ci->clientnum, flags[i].dropoffset, i, p.x, p.y, p.z, q.x, q.y, q.z);
+            capturestate::dropaffinity(i, o, dir, gamemillis);
         }
     }
 
     void leavegame(clientinfo *ci, bool disconnecting = false)
     {
-        if(!hasflaginfo) return;
-        dropaffinity(ci, ci->state.o, vec(ci->state.vel).add(ci->state.falling));
+        if(!canplay(hasflaginfo)) return;
+        dropaffinity(ci, ci->state.feetpos(G(capturedropheight)), vec(ci->state.vel).add(ci->state.falling));
     }
 
-    void dodamage(clientinfo *target, clientinfo *actor, int &damage, int &hurt, int &weap, int &flags, int &material, const ivec &hitpush)
+    void dodamage(clientinfo *m, clientinfo *v, int &damage, int &hurt, int &weap, int &flags, int &material, const ivec &hitpush, const ivec &hitvel, float dist)
     {
-        //if(weaptype[weap].melee) dropaffinity(target, target->state.o, vec(ci->state.vel).add(ci->state.falling));
+        //if(weaptype[weap].melee) dropaffinity(m, m->state.o, vec(ci->state.vel).add(ci->state.falling));
     }
 
-    void died(clientinfo *ci, clientinfo *actor)
+    void died(clientinfo *ci, clientinfo *v)
     {
-        if(!hasflaginfo) return;
-        dropaffinity(ci, ci->state.o, vec(ci->state.vel).add(ci->state.falling));
+        if(!canplay(hasflaginfo)) return;
+        dropaffinity(ci, ci->state.feetpos(G(capturedropheight)), vec(ci->state.vel).add(ci->state.falling));
     }
 
     int addscore(int team, int points = 1)
@@ -63,24 +63,25 @@ struct captureservmode : capturestate, servmode
 
     void moved(clientinfo *ci, const vec &oldpos, const vec &newpos)
     {
-        if(!hasflaginfo || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || ci->state.actortype >= A_ENEMY) return;
         if(G(capturethreshold) > 0 && oldpos.dist(newpos) >= G(capturethreshold))
             dropaffinity(ci, oldpos, vec(ci->state.vel).add(ci->state.falling));
         if(!m_gsp3(gamemode, mutators)) loopv(flags) if(flags[i].owner == ci->clientnum)
         {
+            flag &r = flags[i];
             loopvk(flags)
             {
                 flag &f = flags[k];
-                if(f.team == ci->team && (f.owner < 0 || (f.owner == ci->clientnum && i == k)) && !f.droptime && newpos.dist(f.spawnloc) <= enttype[AFFINITY].radius*2/3)
+                if(f.team == ci->team && (f.owner < 0 || (f.owner == ci->clientnum && i == k && gamemillis-f.taketime >= G(capturepickupdelay))) && !f.droptime && (!f.nextreset || r.team != ci->team) && newpos.dist(f.spawnloc) <= enttype[AFFINITY].radius*2/3)
                 {
                     capturestate::returnaffinity(i, gamemillis);
-                    givepoints(ci, G(capturepoints));
-                    if(flags[i].team != ci->team)
+                    if(r.team != ci->team)
                     {
                         int score = addscore(ci->team);
                         sendf(-1, 1, "ri5", N_SCOREAFFIN, ci->clientnum, i, k, score);
                         mutate(smuts, mut->scoreaffinity(ci));
-                        if(!m_balance(gamemode, mutators) && G(capturelimit) && score >= G(capturelimit))
+                        if(!m_nopoints(gamemode, mutators)) givepoints(ci, G(capturepoints));
+                        if(!m_balance(gamemode, mutators, teamspawns) && G(capturelimit) && score >= G(capturelimit))
                         {
                             ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fyscore limit has been reached");
                             startintermission();
@@ -94,32 +95,36 @@ struct captureservmode : capturestate, servmode
 
     void takeaffinity(clientinfo *ci, int i)
     {
-        if(!hasflaginfo || !flags.inrange(i) || ci->state.state!=CS_ALIVE || !ci->team || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || !flags.inrange(i) || ci->state.state != CS_ALIVE || !ci->team || ci->state.actortype >= A_ENEMY) return;
         flag &f = flags[i];
-        if(f.owner >= 0 || (f.team == ci->team && !m_gsp3(gamemode, mutators) && (m_gsp2(gamemode, mutators) || !f.droptime))) return;
-        if(f.lastowner == ci->clientnum && f.droptime && (G(capturepickupdelay) < 0 || lastmillis-f.droptime <= G(capturepickupdelay))) return;
+        if(f.owner >= 0 || (f.team == ci->team && (f.nextreset || m_gsp2(gamemode, mutators) || (m_gsp1(gamemode, mutators) && !f.droptime)))) return;
+        if(f.lastowner == ci->clientnum && f.droptime && gamemillis-f.droptime <= G(capturepickupdelay)) return;
         if(m_gsp1(gamemode, mutators) && f.team == ci->team)
         {
             capturestate::returnaffinity(i, gamemillis);
-            givepoints(ci, G(capturepoints));
+            if(!m_nopoints(gamemode, mutators)) givepoints(ci, G(capturepoints));
             sendf(-1, 1, "ri3", N_RETURNAFFIN, ci->clientnum, i);
         }
         else
         {
             capturestate::takeaffinity(i, ci->clientnum, gamemillis);
-            if(f.team != ci->team && (!f.droptime || f.lastowner != ci->clientnum)) givepoints(ci, G(capturepickuppoints));
+            if(!m_nopoints(gamemode, mutators) && ((f.team != ci->team && !f.droptime) || f.lastowner != ci->clientnum)) givepoints(ci, G(capturepickuppoints));
             sendf(-1, 1, "ri3", N_TAKEAFFIN, ci->clientnum, i);
         }
     }
 
     void resetaffinity(clientinfo *ci, int i)
     {
-        if(!hasflaginfo || !flags.inrange(i) || ci->state.ownernum >= 0) return;
+        if(!canplay(hasflaginfo) || !flags.inrange(i) || ci->state.ownernum >= 0) return;
         flag &f = flags[i];
         if(f.owner >= 0 || !f.droptime || f.votes.find(ci->clientnum) >= 0) return;
         f.votes.add(ci->clientnum);
-        if(f.votes.length() >= numclients()/2)
+        if(f.votes.length() >= int(floorf(numclients()*0.5f)))
         {
+            f.nextreset = gamemillis;
+            clientinfo *last = (clientinfo *)getinfo(f.lastowner);
+            if(last && last->team == f.team) f.nextreset += G(captureteampenalty);
+            else f.nextreset += G(captureresetpenalty);
             capturestate::returnaffinity(i, gamemillis);
             sendf(-1, 1, "ri3", N_RESETAFFIN, i, 2);
         }
@@ -127,7 +132,7 @@ struct captureservmode : capturestate, servmode
 
     void layout()
     {
-        if(!hasflaginfo) return;
+        if(!canplay(hasflaginfo)) return;
         loopv(flags) if(flags[i].owner >= 0 || flags[i].droptime)
         {
             capturestate::returnaffinity(i, gamemillis);
@@ -137,28 +142,29 @@ struct captureservmode : capturestate, servmode
 
     void update()
     {
-        if(!hasflaginfo) return;
+        if(!canplay(hasflaginfo)) return;
         loopv(flags)
         {
             flag &f = flags[i];
-            if(m_gsp3(gamemode, mutators) && f.owner >= 0 && f.taketime && gamemillis-f.taketime >= G(captureprotectdelay))
+            if(f.nextreset && gamemillis > f.nextreset) f.nextreset = 0;
+            if(m_gsp3(gamemode, mutators) && f.owner >= 0)
             {
                 clientinfo *ci = (clientinfo *)getinfo(f.owner);
-                if(f.team != ci->team)
+                if(f.team != ci->team && f.taketime && gamemillis-f.taketime >= G(captureprotectdelay))
                 {
                     capturestate::returnaffinity(i, gamemillis);
-                    givepoints(ci, G(capturepoints));
+                    if(!m_nopoints(gamemode, mutators)) givepoints(ci, G(capturepoints));
                     int score = addscore(ci->team);
                     sendf(-1, 1, "ri5", N_SCOREAFFIN, ci->clientnum, i, -1, score);
                     mutate(smuts, mut->scoreaffinity(ci));
-                    if(!m_balance(gamemode, mutators) && G(capturelimit) && score >= G(capturelimit))
+                    if(!m_balance(gamemode, mutators, teamspawns) && G(capturelimit) && score >= G(capturelimit))
                     {
                         ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fyscore limit has been reached");
                         startintermission();
                     }
                 }
             }
-            else if(f.owner < 0 && f.droptime && gamemillis-f.droptime >= capturedelay)
+            else if(f.owner < 0 && f.droptime && f.dropleft(gamemillis, capturestore) >= capturedelay)
             {
                 capturestate::returnaffinity(i, gamemillis);
                 sendf(-1, 1, "ri3", N_RESETAFFIN, i, 2);
@@ -182,7 +188,8 @@ struct captureservmode : capturestate, servmode
         {
             flag &f = flags[i];
             putint(p, f.team);
-            putint(p, f.ent);
+            putint(p, f.yaw);
+            putint(p, f.pitch);
             putint(p, f.owner);
             loopj(3) putint(p, int(f.spawnloc[j]*DMF));
             if(f.owner<0)
@@ -190,6 +197,7 @@ struct captureservmode : capturestate, servmode
                 putint(p, f.droptime);
                 if(f.droptime)
                 {
+                    putint(p, f.dropoffset);
                     loopj(3) putint(p, int(f.droploc[j]*DMF));
                     loopj(3) putint(p, int(f.inertia[j]*DMF));
                 }
@@ -208,7 +216,7 @@ struct captureservmode : capturestate, servmode
 
     void regen(clientinfo *ci, int &total, int &amt, int &delay)
     {
-        if(!hasflaginfo || !G(captureregenbuff) || !ci->state.lastbuff) return;
+        if(!canplay(hasflaginfo) || !G(captureregenbuff) || !ci->state.lastbuff) return;
         if(G(maxhealth)) total = max(m_maxhealth(gamemode, mutators, ci->state.model), total);
         if(ci->state.lastregen && G(captureregendelay)) delay = G(captureregendelay);
         if(G(captureregenextra)) amt += G(captureregenextra);
@@ -216,22 +224,30 @@ struct captureservmode : capturestate, servmode
 
     void checkclient(clientinfo *ci)
     {
-        if(!hasflaginfo || ci->state.state != CS_ALIVE || m_insta(gamemode, mutators)) return;
-        #define capturebuffx(a) (G(capturebuffing)&a && owner && owner->team == ci->team && ci->state.o.dist(owner->state.o) <= G(capturebuffarea))
-        #define capturebuff1    (G(capturebuffing)&1 && f.team == ci->team && (owner ? (owner->clientnum == ci->clientnum || capturebuffx(4)): (!f.droptime || m_gsp2(gamemode, mutators)) && ci->state.o.dist(f.droptime ? f.droploc : f.spawnloc) <= G(capturebuffarea)))
-        #define capturebuff2    (G(capturebuffing)&2 && f.team != ci->team && owner && (owner->clientnum == ci->clientnum || capturebuffx(8)))
+        if(!canplay(hasflaginfo) || ci->state.state != CS_ALIVE || m_insta(gamemode, mutators)) return;
+        bool buff = false;
         if(G(capturebuffing)) loopv(flags)
         {
             flag &f = flags[i];
             clientinfo *owner = f.owner >= 0 ? (clientinfo *)getinfo(f.owner) : NULL;
-            if(capturebuff1 || capturebuff2)
+            if(f.team == ci->team)
             {
-                if(!ci->state.lastbuff) sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 1);
-                ci->state.lastbuff = gamemillis;
-                return;
+                if((G(capturebuffing)&1 || G(capturebuffing)&2) && !owner && (!f.droptime || m_gsp2(gamemode, mutators) || G(capturebuffing)&2) && ci->state.o.dist(f.droptime ? f.droploc : f.spawnloc) <= G(capturebuffarea)) { buff = true; break; }
+                if(G(capturebuffing)&4 && owner && ci == owner) { buff = true; break; }
+                if(G(capturebuffing)&8 && owner && ci != owner && owner->team == ci->team && (G(capturebuffarea) > 0 ? ci->state.o.dist(owner->state.o) <= G(capturebuffarea) : true)) { buff = true; break; }
             }
+            else
+            {
+                if(G(capturebuffing)&16 && ci == owner) { buff = true; break; }
+                if(G(capturebuffing)&32 && owner && ci != owner && owner->team == ci->team && (G(capturebuffarea) > 0 ? ci->state.o.dist(owner->state.o) <= G(capturebuffarea) : true)) { buff = true; break; }
+            }
+        }
+        if(buff)
+        {
+            if(!ci->state.lastbuff) sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 1);
+            ci->state.lastbuff = gamemillis;
         }
-        if(ci->state.lastbuff && (!G(capturebuffing) || gamemillis-ci->state.lastbuff > G(capturebuffdelay)))
+        else if(ci->state.lastbuff && (!G(capturebuffing) || gamemillis-ci->state.lastbuff >= G(capturebuffdelay)))
         {
             ci->state.lastbuff = 0;
             sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 0);
@@ -240,11 +256,9 @@ struct captureservmode : capturestate, servmode
 
     void moveaffinity(clientinfo *ci, int cn, int id, const vec &o, const vec &inertia = vec(0, 0, 0))
     {
-        if(!flags.inrange(id)) return;
+        if(!canplay(hasflaginfo) || !flags.inrange(id)) return;
         flag &f = flags[id];
-        if(!f.droptime || f.owner >= 0) return;
-        clientinfo *co = f.lastowner >= 0 ? (clientinfo *)getinfo(f.lastowner) : NULL;
-        if(!co || co->clientnum != ci->clientnum) return;
+        if(!f.droptime || f.owner >= 0 || f.lastowner != ci->clientnum) return;
         f.droploc = o;
         f.inertia = inertia;
         //sendf(-1, 1, "ri9", N_MOVEAFFIN, ci->clientnum, id, int(f.droploc.x*DMF), int(f.droploc.y*DMF), int(f.droploc.z*DMF), int(f.inertia.x*DMF), int(f.inertia.y*DMF), int(f.inertia.z*DMF));
@@ -257,10 +271,11 @@ struct captureservmode : capturestate, servmode
         {
             loopi(numflags)
             {
-                int team = getint(p), ent = getint(p);
+                int team = getint(p), yaw = getint(p), pitch = getint(p);
                 vec o;
                 loopj(3) o[j] = getint(p)/DMF;
-                if(!hasflaginfo) addaffinity(o, team, ent);
+                if(p.overread()) break;
+                if(!hasflaginfo && i < MAXPARAMS) addaffinity(o, team, yaw, pitch);
             }
             if(!hasflaginfo)
             {
@@ -271,11 +286,11 @@ struct captureservmode : capturestate, servmode
         }
     }
 
-    int points(clientinfo *victim, clientinfo *actor)
+    int points(clientinfo *m, clientinfo *v)
     {
-        bool isteam = victim==actor || victim->team == actor->team;
-        int p = isteam ? -1 : 1, v = p;
-        loopv(flags) if(flags[i].owner == victim->clientnum) p += v;
+        bool isteam = m==v || m->team == v->team;
+        int p = isteam ? -1 : 1, q = p;
+        loopv(flags) if(flags[i].owner == m->clientnum) p += q;
         return p;
     }
 } capturemode;
diff --git a/src/game/client.cpp b/src/game/client.cpp
index 9e2d896..b74a82b 100644
--- a/src/game/client.cpp
+++ b/src/game/client.cpp
@@ -4,36 +4,44 @@ namespace client
 {
     bool sendplayerinfo = false, sendcrcinfo = false, sendgameinfo = false, isready = false, remote = false,
         demoplayback = false, needsmap = false, gettingmap = false;
-    int lastping = 0, sessionid = 0;
+    int lastping = 0, sessionid = 0, sessionver = 0, lastplayerinfo = 0;
     string connectpass = "";
     int needclipboard = -1;
 
+    SVAR(IDF_PERSIST, demolist, "");
+    VAR(0, demoendless, 0, 0, 1);
     VAR(IDF_PERSIST, showpresence, 0, 1, 2); // 0 = never show join/leave, 1 = show only during game, 2 = show when connecting/disconnecting
     VAR(IDF_PERSIST, showteamchange, 0, 1, 2); // 0 = never show, 1 = show only when switching between, 2 = show when entering match too
     VAR(IDF_PERSIST, showservervariables, 0, 0, 1); // determines if variables set by the server are printed to the console
+    VAR(IDF_PERSIST, showmapvotes, 0, 1, 3); // shows map votes, 1 = only mid-game (not intermision), 2 = at all times, 3 = verbose
+
+    VAR(IDF_PERSIST, checkpointannounce, 0, 5, 7); // 0 = never, &1 = active players, &2 = all players, &4 = all players in gauntlet
+    VAR(IDF_PERSIST, checkpointannouncefilter, 0, CP_ALL, CP_ALL); // which checkpoint types to announce for
 
     int state() { return game::player1->state; }
     ICOMMAND(0, getplayerstate, "", (), intret(state()));
+    ICOMMAND(0, isloadingmap, "", (), intret(lightmapping ? 2 : (game::maptime <= 0 ? 1 : 0)));
 
     int maxmsglen() { return G(messagelength); }
 
-    int otherclients(bool nospec)
+    int otherclients(bool self, bool nospec)
     {
-        int n = 0; // ai don't count
-        loopv(game::players) if(game::players[i] && game::players[i]->aitype == AI_NONE && (!nospec || game::players[i]->state != CS_SPECTATOR)) n++;
+        int n = self ? 1 : 0;
+        loopv(game::players) if(game::players[i] && game::players[i]->actortype == A_PLAYER && (!nospec || game::players[i]->state != CS_SPECTATOR)) n++;
         return n;
     }
+    ICOMMAND(0, getclientcount, "ii", (int *s, int *n), intret(otherclients(*s!=0, *n!=0)));
 
     int numplayers()
     {
         int n = 1; // count ourselves
-        loopv(game::players) if(game::players[i] && game::players[i]->aitype < AI_START) n++;
+        loopv(game::players) if(game::players[i] && game::players[i]->actortype < A_ENEMY) n++;
         return n;
     }
 
     int waiting(bool state)
     {
-        if(!connected(false) || !isready || game::maptime <= 0 || (state && needsmap && otherclients()))
+        if(!connected(false) || !isready || game::maptime <= 0 || (state && needsmap)) // && otherclients()
             return state && needsmap ? (gettingmap ? 3 : 2) : 1;
         return 0;
     }
@@ -94,7 +102,7 @@ namespace client
         if(found)
         {
             if(!mapvotes.empty()) mapvotes.sort(mapvote::compare);
-            if(msg && d == game::player1) conoutft(CON_EVENT, "%s cleared their previous vote", game::colourname(d));
+            if(msg && showmapvotes >= (d == game::player1 ? 2 : 3)) conoutft(CON_EVENT, "%s cleared their previous vote", game::colourname(d));
         }
     }
 
@@ -121,13 +129,12 @@ namespace client
         }
         m->players.add(d);
         mapvotes.sort(mapvote::compare);
-        if(!isignored(d->clientnum))
+        if(showmapvotes >= (!gs_playing(game::gamestate) ? 2 : 1) && !isignored(d->clientnum))
         {
             SEARCHBINDCACHE(votekey)("showgui maps 2", 0);
-            conoutft(CON_EVENT, "%s suggests: \fs\fy%s\fS on \fs\fo%s\fS, press \fs\fc%s\fS to vote", game::colourname(d), server::gamename(mode, muts), text, votekey);
+            conoutft(CON_EVENT, "%s suggests: \fs\fy%s\fS on \fs\fo%s\fS, press %s to vote", game::colourname(d), server::gamename(mode, muts), text, votekey);
         }
     }
-    ICOMMAND(0, fakevote, "sii", (char *s, int *m, int *n), vote(game::player1, s, *m, *n));
 
     void getvotes(int vote, int prop, int idx)
     {
@@ -148,79 +155,62 @@ namespace client
             }
         }
     }
-    ICOMMAND(0, getvote, "bbb", (int *vote, int *prop, int *idx, int *numargs), getvotes(*vote, *prop, *idx));
+    ICOMMAND(0, getvote, "bbb", (int *vote, int *prop, int *idx), getvotes(*vote, *prop, *idx));
 
     struct demoinfo
     {
         demoheader hdr;
-        string file, mapname;
-        int gamemode, mutators;
+        string file;
     };
     vector<demoinfo> demoinfos;
-
-    int demoint(stream *f)
-    {
-        int c = (char)f->get<uchar>();
-        if(c==-128) { int n = f->get<uchar>(); n |= char(f->get<uchar>())<<8; return n; }
-        else if(c==-127) { int n = f->get<uchar>(); n |= f->get<uchar>()<<8; n |= f->get<uchar>()<<16; return n|(f->get<uchar>()<<24); }
-        else return c;
-    }
+    vector<char *> faildemos;
 
     int scandemo(const char *name)
     {
         if(!name || !*name) return -1;
         loopv(demoinfos) if(!strcmp(demoinfos[i].file, name)) return i;
+        loopv(faildemos) if(!strcmp(faildemos[i], name)) return -1;
         stream *f = opengzfile(name, "rb");
-        if(!f) return -1;
+        if(!f)
+        {
+            faildemos.add(newstring(name));
+            return -1;
+        }
         int num = demoinfos.length();
         demoinfo &d = demoinfos.add();
         copystring(d.file, name);
-        mkstring(msg);
-        if(f->read(&d.hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(d.hdr.magic, DEMO_MAGIC, sizeof(d.hdr.magic)))
+        string msg = "";
+        if(f->read(&d.hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(d.hdr.magic, VERSION_DEMOMAGIC, sizeof(d.hdr.magic)))
             formatstring(msg)("\frsorry, \fs\fc%s\fS is not a demo file", name);
         else
         {
-            lilswap(&d.hdr.version, 2);
-            if(d.hdr.version!=DEMO_VERSION) formatstring(msg)("\frdemo \fs\fc%s\fS requires \fs\fc%s\fS version of %s", name, d.hdr.version<DEMO_VERSION ? "an older" : "a newer", versionname);
-            else if(d.hdr.gamever!=GAMEVERSION) formatstring(msg)("\frdemo \fs\fc%s\fS requires \fs\fc%s\fS version of %s", name, d.hdr.gamever<GAMEVERSION ? "an older" : "a newer", versionname);
+            lilswap(&d.hdr.gamever, 4);
+            if(d.hdr.gamever!=VERSION_GAME)
+                formatstring(msg)("\frdemo \fs\fc%s\fS requires \fs\fc%s\fS version of %s", name, d.hdr.gamever<VERSION_GAME ? "an older" : "a newer", VERSION_NAME);
         }
+        delete f;
         if(msg[0])
         {
             conoutft(CON_INFO, "%s", msg);
             demoinfos.pop();
-            delete f;
+            faildemos.add(newstring(name));
             return -1;
         }
-        if(f->seek(sizeof(int)*3, SEEK_CUR) && demoint(f) == N_WELCOME && demoint(f) == N_MAPCHANGE)
-        {
-            char *t = d.mapname;
-            do // serialised version of getstring
-            {
-                if(t >= &d.mapname[MAXSTRLEN]) { d.mapname[MAXSTRLEN-1] = 0; break; }
-                int c = (char)demoint(f);
-                if(c == -1)
-                {
-                    conoutft(CON_INFO, "\frunable to parse map name from: \fc%s", name);
-                    demoinfos.pop();
-                    delete f;
-                    return -1;
-                }
-                *t = c;
-            } while(*t++);
-            demoint(f);
-            d.gamemode = demoint(f);
-            d.mutators = demoint(f);
-            //conoutft(CON_INFO, "read demo %s: [%d:%d] %s on %s", d.file, d.hdr.version, d.hdr.gamever, server::gamename(d.gamemode, d.mutators), d.mapname);
-            delete f;
-            return num;
-        }
-        conoutft(CON_INFO, "\frunexpected message while reading: \fc%s", name);
-        demoinfos.pop();
-        delete f;
-        return -1;
+        return num;
     }
     ICOMMAND(0, demoscan, "s", (char *name), intret(scandemo(name)));
 
+    void resetdemos(bool all)
+    {
+        if(all) loopvrev(demoinfos) demoinfos.remove(i);
+        loopvrev(faildemos)
+        {
+            DELETEA(faildemos[i]);
+            faildemos.remove(i);
+        }
+    }
+    ICOMMAND(0, demoreset, "i", (int *all), resetdemos(*all!=0));
+
     void infodemo(int idx, int prop)
     {
         if(idx < 0) intret(demoinfos.length());
@@ -229,11 +219,11 @@ namespace client
             demoinfo &d = demoinfos[idx];
             switch(prop)
             {
-                case 0: intret(d.hdr.version); break;
-                case 1: intret(d.hdr.gamever); break;
-                case 2: result(d.mapname); break;
-                case 3: intret(d.gamemode); break;
-                case 4: intret(d.mutators); break;
+                case 0: intret(d.hdr.gamever); break;
+                case 1: result(d.hdr.mapname); break;
+                case 2: intret(d.hdr.gamemode); break;
+                case 3: intret(d.hdr.mutators); break;
+                case 4: intret(d.hdr.starttime); break;
                 default: break;
             }
         }
@@ -247,7 +237,7 @@ namespace client
         setsvar("accountname", name);
         setsvar("accountpass", key);
     });
-    ICOMMAND(0, hasauthkey, "", (), intret(accountname[0] && accountpass[0] ? 1 : 0));
+    ICOMMAND(0, hasauthkey, "i", (int *n), intret(accountname[0] && accountpass[0] && (!*n || authconnect) ? 1 : 0));
 
     void writegamevars(const char *name, bool all = false, bool server = false)
     {
@@ -260,7 +250,7 @@ namespace client
         loopv(ids)
         {
             ident &id = *ids[i];
-            if(id.flags&IDF_CLIENT) switch(id.type)
+            if(id.flags&IDF_CLIENT && !(id.flags&IDF_READONLY) && !(id.flags&IDF_WORLD)) switch(id.type)
             {
                 case ID_VAR:
                     if(*id.storage.i == id.def.i)
@@ -269,7 +259,7 @@ namespace client
                         else break;
                     }
                     if(server) f->printf("sv_");
-                    f->printf((id.flags&IDF_HEX && *id.storage.i >= 0 ? (id.maxval==0xFFFFFF ? "%s 0x%.6X\n" : "%s 0x%X\n") : "%s %d\n"), id.name, *id.storage.i);
+                    f->printf("%s %s\n", escapeid(id), intstr(&id));
                     break;
                 case ID_FVAR:
                     if(*id.storage.f == id.def.f)
@@ -278,7 +268,7 @@ namespace client
                         else break;
                     }
                     if(server) f->printf("sv_");
-                    f->printf("%s %s\n", id.name, floatstr(*id.storage.f));
+                    f->printf("%s %s\n", escapeid(id), floatstr(*id.storage.f));
                     break;
                 case ID_SVAR:
                     if(!strcmp(*id.storage.s, id.def.s))
@@ -287,7 +277,7 @@ namespace client
                         else break;
                     }
                     if(server) f->printf("sv_");
-                    f->printf("%s %s\n", id.name, escapestring(*id.storage.s));
+                    f->printf("%s %s\n", escapeid(id), escapestring(*id.storage.s));
                     break;
             }
         }
@@ -295,11 +285,82 @@ namespace client
     }
     ICOMMAND(0, writevars, "sii", (char *name, int *all, int *sv), if(!(identflags&IDF_WORLD)) writegamevars(name, *all!=0, *sv!=0));
 
+    void writegamevarsinfo(const char *name)
+    {
+        if(!name || !*name) name = "varsinfo.txt";
+        stream *f = openfile(name, "w");
+        if(!f) return;
+        f->printf("// List of vars properties, fields are separated by tabs; empty if nonexistent\n");
+        f->printf("// Fields: NAME TYPE FLAGS ARGS VALTYPE VALUE MIN MAX DESC USAGE\n");
+        vector<ident *> ids;
+        enumerate(idents, ident, id, ids.add(&id));
+        ids.sort(ident::compare);
+        loopv(ids)
+        {
+            ident &id = *ids[i];
+            if(!(id.flags&IDF_SERVER)) // Exclude sv_* duplicates
+            {
+                f->printf("%s\t%d\t%d", id.name, id.type, id.flags);
+                switch(id.type)
+                {
+                    case ID_VAR:
+                        f->printf("\t\t"); // empty ARGS VALTYPE
+                        if(!(id.flags&IDF_HEX))
+                            f->printf("\t%d\t%d\t%d", id.def.i, id.minval, id.maxval);
+                        else
+                        {
+                            if(id.maxval==0xFFFFFF)
+                                f->printf("\t0x%.6X\t0x%.6X\t0x%.6X", id.def.i, id.minval, id.maxval);
+                            else
+                                f->printf("\t0x%X\t0x%X\t0x%X", id.def.i, id.minval, id.maxval);
+                        }
+                        break;
+                    case ID_FVAR:
+                        f->printf("\t\t\t%s\t%s\t%s", floatstr(id.def.f), floatstr(id.minvalf), floatstr(id.maxvalf)); // empty ARGS VALTYPE
+                        break;
+                    case ID_SVAR:
+                        f->printf("\t\t\t%s\t\t", escapestring(id.def.s)); // empty ARGS VALTYPE MIN MAX
+                        break;
+                    case ID_ALIAS:
+                        f->printf("\t\t%d", id.valtype); // empty ARGS
+                        switch(id.valtype)
+                        {
+                            case VAL_NULL:
+                                f->printf("\tNULL");
+                                break;
+                            case VAL_INT:
+                                f->printf("\t%d", id.val.i);
+                                break;
+                            case VAL_FLOAT:
+                                f->printf("\t%s", floatstr(id.val.f));
+                                break;
+                            case VAL_STR:
+                                f->printf("\t%s", escapestring(id.val.s));
+                                break;
+                        }
+                        f->printf("\t\t"); // empty MIN MAX
+                        break;
+                    case ID_COMMAND:
+                        f->printf("\t%s\t\t\t\t", escapestring(id.args)); // empty VALTYPE VALUE MIN MAX
+                        break;
+                }
+                // empty if nonexistent
+                f->printf("\t%s", escapestring(id.desc ? id.desc : ""));
+                f->printf("\t%s", escapestring(id.usage ? id.usage : ""));
+                f->printf("\n");
+            }
+        }
+        delete f;
+    }
+    ICOMMAND(0, writevarsinfo, "s", (char *name), if(!(identflags&IDF_WORLD)) writegamevarsinfo(name));
+
     // collect c2s messages conveniently
     vector<uchar> messages;
     bool messagereliable = false;
 
     VAR(IDF_PERSIST, colourchat, 0, 1, 1);
+    SVAR(IDF_PERSIST, filterwords, "");
+
     VAR(IDF_PERSIST, showlaptimes, 0, 2, 3); // 0 = off, 1 = only player, 2 = +humans, 3 = +bots
 
     const char *defaultserversort()
@@ -336,7 +397,6 @@ namespace client
         styles.deletearrays();
     }
 
-#ifdef VANITY
     void getvitem(gameent *d, int n, int v)
     {
         if(n < 0) intret(d->vitems.length());
@@ -350,7 +410,6 @@ namespace client
     }
     ICOMMAND(0, getplayervanity, "", (), result(game::player1->vanity));
     ICOMMAND(0, getplayervitem, "bi", (int *n, int *v), getvitem(game::player1, *n, *v));
-#endif
 
     ICOMMAND(0, mastermode, "i", (int *val), addmsg(N_MASTERMODE, "ri", *val));
     ICOMMAND(0, getplayername, "", (), result(game::player1->name));
@@ -366,13 +425,12 @@ namespace client
     {
         if(name && *name)
         {
-            string text;
-            filtertext(text, name, true, true, true, MAXNAMELEN);
-            const char *namestr = text;
-            while(*namestr && iscubespace(*namestr)) namestr++;
+            string namestr;
+            filterstring(namestr, name, true, true, true, true, MAXNAMELEN);
             if(*namestr && strcmp(game::player1->name, namestr))
             {
                 game::player1->setname(namestr);
+                if(initing == NOT_INITING) conoutft(CON_EVENT, "\fm* you are now known as %s", game::player1->name);
                 sendplayerinfo = true;
             }
         }
@@ -398,10 +456,35 @@ namespace client
         }
     }
     VARF(IDF_PERSIST, playermodel, 0, 0, PLAYERTYPES-1, setplayermodel(playermodel));
-
-#ifdef VANITY
     SVARF(IDF_PERSIST, playervanity, "", if(game::player1->setvanity(playervanity)) sendplayerinfo = true;);
-#endif
+
+    void setloadweap(const char *list)
+    {
+        vector<int> items;
+        if(list && *list)
+        {
+            vector<char *> chunk;
+            explodelist(list, chunk);
+            loopv(chunk)
+            {
+                if(!chunk[i] || !*chunk[i] || !isnumeric(*chunk[i])) continue;
+                int v = parseint(chunk[i]);
+                items.add(v >= W_OFFSET && v < W_ITEM ? v : 0);
+            }
+            chunk.deletearrays();
+        }
+        game::player1->loadweap.shrink(0);
+        loopv(items) if(game::player1->loadweap.find(items[i]) < 0)
+        {
+            game::player1->loadweap.add(items[i]);
+            if(game::player1->loadweap.length() >= W_LOADOUT) break;
+        }
+        sendplayerinfo = true;
+    }
+    SVARF(IDF_PERSIST, playerloadweap, "", setloadweap(playerloadweap));
+    ICOMMAND(0, getloadweap, "i", (int *n), intret(game::player1->loadweap.inrange(*n) ? game::player1->loadweap[*n] : -1));
+    ICOMMAND(0, allowedweap, "i", (int *n), intret(isweap(*n) && m_check(W(*n, modes), W(*n, muts), game::gamemode, game::mutators) && !W(*n, disabled) ? 1 : 0));
+    ICOMMAND(0, hasloadweap, "bb", (int *g, int *m), intret(m_loadout(m_game(*g) ? *g : game::gamemode, *m >= 0 ? *m : game::mutators) ? 1 : 0));
 
     int teamname(const char *team)
     {
@@ -431,7 +514,7 @@ namespace client
             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);
+                if(isteam(game::gamemode, game::mutators, t, T_FIRST)) addmsg(N_SWITCHTEAM, "ri", t);
             }
             else conoutft(CON_INFO, "\frcan only change teams when actually playing in team games");
         }
@@ -448,6 +531,7 @@ namespace client
 
     void edittoggled(bool edit)
     {
+        if(!edit && (game::maptime <= 0 || game::player1->state != CS_EDITING)) return;
         game::player1->editspawn(game::gamemode, game::mutators);
         game::player1->state = edit ? CS_EDITING : (m_edit(game::gamemode) ? CS_ALIVE : CS_DEAD);
         game::player1->o = camera1->o;
@@ -455,12 +539,25 @@ namespace client
         game::player1->pitch = camera1->pitch;
         game::player1->resetinterp();
         game::resetstate();
-        game::resetfollow();
+        game::specreset();
         physics::entinmap(game::player1, true); // find spawn closest to current floating pos
         projs::remove(game::player1);
         if(m_edit(game::gamemode)) addmsg(N_EDITMODE, "ri", edit ? 1 : 0);
     }
 
+    int getcn(physent *d)
+    {
+        if(!d || !gameent::is(d)) return -1;
+        return ((gameent *)d)->clientnum;
+    }
+
+    int getclientpresence(int cn)
+    {
+        gameent *d = game::getclient(cn);
+        return d ? 1 : 0;
+    }
+    ICOMMAND(0, getclientpresence, "i", (int *cn), intret(getclientpresence(*cn)));
+
     const char *getclientname(int cn, int colour)
     {
         gameent *d = game::getclient(cn);
@@ -485,11 +582,10 @@ namespace client
 
     const char *getmodelname(int mdl, int idx)
     {
-        return mdl >= 0 ? playertypes[mdl%PLAYERTYPES][clamp(idx, 0, 4)] : "";
+        return mdl >= 0 ? playertypes[mdl%PLAYERTYPES][clamp(idx, 0, 5)] : "";
     }
-    ICOMMAND(0, getmodelname, "iiN", (int *mdl, int *idx, int *numargs), result(getmodelname(*mdl, *numargs >= 2 ? *idx : 4)));
+    ICOMMAND(0, getmodelname, "iiN", (int *mdl, int *idx, int *numargs), result(getmodelname(*mdl, *numargs >= 2 ? *idx : 5)));
 
-#ifdef VANITY
     const char *getclientvanity(int cn)
     {
         gameent *d = game::getclient(cn);
@@ -503,7 +599,6 @@ namespace client
         if(d) getvitem(d, n, v);
     }
     ICOMMAND(0, getclientvitem, "ibi", (int *cn, int *n, int *v), getclientvitem(*cn, *n, *v));
-#endif
 
     const char *getclienthost(int cn)
     {
@@ -512,6 +607,13 @@ namespace client
     }
     ICOMMAND(0, getclienthost, "i", (int *cn), result(getclienthost(*cn)));
 
+    const char *getclientip(int cn)
+    {
+        gameent *d = game::getclient(cn);
+        return d ? d->hostip : "";
+    }
+    ICOMMAND(0, getclientip, "i", (int *cn), result(getclientip(*cn)));
+
     int getclientteam(int cn)
     {
         gameent *d = game::getclient(cn);
@@ -519,19 +621,33 @@ namespace client
     }
     ICOMMAND(0, getclientteam, "i", (int *cn), intret(getclientteam(*cn)));
 
+    int getclientweapselect(int cn)
+    {
+        gameent *d = game::getclient(cn);
+        return d ? d->weapselect : -1;
+    }
+    ICOMMAND(0, getclientweapselect, "i", (int *cn), intret(getclientweapselect(*cn)));
+
+    int getclientloadweap(int cn, int n)
+    {
+        gameent *d = game::getclient(cn);
+        return d ? (d->loadweap.inrange(n) ? d->loadweap[n] : 0) : -1;
+    }
+    ICOMMAND(0, getclientloadweap, "ii", (int *cn, int *n), intret(getclientloadweap(*cn, *n)));
+
     bool haspriv(gameent *d, int priv)
     {
         if(!d) return false;
         if(!priv || (d == game::player1 && !remote)) return true;
-        return d->privilege >= priv;
+        return (d->privilege&PRIV_TYPE) >= priv;
     }
     ICOMMAND(0, issupporter, "i", (int *cn), intret(haspriv(game::getclient(*cn), PRIV_SUPPORTER) ? 1 : 0));
     ICOMMAND(0, ismoderator, "i", (int *cn), intret(haspriv(game::getclient(*cn), PRIV_MODERATOR) ? 1 : 0));
     ICOMMAND(0, isadministrator, "i", (int *cn), intret(haspriv(game::getclient(*cn), PRIV_ADMINISTRATOR) ? 1 : 0));
     ICOMMAND(0, isdeveloper, "i", (int *cn), intret(haspriv(game::getclient(*cn), PRIV_DEVELOPER) ? 1 : 0));
-    ICOMMAND(0, iscreator, "i", (int *cn), intret(haspriv(game::getclient(*cn), PRIV_CREATOR) ? 1 : 0));
+    ICOMMAND(0, isfounder, "i", (int *cn), intret(haspriv(game::getclient(*cn), PRIV_CREATOR) ? 1 : 0));
 
-    ICOMMAND(0, getclientpriv, "i", (int *cn, int *priv), intret(haspriv(game::getclient(*cn), *priv) ? 1 : 0));
+    ICOMMAND(0, getclientpriv, "ii", (int *cn, int *priv), intret(haspriv(game::getclient(*cn), *priv) ? 1 : 0));
 
     const char *getclienthandle(int cn)
     {
@@ -540,6 +656,34 @@ namespace client
     }
     ICOMMAND(0, getclienthandle, "i", (int *cn), result(getclienthandle(*cn)));
 
+    void getclientversion(int cn, int prop)
+    {
+        gameent *d = cn >= 0 ? game::getclient(cn) : game::player1;
+        if(d) switch(prop)
+        {
+            case 0: intret(d->version.major); break;
+            case 1: intret(d->version.minor); break;
+            case 2: intret(d->version.patch); break;
+            case 3: intret(d->version.game); break;
+            case 4: intret(d->version.platform); break;
+            case 5: intret(d->version.arch); break;
+            case 6: intret(d->version.gpuglver); break;
+            case 7: intret(d->version.gpuglslver); break;
+            case 8: intret(d->version.crc); break;
+            case 9: result(d->version.gpuvendor); break;
+            case 10: result(d->version.gpurenderer); break;
+            case 11: result(d->version.gpuversion); break;
+            case 12: result(plat_name(d->version.platform)); break;
+            case 13:
+            {
+                defformatstring(str)("%d.%d.%d-%s%d", d->version.major, d->version.minor, d->version.patch, plat_name(d->version.platform), d->version.arch);
+                result(str);
+            }
+            default: break;
+        }
+    }
+    ICOMMAND(0, getclientversion, "ii", (int *cn, int *prop), getclientversion(*cn, *prop));
+
     bool isspectator(int cn)
     {
         gameent *d = game::getclient(cn);
@@ -557,8 +701,8 @@ namespace client
     bool isai(int cn, int type)
     {
         gameent *d = game::getclient(cn);
-        int aitype = type > 0 && type < AI_MAX ? type : AI_BOT;
-        return d && d->aitype == aitype;
+        int actortype = type > 0 && type < A_MAX ? type : A_BOT;
+        return d && d->actortype == actortype;
     }
     ICOMMAND(0, isai, "ii", (int *cn, int *type), intret(isai(*cn, *type) ? 1 : 0));
 
@@ -574,9 +718,9 @@ namespace client
 
     bool ismodelocked(int reqmode, int reqmuts, int askmuts = 0, const char *reqmap = NULL)
     {
-        if(m_local(reqmode) && remote) return true;
-        if(G(modelock) == PRIV_MAX && G(mapslock) == PRIV_MAX && !haspriv(game::player1, PRIV_MAX)) return true;
-        else if(G(votelock)) switch(G(votelocktype))
+        reqmuts |= mutslockforce;
+        if(!m_game(reqmode) || (m_local(reqmode) && remote)) return true;
+        if(G(votelock)) switch(G(votelocktype))
         {
             case 1: if(!haspriv(game::player1, G(votelock))) return true; break;
             case 2:
@@ -588,9 +732,8 @@ namespace client
                 break;
             case 0: default: break;
         }
-        int rmode = reqmode, rmuts = reqmuts;
-        modecheck(rmode, rmuts, askmuts);
-        if(askmuts && !mutscmp(askmuts, rmuts)) return true;
+        modecheck(reqmode, reqmuts, askmuts);
+        if(askmuts && !mutscmp(askmuts, reqmuts)) return true;
         if(G(modelock)) switch(G(modelocktype))
         {
             case 1: if(!haspriv(game::player1, G(modelock))) return true; break;
@@ -605,12 +748,12 @@ namespace client
                 case 1:
                 {
                     list = newstring(G(allowmaps));
-                    mapcull(list, reqmode, reqmuts, numplayers(), G(mapsfilter), true);
+                    mapcull(list, reqmode, reqmuts, otherclients(true), G(mapsfilter), true);
                     break;
                 }
                 case 2:
                 {
-                    maplist(list, reqmode, reqmuts, numplayers(), G(mapsfilter), true);
+                    maplist(list, reqmode, reqmuts, otherclients(true), G(mapsfilter), true);
                     break;
                 }
                 case 0: default: break;
@@ -676,6 +819,14 @@ namespace client
     }
     ICOMMAND(0, listclients, "i", (int *local), listclients(*local!=0));
 
+    void getlastclientnum()
+    {
+        int cn = game::player1->clientnum;
+        loopv(game::players) if(game::players[i] && game::players[i]->clientnum > cn) cn = game::players[i]->clientnum;
+        intret(cn);
+    }
+    ICOMMAND(0, getlastclientnum, "", (), getlastclientnum());
+
     void addcontrol(const char *arg, int type, const char *msg)
     {
         int i = parseplayer(arg);
@@ -686,30 +837,46 @@ namespace client
     ICOMMAND(0, ban, "ss", (char *s, char *m), addcontrol(s, ipinfo::BAN, m));
     ICOMMAND(0, mute, "ss", (char *s, char *m), addcontrol(s, ipinfo::MUTE, m));
     ICOMMAND(0, limit, "ss", (char *s, char *m), addcontrol(s, ipinfo::LIMIT, m));
+    ICOMMAND(0, except, "ss", (char *s, char *m), addcontrol(s, ipinfo::EXCEPT, m));
 
     ICOMMAND(0, clearallows, "", (), addmsg(N_CLRCONTROL, "ri", ipinfo::ALLOW));
     ICOMMAND(0, clearbans, "", (), addmsg(N_CLRCONTROL, "ri", ipinfo::BAN));
     ICOMMAND(0, clearmutes, "", (), addmsg(N_CLRCONTROL, "ri", ipinfo::MUTE));
     ICOMMAND(0, clearlimits, "", (), addmsg(N_CLRCONTROL, "ri", ipinfo::LIMIT));
+    ICOMMAND(0, clearexcepts, "", (), addmsg(N_CLRCONTROL, "ri", ipinfo::EXCEPT));
 
-    vector<int> ignores;
+    vector<char *> ignores;
     void ignore(int cn)
     {
         gameent *d = game::getclient(cn);
         if(!d || d == game::player1) return;
-        conoutft(CON_EVENT, "\fyignoring: \fc%s", d->name);
-        if(ignores.find(cn) < 0) ignores.add(cn);
+        if(ignores.find(d->hostip) < 0)
+        {
+            conoutft(CON_EVENT, "\fyignoring: \fs%s\fS (\fs\fc%s\fS)", game::colourname(d), d->hostip);
+            ignores.add(d->hostip);
+        }
+        else
+            conoutft(CON_EVENT, "\fralready ignoring: \fs%s\fS (\fs\fc%s\fS)", game::colourname(d), d->hostip);
     }
 
     void unignore(int cn)
     {
-        if(ignores.find(cn) < 0) return;
         gameent *d = game::getclient(cn);
-        if(d) conoutft(CON_EVENT, "\fystopped ignoring: \fc%s", d->name);
-        ignores.removeobj(cn);
+        if(!d) return;
+        if(ignores.find(d->hostip) >= 0)
+        {
+            conoutft(CON_EVENT, "\fystopped ignoring: \fs%s\fS (\fs\fc%s\fS)", game::colourname(d), d->hostip);
+            ignores.removeobj(d->hostip);
+        }
+        else
+            conoutft(CON_EVENT, "\fryou are not ignoring: \fs%s\fS (\fs\fc%s\fS)", game::colourname(d), d->hostip);
     }
 
-    bool isignored(int cn) { return ignores.find(cn) >= 0; }
+    bool isignored(int cn)
+    {
+        gameent *d = game::getclient(cn);
+        return ignores.find(d->hostip) >= 0;
+    }
 
     ICOMMAND(0, ignore, "s", (char *arg), ignore(parseplayer(arg)));
     ICOMMAND(0, unignore, "s", (char *arg), unignore(parseplayer(arg)));
@@ -743,7 +910,7 @@ namespace client
     {
         if(!arg[0]) return;
         int val = 1;
-        mkstring(hash);
+        string hash = "";
         if(!arg[1] && isdigit(arg[0])) val = parseint(arg);
         else server::hashpassword(game::player1->clientnum, sessionid, arg, hash);
         addmsg(N_SETPRIV, "ris", val, hash);
@@ -752,8 +919,8 @@ namespace client
 
     void tryauth()
     {
-        if(!accountname[0]) return;
-        addmsg(N_AUTHTRY, "rs", accountname);
+        if(accountname[0]) addmsg(N_AUTHTRY, "rs", accountname);
+        else conoutft(CON_INFO, "\frno account set for \fcauth");
     }
     ICOMMAND(0, auth, "", (), tryauth());
 
@@ -787,8 +954,7 @@ namespace client
     {
         if(editmode) toggleedit();
         gettingmap = needsmap = remote = isready = sendgameinfo = sendplayerinfo = false;
-        sessionid = 0;
-        ignores.shrink(0);
+        sessionid = sessionver = lastplayerinfo = 0;
         messages.shrink(0);
         mapvotes.shrink(0);
         messagereliable = false;
@@ -802,6 +968,7 @@ namespace client
         game::mutators = 0;
         loopv(game::players) if(game::players[i]) game::clientdisconnected(i);
         game::waiting.setsize(0);
+        hud::cleanup();
         emptymap(0, true, NULL, true);
         smartmusic(true, false);
         enumerate(idents, ident, id, {
@@ -862,52 +1029,76 @@ namespace client
         messages.put(buf, p.length());
     }
 
-    void saytext(gameent *d, int flags, char *text)
+    void saytext(gameent *f, gameent *t, int flags, char *text)
     {
-        filtertext(text, text, true, colourchat ? false : true);
-        mkstring(s);
-        defformatstring(m)("%s", game::colourname(d));
-        if(flags&SAY_TEAM)
+        bigstring msg, line;
+        filterbigstring(msg, text, true, colourchat ? false : true, true, true);
+        if(*filterwords) filterword(msg, filterwords);
+
+        defformatstring(name)("%s", game::colourname(f));
+        if(flags&SAY_WHISPER)
+        {
+            if(!t) return;
+            defformatstring(sw)(" (\fs\fcwhispers to %s\fS)", t == game::player1 ? "you" : game::colourname(t));
+            concatstring(name, sw);
+        }
+        else if(flags&SAY_TEAM)
         {
-            defformatstring(t)(" (to team %s)", game::colourteam(d->team));
-            concatstring(m, t);
+            defformatstring(st)(" (to team %s)", game::colourteam(f->team));
+            concatstring(name, st);
         }
-        if(flags&SAY_ACTION) formatstring(s)("\fv* %s %s", m, text);
-        else formatstring(s)("\fw<%s> %s", m, text);
+        if(flags&SAY_ACTION) formatbigstring(line)("\fv* %s %s", name, msg);
+        else formatbigstring(line)("\fw<%s> %s", name, msg);
 
         int snd = S_CHAT;
         ident *wid = idents.access(flags&SAY_ACTION ? "on_action" : "on_text");
         if(wid && wid->type == ID_ALIAS && wid->getstr()[0])
         {
-            defformatstring(act)("%s %d %d %s %s %s",
-                flags&SAY_ACTION ? "on_action" : "on_text", d->clientnum, flags&SAY_TEAM ? 1 : 0,
-                escapestring(game::colourname(d)), escapestring(text), escapestring(s));
+            defformatbigstring(act)("%s %d %d %s %s %s",
+                flags&SAY_ACTION ? "on_action" : "on_text", f->clientnum, flags&SAY_TEAM ? 1 : 0,
+                escapestring(game::colourname(f)), escapestring(text), escapestring(line));
             int ret = execute(act);
             if(ret > 0) snd = ret;
         }
-        conoutft(CON_CHAT, "%s", s);
-        if(snd >= 0 && !issound(d->cschan)) playsound(snd, d->o, d, snd != S_CHAT ? 0 : SND_DIRECT, -1, -1, -1, &d->cschan);
+        if((!(flags&SAY_TEAM) || f->team == game::player1->team) && (!(flags&SAY_WHISPER) || f == game::player1 || t == game::player1))
+        {
+            conoutft(CON_CHAT, "%s", line);
+            if(snd >= 0 && !issound(f->cschan)) playsound(snd, f->o, f, snd != S_CHAT ? 0 : SND_DIRECT, -1, -1, -1, &f->cschan);
+        }
+        ai::scanchat(f, t, flags, text);
     }
 
-    void toserver(int flags, char *text)
+    void toserver(int flags, const char *text, const char *target)
     {
         if(!waiting(false) && !client::demoplayback)
         {
-            string output;
-            copystring(output, text, messagelength);
-            if(flags&SAY_TEAM && !m_team(game::gamemode, game::mutators))
-                flags &= ~SAY_TEAM;
-            saytext(game::player1, flags, output);
-            addmsg(N_TEXT, "ri2s", game::player1->clientnum, flags, output);
+            bigstring output;
+            copybigstring(output, text, messagelength);
+            if(flags&SAY_WHISPER)
+            {
+                gameent *e = game::getclient(parseplayer(target));
+                if(e && e->clientnum != game::player1->clientnum)
+                    addmsg(N_TEXT, "ri3s", game::player1->clientnum, e->clientnum, flags, output);
+            }
+            else
+            {
+                if(flags&SAY_TEAM && !m_team(game::gamemode, game::mutators))
+                    flags &= ~SAY_TEAM;
+                addmsg(N_TEXT, "ri3s", game::player1->clientnum, -1, flags, output);
+            }
         }
     }
     ICOMMAND(0, say, "C", (char *s), toserver(SAY_NONE, s));
     ICOMMAND(0, me, "C", (char *s), toserver(SAY_ACTION, s));
     ICOMMAND(0, sayteam, "C", (char *s), toserver(SAY_TEAM, s));
     ICOMMAND(0, meteam, "C", (char *s), toserver(SAY_ACTION|SAY_TEAM, s));
+    ICOMMAND(0, whisper, "ss", (char *t, char *s), toserver(SAY_WHISPER, s, t));
+    ICOMMAND(0, mewhisper, "ss", (char *t, char *s), toserver(SAY_ACTION|SAY_WHISPER, s, t));
 
     void parsecommand(gameent *d, const char *cmd, const char *arg)
     {
+        const char *oldval = NULL;
+        bool needfreeoldval = false;
         ident *id = idents.access(cmd);
         if(id && id->flags&IDF_CLIENT)
         {
@@ -930,6 +1121,7 @@ namespace client
                 case ID_VAR:
                 {
                     int ret = parseint(arg);
+                    oldval = intstr(id);
                     *id->storage.i = ret;
                     id->changed();
                     val = intstr(id);
@@ -938,6 +1130,7 @@ namespace client
                 case ID_FVAR:
                 {
                     float ret = parsefloat(arg);
+                    oldval = floatstr(*id->storage.f);
                     *id->storage.f = ret;
                     id->changed();
                     val = floatstr(*id->storage.f);
@@ -945,6 +1138,8 @@ namespace client
                 }
                 case ID_SVAR:
                 {
+                    oldval = newstring(*id->storage.s);
+                    needfreeoldval = true;
                     delete[] *id->storage.s;
                     *id->storage.s = newstring(arg);
                     id->changed();
@@ -954,7 +1149,14 @@ namespace client
                 default: return;
             }
             if((d || showservervariables) && val)
-                conoutft(CON_EVENT, "\fy%s set \fs\fc%s\fS to \fs\fc%s\fS", d ? game::colourname(d) : (connected(false) ? "the server" : "you"), cmd, val);
+            {
+                if(oldval)
+                {
+                    conoutft(CON_EVENT, "\fy%s set \fs\fc%s\fS to \fs\fc%s\fS (was: \fs\fc%s\fS)", d ? game::colourname(d) : (connected(false) ? "the server" : "you"), cmd, val, oldval);
+                    if(needfreeoldval) delete[] oldval;
+                }
+                else conoutft(CON_EVENT, "\fy%s set \fs\fc%s\fS to \fs\fc%s\fS", d ? game::colourname(d) : (connected(false) ? "the server" : "you"), cmd, val);
+            }
         }
         else if(verbose) conoutft(CON_EVENT, "\fr%s sent unknown command: \fc%s", d ? game::colourname(d) : "the server", cmd);
     }
@@ -980,6 +1182,7 @@ namespace client
 
     void changemapserv(char *name, int gamemode, int mutators, bool temp)
     {
+        game::gamestate = m_fight(gamemode) ? G_S_WAITING : G_S_PLAYING;
         game::gamemode = gamemode;
         game::mutators = mutators;
         modecheck(game::gamemode, game::mutators);
@@ -993,10 +1196,10 @@ namespace client
         if(m_demo(game::gamemode))
         {
             game::maptime = 1;
-            game::intermission = true;
             game::timeremaining = 0;
             return;
         }
+        else if(demoendless) demoendless = 0;
         if(m_capture(game::gamemode)) capture::reset();
         else if(m_defend(game::gamemode)) defend::reset();
         else if(m_bomber(game::gamemode)) bomber::reset();
@@ -1020,14 +1223,16 @@ namespace client
         {
             case N_SENDDEMO:
             {
+                int ctime = getint(p);
+                if(filetimelocal) ctime += clockoffset;
                 data += p.length();
                 len -= p.length();
                 string fname;
-                if(*filetimeformat) formatstring(fname)("demos/%s.dmo", gettime(clocktime, filetimeformat));
-                else formatstring(fname)("demos/%u.dmo", uint(clocktime));
+                if(*filetimeformat) formatstring(fname)("demos/%s.dmo", gettime(ctime, filetimeformat));
+                else formatstring(fname)("demos/%u.dmo", uint(ctime));
                 stream *demo = openfile(fname, "wb");
                 if(!demo) return;
-                conoutft(CON_MESG, "\fyreceived demo: \fc%s", fname);
+                conoutft(CON_EVENT, "\fyreceived demo: \fc%s", fname);
                 demo->write(data, len);
                 delete demo;
                 break;
@@ -1046,7 +1251,7 @@ namespace client
                 stream *f = openfile(reqfext, "wb");
                 if(!f)
                 {
-                    conoutft(CON_MESG, "\frfailed to open map file: \fc%s", reqfext);
+                    conoutft(CON_EVENT, "\frfailed to open map file: \fc%s", reqfext);
                     break;
                 }
                 gettingmap = true;
@@ -1079,15 +1284,15 @@ namespace client
 
     void getdemo(int i)
     {
-        if(i <= 0) conoutft(CON_MESG, "\fygetting demo, please wait...");
-        else conoutft(CON_MESG, "\fygetting demo \fs\fc%d\fS, please wait...", i);
+        if(i <= 0) conoutft(CON_EVENT, "\fygetting demo, please wait...");
+        else conoutft(CON_EVENT, "\fygetting demo \fs\fc%d\fS, please wait...", i);
         addmsg(N_GETDEMO, "ri", i);
     }
     ICOMMAND(0, getdemo, "i", (int *val), getdemo(*val));
 
     void listdemos()
     {
-        conoutft(CON_MESG, "\fylisting demos...");
+        conoutft(CON_EVENT, "\fylisting demos...");
         addmsg(N_LISTDEMOS, "r");
     }
     ICOMMAND(0, listdemos, "", (), listdemos());
@@ -1113,7 +1318,7 @@ namespace client
 
     void sendmap()
     {
-        conoutft(CON_MESG, "\fysending map...");
+        conoutft(CON_EVENT, "\fysending map...");
         const char *reqmap = mapname;
         if(!reqmap || !*reqmap) reqmap = "maps/untitled";
         bool edit = m_edit(game::gamemode);
@@ -1129,12 +1334,12 @@ namespace client
             stream *f = openfile(reqfext, "rb");
             if(f)
             {
-                conoutft(CON_MESG, "\fytransmitting file: \fc%s", reqfext);
+                conoutft(CON_EVENT, "\fytransmitting file: \fc%s", reqfext);
                 sendfile(-1, 2, f, "ri2", N_SENDMAPFILE, i);
                 if(needclipboard >= 0) needclipboard++;
                 delete f;
             }
-            else if(i <= SENDMAP_MIN) conoutft(CON_MESG, "\frfailed to open map file: \fc%s", reqfext);
+            else if(i <= SENDMAP_MIN) conoutft(CON_EVENT, "\frfailed to open map file: \fc%s", reqfext);
         }
     }
     ICOMMAND(0, sendmap, "", (), sendmap());
@@ -1269,12 +1474,17 @@ namespace client
     void sendintro()
     {
         packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+
         putint(p, N_CONNECT);
+
         sendstring(game::player1->name, p);
         putint(p, game::player1->colour);
         putint(p, game::player1->model);
         sendstring(game::player1->vanity, p);
-        mkstring(hash);
+        putint(p, game::player1->loadweap.length());
+        loopv(game::player1->loadweap) putint(p, game::player1->loadweap[i]);
+
+        string hash = "";
         if(connectpass[0])
         {
             server::hashpassword(game::player1->clientnum, sessionid, connectpass, hash);
@@ -1282,6 +1492,9 @@ namespace client
         }
         sendstring(hash, p);
         sendstring(authconnect ? accountname : "", p);
+
+        game::player1->version.put(p);
+
         sendclientpacket(p.finalize(), 1);
     }
 
@@ -1295,7 +1508,7 @@ namespace client
         putuint(q, d->impulse[IM_METER]);
         ivec o = ivec(vec(d->o.x, d->o.y, d->o.z-d->height).mul(DMF));
         uint vel = min(int(d->vel.magnitude()*DVELF), 0xFFFF), fall = min(int(d->falling.magnitude()*DVELF), 0xFFFF);
-        // 3 bits position, 1 bit velocity, 3 bits falling, 1 bit aim, 1 bit crouching, 1 bit conopen
+        // 3 bits position, 1 bit velocity, 3 bits falling
         uint flags = 0;
         if(o.x < 0 || o.x > 0xFFFF) flags |= 1<<0;
         if(o.y < 0 || o.y > 0xFFFF) flags |= 1<<1;
@@ -1308,10 +1521,7 @@ namespace client
             if(d->falling.x || d->falling.y || d->falling.z > 0) flags |= 1<<6;
         }
         if(d->conopen) flags |= 1<<8;
-        if(d->action[AC_JUMP]) flags |= 1<<9;
-        if(d->action[AC_PACING] == (d!=game::player1 || physics::pacingstyle < 3)) flags |= 1<<10;
-        if(d->action[AC_CROUCH]) flags |= 1<<11;
-        if(d->action[AC_SPECIAL]) flags |= 1<<12;
+        loopk(AC_TOTAL) if(d->action[k]) flags |= 1<<(9+k);
         putuint(q, flags);
         loopk(3)
         {
@@ -1376,56 +1586,62 @@ namespace client
     void sendmessages()
     {
         packetbuf p(MAXTRANS);
-        if(sendplayerinfo)
+        if(isready)
         {
-            p.reliable();
-            sendplayerinfo = false;
-            putint(p, N_SETPLAYERINFO);
-            sendstring(game::player1->name, p);
-            putint(p, game::player1->colour);
-            putint(p, game::player1->model);
-            sendstring(game::player1->vanity, p);
-        }
-        if(sendcrcinfo)
-        {
-            p.reliable();
-            sendcrcinfo = false;
-            putint(p, N_MAPCRC);
-            sendstring(game::clientmap, p);
-            putint(p, game::clientmap[0] ? getmapcrc() : 0);
-        }
-        if(sendgameinfo && !needsmap)
-        {
-            p.reliable();
-            putint(p, N_GAMEINFO);
-            enumerate(idents, ident, id, {
-                if(id.flags&IDF_CLIENT && id.flags&IDF_WORLD) switch(id.type)
-                {
-                    case ID_VAR:
-                        putint(p, id.type);
-                        sendstring(id.name, p);
-                        putint(p, *id.storage.i);
-                        break;
-                    case ID_FVAR:
-                        putint(p, id.type);
-                        sendstring(id.name, p);
-                        putfloat(p, *id.storage.f);
-                        break;
-                    case ID_SVAR:
-                        putint(p, id.type);
-                        sendstring(id.name, p);
-                        sendstring(*id.storage.s, p);
-                        break;
-                    default: break;
-                }
-            });
-            putint(p, -1);
-            entities::putitems(p);
-            putint(p, -1);
-            if(m_capture(game::gamemode)) capture::sendaffinity(p);
-            else if(m_defend(game::gamemode)) defend::sendaffinity(p);
-            else if(m_bomber(game::gamemode)) bomber::sendaffinity(p);
-            sendgameinfo = false;
+            if(sendplayerinfo && (!lastplayerinfo || totalmillis-lastplayerinfo >= setinfowait))
+            {
+                p.reliable();
+                sendplayerinfo = false;
+                lastplayerinfo = totalmillis ? totalmillis : 1;
+                putint(p, N_SETPLAYERINFO);
+                sendstring(game::player1->name, p);
+                putint(p, game::player1->colour);
+                putint(p, game::player1->model);
+                sendstring(game::player1->vanity, p);
+                putint(p, game::player1->loadweap.length());
+                loopv(game::player1->loadweap) putint(p, game::player1->loadweap[i]);
+            }
+            if(sendcrcinfo)
+            {
+                p.reliable();
+                sendcrcinfo = false;
+                putint(p, N_MAPCRC);
+                sendstring(game::clientmap, p);
+                putint(p, game::clientmap[0] ? getmapcrc() : 0);
+            }
+            if(sendgameinfo && !needsmap)
+            {
+                p.reliable();
+                putint(p, N_GAMEINFO);
+                enumerate(idents, ident, id, {
+                    if(id.flags&IDF_CLIENT && id.flags&IDF_WORLD) switch(id.type)
+                    {
+                        case ID_VAR:
+                            putint(p, id.type);
+                            sendstring(id.name, p);
+                            putint(p, *id.storage.i);
+                            break;
+                        case ID_FVAR:
+                            putint(p, id.type);
+                            sendstring(id.name, p);
+                            putfloat(p, *id.storage.f);
+                            break;
+                        case ID_SVAR:
+                            putint(p, id.type);
+                            sendstring(id.name, p);
+                            sendstring(*id.storage.s, p);
+                            break;
+                        default: break;
+                    }
+                });
+                putint(p, -1);
+                entities::putitems(p);
+                putint(p, -1);
+                if(m_capture(game::gamemode)) capture::sendaffinity(p);
+                else if(m_defend(game::gamemode)) defend::sendaffinity(p);
+                else if(m_bomber(game::gamemode)) bomber::sendaffinity(p);
+                sendgameinfo = false;
+            }
         }
         if(messages.length())
         {
@@ -1463,9 +1679,7 @@ namespace client
         d->frags = getint(p);
         d->deaths = getint(p);
         d->health = getint(p);
-        d->armour = getint(p);
         d->cptime = getint(p);
-        d->cplaps = getint(p);
         if(resume && (d == game::player1 || d->ai))
         {
             d->weapreset(false);
@@ -1477,9 +1691,8 @@ namespace client
         {
             d->weapreset(true);
             int weap = getint(p);
-            d->lastweap = d->weapselect = isweap(weap) ? weap : W_MELEE;
+            d->weapselect = isweap(weap) ? weap : W_MELEE;
             loopi(W_MAX) d->ammo[i] = getint(p);
-            loopi(W_MAX) d->reloads[i] = getint(p);
         }
     }
 
@@ -1554,16 +1767,12 @@ namespace client
                 d->turnside = (physstate>>7)&2 ? -1 : (physstate>>7)&1;
                 d->impulse[IM_METER] = meter;
                 d->conopen = flags&(1<<8) ? true : false;
-                #define actmod(x,y) \
-                { \
-                    bool val = d->action[x]; \
-                    d->action[x] = flags&(1<<y) ? true : false; \
-                    if(val != d->action[x]) d->actiontime[x] = lastmillis; \
-                }
-                actmod(AC_JUMP, 9);
-                actmod(AC_PACING, 10);
-                actmod(AC_CROUCH, 11);
-                actmod(AC_SPECIAL, 12);
+                loopk(AC_TOTAL)
+                {
+                    bool val = d->action[k];
+                    d->action[k] = flags&(1<<(9+k)) ? true : false;
+                    if(val != d->action[k]) d->actiontime[k] = lastmillis;
+                }
                 vec oldpos(d->o);
                 d->o = o;
                 d->o.z += d->height;
@@ -1627,25 +1836,27 @@ namespace client
             if(verbose > 5) conoutf("[client] msg: %d, prev: %d", type, prevtype);
             switch(type)
             {
-                case N_SERVERINIT:                 // welcome messsage from the server
+                case N_SERVERINIT: // welcome messsage from the server
                 {
-                    int mycn = getint(p), gver = getint(p);
-                    if(gver!=GAMEVERSION)
+                    game::player1->clientnum = getint(p);
+                    sessionver = getint(p);
+                    getstring(game::player1->hostname, p);
+                    getstring(game::player1->hostip, p);
+                    sessionid = getint(p);
+                    if(sessionver != VERSION_GAME)
                     {
-                        conoutft(CON_MESG, "\fryou are using a different game version (you: \fs\fc%d\fS, server: \fs\fc%d\fS)", GAMEVERSION, gver);
+                        conoutft(CON_EVENT, "\frerror: this server is running an incompatible protocol (%d v %d)", sessionver, VERSION_GAME);
                         disconnect();
                         return;
                     }
-                    getstring(game::player1->hostname, p);
-                    if(!game::player1->hostname[0]) copystring(game::player1->hostname, "unknown");
-                    sessionid = getint(p);
-                    game::player1->clientnum = mycn;
-                    if(getint(p)) conoutft(CON_MESG, "\fothe server is password protected");
-                    else if(verbose >= 2) conoutf("\fythe server welcomes us, yay");
+                    conoutf("connected, starting negotiation with server");
                     sendintro();
                     break;
                 }
-                case N_WELCOME: isready = true; break;
+                case N_WELCOME:
+                    conoutf("negotiation with server complete");
+                    isready = true;
+                    break;
 
                 case N_CLIENT:
                 {
@@ -1658,7 +1869,7 @@ namespace client
 
                 case N_SPHY: // simple phys events
                 {
-                    int lcn = getint(p), st = getint(p), param = st == SPHY_POWER || st == SPHY_BUFF ? getint(p) : 0;
+                    int lcn = getint(p), st = getint(p), param = st == SPHY_COOK || st == SPHY_BUFF ? getint(p) : 0;
                     gameent *t = game::getclient(lcn);
                     if(t && (st == SPHY_EXTINGUISH || st == SPHY_BUFF || (t != game::player1 && !t->ai))) switch(st)
                     {
@@ -1674,12 +1885,19 @@ namespace client
                         {
                             t->doimpulse(0, IM_T_BOOST+(st-SPHY_BOOST), lastmillis);
                             game::impulseeffect(t);
+                            if(st == SPHY_KICK || st == SPHY_VAULT || st == SPHY_SKATE || st == SPHY_MELEE)
+                                game::footstep(d);
+                            break;
+                        }
+                        case SPHY_COOK:
+                        {
+                            int value = getint(p);
+                            t->setweapstate(t->weapselect, param, value, lastmillis);
                             break;
                         }
-                        case SPHY_POWER: t->setweapstate(t->weapselect, W_S_POWER, param, lastmillis); break;
                         case SPHY_EXTINGUISH:
                         {
-                            t->resetburning();
+                            t->resetresidual(WR_BURN);
                             playsound(S_EXTINGUISH, t->o, t);
                             part_create(PART_SMOKE, 500, t->feetpos(t->height/2), 0xAAAAAA, t->radius*4, 1, -10);
                             break;
@@ -1701,12 +1919,12 @@ namespace client
 
                 case N_TEXT:
                 {
-                    int tcn = getint(p);
-                    gameent *t = game::getclient(tcn);
-                    int flags = getint(p);
+                    int fcn = getint(p), tcn = getint(p), flags = getint(p);
                     getstring(text, p);
-                    if(!t || isignored(t->clientnum) || isignored(t->ownernum)) break;
-                    saytext(t, flags, text);
+                    gameent *fcp = game::getclient(fcn);
+                    gameent *tcp = game::getclient(tcn);
+                    if(!fcp || isignored(fcp->clientnum) || isignored(fcp->ownernum)) break;
+                    saytext(fcp, tcp, flags, text);
                     break;
                 }
 
@@ -1742,13 +1960,13 @@ namespace client
                     {
                         case 0: case 2:
                         {
-                            conoutft(CON_MESG, "\fyserver requested map change to \fs\fc%s\fS, and we need it, so asking for it", text);
+                            conoutft(CON_EVENT, "\fyserver requested map change to \fs\fc%s\fS, and we need it, so asking for it", text);
                             addmsg(N_GETMAP, "r");
                             break;
                         }
                         case 1:
                         {
-                            conoutft(CON_MESG, "\fyserver is requesting the map from another client for us");
+                            conoutft(CON_EVENT, "\fyserver is requesting the map from another client for us");
                             break;
                         }
                         default: needsmap = false; break;
@@ -1759,79 +1977,83 @@ namespace client
                 case N_GAMEINFO:
                 {
                     int n;
-                    while((n = getint(p)) != -1) entities::setspawn(n, getint(p));
+                    while(p.remaining() && (n = getint(p)) != -1) entities::setspawn(n, getint(p));
                     sendgameinfo = false;
                     break;
                 }
 
-                case N_NEWGAME: // server requests next game
-                {
-                    hud::showscores(false);
-                    if(!menuactive()) showgui("maps", 1);
-                    if(game::intermission) hud::lastnewgame = totalmillis;
-                    break;
-                }
-
                 case N_SETPLAYERINFO:
                 {
                     getstring(text, p);
                     int colour = getint(p), model = getint(p);
-                    string vanity;
+                    string vanity = "";
                     getstring(vanity, p);
+                    int lw = getint(p);
+                    vector<int> lweaps;
+                    loopk(lw) lweaps.add(getint(p));
                     if(!d) break;
-                    filtertext(text, text, true, true, true, MAXNAMELEN);
-                    const char *namestr = text;
-                    while(*namestr && iscubespace(*namestr)) namestr++;
-                    if(!*namestr) namestr = copystring(text, "unnamed");
+                    string namestr = "";
+                    filterstring(namestr, text, true, true, true, true, MAXNAMELEN);
+                    if(!*namestr) copystring(namestr, "unnamed");
                     if(strcmp(d->name, namestr))
                     {
                         string oldname, newname;
                         copystring(oldname, game::colourname(d));
-                        d->setinfo(namestr, colour, model, vanity);
+                        d->setinfo(namestr, colour, model, vanity, lweaps);
                         copystring(newname, game::colourname(d));
                         if(showpresence >= (waiting(false) ? 2 : 1) && !isignored(d->clientnum))
                             conoutft(CON_EVENT, "\fm%s is now known as %s", oldname, newname);
                     }
-                    else d->setinfo(namestr, colour, model, vanity);
+                    else d->setinfo(namestr, colour, model, vanity, lweaps);
                     break;
                 }
 
                 case N_CLIENTINIT: // another client either connected or changed name/team
                 {
-                    int cn = getint(p);
-                    gameent *d = game::newclient(cn);
+                    int tcn = getint(p);
+                    verinfo dummy;
+                    gameent *d = game::newclient(tcn);
                     if(!d)
                     {
-                        loopi(4) getint(p);
-                        loopi(4) getstring(text, p);
+                        loopk(4) getint(p);
+                        getstring(text, p);
+                        int w = getint(p);
+                        loopk(w) getint(p);
+                        loopk(4) getstring(text, p);
+                        dummy.get(p);
                         break;
                     }
                     int colour = getint(p), model = getint(p), team = clamp(getint(p), int(T_NEUTRAL), int(T_ENEMY)), priv = getint(p);
-                    string name;
-                    getstring(name, p);
-                    filtertext(name, name, true, true, true, MAXNAMELEN);
-                    const char *namestr = name;
-                    while(*namestr && iscubespace(*namestr)) namestr++;
-                    if(!*namestr) namestr = copystring(name, "unnamed");
-                    getstring(d->hostname, p);
-                    if(!d->hostname[0]) copystring(d->hostname, "unknown");
-                    getstring(d->handle, p);
                     getstring(text, p);
+                    string namestr = "";
+                    filterstring(namestr, text, true, true, true, true, MAXNAMELEN);
+                    if(!*namestr) copystring(namestr, "unnamed");
+                    string vanity = "";
+                    getstring(vanity, p);
+                    int lw = getint(p);
+                    vector<int> lweaps;
+                    loopk(lw) lweaps.add(getint(p));
+                    getstring(d->handle, p);
+                    getstring(d->hostname, p);
+                    getstring(d->hostip, p);
+                    if(d != game::player1) d->version.get(p);
+                    else dummy.get(p);
                     if(d == game::focus && d->team != team) hud::lastteam = 0;
                     d->team = team;
                     d->privilege = priv;
-                    if(d->name[0]) d->setinfo(namestr, colour, model, text); // already connected
+                    if(d->name[0]) d->setinfo(namestr, colour, model, vanity, lweaps); // already connected
                     else // new client
                     {
-                        d->setinfo(namestr, colour, model, text);
+                        d->setinfo(namestr, colour, model, vanity, lweaps);
                         if(showpresence >= (waiting(false) ? 2 : 1))
                         {
+                            int amt = otherclients(true);
                             if(priv > PRIV_NONE)
                             {
-                                if(d->handle[0]) conoutft(CON_EVENT, "\fg%s (%s) has joined the game (\fs\fy%s\fS: \fs\fc%s\fS)", game::colourname(d), d->hostname, hud::privname(d->privilege), d->handle);
-                                else conoutft(CON_EVENT, "\fg%s (%s) has joined the game (\fs\fylocal %s\fS)", game::colourname(d), d->hostname, hud::privname(d->privilege));
+                                if(d->handle[0]) conoutft(CON_EVENT, "\fg%s (%s) has joined the game (\fs\fy%s\fS: \fs\fc%s\fS) [%d.%d.%d-%s%d] (%d %s)", game::colourname(d), d->hostname, server::privname(d->privilege), d->handle, d->version.major, d->version.minor, d->version.patch, plat_name(d->version.platform), d->version.arch, amt, amt != 1 ? "players" : "player");
+                                else conoutft(CON_EVENT, "\fg%s (%s) has joined the game (\fs\fy%s\fS) [%d.%d.%d-%s%d] (%d %s)", game::colourname(d), d->hostname, server::privname(d->privilege), d->version.major, d->version.minor, d->version.patch, plat_name(d->version.platform), d->version.arch, amt, amt != 1 ? "players" : "player");
                             }
-                            else conoutft(CON_EVENT, "\fg%s (%s) has joined the game", game::colourname(d), d->hostname);
+                            else conoutft(CON_EVENT, "\fg%s (%s) has joined the game [%d.%d.%d-%s%d] (%d %s)", game::colourname(d), d->hostname, d->version.major, d->version.minor, d->version.patch, plat_name(d->version.platform), d->version.arch, amt, amt != 1 ? "players" : "player");
                         }
                         if(needclipboard >= 0) needclipboard++;
                         game::specreset(d);
@@ -1849,8 +2071,8 @@ namespace client
                 case N_LOADW:
                 {
                     hud::showscores(false);
-                    game::player1->loadweap.shrink(0);
-                    if(!menuactive()) showgui("loadout", -1);
+                    if(!menuactive()) showgui("profile", 2);
+                    lastplayerinfo = 0;
                     break;
                 }
 
@@ -1863,7 +2085,7 @@ namespace client
                         parsestate(NULL, p);
                         break;
                     }
-                    f->respawn(lastmillis);
+                    f->respawn(lastmillis, game::gamemode, game::mutators);
                     parsestate(f, p);
                     break;
                 }
@@ -1878,7 +2100,7 @@ namespace client
                         break;
                     }
                     if(f == game::player1 && editmode) toggleedit();
-                    f->respawn(lastmillis);
+                    f->respawn(lastmillis, game::gamemode, game::mutators);
                     parsestate(f, p);
                     game::respawned(f, true, ent);
                     break;
@@ -1901,10 +2123,10 @@ namespace client
                     if(!t || !isweap(weap) || t == game::player1 || t->ai) break;
                     if(weap != t->weapselect && weap != W_MELEE) t->weapswitch(weap, lastmillis);
                     float scale = 1;
-                    int sub = W2(weap, sub, WS(flags));
-                    if(W2(weap, power, WS(flags)))
+                    int sub = W2(weap, ammosub, WS(flags));
+                    if(W2(weap, cooktime, WS(flags)))
                     {
-                        scale = len/float(W2(weap, power, WS(flags)));
+                        scale = len/float(W2(weap, cooktime, WS(flags)));
                         if(sub > 1) sub = int(ceilf(sub*scale));
                     }
                     projs::shootv(weap, flags, sub, 0, scale, from, shots, t, false);
@@ -1936,29 +2158,30 @@ namespace client
 
                 case N_DAMAGE:
                 {
-                    int tcn = getint(p), acn = getint(p), weap = getint(p), flags = getint(p), damage = getint(p),
-                        health = getint(p), armour = getint(p);
-                    vec dir;
+                    int tcn = getint(p), acn = getint(p), weap = getint(p), flags = getint(p), damage = getint(p), health = getint(p);
+                    vec dir, vel;
                     loopk(3) dir[k] = getint(p)/DNF;
+                    loopk(3) vel[k] = getint(p)/DNF;
                     dir.normalize();
-                    gameent *target = game::getclient(tcn), *actor = game::getclient(acn);
-                    if(!target || !actor) break;
-                    game::damaged(weap, flags, damage, health, armour, target, actor, lastmillis, dir);
+                    float dist = getint(p)/DNF;
+                    gameent *m = game::getclient(tcn), *v = game::getclient(acn);
+                    if(!m || !v) break;
+                    game::damaged(weap, flags, damage, health, m, v, lastmillis, dir, vel, dist);
                     break;
                 }
 
                 case N_RELOAD:
                 {
-                    int trg = getint(p), weap = getint(p), amt = getint(p), ammo = getint(p), reloads = getint(p);
-                    gameent *target = game::getclient(trg);
-                    if(!target || !isweap(weap)) break;
-                    weapons::weapreload(target, weap, amt, ammo, reloads, false);
+                    int trg = getint(p), weap = getint(p), amt = getint(p), ammo = getint(p);
+                    gameent *m = game::getclient(trg);
+                    if(!m || !isweap(weap)) break;
+                    weapons::weapreload(m, weap, amt, ammo, false);
                     break;
                 }
 
                 case N_REGEN:
                 {
-                    int trg = getint(p), heal = getint(p), amt = getint(p), armour = getint(p);
+                    int trg = getint(p), heal = getint(p), amt = getint(p);
                     gameent *f = game::getclient(trg);
                     if(!f) break;
                     if(!amt)
@@ -1968,7 +2191,6 @@ namespace client
                     }
                     else if(amt > 0 && (!f->lastregen || lastmillis-f->lastregen >= 500)) playsound(S_REGEN, f->o, f);
                     f->health = heal;
-                    f->armour = armour;
                     f->lastregen = lastmillis;
                     break;
                 }
@@ -1976,7 +2198,7 @@ namespace client
                 case N_DIED:
                 {
                     int vcn = getint(p), deaths = getint(p), acn = getint(p), frags = getint(p), spree = getint(p), style = getint(p), weap = getint(p), flags = getint(p), damage = getint(p), material = getint(p);
-                    gameent *victim = game::getclient(vcn), *actor = game::getclient(acn);
+                    gameent *m = game::getclient(vcn), *v = game::getclient(acn);
                     static vector<gameent *> assist; assist.setsize(0);
                     int count = getint(p);
                     loopi(count)
@@ -1985,40 +2207,40 @@ namespace client
                         gameent *log = game::getclient(lcn);
                         if(log) assist.add(log);
                     }
-                    if(!actor || !victim) break;
-                    victim->deaths = deaths;
-                    actor->frags = frags;
-                    actor->spree = spree;
-                    game::killed(weap, flags, damage, victim, actor, assist, style, material);
-                    victim->lastdeath = lastmillis;
-                    victim->weapreset(true);
+                    if(!v || !m) break;
+                    m->deaths = deaths;
+                    v->frags = frags;
+                    v->spree = spree;
+                    game::killed(weap, flags, damage, m, v, assist, style, material);
+                    m->lastdeath = lastmillis;
+                    m->weapreset(true);
                     break;
                 }
 
                 case N_POINTS:
                 {
                     int acn = getint(p), add = getint(p), points = getint(p);
-                    gameent *actor = game::getclient(acn);
-                    if(!actor) break;
-                    actor->lastpoints = add;
-                    actor->points = points;
+                    gameent *v = game::getclient(acn);
+                    if(!v) break;
+                    v->lastpoints = add;
+                    v->points = points;
                     break;
                 }
 
                 case N_DROP:
                 {
                     int trg = getint(p), weap = getint(p), ds = getint(p);
-                    gameent *target = game::getclient(trg);
-                    bool local = target && (target == game::player1 || target->ai);
+                    gameent *m = game::getclient(trg);
+                    bool local = m && (m == game::player1 || m->ai);
                     if(ds) loopj(ds)
                     {
-                        int gs = getint(p), drop = getint(p), ammo = getint(p), reloads = getint(p);
-                        if(target) projs::drop(target, gs, drop, ammo, reloads, local, j, weap);
+                        int gs = getint(p), drop = getint(p), ammo = getint(p);
+                        if(m) projs::drop(m, gs, drop, ammo, local, j, weap);
                     }
-                    if(isweap(weap) && target)
+                    if(isweap(weap) && m)
                     {
-                        target->weapswitch(weap, lastmillis, weaponswitchdelay);
-                        playsound(WSND(weap, S_W_SWITCH), target->o, target, 0, -1, -1, -1, &target->wschan);
+                        m->weapswitch(weap, lastmillis, weaponswitchdelay);
+                        playsound(WSND(weap, S_W_SWITCH), m->o, m, 0, -1, -1, -1, &m->wschan);
                     }
                     break;
                 }
@@ -2026,9 +2248,9 @@ namespace client
                 case N_WSELECT:
                 {
                     int trg = getint(p), weap = getint(p);
-                    gameent *target = game::getclient(trg);
-                    if(!target || !isweap(weap)) break;
-                    weapons::weapselect(target, weap, G(weaponinterrupts), false);
+                    gameent *m = game::getclient(trg);
+                    if(!m || !isweap(weap)) break;
+                    weapons::weapselect(m, weap, (1<<W_S_SWITCH)|(1<<W_S_RELOAD), false);
                     break;
                 }
 
@@ -2039,9 +2261,14 @@ namespace client
                         int lcn = getint(p);
                         if(p.overread() || lcn < 0) break;
                         gameent *f = game::newclient(lcn);
-                        if(f && f!=game::player1 && !f->ai) f->respawn();
+                        if(!f)
+                        {
+                            parsestate(NULL, p);
+                            break;
+                        }
+                        if(f && f != game::player1 && !f->ai) f->respawn(lastmillis, game::gamemode, game::mutators);
                         parsestate(f, p, true);
-                        f->setscale(game::rescale(f), 0, true, game::gamemode, game::mutators);
+                        f->setscale(game::rescale(f), 0, true);
                     }
                     break;
                 }
@@ -2053,9 +2280,9 @@ namespace client
                     gameentity &e = *(gameentity *)entities::ents[ent];
                     entities::setspawn(ent, value);
                     ai::itemspawned(ent, value!=0);
-                    if(e.spawned)
+                    if(e.spawned())
                     {
-                        int sweap = m_weapon(game::gamemode, game::mutators), attr = e.type == WEAPON ? w_attr(game::gamemode, game::mutators, e.attrs[0], sweap) : e.attrs[0],
+                        int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap),
                             colour = e.type == WEAPON ? W(attr, colour) : 0xFFFFFF;
                         playsound(e.type == WEAPON && attr >= W_OFFSET ? WSND(attr, S_W_SPAWN) : S_ITEMSPAWN, e.o);
                         if(entities::showentdescs)
@@ -2089,12 +2316,12 @@ namespace client
 
                 case N_ITEMACC:
                 { // uses a specific drop so the client knows what to replace
-                    int lcn = getint(p), ent = getint(p), ammoamt = getint(p), reloadamt = getint(p), spawn = getint(p),
-                        weap = getint(p), drop = getint(p), ammo = getint(p), reloads = getint(p);
-                    gameent *target = game::getclient(lcn);
-                    if(!target) break;
+                    int lcn = getint(p), ent = getint(p), ammoamt = getint(p), spawn = getint(p),
+                        weap = getint(p), drop = getint(p), ammo = getint(p);
+                    gameent *m = game::getclient(lcn);
+                    if(!m) break;
                     if(entities::ents.inrange(ent) && enttype[entities::ents[ent]->type].usetype == EU_ITEM)
-                        entities::useeffects(target, ent, ammoamt, reloadamt, spawn, weap, drop, ammo, reloads);
+                        entities::useeffects(m, ent, ammoamt, spawn, weap, drop, ammo);
                     break;
                 }
 
@@ -2166,8 +2393,8 @@ namespace client
 
                 case N_CLIPBOARD:
                 {
-                    int cn = getint(p), unpacklen = getint(p), packlen = getint(p);
-                    gameent *d = game::getclient(cn);
+                    int tcn = getint(p), unpacklen = getint(p), packlen = getint(p);
+                    gameent *d = game::getclient(tcn);
                     ucharbuf q = p.subbuf(max(packlen, 0));
                     if(d) unpackeditinfo(d->edit, q.buf, q.maxlen, unpacklen);
                     break;
@@ -2209,7 +2436,7 @@ namespace client
                 case N_REMIP:
                 {
                     if(!d) return;
-                    conoutft(CON_MESG, "\fy%s remipped", game::colourname(d));
+                    conoutft(CON_EVENT, "\fy%s remipped", game::colourname(d));
                     mpremip(false);
                     break;
                 }
@@ -2250,8 +2477,11 @@ namespace client
                     break;
 
                 case N_TICK:
-                    game::timeupdate(getint(p));
+                {
+                    int state = getint(p), remain = getint(p);
+                    game::timeupdate(state, remain);
                     break;
+                }
 
                 case N_SERVMSG:
                 {
@@ -2264,26 +2494,51 @@ namespace client
                 case N_SENDDEMOLIST:
                 {
                     int demos = getint(p);
-                    if(demos <= 0) conoutft(CON_MESG, "\fono demos available");
+                    if(demos <= 0) conoutft(CON_EVENT, "\fono demos available");
                     else loopi(demos)
                     {
                         getstring(text, p);
+                        int len = getint(p), ctime = getint(p);
                         if(p.overread()) break;
-                        conoutft(CON_MESG, "\fademo: %d. %s", i+1, text);
+                        conoutft(CON_EVENT, "\fydemo: %2d. \fs\fc%s\fS recorded \fs\fc%s UTC\fS [\fs\fw%.2f%s\fS]", i+1, text, gettime(ctime, "%Y-%m-%d %H:%M.%S"), len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB");
                     }
                     break;
                 }
 
                 case N_DEMOPLAYBACK:
                 {
-                    int on = getint(p);
-                    if(on) game::player1->state = CS_SPECTATOR;
-                    else
+                    bool wasdemopb = demoplayback;
+                    demoplayback = getint(p)!=0;
+                    if(demoplayback) game::player1->state = CS_SPECTATOR;
+                    else loopv(game::players) if(game::players[i]) game::clientdisconnected(i);
+                    game::player1->clientnum = getint(p);
+                    if(!demoplayback && wasdemopb && demoendless)
                     {
-                        loopv(game::players) if(game::players[i]) game::clientdisconnected(i);
+                        string demofile;
+                        demofile[0] = 0;
+                        if(*demolist)
+                        {
+                            int r = rnd(listlen(demolist)), len = 0;
+                            const char *elem = indexlist(demolist, r, len);
+                            if(len > 0) copystring(demofile, elem, len+1);
+                        }
+                        if(!*demofile)
+                        {
+                            vector<char *> files;
+                            listfiles("demos", "dmo", files);
+                            while(!files.empty())
+                            {
+                                int r = rnd(files.length());
+                                if(files[r][0] != '.')
+                                {
+                                    copystring(demofile, files[r]);
+                                    break;
+                                }
+                                else files.remove(r);
+                            }
+                        }
+                        if(*demofile) addmsg(N_MAPVOTE, "rsi2", demofile, G_DEMO, 0);
                     }
-                    demoplayback = on!=0;
-                    game::player1->clientnum = getint(p);
                     break;
                 }
 
@@ -2323,13 +2578,15 @@ namespace client
                     int sn = getint(p), val = getint(p);
                     gameent *s = game::newclient(sn);
                     if(!s) break;
-                    if(s == game::player1) game::resetfollow();
+                    if(s == game::player1)
+                    {
+                        game::specreset();
+                        if(m_race(game::gamemode)) game::specmode = 0;
+                    }
                     if(val != 0)
                     {
                         if(s == game::player1 && editmode) toggleedit();
                         s->state = CS_SPECTATOR;
-                        s->checkpoint = -1;
-                        s->cpmillis = 0;
                         s->quarantine = val == 2;
                     }
                     else
@@ -2337,8 +2594,6 @@ namespace client
                         if(s->state == CS_SPECTATOR)
                         {
                             s->state = CS_WAITING;
-                            s->checkpoint = -1;
-                            s->cpmillis = 0;
                             if(s != game::player1 && !s->ai) s->resetinterp();
                             game::waiting.removeobj(s);
                         }
@@ -2359,12 +2614,13 @@ namespace client
                         s->stopmoving(true);
                         game::waiting.setsize(0);
                         gameent *d;
-                        loopv(game::players) if((d = game::players[i]) && d->aitype == AI_NONE && d->state == CS_WAITING)
+                        loopv(game::players) if((d = game::players[i]) && d->actortype == A_PLAYER && d->state == CS_WAITING)
                             game::waiting.add(d);
                     }
                     else if(!s->ai) s->resetinterp();
                     game::waiting.removeobj(s);
                     if(s->state == CS_ALIVE) s->lastdeath = lastmillis; // so spawndelay shows properly
+                    else entities::spawnplayer(s); // so they're not nowhere
                     s->state = CS_WAITING;
                     s->quarantine = false;
                     s->weapreset(true);
@@ -2378,7 +2634,7 @@ namespace client
                     if(!w) return;
                     if(w->team != tn)
                     {
-                        if(m_team(game::gamemode, game::mutators) && w->aitype == AI_NONE && showteamchange >= (w->team != T_NEUTRAL && tn != T_NEUTRAL ? 1 : 2))
+                        if(m_team(game::gamemode, game::mutators) && w->actortype == A_PLAYER && showteamchange >= (w->team != T_NEUTRAL && tn != T_NEUTRAL ? 1 : 2))
                             conoutft(CON_EVENT, "\fa%s is now on team %s", game::colourname(w), game::colourteam(tn));
                         w->team = tn;
                         if(w == game::focus) hud::lastteam = 0;
@@ -2405,7 +2661,7 @@ namespace client
                     int vn = getint(p);
                     gameent *v = game::getclient(vn);
                     getstring(text, p);
-                    filtertext(text, text);
+                    filterstring(text, text);
                     int reqmode = getint(p), reqmuts = getint(p);
                     if(!v) break;
                     vote(v, text, reqmode, reqmuts);
@@ -2425,18 +2681,27 @@ namespace client
                 {
                     int tn = getint(p), ent = getint(p);
                     gameent *t = game::getclient(tn);
-                    if(!t)
+                    if(!t || !m_race(game::gamemode))
                     {
+                        if(ent < 0) break;
                         if(getint(p) < 0) break;
                         loopi(2) getint(p);
                         break;
                     }
                     if(ent >= 0)
                     {
-                        if(m_trial(game::gamemode) && entities::ents.inrange(ent) && entities::ents[ent]->type == CHECKPOINT)
+                        if(entities::ents.inrange(ent) && entities::ents[ent]->type == CHECKPOINT)
                         {
-                            if(t != game::player1 && !t->ai && (!t->cpmillis || entities::ents[ent]->attrs[6] == CP_START))
-                                t->cpmillis = lastmillis;
+                            if(t != game::player1 && !t->ai && (!t->cpmillis || entities::ents[ent]->attrs[6] == CP_START)) t->cpmillis = lastmillis;
+                            if((checkpointannounce&(t != game::focus ? 2 : 1) || (m_gsp3(game::gamemode, game::mutators) && checkpointannounce&4)) && checkpointannouncefilter&(1<<entities::ents[ent]->attrs[6]))
+                            {
+                                switch(entities::ents[ent]->attrs[6])
+                                {
+                                    case CP_START: game::announce(S_V_START, t); break;
+                                    case CP_FINISH: case CP_LAST: game::announce(S_V_COMPLETE, t); break;
+                                    default: game::announce(S_V_CHECKPOINT, t); break;
+                                }
+                            }
                             entities::execlink(t, ent, false);
                         }
                         int laptime = getint(p);
@@ -2444,14 +2709,15 @@ namespace client
                         {
                             t->cplast = laptime;
                             t->cptime = getint(p);
-                            t->cplaps = getint(p);
+                            t->points = getint(p);
                             t->cpmillis = t->impulse[IM_METER] = 0;
-                            if(showlaptimes > (t != game::focus ? (t->aitype > AI_NONE ? 2 : 1) : 0))
+                            if(showlaptimes >= (t != game::focus ? (t->actortype > A_PLAYER ? 3 : 2) : 1))
                             {
-                                defformatstring(best)("%s", timestr(t->cptime));
-                                conoutft(t != game::player1 ? CON_INFO : CON_SELF, "%s completed in \fs\fg%s\fS (best: \fs\fy%s\fS, laps: \fs\fc%d\fS)", game::colourname(t), timestr(t->cplast), best, t->cplaps);
+                                defformatstring(best)("%s", timestr(t->cptime, 1));
+                                conoutft(t != game::player1 ? CON_INFO : CON_SELF, "%s completed in \fs\fg%s\fS (best: \fs\fy%s\fS, laps: \fs\fc%d\fS)", game::colourname(t), timestr(t->cplast, 1), best, t->points);
                             }
                         }
+                        else if(!m_gsp2(game::gamemode, game::mutators)) t->impulse[IM_METER] = 0;
                     }
                     else
                     {
@@ -2535,12 +2801,12 @@ namespace client
 
                 case N_GETMAP:
                 {
-                    conoutft(CON_MESG, "\fyserver has requested we send the map..");
+                    conoutft(CON_EVENT, "\fyserver has requested we send the map..");
                     if(!needsmap && !gettingmap) sendmap();
                     else
                     {
-                        if(!gettingmap) conoutft(CON_MESG, "\fy..we don't have the map though, so asking for it instead");
-                        else conoutft(CON_MESG, "\fy..but we're in the process of getting it");
+                        if(!gettingmap) conoutft(CON_EVENT, "\fy..we don't have the map though, so asking for it instead");
+                        else conoutft(CON_EVENT, "\fy..but we're in the process of getting it");
                         addmsg(N_GETMAP, "r");
                     }
                     break;
@@ -2548,10 +2814,10 @@ namespace client
 
                 case N_SENDMAP:
                 {
-                    conoutft(CON_MESG, "\fymap data has been uploaded to the server");
+                    conoutft(CON_EVENT, "\fymap data has been uploaded to the server");
                     if(needsmap && !gettingmap)
                     {
-                        conoutft(CON_MESG, "\fy.. and we want the map too, so asking for it");
+                        conoutft(CON_EVENT, "\fy.. and we want the map too, so asking for it");
                         addmsg(N_GETMAP, "r");
                     }
                     break;
@@ -2559,7 +2825,7 @@ namespace client
 
                 case N_FAILMAP:
                 {
-                    if(needsmap) conoutft(CON_MESG, "\fyunable to load map, nobody else has a copy of it..");
+                    if(needsmap) conoutft(CON_EVENT, "\fyunable to load map, nobody else has a copy of it..");
                     needsmap = gettingmap = false;
                     break;
                 }
@@ -2575,7 +2841,7 @@ namespace client
                     {
                         int newsize = 0;
                         while(1<<newsize < getworldsize()) newsize++;
-                        conoutft(CON_MESG, size>=0 ? "\fy%s started a new map of size \fs\fc%d\fS" : "\fy%s enlarged the map to size \fs\fc%d\fS", game::colourname(d), newsize);
+                        conoutft(CON_EVENT, size>=0 ? "\fy%s started a new map of size \fs\fc%d\fS" : "\fy%s enlarged the map to size \fs\fc%d\fS", game::colourname(d), newsize);
                     }
                     break;
                 }
@@ -2587,9 +2853,12 @@ namespace client
                     int tm = getint(p), cl = getint(p), md = getint(p);
                     string vanity;
                     getstring(vanity, p);
+                    int lw = getint(p);
+                    vector<int> lweaps;
+                    loopk(lw) lweaps.add(getint(p));
                     gameent *b = game::newclient(bn);
                     if(!b) break;
-                    ai::init(b, at, et, on, sk, bn, text, tm, cl, md, vanity);
+                    ai::init(b, at, et, on, sk, bn, text, tm, cl, md, vanity, lweaps);
                     break;
                 }
 
@@ -2599,7 +2868,7 @@ namespace client
                     getstring(text, p);
                     if(accountname[0] && accountpass[0])
                     {
-                        if(verbose) conoutft(CON_MESG, "\fyanswering account challenge..");
+                        //conoutft(CON_EVENT, "\fyidentifying as: \fs\fc%s\fS (\fs\fw%d\fS)", accountname, id);
                         vector<char> buf;
                         answerchallenge(accountpass, text, buf);
                         addmsg(N_AUTHANS, "ris", id, buf.getbuf());
@@ -2648,7 +2917,7 @@ namespace client
         if(a->address.host == ENET_HOST_ANY || a->ping >= serverinfo::WAITING || a->attr.empty()) ac = -1;
         else
         {
-            ac = a->attr[0] == GAMEVERSION ? 0x7FFF : clamp(a->attr[0], 0, 0x7FFF-1);
+            ac = a->attr[0] == VERSION_GAME ? 0x7FFF : clamp(a->attr[0], 0, 0x7FFF-1);
             ac <<= 16;
             if(a->address.host == masteraddress.host) ac |= 0xFFFF;
             else ac |= clamp(1 + a->priority, 1, 0xFFFF-1);
@@ -2656,7 +2925,7 @@ namespace client
         if(b->address.host == ENET_HOST_ANY || b->ping >= serverinfo::WAITING || b->attr.empty()) bc = -1;
         else
         {
-            bc = b->attr[0] == GAMEVERSION ? 0x7FFF : clamp(b->attr[0], 0, 0x7FFF-1);
+            bc = b->attr[0] == VERSION_GAME ? 0x7FFF : clamp(b->attr[0], 0, 0x7FFF-1);
             bc <<= 16;
             if(b->address.host == masteraddress.host) bc |= 0xFFFF;
             else bc |= clamp(1 + b->priority, 1, 0xFFFF-1);
@@ -2812,6 +3081,10 @@ namespace client
                     if(idx < 0) intret(si->players.length());
                     else if(si->players.inrange(idx)) result(si->players[idx]);
                     break;
+                case 3:
+                    if(idx < 0) intret(si->handles.length());
+                    else if(si->handles.inrange(idx)) result(si->handles[idx]);
+                    break;
             }
         }
     }
diff --git a/src/game/compass.h b/src/game/compass.h
index 93465c0..f098199 100644
--- a/src/game/compass.h
+++ b/src/game/compass.h
@@ -1,7 +1,7 @@
 FVAR(IDF_PERSIST, compasssize, 0, 0.15f, 1000);
 FVAR(IDF_PERSIST, compassblend, 0, 0.75f, 1);
 VAR(IDF_PERSIST, compassfade, 0, 250, VAR_MAX);
-FVAR(IDF_PERSIST, compassfadeamt, 0, 0.5f, 1);
+FVAR(IDF_PERSIST, compassfadeamt, 0, 0.75f, 1);
 TVAR(IDF_PERSIST, compasstex, "<grey>textures/hud/compass", 3);
 TVAR(IDF_PERSIST, compassringtex, "<grey>textures/hud/progress", 3);
 
@@ -25,12 +25,14 @@ struct cmenu : cstate
 {
     Texture *icon;
     vector<caction> actions;
-    cmenu() : icon(NULL) {}
+    bool keep;
+    cmenu() : icon(NULL), keep(false) {}
     ~cmenu() { reset(); }
     void reset()
     {
         loopvrev(actions) actions.remove(i);
         actions.shrink(0);
+        keep = false;
     }
 
     int locate(const char code)
@@ -101,19 +103,21 @@ ICOMMAND(0, compass, "sss", (char *n, char *a, char *b), if(curcompass)
         char code = '1';
         while(true)
         {
-            if(code > '9') code = 'a';
             if(curcompass->locate(code) < 0) break;
-            else if(code >= 'z')
+            code++;
+            if(code > '9' && code < 'A') code = 'A';
+            else if(code > 'Z')
             {
                 code = 0;
                 break;
             }
-            code++;
         }
         if(code) addaction(n, code, a);
     }
 });
 
+ICOMMAND(0, keepcompass, "", (void), if(curcompass) curcompass->keep = true);
+
 void showcmenu(const char *name)
 {
     if(!name || !*name) return;
@@ -174,7 +178,7 @@ void renderaction(int idx, int size, Texture *t, char code, const char *name, bo
         if(idx) drawslice(0.5f/8+(idx-2)/float(8), 1/float(8), hudwidth/2, hudheight/2, size);
         else drawsized(hudwidth/2-size*3/8, hudheight/2-size*3/8, size*3/4);
     }
-    if(code) y += draw_textx("[%s]", x, y, r, g, b, int((idx ? 255 : f)*compassblend), compassdir[idx].align, -1, -1, getkeyname(code));
+    if(code) y += draw_textx("\f{%s}", x, y, r, g, b, int((idx ? 255 : f)*compassblend), compassdir[idx].align, -1, -1, getkeyname(code));
     popfont();
     pushfont(!idx || hit ? "emphasis" : "reduced");
     draw_textx("%s", x, y, r, g, b, int(f*compassblend), compassdir[idx].align, -1, -1, name);
@@ -199,7 +203,7 @@ void rendercmenu()
         loopi(curcompass->actions.length()-8)
         {
             caction &c = curcompass->actions[i+8];
-            y += draw_textx("\fs\fa[\fS%s\fs\fa]\fS %s", x, y, 255, 255, 255, int(192*compassblend), TEXT_CENTERED, -1, -1, getkeyname(c.code), c.name);
+            y += draw_textx("\f{%s} %s", x, y, 255, 255, 255, int(192*compassblend), TEXT_CENTERED, -1, -1, getkeyname(c.code), c.name);
             if(y >= maxy) break;
         }
         popfont();
@@ -212,6 +216,7 @@ bool runcmenu(int idx)
     cmenu *oldcompass = curcompass;
     if(curcompass)
     {
+        curcompass->keep = false;
         if(idx < 0)
         {
             foundmenu = true;
@@ -222,7 +227,7 @@ bool runcmenu(int idx)
             foundmenu = interactive = true;
             execute(curcompass->actions[idx].contents);
             interactive = false;
-            if(oldcompass == curcompass) clearcmenu();
+            if(oldcompass == curcompass && !curcompass->keep) clearcmenu();
         }
     }
     return foundmenu;
diff --git a/src/game/defend.cpp b/src/game/defend.cpp
index 1b9b0b1..dba44a7 100644
--- a/src/game/defend.cpp
+++ b/src/game/defend.cpp
@@ -10,7 +10,7 @@ namespace defend
 
     void preload()
     {
-        preloadmodel("props/flag");
+        preloadmodel("props/point");
     }
 
     static vec skewcolour(int owner, int enemy, float occupy)
@@ -18,7 +18,7 @@ namespace defend
         vec colour = vec::hexcolor(TEAM(owner, colour));
         if(enemy)
         {
-            int team = owner && enemy && !defendinstant ? T_NEUTRAL : enemy;
+            int team = owner && enemy && !m_gsp1(game::gamemode, game::mutators) ? T_NEUTRAL : enemy;
             int timestep = totalmillis%1000;
             float amt = clamp((timestep <= 500 ? timestep/500.f : (1000-timestep)/500.f)*occupy, 0.f, 1.f);
             colour.lerp(vec::hexcolor(TEAM(team, colour)), amt);
@@ -31,7 +31,6 @@ namespace defend
         loopv(st.flags) // flags/bases
         {
             defendstate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent)) continue;
             cament *c = cameras.add(new cament);
             c->o = f.o;
             c->o.z += enttype[AFFINITY].radius*2/3;
@@ -62,41 +61,34 @@ namespace defend
         loopv(st.flags)
         {
             defendstate::flag &b = st.flags[i];
-            if(!entities::ents.inrange(b.ent)) continue;
-            float occupy = b.occupied(defendinstant, defendcount);
-            entitylight *light = &entities::ents[b.ent]->light;
-            light->material[0] = bvec::fromcolor(skewcolour(b.owner, b.enemy, occupy));
-            rendermodel(light, "props/flag", ANIM_MAPMODEL|ANIM_LOOP, b.o, entities::ents[b.ent]->attrs[1], entities::ents[b.ent]->attrs[2], 0, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED);
+            float occupy = b.occupied(m_gsp1(game::gamemode, game::mutators), defendcount);
+            vec effect = skewcolour(b.owner, b.enemy, occupy);
+            int colour = effect.tohexcolor();
+            b.baselight.material[0] = bvec::fromcolor(effect);
+            rendermodel(&b.baselight, "props/point", ANIM_MAPMODEL|ANIM_LOOP, b.render, b.yaw, 0, 0, MDL_DYNSHADOW|MDL_CULL_VFC|MDL_CULL_OCCLUDED, NULL, NULL, 0, 0, 1);
             if(b.enemy && b.owner)
             {
                 defformatstring(bowner)("%s", game::colourteam(b.owner));
-                formatstring(b.info)("%s vs. %s", bowner, game::colourteam(b.enemy));
+                formatstring(b.info)("%s - %s v %s", b.name, bowner, game::colourteam(b.enemy));
             }
             else
             {
                 int defend = b.owner ? b.owner : b.enemy;
-                formatstring(b.info)("%s", game::colourteam(defend));
+                formatstring(b.info)("%s - %s", b.name, game::colourteam(defend));
             }
-            vec above = b.o;
-            above.z += enttype[AFFINITY].radius*2/3;
-            defformatstring(name)("<huge>%s", b.name);
-            part_textcopy(above, name);
-            above.z += 2.f;
-            part_text(above, b.info);
-            above.z += 3.f;
+            vec above = b.above;
+            float blend = camera1->o.distrange(above, enttype[AFFINITY].radius, enttype[AFFINITY].radius/8);
+            part_explosion(above, 3, PART_SHOCKBALL, 1, colour, 1, 0.5f*blend);
+            above.z += 4;
+            part_text(above, b.info, PART_TEXT, 1, 0xFFFFFF, 2, blend);
+            above.z += 3;
             if(b.enemy)
             {
-                part_icon(above, textureload(hud::progresstex, 3), 3, 1, 0, 0, 1, (int(light->material[0].x)<<16)|(int(light->material[0].y)<<8)|int(light->material[0].z), (lastmillis%1000)/1000.f, 0.1f);
-                part_icon(above, textureload(hud::progresstex, 3), 2, 1, 0, 0, 1, TEAM(b.enemy, colour), 0, occupy);
-                part_icon(above, textureload(hud::progresstex, 3), 2, 0.25f, 0, 0, 1, TEAM(b.owner, colour), occupy, 1-occupy);
+                part_icon(above, textureload(hud::progringtex, 3), 4, blend, 0, 0, 1, colour, (lastmillis%1000)/1000.f, 0.1f);
+                part_icon(above, textureload(hud::progresstex, 3), 4, blend, 0, 0, 1, TEAM(b.enemy, colour), 0, occupy);
+                part_icon(above, textureload(hud::progresstex, 3), 4, 0.25f*blend, 0, 0, 1, TEAM(b.owner, colour), occupy, 1-occupy);
             }
-            else
-            {
-                part_icon(above, textureload(hud::progresstex, 3), 3, 1, 0, 0, 1, TEAM(b.owner, colour));
-                part_icon(above, textureload(hud::progresstex, 3), 2, 1, 0, 0, 1, TEAM(b.owner, colour));
-            }
-            above.z += 1.f;
-            defformatstring(str)("<huge>%d%%", int(occupy*100.f)); part_textcopy(above, str);
+            else part_icon(above, textureload(hud::teamtexname(b.owner), 3), 3, blend, 0, 0, 1, TEAM(b.owner, colour));
         }
     }
 
@@ -106,8 +98,7 @@ namespace defend
         loopv(st.flags)
         {
             defendstate::flag &f = st.flags[i];
-            if(!entities::ents.inrange(f.ent)) continue;
-            float occupy = f.occupied(defendinstant, defendcount);
+            float occupy = f.occupied(m_gsp1(game::gamemode, game::mutators), defendcount);
             adddynlight(vec(f.o).add(vec(0, 0, enttype[AFFINITY].radius)), enttype[AFFINITY].radius*2, skewcolour(f.owner, f.enemy, occupy), 0, 0, DL_KEEP);
         }
     }
@@ -117,17 +108,18 @@ namespace defend
         loopv(st.flags)
         {
             defendstate::flag &f = st.flags[i];
-            float occupy = f.occupied(defendinstant, defendcount);
-            vec colour = skewcolour(f.owner, f.enemy, occupy), dir = vec(f.o).sub(camera1->o);
-            const char *tex = f.hasflag ? hud::arrowtex : (f.owner == game::focus->team && f.enemy ? hud::alerttex : hud::flagtex);
-            float size = hud::radaraffinitysize*(f.hasflag ? 1.25f : 1);
+            float occupy = f.occupied(m_gsp1(game::gamemode, game::mutators), defendcount);
+            vec colour = skewcolour(f.owner, f.enemy, occupy);
+            bool attack = f.owner == game::focus->team && f.enemy;
+            const char *tex = f.hasflag ? hud::arrowtex : (attack ? hud::attacktex : hud::pointtex);
+            float size = hud::radaraffinitysize*(f.hasflag ? 1.5f : (attack ? 0.65f : 1.f));
             if(hud::radaraffinitynames >= (f.hasflag ? 1 : 2))
             {
                 bool overthrow = f.owner && f.enemy == game::focus->team;
-                if(occupy < 1.f) hud::drawblip(tex, f.hasflag ? 3 : 2, w, h, size, blend*hud::radaraffinityblend, f.hasflag ? -1-hud::radarstyle : hud::radarstyle, f.hasflag ? dir : f.o, colour, "little", "\f[%d]%d%%", f.hasflag ? (overthrow ? 0xFF8800 : (occupy < 1.f ? 0xFFFF00 : 0x00FF00)) : TEAM(f.owner, colour), int(occupy*100.f));
-                else hud::drawblip(tex, f.hasflag ? 3 : 2, w, h, size, blend*hud::radaraffinityblend, f.hasflag ? -1-hud::radarstyle : hud::radarstyle, f.hasflag ? dir : f.o, colour, "little", "\f[%d]%s", f.hasflag ? (overthrow ? 0xFF8800 : (occupy < 1.f ? 0xFFFF00 : 0x00FF00)) : TEAM(f.owner, colour), TEAM(f.owner, name));
+                if(occupy < 1.f) hud::drawblip(tex, f.hasflag ? 3 : 2, w, h, size, blend*hud::radaraffinityblend, f.hasflag ? 0 : -1, f.o, colour, "little", "\f[%d]%d%%", f.hasflag ? (overthrow ? 0xFF8800 : (occupy < 1.f ? 0xFFFF00 : 0x00FF00)) : TEAM(f.owner, colour), int(occupy*100.f));
+                else hud::drawblip(tex, f.hasflag ? 3 : 2, w, h, size, blend*hud::radaraffinityblend, f.hasflag ? 0 : -1, f.o, colour, "little", "\f[%d]%s", f.hasflag ? (overthrow ? 0xFF8800 : (occupy < 1.f ? 0xFFFF00 : 0x00FF00)) : TEAM(f.owner, colour), TEAM(f.owner, name));
             }
-            else hud::drawblip(tex, f.hasflag ? 3 : 2, w, h, size, blend*hud::radaraffinityblend, f.hasflag ? -1 : hud::radarstyle, f.hasflag ? dir : f.o, colour);
+            else hud::drawblip(tex, f.hasflag ? 3 : 2, w, h, size, blend*hud::radaraffinityblend, f.hasflag ? 0 : -1, f.o, colour);
         }
     }
 
@@ -147,9 +139,9 @@ namespace defend
             {
                 defendstate::flag &f = st.flags[i];
                 pushfont("emphasis");
-                float occupy = !f.owner || f.enemy ? clamp(f.converted/float((!defendinstant && f.owner ? 2 : 1) * defendcount), 0.f, 1.f) : 1.f;
+                float occupy = !f.owner || f.enemy ? clamp(f.converted/float(defendcount), 0.f, 1.f) : 1.f;
                 bool overthrow = f.owner && f.enemy == game::focus->team;
-                ty += draw_textx("%s \fs\f[%d]\f(%s)\f(%s)\fS \fs%s%d%%\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, overthrow ? "Overthrow" : "Secure", TEAM(f.owner, colour), hud::teamtexname(f.owner), hud::flagtex, overthrow ? "\fy" : (occupy < 1.f ? "\fc" : "\fg"), int(occupy*100.f))*hud::noticescale;
+                ty += draw_textx("%s %s \fs\f[%d]\f(%s)\f(%s)\fS \fs%s%d%%\fS", tx, ty, 255, 255, 255, int(255*blend), TEXT_CENTERED, -1, -1, overthrow ? "Overthrow" : "Secure", f.name, TEAM(f.owner, colour), hud::teamtexname(f.owner), hud::pointtex, overthrow ? "\fy" : (occupy < 1.f ? "\fc" : "\fg"), int(occupy*100.f))*hud::noticescale;
                 popfont();
                 break;
             }
@@ -158,10 +150,11 @@ namespace defend
 
     int drawinventory(int x, int y, int s, int m, float blend)
     {
-        int sy = 0;
+        int sy = 0, numflags = st.flags.length(), estsize = ((numflags-1)*s*hud::inventoryskew)+s, fitsize = y-m, size = s;
+        if(estsize > fitsize) size = int((fitsize/float(estsize))*s);
         loopv(st.flags)
         {
-            if(y-sy-s < m) break;
+            if(y-sy-size < m) break;
             defendstate::flag &f = st.flags[i];
             bool hasflag = game::focus->state == CS_ALIVE && insideaffinity(f, game::focus);
             if(f.hasflag != hasflag) { f.hasflag = hasflag; f.lasthad = lastmillis-max(1000-(lastmillis-f.lasthad), 0); }
@@ -170,7 +163,7 @@ namespace defend
             if(headsup || f.hasflag || millis <= 1000)
             {
                 float skew = headsup ? hud::inventoryskew : 0.f,
-                    occupy = f.enemy ? clamp(f.converted/float((!defendinstant && f.owner ? 2 : 1)*defendcount), 0.f, 1.f) : (f.owner ? 1.f : 0.f);
+                    occupy = f.enemy ? clamp(f.converted/float(defendcount), 0.f, 1.f) : (f.owner ? 1.f : 0.f);
                 vec c = vec::hexcolor(TEAM(f.owner, colour)), c1 = c;
                 if(f.enemy)
                 {
@@ -182,16 +175,17 @@ namespace defend
                 else if(millis <= 1000) skew += (1.f-skew)-(clamp(float(millis)/1000.f, 0.f, 1.f)*(1.f-skew));
                 int oldy = y-sy;
                 if(hasflag || f.enemy)
-                    sy += hud::drawitem(hud::flagtex, x, oldy, s, 0, true, false, c.r, c.g, c.b, blend, skew, "super", "%s%d%%", hasflag ? (f.owner && f.enemy == game::focus->team ? "\fy" : (occupy < 1.f ? "\fc" : "\fg")) : "\fw", int(occupy*100.f));
-                else sy += hud::drawitem(hud::flagtex, x, oldy, s, 0, true, false, c.r, c.g, c.b, blend, skew);
+                    sy += hud::drawitem(hud::pointtex, x, oldy, size, 0, true, false, c.r, c.g, c.b, blend, skew, "super", "%s%d%%", hasflag ? (f.owner && f.enemy == game::focus->team ? "\fy" : (occupy < 1.f ? "\fc" : "\fg")) : "\fw", int(occupy*100.f));
+                else sy += hud::drawitem(hud::pointtex, x, oldy, size, 0, true, false, c.r, c.g, c.b, blend, skew);
                 if(f.enemy)
                 {
                     vec c2 = vec::hexcolor(TEAM(f.enemy, colour));
-                    hud::drawitem(hud::attacktex, x, oldy, s, 0.5f, true, false, c2.r, c2.g, c2.b, blend, skew);
-                    hud::drawitembar(x, oldy, s, false, c.r, c.g, c.b, blend, skew, occupy);
+                    hud::drawitem(hud::attacktex, x, oldy, size, 0.5f, true, false, c2.r, c2.g, c2.b, blend, skew);
+                    hud::drawitembar(x, oldy, size, false, c.r, c.g, c.b, blend, skew, occupy);
                 }
                 else if(f.owner)
-                    hud::drawitem(hud::teamtexname(f.owner), x, oldy, s, 0.5f, true, false, c1.r, c1.g, c1.b, blend, skew);
+                    hud::drawitem(hud::teamtexname(f.owner), x, oldy, size, 0.5f, true, false, c1.r, c1.g, c1.b, blend, skew);
+                hud::drawitemtext(x, oldy, size, false, skew, "default", blend, "%s", f.name);
             }
         }
         return sy;
@@ -204,60 +198,57 @@ namespace defend
 
     void setup()
     {
+        int df = m_gsp2(game::gamemode, game::mutators) ? 0 : defendflags;
         loopv(entities::ents)
         {
             extentity *e = entities::ents[i];
             if(e->type != AFFINITY || !m_check(e->attrs[3], e->attrs[4], game::gamemode, game::mutators)) continue;
             int team = e->attrs[0];
-            switch(defendflags)
+            switch(df)
             {
                 case 3:
-                {
                     if(team && !isteam(game::gamemode, game::mutators, team, T_NEUTRAL)) team = T_NEUTRAL;
                     break;
-                }
                 case 2:
-                {
                     if(!isteam(game::gamemode, game::mutators, team, T_FIRST)) continue;
                     break;
-                }
                 case 1:
-                {
                     if(team && !isteam(game::gamemode, game::mutators, team, T_NEUTRAL)) continue;
                     break;
-                }
                 case 0: team = T_NEUTRAL; break;
             }
-            defendstate::flag &b = st.flags.add();
-            b.o = e->o;
-            defformatstring(alias)("flag_%d", e->attrs[4]);
+            defformatstring(alias)("point_%d", e->attrs[5]);
             const char *name = getalias(alias);
-            if(name[0]) copystring(b.name, name);
-            else formatstring(b.name)("flag %d", st.flags.length());
-            b.ent = i;
-            b.kinship = team;
-            b.reset();
+            if(!name || !*name)
+            {
+                formatstring(alias)("point #%d", st.flags.length()+1);
+                name = alias;
+            }
+            st.addaffinity(e->o, team, e->attrs[1], e->attrs[2], name);
         }
         if(!st.flags.length()) return; // map doesn't seem to support this mode at all..
-        int bases[T_ALL] = {0};
-        bool hasteams = true;
-        loopv(st.flags) bases[st.flags[i].kinship]++;
-        loopi(numteams(game::gamemode, game::mutators)-1) if(!bases[i+1] || (bases[i+1] != bases[i+2]))
+        bool hasteams = df != 0;
+        if(hasteams)
         {
-            loopvk(st.flags) st.flags[k].kinship = T_NEUTRAL;
-            hasteams = false;
-            break;
+            int bases[T_ALL] = {0};
+            loopv(st.flags) bases[st.flags[i].kinship]++;
+            loopi(numteams(game::gamemode, game::mutators)-1) if(!bases[i+1] || (bases[i+1] != bases[i+2]))
+            {
+                loopvk(st.flags) st.flags[k].kinship = T_NEUTRAL;
+                hasteams = false;
+                break;
+            }
         }
         if(m_gsp2(game::gamemode, game::mutators))
         {
             vec average(0, 0, 0);
             int count = 0;
-            loopv(st.flags) if(!hasteams || st.flags[i].kinship != T_NEUTRAL)
+            loopv(st.flags)
             {
                 average.add(st.flags[i].o);
                 count++;
             }
-            int smallest = -1;
+            int smallest = rnd(st.flags.length());
             if(count)
             {
                 average.div(count);
@@ -268,11 +259,13 @@ namespace defend
                     dist = tdist;
                 }
             }
-            if(!st.flags.inrange(smallest)) smallest = rnd(st.flags.length());
-            int ent = st.flags[smallest].ent;
-            copystring(st.flags[smallest].name, "the flag");
-            st.flags[smallest].kinship = T_NEUTRAL;
-            loopv(st.flags) if(st.flags[i].ent != ent) st.flags.remove(i--);
+            if(st.flags.inrange(smallest))
+            {
+                copystring(st.flags[smallest].name, "center");
+                st.flags[smallest].kinship = T_NEUTRAL;
+                loopi(smallest) st.flags.remove(0);
+                while(st.flags.length() > 1) st.flags.remove(1);
+            }
         }
     }
 
@@ -284,7 +277,8 @@ namespace defend
         {
             defendstate::flag &b = st.flags[i];
             putint(p, b.kinship);
-            putint(p, b.ent);
+            putint(p, b.yaw);
+            putint(p, b.pitch);
             loopj(3) putint(p, int(b.o[j]*DMF));
             sendstring(b.name, p);
         }
@@ -296,14 +290,15 @@ namespace defend
         while(st.flags.length() > numflags) st.flags.pop();
         loopi(numflags)
         {
-            int kin = getint(p), ent = getint(p), converted = getint(p), owner = getint(p), enemy = getint(p);
+            int kin = getint(p), yaw = getint(p), pitch = getint(p), converted = getint(p), owner = getint(p), enemy = getint(p);
             vec o;
             loopj(3) o[j] = getint(p)/DMF;
             string name;
             getstring(name, p);
             if(p.overread()) break;
+            if(i >= MAXPARAMS) continue;
             while(!st.flags.inrange(i)) st.flags.add();
-            st.initaffinity(i, kin, ent, o, owner, enemy, converted, name);
+            st.initaffinity(i, kin, yaw, pitch, o, owner, enemy, converted, name);
         }
     }
 
@@ -319,24 +314,22 @@ namespace defend
                 {
                     gameent *d = NULL, *e = NULL;
                     int numdyns = game::numdynents();
-                    loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e->type == ENT_PLAYER && insideaffinity(b, e))
+                    loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e->actortype < A_ENEMY && insideaffinity(b, e))
                         if((d = e) == game::focus) break;
-                    game::announcef(S_V_FLAGSECURED, CON_INFO, d, true, "\fateam %s secured \fw%s", game::colourteam(owner), b.name);
+                    game::announcef(S_V_FLAGSECURED, CON_SELF, d, true, "\fateam %s secured \fw%s", game::colourteam(owner), 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);
+                    if(game::dynlighteffects) adddynlight(b.o, enttype[AFFINITY].radius*2, vec::hexcolor(TEAM(owner, colour)).mul(2.f), 500, 250);
                 }
             }
             else if(b.owner)
             {
                 gameent *d = NULL, *e = NULL;
                 int numdyns = game::numdynents();
-                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e->type == ENT_PLAYER && insideaffinity(b, e))
+                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && e->actortype < A_ENEMY && insideaffinity(b, e))
                     if((d = e) == game::focus) break;
-                game::announcef(S_V_FLAGOVERTHROWN, CON_INFO, d, true, "\fateam %s overthrew \fw%s", game::colourteam(enemy), b.name);
+                game::announcef(S_V_FLAGOVERTHROWN, CON_SELF, d, true, "\fateam %s overthrew \fw%s", game::colourteam(enemy), 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);
+                if(game::dynlighteffects) adddynlight(b.o, enttype[AFFINITY].radius*2, vec::hexcolor(TEAM(enemy, colour)).mul(2.f), 500, 250);
             }
             b.converted = converted;
         }
@@ -349,13 +342,6 @@ namespace defend
         hud::teamscore(team).total = total;
     }
 
-    int aiowner(gameent *d)
-    {
-        loopv(st.flags) if(entities::ents.inrange(st.flags[i].ent) && entities::ents[d->aientity]->links.find(st.flags[i].ent) >= 0)
-            return st.flags[i].owner ? st.flags[i].owner : st.flags[i].enemy;
-        return d->team;
-    }
-
     bool aicheck(gameent *d, ai::aistate &b)
     {
         return false;
@@ -363,7 +349,7 @@ namespace defend
 
     void aifind(gameent *d, ai::aistate &b, vector<ai::interest> &interests)
     {
-        if(d->aitype == AI_BOT)
+        if(d->actortype == A_BOT)
         {
             vec pos = d->feetpos();
             loopvj(st.flags)
@@ -375,13 +361,13 @@ namespace defend
                 gameent *e = NULL;
                 bool regen = !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
                 int numdyns = game::numdynents();
-                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && ai::owner(d) == ai::owner(e))
+                loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && d->team == e->team)
                 {
                     vec ep = e->feetpos();
                     if(targets.find(e->clientnum) < 0 && ep.squaredist(f.o) <= (enttype[AFFINITY].radius*enttype[AFFINITY].radius))
                         targets.add(e->clientnum);
                 }
-                if((!regen && f.owner == ai::owner(d)) || (targets.empty() && (f.owner != ai::owner(d) || f.enemy)))
+                if((!regen && f.owner == d->team) || (targets.empty() && (f.owner != d->team || f.enemy)))
                 {
                     ai::interest &n = interests.add();
                     n.state = ai::AI_S_DEFEND;
@@ -391,6 +377,7 @@ namespace defend
                     n.score = pos.squaredist(f.o)/(!regen ? 100.f : 1.f);
                     n.tolerance = 0.25f;
                     n.team = true;
+                    n.acttype = ai::AI_A_PROTECT;
                 }
             }
         }
@@ -401,19 +388,19 @@ namespace defend
         if(st.flags.inrange(b.target))
         {
             defendstate::flag &f = st.flags[b.target];
-            bool regen = d->aitype != AI_BOT || !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
-            int walk = f.enemy && f.enemy != ai::owner(d) ? 1 : 0;
-            if(regen && (!f.enemy && ai::owner(d) == f.owner))
+            bool regen = d->actortype != A_BOT || !m_regen(game::gamemode, game::mutators) || d->health >= m_health(game::gamemode, game::mutators, d->model);
+            int walk = f.enemy && f.enemy != d->team ? 1 : 0;
+            if(regen && (!f.enemy && d->team == f.owner))
             {
                 static vector<int> targets; // build a list of others who are interested in this
                 targets.setsize(0);
                 ai::checkothers(targets, d, ai::AI_S_DEFEND, ai::AI_T_AFFINITY, b.target, true);
-                if(d->aitype == AI_BOT)
+                if(d->actortype == A_BOT)
                 {
                     gameent *e = NULL;
                     int numdyns = game::numdynents();
                     float mindist = enttype[AFFINITY].radius*4; mindist *= mindist;
-                    loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && ai::owner(d) == ai::owner(e))
+                    loopi(numdyns) if((e = (gameent *)game::iterdynents(i)) && !e->ai && e->state == CS_ALIVE && d->team == e->team)
                     {
                         vec ep = e->feetpos();
                         if(targets.find(e->clientnum) < 0 && ep.squaredist(f.o) <= mindist)
@@ -431,7 +418,7 @@ namespace defend
                 }
                 else walk = 1;
             }
-            return ai::defense(d, b, f.o, !f.enemy || m_gsp2(game::gamemode, game::mutators) ? ai::CLOSEDIST : float(enttype[AFFINITY].radius), !f.enemy || m_gsp2(game::gamemode, game::mutators) ? ai::CLOSEDIST : float(enttype[AFFINITY].radius*walk*8), walk);
+            return ai::defense(d, b, f.o, enttype[AFFINITY].radius, enttype[AFFINITY].radius*walk*8, m_gsp2(game::gamemode, game::mutators) ? 0 : walk);
         }
         return false;
     }
diff --git a/src/game/defend.h b/src/game/defend.h
index 6b74899..6b18f78 100644
--- a/src/game/defend.h
+++ b/src/game/defend.h
@@ -1,9 +1,7 @@
 #ifdef GAMESERVER
     #define defendstate stfservstate
-    #define defendinstant (m_gsp1(gamemode, mutators) || m_gsp2(gamemode, mutators))
     #define defendcount (m_gsp2(gamemode, mutators) ? G(defendking) : G(defendoccupy))
 #else
-    #define defendinstant (m_gsp1(game::gamemode, game::mutators) || m_gsp2(game::gamemode, game::mutators))
     #define defendcount (m_gsp2(game::gamemode, game::mutators) ? G(defendking) : G(defendoccupy))
 #endif
 
@@ -12,21 +10,20 @@ struct defendstate
     struct flag
     {
         vec o;
-        int kinship, ent, owner, enemy;
+        int kinship, yaw, pitch, owner, enemy;
         string name;
 #ifndef GAMESERVER
         string info;
         bool hasflag;
         int lasthad;
+        vec render, above;
+        entitylight baselight;
 #endif
-        int owners, enemies, converted, securetime;
+        int owners, enemies, converted, points;
 
         flag()
         {
             kinship = T_NEUTRAL;
-#ifndef GAMESERVER
-            ent = -1;
-#endif
             reset();
         }
 
@@ -41,8 +38,7 @@ struct defendstate
         {
             noenemy();
             owner = kinship;
-            securetime = -1;
-            owners = 0;
+            yaw = pitch = owners = points = 0;
 #ifndef GAMESERVER
             hasflag = false;
             lasthad = 0;
@@ -92,51 +88,78 @@ struct defendstate
         {
             if(enemy != team) return -1;
             converted += units;
-            if(units<0)
+
+            if(units < 0)
             {
-                if(converted<=0) noenemy();
+                if(converted <= 0) noenemy();
                 return -1;
             }
-            else if(converted<(!instant && owner ? 2 : 1)*occupy) return -1;
-            if(!instant && owner) { owner = T_NEUTRAL; converted = 0; enemy = team; return 0; }
-            else { owner = team; securetime = 0; owners = enemies; noenemy(); return 1; }
+            else if(converted < occupy) return -1;
+
+            if(!instant && owner)
+            {
+                owner = T_NEUTRAL;
+                converted = points = 0;
+                enemy = team;
+                return 0;
+            }
+            else
+            {
+                owner = team;
+                points = 0;
+                owners = enemies;
+                noenemy();
+                return 1;
+            }
         }
 
         float occupied(bool instant, float amt)
         {
-            return (enemy ? enemy : owner) ? (!owner || enemy ? clamp(converted/float((!instant && owner ? 2 : 1)*amt), 0.f, 1.f) : 1.f) : 0.f;
+            return (enemy ? enemy : owner) ? (!owner || enemy ? clamp(converted/amt, 0.f, 1.f) : 1.f) : 0.f;
         }
     };
 
     vector<flag> flags;
-    int secured;
 
-    defendstate() : secured(0) {}
+    defendstate() { reset(); }
 
     void reset()
     {
         flags.shrink(0);
-        secured = 0;
     }
 
-    void addaffinity(const vec &o, int team, int ent, const char *name)
+    void addaffinity(const vec &o, int team, int yaw, int pitch, const char *name)
     {
         flag &b = flags.add();
-        b.o = o;
         b.kinship = team;
         b.reset();
-        b.ent = ent;
+        b.o = o;
+#ifndef GAMESERVER
+        b.render = b.above = o;
+        b.render.z += 2;
+        physics::droptofloor(b.render);
+        if(b.render.z >= b.above.z-1) b.above.z += (b.render.z-(b.above.z-1))+2;
+#endif
+        b.yaw = yaw;
+        b.pitch = pitch;
         copystring(b.name, name);
     }
 
-    void initaffinity(int i, int kin, int ent, vec &o, int owner, int enemy, int converted, const char *name)
+    void initaffinity(int i, int kin, int yaw, int pitch, vec &o, int owner, int enemy, int converted, const char *name)
     {
         if(!flags.inrange(i)) return;
         flag &b = flags[i];
         b.kinship = kin;
         b.reset();
-        b.ent = ent;
+        b.yaw = yaw;
+        b.pitch = pitch;
         b.o = o;
+#ifndef GAMESERVER
+        b.render = b.above = o;
+        b.render.z += 2;
+        physics::droptofloor(b.render);
+        if(b.render.z >= b.above.z-1) b.above.z += (b.render.z-(b.above.z-1))+2;
+#endif
         b.owner = owner;
         b.enemy = enemy;
         b.converted = converted;
@@ -189,7 +212,6 @@ namespace defend
     extern void preload();
     extern void render();
     extern void adddynlights();
-    extern int aiowner(gameent *d);
     extern void aifind(gameent *d, ai::aistate &b, vector<ai::interest> &interests);
     extern bool aicheck(gameent *d, ai::aistate &b);
     extern bool aidefense(gameent *d, ai::aistate &b);
diff --git a/src/game/defendmode.h b/src/game/defendmode.h
index 9c2c2f2..5b9b236 100644
--- a/src/game/defendmode.h
+++ b/src/game/defendmode.h
@@ -5,7 +5,7 @@ struct defendservmode : defendstate, servmode
 
     defendservmode() : scoresec(0), hasflaginfo(false) {}
 
-    void reset(bool empty)
+    void reset()
     {
         defendstate::reset();
         scoresec = 0;
@@ -15,7 +15,7 @@ struct defendservmode : defendstate, servmode
     void stealaffinity(int n, int team)
     {
         flag &b = flags[n];
-        loopv(clients) if(clients[i]->state.aitype < AI_START)
+        loopv(clients) if(clients[i]->state.actortype < A_ENEMY)
         {
             server::clientinfo *ci = clients[i];
             if(ci->state.state==CS_ALIVE && ci->team && ci->team == team && insideaffinity(b, ci->state.o))
@@ -52,7 +52,7 @@ struct defendservmode : defendstate, servmode
     {
         if(!points) return;
         flag &b = flags[i];
-        loopvk(clients) if(clients[k]->state.aitype < AI_START && team == clients[k]->team && insideaffinity(b, clients[k]->state.o)) givepoints(clients[k], points);
+        if(!m_nopoints(gamemode, mutators)) loopvk(clients) if(clients[k]->state.actortype < A_ENEMY && team == clients[k]->team && insideaffinity(b, clients[k]->state.o)) givepoints(clients[k], points);
         score &cs = teamscore(team);
         cs.total += points;
         sendf(-1, 1, "ri3", N_SCORE, team, cs.total);
@@ -71,8 +71,8 @@ struct defendservmode : defendstate, servmode
             {
                 if(!b.owners || !b.enemies)
                 {
-                    int pts = b.occupy(b.enemy, G(defendpoints)*(b.enemies ? b.enemies : -(1+b.owners))*t, defendcount, defendinstant);
-                    if(pts > 0) loopvk(clients) if(clients[k]->state.aitype < AI_START && b.owner == clients[k]->team && insideaffinity(b, clients[k]->state.o)) givepoints(clients[k], G(defendpoints));
+                    int pts = b.occupy(b.enemy, G(defendpoints)*(b.enemies ? b.enemies : -(1+b.owners))*t, defendcount, m_gsp1(gamemode, mutators));
+                    if(!m_nopoints(gamemode, mutators) && pts > 0) loopvk(clients) if(clients[k]->state.actortype < A_ENEMY && b.owner == clients[k]->team && insideaffinity(b, clients[k]->state.o)) givepoints(clients[k], G(defendpoints));
                 }
                 sendaffinity(i);
             }
@@ -80,8 +80,13 @@ struct defendservmode : defendstate, servmode
             {
                 if(!m_gsp2(gamemode, mutators) || b.owners)
                 {
-                    b.securetime += t;
-                    int score = b.securetime/G(defendinterval) - (b.securetime-t)/G(defendinterval);
+                    b.points += G(defendpoints)*t;
+                    int score = 0;
+                    while(b.points >= G(defendhold))
+                    {
+                        b.points -= G(defendhold);
+                        score++;
+                    }
                     if(score) addscore(i, b.owner, score);
                 }
                 sendaffinity(i);
@@ -111,7 +116,8 @@ struct defendservmode : defendstate, servmode
         {
             flag &b = flags[i];
             putint(p, b.kinship);
-            putint(p, b.ent);
+            putint(p, b.yaw);
+            putint(p, b.pitch);
             putint(p, b.converted);
             putint(p, b.owner);
             putint(p, b.enemy);
@@ -137,7 +143,7 @@ struct defendservmode : defendstate, servmode
 
     void endcheck()
     {
-        if(m_balance(gamemode, mutators)) return;
+        if(m_balance(gamemode, mutators, teamspawns)) return;
         int maxscore = G(defendlimit) ? G(defendlimit) : INT_MAX-1;
         loopi(numteams(gamemode, mutators))
         {
@@ -154,37 +160,37 @@ struct defendservmode : defendstate, servmode
 
     void entergame(clientinfo *ci)
     {
-        if(!hasflaginfo || ci->state.state!=CS_ALIVE || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || ci->state.state!=CS_ALIVE || ci->state.actortype >= A_ENEMY) return;
         enteraffinity(ci->team, ci->state.o);
     }
 
     void spawned(clientinfo *ci)
     {
-        if(!hasflaginfo || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || ci->state.actortype >= A_ENEMY) return;
         enteraffinity(ci->team, ci->state.o);
     }
 
     void leavegame(clientinfo *ci, bool disconnecting = false)
     {
-        if(!hasflaginfo || ci->state.state!=CS_ALIVE || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || ci->state.state!=CS_ALIVE || ci->state.actortype >= A_ENEMY) return;
         leaveaffinity(ci->team, ci->state.o);
     }
 
-    void died(clientinfo *ci, clientinfo *actor)
+    void died(clientinfo *ci, clientinfo *v)
     {
-        if(!hasflaginfo || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || ci->state.actortype >= A_ENEMY) return;
         leaveaffinity(ci->team, ci->state.o);
     }
 
     void moved(clientinfo *ci, const vec &oldpos, const vec &newpos)
     {
-        if(!hasflaginfo || ci->state.aitype >= AI_START) return;
+        if(!canplay(hasflaginfo) || ci->state.actortype >= A_ENEMY) return;
         moveaffinity(ci->team, oldpos, newpos);
     }
 
     void regen(clientinfo *ci, int &total, int &amt, int &delay)
     {
-        if(!hasflaginfo || !G(defendregenbuff) || !ci->state.lastbuff) return;
+        if(!canplay(hasflaginfo) || !G(defendregenbuff) || !ci->state.lastbuff) return;
         if(G(maxhealth)) total = max(m_maxhealth(gamemode, mutators, ci->state.model), total);
         if(ci->state.lastregen && G(defendregendelay)) delay = G(defendregendelay);
         if(G(defendregenextra)) amt += G(defendregenextra);
@@ -192,21 +198,21 @@ struct defendservmode : defendstate, servmode
 
     void checkclient(clientinfo *ci)
     {
-        if(!hasflaginfo || ci->state.state != CS_ALIVE || m_insta(gamemode, mutators)) return;
-        #define defendbuff4 (G(defendbuffing)&4 && b.occupied(defendinstant, defendcount) >= G(defendbuffoccupy))
+        if(!canplay(hasflaginfo) || ci->state.state != CS_ALIVE || m_insta(gamemode, mutators)) return;
+        #define defendbuff4 (G(defendbuffing)&4 && b.occupied(m_gsp1(gamemode, mutators), defendcount) >= G(defendbuffoccupy))
         #define defendbuff1 (G(defendbuffing)&1 && b.owner == ci->team && (!b.enemy || defendbuff4))
         #define defendbuff2 (G(defendbuffing)&2 && b.owner == T_NEUTRAL && (b.enemy == ci->team || defendbuff4))
         if(G(defendbuffing)) loopv(flags)
         {
             flag &b = flags[i];
-            if((defendbuff1 || defendbuff2) && insideaffinity(b, ci->state.o, G(defendbuffarea)))
+            if((defendbuff1 || defendbuff2) && (G(defendbuffarea) ? insideaffinity(b, ci->state.o, G(defendbuffarea)) : true))
             {
                 if(!ci->state.lastbuff) sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 1);
                 ci->state.lastbuff = gamemillis;
                 return;
             }
         }
-        if(ci->state.lastbuff && (!G(defendbuffing) || gamemillis-ci->state.lastbuff > G(defendbuffdelay)))
+        if(ci->state.lastbuff && (!G(defendbuffing) || gamemillis-ci->state.lastbuff >= G(defendbuffdelay)))
         {
             ci->state.lastbuff = 0;
             sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 0);
@@ -220,12 +226,13 @@ struct defendservmode : defendstate, servmode
         {
             loopi(numflags)
             {
-                int kin = getint(p), ent = getint(p);
+                int kin = getint(p), yaw = getint(p), pitch = getint(p);
                 vec o;
                 loopj(3) o[j] = getint(p)/DMF;
                 string name;
                 getstring(name, p);
-                if(!hasflaginfo) addaffinity(o, kin, ent, name);
+                if(p.overread()) break;
+                if(!hasflaginfo && i < MAXPARAMS) addaffinity(o, kin, yaw, pitch, name);
             }
             if(!hasflaginfo)
             {
@@ -236,11 +243,11 @@ struct defendservmode : defendstate, servmode
         }
     }
 
-    int points(clientinfo *victim, clientinfo *actor)
+    int points(clientinfo *m, clientinfo *v)
     {
-        bool isteam = victim==actor || victim->team == actor->team;
-        int p = isteam ? -1 : 1, v = p;
-        loopv(flags) if(insideaffinity(flags[i], victim->state.o)) p += v;
+        bool isteam = m==v || m->team == v->team;
+        int p = isteam ? -1 : 1, q = p;
+        loopv(flags) if(insideaffinity(flags[i], m->state.o)) p += q;
         return p;
     }
 
diff --git a/src/game/duelmut.h b/src/game/duelmut.h
index edca1e3..d2178db 100644
--- a/src/game/duelmut.h
+++ b/src/game/duelmut.h
@@ -2,17 +2,32 @@
 struct duelservmode : servmode
 {
     int duelround, dueltime, duelcheck, dueldeath, duelwinner, duelwins;
+    bool waitforhumans;
     vector<clientinfo *> duelqueue, allowed, playing, respawns;
 
     duelservmode() {}
 
-    bool checkready(clientinfo *ci)
+    void shrink()
     {
-        if(ci->state.aitype < AI_START)
-        {
-            if(m_loadout(gamemode, mutators) && !chkloadweap(ci, false)) return false;
-        }
-        return true;
+        allowed.shrink(0);
+        playing.shrink(0);
+        respawns.shrink(0);
+    }
+
+    void doreset()
+    {
+        dueltime = G(duelcooloff);
+        duelcheck = dueldeath = -1;
+    }
+
+    void reset()
+    {
+        waitforhumans = true;
+        shrink();
+        duelqueue.shrink(0);
+        doreset();
+        duelround = duelwins = 0;
+        duelwinner = -1;
     }
 
     void position(clientinfo *ci, int n)
@@ -28,28 +43,36 @@ struct duelservmode : servmode
 
     void queue(clientinfo *ci, bool pos = true, bool top = false, bool wait = true)
     {
-        if(ci->state.state != CS_SPECTATOR && ci->state.state != CS_EDITING && ci->state.aitype < AI_START)
+        if(ci->state.actortype >= A_ENEMY || ci->state.state == CS_SPECTATOR) return;
+        if(ci->state.actortype == A_PLAYER && waitforhumans) waitforhumans = false;
+        int n = duelqueue.find(ci);
+        if(top)
         {
-            int n = duelqueue.find(ci);
-            if(top)
-            {
-                if(n >= 0) duelqueue.remove(n);
-                duelqueue.insert(0, ci);
-                n = 0;
-            }
-            else if(n < 0)
-            {
-                n = duelqueue.length();
-                duelqueue.add(ci);
-            }
-            if(allowed.find(ci) >= 0) allowed.removeobj(ci);
-            if(respawns.find(ci) >= 0) respawns.removeobj(ci);
-            if(wait && ci->state.state != CS_WAITING) waiting(ci, DROP_RESET);
-            if(pos) position(ci, n);
+            if(n >= 0) duelqueue.remove(n);
+            duelqueue.insert(0, ci);
+            n = 0;
         }
+        else if(n < 0)
+        {
+            n = duelqueue.length();
+            duelqueue.add(ci);
+        }
+        if(allowed.find(ci) >= 0) allowed.removeobj(ci);
+        if(respawns.find(ci) >= 0) respawns.removeobj(ci);
+        if(wait && ci->state.state != CS_WAITING) waiting(ci, DROP_RESET);
+        if(pos) position(ci, n);
     }
 
-    void entergame(clientinfo *ci) { queue(ci); }
+    void entergame(clientinfo *ci)
+    {
+        queue(ci);
+        if(dueltime < 0 && dueldeath < 0 && m_affinity(gamemode)) switch(G(duelaffinity))
+        {
+            case 2: allowed.add(ci); // fall through because they need to be in respawns too
+            case 1: respawns.add(ci); break;
+            case 0: default: break;
+        }
+    }
     void leavegame(clientinfo *ci, bool disconnecting = false)
     {
         if(duelwinner == ci->clientnum)
@@ -63,46 +86,45 @@ struct duelservmode : servmode
         respawns.removeobj(ci);
     }
 
-    bool damage(clientinfo *target, clientinfo *actor, int damage, int weap, int flags, int material, const ivec &hitpush)
+    bool damage(clientinfo *m, clientinfo *v, int damage, int weap, int flags, int material, const ivec &hitpush, const ivec &hitvel, float dist)
     {
-        if(dueltime >= 0 && target->state.aitype < AI_START) return false;
+        if(dueltime >= 0 && m->state.actortype < A_ENEMY) return false;
         return true;
     }
 
     bool canspawn(clientinfo *ci, bool tryspawn = false)
     {
-        if(!checkready(ci)) return false;
-        if(allowed.find(ci) >= 0 || ci->state.aitype >= AI_START) return true;
+        if(allowed.find(ci) >= 0 || ci->state.actortype >= A_ENEMY) return true;
+        else if(tryspawn)
+        {
+            if(!m_affinity(gamemode)) queue(ci);
+            return true;
+        }
         if(m_affinity(gamemode) && respawns.find(ci) >= 0)
         {
+            int delay = m_xdelay(gamemode, mutators, ci->team);
+            if(delay && ci->state.respawnwait(gamemillis, delay)) return false;
             if(m_survivor(gamemode, mutators))
             {
                 int alive = 0;
                 vector<clientinfo *> mates;
-                loopv(clients) if(clients[i]->state.aitype < AI_START && clients[i]->team == ci->team)
+                loopv(clients) if(clients[i]->state.actortype < A_ENEMY && clients[i]->team == ci->team)
                 { // includes ci
-                    if(clients[i]->state.state == CS_ALIVE) alive++;
                     mates.add(clients[i]);
+                    if(clients[i]->state.state == CS_ALIVE && (!G(duelbotcheck) || clients[i]->state.actortype != A_BOT)) alive++;
                 }
                 if(!alive)
                 {
-                    loopv(mates)
-                    {
-                        if(allowed.find(mates[i]) < 0) allowed.add(mates[i]);
-                        if(mates[i]->state.state == CS_DEAD) waiting(mates[i], DROP_RESET);
-                    }
+                    loopv(mates) if(allowed.find(mates[i]) < 0) allowed.add(mates[i]);
                     duelcheck = gamemillis+1000;
                     return true;
                 }
                 return false; // only respawn when all dead
             }
-            int delay = m_xdelay(gamemode, mutators);
-            if(delay && ci->state.respawnwait(gamemillis, delay)) return false;
             if(allowed.find(ci) < 0) allowed.add(ci);
             duelcheck = gamemillis+1000;
             return true; // overrides it
         }
-        if(tryspawn && dueltime < 0 && dueldeath < 0) queue(ci);
         return false; // you spawn when we want you to buddy
     }
 
@@ -113,7 +135,7 @@ struct duelservmode : servmode
 
     void layout()
     {
-        loopvj(clients) if(clients[j]->state.aitype < AI_START)
+        loopvj(clients) if(clients[j]->state.actortype < A_ENEMY)
         {
             vector<int> shots;
             loop(a, W_MAX) loop(b, 2)
@@ -131,15 +153,16 @@ struct duelservmode : servmode
 
     void scoreaffinity(clientinfo *ci, bool win)
     {
-        if(!m_affinity(gamemode) || dueltime >= 0 || duelround <= 0 || !win) return;
+        if(!m_affinity(gamemode) || dueltime >= 0 || duelround <= 0) return;
         respawns.shrink(0);
-        loopv(clients) if(clients[i]->state.aitype < AI_START) switch(clients[i]->state.state)
+        #define scoredteam(x,y) (win ? x != y : x == y)
+        loopv(clients) if(clients[i]->state.actortype < A_ENEMY) switch(clients[i]->state.state)
         {
             case CS_ALIVE:
-                if(playing.find(clients[i]) < 0 || clients[i]->team != ci->team) queue(clients[i], false);
+                if(playing.find(clients[i]) < 0 || scoredteam(clients[i]->team, ci->team)) queue(clients[i], false);
                 break;
             case CS_DEAD:
-                if(playing.find(clients[i]) < 0 || clients[i]->team != ci->team)
+                if(playing.find(clients[i]) < 0 || scoredteam(clients[i]->team, ci->team))
                 {
                     queue(clients[i], false);
                     break;
@@ -149,7 +172,7 @@ struct duelservmode : servmode
                 duelcheck = gamemillis+1000;
                 break;
             case CS_WAITING:
-                if(playing.find(clients[i]) < 0 || clients[i]->team != ci->team)
+                if(playing.find(clients[i]) < 0 || scoredteam(clients[i]->team, ci->team))
                 {
                     queue(clients[i], false);
                     break;
@@ -163,8 +186,7 @@ struct duelservmode : servmode
 
     void clear()
     {
-        duelcheck = dueldeath = -1;
-        dueltime = gamemillis+G(duellimit);
+        doreset();
         bool reset = false;
         if(m_duel(gamemode, mutators) && G(duelcycle)&(m_team(gamemode, mutators) ? 2 : 1) && duelwinner >= 0 && duelwins > 0)
         {
@@ -173,7 +195,7 @@ struct duelservmode : servmode
             {
                 int numwins = G(duelcycles), numplrs = 0;
                 loopv(clients)
-                    if(clients[i]->state.aitype < AI_START && clients[i]->state.state != CS_SPECTATOR && clients[i]->team == ci->team)
+                    if(clients[i]->state.actortype < A_ENEMY && clients[i]->state.state != CS_SPECTATOR && clients[i]->team == ci->team)
                         numplrs++;
                 if(numplrs > (m_team(gamemode, mutators) ? 1 : 2))
                 {
@@ -188,74 +210,80 @@ struct duelservmode : servmode
             }
         }
         loopv(clients) queue(clients[i], false, !reset && clients[i]->state.state == CS_ALIVE, reset || G(duelreset) || clients[i]->state.state != CS_ALIVE);
-        allowed.shrink(0);
-        playing.shrink(0);
-        respawns.shrink(0);
+        shrink();
     }
 
     void update()
     {
-        if(interm || !hasgameinfo || numclients(-1, true, AI_BOT) <= 1) return;
+        if(!canplay() || waitforhumans) return;
         if(dueltime >= 0)
         {
-            if(gamemillis >= dueltime && !duelqueue.empty())
+            if(dueltime && ((dueltime -= curtime) <= 0)) dueltime = 0;
+            if(!dueltime)
             {
+                shrink();
                 int wants = max(numteams(gamemode, mutators), 2);
+                loopv(clients) if(clients[i]->state.state != CS_SPECTATOR && clients[i]->state.state != CS_ALIVE) queue(clients[i], false);
                 loopv(duelqueue)
                 {
                     if(m_duel(gamemode, mutators) && playing.length() >= wants) break;
-                    clientinfo *ci = duelqueue[i];
-                    if(!checkready(ci)) continue; // they are not ready yet.
-                    if(ci->state.state != CS_ALIVE)
+                    if(duelqueue[i]->state.state != CS_ALIVE)
                     {
-                        if(ci->state.state != CS_WAITING) waiting(ci, DROP_RESET);
+                        if(duelqueue[i]->state.state != CS_WAITING) waiting(duelqueue[i], DROP_RESET);
                         if(m_duel(gamemode, mutators) && m_team(gamemode, mutators))
                         {
                             bool skip = false;
-                            loopvj(playing) if(ci->team == playing[j]->team) { skip = true; break; }
+                            loopvj(playing) if(duelqueue[i]->team == playing[j]->team) { skip = true; break; }
                             if(skip) continue;
                         }
-                        if(allowed.find(ci) < 0) allowed.add(ci);
-                    }
-                    else
-                    {
-                        ci->state.lastregen = gamemillis;
-                        ci->state.resetresidual();
-                        ci->state.health = m_health(gamemode, mutators, ci->state.model);
-                        ci->state.armour = m_armour(gamemode, mutators, ci->state.model);
-                        sendf(-1, 1, "ri5", N_REGEN, ci->clientnum, ci->state.health, ci->state.armour, 0); // amt = 0 regens impulse
                     }
-                    playing.add(ci);
-                    if(m_affinity(gamemode)) respawns.add(ci);
+                    playing.add(duelqueue[i]);
                 }
-                loopv(playing) duelqueue.removeobj(playing[i]);
                 if(playing.length() >= wants)
                 {
                     if(smode) smode->layout();
                     mutate(smuts, mut->layout());
-                    loopv(duelqueue) position(duelqueue[i], i);
                     duelround++;
-                    string fight;
+                    string fight = "";
                     if(m_duel(gamemode, mutators))
                     {
-                        defformatstring(namea)("%s", colourname(playing[0]));
-                        defformatstring(nameb)("%s", colourname(playing[1]));
-                        formatstring(fight)("\fyduel between %s and %s, round \fs\fr#%d\fS", namea, nameb, duelround);
+                        string names = "";
+                        loopv(playing)
+                        {
+                            concatstring(names, colourname(playing[i]));
+                            if(i == wants-1) break;
+                            else if(i == wants-2) concatstring(names, " and ");
+                            else concatstring(names, ", ");
+                        }
+                        formatstring(fight)("\fyduel between %s, round \fs\fr#%d\fS", names, duelround);
                     }
-                    else if(m_survivor(gamemode, mutators))
-                        formatstring(fight)("\fysurvivor, round \fs\fr#%d\fS", duelround);
+                    else if(m_survivor(gamemode, mutators)) formatstring(fight)("\fysurvivor, round \fs\fr#%d\fS", duelround);
+                    loopv(playing)
+                    {
+                        if(playing[i]->state.state == CS_ALIVE)
+                        {
+                            playing[i]->state.lastregen = gamemillis;
+                            playing[i]->state.resetresidual();
+                            playing[i]->state.health = m_health(gamemode, mutators, playing[i]->state.model);
+                            sendf(-1, 1, "ri4", N_REGEN, playing[i]->clientnum, playing[i]->state.health, 0); // amt = 0 regens impulse
+                        }
+                        else if(allowed.find(playing[i]) < 0) allowed.add(playing[i]);
+                        duelqueue.removeobj(playing[i]);
+                        if(m_affinity(gamemode)) respawns.add(playing[i]);
+                    }
+                    loopv(duelqueue) position(duelqueue[i], i);
                     ancmsgft(-1, S_V_FIGHT, CON_INFO, fight);
                     dueltime = dueldeath = -1;
                     duelcheck = gamemillis+5000;
                 }
-                else loopv(clients) if(playing.find(clients[i]) < 0) queue(clients[i], false);
+                else shrink();
             }
         }
         else if(duelround > 0)
         {
             bool cleanup = false;
             vector<clientinfo *> alive;
-            loopv(clients) if(clients[i]->state.aitype < AI_START && clients[i]->state.state == CS_ALIVE)
+            loopv(clients) if(clients[i]->state.actortype < A_ENEMY && clients[i]->state.state == CS_ALIVE)
             {
                 if(playing.find(clients[i]) < 0) queue(clients[i]);
                 else alive.add(clients[i]);
@@ -274,7 +302,7 @@ struct duelservmode : servmode
                     loopv(alive) if(i && alive[i]->team != alive[i-1]->team) { found = true; break; }
                     if(!found)
                     {
-                        if(dueldeath < 0) dueldeath = gamemillis+DEATHMILLIS;
+                        if(dueldeath < 0) dueldeath = gamemillis+G(dueldelay);
                         else if(gamemillis >= dueldeath)
                         {
                             if(!cleanup)
@@ -294,7 +322,7 @@ struct duelservmode : servmode
                                     }
                                     else ancmsgft(clients[i]->clientnum, S_V_YOULOSE, CON_INFO, end);
                                 }
-                                else ancmsgft(clients[i]->clientnum, S_V_NOTIFY, CON_INFO, end);
+                                else ancmsgft(clients[i]->clientnum, S_V_BOMBSCORE, CON_INFO, end);
                             }
                             clear();
                         }
@@ -307,10 +335,7 @@ struct duelservmode : servmode
                         if(m_affinity(gamemode) && !playing.empty()) break; // this should not happen
                         if(!cleanup)
                         {
-                            defformatstring(end)("\fyeveryone died, \fzoyepic fail");
-                            loopv(clients) if(playing.find(clients[i]) >= 0)
-                                ancmsgft(clients[i]->clientnum, S_V_DRAW, CON_INFO, end);
-                            else ancmsgft(clients[i]->clientnum, S_V_NOTIFY, CON_INFO, end);
+                            ancmsgft(-1, S_V_DRAW, CON_INFO, "\fyeveryone died, \fzoyepic fail");
                             duelwinner = -1;
                             duelwins = 0;
                         }
@@ -323,7 +348,7 @@ struct duelservmode : servmode
                         {
                             if(dueldeath < 0)
                             {
-                                dueldeath = gamemillis+DEATHMILLIS;
+                                dueldeath = gamemillis+G(dueldelay);
                                 break;
                             }
                             else if(gamemillis < dueldeath) break;
@@ -331,9 +356,9 @@ struct duelservmode : servmode
                         if(!cleanup)
                         {
                             string end, hp; hp[0] = 0;
-                            if(!m_affinity(gamemode))
+                            if(!m_insta(gamemode, mutators) && !m_affinity(gamemode))
                             {
-                                if(!m_insta(gamemode, mutators) && !m_vampire(gamemode, mutators) && alive[0]->state.health == m_health(gamemode, mutators, alive[0]->state.model))
+                                if(alive[0]->state.health >= m_health(gamemode, mutators, alive[0]->state.model))
                                     formatstring(hp)(" with a \fs\fcflawless victory\fS");
                                 else formatstring(hp)(" with \fs\fc%d\fS health left", alive[0]->state.health);
                             }
@@ -348,16 +373,19 @@ struct duelservmode : servmode
                                 duelwins++;
                                 formatstring(end)("\fy%s was the winner%s (\fs\fc%d\fS in a row)", colourname(alive[0]), hp, duelwins);
                             }
-                            loopv(clients) if(playing.find(clients[i]) >= 0)
+                            loopv(clients)
                             {
-                                if(clients[i] == alive[0])
+                                if(playing.find(clients[i]) >= 0)
                                 {
-                                    ancmsgft(clients[i]->clientnum, S_V_YOUWIN, CON_INFO, end);
-                                    if(!m_affinity(gamemode)) givepoints(clients[i], 1);
+                                    if(clients[i] == alive[0])
+                                    {
+                                        ancmsgft(clients[i]->clientnum, S_V_YOUWIN, CON_INFO, end);
+                                        if(!m_affinity(gamemode)) givepoints(clients[i], 1);
+                                    }
+                                    else ancmsgft(clients[i]->clientnum, S_V_YOULOSE, CON_INFO, end);
                                 }
-                                else ancmsgft(clients[i]->clientnum, S_V_YOULOSE, CON_INFO, end);
+                                else ancmsgft(clients[i]->clientnum, S_V_BOMBSCORE, CON_INFO, end);
                             }
-                            else ancmsgft(clients[i]->clientnum, S_V_NOTIFY, CON_INFO, end);
                         }
                         clear();
                         break;
@@ -370,20 +398,19 @@ struct duelservmode : servmode
 
     bool wantsovertime()
     {
-        if((dueltime < 0 || dueltime > gamemillis) && duelround > 0) return true;
-        return 0;
+        if(dueltime < 0 && duelround > 0) return true;
+        return false;
     }
 
-    void reset(bool empty)
+    bool canbalance()
     {
-        duelround = duelwins = 0;
-        duelwinner = -1;
-        duelcheck = dueldeath = -1;
-        dueltime = gamemillis+G(duelwarmup);
-        allowed.shrink(0);
-        playing.shrink(0);
-        duelqueue.shrink(0);
-        respawns.shrink(0);
+        if(dueltime < 0 && duelround > 0) return false;
+        return true;
+    }
+
+    void balance(int oldbalance)
+    {
+        doreset();
     }
 } duelmutator;
 #endif
diff --git a/src/game/entities.cpp b/src/game/entities.cpp
index a17a720..9556e0a 100644
--- a/src/game/entities.cpp
+++ b/src/game/entities.cpp
@@ -2,28 +2,56 @@
 namespace entities
 {
     vector<extentity *> ents;
-    int lastenttype[MAXENTTYPES], lastusetype[EU_MAX], numactors = 0, numcheckpoints = 0;
+    int lastenttype[MAXENTTYPES], lastusetype[EU_MAX], numactors = 0, lastroutenode = -1, lastroutefloor = -1, lastroutetime = 0;
+    vector<int> airnodes;
 
     VAR(IDF_PERSIST, showlighting, 0, 0, 1);
     VAR(IDF_PERSIST, showentmodels, 0, 1, 2);
     VAR(IDF_PERSIST, showentdescs, 0, 2, 3);
     VAR(IDF_PERSIST, showentinfo, 0, 21, 127);
+    VAR(IDF_PERSIST, showentattrinfo, 0, 7, 7);
 
     VAR(IDF_PERSIST, showentdir, 0, 1, 3); // 0 = off, 1 = only selected, 2 = always when editing, 3 = always in editmode
     VAR(IDF_PERSIST, showentradius, 0, 1, 3);
     VAR(IDF_PERSIST, showentlinks, 0, 1, 3);
     VAR(IDF_PERSIST, showentinterval, 0, 32, VAR_MAX);
-    VAR(IDF_PERSIST, showentdist, 0, 256, VAR_MAX);
-    FVAR(IDF_PERSIST, showentsize, 0, 2, FVAR_MAX);
+    VAR(IDF_PERSIST, showentdist, 0, 512, VAR_MAX);
+    FVAR(IDF_PERSIST, showentsize, 0, 2, 10);
 
     VAR(IDF_PERSIST, simpleitems, 0, 0, 2); // 0 = items are models, 1 = items are icons, 2 = items are off and only halos appear
-    FVAR(IDF_PERSIST, simpleitemsize, 0, 5, 6);
+    FVAR(IDF_PERSIST, simpleitemsize, 0, 5, 8);
     FVAR(IDF_PERSIST, simpleitemblend, 0, 1, 1);
     FVAR(IDF_PERSIST, simpleitemhalo, 0, 0.5f, 1);
 
+    FVAR(IDF_PERSIST, haloitemsize, 0, 1, 8);
+    FVAR(IDF_PERSIST, haloitemblend, 0, 0.75f, 1);
+
+    VARF(0, routeid, -1, -1, VAR_MAX, lastroutenode = -1; lastroutetime = 0; airnodes.setsize(0)); // selected route in time race
+    VARF(0, droproute, 0, 0, 1, lastroutenode = -1; lastroutetime = 0; airnodes.setsize(0); if(routeid < 0) routeid = 0);
+    VAR(IDF_HEX, routecolour, 0, 0xFF22FF, 0xFFFFFF);
+    VAR(0, droproutedist, 1, 16, VAR_MAX);
+    VAR(0, routemaxdist, 0, 64, VAR_MAX);
+
     vector<extentity *> &getents() { return ents; }
-    int lastent(int type) { return type >= 0 && type < MAXENTTYPES ? lastenttype[type] : 0; }
+    int lastent(int type) { return type >= 0 && type < MAXENTTYPES ? clamp(lastenttype[type], 0, ents.length()) : 0; }
+    int lastuse(int type) { return type >= 0 && type < MAXENTTYPES ? clamp(lastusetype[type], 0, ents.length()) : 0; }
+
     int numattrs(int type) { return type >= 0 && type < MAXENTTYPES ? enttype[type].numattrs : 0; }
+    ICOMMAND(0, entityattrs, "b", (int *n), intret(numattrs(*n)));
+
+    ICOMMAND(0, getentinfo, "b", (int *n), {
+        if(*n < 0) intret(MAXENTTYPES);
+        else if(*n < MAXENTTYPES) result(enttype[*n].name);
+    });
+
+    ICOMMAND(0, getentattr, "bb", (int *n, int *p), {
+        if(*n < 0) intret(MAXENTTYPES);
+        else if(*n < MAXENTTYPES)
+        {
+            if(*p < 0) intret(enttype[*n].numattrs);
+            else if(*p < enttype[*n].numattrs) result(enttype[*n].attrs[*p]);
+        }
+    });
 
     int triggertime(extentity &e)
     {
@@ -35,6 +63,39 @@ namespace entities
         return 0;
     }
 
+    int triggertime(int n)
+    {
+        if(ents.inrange(n)) return triggertime(*ents[n]);
+        return 0;
+    }
+    ICOMMAND(0, entitytriggertime, "b", (int *n), intret(triggertime(*n)));
+
+    void getentity(int id, int val, int ex)
+    {
+        if(id < 0) intret(ents.length());
+        else if(ents.inrange(id))
+        {
+            if(val < 0) intret(3);
+            else switch(val)
+            {
+                case 0: intret(ents[id]->type); break; // type
+                case 1: // attrs
+                {
+                    if(ex < 0) intret(ents[id]->attrs.length());
+                    else if(ents[id]->attrs.inrange(ex)) intret(ents[id]->attrs[ex]);
+                    break;
+                }
+                case 2: // links
+                {
+                    if(ex < 0) intret(ents[id]->links.length());
+                    else if(ents[id]->links.inrange(ex)) intret(ents[id]->links[ex]);
+                    break;
+                }
+            }
+        }
+    }
+    ICOMMAND(0, getentity, "bbb", (int *id, int *val, int *ex), getentity(*id, *val, *ex));
+
     const char *entinfo(int type, attrvector &attr, bool full, bool icon)
     {
         static string entinfostr; entinfostr[0] = 0;
@@ -90,10 +151,10 @@ namespace entities
                     case 15: addentinfo("smoke plume"); break;
                     case 6: addentinfo("progress versus"); break;
                     case 5: addentinfo("progress"); break;
-                    case 32: addentinfo("lensflare (plain)"); break;
-                    case 33: addentinfo("lensflare (sparkle)"); break;
-                    case 34: addentinfo("lensflare (sun)"); break;
-                    case 35: addentinfo("lensflare (sparklesun)"); break;
+                    case 32: addentinfo("lensflare-plain"); break;
+                    case 33: addentinfo("lensflare-sparkle"); break;
+                    case 34: addentinfo("lensflare-sun"); break;
+                    case 35: addentinfo("lensflare-sparklesun"); break;
                     default: break;
                 }
                 switch(attr[0])
@@ -102,21 +163,25 @@ namespace entities
                     {
                         if(attr[1] >= 256)
                         {
+                            bool hasval = true;
                             int val = attr[1]-256;
                             switch(val%32)
                             {
                                 case 0: case 1: case 2: addentinfo("circle"); break;
-                                case 3: case 4: case 5: addentinfo("cylinder"); break;
-                                case 6: case 7: case 8: case 9: case 10: case 11: addentinfo("cone"); break;
-                                case 12: case 13: case 14: addentinfo("plane"); break;
-                                case 15: case 16: case 17: case 18: case 19: case 20: addentinfo("line"); break;
-                                case 21: default: addentinfo("sphere"); break;
+                                case 3: case 4: case 5: addentinfo("cylinder-shell"); break;
+                                case 6: case 7: case 8: case 9: case 10: case 11: addentinfo("cone-shell"); break;
+                                case 12: case 13: case 14: addentinfo("plane-volume"); break;
+                                case 15: case 16: case 17: case 18: case 19: case 20: addentinfo("line-volume"); break;
+                                case 21: case 22: case 23: hasval = false; addentinfo("sphere"); break;
+                                case 24: case 25: case 26: addentinfo("plane-flat"); break;
+                                default: hasval = false; addentinfo("default"); break;
                             }
-                            switch(val%3)
+                            if(hasval) switch(val%3)
                             {
                                 case 0: addentinfo("x-axis"); break;
                                 case 1: addentinfo("y-axis"); break;
                                 case 2: addentinfo("z-axis"); break;
+                                default: break;
                             }
                             if(val%64 >= 32) addentinfo("inverted");
                             break;
@@ -170,17 +235,17 @@ namespace entities
             }
             case ACTOR:
             {
-                if(full && attr[0] >= 0 && attr[0] < AI_TOTAL)
+                if(full && attr[0] >= 0 && attr[0] < A_TOTAL)
                 {
-                    addentinfo(aistyle[attr[0]+AI_START].name);
+                    addentinfo(actor[attr[0]+A_ENEMY].name);
                     addmodeinfo(attr[3], attr[4]);
-                    addentinfo(W(attr[6] > 0 && attr[6] <= W_MAX ? attr[6]-1 : aistyle[attr[0]+AI_START].weap, name));
+                    addentinfo(W(attr[6] > 0 && attr[6] <= W_MAX ? attr[6]-1 : actor[attr[0]+A_ENEMY].weap, name));
                 }
                 break;
             }
             case WEAPON:
             {
-                int sweap = m_weapon(game::gamemode, game::mutators), attr1 = w_attr(game::gamemode, game::mutators, attr[0], sweap);
+                int sweap = m_weapon(game::gamemode, game::mutators), attr1 = w_attr(game::gamemode, game::mutators, type, attr[0], sweap);
                 if(isweap(attr1))
                 {
                     defformatstring(str)("\fs\f[%d]%s%s%s%s\fS", W(attr1, colour), icon ? "\f(" : "", icon ? hud::itemtex(type, attr1) : W(attr1, name), icon ? ")" : "", icon ? W(attr1, name) : "");
@@ -193,28 +258,6 @@ namespace entities
                 }
                 break;
             }
-#ifdef MEK
-            case HEALTH:
-            {
-                if(attr[0] >= 0 && attr[0] < HEALTH_MAX)
-                {
-                    defformatstring(str)("+%d", healthamt[attr[0]]);
-                    addentinfo(str);
-                }
-                if(full) addmodeinfo(attr[1], attr[2]);
-                break;
-            }
-            case ARMOUR:
-            {
-                if(attr[0] >= 0 && attr[0] < ARMOUR_MAX)
-                {
-                    defformatstring(str)("+%d", armouramt[attr[0]]);
-                    addentinfo(str);
-                }
-                if(full) addmodeinfo(attr[1], attr[2]);
-                break;
-            }
-#endif
             case MAPMODEL:
             {
                 mapmodelinfo *mmi = getmminfo(attr[0]);
@@ -223,9 +266,9 @@ namespace entities
                 if(full)
                 {
                     if(attr[6]&MMT_HIDE) addentinfo("hide");
-                    if(attr[6]&MMT_NOCLIP) addentinfo("noclip");
-                    if(attr[6]&MMT_NOSHADOW) addentinfo("noshadow");
-                    if(attr[6]&MMT_NODYNSHADOW) addentinfo("nodynshadow");
+                    if(attr[6]&MMT_NOCLIP) addentinfo("no-clip");
+                    if(attr[6]&MMT_NOSHADOW) addentinfo("no-shadow");
+                    if(attr[6]&MMT_NODYNSHADOW) addentinfo("no-dynshadow");
                 }
                 break;
             }
@@ -239,12 +282,12 @@ namespace entities
                 }
                 if(full)
                 {
-                    if(attr[4]&SND_NOATTEN) addentinfo("noatten");
-                    if(attr[4]&SND_NODELAY) addentinfo("nodelay");
-                    if(attr[4]&SND_NOCULL) addentinfo("nocull");
-                    if(attr[4]&SND_NOPAN) addentinfo("nopan");
-                    if(attr[4]&SND_NODIST) addentinfo("nodist");
-                    if(attr[4]&SND_NOQUIET) addentinfo("noquiet");
+                    if(attr[4]&SND_NOATTEN) addentinfo("no-atten");
+                    if(attr[4]&SND_NODELAY) addentinfo("no-delay");
+                    if(attr[4]&SND_NOCULL) addentinfo("no-cull");
+                    if(attr[4]&SND_NOPAN) addentinfo("no-pan");
+                    if(attr[4]&SND_NODIST) addentinfo("no-dist");
+                    if(attr[4]&SND_NOQUIET) addentinfo("no-quiet");
                     if(attr[4]&SND_CLAMPED) addentinfo("clamped");
                 }
                 break;
@@ -266,10 +309,10 @@ namespace entities
             {
                 if(full) switch(attr[5])
                 {
-                    case 0: addentinfo("conditional"); break;
-                    case 1: addentinfo("additional"); break;
-                    case 2: addentinfo("redirectional"); break;
-                    case 3: addentinfo("absolute"); break;
+                    case 0: addentinfo("conditional-dir"); break;
+                    case 1: addentinfo("add-to-dir"); break;
+                    case 2: addentinfo("redirect-dir"); break;
+                    case 3: addentinfo("absolute-dir"); break;
                     default: break;
                 }
                 break;
@@ -278,15 +321,16 @@ namespace entities
             {
                 if(full)
                 {
-                    switch(attr[5])
+                    if(attr[5] >= 3) addentinfo("pos-offset");
+                    if(attr[5] >= 6) addentinfo("mod-velocity");
+                    switch(attr[5]%3)
                     {
-                        case 0: addentinfo("absolute"); break;
-                        case 1: addentinfo("relative"); break;
-                        case 2: addentinfo("keep"); break;
-                        case 3: addentinfo("positional"); break;
+                        case 0: addentinfo("absolute-dir"); break;
+                        case 1: addentinfo("relative-dir"); break;
+                        case 2: addentinfo("keep-dir"); break;
                         default: break;
                     }
-                    const char *telenames[TELE_MAX] = { "no affinity" };
+                    const char *telenames[TELE_MAX] = { "no-affinity" };
                     loopj(TELE_MAX) if(attr[8]&(1<<j)) { addentinfo(telenames[j]); }
                 }
                 break;
@@ -301,25 +345,18 @@ namespace entities
         switch(type)
         {
             case AFFINITY: return "props/flag";
-#ifdef MEK
             case PLAYERSTART: return playertypes[0][1];
-            case HEALTH: return "props/health";
-            case ARMOUR: return "props/armour";
-#else
-            case PLAYERSTART: return aistyle[AI_NONE].playermodel[1];
-#endif
             case WEAPON:
             {
-                int sweap = m_weapon(game::gamemode, game::mutators), attr1 = w_attr(game::gamemode, game::mutators, attr[0], sweap);
-                return weaptype[attr1].item;
+                int weap = w_attr(game::gamemode, game::mutators, type, attr[0], m_weapon(game::gamemode, game::mutators));
+                return isweap(weap) && *weaptype[weap].item ? weaptype[weap].item : "projectiles/cartridge";
             }
-            case ACTOR: return aistyle[clamp(attr[0]+AI_START, int(AI_START), int(AI_MAX-1))].playermodel[1];
+            case ACTOR: return actor[clamp(attr[0]+A_ENEMY, int(A_ENEMY), int(A_MAX-1))].playermodel[1];
             default: break;
         }
         return "";
     }
 
-
     void checkspawns(int n)
     {
         gameentity &e = *(gameentity *)ents[n];
@@ -332,39 +369,40 @@ namespace entities
         }
     }
 
-    void useeffects(gameent *d, int ent, int ammoamt, int reloadamt, bool spawn, int weap, int drop, int ammo, int reloads)
+    void useeffects(gameent *d, int ent, int ammoamt, bool spawn, int weap, int drop, int ammo)
     {
         gameentity &e = *(gameentity *)ents[ent];
-        int sweap = m_weapon(game::gamemode, game::mutators), attr = e.type == WEAPON ? w_attr(game::gamemode, game::mutators, e.attrs[0], sweap) : e.attrs[0],
-            colour = e.type == WEAPON ? W(attr, colour) : 0xFFFFFF;
+        int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap),
+            colour = e.type == WEAPON ? W(attr, colour) : 0x888888;
         if(e.type == WEAPON) d->addicon(eventicon::WEAPON, lastmillis, game::eventiconshort, attr);
         if(isweap(weap))
         {
             d->setweapstate(weap, W_S_SWITCH, weaponswitchdelay, lastmillis);
-            d->ammo[weap] = d->reloads[weap] = -1;
+            d->ammo[weap] = -1;
             if(d->weapselect != weap)
             {
-                d->lastweap = d->weapselect;
+                d->addlastweap(d->weapselect);
                 d->weapselect = weap;
             }
         }
-        d->useitem(ent, e.type, attr, ammoamt, reloadamt, sweap, lastmillis, weaponswitchdelay);
+        d->useitem(ent, e.type, attr, ammoamt, sweap, lastmillis, weaponswitchdelay);
         playsound(e.type == WEAPON && attr >= W_OFFSET ? WSND(attr, S_W_USE) : S_ITEMUSE, d->o, d, 0, -1, -1, -1, &d->wschan);
         if(game::dynlighteffects) adddynlight(d->center(), enttype[e.type].radius*2, vec::hexcolor(colour).mul(2.f), 250, 250);
         if(ents.inrange(drop) && ents[drop]->type == WEAPON)
         {
             gameentity &f = *(gameentity *)ents[drop];
-            attr = w_attr(game::gamemode, game::mutators, f.attrs[0], sweap);
-            if(isweap(attr)) projs::drop(d, attr, drop, ammo, reloads, d == game::player1 || d->ai, 0, weap);
+            attr = w_attr(game::gamemode, game::mutators, f.type, f.attrs[0], sweap);
+            if(isweap(attr)) projs::drop(d, attr, drop, ammo, d == game::player1 || d->ai, 0, weap);
         }
-        if(e.spawned != spawn)
+        if(e.spawned() != spawn)
         {
-            e.spawned = spawn;
+            e.setspawned(spawn);
             e.lastemit = lastmillis;
         }
         checkspawns(ent);
     }
 
+    /*
     static inline void collateents(octaentities &oe, const vec &pos, float xyrad, float zrad, bool alive, vector<actitem> &actitems)
     {
         vector<extentity *> &ents = entities::getents();
@@ -372,7 +410,7 @@ namespace entities
         {
             int n = oe.other[i];
             extentity &e = *ents[n];
-            if(enttype[e.type].usetype != EU_NONE && (enttype[e.type].usetype != EU_ITEM || (alive && e.spawned)))
+            if(enttype[e.type].usetype != EU_NONE && (enttype[e.type].usetype != EU_ITEM || (alive && e.spawned())))
             {
                 float radius = enttype[e.type].radius;
                 switch(e.type)
@@ -406,11 +444,11 @@ namespace entities
 
     void collateents(const vec &pos, float xyrad, float zrad, bool alive, vector<actitem> &actitems)
     {
-        ivec bo = vec(pos).sub(vec(xyrad, xyrad, zrad)).sub(1),
-             br = vec(xyrad, xyrad, zrad).add(1).mul(2);
-        int diff = (bo.x^(bo.x+br.x)) | (bo.y^(bo.y+br.y)) | (bo.z^(bo.z+br.z)) | octaentsize,
+        ivec bo = vec(pos).sub(vec(xyrad, xyrad, zrad)),
+             br = vec(pos).add(vec(xyrad, xyrad, zrad)).add(1);
+        int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z) | octaentsize,
             scale = worldscale-1;
-        if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|(bo.x+br.x)|(bo.y+br.y)|(bo.z+br.z)) >= uint(hdr.worldsize))
+        if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|br.x|br.y|br.z) >= uint(hdr.worldsize))
         {
             collateents(worldroot, ivec(0, 0, 0), 1<<scale, bo, br, pos, xyrad, zrad, alive, actitems);
             return;
@@ -426,29 +464,54 @@ namespace entities
         }
         if(c->children && 1<<scale >= octaentsize) collateents(c->children, ivec(bo).mask(~((2<<scale)-1)), 1<<scale, bo, br, pos, xyrad, zrad, alive, actitems);
     }
+    */
 
     static inline bool sortitems(const actitem &a, const actitem &b)
     {
         return a.score > b.score;
     }
 
-    bool collateitems(dynent *d, vector<actitem> &actitems)
+    bool collateitems(dynent *d, vec &pos, float radius, vector<actitem> &actitems)
     {
-        vec m = d->center();
-        float eye = gameent::is(d) ? d->height*0.5f : d->radius;
-        collateents(m, d->radius, eye, d->state == CS_ALIVE, actitems);
+        loopv(ents)
+        {
+            extentity &e = *ents[i];
+            if(enttype[e.type].usetype != EU_NONE && (enttype[e.type].usetype != EU_ITEM || (d->state == CS_ALIVE && e.spawned())))
+            {
+                float eradius = enttype[e.type].radius, edist = pos.dist(e.o);
+                switch(e.type)
+                {
+                    case TRIGGER: case TELEPORT: case PUSHER: if(e.attrs[3] > 0) eradius = e.attrs[3]; break;
+                    case CHECKPOINT: if(e.attrs[0] > 0) eradius = e.attrs[0]; break;
+                }
+                float diff = edist-radius;
+                if(diff > eradius) continue;
+                actitem &t = actitems.add();
+                t.type = actitem::ENT;
+                t.target = i;
+                t.score = diff;
+            }
+        }
         if(d->state == CS_ALIVE) loopv(projs::projs)
         {
             projent &proj = *projs::projs[i];
             if(proj.projtype != PRJ_ENT || !proj.ready()) continue;
             if(!ents.inrange(proj.id) || enttype[ents[proj.id]->type].usetype != EU_ITEM) continue;
             if(!(enttype[ents[proj.id]->type].canuse&(1<<d->type))) continue;
-            if(!overlapsbox(m, eye, d->radius, proj.o, enttype[ents[proj.id]->type].radius, enttype[ents[proj.id]->type].radius))
-                continue;
+            //if(!overlapsbox(m, eye, d->radius, proj.o, enttype[ents[proj.id]->type].radius, enttype[ents[proj.id]->type].radius))
+            //    continue;
+            float eradius = enttype[ents[proj.id]->type].radius, edist = pos.dist(proj.o);
+            switch(ents[proj.id]->type)
+            {
+                case TRIGGER: case TELEPORT: case PUSHER: if(ents[proj.id]->attrs[3] > 0) eradius = ents[proj.id]->attrs[3]; break;
+                case CHECKPOINT: if(ents[proj.id]->attrs[0] > 0) eradius = ents[proj.id]->attrs[0]; break;
+            }
+            float diff = edist-radius;
+            if(diff > eradius) continue;
             actitem &t = actitems.add();
             t.type = actitem::PROJ;
             t.target = i;
-            t.score = m.squaredist(proj.o);
+            t.score = diff;
         }
         if(!actitems.empty())
         {
@@ -491,11 +554,11 @@ namespace entities
             d->setused(n, lastmillis);
             switch(e.attrs[1])
             {
-                case TR_EXIT: if(d->aitype >= AI_BOT) break;
+                case TR_EXIT: if(d->actortype >= A_BOT) break;
                 case TR_TOGGLE: case TR_LINK: case TR_ONCE:
                 {
                     client::addmsg(N_TRIGGER, "ri2", d->clientnum, n);
-                    if(!e.spawned || e.attrs[1] == TR_TOGGLE) setspawn(n, e.spawned ? 0 : 1);
+                    if(!e.spawned() || e.attrs[1] == TR_TOGGLE) setspawn(n, e.spawned() ? 0 : 1);
                     break;
                 }
                 case TR_SCRIPT:
@@ -515,34 +578,50 @@ namespace entities
 
     void runtriggers(int n, gameent *d)
     {
-        loopi(lastenttype[TRIGGER]) if(ents[i]->type == TRIGGER && ents[i]->attrs[0] == n && ents[i]->attrs[2] == TA_MANUAL) runtrigger(i, d, false);
+        loopi(lastent(TRIGGER)) if(ents[i]->type == TRIGGER && ents[i]->attrs[0] == n && ents[i]->attrs[2] == TA_MANUAL) runtrigger(i, d, false);
     }
     ICOMMAND(0, exectrigger, "i", (int *n), if(identflags&IDF_WORLD) runtriggers(*n, trigger ? trigger : game::player1));
 
-    bool execitem(int n, dynent *d)
+    bool execitem(int n, dynent *d, vec &pos, float dist)
     {
         gameentity &e = *(gameentity *)ents[n];
         switch(enttype[e.type].usetype)
         {
-            case EU_ITEM: if(gameent::is(d) && (e.type != WEAPON || ((gameent *)d)->action[AC_USE]))
+            case EU_ITEM:
             {
-                gameent *f = (gameent *)d;
-                if(game::allowmove(f))
+                if(gameent::is(d) && (e.type != WEAPON || ((gameent *)d)->action[AC_USE]))
                 {
-                    int sweap = m_weapon(game::gamemode, game::mutators), attr = e.type == WEAPON ? w_attr(game::gamemode, game::mutators, e.attrs[0], sweap) : e.attrs[0];
-                    if(f->canuse(e.type, attr, e.attrs, sweap, lastmillis, G(weaponinterrupts)))
+                    gameent *f = (gameent *)d;
+                    if(game::allowmove(f))
                     {
+                        int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
+                        if(!f->canuse(e.type, attr, e.attrs, sweap, lastmillis, (1<<W_S_SWITCH)))
+                        {
+                            if(e.type != WEAPON) return false;
+                            else if(!f->canuse(e.type, attr, e.attrs, sweap, lastmillis, (1<<W_S_SWITCH)|(1<<W_S_RELOAD))) return true;
+                            else if(!isweap(f->weapselect) || f->weapload[f->weapselect] <= 0) return true;
+                            else
+                            {
+                                int offset = f->weapload[f->weapselect];
+                                f->ammo[f->weapselect] = max(f->ammo[f->weapselect]-offset, 0);
+                                f->weapload[f->weapselect] = -f->weapload[f->weapselect];
+                            }
+                        }
                         client::addmsg(N_ITEMUSE, "ri3", f->clientnum, lastmillis-game::maptime, n);
-                        f->setweapstate(f->weapselect, W_S_WAIT, weaponswitchdelay, lastmillis);
-                        f->action[AC_USE] = false;
+                        if(e.type == WEAPON)
+                        {
+                            f->setweapstate(f->weapselect, W_S_WAIT, weaponswitchdelay, lastmillis);
+                            f->action[AC_USE] = false;
+                        }
                         return false;
                     }
+                    return true;
                 }
-                return true;
-            } break;
-            case EU_AUTO: switch(e.type)
+                break;
+            }
+            case EU_AUTO:
             {
-                case TELEPORT:
+                if(e.type == TELEPORT)
                 {
                     if(e.attrs[8]&(1<<TELE_NOAFFIN))
                     {
@@ -555,96 +634,107 @@ namespace entities
                     static vector<int> teleports;
                     teleports.shrink(0);
                     loopv(e.links)
-                        if(ents.inrange(e.links[i]) && ents[e.links[i]]->type == e.type)
+                        if(e.links[i] != n && ents.inrange(e.links[i]) && ents[e.links[i]]->type == e.type)
                             teleports.add(e.links[i]);
-                    if(!teleports.empty())
+                    if(teleports.empty()) break;
+                    vec orig = d->o, ovel = d->vel;
+                    float oyaw = d->yaw, opitch = d->pitch;
+                    while(!teleports.empty())
                     {
-                        bool teleported = false;
-                        while(!teleports.empty())
+                        int r = rnd(teleports.length()), q = teleports[r];
+                        gameentity &f = *(gameentity *)ents[q];
+                        d->o = vec(f.o).add(f.attrs[5] >= 3 ? vec(orig).sub(e.o) : vec(0, 0, d->height*0.5f));
+                        float mag = max(vec(d->vel).add(d->falling).magnitude(), f.attrs[2] ? float(f.attrs[2]) : 50.f),
+                              yaw = f.attrs[0] < 0 ? (lastmillis/5)%360 : f.attrs[0], pitch = f.attrs[1];
+                        game::fixrange(yaw, pitch);
+                        if(f.attrs[5] < 6) d->vel = vec(yaw*RAD, pitch*RAD).mul(mag);
+                        switch(f.attrs[5]%3)
                         {
-                            int r = e.type == TELEPORT ? rnd(teleports.length()) : 0, q = teleports[r];
-                            gameentity &f = *(gameentity *)ents[q];
-                            d->o = vec(f.o).add(f.attrs[5] != 3 ? vec(0, 0, d->height*0.5f) : vec(e.o).sub(d->o));
-                            if(d->state != CS_ALIVE || physics::entinmap(d, true))
+                            case 2: break; // keep
+                            case 1: // relative
                             {
-                                d->resetinterp();
-                                if(f.attrs[5] != 3)
+                                float relyaw = (e.attrs[0] < 0 ? (lastmillis/5)%360 : e.attrs[0])-180, relpitch = e.attrs[1];
+                                game::fixrange(relyaw, relpitch);
+                                d->yaw = yaw+(d->yaw-relyaw);
+                                d->pitch = pitch+(d->pitch-relpitch);
+                                break;
+                            }
+                            case 0: default: // absolute
+                            {
+                                d->yaw = yaw;
+                                d->pitch = pitch;
+                                break;
+                            }
+                        }
+                        game::fixrange(d->yaw, d->pitch);
+                        if(f.attrs[5] >= 6) d->vel = vec(d->yaw*RAD, d->pitch*RAD).mul(mag);
+                        if(physics::entinmap(d, gameent::is(d))) // entinmap first for getting position
+                        {
+                            f.lastemit = lastmillis;
+                            d->setused(n, lastmillis);
+                            d->setused(q, lastmillis);
+                            d->resetinterp();
+                            if(d->state == CS_ALIVE)
+                            {
+                                if(gameent::is(d))
                                 {
-                                    float mag = max(vec(d->vel).add(d->falling).magnitude(), f.attrs[2] ? float(f.attrs[2]) : 50.f),
-                                          yaw = f.attrs[0] < 0 ? (lastmillis/5)%360 : f.attrs[0], pitch = f.attrs[1];
-                                    game::fixrange(yaw, pitch);
-                                    vecfromyawpitch(yaw, pitch, 1, 0, d->vel);
-                                    d->vel.normalize().mul(mag);
-                                    if(d->state == CS_ALIVE) switch(f.attrs[5])
-                                    {
-                                        case 2: break; // keep
-                                        case 1:
-                                        {
-                                            float offyaw = d->yaw-(e.attrs[0] < 0 ? (lastmillis/5)%360 : e.attrs[0]), offpitch = d->pitch-e.attrs[1];
-                                            d->yaw = yaw+offyaw;
-                                            d->pitch = pitch+offpitch;
-                                            break;
-                                        }
-                                        case 0: default: // absolute
-                                        {
-                                            d->yaw = yaw;
-                                            d->pitch = pitch;
-                                            break;
-                                        }
-                                    }
+                                    gameent *g = (gameent *)d;
+                                    if(g == game::focus) game::resetcamera();
+                                    execlink(g, n, true);
+                                    execlink(g, q, true);
+                                    g->resetair();
+                                    ai::inferwaypoints(g, e.o, f.o, float(e.attrs[3] ? e.attrs[3] : enttype[e.type].radius)+ai::CLOSEDIST);
                                 }
-                                game::fixfullrange(d->yaw, d->pitch, d->roll, true);
-                                f.lastemit = lastmillis;
-                                d->setused(n, lastmillis);
-                                d->setused(q, lastmillis);
-                                if(d->state == CS_ALIVE)
+                                else if(projent::is(d))
                                 {
-                                    if(gameent::is(d))
-                                    {
-                                        gameent *g = (gameent *)d;
-                                        if(g == game::focus) game::resetcamera();
-                                        execlink(g, n, true);
-                                        execlink(g, q, true);
-                                        g->resetair();
-                                        ai::inferwaypoints(g, e.o, f.o, float(e.attrs[3] ? e.attrs[3] : enttype[e.type].radius)+ai::CLOSEDIST);
-                                    }
-                                    else if(projent::is(d))
-                                    {
-                                        projent *g = (projent *)d;
-                                        g->lastbounce = lastmillis;
-                                        g->movement = 0;
-                                        g->from = g->o;
-                                        g->to = g->dest = vec(g->vel).normalize().mul(getworldsize()).add(g->o);
-                                    }
+                                    projent *g = (projent *)d;
+                                    g->lastbounce = lastmillis;
+                                    g->movement = 0;
+                                    g->from = g->deltapos = g->o;
+                                    g->to = g->dest = vec(g->o).add(g->vel);
                                 }
-                                else if(gameent::is(d)) warpragdoll(d, d->vel, vec(f.o).sub(e.o));
-                                teleported = true;
-                                break;
                             }
-                            teleports.remove(r); // must've really sucked, try another one
+                            else if(gameent::is(d)) warpragdoll(d, d->vel, vec(f.o).sub(e.o));
+                            return false;
                         }
-                        if(!teleported)
+                        d->o = orig;
+                        d->vel = ovel;
+                        d->yaw = oyaw;
+                        d->pitch = opitch;
+                        teleports.remove(r); // must've really sucked, try another one
+                    }
+                    if(d->state == CS_ALIVE)
+                    {
+                        if(gameent::is(d)) game::suicide((gameent *)d, HIT_SPAWN);
+                        else if(projent::is(d))
                         {
-                            if(gameent::is(d)) game::suicide((gameent *)d, HIT_SPAWN);
-                            else d->state = CS_DEAD;
+                            projent *g = (projent *)d;
+                            switch(g->projtype)
+                            {
+                                case PRJ_ENT: case PRJ_AFFINITY:
+                                {
+                                    if(!g->beenused)
+                                    {
+                                        g->beenused = 1;
+                                        g->lifetime = min(g->lifetime, g->fadetime);
+                                    }
+                                    if(g->lifetime > 0) break;
+                                }
+                                default: g->state = CS_DEAD; g->escaped = true; break;
+                            }
                         }
+                        else d->state = CS_DEAD;
                     }
-                    break;
                 }
-                case PUSHER:
+                else if(e.type == PUSHER)
                 {
+                    int millis = d->lastused(n, true);
                     e.lastemit = lastmillis;
                     d->setused(n, lastmillis);
-                    float mag = max(e.attrs[2], 1);
-                    if(e.attrs[4] && e.attrs[4] < e.attrs[3])
-                    {
-                        float dist = e.o.dist(d->center());
-                        if(dist > e.attrs[4] && dist < e.attrs[3])
-                            mag *= 1.f-clamp((dist-e.attrs[4])/float(e.attrs[3]-e.attrs[4]), 0.f, 0.99f);
-                    }
-                    vec dir, rel;
-                    vecfromyawpitch(e.attrs[0], e.attrs[1], 1, 0, dir);
-                    (rel = dir.normalize()).mul(mag);
+                    float mag = max(e.attrs[2], 1), maxrad = e.attrs[3] ? e.attrs[3] : enttype[PUSHER].radius, minrad = e.attrs[4];
+                    if(dist > 0 && minrad > 0 && maxrad > minrad && dist > minrad && maxrad >= dist)
+                        mag *= 1.f-clamp((dist-minrad)/float(maxrad-minrad), 0.f, 1.f);
+                    vec dir(e.attrs[0]*RAD, e.attrs[1]*RAD), rel = vec(dir).mul(mag);
                     switch(e.attrs[5])
                     {
                         case 0:
@@ -666,7 +756,7 @@ namespace entities
                         if(gameent::is(d))
                         {
                             gameent *g = (gameent *)d;
-                            execlink(g, n, true);
+                            if(!millis || lastmillis-millis >= triggertime(e)) execlink(g, n, true);
                             g->resetair();
                         }
                         else if(projent::is(d))
@@ -674,34 +764,35 @@ namespace entities
                             projent *g = (projent *)d;
                             g->lastbounce = lastmillis;
                             g->movement = 0;
-                            g->from = g->o;
-                            g->to = g->dest = vec(g->vel).normalize().mul(getworldsize()).add(g->o);
+                            g->from = g->deltapos = g->o;
+                            g->to = g->dest = vec(g->o).add(g->vel);
                         }
                     }
                     else if(gameent::is(d)) warpragdoll(d, d->vel);
-                    break;
                 }
-                case TRIGGER:
+                else if(e.type == TRIGGER)
                 {
                     if(d->state != CS_ALIVE || !gameent::is(d)) break;
                     gameent *g = (gameent *)d;
                     if((e.attrs[2] == TA_ACTION && g->action[AC_USE] && g == game::player1) || e.attrs[2] == TA_AUTO) runtrigger(n, g);
-                    break;
                 }
-                case CHECKPOINT:
+                else if(e.type == CHECKPOINT)
                 {
-                    if(d->state != CS_ALIVE || !gameent::is(d)) break;
+                    if(d->state != CS_ALIVE || !gameent::is(d) || !m_race(game::gamemode)) break;
+                    if(!m_check(e.attrs[3], e.attrs[4], game::gamemode, game::mutators)) break;
                     gameent *g = (gameent *)d;
-                    if(!m_check(e.attrs[3], e.attrs[4], game::gamemode, game::mutators) || !m_checkpoint(game::gamemode)) break;
-                    if(m_gauntlet(game::gamemode) && g->team != T_ALPHA) break;
-                    if(g->checkpoint != n)
+                    if(g->checkpoint == n || (m_race(game::gamemode) && m_gsp3(game::gamemode, game::mutators) && g->team != T_ALPHA)) break;
+                    if(e.attrs[6] == CP_START)
                     {
-                        client::addmsg(N_TRIGGER, "ri2", g->clientnum, n);
-                        g->checkpoint = n;
-                        if(!g->cpmillis || e.attrs[6] == CP_START) g->cpmillis = lastmillis;
+                        if(g->cpmillis || (d->vel.iszero() && !d->move && !d->strafe)) break;
+                        g->cpmillis = lastmillis;
                     }
+                    else if(!g->cpmillis) break;
+                    client::addmsg(N_TRIGGER, "ri2", g->clientnum, n);
+                    g->checkpoint = n;
                 }
-            } break;
+                break;
+            }
         }
         return false;
     }
@@ -710,19 +801,24 @@ namespace entities
     {
         static vector<actitem> actitems;
         actitems.setsize(0);
-        if(collateitems(d, actitems))
+        vec pos = d->center();
+        float radius = max(d->xradius, d->yradius);
+        if(gameent::is(d)) radius = max(d->height*0.5f, radius);
+        if(collateitems(d, pos, radius, actitems))
         {
             bool tried = false;
             while(!actitems.empty())
             {
                 actitem &t = actitems.last();
                 int ent = -1;
+                float dist = 0;
                 switch(t.type)
                 {
                     case actitem::ENT:
                     {
                         if(!ents.inrange(t.target)) break;
                         ent = t.target;
+                        dist = t.score;
                         break;
                     }
                     case actitem::PROJ:
@@ -730,11 +826,12 @@ namespace entities
                         if(!projs::projs.inrange(t.target)) break;
                         projent &proj = *projs::projs[t.target];
                         ent = proj.id;
+                        dist = t.score;
                         break;
                     }
                     default: break;
                 }
-                if(ents.inrange(ent) && execitem(ent, d)) tried = true;
+                if(ents.inrange(ent) && execitem(ent, d, pos, dist)) tried = true;
                 actitems.pop();
             }
             if(tried && gameent::is(d))
@@ -747,24 +844,34 @@ namespace entities
                 }
             }
         }
-        if(m_capture(game::gamemode)) capture::checkaffinity(d);
-        else if(m_bomber(game::gamemode)) bomber::checkaffinity(d);
     }
 
     void putitems(packetbuf &p)
     {
-        loopv(ents) if(enttype[ents[i]->type].syncs)
+        loopv(ents)
         {
-            gameentity &e = *(gameentity *)ents[i];
-            putint(p, i);
-            putint(p, int(e.type));
-            putint(p, e.attrs.length());
-            loopvj(e.attrs) putint(p, e.attrs[j]);
-            if(enttype[e.type].syncpos) loopj(3) putint(p, int(e.o[j]*DMF));
-            if(enttype[e.type].synckin)
+            if(i >= MAXENTS) break;
+            if(enttype[ents[i]->type].syncs)
             {
-                putint(p, e.kin.length());
-                loopvj(e.kin) putint(p, e.kin[j]);
+                gameentity &e = *(gameentity *)ents[i];
+                putint(p, i);
+                putint(p, int(e.type));
+                putint(p, min(e.attrs.length(), MAXENTATTRS));
+                loopvj(e.attrs)
+                {
+                    if(j >= MAXENTATTRS) break;
+                    putint(p, e.attrs[j]);
+                }
+                if(enttype[e.type].syncpos) loopj(3) putint(p, int(e.o[j]*DMF));
+                if(enttype[e.type].synckin)
+                {
+                    putint(p, min(e.kin.length(), MAXENTKIN));
+                    loopvj(e.kin)
+                    {
+                        if(j >= MAXENTKIN) break;
+                        putint(p, e.kin[j]);
+                    }
+                }
             }
         }
     }
@@ -774,11 +881,12 @@ namespace entities
         if(ents.inrange(n))
         {
             gameentity &e = *(gameentity *)ents[n];
-            bool on = m%2, spawned = e.spawned;
-            if((e.spawned = on) == true) e.lastspawn = lastmillis;
+            bool on = m%2, spawned = e.spawned();
+            e.setspawned(on);
+            if(on) e.lastspawn = lastmillis;
             if(e.type == TRIGGER && cantrigger(n) && (e.attrs[1] == TR_TOGGLE || e.attrs[1] == TR_LINK || e.attrs[1] == TR_ONCE))
             {
-                if(m >= 2 || e.lastemit <= 0 || e.spawned != spawned)
+                if(m >= 2 || e.lastemit <= 0 || e.spawned() != spawned)
                 {
                     if(m >= 2) e.lastemit = -1;
                     else if(e.lastemit > 0)
@@ -793,7 +901,7 @@ namespace entities
                     {
                         gameentity &f = *(gameentity *)ents[e.kin[i]];
                         if(!cantrigger(e.kin[i])) continue;
-                        f.spawned = e.spawned;
+                        f.setspawned(e.spawned());
                         f.lastemit = e.lastemit;
                         execlink(NULL, e.kin[i], false, n);
                     }
@@ -865,12 +973,13 @@ namespace entities
             case MAPSOUND:
             case LIGHTFX:
             {
+                e.nextemit = 0;
                 loopv(e.links) if(ents.inrange(e.links[i]))
                 {
                     gameentity &f = *(gameentity *)ents[e.links[i]];
                     if(f.type != TRIGGER || !cantrigger(e.links[i])) continue;
                     e.lastemit = f.lastemit;
-                    e.spawned = TRIGSTATE(f.spawned, f.attrs[4]);
+                    e.setspawned(TRIGSTATE(f.spawned(), f.attrs[4]));
                     break;
                 }
                 break;
@@ -878,9 +987,9 @@ namespace entities
             case LIGHT:
             {
                 if(e.attrs[0] < 0) e.attrs[0] = 0;
-                if(e.attrs[1] < 0) e.attrs[1] = 0;
-                if(e.attrs[2] < 0) e.attrs[2] = 0;
-                if(e.attrs[3] < 0) e.attrs[3] = 0;
+                //if(e.attrs[1] < 0) e.attrs[1] = 0;
+                //if(e.attrs[2] < 0) e.attrs[2] = 0;
+                //if(e.attrs[3] < 0) e.attrs[3] = 0;
                 if(e.attrs[4] < 0) e.attrs[4] = 3;
                 if(e.attrs[4] > 3) e.attrs[4] = 0;
                 break;
@@ -917,15 +1026,10 @@ namespace entities
                 while(e.attrs[2] >= TA_MAX) e.attrs[2] -= TA_MAX;
                 while(e.attrs[4] < 0) e.attrs[4] += 4;
                 while(e.attrs[4] >= 4) e.attrs[4] -= 4;
-                if(e.attrs[4] >= 2)
-                {
-                    while(e.attrs[0] < 0) e.attrs[0] += TRIGGERIDS+1;
-                    while(e.attrs[0] > TRIGGERIDS) e.attrs[0] -= TRIGGERIDS+1;
-                }
                 if(cantrigger(n)) loopv(e.links) if(ents.inrange(e.links[i]) && (ents[e.links[i]]->type == MAPMODEL || ents[e.links[i]]->type == PARTICLES || ents[e.links[i]]->type == MAPSOUND || ents[e.links[i]]->type == LIGHTFX))
                 {
                     ents[e.links[i]]->lastemit = e.lastemit;
-                    ents[e.links[i]]->spawned = TRIGSTATE(e.spawned, e.attrs[4]);
+                    ents[e.links[i]]->setspawned(TRIGSTATE(e.spawned(), e.attrs[4]));
                 }
                 break;
             }
@@ -934,16 +1038,6 @@ namespace entities
                 while(e.attrs[0] < W_OFFSET) e.attrs[0] += W_MAX-W_OFFSET; // don't allow superimposed weaps
                 while(e.attrs[0] >= W_MAX) e.attrs[0] -= W_MAX-W_OFFSET;
                 break;
-#ifdef MEK
-            case HEALTH:
-                while(e.attrs[0] < 0) e.attrs[0] += HEALTH_MAX;
-                while(e.attrs[0] >= HEALTH_MAX) e.attrs[0] -= HEALTH_MAX;
-                break;
-            case ARMOUR:
-                while(e.attrs[0] < 0) e.attrs[0] += ARMOUR_MAX;
-                while(e.attrs[0] >= ARMOUR_MAX) e.attrs[0] -= ARMOUR_MAX;
-                break;
-#endif
             case PLAYERSTART:
                 while(e.attrs[0] < 0) e.attrs[0] += T_ALL;
                 while(e.attrs[0] >= T_ALL) e.attrs[0] -= T_ALL;
@@ -957,17 +1051,14 @@ namespace entities
                     while(e.attrs[6] < 0) e.attrs[6] += CP_MAX;
                     while(e.attrs[6] >= CP_MAX) e.attrs[6] -= CP_MAX;
                 }
-                if(create && (e.attrs[6] == CP_LAST || e.attrs[6] == CP_FINISH)) numcheckpoints++;
                 break;
             case ACTOR:
-                while(e.attrs[0] < 0) e.attrs[0] += AI_TOTAL;
-                while(e.attrs[0] >= AI_TOTAL) e.attrs[0] -= AI_TOTAL;
+                while(e.attrs[0] < 0) e.attrs[0] += A_TOTAL;
+                while(e.attrs[0] >= A_TOTAL) e.attrs[0] -= A_TOTAL;
                 while(e.attrs[1] < 0) e.attrs[1] += 360;
                 while(e.attrs[1] >= 360) e.attrs[1] -= 360;
                 while(e.attrs[2] < -90) e.attrs[2] += 180;
                 while(e.attrs[2] > 90) e.attrs[2] -= 180;
-                while(e.attrs[5] < 0) e.attrs[5] += TRIGGERIDS;
-                while(e.attrs[5] >= TRIGGERIDS) e.attrs[5] -= TRIGGERIDS;
                 while(e.attrs[6] < 0) e.attrs[6] += W_MAX+1; // allow any weapon
                 while(e.attrs[6] > W_MAX) e.attrs[6] -= W_MAX+1;
                 if(e.attrs[7] < 0) e.attrs[7] = 0;
@@ -988,10 +1079,10 @@ namespace entities
                 while(e.attrs[0] >= 360) e.attrs[0] -= 360;
                 while(e.attrs[1] < -90) e.attrs[1] += 180;
                 while(e.attrs[1] > 90) e.attrs[1] -= 180;
-                while(e.attrs[5] < 0) e.attrs[5] += 3;
-                while(e.attrs[5] > 2) e.attrs[5] -= 3;
-                while(e.attrs[6] < 0) e.attrs[6]++;
-                while(e.attrs[7] < 0) e.attrs[7]++;
+                while(e.attrs[5] < 0) e.attrs[5] += 6;
+                while(e.attrs[5] >= 6) e.attrs[5] -= 6;
+                if(e.attrs[6] < 0) e.attrs[6] = 0;
+                if(e.attrs[7] < 0) e.attrs[7] = 0;
                 break;
             default: break;
         }
@@ -1017,7 +1108,7 @@ namespace entities
             gameentity &e = *(gameentity *)ents[index];
             if(e.type == TRIGGER && !cantrigger(index)) return;
             bool commit = false;
-            int numents = max(lastenttype[MAPMODEL], max(lastenttype[LIGHTFX], max(lastenttype[PARTICLES], lastenttype[MAPSOUND])));
+            int numents = max(lastent(MAPMODEL), max(lastent(LIGHTFX), max(lastent(PARTICLES), lastent(MAPSOUND))));
             loopi(numents) if(ents[i]->links.find(index) >= 0)
             {
                 gameentity &f = *(gameentity *)ents[i];
@@ -1028,27 +1119,27 @@ namespace entities
                     case MAPMODEL:
                     {
                         f.lastemit = e.lastemit;
-                        if(e.type == TRIGGER) f.spawned = TRIGSTATE(e.spawned, e.attrs[4]);
+                        if(e.type == TRIGGER) f.setspawned(TRIGSTATE(e.spawned(), e.attrs[4]));
                         break;
                     }
                     case LIGHTFX:
                     case PARTICLES:
                     {
                         f.lastemit = e.lastemit;
-                        if(e.type == TRIGGER) f.spawned = TRIGSTATE(e.spawned, e.attrs[4]);
+                        if(e.type == TRIGGER) f.setspawned(TRIGSTATE(e.spawned(), e.attrs[4]));
                         else if(local) commit = true;
                         break;
                     }
                     case MAPSOUND:
                     {
                         f.lastemit = e.lastemit;
-                        if(e.type == TRIGGER) f.spawned = TRIGSTATE(e.spawned, e.attrs[4]);
+                        if(e.type == TRIGGER) f.setspawned(TRIGSTATE(e.spawned(), e.attrs[4]));
                         else if(local) commit = true;
-                        if(mapsounds.inrange(f.attrs[0]) && !issound(((gameentity *)ents[i])->schan))
+                        if(mapsounds.inrange(f.attrs[0]) && !issound(f.schan))
                         {
                             int flags = SND_MAP;
-                            loopk(SND_LAST)  if(f.attrs[4]&(1<<k)) flags |= 1<<k;
-                            playsound(f.attrs[0], both ? f.o : e.o, NULL, flags, f.attrs[3], f.attrs[1], f.attrs[2], &((gameentity *)ents[i])->schan);
+                            loopk(SND_LAST) if(f.attrs[4]&(1<<k)) flags |= 1<<k;
+                            playsound(f.attrs[0], both ? f.o : e.o, NULL, flags, f.attrs[3] ? f.attrs[3] : -1, f.attrs[1] || f.attrs[2] ? f.attrs[1] : -1, f.attrs[2] ? f.attrs[2] : -1, &f.schan);
                         }
                         break;
                     }
@@ -1062,7 +1153,7 @@ namespace entities
     bool tryspawn(dynent *d, const vec &o, float yaw, float pitch)
     {
         game::fixfullrange(d->yaw = yaw, d->pitch = pitch, d->roll = 0);
-        d->o = vec(o).add(vec(0, 0, d->zradius+d->aboveeye));
+        (d->o = o).z += d->height+d->aboveeye;
         return physics::entinmap(d, true);
     }
 
@@ -1070,14 +1161,31 @@ namespace entities
     {
         if(ent >= 0 && ents.inrange(ent))
         {
+            vec pos = ents[ent]->o;
             switch(ents[ent]->type)
             {
-                case ACTOR: if(d->type == ENT_PLAYER) break;
-                case PLAYERSTART: case CHECKPOINT: if(tryspawn(d, ents[ent]->o, ents[ent]->attrs[1], ents[ent]->attrs[2])) return;
-                default: if(tryspawn(d, ents[ent]->o, rnd(360), 0)) return;
+                case PLAYERSTART: case ACTOR:
+                    if(tryspawn(d, pos, ents[ent]->attrs[1], ents[ent]->attrs[2])) return;
+                    break;
+                case CHECKPOINT:
+                {
+                    float yaw = ents[ent]->attrs[1], pitch = ents[ent]->attrs[2];
+                    if(m_race(game::gamemode) && m_gsp3(game::gamemode, game::mutators) && d->team != T_ALPHA)
+                    {
+                        yaw -= 180;
+                        pitch = -pitch;
+                    }
+                    physics::droptofloor(pos, ENT_DUMMY);
+                    if(tryspawn(d, pos, yaw, pitch)) return;
+                    break;
+                }
+                default:
+                    physics::droptofloor(pos, ENT_DUMMY);
+                    if(tryspawn(d, pos, rnd(360), 0)) return;
+                    break;
             }
         }
-        if(gameent::is(d))
+        else
         {
             vector<int> spawns;
             loopk(4)
@@ -1085,12 +1193,17 @@ namespace entities
                 spawns.shrink(0);
                 switch(k)
                 {
-                    case 0: if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
-                                loopi(lastenttype[PLAYERSTART]) if(ents[i]->type == PLAYERSTART && ents[i]->attrs[0] == d->team && m_check(ents[i]->attrs[3], ents[i]->attrs[4], game::gamemode, game::mutators))
-                                    spawns.add(i);
-                            break;
-                    case 1: case 2: loopi(lastenttype[PLAYERSTART]) if(ents[i]->type == PLAYERSTART && (k == 2 || m_check(ents[i]->attrs[3], ents[i]->attrs[4], game::gamemode, game::mutators))) spawns.add(i); break;
-                    case 3: loopi(lastenttype[WEAPON]) if(ents[i]->type == WEAPON && m_check(ents[i]->attrs[2], ents[i]->attrs[3], game::gamemode, game::mutators)) spawns.add(i); break;
+                    case 0:
+                        if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
+                            loopi(lastent(PLAYERSTART)) if(ents[i]->type == PLAYERSTART && ents[i]->attrs[0] == d->team && m_check(ents[i]->attrs[3], ents[i]->attrs[4], game::gamemode, game::mutators))
+                                spawns.add(i);
+                        break;
+                    case 1: case 2:
+                        loopi(lastent(PLAYERSTART)) if(ents[i]->type == PLAYERSTART && (k == 2 || m_check(ents[i]->attrs[3], ents[i]->attrs[4], game::gamemode, game::mutators))) spawns.add(i);
+                        break;
+                    case 3:
+                        loopi(lastent(WEAPON)) if(ents[i]->type == WEAPON && m_check(ents[i]->attrs[2], ents[i]->attrs[3], game::gamemode, game::mutators)) spawns.add(i);
+                        break;
                     default: break;
                 }
                 while(!spawns.empty())
@@ -1131,7 +1244,7 @@ namespace entities
 
     bool maylink(int type, int ver)
     {
-        if(enttype[type].links && enttype[type].links <= (ver ? ver : GAMEVERSION))
+        if(enttype[type].links && enttype[type].links <= (ver ? ver : VERSION_GAME))
                 return true;
         return false;
     }
@@ -1458,7 +1571,7 @@ namespace entities
             {
                 case WEAPON:
                 {
-                    float mindist = float(enttype[WEAPON].radius*enttype[WEAPON].radius*6);
+                    float mindist = float((enttype[WEAPON].radius*4)*(enttype[WEAPON].radius*4));
                     int weaps[W_MAX];
                     loopj(W_MAX) weaps[j] = j != e.attrs[0] ? 0 : 1;
                     loopvj(ents) if(j != i)
@@ -1631,11 +1744,7 @@ namespace entities
                         {
                             int material = lookupmaterial(e.o), clipmat = material&MATF_CLIP;
                             if(clipmat == MAT_CLIP || (material&MAT_DEATH) || (material&MATF_VOLUME) == MAT_LAVA)
-                            {
-                                vec dir;
-                                vecfromyawpitch(e.attrs[0], e.attrs[1], 1, 0, dir);
-                                e.o.add(dir);
-                            }
+                                e.o.add(vec(e.attrs[0]*RAD, e.attrs[1]*RAD));
                         }
                     }
                     if(e.attrs[0] >= 0) checkyawmode(e, mtype, mver, gver, 0, -1);
@@ -1666,6 +1775,10 @@ namespace entities
                     { // insert mine before rockets (9 -> 10) after grenades (8)
                         if(e.attrs[0] >= 9) e.attrs[0]++;
                     }
+                    if(mtype == MAP_MAPZ && gver <= 221)
+                    { // insert zapper before rifle (7 -> 8) after plasma (6)
+                        if(e.attrs[0] >= 7) e.attrs[0]++;
+                    }
                     break;
                 }
                 case TRIGGER:
@@ -1711,7 +1824,7 @@ namespace entities
                 }
                 case ACTOR:
                 {
-                    if(mtype == MAP_MAPZ && gver <= 200) e.attrs[0] -= 1; // remoe AI_BOT from array
+                    if(mtype == MAP_MAPZ && gver <= 200) e.attrs[0] -= 1; // remoe A_BOT from array
                     if(mtype == MAP_MAPZ && gver <= 207 && e.attrs[0] > 1) // combine into one type
                     {
                         switch(e.attrs[0])
@@ -1751,9 +1864,6 @@ namespace entities
             }
             if(gver <= 216 && enttype[e.type].modesattr > 0) // mode/mutator array updates
             {
-#ifdef CAMPAIGN
-                int attr = enttype[e.type].modesattr+1;
-#else
                 int attr = enttype[e.type].modesattr;
                 if(e.attrs[attr])
                 {
@@ -1768,7 +1878,6 @@ namespace entities
                     e.attrs[attr] = neg ? 0-value : value;
                 }
                 attr++; // mutators
-#endif
                 if(e.attrs[attr])
                 {
                     bool neg = e.attrs[attr] < 0;
@@ -1784,9 +1893,11 @@ namespace entities
         }
     }
 
-    void initents(stream *g, int mtype, int mver, char *gid, int gver)
+    void initents(int mtype, int mver, char *gid, int gver)
     {
-        numactors = numcheckpoints = 0;
+        lastroutenode = routeid = -1;
+        numactors = lastroutetime = droproute = 0;
+        airnodes.setsize(0);
         ai::oldwaypoints.setsize(0);
         loopv(ents)
         {
@@ -1797,7 +1908,7 @@ namespace entities
             else if(e.attrs.length() > num) e.attrs.setsize(num);
         }
         if(mtype == MAP_OCTA || (mtype == MAP_MAPZ && gver <= 49)) importentities(mtype, mver, gver);
-        if(mtype == MAP_OCTA || (mtype == MAP_MAPZ && gver < GAMEVERSION)) updateoldentities(mtype, mver, gver);
+        if(mtype == MAP_OCTA || (mtype == MAP_MAPZ && gver < VERSION_GAME)) updateoldentities(mtype, mver, gver);
         loopv(ents)
         {
             progress(i/float(ents.length()), "fixing entities...");
@@ -1805,13 +1916,12 @@ namespace entities
             switch(ents[i]->type)
             {
                 case ACTOR: numactors++; break;
-                case CHECKPOINT: if(ents[i]->attrs[6] == CP_LAST || ents[i]->attrs[6] == CP_FINISH) numcheckpoints++; break;
                 default: break;
             }
         }
         memset(lastenttype, 0, sizeof(lastenttype));
         memset(lastusetype, 0, sizeof(lastusetype));
-        if(m_enemies(game::gamemode, game::mutators) && !numactors)
+        if(m_onslaught(game::gamemode, game::mutators) && !numactors)
         {
             loopv(ents) if(ents[i]->type == PLAYERSTART || ents[i]->type == WEAPON)
             {
@@ -1820,9 +1930,9 @@ namespace entities
                 e.type = ACTOR;
                 e.o = ents[i]->o;
                 e.attrs.add(0, max(5, enttype[ACTOR].numattrs));
-                e.attrs[0] = (i%5 != 4 ? AI_GRUNT : AI_TURRET)-1;
+                e.attrs[0] = (i%5 != 4 ? A_GRUNT : A_TURRET)-1;
                 e.attrs[6] = (i/5)%(W_MAX+1);
-                if(e.attrs[0] == AI_TURRET && e.attrs[5] == W_MELEE) e.attrs[5] = W_SMG;
+                if(e.attrs[0] == A_TURRET && e.attrs[5] == W_MELEE) e.attrs[5] = W_SMG;
                 switch(ents[i]->type)
                 {
                     case PLAYERSTART:
@@ -1837,36 +1947,14 @@ namespace entities
                 numactors++;
             }
         }
-        if(m_gauntlet(game::gamemode) && !numcheckpoints) loopj(3)
-        {
-            loopv(ents) if(j == 2 || (ents[i]->type == (!j ? AFFINITY : PLAYERSTART) && ents[i]->attrs[0] == T_OMEGA))
-            {
-                extentity &e = *newent();
-                ents.add(&e);
-                e.type = CHECKPOINT;
-                e.o = ents[i]->o;
-                e.attrs.add(0, max(5, enttype[CHECKPOINT].numattrs));
-                e.attrs[0] = enttype[AFFINITY].radius;
-                if(j != 2) e.attrs[1] = ents[i]->attrs[1];
-                e.attrs[6] = CP_FINISH;
-                numcheckpoints++;
-                break;
-            }
-            if(numcheckpoints) break;
-        }
         loopv(ents)
         {
             gameentity &e = *(gameentity *)ents[i];
             progress(i/float(ents.length()), "updating entities...");
             if(mtype == MAP_MAPZ && gver <= 212)
             {
-#ifdef MEK
-                if(e.type == HEALTH) e.type = NOTUSED;
-                else if(e.type == ARMOUR)
-#else
-                if(e.type == DUMMY1) e.type = NOTUSED;
-                else if(e.type == DUMMY2)
-#endif
+                if(e.type == ROUTE) e.type = NOTUSED;
+                else if(e.type == UNUSEDENT)
                 {
                     ai::oldwaypoint &o = ai::oldwaypoints.add();
                     o.o = e.o;
@@ -1875,6 +1963,7 @@ namespace entities
                     e.type = NOTUSED;
                 }
             }
+            if(mtype == MAP_MAPZ && gver <= 221 && (e.type == ROUTE || e.type == UNUSEDENT)) e.type = NOTUSED;
             if(e.type < MAXENTTYPES)
             {
                 lastenttype[e.type] = max(lastenttype[e.type], i+1);
@@ -1939,7 +2028,7 @@ namespace entities
                 }
                 case ACTOR:
                 {
-                    part_radius(vec(e.o).add(vec(0, 0, aistyle[e.attrs[0]].height/2)), vec(aistyle[e.attrs[0]].xradius, aistyle[e.attrs[0]].height/2), showentsize, 1, 1, 0x888888);
+                    part_radius(vec(e.o).add(vec(0, 0, actor[e.attrs[0]].height/2)), vec(actor[e.attrs[0]].xradius, actor[e.attrs[0]].height/2), showentsize, 1, 1, 0x888888);
                     part_radius(e.o, vec(ai::ALERTMAX, ai::ALERTMAX, ai::ALERTMAX), showentsize, 1, 1, 0x888888);
                     break;
                 }
@@ -1995,7 +2084,7 @@ namespace entities
                 }
                 case AFFINITY:
                 {
-                    float radius = (float)enttype[e.type].radius;
+                    float radius = enttype[e.type].radius;
                     part_radius(e.o, vec(radius, radius, radius), showentsize, 1, 1, TEAM(e.attrs[0], colour));
                     radius = radius*2/3; // capture pickup dist
                     part_radius(e.o, vec(radius, radius, radius), showentsize, 1, 1, TEAM(e.attrs[0], colour));
@@ -2003,11 +2092,11 @@ namespace entities
                 }
                 default:
                 {
-                    float radius = (float)enttype[e.type].radius;
+                    float radius = enttype[e.type].radius;
                     if((e.type == TRIGGER || e.type == TELEPORT || e.type == PUSHER || e.type == CHECKPOINT) && e.attrs[e.type == CHECKPOINT ? 0 : 3])
-                        radius = (float)e.attrs[e.type == CHECKPOINT ? 0 : 3];
-                    if(radius > 0.f) part_radius(e.o, vec(radius, radius, radius), showentsize, 1, 1, 0x00FFFF);
-                    if(e.type == PUSHER && e.attrs[4] && e.attrs[4] < e.attrs[3])
+                        radius = e.attrs[e.type == CHECKPOINT ? 0 : 3];
+                    if(radius > 0) part_radius(e.o, vec(radius, radius, radius), showentsize, 1, 1, 0x00FFFF);
+                    if(e.type == PUSHER && e.attrs[4] > 0 && e.attrs[4] < radius)
                         part_radius(e.o, vec(e.attrs[4], e.attrs[4], e.attrs[4]), showentsize, 1, 1, 0x00FFFF);
                     break;
                 }
@@ -2070,11 +2159,11 @@ namespace entities
             if(ents.inrange(enthover) && islightable(ents[enthover]))
                 renderfocus(enthover, renderentlight(e));
         }
-        loopi(lastenttype[LIGHTFX]) if(ents[i]->type == LIGHTFX && ents[i]->attrs[0] != LFX_SPOTLIGHT)
+        loopi(lastent(LIGHTFX)) if(ents[i]->type == LIGHTFX && ents[i]->attrs[0] != LFX_SPOTLIGHT)
         {
-            if(ents[i]->spawned || ents[i]->lastemit)
+            if(ents[i]->spawned() || ents[i]->lastemit)
             {
-                if(!ents[i]->spawned && ents[i]->lastemit > 0 && lastmillis-ents[i]->lastemit > triggertime(*ents[i])/2)
+                if(!ents[i]->spawned() && ents[i]->lastemit > 0 && lastmillis-ents[i]->lastemit > triggertime(*ents[i])/2)
                     continue;
             }
             else
@@ -2090,14 +2179,61 @@ namespace entities
 
     void update()
     {
-        loopi(lastenttype[MAPSOUND])
+        loopi(lastent(MAPSOUND))
         {
             gameentity &e = *(gameentity *)ents[i];
             if(e.type == MAPSOUND && e.links.empty() && mapsounds.inrange(e.attrs[0]) && !issound(e.schan))
             {
                 int flags = SND_MAP|SND_LOOP; // ambient sounds loop
                 loopk(SND_LAST)  if(e.attrs[4]&(1<<k)) flags |= 1<<k;
-                playsound(e.attrs[0], e.o, NULL, flags, e.attrs[3], e.attrs[1], e.attrs[2], &e.schan);
+                playsound(e.attrs[0], e.o, NULL, flags, e.attrs[3] ? e.attrs[3] : 255, e.attrs[1] || e.attrs[2] ? e.attrs[1] : -1, e.attrs[2] ? e.attrs[2] : -1, &e.schan);
+            }
+        }
+        if((m_edit(game::gamemode) || m_race(game::gamemode)) && routeid >= 0 && droproute)
+        {
+            if(game::player1->state == CS_ALIVE)
+            {   // don't start until the player begins moving
+                if(lastroutenode >= 0 || game::player1->move || game::player1->strafe)
+                {
+                    const vec o = game::player1->feetpos();
+                    int curnode = lastroutenode;
+                    if(!ents.inrange(curnode) || ents[curnode]->o.dist(o) >= droproutedist)
+                    {
+                        curnode = -1;
+                        loopi(lastent(ROUTE)) if(ents[i]->type == ROUTE && ents[i]->attrs[0] == routeid)
+                        {
+                            float dist = ents[i]->o.dist(o);
+                            if(dist < droproutedist && (!ents.inrange(curnode) || dist < ents[curnode]->o.dist(o)))
+                                curnode = i;
+                        }
+                    }
+                    if(!ents.inrange(curnode))
+                    {
+                        attrvector attrs;
+                        attrs.add(routeid);
+                        attrs.add(int(game::player1->yaw));
+                        attrs.add(int(game::player1->pitch));
+                        attrs.add(game::player1->move);
+                        attrs.add(game::player1->strafe);
+                        attrs.add(0);
+                        loopi(AC_MAX) if(game::player1->action[i] || (abs(game::player1->actiontime[i]) > lastroutetime))
+                            attrs[5] |= (1<<i);
+                        int n = newentity(o, int(ROUTE), attrs);
+                        if(ents.inrange(lastroutenode)) ents[lastroutenode]->links.add(n);
+                        curnode = lastenttype[ROUTE] = n;
+                        if(game::player1->airmillis) airnodes.add(n);
+                    }
+                    if(!game::player1->airmillis && !airnodes.empty()) airnodes.setsize(0);
+                    if(lastroutenode != curnode) lastroutetime = lastmillis;
+                    lastroutenode = curnode;
+                }
+            }
+            else if(lastroutenode >= 0)
+            {
+                lastroutenode = -1;
+                lastroutetime = 0;
+                if(game::player1->state == CS_DEAD) loopv(airnodes) if(ents.inrange(airnodes[i])) ents[airnodes[i]]->type = ET_EMPTY;
+                airnodes.setsize(0);
             }
         }
     }
@@ -2108,12 +2244,12 @@ namespace entities
             renderfocus(i, renderentshow(e, i, game::player1->state == CS_EDITING ? ((entgroup.find(i) >= 0 || enthover == i) ? 1 : 2) : 3));
         if(!envmapping)
         {
-            int numents = m_edit(game::gamemode) ? ents.length() : lastusetype[EU_ITEM];
+            int numents = m_edit(game::gamemode) ? ents.length() : lastuse(EU_ITEM);
             loopi(numents)
             {
                 gameentity &e = *(gameentity *)ents[i];
                 if(e.type <= NOTUSED || e.type >= MAXENTTYPES || (enttype[e.type].usetype == EU_ITEM && simpleitems)) continue;
-                bool active = enttype[e.type].usetype == EU_ITEM && (e.spawned || (e.lastemit && lastmillis-e.lastemit < 500));
+                bool active = enttype[e.type].usetype == EU_ITEM && (e.spawned() || (e.lastemit && lastmillis-e.lastemit < 500));
                 if((m_edit(game::gamemode) && rendermainview) || active)
                 {
                     const char *mdlname = entmdlname(e.type, e.attrs);
@@ -2136,13 +2272,13 @@ namespace entities
                             {
                                 yaw = e.attrs[1]+90;
                                 pitch = e.attrs[2];
-                                int weap = e.attrs[6] > 0 ? e.attrs[6]-1 : aistyle[e.attrs[0]].weap;
+                                int weap = e.attrs[6] > 0 ? e.attrs[6]-1 : actor[e.attrs[0]].weap;
                                 if(isweap(weap)) colour = W(weap, colour);
-                                size = e.attrs[9] > 0 ? e.attrs[9]/100.f : aistyle[e.attrs[0]].scale;
+                                size = e.attrs[9] > 0 ? e.attrs[9]/100.f : actor[e.attrs[0]].scale;
                             }
                             //fade = 0.5f;
                         }
-                        else if(e.spawned)
+                        else if(e.spawned())
                         {
                             int millis = lastmillis-e.lastspawn;
                             if(millis < 500) size = fade = float(millis)/500.f;
@@ -2155,7 +2291,7 @@ namespace entities
                         if(e.type == WEAPON)
                         {
                             flags |= MDL_LIGHTFX;
-                            int col = W(w_attr(game::gamemode, game::mutators, e.attrs[0], m_weapon(game::gamemode, game::mutators)), colour), interval = lastmillis%1000;
+                            int col = W(w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], m_weapon(game::gamemode, game::mutators)), colour), interval = lastmillis%1000;
                             e.light.effect = vec::hexcolor(col).mul(interval >= 500 ? (1000-interval)/500.f : interval/500.f);
                         }
                         else e.light.effect = vec(0, 0, 0);
@@ -2184,13 +2320,23 @@ namespace entities
         switch(e.type)
         {
             case PARTICLES:
-                if(idx < 0 || e.links.empty()) makeparticles(e);
-                else if(e.spawned || (e.lastemit > 0 && lastmillis-e.lastemit <= triggertime(e)/2))
-                    makeparticle(o, e.attrs);
+                if(idx >= 0 && e.attrs[11])
+                {
+                    if((e.nextemit -= curtime) <= 0) e.nextemit = 0;
+                    if(e.nextemit) break;
+                }
+                if(idx < 0 || e.links.empty() || e.spawned() || (e.lastemit > 0 && lastmillis-e.lastemit <= triggertime(e)/2)) makeparticle(o, e.attrs);
+                if(idx >= 0 && e.attrs[11]) e.nextemit += e.attrs[11];
                 break;
             case TELEPORT:
                 if(e.attrs[4]) maketeleport(e);
                 break;
+            case ROUTE:
+            {
+                if(e.attrs[0] != routeid || (!m_edit(game::gamemode) && !m_race(game::gamemode))) break;
+                loopv(e.links) if(ents.inrange(e.links[i]) && ents[e.links[i]]->type == ROUTE && (!routemaxdist || e.o.dist(ents[e.links[i]]->o) <= routemaxdist))
+                    part_flare(e.o, ents[e.links[i]]->o, 1, PART_LIGHTNING_FLARE, routecolour);
+            }
             default: break;
         }
 
@@ -2200,8 +2346,8 @@ namespace entities
              hasent = isedit && idx >= 0 && (enthover == idx || entgroup.find(idx) >= 0),
              hastop = hasent && e.o.squaredist(camera1->o) <= showentdist*showentdist;
         int sweap = m_weapon(game::gamemode, game::mutators),
-            attr = e.type == WEAPON ? w_attr(game::gamemode, game::mutators, e.attrs[0], sweap) : e.attrs[0],
-            colour = e.type == WEAPON ? W(attr, colour) : 0xFFFFFF, interval = lastmillis%1000;
+            attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap),
+            colour = e.type == WEAPON ? W(attr, colour) : 0x888888, interval = lastmillis%1000;
         float fluc = interval >= 500 ? (1500-interval)/1000.f : (500+interval)/1000.f;
         if(enttype[e.type].usetype == EU_ITEM && (active || isedit))
         {
@@ -2212,6 +2358,11 @@ namespace entities
                 if(radius < simpleitemsize*skew) radius = simpleitemsize*skew;
                 blend *= simpleitemhalo;
             }
+            else
+            {
+                radius *= haloitemsize;
+                blend *= haloitemblend;
+            }
             vec offset = vec(o).sub(camera1->o).rescale(radius/2);
             offset.z = max(offset.z, -1.0f);
             part_create(PART_HINT_BOLD_SOFT, 1, offset.add(o), colour, radius, blend);
@@ -2238,21 +2389,38 @@ namespace entities
                 const char *attrname = enttype[e.type].attrs[k];
                 if(e.type == PARTICLES && k) switch(e.attrs[0])
                 {
-                    case 0: switch(k) { case 1: attrname = "length"; break; case 2: attrname = "height"; break; case 3: attrname = "colour"; break; case 4: attrname = "fade"; break; case 5: attrname = "palette"; break; case 6: attrname = "palindex"; break; case 7: attrname = "size"; break; case 8: attrname = "blend"; break; case 9: attrname = "gravity"; break; case 10: attrname = "velocity"; break; default: attrname = ""; } break;
-                    case 1: switch(k) { case 1: attrname = "dir"; break; default: attrname = ""; } break;
-                    case 2: switch(k) { case 1: attrname = "dir"; break; default: attrname = ""; } break;
-                    case 3: switch(k) { case 1: attrname = "size"; break; case 2: attrname = "colour"; break; case 3: attrname = "palette"; break; case 4: attrname = "palindex"; break; default: attrname = ""; } break;
-                    case 4: case 7: switch(k) { case 1: attrname = "dir"; break; case 2: attrname = "length"; break; case 3: attrname = "colour"; break; case 4: attrname = "fade"; break; case 5: attrname = "size"; break; case 6: attrname = "palette"; break; case 7: attrname = "palindex"; break; default: attrname = ""; break; } break;
-                    case 8: case 9: case 10: case 11: case 12: case 13: switch(k) { case 1: attrname = "dir"; break; case 2: attrname = "length"; break; case 3: attrname = "colour"; break; case 4: attrname = "fade"; break; case 5: attrname = "size"; break; case 6: attrname = "decal"; break; case 7: attrname = "gravity"; break; case 8: attrname = "velocity"; break; case 9: attrname = "palette"; break; case 10: attrname = "palindex"; break; default: attrname = ""; break; } break;
-                    case 14: case 15: switch(k) { case 1: attrname = "radius"; break; case 2: attrname = "height"; break; case 3: attrname = "colour"; break; case 4: attrname = "fade"; break; case 5: attrname = "size"; break; case 6: attrname = "gravity"; break; case 7: attrname = "velocity"; break; case 8: attrname = "palette"; break; case 9: attrname = "palindex"; break; default: attrname = ""; } break;
-                    case 6: switch(k) { case 1: attrname = "amt"; break; case 2: attrname = "colour"; break; case 3: attrname = "colour2"; break; case 4: attrname = "palette1"; break; case 5: attrname = "palindex1"; break; case 6: attrname = "palette2"; break; case 7: attrname = "palindex2"; break; default: attrname = ""; } break;
-                    case 5: switch(k) { case 1: attrname = "amt"; break; case 2: attrname = "colour"; break; case 3: attrname = "palette"; break; case 4: attrname = "palindex"; break; default: attrname = ""; } break;
-                    case 32: case 33: case 34: case 35: switch(k) { case 1: attrname = "red"; break; case 2: attrname = "green"; break; case 3: attrname = "blue"; break; default: attrname = ""; } break;
+                    case 0: switch(k) { case 1: attrname = "length"; break; case 2: attrname = "height"; break; case 3: attrname = "colour"; break; case 4: attrname = "fade"; break; case 5: attrname = "palette"; break; case 6: attrname = "palindex"; break; case 7: attrname = "size"; break; case 8: attrname = "blend"; break; case 9: attrname = "gravity"; break; case 10: attrname = "velocity"; break; case 11: attrname = "millis"; break; default: attrname = ""; } break;
+                    case 1: switch(k) { case 1: attrname = "dir"; break; case 11: attrname = "millis"; break; default: attrname = ""; } break;
+                    case 2: switch(k) { case 1: attrname = "dir"; break; case 11: attrname = "millis"; break; default: attrname = ""; } break;
+                    case 3: switch(k) { case 1: attrname = "size"; break; case 2: attrname = "colour"; break; case 3: attrname = "palette"; break; case 4: attrname = "palindex"; break; case 11: attrname = "millis"; break; default: attrname = ""; } break;
+                    case 4: case 7: switch(k) { case 1: attrname = "dir"; break; case 2: attrname = "length"; break; case 3: attrname = "colour"; break; case 4: attrname = "fade"; break; case 5: attrname = "size"; break; case 6: attrname = "palette"; break; case 7: attrname = "palindex"; break; case 11: attrname = "millis"; break; default: attrname = ""; break; } break;
+                    case 8: case 9: case 10: case 11: case 12: case 13: switch(k) { case 1: attrname = "dir"; break; case 2: attrname = "length"; break; case 3: attrname = "colour"; break; case 4: attrname = "fade"; break; case 5: attrname = "size"; break; case 6: attrname = "decal"; break; case 7: attrname = "gravity"; break; case 8: attrname = "velocity"; break; case 9: attrname = "palette"; break; case 10: attrname = "palindex"; break; case 11: attrname = "millis"; break; default: att [...]
+                    case 14: case 15: switch(k) { case 1: attrname = "radius"; break; case 2: attrname = "height"; break; case 3: attrname = "colour"; break; case 4: attrname = "fade"; break; case 5: attrname = "size"; break; case 6: attrname = "gravity"; break; case 7: attrname = "velocity"; break; case 8: attrname = "palette"; break; case 9: attrname = "palindex"; break; case 11: attrname = "millis"; break; default: attrname = ""; } break;
+                    case 6: switch(k) { case 1: attrname = "amt"; break; case 2: attrname = "colour"; break; case 3: attrname = "colour2"; break; case 4: attrname = "palette1"; break; case 5: attrname = "palindex1"; break; case 6: attrname = "palette2"; break; case 7: attrname = "palindex2"; break; case 11: attrname = "millis"; break; default: attrname = ""; } break;
+                    case 5: switch(k) { case 1: attrname = "amt"; break; case 2: attrname = "colour"; break; case 3: attrname = "palette"; break; case 4: attrname = "palindex"; break; case 11: attrname = "millis"; break; default: attrname = ""; } break;
+                    case 32: case 33: case 34: case 35: switch(k) { case 1: attrname = "red"; break; case 2: attrname = "green"; break; case 3: attrname = "blue"; break; case 11: attrname = "millis"; break; default: attrname = ""; } break;
                     default: attrname = ""; break;
                 }
                 if(attrname && *attrname)
                 {
-                    defformatstring(s)("%s%s:%d", hastop ? "\fw" : "\fa", attrname, e.attrs[k]);
+                    string attrval; attrval[0] = 0;
+                    if(showentattrinfo&1)
+                    {
+                        defformatstring(s)("\fs\fy%d\fS:", k+1);
+                        concatstring(attrval, s);
+                    }
+                    if(showentattrinfo&2)
+                    {
+                        if(*attrval) concatstring(attrval, " ");
+                        concatstring(attrval, attrname);
+                    }
+                    if(showentattrinfo&4)
+                    {
+                        if(*attrval) concatstring(attrval, " = ");
+                        defformatstring(s)("\fs\fc%d\fS", e.attrs[k]);
+                        concatstring(attrval, s);
+                    }
+                    defformatstring(s)("%s%s", hastop ? "\fw" : "\fa", attrval);
                     part_textcopy(pos.add(off), s, hastop ? PART_TEXT_ONTOP : PART_TEXT);
                 }
             }
@@ -2262,22 +2430,17 @@ namespace entities
     void drawparticles()
     {
         float maxdist = float(maxparticledistance)*float(maxparticledistance);
-        int numents = m_edit(game::gamemode) ? ents.length() : max(lastusetype[EU_ITEM], max(lastenttype[PARTICLES], lastenttype[TELEPORT]));
-        if(m_gauntlet(game::gamemode)) numents = max(numents, lastenttype[CHECKPOINT]);
+        int numents = m_edit(game::gamemode) ? ents.length() : max(lastuse(EU_ITEM), max(lastent(PARTICLES), lastent(TELEPORT)));
+        bool hasroute = (m_edit(game::gamemode) || m_race(game::gamemode)) && routeid >= 0;
+        if(hasroute) numents = max(numents, lastent(ROUTE));
         loopi(numents)
         {
             gameentity &e = *(gameentity *)ents[i];
-            if(m_gauntlet(game::gamemode) && e.type == CHECKPOINT && (e.attrs[6] == CP_FINISH || e.attrs[6] == CP_LAST))
-            {
-                float radius = float(max(e.attrs[0], enttype[e.type].radius));
-                part_explosion(e.o, radius, PART_SHOCKWAVE, 1, TEAM(T_ALPHA, colour), 1, 0.125f);
-                part_explosion(e.o, min(radius/3.f, enttype[e.type].radius/3.f), PART_SHOCKBALL, 1, TEAM(T_ALPHA, colour), 1, 0.25f);
-            }
-            else if(e.type != PARTICLES && e.type != TELEPORT && !m_edit(game::gamemode) && enttype[e.type].usetype != EU_ITEM) continue;
+            if(e.type != PARTICLES && e.type != TELEPORT && e.type != ROUTE && !m_edit(game::gamemode) && enttype[e.type].usetype != EU_ITEM) continue;
             else if(e.o.squaredist(camera1->o) > maxdist) continue;
             float skew = 1;
             bool active = false;
-            if(e.spawned)
+            if(e.spawned())
             {
                 int millis = lastmillis-e.lastspawn;
                 if(millis < 500) skew = float(millis)/500.f;
@@ -2292,7 +2455,7 @@ namespace entities
                     active = true;
                 }
             }
-            drawparticle(e, e.o, i, e.spawned, active, skew);
+            drawparticle(e, e.o, i, e.spawned(), active, skew);
         }
         loopv(projs::projs)
         {
diff --git a/src/game/game.cpp b/src/game/game.cpp
index 6907569..d748387 100644
--- a/src/game/game.cpp
+++ b/src/game/game.cpp
@@ -2,9 +2,9 @@
 #include "game.h"
 namespace game
 {
-    int nextmode = G_EDITMODE, nextmuts = 0, gamemode = G_EDITMODE, mutators = 0, maptime = 0, timeremaining = 0,
-        lastcamera = 0, lasttvcam = 0, lasttvchg = 0, lastzoom = 0, liquidchan = -1;
-    bool intermission = false, prevzoom = false, zooming = false, inputmouse = false, inputview = false, inputmode = false;
+    int nextmode = G_EDITMODE, nextmuts = 0, gamestate = G_S_WAITING, gamemode = G_EDITMODE, mutators = 0, maptime = 0, timeremaining = 0, lasttimeremain = 0,
+        lastcamera = 0, lasttvcam = 0, lasttvchg = 0, lastzoom = 0, spectvfollowing = -1;
+    bool prevzoom = false, zooming = false, inputmouse = false, inputview = false, inputmode = false;
     float swayfade = 0, swayspeed = 0, swaydist = 0, bobfade = 0, bobdist = 0;
     vec swaydir(0, 0, 0), swaypush(0, 0, 0);
 
@@ -15,6 +15,25 @@ namespace game
     vector<gameent *> players, waiting;
     vector<cament *> cameras;
 
+    void start()
+    {
+        player1->version.major = VERSION_MAJOR;
+        player1->version.minor = VERSION_MINOR;
+        player1->version.patch = VERSION_PATCH;
+        player1->version.game = VERSION_GAME;
+        player1->version.platform = versionplatform;
+        player1->version.arch = versionarch;
+        player1->version.gpuglver = glversion;
+        player1->version.gpuglslver = glslversion;
+        player1->version.crc = versioncrc;
+        if(player1->version.gpuvendor) delete[] player1->version.gpuvendor;
+        player1->version.gpuvendor = newstring(gfxvendor);
+        if(player1->version.gpurenderer) delete[] player1->version.gpurenderer;
+        player1->version.gpurenderer = newstring(gfxrenderer);
+        if(player1->version.gpuversion) delete[] player1->version.gpuversion;
+        player1->version.gpuversion = newstring(gfxversion);
+    }
+
     FVAR(IDF_WORLD, illumlevel, 0, 0, 2);
     VAR(IDF_WORLD, illumradius, 0, 0, VAR_MAX);
     #define OBITVARS(name) \
@@ -24,16 +43,12 @@ namespace game
         SVAR(IDF_WORLD, obit##name##4, ""); \
         const char *getobit##name(int mat, const char *def = NULL) \
         { \
-            loopi(2) \
+            loopi(2) switch(i ? 0 : mat&MATF_INDEX) \
             { \
-                int type = i ? 0 : mat&MATF_INDEX; \
-                switch(type) \
-                { \
-                    default: case 0: if(!def || *obit##name) return obit##name; break; \
-                    case 1: if(*obit##name##2) return obit##name##2; break; \
-                    case 2: if(*obit##name##3) return obit##name##3; break; \
-                    case 3: if(*obit##name##4) return obit##name##4; break; \
-                } \
+                default: case 0: if(!def || *obit##name) return obit##name; break; \
+                case 1: if(*obit##name##2) return obit##name##2; break; \
+                case 2: if(*obit##name##3) return obit##name##3; break; \
+                case 3: if(*obit##name##4) return obit##name##4; break; \
             } \
             return def ? def : ""; \
         }
@@ -44,32 +59,29 @@ namespace game
 
     void stopmapmusic()
     {
-        if(connected() && maptime > 0 && !intermission) musicdone(true);
+        if(connected() && maptime > 0 && gs_intermission(gamestate)) musicdone(true);
     }
-    VARF(IDF_PERSIST, musictype, 0, 1, 5, stopmapmusic()); // 0 = no in-game music, 1 = map music (or random if none), 2 = always random, 3 = map music (silence if none), 4-5 = same as 1-2 but pick new tracks when done
-    VARF(IDF_PERSIST, musicedit, -1, 0, 5, stopmapmusic()); // same as above for editmode, -1 = use musictype
+    VARF(IDF_PERSIST, musictype, 0, 1, 6, stopmapmusic()); // 0 = no in-game music, 1 = map music (or random if none), 2 = always random, 3 = map music (silence if none), 4-5 = same as 1-2 but pick new tracks when done, 6 = always use theme song
+    VARF(IDF_PERSIST, musicedit, -1, 0, 6, stopmapmusic()); // same as above for editmode, -1 = use musictype
     SVARF(IDF_PERSIST, musicdir, "sounds/music", stopmapmusic());
     SVARF(IDF_WORLD, mapmusic, "", stopmapmusic());
 
     VAR(IDF_PERSIST, thirdperson, 0, 0, 1);
     VAR(IDF_PERSIST, dynlighteffects, 0, 2, 2);
+    VAR(IDF_PERSIST, fullbrightfocus, 0, 0, 3); // bitwise: 0 = don't fullbright focus, 1 = fullbright non-player1, 2 = fullbright player1
 
     VAR(IDF_PERSIST, thirdpersonmodel, 0, 1, 1);
     VAR(IDF_PERSIST, thirdpersonfov, 90, 120, 150);
     FVAR(IDF_PERSIST, thirdpersonblend, 0, 1, 1);
     VAR(IDF_PERSIST, thirdpersoninterp, 0, 100, VAR_MAX);
-    FVAR(IDF_PERSIST, thirdpersondist, FVAR_NONZERO, 14, 100);
-    FVAR(IDF_PERSIST, thirdpersonside, FVAR_MIN, 14, 10);
-    VAR(IDF_PERSIST, thirdpersoncursor, 0, 2, 2);
+    FVAR(IDF_PERSIST, thirdpersondist, FVAR_NONZERO, 14, 20);
+    FVAR(IDF_PERSIST, thirdpersonside, -20, 7, 20);
+    VAR(IDF_PERSIST, thirdpersoncursor, 0, 1, 2);
     FVAR(IDF_PERSIST, thirdpersoncursorx, 0, 0.5f, 1);
     FVAR(IDF_PERSIST, thirdpersoncursory, 0, 0.5f, 1);
+    FVAR(IDF_PERSIST, thirdpersoncursordist, 0, 256, FVAR_MAX);
 
     VARF(0, follow, -1, -1, VAR_MAX, followswitch(0));
-    void resetfollow()
-    {
-        focus = player1;
-        follow = -1;
-    }
 
     VAR(IDF_PERSIST, firstpersonmodel, 0, 2, 2);
     VAR(IDF_PERSIST, firstpersonfov, 90, 100, 150);
@@ -107,10 +119,12 @@ namespace game
     VAR(IDF_PERSIST, editfov, 1, 120, 179);
     VAR(IDF_PERSIST, specfov, 1, 120, 179);
 
+    VAR(IDF_PERSIST, specresetstyle, 0, 1, 1); // 0 = back to player1, 1 = stay at camera
+
     VAR(IDF_PERSIST, followmode, 0, 1, 1); // 0 = never, 1 = tv
-    VARF(IDF_PERSIST, specmode, 0, 1, 1, resetfollow()); // 0 = float, 1 = tv
-    VARF(IDF_PERSIST, waitmode, 0, 1, 1, resetfollow()); // 0 = float, 1 = tv
-    VARF(IDF_PERSIST, intermmode, 0, 1, 1, resetfollow()); // 0 = float, 1 = tv
+    VARF(IDF_PERSIST, specmode, 0, 1, 1, specreset()); // 0 = float, 1 = tv
+    VARF(IDF_PERSIST, waitmode, 0, 1, 1, specreset()); // 0 = float, 1 = tv
+    VARF(IDF_PERSIST, intermmode, 0, 1, 1, specreset()); // 0 = float, 1 = tv
 
     VAR(IDF_PERSIST, followdead, 0, 1, 2); // 0 = never, 1 = in all but duel/survivor, 2 = always
     VAR(IDF_PERSIST, followthirdperson, 0, 1, 1);
@@ -129,12 +143,12 @@ namespace game
     FVAR(IDF_PERSIST, followtvpitchthresh, 0, 0, 180);
 
     VAR(IDF_PERSIST, spectvtime, 1000, 10000, VAR_MAX);
-    VAR(IDF_PERSIST, spectvmintime, 1000, 5000, VAR_MAX);
-    VAR(IDF_PERSIST, spectvmaxtime, 0, 20000, VAR_MAX);
-    VAR(IDF_PERSIST, spectvspeed, 1, 1000, VAR_MAX);
-    VAR(IDF_PERSIST, spectvyawspeed, 1, 1000, VAR_MAX);
-    VAR(IDF_PERSIST, spectvpitchspeed, 1, 750, VAR_MAX);
-    FVAR(IDF_PERSIST, spectvrotate, FVAR_MIN, 45, FVAR_MAX); // rotate style, < 0 = absolute angle, 0 = scaled, > 0 = scaled with max angle
+    VAR(IDF_PERSIST, spectvmintime, 1000, 3000, VAR_MAX);
+    VAR(IDF_PERSIST, spectvmaxtime, 0, 15000, VAR_MAX);
+    VAR(IDF_PERSIST, spectvspeed, 1, 500, VAR_MAX);
+    VAR(IDF_PERSIST, spectvyawspeed, 1, 500, VAR_MAX);
+    VAR(IDF_PERSIST, spectvpitchspeed, 1, 500, VAR_MAX);
+    FVAR(IDF_PERSIST, spectvrotate, FVAR_MIN, 0, FVAR_MAX); // rotate style, < 0 = absolute angle, 0 = scaled, > 0 = scaled with max angle
     FVAR(IDF_PERSIST, spectvyawscale, FVAR_MIN, 1, 1000);
     FVAR(IDF_PERSIST, spectvpitchscale, FVAR_MIN, 1, 1000);
     FVAR(IDF_PERSIST, spectvyawthresh, 0, 0, 360);
@@ -143,6 +157,35 @@ namespace game
     VAR(IDF_PERSIST, spectvfirstperson, 0, 0, 2); // 0 = aim in direction followed player is facing, 1 = aim in direction determined by spectv when dead, 2 = always aim in direction
     VAR(IDF_PERSIST, spectvthirdperson, 0, 2, 2); // 0 = aim in direction followed player is facing, 1 = aim in direction determined by spectv when dead, 2 = always aim in direction
 
+    VAR(IDF_PERSIST, spectvintermtime, 1000, 10000, VAR_MAX);
+    VAR(IDF_PERSIST, spectvintermmintime, 1000, 6000, VAR_MAX);
+    VAR(IDF_PERSIST, spectvintermmaxtime, 0, 20000, VAR_MAX);
+    VAR(IDF_PERSIST, spectvintermspeed, 1, 500, VAR_MAX);
+    VAR(IDF_PERSIST, spectvintermyawspeed, 1, 500, VAR_MAX);
+    VAR(IDF_PERSIST, spectvintermpitchspeed, 1, 500, VAR_MAX);
+    FVAR(IDF_PERSIST, spectvintermrotate, FVAR_MIN, 0, FVAR_MAX); // rotate style, < 0 = absolute angle, 0 = scaled, > 0 = scaled with max angle
+    FVAR(IDF_PERSIST, spectvintermyawscale, FVAR_MIN, 1, 1000);
+    FVAR(IDF_PERSIST, spectvintermpitchscale, FVAR_MIN, 1, 1000);
+    FVAR(IDF_PERSIST, spectvintermyawthresh, 0, 0, 360);
+    FVAR(IDF_PERSIST, spectvintermpitchthresh, 0, 0, 180);
+
+    VARF(0, spectvfollow, -1, -1, VAR_MAX, spectvfollowing = spectvfollow); // attempts to always keep this client in view
+    VAR(0, spectvfollowself, 0, 1, 2); // if we are not spectating, spectv should show us; 0 = off, 1 = not duel/survivor, 2 = always
+    VAR(IDF_PERSIST, spectvfollowtime, 1000, 10000, VAR_MAX);
+    VAR(IDF_PERSIST, spectvfollowmintime, 1000, 1000, VAR_MAX);
+    VAR(IDF_PERSIST, spectvfollowmaxtime, 0, 20000, VAR_MAX);
+    VAR(IDF_PERSIST, spectvfollowspeed, 1, 500, VAR_MAX);
+    VAR(IDF_PERSIST, spectvfollowyawspeed, 1, 500, VAR_MAX);
+    VAR(IDF_PERSIST, spectvfollowpitchspeed, 1, 350, VAR_MAX);
+    FVAR(IDF_PERSIST, spectvfollowrotate, FVAR_MIN, 45, FVAR_MAX); // rotate style, < 0 = absolute angle, 0 = scaled, > 0 = scaled with max angle
+    FVAR(IDF_PERSIST, spectvfollowyawscale, FVAR_MIN, 1, 1000);
+    FVAR(IDF_PERSIST, spectvfollowpitchscale, FVAR_MIN, 1, 1000);
+    FVAR(IDF_PERSIST, spectvfollowyawthresh, 0, 0, 360);
+    FVAR(IDF_PERSIST, spectvfollowpitchthresh, 0, 0, 180);
+
+    FVAR(IDF_PERSIST, spectvmindist, 0, 0, FVAR_MAX);
+    FVAR(IDF_PERSIST, spectvmaxdist, 0, 128, FVAR_MAX);
+
     VAR(IDF_PERSIST, deathcamstyle, 0, 2, 2); // 0 = no follow, 1 = follow attacker, 2 = follow self
     VAR(IDF_PERSIST, deathcamspeed, 0, 500, VAR_MAX);
 
@@ -161,16 +204,24 @@ namespace game
     VAR(IDF_PERSIST, zoomscroll, 0, 0, 1); // 0 = stop at min/max, 1 = go to opposite end
 
     VAR(IDF_PERSIST, aboveheadnames, 0, 1, 1);
+    VAR(IDF_PERSIST, aboveheadinventory, 0, 0, 2); // 0 = off, 1 = weapselect only, 2 = all weapons
     VAR(IDF_PERSIST, aboveheadstatus, 0, 1, 1);
-    VAR(IDF_PERSIST, aboveheadteam, 0, 1, 2);
+    VAR(IDF_PERSIST, aboveheadteam, 0, 0, 3);
     VAR(IDF_PERSIST, aboveheaddamage, 0, 0, 1);
     VAR(IDF_PERSIST, aboveheadicons, 0, 5, 7);
     FVAR(IDF_PERSIST, aboveheadblend, 0.f, 1, 1.f);
-    FVAR(IDF_PERSIST, aboveheadnamesize, 0, 2.5f, 1000);
-    FVAR(IDF_PERSIST, aboveheadstatussize, 0, 2.5f, 1000);
-    FVAR(IDF_PERSIST, aboveheadiconsize, 0, 2.5f, 1000);
-    FVAR(IDF_PERSIST, aboveheadeventsize, 0, 2.5f, 1000);
-    FVAR(IDF_PERSIST, aboveitemiconsize, 0, 2.25f, 1000);
+    FVAR(IDF_PERSIST, aboveheadnamesblend, 0.f, 1, 1.f);
+    FVAR(IDF_PERSIST, aboveheadinventoryblend, 0.f, 0.8f, 1.f);
+    FVAR(IDF_PERSIST, aboveheadinventoryfade, 0.f, 0.5f, 1.f);
+    FVAR(IDF_PERSIST, aboveheadstatusblend, 0.f, 1, 1.f);
+    FVAR(IDF_PERSIST, aboveheadiconsblend, 0.f, 1, 1.f);
+    FVAR(IDF_PERSIST, aboveheadnamessize, 0, 3, 10);
+    FVAR(IDF_PERSIST, aboveheadinventorysize, 0, 4, 10);
+    FVAR(IDF_PERSIST, aboveheadstatussize, 0, 2.5f, 10);
+    FVAR(IDF_PERSIST, aboveheadiconssize, 0, 2.5f, 10);
+    FVAR(IDF_PERSIST, aboveheadeventsize, 0, 3, 10);
+
+    FVAR(IDF_PERSIST, aboveitemiconsize, 0, 2.5f, 10);
 
     FVAR(IDF_PERSIST, aboveheadsmooth, 0, 0.25f, 1);
     VAR(IDF_PERSIST, aboveheadsmoothmillis, 1, 100, 10000);
@@ -179,9 +230,10 @@ namespace game
     VAR(IDF_PERSIST, eventiconshort, 500, 3500, VAR_MAX);
 
     VAR(IDF_PERSIST, showobituaries, 0, 4, 5); // 0 = off, 1 = only me, 2 = 1 + announcements, 3 = 2 + but dying bots, 4 = 3 + but bot vs bot, 5 = all
-    VAR(IDF_PERSIST, showobitdists, 0, 1, 1);
+    VAR(IDF_PERSIST, showobitdists, 0, 2, 2); // 0 = off, 1 = self only, 2 = all
+    VAR(IDF_PERSIST, showobithpleft, 0, 1, 2); // 0 = off, 1 = self only, 2 = all
     VAR(IDF_PERSIST, obitannounce, 0, 2, 2); // 0 = off, 1 = only focus, 2 = everyone
-    VAR(IDF_PERSIST, obitverbose, 0, 2, 2); // 0 = extremely simple, 1 = simplified per-weapon, 2 = regular messages
+    VAR(IDF_PERSIST, obitverbose, 0, 1, 1); // 0 = extremely simple, 1 = regular messages
     VAR(IDF_PERSIST, obitstyles, 0, 1, 1); // 0 = no obituary styles, 1 = show sprees/dominations/etc
 
     VAR(IDF_PERSIST, damagemergedelay, 0, 75, VAR_MAX);
@@ -195,7 +247,8 @@ namespace game
 
     VAR(IDF_PERSIST, deathanim, 0, 2, 2); // 0 = hide player when dead, 1 = old death animation, 2 = ragdolls
     VAR(IDF_PERSIST, deathfade, 0, 1, 1); // 0 = don't fade out dead players, 1 = fade them out
-    VAR(IDF_PERSIST, deathscale, 0, 1, 1); // 0 = don't scale out dead players, 1 = scale them out
+    VAR(IDF_PERSIST, deathscale, 0, 0, 1); // 0 = don't scale out dead players, 1 = scale them out
+    VAR(IDF_PERSIST, deathbuttonmash, 0, 1000, VAR_MAX);
     FVAR(IDF_PERSIST, bloodscale, 0, 1, 1000);
     VAR(IDF_PERSIST, bloodfade, 1, 3000, VAR_MAX);
     VAR(IDF_PERSIST, bloodsize, 1, 50, 1000);
@@ -205,42 +258,60 @@ namespace game
     FVAR(IDF_PERSIST, gibscale, 0, 1, 1000);
     VAR(IDF_PERSIST, gibfade, 1, 5000, VAR_MAX);
     FVAR(IDF_PERSIST, impulsescale, 0, 1, 1000);
-    VAR(IDF_PERSIST, impulsefade, 0, 350, VAR_MAX);
+    VAR(IDF_PERSIST, impulsefade, 0, 200, VAR_MAX);
     VAR(IDF_PERSIST, ragdolleffect, 2, 500, VAR_MAX);
 
     VAR(IDF_PERSIST, playerovertone, -1, CTONE_TEAM, CTONE_MAX-1);
     VAR(IDF_PERSIST, playerundertone, -1, CTONE_TONE, CTONE_MAX-1);
     VAR(IDF_PERSIST, playerdisplaytone, -1, CTONE_TONE, CTONE_MAX-1);
-    VAR(IDF_PERSIST, playereffecttone, -1, CTONE_TEAM, CTONE_MAX-1);
-    VAR(IDF_PERSIST, playerlighttone, -1, CTONE_TEAM, CTONE_MAX-1);
+    VAR(IDF_PERSIST, playereffecttone, -1, CTONE_TEAMED, CTONE_MAX-1);
+    VAR(IDF_PERSIST, playerlighttone, -1, CTONE_TEAMED, CTONE_MAX-1);
     VAR(IDF_PERSIST, playerteamtone, -1, CTONE_TEAM, CTONE_MAX-1);
-    FVAR(IDF_PERSIST, playerlightmix, 0, 0.75f, 100);
+    FVAR(IDF_PERSIST, playerlightmix, 0, 0.5f, 100);
     FVAR(IDF_PERSIST, playertonemix, 0, 0.25f, 1);
     FVAR(IDF_PERSIST, playerblend, 0, 1, 1);
-#ifndef MEK
+    FVAR(IDF_PERSIST, playerghost, 0, 0.5f, 1);
+
+    VAR(IDF_PERSIST, playerhint, 0, 3, 3);
+    VAR(IDF_PERSIST, playerhinthurt, 0, 1, 1);
+    VAR(IDF_PERSIST, playerhinthurtthrob, 0, 1, 1);
+    VAR(IDF_PERSIST, playerhinttone, -1, CTONE_TEAMED, CTONE_MAX-1);
+    FVAR(IDF_PERSIST, playerhintblend, 0, 0.1f, 1);
+    FVAR(IDF_PERSIST, playerhintscale, 0, 0.7f, 1); // scale blend depending on health
+    FVAR(IDF_PERSIST, playerhintsize, 0, 1.2f, 2);
+    FVAR(IDF_PERSIST, playerhintmaxsize, 0, 20, FVAR_MAX);
+    FVAR(IDF_PERSIST, playerhintfadeat, 0, 64, FVAR_MAX);
+    FVAR(IDF_PERSIST, playerhintfadecut, 0, 8, FVAR_MAX);
+    FVAR(IDF_PERSIST, playerhinthurtblend, 0, 0.9f, 1);
+    FVAR(IDF_PERSIST, playerhinthurtsize, 0, 1.2f, 2);
+
+    VAR(IDF_PERSIST, footstepsounds, 0, 3, 3); // 0 = off, &1 = focus, &2 = everyone else
+    FVAR(IDF_PERSIST, footstepsoundmin, 0, 0, FVAR_MAX); // minimum velocity magnitude
+    FVAR(IDF_PERSIST, footstepsoundmax, 0, 150, FVAR_MAX); // maximum velocity magnitude
+    FVAR(IDF_PERSIST, footstepsoundlevel, 0, 1, 10); // a way to scale the volume
+    FVAR(IDF_PERSIST, footstepsoundfocus, 0, 0.85f, 10); // focused player version of above
+    FVAR(IDF_PERSIST, footstepsoundlight, 0, 0.5f, 10); // crouch/walk player version of above
+    VAR(IDF_PERSIST, footstepsoundminvol, 0, 64, 255);
+    VAR(IDF_PERSIST, footstepsoundmaxvol, 0, 255, 255);
+    VAR(IDF_PERSIST, footstepsoundminrad, -1, -1, 255);
+    VAR(IDF_PERSIST, footstepsoundmaxrad, -1, -1, 255);
+
+    VAR(IDF_PERSIST, nogore, 0 , 0, 2); // turns off all gore, 0 = off, 1 = replace, 2 = remove
     VAR(IDF_PERSIST, forceplayermodel, -1, -1, PLAYERTYPES-1);
-#endif
-#ifdef VANITY
     VAR(IDF_PERSIST, vanitymodels, 0, 1, 1);
-#endif
     VAR(IDF_PERSIST, headlessmodels, 0, 1, 1);
-    VAR(IDF_PERSIST, autoloadweap, 0, 0, 1); // 0 = off, 1 = auto-set loadout weapons
-    SVAR(IDF_PERSIST, favloadweaps, "");
+    FVAR(IDF_PERSIST, twitchspeed, 0, 8, FVAR_MAX);
+
+    bool wantsloadoutmenu = false;
+    VAR(IDF_PERSIST, showloadoutmenu, 0, 0, 1); // show the loadout menu at the start of a map
 
     bool needname(gameent *d)
     {
-        if(!d || *d->name || client::waiting()) return false;
+        if(!d || *d->name) return false; // || client::waiting()) return false;
         return true;
     }
     ICOMMAND(0, needname, "b", (int *cn), intret(needname(*cn >= 0 ? getclient(*cn) : player1) ? 1 : 0));
 
-    bool needloadout(gameent *d)
-    {
-        if(!d || !m_loadout(gamemode, mutators) || client::waiting()) return false;
-        return player1->state == CS_WAITING && player1->loadweap.empty();
-    }
-    ICOMMAND(0, needloadout, "b", (int *cn), intret(needloadout(*cn >= 0 ? getclient(*cn) : player1) ? 1 : 0));
-
     ICOMMAND(0, gamemode, "", (), intret(gamemode));
     ICOMMAND(0, mutators, "", (), intret(mutators));
 
@@ -254,19 +325,19 @@ namespace game
     ICOMMAND(0, mutsallowed, "ii", (int *g, int *h), intret(*g >= 0 && *g < G_MAX ? gametype[*g].mutators[*h >= 0 && *h < G_M_GSP+1 ? *h : 0] : 0));
     ICOMMAND(0, mutsimplied, "ii", (int *g, int *m), intret(*g >= 0 && *g < G_MAX ? gametype[*g].implied : 0));
     ICOMMAND(0, gspmutname, "ii", (int *g, int *n), result(*g >= 0 && *g < G_MAX && *n >= 0 && *n < G_M_GSN ? gametype[*g].gsp[*n] : ""));
-    ICOMMAND(0, getintermission, "", (), intret(intermission ? 1 : 0));
+    ICOMMAND(0, getintermission, "", (), intret(gs_intermission(gamestate) ? 1 : 0));
+    ICOMMAND(0, getgamestate, "", (), intret(gamestate));
 
     const char *gametitle() { return connected() ? server::gamename(gamemode, mutators) : "ready"; }
     const char *gametext() { return connected() ? mapname : "not connected"; }
 
-#ifdef VANITY
     void vanityreset()
     {
         loopvrev(vanities) vanities.remove(i);
     }
     ICOMMAND(0, resetvanity, "", (), vanityreset());
 
-    int vanityitem(int type, const char *ref, const char *name, const char *tag, int cond, int style, int priv)
+    int vanityitem(int type, const char *ref, const char *name, const char *tag, int cond, int style)
     {
         if(type < 0 || type >= VANITYMAX || !ref || !name || !tag) return -1;
         int num = vanities.length();
@@ -278,10 +349,9 @@ namespace game
         v.tag = newstring(tag);
         v.cond = cond;
         v.style = style;
-        v.priv = priv;
         return num;
     }
-    ICOMMAND(0, addvanity, "isssiii", (int *t, char *r, char *n, char *g, int *c, int *s, int *p), intret(vanityitem(*t, r, n, g, *c, *s, *p)));
+    ICOMMAND(0, addvanity, "isssii", (int *t, char *r, char *n, char *g, int *c, int *s), intret(vanityitem(*t, r, n, g, *c, *s)));
 
     void vanityinfo(int id, int value)
     {
@@ -295,8 +365,7 @@ namespace game
             case 3: result(vanities[id].tag); break;
             case 4: intret(vanities[id].cond); break;
             case 5: intret(vanities[id].style); break;
-            case 6: intret(vanities[id].priv); break;
-            case 7: result(vanities[id].model); break;
+            case 6: result(vanities[id].model); break;
             default: break;
         }
     }
@@ -326,7 +395,7 @@ namespace game
         {
             case 1:
             {
-                const char *id = hud::privname(d->privilege, d->aitype);
+                const char *id = server::privnamex(d->privilege, d->actortype, true);
                 loopv(vanities[n].files)
                     if(vanities[n].files[i].proj == proj && !strcmp(vanities[n].files[i].id, id))
                         file = vanities[n].files[i].name;
@@ -354,11 +423,16 @@ namespace game
         }
         return file;
     }
-#endif
 
-    bool allowspec(gameent *d, int level)
+    bool allowspec(gameent *d, int level, int cn = -1)
     {
+        if(d->o.magnitude() <= 0 || d->o.z < 0) return false;
         if(d->state == CS_SPECTATOR || ((d->state == CS_DEAD || d->state == CS_WAITING) && !d->lastdeath)) return false;
+        if(cn >= 0)
+        {
+            if(cn == player1->clientnum && player1->state != CS_ALIVE && d->clientnum == player1->lastattacker) return true;
+            return d->clientnum == cn; // override
+        }
         switch(level)
         {
             case 0: if(d->state != CS_ALIVE) return false; break;
@@ -370,12 +444,12 @@ namespace game
 
     bool thirdpersonview(bool viewonly, physent *d)
     {
-        if(intermission) return true;
+        if(!gs_playing(gamestate)) return true;
         if(!d) d = focus;
         if(!viewonly && (d->state == CS_DEAD || d->state == CS_WAITING)) return true;
         if(player1->state == CS_EDITING) return false;
         if(player1->state >= CS_SPECTATOR && d == player1) return false;
-        if(d == player1 && inzoom()) return false;
+        if(d == focus && inzoom()) return false;
         if(!(d != player1 ? followthirdperson : thirdperson)) return false;
         return true;
     }
@@ -421,23 +495,9 @@ namespace game
         zooming = on;
     }
 
-    bool zoomallow()
-    {
-        if(W(player1->weapselect, zooms)) switch(zoomlock)
-        {
-            case 4: if(!physics::iscrouching(player1)) break;
-            case 3: if(player1->physstate != PHYS_FLOOR) break;
-            case 2: if(player1->move || player1->strafe) break;
-            case 1: if(physics::sliding(player1, true) || (player1->airmillis && (!zooming || !lastzoom || player1->airtime(lastmillis) >= zoomlocktime || player1->impulse[IM_JUMP]))) break;
-            case 0: default: return true; break;
-        }
-        zoomset(false, 0);
-        return false;
-    }
-
     bool inzoom()
     {
-        if(zoomallow() && lastzoom && (zooming || lastmillis-lastzoom <= zoomtime))
+        if(lastzoom && (zooming || lastmillis-lastzoom <= zoomtime))
             return true;
         return false;
     }
@@ -445,7 +505,7 @@ namespace game
 
     bool inzoomswitch()
     {
-        if(zoomallow() && lastzoom && ((zooming && lastmillis-lastzoom >= zoomtime/2) || (!zooming && lastmillis-lastzoom <= zoomtime/2)))
+        if(lastzoom && ((zooming && lastmillis-lastzoom >= zoomtime/2) || (!zooming && lastmillis-lastzoom <= zoomtime/2)))
             return true;
         return false;
     }
@@ -499,25 +559,105 @@ namespace game
             playsound(S_ERROR, d->o, d, SND_FORCED, -1, -1, -1, &errorchan);
     }
 
+    int announcerchan = -1;
+    struct ancbuf
+    {
+        int idx;
+        physent *t;
+        int *chan;
+
+        ancbuf() : idx(-1), t(NULL), chan(NULL) {}
+        ~ancbuf() {}
+
+        bool play()
+        {
+            if(!issound(*chan))
+            {
+                playsound(idx, t->o, t, t != camera1 ? SND_IMPORT : SND_FORCED, -1, -1, -1, chan);
+                return true;
+            }
+            return false;
+        }
+    };
+    vector<ancbuf> anclist;
+
+    VAR(IDF_PERSIST, announcecollect, 1, 1, 3);
+    VAR(IDF_PERSIST, announcededupe, 0, 2, 3);
+    VAR(IDF_PERSIST, announcebuffer, 1, 64, VAR_MAX);
+
+    void removeannounceall()
+    {
+        loopvrev(anclist) anclist.remove(i);
+        if(issound(announcerchan)) removesound(announcerchan);
+        announcerchan = -1;
+    }
+
+    void removeannounce(gameent *d)
+    {
+        loopvrev(anclist) if(anclist[i].chan == &d->aschan) anclist.remove(i);
+    }
+
+    void checkannounce()
+    {
+        loopv(anclist) if(anclist[i].play()) anclist.remove(i--);
+        while(anclist.length() > announcebuffer) anclist.pop();
+    }
+
     void announce(int idx, gameent *d, bool forced)
     {
-        if(idx >= 0)
+        if(idx < 0) return;
+        physent *t = !d || d == player1 || forced ? camera1 : d;
+        bool ispl = d && !forced, inuse = false;
+        int *chan = ispl ? &d->aschan : &announcerchan;
+        if(issound(*chan))
         {
-            physent *t = !d || d == focus || forced ? camera1 : d;
-            playsound(idx, t->o, t, t != camera1 ? SND_IMPORT : SND_FORCED, -1, -1, -1, d && !forced ? &d->aschan : NULL);
+            if(announcededupe&(ispl ? 2 : 1) && sounds[*chan].slotnum == idx) return; // de-dupe
+            inuse = true;
         }
+        if(announcecollect&(ispl ? 2 : 1))
+        {
+            loopv(anclist) if(anclist[i].chan == chan)
+            {
+                if(announcededupe&(ispl ? 2 : 1) && anclist[i].idx == idx) return; // de-dupe
+                inuse = true;
+            }
+            if(inuse)
+            {
+                ancbuf &a = anclist.add();
+                a.idx = idx;
+                a.t = t;
+                a.chan = chan;
+                return;
+            }
+        }
+        playsound(idx, t->o, t, t != camera1 ? SND_IMPORT : SND_FORCED, -1, -1, -1, chan);
     }
+
     void announcef(int idx, int targ, gameent *d, bool forced, const char *msg, ...)
     {
         if(targ >= 0 && msg && *msg)
         {
-            defvformatstring(text, msg, msg);
+            defvformatbigstring(text, msg, msg);
             conoutft(targ == CON_INFO && d == player1 ? CON_SELF : targ, "%s", text);
         }
         announce(idx, d, forced);
     }
     ICOMMAND(0, announce, "iiisN", (int *idx, int *targ, int *cn, int *forced, char *s, int *numargs), (*numargs >= 5 ? announcef(*numargs >= 1 ? *idx : -1, *numargs >= 2 ? *targ : CON_MESG, *numargs >= 3 ? getclient(*cn) : NULL, *numargs >= 4 ? *forced!=0 : false, "\fw%s", s) : announcef(*numargs >= 1 ? *idx : -1, *numargs >= 2 ? *targ : CON_MESG, *numargs >= 3 ? getclient(*cn) : NULL, *numargs >= 4 ? *forced!=0 : false, NULL)));
 
+    void resetfollow()
+    {
+        follow = spectvfollow = -1;
+        if(specresetstyle && (player1->state == CS_WAITING || player1->state == CS_SPECTATOR))
+        {
+            player1->o = camera1->o;
+            player1->yaw = camera1->yaw;
+            player1->pitch = camera1->pitch;
+            player1->resetinterp();
+        }
+        focus = player1;
+        resetcamera();
+    }
+
     void specreset(gameent *d, bool clear)
     {
         if(d)
@@ -537,16 +677,13 @@ namespace game
                 }
                 if(d == focus) resetfollow();
             }
-            else if(maptime > 0)
+            else if(maptime > 0 && gameent::is(d) && d->actortype < A_ENEMY)
             {
-                if((gameent::is(d)) && d->aitype < AI_START)
-                {
-                    cament *c = cameras.add(new cament);
-                    c->o = d->headpos();
-                    c->type = cament::PLAYER;
-                    c->id = d->clientnum;
-                    c->player = d;
-                }
+                cament *c = cameras.add(new cament);
+                c->o = d->headpos();
+                c->type = cament::PLAYER;
+                c->id = d->clientnum;
+                c->player = d;
             }
         }
         else
@@ -563,7 +700,7 @@ namespace game
     {
         if(!m_edit(gamemode) && (!check || !cameras.empty()))
         {
-            if(intermission && intermmode) return true;
+            if(!gs_playing(gamestate) && intermmode) return true;
             else switch(player1->state)
             {
                 case CS_SPECTATOR: if(specmode || (force && focus != player1 && followmode && followaim())) return true; break;
@@ -579,38 +716,42 @@ namespace game
         if(tvmode(true, true))
         {
             if(!tvmode(true, false)) followmode = 0;
-            else { specmode = 0; resetfollow(); }
+            else specmode = 0;
         }
         else if(focus != player1) followmode = 1;
         else specmode = 1;
+        specreset();
     });
     ICOMMAND(0, waitmodeswitch, "", (), {
         if(tvmode(true, true))
         {
             if(!tvmode(true, false)) followmode = 0;
-            else { waitmode = 0; resetfollow(); }
+            else waitmode = 0;
         }
         else if(focus != player1) followmode = 1;
         else waitmode = 1;
+        specreset();
     });
 
     bool followswitch(int n, bool other)
     {
-        if(!tvmode(true, false) && player1->state >= CS_SPECTATOR)
+        if(player1->state == CS_SPECTATOR || (player1->state == CS_WAITING && (!player1->lastdeath || !deathbuttonmash || lastmillis-player1->lastdeath > deathbuttonmash)))
         {
+            bool istv = tvmode(true, false);
+            int *f = istv ? &spectvfollow : &follow;
             #define checkfollow \
-                if(follow >= players.length()) follow = -1; \
-                else if(follow < -1) follow = players.length()-1;
+                if(*f >= players.length()) *f = -1; \
+                else if(*f < -1) *f = players.length()-1;
             #define addfollow \
             { \
-                follow += clamp(n, -1, 1); \
+                *f += clamp(n, -1, 1); \
                 checkfollow; \
-                if(follow == -1) \
+                if(*f == -1) \
                 { \
-                    if(other) follow += clamp(n, -1, 1); \
+                    if(other) *f += clamp(n, -1, 1); \
                     else \
                     { \
-                        resetfollow(); \
+                        specreset(); \
                         return true; \
                     } \
                     checkfollow; \
@@ -620,11 +761,11 @@ namespace game
             if(!n) n = 1;
             loopi(players.length())
             {
-                if(!players.inrange(follow)) addfollow
+                if(!players.inrange(*f)) addfollow
                 else
                 {
-                    gameent *d = players[follow];
-                    if(!d || d->aitype >= AI_START || !allowspec(d, followdead)) addfollow
+                    gameent *d = players[*f];
+                    if(!d || d->actortype >= A_ENEMY || !allowspec(d, istv ? spectvdead : followdead)) addfollow
                     else
                     {
                         focus = d;
@@ -632,7 +773,7 @@ namespace game
                     }
                 }
             }
-            resetfollow();
+            specreset();
         }
         return false;
     }
@@ -642,7 +783,7 @@ namespace game
     {
         if(gameent::is(d))
         {
-            if((d == player1 && tvmode()) || d->state == CS_DEAD || d->state >= CS_SPECTATOR || intermission)
+            if((d == player1 && tvmode()) || d->state == CS_DEAD || d->state >= CS_SPECTATOR || !gs_playing(gamestate))
                 return false;
         }
         return true;
@@ -650,6 +791,7 @@ namespace game
 
     void respawn(gameent *d)
     {
+        if(d == game::player1 && (maptime <= 0 || needname(d) || wantsloadoutmenu)) return; // prevent spawning
         if(d->state == CS_DEAD && d->respawned < 0 && (!d->lastdeath || lastmillis-d->lastdeath >= 500))
         {
             client::addmsg(N_TRYSPAWN, "ri", d->clientnum);
@@ -665,12 +807,12 @@ namespace game
             entities::spawnplayer(d, ent, true);
             client::addmsg(N_SPAWN, "ri", d->clientnum);
         }
-        d->setscale(rescale(d), 0, true, gamemode, mutators);
+        d->setscale(rescale(d), 0, true);
 
-        if(d == player1) resetfollow();
-        if(d == focus) resetcamera();
+        if(d == player1) specreset();
+        else if(d == focus) resetcamera();
 
-        if(d->aitype < AI_START)
+        if(d->actortype < A_ENEMY)
         {
             playsound(S_RESPAWN, d->o, d);
             if(dynlighteffects)
@@ -679,17 +821,16 @@ namespace game
                 regularshape(PART_SPARK, d->height*2, getcolour(d, playerundertone), 53, 50, 350, d->center(), 1.5f, 1, 1, 0, 35);
                 regularshape(PART_SPARK, d->height*2, getcolour(d, playerovertone), 53, 50, 350, d->center(), 1.5f, 1, 1, 0, 35);
             }
+            if(entities::ents.inrange(ent) && entities::ents[ent]->type == PLAYERSTART) entities::execlink(d, ent, false);
         }
-        if(local && d->aitype <= AI_BOT && entities::ents.inrange(ent) && entities::ents[ent]->type == PLAYERSTART)
-            entities::execlink(d, ent, true);
         ai::respawned(d, local, ent);
     }
 
     vec pulsecolour(physent *d, int i, int cycle)
     {
         size_t seed = size_t(d) + (lastmillis/cycle);
-        int n = detrnd(seed, PULSECOLOURS), n2 = detrnd(seed + 1, PULSECOLOURS);
-        return vec::hexcolor(pulsecols[i][n]).lerp(vec::hexcolor(pulsecols[i][n2]), (lastmillis%cycle)/float(cycle));
+        int n = detrnd(seed, PULSECOLOURS), n2 = detrnd(seed + 1, PULSECOLOURS), q = clamp(i, 0, int(PULSE_LAST));
+        return vec::hexcolor(pulsecols[q][n]).lerp(vec::hexcolor(pulsecols[q][n2]), (lastmillis%cycle)/float(cycle));
     }
 
     int hexpulsecolour(physent *d, int i, int cycle)
@@ -700,41 +841,38 @@ namespace game
 
     vec getpalette(int palette, int index)
     { // colour palette abstractions for textures, etc.
-        switch(palette)
+        if(palette >= 0 && index >= 0) switch(palette)
         {
             case 0: // misc
             {
                 switch(index)
                 {
                     case 0: break; // off
-                    case 1: case 2: case 3:
+                    case 1: case 2: case 3: case 4:
                         return vec::hexcolor(pulsecols[index-1][clamp((lastmillis/100)%PULSECOLOURS, 0, PULSECOLOURS-1)]);
                         break;
-                    case 4: case 5: case 6:
-                        return pulsecolour(camera1, index-4, 50);
-                        break;
                     default: break;
                 }
                 break;
             }
             case 1: // teams
             {
-                int team = index;
-                if(team < 0 || team >= T_MAX+T_TOTAL || (!m_team(gamemode, mutators) && !m_edit(gamemode) && team >= T_FIRST && team <= T_MULTI))
-                    team = T_NEUTRAL; // abstract team coloured levels to neutral
-                else if(team >= T_MAX) team = (team%T_MAX)+T_FIRST; // force team colour palette
+                int team = index%T_ALL;
+                if(!m_edit(gamemode) && index < T_ALL)
+                {
+                    if(!m_team(gamemode, mutators) || team > (m_multi(gamemode, mutators) ? T_MULTI : T_LAST))
+                        team = T_NEUTRAL; // abstract team coloured levels to neutral
+                }
                 return vec::hexcolor(TEAM(team, colour));
                 break;
             }
             case 2: // weapons
             {
-                int weap = index;
-                if(weap < 0 || weap >= W_MAX*2-1) weap = -1;
-                else if(weap >= W_MAX) weap = w_attr(gamemode, mutators, weap%W_MAX, m_weapon(gamemode, mutators));
-                else
+                int weap = index%W_MAX;
+                if(!m_edit(gamemode) && index < W_MAX)
                 {
-                    weap = w_attr(gamemode, mutators, weap, m_weapon(gamemode, mutators));
-                    if(!isweap(weap) || (m_loadout(gamemode, mutators) && weap < W_ITEM) || !m_check(W(weap, modes), W(weap, muts), gamemode, mutators))
+                    weap = w_attr(gamemode, mutators, WEAPON, weap, m_weapon(gamemode, mutators));
+                    if(!isweap(weap) || W(weap, disabled) || (m_loadout(gamemode, mutators) && weap < W_ITEM) || !m_check(W(weap, modes), W(weap, muts), gamemode, mutators))
                         weap = -1;
                 }
                 if(isweap(weap)) return vec::hexcolor(W(weap, colour));
@@ -756,12 +894,6 @@ namespace game
                 if(m_capture(gamemode)) capture::adddynlights();
                 else if(m_defend(gamemode)) defend::adddynlights();
                 else if(m_bomber(gamemode)) bomber::adddynlights();
-                else if(m_gauntlet(gamemode))
-                {
-                    int numents = entities::lastenttype[CHECKPOINT];
-                    loopi(numents) if(entities::ents[i]->type == CHECKPOINT && (entities::ents[i]->attrs[6] == CP_LAST || entities::ents[i]->attrs[6] == CP_FINISH))
-                        adddynlight(entities::ents[i]->o, float(max(entities::ents[i]->attrs[0], enttype[entities::ents[i]->type].radius)), vec::hexcolor(TEAM(T_ALPHA, colour)), 0, 0, DL_KEEP);
-                }
             }
             gameent *d = NULL;
             int numdyns = numdynents();
@@ -774,24 +906,18 @@ namespace game
                          reloading = last && d->weapstate[d->weapselect] == W_S_RELOAD,
                          secondary = physics::secondaryweap(d);
                     float amt = last ? clamp(float(lastmillis-d->weaplast[d->weapselect])/d->weapwait[d->weapselect], 0.f, 1.f) : 0.f;
-                    vec col = WPCOL(d, d->weapselect, partcol, secondary);
+                    vec col = WPCOL(d, d->weapselect, lightcol, secondary);
                     if(d->weapselect == W_FLAMER && (!reloading || amt > 0.5f) && !physics::liquidcheck(d))
                     {
                         float scale = powering ? 1.f+(amt*1.5f) : (d->weapstate[d->weapselect] == W_S_IDLE ? 1.f : (reloading ? (amt-0.5f)*2 : amt));
                         adddynlight(d->ejectpos(d->weapselect), 16*scale, col, 0, 0, DL_KEEP);
                     }
-                    if(d->weapselect == W_SWORD || powering)
+                    if((W(d->weapselect, lightpersist) || powering) && W(d->weapselect, lightradius) > 0)
                     {
-                        static float powerdl[W_MAX] = {
-                            0, 16, 18, 20, 18, 20, 24, 18, 18, 18, 18
-                        };
-                        if(powerdl[d->weapselect] > 0)
-                        {
-                            float thresh = max(amt, 0.25f), size = 4+powerdl[d->weapselect]*thresh;
-                            int span = max(W2(d->weapselect, power, physics::secondaryweap(d))/4, 500), interval = lastmillis%span, part = span/2;
-                            if(interval) size += size*0.5f*(interval <= part ? interval/float(part) : (span-interval)/float(part));
-                            adddynlight(d->muzzlepos(d->weapselect), size, vec(col).mul(thresh), 0, 0, DL_KEEP);
-                        }
+                        float thresh = max(amt, 0.25f), size = W(d->weapselect, lightradius)*thresh;
+                        int span = max(W2(d->weapselect, cooktime, physics::secondaryweap(d))/4, 500), interval = lastmillis%span, part = span/2;
+                        if(interval) size += size*0.5f*(interval <= part ? interval/float(part) : (span-interval)/float(part));
+                        adddynlight(d->muzzlepos(d->weapselect), size, vec(col).mul(thresh), 0, 0, DL_KEEP);
                     }
                 }
                 if(burntime && d->burning(lastmillis, burntime))
@@ -822,7 +948,7 @@ namespace game
                     }
                     adddynlight(d->center(), d->height*intensity*pc, rescolour(d, PULSE_SHOCK).mul(pc), 0, 0, DL_KEEP);
                 }
-                if(d->aitype < AI_START && illumlevel > 0 && illumradius > 0)
+                if(d->actortype < A_ENEMY && illumlevel > 0 && illumradius > 0)
                 {
                     vec col = vec::hexcolor(getcolour(d, playereffecttone)).mul(illumlevel);
                     adddynlight(d->center(), illumradius, col, 0, 0, DL_KEEP);
@@ -833,47 +959,21 @@ namespace game
 
     void boosteffect(gameent *d, const vec &pos, int num, int len, bool shape = false)
     {
-        float scale = 0.6f+(rnd(40)/100.f);
-        part_create(PART_HINT, shape ? len/2 : 1, pos, 0x1818A8, scale*1.5f, min(0.75f*scale, 0.95f), 0, 0);
-        part_create(PART_FIREBALL, shape ? len/2 : 1, pos, pulsecols[PULSE_FIRE][rnd(PULSECOLOURS)], scale*1.25f, min(0.75f*scale, 0.95f), 0, 0);
+        float scale = 0.4f+(rnd(40)/100.f);
+        part_create(PART_HINT, shape ? len/2 : len/10, pos, getcolour(d, playereffecttone), scale*1.5f, scale*0.75f, 0, 0);
+        part_create(PART_FIREBALL, shape ? len/2 : len/10, pos, pulsecols[PULSE_FIRE][rnd(PULSECOLOURS)], scale*1.25f, scale*0.75f, 0, 0);
         if(shape) loopi(num) regularshape(PART_FIREBALL, int(d->radius)*2, pulsecols[PULSE_FIRE][rnd(PULSECOLOURS)], 21, 1, len, pos, scale*1.25f, 0.75f, -5, 0, 10);
     }
 
     void impulseeffect(gameent *d, int effect)
     {
-        if(!gameent::is(d)) return;
         int num = int((effect ? 5 : 20)*impulsescale);
         switch(effect)
         {
             case 0: playsound(S_IMPULSE, d->o, d); // fail through
-            case 1:
-            {
-                if(num > 0 && impulsefade > 0) loopi(2) boosteffect(d, d->jet[i], num, impulsefade, effect==0);
-                break;
-            }
-            case 2:
-            {
-                if(issound(d->jschan))
-                {
-                    sounds[d->jschan].vol = min(lastmillis-sounds[d->jschan].millis, 255);
-                    sounds[d->jschan].ends = lastmillis+250;
-                }
-                else playsound(S_JET, d->o, d, SND_LOOP, 1, -1, -1, &d->jschan, lastmillis+250);
-                if(num > 0 && impulsefade > 0) boosteffect(d, d->jet[2], num, impulsefade);
-            }
-        }
-    }
-
-    gameent *pointatplayer()
-    {
-        vec pos = focus->headpos();
-        loopv(players) if(players[i])
-        {
-            gameent *o = players[i];
-            float dist;
-            if(intersect(o, pos, worldpos, dist)) return o;
+            case 1: if(num > 0 && impulsefade > 0) loopi(3) boosteffect(d, d->jet[i], num, impulsefade, effect==0);
+                    break;
         }
-        return NULL;
     }
 
     void setmode(int nmode, int nmuts) { modecheck(nextmode = nmode, nextmuts = nmuts); }
@@ -881,7 +981,7 @@ namespace game
 
     float spawnfade(gameent *d)
     {
-        int len = d->aitype >= AI_START ? (aistyle[d->aitype].living ? min(ai::aideadfade, enemyspawntime ? enemyspawntime : INT_MAX-1) : 500) : m_delay(gamemode, mutators);
+        int len = d->actortype >= A_ENEMY ? (actor[d->actortype].living ? min(ai::aideadfade, enemyspawntime) : 500) : m_delay(gamemode, mutators, d->team);
         if(len > 0)
         {
             int interval = min(len/3, ragdolleffect), over = max(len-interval, 1), millis = lastmillis-d->lastdeath;
@@ -893,18 +993,18 @@ namespace game
 
     float rescale(gameent *d)
     {
-        float total = actorscale;
-        if(d->aitype > AI_NONE)
+        float total = d->actortype != A_PLAYER ? (d->actortype >= A_ENEMY ? enemyscale : botscale) : PLAYER(d->model, scale);
+        if(d->actortype >= A_ENEMY)
         {
-            bool hasent = d->aitype >= AI_START && entities::ents.inrange(d->aientity) && entities::ents[d->aientity]->type == ACTOR;
-            if(hasent && entities::ents[d->aientity]->attrs[9] > 0) total *= (entities::ents[d->aientity]->attrs[9]/100.f)*enemyscale;
-            else total *= aistyle[clamp(d->aitype, int(AI_NONE), int(AI_MAX-1))].scale*(d->aitype >= AI_START ? enemyscale : botscale);
+            bool hasent = entities::ents.inrange(d->spawnpoint) && entities::ents[d->spawnpoint]->type == ACTOR;
+            if(hasent && entities::ents[d->spawnpoint]->attrs[9] > 0) total *= (entities::ents[d->spawnpoint]->attrs[9]/100.f);
+            else total *= actor[clamp(d->actortype, int(A_PLAYER), int(A_MAX-1))].scale;
         }
         if(d->state != CS_SPECTATOR && d->state != CS_EDITING)
         {
-            if(m_resize(gamemode, mutators) || d->aitype >= AI_START)
+            if(m_resize(gamemode, mutators))
             {
-                float minscale = 1, amtscale = m_insta(gamemode, mutators) ? 1+(d->spree*instaresizeamt) : max(d->health, 1)/float(d->aitype >= AI_START ? aistyle[d->aitype].health*enemystrength : m_health(gamemode, mutators, d->model));
+                float minscale = 1, amtscale = m_insta(gamemode, mutators) ? 1+(d->spree*instaresizeamt) : max(d->health, 1)/float(d->actortype >= A_ENEMY ? actor[d->actortype].health*enemystrength : m_health(gamemode, mutators, d->model));
                 if(m_resize(gamemode, mutators))
                 {
                     minscale = minresizescale;
@@ -917,21 +1017,17 @@ namespace game
         return total;
     }
 
-    float opacity(gameent *d, bool third = true)
+    float opacity(gameent *d, bool third)
     {
         float total = d == focus ? (third ? (d != player1 ? followblend : thirdpersonblend) : firstpersonblend) : playerblend;
-        if(physics::isghost(d, focus)) total *= 0.5f;
+        if(physics::isghost(d, focus)) total *= playerghost;
         if(deathfade && (d->state == CS_DEAD || d->state == CS_WAITING)) total *= spawnfade(d);
         else if(d->state == CS_ALIVE)
         {
-            if(d == focus)
-            {
-                if(third) total *= camera1->o.dist(d->o)/(d != player1 ? followdist : thirdpersondist);
-                else if(d->weapselect == W_MELEE) return 0; // hack
-            }
+            if(d == focus && third) total *= camera1->o.dist(d->o)/(d != player1 ? followdist : thirdpersondist);
             int prot = m_protect(gamemode, mutators), millis = d->protect(lastmillis, prot); // protect returns time left
             if(millis > 0) total *= 1.f-(float(millis)/float(prot));
-            if(d == player1 && inzoom())
+            if(d == focus && inzoom())
             {
                 int frame = lastmillis-lastzoom;
                 float pc = frame <= zoomtime ? (frame)/float(zoomtime) : 1.f;
@@ -941,22 +1037,47 @@ namespace game
         return total;
     }
 
+    void footstep(gameent *d, int curfoot)
+    {
+        bool moving = d->move || d->strafe, liquid = physics::liquidcheck(d), onfloor = d->physstate >= PHYS_SLOPE || d->onladder || d->turnside;
+        if(curfoot < 0 || (moving && (liquid || onfloor)))
+        {
+            float mag = d->vel.magnitude(), m = min(footstepsoundmax, footstepsoundmin), n = max(footstepsoundmax, footstepsoundmin);
+            if(n > m && mag > m)
+            {
+                if(curfoot < 0) curfoot = d->lastfoot;
+                vec pos = d->footpos(curfoot);
+                float amt = clamp(mag/n, 0.f, 1.f)*(d != focus ? footstepsoundlevel : footstepsoundfocus);
+                if(onfloor && (!d->running(moveslow) || d->crouching())) amt *= footstepsoundlight;
+                int vol = clamp(int(amt*footstepsoundmaxvol), footstepsoundminvol, footstepsoundmaxvol);
+                playsound(liquid && (!onfloor || rnd(4)) ? S_SWIMSTEP : S_FOOTSTEP, pos, NULL, d != focus ? 0 : SND_NOCULL, vol, footstepsoundmaxrad, footstepsoundminrad, &d->sschan[curfoot]);
+            }
+        }
+    }
+
+    bool canregenimpulse(gameent *d)
+    {
+        if(d->state == CS_ALIVE && impulseregen > 0 && (!impulseregendelay || lastmillis-d->impulse[IM_REGEN] >= impulseregendelay))
+            return true;
+        return false;
+    }
+
     void checkoften(gameent *d, bool local)
     {
         adjustscaled(d->quake, quakefade);
 
-        d->setscale(rescale(d), curtime, false, gamemode, mutators);
+        d->setscale(rescale(d), curtime, false);
         d->speedscale = d->curscale;
-        if(d->aitype > AI_NONE)
+        if(d->actortype > A_PLAYER)
         {
-            bool hasent = d->aitype >= AI_START && entities::ents.inrange(d->aientity) && entities::ents[d->aientity]->type == ACTOR;
-            if(hasent && entities::ents[d->aientity]->attrs[8] > 0) d->speedscale *= entities::ents[d->aientity]->attrs[8]*enemyspeed;
-            else d->speedscale *= d->aitype >= AI_START ? enemyspeed : botspeed;
+            bool hasent = d->actortype >= A_ENEMY && entities::ents.inrange(d->spawnpoint) && entities::ents[d->spawnpoint]->type == ACTOR;
+            if(hasent && entities::ents[d->spawnpoint]->attrs[8] > 0) d->speedscale *= entities::ents[d->spawnpoint]->attrs[8]*enemyspeed;
+            else d->speedscale *= d->actortype >= A_ENEMY ? enemyspeed : botspeed;
         }
 
         float offset = d->height;
         d->o.z -= d->height;
-        if(d->state == CS_ALIVE && aistyle[clamp(d->aitype, int(AI_NONE), int(AI_MAX-1))].cancrouch)
+        if(d->state == CS_ALIVE && actor[clamp(d->actortype, int(A_PLAYER), int(A_MAX-1))].cancrouch)
         {
             bool crouching = d->action[AC_CROUCH];
             float crouchoff = 1.f-CROUCHHEIGHT, zrad = d->zradius-(d->zradius*crouchoff);
@@ -967,15 +1088,15 @@ namespace game
                 {
                     vec dir;
                     vecfromyawpitch(d->yaw, 0, d->move, d->strafe, dir);
-                    d->o.add(dir.normalize().mul(2));
+                    d->o.add(dir.mul(2));
                 }
                 d->o.z += d->zradius;
                 d->height = d->zradius;
-                if(!collide(d, vec(0, 0, 1), 0, false) || inside)
+                if(collide(d, vec(0, 0, 1), 0, false) || collideinside)
                 {
                     d->o.z -= d->zradius-zrad;
                     d->height = zrad;
-                    if(collide(d, vec(0, 0, 1), 0, false) && !inside) crouching = true;
+                    if(!collide(d, vec(0, 0, 1), 0, false) && !collideinside) crouching = true;
                 }
                 d->o = old;
                 d->height = offset;
@@ -991,7 +1112,7 @@ namespace game
                     break;
                 }
             }
-            if(physics::iscrouching(d))
+            if(d->crouching())
             {
                 int crouchtime = abs(d->actiontime[AC_CROUCH]);
                 float amt = lastmillis-crouchtime <= PHYSMILLIS ? clamp(float(lastmillis-crouchtime)/PHYSMILLIS, 0.f, 1.f) : 1.f;
@@ -1006,57 +1127,93 @@ namespace game
 
         d->checktags();
 
-        loopi(W_MAX) if(d->weapstate[i] != W_S_IDLE)
+        if(m_impulsemeter(gamemode, mutators) && canregenimpulse(d) && d->impulse[IM_METER] > 0)
         {
-            bool timeexpired = lastmillis-d->weaplast[i] >= d->weapwait[i]+(d->weapselect != i || d->weapstate[i] != W_S_POWER ? 0 : PHYSMILLIS);
-            if(d->state == CS_ALIVE && i == d->weapselect && d->weapstate[i] == W_S_RELOAD && timeexpired)
+            bool onfloor = d->physstate >= PHYS_SLOPE || d->onladder || physics::liquidcheck(d),
+                 collect = true; // collect time until we are able to act upon it
+            int timeslice = int((curtime+d->impulse[IM_COLLECT])*impulseregen);
+            #define impulsemod(x,y) \
+                if(collect && (x)) \
+                { \
+                    if(y > 0) { if(timeslice > 0) timeslice = int(timeslice*y); } \
+                    else collect = false; \
+                }
+            impulsemod(d->running(), impulseregenrun);
+            impulsemod(d->move || d->strafe, impulseregenmove);
+            impulsemod((!onfloor && PHYS(gravity) > 0) || d->sliding(), impulseregeninair);
+            impulsemod(onfloor && d->crouching() && !d->sliding(), impulseregencrouch);
+            impulsemod(d->sliding(), impulseregenslide);
+            if(collect)
             {
-                if(timeexpired && playreloadnotify&(d == focus ? 1 : 2) && (d->ammo[i] >= W(i, max) || playreloadnotify&(d == focus ? 4 : 8)))
-                    playsound(WSND(i, S_W_NOTIFY), d->o, d, 0, reloadnotifyvol, -1, -1, &d->wschan);
+                if(timeslice > 0)
+                {
+                    if((d->impulse[IM_METER] -= timeslice) < 0) d->impulse[IM_METER] = 0;
+                    d->impulse[IM_COLLECT] = 0;
+                }
+                else d->impulse[IM_COLLECT] += curtime;
             }
-            if(d->state != CS_ALIVE || timeexpired)
-                d->setweapstate(i, W_S_IDLE, 0, lastmillis);
         }
-        if(d->state == CS_ALIVE && isweap(d->weapselect) && d->weapstate[d->weapselect] == W_S_POWER)
+
+        loopi(W_MAX) if(d->weapstate[i] != W_S_IDLE && (!gs_playing(gamestate) || d->weapselect != i || d->weapstate[i] != W_S_ZOOM))
+        {
+            bool timeexpired = lastmillis-d->weaplast[i] >= d->weapwait[i]+(d->weapselect != i || d->weapstate[i] != W_S_POWER ? 0 : PHYSMILLIS);
+            if(gs_playing(gamestate) && d->state == CS_ALIVE && i == d->weapselect && d->weapstate[i] == W_S_RELOAD && timeexpired && playreloadnotify&(d == focus ? 1 : 2) && (d->ammo[i] >= W(i, ammomax) || playreloadnotify&(d == focus ? 4 : 8)))
+                playsound(WSND(i, S_W_NOTIFY), d->o, d, 0, reloadnotifyvol, -1, -1, &d->wschan);
+            if(!gs_playing(gamestate) || d->state != CS_ALIVE || timeexpired) d->setweapstate(i, W_S_IDLE, 0, lastmillis);
+        }
+        if(gs_playing(gamestate) && d->state == CS_ALIVE && isweap(d->weapselect) && (d->weapstate[d->weapselect] == W_S_POWER || d->weapstate[d->weapselect] == W_S_ZOOM))
         {
             int millis = lastmillis-d->weaplast[d->weapselect];
-            if(millis > 0)
+            if(millis >= 0 && millis <= d->weapwait[d->weapselect])
             {
                 bool secondary = physics::secondaryweap(d);
                 float amt = millis/float(d->weapwait[d->weapselect]);
-                int vol = 255;
-                if(W2(d->weapselect, power, secondary)) switch(W2(d->weapselect, cooked, secondary))
+                int vol = 255, snd = d->weapstate[d->weapselect] == W_S_POWER ? WSND2(d->weapselect, secondary, S_W_POWER) : WSND(d->weapselect, S_W_ZOOM);
+                if(W2(d->weapselect, cooktime, secondary)) switch(W2(d->weapselect, cooked, secondary))
                 {
                     case 4: case 5: vol = 10+int(245*(1.f-amt)); break; // longer
                     case 1: case 2: case 3: default: vol = 10+int(245*amt); break; // shorter
                 }
                 if(issound(d->pschan)) sounds[d->pschan].vol = vol;
-                else playsound(WSND2(d->weapselect, secondary, S_W_POWER), d->o, d, SND_LOOP, vol, -1, -1, &d->pschan);
+                else playsound(snd, d->o, d, SND_LOOP, vol, -1, -1, &d->pschan);
+            }
+            else if(d->pschan >= 0)
+            {
+                if(issound(d->pschan)) removesound(d->pschan);
+                d->pschan = -1;
             }
         }
-        else if(issound(d->pschan)) removesound(d->pschan);
+        else if(d->pschan >= 0)
+        {
+            if(issound(d->pschan)) removesound(d->pschan);
+            d->pschan = -1;
+        }
         if(local)
         {
             if(d->respawned > 0 && lastmillis-d->respawned >= 2500) d->respawned = -1;
             if(d->suicided > 0 && lastmillis-d->suicided >= 2500) d->suicided = -1;
         }
-        if(d->lastres[WR_BURN] > 0 && lastmillis-d->lastres[WR_BURN] >= burntime-500)
+        int restime[WR_MAX] = { burntime, bleedtime, shocktime };
+        loopi(WR_MAX) if(d->lastres[i] > 0 && lastmillis-d->lastres[i] >= restime[i]) d->resetresidual(i);
+        if(gs_playing(gamestate) && d->state == CS_ALIVE)
         {
-            if(lastmillis-d->lastres[WR_BURN] >= burntime) d->resetburning();
-            else if(issound(d->fschan)) sounds[d->fschan].vol = int((d != focus ? 128 : 224)*(1.f-(lastmillis-d->lastres[WR_BURN]-(burntime-500))/500.f));
-        }
-        else if(issound(d->fschan)) removesound(d->fschan);
-        if(d->lastres[WR_BLEED] > 0 && lastmillis-d->lastres[WR_BLEED] >= bleedtime) d->resetbleeding();
-        if(d->lastres[WR_SHOCK] > 0 && lastmillis-d->lastres[WR_SHOCK] >= shocktime) d->resetshocking();
-        if(issound(d->jschan))
-        {
-            if(physics::jetpack(d))
+            int curfoot = d->curfoot();
+            bool hassound = footstepsounds&(d != focus ? 2 : 1);
+            if(!hassound) loopi(2) if(d->sschan[i] >= 0)
             {
-                sounds[d->jschan].vol = min(lastmillis-sounds[d->jschan].millis, 255);
-                sounds[d->jschan].ends = lastmillis+250;
+                if(issound(d->sschan[i])) removesound(d->sschan[i]);
+                d->sschan[i] = -1;
             }
-            else if(sounds[d->jschan].ends < lastmillis) removesound(d->jschan);
-            else sounds[d->jschan].vol = int(ceilf(255*(float(sounds[d->jschan].ends-lastmillis)/250.f)));
+            if(curfoot != d->lastfoot)
+            {
+                if(hassound)
+                {
+                    if(d->lastfoot >= 0 && issound(d->sschan[d->lastfoot])) sounds[d->sschan[d->lastfoot]].pos = d->footpos(d->lastfoot);
+                    footstep(d, curfoot);
+                    d->lastfoot = curfoot;
+                }
+            }
+            else if(hassound) loopi(2) if(issound(d->sschan[i])) sounds[d->sschan[i]].pos = d->footpos(i);
         }
         loopv(d->icons) if(lastmillis-d->icons[i].millis > d->icons[i].fade) d->icons.remove(i--);
     }
@@ -1068,10 +1225,10 @@ namespace game
         {
             gameent *d = players[i];
             const int lagtime = totalmillis-d->lastupdate;
-            if(d->ai || !lagtime || intermission) continue;
+            if(d->ai || !lagtime || !gs_playing(gamestate)) continue;
             //else if(lagtime > 1000) continue;
             physics::smoothplayer(d, 1, false);
-            if(!intermission && (d->state == CS_DEAD || d->state == CS_WAITING)) entities::checkitems(d);
+            if(gs_playing(gamestate) && (d->state == CS_DEAD || d->state == CS_WAITING)) entities::checkitems(d);
         }
     }
 
@@ -1080,7 +1237,6 @@ namespace game
         if(wr_burns(weap, flags))
         {
             d->lastrestime[WR_BURN] = lastmillis;
-            if(!issound(d->fschan)) playsound(S_BURNING, d->o, d, SND_LOOP, -1, -1, -1, &d->fschan);
             if(isweap(weap)) d->lastres[WR_BURN] = lastmillis;
             else return true;
         }
@@ -1113,22 +1269,22 @@ namespace game
     {
         enum { BURN = 1<<0, BLEED = 1<<1, SHOCK = 1<<2 };
 
-        gameent *d, *actor;
+        gameent *d, *v;
         int weap, damage, flags, millis;
 
         damagemerge() { millis = totalmillis; }
-        damagemerge(gameent *d, gameent *actor, int weap, int damage, int flags) : d(d), actor(actor), weap(weap), damage(damage), flags(flags) { millis = totalmillis; }
+        damagemerge(gameent *d, gameent *v, int weap, int damage, int flags) : d(d), v(v), weap(weap), damage(damage), flags(flags) { millis = totalmillis; }
 
         bool merge(const damagemerge &m)
         {
-            if(actor != m.actor || flags != m.flags) return false;
+            if(d != m.d || v != m.v || flags != m.flags) return false;
             damage += m.damage;
             return true;
         }
 
         void play()
         {
-            if(playdamagetones >= (actor == focus ? 1 : (d == focus ? 2 : 3)))
+            if(playdamagetones >= (v == focus ? 1 : (d == focus ? 2 : 3)))
             {
                 const int dmgsnd[8] = { 0, 10, 25, 50, 75, 100, 150, 200 };
                 int snd = -1;
@@ -1136,25 +1292,30 @@ namespace game
                 else if(flags&BLEED) snd = S_BLEED;
                 else if(flags&SHOCK) snd = S_SHOCK;
                 else loopirev(8) if(damage >= dmgsnd[i]) { snd = S_DAMAGE+i; break; }
-                if(snd >= 0) playsound(snd, d->o, d, actor == focus ? SND_FORCED : SND_DIRECT, damagetonevol);
+                if(snd >= 0) playsound(snd, d->o, d, SND_IMPORT|(v == focus ? SND_NODIST : SND_CLAMPED), damagetonevol);
             }
             if(aboveheaddamage)
             {
-                defformatstring(ds)("<sub>\fr+%d", damage);
+                defformatstring(ds)("<sub>\fo%c%d", damage > 0 ? '-' : (damage < 0 ? '+' : '~'), damage < 0 ? 0-damage : damage);
                 part_textcopy(d->abovehead(), ds, d != focus ? PART_TEXT : PART_TEXT_ONTOP, eventiconfade, 0xFFFFFF, 4, 1, -10, 0, d);
             }
         }
     };
     vector<damagemerge> damagemerges;
 
+    void removedamagemergeall()
+    {
+        loopvrev(damagemerges) damagemerges.remove(i);
+    }
+
     void removedamagemerges(gameent *d)
     {
-        loopvrev(damagemerges) if(damagemerges[i].d == d || damagemerges[i].actor == d) damagemerges.removeunordered(i);
+        loopvrev(damagemerges) if(damagemerges[i].d == d || damagemerges[i].v == d) damagemerges.remove(i);
     }
 
-    void pushdamagemerge(gameent *d, gameent *actor, int weap, int damage, int flags)
+    void pushdamagemerge(gameent *d, gameent *v, int weap, int damage, int flags)
     {
-        damagemerge dt(d, actor, weap, damage, flags);
+        damagemerge dt(d, v, weap, damage, flags);
         loopv(damagemerges) if(damagemerges[i].merge(dt)) return;
         damagemerges.add(dt);
     }
@@ -1176,252 +1337,186 @@ namespace game
     }
 
     static int alarmchan = -1;
-    void hiteffect(int weap, int flags, int damage, gameent *d, gameent *actor, vec &dir, bool local)
+    void hiteffect(int weap, int flags, int damage, gameent *d, gameent *v, vec &dir, vec &vel, float dist, bool local)
     {
         bool burning = burn(d, weap, flags), bleeding = bleed(d, weap, flags), shocking = shock(d, weap, flags);
         if(!local || burning || bleeding || shocking)
         {
-            if(hithurts(flags))
+            float scale = isweap(weap) && WF(WK(flags), weap, damage, WS(flags)) ? abs(damage)/float(WF(WK(flags), weap, damage, WS(flags))) : 1.f;
+            if(hithurts(flags) && damage > 0)
             {
-                if(d == focus) hud::damage(damage, actor->o, actor, weap, flags);
-                if(aistyle[d->aitype].living)
+                if(d == focus) hud::damage(damage, v->o, v, weap, flags);
+                if(actor[d->actortype].living)
                 {
                     vec p = d->headpos(-d->height/4);
-                    if(bloodscale > 0)
+                    if(!nogore && bloodscale > 0)
                         part_splash(PART_BLOOD, int(clamp(damage/2, 2, 10)*bloodscale)*(bleeding ? 2 : 1), bloodfade, p, 0x229999, (rnd(bloodsize/2)+(bloodsize/2))/10.f, 1, 100, DECAL_BLOOD, int(d->radius), 10);
-                    if(bloodscale <= 0 || bloodsparks)
+                    if(nogore != 2 && (bloodscale <= 0 || bloodsparks))
                         part_splash(PART_PLASMA, int(clamp(damage/2, 2, 10))*(bleeding ? 2: 1), bloodfade, p, 0x882222, 1, 0.5f, 50, DECAL_STAIN, int(d->radius));
                 }
-                if(d != actor)
+                if(d != v)
                 {
-                    bool sameteam = m_team(gamemode, mutators) && d->team == actor->team;
-                    if(!sameteam) pushdamagemerge(d, actor, weap, damage, (burning ? damagemerge::BURN : 0)|(bleeding ? damagemerge::BLEED : 0)|(shocking ? damagemerge::SHOCK : 0));
-                    else if(actor == player1 && !burning && !bleeding && !shocking)
+                    bool sameteam = m_team(gamemode, mutators) && d->team == v->team;
+                    if(!sameteam) pushdamagemerge(d, v, weap, damage, (burning ? damagemerge::BURN : 0)|(bleeding ? damagemerge::BLEED : 0)|(shocking ? damagemerge::SHOCK : 0));
+                    else if(v == player1 && !burning && !bleeding && !shocking)
                     {
                         player1->lastteamhit = d->lastteamhit = lastmillis;
-                        if(!issound(alarmchan)) playsound(S_ALARM, actor->o, actor, 0, -1, -1, -1, &alarmchan);
+                        if(!issound(alarmchan)) playsound(S_ALARM, v->o, v, 0, -1, -1, -1, &alarmchan);
                     }
-                    if(!burning && !bleeding && !shocking && !sameteam) actor->lasthit = totalmillis;
+                    if(!burning && !bleeding && !shocking && !sameteam) v->lasthit = totalmillis;
                 }
-                if(d->aitype < AI_START && !issound(d->vschan)) playsound(S_PAIN, d->o, d, 0, -1, -1, -1, &d->vschan);
+                if(d->actortype < A_ENEMY && !issound(d->vschan)) playsound(S_PAIN, d->o, d, 0, -1, -1, -1, &d->vschan);
                 d->lastpain = lastmillis;
+                if(!WK(flags)) playsound(WSND2(weap, WS(flags), S_W_IMPACT), vec(d->center()).add(vec(dir).mul(dist)), NULL, 0, clamp(int(255*scale), 64, 255));
             }
-            if(d->aitype < AI_START || aistyle[d->aitype].canmove)
+            if(actor[d->actortype].canmove)
             {
-                if(weap == -1 && shocking)
+                if(weap == -1 && shocking && shockstun)
                 {
-                    float amt = WRS(d->health <= 0 ? deadstunscale : hitstunscale, stun, gamemode, mutators),
+                    float amt = WRS(flags&HIT_WAVE || !hithurts(flags) ? wavestunscale : (d->health <= 0 ? deadstunscale : hitstunscale), stun, gamemode, mutators),
                           s = G(shockstunscale)*amt, g = G(shockstunfall)*amt;
-                    if(s > 0 || g > 0) d->addstun(weap, lastmillis, G(shockstuntime), s, g);
-                    if(s > 0) d->vel.mul(1.f-clamp(s, 0.f, 1.f));
-                    if(g > 0) d->falling.mul(1.f-clamp(g, 0.f, 1.f));
+                    d->addstun(weap, lastmillis, G(shockstuntime), shockstun&W_N_STADD ? s : 0.f, shockstun&W_N_GRADD ? g : 0.f);
+                    if(shockstun&W_N_STIMM && s > 0) d->vel.mul(1.f-clamp(s, 0.f, 1.f));
+                    if(shockstun&W_N_GRIMM && g > 0) d->falling.mul(1.f-clamp(g, 0.f, 1.f));
                 }
-                else if(isweap(weap) && !burning && !bleeding && !shocking && WF(WK(flags), weap, damage, WS(flags)) != 0)
+                else if(isweap(weap) && !burning && !bleeding && !shocking && WF(WK(flags), weap, damage, WS(flags)))
                 {
-                    float scale = damage/float(WF(WK(flags), weap, damage, WS(flags)));
-                    if(WF(WK(flags), weap, stuntime, WS(flags)))
+                    if(WF(WK(flags), weap, stun, WS(flags)))
                     {
+                        int stun = WF(WK(flags), weap, stun, WS(flags));
                         float amt = scale*WRS(flags&HIT_WAVE || !hithurts(flags) ? wavestunscale : (d->health <= 0 ? deadstunscale : hitstunscale), stun, gamemode, mutators),
                               s = WF(WK(flags), weap, stunscale, WS(flags))*amt, g = WF(WK(flags), weap, stunfall, WS(flags))*amt;
-                        if(s > 0 || g > 0) d->addstun(weap, lastmillis, int(scale*WF(WK(flags), weap, stuntime, WS(flags))), s, g);
-                        if(s > 0) d->vel.mul(1.f-clamp(s, 0.f, 1.f));
-                        if(g > 0) d->falling.mul(1.f-clamp(g, 0.f, 1.f));
+                        d->addstun(weap, lastmillis, int(scale*WF(WK(flags), weap, stuntime, WS(flags))), stun&W_N_STADD ? s : 0.f, stun&W_N_GRADD ? g : 0.f);
+                        if(stun&W_N_STIMM && s > 0) d->vel.mul(1.f-clamp(s, 0.f, 1.f));
+                        if(stun&W_N_GRIMM && g > 0) d->falling.mul(1.f-clamp(g, 0.f, 1.f));
                     }
-                    if(WF(WK(flags), weap, hitpush, WS(flags)) != 0)
+                    if(WF(WK(flags), weap, hitpush, WS(flags)) != 0 || WF(WK(flags), weap, hitvel, WS(flags)) != 0)
                     {
                         float amt = scale*WRS(flags&HIT_WAVE || !hithurts(flags) ? wavepushscale : (d->health <= 0 ? deadpushscale : hitpushscale), push, gamemode, mutators);
-                        if(d == actor)
+                        bool doquake = hithurts(flags) && damage > 0;
+                        if(d == v)
                         {
-                            float modify = WF(WK(flags), weap, selfdamage, WS(flags))*G(selfdamagescale);
+                            float modify = WF(WK(flags), weap, damageself, WS(flags))*G(damageselfscale);
                             if(modify != 0) amt *= 1/modify;
+                            else doquake = false;
                         }
-                        else if(m_team(gamemode, mutators) && d->team == actor->team)
+                        else if(m_team(gamemode, mutators) && d->team == v->team)
                         {
-                            float modify = WF(WK(flags), weap, teamdamage, WS(flags))*G(teamdamagescale);
+                            float modify = WF(WK(flags), weap, damageteam, WS(flags))*G(damageteamscale);
                             if(modify != 0) amt *= 1/modify;
+                            else doquake = false;
                         }
                         float hit = WF(WK(flags), weap, hitpush, WS(flags))*amt;
-                        if(hit > 0)
+                        if(hit != 0)
                         {
                             d->vel.add(vec(dir).mul(hit));
-                            d->quake = min(d->quake+max(int(hit), 1), quakelimit);
+                            if(doquake) d->quake = min(d->quake+max(int(hit), 1), quakelimit);
                         }
+                        hit = WF(WK(flags), weap, hitvel, WS(flags))*amt;
+                        if(hit != 0) d->vel.add(vec(vel).mul(hit));
                     }
                 }
             }
-            ai::damaged(d, actor, weap, flags, damage);
+            ai::damaged(d, v, weap, flags, damage);
         }
     }
 
-    void damaged(int weap, int flags, int damage, int health, int armour, gameent *d, gameent *actor, int millis, vec &dir)
+    void damaged(int weap, int flags, int damage, int health, gameent *d, gameent *v, int millis, vec &dir, vec &vel, float dist)
     {
-        if(d->state != CS_ALIVE || intermission) return;
+        if(d->state != CS_ALIVE || !gs_playing(gamestate)) return;
         if(hithurts(flags))
         {
             d->health = health;
-            d->armour = armour;
-            if(d->health <= m_health(gamemode, mutators, d->model)) d->lastregen = 0;
-            d->lastpain = lastmillis;
-            actor->totaldamage += damage;
+            if(damage > 0)
+            {
+                d->lastregen = 0;
+                d->lastpain = lastmillis;
+                v->totaldamage += damage;
+            }
         }
-        hiteffect(weap, flags, damage, d, actor, dir, actor == player1 || actor->ai);
+        hiteffect(weap, flags, damage, d, v, dir, vel, dist, v == player1 || v->ai);
     }
 
-    void killed(int weap, int flags, int damage, gameent *d, gameent *actor, vector<gameent *> &log, int style, int material)
+    void killed(int weap, int flags, int damage, gameent *d, gameent *v, vector<gameent *> &log, int style, int material)
     {
-        if(d->type != ENT_PLAYER && d->type != ENT_AI) return;
         d->lastregen = 0;
         d->lastpain = lastmillis;
         d->state = CS_DEAD;
-        if(style&FRAG_OBLITERATE) d->obliterated = true;
-        if(style&FRAG_HEADSHOT) d->headless = true;
-        bool burning = burn(d, weap, flags), bleeding = bleed(d, weap, flags), shocking = shock(d, weap, flags), isfocus = d == focus || actor == focus,
-             isme = d == player1 || actor == player1, allowanc = obitannounce && (obitannounce >= 2 || isfocus) && (m_fight(gamemode) || isme) && actor->aitype < AI_START;
-        int anc = d == focus && allowanc ? S_V_FRAGGED : -1, dth = d->aitype >= AI_START || d->obliterated ? S_SPLOSH : S_DEATH, curmat = material&MATF_VOLUME;
+        d->obliterated = (style&FRAG_OBLITERATE)!=0;
+        d->headless = (style&FRAG_HEADSHOT)!=0;
+        bool burning = burn(d, weap, flags), bleeding = bleed(d, weap, flags), shocking = shock(d, weap, flags),
+             isfocus = d == focus || v == focus, isme = d == player1 || v == player1,
+             allowanc = obitannounce && (obitannounce >= 2 || isfocus) && (m_fight(gamemode) || isme) && v->actortype < A_ENEMY;
+        int anc = d == focus && allowanc ? S_V_FRAGGED : -1, dth = d->actortype >= A_ENEMY || d->obliterated ? S_SPLOSH : S_DEATH,
+            curmat = material&MATF_VOLUME;
         if(d != player1) d->resetinterp();
-        if(!isme) { loopv(log) if(log[i] == player1) { isme = true; break; } }
+        if(!isme) loopv(log) if(log[i] == player1)
+        {
+            isme = true;
+            break;
+        }
         formatstring(d->obit)("%s ", colourname(d));
-        if(d != actor && actor->lastattacker == d->clientnum) actor->lastattacker = -1;
-        d->lastattacker = actor->clientnum;
-        if(d == actor)
-        {
-            if(!aistyle[d->aitype].living) concatstring(d->obit, "was destroyed");
-            else if(!obitverbose) concatstring(d->obit, "died");
-            else if(flags&HIT_SPAWN) concatstring(d->obit, obitverbose != 2 ? "couldn't respawn" : "tried to spawn inside solid matter");
-            else if(flags&HIT_SPEC) concatstring(d->obit, obitverbose != 2 ? "entered spectator" : "gave up their corporeal form");
-            else if(flags&HIT_MATERIAL && curmat&MAT_WATER) concatstring(d->obit, getobitwater(material, "drowned"));
-            else if(flags&HIT_MATERIAL && curmat&MAT_LAVA) concatstring(d->obit, getobitlava(material, "melted into a ball of fire"));
-            else if(flags&HIT_MATERIAL) concatstring(d->obit, *obitdeath ? obitdeath : "met their end");
-            else if(flags&HIT_LOST) concatstring(d->obit, *obitfall ? obitfall : "fell to their death");
-            else if(flags && isweap(weap) && !burning && !bleeding && !shocking)
-            {
-                static const char *suicidenames[W_MAX][2] = {
-                    { "hit themself", "hit themself" },
-                    { "ate a bullet", "shot themself" },
-                    { "created too much torsional stress", "cut themself" },
-                    { "tested the effectiveness of their own shrapnel", "shot themself" },
-                    { "fell victim to their own crossfire", "shot themself" },
-                    { "spontaneously combusted", "burned themself" },
-                    { "was caught up in their own plasma-filled mayhem", "plasmified themself" },
-                    { "got a good shock", "shocked themself" },
-                    { "kicked it, kamikaze style", "blew themself up" },
-                    { "kicked it, kamikaze style", "blew themself up" },
-                    { "exploded with style", "exploded themself" },
-                };
-                concatstring(d->obit, suicidenames[weap][obitverbose == 2 ? 0 : 1]);
-            }
-            else if(flags&HIT_BURN || burning) concatstring(d->obit, "burned up");
-            else if(flags&HIT_BLEED || bleeding) concatstring(d->obit, "bled out");
-            else if(flags&HIT_SHOCK || shocking) concatstring(d->obit, "twitched to death");
-            else if(d->obliterated) concatstring(d->obit, "was obliterated");
-            else if(d->headless) concatstring(d->obit, "had their head caved in");
-            else concatstring(d->obit, "suicided");
+        if(d != v && v->lastattacker == d->clientnum) v->lastattacker = -1;
+        d->lastattacker = v->clientnum;
+        if(d == v)
+        {
+            if(!actor[d->actortype].living) concatstring(d->obit, obitdestroyed);
+            else if(!obitverbose) concatstring(d->obit, obitdied);
+            else if(flags&HIT_SPAWN) concatstring(d->obit, obitspawn);
+            else if(flags&HIT_SPEC) concatstring(d->obit, obitspectator);
+            else if(flags&HIT_MATERIAL && curmat&MAT_WATER) concatstring(d->obit, getobitwater(material, obitdrowned));
+            else if(flags&HIT_MATERIAL && curmat&MAT_LAVA) concatstring(d->obit, getobitlava(material, obitmelted));
+            else if(flags&HIT_MATERIAL) concatstring(d->obit, *obitdeath ? obitdeath : obitdeathmat);
+            else if(flags&HIT_LOST) concatstring(d->obit, *obitfall ? obitfall : obitlost);
+            else if(flags && isweap(weap) && !burning && !bleeding && !shocking) concatstring(d->obit, WF(WK(flags), weap, obitsuicide, WS(flags)));
+            else if(flags&HIT_BURN || burning) concatstring(d->obit, obitburnself);
+            else if(flags&HIT_BLEED || bleeding) concatstring(d->obit, obitbleedself);
+            else if(flags&HIT_SHOCK || shocking) concatstring(d->obit, obitshockself);
+            else if(d->obliterated) concatstring(d->obit, obitobliterated);
+            else if(d->headless) concatstring(d->obit, obitheadless);
+            else concatstring(d->obit, obitsuicide);
         }
         else
         {
             concatstring(d->obit, "was ");
-            if(!aistyle[d->aitype].living) concatstring(d->obit, "destroyed by");
-            else if(!obitverbose) concatstring(d->obit, "fragged by");
-            else if(burning) concatstring(d->obit, "set ablaze by");
-            else if(bleeding) concatstring(d->obit, "fatally wounded by");
-            else if(shocking) concatstring(d->obit, "given a terminal dose of shock therapy by");
+            if(!actor[d->actortype].living) concatstring(d->obit, obitdestroyed);
+            else if(!obitverbose) concatstring(d->obit, obitfragged);
+            else if(burning) concatstring(d->obit, obitburn);
+            else if(bleeding) concatstring(d->obit, obitbleed);
+            else if(shocking) concatstring(d->obit, obitshock);
             else if(isweap(weap))
             {
-                static const char *obitnames[5][W_MAX][2] = {
-                    {
-                        { "punched by", "punched by" },
-                        { "pierced by", "pierced by" },
-                        { "impaled by", "impaled by" },
-                        { "sprayed with buckshot by", "shot by" },
-                        { "riddled with holes by", "shot by" },
-                        { "char-grilled by", "burned by" },
-                        { "plasmified by", "plasmified by" },
-                        { "laser shocked by", "shocked by" },
-                        { "blown to pieces by", "blown up by" },
-                        { "blown to pieces by", "blown up by" },
-                        { "exploded by", "exploded by" },
-                    },
-                    {
-                        { "kicked by", "kicked by" },
-                        { "pierced by", "pierced by" },
-                        { "impaled by", "impaled by" },
-                        { "filled with lead by", "shot by" },
-                        { "spliced apart by", "shot by" },
-                        { "fireballed by", "burned by" },
-                        { "shown the light by", "melted by" },
-                        { "given laser burn by", "lasered by" },
-                        { "blown to pieces by", "blown up by" },
-                        { "blown to pieces by", "blown up by" },
-                        { "exploded by", "exploded by" },
-                    },
-                    {
-                        { "given kung-fu lessons by", "kung-fu'd by" },
-                        { "capped by", "capped by" },
-                        { "sliced in half by", "sliced by" },
-                        { "scrambled by", "scrambled by" },
-                        { "air conditioned courtesy of", "aerated by" },
-                        { "char-grilled by", "grilled by" },
-                        { "plasmafied by", "plasmified by" },
-                        { "expertly sniped by", "sniped by" },
-                        { "blown to pieces by", "blown up by" },
-                        { "blown to pieces by", "blown up by" },
-                        { "exploded by", "exploded by" },
-                    },
-                    {
-                        { "given kung-fu lessons by", "kung-fu'd by" },
-                        { "skewered by", "skewered by" },
-                        { "sliced in half by", "sliced by" },
-                        { "turned into little chunks by", "scrambled by" },
-                        { "swiss-cheesed by", "aerated by" },
-                        { "barbequed by", "grilled by" },
-                        { "reduced to ooze by", "plasmified by" },
-                        { "given laser shock treatment by", "shocked by" },
-                        { "turned into shrapnel by", "gibbed by" },
-                        { "turned into shrapnel by", "gibbed by" },
-                        { "obliterated by", "obliterated by" },
-                    },
-                    {
-                        { "given kung-fu lessons by", "kung-fu'd by" },
-                        { "picked to pieces by", "skewered by" },
-                        { "melted in half by", "sliced by" },
-                        { "filled with shrapnel by", "flak-filled by" },
-                        { "air-conditioned by", "aerated by" },
-                        { "cooked alive by", "cooked by" },
-                        { "melted alive by", "plasmified by" },
-                        { "electrified by", "electrified by" },
-                        { "turned into shrapnel by", "gibbed by" },
-                        { "turned into shrapnel by", "gibbed by" },
-                        { "obliterated by", "obliterated by" },
-                    }
-                };
-                concatstring(d->obit, obitnames[WK(flags) ? 4 : (d->obliterated ? 3 : (d->headless ? 2 : (WS(flags) ? 1 : 0)))][weap][obitverbose == 2 ? 0 : 1]);
+                if(d->obliterated) concatstring(d->obit, WF(WK(flags), weap, obitobliterated, WS(flags)));
+                else if(d->headless) concatstring(d->obit, WF(WK(flags), weap, obitheadless, WS(flags)));
+                else concatstring(d->obit, WF(WK(flags), weap, obituary, WS(flags)));
             }
-            else concatstring(d->obit, "killed by");
+            else concatstring(d->obit, obitkilled);
+            concatstring(d->obit, " by");
             bool override = false;
             if(d->headless)
             {
-                actor->addicon(eventicon::HEADSHOT, lastmillis, eventiconfade, 0);
+                v->addicon(eventicon::HEADSHOT, lastmillis, eventiconfade, 0);
                 if(!override && allowanc) anc = S_V_HEADSHOT;
             }
-            if(!m_fight(gamemode) || actor->aitype >= AI_START)
+            if(!m_fight(gamemode) || v->actortype >= A_ENEMY)
             {
-                concatstring(d->obit, actor->aitype >= AI_START ? " a " : " ");
-                concatstring(d->obit, colourname(actor));
+                concatstring(d->obit, v->actortype >= A_ENEMY ? " a " : " ");
+                concatstring(d->obit, colourname(v));
             }
-            else if(m_team(gamemode, mutators) && d->team == actor->team)
+            else if(m_team(gamemode, mutators) && d->team == v->team)
             {
                 concatstring(d->obit, " \fs\fzawteam-mate\fS ");
-                concatstring(d->obit, colourname(actor));
-                if(actor == focus) { anc = S_ALARM; override = true; }
+                concatstring(d->obit, colourname(v));
+                if(v == focus) { anc = S_ALARM; override = true; }
             }
             else if(obitstyles)
             {
                 if(style&FRAG_REVENGE)
                 {
                     concatstring(d->obit, " \fs\fzoyvengeful\fS");
-                    actor->addicon(eventicon::REVENGE, lastmillis, eventiconfade); // revenge
-                    actor->dominating.removeobj(d);
-                    d->dominated.removeobj(actor);
+                    v->addicon(eventicon::REVENGE, lastmillis, eventiconfade); // revenge
+                    v->dominating.removeobj(d);
+                    d->dominated.removeobj(v);
                     if(allowanc)
                     {
                         anc = S_V_REVENGE;
@@ -1431,9 +1526,9 @@ namespace game
                 else if(style&FRAG_DOMINATE)
                 {
                     concatstring(d->obit, " \fs\fzoydominating\fS");
-                    actor->addicon(eventicon::DOMINATE, lastmillis, eventiconfade); // dominating
-                    if(actor->dominated.find(d) < 0) actor->dominated.add(d);
-                    if(d->dominating.find(actor) < 0) d->dominating.add(actor);
+                    v->addicon(eventicon::DOMINATE, lastmillis, eventiconfade); // dominating
+                    if(v->dominated.find(d) < 0) v->dominated.add(d);
+                    if(d->dominating.find(v) < 0) d->dominating.add(v);
                     if(allowanc)
                     {
                         anc = S_V_DOMINATE;
@@ -1441,12 +1536,12 @@ namespace game
                     }
                 }
                 concatstring(d->obit, " ");
-                concatstring(d->obit, colourname(actor));
+                concatstring(d->obit, colourname(v));
 
                 if(style&FRAG_BREAKER)
                 {
                     concatstring(d->obit, " \fs\fzPwspree-breaking\fS");
-                    actor->addicon(eventicon::BREAKER, lastmillis, eventiconfade);
+                    v->addicon(eventicon::BREAKER, lastmillis, eventiconfade);
                     if(!override && allowanc) anc = S_V_BREAKER;
                 }
 
@@ -1454,36 +1549,36 @@ namespace game
                 {
                     if(style&FRAG_BREAKER) concatstring(d->obit, " and");
                     concatstring(d->obit, " \fs\fzcwdouble-killing\fS");
-                    actor->addicon(eventicon::MULTIKILL, lastmillis, eventiconfade, 0);
+                    v->addicon(eventicon::MULTIKILL, lastmillis, eventiconfade, 0);
                     if(!override && allowanc) anc = S_V_MULTI;
                 }
                 else if(style&FRAG_MKILL2)
                 {
                     if(style&FRAG_BREAKER) concatstring(d->obit, " and");
                     concatstring(d->obit, " \fs\fzcwtriple-killing\fS");
-                    actor->addicon(eventicon::MULTIKILL, lastmillis, eventiconfade, 1);
+                    v->addicon(eventicon::MULTIKILL, lastmillis, eventiconfade, 1);
                     if(!override && allowanc) anc = S_V_MULTI2;
                 }
                 else if(style&FRAG_MKILL3)
                 {
                     if(style&FRAG_BREAKER) concatstring(d->obit, " and");
                     concatstring(d->obit, " \fs\fzcwmulti-killing\fS");
-                    actor->addicon(eventicon::MULTIKILL, lastmillis, eventiconfade, 2);
+                    v->addicon(eventicon::MULTIKILL, lastmillis, eventiconfade, 2);
                     if(!override && allowanc) anc = S_V_MULTI3;
                 }
             }
             else
             {
                 concatstring(d->obit, " ");
-                concatstring(d->obit, colourname(actor));
+                concatstring(d->obit, colourname(v));
             }
             if(obitstyles)
             {
                 if(style&FRAG_FIRSTBLOOD)
                 {
                     concatstring(d->obit, " for \fs\fzrwfirst blood\fS");
-                    actor->addicon(eventicon::FIRSTBLOOD, lastmillis, eventiconfade, 0);
-                    if(!override && allowanc)
+                    v->addicon(eventicon::FIRSTBLOOD, lastmillis, eventiconfade, 0);
+                    if(allowanc)
                     {
                         anc = S_V_FIRSTBLOOD;
                         override = true;
@@ -1493,7 +1588,7 @@ namespace game
                 if(style&FRAG_SPREE1)
                 {
                     concatstring(d->obit, " in total \fs\fzYwcarnage\fS");
-                    actor->addicon(eventicon::SPREE, lastmillis, eventiconfade, 0);
+                    v->addicon(eventicon::SPREE, lastmillis, eventiconfade, 0);
                     if(!override && allowanc)
                     {
                         anc = S_V_SPREE;
@@ -1503,7 +1598,7 @@ namespace game
                 else if(style&FRAG_SPREE2)
                 {
                     concatstring(d->obit, " on a \fs\fzYwslaughter\fS");
-                    actor->addicon(eventicon::SPREE, lastmillis, eventiconfade, 1);
+                    v->addicon(eventicon::SPREE, lastmillis, eventiconfade, 1);
                     if(!override && allowanc)
                     {
                         anc = S_V_SPREE2;
@@ -1513,7 +1608,7 @@ namespace game
                 else if(style&FRAG_SPREE3)
                 {
                     concatstring(d->obit, " on a \fs\fzYwmassacre\fS");
-                    actor->addicon(eventicon::SPREE, lastmillis, eventiconfade, 2);
+                    v->addicon(eventicon::SPREE, lastmillis, eventiconfade, 2);
                     if(!override && allowanc)
                     {
                         anc = S_V_SPREE3;
@@ -1523,7 +1618,7 @@ namespace game
                 else if(style&FRAG_SPREE4)
                 {
                     concatstring(d->obit, " in a \fs\fzYPbloodbath\fS");
-                    actor->addicon(eventicon::SPREE, lastmillis, eventiconfade, 3);
+                    v->addicon(eventicon::SPREE, lastmillis, eventiconfade, 3);
                     if(!override && allowanc)
                     {
                         anc = S_V_SPREE4;
@@ -1532,53 +1627,69 @@ namespace game
                 }
             }
         }
+        if(d != v)
+        {
+            if(showobitdists >= (d != player1 ? 2 : 1))
+            {
+                defformatstring(obitx)(" \fs\fo@\fy%.2f\fom\fS", v->o.dist(d->o)/8.f);
+                concatstring(d->obit, obitx);
+            }
+            if(showobithpleft >= (d != player1 ? 2 : 1))
+            {
+                defformatstring(obitx)(" (\fs\fc%d\fS)", v->health);
+                concatstring(d->obit, obitx);
+            }
+        }
         if(!log.empty())
         {
-            if(obitverbose == 2 || obitstyles) concatstring(d->obit, rnd(2) ? ", assisted by" : ", helped by");
+            if(obitverbose || obitstyles) concatstring(d->obit, rnd(2) ? ", assisted by" : ", helped by");
             else concatstring(d->obit, " +");
             loopv(log) if(log[i])
             {
-                if(obitverbose == 2 || obitstyles)
+                if(obitverbose || obitstyles)
                     concatstring(d->obit, log.length() > 1 && i == log.length()-1 ? " and " : (i ? ", " : " "));
                 else concatstring(d->obit, log.length() > 1 && i == log.length()-1 ? " + " : (i ? " + " : " "));
-                if(log[i]->aitype >= AI_START) concatstring(d->obit, "a ");
+                if(log[i]->actortype >= A_ENEMY) concatstring(d->obit, "a ");
                 concatstring(d->obit, colourname(log[i]));
+                if(showobithpleft >= (d != player1 ? 2 : 1))
+                {
+                    defformatstring(obitx)(" (\fs\fc%d\fS)", log[i]->health);
+                    concatstring(d->obit, obitx);
+                }
             }
         }
-        if(d != actor)
+        if(d != v)
         {
-            if(actor->state == CS_ALIVE && d->aitype < AI_START)
+            if(v->state == CS_ALIVE && d->actortype < A_ENEMY)
             {
-                copystring(actor->obit, d->obit);
-                actor->lastkill = totalmillis;
+                copystring(v->obit, d->obit);
+                v->lastkill = totalmillis;
             }
         }
         if(dth >= 0) playsound(dth, d->o, d, 0, -1, -1, -1, &d->vschan);
-        if(d->aitype < AI_START)
+        if(d->actortype < A_ENEMY)
         {
             if(showobituaries)
             {
                 bool show = false;
-                if(flags&HIT_LOST) show = true;
-                else switch(showobituaries)
+                switch(showobituaries)
                 {
                     case 1: if(isme || m_duke(gamemode, mutators)) show = true; break;
                     case 2: if(isme || anc >= 0 || m_duke(gamemode, mutators)) show = true; break;
-                    case 3: if(isme || d->aitype == AI_NONE || anc >= 0 || m_duke(gamemode, mutators)) show = true; break;
-                    case 4: if(isme || d->aitype == AI_NONE || actor->aitype == AI_NONE || anc >= 0 || m_duke(gamemode, mutators)) show = true; break;
+                    case 3: if(isme || d->actortype == A_PLAYER || anc >= 0 || m_duke(gamemode, mutators)) show = true; break;
+                    case 4: if(isme || d->actortype == A_PLAYER || v->actortype == A_PLAYER || anc >= 0 || m_duke(gamemode, mutators)) show = true; break;
                     case 5: default: show = true; break;
                 }
                 int target = show ? (isme ? CON_SELF : CON_INFO) : -1;
-                if(showobitdists && d != actor) announcef(anc, target, d, false, "\fw%s \fs[\fo@\fy%.2f\fom\fS]", d->obit, actor->o.dist(d->o)/8.f);
-                else announcef(anc, target, d, false, "\fw%s", d->obit);
+                announcef(anc, target, d, false, "\fw%s", d->obit);
             }
             else if(anc >= 0) announce(anc, d);
-            if(anc >= 0 && d != actor) announce(anc, actor);
+            if(anc >= 0 && d != v) announce(anc, v);
         }
         vec pos = d->wantshitbox() ? d->head : d->headpos();
         pos.z -= d->zradius*0.125f;
-#ifdef VANITY
-        if(vanitymodels && d->headless && headlessmodels && *d->vanity)
+
+        if(vanitymodels && d->headless && !nogore && headlessmodels && *d->vanity)
         {
             if(d->vitems.empty()) vanitybuild(d);
             int found[VANITYMAX] = {0};
@@ -1586,37 +1697,43 @@ namespace game
             {
                 if(found[vanities[d->vitems[k]].type]) continue;
                 if(!(vanities[d->vitems[k]].cond&2)) continue;
-                if(!client::haspriv(d, vanities[d->vitems[k]].priv)) continue;
                 projs::create(pos, pos, true, d, PRJ_VANITY, (rnd(gibfade)+gibfade)*2, 0, 0, rnd(50)+10, -1, d->vitems[k], 0, 0);
                 found[vanities[d->vitems[k]].type]++;
             }
         }
-#endif
-        if(aistyle[d->aitype].living && gibscale > 0)
+
+        if(actor[d->actortype].living && nogore != 2 && gibscale > 0 && !(flags&HIT_LOST))
         {
             int gib = clamp(max(damage, 10)/10, 1, 20), amt = int((rnd(gib)+gib)*gibscale);
             if(d->obliterated) amt *= 2;
-            loopi(amt) projs::create(pos, pos, true, d, PRJ_GIBS, rnd(gibfade)+gibfade, 0, rnd(500)+1, rnd(50)+10);
+            loopi(amt) projs::create(pos, pos, true, d, nogore ? PRJ_DEBRIS : PRJ_GIBS, rnd(gibfade)+gibfade, 0, rnd(500)+1, rnd(50)+10);
         }
-        if(m_team(gamemode, mutators) && d->team == actor->team && d != actor && actor == player1 && isweap(weap) && WF(WK(flags), weap, teampenalty, WS(flags)))
+        if(m_team(gamemode, mutators) && d->team == v->team && d != v && v == player1 && isweap(weap) && WF(WK(flags), weap, damagepenalty, WS(flags)))
         {
             hud::teamkills.add(totalmillis);
             if(hud::numteamkills() >= teamkillwarn) hud::lastteam = totalmillis;
         }
-        if(m_bomber(gamemode)) bomber::killed(d, actor);
-        ai::killed(d, actor);
+        if(m_bomber(gamemode)) bomber::killed(d, v);
+        ai::killed(d, v);
     }
 
-    void timeupdate(int timeremain)
+    void timeupdate(int state, int remain)
     {
-        timeremaining = timeremain;
-        if(!timeremain && !intermission)
+        int oldstate = gamestate;
+        gamestate = state;
+        timeremaining = remain;
+        lasttimeremain = lastmillis;
+        if(gs_intermission(gamestate) && gs_playing(oldstate))
         {
             player1->stopmoving(true);
-            hud::showscores(true, true);
-            intermission = true;
+            if(gamestate == G_S_INTERMISSION) hud::showscores(true, true);
             smartmusic(true, false);
         }
+        if(gamestate == G_S_VOTING && oldstate != G_S_VOTING)
+        {
+            hud::showscores(false);
+            showgui("maps", 1);
+        }
     }
 
     gameent *newclient(int cn)
@@ -1661,21 +1778,34 @@ namespace game
         if(!players.inrange(cn)) return;
         gameent *d = players[cn];
         if(!d) return;
-        if(d->name[0] && client::showpresence >= (client::waiting(false) ? 2 : 1) && (d->aitype == AI_NONE || ai::showaiinfo))
-            conoutft(CON_EVENT, "\fo%s (%s) left the game (%s)", colourname(d), d->hostname, reason >= 0 ? disc_reasons[reason] : "normal");
+        if(d->name[0] && client::showpresence >= (client::waiting(false) ? 2 : 1))
+        {
+            if(d->actortype == A_PLAYER)
+            {
+                int amt = client::otherclients(); // not including self to disclude this player
+                conoutft(CON_EVENT, "\fo%s (%s) left the game (%s) (%d %s)", colourname(d), d->hostname, reason >= 0 ? disc_reasons[reason] : "normal", amt, amt != 1 ? "players" : "player");
+            }
+            else if(d->actortype == A_BOT && ai::showaiinfo)
+                conoutft(CON_EVENT, "\fo%s was removed from the game (%s)", colourname(d), reason >= 0 ? disc_reasons[reason] : "normal");
+        }
         gameent *e = NULL;
         int numdyns = numdynents();
         loopi(numdyns) if((e = (gameent *)iterdynents(i)))
         {
             e->dominating.removeobj(d);
             e->dominated.removeobj(d);
+            if(e->ai) loopvj(e->ai->state)
+            {
+                if(e->ai->state[j].owner == cn) e->ai->state[j].owner = -1;
+                if(e->ai->state[j].targtype == ai::AI_T_ACTOR && e->ai->state[j].target == cn) e->ai->state.remove(j--);
+            }
         }
         specreset(d, true);
-        client::unignore(d->clientnum);
         waiting.removeobj(d);
         client::clearvotes(d);
         projs::remove(d);
         removedamagemerges(d);
+        removeannounce(d);
         if(m_capture(gamemode)) capture::removeplayer(d);
         else if(m_defend(gamemode)) defend::removeplayer(d);
         else if(m_bomber(gamemode)) bomber::removeplayer(d);
@@ -1700,91 +1830,28 @@ namespace game
         if(!empty) smartmusic(true, false);
     }
 
-    int lookupweap(const char *a)
-    {
-        if(isnumeric(*a)) return parseint(a);
-        else loopi(W_MAX) if(!strcasecmp(W(i, name), a)) return i;
-        return -1;
-    }
-
-    void chooseloadweap(gameent *d, const char *list, bool saved = false, bool respawn = false, bool echo = false)
-    {
-        if(m_loadout(gamemode, mutators))
-        {
-            if(saved && d != player1) saved = false;
-            vector<int> items;
-            if(list && *list)
-            {
-                vector<char *> chunk;
-                explodelist(list, chunk);
-                loopv(chunk)
-                {
-                    if(!chunk[i] || !*chunk[i] || !isnumeric(*chunk[i])) continue;
-                    int v = parseint(chunk[i]);
-                    items.add(v >= W_OFFSET && v < W_ITEM ? v : 0);
-                }
-                chunk.deletearrays();
-            }
-            int r = max(maxcarry, items.length());
-            while(d->loadweap.length() < r) d->loadweap.add(0);
-            loopi(r)
-            {
-                int n = d->loadweap.find(items[i]);
-                d->loadweap[i] = n < 0 || n >= i ? items[i] : 0;
-            }
-            client::addmsg(N_LOADW, "ri3v", d->clientnum, respawn ? 1 : 0, d->loadweap.length(), d->loadweap.length(), d->loadweap.getbuf());
-            vector<char> value, msg;
-            loopi(r)
-            {
-                if(saved)
-                {
-                    if(!value.empty()) value.add(' ');
-                    value.add(char(d->loadweap[i]+48));
-                }
-                int colour = W(d->loadweap[i] ? d->loadweap[i] : W_MELEE, colour);
-                const char *pre = msg.empty() ? "" : (i == r-1 ? ", and " : ", "),
-                           *tex = d->loadweap[i] ? hud::itemtex(WEAPON, d->loadweap[i]) : hud::questiontex,
-                           *name = d->loadweap[i] ? W(d->loadweap[i], name) : "random";
-                defformatstring(weap)("%s\fs\f[%d]\f(%s)%s\fS", pre, colour, tex, name);
-                msg.put(weap, strlen(weap));
-            }
-            if(!value.empty()) setsvar("favloadweaps", value.getbuf(), true);
-            if(d == player1 && !msg.empty() && echo) conoutft(CON_SELF, "weapon selection is now: %s", msg.getbuf());
-        }
-        else conoutft(CON_MESG, "\foweapon selection is not currently available");
-    }
-    ICOMMAND(0, loadweap, "sii", (char *s, int *n, int *r), chooseloadweap(player1, s, *n!=0, *r!=0, true));
-    ICOMMAND(0, getloadweap, "i", (int *n), intret(player1->loadweap.inrange(*n) ? player1->loadweap[*n] : -1));
-    ICOMMAND(0, allowedweap, "i", (int *n), intret(isweap(*n) && m_check(W(*n, modes), W(*n, muts), gamemode, mutators) ? 1 : 0));
-    ICOMMAND(0, hasloadweap, "bb", (int *g, int *m), intret(m_loadout(m_game(*g) ? *g : gamemode, *m >= 0 ? *m : mutators) ? 1 : 0));
-
     void startmap(const char *name, const char *reqname, bool empty)    // called just after a map load
     {
         ai::startmap(name, reqname, empty);
-        intermission = false;
-        maptime = hud::lastnewgame = 0;
+        gamestate = m_fight(gamemode) ? G_S_WAITING : G_S_PLAYING;
+        maptime = 0;
+        removedamagemergeall();
+        removeannounceall();
         projs::reset();
         physics::reset();
         resetworld();
         resetcursor();
-        if(*name)
-        {
-            conoutft(CON_MESG, "\fs%s\fS by \fs%s\fS \fs[\fa%s\fS]", *maptitle ? maptitle : "Untitled", *mapauthor ? mapauthor : "Unknown", server::gamename(gamemode, mutators));
-            preload();
-        }
+        resetsway();
+        if(!empty) preload();
         // reset perma-state
         gameent *d;
         int numdyns = numdynents();
-        loopi(numdyns) if((d = (gameent *)iterdynents(i)) && (gameent::is(d)))
-            d->mapchange(lastmillis, m_health(gamemode, mutators, d->model), m_armour(gamemode, mutators, d->model));
-        if(!client::demoplayback && m_loadout(gamemode, mutators) && autoloadweap && *favloadweaps)
-            chooseloadweap(player1, favloadweaps);
-        entities::spawnplayer(player1, -1, false); // prevent the player from being in the middle of nowhere
-        specreset();
-        resetsway();
+        loopi(numdyns) if((d = (gameent *)iterdynents(i)) && gameent::is(d)) d->mapchange(lastmillis, gamemode, mutators);
+        entities::spawnplayer(player1); // prevent the player from being in the middle of nowhere
         resetcamera();
-        if(!empty) client::sendgameinfo = client::sendcrcinfo = client::sendplayerinfo = true;
+        if(!empty) client::sendgameinfo = client::sendcrcinfo = true;
         copystring(clientmap, reqname ? reqname : (name ? name : ""));
+        if(showloadoutmenu) wantsloadoutmenu = true;
     }
 
     gameent *intersectclosest(vec &from, vec &to, gameent *at)
@@ -1841,8 +1908,22 @@ namespace game
     {
         if(tone)
         {
-            int col = d->aitype < AI_START ? d->colour : 0;
-            if(!col && isweap(d->weapselect)) col = W(d->weapselect, colour);
+            int col = d->actortype >= A_ENEMY ? 0x060606 : d->colour;
+            if(!col && isweap(d->weapselect))
+            {
+                col = W(d->weapselect, colour);
+                int lastweap = d->getlastweap(m_weapon(gamemode, mutators));
+                if(isweap(lastweap) && d->weapselect != lastweap && (d->weapstate[d->weapselect] == W_S_USE || d->weapstate[d->weapselect] == W_S_SWITCH))
+                {
+                    float amt = (lastmillis-d->weaplast[d->weapselect])/float(d->weapwait[d->weapselect]);
+                    int r2 = (col>>16), g2 = ((col>>8)&0xFF), b2 = (col&0xFF),
+                        c = W(lastweap, colour), r1 = (c>>16), g1 = ((c>>8)&0xFF), b1 = (c&0xFF),
+                        r3 = clamp(int((r1*(1-amt))+(r2*amt)), 0, 255),
+                        g3 = clamp(int((g1*(1-amt))+(g2*amt)), 0, 255),
+                        b3 = clamp(int((b1*(1-amt))+(b2*amt)), 0, 255);
+                    col = (r3<<16)|(g3<<8)|b3;
+                }
+            }
             if(col)
             {
                 if(mix)
@@ -1875,24 +1956,33 @@ namespace game
         }
     }
 
-    const char *colourname(gameent *d, char *name, bool icon, bool dupname)
+    const char *colourname(gameent *d, char *name, bool icon, bool dupname, int colour)
     {
         if(!name) name = d->name;
         static string colored; colored[0] = 0; string colortmp;
-        concatstring(colored, "\fs");
+        if(colour) concatstring(colored, "\fs");
         if(icon)
         {
-            formatstring(colortmp)("\f[%d]\f($priv%stex)", findcolour(d), hud::privname(d->privilege, d->aitype));
+            if(colour&1)
+            {
+                formatstring(colortmp)("\f[%d]", findcolour(d));
+                concatstring(colored, colortmp);
+            }
+            formatstring(colortmp)("\f($priv%stex)", server::privnamex(d->privilege, d->actortype, true));
+            concatstring(colored, colortmp);
+        }
+        if(colour&2)
+        {
+            formatstring(colortmp)("\f[%d]", TEAM(d->team, colour));
             concatstring(colored, colortmp);
         }
-        formatstring(colortmp)("\f[%d]%s", TEAM(d->team, colour), name);
-        concatstring(colored, colortmp);
-        if(!name[0] || (d->aitype < AI_START && dupname && duplicatename(d, name)))
+        concatstring(colored, name);
+        if(!name[0] || (d->actortype < A_ENEMY && dupname && duplicatename(d, name)))
         {
             formatstring(colortmp)("%s[%d]", name[0] ? " " : "", d->clientnum);
             concatstring(colored, colortmp);
         }
-        concatstring(colored, "\fS");
+        if(colour) concatstring(colored, "\fS");
         return colored;
     }
 
@@ -1928,7 +2018,7 @@ namespace game
             d->suicided = lastmillis;
         }
     }
-    ICOMMAND(0, kill, "",  (), { suicide(player1, 0); });
+    ICOMMAND(0, suicide, "",  (), { suicide(player1, 0); });
 
     vec rescolour(dynent *d, int c)
     {
@@ -1939,7 +2029,7 @@ namespace game
 
     void particletrack(particle *p, uint type, int &ts,  bool lastpass)
     {
-        if(!p || !p->owner || (p->owner->type != ENT_PLAYER && p->owner->type != ENT_AI)) return;
+        if(!p || !p->owner || !gameent::is(p->owner)) return;
         gameent *d = (gameent *)p->owner;
         switch(type&0xFF)
         {
@@ -1954,8 +2044,7 @@ namespace game
             {
                 float dist = p->o.dist(p->d);
                 p->d = p->o = d->muzzlepos(d->weapselect);
-                vec dir; vecfromyawpitch(d->yaw, d->pitch, 1, 0, dir);
-                p->d.add(dir.mul(dist));
+                p->d.add(vec(d->yaw*RAD, d->pitch*RAD).mul(dist));
                 break;
             }
             case PT_PART: case PT_FIREBALL: case PT_FLARE:
@@ -2020,10 +2109,10 @@ namespace game
         }
         else if(!tvmode())
         {
-            physent *d = (intermission || player1->state >= CS_SPECTATOR) && (focus == player1 || followaim()) ? camera1 : (allowmove(player1) ? player1 : NULL);
+            physent *d = (!gs_playing(gamestate) || player1->state >= CS_SPECTATOR) && (focus == player1 || followaim()) ? camera1 : (allowmove(player1) ? player1 : NULL);
             if(d)
             {
-                float scale = (inzoom() && zoomsensitivity > 0 ? (1.f-((zoomlevel+1)/float(zoomlevels+2)))*zoomsensitivity : 1.f)*sensitivity;
+                float scale = (focus == player1 && inzoom() && zoomsensitivity > 0 ? (1.f-((zoomlevel+1)/float(zoomlevels+2)))*zoomsensitivity : 1.f)*sensitivity;
                 d->yaw += mousesens(dx, sensitivityscale, yawsensitivity*scale);
                 d->pitch -= mousesens(dy, sensitivityscale, pitchsensitivity*scale*(mouseinvert ? -1.f : 1.f));
                 fixrange(d->yaw, d->pitch);
@@ -2039,7 +2128,7 @@ namespace game
         if(input != inputmouse || (view != inputview || mode != inputmode || focus != lastfocus))
         {
             if(input != inputmouse) resetcursor();
-            else resetcamera(focus != lastfocus);
+            else resetcamera(true);
             inputmouse = input;
             inputview = view;
             inputmode = mode;
@@ -2052,8 +2141,9 @@ namespace game
             {
                 case 1:
                 {
-                    vec loc(0, 0, 0);
-                    if(vectocursor(worldpos, loc.x, loc.y, loc.z))
+                    vec loc(0, 0, 0), pos = worldpos, dir = vec(worldpos).sub(focus->o);
+                    if(thirdpersoncursordist > 0 && dir.magnitude() > thirdpersoncursordist) pos = dir.normalize().mul(thirdpersoncursordist).add(focus->o);
+                    if(vectocursor(pos, loc.x, loc.y, loc.z))
                     {
                         float amt = curtime/float(thirdpersoninterp);
                         cursorx = clamp(cursorx+((loc.x-cursorx)*amt), 0.f, 1.f);
@@ -2116,7 +2206,6 @@ namespace game
             {
                 physent::reset();
                 type = ENT_CAMERA;
-                collidetype = COLLIDE_AABB;
                 state = CS_ALIVE;
                 height = zradius = radius = xradius = yradius = 2;
             }
@@ -2141,7 +2230,6 @@ namespace game
             {
                 physent::reset();
                 type = ENT_CAMERA;
-                collidetype = COLLIDE_AABB;
                 state = CS_ALIVE;
                 height = zradius = radius = xradius = yradius = 2;
             }
@@ -2157,10 +2245,10 @@ namespace game
             if(firstpersonpitchscale >= 0) lean *= firstpersonpitchscale;
             to.add(vec(yaw*RAD, (lean+90)*RAD).mul(spineoff));
         }
-        if(firstpersonbob && !intermission && d->state == CS_ALIVE)
+        if(firstpersonbob && gs_playing(gamestate) && d->state == CS_ALIVE)
         {
             float scale = 1;
-            if(d == player1 && inzoom())
+            if(d == focus && inzoom())
             {
                 int frame = lastmillis-lastzoom;
                 float pc = frame <= zoomtime ? (frame)/float(zoomtime) : 1.f;
@@ -2169,10 +2257,8 @@ namespace game
             if(firstpersonbobtopspeed) scale *= clamp(d->vel.magnitude()/firstpersonbobtopspeed, firstpersonbobmin, 1.f);
             if(scale > 0)
             {
-                vec dir;
-                vecfromyawpitch(yaw, 0, 0, 1, dir);
                 float steps = bobdist/firstpersonbobstep*M_PI;
-                dir.mul(firstpersonbobside*cosf(steps)*scale);
+                vec dir = vec(yaw*RAD, 0.f).mul(firstpersonbobside*cosf(steps)*scale);
                 dir.z = firstpersonbobup*(fabs(sinf(steps)) - 1)*scale;
                 to.add(dir);
             }
@@ -2211,17 +2297,19 @@ namespace game
             gameent *a = deathcamstyle > 1 ? d : getclient(d->lastattacker);
             if(a)
             {
+                float y = 0, p = 0;
                 vec dir = vec(a->center()).sub(camera1->o).normalize();
-                vectoyawpitch(dir, camera1->yaw, camera1->pitch);
+                vectoyawpitch(dir, y, p);
+                fixrange(y, p);
                 if(deathcamspeed > 0)
                 {
                     float speed = curtime/float(deathcamspeed);
-                    scaleyawpitch(yaw, pitch, camera1->yaw, camera1->pitch, speed, speed*4.f);
+                    scaleyawpitch(yaw, pitch, y, p, speed, speed*4.f);
                 }
                 else
                 {
-                    yaw = camera1->yaw;
-                    pitch = camera1->pitch;
+                    yaw = y;
+                    pitch = p;
                 }
             }
         }
@@ -2268,7 +2356,7 @@ namespace game
     {
         float foglevel = float(fog*2/3);
         c->reset();
-        if(!force && c->player && !allowspec(c->player, spectvdead)) return false;
+        if(!force && c->player && !allowspec(c->player, spectvdead, spectvfollowing)) return false;
         bool aim = !c->player || spectvaiming(c->player);
         float yaw = c->player ? c->player->yaw : camera1->yaw, pitch = c->player ? c->player->pitch : camera1->pitch;
         fixrange(yaw, pitch);
@@ -2282,9 +2370,10 @@ namespace game
                 switch(cam->type)
                 {
                     case cament::ENTITY: continue;
+                    case cament::AFFINITY: if(cam->player == c->player) continue;
                     case cament::PLAYER:
                     {
-                        if(!cam->player || c->player == cam->player || !allowspec(cam->player, spectvdead)) continue;
+                        if(!cam->player || c->player == cam->player || !allowspec(cam->player, spectvdead, spectvfollowing)) continue;
                         break;
                     }
                     default: break;
@@ -2294,8 +2383,8 @@ namespace game
                     vectoyawpitch(vec(cam->o).sub(from).normalize(), yaw, pitch);
                     fixrange(yaw, pitch);
                 }
-                float dist = from.dist(cam->o), fogdist = min(c->maxdist, foglevel);
-                if(dist >= c->mindist && getsight(from, yaw, pitch, cam->o, trg, fogdist, curfov, fovy))
+                float dist = from.dist(cam->o), fogdist = min(spectvmaxdist, foglevel);
+                if(dist >= spectvmindist && getsight(from, yaw, pitch, cam->o, trg, fogdist, curfov, fovy))
                 {
                     c->inview[cam->type]++;
                     dir.add(cam->o);
@@ -2323,7 +2412,7 @@ namespace game
             yaw = c->player ? c->player->yaw : float(rnd(360));
             pitch = c->player ? c->player->pitch : float(rnd(91)-45);
             fixrange(yaw, pitch);
-            vecfromyawpitch(yaw, pitch, 1, 0, c->dir);
+            c->dir = vec(yaw*RAD, pitch*RAD);
             if(force) return true;
         }
         return false;
@@ -2332,6 +2421,10 @@ namespace game
     bool cameratv()
     {
         if(!tvmode(false)) return false;
+        if(!gs_playing(gamestate)) spectvfollowing = -1;
+        else if(player1->state != CS_SPECTATOR && spectvfollowself >= (m_duke(gamemode, mutators) ? 2 : 1))
+            spectvfollowing = player1->clientnum;
+        else spectvfollowing = spectvfollow;
         if(cameras.empty())
         {
             loopv(entities::ents)
@@ -2347,25 +2440,31 @@ namespace game
             loopv(players) if(players[i])
             {
                 gameent *d = players[i];
-                if((d->type != ENT_PLAYER && d->type != ENT_AI) || d->aitype >= AI_START) continue;
+                if(d->actortype >= A_ENEMY) continue;
                 cament *c = cameras.add(new cament);
                 c->o = d->headpos();
                 c->type = cament::PLAYER;
                 c->id = d->clientnum;
                 c->player = d;
             }
+            cament *c = cameras.add(new cament);
+            c->o = player1->headpos();
+            c->type = cament::PLAYER;
+            c->id = player1->clientnum;
+            c->player = player1;
             if(m_capture(gamemode)) capture::checkcams(cameras);
             else if(m_defend(gamemode)) defend::checkcams(cameras);
             else if(m_bomber(gamemode)) bomber::checkcams(cameras);
         }
         if(cameras.empty()) return false;
         cament *cam = cameras[0];
-        bool forced = !tvmode(false, false), renew = !lastcamera;
+        bool forced = !tvmode(false, false), renew = !lastcamera, found = spectvfollowing < 0;
         float amt = 0;
         loopv(cameras)
         {
             if(cameras[i]->type == cament::PLAYER && (cameras[i]->player || ((cameras[i]->player = getclient(cameras[i]->id)) != NULL)))
             {
+                if(!found && cameras[i]->id == spectvfollowing && cameras[i]->player->state != CS_SPECTATOR) found = true;
                 cameras[i]->o = cameras[i]->player->headpos();
                 if(forced && cameras[i]->player == focus) cam = cameras[i];
             }
@@ -2374,7 +2473,9 @@ namespace game
             else if(m_defend(gamemode)) defend::updatecam(cameras[i]);
             else if(m_bomber(gamemode)) bomber::updatecam(cameras[i]);
         }
+        if(!found) spectvfollow = spectvfollowing = -1;
         camrefresh(cam);
+        #define stvf(z) (!gs_playing(gamestate) ? spectvinterm##z : (spectvfollowing >= 0 ? spectvfollow##z : spectv##z))
         if(forced)
         {
             camupdate(cam, amt, renew, true);
@@ -2384,12 +2485,17 @@ namespace game
                 cam->resetlast();
             }
         }
-        else
+        else loopk(spectvfollowing >= 0 ? 2 : 1)
         {
             int lasttype = cam->type, lastid = cam->id, millis = lasttvchg ? lastmillis-lasttvchg : 0;
-            if(millis) amt = float(millis)/float(spectvmaxtime);
-            bool updated = camupdate(cam, amt, renew), override = !lasttvchg || millis >= spectvmintime,
-                 reset = (spectvmaxtime && millis >= spectvmaxtime) || !lasttvcam || lastmillis-lasttvcam >= spectvtime;
+            if(millis) amt = float(millis)/float(stvf(maxtime));
+            bool updated = camupdate(cam, amt, renew), override = !lasttvchg || millis >= stvf(mintime),
+                 reset = (stvf(maxtime) && millis >= stvf(maxtime)) || !lasttvcam || lastmillis-lasttvcam >= stvf(time);
+            if(spectvfollowing >= 0 && !reset && !updated && !override)
+            {
+                spectvfollowing = -1;
+                continue;
+            }
             if(reset || (!updated && override))
             {
                 cam->ignore = true;
@@ -2402,7 +2508,7 @@ namespace game
             }
             if(reset)
             {
-                cameras.sort(cament::camsort);
+                cameras.sort(cament::compare);
                 loopv(cameras) if(cameras[i]->ignore) cameras[i]->ignore = false;
                 cam = cameras[0];
                 lasttvcam = lastmillis;
@@ -2437,6 +2543,7 @@ namespace game
                     }
                 }
             }
+            break;
         }
         bool chase = cam->player && (forced || spectvaiming(cam->player));
         if(!cam->player || chase)
@@ -2446,9 +2553,9 @@ namespace game
             fixrange(yaw, pitch);
             if(!renew)
             {
-                float speed = curtime/float(chase ? followtvspeed : spectvspeed);
+                float speed = curtime/float(chase ? followtvspeed : stvf(speed));
                 #define SCALEAXIS(x) \
-                    float x##scale = 1, adj##x = camera1->x, off##x = x, x##thresh = chase ? followtv##x##thresh : spectv##x##thresh; \
+                    float x##scale = 1, adj##x = camera1->x, off##x = x, x##thresh = chase ? followtv##x##thresh : stvf(x##thresh); \
                     if(adj##x < x - 180.0f) adj##x += 360.0f; \
                     if(adj##x > x + 180.0f) adj##x -= 360.0f; \
                     off##x -= adj##x; \
@@ -2459,14 +2566,14 @@ namespace game
                     } \
                     else if(cam->last##x##time) \
                     { \
-                        int offtime = lastmillis-cam->last##x##time, x##speed = chase ? followtv##x##speed : spectv##x##speed; \
+                        int offtime = lastmillis-cam->last##x##time, x##speed = chase ? followtv##x##speed : stvf(x##speed); \
                         if(offtime <= x##speed) x##scale = offtime/float(x##speed); \
                     } \
                     cam->last##x = off##x; \
-                    float x##speed = speed*(chase ? followtv##x##scale : spectv##x##scale)*x##scale;
+                    float x##speed = speed*(chase ? followtv##x##scale : stvf(x##scale))*x##scale;
                 SCALEAXIS(yaw);
                 SCALEAXIS(pitch);
-                scaleyawpitch(camera1->yaw, camera1->pitch, yaw, pitch, yawspeed, pitchspeed, (chase ? followtvrotate : spectvrotate));
+                scaleyawpitch(camera1->yaw, camera1->pitch, yaw, pitch, yawspeed, pitchspeed, (chase ? followtvrotate : stvf(rotate)));
             }
             else
             {
@@ -2497,11 +2604,10 @@ namespace game
         {
             camera1->reset();
             camera1->type = ENT_CAMERA;
-            camera1->collidetype = COLLIDE_AABB;
             camera1->state = CS_ALIVE;
             camera1->height = camera1->zradius = camera1->radius = camera1->xradius = camera1->yradius = 2;
         }
-        if(player1->state < CS_SPECTATOR && focus != player1) resetfollow();
+        if(player1->state < CS_SPECTATOR && focus != player1 && gs_playing(gamestate)) specreset();
         if(tvmode() || player1->state < CS_SPECTATOR)
         {
             camera1->vel = vec(0, 0, 0);
@@ -2516,7 +2622,7 @@ namespace game
         switch(d->state)
         {
             case CS_SPECTATOR: case CS_WAITING: r = wobble*0.5f; break;
-            case CS_ALIVE: if(physics::iscrouching(d)) wobble *= 0.5f; r += wobble; break;
+            case CS_ALIVE: if(d->crouching()) wobble *= 0.5f; r += wobble; break;
             case CS_DEAD: r += wobble; break;
             default: break;
         }
@@ -2526,10 +2632,10 @@ namespace game
     void calcangles(physent *c, gameent *d)
     {
         c->roll = calcroll(d);
-        if(firstpersonbob && !intermission && d->state == CS_ALIVE && !thirdpersonview(true))
+        if(firstpersonbob && gs_playing(gamestate) && d->state == CS_ALIVE && !thirdpersonview(true))
         {
             float scale = 1;
-            if(d == player1 && inzoom())
+            if(d == focus && inzoom())
             {
                 int frame = lastmillis-lastzoom;
                 float pc = frame <= zoomtime ? (frame)/float(zoomtime) : 1.f;
@@ -2571,7 +2677,7 @@ namespace game
 
     void resetworld()
     {
-        resetfollow();
+        specreset();
         hud::showscores(false);
         cleargui();
     }
@@ -2586,18 +2692,26 @@ namespace game
     {
         if(connected())
         {
+            if(!curtime || !client::isready)
+            {
+                gets2c();
+                if(player1->clientnum >= 0) client::c2sinfo();
+                return;
+            }
+            int type = client::needsmap ? 6 : (m_edit(gamemode) && musicedit >= 0 ? musicedit : musictype);
             if(!maptime) { maptime = -1; return; } // skip the first loop
             else if(maptime < 0)
             {
-                maptime = NZT(lastmillis);
-                musicdone(false);
+                maptime = lastmillis ? lastmillis : 1;
+                if(type != 6) musicdone(false);
                 RUNWORLD("on_start");
+                resetcamera();
                 return;
             }
-            else if(!nosound && mastervol && musicvol)
+            else if(!nosound && mastervol && musicvol && type && !playingmusic())
             {
-                int type = m_edit(gamemode) && musicedit >= 0 ? musicedit : musictype;
-                if(type && !playingmusic())
+                if(type == 6) smartmusic(true, false);
+                else
                 {
                     defformatstring(musicfile)("%s", mapmusic);
                     if(*musicdir && (type == 2 || type == 5 || ((type == 1 || type == 4) && (!*musicfile || !fileexists(findfile(musicfile, "r"), "r")))))
@@ -2608,61 +2722,45 @@ namespace game
                         {
                             int r = rnd(files.length());
                             formatstring(musicfile)("%s/%s", musicdir, files[r]);
-                            if(files[r][0] != '.' && playmusic(musicfile, type >= 4 ? "music" : NULL)) break;
+                            if(files[r][0] != '.' && strcmp(files[r], "readme.txt") && playmusic(musicfile, type >= 4 ? "music" : NULL)) break;
                             else files.remove(r);
                         }
                     }
                     else if(*musicfile) playmusic(musicfile, type >= 4 ? "music" : NULL);
                 }
             }
-        }
-        if(!curtime)
-        {
-            gets2c();
-            if(player1->clientnum >= 0) client::c2sinfo();
-            return;
-        }
-
-        if(needname(player1) && !menuactive()) showgui("profile", -1);
-        if(connected())
-        {
             player1->conopen = commandmillis > 0 || hud::hasinput(true);
             checkoften(player1, true);
             loopv(players) if(players[i]) checkoften(players[i], players[i]->ai != NULL);
             if(!allowmove(player1)) player1->stopmoving(player1->state < CS_SPECTATOR);
-            if(player1->state == CS_ALIVE)
-            {
-                int state = player1->weapstate[player1->weapselect];
-                if(W(player1->weapselect, zooms))
-                {
-                    if(state == W_S_PRIMARY || state == W_S_SECONDARY || (state == W_S_RELOAD && lastmillis-player1->weaplast[player1->weapselect] >= max(player1->weapwait[player1->weapselect]-zoomtime, 1)))
-                        state = W_S_IDLE;
-                }
-                if(zooming && (!W(player1->weapselect, zooms) || state != W_S_IDLE)) zoomset(false, lastmillis);
-                else if(W(player1->weapselect, zooms) && state == W_S_IDLE && zooming != player1->action[AC_SECONDARY])
-                    zoomset(player1->action[AC_SECONDARY], lastmillis);
-            }
-            else if(zooming) zoomset(false, lastmillis);
+            if(focus->state == CS_ALIVE && gs_playing(gamestate)) zoomset(focus->zooming(), lastmillis);
+            else if(zooming) zoomset(false, 0);
 
             physics::update();
             ai::navigate();
             projs::update();
             ai::update();
-            if(!intermission)
+            if(gs_playing(gamestate))
             {
                 entities::update();
+                if(m_capture(gamemode)) capture::update();
+                else if(m_bomber(gamemode)) bomber::update();
                 if(player1->state == CS_ALIVE) weapons::shoot(player1, worldpos);
             }
             otherplayers();
-            if(needloadout(player1) && !menuactive()) showgui("loadout", -1);
+            checkannounce();
+            flushdamagemerges();
+            if(!menuactive())
+            {
+                if(needname(player1)) showgui("profile", 1);
+                else if(wantsloadoutmenu) showgui("profile", 2, &wantsloadoutmenu);
+            }
         }
-        else if(!menuactive()) showgui("main", -1);
-
+        else if(!menuactive()) showgui(needname(player1) ? "profile" : "main", 1);
         gets2c();
         adjustscaled(hud::damageresidue, hud::damageresiduefade);
         if(connected())
         {
-            flushdamagemerges();
             checkcamera();
             if(player1->state == CS_DEAD || player1->state == CS_WAITING)
             {
@@ -2674,13 +2772,13 @@ namespace game
             {
                 if(player1->ragdoll) cleanragdoll(player1);
                 if(player1->state == CS_EDITING) physics::move(player1, 10, true);
-                else if(player1->state == CS_ALIVE && !intermission && !tvmode())
+                else if(player1->state == CS_ALIVE && gs_playing(gamestate) && !tvmode())
                 {
                     physics::move(player1, 10, true);
                     weapons::checkweapons(player1);
                 }
             }
-            if(!intermission)
+            if(gs_playing(gamestate))
             {
                 addsway(focus);
                 if(player1->state == CS_ALIVE || player1->state == CS_DEAD || player1->state == CS_WAITING)
@@ -2711,10 +2809,10 @@ namespace game
                     deathcamyawpitch(focus, camera1->yaw, camera1->pitch);
                 else
                 {
-                    physent *d = player1->state >= CS_SPECTATOR || (intermission && focus == player1) ? camera1 : focus;
-                    if(d != camera1 || focus != player1 || intermission)
+                    physent *d = player1->state >= CS_SPECTATOR || (!gs_playing(gamestate) && focus == player1) ? camera1 : focus;
+                    if(d != camera1 || focus != player1 || !gs_playing(gamestate))
                         camera1->o = camerapos(focus, true, true, d->yaw, d->pitch);
-                    if(d != camera1 || (intermission && focus == player1) || (focus != player1 && !followaim()))
+                    if(d != camera1 || (!gs_playing(gamestate) && focus == player1) || (focus != player1 && !followaim()))
                     {
                         camera1->yaw = (d != camera1 ? d : focus)->yaw;
                         camera1->pitch = (d != camera1 ? d : focus)->pitch;
@@ -2751,22 +2849,6 @@ namespace game
 
             camera1->inmaterial = lookupmaterial(camera1->o);
             camera1->inliquid = isliquid(camera1->inmaterial&MATF_VOLUME);
-
-            switch(camera1->inmaterial&MATF_VOLUME)
-            {
-                case MAT_WATER:
-                {
-                    if(!issound(liquidchan))
-                        playsound(S_UNDERWATER, camera1->o, camera1, SND_LOOP|SND_NOATTEN|SND_NODELAY|SND_NOCULL, -1, -1, -1, &liquidchan);
-                    break;
-                }
-                default:
-                {
-                    if(issound(liquidchan)) removesound(liquidchan);
-                    liquidchan = -1;
-                    break;
-                }
-            }
             lastcamera = lastmillis;
         }
     }
@@ -2785,16 +2867,11 @@ namespace game
 
     void renderclient(gameent *d, int third, float trans, float size, int team, modelattach *attachments, bool secondary, int animflags, int animdelay, int lastaction, bool early)
     {
-#ifdef MEK
-        const char *mdl = playertypes[0][third];
-        if(d->aitype >= AI_START) mdl = aistyle[d->aitype%AI_MAX].playermodel[third];
-        else mdl = playertypes[d->model%PLAYERTYPES][third];
-#else
-        int idx = third == 1 && d->headless && headlessmodels ? 3 : third;
+        int idx = third == 1 && d->headless && !nogore && headlessmodels ? 3 : third;
         const char *mdl = playertypes[forceplayermodel >= 0 ? forceplayermodel : 0][idx];
-        if(d->aitype >= AI_START && d->aitype != AI_GRUNT) mdl = aistyle[d->aitype%AI_MAX].playermodel[idx];
+        if(d->actortype >= A_ENEMY && d->actortype != A_GRUNT) mdl = actor[d->actortype%A_MAX].playermodel[idx];
         else if(forceplayermodel < 0) mdl = playertypes[d->model%PLAYERTYPES][idx];
-#endif
+
         bool onfloor = d->physstate >= PHYS_SLOPE || d->onladder || physics::liquidcheck(d);
         float yaw = d->yaw, pitch = d->pitch, roll = calcroll(focus);
         vec o = third ? d->feetpos() : camerapos(d);
@@ -2802,28 +2879,23 @@ namespace game
         {
             o.sub(vec(yaw*RAD, 0.f).mul(firstpersonbodydist+firstpersonspineoffset));
             o.sub(vec(yaw*RAD, 0.f).rotate_around_z(90*RAD).mul(firstpersonbodyside));
-            if(d->zradius > d->height)
-            {
-                float offz = d->zradius-d->height;
-                if(onfloor || d->turnside) { if(d->floortime(lastmillis) <= 50) { offz *= 1-(d->floortime(lastmillis)/50.f); } }
-                else if(d->airtime(lastmillis) <= 50) offz *= d->airtime(lastmillis)/50.f;
-                o.z -= offz;
-            }
             if(firstpersonbodyfeet >= 0 && d->wantshitbox())
             {
                 float minz = max(d->toe[0].z, d->toe[1].z)+firstpersonbodyfeet;
                 if(minz > camera1->o.z) o.z -= minz-camera1->o.z;
             }
         }
-        else if(third == 1 && d == focus && d == player1 && thirdpersonview(true, d))
-            vectoyawpitch(vec(worldpos).sub(d->headpos()).normalize(), yaw, pitch);
-        else if(!third && firstpersonsway && !intermission)
+        else if(gs_playing(gamestate))
         {
-            vec dir; vecfromyawpitch(d->yaw, 0, 0, 1, dir);
-            float steps = swaydist/(firstpersonbob ? firstpersonbobstep : firstpersonswaystep)*M_PI;
-            dir.mul(firstpersonswayside*cosf(steps));
-            dir.z = firstpersonswayup*(fabs(sinf(steps)) - 1);
-            o.add(dir).add(swaydir).add(swaypush);
+            if(third == 1 && d == focus && d == player1 && thirdpersonview(true, d))
+                vectoyawpitch(vec(worldpos).sub(d->headpos()).normalize(), yaw, pitch);
+            else if(!third && firstpersonsway)
+            {
+                float steps = swaydist/(firstpersonbob ? firstpersonbobstep : firstpersonswaystep)*M_PI;
+                vec dir = vec(d->yaw*RAD, 0.f).mul(firstpersonswayside*cosf(steps));
+                dir.z = firstpersonswayup*(fabs(sinf(steps)) - 1);
+                o.add(dir).add(swaydir).add(swaypush);
+            }
         }
 
         int anim = animflags, basetime = lastaction, basetime2 = 0;
@@ -2834,22 +2906,10 @@ namespace game
         }
         else
         {
-            bool melee = d->hasmelee(lastmillis, true, physics::sliding(d, true), onfloor);
-            if(secondary && allowmove(d) && aistyle[d->aitype].canmove)
+            bool melee = d->hasmelee(lastmillis, true, d->sliding(true), onfloor);
+            if(secondary && allowmove(d) && actor[d->actortype].canmove)
             {
-                if(physics::jetpack(d))
-                {
-                    if(melee)
-                    {
-                        anim |= ANIM_FLYKICK<<ANIM_SECONDARY;
-                        basetime2 = d->weaplast[W_MELEE];
-                    }
-                    else if(d->move>0) anim |= (ANIM_JET_FORWARD|ANIM_LOOP)<<ANIM_SECONDARY;
-                    else if(d->strafe) anim |= ((d->strafe>0 ? ANIM_JET_LEFT : ANIM_JET_RIGHT)|ANIM_LOOP)<<ANIM_SECONDARY;
-                    else if(d->move<0) anim |= (ANIM_JET_BACKWARD|ANIM_LOOP)<<ANIM_SECONDARY;
-                    else anim |= (ANIM_JET_UP|ANIM_LOOP)<<ANIM_SECONDARY;
-                }
-                else if(physics::liquidcheck(d) && d->physstate <= PHYS_FALL)
+                if(physics::liquidcheck(d) && d->physstate <= PHYS_FALL)
                     anim |= ((d->move || d->strafe || d->vel.z+d->falling.z>0 ? int(ANIM_SWIM) : int(ANIM_SINK))|ANIM_LOOP)<<ANIM_SECONDARY;
                 else if(d->turnside) anim |= ((d->turnside>0 ? ANIM_WALL_RUN_LEFT : ANIM_WALL_RUN_RIGHT)|ANIM_LOOP)<<ANIM_SECONDARY;
                 else if(d->physstate == PHYS_FALL && !d->onladder && d->impulse[IM_TYPE] != IM_T_NONE && lastmillis-d->impulse[IM_TIME] <= 1000)
@@ -2887,20 +2947,20 @@ namespace game
                     else anim |= ANIM_JUMP<<ANIM_SECONDARY;
                     if(!basetime2) anim |= ANIM_END<<ANIM_SECONDARY;
                 }
-                else if(physics::sliding(d, true)) anim |= (ANIM_POWERSLIDE|ANIM_LOOP)<<ANIM_SECONDARY;
-                else if(physics::pacing(d))
-                {
-                    if(d->move>0) anim |= (ANIM_IMPULSE_FORWARD|ANIM_LOOP)<<ANIM_SECONDARY;
-                    else if(d->strafe) anim |= ((d->strafe>0 ? ANIM_IMPULSE_LEFT : ANIM_IMPULSE_RIGHT)|ANIM_LOOP)<<ANIM_SECONDARY;
-                    else if(d->move<0) anim |= (ANIM_IMPULSE_BACKWARD|ANIM_LOOP)<<ANIM_SECONDARY;
-                }
-                else if(d->action[AC_CROUCH] || d->actiontime[AC_CROUCH]<0)
+                else if(d->sliding(true)) anim |= (ANIM_POWERSLIDE|ANIM_LOOP)<<ANIM_SECONDARY;
+                else if(d->crouching(true))
                 {
                     if(d->move>0) anim |= (ANIM_CRAWL_FORWARD|ANIM_LOOP)<<ANIM_SECONDARY;
                     else if(d->strafe) anim |= ((d->strafe>0 ? ANIM_CRAWL_LEFT : ANIM_CRAWL_RIGHT)|ANIM_LOOP)<<ANIM_SECONDARY;
                     else if(d->move<0) anim |= (ANIM_CRAWL_BACKWARD|ANIM_LOOP)<<ANIM_SECONDARY;
                     else anim |= (ANIM_CROUCH|ANIM_LOOP)<<ANIM_SECONDARY;
                 }
+                else if(d->running(moveslow))
+                {
+                    if(d->move>0) anim |= (ANIM_IMPULSE_FORWARD|ANIM_LOOP)<<ANIM_SECONDARY;
+                    else if(d->strafe) anim |= ((d->strafe>0 ? ANIM_IMPULSE_LEFT : ANIM_IMPULSE_RIGHT)|ANIM_LOOP)<<ANIM_SECONDARY;
+                    else if(d->move<0) anim |= (ANIM_IMPULSE_BACKWARD|ANIM_LOOP)<<ANIM_SECONDARY;
+                }
                 else if(d->move>0) anim |= (ANIM_FORWARD|ANIM_LOOP)<<ANIM_SECONDARY;
                 else if(d->strafe) anim |= ((d->strafe>0 ? ANIM_LEFT : ANIM_RIGHT)|ANIM_LOOP)<<ANIM_SECONDARY;
                 else if(d->move<0) anim |= (ANIM_BACKWARD|ANIM_LOOP)<<ANIM_SECONDARY;
@@ -2909,7 +2969,7 @@ namespace game
             if((anim>>ANIM_SECONDARY)&ANIM_INDEX) switch(anim&ANIM_INDEX)
             {
                 case ANIM_IDLE: case ANIM_MELEE: case ANIM_PISTOL: case ANIM_SWORD:
-                case ANIM_SHOTGUN: case ANIM_SMG: case ANIM_FLAMER: case ANIM_PLASMA:
+                case ANIM_SHOTGUN: case ANIM_SMG: case ANIM_FLAMER: case ANIM_PLASMA: case ANIM_ZAPPER:
                 case ANIM_RIFLE: case ANIM_GRENADE: case ANIM_MINE: case ANIM_ROCKET:
                 {
                     anim = (anim>>ANIM_SECONDARY) | ((anim&((1<<ANIM_SECONDARY)-1))<<ANIM_SECONDARY);
@@ -2925,12 +2985,9 @@ namespace game
         if(!((anim>>ANIM_SECONDARY)&ANIM_INDEX)) anim |= (ANIM_IDLE|ANIM_LOOP)<<ANIM_SECONDARY;
 
         int flags = MDL_LIGHT|MDL_LIGHTFX;
+        if(d->actortype >= A_ENEMY) flags |= MDL_CULL_DIST;
+        if(d != focus || (d != player1 ? fullbrightfocus&1 : fullbrightfocus&2)) flags |= MDL_FULLBRIGHT;
         if(d != focus && !(anim&ANIM_RAGDOLL)) flags |= MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_CULL_QUERY;
-        if(d->type == ENT_PLAYER)
-        {
-            if(!early && third) flags |= MDL_FULLBRIGHT;
-        }
-        else flags |= MDL_CULL_DIST;
         if(early) flags |= MDL_NORENDER;
         else if(third && (anim&ANIM_INDEX)!=ANIM_DEAD) flags |= MDL_DYNSHADOW;
         if(modelpreviewing) flags &= ~(MDL_LIGHT|MDL_FULLBRIGHT|MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_CULL_QUERY|MDL_CULL_DIST|MDL_DYNSHADOW);
@@ -2940,9 +2997,9 @@ namespace game
             e->light.effect = vec::hexcolor(getcolour(d, playerlighttone)).mul(playerlightmix);
             e->light.material[0] = bvec(getcolour(d, playerovertone));
             e->light.material[1] = bvec(getcolour(d, playerundertone));
-            if(renderpath != R_FIXEDFUNCTION && isweap(d->weapselect) && (W2(d->weapselect, sub, false) || W2(d->weapselect, sub, true)) && W(d->weapselect, max) > 1)
+            if(renderpath != R_FIXEDFUNCTION && isweap(d->weapselect) && (W2(d->weapselect, ammosub, false) || W2(d->weapselect, ammosub, true)) && W(d->weapselect, ammomax) > 1)
             {
-                int ammo = d->ammo[d->weapselect], maxammo = W(d->weapselect, max);
+                int ammo = d->ammo[d->weapselect], maxammo = W(d->weapselect, ammomax);
                 float scale = 1;
                 switch(d->weapstate[d->weapselect])
                 {
@@ -2986,7 +3043,7 @@ namespace game
                 flashcolour(e->light.material[1].r, e->light.material[1].g, e->light.material[1].b, uchar(192), uchar(192), uchar(192), amt);
                 flashcolour(e->light.effect.r, e->light.effect.g, e->light.effect.b, 1.f, 1.f, 1.f, amt);
             }
-            if(m_bomber(gamemode) && m_gsp1(gamemode, mutators) && bomber::carryaffinity(d))
+            if(m_bomber(gamemode) && bomber::carryaffinity(d))
             {
                 vec col = rescolour(d, PULSE_DISCO);
                 e->light.material[1] = bvec::fromcolor(e->light.material[1].tocolor().max(col));
@@ -2998,13 +3055,28 @@ namespace game
 
     void renderabovehead(gameent *d, float trans)
     {
-        vec pos = d->abovehead(d->state != CS_DEAD ? 2 : -2);
+        vec pos = d->abovehead(d->state != CS_DEAD && d->state != CS_WAITING ? 1 : 0);
         float blend = aboveheadblend*trans;
         if(aboveheadnames && d != player1)
         {
-            pos.z += aboveheadnamesize/2;
-            part_textcopy(pos, colourname(d), PART_TEXT, 1, 0xFFFFFF, aboveheadnamesize, blend);
-            pos.z += aboveheadnamesize/2+0.5f;
+            pos.z += aboveheadnamessize/2;
+            part_textcopy(pos, colourname(d), PART_TEXT, 1, 0xFFFFFF, aboveheadnamessize, blend*aboveheadnamesblend);
+        }
+        if(aboveheadinventory && d != player1)
+        {
+            string weapons = "";
+            #define printweapon(q) \
+                if((q != W_MELEE || q == d->weapselect) && d->hasweap(q, m_weapon(gamemode, mutators))) \
+                { \
+                    vec colour = vec::hexcolor(W(q, colour)); \
+                    if(q != d->weapselect) colour.mul(aboveheadinventoryfade); \
+                    defformatstring(str)("\fs\f[%d]%s\f(%s)\fS", colour.tohexcolor(), q != d->weapselect ? "\fE" : "", hud::itemtex(WEAPON, q)); \
+                    concatstring(weapons, str); \
+                }
+            if(aboveheadinventory >= 2) { loopi(W_MAX) printweapon(i); }
+            else { printweapon(d->weapselect); }
+            pos.z += aboveheadinventorysize/2;
+            part_textcopy(pos, weapons, PART_TEXT, 1, 0xFFFFFF, aboveheadinventorysize, blend*aboveheadinventoryblend);
         }
         if(aboveheadstatus)
         {
@@ -3014,20 +3086,26 @@ namespace game
             else if(d->state == CS_ALIVE)
             {
                 if(d->conopen) t = textureload(hud::chattex, 3);
-                else if(m_team(gamemode, mutators) && (hud::numteamkills() >= teamkillwarn || aboveheadteam > (d->team != focus->team ? 1 : 0)))
+                else if(m_team(gamemode, mutators) && (hud::numteamkills() >= teamkillwarn || aboveheadteam&(d->team != focus->team ? 2 : 1)))
                     t = textureload(hud::teamtexname(d->team), 3);
-                else if(!m_team(gamemode, mutators) || d->team != focus->team)
+                if(!m_team(gamemode, mutators) || d->team != focus->team)
                 {
-                    if(d->dominating.find(focus) >= 0) t = textureload(hud::dominatingtex, 3);
-                    else if(d->dominated.find(focus) >= 0) t = textureload(hud::dominatedtex, 3);
-                    colour = pulsecols[PULSE_DISCO][clamp((lastmillis/100)%PULSECOLOURS, 0, PULSECOLOURS-1)];
+                    if(d->dominating.find(focus) >= 0)
+                    {
+                        t = textureload(hud::dominatingtex, 3);
+                        colour = pulsecols[PULSE_DISCO][clamp((lastmillis/100)%PULSECOLOURS, 0, PULSECOLOURS-1)];
+                    }
+                    else if(d->dominated.find(focus) >= 0)
+                    {
+                        t = textureload(hud::dominatedtex, 3);
+                        colour = pulsecols[PULSE_DISCO][clamp((lastmillis/100)%PULSECOLOURS, 0, PULSECOLOURS-1)];
+                    }
                 }
             }
             if(t && t != notexture)
             {
-                pos.z += aboveheadstatussize/2;
-                part_icon(pos, t, aboveheadstatussize, blend, 0, 0, 1, colour);
-                pos.z += aboveheadstatussize/2+1.5f;
+                pos.z += aboveheadstatussize;
+                part_icon(pos, t, aboveheadstatussize, blend*aboveheadstatusblend, 0, 0, 1, colour);
             }
         }
         if(aboveheadicons && d->state != CS_EDITING && d->state != CS_SPECTATOR) loopv(d->icons)
@@ -3045,7 +3123,7 @@ namespace game
                           size = skew, fade = blend*skew;
                     if(d->icons[i].type >= eventicon::SORTED)
                     {
-                        size *= aboveheadiconsize;
+                        size *= aboveheadiconssize;
                         switch(d->icons[i].type)
                         {
                             case eventicon::WEAPON: colour = W(d->icons[i].value, colour); break;
@@ -3061,10 +3139,9 @@ namespace game
                         }
                     }
                     else size *= aboveheadeventsize;
-                    pos.z += size/2;
-                    part_icon(pos, t, size, fade, 0, 0, 1, colour);
-                    pos.z += size/2;
-                    if(d->icons[i].type >= eventicon::SORTED) pos.z += 1.5f;
+                    pos.z += size;
+                    part_icon(pos, t, size, fade*aboveheadiconsblend, 0, 0, 1, colour);
+                    //if(d->icons[i].type >= eventicon::SORTED) pos.z += 1.5f;
                 }
             }
         }
@@ -3081,7 +3158,7 @@ namespace game
         }
         int team = m_fight(gamemode) && m_team(gamemode, mutators) ? d->team : T_NEUTRAL,
             weap = d->weapselect, lastaction = 0, animflags = ANIM_IDLE|ANIM_LOOP, weapflags = animflags, weapaction = 0, animdelay = 0;
-        bool secondary = false, showweap = third != 2 && isweap(weap) && (d->aitype < AI_START || aistyle[d->aitype].useweap);
+        bool secondary = false, showweap = third != 2 && isweap(weap) && actor[d->actortype].useweap;
         float weapscale = 1.f;
         if(d->state == CS_DEAD || d->state == CS_WAITING)
         {
@@ -3113,50 +3190,49 @@ namespace game
         else
         {
             secondary = third;
-            if(showweap)
+            if(showweap && isweap(weap))
             {
                 weapaction = lastaction = d->weaplast[weap];
                 animdelay = d->weapwait[weap];
                 switch(d->weapstate[weap])
                 {
-                    case W_S_SWITCH:
-                    case W_S_USE:
+                    case W_S_SWITCH: case W_S_USE:
                     {
-                        int millis = lastmillis-d->weaplast[weap], off = d->weapwait[weap]/3;
-                        if(millis <= off)
+                        int millis = lastmillis-d->weaplast[weap], off = d->weapwait[weap]/3, lastweap = d->getlastweap(m_weapon(gamemode, mutators));
+                        if(isweap(lastweap) && millis <= off)
                         {
-                            if(!d->hasweap(d->lastweap, m_weapon(gamemode, mutators))) showweap = false;
-                            else
-                            {
-                                weap = d->lastweap;
-                                weapscale = 1-(millis/float(off));
-                            }
+                            weap = lastweap;
+                            weapscale = 1-(millis/float(off));
                         }
                         else if(!d->hasweap(weap, m_weapon(gamemode, mutators))) showweap = false;
                         else if(millis <= off*2) weapscale = (millis-off)/float(off);
-                        weapflags = animflags = ANIM_SWITCH+(d->weapstate[weap]-W_S_SWITCH);
+                        weapflags = animflags = d->weapstate[weap] == W_S_SWITCH ? ANIM_SWITCH : ANIM_USE;
                         break;
                     }
-                    case W_S_POWER:
+                    case W_S_POWER: case W_S_ZOOM:
                     {
                         switch(weaptype[weap].anim)
                         {
-                            case ANIM_GRENADE: case ANIM_MINE: weapflags = animflags = weaptype[weap].anim+d->weapstate[weap]; break;
+                            case ANIM_GRENADE: weapflags = animflags = ANIM_GRENADE_POWER; break;
                             default: weapflags = animflags = weaptype[weap].anim|ANIM_LOOP; break;
                         }
                         break;
                     }
-                    case W_S_PRIMARY:
-                    case W_S_SECONDARY:
+                    case W_S_PRIMARY: case W_S_SECONDARY:
                     {
-                        if(weaptype[weap].thrown[d->weapstate[weap] != W_S_SECONDARY ? 0 : 1] > 0 && (lastmillis-d->weaplast[weap] <= d->weapwait[weap]/2 || !d->hasweap(weap, m_weapon(gamemode, mutators))))
-                            showweap = false;
+                        if(weaptype[weap].thrown[d->weapstate[weap]-W_S_PRIMARY] > 0)
+                        {
+                            int millis = lastmillis-d->weaplast[weap], off = d->weapwait[weap]/2;
+                            if(millis <= off || !d->hasweap(weap, m_weapon(gamemode, mutators)))
+                                showweap = false;
+                            else weapscale = (millis-off)/float(off);
+                        }
                         weapflags = animflags = (weaptype[weap].anim+d->weapstate[weap])|ANIM_CLAMP;
                         break;
                     }
                     case W_S_RELOAD:
                     {
-                        if(!d->hasweap(weap, m_weapon(gamemode, mutators))) showweap = false;// || (!d->canreload(weap, m_weapon(gamemode, mutators), false, lastmillis) && lastmillis-d->weaplast[weap] <= d->weapwait[weap]/3))
+                        if(!d->hasweap(weap, m_weapon(gamemode, mutators))) showweap = false;
                         weapflags = animflags = weaptype[weap].anim+d->weapstate[weap];
                         break;
                     }
@@ -3176,14 +3252,13 @@ namespace game
                 animdelay = 300;
             }
         }
-        if(!early && third == 1 && d->type == ENT_PLAYER && !shadowmapping && !envmapping) renderabovehead(d, trans);
+        if(!early && third == 1 && d->actortype < A_ENEMY && !shadowmapping && !envmapping) renderabovehead(d, trans);
         const char *weapmdl = showweap && isweap(weap) ? (third ? weaptype[weap].vwep : weaptype[weap].hwep) : "";
         int ai = 0;
-#ifdef VANITY
         modelattach a[1+VANITYMAX+12];
         if(vanitymodels && third && *d->vanity)
         {
-            int idx = third == 1 && (d->state == CS_DEAD || d->state == CS_WAITING) && d->headless && headlessmodels ? 3 : third;
+            int idx = third == 1 && (d->state == CS_DEAD || d->state == CS_WAITING) && d->headless && !nogore && headlessmodels ? 3 : third;
             if(d->vitems.empty())
             {
                 vector<char *> vanitylist;
@@ -3199,7 +3274,6 @@ namespace game
                 if(found[vanities[d->vitems[k]].type]) continue;
                 if(vanities[d->vitems[k]].cond&1 && idx == 2) continue;
                 if(vanities[d->vitems[k]].cond&2 && idx == 3) continue;
-                if(!client::haspriv(d, vanities[d->vitems[k]].priv)) continue;
                 const char *file = vanityfname(d, d->vitems[k]);
                 if(file)
                 {
@@ -3208,27 +3282,19 @@ namespace game
                 }
             }
         }
-#else
-        modelattach a[1+12];
-#endif
-#ifdef MEK
-        bool hasweapon = false; // TEMP
-#else
         bool hasweapon = showweap && *weapmdl;
-#endif
         if(hasweapon) a[ai++] = modelattach("tag_weapon", weapmdl, weapflags, weapaction, trans, weapscale*size);
         if(rendernormally && (early || d != focus))
         {
             if(third != 2)
             {
-                const char *muzzle = "tag_weapon";
-                if(hasweapon)
+                a[ai++] = modelattach(hasweapon ? "tag_muzzle" : "tag_weapon", &d->muzzle);
+                a[ai++] = modelattach("tag_weapon", &d->origin);
+                if(weaptype[weap].eject || weaptype[weap].tape)
                 {
-                    muzzle = "tag_muzzle";
-                    if(weaptype[weap].eject) a[ai++] = modelattach("tag_eject", &d->eject);
+                    a[ai++] = modelattach("tag_eject", &d->eject[0]);
+                    a[ai++] = modelattach("tag_eject2", &d->eject[1]);
                 }
-                a[ai++] = modelattach(muzzle, &d->muzzle);
-                a[ai++] = modelattach("tag_weapon", &d->origin);
             }
             if(third && d->wantshitbox())
             {
@@ -3248,105 +3314,154 @@ namespace game
     void rendercheck(gameent *d, bool third = false)
     {
         d->checktags();
-        if(rendernormally)
-        {
-            float blend = opacity(d, thirdpersonview(true));
-            if(d->state == CS_ALIVE)
-            {
-                float minz = d == focus && !third && firstpersonbodyfeet >= 0 && d->wantshitbox() ? camera1->o.z-firstpersonbodyfeet : 0.f;
-                if(d->hasmelee(lastmillis, true, physics::sliding(d, true), d->physstate >= PHYS_SLOPE || d->onladder || physics::liquidcheck(d))) loopi(2)
-                {
-                    vec pos = d->toe[i];
-                    if(minz > 0 && pos.z > minz) pos.z -= pos.z-minz;
-                    float amt = 1-((lastmillis-d->weaplast[W_MELEE])/float(d->weapwait[W_MELEE]));
-                    part_create(PART_HINT, 1, pos, TEAM(d->team, colour), 2.f, amt*blend, 0, 0);
-                }
-                bool last = lastmillis-d->weaplast[d->weapselect] > 0,
-                     powering = last && d->weapstate[d->weapselect] == W_S_POWER,
-                     reloading = last && d->weapstate[d->weapselect] == W_S_RELOAD,
-                     secondary = physics::secondaryweap(d);
-                float amt = last ? (lastmillis-d->weaplast[d->weapselect])/float(d->weapwait[d->weapselect]) : 1.f;
-                int colour = WHCOL(d, d->weapselect, partcol, secondary);
-                if(d->weapselect == W_FLAMER && (!reloading || amt > 0.5f) && !physics::liquidcheck(d))
-                {
-                    float scale = powering ? 1.f+(amt*1.5f) : (d->weapstate[d->weapselect] == W_S_IDLE ? 1.f : (reloading ? (amt-0.5f)*2 : amt));
-                    part_create(PART_HINT, 1, d->ejectpos(d->weapselect), 0x1818A8, 0.75f*scale, min(0.65f*scale, 0.8f)*blend, 0, 0);
-                    part_create(PART_FIREBALL, 1, d->ejectpos(d->weapselect), colour, 0.5f*scale, min(0.75f*scale, 0.95f)*blend, 0, 0);
-                    regular_part_create(PART_FIREBALL, d->vel.magnitude() > 10 ? 30 : 75, d->ejectpos(d->weapselect), colour, 0.5f*scale, min(0.75f*scale, 0.95f)*blend, d->vel.magnitude() > 10 ? -40 : -10, 0);
-                }
-                if(W(d->weapselect, laser) && !reloading)
-                {
-                    vec v, origin = d->originpos(), muzzle = d->muzzlepos(d->weapselect);
-                    origin.z += 0.25f; muzzle.z += 0.25f;
-                    float yaw, pitch;
-                    vectoyawpitch(vec(muzzle).sub(origin).normalize(), yaw, pitch);
-                    findorientation(d->o, d->yaw, d->pitch, v);
-                    part_flare(origin, v, 1, PART_FLARE, colour, 0.5f*amt, amt*blend);
-                }
-                if(d->weapselect == W_SWORD || powering)
-                {
-                    static const struct powerfxs {
-                        int type, parttype;
-                        float size, radius;
-                    } powerfx[W_MAX] = {
-                        { 0, 0, 0, 0 },
-                        { 2, PART_SPARK, 0.1f, 1.5f },
-                        { 4, PART_LIGHTNING, 1, 1 },
-                        { 2, PART_SPARK, 0.15f, 2 },
-                        { 2, PART_SPARK, 0.1f, 2 },
-                        { 2, PART_FIREBALL, 0.1f, 6 },
-                        { 1, PART_PLASMA, 0.05f, 2 },
-                        { 2, PART_PLASMA, 0.05f, 2.5f },
-                        { 3, PART_PLASMA, 0.1f, 0.125f },
-                        { 0, 0, 0, 0 },
-                        { 0, 0, 0, 0 },
-                    };
-                    switch(powerfx[d->weapselect].type)
+        float blend = opacity(d, third);
+        if(d->state == CS_ALIVE)
+        {
+            bool useth = hud::teamhurthud&1 && hud::teamhurttime && m_team(gamemode, mutators) && focus == player1 &&
+                 d->team == player1->team && d->lastteamhit >= 0 && lastmillis-d->lastteamhit <= hud::teamhurttime,
+                 hashint = playerhint&(d->team != focus->team ? 2 : 1);
+            if(d->actortype < A_ENEMY && d != focus && (useth || hashint))
+            {
+                if(hashint)
+                {
+                    vec c = vec::hexcolor(getcolour(d, playerhinttone));
+                    float height = d->height, fade = blend;
+                    if(playerhintscale > 0)
                     {
-                        case 1: case 2:
+                        float per = d->health/float(m_health(gamemode, mutators, d->model));
+                        fade = (fade*(1.f-playerhintscale))+(fade*per*playerhintscale);
+                        if(fade > 1)
                         {
-                            regularshape(powerfx[d->weapselect].parttype, 1+(amt*powerfx[d->weapselect].radius), colour, powerfx[d->weapselect].type == 2 ? 21 : 53, 5, 60+int(30*amt), d->muzzlepos(d->weapselect), powerfx[d->weapselect].size*max(amt, 0.25f), max(amt*0.25f, 0.05f)*blend, 1, 0, 5+(amt*5));
-                            break;
+                            height *= 1.f+(fade-1.f);
+                            fade = 1;
                         }
-                        case 3:
-                        {
-                            int interval = lastmillis%1000;
-                            float fluc = powerfx[d->weapselect].size+(interval ? (interval <= 500 ? interval/500.f : (1000-interval)/500.f) : 0.f);
-                            part_create(powerfx[d->weapselect].parttype, 1, d->originpos(), colour, (powerfx[d->weapselect].radius*max(amt, 0.25f))+fluc, max(amt, 0.1f)*blend);
-                            break;
-                        }
-                        case 4:
-                        {
-                            part_flare(d->originpos(), d->muzzlepos(d->weapselect), 1, powerfx[d->weapselect].parttype, colour, W2(d->weapselect, partsize, secondary)*0.75f, blend);
-                            break;
-                        }
-                        case 0: default: break;
                     }
+                    if(d->state == CS_ALIVE && d->lastbuff)
+                    {
+                        int millis = lastmillis%1000;
+                        float amt = millis <= 500 ? 1.f-(millis/500.f) : (millis-500)/500.f;
+                        flashcolour(c.r, c.g, c.b, 1.f, 1.f, 1.f, amt);
+                        height += height*amt*0.1f;
+                    }
+                    vec o = d->center(), offset = vec(o).sub(camera1->o).rescale(d->radius/2);
+                    offset.z = max(offset.z, -1.0f);
+                    offset.add(o);
+                    part_create(PART_HINT_BOLD_SOFT, 1, offset, c.tohexcolor(), clamp(height*playerhintsize, 1.f, playerhintmaxsize), fade*playerhintblend*camera1->o.distrange(o, playerhintfadeat, playerhintfadecut));
+                }
+                if(useth)
+                {
+                    vec c(0.25f, 0.25f, 0.25f);
+                    int millis = lastmillis%500;
+                    float amt = millis <= 250 ? 1.f-(millis/250.f) : (millis-250)/250.f, height = d->height*0.5f;
+                    flashcolour(c.r, c.g, c.b, 1.f, 0.f, 0.f, amt);
+                    if(playerhinthurtthrob) height += height*amt*0.1f;
+                    vec o = d->center(), offset = vec(o).sub(camera1->o).rescale(-d->radius);
+                    offset.z = max(offset.z, -1.0f);
+                    offset.add(o);
+                    part_icon(offset, textureload(hud::warningtex, 3, true, false), height*playerhinthurtsize, amt*blend*playerhinthurtblend, 0, 0, 1, c.tohexcolor());
                 }
-                if(d->turnside || d->impulse[IM_JUMP] || physics::sliding(d)) impulseeffect(d, 1);
-                if(physics::jetpack(d)) impulseeffect(d, 2);
             }
-            if(burntime && d->burning(lastmillis, burntime))
+            float minz = d == focus && !third && firstpersonbodyfeet >= 0 && d->wantshitbox() ? camera1->o.z-firstpersonbodyfeet : 0.f;
+            if(d->hasmelee(lastmillis, true, d->sliding(true), d->physstate >= PHYS_SLOPE || d->onladder || physics::liquidcheck(d))) loopi(2)
             {
-                int millis = lastmillis-d->lastres[WR_BURN];
-                float pc = 1, intensity = 0.5f+(rnd(50)/100.f), fade = (d != focus ? 0.5f : 0.f)+(rnd(50)/100.f);
-                if(burntime-millis < burndelay) pc *= float(burntime-millis)/float(burndelay);
-                else pc *= 0.75f+(float(millis%burndelay)/float(burndelay*4));
-                vec pos = vec(d->center()).sub(vec(rnd(11)-5, rnd(11)-5, rnd(5)-2).mul(pc));
-                regular_part_create(PART_FIREBALL, 50, pos, pulsecols[PULSE_FIRE][rnd(PULSECOLOURS)], d->height*0.75f*intensity*pc, fade*blend*pc, -10, 0);
+                vec pos = d->footpos(i);
+                if(minz > 0 && pos.z > minz) pos.z -= pos.z-minz;
+                float amt = 1-((lastmillis-d->weaplast[W_MELEE])/float(d->weapwait[W_MELEE]));
+                part_create(PART_HINT, 1, pos, TEAM(d->team, colour), 2.f, amt*blend, 0, 0);
             }
-            if(shocktime && d->shocking(lastmillis, shocktime))
+            int millis = lastmillis-d->weaplast[d->weapselect];
+            bool last = millis > 0 && millis < d->weapwait[d->weapselect],
+                 powering = last && d->weapstate[d->weapselect] == W_S_POWER,
+                 reloading = last && d->weapstate[d->weapselect] == W_S_RELOAD,
+                 secondary = physics::secondaryweap(d);
+            float amt = last ? (lastmillis-d->weaplast[d->weapselect])/float(d->weapwait[d->weapselect]) : 1.f;
+            int colour = WHCOL(d, d->weapselect, partcol, secondary);
+            if(d->weapselect == W_FLAMER && (!reloading || amt > 0.5f) && !physics::liquidcheck(d))
+            {
+                float scale = powering ? 1.f+(amt*1.5f) : (d->weapstate[d->weapselect] == W_S_IDLE ? 1.f : (reloading ? (amt-0.5f)*2 : amt));
+                part_create(PART_HINT, 1, d->ejectpos(d->weapselect), 0x1818A8, 0.75f*scale, min(0.65f*scale, 0.8f)*blend, 0, 0);
+                part_create(PART_FIREBALL, 1, d->ejectpos(d->weapselect), colour, 0.5f*scale, min(0.75f*scale, 0.95f)*blend, 0, 0);
+                regular_part_create(PART_FIREBALL, d->vel.magnitude() > 10 ? 30 : 75, d->ejectpos(d->weapselect), colour, 0.5f*scale, min(0.75f*scale, 0.95f)*blend, d->vel.magnitude() > 10 ? -40 : -10, 0);
+            }
+            if(W(d->weapselect, laser) && !reloading)
+            {
+                vec v, origin = d->originpos(), muzzle = d->muzzlepos(d->weapselect);
+                origin.z += 0.25f; muzzle.z += 0.25f;
+                float yaw, pitch;
+                vectoyawpitch(vec(muzzle).sub(origin).normalize(), yaw, pitch);
+                findorientation(d->o, d->yaw, d->pitch, v);
+                part_flare(origin, v, 1, PART_FLARE, colour, 0.5f*amt, amt*blend);
+            }
+            if(d->weapselect == W_SWORD || d->weapselect == W_ZAPPER || powering)
+            {
+                static const struct powerfxs {
+                    int type, parttype;
+                    float size, radius;
+                } powerfx[W_MAX] = {
+                    { 0, 0, 0, 0 },
+                    { 2, PART_SPARK, 0.1f, 1.5f },
+                    { 4, PART_LIGHTNING, 2.f, 1 },
+                    { 2, PART_SPARK, 0.15f, 2 },
+                    { 2, PART_SPARK, 0.1f, 2 },
+                    { 2, PART_FIREBALL, 0.1f, 6 },
+                    { 1, PART_PLASMA, 0.05f, 2 },
+                    { 4, PART_LIGHTZAP, 0.75f, 1 },
+                    { 2, PART_PLASMA, 0.05f, 2.5f },
+                    { 3, PART_PLASMA, 0.1f, 0.125f },
+                    { 0, 0, 0, 0 },
+                    { 0, 0, 0, 0 },
+                };
+                switch(powerfx[d->weapselect].type)
+                {
+                    case 1: case 2:
+                    {
+                        regularshape(powerfx[d->weapselect].parttype, 1+(amt*powerfx[d->weapselect].radius), colour, powerfx[d->weapselect].type == 2 ? 21 : 53, 5, 60+int(30*amt), d->muzzlepos(d->weapselect), powerfx[d->weapselect].size*max(amt, 0.25f), max(amt, 0.1f)*blend, 1, 0, 5+(amt*5));
+                        break;
+                    }
+                    case 3:
+                    {
+                        int interval = lastmillis%1000;
+                        float fluc = powerfx[d->weapselect].size+(interval ? (interval <= 500 ? interval/500.f : (1000-interval)/500.f) : 0.f);
+                        part_create(powerfx[d->weapselect].parttype, 1, d->originpos(), colour, (powerfx[d->weapselect].radius*max(amt, 0.25f))+fluc, max(amt, 0.1f)*blend);
+                        break;
+                    }
+                    case 4:
+                    {
+                        part_flare(d->ejectpos(d->weapselect), d->ejectpos(d->weapselect, true), 1, powerfx[d->weapselect].parttype, colour, powerfx[d->weapselect].size*max(amt, 0.25f), max(amt, 0.1f)*blend);
+                        break;
+                    }
+                    case 0: default: break;
+                }
+            }
+            if(d->turnside || d->impulse[IM_JUMP] || d->sliding()) impulseeffect(d, 1);
+        }
+        if(burntime && d->burning(lastmillis, burntime))
+        {
+            int millis = lastmillis-d->lastres[WR_BURN];
+            float pc = 1, intensity = 0.5f+(rnd(50)/100.f), fade = (d != focus ? 0.5f : 0.f)+(rnd(50)/100.f);
+            if(burntime-millis < burndelay) pc *= float(burntime-millis)/float(burndelay);
+            else pc *= 0.75f+(float(millis%burndelay)/float(burndelay*4));
+            vec pos = vec(d->center()).sub(vec(rnd(11)-5, rnd(11)-5, rnd(5)-2).mul(pc));
+            regular_part_create(PART_FIREBALL, 50, pos, pulsecols[PULSE_FIRE][rnd(PULSECOLOURS)], d->height*0.75f*intensity*blend*pc, fade*blend*pc, -10, 0);
+        }
+        if(shocktime && d->shocking(lastmillis, shocktime))
+        {
+            vec origin = d->center(), col = rescolour(d, PULSE_SHOCK), rad = vec(d->xradius, d->yradius, d->height/(d->state == CS_ALIVE ? 2 : 3)).mul(blend);
+            int millis = lastmillis-d->lastres[WR_SHOCK], colour = (int(col.x*255)<<16)|(int(col.y*255)<<8)|(int(col.z*255));
+            float pc = shocktime-millis < shockdelay ? (shocktime-millis)/float(shockdelay) : 0.5f+(float(millis%shockdelay)/float(shockdelay*4)), fade = (d != focus ? 0.5f : 0.f)+(pc*0.5f);
+            loopi(10+rnd(10))
             {
-                vec origin = d->center(), col = rescolour(d, PULSE_SHOCK);
-                int millis = lastmillis-d->lastres[WR_SHOCK], colour = (int(col.x*255)<<16)|(int(col.y*255)<<8)|(int(col.z*255));
-                float pc = shocktime-millis < shockdelay ? (shocktime-millis)/float(shockdelay) : 0.5f+(float(millis%shockdelay)/float(shockdelay*4));
-                loopi(10+rnd(20))
+                float q = 0.75f;
+                vec from = vec(origin).add(vec(rnd(201)-100, rnd(201)-100, rnd(201)-100).div(100.f).normalize().mul(rad).mul(rnd(200)/100.f)), to = from;
+                loopj(1+rnd(10))
                 {
-                    vec from = vec(origin).add(vec((rnd(201)-100)/100.f, (rnd(201)-100)/100.f, (rnd(201)-100)/100.f).normalize().mul(vec(d->xradius, d->yradius, d->height).mul(0.35f*pc))),
-                        to = vec(origin).add(vec((rnd(201)-100)/100.f, (rnd(201)-100)/100.f, (rnd(201)-100)/100.f).normalize().mul(d->height*pc*rnd(100)/80.f));
-                    part_flare(from, to, 1, PART_LIGHTNING_FLARE, colour, 0.1f+pc, 0.25f+pc*0.5f);
+                    to = vec(from).add(vec(rnd(201)-100, rnd(201)-100, rnd(201)-100).div(100.f).normalize().mul(rad).mul(rnd(200)/100.f*q));
+                    part_flare(from, to, 1, PART_LIGHTNING_FLARE, colour, q, fade*blend*q);
+                    from = to;
+                    q = q*0.75f;
                 }
             }
+            if(rendernormally && d->ragdoll && twitchspeed > 0) twitchragdoll(d, twitchspeed*pc*blend*rnd(100)/80.f);
         }
     }
 
@@ -3393,14 +3508,16 @@ namespace game
             previewent->state = CS_ALIVE;
             previewent->physstate = PHYS_FLOOR;
             previewent->o = vec(0, 0.75f*(previewent->height + previewent->aboveeye), previewent->height - (previewent->height + previewent->aboveeye)/2);
-            previewent->spawnstate(G_DEATHMATCH, 0);
+            previewent->spawnstate(G_DEATHMATCH, 0, -1, m_health(G_DEATHMATCH, 0, 0));
             previewent->light.color = vec(1, 1, 1);
             previewent->light.dir = vec(0, -1, 2).normalize();
-            loopi(W_MAX) previewent->ammo[i] = W(i, max);
+            loopi(W_MAX) previewent->ammo[i] = W(i, ammomax);
         }
-        previewent->setinfo(NULL, color, model, vanity);
+        previewent->colour = color;
+        previewent->model = model;
         previewent->team = clamp(team, 0, int(T_MULTI));
         previewent->weapselect = clamp(weap, 0, W_MAX-1);
+        previewent->setvanity(vanity);
         previewent->yaw = fmod(lastmillis/10000.0f*360.0f, 360.0f);
         previewent->light.millis = -1;
         renderplayer(previewent, 1, blend, scale);
diff --git a/src/game/game.h b/src/game/game.h
index a973db1..3389aed 100644
--- a/src/game/game.h
+++ b/src/game/game.h
@@ -3,43 +3,32 @@
 
 #include "engine.h"
 
-#if defined(FPS)
-#define GAMEID              "fps"
-#define GAMEVERSION         220
-#define DEMO_MAGIC          "RED_ECLIPSE_DEMO"
-#define VANITY              1
-#elif defined(MEK)
-#define GAMEID              "mek"
-#define GAMEVERSION         220
-#define DEMO_MAGIC          "MEK_ARCADE_DEMO"
-#define CAMPAIGN            1
-#else
-#error "You must add either -DFPS or -DMEK"
-#endif
-#define DEMO_VERSION        GAMEVERSION
+#define VERSION_GAMEID "fps"
+#define VERSION_GAME 226
+#define VERSION_DEMOMAGIC "RED_ECLIPSE_DEMO"
 
 #define MAXAI 256
-#define MAXPLAYERS (MAXCLIENTS + MAXAI)
+#define MAXPLAYERS (MAXCLIENTS + MAXAI*2)
+#define MAXPARAMS 256
 
 // network quantization scale
-#define DMF 16.0f           // for world locations
-#define DNF 1000.0f         // for normalized vectors
-#define DVELF 1.0f          // for playerspeed based velocity vectors
+#define DMF 16.0f // for world locations
+#define DNF 1000.0f // for normalized vectors
+#define DVELF 1.0f // for playerspeed based velocity vectors
 
 enum
 {
-    S_JUMP = S_GAMESPECIFIC, S_IMPULSE, S_JET, S_LAND, S_PAIN, S_DEATH,
-    S_SPLASH1, S_SPLASH2, S_UNDERWATER, S_SPLOSH, S_DEBRIS, S_BURNLAVA, S_BURNING,
+    S_JUMP = S_GAMESPECIFIC, S_IMPULSE, S_LAND, S_FOOTSTEP, S_SWIMSTEP, S_PAIN, S_DEATH,
+    S_SPLASH1, S_SPLASH2, S_SPLOSH, S_DEBRIS, S_BURNLAVA,
     S_EXTINGUISH, S_SHELL, S_ITEMUSE, S_ITEMSPAWN,
     S_REGEN, S_DAMAGE, S_DAMAGE2, S_DAMAGE3, S_DAMAGE4, S_DAMAGE5, S_DAMAGE6, S_DAMAGE7, S_DAMAGE8,
-    S_BURNED, S_BLEED, S_SHOCK, S_RESPAWN, S_CHAT, S_ERROR, S_ALARM, S_CATCH,
+    S_BURNED, S_BLEED, S_SHOCK, S_RESPAWN, S_CHAT, S_ERROR, S_ALARM, S_CATCH, S_BOUNCE,
     S_V_FLAGSECURED, S_V_FLAGOVERTHROWN, S_V_FLAGPICKUP, S_V_FLAGDROP, S_V_FLAGRETURN, S_V_FLAGSCORE, S_V_FLAGRESET,
     S_V_BOMBSTART, S_V_BOMBDUEL, S_V_BOMBPICKUP, S_V_BOMBSCORE, S_V_BOMBRESET,
-    S_V_NOTIFY, S_V_FIGHT, S_V_CHECKPOINT, S_V_OVERTIME, S_V_ONEMINUTE, S_V_HEADSHOT,
+    S_V_NOTIFY, S_V_FIGHT, S_V_START, S_V_CHECKPOINT, S_V_COMPLETE, S_V_OVERTIME, S_V_ONEMINUTE, S_V_HEADSHOT,
     S_V_SPREE, S_V_SPREE2, S_V_SPREE3, S_V_SPREE4, S_V_MULTI, S_V_MULTI2, S_V_MULTI3,
     S_V_REVENGE, S_V_DOMINATE, S_V_FIRSTBLOOD, S_V_BREAKER,
-    S_V_YOUWIN, S_V_YOULOSE, S_V_DRAW,
-    S_V_FRAGGED, S_V_BALWARN, S_V_BALALERT,
+    S_V_YOUWIN, S_V_YOULOSE, S_V_DRAW, S_V_FRAGGED, S_V_BALWARN, S_V_BALALERT,
     S_GAME
 };
 
@@ -48,11 +37,7 @@ enum                                // entity types
     NOTUSED = ET_EMPTY, LIGHT = ET_LIGHT, MAPMODEL = ET_MAPMODEL, PLAYERSTART = ET_PLAYERSTART, ENVMAP = ET_ENVMAP, PARTICLES = ET_PARTICLES,
     MAPSOUND = ET_SOUND, LIGHTFX = ET_LIGHTFX, SUNLIGHT = ET_SUNLIGHT, WEAPON = ET_GAMESPECIFIC,
     TELEPORT, ACTOR, TRIGGER, PUSHER, AFFINITY, CHECKPOINT,
-#ifdef MEK
-    HEALTH, ARMOUR,
-#else
-    DUMMY1, DUMMY2,
-#endif
+    ROUTE, UNUSEDENT,
     MAXENTTYPES
 };
 
@@ -63,43 +48,39 @@ enum { TA_MANUAL = 0, TA_AUTO, TA_ACTION, TA_MAX };
 #define TRIGGERIDS      16
 #define TRIGSTATE(a,b)  (b%2 ? !a : a)
 
-enum { CP_RESPAWN = 0, CP_START, CP_FINISH, CP_LAST, CP_MAX };
-#ifdef MEK
-enum { HEALTH_SMALL = 0, HEALTH_REGULAR, HEALTH_LARGE, HEALTH_MAX };
-enum { ARMOUR_SMALL = 0, ARMOUR_REGULAR, ARMOUR_LARGE, ARMOUR_MAX };
-#endif
+enum { CP_RESPAWN = 0, CP_START, CP_FINISH, CP_LAST, CP_MAX, CP_ALL = (1<<CP_RESPAWN)|(1<<CP_START)|(1<<CP_FINISH)|(1<<CP_LAST) };
 
 enum { TELE_NOAFFIN = 0, TELE_MAX };
 
 struct enttypes
 {
-    int type,           priority, links,    radius, usetype,    numattrs,   modesattr,
+    int type,           priority, links,    radius, usetype,    numattrs,   modesattr,  idattr,
             canlink, reclink, canuse;
     bool    noisy,  syncs,  resyncs,    syncpos,    synckin;
-    const char *name,           *attrs[11];
+    const char *name,           *attrs[12];
 };
 #ifdef GAMESERVER
 enttypes enttype[] = {
     {
-        NOTUSED,        -1,         0,      0,      EU_NONE,    0,          -1,
+        NOTUSED,        -1,         0,      0,      EU_NONE,    0,          -1,         -1,
             0, 0, 0,
             true,   false,  false,      false,      false,
                 "none",         { "" }
     },
     {
-        LIGHT,          1,          59,     0,      EU_NONE,    5,          -1,
+        LIGHT,          1,          59,     0,      EU_NONE,    5,          -1,         -1,
             (1<<LIGHTFX), (1<<LIGHTFX), 0,
             false,  false,  false,      false,      false,
                 "light",        { "radius", "red",      "green",    "blue",     "flare"  }
     },
     {
-        MAPMODEL,       1,          58,     0,      EU_NONE,    10,         -1,
+        MAPMODEL,       1,          58,     0,      EU_NONE,    10,         -1,         -1,
             (1<<TRIGGER), (1<<TRIGGER), 0,
             false,  false,  false,      false,      false,
                 "mapmodel",     { "type",   "yaw",      "pitch",    "roll",     "blend",    "scale",    "flags",    "colour",   "palette",  "palindex" }
     },
     {
-        PLAYERSTART,    1,          59,     0,      EU_NONE,    6,          3,
+        PLAYERSTART,    1,          59,     0,      EU_NONE,    6,          3,          5,
             (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             0,
@@ -107,50 +88,50 @@ enttypes enttype[] = {
                 "playerstart",  { "team",   "yaw",      "pitch",    "modes",    "muts",     "id" }
     },
     {
-        ENVMAP,         1,          0,      0,      EU_NONE,    3,          -1,
+        ENVMAP,         1,          0,      0,      EU_NONE,    3,          -1,         -1,
             0, 0, 0,
             false,  false,  false,      false,      false,
                 "envmap",       { "radius", "size", "blur" }
     },
     {
-        PARTICLES,      1,          59,     0,      EU_NONE,    11,         -1,
-            (1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
-            (1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
+        PARTICLES,      1,          59,     0,      EU_NONE,    12,         -1,         -1,
+            (1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<CHECKPOINT),
+            (1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<CHECKPOINT),
             0,
             false,  false,  false,      false,      false,
-                "particles",    { "type",   "a",        "b",        "c",        "d",        "e",        "f",        "g",        "i",        "j",        "k" }
+                "particles",    { "type",   "a",        "b",        "c",        "d",        "e",        "f",        "g",        "i",        "j",        "k",        "j" }
     },
     {
-        MAPSOUND,       1,          58,     0,      EU_NONE,    5,          -1,
-            (1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
-            (1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<AFFINITY)|(1<<CHECKPOINT),
+        MAPSOUND,       1,          58,     0,      EU_NONE,    5,          -1,         -1,
+            (1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<CHECKPOINT),
+            (1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<CHECKPOINT),
             0,
             false,  false,  false,      false,      false,
                 "sound",        { "type",   "maxrad",   "minrad",   "volume",   "flags" }
     },
     {
-        LIGHTFX,        1,          1,      0,      EU_NONE,    5,          -1,
-            (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),
+        LIGHTFX,        1,          1,      0,      EU_NONE,    5,          -1,         -1,
+            (1<<LIGHT)|(1<<TELEPORT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<CHECKPOINT),
+            (1<<LIGHT)|(1<<TRIGGER)|(1<<PUSHER)|(1<<PLAYERSTART)|(1<<CHECKPOINT),
             0,
             false,  false,  false,      false,      false,
                 "lightfx",      { "type",   "mod",      "min",      "max",      "flags" }
     },
     {
-        SUNLIGHT,       1,          160,    0,      EU_NONE,    7,          -1,
+        SUNLIGHT,       1,          160,    0,      EU_NONE,    7,          -1,         -1,
             0, 0, 0,
             false,  false,  false,      false,      false,
                 "sunlight",     { "yaw",    "pitch",    "red",      "green",    "blue",     "offset",   "flare" }
     },
     {
-        WEAPON,         2,          59,     24,     EU_ITEM,    5,          2,
+        WEAPON,         2,          59,     24,     EU_ITEM,    5,          2,          4,
             0, 0,
             (1<<ENT_PLAYER)|(1<<ENT_AI),
             false,  true,   true,      false,      false,
                 "weapon",       { "type",   "flags",    "modes",    "muts",     "id" }
     },
     {
-        TELEPORT,       1,          50,     12,     EU_AUTO,    9,          -1,
+        TELEPORT,       1,          50,     12,     EU_AUTO,    9,          -1,         -1,
             (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX)|(1<<TELEPORT),
             (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             (1<<ENT_PLAYER)|(1<<ENT_AI)|(1<<ENT_PROJ),
@@ -158,13 +139,13 @@ enttypes enttype[] = {
                 "teleport",     { "yaw",    "pitch",    "push",     "radius",   "colour",   "type",     "palette",  "palindex", "flags" }
     },
     {
-        ACTOR,          1,          59,     0,      EU_NONE,    10,         3,
-            (1<<AFFINITY), 0, 0,
+        ACTOR,          1,          59,     0,      EU_NONE,    10,         3,          5,
+            0, 0, 0,
             false,  true,   false,      true,       false,
                 "actor",        { "type",   "yaw",      "pitch",    "modes",    "muts",     "id",       "weap",     "health",   "speed",    "scale" }
     },
     {
-        TRIGGER,        1,          58,     16,     EU_AUTO,    7,          5,
+        TRIGGER,        1,          58,     16,     EU_AUTO,    7,          5,          -1,
             (1<<MAPMODEL)|(1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             (1<<MAPMODEL)|(1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             (1<<ENT_PLAYER)|(1<<ENT_AI),
@@ -172,7 +153,7 @@ enttypes enttype[] = {
                 "trigger",      { "id",     "type",     "action",   "radius",   "state",    "modes",    "muts" }
     },
     {
-        PUSHER,         1,          58,     12,     EU_AUTO,    6,          -1,
+        PUSHER,         1,          58,     12,     EU_AUTO,    6,          -1,         -1,
             (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             (1<<ENT_PLAYER)|(1<<ENT_AI)|(1<<ENT_PROJ),
@@ -180,59 +161,34 @@ enttypes enttype[] = {
                 "pusher",       { "yaw",    "pitch",    "force",    "maxrad",   "minrad",   "type" }
     },
     {
-        AFFINITY,       1,          48,     32,     EU_NONE,    7,          3,
-            (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
-            (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
-            0,
+        AFFINITY,       1,          48,     32,     EU_NONE,    6,          3,          5,
+            0, 0, 0,
             false,  false,  false,      false,      false,
                 "affinity",     { "team",   "yaw",      "pitch",    "modes",    "muts",     "id" }
     },
     {
-        CHECKPOINT,     1,          48,     16,     EU_AUTO,    7,          3,
+        CHECKPOINT,     1,          48,     16,     EU_AUTO,    7,          3,          5,
             (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             (1<<MAPSOUND)|(1<<PARTICLES)|(1<<LIGHTFX),
             (1<<ENT_PLAYER)|(1<<ENT_AI),
             false,  true,   false,      true,       false,
                 "checkpoint",   { "radius", "yaw",      "pitch",    "modes",    "muts",     "id",       "type" }
     },
-#ifdef MEK
     {
-        HEALTH,         2,          59,     24,     EU_ITEM,    4,          1,
-            0, 0, 0,
-            (1<<ENT_PLAYER)|(1<<ENT_AI),
-            false,  true,   true,      false,      false,
-                "health",       { "type",   "modes",    "muts",     "id" }
+        ROUTE,          1,         224,      16,    EU_NONE,    6,          -1,         -1,
+            (1<<ROUTE), 0, 0,
+            false,   false,  false,      false,      false,
+                "route",         { "num",   "yaw",      "pitch",    "move",     "strafe",   "action" }
     },
     {
-        ARMOUR,         2,          59,     24,     EU_ITEM,    4,          1,
-            0, 0, 0,
-            (1<<ENT_PLAYER)|(1<<ENT_AI),
-            false,  true,   true,      false,      false,
-                "armour",       { "type",   "modes",    "muts",     "id" }
-    }
-#else
-    {
-        DUMMY1,         1,          48,     0,      EU_NONE,    4,          -1,
+        UNUSEDENT,      -1,          0,      0,     EU_NONE,    0,          -1,         -1,
             0, 0, 0,
             true,   false,  false,      false,      false,
-                "dummy1",       { "" }
-    },
-    {
-        DUMMY2,         0,          1,      16,     EU_NONE,    2,          -1,
-            0, 0, 0,
-            true,   false,  false,      false,      false,
-                "dummy2",     { "" }
+                "unused",         { "" }
     }
-#endif
 };
-#ifdef MEK
-int healthamt[HEALTH_MAX] = { 25, 50, 100 }, armouramt[ARMOUR_MAX] = { 25, 50, 100 };
-#endif
 #else
 extern enttypes enttype[];
-#ifdef MEK
-extern int healthamt[HEALTH_MAX], armouramt[ARMOUR_MAX];
-#endif
 #endif
 
 enum
@@ -241,7 +197,6 @@ enum
     ANIM_JUMP_FORWARD, ANIM_JUMP_BACKWARD, ANIM_JUMP_LEFT, ANIM_JUMP_RIGHT, ANIM_JUMP,
     ANIM_IMPULSE_FORWARD, ANIM_IMPULSE_BACKWARD, ANIM_IMPULSE_LEFT, ANIM_IMPULSE_RIGHT,
     ANIM_DASH_FORWARD, ANIM_DASH_BACKWARD, ANIM_DASH_LEFT, ANIM_DASH_RIGHT, ANIM_DASH_UP,
-    ANIM_JET_FORWARD, ANIM_JET_BACKWARD, ANIM_JET_LEFT, ANIM_JET_RIGHT, ANIM_JET_UP,
     ANIM_WALL_RUN_LEFT, ANIM_WALL_RUN_RIGHT, ANIM_WALL_JUMP, ANIM_POWERSLIDE, ANIM_FLYKICK,
     ANIM_SINK, ANIM_EDIT, ANIM_WIN, ANIM_LOSE,
     ANIM_CROUCH, ANIM_CRAWL_FORWARD, ANIM_CRAWL_BACKWARD, ANIM_CRAWL_LEFT, ANIM_CRAWL_RIGHT,
@@ -253,6 +208,7 @@ enum
     ANIM_SMG, ANIM_SMG_PRIMARY, ANIM_SMG_SECONDARY, ANIM_SMG_RELOAD,
     ANIM_FLAMER, ANIM_FLAMER_PRIMARY, ANIM_FLAMER_SECONDARY, ANIM_FLAMER_RELOAD,
     ANIM_PLASMA, ANIM_PLASMA_PRIMARY, ANIM_PLASMA_SECONDARY, ANIM_PLASMA_RELOAD,
+    ANIM_ZAPPER, ANIM_ZAPPER_PRIMARY, ANIM_ZAPPER_SECONDARY, ANIM_ZAPPER_RELOAD,
     ANIM_RIFLE, ANIM_RIFLE_PRIMARY, ANIM_RIFLE_SECONDARY, ANIM_RIFLE_RELOAD,
     ANIM_GRENADE, ANIM_GRENADE_PRIMARY, ANIM_GRENADE_SECONDARY, ANIM_GRENADE_RELOAD, ANIM_GRENADE_POWER,
     ANIM_MINE, ANIM_MINE_PRIMARY, ANIM_MINE_SECONDARY, ANIM_MINE_RELOAD,
@@ -261,15 +217,17 @@ enum
     ANIM_MAX
 };
 
-#ifndef GAMESERVER
-enum { PULSE_FIRE = 0, PULSE_BURN, PULSE_DISCO, PULSE_SHOCK, PULSE_MAX };
+enum { PULSE_FIRE = 0, PULSE_BURN, PULSE_DISCO, PULSE_SHOCK, PULSE_MAX, PULSE_LAST = PULSE_MAX-1 };
 #define PULSECOLOURS 8
-const int pulsecols[PULSE_MAX][PULSECOLOURS] = {
+#ifdef GAMESERVER
+int pulsecols[PULSE_MAX][PULSECOLOURS] = {
     { 0xFF5808, 0x981808, 0x782808, 0x481808, 0x983818, 0x681808, 0xC81808, 0x381808 },
     { 0xFFC848, 0xF86838, 0xA85828, 0xA84838, 0xF8A858, 0xC84828, 0xF86848, 0xA89858 },
     { 0xFF8888, 0xFFAA88, 0xFFFF88, 0x88FF88, 0x88FFFF, 0x8888FF, 0xFF88FF, 0xFFFFFF },
     { 0xAA88FF, 0xAA88FF, 0xAAAAFF, 0x44AAFF, 0x88AAFF, 0x4444FF, 0xAA44FF, 0xFFFFFF }
 };
+#else
+extern int pulsecols[PULSE_MAX][PULSECOLOURS];
 #endif
 
 #include "gamemode.h"
@@ -300,7 +258,7 @@ enum
     N_SHOOT, N_DESTROY, N_STICKY, N_SUICIDE, N_DIED, N_POINTS, N_DAMAGE, N_SHOTFX,
     N_LOADW, N_TRYSPAWN, N_SPAWNSTATE, N_SPAWN, N_DROP, N_WSELECT,
     N_MAPCHANGE, N_MAPVOTE, N_CLEARVOTE, N_CHECKPOINT, N_ITEMSPAWN, N_ITEMUSE, N_TRIGGER, N_EXECLINK,
-    N_PING, N_PONG, N_CLIENTPING, N_TICK, N_NEWGAME, N_ITEMACC, N_SERVMSG, N_GAMEINFO, N_RESUME,
+    N_PING, N_PONG, N_CLIENTPING, N_TICK, N_ITEMACC, N_SERVMSG, N_GAMEINFO, N_RESUME,
     N_EDITMODE, N_EDITENT, N_EDITLINK, N_EDITVAR, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, N_DELCUBE, N_REMIP, N_CLIPBOARD, N_NEWMAP,
     N_GETMAP, N_SENDMAP, N_FAILMAP, N_SENDMAPFILE,
     N_MASTERMODE, N_ADDCONTROL, N_CLRCONTROL, N_CURRENTPRIV, N_SPECTATOR, N_WAITING, N_SETPRIV, N_SETTEAM,
@@ -308,9 +266,7 @@ enum
     N_TAKEAFFIN, N_RETURNAFFIN, N_RESETAFFIN, N_DROPAFFIN, N_SCOREAFFIN, N_INITAFFIN, N_SCORE,
     N_LISTDEMOS, N_SENDDEMOLIST, N_GETDEMO, N_SENDDEMO,
     N_DEMOPLAYBACK, N_RECORDDEMO, N_STOPDEMO, N_CLEARDEMOS,
-    N_CLIENT, N_RELOAD, N_REGEN,
-    N_ADDBOT, N_DELBOT, N_INITAI,
-    N_MAPCRC, N_CHECKMAPS,
+    N_CLIENT, N_RELOAD, N_REGEN, N_INITAI, N_MAPCRC, N_CHECKMAPS,
     N_SETPLAYERINFO, N_SWITCHTEAM,
     N_AUTHTRY, N_AUTHCHAL, N_AUTHANS,
     NUMMSG
@@ -323,12 +279,12 @@ char msgsizelookup(int msg)
     {
         N_CONNECT, 0, N_SERVERINIT, 5, N_WELCOME, 1, N_CLIENTINIT, 0, N_POS, 0, N_SPHY, 0, N_TEXT, 0, N_COMMAND, 0,
         N_ANNOUNCE, 0, N_DISCONNECT, 3,
-        N_SHOOT, 0, N_DESTROY, 0, N_STICKY, 0, N_SUICIDE, 4, N_DIED, 0, N_POINTS, 4, N_DAMAGE, 10, N_SHOTFX, 0,
+        N_SHOOT, 0, N_DESTROY, 0, N_STICKY, 0, N_SUICIDE, 4, N_DIED, 0, N_POINTS, 4, N_DAMAGE, 14, N_SHOTFX, 0,
         N_LOADW, 0, N_TRYSPAWN, 2, N_SPAWNSTATE, 0, N_SPAWN, 0,
         N_DROP, 0, N_WSELECT, 0,
         N_MAPCHANGE, 0, N_MAPVOTE, 0, N_CLEARVOTE, 0, N_CHECKPOINT, 0, N_ITEMSPAWN, 3, N_ITEMUSE, 0, N_TRIGGER, 0, N_EXECLINK, 3,
         N_PING, 2, N_PONG, 2, N_CLIENTPING, 2,
-        N_TICK, 2, N_NEWGAME, 1, N_ITEMACC, 0,
+        N_TICK, 3, N_ITEMACC, 0,
         N_SERVMSG, 0, N_GAMEINFO, 0, N_RESUME, 0,
         N_EDITMODE, 2, N_EDITENT, 0, N_EDITLINK, 4, N_EDITVAR, 0, N_EDITF, 16, N_EDITT, 16, N_EDITM, 17, N_FLIP, 14, N_COPY, 14, N_PASTE, 14, N_ROTATE, 15, N_REPLACE, 17, N_DELCUBE, 14, N_REMIP, 1, N_NEWMAP, 2,
         N_GETMAP, 0, N_SENDMAP, 0, N_FAILMAP, 0, N_SENDMAPFILE, 0,
@@ -337,9 +293,7 @@ char msgsizelookup(int msg)
         N_DROPAFFIN, 0, N_SCOREAFFIN, 0, N_RETURNAFFIN, 0, N_TAKEAFFIN, 0, N_RESETAFFIN, 0, N_INITAFFIN, 0, N_SCORE, 0,
         N_LISTDEMOS, 1, N_SENDDEMOLIST, 0, N_GETDEMO, 2, N_SENDDEMO, 0,
         N_DEMOPLAYBACK, 3, N_RECORDDEMO, 2, N_STOPDEMO, 1, N_CLEARDEMOS, 2,
-        N_CLIENT, 0, N_RELOAD, 0, N_REGEN, 0,
-        N_ADDBOT, 0, N_DELBOT, 0, N_INITAI, 0,
-        N_MAPCRC, 0, N_CHECKMAPS, 1,
+        N_CLIENT, 0, N_RELOAD, 0, N_REGEN, 0, N_INITAI, 0, N_MAPCRC, 0, N_CHECKMAPS, 1,
         N_SETPLAYERINFO, 0, N_SWITCHTEAM, 0,
         N_AUTHTRY, 0, N_AUTHCHAL, 0, N_AUTHANS, 0,
         -1
@@ -360,10 +314,13 @@ enum { CON_CHAT = CON_GAMESPECIFIC, CON_EVENT, CON_MAX, CON_LO = CON_MESG, CON_H
 struct demoheader
 {
     char magic[16];
-    int version, gamever;
+    int gamever, gamemode, mutators, starttime;
+    string mapname;
 };
-
 #include "player.h"
+#ifndef GAMESERVER
+#include "ai.h"
+#endif
 
 template<class T>
 static inline void adjustscaled(T &n, int s)
@@ -381,9 +338,12 @@ static inline void adjustscaled(T &n, int s)
 }
 
 #define MAXNAMELEN 24
-enum { SAY_NONE = 0, SAY_ACTION = 1<<0, SAY_TEAM = 1<<1, SAY_NUM = 2 };
+enum { SAY_NONE = 0, SAY_ACTION = 1<<0, SAY_TEAM = 1<<1, SAY_WHISPER = 1<<2, SAY_NUM = 3 };
 
-enum { PRIV_NONE = 0, PRIV_PLAYER, PRIV_SUPPORTER, PRIV_MODERATOR, PRIV_OPERATOR, PRIV_ADMINISTRATOR, PRIV_DEVELOPER, PRIV_CREATOR, PRIV_MAX, PRIV_START = PRIV_PLAYER, PRIV_ELEVATED = PRIV_MODERATOR };
+enum {
+    PRIV_NONE = 0, PRIV_PLAYER, PRIV_SUPPORTER, PRIV_MODERATOR, PRIV_OPERATOR, PRIV_ADMINISTRATOR, PRIV_DEVELOPER, PRIV_CREATOR, PRIV_MAX,
+    PRIV_START = PRIV_PLAYER, PRIV_ELEVATED = PRIV_MODERATOR, PRIV_LAST = PRIV_CREATOR, PRIV_TYPE = 0xFF, PRIV_LOCAL = 1<<8
+};
 
 #define MM_MODE 0xF
 #define MM_AUTOAPPROVE 0x1000
@@ -396,16 +356,18 @@ enum { MM_OPEN = 0, MM_VETO, MM_LOCKED, MM_PRIVATE, MM_PASSWORD };
 enum { SINFO_NONE = 0, SINFO_STATUS, SINFO_NAME, SINFO_PORT, SINFO_QPORT, SINFO_DESC, SINFO_MODE, SINFO_MUTS, SINFO_MAP, SINFO_TIME, SINFO_NUMPLRS, SINFO_MAXPLRS, SINFO_PING, SINFO_MAX };
 enum { SSTAT_OPEN = 0, SSTAT_LOCKED, SSTAT_PRIVATE, SSTAT_FULL, SSTAT_UNKNOWN, SSTAT_MAX };
 
-enum { AC_PRIMARY = 0, AC_SECONDARY, AC_RELOAD, AC_USE, AC_JUMP, AC_PACING, AC_CROUCH, AC_SPECIAL, AC_DROP, AC_AFFINITY, AC_TOTAL, AC_DASH = AC_TOTAL, AC_MAX };
-enum { IM_METER = 0, IM_TYPE, IM_TIME, IM_REGEN, IM_COUNT, IM_COLLECT, IM_SLIP, IM_SLIDE, IM_JUMP, IM_JET, IM_MAX };
-enum { IM_A_NONE = 0, IM_A_DASH = 1<<0, IM_A_BOOST = 1<<1, IM_A_SPRINT = 1<<2, IM_A_PARKOUR = 1<<3, IM_A_ALL = IM_A_DASH|IM_A_BOOST|IM_A_SPRINT|IM_A_PARKOUR, IM_A_RELAX = IM_A_PARKOUR };
+enum {
+    AC_PRIMARY = 0, AC_SECONDARY, AC_RELOAD, AC_USE, AC_JUMP, AC_WALK, AC_CROUCH, AC_SPECIAL, AC_DROP, AC_AFFINITY, AC_TOTAL, AC_DASH = AC_TOTAL, AC_MAX,
+    AC_ALL = (1<<AC_PRIMARY)|(1<<AC_SECONDARY)|(1<<AC_RELOAD)|(1<<AC_USE)|(1<<AC_JUMP)|(1<<AC_WALK)|(1<<AC_CROUCH)|(1<<AC_SPECIAL)|(1<<AC_DROP)|(1<<AC_AFFINITY)
+};
+enum { IM_METER = 0, IM_TYPE, IM_TIME, IM_REGEN, IM_COUNT, IM_COLLECT, IM_SLIP, IM_SLIDE, IM_JUMP, IM_MAX };
+enum { IM_A_NONE = 0, IM_A_DASH = 1<<0, IM_A_BOOST = 1<<1, IM_A_PARKOUR = 1<<2, IM_A_ALL = IM_A_DASH|IM_A_BOOST|IM_A_PARKOUR, IM_A_RELAX = IM_A_PARKOUR };
 enum { IM_T_NONE = 0, IM_T_BOOST, IM_T_DASH, IM_T_MELEE, IM_T_KICK, IM_T_VAULT, IM_T_SKATE, IM_T_MAX, IM_T_WALL = IM_T_MELEE };
-enum { SPHY_NONE = 0, SPHY_JUMP, SPHY_BOOST, SPHY_DASH, SPHY_MELEE, SPHY_KICK, SPHY_VAULT, SPHY_SKATE, SPHY_POWER, SPHY_EXTINGUISH, SPHY_BUFF, SPHY_MAX, SPHY_SERVER = SPHY_BUFF };
+enum { SPHY_NONE = 0, SPHY_JUMP, SPHY_BOOST, SPHY_DASH, SPHY_MELEE, SPHY_KICK, SPHY_VAULT, SPHY_SKATE, SPHY_COOK, SPHY_EXTINGUISH, SPHY_BUFF, SPHY_MAX, SPHY_SERVER = SPHY_BUFF };
 
 #define CROUCHHEIGHT 0.7f
 #define PHYSMILLIS 250
 
-#include "ai.h"
 #include "vars.h"
 
 static inline void modecheck(int &mode, int &muts, int trying = 0)
@@ -437,7 +399,7 @@ static inline void modecheck(int &mode, int &muts, int trying = 0)
             if(changed) break;
             if(gametype[mode].flags&(1<<G_F_GSP))
             {
-                trying |= m; // game specific mutator exclusively provides allowed bits
+                //trying |= m; // game specific mutator exclusively provides allowed bits
                 mutsidx = gametype[mode].mutators[j+1];
             }
         }
@@ -482,24 +444,107 @@ static inline void modecheck(int &mode, int &muts, int trying = 0)
     }
 }
 
+struct verinfo
+{
+    int type, flag, version;
+    int major, minor, patch, game, platform, arch, gpuglver, gpuglslver;
+    uint crc;
+    char *gpuvendor, *gpurenderer, *gpuversion;
+
+    verinfo() : gpuvendor(NULL), gpurenderer(NULL), gpuversion(NULL) { reset(); }
+    ~verinfo() { reset(); }
+
+    void reset()
+    {
+        if(gpuvendor) delete[] gpuvendor;
+        if(gpurenderer) delete[] gpurenderer;
+        if(gpuversion) delete[] gpuversion;
+        gpuvendor = gpurenderer = gpuversion = NULL;
+        type = flag = version = 0;
+        major = minor = patch = game = arch = gpuglver = gpuglslver = 0;
+        platform = -1;
+        crc = 0;
+    }
+
+    template <class T>
+    void get(T &p)
+    {
+        string text = "";
+        major = getint(p);
+        minor = getint(p);
+        patch = getint(p);
+        game = getint(p);
+        platform = getint(p);
+        arch = getint(p);
+        gpuglver = getint(p);
+        gpuglslver = getint(p);
+        crc = uint(getint(p));
+        if(gpuvendor) delete[] gpuvendor;
+        getstring(text, p); gpuvendor = newstring(text);
+        if(gpurenderer) delete[] gpurenderer;
+        getstring(text, p); gpurenderer = newstring(text);
+        if(gpuversion) delete[] gpuversion;
+        getstring(text, p); gpuversion = newstring(text);
+    }
+
+    template <class T>
+    void put(T &p)
+    {
+        putint(p, major);
+        putint(p, minor);
+        putint(p, patch);
+        putint(p, game);
+        putint(p, platform);
+        putint(p, arch);
+        putint(p, gpuglver);
+        putint(p, gpuglslver);
+        putint(p, crc);
+        sendstring(gpuvendor, p);
+        sendstring(gpurenderer, p);
+        sendstring(gpuversion, p);
+    }
+
+    void grab(verinfo &v)
+    {
+        major = v.major;
+        minor = v.minor;
+        patch = v.patch;
+        game = v.game;
+        platform = v.platform;
+        arch = v.arch;
+        gpuglver = v.gpuglver;
+        gpuglslver = v.gpuglslver;
+        crc = v.crc;
+        if(gpuvendor) delete[] gpuvendor;
+        gpuvendor = newstring(v.gpuvendor ? v.gpuvendor : "");
+        if(gpurenderer) delete[] gpurenderer;
+        gpurenderer = newstring(v.gpurenderer ? v.gpurenderer : "");
+        if(gpuversion) delete[] gpuversion;
+        gpuversion = newstring(v.gpuversion ? v.gpuversion : "");
+    }
+};
+
 // inherited by gameent and server clients
-struct gamestate
+struct clientstate
 {
-    int health, armour, ammo[W_MAX], entid[W_MAX], reloads[W_MAX], colour, model;
-    int lastweap, weapselect, weapload[W_MAX], weapshot[W_MAX], weapstate[W_MAX], weapwait[W_MAX], weaplast[W_MAX];
-    int lastdeath, lastspawn, lastrespawn, lastpain, lastregen, lastbuff, lastres[WR_MAX], lastrestime[WR_MAX];
-    int aitype, aientity, ownernum, skill, points, frags, deaths, cpmillis, cptime, cplaps;
+    int health, ammo[W_MAX], entid[W_MAX], colour, model;
+    int weapselect, weapload[W_MAX], weapshot[W_MAX], weapstate[W_MAX], weapwait[W_MAX], weaplast[W_MAX];
+    int lastdeath, lastspawn, lastrespawn, lastpain, lastregen, lastbuff, lastshoot, lastres[WR_MAX], lastrestime[WR_MAX];
+    int actortype, spawnpoint, ownernum, skill, points, frags, deaths, cpmillis, cptime;
     bool quarantine;
     string vanity;
-    vector<int> loadweap;
-    gamestate() : colour(0), model(0), weapselect(W_MELEE), lastdeath(0), lastspawn(0), lastrespawn(0), lastpain(0), lastregen(0), lastbuff(0),
-        aitype(AI_NONE), aientity(-1), ownernum(-1), skill(0), points(0), frags(0), deaths(0), cpmillis(0), cptime(0), cplaps(0), quarantine(false)
+    vector<int> loadweap, lastweap;
+    verinfo version;
+
+    clientstate() : colour(0), model(0), weapselect(W_MELEE), lastdeath(0), lastspawn(0), lastrespawn(0), lastpain(0), lastregen(0), lastbuff(0), lastshoot(0),
+        actortype(A_PLAYER), spawnpoint(-1), ownernum(-1), skill(0), points(0), frags(0), deaths(0), cpmillis(0), cptime(0), quarantine(false)
     {
         setvanity();
         loadweap.shrink(0);
+        lastweap.shrink(0);
         resetresidual();
     }
-    ~gamestate() {}
+    ~clientstate() {}
 
     bool setvanity(const char *v = "")
     {
@@ -527,8 +572,9 @@ struct gamestate
                 case 1: if(w_carry(weap, sweap)) return true; break; // only carriable
                 case 2: if(ammo[weap] > 0) return true; break; // only with actual ammo
                 case 3: if(ammo[weap] > 0 && canreload(weap, sweap)) return true; break; // only reloadable with actual ammo
-                case 4: if(ammo[weap] >= (canreload(weap, sweap) ? 0 : W(weap, max))) return true; break; // only reloadable or those with < max
-                case 5: if(w_carry(weap, sweap) || (!canreload(weap, sweap) && weap >= W_OFFSET)) return true; break; // special case for usable weapons
+                case 4: if(ammo[weap] >= (canreload(weap, sweap) ? 0 : W(weap, ammomax))) return true; break; // only reloadable or those with < max
+                case 5: if(weap != sweap && weap >= W_ITEM) return true; break; // special case to determine drop in loadout games
+                case 6: if(weap != sweap && weap >= W_OFFSET) return true; break; // special case to determine drop in classic games
             }
         }
         return false;
@@ -539,9 +585,29 @@ struct gamestate
         return weap == weapselect || millis-weaplast[weap] < weapwait[weap] || hasweap(weap, sweap);
     }
 
+    void addlastweap(int weap)
+    {
+        lastweap.add(weap);
+        if(lastweap.length() >= W_MAX) lastweap.remove(0);
+    }
+
+    int getlastweap(int sweap, int exclude = -1)
+    {
+        loopvrev(lastweap)
+        {
+            if(lastweap[i] == exclude) continue;
+            else if(hasweap(lastweap[i], sweap)) return lastweap[i];
+        }
+        return -1;
+    }
+
     int bestweap(int sweap, bool last = false)
     {
-        if(last && hasweap(lastweap, sweap)) return lastweap;
+        if(last)
+        {
+            int w = getlastweap(sweap);
+            if(hasweap(w, sweap)) return w;
+        }
         loopirev(W_MAX) if(hasweap(i, sweap, 3)) return i; // reloadable first
         loopirev(W_MAX) if(hasweap(i, sweap, 1)) return i; // carriable second
         loopirev(W_MAX) if(hasweap(i, sweap, 0)) return i; // any just to bail us out
@@ -558,7 +624,8 @@ struct gamestate
     int drop(int sweap)
     {
         if(hasweap(weapselect, sweap, 1)) return weapselect;
-        if(hasweap(lastweap, sweap, 1)) return lastweap;
+        int w = getlastweap(sweap, weapselect);
+        if(hasweap(w, sweap, 1)) return w;
         loopi(W_MAX) if(hasweap(i, sweap, 1)) return i;
         return -1;
     }
@@ -569,8 +636,9 @@ struct gamestate
         {
             weapstate[i] = W_S_IDLE;
             weapwait[i] = weaplast[i] = weapload[i] = weapshot[i] = 0;
-            if(full) ammo[i] = entid[i] = reloads[i] = -1;
+            if(full) ammo[i] = entid[i] = -1;
         }
+        lastweap.shrink(0);
     }
 
     void setweapstate(int weap, int state, int delay, int millis)
@@ -584,8 +652,11 @@ struct gamestate
     {
         if(isweap(weap))
         {
-            lastweap = weapselect;
-            setweapstate(lastweap, W_S_SWITCH, delay, millis);
+            if(isweap(weapselect))
+            {
+                lastweap.add(weapselect);
+                setweapstate(weapselect, W_S_SWITCH, delay, millis);
+            }
             weapselect = weap;
             setweapstate(weap, state, delay, millis);
         }
@@ -594,20 +665,20 @@ struct gamestate
     bool weapwaited(int weap, int millis, int skip = 0)
     {
         if(weap != weapselect) skip &= ~(1<<W_S_RELOAD);
-        if(!weapwait[weap] || weapstate[weap] == W_S_IDLE || weapstate[weap] == W_S_POWER || (skip && skip&(1<<weapstate[weap]))) return true;
+        if(!weapwait[weap] || W_S_EXCLUDE&(1<<weapstate[weap]) || (skip && skip&(1<<weapstate[weap]))) return true;
         return millis-weaplast[weap] >= weapwait[weap];
     }
 
-    bool candrop(int weap, int sweap, int millis, int skip = 0)
+    bool candrop(int weap, int sweap, int millis, bool load, int skip = 0)
     {
-        if(weapwaited(weapselect, millis, skip) && weap >= W_OFFSET && hasweap(weap, sweap) && weapwaited(weap, millis, skip) && (isweap(sweap) ? weap != sweap : weap >= 0-sweap))
+        if(hasweap(weap, sweap, load ? 5 : 6) && weapwaited(weap, millis, skip) && weapwaited(weapselect, millis, skip))
             return true;
         return false;
     }
 
     bool canswitch(int weap, int sweap, int millis, int skip = 0)
     {
-        if((aitype >= AI_START || weap != W_MELEE || sweap == W_MELEE || weapselect == W_MELEE) && weap != weapselect && weapwaited(weapselect, millis, skip) && hasweap(weap, sweap) && weapwaited(weap, millis, skip))
+        if((actortype >= A_ENEMY || weap != W_MELEE || sweap == W_MELEE || weapselect == W_MELEE) && weap != weapselect && weapwaited(weapselect, millis, skip) && hasweap(weap, sweap) && weapwaited(weap, millis, skip))
             return true;
         return false;
     }
@@ -615,23 +686,15 @@ struct gamestate
     bool canshoot(int weap, int flags, int sweap, int millis, int skip = 0)
     {
         if(weap == weapselect || weap == W_MELEE)
-            if(hasweap(weap, sweap) && getammo(weap, millis) >= (W2(weap, power, WS(flags)) ? 1 : W2(weap, sub, WS(flags))) && weapwaited(weap, millis, skip))
+            if(hasweap(weap, sweap) && getammo(weap, millis) >= (W2(weap, cooktime, WS(flags)) ? 1 : W2(weap, ammosub, WS(flags))) && weapwaited(weap, millis, skip))
                 return true;
         return false;
     }
 
     bool canreload(int weap, int sweap, bool check = true, int millis = 0, int skip = 0)
     {
-        if(check || (weap == weapselect && hasweap(weap, sweap) && ammo[weap] < W(weap, max) && weapwaited(weap, millis, skip)))
-        {
-            int n = w_reload(weap, sweap);
-            switch(n)
-            {
-                case -1: return true;
-                case 0: return false;
-                default: return reloads[weap] < n;
-            }
-        }
+        if(weapstate[weap] != W_S_ZOOM && (check || (weap == weapselect && hasweap(weap, sweap) && ammo[weap] < W(weap, ammomax) && weapwaited(weap, millis, skip))))
+            return w_reload(weap, sweap);
         return false;
     }
 
@@ -642,16 +705,9 @@ struct gamestate
             case EU_AUTO: case EU_ACT: return true; break;
             case EU_ITEM:
             { // can't use when reloading or firing
-                switch(type)
-                {
-                    case WEAPON:
-                    {
-                        if(isweap(attr) && !hasweap(attr, sweap, 4) && weapwaited(weapselect, millis, skip))
-                            return true;
-                        break;
-                    }
-                    default: return true;
-                }
+                if(type != WEAPON || !isweap(attr)) return false;
+                if(!hasweap(attr, sweap, 4) && weapwaited(weapselect, millis, skip))
+                    return true;
                 break;
             }
             default: break;
@@ -659,37 +715,19 @@ struct gamestate
         return false;
     }
 
-    void useitem(int id, int type, int attr, int ammoamt, int reloadamt, int sweap, int millis, int delay)
+    void useitem(int id, int type, int attr, int ammoamt, int sweap, int millis, int delay)
     {
-        switch(type)
-        {
-            case TRIGGER: break;
-            case WEAPON:
-            {
-                int prev = max(ammo[attr], 0), ammoval = ammoamt >= 0 ? ammoamt : WUSE(attr);
-                weapswitch(attr, millis, delay, W_S_USE);
-                ammo[attr] = clamp(prev+ammoval, 0, W(attr, max));
-                weapload[attr] = ammo[attr]-prev;
-                reloads[attr] = reloadamt >= 0 ? reloadamt : 0;
-                entid[attr] = id;
-                break;
-            }
-#ifdef MEK
-            case HEALTH:
-            {
-                int value = ammoamt >= 0 ? ammoamt : healthamt[attr];
-                health = max(health + value, CLASS(model, health));
-                break;
-            }
-            case ARMOUR:
-            {
-                int value = ammoamt >= 0 ? ammoamt : armouramt[attr];
-                armour = max(armour + value, CLASS(model, armour));
-                break;
-            }
-#endif
-            default: break;
-        }
+        if(type != WEAPON || !isweap(attr)) return;
+        int prev = max(ammo[attr], 0), ammoval = ammoamt >= 0 ? ammoamt : WUSE(attr);
+        weapswitch(attr, millis, delay, W_S_USE);
+        ammo[attr] = clamp(prev+ammoval, 0, W(attr, ammomax));
+        weapload[attr] = ammo[attr]-prev;
+        entid[attr] = id;
+    }
+
+    bool zooming()
+    {
+        return isweap(weapselect) && weapstate[weapselect] == W_S_ZOOM;
     }
 
     void resetresidual(int n = -1)
@@ -700,105 +738,83 @@ struct gamestate
 
     void clearstate()
     {
-        lastdeath = lastpain = lastregen = lastbuff = 0;
+        lastdeath = lastpain = lastregen = lastbuff = lastshoot = 0;
         lastrespawn = -1;
         resetresidual();
     }
 
     void mapchange()
     {
-        points = cpmillis = cptime = cplaps = 0;
-        loadweap.shrink(0);
+        points = cpmillis = cptime = 0;
     }
 
-    void respawn(int millis, int heal = 0, int armr = -1)
+    void respawn(int millis)
     {
-        health = heal ? heal : 100;
-        armour = armr >= 0 ? armr : 100;
         lastspawn = millis;
         clearstate();
         weapreset(true);
     }
 
-    void spawnstate(int gamemode, int mutators, int sweap = -1, int heal = 0, int armr = -1)
+    void spawnstate(int gamemode, int mutators, int sweap, int heal)
     {
         weapreset(true);
-        if(!isweap(sweap))
-        {
-            if(aitype >= AI_START) sweap = W_MELEE;
-            else if(m_kaboom(gamemode, mutators)) sweap = W_GRENADE;
-            else sweap = isweap(m_weapon(gamemode, mutators)) ? m_weapon(gamemode, mutators) : W_PISTOL;
-        }
-        if(isweap(sweap))
+        health = heal > 0 ? heal : (actortype >= A_ENEMY ? actor[actortype].health : m_health(gamemode, mutators, model));
+        int s = sweap;
+        if(!isweap(s))
         {
-            ammo[sweap] = max(1, W(sweap, max));
-            reloads[sweap] = 0;
+            if(actortype >= A_ENEMY) s = actor[actortype].weap;
+            else if(m_kaboom(gamemode, mutators)) s = W_GRENADE;
+            else s = isweap(m_weapon(gamemode, mutators)) ? m_weapon(gamemode, mutators) : W_PISTOL;
         }
-        if(aitype >= AI_START)
+        if(isweap(s))
         {
-            loadweap.shrink(0);
-            lastweap = weapselect = sweap;
+            ammo[s] = max(1, W(s, ammomax));
+            weapselect = s;
         }
-        else
+        if(s != W_MELEE && actor[actortype].canmove) ammo[W_MELEE] = max(1, W(W_MELEE, ammomax));
+        if(actortype < A_ENEMY)
         {
-#ifndef MEK
-            if(sweap != W_MELEE)
+            if(!m_race(gamemode))
             {
-                ammo[W_MELEE] = max(1, W(W_MELEE, max));
-                reloads[W_MELEE] = 0;
-            }
-#endif
-            if(sweap != W_GRENADE && G(spawngrenades) >= (m_insta(gamemode, mutators) || m_trial(gamemode) ? 2 : 1))
-            {
-                ammo[W_GRENADE] = max(1, W(W_GRENADE, max));
-                reloads[W_GRENADE] = 0;
-            }
-            if(sweap != W_MINE && (m_kaboom(gamemode, mutators) || G(spawnmines) >= (m_insta(gamemode, mutators) || m_trial(gamemode) ? 2 : 1)))
-            {
-                ammo[W_MINE] = max(1, W(W_MINE, max));
-                reloads[W_MINE] = 0;
+                if(s != W_GRENADE && G(spawngrenades) >= (m_insta(gamemode, mutators) ? 2 : 1))
+                    ammo[W_GRENADE] = max(1, W(W_GRENADE, ammomax));
+                if(s != W_MINE && (m_kaboom(gamemode, mutators) || G(spawnmines) >= (m_insta(gamemode, mutators) ? 2 : 1)))
+                    ammo[W_MINE] = max(1, W(W_MINE, ammomax));
             }
             if(m_loadout(gamemode, mutators))
             {
+                int n = 0;
                 vector<int> aweap;
-                loopj(G(maxcarry))
+                loopj(W_LOADOUT) aweap.add(loadweap.inrange(j) ? loadweap[j] : 0);
+                loopvj(aweap)
                 {
-                    if(!loadweap.inrange(j)) aweap.add(0);
-                    else if(loadweap[j] < W_OFFSET || loadweap[j] >= W_ITEM || hasweap(loadweap[j], sweap))
+                    if(!aweap[j]) // specifically asking for random
                     {
-                        int r = rnd(W_ITEM-W_OFFSET)+W_OFFSET; // random
-                        int iters = 0;
-                        while(hasweap(r, sweap) || !m_check(W(r, modes), W(r, muts), gamemode, mutators))
+                        for(int t = rnd(W_ITEM-W_OFFSET)+W_OFFSET, r = 0; r < W_LOADOUT; r++)
                         {
-                            if(++iters > W_MAX)
+                            if(t >= W_OFFSET && t < W_ITEM && !hasweap(t, sweap) && m_check(W(t, modes), W(t, muts), gamemode, mutators) && !W(t, disabled))
                             {
-                                r = 0;
+                                aweap[j] = t;
                                 break;
                             }
-                            if(++r >= W_ITEM) r = W_OFFSET;
+                            else if(++t >= W_ITEM) t = W_OFFSET;
                         }
-                        aweap.add(r);
                     }
-                    else aweap.add(loadweap[j]);
-                    ammo[aweap[j]] = max(1, W(aweap[j], max));
-                    reloads[aweap[j]] = 0;
+                    if(aweap[j] >= W_OFFSET && aweap[j] < W_ITEM && !hasweap(aweap[j], sweap) && m_check(W(aweap[j], modes), W(aweap[j], muts), gamemode, mutators) && !W(aweap[j], disabled))
+                    {
+                        ammo[aweap[j]] = max(1, W(aweap[j], ammomax));
+                        if(!n) weapselect = aweap[j];
+                        if(++n >= G(maxcarry)) break;
+                    }
                 }
-                lastweap = weapselect = aweap[0]; // if '0' isn't present, maxcarry isn't doing its job
-            }
-            else
-            {
-                loadweap.shrink(0);
-                lastweap = weapselect = sweap;
             }
         }
-        health = heal ? heal : m_health(gamemode, mutators, model);
-        armour = armr >= 0 ? armr : m_armour(gamemode, mutators, model);
     }
 
-    void editspawn(int gamemode, int mutators, int sweap = -1, int heal = 0, int armr = -1)
+    void editspawn(int gamemode, int mutators)
     {
         clearstate();
-        spawnstate(gamemode, mutators, sweap, heal, armr);
+        spawnstate(gamemode, mutators, m_weapon(gamemode, mutators), m_health(gamemode, mutators, model));
     }
 
     int respawnwait(int millis, int delay)
@@ -808,9 +824,11 @@ struct gamestate
 
     int protect(int millis, int delay)
     {
-        int amt = 0;
-        if(aitype < AI_START && lastspawn && delay && millis-lastspawn <= delay) amt = delay-(millis-lastspawn);
-        return amt;
+        if(actortype >= A_ENEMY || !lastspawn || !delay) return 0;
+        if(G(protectbreak) && lastshoot) return 0;
+        int len = millis-lastspawn;
+        if(len > delay) return 0;
+        return delay-len;
     }
 
     bool burning(int millis, int len) { return len && lastres[WR_BURN] && millis-lastres[WR_BURN] <= len; }
@@ -824,10 +842,13 @@ namespace server
     extern void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen = MAXSTRLEN);
     extern bool servcmd(int nargs, const char *cmd, const char *arg);
     extern const char *gamename(int mode, int muts, int compact = 0, int limit = 0);
+    extern const char *privname(int priv = PRIV_NONE, int actortype = A_PLAYER);
+    extern const char *privnamex(int priv = PRIV_NONE, int actortype = A_PLAYER, bool local = false);
 #ifdef GAMESERVER
     struct clientinfo;
-    extern void waiting(clientinfo *ci, int drop = 0, bool exclude = false);
-    extern void setteam(clientinfo *ci, int team, int flags = TT_DEFAULT);
+    extern void waiting(clientinfo *ci, int drop = 0, bool doteam = true, bool exclude = false);
+    extern void setteam(clientinfo *ci, int team, int flags = TT_RESET, bool swaps = true);
+    extern int chooseteam(clientinfo *ci, int suggest = -1, bool wantbal = false);
 #endif
 }
 
@@ -850,10 +871,10 @@ template<class T> inline void flashcolourf(T &r, T &g, T &b, T &f, T br, T bg, T
 struct gameentity : extentity
 {
     int schan;
-    int lastspawn;
+    int lastspawn, nextemit;
     linkvector kin;
 
-    gameentity() : schan(-1), lastspawn(0) {}
+    gameentity() : schan(-1), lastspawn(0), nextemit(0) {}
     ~gameentity()
     {
         if(issound(schan)) removesound(schan);
@@ -890,7 +911,6 @@ const char * const animnames[] =
     "jump forward", "jump backward", "jump left", "jump right", "jump",
     "impulse forward", "impulse backward", "impulse left", "impulse right",
     "dash forward", "dash backward", "dash left", "dash right", "dash up",
-    "jet forward", "jet backward", "jet left", "jet right", "jet up",
     "wall run left", "wall run right", "wall jump", "power slide", "fly kick",
     "sink", "edit", "win", "lose",
     "crouch", "crawl forward", "crawl backward", "crawl left", "crawl right",
@@ -902,6 +922,7 @@ const char * const animnames[] =
     "smg", "smg primary", "smg secondary", "smg reload",
     "flamer", "flamer primary", "flamer secondary", "flamer reload",
     "plasma", "plasma primary", "plasma secondary", "plasma reload",
+    "zapper", "zapper primary", "zapper secondary", "zapper reload",
     "rifle", "rifle primary", "rifle secondary", "rifle reload",
     "grenade", "grenade primary", "grenade secondary", "grenade reload", "grenade power",
     "mine", "mine primary", "mine secondary", "mine reload",
@@ -925,32 +946,35 @@ struct stunevent
     float scale, gravity;
 };
 
-struct gameent : dynent, gamestate
+struct gameent : dynent, clientstate
 {
-    editinfo *edit; ai::aiinfo *ai;
+    editinfo *edit;
+    ai::aiinfo *ai;
     int team, clientnum, privilege, projid, lastnode, checkpoint, cplast, respawned, suicided, lastupdate, lastpredict, plag, ping, lastflag, totaldamage,
-        actiontime[AC_MAX], impulse[IM_MAX], smoothmillis, turnmillis, turnside, aschan, cschan, vschan, wschan, pschan, fschan, jschan,
-        lasthit, lastteamhit, lastkill, lastattacker, lastpoints, quake, spree;
+        actiontime[AC_MAX], impulse[IM_MAX], smoothmillis, turnmillis, turnside, aschan, cschan, vschan, wschan, pschan, sschan[2],
+        lasthit, lastteamhit, lastkill, lastattacker, lastpoints, quake, spree, lastfoot;
     float deltayaw, deltapitch, newyaw, newpitch, turnyaw, turnroll;
-    vec head, torso, muzzle, origin, eject, waist, jet[3], legs, hrad, trad, lrad, toe[2];
+    vec head, torso, muzzle, origin, eject[2], waist, jet[3], legs, hrad, trad, lrad, toe[2];
     bool action[AC_MAX], conopen, k_up, k_down, k_left, k_right, obliterated, headless;
-    string hostname, name, handle, info, obit;
+    string hostname, hostip, name, handle, info, obit;
     vector<gameent *> dominating, dominated;
     vector<eventicon> icons;
     vector<stunevent> stuns;
     vector<int> vitems;
 
     gameent() : edit(NULL), ai(NULL), team(T_NEUTRAL), clientnum(-1), privilege(PRIV_NONE), projid(0), checkpoint(-1), cplast(0), lastupdate(0), lastpredict(0), plag(0), ping(0),
-        totaldamage(0), smoothmillis(-1), turnmillis(0), aschan(-1), cschan(-1), vschan(-1), wschan(-1), pschan(-1), fschan(-1), jschan(-1), lastattacker(-1), lastpoints(0), quake(0),
+        totaldamage(0), smoothmillis(-1), turnmillis(0), lastattacker(-1), lastpoints(0), quake(0),
         conopen(false), k_up(false), k_down(false), k_left(false), k_right(false), obliterated(false)
     {
         state = CS_DEAD;
         type = ENT_PLAYER;
         copystring(hostname, "unknown");
+        copystring(hostip, "0.0.0.0");
         name[0] = handle[0] = info[0] = obit[0] = 0;
+        removesounds();
         cleartags();
         checktags();
-        respawn(-1, 100);
+        respawn(-1, 0, 0);
     }
     ~gameent()
     {
@@ -964,43 +988,39 @@ struct gameent : dynent, gamestate
     static bool is(int t) { return t == ENT_PLAYER || t == ENT_AI; }
     static bool is(physent *d) { return d->type == ENT_PLAYER || d->type == ENT_AI; }
 
-    void setparams(bool reset = false, int gamemode = 0, int mutators = 0)
+    void setparams(bool reset)
     {
-        int type = clamp(aitype, 0, int(AI_MAX-1));
-#ifdef MEK
-        if(type >= AI_START)
+        int type = clamp(actortype, 0, int(A_MAX-1));
+        if(type >= A_ENEMY)
         {
-#endif
-            speed = aistyle[type].speed;
-            xradius = aistyle[type].xradius*curscale;
-            yradius = aistyle[type].yradius*curscale;
-            zradius = height = aistyle[type].height*curscale;
-            weight = aistyle[type].weight*curscale;
-#ifdef MEK
+            xradius = actor[type].xradius*curscale;
+            yradius = actor[type].yradius*curscale;
+            zradius = height = actor[type].height*curscale;
+            speed = actor[type].speed;
+            weight = actor[type].weight*curscale;
         }
         else
         {
-            speed = CLASS(model, speed);
-            xradius = CLASS(model, xradius)*curscale;
-            yradius = CLASS(model, yradius)*curscale;
-            zradius = height = CLASS(model, height)*curscale;
-            weight = CLASS(model, weight)*curscale;
+            xradius = playerdims[model][0]*curscale;
+            yradius = playerdims[model][1]*curscale;
+            zradius = height = playerdims[model][2]*curscale;
+            speed = PLAYER(model, speed);
+            weight = PLAYER(model, weight)*curscale;
         }
-#endif
         radius = max(xradius, yradius);
         aboveeye = curscale;
     }
 
-    void setscale(float scale = 1, int millis = 0, bool reset = false, int gamemode = 0, int mutators = 0)
+    void setscale(float scale, int millis, bool reset)
     {
         if(scale != curscale)
         {
             if(state == CS_ALIVE && millis > 0)
                 curscale = scale > curscale ? min(curscale+millis/2000.f, scale) : max(curscale-millis/2000.f, scale);
             else curscale = scale;
-            setparams(reset, gamemode, mutators);
+            setparams(reset);
         }
-        else if(reset) setparams(reset, gamemode, mutators);
+        else if(reset) setparams(reset);
     }
 
     int getprojid()
@@ -1016,9 +1036,12 @@ struct gameent : dynent, gamestate
         if(issound(vschan)) removesound(vschan);
         if(issound(wschan)) removesound(wschan);
         if(issound(pschan)) removesound(pschan);
-        if(issound(fschan)) removesound(fschan);
-        if(issound(jschan)) removesound(jschan);
-        aschan = cschan = vschan = wschan = pschan = fschan = jschan = -1;
+        aschan = cschan = vschan = wschan = pschan = -1;
+        loopi(2)
+        {
+            if(issound(sschan[i])) removesound(sschan[i]);
+            sschan[i] = -1;
+        }
     }
 
     void stopmoving(bool full)
@@ -1031,33 +1054,33 @@ struct gameent : dynent, gamestate
         }
     }
 
-    void clearstate(int gamemode = 0, int mutators = 0)
+    void clearstate(int gamemode, int mutators)
     {
-        loopi(IM_MAX) impulse[i] = 0;
-        cplast = lasthit = lastkill = quake = turnmillis = turnside = spree = 0;
+        loopi(IM_MAX) if(i != IM_METER || !m_race(gamemode) || !m_gsp2(gamemode, mutators)) impulse[i] = 0;
+        lasthit = lastkill = quake = turnmillis = turnside = spree = 0;
         turnroll = turnyaw = 0;
-        lastteamhit = lastflag = respawned = suicided = lastnode = -1;
+        lastteamhit = lastflag = respawned = suicided = lastnode = lastfoot = -1;
         obit[0] = 0;
         obliterated = headless = false;
-        setscale(1, 0, true, gamemode, mutators);
+        setscale(1, 0, true);
         icons.shrink(0);
         stuns.shrink(0);
         used.shrink(0);
     }
 
-    void respawn(int millis = 0, int heal = 0, int armr = -1, int gamemode = 0, int mutators = 0)
+    void respawn(int millis, int gamemode, int mutators)
     {
         stopmoving(true);
         removesounds();
         clearstate(gamemode, mutators);
         physent::reset();
-        gamestate::respawn(millis, heal, armr);
+        clientstate::respawn(millis);
     }
 
-    void editspawn(int gamemode, int mutators, int sweap = -1, int heal = 0, int armr = -1)
+    void editspawn(int gamemode, int mutators)
     {
         stopmoving(true);
-        clearstate();
+        clearstate(gamemode, mutators);
         inmaterial = airmillis = floormillis = 0;
         inliquid = onladder = false;
         strafe = move = 0;
@@ -1065,33 +1088,60 @@ struct gameent : dynent, gamestate
         vel = falling = vec(0, 0, 0);
         floor = vec(0, 0, 1);
         resetinterp();
-        gamestate::editspawn(gamemode, mutators, sweap, heal, armr);
+        clientstate::editspawn(gamemode, mutators);
     }
 
-    void resetstate(int millis, int heal, int armr)
+    void resetstate(int millis, int gamemode, int mutators)
     {
-        respawn(millis, heal, armr);
-        frags = deaths = totaldamage = 0;
+        respawn(millis, gamemode, mutators);
+        checkpoint = -1;
+        frags = deaths = totaldamage = cplast = 0;
     }
 
-    void mapchange(int millis, int heal, int armr)
+    void mapchange(int millis, int gamemode, int mutators)
     {
-        checkpoint = -1;
         dominating.shrink(0);
         dominated.shrink(0);
         icons.shrink(0);
-        resetstate(millis, heal, armr);
-        gamestate::mapchange();
+        resetstate(millis, gamemode, mutators);
+        clientstate::mapchange();
+    }
+
+    void cleartags() { head = torso = muzzle = origin = eject[0] = eject[1] = waist = jet[0] = jet[1] = jet[2] = toe[0] = toe[1] = vec(-1, -1, -1); }
+
+    vec checkfootpos(int foot)
+    {
+        if(foot < 0 || foot > 1) return feetpos();
+        if(toe[foot] == vec(-1, -1, -1))
+        {
+            int millis = lastmillis%500;
+            float amt = millis > 250 ? (500-millis)/250.f : millis/250.f;
+            vec dir, right;
+            vecfromyawpitch(yaw, pitch, 1, 0, dir);
+            vecfromyawpitch(yaw, pitch, 0, foot ? 1 : -1, right);
+            dir.mul(radius*0.5f);
+            right.mul(radius*(!move && strafe ? amt-0.5f : 0.5f));
+            dir.z -= height*0.6f+(height*0.4f*(foot ? 1-amt : amt));
+            toe[foot] = vec(o).add(dir).add(right);
+        }
+        return toe[foot];
     }
 
-    void cleartags() { head = torso = muzzle = origin = eject = waist = jet[0] = jet[1] = jet[2] = toe[0] = toe[1] = vec(-1, -1, -1); }
+    vec footpos(int foot)
+    {
+        return checkfootpos(foot);
+    }
 
     vec checkoriginpos()
     {
         if(origin == vec(-1, -1, -1))
         {
-            vec dir, right; vecfromyawpitch(yaw, pitch, 1, 0, dir); vecfromyawpitch(yaw, pitch, 0, -1, right);
-            dir.mul(radius*0.75f); right.mul(radius*0.85f); dir.z -= height*0.25f;
+            vec dir, right;
+            vecfromyawpitch(yaw, pitch, 1, 0, dir);
+            vecfromyawpitch(yaw, pitch, 0, -1, right);
+            dir.mul(radius*0.75f);
+            right.mul(radius*0.85f);
+            dir.z -= height*0.25f;
             origin = vec(o).add(dir).add(right);
         }
         return origin;
@@ -1125,14 +1175,16 @@ struct gameent : dynent, gamestate
                     if(px >= 180) px -= 360;
                     if(px < -180) px += 360;
                 }
-                vec dir;
-                vecfromyawpitch(yx, px, 1, 0, dir);
-                muzzle = vec(originpos()).add(dir.mul(8));
+                muzzle = vec(originpos()).add(vec(yx*RAD, px*RAD).mul(8));
             }
             else
             {
-                vec dir, right; vecfromyawpitch(yaw, pitch, 1, 0, dir); vecfromyawpitch(yaw, pitch, 0, -1, right);
-                dir.mul(radius*0.9f); right.mul(radius*0.6f); dir.z -= height*0.25f;
+                vec dir, right;
+                vecfromyawpitch(yaw, pitch, 1, 0, dir);
+                vecfromyawpitch(yaw, pitch, 0, -1, right);
+                dir.mul(radius*0.9f);
+                right.mul(radius*0.6f);
+                dir.z -= height*0.25f;
                 muzzle = vec(o).add(dir).add(right);
             }
         }
@@ -1145,15 +1197,15 @@ struct gameent : dynent, gamestate
         return originpos(weap == W_MELEE, secondary);
     }
 
-    vec checkejectpos()
+    vec checkejectpos(bool alt = false)
     {
-        if(eject == vec(-1, -1, -1)) eject = checkmuzzlepos();
-        return eject;
+        if(eject[alt ? 1 : 0] == vec(-1, -1, -1)) eject[alt ? 1 : 0] = alt ? checkoriginpos() : checkmuzzlepos();
+        return eject[alt ? 1 : 0];
     }
 
-    vec ejectpos(int weap = -1)
+    vec ejectpos(int weap = -1, bool alt = false)
     {
-        if(isweap(weap) && weap != W_MELEE) return checkejectpos();
+        if(isweap(weap) && weap != W_MELEE) return checkejectpos(alt);
         return muzzlepos();
     }
 
@@ -1179,14 +1231,16 @@ struct gameent : dynent, gamestate
         lrad = vec(xradius*0.85f, yradius*0.85f, lsize);
         if(waist == vec(-1, -1, -1))
         {
-            vec dir; vecfromyawpitch(yaw, 0, -1, 0, dir);
+            vec dir;
+            vecfromyawpitch(yaw, 0, -1, 0, dir);
             dir.mul(radius*1.5f);
             dir.z -= height*0.5f;
             waist = vec(o).add(dir);
         }
         if(jet[0] == vec(-1, -1, -1))
         {
-            vec dir; vecfromyawpitch(yaw, 0, -1, -1, dir);
+            vec dir;
+            vecfromyawpitch(yaw, 0, -1, -1, dir);
             dir.mul(radius);
             dir.z -= height;
             jet[0] = vec(o).add(dir);
@@ -1201,35 +1255,21 @@ struct gameent : dynent, gamestate
         }
         if(jet[2] == vec(-1, -1, -1))
         {
-            vec dir; vecfromyawpitch(yaw, 0, -1, 0, dir);
+            vec dir;
+            vecfromyawpitch(yaw, 0, -1, 0, dir);
             dir.mul(radius*1.25f);
             dir.z -= height*0.35f;
             jet[2] = vec(o).add(dir);
         }
-        if(toe[0] == vec(-1, -1, -1))
-        {
-            vec dir; vecfromyawpitch(yaw, 0, 1, -1, dir);
-            dir.mul(radius);
-            dir.z -= height;
-            toe[0] = vec(o).add(dir);
-        }
-        if(toe[1] == vec(-1, -1, -1))
-        {
-            vec dir;
-            vecfromyawpitch(yaw, 0, 1, 1, dir);
-            dir.mul(radius);
-            dir.z -= height;
-            toe[1] = vec(o).add(dir);
-        }
     }
 
-    bool wantshitbox() { return type == ENT_PLAYER || (type == ENT_AI && aistyle[aitype].hitbox); }
+    bool wantshitbox() { return type == ENT_PLAYER || (type == ENT_AI && actor[actortype].hitbox); }
 
     void checktags()
     {
         checkoriginpos();
         checkmuzzlepos();
-        checkejectpos();
+        loopi(2) checkejectpos(i!=0);
         if(wantshitbox()) checkhitboxes();
     }
 
@@ -1249,7 +1289,7 @@ struct gameent : dynent, gamestate
 
     void resetjump()
     {
-        airmillis = turnside = impulse[IM_COUNT] = impulse[IM_TYPE] = impulse[IM_JUMP] = impulse[IM_JET] = 0;
+        airmillis = turnside = impulse[IM_COUNT] = impulse[IM_TYPE] = impulse[IM_JUMP] = 0;
     }
 
     void resetair()
@@ -1258,30 +1298,6 @@ struct gameent : dynent, gamestate
         resetjump();
     }
 
-    void resetburning()
-    {
-        if(issound(fschan)) removesound(fschan);
-        fschan = -1;
-        gamestate::resetresidual(WR_BURN);
-    }
-
-    void resetbleeding()
-    {
-        gamestate::resetresidual(WR_BLEED);
-    }
-
-    void resetshocking()
-    {
-        gamestate::resetresidual(WR_SHOCK);
-    }
-
-    void resetresidual()
-    {
-        resetburning();
-        resetbleeding();
-        resetshocking();
-    }
-
     void addicon(int type, int millis, int fade, int value = 0)
     {
         int pos = -1;
@@ -1314,15 +1330,15 @@ struct gameent : dynent, gamestate
         else icons.insert(pos, e);
     }
 
-    void setname(const char *n = NULL)
+    void setname(const char *n)
     {
         if(n && *n) copystring(name, n, MAXNAMELEN+1);
         else name[0] = 0;
     }
 
-    bool setvanity(const char *v = "")
+    bool setvanity(const char *v)
     {
-        if(gamestate::setvanity(v))
+        if(clientstate::setvanity(v))
         {
             vitems.shrink(0);
             return true;
@@ -1330,16 +1346,19 @@ struct gameent : dynent, gamestate
         return false;
     }
 
-    void setinfo(const char *n = NULL, int c = 0, int m = 0, const char *v = "")
+    void setinfo(const char *n, int c, int m, const char *v, vector<int> &w)
     {
         setname(n);
         colour = c;
         model = m;
         setvanity(v);
+        loadweap.shrink(0);
+        loopv(w) loadweap.add(w[i]);
     }
 
     void addstun(int weap, int millis, int delay, float scale, float gravity)
     {
+        if(delay <= 0 || (scale == 0 && gravity == 0)) return;
         stunevent &s = stuns.add();
         s.weap = weap;
         s.millis = millis;
@@ -1373,18 +1392,51 @@ struct gameent : dynent, gamestate
         if(!canshoot(W_MELEE, HIT_ALT, sweap, millis, (1<<W_S_RELOAD))) return false;
         return true;
     }
+
+    int curfoot()
+    {
+        vec dir;
+        vecfromyawpitch(yaw, 0, move, strafe && !move ? strafe : 0, dir);
+        dir.mul(radius).add(o).z -= height; // foot furthest away is one being set down
+        return footpos(0).squaredist(dir) > footpos(1).squaredist(dir) ? 0 : 1;
+    }
+
+    bool crouching(bool limit = false)
+    {
+        return action[AC_CROUCH] || actiontime[AC_CROUCH] < 0 || (!limit && lastmillis-actiontime[AC_CROUCH] <= PHYSMILLIS);
+    }
+
+    bool running(float minspeed = 0)
+    {
+        if(minspeed != 0 && vel.magnitude() >= minspeed) return true;
+        return !action[AC_WALK];
+    }
+
+    bool sliding(bool power = false)
+    {
+        if((!power && turnside) || (G(impulseslip) && impulse[IM_SLIP] && lastmillis-impulse[IM_SLIP] <= G(impulseslip)) || (G(impulseslide) && impulse[IM_SLIDE] && lastmillis-impulse[IM_SLIDE] <= G(impulseslide)))
+        {
+            if(!power || crouching())
+            {
+                if(power && G(impulseslide) && G(impulseslip) && move == 1 && impulse[IM_SLIP] > impulse[IM_SLIDE])
+                    impulse[IM_SLIDE] = impulse[IM_SLIP];
+                return true;
+            }
+        }
+        return false;
+    }
 };
 
 enum { PRJ_SHOT = 0, PRJ_GIBS, PRJ_DEBRIS, PRJ_EJECT, PRJ_ENT, PRJ_AFFINITY, PRJ_VANITY, PRJ_MAX };
 
 struct projent : dynent
 {
-    vec from, to, dest, norm, inertia, sticknrm, stickpos, effectpos;
+    vec from, to, dest, norm, inertia, sticknrm, stickpos, effectpos, lastgood;
     int addtime, lifetime, lifemillis, waittime, spawntime, fadetime, lastradial, lasteffect, lastbounce, beenused, extinguish, stuck;
-    float movement, distance, lifespan, lifesize, minspeed;
+    float movement, distance, lifespan, lifesize, speedmin, speedmax;
     bool local, limited, escaped, child;
     int projtype, projcollide, interacts;
-    float elasticity, reflectivity, relativity, waterfric;
+    float elasticity, reflectivity, relativity, liquidcoast;
     int schan, id, weap, value, flags, hitflags;
     entitylight light;
     gameent *owner, *target, *stick;
@@ -1409,11 +1461,11 @@ struct projent : dynent
         type = ENT_PROJ;
         state = CS_ALIVE;
         norm = vec(0, 0, 1);
-        inertia = sticknrm, stickpos = vec(0, 0, 0);
+        inertia = sticknrm = stickpos = lastgood = vec(0, 0, 0);
         effectpos = vec(-1e16f, -1e16f, -1e16f);
         addtime = lifetime = lifemillis = waittime = spawntime = fadetime = lastradial = lasteffect = lastbounce = beenused = flags = 0;
         schan = id = weap = value = -1;
-        movement = distance = lifespan = minspeed = 0;
+        movement = distance = lifespan = speedmin = speedmax = 0;
         curscale = speedscale = lifesize = 1;
         extinguish = stuck = interacts = 0;
         limited = escaped = child = false;
@@ -1430,17 +1482,16 @@ struct projent : dynent
 
 struct cament
 {
-    enum { DISTMIN = 8, DISTMAX = 512, TRACKMAX = 8 };
     enum { ENTITY = 0, PLAYER, AFFINITY, MAX };
 
     int type, id, inview[MAX], lastyawtime, lastpitchtime;
     vec o, dir;
-    float dist, mindist, maxdist, lastyaw, lastpitch;
+    float dist, lastyaw, lastpitch;
     gameent *player;
     bool ignore;
     cament *moveto;
 
-    cament() : type(-1), id(-1), mindist(DISTMIN), maxdist(DISTMAX), player(NULL), ignore(false), moveto(NULL)
+    cament() : type(-1), id(-1), player(NULL), ignore(false), moveto(NULL)
     {
         reset();
         resetlast();
@@ -1459,7 +1510,7 @@ struct cament
         lastyaw = lastpitch = 0;
     }
 
-    static bool camsort(const cament *a, const cament *b)
+    static bool compare(const cament *a, const cament *b)
     {
         if(!a->ignore && b->ignore) return true;
         if(a->ignore && !b->ignore) return false;
@@ -1480,29 +1531,26 @@ struct cament
 namespace client
 {
     extern int showpresence, showteamchange;
-    extern bool sendplayerinfo, sendcrcinfo, sendgameinfo, demoplayback;
+    extern bool sendplayerinfo, sendcrcinfo, sendgameinfo, demoplayback, isready, needsmap, gettingmap;
     extern void clearvotes(gameent *d, bool msg = false);
     extern void ignore(int cn);
     extern void unignore(int cn);
     extern bool isignored(int cn);
     extern void addmsg(int type, const char *fmt = NULL, ...);
+    extern void saytext(gameent *f, gameent *t, int flags, char *text);
     extern void c2sinfo(bool force = false);
     extern bool haspriv(gameent *d, int priv = PRIV_NONE);
 }
 
 namespace physics
 {
-    extern int smoothmove, smoothdist, pacingstyle;
+    extern int smoothmove, smoothdist;
     extern bool isghost(gameent *d, gameent *e);
     extern bool carryaffinity(gameent *d);
     extern bool dropaffinity(gameent *d);
     extern bool secondaryweap(gameent *d, bool zoom = false);
     extern bool allowimpulse(physent *d, int level = 0);
-    extern bool jetpack(physent *d);
-    extern bool sliding(physent *d, bool power = false);
-    extern bool pacing(physent *d, bool turn = true);
     extern bool canimpulse(physent *d, int level = 0, bool kick = false);
-    extern bool canjet(physent *d);
     extern bool movecamera(physent *pl, const vec &dir, float dist, float stepdist);
     extern void smoothplayer(gameent *d, int res, bool local);
     extern void update();
@@ -1521,7 +1569,7 @@ namespace projs
     extern void destruct(gameent *d, int id);
     extern void sticky(gameent *d, int id, vec &norm, vec &pos, gameent *f = NULL);
     extern void shootv(int weap, int flags, int sub, int offset, float scale, vec &from, vector<shotmsg> &shots, gameent *d, bool local);
-    extern void drop(gameent *d, int weap, int ent, int ammo = -1, int reloads = -1, bool local = true, int index = 0, int targ = -1);
+    extern void drop(gameent *d, int weap, int ent, int ammo = -1, bool local = true, int index = 0, int targ = -1);
     extern void adddynlights();
     extern void render();
 }
@@ -1531,7 +1579,7 @@ namespace weapons
     extern int autoreloading;
     extern int slot(gameent *d, int n, bool back = false);
     extern bool weapselect(gameent *d, int weap, int filter, bool local = true);
-    extern bool weapreload(gameent *d, int weap, int load = -1, int ammo = -1, int reloads = -1, bool local = true);
+    extern bool weapreload(gameent *d, int weap, int load = -1, int ammo = -1, bool local = true);
     extern void weapdrop(gameent *d, int w = -1);
     extern void checkweapons(gameent *d);
     extern float accmod(gameent *d, bool zooming, int *x = NULL);
@@ -1542,10 +1590,16 @@ namespace weapons
 
 namespace hud
 {
-    extern char *chattex, *insigniatex, *playertex, *deadtex, *waitingtex, *spectatortex, *editingtex, *dominatingtex, *dominatedtex, *inputtex, *bliptex, *flagtex, *bombtex, *arrowtex, *alerttex, *questiontex, *flagdroptex, *flagtakentex, *bombdroptex, *bombtakentex, *attacktex,
-                *inventorytex, *indicatortex, *crosshairtex, *hithairtex, *spree1tex, *spree2tex, *spree3tex, *spree4tex, *multi1tex, *multi2tex, *multi3tex, *headshottex, *dominatetex, *revengetex, *firstbloodtex, *breakertex;
-    extern int hudwidth, hudheight, hudsize, lastteam, lastnewgame, damageresidue, damageresiduefade, shownotices, radarstyle, radaraffinitynames, inventorygame, inventoryscore, inventoryscorebg;
-    extern float noticescale, inventoryblend, inventoryskew, radaraffinityblend, radarblipblend, radaraffinitysize, inventoryglow, inventoryscoresize, inventoryscoreshrink, inventoryscoreshrinkmax;
+    extern char *chattex, *insigniatex, *playertex, *deadtex, *waitingtex, *spectatortex, *editingtex, *dominatingtex, *dominatedtex, *inputtex,
+        *bliptex, *pointtex, *flagtex, *bombtex, *arrowtex, *arrowrighttex, *arrowdowntex, *arrowlefttex, *alerttex, *questiontex, *flagdroptex,
+        *flagtakentex, *bombdroptex, *bombtakentex, *attacktex, *warningtex, *inventorytex, *indicatortex, *crosshairtex, *hithairtex,
+        *spree1tex, *spree2tex, *spree3tex, *spree4tex, *multi1tex, *multi2tex, *multi3tex, *headshottex, *dominatetex, *revengetex,
+        *firstbloodtex, *breakertex;
+    extern int hudwidth, hudheight, hudsize, lastteam, damageresidue, damageresiduefade, shownotices, radaraffinitynames,
+        inventorygame, inventoryscore, inventoryscorespec, inventoryscorebg, inventoryscoreinfo, inventoryscorebreak, inventoryscorepos, inventoryracestyle,
+        teamhurthud, teamhurttime, teamhurtdist;
+    extern float noticescale, inventoryblend, inventoryskew, radaraffinityblend, radarblipblend, radaraffinitysize,
+        inventoryglow, inventoryscoresize, inventoryscoreshrink, inventoryscoreshrinkmax;
     extern vector<int> teamkills;
     extern const char *icontex(int type, int value);
     extern bool chkcond(int val, bool cond);
@@ -1554,45 +1608,42 @@ namespace hud
     extern void drawpointertex(const char *tex, int x, int y, int s, float r = 1, float g = 1, float b = 1, float fade = 1);
     extern void drawpointer(int w, int h, int index);
     extern int numteamkills();
-    extern float radarrange();
+    extern int radarrange();
     extern void drawblip(const char *tex, float area, int w, int h, float s, float blend, int style, vec &pos, const vec &colour = vec(1, 1, 1), const char *font = "reduced", const char *text = NULL, ...);
     extern int drawprogress(int x, int y, float start, float length, float size, bool left, float r = 1, float g = 1, float b = 1, float fade = 1, float skew = 1, const char *font = NULL, const char *text = NULL, ...);
     extern int drawitembar(int x, int y, float size, bool left, float r = 1, float g = 1, float b = 1, float fade = 1, float skew = 1, float amt = 1, int type = 0);
     extern int drawitem(const char *tex, int x, int y, float size, float sub = 0, bool bg = true, bool left = false, float r = 1, float g = 1, float b = 1, float fade = 1, float skew = 1, const char *font = NULL, const char *text = NULL, ...);
+    extern int drawitemtextx(int x, int y, float size, int align = TEXT_LEFT_UP, float skew = 1, const char *font = NULL, float blend = 1, const char *text = NULL, ...);
     extern int drawitemtext(int x, int y, float size, bool left = false, float skew = 1, const char *font = NULL, float blend = 1, const char *text = NULL, ...);
     extern int drawweapons(int x, int y, int s, float blend = 1);
     extern int drawhealth(int x, int y, int s, float blend = 1, bool interm = false);
     extern void drawinventory(int w, int h, int edge, float blend = 1);
-    extern void damage(int n, const vec &loc, gameent *actor, int weap, int flags);
+    extern void damage(int n, const vec &loc, gameent *v, int weap, int flags);
     extern const char *teamtexname(int team = T_NEUTRAL);
     extern const char *itemtex(int type, int stype);
-    extern const char *privname(int priv = PRIV_NONE, int aitype = AI_NONE);
-    extern const char *privtex(int priv = PRIV_NONE, int aitype = AI_NONE);
+    extern const char *privtex(int priv = PRIV_NONE, int actortype = A_PLAYER);
     extern bool canshowscores();
     extern void showscores(bool on, bool interm = false, bool onauto = true, bool ispress = false);
     extern score &teamscore(int team);
     extern void resetscores();
-    extern int trialinventory(int x, int y, int s, float blend);
-    extern int drawscore(int x, int y, int s, int m, float blend);
+    extern int raceinventory(int x, int y, int s, float blend);
+    extern int drawscore(int x, int y, int s, int m, float blend, int count);
+    extern void cleanup();
 }
 
 enum { CTONE_TEAM = 0, CTONE_TONE, CTONE_TEAMED, CTONE_ALONE, CTONE_MIXED, CTONE_TMIX, CTONE_AMIX, CTONE_MAX };
 namespace game
 {
-    extern int gamemode, mutators, nextmode, nextmuts, timeremaining, maptime, lastzoom, lasttvcam, lasttvchg, spectvtime, waittvtime,
+    extern int gamestate, gamemode, mutators, nextmode, nextmuts, timeremaining, lasttimeremain, maptime, lastzoom, lasttvcam, lasttvchg, spectvtime, waittvtime,
             bloodfade, bloodsize, bloodsparks, debrisfade, eventiconfade, eventiconshort,
-            announcefilter, dynlighteffects, aboveheadnames, followthirdperson,
-#ifndef MEK
-            forceplayermodel,
-#endif
-            playerovertone, playerundertone, playerdisplaytone, playereffecttone;
+            announcefilter, dynlighteffects, aboveheadnames, followthirdperson, nogore, forceplayermodel,
+            playerovertone, playerundertone, playerdisplaytone, playereffecttone, playerteamtone, follow, specmode, spectvfollow, spectvfollowing;
     extern float bloodscale, debrisscale, aboveitemiconsize;
-    extern bool intermission, zooming;
+    extern bool zooming;
     extern vec swaypush, swaydir;
     extern string clientmap;
 
     extern gameent *player1, *focus;
-    extern void resetfollow();
     extern vector<gameent *> players, waiting;
 
     struct avatarent : dynent
@@ -1601,12 +1652,9 @@ namespace game
     };
     extern avatarent avatarmodel, bodymodel;
 
-#ifdef VANITY
     extern void vanityreset();
     extern void vanitybuild(gameent *d);
     extern const char *vanityfname(gameent *d, int n, bool proj = false);
-#endif
-
     extern bool followswitch(int n, bool other = false);
     extern vector<cament *> cameras;
     extern int numwaiting();
@@ -1614,7 +1662,7 @@ namespace game
     extern gameent *getclient(int cn);
     extern gameent *intersectclosest(vec &from, vec &to, gameent *at);
     extern void clientdisconnected(int cn, int reason = DISC_NONE);
-    extern const char *colourname(gameent *d, char *name = NULL, bool icon = true, bool dupname = true);
+    extern const char *colourname(gameent *d, char *name = NULL, bool icon = true, bool dupname = true, int colour = 3);
     extern const char *colourteam(int team, const char *icon = "");
     extern int findcolour(gameent *d, bool tone = true, bool mix = false);
     extern int getcolour(gameent *d, int level = 0);
@@ -1642,21 +1690,23 @@ namespace game
     extern void resetcamera(bool cam = true, bool input = true);
     extern void resetworld();
     extern void resetstate();
-    extern void hiteffect(int weap, int flags, int damage, gameent *d, gameent *actor, vec &dir, bool local = false);
-    extern void damaged(int weap, int flags, int damage, int health, int armour, gameent *d, gameent *actor, int millis, vec &dir);
-    extern void killed(int weap, int flags, int damage, gameent *d, gameent *actor, vector<gameent*> &log, int style, int material);
-    extern void timeupdate(int timeremain);
+    extern void hiteffect(int weap, int flags, int damage, gameent *d, gameent *v, vec &dir, vec &vel, float dist, bool local = false);
+    extern void damaged(int weap, int flags, int damage, int health, gameent *d, gameent *v, int millis, vec &dir, vec &vel, float dist);
+    extern void killed(int weap, int flags, int damage, gameent *d, gameent *v, vector<gameent*> &log, int style, int material);
+    extern void timeupdate(int state, int remain);
     extern vec rescolour(dynent *d, int c = PULSE_BURN);
     extern float rescale(gameent *d);
+    extern float opacity(gameent *d, bool third = true);
+    extern void footstep(gameent *d, int curfoot = -1);
 }
 
 namespace entities
 {
     extern int showentdescs, simpleitems;
     extern vector<extentity *> ents;
-    extern int lastenttype[MAXENTTYPES], lastusetype[EU_MAX];
-    extern bool execitem(int n, dynent *d);
-    extern bool collateitems(dynent *d, vector<actitem> &actitems);
+    extern int lastuse(int type);
+    extern bool execitem(int n, dynent *d, vec &pos, float dist);
+    extern bool collateitems(dynent *d, vec &pos, float radius, vector<actitem> &actitems);
     extern void checkitems(dynent *d);
     extern void putitems(packetbuf &p);
     extern void execlink(gameent *d, int index, bool local, int ignore = -1);
@@ -1664,19 +1714,13 @@ namespace entities
     extern bool tryspawn(dynent *d, const vec &o, float yaw = 0, float pitch = 0);
     extern void spawnplayer(gameent *d, int ent = -1, bool suicide = false);
     extern const char *entinfo(int type, attrvector &attr, bool full = false, bool icon = false);
-    extern void useeffects(gameent *d, int ent, int ammoamt, int reloadamt, bool spawn, int weap, int drop, int ammo = -1, int reloads = -1);
+    extern void useeffects(gameent *d, int ent, int ammoamt, bool spawn, int weap, int drop, int ammo = -1);
     extern const char *entmdlname(int type, attrvector &attr);
     extern const char *findname(int type);
     extern void adddynlights();
     extern void render();
     extern void update();
 }
-#elif defined(GAMESERVER)
-namespace client
-{
-    extern const char *getname();
-    extern bool sendcmd(int nargs, const char *cmd, const char *arg);
-}
 #endif
 #include "capture.h"
 #include "defend.h"
diff --git a/src/game/gamemode.h b/src/game/gamemode.h
index c9933b3..1dd3472 100644
--- a/src/game/gamemode.h
+++ b/src/game/gamemode.h
@@ -1,42 +1,32 @@
-#ifdef CAMPAIGN
 enum
 {
-    G_DEMO = 0, G_EDITMODE, G_CAMPAIGN, G_DEATHMATCH, G_CAPTURE, G_DEFEND, G_BOMBER, G_TRIAL, G_GAUNTLET, G_MAX,
-    G_START = G_EDITMODE, G_PLAY = G_CAMPAIGN, G_FIGHT = G_DEATHMATCH,
-    G_RAND = G_BOMBER-G_DEATHMATCH+1, G_COUNT = G_MAX-G_PLAY,
-    G_NEVER = (1<<G_DEMO)|(1<<G_EDITMODE)|(1<<G_GAUNTLET),
-    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)|(1<<G_GAUNTLET),
-    G_SW = (1<<G_TRIAL),
-};
-#else
-enum
-{
-    G_DEMO = 0, G_EDITMODE, G_DEATHMATCH, G_CAPTURE, G_DEFEND, G_BOMBER, G_TRIAL, G_GAUNTLET, G_MAX,
+    G_DEMO = 0, G_EDITMODE, G_DEATHMATCH, G_CAPTURE, G_DEFEND, G_BOMBER, G_RACE, G_MAX,
     G_START = G_EDITMODE, G_PLAY = G_DEATHMATCH, G_FIGHT = G_DEATHMATCH,
     G_RAND = G_BOMBER-G_DEATHMATCH+1, G_COUNT = G_MAX-G_PLAY,
-    G_NEVER = (1<<G_DEMO)|(1<<G_EDITMODE)|(1<<G_GAUNTLET),
-    G_LIMIT = (1<<G_DEATHMATCH)|(1<<G_CAPTURE)|(1<<G_DEFEND)|(1<<G_BOMBER)|(1<<G_GAUNTLET),
-    G_ALL = (1<<G_DEMO)|(1<<G_EDITMODE)|(1<<G_DEATHMATCH)|(1<<G_CAPTURE)|(1<<G_DEFEND)|(1<<G_BOMBER)|(1<<G_TRIAL)|(1<<G_GAUNTLET),
-    G_SW = (1<<G_TRIAL),
+    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_DEATHMATCH)|(1<<G_CAPTURE)|(1<<G_DEFEND)|(1<<G_BOMBER)|(1<<G_RACE),
+    G_SW = (1<<G_RACE),
 };
-#endif
 enum
 {
     G_M_MULTI = 0, G_M_FFA, G_M_COOP, G_M_INSTA, G_M_MEDIEVAL, G_M_KABOOM, G_M_DUEL, G_M_SURVIVOR,
-    G_M_CLASSIC, G_M_ONSLAUGHT, G_M_JETPACK, G_M_VAMPIRE, G_M_EXPERT, G_M_RESIZE,
+    G_M_CLASSIC, G_M_ONSLAUGHT, G_M_FREESTYLE, G_M_VAMPIRE, G_M_RESIZE, G_M_HARD, G_M_BASIC,
     G_M_GSP, G_M_GSP1 = G_M_GSP, G_M_GSP2, G_M_GSP3, G_M_NUM,
     G_M_GSN = G_M_NUM-G_M_GSP,
-    G_M_ALL = (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-    G_M_FILTER = (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-    G_M_ROTATE = (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+    G_M_ALL = (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+    G_M_FILTER = (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+    G_M_ROTATE = (1<<G_M_FFA)|(1<<G_M_CLASSIC),
     G_M_SW = (1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM),
     G_M_DK = (1<<G_M_DUEL)|(1<<G_M_SURVIVOR),
     G_M_IM = (1<<G_M_INSTA)|(1<<G_M_MEDIEVAL),
 };
-
 enum { G_F_GSP = 0, G_F_NUM };
 
+enum { G_S_WAITING = 0, G_S_VOTING, G_S_INTERMISSION, G_S_PLAYING, G_S_OVERTIME, G_S_MAX };
+#define gs_playing(a) (a == G_S_PLAYING || a == G_S_OVERTIME)
+#define gs_intermission(a) (a == G_S_INTERMISSION || a == G_S_VOTING)
+
 struct gametypes
 {
     int type, flags, implied, mutators[G_M_GSN+1];
@@ -57,172 +47,156 @@ gametypes gametype[] = {
     {
         G_EDITMODE, 0, (1<<G_M_FFA)|(1<<G_M_CLASSIC),
         {
-            (1<<G_M_FFA)|(1<<G_M_CLASSIC)|(1<<G_M_JETPACK),
+            (1<<G_M_FFA)|(1<<G_M_CLASSIC)|(1<<G_M_FREESTYLE),
             0, 0, 0
         },
         "editing", "editing", { "", "", "" },
         "create and edit existing maps", { "", "", "" },
     },
-#ifdef CAMPAIGN
-    {
-        G_CAMPAIGN, 0, 0,
-        {
-            (1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE),
-            0, 0, 0
-        },
-        "campaign", "campaign", { "", "", "" },
-        "make your way through the mission alive", { "", "", "" },
-    },
-#endif
     {
         G_DEATHMATCH, 0, 0,
         {
-            (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE),
+            (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC),
             0, 0, 0
         },
         "deathmatch", "dm", { "", "", "" },
-        "shoot to kill and earn points by fragging", { "", "", "" },
+        "shoot to kill and increase score by fragging", { "", "", "" },
     },
     {
         G_CAPTURE, 0, 0,
         {
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1),
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP2),
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP3)
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1),
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP2),
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP3)
         },
         "capture-the-flag", "capture", { "quick", "defend", "protect" },
-        "take the enemy flag and return it to the base", { "dropped flags instantly return to base", "dropped flags must be defended until they reset", "protect the flag and hold the enemy flag to score" },
+        "take the enemy flag and return it to the base to score", { "dropped flags instantly return to base", "dropped flags must be defended until they reset", "protect the flag and hold the enemy flag to score" },
     },
     {
         G_DEFEND, 0, 0,
         {
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
             0
         },
-        "defend-the-flag", "defend", { "quick", "king", "" },
-        "defend the flags to earn points", { "flags secure quicker than normal", "king of the hill with one flag", ""},
+        "defend-and-control", "defend", { "quick", "king", "" },
+        "defend control points to score", { "control points secure quicker than normal", "remain king of the hill to score", ""},
     },
     {
         G_BOMBER, (1<<G_F_GSP), 0,
         {
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
-            (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1),
-            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP2),
-            0
-        },
-        "bomber-ball", "bomber", { "hold", "touchdown", "" },
-        "carry or throw the bomb into the enemy goal to score", { "hold the bomb as long as possible to score points", "carry the bomb into the enemy goal to score", "" },
-    },
-    {
-        G_TRIAL, 0, 0,
-        {
-            (1<<G_M_FFA)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE),
-            0, 0, 0
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+            (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1),
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+            (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP2)|(1<<G_M_GSP3)
         },
-        "time-trial", "trial", { "", "", "" },
-        "compete for the fastest time completing a lap", { "", "", "" },
+        "bomber-ball", "bomber", { "hold", "basket", "attack" },
+        "carry the bomb into the enemy goal to score", { "hold the bomb as long as possible to score", "throw the bomb into the enemy goal to score", "teams take turns attacking and defending" },
     },
     {
-        G_GAUNTLET, 0, 0,
+        G_RACE, (1<<G_F_GSP), 0,
         {
-            (1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
-            (1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
-            (1<<G_M_INSTA)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2),
-            0
+            (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+            (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+            (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_ONSLAUGHT)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+            (1<<G_M_MULTI)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3)
         },
-        "gauntlet", "gauntlet", { "timed", "hard", "" },
-        "attack and defend for the most number laps completed", { "attack and defend for the fastest time completing a lap", "the defending team is permanently buffed", "" },
-    },
+        "race", "race", { "timed", "endurance", "gauntlet" },
+        "compete for the most number of laps", { "compete for the fastest time completing a lap", "impulse meter does not reset at all", "teams take turns running the gauntlet" },
+    }
 };
 mutstypes mutstype[] = {
     {
         G_M_MULTI, (1<<G_M_MULTI),
-        (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-        "multi", "four teams fight to determine the winning side"
+        (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "multi", "four teams battle to determine the winning side"
     },
     {
         G_M_FFA, (1<<G_M_FFA),
-        (1<<G_M_FFA)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_FFA)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "ffa", "every player for themselves"
     },
     {
         G_M_COOP, (1<<G_M_COOP),
-        (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_MULTI)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "coop", "players versus drones"
     },
     {
         G_M_INSTA, (1<<G_M_INSTA),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-        "instagib", "one shot, one kill"
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_FREESTYLE)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "instagib", "one hit kills instantly"
     },
     {
         G_M_MEDIEVAL, (1<<G_M_MEDIEVAL),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_MEDIEVAL)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-        "medieval", "everyone spawns only with swords"
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_MEDIEVAL)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "medieval", "players spawn only with swords"
     },
     {
         G_M_KABOOM,  (1<<G_M_KABOOM),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-        "kaboom",
-        "everyone spawns with explosives only"
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "kaboom", "players spawn with explosives only"
     },
     {
         G_M_DUEL, (1<<G_M_DUEL),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_DUEL)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "duel", "one on one battles to determine the winner"
     },
     {
         G_M_SURVIVOR, (1<<G_M_SURVIVOR),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-        "survivor", "everyone battles to determine the winner"
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "survivor", "players battle to determine the winner"
     },
     {
         G_M_CLASSIC,    (1<<G_M_CLASSIC),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "classic",
         "weapons must be collected from spawns in the arena"
     },
     {
         G_M_ONSLAUGHT, (1<<G_M_ONSLAUGHT),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "onslaught", "waves of enemies fill the battle arena"
     },
     {
-        G_M_JETPACK, (1<<G_M_JETPACK),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-        "jetpack", "everyone comes equipped with a jetpack"
+        G_M_FREESTYLE, (1<<G_M_FREESTYLE),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "freestyle", "players can parkour without limits"
     },
     {
         G_M_VAMPIRE, (1<<G_M_VAMPIRE),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "vampire", "deal damage to regenerate health"
     },
     {
-        G_M_EXPERT, (1<<G_M_EXPERT),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-        "expert", "players can only deal damage by landing headshots"
+        G_M_RESIZE, (1<<G_M_RESIZE),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "resize", "players change size depending on their health"
+    },
+    {
+        G_M_HARD, (1<<G_M_HARD),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "hard", "no regeneration, no radar, no impulse reset"
     },
     {
-        G_M_RESIZE, (1<<G_M_RESIZE),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
-        "resize", "everyone changes size depending on their health"
+        G_M_BASIC, (1<<G_M_BASIC),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        "basic", "players only have the basic weapons that they spawn with"
     },
     {
         G_M_GSP1, (1<<G_M_GSP1),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "gsp1", ""
     },
     {
         G_M_GSP2, (1<<G_M_GSP2),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "gsp2", ""
     },
     {
         G_M_GSP3, (1<<G_M_GSP3),
-        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_JETPACK)|(1<<G_M_VAMPIRE)|(1<<G_M_EXPERT)|(1<<G_M_RESIZE)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
+        (1<<G_M_MULTI)|(1<<G_M_FFA)|(1<<G_M_COOP)|(1<<G_M_INSTA)|(1<<G_M_MEDIEVAL)|(1<<G_M_KABOOM)|(1<<G_M_DUEL)|(1<<G_M_SURVIVOR)|(1<<G_M_CLASSIC)|(1<<G_M_ONSLAUGHT)|(1<<G_M_FREESTYLE)|(1<<G_M_VAMPIRE)|(1<<G_M_RESIZE)|(1<<G_M_HARD)|(1<<G_M_BASIC)|(1<<G_M_GSP1)|(1<<G_M_GSP2)|(1<<G_M_GSP3),
         "gsp3", ""
     },
 };
@@ -237,15 +211,11 @@ extern mutstypes mutstype[];
 
 #define m_demo(a)           (a == G_DEMO)
 #define m_edit(a)           (a == G_EDITMODE)
-#ifdef CAMPAIGN
-#define m_campaign(a)       (a == G_CAMPAIGN)
-#endif
 #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_gauntlet(a)       (a == G_GAUNTLET)
+#define m_race(a)           (a == G_RACE)
 
 #define m_play(a)           (a >= G_PLAY)
 #define m_affinity(a)       (m_capture(a) || m_defend(a) || m_bomber(a))
@@ -261,10 +231,11 @@ extern mutstypes mutstype[];
 #define m_survivor(a,b)     ((b&(1<<G_M_SURVIVOR)) || (gametype[a].implied&(1<<G_M_SURVIVOR)))
 #define m_classic(a,b)      ((b&(1<<G_M_CLASSIC)) || (gametype[a].implied&(1<<G_M_CLASSIC)))
 #define m_onslaught(a,b)    ((b&(1<<G_M_ONSLAUGHT)) || (gametype[a].implied&(1<<G_M_ONSLAUGHT)))
-#define m_jetpack(a,b)      ((b&(1<<G_M_JETPACK)) || (gametype[a].implied&(1<<G_M_JETPACK)))
+#define m_freestyle(a,b)    ((b&(1<<G_M_FREESTYLE)) || (gametype[a].implied&(1<<G_M_FREESTYLE)))
 #define m_vampire(a,b)      ((b&(1<<G_M_VAMPIRE)) || (gametype[a].implied&(1<<G_M_VAMPIRE)))
-#define m_expert(a,b)       ((b&(1<<G_M_EXPERT)) || (gametype[a].implied&(1<<G_M_EXPERT)))
 #define m_resize(a,b)       ((b&(1<<G_M_RESIZE)) || (gametype[a].implied&(1<<G_M_RESIZE)))
+#define m_hard(a,b)         ((b&(1<<G_M_HARD)) || (gametype[a].implied&(1<<G_M_HARD)))
+#define m_basic(a,b)         ((b&(1<<G_M_BASIC)) || (gametype[a].implied&(1<<G_M_BASIC)))
 
 #define m_gsp1(a,b)         ((b&(1<<G_M_GSP1)) || (gametype[a].implied&(1<<G_M_GSP1)))
 #define m_gsp2(a,b)         ((b&(1<<G_M_GSP2)) || (gametype[a].implied&(1<<G_M_GSP2)))
@@ -272,46 +243,33 @@ extern mutstypes mutstype[];
 #define m_gsp(a,b)          (m_gsp1(a,b) || m_gsp2(a,b) || m_gsp3(a,b))
 
 #define m_team(a,b)         (m_multi(a, b) || !m_ffa(a, b))
-#define m_sweaps(a,b)       (m_insta(a, b) || m_medieval(a, b) || m_kaboom(a, b))
-#define m_limited(a,b)      (m_insta(a, b) || m_medieval(a, b))
-#define m_special(a,b)      (m_sweaps(a, b) || !m_classic(a, b))
+#define m_sweaps(a,b)       (m_race(a) || m_insta(a, b) || m_medieval(a, b) || m_kaboom(a, b))
 #define m_loadout(a,b)      (!m_classic(a, b) && !m_sweaps(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))
-#ifdef CAMPAIGN
-#define m_enemies(a,b)      (m_campaign(a) || m_onslaught(a, b))
-#define m_checkpoint(a)     (m_campaign(a) || m_trial(a) || m_gauntlet(a))
-#define m_ghost(a)          (m_campaign(a) ? (G(campaignghost) ? 2 : 0) : (m_trial(a) ? G(trialghost) : (m_gauntlet(a) ? G(gauntletghost) : 0)))
-#else
-#define m_enemies(a,b)      (m_onslaught(a, b))
-#define m_checkpoint(a)     (m_trial(a) || m_gauntlet(a))
-#define m_ghost(a)          (m_trial(a) ? G(trialghost) : (m_gauntlet(a) ? G(gauntletghost) : 0))
-#endif
-#define m_bots(a)           (m_fight(a) && !m_trial(a))
-#define m_nopoints(a,b)     (m_duke(a, b) || (m_bomber(a) && m_gsp1(a, b)))
-#define m_teamscore(a)      (m_dm(a))
-#define m_laptime(a,b)      (m_trial(a) || (m_gauntlet(a) && m_gsp1(a, b)))
+#define m_regen(a,b)        (!m_hard(a,b) && (G(duelregen) || !m_duke(a, b)) && !m_insta(a, b))
+#define m_ghost(a,b)        (m_race(a) && !m_gsp3(a, b))
+#define m_bots(a)           (m_fight(a) && !m_race(a))
+#define m_botbal(a,b)       (m_duel(a, b) ? G(botbalanceduel) : (m_survivor(a, b) ? G(botbalancesurvivor) : G(botbalance)))
+#define m_nopoints(a,b)     (m_duke(a, b) || (m_bomber(a) && m_gsp1(a, b)) || m_race(a))
+#define m_laptime(a,b)      (m_race(a) && m_gsp1(a, b))
+#define m_impulsemeter(a,b) ((m_race(a) && m_gsp2(a, b)) || !m_freestyle(a, b))
 
-#define m_weapon(a,b)       (m_loadout(a, b) ? 0-W_ITEM : (m_medieval(a, b) ? W_SWORD : (m_kaboom(a, b) ? 0-W_BOOM : (m_insta(a, b) ? G(instaweapon) : (m_gauntlet(a) ? G(gauntletweapon) : (m_trial(a) ? G(trialweapon) : G(spawnweapon)))))))
-#define m_xdelay(a,b)       (m_play(a) ? (m_trial(a) ? G(trialdelay) : (m_bomber(a) ? G(bomberdelay) : (m_insta(a, b) ? G(instadelay) : G(spawndelay)))) : 0)
-#define m_delay(a,b)        (m_duke(a,b) ? 0 : m_xdelay(a, b))
+#define m_weapon(a,b)       (m_medieval(a, b) ? W_SWORD : (m_kaboom(a, b) ? W_GRENADE : (m_insta(a, b) ? G(instaweapon) : (m_race(a) && !m_gsp3(a, b) ? G(raceweapon) : G(spawnweapon)))))
+#define m_xdelay(a,b,c)     (m_play(a) ? (m_race(a) ? (!m_gsp3(a, b) || c == T_ALPHA ? G(racedelay) : G(racedelayex)) : (m_bomber(a) ? G(bomberdelay) : (m_insta(a, b) ? G(instadelay) : G(spawndelay)))) : 0)
+#define m_delay(a,b,c)      (m_duke(a,b) ? 0 : m_xdelay(a, b, c))
 #define m_protect(a,b)      (m_duke(a,b) ? G(duelprotect) : (m_insta(a, b) ? G(instaprotect) : G(spawnprotect)))
-#define m_noitems(a,b)      (m_trial(a) || G(itemsallowed) < (m_limited(a,b) ? 2 : 1))
-#ifdef MEK
-#define m_health(a,b,c)     (m_insta(a,b) ? 1 : CLASS(c, health))
-#define m_armour(a,b,c)     (m_insta(a,b) ? 0 : CLASS(c, armour))
-#else
-#define m_health(a,b,c)     (m_insta(a,b) ? 1 : G(spawnhealth))
-#define m_armour(a,b,c)     (m_insta(a,b) ? 0 : G(spawnarmour))
-#endif
+#define m_health(a,b,c)     (m_insta(a,b) ? 1 : PLAYER(c, health))
 #define m_maxhealth(a,b,c)  (int(m_health(a, b, c)*(m_vampire(a,b) ? G(maxhealthvampire) : G(maxhealth))))
-#define m_balance(a,b)      (m_team(a, b) && !m_trial(a) && m_fight(a) && !m_duke(a, b) && (m_gauntlet(a) || ((G(balancemaps) >= 0 ? G(balancemaps) : G(mapbalance)) >= (m_affinity(a) ? 1 : 2))))
-#define m_balreset(a)       (m_capture(a) || m_bomber(a) || m_gauntlet(a))
-#define m_jet(a,b)          (PHYS(gravity) == 0 || m_jetpack(a, b))
+#define m_swapteam(a,b)     (m_team(a, b) && (!m_race(a) || m_gsp3(a, b)) && m_fight(a) && (G(teambalanceduel) || !m_duel(a, b)) && !m_coop(gamemode, mutators) && G(teambalance) >= 3 && G(teambalanceswap))
+#define m_balteam(a,b,c)    (m_team(a, b) && (!m_race(a) || m_gsp3(a, b)) && m_fight(a) && (G(teambalanceduel) || !m_duel(a, b)) && !m_coop(gamemode, mutators) && G(teambalance) >= c)
+#define m_forcebal(a,b)     ((m_bomber(a) && m_gsp3(a, b)) || (m_race(a) && m_gsp3(a, b)))
+#define m_balance(a,b,c)    (m_team(a, b) && (!m_race(a) || m_gsp3(a, b)) && m_fight(a) && (m_forcebal(a, b) || ((G(balanceduke) || !m_duke(a, b)) && ((G(balancemaps) >= 0 ? G(balancemaps) : G(mapbalance)) >= (m_affinity(a) ? 1 : (c ? 2 : 3))))))
+#define m_balreset(a,b)     (G(balancereset) && (G(balancereset) == 2 || m_capture(a) || m_bomber(a) || m_race(a) || m_duke(a, b)))
 
-#define w_reload(w1,w2)     (w1 != W_MELEE ? (isweap(w2) ? (w1 == w2 ? -1 : W(w1, reloads)) : (w1 < 0-w2 ? -1 : W(w1, reloads))) : 0)
-#define w_carry(w1,w2)      (w1 > W_MELEE && (isweap(w2) ? w1 != w2 : w1 >= 0-w2) && (isweap(w1) && W(w1, carried)))
-#define w_attr(a,b,w1,w2)   (m_edit(a) ? w1 : ((w1 >= W_OFFSET && w1 != w2) ? w1 : (w2 == W_GRENADE ? W_MINE : W_GRENADE)))
+#define w_carry(w1,w2)      (isweap(w1) && w1 != W_MELEE && (!isweap(w2) || (w1 != w2 && (w2 != W_GRENADE || w1 != W_MINE))) && (w1 == W_ROCKET || (w1 >= W_OFFSET && w1 < W_ITEM)))
+#define w_reload(w1,w2)     (isweap(w1) && (w1 == W_MELEE || (w1 >= W_OFFSET && w1 < W_ITEM) || (isweap(w2) && (w1 == w2 || (w2 == W_GRENADE && w1 == W_MINE)))))
+#define w_item(w1,w2)       (isweap(w1) && (w1 >= W_OFFSET && w1 < W_MAX && (!isweap(w2) || (w1 != w2 && (w2 != W_GRENADE || w1 != W_MINE)))))
+#define w_attr(a,b,t,w1,w2) (t != WEAPON || m_edit(a) ? w1 : (w1 != w2 ? (!m_classic(a, b) ? (w1 >= W_ITEM ? w1 : -1) : (w1 >= W_OFFSET && w1 < W_MAX ? w1 : -1)) : (w1 != W_GRENADE ? W_GRENADE : W_MINE)))
 #define w_spawn(weap)       int(ceilf(G(itemspawntime)*W(weap, frequency)))
 
 #define mapshrink(a,b,c,d) if((a) && (b) && (c) && *(c)) \
@@ -325,9 +283,8 @@ extern mutstypes mutstype[];
 }
 #define mapcull(a,b,c,d,e,f) \
 { \
-    mapshrink(m_multi(b, c) && (m_capture(b) || (m_bomber(b) && !m_gsp2(b, c))), a, G(multimaps), false) \
+    mapshrink(m_multi(b, c) && (m_capture(b) || (m_bomber(b) && !m_gsp1(b, c))), a, G(multimaps), false) \
     mapshrink(m_duel(b, c), a, G(duelmaps), false) \
-    mapshrink(m_jetpack(b, c), a, G(jetpackmaps), false) \
     if(d > 0 && e >= 2 && m_fight(b) && !m_duel(b, c)) \
     { \
         mapshrink(G(smallmapmax) && d <= G(smallmapmax), a, G(smallmaps), false) \
@@ -336,59 +293,40 @@ extern mutstypes mutstype[];
     } \
     mapshrink(!f, a, G(previousmaps), true) \
 }
-#ifdef CAMPAIGN
-#define maplist(a,b,c,d,e,f) \
-{ \
-    if(m_campaign(b)) a = newstring(G(campaignmaps)); \
-    else if(m_capture(b)) a = newstring(G(capturemaps)); \
-    else if(m_defend(b)) a = newstring(m_gsp3(b, c) ? G(kingmaps) : G(defendmaps)); \
-    else if(m_bomber(b)) a = newstring(m_gsp2(b, c) ? G(holdmaps) : G(bombermaps)); \
-    else if(m_trial(b)) a = newstring(G(trialmaps)); \
-    else if(m_gauntlet(b)) a = newstring(G(gauntletmaps)); \
-    else if(m_fight(b)) a = newstring(G(mainmaps)); \
-    else a = newstring(G(allowmaps)); \
-    if(e) mapcull(a, b, c, d, e, f) \
-    else mapshrink(!f, a, G(previousmaps), true) \
-}
-#else
 #define maplist(a,b,c,d,e,f) \
 { \
     if(m_capture(b)) a = newstring(G(capturemaps)); \
-    else if(m_defend(b)) a = newstring(m_gsp3(b, c) ? G(kingmaps) : G(defendmaps)); \
-    else if(m_bomber(b)) a = newstring(m_gsp2(b, c) ? G(holdmaps) : G(bombermaps)); \
-    else if(m_trial(b)) a = newstring(G(trialmaps)); \
-    else if(m_gauntlet(b)) a = newstring(G(gauntletmaps)); \
+    else if(m_defend(b)) a = newstring(m_gsp2(b, c) ? G(kingmaps) : G(defendmaps)); \
+    else if(m_bomber(b)) a = newstring(m_gsp1(b, c) ? G(holdmaps) : G(bombermaps)); \
+    else if(m_race(b)) a = newstring(G(racemaps)); \
     else if(m_fight(b)) a = newstring(G(mainmaps)); \
     else a = newstring(G(allowmaps)); \
     if(e) mapcull(a, b, c, d, e, f) \
     else mapshrink(!f, a, G(previousmaps), true) \
 }
-#endif
 #ifdef GAMESERVER
-#ifdef CAMPAIGN
-SVAR(0, modename, "demo editing campaign deathmatch capture-the-flag defend-the-flag bomber-ball time-trial gauntlet");
-SVAR(0, modeidxname, "demo editing campaign deathmatch capture defend bomber trial gauntlet");
-VAR(0, modeidxcampaign, 1, G_CAMPAIGN, -1);
-VAR(0, modebitcampaign, 1, (1<<G_CAMPAIGN), -1);
-#else
-SVAR(0, modename, "demo editing deathmatch capture-the-flag defend-the-flag bomber-ball time-trial gauntlet");
-SVAR(0, modeidxname, "demo editing deathmatch capture defend bomber trial gauntlet");
-#endif
+SVAR(0, gamestatename, "waiting voting intermission playing overtime");
+VAR(0, gamestatewaiting, 1, G_S_WAITING, -1);
+VAR(0, gamestatevoting, 1, G_S_VOTING, -1);
+VAR(0, gamestateintermission, 1, G_S_INTERMISSION, -1);
+VAR(0, gamestateplaying, 1, G_S_PLAYING, -1);
+VAR(0, gamestateovertime, 1, G_S_OVERTIME, -1);
+VAR(0, gamestatenum, 1, G_S_MAX, -1);
+SVAR(0, modename, "demo editing deathmatch capture-the-flag defend-and-control bomber-ball race");
+SVAR(0, modeidxname, "demo editing deathmatch capture defend bomber race");
 VAR(0, modeidxdemo, 1, G_DEMO, -1);
 VAR(0, modeidxediting, 1, G_EDITMODE, -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, modeidxgauntlet, 1, G_GAUNTLET, -1);
+VAR(0, modeidxrace, 1, G_RACE, -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);
 VAR(0, modebitdemo, 1, (1<<G_DEMO), -1);
 VAR(0, modebitediting, 1, (1<<G_EDITMODE), -1);
@@ -396,10 +334,10 @@ VAR(0, modebitdeathmatch, 1, (1<<G_DEATHMATCH), -1);
 VAR(0, modebitcapture, 1, (1<<G_CAPTURE), -1);
 VAR(0, modebitdefend, 1, (1<<G_DEFEND), -1);
 VAR(0, modebitbomber, 1, (1<<G_BOMBER), -1);
-VAR(0, modebittrial, 1, (1<<G_TRIAL), -1);
-VAR(0, modebitgauntlet, 1, (1<<G_GAUNTLET), -1);
-SVAR(0, mutsname, "multi ffa coop instagib medieval kaboom duel survivor classic onslaught jetpack vampire expert resize");
-SVAR(0, mutsidxname, "multi ffa coop instagib medieval kaboom duel survivor classic onslaught jetpack vampire expert resize");
+VAR(0, modebitrace, 1, (1<<G_RACE), -1);
+VAR(0, modebitall, 1, G_ALL, -1);
+SVAR(0, mutsname, "multi ffa coop instagib medieval kaboom duel survivor classic onslaught freestyle vampire resize hard basic");
+SVAR(0, mutsidxname, "multi ffa coop instagib medieval kaboom duel survivor classic onslaught freestyle vampire resize hard basic");
 VAR(0, mutsidxmulti, 1, G_M_MULTI, -1);
 VAR(0, mutsidxffa, 1, G_M_FFA, -1);
 VAR(0, mutsidxcoop, 1, G_M_COOP, -1);
@@ -410,10 +348,11 @@ VAR(0, mutsidxduel, 1, G_M_DUEL, -1);
 VAR(0, mutsidxsurvivor, 1, G_M_SURVIVOR, -1);
 VAR(0, mutsidxclassic, 1, G_M_CLASSIC, -1);
 VAR(0, mutsidxonslaught, 1, G_M_ONSLAUGHT, -1);
-VAR(0, mutsidxjetpack, 1, G_M_JETPACK, -1);
+VAR(0, mutsidxfreestyle, 1, G_M_FREESTYLE, -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, mutsidxhard, 1, G_M_HARD, -1);
+VAR(0, mutsidxbasic, 1, G_M_BASIC, -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);
@@ -430,11 +369,13 @@ VAR(0, mutsbitduel, 1, (1<<G_M_DUEL), -1);
 VAR(0, mutsbitsurvivor, 1, (1<<G_M_SURVIVOR), -1);
 VAR(0, mutsbitclassic, 1, (1<<G_M_CLASSIC), -1);
 VAR(0, mutsbitonslaught, 1, (1<<G_M_ONSLAUGHT), -1);
-VAR(0, mutsbitjetpack, 1, (1<<G_M_JETPACK), -1);
+VAR(0, mutsbitfreestyle, 1, (1<<G_M_FREESTYLE), -1);
 VAR(0, mutsbitvampire, 1, (1<<G_M_VAMPIRE), -1);
-VAR(0, mutsbitexpert, 1, (1<<G_M_EXPERT), -1);
 VAR(0, mutsbitresize, 1, (1<<G_M_RESIZE), -1);
+VAR(0, mutsbithard, 1, (1<<G_M_HARD), -1);
+VAR(0, mutsbitbasic, 1, (1<<G_M_BASIC), -1);
 VAR(0, mutsbitgsp1, 1, (1<<G_M_GSP1), -1);
 VAR(0, mutsbitgsp2, 1, (1<<G_M_GSP2), -1);
 VAR(0, mutsbitgsp3, 1, (1<<G_M_GSP3), -1);
+VAR(0, mutsbitall, 1, G_M_ALL, -1);
 #endif
diff --git a/src/game/hud.cpp b/src/game/hud.cpp
index 101c803..49ea254 100644
--- a/src/game/hud.cpp
+++ b/src/game/hud.cpp
@@ -2,8 +2,7 @@
 namespace hud
 {
     const int NUMSTATS = 11;
-    int damageresidue = 0, hudwidth = 0, hudheight = 0, lastteam = 0, lastnewgame = 0, laststats = 0, prevstats[NUMSTATS] = {0}, curstats[NUMSTATS] = {0};
-    bool forceprogress = false;
+    int damageresidue = 0, hudwidth = 0, hudheight = 0, lastteam = 0, laststats = 0, prevstats[NUMSTATS] = {0}, curstats[NUMSTATS] = {0};
 
     #include "compass.h"
     vector<int> teamkills;
@@ -24,13 +23,18 @@ namespace hud
     VAR(IDF_PERSIST, hudminimal, 0, 0, 1);
 
     VAR(IDF_PERSIST, showdemoplayback, 0, 1, 1);
-    FVAR(IDF_PERSIST, gapsize, 0, 0.01f, 1000);
+    FVAR(IDF_PERSIST, edgesize, 0, 0.005f, 1000);
 
     VAR(IDF_PERSIST, showconsole, 0, 2, 2);
     VAR(IDF_PERSIST, shownotices, 0, 3, 4);
     VAR(IDF_PERSIST, showevents, 0, 3, 7);
+    VAR(IDF_PERSIST, showloadingaspect, 0, 2, 3);
+    VAR(IDF_PERSIST, showloadingmapbg, 0, 1, 1);
+    VAR(IDF_PERSIST, showloadinggpu, 0, 1, 1);
+    VAR(IDF_PERSIST, showloadingversion, 0, 1, 1);
+    VAR(IDF_PERSIST, showloadingurl, 0, 1, 1);
 
-    VAR(IDF_PERSIST, showfps, 0, 1, 3);
+    VAR(IDF_PERSIST, showfps, 0, 0, 3);
     VAR(IDF_PERSIST, showstats, 0, 1, 2);
     VAR(IDF_PERSIST, statrate, 0, 200, 1000);
     FVAR(IDF_PERSIST, statblend, 0, 1, 1);
@@ -40,14 +44,14 @@ namespace hud
     COMMAND(0, toggleconsole, "");
 
     VAR(IDF_PERSIST, titlefade, 0, 1000, 10000);
-    VAR(IDF_PERSIST, tvmodefade, 0, 1000, VAR_MAX);
-    VAR(IDF_PERSIST, spawnfade, 0, 500, VAR_MAX);
+    VAR(IDF_PERSIST, tvmodefade, 0, 250, VAR_MAX);
+    VAR(IDF_PERSIST, spawnfade, 0, 250, VAR_MAX);
 
-    VAR(IDF_PERSIST, commandfade, 0, 200, VAR_MAX);
-    FVAR(IDF_PERSIST, commandfadeamt, 0, 0.5f, 1);
+    VAR(IDF_PERSIST, commandfade, 0, 250, VAR_MAX);
+    FVAR(IDF_PERSIST, commandfadeamt, 0, 0.75f, 1);
     FVAR(IDF_PERSIST, commandfadeskew, 0, 0, 1);
     FVAR(IDF_PERSIST, commandscale, FVAR_NONZERO, 1, FVAR_MAX);
-    VAR(IDF_PERSIST, uifade, 0, 200, VAR_MAX);
+    VAR(IDF_PERSIST, uifade, 0, 250, VAR_MAX);
     FVAR(IDF_PERSIST, uifadeamt, 0, 0.5f, 1);
 
     int conskip = 0;
@@ -58,24 +62,30 @@ namespace hud
     }
     COMMANDN(0, conskip, setconskip, "i");
 
-    VAR(IDF_PERSIST, consize, 0, 5, 100);
+    VAR(IDF_PERSIST, consize, 1, 5, 100);
     VAR(IDF_PERSIST, contime, 0, 30000, VAR_MAX);
-    VAR(IDF_PERSIST, confade, 0, 1000, VAR_MAX);
+    VAR(IDF_PERSIST, confade, 0, 500, VAR_MAX);
     VAR(IDF_PERSIST, conoverflow, 0, 5, VAR_MAX);
     VAR(IDF_PERSIST, concenter, 0, 0, 1);
     VAR(IDF_PERSIST, confilter, 0, 1, 1);
+    VAR(IDF_PERSIST, condate, 0, 0, 1);
     FVAR(IDF_PERSIST, conblend, 0, 0.6f, 1);
     FVAR(IDF_PERSIST, conscale, FVAR_NONZERO, 1, FVAR_MAX);
-    VAR(IDF_PERSIST, chatconsize, 0, 5, 100);
+    SVAR(IDF_PERSIST, condateformat, "%H:%M:%S");
+    VAR(IDF_PERSIST, chatconsize, 1, 5, 100);
     VAR(IDF_PERSIST, chatcontime, 0, 30000, VAR_MAX);
-    VAR(IDF_PERSIST, chatconfade, 0, 2000, VAR_MAX);
+    VAR(IDF_PERSIST, chatconfade, 0, 1000, VAR_MAX);
     VAR(IDF_PERSIST, chatconoverflow, 0, 5, VAR_MAX);
+    VAR(IDF_PERSIST, chatcondate, 0, 0, 1);
     FVAR(IDF_PERSIST, chatconblend, 0, 1, 1);
     FVAR(IDF_PERSIST, chatconscale, FVAR_NONZERO, 1, FVAR_MAX);
+    SVAR(IDF_PERSIST, chatcondateformat, "%H:%M:%S");
 
     FVAR(IDF_PERSIST, selfconblend, 0, 1, 1);
     FVAR(IDF_PERSIST, fullconblend, 0, 1, 1);
 
+    VAR(IDF_PERSIST, capslockwarn, 0, 1, 1);
+
     FVAR(IDF_PERSIST, noticeoffset, -1, 0.3f, 1);
     FVAR(IDF_PERSIST, noticeblend, 0, 1, 1);
     FVAR(IDF_PERSIST, noticescale, 1e-4f, 1, 1000);
@@ -86,7 +96,7 @@ namespace hud
     VAR(IDF_PERSIST, noticetime, 0, 5000, VAR_MAX);
     VAR(IDF_PERSIST, obitnotices, 0, 2, 2);
     VAR(IDF_PERSIST, teamnotices, 0, 2, 2);
-    VAR(IDF_PERSIST, teamnoticedelay, 0, 5000, VAR_MAX);
+    VAR(IDF_PERSIST, teamnoticedelay, 0, 2500, VAR_MAX);
 
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, teamtex, "<grey>textures/team", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, teamalphatex, "<grey>textures/teamalpha", 3);
@@ -106,20 +116,23 @@ namespace hud
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, editingtex, "<grey>textures/editing", 3);
 
     TVAR(IDF_PERSIST|IDF_PRELOAD, progresstex, "<grey>textures/progress", 3);
+    TVAR(IDF_PERSIST|IDF_PRELOAD, progringtex, "<grey>textures/progring", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, warningtex, "<grey>textures/warning", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, questiontex, "<grey>textures/question", 3);
 
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, healthtex, "<grey>textures/hud/health", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, healthbgtex, "<grey>textures/hud/healthbg", 3);
-    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, armourtex, "<grey>textures/hud/impulse", 3);
-    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, armourbgtex, "<grey>textures/hud/impulsebg", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, impulsetex, "<grey>textures/hud/impulse", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, impulsebgtex, "<grey>textures/hud/impulsebg", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, inventorytex, "<grey>textures/hud/inventory", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, inventorybartex, "<grey>textures/hud/inventorybar", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, flagtex, "<grey>textures/hud/flag", 3);
+    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, pointtex, "<grey>textures/hud/point", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, bombtex, "<grey>textures/hud/bomb", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, arrowtex, "<grey>textures/hud/arrow", 3);
+    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, arrowrighttex, "<grey><rotate:1>textures/hud/arrow", 3);
+    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, arrowdowntex, "<grey><rotate:2>textures/hud/arrow", 3);
+    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, arrowlefttex, "<grey><rotate:3>textures/hud/arrow", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, alerttex, "<grey>textures/hud/alert", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, flagdroptex, "<grey>textures/hud/flagdrop", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, flagtakentex, "<grey>textures/hud/flagtaken", 3);
@@ -129,14 +142,47 @@ namespace hud
 
     VAR(IDF_PERSIST|IDF_HEX, inventorytone, -CTONE_MAX, -CTONE_TEAM-1, 0xFFFFFF);
     VAR(IDF_PERSIST|IDF_HEX, crosshairtone, -CTONE_MAX, 0, 0xFFFFFF);
+    VAR(IDF_PERSIST|IDF_HEX, hitcrosshairtone, -CTONE_MAX, 0, 0xFFFFFF);
     VAR(IDF_PERSIST|IDF_HEX, noticestone, -CTONE_MAX, 0, 0xFFFFFF);
     VAR(IDF_PERSIST|IDF_HEX, clipstone, -CTONE_MAX, -CTONE_TEAM-1, 0xFFFFFF);
     VAR(IDF_PERSIST|IDF_HEX, radartone, -CTONE_MAX, -CTONE_TEAM-1, 0xFFFFFF);
 
+    VAR(IDF_PERSIST, teamhurthud, 0, 1, 3); // 0 = off, 1 = full body particle, 2 = fixed position and size
     VAR(IDF_PERSIST, teamhurttime, 0, 2500, VAR_MAX);
     VAR(IDF_PERSIST, teamhurtdist, 0, 0, VAR_MAX);
     FVAR(IDF_PERSIST, teamhurtsize, 0, 0.0175f, 1000);
 
+    enum { BORDER_PLAY, BORDER_EDIT, BORDER_SPEC, BORDER_WAIT, BORDER_BG, BORDER_MAX };
+    enum { BORDERP_TOP, BORDERP_BOTTOM, BORDERP_MAX, BORDERP_ALL = (1<<BORDERP_TOP)|(1<<BORDERP_BOTTOM) };
+
+    VAR(IDF_PERSIST, playborder, 0, 0, BORDERP_ALL);
+    VAR(IDF_PERSIST|IDF_HEX, playbordertone, -CTONE_MAX, -CTONE_TEAM-1, 0xFFFFFF);
+    FVAR(IDF_PERSIST, playbordersize, 0, 0.05f, 1);
+    FVAR(IDF_PERSIST, playborderblend, 0, 0.5f, 1);
+
+    VAR(IDF_PERSIST, editborder, 0, 0, BORDERP_ALL);
+    VAR(IDF_PERSIST|IDF_HEX, editbordertone, -CTONE_MAX, 0, 0xFFFFFF);
+    FVAR(IDF_PERSIST, editbordersize, 0, 0.05f, 1);
+    FVAR(IDF_PERSIST, editborderblend, 0, 0.9f, 1);
+
+    VAR(IDF_PERSIST, specborder, 0, BORDERP_ALL, BORDERP_ALL);
+    VAR(IDF_PERSIST|IDF_HEX, specbordertone, -CTONE_MAX, 0, 0xFFFFFF);
+    FVAR(IDF_PERSIST, specbordersize, 0, 0.05f, 1);
+    FVAR(IDF_PERSIST, specborderblend, 0, 0.9f, 1);
+
+    VAR(IDF_PERSIST, waitborder, 0, BORDERP_ALL, BORDERP_ALL);
+    VAR(IDF_PERSIST|IDF_HEX, waitbordertone, -CTONE_MAX, 0, 0xFFFFFF);
+    FVAR(IDF_PERSIST, waitbordersize, 0, 0.05f, 1);
+    FVAR(IDF_PERSIST, waitborderblend, 0, 0.9f, 1);
+
+    VAR(IDF_PERSIST, backgroundborder, 0, 0, BORDERP_ALL);
+    VAR(IDF_PERSIST|IDF_HEX, backgroundbordertone, -CTONE_MAX, 0, 0xFFFFFF);
+    FVAR(IDF_PERSIST, backgroundbordersize, 0, 0.05f, 1);
+    FVAR(IDF_PERSIST, backgroundborderblend, 0, 0.5f, 1);
+
+    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, bordertoptex, "<grey>textures/hud/border", 3);
+    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, borderbottomtex, "<grey><rotate:2>textures/hud/border", 3);
+
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, underlaytex, "", 3);
     VAR(IDF_PERSIST, underlaydisplay, 0, 0, 2); // 0 = only firstperson and alive, 1 = only when alive, 2 = always
     VAR(IDF_PERSIST, underlayblend, 0, 1, 1);
@@ -158,6 +204,9 @@ namespace hud
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, indicatortex, "<grey>textures/hud/indicator", 3);
 
     VAR(IDF_PERSIST, showcrosshair, 0, 2, 2); // 0 = off, 1 = on, 2 = blend depending on current accuracy level
+    VAR(IDF_PERSIST, crosshairdistance, 0, 0, 1); // 0 = off, 1 = shows distance to crosshair target
+    VAR(IDF_PERSIST, crosshairdistancex, VAR_MIN, 160, VAR_MAX); // offset from the crosshair
+    VAR(IDF_PERSIST, crosshairdistancey, VAR_MIN, 80, VAR_MAX); // offset from the crosshair
     VAR(IDF_PERSIST, crosshairweapons, 0, 0, 3); // 0 = off, &1 = crosshair-specific weapons, &2 = also appy colour
     FVAR(IDF_PERSIST, crosshairsize, 0, 0.04f, 1000);
     VAR(IDF_PERSIST, crosshairhitspeed, 0, 500, VAR_MAX);
@@ -182,6 +231,8 @@ namespace hud
     TVAR(IDF_PERSIST, smghithairtex, "crosshairs/simple-03-hit", 3);
     TVAR(IDF_PERSIST, plasmacrosshairtex, "crosshairs/circle-03", 3);
     TVAR(IDF_PERSIST, plasmahithairtex, "crosshairs/circle-03-hit", 3);
+    TVAR(IDF_PERSIST, zappercrosshairtex, "crosshairs/circle-03", 3);
+    TVAR(IDF_PERSIST, zapperhithairtex, "crosshairs/circle-03-hit", 3);
     TVAR(IDF_PERSIST, flamercrosshairtex, "crosshairs/circle-04", 3);
     TVAR(IDF_PERSIST, flamerhithairtex, "crosshairs/circle-04-hit", 3);
     TVAR(IDF_PERSIST, riflecrosshairtex, "crosshairs/simple-01", 3);
@@ -210,21 +261,39 @@ namespace hud
     VAR(IDF_PERSIST, circlebartype, 0, 7, 7); // 0 = off, &1 = health, &2 = impulse, &4 = ammo
     FVAR(IDF_PERSIST, circlebarsize, 0, 0.04f, 1000);
     FVAR(IDF_PERSIST, circlebarblend, 0, 0.75f, 1);
+    VAR(IDF_PERSIST|IDF_HEX, circlebarhealthtone, -CTONE_MAX, 0x88FF88, 0xFFFFFF);
+    VAR(IDF_PERSIST|IDF_HEX, circlebarimpulsetone, -CTONE_MAX, 0xFF88FF, 0xFFFFFF);
+    VAR(IDF_PERSIST|IDF_HEX, circlebarammocolour, 0, 1, 1);
+    VAR(IDF_PERSIST|IDF_HEX, circlebarammotone, -CTONE_MAX-1, 0xFFAA66, 0xFFFFFF);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, circlebartex, "textures/hud/circlebar", 3);
 
     VAR(IDF_PERSIST, showinventory, 0, 1, 1);
-    VAR(IDF_PERSIST, inventoryammo, 0, 1, 3);
+    VAR(IDF_PERSIST, inventoryammo, 0, 3, 3);
     VAR(IDF_PERSIST, inventoryammobar, 0, 2, 2);
+    VAR(IDF_PERSIST, inventoryammostyle, 0, 1, 1);
     VAR(IDF_PERSIST, inventoryhidemelee, 0, 1, 1);
     VAR(IDF_PERSIST, inventorygame, 0, 2, 2);
+    VAR(IDF_PERSIST, inventorydate, 0, 0, 1);
+    SVAR(IDF_PERSIST, inventorydateformat, "%H:%M:%S");
+    VAR(IDF_PERSIST, inventorytime, 0, 1, 1);
+    VAR(IDF_PERSIST, inventorytimestyle, 0, 3, 4);
     VAR(IDF_PERSIST, inventoryscore, 0, 1, VAR_MAX);
+    VAR(IDF_PERSIST, inventoryscorespec, 0, 2, VAR_MAX);
     VAR(IDF_PERSIST, inventoryscorebg, 0, 0, 1);
+    VAR(IDF_PERSIST, inventoryscoreinfo, 0, 1, 1); // shows offset
+    VAR(IDF_PERSIST, inventoryscorebreak, 0, 1, 1); // two lines instead of one
+    VAR(IDF_PERSIST, inventoryscorepos, 0, 1, 1); // shows position
     VAR(IDF_PERSIST, inventoryweapids, 0, 2, 2);
     VAR(IDF_PERSIST, inventorycolour, 0, 2, 2);
     VAR(IDF_PERSIST, inventoryflash, 0, 0, 1);
-    FVAR(IDF_PERSIST, inventorysize, 0, 0.06f, 1000);
+    FVAR(IDF_PERSIST, inventoryleft, 0, 0.045f, 1000);
+    FVAR(IDF_PERSIST, inventoryright, 0, 0.05f, 1000);
     FVAR(IDF_PERSIST, inventoryskew, 1e-4f, 0.65f, 1000);
-    FVAR(IDF_PERSIST, inventoryscoresize, 0, 0.65f, 1);
+    FVAR(IDF_PERSIST, inventorydateskew, 1e-4f, 0.85f, 1000);
+    FVAR(IDF_PERSIST, inventorydateblend, 1e-4f, 1, 1);
+    FVAR(IDF_PERSIST, inventorytimeskew, 1e-4f, 1, 1000);
+    FVAR(IDF_PERSIST, inventorytimeblend, 1e-4f, 1, 1);
+    FVAR(IDF_PERSIST, inventoryscoresize, 0, 0.75f, 1);
     FVAR(IDF_PERSIST, inventoryscoreshrink, 0, 0.15f, 1);
     FVAR(IDF_PERSIST, inventoryscoreshrinkmax, 0, 0.45f, 1);
     FVAR(IDF_PERSIST, inventoryblend, 0, 1, 1);
@@ -254,9 +323,10 @@ namespace hud
     FVAR(IDF_PERSIST, inventoryhealthbgglow, 0, 0.05f, 1);
     FVAR(IDF_PERSIST, inventoryhealthbgblend, 0, 0.5f, 1);
 
-    VAR(IDF_PERSIST, inventoryimpulse, 0, 2, 2); // 0 = off, 1 = text, 2 = bar
+    VAR(IDF_PERSIST, inventoryimpulse, 0, 2, 3); // 0 = off, 1 = text, 2 = bar, 3 = both
     VAR(IDF_PERSIST, inventoryimpulseflash, 0, 1, 1);
     FVAR(IDF_PERSIST, inventoryimpulseblend, 0, 1, 1);
+    FVAR(IDF_PERSIST, inventoryimpulsethrob, 0, 0.035f, 1);
     FVAR(IDF_PERSIST, inventoryimpulsebartop, 0, 0.171875f, 1); // starts from this offset
     FVAR(IDF_PERSIST, inventoryimpulsebarbottom, 0, 0.0859375f, 1); // ends at this offset
     FVAR(IDF_PERSIST, inventoryimpulsebgglow, 0, 0.05f, 1);
@@ -264,11 +334,12 @@ namespace hud
 
     VAR(IDF_PERSIST, inventoryvelocity, 0, 0, 2);
     FVAR(IDF_PERSIST, inventoryvelocityblend, 0, 1, 1);
-    VAR(IDF_PERSIST, inventorycheckpoint, 0, 2, 2);
-    FVAR(IDF_PERSIST, inventorycheckpointblend, 0, 1, 1);
-    VAR(IDF_PERSIST, inventorystatus, 0, 3, 3); // 0 = off, 1 = text, 2 = icon, 3 = icon + tex
+    VAR(IDF_PERSIST, inventoryrace, 0, 2, 2);
+    VAR(IDF_PERSIST, inventoryracestyle, 0, 1, 4);
+    FVAR(IDF_PERSIST, inventoryraceblend, 0, 1, 1);
+    VAR(IDF_PERSIST, inventorystatus, 0, 2, 3); // 0 = off, 1 = text, 2 = icon, 3 = icon + tex
     FVAR(IDF_PERSIST, inventorystatusblend, 0, 1, 1);
-    FVAR(IDF_PERSIST, inventorystatusiconblend, 0, 0.5f, 1);
+    FVAR(IDF_PERSIST, inventorystatusiconblend, 0, 0.65f, 1);
 
     VAR(IDF_PERSIST, inventoryalert, 0, 1, 1);
     FVAR(IDF_PERSIST, inventoryalertblend, 0, 0.5f, 1);
@@ -285,6 +356,13 @@ namespace hud
     VAR(IDF_PERSIST, inventoryconopen, 0, 1, 1);
     FVAR(IDF_PERSIST, inventoryconopenblend, 0, 0.5f, 1);
     VAR(IDF_PERSIST, inventoryconopenflash, 0, 0, 1);
+    VAR(IDF_PERSIST, inventoryinput, 0, 0, 3); // bitwise, 1 = focus=player1, 2 = focus!=player1
+    FVAR(IDF_PERSIST, inventoryinputblend, 0, 0.75f, 1);
+    VAR(IDF_PERSIST, inventoryinputfilter, 0, AC_ALL, AC_ALL);
+    VAR(IDF_PERSIST, inventoryinputlinger, 0, AC_ALL, AC_ALL);
+    VAR(IDF_PERSIST, inventoryinputdelay, 0, 125, VAR_MAX);
+    VAR(IDF_PERSIST, inventoryinputactive, 0, 0x22FF22, 0xFFFFFF);
+    VAR(IDF_PERSIST, inventoryinputcolour, 0, 0x888888, 0xFFFFFF);
 
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, meleetex, "<grey>textures/weapons/melee", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, pistoltex, "<grey>textures/weapons/pistol", 3);
@@ -296,9 +374,10 @@ namespace hud
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, rockettex, "<grey>textures/weapons/rocket", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, flamertex, "<grey>textures/weapons/flamer", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, plasmatex, "<grey>textures/weapons/plasma", 3);
+    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, zappertex, "<grey>textures/weapons/zapper", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, rifletex, "<grey>textures/weapons/rifle", 3);
 
-    VAR(IDF_PERSIST, showclips, 0, 1, 1);
+    VAR(IDF_PERSIST, showclips, 0, 2, 2);
     VAR(IDF_PERSIST, clipanims, 0, 2, 2);
     FVAR(IDF_PERSIST, clipsize, 0, 0.035f, 1000);
     FVAR(IDF_PERSIST, clipoffset, 0, 0.04f, 1000);
@@ -314,6 +393,7 @@ namespace hud
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, smgcliptex, "<grey>textures/weapons/clips/smg", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, flamercliptex, "<grey>textures/weapons/clips/flamer", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, plasmacliptex, "<grey>textures/weapons/clips/plasma", 3);
+    TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, zappercliptex, "<grey>textures/weapons/clips/zapper", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, riflecliptex, "<grey>textures/weapons/clips/rifle", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, grenadecliptex, "<grey>textures/weapons/clips/grenade", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, minecliptex, "<grey>textures/weapons/clips/mine", 3);
@@ -325,17 +405,19 @@ namespace hud
     FVAR(IDF_PERSIST, smgclipoffset, 0, 0.35f, 0.5f);
     FVAR(IDF_PERSIST, flamerclipoffset, 0, 0.5f, 0.5f);
     FVAR(IDF_PERSIST, plasmaclipoffset, 0, 0.1f, 0.5f);
+    FVAR(IDF_PERSIST, zapperclipoffset, 0, 0.3f, 0.5f);
     FVAR(IDF_PERSIST, rifleclipoffset, 0, 0.25f, 0.5f);
     FVAR(IDF_PERSIST, grenadeclipoffset, 0, 0, 0.5f);
     FVAR(IDF_PERSIST, mineclipoffset, 0, 0, 0.5f);
     FVAR(IDF_PERSIST, rocketclipoffset, 0, 0, 0.5f);
     FVAR(IDF_PERSIST, meleeclipskew, 0, 0.75f, 10);
-    FVAR(IDF_PERSIST, pistolclipskew, 0, 0.75f, 10);
-    FVAR(IDF_PERSIST, swordclipskew, 0, 2, 10);
+    FVAR(IDF_PERSIST, pistolclipskew, 0, 0.65f, 10);
+    FVAR(IDF_PERSIST, swordclipskew, 0, 1, 10);
     FVAR(IDF_PERSIST, shotgunclipskew, 0, 0.75f, 10);
-    FVAR(IDF_PERSIST, smgclipskew, 0, 0.65f, 10);
-    FVAR(IDF_PERSIST, flamerclipskew, 0, 0.5f, 10);
-    FVAR(IDF_PERSIST, plasmaclipskew, 0, 0.6f, 10);
+    FVAR(IDF_PERSIST, smgclipskew, 0, 0.55f, 10);
+    FVAR(IDF_PERSIST, flamerclipskew, 0, 0.45f, 10);
+    FVAR(IDF_PERSIST, plasmaclipskew, 0, 0.55f, 10);
+    FVAR(IDF_PERSIST, zapperclipskew, 0, 0.55f, 10);
     FVAR(IDF_PERSIST, rifleclipskew, 0, 1, 10);
     FVAR(IDF_PERSIST, grenadeclipskew, 0, 1.5f, 10);
     FVAR(IDF_PERSIST, mineclipskew, 0, 2.f, 10);
@@ -347,6 +429,7 @@ namespace hud
     VAR(IDF_PERSIST, smgcliprotate, 0, 12, 15);
     VAR(IDF_PERSIST, flamercliprotate, 0, 12, 15);
     VAR(IDF_PERSIST, plasmacliprotate, 0, 12, 15);
+    VAR(IDF_PERSIST, zappercliprotate, 0, 12, 15);
     VAR(IDF_PERSIST, riflecliprotate, 0, 12, 15);
     VAR(IDF_PERSIST, grenadecliprotate, 0, 11, 15);
     VAR(IDF_PERSIST, minecliprotate, 0, 11, 15);
@@ -362,11 +445,11 @@ namespace hud
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, hinttex, "<grey>textures/hint", 3);
     FVAR(IDF_PERSIST, radarblend, 0, 1, 1);
     FVAR(IDF_PERSIST, radarplayerblend, 0, 1, 1);
-    FVAR(IDF_PERSIST, radarplayerhintblend, 0, 1, 1);
-    FVAR(IDF_PERSIST, radarplayersize, 0, 0.3f, 1000);
-    FVAR(IDF_PERSIST, radarplayerhintsize, 0, 0.7f, 1);
+    FVAR(IDF_PERSIST, radarplayerhintblend, 0, 0.65f, 1);
+    FVAR(IDF_PERSIST, radarplayersize, 0, 0.35f, 1000);
+    FVAR(IDF_PERSIST, radarplayerhintsize, 0, 0.8f, 1);
     FVAR(IDF_PERSIST, radarblipblend, 0, 1, 1);
-    FVAR(IDF_PERSIST, radarblipsize, 0, 0.3f, 1000);
+    FVAR(IDF_PERSIST, radarblipsize, 0, 0.35f, 1000);
     FVAR(IDF_PERSIST, radarbliprotate, 0, 1, 1);
     FVAR(IDF_PERSIST, radaraffinityblend, 0, 1, 1);
     FVAR(IDF_PERSIST, radaraffinitysize, 0, 1, 1000);
@@ -417,10 +500,6 @@ namespace hud
     FVAR(IDF_PERSIST, motionblurmax, 0, 0.75f, 1); // maximum
     FVAR(IDF_PERSIST, motionbluramt, 0, 0.5f, 1); // used for override
 
-    TVAR(IDF_PERSIST|IDF_PRELOAD, bgtex, "textures/background", 3);
-    TVAR(IDF_PERSIST|IDF_PRELOAD, logotex, "textures/logo", 3);
-    TVAR(IDF_PERSIST|IDF_PRELOAD, badgetex, "textures/cube2badge", 3);
-
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, spree1tex, "textures/rewards/carnage", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, spree2tex, "textures/rewards/slaughter", 3);
     TVAR(IDF_PERSIST|IDF_GAMEPRELOAD, spree3tex, "textures/rewards/massacre", 3);
@@ -442,7 +521,11 @@ namespace hud
     TVAR(IDF_PERSIST, privoperatortex, "<grey>textures/privs/operator", 3);
     TVAR(IDF_PERSIST, privadministratortex, "<grey>textures/privs/administrator", 3);
     TVAR(IDF_PERSIST, privdevelopertex, "<grey>textures/privs/developer", 3);
-    TVAR(IDF_PERSIST, privcreatortex, "<grey>textures/privs/creator", 3);
+    TVAR(IDF_PERSIST, privfoundertex, "<grey>textures/privs/founder", 3);
+    TVAR(IDF_PERSIST, privlocalsupportertex, "<grey>textures/privs/localsupporter", 3);
+    TVAR(IDF_PERSIST, privlocalmoderatortex, "<grey>textures/privs/localmoderator", 3);
+    TVAR(IDF_PERSIST, privlocaloperatortex, "<grey>textures/privs/localoperator", 3);
+    TVAR(IDF_PERSIST, privlocaladministratortex, "<grey>textures/privs/localadministrator", 3);
 
     VAR(IDF_PERSIST|IDF_HEX, privnonecolour, 0, 0x888888, 0xFFFFFF);
     VAR(IDF_PERSIST|IDF_HEX, privplayercolour, 0, 0xAAAAAA, 0xFFFFFF);
@@ -451,15 +534,10 @@ namespace hud
     VAR(IDF_PERSIST|IDF_HEX, privoperatorcolour, 0, 0x44FF44, 0xFFFFFF);
     VAR(IDF_PERSIST|IDF_HEX, privadministratorcolour, 0, 0xFF4444, 0xFFFFFF);
     VAR(IDF_PERSIST|IDF_HEX, privdevelopercolour, 0, 0x44FF88, 0xFFFFFF);
-    VAR(IDF_PERSIST|IDF_HEX, privcreatorcolour, 0, 0x8844FF, 0xFFFFFF);
+    VAR(IDF_PERSIST|IDF_HEX, privfoundercolour, 0, 0x8844FF, 0xFFFFFF);
 
     TVAR(IDF_PERSIST, modeeditingtex, "<grey>textures/modes/editing", 3);
-#ifdef CAMPAIGN
-    TVAR(IDF_PERSIST, modecampaigntex, "<grey>textures/modes/campaign", 3);
-#endif
     TVAR(IDF_PERSIST, modedeathmatchtex, "<grey>textures/modes/deathmatch", 3);
-    TVAR(IDF_PERSIST, modetimetrialtex, "<grey>textures/modes/timetrial", 3);
-    TVAR(IDF_PERSIST, modegauntlettex, "<grey>textures/modes/gauntlet", 3);
 
     TVAR(IDF_PERSIST, modecapturetex, "<grey>textures/modes/capture", 3);
     TVAR(IDF_PERSIST, modecapturequicktex, "<grey>textures/modes/capturequick", 3);
@@ -472,7 +550,13 @@ namespace hud
 
     TVAR(IDF_PERSIST, modebombertex, "<grey>textures/modes/bomber", 3);
     TVAR(IDF_PERSIST, modebomberholdtex, "<grey>textures/modes/bomberhold", 3);
-    TVAR(IDF_PERSIST, modebombertouchtex, "<grey>textures/modes/bombertouch", 3);
+    TVAR(IDF_PERSIST, modebomberbaskettex, "<grey>textures/modes/bomberbasket", 3);
+    TVAR(IDF_PERSIST, modebomberattacktex, "<grey>textures/modes/bomberattack", 3);
+
+    TVAR(IDF_PERSIST, moderacetex, "<grey>textures/modes/race", 3);
+    TVAR(IDF_PERSIST, moderacetimedtex, "<grey>textures/modes/racetimed", 3);
+    TVAR(IDF_PERSIST, moderaceendurancetex, "<grey>textures/modes/raceendurance", 3);
+    TVAR(IDF_PERSIST, moderacegauntlettex, "<grey>textures/modes/racegauntlet", 3);
 
     TVAR(IDF_PERSIST, modemultitex, "<grey>textures/modes/multi", 3);
     TVAR(IDF_PERSIST, modeffatex, "<grey>textures/modes/ffa", 3);
@@ -484,18 +568,15 @@ namespace hud
     TVAR(IDF_PERSIST, modesurvivortex, "<grey>textures/modes/survivor", 3);
     TVAR(IDF_PERSIST, modeclassictex, "<grey>textures/modes/classic", 3);
     TVAR(IDF_PERSIST, modeonslaughttex, "<grey>textures/modes/onslaught", 3);
-    TVAR(IDF_PERSIST, modejetpacktex, "<grey>textures/modes/jetpack", 3);
+    TVAR(IDF_PERSIST, modefreestyletex, "<grey>textures/modes/freestyle", 3);
     TVAR(IDF_PERSIST, modevampiretex, "<grey>textures/modes/vampire", 3);
-    TVAR(IDF_PERSIST, modeexperttex, "<grey>textures/modes/expert", 3);
     TVAR(IDF_PERSIST, moderesizetex, "<grey>textures/modes/resize", 3);
+    TVAR(IDF_PERSIST, modehardtex, "<grey>textures/modes/hard", 3);
+    TVAR(IDF_PERSIST, modebasictex, "<grey>textures/modes/basic", 3);
 
-#ifdef CAMPAIGN
     #define ADDMODEICON(g,m) \
     { \
         if(m_edit(g)) ADDMODE(modeeditingtex) \
-        else if(m_trial(g)) ADDMODE(modetimetrialtex) \
-        else if(m_gauntlet(g)) ADDMODE(modegauntlettex) \
-        else if(m_campaign(g)) ADDMODE(modecampaigntex) \
         else if(m_capture(g)) \
         { \
             if(m_gsp1(g, m)) ADDMODE(modecapturequicktex) \
@@ -505,46 +586,43 @@ namespace hud
         } \
         else if(m_defend(g)) \
         { \
-            if(m_gsp1(g, m)) ADDMODE(modedefendquicktex) \
-            else if(m_gsp2(g, m)) ADDMODE(modedefendkingtex) \
+            if(m_gsp2(g, m)) \
+            { \
+                ADDMODE(modedefendkingtex) \
+                if(m_gsp1(g, m)) ADDMODE(modedefendquicktex) \
+            } \
+            else if(m_gsp1(g, m)) ADDMODE(modedefendquicktex) \
             else ADDMODE(modedefendtex) \
         } \
         else if(m_bomber(g)) \
         { \
             if(m_gsp1(g, m)) ADDMODE(modebomberholdtex) \
-            else if(m_gsp2(g, m)) ADDMODE(modebombertouchtex) \
+            else if(m_gsp3(g, m)) \
+            { \
+                ADDMODE(modebomberattacktex) \
+                if(m_gsp2(g, m)) ADDMODE(modebomberbaskettex) \
+            } \
+            else if(m_gsp2(g, m)) ADDMODE(modebomberbaskettex) \
             else ADDMODE(modebombertex) \
         } \
-        else ADDMODE(modedeathmatchtex) \
-    }
-#else
-    #define ADDMODEICON(g,m) \
-    { \
-        if(m_edit(g)) ADDMODE(modeeditingtex) \
-        else if(m_trial(g)) ADDMODE(modetimetrialtex) \
-        else if(m_gauntlet(g)) ADDMODE(modegauntlettex) \
-        else if(m_capture(g)) \
-        { \
-            if(m_gsp1(g, m)) ADDMODE(modecapturequicktex) \
-            else if(m_gsp2(g, m)) ADDMODE(modecapturedefendtex) \
-            else if(m_gsp3(g, m)) ADDMODE(modecaptureprotecttex) \
-            else ADDMODE(modecapturetex) \
-        } \
-        else if(m_defend(g)) \
+        else if(m_race(g)) \
         { \
-            if(m_gsp1(g, m)) ADDMODE(modedefendquicktex) \
-            else if(m_gsp2(g, m)) ADDMODE(modedefendkingtex) \
-            else ADDMODE(modedefendtex) \
-        } \
-        else if(m_bomber(g)) \
-        { \
-            if(m_gsp1(g, m)) ADDMODE(modebomberholdtex) \
-            else if(m_gsp2(g, m)) ADDMODE(modebombertouchtex) \
-            else ADDMODE(modebombertex) \
+            if(m_gsp3(g, m)) \
+            { \
+                ADDMODE(moderacegauntlettex) \
+                if(m_gsp1(g, m)) ADDMODE(moderacetimedtex) \
+                if(m_gsp2(g, m)) ADDMODE(moderaceendurancetex) \
+            } \
+            else if(m_gsp1(g, m)) \
+            { \
+                ADDMODE(moderacetimedtex) \
+                if(m_gsp2(g, m)) ADDMODE(moderaceendurancetex) \
+            } \
+            else if(m_gsp2(g, m)) ADDMODE(moderaceendurancetex) \
+            else ADDMODE(moderacetex) \
         } \
         else ADDMODE(modedeathmatchtex) \
     }
-#endif
 
     void modetexs(int g, int m, bool before, bool implied, vector<char> &list)
     {
@@ -561,10 +639,11 @@ namespace hud
         if(m_survivor(g, m) && (implied || !(gametype[g].implied&(1<<G_M_SURVIVOR)))) ADDMODE(modesurvivortex)
         if(m_classic(g, m) && (implied || !(gametype[g].implied&(1<<G_M_CLASSIC)))) ADDMODE(modeclassictex)
         if(m_onslaught(g, m) && (implied || !(gametype[g].implied&(1<<G_M_ONSLAUGHT)))) ADDMODE(modeonslaughttex)
-        if(m_jetpack(g, m) && (implied || !(gametype[g].implied&(1<<G_M_JETPACK)))) ADDMODE(modejetpacktex)
+        if(m_freestyle(g, m) && (implied || !(gametype[g].implied&(1<<G_M_FREESTYLE)))) ADDMODE(modefreestyletex)
         if(m_vampire(g, m) && (implied || !(gametype[g].implied&(1<<G_M_VAMPIRE)))) ADDMODE(modevampiretex)
-        if(m_expert(g, m) && (implied || !(gametype[g].implied&(1<<G_M_EXPERT)))) ADDMODE(modeexperttex)
         if(m_resize(g, m) && (implied || !(gametype[g].implied&(1<<G_M_RESIZE)))) ADDMODE(moderesizetex)
+        if(m_hard(g, m) && (implied || !(gametype[g].implied&(1<<G_M_HARD)))) ADDMODE(modehardtex)
+        if(m_basic(g, m) && (implied || !(gametype[g].implied&(1<<G_M_BASIC)))) ADDMODE(modebasictex)
         if(!before) ADDMODEICON(g, m)
         #undef ADDMODE
     }
@@ -593,11 +672,23 @@ namespace hud
 
     bool hasinput(bool pass, bool focus)
     {
-        if(pass && (game::intermission || game::focus->state != CS_ALIVE)) pass = false;
         if(focus && (commandmillis > 0 || curcompass)) return true;
         return UI::active(pass);
     }
 
+    bool hastkwarn(gameent *d)
+    {
+        if(!m_fight(game::gamemode)) return false;
+        return teamkillwarn && m_team(game::gamemode, game::mutators) && numteamkills() >= teamkillwarn;
+    }
+
+    bool hasteaminfo(gameent *d)
+    {
+        if(!m_fight(game::gamemode) || game::focus->state != CS_ALIVE) return false;
+        if(!lastteam) lastteam = totalmillis;
+        return teamnotices >= 1 && totalmillis-lastteam <= teamnoticedelay;
+    }
+
     bool keypress(int code, bool isdown, int cooked)
     {
         if(curcompass) return keycmenu(code, isdown, cooked);
@@ -624,7 +715,6 @@ namespace hud
                     amt += 0.25f+(float((lastmillis-game::focus->lastres[WR_SHOCK])%shockdelay)/float(shockdelay))*0.35f;
                 if(game::focus->turnside || game::focus->impulse[IM_JUMP])
                     amt += game::focus->turnside ? 0.125f : 0.25f;
-                if(physics::jetpack(game::focus)) amt += 0.125f;
                 break;
             }
             case 2: amt += motionbluramt; break;
@@ -633,22 +723,22 @@ namespace hud
         return clamp(amt, motionblurmin, motionblurmax)*scale;
     }
 
-    void damage(int n, const vec &loc, gameent *actor, int weap, int flags)
+    void damage(int n, const vec &loc, gameent *v, int weap, int flags)
     {
         damageresidue = clamp(damageresidue+(n*(flags&HIT_BLEED ? 3 : 1)), 0, 200);
-        vec colour = wr_burns(weap, flags) ? vec(1.f, 0.35f, 0.0625f) : (game::bloodscale <= 0 ? vec(1, 0.25f, 1) : vec(1.f, 0, 0)),
+        vec colour = wr_burns(weap, flags) ? vec(1.f, 0.35f, 0.0625f) : (game::nogore || game::bloodscale <= 0 ? vec(1, 0.25f, 1) : vec(1.f, 0, 0)),
             dir = vec(loc).sub(camera1->o).normalize();
         loopv(damagelocs)
         {
             damageloc &d = damagelocs[i];
-            if(actor->clientnum != d.attacker) continue;
+            if(v->clientnum != d.attacker) continue;
             if(lastmillis-d.outtime > radardamagemerge) continue;
             if(d.colour != colour) continue;
             d.damage += n;
             d.dir = dir;
             return; // accumulate
         }
-        damagelocs.add(damageloc(actor->clientnum, lastmillis, n, dir, colour));
+        damagelocs.add(damageloc(v->clientnum, lastmillis, n, dir, colour));
     }
 
     void drawquad(float x, float y, float w, float h, float tx1, float ty1, float tx2, float ty2, bool flipx, bool flipy)
@@ -739,7 +829,7 @@ namespace hud
                 {
                     const char *crosshairtexs[W_MAX] = {
                         meleecrosshairtex, pistolcrosshairtex, swordcrosshairtex, shotguncrosshairtex, smgcrosshairtex,
-                        flamercrosshairtex, plasmacrosshairtex, riflecrosshairtex, grenadecrosshairtex, minecrosshairtex,
+                        flamercrosshairtex, plasmacrosshairtex, zappercrosshairtex, riflecrosshairtex, grenadecrosshairtex, minecrosshairtex,
                         rocketcrosshairtex, // end of regular weapons
                     };
                     if(*crosshairtexs[weap]) return crosshairtexs[weap];
@@ -754,7 +844,7 @@ namespace hud
                 {
                     const char *hithairtexs[W_MAX] = {
                         meleehithairtex, pistolhithairtex, swordhithairtex, shotgunhithairtex, smghithairtex,
-                        flamerhithairtex, plasmahithairtex, riflehithairtex, grenadehithairtex, minehithairtex,
+                        flamerhithairtex, plasmahithairtex, zapperhithairtex, riflehithairtex, grenadehithairtex, minehithairtex,
                         rockethithairtex, // end of regular weapons
                     };
                     if(*hithairtexs[weap]) return hithairtexs[weap];
@@ -773,32 +863,30 @@ namespace hud
         float r = 1, g = 1, b = 1, amt = 0;
         switch(game::focus->weapstate[weap])
         {
-            case W_S_POWER: if(game::focus->weapwait[weap])
+            case W_S_POWER: case W_S_ZOOM: if(game::focus->weapwait[weap])
             {
+                if(millis >= game::focus->weapwait[weap]) return;
                 amt = clamp(float(millis)/float(game::focus->weapwait[weap]), 0.f, 1.f);
                 colourskew(r, g, b, 1.f-amt);
                 break;
             }
-            case W_S_RELOAD: if(showindicator >= (W(weap, add) < W(weap, max) ? 3 : 2) && millis <= game::focus->weapwait[weap])
+            case W_S_RELOAD: if(showindicator >= (W(weap, ammoadd) < W(weap, ammomax) ? 3 : 2) && millis <= game::focus->weapwait[weap])
             {
                 amt = 1.f-clamp(float(millis)/float(game::focus->weapwait[weap]), 0.f, 1.f);
                 colourskew(r, g, b, 1.f-amt);
                 break;
             }
-            default: amt = 0; break;
-        }
-        if(amt > 0)
-        {
-            Texture *t = textureload(indicatortex, 3);
-            if(t->type&Texture::ALPHA) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-            else glBlendFunc(GL_ONE, GL_ONE);
-            glBindTexture(GL_TEXTURE_2D, t->id);
-            float val = amt < 0.25f ? amt : (amt > 0.75f ? 1.f-amt : 0.25f);
-            glColor4f(val*4.f, val*4.f, val*4.f, indicatorblend*hudblend*val);
-            drawsized(x-s, y-s, s*2);
-            glColor4f(r, g, b, indicatorblend*hudblend);
-            drawslice(0, clamp(amt, 0.f, 1.f), x, y, s);
+            default: return;
         }
+        Texture *t = textureload(indicatortex, 3);
+        if(t->type&Texture::ALPHA) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+        else glBlendFunc(GL_ONE, GL_ONE);
+        glBindTexture(GL_TEXTURE_2D, t->id);
+        float val = amt < 0.25f ? amt : (amt > 0.75f ? 1.f-amt : 0.25f);
+        glColor4f(val*4.f, val*4.f, val*4.f, indicatorblend*hudblend*val);
+        drawsized(x-s, y-s, s*2);
+        glColor4f(r, g, b, indicatorblend*hudblend);
+        drawslice(0, clamp(amt, 0.f, 1.f), x, y, s);
     }
 
     void vecfromyaw(float yaw, int move, int strafe, vec2 &m)
@@ -832,7 +920,7 @@ namespace hud
             while(rot < 0.0f) rot += 360.0f;
             while(rot >= 360.0f) rot -= 360.0f;
             vec2 loc(x+offset*sinf(RAD*angle), y+offset*-cosf(RAD*angle));
-            glColor4f(colour.x, colour.y, colour.z, blend);
+            glColor4f(colour.r, colour.g, colour.b, blend);
             glBindTexture(GL_TEXTURE_2D, t->id);
             glBegin(GL_TRIANGLE_STRIP);
             loopk(4)
@@ -852,33 +940,43 @@ namespace hud
         }
     }
 
+    void drawsimpleclipitem(float x, float y, float start, float length, float size, float blend, const vec &colour)
+    {
+        Texture *t = textureload(progringtex, 3);
+        if(t->type&Texture::ALPHA) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+        else glBlendFunc(GL_ONE, GL_ONE);
+        glBindTexture(GL_TEXTURE_2D, t->id);
+        glColor4f(colour.r, colour.g, colour.b, blend);
+        drawslice(start, length, x, y, size);
+    }
+
     void drawclip(int weap, int x, int y, float s)
     {
-        if(!isweap(weap) || (!W2(weap, sub, false) && !W2(weap, sub, true)) || W(weap, max) < cliplength) return;
+        if(!isweap(weap) || (!W2(weap, ammosub, false) && !W2(weap, ammosub, true)) || W(weap, ammomax) < cliplength) return;
         const char *cliptexs[W_MAX] = {
             meleecliptex, pistolcliptex, swordcliptex, shotguncliptex, smgcliptex,
-            flamercliptex, plasmacliptex, riflecliptex, grenadecliptex, minecliptex, rocketcliptex
+            flamercliptex, plasmacliptex, zappercliptex, riflecliptex, grenadecliptex, minecliptex, rocketcliptex
         };
         const float clipoffs[W_MAX] = {
             meleeclipoffset, pistolclipoffset, swordclipoffset, shotgunclipoffset, smgclipoffset,
-            flamerclipoffset, plasmaclipoffset, rifleclipoffset, grenadeclipoffset, mineclipoffset, rocketclipoffset
+            flamerclipoffset, plasmaclipoffset, zapperclipoffset, rifleclipoffset, grenadeclipoffset, mineclipoffset, rocketclipoffset
         };
         const float clipskew[W_MAX] = {
             meleeclipskew, pistolclipskew, swordclipskew, shotgunclipskew, smgclipskew,
-            flamerclipskew, plasmaclipskew, rifleclipskew, grenadeclipskew, mineclipskew, rocketclipskew
+            flamerclipskew, plasmaclipskew, zapperclipskew, rifleclipskew, grenadeclipskew, mineclipskew, rocketclipskew
         };
         const int cliprots[W_MAX] = {
             meleecliprotate, pistolcliprotate, swordcliprotate, shotguncliprotate, smgcliprotate,
-            flamercliprotate, plasmacliprotate, riflecliprotate, grenadecliprotate, minecliprotate, rocketcliprotate
+            flamercliprotate, plasmacliprotate, zappercliprotate, riflecliprotate, grenadecliprotate, minecliprotate, rocketcliprotate
         };
-        int ammo = game::focus->ammo[weap], maxammo = W(weap, max), interval = lastmillis-game::focus->weaplast[weap];
-        float fade = clipblend*hudblend, size = s*clipsize*clipskew[weap], offset = s*clipoffset, amt = 0, spin = 0,
-              slice = 360/float(maxammo), angle = (maxammo > (cliprots[weap]&4 ? 4 : 3) || maxammo%2 ? 360.f : 360.f-slice*0.5f)-((maxammo-ammo)*slice),
+        int ammo = game::focus->ammo[weap], maxammo = W(weap, ammomax), interval = lastmillis-game::focus->weaplast[weap];
+        bool simple = showclips == 1 || maxammo > 360;
+        float fade = clipblend*hudblend, size = s*clipsize*(simple ? 1.f : clipskew[weap]), offset = s*clipoffset, amt = 0, spin = 0,
+              slice = 360/float(maxammo), angle = (simple || maxammo > (cliprots[weap]&4 ? 4 : 3) || maxammo%2 ? 360.f : 360.f-slice*0.5f)-((maxammo-ammo)*slice),
               area = 1-clamp(clipoffs[weap]*2, 1e-3f, 1.f), need = s*clipsize*clipskew[weap]*area*maxammo, have = 2*M_PI*s*clipoffset,
               scale = clamp(have/need, clipminscale, clipmaxscale);
-        vec c(clipcolour, clipcolour, clipcolour);
-        if(clipcolour > 0) c.mul(vec::hexcolor(W(weap, colour)));
-        else if(clipstone) skewcolour(c.r, c.g, c.b, clipstone);
+        vec c(1, 1, 1);
+        if(clipstone || clipcolour) skewcolour(c.r, c.g, c.b, clipcolour ? W(weap, colour) : clipstone);
         if(interval <= game::focus->weapwait[weap]) switch(game::focus->weapstate[weap])
         {
             case W_S_PRIMARY: case W_S_SECONDARY:
@@ -892,8 +990,12 @@ namespace hud
                     if(clipanims >= 2) spin = 360*amt;
                 }
                 int shot = game::focus->weapshot[weap] ? game::focus->weapshot[weap] : 1;
-                float rewind = angle;
-                loopi(shot) drawclipitem(cliptexs[weap], x, y, offset, size*scale, fade, rewind += slice, spin, cliprots[weap], c);
+                if(simple) drawsimpleclipitem(x, y, angle/360.f, slice*shot/360.f, size, fade, c);
+                else
+                {
+                    float rewind = angle;
+                    loopi(shot) drawclipitem(cliptexs[weap], x, y, offset, size*scale, fade, rewind += slice, spin, cliprots[weap], c);
+                }
                 fade = clipblend*hudblend;
                 size = s*clipsize*clipskew[weap];
                 offset = s*clipoffset;
@@ -921,7 +1023,13 @@ namespace hud
                         fade = spin = 0;
                         if(clipanims) size = offset = 0;
                     }
-                    loopi(game::focus->weapload[weap])
+                    if(simple)
+                    {
+                        float amt = slice*game::focus->weapload[weap];
+                        angle -= amt;
+                        drawsimpleclipitem(x, y, angle/360.f, amt/360.f, size, fade, c);
+                    }
+                    else loopi(game::focus->weapload[weap])
                     {
                         drawclipitem(cliptexs[weap], x, y, offset, size*scale, fade, angle, spin, cliprots[weap], c);
                         angle -= slice;
@@ -950,7 +1058,8 @@ namespace hud
             }
             default: break;
         }
-        loopi(ammo)
+        if(simple) drawsimpleclipitem(x, y, 0, slice*ammo/360.f, size, fade, c);
+        else loopi(ammo)
         {
             drawclipitem(cliptexs[weap], x, y, offset, size*scale, fade, angle, spin, cliprots[weap], c);
             angle -= slice;
@@ -977,24 +1086,25 @@ namespace hud
             {
                 case 0:
                     val = min(1.f, game::focus->health/float(m_health(game::gamemode, game::mutators, game::focus->model)));
-                    c = vec(0.25f, 0.75f, 0.125f);
+                    if(circlebarhealthtone) skewcolour(c.r, c.g, c.b, circlebarhealthtone);
                     break;
                 case 1:
+                    if(!m_impulsemeter(game::gamemode, game::mutators)) continue;
                     val = 1-clamp(float(game::focus->impulse[IM_METER])/float(impulsemeter), 0.f, 1.f);
-                    c = vec(0.75f, 0.35f, 0.95f);
+                    if(circlebarimpulsetone) skewcolour(c.r, c.g, c.b, circlebarimpulsetone);
                     break;
                 case 2:
                 {
                     if(!isweap(game::focus->weapselect)) continue;
                     int weap = game::focus->weapselect, interval = lastmillis-game::focus->weaplast[weap];
-                    val = game::focus->ammo[weap]/float(W(weap, max));
-                    c = vec(0.8f, 0.55f, 0.15f);
+                    val = game::focus->ammo[weap]/float(W(weap, ammomax));
+                    if(circlebarammotone || circlebarammocolour) skewcolour(c.r, c.g, c.b, circlebarammocolour ? W(weap, colour) : circlebarammotone);
                     if(interval <= game::focus->weapwait[weap]) switch(game::focus->weapstate[weap])
                     {
                         case W_S_RELOAD: case W_S_USE:
                             if(game::focus->weapload[weap] > 0)
                             {
-                                val -= game::focus->weapload[weap]/float(W(weap, max));
+                                val -= game::focus->weapload[weap]/float(W(weap, ammomax));
                                 break;
                             }
                             else if(game::focus->weapstate[weap] == W_S_RELOAD) break;
@@ -1031,7 +1141,7 @@ namespace hud
                         {
                             float amt = 1.f-clamp(float(interval)/float(game::focus->weapwait[weap]), 0.f, 1.f);
                             fade *= amt;
-                            val = (game::focus->weapshot[weap] ? game::focus->weapshot[weap] : 1)/float(W(weap, max))*amt;
+                            val = (game::focus->weapshot[weap] ? game::focus->weapshot[weap] : 1)/float(W(weap, ammomax))*amt;
                             break;
                         }
                         case W_S_RELOAD: case W_S_USE:
@@ -1043,7 +1153,7 @@ namespace hud
                                 {
                                     float amt = clamp(float(interval-check)/float(check), 0.f, 1.f);
                                     fade *= amt;
-                                    val = game::focus->weapload[weap]/float(W(weap, max))*amt;
+                                    val = game::focus->weapload[weap]/float(W(weap, ammomax))*amt;
                                 }
                             }
                             break;
@@ -1105,7 +1215,7 @@ namespace hud
         if(game::focus->state == CS_ALIVE && index >= POINTER_HAIR)
         {
             if(crosshairweapons&2) c = vec::hexcolor(W(game::focus->weapselect, colour));
-            if(index == POINTER_ZOOM && game::inzoom() && W(game::focus->weapselect, zooms))
+            if(index == POINTER_ZOOM && game::inzoom())
             {
                 int frame = lastmillis-game::lastzoom, off = int(zoomcrosshairsize*hudsize)-cs;
                 float amt = frame <= zoomtime ? clamp(float(frame)/float(zoomtime), 0.f, 1.f) : 1.f;
@@ -1141,7 +1251,13 @@ namespace hud
                     if(showindicator) drawindicator(game::focus->weapselect, cx, cy, int(indicatorsize*hudsize), physics::secondaryweap(game::focus));
                 }
                 if(crosshairhitspeed && totalmillis-game::focus->lasthit <= crosshairhitspeed)
-                    drawpointertex(getpointer(POINTER_HIT, game::focus->weapselect), cx-cs/2, cy-cs/2, cs, c.r, c.g, c.b, crosshairblend*hudblend);
+                {
+                    vec c2(1, 1, 1);
+                    if(hitcrosshairtone) skewcolour(c2.r, c2.g, c2.b, hitcrosshairtone);
+                    else c2 = c;
+                    drawpointertex(getpointer(POINTER_HIT, game::focus->weapselect), cx-cs/2, cy-cs/2, cs, c2.r, c2.g, c2.b, crosshairblend*hudblend);
+                }
+                if(crosshairdistance && (game::focus->state == CS_ALIVE || game::focus->state == CS_EDITING)) draw_textx("\fa%.1f\fwm", cx+crosshairdistancex, cy+crosshairdistancey, 255, 255, 255, int(hudblend*255), TEXT_RIGHT_JUSTIFY, -1, -1, game::focus->o.dist(worldpos)/8.f);
             }
         }
         else drawpointertex(getpointer(index, game::focus->weapselect), cx, cy, cs, c.r, c.g, c.b, fade*hudblend);
@@ -1151,17 +1267,16 @@ namespace hud
     {
         int index = POINTER_NONE;
         if(hasinput()) index = !hasinput(true) || commandmillis > 0 ? POINTER_NONE : POINTER_GUI;
-        else if(!showhud || !showcrosshair || game::focus->state == CS_DEAD || game::intermission || client::waiting() || (game::thirdpersonview(true) && game::focus != game::player1))
+        else if(!showhud || !showcrosshair || game::focus->state == CS_DEAD || !gs_playing(game::gamestate) || client::waiting() || (game::thirdpersonview(true) && game::focus != game::player1))
             index = POINTER_NONE;
         else if(game::focus->state == CS_EDITING) index = POINTER_EDIT;
         else if(game::focus->state >= CS_SPECTATOR) index = POINTER_SPEC;
-        else if(game::inzoom() && W(game::focus->weapselect, zooms)) index = POINTER_ZOOM;
+        else if(game::inzoom()) index = POINTER_ZOOM;
         else if(m_team(game::gamemode, game::mutators))
         {
             vec pos = game::focus->headpos();
             gameent *d = game::intersectclosest(pos, worldpos, game::focus);
-            if(d && d->type == ENT_PLAYER && d->team == game::focus->team)
-                index = POINTER_TEAM;
+            if(d && d->actortype < A_ENEMY && d->team == game::focus->team) index = POINTER_TEAM;
             else index = POINTER_HAIR;
         }
         else index = POINTER_HAIR;
@@ -1198,59 +1313,76 @@ namespace hud
         return false;
     }
 
+    const char *specviewname()
+    {
+        if(showname()) return game::colourname(game::focus);
+        if(game::tvmode())
+        {
+            if(game::spectvfollow >= 0)
+            {
+                gameent *d = game::getclient(game::spectvfollow);
+                if(d) return game::colourname(d);
+            }
+            return "SpecTV";
+        }
+        return "Spectating";
+    }
+
     void drawnotices()
     {
         glPushMatrix();
         glScalef(noticescale, noticescale, 1);
-        pushfont("default");
         int ty = int(((hudheight/2)+(hudheight/2*noticeoffset))/noticescale), tx = int((hudwidth/2)/noticescale),
             tf = int(255*hudblend*noticeblend), tr = 255, tg = 255, tb = 255,
-            tw = int((hudwidth-((hudsize*gapsize)*2+(hudsize*inventorysize)*2))/noticescale);
+            tw = int((hudwidth-((hudsize*edgesize)*2+(hudsize*inventoryleft)+(hudsize*inventoryright)))/noticescale);
         if(noticestone) skewcolour(tr, tg, tb, noticestone);
+
+        pushfont("emphasis");
         if(lastmillis-game::maptime <= noticetitle)
         {
-
             ty += draw_textx("%s", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, tw, *maptitle ? maptitle : mapname);
             pushfont("reduced");
             if(*mapauthor) ty += draw_textx("by %s", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, tw, mapauthor);
-            popfont();
-            pushfont("little");
             defformatstring(gname)("%s", server::gamename(game::gamemode, game::mutators, 0, 32));
             ty += draw_textx("[ \fs\fa%s\fS ]", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, tw, gname);
             popfont();
+            ty += FONTH/3;
         }
+        if(game::gamestate == G_S_INTERMISSION)
+            ty += draw_textx("Intermission", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, tw)+FONTH/3;
+        else if(game::gamestate == G_S_VOTING) ty += draw_textx("Voting in progress", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw)+FONTH/3;
+        else if(client::demoplayback && showdemoplayback)
+            ty += draw_textx("Demo playback in progress", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, tw)+FONTH/3;
+        else if(game::gamestate == G_S_WAITING) ty += draw_textx("Waiting for players", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw)+FONTH/3;
+        else if(hastkwarn(game::focus)) // first and foremost
+            ty += draw_textx("\fzryDo NOT shoot team-mates", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, -1)+FONTH/3;
+        popfont();
 
+        pushfont("default");
         if(game::player1->quarantine)
         {
-            pushfont("emphasis");
             ty += draw_textx("You are \fzoyQUARANTINED", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw);
-            popfont();
-            ty += draw_textx("Please await instructions from a moderator", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw);
+            ty += draw_textx("Please await instructions from a moderator", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw)+FONTH/3;
         }
         else if(game::player1->state == CS_SPECTATOR)
-            ty += draw_textx("[ %s ]", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, showname() ? game::colourname(game::focus) : (game::tvmode() ? "SpecTV" : "Spectating"));
+            ty += draw_textx("[ %s ]", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, specviewname())+FONTH/3;
         else if(game::player1->state == CS_WAITING && showname())
-            ty += draw_textx("[ %s ]", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, game::colourname(game::focus));
-
+            ty += draw_textx("[ %s ]", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, game::colourname(game::focus))+FONTH/3;
 
-        if(game::intermission)
-            ty += draw_textx("Intermission", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, tw);
-        else if(client::demoplayback && showdemoplayback)
-            ty += draw_textx("Demo Playback in Progress", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, tw);
-        if(!game::intermission)
+        if(gs_playing(game::gamestate))
         {
             gameent *target = game::player1->state != CS_SPECTATOR ? game::player1 : game::focus;
             if(target->state == CS_DEAD || target->state == CS_WAITING)
             {
-                int sdelay = m_delay(game::gamemode, game::mutators), delay = target->respawnwait(lastmillis, sdelay);
+                int delay = target->respawnwait(lastmillis, m_delay(game::gamemode, game::mutators, target->team));
                 SEARCHBINDCACHE(attackkey)("primary", 0);
                 if(delay || m_duke(game::gamemode, game::mutators) || (m_fight(game::gamemode) && maxalive > 0))
                 {
-                    if(target->state == CS_WAITING && m_duke(game::gamemode, game::mutators)) ty += draw_textx("Queued for new round", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw);
-                    else if(delay) ty += draw_textx("%s: Down for \fs\fy%s\fS", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, target == game::player1 && target->state == CS_WAITING ? "Please Wait" : "Fragged", timestr(delay, -1));
+                    if(game::gamestate == G_S_WAITING || m_duke(game::gamemode, game::mutators)) ty += draw_textx("Queued for new round", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw);
+                    else if(delay) ty += draw_textx("%s: Down for \fs\fy%s\fS", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, target == game::player1 && target->state == CS_WAITING ? "Please Wait" : "Fragged", timestr(delay));
                     else if(target == game::player1 && target->state == CS_WAITING && m_fight(game::gamemode) && maxalive > 0 && maxalivequeue)
                     {
-                        int n = game::numwaiting(), x = max(int(G(maxalive)*G(maxplayers)), max(int(client::otherclients(true)*G(maxalivethreshold)), G(maxaliveminimum)));
+                        int n = game::numwaiting(), x = max(int(G(maxalive)*G(maxplayers)), max(int(client::otherclients(true, true)*G(maxalivethreshold)), G(maxaliveminimum)));
                         if(m_team(game::gamemode, game::mutators))
                         {
                             if(x%2) x++;
@@ -1265,7 +1397,7 @@ namespace hud
                     if(target == game::player1 && target->state != CS_WAITING && shownotices >= 2 && lastmillis-target->lastdeath >= 500)
                     {
                         pushfont("little");
-                        ty += draw_textx("Press \fs\fc%s\fS to enter respawn queue", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, attackkey);
+                        ty += draw_textx("Press %s to enter respawn queue", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, attackkey);
                         popfont();
                     }
                 }
@@ -1275,7 +1407,7 @@ namespace hud
                     if(target == game::player1 && target->state != CS_WAITING && shownotices >= 2)
                     {
                         pushfont("little");
-                        ty += draw_textx("Press \fs\fc%s\fS to respawn now", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, attackkey);
+                        ty += draw_textx("Press %s to respawn now", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, attackkey);
                         popfont();
                     }
                 }
@@ -1293,21 +1425,21 @@ namespace hud
                         {
                             SEARCHBINDCACHE(waitmodekey)("waitmodeswitch", 3);
                             pushfont("little");
-                            ty += draw_textx("Press \fs\fc%s\fS to %s", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, waitmodekey, game::tvmode() ? "interact" : "switch to TV");
+                            ty += draw_textx("Press %s to %s", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, waitmodekey, game::tvmode() ? "interact" : "switch to TV");
                             popfont();
                         }
                         if(m_loadout(game::gamemode, game::mutators))
                         {
-                            SEARCHBINDCACHE(loadkey)("showgui loadout", 0);
+                            SEARCHBINDCACHE(loadkey)("showgui profile 2", 0);
                             pushfont("little");
-                            ty += draw_textx("Press \fs\fc%s\fS to \fs%s\fS loadout", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, loadkey, target->loadweap.empty() ? "\fzoyselect" : "change");
+                            ty += draw_textx("Press %s to \fs%s\fS loadout", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, loadkey, target->loadweap.empty() ? "\fzoyselect" : "change");
                             popfont();
                         }
                         if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
                         {
                             SEARCHBINDCACHE(teamkey)("showgui team", 0);
                             pushfont("little");
-                            ty += draw_textx("Press \fs\fc%s\fS to change teams", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, teamkey);
+                            ty += draw_textx("Press %s to change teams", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, teamkey);
                             popfont();
                         }
                     }
@@ -1326,9 +1458,11 @@ namespace hud
                     pushfont("emphasis");
                     static vector<actitem> actitems;
                     actitems.setsize(0);
-                    if(entities::collateitems(target, actitems))
+                    vec pos = target->center();
+                    float radius = max(target->height*0.5f, max(target->xradius, target->yradius));
+                    if(entities::collateitems(target, pos, radius, actitems))
                     {
-                        SEARCHBINDCACHE(actionkey)("use", 0);
+                        SEARCHBINDCACHE(actionkey)("use", 0, "\f{\fs\fzuy", "\fS}");
                         while(!actitems.empty())
                         {
                             actitem &t = actitems.last();
@@ -1353,32 +1487,28 @@ namespace hud
                             if(entities::ents.inrange(ent))
                             {
                                 extentity &e = *entities::ents[ent];
-                                if(enttype[e.type].usetype == EU_ITEM)
+                                if(enttype[e.type].usetype == EU_ITEM && e.type == WEAPON)
                                 {
-                                    int sweap = m_weapon(game::gamemode, game::mutators), attr = e.type == WEAPON ? w_attr(game::gamemode, game::mutators, e.attrs[0], sweap) : e.attrs[0];
-                                    if(target->canuse(e.type, attr, e.attrs, sweap, lastmillis, W_S_ALL))
+                                    int sweap = m_weapon(game::gamemode, game::mutators), attr = w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], sweap);
+                                    if(target->canuse(e.type, attr, e.attrs, sweap, lastmillis, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
                                     {
-                                        if(e.type == WEAPON)
+                                        int drop = -1;
+                                        if(m_classic(game::gamemode, game::mutators) && w_carry(target->weapselect, sweap) && target->ammo[attr] < 0 && w_carry(attr, sweap) && target->carry(sweap) >= maxcarry)
+                                            drop = target->drop(sweap);
+                                        if(isweap(drop))
                                         {
-                                            int drop = -1;
-                                            if(w_carry(target->weapselect, sweap) && target->ammo[attr] < 0 && w_carry(attr, sweap) && target->carry(sweap) >= maxcarry)
-                                                drop = target->drop(sweap);
-                                            if(isweap(drop))
-                                            {
-                                                static struct dropattrs : attrvector { dropattrs() { add(0, 5); } } attrs;
-                                                attrs[0] = drop;
-                                                defformatstring(dropweap)("%s", entities::entinfo(WEAPON, attrs, false, true));
-                                                ty += draw_textx("Press \fs\fc%s\fS to swap \fs%s\fS for \fs%s\fS", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, actionkey, dropweap, entities::entinfo(e.type, e.attrs, false, true));
-                                            }
-                                            else ty += draw_textx("Press \fs\fc%s\fS to pickup \fs%s\fS", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, actionkey, entities::entinfo(e.type, e.attrs, false, true));
+                                            static struct dropattrs : attrvector { dropattrs() { add(0, 5); } } attrs;
+                                            attrs[0] = drop;
+                                            defformatstring(dropweap)("%s", entities::entinfo(WEAPON, attrs, false, true));
+                                            ty += draw_textx("Press %s to swap \fs%s\fS for \fs%s\fS", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, actionkey, dropweap, entities::entinfo(e.type, e.attrs, false, true));
                                         }
-                                        else ty += draw_textx("Press \fs\fc%s\fS to use \fs%s\fS", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, actionkey, entities::entinfo(e.type, e.attrs, false, true));
+                                        else ty += draw_textx("Press %s to pickup \fs%s\fS", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, actionkey, entities::entinfo(e.type, e.attrs, false, true));
                                         break;
                                     }
                                 }
                                 else if(e.type == TRIGGER && e.attrs[2] == TA_ACTION)
                                 {
-                                    ty += draw_textx("Press \fs\fc%s\fS to interact", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, actionkey);
+                                    ty += draw_textx("Press %s to interact", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, actionkey);
                                     break;
                                 }
                             }
@@ -1392,14 +1522,14 @@ namespace hud
                         if(target->canshoot(target->weapselect, 0, m_weapon(game::gamemode, game::mutators), lastmillis, (1<<W_S_RELOAD)))
                         {
                             SEARCHBINDCACHE(attackkey)("primary", 0);
-                            ty += draw_textx("Press \fs\fc%s\fS to attack", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, attackkey);
+                            ty += draw_textx("Press %s to attack", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, attackkey);
                             SEARCHBINDCACHE(altkey)("secondary", 0);
-                            ty += draw_textx("Press \fs\fc%s\fS to %s", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, altkey, W(target->weapselect, zooms) ? "zoom" : "alt-attack");
+                            ty += draw_textx("Press %s to %s", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, altkey, W2(target->weapselect, cooked, true)&W_C_ZOOM ? "zoom" : "alt-attack");
                         }
                         if(target->canreload(target->weapselect, m_weapon(game::gamemode, game::mutators), false, lastmillis))
                         {
                             SEARCHBINDCACHE(reloadkey)("reload", 0);
-                            ty += draw_textx("Press \fs\fc%s\fS to reload ammo", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, reloadkey);
+                            ty += draw_textx("Press %s to reload ammo", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, reloadkey);
                         }
                         popfont();
                     }
@@ -1412,17 +1542,17 @@ namespace hud
                 pushfont("little");
                 if(!client::demoplayback)
                 {
-                    ty += draw_textx("Press \fs\fc%s\fS to join the game", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, speconkey);
+                    ty += draw_textx("Press %s 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);
+                        ty += draw_textx("Press %s 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);
-                    ty += draw_textx("Press \fs\fc%s\fS to %s", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, specmodekey, game::tvmode() ? "interact" : "switch to TV");
+                    ty += draw_textx("Press %s to %s", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, specmodekey, game::tvmode() ? "interact" : "switch to TV");
                 }
                 popfont();
             }
@@ -1431,21 +1561,13 @@ namespace hud
             {
                 SEARCHBINDCACHE(editkey)("edittoggle", 1);
                 pushfont("little");
-                ty += draw_textx("Press \fs\fc%s\fS to %s editmode", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, editkey, game::focus->state != CS_EDITING ? "enter" : "exit");
+                ty += draw_textx("Press %s to %s editmode", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, editkey, game::focus->state != CS_EDITING ? "enter" : "exit");
                 popfont();
             }
 
             if(m_capture(game::gamemode)) capture::drawnotices(hudwidth, hudheight, tx, ty, tf/255.f);
             else if(m_defend(game::gamemode)) defend::drawnotices(hudwidth, hudheight, tx, ty, tf/255.f);
             else if(m_bomber(game::gamemode)) bomber::drawnotices(hudwidth, hudheight, tx, ty, tf/255.f);
-            else if(m_gauntlet(game::gamemode) && game::focus->state == CS_ALIVE && game::focus->lastbuff && shownotices >= 3)
-            {
-                pushfont("reduced");
-                if(m_regen(game::gamemode, game::mutators) && gauntletregenbuff && gauntletregenextra)
-                    ty += draw_textx("Buffing: \fs\fo%d%%\fS damage, \fs\fg%d%%\fS shield, +\fs\fy%d\fS regen", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, -1, int(gauntletbuffdamage*100), int(gauntletbuffshield*100), gauntletregenextra)*noticescale;
-                else ty += draw_textx("Buffing: \fs\fo%d%%\fS damage, \fs\fg%d%%\fS shield", tx, ty, 255, 255, 255, tf, TEXT_CENTERED, -1, -1, int(gauntletbuffdamage*100), int(gauntletbuffshield*100))*noticescale;
-                popfont();
-            }
         }
         popfont();
         glPopMatrix();
@@ -1474,7 +1596,7 @@ namespace hud
             }
             glDisable(GL_BLEND);
         }
-        if(UI::ready && (progressing || (commandmillis <= 0 && !curcompass))) UI::render();
+        if(progressing || (commandmillis <= 0 && !curcompass)) UI::render();
         if(!progressing) drawpointers(hudwidth, hudheight);
     }
 
@@ -1487,53 +1609,52 @@ namespace hud
         if(type >= 2)
         {
             int numl = chatconsize, numo = chatconsize+chatconoverflow;
-            if(numl)
+            loopvj(conlines) if(conlines[j].type >= CON_CHAT)
             {
-                loopvj(conlines) if(conlines[j].type >= CON_CHAT)
+                int len = !full && conlines[j].type > CON_CHAT ? chatcontime/2 : chatcontime;
+                if(full || totalmillis-conlines[j].reftime <= len+chatconfade)
                 {
-                    int len = !full && conlines[j].type > CON_CHAT ? chatcontime/2 : chatcontime;
-                    if(full || totalmillis-conlines[j].reftime <= len+chatconfade)
+                    if(refs.length() >= numl)
                     {
-                        if(refs.length() >= numl)
+                        if(refs.length() >= numo)
                         {
-                            if(refs.length() >= numo)
+                            if(full) break;
+                            bool found = false;
+                            loopvrev(refs) if(conlines[refs[i]].reftime+(conlines[refs[i]].type > CON_CHAT ? chatcontime/2 : chatcontime) < conlines[j].reftime+len)
                             {
-                                if(full) break;
-                                bool found = false;
-                                loopvrev(refs) if(conlines[refs[i]].reftime+(conlines[refs[i]].type > CON_CHAT ? chatcontime/2 : chatcontime) < conlines[j].reftime+len)
-                                {
-                                    refs.remove(i);
-                                    found = true;
-                                    break;
-                                }
-                                if(!found) continue;
+                                refs.remove(i);
+                                found = true;
+                                break;
                             }
-                            conlines[j].reftime = min(conlines[j].reftime, totalmillis-len);
+                            if(!found) continue;
                         }
-                        refs.add(j);
+                        conlines[j].reftime = min(conlines[j].reftime, totalmillis-len);
                     }
+                    refs.add(j);
                 }
-                glPushMatrix();
-                glScalef(chatconscale, chatconscale, 1);
-                int tx = int(x/chatconscale), ty = int(y/chatconscale),
-                    ts = int(s/chatconscale), tr = tx+FONTW;
-                tz = int(tz/chatconscale);
-                loopvj(refs)
-                {
-                    int len = !full && conlines[refs[j]].type > CON_CHAT ? chatcontime/2 : chatcontime;
-                    float f = full || !chatconfade ? 1.f : clamp(((len+chatconfade)-(totalmillis-conlines[refs[j]].reftime))/float(chatconfade), 0.f, 1.f),
-                        g = conlines[refs[j]].type > CON_CHAT ? conblend : chatconblend;
-                    tz += draw_textx("%s", tr, ty-tz, 255, 255, 255, int(255*fade*f*g), TEXT_LEFT_UP, -1, ts, conlines[refs[j]].cref)*f;
-                }
-                glPopMatrix();
-                tz = int(tz*chatconscale);
             }
+            glPushMatrix();
+            glScalef(chatconscale, chatconscale, 1);
+            int tx = int(x/chatconscale), ty = int(y/chatconscale),
+                ts = int(s/chatconscale), tr = tx+FONTW;
+            tz = int(tz/chatconscale);
+            loopvj(refs)
+            {
+                int len = !full && conlines[refs[j]].type > CON_CHAT ? chatcontime/2 : chatcontime;
+                float f = full || !chatconfade ? 1.f : clamp(((len+chatconfade)-(totalmillis-conlines[refs[j]].reftime))/float(chatconfade), 0.f, 1.f),
+                    g = conlines[refs[j]].type > CON_CHAT ? conblend : chatconblend;
+                if(chatcondate && *chatcondateformat)
+                    tz += draw_textx("%s %s", tr, ty-tz, 255, 255, 255, int(255*fade*f*g), TEXT_LEFT_UP, -1, ts, gettime(conlines[refs[j]].realtime, chatcondateformat), conlines[refs[j]].cref)*f;
+                else tz += draw_textx("%s", tr, ty-tz, 255, 255, 255, int(255*fade*f*g), TEXT_LEFT_UP, -1, ts, conlines[refs[j]].cref)*f;
+            }
+            glPopMatrix();
+            tz = int(tz*chatconscale);
         }
         else
         {
-            int numl = consize, numo = consize+conoverflow;
-            if(numl)
+            if((showconsole && showhud) || commandmillis > 0)
             {
+                int numl = consize, numo = consize+conoverflow;
                 loopvj(conlines) if(type ? conlines[j].type >= (confilter && !full ? CON_LO : 0) && conlines[j].type <= CON_HI : conlines[j].type >= (confilter && !full ? CON_LO : 0))
                 {
                     int len = conlines[j].type >= CON_CHAT ? (!full && conlines[j].type > CON_CHAT ? chatcontime/2 : chatcontime) : (!full && conlines[j].type < CON_IMPORTANT ? contime/2 : contime),
@@ -1573,7 +1694,9 @@ namespace hud
                     int len = !full && conlines[refs[i]].type < CON_IMPORTANT ? contime/2 : contime;
                     float f = full || !confade ? 1.f : clamp(((len+confade)-(totalmillis-conlines[refs[i]].reftime))/float(confade), 0.f, 1.f),
                         g = full ? fullconblend  : (conlines[refs[i]].type >= CON_IMPORTANT ? selfconblend : conblend);
-                    tz += draw_textx("%s", tr, ty+tz, 255, 255, 255, int(255*fade*f*g), concenter ? TEXT_CENTERED : TEXT_LEFT_JUSTIFY, -1, ts, conlines[refs[i]].cref)*f;
+                    if(condate && *condateformat)
+                        tz += draw_textx("%s %s", tr, ty+tz, 255, 255, 255, int(255*fade*f*g), concenter ? TEXT_CENTERED : TEXT_LEFT_JUSTIFY, -1, ts, gettime(conlines[refs[i]].realtime, condateformat), conlines[refs[i]].cref)*f;
+                    else tz += draw_textx("%s", tr, ty+tz, 255, 255, 255, int(255*fade*f*g), concenter ? TEXT_CENTERED : TEXT_LEFT_JUSTIFY, -1, ts, conlines[refs[i]].cref)*f;
                 }
                 glPopMatrix();
                 tz = int(tz*conscale);
@@ -1597,6 +1720,8 @@ namespace hud
                 drawtexture(tx, ty+tz, th, tw);
                 int cp = commandpos >= 0 ? commandpos : strlen(commandbuf);//, fp = completesize && completeoffset >= 0 ? min(pos, completeoffset+completesize) : -1;
                 tz += draw_textx("%s", tq+tr, ty+tz, 255, 255, 255, int(255*fullconblend*fade), concenter ? TEXT_CENTERED : TEXT_LEFT_JUSTIFY, cp, tt, commandbuf);
+                if(capslockwarn && capslockon)
+                    tz += draw_textx("\fs\fzoy^\fS CapsLock is \fs\fcON\fS", tq+tr, ty+tz, 255, 255, 255, int(255*fullconblend*fade), concenter ? TEXT_CENTERED : TEXT_LEFT_JUSTIFY, -1, tt);
                 popfont();
                 if(commandbuf[0] == '/' && commandbuf[1])
                 {
@@ -1619,8 +1744,7 @@ namespace hud
                             ident *id = idents.access(idname);
                             if(id)
                             {
-                                pushfont("reduced");
-                                mkstring(idtype);
+                                string idtype = "";
                                 if(id->flags&IDF_CLIENT || id->flags&IDF_SERVER)
                                 {
                                     if(id->flags&IDF_ADMIN) concatstring(idtype, "admin-only ");
@@ -1677,11 +1801,18 @@ namespace hud
                                         break;
                                     }
                                 }
+
                                 if(id->desc)
                                     tz += draw_textx("\fa%s", tq, ty+tz, 255, 255, 255, int(255*fullconblend*fade), concenter ? TEXT_CENTERED : TEXT_LEFT_JUSTIFY, -1, tt, id->desc);
                                 if(id->usage)
                                     tz += draw_textx("usage: \fa/%s %s", tq, ty+tz, 255, 255, 255, int(255*fullconblend*fade), concenter ? TEXT_CENTERED : TEXT_LEFT_JUSTIFY, -1, tt, id->name, id->usage);
-                                popfont();
+
+                                if(id->type == ID_ALIAS)
+                                {
+                                    pushfont("consub");
+                                    tz += draw_textx("\facontents: \fw%s", tq, ty+tz, 255, 255, 255, int(255*fullconblend*fade), concenter ? TEXT_CENTERED : TEXT_LEFT_JUSTIFY, -1, tt, id->getstr());
+                                    popfont();
+                                }
                             }
                         }
                     }
@@ -1693,31 +1824,30 @@ namespace hud
         popfont();
     }
 
-    float radarrange()
+    int radartype()
+    {
+        if(game::focus->state == CS_EDITING) return editradarstyle;
+        return radarstyle;
+    }
+
+    int radarrange()
     {
         if(game::focus->state == CS_EDITING) return editradardist ? editradardist : getworldsize();
-        switch(radarstyle)
+        int dist = getworldsize();
+        switch(radartype())
         {
-            case 3: return radarcornerdist ? radarcornerdist : getworldsize();
-            case 2: case 1: case 0: case -1: default: return radardist ? radardist : getworldsize();
+            case 3: dist = radarcornerdist ? radarcornerdist : getworldsize(); break;
+            case 2: case 1: case 0: case -1: default: dist = radardist ? radardist : getworldsize(); break;
         }
-        return getworldsize();
+        if(radardistlimit) dist = min(radardistlimit, dist);
+        return dist;
     }
 
     void drawblip(const char *tex, float area, int w, int h, float s, float blend, int style, vec &pos, const vec &colour, const char *font, const char *text, ...)
     {
-        vec dir;
-        float dist = 1;
-        if(style < 0)
-        {
-            style = -style-1;
-            dir = pos;
-        }
-        else
-        {
-            dir = style ? vec(pos).sub(camera1->o) : pos;
-            dist = clamp(dir.magnitude()/radarrange(), 0.f, 1.f);
-        }
+        if(style < 0) style = radartype();
+        vec dir = vec(pos).sub(camera1->o);
+        float dist = clamp(dir.magnitude()/float(radarrange()), 0.f, 1.f);
         dir.rotate_around_z(-camera1->yaw*RAD).normalize();
         vec loc(0, 0, 0);
         if(style == 2)
@@ -1823,8 +1953,8 @@ namespace hud
         if(radarplayerkill && (game::focus->state == CS_DEAD || game::focus->state == CS_WAITING) && game::focus->lastdeath)
         {
             if(d->clientnum == game::focus->lastattacker)
-                killer = (radarplayerkill >= 2 || d->aitype == AI_NONE) && (d->state == CS_ALIVE || d->state == CS_DEAD || d->state == CS_WAITING);
-            if(d == game::focus) self = lastmillis-game::focus->lastdeath <= m_delay(game::gamemode, game::mutators);
+                killer = (radarplayerkill >= 2 || d->actortype == A_PLAYER) && (d->state == CS_ALIVE || d->state == CS_DEAD || d->state == CS_WAITING);
+            if(d == game::focus) self = lastmillis-game::focus->lastdeath <= m_delay(game::gamemode, game::mutators, d->team);
         }
         if(d == game::focus && !self) return;
         vec dir = vec(d->o).sub(camera1->o);
@@ -1867,13 +1997,13 @@ namespace hud
             else colour[0] = vec::hexcolor(game::getcolour(d, game::playerundertone));
             colour[1] = vec::hexcolor(game::getcolour(d, game::playerovertone));
             const char *tex = isdominated ? dominatedtex : (killer || self ? arrowtex : playerbliptex);
-            float fade = (force || killer || self || dominated ? 1.f : clamp(1.f-(dist/radarrange()), isdominated ? 0.25f : 0.f, 1.f))*blend, size = killer || self ? 1.5f : (isdominated ? 1.25f : 1.f);
+            float fade = (force || killer || self || dominated ? 1.f : clamp(1.f-(dist/float(radarrange())), isdominated ? 0.25f : 0.f, 1.f))*blend, size = killer || self ? 1.5f : (isdominated ? 1.25f : 1.f);
             if(!self && (d->state == CS_DEAD || d->state == CS_WAITING))
             {
                 int millis = d->lastdeath ? lastmillis-d->lastdeath : 0;
                 if(millis > 0)
                 {
-                    int len = min(d->aitype >= AI_START ? (aistyle[d->aitype].living ? min(ai::aideadfade, enemyspawntime ? enemyspawntime : INT_MAX-1) : 500) : m_delay(game::gamemode, game::mutators), 2500);
+                    int len = min(d->actortype >= A_ENEMY ? (actor[d->actortype].living ? min(ai::aideadfade, enemyspawntime ? enemyspawntime : INT_MAX-1) : 500) : m_delay(game::gamemode, game::mutators, d->team), 2500);
                     if(len > 0) fade *= clamp(float(len-millis)/float(len), 0.f, 1.f);
                     else return;
                 }
@@ -1893,7 +2023,7 @@ namespace hud
                 {
                     if(killer && d->state == CS_ALIVE)
                     {
-                        drawblip(tex, 1, w, h, size*(i ? radarplayersize : radarplayerhintsize), fade*(i ? radarplayerblend : radarplayerhintblend), style, d->o, colour[i], "tiny", "%s (%d)", game::colourname(d), d->health);
+                        drawblip(tex, 1, w, h, size*(i ? radarplayersize : radarplayerhintsize), fade*(i ? radarplayerblend : radarplayerhintblend), style, d->o, colour[i], "tiny", "%s (\fs\fc%d\fS)", game::colourname(d), d->health);
                         continue;
                     }
                     if(force || self || chkcond(radarplayernames, !game::tvmode()))
@@ -1922,10 +2052,10 @@ namespace hud
             }
             const char *tex = bliptex;
             vec colour(1, 1, 1);
-            float fade = insel ? 1.f : clamp(1.f-(dist/radarrange()), 0.1f, 1.f), size = radarblipsize;
+            float fade = insel ? 1.f : clamp(1.f-(dist/float(radarrange())), 0.1f, 1.f), size = radarblipsize;
             if(type == WEAPON)
             {
-                int attr1 = w_attr(game::gamemode, game::mutators, attr[0], m_weapon(game::gamemode, game::mutators));
+                int attr1 = w_attr(game::gamemode, game::mutators, type, attr[0], m_weapon(game::gamemode, game::mutators));
                 tex = itemtex(WEAPON, attr1);
                 colour = vec::hexcolor(W(attr1, colour));
                 fade *= radaritemblend;
@@ -1938,15 +2068,11 @@ namespace hud
                 else size = radaritemsize;
                 fade *= radarblipblend;
             }
-            int style = editradarstyle;
             if(game::focus->state != CS_EDITING && !insel && inspawn > 0.f)
-            {
                 fade = radaritemspawn ? 1.f-inspawn : fade+((1.f-fade)*(1.f-inspawn));
-                style = radarstyle;
-            }
-            if(insel) drawblip(tex, 0, w, h, size, fade*blend, style, o, colour, "tiny", "%s %s", enttype[type].name, entities::entinfo(type, attr, insel));
-            else if(chkcond(radaritemnames, !game::tvmode())) drawblip(tex, 0, w, h, size, fade*blend, style, o, colour, "tiny", "%s", entities::entinfo(type, attr, false));
-            else drawblip(tex, 0, w, h, size, fade*blend, style, o, colour);
+            if(insel) drawblip(tex, 0, w, h, size, fade*blend, -1, o, colour, "tiny", "%s %s", enttype[type].name, entities::entinfo(type, attr, insel));
+            else if(chkcond(radaritemnames, !game::tvmode())) drawblip(tex, 0, w, h, size, fade*blend, -1, o, colour, "tiny", "%s", entities::entinfo(type, attr, false));
+            else drawblip(tex, 0, w, h, size, fade*blend, -1, o, colour);
         }
     }
 
@@ -1958,20 +2084,20 @@ namespace hud
             loopv(entgroup) if(entities::ents.inrange(entgroup[i]) && entgroup[i] != hover)
             {
                 gameentity &e = *(gameentity *)entities::ents[entgroup[i]];
-                drawentblip(w, h, blend, entgroup[i], e.o, e.type, e.attrs, e.spawned, e.lastspawn, true);
+                drawentblip(w, h, blend, entgroup[i], e.o, e.type, e.attrs, e.spawned(), e.lastspawn, true);
             }
             if(entities::ents.inrange(hover))
             {
                 gameentity &e = *(gameentity *)entities::ents[hover];
-                drawentblip(w, h, blend, hover, e.o, e.type, e.attrs, e.spawned, e.lastspawn, true);
+                drawentblip(w, h, blend, hover, e.o, e.type, e.attrs, e.spawned(), e.lastspawn, true);
             }
         }
         else
         {
-            loopi(entities::lastusetype[EU_ITEM])
+            loopi(entities::lastuse(EU_ITEM))
             {
                 gameentity &e = *(gameentity *)entities::ents[i];
-                drawentblip(w, h, blend, i, e.o, e.type, e.attrs, e.spawned, e.lastspawn, false);
+                drawentblip(w, h, blend, i, e.o, e.type, e.attrs, e.spawned(), e.lastspawn, false);
             }
             loopv(projs::projs) if(projs::projs[i]->projtype == PRJ_ENT && projs::projs[i]->ready())
             {
@@ -1995,22 +2121,22 @@ namespace hud
                     range = clamp(max(d.damage, radardamagemin)/float(max(radardamagemax-radardamagemin, 1)), radardamagemin/100.f, 1.f),
                     fade = clamp(radardamageblend*blend, min(radardamageblend*radardamagemin/100.f, 1.f), radardamageblend)*amt,
                     size = clamp(range*radardamagesize, min(radardamagesize*radardamagemin/100.f, 1.f), radardamagesize)*amt;
+                vec o = vec(camera1->o).add(vec(d.dir).mul(radarrange()));
                 if(radardamage >= 5)
                 {
                     gameent *a = game::getclient(d.attacker);
-                    drawblip(hurttex, 2+size/3, w, h, size, fade, 0, d.dir, d.colour, "tiny", "%s +%d", a ? game::colourname(a) : "?", d.damage);
+                    drawblip(hurttex, 2+size/3, w, h, size, fade, 0, o, d.colour, "tiny", "%s +%d", a ? game::colourname(a) : "?", d.damage);
                 }
-                else drawblip(hurttex, 2+size/3, w, h, size, fade, 0, d.dir, d.colour);
+                else drawblip(hurttex, 2+size/3, w, h, size, fade, 0, o, d.colour);
             }
         }
     }
 
     void drawradar(int w, int h, float blend)
     {
-        if(radarstyle == 3)
+        if(radartype() == 3)
         {
-            vec pos = vec(camera1->o).sub(minimapcenter).mul(minimapscale).add(0.5f), dir;
-            vecfromyawpitch(camera1->yaw, 0, 1, 0, dir);
+            vec pos = vec(camera1->o).sub(minimapcenter).mul(minimapscale).add(0.5f), dir(camera1->yaw*RAD, 0.f);
             float scale = radarrange(), size = max(w, h)/2, s = size*radarcorner, x = w-s*2, y = 0, q = s*2*radarcorneroffset, r = s-q;
             bindminimap();
             glColor4f(radarcornerbright, radarcornerbright, radarcornerbright, radarcornerblend);
@@ -2035,28 +2161,17 @@ namespace hud
             if(m_capture(game::gamemode)) capture::drawblips(w, h, blend*radarblend);
             else if(m_defend(game::gamemode)) defend::drawblips(w, h, blend*radarblend);
             else if(m_bomber(game::gamemode)) bomber::drawblips(w, h, blend*radarblend);
-            else if(m_gauntlet(game::gamemode))
-            {
-                int numents = entities::lastenttype[CHECKPOINT];
-                loopi(numents) if(entities::ents[i]->type == CHECKPOINT && (entities::ents[i]->attrs[6] == CP_LAST || entities::ents[i]->attrs[6] == CP_FINISH))
-                {
-                    vec dir = vec(entities::ents[i]->o).sub(camera1->o), colour = vec::hexcolor(TEAM(T_ALPHA, colour));
-                    if(radaraffinitynames > 0) drawblip(arrowtex, 3, w, h, radaraffinitysize*1.25f, blend*radaraffinityblend, -1-radarstyle, dir, colour, "little", "goal");
-                    else drawblip(arrowtex, 3, w, h, radaraffinitysize*1.25f, blend*radaraffinityblend, -1-radarstyle, dir, colour);
-                }
-            }
-
         }
         if(chkcond(radarplayers, radarplayerfilter != 3 || m_duke(game::gamemode, game::mutators) || m_edit(game::gamemode))) // 4
         {
             gameent *d = NULL;
-            int numdyns = game::numdynents(), style = radarstyle != 2 ? radarstyle : 1, others[T_MAX] = {0};
+            int numdyns = game::numdynents(), style = radartype() != 2 ? radartype() : 1, others[T_MAX] = {0};
             if(radarplayerduke && game::focus->state == CS_ALIVE && m_survivor(game::gamemode, game::mutators))
             {
-                loopi(numdyns) if((d = (gameent *)game::iterdynents(i)) && d->state == CS_ALIVE && d->aitype < AI_START)
+                loopi(numdyns) if((d = (gameent *)game::iterdynents(i)) && d->state == CS_ALIVE && d->actortype < A_ENEMY)
                     others[d->team]++;
             }
-            loopi(numdyns) if((d = (gameent *)game::iterdynents(i)) && d->state != CS_SPECTATOR && d->aitype < AI_START)
+            loopi(numdyns) if((d = (gameent *)game::iterdynents(i)) && d->state != CS_SPECTATOR && d->actortype < A_ENEMY)
             {
                 bool force = false;
                 if(radarplayerduke && game::focus->state == CS_ALIVE)
@@ -2075,12 +2190,14 @@ namespace hud
     {
         if(skew <= 0.f) return 0;
         float q = clamp(skew, 0.f, 1.f), cr = r*q, cg = g*q, cb = b*q, s = size*skew, cs = s/2, cx = left ? x+cs : x-cs, cy = y-cs;
+        glColor4f(cr, cg, cb, fade);
+        settexture(progringtex, 3);
+        drawslice((SDL_GetTicks()%1000)/1000.f, 0.1f, cx, cy, cs);
         settexture(progresstex, 3);
         glColor4f(cr, cg, cb, fade*0.25f);
-        drawslice(0, 1, cx, cy, cs*2/3);
+        drawslice(0, 1, cx, cy, cs);
         glColor4f(cr, cg, cb, fade);
-        drawslice((SDL_GetTicks()%1000)/1000.f, 0.1f, cx, cy, cs);
-        drawslice(start, length, cx, cy, cs*2/3);
+        drawslice(start, length, cx, cy, cs);
         if(text && *text)
         {
             glPushMatrix();
@@ -2098,10 +2215,11 @@ namespace hud
     const struct barstep
     {
         float amt, r, g, b;
-    } barsteps[3][4] = {
+    } barsteps[4][4] = {
         { { 0, 1, 1, 1 }, { 0.35f, 0.75f, 0.75f, 0.75f }, { 0.65f, 0.65f, 0.65f, 0.65f }, { 1, 1, 1, 1 } },
         { { 0, 0.75f, 0, 0 }, { 0.35f, 1, 0.5f, 0 }, { 0.65f, 1, 1, 0 }, { 1, 0, 1, 0 } },
-        { { 0, 1, 0.25f, 0.25f }, { 0.35f, 1, 0, 1 }, { 0.65f, 0.25f, 0.25f, 1 }, { 1, 0, 1, 1 } }
+        { { 0, 1, 0.25f, 0.25f }, { 0.35f, 1, 0, 1 }, { 0.65f, 0.25f, 0.25f, 1 }, { 1, 0, 1, 1 } },
+        { { 0, 0.5f, 0, 0 }, { 0.35f, 0.25f, 0.f, 0.5f }, { 0.65f, 0.25f, 0.25f, 0.75f }, { 1, 0.75f, 0.75f, 1 } }
     };
 
     int drawitembar(int x, int y, float size, bool left, float r, float g, float b, float fade, float skew, float amt, int type)
@@ -2110,7 +2228,7 @@ namespace hud
         Texture *t = textureload(inventorybartex, 3);
         float q = clamp(skew, 0.f, 1.f), cr = left ? r : r*q, cg = left ? g : g*q, cb = left ? b : b*q, s = size*skew,
               w = float(t->w)/float(t->h)*s, btoff = 1-inventorybarbottom, middle = btoff-inventorybartop;
-        int sx = int(w), sy = int(s), so = int(sx*inventorybaroffset), cx = left ? x-so : x-sx+so, cy = y-sy+int(sy*inventorybartop), cw = sx, ch = int(sy*middle), id = clamp(type, 0, 2);
+        int sx = int(w), sy = int(s), so = int(sx*inventorybaroffset), cx = left ? x-so : x-sx+so, cy = y-sy+int(sy*inventorybartop), cw = sx, ch = int(sy*middle), id = clamp(type, 0, 3);
         glBindTexture(GL_TEXTURE_2D, t->id);
         glBegin(GL_TRIANGLE_STRIP);
         const float margin = 0.1f;
@@ -2157,8 +2275,9 @@ namespace hud
         bool pulse = inventoryflash && game::focus->state == CS_ALIVE && game::focus->health < heal;
         if(bg && sub == 0 && inventorybg)
         {
-            int glow = 0;
+            Texture *u = textureload(*inventorytex ? inventorytex : tex, 3);
             float gr = 1, gb = 1, gg = 1, gf = fade*inventorybgblend;
+            int glow = 0, bw = int(float(u->w)/float(u->h)*s);
             if(inventorytone) skewcolour(gr, gg, gb, inventorytone);
             if(pulse)
             {
@@ -2167,9 +2286,9 @@ namespace hud
                 flashcolourf(gr, gg, gb, gf, 1.f, 0.f, 0.f, 1.f, amt);
                 glow += int(s*inventoryglow*amt);
             }
-            settexture(inventorytex, 3);
+            glBindTexture(GL_TEXTURE_2D, u->id);
             glColor4f(gr, gg, gb, fade*gf);
-            drawtexture(left ? cx-glow : cx-cw-glow, cy-cs-glow, cs+glow*2, cw+glow*2, left);
+            drawtexture(left ? cx-glow : cx-bw-glow, cy-cs-glow, bw+glow*2, cs+glow*2, left);
         }
         if(bg && inventorybg)
         {
@@ -2189,7 +2308,7 @@ namespace hud
         }
         glColor4f(cr, cg, cb, fade);
         glBindTexture(GL_TEXTURE_2D, t->id);
-        drawtexture(left ? cx : cx-cw, cy-cs, cs, cw);
+        drawtexture(left ? cx : cx-cw, cy-cs, cw, cs);
         if(text && *text)
         {
             glPushMatrix();
@@ -2197,7 +2316,7 @@ namespace hud
             if(font && *font) pushfont(font);
             int ox = int(cw*inventorytextoffsetx), oy = int(cs*inventorytextoffsety),
                 tx = int((left ? (cx+cw-ox) : (cx-cw+ox))/skew),
-                ty = int((cy-cs+cs/2-(FONTH/2*skew)+oy)/skew), tj = left ? TEXT_LEFT_JUSTIFY : TEXT_RIGHT_JUSTIFY;
+                ty = int((cy-cs/2+oy)/skew), tj = left ? TEXT_LEFT_BAL : TEXT_RIGHT_BAL;
             defvformatstring(str, text, text);
             draw_textx("%s", tx, ty, 255, 255, 255, int(255*fade), tj|TEXT_NO_INDENT, -1, -1, str);
             if(font && *font) popfont();
@@ -2206,7 +2325,7 @@ namespace hud
         return sy;
     }
 
-    int drawitemtext(int x, int y, float size, bool left, float skew, const char *font, float blend, const char *text, ...)
+    int drawitemtextx(int x, int y, float size, int align, float skew, const char *font, float blend, const char *text, ...)
     {
         if(skew <= 0.f) return 0;
         glPushMatrix();
@@ -2216,37 +2335,38 @@ namespace hud
         if(inventorybg && size > 0)
         {
             int cs = int(size*skew), co = int(cs*inventorybgskew);
-            cx += left ? co/2 : -co/2;
+            cx += (align&TEXT_ALIGN) == TEXT_LEFT_JUSTIFY ? co/2 : -co/2;
             cy -= co/2;
         }
-        int sy = int(FONTH*skew), tj = left ? TEXT_LEFT_UP : TEXT_RIGHT_UP,
-            tx = int((left ? (cx+(FONTW*skew*0.5f)) : (cx-(FONTW*skew*0.5f)))/skew),
-            ty = int(cy/skew), ti = int(255.f*blend);
         defvformatstring(str, text, text);
-        draw_textx("%s", tx, ty, 255, 255, 255, ti, tj|TEXT_NO_INDENT, -1, -1, str);
+        int tx = int(((align&TEXT_ALIGN) == TEXT_LEFT_JUSTIFY ? (cx+(FONTW*skew*0.5f)) : (cx-(FONTW*skew*0.5f)))/skew),
+            ty = int(cy/skew), ti = int(255.f*blend),
+            sy = draw_textx("%s", tx, ty, 255, 255, 255, ti, align|TEXT_NO_INDENT, -1, -1, str)*skew;
         if(font && *font) popfont();
         glPopMatrix();
         return sy;
     }
 
-    const char *teamtexname(int team)
+    int drawitemtext(int x, int y, float size, bool left, float skew, const char *font, float blend, const char *text, ...)
     {
-        const char *teamtexs[T_MAX] = { teamtex, teamalphatex, teamomegatex, teamkappatex, teamsigmatex, teamtex };
-        return teamtexs[clamp(team, 0, T_MAX-1)];
+        defvformatstring(str, text, text);
+        return drawitemtextx(x, y, size, left ? TEXT_LEFT_UP : TEXT_RIGHT_UP, skew, font, blend, "%s", str);
     }
 
-    const char *privname(int priv, int aitype)
+    const char *teamtexname(int team)
     {
-        if(aitype != AI_NONE) return "bot";
-        const char *privnames[PRIV_MAX] = { "none", "player", "supporter", "moderator", "operator", "administrator", "developer", "creator" };
-        return privnames[clamp(priv, 0, PRIV_MAX-1)];
+        const char *teamtexs[T_MAX] = { teamtex, teamalphatex, teamomegatex, teamkappatex, teamsigmatex, teamtex };
+        return teamtexs[clamp(team, 0, T_MAX-1)];
     }
 
-    const char *privtex(int priv, int aitype)
+    const char *privtex(int priv, int actortype)
     {
-        if(aitype > AI_NONE) return privbottex;
-        const char *privtexs[PRIV_MAX] = { privnonetex, privplayertex, privsupportertex, privmoderatortex, privoperatortex, privadministratortex, privdevelopertex, privcreatortex };
-        return privtexs[clamp(priv, 0, PRIV_MAX-1)];
+        if(actortype != A_PLAYER) return privbottex;
+        const char *privtexs[2][PRIV_MAX] = {
+            { privnonetex, privplayertex, privsupportertex, privmoderatortex, privoperatortex, privadministratortex, privdevelopertex, privfoundertex },
+            { privnonetex, privplayertex, privlocalsupportertex, privlocalmoderatortex, privlocaloperatortex, privlocaladministratortex, privnonetex, privnonetex }
+        };
+        return privtexs[priv&PRIV_LOCAL ? 1 : 0][clamp(priv&PRIV_TYPE, 0, int(priv&PRIV_LOCAL ? PRIV_ADMINISTRATOR : PRIV_LAST))];
     }
 
     const char *itemtex(int type, int stype)
@@ -2255,16 +2375,12 @@ namespace hud
         {
             case PLAYERSTART: return playertex; break;
             case AFFINITY: return flagtex; break;
-#ifdef MEK
-            case HEALTH: return healthtex; break;
-            case ARMOUR: return armourtex; break;
-#endif
             case WEAPON:
             {
                 const char *weaptexs[W_MAX] = {
-                    meleetex, pistoltex, swordtex, shotguntex, smgtex, flamertex, plasmatex, rifletex, grenadetex, minetex, rockettex
+                    meleetex, pistoltex, swordtex, shotguntex, smgtex, flamertex, plasmatex, zappertex, rifletex, grenadetex, minetex, rockettex
                 };
-                if(isweap(stype)) return weaptexs[stype];
+                return isweap(stype) ? weaptexs[stype] : questiontex;
                 break;
             }
             default: break;
@@ -2306,6 +2422,7 @@ namespace hud
             case eventicon::AFFINITY:
             {
                 if(m_bomber(game::gamemode)) return bombtex;
+                if(m_defend(game::gamemode)) return pointtex;
                 return flagtex;
             }
         }
@@ -2336,53 +2453,56 @@ namespace hud
     int drawselection(int x, int y, int s, int m, float blend)
     {
         int sy = 0;
-        if(game::focus->state == CS_ALIVE)
+        if(game::focus->state == CS_ALIVE && inventoryammo)
         {
-            if(inventoryammo)
+            const char *hudtexs[W_MAX] = {
+                meleetex, pistoltex, swordtex, shotguntex, smgtex, flamertex, plasmatex, zappertex, rifletex, grenadetex, minetex, rockettex
+            };
+            int sweap = m_weapon(game::gamemode, game::mutators);//, lastweap = game::focus->getlastweap(sweap);
+            loopi(W_MAX) if((i != W_MELEE || sweap == W_MELEE || game::focus->weapselect == W_MELEE || !inventoryhidemelee) && game::focus->holdweap(i, sweap, lastmillis))
             {
-                const char *hudtexs[W_MAX] = {
-                    meleetex, pistoltex, swordtex, shotguntex, smgtex, flamertex, plasmatex, rifletex, grenadetex, minetex, rockettex
-                };
-                int sweap = m_weapon(game::gamemode, game::mutators);
-                loopi(W_MAX) if((i != W_MELEE || sweap == W_MELEE || game::focus->weapselect == W_MELEE || !inventoryhidemelee) && game::focus->holdweap(i, sweap, lastmillis))
+                if(y-sy-s < m) break;
+                float size = s, skew = 0.f;
+                if(game::focus->weapstate[i] == W_S_SWITCH || game::focus->weapstate[i] == W_S_USE)// && (i != game::focus->weapselect || i != lastweap))
                 {
-                    if(y-sy-s < m) break;
-                    float size = s, skew = 0.f;
-                    if((game::focus->weapstate[i] == W_S_SWITCH || game::focus->weapstate[i] == W_S_USE) && (i != game::focus->weapselect || i != game::focus->lastweap))
-                    {
-                        float amt = clamp(float(lastmillis-game::focus->weaplast[i])/float(game::focus->weapwait[i]), 0.f, 1.f);
-                        if(i != game::focus->weapselect) skew = game::focus->hasweap(i, sweap) ? 1.f-(amt*(1.f-inventoryskew)) : 1.f-amt;
-                        else skew = game::focus->weapstate[i] == W_S_USE ? amt : inventoryskew+(amt*(1.f-inventoryskew));
-                    }
-                    else if(game::focus->hasweap(i, sweap) || i == game::focus->weapselect) skew = i != game::focus->weapselect ? inventoryskew : 1.f;
-                    else continue;
-                    vec c(1, 1, 1);
-                    if(inventorycolour) c.mul(vec::hexcolor(W(i, colour)));
-                    else if(inventorytone) skewcolour(c.r, c.g, c.b, inventorytone);
-                    int oldy = y-sy;
-                    if(inventoryammo >= 2 && (i == game::focus->weapselect || inventoryammo >= 3) && W(i, max) > 1 && game::focus->hasweap(i, sweap))
-                        sy += drawitem(hudtexs[i], x, y-sy, size, 0, true, false, c.r, c.g, c.b, blend, skew, "super", "%d", game::focus->ammo[i]);
-                    else sy += drawitem(hudtexs[i], x, y-sy, size, 0, true, false, c.r, c.g, c.b, blend, skew);
-                    if(inventoryammobar && (i == game::focus->weapselect || inventoryammobar >= 2) && W(i, max) > 1 && game::focus->hasweap(i, sweap))
-                        drawitembar(x, oldy, size, false, c.r, c.g, c.b, blend, skew, game::focus->ammo[i]/float(W(i, max)));
-                    if(inventoryweapids && (i == game::focus->weapselect || inventoryweapids >= 2))
+                    float amt = clamp(float(lastmillis-game::focus->weaplast[i])/float(game::focus->weapwait[i]), 0.f, 1.f);
+                    if(i != game::focus->weapselect) skew = game::focus->hasweap(i, sweap) ? 1.f-(amt*(1.f-inventoryskew)) : 1.f-amt;
+                    else skew = game::focus->weapstate[i] == W_S_USE ? amt : inventoryskew+(amt*(1.f-inventoryskew));
+                }
+                else if(game::focus->hasweap(i, sweap) || i == game::focus->weapselect) skew = i != game::focus->weapselect ? inventoryskew : 1.f;
+                else continue;
+                vec c(1, 1, 1);
+                if(inventorytone || inventorycolour) skewcolour(c.r, c.g, c.b, inventorycolour ? W(i, colour) : inventorytone);
+                int oldy = y-sy, curammo = game::focus->ammo[i];
+                if(inventoryammostyle && (game::focus->weapstate[i] == W_S_RELOAD || game::focus->weapstate[i] == W_S_USE) && game::focus->weapload[i] > 0)
+                {
+                    int reloaded = int(curammo*clamp(float(lastmillis-game::focus->weaplast[i])/float(game::focus->weapwait[i]), 0.f, 1.f));
+                    curammo = max(curammo-game::focus->weapload[i], 0);
+                    if(reloaded > curammo) curammo = reloaded;
+                }
+
+                if(inventoryammo >= 2 && (i == game::focus->weapselect || inventoryammo >= 3) && W(i, ammomax) > 1 && game::focus->hasweap(i, sweap))
+                    sy += drawitem(hudtexs[i], x, y-sy, size, 0, true, false, c.r, c.g, c.b, blend, skew, "super", "%d", curammo);
+                else sy += drawitem(hudtexs[i], x, y-sy, size, 0, true, false, c.r, c.g, c.b, blend, skew);
+                if(inventoryammobar && (i == game::focus->weapselect || inventoryammobar >= 2) && W(i, ammomax) > 1 && game::focus->hasweap(i, sweap))
+                    drawitembar(x, oldy, size, false, c.r, c.g, c.b, blend, skew, curammo/float(W(i, ammomax)));
+                if(inventoryweapids && (i == game::focus->weapselect || inventoryweapids >= 2))
+                {
+                    static string weapids[W_MAX];
+                    static int lastweapids = -1;
+                    int n = weapons::slot(game::focus, i);
+                    if(lastweapids != changedkeys)
                     {
-                        static string weapids[W_MAX];
-                        static int lastweapids = -1;
-                        int n = weapons::slot(game::focus, i);
-                        if(lastweapids != changedkeys)
+                        loopj(W_MAX)
                         {
-                            loopj(W_MAX)
-                            {
-                                defformatstring(action)("weapon %d", j);
-                                const char *actkey = searchbind(action, 0);
-                                if(actkey && *actkey) copystring(weapids[j], actkey);
-                                else formatstring(weapids[j])("%d", j);
-                            }
-                            lastweapids = changedkeys;
+                            defformatstring(action)("weapon %d", j);
+                            const char *actkey = searchbind(action, 0);
+                            if(actkey && *actkey) copystring(weapids[j], actkey);
+                            else formatstring(weapids[j])("%d", j);
                         }
-                        drawitemtext(x, oldy, size, false, skew, "reduced", blend, "\f[%d]%s", inventorycolour >= 2 ? W(i, colour) : 0xAAAAAA, isweap(n) ? weapids[n] : "?");
+                        lastweapids = changedkeys;
                     }
+                    drawitemtext(x, oldy, size, false, skew, "default", blend, "\f[%d]%s", inventorycolour >= 2 ? W(i, colour) : 0xAAAAAA, isweap(n) ? weapids[n] : "?");
                 }
             }
         }
@@ -2402,8 +2522,8 @@ namespace hud
 
     int drawbar(int x, int y, int w, int h, int type, float top, float bottom, float fade, float amt, const char *tex, const char *bgtex, int tone, float bgglow, float blend, float pulse, float throb)
     {
-        int offset = int(w*throb), id = clamp(type, 0, 2);
-        if(*bgtex)
+        int offset = int(w*throb), id = clamp(type, 0, 3);
+        if(bgtex && *bgtex)
         {
             int glow = 0;
             float gr = 1, gg = 1, gb = 1, gf = fade*blend;
@@ -2412,7 +2532,7 @@ namespace hud
             {
                 int millis = lastmillis%1000;
                 float skew = (millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f))*pulse;
-                flashcolourf(gr, gg, gb, gf, id != 1 ? 0.5f : 1.f, 0.f, id != 1 ? 0.5f : 0.f, 1.f, skew);
+                flashcolourf(gr, gg, gb, gf, id != 1 && (id != 2 || throb <= 0) ? 0.5f : 1.f, 0.f, id != 1 && (id != 2 || throb <= 0) ? 0.5f : 0.f, 1.f, skew);
                 glow += int(w*bgglow*skew);
             }
             settexture(bgtex, 3);
@@ -2461,7 +2581,7 @@ namespace hud
 
     int drawhealth(int x, int y, int s, float blend, bool interm)
     {
-        int size = s+s/2, width = s-s/4, sy = 0;
+        int size = s*2, sy = 0;
         bool alive = !interm && game::focus->state == CS_ALIVE;
         if(alive)
         {
@@ -2469,52 +2589,75 @@ namespace hud
             {
                 float fade = blend*inventoryhealthblend;
                 int heal = m_health(game::gamemode, game::mutators, game::focus->model);
-                float pulse = inventoryhealthflash && game::focus->health < heal ? float(heal-game::focus->health)/float(heal) : 0.f,
-                    throb = inventoryhealththrob > 0 && regentime && game::focus->lastregen && lastmillis-game::focus->lastregen <= regentime ? clamp((lastmillis-game::focus->lastregen)/float(regentime/2), 0.f, 2.f) : 0.f;
+                float hpulse = inventoryhealthflash ? clamp((heal-game::focus->health)/float(heal), 0.f, 1.f) : 0.f,
+                      hthrob = inventoryhealththrob > 0 && regentime && game::focus->lastregen && lastmillis-game::focus->lastregen <= regentime ? clamp((lastmillis-game::focus->lastregen)/float(regentime/2), 0.f, 2.f) : 0.f;
                 if(inventoryhealth&2)
-                    sy += drawbar(x, y, width, size, 1, inventoryhealthbartop, inventoryhealthbarbottom, fade, clamp(game::focus->health/float(heal), 0.0f, 1.0f), healthtex, healthbgtex, inventorytone, inventoryhealthbgglow, inventoryhealthbgblend, pulse, (throb > 1.f ? 1.f-throb : throb)*inventoryhealththrob);
-                float gr = 1, gg = 1, gb = 1;
-                if(pulse > 0)
+                    sy += drawbar(x, y, s, size, 1, inventoryhealthbartop, inventoryhealthbarbottom, fade, clamp(game::focus->health/float(heal), 0.f, 1.f), healthtex, healthbgtex, inventorytone, inventoryhealthbgglow, inventoryhealthbgblend, hpulse, (hthrob > 1.f ? 1.f-hthrob : hthrob)*inventoryhealththrob);
+                if(inventoryhealth&1)
+                {
+                    float gr = 1, gg = 1, gb = 1;
+                    if(hpulse > 0)
+                    {
+                        int millis = lastmillis%1000;
+                        float amt = (millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f))*hpulse;
+                        flashcolour(gr, gg, gb, 1.f, 0.f, 0.f, amt);
+                    }
+                    pushfont("super");
+                    int ty = inventoryhealth&2 ? 0-size/2 : 0;
+                    ty += draw_textx("%d", x+s/2, y-sy-ty, int(gr*255), int(gg*255), int(gb*255), int(fade*255), TEXT_CENTER_UP, -1, -1, max(game::focus->health, 0));
+                    popfont();
+                    if(!(inventoryhealth&2))
+                    {
+                        pushfont("reduced");
+                        ty += draw_textx("health", x+s/2, y-sy-ty, 255, 255, 255, int(fade*255), TEXT_CENTER_UP, -1, -1);
+                        popfont();
+                        sy += ty;
+                    }
+                }
+            }
+            if(game::focus->actortype < A_ENEMY && physics::allowimpulse(game::focus) && m_impulsemeter(game::gamemode, game::mutators) && inventoryimpulse)
+            {
+                float fade = blend*inventoryimpulseblend, span = 1-clamp(float(game::focus->impulse[IM_METER])/float(impulsemeter), 0.f, 1.f),
+                      pulse = inventoryimpulseflash && game::focus->impulse[IM_METER] ? 1-span : 0.f, throb = 0, gr = 1, gg = 1, gb = 1;
+                flashcolour(gr, gg, gb, 0.25f, 0.25f, 0.25f, 1-span);
+                if(pulse > 0 && inventoryimpulsethrob > 0 && impulsemeter-game::focus->impulse[IM_METER] < impulsecost)
                 {
                     int millis = lastmillis%1000;
-                    float amt = (millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f))*pulse;
-                    flashcolour(gr, gg, gb, 1.f, 0.f, 0.f, amt);
+                    throb = millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f);
+                    flashcolour(gr, gg, gb, 1.f, 0.f, 0.f, throb);
+                }
+                if(inventoryimpulse&2)
+                    sy += drawbar(x, y-sy, s, size, 2, inventoryimpulsebartop, inventoryimpulsebarbottom, fade, span, impulsetex, impulsebgtex, inventorytone, inventoryimpulsebgglow, inventoryimpulsebgblend, pulse, throb*inventoryimpulsethrob);
+                if(inventoryimpulse&1)
+                {
+                    if(!(inventoryimpulse&2))
+                    {
+                        pushfont("super");
+                        int ty = draw_textx("%d%%", x+s/2, y-sy+(inventoryimpulse&2 ? size/2 : 0), int(gr*255), int(gg*255), int(gb*255), int(fade*255), TEXT_CENTER_UP, -1, -1, int(span*100));
+                        popfont();
+                        pushfont("reduced");
+                        ty += draw_textx("impulse", x+s/2, y-sy-ty, 255, 255, 255, int(fade*255), TEXT_CENTER_UP, -1, -1);
+                        popfont();
+                        sy += ty;
+                    }
+                    else
+                    {
+                        pushfont("super");
+                        draw_textx("%d", x+s/2, y-sy+(inventoryimpulse&2 ? size/2 : 0), int(gr*255), int(gg*255), int(gb*255), int(fade*255), TEXT_CENTER_UP, -1, -1, int(span*100));
+                        popfont();
+                    }
                 }
-                pushfont("super");
-                int ty = 0;
-                if(inventoryhealth&1)
-                    ty = draw_textx("%d", x+width/2, y-sy+(inventoryhealth == 1 ? FONTH : size/2-FONTH), int(gr*255), int(gg*255), int(gb*255), int(fade*255), TEXT_CENTERED, -1, -1, max(game::focus->health, 0));
-                if(inventoryhealth == 1) sy += ty;
-                popfont();
             }
-            if(inventoryvelocity >= (m_checkpoint(game::gamemode) ? 1 : 2))
+            if(inventoryvelocity >= (m_race(game::gamemode) ? 1 : 2))
             {
                 float fade = blend*inventoryvelocityblend;
                 pushfont("emphasis");
-                sy += draw_textx("%d", x+width/2, y-sy, 255, 255, 255, int(fade*255), TEXT_CENTER_UP, -1, -1, int(vec(game::focus->vel).add(game::focus->falling).magnitude()));
+                sy += draw_textx("%d", x+s/2, y-sy, 255, 255, 255, int(fade*255), TEXT_CENTER_UP, -1, -1, int(vec(game::focus->vel).add(game::focus->falling).magnitude()));
                 popfont();
                 pushfont("reduced");
-                sy += draw_textx("speed", x+width/2, y-sy, 255, 255, 255, int(fade*255), TEXT_CENTER_UP, -1, -1);
+                sy += draw_textx("speed", x+s/2, y-sy, 255, 255, 255, int(fade*255), TEXT_CENTER_UP, -1, -1);
                 popfont();
             }
-            if(game::focus->aitype < AI_START && physics::allowimpulse(game::focus) && impulsemeter && impulsecost && inventoryimpulse)
-            {
-                float fade = blend*inventoryimpulseblend;
-                float amt = 1-clamp(float(game::focus->impulse[IM_METER])/float(impulsemeter), 0.f, 1.f);
-                if(inventoryimpulse == 2)
-                    sy += drawbar(x, y-sy, width, size, 2, inventoryimpulsebartop, inventoryimpulsebarbottom, fade, amt, impulsetex, impulsebgtex, inventorytone, inventoryimpulsebgglow, inventoryimpulsebgblend, inventoryimpulseflash && game::focus->impulse[IM_METER] ? 1-amt : 0.f, 0.f);
-                else
-                {
-                    pushfont("emphasis");
-                    sy += draw_textx("%s%d%%", x+width/2, y-sy, 255, 255, 255, int(fade*255), TEXT_CENTER_UP, -1, -1,
-                        game::focus->impulse[IM_METER] > 0 ? (impulsemeter-game::focus->impulse[IM_METER] > impulsecost ? "\fc" : "\fy") : "\fg",
-                            int(amt*100));
-                    popfont();
-                    pushfont("reduced");
-                    sy += draw_textx("impulse", x+width/2, y-sy, 255, 255, 255, int(fade*255), TEXT_CENTER_UP, -1, -1);
-                    popfont();
-                }
-            }
             if(inventoryalert)
             {
                 float fade = blend*inventoryalertblend;
@@ -2528,7 +2671,7 @@ namespace hud
                         float amt = millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f);
                         flashcolour(gr, gg, gb, 1.f, 1.f, 1.f, amt);
                     }
-                    sy += drawitem(buffedtex, x, y-sy, width, 0, false, true, gr, gg, gb, fade);
+                    sy += drawitem(buffedtex, x, y-sy, s, 0, false, true, gr, gg, gb, fade);
                 }
                 if(burntime && game::focus->burning(lastmillis, burntime))
                 {
@@ -2540,7 +2683,7 @@ namespace hud
                         float amt = millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f);
                         flashcolour(gr, gg, gb, 1.f, 0.5f, 0.f, amt);
                     }
-                    sy += drawitem(burningtex, x, y-sy, width, 0, false, true, gr, gg, gb, fade);
+                    sy += drawitem(burningtex, x, y-sy, s, 0, false, true, gr, gg, gb, fade);
                 }
                 if(bleedtime && game::focus->bleeding(lastmillis, bleedtime))
                 {
@@ -2552,7 +2695,7 @@ namespace hud
                         float amt = millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f);
                         flashcolour(gr, gg, gb, 1.f, 0.f, 0.f, amt);
                     }
-                    sy += drawitem(bleedingtex, x, y-sy, width, 0, false, true, gr, gg, gb, fade);
+                    sy += drawitem(bleedingtex, x, y-sy, s, 0, false, true, gr, gg, gb, fade);
                 }
                 if(shocktime && game::focus->shocking(lastmillis, shocktime))
                 {
@@ -2564,7 +2707,7 @@ namespace hud
                         float amt = millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f);
                         flashcolour(gr, gg, gb, 0.f, 0.8f, 1.f, amt);
                     }
-                    sy += drawitem(shockingtex, x, y-sy, width, 0, false, true, gr, gg, gb, fade);
+                    sy += drawitem(shockingtex, x, y-sy, s, 0, false, true, gr, gg, gb, fade);
                 }
             }
             if(inventoryconopen)
@@ -2580,8 +2723,34 @@ namespace hud
                         float amt = millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f);
                         flashcolour(gr, gg, gb, 1.f, 1.f, 1.f, amt);
                     }
-                    sy += drawitem(chattex, x, y-sy, width, 0, false, true, gr, gg, gb, fade);
+                    sy += drawitem(chattex, x, y-sy, s, 0, false, true, gr, gg, gb, fade);
+                }
+            }
+            if(inventoryinput&(game::focus != game::player1 ? 2 : 1))
+            {
+                static const char *actionnames[AC_TOTAL] = {
+                    "shoot1", "shoot2", "reload", "use", "jump", "walk", "crouch", "special", "drop", "affinity"
+                };
+                pushfont("little");
+
+                sy += draw_textx("\f{\f[%d]\f(%s)}", x+s/2, y-sy, 255, 255, 255, int(blend*inventoryinputblend*255), TEXT_CENTER_UP, -1, -1,
+                        game::focus->move == -1 ? inventoryinputactive : inventoryinputcolour, arrowdowntex);
+
+                sy += draw_textx("\f{\f[%d]\f(%s)}  \f{\f[%d]\f(%s)}", x+s/2, y-sy, 255, 255, 255, int(blend*inventoryinputblend*255), TEXT_CENTER_UP, -1, -1,
+                        game::focus->strafe == 1 ? inventoryinputactive : inventoryinputcolour, arrowlefttex,
+                        game::focus->strafe == -1 ? inventoryinputactive : inventoryinputcolour, arrowrighttex);
+
+                sy += draw_textx("\f{\f[%d]\f(%s)}", x+s/2, y-sy, 255, 255, 255, int(blend*inventoryinputblend*255), TEXT_CENTER_UP, -1, -1,
+                        game::focus->move == 1 ? inventoryinputactive : inventoryinputcolour, arrowtex);
+
+                loopi(AC_TOTAL) if(inventoryinputfilter&(1<<i))
+                {
+                    bool active = game::focus->action[i] || (inventoryinputlinger&(1<<i) && game::focus->actiontime[i] && lastmillis-abs(game::focus->actiontime[i]) <= inventoryinputdelay);
+                    sy += draw_textx("\f{\f[%d]%s}", x+s/2, y-sy, 255, 255, 255, int(blend*inventoryinputblend*255), TEXT_CENTER_UP, -1, -1,
+                            active ? inventoryinputactive : inventoryinputcolour, actionnames[i]);
                 }
+
+                popfont();
             }
         }
         else
@@ -2599,14 +2768,14 @@ namespace hud
             {
                 sy -= x/2;
                 pushfont("emphasis");
-                sy += draw_textx("%s", x+width/2, y-sy, 255, 255, 255, int(blend*inventorystatusblend*255), TEXT_CENTER_UP, -1, -1, state);
+                sy += draw_textx("%s", x+s/2, y-sy, 255, 255, 255, int(blend*inventorystatusblend*255), TEXT_CENTER_UP, -1, -1, state);
                 popfont();
             }
             if(inventorystatus&2 && *tex)
             {
                 float gr = 1, gg = 1, gb = 1, fade = blend*inventorystatusiconblend;
                 if(inventorytone) skewcolour(gr, gg, gb, inventorytone);
-                sy += drawitem(tex, x, y-sy, width, 0, false, true, gr, gg, gb, fade);
+                sy += drawitem(tex, x, y-sy, s, 0, false, true, gr, gg, gb, fade);
             }
         }
         if(inventorygameinfo)
@@ -2618,9 +2787,9 @@ namespace hud
             {
                 int millis = lastmillis%1000;
                 float amt = millis <= 500 ? millis/500.f : 1.f-((millis-500)/500.f);
-                flashcolour(gr, gg, gb, 1.f, 1.f, 1.f, amt);
+                flashcolour(gr, gg, gb, 0.f, 1.f, 1.f, amt);
             }
-            #define ADDMODE(a) sy += drawitem(a, x, y-sy, width, 0, false, true, gr, gg, gb, fade);
+            #define ADDMODE(a) sy += drawitem(a, x, y-sy, s, 0, false, true, gr, gg, gb, fade);
             if(game::focus->state != CS_EDITING && !m_dm(game::gamemode) && ((alive && inventorygameinfo&1) || over)) ADDMODEICON(game::gamemode, game::mutators)
             if(over && m_multi(game::gamemode, game::mutators) && !(gametype[game::gamemode].implied&(1<<G_M_MULTI))) ADDMODE(modemultitex)
             if(over && m_ffa(game::gamemode, game::mutators) && !(gametype[game::gamemode].implied&(1<<G_M_FFA))) ADDMODE(modeffatex)
@@ -2632,10 +2801,11 @@ namespace hud
             if(((alive && inventorygameinfo&4) || over) && m_survivor(game::gamemode, game::mutators) && !(gametype[game::gamemode].implied&(1<<G_M_SURVIVOR))) ADDMODE(modesurvivortex)
             if(over && m_classic(game::gamemode, game::mutators) && !(gametype[game::gamemode].implied&(1<<G_M_CLASSIC))) ADDMODE(modeclassictex)
             if(over && m_onslaught(game::gamemode, game::mutators) && !(gametype[game::gamemode].implied&(1<<G_M_ONSLAUGHT))) ADDMODE(modeonslaughttex)
-            if(((alive && inventorygameinfo&2) || over) && m_jet(game::gamemode, game::mutators)) ADDMODE(modejetpacktex)
+            if(((alive && inventorygameinfo&2) || over) && m_freestyle(game::gamemode, game::mutators)) ADDMODE(modefreestyletex)
             if(((alive && inventorygameinfo&2) || over) && m_vampire(game::gamemode, game::mutators)) ADDMODE(modevampiretex)
-            if(((alive && inventorygameinfo&2) || over) && m_expert(game::gamemode, game::mutators) && !(gametype[game::gamemode].implied&(1<<G_M_EXPERT))) ADDMODE(modeexperttex)
             if((alive && inventorygameinfo&4) && m_resize(game::gamemode, game::mutators) && !(gametype[game::gamemode].implied&(1<<G_M_RESIZE))) ADDMODE(moderesizetex)
+            if(((alive && inventorygameinfo&2) || over) && m_hard(game::gamemode, game::mutators)) ADDMODE(modehardtex)
+            if(((alive && inventorygameinfo&2) || over) && m_basic(game::gamemode, game::mutators)) ADDMODE(modebasictex)
             #undef ADDMODE
         }
         return sy;
@@ -2644,109 +2814,195 @@ namespace hud
     int drawtimer(int x, int y, int s, float blend)
     {
         if(game::focus->state == CS_EDITING || game::focus->state == CS_SPECTATOR) return 0;
-        if(m_gauntlet(game::gamemode) && game::focus->team != T_ALPHA) return 0;
         int sy = 0;
-        if(inventorycheckpoint && m_checkpoint(game::gamemode))
+        if(inventoryrace && m_race(game::gamemode))
         {
-            float fade = blend*inventorycheckpointblend;
+            float fade = blend*inventoryraceblend;
             pushfont("default");
-            if((game::focus->cpmillis || game::focus->cptime) && (game::focus->state == CS_ALIVE || game::focus->state == CS_DEAD || game::focus->state == CS_WAITING))
+            if((!m_gsp3(game::gamemode, game::mutators) || game::focus->team == T_ALPHA) && (game::focus->cpmillis || game::focus->cptime) && (game::focus->state == CS_ALIVE || game::focus->state == CS_DEAD || game::focus->state == CS_WAITING))
             {
-                sy += draw_textx("\falap: \fw%d", x, y-sy, 255, 255, 255, int(fade*255), TEXT_LEFT_UP, -1, -1, game::focus->cplaps+1);
+                sy += draw_textx("\falap: \fw%d", x, y-sy, 255, 255, 255, int(fade*255), TEXT_LEFT_UP, -1, -1, game::focus->points+1);
                 if(game::focus->cptime)
-                    sy += draw_textx("\fy%s", x, y-sy, 255, 255, 255, int(fade*255), TEXT_LEFT_UP, -1, -1, timestr(game::focus->cptime));
+                    sy += draw_textx("\fy%s", x, y-sy, 255, 255, 255, int(fade*255), TEXT_LEFT_UP, -1, -1, timestr(game::focus->cptime, inventoryracestyle));
                 if(game::focus->cpmillis)
-                    sy += draw_textx("%s", x, y-sy, 255, 255, 255, int(fade*255), TEXT_LEFT_UP, -1, -1, timestr(lastmillis-game::focus->cpmillis, 1));
+                    sy += draw_textx("%s", x, y-sy, 255, 255, 255, int(fade*255), TEXT_LEFT_UP, -1, -1, timestr(lastmillis-game::focus->cpmillis, inventoryracestyle));
                 else if(game::focus->cplast)
-                    sy += draw_textx("\fzwe%s", x, y-sy, 255, 255, 255, int(fade*255), TEXT_LEFT_UP, -1, -1, timestr(game::focus->cplast));
+                    sy += draw_textx("\fzwe%s", x, y-sy, 255, 255, 255, int(fade*255), TEXT_LEFT_UP, -1, -1, timestr(game::focus->cplast, inventoryracestyle));
             }
-            sy += trialinventory(x, y-sy, s, fade);
+            sy += raceinventory(x, y-sy, s, fade);
             popfont();
         }
         return sy;
     }
 
-    void drawinventory(int w, int h, int edge, float blend)
+    int drawinventory(int w, int h, int edge, int top, int bottom, float blend)
     {
-        pushfont("console");
+        int cx[2] = { edge, w-edge }, cy[2] = { h-edge-bottom, h-edge-bottom }, left = edge,
+            csl = int(inventoryleft*w), csr = int(inventoryright*w), cr = edge/2, cc = 0, bf = blend*255, bs = (w-edge*2)/2;
+        if(!texpaneltimer)
+        {
+            if(totalmillis-laststats >= statrate)
+            {
+                memcpy(prevstats, curstats, sizeof(prevstats));
+                laststats = totalmillis-(totalmillis%statrate);
+            }
+            int nextstats[NUMSTATS] = {
+                vtris*100/max(wtris, 1), vverts*100/max(wverts, 1), xtraverts/1024, xtravertsva/1024, glde, gbatches, getnumqueries(), rplanes, curfps, bestfpsdiff, worstfpsdiff
+            };
+            loopi(NUMSTATS) if(prevstats[i] == curstats[i]) curstats[i] = nextstats[i];
+            pushfont("consub");
+            if(showfps)
+            {
+                pushfont("console");
+                cy[1] -= draw_textx("%d fps", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, curstats[8]);
+                popfont();
+                switch(showfps)
+                {
+                    case 3:
+                        cy[1] -= draw_textx("+%d-%d range", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, maxfps, curstats[9], curstats[10]);
+                    case 2:
+                        cy[1] -= draw_textx("%d max", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, maxfps);
+                    default: break;
+                }
+            }
+            if(showstats >= (m_edit(game::gamemode) ? 1 : 2))
+            {
+                cy[1] -= draw_textx("ond:%d va:%d gl:%d(%d) oq:%d", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, allocnodes*8, allocva, curstats[4], curstats[5], curstats[6]);
+                cy[1] -= draw_textx("wtr:%dk(%d%%) wvt:%dk(%d%%) evt:%dk eva:%dk", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, wtris/1024, curstats[0], wverts/1024, curstats[1], curstats[2], curstats[3]);
+                cy[1] -= draw_textx("ents:%d(%d) wp:%d lm:%d rp:%d pvs:%d", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, entities::ents.length(), entgroup.length(), ai::waypoints.length(), lightmaps.length(), curstats[7], getnumviewcells());
+                if(game::player1->state == CS_EDITING)
+                {
+                    cy[1] -= draw_textx("cube:%s%d corner:%d orient:%d grid:%d%s", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs,
+                            selchildcount<0 ? "1/" : "", abs(selchildcount), sel.corner, sel.orient, sel.grid, showmat && selchildmat > 0 ? getmaterialdesc(selchildmat, " mat:") : "");
+                    cy[1] -= draw_textx("sel:%d,%d,%d %d,%d,%d (%d,%d,%d,%d)", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs,
+                            sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z,
+                                sel.cx, sel.cxs, sel.cy, sel.cys);
+                }
+                cy[1] -= draw_textx("pos:%.2f,%.2f,%.2f yaw:%.2f pitch:%.2f", cx[1], cy[1], 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs,
+                        camera1->o.x, camera1->o.y, camera1->o.z, camera1->yaw, camera1->pitch);
+            }
+            popfont();
+        }
+        if(!minimal(showinventory, true)) return left;
         float fade = blend*inventoryblend;
-        int cx[2] = { edge, w-edge }, cy[2] = { h-edge, h-edge }, cs = int(inventorysize*w), cr = edge/2, cc = 0;
-        popfont();
-        bool interm = game::intermission && game::tvmode() && game::focus == game::player1;
+        bool interm = !gs_playing(game::gamestate) && game::tvmode() && game::focus == game::player1;
         loopi(2) switch(i)
         {
-            case 0: default:
+            case 0:
             {
-                if((cc = drawhealth(cx[i], cy[i], cs, fade, interm)) > 0) cy[i] -= cc+cr;
-                if(!interm && (cc = drawtimer(cx[i], cy[i], cs, fade)) > 0) cy[i] -= cc+cr;
+                bool found = false;
+                if((cc = drawhealth(cx[i], cy[i], csl, fade, interm)) > 0) { cy[i] -= cc+cr; found = true; }
+                if(!interm && (cc = drawtimer(cx[i], cy[i], csl, fade)) > 0) { cy[i] -= cc+cr; found = true; }
+                if(found) left += csl;
                 break;
             }
             case 1:
             {
-                int cm = edge;
-                if(radarstyle == 3 && !game::intermission && !hasinput(true) && (game::focus->state == CS_EDITING ? showeditradar >= 1 : chkcond(showradar, !game::tvmode() || (game::focus != game::player1 && radarstyle==3))))
+                int cm = cr+top;
+                if(!radardisabled && !m_hard(game::gamemode, game::mutators) && radartype() == 3 && !hasinput(true) && (game::focus->state == CS_EDITING ? showeditradar >= 1 : chkcond(showradar, !game::tvmode() || (game::focus != game::player1 && radartype() == 3))))
                     cm += int(max(w, h)/2*radarcorner*2);
-                if(!texpaneltimer)
+                if(inventorydate)
+                    cm += drawitemtextx(cx[i], cm, 0, TEXT_RIGHT_JUSTIFY, inventorydateskew, "super", fade*inventorydateblend, "%s", gettime(currenttime, inventorydateformat));
+                if(inventorytime)
                 {
-                    cy[i] -= showfps || showstats >= (m_edit(game::gamemode) ? 1 : 2) ? FONTH*2 : 0;
-                    if(lastnewgame)
+                    if(paused) cm += drawitemtextx(cx[i], cm, 0, TEXT_RIGHT_JUSTIFY, inventorytimeskew, "super", fade*inventorytimeblend, "\fs\fopaused\fS", 0xFFFFFF);
+                    else if(m_edit(game::gamemode)) cm += drawitemtextx(cx[i], cm, 0, TEXT_RIGHT_JUSTIFY, inventorytimeskew, "super", fade*inventorytimeblend, "\fs\fgediting\fS");
+                    else if(m_play(game::gamemode) || client::demoplayback)
                     {
-                        if(!game::intermission) lastnewgame = 0;
-                        else
+                        int timecorrected = max(game::timeremaining*1000-(lastmillis-game::lasttimeremain), 0);
+                        if(game::gamestate != G_S_PLAYING)
                         {
-                            int millis = votelimit-(totalmillis-lastnewgame);
-                            float amt = float(millis)/float(votelimit);
-                            const char *col = "\fw";
-                            if(amt > 0.75f) col = "\fg";
-                            else if(amt > 0.5f) col = "\fc";
-                            else if(amt > 0.25f) col = "\fy";
-                            else col = "\fo";
-                            drawprogress(cx[i], cm+cs, 0, 1, cs, false, 1, 1, 1, fade*0.25f, 1);
-                            cm += drawprogress(cx[i], cm+cs, 1-amt, amt, cs, false, 1, 1, 1, fade, 1, "default", "%s%d", col, int(millis/1000.f));
+                            const char *name = "";
+                            float amt = 0.f;
+                            switch(game::gamestate)
+                            {
+                                case G_S_WAITING:
+                                {
+                                    if(!waitforplayertime) break;
+                                    amt = float(timecorrected)/float(waitforplayertime);
+                                    name = "waiting";
+                                    break;
+                                }
+                                case G_S_VOTING:
+                                {
+                                    if(!votelimit) break;
+                                    amt = float(timecorrected)/float(votelimit);
+                                    name = "voting";
+                                    break;
+                                }
+                                case G_S_INTERMISSION:
+                                {
+                                    if(!intermlimit) break;
+                                    amt = float(timecorrected)/float(intermlimit);
+                                    name = "intermission";
+                                    break;
+                                }
+                                case G_S_OVERTIME:
+                                {
+                                    if(!overtimelimit) break;
+                                    amt = float(timecorrected)/float(overtimelimit);
+                                    name = "overtime";
+                                    break;
+                                }
+                            }
+                            const char *col = "\fo";
+                            if(amt > 0.66f) col = "\fg";
+                            else if(amt > 0.33f) col = "\fy";
+                            cm += drawitemtextx(cx[i], cm, 0, TEXT_RIGHT_JUSTIFY, inventorytimeskew, "super", fade*inventorytimeblend, "%s \fs%s%s\fS", name, col, timestr(timecorrected, inventorytimestyle));
                         }
+                        else if(timelimit) cm += drawitemtextx(cx[i], cm, 0, TEXT_RIGHT_JUSTIFY, inventorytimeskew, "super", fade*inventorytimeblend, "\fs%s%s\fS", timecorrected > 60000 ? "\fg" : "\fy", timestr(timecorrected, inventorytimestyle));
                     }
-                    else if(!interm && !m_edit(game::gamemode) && inventoryscore && ((cc = drawscore(cx[i], cm, cs, (h-edge*2)/2, fade)) > 0)) cm += cc+cr;
-
-                    if((cc = drawselection(cx[i], cy[i], cs, cm, fade)) > 0) cy[i] -= cc+cr;
-                    if(inventorygame)
-                    {
-                        if(m_capture(game::gamemode) && ((cc = capture::drawinventory(cx[i], cy[i], cs, cm, fade)) > 0)) cy[i] -= cc+cr;
-                        else if(m_defend(game::gamemode) && ((cc = defend::drawinventory(cx[i], cy[i], cs, cm, fade)) > 0)) cy[i] -= cc+cr;
-                        else if(m_bomber(game::gamemode) && ((cc = bomber::drawinventory(cx[i], cy[i], cs, cm, fade)) > 0)) cy[i] -= cc+cr;
-                    }
+                }
+                if(texpaneltimer) break;
+                if(m_fight(game::gamemode))
+                {
+                    int count = game::player1->state == CS_SPECTATOR ? inventoryscorespec : inventoryscore;
+                    if(count && ((cc = drawscore(cx[i], cm, csr, (h-edge*2)/2, fade, count)) > 0)) cm += cc+cr;
+                }
+                if((cc = drawselection(cx[i], cy[i], csr, cm, fade)) > 0) cy[i] -= cc+cr;
+                if(inventorygame)
+                {
+                    if(m_capture(game::gamemode) && ((cc = capture::drawinventory(cx[i], cy[i], csr, cm, fade)) > 0)) cy[i] -= cc+cr;
+                    else if(m_defend(game::gamemode) && ((cc = defend::drawinventory(cx[i], cy[i], csr, cm, fade)) > 0)) cy[i] -= cc+cr;
+                    else if(m_bomber(game::gamemode) && ((cc = bomber::drawinventory(cx[i], cy[i], csr, cm, fade)) > 0)) cy[i] -= cc+cr;
                 }
                 break;
             }
+            default: break;
         }
+        return left;
     }
 
-    void drawdamage(int w, int h, int s, float blend)
+    void drawdamage(int w, int h, int top, int bottom, float blend)
     {
-        float pc = game::focus->state == CS_DEAD ? 0.5f : (game::focus->state == CS_ALIVE ? min(damageresidue, 100)/100.f : 0.f);
-        if(pc > 0)
+        if(*damagetex)
         {
-            Texture *t = *damagetex ? textureload(damagetex, 3) : notexture;
-            if(t != notexture)
+            float pc = game::focus->state == CS_DEAD ? 0.5f : (game::focus->state == CS_ALIVE ? min(damageresidue, 100)/100.f : 0.f);
+            if(pc > 0)
             {
-                glBindTexture(GL_TEXTURE_2D, t->id);
-                glColor4f(0.85f, 0.09f, 0.09f, pc*blend*damageblend);
-                drawtexture(0, 0, w, h);
+                Texture *t = textureload(damagetex, 3);
+                if(t && t != notexture)
+                {
+                    glBindTexture(GL_TEXTURE_2D, t->id);
+                    glColor4f(0.85f, 0.09f, 0.09f, pc*blend*damageblend);
+                    drawtexture(0, top, w, h-top-bottom);
+                }
             }
         }
     }
 
-    void drawfire(int w, int h, int s, float blend)
+    void drawfire(int w, int h, int top, int bottom, float blend)
     {
-        if(game::focus->burning(lastmillis, burntime))
+        if(*burntex && game::focus->burning(lastmillis, burntime))
         {
-            int interval = lastmillis-game::focus->lastres[WR_BURN];
-            Texture *t = *burntex ? textureload(burntex, 3) : notexture;
-            if(t != notexture)
+            Texture *t = textureload(burntex, 3);
+            if(t && t != notexture)
             {
+                int interval = lastmillis-game::focus->lastres[WR_BURN];
                 float pc = interval >= burntime-500 ? 1.f+(interval-(burntime-500))/500.f : (interval%burndelay)/float(burndelay/2); if(pc > 1.f) pc = 2.f-pc;
                 glBindTexture(GL_TEXTURE_2D, t->id);
                 glColor4f(0.9f*max(pc,0.5f), 0.3f*pc, 0.0625f*max(pc,0.25f), blend*burnblend*(interval >= burntime-(burndelay/2) ? pc : min(pc+0.5f, 1.f)));
-                drawtexture(0, 0, w, h);
+                drawtexture(0, top, w, h-top-bottom);
             }
         }
     }
@@ -2782,54 +3038,125 @@ namespace hud
         drawtexture(x, y, c, c);
     }
 
-    void drawbackground(int w, int h)
+    void drawspecborder(int w, int h, int type, int &top, int &bottom)
     {
-        Texture *t = textureload(bgtex, 3);
+        if(type < 0 || type >= BORDER_MAX) return;
+        int btype[BORDER_MAX] = { playborder, editborder, specborder, waitborder, backgroundborder };
+        if(!btype[type]) return;
+        int bcolour[BORDER_MAX] = { playbordertone, editbordertone, specbordertone, waitbordertone, backgroundbordertone };
+        float bsize[BORDER_MAX] = { playbordersize, editbordersize, specbordersize, waitbordersize, backgroundbordersize },
+              bfade[BORDER_MAX] = { playborderblend, editborderblend, specborderblend, waitborderblend, backgroundborderblend };
+        int s = int(h*0.5f*bsize[type]);
+        if(!s) return;
+        vec col = vec(0, 0, 0);
+        if(bcolour[type]) skewcolour(col.x, col.y, col.z, bcolour[type]);
+        glColor4f(col.r, col.g, col.b, bfade[type]);
+        loopi(BORDERP_MAX) if(btype[type]&(1<<i))
+        {
+            const char *bptex = i ? borderbottomtex : bordertoptex;
+            if(*bptex)
+            {
+                Texture *t = textureload(bptex, 3);
+                glBindTexture(GL_TEXTURE_2D, t->id);
+                float tw = t->w*(s/float(t->h));
+                int cw = int(ceilf(w/tw));
+                loopk(cw) switch(i)
+                {
+                    case BORDERP_TOP: drawtexture(k*tw, 0, tw, s); break;
+                    case BORDERP_BOTTOM: drawtexture(k*tw, h-s, tw, s); break;
+                    default: break;
+                }
+            }
+            else
+            {
+                usetexturing(false);
+                switch(i)
+                {
+                    case BORDERP_TOP: drawblend(0, 0, w, s, col.r, col.g, col.b, true); break;
+                    case BORDERP_BOTTOM: drawblend(0, h-s, w, s, col.r, col.g, col.b, true); break;
+                    default: break;
+                }
+                usetexturing(true);
+            }
+            switch(i)
+            {
+                case BORDERP_TOP: top += s; break;
+                case BORDERP_BOTTOM: bottom += s; break;
+                default: break;
+            }
+        }
+    }
+
+    void drawbackground(int w, int h, int &top, int &bottom)
+    {
+        glColor4f(1, 1, 1, 1);
+
+        Texture *t = NULL;
+        int mapbg = 0;
+        if(showloadingmapbg && *mapname && strcmp(mapname, "maps/untitled"))
+        {
+            defformatstring(tex)("<blur:2>%s", mapname);
+            t = textureload(tex, 3, true, false);
+            mapbg = showloadingmapbg;
+        }
+        if(!t || t == notexture)
+        {
+            t = textureload(backgroundtex, 3);
+            mapbg = 0;
+        }
         glBindTexture(GL_TEXTURE_2D, t->id);
-        glBegin(GL_TRIANGLE_STRIP);
-        glTexCoord2f(0, 0); glVertex2f(0, 0);
-        glTexCoord2f(1, 0); glVertex2f(w, 0);
-        glTexCoord2f(0, 1); glVertex2f(0, h);
-        glTexCoord2f(1, 1); glVertex2f(w, h);
-        glEnd();
+        float offsetx = 0, offsety = 0;
+        if(showloadingaspect&(1<<mapbg))
+        {
+            if(w > h) offsety = ((w-h)/float(w))*0.5f;
+            else if(h > w) offsetx = ((h-w)/float(h))*0.5f;
+        }
+        drawquad(0, 0, w, h, offsetx, offsety, 1-offsetx, 1-offsety);
+
+        drawspecborder(w, h, BORDER_BG, top, bottom);
+
+        glColor4f(1, 1, 1, 1);
 
         t = textureload(logotex, 3);
         glBindTexture(GL_TEXTURE_2D, t->id);
-        glBegin(GL_TRIANGLE_STRIP);
-        glTexCoord2f(0, 0); glVertex2f(w-1024, 0);
-        glTexCoord2f(1, 0); glVertex2f(w, 0);
-        glTexCoord2f(0, 1); glVertex2f(w-1024, 256);
-        glTexCoord2f(1, 1); glVertex2f(w, 256);
-        glEnd();
+        drawtexture(w-1024, top, 1024, 256);
 
         t = textureload(badgetex, 3);
         glBindTexture(GL_TEXTURE_2D, t->id);
-        glBegin(GL_TRIANGLE_STRIP); // goes off the edge on purpose
-        glTexCoord2f(0, 0); glVertex2f(w-336, 0);
-        glTexCoord2f(1, 0); glVertex2f(w-80, 0);
-        glTexCoord2f(0, 1); glVertex2f(w-336, 128);
-        glTexCoord2f(1, 1); glVertex2f(w-80, 128);
-        glEnd();
-        if(!forceprogress)
+        drawtexture(w-336, top, 256, 128);
+
+        pushfont("console");
+        int y = h-bottom-FONTH/2;
+        bool p = progressing;
+        const char *ptitle = progresstitle, *ptext = progresstext;
+        float pamt = progressamt, ppart = progresspart;
+        if(!p)
         {
-            pushfont("console");
-            int y = h-FONTH/2;
-            if(progressing)
+            int wait = client::waiting();
+            if(wait > 1)
             {
-                if(progressamt > 0) drawprogress(FONTH, y, 0, progressamt, FONTH*2, true, 1, 1, 1, 1, 1, "consub", "\fy%d%%", int(progressamt*100));
-                else drawprogress(FONTH, y, 0, progressamt, FONTH*2, true, 1, 1, 1, 1, 1, "consub", "\fg...");
-                y -= FONTH/2;
-                if(*progresstext) y -= draw_textx("%s %s [\fs\fa%d%%\fS]", FONTH*7/2, y, 255, 255, 255, 255, TEXT_LEFT_UP, -1, -1, *progresstitle ? progresstitle : "please wait...", progresstext, int(progresspart*100));
-                else y -= draw_textx("%s", FONTH*7/2, y, 255, 255, 255, 255, TEXT_LEFT_UP, -1, -1, *progresstitle ? progresstitle : "please wait...");
+                p = true;
+                ptitle = wait == 2 ? "requesting map.." : "downloading map..";
+                pamt = ppart = 0;
+                ptext = "";
             }
-            y = h-FONTH/2;
-            y -= draw_textx("v%s-%s %d bit (%s)", w-FONTH, y, 255, 255, 255, 255, TEXT_RIGHT_UP, -1, -1, versionstring, CUR_PLATFORM, CUR_ARCH, versionrelease);
-            y -= draw_textx("%s", w-FONTH, y, 255, 255, 255, 255, TEXT_RIGHT_UP, -1, -1, versionurl);
-            popfont();
         }
+        if(p)
+        {
+            if(pamt > 0) drawprogress(FONTH, y, 0, pamt, FONTH*2, true, 1, 1, 1, 1, 1, "consub", "\fy%d%%", int(pamt*100));
+            else drawprogress(FONTH, y, 0, pamt, FONTH*2, true, 1, 1, 1, 1, 1, "consub", "\fg...");
+            y -= FONTH/2;
+            if(*ptext) y -= draw_textx("%s %s [\fs\fa%d%%\fS]", FONTH*7/2, y, 255, 255, 255, 255, TEXT_LEFT_UP, -1, -1, *ptitle ? ptitle : "please wait...", ptext, int(ppart*100));
+            else y -= draw_textx("%s", FONTH*7/2, y, 255, 255, 255, 255, TEXT_LEFT_UP, -1, -1, *ptitle ? ptitle : "please wait...");
+        }
+        y = h-bottom-FONTH/2;
+        if(showloadinggpu) y -= draw_textx("%s (%s v%s)", w-FONTH, y, 255, 255, 255, 255, TEXT_RIGHT_UP, -1, -1, gfxrenderer, gfxvendor, gfxversion);
+        if(showloadingversion) y -= draw_textx("%s v%s-%s%d (%s)", w-FONTH, y, 255, 255, 255, 255, TEXT_RIGHT_UP, -1, -1, VERSION_NAME, VERSION_STRING, versionplatname, versionarch, VERSION_RELEASE);
+        if(showloadingurl && *VERSION_URL) y -= draw_textx("%s", w-FONTH, y, 255, 255, 255, 255, TEXT_RIGHT_UP, -1, -1, VERSION_URL);
+        popfont();
     }
 
-    void drawheadsup(int w, int h, float fade, int os, int is, int br, int bs, int bx, int by)
+    int drawheadsup(int w, int h, int edge, int &top, int &bottom, float fade)
     {
         if(underlaydisplay >= 2 || (game::focus->state == CS_ALIVE && (underlaydisplay || !game::thirdpersonview(true))))
         {
@@ -2838,19 +3165,19 @@ namespace hud
             {
                 glBindTexture(GL_TEXTURE_2D, t->id);
                 glColor4f(1.f, 1.f, 1.f, underlayblend*hudblend);
-                drawtexture(0, 0, w, h);
+                drawtexture(0, top, w, h-top-bottom);
             }
         }
-        if(!game::intermission)
+        if(gs_playing(game::gamestate))
         {
             bool third = game::thirdpersonview(true) && game::focus != game::player1;
-            if(game::focus->state == CS_ALIVE && game::inzoom() && W(game::focus->weapselect, zooms)) drawzoom(w, h);
+            if(game::focus->state == CS_ALIVE && game::inzoom()) drawzoom(w, h);
             if(showdamage && !third)
             {
-                if(burntime && game::focus->state == CS_ALIVE) drawfire(w, h, os, fade);
-                if(game::bloodscale > 0) drawdamage(w, h, os, fade);
+                if(burntime && game::focus->state == CS_ALIVE) drawfire(w, h, top, bottom, fade);
+                drawdamage(w, h, top, bottom, fade);
             }
-            if(teamhurttime && m_team(game::gamemode, game::mutators) && game::focus == game::player1 && game::player1->lastteamhit >= 0 && lastmillis-game::player1->lastteamhit <= teamhurttime)
+            if(teamhurthud&2 && teamhurttime && m_team(game::gamemode, game::mutators) && game::focus == game::player1 && game::player1->lastteamhit >= 0 && lastmillis-game::player1->lastteamhit <= teamhurttime)
             {
                 vec targ;
                 bool hasbound = false;
@@ -2875,96 +3202,42 @@ namespace hud
                     }
                 }
             }
-            if(!hasinput(true) && (game::focus->state == CS_EDITING ? showeditradar >= 1 : chkcond(showradar, !game::tvmode() || (game::focus != game::player1 && radarstyle==3))))
+            if(!radardisabled && !m_hard(game::gamemode, game::mutators) && !hasinput(true) && (game::focus->state == CS_EDITING ? showeditradar >= 1 : chkcond(showradar, !game::tvmode() || (game::focus != game::player1 && radartype() == 3))))
                 drawradar(w, h, fade);
         }
-        if(minimal(showinventory, true)) drawinventory(w, h, os, fade);
-
-        if(!texpaneltimer)
-        {
-            int bf = int(255*fade*statblend);
-            bx -= os;
-            if(totalmillis-laststats >= statrate)
-            {
-                memcpy(prevstats, curstats, sizeof(prevstats));
-                laststats = totalmillis-(totalmillis%statrate);
-            }
-            int nextstats[NUMSTATS] = {
-                vtris*100/max(wtris, 1), vverts*100/max(wverts, 1), xtraverts/1024, xtravertsva/1024, glde, gbatches, getnumqueries(), rplanes, curfps, bestfpsdiff, worstfpsdiff
-            };
-            loopi(NUMSTATS) if(prevstats[i] == curstats[i]) curstats[i] = nextstats[i];
-            pushfont("consub");
-            if(showfps)
-            {
-                pushfont("console");
-                draw_textx("%d", w-br/2, by-FONTH*2, 255, 255, 255, bf, TEXT_CENTERED, -1, bs, curstats[8]);
-                draw_textx("fps", w-br/2, by-FONTH, 255, 255, 255, bf, TEXT_CENTERED, -1, -1);
-                popfont();
-                switch(showfps)
-                {
-                    case 3:
-                        by -= draw_textx("max:%d range:+%d-%d", bx, by, 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, maxfps, curstats[9], curstats[10]);
-                        break;
-                    case 2:
-                        by -= draw_textx("max:%d", bx, by, 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, maxfps);
-                        break;
-                    default: break;
-                }
-            }
-            if(showstats >= (m_edit(game::gamemode) ? 1 : 2))
-            {
-                by -= draw_textx("ond:%d va:%d gl:%d(%d) oq:%d", bx, by, 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, allocnodes*8, allocva, curstats[4], curstats[5], curstats[6]);
-                by -= draw_textx("wtr:%dk(%d%%) wvt:%dk(%d%%) evt:%dk eva:%dk", bx, by, 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, wtris/1024, curstats[0], wverts/1024, curstats[1], curstats[2], curstats[3]);
-                by -= draw_textx("ents:%d(%d) wp:%d lm:%d rp:%d pvs:%d", bx, by, 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs, entities::ents.length(), entgroup.length(), ai::waypoints.length(), lightmaps.length(), curstats[7], getnumviewcells());
-                if(game::focus->state == CS_EDITING)
-                {
-                    by -= draw_textx("cube:%s%d corner:%d orient:%d grid:%d%s", bx, by, 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs,
-                            selchildcount<0 ? "1/" : "", abs(selchildcount), sel.corner, sel.orient, sel.grid, showmat && selchildmat > 0 ? getmaterialdesc(selchildmat, " mat:") : "");
-                    by -= draw_textx("sel:%d,%d,%d %d,%d,%d (%d,%d,%d,%d)", bx, by, 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs,
-                            sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z,
-                                sel.cx, sel.cxs, sel.cy, sel.cys);
-                    by -= draw_textx("pos:%d,%d,%d yaw:%d pitch:%d", bx, by, 255, 255, 255, bf, TEXT_RIGHT_UP, -1, bs,
-                            (int)game::focus->o.x, (int)game::focus->o.y, (int)game::focus->o.z,
-                            (int)game::focus->yaw, (int)game::focus->pitch);
-                }
-            }
-            popfont();
-        }
+        drawspecborder(w, h, !gs_playing(game::gamestate) || game::player1->state == CS_SPECTATOR ? BORDER_SPEC : (game::player1->state == CS_WAITING ? BORDER_WAIT : (game::player1->state == CS_WAITING ? BORDER_EDIT : BORDER_PLAY)), top, bottom);
+        return drawinventory(w, h, edge, top, bottom, fade);
     }
 
     void drawevents(float blend)
     {
-        int to = 0;
-        if(game::focus->state == CS_ALIVE && m_fight(game::gamemode) && shownotices && game::focus == game::player1 && teamnotices)
+        int ty = int((hudheight/2)-(hudheight/2*eventoffset)), tx = int(hudwidth/2);
+        if(hasteaminfo(game::focus))
         {
+            ty /= noticescale;
+            tx /= noticescale;
             glPushMatrix();
             glScalef(noticescale, noticescale, 1);
             pushfont("huge");
-            int ty = int(((hudheight/2)-int(hudheight/2*eventoffset))/noticescale), tx = int((hudwidth/2)/noticescale),
-                tf = int(255*hudblend*noticeblend), tr = 255, tg = 255, tb = 255,
-                tw = int((hudwidth-(int(hudsize*gapsize)*2+int(hudsize*inventorysize)*2))/noticescale);
+            const char *col = teamnotices >= 2 ? "\fs\fzyS" : "";
+            int tf = int(255*hudblend*noticeblend), tr = 255, tg = 255, tb = 255,
+                tw = int((hudwidth-(int(hudsize*edgesize)*2+int(hudsize*inventoryleft)+(hudsize*inventoryright)))/noticescale);
             if(noticestone) skewcolour(tr, tg, tb, noticestone);
-            if(teamkillwarn && m_team(game::gamemode, game::mutators) && numteamkills() >= teamkillwarn)
-                to += draw_textx("\fzryDo NOT shoot team mates", tx, ty-to, tr, tg, tb, tf, TEXT_CENTERED, -1, -1);
-            if(teamnotices >= 2)
-            {
-                if(game::focus->state == CS_ALIVE && !lastteam) lastteam = totalmillis;
-                if(totalmillis-lastteam <= teamnoticedelay)
-                {
-                    if(m_trial(game::gamemode)) to += draw_textx("Time Trial", tx, ty-to, tr, tg, tb, tf, TEXT_CENTERED, -1, -1);
-                    else if(!m_team(game::gamemode, game::mutators)) to += draw_textx("Free-for-all %s", tx, ty-to, tr, tg, tb, tf, TEXT_CENTERED, -1, -1, m_bomber(game::gamemode) ? "Bomber-ball" : "Deathmatch");
-                    else to += draw_textx("You are on team %s", tx, ty-to, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, game::colourteam(game::focus->team));
-                }
-            }
+            if(m_race(game::gamemode)) ty -= draw_textx("%sRace", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, -1, col);
+            else if(!m_team(game::gamemode, game::mutators)) ty -= draw_textx("%sFree-for-all %s", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, -1, col, m_bomber(game::gamemode) ? "Bomber-ball" : "Deathmatch");
+            else ty -= draw_textx("%sYou are on team %s", tx, ty, tr, tg, tb, tf, TEXT_CENTERED, -1, tw, col, game::colourteam(game::focus->team));
             popfont();
             glPopMatrix();
+            ty *= noticescale;
+            tx *= noticescale;
         }
         if(showevents && game::focus->state != CS_EDITING && game::focus->state != CS_SPECTATOR)
         {
+            ty /= eventscale;
+            tx /= eventscale;
             glPushMatrix();
             glScalef(eventscale, eventscale, 1);
             pushfont("emphasis");
-            int ty = int(((hudheight/2)-int(hudheight/2*eventoffset)-(to*noticescale))/eventscale), tx = int((hudwidth/2)/eventscale);
             loopv(game::focus->icons)
             {
                 if(game::focus->icons[i].type == eventicon::AFFINITY && !(showevents&2)) continue;
@@ -2995,6 +3268,8 @@ namespace hud
             }
             popfont();
             glPopMatrix();
+            //ty *= eventscale;
+            //tx *= eventscale; // don't really care
         }
     }
 
@@ -3073,24 +3348,25 @@ namespace hud
             }
         }
 
-        int gap = int(hudsize*gapsize), inv = int(hudsize*inventorysize), br = inv+gap*2, bs = (hudwidth-br*2)/2, bx = hudwidth-br, by = hudheight-gap;
+        int edge = int(hudsize*edgesize), left = 0, top = 0, bottom = 0;
         glEnable(GL_BLEND);
         glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
         glColor3f(1, 1, 1);
 
-        if(noview) drawbackground(hudwidth, hudheight);
-        else if(!client::waiting() && showhud && fade > 0)
+        if(noview) drawbackground(hudwidth, hudheight, top, bottom);
+        else if(!client::waiting())
         {
-            drawheadsup(hudwidth, hudheight, fade, gap, inv, br, bs, bx, by);
-            if(!texpaneltimer && !game::tvmode() && !client::waiting() && !hasinput(false)) drawevents(fade);
-        }
-        if(UI::ready && showconsole && showhud)
-        {
-            drawconsole(showconsole < 2 || noview ? 0 : 1, hudwidth, hudheight, gap, gap, hudwidth-gap*2, consolefade);
-            if(showconsole >= 2 && !noview)
-                drawconsole(2, hudwidth, hudheight, br+gap*2, by, showfps >= 2 || showstats >= (m_edit(game::gamemode) ? 1 : 2) ? bs-gap*4 : (bs-gap*4)*2, consolefade);
+            if(showhud)
+            {
+                left += drawheadsup(hudwidth, hudheight, edge, top, bottom, fade);
+                if(!texpaneltimer && !game::tvmode() && !client::waiting() && !hasinput(false)) drawevents(fade);
+            }
+            else if(gs_playing(game::gamestate) && game::focus == game::player1 && game::focus->state == CS_ALIVE && game::inzoom())
+                drawzoom(hudwidth, hudheight);
         }
-
+        drawconsole(showconsole < 2 || noview ? 0 : 1, hudwidth, hudheight, edge*2, edge+top, hudwidth-edge*2, consolefade);
+        if(showconsole >= 2 && !noview && showconsole && showhud)
+            drawconsole(2, hudwidth, hudheight, left, hudheight-edge-bottom, showfps >= 2 || showstats >= (m_edit(game::gamemode) ? 1 : 2) ? (hudwidth-left*2)/2-edge*4 : ((hudwidth-left*2)/2-edge*4)*2, consolefade);
         glDisable(GL_BLEND);
     }
 
@@ -3109,24 +3385,12 @@ namespace hud
             hudheight = int(ceil(hudsize/aspect));
         }
         else hudwidth = hudheight = hudsize;
-        if(!hasinput(true))
-        {
-            int wait = client::waiting();
-            if(wait > 1)
-            {
-                forceprogress = progressing = true;
-                setfvar("progressamt", 0);
-                switch(wait)
-                {
-                    case 3: setsvar("progresstitle", "downloading map.."); break;
-                    case 2: setsvar("progresstitle", "requesting map.."); break;
-                    case 1: case 0: default: break;
-                }
-                setsvar("progresstext", "this could take some time..");
-                setfvar("progresspart", 0);
-            }
-            else if(forceprogress) forceprogress = progressing = false;
-        }
-        else if(forceprogress) forceprogress = progressing = false;
+    }
+
+    void cleanup()
+    {
+        teamkills.shrink(0);
+        damagelocs.shrink(0);
+        damageresidue = lastteam = 0;
     }
 }
diff --git a/src/game/physics.cpp b/src/game/physics.cpp
index d50bfa3..41a5da0 100644
--- a/src/game/physics.cpp
+++ b/src/game/physics.cpp
@@ -13,21 +13,26 @@ namespace physics
     VAR(IDF_PERSIST, physframetime, 5, 5, 20);
     VAR(IDF_PERSIST, physinterp, 0, 1, 1);
 
-    FVAR(IDF_PERSIST, impulsekick, 0, 150, 180); // determines the minimum angle to switch between wall kick and run
+    FVAR(IDF_PERSIST, impulsekick, 0, 150, 180); // determines the minimum yaw angle to switch between wall kick and run
     VAR(IDF_PERSIST, impulsemethod, 0, 3, 3); // determines which impulse method to use, 0 = none, 1 = power jump, 2 = power slide, 3 = both
     VAR(IDF_PERSIST, impulseaction, 0, 3, 3); // determines how impulse action works, 0 = off, 1 = impulse jump, 2 = impulse dash, 3 = both
     FVAR(IDF_PERSIST, impulseroll, 0, 15, 89);
 
+    VAR(IDF_PERSIST, jumpstyle, 0, 1, 1); // 0 = unpressed on action, 1 = remains pressed
     VAR(IDF_PERSIST, dashstyle, 0, 1, 1); // 0 = only with impulse, 1 = double tap
     VAR(IDF_PERSIST, crouchstyle, 0, 0, 2); // 0 = press and hold, 1 = double-tap toggle, 2 = toggle
-    VAR(IDF_PERSIST, pacingstyle, 0, 3, 5); // 0 = press and hold, 1 = double-tap toggle, 2 = toggle, 3-5 = same, but auto engage if impulsepacing == 0
+    VAR(IDF_PERSIST, walkstyle, 0, 0, 2); // 0 = press and hold, 1 = double-tap toggle, 2 = toggle
+    VAR(IDF_PERSIST, kickoffstyle, 0, 1, 1); // 0 = old method 1 = new method
+    FVAR(IDF_PERSIST, kickoffangle, 0, 60, 89);
+    VAR(IDF_PERSIST, kickupstyle, 0, 1, 1); // 0 = old method 1 = new method
+    FVAR(IDF_PERSIST, kickupangle, 0, 89, 89);
 
-    int physsteps = 0, lastphysframe = 0, lastmove = 0, lastdirmove = 0, laststrafe = 0, lastdirstrafe = 0, lastcrouch = 0, lastpacing = 0;
+    int physsteps = 0, lastphysframe = 0, lastmove = 0, lastdirmove = 0, laststrafe = 0, lastdirstrafe = 0, lastcrouch = 0, lastwalk = 0;
 
     bool allowimpulse(physent *d, int type)
     {
         if(d && gameent::is(d))
-            return (type ? impulseallowed&type : impulseallowed != 0) && (impulsestyle || m_jet(game::gamemode, game::mutators));
+            return (type ? impulseallowed&type : impulseallowed != 0) && (impulsestyle || PHYS(gravity) == 0);
         return false;
     }
 
@@ -37,24 +42,19 @@ namespace physics
         {
             gameent *e = (gameent *)d;
             if(!kick && impulsestyle == 1 && e->impulse[IM_TYPE] > IM_T_NONE && e->impulse[IM_TYPE] < IM_T_WALL) return false;
-            if(e->impulse[IM_TIME] && lastmillis-e->impulse[IM_TIME] <= impulsedelay) return false;
-            if(impulsestyle <= 2 && e->impulse[IM_COUNT] >= impulsecount) return false;
+            int time = e->impulse[IM_TIME], delay = e->impulse[IM_TYPE] == IM_T_KICK || e->impulse[IM_TYPE] == IM_T_VAULT ? impulsekickdelay : impulseboostdelay;
+            if(!time)
+            {
+                time = e->impulse[IM_JUMP];
+                delay = impulsejumpdelay;
+            }
+            if(time && delay && lastmillis-time <= delay) return false;
+            if(!m_freestyle(game::gamemode, game::mutators) && impulsestyle <= 2 && e->impulse[IM_COUNT] >= impulsecount) return false;
             return true;
         }
         return false;
     }
 
-    bool canjet(physent *d)
-    {
-        if((gameent::is(d)) && d->state == CS_ALIVE && m_jet(game::gamemode, game::mutators))
-        {
-            gameent *e = (gameent *)d;
-            if(e->physstate == PHYS_FALL && !e->onladder && (!e->impulse[IM_TIME] || lastmillis-e->impulse[IM_TIME] > jetdelay))
-                return true;
-        }
-        return false;
-    }
-
     #define imov(name,v,u,d,s,os) \
         void do##name(bool down) \
         { \
@@ -90,7 +90,7 @@ namespace physics
                 switch(type)
                 {
                     case AC_CROUCH: style = crouchstyle; last = &lastcrouch; break;
-                    case AC_PACING: style = pacingstyle%3; last = &lastpacing; break;
+                    case AC_WALK: style = walkstyle; last = &lastwalk; break;
                     default: break;
                 }
                 switch(style)
@@ -137,13 +137,13 @@ namespace physics
             }
         }
     }
-    //ICOMMAND(0, action, "iD", (int *i, int *n), doaction(*i, *n!=0)); // deprecated
+
     ICOMMAND(0, primary, "D", (int *n), doaction(AC_PRIMARY, *n!=0));
     ICOMMAND(0, secondary, "D", (int *n), doaction(AC_SECONDARY, *n!=0));
     ICOMMAND(0, reload, "D", (int *n), doaction(AC_RELOAD, *n!=0));
     ICOMMAND(0, use, "D", (int *n), doaction(AC_USE, *n!=0));
     ICOMMAND(0, jump, "D", (int *n), doaction(AC_JUMP, *n!=0));
-    ICOMMAND(0, pacing, "D", (int *n), doaction(AC_PACING, *n!=0));
+    ICOMMAND(0, walk, "D", (int *n), doaction(AC_WALK, *n!=0));
     ICOMMAND(0, crouch, "D", (int *n), doaction(AC_CROUCH, *n!=0));
     ICOMMAND(0, special, "D", (int *n), doaction(AC_SPECIAL, *n!=0));
     ICOMMAND(0, drop, "D", (int *n), doaction(AC_DROP, *n!=0));
@@ -166,33 +166,23 @@ namespace physics
     bool secondaryweap(gameent *d, bool zoom)
     {
         if(!isweap(d->weapselect)) return false;
-        if(W(d->weapselect, zooms)) { if(d == game::player1 && game::zooming && game::inzoomswitch()) return true; }
-        else if(!zoom)
+        if(d->weapselect != W_MELEE || (d->physstate == PHYS_FALL && !d->onladder))
         {
-            if(d->weapselect != W_MELEE || (d->physstate == PHYS_FALL && !d->onladder))
-            {
-                if(d->action[AC_SECONDARY] && (!d->action[AC_PRIMARY] || d->actiontime[AC_SECONDARY] > d->actiontime[AC_PRIMARY])) return true;
-                else if(d->actiontime[AC_SECONDARY] > d->actiontime[AC_PRIMARY] && W2(d->weapselect, power, true) && d->weapstate[d->weapselect] == W_S_POWER) return true;
-            }
+            if(d->action[AC_SECONDARY] && (!d->action[AC_PRIMARY] || d->actiontime[AC_SECONDARY] > d->actiontime[AC_PRIMARY])) return true;
+            else if(d->actiontime[AC_SECONDARY] > d->actiontime[AC_PRIMARY] && d->weapstate[d->weapselect] == W_S_POWER) return true;
         }
         return false;
     }
 
     bool isghost(gameent *d, gameent *e)
     {
-        if(d != e)
+        if(d != e && d->actortype < A_ENEMY && (!e || e->actortype < A_ENEMY))
         {
-            if(d->aitype >= AI_START && e && e->aitype >= AI_START && ai::owner(d) == ai::owner(e)) return true;
-            switch(m_ghost(game::gamemode))
+            if(m_ghost(game::gamemode, game::mutators)) return true;
+            if(m_team(game::gamemode, game::mutators)) switch(G(damageteam))
             {
-                case 2: if(e && ai::owner(d) == ai::owner(e)) return true; break;
-                case 1: return true; break;
-                case 0: default: break;
-            }
-            if(m_team(game::gamemode, game::mutators)) switch(G(teamdamage))
-            {
-                case 1: if(d->aitype > AI_NONE || (e && e->aitype == AI_NONE)) break;
-                case 0: if(e && ai::owner(d) == ai::owner(e)) return true; break;
+                case 1: if(d->actortype > A_PLAYER || (e && e->actortype == A_PLAYER)) break;
+                case 0: if(e && d->team == e->team) return true; break;
                 case 2: default: break;
             }
         }
@@ -233,49 +223,6 @@ namespace physics
         return false;
     }
 
-    bool iscrouching(physent *d)
-    {
-        if(d->state == CS_ALIVE && (gameent::is(d)))
-        {
-            gameent *e = (gameent *)d;
-            return e->action[AC_CROUCH] || e->actiontime[AC_CROUCH] < 0 || lastmillis-e->actiontime[AC_CROUCH] <= PHYSMILLIS;
-        }
-        return false;
-    }
-
-    bool jetpack(physent *d)
-    {
-        if(canjet(d))
-        {
-            gameent *e = (gameent *)d;
-            if(e->action[AC_JUMP])
-            {
-                e->impulse[IM_JET] = lastmillis;
-                return true;
-            }
-        }
-        return false;
-    }
-
-    bool pacing(physent *d, bool turn)
-    {
-        if(allowimpulse(d, IM_A_SPRINT) && (gameent::is(d)) && d->state == CS_ALIVE && movepacing > 0)
-        {
-            gameent *e = (gameent *)d;
-            if(!iscrouching(e) && (e != game::player1 || !W(e->weapselect, zooms) || !game::inzoom()))
-            {
-                if(turn && e->turnside) return true;
-                if((e != game::player1 && !e->ai) || !impulsemeter || e->impulse[IM_METER] < impulsemeter)
-                {
-                    bool value = e->action[AC_PACING];
-                    if(d == game::player1 && pacingstyle >= 3 && impulsepacing == 0) value = !value;
-                    if(value && (e->move || e->strafe)) return true;
-                }
-            }
-        }
-        return false;
-    }
-
     bool liquidcheck(physent *d) { return d->inliquid && !d->onladder && d->submerged >= PHYS(liquidsubmerge); }
 
     float liquidmerge(physent *d, float from, float to)
@@ -317,33 +264,18 @@ namespace physics
         return 1.f;
     }
 
-    bool sliding(physent *d, bool power)
+    bool sticktofloor(physent *d)
     {
-        if(gameent::is(d))
+        if(!d->onladder)
         {
-            gameent *e = (gameent *)d;
-            if((!power && e->turnside) || (impulseslip && e->impulse[IM_SLIP] && lastmillis-e->impulse[IM_SLIP] <= impulseslip) || (impulseslide && e->impulse[IM_SLIDE] && lastmillis-e->impulse[IM_SLIDE] <= impulseslide))
+            if(liquidcheck(d)) return false;
+            if(gameent::is(d))
             {
-                if(!power || e->action[AC_CROUCH])
-                {
-                    if(power && impulseslide && impulseslip && e->move == 1 && e->impulse[IM_SLIP] > e->impulse[IM_SLIDE])
-                        e->impulse[IM_SLIDE] = e->impulse[IM_SLIP];
-                    return true;
-                }
+                gameent *e = (gameent *)d;
+                if(e->sliding()) return false;
             }
         }
-        return false;
-    }
-
-    bool sticktofloor(physent *d)
-    {
-        if(!d->onladder && !liquidcheck(d) && (gameent::is(d)) && PHYS(gravity) > 0)
-        {
-            gameent *e = (gameent *)d;
-            if((e->turnside || !e->action[AC_CROUCH]) && sliding(e)) return false;
-            return true;
-        }
-        return false;
+        return true;
     }
 
     bool sticktospecial(physent *d)
@@ -360,34 +292,37 @@ namespace physics
     float movevelocity(physent *d, bool floating)
     {
         physent *pl = d->type == ENT_CAMERA ? game::player1 : d;
-        float vel = pl->speed*pl->speedscale;
+        float vel = pl->speed;
         if(floating) vel *= floatspeed/100.0f;
-        else if(gameent::is(pl))
-        {
-            gameent *e = (gameent *)pl;
-            vel *= movespeed/100.f*(1.f-clamp(e->stunned(lastmillis), 0.f, 1.f));
-            if((!e->airmillis && !sliding(e) && iscrouching(e)) || (e == game::player1 && game::inzoom()))
-                vel *= movecrawl;
-            if(e->move >= 0) vel *= e->strafe ? movestrafe : movestraight;
-            switch(e->physstate)
-            {
-                case PHYS_FALL: if(PHYS(gravity) > 0) vel *= moveinair; break;
-                case PHYS_STEP_DOWN: vel *= movestepdown; break;
-                case PHYS_STEP_UP: vel *= movestepup; break;
-                default: break;
-            }
-            if(pacing(e, false)) vel *= movepacing;
-            if(jetpack(e)) vel *= movejet;
-            if(carryaffinity(e))
+        else
+        {
+            vel *= pl->speedscale;
+            if(gameent::is(pl))
             {
-                if(m_capture(game::gamemode)) vel *= capturecarryspeed;
-                else if(m_bomber(game::gamemode)) vel *= bombercarryspeed;
+                gameent *e = (gameent *)pl;
+                vel *= movespeed/100.f*(1.f-clamp(e->stunned(lastmillis), 0.f, 1.f));
+                if((d->physstate >= PHYS_SLOPE || d->onladder) && !e->sliding() && e->crouching()) vel *= movecrawl;
+                else if(isweap(e->weapselect) && e->weapstate[e->weapselect] == W_S_ZOOM) vel *= movecrawl;
+                if(e->move >= 0) vel *= e->strafe ? movestrafe : movestraight;
+                if(e->running()) vel *= moverun;
+                switch(e->physstate)
+                {
+                    case PHYS_FALL: if(PHYS(gravity) > 0) vel *= moveinair; break;
+                    case PHYS_STEP_DOWN: vel *= movestepdown; break;
+                    case PHYS_STEP_UP: vel *= movestepup; break;
+                    default: break;
+                }
+                if(carryaffinity(e))
+                {
+                    if(m_capture(game::gamemode)) vel *= capturecarryspeed;
+                    else if(m_bomber(game::gamemode)) vel *= bombercarryspeed;
+                }
             }
         }
         return vel;
     }
 
-    float impulsevelocity(physent *d, float amt, int &cost, int type)
+    float impulsevelocity(physent *d, float amt, int &cost, int type, float redir, vec &keep)
     {
         float scale = d->speedscale;
         if(gameent::is(d))
@@ -399,7 +334,7 @@ namespace physics
                 if(m_capture(game::gamemode)) scale *= capturecarryspeed;
                 else if(m_bomber(game::gamemode)) scale *= bombercarryspeed;
             }
-            if(impulsemeter)
+            if(m_impulsemeter(game::gamemode, game::mutators))
             {
                 if(impulsecostscale) cost = int(cost*scale);
                 int diff = impulsemeter-e->impulse[IM_METER];
@@ -415,7 +350,8 @@ namespace physics
             }
             else cost = 0;
         }
-        float speed = (impulsespeed*amt*scale)+vec(d->vel).add(d->falling).magnitude();
+        float speed = (impulsespeed*amt*scale)+(keep.magnitude()*redir);
+        keep.mul(1-min(redir, 1.f));
         if(impulselimit > 0) return min(speed, impulselimit*scale);
         return speed;
     }
@@ -423,7 +359,7 @@ namespace physics
     bool movepitch(physent *d)
     {
         if(d->type == ENT_CAMERA || d->state == CS_EDITING || d->state == CS_SPECTATOR) return true;
-        if(d->onladder || (d->inliquid && (liquidcheck(d) || d->pitch < 0.f)) || jetpack(d) || PHYS(gravity) == 0) return true;
+        if(d->onladder || (d->inliquid && (liquidcheck(d) || d->pitch < 0.f)) || PHYS(gravity) == 0) return true;
         return false;
     }
 
@@ -483,10 +419,10 @@ namespace physics
             checkdir.mul(0.1f);
             checkdir.z += maxstep + 0.1f;
             d->o.add(checkdir);
-            if(!collide(d))
+            if(collide(d))
             {
                 d->o = old;
-                if(collide(d, vec(0, 0, -1), slopez)) return false;
+                if(!collide(d, vec(0, 0, -1), slopez)) return false;
                 cansmooth = false;
             }
         }
@@ -499,21 +435,21 @@ namespace physics
             d->o = old;
             d->o.add(checkdir);
             int scale = 2;
-            if(!collide(d, checkdir))
+            if(collide(d, checkdir))
             {
-                if(collide(d, vec(0, 0, -1), slopez))
+                if(!collide(d, vec(0, 0, -1), slopez))
                 {
                     d->o = old;
                     return false;
                 }
                 d->o.add(checkdir);
-                if(!collide(d, vec(0, 0, -1), slopez)) scale = 1;
+                if(collide(d, vec(0, 0, -1), slopez)) scale = 1;
             }
             if(scale != 1)
             {
                 d->o = old;
                 d->o.sub(checkdir.mul(vec(2, 2, 1)));
-                if(collide(d, vec(0, 0, -1), slopez)) scale = 1;
+                if(!collide(d, vec(0, 0, -1), slopez)) scale = 1;
             }
 
             d->o = old;
@@ -531,7 +467,7 @@ namespace physics
                 d->o.add(smoothdir.mul(force));
                 float margin = (maxstep + 0.1f)*ceil(force);
                 d->o.z += margin;
-                if(collide(d, smoothdir))
+                if(!collide(d, smoothdir))
                 {
                     d->o.z -= margin;
                     if(d->physstate == PHYS_FALL || d->floor != floor)
@@ -549,7 +485,7 @@ namespace physics
         /* try stepping up */
         d->o = old;
         d->o.z += dir.magnitude()*force;
-        if(collide(d, vec(0, 0, 1)))
+        if(!collide(d, vec(0, 0, 1)))
         {
             if(d->physstate == PHYS_FALL || d->floor != floor)
             {
@@ -574,12 +510,12 @@ namespace physics
         vec old(d->o);
         d->o.add(vec(stepdir).mul(stairheight/fabs(stepdir.z))).z -= stairheight;
         d->zmargin = -stairheight;
-        if(!collide(d, vec(0, 0, -1), slopez))
+        if(collide(d, vec(0, 0, -1), slopez))
         {
             d->o = old;
             d->o.add(vec(stepdir).mul(step));
             d->zmargin = 0;
-            if(collide(d, vec(0, 0, -1)))
+            if(!collide(d, vec(0, 0, -1)))
             {
                 vec stepfloor(stepdir);
                 stepfloor.mul(-stepfloor.z).z += 1;
@@ -590,10 +526,10 @@ namespace physics
                     vec stepped(d->o);
                     d->o.z -= 0.5f;
                     d->zmargin = -0.5f;
-                    if(!collide(d, stepdir) && wall == d->floor)
+                    if(collide(d, stepdir) && collidewall == d->floor)
                     {
                         d->o = old;
-                        if(!init) { d->o.x += dir.x; d->o.y += dir.y; if(dir.z <= 0 || !collide(d, dir)) d->o.z += dir.z; }
+                        if(!init) { d->o.x += dir.x; d->o.y += dir.y; if(dir.z <= 0 || collide(d, dir)) d->o.z += dir.z; }
                         d->zmargin = 0;
                         d->physstate = PHYS_STEP_DOWN;
                         return true;
@@ -623,7 +559,7 @@ namespace physics
         vec old(d->o);
         d->o.z -= stairheight;
         d->zmargin = -stairheight;
-        if(collide(d, vec(0, 0, -1), slopez))
+        if(!collide(d, vec(0, 0, -1), slopez))
         {
             d->o = old;
             d->zmargin = 0;
@@ -676,9 +612,9 @@ namespace physics
             floor = vec(0, 0, 1);
             found = true;
         }
-        else if(d->physstate != PHYS_FALL && !collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? slopez : floorz))
+        else if(d->physstate != PHYS_FALL && collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? slopez : floorz))
         {
-            floor = wall;
+            floor = collidewall;
             found = true;
         }
         else if(collided && obstacle.z >= slopez)
@@ -689,17 +625,17 @@ namespace physics
         }
         else if(d->physstate == PHYS_STEP_UP || d->physstate == PHYS_SLIDE)
         {
-            if(!collide(d, vec(0, 0, -1)) && wall.z > 0.0f)
+            if(collide(d, vec(0, 0, -1)) && collidewall.z > 0.0f)
             {
-                floor = wall;
+                floor = collidewall;
                 if(floor.z >= slopez) found = true;
             }
         }
         else if(d->physstate >= PHYS_SLOPE && d->floor.z < 1.0f)
         {
-            if(!collide(d, vec(d->floor).neg(), 0.95f) || !collide(d, vec(0, 0, -1)))
+            if(collide(d, vec(d->floor).neg(), 0.95f) || !collide(d, vec(0, 0, -1)))
             {
-                floor = wall;
+                floor = collidewall;
                 if(floor.z >= slopez && floor.z < 1.0f) found = true;
             }
         }
@@ -716,24 +652,24 @@ namespace physics
     {
         vec old(d->o), obstacle; d->o.add(dir);
         bool collided = false, slidecollide = false;
-        if(!collide(d, dir))
+        if(collide(d, dir))
         {
-            obstacle = wall;
+            obstacle = collidewall;
             /* check to see if there is an obstacle that would prevent this one from being used as a floor */
-            if((gameent::is(d)) && ((wall.z>=slopez && dir.z<0) || (wall.z<=-slopez && dir.z>0)) && (dir.x || dir.y) && !collide(d, vec(dir.x, dir.y, 0)))
+            if((gameent::is(d)) && ((collidewall.z>=slopez && dir.z<0) || (collidewall.z<=-slopez && dir.z>0)) && (dir.x || dir.y) && collide(d, vec(dir.x, dir.y, 0)))
             {
-                if(wall.dot(dir) >= 0) slidecollide = true;
-                obstacle = wall;
+                if(collidewall.dot(dir) >= 0) slidecollide = true;
+                obstacle = collidewall;
             }
 
             d->o = old;
             d->o.z -= stairheight;
             d->zmargin = -stairheight;
-            if(d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR  || (!collide(d, vec(0, 0, -1), slopez) && (d->physstate == PHYS_STEP_UP || d->physstate == PHYS_STEP_DOWN || wall.z >= floorz)))
+            if(d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR  || (collide(d, vec(0, 0, -1), slopez) && (d->physstate == PHYS_STEP_UP || d->physstate == PHYS_STEP_DOWN || collidewall.z >= floorz)))
             {
                 d->o = old;
                 d->zmargin = 0;
-                if(trystepup(d, dir, obstacle, stairheight, d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR ? d->floor : vec(wall)))
+                if(trystepup(d, dir, obstacle, stairheight, d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR ? d->floor : vec(collidewall)))
                     return true;
             }
             else
@@ -745,10 +681,10 @@ namespace physics
         }
         else if(d->physstate == PHYS_STEP_UP)
         {
-            if(!collide(d, vec(0, 0, -1), slopez))
+            if(collide(d, vec(0, 0, -1), slopez))
             {
                 d->o = old;
-                if(trystepup(d, dir, vec(0, 0, 1), stairheight, vec(wall))) return true;
+                if(trystepup(d, dir, vec(0, 0, 1), stairheight, vec(collidewall))) return true;
                 d->o.add(dir);
             }
         }
@@ -771,33 +707,22 @@ namespace physics
         return !collided;
     }
 
-    bool canregenimpulse(gameent *d)
+    bool impulseplayer(gameent *d, bool &onfloor, bool melee = false)
     {
-        if(impulseregen > 0 && (!impulseregendelay || lastmillis-d->impulse[IM_REGEN] >= impulseregendelay))
-        {
-            if(impulseregenjetdelay && d->impulse[IM_JET] && (impulseregenjetdelay < 0 || lastmillis-d->impulse[IM_JET] < impulseregenjetdelay))
-                return false;
-            return true;
-        }
-        return false;
-    }
-
-    bool impulseplayer(gameent *d, bool &onfloor, bool &jetting, bool melee = false)
-    {
-        bool power = !melee && onfloor && !jetting && impulsemethod&1 && sliding(d, true) && d->action[AC_JUMP];
-        if(power || d->ai || impulseaction || melee)
+        bool power = !melee && onfloor && impulsemethod&1 && d->sliding(true) && d->action[AC_JUMP];
+        if(power || d->actortype >= A_BOT || impulseaction || melee)
         {
             bool dash = false, pulse = false;
             if(melee) { dash = onfloor; pulse = !onfloor; }
             else if(!power)
             {
                 if(onfloor) dash = impulseaction&2 && d->action[AC_DASH] && (!d->impulse[IM_TIME] || lastmillis-d->impulse[IM_TIME] > impulsedashdelay);
-                else pulse = ((d->ai || impulseaction&1) && d->action[AC_JUMP]) || ((d->ai || impulseaction&2) && d->action[AC_DASH]);
+                else pulse = ((d->actortype >= A_BOT || impulseaction&1) && d->action[AC_JUMP]) || ((d->actortype >= A_BOT || impulseaction&2) && d->action[AC_DASH]);
             }
             if(!canimpulse(d, dash ? IM_A_DASH : IM_A_BOOST, false)) return false;
             if(power || dash || pulse)
             {
-                bool mchk = !melee || onfloor, action = mchk && (d->ai || melee || impulseaction&2);
+                bool mchk = !melee || onfloor, action = mchk && (d->actortype >= A_BOT || melee || impulseaction&2);
                 int move = action ? d->move : 0, strafe = action ? d->strafe : 0;
                 bool moving = mchk && (move || strafe);
                 float skew = power ? impulsepower : (moving ? impulseboost : impulsejump);
@@ -807,7 +732,8 @@ namespace physics
                     if(!dash && !melee) d->impulse[IM_JUMP] = lastmillis;
                 }
                 int cost = impulsecost;
-                float force = impulsevelocity(d, skew, cost, melee ? IM_A_PARKOUR : (dash ? IM_A_DASH : IM_A_BOOST));
+                vec keepvel = vec(d->vel).add(d->falling);
+                float force = impulsevelocity(d, skew, cost, melee ? IM_A_PARKOUR : (dash ? IM_A_DASH : IM_A_BOOST), melee ? impulsemeleeredir : (dash ? impulsedashredir : (moving ? impulseboostredir : impulsejumpredir)), keepvel);
                 if(force > 0)
                 {
                     vec dir(0, 0, 1);
@@ -821,10 +747,10 @@ namespace physics
                             if(dir.z < 0) force += -dir.z*force;
                         }
                     }
-                    (d->vel = dir.normalize()).mul(force);
+                    d->vel = vec(dir).mul(force).add(keepvel);
                     if(power) d->vel.z += jumpvel(d, true);
                     d->doimpulse(cost, melee ? IM_T_MELEE : (dash ? IM_T_DASH : IM_T_BOOST), lastmillis);
-                    if(!m_jet(game::gamemode, game::mutators)) d->action[AC_JUMP] = false;
+                    d->action[AC_JUMP] = false;
                     if(power || pulse) onfloor = false;
                     client::addmsg(N_SPHY, "ri2", d->clientnum, melee ? SPHY_MELEE : (dash ? SPHY_DASH : SPHY_BOOST));
                     game::impulseeffect(d);
@@ -837,100 +763,39 @@ namespace physics
 
     void modifyinput(gameent *d, vec &m, bool wantsmove, int millis)
     {
-        bool onfloor = d->physstate >= PHYS_SLOPE || d->onladder || liquidcheck(d), jetting = jetpack(d);
-        if(impulsemeter && millis)
-        {
-            #define impchk (!impulsemeter || d->impulse[IM_METER]+len <= impulsemeter)
-            bool quickpace = pacing(d, false);
-            if(quickpace && impulsepacing > 0)
-            {
-                int len = int(ceilf(millis*impulsepacing));
-                if(len > 0 && impchk)
-                {
-                    d->impulse[IM_METER] += len;
-                    d->impulse[IM_REGEN] = lastmillis;
-                }
-                else quickpace = d->action[AC_PACING] = false;
-            }
-            if(jetting)
-            {
-                if(m_jet(game::gamemode, game::mutators) && impulsejet > 0)
-                {
-                    int len = int(ceilf(millis*impulsejet));
-                    if(len > 0 && impchk)
-                    {
-                        d->impulse[IM_METER] += len;
-                        d->impulse[IM_REGEN] = lastmillis;
-                    }
-                    else jetting = d->action[AC_JUMP] = false;
-                }
-                else jetting = d->action[AC_JUMP] = false;
-            }
-            if(d->impulse[IM_METER] > 0 && canregenimpulse(d))
-            {
-                bool collect = true; // collect time until it is able to act upon it
-                int timeslice = int((millis+d->impulse[IM_COLLECT])*impulseregen);
-                #define impulsemod(x,y) \
-                    if(collect && (x)) \
-                    { \
-                        if(y > 0) { if(timeslice > 0) timeslice = int(timeslice*y); } \
-                        else collect = false; \
-                    }
-                impulsemod(m_jet(game::gamemode, game::mutators), impulseregenjet);
-                impulsemod(quickpace, impulseregenpacing);
-                impulsemod(d->move || d->strafe, impulseregenmove);
-                impulsemod((!onfloor && PHYS(gravity) > 0) || sliding(d), impulseregeninair);
-                impulsemod(onfloor && iscrouching(d) && !sliding(d), impulseregencrouch);
-                impulsemod(sliding(d), impulseregenslide);
-                if(collect)
-                {
-                    if(timeslice > 0)
-                    {
-                        if((d->impulse[IM_METER] -= timeslice) < 0) d->impulse[IM_METER] = 0;
-                        d->impulse[IM_COLLECT] = 0;
-                    }
-                    else d->impulse[IM_COLLECT] += millis;
-                }
-            }
-        }
-
-        if(m_jet(game::gamemode, game::mutators) && jetting)
-        {
-            if(d->o.z >= hdr.worldsize) m.z = min(m.z, 0-(millis/jetdecay));
-            else if(jetheight > 0)
-            {
-                vec v(0, 0, -1);
-                float ray = raycube(d->o, v, hdr.worldsize), floor = ray < hdr.worldsize ? d->o.z-ray : 0.f-jetheight;
-                if(d->o.z-floor >= jetheight) m.z = min(m.z, 0-(millis/jetdecay));
-            }
-        }
-
+        bool onfloor = d->physstate >= PHYS_SLOPE || d->onladder || liquidcheck(d);
         if(d->turnside && (!allowimpulse(d, IM_A_PARKOUR) || d->impulse[IM_TYPE] != IM_T_SKATE || (impulseskate && lastmillis-d->impulse[IM_TIME] > impulseskate) || d->vel.magnitude() <= 1))
+        {
             d->turnside = 0;
-
+            d->resetphys(true);
+            onfloor = false;
+        }
         if(d->turnside)
         {
             if(d->action[AC_JUMP] && canimpulse(d, IM_A_PARKOUR, true))
             {
                 int cost = impulsecost;
-                float mag = impulsevelocity(d, impulseparkourkick, cost, IM_A_PARKOUR);
+                vec keepvel = vec(d->vel).add(d->falling);
+                float mag = impulsevelocity(d, impulseparkourkick, cost, IM_A_PARKOUR, impulseparkourkickredir, keepvel);
                 if(mag > 0)
                 {
-                    vec rft; vecfromyawpitch(d->yaw, 0, 1, 0, rft);
-                    (d->vel = rft.normalize()).mul(mag); d->vel.z += mag/2;
+                    vec rft;
+                    vecfromyawpitch(d->yaw, d->actortype >= A_BOT || !kickoffstyle ? kickoffangle : d->pitch, 1, 0, rft);
+                    d->vel = vec(rft).mul(mag).add(keepvel);
                     d->doimpulse(cost, IM_T_KICK, lastmillis);
                     d->turnmillis = PHYSMILLIS;
                     d->turnside = 0; d->turnyaw = d->turnroll = 0;
                     d->action[AC_JUMP] = onfloor = false;
                     client::addmsg(N_SPHY, "ri2", d->clientnum, SPHY_KICK);
                     game::impulseeffect(d);
+                    game::footstep(d);
                 }
             }
         }
         else
         {
-            impulseplayer(d, onfloor, jetting);
-            if(onfloor && d->action[AC_JUMP])// && (d->ai || !(impulsemethod&1) || !d->action[AC_CROUCH]))
+            impulseplayer(d, onfloor);
+            if(onfloor && d->action[AC_JUMP])
             {
                 float force = jumpvel(d, true);
                 if(force > 0)
@@ -944,25 +809,23 @@ namespace physics
                     }
                     d->resetphys();
                     d->impulse[IM_JUMP] = lastmillis;
-                    if(m_jet(game::gamemode, game::mutators) && !allowimpulse(d, IM_A_BOOST)) d->doimpulse(0, IM_T_BOOST, lastmillis);
                     d->action[AC_JUMP] = onfloor = false;
                     client::addmsg(N_SPHY, "ri2", d->clientnum, SPHY_JUMP);
                     playsound(S_JUMP, d->o, d);
                     regularshape(PART_SMOKE, int(d->radius), 0x222222, 21, 20, 250, d->feetpos(), 1, 1, -10, 0, 10.f);
                 }
             }
-            if(d->hasmelee(lastmillis, true, sliding(d, true), onfloor))
+            if(d->hasmelee(lastmillis, true, d->sliding(true), onfloor))
             {
-                vec oldpos = d->o, dir;
-                vecfromyawpitch(d->yaw, 0, 1, 0, dir);
-                d->o.add(dir.normalize());
+                vec oldpos = d->o, dir(d->yaw*RAD, 0.f);
+                d->o.add(dir);
                 bool collided = collide(d, dir);
                 d->o = oldpos;
-                if(!collided && hitplayer && gameent::is(hitplayer))
+                if(collided && hitplayer && gameent::is(hitplayer))
                 {
-                    d->action[AC_SPECIAL] = false;
+                    //d->action[AC_SPECIAL] = false;
                     d->resetjump();
-                    impulseplayer(d, onfloor, jetting, true);
+                    impulseplayer(d, onfloor, true);
                     if(d->turnside)
                     {
                         d->turnmillis = PHYSMILLIS;
@@ -981,28 +844,28 @@ namespace physics
         bool found = false;
         if(d->turnside || d->action[AC_SPECIAL])
         {
+            vec oldpos = d->o, dir;
             const int movements[6][2] = { { 2, 2 }, { 1, 2 }, { 1, -1 }, { 1, 1 }, { 0, 2 }, { -1, 2 } };
-            loopi(d->turnside ? 6 : 4)
+            loopi(d->turnside ? 6 : 4) // we do these insane checks so that running along walls works at all times
             {
-                vec oldpos = d->o, dir;
                 int move = movements[i][0], strafe = movements[i][1];
                 if(move == 2) move = d->move > 0 ? d->move : 0;
                 if(strafe == 2) strafe = d->turnside ? d->turnside : d->strafe;
                 if(!move && !strafe) continue;
                 vecfromyawpitch(d->yaw, 0, move, strafe, dir);
-                dir.normalize();
                 d->o.add(dir);
                 bool collided = collide(d, dir);
                 d->o = oldpos;
-                if(collided || hitplayer || wall.iszero()) continue;
-                vec face = vec(wall).normalize();
+                if(!collided || hitplayer || collidewall.iszero()) continue;
+                vec face = vec(collidewall).normalize();
                 if(fabs(face.z) <= impulseparkournorm)
                 {
                     bool cankick = d->action[AC_SPECIAL] && canimpulse(d, IM_A_PARKOUR, true), parkour = cankick && !onfloor && !d->onladder;
                     float yaw = 0, pitch = 0;
                     vectoyawpitch(face, yaw, pitch);
                     float off = yaw-d->yaw;
-                    if(off > 180) off -= 360; else if(off < -180) off += 360;
+                    if(off > 180) off -= 360;
+                    else if(off < -180) off += 360;
                     bool iskick = impulsekick > 0 && fabs(off) >= impulsekick, vault = false;
                     if(cankick && iskick)
                     {
@@ -1011,66 +874,73 @@ namespace physics
                         if(onfloor)
                         {
                             d->o.z += space*m;
-                            if(!collide(d, dir))
+                            if(collide(d, dir))
                             {
                                 d->o.z += space*n-space*m;
-                                if(collide(d, dir) || hitplayer) vault = true;
+                                if(!collide(d, dir) || hitplayer) vault = true;
                             }
                         }
                         else
                         {
                             d->o.z += space*n;
-                            if(collide(d, dir) || hitplayer) vault = true;
+                            if(!collide(d, dir) || hitplayer) vault = true;
                         }
                         d->o = oldpos;
                     }
                     if(!d->turnside && (parkour || vault) && iskick)
                     {
-                        if(!d->impulse[IM_TIME] || (d->impulse[IM_TYPE] != IM_T_KICK && d->impulse[IM_TYPE] != IM_T_VAULT) || lastmillis-d->impulse[IM_TIME] > impulsekickdelay)
+                        int cost = impulsecost;
+                        vec keepvel = vec(d->vel).add(d->falling);
+                        float mag = impulsevelocity(d, vault ? impulseparkourvault : impulseparkourclimb, cost, IM_A_PARKOUR, vault ? impulseparkourvaultredir : impulseparkourclimbredir, keepvel);
+                        if(mag > 0)
                         {
-                            int cost = impulsecost;
-                            float mag = impulsevelocity(d, vault ? impulseparkourvault : impulseparkourkick, cost, IM_A_PARKOUR);
-                            if(mag > 0)
-                            {
-                                vecfromyawpitch(d->yaw, vault ? 90.f : fabs(d->pitch), 1, 0, dir);
-                                (d->vel = dir.normalize()).reflect(face).normalize().mul(mag);
-                                d->doimpulse(cost, vault ? IM_T_VAULT : IM_T_KICK, lastmillis);
-                                d->turnmillis = PHYSMILLIS;
-                                d->turnside = 0; d->turnyaw = d->turnroll = 0;
-                                client::addmsg(N_SPHY, "ri2", d->clientnum, vault ? SPHY_VAULT : SPHY_KICK);
-                                game::impulseeffect(d);
-                            }
+                            vec rft;
+                            vecfromyawpitch(d->yaw, vault || d->actortype >= A_BOT || !kickupstyle ? kickupangle : fabs(d->pitch), 1, 0, rft);
+                            rft.reflect(face);
+                            d->vel = vec(rft).mul(mag).add(keepvel);
+                            d->doimpulse(cost, vault ? IM_T_VAULT : IM_T_KICK, lastmillis);
+                            d->turnmillis = PHYSMILLIS;
+                            d->turnside = 0;
+                            d->turnyaw = d->turnroll = 0;
+                            client::addmsg(N_SPHY, "ri2", d->clientnum, vault ? SPHY_VAULT : SPHY_KICK);
+                            game::impulseeffect(d);
+                            game::footstep(d);
                         }
                         break;
                     }
                     else if(d->turnside || parkour)
                     {
                         int side = off < 0 ? -1 : 1;
-                        if(off < 0) yaw += 90; else yaw -= 90;
+                        if(off < 0) yaw += 90;
+                        else yaw -= 90;
                         while(yaw >= 360) yaw -= 360;
                         while(yaw < 0) yaw += 360;
-                        vec rft; vecfromyawpitch(yaw, 0, 1, 0, rft);
+                        vec rft;
+                        vecfromyawpitch(yaw, 0.f, 1, 0, rft);
                         if(!d->turnside)
                         {
                             int cost = impulsecost;
-                            float mag = impulsevelocity(d, impulseparkour, cost, IM_A_PARKOUR);
+                            vec keepvel = vec(d->vel).add(d->falling);
+                            float mag = impulsevelocity(d, impulseparkour, cost, IM_A_PARKOUR, impulseparkourredir, keepvel);
                             if(mag > 0)
                             {
-                                (d->vel = rft.normalize()).mul(mag);
+                                d->vel = vec(rft).mul(mag).add(keepvel);
                                 off = yaw-d->yaw;
                                 if(off > 180) off -= 360;
                                 else if(off < -180) off += 360;
                                 d->doimpulse(cost, IM_T_SKATE, lastmillis);
                                 d->turnmillis = PHYSMILLIS;
-                                d->turnside = side; d->turnyaw = off;
+                                d->turnside = side;
+                                d->turnyaw = off;
                                 d->turnroll = (impulseroll*d->turnside)-d->roll;
                                 client::addmsg(N_SPHY, "ri2", d->clientnum, SPHY_SKATE);
                                 game::impulseeffect(d);
+                                game::footstep(d);
                                 found = true;
                             }
                             break;
                         }
-                        if(side == d->turnside)
+                        else if(side == d->turnside)
                         {
                             (m = rft).normalize(); // re-project and override
                             found = true;
@@ -1080,7 +950,8 @@ namespace physics
                 }
             }
         }
-        if(d->canmelee(m_weapon(game::gamemode, game::mutators), lastmillis, true, sliding(d, true), onfloor)) weapons::doshot(d, d->o, W_MELEE, true, true);
+        if(d->canmelee(m_weapon(game::gamemode, game::mutators), lastmillis, true, d->sliding(true), onfloor))
+            weapons::doshot(d, d->o, W_MELEE, true, true);
         if(!found && d->turnside) d->turnside = 0;
         d->action[AC_DASH] = false;
     }
@@ -1091,55 +962,47 @@ namespace physics
         { // move up or down slopes in air but only move up slopes in liquid
             float dz = -(m.x*d->floor.x + m.y*d->floor.y)/d->floor.z;
             m.z = liquidcheck(d) ? max(m.z, dz) : dz;
-            m.normalize();
+            if(!m.iszero()) m.normalize();
         }
         if(!d->turnside && (d->physstate >= PHYS_SLOPE || d->onladder || liquidcheck(d))) d->resetjump();
-        if(local)
-        {
-            if(game::allowmove(d)) modifyinput(d, m, wantsmove, millis);
-            else d->action[AC_JUMP] = d->action[AC_CROUCH] = false;
-        }
+        if(local && game::allowmove(d)) modifyinput(d, m, wantsmove, millis);
         if(d->physstate == PHYS_FALL && !d->onladder && !d->turnside)
         {
-            if(!d->airmillis) d->airmillis = NZT(lastmillis);
+            if(!d->airmillis) d->airmillis = lastmillis ? lastmillis : 1;
             d->floormillis = 0;
         }
         else
         {
             d->airmillis = 0;
-            if(!d->floormillis) d->floormillis = NZT(lastmillis);
+            if(!d->floormillis) d->floormillis = lastmillis ? lastmillis : 1;
         }
         if(!d->turnside)
         {
             if(d->onladder && !m.iszero()) m.add(vec(0, 0, m.z >= 0 ? 1 : -1)).normalize();
-            else if(jetpack(d) && m.iszero()) m = vec(0, 0, 1);
+            else if(PHYS(gravity) == 0 && m.iszero()) m = vec(0, 0, 1);
         }
     }
 
     float coastscale(const vec &o)
     {
-        return lookupvslot(lookupcube(int(o.x), int(o.y), int(o.z)).texture[0], false).coastscale;
+        return lookupvslot(lookupcube(int(o.x), int(o.y), int(o.z)).texture[O_TOP], false).coastscale;
     }
 
     void modifyvelocity(physent *pl, bool local, bool floating, int millis)
     {
         vec m(0, 0, 0);
         bool wantsmove = game::allowmove(pl) && (pl->move || pl->strafe);
-        if(wantsmove)
-        {
-            vecfromyawpitch(pl->yaw, movepitch(pl) ? pl->pitch : 0, pl->move, pl->strafe, m);
-            m.normalize();
-        }
+        if(wantsmove) vecfromyawpitch(pl->yaw, movepitch(pl) ? pl->pitch : 0, pl->move, pl->strafe, m);
         if(!floating && gameent::is(pl)) modifymovement((gameent *)pl, m, local, wantsmove, millis);
         else if(pl->physstate == PHYS_FALL && !pl->onladder)
         {
-            if(!pl->airmillis) pl->airmillis = NZT(lastmillis);
+            if(!pl->airmillis) pl->airmillis = lastmillis ? lastmillis : 1;
             pl->floormillis = 0;
         }
         else
         {
             pl->airmillis = 0;
-            if(!pl->floormillis) pl->floormillis = NZT(lastmillis);
+            if(!pl->floormillis) pl->floormillis = lastmillis ? lastmillis : 1;
         }
 
         m.mul(movevelocity(pl, floating));
@@ -1147,8 +1010,8 @@ namespace physics
         if(floating || pl->type == ENT_CAMERA) coast = floatcoast;
         else
         {
-            bool slide = gameent::is(pl) && sliding((gameent *)pl);
-            float c = pl->physstate >= PHYS_SLOPE || pl->onladder ? (slide ? PHYS(slidecoast) : PHYS(floorcoast))*coastscale(pl->feetpos(-2)) : PHYS(aircoast);
+            bool slide = gameent::is(pl) && ((gameent *)pl)->sliding();
+            float c = pl->physstate >= PHYS_SLOPE || pl->onladder ? (slide ? PHYS(slidecoast) : PHYS(floorcoast))*coastscale(pl->feetpos(-1)) : PHYS(aircoast);
             coast = pl->inliquid ? liquidmerge(pl, c, PHYS(liquidcoast)) : c;
         }
         pl->vel.lerp(m, pl->vel, pow(max(1.0f - 1.0f/coast, 0.0f), millis/20.0f));
@@ -1156,10 +1019,10 @@ namespace physics
 
     void modifygravity(physent *pl, int curtime)
     {
-        float secs = curtime/1000.0f;
-        vec g(0, 0, 0);
         if(PHYS(gravity) > 0)
         {
+            vec g(0, 0, 0);
+            float secs = curtime/1000.0f;
             if(pl->physstate == PHYS_FALL) g.z -= gravityvel(pl)*secs;
             else if(pl->floor.z > 0 && pl->floor.z < floorz)
             {
@@ -1168,15 +1031,17 @@ namespace physics
                 g.normalize();
                 g.mul(gravityvel(pl)*secs);
             }
-            if(!liquidcheck(pl) || (!pl->move && !pl->strafe)) pl->falling.add(g);
-        }
-        else pl->falling = g;
-        if(liquidcheck(pl) || pl->physstate >= PHYS_SLOPE)
-        {
-            float coast = liquidcheck(pl) ? liquidmerge(pl, PHYS(aircoast), PHYS(liquidcoast)) : PHYS(floorcoast)*coastscale(pl->feetpos(-2)),
-                  c = liquidcheck(pl) ? 1.0f : clamp((pl->floor.z - slopez)/(floorz-slopez), 0.0f, 1.0f);
-            pl->falling.mul(pow(max(1.0f - c/coast, 0.0f), curtime/20.0f));
+            bool liquid = liquidcheck(pl);
+            if(!liquid || (!pl->move && !pl->strafe) || (gameent::is(pl) && ((gameent *)pl)->crouching()))
+                pl->falling.add(g);
+            if(liquid || pl->physstate >= PHYS_SLOPE)
+            {
+                float coast = liquid ? liquidmerge(pl, PHYS(aircoast), PHYS(liquidcoast)) : PHYS(floorcoast)*coastscale(pl->feetpos(-1)),
+                      c = liquid ? 1.0f : clamp((pl->floor.z - slopez)/(floorz-slopez), 0.0f, 1.0f);
+                pl->falling.mul(pow(max(1.0f - c/coast, 0.0f), curtime/20.0f));
+            }
         }
+        else pl->falling = vec(0, 0, 0);
     }
 
     void updatematerial(physent *pl, const vec &center, const vec &bottom, bool local)
@@ -1227,7 +1092,7 @@ namespace physics
                     gameent *d = (gameent *)pl;
                     if(d->burning(lastmillis, burntime) && lastmillis-d->lastres[WR_BURN] > PHYSMILLIS)
                     {
-                        d->resetburning();
+                        d->resetresidual(WR_BURN);
                         playsound(S_EXTINGUISH, d->o, d);
                         part_create(PART_SMOKE, 500, center, 0xAAAAAA, radius*4, 1, -10);
                         if(d->state == CS_ALIVE) client::addmsg(N_SPHY, "ri2", d->clientnum, SPHY_EXTINGUISH);
@@ -1248,7 +1113,7 @@ namespace physics
 
     bool moveplayer(physent *pl, int moveres, bool local, int millis)
     {
-        bool floating = isfloating(pl), player = !floating && gameent::is(pl), jetting = false;
+        bool floating = isfloating(pl), player = !floating && gameent::is(pl);
         float secs = millis/1000.f;
 
         pl->blocked = false;
@@ -1256,14 +1121,12 @@ namespace physics
         {
             updatematerial(pl, pl->center(), pl->feetpos(), local);
             modifyvelocity(pl, local, false, millis);
-            jetting = jetpack(pl);
-            if(!sticktospecial(pl) && !pl->onladder && !jetting) modifygravity(pl, millis); // apply gravity
+            if(!sticktospecial(pl) && !pl->onladder) modifygravity(pl, millis); // apply gravity
             else pl->resetphys(false);
         }
         else
         {
-            pl->inliquid = 0;
-            pl->onladder = false;
+            pl->inliquid = pl->onladder = false;
             pl->submerged = 0;
             modifyvelocity(pl, local, floating, millis);
         }
@@ -1292,18 +1155,21 @@ namespace physics
             if(player)
             {
                 gameent *d = (gameent *)pl;
-                if(local && jetting && !jetpack(d)) d->action[AC_JUMP] = false;
                 if(!d->airmillis)
                 {
-                    if(local && impulsemethod&2 && timeinair >= impulsedelay && d->move == 1 && allowimpulse(d, IM_A_DASH) && d->action[AC_CROUCH])
+                    if(local && impulsemethod&2 && timeinair >= impulseboostdelay && d->move == 1 && allowimpulse(d, IM_A_DASH) && d->action[AC_CROUCH])
                     {
                         d->action[AC_DASH] = true;
                         d->actiontime[AC_DASH] = lastmillis;
                     }
-                    if(timeinair >= PHYSMILLIS*2 && mag >= 20)
+                    if(timeinair >= PHYSMILLIS)
                     {
-                        int vol = min(int(mag*1.25f), 255); if(d->inliquid) vol /= 2;
-                        playsound(S_LAND, d->o, d, 0, vol);
+                        if(mag >= 20)
+                        {
+                            int vol = min(int(mag*1.25f), 255); if(d->inliquid) vol *= 0.5f;
+                            playsound(S_LAND, d->o, d, 0, vol);
+                        }
+                        else game::footstep(d);
                     }
                 }
             }
@@ -1370,7 +1236,7 @@ namespace physics
         {
             vec oldpos(pl->o);
             pl->o.add(d);
-            if(!collide(pl, vec(0, 0, 0), 0, false))
+            if(collide(pl, vec(0, 0, 0), 0, false))
             {
                 pl->o = oldpos;
                 return false;
@@ -1444,27 +1310,27 @@ namespace physics
             case PHYS_FLOOR:
             case PHYS_STEP_DOWN:
                 d->o.z -= 0.15f;
-                if(!collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? slopez : floorz))
+                if(collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? slopez : floorz))
                 {
-                    d->floor = wall;
+                    d->floor = collidewall;
                     foundfloor = true;
                 }
                 break;
 
             case PHYS_STEP_UP:
                 d->o.z -= stairheight+0.15f;
-                if(!collide(d, vec(0, 0, -1), slopez))
+                if(collide(d, vec(0, 0, -1), slopez))
                 {
-                    d->floor = wall;
+                    d->floor = collidewall;
                     foundfloor = true;
                 }
                 break;
 
             case PHYS_SLIDE:
                 d->o.z -= 0.15f;
-                if(!collide(d, vec(0, 0, -1)) && wall.z < slopez)
+                if(collide(d, vec(0, 0, -1)) && collidewall.z < slopez)
                 {
-                    d->floor = wall;
+                    d->floor = collidewall;
                     foundfloor = true;
                 }
                 break;
@@ -1482,21 +1348,21 @@ namespace physics
             gameent *e = (gameent *)o;
             if(e->wantshitbox())
             {
-                if(!d->o.reject(e->legs, d->radius+max(e->lrad.x, e->lrad.y)) && !ellipsecollide(d, dir, e->legs, vec(0, 0, 0), e->yaw, e->lrad.x, e->lrad.y, e->lrad.z, e->lrad.z))
+                if(!d->o.reject(e->legs, d->radius+max(e->lrad.x, e->lrad.y)) && ellipsecollide(d, dir, e->legs, vec(0, 0, 0), e->yaw, e->lrad.x, e->lrad.y, e->lrad.z, e->lrad.z))
                     hitflags |= HITFLAG_LEGS;
-                if(!d->o.reject(e->torso, d->radius+max(e->trad.x, e->trad.y)) && !ellipsecollide(d, dir, e->torso, vec(0, 0, 0), e->yaw, e->trad.x, e->trad.y, e->trad.z, e->trad.z))
+                if(!d->o.reject(e->torso, d->radius+max(e->trad.x, e->trad.y)) && ellipsecollide(d, dir, e->torso, vec(0, 0, 0), e->yaw, e->trad.x, e->trad.y, e->trad.z, e->trad.z))
                     hitflags |= HITFLAG_TORSO;
-                if(!d->o.reject(e->head, d->radius+max(e->hrad.x, e->hrad.y)) && !ellipsecollide(d, dir, e->head, vec(0, 0, 0), e->yaw, e->hrad.x, e->hrad.y, e->hrad.z, e->hrad.z))
+                if(!d->o.reject(e->head, d->radius+max(e->hrad.x, e->hrad.y)) && ellipsecollide(d, dir, e->head, vec(0, 0, 0), e->yaw, e->hrad.x, e->hrad.y, e->hrad.z, e->hrad.z))
                     hitflags |= HITFLAG_HEAD;
-                return hitflags == HITFLAG_NONE;
+                return hitflags != HITFLAG_NONE;
             }
         }
-        if(!plcollide(d, dir, o))
+        if(plcollide(d, dir, o))
         {
             hitflags |= HITFLAG_TORSO;
-            return false;
+            return true;
         }
-        return true;
+        return false;
     }
 
     bool xtracecollide(physent *d, const vec &from, const vec &to, float x1, float x2, float y1, float y2, float maxdist, float &dist, physent *o)
@@ -1510,30 +1376,30 @@ namespace physics
                 float bestdist = 1e16f;
                 if(e->legs.x+e->lrad.x >= x1 && e->legs.y+e->lrad.y >= y1 && e->legs.x-e->lrad.x <= x2 && e->legs.y-e->lrad.y <= y2)
                 {
-                    vec bottom(e->legs), top(e->legs); bottom.z -= e->lrad.z; top.z += e->lrad.z; float d = 1e16f;
-                    if(linecylinderintersect(from, to, bottom, top, max(e->lrad.x, e->lrad.y), d)) { hitflags |= HITFLAG_LEGS; bestdist = min(bestdist, d); }
+                    vec bottom(e->legs), top(e->legs); bottom.z -= e->lrad.z; top.z += e->lrad.z; float t = 1e16f;
+                    if(linecylinderintersect(from, to, bottom, top, max(e->lrad.x, e->lrad.y), t)) { hitflags |= HITFLAG_LEGS; bestdist = min(bestdist, t); }
                 }
                 if(e->torso.x+e->trad.x >= x1 && e->torso.y+e->trad.y >= y1 && e->torso.x-e->trad.x <= x2 && e->torso.y-e->trad.y <= y2)
                 {
-                    vec bottom(e->torso), top(e->torso); bottom.z -= e->trad.z; top.z += e->trad.z; float d = 1e16f;
-                    if(linecylinderintersect(from, to, bottom, top, max(e->trad.x, e->trad.y), d)) { hitflags |= HITFLAG_TORSO; bestdist = min(bestdist, d); }
+                    vec bottom(e->torso), top(e->torso); bottom.z -= e->trad.z; top.z += e->trad.z; float t = 1e16f;
+                    if(linecylinderintersect(from, to, bottom, top, max(e->trad.x, e->trad.y), t)) { hitflags |= HITFLAG_TORSO; bestdist = min(bestdist, t); }
                 }
                 if(e->head.x+e->hrad.x >= x1 && e->head.y+e->hrad.y >= y1 && e->head.x-e->hrad.x <= x2 && e->head.y-e->hrad.y <= y2)
                 {
-                    vec bottom(e->head), top(e->head); bottom.z -= e->hrad.z; top.z += e->hrad.z; float d = 1e16f;
-                    if(linecylinderintersect(from, to, bottom, top, max(e->hrad.x, e->hrad.y), d)) { hitflags |= HITFLAG_HEAD; bestdist = min(bestdist, d); }
+                    vec bottom(e->head), top(e->head); bottom.z -= e->hrad.z; top.z += e->hrad.z; float t = 1e16f;
+                    if(linecylinderintersect(from, to, bottom, top, max(e->hrad.x, e->hrad.y), t)) { hitflags |= HITFLAG_HEAD; bestdist = min(bestdist, t); }
                 }
-                if(hitflags == HITFLAG_NONE) return true;
+                if(hitflags == HITFLAG_NONE) return false;
                 dist = bestdist*from.dist(to);
-                return false;
+                return true;
             }
         }
         if(o->o.x+o->radius >= x1 && o->o.y+o->radius >= y1 && o->o.x-o->radius <= x2 && o->o.y-o->radius <= y2 && intersect(o, from, to, dist))
         {
             hitflags |= HITFLAG_TORSO;
-            return false;
+            return true;
         }
-        return true;
+        return false;
     }
 
     void complexboundbox(physent *d)
@@ -1563,37 +1429,54 @@ namespace physics
             return insideworld(d->o);
         }
         vec orig = d->o;
+        float maxrad = max(d->radius, max(d->xradius, d->yradius));
+        #define doposchk \
+            if(insideworld(d->o) && !collide(d, vec(0, 0, 0), 0, avoidplayers)) \
+            { \
+                d->resetinterp(); \
+                return true; \
+            } \
+            else d->o = orig;
         #define inmapchk(x,y) \
-        { \
             loopi(x) \
             { \
-                if(i) { y; } \
-                if(insideworld(d->o) && collide(d) && !inside && (!avoidplayers || !hitplayer)) \
-                { \
-                    d->resetinterp(); \
-                    return true; \
-                } \
-                d->o = orig; \
-            } \
-        }
-        if((gameent::is(d)) && d->state == CS_ALIVE)
+                int n = i+1; \
+                y; \
+                doposchk; \
+            }
+        doposchk;
+        inmapchk(20, d->o.z += (d->height+d->aboveeye)*n/10.f);
+        if(gameent::is(d))
         {
-            vec dir;
-            vecfromyawpitch(d->yaw, d->pitch, 1, 0, dir);
-            if(!dir.iszero()) loopk(2) inmapchk(100, d->o.add(vec(dir).mul(i/20.f).mul(k ? 1 : -1)));
+            vec dir = vec(d->yaw, d->pitch).mul(maxrad);
+            if(!dir.iszero())
+            {
+                inmapchk(200, d->o.add(vec(dir).mul(n/10.f)));
+                inmapchk(50, d->o.add(vec(dir).mul(-n/10.f)));
+            }
+            dir = vec(d->vel).normalize().mul(maxrad);
+            if(!dir.iszero())
+            {
+                inmapchk(200, d->o.add(vec(dir).mul(n/10.f)));
+                inmapchk(50, d->o.add(vec(dir).mul(-n/10.f)));
+            }
+            inmapchk(50, d->o.add(vec((rnd(21)-10)/10.f, (rnd(21)-10)/10.f, (rnd(21)-10)/10.f).normalize().mul(maxrad).mul(vec(n/10.f, n/10.f, n/25.f))));
         }
-        if(gameent::is(d) || d->type == ENT_PROJ)
+        else
         {
-            vec dir = vec(d->vel).normalize();
-            if(!dir.iszero()) loopk(2) inmapchk(100, d->o.add(vec(dir).mul(i/20.f).mul(k ? 1 : -1)));
+            vec dir = vec(d->vel).normalize().mul(maxrad);
+            if(!dir.iszero())
+            {
+                inmapchk(100, d->o.add(vec(dir).mul(n/10.f)));
+                inmapchk(20, d->o.add(vec(dir).mul(-n/10.f)));
+            }
         }
-        inmapchk(100, d->o.add(vec((rnd(21)-10)*i/20.f, (rnd(21)-10)*i/20.f, (rnd(21)-10)*i/20.f)));
         d->o = orig;
         d->resetinterp();
         return false;
     }
 
-    VAR(IDF_PERSIST, smoothmove, 0, 90, 200);
+    VAR(IDF_PERSIST, smoothmove, 0, 100, 200);
     VAR(IDF_PERSIST, smoothdist, 0, 64, 1024);
 
     void predictplayer(gameent *d, bool domove, int res = 0, bool local = false)
@@ -1637,7 +1520,7 @@ namespace physics
         }
     }
 
-    bool droptofloor(vec &o, float radius, float height)
+    bool droptofloor(vec &o, int type, float radius, float height)
     {
         static struct dropent : physent
         {
@@ -1646,11 +1529,11 @@ namespace physics
                 physent::reset();
                 radius = xradius = yradius = height = aboveeye = 1;
                 type = ENT_DUMMY;
-                collidetype = COLLIDE_AABB;
                 vel = vec(0, 0, -1);
             }
         } d;
         d.o = o;
+        d.type = type;
         if(!insideworld(d.o))
         {
             if(d.o.z < hdr.worldsize) return false;
diff --git a/src/game/player.h b/src/game/player.h
index b2808c7..e89febf 100644
--- a/src/game/player.h
+++ b/src/game/player.h
@@ -1,3 +1,47 @@
+struct actors
+{
+    int type,           weap,           health;
+    float   xradius,    yradius,    height,     weight,     speed,      scale;
+    bool    canmove,    canstrafe,  canjump,    cancrouch,  useweap,    living,     hitbox;
+    const char  *name,      *playermodel[4];
+};
+
+enum { A_PLAYER = 0, A_BOT, A_TURRET, A_GRUNT, A_DRONE, A_MAX, A_ENEMY = A_TURRET, A_TOTAL = A_MAX-A_ENEMY };
+#ifdef GAMESERVER
+actors actor[] = {
+    {
+        A_PLAYER,         -1,             0,
+            3,          3,          14,         200,        50,         1,
+            true,       true,       true,       true,       true,       true,       true,
+                "player",   { "actors/player/male/hwep",      "actors/player/male",     "actors/player/male/body",      "actors/player/male/headless" }
+    },
+    {
+        A_BOT,         -1,             0,
+            3,          3,          14,         200,        50,         1,
+            true,       true,       true,       true,       true,       true,       true,
+                "bot",      { "actors/player/male/hwep",      "actors/player/male",     "actors/player/male/body",      "actors/player/male/headless" }
+    },
+    {
+        A_TURRET,      W_SMG,       100,
+            4.75,       4.75,       8.75,       150,        1,          1,
+            false,      false,      false,      false,      false,      false,      false,
+                "turret",   { "actors/player/male/hwep",      "actors/turret",          "actors/player/male/body",      "actors/turret" }
+    },
+    {
+        A_GRUNT,       W_PISTOL,   50,
+            3,          3,          14,         200,        50,         1,
+            true,       true,       true,       true,       true,       true,       true,
+                "grunt",   { "actors/player/male/hwep",      "actors/player/male",      "actors/player/male/body",      "actors/player/male/headless" }
+    },
+    {
+        A_DRONE,       W_MELEE,     50,
+            3,          3,          14,         150,        40,         1,
+            true,       true,       true,       true,       true,       true,       true,
+                "drone",    { "actors/player/male/hwep",      "actors/drone",           "actors/player/male/body",      "actors/drone" }
+    },
+};
+#endif
+
 enum
 {
     T_NEUTRAL = 0, T_ALPHA, T_OMEGA, T_KAPPA, T_SIGMA, T_ENEMY, T_MAX,
@@ -7,38 +51,40 @@ enum
     T_TOTAL = (T_MULTI-T_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, 0xF88820);
+enum
+{
+    TT_INFO = 1<<0, TT_RESET = 1<<1, TT_SMODE = 1<<2,
+    TT_INFOSM = TT_INFO|TT_SMODE,
+    TT_RESETX = TT_INFO|TT_RESET
+};
 
 #ifdef GAMESERVER
-enum { TT_INFO = 1<<0, TT_RESET = 1<<1, TT_SMODE = 1<<2, TT_DEFAULT = TT_RESET|TT_SMODE, TT_SMINFO = TT_INFO|TT_SMODE, TT_DFINFO = TT_INFO|TT_DEFAULT };
-
-#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 };
+const char *teamnames[T_MAX] = { "neutral", "alpha", "omega", "kappa", "sigma", "enemy" };
+int mapbals[T_TOTAL][T_TOTAL] = {
+    { T_ALPHA, T_OMEGA, T_KAPPA, T_SIGMA },
+    { T_OMEGA, T_ALPHA, T_SIGMA, T_KAPPA },
+    { T_KAPPA, T_SIGMA, T_ALPHA, T_OMEGA },
+    { T_SIGMA, T_KAPPA, T_OMEGA, T_ALPHA }
+};
 #else
-#define TEAMDEF(proto,name)     extern proto *team_stat_##name[];
-#endif
-#define TEAM(id,name)           (*team_stat_##name[id])
+extern const char *teamnames[T_MAX];
+extern int mapbals[T_TOTAL][T_TOTAL];
 #endif
-TEAMDEF(char *, name);
-TEAMDEF(int, colour);
+
+#include "teamdef.h"
+
+TPSVAR(0, name,
+    "neutral",  "alpha",    "omega",    "kappa",    "sigma",    "enemy"
+);
+TPVAR(IDF_HEX, colour, 0, 0xFFFFFF,
+    0x90A090,   0x5F66FF,   0xFF4F44,   0xFFD022,   0x22FF22,   0xB0B0B0
+);
 
 struct score
 {
     int team, total;
-    score() {}
-    score(int s, int n) : team(s), total(n) {}
+    score(int s = -1, int n = 0) : team(s), total(n) {}
+    ~score() {}
 };
 
 #define numteams(a,b)   (m_fight(a) && m_team(a,b) ? (m_multi(a,b) ? T_TOTAL : T_NUM) : 1)
@@ -46,77 +92,36 @@ struct score
 #define isteam(a,b,c,d) (m_fight(a) && m_team(a,b) ? (c >= d && c <= numteams(a,b)) : c == T_NEUTRAL)
 #define valteam(a,b)    (a >= b && a <= T_TOTAL)
 
+#define PLAYERTYPES 2
 #ifdef GAMESERVER
-const int mapbals[T_TOTAL][T_TOTAL] = {
-    { T_ALPHA, T_OMEGA, T_KAPPA, T_SIGMA },
-    { T_OMEGA, T_ALPHA, T_SIGMA, T_KAPPA },
-    { T_KAPPA, T_SIGMA, T_ALPHA, T_OMEGA },
-    { T_SIGMA, T_KAPPA, T_OMEGA, T_ALPHA }
+const char *playertypes[PLAYERTYPES][6] = {
+    { "actors/player/male/hwep",      "actors/player/male",     "actors/player/male/body",      "actors/player/male/headless",      "player",   "male" },
+    { "actors/player/female/hwep",    "actors/player/female",   "actors/player/male/body",      "actors/player/female/headless",    "player",   "female" }
 };
-#else
-extern const int mapbals[T_TOTAL][T_TOTAL];
-#endif
-
-#ifdef MEK
-#define PLAYERTYPES 4
-#ifdef GAMEWORLD
-const char *playertypes[PLAYERTYPES][5] = {
-    { "actors/mek1/hwep",    "actors/mek1",    "actors/mek1",   "mek1",   "light" },
-    { "actors/mek2/hwep",    "actors/mek2",    "actors/mek2",   "mek2",   "medium" },
-    { "actors/mek3/hwep",    "actors/mek3",    "actors/mek3",   "mek3",   "flyer" },
-    { "actors/mek4/hwep",    "actors/mek4",    "actors/mek4"    ,   "mek4",   "heavy" },
+float playerdims[PLAYERTYPES][3] = {
+    { 3,      3,      14 },
+    { 3,      3,      14 },
 };
 #else
-extern const char *playertypes[PLAYERTYPES][4]; //3
+extern const char *playertypes[PLAYERTYPES][6];
+extern float playerdims[PLAYERTYPES][3];
 #endif
 
-#define CLASSES(a,b1,b2,c1,c2,c3,c4,c5) \
-    GSVAR(0, class##a##name, #a); \
-    GVAR(0, class##a##health, 0, b1, VAR_MAX); \
-    GVAR(0, class##a##armour, 0, b2, VAR_MAX); \
-    GFVAR(0, class##a##xradius, 0, c1, FVAR_MAX); \
-    GFVAR(0, class##a##yradius, 0, c2, FVAR_MAX); \
-    GFVAR(0, class##a##height, 0, c3, FVAR_MAX); \
-    GFVAR(0, class##a##weight, 0, c4, FVAR_MAX); \
-    GFVAR(0, class##a##speed, 0, c5, FVAR_MAX);
+#include "playerdef.h"
 
-//      name    health  armour  xrad    yrad    height  weight      speed
-CLASSES(mek1,   300,    80,     6,      6,      16,     200,        80); // light
-CLASSES(mek2,   400,    100,    6,      6,      16,     300,        60); // medium
-CLASSES(mek3,   330,    90,     6,      6,      16,     250,        70); // flyer
-CLASSES(mek4,   500,    200,    6,      6,      16,     350,        40); // heavy
+PPVAR(0, health, 1, VAR_MAX,
+    100
+);
+PPFVAR(0, weight, 0, FVAR_MAX,
+    200
+);
+PPFVAR(0, scale, FVAR_NONZERO, FVAR_MAX,
+    1
+);
+PPFVAR(0, speed, FVAR_NONZERO, FVAR_MAX,
+    50
+);
 
-#ifdef GAMESERVER
-#define CLASSDEF(proto,name)     proto *sv_class_stat_##name[] = { &sv_classmek1##name, &sv_classmek2##name, &sv_classmek3##name, &sv_classmek4##name };
-#define CLASS(id,name)           (*sv_class_stat_##name[max(id,0)%PLAYERTYPES])
-#else
-#ifdef GAMEWORLD
-#define CLASSDEF(proto,name)     proto *class_stat_##name[] = { &classmek1##name, &classmek2##name, &classmek3##name, &classmek4##name };
-#else
-#define CLASSDEF(proto,name)     extern proto *class_stat_##name[];
-#endif
-#define CLASS(id,name)           (*class_stat_##name[max(id,0)%PLAYERTYPES])
-#endif
-CLASSDEF(char *, name);
-CLASSDEF(int, health);
-CLASSDEF(int, armour);
-CLASSDEF(float, xradius);
-CLASSDEF(float, yradius);
-CLASSDEF(float, height);
-CLASSDEF(float, weight);
-CLASSDEF(float, speed);
-#else // FPS
-#define PLAYERTYPES 2
-#ifdef GAMEWORLD
-const char *playertypes[PLAYERTYPES][5] = {
-    { "actors/player/male/hwep",      "actors/player/male",     "actors/player/male/body",      "actors/player/male/headless",      "male" },
-    { "actors/player/female/hwep",    "actors/player/female",   "actors/player/male/body",      "actors/player/female/headless",      "female" }
-};
-#else
-extern const char *playertypes[PLAYERTYPES][3];
-#endif
-#endif
-#ifdef VANITY
 #define VANITYMAX 16
 struct vanityfile
 {
@@ -167,4 +172,3 @@ vector<vanitys> vanities;
 #else
 extern vector<vanitys> vanities;
 #endif
-#endif
diff --git a/src/game/playerdef.h b/src/game/playerdef.h
new file mode 100644
index 0000000..eed521a
--- /dev/null
+++ b/src/game/playerdef.h
@@ -0,0 +1,58 @@
+#ifdef GAMESERVER
+    #define PPVAR(flags, name, mn, mx, w00) \
+        GVAR(flags, player##name, mn, w00, mx); \
+        int *sv_player_stat_##name[] = { \
+            &sv_player##name, \
+            &sv_player##name \
+        };
+
+    #define PPFVAR(flags, name, mn, mx, w00) \
+        GFVAR(flags, player##name, mn, w00, mx); \
+        float *sv_player_stat_##name[] = { \
+            &sv_player##name, \
+            &sv_player##name \
+        };
+
+    #define PPSVAR(flags, name, w00) \
+        GSVAR(flags, player##name, w00); \
+        char **sv_player_stat_##name[] = { \
+            &sv_player##name, \
+            &sv_player##name \
+        };
+
+    #define PLAYER(t,name)         (*sv_player_stat_##name[t])
+#else
+#ifdef GAMEWORLD
+    #define PPVAR(flags, name, mn, mx, w00) \
+        GVAR(flags, player##name, mn, w00, mx); \
+        int *player_stat_##name[] = { \
+            &player##name, \
+            &player##name \
+        };
+
+    #define PPFVAR(flags, name, mn, mx, w00) \
+        GFVAR(flags, player##name, mn, w00, mx); \
+        float *player_stat_##name[] = { \
+            &player##name, \
+            &player##name \
+        };
+
+    #define PPSVAR(flags, name, w00) \
+        GSVAR(flags, player##name, w00); \
+        char **player_stat_##name[] = { \
+            &player##name, \
+            &player##name \
+        };
+#else
+    #define PPVAR(flags, name, mn, mx, w00) \
+        GVAR(flags, player##name, mn, w00, mx); \
+        extern int *player_stat_##name[];
+    #define PPFVAR(flags, name, mn, mx, w00) \
+        GFVAR(flags, player##name, mn, w00, mx); \
+        extern float *player_stat_##name[];
+    #define PPSVAR(flags, name, w00) \
+        GSVAR(flags, player##name, w00); \
+        extern char **player_stat_##name[];
+#endif
+    #define PLAYER(t,name)         (*player_stat_##name[t%PLAYERTYPES])
+#endif
diff --git a/src/game/projs.cpp b/src/game/projs.cpp
index 4362662..f83bb7d 100644
--- a/src/game/projs.cpp
+++ b/src/game/projs.cpp
@@ -28,34 +28,34 @@ namespace projs
 
     FVAR(IDF_PERSIST, gibselasticity, -10000, 0.35f, 10000);
     FVAR(IDF_PERSIST, gibsrelativity, -10000, 0.95f, 10000);
-    FVAR(IDF_PERSIST, gibswaterfric, 0, 2, 10000);
+    FVAR(IDF_PERSIST, gibsliquidcoast, 0, 2, 10000);
     FVAR(IDF_PERSIST, gibsweight, -10000, 150, 10000);
 
-#ifdef VANITY
     FVAR(IDF_PERSIST, vanityelasticity, -10000, 0.5f, 10000);
     FVAR(IDF_PERSIST, vanityrelativity, -10000, 0.95f, 10000);
-    FVAR(IDF_PERSIST, vanitywaterfric, 0, 2, 10000);
+    FVAR(IDF_PERSIST, vanityliquidcoast, 0, 2, 10000);
     FVAR(IDF_PERSIST, vanityweight, -10000, 100, 10000);
-#endif
 
     FVAR(IDF_PERSIST, debriselasticity, -10000, 0.6f, 10000);
-    FVAR(IDF_PERSIST, debriswaterfric, 0, 1.7f, 10000);
+    FVAR(IDF_PERSIST, debrisliquidcoast, 0, 1.7f, 10000);
     FVAR(IDF_PERSIST, debrisweight, -10000, 165, 10000);
 
     FVAR(IDF_PERSIST, ejectelasticity, -10000, 0.35f, 10000);
     FVAR(IDF_PERSIST, ejectrelativity, -10000, 1, 10000);
-    FVAR(IDF_PERSIST, ejectwaterfric, 0, 1.75f, 10000);
+    FVAR(IDF_PERSIST, ejectliquidcoast, 0, 1.75f, 10000);
     FVAR(IDF_PERSIST, ejectweight, -10000, 180, 10000);
 
     VAR(IDF_PERSIST, projtrails, 0, 1, 1);
-    VAR(IDF_PERSIST, projtraildelay, 1, 15, VAR_MAX);
-    VAR(IDF_PERSIST, projtraillength, 1, 350, VAR_MAX);
+    VAR(IDF_PERSIST, projtraildelay, 2, 15, VAR_MAX);
+    VAR(IDF_PERSIST, projtraillength, 1, 250, VAR_MAX);
     VAR(IDF_PERSIST, projhints, 0, 1, 6);
     VAR(IDF_PERSIST, projfirehint, 0, 1, 1);
     FVAR(IDF_PERSIST, projhintblend, 0, 0.75f, 1);
     FVAR(IDF_PERSIST, projhintsize, 0, 1.45f, FVAR_MAX);
     FVAR(IDF_PERSIST, projfirehintsize, 0, 1.85f, FVAR_MAX);
 
+    VAR(0, projdebug, 0, 0, 1);
+
     #define projhint(a,b)   (projhints >= 2 ? game::getcolour(a, projhints-2) : b)
 
     VAR(IDF_PERSIST, muzzleflash, 0, 3, 3); // 0 = off, 1 = only other players, 2 = only thirdperson, 3 = all
@@ -64,14 +64,14 @@ namespace projs
     FVAR(IDF_PERSIST, muzzlefade, 0, 0.5f, 1);
 
     #define muzzlechk(a,b) (a == 3 || (a == 2 && game::thirdpersonview(true)) || (a == 1 && b != game::focus))
-    int calcdamage(gameent *actor, gameent *target, int weap, int &flags, float radial, float size, float dist, float scale)
+    int calcdamage(gameent *v, gameent *target, int weap, int &flags, float radial, float size, float dist, float scale)
     {
-        int nodamage = 0; flags &= ~HIT_SFLAGS;
-        if(actor->aitype < AI_START)
+        int nodamage = 0;
+        flags &= ~HIT_SFLAGS;
+        if(v->actortype < A_ENEMY)
         {
-            if(actor == target && !selfdamage) nodamage++;
-            else if(physics::isghost(target, actor)) nodamage++;
-            if(m_expert(game::gamemode, game::mutators) && !hithead(flags)) nodamage++;
+            if(v == target && !damageself) nodamage++;
+            else if(physics::isghost(target, v)) nodamage++;
         }
 
         if(nodamage || !hithurts(flags))
@@ -81,41 +81,38 @@ namespace projs
         }
 
         float skew = clamp(scale, 0.f, 1.f)*damagescale;
+
+        if(flags&HIT_WHIPLASH) skew *= WF(WK(flags), weap, damagewhiplash, WS(flags));
+        else if(flags&HIT_HEAD) skew *= WF(WK(flags), weap, damagehead, WS(flags));
+        else if(flags&HIT_TORSO) skew *= WF(WK(flags), weap, damagetorso, WS(flags));
+        else if(flags&HIT_LEGS) skew *= WF(WK(flags), weap, damagelegs, WS(flags));
+        else return 0;
+
         if(radial > 0) skew *= clamp(1.f-dist/size, 1e-6f, 1.f);
         else if(WF(WK(flags), weap, taper, WS(flags))) skew *= clamp(dist, 0.f, 1.f);
+
         if(!m_insta(game::gamemode, game::mutators))
         {
             if(m_capture(game::gamemode) && capturebuffdelay)
             {
-                if(actor->lastbuff) skew *= capturebuffdamage;
+                if(v->lastbuff) skew *= capturebuffdamage;
                 if(target->lastbuff) skew /= capturebuffshield;
             }
             else if(m_defend(game::gamemode) && defendbuffdelay)
             {
-                if(actor->lastbuff) skew *= defendbuffdamage;
+                if(v->lastbuff) skew *= defendbuffdamage;
                 if(target->lastbuff) skew /= defendbuffshield;
             }
             else if(m_bomber(game::gamemode) && bomberbuffdelay)
             {
-                if(actor->lastbuff) skew *= bomberbuffdamage;
+                if(v->lastbuff) skew *= bomberbuffdamage;
                 if(target->lastbuff) skew /= bomberbuffshield;
             }
-            else if(m_gauntlet(game::gamemode) && gauntletbuffdelay)
-            {
-                if(actor->lastbuff) skew *= gauntletbuffdamage;
-                if(target->lastbuff) skew /= gauntletbuffshield;
-            }
-        }
-        if(!(flags&HIT_HEAD))
-        {
-            if(flags&HIT_WHIPLASH) skew *= WF(WK(flags), weap, whipdamage, WS(flags));
-            else if(flags&HIT_TORSO) skew *= WF(WK(flags), weap, torsodamage, WS(flags));
-            else if(flags&HIT_LEGS) skew *= WF(WK(flags), weap, legdamage, WS(flags));
-            else skew = 0;
         }
-        if(actor == target)
+
+        if(v == target)
         {
-            float modify = WF(WK(flags), weap, selfdamage, WS(flags))*G(selfdamagescale);
+            float modify = WF(WK(flags), weap, damageself, WS(flags))*G(damageselfscale);
             if(modify != 0) skew *= modify;
             else
             {
@@ -123,9 +120,9 @@ namespace projs
                 flags |= HIT_WAVE;
             }
         }
-        else if(m_team(game::gamemode, game::mutators) && actor->team == target->team)
+        else if(m_team(game::gamemode, game::mutators) && v->team == target->team)
         {
-            float modify = WF(WK(flags), weap, teamdamage, WS(flags))*G(teamdamagescale);
+            float modify = WF(WK(flags), weap, damageteam, WS(flags))*G(damageteamscale);
             if(modify != 0) skew *= modify;
             else
             {
@@ -133,34 +130,26 @@ namespace projs
                 flags |= HIT_WAVE;
             }
         }
+
         return int(ceilf(WF(WK(flags), weap, damage, WS(flags))*skew));
     }
 
     void hitpush(gameent *d, projent &proj, int flags = 0, float radial = 0, float dist = 0, float scale = 1)
     {
         if(dist < 0) dist = 0.f;
-        vec dir, middle = d->center();
+        vec dir, vel(0, 0, 0), middle = d->center();
         dir = vec(middle).sub(proj.o);
         float dmag = dir.magnitude();
         if(dmag > 1e-3f) dir.div(dmag);
         else dir = vec(0, 0, 1);
-        if(flags&HIT_PROJ)
-        { // transfer the momentum
-            float speed = proj.vel.magnitude();
-            if(speed > 1e-6f)
-            {
-                dir.add(vec(proj.vel).div(speed));
-                dmag = dir.magnitude();
-                if(dmag > 1e-3f) dir.div(dmag);
-                else dir = vec(0, 0, 1);
-            }
-        }
+        if(isweap(proj.weap) && !weaptype[proj.weap].traced && flags&HIT_PROJ && proj.weight != 0 && d->weight != 0)
+            vel = vec(proj.vel).mul(proj.weight).div(d->weight);
         if(proj.owner && proj.local)
         {
             int hflags = proj.flags|flags;
             float size = hflags&HIT_WAVE ? radial*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) : radial;
             int damage = calcdamage(proj.owner, d, proj.weap, hflags, radial, size, dist, scale);
-            if(damage) game::hiteffect(proj.weap, hflags, damage, d, proj.owner, dir, false);
+            if(damage) game::hiteffect(proj.weap, hflags, damage, d, proj.owner, dir, vel, dist, false);
             else return;
         }
         hitmsg &h = hits.add();
@@ -169,6 +158,7 @@ namespace projs
         h.target = d->clientnum;
         h.dist = int(dist*DNF);
         h.dir = ivec(int(dir.x*DNF), int(dir.y*DNF), int(dir.z*DNF));
+        h.vel = ivec(int(vel.x*DNF), int(vel.y*DNF), int(vel.z*DNF));
     }
 
     void projpush(projent *p)
@@ -183,7 +173,7 @@ namespace projs
                 h.proj = p->id;
                 h.target = p->owner->clientnum;
                 h.dist = 0;
-                h.dir = ivec(0, 0, 0);
+                h.dir = h.vel = ivec(0, 0, 0);
             }
         }
     }
@@ -202,7 +192,7 @@ namespace projs
         if(gameent::is(d))
         {
             gameent *e = (gameent *)d;
-            if(aistyle[e->aitype].hitbox)
+            if(e->wantshitbox())
             {
                 float rdist[3] = { -1, -1, -1 };
                 radialpush(e->legs, e->lrad.x, e->lrad.y, e->lrad.z, e->lrad.z, rdist[0]);
@@ -224,9 +214,9 @@ namespace projs
                         hitpush(e, proj, flag|flags, radius, rdist[i], proj.curscale);
                         radiated = true;
                     }
-                    else if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) > 1 && rdist[i] <= maxdist)
+                    else if(push && rdist[i] <= maxdist)
                     {
-                        hitpush(e, proj, flag|HIT_WAVE, radius, rdist[i], proj.curscale);
+                        hitpush(e, proj, flag|HIT_WAVE, maxdist, rdist[i], proj.curscale);
                         radiated = true;
                     }
                 }
@@ -239,12 +229,12 @@ namespace projs
                 {
                     if(dist <= radius)
                     {
-                        hitpush(e, proj, (m_expert(game::gamemode, game::mutators) ? HIT_WHIPLASH : HIT_TORSO)|flags, radius, dist, proj.curscale);
+                        hitpush(e, proj, HIT_TORSO|flags, radius, dist, proj.curscale);
                         radiated = true;
                     }
-                    else if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) > 1 && dist <= maxdist)
+                    else if(push && dist <= maxdist)
                     {
-                        hitpush(e, proj, (m_expert(game::gamemode, game::mutators) ? HIT_WHIPLASH : HIT_TORSO)|HIT_WAVE, radius, dist, proj.curscale);
+                        hitpush(e, proj, HIT_TORSO|HIT_WAVE, maxdist, dist, proj.curscale);
                         radiated = true;
                     }
                 }
@@ -358,6 +348,7 @@ namespace projs
     {
         if(proj.stuck)
         {
+            if(init) proj.vel = vec(0, 0, 0);
             if(proj.stick)
             {
                 if(proj.stick->state != CS_ALIVE)
@@ -393,21 +384,21 @@ namespace projs
     {
         if(proj.projtype != PRJ_SHOT || (proj.owner && proj.local))
         {
-            proj.vel = vec(0, 0, 0);
+            proj.stick = d;
             proj.sticknrm = proj.norm;
-            proj.stuck = proj.lastbounce = NZT(lastmillis);
+            proj.stuck = proj.lastbounce = lastmillis ? lastmillis : 1;
             vec fwd = dir.iszero() ? vec(proj.vel).normalize() : dir;
-            if(!fwd.iszero()) loopi(max(int(proj.radius), 100))
+            if(!fwd.iszero()) loopi(20)
             {
                 proj.o.sub(fwd);
-                if(collide(&proj, dir, 0.f, proj.projcollide&COLLIDE_DYNENT) && !inside && !hitplayer) break;
+                if(!collide(&proj, vec(0, 0, 0), 0.f, proj.projcollide&COLLIDE_DYNENT) && !collideinside && (proj.stick ? hitplayer != proj.stick : !hitplayer))
+                    break;
             }
-            if(d)
+            if(proj.stick)
             {
-                proj.stick = d;
                 proj.stickpos = vec(proj.o).sub(d->center());
-                proj.stickpos.rotate_around_z(-d->yaw*RAD);
-                proj.sticknrm.rotate_around_z(-d->yaw*RAD);
+                proj.stickpos.rotate_around_z(-proj.stick->yaw*RAD);
+                proj.sticknrm.rotate_around_z(-proj.stick->yaw*RAD);
             }
             else proj.stickpos = proj.o;
             if(updatesticky(proj, true) && proj.projtype == PRJ_SHOT)
@@ -421,11 +412,10 @@ namespace projs
     {
         loopv(projs) if(projs[i]->owner == d && projs[i]->projtype == PRJ_SHOT && projs[i]->id == id)
         {
-            projs[i]->stuck = projs[i]->lastbounce = NZT(lastmillis);
+            projs[i]->stuck = projs[i]->lastbounce = lastmillis ? lastmillis : 1;
             projs[i]->sticknrm = norm;
             projs[i]->stickpos = pos;
-            if(f) projs[i]->stick = f;
-            else
+            if(!(projs[i]->stick = f))
             {
                 projs[i]->o = pos;
                 projs[i]->stick = NULL;
@@ -448,38 +438,26 @@ namespace projs
                 int millis = proj.lastused(list[i].ent, true);
                 if(millis && lastmillis-millis < 1000) continue;
             }
-            float test = 1e16f, radius = list[i].radius;
+            float test = 1e16f;
             if(ray.iszero())
             {
-                vec to = vec(ray).mul(dist).add(proj.o);
-                float x1 = floor(min(proj.o.x, to.x)), y1 = floor(min(proj.o.y, to.y)),
-                      x2 = ceil(max(proj.o.x, to.x)), y2 = ceil(max(proj.o.y, to.y));
-                if(list[i].o.x+radius >= x1 && list[i].o.y+radius >= y1 && list[i].o.x-radius <= x2 && list[i].o.y-radius <= y2)
-                {
-                    vec bottom(list[i].o), top(list[i].o);
-                    bottom.z -= radius;
-                    top.z += radius;
-                    if(!linecylinderintersect(proj.o, to, bottom, top, radius, test)) continue;
-                    test *= proj.o.dist(to);
-                    if(test < 0) continue;
-                }
-                else continue;
+                if(!raysphereintersect(list[i].o, list[i].radius, proj.o, ray, test) || test > dist) continue;
             }
             else
             {
-                radius *= radius;
-                if((test = proj.o.squaredist(list[i].o)) > radius) continue;
+                test = proj.o.dist(list[i].o);
+                if(test > list[i].radius) continue;
             }
 
             if(closeent < 0 || test <= closedist)
             {
                 closeent = list[i].ent;
-                closedist = dist;
+                closedist = test;
             }
         }
         if(entities::ents.inrange(closeent))
         {
-            entities::execitem(closeent, &proj);
+            entities::execitem(closeent, &proj, proj.o, closedist);
             return true;
         }
         return false;
@@ -561,14 +539,12 @@ namespace projs
                 }
                 vecfromyawpitch(aim[0][1], aim[1][1], 1, 0, dir[1]);
             }
-            float minspeed = proj.minspeed;
             #define repel(x,r,z) \
             { \
                 if(overlapsbox(proj.o, r, r, x, r, r)) \
                 { \
                     vec nrm = vec(proj.o).sub(x).normalize(); \
                     dir[1].add(nrm).normalize(); \
-                    minspeed = max(minspeed, z); \
                     break; \
                 } \
             }
@@ -580,7 +556,7 @@ namespace projs
                     {
                         loopv(projs) if(projs[i]->projtype == PRJ_ENT && projs[i] != &proj && entities::ents.inrange(projs[i]->id) && enttype[entities::ents[projs[i]->id]->type].usetype == EU_ITEM)
                             repel(projs[i]->o, itemrepulsion, itemrepelspeed);
-                        if(!minspeed) loopi(entities::lastusetype[EU_ITEM]) if(enttype[entities::ents[i]->type].usetype == EU_ITEM && entities::ents[i]->spawned)
+                        loopi(entities::lastuse(EU_ITEM)) if(enttype[entities::ents[i]->type].usetype == EU_ITEM && entities::ents[i]->spawned())
                             repel(entities::ents[i]->o, itemrepulsion, itemrepelspeed);
                     }
                     break;
@@ -595,7 +571,12 @@ namespace projs
                     break;
                 }
             }
-            if(!dir[1].iszero()) proj.vel = vec(dir[1]).mul(max(mag, minspeed));
+            if(!dir[1].iszero())
+            {
+                mag = max(mag, proj.speedmin);
+                if(proj.speedmax > 0) mag = min(mag, proj.speedmax);
+                proj.vel = vec(dir[1]).mul(mag);
+            }
         }
         else proj.vel = vec(0, 0, 0);
     }
@@ -618,7 +599,7 @@ namespace projs
                     case W_SHOTGUN: case W_SMG:
                     {
                         part_splash(PART_SPARK, 5, 350, proj.o, FWCOL(H, partcol, proj), 0.35f, 1, 1, 0, 16, 15);
-                        adddecal(DECAL_BULLET, proj.o, proj.norm, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale);
+                        adddecal(DECAL_BULLET, proj.o, proj.norm, max(WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags)), 0.25f)*4*proj.curscale);
                         break;
                     }
                     case W_FLAMER:
@@ -628,34 +609,38 @@ namespace projs
                     }
                     default: break;
                 }
-                if(ricochet)
+                if(ricochet && !proj.limited && !WK(proj.flags))
                 {
-                    int mag = int(proj.vel.magnitude()), vol = int(ceilf(clamp(mag*2, 10, 255)*proj.curscale));
-                    if(vol) playsound(WSND2(proj.weap, WS(proj.flags), S_W_BOUNCE), proj.o, NULL, 0, vol);
+                    int vol = clamp(int(proj.vel.magnitude()*proj.curscale)*2, 0, 255);
+                    if(vol > 0) playsound(WSND2(proj.weap, WS(proj.flags), S_W_BOUNCE), proj.o, NULL, 0, vol);
                 }
                 break;
             }
             case PRJ_GIBS:
             {
-                if(game::bloodscale > 0)
+                if(game::nogore == 2) break;
+                if(game::nogore || game::bloodscale > 0)
                 {
                     adddecal(DECAL_BLOOD, proj.o, proj.norm, proj.radius*clamp(proj.vel.magnitude()/2, 1.f, 4.f), bvec(125, 255, 255));
-                    int mag = int(proj.vel.magnitude()), vol = int(ceilf(clamp(mag*2, 10, 255)*proj.curscale));
-                    if(vol) playsound(S_SPLOSH, proj.o, NULL, 0, vol);
+                    int vol = clamp(int(proj.vel.magnitude()*proj.curscale)*2, 0, 255);
+                    if(vol > 0) playsound(S_SPLOSH, proj.o, NULL, 0, vol);
                     break;
                 } // otherwise fall through
             }
             case PRJ_DEBRIS: case PRJ_VANITY:
             {
-                int mag = int(proj.vel.magnitude()), vol = int(ceilf(clamp(mag*2, 10, 255)*proj.curscale));
-                if(proj.projtype == PRJ_VANITY) vol /= 2;
-                if(vol) playsound(S_DEBRIS, proj.o, NULL, 0, vol);
+                int vol = clamp(int(proj.vel.magnitude()*proj.curscale)*2, 0, 255);
+                if(vol > 0)
+                {
+                    if(proj.projtype == PRJ_VANITY) vol /= 2;
+                    playsound(S_DEBRIS, proj.o, NULL, 0, vol);
+                }
                 break;
             }
             case PRJ_EJECT: case PRJ_AFFINITY:
             {
-                int mag = int(proj.vel.magnitude()), vol = int(ceilf(clamp(mag*2, 10, 255)*proj.curscale));
-                if(vol) playsound(S_SHELL, proj.o, NULL, 0, vol);
+                int vol = clamp(int(proj.vel.magnitude()*proj.curscale)*3, proj.projtype == PRJ_AFFINITY ? 32 : 0, 255);
+                if(vol > 0) playsound(proj.projtype == PRJ_EJECT ? int(S_SHELL) : int(S_BOUNCE), proj.o, NULL, 0, vol);
                 break;
             }
             default: break;
@@ -664,7 +649,15 @@ namespace projs
 
     float fadeweap(projent &proj)
     {
-        float trans = 1;
+        float trans = WF(WK(proj.flags), proj.weap, blend, WS(proj.flags));
+        if(WF(WK(proj.flags), proj.weap, fade, WS(proj.flags))&(proj.owner != game::focus ? 2 : 1))
+        {
+            int millis = lastmillis-proj.spawntime;
+            if(millis < WF(WK(proj.flags), proj.weap, fadetime, WS(proj.flags)))
+                trans *= 1.f-(millis/float(WF(WK(proj.flags), proj.weap, fadetime, WS(proj.flags))));
+            if(!proj.escaped && proj.owner == game::focus && WF(WK(proj.flags), proj.weap, fadeat, WS(proj.flags)) > 0)
+                trans *= camera1->o.distrange(proj.o, WF(WK(proj.flags), proj.weap, fadeat, WS(proj.flags)), WF(WK(proj.flags), proj.weap, fadecut, WS(proj.flags)));
+        }
         if(proj.stuck && isweap(proj.weap) && WF(WK(proj.flags), proj.weap, vistime, WS(proj.flags)))
         {
             int millis = lastmillis-proj.stuck;
@@ -672,12 +665,56 @@ namespace projs
                 trans *= 1.f-(WF(WK(proj.flags), proj.weap, visfade, WS(proj.flags))*millis/float(WF(WK(proj.flags), proj.weap, vistime, WS(proj.flags))));
             else trans *= 1.f-WF(WK(proj.flags), proj.weap, visfade, WS(proj.flags));
             if(proj.beenused && proj.lifetime < WF(WK(proj.flags), proj.weap, proxtime, WS(proj.flags)))
-                    trans += (1.f-trans)*((WF(WK(proj.flags), proj.weap, proxtime, WS(proj.flags))-proj.lifetime)/float(WF(WK(proj.flags), proj.weap, proxtime, WS(proj.flags))));
+                trans += (1.f-trans)*((WF(WK(proj.flags), proj.weap, proxtime, WS(proj.flags))-proj.lifetime)/float(WF(WK(proj.flags), proj.weap, proxtime, WS(proj.flags))));
         }
         return trans;
     }
 
-    void updatebb(projent &proj, bool init = false)
+    bool spherecheck(projent &proj, bool rev = false)
+    {
+        vec dir = vec(proj.vel).normalize();
+        if(collide(&proj, dir, 0, false))
+        {
+            vec orig = proj.o;
+            if(!proj.lastgood.iszero())
+            {
+                proj.o = proj.lastgood;
+                if(insideworld(proj.o) && !collide(&proj, dir, 0, false))
+                {
+                    if(rev)
+                    {
+                        float mag = max(max(proj.vel.magnitude()*proj.elasticity, proj.speedmin), 1.f);
+                        if(proj.speedmax > 0) mag = min(mag, proj.speedmax);
+                        proj.vel = vec(proj.o).sub(orig).normalize().mul(mag);
+                    }
+                    return true;
+                }
+                else proj.o = orig;
+            }
+            float yaw, pitch;
+            vectoyawpitch(dir, yaw, pitch);
+            if((yaw += 180) >= 360) yaw -= 360;
+            loopi(10) loopj(8) loopk(8)
+            {
+                proj.o.add(vec((int(yaw+k*45)%360)*RAD, j*45*RAD).mul(proj.radius*i));
+                if(insideworld(proj.o) && !collide(&proj, dir, 0, false))
+                {
+                    if(rev)
+                    {
+                        float mag = max(max(proj.vel.magnitude()*proj.elasticity, proj.speedmin), 1.f);
+                        if(proj.speedmax > 0) mag = min(mag, proj.speedmax);
+                        proj.vel = vec(proj.o).sub(orig).normalize().mul(mag);
+                    }
+                    return true;
+                }
+                else proj.o = orig;
+            }
+        }
+        else proj.lastgood = proj.o;
+        return false;
+    }
+
+    bool updatebb(projent &proj, bool init = false)
     {
         float size = 1;
         switch(proj.projtype)
@@ -695,20 +732,19 @@ namespace projs
                         break;
                     }
                 } // all falls through to ..
-            default: return;
+            default: return false;
         }
         model *m = NULL;
         if(proj.mdl && *proj.mdl && ((m = loadmodel(proj.mdl)) != NULL))
         {
             vec center, radius;
-            m->boundbox(0, center, radius);
+            m->boundbox(center, radius);
             center.mul(size*proj.curscale);
             radius.mul(size*proj.curscale);
-            rotatebb(center, radius, proj.yaw, proj.pitch, proj.roll);
+            rotatebb(center, radius, proj.yaw, 0, 0);
             proj.xradius = radius.x;
             proj.yradius = radius.y;
             proj.radius = max(radius.x, radius.y);
-            if(proj.projtype == PRJ_AFFINITY && m_capture(game::gamemode)) proj.o.z += radius.z-proj.zradius;
             proj.height = proj.zradius = proj.aboveeye = radius.z;
         }
         else switch(proj.projtype)
@@ -723,8 +759,7 @@ namespace projs
                 break;
             }
         }
-        if(!init) physics::entinmap(&proj, proj.projcollide&COLLIDE_PLAYER);
-        else if(proj.projtype == PRJ_AFFINITY) proj.o.z += proj.height*2;
+        return true;
     }
 
     void updatetargets(projent &proj, int style = 0)
@@ -738,7 +773,7 @@ namespace projs
                     proj.o = proj.to = proj.from = proj.dest = proj.owner->center();
                     if(proj.target && proj.target->state == CS_ALIVE)
                         proj.to.add(vec(proj.target->center()).sub(proj.from).normalize().mul(proj.owner->radius*2));
-                    else proj.to.add(vec(RAD*proj.owner->yaw, RAD*proj.owner->pitch).mul(proj.owner->radius*2));
+                    else proj.to.add(vec(proj.owner->yaw*RAD, proj.owner->pitch*RAD).mul(proj.owner->radius*2));
                 }
                 else
                 {
@@ -771,10 +806,11 @@ namespace projs
                 proj.elasticity = WF(WK(proj.flags), proj.weap, elasticity, WS(proj.flags));
                 proj.reflectivity = WF(WK(proj.flags), proj.weap, reflectivity, WS(proj.flags));
                 proj.relativity = W2(proj.weap, relativity, WS(proj.flags));
-                proj.waterfric = WF(WK(proj.flags), proj.weap, waterfric, WS(proj.flags));
+                proj.liquidcoast = WF(WK(proj.flags), proj.weap, liquidcoast, WS(proj.flags));
                 proj.weight = WF(WK(proj.flags), proj.weap, weight, WS(proj.flags));
                 proj.projcollide = WF(WK(proj.flags), proj.weap, collide, WS(proj.flags));
-                proj.minspeed = WF(WK(proj.flags), proj.weap, minspeed, WS(proj.flags));
+                proj.speedmin = WF(WK(proj.flags), proj.weap, speedmin, WS(proj.flags));
+                proj.speedmax = WF(WK(proj.flags), proj.weap, speedmax, WS(proj.flags));
                 proj.extinguish = WF(WK(proj.flags), proj.weap, extinguish, WS(proj.flags))|4;
                 proj.interacts = WF(WK(proj.flags), proj.weap, interacts, WS(proj.flags));
                 proj.mdl = weaptype[proj.weap].proj;
@@ -787,9 +823,16 @@ namespace projs
             }
             case PRJ_GIBS:
             {
-                if(game::bloodscale > 0)
+                if(game::nogore == 2)
+                {
+                    proj.lifemillis = proj.lifetime = 1;
+                    proj.lifespan = 1.f;
+                    proj.state = CS_DEAD;
+                    proj.escaped = true;
+                    return;
+                }
+                if(!game::nogore || game::bloodscale > 0)
                 {
-                    proj.collidetype = COLLIDE_AABB;
                     proj.height = proj.aboveeye = proj.radius = proj.xradius = proj.yradius = 0.5f;
                     proj.lifesize = 1.5f-(rnd(100)/100.f);
                     if(proj.owner)
@@ -817,7 +860,7 @@ namespace projs
                     proj.reflectivity = 0.f;
                     proj.elasticity = gibselasticity;
                     proj.relativity = gibsrelativity;
-                    proj.waterfric = gibswaterfric;
+                    proj.liquidcoast = gibsliquidcoast;
                     proj.weight = gibsweight*proj.lifesize;
                     proj.vel.add(vec(rnd(21)-10, rnd(21)-10, proj.owner && proj.owner->headless ? rnd(61)+10 : rnd(21)-10));
                     proj.projcollide = BOUNCE_GEOM|BOUNCE_PLAYER;
@@ -830,7 +873,6 @@ namespace projs
             }
             case PRJ_DEBRIS:
             {
-                proj.collidetype = COLLIDE_AABB;
                 proj.height = proj.aboveeye = proj.radius = proj.xradius = proj.yradius = 0.5f;
                 proj.lifesize = 1.5f-(rnd(100)/100.f);
                 switch(rnd(4))
@@ -842,7 +884,7 @@ namespace projs
                 }
                 proj.relativity = proj.reflectivity = 0.f;
                 proj.elasticity = debriselasticity;
-                proj.waterfric = debriswaterfric;
+                proj.liquidcoast = debrisliquidcoast;
                 proj.weight = debrisweight*proj.lifesize;
                 proj.vel.add(vec(rnd(101)-50, rnd(101)-50, rnd(151)-50)).mul(2);
                 proj.projcollide = BOUNCE_GEOM|BOUNCE_PLAYER|COLLIDE_OWNER;
@@ -871,7 +913,7 @@ namespace projs
                 proj.reflectivity = 0.f;
                 proj.elasticity = ejectelasticity;
                 proj.relativity = ejectrelativity;
-                proj.waterfric = ejectwaterfric;
+                proj.liquidcoast = ejectliquidcoast;
                 proj.weight = (ejectweight+(proj.speed*2))*proj.lifesize; // so they fall better in relation to their speed
                 proj.projcollide = BOUNCE_GEOM;
                 proj.escaped = true;
@@ -891,15 +933,16 @@ namespace projs
                 proj.reflectivity = 0.f;
                 proj.elasticity = itemelasticity;
                 proj.relativity = itemrelativity;
-                proj.waterfric = itemwaterfric;
+                proj.liquidcoast = itemliquidcoast;
                 proj.weight = itemweight;
                 proj.projcollide = itemcollide;
-                proj.minspeed = itemminspeed;
+                proj.speedmin = itemspeedmin;
+                proj.speedmax = itemspeedmax;
                 proj.escaped = true;
                 float mag = proj.inertia.magnitude();
                 if(mag <= 50)
                 {
-                    if(mag <= 0) vecfromyawpitch(proj.yaw, proj.pitch, 1, 0, proj.inertia);
+                    if(mag <= 0) proj.inertia = vec(proj.yaw*RAD, proj.pitch*RAD);
                     proj.inertia.normalize().mul(50);
                 }
                 proj.to.add(proj.inertia);
@@ -917,7 +960,7 @@ namespace projs
                 vectoyawpitch(dir, proj.yaw, proj.pitch);
                 proj.reflectivity = 0.f;
                 proj.escaped = true;
-                proj.fadetime = 500;
+                proj.fadetime = 1;
                 switch(game::gamemode)
                 {
                     case G_BOMBER:
@@ -928,8 +971,9 @@ namespace projs
                         proj.elasticity = bomberelasticity;
                         proj.weight = bomberweight;
                         proj.relativity = bomberrelativity;
-                        proj.waterfric = bomberwaterfric;
-                        proj.minspeed = bomberminspeed;
+                        proj.liquidcoast = bomberliquidcoast;
+                        proj.speedmin = bomberspeedmin;
+                        proj.speedmax = bomberspeedmax;
                         break;
                     case G_CAPTURE: default:
                         proj.mdl = "props/flag";
@@ -939,15 +983,15 @@ namespace projs
                         proj.elasticity = captureelasticity;
                         proj.weight = captureweight;
                         proj.relativity = capturerelativity;
-                        proj.waterfric = capturewaterfric;
-                        proj.minspeed = captureminspeed;
+                        proj.liquidcoast = captureliquidcoast;
+                        proj.speedmin = capturespeedmin;
+                        proj.speedmax = capturespeedmax;
                         break;
                 }
                 break;
             }
             case PRJ_VANITY:
             {
-                proj.collidetype = COLLIDE_AABB;
                 proj.height = proj.aboveeye = proj.radius = proj.xradius = proj.yradius = 1;
                 if(proj.owner)
                 {
@@ -965,14 +1009,12 @@ namespace projs
                         return;
                     }
                 }
-#ifdef VANITY
                 proj.mdl = game::vanityfname(proj.owner, proj.weap, true);
                 proj.reflectivity = 0.f;
                 proj.elasticity = vanityelasticity;
                 proj.relativity = vanityrelativity;
-                proj.waterfric = vanitywaterfric;
+                proj.liquidcoast = vanityliquidcoast;
                 proj.weight = vanityweight;
-#endif
                 proj.vel.add(vec(rnd(21)-10, rnd(21)-10, rnd(61)+10));
                 proj.projcollide = BOUNCE_GEOM|BOUNCE_PLAYER;
                 proj.escaped = !proj.owner || proj.owner->state != CS_ALIVE;
@@ -984,6 +1026,25 @@ namespace projs
             default: break;
         }
         if(proj.projtype != PRJ_SHOT) updatebb(proj, true);
+        proj.spawntime = lastmillis;
+        proj.hit = NULL;
+        proj.hitflags = HITFLAG_NONE;
+        proj.movement = 1;
+        if(proj.owner && (proj.projtype != PRJ_SHOT || (!proj.child && !WK(proj.flags) && !weaptype[proj.weap].traced)))
+        {
+            vec eyedir = vec(proj.o).sub(proj.owner->o);
+            float eyedist = eyedir.magnitude();
+            if(eyedist > 0)
+            {
+                eyedir.normalize();
+                float blocked = tracecollide(&proj, proj.owner->o, eyedir, eyedist, RAY_CLIPMAT|RAY_ALPHAPOLY, false);
+                if(blocked >= 0)
+                {
+                    proj.o = proj.from = vec(eyedir).mul(blocked-max(proj.radius, 1e-3f)).add(proj.owner->o);
+                    proj.to = vec(eyedir).mul(blocked).add(proj.owner->o);
+                }
+            }
+        }
         if(proj.projtype != PRJ_SHOT || !weaptype[proj.weap].traced)
         {
             vec dir = vec(proj.to).sub(proj.o);
@@ -1000,36 +1061,13 @@ namespace projs
                     proj.yaw = proj.owner->yaw;
                     proj.pitch = proj.owner->pitch;
                 }
-                vecfromyawpitch(proj.yaw, proj.pitch, 1, 0, dir);
-            }
-            vec rel = vec(proj.vel).add(dir);
-            if(proj.relativity > 0)
-            {
-                if(proj.owner) loopi(3) if(proj.inertia[i]*rel[i] < 0) proj.inertia[i] = 0;
-                rel.add(proj.inertia.mul(proj.relativity));
+                dir = vec(proj.yaw*RAD, proj.pitch*RAD);
             }
+            vec rel = vec(proj.vel).add(dir).add(proj.inertia.mul(proj.relativity));
             proj.vel = vec(rel).add(vec(dir).mul(physics::movevelocity(&proj)));
         }
-        proj.spawntime = lastmillis;
-        proj.hit = NULL;
-        proj.hitflags = HITFLAG_NONE;
-        proj.movement = 1;
-        if(proj.projtype != PRJ_SHOT || (!proj.child && !WK(proj.flags) && !weaptype[proj.weap].traced))
-        {
-            vec retract = vec(proj.vel).normalize().mul(vec(proj.xradius, proj.yradius, proj.zradius)),
-                loc = vec(proj.projtype != PRJ_SHOT || !proj.owner || proj.child || WK(proj.flags) ? proj.o : proj.owner->o).sub(retract),
-                eyedir = vec(proj.o).sub(loc);
-            float eyedist = eyedir.magnitude();
-            if(eyedist >= 1e-6f)
-            {
-                eyedir.div(eyedist);
-                float blocked = tracecollide(&proj, loc, eyedir, eyedist, RAY_CLIPMAT|RAY_ALPHAPOLY, proj.projcollide&COLLIDE_PLAYER);
-                if(blocked >= 0) proj.o = vec(eyedir).mul(blocked+proj.radius+1e-6f).add(loc);
-            }
-        }
-        //if(proj.projtype != PRJ_SHOT || proj.child || WK(proj.flags))
-            physics::entinmap(&proj, proj.projcollide&COLLIDE_PLAYER);
-        //else proj.resetinterp();
+        if(proj.projtype != PRJ_SHOT) spherecheck(proj);
+        proj.resetinterp();
     }
 
     projent *create(const vec &from, const vec &to, bool local, gameent *d, int type, int lifetime, int lifemillis, int waittime, int speed, int id, int weap, int value, int flags, float scale, bool child, projent *parent)
@@ -1071,8 +1109,8 @@ namespace projs
                 proj.yaw = d->yaw;
                 proj.pitch = d->pitch;
                 proj.inertia = vec(d->vel).add(d->falling);
-                if(proj.projtype == PRJ_SHOT && isweap(proj.weap) && issound(d->pschan) && proj.weap != W_MELEE)
-                    playsound(WSND2(proj.weap, WS(proj.flags), S_W_TRANSIT), proj.o, &proj, SND_LOOP, int(ceilf(sounds[d->pschan].vol)), -1, -1, &proj.schan, 0, &d->pschan);
+                if(proj.projtype == PRJ_SHOT && isweap(proj.weap) && issound(d->pschan) && proj.weap != W_MELEE && !WK(proj.flags))
+                    playsound(WSND2(proj.weap, WS(proj.flags), S_W_TRANSIT), proj.o, &proj, SND_LOOP, sounds[d->pschan].vol, -1, -1, &proj.schan, 0, &d->pschan);
             }
         }
         if(!proj.waittime) init(proj, false);
@@ -1080,15 +1118,15 @@ namespace projs
         return &proj;
     }
 
-    void drop(gameent *d, int weap, int ent, int ammo, int reloads, bool local, int index, int targ)
+    void drop(gameent *d, int weap, int ent, int ammo, bool local, int index, int targ)
     {
         if(weap >= W_OFFSET && isweap(weap))
         {
             if(ammo >= 0)
             {
                 if(entities::ents.inrange(ent))
-                    create(d->muzzlepos(), d->muzzlepos(), local, d, PRJ_ENT, w_spawn(weap), w_spawn(weap), 1, 1, ent, ammo, reloads, index);
-                d->ammo[weap] = d->reloads[weap] = -1;
+                    create(d->muzzlepos(), d->muzzlepos(), local, d, PRJ_ENT, w_spawn(weap), w_spawn(weap), 1, 1, ent, ammo, index);
+                d->ammo[weap] = -1;
                 if(targ >= 0) d->setweapstate(weap, W_S_SWITCH, weaponswitchdelay, lastmillis);
             }
             else if(weap == W_GRENADE || weap == W_MINE)
@@ -1099,29 +1137,30 @@ namespace projs
 
     void shootv(int weap, int flags, int sub, int offset, float scale, vec &from, vector<shotmsg> &shots, gameent *d, bool local)
     {
-        int delay = W2(weap, projdelay, WS(flags)), attackdelay = W2(weap, attackdelay, WS(flags)),
-            power = W2(weap, power, WS(flags)), cooked = W2(weap, cooked, WS(flags)),
+        int delay = W2(weap, timedelay, WS(flags)), iter = W2(weap, timeiter, WS(flags)),
+            delayattack = W2(weap, delayattack, WS(flags)),
+            cook = W2(weap, cooktime, WS(flags)), cooked = W2(weap, cooked, WS(flags)),
             life = W2(weap, time, WS(flags)), speed = W2(weap, speed, WS(flags)),
-            limspeed = W2(weap, limspeed, WS(flags));
+            speedlimit = W2(weap, speedlimit, WS(flags));
         float skew = 1;
-        if(power && cooked)
+        if(cook && cooked)
         {
-            if(cooked&1)  skew = scale; // scaled
-            if(cooked&2)  skew = 1-scale; // inverted scale
-            if(cooked&4)  life = int(ceilf(life*scale)); // life scale
-            if(cooked&8)  life = int(ceilf(life*(1-scale))); // inverted life
-            if(cooked&16) speed = limspeed+int(ceilf(max(speed-limspeed, 0)*scale)); // speed scale
-            if(cooked&32) speed = limspeed+int(ceilf(max(speed-limspeed, 0)*(1-scale))); // inverted speed
+            if(cooked&W_C_SCALE)   skew = scale; // scaled
+            if(cooked&W_C_SCALEN)  skew = 1-scale; // inverted scale
+            if(cooked&W_C_LIFE)    life = int(ceilf(life*scale)); // life scale
+            if(cooked&W_C_LIFEN)   life = int(ceilf(life*(1-scale))); // inverted life
+            if(cooked&W_C_SPEED)   speed = speedlimit+int(ceilf(max(speed-speedlimit, 0)*scale)); // speed scale
+            if(cooked&W_C_SPEEDN)  speed = speedlimit+int(ceilf(max(speed-speedlimit, 0)*(1-scale))); // inverted speed
         }
 
         if(weaptype[weap].sound >= 0 && (weap != W_MELEE || !(WS(flags))))
         {
-            int slot = WSNDF(weap, WS(flags)), vol = int(ceilf(255*skew));
+            int slot = WSNDF(weap, WS(flags)), vol = clamp(int(ceilf(255*skew)), 0, 255);
             if(slot >= 0 && vol > 0)
             {
                 if(weap == W_FLAMER && !(WS(flags)))
                 {
-                    int ends = lastmillis+attackdelay+PHYSMILLIS;
+                    int ends = lastmillis+delayattack+PHYSMILLIS;
                     if(issound(d->wschan) && sounds[d->wschan].slotnum == slot) sounds[d->wschan].ends = ends;
                     else playsound(slot, d->o, d, SND_LOOP, vol, -1, -1, &d->wschan, ends);
                 }
@@ -1136,10 +1175,10 @@ namespace projs
                 }
             }
         }
-        if(attackdelay >= 5)
+        if(delayattack >= 5 && weap != W_MELEE)
         {
             int colour = WHCOL(d, weap, partcol, WS(flags));
-            float muz = muzzleblend;
+            float muz = muzzleblend*W2(weap, partblend, WS(flags));
             if(d == game::focus) muz *= muzzlefade;
             const struct weapfxs
             {
@@ -1153,6 +1192,7 @@ namespace projs
                 { 50, PART_MUZZLE_FLASH, 350, 5, 6, 1.5f, 2, 4, 0.0125f },
                 { 150, PART_MUZZLE_FLASH, 250, 5, 8, 1.5f, 0, 0, 0.025f },
                 { 150, PART_PLASMA, 250, 10, 6, 1.5f, 0, 0, 0.0125f },
+                { 150, PART_ELECZAP, 250, 10, 6, 1.5f, 0, 0, 0.0125f },
                 { 150, PART_PLASMA, 250, 5, 6, 2, 3, 6, 0.0125f },
                 { 0, 0, 0, 0, 0, 0, 0, 0, 0 },
                 { 0, 0, 0, 0, 0, 0, 0, 0, 0 },
@@ -1162,46 +1202,35 @@ namespace projs
             if(weapfx[weap].sparktime && weapfx[weap].sparknum)
                 part_splash(weap == W_FLAMER ? PART_FIREBALL : PART_SPARK, weapfx[weap].sparknum, weapfx[weap].sparktime, from, colour, weapfx[weap].sparksize, muz, 1, 0, weapfx[weap].sparkrad, 15);
             if(muzzlechk(muzzleflash, d) && weapfx[weap].partsize > 0)
-                part_create(weapfx[weap].parttype, attackdelay/3, from, colour, weapfx[weap].partsize, muz, 0, 0, d);
+                part_create(weapfx[weap].parttype, delayattack/3, from, colour, weapfx[weap].partsize, muz, 0, 0, d);
             if(muzzlechk(muzzleflare, d) && weapfx[weap].flaresize > 0)
             {
                 vec targ; findorientation(d->o, d->yaw, d->pitch, targ);
                 targ.sub(from).normalize().mul(weapfx[weap].flarelen).add(from);
-                part_flare(from, targ, attackdelay/2, PART_MUZZLE_FLARE, colour, weapfx[weap].flaresize, muz, 0, 0, d);
-            }
-            if(weap != W_MELEE)
-            {
-                int peak = attackdelay/4, fade = min(peak/2, 75);
-                adddynlight(from, 32, vec::hexcolor(colour).mul(0.5f), fade, peak - fade, DL_FLASH);
+                part_flare(from, targ, delayattack/2, PART_MUZZLE_FLARE, colour, weapfx[weap].flaresize, muz, 0, 0, d);
             }
+            int peak = delayattack/4, fade = min(peak/2, 75);
+            adddynlight(from, 32, vec::hexcolor(colour).mul(0.5f), fade, peak - fade, DL_FLASH);
         }
-
         loopv(shots)
-            create(from, shots[i].pos.tovec().div(DMF), local, d, PRJ_SHOT, max(life, 1), W2(weap, time, WS(flags)), delay, speed, shots[i].id, weap, -1, flags, skew);
-        if(ejectfade && weaptype[weap].eject && *weaptype[weap].eprj) loopi(clamp(sub, 1, W2(weap, sub, WS(flags))))
+            create(from, shots[i].pos.tovec().div(DMF), local, d, PRJ_SHOT, max(life, 1), W2(weap, time, WS(flags)), delay+(iter*i), speed, shots[i].id, weap, -1, flags, skew);
+        if(ejectfade && weaptype[weap].eject && *weaptype[weap].eprj) loopi(clamp(sub, 1, W2(weap, ammosub, WS(flags))))
             create(from, from, local, d, PRJ_EJECT, rnd(ejectfade)+ejectfade, 0, delay, rnd(weaptype[weap].espeed)+weaptype[weap].espeed, 0, weap, -1, flags);
 
-        if(d->aitype >= AI_BOT && d->skill <= 100 && (!W2(weap, fullauto, WS(flags)) || attackdelay >= PHYSMILLIS))
-            attackdelay += int(ceilf(attackdelay*(10.f/d->skill)));
-        d->setweapstate(weap, WS(flags) ? W_S_SECONDARY : W_S_PRIMARY, attackdelay, lastmillis);
+        d->setweapstate(weap, WS(flags) ? W_S_SECONDARY : W_S_PRIMARY, delayattack, lastmillis);
         d->ammo[weap] = max(d->ammo[weap]-sub-offset, 0);
         d->weapshot[weap] = sub;
-        if(offset > 0)
+        if(offset > 0) d->weapload[weap] = -offset;
+        d->lastshoot = lastmillis;
+        if(d->actortype < A_ENEMY || actor[d->actortype].canmove)
         {
-            d->reloads[weap] = max(d->reloads[weap]-1, 0);
-            d->weapload[weap] = -offset;
-        }
-        if(d->aitype < AI_START || aistyle[d->aitype].canmove)
-        {
-            vec kick;
-            vecfromyawpitch(d->yaw, d->pitch, 1, 0, kick);
-            kick.normalize().mul(-W2(weap, kickpush, WS(flags))*skew);
+            vec kick = vec(d->yaw*RAD, d->pitch*RAD).mul(-W2(weap, kickpush, WS(flags))*skew);
             if(!kick.iszero())
             {
                 if(d == game::focus) game::swaypush.add(vec(kick).mul(kickpushsway));
                 float kickmod = kickpushscale;
-                if(W(weap, zooms) && WS(flags)) kickmod *= kickpushzoom;
-                if(physics::iscrouching(d) && !physics::sliding(d)) kickmod *= kickpushcrouch;
+                if(W2(weap, cooked, WS(flags))&W_C_ZOOM && WS(flags)) kickmod *= kickpushzoom;
+                if(d->crouching() && !d->sliding()) kickmod *= kickpushcrouch;
                 d->vel.add(vec(kick).mul(kickmod));
             }
         }
@@ -1296,27 +1325,34 @@ namespace projs
             updatetargets(proj);
             updatetaper(proj, proj.distance, true);
         }
-        updatebb(proj);
+        else if(updatebb(proj) && spherecheck(proj, proj.projcollide&BOUNCE_GEOM)) proj.resetinterp();
     }
 
     void effect(projent &proj)
     {
+        if(projdebug)
+        {
+            float yaw, pitch;
+            vectoyawpitch(vec(proj.vel).normalize(), yaw, pitch);
+            part_radius(proj.o, vec(proj.radius, proj.radius, proj.radius), 2, 1, 1, 0x22FFFF);
+            part_dir(proj.o, yaw, pitch, max(proj.vel.magnitude(), proj.radius+2), 2, 1, 1, 0xFF22FF);
+        }
         switch(proj.projtype)
         {
             case PRJ_SHOT:
             {
                 updatetaper(proj, proj.distance);
-                float trans = fadeweap(proj);
-                if(!WK(proj.flags) && !proj.limited && proj.weap != W_MELEE)
+                float trans = fadeweap(proj)*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags));
+                if(!proj.limited && !WK(proj.flags))// && proj.weap != W_MELEE)
                 {
-                    int vol = int(ceilf(255*proj.curscale*trans));
-                    if(W2(proj.weap, power, WS(proj.flags))) switch(W2(proj.weap, cooked, WS(proj.flags)))
+                    int vol = clamp(int(ceilf(255*proj.curscale)), 0, 255);
+                    if(W2(proj.weap, cooktime, WS(proj.flags))) switch(W2(proj.weap, cooked, WS(proj.flags)))
                     {
-                        case 4: case 5: vol = 10+int(245*(1.f-proj.lifespan)*proj.lifesize*proj.curscale); break; // longer
-                        case 1: case 2: case 3: default: vol = 10+int(245*proj.lifespan*proj.lifesize*proj.curscale); break; // shorter
+                        case 4: case 5: vol = clamp(10+int(245*(1.f-proj.lifespan)*proj.lifesize*proj.curscale), 0, 255); break; // longer
+                        case 1: case 2: case 3: default: vol = clamp(10+int(245*proj.lifespan*proj.lifesize*proj.curscale), 0, 255); break; // shorter
                     }
                     if(issound(proj.schan)) sounds[proj.schan].vol = vol;
-                    else if(vol) playsound(WSND2(proj.weap, WS(proj.flags), S_W_TRANSIT), proj.o, &proj, SND_LOOP, vol, -1, -1, &proj.schan);
+                    else if(vol > 0) playsound(WSND2(proj.weap, WS(proj.flags), S_W_TRANSIT), proj.o, &proj, SND_LOOP, vol, -1, -1, &proj.schan);
                 }
                 int type = WF(WK(proj.flags), proj.weap, parttype, WS(proj.flags));
                 switch(type)
@@ -1335,22 +1371,26 @@ namespace projs
                     }
                     case W_PISTOL:
                     {
-                        float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, proj.curscale, min(min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.movement), proj.o.dist(proj.from)));
+                        float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.o.dist(proj.from)));
+                        if(proj.lastbounce) size = min(size, max(proj.movement, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale));
                         if(size > 0)
                         {
                             proj.to = vec(proj.o).sub(vec(proj.vel).normalize().mul(size));
                             part_flare(proj.to, proj.o, 1, PART_FLARE, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, clamp(1.25f-proj.lifespan, 0.35f, 0.85f)*trans);
-                            if(projhints) part_flare(proj.to, proj.o, 1, PART_FLARE, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.curscale, clamp(1.25f-proj.lifespan, 0.35f, 0.85f)*projhintblend*trans);
-                            part_create(PART_PLASMA, 1, proj.o, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, clamp(1.25f-proj.lifespan, 0.35f, 0.85f)*trans);
-                            if(projhints) part_create(PART_HINT, 1, proj.o, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.curscale, clamp(1.25f-proj.lifespan, 0.35f, 0.85f)*projhintblend*trans);
+                            if(projhints)
+                            {
+                                part_flare(proj.to, proj.o, 1, PART_FLARE, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.curscale, clamp(1.25f-proj.lifespan, 0.35f, 0.85f)*projhintblend*trans);
+                                part_create(PART_HINT, 1, proj.o, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.curscale, clamp(1.25f-proj.lifespan, 0.35f, 0.85f)*projhintblend*trans);
+                            }
                         }
                         break;
                     }
                     case W_FLAMER:
                     {
-                        float size = WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*1.25f*proj.lifesize*proj.curscale, blend = clamp(1.25f-proj.lifespan, 0.35f, 0.85f)*(0.6f+(rnd(40)/100.f))*trans;
-                        if(projfirehint)
-                            part_create(PART_HINT_SOFT, 1, proj.o, projhint(proj.owner, 0x120228), size*projfirehintsize, blend*projhintblend);
+                        float blend = clamp(1.25f-proj.lifespan, 0.35f, 0.85f)*(0.6f+(rnd(40)/100.f))*trans,
+                            size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.o.dist(proj.from)));
+                        if(proj.lastbounce) size = min(size, max(proj.movement, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale));
+                        if(projfirehint) part_create(PART_HINT_SOFT, 1, proj.o, projhint(proj.owner, 0x120228), size*projfirehintsize, blend*projhintblend);
                         if(projtrails && lastmillis-proj.lasteffect >= projtraildelay*2)
                         {
                             part_create(PART_FIREBALL_SOFT, max(int(projtraillength*0.5f*max(1.f-proj.lifespan, 0.1f)), 1), proj.o, FWCOL(H, partcol, proj), size, blend, -5);
@@ -1387,11 +1427,11 @@ namespace projs
                         float fluc = 1.f+(interval ? (interval <= 500 ? interval/500.f : (1000-interval)/500.f) : 0.f);
                         part_create(PART_PLASMA_SOFT, 1, proj.o, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))+fluc, trans);
                         if(projhints) part_create(PART_HINT_SOFT, 1, proj.o, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize+fluc, projhintblend*trans);
-                        if(projtrails && lastmillis-proj.lasteffect >= projtraildelay)
+                        if(projtrails && lastmillis-proj.lasteffect >= projtraildelay/2)
                         {
-                            part_create(PART_FIREBALL_SOFT, max(projtraillength/2, 1), proj.o, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*0.5f, 0.85f*trans, -5);
-                            part_create(PART_SMOKE_LERP, projtraillength, proj.o, 0x222222, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags)), trans, -10);
-                            proj.lasteffect = lastmillis - (lastmillis%projtraildelay);
+                            part_create(PART_FIREBALL_SOFT, max(projtraillength, 1), proj.o, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*0.5f, 0.85f*trans, -5);
+                            part_create(PART_SMOKE_LERP, projtraillength*2, proj.o, 0x222222, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags)), trans, -10);
+                            proj.lasteffect = lastmillis - (lastmillis%(projtraildelay/2));
                         }
                         break;
                     }
@@ -1405,7 +1445,8 @@ namespace projs
                         }
                         else
                         {
-                            float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, proj.curscale, min(min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.movement), proj.o.dist(proj.from)));
+                            float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.o.dist(proj.from)));
+                            if(proj.lastbounce) size = min(size, max(proj.movement, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale));
                             if(size > 0)
                             {
                                 proj.to = vec(proj.o).sub(vec(proj.vel).normalize().mul(size));
@@ -1417,7 +1458,8 @@ namespace projs
                     }
                     case W_SMG:
                     {
-                        float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, proj.curscale, min(min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.movement), proj.o.dist(proj.from)));
+                        float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.o.dist(proj.from)));
+                        if(proj.lastbounce) size = min(size, max(proj.movement, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale));
                         if(size > 0)
                         {
                             proj.to = vec(proj.o).sub(vec(proj.vel).normalize().mul(size));
@@ -1440,7 +1482,7 @@ namespace projs
                         float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
                         if(expl > 0)
                         {
-                            part_explosion(proj.o, expl, PART_SHOCKBALL, 1, FWCOL(H, partcol, proj), 1.f, 0.95f);
+                            part_explosion(proj.o, expl, PART_SHOCKBALL, 1, FWCOL(H, partcol, proj), 1.f, trans);
                             if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) >= 1)
                                 part_explosion(proj.o, expl*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, 1, projhint(proj.owner, FWCOL(H, partcol, proj)), 1.f, 0.125f*projhintblend*trans);
                         }
@@ -1454,15 +1496,19 @@ namespace projs
                         if(projhints) part_create(PART_HINT_SOFT, 1, proj.o, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.lifesize*proj.curscale, 0.5f*projhintblend*trans);
                         break;
                     }
-                    case W_RIFLE:
+                    case W_RIFLE: case W_ZAPPER:
                     {
-                        float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, proj.curscale, min(min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.movement), proj.o.dist(proj.from)));
+                        vec from = type != W_ZAPPER || !proj.owner || proj.owner->weapselect != proj.weap || WK(proj.flags) ? proj.from : proj.owner->muzzlepos(proj.weap);
+                        float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.o.dist(from)));
+                        if(proj.lastbounce) size = min(size, max(proj.movement, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale));
                         if(size > 0)
                         {
-                            proj.to = vec(proj.o).sub(vec(proj.vel).normalize().mul(size));
-                            part_flare(proj.to, proj.o, 1, PART_FLARE, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, 0.85f*trans);
-                            if(projhints) part_flare(proj.to, proj.o, 1, PART_FLARE, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.curscale, projhintblend*trans);
-                            if(!WK(proj.flags) && W2(proj.weap, fragweap, WS(proj.flags)) >= 0)
+                            if(type != W_ZAPPER || !proj.owner || proj.owner->weapselect != proj.weap || WK(proj.flags))
+                                proj.to = vec(proj.o).sub(vec(proj.vel).normalize().mul(size));
+                            else proj.to = vec(proj.o).sub(vec(proj.o).sub(from).normalize().mul(size));
+                            part_flare(proj.to, proj.o, 1, type != W_ZAPPER ? PART_FLARE : PART_LIGHTZAP_FLARE, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, 0.85f*trans);
+                            if(projhints) part_flare(proj.to, proj.o, 1, type != W_ZAPPER ? PART_FLARE : PART_LIGHTZAP_FLARE, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.curscale, projhintblend*trans);
+                            if(type != W_ZAPPER && !WK(proj.flags) && W2(proj.weap, fragweap, WS(proj.flags)) >= 0)
                             {
                                 part_create(PART_PLASMA, 1, proj.o, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale*5, 0.5f*trans);
                                 if(projhints) part_create(PART_HINT_SOFT, 1, proj.o, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*5*projhintsize*proj.curscale, projhintblend*trans);
@@ -1494,7 +1540,7 @@ namespace projs
             }
             case PRJ_GIBS: case PRJ_DEBRIS:
             {
-                if(proj.projtype == PRJ_GIBS && game::bloodscale > 0)
+                if(proj.projtype == PRJ_GIBS && !game::nogore && game::bloodscale > 0)
                 {
                     if(proj.movement >= 1 && lastmillis-proj.lasteffect >= 1000 && proj.lifetime >= min(proj.lifemillis, proj.fadetime))
                     {
@@ -1510,7 +1556,7 @@ namespace projs
                     if(projtrails && lastmillis-proj.lasteffect >= projtraildelay) { effect = true; proj.lasteffect = lastmillis - (lastmillis%projtraildelay); }
                     int len = effect ? max(int(projtraillength*0.5f*max(1.f-proj.lifespan, 0.1f)), 1) : 1,
                         colour = !proj.id && isweap(proj.weap) ? FWCOL(H, explcol, proj) : pulsecols[PULSE_FIRE][rnd(PULSECOLOURS)];
-                    part_create(PART_FIREBALL, len, proj.o, colour, radius, blend, -5);
+                    part_create(proj.projtype == PRJ_GIBS || (!proj.id && isweap(proj.weap) && WF(WK(proj.flags), proj.weap, explcol, WS(proj.flags)) < -PULSE_DISCO) ? PART_SPARK : PART_FIREBALL, len, proj.o, colour, radius, blend, -5);
                 }
                 break;
             }
@@ -1551,116 +1597,111 @@ namespace projs
             {
                 updatetargets(proj, 2);
                 if(proj.projcollide&COLLIDE_PROJ) collideprojs.removeobj(&proj);
-                int vol = int(255*proj.curscale), type = WF(WK(proj.flags), proj.weap, parttype, WS(proj.flags));
+                int vol = clamp(int(255*proj.curscale), 0, 255), type = WF(WK(proj.flags), proj.weap, parttype, WS(proj.flags)), len = W2(proj.weap, partfade, WS(proj.flags));
                 if(!proj.limited) switch(type)
                 {
                     case W_PISTOL:
                     {
-                        vol = int(vol*(1.f-proj.lifespan));
-                        part_create(PART_SMOKE_LERP, 200, proj.o, 0x999999, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, 0.25f, -20);
+                        vol = clamp(int(vol*(1.f-proj.lifespan)), 0, 255);
+                        part_create(PART_SMOKE_LERP, len*2, proj.o, 0x999999, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), -20);
                         float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
                         if(expl > 0)
                         {
-                            part_explosion(proj.o, expl*0.5f, PART_EXPLOSION, 200, FWCOL(H, explcol, proj), 1.f, 0.95f);
-                            part_splash(PART_SPARK, 5, 250, proj.o, FWCOL(H, partcol, proj), 0.25f, 1, 1, 0, expl, 15);
+                            part_explosion(proj.o, expl*0.5f, PART_EXPLOSION, len, FWCOL(H, explcol, proj), 1.f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
+                            part_splash(PART_SPARK, 5, 250, proj.o, FWCOL(H, partcol, proj), 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), 1, 1, 0, expl, 15);
                             if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) >= 1)
-                                part_explosion(proj.o, expl*0.5f*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, 100, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.25f*projhintblend);
+                                part_explosion(proj.o, expl*0.5f*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, len/2, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags))*projhintblend);
                             adddecal(DECAL_SCORCH_SHORT, proj.o, proj.norm, expl*0.5f);
-                            adddynlight(proj.o, expl, FWCOL(P, explcol, proj), 150, 10);
+                            adddynlight(proj.o, expl, FWCOL(P, explcol, proj), len, 10);
                         }
-                        else adddecal(DECAL_BULLET, proj.o, proj.norm, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags)));
+                        else adddecal(DECAL_BULLET, proj.o, proj.norm, max(WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags)), 0.25f)*4);
                         break;
                     }
                     case W_FLAMER: case W_GRENADE: case W_MINE: case W_ROCKET:
                     { // all basically explosions
                         float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
-                        if(type == W_FLAMER)
+                        if(type != W_FLAMER) part_create(PART_PLASMA_SOFT, len, proj.o, FWCOL(H, partcol, proj), max(expl*0.5f, 0.5f), 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags))); // corona
+                        if(expl > 0)
                         {
-                            if(expl <= 0) expl = WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags));
-                            part_create(PART_SMOKE_LERP_SOFT, projtraillength, proj.o, 0x666666, expl*0.75f, 0.25f+(rnd(50)/100.f), -5);
+                            if(type != W_FLAMER) part_explosion(proj.o, expl, PART_EXPLOSION, len, FWCOL(H, explcol, proj), 1.f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
+                            part_splash(PART_SPARK, 20, len*2, proj.o, FWCOL(H, partcol, proj), 0.75f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), 1, 0, expl, 20);
+                            if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) >= 1)
+                                part_explosion(proj.o, expl*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, len/2, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags))*projhintblend);
                         }
-                        else
+                        else expl = max(WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags)), 0.25f)*4;
+                        part_create(PART_SMOKE_LERP_SOFT, len*3, proj.o, 0x444444, expl*0.75f, 0.75f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), type != W_FLAMER ? -15 : -10);
+                        if(type != W_FLAMER && !m_kaboom(game::gamemode, game::mutators) && game::nogore != 2 && game::debrisscale > 0)
                         {
-                            part_create(PART_PLASMA_SOFT, projtraillength*(type != W_ROCKET ? 2 : 3), proj.o, FWCOL(H, partcol, proj), max(expl*0.5f, 0.5f), 0.5f); // corona
-                            if(expl > 0)
-                            {
-                                int len = type != W_ROCKET ? 500 : 750;
-                                part_explosion(proj.o, expl, PART_EXPLOSION, len, FWCOL(H, explcol, proj), 1.f, 1);
-                                part_splash(PART_SPARK, 20, len*2, proj.o, FWCOL(H, partcol, proj), 0.75f, 1, 1, 0, expl, 20);
-                                if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) >= 1)
-                                    part_explosion(proj.o, expl*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, len/2, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.5f*projhintblend);
-                            }
-                            else expl = WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags));
-                            part_create(PART_SMOKE_LERP_SOFT, projtraillength*(type != W_ROCKET ? 3 : 4), proj.o, 0x333333, expl*0.75f, 0.5f, -15);
                             int debris = rnd(type != W_ROCKET ? 5 : 10)+5, amt = int((rnd(debris)+debris+1)*game::debrisscale);
-                            loopi(amt) create(proj.o, vec(proj.o).add(proj.vel), true, proj.owner, PRJ_DEBRIS, rnd(game::debrisfade)+game::debrisfade, 0, rnd(501), rnd(101)+50, 0, proj.weap, 0, proj.flags);
-                            adddecal(DECAL_ENERGY, proj.o, proj.norm, expl*0.75f, bvec::fromcolor(FWCOL(P, explcol, proj)));
+                            loopi(amt) create(proj.o, vec(proj.o).add(proj.vel), true, NULL, PRJ_DEBRIS, rnd(game::debrisfade)+game::debrisfade, 0, rnd(501), rnd(101)+50, 0, proj.weap, 0, proj.flags);
                         }
-                        if(type != W_FLAMER || WK(proj.flags) || W2(proj.weap, fragweap, WS(proj.flags))%W_MAX != W_FLAMER)
+                        adddecal(DECAL_ENERGY, proj.o, proj.norm, expl*0.75f, bvec::fromcolor(FWCOL(P, explcol, proj)));
+                        if(type != W_FLAMER || WK(proj.flags) || W2(proj.weap, fragweap, WS(proj.flags))%W_MAX != W_FLAMER) loopi(type != W_ROCKET ? 5 : 10)
                         {
-                            loopi(type != W_ROCKET ? 5 : 10)
-                            {
-                                vec to(proj.o);
-                                loopk(3) to.v[k] += expl*(rnd(201)-100)/200.f;
-                                part_create(PART_FIREBALL_SOFT, projtraillength*(type != W_ROCKET ? 2 : 3), to, FWCOL(H, explcol, proj), expl*1.25f, 0.5f+(rnd(50)/100.f), -5);
-                            }
+                            vec to(proj.o);
+                            loopk(3) to.v[k] += expl*(rnd(201)-100)/200.f;
+                            part_create(PART_FIREBALL_SOFT, len*2, to, FWCOL(H, explcol, proj), expl*1.25f, (0.5f+(rnd(50)/100.f))*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), -5);
                         }
                         adddecal(type == W_FLAMER ? DECAL_SCORCH_SHORT : DECAL_SCORCH, proj.o, proj.norm, expl*0.5f);
-                        adddynlight(proj.o, expl, FWCOL(P, explcol, proj), type != W_ROCKET ? 500 : 1000, 10);
+                        adddynlight(proj.o, expl, FWCOL(P, explcol, proj), len, 10);
                         break;
                     }
                     case W_SHOTGUN: case W_SMG:
                     {
-                        vol = int(vol*(1.f-proj.lifespan));
-                        part_splash(PART_SPARK, type == W_SHOTGUN ? 5 : 3, 250, proj.o, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale*0.5f, 1, 1, 0, 16, 15);
+                        vol = clamp(int(vol*(1.f-proj.lifespan)), 0, 255);
+                        part_splash(PART_SPARK, type == W_SHOTGUN ? 5 : 3, len*2, proj.o, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale*0.5f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), 1, 0, 16, 15);
                         float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
                         if(expl > 0)
                         {
-                            part_explosion(proj.o, expl*0.5f, PART_EXPLOSION, 250, FWCOL(H, explcol, proj), 1.f, 0.95f);
+                            part_explosion(proj.o, expl*0.5f, PART_EXPLOSION, len, FWCOL(H, explcol, proj), 1.f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
                             if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) >= 1)
-                                part_explosion(proj.o, expl*0.5f*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, 125, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.25f*projhintblend);
+                                part_explosion(proj.o, expl*0.5f*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, len/2, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags))*projhintblend);
                             adddecal(DECAL_SCORCH_SHORT, proj.o, proj.norm, expl*0.5f);
-                            adddynlight(proj.o, expl, FWCOL(P, explcol, proj), WS(proj.flags) ? 300 : 100, 10);
+                            adddynlight(proj.o, expl, FWCOL(P, explcol, proj), len, 10);
                         }
+                        else adddecal(DECAL_BULLET, proj.o, proj.norm, max(WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags)), 0.25f)*4);
                         break;
                     }
                     case W_PLASMA:
                     {
-                        int len = WS(proj.flags) ? 750 : 500;
                         float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
                         if(expl > 0)
                         {
-                            part_explosion(proj.o, expl, PART_SHOCKBALL, len, FWCOL(H, explcol, proj), 1.f, 0.95f);
+                            part_explosion(proj.o, expl, PART_SHOCKBALL, len, FWCOL(H, explcol, proj), 1.f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
                             if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) >= 1)
-                                part_explosion(proj.o, expl*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, len/2, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.125f*projhintblend);
+                                part_explosion(proj.o, expl*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, len/2, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.25f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags))*projhintblend);
                         }
-                        else expl = WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags));
-                        part_splash(PART_SPARK, 20, len*2, proj.o, FWCOL(H, partcol, proj), 0.25f, 1, 1, 0, expl, 20);
-                        part_create(PART_PLASMA_SOFT, len, proj.o, FWCOL(H, partcol, proj), expl*0.75f, 0.5f);
-                        part_create(PART_ELECTRIC_SOFT, len/2, proj.o, FWCOL(H, partcol, proj), expl*0.375f);
-                        part_create(PART_SMOKE, len, proj.o, FWCOL(H, partcol, proj), expl*0.35f, 0.35f, -30);
+                        else expl = max(WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags)), 0.25f)*4;
+                        part_splash(PART_SPARK, 20, len*2, proj.o, FWCOL(H, partcol, proj), 0.25f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), 1, 0, expl, 20);
+                        part_create(PART_PLASMA_SOFT, len, proj.o, FWCOL(H, partcol, proj), expl*0.75f, 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
+                        part_create(PART_ELECTRIC_SOFT, len/2, proj.o, FWCOL(H, partcol, proj), expl*0.375f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
+                        part_create(PART_SMOKE, len, proj.o, FWCOL(H, partcol, proj), expl*0.35f, 0.35f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), -30);
                         adddecal(DECAL_ENERGY, proj.o, proj.norm, expl*0.75f, bvec::fromcolor(FWCOL(P, explcol, proj)));
                         adddynlight(proj.o, 1.1f*expl, FWCOL(P, explcol, proj), len, 10);
                         break;
                     }
-                    case W_RIFLE:
+                    case W_RIFLE: case W_ZAPPER:
                     {
-                        float dist = proj.o.dist(proj.from), size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*proj.curscale, proj.curscale, min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.movement)),
-                            expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
-                        vec dir = dist >= size ? vec(proj.vel).normalize() : vec(proj.o).sub(proj.from).normalize();
-                        proj.to = vec(proj.o).sub(vec(dir).mul(size));
-                        int len = WS(proj.flags) ? 750 : 500;
+                        vec from = type != W_ZAPPER || !proj.owner || proj.owner->weapselect != proj.weap || WK(proj.flags) ? proj.from : proj.owner->muzzlepos(proj.weap);
+                        float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize),
+                              size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.o.dist(from)));
+                        if(proj.lastbounce) size = min(size, max(proj.movement, WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale));
+                        if(size > 0)
+                        {
+                            if(type != W_ZAPPER || !proj.owner || proj.owner->weapselect != proj.weap || WK(proj.flags))
+                                proj.to = vec(proj.o).sub(vec(proj.vel).normalize().mul(size));
+                            else proj.to = vec(proj.o).sub(vec(proj.o).sub(from).normalize().mul(size));
+                        }
                         if(expl > 0)
                         {
-                            part_create(PART_PLASMA_SOFT, len, proj.o, FWCOL(H, partcol, proj), expl*0.5f, 0.5f); // corona
-                            part_splash(PART_SPARK, 10, len*2, proj.o, FWCOL(H, partcol, proj), 0.25f, 1, 1, 0, expl, 15);
-                            part_explosion(proj.o, expl, PART_SHOCKBALL, len, FWCOL(H, explcol, proj), 1.f, 0.95f);
+                            part_create(type != W_ZAPPER ? PART_PLASMA_SOFT : PART_ELECZAP_SOFT, len, proj.o, FWCOL(H, partcol, proj), expl*0.5f, 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags))); // corona
+                            part_splash(PART_SPARK, 10, len*2, proj.o, FWCOL(H, partcol, proj), 0.25f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)), 1, 0, expl, 15);
+                            part_explosion(proj.o, expl, PART_SHOCKBALL, len, FWCOL(H, explcol, proj), 1.f, WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
                             if(WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)) >= 1)
-                                part_explosion(proj.o, expl*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, len/2, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.25f*projhintblend);
+                                part_explosion(proj.o, expl*WF(WK(proj.flags), proj.weap, wavepush, WS(proj.flags)), PART_SHOCKWAVE, len/2, projhint(proj.owner, FWCOL(H, explcol, proj)), 1.f, 0.5f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags))*projhintblend);
                         }
-                        else expl = WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags));
-                        part_flare(proj.to, proj.o, len, PART_FLARE, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, 0.85f);
-                        if(projhints) part_flare(proj.to, proj.o, len, PART_FLARE, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.curscale, projhintblend);
+                        part_flare(proj.to, proj.o, len, type != W_ZAPPER ? PART_FLARE : PART_LIGHTZAP_FLARE, FWCOL(H, partcol, proj), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*proj.curscale, 0.85f*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
+                        if(projhints) part_flare(proj.to, proj.o, len, type != W_ZAPPER ? PART_FLARE : PART_LIGHTZAP_FLARE, projhint(proj.owner, FWCOL(H, partcol, proj)), WF(WK(proj.flags), proj.weap, partsize, WS(proj.flags))*projhintsize*proj.curscale, projhintblend*WF(WK(proj.flags), proj.weap, partblend, WS(proj.flags)));
                         adddecal(DECAL_SCORCH, proj.o, proj.norm, max(expl, 2.f));
                         adddecal(DECAL_ENERGY, proj.o, proj.norm, max(expl*0.5f, 1.f), bvec::fromcolor(FWCOL(P, explcol, proj)));
                         adddynlight(proj.o, 1.1f*expl, FWCOL(P, explcol, proj), len, 10);
@@ -1668,23 +1709,24 @@ namespace projs
                     }
                     default: break;
                 }
-                if(!proj.limited && !WK(proj.flags) && vol > 0 && proj.weap != W_MELEE)
+                if(vol > 0 && !proj.limited && !WK(proj.flags))
                 {
                     int slot = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize) > 0 ? S_W_EXPLODE : S_W_DESTROY;
-                    if(vol) playsound(WSND2(proj.weap, WS(proj.flags), slot), proj.o, NULL, 0, vol);
+                    playsound(WSND2(proj.weap, WS(proj.flags), slot), proj.o, NULL, 0, vol);
                 }
                 if(proj.owner)
                 {
                     if(!WK(proj.flags) && !m_insta(game::gamemode, game::mutators) && W2(proj.weap, fragweap, WS(proj.flags)) >= 0)
                     {
                         int f = W2(proj.weap, fragweap, WS(proj.flags)), w = f%W_MAX,
-                            life = W2(proj.weap, fragtime, WS(proj.flags)), delay = 0;
-                        float mag = max(proj.vel.magnitude(), W2(proj.weap, flakminspeed, WS(proj.flags))),
+                            life = W2(proj.weap, fragtime, WS(proj.flags)), delay = W2(proj.weap, fragtimedelay, WS(proj.flags));
+                        float mag = max(proj.vel.magnitude(), W2(proj.weap, fragspeedmin, WS(proj.flags))),
                               scale = W2(proj.weap, fragscale, WS(proj.flags))*proj.curscale,
                               offset = proj.hit || proj.stick ? W2(proj.weap, fragoffset, WS(proj.flags)) : 1e-6f,
                               skew = proj.hit || proj.stuck ? W2(proj.weap, fragskew, WS(proj.flags)) : W2(proj.weap, fragspread, WS(proj.flags));
                         vec dir = vec(proj.stuck ? proj.norm : proj.vel).normalize(),
                             pos = vec(proj.o).sub(vec(dir).mul(offset));
+                        if(W2(proj.weap, fragspeedmax, WS(proj.flags)) > 0) mag = min(mag, W2(proj.weap, fragspeedmax, WS(proj.flags)));
                         if(W2(proj.weap, fragjump, WS(proj.flags)) > 0) life -= int(ceilf(life*W2(proj.weap, fragjump, WS(proj.flags))));
                         loopi(W2(proj.weap, fragrays, WS(proj.flags)))
                         {
@@ -1692,9 +1734,9 @@ namespace projs
                             if(W2(proj.weap, fragspeed, WS(proj.flags)) > 0)
                                 mag = rnd(W2(proj.weap, fragspeed, WS(proj.flags)))*0.5f+W2(proj.weap, fragspeed, WS(proj.flags))*0.5f;
                             if(skew > 0) to.add(vec(rnd(2001)-1000, rnd(2001)-1000, rnd(2001)-1000).normalize().mul(skew*mag));
-                            if(W2(proj.weap, fragrel, WS(proj.flags)) > 0) to.add(vec(dir).mul(W2(proj.weap, fragrel, WS(proj.flags))*mag));
+                            if(W2(proj.weap, fragrel, WS(proj.flags)) != 0) to.add(vec(dir).mul(W2(proj.weap, fragrel, WS(proj.flags))*mag));
                             create(pos, to, proj.local, proj.owner, PRJ_SHOT, max(life, 1), W2(proj.weap, fragtime, WS(proj.flags)), delay, W2(proj.weap, fragspeed, WS(proj.flags)), proj.id, w, -1, (f >= W_MAX ? HIT_ALT : 0)|HIT_FLAK, scale, true, &proj);
-                            delay += W2(proj.weap, fragdelay, WS(proj.flags));
+                            delay += W2(proj.weap, fragtimeiter, WS(proj.flags));
                         }
                     }
                     if(proj.local)
@@ -1721,30 +1763,30 @@ namespace projs
 
     int check(projent &proj, const vec &dir, int mat = -1)
     {
-        if(proj.o.z < 0) return 0; // remove, always..
+        if(proj.projtype == PRJ_SHOT ? proj.o.z < 0 : !insideworld(proj.o, false)) return 0; // remove, always..
         int chk = 0;
-        if(proj.extinguish&(1|2))
+        if(proj.extinguish&1 || proj.extinguish&2)
         {
-            if(mat < 0) mat = lookupmaterial(vec(proj.o.x, proj.o.y, proj.o.z + (proj.aboveeye - proj.height)/2));
+            if(mat < 0) mat = lookupmaterial(proj.o);
             if(proj.extinguish&1 && (mat&MATF_VOLUME) == MAT_WATER) chk |= 1;
             if(proj.extinguish&2 && ((mat&MATF_VOLUME) == MAT_LAVA || mat&MAT_DEATH)) chk |= 2;
         }
         if(chk)
         {
-            if(chk&1 && !proj.limited && !WK(proj.flags) && proj.weap != W_MELEE)
+            if(chk&1 && !proj.limited && !WK(proj.flags))// && proj.weap != W_MELEE)
             {
-                int vol = int(ceilf(48*proj.curscale)), snd = S_EXTINGUISH;
+                int vol = clamp(int(ceilf(48*proj.curscale)), 0, 255), snd = S_EXTINGUISH;
                 float size = max(proj.radius, 1.f);
                 if(proj.projtype == PRJ_SHOT && isweap(proj.weap))
                 {
                     snd = WSND2(proj.weap, WS(proj.flags), S_W_EXTINGUISH);
-                    vol = 10+int(245*proj.lifespan*proj.lifesize*proj.curscale);
+                    vol = clamp(10+int(245*proj.lifespan*proj.lifesize*proj.curscale), 0, 255);
                     float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
                     if(expl > 0) size *= expl*1.5f;
                     else size *= 2.5f;
                 }
                 else size *= 2.5f;
-                if(vol) playsound(snd, proj.o, NULL, 0, vol);
+                if(vol > 0) playsound(snd, proj.o, NULL, 0, vol);
                 part_create(PART_SMOKE, 500, proj.o, 0xAAAAAA, max(size, 1.5f), 1, -10);
                 proj.limited = true;
                 if(proj.projtype == PRJ_DEBRIS) proj.light.material[0] = bvec(255, 255, 255);
@@ -1759,11 +1801,12 @@ namespace projs
     {
         if((!d || dynent::is(d)) && (d ? proj.projcollide&(d->type == ENT_PROJ ? COLLIDE_SHOTS : COLLIDE_PLAYER) : proj.projcollide&COLLIDE_GEOM))
         {
+            proj.norm = norm;
             if(d)
             {
-                proj.norm = vec(proj.o).sub(d->center()).normalize();
+                /*if(proj.norm.iszero())*/ proj.norm = vec(proj.o).sub(d->center()).normalize();
                 if(proj.norm.iszero()) proj.norm = vec(proj.vel).normalize().neg();
-                if((d->type == ENT_AI || d->type == ENT_PLAYER) && proj.projcollide&IMPACT_PLAYER && proj.projcollide&STICK_PLAYER)
+                if(gameent::is(d) && proj.projcollide&IMPACT_PLAYER && proj.projcollide&STICK_PLAYER)
                 {
                     stick(proj, dir, (gameent *)d);
                     return 1;
@@ -1772,7 +1815,6 @@ namespace projs
             }
             else
             {
-                proj.norm = norm;
                 if(proj.norm.iszero()) proj.norm = vec(proj.vel).normalize().neg();
                 if(proj.projcollide&IMPACT_GEOM && proj.projcollide&STICK_GEOM)
                 {
@@ -1785,7 +1827,7 @@ namespace projs
                     loopi(WF(WK(proj.flags), proj.weap, drill, WS(proj.flags)))
                     {
                         proj.o.add(vec(dir).normalize());
-                        if(collide(&proj, dir, 0.f, proj.projcollide&COLLIDE_DYNENT) && !inside && !hitplayer) return 1;
+                        if(!collide(&proj, dir, 0.f, proj.projcollide&COLLIDE_DYNENT) && !collideinside && !hitplayer) return 1;
                     }
                     proj.o = orig; // continues below
                 }
@@ -1815,8 +1857,8 @@ namespace projs
         if(!ret) return 0;
         if(!skip && proj.interacts && checkitems(proj)) return -1;
         if(proj.projtype == PRJ_SHOT) updatetaper(proj, proj.distance+proj.o.dist(oldpos));
-        if(ret == 1 && (!collide(&proj, dir, 0.f, proj.projcollide&COLLIDE_DYNENT) || inside))
-            ret = impact(proj, dir, hitplayer, hitflags, wall);
+        if(ret == 1 && (collide(&proj, dir, 0.f, proj.projcollide&COLLIDE_DYNENT) || collideinside))
+            ret = impact(proj, dir, hitplayer, hitflags, collidewall);
         return ret;
     }
 
@@ -1855,9 +1897,9 @@ namespace projs
                     float x1 = floor(min(pos.x, to.x)), y1 = floor(min(pos.y, to.y)),
                           x2 = ceil(max(pos.x, to.x)), y2 = ceil(max(pos.y, to.y)),
                           maxdist = dir.magnitude(), dist = 1e16f;
-                    if(physics::xtracecollide(&proj, pos, to, x1, x2, y1, y2, maxdist, dist, proj.owner) || dist > maxdist) proj.escaped = true;
+                    if(!physics::xtracecollide(&proj, pos, to, x1, x2, y1, y2, maxdist, dist, proj.owner) || dist > maxdist) proj.escaped = true;
                 }
-                else if(physics::xcollide(&proj, dir, proj.owner)) proj.escaped = true;
+                else if(!physics::xcollide(&proj, dir, proj.owner)) proj.escaped = true;
             }
         }
     }
@@ -1865,8 +1907,8 @@ namespace projs
     bool moveproj(projent &proj, float secs, bool skip = false)
     {
         vec dir(proj.vel), pos(proj.o);
-        int mat = lookupmaterial(vec(proj.o.x, proj.o.y, proj.o.z + (proj.aboveeye - proj.height)/2));
-        if(isliquid(mat&MATF_VOLUME) && proj.waterfric > 0) dir.div(proj.waterfric);
+        int mat = lookupmaterial(pos);
+        if(isliquid(mat&MATF_VOLUME) && proj.liquidcoast > 0) dir.div(proj.liquidcoast);
         dir.mul(secs);
 
         if(!proj.escaped && proj.owner) escaped(proj, pos, dir);
@@ -1995,7 +2037,8 @@ namespace projs
             if(!targ.iszero())
             {
                 vec dir = vec(proj.vel).normalize();
-                float amt = clamp(bomberdelta*secs, 1e-8f, 1.f), mag = max(proj.vel.magnitude(), bomberminspeed);
+                float amt = clamp(bomberspeeddelta*secs, 1e-8f, 1.f), mag = max(proj.vel.magnitude(), bomberspeedmin);
+                if(bomberspeedmax > 0) mag = min(mag, bomberspeedmax);
                 dir.mul(1.f-amt).add(targ.mul(amt)).normalize();
                 if(!dir.iszero()) (proj.vel = dir).mul(mag);
             }
@@ -2049,7 +2092,7 @@ namespace projs
             }
             if(!proj.dest.iszero())
             {
-                float amt = clamp(WF(WK(proj.flags), proj.weap, delta, WS(proj.flags))*secs, 1e-8f, 1.f),
+                float amt = clamp(WF(WK(proj.flags), proj.weap, speeddelta, WS(proj.flags))*secs, 1e-8f, 1.f),
                       mag = max(proj.vel.magnitude(), physics::movevelocity(&proj));
                 dir.mul(1.f-amt).add(vec(proj.dest).sub(proj.o).normalize().mul(amt)).normalize();
                 if(!dir.iszero()) (proj.vel = dir).mul(mag);
@@ -2118,23 +2161,41 @@ namespace projs
         return true;
     }
 
+    struct canrem
+    {
+        projent *p;
+        float dist;
+
+        canrem(projent *p, float dist = 0) : p(p), dist(dist) {}
+        ~canrem() {}
+
+        static bool cmsort(const canrem *a, const canrem *b)
+        {
+            if(a->dist > b->dist) return true;
+            if(a->dist < b->dist) return false;
+            if(a->p->addtime < b->p->addtime) return true;
+            if(a->p->addtime > b->p->addtime) return false;
+            return false;
+        }
+    };
+
+
     void update()
     {
-        vector<projent *> canremove;
+        vector<canrem *> canremove;
         loopvrev(projs) if(projs[i]->projtype == PRJ_DEBRIS || projs[i]->projtype == PRJ_GIBS || projs[i]->projtype == PRJ_EJECT)
-            canremove.add(projs[i]);
-        while(!canremove.empty() && canremove.length() > maxprojectiles)
+            canremove.add(new canrem(projs[i], camera1->o.dist(projs[i]->o)));
+        int count = canremove.length()-maxprojectiles;
+        if(count > 0)
         {
-            int oldest = 0;
-            loopv(canremove) if(canremove[i]->addtime < canremove[oldest]->addtime) oldest = i;
-            if(canremove.inrange(oldest))
+            canremove.sort(canrem::cmsort);
+            loopi(count)
             {
-                canremove[oldest]->state = CS_DEAD;
-                canremove[oldest]->escaped = true;
-                canremove.removeunordered(oldest);
+                canremove[i]->p->state = CS_DEAD;
+                canremove[i]->p->escaped = true;
             }
-            else break;
         }
+        canremove.deletecontents();
 
         loopv(projs)
         {
@@ -2195,20 +2256,6 @@ namespace projs
                 proj.state = CS_DEAD;
                 proj.escaped = true;
             }
-            if(m_bomber(game::gamemode) && !m_gsp2(game::gamemode, game::mutators) && proj.projtype == PRJ_AFFINITY && !proj.beenused && bomber::st.flags.inrange(proj.id))
-            {
-                gameent *d = bomber::st.flags[proj.id].lastowner;
-                if(d && (d == game::player1 || d->ai))
-                {
-                    loopv(bomber::st.flags) if(!isbomberaffinity(bomber::st.flags[i]) && proj.o.dist(bomber::st.flags[i].spawnloc) <= enttype[AFFINITY].radius/2)
-                    {
-                        proj.beenused = 1;
-                        proj.lifetime = min(proj.lifetime, proj.fadetime);
-                        client::addmsg(N_SCOREAFFIN, "ri3", d->clientnum, proj.id, i);
-                        break;
-                    }
-                }
-            }
             if(proj.local && proj.owner && proj.projtype == PRJ_SHOT)
             {
                 float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
@@ -2232,13 +2279,10 @@ namespace projs
                             dynent *f = game::iterdynents(j);
                             if(!f || f->state != CS_ALIVE || !physics::issolid(f, &proj, true, false)) continue;
                             if(radial && radialeffect(f, proj, HIT_BURN, expl)) proj.lastradial = lastmillis;
-                            if(proxim == 1 && !proj.beenused && f != oldstick)
+                            if(proxim == 1 && !proj.beenused && f != oldstick && f->center().dist(proj.o) <= dist)
                             {
-                                if(f->center().dist(proj.o) <= dist)
-                                {
-                                    proj.beenused = 1;
-                                    proj.lifetime = min(proj.lifetime, WF(WK(proj.flags), proj.weap, proxtime, WS(proj.flags)));
-                                }
+                                proj.beenused = 1;
+                                proj.lifetime = min(proj.lifetime, WF(WK(proj.flags), proj.weap, proxtime, WS(proj.flags)));
                             }
                         }
                         if(proxim == 2 && !proj.beenused)
@@ -2249,7 +2293,7 @@ namespace projs
                             {
                                 dir.div(mag);
                                 float blocked = tracecollide(&proj, from, dir, mag, RAY_CLIPMAT|RAY_ALPHAPOLY, true);
-                                if(blocked >= 0 && hitplayer)
+                                if(blocked >= 0 && hitplayer && hitplayer->state == CS_ALIVE && physics::issolid(hitplayer, &proj, true, false))
                                 {
                                     proj.beenused = 1;
                                     proj.lifetime = min(proj.lifetime, WF(WK(proj.flags), proj.weap, proxtime, WS(proj.flags)));
@@ -2279,7 +2323,7 @@ namespace projs
                 if(!hits.empty())
                     client::addmsg(N_DESTROY, "ri7iv", proj.owner->clientnum, lastmillis-game::maptime, proj.weap, proj.flags, WK(proj.flags) ? -proj.id : proj.id,
                             int(expl*DNF), int(proj.curscale*DNF), hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf());
-                if(proj.weap == W_MELEE && WS(proj.flags)) proj.target = NULL;
+                //if(proj.weap == W_MELEE && WS(proj.flags)) proj.target = NULL;
             }
             if(proj.state == CS_DEAD)
             {
@@ -2386,7 +2430,7 @@ namespace projs
                         if(e.type == WEAPON)
                         {
                             flags |= MDL_LIGHTFX;
-                            int col = W(w_attr(game::gamemode, game::mutators, e.attrs[0], m_weapon(game::gamemode, game::mutators)), colour), interval = lastmillis%1000;
+                            int col = W(w_attr(game::gamemode, game::mutators, e.type, e.attrs[0], m_weapon(game::gamemode, game::mutators)), colour), interval = lastmillis%1000;
                             proj.light.effect = vec::hexcolor(col).mul(interval >= 500 ? (1000-interval)/500.f : interval/500.f);
                         }
                     }
@@ -2404,20 +2448,27 @@ namespace projs
         {
             projent &proj = *projs[i];
             float trans = fadeweap(proj);
-            if(trans > 0) switch(WF(WK(proj.flags), proj.weap, parttype, WS(proj.flags)))
+            int type = WF(WK(proj.flags), proj.weap, parttype, WS(proj.flags));
+            if(trans > 0) switch(type)
             {
-                case W_SWORD: adddynlight(proj.o, 16*trans, FWCOL(P, partcol, proj)); break;
-                case W_PISTOL: case W_SHOTGUN: case W_SMG: case W_RIFLE: if(proj.movement >= 1)
+                case W_SWORD: adddynlight(proj.o, 24*trans, FWCOL(P, partcol, proj)); break;
+                case W_PISTOL: case W_SHOTGUN: case W_SMG: case W_ZAPPER: case W_RIFLE:
                 {
-                    float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, proj.curscale, min(64.f, min(min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.movement), proj.o.dist(proj.from))));
-                    adddynlight(proj.o, 1.25f*size*trans, FWCOL(P, partcol, proj));
-                } break;
-                case W_PLASMA: case W_FLAMER: case W_GRENADE: case W_ROCKET:
+                    float expl = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
+                    if(type != W_ZAPPER || expl <= 0)
+                    {
+                        float size = clamp(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*(1.f-proj.lifespan)*proj.curscale, proj.curscale, min(16.f, min(min(WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags)), proj.movement), proj.o.dist(proj.from))));
+                        adddynlight(proj.o, 1.25f*size*trans, FWCOL(P, partcol, proj));
+                        break;
+                    }
+                }
+                case W_FLAMER: case W_PLASMA: case W_GRENADE: case W_ROCKET:
                 {
                     float size = WX(WK(proj.flags), proj.weap, explode, WS(proj.flags), game::gamemode, game::mutators, proj.curscale*proj.lifesize);
-                    if(size <= 0) size = WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*proj.lifesize*proj.curscale;
+                    if(size <= 0) size = WF(WK(proj.flags), proj.weap, partlen, WS(proj.flags))*proj.lifesize*proj.curscale*0.5f;
                     adddynlight(proj.o, 1.5f*size*trans, FWCOL(P, partcol, proj));
-                } break;
+                    break;
+                }
                 default: break;
             }
         }
diff --git a/src/game/scoreboard.cpp b/src/game/scoreboard.cpp
index 552cb35..7d78232 100644
--- a/src/game/scoreboard.cpp
+++ b/src/game/scoreboard.cpp
@@ -27,26 +27,38 @@ namespace hud
         vector<gameent *> players;
     };
     vector<scoregroup *> groups;
-    vector<gameent *> spectators;
+    scoregroup spectators;
 
-    VAR(IDF_PERSIST, autoscores, 0, 2, 3); // 1 = when dead, 2 = also in spectv, 3 = and in waittv too
+    VAR(IDF_PERSIST, autoscores, 0, 1, 3); // 1 = when dead, 2 = also in spectv, 3 = and in waittv too
     VAR(IDF_PERSIST, scoresdelay, 0, 0, VAR_MAX); // otherwise use respawn delay
     VAR(IDF_PERSIST, scoresinfo, 0, 1, 1);
     VAR(IDF_PERSIST, scorehandles, 0, 1, 1);
 
     VAR(IDF_PERSIST, scorepj, 0, 0, 1);
     VAR(IDF_PERSIST, scoreping, 0, 1, 1);
-    VAR(IDF_PERSIST, scorepoints, 0, 1, 1);
+    VAR(IDF_PERSIST, scorepoints, 0, 1, 2);
     VAR(IDF_PERSIST, scoretimer, 0, 1, 2);
-    VAR(IDF_PERSIST, scorelaps, 0, 1, 2);
     VAR(IDF_PERSIST, scorefrags, 0, 2, 2);
+    VAR(IDF_PERSIST, scoredeaths, 0, 2, 2);
+    VAR(IDF_PERSIST, scoreratios, 0, 0, 2);
     VAR(IDF_PERSIST, scoreclientnum, 0, 1, 1);
+    VAR(IDF_PERSIST, scoretimestyle, 0, 3, 4);
+    VAR(IDF_PERSIST, scoreracestyle, 0, 1, 4);
     VAR(IDF_PERSIST, scorebotinfo, 0, 0, 1);
     VAR(IDF_PERSIST, scorespectators, 0, 1, 1);
     VAR(IDF_PERSIST, scoreconnecting, 0, 0, 1);
     VAR(IDF_PERSIST, scorehostinfo, 0, 0, 1);
+    VAR(IDF_PERSIST, scoreipinfo, 0, 0, 1);
+    VAR(IDF_PERSIST, scoreverinfo, 0, 0, 1);
     VAR(IDF_PERSIST, scoreicons, 0, 1, 1);
-    VAR(IDF_PERSIST|IDF_HEX, scorehilight, 0, 0x888888, 0xFFFFFF);
+    VAR(IDF_PERSIST|IDF_HEX, scorehilight, 0, 0xFFFFFF, 0xFFFFFF);
+    VAR(IDF_PERSIST, scoreimage, 0, 1, 1);
+    FVAR(IDF_PERSIST, scoreimagesize, FVAR_NONZERO, 6, 10);
+    VAR(IDF_PERSIST, scoresideinfo, 0, 1, 1);
+    VAR(IDF_PERSIST, scorebgfx, 0, 1, 1);
+    VAR(IDF_PERSIST, scorebgrows, 0, 2, 2);
+    VAR(IDF_PERSIST, scorebgborder, 0, 1, 1);
+    FVAR(IDF_PERSIST, scorebgblend, 0, 0.5f, 1);
 
     static bool scoreson = false, scoresoff = false, shownscores = false;
     static int menustart = 0, menulastpress = 0;
@@ -57,7 +69,7 @@ namespace hud
         {
             if(game::player1->state == CS_DEAD)
             {
-                int delay = scoresdelay ? scoresdelay : m_delay(game::gamemode, game::mutators);
+                int delay = scoresdelay ? scoresdelay : m_delay(game::gamemode, game::mutators, game::player1->team);
                 if(!delay || lastmillis-game::player1->lastdeath > delay) return true;
             }
             else return game::tvmode() && autoscores >= (game::player1->state == CS_SPECTATOR ? 2 : 3);
@@ -67,26 +79,24 @@ namespace hud
 
     static inline bool playersort(const gameent *a, const gameent *b)
     {
-        if(a->state == CS_SPECTATOR)
+        if(a->state == CS_SPECTATOR || a->state == CS_EDITING)
         {
-            if(b->state == CS_SPECTATOR) return strcmp(a->name, b->name) < 0;
+            if(b->state == CS_SPECTATOR || b->state == CS_EDITING) return strcmp(a->name, b->name) < 0;
             else return false;
         }
-        else if(b->state == CS_SPECTATOR) return true;
+        else if(b->state == CS_SPECTATOR || b->state == CS_EDITING) return true;
         if(m_laptime(game::gamemode, game::mutators))
         {
             if((a->cptime && !b->cptime) || (a->cptime && b->cptime && a->cptime < b->cptime)) return true;
             if((b->cptime && !a->cptime) || (a->cptime && b->cptime && b->cptime < a->cptime)) return false;
         }
-        else if(m_gauntlet(game::gamemode))
-        {
-            if((a->cplaps && !b->cplaps) || (a->cplaps && b->cplaps && a->cplaps > b->cplaps)) return true;
-            if((b->cplaps && !a->cplaps) || (a->cplaps && b->cplaps && b->cplaps > a->cplaps)) return false;
-        }
         if(a->points > b->points) return true;
         if(a->points < b->points) return false;
-        if(a->frags > b->frags) return true;
-        if(a->frags < b->frags) return false;
+        if(!m_race(game::gamemode))
+        {
+            if(a->frags > b->frags) return true;
+            if(a->frags < b->frags) return false;
+        }
         return strcmp(a->name, b->name) < 0;
     }
 
@@ -99,8 +109,8 @@ namespace hud
         else if(!y->team) return true;
         if(m_laptime(game::gamemode, game::mutators))
         {
-            if(x->total < y->total) return true;
-            if(x->total > y->total) return false;
+            if((x->total && !y->total) || (x->total && y->total && x->total < y->total)) return true;
+            if((y->total && !x->total) || (x->total && y->total && x->total > y->total)) return false;
         }
         else
         {
@@ -115,15 +125,15 @@ namespace hud
     int groupplayers()
     {
         int numgroups = 0;
-        spectators.shrink(0);
+        spectators.players.shrink(0);
         int numdyns = game::numdynents();
         loopi(numdyns)
         {
             gameent *o = (gameent *)game::iterdynents(i);
-            if(!o || o->type != ENT_PLAYER || (!scoreconnecting && !o->name[0])) continue;
+            if(!o || o->actortype >= A_ENEMY || (!scoreconnecting && !o->name[0])) continue;
             if(o->state == CS_SPECTATOR)
             {
-                if(o != game::player1 || !client::demoplayback) spectators.add(o);
+                if(o != game::player1 || !client::demoplayback) spectators.players.add(o);
                 continue;
             }
             int team = m_fight(game::gamemode) && m_team(game::gamemode, game::mutators) ? o->team : T_NEUTRAL;
@@ -149,7 +159,7 @@ namespace hud
             g.players.add(o);
         }
         loopi(numgroups) groups[i]->players.sort(playersort);
-        spectators.sort(playersort);
+        spectators.players.sort(playersort);
         groups.sort(scoregroupcmp, 0, numgroups);
         return numgroups;
     }
@@ -185,7 +195,7 @@ namespace hud
                     {
                         if(numgroups > 1 && sg.total == groups[1]->total)
                         {
-                            mkstring(winner);
+                            string winner = "";
                             loopi(numgroups) if(i)
                             {
                                 if(sg.total == groups[i]->total)
@@ -195,9 +205,9 @@ namespace hud
                                 }
                                 else break;
                             }
-                            game::announcef(S_V_DRAW, CON_MESG, NULL, true, "\fw%s tied %swith a total score of: \fs\fc%s\fS", game::colourteam(sg.team), winner, m_laptime(game::gamemode, game::mutators) ? timestr(sg.total) : intstr(sg.total));
+                            game::announcef(S_V_DRAW, CON_MESG, NULL, true, "\fw%s tied %swith a total score of \fs\fc%s\fS", game::colourteam(sg.team), winner, m_laptime(game::gamemode, game::mutators) ? timestr(sg.total, scoreracestyle) : intstr(sg.total));
                         }
-                        else game::announcef(anc, CON_MESG, NULL, true, "\fwteam %s won the match with a total score of: \fs\fc%s\fS", game::colourteam(sg.team), m_laptime(game::gamemode, game::mutators) ? timestr(sg.total) : intstr(sg.total));
+                        else game::announcef(anc, CON_MESG, NULL, true, "\fwteam %s won the match with a total score of \fs\fc%s\fS", game::colourteam(sg.team), m_laptime(game::gamemode, game::mutators) ? timestr(sg.total, scoreracestyle) : intstr(sg.total));
                     }
                 }
                 else
@@ -207,7 +217,7 @@ namespace hud
                     {
                         if(sg.players.length() > 1 && sg.players[0]->cptime == sg.players[1]->cptime)
                         {
-                            mkstring(winner);
+                            string winner = "";
                             loopv(sg.players) if(i)
                             {
                                 if(sg.players[0]->cptime == sg.players[i]->cptime)
@@ -217,15 +227,15 @@ namespace hud
                                 }
                                 else break;
                             }
-                            game::announcef(S_V_DRAW, CON_MESG, NULL, true, "\fw%s tied %swith the fastest lap: \fs\fc%s\fS", game::colourname(sg.players[0]), winner, sg.players[0]->cptime ? timestr(sg.players[0]->cptime) : "dnf");
+                            game::announcef(S_V_DRAW, CON_MESG, NULL, true, "\fw%s tied %swith the fastest lap \fs\fc%s\fS", game::colourname(sg.players[0]), winner, sg.players[0]->cptime ? timestr(sg.players[0]->cptime, scoreracestyle) : "dnf");
                         }
-                        else game::announcef(anc, CON_MESG, NULL, true, "\fw%s won the match with the fastest lap: \fs\fc%s\fS", game::colourname(sg.players[0]), sg.players[0]->cptime ? timestr(sg.players[0]->cptime) : "dnf");
+                        else game::announcef(anc, CON_MESG, NULL, true, "\fw%s won the match with the fastest lap \fs\fc%s\fS", game::colourname(sg.players[0]), sg.players[0]->cptime ? timestr(sg.players[0]->cptime, scoreracestyle) : "dnf");
                     }
                     else
                     {
                         if(sg.players.length() > 1 && sg.players[0]->points == sg.players[1]->points)
                         {
-                            mkstring(winner);
+                            string winner = "";
                             loopv(sg.players) if(i)
                             {
                                 if(sg.players[0]->points == sg.players[i]->points)
@@ -235,9 +245,9 @@ namespace hud
                                 }
                                 else break;
                             }
-                            game::announcef(S_V_DRAW, CON_MESG, NULL, true, "\fw%s tied %swith a total score of: \fs\fc%d\fS", game::colourname(sg.players[0]), winner, sg.players[0]->points);
+                            game::announcef(S_V_DRAW, CON_MESG, NULL, true, "\fw%s tied %swith a total score of \fs\fc%d\fS", game::colourname(sg.players[0]), winner, sg.players[0]->points);
                         }
-                        else game::announcef(anc, CON_MESG, NULL, true, "\fw%s won the match with a total score of: \fs\fc%d\fS", game::colourname(sg.players[0]), sg.players[0]->points);
+                        else game::announcef(anc, CON_MESG, NULL, true, "\fw%s won the match with a total score of \fs\fc%d\fS", game::colourname(sg.players[0]), sg.players[0]->points);
                     }
                 }
             }
@@ -245,482 +255,635 @@ namespace hud
         else scoresoff = scoreson = false;
     }
 
-    const char *scorehost(gameent *d)
+    const char *scorehost(gameent *d, bool hostname = true)
     {
-        if(d->aitype > AI_NONE)
+        if(hostname && d->actortype > A_PLAYER)
         {
             static string hoststr;
             hoststr[0] = 0;
             gameent *e = game::getclient(d->ownernum);
             if(e)
             {
-                concatstring(hoststr, game::colourname(e));
-                concatstring(hoststr, ":");
+                concatstring(hoststr, game::colourname(e, NULL, false, false));
+                concatstring(hoststr, " ");
             }
             defformatstring(owner)("[%d]", d->ownernum);
             concatstring(hoststr, owner);
             return hoststr;
         }
-        return d->hostname;
+        return hostname ? d->hostname : d->hostip;
+    }
+
+    const char *scoreversion(gameent *d)
+    {
+        static string verstr;
+        formatstring(verstr)("%d.%d.%d-%s%d", d->version.major, d->version.minor, d->version.patch, plat_name(d->version.platform), d->version.arch);
+        return verstr;
     }
 
     void renderscoreboard(guient &g, bool firstpass)
     {
-        g.start(menustart, menuscale, NULL, false, false);
+        g.start(menustart, menuscale, NULL, false, false, scorebgfx!=0);
         int numgroups = groupplayers();
         uilist(g, {
-            g.image(NULL, 6, true);
-            g.space(2);
-            uicenter(g, {
+            if(scoresideinfo)
+            {
                 uicenterlist(g, {
+                    g.pushlist();
+                    g.space(0.5f);
+                    g.pushlist();
                     g.space(0.25f);
-                    uicenterlist(g, uifont(g, "emphasis", g.textf("%s", 0xFFFFFF, NULL, 0, *maptitle ? maptitle : mapname)));
-                    if(*mapauthor) uicenterlist(g, uifont(g, "default", g.textf("by %s", 0xFFFFFF, NULL, 0, mapauthor)));
-                    uicenterlist(g, uifont(g, "reduced", {
-                        defformatstring(gname)("%s", server::gamename(game::gamemode, game::mutators, 0, 32));
-                        g.textf("%s", 0xFFFFFF, NULL, 0, gname);
-                        if((m_play(game::gamemode) || client::demoplayback) && game::timeremaining >= 0)
-                        {
-                            if(game::intermission) g.textf(", \fs\fyintermission\fS", 0xFFFFFF, NULL, 0);
-                            else if(paused) g.textf(", \fs\fopaused\fS", 0xFFFFFF, NULL, 0);
-                            else if(game::timeremaining) g.textf(", \fs\fg%s\fS remain", 0xFFFFFF, NULL, 0, timestr(game::timeremaining, 2));
-                        }
-                    }));
-                    if(*connectname)
-                    {
-                        uicenterlist(g, {
-                            uifont(g, "little", g.textf("\fdon ", 0xFFFFFF, NULL, 0));
-                            if(*serverdesc) uifont(g, "reduced", g.textf("%s ", 0xFFFFFF, NULL, 0, serverdesc));
-                            uifont(g, "little", g.textf("\fd(\fa%s:[%d]\fd)", 0xFFFFFF, NULL, 0, connectname, connectport));
-                        });
-                    }
-                    if(game::player1->quarantine)
-                    {
-                        uicenterlist(g, uifont(g, "default", g.textf("You are \fzoyQUARANTINED", 0xFFFFFF, NULL, 0)));
-                        uicenterlist(g, uifont(g, "reduced", g.textf("Please await instructions from a moderator", 0xFFFFFF, NULL, 0)));
-                    }
-                    if(client::demoplayback)
-                    {
-                        uicenterlist(g, {
-                            uifont(g, "reduced", g.textf("Demo Playback in Progress", 0xFFFFFF, NULL, 0));
-                        });
-                    }
-                    else if(!game::intermission)
-                    {
-                        if(game::player1->state == CS_DEAD || game::player1->state == CS_WAITING)
-                        {
-                            SEARCHBINDCACHE(attackkey)("primary", 0);
-                            int sdelay = m_delay(game::gamemode, game::mutators);
-                            int delay = game::player1->respawnwait(lastmillis, sdelay);
-                            if(delay || m_duke(game::gamemode, game::mutators) || (m_fight(game::gamemode) && maxalive > 0))
+                    uicenter(g, {
+                        uicenterlist(g, uicenterlist(g, {
+                            uicenterlist(g, uifont(g, "emphasis", g.textf("%s", 0xFFFFFF, NULL, 0, -1, *maptitle ? maptitle : mapname)));
+                            if(*mapauthor)
                             {
-                                uicenterlist(g, uifont(g, "reduced", {
-                                    if(m_duke(game::gamemode, game::mutators)) g.textf("Queued for new round", 0xFFFFFF, NULL, 0);
-                                    else if(delay) g.textf("%s: Down for \fs\fy%s\fS", 0xFFFFFF, NULL, 0, game::player1->state == CS_WAITING ? "Please Wait" : "Fragged", timestr(delay, -1));
-                                    else if(game::player1->state == CS_WAITING && m_fight(game::gamemode) && maxalive > 0 && maxalivequeue)
+                                int len = strlen(mapauthor);
+                                uicenterlist(g, uifont(g, (len >= 24 ? (len >= 40 ? "tiny" : "little") : "reduced"), g.textf("by %s", 0xFFFFFF, NULL, 0, -1, mapauthor)));
+                            }
+                            uicenterlist(g, uifont(g, "little", {
+                                g.textf("\fy%s", 0xFFFFFF, NULL, 0, -1, server::gamename(game::gamemode, game::mutators, 0, 32));
+                                if(paused) g.text(", \fs\fopaused\fS", 0xFFFFFF);
+                                else if(m_play(game::gamemode) || client::demoplayback)
+                                {
+                                    int timecorrected = max(game::timeremaining*1000-(lastmillis-game::lasttimeremain), 0);
+                                    switch(game::gamestate)
                                     {
-                                        int n = game::numwaiting();
-                                        if(n) g.textf("Waiting for \fs\fy%d\fS %s", 0xFFFFFF, NULL, 0, n, n != 1 ? "players" : "player");
-                                        else g.textf("You are \fs\fgnext\fS in the queue", 0xFFFFFF, NULL, 0);
+                                        case G_S_WAITING: default: g.text(", \fs\fcwaiting\fS", 0xFFFFFF); break;
+                                        case G_S_VOTING: g.text(", \fs\fcvoting\fS", 0xFFFFFF); break;
+                                        case G_S_INTERMISSION: g.text(", \fs\fcintermission\fS", 0xFFFFFF); break;
+                                        case G_S_PLAYING: g.text(", \fs\fcplaying\fS", 0xFFFFFF); break;
+                                        case G_S_OVERTIME: g.text(", \fs\fcovertime\fS", 0xFFFFFF); break;
                                     }
+                                    g.textf(", \fs\fg%s\fS remain", 0xFFFFFF, NULL, 0, -1, timestr(timecorrected, scoretimestyle));
+                                }
+                            }));
+                            if(*connectname)
+                            {
+                                uicenterlist(g, uifont(g, "little", {
+                                    g.textf("\faon: \fw%s:[%d]", 0xFFFFFF, NULL, 0, -1, connectname, connectport);
                                 }));
-                                if(game::player1->state != CS_WAITING && lastmillis-game::player1->lastdeath >= 500)
-                                    uicenterlist(g, uifont(g, "little", g.textf("Press \fs\fc%s\fS to enter respawn queue", 0xFFFFFF, NULL, 0, attackkey)));
+                                if(*serverdesc)
+                                {
+                                    uicenterlist(g, uifont(g, (strlen(serverdesc) >= 32 ? "tiny" : "little"), {
+                                        g.textf("\"\fs%s\fS\"", 0xFFFFFF, NULL, 0, -1, serverdesc);
+                                    }));
+                                }
+                            }
+                        }));
+                        if(scoreimage)
+                        {
+                            g.space(0.25f);
+                            uicenterlist(g, {
+                                g.image(NULL, scoreimagesize, true);
+                            });
+                        }
+                        g.space(0.25f);
+                        uicenterlist(g, uicenterlist(g, {
+                            if(game::player1->quarantine)
+                            {
+                                uicenterlist(g, uifont(g, "default", g.text("You are \fzoyQUARANTINED", 0xFFFFFF)));
+                                uicenterlist(g, uifont(g, "reduced", g.text("Please await instructions from a moderator", 0xFFFFFF)));
                             }
-                            else
+                            if(client::demoplayback)
                             {
-                                uicenterlist(g, uifont(g, "reduced", g.textf("Ready to respawn", 0xFFFFFF, NULL, 0)));
-                                if(game::player1->state != CS_WAITING) uicenterlist(g, uifont(g, "little", g.textf("Press \fs\fc%s\fS to respawn now", 0xFFFFFF, NULL, 0, attackkey)));
+                                uicenterlist(g, {
+                                    uifont(g, "default", g.text("Demo Playback in Progress", 0xFFFFFF));
+                                });
                             }
-                            if(shownotices >= 2)
+                            if(gs_playing(game::gamestate) && !client::demoplayback)
                             {
-                                uifont(g, "little", {
-                                    if(game::player1->state == CS_WAITING && lastmillis-game::player1->lastdeath >= 500)
+                                if(game::player1->state == CS_DEAD || game::player1->state == CS_WAITING)
+                                {
+                                    SEARCHBINDCACHE(attackkey)("primary", 0);
+                                    int sdelay = m_delay(game::gamemode, game::mutators, game::player1->team);
+                                    int delay = game::player1->respawnwait(lastmillis, sdelay);
+                                    if(delay || m_duke(game::gamemode, game::mutators) || (m_fight(game::gamemode) && maxalive > 0))
                                     {
-                                        SEARCHBINDCACHE(waitmodekey)("waitmodeswitch", 3);
-                                        uicenterlist(g, g.textf("Press \fs\fc%s\fS to %s", 0xFFFFFF, NULL, 0, waitmodekey, game::tvmode() ? "interact" : "switch to TV"));
+                                        uicenterlist(g, uifont(g, "default", {
+                                            if(game::gamestate == G_S_WAITING || m_duke(game::gamemode, game::mutators)) g.text("Queued for new round", 0xFFFFFF);
+                                            else if(delay) g.textf("%s: Down for \fs\fy%s\fS", 0xFFFFFF, NULL, 0, -1, game::player1->state == CS_WAITING ? "Please Wait" : "Fragged", timestr(delay));
+                                            else if(game::player1->state == CS_WAITING && m_fight(game::gamemode) && maxalive > 0 && maxalivequeue)
+                                            {
+                                                int n = game::numwaiting();
+                                                if(n) g.textf("Waiting for \fs\fy%d\fS %s", 0xFFFFFF, NULL, 0, -1, n, n != 1 ? "players" : "player");
+                                                else g.text("You are \fs\fgnext\fS in the queue", 0xFFFFFF);
+                                            }
+                                        }));
+                                        if(game::player1->state != CS_WAITING && lastmillis-game::player1->lastdeath >= 500)
+                                            uicenterlist(g, uifont(g, "little", g.textf("Press %s to enter respawn queue", 0xFFFFFF, NULL, 0, -1, attackkey)));
                                     }
-                                    if(m_loadout(game::gamemode, game::mutators))
+                                    else
                                     {
-                                        SEARCHBINDCACHE(loadkey)("showgui loadout", 0);
-                                        uicenterlist(g, g.textf("Press \fs\fc%s\fS to \fs%s\fS loadout", 0xFFFFFF, NULL, 0, loadkey, game::player1->loadweap.empty() ? "\fzoyselect" : "change"));
+                                        uicenterlist(g, uifont(g, "default", g.text("Ready to respawn", 0xFFFFFF)));
+                                        if(game::player1->state != CS_WAITING) uicenterlist(g, uifont(g, "little", g.textf("Press %s to respawn now", 0xFFFFFF, NULL, 0, -1, attackkey)));
                                     }
-                                    if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
+                                    if(shownotices >= 2)
                                     {
-                                        SEARCHBINDCACHE(teamkey)("showgui team", 0);
-                                        uicenterlist(g, g.textf("Press \fs\fc%s\fS to change teams", 0xFFFFFF, NULL, 0, teamkey));
+                                        uifont(g, "little", {
+                                            if(game::player1->state == CS_WAITING && lastmillis-game::player1->lastdeath >= 500)
+                                            {
+                                                SEARCHBINDCACHE(waitmodekey)("waitmodeswitch", 3);
+                                                uicenterlist(g, g.textf("Press %s to %s", 0xFFFFFF, NULL, 0, -1, waitmodekey, game::tvmode() ? "interact" : "switch to TV"));
+                                            }
+                                            if(m_loadout(game::gamemode, game::mutators))
+                                            {
+                                                SEARCHBINDCACHE(loadkey)("showgui profile 2", 0);
+                                                uicenterlist(g, g.textf("Press %s to \fs%s\fS loadout", 0xFFFFFF, NULL, 0, -1, loadkey, game::player1->loadweap.empty() ? "\fzoyselect" : "change"));
+                                            }
+                                            if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
+                                            {
+                                                SEARCHBINDCACHE(teamkey)("showgui team", 0);
+                                                uicenterlist(g, g.textf("Press %s to change teams", 0xFFFFFF, NULL, 0, -1, teamkey));
+                                            }
+                                        });
                                     }
-                                });
-                            }
-                        }
-                        else if(game::player1->state == CS_ALIVE)
-                        {
-                            uifont(g, "reduced", {
-                                uicenterlist(g, {
-                                    // In two cases, the main mode-description is not applicable
-                                    if(m_bomber(game::gamemode) && m_gsp2(game::gamemode, game::mutators)) // hold bomber
-                                        g.textf("%s", 0xFFFFFF, NULL, 0, gametype[game::gamemode].gsd[1]);
-                                    else if(m_capture(game::gamemode) && m_gsp3(game::gamemode, game::mutators)) // protect capture
-                                        g.textf("%s", 0xFFFFFF, NULL, 0, gametype[game::gamemode].gsd[2]);
-                                    else if(m_gauntlet(game::gamemode) && m_gsp1(game::gamemode, game::mutators)) // timed gauntlet
-                                        g.textf("%s", 0xFFFFFF, NULL, 0, gametype[game::gamemode].gsd[0]);
-                                    else g.textf("%s", 0xFFFFFF, NULL, 0, gametype[game::gamemode].desc);
-                                });
-                                if(m_team(game::gamemode, game::mutators))
-                                    uicenterlist(g, g.textf("Playing for team %s", 0xFFFFFF, NULL, 0, game::colourteam(game::player1->team)));
-                            });
-                        }
-                        else if(game::player1->state == CS_SPECTATOR)
-                        {
-                            uicenterlist(g, uifont(g, "reduced", g.textf("%s", 0xFFFFFF, NULL, 0, game::tvmode() ? "SpecTV" : "Spectating")));
-                            SEARCHBINDCACHE(speconkey)("spectator 0", 1);
-                            uifont(g, "little", {
-                                uicenterlist(g, g.textf("Press \fs\fc%s\fS to join the game", 0xFFFFFF, NULL, 0, speconkey));
-                                if(!m_edit(game::gamemode) && shownotices >= 2)
+                                }
+                                else if(game::player1->state == CS_ALIVE)
                                 {
-                                    SEARCHBINDCACHE(specmodekey)("specmodeswitch", 1);
-                                    uicenterlist(g, g.textf("Press \fs\fc%s\fS to %s", 0xFFFFFF, NULL, 0, specmodekey, game::tvmode() ? "interact" : "switch to TV"));
+                                    uifont(g, "default", uicenterlist(g, {
+                                        if(m_team(game::gamemode, game::mutators))
+                                            g.textf("Playing for team %s", 0xFFFFFF, NULL, 0, -1, game::colourteam(game::player1->team));
+                                        else g.text("Playing free-for-all", 0xFFFFFF);
+                                    }));
+                                }
+                                else if(game::player1->state == CS_SPECTATOR)
+                                {
+                                    uifont(g, "default", uicenterlist(g, {
+                                        g.textf("You are %s", 0xFFFFFF, NULL, 0, -1, game::tvmode() ? "watching SpecTV" : "a spectator");
+                                    }));
+                                    SEARCHBINDCACHE(speconkey)("spectator 0", 1);
+                                    uifont(g, "little", {
+                                        uicenterlist(g, g.textf("Press %s to join the game", 0xFFFFFF, NULL, 0, -1, speconkey));
+                                        if(!m_edit(game::gamemode) && shownotices >= 2)
+                                        {
+                                            SEARCHBINDCACHE(specmodekey)("specmodeswitch", 1);
+                                            uicenterlist(g, g.textf("Press %s to %s", 0xFFFFFF, NULL, 0, -1, specmodekey, game::tvmode() ? "interact" : "switch to TV"));
+                                        }
+                                    });
                                 }
-                            });
-                        }
 
-                        if(m_edit(game::gamemode) && (game::focus->state != CS_EDITING || shownotices >= 4))
-                        {
-                            SEARCHBINDCACHE(editkey)("edittoggle", 1);
-                            uicenterlist(g, uifont(g, "reduced", g.textf("Press \fs\fc%s\fS to %s editmode", 0xFFFFFF, NULL, 0, editkey, game::focus->state != CS_EDITING ? "enter" : "exit")));
-                        }
-                    }
+                                if(m_edit(game::gamemode) && (game::player1->state != CS_EDITING || shownotices >= 4))
+                                {
+                                    SEARCHBINDCACHE(editkey)("edittoggle", 1);
+                                    uicenterlist(g, uifont(g, "reduced", g.textf("Press %s to %s editmode", 0xFFFFFF, NULL, 0, -1, editkey, game::player1->state != CS_EDITING ? "enter" : "exit")));
+                                }
+                            }
 
-                    SEARCHBINDCACHE(scoreboardkey)("showscores", 1);
-                    uicenterlist(g, uifont(g, "little", g.textf("%s \fs\fc%s\fS to close this window", 0xFFFFFF, NULL, 0, scoresoff ? "Release" : "Press", scoreboardkey)));
-                    uicenterlist(g, uifont(g, "tiny", g.textf("Double-tap to keep the window open", 0xFFFFFF, NULL, 0)));
-                });
-            });
-        });
-        g.space(0.5f);
-        #define loopscoregroup(b) \
-        { \
-            loopv(sg.players) \
-            { \
-                gameent *o = sg.players[i]; \
-                b; \
-            } \
-        }
-        uifont(g, numgroups > 1 ? "little" : "reduced", {
-            float namepad = 0;
-            float handlepad = 0;
-            float hostpad = 0;
-            bool hashandle = false;
-            bool hashost = false;
-            bool hasbots = false;
-            loopk(numgroups)
-            {
-                scoregroup &sg = *groups[k];
-                loopscoregroup({
-                    if(scorebotinfo && o->aitype > AI_NONE) hasbots = true;
-                    namepad = max(namepad, (float)text_width(game::colourname(o, NULL, false, true)));
-                    if(scorehandles && o->handle[0])
-                    {
-                        handlepad = max(handlepad, (float)text_width(o->handle));
-                        hashandle = true;
-                    }
-                    if(scorehostinfo)
-                    {
-                        const char *host = scorehost(o);
-                        if(host && *host)
-                        {
-                            hostpad = max(hostpad, (float)text_width(host));
-                            if(o->ownernum != game::player1->clientnum) hashost = true;
-                        }
-                    }
+                            SEARCHBINDCACHE(scoreboardkey)("showscores", 1);
+                            uicenterlist(g, uifont(g, "little", g.textf("%s %s to close this window", 0xFFFFFF, NULL, 0, -1, scoresoff ? "Release" : "Press", scoreboardkey)));
+                            uicenterlist(g, uifont(g, "tiny", g.text("Double-tap to keep the window open", 0xFFFFFF)));
+
+                            if(m_play(game::gamemode) && game::player1->state != CS_SPECTATOR && (!gs_playing(game::gamestate) || scoresinfo))
+                            {
+                                float ratio = game::player1->frags >= game::player1->deaths ? (game::player1->frags/float(max(game::player1->deaths, 1))) : -(game::player1->deaths/float(max(game::player1->frags, 1)));
+                                uicenterlist(g, uifont(g, "little", {
+                                    g.textf("\fs\fg%d\fS %s, \fs\fg%d\fS %s (\fs\fy%.1f\fS:\fs\fy%.1f\fS) \fs\fg%d\fS damage", 0xFFFFFF, NULL, 0, -1,
+                                        game::player1->frags, game::player1->frags != 1 ? "frags" : "frag",
+                                        game::player1->deaths, game::player1->deaths != 1 ? "deaths" : "death", ratio >= 0 ? ratio : 1.f, ratio >= 0 ? 1.f : -ratio,
+                                        game::player1->totaldamage);
+                                }));
+                            }
+                        }));
+                    });
+                    g.space(0.25f);
+                    g.poplist();
+                    g.space(0.5f);
+                    g.poplist();
                 });
+                g.space(1);
             }
-            namepad = max((namepad-text_width("name"))/float(guibound[0]), 0.25f);
-            if(hashandle) handlepad = max((handlepad-guibound[0])/float(guibound[0]), 0.25f);
-            if(hashost) hostpad = max((hostpad-guibound[0])/float(guibound[0]), 0.25f);
-            loopk(numgroups)
-            {
-                if((k%2)==0)
-                {
-                    if(k) g.space(0.5f);
-                    g.pushlist();
-                }
+            uicenterlist(g, {
                 uicenter(g, {
-                    scoregroup &sg = *groups[k];
-                    int bgcolor = sg.team && m_fight(game::gamemode) && m_team(game::gamemode, game::mutators) ? TEAM(sg.team, colour) : 0x333333;
-                    if(sg.team && m_team(game::gamemode, game::mutators))
-                    {
-                        g.pushlist();
-                        uilist(g, {
-                            g.background(bgcolor);
-                            if(m_defend(game::gamemode) && ((defendlimit && sg.total >= defendlimit) || sg.total == INT_MAX))
-                                g.textf("%s: WIN", 0xFFFFFF, teamtexname(sg.team), TEAM(sg.team, colour), TEAM(sg.team, name));
-                            else if(m_laptime(game::gamemode, game::mutators)) g.textf("%s: %s", 0xFFFFFF, teamtexname(sg.team), TEAM(sg.team, colour), TEAM(sg.team, name), sg.total ? timestr(sg.total) : "\fadnf");
-                            else g.textf("%s: %d", 0xFFFFFF, teamtexname(sg.team), TEAM(sg.team, colour), TEAM(sg.team, name), sg.total);
-                            g.spring();
-                        });
-                        g.pushlist();
+                    int ngroup = numgroups;
+                    if(scorespectators && !spectators.players.empty()) ngroup++;
+                    #define loopscorelist(b) \
+                    { \
+                        int _n = sg.players.length(); \
+                        loopi(_n) if(sg.players[i]) \
+                        { \
+                            b; \
+                        } \
                     }
-
-                    uilist(g, {
-                        uicenterlist(g, uipad(g, 0.25f, g.strut(1)));
-                        loopscoregroup(uicenterlist(g, {
-                            uipad(g, 0.25f, uicenterlist(g, g.textf("\f[%d]\f($priv%stex)", 0xFFFFFF, NULL, 0, game::findcolour(o), hud::privname(o->privilege, o->aitype))));
-                        }));
-                    });
-
-                    uilist(g, {
-                        uicenterlist(g, uipad(g, namepad, uicenterlist(g, g.text("name", 0xFFFFFF))));
-                        loopscoregroup(uicenterlist(g, {
-                            uipad(g, 0.25f, uicenterlist(g, {
-                                if(o == game::player1 && scorehilight) g.background(scorehilight);
-                                uilist(g, uipad(g, 0.25f, g.textf("%s", 0xFFFFFF, NULL, 0, game::colourname(o, NULL, false, true))));
-                            }));
-                        }));
-                    });
-
-                    if(scorepoints)
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, 1, g.text("points", 0xFFFFFF)));
-                            loopscoregroup(uicenterlist(g, {
-                                uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, o->points));
-                            }));
-                        });
+                    #define loopscoregroup(b) \
+                    { \
+                        loopv(sg.players) if(sg.players[i]) \
+                        { \
+                            gameent *o = sg.players[i]; \
+                            b; \
+                        } \
                     }
-
-                    if(m_trial(game::gamemode) || m_gauntlet(game::gamemode))
-                    {
-                        if(scoretimer && (scoretimer >= 2 || m_laptime(game::gamemode, game::mutators)))
+                    uifont(g, ngroup > 1 ? "little" : "reduced", {
+                        float namepad = 0;
+                        float handlepad = 0;
+                        float ippad = 0;
+                        float hostpad = 0;
+                        float verpad = 0;
+                        bool hashandle = false;
+                        bool hasip = false;
+                        bool hashost = false;
+                        bool hasver = false;
+                        bool hasbots = false;
+                        loopk(ngroup)
                         {
-                            uilist(g, {
-                                uicenterlist(g, uipad(g, 4, g.text("best", 0xFFFFFF)));
-                                loopscoregroup(uicenterlist(g, {
-                                    uipad(g, 0.5f, g.textf("%s", 0xFFFFFF, NULL, 0, o->cptime ? timestr(o->cptime) : "\fadnf"))
-                                }));
+                            scoregroup &sg = k == numgroups ? spectators : *groups[k];
+                            loopscoregroup({
+                                if(scorebotinfo && o->actortype > A_PLAYER) hasbots = true;
+                                namepad = max(namepad, (float)text_width(game::colourname(o, NULL, false, true))/FONTW*0.51f);
+                                if(scorehandles && o->handle[0])
+                                {
+                                    handlepad = max(handlepad, (float)text_width(o->handle)/FONTW*0.51f);
+                                    hashandle = true;
+                                }
+                                if(scoreipinfo)
+                                {
+                                    const char *host = scorehost(o, false);
+                                    if(host && *host)
+                                    {
+                                        ippad = max(ippad, (float)text_width(host)/FONTW*0.51f);
+                                        if(o->ownernum != game::player1->clientnum) hasip = true;
+                                    }
+                                }
+                                if(scorehostinfo)
+                                {
+                                    const char *host = scorehost(o, true);
+                                    if(host && *host)
+                                    {
+                                        hostpad = max(hostpad, (float)text_width(host)/FONTW*0.51f);
+                                        if(o->ownernum != game::player1->clientnum) hashost = true;
+                                    }
+                                }
+                                if(scoreverinfo)
+                                {
+                                    const char *ver = scoreversion(o);
+                                    if(ver && *ver)
+                                    {
+                                        verpad = max(verpad, (float)text_width(ver)/FONTW*0.51f);
+                                        if(o->ownernum != game::player1->clientnum) hasver = true;
+                                    }
+                                }
                             });
                         }
-                        if(scorelaps && (scorelaps >= 2 || !m_laptime(game::gamemode, game::mutators)))
+                        loopk(ngroup)
                         {
-                            uilist(g, {
-                                uicenterlist(g, uipad(g, 4, g.text("laps", 0xFFFFFF)));
-                                loopscoregroup(uicenterlist(g, {
-                                    uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, o->cplaps))
+                            scoregroup &sg = k == numgroups ? spectators : *groups[k];
+                            if(k) g.space(0.5f);
+                            int colour = k < numgroups && sg.team >= 0 && m_fight(game::gamemode) && m_team(game::gamemode, game::mutators) ? TEAM(sg.team, colour) : TEAM(T_NEUTRAL, colour);
+                            vec c = vec::hexcolor(colour);
+                            int bgcolor = vec(c).mul(k == numgroups ? 0.45f : 0.65f).tohexcolor();
+                            int bgc1 = vec(c).mul(k == numgroups ? 0.1f : 0.25f).tohexcolor();
+                            int bgc2 = vec(c).mul(k == numgroups ? 0.25f : 0.45f).tohexcolor();
+                            uicenterlist(g, {
+                                if(scorebgrows) g.background(bgcolor, scorebgblend, bgcolor, scorebgblend);
+                                g.space(0.5f);
+                                g.pushlist();
+                                g.space(0.25f);
+                                uilist(g, uifont(g, "default", {
+                                    if(scorebgrows) g.background(bgc2, scorebgblend, bgc2, scorebgblend);
+                                    g.space(0.15f);
+                                    if(k == numgroups)
+                                    {
+                                        g.text("spectators", 0xFFFFFF, spectatortex, colour);
+                                        g.spring();
+                                    }
+                                    else if(sg.team > 0 && m_team(game::gamemode, game::mutators))
+                                    {
+                                        g.textf("team %s", 0xFFFFFF, teamtexname(sg.team), colour, -1, TEAM(sg.team, name));
+                                        g.spring();
+                                        if(m_defend(game::gamemode) && ((defendlimit && sg.total >= defendlimit) || sg.total == INT_MAX)) g.text("WINNER", 0xFFFFFF);
+                                        else if(m_laptime(game::gamemode, game::mutators)) g.textf("%s", 0xFFFFFF, NULL, 0, -1, sg.total ? timestr(sg.total, scoreracestyle) : "\fadnf");
+                                        else if(m_race(game::gamemode)) g.textf("%d %s", 0xFFFFFF, NULL, 0, -1, sg.total, sg.total != 1 ? "laps" : "lap");
+                                        else g.textf("%d %s", 0xFFFFFF, NULL, 0, -1, sg.total, sg.total != 1 ? "points" : "point");
+                                    }
+                                    else
+                                    {
+                                        g.text("free-for-all", 0xFFFFFF, playertex, colour);
+                                        g.spring();
+                                    }
+                                    g.space(0.25f);
                                 }));
-                            });
-                        }
-                    }
-
-                    if(scorefrags && (scorefrags >= 2 || m_dm(game::gamemode)))
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, 1, g.text("frags", 0xFFFFFF)));
-                            loopscoregroup(uicenterlist(g, {
-                                uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, o->frags))
-                            }));
-                        });
-                    }
-
-                    if(scorepj)
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, 2, g.text("pj", 0xFFFFFF)));
-                            loopscoregroup(uicenterlist(g, {
-                                uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, o->plag))
-                            }));
-                        });
-                    }
+                                g.pushlist();
+                                uilist(g, {
+                                    uilist(g, {
+                                        uicenter(g, uipad(g, 0.25f, g.space(1); g.strut(1)));
+                                    });
+                                    loopscoregroup(uilist(g, {
+                                        uicenter(g, uipad(g, 0.25f, uicenterlist(g, g.textf("\f($priv%stex)", game::findcolour(o), NULL, 0, -1, server::privnamex(o->privilege, o->actortype, true)))));
+                                    }));
+                                });
 
-                    if(scoreping)
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, 2, g.text("ping", 0xFFFFFF)));
-                            loopscoregroup(uicenterlist(g, {
-                                uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, o->ping))
-                            }));
-                        });
-                    }
+                                uilist(g, {
+                                    uilist(g, {
+                                        uicenter(g, uipad(g, namepad, uicenterlist(g, g.text("name", 0xFFFFFF))));
+                                    });
+                                    loopscoregroup(uilist(g, {
+                                        if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                        uicenter(g, uipad(g, 0.25f, uicenterlist(g, g.textf("%s", 0xFFFFFF, NULL, 0, -1, game::colourname(o, NULL, false, true, scorebgrows >= 2 || (scorehilight && o == game::player1) ? 0 : 3)))));
+                                    }));
+                                });
 
-                    if(scoreclientnum || game::player1->privilege >= PRIV_ELEVATED)
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, 1, g.text("cn", 0xFFFFFF)));
-                            loopscoregroup(uicenterlist(g, {
-                                uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, o->clientnum))
-                            }));
-                        });
-                    }
+                                if(scorepoints >= (m_laptime(game::gamemode, game::mutators) ? 2 : 1))
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 1, g.text("points", 0xFFFFFF)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, -1, o->points)));
+                                        }));
+                                    });
+                                }
 
-                    if(scorebotinfo && hasbots)
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, 1, g.text("sk", 0xFFFFFF)));
-                            loopscoregroup(uicenterlist(g, {
-                                uipad(g, 0.5f, {
-                                    if(o->aitype > AI_NONE) g.textf("%d", 0xFFFFFF, NULL, 0, o->skill);
-                                    else g.strut(1);
-                                });
-                            }));
-                        });
-                    }
-                    if(scorehandles && hashandle)
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, handlepad, g.strut(1)));
-                            loopscoregroup({
-                                uicenterlist(g, {
-                                    uipad(g, 0.5f, g.textf("%s", 0xFFFFFF, NULL, 0, o->handle[0] ? o->handle : "-"))
-                                });
-                            });
-                        });
-                    }
-                    if(scorehostinfo && hashost)
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, hostpad, g.strut(1)));
-                            loopscoregroup(uicenterlist(g, {
-                                uipad(g, 0.5f, g.textf("%s", 0xFFFFFF, NULL, 0, scorehost(o)))
-                            }));
-                        });
-                    }
-                    if(scoreicons)
-                    {
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, 0.125f, g.strut(1)));
-                            loopscoregroup(uicenterlist(g, {
-                                uipad(g, 0.125f, {
-                                    if(!m_team(game::gamemode, game::mutators) || o->team != game::focus->team)
+                                if(m_race(game::gamemode))
+                                {
+                                    if(scoretimer && (scoretimer >= 2 || m_laptime(game::gamemode, game::mutators)))
                                     {
-                                        if(game::focus->dominating.find(o) >= 0) g.text("", 0, dominatedtex, TEAM(sg.team, colour));
-                                        else if(game::focus->dominated.find(o) >= 0) g.text("", 0, dominatingtex, TEAM(sg.team, colour));
-                                        else g.strut(1);
+                                        uilist(g, {
+                                            uilist(g, {
+                                                uicenter(g, uipad(g, 4, g.text("best", 0xFFFFFF)));
+                                            });
+                                            loopscoregroup(uilist(g, {
+                                                if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                                uicenter(g, uipad(g, 0.5f, g.textf("%s", 0xFFFFFF, NULL, 0, -1, o->cptime ? timestr(o->cptime, scoreracestyle) : "\fadnf")));
+                                            }));
+                                        });
                                     }
-                                    else g.strut(1);
-                                });
-                            }));
-                        });
-                        uilist(g, {
-                            uicenterlist(g, uipad(g, 0.125f, g.strut(1)));
-                            loopscoregroup(uicenterlist(g, {
-                                const char *status = questiontex;
-                                switch(o->state)
+                                }
+
+                                if(scorefrags >= (!m_dm(game::gamemode) ? 2 : 1))
                                 {
-                                    case CS_ALIVE: status = playertex; break;
-                                    case CS_DEAD: status = deadtex; break;
-                                    case CS_WAITING: status = waitingtex; break;
-                                    case CS_EDITING: status = editingtex; break;
-                                    default: break; // spectators shouldn't be here
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 1, g.text("frags", 0xFFFFFF)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, -1, o->frags)));
+                                        }));
+                                    });
                                 }
-                                uipad(g, 0.125f, g.text("", 0, status, TEAM(sg.team, colour)));
-                            }));
-                        });
-                    }
-                    if(sg.team && m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
-                    {
-                        g.poplist(); // horizontal
-                        g.poplist(); // vertical
-                    }
+
+                                if(scoredeaths >= (!m_dm(game::gamemode) ? 2 : 1))
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 1, g.text("deaths", 0xFFFFFF)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, -1, o->deaths)));
+                                        }));
+                                    });
+                                }
+
+                                if(scoreratios >= (!m_dm(game::gamemode) ? 2 : 1))
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 1, g.text("ratio", 0xFFFFFF)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            float ratio = o->frags >= o->deaths ? (o->frags/float(max(o->deaths, 1))) : -(o->deaths/float(max(o->frags, 1)));
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%.1f\fs\fa:\fS%.1f", 0xFFFFFF, NULL, 0, -1, ratio >= 0 ? ratio : 1.f, ratio >= 0 ? 1.f : -ratio)));
+                                        }));
+                                    });
+                                }
+
+                                if(scorepj)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 2, g.text("pj", 0xFFFFFF)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, -1, o->plag)));
+                                        }));
+                                    });
+                                }
+
+                                if(scoreping)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 2, g.text("ping", 0xFFFFFF)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, -1, o->ping)));
+                                        }));
+                                    });
+                                }
+
+                                if(scoreclientnum || (game::player1->privilege&PRIV_TYPE) >= PRIV_ELEVATED)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 1, g.text("cn", 0xFFFFFF)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%d", 0xFFFFFF, NULL, 0, -1, o->clientnum)));
+                                        }));
+                                    });
+                                }
+
+                                if(scorebotinfo && hasbots)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 1, g.text("sk", 0xFFFFFF)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, {
+                                                if(o->actortype > A_PLAYER) g.textf("%d", 0xFFFFFF, NULL, 0, -1, o->skill);
+                                                else { g.space(1); g.strut(1); }
+                                            }));
+                                        }));
+                                    });
+                                }
+                                if(scorehandles && hashandle)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, handlepad, g.space(1); g.strut(1)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%s", 0xFFFFFF, NULL, 0, -1, o->handle[0] ? o->handle : "-")));
+                                        }));
+                                    });
+                                }
+                                if(scoreipinfo && hasip)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                           uicenter(g, uipad(g, ippad, g.space(1); g.strut(1)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%s", 0xFFFFFF, NULL, 0, -1, scorehost(o, false))));
+                                        }));
+                                    });
+                                }
+                                if(scorehostinfo && hashost)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                           uicenter(g, uipad(g, hostpad, g.space(1); g.strut(1)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%s", 0xFFFFFF, NULL, 0, -1, scorehost(o, true))));
+                                        }));
+                                    });
+                                }
+                                if(scoreverinfo && hasver)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                           uicenter(g, uipad(g, verpad, g.space(1); g.strut(1)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            if((scorehilight && o == game::player1) || scorebgrows >= 2) g.background(i%2 ? bgc2 : bgc1, scorebgblend, scorehilight && o == game::player1 ? scorehilight : (i%2 ? bgc2 : bgc1), scorebgblend, scorehilight && o == game::player1);
+                                            uicenter(g, uipad(g, 0.5f, g.textf("%s", 0xFFFFFF, NULL, 0, -1, scoreversion(o))));
+                                        }));
+                                    });
+                                }
+                                if(scoreicons)
+                                {
+                                    uilist(g, {
+                                        uilist(g, {
+                                            uicenter(g, uipad(g, 0.125f, g.space(1); g.strut(1)));
+                                        });
+                                        loopscoregroup(uilist(g, {
+                                            const char *status = questiontex;
+                                            if(k == numgroups) status = spectatortex;
+                                            else if(game::player1->dominating.find(o) >= 0) status = dominatedtex;
+                                            else if(game::player1->dominated.find(o) >= 0) status = dominatingtex;
+                                            else switch(o->state)
+                                            {
+                                                case CS_ALIVE: status = playertex; break;
+                                                case CS_DEAD: status = deadtex; break;
+                                                case CS_WAITING: status = waitingtex; break;
+                                                case CS_EDITING: status = editingtex; break;
+                                                default: break; // spectators shouldn't be here
+                                            }
+                                            uicenter(g, uipad(g, 0.125f, g.textf("\f(%s)", colour, NULL, 0, -1, status)));
+                                        }));
+                                    });
+                                }
+                                #if 0
+                                uilist(g, {
+                                    uilist(g, {
+                                        uicenter(g, uipad(g, 0.25f, g.space(1); g.strut(1)));
+                                    });
+                                    loopscorelist(uilist(g, {
+                                        uicenter(g, uipad(g, 0.25f, g.space(1); g.strut(1)));
+                                    }));
+                                });
+                                #endif
+                                g.poplist(); // horizontal
+                                g.space(0.25f);
+                                g.poplist(); // vertical
+                                g.space(0.5f);
+                            });
+                        }
+                    });
                 });
-                if(k+1<numgroups && (k+1)%2) g.space(1);
-                else g.poplist();
-            }
+            });
         });
-        if(scorespectators && spectators.length())
-        {
-            g.space(0.5f);
-            uicenterlist(g, uicenterlist(g, uifont(g, "little", {
-                int count = numgroups > 1 ? 5 : 3;
-                bool pushed = false;
-                loopv(spectators)
-                {
-                    gameent *o = spectators[i];
-                    if((i%count)==0)
-                    {
-                        g.pushlist();
-                        pushed = true;
-                    }
-                    uicenterlist(g, uicenterlist(g, uipad(g, 0.25f, {
-                        if(o == game::player1 && scorehilight) g.background(scorehilight);
-                        uipad(g, 0.25f, {
-                            if(scoreclientnum || game::player1->privilege >= PRIV_ELEVATED)
-                                g.textf("%s [%d]", 0xFFFFFF, NULL, 0, game::colourname(o, NULL, true, false), o->clientnum);
-                            else g.textf("%s ", 0xFFFFFF, NULL, 0, game::colourname(o));
-                        });
-                    })));
-                    if(!((i+1)%count) && pushed)
-                    {
-                        g.poplist();
-                        pushed = false;
-                    }
-                }
-                if(pushed) g.poplist();
-            })));
-        }
-        if(m_play(game::gamemode) && game::player1->state != CS_SPECTATOR && (game::intermission || scoresinfo))
-        {
-            float ratio = game::player1->frags >= game::player1->deaths ? (game::player1->frags/float(max(game::player1->deaths, 1))) : -(game::player1->deaths/float(max(game::player1->frags, 1)));
-            g.space(0.5f);
-            uicenterlist(g, uifont(g, "reduced", {
-                g.textf("\fs\fg%d\fS %s, \fs\fg%d\fS %s, \fs\fy%.1f\fS:\fs\fy%.1f\fS ratio, \fs\fg%d\fS damage", 0xFFFFFF, NULL, 0,
-                    game::player1->frags, game::player1->frags != 1 ? "frags" : "frag",
-                    game::player1->deaths, game::player1->deaths != 1 ? "deaths" : "death", ratio >= 0 ? ratio : 1.f, ratio >= 0 ? 1.f : -ratio,
-                    game::player1->totaldamage);
-            }));
-        }
         g.end();
     }
 
-    int drawscoreitem(const char *icon, int colour, int x, int y, int s, float skew, float fade, int pos, int score, const char *name)
+    static const char *posnames[10] = { "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" };
+
+    int drawscoreitem(const char *icon, int colour, int x, int y, int s, float skew, float fade, int pos, int score, int offset, const char *name, const char *ext = NULL)
     {
-        const char *col = "\fa";
+        int col = 0xFF0000;
         switch(pos)
         {
-            case 0: col = "\fg"; break;
-            case 1: col = "\fc"; break;
-            case 2: col = "\fy"; break;
+            case 1: col = 0x00FF00; break;
+            case 2: col = 0x00FFFF; break;
+            case 3: col = 0xFFFF00; break;
+            case 4: col = 0xFF8800; break;
+            default: break;
+        }
+        int size = int(s*skew);
+        string str, q;
+        if(m_laptime(game::gamemode, game::mutators)) { formatstring(str)("\fs\f[%d]\f(%s)\fS %s", col, insigniatex, timestr(score, inventoryracestyle)); }
+        else if(m_defend(game::gamemode) && score == INT_MAX) { formatstring(str)("\fs\f[%d]\f(%s)\fS WIN", col, insigniatex); }
+        else { formatstring(str)("\fs\f[%d]\f(%s)\fS %d", col, insigniatex, score); }
+        if(inventoryscoreinfo)
+        {
+            if(m_laptime(game::gamemode, game::mutators))
+                { formatstring(q)("\n\fs\f[%d]\f(%s)\fS %s", col, offset ? (offset < 0 ? arrowtex : arrowdowntex) : arrowrighttex, timestr(offset < 0 ? 0-offset : offset, inventoryracestyle)); }
+            else { formatstring(q)("%s\fs\f[%d]\f(%s)\fS %d", inventoryscorebreak ? "\n" : " ", col, offset ? (offset > 0 ? arrowtex : arrowdowntex) : arrowrighttex, offset < 0 ? 0-offset : offset); }
+            concatstring(str, q);
         }
         vec c = vec::hexcolor(colour);
-        int size = int(s*skew); size += int(size*inventoryglow);
-        if(m_laptime(game::gamemode, game::mutators)) drawitem(icon, x, y+size, s, inventoryscorebg!=0, 0, false, c.r, c.g, c.b, fade, skew, "huge", "%s%s", col, timestr(score));
-        else if(m_defend(game::gamemode) && score == INT_MAX)
-            drawitem(icon, x, y+size, s, inventoryscorebg!=0, 0, false, c.r, c.g, c.b, fade, skew, "huge", "%sWIN", col);
-        else drawitem(icon, x, y+size, s, inventoryscorebg!=0, 0, false, c.r, c.g, c.b, fade, skew, "huge", "%s%d", col, score);
-        drawitemtext(x, y+size, 0, false, skew, "default", fade, "\f[%d]%s", colour, name);
+        drawitem(icon, x, y+size, s, 0, inventoryscorebg!=0, false, c.r, c.g, c.b, fade, skew, m_laptime(game::gamemode, game::mutators) ? "reduced" : "emphasis", "%s", str);
+        int sy = 0;
+        if(ext) sy += drawitemtextx(x, y+size, 0, TEXT_RIGHT_UP, skew, "default", fade, "%s", ext);
+        drawitemtextx(x, y+size-sy, 0, TEXT_RIGHT_UP, skew, "default", fade, "\f[%d]%s", colour, name);
+        if(inventoryscorepos) drawitemtextx(x, y, 0, TEXT_RIGHT_JUSTIFY, skew, "emphasis", fade, "\f[%d]%d%s", col, pos, posnames[pos < 10 || pos > 13 ? pos%10 : 0]);
         return size;
     }
 
-    int drawscore(int x, int y, int s, int m, float blend)
+    int drawscore(int x, int y, int s, int m, float blend, int count)
     {
-        if(!m_fight(game::gamemode) || (inventoryscore == 1 && game::player1->state == CS_SPECTATOR && game::focus == game::player1)) return 0;
         int sy = 0, numgroups = groupplayers(), numout = 0;
-        loopi(2) loopk(numgroups)
+        loopi(2)
         {
-            if(sy > m) break;
-            scoregroup &sg = *groups[k];
-            if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
-            {
-                if(!sg.team || ((sg.team != game::focus->team) == !i)) continue;
-                float sk = numout && inventoryscoreshrink > 0 ? 1.f-min(numout*inventoryscoreshrink, inventoryscoreshrinkmax) : 1;
-                sy += drawscoreitem(teamtexname(sg.team), TEAM(sg.team, colour), x, y+sy, s, sk*inventoryscoresize, blend*inventoryblend, k, sg.total, TEAM(sg.team, name));
-                if(++numout >= inventoryscore) return sy;
-            }
-            else
+            if(!i && game::focus->state == CS_SPECTATOR) continue;
+            int pos = 0, realpos = 0, lastpos = -1;
+            loopk(numgroups)
             {
-                if(sg.team) continue;
-                loopvj(sg.players)
+                if(sy > m) break;
+                scoregroup &sg = *groups[k];
+                if(m_fight(game::gamemode) && m_team(game::gamemode, game::mutators))
                 {
-                    gameent *d = sg.players[j];
-                    if((d != game::focus) == !i) continue;
+                    realpos++;
+                    if(!pos || (m_laptime(game::gamemode, game::mutators) ? ((!sg.total && groups[lastpos]->total) || sg.total > groups[lastpos]->total) : sg.total < groups[lastpos]->total))
+                    {
+                        pos = realpos;
+                        lastpos = k;
+                    }
+                    if(!sg.team || ((sg.team != game::focus->team) == !i)) continue;
                     float sk = numout && inventoryscoreshrink > 0 ? 1.f-min(numout*inventoryscoreshrink, inventoryscoreshrinkmax) : 1;
-                    sy += drawscoreitem(playertex, game::getcolour(d, game::playerdisplaytone), x, y+sy, s, sk*inventoryscoresize, blend*inventoryblend, j, d->points, game::colourname(d));
-                    if(++numout >= inventoryscore) return sy;
+                    int offset = numgroups > 1 ? sg.total-groups[k ? 0 : 1]->total : 0;
+                    sy += drawscoreitem(teamtexname(sg.team), TEAM(sg.team, colour), x, y+sy, s, sk*inventoryscoresize, blend*inventoryblend, pos, sg.total, offset, TEAM(sg.team, name), i ? NULL : game::colourname(game::focus));
+                    if(++numout >= count) return sy;
+                }
+                else
+                {
+                    if(sg.team) continue;
+                    loopvj(sg.players)
+                    {
+                        gameent *d = sg.players[j];
+                        realpos++;
+                        if(!pos || (m_laptime(game::gamemode, game::mutators) ? ((!d->cptime && sg.players[lastpos]->cptime) || d->cptime > sg.players[lastpos]->cptime) : d->points < sg.players[lastpos]->points))
+                        {
+                            pos = realpos;
+                            lastpos = j;
+                        }
+                        if((d != game::focus) == !i) continue;
+                        float sk = numout && inventoryscoreshrink > 0 ? 1.f-min(numout*inventoryscoreshrink, inventoryscoreshrinkmax) : 1;
+                        int score = m_laptime(game::gamemode, game::mutators) ? d->cptime : d->points,
+                            offset = (sg.players.length() > 1 && (!m_laptime(game::gamemode, game::mutators) || score)) ? score-(m_laptime(game::gamemode, game::mutators) ? sg.players[j ? 0 : 1]->cptime : sg.players[j ? 0 : 1]->points) : 0;
+                        sy += drawscoreitem(playertex, game::getcolour(d, game::playerteamtone), x, y+sy, s, sk*inventoryscoresize, blend*inventoryblend, pos, score, offset, game::colourname(d));
+                        if(++numout >= count) return sy;
+                    }
                 }
             }
         }
         return sy;
     }
 
-    int trialinventory(int x, int y, int s, float blend)
+    int raceinventory(int x, int y, int s, float blend)
     {
         int sy = 0;
         if(groupplayers())
@@ -728,20 +891,43 @@ namespace hud
             scoregroup &sg = *groups[0];
             if(m_laptime(game::gamemode, game::mutators))
             {
-                pushfont("reduced");
                 if(m_team(game::gamemode, game::mutators))
                 {
-                    if(sg.total) sy += draw_textx("best: \fg%s", x, y, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, timestr(sg.total));
+                    if(sg.total)
+                    {
+                        pushfont("little");
+                        sy += draw_textx("by %s", x+FONTW*2, y, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, game::colourteam(sg.team));
+                        popfont();
+                        sy += draw_textx("\fg%s", x, y-sy, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, timestr(sg.total, inventoryracestyle));
+                    }
                 }
                 else if(!sg.players.empty())
                 {
-                    if(sg.players[0]->cptime) sy += draw_textx("best: \fg%s", x, y, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, timestr(sg.players[0]->cptime));
+                    if(sg.players[0]->cptime)
+                    {
+                        pushfont("little");
+                        sy += draw_textx("by %s", x+FONTW*2, y, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, game::colourname(sg.players[0]));
+                        popfont();
+                        sy += draw_textx("\fg%s", x, y-sy, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, timestr(sg.players[0]->cptime, inventoryracestyle));
+                    }
                 }
-                popfont();
             }
             else if(m_team(game::gamemode, game::mutators))
             {
-                if(sg.total) sy += draw_textx("best: \fg%d", x, y, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, sg.total);
+                if(sg.total)
+                {
+                    pushfont("little");
+                    sy += draw_textx("by %s", x+FONTW*2, y, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, game::colourteam(sg.team));
+                    popfont();
+                    sy += draw_textx("\fs\fg%d\fS %s", x, y-sy, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, sg.total, sg.total != 1 ? "laps" : "lap");
+                }
+            }
+            else if(!sg.players.empty() && sg.players[0]->points)
+            {
+                pushfont("little");
+                sy += draw_textx("by %s", x+FONTW*2, y, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, game::colourname(sg.players[0]));
+                popfont();
+                sy += draw_textx("\fs\fg%d\fS %s", x, y-sy, 255, 255, 255, int(blend*255), TEXT_LEFT_UP, -1, -1, sg.players[0]->points, sg.players[0]->points != 1 ? "laps" : "lap");
             }
         }
         return sy;
diff --git a/src/game/server.cpp b/src/game/server.cpp
index c8c0757..4bd48b2 100644
--- a/src/game/server.cpp
+++ b/src/game/server.cpp
@@ -1,3 +1,28 @@
+// WARNING: Before modifying this file, please read our Guidelines
+// This file can be found in the distribution under: ./docs/guidelines.txt
+// Or at: http://redeclipse.net/wiki/Multiplayer_Guidelines
+//
+// The Red Eclipse Team provides the play.redeclipse.net master server for the
+// benefit of the Red Eclipse Community. We impose a general set of guidelines
+// for any server/user which connects to the play.redeclipse.net master server.
+// The team reserves the right to block any attempt to connect to the master
+// server at their discretion. Access to services provided by the project are
+// considered to be a privilege, not a right.
+
+// These guidelines are imposed to ensure the integrity of both the Red Eclipse
+// game, and its community. If you do not agree to these terms, you should not
+// connect to the play.redeclipse.net master server, or any servers which are
+// connected to it. These guidelines are not designed to limit your opinion or
+// freedoms granted to you by the open source licenses employed by the project,
+// nor do they cover usage of the game in either offline play or on servers
+// which are not connected to the Red Eclipse master.
+
+// If you have questions or comments regarding these guidelines please contact
+// the Red Eclipse Team. Any person seeking to modify their game or server for
+// use on the master server should first seek permission from the Red Eclipse
+// Team, each modification must be approved and will be done on a case-by-case
+// basis.
+
 #define GAMESERVER 1
 #include "game.h"
 
@@ -74,7 +99,7 @@ namespace server
             int rays;
             int dist;
         };
-        ivec dir;
+        ivec dir, vel;
     };
 
     struct destroyevent : timedevent
@@ -107,8 +132,8 @@ namespace server
 
     struct projectile
     {
-        int id, ammo, reloads;
-        projectile(int n, int a, int r) : id(n), ammo(a), reloads(r) {}
+        int id, ammo;
+        projectile(int n, int a) : id(n), ammo(a) {}
         ~projectile() {}
     };
     struct projectilestate
@@ -116,9 +141,9 @@ namespace server
         vector<projectile> projs;
         projectilestate() { reset(); }
         void reset() { projs.shrink(0); }
-        void add(int id, int ammo = -1, int reloads = -1)
+        void add(int id, int ammo = -1)
         {
-            projs.add(projectile(id, ammo, reloads));
+            projs.add(projectile(id, ammo));
         }
         bool remove(int id)
         {
@@ -144,10 +169,10 @@ namespace server
             loopv(projs) if(projs[i].id==id) return true;
             return false;
         }
-        void values(int id, int &a, int &v)
+        void values(int id, int &a)
         {
-            loopv(projs) if(projs[i].id==id) { a = projs[i].ammo; v = projs[i].reloads; return; }
-            a = v = -1;
+            loopv(projs) if(projs[i].id==id) { a = projs[i].ammo; return; }
+            a = -1;
         }
     };
 
@@ -171,7 +196,7 @@ namespace server
 
     enum { WARN_CHAT = 0, WARN_TEAMKILL, WARN_MAX };
 
-    struct servstate : gamestate
+    struct servstate : clientstate
     {
         vec o, vel, falling;
         float yaw, pitch, roll;
@@ -201,14 +226,14 @@ namespace server
             dropped.reset();
             loopi(W_MAX) loopj(2) weapshots[i][j].reset();
             if(!change) score = timeplayed = 0;
-            else gamestate::mapchange();
+            else clientstate::mapchange();
             frags = spree = rewards[0] = rewards[1] = deaths = shotdamage = damage = 0;
             fraglog.shrink(0);
             fragmillis.shrink(0);
             cpnodes.shrink(0);
             damagelog.shrink(0);
             teamkills.shrink(0);
-            respawn();
+            respawn(0);
         }
 
         void resetresidualowner(int n = -1)
@@ -217,22 +242,38 @@ namespace server
             else loopi(WR_MAX) lastresowner[i] = -1;
         }
 
-        void respawn(int millis = 0, int heal = 0, int armr = -1)
+        void respawn(int millis)
         {
             lastboost = rewards[1] = 0;
             resetresidualowner();
-            gamestate::respawn(millis, heal, armr);
+            clientstate::respawn(millis);
             o = vec(-1e10f, -1e10f, -1e10f);
             vel = falling = vec(0, 0, 0);
             yaw = pitch = roll = 0;
         }
+
+        void updatetimeplayed(bool last = true)
+        {
+            timeplayed += totalmillis-lasttimeplayed;
+            if(last) lasttimeplayed = totalmillis;
+        }
+
+        float scoretime(bool update = true)
+        {
+            if(update) updatetimeplayed();
+            return score/float(max(timeplayed, 1));
+        }
+
+        vec feetpos(float offset = 0) const { return vec(o).add(vec(0, 0, offset)); }
+        vec headpos(float offset = 0) const { return vec(o).add(vec(0, 0, offset+actor[actortype].height)); }
+        vec center() const { return vec(o).add(vec(0, 0, actor[actortype].height*0.5f)); }
     };
 
     struct savedscore
     {
         uint ip;
-        string name;
-        int points, score, frags, spree, rewards, timeplayed, deaths, shotdamage, damage;
+        string name, handle;
+        int points, score, frags, spree, rewards, timeplayed, deaths, shotdamage, damage, cptime;
         int warnings[WARN_MAX][2];
         vector<teamkill> teamkills;
         bool quarantine;
@@ -249,6 +290,7 @@ namespace server
             teamkills = gs.teamkills;
             shotdamage = gs.shotdamage;
             damage = gs.damage;
+            cptime = gs.cptime;
             loopi(WARN_MAX) loopj(2) warnings[i][j] = gs.warnings[i][j];
             quarantine = gs.quarantine;
         }
@@ -265,13 +307,14 @@ namespace server
             gs.teamkills = teamkills;
             gs.shotdamage = shotdamage;
             gs.damage = damage;
+            gs.cptime = cptime;
             loopi(WARN_MAX) loopj(2) gs.warnings[i][j] = warnings[i][j];
             gs.quarantine = quarantine;
         }
 
         void mapchange()
         {
-            points = frags = spree = rewards = deaths = shotdamage = damage = 0;
+            points = frags = spree = rewards = deaths = shotdamage = damage = cptime = 0;
             teamkills.shrink(0);
         }
     };
@@ -286,23 +329,16 @@ namespace server
 
     struct clientinfo
     {
-        int clientnum, connectmillis, sessionid, overflow, ping, team, lastteam;
-        string name, handle, mapvote;
-        int modevote, mutsvote, lastvote;
-        int privilege;
-        bool connected, ready, local, timesync, online, wantsmap, failedmap, connectauth;
-        int gameoffset, lastevent;
         servstate state;
+        string name, handle, mapvote, authname, clientmap;
+        int clientnum, connectmillis, sessionid, overflow, ping, team, lastteam, lastplayerinfo,
+            modevote, mutsvote, lastvote, privilege, gameoffset, lastevent, wslen, mapcrc, swapteam;
+        bool connected, ready, local, timesync, online, wantsmap, failedmap, connectauth, warned, kicked;
         vector<gameevent *> events;
         vector<uchar> position, messages;
         uchar *wsdata;
         vector<clientinfo *> bots;
-        int wslen;
         uint authreq;
-        string authname;
-        string clientmap;
-        int mapcrc;
-        bool warned, kicked;
         ENetPacket *clipboard;
         int lastclipboard, needclipboard;
 
@@ -323,7 +359,8 @@ namespace server
             overflow = 0;
             ready = timesync = wantsmap = failedmap = false;
             lastevent = gameoffset = lastvote = 0;
-            team = lastteam = T_NEUTRAL;
+            if(!change) lastteam = T_NEUTRAL;
+            team = swapteam = T_NEUTRAL;
             clientmap[0] = '\0';
             mapcrc = 0;
             warned = false;
@@ -337,7 +374,7 @@ namespace server
 
         void reset()
         {
-            ping = 0;
+            ping = lastplayerinfo = 0;
             name[0] = handle[0] = 0;
             privilege = PRIV_NONE;
             connected = ready = local = online = wantsmap = failedmap = connectauth = kicked = false;
@@ -362,7 +399,7 @@ namespace server
     };
 
     namespace aiman {
-        int dorefresh = 0;
+        extern void setskill(clientinfo *ci);
         extern bool addai(int type, int ent = -1, int skill = -1);
         extern void deleteai(clientinfo *ci);
         extern bool delai(int type, bool skip = true);
@@ -371,24 +408,23 @@ namespace server
         extern void checkskills();
         extern void clearai(int type = 0);
         extern void checkai();
+        extern void poke();
     }
 
-    bool hasgameinfo = false;
-    int gamemode = G_EDITMODE, mutators = 0, gamemillis = 0, gamelimit = 0;
     string smapname;
-    int interm = 0, timeremaining = -1, oldtimelimit = -1;
-    bool maprequest = false, inovertime = false;
+    int gamestate = G_S_WAITING, gamemode = G_EDITMODE, mutators = 0, gamemillis = 0, gamelimit = 0, mastermode = MM_OPEN;
+    int timeremaining = -1, oldtimelimit = -1, gamewait = 0, lastteambalance = 0, nextteambalance = 0, lastrotatecycle = 0;
+    bool hasgameinfo = false, updatecontrols = false, mapsending = false, shouldcheckvotes = false, firstblood = false;
     enet_uint32 lastsend = 0;
-    int mastermode = MM_OPEN;
-    bool updatecontrols = false, mapsending = false, shouldcheckvotes = false;
     stream *mapdata[SENDMAP_MAX] = { NULL };
+    uint mapcrc = 0;
     vector<clientinfo *> clients, connects;
 
     struct demofile
     {
         string info;
         uchar *data;
-        int len;
+        int ctime, len;
     };
 
     vector<demofile> demos;
@@ -404,6 +440,13 @@ namespace server
         void reset(int n = 0) { id = n; ents.shrink(0); }
     } triggers[TRIGGERIDS+1];
 
+    bool canplay(bool chk = true)
+    {
+        if(!demoplayback && !m_demo(gamemode))
+            if(!chk || (m_fight(gamemode) && !hasgameinfo) || !gs_playing(gamestate)) return false;
+        return true;
+    }
+
     struct servmode
     {
         servmode() {}
@@ -415,25 +458,26 @@ namespace server
         virtual void moved(clientinfo *ci, const vec &oldpos, const vec &newpos) {}
         virtual bool canspawn(clientinfo *ci, bool tryspawn = false) { return true; }
         virtual void spawned(clientinfo *ci) {}
-        virtual int points(clientinfo *victim, clientinfo *actor)
+        virtual int points(clientinfo *m, clientinfo *v)
         {
-            if(victim==actor || victim->team == actor->team) return -1;
+            if(m==v || m->team == v->team) return -1;
             return 1;
         }
-        virtual void died(clientinfo *victim, clientinfo *actor = NULL) {}
+        virtual void died(clientinfo *m, clientinfo *v = NULL) {}
         virtual void changeteam(clientinfo *ci, int oldteam, int newteam) {}
         virtual void initclient(clientinfo *ci, packetbuf &p, bool connecting) {}
         virtual void update() {}
-        virtual void reset(bool empty) {}
+        virtual void reset() {}
         virtual void layout() {}
         virtual void balance(int oldbalance) {}
         virtual void intermission() {}
         virtual bool wantsovertime() { return false; }
-        virtual bool damage(clientinfo *target, clientinfo *actor, int damage, int weap, int flags, int material, const ivec &hitpush = ivec(0, 0, 0)) { return true; }
-        virtual void dodamage(clientinfo *target, clientinfo *actor, int &damage, int &hurt, int &weap, int &flags, int &material, const ivec &hitpush = ivec(0, 0, 0)) { }
+        virtual bool damage(clientinfo *m, clientinfo *v, int damage, int weap, int flags, int material, const ivec &hitpush = ivec(0, 0, 0), const ivec &hitvel = ivec(0, 0, 0), float dist = 0) { return true; }
+        virtual void dodamage(clientinfo *m, clientinfo *v, int &damage, int &hurt, int &weap, int &flags, int &material, const ivec &hitpush = ivec(0, 0, 0), const ivec &hitvel = ivec(0, 0, 0), float dist = 0) { }
         virtual void regen(clientinfo *ci, int &total, int &amt, int &delay) {}
         virtual void checkclient(clientinfo *ci) {}
         virtual void scoreaffinity(clientinfo *ci, bool win = true) {}
+        virtual bool canbalance() { return true; }
     };
 
     vector<srventity> sents;
@@ -442,6 +486,7 @@ namespace server
     vector<servmode *> smuts;
     #define mutate(a,b) { loopvk(a) { servmode *mut = a[k]; { b; } } }
     int curbalance = 0, nextbalance = 0, totalspawns = 0;
+    bool teamspawns = false;
 
     vector<score> scores;
     score &teamscore(int team)
@@ -459,28 +504,14 @@ namespace server
 
     bool chkloadweap(clientinfo *ci, bool request = true)
     {
-        while(ci->state.loadweap.length() < G(maxcarry)) ci->state.loadweap.add(-1);
-        loopj(G(maxcarry))
+        if(ci->state.actortype == A_PLAYER && ci->state.loadweap.empty())
         {
-            int aweap = ci->state.loadweap[j];
-            if(isweap(ci->state.loadweap[j]))
+            if(request)
             {
-                if(aweap < W_OFFSET || aweap >= W_ITEM) ci->state.loadweap[j] = 0;
-                else if(!m_check(W(aweap, modes), W(aweap, muts), gamemode, mutators)) ci->state.loadweap[j] = request ? -1 : 0;
-            }
-            if(!isweap(ci->state.loadweap[j]))
-            {
-                if(ci->state.aitype != AI_NONE) ci->state.loadweap[j] = 0;
-                else
-                {
-                    if(request)
-                    {
-                        if(isweap(aweap)) srvmsgft(ci->clientnum, CON_EVENT, "sorry, the \fs\f[%d]%s\fS is not available, please select a different weapon", W(aweap, colour), W(aweap, name));
-                        sendf(ci->clientnum, 1, "ri", N_LOADW);
-                    }
-                    return false;
-                }
+                ci->lastplayerinfo = 0;
+                sendf(ci->clientnum, 1, "ri", N_LOADW);
             }
+            return false;
         }
         return true;
     }
@@ -493,7 +524,7 @@ namespace server
             sents[ent].spawned = spawned;
             sents[ent].millis = sents[ent].last = gamemillis;
             if(sents[ent].type == WEAPON && !(sents[ent].attrs[1]&W_F_FORCED))
-                sents[ent].millis += w_spawn(w_attr(gamemode, mutators, sents[ent].attrs[0], m_weapon(gamemode, mutators)));
+                sents[ent].millis += w_spawn(w_attr(gamemode, mutators, sents[ent].type, sents[ent].attrs[0], m_weapon(gamemode, mutators)));
             else sents[ent].millis += G(itemspawntime);
             if(msg) sendf(-1, 1, "ri3", N_ITEMSPAWN, ent, sents[ent].spawned ? 1 : 0);
         }
@@ -501,64 +532,69 @@ namespace server
 
     void takeammo(clientinfo *ci, int weap, int amt = 1) { ci->state.ammo[weap] = max(ci->state.ammo[weap]-amt, 0); }
 
-    struct droplist { int weap, ent, ammo, reloads; };
+    struct droplist { int weap, ent, ammo; };
     enum
     {
         DROP_NONE = 0, DROP_WEAPONS = 1<<0, DROP_WCLR = 1<<1, DROP_KAMIKAZE = 1<<2, DROP_EXPLODE = 1<<3,
         DROP_DEATH = DROP_WEAPONS|DROP_KAMIKAZE, DROP_EXPIRE = DROP_WEAPONS|DROP_EXPLODE, DROP_RESET = DROP_WEAPONS|DROP_WCLR
     };
 
-    void dropweapon(clientinfo *ci, servstate &ts, int flags, int weap, vector<droplist> &drop)
+    void dropweapon(clientinfo *ci, int flags, int weap, vector<droplist> &drop)
     {
-        if(isweap(weap) && weap != m_weapon(gamemode, mutators) && ts.hasweap(weap, m_weapon(gamemode, mutators)) && sents.inrange(ts.entid[weap]))
+        if(isweap(weap) && weap != m_weapon(gamemode, mutators) && ci->state.hasweap(weap, m_weapon(gamemode, mutators)) && sents.inrange(ci->state.entid[weap]))
         {
-            setspawn(ts.entid[weap], false);
+            setspawn(ci->state.entid[weap], false);
             droplist &d = drop.add();
             d.weap = weap;
-            d.ent = ts.entid[weap];
-            d.ammo = ts.ammo[weap];
-            d.reloads = ts.reloads[weap];
-            ts.dropped.add(d.ent, d.ammo, d.reloads);
-            ts.entid[weap] = -1;
-            if(flags&DROP_WCLR) ts.ammo[weap] = ts.reloads[weap] = -1;
+            d.ent = ci->state.entid[weap];
+            d.ammo = ci->state.ammo[weap];
+            ci->state.dropped.add(d.ent, d.ammo);
+            ci->state.entid[weap] = -1;
+            if(flags&DROP_WCLR) ci->state.ammo[weap] = -1;
         }
     }
 
-    void dropitems(clientinfo *ci, int flags = DROP_RESET)
+    bool dropitems(clientinfo *ci, int flags = DROP_RESET)
     {
-        servstate &ts = ci->state;
+        bool kamikaze = false;
         vector<droplist> drop;
-        if(flags&DROP_EXPLODE || (flags&DROP_KAMIKAZE && G(kamikaze) && (G(kamikaze) > 2 || (ts.hasweap(W_GRENADE, m_weapon(gamemode, mutators)) && (G(kamikaze) > 1 || ts.weapselect == W_GRENADE)))))
+        if(flags&DROP_EXPLODE || (flags&DROP_KAMIKAZE && G(kamikaze) && (G(kamikaze) > 2 || (ci->state.hasweap(W_GRENADE, m_weapon(gamemode, mutators)) && (G(kamikaze) > 1 || ci->state.weapselect == W_GRENADE)))))
         {
             ci->state.weapshots[W_GRENADE][0].add(1);
             droplist &d = drop.add();
             d.weap = W_GRENADE;
-            d.ent = d.ammo = d.reloads = -1;
-            if(!(flags&DROP_EXPLODE)) takeammo(ci, W_GRENADE, W2(W_GRENADE, sub, false));
+            d.ent = d.ammo = -1;
+            if(!(flags&DROP_EXPLODE)) takeammo(ci, W_GRENADE, W2(W_GRENADE, ammosub, false));
+            kamikaze = true;
         }
-        if(flags&DROP_WEAPONS) loopi(W_MAX) dropweapon(ci, ts, flags, i, drop);
+        if(flags&DROP_WEAPONS) loopi(W_MAX) dropweapon(ci, flags, i, drop);
         if(!drop.empty())
             sendf(-1, 1, "ri3iv", N_DROP, ci->clientnum, -1, drop.length(), drop.length()*sizeof(droplist)/sizeof(int), drop.getbuf());
+        return kamikaze;
     }
 
     struct vampireservmode : servmode
     {
         vampireservmode() {}
-        void dodamage(clientinfo *target, clientinfo *actor, int &damage, int &hurt, int &weap, int &flags, int &material, const ivec &hitpush = ivec(0, 0, 0))
+        void dodamage(clientinfo *m, clientinfo *v, int &damage, int &hurt, int &weap, int &flags, int &material, const ivec &hitpush, const ivec &hitvel, float dist)
         {
-            if(actor != target && (!m_team(gamemode, mutators) || actor->team != target->team) && actor->state.state == CS_ALIVE && hurt > 0)
+            if(v != m && (!m_team(gamemode, mutators) || v->team != m->team) && v->state.state == CS_ALIVE && hurt > 0)
             {
-                int rgn = actor->state.health, heal = min(actor->state.health+hurt, m_maxhealth(gamemode, mutators, actor->state.model)), eff = heal-rgn;
-                if(eff)
+                int real = int(ceilf(hurt*G(vampirescale))), heal = v->state.health+real;
+                if(v->state.actortype < A_ENEMY) heal = min(heal, m_maxhealth(gamemode, mutators, v->state.model));
+                int eff = heal-v->state.health;
+                if(eff > 0)
                 {
-                    actor->state.health = heal;
-                    actor->state.lastregen = gamemillis;
-                    sendf(-1, 1, "ri5", N_REGEN, actor->clientnum, actor->state.health, eff, actor->state.armour);
+                    v->state.health = heal;
+                    v->state.lastregen = gamemillis;
+                    sendf(-1, 1, "ri4", N_REGEN, v->clientnum, v->state.health, eff);
                 }
             }
         }
     } vampiremutator;
 
+    extern bool canbalancenow();
+
     struct spawnservmode : servmode // pseudo-mutator to regulate spawning clients
     {
         vector<clientinfo *> spawnq, playing;
@@ -567,12 +603,12 @@ namespace server
 
         bool spawnqueue(bool all = false, bool needinfo = true)
         {
-            return m_fight(gamemode) && !m_duke(gamemode, mutators) && G(maxalive) > 0 && (!needinfo || hasgameinfo) && (!all || G(maxalivequeue)) && numclients() > 1;
+            return m_fight(gamemode) && !m_race(gamemode) && !m_duke(gamemode, mutators) && G(maxalive) > 0 && (!needinfo || canplay()) && (!all || G(maxalivequeue)) && numclients() > 1;
         }
 
         void queue(clientinfo *ci, bool msg = true, bool wait = true, bool top = false)
         {
-            if(spawnqueue(true) && ci->online && ci->state.state != CS_SPECTATOR && ci->state.state != CS_EDITING && ci->state.aitype < AI_START)
+            if(spawnqueue(true) && ci->online && ci->state.actortype < A_ENEMY && ci->state.state != CS_SPECTATOR && ci->state.state != CS_EDITING)
             {
                 int n = spawnq.find(ci);
                 playing.removeobj(ci);
@@ -590,20 +626,20 @@ namespace server
                     {
                         if(x%2) x++;
                         x = x/2;
-                        if(m_coop(gamemode, mutators) && ci->state.aitype == AI_BOT)
+                        if(m_coop(gamemode, mutators) && ci->state.actortype == A_BOT)
                             x = int(x*G(coopbalance));
                     }
                     int slots = x;
                     loopv(playing) if(playing[i] && ci->team == playing[i]->team) slots--;
                     if(!slots)
                     {
-                        int wait = 0;
-                        loopv(spawnq) if(spawnq[i] && spawnq[i]->team == ci->team && spawnq[i]->state.aitype == AI_NONE)
+                        int qn = 0;
+                        loopv(spawnq) if(spawnq[i] && spawnq[i]->team == ci->team && spawnq[i]->state.actortype == A_PLAYER)
                         {
-                            wait++;
+                            qn++;
                             if(spawnq[i] == ci)
                             {
-                                if(wait > 1) srvmsgft(spawnq[i]->clientnum, CON_EVENT, "\fyyou are \fs\fzcg#%d\fS in the \fs\fgrespawn queue\fS", wait);
+                                if(qn > 1) srvmsgft(spawnq[i]->clientnum, CON_EVENT, "\fyyou are \fs\fzcg#%d\fS in the \fs\fgrespawn queue\fS", qn);
                                 else srvmsgft(spawnq[i]->clientnum, CON_EVENT, "\fyyou are \fs\fzcrNEXT\fS in the \fs\fgrespawn queue\fS");
                                 break;
                             }
@@ -628,85 +664,79 @@ namespace server
 
         bool canspawn(clientinfo *ci, bool tryspawn = false)
         {
-            if(ci->state.aitype >= AI_START) return true;
+            if(ci->state.actortype >= A_ENEMY || !m_fight(gamemode)) return true;
             else if(tryspawn)
             {
-                if(m_balance(gamemode, mutators) && G(balancenospawn) && nextbalance && nextbalance >= gamemillis) return false;
                 if(m_loadout(gamemode, mutators) && !chkloadweap(ci)) return false;
                 if(spawnqueue(true) && spawnq.find(ci) < 0 && playing.find(ci) < 0) queue(ci);
                 return true;
             }
-            else
+            if(m_balance(gamemode, mutators, teamspawns) && G(balancenospawn) && nextbalance && m_balreset(gamemode, mutators) && canbalancenow()) return false;
+            int delay = ci->state.actortype >= A_ENEMY && ci->state.lastdeath ? G(enemyspawntime) : m_delay(gamemode, mutators, ci->team);
+            if(delay && ci->state.respawnwait(gamemillis, delay)) return false;
+            if(spawnqueue() && playing.find(ci) < 0)
             {
-                if(m_balance(gamemode, mutators) && G(balancenospawn) && nextbalance && nextbalance >= gamemillis) return false;
-                if(m_loadout(gamemode, mutators) && !chkloadweap(ci, false)) return false;
-                int delay = ci->state.aitype >= AI_START && ci->state.lastdeath ? G(enemyspawntime) : m_delay(gamemode, mutators);
-                if(delay && ci->state.respawnwait(gamemillis, delay)) return false;
-                if(spawnqueue() && playing.find(ci) < 0)
+                if(!canplay()) return false;
+                if(G(maxalivequeue) && spawnq.find(ci) < 0) queue(ci);
+                int x = max(int(G(maxalive)*G(maxplayers)), max(int(numclients()*G(maxalivethreshold)), G(maxaliveminimum)));
+                if(m_team(gamemode, mutators))
                 {
-                    if(!hasgameinfo) return false;
-                    if(G(maxalivequeue) && spawnq.find(ci) < 0) queue(ci);
-                    int x = max(int(G(maxalive)*G(maxplayers)), max(int(numclients()*G(maxalivethreshold)), G(maxaliveminimum)));
-                    if(m_team(gamemode, mutators))
-                    {
-                        if(x%2) x++;
-                        x = x/2;
-                        if(m_coop(gamemode, mutators) && ci->state.aitype == AI_BOT)
-                            x = int(x*G(coopbalance));
-                    }
-                    int alive = 0;
-                    loopv(playing)
+                    if(x%2) x++;
+                    x = x/2;
+                    if(m_coop(gamemode, mutators) && ci->state.actortype == A_BOT)
+                        x = int(x*G(coopbalance));
+                }
+                int alive = 0;
+                loopv(playing)
+                {
+                    if(playing[i]->state.state != CS_DEAD && playing[i]->state.state != CS_ALIVE)
                     {
-                        if(playing[i]->state.state != CS_DEAD && playing[i]->state.state != CS_ALIVE)
+                        if(playing[i]->state.state != CS_WAITING || !G(maxalivequeue))
                         {
-                            if(playing[i]->state.state != CS_WAITING || !G(maxalivequeue))
-                            {
-                                playing.removeobj(playing[i--]);
-                                continue;
-                            }
+                            playing.removeobj(playing[i--]);
+                            continue;
                         }
-                        if(spawnq.find(playing[i]) >= 0) spawnq.removeobj(playing[i]);
-                        if(ci->team == playing[i]->team) alive++;
                     }
-                    if(alive >= x)
+                    if(spawnq.find(playing[i]) >= 0) spawnq.removeobj(playing[i]);
+                    if(ci->team == playing[i]->team) alive++;
+                }
+                if(alive >= x)
+                {
+                    if(ci->state.actortype == A_PLAYER) loopv(playing)
+                    { // kill off bots for the human
+                        if(playing[i]->state.actortype != A_BOT || ci->team != playing[i]->team)
+                            continue;
+                        queue(playing[i--]);
+                        if(--alive < x) break;
+                    }
+                    if(alive >= x) return false;
+                }
+                if(G(maxalivequeue))
+                {
+                    if(ci->state.actortype == A_BOT) loopv(spawnq) if(spawnq[i]->team == ci->team)
                     {
-                        if(ci->state.aitype == AI_NONE) loopv(playing)
-                        { // kill off bots for the human
-                            if(playing[i]->state.aitype != AI_BOT || ci->team != playing[i]->team)
-                                continue;
-                            queue(playing[i--]);
-                            if(--alive < x) break;
-                        }
-                        if(alive >= x) return false;
+                        if(spawnq[i] != ci && spawnq[i]->state.actortype == A_PLAYER) return false;
+                        break;
                     }
-                    if(G(maxalivequeue))
+                    // at this point is where it decides this player is spawning, so tell everyone else their position
+                    if(x-alive == 1)
                     {
-                        if(ci->state.aitype == AI_BOT) loopv(spawnq) if(spawnq[i]->team == ci->team)
+                        int qn = 0;
+                        loopv(spawnq) if(spawnq[i] != ci && spawnq[i]->team == ci->team && spawnq[i]->state.actortype == A_PLAYER)
                         {
-                            if(spawnq[i] != ci && spawnq[i]->state.aitype == AI_NONE) return false;
-                            break;
-                        }
-                        // at this point is where it decides this player is spawning, so tell everyone else their position
-                        if(x-alive == 1)
-                        {
-                            int wait = 0;
-                            loopv(spawnq) if(spawnq[i] != ci && spawnq[i]->team == ci->team && spawnq[i]->state.aitype == AI_NONE)
+                            qn++;
+                            if(allowbroadcast(spawnq[i]->clientnum))
                             {
-                                wait++;
-                                if(allowbroadcast(spawnq[i]->clientnum))
-                                {
-                                    if(wait > 1) srvmsgft(spawnq[i]->clientnum, CON_EVENT, "\fyyou are \fs\fzcg#%d\fS in the \fs\fgrespawn queue\fS", wait);
-                                    else srvmsgft(spawnq[i]->clientnum, CON_EVENT, "\fyyou are \fs\fzcrNEXT\fS in the \fs\fgrespawn queue\fS");
-                                }
+                                if(qn > 1) srvmsgft(spawnq[i]->clientnum, CON_EVENT, "\fyyou are \fs\fzcg#%d\fS in the \fs\fgrespawn queue\fS", qn);
+                                else srvmsgft(spawnq[i]->clientnum, CON_EVENT, "\fyyou are \fs\fzcrNEXT\fS in the \fs\fgrespawn queue\fS");
                             }
                         }
                     }
-                    spawnq.removeobj(ci);
-                    if(playing.find(ci) < 0) playing.add(ci);
                 }
-                return true;
+                spawnq.removeobj(ci);
+                if(playing.find(ci) < 0) playing.add(ci);
             }
-            return false;
+            return true;
         }
 
         void spawned(clientinfo *ci)
@@ -721,69 +751,20 @@ namespace server
             if(G(maxalivequeue)) playing.removeobj(ci);
         }
 
-        void reset(bool empty)
+        void reset()
         {
             spawnq.shrink(0);
             playing.shrink(0);
         }
     } spawnmutator;
 
-    struct gauntletservmode : servmode
+    bool canbalancenow()
     {
-        void spawned(clientinfo *ci)
-        {
-            ci->state.cpmillis = gamemillis;
-            ci->state.cpnodes.shrink(0);
-            sendf(-1, 1, "ri3", N_CHECKPOINT, ci->clientnum, -2);
-        }
-
-        void initclient(clientinfo *ci, packetbuf &p, bool connecting)
-        {
-            loopv(clients)
-            {
-                clientinfo *oi = clients[i];
-                if(!oi || !oi->connected || (ci && oi->clientnum == ci->clientnum) || oi->team != T_OMEGA || !oi->state.lastbuff) continue;
-                putint(p, N_SPHY);
-                putint(p, oi->clientnum);
-                putint(p, SPHY_BUFF);
-                putint(p, 1);
-            }
-        }
-
-        void regen(clientinfo *ci, int &total, int &amt, int &delay)
-        {
-            if(!G(gauntletregenbuff) || ci->team != T_OMEGA || !ci->state.lastbuff) return;
-            if(G(maxhealth)) total = max(m_maxhealth(gamemode, mutators, ci->state.model), total);
-            if(ci->state.lastregen && G(gauntletregendelay)) delay = G(gauntletregendelay);
-            if(G(gauntletregenextra)) amt += G(gauntletregenextra);
-        }
-
-        void checkclient(clientinfo *ci)
-        {
-            if(ci->state.state != CS_ALIVE || m_insta(gamemode, mutators) || ci->team != T_OMEGA) return;
-            if(G(gauntletbuffing))
-            {
-                if(m_gsp2(gamemode, mutators))
-                {
-                    if(!ci->state.lastbuff) sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 1);
-                    ci->state.lastbuff = gamemillis;
-                    return;
-                }
-                else loopv(sents) if(sents[i].type == CHECKPOINT && (sents[i].attrs[6] == CP_LAST || sents[i].attrs[6] == CP_FINISH))
-                {
-                    if(ci->state.o.dist(sents[i].o) > G(gauntletbuffarea)) continue;
-                    if(!ci->state.lastbuff) sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 1);
-                    ci->state.lastbuff = gamemillis;
-                    return;
-                }
-            }
-            if(ci->state.lastbuff && (!G(gauntletbuffing) || gamemillis-ci->state.lastbuff > G(gauntletbuffdelay)))
-            {
-                ci->state.lastbuff = 0;
-                sendf(-1, 1, "ri4", N_SPHY, ci->clientnum, SPHY_BUFF, 0);
-            }
-        }
-    } gauntletmode;
+        bool ret = true;
+        if(smode) if(!smode->canbalance()) ret = false;
+        if(ret) mutate(smuts, if(!mut->canbalance()) { ret = false; break; });
+        return ret;
+    }
 
     SVAR(0, serverpass, "");
     SVAR(0, adminpass, "");
@@ -846,31 +827,35 @@ namespace server
     }
 
     int numgamevars = 0, numgamemods = 0;
-    void resetgamevars(bool flush)
+    void resetgamevars(bool flush, bool all)
     {
         numgamevars = numgamemods = 0;
         enumerate(idents, ident, id, {
-            if(id.flags&IDF_SERVER && !(id.flags&IDF_READONLY)) // reset vars
+            if(id.flags&IDF_SERVER && !(id.flags&IDF_READONLY) && (all || !(id.flags&IDF_WORLD))) // reset vars
             {
+                bool check = !(id.flags&IDF_ADMIN) && !(id.flags&IDF_WORLD);
                 const char *val = NULL;
-                numgamevars++;
+                if(check) numgamevars++;
                 switch(id.type)
                 {
                     case ID_VAR:
                     {
                         setvar(id.name, id.def.i, true);
+                        if(check && *id.storage.i != id.bin.i) numgamemods++;
                         if(flush) val = intstr(&id);
                         break;
                     }
                     case ID_FVAR:
                     {
                         setfvar(id.name, id.def.f, true);
+                        if(check && *id.storage.f != id.bin.f) numgamemods++;
                         if(flush) val = floatstr(*id.storage.f);
                         break;
                     }
                     case ID_SVAR:
                     {
                         setsvar(id.name, id.def.s && *id.def.s ? id.def.s : "", true);
+                        if(check && strcmp(*id.storage.s, id.bin.s)) numgamemods++;
                         if(flush) val = *id.storage.s;
                         break;
                     }
@@ -881,10 +866,57 @@ namespace server
         });
     }
 
-    const char *pickmap(const char *suggest, int mode, int muts)
+    void savegamevars()
+    {
+        enumerate(idents, ident, id, {
+            if(id.flags&IDF_SERVER && !(id.flags&IDF_READONLY) && !(id.flags&IDF_WORLD)) switch(id.type)
+            {
+                case ID_VAR: id.def.i = *id.storage.i; break;
+                case ID_FVAR: id.def.f = *id.storage.f; break;
+                case ID_SVAR:
+                {
+                    delete[] id.def.s;
+                    id.def.s = newstring(*id.storage.s);
+                    break;
+                }
+                default: break;
+            }
+        });
+    }
+
+    const char *pickmap(const char *suggest, int mode, int muts, bool notry)
     {
         const char *map = G(defaultmap);
-        if(!map || !*map) map = choosemap(suggest, mode, muts, G(rotatemaps));
+        if(!notry)
+        {
+            if(!map || !*map) map = choosemap(suggest, mode, muts, G(rotatemaps), true);
+            else if(strchr(map, ' '))
+            {
+                static string defaultmap;
+                defaultmap[0] = 0;
+                vector<char *> maps;
+                explodelist(map, maps);
+                if(*sv_previousmaps)
+                {
+                    vector<char *> prev;
+                    explodelist(sv_previousmaps, prev);
+                    loopvj(prev) loopvrev(maps) if(strcmp(prev[j], maps[i]))
+                    {
+                        delete[] maps[i];
+                        maps.remove(i);
+                        if(maps.length() <= 1) break;
+                    }
+                    prev.deletearrays();
+                }
+                if(!maps.empty())
+                {
+                    int r = rnd(maps.length());
+                    copystring(defaultmap, maps[r]);
+                }
+                maps.deletearrays();
+                map = *defaultmap ? defaultmap : choosemap(suggest, mode, muts, G(rotatemaps), true);
+            }
+        }
         return map && *map ? map : "maps/untitled";
     }
 
@@ -927,17 +959,23 @@ namespace server
         loopvrev(control) if(control[i].type == ipinfo::LIMIT && control[i].flag == ipinfo::TEMPORARY) control.remove(i);
     }
 
+    void resetexcepts()
+    {
+        loopvrev(control) if(control[i].type == ipinfo::EXCEPT && control[i].flag == ipinfo::TEMPORARY) control.remove(i);
+    }
+
     void cleanup(bool init = false)
     {
         setpause(false);
         setmod(sv_botoffset, 0);
-        if(*sv_previousmaps) setmods(sv_previousmaps, "");
         if(G(resetmmonend)) { mastermode = MM_OPEN; resetallows(); }
         if(G(resetbansonend)) resetbans();
         if(G(resetmutesonend)) resetmutes();
         if(G(resetlimitsonend)) resetlimits();
-        if(G(resetvarsonend) || init) resetgamevars(true);
+        if(G(resetexceptsonend)) resetexcepts();
+        if(G(resetvarsonend) || init) resetgamevars(true, true);
         changemap();
+        lastrotatecycle = clocktime;
     }
 
     void start()
@@ -963,6 +1001,7 @@ namespace server
 
     int numchannels() { return 3; }
     int reserveclients() { return G(serverclients)+4; }
+    int dupclients() { return G(serverdupclients); }
 
     bool hasclient(clientinfo *ci, clientinfo *cp = NULL)
     {
@@ -973,14 +1012,14 @@ namespace server
     int peerowner(int n)
     {
         clientinfo *ci = (clientinfo *)getinfo(n);
-        if(ci && ci->state.aitype > AI_NONE) return ci->state.ownernum;
+        if(ci && ci->state.actortype > A_PLAYER) return ci->state.ownernum;
         return n;
     }
 
     bool allowbroadcast(int n)
     {
         clientinfo *ci = (clientinfo *)getinfo(n);
-        return ci && ci->connected && ci->state.aitype == AI_NONE;
+        return ci && ci->connected && ci->state.actortype == A_PLAYER;
     }
 
     const char *mastermodename(int type)
@@ -996,31 +1035,14 @@ namespace server
         }
     }
 
-    const char *privname(int type, bool prefix = false, int aitype = AI_NONE)
-    {
-        if(aitype != AI_NONE) return prefix ? "a bot" : "bot";
-        switch(type)
-        {
-            case PRIV_CREATOR: return prefix ? "a creator" : "creator";
-            case PRIV_DEVELOPER: return prefix ? "a developer" : "developer";
-            case PRIV_ADMINISTRATOR: return prefix ? "an administrator" : "administrator";
-            case PRIV_OPERATOR: return prefix ? "an operator" : "operator";
-            case PRIV_MODERATOR: return prefix ? "a moderator" : "moderator";
-            case PRIV_SUPPORTER: return prefix ? "a supporter" : "supporter";
-            case PRIV_PLAYER: return prefix ? "a player" : "player";
-            case PRIV_MAX: return prefix ? "connected locally" : "local";
-            default: return prefix ? "playing alone" : "alone";
-        }
-    }
-
-    int numclients(int exclude, bool nospec, int aitype)
+    int numclients(int exclude, bool nospec, int actortype)
     {
         int n = 0;
         loopv(clients)
         {
             if(clients[i]->clientnum >= 0 && clients[i]->name[0] && clients[i]->clientnum != exclude &&
                 (!nospec || clients[i]->state.state != CS_SPECTATOR) &&
-                    (clients[i]->state.aitype == AI_NONE || (aitype > AI_NONE && clients[i]->state.aitype <= aitype && clients[i]->state.ownernum >= 0)))
+                    (clients[i]->state.actortype == A_PLAYER || (actortype > A_PLAYER && clients[i]->state.actortype <= actortype && clients[i]->state.ownernum >= 0)))
                         n++;
         }
         return n;
@@ -1037,38 +1059,60 @@ namespace server
     {
         if(tone)
         {
-            int col = ci->state.aitype < AI_START ? ci->state.colour : 0;
+            int col = ci->state.actortype < A_ENEMY ? ci->state.colour : 0x060606;
             if(!col && isweap(ci->state.weapselect)) col = W(ci->state.weapselect, colour);
             if(col) return col;
         }
         return TEAM(ci->team, colour);
     }
 
-    const char *privnamex(int priv, int aitype)
+    const char *privname(int priv, int actortype)
+    {
+        if(actortype != A_PLAYER) return "bot";
+        const char *privnames[2][PRIV_MAX] = {
+            { "none", "player account", "global supporter", "global moderator", "global operator", "global administrator", "project developer", "project founder" },
+            { "none", "player account", "local supporter", "local moderator", "local operator", "local administrator", "none", "none" }
+        };
+        return privnames[priv&PRIV_LOCAL ? 1 : 0][clamp(priv&PRIV_TYPE, 0, int(priv&PRIV_LOCAL ? PRIV_ADMINISTRATOR : PRIV_LAST))];
+    }
+
+    const char *privnamex(int priv, int actortype, bool local)
     {
-        if(aitype != AI_NONE) return "bot";
-        const char *privnames[PRIV_MAX] = { "none", "player", "supporter", "moderator", "operator", "administrator", "developer", "creator" };
-        return privnames[clamp(priv, 0, PRIV_MAX-1)];
+        if(actortype != A_PLAYER) return "bot";
+        const char *privnames[2][PRIV_MAX] = {
+            { "none", "player", "supporter", "moderator", "operator", "administrator", "developer", "founder" },
+            { "none", "player", "localsupporter", "localmoderator", "localoperator", "localadministrator", "developer", "founder" }
+        };
+        return privnames[local && priv&PRIV_LOCAL ? 1 : 0][clamp(priv&PRIV_TYPE, 0, int(priv&PRIV_LOCAL ? PRIV_ADMINISTRATOR : PRIV_LAST))];
     }
 
-    const char *colourname(clientinfo *ci, char *name = NULL, bool icon = true, bool dupname = true)
+    const char *colourname(clientinfo *ci, char *name = NULL, bool icon = true, bool dupname = true, int colour = 3)
     {
         if(!name) name = ci->name;
         static string colored; colored[0] = 0; string colortmp;
-        concatstring(colored, "\fs");
+        if(colour) concatstring(colored, "\fs");
         if(icon)
         {
-            formatstring(colortmp)("\f[%d]\f($priv%stex)", findcolour(ci), privnamex(ci->privilege, ci->state.aitype));
+            if(colour&1)
+            {
+                formatstring(colortmp)("\f[%d]", findcolour(ci));
+                concatstring(colored, colortmp);
+            }
+            formatstring(colortmp)("\f($priv%stex)", privnamex(ci->privilege, ci->state.actortype, true));
+            concatstring(colored, colortmp);
+        }
+        if(colour&2)
+        {
+            formatstring(colortmp)("\f[%d]", TEAM(ci->team, colour));
             concatstring(colored, colortmp);
         }
-        formatstring(colortmp)("\f[%d]%s", TEAM(ci->team, colour), name);
-        concatstring(colored, colortmp);
-        if(!name[0] || (ci->state.aitype < AI_START && dupname && duplicatename(ci, name)))
+        concatstring(colored, name);
+        if(!name[0] || (ci->state.actortype < A_ENEMY && dupname && duplicatename(ci, name)))
         {
             formatstring(colortmp)("%s[%d]", name[0] ? " " : "", ci->clientnum);
             concatstring(colored, colortmp);
         }
-        concatstring(colored, "\fS");
+        if(colour) concatstring(colored, "\fS");
         return colored;
     }
 
@@ -1097,22 +1141,22 @@ namespace server
 
     bool haspriv(clientinfo *ci, int flag, const char *msg = NULL)
     {
-        if(ci->local || ci->privilege >= flag) return true;
+        if(ci->local || (ci->privilege&PRIV_TYPE) >= flag) return true;
         else if(mastermask()&MM_AUTOAPPROVE && flag <= PRIV_ELEVATED && !numclients(ci->clientnum)) return true;
         else if(msg && *msg)
-            srvmsgft(ci->clientnum, CON_CHAT, "\fraccess denied, you need to be \fs\fc%s\fS to \fs\fc%s\fS", privname(flag, true), msg);
+            srvmsgft(ci->clientnum, CON_CHAT, "\fraccess denied, you need to be \fs\fc%s\fS to \fs\fc%s\fS", privnamex(flag), msg);
         return false;
     }
 
     bool cmppriv(clientinfo *ci, clientinfo *cp, const char *msg = NULL)
     {
-        mkstring(str);
+        string str = "";
         if(msg && *msg) formatstring(str)("%s %s", msg, colourname(cp));
-        if(haspriv(ci, cp->local ? PRIV_MAX : cp->privilege, str)) return true;
+        if(haspriv(ci, cp->local ? PRIV_ADMINISTRATOR : cp->privilege&PRIV_TYPE, str)) return true;
         return false;
     }
 
-    const char *gameid() { return GAMEID; }
+    const char *gameid() { return VERSION_GAMEID; }
     ICOMMAND(0, gameid, "", (), result(gameid()));
 
     int getver(int n)
@@ -1120,7 +1164,7 @@ namespace server
         switch(n)
         {
             case 0: return CUR_VERSION;
-            case 1: return GAMEVERSION;
+            case 1: return VERSION_GAME;
             case 2: case 3: return version[n%2];
             case 4: return CUR_ARCH;
             default: break;
@@ -1181,9 +1225,11 @@ namespace server
         if(type == 2 || type == 3 || type == 4 || type == 5)
         {
             if((type == 4 || type == 5) && m_capture(mode) && m_gsp3(mode, muts)) concatstring(mdname, gametype[mode].gsd[2]);
+            else if((type == 4 || type == 5) && m_defend(mode) && m_gsp2(mode, muts)) concatstring(mdname, gametype[mode].gsd[1]);
             else if((type == 4 || type == 5) && m_bomber(mode) && m_gsp1(mode, muts)) concatstring(mdname, gametype[mode].gsd[0]);
-            else if((type == 4 || type == 5) && m_bomber(mode) && m_gsp2(mode, muts)) concatstring(mdname, gametype[mode].gsd[1]);
-            else if((type == 4 || type == 5) && m_gauntlet(mode) && m_gsp1(mode, muts)) concatstring(mdname, gametype[mode].gsd[0]);
+            else if((type == 4 || type == 5) && m_bomber(mode) && m_gsp3(mode, muts)) concatstring(mdname, gametype[mode].gsd[2]);
+            else if((type == 4 || type == 5) && m_race(mode) && m_gsp1(mode, muts)) concatstring(mdname, gametype[mode].gsd[0]);
+            else if((type == 4 || type == 5) && m_race(mode) && m_gsp3(mode, muts)) concatstring(mdname, gametype[mode].gsd[2]);
             else concatstring(mdname, gametype[mode].desc);
         }
         return mdname;
@@ -1200,8 +1246,9 @@ namespace server
         if(type == 4 || type == 5)
         {
             if(m_capture(mode) && m_gsp3(mode, muts)) return "";
-            else if(m_bomber(mode) && (m_gsp1(mode, muts) || m_gsp2(mode, muts))) return "";
-            else if(m_gauntlet(mode) && m_gsp1(mode, muts)) return "";
+            else if(m_defend(mode) && m_gsp2(mode, muts)) return "";
+            else if(m_bomber(mode) && (m_gsp1(mode, muts) || m_gsp3(mode, muts))) return "";
+            else if(m_race(mode) && (m_gsp1(mode, muts) || m_gsp3(mode, muts))) return "";
         }
         if(type == 1 || type == 3 || type == 4)
         {
@@ -1232,7 +1279,7 @@ namespace server
                 if(!num) mode = rnd(G_RAND)+G_FIGHT;
                 else
                 {
-                    int r = num > 1 ? rnd(num) : 0, n = 0;
+                    int r = rnd(num), n = 0;
                     loopi(G_MAX) if(G(rotatemodefilter)&(1<<i))
                     {
                         if(n != r) n++;
@@ -1260,7 +1307,7 @@ namespace server
         modecheck(mode, muts);
     }
 
-    const char *choosemap(const char *suggest, int mode, int muts, int force)
+    const char *choosemap(const char *suggest, int mode, int muts, int force, bool notry)
     {
         static string chosen;
         if(suggest && *suggest)
@@ -1293,58 +1340,62 @@ namespace server
                 if(found) break;
             }
         }
-        return *chosen ? chosen : pickmap(suggest, mode, muts);
+        return *chosen ? chosen : pickmap(suggest, mode, muts, notry);
     }
 
     bool canload(const char *type)
     {
         if(!strcmp(type, gameid())) return true;
-#ifdef MEK
-        if(!strcmp(type, "fps")) return true;
-#endif
-        if(!strcmp(type, "bfa") || !strcmp(type, "bfg")) return true;
+        if(!strcmp(type, "bfa")) return true;
+        if(!strcmp(type, "bfg")) return true;
         return false;
     }
 
     bool checkvotes(bool force = false);
     void startintermission(bool req = false)
     {
-        if(!interm)
+        if(gs_playing(gamestate))
         {
             setpause(false);
             timeremaining = 0;
             gamelimit = min(gamelimit, gamemillis);
-            inovertime = maprequest = false;
             if(smode) smode->intermission();
             mutate(smuts, mut->intermission());
-            sendf(-1, 1, "ri2", N_TICK, 0);
         }
-        if(req)
+        if(req || !G(intermlimit))
         {
             checkdemorecord(true);
-            if(!maprequest && G(votelimit) && G(votelock) != PRIV_MAX && G(modelock) != PRIV_MAX && G(mapslock) != PRIV_MAX)
-            {
-                sendf(-1, 1, "ri", N_NEWGAME);
-                maprequest = true;
-                if(!(interm = totalmillis+G(votelimit))) interm = 1;
-            }
-            else // if they can't vote, no point in waiting for them to do so
+            if(gamestate != G_S_VOTING && G(votelimit))
             {
-                interm = 0;
-                checkvotes(true);
+                gamestate = G_S_VOTING;
+                gamewait = totalmillis+G(votelimit);
+                sendf(-1, 1, "ri3", N_TICK, G_S_VOTING, G(votelimit)/1000);
             }
+            else checkvotes(true);
         }
         else
         {
-            maprequest = false;
-            if(!(interm = totalmillis+G(intermlimit))) interm = 1;
+            gamestate = G_S_INTERMISSION;
+            gamewait = totalmillis+G(intermlimit);
+            sendf(-1, 1, "ri3", N_TICK, G_S_INTERMISSION, G(intermlimit)/1000);
         }
     }
 
+    int timeleft()
+    {
+        switch(gamestate)
+        {
+            case G_S_PLAYING: case G_S_OVERTIME: return timeremaining;
+            default: return max(gamewait-totalmillis, 0)/1000;
+        }
+        return 0;
+    }
+
     bool wantsovertime()
     {
         if(smode && smode->wantsovertime()) return true;
         mutate(smuts, if(mut->wantsovertime()) return true);
+        if(!G(overtimeallow) || m_balance(gamemode, mutators, teamspawns)) return false;
         bool result = false;
         if(m_team(gamemode, mutators))
         {
@@ -1357,38 +1408,128 @@ namespace server
                     best = i+T_FIRST;
                     result = false;
                 }
-                else if(best >= 0 && cs.total == teamscore(best).total)
-                    result = true;
+                else if(cs.total == teamscore(best).total) result = true;
             }
         }
         else
         {
             int best = -1;
-            loopv(clients) if(clients[i]->state.aitype < AI_START)
+            loopv(clients) if(clients[i]->state.actortype < A_ENEMY && clients[i]->state.state != CS_SPECTATOR)
             {
-                if(best < 0 || clients[i]->state.points > clients[best]->state.points)
+                if(best < 0 || (m_laptime(gamemode, mutators) ? (clients[best]->state.cptime <= 0 || (clients[i]->state.cptime > 0 && clients[i]->state.cptime < clients[best]->state.cptime)) : clients[i]->state.points > clients[best]->state.points))
                 {
                     best = i;
                     result = false;
                 }
-                else if(best >= 0 && clients[i]->state.points == clients[best]->state.points)
-                    result = true;
+                else if(m_laptime(gamemode, mutators) ? clients[i]->state.cptime == clients[best]->state.cptime : clients[i]->state.points == clients[best]->state.points) result = true;
             }
         }
         return result;
     }
 
+    void doteambalance(bool init)
+    {
+        vector<clientinfo *> tc[T_TOTAL];
+        int numplaying = 0;
+        loopv(clients)
+        {
+            clientinfo *cp = clients[i];
+            if(!cp->team || cp->state.state == CS_SPECTATOR || cp->state.actortype > A_PLAYER) continue;
+            cp->state.updatetimeplayed();
+            tc[cp->team-T_FIRST].add(cp);
+            numplaying++;
+        }
+        if(numplaying >= G(teambalanceplaying))
+        {
+            int nt = numteams(gamemode, mutators), mid = numplaying/nt, pmax = -1, pmin = -1;
+            loopi(nt)
+            {
+                int cl = tc[i].length();
+                if(pmax < 0 || cl > pmax) pmax = cl;
+                if(pmin < 0 || cl < pmin) pmin = cl;
+            }
+            int offset = pmax-pmin;
+            if(offset >= G(teambalanceamt))
+            {
+                if(!init && !nextteambalance)
+                {
+                    int secs = G(teambalancedelay)/1000;
+                    nextteambalance = gamemillis+G(teambalancedelay);
+                    ancmsgft(-1, S_V_BALWARN, CON_EVENT, "\fy\fs\fzoyWARNING:\fS \fs\fcteams\fS will be \fs\fcbalanced\fS in \fs\fc%d\fS %s", secs, secs != 1 ? "seconds" : "second");
+                }
+                else if(init || canbalancenow())
+                {
+                    int moved = 0;
+                    loopi(nt) for(int team = i+T_FIRST, iters = tc[i].length(); iters > 0 && tc[i].length() > mid; iters--)
+                    {
+                        int id = -1;
+                        loopvj(tc[i])
+                        {
+                            clientinfo *cp = tc[i][j];
+                            if(m_swapteam(gamemode, mutators) && cp->swapteam && cp->swapteam == team) { id = j; break; }
+                            switch(G(teambalancestyle))
+                            {
+                                case 1: if(id < 0 || tc[i][id]->state.timeplayed > cp->state.timeplayed) id = j; break;
+                                case 2: if(id < 0 || tc[i][id]->state.points > cp->state.points) id = j; break;
+                                case 3: if(id < 0 || tc[i][id]->state.frags > cp->state.frags) id = j; break;
+                                case 4: if(id < 0 || tc[i][id]->state.scoretime(false) > cp->state.scoretime(false)) id = j; break;
+                                case 0: default: if(id < 0) id = j; break;
+                            }
+                            if(!G(teambalancestyle) && !m_swapteam(gamemode, mutators)) break;
+                        }
+                        if(id >= 0)
+                        {
+                            clientinfo *cp = tc[i][id];
+                            cp->swapteam = T_NEUTRAL; // make them rechoose if necessary
+                            int t = chooseteam(cp, -1, true);
+                            if(t != cp->team)
+                            {
+                                setteam(cp, t, (m_balreset(gamemode, mutators) ? TT_RESET : 0)|TT_INFOSM, false);
+                                cp->state.lastdeath = 0;
+                                tc[i].removeobj(cp);
+                                tc[t-T_FIRST].add(cp);
+                                moved++;
+                            }
+                        }
+                        else break; // won't get any more
+                    }
+                    if(!init)
+                    {
+                        if(moved) ancmsgft(-1, S_V_BALALERT, CON_EVENT, "\fy\fs\fzoyALERT:\fS \fs\fcteams\fS have now been \fs\fcbalanced\fS");
+                        else ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fy\fs\fzoyALERT:\fS \fs\fcteams\fS failed to be \fs\fcbalanced\fS");
+                    }
+                    lastteambalance = gamemillis+G(teambalancewait);
+                    nextteambalance = 0;
+                }
+            }
+            else
+            {
+                if(!init && nextteambalance) ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fy\fs\fzoyALERT:\fS \fs\fcteams\fS no longer need to be \fs\fcbalanced\fS");
+                lastteambalance = gamemillis+(nextteambalance ? G(teambalancewait) : G(teambalancedelay));
+                nextteambalance = 0;
+            }
+        }
+        else
+        {
+            if(!init && nextteambalance) ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fy\fs\fzoyALERT:\fS \fs\fcteams\fS are no longer able to be \fs\fcbalanced\fS");
+            lastteambalance = gamemillis+(nextteambalance ? G(teambalancewait) : G(teambalancedelay));
+            nextteambalance = 0;
+        }
+    }
+
     void checklimits()
     {
         if(!m_fight(gamemode)) return;
-        int limit = inovertime ? max(G(overtimelimit), 1) : G(timelimit), numt = numteams(gamemode, mutators);
+        bool wasinovertime = gamestate == G_S_OVERTIME;
+        int limit = wasinovertime ? G(overtimelimit) : G(timelimit), numt = numteams(gamemode, mutators);
         bool newlimit = limit != oldtimelimit, newtimer = gamemillis-curtime>0 && gamemillis/1000!=(gamemillis-curtime)/1000,
-             iterate = newlimit || newtimer, wasinovertime = inovertime;
+             iterate = newlimit || newtimer;
         if(iterate)
         {
             if(newlimit)
             {
-                if(limit) gamelimit += (limit-oldtimelimit)*60000;
+                if(limit && oldtimelimit) gamelimit += (limit-oldtimelimit)*60000;
+                else if(limit) gamelimit = max(gamemillis, limit*60000);
                 oldtimelimit = limit;
             }
             if(timeremaining)
@@ -1402,7 +1543,7 @@ namespace server
                 bool wantsoneminute = true;
                 if(!timeremaining)
                 {
-                    if(!inovertime && !m_balance(gamemode, mutators) && G(overtimeallow) && wantsovertime())
+                    if(gamestate != G_S_OVERTIME && wantsovertime())
                     {
                         limit = oldtimelimit = G(overtimelimit);
                         if(limit)
@@ -1417,7 +1558,7 @@ namespace server
                             gamelimit = 0;
                             ancmsgft(-1, S_V_OVERTIME, CON_EVENT, "\fyovertime, match extended until someone wins");
                         }
-                        inovertime = true;
+                        gamestate = G_S_OVERTIME;
                         wantsoneminute = false;
                     }
                     else
@@ -1427,9 +1568,9 @@ namespace server
                         return; // bail
                     }
                 }
-                if(timeremaining != 0)
+                if(gs_playing(gamestate) && timeremaining != 0)
                 {
-                    sendf(-1, 1, "ri2", N_TICK, timeremaining);
+                    sendf(-1, 1, "ri3", N_TICK, gamestate, timeremaining);
                     if(wantsoneminute && timeremaining == 60) ancmsgft(-1, S_V_ONEMINUTE, CON_EVENT, "\fzygone minute remains");
                 }
             }
@@ -1440,7 +1581,7 @@ namespace server
             startintermission();
             return; // bail
         }
-        if(!m_balance(gamemode, mutators) && G(pointlimit) && m_teamscore(gamemode))
+        if(!m_balance(gamemode, mutators, teamspawns) && G(pointlimit) && m_dm(gamemode))
         {
             if(m_team(gamemode, mutators))
             {
@@ -1457,7 +1598,7 @@ namespace server
             else
             {
                 int best = -1;
-                loopv(clients) if(clients[i]->state.aitype < AI_START && (best < 0 || clients[i]->state.points > clients[best]->state.points))
+                loopv(clients) if(clients[i]->state.actortype < A_ENEMY && (best < 0 || clients[i]->state.points > clients[best]->state.points))
                     best = i;
                 if(best >= 0 && clients[best]->state.points >= G(pointlimit))
                 {
@@ -1467,73 +1608,78 @@ namespace server
                 }
             }
         }
-        if(iterate && m_balance(gamemode, mutators) && gamelimit > 0 && curbalance < (numt-1))
+        if(m_balance(gamemode, mutators, teamspawns) && gamelimit > 0 && curbalance < (numt-1))
         {
             int delpart = min(gamelimit/(numt*2), G(balancedelay)), balpart = (gamelimit/numt*(curbalance+1))-delpart;
             if(gamemillis >= balpart)
             {
                 if(!nextbalance)
                 {
-                    nextbalance = NZT(gamemillis+delpart);
+                    nextbalance = gamemillis+delpart;
                     if(delpart >= 1000)
                     {
                         int secs = delpart/1000;
-                        ancmsgft(-1, S_V_BALWARN, CON_EVENT, "\fy\fs\fzoyWARNING\fS: \fs\fcteams\fS will be \fs\fcreassigned\fS in \fs\fc%d\fS %s%s", secs, secs != 1 ? "seconds" : "second", !m_gauntlet(gamemode) ? " for map symmetry" : "");
+                        ancmsgft(-1, S_V_BALWARN, CON_EVENT, "\fy\fs\fzoyWARNING:\fS \fs\fcteams\fS will be \fs\fcreassigned\fS in \fs\fc%d\fS %s %s", secs, secs != 1 ? "seconds" : "second", m_forcebal(gamemode, mutators) ? "to switch roles" : "for map symmetry");
                     }
                 }
-                if(gamemillis >= nextbalance)
+                if(gamemillis >= nextbalance && canbalancenow())
                 {
                     int oldbalance = curbalance;
                     if(++curbalance >= numt) curbalance = 0; // safety first
-                    nextbalance = 0;
-                    if(smode) smode->balance(oldbalance);
-                    mutate(smuts, mut->balance(oldbalance));
                     static vector<clientinfo *> assign[T_TOTAL];
                     loopk(T_TOTAL) assign[k].setsize(0);
                     loopv(clients) if(isteam(gamemode, mutators, clients[i]->team, T_FIRST))
                         assign[clients[i]->team-T_FIRST].add(clients[i]);
-                    int scores[T_TOTAL] = {0}, flags = (m_balreset(gamemode) ? TT_DEFAULT : 0)|TT_INFO;
+                    int scores[T_TOTAL] = {0};
                     loopk(numt) scores[k] = teamscore(k+T_FIRST).total;
                     loopk(numt)
                     {
                         int from = mapbals[oldbalance][k], fromt = from-T_FIRST,
                             to = mapbals[curbalance][k], tot = to-T_FIRST;
-                        loopv(assign[fromt]) setteam(assign[fromt][i], to, flags);
+                        loopv(assign[fromt])
+                        {
+                            clientinfo *cp = assign[fromt][i];
+                            if(cp->swapteam)
+                            {
+                                loopj(numt) if(mapbals[oldbalance][j] == cp->swapteam)
+                                {
+                                    cp->swapteam = mapbals[curbalance][j];
+                                    break;
+                                }
+                            }
+                            if(m_race(gamemode))
+                            {
+                                cp->state.cpmillis = 0;
+                                cp->state.cpnodes.shrink(0);
+                                sendf(-1, 1, "ri3", N_CHECKPOINT, cp->clientnum, -1);
+                            }
+                            setteam(cp, to, (m_balreset(gamemode, mutators) ? TT_RESET : 0)|TT_INFO, false);
+                            cp->state.lastdeath = 0;
+                        }
                         score &cs = teamscore(from);
                         cs.total = scores[tot];
                         sendf(-1, 1, "ri3", N_SCORE, cs.team, cs.total);
                     }
-                    ancmsgft(-1, S_V_BALALERT, CON_EVENT, "\fy\fs\fzoyALERT\fS: \fs\fcteams\fS have %sbeen \fs\fcreassigned\fS%s", delpart > 0 ? "now " : "", !m_gauntlet(gamemode) ? " for map symmetry" : "");
+                    ancmsgft(-1, S_V_BALALERT, CON_EVENT, "\fy\fs\fzoyALERT:\fS \fs\fcteams\fS have %sbeen \fs\fcreassigned\fS %s", delpart > 0 ? "now " : "", m_forcebal(gamemode, mutators) ? "to switch roles" : "for map symmetry");
+                    if(smode) smode->balance(oldbalance);
+                    mutate(smuts, mut->balance(oldbalance));
                     if(smode) smode->layout();
                     mutate(smuts, mut->layout());
+                    nextbalance = 0;
                 }
             }
         }
+        if(m_balteam(gamemode, mutators, 4) && gamestate != G_S_OVERTIME && gamemillis >= G(teambalancewait) &&
+           (!lastteambalance || gamemillis >= lastteambalance) && (!nextteambalance || gamemillis >= nextteambalance))
+                doteambalance(false);
     }
 
     bool hasitem(int i)
     {
-        if(!sents.inrange(i) || m_noitems(gamemode, mutators)) return false;
-        switch(sents[i].type)
-        {
-            case WEAPON:
-            {
-                int sweap = m_weapon(gamemode, mutators), attr = w_attr(gamemode, mutators, sents[i].attrs[0], sweap);
-                if(!isweap(attr) || (sweap < 0 && attr < 0-sweap) || !m_check(W(attr, modes), W(attr, muts), gamemode, mutators))
-                    return false;
-                if((sents[i].attrs[4] && sents[i].attrs[4] != triggerid) || !m_check(sents[i].attrs[2], sents[i].attrs[3], gamemode, mutators))
-                    return false;
-                break;
-            }
-#ifdef MEK
-            case HEALTH: case ARMOUR:
-            {
-                if(m_insta(gamemode, mutators) || (sents[i].attrs[3] && sents[i].attrs[3] != triggerid) || !m_check(sents[i].attrs[1], sents[i].attrs[2], gamemode, mutators)) return false;
-                break;
-            }
-#endif
-            default: break;
-        }
+        if((m_race(gamemode) && !m_gsp3(gamemode, mutators)) || m_basic(gamemode, mutators) || !sents.inrange(i) || sents[i].type != WEAPON) return false;
+        int sweap = m_weapon(gamemode, mutators), attr = w_attr(gamemode, mutators, sents[i].type, sents[i].attrs[0], sweap);
+        if(!isweap(attr) || !w_item(attr, sweap) || !m_check(W(attr, modes), W(attr, muts), gamemode, mutators) || W(attr, disabled)) return false;
+        if((sents[i].attrs[4] && sents[i].attrs[4] != triggerid) || !m_check(sents[i].attrs[2], sents[i].attrs[3], gamemode, mutators)) return false;
         return true;
     }
 
@@ -1563,34 +1709,29 @@ namespace server
 
     void setupitems(bool update)
     {
-        static vector<int> items, actors;
-        items.setsize(0); actors.setsize(0);
+        vector<int> items, enemies;
         int sweap = m_weapon(gamemode, mutators);
         loopv(sents)
         {
-            if(sents[i].type == ACTOR && sents[i].attrs[0] >= 0 && sents[i].attrs[0] < AI_TOTAL && (sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))
+            if(sents[i].type == ACTOR && sents[i].attrs[0] >= 0 && sents[i].attrs[0] < A_TOTAL && (sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))
             {
-#ifdef CAMPAIGN
-                sents[i].millis += m_campaign(gamemode) ? 50 : G(enemyspawndelay);
-#else
-                sents[i].millis += G(enemyspawndelay);
-#endif
+                sents[i].millis = gamemillis+G(enemyspawndelay);
                 switch(G(enemyspawnstyle) == 3 ? rnd(2)+1 : G(enemyspawnstyle))
                 {
-                    case 1: actors.add(i); break;
+                    case 1: enemies.add(i); break;
                     case 2: sents[i].millis += (G(enemyspawntime)+rnd(G(enemyspawntime)))/2; break;
                     default: break;
                 }
             }
             else if(m_fight(gamemode) && enttype[sents[i].type].usetype == EU_ITEM && hasitem(i))
             {
-                sents[i].millis += G(itemspawndelay);
+                sents[i].millis = gamemillis+G(itemspawndelay);
                 switch(G(itemspawnstyle) == 3 ? rnd(2)+1 : G(itemspawnstyle))
                 {
                     case 1: items.add(i); break;
                     case 2:
                     {
-                        int delay = sents[i].type == WEAPON ? w_spawn(w_attr(gamemode, mutators, sents[i].attrs[0], sweap)) : G(itemspawntime);
+                        int delay = sents[i].type == WEAPON ? w_spawn(w_attr(gamemode, mutators, sents[i].type, sents[i].attrs[0], sweap)) : G(itemspawntime);
                         if(delay > 1) sents[i].millis += (delay+rnd(delay))/2;
                         break;
                     }
@@ -1603,37 +1744,32 @@ namespace server
             sortrandomly(items);
             loopv(items) sents[items[i]].millis += G(itemspawndelay)*i;
         }
-        if(!actors.empty())
+        if(!enemies.empty())
         {
-            sortrandomly(actors);
-            loopv(actors)
-#ifdef CAMPAIGN
-                sents[actors[i]].millis += (m_campaign(gamemode) ? 50 : G(enemyspawndelay))*i;
-#else
-                sents[actors[i]].millis += G(enemyspawndelay)*i;
-#endif
+            sortrandomly(enemies);
+            loopv(enemies) sents[enemies[i]].millis += G(enemyspawndelay)*i;
         }
     }
 
     void setuptriggers(bool update)
     {
+        triggerid = 0;
         loopi(TRIGGERIDS+1) triggers[i].reset(i);
-        if(update)
-        {
-            loopv(sents) if(sents[i].type == TRIGGER && sents[i].attrs[4] >= 2 && sents[i].attrs[0] >= 0 && sents[i].attrs[0] <= TRIGGERIDS+1 && m_check(sents[i].attrs[5], sents[i].attrs[6], gamemode, mutators))
-                triggers[sents[i].attrs[0]].ents.add(i);
-        }
-        else triggerid = 0;
+        if(!update) return;
 
-        if(triggerid <= 0)
+        loopv(sents) if(enttype[sents[i].type].idattr >= 0 && sents[i].attrs[enttype[sents[i].type].idattr] >= 0 && sents[i].attrs[enttype[sents[i].type].idattr] <= TRIGGERIDS)
         {
-            static vector<int> valid; valid.setsize(0);
-            loopi(TRIGGERIDS) if(!triggers[i+1].ents.empty()) valid.add(triggers[i+1].id);
-            if(!valid.empty()) triggerid = valid[rnd(valid.length())];
+            if(enttype[sents[i].type].modesattr >= 0 && !m_check(sents[i].attrs[enttype[sents[i].type].modesattr], sents[i].attrs[enttype[sents[i].type].modesattr+1], gamemode, mutators)) continue;
+            triggers[sents[i].attrs[enttype[sents[i].type].idattr]].ents.add(i);
         }
 
-        if(triggerid > 0) loopi(TRIGGERIDS) if(triggers[i+1].id != triggerid) loopvk(triggers[i+1].ents)
+        vector<int> valid;
+        loopi(TRIGGERIDS) if(!triggers[i+1].ents.empty()) valid.add(triggers[i+1].id);
+        if(!valid.empty()) triggerid = valid[rnd(valid.length())];
+
+        loopi(TRIGGERIDS) if(triggers[i+1].id != triggerid) loopvk(triggers[i+1].ents)
         {
+            if(sents[triggers[i+1].ents[k]].type != TRIGGER) continue;
             bool spawn = sents[triggers[i+1].ents[k]].attrs[4]%2;
             if(spawn != sents[triggers[i+1].ents[k]].spawned)
             {
@@ -1675,11 +1811,43 @@ namespace server
     void setupspawns(bool update)
     {
         totalspawns = 0;
+        teamspawns = m_team(gamemode, mutators);
         loopi(T_ALL) spawns[i].reset();
         if(update)
         {
             int numt = numteams(gamemode, mutators), cplayers = 0;
-            bool teamspawns = m_team(gamemode, mutators) && !m_trial(gamemode);
+            if(m_race(gamemode))
+            {
+                loopv(sents) if(sents[i].type == PLAYERSTART && sents[i].attrs[0] == T_NEUTRAL && (sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))
+                {
+                    spawns[m_gsp3(gamemode, mutators) ? T_ALPHA : T_NEUTRAL].add(i);
+                    totalspawns++;
+                }
+                if(!totalspawns) loopv(sents) if(sents[i].type == CHECKPOINT && sents[i].attrs[6] == CP_START && (sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))
+                {
+                    spawns[m_gsp3(gamemode, mutators) ? T_ALPHA : T_NEUTRAL].add(i);
+                    totalspawns++;
+                }
+                if(m_gsp3(gamemode, mutators))
+                {
+                    int enemyspawns = 0;
+                    loopv(sents) if(sents[i].type == PLAYERSTART && sents[i].attrs[0] >= T_OMEGA && (sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))
+                    {
+                        loopk(numt-1) spawns[T_OMEGA+k].add(i);
+                        totalspawns++;
+                        enemyspawns++;
+                    }
+                    if(!enemyspawns) loopv(sents) if(sents[i].type == CHECKPOINT && sents[i].attrs[6] == CP_RESPAWN && (sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))
+                    {
+                        loopk(numt-1) spawns[T_OMEGA+k].add(i);
+                        totalspawns++;
+                        enemyspawns++;
+                    }
+                }
+                setmod(sv_numplayers, 0);
+                setmod(sv_maxplayers, 0);
+                return;
+            }
             if(!teamspawns && m_duel(gamemode, mutators))
             { // iterate through teams so players spawn on opposite sides in duel
                 teamspawns = true;
@@ -1698,7 +1866,7 @@ namespace server
                         spawns[k ? T_NEUTRAL : sents[i].attrs[0]].add(i);
                         totalspawns++;
                     }
-                    if(!k && m_team(gamemode, mutators))
+                    if(totalspawns && m_team(gamemode, mutators))
                     {
                         loopi(numt) if(spawns[i+T_FIRST].ents.empty())
                         {
@@ -1708,29 +1876,46 @@ namespace server
                         }
                     }
                     if(totalspawns) break;
+                    teamspawns = false;
+                }
+                if(totalspawns && teamspawns)
+                {
+                    int actt = numteams(gamemode, mutators), off = numt-actt;
+                    if(off > 0) loopk(off)
+                    {
+                        int t = T_ALPHA+k*2, v = t+2;
+                        if(isteam(gamemode, mutators, t, T_FIRST) && isteam(gamemode, mutators, v, T_FIRST))
+                            loopv(spawns[t].ents) spawns[v].add(spawns[t].ents[i]);
+                    }
                 }
             }
-            else
+            if(!totalspawns)
             { // use all neutral spawns
+                teamspawns = false;
                 loopv(sents) if(sents[i].type == PLAYERSTART && sents[i].attrs[0] == T_NEUTRAL && (sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))
                 {
                     spawns[T_NEUTRAL].add(i);
                     totalspawns++;
                 }
             }
-            if(!totalspawns) loopk(2)
+            if(!totalspawns)
             { // use all spawns
-                loopv(sents) if(sents[i].type == PLAYERSTART && (k || ((sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))))
+                teamspawns = false;
+                loopk(2)
                 {
-                    spawns[T_NEUTRAL].add(i);
-                    totalspawns++;
+                    loopv(sents) if(sents[i].type == PLAYERSTART && (k || ((sents[i].attrs[5] == triggerid || !sents[i].attrs[5]) && m_check(sents[i].attrs[3], sents[i].attrs[4], gamemode, mutators))))
+                    {
+                        spawns[T_NEUTRAL].add(i);
+                        totalspawns++;
+                    }
+                    if(totalspawns) break;
                 }
-                if(totalspawns) break;
             }
 
             if(totalspawns) cplayers = totalspawns/2;
             else
             { // we can cheat and use weapons for spawns
+                teamspawns = false;
                 loopv(sents) if(sents[i].type == WEAPON)
                 {
                     spawns[T_NEUTRAL].add(i);
@@ -1741,7 +1926,7 @@ namespace server
             if(!m_edit(gamemode))
             {
                 if(!cplayers) cplayers = totalspawns ? totalspawns : 1;
-                int np = G(numplayers) ? G(numplayers) : cplayers, mp = G(maxplayers) ? G(maxplayers) : np*5/2;
+                int np = G(numplayers) ? G(numplayers) : cplayers, mp = G(maxplayers) ? G(maxplayers) : np*3;
                 if(m_fight(gamemode) && m_team(gamemode, mutators))
                 {
                     int offt = np%numt, offq = mp%numt;
@@ -1757,10 +1942,10 @@ namespace server
 
     int pickspawn(clientinfo *ci)
     {
-        if(ci->state.aitype >= AI_START) return ci->state.aientity;
+        if(ci->state.actortype >= A_ENEMY) return ci->state.spawnpoint;
         else
         {
-            if(m_checkpoint(gamemode) && !m_gauntlet(gamemode) && !ci->state.cpnodes.empty())
+            if(m_race(gamemode) && !ci->state.cpnodes.empty() && (!m_gsp3(gamemode, mutators) || ci->team == T_ALPHA))
             {
                 int checkpoint = ci->state.cpnodes.last();
                 if(sents.inrange(checkpoint)) return checkpoint;
@@ -1774,9 +1959,8 @@ namespace server
                         team = spawns[T_ALPHA].iteration <= spawns[T_OMEGA].iteration ? T_ALPHA : T_OMEGA;
                     if(!rotate) rotate = 2;
                 }
-                else if(m_fight(gamemode) && m_team(gamemode, mutators) && !m_trial(gamemode) && !spawns[ci->team].ents.empty())
-                    team = ci->team;
-                switch(rotate)
+                else if(m_fight(gamemode) && m_team(gamemode, mutators) && (!m_race(gamemode) || m_gsp3(gamemode, mutators)) && !spawns[ci->team].ents.empty()) team = ci->team;
+                else switch(rotate)
                 {
                     case 2:
                     { // random
@@ -1819,29 +2003,25 @@ namespace server
         setupitems(true);
         setupspawns(true);
         hasgameinfo = true;
-        aiman::dorefresh = numclients() >= 2 ? (m_duke(gamemode, mutators) || m_bomber(gamemode) ? G(ailongdelay) : G(aiinitdelay)) : G(airefreshdelay);
+        aiman::poke();
     }
 
     void sendspawn(clientinfo *ci)
     {
         servstate &gs = ci->state;
         int weap = -1, health = 0;
-        if(ci->state.aitype >= AI_START)
+        if(ci->state.actortype >= A_ENEMY)
         {
-            bool hasent = sents.inrange(ci->state.aientity) && sents[ci->state.aientity].type == ACTOR;
-            if(m_insta(gamemode, mutators) && !m_loadout(gamemode, mutators)) weap = m_weapon(gamemode, mutators);
-            else weap = hasent && sents[ci->state.aientity].attrs[6] > 0 ? sents[ci->state.aientity].attrs[6]-1 : aistyle[ci->state.aitype].weap;
-            if(!m_insta(gamemode, mutators))
-            {
-                int heal = hasent && sents[ci->state.aientity].attrs[7] > 0 ? sents[ci->state.aientity].attrs[7] : aistyle[ci->state.aitype].health,
-                    amt = heal/2+rnd(heal);
-                health = G(enemystrength) != 1 ? max(int(amt*G(enemystrength)), 1) : amt;
-            }
-            if(!isweap(weap)) weap = rnd(W_MAX-1)+1;
+            bool hasent = sents.inrange(ci->state.spawnpoint) && sents[ci->state.spawnpoint].type == ACTOR;
+            if(m_sweaps(gamemode, mutators)) weap = m_weapon(gamemode, mutators);
+            else weap = hasent && sents[ci->state.spawnpoint].attrs[6] > 0 ? sents[ci->state.spawnpoint].attrs[6]-1 : actor[ci->state.actortype].weap;
+            if(!m_insta(gamemode, mutators)) health = max(int((hasent && sents[ci->state.spawnpoint].attrs[7] > 0 ? sents[ci->state.spawnpoint].attrs[7] : actor[ci->state.actortype].health)*G(enemystrength)), 1);
+            if(!isweap(weap) || (!actor[ci->state.actortype].canmove && weaptype[weap].melee)) weap = -1; // let spawnstate figure it out
         }
+        else health = m_health(gamemode, mutators, ci->state.model);
         int spawn = pickspawn(ci);
-        gs.spawnstate(gamemode, mutators, weap, health, 0);
-        sendf(ci->clientnum, 1, "ri9i3vv", N_SPAWNSTATE, ci->clientnum, spawn, gs.state, gs.points, gs.frags, gs.deaths, gs.health, gs.armour, gs.cptime, gs.cplaps, gs.weapselect, W_MAX, &gs.ammo[0], W_MAX, &gs.reloads[0]);
+        gs.spawnstate(gamemode, mutators, weap, health);
+        sendf(ci->clientnum, 1, "ri9iv", N_SPAWNSTATE, ci->clientnum, spawn, gs.state, gs.points, gs.frags, gs.deaths, gs.health, gs.cptime, gs.weapselect, W_MAX, &gs.ammo[0]);
         gs.lastrespawn = gs.lastspawn = gamemillis;
     }
 
@@ -1853,47 +2033,44 @@ namespace server
         putint(p, gs.frags);
         putint(p, gs.deaths);
         putint(p, gs.health);
-        putint(p, gs.armour);
         putint(p, gs.cptime);
-        putint(p, gs.cplaps);
         putint(p, gs.weapselect);
         loopi(W_MAX) putint(p, gs.ammo[i]);
-        loopi(W_MAX) putint(p, gs.reloads[i]);
     }
 
     void relayf(int r, const char *s, ...)
     {
-        defvformatstring(str, s, s);
+        defvformatbigstring(str, s, s);
         ircoutf(r, "%s", str);
 #ifdef STANDALONE
-        string ft;
-        filtertext(ft, str);
+        bigstring ft;
+        filterbigstring(ft, str);
         logoutf("%s", ft);
 #endif
     }
 
     void ancmsgft(int cn, int snd, int conlevel, const char *s, ...)
     {
-        defvformatstring(str, s, s);
+        defvformatbigstring(str, s, s);
         if(cn < 0 || allowbroadcast(cn)) sendf(cn, 1, "ri3s", N_ANNOUNCE, snd, conlevel, str);
     }
 
     void srvmsgft(int cn, int conlevel, const char *s, ...)
     {
-        defvformatstring(str, s, s);
+        defvformatbigstring(str, s, s);
         if(cn < 0 || allowbroadcast(cn)) sendf(cn, 1, "ri2s", N_SERVMSG, conlevel, str);
     }
 
     void srvmsgftforce(int cn, int conlevel, const char *s, ...)
     {
-        defvformatstring(str, s, s);
+        defvformatbigstring(str, s, s);
         if(cn < 0 || allowbroadcast(cn)) sendf(cn, 1, "ri2s", N_SERVMSG, conlevel, str);
         if(cn >= 0 && !allowbroadcast(cn)) sendf(cn, 1, "ri2s", N_SERVMSG, conlevel, str);
     }
 
     void srvmsgf(int cn, const char *s, ...)
     {
-        defvformatstring(str, s, s);
+        defvformatbigstring(str, s, s);
         if(cn < 0 || allowbroadcast(cn))
         {
             int conlevel = CON_MESG;
@@ -1909,14 +2086,14 @@ namespace server
 
     void srvoutf(int r, const char *s, ...)
     {
-        defvformatstring(str, s, s);
+        defvformatbigstring(str, s, s);
         srvmsgf(r >= 0 ? -1 : -2, "%s", str);
         relayf(abs(r), "%s", str);
     }
 
     void srvoutforce(clientinfo *ci, int r, const char *s, ...)
     {
-        defvformatstring(str, s, s);
+        defvformatbigstring(str, s, s);
         srvmsgf(r >= 0 ? -1 : -2, "%s", str);
         if(!allowbroadcast(ci->clientnum))
             sendf(ci->clientnum, 1, "ri2s", N_SERVMSG, r >= 0 ? int(CON_MESG) : int(CON_EVENT), str);
@@ -1928,7 +2105,12 @@ namespace server
         packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
         putint(p, N_SENDDEMOLIST);
         putint(p, demos.length());
-        loopv(demos) sendstring(demos[i].info, p);
+        loopv(demos)
+        {
+            sendstring(demos[i].info, p);
+            putint(p, demos[i].len);
+            putint(p, demos[i].ctime);
+        }
         sendpacket(cn, 1, p.finalize());
     }
 
@@ -1953,7 +2135,7 @@ namespace server
         if(!num) num = demos.length();
         if(!demos.inrange(num-1)) return;
         demofile &d = demos[num-1];
-        sendf(cn, 2, "rim", N_SENDDEMO, d.len, d.data);
+        sendf(cn, 2, "ri2m", N_SENDDEMO, d.ctime, d.len, d.data);
     }
 
     void sendwelcome(clientinfo *ci);
@@ -1967,22 +2149,23 @@ namespace server
         srvoutf(4, "\fydemo playback finished");
         loopv(clients) sendwelcome(clients[i]);
         startintermission(true);
+        resetgamevars(true, true);
     }
 
     void setupdemoplayback()
     {
         demoheader hdr;
-        mkstring(msg);
+        string msg = "";
         defformatstring(file)(strstr(smapname, "maps/")==smapname || strstr(smapname, "maps\\")==smapname ? "%s.dmo" : "demos/%s.dmo", smapname);
         demoplayback = opengzfile(file, "rb");
         if(!demoplayback) formatstring(msg)("\frcould not read demo \fs\fc%s\fS", file);
-        else if(demoplayback->read(&hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic)))
+        else if(demoplayback->read(&hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(hdr.magic, VERSION_DEMOMAGIC, sizeof(hdr.magic)))
             formatstring(msg)("\frsorry, \fs\fc%s\fS is not a demo file", file);
         else
         {
-            lilswap(&hdr.version, 2);
-            if(hdr.version!=DEMO_VERSION) formatstring(msg)("\frdemo \fs\fc%s\fS requires %s version of %s", file, hdr.version<DEMO_VERSION ? "an older" : "a newer", versionname);
-            else if(hdr.gamever!=GAMEVERSION) formatstring(msg)("\frdemo \fs\fc%s\fS requires %s version of %s", file, hdr.gamever<GAMEVERSION ? "an older" : "a newer", versionname);
+            lilswap(&hdr.gamever, 4);
+            if(hdr.gamever!=VERSION_GAME)
+                formatstring(msg)("\frdemo \fs\fc%s\fS requires %s version of %s", file, hdr.gamever<VERSION_GAME ? "an older" : "a newer", VERSION_NAME);
         }
         if(msg[0])
         {
@@ -1992,7 +2175,6 @@ namespace server
         }
 
         srvoutf(4, "\fyplaying demo \fs\fc%s\fS", file);
-
         sendf(-1, 1, "ri3", N_DEMOPLAYBACK, 1, -1);
 
         if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback))
@@ -2018,7 +2200,7 @@ namespace server
             lilswap(&chan, 1);
             lilswap(&len, 1);
             ENetPacket *packet = enet_packet_create(NULL, len, 0);
-            if(!packet || demoplayback->read(packet->data, len)!=len)
+            if(!packet || demoplayback->read(packet->data, len)!=size_t(len))
             {
                 if(packet) enet_packet_destroy(packet);
                 enddemoplayback();
@@ -2049,12 +2231,11 @@ namespace server
         if(!demotmp) return;
         int len = (int)min(demotmp->size(), stream::offset((G(demomaxsize)<<20) + 0x10000));
         demofile &d = demos.add();
-        char *timestr = ctime(&clocktime), *trim = timestr + strlen(timestr);
-        while(trim>timestr && iscubespace(*--trim)) *trim = '\0';
-        formatstring(d.info)("%s: %s, %s, %.2f%s", timestr, gamename(gamemode, mutators, 0, 32), smapname, len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB");
-        srvoutf(4, "\fydemo \fs\fc%s\fS recorded", d.info);
+        d.ctime = clocktime;
         d.data = new uchar[len];
         d.len = len;
+        formatstring(d.info)("%s on %s", gamename(gamemode, mutators, 0, 32), smapname);
+        srvoutf(4, "\fydemo \fs\fc%s\fS recorded \fs\fc%s UTC\fS [\fs\fw%.2f%s\fS]", d.info, gettime(d.ctime, "%Y-%m-%d %H:%M.%S"), d.len > 1024*1024 ? d.len/(1024*1024.f) : d.len/1024.0f, d.len > 1024*1024 ? "MB" : "kB");
         demotmp->seek(0, SEEK_SET);
         demotmp->read(d.data, len);
         DELETEP(demotmp);
@@ -2080,7 +2261,7 @@ namespace server
         lilswap(stamp, 3);
         demorecord->write(stamp, sizeof(stamp));
         demorecord->write(data, len);
-        if(demorecord->rawtell() >= (G(demomaxsize)<<20)) enddemorecord(interm != 0);
+        if(demorecord->rawtell() >= (G(demomaxsize)<<20)) enddemorecord(!gs_playing(gamestate));
     }
 
     void recordpacket(int chan, void *data, int len)
@@ -2098,15 +2279,16 @@ namespace server
         stream *f = opengzfile(NULL, "wb", demotmp);
         if(!f) { DELETEP(demotmp); return; }
 
-        //srvoutf(4, "\fyrecording demo");
-
         demorecord = f;
 
         demoheader hdr;
-        memcpy(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic));
-        hdr.version = DEMO_VERSION;
-        hdr.gamever = GAMEVERSION;
-        lilswap(&hdr.version, 2);
+        memcpy(hdr.magic, VERSION_DEMOMAGIC, sizeof(hdr.magic));
+        hdr.gamever = VERSION_GAME;
+        hdr.gamemode = gamemode;
+        hdr.mutators = mutators;
+        hdr.starttime = clocktime;
+        lilswap(&hdr.gamever, 4);
+        copystring(hdr.mapname, smapname);
         demorecord->write(&hdr, sizeof(demoheader));
 
         packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
@@ -2120,57 +2302,67 @@ namespace server
         checkdemorecord(true);
         setmod(sv_botoffset, 0);
         if(G(resetmmonend) >= 2) { mastermode = MM_OPEN; resetallows(); }
-        if(G(resetvarsonend) >= 2) resetgamevars(true);
+        if(G(resetvarsonend) >= 2) resetgamevars(true, false);
         if(G(resetbansonend) >= 2) resetbans();
         if(G(resetmutesonend) >= 2) resetmutes();
         if(G(resetlimitsonend) >= 2) resetlimits();
+        if(G(resetexceptsonend) >= 2) resetexcepts();
     }
 
     bool checkvotes(bool force)
     {
         shouldcheckvotes = false;
-
+        int style = gamestate == G_S_VOTING ? G(voteinterm) : G(votestyle);
+        if(style == 3 && !force) return false;
         vector<votecount> votes;
         int maxvotes = 0;
         loopv(clients)
         {
             clientinfo *oi = clients[i];
-            if(oi->state.aitype > AI_NONE) continue;
+            if(oi->state.actortype > A_PLAYER) continue;
             maxvotes++;
             if(!oi->mapvote[0]) continue;
-            votecount *vc = NULL;
-            loopvj(votes) if(!strcmp(oi->mapvote, votes[j].map) && oi->modevote == votes[j].mode && oi->mutsvote == votes[j].muts)
+            if(style == 3) votes.add(votecount(oi->mapvote, oi->modevote, oi->mutsvote));
+            else
             {
-                vc = &votes[j];
-                break;
+                votecount *vc = NULL;
+                loopvj(votes) if(!strcmp(oi->mapvote, votes[j].map) && oi->modevote == votes[j].mode && oi->mutsvote == votes[j].muts)
+                {
+                    vc = &votes[j];
+                    break;
+                }
+                if(!vc) vc = &votes.add(votecount(oi->mapvote, oi->modevote, oi->mutsvote));
+                vc->count++;
             }
-            if(!vc) vc = &votes.add(votecount(oi->mapvote, oi->modevote, oi->mutsvote));
-            vc->count++;
         }
 
         votecount *best = NULL;
-        int morethanone = 0;
-        loopv(votes) if(!best || votes[i].count >= best->count)
-        {
-            if(best && votes[i].count == best->count) morethanone++;
-            else morethanone = 0;
-            best = &votes[i];
-        }
-        if(force && morethanone)
+        bool passed = force;
+        if(style == 3) best = !votes.empty() ? &votes[rnd(votes.length())] : NULL;
+        else
         {
-            int r = rnd(morethanone+1), n = 0;
-            loopv(votes) if(votes[i].count == best->count)
+            int morethanone = 0;
+            loopv(votes) if(!best || votes[i].count >= best->count)
             {
-                if(n != r) n++;
-                else { best = &votes[i]; break; }
+                if(best && votes[i].count == best->count) morethanone++;
+                else morethanone = 0;
+                best = &votes[i];
+            }
+            if(force && morethanone)
+            {
+                int r = rnd(morethanone+1), n = 0;
+                loopv(votes) if(votes[i].count == best->count)
+                {
+                    if(n != r) n++;
+                    else { best = &votes[i]; break; }
+                }
+            }
+            if(!passed && best) switch(style)
+            {
+                case 2: passed = best->count >= maxvotes; break;
+                case 1: passed = best->count >= maxvotes*G(votethreshold); break;
+                case 0: default: break;
             }
-        }
-        bool passed = force;
-        if(!passed && best) switch(maprequest ? G(voteinterm) : G(votestyle))
-        {
-            case 2: passed = best->count >= maxvotes; break;
-            case 1: passed = best->count >= maxvotes*G(votethreshold); break;
-            case 0: default: break;
         }
         if(passed)
         {
@@ -2208,9 +2400,15 @@ namespace server
     void vote(const char *reqmap, int &reqmode, int &reqmuts, int sender)
     {
         clientinfo *ci = (clientinfo *)getinfo(sender);
+        reqmuts |= G(mutslockforce);
         modecheck(reqmode, reqmuts);
         if(!ci || !m_game(reqmode) || !reqmap || !*reqmap) return;
-        bool hasvote = false, hasveto = haspriv(ci, G(vetolock)) && (mastermode >= MM_VETO || !numclients(ci->clientnum));
+        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;
+        }
+        bool hasvote = false, hasveto = (mastermode >= MM_VETO && haspriv(ci, G(vetolock))) || !numclients(ci->clientnum);
         if(!hasveto)
         {
             if(ci->lastvote && totalmillis-ci->lastvote <= G(votewait)) return;
@@ -2219,7 +2417,7 @@ namespace server
         loopv(clients)
         {
             clientinfo *oi = clients[i];
-            if(oi->state.aitype > AI_NONE || !oi->mapvote[0] || ci == oi) continue;
+            if(oi->state.actortype > A_PLAYER || !oi->mapvote[0] || ci == oi) continue;
             if(!strcmp(oi->mapvote, reqmap) && oi->modevote == reqmode && oi->mutsvote == reqmuts)
             {
                 hasvote = true;
@@ -2228,8 +2426,7 @@ namespace server
         }
         if(!hasvote)
         {
-            if(G(modelock) == PRIV_MAX && G(mapslock) == PRIV_MAX && !haspriv(ci, PRIV_MAX, "vote for a new game")) return;
-            else if(G(votelock)) switch(G(votelocktype))
+            if(G(votelock)) switch(G(votelocktype))
             {
                 case 1: if(!haspriv(ci, G(votelock), "vote for a new game")) return; break;
                 case 2:
@@ -2241,11 +2438,6 @@ namespace server
                     break;
                 case 0: default: break;
             }
-            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;
-            }
             if(G(modelock)) switch(G(modelocktype))
             {
                 case 1: if(!haspriv(ci, G(modelock), "change game modes")) return; break;
@@ -2298,34 +2490,38 @@ namespace server
         checkvotes();
     }
 
+    bool scorecmp(clientinfo *ci, uint ip, const char *name, const char *handle, uint clientip)
+    {
+        if(ci->handle[0] && !strcmp(handle, ci->handle)) return true;
+        if(ip && clientip == ip && !strcmp(name, ci->name)) return true;
+        return false;
+    }
+
     savedscore *findscore(clientinfo *ci, bool insert)
     {
         uint ip = getclientip(ci->clientnum);
-        if(!ip) return 0;
-        if(!insert)
+        if(!ip && !ci->handle[0]) return NULL;
+        if(!insert) loopv(clients)
         {
-            loopv(clients)
+            clientinfo *oi = clients[i];
+            if(oi->clientnum != ci->clientnum && scorecmp(ci, ip, oi->name, oi->handle, getclientip(oi->clientnum)))
             {
-                clientinfo *oi = clients[i];
-                if(oi->clientnum != ci->clientnum && getclientip(oi->clientnum) == ip && !strcmp(oi->name, ci->name))
-                {
-                    oi->state.timeplayed += lastmillis-oi->state.lasttimeplayed;
-                    oi->state.lasttimeplayed = lastmillis;
-                    static savedscore curscore;
-                    curscore.save(oi->state);
-                    return &curscore;
-                }
+                oi->state.updatetimeplayed();
+                static savedscore curscore;
+                curscore.save(oi->state);
+                return &curscore;
             }
         }
         loopv(savedscores)
         {
             savedscore &sc = savedscores[i];
-            if(sc.ip == ip && !strcmp(sc.name, ci->name)) return ≻
+            if(scorecmp(ci, ip, sc.name, sc.handle, sc.ip)) return ≻
         }
-        if(!insert) return 0;
+        if(!insert) return NULL;
         savedscore &sc = savedscores.add();
-        sc.ip = ip;
         copystring(sc.name, ci->name);
+        copystring(sc.handle, ci->handle);
+        sc.ip = ip;
         return ≻
     }
 
@@ -2334,7 +2530,7 @@ namespace server
         ci->state.score += points;
         ci->state.points += points;
         sendf(-1, 1, "ri4", N_POINTS, ci->clientnum, points, ci->state.points);
-        if(team && m_team(gamemode, mutators) && m_teamscore(gamemode))
+        if(team && m_team(gamemode, mutators) && m_dm(gamemode))
         {
             score &ts = teamscore(ci->team);
             ts.total += points;
@@ -2344,22 +2540,69 @@ namespace server
 
     void savescore(clientinfo *ci)
     {
+        ci->state.updatetimeplayed(false);
         savedscore *sc = findscore(ci, true);
-        if(sc) sc->save(ci->state);
+        if(sc)
+        {
+            if(ci->state.actortype == A_PLAYER && m_dm(gamemode) && m_team(gamemode, mutators) && !m_nopoints(gamemode, mutators) && G(teamkillrestore) && canplay())
+            {
+                int restorepoints[T_MAX] = {0};
+                loopv(ci->state.teamkills) restorepoints[ci->state.teamkills[i].team] += ci->state.teamkills[i].points;
+                loopi(T_MAX) if(restorepoints[i] >= G(teamkillrestore))
+                {
+                    score &ts = teamscore(i);
+                    ts.total += restorepoints[i];
+                    sendf(-1, 1, "ri3", N_SCORE, ts.team, ts.total);
+                }
+            }
+            sc->save(ci->state);
+        }
     }
 
-    void setteam(clientinfo *ci, int team, int flags)
+    void swapteam(clientinfo *ci, int oldteam, int newteam = T_NEUTRAL, bool swaps = true)
     {
-        if(ci->team != team)
+        if(ci->swapteam && (!newteam || ci->swapteam == newteam)) ci->swapteam = T_NEUTRAL;
+        if(!swaps || ci->state.actortype != A_PLAYER || !oldteam || oldteam == newteam || !m_swapteam(gamemode, mutators)) return;
+        loopv(clients) if(clients[i] && clients[i] != ci)
         {
-            bool reenter = false;
-            if(m_checkpoint(gamemode))
+            clientinfo *cp = clients[i];
+            if(cp->state.actortype != A_PLAYER || (newteam && cp->team != newteam) || !cp->swapteam || cp->swapteam != oldteam) continue;
+            setteam(cp, oldteam, TT_RESET|TT_INFOSM, false);
+            cp->state.lastdeath = 0;
+            ancmsgft(cp->clientnum, S_V_BALALERT, CON_EVENT, "\fyyou have been moved to %s as previously requested", colourteam(oldteam));
+            return;
+        }
+        if(haspriv(ci, G(teambalancelock)))
+        {
+            int worst = -1;
+            float csk = ci->state.scoretime(), wsk = 0;
+            loopv(clients) if(clients[i] && clients[i] != ci)
+            {
+                clientinfo *cp = clients[i];
+                if(cp->state.actortype != A_PLAYER || (newteam && cp->team != newteam)) continue;
+                float psk = cp->state.scoretime();
+                if(psk > csk || worst < 0 || psk > wsk) continue;
+                worst = i;
+                wsk = psk;
+            }
+            if(worst >= 0)
             {
-                ci->state.cpmillis = 0;
-                ci->state.cpnodes.shrink(0);
-                sendf(-1, 1, "ri3", N_CHECKPOINT, ci->clientnum, -1);
+                clientinfo *cp = clients[worst];
+                setteam(cp, oldteam, TT_RESET|TT_INFOSM, false);
+                cp->state.lastdeath = 0;
+                ancmsgft(cp->clientnum, S_V_BALALERT, CON_EVENT, "\fyyou have been moved to %s by higher skilled player %s", colourteam(oldteam), colourname(ci));
+                return;
             }
-            if(flags&TT_RESET) waiting(ci, DROP_WEAPONS);
+        }
+    }
+
+    void setteam(clientinfo *ci, int team, int flags, bool swaps)
+    {
+        swapteam(ci, ci->team, team, swaps);
+        if(ci->team != team)
+        {
+            bool reenter = false;
+            if(flags&TT_RESET) waiting(ci, DROP_WEAPONS, false);
             else if(flags&TT_SMODE && ci->state.state == CS_ALIVE)
             {
                 if(smode) smode->leavegame(ci);
@@ -2373,7 +2616,7 @@ namespace server
                 if(smode) smode->entergame(ci);
                 mutate(smuts, mut->entergame(ci));
             }
-            if(ci->state.aitype == AI_NONE) aiman::dorefresh = max(aiman::dorefresh, G(airefreshdelay)); // get the ai to reorganise
+            if(ci->state.actortype == A_PLAYER && ci->ready) aiman::poke();
         }
         if(flags&TT_INFO) sendf(-1, 1, "ri3", N_SETTEAM, ci->clientnum, ci->team);
     }
@@ -2392,24 +2635,28 @@ namespace server
         ~teamcheck() {}
     };
 
-    bool allowteam(clientinfo *ci, int team, int first = T_FIRST)
+    bool allowteam(clientinfo *ci, int team, int first = T_FIRST, bool check = true)
     {
         if(isteam(gamemode, mutators, team, first))
         {
-            if(!m_coop(gamemode, mutators)) return true;
-            else if(ci->state.aitype > AI_NONE) return team != mapbals[curbalance][0];
+            if(!m_coop(gamemode, mutators))
+            {
+                if(check && m_balteam(gamemode, mutators, 3) && team != chooseteam(ci, team)) return false;
+                return true;
+            }
+            else if(ci->state.actortype >= A_BOT) return team != mapbals[curbalance][0];
             else return team == mapbals[curbalance][0];
         }
         return false;
     }
 
-    int chooseteam(clientinfo *ci, int suggest = -1)
+    int chooseteam(clientinfo *ci, int suggest, bool wantbal)
     {
-        if(ci->state.aitype >= AI_START) return T_ENEMY;
+        if(ci->state.actortype >= A_ENEMY) return T_ENEMY;
         else if(m_fight(gamemode) && m_team(gamemode, mutators) && ci->state.state != CS_SPECTATOR && ci->state.state != CS_EDITING)
         {
-            bool human = ci->state.aitype == AI_NONE;
-            int team = -1, bal = human ? G(teambalance) : 1;
+            bool human = ci->state.actortype == A_PLAYER;
+            int team = -1, bal = human && !wantbal && (G(teambalance) != 6 || !gs_playing(gamestate)) ? G(teambalance) : 1;
             if(human)
             {
                 if(m_coop(gamemode, mutators)) return mapbals[curbalance][0];
@@ -2418,54 +2665,59 @@ namespace server
                     { suggest, ci->team, ci->lastteam },
                     { suggest, ci->lastteam, ci->team }
                 };
-                loopi(3) if(allowteam(ci, teams[G(teampersist)][i], T_FIRST))
+                loopi(3) if(allowteam(ci, teams[G(teampersist)][i], T_FIRST, false))
                 {
                     team = teams[G(teampersist)][i];
-                    if(G(teampersist) == 2) return team;
+                    if(bal <= 2 && G(teampersist) == 2) return team;
                     break;
                 }
             }
-            if(bal || team < 0)
+            teamcheck teamchecks[T_TOTAL];
+            loopk(T_TOTAL) teamchecks[k].team = T_FIRST+k;
+            loopv(clients) if(clients[i] != ci)
             {
-                teamcheck teamchecks[T_TOTAL];
-                loopk(T_TOTAL) teamchecks[k].team = T_FIRST+k;
-                loopv(clients)
-                {
-                    clientinfo *cp = clients[i];
-                    if(!cp->team || cp == ci || cp->state.state == CS_SPECTATOR) continue;
-                    if((cp->state.aitype > AI_NONE && cp->state.ownernum < 0) || cp->state.aitype >= AI_START) continue;
-                    if(ci->state.aitype > AI_NONE || (ci->state.aitype == AI_NONE && cp->state.aitype == AI_NONE))
-                    { // remember: ai just balance teams
-                        cp->state.timeplayed += lastmillis-cp->state.lasttimeplayed;
-                        cp->state.lasttimeplayed = lastmillis;
-                        teamcheck &ts = teamchecks[cp->team-T_FIRST];
-                        ts.score += cp->state.score/float(max(cp->state.timeplayed, 1));
-                        ts.clients++;
-                    }
+                clientinfo *cp = clients[i];
+                if(!cp->team || cp->state.state == CS_SPECTATOR) continue;
+                if((cp->state.actortype > A_PLAYER && cp->state.ownernum < 0) || cp->state.actortype >= A_ENEMY) continue;
+                teamcheck &ts = teamchecks[cp->team-T_FIRST];
+                if(team > 0 && m_swapteam(gamemode, mutators) && ci->state.actortype == A_PLAYER && cp->state.actortype == A_PLAYER && cp->swapteam && ci->team == cp->swapteam && cp->team == team)
+                    return team; // swapteam
+                if(ci->state.actortype > A_PLAYER || (ci->state.actortype == A_PLAYER && cp->state.actortype == A_PLAYER))
+                { // remember: ai just balance teams
+                    ts.score += cp->state.scoretime();
+                    ts.clients++;
                 }
+            }
+            if(bal || team <= 0) loopj(team > 0 ? 2 : 1)
+            {
                 teamcheck *worst = NULL;
-                loopi(numteams(gamemode, mutators)) if(allowteam(ci, teamchecks[i].team, T_FIRST))
+                loopi(numteams(gamemode, mutators)) if(allowteam(ci, teamchecks[i].team, T_FIRST, false))
                 {
                     teamcheck &ts = teamchecks[i];
                     switch(bal)
                     {
-                        case 2:
+                        case 2: case 5: case 6:
                         {
-                            if(!worst || ts.score < worst->score || (ts.score == worst->score && ts.clients < worst->clients))
+                            if(!worst || (team > 0 && ts.team == team && ts.score <= worst->score) || ts.score < worst->score || ((team <= 0 || worst->team != team) && ts.score == worst->score && ts.clients < worst->clients))
                                 worst = &ts;
                             break;
                         }
-                        case 1: default:
+                        case 1: case 3: case 4: default:
                         {
-                            if(!worst || ts.clients < worst->clients || (ts.clients == worst->clients && ts.score < worst->score))
+                            if(!worst || (team > 0 && ts.team == team && ts.clients <= worst->clients) || ts.clients < worst->clients || ((team <= 0 || worst->team != team) && ts.clients == worst->clients && ts.score < worst->score))
                                 worst = &ts;
                             break;
                         }
                     }
                 }
-                team = worst ? worst->team : T_ALPHA;
+                if(worst)
+                {
+                    team = worst->team;
+                    break;
+                }
+                team = -1;
             }
-            return team;
+            return allowteam(ci, team, T_FIRST, false) ? team : T_ALPHA;
         }
         return T_NEUTRAL;
     }
@@ -2473,7 +2725,7 @@ namespace server
     void stopdemo()
     {
         if(m_demo(gamemode)) enddemoplayback();
-        else checkdemorecord(interm != 0);
+        else checkdemorecord(!gs_playing(gamestate));
     }
 
     void connected(clientinfo *ci);
@@ -2482,34 +2734,34 @@ namespace server
 
     void spectator(clientinfo *ci, bool quarantine = false, int sender = -1)
     {
-        if(!ci || ci->state.aitype > AI_NONE) return;
+        if(!ci || ci->state.actortype > A_PLAYER) return;
         ci->state.state = CS_SPECTATOR;
         ci->state.quarantine = quarantine;
         sendf(sender, 1, "ri3", N_SPECTATOR, ci->clientnum, quarantine ? 2 : 1);
-        setteam(ci, T_NEUTRAL, TT_SMINFO);
+        setteam(ci, T_NEUTRAL, TT_INFOSM);
     }
 
-    enum { ALST_FIRST = 0, ALST_TRY, ALST_SPAWN, ALST_SPEC, ALST_EDIT, ALST_WALK, ALST_MAX };
+    enum { ALST_TRY = 0, ALST_SPAWN, ALST_SPEC, ALST_EDIT, ALST_WALK, ALST_MAX };
 
     //bool crclocked(clientinfo *ci)
     //{
-    //    if(m_play(gamemode) && G(mapcrclock) && ci->state.aitype == AI_NONE && (!ci->clientmap[0] || ci->mapcrc <= 0 || ci->warned) && !haspriv(ci, G(mapcrclock)))
+    //    if(m_play(gamemode) && G(mapcrclock) && ci->state.actortype == A_PLAYER && (!ci->clientmap[0] || ci->mapcrc <= 0 || ci->warned) && !haspriv(ci, G(mapcrclock)))
     //        return true;
     //    return false;
     //}
 
-    bool allowstate(clientinfo *ci, int n)
+    bool allowstate(clientinfo *ci, int n, int lock = -1)
     {
         if(!ci) return false;
+        uint ip = getclientip(ci->clientnum);
         switch(n)
         {
-            case ALST_FIRST: if(ci->state.state == CS_SPECTATOR || gamemode >= G_EDITMODE) return false; // first spawn, falls through
             case ALST_TRY: // try spawn
             {
                 if(ci->state.quarantine) return false;
-                uint ip = getclientip(ci->clientnum);
-                if(ci->state.aitype == AI_NONE && mastermode >= MM_LOCKED && ci->state.state == CS_SPECTATOR && ip && !checkipinfo(control, ipinfo::ALLOW, ip))
-                    return false;
+                if(ci->state.actortype == A_PLAYER)
+                    if(mastermode >= MM_LOCKED && ip && !checkipinfo(control, ipinfo::ALLOW, ip) && !haspriv(ci, lock, "spawn"))
+                        return false;
                 if(ci->state.state == CS_ALIVE || ci->state.state == CS_WAITING) return false;
                 if(ci->state.lastdeath && gamemillis-ci->state.lastdeath <= DEATHMILLIS) return false;
                 //if(crclocked(ci)) return false;
@@ -2523,12 +2775,12 @@ namespace server
                 //if(crclocked(ci)) return false;
                 break;
             }
-            case ALST_SPEC: return ci->state.aitype == AI_NONE; // spec
-            case ALST_WALK: if(ci->state.quarantine || ci->state.state != CS_EDITING) return false;
+            case ALST_SPEC: return ci->state.actortype == A_PLAYER; // spec
+            case ALST_WALK: if(ci->state.state != CS_EDITING) return false;
             case ALST_EDIT: // edit on/off
             {
-                uint ip = getclientip(ci->clientnum);
-                if(ci->state.quarantine || ci->state.aitype != AI_NONE || !m_edit(gamemode) || (mastermode >= MM_LOCKED && ci->state.state == CS_SPECTATOR && ip && !checkipinfo(control, ipinfo::ALLOW, ip))) return false;
+                if(ci->state.quarantine || ci->state.actortype != A_PLAYER || !m_edit(gamemode)) return false;
+                if(mastermode >= MM_LOCKED && ip && !checkipinfo(control, ipinfo::ALLOW, ip) && !haspriv(ci, lock, "edit")) return false;
                 break;
             }
             default: break;
@@ -2542,28 +2794,33 @@ namespace server
     #include "duelmut.h"
     #include "aiman.h"
 
-    bool firstblood = false;
+    bool needswait()
+    {
+        return m_fight(gamemode) && G(waitforplayers) && numclients() > 1;
+    }
+
     void changemap(const char *name, int mode, int muts)
     {
-        hasgameinfo = maprequest = mapsending = shouldcheckvotes = firstblood = false;
+        hasgameinfo = mapsending = shouldcheckvotes = firstblood = false;
         stopdemo();
         changemode(gamemode = mode, mutators = muts);
-        curbalance = nextbalance = gamemillis = interm = 0;
-        oldtimelimit = G(timelimit);
-        timeremaining = G(timelimit) ? G(timelimit)*60 : -1;
-        gamelimit = G(timelimit) ? timeremaining*1000 : 0;
-        inovertime = false;
+        curbalance = nextbalance = lastteambalance = nextteambalance = gamemillis = 0;
+        gamestate = m_fight(gamemode) ? G_S_WAITING : G_S_PLAYING;
+        oldtimelimit = m_fight(gamemode) && G(timelimit) ? G(timelimit) : -1;
+        timeremaining = m_fight(gamemode) && G(timelimit) ? G(timelimit)*60 : -1;
+        gamelimit = m_fight(gamemode) && G(timelimit) ? timeremaining*1000 : 0;
+        gamewait = m_fight(gamemode) ? totalmillis+G(waitforplayertime) : 0;
         sents.shrink(0);
         scores.shrink(0);
         loopv(savedscores) savedscores[i].mapchange();
         setuptriggers(false);
         setupspawns(false);
-        if(smode) smode->reset(false);
-        mutate(smuts, mut->reset(false));
+        if(smode) smode->reset();
+        mutate(smuts, mut->reset());
         aiman::clearai();
-        aiman::dorefresh = 0;
-
-        const char *reqmap = name && *name ? name : pickmap(smapname, gamemode, mutators);
+        aiman::poke();
+        mapcrc = 0;
+        const char *reqmap = name && *name ? name : pickmap(NULL, gamemode, mutators);
 #ifdef STANDALONE // interferes with savemap on clients, in which case we can just use the auto-request
         loopi(SENDMAP_MAX)
         {
@@ -2576,6 +2833,7 @@ namespace server
                 break;
             }
         }
+        if(mapdata[0]) mapcrc = mapdata[0]->getcrc();
 #else
         loopi(SENDMAP_MAX) if(mapdata[i]) DELETEP(mapdata[i]);
 #endif
@@ -2585,34 +2843,27 @@ namespace server
         if(m_capture(gamemode)) smode = &capturemode;
         else if(m_defend(gamemode)) smode = &defendmode;
         else if(m_bomber(gamemode)) smode = &bombermode;
-        else if(m_gauntlet(gamemode)) smode = &gauntletmode;
         else smode = NULL;
         smuts.shrink(0);
         smuts.add(&spawnmutator);
         if(m_duke(gamemode, mutators)) smuts.add(&duelmutator);
         if(m_vampire(gamemode, mutators)) smuts.add(&vampiremutator);
-        if(smode) smode->reset(false);
-        mutate(smuts, mut->reset(false));
+        if(smode) smode->reset();
+        mutate(smuts, mut->reset());
 
         if(m_local(gamemode)) kicknonlocalclients(DISC_PRIVATE);
 
-        loopv(clients) clients[i]->mapchange(true);
         loopv(clients)
         {
-            clientinfo *ci = clients[i];
-            if(allowstate(ci, ALST_FIRST))
-            {
-                ci->state.state = CS_DEAD;
-                waiting(ci, DROP_RESET);
-            }
-            else spectator(ci);
+            clients[i]->mapchange(true);
+            spectator(clients[i]);
         }
 
-        if(m_fight(gamemode) && G(maphistory))
+        if(!demoplayback && m_fight(gamemode) && numclients())
         {
             vector<char> buf;
             buf.put(smapname, strlen(smapname));
-            if(*sv_previousmaps && numclients())
+            if(*sv_previousmaps && G(maphistory))
             {
                 vector<char *> prev;
                 explodelist(sv_previousmaps, prev);
@@ -2638,10 +2889,11 @@ namespace server
             const char *str = buf.getbuf();
             if(*str) setmods(sv_previousmaps, str);
         }
+        else setmods(sv_previousmaps, "");
 
         if(numclients())
         {
-            if(m_fight(gamemode)) sendf(-1, 1, "ri2", N_TICK, timeremaining);
+            sendf(-1, 1, "ri3", N_TICK, gamestate, timeleft());
             if(m_demo(gamemode)) setupdemoplayback();
             else if(demonextmatch) setupdemorecord();
         }
@@ -2668,7 +2920,7 @@ namespace server
         loopv(clients)
         {
             clientinfo *ci = clients[i];
-            if(ci->state.aitype > AI_NONE) continue;
+            if(ci->state.actortype > A_PLAYER) continue;
             total++;
             if(!ci->clientmap[0])
             {
@@ -2688,8 +2940,8 @@ namespace server
         loopv(clients)
         {
             clientinfo *ci = clients[i];
-            if(ci->state.aitype > AI_NONE || ci->clientmap[0] || ci->mapcrc >= 0 || (req < 0 && ci->warned)) continue;
-            srvmsgf(req, "\fy\fs%s\fS has modified map \"%s\"", colourname(ci), smapname);
+            if(ci->state.actortype > A_PLAYER || ci->clientmap[0] || ci->mapcrc >= 0 || (req < 0 && ci->warned)) continue;
+            srvmsgf(req, "\fy%s is using a modified map (\fs\fc0x%X\fS)", colourname(ci), ci->mapcrc);
             if(req < 0) ci->warned = true;
         }
         if(crcs.empty() || crcs.length() < 2) return;
@@ -2699,8 +2951,8 @@ namespace server
             if(i || info.matches <= crcs[i+1].matches) loopvj(clients)
             {
                 clientinfo *ci = clients[j];
-                if(ci->state.aitype > AI_NONE || !ci->clientmap[0] || ci->mapcrc != info.crc || (req < 0 && ci->warned)) continue;
-                srvmsgf(req, "\fy\fs%s\fS has modified map \"%s\"", colourname(ci), smapname);
+                if(ci->state.actortype > A_PLAYER || !ci->clientmap[0] || ci->mapcrc != info.crc || (req < 0 && ci->warned)) continue;
+                srvmsgf(req, "\fy%s is using a modified map (\fs\fc0x%X\fS)", colourname(ci), ci->mapcrc);
                 if(req < 0) ci->warned = true;
             }
         }
@@ -2708,26 +2960,26 @@ namespace server
 
     void checkvar(ident *id, const char *arg)
     {
-        if(id && id->flags&IDF_SERVER && !(id->flags&IDF_ADMIN) && !(id->flags&IDF_WORLD)) switch(id->type)
+        if(id && id->flags&IDF_SERVER && !(id->flags&IDF_READONLY) && !(id->flags&IDF_ADMIN) && !(id->flags&IDF_WORLD)) switch(id->type)
         {
             case ID_VAR:
             {
                 int ret = parseint(arg);
-                if(*id->storage.i == id->def.i) { if(ret != id->def.i) numgamemods++; }
-                else if(ret == id->def.i) numgamemods--;
+                if(*id->storage.i == id->bin.i) { if(ret != id->bin.i) numgamemods++; }
+                else if(ret == id->bin.i) numgamemods--;
                 break;
             }
             case ID_FVAR:
             {
                 int ret = parsefloat(arg);
-                if(*id->storage.f == id->def.f) { if(ret != id->def.f) numgamemods++; }
-                else if(ret == id->def.f) numgamemods--;
+                if(*id->storage.f == id->bin.f) { if(ret != id->bin.f) numgamemods++; }
+                else if(ret == id->bin.f) numgamemods--;
                 break;
             }
             case ID_SVAR:
             {
-                if(!strcmp(*id->storage.s, id->def.s)) { if(strcmp(arg, id->def.s)) numgamemods++; }
-                else if(!strcmp(arg, id->def.s)) numgamemods--;
+                if(!strcmp(*id->storage.s, id->bin.s)) { if(strcmp(arg, id->bin.s)) numgamemods++; }
+                else if(!strcmp(arg, id->bin.s)) numgamemods--;
                 break;
             }
             default: break;
@@ -2735,7 +2987,7 @@ namespace server
     }
 
     bool servcmd(int nargs, const char *cmd, const char *arg)
-    { // incoming command from scripts
+    { // incoming commands
 #ifndef STANDALONE
         if(::connected(false, false)) return false;
 #endif
@@ -2782,9 +3034,13 @@ namespace server
                                     "\frvalid range for %s is %d..%d", id->name, id->minval, id->maxval);
                         return true;
                     }
+                    if(versioning)
+                    {
+                        id->def.i = ret;
+                        if(versioning == 2) id->bin.i = ret;
+                    }
                     checkvar(id, arg);
                     *id->storage.i = ret;
-                    if(versioning) id->def.i = ret;
                     id->changed();
 #ifndef STANDALONE
                     if(versioning) setvar(&id->name[3], ret, true);
@@ -2810,9 +3066,13 @@ namespace server
                         conoutft(CON_MESG, "\frvalid range for %s is %s..%s", id->name, floatstr(id->minvalf), floatstr(id->maxvalf));
                         return true;
                     }
+                    if(versioning)
+                    {
+                        id->def.f = ret;
+                        if(versioning == 2) id->bin.f = ret;
+                    }
                     checkvar(id, arg);
                     *id->storage.f = ret;
-                    if(versioning) id->def.f = ret;
                     id->changed();
 #ifndef STANDALONE
                     if(versioning) setfvar(&id->name[3], ret, true);
@@ -2832,14 +3092,19 @@ namespace server
                         conoutft(CON_MESG, "\frcannot override variable: %s", id->name);
                         return true;
                     }
-                    checkvar(id, arg);
-                    delete[] *id->storage.s;
-                    *id->storage.s = newstring(arg);
                     if(versioning)
                     {
                         delete[] id->def.s;
                         id->def.s = newstring(arg);
+                        if(versioning == 2)
+                        {
+                            delete[] id->bin.s;
+                            id->bin.s = newstring(arg);
+                        }
                     }
+                    checkvar(id, arg);
+                    delete[] *id->storage.s;
+                    *id->storage.s = newstring(arg);
                     id->changed();
 #ifndef STANDALONE
                     if(versioning) setsvar(&id->name[3], arg, true);
@@ -2865,16 +3130,19 @@ namespace server
         ident *id = idents.access(cmdname);
         if(id && id->flags&IDF_SERVER)
         {
-            const char *name = &id->name[3], *val = NULL;
+            const char *name = &id->name[3], *val = NULL, *oldval = NULL;
+            bool needfreeoldval = false;
             int locked = max(id->flags&IDF_ADMIN ? PRIV_ADMINISTRATOR : 0, G(varslock));
             #ifndef STANDALONE
-            if(servertype < 3)
-            {
-                if(!strcmp(id->name, "sv_gamespeed")) locked = PRIV_MAX;
-                if(!strcmp(id->name, "sv_gamepaused")) locked = PRIV_MAX;
-            }
+            if(servertype < 3 && (!strcmp(id->name, "sv_gamespeed") || !strcmp(id->name, "sv_gamepaused"))) locked = PRIV_ADMINISTRATOR;
             #endif
             if(!strcmp(id->name, "sv_gamespeed") && G(gamespeedlock) > locked) locked = G(gamespeedlock);
+            else if(id->type == ID_VAR)
+            {
+                int len = strlen(id->name);
+                if(len > 4 && !strcmp(&id->name[len-4], "lock"))
+                    locked = max(max(*id->storage.i, parseint(arg)), locked);
+            }
             switch(id->type)
             {
                 case ID_COMMAND:
@@ -2887,8 +3155,8 @@ namespace server
                     else formatstring(s)(slen, "%s %s", id->name, arg);
                     char *ret = executestr(s);
                     delete[] s;
-                    if(ret && *ret) srvoutf(-3, "\fy%s executed %s (returned: %s)", colourname(ci), name, ret);
-                    else srvoutf(-3, "\fy%s executed %s", colourname(ci), name);
+                    if(ret && *ret) srvoutf(-3, "\fy%s executed \fs\fc%s\fS (returned: \fs\fc%s\fS)", colourname(ci), name, ret);
+                    else srvoutf(-3, "\fy%s executed \fs\fc%s\fS", colourname(ci), name);
                     delete[] ret;
                     return;
                 }
@@ -2920,6 +3188,7 @@ namespace server
                         return;
                     }
                     checkvar(id, arg);
+                    oldval = intstr(id);
                     *id->storage.i = ret;
                     id->changed();
                     val = intstr(id);
@@ -2950,6 +3219,7 @@ namespace server
                         return;
                     }
                     checkvar(id, arg);
+                    oldval = floatstr(*id->storage.f);
                     *id->storage.f = ret;
                     id->changed();
                     val = floatstr(*id->storage.f);
@@ -2974,6 +3244,8 @@ namespace server
                         return;
                     }
                     checkvar(id, arg);
+                    oldval = newstring(*id->storage.s);
+                    needfreeoldval = true;
                     delete[] *id->storage.s;
                     *id->storage.s = newstring(arg);
                     id->changed();
@@ -2985,7 +3257,12 @@ namespace server
             if(val)
             {
                 sendf(-1, 1, "ri2sis", N_COMMAND, ci->clientnum, name, strlen(val), val);
-                relayf(3, "\fy%s set %s to %s", colourname(ci), name, val);
+                if(oldval)
+                {
+                    relayf(3, "\fy%s set %s to %s (was: %s)", colourname(ci), name, val, oldval);
+                    if(needfreeoldval) delete[] oldval;
+                }
+                else relayf(3, "\fy%s set %s to %s", colourname(ci), name, val);
             }
         }
         else srvmsgf(ci->clientnum, "\frunknown command: %s", cmd);
@@ -2996,7 +3273,7 @@ namespace server
         bool found = false;
         const char *argstr = numargs > 2 ? conc(&args[1], numargs-1, true) : (numargs > 1 ? args[1].getstr() : "");
         if(id && id->flags&IDF_WORLD && identflags&IDF_WORLD) found = true;
-        else if(id && id->flags&IDF_SERVER && id->type!=ID_COMMAND) found = servcmd(numargs, args[0].s, argstr);
+        else if(id && id->flags&IDF_SERVER && id->type != ID_COMMAND) found = servcmd(numargs, args[0].s, argstr);
 #ifndef STANDALONE
         else if(!id || id->flags&IDF_CLIENT) found = client::sendcmd(numargs, args[0].s, argstr);
 #endif
@@ -3010,7 +3287,8 @@ namespace server
         loopv(clients)
         {
             clientinfo *cs = clients[i];
-            if(cs->state.aitype > AI_NONE || !cs->name[0] || !cs->online || cs->wantsmap) continue;
+            if(cs->state.actortype > A_PLAYER || !cs->name[0] || !cs->online || cs->wantsmap || cs->warned) continue;
+            cs->state.updatetimeplayed();
             if(!best || cs->state.timeplayed > best->state.timeplayed) best = cs;
         }
         return best;
@@ -3018,7 +3296,7 @@ namespace server
 
     void sendservinit(clientinfo *ci)
     {
-        sendf(ci->clientnum, 1, "ri3si2", N_SERVERINIT, ci->clientnum, GAMEVERSION, gethostname(ci->clientnum), ci->sessionid, serverpass[0] ? 1 : 0);
+        sendf(ci->clientnum, 1, "ri3ssi", N_SERVERINIT, ci->clientnum, VERSION_GAME, gethostname(ci->clientnum), gethostip(ci->clientnum), ci->sessionid);
     }
 
     bool restorescore(clientinfo *ci)
@@ -3027,6 +3305,17 @@ namespace server
         if(sc)
         {
             sc->restore(ci->state);
+            if(ci->state.actortype == A_PLAYER && m_dm(gamemode) && m_team(gamemode, mutators) && !m_nopoints(gamemode, mutators) && G(teamkillrestore) && canplay())
+            {
+                int restorepoints[T_MAX] = {0};
+                loopv(ci->state.teamkills) restorepoints[ci->state.teamkills[i].team] += ci->state.teamkills[i].points;
+                loopi(T_MAX) if(restorepoints[i] >= G(teamkillrestore))
+                {
+                    score &ts = teamscore(i);
+                    ts.total -= restorepoints[i];
+                    sendf(-1, 1, "ri3", N_SCORE, ts.team, ts.total);
+                }
+            }
             return true;
         }
         return false;
@@ -3035,26 +3324,28 @@ namespace server
     void sendresume(clientinfo *ci)
     {
         servstate &gs = ci->state;
-        sendf(-1, 1, "ri9i2vvi", N_RESUME, ci->clientnum, gs.state, gs.points, gs.frags, gs.deaths, gs.health, gs.armour, gs.cptime, gs.cplaps, gs.weapselect, W_MAX, &gs.ammo[0], W_MAX, &gs.reloads[0], -1);
+        sendf(-1, 1, "ri9vi", N_RESUME, ci->clientnum, gs.state, gs.points, gs.frags, gs.deaths, gs.health, gs.cptime, gs.weapselect, W_MAX, &gs.ammo[0], -1);
     }
 
     void putinitclient(clientinfo *ci, packetbuf &p)
     {
-        if(ci->state.aitype > AI_NONE)
+        if(ci->state.actortype > A_PLAYER)
         {
             if(ci->state.ownernum >= 0)
             {
                 putint(p, N_INITAI);
                 putint(p, ci->clientnum);
                 putint(p, ci->state.ownernum);
-                putint(p, ci->state.aitype);
-                putint(p, ci->state.aientity);
+                putint(p, ci->state.actortype);
+                putint(p, ci->state.spawnpoint);
                 putint(p, ci->state.skill);
                 sendstring(ci->name, p);
                 putint(p, ci->team);
                 putint(p, ci->state.colour);
                 putint(p, ci->state.model);
                 sendstring(ci->state.vanity, p);
+                putint(p, ci->state.loadweap.length());
+                loopv(ci->state.loadweap) putint(p, ci->state.loadweap[i]);
             }
         }
         else
@@ -3066,9 +3357,13 @@ namespace server
             putint(p, ci->team);
             putint(p, ci->privilege);
             sendstring(ci->name, p);
-            sendstring(gethostname(ci->clientnum), p);
-            sendstring(ci->handle, p);
             sendstring(ci->state.vanity, p);
+            putint(p, ci->state.loadweap.length());
+            loopv(ci->state.loadweap) putint(p, ci->state.loadweap[i]);
+            sendstring(ci->handle, p);
+            sendstring(gethostname(ci->clientnum), p);
+            sendstring(gethostip(ci->clientnum), p);
+            ci->state.version.put(p);
         }
     }
 
@@ -3079,13 +3374,19 @@ namespace server
         sendpacket(-1, 1, p.finalize(), ci->clientnum);
     }
 
+    void sendinitclientself(clientinfo *ci)
+    {
+        packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+        putinitclient(ci, p);
+        sendpacket(ci->clientnum, 1, p.finalize(), ci->clientnum);
+    }
+
     void welcomeinitclient(packetbuf &p, int exclude = -1)
     {
         loopv(clients)
         {
             clientinfo *ci = clients[i];
             if(!ci->connected || ci->clientnum == exclude) continue;
-
             putinitclient(ci, p);
         }
     }
@@ -3106,6 +3407,7 @@ namespace server
         else if(!ci->online && m_edit(gamemode) && numclients(ci->clientnum))
         {
             loopi(SENDMAP_MAX) if(mapdata[i]) DELETEP(mapdata[i]);
+            mapcrc = 0;
             ci->wantsmap = true;
             if(!mapsending)
             {
@@ -3174,7 +3476,8 @@ namespace server
         if(!ci || (m_fight(gamemode) && numclients()))
         {
             putint(p, N_TICK);
-            putint(p, timeremaining);
+            putint(p, gamestate);
+            putint(p, timeleft());
         }
 
         if(hasgameinfo)
@@ -3216,7 +3519,7 @@ namespace server
             loopv(clients)
             {
                 clientinfo *oi = clients[i];
-                if(oi->state.aitype > AI_NONE || (ci && oi->clientnum == ci->clientnum)) continue;
+                if(oi->state.actortype > A_PLAYER || (ci && oi->clientnum == ci->clientnum)) continue;
                 if(oi->mapvote[0])
                 {
                     putint(p, N_MAPVOTE);
@@ -3252,45 +3555,40 @@ namespace server
 
     void clearevent(clientinfo *ci) { delete ci->events.remove(0); }
 
-    void addhistory(clientinfo *target, clientinfo *actor, int millis)
+    void addhistory(clientinfo *m, clientinfo *v, int millis)
     {
         bool found = false;
-        loopv(target->state.damagelog) if (target->state.damagelog[i].clientnum == actor->clientnum)
+        loopv(m->state.damagelog) if (m->state.damagelog[i].clientnum == v->clientnum)
         {
-            target->state.damagelog[i].millis = millis;
+            m->state.damagelog[i].millis = millis;
             found = true;
             break;
         }
-        if(!found) target->state.damagelog.add(dmghist(actor->clientnum, millis));
+        if(!found) m->state.damagelog.add(dmghist(v->clientnum, millis));
     }
 
-    void gethistory(clientinfo *target, clientinfo *actor, int millis, vector<int> &log, bool clear = false, int points = 0)
+    void gethistory(clientinfo *m, clientinfo *v, int millis, vector<int> &log, bool clear = false, int points = 0)
     {
-        loopv(target->state.damagelog) if(target->state.damagelog[i].clientnum != actor->clientnum && millis-target->state.damagelog[i].millis <= G(assistkilldelay))
+        loopv(m->state.damagelog) if(m->state.damagelog[i].clientnum != v->clientnum && millis-m->state.damagelog[i].millis <= G(assistkilldelay))
         {
-            clientinfo *assist = (clientinfo *)getinfo(target->state.damagelog[i].clientnum);
+            clientinfo *assist = (clientinfo *)getinfo(m->state.damagelog[i].clientnum);
             if(assist)
             {
                 log.add(assist->clientnum);
                 if(points && !m_nopoints(gamemode, mutators)) givepoints(assist, points);
             }
         }
-        if(clear) target->state.damagelog.shrink(0);
+        if(clear) m->state.damagelog.shrink(0);
     }
 
     bool isghost(clientinfo *d, clientinfo *e)
     {
-        if(d != e)
+        if(d != e && d->state.actortype < A_ENEMY && (!e || e->state.actortype < A_ENEMY))
         {
-            switch(m_ghost(gamemode))
-            {
-                case 2: if(!e || e->team == d->team) return true; break;
-                case 1: return true; break;
-                case 0: default: break;
-            }
-            if(m_team(gamemode, mutators)) switch(G(teamdamage))
+            if(m_ghost(gamemode, mutators)) return true;
+            if(m_team(gamemode, mutators)) switch(G(damageteam))
             {
-                case 1: if(d->state.aitype > AI_NONE || (e && e->state.aitype == AI_NONE)) break;
+                case 1: if(d->state.actortype > A_PLAYER || (e && e->state.actortype == A_PLAYER)) break;
                 case 0: if(e && d->team == e->team) return true; break;
                 case 2: default: break;
             }
@@ -3298,19 +3596,31 @@ namespace server
         return false;
     }
 
-    void dodamage(clientinfo *target, clientinfo *actor, int damage, int weap, int flags, int material, const ivec &hitpush = ivec(0, 0, 0))
+    void dodamage(clientinfo *m, clientinfo *v, int damage, int weap, int flags, int material, const ivec &hitpush = ivec(0, 0, 0), const ivec &hitvel = ivec(0, 0, 0), float dist = 0)
     {
         int realdamage = damage, realflags = flags, nodamage = 0, hurt = 0;
         realflags &= ~HIT_SFLAGS;
         if(realflags&HIT_MATERIAL && (material&MATF_VOLUME) == MAT_LAVA) realflags |= HIT_BURN;
 
-        if(smode && !smode->damage(target, actor, realdamage, weap, realflags, material, hitpush)) { nodamage++; }
-        mutate(smuts, if(!mut->damage(target, actor, realdamage, weap, realflags, material, hitpush)) { nodamage++; });
-        if(actor->state.aitype < AI_START)
+        if(smode && !smode->damage(m, v, realdamage, weap, realflags, material, hitpush, hitvel, dist)) { nodamage++; }
+        mutate(smuts, if(!mut->damage(m, v, realdamage, weap, realflags, material, hitpush, hitvel, dist)) { nodamage++; });
+        if(v->state.actortype < A_ENEMY)
         {
-            if(actor == target && !G(selfdamage)) nodamage++;
-            else if(isghost(target, actor)) nodamage++;
-            if(m_expert(gamemode, mutators) && !hithead(flags)) nodamage++;
+            if(v == m && !G(damageself)) nodamage++;
+            else if(isghost(m, v)) nodamage++;
+        }
+
+        if(isweap(weap) && WF(WK(flags), weap, residualundo, WS(flags)))
+        {
+            if(WF(WK(flags), weap, residualundo, WS(flags))&WR(BURN) && m->state.burning(gamemillis, G(burntime)))
+            {
+                m->state.lastres[WR_BURN] = m->state.lastrestime[WR_BURN] = 0;
+                sendf(-1, 1, "ri3", N_SPHY, m->clientnum, SPHY_EXTINGUISH);
+            }
+            if(WF(WK(flags), weap, residualundo, WS(flags))&WR(BLEED) && m->state.bleeding(gamemillis, G(bleedtime)))
+                m->state.lastres[WR_BLEED] = m->state.lastrestime[WR_BLEED] = 0;
+            if(WF(WK(flags), weap, residualundo, WS(flags))&WR(SHOCK) && m->state.shocking(gamemillis, G(shocktime)))
+                m->state.lastres[WR_SHOCK] = m->state.lastrestime[WR_SHOCK] = 0;
         }
 
         if(nodamage || !hithurts(realflags))
@@ -3320,136 +3630,123 @@ namespace server
         }
         else
         {
-            if(realdamage >= 0 && target->state.armour > 0)
-            {
-                int absorb = realdamage/2; // armour absorbs half until depleted
-                if(target->state.armour < absorb) absorb = target->state.armour;
-                target->state.armour -= absorb;
-                realdamage -= absorb;
-            }
-            hurt = min(target->state.health, realdamage);
-            target->state.health = min(target->state.health-realdamage, m_maxhealth(gamemode, mutators, target->state.model));
-            if(target->state.health <= m_health(gamemode, mutators, target->state.model)) target->state.lastregen = 0;
-            target->state.lastpain = gamemillis;
-            actor->state.damage += realdamage;
-            if(target->state.health <= 0) realflags |= HIT_KILL;
-            if(wr_burning(weap, flags))
-            {
-                target->state.lastres[WR_BURN] = target->state.lastrestime[WR_BURN] = gamemillis;
-                target->state.lastresowner[WR_BURN] = actor->clientnum;
-            }
-            if(wr_bleeding(weap, flags))
+            m->state.health = m->state.health-realdamage;
+            if(m->state.actortype < A_ENEMY) m->state.health = min(m->state.health, m_maxhealth(gamemode, mutators, m->state.model));
+            if(realdamage > 0)
             {
-                target->state.lastres[WR_BLEED] = target->state.lastrestime[WR_BLEED] = gamemillis;
-                target->state.lastresowner[WR_BLEED] = actor->clientnum;
-            }
-            if(wr_shocking(weap, flags))
-            {
-                target->state.lastres[WR_SHOCK] = target->state.lastrestime[WR_SHOCK] = gamemillis;
-                target->state.lastresowner[WR_SHOCK] = actor->clientnum;
+                hurt = min(m->state.health, realdamage);
+                m->state.lastregen = 0;
+                m->state.lastpain = gamemillis;
+                v->state.damage += realdamage;
+                if(m->state.health <= 0) realflags |= HIT_KILL;
+                if(wr_burning(weap, flags))
+                {
+                    m->state.lastres[WR_BURN] = m->state.lastrestime[WR_BURN] = gamemillis;
+                    m->state.lastresowner[WR_BURN] = v->clientnum;
+                }
+                if(wr_bleeding(weap, flags))
+                {
+                    m->state.lastres[WR_BLEED] = m->state.lastrestime[WR_BLEED] = gamemillis;
+                    m->state.lastresowner[WR_BLEED] = v->clientnum;
+                }
+                if(wr_shocking(weap, flags))
+                {
+                    m->state.lastres[WR_SHOCK] = m->state.lastrestime[WR_SHOCK] = gamemillis;
+                    m->state.lastresowner[WR_SHOCK] = v->clientnum;
+                }
             }
         }
-        if(smode) smode->dodamage(target, actor, realdamage, hurt, weap, realflags, material, hitpush);
-        mutate(smuts, mut->dodamage(target, actor, realdamage, hurt, weap, realflags, material, hitpush));
-        if(target != actor && (!m_team(gamemode, mutators) || target->team != actor->team))
-            addhistory(target, actor, gamemillis);
-        sendf(-1, 1, "ri8i3", N_DAMAGE, target->clientnum, actor->clientnum, weap, realflags, realdamage, target->state.health, target->state.armour, hitpush.x, hitpush.y, hitpush.z);
+        if(smode) smode->dodamage(m, v, realdamage, hurt, weap, realflags, material, hitpush, hitvel, dist);
+        mutate(smuts, mut->dodamage(m, v, realdamage, hurt, weap, realflags, material, hitpush, hitvel, dist));
+        if(realdamage >= 0 && m != v && (!m_team(gamemode, mutators) || m->team != v->team))
+            addhistory(m, v, gamemillis);
+        sendf(-1, 1, "ri9i5", N_DAMAGE, m->clientnum, v->clientnum, weap, realflags, realdamage, m->state.health, hitpush.x, hitpush.y, hitpush.z, hitvel.x, hitvel.y, hitvel.z, int(dist*DNF));
         if(realflags&HIT_KILL)
         {
             int fragvalue = 1;
-            if(target != actor && (!m_team(gamemode, mutators) || target->team != actor->team)) actor->state.frags++;
+            if(m != v && (!m_team(gamemode, mutators) || m->team != v->team)) v->state.frags++;
             else fragvalue = -fragvalue;
-#ifdef CAMPAIGN
-            bool isai = target->state.aitype >= AI_START && !m_campaign(gamemode),
-#else
-            bool isai = target->state.aitype >= AI_START,
-#endif
-                 isteamkill = false;
-            int pointvalue = (smode && !isai ? smode->points(target, actor) : fragvalue), style = FRAG_NONE;
+            bool isai = m->state.actortype >= A_ENEMY, isteamkill = false;
+            int pointvalue = (smode && !isai ? smode->points(m, v) : fragvalue), style = FRAG_NONE;
             pointvalue *= isai ? G(enemybonus) : G(fragbonus);
-            if(realdamage >= (realflags&HIT_EXPLODE ? m_health(gamemode, mutators, target->state.model)/2 : m_health(gamemode, mutators, target->state.model)))
+            if(realdamage >= (realflags&HIT_EXPLODE ? m_health(gamemode, mutators, m->state.model)/2 : m_health(gamemode, mutators, m->state.model)))
                 style = FRAG_OBLITERATE;
-            target->state.spree = 0;
-            if(m_team(gamemode, mutators) && actor->team == target->team)
+            m->state.spree = 0;
+            if(m_team(gamemode, mutators) && v->team == m->team)
             {
-                actor->state.spree = 0;
-                if(actor->state.aitype < AI_START && isweap(weap) && WF(WK(flags), weap, teampenalty, WS(flags)))
+                v->state.spree = 0;
+                if(isweap(weap) && (v == m || WF(WK(flags), weap, damagepenalty, WS(flags))))
                 {
                     pointvalue *= G(teamkillpenalty);
-                    if(actor != target) isteamkill = true;
+                    if(v != m) isteamkill = true;
                 }
-                if(flags&HIT_HEAD) style |= FRAG_HEADSHOT;
+                else pointvalue = 0; // no penalty
             }
-            else if(actor != target && actor->state.aitype < AI_START)
+            else if(v != m && v->state.actortype < A_ENEMY)
             {
-#ifdef CAMPAIGN
-                if(!m_campaign(gamemode))
-#endif
-                {
-                    if(!firstblood && !m_duel(gamemode, mutators) && actor->state.aitype == AI_NONE && target->state.aitype < AI_START)
-                    {
-                        firstblood = true;
-                        style |= FRAG_FIRSTBLOOD;
-                        pointvalue += G(firstbloodpoints);
-                    }
+                if(!firstblood && !m_duel(gamemode, mutators) && ((v->state.actortype == A_PLAYER && m->state.actortype < A_ENEMY) || (v->state.actortype < A_ENEMY && m->state.actortype == A_PLAYER)))
+                {
+                    firstblood = true;
+                    style |= FRAG_FIRSTBLOOD;
+                    pointvalue += G(firstbloodpoints);
                 }
                 if(flags&HIT_HEAD) // NOT HZONE
                 {
                     style |= FRAG_HEADSHOT;
                     pointvalue += G(headshotpoints);
                 }
-                if(m_fight(gamemode) && target->state.aitype < AI_START)
+                if(m_fight(gamemode) && m->state.actortype < A_ENEMY)
                 {
                     int logs = 0;
-                    actor->state.spree++;
-                    actor->state.fraglog.add(target->clientnum);
+                    v->state.spree++;
+                    v->state.fraglog.add(m->clientnum);
                     if(G(multikilldelay))
                     {
                         logs = 0;
-                        loopv(actor->state.fragmillis)
+                        loopv(v->state.fragmillis)
                         {
-                            if(lastmillis-actor->state.fragmillis[i] > G(multikilldelay)) actor->state.fragmillis.remove(i--);
+                            if(gamemillis-v->state.fragmillis[i] > G(multikilldelay)) v->state.fragmillis.remove(i--);
                             else logs++;
                         }
-                        if(!logs) actor->state.rewards[0] &= ~FRAG_MULTI;
-                        actor->state.fragmillis.add(lastmillis); logs++;
+                        if(!logs) v->state.rewards[0] &= ~FRAG_MULTI;
+                        v->state.fragmillis.add(gamemillis);
+                        logs++;
                         if(logs >= 2)
                         {
                             int offset = clamp(logs-2, 0, 2), type = 1<<(FRAG_MKILL+offset); // double, triple, multi..
-                            if(!(actor->state.rewards[0]&type))
+                            if(!(v->state.rewards[0]&type))
                             {
                                 style |= type;
-                                actor->state.rewards[0] |= type;
-                                pointvalue *= (G(multikillpoints) ? offset+1 : 1)*G(multikillbonus);
-                                //loopv(actor->state.fragmillis) actor->state.fragmillis[i] = lastmillis;
+                                v->state.rewards[0] |= type;
+                                pointvalue += (G(multikillbonus) ? offset+1 : 1)*G(multikillpoints);
                             }
                         }
                     }
-                    loopj(FRAG_SPREES) if(target->state.rewards[1]&(1<<(FRAG_SPREE+j)))
+                    loopj(FRAG_SPREES) if(m->state.rewards[1]&(1<<(FRAG_SPREE+j)))
                     {
                         style |= FRAG_BREAKER;
                         pointvalue += G(spreebreaker);
                         break;
                     }
-                    if(actor->state.spree <= G(spreecount)*FRAG_SPREES && !(actor->state.spree%G(spreecount)))
+                    if(v->state.spree <= G(spreecount)*FRAG_SPREES && !(v->state.spree%G(spreecount)))
                     {
-                        int offset = clamp((actor->state.spree/G(spreecount)), 1, int(FRAG_SPREES))-1, type = 1<<(FRAG_SPREE+offset);
-                        if(!(actor->state.rewards[0]&type))
+                        int offset = clamp((v->state.spree/G(spreecount)), 1, int(FRAG_SPREES))-1, type = 1<<(FRAG_SPREE+offset);
+                        if(!(v->state.rewards[0]&type))
                         {
                             style |= type;
-                            loopj(2) actor->state.rewards[j] |= type;
+                            loopj(2) v->state.rewards[j] |= type;
                             pointvalue += G(spreepoints);
                         }
                     }
                     logs = 0;
-                    loopv(target->state.fraglog) if(target->state.fraglog[i] == actor->clientnum) { logs++; target->state.fraglog.remove(i--); }
+                    loopv(m->state.fraglog) if(m->state.fraglog[i] == v->clientnum) { logs++; m->state.fraglog.remove(i--); }
                     if(logs >= G(dominatecount))
                     {
                         style |= FRAG_REVENGE;
                         pointvalue += G(revengepoints);
                     }
                     logs = 0;
-                    loopv(actor->state.fraglog) if(actor->state.fraglog[i] == target->clientnum) logs++;
+                    loopv(v->state.fraglog) if(v->state.fraglog[i] == m->clientnum) logs++;
                     if(logs == G(dominatecount))
                     {
                         style |= FRAG_DOMINATE;
@@ -3457,63 +3754,66 @@ namespace server
                     }
                 }
             }
-            if(m_checkpoint(gamemode) && (m_gauntlet(gamemode) || target->state.cpnodes.length() == 1))
+            if(m_race(gamemode) && (!m_gsp3(gamemode, mutators) || m->team == T_ALPHA) && m->state.cpnodes.length() == 1)
             {  // reset if hasn't reached another checkpoint yet
-                target->state.cpmillis = 0;
-                target->state.cpnodes.shrink(0);
-                sendf(-1, 1, "ri3", N_CHECKPOINT, target->clientnum, -1);
+                m->state.cpmillis = 0;
+                m->state.cpnodes.shrink(0);
+                sendf(-1, 1, "ri3", N_CHECKPOINT, m->clientnum, -1);
             }
             if(pointvalue && !m_nopoints(gamemode, mutators))
             {
-                if(actor != target && actor->state.aitype >= AI_START && target->state.aitype < AI_START)
+                if(v != m && v->state.actortype >= A_ENEMY && m->state.actortype < A_ENEMY)
                 {
                     pointvalue = -pointvalue;
-                    givepoints(target, pointvalue);
+                    givepoints(m, pointvalue);
                 }
-                else if(actor->state.aitype < AI_START) givepoints(actor, pointvalue);
+                else if(v->state.actortype < A_ENEMY) givepoints(v, pointvalue);
             }
-            target->state.deaths++;
-            target->state.rewards[1] = 0;
-            dropitems(target, aistyle[target->state.aitype].living ? DROP_DEATH : DROP_EXPIRE);
+            m->state.deaths++;
+            m->state.rewards[1] = 0;
+            dropitems(m, actor[m->state.actortype].living ? DROP_DEATH : DROP_EXPIRE);
             static vector<int> dmglog;
             dmglog.setsize(0);
-            gethistory(target, actor, gamemillis, dmglog, true, 1);
-            sendf(-1, 1, "ri9i3v", N_DIED, target->clientnum, target->state.deaths, actor->clientnum, actor->state.frags, actor->state.spree, style, weap, realflags, realdamage, material, dmglog.length(), dmglog.length(), dmglog.getbuf());
-            target->position.setsize(0);
-            if(smode) smode->died(target, actor);
-            mutate(smuts, mut->died(target, actor));
-            target->state.state = CS_DEAD; // don't issue respawn yet until DEATHMILLIS has elapsed
-            target->state.lastdeath = gamemillis;
-            if(isteamkill && actor->state.aitype == AI_NONE) // don't punish the idiot bots
-            {
-                actor->state.teamkills.add(teamkill(totalmillis, actor->team, -pointvalue));
-                if(G(teamkilllock) && !haspriv(actor, G(teamkilllock)))
+            gethistory(m, v, gamemillis, dmglog, true, 1);
+            sendf(-1, 1, "ri9i3v", N_DIED, m->clientnum, m->state.deaths, v->clientnum, v->state.frags, v->state.spree, style, weap, realflags, realdamage, material, dmglog.length(), dmglog.length(), dmglog.getbuf());
+            m->position.setsize(0);
+            if(smode) smode->died(m, v);
+            mutate(smuts, mut->died(m, v));
+            m->state.state = CS_DEAD; // don't issue respawn yet until DEATHMILLIS has elapsed
+            m->state.lastdeath = gamemillis;
+            if(m->state.actortype == A_BOT) aiman::setskill(m);
+            if(m != v && v->state.actortype == A_BOT) aiman::setskill(v);
+            if(isteamkill && v->state.actortype == A_PLAYER) // don't punish the idiot bots
+            {
+                v->state.teamkills.add(teamkill(totalmillis, v->team, 0-pointvalue));
+                if(G(teamkilllock) && !haspriv(v, G(teamkilllock)))
                 {
                     int numkills = 0;
-                    if(!G(teamkilltime)) numkills = actor->state.teamkills.length();
-                    else loopv(actor->state.teamkills)
-                        if(totalmillis-actor->state.teamkills[i].millis <= G(teamkilltime)*1000*60) numkills++;
+                    if(!G(teamkilltime)) numkills = v->state.teamkills.length();
+                    else loopv(v->state.teamkills)
+                        if(totalmillis-v->state.teamkills[i].millis <= G(teamkilltime)*1000*60) numkills++;
                     if(numkills >= G(teamkillwarn) && numkills%G(teamkillwarn) == 0)
                     {
-                        uint ip = getclientip(actor->clientnum);
-                        actor->state.warnings[WARN_TEAMKILL][0]++;
-                        actor->state.warnings[WARN_TEAMKILL][1] = totalmillis ? totalmillis : 1;
-                        if(ip && G(teamkillban) && actor->state.warnings[WARN_TEAMKILL][0] >= G(teamkillban) && !haspriv(actor, PRIV_MODERATOR) && !checkipinfo(control, ipinfo::ALLOW, ip))
+                        uint ip = getclientip(v->clientnum);
+                        v->state.warnings[WARN_TEAMKILL][0]++;
+                        v->state.warnings[WARN_TEAMKILL][1] = totalmillis ? totalmillis : 1;
+                        if(ip && G(teamkillban) && v->state.warnings[WARN_TEAMKILL][0] >= G(teamkillban) && !haspriv(v, PRIV_MODERATOR) && !checkipinfo(control, ipinfo::EXCEPT, ip))
                         {
                             ipinfo &c = control.add();
                             c.ip = ip;
                             c.mask = 0xFFFFFFFF;
                             c.type = ipinfo::BAN;
                             c.time = totalmillis ? totalmillis : 1;
-                            srvoutf(-3, "\fs\fcbanned\fS %s (%s): team killing is not permitted", colourname(actor), gethostname(actor->clientnum));
+                            c.reason = newstring("team killing is not permitted");
+                            srvoutf(-3, "\fs\fcbanned\fS %s (%s/%s): %s", colourname(v), gethostname(v->clientnum), gethostip(v->clientnum), c.reason);
                             updatecontrols = true;
                         }
-                        else if(G(teamkillkick) && actor->state.warnings[WARN_TEAMKILL][0] >= G(teamkillkick))
+                        else if(G(teamkillkick) && v->state.warnings[WARN_TEAMKILL][0] >= G(teamkillkick))
                         {
-                            srvoutf(-3, "\fs\fckicked\fS %s: team killing is not permitted", colourname(actor));
-                            actor->kicked = updatecontrols = true;
+                            srvoutf(-3, "\fs\fckicked\fS %s: team killing is not permitted", colourname(v));
+                            v->kicked = updatecontrols = true;
                         }
-                        else srvmsgft(actor->clientnum, CON_CHAT, "\fy\fs\fzoyWARNING\fS: team killing is not permitted, action will be taken if you continue");
+                        else srvmsgft(v->clientnum, CON_CHAT, "\fy\fs\fzoyWARNING:\fS team killing is not permitted, action will be taken if you continue");
                     }
                 }
             }
@@ -3531,15 +3831,20 @@ namespace server
             mutate(smuts, if(!mut->damage(ci, ci, ci->state.health, -1, flags, material)) { return; });
         }
         ci->state.spree = 0;
-        if(m_checkpoint(gamemode) && (!flags || m_gauntlet(gamemode) || ci->state.cpnodes.length() == 1))
+        ci->state.deaths++;
+        bool kamikaze = dropitems(ci, actor[ci->state.actortype].living ? DROP_DEATH : DROP_EXPIRE);
+        if(m_race(gamemode) && (!m_gsp3(gamemode, mutators) || ci->team == T_ALPHA) && !(flags&HIT_SPEC) && (!flags || ci->state.cpnodes.length() == 1))
         { // reset if suicided, hasn't reached another checkpoint yet
             ci->state.cpmillis = 0;
             ci->state.cpnodes.shrink(0);
             sendf(-1, 1, "ri3", N_CHECKPOINT, ci->clientnum, -1);
         }
-        else if(!m_nopoints(gamemode, mutators)) givepoints(ci, smode ? smode->points(ci, ci) : -1);
-        ci->state.deaths++;
-        dropitems(ci, aistyle[ci->state.aitype].living ? DROP_DEATH : DROP_EXPIRE);
+        else if(!m_nopoints(gamemode, mutators) && ci->state.actortype == A_PLAYER)
+        {
+            int pointvalue = (smode ? smode->points(ci, ci) : -1)*G(fragbonus);
+            if(kamikaze) pointvalue *= G(teamkillpenalty);
+            givepoints(ci, pointvalue);
+        }
         if(G(burntime) && flags&HIT_BURN)
         {
             ci->state.lastres[WR_BURN] = ci->state.lastrestime[WR_BURN] = gamemillis;
@@ -3553,9 +3858,10 @@ namespace server
         mutate(smuts, mut->died(ci, NULL));
         gs.state = CS_DEAD;
         gs.lastdeath = gamemillis;
+        if(ci->state.actortype == A_BOT) aiman::setskill(ci);
     }
 
-    int calcdamage(clientinfo *actor, clientinfo *target, int weap, int &flags, float radial, float size, float dist, float scale, bool self)
+    int calcdamage(clientinfo *v, clientinfo *m, int weap, int &flags, float radial, float size, float dist, float scale, bool self)
     {
         flags &= ~HIT_SFLAGS;
         if(!hithurts(flags))
@@ -3565,41 +3871,38 @@ namespace server
         }
 
         float skew = clamp(scale, 0.f, 1.f)*G(damagescale);
+
+        if(flags&HIT_WHIPLASH) skew *= WF(WK(flags), weap, damagewhiplash, WS(flags));
+        else if(flags&HIT_HEAD) skew *= WF(WK(flags), weap, damagehead, WS(flags));
+        else if(flags&HIT_TORSO) skew *= WF(WK(flags), weap, damagetorso, WS(flags));
+        else if(flags&HIT_LEGS) skew *= WF(WK(flags), weap, damagelegs, WS(flags));
+        else return 0;
+
         if(radial > 0) skew *= clamp(1.f-dist/size, 1e-6f, 1.f);
         else if(WF(WK(flags), weap, taper, WS(flags))) skew *= clamp(dist, 0.f, 1.f);
+
         if(!m_insta(gamemode, mutators))
         {
             if(m_capture(gamemode) && G(capturebuffdelay))
             {
-                if(actor->state.lastbuff) skew *= G(capturebuffdamage);
-                if(target->state.lastbuff) skew /= G(capturebuffshield);
+                if(v->state.lastbuff) skew *= G(capturebuffdamage);
+                if(m->state.lastbuff) skew /= G(capturebuffshield);
             }
             else if(m_defend(gamemode) && G(defendbuffdelay))
             {
-                if(actor->state.lastbuff) skew *= G(defendbuffdamage);
-                if(target->state.lastbuff) skew /= G(defendbuffshield);
+                if(v->state.lastbuff) skew *= G(defendbuffdamage);
+                if(m->state.lastbuff) skew /= G(defendbuffshield);
             }
             else if(m_bomber(gamemode) && G(bomberbuffdelay))
             {
-                if(actor->state.lastbuff) skew *= G(bomberbuffdamage);
-                if(target->state.lastbuff) skew /= G(bomberbuffshield);
+                if(v->state.lastbuff) skew *= G(bomberbuffdamage);
+                if(m->state.lastbuff) skew /= G(bomberbuffshield);
             }
-            else if(m_gauntlet(gamemode) && G(gauntletbuffdelay))
-            {
-                if(actor->state.lastbuff) skew *= G(gauntletbuffdamage);
-                if(target->state.lastbuff) skew /= G(gauntletbuffshield);
-            }
-        }
-        if(!(flags&HIT_HEAD))
-        {
-            if(flags&HIT_WHIPLASH) skew *= WF(WK(flags), weap, whipdamage, WS(flags));
-            else if(flags&HIT_TORSO) skew *= WF(WK(flags), weap, torsodamage, WS(flags));
-            else if(flags&HIT_LEGS) skew *= WF(WK(flags), weap, legdamage, WS(flags));
-            else skew = 0;
         }
+
         if(self)
         {
-            float modify = WF(WK(flags), weap, selfdamage, WS(flags))*G(selfdamagescale);
+            float modify = WF(WK(flags), weap, damageself, WS(flags))*G(damageselfscale);
             if(modify != 0) skew *= modify;
             else
             {
@@ -3607,9 +3910,9 @@ namespace server
                 flags |= HIT_WAVE;
             }
         }
-        else if(m_team(gamemode, mutators) && actor->team == target->team)
+        else if(m_team(gamemode, mutators) && v->team == m->team)
         {
-            float modify = WF(WK(flags), weap, teamdamage, WS(flags))*G(teamdamagescale);
+            float modify = WF(WK(flags), weap, damageteam, WS(flags))*G(damageteamscale);
             if(modify != 0) skew *= modify;
             else
             {
@@ -3631,8 +3934,8 @@ namespace server
                 if(G(serverdebug) >= 2) srvmsgf(ci->clientnum, "sync error: sticky [%d (%d)] failed - not found", weap, id);
                 return;
             }
-            clientinfo *victim = target >= 0 ? (clientinfo *)getinfo(target) : NULL;
-            if(target < 0 || (victim && victim->state.state == CS_ALIVE && !victim->state.protect(gamemillis, m_protect(gamemode, mutators))))
+            clientinfo *m = m >= 0 ? (clientinfo *)getinfo(target) : NULL;
+            if(target < 0 || (m && m->state.state == CS_ALIVE && !m->state.protect(gamemillis, m_protect(gamemode, mutators))))
                 sendf(-1, 1, "ri9ix", N_STICKY, ci->clientnum, target, id, norm.x, norm.y, norm.z, pos.x, pos.y, pos.z, ci->clientnum);
             else if(G(serverdebug) >= 2) srvmsgf(ci->clientnum, "sync error: sticky [%d (%d)] failed - state disallows it", weap, id);
         }
@@ -3661,7 +3964,7 @@ namespace server
                     int f = W2(weap, fragweap, WS(flags));
                     if(f >= 0)
                     {
-                        int w = f%W_MAX, r = W2(weap, fragrays, WS(flags));
+                        int w = f%W_MAX, r = min(W2(weap, fragrays, WS(flags)), MAXPARAMS);
                         loopi(r) gs.weapshots[w][f >= W_MAX ? 1 : 0].add(-id);
                     }
                 }
@@ -3670,18 +3973,18 @@ namespace server
             else loopv(hits)
             {
                 hitset &h = hits[i];
-                clientinfo *target = (clientinfo *)getinfo(h.target);
-                if(!target)
+                clientinfo *m = (clientinfo *)getinfo(h.target);
+                if(!m)
                 {
                     if(G(serverdebug) >= 2) srvmsgf(ci->clientnum, "sync error: destroy [%d (%d)] failed - hit %d [%d] not found", weap, id, i, h.target);
                     continue;
                 }
                 if(h.proj)
                 {
-                    servstate &ts = target->state;
+                    servstate &ts = m->state;
                     loopj(W_MAX) loopk(2) if(ts.weapshots[j][k].find(h.proj))
                     {
-                        sendf(target->clientnum, 1, "ri4", N_DESTROY, target->clientnum, 1, h.proj);
+                        sendf(m->clientnum, 1, "ri4", N_DESTROY, m->clientnum, 1, h.proj);
                         break;
                     }
                 }
@@ -3690,10 +3993,10 @@ namespace server
                     int hflags = flags|h.flags;
                     float skew = float(scale)/DNF, rad = radial > 0 ? clamp(radial/DNF, 0.f, WX(WK(flags), weap, explode, WS(flags), gamemode, mutators, skew)) : 0.f,
                           size = rad > 0 ? (hflags&HIT_WAVE ? rad*WF(WK(flags), weap, wavepush, WS(flags)) : rad) : 0.f, dist = float(h.dist)/DNF;
-                    if(target->state.state == CS_ALIVE && !target->state.protect(gamemillis, m_protect(gamemode, mutators)))
+                    if(m->state.state == CS_ALIVE && !m->state.protect(gamemillis, m_protect(gamemode, mutators)))
                     {
-                        int damage = calcdamage(ci, target, weap, hflags, rad, size, dist, skew, ci == target);
-                        if(damage) dodamage(target, ci, damage, weap, hflags, 0, h.dir);
+                        int damage = calcdamage(ci, m, weap, hflags, rad, size, dist, skew, ci == m);
+                        if(damage) dodamage(m, ci, damage, weap, hflags, 0, h.dir, h.vel, dist);
                         else if(G(serverdebug) >= 2)
                             srvmsgf(ci->clientnum, "sync error: destroy [%d (%d)] failed - hit %d [%d] determined zero damage", weap, id, i, h.target);
                     }
@@ -3712,36 +4015,34 @@ namespace server
             if(G(serverdebug) >= 3) srvmsgf(ci->clientnum, "sync error: shoot [%d] failed - unexpected message", weap);
             return;
         }
-        int sub = W2(weap, sub, WS(flags));
-        if(sub > 1 && W2(weap, power, WS(flags)))
+        int sub = W2(weap, ammosub, WS(flags));
+        if(sub > 1 && W2(weap, cooktime, WS(flags)))
         {
             if(ci->state.ammo[weap] < sub)
             {
-                int maxscale = int(ci->state.ammo[weap]/float(sub)*W2(weap, power, WS(flags)));
+                int maxscale = int(ci->state.ammo[weap]/float(sub)*W2(weap, cooktime, WS(flags)));
                 if(scale > maxscale) scale = maxscale;
             }
-            sub = int(ceilf(sub*scale/float(W2(weap, power, WS(flags)))));
+            sub = int(ceilf(sub*scale/float(W2(weap, cooktime, WS(flags)))));
         }
         if(!gs.canshoot(weap, flags, m_weapon(gamemode, mutators), millis))
         {
             if(!gs.canshoot(weap, flags, m_weapon(gamemode, mutators), millis, (1<<W_S_RELOAD)))
             {
-                if(sub && W(weap, max)) ci->state.ammo[weap] = max(ci->state.ammo[weap]-sub, 0);
+                if(sub && W(weap, ammomax)) ci->state.ammo[weap] = max(ci->state.ammo[weap]-sub, 0);
                 if(!gs.hasweap(weap, m_weapon(gamemode, mutators))) gs.entid[weap] = -1; // its gone..
                 if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: shoot [%d] failed - current state disallows it", weap);
                 return;
             }
-            else if(gs.weapload[gs.weapselect] > 0)
+            if(gs.weapload[gs.weapselect] > 0)
             {
                 takeammo(ci, gs.weapselect, gs.weapload[gs.weapselect]);
-                gs.reloads[gs.weapselect] = max(gs.reloads[gs.weapselect]-1, 0);
-                gs.weapload[gs.weapselect] = -gs.weapload[gs.weapselect];
-                sendf(-1, 1, "ri6", N_RELOAD, ci->clientnum, gs.weapselect, gs.weapload[gs.weapselect], gs.ammo[gs.weapselect], gs.reloads[gs.weapselect]);
+                gs.weapload[gs.weapselect] = -gs.weapload[gs.weapselect]; // the client should already do this for themself
+                sendf(-1, 1, "ri5x", N_RELOAD, ci->clientnum, gs.weapselect, gs.weapload[gs.weapselect], gs.ammo[gs.weapselect], ci->clientnum);
             }
-            else return;
         }
         takeammo(ci, weap, sub);
-        gs.setweapstate(weap, WS(flags) ? W_S_SECONDARY : W_S_PRIMARY, W2(weap, attackdelay, WS(flags)), millis);
+        gs.setweapstate(weap, WS(flags) ? W_S_SECONDARY : W_S_PRIMARY, W2(weap, delayattack, WS(flags)), millis);
         sendf(-1, 1, "ri8ivx", N_SHOTFX, ci->clientnum, weap, flags, scale, from.x, from.y, from.z, shots.length(), shots.length()*sizeof(shotmsg)/sizeof(int), shots.getbuf(), ci->clientnum);
         gs.weapshot[weap] = sub;
         gs.shotdamage += W2(weap, damage, WS(flags))*shots.length();
@@ -3749,8 +4050,8 @@ namespace server
         if(!gs.hasweap(weap, m_weapon(gamemode, mutators)))
         {
             //if(sents.inrange(gs.entid[weap])) setspawn(gs.entid[weap], false);
-            sendf(-1, 1, "ri8", N_DROP, ci->clientnum, -1, 1, weap, -1, 0, 0);
-            gs.ammo[weap] = gs.entid[weap] = gs.reloads[weap] = -1; // its gone..
+            sendf(-1, 1, "ri7", N_DROP, ci->clientnum, -1, 1, weap, -1, 0);
+            gs.ammo[weap] = gs.entid[weap] = -1; // its gone..
         }
     }
 
@@ -3765,20 +4066,18 @@ namespace server
         }
         if(!gs.canswitch(weap, m_weapon(gamemode, mutators), millis, (1<<W_S_SWITCH)))
         {
-            if(!gs.canswitch(weap, m_weapon(gamemode, mutators), millis, (1<<W_S_RELOAD)))
+            if(!gs.canswitch(weap, m_weapon(gamemode, mutators), millis, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
             {
                 if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: switch [%d] failed - current state disallows it", weap);
-                sendf(ci->clientnum, 1, "ri3", N_WSELECT, ci->clientnum, gs.weapselect);
+                sendf(-1, 1, "ri3", N_WSELECT, ci->clientnum, gs.weapselect);
                 return;
             }
-            else if(gs.weapload[gs.weapselect] > 0)
+            if(gs.weapload[gs.weapselect] > 0)
             {
                 takeammo(ci, gs.weapselect, gs.weapload[gs.weapselect]);
-                gs.reloads[gs.weapselect] = max(gs.reloads[gs.weapselect]-1, 0);
-                gs.weapload[gs.weapselect] = -gs.weapload[gs.weapselect];
-                sendf(-1, 1, "ri6", N_RELOAD, ci->clientnum, gs.weapselect, gs.weapload[gs.weapselect], gs.ammo[gs.weapselect], gs.reloads[gs.weapselect]);
+                gs.weapload[gs.weapselect] = -gs.weapload[gs.weapselect]; // the client should already do this for themself
+                sendf(-1, 1, "ri5x", N_RELOAD, ci->clientnum, gs.weapselect, gs.weapload[gs.weapselect], gs.ammo[gs.weapselect], ci->clientnum);
             }
-            else return;
         }
         gs.weapswitch(weap, millis, G(weaponswitchdelay));
         sendf(-1, 1, "ri3x", N_WSELECT, ci->clientnum, weap, ci->clientnum);
@@ -3793,32 +4092,31 @@ namespace server
             return;
         }
         int sweap = m_weapon(gamemode, mutators);
-        if(!gs.candrop(weap, sweap, millis, (1<<W_S_SWITCH)))
+        if(!gs.candrop(weap, sweap, millis, m_loadout(gamemode, mutators), (1<<W_S_SWITCH)))
         {
-            if(!gs.candrop(weap, sweap, millis, (1<<W_S_RELOAD)))
+            if(!gs.candrop(weap, sweap, millis, m_loadout(gamemode, mutators), (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
             {
                 if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: drop [%d] failed - current state disallows it", weap);
                 return;
             }
-            else if(gs.weapload[weap] > 0)
+            if(gs.weapload[gs.weapselect] > 0)
             {
-                takeammo(ci, weap, gs.weapload[weap]);
-                gs.weapload[weap] = -gs.weapload[weap];
+                takeammo(ci, gs.weapselect, gs.weapload[gs.weapselect]);
+                gs.weapload[gs.weapselect] = -gs.weapload[gs.weapselect];
+                sendf(-1, 1, "ri5x", N_RELOAD, ci->clientnum, gs.weapselect, gs.weapload[gs.weapselect], gs.ammo[gs.weapselect], ci->clientnum);
             }
-            else return;
         }
-        int dropped = -1, ammo = -1, reloads = -1, nweap = gs.bestweap(sweap, true); // switch to best weapon
+        int dropped = -1, ammo = -1, nweap = gs.bestweap(sweap, true); // switch to best weapon
         if(sents.inrange(gs.entid[weap]))
         {
             dropped = gs.entid[weap];
-            ammo = gs.ammo[weap] ? gs.ammo[weap] : W(weap, max);
-            reloads = gs.reloads[weap];
+            ammo = gs.ammo[weap] ? gs.ammo[weap] : W(weap, ammomax);
             setspawn(dropped, false);
-            gs.dropped.add(dropped, ammo, reloads);
+            gs.dropped.add(dropped, ammo);
         }
-        gs.ammo[weap] = gs.entid[weap] = gs.reloads[weap] = -1;
+        gs.ammo[weap] = gs.entid[weap] = -1;
         gs.weapswitch(nweap, millis, G(weaponswitchdelay));
-        sendf(-1, 1, "ri8", N_DROP, ci->clientnum, nweap, 1, weap, dropped, ammo, reloads);
+        sendf(-1, 1, "ri7", N_DROP, ci->clientnum, nweap, 1, weap, dropped, ammo);
     }
 
     void reloadevent::process(clientinfo *ci)
@@ -3827,92 +4125,70 @@ namespace server
         if(!gs.isalive(gamemillis) || !isweap(weap))
         {
             if(G(serverdebug) >= 3) srvmsgf(ci->clientnum, "sync error: reload [%d] failed - unexpected message", weap);
-            sendf(ci->clientnum, 1, "ri6", N_RELOAD, ci->clientnum, weap, gs.weapload[weap], gs.ammo[weap], gs.reloads[weap]);
             return;
         }
         if(!gs.canreload(weap, m_weapon(gamemode, mutators), false, millis))
         {
             if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: reload [%d] failed - current state disallows it", weap);
-            sendf(ci->clientnum, 1, "ri6", N_RELOAD, ci->clientnum, weap, gs.weapload[weap], gs.ammo[weap], gs.reloads[weap]);
             return;
         }
-        gs.setweapstate(weap, W_S_RELOAD, W(weap, reloaddelay), millis);
+        gs.setweapstate(weap, W_S_RELOAD, W(weap, delayreload), millis);
         int oldammo = gs.ammo[weap];
-        gs.ammo[weap] = min(max(gs.ammo[weap], 0) + W(weap, add), W(weap, max));
-        gs.reloads[weap]++;
+        gs.ammo[weap] = min(max(gs.ammo[weap], 0) + W(weap, ammoadd), W(weap, ammomax));
         gs.weapload[weap] = gs.ammo[weap]-oldammo;
-        sendf(-1, 1, "ri6x", N_RELOAD, ci->clientnum, weap, gs.weapload[weap], gs.ammo[weap], gs.reloads[weap], ci->clientnum);
+        sendf(-1, 1, "ri5x", N_RELOAD, ci->clientnum, weap, gs.weapload[weap], gs.ammo[weap], ci->clientnum);
     }
 
     void useevent::process(clientinfo *ci)
     {
         servstate &gs = ci->state;
-        if(gs.state != CS_ALIVE || !sents.inrange(ent) || enttype[sents[ent].type].usetype != EU_ITEM)
+        if(gs.state != CS_ALIVE || !sents.inrange(ent) || sents[ent].type != WEAPON)
         {
             if(G(serverdebug) >= 3) srvmsgf(ci->clientnum, "sync error: use [%d] failed - unexpected message", ent);
             return;
         }
-        int sweap = m_weapon(gamemode, mutators), attr = sents[ent].type == WEAPON ? w_attr(gamemode, mutators, sents[ent].attrs[0], sweap) : sents[ent].attrs[0];
         if(!finditem(ent))
         {
             if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: use [%d] failed - doesn't seem to be spawned anywhere", ent);
             return;
         }
+        int sweap = m_weapon(gamemode, mutators), attr = w_attr(gamemode, mutators, sents[ent].type, sents[ent].attrs[0], sweap);
         if(!gs.canuse(sents[ent].type, attr, sents[ent].attrs, sweap, millis, (1<<W_S_SWITCH)))
         {
-            if(!gs.canuse(sents[ent].type, attr, sents[ent].attrs, sweap, millis, (1<<W_S_RELOAD)))
+            if(!gs.canuse(sents[ent].type, attr, sents[ent].attrs, sweap, millis, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
             {
                 if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: use [%d] failed - current state disallows it", ent);
                 return;
             }
-            else if(gs.weapload[gs.weapselect] > 0)
+            if(gs.weapload[gs.weapselect] > 0)
             {
                 takeammo(ci, gs.weapselect, gs.weapload[gs.weapselect]);
-                gs.reloads[gs.weapselect] = max(gs.reloads[gs.weapselect]-1, 0);
-                gs.weapload[gs.weapselect] = -gs.weapload[gs.weapselect];
-                sendf(-1, 1, "ri6", N_RELOAD, ci->clientnum, gs.weapselect, gs.weapload[gs.weapselect], gs.ammo[gs.weapselect], gs.reloads[gs.weapselect]);
+                gs.weapload[gs.weapselect] = -gs.weapload[gs.weapselect]; // the client should already do this for themself
+                sendf(-1, 1, "ri5x", N_RELOAD, ci->clientnum, gs.weapselect, gs.weapload[gs.weapselect], gs.ammo[gs.weapselect], ci->clientnum);
             }
-            else return;
         }
-        int weap = -1, ammoamt = -1, reloadamt = -1, dropped = -1, ammo = -1, reloads = -1;
-        switch(sents[ent].type)
+        int weap = -1, ammoamt = -1, dropped = -1, ammo = -1;
+        if(m_classic(gamemode, mutators) && !gs.hasweap(attr, sweap) && w_carry(attr, sweap) && gs.carry(sweap) >= G(maxcarry)) weap = gs.drop(sweap);
+        loopvk(clients) if(clients[k]->state.dropped.find(ent))
         {
-            case WEAPON:
-            {
-                if(!gs.hasweap(attr, sweap) && w_carry(attr, sweap) && gs.carry(sweap) >= G(maxcarry))
-                    weap = gs.drop(sweap);
-                loopvk(clients) if(clients[k]->state.dropped.find(ent))
-                {
-                    clients[k]->state.dropped.values(ent, ammoamt, reloadamt);
-                    break;
-                }
-                if(isweap(weap))
-                {
-                    if(sents.inrange(gs.entid[weap]))
-                    {
-                        dropped = gs.entid[weap];
-                        ammo = gs.ammo[weap];
-                        reloads = gs.reloads[weap];
-                        setspawn(dropped, false);
-                        gs.setweapstate(weap, W_S_SWITCH, G(weaponswitchdelay), millis);
-                        gs.dropped.add(dropped, ammo, reloads);
-                    }
-                    gs.ammo[weap] = gs.entid[weap] = gs.reloads[weap] = -1;
-                }
-                break;
-            }
-#ifdef MEK
-            case HEALTH:
+            clients[k]->state.dropped.values(ent, ammoamt);
+            break;
+        }
+        if(isweap(weap))
+        {
+            if(sents.inrange(gs.entid[weap]))
             {
-                ammoamt = healthamt[attr];
-                break;
+                dropped = gs.entid[weap];
+                ammo = gs.ammo[weap];
+                setspawn(dropped, false);
+                gs.setweapstate(weap, W_S_SWITCH, G(weaponswitchdelay), millis);
+                gs.dropped.add(dropped, ammo);
             }
-#endif
-            default: break;
+            gs.ammo[weap] = gs.entid[weap] = -1;
         }
         setspawn(ent, false, true);
-        gs.useitem(ent, sents[ent].type, attr, ammoamt, reloadamt, sweap, millis, G(weaponswitchdelay));
-        sendf(-1, 1, "ri9i", N_ITEMACC, ci->clientnum, ent, ammoamt, reloadamt, sents[ent].spawned ? 1 : 0, weap, dropped, ammo, reloads);
+        gs.useitem(ent, sents[ent].type, attr, ammoamt, sweap, millis, G(weaponswitchdelay));
+        sendf(-1, 1, "ri8", N_ITEMACC, ci->clientnum, ent, ammoamt, sents[ent].spawned ? 1 : 0, weap, dropped, ammo);
     }
 
     bool gameevent::flush(clientinfo *ci, int fmillis)
@@ -3971,21 +4247,22 @@ namespace server
         while(ci->events.length() > keep) delete ci->events.pop();
     }
 
-    void waiting(clientinfo *ci, int drop, bool exclude)
+    int requestswap(clientinfo *ci, int team)
     {
-#ifdef CAMPAIGN
-        if(m_campaign(gamemode) && ci->state.cpnodes.empty())
+        if(!allowteam(ci, team, T_FIRST, numclients() > 1))
         {
-            int maxnodes = -1;
-            loopv(clients)
+            if(team && m_swapteam(gamemode, mutators) && ci->team != team && ci->state.actortype == A_PLAYER && ci->swapteam != team && canplay())
             {
-                clientinfo *oi = clients[i];
-                if(oi->clientnum >= 0 && oi->name[0] && oi->state.aitype < AI_START && (!clients.inrange(maxnodes) || oi->state.cpnodes.length() > clients[maxnodes]->state.cpnodes.length()))
-                    maxnodes = i;
+                ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fy%s requests swap to team %s, change teams to accept", colourname(ci), colourteam(team));
+                ci->swapteam = team;
             }
-            if(clients.inrange(maxnodes)) loopv(clients[maxnodes]->state.cpnodes) ci->state.cpnodes.add(clients[maxnodes]->state.cpnodes[i]);
+            team = chooseteam(ci);
         }
-#endif
+        return team;
+    }
+
+    void waiting(clientinfo *ci, int drop, bool doteam, bool exclude)
+    {
         if(ci->state.state == CS_ALIVE)
         {
             if(drop) dropitems(ci, drop);
@@ -3998,8 +4275,7 @@ namespace server
         else sendf(-1, 1, "ri2", N_WAITING, ci->clientnum);
         ci->state.state = CS_WAITING;
         ci->state.weapreset(false);
-        if(m_loadout(gamemode, mutators)) chkloadweap(ci);
-        if(!allowteam(ci, ci->team, T_FIRST)) setteam(ci, chooseteam(ci), TT_SMINFO);
+        if(doteam && !allowteam(ci, ci->team, T_FIRST, false)) setteam(ci, chooseteam(ci), TT_INFO);
     }
 
     int triggertime(int i)
@@ -4014,25 +4290,6 @@ namespace server
 
     void checkents()
     {
-        bool thresh = m_fight(gamemode) && !m_noitems(gamemode, mutators) && !m_special(gamemode, mutators) && G(itemthreshold) > 0;
-        int items[MAXENTTYPES], lowest[MAXENTTYPES], sweap = m_weapon(gamemode, mutators), plr = 0;
-        memset(items, 0, sizeof(items)); memset(lowest, -1, sizeof(lowest));
-        if(thresh)
-        {
-            loopv(clients) if(clients[i]->clientnum >= 0 && clients[i]->online && clients[i]->state.state == CS_ALIVE && clients[i]->state.aitype < AI_START)
-                plr++;
-            loopv(sents) if(enttype[sents[i].type].usetype == EU_ITEM && hasitem(i))
-            {
-                if(sents[i].type == WEAPON)
-                {
-                    int attr = w_attr(gamemode, mutators, sents[i].attrs[0], sweap);
-                    if(attr < W_OFFSET || attr >= W_ITEM) continue;
-                }
-                if(finditem(i, true, true)) items[sents[i].type]++;
-                else if(!sents.inrange(lowest[sents[i].type]) || sents[i].millis < sents[lowest[sents[i].type]].millis)
-                    lowest[sents[i].type] = i;
-            }
-        }
         loopv(sents) switch(sents[i].type)
         {
             case TRIGGER:
@@ -4054,21 +4311,10 @@ namespace server
             }
             default:
             {
+                if(enttype[sents[i].type].usetype != EU_ITEM) break;
                 bool allowed = hasitem(i);
-                if(enttype[sents[i].type].usetype == EU_ITEM && (allowed || sents[i].spawned))
-                {
-                    bool found = finditem(i, true, true);
-                    if(allowed && thresh && i == lowest[sents[i].type] && (gamemillis-sents[lowest[sents[i].type]].last > G(itemspawndelay)))
-                    {
-                        float dist = items[sents[i].type]/float(plr*G(maxcarry));
-                        if(dist < G(itemthreshold)) found = false;
-                    }
-                    if((!found && !sents[i].spawned) || (!allowed && sents[i].spawned))
-                    {
-                        setspawn(i, allowed, true, true);
-                        items[sents[i].type]++;
-                    }
-                }
+                if((allowed && !sents[i].spawned && !finditem(i, true, true)) || (!allowed && sents[i].spawned))
+                    setspawn(i, allowed, true, true);
                 break;
             }
         }
@@ -4087,27 +4333,31 @@ namespace server
             if(smode) smode->leavegame(ci);
             mutate(smuts, mut->leavegame(ci));
             sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, quarantine ? 2 : 1);
-            ci->state.cpnodes.shrink(0);
-            ci->state.cpmillis = 0;
             ci->state.state = CS_SPECTATOR;
             ci->state.quarantine = quarantine;
-            ci->state.timeplayed += lastmillis-ci->state.lasttimeplayed;
-            setteam(ci, T_NEUTRAL, TT_SMINFO);
-            aiman::dorefresh = max(aiman::dorefresh, G(airefreshdelay));
+            ci->state.updatetimeplayed(false);
+            setteam(ci, T_NEUTRAL, TT_INFO);
+            if(ci->ready) aiman::poke();
         }
         else if(ci->state.state == CS_SPECTATOR && !val)
         {
             if(ci->clientmap[0] || ci->mapcrc) checkmaps();
-            //if(crclocked(ci)) return false;
-            ci->state.cpnodes.shrink(0);
-            ci->state.cpmillis = 0;
+            int nospawn = 0;
+            if(smode && !smode->canspawn(ci, true)) { nospawn++; }
+            mutate(smuts, if(!mut->canspawn(ci, true)) { nospawn++; });
             ci->state.state = CS_DEAD;
-            ci->state.lasttimeplayed = lastmillis;
+            if(nospawn)
+            {
+                spectate(ci, true);
+                return false;
+            }
+            //if(crclocked(ci)) return false;
+            ci->state.lasttimeplayed = totalmillis;
             ci->state.quarantine = false;
             waiting(ci, DROP_RESET);
             if(smode) smode->entergame(ci);
             mutate(smuts, mut->entergame(ci));
-            aiman::dorefresh = max(aiman::dorefresh, G(airefreshdelay));
+            if(ci->ready) aiman::poke();
         }
         return true;
     }
@@ -4154,7 +4404,7 @@ namespace server
                     }
                 }
                 else if(ci->state.lastres[WR_SHOCK]) ci->state.lastres[WR_SHOCK] = ci->state.lastrestime[WR_SHOCK] = 0;
-                if(m_regen(gamemode, mutators) && ci->state.aitype < AI_START)
+                if(m_regen(gamemode, mutators) && ci->state.actortype < A_ENEMY)
                 {
                     int total = m_health(gamemode, mutators, ci->state.model), amt = G(regenhealth),
                         delay = ci->state.lastregen ? G(regentime) : G(regendelay);
@@ -4171,12 +4421,12 @@ namespace server
                                 total = ci->state.health;
                                 low = m_health(gamemode, mutators, ci->state.model);
                             }
-                            int rgn = ci->state.health, heal = clamp(ci->state.health+amt, low, total), eff = heal-rgn;
+                            int heal = clamp(ci->state.health+amt, low, total), eff = heal-ci->state.health;
                             if(eff)
                             {
                                 ci->state.health = heal;
                                 ci->state.lastregen = gamemillis;
-                                sendf(-1, 1, "ri5", N_REGEN, ci->clientnum, ci->state.health, eff, ci->state.armour);
+                                sendf(-1, 1, "ri4", N_REGEN, ci->clientnum, ci->state.health, eff);
                             }
                         }
                     }
@@ -4192,7 +4442,7 @@ namespace server
                     if(ci->state.lastdeath) flushevents(ci, ci->state.lastdeath + DEATHMILLIS);
                     cleartimedevents(ci);
                     ci->state.state = CS_DEAD; // safety
-                    ci->state.respawn(gamemillis, m_health(gamemode, mutators, ci->state.model), m_armour(gamemode, mutators, ci->state.model));
+                    ci->state.respawn(gamemillis);
                     sendspawn(ci);
                 }
             }
@@ -4203,14 +4453,44 @@ namespace server
 
     void serverupdate()
     {
-        loopvrev(connects) if(totalmillis-connects[i]->connectmillis > 15000) disconnect_client(connects[i]->clientnum, DISC_TIMEOUT);
-        loopvrev(control) if(control[i].flag == ipinfo::TEMPORARY && totalmillis-control[i].time > 4*60*60000) control.remove(i);
+        loopvrev(connects) if(totalmillis-connects[i]->connectmillis >= G(connecttimeout))
+        {
+            clientinfo *ci = connects[i];
+            if(ci->connectauth)
+            { // auth might have stalled
+                ci->connectauth = false;
+                ci->authreq = ci->authname[0] = 0;
+                srvmsgftforce(ci->clientnum, CON_EVENT, "\founable to verify, authority request timed out");
+                int disc = auth::allowconnect(ci);
+                if(disc) disconnect_client(ci->clientnum, disc);
+                else
+                {
+                    ci->connectmillis = totalmillis; // in case it doesn't work
+                    connected(ci);
+                }
+            }
+            else disconnect_client(ci->clientnum, DISC_TIMEOUT);
+        }
+        if(G(bantimeout)) loopvrev(control) if(control[i].flag == ipinfo::TEMPORARY)
+        {
+            int timeout = 0;
+            switch(control[i].type)
+            {
+                case ipinfo::ALLOW: timeout = G(allowtimeout); break;
+                case ipinfo::BAN: timeout = G(bantimeout); break;
+                case ipinfo::MUTE: timeout = G(mutetimeout); break;
+                case ipinfo::LIMIT: timeout = G(limittimeout); break;
+                case ipinfo::EXCEPT: timeout = G(excepttimeout); break;
+                default: break;
+            }
+            if(timeout && totalmillis-control[i].time >= timeout) control.remove(i);
+        }
         if(updatecontrols)
         {
             loopvrev(clients)
             {
                 uint ip = getclientip(clients[i]->clientnum);
-                if(ip && !haspriv(clients[i], PRIV_MODERATOR) && checkipinfo(control, ipinfo::BAN, ip) && !checkipinfo(control, ipinfo::ALLOW, ip))
+                if(ip && !haspriv(clients[i], G(banlock)) && checkipinfo(control, ipinfo::BAN, ip) && !checkipinfo(control, ipinfo::EXCEPT, ip))
                 {
                     disconnect_client(clients[i]->clientnum, DISC_IPBAN);
                     continue;
@@ -4218,16 +4498,37 @@ namespace server
                 if(clients[i]->kicked)
                 {
                     disconnect_client(clients[i]->clientnum, DISC_KICK);
+                    continue;
                 }
             }
             updatecontrols = false;
         }
 
+        if(gamestate == G_S_WAITING)
+        {
+            bool ready = false;
+            if(!needswait() || gamewait <= totalmillis) ready = true;
+            else
+            {
+                int numwait = 0;
+                loopv(clients) if(!clients[i]->ready || (G(waitforplayers) == 2 && clients[i]->state.state == CS_SPECTATOR)) numwait++;
+                if(!numwait) ready = true;
+            }
+            if(ready)
+            {
+                gamewait = 0;
+                gamestate = G_S_PLAYING;
+                sendf(-1, 1, "ri3", N_TICK, G_S_PLAYING, timeremaining);
+                if(m_team(gamemode, mutators)) doteambalance(true);
+                if(m_fight(gamemode) && !m_bomber(gamemode) && !m_duke(gamemode, mutators)) // they do their own "fight"
+                    sendf(-1, 1, "ri3s", N_ANNOUNCE, S_V_FIGHT, CON_INFO, "match start, fight!");
+            }
+        }
         if(numclients())
         {
-            if(!paused) gamemillis += curtime;
+            if(canplay(!paused)) gamemillis += curtime;
             if(m_demo(gamemode)) readdemo();
-            else if(!paused && !interm)
+            else if(canplay(!paused))
             {
                 processevents();
                 checkents();
@@ -4236,9 +4537,10 @@ namespace server
                 if(smode) smode->update();
                 mutate(smuts, mut->update());
             }
-            if(interm && totalmillis-interm >= 0) startintermission(true); // wait then call for next map
+            if(gs_intermission(gamestate) && gamewait <= totalmillis) startintermission(true); // wait then call for next map
             if(shouldcheckvotes) checkvotes();
         }
+        else if(G(rotatecycle) && clocktime-lastrotatecycle >= G(rotatecycle)*60) cleanup();
         aiman::checkai();
         auth::update();
     }
@@ -4268,6 +4570,12 @@ namespace server
         {
             if(reason != DISC_SHUTDOWN)
             {
+                aiman::removeai(ci, complete);
+                if(!complete)
+                {
+                    aiman::poke();
+                    swapteam(ci, ci->team);
+                }
                 loopv(clients) if(clients[i] != ci)
                 {
                     loopvk(clients[i]->state.fraglog) if(clients[i]->state.fraglog[k] == ci->clientnum)
@@ -4276,21 +4584,7 @@ namespace server
                 if(ci->privilege) auth::setprivilege(ci, -1);
                 if(smode) smode->leavegame(ci, true);
                 mutate(smuts, mut->leavegame(ci, true));
-                ci->state.timeplayed += lastmillis-ci->state.lasttimeplayed;
-                if(m_teamscore(gamemode) && m_team(gamemode, mutators) && G(teamkillrestore) && !interm && ci->state.aitype == AI_NONE)
-                {
-                    int restorepoints[T_MAX] = {0};
-                    loopv(ci->state.teamkills) restorepoints[ci->state.teamkills[i].team] += ci->state.teamkills[i].points;
-                    loopi(T_MAX) if(restorepoints[i] >= G(teamkillrestore))
-                    {
-                        score &ts = teamscore(i);
-                        ts.total += restorepoints[i];
-                        sendf(-1, 1, "ri3", N_SCORE, ts.team, ts.total);
-                    }
-                }
                 savescore(ci);
-                aiman::removeai(ci, complete);
-                if(!complete) aiman::dorefresh = max(aiman::dorefresh, G(airefreshdelay));
             }
             sendf(-1, 1, "ri3", N_DISCONNECT, n, reason);
             ci->connected = false;
@@ -4306,19 +4600,42 @@ namespace server
         else shouldcheckvotes = true;
     }
 
+    int lastquerysort = 0;
+    static int querysort(const clientinfo *a, const clientinfo *b)
+    {
+        if(a->state.points > b->state.points) return -1;
+        if(a->state.points < b->state.points) return 1;
+        return strcmp(a->name, b->name);
+    }
+    vector<clientinfo *> queryplayers;
+
     void queryreply(ucharbuf &req, ucharbuf &p)
     {
         if(!getint(req)) return;
-        putint(p, numclients());
-        putint(p, 8);                   // number of attrs following
-        putint(p, GAMEVERSION);         // 1
-        putint(p, gamemode);            // 2
-        putint(p, mutators);            // 3
-        putint(p, timeremaining);       // 4
+        if(!lastquerysort || totalmillis-lastquerysort >= G(queryinterval))
+        {
+            queryplayers.setsize(0);
+            loopv(clients) if(clients[i]->clientnum >= 0 && clients[i]->name[0] && clients[i]->state.actortype == A_PLAYER) queryplayers.add(clients[i]);
+            queryplayers.sort(querysort);
+            lastquerysort = totalmillis;
+        }
+        putint(p, queryplayers.length());
+        putint(p, 15); // number of attrs following
+        putint(p, VERSION_GAME); // 1
+        putint(p, gamemode); // 2
+        putint(p, mutators); // 3
+        putint(p, timeremaining); // 4
         putint(p, G(serverclients)); // 5
-        putint(p, serverpass[0] ? MM_PASSWORD : (m_local(gamemode) ? MM_PRIVATE : mastermode)); // 6
+        putint(p, serverpass[0] || G(connectlock) ? MM_PASSWORD : (m_local(gamemode) ? MM_PRIVATE : mastermode)); // 6
         putint(p, numgamevars); // 7
         putint(p, numgamemods); // 8
+        putint(p, VERSION_MAJOR); // 9
+        putint(p, VERSION_MINOR); // 10
+        putint(p, VERSION_PATCH); // 11
+        putint(p, versionplatform); // 12
+        putint(p, versionarch); // 13
+        putint(p, gamestate); // 14
+        putint(p, timeleft()); // 15
         sendstring(smapname, p);
         if(*G(serverdesc)) sendstring(G(serverdesc), p);
         else
@@ -4331,8 +4648,11 @@ namespace server
             sendstring(cname, p);
             #endif
         }
-        loopv(clients) if(clients[i]->clientnum >= 0 && clients[i]->name[0] && clients[i]->state.aitype == AI_NONE)
-            sendstring(colourname(clients[i]), p);
+        if(!queryplayers.empty())
+        {
+            loopv(queryplayers) sendstring(colourname(queryplayers[i]), p);
+            loopv(queryplayers) sendstring(queryplayers[i]->handle, p);
+        }
         sendqueryreply(p);
     }
 
@@ -4371,30 +4691,59 @@ namespace server
             return false;
         }
         mapdata[n]->write(data, len);
-        return n == 2;
+        return n == SENDMAP_PNG;
     }
 
-    int checktype(int type, clientinfo *ci)
+    static struct msgfilter
     {
-        if(ci)
+        uchar msgmask[NUMMSG];
+
+        msgfilter(int msg, ...)
         {
-            if(!ci->connected) return type == (ci->connectauth ? N_AUTHANS : N_CONNECT) || type == N_PING ? type : -1;
-            if(ci->local) return type;
+            memset(msgmask, 0, sizeof(msgmask));
+            va_list msgs;
+            va_start(msgs, msg);
+            for(uchar val = 1; msg < NUMMSG; msg = va_arg(msgs, int))
+            {
+                if(msg < 0) val = uchar(-msg);
+                else msgmask[msg] = val;
+            }
+            va_end(msgs);
         }
-        // only allow edit messages in coop-edit mode
-        if(type >= N_EDITENT && type <= N_NEWMAP && (!m_edit(gamemode) || !ci || ci->state.state == CS_SPECTATOR)) return -1;
-        // server only messages
-        static const int servtypes[] = { N_SERVERINIT, N_CLIENTINIT, N_WELCOME, N_NEWGAME, N_MAPCHANGE, N_SERVMSG, N_DAMAGE, N_SHOTFX, N_DIED, N_POINTS, N_SPAWNSTATE, N_ITEMACC, N_ITEMSPAWN, N_TICK, N_DISCONNECT, N_CURRENTPRIV, N_PONG, N_RESUME, N_SCORE, N_ANNOUNCE, N_SENDDEMOLIST, N_SENDDEMO, N_DEMOPLAYBACK, N_REGEN, N_CLIENT, N_AUTHCHAL };
+
+        uchar operator[](int msg) const { return msg >= 0 && msg < NUMMSG ? msgmask[msg] : 0; }
+    } msgfilter(-1, N_CONNECT, N_SERVERINIT, N_CLIENTINIT, N_WELCOME, N_MAPCHANGE, N_SERVMSG, N_DAMAGE, N_SHOTFX, N_LOADW, N_DIED, N_POINTS, N_SPAWNSTATE, N_ITEMACC, N_ITEMSPAWN, N_TICK, N_DISCONNECT, N_CURRENTPRIV, N_PONG, N_RESUME, N_SCOREAFFIN, N_SCORE, N_ANNOUNCE, N_SENDDEMOLIST, N_SENDDEMO, N_DEMOPLAYBACK, N_REGEN, N_CLIENT, N_AUTHCHAL, -2, N_REMIP, N_NEWMAP, N_CLIPBOARD, -3, N_EDITENT, N_EDITLINK, N_EDITVAR, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, N [...]
+      connectfilter(-1, N_CONNECT, -2, N_AUTHANS, -3, N_PING, NUMMSG);
+
+    int checktype(int type, clientinfo *ci)
+    {
         if(ci)
         {
-            loopi(sizeof(servtypes)/sizeof(int)) if(type == servtypes[i]) return -1;
-            if(type < N_EDITENT || type > N_NEWMAP || !m_edit(gamemode) || !ci || ci->state.state != CS_EDITING)
+            if(!ci->connected) switch(connectfilter[type])
             {
-                static const int exempt[] = { N_POS, N_SPAWN, N_DESTROY };
-                loopi(sizeof(exempt)/sizeof(int)) if(type == exempt[i]) return type;
-                if(++ci->overflow >= 250) return -2;
+                // allow only before authconnect
+                case 1: return !ci->connectauth ? type : -1;
+                // allow only during authconnect
+                case 2: return ci->connectauth ? type : -1;
+                // always allow
+                case 3: return type;
+                // never allow
+                default: return -1;
             }
+            if(ci->local) return type;
+        }
+        switch(msgfilter[type])
+        {
+            // server-only messages
+            case 1: return ci ? -1 : type;
+            // only allowed in coop-edit
+            case 2: if(m_edit(gamemode) && ci && ci->state.state == CS_EDITING) break; return -1;
+            // only allowed in coop-edit, no overflow check
+            case 3: return m_edit(gamemode) && ci && ci->state.state == CS_EDITING ? type : -1;
+            // no overflow check
+            case 4: return type;
         }
+        if(ci && !haspriv(ci, G(overflowlock)) && ++ci->overflow >= G(overflowsize)) return -2;
         return type;
     }
 
@@ -4437,7 +4786,7 @@ namespace server
         loopv(clients)
         {
             clientinfo &ci = *clients[i];
-            if(ci.state.aitype != AI_NONE) continue;
+            if(ci.state.actortype != A_PLAYER) continue;
             uchar *data = wsbuf.buf;
             int size = wslen;
             if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; }
@@ -4471,7 +4820,7 @@ namespace server
         loopv(clients)
         {
             clientinfo &ci = *clients[i];
-            if(ci.state.aitype != AI_NONE) continue;
+            if(ci.state.actortype != A_PLAYER) continue;
             uchar *data = wsbuf.buf;
             int size = wslen;
             if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; }
@@ -4523,7 +4872,7 @@ namespace server
         loopv(clients)
         {
             clientinfo &ci = *clients[i];
-            if(ci.state.aitype != AI_NONE) continue;
+            if(ci.state.actortype != A_PLAYER) continue;
             addposition(ws, wsbuf, mtu, ci, ci);
             loopvj(ci.bots) addposition(ws, wsbuf, mtu, *ci.bots[j], ci);
         }
@@ -4531,7 +4880,7 @@ namespace server
         loopv(clients)
         {
             clientinfo &ci = *clients[i];
-            if(ci.state.aitype != AI_NONE) continue;
+            if(ci.state.actortype != A_PLAYER) continue;
             addmessages(ws, wsbuf, mtu, ci, ci);
             loopvj(ci.bots) addmessages(ws, wsbuf, mtu, *ci.bots[j], ci);
         }
@@ -4577,35 +4926,29 @@ namespace server
 
         ci->connected = true;
         ci->needclipboard = totalmillis ? totalmillis : 1;
-        ci->state.lasttimeplayed = lastmillis;
+        ci->state.lasttimeplayed = totalmillis;
 
-        sendwelcome(ci);
-        if(m_teamscore(gamemode) && m_team(gamemode, mutators) && G(teamkillrestore) && !interm && ci->state.aitype == AI_NONE)
+        if(ci->handle[0]) // kick old logins
         {
-            int restorepoints[T_MAX] = {0};
-            loopv(ci->state.teamkills) restorepoints[ci->state.teamkills[i].team] += ci->state.teamkills[i].points;
-            loopi(T_MAX) if(restorepoints[i] >= G(teamkillrestore))
-            {
-                score &ts = teamscore(i);
-                ts.total -= restorepoints[i];
-                sendf(-1, 1, "ri3", N_SCORE, ts.team, ts.total);
-            }
+            loopvrev(clients) if(clients[i] != ci && clients[i]->handle[0] && !strcmp(clients[i]->handle, ci->handle))
+                disconnect_client(clients[i]->clientnum, DISC_KICK);
         }
+        sendwelcome(ci);
         if(restorescore(ci)) sendresume(ci);
         sendinitclient(ci);
         int amt = numclients();
-        if(ci->privilege > PRIV_NONE)
+        if((ci->privilege&PRIV_TYPE) > PRIV_NONE)
         {
-            if(ci->handle[0]) relayf(2, "\fg%s (%s) has joined the game (\fs\fy%s\fS: \fs\fc%s\fS) (%d %s)", colourname(ci), gethostname(ci->clientnum), privname(ci->privilege), ci->handle, amt, amt != 1 ? "players" : "player");
-            else relayf(2, "\fg%s (%s) has joined the game (\fs\fylocal %s\fS) (%d %s)", colourname(ci), gethostname(ci->clientnum), privname(ci->privilege), amt, amt != 1 ? "players" : "player");
+            if(ci->handle[0]) relayf(2, "\fg%s (%s) has joined the game (\fs\fy%s\fS: \fs\fc%s\fS) [%d.%d.%d-%s%d] (%d %s)", colourname(ci), gethostname(ci->clientnum), privname(ci->privilege), ci->handle, ci->state.version.major, ci->state.version.minor, ci->state.version.patch, plat_name(ci->state.version.platform), ci->state.version.arch, amt, amt != 1 ? "players" : "player");
+            else relayf(2, "\fg%s (%s) has joined the game (\fs\fy%s\fS) [%d.%d.%d-%s%d] (%d %s)", colourname(ci), gethostname(ci->clientnum), privname(ci->privilege), ci->state.version.major, ci->state.version.minor, ci->state.version.patch, plat_name(ci->state.version.platform), ci->state.version.arch, amt, amt != 1 ? "players" : "player");
         }
-        else relayf(2, "\fg%s (%s) has joined the game (%d %s)", colourname(ci), gethostname(ci->clientnum), amt, amt != 1 ? "players" : "player");
+        else relayf(2, "\fg%s (%s) has joined the game [%d.%d.%d-%s%d] (%d %s)", colourname(ci), gethostname(ci->clientnum), ci->state.version.major, ci->state.version.minor, ci->state.version.patch, plat_name(ci->state.version.platform), ci->state.version.arch, amt, amt != 1 ? "players" : "player");
 
         if(m_demo(gamemode)) setupdemoplayback();
         else if(m_edit(gamemode))
         {
             ci->ready = true;
-            aiman::dorefresh = max(aiman::dorefresh, G(airefreshdelay));
+            aiman::poke();
         }
     }
 
@@ -4618,52 +4961,73 @@ namespace server
         if(ci && !ci->connected)
         {
             if(chan==0) return;
-            else if(chan!=1) { disconnect_client(sender, DISC_MSGERR); return; }
-            else while(p.length() < p.maxlen) switch(checktype(getint(p), ci))
+            else if(chan!=1)
             {
-                case N_CONNECT:
+                conoutf("\fy[msg error] from: %d, chan: %d while connecting", sender, chan);
+                disconnect_client(sender, DISC_MSGERR);
+                return;
+            }
+            else while(p.length() < p.maxlen)
+            {
+                int curtype = getint(p);
+                prevtype = type;
+                switch(type = checktype(curtype, ci))
                 {
-                    getstring(text, p);
-                    filtertext(text, text, true, true, true, MAXNAMELEN);
-                    const char *namestr = text;
-                    while(*namestr && iscubespace(*namestr)) namestr++;
-                    if(!*namestr) namestr = copystring(text, "unnamed");
-                    copystring(ci->name, namestr, MAXNAMELEN+1);
-                    ci->state.colour = max(getint(p), 0);
-                    ci->state.model = max(getint(p), 0);
-                    getstring(text, p);
-                    ci->state.setvanity(text);
-
-                    string password = "", authname = "";
-                    getstring(text, p); copystring(password, text);
-                    getstring(text, p); copystring(authname, text);
-                    int disc = auth::allowconnect(ci, true, password, authname);
-                    if(disc)
+                    case N_CONNECT:
                     {
-                        disconnect_client(sender, disc);
-                        return;
-                    }
+                        getstring(text, p);
+                        string namestr = "";
+                        filterstring(namestr, text, true, true, true, true, MAXNAMELEN);
+                        if(!*namestr) copystring(namestr, "unnamed");
+                        copystring(ci->name, namestr, MAXNAMELEN+1);
+                        ci->state.colour = max(getint(p), 0);
+                        ci->state.model = max(getint(p), 0);
+                        getstring(text, p);
+                        ci->state.setvanity(text);
+                        int lw = getint(p);
+                        ci->state.loadweap.shrink(0);
+                        loopk(lw)
+                        {
+                            if(k >= W_LOADOUT) getint(p);
+                            else ci->state.loadweap.add(getint(p));
+                        }
 
-                    if(!ci->connectauth) connected(ci);
+                        string password = "", authname = "";
+                        getstring(password, p);
+                        getstring(text, p);
+                        filterstring(authname, text, true, true, true, true, 100);
 
-                    break;
-                }
+                        ci->state.version.get(p);
 
-                case N_AUTHANS:
-                {
-                    uint id = (uint)getint(p);
-                    getstring(text, p);
-                    auth::answerchallenge(ci, id, text);
-                    break;
-                }
+                        int disc = auth::allowconnect(ci, authname, password);
+                        if(disc)
+                        {
+                            disconnect_client(sender, disc);
+                            return;
+                        }
 
-                case N_PING:
-                    getint(p);
-                    break;
+                        if(!ci->connectauth) connected(ci);
 
-                default:
-                    disconnect_client(sender, DISC_MSGERR);
-                    return;
+                        break;
+                    }
+
+                    case N_AUTHANS:
+                    {
+                        uint id = (uint)getint(p);
+                        getstring(text, p);
+                        if(!auth::answerchallenge(ci, id, text)) auth::authfailed(ci->authreq);
+                        break;
+                    }
+
+                    case N_PING:
+                        getint(p);
+                        break;
+
+                    default:
+                        conoutf("\fy[msg error] from: %d, cur: %d, msg: %d, prev: %d", sender, curtype, type, prevtype);
+                        disconnect_client(sender, DISC_MSGERR);
+                        return;
+                }
             }
             return;
         }
@@ -4671,6 +5035,7 @@ namespace server
         {
             if(receivefile(sender, p.buf, p.maxlen))
             {
+                mapcrc = mapdata[0]->getcrc();
                 mapsending = false;
                 sendf(-1, 1, "ri", N_SENDMAP);
             }
@@ -4767,37 +5132,43 @@ namespace server
                 {
                     int lcn = getint(p), idx = getint(p);
                     if(idx >= SPHY_SERVER) break; // clients can't send this
-                    if(idx == SPHY_POWER) getint(p);
+                    if(idx == SPHY_COOK) loopj(2) getint(p);
                     clientinfo *cp = (clientinfo *)getinfo(lcn);
                     if(!hasclient(cp, ci)) break;
-                    if(idx == SPHY_POWER)
+                    if(idx == SPHY_COOK)
                     {
-                        gamestate &gs = cp->state;
-                        if(gs.weapstate[gs.weapselect] == W_S_RELOAD && !gs.weapwaited(gs.weapselect, gamemillis))
+                        if(!cp->state.isalive(gamemillis))
+                        {
+                            if(G(serverdebug)) srvmsgf(cp->clientnum, "sync error: power [%d] failed - unexpected message", cp->state.weapselect);
+                            break;
+                        }
+                        if(cp->state.weapstate[cp->state.weapselect] == W_S_RELOAD && !cp->state.weapwaited(cp->state.weapselect, gamemillis))
                         {
-                            if(!gs.weapwaited(gs.weapselect, gamemillis, (1<<W_S_RELOAD)))
+                            if(!cp->state.weapwaited(cp->state.weapselect, gamemillis, (1<<W_S_RELOAD)))
                             {
-                                if(!gs.hasweap(gs.weapselect, m_weapon(gamemode, mutators))) gs.entid[gs.weapselect] = -1; // its gone..
-                                if(G(serverdebug)) srvmsgf(cp->clientnum, "sync error: power [%d] failed - current state disallows it", gs.weapselect);
+                                if(!cp->state.hasweap(cp->state.weapselect, m_weapon(gamemode, mutators))) cp->state.entid[cp->state.weapselect] = -1; // its gone..
+                                if(G(serverdebug)) srvmsgf(cp->clientnum, "sync error: power [%d] failed - current state disallows it", cp->state.weapselect);
                                 break;
                             }
-                            else if(gs.weapload[gs.weapselect] > 0)
+                            else if(cp->state.weapload[cp->state.weapselect] > 0)
                             {
-                                takeammo(cp, gs.weapselect, gs.weapload[gs.weapselect]);
-                                gs.reloads[gs.weapselect] = max(gs.reloads[gs.weapselect]-1, 0);
-                                gs.weapload[gs.weapselect] = -gs.weapload[gs.weapselect];
-                                sendf(-1, 1, "ri6", N_RELOAD, cp->clientnum, gs.weapselect, gs.weapload[gs.weapselect], gs.ammo[gs.weapselect], gs.reloads[gs.weapselect]);
+                                takeammo(cp, cp->state.weapselect, cp->state.weapload[cp->state.weapselect]);
+                                cp->state.weapload[cp->state.weapselect] = -cp->state.weapload[cp->state.weapselect];
+                                sendf(-1, 1, "ri5x", N_RELOAD, cp->clientnum, cp->state.weapselect, cp->state.weapload[cp->state.weapselect], cp->state.ammo[cp->state.weapselect], cp->clientnum);
                             }
                             else break;
                         }
                     }
                     else if(idx == SPHY_EXTINGUISH)
                     {
-                        if(cp->state.burning(gamemillis, G(burntime))) cp->state.lastres[WR_BURN] = cp->state.lastrestime[WR_BURN] = 0;
-                        else break; // don't propogate
+                        if(!cp->state.burning(gamemillis, G(burntime))) break;
+                        cp->state.lastres[WR_BURN] = cp->state.lastrestime[WR_BURN] = 0;
                     }
-                    else if((idx == SPHY_BOOST || idx == SPHY_DASH) && (!cp->state.lastboost || gamemillis-cp->state.lastboost > G(impulsedelay)))
+                    else if(idx == SPHY_BOOST || idx == SPHY_DASH)
+                    {
+                        if(!cp->state.isalive(gamemillis) || (cp->state.lastboost && gamemillis-cp->state.lastboost <= G(impulseboostdelay))) break;
                         cp->state.lastboost = gamemillis;
+                    }
                     QUEUE_MSG;
                     break;
                 }
@@ -4805,9 +5176,10 @@ namespace server
                 case N_EDITMODE:
                 {
                     int val = getint(p);
-                    if(!ci || ci->state.aitype > AI_NONE) break;
-                    if(!allowstate(ci, val ? ALST_EDIT : ALST_WALK) && !haspriv(ci, G(editlock), val ? "enter editmode" : "exit editmode"))
+                    if(!ci || ci->state.actortype > A_PLAYER) break;
+                    if(!allowstate(ci, val ? ALST_EDIT : ALST_WALK, G(editlock)))
                     {
+                        if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: unable to switch state %s - %d [%d, %d]", colourname(ci), ci->state.state, ci->state.lastdeath, gamemillis);
                         spectator(ci);
                         break;
                     }
@@ -4837,7 +5209,7 @@ namespace server
                     if(!ci->ready)
                     {
                         ci->ready = true;
-                        aiman::dorefresh = max(aiman::dorefresh, G(airefreshdelay));
+                        aiman::poke();
                     }
                     if(strcmp(text, smapname))
                     {
@@ -4871,25 +5243,20 @@ namespace server
                         checkmaps();
                     }
                     #endif
-                    if(!allowstate(cp, ALST_TRY)) break;
-                    if(smode) smode->canspawn(cp, true);
-                    mutate(smuts, mut->canspawn(cp, true));
-                    cp->state.state = CS_DEAD;
-                    waiting(cp, DROP_RESET);
-                    break;
-                }
-
-                case N_LOADW:
-                {
-                    int lcn = getint(p), r = getint(p), n = getint(p);
-                    clientinfo *cp = (clientinfo *)getinfo(lcn);
-                    vector<int> items;
-                    loopk(n) items.add(getint(p));
-                    if(!hasclient(cp, ci) || !m_loadout(gamemode, mutators)) break;
-                    cp->state.loadweap.shrink(0);
-                    loopvk(items) cp->state.loadweap.add(items[k]);
-                    if(chkloadweap(cp) && r && cp->state.state == CS_ALIVE)
-                        waiting(cp, DROP_WEAPONS);
+                    if(!allowstate(cp, ALST_TRY, m_edit(gamemode) ? G(spawneditlock) : G(spawnlock)))
+                    {
+                        if(G(serverdebug)) srvmsgf(cp->clientnum, "sync error: unable to spawn %s - %d [%d, %d]", colourname(cp), cp->state.state, cp->state.lastdeath, gamemillis);
+                        spectator(cp);
+                        break;
+                    }
+                    int nospawn = 0;
+                    if(smode && !smode->canspawn(cp, true)) { nospawn++; }
+                    mutate(smuts, if(!mut->canspawn(cp, true)) { nospawn++; });
+                    if(!nospawn)
+                    {
+                        cp->state.state = CS_DEAD;
+                        waiting(cp, DROP_RESET);
+                    }
                     break;
                 }
 
@@ -4910,7 +5277,13 @@ namespace server
                 {
                     int lcn = getint(p);
                     clientinfo *cp = (clientinfo *)getinfo(lcn);
-                    if(!hasclient(cp, ci) || !allowstate(cp, ALST_SPAWN)) break;
+                    if(!hasclient(cp, ci)) break;
+                    if(!allowstate(cp, ALST_SPAWN))
+                    {
+                        if(G(serverdebug)) srvmsgf(cp->clientnum, "sync error: unable to spawn %s - %d [%d, %d]", colourname(cp), cp->state.state, cp->state.lastdeath, gamemillis);
+                        spectator(cp);
+                        break;
+                    }
                     cp->state.lastrespawn = -1;
                     cp->state.state = CS_ALIVE;
                     if(smode) smode->spawned(cp);
@@ -4948,7 +5321,7 @@ namespace server
                     if(!isweap(ev->weap)) havecn = false;
                     else
                     {
-                        ev->scale = clamp(ev->scale, 0, W2(ev->weap, power, WS(ev->flags)));
+                        ev->scale = clamp(ev->scale, 0, W2(ev->weap, cooktime, WS(ev->flags)));
                         if(havecn) ev->millis = cp->getmillis(gamemillis, ev->id);
                     }
                     loopk(3) ev->from[k] = getint(p);
@@ -4956,24 +5329,29 @@ namespace server
                     loopj(ev->num)
                     {
                         if(p.overread()) break;
-                        if(j >= 100) { loopk(3) getint(p); continue; }
+                        if(j >= MAXPARAMS || !havecn)
+                        {
+                            loopk(4) getint(p);
+                            continue;
+                        }
                         shotmsg &s = ev->shots.add();
                         s.id = getint(p);
                         loopk(3) s.pos[k] = getint(p);
                     }
                     if(havecn)
                     {
-                        int rays = W2(ev->weap, rays, WS(ev->flags));
-                        if(rays > 1 && W2(ev->weap, power, WS(ev->flags))) rays = int(ceilf(rays*ev->scale/float(W2(ev->weap, power, WS(ev->flags)))));
+                        int rays = min(W2(ev->weap, rays, WS(ev->flags)), MAXPARAMS);
+                        if(rays > 1 && W2(ev->weap, cooktime, WS(ev->flags))) rays = int(ceilf(rays*ev->scale/float(W2(ev->weap, cooktime, WS(ev->flags)))));
                         while(ev->shots.length() > rays) ev->shots.remove(rnd(ev->shots.length()));
                         cp->addevent(ev);
+                        cp->state.lastshoot = gamemillis;
                     }
                     else delete ev;
                     break;
                 }
 
                 case N_DROP:
-                { // gee this looks familiar
+                {
                     int lcn = getint(p), id = getint(p), weap = getint(p);
                     clientinfo *cp = (clientinfo *)getinfo(lcn);
                     if(!hasclient(cp, ci)) break;
@@ -4998,7 +5376,7 @@ namespace server
                     break;
                 }
 
-                case N_DESTROY: // cn millis weap flags id radial hits
+                case N_DESTROY:
                 {
                     int lcn = getint(p), millis = getint(p);
                     clientinfo *cp = (clientinfo *)getinfo(lcn);
@@ -5021,13 +5399,14 @@ namespace server
                         hit.target = getint(p);
                         hit.dist = max(getint(p), 0);
                         loopk(3) hit.dir[k] = getint(p);
+                        loopk(3) hit.vel[k] = getint(p);
                     }
                     if(havecn) cp->events.add(ev);
                     else delete ev;
                     break;
                 }
 
-                case N_STICKY: // cn millis weap flags id target norm pos
+                case N_STICKY:
                 {
                     int lcn = getint(p), millis = getint(p);
                     clientinfo *cp = (clientinfo *)getinfo(lcn);
@@ -5067,19 +5446,20 @@ namespace server
                     {
                         if(sents[ent].type == CHECKPOINT)
                         {
-                            if((m_gauntlet(gamemode) && cp->team != T_ALPHA) || cp->state.cpnodes.find(ent) >= 0) break;
                             if(sents[ent].attrs[5] && sents[ent].attrs[5] != triggerid) break;
                             if(!m_check(sents[ent].attrs[3], sents[ent].attrs[4], gamemode, mutators)) break;
-                            if(m_checkpoint(gamemode)) switch(sents[ent].attrs[6])
+                            if(!m_race(gamemode) || (m_gsp3(gamemode, mutators) && cp->team != T_ALPHA)) break;
+                            if(cp->state.cpnodes.find(ent) >= 0) break;
+                            switch(sents[ent].attrs[6])
                             {
                                 case CP_LAST: case CP_FINISH:
                                 {
                                     if(cp->state.cpmillis)
                                     {
-                                        int laptime = gamemillis-cp->state.cpmillis;
+                                        int laptime = gamemillis-cp->state.cpmillis, total = 0;
                                         if(cp->state.cptime <= 0 || laptime < cp->state.cptime) cp->state.cptime = laptime;
-                                        cp->state.cplaps++;
-                                        sendf(-1, 1, "ri6", N_CHECKPOINT, cp->clientnum, ent, laptime, cp->state.cptime, cp->state.cplaps);
+                                        cp->state.points++;
+                                        sendf(-1, 1, "ri6", N_CHECKPOINT, cp->clientnum, ent, laptime, cp->state.cptime, cp->state.points);
                                         if(m_team(gamemode, mutators))
                                         {
                                             if(m_laptime(gamemode, mutators))
@@ -5087,28 +5467,60 @@ namespace server
                                                 score &ts = teamscore(cp->team);
                                                 if(!ts.total || ts.total > cp->state.cptime)
                                                 {
-                                                    ts.total = cp->state.cptime;
+                                                    total = ts.total = cp->state.cptime;
                                                     sendf(-1, 1, "ri3", N_SCORE, ts.team, ts.total);
                                                 }
                                             }
                                             else
                                             {
                                                 score &ts = teamscore(cp->team);
-                                                ts.total++;
+                                                total = ++ts.total;
                                                 sendf(-1, 1, "ri3", N_SCORE, ts.team, ts.total);
                                             }
+                                            if(total && m_gsp3(gamemode, mutators) && G(racegauntletwinner))
+                                            {
+                                                int numt = numteams(gamemode, mutators);
+                                                if(curbalance == numt-1)
+                                                {
+                                                    bool found = false;
+                                                    loopi(numt)
+                                                    {
+                                                        int t = i+T_FIRST, s = teamscore(t).total;
+                                                        if(t != T_OMEGA && (m_laptime(gamemode, mutators) ? s <= total : s >= total))
+                                                        {
+                                                            found = true;
+                                                            break;
+                                                        }
+                                                    }
+                                                    if(!found)
+                                                    {
+                                                        ancmsgft(-1, S_V_NOTIFY, CON_EVENT, "\fybest score has been reached");
+                                                        startintermission();
+                                                    }
+                                                }
+                                            }
                                         }
                                     }
+                                    else waiting(cp);
                                     cp->state.cpmillis = 0;
                                     cp->state.cpnodes.shrink(0);
-                                    if(sents[ent].attrs[6] == CP_FINISH) waiting(cp); // so they start again
+                                    if(sents[ent].attrs[6] == CP_FINISH) waiting(cp);
                                     break;
                                 }
-                                case CP_RESPAWN: case CP_START:
+                                case CP_START: case CP_RESPAWN:
                                 {
-                                    if(m_gauntlet(gamemode) || cp->state.cpnodes.find(ent) >= 0) break;
+                                    if(cp->state.cpnodes.find(ent) >= 0) break;
+                                    if(sents[ent].attrs[6] == CP_START)
+                                    {
+                                        if(cp->state.cpmillis) break;
+                                        cp->state.cpmillis = gamemillis;
+                                    }
+                                    else if(!cp->state.cpmillis)
+                                    {
+                                        waiting(cp);
+                                        break;
+                                    }
                                     sendf(-1, 1, "ri4", N_CHECKPOINT, cp->clientnum, ent, -1);
-                                    if(!cp->state.cpmillis || sents[ent].attrs[6] != CP_RESPAWN) cp->state.cpmillis = gamemillis;
                                     cp->state.cpnodes.add(ent);
                                 }
                                 default: break;
@@ -5144,9 +5556,6 @@ namespace server
                                 {
                                     if(sents[ent].spawned) break;
                                     sents[ent].spawned = true;
-#ifdef CAMPAIGN
-                                    if(m_campaign(gamemode)) startintermission();
-#endif
                                 }
                             }
                             if(commit) sendf(-1, 1, "ri3x", N_TRIGGER, ent, sents[ent].spawned ? 1 : 0, cp->clientnum);
@@ -5165,62 +5574,94 @@ namespace server
 
                 case N_TEXT:
                 {
-                    int lcn = getint(p), flags = getint(p);
+                    int fcn = getint(p), tcn = getint(p), flags = getint(p);
                     getstring(text, p);
-                    clientinfo *cp = (clientinfo *)getinfo(lcn);
-                    if(!hasclient(cp, ci)) break;
-                    uint ip = getclientip(cp->clientnum);
-                    if(!ip || !checkipinfo(control, ipinfo::ALLOW, ip))
-                    {
-                        if(!haspriv(ci, G(messagelock), "send messages on this server")) break;
-                        if(ip && checkipinfo(control, ipinfo::MUTE, ip) && !haspriv(cp, G(mutelock), "send messages while muted")) break;
-                    }
+                    clientinfo *fcp = (clientinfo *)getinfo(fcn);
+                    clientinfo *tcp = (clientinfo *)getinfo(tcn);
+                    if(!hasclient(fcp, ci)) break;
+                    if(!haspriv(fcp, G(messagelock), "send messages on this server")) break;
+                    uint ip = getclientip(fcp->clientnum);
+                    if(ip && checkipinfo(control, ipinfo::MUTE, ip) && !checkipinfo(control, ipinfo::EXCEPT, ip) && !haspriv(fcp, G(mutelock), "send messages while muted")) break;
                     if(G(floodlock))
                     {
                         int numlines = 0;
-                        loopvrev(cp->state.chatmillis)
+                        loopvrev(fcp->state.chatmillis)
                         {
-                            if(totalmillis-cp->state.chatmillis[i] <= G(floodtime)) numlines++;
-                            else cp->state.chatmillis.remove(i);
+                            if(totalmillis-fcp->state.chatmillis[i] <= G(floodtime)) numlines++;
+                            else fcp->state.chatmillis.remove(i);
                         }
                         if(numlines >= G(floodlines))
                         {
-                            if((!cp->state.warnings[WARN_CHAT][1] || totalmillis-cp->state.warnings[WARN_CHAT][1] >= 1000) && !haspriv(cp, G(floodlock), "send too many messages consecutively"))
+                            if((!fcp->state.warnings[WARN_CHAT][1] || totalmillis-fcp->state.warnings[WARN_CHAT][1] >= 1000) && !haspriv(fcp, G(floodlock), "send too many messages consecutively"))
                             {
-                                cp->state.warnings[WARN_CHAT][0]++;
-                                cp->state.warnings[WARN_CHAT][1] = totalmillis ? totalmillis : 1;
-                                if(ip && G(floodmute) && cp->state.warnings[WARN_CHAT][0] >= G(floodmute) && !checkipinfo(control, ipinfo::ALLOW, ip) && !haspriv(cp, G(mutelock)))
+                                fcp->state.warnings[WARN_CHAT][0]++;
+                                fcp->state.warnings[WARN_CHAT][1] = totalmillis ? totalmillis : 1;
+                                if(ip && G(floodmute) && fcp->state.warnings[WARN_CHAT][0] >= G(floodmute) && !checkipinfo(control, ipinfo::EXCEPT, ip) && !haspriv(fcp, G(mutelock)))
                                 {
                                     ipinfo &c = control.add();
                                     c.ip = ip;
                                     c.mask = 0xFFFFFFFF;
                                     c.type = ipinfo::MUTE;
                                     c.time = totalmillis ? totalmillis : 1;
-                                    srvoutf(-3, "\fs\fcmute\fS added on %s: exceeded the number of allowed flood warnings", colourname(cp));
+                                    c.reason = newstring("exceeded the number of allowed flood warnings");
+                                    srvoutf(-3, "\fs\fcmute\fS added on %s: %s", colourname(fcp), c.reason);
                                 }
                             }
                             break;
                         }
-                        cp->state.chatmillis.add(totalmillis ? totalmillis : 1);
+                        fcp->state.chatmillis.add(totalmillis ? totalmillis : 1);
                     }
-                    string output;
-                    copystring(output, text, G(messagelength));
+                    bigstring output;
+                    copybigstring(output, text, G(messagelength));
+                    filterstring(text, text, true, true, true, true, G(messagelength));
+                    if(*(G(censorwords))) filterword(output, G(censorwords));
                     if(flags&SAY_TEAM && !m_team(gamemode, mutators)) flags &= ~SAY_TEAM;
-                    sendf(-1, -1, "ri3s", N_TEXT, cp->clientnum, flags, output); // sent to negative chan for recordpacket
-                    loopv(clients)
+                    sendf(-1, -1, "ri4s", N_TEXT, fcp->clientnum, tcp ? tcp->clientnum : -1, flags, output); // sent to negative chan for recordpacket
+                    if(flags&SAY_WHISPER && tcp)
                     {
-                        clientinfo *t = clients[i];
-                        if(t == cp || !allowbroadcast(t->clientnum) || (flags&SAY_TEAM && cp->team != t->team)) continue;
-                        sendf(t->clientnum, 1, "ri3s", N_TEXT, cp->clientnum, flags, output);
+                        int scn = allowbroadcast(tcp->clientnum) ? tcp->clientnum : tcp->state.ownernum;
+                        if(allowbroadcast(scn)) sendf(scn, 1, "ri4s", N_TEXT, fcp->clientnum, tcp->clientnum, flags, output);
+                        if(allowbroadcast(fcp->clientnum) && scn != fcp->clientnum)
+                            sendf(fcp->clientnum, 1, "ri4s", N_TEXT, fcp->clientnum, tcp->clientnum, flags, output);
                     }
-                    defformatstring(m)("%s", colourname(cp));
-                    if(flags&SAY_TEAM)
+                    else
                     {
-                        defformatstring(t)(" (to team %s)", colourteam(cp->team));
-                        concatstring(m, t);
+                        static vector<int> sentto;
+                        sentto.setsize(0);
+                        loopv(clients)
+                        {
+                            clientinfo *t = clients[i];
+                            if(flags&SAY_TEAM && fcp->team != t->team) continue;
+                            int scn = t->clientnum;
+                            if(!allowbroadcast(scn) && t->state.ownernum >= 0)
+                            {
+                                if(strncmp(text, "bots", 4))
+                                {
+                                    size_t len = strlen(t->name);
+                                    if(!len || strncasecmp(text, t->name, len)) continue;
+                                    switch(text[len])
+                                    {
+                                        case 0: break;
+                                        case ':': case ',': case ';': len++; break;
+                                        default: continue;
+                                    }
+                                    if(text[len] != 0) continue;
+                                }
+                                scn = t->state.ownernum;
+                            }
+                            if(!allowbroadcast(scn) || sentto.find(scn) >= 0) continue;
+                            sendf(scn, 1, "ri4s", N_TEXT, fcp->clientnum, tcp ? tcp->clientnum : -1, flags, output);
+                            sentto.add(scn);
+                        }
+                        defformatstring(m)("%s", colourname(fcp));
+                        if(flags&SAY_TEAM)
+                        {
+                            defformatstring(t)(" (to team %s)", colourteam(fcp->team));
+                            concatstring(m, t);
+                        }
+                        if(flags&SAY_ACTION) relayf(0, "\fv* %s %s", m, output);
+                        else relayf(0, "\fw<%s> %s", m, output);
                     }
-                    if(flags&SAY_ACTION) relayf(0, "\fv* %s %s", m, output);
-                    else relayf(0, "\fw<%s> %s", m, output);
                     break;
                 }
 
@@ -5240,13 +5681,30 @@ namespace server
 
                 case N_SETPLAYERINFO:
                 {
+                    uint ip = getclientip(ci->clientnum);
+                    if(ci->lastplayerinfo)
+                    {
+                        bool allow = true;
+                        if(!haspriv(ci, G(setinfolock), "change player info on this server")) allow = false;
+                        else if(ip && checkipinfo(control, ipinfo::MUTE, ip) && !checkipinfo(control, ipinfo::EXCEPT, ip) && !haspriv(ci, G(mutelock), "change player info while muted")) allow = false;
+                        else if(totalmillis-ci->lastplayerinfo < G(setinfowait)) allow = false;
+                        if(!allow)
+                        {
+                            getstring(text, p);
+                            loopk(2) getint(p);
+                            getstring(text, p);
+                            int w = getint(p);
+                            loopk(w) getint(p);
+                            sendinitclientself(ci);
+                            break;
+                        }
+                    }
                     QUEUE_MSG;
                     defformatstring(oldname)("%s", colourname(ci));
                     getstring(text, p);
-                    filtertext(text, text, true, true, true, MAXNAMELEN);
-                    const char *namestr = text;
-                    while(*namestr && iscubespace(*namestr)) namestr++;
-                    if(!*namestr) namestr = copystring(text, "unnamed");
+                    string namestr = "";
+                    filterstring(namestr, text, true, true, true, true, MAXNAMELEN);
+                    if(!*namestr) copystring(namestr, "unnamed");
                     if(strcmp(ci->name, namestr))
                     {
                         copystring(ci->name, namestr, MAXNAMELEN+1);
@@ -5256,36 +5714,62 @@ namespace server
                     ci->state.model = max(getint(p), 0);
                     getstring(text, p);
                     ci->state.setvanity(text);
+                    ci->state.loadweap.shrink(0);
+                    int lw = getint(p);
+                    vector<int> lweaps;
+                    loopk(lw)
+                    {
+                        if(k >= W_LOADOUT) getint(p);
+                        else ci->state.loadweap.add(getint(p));
+                    }
+                    ci->lastplayerinfo = totalmillis;
                     QUEUE_STR(ci->name);
                     QUEUE_INT(ci->state.colour);
                     QUEUE_INT(ci->state.model);
                     QUEUE_STR(ci->state.vanity);
+                    QUEUE_INT(ci->state.loadweap.length());
+                    loopvk(ci->state.loadweap) QUEUE_INT(ci->state.loadweap[k]);
                     break;
                 }
 
                 case N_SWITCHTEAM:
                 {
                     int team = getint(p);
-                    if(!allowteam(ci, team, T_FIRST)) team = chooseteam(ci);
-                    if(!m_team(gamemode, mutators) || ci->state.aitype >= AI_START || team == ci->team) break;
+                    if(!m_team(gamemode, mutators) || ci->state.actortype >= A_ENEMY || !isteam(gamemode, mutators, team, T_FIRST)) break;
+                    if(team == ci->team)
+                    {
+                        if(ci->swapteam)
+                        {
+                            if(m_swapteam(gamemode, mutators))
+                                srvmsgft(-1, CON_EVENT, "\fy%s no longer wishes to swap to team %s", colourname(ci), colourteam(ci->swapteam));
+                            ci->swapteam = T_NEUTRAL;
+                        }
+                        break;
+                    }
                     uint ip = getclientip(ci->clientnum);
-                    if(ip && checkipinfo(control, ipinfo::LIMIT, ip) && !checkipinfo(control, ipinfo::ALLOW, ip) && !haspriv(ci, G(limitlock), "change teams while limited")) break;
+                    if(ip && checkipinfo(control, ipinfo::LIMIT, ip) && !checkipinfo(control, ipinfo::EXCEPT, ip) && !haspriv(ci, G(limitlock), "change teams while limited")) break;
+                    int newteam = requestswap(ci, team);
+                    if(newteam != team || newteam == ci->team) break;
                     bool reset = true;
                     if(ci->state.state == CS_SPECTATOR)
                     {
-                        if(!allowstate(ci, ALST_TRY) && !haspriv(ci, G(speclock), "exit spectator"))
+                        if(!allowstate(ci, ALST_TRY, m_edit(gamemode) ? G(spawneditlock) : G(spawnlock)))
+                        {
+                            if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: unable to spawn %s - %d [%d, %d]", colourname(ci), ci->state.state, ci->state.lastdeath, gamemillis);
+                            spectator(ci);
                             break;
+                        }
                         if(!spectate(ci, false)) break;
                         reset = false;
                     }
-                    setteam(ci, team, (reset ? TT_RESET : 0)|TT_SMINFO);
+                    setteam(ci, newteam, (reset ? TT_RESET : 0)|TT_INFOSM);
                     break;
                 }
 
                 case N_MAPVOTE:
                 {
                     getstring(text, p);
-                    filtertext(text, text);
+                    filterstring(text, text);
                     const char *s = text;
                     if(!strncasecmp(s, "maps/", 5) || !strncasecmp(s, "maps\\", 5)) s += 5;
                     int reqmode = getint(p), reqmuts = getint(p);
@@ -5309,6 +5793,7 @@ namespace server
                     int n;
                     while((n = getint(p)) != -1)
                     {
+                        if(p.overread()) break;
                         getstring(text, p);
                         defformatstring(cmdname)("sv_%s", text);
                         ident *id = idents.access(cmdname);
@@ -5340,7 +5825,7 @@ namespace server
                                     id->changed();
                                     break;
                                 }
-                                default: return;
+                                default: break;
                             }
                         }
                         else switch(n)
@@ -5363,13 +5848,27 @@ namespace server
                             sents[n].spawned = false; // wait a bit then load 'em up
                             sents[n].millis = gamemillis;
                             sents[n].attrs.add(0, clamp(numattr, type >= 0 && type < MAXENTTYPES ? enttype[type].numattrs : 0, MAXENTATTRS));
-                            loopk(numattr) { if(p.overread()) break; int attr = getint(p); if(sents[n].attrs.inrange(k)) sents[n].attrs[k] = attr; }
-                            if(enttype[type].syncpos) loopj(3) { if(p.overread()) break; sents[n].o[j] = getint(p)/DMF; }
+                            loopk(numattr)
+                            {
+                                if(p.overread()) break;
+                                int attr = getint(p);
+                                if(sents[n].attrs.inrange(k)) sents[n].attrs[k] = attr;
+                            }
+                            if(enttype[type].syncpos) loopj(3)
+                            {
+                                if(p.overread()) break;
+                                sents[n].o[j] = getint(p)/DMF;
+                            }
                             if(enttype[type].synckin)
                             {
                                 int numkin = getint(p);
                                 sents[n].kin.add(0, clamp(numkin, 0, MAXENTKIN));
-                                loopk(numkin) { if(p.overread()) break; int kin = getint(p); if(sents[n].kin.inrange(k)) sents[n].kin[k] = kin; }
+                                loopk(numkin)
+                                {
+                                    if(p.overread()) break;
+                                    int kin = getint(p);
+                                    if(k < MAXENTKIN && sents[n].kin.inrange(k)) sents[n].kin[k] = kin;
+                                }
                             }
                         }
                         else
@@ -5437,15 +5936,6 @@ namespace server
                     break;
                 }
 
-                case N_SCOREAFFIN:
-                {
-                    int lcn = getint(p), relay = getint(p), goal = getint(p);
-                    clientinfo *cp = (clientinfo *)getinfo(lcn);
-                    if(!hasclient(cp, ci) || cp->state.state == CS_SPECTATOR) break;
-                    if(smode==&bombermode) bombermode.scoreaffinity(cp, relay, goal);
-                    break;
-                }
-
                 case N_DROPAFFIN:
                 {
                     int lcn = getint(p), tcn = getint(p);
@@ -5500,6 +5990,7 @@ namespace server
                                     allow.mask = 0xFFFFFFFF;
                                     allow.type = ipinfo::ALLOW;
                                     allow.time = totalmillis ? totalmillis : 1;
+                                    allow.reason = newstring("mastermode set private");
                                 }
                             }
                             srvoutf(-3, "\fymastermode is now \fs\fc%d\fS (\fs\fc%s\fS)", mastermode, mastermodename(mastermode));
@@ -5529,6 +6020,7 @@ namespace server
                         CONTROLSWITCH(ipinfo::BAN, ban);
                         CONTROLSWITCH(ipinfo::MUTE, mute);
                         CONTROLSWITCH(ipinfo::LIMIT, limit);
+                        CONTROLSWITCH(ipinfo::EXCEPT, except);
                         default: break;
                     }
                     #undef CONTROLSWITCH
@@ -5537,22 +6029,22 @@ namespace server
 
                 case N_ADDCONTROL:
                 {
-                    int victim = getint(p), value = getint(p);
+                    int m = getint(p), value = getint(p);
                     getstring(text, p);
                     #define CONTROLSWITCH(x,y) \
                         case x: \
                         { \
-                            if(haspriv(ci, G(y##lock), #y " people") && victim >= 0) \
+                            if(haspriv(ci, G(y##lock), #y " players") && m >= 0) \
                             { \
-                                clientinfo *cp = (clientinfo *)getinfo(victim); \
-                                if(!cp || cp->state.ownernum >= 0 || (value != ipinfo::ALLOW && !cmppriv(ci, cp, #y))) break; \
+                                clientinfo *cp = (clientinfo *)getinfo(m); \
+                                if(!cp || cp->state.ownernum >= 0 || (value != ipinfo::EXCEPT && !cmppriv(ci, cp, #y))) break; \
                                 uint ip = getclientip(cp->clientnum); \
                                 if(!ip) break; \
-                                if(checkipinfo(control, ipinfo::ALLOW, ip)) \
+                                if(checkipinfo(control, ipinfo::EXCEPT, ip)) \
                                 { \
-                                    if(!haspriv(ci, PRIV_ADMINISTRATOR, #y " protected people")) break; \
+                                    if(!haspriv(ci, PRIV_ADMINISTRATOR, #y " protected players")) break; \
                                     else if(value >= ipinfo::BAN) loopvrev(control) \
-                                        if(control[i].type == ipinfo::ALLOW && (ip & control[i].mask) == control[i].ip) \
+                                        if(control[i].type == ipinfo::EXCEPT && (ip & control[i].mask) == control[i].ip) \
                                             control.remove(i); \
                                 } \
                                 string name; \
@@ -5564,14 +6056,16 @@ namespace server
                                     c.mask = 0xFFFFFFFF; \
                                     c.type = value; \
                                     c.time = totalmillis ? totalmillis : 1; \
-                                    if(text[0]) srvoutf(-3, "\fP%s added \fs\fc" #y "\fS on %s (%s): %s", name, colourname(cp), gethostname(cp->clientnum), text); \
-                                    else srvoutf(-3, "\fP%s added \fs\fc" #y "\fS on %s", name, colourname(cp)); \
+                                    c.reason = newstring(text); \
+                                    if(text[0]) srvoutf(-3, "%s added \fs\fc" #y "\fS on %s (%s/%s): %s", name, colourname(cp), gethostname(cp->clientnum), gethostip(cp->clientnum), text); \
+                                    else srvoutf(-3, "%s added \fs\fc" #y "\fS on %s", name, colourname(cp)); \
                                     if(value == ipinfo::BAN) updatecontrols = true; \
+                                    else if(value == ipinfo::LIMIT) cp->swapteam = 0; \
                                 } \
                                 else \
                                 { \
-                                    if(text[0]) srvoutf(-3, "\fP%s \fs\fckicked\fS %s: %s", name, colourname(cp), text); \
-                                    else srvoutf(-3, "\fP%s \fs\fckicked\fS %s", name, colourname(cp)); \
+                                    if(text[0]) srvoutf(-3, "%s \fs\fckicked\fS %s: %s", name, colourname(cp), text); \
+                                    else srvoutf(-3, "%s \fs\fckicked\fS %s", name, colourname(cp)); \
                                     cp->kicked = updatecontrols = true; \
                                 } \
                             } \
@@ -5584,6 +6078,7 @@ namespace server
                         CONTROLSWITCH(ipinfo::BAN, ban);
                         CONTROLSWITCH(ipinfo::MUTE, mute);
                         CONTROLSWITCH(ipinfo::LIMIT, limit);
+                        CONTROLSWITCH(ipinfo::EXCEPT, except);
                         default: break;
                     }
                     #undef CONTROLSWITCH
@@ -5594,20 +6089,31 @@ namespace server
                 {
                     int sn = getint(p), val = getint(p);
                     clientinfo *cp = (clientinfo *)getinfo(sn);
-                    if(!cp || cp->state.aitype > AI_NONE || (val ? cp->state.state == CS_SPECTATOR : cp->state.state != CS_SPECTATOR)) break;
-                    if((sn != sender || !allowstate(cp, val ? ALST_SPEC : ALST_TRY)) && !haspriv(ci, G(speclock), sn != sender ? "control other players" : (val ? "enter spectator" : "exit spectator")))
+                    if(!cp || cp->state.actortype > A_PLAYER || (val ? cp->state.state == CS_SPECTATOR : cp->state.state != CS_SPECTATOR))
+                    {
+                        if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: unable to modify spectator %s - %d [%d, %d] - invalid", colourname(cp), cp->state.state, cp->state.lastdeath, gamemillis);
+                        break;
+                    }
+                    if(sn != sender ? !haspriv(ci, max(m_edit(gamemode) ? G(spawneditlock) : G(spawnlock), G(speclock)), "control other players") : !allowstate(cp, val ? ALST_SPEC : ALST_TRY, m_edit(gamemode) ? G(spawneditlock) : G(spawnlock)))
+                    {
+                        if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: unable to modify spectator %s - %d [%d, %d] - restricted", colourname(cp), cp->state.state, cp->state.lastdeath, gamemillis);
                         break;
-                    bool spec = val != 0, quarantine = cp != ci && val != 0 && val == 2, wasq = cp->state.quarantine;
-                    spectate(cp, spec, quarantine);
+                    }
+                    bool spec = val != 0, quarantine = cp != ci && val == 2, wasq = cp->state.quarantine;
+                    if(!spectate(cp, spec, quarantine))
+                    {
+                        if(G(serverdebug)) srvmsgf(ci->clientnum, "sync error: unable to modify spectator %s - %d [%d, %d] - failed", colourname(cp), cp->state.state, cp->state.lastdeath, gamemillis);
+                        break;
+                    }
                     if(quarantine && cp->state.quarantine)
                     {
                         defformatstring(name)("%s", colourname(ci));
-                        srvoutf(-3, "\fP%s \fs\fcquarantined\fS %s", name, colourname(cp));
+                        srvoutf(-3, "%s \fs\fcquarantined\fS %s", name, colourname(cp));
                     }
                     else if(wasq && !cp->state.quarantine)
                     {
                         defformatstring(name)("%s", colourname(ci));
-                        srvoutf(-3, "\fP%s \fs\fcreleased\fS %s from \fs\fcquarantine\fS", name, colourname(cp));
+                        srvoutf(-3, "%s \fs\fcreleased\fS %s from \fs\fcquarantine\fS", name, colourname(cp));
                     }
                     break;
                 }
@@ -5615,11 +6121,11 @@ namespace server
                 case N_SETTEAM:
                 {
                     int who = getint(p), team = getint(p);
-                    if(who<0 || who>=getnumclients() || !haspriv(ci, G(speclock), "change the team of others")) break;
                     clientinfo *cp = (clientinfo *)getinfo(who);
-                    if(!cp || cp == ci || !m_team(gamemode, mutators) || m_local(gamemode) || cp->state.aitype >= AI_START) break;
-                    if(cp->state.state == CS_SPECTATOR || !allowteam(cp, team, T_FIRST)) break;
-                    setteam(cp, team, TT_DFINFO);
+                    if(!cp || !m_team(gamemode, mutators) || m_local(gamemode) || cp->state.actortype >= A_ENEMY) break;
+                    if(who < 0 || who >= getnumclients() || !haspriv(ci, G(teamlock), "change the team of others")) break;
+                    if(cp->state.state == CS_SPECTATOR || !allowteam(cp, team, T_FIRST, false)) break;
+                    setteam(cp, team, TT_RESETX);
                     break;
                 }
 
@@ -5635,7 +6141,7 @@ namespace server
                 {
                     if(!haspriv(ci, G(demolock), "stop demos")) break;
                     if(m_demo(gamemode)) enddemoplayback();
-                    else checkdemorecord(interm != 0);
+                    else checkdemorecord(!gs_playing(gamestate));
                     break;
                 }
 
@@ -5648,36 +6154,47 @@ namespace server
                 }
 
                 case N_LISTDEMOS:
-                    //if(ci->state.state==CS_SPECTATOR) break;
                     listdemos(sender);
                     break;
 
                 case N_GETDEMO:
                 {
                     int n = getint(p);
-                    //if(ci->state.state==CS_SPECTATOR) break;
                     senddemo(sender, n);
                     break;
                 }
 
                 case N_EDITENT:
                 {
-                    int n = getint(p), oldtype = NOTUSED;
-                    bool tweaked = false;
+                    int n = getint(p), oldtype = NOTUSED, newtype = NOTUSED;
+                    bool tweaked = false, inrange = n < MAXENTS;
                     loopk(3) getint(p);
+                    if(p.overread()) break;
                     if(sents.inrange(n)) oldtype = sents[n].type;
-                    else while(sents.length() <= n) sents.add();
-                    if((sents[n].type = getint(p)) != oldtype) tweaked = true;
-                    int numattrs = getint(p);
-                    while(sents[n].attrs.length() < max(5, numattrs)) sents[n].attrs.add(0);
-                    loopk(numattrs) sents[n].attrs[k] = getint(p);
-                    if(oldtype == PLAYERSTART || sents[n].type == PLAYERSTART) setupspawns(true);
-                    hasgameinfo = true;
-                    QUEUE_MSG;
-                    if(tweaked && enttype[sents[n].type].usetype != EU_NONE)
+                    else if(inrange) while(sents.length() <= n) sents.add();
+                    if((newtype = getint(p)) != oldtype && inrange)
+                    {
+                        sents[n].type = newtype;
+                        tweaked = true;
+                    }
+                    int numattrs = getint(p), realattrs =  min(max(5, numattrs), MAXENTATTRS);
+                    if(inrange) while(sents[n].attrs.length() < realattrs) sents[n].attrs.add(0);
+                    loopk(numattrs)
+                    {
+                        int attr = getint(p);
+                        if(p.overread()) break;
+                        if(inrange && k < MAXENTATTRS) sents[n].attrs[k] = attr;
+                    }
+                    if(inrange)
                     {
-                        if(enttype[sents[n].type].usetype == EU_ITEM) setspawn(n, true, true, true);
-                        if(sents[n].type == TRIGGER) setuptriggers(true);
+                        if(oldtype == PLAYERSTART || sents[n].type == PLAYERSTART) setupspawns(true);
+                        hasgameinfo = true;
+                        QUEUE_MSG;
+                        if(tweaked && enttype[sents[n].type].usetype != EU_NONE)
+                        {
+                            if(enttype[sents[n].type].usetype == EU_ITEM) setspawn(n, true, true, true);
+                            if(sents[n].type == TRIGGER) setuptriggers(true);
+                        }
                     }
                     break;
                 }
@@ -5761,6 +6278,7 @@ namespace server
                         else if(best)
                         {
                             loopk(SENDMAP_MAX) if(mapdata[k]) DELETEP(mapdata[k]);
+                            mapcrc = 0;
                             srvmsgft(ci->clientnum, CON_EVENT, "map is being requested, please wait..");
                             sendf(best->clientnum, 1, "ri", N_GETMAP);
                             mapsending = true;
@@ -5778,14 +6296,14 @@ namespace server
                 case N_NEWMAP:
                 {
                     int size = getint(p);
-                    if(ci->state.state == CS_SPECTATOR) break;
+                    if(ci->state.state != CS_EDITING) break;
                     if(size >= 0)
                     {
                         copystring(smapname, "maps/untitled");
                         sents.shrink(0);
                         hasgameinfo = true;
-                        if(smode) smode->reset(true);
-                        mutate(smuts, mut->reset(true));
+                        if(smode) smode->reset();
+                        mutate(smuts, mut->reset());
                     }
                     QUEUE_MSG;
                     break;
@@ -5797,36 +6315,39 @@ namespace server
                     getstring(text, p);
                     if(val != 0)
                     {
-                        if(adminpass[0] && (ci->local || (text[0] && checkpassword(ci, adminpass, text))))
-                            auth::setprivilege(ci, 1, PRIV_ADMINISTRATOR);
-                        else if(ci->privilege <= PRIV_PLAYER)
+                        if(text[0])
+                        {
+                            if(!adminpass[0]) srvmsgft(ci->clientnum, CON_EVENT, "\fraccess denied, no administrator password set");
+                            else if(!checkpassword(ci, adminpass, text)) srvmsgft(ci->clientnum, CON_EVENT, "\fraccess denied, invalid administrator password");
+                            else auth::setprivilege(ci, 1, PRIV_ADMINISTRATOR|PRIV_LOCAL);
+                        }
+                        else if((ci->privilege&PRIV_TYPE) < PRIV_ELEVATED)
                         {
                             bool fail = false;
-                            if(!(mastermask()&MM_AUTOAPPROVE) && !ci->privilege)
+                            if(!(mastermask()&MM_AUTOAPPROVE))
                             {
-                                srvmsgft(ci->clientnum, CON_EVENT, "\fraccess denied, you need \fs\fcmoderator/administrator\fS access to \fs\fcelevate privileges\fS");
+                                srvmsgft(ci->clientnum, CON_EVENT, "\fraccess denied, you need a \fs\fcpassword/account\fS to \fs\fcelevate privileges\fS");
                                 fail = true;
                             }
-                            else loopv(clients) if(ci != clients[i] && clients[i]->privilege >= PRIV_ELEVATED)
+                            else loopv(clients) if(ci != clients[i] && (clients[i]->privilege&PRIV_TYPE) >= PRIV_ELEVATED)
                             {
                                 srvmsgft(ci->clientnum, CON_EVENT, "\fraccess denied, there is already another player with elevated privileges");
                                 fail = true;
                                 break;
                             }
-                            if(!fail) auth::setprivilege(ci, 1, PRIV_ELEVATED);
+                            if(!fail) auth::setprivilege(ci, 1, PRIV_ELEVATED|PRIV_LOCAL);
                         }
                     }
                     else auth::setprivilege(ci, 0);
                     break; // don't broadcast the password
                 }
 
-                case N_ADDBOT: getint(p); break;
-                case N_DELBOT: break;
-
                 case N_AUTHTRY:
                 {
                     getstring(text, p);
-                    auth::tryauth(ci, text);
+                    string authname = "";
+                    filterstring(authname, text, true, true, true, true, 100);
+                    auth::tryauth(ci, authname);
                     break;
                 }
 
@@ -5844,14 +6365,14 @@ namespace server
                     goto genericmsg;
 
                 case N_PASTE:
-                    if(ci->state.state!=CS_SPECTATOR) sendclipboard(ci);
+                    if(ci->state.state == CS_EDITING) sendclipboard(ci);
                     goto genericmsg;
 
                 case N_CLIPBOARD:
                 {
                     int unpacklen = getint(p), packlen = getint(p);
                     ci->cleanclipboard(false);
-                    if(ci->state.state==CS_SPECTATOR)
+                    if(ci->state.state != CS_EDITING)
                     {
                         if(packlen > 0) p.subbuf(packlen);
                         break;
diff --git a/src/game/teamdef.h b/src/game/teamdef.h
new file mode 100644
index 0000000..a5d63e0
--- /dev/null
+++ b/src/game/teamdef.h
@@ -0,0 +1,129 @@
+#ifdef GAMESERVER
+    #define TPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05) \
+        GVAR(flags, teamneutral##name, mn, w00, mx); \
+        GVAR(flags, teamalpha##name, mn, w01, mx); \
+        GVAR(flags, teamomega##name, mn, w02, mx); \
+        GVAR(flags, teamkappa##name, mn, w03, mx); \
+        GVAR(flags, teamsigma##name, mn, w04, mx); \
+        GVAR(flags, teamenemy##name, mn, w05, mx); \
+        int *sv_team_stat_##name[] = { \
+            &sv_teamneutral##name, \
+            &sv_teamalpha##name, \
+            &sv_teamomega##name, \
+            &sv_teamkappa##name, \
+            &sv_teamsigma##name, \
+            &sv_teamenemy##name \
+        };
+
+    #define TPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05) \
+        GFVAR(flags, teamneutral##name, mn, w00, mx); \
+        GFVAR(flags, teamalpha##name, mn, w01, mx); \
+        GFVAR(flags, teamomega##name, mn, w02, mx); \
+        GFVAR(flags, teamkappa##name, mn, w03, mx); \
+        GFVAR(flags, teamsigma##name, mn, w04, mx); \
+        GFVAR(flags, teamenemy##name, mn, w05, mx); \
+        float *sv_team_stat_##name[] = { \
+            &sv_teamneutral##name, \
+            &sv_teamalpha##name, \
+            &sv_teamomega##name, \
+            &sv_teamkappa##name, \
+            &sv_teamsigma##name, \
+            &sv_teamenemy##name \
+        };
+
+    #define TPSVAR(flags, name, w00, w01, w02, w03, w04, w05) \
+        GSVAR(flags, teamneutral##name, w00); \
+        GSVAR(flags, teamalpha##name, w01); \
+        GSVAR(flags, teamomega##name, w02); \
+        GSVAR(flags, teamkappa##name, w03); \
+        GSVAR(flags, teamsigma##name, w04); \
+        GSVAR(flags, teamenemy##name, w05); \
+        char **sv_team_stat_##name[] = { \
+            &sv_teamneutral##name, \
+            &sv_teamalpha##name, \
+            &sv_teamomega##name, \
+            &sv_teamkappa##name, \
+            &sv_teamsigma##name, \
+            &sv_teamenemy##name \
+        };
+
+    #define TEAM(team,name)         (*sv_team_stat_##name[team])
+    #define TEAMSTR(a,team,attr)    defformatstring(a)("sv_%s%s", teamtype[team].name, #attr)
+#else
+#ifdef GAMEWORLD
+    #define TPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05) \
+        GVAR(flags, teamneutral##name, mn, w00, mx); \
+        GVAR(flags, teamalpha##name, mn, w01, mx); \
+        GVAR(flags, teamomega##name, mn, w02, mx); \
+        GVAR(flags, teamkappa##name, mn, w03, mx); \
+        GVAR(flags, teamsigma##name, mn, w04, mx); \
+        GVAR(flags, teamenemy##name, mn, w05, mx); \
+        int *team_stat_##name[] = { \
+            &teamneutral##name, \
+            &teamalpha##name, \
+            &teamomega##name, \
+            &teamkappa##name, \
+            &teamsigma##name, \
+            &teamenemy##name \
+        };
+
+    #define TPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05) \
+        GFVAR(flags, teamneutral##name, mn, w00, mx); \
+        GFVAR(flags, teamalpha##name, mn, w01, mx); \
+        GFVAR(flags, teamomega##name, mn, w02, mx); \
+        GFVAR(flags, teamkappa##name, mn, w03, mx); \
+        GFVAR(flags, teamsigma##name, mn, w04, mx); \
+        GFVAR(flags, teamenemy##name, mn, w05, mx); \
+        float *team_stat_##name[] = { \
+            &teamneutral##name, \
+            &teamalpha##name, \
+            &teamomega##name, \
+            &teamkappa##name, \
+            &teamsigma##name, \
+            &teamenemy##name \
+        };
+
+    #define TPSVAR(flags, name, w00, w01, w02, w03, w04, w05) \
+        GSVAR(flags, teamneutral##name, w00); \
+        GSVAR(flags, teamalpha##name, w01); \
+        GSVAR(flags, teamomega##name, w02); \
+        GSVAR(flags, teamkappa##name, w03); \
+        GSVAR(flags, teamsigma##name, w04); \
+        GSVAR(flags, teamenemy##name, w05); \
+        char **team_stat_##name[] = { \
+            &teamneutral##name, \
+            &teamalpha##name, \
+            &teamomega##name, \
+            &teamkappa##name, \
+            &teamsigma##name, \
+            &teamenemy##name \
+        };
+#else
+    #define TPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05) \
+        GVAR(flags, teamneutral##name, mn, w00, mx); \
+        GVAR(flags, teamalpha##name, mn, w01, mx); \
+        GVAR(flags, teamomega##name, mn, w02, mx); \
+        GVAR(flags, teamkappa##name, mn, w03, mx); \
+        GVAR(flags, teamsigma##name, mn, w04, mx); \
+        GVAR(flags, teamenemy##name, mn, w05, mx); \
+        extern int *team_stat_##name[];
+    #define TPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05) \
+        GFVAR(flags, teamneutral##name, mn, w00, mx); \
+        GFVAR(flags, teamalpha##name, mn, w01, mx); \
+        GFVAR(flags, teamomega##name, mn, w02, mx); \
+        GFVAR(flags, teamkappa##name, mn, w03, mx); \
+        GFVAR(flags, teamsigma##name, mn, w04, mx); \
+        GFVAR(flags, teamenemy##name, mn, w05, mx); \
+        extern float *team_stat_##name[];
+    #define TPSVAR(flags, name, w00, w01, w02, w03, w04, w05) \
+        GSVAR(flags, teamneutral##name, w00); \
+        GSVAR(flags, teamalpha##name, w01); \
+        GSVAR(flags, teamomega##name, w02); \
+        GSVAR(flags, teamkappa##name, w03); \
+        GSVAR(flags, teamsigma##name, w04); \
+        GSVAR(flags, teamenemy##name, w05); \
+        extern char **team_stat_##name[];
+#endif
+    #define TEAM(team,name)         (*team_stat_##name[team])
+    #define TEAMSTR(a,team,attr)    defformatstring(a)("%s%s", teamnames[team], #attr)
+#endif
diff --git a/src/game/vars.h b/src/game/vars.h
index 983f68d..4cee757 100644
--- a/src/game/vars.h
+++ b/src/game/vars.h
@@ -1,6 +1,6 @@
 GVAR(IDF_WORLD, numplayers, 0, 4, MAXCLIENTS); // 0 = determine from number of spawns
-GVAR(IDF_WORLD, maxplayers, 0, 0, MAXCLIENTS); // 0 = numplayers*2
-GVAR(IDF_WORLD, mapbalance, 0, 0, 2); // switches teams for asymmetrical maps, 0 = off, 1 = ctf/df/bb, 2 = all
+GVAR(IDF_WORLD, maxplayers, 0, 0, MAXCLIENTS); // 0 = numplayers*3
+GVAR(IDF_WORLD, mapbalance, 0, 0, 3); // switches teams for asymmetrical maps, 0 = off, 1 = ctf/dnc/bb, 2 = with team spawns, 3 = forced
 
 GFVAR(IDF_WORLD, gravity, 0, 50.f, 1000); // gravity
 GFVAR(0, gravityscale, 0, 1, FVAR_MAX);
@@ -22,41 +22,62 @@ GFVAR(0, liquidextinguishscale, 0, 1, FVAR_MAX);
 
 GVAR(IDF_ADMIN, serverdebug, 0, 0, 3);
 GVAR(IDF_ADMIN, serverclients, 1, 16, MAXCLIENTS);
+GVARF(IDF_ADMIN, serverdupclients, 0, 0, MAXCLIENTS, limitdupclients(), );
 GVAR(IDF_ADMIN, serveropen, 0, 3, 3);
 GSVAR(IDF_ADMIN, serverdesc, "");
 GSVAR(IDF_ADMIN, servermotd, "");
 
 GVAR(IDF_ADMIN, autoadmin, 0, 0, 1);
 
+GVAR(IDF_ADMIN, queryinterval, 0, 5000, VAR_MAX); // rebuild client list for server queries this often
+GVAR(IDF_ADMIN, masterinterval, 300000, 300000, VAR_MAX); // keep connection alive every this often
+GVAR(IDF_ADMIN, connecttimeout, 5000, 15000, VAR_MAX); // disconnected when attempt exceeds this time
+GVAR(IDF_ADMIN, allowtimeout, 0, 3600000, VAR_MAX); // temporary allows last this long
+GVAR(IDF_ADMIN, bantimeout, 0, 14400000, VAR_MAX); // temporary bans last this long
+GVAR(IDF_ADMIN, mutetimeout, 0, 3600000, VAR_MAX); // temporary mutes last this long
+GVAR(IDF_ADMIN, limittimeout, 0, 3600000, VAR_MAX); // temporary limits last this long
+GVAR(IDF_ADMIN, excepttimeout, 0, 3600000, VAR_MAX); // temporary allows last this long
+
 GVAR(IDF_ADMIN, connectlock, 0, PRIV_NONE, PRIV_CREATOR);
 GVAR(IDF_ADMIN, messagelock, 0, PRIV_NONE, PRIV_CREATOR);
-GVAR(IDF_ADMIN, messagelength, 32, 128, MAXSTRLEN-1);
+GVAR(IDF_ADMIN, messagelength, 32, 128, BIGSTRLEN-1);
+GSVAR(IDF_ADMIN, censorwords, "");
+
+GVAR(IDF_ADMIN, setinfolock, 0, PRIV_NONE, PRIV_CREATOR);
+GVAR(IDF_ADMIN, setinfowait, 0, 10000, VAR_MAX);
 
 GVAR(IDF_ADMIN, demolock, 0, PRIV_OPERATOR, PRIV_CREATOR);
-GVAR(IDF_ADMIN, democount, 1, 5, VAR_MAX);
+GVAR(IDF_ADMIN, democount, 1, 10, VAR_MAX);
 GVAR(IDF_ADMIN, demomaxsize, 1, 16, VAR_MAX);
 GVAR(IDF_ADMIN, demoautorec, 0, 1, 1); // 0 = off, 1 = automatically record demos each match
 GVAR(IDF_ADMIN, demokeep, 0, 0, 1); // 0 = off, 1 = keep demos that don't run to end of match
 
 GVAR(IDF_ADMIN, speclock, 0, PRIV_MODERATOR, PRIV_CREATOR);
+GVAR(IDF_ADMIN, teamlock, 0, PRIV_MODERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, kicklock, 0, PRIV_MODERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, allowlock, 0, PRIV_OPERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, banlock, 0, PRIV_OPERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, mutelock, 0, PRIV_MODERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, limitlock, 0, PRIV_MODERATOR, PRIV_CREATOR);
+GVAR(IDF_ADMIN, exceptlock, 0, PRIV_MODERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, vetolock, 0, PRIV_OPERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, editlock, 0, PRIV_OPERATOR, PRIV_CREATOR);
+GVAR(IDF_ADMIN, spawnlock, 0, PRIV_MODERATOR, PRIV_CREATOR); // if locked, require this to spawn
+GVAR(IDF_ADMIN, spawneditlock, 0, PRIV_MODERATOR, PRIV_CREATOR); // if locked in editmode, require this to spawn
 GVAR(IDF_ADMIN, masterlock, 0, PRIV_MODERATOR, PRIV_CREATOR);
 
+GVAR(IDF_ADMIN, overflowlock, 0, PRIV_MODERATOR, PRIV_CREATOR); // normal message queue override
+GVAR(IDF_ADMIN, overflowsize, 0, 255, VAR_MAX); // kick if queued messages >= this
+
 GVAR(IDF_ADMIN, floodlock, 0, PRIV_OPERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, floodmute, 0, 3, VAR_MAX); // automatically mute player when warned this many times
 GVAR(IDF_ADMIN, floodtime, 250, 10000, VAR_MAX); // time span to check for floody messages
 GVAR(IDF_ADMIN, floodlines, 1, 5, VAR_MAX); // number of lines in aforementioned span before too many
 
 GVAR(IDF_ADMIN, teamkilllock, 0, PRIV_OPERATOR, PRIV_CREATOR);
-GVAR(IDF_ADMIN, teamkillwarn, 1, 5, VAR_MAX); // automatically warn player every this many team kills
-GVAR(IDF_ADMIN, teamkillkick, 0, 2, VAR_MAX); // automatically kick player at this many warnings
-GVAR(IDF_ADMIN, teamkillban, 0, 3, VAR_MAX); // automatically ban player at this many warnings
+GVAR(IDF_ADMIN, teamkillwarn, 1, 3, VAR_MAX); // automatically warn player every this many team kills
+GVAR(IDF_ADMIN, teamkillkick, 0, 3, VAR_MAX); // automatically kick player at this many warnings
+GVAR(IDF_ADMIN, teamkillban, 0, 4, VAR_MAX); // automatically ban player at this many warnings
 GVAR(IDF_ADMIN, teamkilltime, 0, 5, VAR_MAX); // time threshold (in minutes) to count
 GVAR(IDF_ADMIN, teamkillrestore, 0, 1, VAR_MAX); // restore the team score as if the offender was never there if it was by this much
 
@@ -67,6 +88,7 @@ GVAR(IDF_ADMIN, resetallowsonend, 0, 1, 2); // reset allows on end (1: just when
 GVAR(IDF_ADMIN, resetbansonend, 0, 1, 2); // reset bans on end (1: just when empty, 2: when matches end)
 GVAR(IDF_ADMIN, resetmutesonend, 0, 1, 2); // reset mutes on end (1: just when empty, 2: when matches end)
 GVAR(IDF_ADMIN, resetlimitsonend, 0, 1, 2); // reset limits on end (1: just when empty, 2: when matches end)
+GVAR(IDF_ADMIN, resetexceptsonend, 0, 1, 2); // reset excepts on end (1: just when empty, 2: when matches end)
 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)
 
@@ -78,11 +100,6 @@ GSVAR(IDF_ADMIN, defaultmap, "");
 GVAR(IDF_ADMIN, defaultmode, G_START, G_DEATHMATCH, G_MAX-1);
 GVAR(IDF_ADMIN, defaultmuts, 0, 0, G_M_ALL);
 
-#ifdef CAMPAIGN
-GVAR(IDF_ADMIN, campaignplayers, 1, 4, MAXPLAYERS);
-GSVAR(IDF_ADMIN, campaignmaps, "");
-#endif
-
 GSVAR(IDF_ADMIN, allowmaps, "untitled");
 
 GSVAR(IDF_ADMIN, mainmaps, "untitled");
@@ -91,21 +108,20 @@ GSVAR(IDF_ADMIN, defendmaps, "untitled");
 GSVAR(IDF_ADMIN, kingmaps, "untitled");
 GSVAR(IDF_ADMIN, bombermaps, "untitled");
 GSVAR(IDF_ADMIN, holdmaps, "untitled");
-GSVAR(IDF_ADMIN, trialmaps, "untitled");
-GSVAR(IDF_ADMIN, gauntletmaps, "untitled");
+GSVAR(IDF_ADMIN, racemaps, "untitled");
 
 GSVAR(IDF_ADMIN, multimaps, "untitled"); // applies to modes which *require* multi spawns (ctf/bb)
 GSVAR(IDF_ADMIN, duelmaps, "untitled");
-GSVAR(IDF_ADMIN, jetpackmaps, "untitled");
 
 GSVAR(IDF_ADMIN, smallmaps, "untitled");
 GSVAR(IDF_ADMIN, mediummaps, "untitled");
 GSVAR(IDF_ADMIN, largemaps, "untitled");
 
 GVAR(IDF_ADMIN, modelock, 0, PRIV_OPERATOR, PRIV_CREATOR);
-GVAR(IDF_ADMIN, modelocktype, 0, 2, 2); // 0 = off, 1 = lock level only, 2 = lock level can set limited mode and higher
+GVAR(IDF_ADMIN, modelocktype, 0, 2, 2); // 0 = off, 1 = only lock level can change modes, 2 = lock level can set limited modes
 GVAR(IDF_ADMIN, modelockfilter, 0, G_LIMIT, G_ALL);
 GVAR(IDF_ADMIN, mutslockfilter, 0, G_M_FILTER, G_M_ALL);
+GVAR(IDF_ADMIN, mutslockforce, 0, 0, G_M_ALL);
 
 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, PRIV_OPERATOR, PRIV_CREATOR);
@@ -120,24 +136,33 @@ GVARF(IDF_ADMIN, rotatemodefilter, 0, G_LIMIT, G_ALL, sv_rotatemodefilter &= ~G_
 GVAR(IDF_ADMIN, rotatemuts, 0, 3, VAR_MAX); // any more than one decreases the chances of it picking
 GVAR(IDF_ADMIN, rotatemutsfilter, 0, G_M_ROTATE, G_M_ALL); // mutators not in this array are filtered out
 GVAR(IDF_ADMIN, rotatemapsfilter, 0, 2, 2); // 0 = off, 1 = filter based on mutators, 2 = also filter based on players
+GVAR(IDF_ADMIN, rotatecycle, 0, 10, VAR_MAX); // 0 = off, else = minutes between forced re-cycles
 
 GVAR(IDF_ADMIN, varslock, 0, PRIV_OPERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, votelock, 0, PRIV_OPERATOR, PRIV_CREATOR);
 GVAR(IDF_ADMIN, votelocktype, 0, 2, 2); // 0 = off, 1 = lock level only, 2 = lock level can select previousmaps
 GVAR(IDF_ADMIN, votewait, 0, 2500, VAR_MAX);
 GVAR(IDF_ADMIN, votestyle, 0, 2, 2); // 0 = votes don't pass mid-match, 1 = passes if votethreshold is met, 2 = passes if unanimous
-GVAR(IDF_ADMIN, voteinterm, 0, 2, 2); // 0 = must wait entire time, 1 = passes if votethreshold is met, 2 = passes if unanimous
+GVAR(IDF_ADMIN, voteinterm, 0, 2, 3); // 0 = must wait entire time, 1 = passes if votethreshold is met, 2 = passes if unanimous, 3 = passes after waiting then selects a random vote
 GFVAR(IDF_ADMIN, votethreshold, 0, 0.5f, 1); // auto-pass votes when this many agree
 
 GVAR(IDF_ADMIN, smallmapmax, 0, 6, VAR_MAX); // maximum number of players for a small map
 GVAR(IDF_ADMIN, mediummapmax, 0, 12, VAR_MAX); // maximum number of players for a medium map
 
-namespace server { extern void resetgamevars(bool flush); }
-GICOMMAND(0, resetvars, "", (), server::resetgamevars(true), );
-GICOMMAND(IDF_ADMIN, resetconfig, "", (), rehash(true), );
+GVAR(IDF_ADMIN, waitforplayers, 0, 2, 2); // wait this long for players, 0 = off, 1 = load the map, 2 = join the game
+GVAR(IDF_ADMIN, waitforplayertime, 1000, 30000, VAR_MAX); // wait this long for players to be ready
 
-GFVAR(0, maxalive, 0, 1, FVAR_MAX); // only allow this*maxplayers to be alive at once
-GVAR(0, maxalivequeue, 0, 1, 1); // if number of players exceeds this amount, use a queue system
+namespace server
+{
+    extern void resetgamevars(bool flush, bool all);
+    extern void savegamevars();
+}
+GICOMMAND(0, resetvars, "", (), server::resetgamevars(true, false); result("success"), );
+GICOMMAND(0, savevars, "", (), server::savegamevars(); result("success"), );
+GICOMMAND(IDF_ADMIN, resetconfig, "", (), rehash(true); result("success"), );
+
+GFVAR(0, maxalive, 0, 0, FVAR_MAX); // only allow this*maxplayers to be alive at once, 0 = turn off maxalive
+GVAR(0, maxalivequeue, 0, 1, 1); // upon triggering maxalive, use a queue system
 GVAR(0, maxaliveminimum, 2, 4, VAR_MAX); // kicks in if alive >= this
 GFVAR(0, maxalivethreshold, 0, 0, FVAR_MAX); // .. or this percentage of clients
 
@@ -145,37 +170,34 @@ GVAR(0, maxcarry, 1, 2, W_LOADOUT);
 GVAR(0, spawnrotate, 0, 2, 2); // 0 = let client decide, 1 = sequence, 2 = random
 GVAR(0, spawnweapon, 0, W_PISTOL, W_MAX-1);
 GVAR(0, instaweapon, 0, W_RIFLE, W_MAX-1);
-GVAR(0, trialweapon, 0, W_MELEE, W_MAX-1);
-GVAR(0, gauntletweapon, 0, W_RIFLE, W_MAX-1);
-GVAR(0, spawngrenades, 0, 0, 2); // 0 = never, 1 = all but insta/trial, 2 = always
-GVAR(0, spawnmines, 0, 0, 2); // 0 = never, 1 = all but insta/trial, 2 = always
+GVAR(0, raceweapon, 0, W_MELEE, W_MAX-1);
+GVAR(0, spawngrenades, 0, 0, 2); // 0 = never, 1 = all but insta/race, 2 = always
+GVAR(0, spawnmines, 0, 0, 2); // 0 = never, 1 = all but insta/race, 2 = always
 GVAR(0, spawndelay, 0, 5000, VAR_MAX); // delay before spawning in most modes
 GVAR(0, instadelay, 0, 3000, VAR_MAX); // .. in instagib matches
-GVAR(0, trialdelay, 0, 500, VAR_MAX); // .. in time trial matches
+GVAR(0, racedelay, 0, 1000, VAR_MAX); // .. in time race matches
+GVAR(0, racedelayex, 0, 3000, VAR_MAX); // .. for defenders in gauntlet time race matches
 GVAR(0, bomberdelay, 0, 3000, VAR_MAX); // delay before spawning in bomber
 GVAR(0, spawnprotect, 0, 3000, VAR_MAX); // delay before damage can be dealt to spawning player
 GVAR(0, duelprotect, 0, 5000, VAR_MAX); // .. in duel/survivor matches
 GVAR(0, instaprotect, 0, 3000, VAR_MAX); // .. in instagib matches
+GVAR(0, protectbreak, 0, 1, 1); // 0 = off, 1 = protection is broken when player starts firing
+GVAR(0, duelaffinity, 0, 1, 2); // 0 = off, 1 = on enter can respawn next iter, 2 = on enter can respawn immediately
+GVAR(0, duelbotcheck, 0, 1, 1); // 0 = off, 1 = skip bots when checking respawns
 
-GVAR(0, balancemaps, -1, -1, 1); // determined if map team balancing is used: -1 = map default, 0 = off, 1 = on
-GVAR(0, balancedelay, 0, 10000, 30000); // before mapbalance forces
-GVAR(0, balancenospawn, 0, 1, 1); // prevent respawning when waiting to balance
-
-#ifdef CAMPAIGN
-GVAR(0, campaignghost, 0, 0, 1); // 0 = all players are solid, 1 = all players are ghosts
-#endif
-GVAR(0, trialghost, 0, 1, 1); // 0 = all players are solid, 1 = all players are ghosts
-GVAR(0, gauntletghost, 0, 2, 2); // 0 = all players are solid, 1 = all players are ghosts, 2 = team mates are ghosts
+GVAR(0, radardisabled, 0, 0, 1); // forces the radar to be off
+GVAR(0, radardistlimit, 0, 0, VAR_MAX); // forces the radar to this distance max, 0 = off
 
-#ifndef MEK
-GVAR(0, spawnhealth, 0, 100, VAR_MAX);
-GVAR(0, spawnarmour, 0, 0, VAR_MAX);
-#endif
+GVAR(0, balancemaps, -1, -1, 3); // determined if map team balancing is used: -1 = map default, 0 = off, 1 = ctf/dnc/bb, 2 = with team spawns, 3 = forced
+GVAR(0, balancereset, 0, 2, 2); // reset players when balancing them, 0 = off, 1 = only when necessary, 2 = always
+GVAR(0, balancedelay, 0, 10000, 30000); // before mapbalance forces
+GVAR(0, balancenospawn, 0, 0, 1); // prevent respawning when waiting to balance
+GVAR(0, balanceduke, 0, 1, 1); // enable in duel/survivor
 
 GFVAR(0, maxhealth, 0, 1.5f, FVAR_MAX);
-GFVAR(0, maxhealthvampire, 0, 2.0f, FVAR_MAX);
+GFVAR(0, maxhealthvampire, 0, 3.0f, FVAR_MAX);
+GFVAR(0, vampirescale, 0, 1.0f, FVAR_MAX);
 
-GFVAR(0, actorscale, FVAR_NONZERO, 1, FVAR_MAX);
 GFVAR(0, maxresizescale, 1, 2, FVAR_MAX);
 GFVAR(0, minresizescale, FVAR_NONZERO, 0.5f, 1);
 GFVAR(0, instaresizeamt, FVAR_NONZERO, 0.1f, 1); // each kill adds this much size in insta-resize
@@ -188,10 +210,11 @@ GVAR(0, bleeddelay, 0, 1000, VAR_MAX);
 GVAR(0, bleeddamage, 0, 3, VAR_MAX);
 GVAR(0, shocktime, 0, 5500, VAR_MAX);
 GVAR(0, shockdelay, 0, 1000, VAR_MAX);
-GVAR(0, shockdamage, 0, 3, VAR_MAX);
+GVAR(0, shockdamage, 0, 2, VAR_MAX);
+GVAR(0, shockstun, 0, W_N_ST, W_N_ALL);
 GFVAR(0, shockstunscale, 0, 0.5f, FVAR_MAX);
-GFVAR(0, shockstunfall, 0, 0.1f, FVAR_MAX);
-GVAR(0, shockstuntime, 0, 1000, VAR_MAX);
+GFVAR(0, shockstunfall, 0, 0.01f, FVAR_MAX);
+GVAR(0, shockstuntime, 0, 500, VAR_MAX);
 
 GVAR(0, regendelay, 0, 3000, VAR_MAX); // regen after no damage for this long
 GVAR(0, regentime, 0, 1000, VAR_MAX); // regen this often when regenerating normally
@@ -199,19 +222,18 @@ GVAR(0, regenhealth, 0, 5, VAR_MAX); // regen this amount each regen
 GVAR(0, regendecay, 0, 3, VAR_MAX); // if over maxhealth, decay this amount each regen
 
 GVAR(0, kamikaze, 0, 1, 3); // 0 = never, 1 = holding grenade, 2 = have grenade, 3 = always
-GVAR(0, itemsallowed, 0, 2, 2); // 0 = never, 1 = all but limited, 2 = always
 GVAR(0, itemspawntime, 1, 15000, VAR_MAX); // when items respawn
-GVAR(0, itemspawndelay, 0, 1000, VAR_MAX); // after map start items first spawn
-GVAR(0, itemspawnstyle, 0, 1, 3); // 0 = all at once, 1 = staggered, 2 = random, 3 = randomise between both
-GFVAR(0, itemthreshold, 0, 2, FVAR_MAX); // if numitems/(players*maxcarry) is less than this, spawn one of this type
+GVAR(0, itemspawndelay, 0, 0, VAR_MAX); // after map start items first spawn
+GVAR(0, itemspawnstyle, 0, 0, 3); // 0 = all at once, 1 = staggered, 2 = random, 3 = randomise between both
 GVAR(0, itemcollide, 0, BOUNCE_GEOM, VAR_MAX);
 GVAR(0, itemextinguish, 0, 6, 7);
 GVAR(0, iteminteracts, 0, 3, 3);
 GFVAR(0, itemelasticity, FVAR_MIN, 0.4f, FVAR_MAX);
 GFVAR(0, itemrelativity, FVAR_MIN, 1, FVAR_MAX);
-GFVAR(0, itemwaterfric, 0, 1.75f, FVAR_MAX);
+GFVAR(0, itemliquidcoast, 0, 1.75f, FVAR_MAX);
 GFVAR(0, itemweight, FVAR_MIN, 150, FVAR_MAX);
-GFVAR(0, itemminspeed, 0, 0, FVAR_MAX);
+GFVAR(0, itemspeedmin, 0, 0, FVAR_MAX);
+GFVAR(0, itemspeedmax, 0, 0, FVAR_MAX);
 GFVAR(0, itemrepulsion, 0, 8, FVAR_MAX);
 GFVAR(0, itemrepelspeed, 0, 25, FVAR_MAX);
 
@@ -222,43 +244,60 @@ GVAR(0, intermlimit, 0, 15000, VAR_MAX); // .. before vote menu comes up
 GVAR(0, votelimit, 0, 45000, VAR_MAX); // .. before vote passes by default
 GVAR(0, duelreset, 0, 1, 1); // reset winner in duel
 GVAR(0, duelclear, 0, 1, 1); // clear items in duel
-GVAR(0, duelwarmup, 0, 15000, VAR_MAX); // .. before duel starts playing initially
-GVAR(0, duellimit, 0, 5000, VAR_MAX); // .. before duel goes to next round
+GVAR(0, duelregen, 0, 0, 1); // allow regen in duel
+GVAR(0, dueldelay, 500, 1000, VAR_MAX); // round continues for this length after winning
+GVAR(0, duelcooloff, 0, 5000, VAR_MAX); // cool off period before duel goes to next round
 GVAR(0, duelcycle, 0, 2, 3); // determines if players are force-cycled after a certain number of wins (bit: 0 = off, 1 = non-team games, 2 = team games)
 GVAR(0, duelcycles, 0, 2, VAR_MAX); // maximum wins in a row before force-cycling (0 = num team/total players)
 
-GVAR(0, selfdamage, 0, 1, 1); // 0 = off, 1 = either hurt self or use teamdamage rules
-GFVAR(0, selfdamagescale, FVAR_MIN, 1, FVAR_MAX); // 0 = off, anything else = scale for damage
-GVAR(0, teamdamage, 0, 1, 2); // 0 = off, 1 = non-bots damage team, 2 = all players damage team
-GFVAR(0, teamdamagescale, FVAR_MIN, 1, FVAR_MAX); // 0 = off, anything else = scale for damage
-GVAR(0, teambalance, 0, 1, 2); // 0 = off, 1 = by number then rank, 2 = by rank then number
-GVAR(0, teampersist, 0, 1, 2); // 0 = off, 1 = only attempt, 2 = forced
 GVAR(0, pointlimit, 0, 0, VAR_MAX); // finish when score is this or more
+GVAR(0, teampersist, 0, 1, 2); // 0 = off, 1 = only attempt, 2 = forced
+GVAR(0, damageself, 0, 1, 1); // 0 = off, 1 = either hurt self or use damageteam rules
+GFVAR(0, damageselfscale, FVAR_MIN, 1, FVAR_MAX); // 0 = off, anything else = scale for damage
+GVAR(0, damageteam, 0, 1, 2); // 0 = off, 1 = non-bots damage team, 2 = all players damage team
+GFVAR(0, damageteamscale, FVAR_MIN, 1, FVAR_MAX); // 0 = off, anything else = scale for damage
+
+GVAR(0, teambalance, 0, 5, 6); // 0 = off, 1 = by number then skill, 2 = by skill then number, 3 = by number and enforce, 4 = number, enforce, reassign, 5 = skill, number, enforce, reassign, 6 = skill during waiting, revert to 4 otherwise
+GVAR(0, teambalanceduel, 0, 0, 1); // allow reassignments in duel
+GVAR(0, teambalanceplaying, 2, 2, VAR_MAX); // min players before reassignments occur
+GVAR(0, teambalanceamt, 2, 2, VAR_MAX); // max-min offset before reassignments occur
+GVAR(0, teambalancewait, 10000, 60000, VAR_MAX); // how long before can happen again
+GVAR(0, teambalancedelay, 2000, 15000, VAR_MAX); // how long before reassignments start
+GVAR(0, teambalanceswap, 0, 1, 1); // allow swap requests if unable to change team
+GVAR(0, teambalancelock, 0, PRIV_PLAYER, PRIV_CREATOR); // level at which one can override swap and automatically reassign a lower player
+GVAR(0, teambalancestyle, 0, 4, 4); // when moving players, sort by: 0 = top of list, 1 = lowest time played, 2 = lowest points, 3 = lowest frags, 4 = lowest skill
+
+GVAR(0, racegauntletwinner, 0, 1, 1); // declare the winner when the final team exceeds best score
 
 GVAR(0, capturelimit, 0, 0, VAR_MAX); // finish when score is this or more
+GVAR(0, captureresetstore, 0, 2, 15);
 GVAR(0, captureresetdelay, 0, 30000, VAR_MAX);
 GVAR(0, capturedefenddelay, 0, 15000, VAR_MAX);
 GVAR(0, captureprotectdelay, 0, 15000, VAR_MAX);
-GVAR(0, capturepickupdelay, -1, 5000, VAR_MAX);
+GVAR(0, capturepickupdelay, 500, 2500, VAR_MAX);
+GVAR(0, captureteampenalty, -1, 7500, VAR_MAX);
+GVAR(0, captureresetpenalty, -1, 3500, VAR_MAX);
 GFVAR(0, capturecarryspeed, 0, 0.9f, FVAR_MAX);
+GFVAR(0, capturedropheight, 0, 8, FVAR_MAX);
 GVAR(0, capturepoints, 0, 5, VAR_MAX); // points added to score
 GVAR(0, capturepickuppoints, 0, 3, VAR_MAX); // points added to score
 GVAR(0, capturecollide, 0, BOUNCE_GEOM, VAR_MAX);
 GVAR(0, captureextinguish, 0, 6, 7);
 GVAR(0, captureinteracts, 0, 3, 3);
-GFVAR(0, capturerelativity, 0, 0.25f, FVAR_MAX);
-GFVAR(0, captureelasticity, FVAR_MIN, 0.5f, FVAR_MAX);
-GFVAR(0, capturewaterfric, FVAR_MIN, 1.75f, FVAR_MAX);
-GFVAR(0, captureweight, FVAR_MIN, 200, FVAR_MAX);
-GFVAR(0, captureminspeed, 0, 0, FVAR_MAX);
+GFVAR(0, capturerelativity, 0, 0, FVAR_MAX);
+GFVAR(0, captureelasticity, FVAR_MIN, 0.65f, FVAR_MAX);
+GFVAR(0, captureliquidcoast, FVAR_MIN, 1.75f, FVAR_MAX);
+GFVAR(0, captureweight, FVAR_MIN, 400, FVAR_MAX);
+GFVAR(0, capturespeedmin, 0, 0, FVAR_MAX);
+GFVAR(0, capturespeedmax, 0, 100, FVAR_MAX);
 GFVAR(0, capturerepulsion, 0, 16, FVAR_MAX);
 GFVAR(0, capturerepelspeed, 0, 25, FVAR_MAX);
 GFVAR(0, capturethreshold, 0, 0, FVAR_MAX); // if someone 'warps' more than this distance, auto-drop
-GVAR(0, capturebuffing, 0, 5, 15); // buffed; 0 = off, &1 = when guarding/secured, &2 = when carrying enemy, &4 = also defending secured, &8 = also defending enemy carrier
-GVAR(0, capturebuffdelay, 0, 1000, VAR_MAX); // buffed for this long after leaving
-GFVAR(0, capturebuffarea, FVAR_NONZERO, 96, FVAR_MAX); // radius in which buffing occurs
-GFVAR(0, capturebuffdamage, 1, 1.5f, FVAR_MAX); // multiply outgoing damage by this much when buffed
-GFVAR(0, capturebuffshield, 1, 1.5f, FVAR_MAX); // divide incoming damage by this much when buffed
+GVAR(0, capturebuffing, 0, 9, 63); // buffed; 0 = off, &1 = when defending, &2 = when defending dropped, &4 = when secured, &8 = when defending secured, &16 = when secured enemy, &32 = when defending secured enemy
+GVAR(0, capturebuffdelay, 0, 3000, VAR_MAX); // buffed for this long after leaving
+GFVAR(0, capturebuffarea, 0, 128, FVAR_MAX); // radius in which buffing occurs
+GFVAR(0, capturebuffdamage, 1, 1.25f, FVAR_MAX); // multiply outgoing damage by this much when buffed
+GFVAR(0, capturebuffshield, 1, 1.25f, FVAR_MAX); // divide incoming damage by this much when buffed
 GVAR(0, captureregenbuff, 0, 1, 1); // 0 = off, 1 = modify regeneration when buffed
 GVAR(0, captureregendelay, 0, 1000, VAR_MAX); // regen this often when buffed
 GVAR(0, captureregenextra, 0, 2, VAR_MAX); // add this to regen when buffed
@@ -266,25 +305,31 @@ GVAR(0, captureregenextra, 0, 2, VAR_MAX); // add this to regen when buffed
 GVAR(0, defendlimit, 0, 0, VAR_MAX); // finish when score is this or more
 GVAR(0, defendpoints, 0, 1, VAR_MAX); // points added to score
 GVAR(0, defendinterval, 0, 50, VAR_MAX);
+GVAR(0, defendhold, 1, 100, VAR_MAX); // points needed to gain a score point
 GVAR(0, defendoccupy, 1, 100, VAR_MAX); // points needed to occupy in regular games
-GVAR(0, defendking, 1, 25, VAR_MAX); // points needed to occupy in king of the hill
+GVAR(0, defendking, 1, 100, VAR_MAX); // points needed to occupy in king of the hill
 GVAR(0, defendflags, 0, 3, 3); // 0 = init all (neutral), 1 = init neutral and team only, 2 = init team only, 3 = init all (team + neutral + converted)
 GVAR(0, defendbuffing, 0, 1, 7); // buffed; 0 = off, &1 = when guarding, &2 = when securing, &4 = even when enemies are present
 GFVAR(0, defendbuffoccupy, 0, 0.5f, 1); // for defendbuffing&4, must be occupied this much before passing
 GVAR(0, defendbuffdelay, 0, 1000, VAR_MAX); // buffed for this long after leaving
-GFVAR(0, defendbuffarea, FVAR_NONZERO, 64, FVAR_MAX); // radius in which buffing occurs
-GFVAR(0, defendbuffdamage, 1, 1.5f, FVAR_MAX); // multiply outgoing damage by this much when buffed
-GFVAR(0, defendbuffshield, 1, 1.5f, FVAR_MAX); // divide incoming damage by this much when buffed
+GFVAR(0, defendbuffarea, 0, 128, FVAR_MAX); // radius in which buffing occurs
+GFVAR(0, defendbuffdamage, 1, 1.25f, FVAR_MAX); // multiply outgoing damage by this much when buffed
+GFVAR(0, defendbuffshield, 1, 1.25f, FVAR_MAX); // divide incoming damage by this much when buffed
 GVAR(0, defendregenbuff, 0, 1, 1); // 0 = off, 1 = modify regeneration when buffed
 GVAR(0, defendregendelay, 0, 1000, VAR_MAX); // regen this often when buffed
 GVAR(0, defendregenextra, 0, 2, VAR_MAX); // add this to regen when buffed
 
 GVAR(0, bomberlimit, 0, 0, VAR_MAX); // finish when score is this or more (non-hold)
 GVAR(0, bomberholdlimit, 0, 0, VAR_MAX); // finish when score is this or more (hold)
+GVAR(0, bomberbasketonly, 0, 1, 1); // prohibit touchdowns in basket game
+GVAR(0, bomberattackreset, 0, 1, 1); // defenders reset rather than carry the ball
+GVAR(0, bomberattackwinner, 0, 1, 1); // declare the winner when the final team exceeds best score
+GFVAR(0, bomberbasketmindist, 0, 48, FVAR_MAX); // prohibit baskets less than this far away
 GVAR(0, bomberresetdelay, 0, 15000, VAR_MAX);
-GVAR(0, bomberpickupdelay, -1, 5000, VAR_MAX);
+GVAR(0, bomberpickupdelay, 500, 5000, VAR_MAX);
 GVAR(0, bombercarrytime, 0, 15000, VAR_MAX);
 GFVAR(0, bombercarryspeed, 0, 0.9f, FVAR_MAX);
+GFVAR(0, bomberdropheight, 0, 8, FVAR_MAX);
 GVAR(0, bomberpoints, 0, 5, VAR_MAX); // points added to score
 GVAR(0, bomberpenalty, 0, 5, VAR_MAX); // points taken from score
 GVAR(0, bomberpickuppoints, 0, 3, VAR_MAX); // points added to score
@@ -293,42 +338,36 @@ GVAR(0, bomberholdpoints, 0, 1, VAR_MAX); // points added to score
 GVAR(0, bomberholdpenalty, 0, 10, VAR_MAX); // penalty for holding too long
 GVAR(0, bomberholdinterval, 0, 1000, VAR_MAX);
 GVAR(0, bomberlockondelay, 0, 250, VAR_MAX);
-GFVAR(0, bomberspeed, 0, 200, FVAR_MAX);
-GFVAR(0, bomberdelta, 0, 1000, FVAR_MAX);
+GFVAR(0, bomberspeed, 0, 250, FVAR_MAX);
+GFVAR(0, bomberspeeddelta, 0, 1000, FVAR_MAX);
+GFVAR(0, bomberspeedmin, 0, 65, FVAR_MAX);
+GFVAR(0, bomberspeedmax, 0, 200, FVAR_MAX);
 GVAR(0, bombercollide, 0, BOUNCE_GEOM, VAR_MAX);
 GVAR(0, bomberextinguish, 0, 6, 7);
 GVAR(0, bomberinteracts, 0, 3, 3);
 GFVAR(0, bomberrelativity, 0, 0.25f, FVAR_MAX);
-GFVAR(0, bomberelasticity, FVAR_MIN, 0.7f, FVAR_MAX);
-GFVAR(0, bomberwaterfric, FVAR_MIN, 1.75f, FVAR_MAX);
-GFVAR(0, bomberweight, FVAR_MIN, 200, FVAR_MAX);
-GFVAR(0, bomberminspeed, 0, 50, FVAR_MAX);
+GFVAR(0, bomberelasticity, FVAR_MIN, 0.75f, FVAR_MAX);
+GFVAR(0, bomberliquidcoast, FVAR_MIN, 1.75f, FVAR_MAX);
+GFVAR(0, bomberweight, FVAR_MIN, 350, FVAR_MAX);
 GFVAR(0, bomberthreshold, 0, 0, FVAR_MAX); // if someone 'warps' more than this distance, auto-drop
-GVAR(0, bomberbuffing, 0, 1, 3); // buffed; 0 = off, &1 = when guarding, &2 = when securing
-GVAR(0, bomberbuffdelay, 0, 1000, VAR_MAX); // buffed for this long after leaving
-GFVAR(0, bomberbuffarea, FVAR_NONZERO, 128, FVAR_MAX); // multiply affinity radius by this much for buff
-GFVAR(0, bomberbuffdamage, 1, 1.5f, FVAR_MAX); // multiply outgoing damage by this much when buffed
-GFVAR(0, bomberbuffshield, 1, 1.5f, FVAR_MAX); // divide incoming damage by this much when buffed
+GVAR(0, bomberbuffing, 0, 1, 7); // buffed; 0 = off, &1 = when guarding, &2 = when securing, &4 = when secured as defender in attack
+GVAR(0, bomberbuffdelay, 0, 3000, VAR_MAX); // buffed for this long after leaving
+GFVAR(0, bomberbuffarea, FVAR_NONZERO, 128, FVAR_MAX); // radius in which buffing occurs
+GFVAR(0, bomberbuffdamage, 1, 1.25f, FVAR_MAX); // multiply outgoing damage by this much when buffed
+GFVAR(0, bomberbuffshield, 1, 1.25f, FVAR_MAX); // divide incoming damage by this much when buffed
 GVAR(0, bomberregenbuff, 0, 1, 1); // 0 = off, 1 = modify regeneration when buffed
 GVAR(0, bomberregendelay, 0, 1000, VAR_MAX); // regen this often when buffed
 GVAR(0, bomberregenextra, 0, 2, VAR_MAX); // add this to regen when buffed
 
-GVAR(0, gauntletbuffing, 0, 1, 2); // buffed; 0 = off, 1 = when guarding, 2 = always (always on if gsp2)
-GVAR(0, gauntletbuffdelay, 0, 1000, VAR_MAX); // buffed for this long after leaving
-GFVAR(0, gauntletbuffarea, FVAR_NONZERO, 256, FVAR_MAX); // radius in which buffing occurs
-GFVAR(0, gauntletbuffdamage, 1, 1.5f, FVAR_MAX); // multiply outgoing damage by this much when buffed
-GFVAR(0, gauntletbuffshield, 1, 1.5f, FVAR_MAX); // divide incoming damage by this much when buffed
-GVAR(0, gauntletregenbuff, 0, 1, 1); // 0 = off, 1 = modify regeneration when buffed
-GVAR(0, gauntletregendelay, 0, 1000, VAR_MAX); // regen this often when buffed
-GVAR(0, gauntletregenextra, 0, 2, VAR_MAX); // add this to regen when buffed
-
-GVAR(IDF_ADMIN, aiinitdelay, 0, 5000, VAR_MAX);
-GVAR(IDF_ADMIN, ailongdelay, 0, 10000, VAR_MAX);
 GVAR(IDF_ADMIN, airefreshdelay, 0, 1000, VAR_MAX);
-GVAR(0, botbalance, -1, -1, VAR_MAX); // -1 = always use numplayers, 0 = don't balance, 1 or more = fill only with this*numteams
+GVAR(0, botbalance, -1, -1, VAR_MAX); // -1 = always use numplayers, 0 = don't balance, 1 or more = fill only with this many
+GVAR(0, botbalanceduel, -1, 2, VAR_MAX); // -1 = always use numplayers, 0 = don't balance, 1 or more = fill only with this many
+GVAR(0, botbalancesurvivor, -1, -1, VAR_MAX); // -1 = always use numplayers, 0 = don't balance, 1 or more = fill only with this many
 GVAR(0, botskillmin, 1, 60, 101);
 GVAR(0, botskillmax, 1, 75, 101);
-GVAR(0, botlimit, 0, 32, VAR_MAX);
+GFVAR(0, botskillfrags, 0, 1, 100);
+GFVAR(0, botskilldeaths, 0, 1, 100);
+GVAR(0, botlimit, 0, 32, MAXAI);
 GVAR(0, botoffset, VAR_MIN, 0, VAR_MAX);
 GSVAR(IDF_ADMIN, botmalenames, "");
 GSVAR(IDF_ADMIN, botfemalenames, "");
@@ -340,98 +379,98 @@ GFVAR(0, coopbalance, FVAR_NONZERO, 1.5f, FVAR_MAX);
 GFVAR(0, coopmultibalance, FVAR_NONZERO, 2, FVAR_MAX);
 GVAR(0, coopskillmin, 1, 75, 101);
 GVAR(0, coopskillmax, 1, 85, 101);
+GFVAR(0, coopskillfrags, 0, 1, 100);
+GFVAR(0, coopskilldeaths, 0, 1.5f, 100);
 GVAR(0, enemybalance, 1, 1, 3);
 GVAR(0, enemyskillmin, 1, 65, 101);
 GVAR(0, enemyskillmax, 1, 80, 101);
-GVAR(0, enemylimit, 0, 32, VAR_MAX);
+GVAR(0, enemylimit, 0, 32, MAXAI);
 GVAR(0, enemyspawntime, 1, 30000, VAR_MAX); // when enemies respawn
-GVAR(0, enemyspawndelay, 0, 1000, VAR_MAX); // after map start enemies first spawn
+GVAR(0, enemyspawndelay, 0, 5000, VAR_MAX); // after map start enemies first spawn
 GVAR(0, enemyspawnstyle, 0, 1, 3); // 0 = all at once, 1 = staggered, 2 = random, 3 = randomise between both
 GFVAR(0, enemyspeed, 0, 1, FVAR_MAX);
 GFVAR(0, enemyscale, FVAR_NONZERO, 1, FVAR_MAX);
 GFVAR(0, enemystrength, FVAR_NONZERO, 1, FVAR_MAX); // scale enemy health values by this much
-GFVAR(0, enemyspawndistmin, 0, 32, FVAR_MAX);
-GFVAR(0, enemyspawndistmax, 0, 512, FVAR_MAX);
 
-GFVAR(0, movespeed, FVAR_NONZERO, 100, FVAR_MAX); // speed
+GFVAR(0, movespeed, FVAR_NONZERO, 125, FVAR_MAX); // speed
+GFVAR(0, moveslow, 0, 50, FVAR_MAX); // threshold for moving
 GFVAR(0, movecrawl, 0, 0.6f, FVAR_MAX); // crawl modifier
-GFVAR(0, movepacing, FVAR_NONZERO, 1.6f, FVAR_MAX); // pacing modifier
-GFVAR(0, movejet, FVAR_NONZERO, 1.6f, FVAR_MAX); // jetpack modifier
+GFVAR(0, moverun, FVAR_NONZERO, 1.3f, FVAR_MAX); // running modifier
 GFVAR(0, movestraight, FVAR_NONZERO, 1.2f, FVAR_MAX); // non-strafe modifier
 GFVAR(0, movestrafe, FVAR_NONZERO, 1, FVAR_MAX); // strafe modifier
 GFVAR(0, moveinair, FVAR_NONZERO, 0.9f, FVAR_MAX); // in-air modifier
 GFVAR(0, movestepup, FVAR_NONZERO, 0.95f, FVAR_MAX); // step-up modifier
 GFVAR(0, movestepdown, FVAR_NONZERO, 1.15f, FVAR_MAX); // step-down modifier
 
-GVAR(0, jetdelay, 0, 250, VAR_MAX); // minimum time between jetpack
-GFVAR(0, jetheight, 0, 0, FVAR_MAX); // jetpack maximum height off ground
-GFVAR(0, jetdecay, 1, 15, FVAR_MAX); // decay rate of one unit per this many ms
-
 GFVAR(0, jumpspeed, FVAR_NONZERO, 110, FVAR_MAX); // extra velocity to add when jumping
 GFVAR(0, impulsespeed, FVAR_NONZERO, 90, FVAR_MAX); // extra velocity to add when impulsing
-
 GFVAR(0, impulselimit, 0, 0, FVAR_MAX); // maximum impulse speed
 GFVAR(0, impulseboost, 0, 1, FVAR_MAX); // thrust modifier
+GFVAR(0, impulseboostredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
 GFVAR(0, impulsepower, 0, 1.5f, FVAR_MAX); // power jump modifier
-GFVAR(0, impulsedash, 0, 1.2f, FVAR_MAX); // dashing/powerslide modifier
+GFVAR(0, impulsepowerredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
+GFVAR(0, impulsedash, 0, 1.3f, FVAR_MAX); // dashing/powerslide modifier
+GFVAR(0, impulsedashredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
 GFVAR(0, impulsejump, 0, 1.1f, FVAR_MAX); // jump modifier
+GFVAR(0, impulsejumpredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
 GFVAR(0, impulsemelee, 0, 0.75f, FVAR_MAX); // melee modifier
+GFVAR(0, impulsemeleeredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
 GFVAR(0, impulseparkour, 0, 1, FVAR_MAX); // parkour modifier
-GFVAR(0, impulseparkourkick, 0, 1.3f, FVAR_MAX); // parkour kick modifier
-GFVAR(0, impulseparkourvault, 0, 1.3f, FVAR_MAX); // parkour vault modifier
+GFVAR(0, impulseparkourredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
+GFVAR(0, impulseparkourkick, 0, 1.5f, FVAR_MAX); // parkour kick modifier
+GFVAR(0, impulseparkourkickredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
+GFVAR(0, impulseparkourclimb, 0, 1.4f, FVAR_MAX); // parkour climb modifier
+GFVAR(0, impulseparkourclimbredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
+GFVAR(0, impulseparkourvault, 0, 1.5f, FVAR_MAX); // parkour vault modifier
+GFVAR(0, impulseparkourvaultredir, 0, 1, FVAR_MAX); // how much of the old velocity is redirected into the new one
 GFVAR(0, impulseparkournorm, 0, 0.5f, FVAR_MAX); // minimum parkour surface z normal
-GVAR(0, impulseallowed, 0, IM_A_ALL, IM_A_ALL); // impulse actions allowed; bit: 0 = off, 1 = dash, 2 = boost, 4 = pacing, 8 = parkour
+GVAR(0, impulseallowed, 0, IM_A_ALL, IM_A_ALL); // impulse actions allowed; bit: 0 = off, 1 = dash, 2 = boost, 4 = parkour
 GVAR(0, impulsestyle, 0, 1, 3); // impulse style; 0 = off, 1 = touch and count, 2 = count only, 3 = freestyle
 GVAR(0, impulsecount, 0, 6, VAR_MAX); // number of impulse actions per air transit
 GVAR(0, impulseslip, 0, 250, VAR_MAX); // time before floor friction kicks back in
 GVAR(0, impulseslide, 0, 1000, VAR_MAX); // time before powerslides end
-GVAR(0, impulsedelay, 0, 250, VAR_MAX); // minimum time between boosts
-GVAR(0, impulsedashdelay, 0, 1000, VAR_MAX); // minimum time between dashes/powerslides
+GVAR(0, impulsejumpdelay, 0, 100, VAR_MAX); // minimum time after jump for boost
+GVAR(0, impulseboostdelay, 0, 250, VAR_MAX); // minimum time between boosts
+GVAR(0, impulsedashdelay, 0, 500, VAR_MAX); // minimum time between dashes/powerslides
 GVAR(0, impulsekickdelay, 0, 350, VAR_MAX); // minimum time between wall kicks/climbs
 GFVAR(0, impulsevaultmin, FVAR_NONZERO, 0.25f, FVAR_MAX); // minimum percentage of height for vault
 GFVAR(0, impulsevaultmax, FVAR_NONZERO, 1.f, FVAR_MAX); // maximum percentage of height for vault
 
-GVAR(0, impulsemeter, 0, 30000, VAR_MAX); // impulse dash length; 0 = unlimited, anything else = timer
-GVAR(0, impulsecost, 0, 5000, VAR_MAX); // cost of impulse move
-GVAR(0, impulsecostrelax, 0, IM_A_RELAX, IM_A_ALL); // whether the cost of an impulse move is unimportant; bit: 0 = off, 1 = dash, 2 = boost, 4 = pacing, 8 = parkour
+GVAR(0, impulsemeter, 1, 30000, VAR_MAX); // impulse dash length; timer
+GVAR(0, impulsecost, 1, 5000, VAR_MAX); // cost of impulse move
+GVAR(0, impulsecostrelax, 0, IM_A_RELAX, IM_A_ALL); // whether the cost of an impulse move is unimportant; bit: 0 = off, 1 = dash, 2 = boost, 4 = parkour
 GVAR(0, impulsecostscale, 0, 0, 1); // whether the cost scales depend on the amount the impulse scales
 GVAR(0, impulseskate, 0, 1000, VAR_MAX); // length of time a run along a wall can last
-GFVAR(0, impulsepacing, 0, 0, FVAR_MAX); // pacing impulse meter depletion
-GFVAR(0, impulsejet, 0, 0.5f, FVAR_MAX); // jetpack impulse meter depletion
 GFVAR(0, impulseregen, 0, 5, FVAR_MAX); // impulse regen multiplier
 GFVAR(0, impulseregencrouch, 0, 2.5f, FVAR_MAX); // impulse regen crouch modifier
-GFVAR(0, impulseregenpacing, 0, 0.75f, FVAR_MAX); // impulse regen pacing modifier
-GFVAR(0, impulseregenjet, 0, 2, FVAR_MAX); // impulse regen jetpack modifier
+GFVAR(0, impulseregenrun, 0, 0.75f, FVAR_MAX); // impulse regen running modifier
 GFVAR(0, impulseregenmove, 0, 1, FVAR_MAX); // impulse regen moving modifier
 GFVAR(0, impulseregeninair, 0, 0.75f, FVAR_MAX); // impulse regen in-air modifier
 GFVAR(0, impulseregenslide, 0, 0, FVAR_MAX); // impulse regen sliding modifier
 GVAR(0, impulseregendelay, 0, 350, VAR_MAX); // delay before impulse regens
-GVAR(0, impulseregenjetdelay, -1, -1, VAR_MAX); // delay before impulse regens after jetting, -1 = must touch ground
 
-GFVAR(0, inairspread, 0, 2, FVAR_MAX);
-GFVAR(0, movespread, 0, 1, FVAR_MAX);
-GFVAR(0, impulsespread, 0, 1, FVAR_MAX);
-GFVAR(0, stillspread, 0, 0, FVAR_MAX);
+GFVAR(0, spreadstill, 0, 0.f, FVAR_MAX);
+GFVAR(0, spreadmoving, 0, 1.f, FVAR_MAX);
+GFVAR(0, spreadrunning, 0, 1.f, FVAR_MAX);
+GFVAR(0, spreadinair, 0, 1.f, FVAR_MAX);
 
 GVAR(0, quakefade, 0, 250, VAR_MAX);
 GVAR(0, quakewobble, 1, 18, VAR_MAX);
 GVAR(0, quakelimit, 0, 200, VAR_MAX);
 
-GVAR(0, zoomlock, 0, 0, 4); // 0 = unrestricted, 1 = must be on floor, 2 = also must not be moving, 3 = also must be on flat floor, 4 = must also be crouched
-GVAR(0, zoomlocktime, 0, 500, VAR_MAX); // time before zoomlock kicks in when in the air
 GVAR(0, zoomlimitmin, 1, 10, 150);
 GVAR(0, zoomlimitmax, 1, 65, 150);
-GVAR(0, zoomtime, 1, 100, VAR_MAX);
+GVAR(0, zoomtime, 1, 250, VAR_MAX);
 
 GFVAR(0, radialscale, 0, 1, FVAR_MAX);
 GFVAR(0, radiallimited, 0, 0.75f, FVAR_MAX);
 GFVAR(0, damagescale, 0, 1, FVAR_MAX);
 GVAR(0, weaponswitchdelay, 0, 500, VAR_MAX);
-GVAR(0, weaponinterrupts, 0, W_S_INTERRUPT, W_S_ALL); // what weapon states can have their delay interrupted
 
 GFVAR(0, pushscale, 0, 1, FVAR_MAX);
 GFVAR(0, pushlimited, 0, 0.75f, FVAR_MAX);
 GFVAR(0, hitpushscale, 0, 1, FVAR_MAX);
+GFVAR(0, hitvelscale, 0, 1, FVAR_MAX);
 GFVAR(0, deadpushscale, 0, 2, FVAR_MAX);
 GFVAR(0, wavepushscale, 0, 1, FVAR_MAX);
 
@@ -454,11 +493,31 @@ GVAR(0, headshotpoints, 0, 1, VAR_MAX);
 
 GVAR(0, assistkilldelay, 0, 5000, VAR_MAX);
 GVAR(0, multikilldelay, 0, 5000, VAR_MAX);
-GVAR(0, multikillpoints, 0, 1, 1);
-GVAR(0, multikillbonus, 0, 1, VAR_MAX);
+GVAR(0, multikillpoints, 0, 1, VAR_MAX);
+GVAR(0, multikillbonus, 0, 0, 1); // if bonus is on, then points are multiplied by the current kill mutliplier (x2, x3, x4)
 GVAR(0, spreecount, 0, 5, VAR_MAX);
 GVAR(0, spreepoints, 0, 1, VAR_MAX);
 GVAR(0, spreebreaker, 0, 1, VAR_MAX);
 GVAR(0, dominatecount, 0, 5, VAR_MAX);
 GVAR(0, dominatepoints, 0, 1, VAR_MAX);
 GVAR(0, revengepoints, 0, 1, VAR_MAX);
+
+GSVAR(0, obitdestroyed, "was destroyed");
+GSVAR(0, obitdied, "died");
+GSVAR(0, obitfragged, "fragged");
+GSVAR(0, obitspawn, "tried to spawn inside solid matter");
+GSVAR(0, obitspectator, "gave up their corporeal form");
+GSVAR(0, obitdrowned, "drowned");
+GSVAR(0, obitmelted, "melted into a ball of fire");
+GSVAR(0, obitdeathmat, "met their end");
+GSVAR(0, obitlost, "fell to their death");
+GSVAR(0, obitburn, "set ablaze");
+GSVAR(0, obitburnself, "burned up");
+GSVAR(0, obitbleed, "fatally wounded");
+GSVAR(0, obitbleedself, "bled out");
+GSVAR(0, obitshock, "given a terminal dose of shock therapy");
+GSVAR(0, obitshockself, "twitched to death");
+GSVAR(0, obitobliterated, "was obliterated");
+GSVAR(0, obitheadless, "had their head caved in");
+GSVAR(0, obitsuicide, "suicided");
+GSVAR(0, obitkilled, "killed");
diff --git a/src/game/waypoint.cpp b/src/game/waypoint.cpp
index c54e20b..17937a7 100644
--- a/src/game/waypoint.cpp
+++ b/src/game/waypoint.cpp
@@ -40,20 +40,20 @@ namespace ai
         NUMWPCACHES
     };
 
-    struct wpcachenode
+    struct wpcache
     {
-        float split[2];
-        uint child[2];
+        struct node
+        {
+            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; }
-    };
+            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;
+        vector<node> nodes;
+        int firstwp, lastwp;
         vec bbmin, bbmax;
 
         wpcache() { clear(); }
@@ -62,12 +62,11 @@ namespace ai
         {
             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)
+        void build(int first = 0, int last = -1)
         {
             if(last < 0) last = waypoints.length();
             vector<int> indices;
@@ -81,10 +80,14 @@ namespace ai
                 bbmax.max(vec(w.o).add(radius));
             }
             if(first < last) lastwp = max(lastwp, last-1);
-            build(indices.getbuf(), indices.length(), bbmin, bbmax);
+            if(indices.length())
+            {
+                nodes.reserve(indices.length());
+                build(indices.getbuf(), indices.length(), bbmin, bbmax);
+            }
         }
 
-        void build(int *indices, int numindices, const vec &vmin, const vec &vmax, int depth = 1)
+        void build(int *indices, int numindices, const vec &vmin, const vec &vmax)
         {
             int axis = 2;
             loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k;
@@ -139,26 +142,24 @@ namespace ai
                 }
             }
 
-            int node = nodes.length();
-            nodes.add();
-            nodes[node].split[0] = splitleft;
-            nodes[node].split[1] = splitright;
+            int offset = nodes.length();
+            node &curnode = nodes.add();
+            curnode.split[0] = splitleft;
+            curnode.split[1] = splitright;
 
-            if(left<=1) nodes[node].child[0] = (axis<<30) | (left>0 ? indices[0] : 0x3FFFFFFF);
+            if(left<=1) curnode.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);
+                curnode.child[0] = (axis<<30) | (nodes.length()-offset);
+                if(left) build(indices, left, leftmin, leftmax);
             }
 
-            if(numindices-right<=1) nodes[node].child[1] = (1<<31) | (left<=1 ? 1<<30 : 0) | (numindices-right>0 ? indices[right] : 0x3FFFFFFF);
+            if(numindices-right<=1) curnode.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);
+                curnode.child[1] = (left<=1 ? 1<<30 : 0) | (nodes.length()-offset);
+                if(numindices-right) build(&indices[right], numindices-right, rightmin, rightmax);
             }
-
-            maxdepth = max(maxdepth, depth);
         }
     } wpcaches[NUMWPCACHES];
 
@@ -167,7 +168,11 @@ namespace ai
     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; }
+        else
+        {
+            loopi(WPCACHE_DYNAMIC) if(wp >= wpcaches[i].firstwp && wp <= wpcaches[i].lastwp) { invalidatedwpcaches |= 1<<i; return; }
+            invalidatedwpcaches |= 1<<WPCACHE_DYNAMIC;
+        }
     }
 
     void clearwpcache(bool full = true)
@@ -184,8 +189,8 @@ namespace ai
 
     void buildwpcache()
     {
-        loopi(NUMWPCACHES) if(wpcaches[i].maxdepth < 0)
-            wpcaches[i].build(i > 0 ? wpcaches[i-1].lastwp+1 : 1, i+1 >= NUMWPCACHES || wpcaches[i+1].maxdepth < 0 ? -1 : wpcaches[i+1].firstwp);
+        loopi(NUMWPCACHES) if(wpcaches[i].firstwp < 0)
+            wpcaches[i].build(i > 0 ? wpcaches[i-1].lastwp+1 : 1, i+1 >= NUMWPCACHES || wpcaches[i+1].firstwp < 0 ? -1 : wpcaches[i+1].firstwp);
         clearedwpcaches = 0;
         lastwpcache = waypoints.length();
 
@@ -195,11 +200,11 @@ namespace ai
 
     struct wpcachestack
     {
-        wpcachenode *node;
+        wpcache::node *node;
         float tmin, tmax;
     };
 
-    vector<wpcachenode *> wpcachestack;
+    vector<wpcache::node *> wpcachestack;
 
     int closestwaypoint(const vec &pos, float mindist, bool links)
     {
@@ -208,7 +213,7 @@ namespace ai
 
         #define CHECKCLOSEST(index) do { \
             int n = (index); \
-            if(waypoints.inrange(n)) \
+            if(n < waypoints.length()) \
             { \
                 waypoint &w = waypoints[n]; \
                 if(!links || w.haslinks()) \
@@ -219,10 +224,9 @@ namespace ai
             } \
         } while(0)
         int closest = -1;
-        wpcachenode *curnode;
-        loop(which, NUMWPCACHES) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
+        wpcache::node *curnode;
+        loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
         {
-            if(!curnode) continue;
             int axis = curnode->axis();
             float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
             if(dist1 >= mindist)
@@ -267,17 +271,16 @@ namespace ai
         float mindist2 = mindist*mindist, maxdist2 = maxdist*maxdist;
         #define CHECKWITHIN(index) do { \
             int n = (index); \
-            if(waypoints.inrange(n)) \
+            if(n < waypoints.length()) \
             { \
                 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);;)
+        wpcache::node *curnode;
+        loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
         {
-            if(!curnode) continue;
             int axis = curnode->axis();
             float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
             if(dist1 >= maxdist)
@@ -321,16 +324,15 @@ namespace ai
         float limit2 = limit*limit;
         #define CHECKNEAR(index) do { \
             int n = (index); \
-            if(ai::waypoints.inrange(n)) \
+            if(n < ai::waypoints.length()) \
             { \
                 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);;)
+        wpcache::node *curnode;
+        loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
         {
-            if(!curnode) continue;
             int axis = curnode->axis();
             float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
             if(dist1 >= limit)
@@ -399,7 +401,7 @@ namespace ai
                             d->o = vec(above).add(vec(0, 0, d->height));
                             bool col = collide(d, vec(0, 0, 1));
                             d->o = old;
-                            if(col)
+                            if(!col)
                             {
                                 pos = above;
                                 return n;
@@ -511,6 +513,7 @@ namespace ai
         if(waypoints.length() > MAXWAYPOINTS) return -1;
         int n = waypoints.length();
         waypoints.add(waypoint(o, weight >= 0 ? weight : getweight(o)));
+        invalidatewpcache(n);
         return n;
     }
 
@@ -527,7 +530,7 @@ namespace ai
     static inline bool shouldnavigate()
     {
         if(dropwaypoints) return true;
-        loopvrev(players) if(players[i] && players[i]->aitype != AI_NONE) return true;
+        loopvrev(players) if(players[i] && players[i]->actortype != A_PLAYER) return true;
         return false;
     }
 
@@ -576,7 +579,7 @@ namespace ai
         }
         else if(!iswaypoint(d->lastnode) || waypoints[d->lastnode].o.squaredist(v) > dist*dist)
         {
-            dist = physics::jetpack(d) ? JETDIST : RETRYDIST; // workaround
+            dist = RETRYDIST; // workaround
 			d->lastnode = closestwaypoint(v, dist, false);
         }
     }
@@ -597,7 +600,7 @@ namespace ai
         clearwpcache();
         if(full) loadedwaypoints[0] = '\0';
     }
-    ICOMMAND(0, clearwaypoints, "", (), clearwaypoints());
+    ICOMMAND(0, clearwaypoints, "", (), clearwaypoints(true));
 
     void remapwaypoints()
     {
@@ -627,7 +630,7 @@ namespace ai
     {
         if(o.dist(v) > CLOSEDIST)
         {
-            loopi(entities::lastenttype[TELEPORT]) if(entities::ents[i]->type == TELEPORT)
+            loopi(entities::lastent(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)
@@ -728,7 +731,7 @@ namespace ai
         if(!cleanwaypoints()) clearwpcache();
         return true;
     }
-    ICOMMAND(0, loadwaypoints, "s", (char *mname), getwaypoints(true, mname));
+    ICOMMAND(0, loadwaypoints, "s", (char *mname), if(!(identflags&IDF_WORLD)) getwaypoints(true, mname));
 
     void savewaypoints(bool force, const char *mname)
     {
@@ -757,7 +760,7 @@ namespace ai
         conoutf("saved %d waypoints to %s", waypoints.length()-1, wptname);
     }
 
-    ICOMMAND(0, savewaypoints, "s", (char *mname), savewaypoints(true, mname));
+    ICOMMAND(0, savewaypoints, "s", (char *mname), if(!(identflags&IDF_WORLD)) savewaypoints(true, mname));
 
     bool importwaypoints()
     {
@@ -791,6 +794,22 @@ namespace ai
         return loadwaypoints(force, mname) || importwaypoints();
     }
 
+    void delwaypoint(int n)
+    {
+        if(n < 0)
+        {
+            if(noedit(true)) return;
+            n = closestwaypoint(camera1->o);
+        }
+        if(!iswaypoint(n)) return;
+        waypoints[n].links[0] = 0;
+        waypoints[n].links[1] = 0xFFFF;
+        remapwaypoints();
+        clearwpcache();
+    }
+    ICOMMAND(0, delwaypoint, "b", (int *n), delwaypoint(*n));
+
+
     void delselwaypoints()
     {
         if(noedit(true)) return;
@@ -814,5 +833,27 @@ namespace ai
         }
     }
     COMMAND(0, delselwaypoints, "");
+
+    void moveselwaypoints(vec &offset)
+    {
+        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 moved = 0;
+        for(int i = 1; i < waypoints.length(); i++)
+        {
+            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.o.add(offset);
+                moved++;
+            }
+        }
+        if(moved)
+        {
+            player1->lastnode = -1;
+            clearwpcache();
+        }
+    }
+    ICOMMAND(0, moveselwaypoints, "fff", (float *x, float *y, float *z), vec v(*x, *y, *z); moveselwaypoints(v));
 }
 
diff --git a/src/game/weapdef.h b/src/game/weapdef.h
index 6fd5bcc..99aaa5a 100644
--- a/src/game/weapdef.h
+++ b/src/game/weapdef.h
@@ -1,5 +1,5 @@
 #ifdef GAMESERVER
-    #define WPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GVAR(flags, melee##name, mn, w00, mx); \
         GVAR(flags, pistol##name, mn, w01, mx); \
         GVAR(flags, sword##name, mn, w02, mx); \
@@ -7,10 +7,11 @@
         GVAR(flags, smg##name, mn, w04, mx); \
         GVAR(flags, flamer##name, mn, w05, mx); \
         GVAR(flags, plasma##name, mn, w06, mx); \
-        GVAR(flags, rifle##name, mn, w07, mx); \
-        GVAR(flags, grenade##name, mn, w08, mx); \
-        GVAR(flags, mine##name, mn, w09, mx); \
-        GVAR(flags, rocket##name, mn, w10, mx); \
+        GVAR(flags, zapper##name, mn, w07, mx); \
+        GVAR(flags, rifle##name, mn, w08, mx); \
+        GVAR(flags, grenade##name, mn, w09, mx); \
+        GVAR(flags, mine##name, mn, w10, mx); \
+        GVAR(flags, rocket##name, mn, w11, mx); \
         int *sv_weap_stat_##name[] = { \
             &sv_melee##name, \
             &sv_pistol##name, \
@@ -19,13 +20,14 @@
             &sv_smg##name, \
             &sv_flamer##name, \
             &sv_plasma##name, \
+            &sv_zapper##name, \
             &sv_rifle##name, \
             &sv_grenade##name, \
             &sv_mine##name, \
             &sv_rocket##name \
         };
 
-    #define WPVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GVAR(flags, melee##name##1, mn, w100, mx);   GVAR(flags, melee##name##2, mn, w200, mx); \
         GVAR(flags, pistol##name##1, mn, w101, mx);  GVAR(flags, pistol##name##2, mn, w201, mx); \
         GVAR(flags, sword##name##1, mn, w102, mx);   GVAR(flags, sword##name##2, mn, w202, mx); \
@@ -33,10 +35,11 @@
         GVAR(flags, smg##name##1, mn, w104, mx);     GVAR(flags, smg##name##2, mn, w204, mx); \
         GVAR(flags, flamer##name##1, mn, w105, mx);  GVAR(flags, flamer##name##2, mn, w205, mx); \
         GVAR(flags, plasma##name##1, mn, w106, mx);  GVAR(flags, plasma##name##2, mn, w206, mx); \
-        GVAR(flags, rifle##name##1, mn, w107, mx);   GVAR(flags, rifle##name##2, mn, w207, mx);\
-        GVAR(flags, grenade##name##1, mn, w108, mx); GVAR(flags, grenade##name##2, mn, w208, mx); \
-        GVAR(flags, mine##name##1, mn, w109, mx);    GVAR(flags, mine##name##2, mn, w209, mx); \
-        GVAR(flags, rocket##name##1, mn, w110, mx);  GVAR(flags, rocket##name##2, mn, w210, mx); \
+        GVAR(flags, zapper##name##1, mn, w107, mx);  GVAR(flags, zapper##name##2, mn, w207, mx); \
+        GVAR(flags, rifle##name##1, mn, w108, mx);   GVAR(flags, rifle##name##2, mn, w208, mx);\
+        GVAR(flags, grenade##name##1, mn, w109, mx); GVAR(flags, grenade##name##2, mn, w209, mx); \
+        GVAR(flags, mine##name##1, mn, w110, mx);    GVAR(flags, mine##name##2, mn, w210, mx); \
+        GVAR(flags, rocket##name##1, mn, w111, mx);  GVAR(flags, rocket##name##2, mn, w211, mx); \
         int *sv_weap_stat_##name[][2] = { \
             { &sv_melee##name##1,    &sv_melee##name##2 }, \
             { &sv_pistol##name##1,   &sv_pistol##name##2 }, \
@@ -45,13 +48,14 @@
             { &sv_smg##name##1,      &sv_smg##name##2 }, \
             { &sv_flamer##name##1,   &sv_flamer##name##2 }, \
             { &sv_plasma##name##1,   &sv_plasma##name##2 }, \
+            { &sv_zapper##name##1,   &sv_zapper##name##2 }, \
             { &sv_rifle##name##1,    &sv_rifle##name##2 }, \
             { &sv_grenade##name##1,  &sv_grenade##name##2 }, \
             { &sv_mine##name##1,     &sv_mine##name##2 }, \
             { &sv_rocket##name##1,   &sv_rocket##name##2 } \
         };
 
-    #define WPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GFVAR(flags, melee##name, mn, w00, mx); \
         GFVAR(flags, pistol##name, mn, w01, mx); \
         GFVAR(flags, sword##name, mn, w02, mx); \
@@ -59,10 +63,11 @@
         GFVAR(flags, smg##name, mn, w04, mx); \
         GFVAR(flags, flamer##name, mn, w05, mx); \
         GFVAR(flags, plasma##name, mn, w06, mx); \
-        GFVAR(flags, rifle##name, mn, w07, mx); \
-        GFVAR(flags, grenade##name, mn, w08, mx); \
-        GFVAR(flags, mine##name, mn, w09, mx); \
-        GFVAR(flags, rocket##name, mn, w10, mx); \
+        GFVAR(flags, zapper##name, mn, w07, mx); \
+        GFVAR(flags, rifle##name, mn, w08, mx); \
+        GFVAR(flags, grenade##name, mn, w09, mx); \
+        GFVAR(flags, mine##name, mn, w10, mx); \
+        GFVAR(flags, rocket##name, mn, w11, mx); \
         float *sv_weap_stat_##name[] = { \
             &sv_melee##name, \
             &sv_pistol##name, \
@@ -71,13 +76,14 @@
             &sv_smg##name, \
             &sv_flamer##name, \
             &sv_plasma##name, \
+            &sv_zapper##name, \
             &sv_rifle##name, \
             &sv_grenade##name, \
             &sv_mine##name, \
             &sv_rocket##name \
         };
 
-    #define WPFVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPFVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GFVAR(flags, melee##name##1, mn, w100, mx);   GFVAR(flags, melee##name##2, mn, w200, mx); \
         GFVAR(flags, pistol##name##1, mn, w101, mx);  GFVAR(flags, pistol##name##2, mn, w201, mx); \
         GFVAR(flags, sword##name##1, mn, w102, mx);   GFVAR(flags, sword##name##2, mn, w202, mx); \
@@ -85,10 +91,11 @@
         GFVAR(flags, smg##name##1, mn, w104, mx);     GFVAR(flags, smg##name##2, mn, w204, mx); \
         GFVAR(flags, flamer##name##1, mn, w105, mx);  GFVAR(flags, flamer##name##2, mn, w205, mx); \
         GFVAR(flags, plasma##name##1, mn, w106, mx);  GFVAR(flags, plasma##name##2, mn, w206, mx); \
-        GFVAR(flags, rifle##name##1, mn, w107, mx);   GFVAR(flags, rifle##name##2, mn, w207, mx);\
-        GFVAR(flags, grenade##name##1, mn, w108, mx); GFVAR(flags, grenade##name##2, mn, w208, mx); \
-        GFVAR(flags, mine##name##1, mn, w109, mx);    GFVAR(flags, mine##name##2, mn, w209, mx); \
-        GFVAR(flags, rocket##name##1, mn, w110, mx);  GFVAR(flags, rocket##name##2, mn, w210, mx); \
+        GFVAR(flags, zapper##name##1, mn, w107, mx);  GFVAR(flags, zapper##name##2, mn, w207, mx); \
+        GFVAR(flags, rifle##name##1, mn, w108, mx);   GFVAR(flags, rifle##name##2, mn, w208, mx);\
+        GFVAR(flags, grenade##name##1, mn, w109, mx); GFVAR(flags, grenade##name##2, mn, w209, mx); \
+        GFVAR(flags, mine##name##1, mn, w110, mx);    GFVAR(flags, mine##name##2, mn, w210, mx); \
+        GFVAR(flags, rocket##name##1, mn, w111, mx);  GFVAR(flags, rocket##name##2, mn, w211, mx); \
         float *sv_weap_stat_##name[][2] = { \
             { &sv_melee##name##1,    &sv_melee##name##2 }, \
             { &sv_pistol##name##1,   &sv_pistol##name##2 }, \
@@ -97,13 +104,14 @@
             { &sv_smg##name##1,      &sv_smg##name##2 }, \
             { &sv_flamer##name##1,   &sv_flamer##name##2 }, \
             { &sv_plasma##name##1,   &sv_plasma##name##2 }, \
+            { &sv_zapper##name##1,   &sv_zapper##name##2 }, \
             { &sv_rifle##name##1,    &sv_rifle##name##2 }, \
             { &sv_grenade##name##1,  &sv_grenade##name##2 }, \
             { &sv_mine##name##1,     &sv_mine##name##2 }, \
             { &sv_rocket##name##1,   &sv_rocket##name##2 } \
         };
 
-    #define WPSVAR(flags, name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPSVAR(flags, name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GSVAR(flags, melee##name, w00); \
         GSVAR(flags, pistol##name, w01); \
         GSVAR(flags, sword##name, w02); \
@@ -111,10 +119,11 @@
         GSVAR(flags, smg##name, w04); \
         GSVAR(flags, flamer##name, w05); \
         GSVAR(flags, plasma##name, w06); \
-        GSVAR(flags, rifle##name, w07); \
-        GSVAR(flags, grenade##name, w08); \
-        GSVAR(flags, mine##name, w09); \
-        GSVAR(flags, rocket##name, w10); \
+        GSVAR(flags, zapper##name, w07); \
+        GSVAR(flags, rifle##name, w08); \
+        GSVAR(flags, grenade##name, w09); \
+        GSVAR(flags, mine##name, w10); \
+        GSVAR(flags, rocket##name, w11); \
         char **sv_weap_stat_##name[] = { \
             &sv_melee##name, \
             &sv_pistol##name, \
@@ -123,13 +132,14 @@
             &sv_smg##name, \
             &sv_flamer##name, \
             &sv_plasma##name, \
+            &sv_zapper##name, \
             &sv_rifle##name, \
             &sv_grenade##name, \
             &sv_mine##name, \
             &sv_rocket##name \
         };
 
-    #define WPSVARM(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPSVARM(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GSVAR(flags, melee##name##1, w100);   GSVAR(flags, melee##name##2, w200); \
         GSVAR(flags, pistol##name##1, w101);  GSVAR(flags, pistol##name##2, w201); \
         GSVAR(flags, sword##name##1, w102);   GSVAR(flags, sword##name##2, w202); \
@@ -137,10 +147,11 @@
         GSVAR(flags, smg##name##1, w104);     GSVAR(flags, smg##name##2, w204); \
         GSVAR(flags, flamer##name##1, w105);  GSVAR(flags, flamer##name##2, w205); \
         GSVAR(flags, plasma##name##1, w106);  GSVAR(flags, plasma##name##2, w206); \
-        GSVAR(flags, rifle##name##1, w107);   GSVAR(flags, rifle##name##2, w207);\
-        GSVAR(flags, grenade##name##1, w108); GSVAR(flags, grenade##name##2, w208); \
-        GSVAR(flags, mine##name##1, w109);    GSVAR(flags, mine##name##2, w209); \
-        GSVAR(flags, rocket##name##1, w110);  GSVAR(flags, rocket##name##2, w210); \
+        GSVAR(flags, zapper##name##1, w107);  GSVAR(flags, zapper##name##2, w207); \
+        GSVAR(flags, rifle##name##1, w108);   GSVAR(flags, rifle##name##2, w208);\
+        GSVAR(flags, grenade##name##1, w109); GSVAR(flags, grenade##name##2, w209); \
+        GSVAR(flags, mine##name##1, w110);    GSVAR(flags, mine##name##2, w210); \
+        GSVAR(flags, rocket##name##1, w111);  GSVAR(flags, rocket##name##2, w211); \
         char **sv_weap_stat_##name[][2] = { \
             { &sv_melee##name##1,    &sv_melee##name##2 }, \
             { &sv_pistol##name##1,   &sv_pistol##name##2 }, \
@@ -149,6 +160,7 @@
             { &sv_smg##name##1,      &sv_smg##name##2 }, \
             { &sv_flamer##name##1,   &sv_flamer##name##2 }, \
             { &sv_plasma##name##1,   &sv_plasma##name##2 }, \
+            { &sv_zapper##name##1,   &sv_zapper##name##2 }, \
             { &sv_rifle##name##1,    &sv_rifle##name##2 }, \
             { &sv_grenade##name##1,  &sv_grenade##name##2 }, \
             { &sv_mine##name##1,     &sv_mine##name##2 }, \
@@ -157,10 +169,9 @@
 
     #define W(weap,name)         (*sv_weap_stat_##name[weap])
     #define W2(weap,name,second) (*sv_weap_stat_##name[weap][second?1:0])
-    #define WSTR(a,weap,attr)    defformatstring(a)("sv_%s%s", weaptype[weap].name, #attr)
 #else
 #ifdef GAMEWORLD
-    #define WPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GVAR(flags, melee##name, mn, w00, mx); \
         GVAR(flags, pistol##name, mn, w01, mx); \
         GVAR(flags, sword##name, mn, w02, mx); \
@@ -168,10 +179,11 @@
         GVAR(flags, smg##name, mn, w04, mx); \
         GVAR(flags, flamer##name, mn, w05, mx); \
         GVAR(flags, plasma##name, mn, w06, mx); \
-        GVAR(flags, rifle##name, mn, w07, mx); \
-        GVAR(flags, grenade##name, mn, w08, mx); \
-        GVAR(flags, mine##name, mn, w09, mx); \
-        GVAR(flags, rocket##name, mn, w10, mx); \
+        GVAR(flags, zapper##name, mn, w07, mx); \
+        GVAR(flags, rifle##name, mn, w08, mx); \
+        GVAR(flags, grenade##name, mn, w09, mx); \
+        GVAR(flags, mine##name, mn, w10, mx); \
+        GVAR(flags, rocket##name, mn, w11, mx); \
         int *weap_stat_##name[] = { \
             &melee##name, \
             &pistol##name, \
@@ -180,13 +192,14 @@
             &smg##name, \
             &flamer##name, \
             &plasma##name, \
+            &zapper##name, \
             &rifle##name, \
             &grenade##name, \
             &mine##name, \
             &rocket##name \
         };
 
-    #define WPVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GVAR(flags, melee##name##1, mn, w100, mx);   GVAR(flags, melee##name##2, mn, w200, mx); \
         GVAR(flags, pistol##name##1, mn, w101, mx);  GVAR(flags, pistol##name##2, mn, w201, mx); \
         GVAR(flags, sword##name##1, mn, w102, mx);   GVAR(flags, sword##name##2, mn, w202, mx); \
@@ -194,10 +207,11 @@
         GVAR(flags, smg##name##1, mn, w104, mx);     GVAR(flags, smg##name##2, mn, w204, mx); \
         GVAR(flags, flamer##name##1, mn, w105, mx);  GVAR(flags, flamer##name##2, mn, w205, mx); \
         GVAR(flags, plasma##name##1, mn, w106, mx);  GVAR(flags, plasma##name##2, mn, w206, mx); \
-        GVAR(flags, rifle##name##1, mn, w107, mx);   GVAR(flags, rifle##name##2, mn, w207, mx);\
-        GVAR(flags, grenade##name##1, mn, w108, mx); GVAR(flags, grenade##name##2, mn, w208, mx); \
-        GVAR(flags, mine##name##1, mn, w109, mx);    GVAR(flags, mine##name##2, mn, w209, mx); \
-        GVAR(flags, rocket##name##1, mn, w110, mx);  GVAR(flags, rocket##name##2, mn, w210, mx); \
+        GVAR(flags, zapper##name##1, mn, w107, mx);  GVAR(flags, zapper##name##2, mn, w207, mx); \
+        GVAR(flags, rifle##name##1, mn, w108, mx);   GVAR(flags, rifle##name##2, mn, w208, mx);\
+        GVAR(flags, grenade##name##1, mn, w109, mx); GVAR(flags, grenade##name##2, mn, w209, mx); \
+        GVAR(flags, mine##name##1, mn, w110, mx);    GVAR(flags, mine##name##2, mn, w210, mx); \
+        GVAR(flags, rocket##name##1, mn, w111, mx);  GVAR(flags, rocket##name##2, mn, w211, mx); \
         int *weap_stat_##name[][2] = { \
             { &melee##name##1,    &melee##name##2 }, \
             { &pistol##name##1,   &pistol##name##2 }, \
@@ -206,13 +220,14 @@
             { &smg##name##1,      &smg##name##2 }, \
             { &flamer##name##1,   &flamer##name##2 }, \
             { &plasma##name##1,   &plasma##name##2 }, \
+            { &zapper##name##1,   &zapper##name##2 }, \
             { &rifle##name##1,    &rifle##name##2 }, \
             { &grenade##name##1,  &grenade##name##2 }, \
             { &mine##name##1,     &mine##name##2 }, \
             { &rocket##name##1,   &rocket##name##2 } \
         };
 
-    #define WPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GFVAR(flags, melee##name, mn, w00, mx); \
         GFVAR(flags, pistol##name, mn, w01, mx); \
         GFVAR(flags, sword##name, mn, w02, mx); \
@@ -220,10 +235,11 @@
         GFVAR(flags, smg##name, mn, w04, mx); \
         GFVAR(flags, flamer##name, mn, w05, mx); \
         GFVAR(flags, plasma##name, mn, w06, mx); \
-        GFVAR(flags, rifle##name, mn, w07, mx); \
-        GFVAR(flags, grenade##name, mn, w08, mx); \
-        GFVAR(flags, mine##name, mn, w09, mx); \
-        GFVAR(flags, rocket##name, mn, w10, mx); \
+        GFVAR(flags, zapper##name, mn, w07, mx); \
+        GFVAR(flags, rifle##name, mn, w08, mx); \
+        GFVAR(flags, grenade##name, mn, w09, mx); \
+        GFVAR(flags, mine##name, mn, w10, mx); \
+        GFVAR(flags, rocket##name, mn, w11, mx); \
         float *weap_stat_##name[] = { \
             &melee##name, \
             &pistol##name, \
@@ -232,13 +248,14 @@
             &smg##name, \
             &flamer##name, \
             &plasma##name, \
+            &zapper##name, \
             &rifle##name, \
             &grenade##name, \
             &mine##name, \
             &rocket##name \
         };
 
-    #define WPFVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPFVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GFVAR(flags, melee##name##1, mn, w100, mx);   GFVAR(flags, melee##name##2, mn, w200, mx); \
         GFVAR(flags, pistol##name##1, mn, w101, mx);  GFVAR(flags, pistol##name##2, mn, w201, mx); \
         GFVAR(flags, sword##name##1, mn, w102, mx);   GFVAR(flags, sword##name##2, mn, w202, mx); \
@@ -246,10 +263,11 @@
         GFVAR(flags, smg##name##1, mn, w104, mx);     GFVAR(flags, smg##name##2, mn, w204, mx); \
         GFVAR(flags, flamer##name##1, mn, w105, mx);  GFVAR(flags, flamer##name##2, mn, w205, mx); \
         GFVAR(flags, plasma##name##1, mn, w106, mx);  GFVAR(flags, plasma##name##2, mn, w206, mx); \
-        GFVAR(flags, rifle##name##1, mn, w107, mx);   GFVAR(flags, rifle##name##2, mn, w207, mx);\
-        GFVAR(flags, grenade##name##1, mn, w108, mx); GFVAR(flags, grenade##name##2, mn, w208, mx); \
-        GFVAR(flags, mine##name##1, mn, w109, mx);    GFVAR(flags, mine##name##2, mn, w209, mx); \
-        GFVAR(flags, rocket##name##1, mn, w110, mx);  GFVAR(flags, rocket##name##2, mn, w210, mx); \
+        GFVAR(flags, zapper##name##1, mn, w107, mx);  GFVAR(flags, zapper##name##2, mn, w207, mx); \
+        GFVAR(flags, rifle##name##1, mn, w108, mx);   GFVAR(flags, rifle##name##2, mn, w208, mx);\
+        GFVAR(flags, grenade##name##1, mn, w109, mx); GFVAR(flags, grenade##name##2, mn, w209, mx); \
+        GFVAR(flags, mine##name##1, mn, w110, mx);    GFVAR(flags, mine##name##2, mn, w210, mx); \
+        GFVAR(flags, rocket##name##1, mn, w111, mx);  GFVAR(flags, rocket##name##2, mn, w211, mx); \
         float *weap_stat_##name[][2] = { \
             { &melee##name##1,    &melee##name##2 }, \
             { &pistol##name##1,   &pistol##name##2 }, \
@@ -258,13 +276,14 @@
             { &smg##name##1,      &smg##name##2 }, \
             { &flamer##name##1,   &flamer##name##2 }, \
             { &plasma##name##1,   &plasma##name##2 }, \
+            { &zapper##name##1,   &zapper##name##2 }, \
             { &rifle##name##1,    &rifle##name##2 }, \
             { &grenade##name##1,  &grenade##name##2 }, \
             { &mine##name##1,     &mine##name##2 }, \
             { &rocket##name##1,   &rocket##name##2 } \
         };
 
-    #define WPSVAR(flags, name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPSVAR(flags, name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GSVAR(flags, melee##name, w00); \
         GSVAR(flags, pistol##name, w01); \
         GSVAR(flags, sword##name, w02); \
@@ -272,10 +291,11 @@
         GSVAR(flags, smg##name, w04); \
         GSVAR(flags, flamer##name, w05); \
         GSVAR(flags, plasma##name, w06); \
-        GSVAR(flags, rifle##name, w07); \
-        GSVAR(flags, grenade##name, w08); \
-        GSVAR(flags, mine##name, w09); \
-        GSVAR(flags, rocket##name, w10); \
+        GSVAR(flags, zapper##name, w07); \
+        GSVAR(flags, rifle##name, w08); \
+        GSVAR(flags, grenade##name, w09); \
+        GSVAR(flags, mine##name, w10); \
+        GSVAR(flags, rocket##name, w11); \
         char **weap_stat_##name[] = { \
             &melee##name, \
             &pistol##name, \
@@ -284,13 +304,14 @@
             &smg##name, \
             &flamer##name, \
             &plasma##name, \
+            &zapper##name, \
             &rifle##name, \
             &grenade##name, \
             &mine##name, \
             &rocket##name \
         };
 
-    #define WPSVARM(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPSVARM(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GSVAR(flags, melee##name##1, w100);   GSVAR(flags, melee##name##2, w200); \
         GSVAR(flags, pistol##name##1, w101);  GSVAR(flags, pistol##name##2, w201); \
         GSVAR(flags, sword##name##1, w102);   GSVAR(flags, sword##name##2, w202); \
@@ -298,10 +319,11 @@
         GSVAR(flags, smg##name##1, w104);     GSVAR(flags, smg##name##2, w204); \
         GSVAR(flags, flamer##name##1, w105);  GSVAR(flags, flamer##name##2, w205); \
         GSVAR(flags, plasma##name##1, w106);  GSVAR(flags, plasma##name##2, w206); \
-        GSVAR(flags, rifle##name##1, w107);   GSVAR(flags, rifle##name##2, w207);\
-        GSVAR(flags, grenade##name##1, w108); GSVAR(flags, grenade##name##2, w208); \
-        GSVAR(flags, mine##name##1, w109);    GSVAR(flags, mine##name##2, w209); \
-        GSVAR(flags, rocket##name##1, w110);  GSVAR(flags, rocket##name##2, w210); \
+        GSVAR(flags, zapper##name##1, w107);  GSVAR(flags, zapper##name##2, w207); \
+        GSVAR(flags, rifle##name##1, w108);   GSVAR(flags, rifle##name##2, w208);\
+        GSVAR(flags, grenade##name##1, w109); GSVAR(flags, grenade##name##2, w209); \
+        GSVAR(flags, mine##name##1, w110);    GSVAR(flags, mine##name##2, w210); \
+        GSVAR(flags, rocket##name##1, w111);  GSVAR(flags, rocket##name##2, w211); \
         char **weap_stat_##name[][2] = { \
             { &melee##name##1,    &melee##name##2 }, \
             { &pistol##name##1,   &pistol##name##2 }, \
@@ -310,13 +332,14 @@
             { &smg##name##1,      &smg##name##2 }, \
             { &flamer##name##1,   &flamer##name##2 }, \
             { &plasma##name##1,   &plasma##name##2 }, \
+            { &zapper##name##1,   &zapper##name##2 }, \
             { &rifle##name##1,    &rifle##name##2 }, \
             { &grenade##name##1,  &grenade##name##2 }, \
             { &mine##name##1,     &mine##name##2 }, \
             { &rocket##name##1,   &rocket##name##2 } \
         };
 #else
-    #define WPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GVAR(flags, melee##name, mn, w00, mx); \
         GVAR(flags, pistol##name, mn, w01, mx); \
         GVAR(flags, sword##name, mn, w02, mx); \
@@ -324,12 +347,13 @@
         GVAR(flags, smg##name, mn, w04, mx); \
         GVAR(flags, flamer##name, mn, w05, mx); \
         GVAR(flags, plasma##name, mn, w06, mx); \
-        GVAR(flags, rifle##name, mn, w07, mx); \
-        GVAR(flags, grenade##name, mn, w08, mx); \
-        GVAR(flags, mine##name, mn, w09, mx); \
-        GVAR(flags, rocket##name, mn, w10, mx); \
+        GVAR(flags, zapper##name, mn, w07, mx); \
+        GVAR(flags, rifle##name, mn, w08, mx); \
+        GVAR(flags, grenade##name, mn, w09, mx); \
+        GVAR(flags, mine##name, mn, w10, mx); \
+        GVAR(flags, rocket##name, mn, w11, mx); \
         extern int *weap_stat_##name[];
-    #define WPVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GVAR(flags, melee##name##1, mn, w100, mx);   GVAR(flags, melee##name##2, mn, w200, mx); \
         GVAR(flags, pistol##name##1, mn, w101, mx);  GVAR(flags, pistol##name##2, mn, w201, mx); \
         GVAR(flags, sword##name##1, mn, w102, mx);   GVAR(flags, sword##name##2, mn, w202, mx); \
@@ -337,12 +361,13 @@
         GVAR(flags, smg##name##1, mn, w104, mx);     GVAR(flags, smg##name##2, mn, w204, mx); \
         GVAR(flags, flamer##name##1, mn, w105, mx);  GVAR(flags, flamer##name##2, mn, w205, mx); \
         GVAR(flags, plasma##name##1, mn, w106, mx);  GVAR(flags, plasma##name##2, mn, w206, mx); \
-        GVAR(flags, rifle##name##1, mn, w107, mx);   GVAR(flags, rifle##name##2, mn, w207, mx);\
-        GVAR(flags, grenade##name##1, mn, w108, mx); GVAR(flags, grenade##name##2, mn, w208, mx); \
-        GVAR(flags, mine##name##1, mn, w109, mx);    GVAR(flags, mine##name##2, mn, w209, mx); \
-        GVAR(flags, rocket##name##1, mn, w110, mx);  GVAR(flags, rocket##name##2, mn, w210, mx); \
+        GVAR(flags, zapper##name##1, mn, w107, mx);  GVAR(flags, zapper##name##2, mn, w207, mx); \
+        GVAR(flags, rifle##name##1, mn, w108, mx);   GVAR(flags, rifle##name##2, mn, w208, mx);\
+        GVAR(flags, grenade##name##1, mn, w109, mx); GVAR(flags, grenade##name##2, mn, w209, mx); \
+        GVAR(flags, mine##name##1, mn, w110, mx);    GVAR(flags, mine##name##2, mn, w210, mx); \
+        GVAR(flags, rocket##name##1, mn, w111, mx);  GVAR(flags, rocket##name##2, mn, w211, mx); \
         extern int *weap_stat_##name[][2];
-    #define WPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPFVAR(flags, name, mn, mx, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GFVAR(flags, melee##name, mn, w00, mx); \
         GFVAR(flags, pistol##name, mn, w01, mx); \
         GFVAR(flags, sword##name, mn, w02, mx); \
@@ -350,12 +375,13 @@
         GFVAR(flags, smg##name, mn, w04, mx); \
         GFVAR(flags, flamer##name, mn, w05, mx); \
         GFVAR(flags, plasma##name, mn, w06, mx); \
-        GFVAR(flags, rifle##name, mn, w07, mx); \
-        GFVAR(flags, grenade##name, mn, w08, mx); \
-        GFVAR(flags, mine##name, mn, w09, mx); \
-        GFVAR(flags, rocket##name, mn, w10, mx); \
+        GFVAR(flags, zapper##name, mn, w07, mx); \
+        GFVAR(flags, rifle##name, mn, w08, mx); \
+        GFVAR(flags, grenade##name, mn, w09, mx); \
+        GFVAR(flags, mine##name, mn, w10, mx); \
+        GFVAR(flags, rocket##name, mn, w11, mx); \
         extern float *weap_stat_##name[];
-    #define WPFVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPFVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GFVAR(flags, melee##name##1, mn, w100, mx);   GFVAR(flags, melee##name##2, mn, w200, mx); \
         GFVAR(flags, pistol##name##1, mn, w101, mx);  GFVAR(flags, pistol##name##2, mn, w201, mx); \
         GFVAR(flags, sword##name##1, mn, w102, mx);   GFVAR(flags, sword##name##2, mn, w202, mx); \
@@ -363,12 +389,13 @@
         GFVAR(flags, smg##name##1, mn, w104, mx);     GFVAR(flags, smg##name##2, mn, w204, mx); \
         GFVAR(flags, flamer##name##1, mn, w105, mx);  GFVAR(flags, flamer##name##2, mn, w205, mx); \
         GFVAR(flags, plasma##name##1, mn, w106, mx);  GFVAR(flags, plasma##name##2, mn, w206, mx); \
-        GFVAR(flags, rifle##name##1, mn, w107, mx);   GFVAR(flags, rifle##name##2, mn, w207, mx);\
-        GFVAR(flags, grenade##name##1, mn, w108, mx); GFVAR(flags, grenade##name##2, mn, w208, mx); \
-        GFVAR(flags, mine##name##1, mn, w109, mx);    GFVAR(flags, mine##name##2, mn, w209, mx); \
-        GFVAR(flags, rocket##name##1, mn, w110, mx);  GFVAR(flags, rocket##name##2, mn, w210, mx); \
+        GFVAR(flags, zapper##name##1, mn, w107, mx);  GFVAR(flags, zapper##name##2, mn, w207, mx); \
+        GFVAR(flags, rifle##name##1, mn, w108, mx);   GFVAR(flags, rifle##name##2, mn, w208, mx);\
+        GFVAR(flags, grenade##name##1, mn, w109, mx); GFVAR(flags, grenade##name##2, mn, w209, mx); \
+        GFVAR(flags, mine##name##1, mn, w110, mx);    GFVAR(flags, mine##name##2, mn, w210, mx); \
+        GFVAR(flags, rocket##name##1, mn, w111, mx);  GFVAR(flags, rocket##name##2, mn, w211, mx); \
         extern float *weap_stat_##name[][2];
-    #define WPSVAR(flags, name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10) \
+    #define WPSVAR(flags, name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
         GSVAR(flags, melee##name, w00); \
         GSVAR(flags, pistol##name, w01); \
         GSVAR(flags, sword##name, w02); \
@@ -376,12 +403,13 @@
         GSVAR(flags, smg##name, w04); \
         GSVAR(flags, flamer##name, w05); \
         GSVAR(flags, plasma##name, w06); \
-        GSVAR(flags, rifle##name, w07); \
-        GSVAR(flags, grenade##name, w08); \
-        GSVAR(flags, mine##name, w09); \
-        GSVAR(flags, rocket##name, w10); \
+        GSVAR(flags, zapper##name, w07); \
+        GSVAR(flags, rifle##name, w08); \
+        GSVAR(flags, grenade##name, w09); \
+        GSVAR(flags, mine##name, w10); \
+        GSVAR(flags, rocket##name, w11); \
         extern char **weap_stat_##name[];
-    #define WPSVARM(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210) \
+    #define WPSVARM(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211) \
         GSVAR(flags, melee##name##1, w100);   GSVAR(flags, melee##name##2, w200); \
         GSVAR(flags, pistol##name##1, w101);  GSVAR(flags, pistol##name##2, w201); \
         GSVAR(flags, sword##name##1, w102);   GSVAR(flags, sword##name##2, w202); \
@@ -389,25 +417,28 @@
         GSVAR(flags, smg##name##1, w104);     GSVAR(flags, smg##name##2, w204); \
         GSVAR(flags, flamer##name##1, w105);  GSVAR(flags, flamer##name##2, w205); \
         GSVAR(flags, plasma##name##1, w106);  GSVAR(flags, plasma##name##2, w206); \
-        GSVAR(flags, rifle##name##1, w107);   GSVAR(flags, rifle##name##2, w207);\
-        GSVAR(flags, grenade##name##1, w108); GSVAR(flags, grenade##name##2, w208); \
-        GSVAR(flags, mine##name##1, w109);    GSVAR(flags, mine##name##2, w209); \
-        GSVAR(flags, rocket##name##1, w110);  GSVAR(flags, rocket##name##2, w210); \
+        GSVAR(flags, zapper##name##1, w107);  GSVAR(flags, zapper##name##2, w207); \
+        GSVAR(flags, rifle##name##1, w108);   GSVAR(flags, rifle##name##2, w208);\
+        GSVAR(flags, grenade##name##1, w109); GSVAR(flags, grenade##name##2, w209); \
+        GSVAR(flags, mine##name##1, w110);    GSVAR(flags, mine##name##2, w210); \
+        GSVAR(flags, rocket##name##1, w111);  GSVAR(flags, rocket##name##2, w211); \
         extern char **weap_stat_##name[][2];
 #endif
     #define W(weap,name)         (*weap_stat_##name[weap])
     #define W2(weap,name,second) (*weap_stat_##name[weap][second ? 1 : 0])
-    #define WSTR(a,weap,attr)    defformatstring(a)("%s%s", weaptype[weap].name, #attr)
 #endif
-#define WPVARK(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410) \
-    WPVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210); \
-    WPVARM(flags, flak##name, mn, mx, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410);
-#define WPFVARK(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410) \
-    WPFVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210); \
-    WPFVARM(flags, flak##name, mn, mx, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410);
-#define WPSVARK(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410) \
-    WPSVARM(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210); \
-    WPSVARM(flags, flak##name, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410);
+#define WPVARK(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w311, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410, w411) \
+    WPVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211); \
+    WPVARM(flags, flak##name, mn, mx, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w311, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410, w411);
+#define WPFVARK(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w311, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410, w411) \
+    WPFVARM(flags, name, mn, mx, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211); \
+    WPFVARM(flags, flak##name, mn, mx, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w311, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410, w411);
+#define WPSVARK(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w311, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410, w411) \
+    WPSVARM(flags, name, w100, w101, w102, w103, w104, w105, w106, w107, w108, w109, w110, w111, w200, w201, w202, w203, w204, w205, w206, w207, w208, w209, w210, w211); \
+    WPSVARM(flags, flak##name, w300, w301, w302, w303, w304, w305, w306, w307, w308, w309, w310, w311, w400, w401, w402, w403, w404, w405, w406, w407, w408, w409, w410, w411);
+#define WPSVARR(flags, name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11) \
+    WPSVARM(flags, name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11); \
+    WPSVARM(flags, flak##name, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11, w00, w01, w02, w03, w04, w05, w06, w07, w08, w09, w10, w11);
 #define WF(c,w,v,s) (c ? W2(w, flak##v, s) : W2(w, v, s))
 #define WS(flags)  (flags&HIT_ALT)
 #define WK(flags)  (flags&HIT_FLAK)
diff --git a/src/game/weapons.cpp b/src/game/weapons.cpp
index 9b0dc14..25518f2 100644
--- a/src/game/weapons.cpp
+++ b/src/game/weapons.cpp
@@ -2,7 +2,7 @@
 namespace weapons
 {
     VAR(IDF_PERSIST, autoreloading, 0, 2, 4); // 0 = never, 1 = when empty, 2 = weapons that don't add a full clip, 3 = always (+1 zooming weaps too)
-    VAR(IDF_PERSIST, autoreloaddelay, 0, 100, VAR_MAX);
+    VAR(IDF_PERSIST, autodelayreload, 0, 0, VAR_MAX);
 
     VAR(IDF_PERSIST, skipspawnweapon, 0, 0, 6); // skip spawnweapon; 0 = never, 1 = if numweaps > 1 (+1), 3 = if carry > 0 (+2), 6 = always
     VAR(IDF_PERSIST, skipmelee, 0, 7, 10); // skip melee; 0 = never, 1 = if numweaps > 1 (+2), 4 = if carry > 0 (+2), 7 = if carry > 0 and is offset (+2), 10 = always
@@ -11,7 +11,7 @@ namespace weapons
     VAR(IDF_PERSIST, skipmine, 0, 0, 10); // skip mine; 0 = never, 1 = if numweaps > 1 (+2), 4 = if carry > 0 (+2), 7 = if carry > 0 and is offset (+2), 10 = always
 
     int lastweapselect = 0;
-    VAR(IDF_PERSIST, weapselectdelay, 0, 150, VAR_MAX);
+    VAR(IDF_PERSIST, weapselectdelay, 0, 250, VAR_MAX);
     VARF(IDF_PERSIST, weapselectslot, 0, 1, 1, changedkeys = lastmillis); // 0 = by id, 1 = by slot
 
     int slot(gameent *d, int n, bool back)
@@ -32,7 +32,7 @@ namespace weapons
     ICOMMAND(0, weapslot, "i", (int *o), intret(slot(game::player1, *o >= 0 ? *o : game::player1->weapselect))); // -1 = weapselect slot
     ICOMMAND(0, weapselect, "", (), intret(game::player1->weapselect));
     ICOMMAND(0, ammo, "i", (int *n), intret(isweap(*n) ? game::player1->ammo[*n] : -1));
-    ICOMMAND(0, reloads, "i", (int *n), intret(isweap(*n) ? game::player1->reloads[*n] : -1));
+    ICOMMAND(0, reloadweap, "i", (int *n), intret(isweap(*n) && w_reload(*n, m_weapon(game::gamemode, game::mutators)) ? 1 : 0));
     ICOMMAND(0, hasweap, "ii", (int *n, int *o), intret(isweap(*n) && game::player1->hasweap(*n, *o) ? 1 : 0));
     ICOMMAND(0, getweap, "ii", (int *n, int *o), {
         if(isweap(*n)) switch(*o)
@@ -46,20 +46,19 @@ namespace weapons
 
     bool weapselect(gameent *d, int weap, int filter, bool local)
     {
-        if(game::intermission || (!local && (d == game::player1 || d->ai))) return false;
+        if(!gs_playing(game::gamestate)) return false;
         if(local)
         {
             int interrupts = filter;
             interrupts &= ~(1<<W_S_RELOAD);
             if(!d->canswitch(weap, m_weapon(game::gamemode, game::mutators), lastmillis, interrupts))
             {
-                if(!d->canswitch(weap, m_weapon(game::gamemode, game::mutators), lastmillis, (1<<W_S_RELOAD))) return false;
+                if(!d->canswitch(weap, m_weapon(game::gamemode, game::mutators), lastmillis, filter)) return false;
                 else if(!isweap(d->weapselect) || d->weapload[d->weapselect] <= 0) return false;
                 else
                 {
                     int offset = d->weapload[d->weapselect];
                     d->ammo[d->weapselect] = max(d->ammo[d->weapselect]-offset, 0);
-                    d->reloads[d->weapselect] = max(d->reloads[d->weapselect]-1, 0);
                     d->weapload[d->weapselect] = -d->weapload[d->weapselect];
                 }
             }
@@ -70,67 +69,71 @@ namespace weapons
         return true;
     }
 
-    bool weapreload(gameent *d, int weap, int load, int ammo, int reloads, bool local)
+    bool weapreload(gameent *d, int weap, int load, int ammo, bool local)
     {
-        if(game::intermission || (!local && (d == game::player1 || d->ai))) return false;
+        if(!gs_playing(game::gamestate) || (!local && (d == game::player1 || d->ai))) return false; // this can't be fixed until 1.5
         if(local)
         {
-            if(!d->canreload(weap, m_weapon(game::gamemode, game::mutators), false, lastmillis)) return false;
+            if(!d->canreload(weap, m_weapon(game::gamemode, game::mutators), false, lastmillis))
+            {
+                if(d->weapstate[weap] == W_S_POWER) d->setweapstate(weap, W_S_WAIT, 100, lastmillis);
+                return false;
+            }
             client::addmsg(N_RELOAD, "ri3", d->clientnum, lastmillis-game::maptime, weap);
             int oldammo = d->ammo[weap];
-            ammo = min(max(d->ammo[weap], 0) + W(weap, add), W(weap, max));
-            reloads = max(d->reloads[weap], 0) + 1;
+            ammo = min(max(d->ammo[weap], 0) + W(weap, ammoadd), W(weap, ammomax));
             load = ammo-oldammo;
         }
         d->weapload[weap] = load;
-        d->ammo[weap] = min(ammo, W(weap, max));
-        d->reloads[weap] = max(reloads, 0);
+        d->ammo[weap] = min(ammo, W(weap, ammomax));
         playsound(WSND(weap, S_W_RELOAD), d->o, d, 0, -1, -1, -1, &d->wschan);
-        d->setweapstate(weap, W_S_RELOAD, W(weap, reloaddelay), lastmillis);
+        d->setweapstate(weap, W_S_RELOAD, W(weap, delayreload), lastmillis);
         return true;
     }
 
     void weaponswitch(gameent *d, int a = -1, int b = -1)
     {
-        if(game::intermission || a < -1 || b < -1 || a >= W_MAX || b >= W_MAX) return;
+        if(!gs_playing(game::gamestate) || a < -1 || b < -1 || a >= W_MAX || b >= W_MAX) return;
         if(weapselectdelay && lastweapselect && totalmillis-lastweapselect < weapselectdelay) return;
-        if(!d->weapwaited(d->weapselect, lastmillis, G(weaponinterrupts))) return;
-        int s = slot(d, d->weapselect);
-        loopi(W_MAX) // only loop the amount of times we have weaps for
+        if(d->weapwaited(d->weapselect, lastmillis, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
         {
-            if(a >= 0) s = a;
-            else s += b;
-            while(s > W_MAX-1) s -= W_MAX;
-            while(s < 0) s += W_MAX;
+            int s = slot(d, d->weapselect);
+            loopi(W_MAX) // only loop the amount of times we have weaps for
+            {
+                if(a >= 0) s = a;
+                else s += b;
+                while(s > W_MAX-1) s -= W_MAX;
+                while(s < 0) s += W_MAX;
 
-            int n = slot(d, s, true);
-            if(a < 0)
-            { // weapon skipping when scrolling
-                int p = m_weapon(game::gamemode, game::mutators);
-                #define skipweap(q,w) \
-                { \
-                    if(q && n == w && (d->aitype >= AI_START || w != W_MELEE || p == W_MELEE || d->weapselect == W_MELEE)) switch(q) \
+                int n = slot(d, s, true);
+                if(a < 0)
+                { // weapon skipping when scrolling
+                    int p = m_weapon(game::gamemode, game::mutators);
+                    #define skipweap(q,w) \
                     { \
-                        case 10: continue; break; \
-                        case 7: case 8: case 9: if(d->carry(p, 5, w) > (q-7)) continue; break; \
-                        case 4: case 5: case 6: if(d->carry(p, 1, w) > (q-3)) continue; break; \
-                        case 1: case 2: case 3: if(d->carry(p, 0, w) > q) continue; break; \
-                        case 0: default: break; \
-                    } \
+                        if(q && n == w && (d->actortype >= A_ENEMY || w != W_MELEE || p == W_MELEE || d->weapselect == W_MELEE)) switch(q) \
+                        { \
+                            case 10: continue; break; \
+                            case 7: case 8: case 9: if(d->carry(p, 5, w) > (q-7)) continue; break; \
+                            case 4: case 5: case 6: if(d->carry(p, 1, w) > (q-3)) continue; break; \
+                            case 1: case 2: case 3: if(d->carry(p, 0, w) > q) continue; break; \
+                            case 0: default: break; \
+                        } \
+                    }
+                    skipweap(skipspawnweapon, p);
+                    skipweap(skipmelee, W_MELEE);
+                    skipweap(skippistol, W_PISTOL);
+                    skipweap(skipgrenade, W_GRENADE);
+                    skipweap(skipmine, W_MINE);
                 }
-                skipweap(skipspawnweapon, p);
-                skipweap(skipmelee, W_MELEE);
-                skipweap(skippistol, W_PISTOL);
-                skipweap(skipgrenade, W_GRENADE);
-                skipweap(skipmine, W_MINE);
-            }
 
-            if(weapselect(d, n, G(weaponinterrupts)))
-            {
-                lastweapselect = totalmillis;
-                return;
+                if(weapselect(d, n, (1<<W_S_SWITCH)|(1<<W_S_RELOAD)))
+                {
+                    lastweapselect = totalmillis;
+                    return;
+                }
+                else if(a >= 0) break;
             }
-            else if(a >= 0) break;
         }
         game::errorsnd(d);
     }
@@ -138,26 +141,35 @@ namespace weapons
 
     void weapdrop(gameent *d, int w)
     {
-        if(game::intermission) return;
+        if(!gs_playing(game::gamestate)) return;
         int weap = isweap(w) ? w : d->weapselect, sweap = m_weapon(game::gamemode, game::mutators);
         d->action[AC_DROP] = false;
-        if(d->candrop(weap, sweap, lastmillis, G(weaponinterrupts)))
+        if(!d->candrop(weap, sweap, lastmillis, m_loadout(game::gamemode, game::mutators), (1<<W_S_SWITCH)))
         {
-            client::addmsg(N_DROP, "ri3", d->clientnum, lastmillis-game::maptime, weap);
-            d->setweapstate(weap, W_S_WAIT, weaponswitchdelay, lastmillis);
-            return;
+            if(!d->candrop(weap, sweap, lastmillis, m_loadout(game::gamemode, game::mutators), (1<<W_S_SWITCH)|(1<<W_S_RELOAD)) || !isweap(d->weapselect) || d->weapload[d->weapselect] <= 0)
+            {
+                game::errorsnd(d);
+                return;
+            }
+            else
+            {
+                int offset = d->weapload[d->weapselect];
+                d->ammo[d->weapselect] = max(d->ammo[d->weapselect]-offset, 0);
+                d->weapload[d->weapselect] = -d->weapload[d->weapselect];
+            }
         }
-        game::errorsnd(d);
+        client::addmsg(N_DROP, "ri3", d->clientnum, lastmillis-game::maptime, weap);
+        d->setweapstate(weap, W_S_WAIT, weaponswitchdelay, lastmillis);
     }
 
     bool autoreload(gameent *d, int flags = 0)
     {
-        if(!game::intermission && d == game::player1 && W2(d->weapselect, sub, WS(flags)) && d->canreload(d->weapselect, m_weapon(game::gamemode, game::mutators), false, lastmillis))
+        if(gs_playing(game::gamestate) && d == game::player1 && W2(d->weapselect, ammosub, WS(flags)) && d->canreload(d->weapselect, m_weapon(game::gamemode, game::mutators), false, lastmillis))
         {
-            bool noammo = d->ammo[d->weapselect] < W2(d->weapselect, sub, WS(flags)),
+            bool noammo = d->ammo[d->weapselect] < W2(d->weapselect, ammosub, WS(flags)),
                  noattack = !d->action[AC_PRIMARY] && !d->action[AC_SECONDARY];
-            if((noammo || noattack) && !d->action[AC_USE] && d->weapstate[d->weapselect] == W_S_IDLE && (noammo || lastmillis-d->weaplast[d->weapselect] >= autoreloaddelay))
-                return autoreloading >= (noammo ? 1 : (W(d->weapselect, add) < W(d->weapselect, max) ? 2 : (W(d->weapselect, zooms) ? 4 : 3)));
+            if((noammo || noattack) && !d->action[AC_USE] && d->weapstate[d->weapselect] == W_S_IDLE && (noammo || lastmillis-d->weaplast[d->weapselect] >= autodelayreload))
+                return autoreloading >= (noammo ? 1 : (W(d->weapselect, ammoadd) < W(d->weapselect, ammomax) ? 2 : (W2(d->weapselect, cooked, true)&W_C_ZOOM ? 4 : 3)));
         }
         return false;
     }
@@ -190,12 +202,23 @@ namespace weapons
 
     float accmod(gameent *d, bool zooming, int *x)
     {
-        float r = 1; int y = 0;
-        if(inairspread && (physics::jetpack(d) || d->airmillis) && !d->onladder) { r += inairspread; y++; }
-        if(impulsespread > 0 && physics::pacing(d)) { r += impulsespread; y++; }
-        else if(movespread > 0 && (d->move || d->strafe)) { r += movespread; y++; }
-        else if(stillspread > 0 && !physics::sliding(d) && !physics::iscrouching(d) && !zooming) { r += stillspread; y++; }
-        if(x) *x = y;
+        float r = 1;
+        if(spreadinair > 0 && d->airmillis && !d->onladder)
+        {
+            if(x) (*x)++;
+            r += spreadinair;
+        }
+        bool running = d->running(moveslow) || d->sliding();
+        if((running && spreadrunning > 0 ? spreadrunning : spreadmoving) > 0 && (d->move || d->strafe || running))
+        {
+            if(x) (*x)++;
+            r += running ? spreadrunning : spreadmoving;
+        }
+        else if(spreadstill > 0 && !d->crouching() && !zooming)
+        {
+            if(x) (*x)++;
+            r += spreadstill;
+        }
         return r;
     }
 
@@ -214,33 +237,47 @@ namespace weapons
             else offset = d->weapload[weap];
         }
         float scale = 1;
-        int sub = W2(weap, sub, secondary), cooked = force;
-        if(W2(weap, power, secondary) && !W(weap, zooms))
+        bool zooming = (pressed && secondary && W2(weap, cooked, true)&W_C_ZOOM) || d->weapstate[weap] == W_S_ZOOM, wassecond = secondary;
+        if(zooming)
+        {
+            if(!pressed)
+            {
+                client::addmsg(N_SPHY, "ri4", d->clientnum, SPHY_COOK, W_S_IDLE, 0);
+                d->setweapstate(weap, W_S_IDLE, 0, lastmillis);
+                return false;
+            }
+            else secondary = zooming;
+        }
+        int sub = W2(weap, ammosub, secondary), cooked = force;
+        if(W2(weap, cooktime, secondary) || zooming)
         {
             float maxscale = 1;
             if(sub > 1 && d->ammo[weap] < sub) maxscale = d->ammo[weap]/float(sub);
-            int len = int(W2(weap, power, secondary)*maxscale);
+            int len = int(W2(weap, cooktime, secondary)*maxscale), type = zooming ? W_S_ZOOM : W_S_POWER;
             if(!cooked)
             {
-                if(d->weapstate[weap] != W_S_POWER)
+                if(d->weapstate[weap] != type)
                 {
                     if(pressed)
                     {
                         if(offset > 0)
                         {
                             d->ammo[weap] = max(d->ammo[weap]-offset, 0);
-                            d->reloads[weap] = max(d->reloads[weap]-1, 0);
                             d->weapload[weap] = -offset;
                         }
-                        client::addmsg(N_SPHY, "ri3", d->clientnum, SPHY_POWER, len);
-                        d->setweapstate(weap, W_S_POWER, len, lastmillis);
+                        client::addmsg(N_SPHY, "ri4", d->clientnum, SPHY_COOK, type, len);
+                        d->setweapstate(weap, type, len, lastmillis);
                     }
                     else return false;
                 }
-                cooked = clamp(lastmillis-d->weaplast[weap], 1, len);
-                if(pressed && cooked < len) return false;
+                cooked = len ? clamp(lastmillis-d->weaplast[weap], 1, len) : 1;
+                if(zooming)
+                {
+                    if(pressed && wassecond) return false;
+                }
+                else if(pressed && cooked < len) return false;
             }
-            scale = cooked/float(W2(weap, power, secondary));
+            scale = len ? cooked/float(W2(weap, cooktime, secondary)) : 1;
             if(sub > 1 && scale < 1) sub = int(ceilf(sub*scale));
         }
         else if(!pressed) return false;
@@ -253,52 +290,22 @@ namespace weapons
             s.id = d->getprojid(); \
             s.pos = ivec(int(p.x*DMF), int(p.y*DMF), int(p.z*DMF)); \
         }
+        int rays = W2(weap, rays, secondary);
+        if(rays > 1 && W2(weap, cooked, secondary)&W_C_RAYS && W2(weap, cooktime, secondary) && scale < 1)
+            rays = max(1, int(ceilf(rays*scale)));
         if(weaptype[weap].traced)
         {
             from = d->originpos(weap == W_MELEE, secondary);
             if(weap == W_MELEE) to = vec(targ).sub(from).normalize().mul(d->radius).add(from);
             else to = d->muzzlepos(weap, secondary);
-            int rays = W2(weap, rays, secondary);
-            if(rays > 1 && W2(weap, power, secondary) && scale < 1) rays = int(ceilf(rays*scale));
             loopi(rays) addshot(to);
         }
         else
         {
             from = d->muzzlepos(weap, secondary);
             to = targ;
-
-            vec unitv;
-            float dist = to.dist(from, unitv);
-            if(dist > 0) unitv.div(dist);
-            else vecfromyawpitch(d->yaw, d->pitch, 1, 0, unitv);
-
-            // move along the eye ray towards the weap origin, stopping when something is hit
-            // nudge the target a tiny bit forward in the direction of the target for stability
-
-            vec eyedir(from);
-            eyedir.sub(d->o);
-            float eyedist = eyedir.magnitude();
-            if(eyedist > 0) eyedir.div(eyedist);
-            float barrier = eyedist > 0 ? raycube(d->o, eyedir, eyedist, RAY_CLIPMAT) : eyedist;
-            if(barrier < eyedist)
-            {
-                (from = eyedir).mul(barrier).add(d->o);
-                (to = targ).sub(from).rescale(1e-3f).add(from);
-            }
-            else
-            {
-                barrier = raycube(from, unitv, dist, RAY_CLIPMAT|RAY_ALPHAPOLY);
-                if(barrier < dist)
-                {
-                    to = unitv;
-                    to.mul(barrier);
-                    to.add(from);
-                }
-            }
-
-            int rays = W2(weap, rays, secondary), x = 0;
-            if(rays > 1 && W2(weap, power, secondary) && scale < 1) rays = int(ceilf(rays*scale));
-            float m = accmod(d, W(d->weapselect, zooms) && secondary, &x);
+            int x = 0;
+            float m = accmod(d, W2(d->weapselect, cooked, true)&W_C_ZOOM && secondary, &x);
             float spread = WSP(weap, secondary, game::gamemode, game::mutators, m, x);
             loopi(rays)
             {
@@ -319,9 +326,9 @@ namespace weapons
     void shoot(gameent *d, vec &targ, int force)
     {
         if(!game::allowmove(d)) return;
-        bool secondary = physics::secondaryweap(d), alt = secondary && !W(d->weapselect, zooms);
-        if(doshot(d, targ, d->weapselect, d->action[alt ? AC_SECONDARY : AC_PRIMARY], secondary, force))
-            if(!W2(d->weapselect, fullauto, secondary)) d->action[alt ? AC_SECONDARY : AC_PRIMARY] = false;
+        bool secondary = physics::secondaryweap(d);
+        if(doshot(d, targ, d->weapselect, d->action[secondary ? AC_SECONDARY : AC_PRIMARY], secondary, force))
+            if(!W2(d->weapselect, fullauto, secondary)) d->action[secondary ? AC_SECONDARY : AC_PRIMARY] = false;
     }
 
     void preload()
diff --git a/src/game/weapons.h b/src/game/weapons.h
index b5a8f65..61a5ec6 100644
--- a/src/game/weapons.h
+++ b/src/game/weapons.h
@@ -1,27 +1,38 @@
 enum
 {
     W_MELEE = 0, W_PISTOL, W_OFFSET, // end of unselectable weapon set
-    W_SWORD = W_OFFSET, W_SHOTGUN, W_SMG, W_FLAMER, W_PLASMA, W_RIFLE, W_ITEM,
-    W_GRENADE = W_ITEM, W_MINE, W_BOOM, W_ROCKET = W_BOOM, // end of item weapon set
+    W_SWORD = W_OFFSET, W_SHOTGUN, W_SMG, W_FLAMER, W_PLASMA, W_ZAPPER, W_RIFLE, W_ITEM,
+    W_GRENADE = W_ITEM, W_MINE, W_ROCKET, // end of item weapon set
     W_MAX, W_LOADOUT = W_ITEM-W_OFFSET // if you add to this at all, check all arrays with W_MAX
 };
 #define isweap(a)       (a >= 0 && a < W_MAX)
 
 enum { W_F_NONE = 0, W_F_FORCED = 1<<0 };
 enum {
-    W_S_IDLE = 0, W_S_PRIMARY, W_S_SECONDARY, W_S_RELOAD, W_S_POWER,
-    W_S_SWITCH, W_S_USE, W_S_WAIT, W_S_MAX,
-    W_S_INTERRUPT = (1<<W_S_RELOAD)|(1<<W_S_SWITCH)|(1<<W_S_POWER),
-    W_S_ALL = (1<<W_S_PRIMARY)|(1<<W_S_SECONDARY)|(1<<W_S_RELOAD)|(1<<W_S_POWER)|(1<<W_S_SWITCH)|(1<<W_S_USE)|(1<<W_S_WAIT)
+    W_C_SCALE = 1<<0, W_C_SCALEN = 1<<1, W_C_LIFE = 1<<2, W_C_LIFEN = 1<<3,
+    W_C_SPEED = 1<<4, W_C_SPEEDN = 1<<5, W_C_RAYS = 1<<6, W_C_ZOOM = 1<<7,
+    W_C_ALL = W_C_SCALE|W_C_SCALEN|W_C_LIFE|W_C_LIFEN|W_C_SPEED|W_C_SPEEDN|W_C_RAYS|W_C_ZOOM,
+    W_C_SS = W_C_SCALE|W_C_SPEED, W_C_SZ = W_C_SCALE|W_C_ZOOM
+};
+enum {
+    W_N_STADD = 1<<0, W_N_GRADD = 1<<1, W_N_STIMM = 1<<2, W_N_GRIMM = 1<<3,
+    W_N_ADD = W_N_STADD|W_N_GRADD, W_N_IMM = W_N_STIMM|W_N_GRIMM,
+    W_N_ST = W_N_STADD|W_N_STIMM, W_N_GR = W_N_GRADD|W_N_GRIMM,
+    W_N_ALL = W_N_STADD|W_N_GRADD|W_N_STIMM|W_N_GRIMM
+};
+enum {
+    W_S_IDLE = 0, W_S_PRIMARY, W_S_SECONDARY, W_S_RELOAD, W_S_SWITCH, W_S_USE, W_S_POWER, W_S_ZOOM, W_S_WAIT, W_S_MAX,
+    W_S_EXCLUDE = (1<<W_S_IDLE)|(1<<W_S_POWER)|(1<<W_S_ZOOM)
 };
 
 enum
 {
     S_W_PRIMARY = 0, S_W_SECONDARY,
-    S_W_POWER, S_W_POWER2,
+    S_W_POWER, S_W_POWER2, S_W_ZOOM,
     S_W_SWITCH, S_W_RELOAD, S_W_NOTIFY,
     S_W_EXPLODE, S_W_EXPLODE2,
     S_W_DESTROY, S_W_DESTROY2,
+    S_W_IMPACT, S_W_IMPACT2,
     S_W_EXTINGUISH, S_W_EXTINGUISH2,
     S_W_TRANSIT, S_W_TRANSIT2,
     S_W_BOUNCE, S_W_BOUNCE2,
@@ -39,11 +50,12 @@ enum
     S_SMG       = S_SHOTGUN+S_W_MAX,
     S_FLAMER    = S_SMG+S_W_MAX,
     S_PLASMA    = S_FLAMER+S_W_MAX,
-    S_RIFLE     = S_PLASMA+S_W_MAX,
+    S_ZAPPER    = S_PLASMA+S_W_MAX,
+    S_RIFLE     = S_ZAPPER+S_W_MAX,
     S_GRENADE   = S_RIFLE+S_W_MAX,
     S_MINE      = S_GRENADE+S_W_MAX,
     S_ROCKET    = S_MINE+S_W_MAX,
-    S_MAX
+    S_MAX       = S_ROCKET+S_W_MAX
 };
 
 enum
@@ -56,7 +68,8 @@ enum
     COLLIDE_GEOM = IMPACT_GEOM|BOUNCE_GEOM,
     COLLIDE_PLAYER = IMPACT_PLAYER|BOUNCE_PLAYER,
     COLLIDE_SHOTS = IMPACT_SHOTS|BOUNCE_SHOTS,
-    COLLIDE_DYNENT = COLLIDE_PLAYER|COLLIDE_SHOTS
+    COLLIDE_DYNENT = COLLIDE_PLAYER|COLLIDE_SHOTS,
+    COLLIDE_ALL = COLLIDE_TRACE|COLLIDE_PROJ|COLLIDE_OWNER|IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|BOUNCE_GEOM|BOUNCE_PLAYER|BOUNCE_SHOTS|DRILL_GEOM|DRILL_PLAYER|DRILL_SHOTS|STICK_GEOM|STICK_PLAYER
 };
 
 enum
@@ -71,7 +84,7 @@ enum
 enum { WR_BURN = 0, WR_BLEED, WR_SHOCK, WR_MAX, WR_ALL = (1<<WR_BURN)|(1<<WR_BLEED)|(1<<WR_SHOCK) };
 
 struct shotmsg { int id; ivec pos; };
-struct hitmsg { int flags, proj, target, dist; ivec dir; };
+struct hitmsg { int flags, proj, target, dist; ivec dir, vel; };
 
 #define hithead(x)       (x&HIT_WHIPLASH || x&HIT_HEAD)
 #define hithurts(x)      (x&HIT_BURN || x&HIT_BLEED || x&HIT_SHOCK || x&HIT_EXPLODE || x&HIT_PROJ || x&HIT_MATERIAL)
@@ -90,27 +103,33 @@ struct hitmsg { int flags, proj, target, dist; ivec dir; };
 #include "weapdef.h"
 
 WPSVAR(0, name,
-    "melee",    "pistol",   "sword",    "shotgun",  "smg",      "flamer",   "plasma",   "rifle",    "grenade",  "mine",     "rocket"
-);
-WPVAR(0, add, 1, VAR_MAX,
-    1,          10,         1,          2,          50,         30,         16,         6,          1,          1,          1
+    "melee",    "pistol",   "sword",    "shotgun",  "smg",      "flamer",   "plasma",   "zapper",   "rifle",    "grenade",  "mine",     "rocket"
 );
 WPFVARM(0, aidist, 0, FVAR_MAX,
-    16.0f,      256.0f,     48.0f,      256.0f,     512.0f,     64.0f,      128.0f,     768.0f,     384.0f,     128.0f,     1024.0f,
-    16.0f,      256.0f,     48.0f,      512.0f,     96.0f,      128.0f,     64.0f,      2048.0f,    256.0f,     128.0f,     512.0f
+    16.0f,      512.0f,     48.0f,      64.0f,      512.0f,     64.0f,      512.0f,     256.f,      768.0f,     384.0f,     128.0f,     1024.0f,
+    16.0f,      256.0f,     48.0f,      128.0f,     128.0f,     64.0f,      64.0f,      256.f,      2048.0f,    256.0f,     128.0f,     512.0f
 );
 WPVARM(0, aiskew, 0, VAR_MAX,
-    1,          100,        1,          10,         20,         10,         50,         40,         5,          5,          10,
-    1,          100,        1,          10,         20,         10,         10,         40,         5,          5,          10
+    1,          2,          1,          3,          4,          3,          4,          2,          5,          1,          1,          5,
+    1,          2,          1,          3,          4,          3,          4,          2,          5,          1,          1,          5
 );
-WPVARM(0, attackdelay, 1, VAR_MAX,
-    500,        100,        600,        600,        100,        125,        350,        750,        750,        1000,       1500,
-    1000,       200,        1000,       1125,       400,        1000,       1500,       1250,       750,        1000,       1500
+WPVAR(0, ammoadd, 1, VAR_MAX,
+    1,          10,         1,          2,          40,         60,         25,         30,         6,          1,          1,          1
 );
-WPVAR(0, carried, 0, 1,
-    0,          0,          1,          1,          1,          1,          1,          1,          0,          0,          1
+WPVAR(0, ammomax, 1, VAR_MAX,
+    1,          10,          1,         8,          40,         60,         25,         30,         6,          2,          2,          1
 );
-WPVARK(0, collide, 0, VAR_MAX,
+WPVARM(0, ammosub, 0, VAR_MAX,
+    0,          1,          0,          1,          1,          1,          1,          1,          1,          1,          1,          1,
+    0,          1,          0,          2,          4,          12,         25,         5,          1,          1,          1,          1
+);
+WPFVARK(0, blend, 0, 1,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
+);
+WPVARK(0, collide, 0, COLLIDE_ALL,
     IMPACT_PLAYER|COLLIDE_TRACE,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_TRACE,
     BOUNCE_GEOM|IMPACT_PLAYER|COLLIDE_TRACE|IMPACT_SHOTS|DRILL_PLAYER,
@@ -118,6 +137,7 @@ WPVARK(0, collide, 0, VAR_MAX,
     BOUNCE_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_TRACE|COLLIDE_OWNER|DRILL_GEOM,
     BOUNCE_GEOM|IMPACT_PLAYER|COLLIDE_OWNER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER,
+    IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_TRACE,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_TRACE|DRILL_GEOM,
     BOUNCE_GEOM|BOUNCE_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_PROJ,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER|COLLIDE_PROJ,
@@ -128,8 +148,9 @@ WPVARK(0, collide, 0, VAR_MAX,
     BOUNCE_GEOM|IMPACT_PLAYER|COLLIDE_TRACE|IMPACT_SHOTS|DRILL_PLAYER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_TRACE|COLLIDE_OWNER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_TRACE|COLLIDE_OWNER|STICK_GEOM,
-    IMPACT_GEOM|IMPACT_PLAYER|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER,
+    BOUNCE_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER,
     IMPACT_GEOM|IMPACT_SHOTS|COLLIDE_OWNER|STICK_GEOM,
+    IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_TRACE|DRILL_PLAYER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_TRACE|DRILL_GEOM|DRILL_PLAYER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER|COLLIDE_PROJ,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER|COLLIDE_PROJ,
@@ -143,6 +164,7 @@ WPVARK(0, collide, 0, VAR_MAX,
     BOUNCE_GEOM|IMPACT_PLAYER|COLLIDE_OWNER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_TRACE,
+    IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_TRACE,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_PROJ,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER|COLLIDE_PROJ,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_PROJ,
@@ -152,419 +174,603 @@ WPVARK(0, collide, 0, VAR_MAX,
     BOUNCE_GEOM|IMPACT_PLAYER|COLLIDE_TRACE|IMPACT_SHOTS|DRILL_PLAYER|COLLIDE_OWNER,
     BOUNCE_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_TRACE|COLLIDE_OWNER,
     BOUNCE_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_TRACE|COLLIDE_OWNER,
-    IMPACT_GEOM|IMPACT_PLAYER|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER,
+    BOUNCE_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER,
     IMPACT_GEOM|IMPACT_SHOTS|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_TRACE|DRILL_PLAYER|COLLIDE_OWNER,
+    IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_TRACE|DRILL_PLAYER|COLLIDE_OWNER,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER|COLLIDE_PROJ,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|STICK_GEOM|STICK_PLAYER|COLLIDE_PROJ,
     IMPACT_GEOM|IMPACT_PLAYER|IMPACT_SHOTS|COLLIDE_OWNER|COLLIDE_PROJ
 );
 WPVAR(IDF_HEX, colour, 0, 0xFFFFFF,
-    0x707070,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   0xF02020,   0x40F0C8,   0xA020F0,   0x40F000,   0x00F068,   0x803000
+    0x606060,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   0xF02020,   0x40F0C8,   0xC090F0,   0xA020F0,   0x40F000,   0x00F068,   0x803000
 );
-WPVARM(0, cooked, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          0,          0,          0,          8,          8,          8,
-    0,          0,          0,          0,          0,          1,          33,         0,          8,          8,          8
+WPVARM(0, cooked, 0, W_C_ALL,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          W_C_LIFEN,  0,          0,
+    0,          0,          0,          0,          0,          0,          W_C_SS,     0,          W_C_SZ,     W_C_LIFEN,  0,          0
 );
-WPVARK(0, damage, VAR_MIN, VAR_MAX,
-    30,         25,         25,         15,         18,         4,          22,         45,         150,        150,        200,
-    40,         40,         50,         4,          4,          4,          10,         150,        150,        150,        200,
-    25,         25,         25,         15,         18,         4,          22,         10,         150,        150,        200,
-    10,         40,         50,         8,          10,         4,          10,         10,         150,        150,        200
+WPVARM(0, cooktime, 0, VAR_MAX,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          3000,       0,          0,
+    0,          0,          0,          0,          0,          0,          2000,       0,          500,        3000,       0,          0
 );
-WPFVARK(0, delta, 0, FVAR_MAX,
-    10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,
-    10.0f,      10.0f,      10.0f,      10.0f,      1000.0f,    10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,
-    10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,
-    10.0f,      10.0f,      10.0f,      10.0f,      1000.0f,    10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f
+WPVARK(0, damage, VAR_MIN, VAR_MAX,
+    30,         25,         30,         6,          12,         4,          18,         10,         33,         100,        100,        200,
+    40,         35,         65,         6,          12,         10,         10,         10,         100,        100,        100,        200,
+    30,         25,         30,         6,          12,         4,          18,         10,         10,         100,        100,        200,
+    40,         35,         65,         6,          6,          10,         10,         10,         10,         100,        100,        200
+);
+WPFVARK(0, damagehead, FVAR_MIN, FVAR_MAX,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.75f,      1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
+);
+WPFVARK(0, damagelegs, FVAR_MIN, FVAR_MAX,
+    0.3f,       0.325f,     0.3f,       0.3f,       0.25f,      0.25f,      0.25f,      0.25f,      0.3f,       0.2f,       0.2f,       0.2f,
+    0.6f,       0.325f,     0.3f,       0.3f,       0.25f,      1.0f,       0.25f,      0.25f,      0.3f,       0.2f,       0.2f,       0.2f,
+    0.3f,       0.325f,     0.3f,       0.3f,       0.25f,      0.25f,      0.25f,      0.25f,      0.3f,       0.2f,       0.2f,       0.2f,
+    0.6f,       0.325f,     0.3f,       0.3f,       0.25f,      0.25f,      0.25f,      0.25f,      0.3f,       0.2f,       0.2f,       0.2f
+);
+WPFVARK(0, damageself, FVAR_MIN, FVAR_MAX,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
+);
+WPFVARK(0, damageteam, FVAR_MIN, FVAR_MAX,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
+);
+WPFVARK(0, damagetorso, FVAR_MIN, FVAR_MAX,
+    0.5f,       0.5f,       0.55f,      0.6f,       0.5f,       0.5f,       0.4f,       0.4f,       0.5f,       0.4f,       0.4f,       0.4f,
+    0.75f,      0.6f,       0.55f,      0.6f,       0.5f,       1.5f,       0.4f,       0.4f,       0.5f,       0.4f,       0.4f,       0.4f,
+    0.5f,       0.5f,       0.55f,      0.6f,       0.5f,       0.5f,       0.4f,       0.4f,       0.5f,       0.4f,       0.4f,       0.4f,
+    0.75f,      0.6f,       0.55f,      0.6f,       0.5f,       1.5f,       0.4f,       0.4f,       0.5f,       0.4f,       0.4f,       0.4f
+);
+WPFVARK(0, damagepenalty, 0, 1,
+    1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          0,          1,
+    1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          0,          1,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
+);
+WPFVARK(0, damagewhiplash, FVAR_MIN, FVAR_MAX,
+    0.8f,       0.8f,       0.8f,       0.8f,       0.8f,       0.65f,      0.6f,       0.6f,       0.6f,       0.6f,       0.6f,       0.6f,
+    0.8f,       0.8f,       0.8f,       0.8f,       0.8f,       0.75f,      0.6f,       0.6f,       0.6f,       0.6f,       0.6f,       0.6f,
+    0.8f,       0.8f,       0.8f,       0.8f,       0.8f,       0.65f,      0.6f,       0.6f,       0.6f,       0.6f,       0.6f,       0.6f,
+    0.8f,       0.8f,       0.8f,       0.8f,       0.8f,       0.75f,      0.6f,       0.6f,       0.6f,       0.6f,       0.6f,       0.6f
+);
+WPVARM(0, delayattack, 1, VAR_MAX,
+    500,        130,        500,        800,        90,         100,        275,        135,        850,        1000,       1000,       1500,
+    1000,       200,        800,        1200,       600,        1250,       2000,       700,        850,        1000,       1000,       1500
+);
+WPVAR(0, delayreload, 0, VAR_MAX,
+    50,         1000,       50,         850,        1250,       1850,       1750,       1500,       1500,       800,        1400,       2200
+);
+WPVAR(0, disabled, 0, 1,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
 );
 WPVARK(0, drill, 0, VAR_MAX,
-    0,          0,          0,          2,          2,          0,          0,          2,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          8,          0,          0,          0,
-    0,          0,          0,          2,          2,          0,          0,          2,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          8,          0,          0,          0
+    0,          0,          0,          0,          2,          0,          0,          0,          2,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          8,          0,          0,          0,
+    0,          0,          0,          0,          2,          0,          0,          2,          2,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          8,          8,          0,          0,          0
 );
 WPFVARK(0, elasticity, 0, FVAR_MAX,
-    0.5f,       0.5f,       0.5f,       0.5f,       0.65f,      0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
-    0.5f,       0.5f,       0.5f,       0.5f,       0.45f,      0.35f,      0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
-    0.5f,       0.5f,       0.5f,       0.5f,       0.65f,      0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
-    0.5f,       0.5f,       0.5f,       0.5f,       0.45f,      0.35f,      0.5f,       0.5f,       0.5f,       0.5f,       0.5f
+    0.5f,       0.5f,       0.5f,       0.5f,       0.65f,      0.35f,      0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
+    0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
+    0.5f,       0.5f,       0.5f,       0.5f,       0.65f,      0.35f,      0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
+    0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f
 );
 WPVARM(0, escapedelay, 0, VAR_MAX,
-    200,        200,        200,        200,        200,        200,        200,        200,        200,        200,        200,
-    200,        200,        200,        200,        200,        200,        200,        200,        200,        200,        200
+    200,        200,        200,        200,        200,        200,        200,        200,        200,        200,        200,        200,
+    200,        200,        200,        200,        200,        200,        200,        200,        200,        200,        200,        200
 );
-WPVARK(IDF_HEX, explcol, -3, 0xFFFFFF,
-    -1,         -1,         0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   0xA020F0,   0x981808,   0x00F068,   0x981808,
-    -1,         -1,         0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   0xA020F0,   0x981808,   0x00F068,   0x981808,
-    -1,         -1,         0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   0x00F068,   0x981808,   0x00F068,   0x981808,
-    -1,         -1,         0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   0x00F068,   0x981808,   0x00F068,   0x981808
+WPVARK(IDF_HEX, explcol, -PULSE_MAX, 0xFFFFFF,
+    -1,         -1,         0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   -4,         0xA020F0,   -1,         -4,         -1,
+    -1,         -1,         0x4040F0,   0xF0F020,   0xF05820,   0x808080,   0x40F0C8,   -4,         0xA020F0,   -1,         -4,         -1,
+    -1,         -1,         0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   -4,         0xA020F0,   -1,         -4,         -1,
+    -1,         -1,         0x4040F0,   0xF0F020,   0xF05820,   0x808080,   0x40F0C8,   -4,         0xA020F0,   -1,         -4,         -1
 );
 WPFVARK(0, explode, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       8.0f,       10.0f,      8.0f,       86.0f,      32.0f,      128.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       12.0f,      48.0f,      0.0f,       86.0f,      32.0f,      128.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       8.0f,       10.0f,      4.0f,       86.0f,      32.0f,      128.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       12.0f,      48.0f,      8.0f,       86.0f,      32.0f,      128.0f
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       6.0f,       6.0f,       0.0f,       4.0f,       64.0f,      32.0f,      96.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       12.0f,      48.0f,      0.0f,       0.0f,       52.0f,      32.0f,      96.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       6.0f,       6.0f,       4.0f,       4.0f,       64.0f,      32.0f,      96.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       12.0f,      48.0f,      8.0f,       4.0f,       52.0f,      32.0f,      96.0f
 );
 WPVARK(0, extinguish, 0, 7,
-    2,          2,          2,          2,          2,          3,          1,          2,          2,          2,          2,
-    2,          2,          2,          2,          2,          3,          0,          2,          2,          2,          2,
-    2,          2,          2,          2,          2,          3,          1,          2,          2,          2,          2,
-    2,          2,          2,          2,          2,          3,          0,          2,          2,          2,          2
+    0,          2,          2,          2,          2,          3,          1,          1,          2,          2,          2,          2,
+    0,          2,          2,          2,          2,          2,          0,          1,          2,          2,          2,          2,
+    0,          2,          2,          2,          2,          3,          1,          1,          2,          2,          2,          2,
+    0,          2,          2,          2,          2,          2,          0,          1,          2,          2,          2,          2
+);
+WPVARK(0, fade, 0, 3,
+    1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          1,
+    1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          1,
+    1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          1,
+    1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          1,          1
+);
+WPFVARK(0, fadeat, 0, FVAR_MAX,
+    4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,
+    4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,
+    4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,
+    4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f
+);
+WPFVARK(0, fadecut, 0, FVAR_MAX,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
+);
+WPVARK(0, fadetime, 0, VAR_MAX,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
 );
 WPFVARM(0, fragjump, 0, 1,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
 );
 WPFVARM(0, fragoffset, 0, FVAR_MAX,
-    4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       1.0f,       4.0f,       1.0f,       2.0f,
-    4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       1.0f,       4.0f,       1.0f,       2.0f
+    4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       1.0f,       1.0f,       4.0f,       1.0f,       2.0f,
+    4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       4.0f,       1.0f,       1.0f,       4.0f,       1.0f,       2.0f
 );
-WPVARM(0, fragrays, 0, VAR_MAX,
-    5,          5,          5,          5,          5,          5,          5,          5,          30,         30,         30,
-    5,          5,          5,          20,         20,         5,          5,          5,          30,         10,         30
+WPVARM(0, fragrays, 1, MAXPARAMS,
+    5,          5,          5,          5,          5,          5,          5,          5,          5,          25,         30,         35,
+    5,          5,          5,          22,         30,         5,          5,          5,          5,          25,         10,         35
 );
-WPFVARM(0, fragrel, 0, FVAR_MAX,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       0.0f,       0.0f,
-    1.0f,       1.0f,       1.0f,       1.5f,       0.1f,       1.0f,       1.0f,       1.0f,       0.0f,       0.0f,       0.0f
+WPFVARM(0, fragrel, FVAR_MIN, FVAR_MAX,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       0.0f,       0.0f,
+    1.0f,       1.0f,       1.0f,       1.5f,       0.1f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       0.0f,       0.0f
 );
 WPFVARM(0, fragscale, FVAR_NONZERO, FVAR_MAX,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.5f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.5f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.5f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.5f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
 );
 WPFVARM(0, fragskew, 0, FVAR_MAX,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.5f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       1.0f
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.5f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       0.0f,       1.0f
 );
 WPVARM(0, fragspeed, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          200,        0,          0,          300,        750,        400,
-    0,          0,          0,          0,          250,        250,        0,          0,          300,        7500,       400
+    0,          0,          0,          0,          0,          200,        0,          0,          0,          250,        750,        400,
+    0,          0,          0,          0,          350,        250,        0,          0,          0,          250,        7500,       400
 );
-WPFVARM(0, fragspread, 0, FVAR_MAX,
-    1.0f,       1.0f,       1.0f,       1.0f,       0.2f,       0.1f,       1.0f,       0.25f,      1.0f,       0.5f,       1.0f,
-    1.0f,       1.0f,       1.0f,       0.2f,       0.5f,       0.1f,       1.0f,       0.25f,      1.0f,       0.1f,       1.0f
+WPFVARM(0, fragspeedmin, 0, FVAR_MAX,
+    50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      1.0f,       50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,
+    50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      1.0f,       50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f
 );
-WPVARM(0, fragdelay, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          0,          0,          0,          3,          5,          3,
-    0,          0,          0,          0,          3,          0,          0,          0,          3,          100,        3
+WPFVARM(0, fragspeedmax, 0, FVAR_MAX,
+    0.0f,       0.0f,       0.0f,       0.0f,        0.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,        0.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
+);
+WPFVARM(0, fragspread, 0, FVAR_MAX,
+    1.0f,       1.0f,       1.0f,       1.0f,       0.2f,       0.1f,       1.0f,       0.25f,      0.25f,      1.0f,       0.5f,       1.0f,
+    1.0f,       1.0f,       1.0f,       0.2f,       0.75f,      0.1f,       1.0f,       0.25f,      0.25f,      1.0f,       0.1f,       1.0f
 );
 WPVARM(0, fragtime, 1, VAR_MAX,
-    500,        500,        500,        250,        500,        1000,       500,        500,        1000,       1000,       1000,
-    500,        500,        500,        2000,       1000,       3000,       500,        500,        1000,       5000,       1000
+    500,        500,        500,        250,        500,        1000,       500,        500,        500,        1000,       1000,       1000,
+    500,        500,        500,        2000,       1000,       3000,       500,        500,        500,        1000,       5000,       1000
+);
+WPVARM(0, fragtimedelay, 0, VAR_MAX,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
+);
+WPVARM(0, fragtimeiter, 0, VAR_MAX,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          3,          5,          3,
+    0,          0,          0,          0,          3,          0,          0,          0,          0,          3,          100,        3
 );
 WPVARM(0, fragweap, -1, W_MAX*2-1,
-    -1,         -1,         -1,         -1,         -1,         -1,         -1,         -1,         W_SHOTGUN,  W_RIFLE,    W_SMG,
-    -1,         -1,         -1,         WZ(SHOTGUN),WZ(SMG),    -1,         -1,         -1,         W_SHOTGUN,  WZ(RIFLE),  W_SMG
+    -1,         -1,         -1,         -1,         -1,         -1,         -1,         -1,         -1,         W_SHOTGUN,  W_ZAPPER,   W_SMG,
+    -1,         -1,         -1,         WZ(SHOTGUN),WZ(SMG),    -1,         -1,         -1,         -1,         W_SHOTGUN,  WZ(ZAPPER), W_SMG
 );
 WPFVAR(0, frequency, 0, FVAR_MAX,
-    0.0f,       0.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       2.0f,       2.0f,       4.0f
+    0.0f,       0.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       2.0f,       2.0f,       4.0f
 );
 WPVARM(0, fullauto, 0, 1,
-    1,          0,          1,          0,          1,          1,          1,          0,          0,          0,          0,
-    1,          0,          1,          0,          1,          0,          0,          0,          0,          0,          0
+    1,          0,          1,          0,          1,          1,          1,          1,          0,          0,          0,          0,
+    1,          0,          1,          0,          1,          0,          0,          0,          0,          0,          0,          0
 );
 WPVARK(0, guided, 0, 6,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          1,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          1
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          1,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          1
 );
 WPVARK(0, guideddelay, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          100,        0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          100,        0,          0,          0,          0,          0,          0
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          100,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          100
 );
 WPFVARK(0, headmin, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       4.0f,       8.0f,       4.0f,       8.0f,       8.0f,       16.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       4.0f,       4.0f,       4.0f,       8.0f,       8.0f,       16.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       4.0f,       8.0f,       4.0f,       8.0f,       8.0f,       16.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       4.0f,       4.0f,       4.0f,       8.0f,       8.0f,       16.0f
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       4.0f,       4.0f,       4.0f,       4.0f,       8.0f,       8.0f,       8.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       4.0f,       4.0f,       4.0f,       4.0f,       8.0f,       8.0f,       8.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       4.0f,       4.0f,       4.0f,       4.0f,       8.0f,       8.0f,       8.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       4.0f,       4.0f,       4.0f,       4.0f,       8.0f,       8.0f,       8.0f
 );
 WPFVARK(0, hitpush, FVAR_MIN, FVAR_MAX,
-    100.0f,     35.0f,      50.0f,      50.0f,      50.0f,      5.0f,       20.0f,      25.0f,      250.0f,     0.0f,       500.0f,
-    200.0f,     75.0f,      100.0f,     25.0f,      50.0f,      10.0f,      -100.0f,    100.0f,     250.0f,     0.0f,       500.0f,
-    100.0f,     35.0f,      50.0f,      50.0f,      50.0f,      5.0f,       20.0f,      0.0f,       250.0f,     0.0f,       500.0f,
-    200.0f,     75.0f,      100.0f,     25.0f,      50.0f,      10.0f,      -100.0f,    0.0f,       250.0f,     0.0f,       500.0f
+    100.0f,     35.0f,      50.0f,      20.0f,      50.0f,      5.0f,       20.0f,      20.0f,      50.0f,      125.0f,     0.0f,       250.0f,
+    200.0f,     35.0f,      100.0f,     25.0f,      50.0f,      100.0f,     -75.0f,     20.0f ,     100.0f,     125.0f,     0.0f,       250.0f,
+    100.0f,     35.0f,      50.0f,      20.0f,      50.0f,      5.0f,       20.0f,      20.0f,      20.0f,      125.0f,     0.0f,       250.0f,
+    200.0f,     35.0f,      100.0f,     25.0f,      50.0f,      100.0f,     -75.0f,     20.0f,      20.0f,      125.0f,     0.0f,       250.0f
+);
+WPFVARK(0, hitvel, 0, FVAR_MAX,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
 );
 WPVARK(0, interacts, 0, 3,
-    0,          1,          0,          1,          1,          1,          1,          1,          3,          3,          1,
-    0,          1,          0,          3,          3,          1,          1,          1,          3,          3,          1,
-    0,          1,          0,          1,          1,          1,          1,          1,          3,          3,          1,
-    0,          1,          0,          3,          3,          1,          1,          1,          3,          3,          1
+    0,          1,          0,          1,          1,          1,          1,          1,          1,          3,          3,          1,
+    0,          1,          0,          3,          3,          1,          1,          1,          1,          3,          3,          1,
+    0,          1,          0,          1,          1,          1,          1,          1,          1,          3,          3,          1,
+    0,          1,          0,          3,          3,          1,          1,          1,          1,          3,          3,          1
 );
 WPFVARM(0, kickpush, FVAR_MIN, FVAR_MAX,
-    0.0f,       4.0f,       0.0f,       50.0f,      5.0f,       1.0f,       25.0f,      35.0f,      5.0f,       5.0f,       150.0f,
-    0.0f,       8.0f,       0.0f,       75.0f,      10.0f,      2.0f,       150.0f,     50.0f,      5.0f,       5.0f,       150.0f
+    0.0f,       4.0f,       -15.0f,     50.0f,      5.0f,       1.0f,       20.0f,      5.0f,       35.0f,      5.0f,       5.0f,       150.0f,
+    0.0f,       6.0f,       -30.0f,     75.0f,      25.0f,      5.0f,       150.0f,     50.0f,      50.0f,      5.0f,       5.0f,       150.0f
 );
 WPVAR(0, laser, 0, 1,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
 );
-WPFVARK(0, legdamage, FVAR_MIN, FVAR_MAX,
-    0.3f,       0.325f,     0.3f,       0.3f,       0.3f,       0.25f,      0.2f,       0.2f,       0.2f,       0.2f,       0.2f,
-    0.6f,       0.325f,     0.3f,       0.3f,       0.3f,       0.25f,      0.2f,       0.2f,       0.2f,       0.2f,       0.2f,
-    0.3f,       0.325f,     0.3f,       0.3f,       0.3f,       0.25f,      0.2f,       0.2f,       0.2f,       0.2f,       0.2f,
-    0.6f,       0.325f,     0.3f,       0.3f,       0.3f,       0.25f,      0.2f,       0.2f,       0.2f,       0.2f,       0.2f
+WPVARK(IDF_HEX, lightcol, -PULSE_MAX, 0xFFFFFF,
+    0xEEEE22,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   -4,         0xA020F0,   -1,         0x00F068,   -1,
+    0xEEEE22,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   0xF0F0F0,   0x40F0C8,   -4,         0xA020F0,   -1,         0x00F068,   -1,
+    0xEEEE22,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   -4,         0xA020F0,   -1,         0x00F068,   -1,
+    0xEEEE22,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   0xF0F0F0,   0x40F0C8,   -4,         0xA020F0,   -1,         0x00F068,   -1
 );
-WPVARM(0, limspeed, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          35,         0,          0,          0,          0
+WPVAR(0, lightpersist, 0, 1,
+    0,          0,          1,          0,          0,          1,          0,          1,          0,          0,          0,          0
 );
-WPVAR(0, max, 1, VAR_MAX,
-    1,          10,         1,          10,         50,         30,         16,         6,          2,          2,          1
+WPFVAR(0, lightradius, 0, FVAR_MAX,
+    0,          8,          8,          8,          8,          8,          16,         4,          16,         16,         16,         16
 );
-WPFVARK(0, minspeed, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       25.0f,      25.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       25.0f,      25.0f,      25.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,
-    50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      50.0f
+WPFVARK(0, liquidcoast, 0, FVAR_MAX,
+    0.0f,       2.0f,       0.0f,       2.0f,       2.0f,       1.0f,       1.0f,       1.0f,       2.0f,       2.0f,       2.0f,       2.0f,
+    0.0f,       2.0f,       0.0f,       2.0f,       2.0f,       1.0f,       1.0f,       1.0f,       2.0f,       2.0f,       2.0f,       2.0f,
+    0.0f,       2.0f,       0.0f,       2.0f,       2.0f,       1.0f,       1.0f,       1.0f,       2.0f,       2.0f,       2.0f,       2.0f,
+    0.0f,       2.0f,       0.0f,       2.0f,       2.0f,       1.0f,       1.0f,       1.0f,       2.0f,       2.0f,       2.0f,       2.0f
 );
 WPVAR(0, modes, -G_ALL, G_ALL,
-    0,          -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW
+    0,          -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW,      -G_SW
 );
 WPVAR(0, muts, -G_M_ALL, G_M_ALL,
-    0,          -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    0,          -G_M_IM,    -G_M_DK
-);
-WPVARK(IDF_HEX, partcol, -3, 0xFFFFFF,
-    0xEEEE22,   0x666611,   0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   0xA020F0,   -1,         0x00F068,   -1,
-    0xEEEE22,   0x666611,   0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   0xA020F0,   -1,         0x00F068,   -1,
-    0xEEEE22,   0x666611,   0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   0x00F068,   -1,         0x00F068,   -1,
-    0xEEEE22,   0x666611,   0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   0x00F068,   -1,         0x00F068,   -1
+    0,          -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    -G_M_SW,    0,          -G_M_IM,    -G_M_DK
+);
+WPSVARR(0, obitsuicide,
+    "hit themself",
+    "ate a bullet",
+    "created too much torsional stress",
+    "tested the effectiveness of their own shrapnel",
+    "fell victim to their own crossfire",
+    "spontaneously combusted",
+    "was caught up in their own plasma-filled mayhem",
+    "tried to use themself as a circuit breaker",
+    "got a good shock",
+    "kicked it, kamikaze style",
+    "kicked it, kamikaze style",
+    "exploded with style"
+);
+WPSVARR(0, obitobliterated,
+    "given kung-fu lessons",
+    "skewered",
+    "sliced in half",
+    "turned into little chunks",
+    "swiss-cheesed",
+    "barbequed",
+    "reduced to ooze",
+    "shocked relentlessly",
+    "given laser shock treatment",
+    "turned into shrapnel",
+    "turned into shrapnel",
+    "obliterated"
+);
+WPSVARR(0, obitheadless,
+    "given kung-fu lessons",
+    "capped",
+    "sliced in half",
+    "scrambled",
+    "air conditioned",
+    "char-grilled",
+    "plasmafied",
+    "electrocuted",
+    "expertly sniped",
+    "blown to pieces",
+    "blown to pieces",
+    "exploded"
+);
+WPSVARK(0, obituary,
+    "punched",
+    "pierced",
+    "impaled",
+    "sprayed with buckshot",
+    "riddled with holes",
+    "char-grilled",
+    "plasmified",
+    "electrocuted",
+    "laser shocked",
+    "blown to pieces",
+    "blown to pieces",
+    "exploded",
+
+    "kicked",
+    "pierced",
+    "impaled",
+    "filled with lead",
+    "spliced apart",
+    "snuffed out",
+    "shown the light",
+    "shocked into submission",
+    "given laser burn",
+    "blown to pieces",
+    "blown to pieces",
+    "exploded",
+
+    "given kung-fu lessons",
+    "picked to pieces",
+    "melted in half",
+    "filled with shrapnel",
+    "air-conditioned",
+    "cooked alive",
+    "melted alive",
+    "turned into a lightning rod",
+    "shocked to pieces",
+    "turned into shrapnel",
+    "turned into shrapnel",
+    "obliterated",
+
+    "given kung-fu lessons",
+    "picked to pieces",
+    "melted in half",
+    "filled with shrapnel",
+    "air-conditioned",
+    "cooked alive",
+    "melted alive",
+    "turned into a lightning rod",
+    "shocked to pieces",
+    "turned into shrapnel",
+    "turned into shrapnel",
+    "obliterated"
+);
+WPFVARK(0, partblend, 0, 1,
+    1.0f,       0.3f,       1.0f,       1.0f,       1.0f,       0.8f,       0.8f,       1.0f,       1.0f,       1.0f,       0.75f,      1.0f,
+    1.0f,       0.6f,       1.0f,       1.0f,       1.0f,       0.15f,      1.0f,       1.0f,       1.0f,       1.0f,       0.75f,      1.0f,
+    1.0f,       0.3f,       1.0f,       1.0f,       1.0f,       0.8f,       0.8f,       1.0f,       1.0f,       1.0f,       0.75f,      1.0f,
+    1.0f,       0.6f,       1.0f,       1.0f,       1.0f,       0.15f,      1.0f,       1.0f,       1.0f,       1.0f,       0.75f,      1.0f
+);
+WPVARK(IDF_HEX, partcol, -PULSE_MAX, 0xFFFFFF,
+    0xEEEE22,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   -4,         0xA020F0,   -1,         0x00F068,   -1,
+    0xEEEE22,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   0xF0F0F0,   0x40F0C8,   -4,         0xA020F0,   -1,         0x00F068,   -1,
+    0xEEEE22,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   -1,         0x40F0C8,   -4,         0xA020F0,   -1,         0x00F068,   -1,
+    0xEEEE22,   0xC0C0C0,   0x4040F0,   0xF0F020,   0xF05820,   0xF0F0F0,   0x40F0C8,   -4,         0xA020F0,   -1,         0x00F068,   -1
+);
+WPVARK(0, partfade, 1, VAR_MAX,
+    500,        250,        500,        250,        250,        200,        500,        100,        500,        500,        500,        750,
+    500,        250,        500,        250,        250,        500,        500,        200,        950,        500,        500,        750,
+    500,        250,        500,        250,        250,        200,        500,        250,        500,        500,        500,        750,
+    500,        250,        500,        250,        250,        500,        500,        500,        750,        500,        500,        750
 );
 WPFVARK(0, partlen, 0, FVAR_MAX,
-    0.0f,       10.0f,      0.0f,       25.0f,      30.0f,      0.0f,       0.0f,       256.0f,     0.0f,       4.0f,       0.0f,
-    0.0f,       15.0f,      0.0f,       15.0f,      15.0f,      5.0f,       0.0f,       1024.0f,    0.0f,       4.0f,       0.0f,
-    0.0f,       10.0f,      0.0f,       7.5f,       7.5f,       0.0f,       0.0f,       256.0f,     0.0f,       4.0f,       0.0f,
-    0.0f,       15.0f,      0.0f,       7.5f,       7.5f,       5.0f,       0.0f,       1024.0f,    0.0f,       4.0f,       0.0f
+    0.0f,       8.0f,       0.0f,       30.0f,      20.0f,      0.0f,       0.0f,       1024.f,     256.0f,     0.0f,       4.0f,       0.0f,
+    0.0f,       16.0f,      0.0f,       15.0f,      15.0f,      0.0f,       0.0f,       1024.f,     1024.0f,    0.0f,       4.0f,       0.0f,
+    0.0f,       8.0f,       0.0f,       7.5f,       7.5f,       0.0f,       0.0f,       1024.f,     256.0f,     0.0f,       4.0f,       0.0f,
+    0.0f,       16.0f,      0.0f,       7.5f,       7.5f,       0.0f,       0.0f,       1024.f,     512.0f,     0.0f,       4.0f,       0.0f
 );
 WPFVARK(0, partsize, 0, FVAR_MAX,
-    1.0f,       2.0f,       1.0f,       0.65f,      0.5f,       10.0f,      8.0f,       2.0f,       1.0f,       2.0f,       3.0f,
-    2.0f,       2.5f,       1.25f,      0.45f,      0.35f,      10.0f,      24.0f,      3.0f,       1.0f,       2.0f,       3.0f,
-    1.0f,       2.0f,       1.0f,       0.35f,      0.35f,      10.0f,      8.0f,       0.75f,      1.0f,       2.0f,       3.0f,
-    2.0f,       2.5f,       1.25f,      0.35f,      0.35f,      10.0f,      24.0f,      4.0f,       1.0f,       2.0f,       3.0f
+    1.0f,       0.125f,     1.0f,       0.75f,      0.6f,       6.0f,       6.0f,       2.0f,       1.5f,       1.0f,       2.0f,       1.0f,
+    2.0f,       0.25f,      1.25f,      0.45f,      0.4f,       12.0f,      28.0f,      3.0f,       3.0f,       1.0f,       2.0f,       1.0f,
+    1.0f,       0.125f,     1.0f,       0.45f,      0.4f,       6.0f,       6.0f,       3.0f,       1.5f,       1.0f,       2.0f,       1.0f,
+    2.0f,       0.25f,      1.25f,      0.45f,      0.4f,       12.0f,      28.0f,      5.0f,       3.0f,       1.0f,       2.0f,       1.0f
 );
 WPVARK(0, parttype, 0, W_MAX-1,
-    W_MELEE,    W_PISTOL,   W_SWORD,    W_SHOTGUN,  W_SMG,      W_FLAMER,   W_PLASMA,   W_RIFLE,    W_GRENADE,  W_MINE,     W_ROCKET,
-    W_MELEE,    W_PISTOL,   W_SWORD,    W_SHOTGUN,  W_SMG,      W_FLAMER,   W_PLASMA,   W_RIFLE,    W_GRENADE,  W_MINE,     W_ROCKET,
-    W_MELEE,    W_PISTOL,   W_SWORD,    W_SHOTGUN,  W_SMG,      W_FLAMER,   W_PLASMA,   W_RIFLE,    W_GRENADE,  W_MINE,     W_ROCKET,
-    W_MELEE,    W_PISTOL,   W_SWORD,    W_SHOTGUN,  W_SMG,      W_FLAMER,   W_PLASMA,   W_RIFLE,    W_GRENADE,  W_MINE,     W_ROCKET
-);
-WPVARM(0, projdelay, 0, VAR_MAX,
-    0,          0,          10,         0,          0,          0,          0,          0,          75,         75,         0,
-    0,          0,          10,         0,          0,          25,         75,         0,          75,         75,         0
-);
-WPVARM(0, power, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          0,          0,          0,          3000,       0,          3000,
-    0,          0,          0,          0,          0,          500,        2000,       0,          3000,       0,          3000
+    W_MELEE,    W_PISTOL,   W_SWORD,    W_SHOTGUN,  W_SMG,      W_FLAMER,   W_PLASMA,   W_ZAPPER,   W_RIFLE,    W_GRENADE,  W_MINE,     W_ROCKET,
+    W_MELEE,    W_PISTOL,   W_SWORD,    W_SHOTGUN,  W_SMG,      W_FLAMER,   W_PLASMA,   W_ZAPPER,   W_RIFLE,    W_GRENADE,  W_MINE,     W_ROCKET,
+    W_MELEE,    W_PISTOL,   W_SWORD,    W_SHOTGUN,  W_SMG,      W_FLAMER,   W_PLASMA,   W_ZAPPER,   W_RIFLE,    W_GRENADE,  W_MINE,     W_ROCKET,
+    W_MELEE,    W_PISTOL,   W_SWORD,    W_SHOTGUN,  W_SMG,      W_FLAMER,   W_PLASMA,   W_ZAPPER,   W_RIFLE,    W_GRENADE,  W_MINE,     W_ROCKET
 );
 WPVARK(0, proxdelay, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          3000,       0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          1500000,    0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          3000,       0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          1500000,    0
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          3000,       0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          1500000,    0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          3000,       0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          1500000,    0
 );
 WPFVARK(0, proxdist, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       32.0f,      0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       FVAR_MAX,   0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       32.0f,      0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       FVAR_MAX,   0.0f
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       32.0f,      0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       FVAR_MAX,   0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       32.0f,      0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       FVAR_MAX,   0.0f
 );
 WPVARK(0, proxtime, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          100,        0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          100,        0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          100,        0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          100,        0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
 );
 WPVARK(0, proxtype, 0, 2,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          1,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          2,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          1,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          2,          0
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          1,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          2,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          1,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          2,          0
 );
 WPVARK(0, radial, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          20,         10,         0,          0,          0,          0,
-    0,          0,          0,          0,          0,          20,         80,         0,          0,          0,          0,
-    0,          0,          0,          0,          0,          20,         10,         0,          0,          0,          0,
-    0,          0,          0,          0,          0,          20,         80,         0,          0,          0,          0
+    0,          0,          0,          0,          0,          50,         50,         0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          50,         100,        0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          50,         50,         0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          50,         100,        0,          0,          0,          0,          0
 );
 WPFVARK(0, radius, FVAR_NONZERO, FVAR_MAX,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.5f,       2.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.5f,       2.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.5f,       2.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.5f,       2.0f,       1.0f,       1.0f,       1.0f,       1.0f
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
 );
-WPVARM(0, rays, 0, VAR_MAX,
-    1,          1,          1,          10,         1,          1,          1,          1,          1,          1,          1,
-    1,          1,          1,          1,          1,          4,          1,          1,          1,          1,          1
+WPVARM(0, rays, 0, MAXPARAMS,
+    1,          1,          1,          22,         1,          1,          1,          1,          1,          1,          1,          1,
+    1,          1,          1,          1,          1,          1,          1,          5,          1,          1,          1,          1
 );
 WPFVARK(0, reflectivity, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
 );
 WPFVARM(0, relativity, 0, FVAR_MAX,
-    0.0f,       0.05f,      0.0f,       0.05f,      0.05f,      0.95f,      0.1f,       1.0f,       1.0f,       1.0f,       0.0f,
-    0.0f,       0.05f,      0.0f,       0.75f,      0.05f,      0.5f,       0.1f,       0.0f,       1.0f,       1.0f,       0.0f
-);
-WPVAR(0, reloaddelay, 0, VAR_MAX,
-    50,         1000,       50,         750,        2000,       1750,       2000,       2500,       2000,       3000,       5000
-);
-WPVAR(0, reloads, -1, VAR_MAX,
-    -1,         -1,         -1,         -1,         -1,         -1,         -1,         -1,         0,          0,          0
+    0.0f,       0.05f,      0.0f,       0.15f,      0.15f,      0.95f,      0.15f,      0.0f,       0.5f,       0.75f,      0.5f,       0.0f,
+    0.0f,       0.05f,      0.0f,       0.35f,      0.25f,      0.15f,      0.15f,      0.0f,       0.1f,       0.75f,      0.5f,       0.0f
 );
 WPVARK(0, residual, 0, WR_ALL,
-    0,          0,          WR(BLEED),  0,          0,          WR(BURN),   0,          0,          WR(BURN),   WR(SHOCK),  WR(BURN),
-    0,          0,          WR(BLEED),  WR(BLEED),  0,          WR(BURN),   0,          0,          WR(BURN),   WR(SHOCK),  WR(BURN),
-    0,          0,          WR(BLEED),  0,          0,          WR(BURN),   0,          WR(SHOCK),  WR(BURN),   WR(SHOCK),  WR(BURN),
-    0,          0,          WR(BLEED),  WR(BLEED),  0,          WR(BURN),   0,          WR(SHOCK),  WR(BURN),   WR(SHOCK),  WR(BURN)
+    0,          0,          WR(BLEED),  0,          0,          WR(BURN),   0,          WR(SHOCK),  0,          WR(BURN),   WR(SHOCK),  WR(BURN),
+    0,          0,          WR(BLEED),  WR(BLEED),  0,          0,          0,          WR(SHOCK),  0,          WR(BURN),   WR(SHOCK),  WR(BURN),
+    0,          0,          WR(BLEED),  0,          0,          WR(BURN),   0,          WR(SHOCK),  0,          WR(BURN),   WR(SHOCK),  WR(BURN),
+    0,          0,          WR(BLEED),  WR(BLEED),  0,          0,          0,          WR(SHOCK),  0,          WR(BURN),   WR(SHOCK),  WR(BURN)
 );
-WPFVARK(0, selfdamage, FVAR_MIN, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.5f,       0.5f,       0.3f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
-    0.0f,       0.0f,       0.0f,       0.5f,       0.5f,       0.3f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
-    0.0f,       0.0f,       0.0f,       0.5f,       0.5f,       0.3f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f,
-    0.0f,       0.0f,       0.0f,       0.5f,       0.5f,       0.3f,       0.5f,       0.5f,       0.5f,       0.5f,       0.5f
+WPVARK(0, residualundo, 0, WR_ALL,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          WR(BURN),   0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          WR(BURN),   0,          0,          0,          0,          0,          0
 );
 WPVARM(0, speed, 0, VAR_MAX,
-    0,          1750,       0,          1250,       2500,       400,        1250,       10000,      250,        100,        1000,
-    0,          1000,       0,          250,        500,        200,        85,         100000,     250,        100,        250
+    0,          1500,       0,          1000,       2000,       400,        1200,       15000,       10000,      200,        100,        1000,
+    0,          1000,       0,          250,        750,        750,        85,         100000,       100000,     200,        100,        250
+);
+WPFVARK(0, speeddelta, 0, FVAR_MAX,
+    10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,
+    10.0f,      10.0f,      10.0f,      10.0f,      1000.0f,    10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,
+    10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,
+    10.0f,      10.0f,      10.0f,      10.0f,      1000.0f,    10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f,      10.0f
+);
+WPVARM(0, speedlimit, 0, VAR_MAX,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          35,         0,          0,          0,          0,          0
+);
+WPFVARK(0, speedmin, 0, FVAR_MAX,
+    0.0f,       0.0f,       0.0f,       25.0f,      25.0f,      1.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       25.0f,      25.0f,      1.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      1.0f,       50.0f,      0.0f,       50.0f,      50.0f,      50.0f,      50.0f,
+    50.0f,      50.0f,      50.0f,      50.0f,      50.0f,      1.0f,       50.0f,      0.0f,       50.0f,      50.0f,      50.0f,      50.0f
+);
+WPFVARK(0, speedmax, 0, FVAR_MAX,
+    0.0f,       0.0f,       0.0f,       0.0f,        0.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,        0.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,        0.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,        0.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
 );
 WPFVARM(0, spread, 0, FVAR_MAX,
-    1.0f,       1.0f,       1.0f,       6.0f,       3.0f,       5.0f,       2.0f,       0.1f,       1.0f,       1.0f,       1.0f,
-    1.0f,       2.0f,       1.0f,       3.0f,       3.0f,       20.0f,      1.0f,       0.0f,       1.0f,       1.0f,       1.0f
+    1.0f,       1.0f,       1.0f,       12.0f,      2.5f,       7.5f,       2.0f,       2.0f,       1.5f,       1.0f,       1.0f,       1.0f,
+    1.0f,       2.0f,       1.0f,       2.0f,       2.0f,       1.0f,       1.0f,       3.0f,       0.25f,      1.0f,       1.0f,       1.0f
 );
 WPFVARM(0, spreadmax, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
 );
 WPFVARM(0, spreadmin, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       16.0f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
 );
 WPFVARM(0, spreadz, 0, FVAR_MAX,
-    1.0f,       2.0f,       1.0f,       1.0f,       1.0f,       0.0f,       2.0f,       1.0f,       0.0f,       0.0f,       0.0f,
-    1.0f,       4.0f,       1.0f,       4.0f,       2.0f,       1.0f,       1.0f,       1.0f,       0.0f,       0.0f,       0.0f
+    1.0f,       2.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       2.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    1.0f,       2.0f,       1.0f,       4.0f,       2.0f,       4.0f,       1.0f,       1.0f,       0.0f,       0.0f,       0.0f,       0.0f
+);
+WPVARK(0, stun, 0, W_N_ALL,
+    W_N_ADD,    W_N_ADD,    W_N_ALL,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,
+    W_N_ADD,    W_N_ADD,    W_N_ALL,    W_N_ADD,    W_N_ADD,    W_N_ALL,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,
+    W_N_ADD,    W_N_ADD,    W_N_ALL,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ALL,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD,
+    W_N_ADD,    W_N_ADD,    W_N_ALL,    W_N_ADD,    W_N_ADD,    W_N_ALL,    W_N_ALL,    W_N_ALL,    W_N_ADD,    W_N_ADD,    W_N_ADD,    W_N_ADD
 );
 WPFVARK(0, stunfall, 0, FVAR_MAX,
-    0,          0,          0,          0,          0,          0,          0.5f,       0,          0,          8.0f,       0,
-    0,          0,          0,          0,          0,          0,          1.0f,       0,          0,          16.0f,      0,
-    0,          0,          0,          0,          0,          0,          0.5f,       8.0f,       0,          8.0f,       0,
-    0,          0,          0,          0,          0,          0,          1.0f,       16.0f,      0,          16.0f,      0
+    0.0f,       0.0f,       0.1f,       0.0f,       0.0f,       0.0f,       0.1f,       0.01f,      0.1f,       0.0f,       8.0f,       0.2f,
+    0.0f,       0.0f,       0.1f,       0.0f,       0.0f,       1.5f,       0.5f,       0.01f,      1.0f,       0.0f,       16.0f,      0.1f,
+    0.0f,       0.0f,       0.1f,       0.0f,       0.0f,       0.0f,       0.1f,       8.0f,       8.0f,       0.0f,       8.0f,       0.2f,
+    0.0f,       0.0f,       0.1f,       0.0f,       0.0f,       1.5f,       0.5f,       16.0f,      16.0f,      0.0f,       16.0f,      0.1f
 );
 WPFVARK(0, stunscale, 0, FVAR_MAX,
-    0.5f,       0.25f,      1.0f,       1.0f,       0.5f,       0.0f,       0.5f,       0.5f,       2.0f,       8.0f,       4.0f,
-    1.0f,       0.5f,       2.0f,       0.5f,       1.0f,       0.0f,       1.0f,       1.0f,       2.0f,       16.0f,      4.0f,
-    0.5f,       0.25f,      1.0f,       1.0f,       0.5f,       0.0f,       0.5f,       8.0f,       2.0f,       8.0f,       4.0f,
-    1.0f,       0.5f,       2.0f,       0.5f,       1.0f,       0.0f,       1.0f,       16.0f,      2.0f,       16.0f,      4.0f
+    0.5f,       0.25f,      2.0f,       0.25f,      0.5f,       0.0f,       0.2f,       0.5f,       0.5f,       2.0f,       8.0f,       16.0f,
+    1.0f,       0.25f,      4.0f,       0.35f,      1.0f,       0.15f,      0.5f,       0.5f,       1.0f,       2.0f,       16.0f,      8.0f,
+    0.5f,       0.25f,      2.0f,       0.25f,      0.5f,       0.0f,       0.2f,       8.0f,       8.0f,       2.0f,       8.0f,       16.0f,
+    1.0f,       0.25f,      4.0f,       0.35f,      1.0f,       0.15f,      0.5f,       16.0f,      16.0f,      2.0f,       16.0f,      8.0f
 );
 WPVARK(0, stuntime, 0, VAR_MAX,
-    100,        25,         200,        100,        100,        0,          100,        75,         200,        500,        500,
-    200,        50,         200,        100,        200,        0,          200,        200,        200,        750,        500,
-    100,        25,         200,        100,        100,        0,          100,        500,        200,        500,        500,
-    200,        50,         200,        100,        200,        0,          200,        750,        200,        750,        500
-);
-WPVARM(0, sub, 0, VAR_MAX,
-    0,          1,          0,          1,          1,          1,          1,          1,          1,          1,          1,
-    0,          2,          0,          2,          5,          5,          16,         1,          1,          1,          1
+    100,        25,         300,        75,         75,         0,          0,          100,        75,         200,        500,        750,
+    200,        25,         500,        150,        150,        500,        200,        100,        200,        200,        750,        500,
+    100,        25,         300,        75,         75,         0,          0,          500,        500,        200,        500,        750,
+    200,        25,         500,        150,        150,        500,        200,        750,        750,        200,        750,        500
 );
 WPVARK(0, taper, 0, 6,
-    0,          0,          0,          0,          0,          1,          1,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          1,          2,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          1,          1,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          1,          2,          0,          0,          0,          0
+    0,          0,          0,          2,          2,          2,          2,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          2,          0,          0,          0,          0,          0,
+    0,          0,          0,          2,          2,          2,          2,          0,          0,          0,          0,          0,
+    0,          0,          0,          2,          2,          2,          2,          0,          0,          0,          0,          0
 );
 WPFVARK(0, taperin, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.00625f,   0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.00625f,   0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.1f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
 );
 WPFVARK(0, taperout, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f
-);
-WPFVARK(0, teamdamage, FVAR_MIN, FVAR_MAX,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
-);
-WPFVARK(0, teampenalty, 0, 1,
-    1,          1,          1,          1,          1,          1,          1,          1,          0,          0,          0,
-    1,          1,          1,          1,          1,          1,          0,          1,          0,          0,          0,
-    1,          1,          1,          1,          1,          1,          1,          0,          0,          0,          0,
-    1,          1,          1,          0,          0,          0,          0,          0,          0,          0,          0
+    0.0f,       0.0f,       0.0f,       0.01f,      0.01f,      0.01f,      0.01f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       0.5f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.01f,      0.01f,      0.01f,      0.01f,      0.0f,       0.0f,       0.0f,       0.0f,       0.0f,
+    0.0f,       0.0f,       0.0f,       0.01f,      0.01f,      0.01f,      0.5f,       0.0f,       0.0f,       0.0f,       0.0f,       0.0f
 );
 WPVARM(0, time, 1, VAR_MAX,
-    100,        2000,       350,        450,        550,        200,        600,        5000,       3000,       60000,      5000,
-    350,        1000,       500,        5000,       2000,       2000,       5000,       5000,       3000,       30000,      5000
+    100,        2000,       350,        260,        510,        200,        350,        80,         5000,       3000,       60000,      5000,
+    350,        1000,       500,        2500,       1000,       100,        3500,       80,         5000,       3000,       30000,      5000
+);
+WPVARM(0, timedelay, 0, VAR_MAX,
+    0,          0,          10,         0,          0,          0,          0,          0,          0,          75,         75,         0,
+    0,          0,          10,         0,          0,          0,          75,         0,          0,          75,         75,         0
 );
-WPFVARK(0, torsodamage, FVAR_MIN, FVAR_MAX,
-    0.5f,       0.65f,      0.65f,      0.6f,       0.6f,       0.45f,      0.4f,       0.4f,       0.4f,       0.4f,       0.4f,
-    0.8f,       0.65f,      0.65f,      0.6f,       0.6f,       0.45f,      0.4f,       0.4f,       0.4f,       0.4f,       0.4f,
-    0.5f,       0.65f,      0.65f,      0.6f,       0.6f,       0.45f,      0.4f,       0.4f,       0.4f,       0.4f,       0.4f,
-    0.8f,       0.65f,      0.65f,      0.6f,       0.6f,       0.45f,      0.4f,       0.4f,       0.4f,       0.4f,       0.4f
+WPVARM(0, timeiter, 0, VAR_MAX,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          80,         0,          0,          0,          0
 );
 WPFVARM(0, trace, 0, FVAR_MAX,
-    2.0f,       1.0f,       2.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    2.0f,       1.0f,       2.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
+    2.0f,       1.0f,       1.5f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    2.0f,       1.0f,       1.5f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
 );
 WPFVARK(0, visfade, 0, 1,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
-    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,
+    1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f,       1.0f
 );
 WPVARK(0, vistime, 0, VAR_MAX,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
-    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
-);
-WPFVARK(0, waterfric, 0, FVAR_MAX,
-    0.0f,       2.0f,       0.0f,       2.0f,       2.0f,       1.0f,       1.0f,       2.0f,       2.0f,       2.0f,       2.0f,
-    0.0f,       2.0f,       0.0f,       2.0f,       2.0f,       1.0f,       1.0f,       2.0f,       2.0f,       2.0f,       2.0f,
-    0.0f,       2.0f,       0.0f,       2.0f,       2.0f,       1.0f,       1.0f,       2.0f,       2.0f,       2.0f,       2.0f,
-    0.0f,       2.0f,       0.0f,       2.0f,       2.0f,       1.0f,       1.0f,       2.0f,       2.0f,       2.0f,       2.0f
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,
+    0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0,          0
 );
 WPFVARK(0, wavepush, 0, FVAR_MAX,
-    1.5f,       1.5f,       1.5f,       1.5f,       1.5f,       0.0f,       1.5f,       1.5f,       2.0f,       2.0f,       4.0f,
-    1.5f,       1.5f,       1.5f,       1.5f,       1.5f,       0.0f,       2.0f,       1.5f,       2.0f,       2.0f,       4.0f,
-    1.5f,       1.5f,       1.5f,       1.5f,       1.5f,       0.0f,       1.5f,       1.5f,       2.0f,       2.0f,       4.0f,
-    1.5f,       1.5f,       1.5f,       1.5f,       1.5f,       0.0f,       2.0f,       1.5f,       2.0f,       2.0f,       4.0f
-);
-WPFVARK(0, weight, 0, FVAR_MAX,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       200.0f,     0.0f,       0.0f,       65.0f,      150.0f,     0.0f,
-    0.0f,       0.0f,       0.0f,       250.0f,     0.0f,       100.0f,     0.0f,       0.0f,       65.0f,      150.0f,     0.0f,
-    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       200.0f,     0.0f,       0.0f,       65.0f,      150.0f,     0.0f,
-    0.0f,       0.0f,       0.0f,       250.0f,     0.0f,       100.0f,     0.0f,       0.0f,       65.0f,      150.0f,     0.0f
-);
-WPFVARK(0, whipdamage, FVAR_MIN, FVAR_MAX,
-    0.8f,       0.8f,       0.8f,       0.8f,       0.8f,       0.65f,      0.6f,       0.6f,       0.6f,       0.6f,       0.6f,
-    0.8f,       0.8f,       0.8f,       0.8f,       0.8f,       0.65f,      0.6f,       0.6f,       0.6f,       0.6f,       0.6f,
-    0.8f,       0.8f,       0.8f,       0.8f,       0.8f,       0.65f,      0.6f,       0.6f,       0.6f,       0.6f,       0.6f,
-    0.8f,       0.8f,       0.8f,       0.8f,       0.8f,       0.65f,      0.6f,       0.6f,       0.6f,       0.6f,       0.6f
-);
-WPVAR(0, zooms, 0, 1,
-    0,          0,          0,          0,          0,          0,          0,          1,          0,          0,          0
+    1.5f,       1.5f,       1.5f,       1.5f,       1.5f,       0.0f,       1.5f,       2.5f,       2.5f,       2.0f,       2.0f,       3.0f,
+    1.5f,       1.5f,       1.5f,       1.5f,       1.5f,       2.5f,       2.5f,       2.5f,       1.5f,       2.0f,       2.0f,       3.0f,
+    1.5f,       1.5f,       1.5f,       1.5f,       1.5f,       0.0f,       1.5f,       1.5f,       1.5f,       2.0f,       2.0f,       3.0f,
+    1.5f,       1.5f,       1.5f,       1.5f,       1.5f,       2.5f,       2.5f,       2.5f,       2.5f,       2.0f,       2.0f,       3.0f
+);
+WPFVARK(0, weight, FVAR_MIN, FVAR_MAX,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       -25.0f,     0.0f,       0.0f,       0.0f,       75.0f,      150.0f,     0.0f,
+    0.0f,       0.0f,       0.0f,       250.0f,     0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       75.0f,      150.0f,     0.0f,
+    0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       -25.0f,     0.0f,       0.0f,       0.0f,       75.0f,      150.0f,     0.0f,
+    0.0f,       0.0f,       0.0f,       250.0f,     0.0f,       0.0f,       0.0f,       0.0f,       0.0f,       75.0f,      150.0f,     0.0f
 );
 
-#define WRS(a,b,c,d)         (a*(m_limited(c, d) ? G(b##limited) : G(b##scale)))
-#define WX(k,a,b,c,d,e,f)    (!m_insta(d, e) || m_loadout(d, e) || a != W_RIFLE ? WRS(WF(k, a, b, c)*f, radial, d, e) : 0)
-#define WSP(a,b,c,d,e,f)     (!m_insta(c, d) || m_loadout(c, d) || a != W_RIFLE ? clamp(max(W2(a, spread, b), f*0.5f)*e, W2(a, spreadmin, b), W2(a, spreadmax, b) > 0 ? W2(a, spreadmax, b) : FVAR_MAX) : 0.f)
-#define WSND(a,b)            (weaptype[a].sound+b)
-#define WSNDF(a,b)           (weaptype[a].sound+(b ? S_W_SECONDARY : S_W_PRIMARY))
-#define WSND2(a,b,c)         (weaptype[a].sound+(b ? c+1 : c))
-#define WUSE(a)              (W(a, reloads) != 0 ? W(a, max) : W(a, add))
-#define WHCOL(d,a,b,c)       (W2(a, b, c) >= 0 ? W2(a, b, c) : game::hexpulsecolour(d, clamp(-1-W2(a, b, c), 0, 2), 50))
-#define WPCOL(d,a,b,c)       (W2(a, b, c) >= 0 ? vec::hexcolor(W2(a, b, c)) : game::pulsecolour(d, clamp(-1-W2(a, b, c), 0, 2), 50))
+#define WRS(a,b,c,d)         ((a)*(m_sweaps(c, d) ? G(b##limited) : G(b##scale)))
+#define WX(k,a,b,c,d,e,f)    (!m_insta(d, e) || (a) != W_RIFLE ? WRS(WF(k, a, b, c)*f, radial, d, e) : 0)
+#define WSP(a,b,c,d,e,f)     (!m_insta(c, d) || (a) != W_RIFLE ? clamp(max(W2(a, spread, b), f*0.5f)*(e), W2(a, spreadmin, b), W2(a, spreadmax, b) > 0 ? W2(a, spreadmax, b) : FVAR_MAX) : 0.f)
+#define WSND(a,b)            (weaptype[a].sound+(b))
+#define WSNDF(a,b)           (weaptype[a].sound+((b) ? S_W_SECONDARY : S_W_PRIMARY))
+#define WSND2(a,b,c)         (weaptype[a].sound+((b) ? (c)+1 : (c)))
+#define WUSE(a)              (a < W_ITEM ? W(a, ammomax) : W(a, ammoadd))
+#define WHCOL(d,a,b,c)       (W2(a, b, c) >= 0 ? W2(a, b, c) : game::hexpulsecolour(d, clamp(-1-W2(a, b, c), 0, int(PULSE_LAST)), 50))
+#define WPCOL(d,a,b,c)       (W2(a, b, c) >= 0 ? vec::hexcolor(W2(a, b, c)) : game::pulsecolour(d, clamp(-1-W2(a, b, c), 0, int(PULSE_LAST)), 50))
 
 struct weaptypes
 {
     int     anim,               sound,      espeed;
-    bool    melee,      traced,     muzzle,     eject;
+    bool    melee,      traced,     muzzle,     eject,      tape;
     float   thrown[2],              halo,       esize;
     const char *name,   *item,                      *vwep, *hwep,                     *proj,                  *eprj;
 };
@@ -573,72 +779,78 @@ weaptypes weaptype[] =
 {
     {
             ANIM_MELEE,         S_MELEE,    1,
-            true,       true,       false,      false,
+            true,       true,       false,      false,      false,
             { 0, 0 },               1,          0,
-            "melee",    "",                         "", "",                        "",                     ""
+            "melee",    "", "", "", "", ""
     },
     {
             ANIM_PISTOL,        S_PISTOL,   10,
-            false,      false,      true,       true,
+            false,      false,      true,       true,       false,
             { 0, 0 },               8,          0.35f,
-            "pistol",   "weapons/pistol/item",      "weapons/pistol/vwep", "weapons/pistol/hwep",     "",                     "projectiles/cartridge"
+            "pistol", "weapons/pistol/item", "weapons/pistol/vwep", "weapons/pistol/hwep", "weapons/pistol/proj", "projectiles/cartridge"
     },
     {
             ANIM_SWORD,         S_SWORD,    1,
-            true,       true,       true,       false,
+            true,       true,       true,       false,      true,
             { 0, 0 },               14,         0,
-            "sword",    "weapons/sword/item",       "weapons/sword/vwep", "weapons/sword/hwep",    "",                     ""
+            "sword", "weapons/sword/item", "weapons/sword/vwep", "weapons/sword/hwep", "", ""
     },
     {
             ANIM_SHOTGUN,       S_SHOTGUN,  10,
-            false,      false,      true,       true,
+            false,      false,      true,       true,       false,
             { 0, 0 },               12,         0.45f,
-            "shotgun",  "weapons/shotgun/item",     "weapons/shotgun/vwep", "weapons/shotgun/hwep",    "",                     "projectiles/shell"
+            "shotgun", "weapons/shotgun/item", "weapons/shotgun/vwep", "weapons/shotgun/hwep", "", "projectiles/shell"
     },
     {
             ANIM_SMG,           S_SMG,      20,
-            false,      false,      true,       true,
+            false,      false,      true,       true,       false,
             { 0, 0 },               10,         0.35f,
-            "smg",      "weapons/smg/item",         "weapons/smg/vwep", "weapons/smg/hwep",        "",                     "projectiles/cartridge"
+            "smg", "weapons/smg/item", "weapons/smg/vwep", "weapons/smg/hwep", "", "projectiles/cartridge"
     },
     {
             ANIM_FLAMER,        S_FLAMER,   1,
-            false,      false,      true,       true,
+            false,      false,      true,       true,       false,
             { 0, 0 },               12,         0,
-            "flamer",   "weapons/flamer/item",      "weapons/flamer/vwep", "weapons/flamer/hwep",     "",                     ""
+            "flamer", "weapons/flamer/item", "weapons/flamer/vwep", "weapons/flamer/hwep", "", ""
     },
     {
             ANIM_PLASMA,        S_PLASMA,   1,
-            false,      false,      true,       false,
+            false,      false,      true,       false,      false,
+            { 0, 0 },               8,         0,
+            "plasma", "weapons/plasma/item", "weapons/plasma/vwep", "weapons/plasma/hwep", "", ""
+    },
+    {
+            ANIM_ZAPPER,        S_ZAPPER,   1,
+            false,      false,      true,       false,      true,
             { 0, 0 },               10,         0,
-            "plasma",   "weapons/plasma/item",      "weapons/plasma/vwep", "weapons/plasma/hwep",     "",                     ""
+            "zapper", "weapons/zapper/item", "weapons/zapper/vwep", "weapons/zapper/hwep", "", ""
     },
     {
             ANIM_RIFLE,         S_RIFLE,    1,
-            false,      false,      true,       false,
+            false,      false,      true,       false,      false,
             { 0, 0 },               12,         0,
-            "rifle",    "weapons/rifle/item",       "weapons/rifle/vwep", "weapons/rifle/hwep",      "",                     ""
+            "rifle", "weapons/rifle/item", "weapons/rifle/vwep", "weapons/rifle/hwep", "", ""
     },
     {
             ANIM_GRENADE,       S_GRENADE,  1,
-            false,      false,      false,      false,
+            false,      false,      false,      false,      false,
             { 0.0625f, 0.0625f },   6,          0,
-            "grenade",  "weapons/grenade/item",     "weapons/grenade/vwep", "weapons/grenade/hwep",    "weapons/grenade/proj", ""
+            "grenade", "weapons/grenade/item", "weapons/grenade/vwep", "weapons/grenade/hwep", "weapons/grenade/proj", ""
     },
     {
             ANIM_MINE,          S_MINE,     1,
-            false,      false,      false,      false,
+            false,      false,      false,      false,      false,
             { 0.0625f, 0.0625f },   6,          0,
-            "mine",     "weapons/mine/item",        "weapons/mine/vwep",    "weapons/mine/hwep",        "weapons/mine/proj", ""
+            "mine", "weapons/mine/item", "weapons/mine/vwep", "weapons/mine/hwep", "weapons/mine/proj", ""
     },
     {
             ANIM_ROCKET,        S_ROCKET,   1,
-            false,      false,      true,      false,
+            false,      false,      true,      false,       false,
             { 0, 0 },               10,          0,
-            "rocket",   "weapons/rocket/item",       "weapons/rocket/vwep", "weapons/rocket/hwep",     "weapons/rocket/proj",  ""
+            "rocket", "weapons/rocket/item", "weapons/rocket/vwep", "weapons/rocket/hwep", "weapons/rocket/proj",  ""
     }
 };
-SVAR(0, weapname, "melee pistol sword shotgun smg flamer plasma rifle grenade mine rocket");
+SVAR(0, weapname, "melee pistol sword shotgun smg flamer plasma zapper rifle grenade mine rocket");
 VAR(0, weapidxmelee, 1, W_MELEE, -1);
 VAR(0, weapidxpistol, 1, W_PISTOL, -1);
 VAR(0, weapidxsword, 1, W_SWORD, -1);
@@ -646,6 +858,7 @@ VAR(0, weapidxshotgun, 1, W_SHOTGUN, -1);
 VAR(0, weapidxsmg, 1, W_SMG, -1);
 VAR(0, weapidxflamer, 1, W_FLAMER, -1);
 VAR(0, weapidxplasma, 1, W_PLASMA, -1);
+VAR(0, weapidxzapper, 1, W_ZAPPER, -1);
 VAR(0, weapidxrifle, 1, W_RIFLE, -1);
 VAR(0, weapidxgrenade, 1, W_GRENADE, -1);
 VAR(0, weapidxmine, 1, W_MINE, -1);
diff --git a/src/install/nix/redeclipse.appdata.xml b/src/install/nix/redeclipse.appdata.xml
new file mode 100644
index 0000000..b52cd7b
--- /dev/null
+++ b/src/install/nix/redeclipse.appdata.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<application>
+  <id type="desktop">redeclipse.desktop</id>
+  <metadata_license>CC-BY-SA-3.0+</metadata_license>
+  <project_license>ZLIB and CC-BY-SA-3.0+</project_license>
+  <description>
+    <p>
+      Red Eclipse is a fun-filled new take on the casual first person arena
+      shooter with a general theme of agility in a variety of environments.
+    </p>
+    <p>
+      Red Eclipse features parkour, impulse boosts, dashing, other tricks,
+      and favourite game modes with an array of mutators and variables.
+      A builtin editor lets you create your own maps cooperatively online.
+    </p>
+  </description>
+  <url type="homepage">http://redeclipse.net/</url>
+  <screenshots>
+    <screenshot type="default">http://redeclipse.net/bits/images/003.jpg</screenshot>
+    <screenshot>http://redeclipse.net/bits/images/006.jpg</screenshot>
+    <screenshot>http://redeclipse.net/bits/images/007.jpg</screenshot>
+  </screenshots>
+  <updatecontact>http://redeclipse.net/forum/</updatecontact>
+</application>
diff --git a/src/install/nix/redeclipse.desktop.am b/src/install/nix/redeclipse.desktop.am
index e3215c5..f10ba37 100644
--- a/src/install/nix/redeclipse.desktop.am
+++ b/src/install/nix/redeclipse.desktop.am
@@ -2,24 +2,22 @@
 Type=Application
 Version=1.0
 Name=Red Eclipse
-Name[de]=Roter Eklipse
-Name[es]=Eclipse Rojo
-Name[gl]=Eclipse Vermello
-Name[it]=Eclisse Rosso
-Name[pt]=Eclipse Vermelho
 GenericName=First-person shooter game
 GenericName[es]=Juego de tiros en primera persona
 GenericName[gl]=Xogo de tiros en primeira persoa
 GenericName[it]=Sparatutto in prima persona
 GenericName[pt]=Jogo de tiros em primeira pessoa
 GenericName[sv]=Förstapersonsskjutare
-Comment=First-person shooter with agile gameplay and built-in editor.
-Comment[de]=Ego-Shooter Spiel mit agilen Gameplay und integriertem Editor.
-Comment[es]=Juego de tiros en primera persona con jugabilidad ágil y editor incorporado.
-Comment[gl]=Xogo de tiros en primeira persoa con xogo áxil e editor incorporado.
-Comment[it]=Sparatutto in prima persona con agile gameplay ed editor incorporato.
-Comment[pt]=Jogo de tiros em primeira pessoa com jogabilidade ágil e editor incorporado.
-Comment[sv]=Förstapersonsskjutare med rörlig spelstil och inbyggd baneditor.
-Icon="@APPNAME@"
-Exec="@APPNAME@"
+GenericName[fr]=Jeu de tir à la première personne
+Comment=First-person shooter with agile gameplay and built-in editor
+Comment[de]=Ego-Shooter-Spiel mit agilem Gameplay und integriertem Editor
+Comment[es]=Juego de tiros en primera persona con jugabilidad ágil y editor incorporado
+Comment[gl]=Xogo de tiros en primeira persoa con xogo áxil e editor incorporado
+Comment[it]=Sparatutto in prima persona con agile gameplay ed editor incorporato
+Comment[pt]=Jogo de tiros em primeira pessoa com jogabilidade ágil e editor incorporado
+Comment[sv]=Förstapersonsskjutare med rörlig spelstil och inbyggd baneditor
+Comment[fr]=Jeu de tir à la première personne avec un éditeur de cartes intégré
+Icon=@APPNAME@
+Exec=@APPNAME@
 Categories=Game;ActionGame;
+Keywords=fps,action,multiplayer,play,arena,mapeditor
diff --git a/src/redeclipse.cbp b/src/redeclipse.cbp
index 7ca5997..dc38b90 100644
--- a/src/redeclipse.cbp
+++ b/src/redeclipse.cbp
@@ -21,6 +21,7 @@
 				<Option projectLibDirsRelation="1" />
 				<Compiler>
 					<Add option="-fomit-frame-pointer" />
+					<Add option="-ffast-math" />
 					<Add option="-O3" />
 					<Add option="-Wall" />
 					<Add option="-m32" />
@@ -28,7 +29,6 @@
 					<Add option="-fsigned-char" />
 					<Add option="-fno-exceptions" />
 					<Add option="-fno-rtti" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -73,7 +73,6 @@
 					<Add option="-m32" />
 					<Add option="-mwindows" />
 					<Add option="-fsigned-char" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -118,7 +117,6 @@
 					<Add option="-mwindows" />
 					<Add option="-fsigned-char" />
 					<Add option="-finline-functions" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -158,6 +156,7 @@
 				<Option projectLibDirsRelation="1" />
 				<Compiler>
 					<Add option="-fomit-frame-pointer" />
+					<Add option="-ffast-math" />
 					<Add option="-O3" />
 					<Add option="-Wall" />
 					<Add option="-m32" />
@@ -166,7 +165,6 @@
 					<Add option="-fno-exceptions" />
 					<Add option="-fno-rtti" />
 					<Add option="-DSTANDALONE" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -207,7 +205,6 @@
 					<Add option="-mwindows" />
 					<Add option="-fsigned-char" />
 					<Add option="-DSTANDALONE" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -234,7 +231,7 @@
 				<Option object_output=".objs/redeclipse-amd64" />
 				<Option deps_output=".deps/redeclipse-amd64" />
 				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
+				<Option compiler="gcc" />
 				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
@@ -243,6 +240,7 @@
 				<Option projectLibDirsRelation="1" />
 				<Compiler>
 					<Add option="-fomit-frame-pointer" />
+					<Add option="-ffast-math" />
 					<Add option="-O3" />
 					<Add option="-Wall" />
 					<Add option="-m64" />
@@ -250,7 +248,6 @@
 					<Add option="-fsigned-char" />
 					<Add option="-fno-exceptions" />
 					<Add option="-fno-rtti" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -282,7 +279,7 @@
 				<Option object_output=".objs/redeclipse-amd64-dbg" />
 				<Option deps_output=".deps/redeclipse-amd64-dbg" />
 				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
+				<Option compiler="gcc" />
 				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
@@ -290,13 +287,11 @@
 				<Option projectResourceIncludeDirsRelation="1" />
 				<Option projectLibDirsRelation="1" />
 				<Compiler>
-					<Add option="-O3" />
 					<Add option="-Wall" />
 					<Add option="-g" />
 					<Add option="-m64" />
 					<Add option="-mwindows" />
 					<Add option="-fsigned-char" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -327,7 +322,7 @@
 				<Option object_output=".objs/redeclipse-amd64-prof" />
 				<Option deps_output=".deps/redeclipse-amd64-prof" />
 				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
+				<Option compiler="gcc" />
 				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
@@ -341,7 +336,6 @@
 					<Add option="-mwindows" />
 					<Add option="-fsigned-char" />
 					<Add option="-finline-functions" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -373,7 +367,7 @@
 				<Option object_output=".objs/redeclipse_server-amd64" />
 				<Option deps_output=".deps/redeclipse_server-amd64" />
 				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
+				<Option compiler="gcc" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
 				<Option projectIncludeDirsRelation="1" />
@@ -381,6 +375,7 @@
 				<Option projectLibDirsRelation="1" />
 				<Compiler>
 					<Add option="-fomit-frame-pointer" />
+					<Add option="-ffast-math" />
 					<Add option="-O3" />
 					<Add option="-Wall" />
 					<Add option="-m64" />
@@ -389,7 +384,6 @@
 					<Add option="-fno-exceptions" />
 					<Add option="-fno-rtti" />
 					<Add option="-DSTANDALONE" />
-					<Add option="-DFPS=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -417,453 +411,7 @@
 				<Option object_output=".objs/redeclipse_server-amd64-dbg" />
 				<Option deps_output=".deps/redeclipse_server-amd64-dbg" />
 				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-Wall" />
-					<Add option="-g" />
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-DSTANDALONE" />
-					<Add option="-DFPS=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add library="zlib1" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/amd64" />
-					<Add directory="lib/amd64" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-x86-64" />
-				</Environment>
-			</Target>
-			<Target title="mekclient-x86">
-				<Option output="../bin/x86/mekclient.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekclient-x86" />
-				<Option deps_output=".deps/mekclient-x86" />
-				<Option type="0" />
 				<Option compiler="gcc" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-fomit-frame-pointer" />
-					<Add option="-O3" />
-					<Add option="-Wall" />
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-fno-exceptions" />
-					<Add option="-fno-rtti" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-s" />
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add library="SDL" />
-					<Add library="SDL_image" />
-					<Add library="SDL_mixer" />
-					<Add library="zlib1" />
-					<Add library="opengl32" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/x86" />
-					<Add directory="lib/x86" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-i386" />
-				</Environment>
-			</Target>
-			<Target title="mekclient-x86-dbg">
-				<Option output="../bin/x86/mekclient-dbg.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekclient-x86-dbg" />
-				<Option deps_output=".deps/mekclient-x86-dbg" />
-				<Option type="0" />
-				<Option compiler="gcc" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-O3" />
-					<Add option="-Wall" />
-					<Add option="-g" />
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add library="SDL" />
-					<Add library="SDL_image" />
-					<Add library="SDL_mixer" />
-					<Add library="zlib1" />
-					<Add library="opengl32" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/x86" />
-					<Add directory="lib/x86" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-i386" />
-				</Environment>
-			</Target>
-			<Target title="mekclient-x86-prof">
-				<Option output="../bin/x86/mekclient.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekclient-x86-prof" />
-				<Option deps_output=".deps/mekclient-x86-prof" />
-				<Option type="0" />
-				<Option compiler="gcc" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-Wall" />
-					<Add option="-pg" />
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-finline-functions" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-pg -lgmon" />
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add library="SDL" />
-					<Add library="SDL_image" />
-					<Add library="SDL_mixer" />
-					<Add library="zlib1" />
-					<Add library="opengl32" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/x86" />
-					<Add directory="lib/x86" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-i386" />
-				</Environment>
-			</Target>
-			<Target title="mekserver-x86">
-				<Option output="../bin/x86/mekserver.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekserver-x86" />
-				<Option deps_output=".deps/mekserver-x86" />
-				<Option type="0" />
-				<Option compiler="gcc" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-fomit-frame-pointer" />
-					<Add option="-O3" />
-					<Add option="-Wall" />
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-fno-exceptions" />
-					<Add option="-fno-rtti" />
-					<Add option="-DSTANDALONE" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-s" />
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add library="zlib1" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/x86" />
-					<Add directory="lib/x86" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-i386" />
-				</Environment>
-			</Target>
-			<Target title="mekserver-x86-dbg">
-				<Option output="../bin/x86/mekserver-dbg.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekserver-x86-dbg" />
-				<Option deps_output=".deps/mekserver-x86-dbg" />
-				<Option type="0" />
-				<Option compiler="gcc" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-Wall" />
-					<Add option="-g" />
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-DSTANDALONE" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-m32" />
-					<Add option="-mwindows" />
-					<Add library="zlib1" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/x86" />
-					<Add directory="lib/x86" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-i386" />
-				</Environment>
-			</Target>
-			<Target title="mekclient-amd64">
-				<Option output="../bin/amd64/mekclient.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekclient-amd64" />
-				<Option deps_output=".deps/mekclient-amd64" />
-				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-fomit-frame-pointer" />
-					<Add option="-O3" />
-					<Add option="-Wall" />
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-fno-exceptions" />
-					<Add option="-fno-rtti" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-s" />
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add library="SDL" />
-					<Add library="SDL_image" />
-					<Add library="SDL_mixer" />
-					<Add library="zlib1" />
-					<Add library="opengl32" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/amd64" />
-					<Add directory="lib/amd64" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-x86-64" />
-				</Environment>
-			</Target>
-			<Target title="mekclient-amd64-dbg">
-				<Option output="../bin/amd64/mekclient-dbg.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekclient-amd64-dbg" />
-				<Option deps_output=".deps/mekclient-amd64-dbg" />
-				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-O3" />
-					<Add option="-Wall" />
-					<Add option="-g" />
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add library="SDL" />
-					<Add library="SDL_image" />
-					<Add library="SDL_mixer" />
-					<Add library="zlib1" />
-					<Add library="opengl32" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/amd64" />
-					<Add directory="lib/amd64" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-x86-64" />
-				</Environment>
-			</Target>
-			<Target title="mekclient-amd64-prof">
-				<Option output="../bin/amd64/mekclient.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekclient-amd64-prof" />
-				<Option deps_output=".deps/mekclient-amd64-prof" />
-				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
-				<Option parameters="-hhome -r -df0 -dw800 -dh600 -glog.txt" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-Wall" />
-					<Add option="-pg" />
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-finline-functions" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-pg -lgmon" />
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add library="SDL" />
-					<Add library="SDL_image" />
-					<Add library="SDL_mixer" />
-					<Add library="zlib1" />
-					<Add library="opengl32" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/amd64" />
-					<Add directory="lib/amd64" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-x86-64" />
-				</Environment>
-			</Target>
-			<Target title="mekserver-amd64">
-				<Option output="../bin/amd64/mekserver.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekserver-amd64" />
-				<Option deps_output=".deps/mekserver-amd64" />
-				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
-				<Option projectCompilerOptionsRelation="1" />
-				<Option projectLinkerOptionsRelation="1" />
-				<Option projectIncludeDirsRelation="1" />
-				<Option projectResourceIncludeDirsRelation="1" />
-				<Option projectLibDirsRelation="1" />
-				<Compiler>
-					<Add option="-fomit-frame-pointer" />
-					<Add option="-O3" />
-					<Add option="-Wall" />
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add option="-fsigned-char" />
-					<Add option="-fno-exceptions" />
-					<Add option="-fno-rtti" />
-					<Add option="-DSTANDALONE" />
-					<Add option="-DMEK=1" />
-					<Add directory="shared" />
-					<Add directory="engine" />
-					<Add directory="game" />
-					<Add directory="enet/include" />
-					<Add directory="include" />
-				</Compiler>
-				<Linker>
-					<Add option="-s" />
-					<Add option="-m64" />
-					<Add option="-mwindows" />
-					<Add library="zlib1" />
-					<Add library="enet" />
-					<Add library="ws2_32" />
-					<Add library="winmm" />
-					<Add directory="../bin/amd64" />
-					<Add directory="lib/amd64" />
-				</Linker>
-				<Environment>
-					<Variable name="WINDRES_TARGET" value="pe-x86-64" />
-				</Environment>
-			</Target>
-			<Target title="mekserver-amd64-dbg">
-				<Option output="../bin/amd64/mekserver-dbg.exe" prefix_auto="0" extension_auto="0" />
-				<Option working_dir="../" />
-				<Option object_output=".objs/mekserver-amd64-dbg" />
-				<Option deps_output=".deps/mekserver-amd64-dbg" />
-				<Option type="0" />
-				<Option compiler="gnu_gcc_compiler_for_amd64" />
 				<Option projectCompilerOptionsRelation="1" />
 				<Option projectLinkerOptionsRelation="1" />
 				<Option projectIncludeDirsRelation="1" />
@@ -876,7 +424,6 @@
 					<Add option="-mwindows" />
 					<Add option="-fsigned-char" />
 					<Add option="-DSTANDALONE" />
-					<Add option="-DMEK=1" />
 					<Add directory="shared" />
 					<Add directory="engine" />
 					<Add directory="game" />
@@ -912,6 +459,7 @@
 				<Option projectLibDirsRelation="1" />
 				<Compiler>
 					<Add option="-fomit-frame-pointer" />
+					<Add option="-ffast-math" />
 					<Add option="-O3" />
 					<Add option="-Wall" />
 					<Add option="-fsigned-char" />
@@ -953,6 +501,7 @@
 				<Option projectLibDirsRelation="1" />
 				<Compiler>
 					<Add option="-fomit-frame-pointer" />
+					<Add option="-ffast-math" />
 					<Add option="-O3" />
 					<Add option="-Wall" />
 					<Add option="-fsigned-char" />
@@ -981,2168 +530,1246 @@
 			</Target>
 		</Build>
 		<VirtualTargets>
-			<Add alias="ALL" targets="redeclipse-x86;redeclipse_server-x86;redeclipse-amd64;redeclipse_server-amd64;mekclient-x86;mekserver-x86;mekclient-amd64;mekserver-amd64;" />
-			<Add alias="ALL-amd64" targets="redeclipse-amd64;redeclipse_server-amd64;mekclient-amd64;mekserver-amd64;" />
-			<Add alias="ALL-x86" targets="redeclipse-x86;redeclipse_server-x86;mekclient-x86;mekserver-x86;" />
-			<Add alias="REDECLIPSE" targets="redeclipse-x86;redeclipse_server-x86;redeclipse-amd64;redeclipse_server-amd64;" />
-			<Add alias="REDECLIPSE-amd64" targets="redeclipse-amd64;redeclipse_server-amd64;" />
-			<Add alias="REDECLIPSE-x86" targets="redeclipse-x86;redeclipse_server-x86;" />
-			<Add alias="MEKARCADE" targets="mekclient-x86;mekserver-x86;mekclient-amd64;mekserver-amd64;" />
-			<Add alias="MEKARCADE-amd64" targets="mekclient-amd64;mekserver-amd64;" />
-			<Add alias="MEKARCADE-x86" targets="mekclient-x86;mekserver-x86;" />
+			<Add alias="ALL" targets="redeclipse-x86;redeclipse_server-x86;redeclipse-amd64;redeclipse_server-amd64;" />
+			<Add alias="ALL-amd64" targets="redeclipse-amd64;redeclipse_server-amd64;" />
+			<Add alias="ALL-x86" targets="redeclipse-x86;redeclipse_server-x86;" />
 			<Add alias="TOOLS" targets="cube2font;genkey;" />
 		</VirtualTargets>
 		<Unit filename="dpiaware.manifest">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="engine/animmodel.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/bih.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/bih.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/blend.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/blob.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/client.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/command.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="engine/console.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/decal.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/depthfx.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/dynlight.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/engine.h">
 			<Option compile="1" />
 			<Option weight="0" />
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/explosion.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/genkey.cpp">
 			<Option target="genkey" />
 		</Unit>
 		<Unit filename="engine/glare.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
+		</Unit>
+		<Unit filename="engine/glexts.h">
+			<Option target="redeclipse-x86" />
+			<Option target="redeclipse-x86-dbg" />
+			<Option target="redeclipse-x86-prof" />
+			<Option target="redeclipse-amd64" />
+			<Option target="redeclipse-amd64-dbg" />
+			<Option target="redeclipse-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/grass.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/iqm.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/irc.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="engine/irc.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="engine/lensflare.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/lightmap.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/lightmap.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/lightning.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/main.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/master.cpp">
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="engine/material.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/md2.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/md3.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/md5.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/menus.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/model.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/movie.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/mpr.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/normal.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/obj.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/octa.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/octa.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/octaedit.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/octarender.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/physics.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/pvs.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/ragdoll.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/rendergl.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/rendermodel.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/renderparticles.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/rendersky.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/rendertarget.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/rendertext.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/renderva.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/scale.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/server.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="engine/serverbrowser.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/shader.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/shadowmap.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/skelmodel.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/smd.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/sound.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/sound.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/textedit.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/texture.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/texture.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/ui.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/varray.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/vertmodel.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/water.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/world.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/world.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="engine/worldio.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/ai.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/ai.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/aiman.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/auth.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/bomber.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/bomber.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/bombermode.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/capture.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/capture.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/capturemode.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/client.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/compass.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/defend.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/defend.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/defendmode.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/duelmut.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/entities.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/game.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/game.h">
 			<Option weight="0" />
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/gamemode.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/hud.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/physics.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/player.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
+		</Unit>
+		<Unit filename="game/playerdef.h">
+			<Option target="redeclipse-x86" />
+			<Option target="redeclipse-x86-dbg" />
+			<Option target="redeclipse-x86-prof" />
+			<Option target="redeclipse_server-x86" />
+			<Option target="redeclipse_server-x86-dbg" />
+			<Option target="redeclipse-amd64" />
+			<Option target="redeclipse-amd64-dbg" />
+			<Option target="redeclipse-amd64-prof" />
+			<Option target="redeclipse_server-amd64" />
+			<Option target="redeclipse_server-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/projs.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/scoreboard.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/server.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
+		</Unit>
+		<Unit filename="game/teamdef.h">
+			<Option target="redeclipse-x86" />
+			<Option target="redeclipse-x86-dbg" />
+			<Option target="redeclipse-x86-prof" />
+			<Option target="redeclipse_server-x86" />
+			<Option target="redeclipse_server-x86-dbg" />
+			<Option target="redeclipse-amd64" />
+			<Option target="redeclipse-amd64-dbg" />
+			<Option target="redeclipse-amd64-prof" />
+			<Option target="redeclipse_server-amd64" />
+			<Option target="redeclipse_server-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/vars.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/waypoint.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/weapdef.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="game/weapons.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="game/weapons.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="include/GL/glext.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_active.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_audio.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_byteorder.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_cdrom.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_config.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_config_macosx.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_config_win32.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_copying.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_cpuinfo.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_endian.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_error.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_events.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_getenv.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_image.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_joystick.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_keyboard.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_keysym.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_loadso.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_main.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_mixer.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_mouse.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_mutex.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_name.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_opengl.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_platform.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_quit.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_rwops.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_stdinc.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_syswm.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_thread.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_timer.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
-			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
-			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
-			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
-			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
-			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
-		</Unit>
-		<Unit filename="include/SDL_ttf.h">
-			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_types.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_version.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/SDL_video.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/begin_code.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/close_code.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/zconf.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="include/zlib.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
-		</Unit>
-		<Unit filename="mekarcade.rc">
-			<Option compilerVar="WINDRES" />
-			<Option compiler="gcc" use="1" buildCommand="$rescomp -F $WINDRES_TARGET -i $file -J rc -o $resource_output -O coff $res_includes" />
-			<Option target="mekclient-x86" />
-			<Option target="mekclient-x86-dbg" />
-			<Option target="mekserver-x86" />
-			<Option target="mekclient-x86-prof" />
-			<Option target="mekserver-x86-dbg" />
-			<Option target="mekclient-amd64" />
-			<Option target="mekclient-amd64-dbg" />
-			<Option target="mekclient-amd64-prof" />
-			<Option target="mekserver-amd64" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="redeclipse.rc">
 			<Option compilerVar="WINDRES" />
@@ -3160,65 +1787,39 @@
 		</Unit>
 		<Unit filename="shared/command.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="genkey" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="shared/crypto.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="genkey" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="shared/cube.h">
 			<Option compile="1" />
 			<Option weight="0" />
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="shared/cube2font.c">
 			<Option compilerVar="CC" />
@@ -3226,164 +1827,110 @@
 		</Unit>
 		<Unit filename="shared/ents.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="shared/geom.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="shared/geom.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="shared/iengine.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="shared/igame.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 		</Unit>
 		<Unit filename="shared/stream.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="genkey" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="shared/tools.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="genkey" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="shared/tools.h">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse_server-x86" />
-			<Option target="mekserver-x86" />
 			<Option target="redeclipse_server-x86-dbg" />
-			<Option target="mekserver-x86-dbg" />
 			<Option target="genkey" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
 			<Option target="redeclipse_server-amd64" />
-			<Option target="mekserver-amd64" />
 			<Option target="redeclipse_server-amd64-dbg" />
-			<Option target="mekserver-amd64-dbg" />
 		</Unit>
 		<Unit filename="shared/zip.cpp">
 			<Option target="redeclipse-x86" />
-			<Option target="mekclient-x86" />
 			<Option target="redeclipse-x86-dbg" />
-			<Option target="mekclient-x86-dbg" />
 			<Option target="redeclipse-x86-prof" />
-			<Option target="mekclient-x86-prof" />
 			<Option target="redeclipse-amd64" />
-			<Option target="mekclient-amd64" />
 			<Option target="redeclipse-amd64-dbg" />
-			<Option target="mekclient-amd64-dbg" />
 			<Option target="redeclipse-amd64-prof" />
-			<Option target="mekclient-amd64-prof" />
+			<Option target="redeclipse_server-x86" />
+			<Option target="redeclipse_server-x86-dbg" />
+			<Option target="redeclipse_server-amd64" />
+			<Option target="redeclipse_server-amd64-dbg" />
+		</Unit>
+		<Unit filename="version.h">
+			<Option target="redeclipse-x86" />
+			<Option target="redeclipse-x86-dbg" />
+			<Option target="redeclipse-x86-prof" />
+			<Option target="redeclipse_server-x86" />
+			<Option target="redeclipse_server-x86-dbg" />
+			<Option target="redeclipse-amd64" />
+			<Option target="redeclipse-amd64-dbg" />
+			<Option target="redeclipse-amd64-prof" />
+			<Option target="redeclipse_server-amd64" />
+			<Option target="redeclipse_server-amd64-dbg" />
 		</Unit>
 		<Extensions>
 			<code_completion />
diff --git a/src/redeclipse.ico b/src/redeclipse.ico
index f93ebfa..24b474d 100644
Binary files a/src/redeclipse.ico and b/src/redeclipse.ico differ
diff --git a/src/redeclipse.mk b/src/redeclipse.mk
deleted file mode 100644
index ab9103e..0000000
--- a/src/redeclipse.mk
+++ /dev/null
@@ -1,9 +0,0 @@
-ifeq ($(APPNAME),redeclipse)
-# core
-APPSHORTNAME=fps
-APPFLAGS= -DFPS=1
-
-# system-install and dist
-ICON=../data/textures/icon.png
-FILES=readme.txt
-endif
diff --git a/src/redeclipse.rc b/src/redeclipse.rc
index 6a8fe57..fb314be 100644
--- a/src/redeclipse.rc
+++ b/src/redeclipse.rc
@@ -1,6 +1,34 @@
-#include "winresrc.h"
-#define IDI_ICON1 1
-
-CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "dpiaware.manifest"
-LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
-IDI_ICON1 ICON "redeclipse.ico"
+#include "winresrc.h"
+#include "version.h"
+#define IDI_ICON1 1
+
+CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "dpiaware.manifest"
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+IDI_ICON1 ICON "redeclipse.ico"
+
+VS_VERSION_INFO VERSIONINFO
+    FILEVERSION    VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0
+    PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0
+{
+    BLOCK "StringFileInfo"
+    {
+        BLOCK "040904b0"
+        {
+            VALUE "CompanyName",        VERSION_NAME " Team\0"
+            VALUE "FileDescription",    VERSION_NAME "\0"
+            VALUE "FileVersion",        VERSION_STRING "\0"
+            VALUE "LegalCopyright",     "(C) " VERSION_COPY " " VERSION_NAME " Team\0"
+#ifdef STANDALONE
+            VALUE "OriginalFilename",   VERSION_UNAME "_server.exe\0"
+#else
+            VALUE "OriginalFilename",   VERSION_UNAME ".exe\0"
+#endif
+            VALUE "ProductName",        VERSION_NAME "\0"
+            VALUE "ProductVersion",     VERSION_STRING "\0"
+        }
+    }
+    BLOCK "VarFileInfo"
+    {
+        VALUE "Translation", 0x409, 1200
+    }
+}
diff --git a/src/scripts/check-wiki-all-result b/src/scripts/check-wiki-all-result
deleted file mode 100755
index c0bc7c8..0000000
--- a/src/scripts/check-wiki-all-result
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-#
-# "$1" is data/usage.cfg
-# "$2" is doc/wiki-all-vars-commands.txt
-
-DIR="$(cd "$(dirname "$0")" && pwd)"
-
-source "$DIR"/wiki-common
-
-diff -u0 \
-    <(sed "$1" -n -e 's%\^\^%^%' -e 's%setdesc "\([^"]*\)".*%\1%p' -e 's% *setdesc (concatword $w \([^ ^)]*\).*%[weap]\1%p' | sort) \
-    <(sed "$2" -n -e "s%'''<nowiki>\(.*\)<\/nowiki>'''.*%\1%p" | sed -e 's%^| %%' -e 's%<nowiki>\([^<]*\)<\/nowiki>%\1%' -e 's%\^\^%^%' | sort) \
-    || exit 1
diff --git a/src/scripts/generate-cube2font-txt b/src/scripts/cube2font-txt
similarity index 100%
rename from src/scripts/generate-cube2font-txt
rename to src/scripts/cube2font-txt
diff --git a/src/scripts/generate-wiki-aliases b/src/scripts/generate-wiki-aliases
deleted file mode 100755
index ad6083e..0000000
--- a/src/scripts/generate-wiki-aliases
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-#
-# "$1" is usage.cfg
-# "$2" is output file
-
-DIR="$(cd "$(dirname "$0")" && pwd)"
-CFG_FILES="$DIR/../../data/*.cfg"
-
-source "$DIR"/wiki-common
-
-noedit_header > "$2"
-table_header_simple >> "$2"
-
-IFS=$'\n'
-for i in $(grep "^setdesc" "$1" | sort)
-do
-    ALI="$(format_name "$i")"
-
-    CFG_DEF="$(grep -hr "^$ALI *=.*" $CFG_FILES)"
-    if [ -z "$CFG_DEF" ]; then continue; fi
-
-    DESC="$(format_desc "$i")"
-    PARAM="$(format_param "$i")"
-
-    TYPE_STRING="alias"
-
-    table_entry_simple "$ALI" "$PARAM" "$DESC" "$TYPE_STRING" >> "$2"
-done
-
-table_end >> "$2"
diff --git a/src/scripts/generate-wiki-all-vars-commands b/src/scripts/generate-wiki-all-vars-commands
deleted file mode 100755
index 8f781cb..0000000
--- a/src/scripts/generate-wiki-all-vars-commands
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash
-#
-# "$1..n-1" is input files
-# "$n" is output file
-
-DIR="$(cd "$(dirname "$0")" && pwd)"
-source "$DIR"/wiki-common
-
-for i
-do
-    if [ -n "$INFILES" ]
-    then
-        INFILES="$INFILES $OUTFILE"
-    else
-        INFILES="$OUTFILE"
-    fi
-    OUTFILE="$i"
-done
-
-noedit_header > "$OUTFILE"
-table_header >> "$OUTFILE"
-
-sed -n '/|-/,/|}/p' $INFILES | sed '/|}/d' >> "$OUTFILE"
-
-table_end >> "$OUTFILE"
diff --git a/src/scripts/generate-wiki-commands b/src/scripts/generate-wiki-commands
deleted file mode 100755
index a3f0d1f..0000000
--- a/src/scripts/generate-wiki-commands
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/bash
-#
-# "$1" is usage.cfg
-# "$2" is output file
-
-DIR="$(cd "$(dirname "$0")" && pwd)"
-source "$DIR"/wiki-common
-
-noedit_header > "$2"
-table_header_simple >> "$2"
-
-SRC_DEFS="$(grep -h '^[^A-Z]*[A-Z]*COMMAND[A-Z]*([^,]*, [^,]*,' $(find_src_files "$DIR/..") | sed 's%^ *%%')"
-
-IFS=$'\n'
-for i in $(grep "^setdesc" "$1" | sort)
-do
-    COM="$(format_name "$i")"
-
-    # Get corresponding source lines
-    SRC_DEF="$(extract_src_def "$SRC_DEFS" "$COM")"
-    if [ -z "$SRC_DEF" ]; then continue; fi
-
-    DESC="$(format_desc "$i")"
-    PARAM="$(format_param "$i")"
-
-    SRC_IDF="$(echo $SRC_DEF | sed 's%^[^A-Z]*[A-Z]*COMMAND[A-Z]*(\([^,]*\),%\1%')"
-    IDF_ADMIN=""
-    # Currently only one case
-    case "$SRC_IDF" in
-        *IDF_ADMIN*)
-            IDF_ADMIN="admin-only"
-            ;;
-    esac
-
-    TYPE_STRING="$(echo "$IDF_ADMIN command" | sed -e 's%^ *%%' -e 's%  *% %g')"
-
-    table_entry_simple "$COM" "$PARAM" "$DESC" "$TYPE_STRING" >> "$2"
-done
-
-table_end >> "$2"
diff --git a/src/scripts/generate-wiki-engine-vars b/src/scripts/generate-wiki-engine-vars
deleted file mode 100755
index 1aadb14..0000000
--- a/src/scripts/generate-wiki-engine-vars
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/bin/bash
-#
-# "$1" is usage.cfg
-# "$2" is output file
-
-DIR="$(cd "$(dirname "$0")" && pwd)"
-source "$DIR"/wiki-common
-
-noedit_header > "$2"
-table_header >> "$2"
-
-# Only consider *VAR*s (without intial "G")
-SRC_DEFS="$(grep -h '^[^A-Z]*[A-FH-Z]*VAR[A-Z]*([^,]*, [^,]*, ' $(find_src_files "$DIR/..") | sed 's%^ *%%')"
-
-IFS=$'\n'
-for i in $(grep "^setdesc" "$1" | sort)
-do
-    VAR="$(format_name "$i")"
-
-    SRC_DEF="$(extract_src_def "$SRC_DEFS" "$VAR")"
-    if [ -z "$SRC_DEF" ]; then continue; fi
-
-    DESC="$(format_desc "$i")"
-    PARAM="$(format_param "$i")"
-
-    SRC_TYPE="$(echo $SRC_DEF | sed 's%^[^A-Z]*\([A-FH-Z]*VAR[A-Z]*\)(.*%\1%')"
-    TYPE="integer"
-    case "$SRC_TYPE" in
-        *FVAR*)
-            TYPE="float" ;;
-        *SVAR*)
-            TYPE="string" ;;
-        *TVAR*)
-            TYPE="texture" ;;
-    esac
-
-    SRC_IDF="$(echo $SRC_DEF | sed 's%^[^A-Z]*[A-FH-Z]*VAR[A-Z]*(\([^,]*\),%\1%')"
-    IDF_ADMIN=""
-    IDF_HEX=""
-    IDF_PRELOAD=""
-    IDF_PERSIST=""
-    IDF_WORLD=""
-    # Currently only one case
-    case "$SRC_IDF" in
-        *IDF_ADMIN*)
-            IDF_ADMIN="admin-only"
-            ;;&
-        *IDF_HEX*)
-            IDF_HEX="hex"
-            ;;&
-        *IDF_PRELOAD*)
-            IDF_PRELOAD="preloaded"
-            ;;&
-        *IDF_GAMEPRELOAD*)
-            IDF_PRELOAD="game-preloaded"
-            ;;&
-        *IDF_PERSIST*)
-            IDF_PERSIST="persistent"
-            ;;&
-        *IDF_WORLD*)
-            IDF_WORLD="world"
-            ;;
-    esac
-
-    TYPE_STRING="$(echo "$IDF_ADMIN $IDF_HEX $IDF_PRELOAD $IDF_PERSIST $IDF_WORLD engine $TYPE" | sed -e 's%^ *%%' -e 's%  *% %g')"
-
-    MIN_MAX="n/a"
-    case "$TYPE" in
-        string|texture)
-            DEFAULT="$(echo $SRC_DEF | sed 's%^[^A-Z]*[A-FH-Z]*VAR[A-Z]*([^,]*, [^,]*, \([^,^)]*\).*%\1%')"
-            ;;
-        integer|float)
-            MIN_MAX="$(echo $SRC_DEF | sed 's%^[^A-Z]*[A-FH-Z]*VAR[A-Z]*([^,]*, [^,]*, \([^,]*\), [^,]*, \([^,^)]*\).*%\1..\2%')"
-            DEFAULT="$(echo $SRC_DEF | sed 's%^[^A-Z]*[A-FH-Z]*VAR[A-Z]*([^,]*, [^,]*, [^,]*, \([^,^)]*\).*%\1%')"
-            ;;
-    esac
-    table_entry "$VAR" "$PARAM" "$DESC" "$TYPE_STRING" "$MIN_MAX" "$DEFAULT" >> "$2"
-done
-
-table_end >> "$2"
diff --git a/src/scripts/generate-wiki-game-vars b/src/scripts/generate-wiki-game-vars
deleted file mode 100755
index 262eb04..0000000
--- a/src/scripts/generate-wiki-game-vars
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/bin/bash
-#
-# "$1" is usage.cfg
-# "$2" is output file
-
-DIR="$(cd "$(dirname "$0")" && pwd)"
-source "$DIR"/wiki-common
-
-noedit_header > "$2"
-table_header >> "$2"
-
-SRC_DEFS="$(grep -h '^[^A-Z]*G[A-Z]*VAR[A-Z]*([^,]*, [^,]*, ' $(find_src_files "$DIR/..") | sed 's%^ *%%')"
-
-IFS=$'\n'
-for i in $(grep "^setdesc" "$1" | sort)
-do
-    VAR="$(format_name "$i")"
-
-    # Only consider G*VAR*s
-    SRC_DEF="$(extract_src_def "$SRC_DEFS" "$VAR")"
-    if [ -z "$SRC_DEF" ]; then continue; fi
-
-    DESC="$(format_desc "$i")"
-    PARAM="$(format_param "$i")"
-
-    SRC_TYPE="$(echo $SRC_DEF | sed 's%.*\(G[A-Z]*VAR[A-Z]*\)(.*%\1%')"
-    case "$SRC_TYPE" in
-        GVAR*)
-            TYPE="integer" ;;
-        GFVAR*)
-            TYPE="float" ;;
-        GSVAR*)
-            TYPE="string" ;;
-        *)
-            TYPE="BUG" ;;
-    esac
-
-    SRC_IDF="$(echo $SRC_DEF | sed 's%.*G[A-Z]*VAR[A-Z]*(\([^,]*\),%\1%')"
-    IDF_ADMIN=""
-    # Currently only one case
-    case "$SRC_IDF" in
-        *IDF_ADMIN*)
-            IDF_ADMIN="admin-only"
-            ;;
-    esac
-
-    TYPE_STRING="$(echo "$IDF_ADMIN game $TYPE" | sed 's%^ %%')"
-
-    MIN_MAX="n/a"
-    if [ "x$TYPE" != "xstring" ]
-    then
-        MIN_MAX="$(echo $SRC_DEF | sed 's%.*G[A-Z]*VAR[A-Z]*([^,]*, [^,]*, \([^,]*\), [^,]*, \([^,^)]*\).*%\1..\2%')"
-        DEFAULT="$(echo $SRC_DEF | sed 's%.*G[A-Z]*VAR[A-Z]*([^,]*, [^,]*, [^,]*, \([^,^)]*\).*%\1%')"
-    else
-        DEFAULT="$(echo $SRC_DEF | sed 's%.*G[A-Z]*VAR[A-Z]*([^,]*, [^,]*, \([^,^)]*\).*%\1%')"
-    fi
-    table_entry "$VAR" "$PARAM" "$DESC" "$TYPE_STRING" "$MIN_MAX" "$DEFAULT" >> "$2"
-done
-
-table_end >> "$2"
diff --git a/src/scripts/generate-wiki-weapon-vars b/src/scripts/generate-wiki-weapon-vars
deleted file mode 100755
index bb56f73..0000000
--- a/src/scripts/generate-wiki-weapon-vars
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/bin/bash
-#
-# "$1" is usage.cfg
-# "$2" is output file
-
-DIR="$(cd "$(dirname "$0")" && pwd)"
-WEAPONS_H="$DIR/../game/weapons.h"
-
-source "$DIR"/wiki-common
-
-noedit_header > "$2"
-echo -e "To use these variable templates, replace [weap] with the weapon name, and (if given) [1|2] with either 1 or 2.\n" >> "$2"
-table_header >> "$2"
-
-IFS=$'\n'
-for i in $(grep "^  *setdesc" "$1" | sed 's%^  *%%' | sort)
-do
-    VAR="$(format_name_weap "$i")"
-    DESC="$(format_desc_weap "$i")"
-    PARAM="$(format_param_weap "$i")"
-
-    SRC_DEF="$(sed -n 's%.*\(G[A-Z]*VAR[A-Z]*([^,]*, a##'"$VAR"'[1,][^)]*)\).*%\1%p' "$WEAPONS_H")"
-
-    SPLIT="$(echo "$SRC_DEF" | sed -n 's%.*'"$VAR"'[12].*%[1|2]%p')"
-
-    SRC_TYPE="$(echo "$SRC_DEF" | sed 's%.*\(G[A-Z]*VAR[A-Z]*\)(([^,]*, a##'"$VAR"'.*%\1%')"
-    case "$SRC_TYPE" in
-        GVAR*)
-            TYPE="integer" ;;
-        GFVAR*)
-            TYPE="float" ;;
-        GSVAR*)
-            TYPE="string" ;;
-        *)
-            TYPE="BUG" ;;
-    esac
-
-    SRC_IDF="$(echo "$SRC_DEF" | sed 's%.*G[A-Z]*VAR[A-Z]*(\([^,]*\),%\1%')"
-    IDF_HEX=""
-    # Currently only one case
-    case "$SRC_IDF" in
-        *IDF_HEX*)
-            IDF_HEX="hex"
-            ;;
-    esac
-
-    TYPE_STRING="$(echo "$IDF_HEX weapon $TYPE" | sed 's%^ %%')"
-
-    MIN_MAX="n/a"
-    DEFAULT="n/a"
-    if [ "x$TYPE" != "xstring" ]
-    then
-        MIN_MAX="$(echo $SRC_DEF | sed 's%.*G[A-Z]*VAR[A-Z]*([^,]*, [^,]*, \([^,]*\), [^,]*, \([^,^)]*\).*%\1..\2%')"
-    fi
-    cat <<EOF >> "$2"
-|-
-| <nowiki>[weap]</nowiki>'''<nowiki>$VAR</nowiki>'''<nowiki>$SPLIT</nowiki> ''<nowiki>$PARAM</nowiki>''
-| <nowiki>$DESC</nowiki>
-| <nowiki>$TYPE_STRING</nowiki>
-| <nowiki>$MIN_MAX</nowiki>
-| <nowiki>$DEFAULT</nowiki>
-EOF
-done
-
-table_end >> "$2"
diff --git a/src/scripts/update-servinit-comments b/src/scripts/servinit-comments
similarity index 90%
rename from src/scripts/update-servinit-comments
rename to src/scripts/servinit-comments
index 4d50c8d..aa65d1b 100755
--- a/src/scripts/update-servinit-comments
+++ b/src/scripts/servinit-comments
@@ -7,10 +7,6 @@
 #
 # Usage: update-servexec-comments /path/usage.cfg /path/servexec.cfg
 
-DIR="$(cd "$(dirname "$0")" && pwd)"
-
-source "$DIR"/wiki-common
-
 IFS=$'\n'
 for i in $(grep "// sv_" "$2")
 do
diff --git a/src/scripts/update-servinit-defaults b/src/scripts/servinit-defaults
similarity index 100%
rename from src/scripts/update-servinit-defaults
rename to src/scripts/servinit-defaults
diff --git a/src/scripts/wiki-common b/src/scripts/wiki-common
deleted file mode 100755
index 3a0fd9a..0000000
--- a/src/scripts/wiki-common
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/bin/bash
-
-find_src_files ()
-{
-    find "$1" \
-    ! -wholename '*include*' \
-    ! -wholename '*xcode*' \
-    ! -wholename '*enet*' \
-    \( \
-        -name '*.c' \
-        -or -name '*.h' \
-        -or -name '*.cpp' \
-        -or -name '*.hpp' \
-    \)
-}
-
-extract_src_def ()
-{
-    echo "$1" | grep '^[^A-Z]*[A-Z]*([A-Z_0,| ]*'"$(echo "$2" | sed 's%\*%\\*%')"','
-}
-
-replace_escapes ()
-{
-    echo "$1" | sed -e ':again s%\([^^]\)\^"%\1ESC_QUOT%g; t again' -e 's%\([^^]\)\^n%\1 %g' -e 's%\^\(.\)%\1%g'
-}
-
-format_name ()
-{
-    replace_escapes "$1" | sed 's%^setdesc "\([^"]*\)".*%\1%'
-}
-
-format_name_weap ()
-{
-    replace_escapes "$1" | sed 's%^setdesc (concatword \$w \([^ ^)]*\).*%\1%'
-}
-
-format_desc ()
-{
-    replace_escapes "$1" | sed -e's%^setdesc "[^"]*" "\([^"]*\)".*%\1%' -e 's%ESC_QUOT%"%g'
-}
-
-format_desc_weap ()
-{
-    replace_escapes "$1" | sed -e 's%^setdesc ([^)]*) "\([^"]*\)".*%\1%' -e 's%ESC_QUOT%"%g'
-}
-
-format_param ()
-{
-    replace_escapes "$1" | sed -n 's%^setdesc "[^"]*" ".*" "\([^"]*\)"%\1%p'
-}
-
-format_param_weap ()
-{
-    replace_escapes "$1" | sed -n 's%^setdesc ([^)]*) ".*" "\([^"]*\)"%\1%p'
-}
-
-noedit_header ()
-{
-    echo -e "This page was automatically generated from the source of Red Eclipse, please do not edit it manually.\n"
-}
-
-table_header ()
-{
-    cat <<EOF
-{| border="1" class="wikitable sortable"
-! Name & Parameters
-! class="unsortable"|Description
-! Type
-! class="unsortable"|Range
-! class="unsortable"|Default Value
-EOF
-}
-
-table_entry ()
-{
-    if [ -n "$2" ]
-    then
-        cat <<EOF
-|-
-| '''<nowiki>$1</nowiki>''' ''<nowiki>$2</nowiki>''
-| <nowiki>$3</nowiki>
-| <nowiki>$4</nowiki>
-| <nowiki>$5</nowiki>
-| <nowiki>$6</nowiki>
-EOF
-    else
-        cat <<EOF
-|-
-| '''<nowiki>$1</nowiki>'''
-| <nowiki>$3</nowiki>
-| <nowiki>$4</nowiki>
-| <nowiki>$5</nowiki>
-| <nowiki>$6</nowiki>
-EOF
-    fi
-}
-
-table_header_simple ()
-{
-    cat <<EOF
-{| border="1" class="wikitable sortable"
-! Name & Parameters
-! class="unsortable"|Description
-! Type
-EOF
-}
-
-table_entry_simple ()
-{
-    if [ -n "$2" ]
-    then
-        cat <<EOF
-|-
-| '''<nowiki>$1</nowiki>''' ''<nowiki>$2</nowiki>''
-| <nowiki>$3</nowiki>
-| <nowiki>$4</nowiki>
-EOF
-    else
-        cat <<EOF
-|-
-| '''<nowiki>$1</nowiki>'''
-| <nowiki>$3</nowiki>
-| <nowiki>$4</nowiki>
-EOF
-    fi
-}
-
-table_end ()
-{
-    echo "|}"
-}
-
diff --git a/src/scripts/generate-wiki-contributors b/src/scripts/wiki-contributors
similarity index 88%
rename from src/scripts/generate-wiki-contributors
rename to src/scripts/wiki-contributors
index 6fbc156..e18970a 100755
--- a/src/scripts/generate-wiki-contributors
+++ b/src/scripts/wiki-contributors
@@ -3,13 +3,9 @@
 # "$1" is doc/readme.txt
 # "$2" is output file
 
-DIR="$(cd "$(dirname "$0")" && pwd)"
-
-source "$DIR"/wiki-common
-
-noedit_header > "$2"
-
 cat <<EOF >> "$2"
+This page was automatically generated from the source of Red Eclipse, please do not edit it manually.
+
 == Red Eclipse Team ==
 
 === Team Lead ===
diff --git a/src/scripts/wiki-convert b/src/scripts/wiki-convert
new file mode 100755
index 0000000..d8f3090
--- /dev/null
+++ b/src/scripts/wiki-convert
@@ -0,0 +1,179 @@
+#!/usr/bin/gawk -f
+
+BEGIN {
+    FS = "\t"
+
+    print "This page was automatically generated from the source of Red Eclipse, please do not edit it manually."
+    print ""
+    print "{| border=\"1\" class=\"wikitable sortable\""
+    print "! Name & Parameters"
+    print "! class=\"unsortable\"|Description"
+    print "! Type"
+    print "! class=\"unsortable\"|Default Value"
+    print "! class=\"unsortable\"|Range"
+    print "! identifier flags"
+}
+
+{
+    if (/^\/\//)
+        next
+
+    switch ($2) {
+    case 0:
+        type = "variable"
+        break
+    case 1:
+        type = "float variable"
+        break
+    case 2:
+        type = "string variable"
+        break
+    case 3:
+        type = "command"
+        break
+    case 4:
+        type = "alias"
+        break
+    case 5:
+        type = "local"
+        break
+    default:
+        type = "BUG"
+        break
+    }
+
+    for (i = 0; i < 14; i++) {
+        switch (and($3, lshift(1, i))) {
+        case 1:
+            flags[len++] = "IDF_PERSIST"
+            break
+        case 2:
+            flags[len++] = "IDF_READONLY"
+            break
+        case 4:
+            flags[len++] = "IDF_REWRITE"
+            break
+        case 8:
+            flags[len++] = "IDF_WORLD"
+            break
+        case 16:
+            flags[len++] = "IDF_COMPLETE"
+            break
+        case 32:
+            flags[len++] = "IDF_TEXTURE"
+            break
+        case 64:
+            flags[len++] = "IDF_CLIENT"
+            break
+        case 128:
+            flags[len++] = "IDF_SERVER"
+            break
+        case 256:
+            flags[len++] = "IDF_HEX"
+            break
+        case 512:
+            flags[len++] = "IDF_ADMIN"
+            break
+        case 1024:
+            flags[len++] = "IDF_UNKNOWN"
+            break
+        case 2048:
+            flags[len++] = "IDF_ARGS"
+            break
+        case 4096:
+            flags[len++] = "IDF_PRELOAD"
+            break
+        case 8192:
+            flags[len++] = "IDF_GAMEPRELOAD"
+            break
+        default:
+            break
+        }
+    }
+
+    if (type == "command")
+        value = $4
+
+    switch ($5) {
+    case 0:
+        type = ("NULL " type)
+        break
+    case 1:
+        type = ("integer " type)
+        break
+    case 2:
+        type = ("float " type)
+        break
+    case 3:
+        type = ("string " type)
+        break
+    case 4:
+        type = ("any " type)
+        break
+    case 5:
+        type = ("code " type)
+        break
+    case 6:
+        type = ("macro " type)
+        break
+    case 7:
+        type = ("ident " type)
+        break
+    default:
+        break
+    }
+
+    if (length($6)) {
+        value = $6
+        sub(/^\"/, "", value)
+        sub(/\"$/, "", value)
+    } else {
+        value = "n/a"
+    }
+
+    if (length($7) && length($8))
+        range = ($7 ".." $8)
+    else
+        range = "n/a"
+
+    if (length($9) > 2) {
+        desc = $9
+        sub(/^\"/, "", desc)
+        sub(/\"$/, "", desc)
+        desc = gensub(/([^^])\^f./, "\\1", "g", desc)
+        desc = gensub(/([^^])\^n/, "\\1 ", "g", desc)
+        desc = gensub(/([^^])\^"/, "\\1\"", "g", desc)
+        desc = gensub(/\^(.)/, "\\1", "g", desc)
+    } else {
+        desc = "#undocumented#"
+    }
+
+    if (length($10) > 2) {
+        usage = $10
+        sub(/^\"/, "", usage)
+        sub(/\"$/, "", usage)
+    } else {
+        usage = "<#undocumented#>"
+    }
+
+    print "|-"
+    print ("| <b><nowiki>" $1 "</nowiki></b> <i><nowiki>" usage "</nowiki></i>")
+    print ("| <nowiki>" desc "</nowiki>")
+    print ("| <nowiki>" type "</nowiki>")
+    print ("| <nowiki>" value "</nowiki>")
+    print ("| <nowiki>" range "</nowiki>")
+    printf "| <nowiki>"
+    for (i = 0; i < len; i++) {
+        if (i == 0)
+            printf flags[i]
+        else
+            printf (" " flags[i])
+    }
+    print "</nowiki>"
+
+    len = 0
+}
+
+END {
+    print "|}"
+}
diff --git a/src/scripts/generate-wiki-guidelines b/src/scripts/wiki-guidelines
similarity index 100%
rename from src/scripts/generate-wiki-guidelines
rename to src/scripts/wiki-guidelines
diff --git a/src/semaphore.sh b/src/semaphore.sh
new file mode 100755
index 0000000..401bca5
--- /dev/null
+++ b/src/semaphore.sh
@@ -0,0 +1,170 @@
+#!/bin/sh
+SEMABUILD_PWD=`pwd`
+SEMABUILD_SCP='scp -BC -o StrictHostKeyChecking=no'
+SEMABUILD_TARGET='qreeves at icculus.org:/webspace/redeclipse.net/files'
+SEMABUILD_APT='DEBIAN_FRONTEND=noninteractive apt-get'
+SEMABUILD_SOURCE="http://redeclipse.net/files"
+SEMABUILD_BUILD="${HOME}/build"
+SEMABUILD_DIR="${SEMABUILD_BUILD}/${BRANCH_NAME}"
+
+semabuild_setup() {
+    echo "Setting up ${BRANCH_NAME}..."
+    rm -rfv "${SEMABUILD_BUILD}" || return 1
+    mkdir -pv "${SEMABUILD_DIR}" || return 1
+    SEMABUILD_BASE=`git rev-parse HEAD` || return 1
+    if [ "${BRANCH_NAME}" != "master" ]; then
+        SEMABUILD_BASE_ANCESTOR=`git merge-base origin/master ${SEMABUILD_BASE}` || return 1
+    fi
+    git submodule init || return 1
+    git submodule update || return 1
+    cd "${SEMABUILD_PWD}/data" || return 1
+    SEMABUILD_DATA=`git rev-parse HEAD` || return 1
+    #SEMABUILD_DATA=`git submodule status -- data | sed -e 's/^.\(.*\) .*/\1/'`
+    if [ "${BRANCH_NAME}" != "master" ]; then
+        SEMABUILD_DATA_ANCESTOR=`git merge-base origin/master ${SEMABUILD_DATA}` || return 1
+    fi
+    cd "${SEMABUILD_PWD}" || return 1
+    SEMABUILD_BASE_LAST=`curl --fail --silent "${SEMABUILD_SOURCE}/${BRANCH_NAME}/base.txt"`
+    SEMABUILD_DATA_LAST=`curl --fail --silent "${SEMABUILD_SOURCE}/${BRANCH_NAME}/data.txt"`
+    if [ "${BRANCH_NAME}" != "master" ]; then
+        SEMABUILD_BASE_ANCESTOR_LAST=`curl --fail --silent "${SEMABUILD_SOURCE}/${BRANCH_NAME}/base.ancestor.txt"`
+        SEMABUILD_DATA_ANCESTOR_LAST=`curl --fail --silent "${SEMABUILD_SOURCE}/${BRANCH_NAME}/data.ancestor.txt"`
+    fi
+    if [ -n "${SEMABUILD_BASE_LAST}" ]; then
+        SEMABUILD_SRC_HASH="${SEMABUILD_BASE_LAST}"
+    elif [ -n "${SEMABUILD_BASE_ANCESTOR}" ]; then
+        SEMABUILD_SRC_HASH="${SEMABUILD_BASE_ANCESTOR}"
+    else
+        echo "Unable to determine a proper ancestor hash!"
+        return 1
+    fi
+    SEMABUILD_BINS_LAST=`curl --fail --silent "${SEMABUILD_SOURCE}/${BRANCH_NAME}/bins.txt"`
+    if [ -n "${SEMABUILD_BINS_LAST}" ]; then
+        SEMABUILD_SRC_CHANGES=`git diff --name-only HEAD ${SEMABUILD_SRC_HASH} -- src` || return 1
+        if [ -z "${SEMABUILD_SRC_CHANGES}" ]; then
+            echo "No source files have been modified"
+            SEMABUILD_DEPLOY="sync"
+        else
+            echo "Source files modified:"
+            echo "${SEMABUILD_SRC_CHANGES}"
+        fi
+    fi
+    return 0
+}
+
+semabuild_build() {
+    echo "Building ${BRANCH_NAME}..."
+    sudo dpkg --add-architecture i386 || return 1
+    sudo ${SEMABUILD_APT} update || return 1
+    sudo ${SEMABUILD_APT} -fy install build-essential zlib1g-dev libsdl-mixer1.2-dev libsdl-image1.2-dev || return 1
+    make PLATFORM=linux64 PLATFORM_BIN=amd64 INSTDIR=${SEMABUILD_DIR}/linux/bin/amd64 CFLAGS=-m64 CXXFLAGS=-m64 LDFLAGS=-m64 -C src clean install || return 1
+    sudo ${SEMABUILD_APT} -fy install binutils-mingw-w64 g++-mingw-w64 || return 1
+    make PLATFORM=crossmingw64 PLATFORM_BIN=amd64 INSTDIR=${SEMABUILD_DIR}/windows/bin/amd64 CFLAGS=-m64 CXXFLAGS=-m64 LDFLAGS=-m64 -C src clean install || return 1
+    make PLATFORM=crossmingw32 PLATFORM_BIN=x86 INSTDIR=${SEMABUILD_DIR}/windows/bin/x86 CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32 -C src clean install || return 1
+    sudo ${SEMABUILD_APT} -fy remove zlib1g-dev libsdl1.2-dev libsdl-mixer1.2-dev libsdl-image1.2-dev libpng-dev || return 1
+    sudo ${SEMABUILD_APT} -fy autoremove || return 1
+    sudo ${SEMABUILD_APT} -fy install build-essential multiarch-support g++-multilib zlib1g-dev:i386 libsdl1.2-dev:i386 libsdl-mixer1.2-dev:i386 libsdl-image1.2-dev:i386 libpng-dev:i386 || return 1
+    make PLATFORM=linux32 PLATFORM_BIN=x86 INSTDIR=${SEMABUILD_DIR}/linux/bin/x86 CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32 -C src clean install || return 1
+    return 0
+}
+
+semabuild_sync() {
+    echo "Syncing ${BRANCH_NAME} as no source files have changed."
+    echo "base data" > "${SEMABUILD_DIR}/modules.txt"
+    if [ -n "${SEMABUILD_BASE}" ] && [ "${SEMABUILD_BASE}" != "${SEMABUILD_BASE_LAST}" ]; then
+        echo "Module 'base' commit updated, syncing that: ${SEMABUILD_BASE} -> ${SEMABUILD_BASE_LAST}"
+        echo "${SEMABUILD_BASE}" > "${SEMABUILD_DIR}/base.txt"
+    fi
+    if [ -n "${SEMABUILD_DATA}" ] && [ "${SEMABUILD_DATA}" != "${SEMABUILD_DATA_LAST}" ]; then
+        echo "Module 'data' commit updated, syncing that: ${SEMABUILD_DATA} -> ${SEMABUILD_DATA_LAST}"
+        echo "${SEMABUILD_DATA}" > "${SEMABUILD_DIR}/data.txt"
+    fi
+    if [ "${BRANCH_NAME}" != "master" ]; then
+        if [ -n "${SEMABUILD_BASE_ANCESTOR}" ] && [ "${SEMABUILD_BASE_ANCESTOR}" != "${SEMABUILD_BASE_ANCESTOR_LAST}" ]; then
+            echo "Module 'base' ancestor updated, syncing that: ${SEMABUILD_BASE_ANCESTOR} -> ${SEMABUILD_BASE_ANCESTOR_LAST}"
+            echo "${SEMABUILD_BASE_ANCESTOR}" > "${SEMABUILD_DIR}/base.ancestor.txt"
+        fi
+        if [ -n "${SEMABUILD_DATA_ANCESTOR}" ] && [ "${SEMABUILD_DATA_ANCESTOR}" != "${SEMABUILD_DATA_ANCESTOR_LAST}" ]; then
+            echo "Module 'data' ancestor updated, syncing that: ${SEMABUILD_DATA_ANCESTOR} -> ${SEMABUILD_DATA_ANCESTOR_LAST}"
+            echo "${SEMABUILD_DATA_ANCESTOR}" > "${SEMABUILD_DIR}/data.ancestor.txt"
+        fi
+    fi
+    return 0
+}
+
+semabuild_deploy() {
+    echo "Deploying ${BRANCH_NAME}..."
+    # windows
+    cd "${SEMABUILD_DIR}/windows" || return 1
+    zip -r "${SEMABUILD_DIR}/windows.zip" . || return 1
+    # linux
+    cd "${SEMABUILD_DIR}/linux" || return 1
+    tar -zcvf "${SEMABUILD_DIR}/linux.tar.gz" . || return 1
+    # env
+    cd "${SEMABUILD_PWD}" || return 1
+    # sha
+    rm -rfv "${SEMABUILD_DIR}/windows" "${SEMABUILD_DIR}/linux" || return 1
+    echo "base data" > "${SEMABUILD_DIR}/modules.txt"
+    echo "Module 'base' commit, syncing: ${SEMABUILD_BASE}"
+    echo "${SEMABUILD_BASE}" > "${SEMABUILD_DIR}/bins.txt"
+    echo "${SEMABUILD_BASE}" > "${SEMABUILD_DIR}/base.txt"
+    echo "Module 'data' commit, syncing: ${SEMABUILD_DATA}"
+    echo "${SEMABUILD_DATA}" > "${SEMABUILD_DIR}/data.txt"
+    if [ "${BRANCH_NAME}" != "master" ]; then
+        echo "Module 'base' ancestor, syncing: ${SEMABUILD_BASE_ANCESTOR}"
+        echo "${SEMABUILD_BASE_ANCESTOR}" > "${SEMABUILD_DIR}/base.ancestor.txt"
+        echo "Module 'data' ancestor, syncing: ${SEMABUILD_DATA_ANCESTOR}"
+        echo "${SEMABUILD_DATA_ANCESTOR}" > "${SEMABUILD_DIR}/data.ancestor.txt"
+    fi
+    return 0
+}
+
+semabuild_send() {
+    echo "Sending ${BRANCH_NAME}..."
+    cd "${SEMABUILD_BUILD}" || return 1
+    if [ -e "${BRANCH_NAME}" ]; then
+        ${SEMABUILD_SCP} -r "${BRANCH_NAME}" "${SEMABUILD_TARGET}" || return 1
+    else
+        echo "Failed to send ${BRANCH_NAME} as the folder doesn't exist!"
+        return 1
+    fi
+    cd "${SEMABUILD_PWD}" || return 1
+    return 0
+}
+
+semabuild_setup
+if [ $? -ne 0 ]; then
+    echo "Failed to setup ${BRANCH_NAME}!"
+    exit 1
+fi
+if [ "${SEMABUILD_DEPLOY}" = "sync" ]; then
+    semabuild_sync
+    if [ $? -ne 0 ]; then
+        echo "Failed to sync ${BRANCH_NAME}!"
+        exit 1
+    fi
+    semabuild_send
+    if [ $? -ne 0 ]; then
+        cd "${SEMABUILD_PWD}"
+        echo "Failed to deploy ${BRANCH_NAME}!"
+        exit 1
+    fi
+else
+    semabuild_build
+    if [ $? -ne 0 ]; then
+        echo "Failed to build ${BRANCH_NAME}!"
+        exit 1
+    fi
+    semabuild_deploy
+    if [ $? -ne 0 ]; then
+        cd "${SEMABUILD_PWD}"
+        echo "Failed to deploy ${BRANCH_NAME}!"
+        exit 1
+    fi
+    semabuild_send
+    if [ $? -ne 0 ]; then
+        cd "${SEMABUILD_PWD}"
+        echo "Failed to deploy ${BRANCH_NAME}!"
+        exit 1
+    fi
+fi
diff --git a/src/semdeploy.sh b/src/semdeploy.sh
new file mode 100644
index 0000000..5551140
--- /dev/null
+++ b/src/semdeploy.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+SEMABUILD_PWD=`pwd`
+SEMABUILD_SCP='scp -BC -o StrictHostKeyChecking=no'
+SEMABUILD_TARGET='qreeves at icculus.org:/webspace/redeclipse.net/files'
+SEMABUILD_APT='DEBIAN_FRONTEND=noninteractive apt-get'
+
+sudo ${SEMABUILD_APT} update || exit 1
+sudo ${SEMABUILD_APT} -fy install build-essential unzip zip nsis nsis-common mktorrent || exit 1
+
+mkdir -p "${HOME}/deploy/data" || exit 1
+( git archive stable | tar -x -C "${HOME}/deploy" ) || exit 1
+pushd "${SEMABUILD_PWD}/data" || exit 1
+( git archive master | tar -x -C "${HOME}/deploy/data" ) || exit 1
+popd || exit 1
+
+pushd "${HOME}/deploy/src" || exit 1
+make dist-clean dist-bz2 dist-bz2-combined dist-win dist-torrents || exit 1
+popd || exit 1
+
+pushd "${HOME}/deploy" || exit 1
+mkdir -p releases || exit 1
+mv -vf redeclipse_*.*_*.tar.bz2 releases/ || exit 1
+mv -vf redeclipse_*.*_*.exe releases/ || exit 1
+${SEMABUILD_SCP} -r "releases" "${SEMABUILD_TARGET}" || exit 1
+popd || exit 1
diff --git a/src/shared/command.h b/src/shared/command.h
index ee0d539..2279b8e 100644
--- a/src/shared/command.h
+++ b/src/shared/command.h
@@ -99,69 +99,76 @@ typedef void (__cdecl *identfun)();
 
 struct ident
 {
-    int type;           // one of ID_* above
-    const char *name;
-    union
-    {
-        int minval;    // ID_VAR
-        float minvalf; // ID_FVAR
-        int valtype;   // ID_ALIAS
-    };
+    uchar type; // one of ID_* above
     union
     {
-        int maxval;    // ID_VAR
-        float maxvalf; // ID_FVAR
-        uint *code;    // ID_ALIAS
-    };
-    union
-    {
-        const char *args;    // ID_COMMAND
-        identval val;        // ID_ALIAS
-        identvalptr storage; // ID_VAR, ID_FVAR, ID_SVAR
+        uchar valtype; // ID_ALIAS
+        uchar numargs; // ID_COMMAND
     };
+    ushort flags;
+    int index;
+    const char *name;
     union
     {
-        identval overrideval; // ID_VAR, ID_FVAR, ID_SVAR
-        identstack *stack;    // ID_ALIAS
-        uint argmask;         // ID_COMMAND
+        struct // ID_VAR, ID_FVAR, ID_SVAR
+        {
+            union
+            {
+                struct { int minval, maxval; };     // ID_VAR
+                struct { float minvalf, maxvalf; }; // ID_FVAR
+            };
+            identvalptr storage;
+            identval overrideval;
+            identval def; // declared-default (by *init.cfg)
+            identval bin; // builtin-default (hard coded or version.cfg)
+        };
+        struct // ID_ALIAS
+        {
+            uint *code;
+            identval val;
+            identstack *stack;
+        };
+        struct // ID_COMMAND
+        {
+            const char *args;
+            uint argmask;
+        };
     };
     identfun fun; // ID_VAR, ID_FVAR, ID_SVAR, ID_COMMAND
-    identval def;
-    int flags, index;
     char *desc, *usage;
 
     ident() {}
     // ID_VAR
     ident(int t, const char *n, int m, int c, int x, int *s, void *f = NULL, int flags = IDF_COMPLETE)
-        : type(t), name(n), minval(m), maxval(x), fun((identfun)f), flags(flags | (m > x ? IDF_READONLY : 0)), desc(NULL), usage(NULL)
-    { def.i = c; storage.i = s; }
+        : type(t), flags(flags | (m > x ? IDF_READONLY : 0)), name(n), minval(m), maxval(x), fun((identfun)f), desc(NULL), usage(NULL)
+    { def.i = c; bin.i = c; storage.i = s; }
     // ID_FVAR
     ident(int t, const char *n, float m, float c, float x, float *s, void *f = NULL, int flags = IDF_COMPLETE)
-        : type(t), name(n), minvalf(m), maxvalf(x), fun((identfun)f), flags(flags | (m > x ? IDF_READONLY : 0)), desc(NULL), usage(NULL)
-    { def.f = c; storage.f = s; }
+        : type(t), flags(flags | (m > x ? IDF_READONLY : 0)), name(n), minvalf(m), maxvalf(x), fun((identfun)f), desc(NULL), usage(NULL)
+    { def.f = c; bin.f = c; storage.f = s; }
     // ID_SVAR
     ident(int t, const char *n, char *c, char **s, void *f = NULL, int flags = IDF_COMPLETE)
-        : type(t), name(n), fun((identfun)f), flags(flags), desc(NULL), usage(NULL)
-    { def.s = c; storage.s = s; }
+        : type(t), flags(flags), name(n), fun((identfun)f), desc(NULL), usage(NULL)
+    { def.s = c; bin.s = newstring(c); storage.s = s; }
     // ID_ALIAS
     ident(int t, const char *n, char *a, int flags)
-        : type(t), name(n), valtype(VAL_STR), code(NULL), stack(NULL), flags(flags), desc(NULL), usage(NULL)
+        : type(t), valtype(VAL_STR), flags(flags), name(n), code(NULL), stack(NULL), desc(NULL), usage(NULL)
     { val.s = a; }
     ident(int t, const char *n, int a, int flags)
-        : type(t), name(n), valtype(VAL_INT), code(NULL), stack(NULL), flags(flags), desc(NULL), usage(NULL)
+        : type(t), valtype(VAL_INT), flags(flags), name(n), code(NULL), stack(NULL), desc(NULL), usage(NULL)
     { val.i = a; }
     ident(int t, const char *n, float a, int flags)
-        : type(t), name(n), valtype(VAL_FLOAT), code(NULL), stack(NULL), flags(flags), desc(NULL), usage(NULL)
+        : type(t), valtype(VAL_FLOAT), flags(flags), name(n), code(NULL), stack(NULL), desc(NULL), usage(NULL)
     { val.f = a; }
     ident(int t, const char *n, int flags)
-        : type(t), name(n), valtype(VAL_NULL), code(NULL), stack(NULL), flags(flags), desc(NULL), usage(NULL)
+        : type(t), valtype(VAL_NULL), flags(flags), name(n), code(NULL), stack(NULL), desc(NULL), usage(NULL)
     {}
     ident(int t, const char *n, const tagval &v, int flags)
-        : type(t), name(n), valtype(v.type), code(NULL), stack(NULL), flags(flags), desc(NULL), usage(NULL)
+        : type(t), valtype(v.type), flags(flags), name(n), code(NULL), stack(NULL), desc(NULL), usage(NULL)
     { val = v; }
     // ID_COMMAND
-    ident(int t, const char *n, const char *args, uint argmask, void *f = NULL, int flags = IDF_COMPLETE)
-        : type(t), name(n), args(args), argmask(argmask), fun((identfun)f), flags(flags), desc(NULL), usage(NULL)
+    ident(int t, const char *n, const char *args, uint argmask, int numargs, void *f = NULL, int flags = IDF_COMPLETE)
+        : type(t), numargs(numargs), flags(flags), name(n), args(args), argmask(argmask), fun((identfun)f), desc(NULL), usage(NULL)
     {}
 
     void changed() { if(fun) fun(); }
@@ -206,11 +213,21 @@ extern void floatret(float v);
 extern void stringret(char *s);
 extern void result(tagval &v);
 extern void result(const char *s);
-extern char *conc(tagval *v, int n, bool space, const char *prefix = NULL);
+extern char *conc(tagval *v, int n, bool space, const char *prefix, int prefixlen);
+
+static inline char *conc(tagval *v, int n, bool space)
+{
+    return conc(v, n, space, NULL, 0);
+}
+
+static inline char *conc(tagval *v, int n, bool space, const char *prefix)
+{
+    return conc(v, n, space, prefix, strlen(prefix));
+}
 
 static inline int parseint(const char *s)
 {
-    return int(strtol(s, NULL, 0));
+    return int(strtoul(s, NULL, 0));
 }
 
 static inline float parsefloat(const char *s)
@@ -221,6 +238,9 @@ static inline float parsefloat(const char *s)
     return val || end==s || (*end!='x' && *end!='X') ? float(val) : float(parseint(s));
 }
 
+static inline void intformat(char *buf, int v) { formatstring(buf)("%d", v); }
+static inline void floatformat(char *buf, float v) { formatstring(buf)(v==int(v) ? "%.1f" : "%.7g", v); }
+
 static inline const char *getstr(const identval &v, int type)
 {
     switch(type)
@@ -283,7 +303,7 @@ extern void touchvar(const char *name);
 extern int getvar(const char *name);
 extern int getvarmin(const char *name);
 extern int getvarmax(const char *name);
-extern int getvardef(const char *name);
+extern int getvardef(const char *name, bool rb = false);
 extern float getfvarmin(const char *name);
 extern float getfvarmax(const char *name);
 extern bool identexists(const char *name);
@@ -306,12 +326,19 @@ extern int execute(const char *p);
 extern int execute(const char *p, bool nonworld);
 extern bool executebool(const uint *code);
 extern bool executebool(const char *p);
-enum { EXEC_NOWORLD = 1<<0, EXEC_VERSION = 1<<1 };
+enum { EXEC_NOWORLD = 1<<0, EXEC_VERSION = 1<<1, EXEC_BUILTIN = 1<<2 };
 extern bool execfile(const char *cfgfile, bool msg = true, int flags = 0);
 extern void alias(const char *name, const char *action);
 extern void alias(const char *name, tagval &v);
 extern void worldalias(const char *name, const char *action);
 extern const char *getalias(const char *name);
+extern void loopiter(ident *id, identstack &stack, const tagval &v);
+extern void loopend(ident *id, identstack &stack);
+
+#define loopstart(id, stack) if((id)->type != ID_ALIAS) return; identstack stack;
+static inline void loopiter(ident *id, identstack &stack, int i) { tagval v; v.setint(i); loopiter(id, stack, v); }
+static inline void loopiter(ident *id, identstack &stack, float f) { tagval v; v.setfloat(f); loopiter(id, stack, v); }
+static inline void loopiter(ident *id, identstack &stack, const char *s) { tagval v; v.setstr(newstring(s)); loopiter(id, stack, v); }
 
 extern int identflags;
 extern bool interactive;
@@ -332,11 +359,12 @@ extern void checksleep(int millis);
 extern void clearsleep(bool clearworlds = true);
 
 extern char *logtimeformat, *filetimeformat;
+extern int filetimelocal;
 extern const char *gettime(time_t ctime = 0, const char *format = NULL);
 
 // nasty macros for registering script functions, abuses globals to avoid excessive infrastructure
-#define KEYWORD(flags, name, type) static bool __dummy_##name = addkeyword(type, #name, flags)
-#define COMMANDN(flags, name, fun, nargs) static bool __dummy_##fun = addcommand(#name, (identfun)fun, nargs, flags|IDF_COMPLETE)
+#define KEYWORD(flags, name, type) UNUSED static bool __dummy_##name = addkeyword(type, #name, flags)
+#define COMMANDN(flags, name, fun, nargs) UNUSED static bool __dummy_##fun = addcommand(#name, (identfun)fun, nargs, flags|IDF_COMPLETE)
 #define COMMAND(flags, name, nargs) COMMANDN(flags, name, name, nargs)
 
 // anonymous inline commands, uses nasty template trick with line numbers to keep names unique
diff --git a/src/shared/crypto.cpp b/src/shared/crypto.cpp
index 3e8665d..ab6cd6e 100644
--- a/src/shared/crypto.cpp
+++ b/src/shared/crypto.cpp
@@ -249,6 +249,8 @@ template<int BI_DIGITS> struct bigint
 
     bool hasbit(int n) const { return n/BI_DIGIT_BITS < len && ((digits[n/BI_DIGIT_BITS]>>(n%BI_DIGIT_BITS))&1); }
 
+    bool morebits(int n) const { return len > n/BI_DIGIT_BITS; }
+
     template<int X_DIGITS, int Y_DIGITS> bigint &add(const bigint<X_DIGITS> &x, const bigint<Y_DIGITS> &y)
     {
         dbldigit carry = 0;
@@ -284,7 +286,20 @@ template<int BI_DIGITS> struct bigint
     template<int Y_DIGITS> bigint &sub(const bigint<Y_DIGITS> &y) { return sub(*this, y); }
 
     void shrink() { while(len && !digits[len-1]) len--; }
+    void shrinkdigits(int n) { len = n; shrink(); }
+    void shrinkbits(int n) { shrinkdigits(n/BI_DIGIT_BITS); }
 
+    template<int Y_DIGITS> void copyshrinkdigits(const bigint<Y_DIGITS> &y, int n)
+    {
+        len = min(y.len, n);
+        memcpy(digits, y.digits, len*sizeof(digit));
+        shrink();
+    }
+    template<int Y_DIGITS> void copyshrinkbits(const bigint<Y_DIGITS> &y, int n)
+    {
+        copyshrinkdigits(y, n/BI_DIGIT_BITS);
+    }
+    
     template<int X_DIGITS, int Y_DIGITS> bigint &mul(const bigint<X_DIGITS> &x, const bigint<Y_DIGITS> &y)
     {
         if(!x.len || !y.len) { len = 0; return *this; }
@@ -305,34 +320,34 @@ template<int BI_DIGITS> struct bigint
         return *this;
     }
 
-    template<int X_DIGITS> bigint &rshift(const bigint<X_DIGITS> &x, int n)
+    bigint &rshift(int n)
     {
-        if(!len || !n) return *this;
+        if(!len || n<=0) return *this;
+        if(n >= len*BI_DIGIT_BITS) { len = 0; return *this; }
         int dig = (n-1)/BI_DIGIT_BITS;
         n = ((n-1) % BI_DIGIT_BITS)+1;
-        digit carry = digit(x.digits[dig]>>n);
-        loopi(len-dig-1)
+        digit carry = digit(digits[dig]>>n);
+        for(int i = dig+1; i < len; i++)
         {
-            digit tmp = x.digits[i+dig+1];
-            digits[i] = digit((tmp<<(BI_DIGIT_BITS-n)) | carry);
+            digit tmp = digits[i];
+            digits[i-dig-1] = digit((tmp<<(BI_DIGIT_BITS-n)) | carry);
             carry = digit(tmp>>n);
         }
         digits[len-dig-1] = carry;
-        len -= dig + (n>>BI_DIGIT_BITS);
+        len -= dig + (n/BI_DIGIT_BITS);
         shrink();
         return *this;
     }
-    bigint &rshift(int n) { return rshift(*this, n); }
 
-    template<int X_DIGITS> bigint &lshift(const bigint<X_DIGITS> &x, int n)
+    bigint &lshift(int n)
     {
-        if(!len || !n) return *this;
+        if(!len || n<=0) return *this;
         int dig = n/BI_DIGIT_BITS;
         n %= BI_DIGIT_BITS;
         digit carry = 0;
-        for(int i = len-1; i>=0; i--)
+        loopirev(len)
         {
-            digit tmp = x.digits[i];
+            digit tmp = digits[i];
             digits[i+dig] = digit((tmp<<n) | carry);
             carry = digit(tmp>>(BI_DIGIT_BITS-n));
         }
@@ -341,12 +356,40 @@ template<int BI_DIGITS> struct bigint
         if(dig) memset(digits, 0, dig*sizeof(digit));
         return *this;
     }
-    bigint &lshift(int n) { return lshift(*this, n); }
+
+    void zerodigits(int i, int n)
+    {
+        memset(&digits[i], 0, n*sizeof(digit));
+    }
+    void zerobits(int i, int n)
+    {
+        zerodigits(i/BI_DIGIT_BITS, n/BI_DIGIT_BITS); 
+    }
+    
+    template<int Y_DIGITS> void copydigits(int to, const bigint<Y_DIGITS> &y, int from, int n)
+    {
+        int avail = min(y.len-from, n);
+        memcpy(&digits[to], &y.digits[from], avail*sizeof(digit));
+        if(avail < n) memset(&digits[to+avail], 0, (n-avail)*sizeof(digit));
+    }
+    template<int Y_DIGITS> void copybits(int to, const bigint<Y_DIGITS> &y, int from, int n)
+    {
+        copydigits(to/BI_DIGIT_BITS, y, from/BI_DIGIT_BITS, n/BI_DIGIT_BITS);
+    }
+
+    void dupdigits(int to, int from, int n)
+    {
+        memcpy(&digits[to], &digits[from], n*sizeof(digit));
+    }
+    void dupbits(int to, int from, int n)
+    {
+        dupdigits(to/BI_DIGIT_BITS, from/BI_DIGIT_BITS, n/BI_DIGIT_BITS);
+    }
 
     template<int Y_DIGITS> bool operator==(const bigint<Y_DIGITS> &y) const
     {
         if(len!=y.len) return false;
-        for(int i = len-1; i>=0; i--) if(digits[i]!=y.digits[i]) return false;
+        loopirev(len) if(digits[i]!=y.digits[i]) return false;
         return true;
     }
     template<int Y_DIGITS> bool operator!=(const bigint<Y_DIGITS> &y) const { return !(*this==y); }
@@ -354,7 +397,7 @@ template<int BI_DIGITS> struct bigint
     {
         if(len<y.len) return true;
         if(len>y.len) return false;
-        for(int i = len-1; i>=0; i--)
+        loopirev(len)
         {
             if(digits[i]<y.digits[i]) return true;
             if(digits[i]>y.digits[i]) return false;
@@ -372,7 +415,7 @@ template<int BI_DIGITS> struct bigint
 typedef bigint<GF_DIGITS+1> gfint;
 
 /* NIST prime Galois fields.
- * Currently only supports NIST P-192, where P=2^192-2^64-1.
+ * Currently only supports NIST P-192, where P=2^192-2^64-1, and P-256, where P=2^256-2^224+2^192+2^96-1.
  */
 struct gfield : gfint
 {
@@ -401,13 +444,12 @@ struct gfield : gfint
     template<int X_DIGITS> gfield &mul2(const bigint<X_DIGITS> &x) { return add(x, x); }
     gfield &mul2() { return mul2(*this); }
 
-    template<int X_DIGITS> gfield &div2(const bigint<X_DIGITS> &x)
+    gfield &div2()
     {
-        if(hasbit(0)) { gfint::add(x, P); rshift(1); }
-        else rshift(x, 1);
+        if(hasbit(0)) gfint::add(*this, P);
+        rshift(1);
         return *this;
     }
-    gfield &div2() { return div2(*this); }
 
     template<int X_DIGITS, int Y_DIGITS> gfield &sub(const bigint<X_DIGITS> &x, const bigint<Y_DIGITS> &y)
     {
@@ -444,43 +486,102 @@ struct gfield : gfint
     template<int RESULT_DIGITS> void reduce(const bigint<RESULT_DIGITS> &result)
     {
 #if GF_BITS==192
-        len = min(result.len, GF_DIGITS);
-        memcpy(digits, result.digits, len*sizeof(digit));
-        shrink();
+        // B = T + S1 + S2 + S3 mod p
+        copyshrinkdigits(result, GF_DIGITS); // T
 
-        if(result.len > 192/BI_DIGIT_BITS)
+        if(result.morebits(192))
         {
             gfield s;
-            memcpy(s.digits, &result.digits[192/BI_DIGIT_BITS], min(result.len-192/BI_DIGIT_BITS, 64/BI_DIGIT_BITS)*sizeof(digit));
-            if(result.len < 256/BI_DIGIT_BITS) memset(&s.digits[result.len-192/BI_DIGIT_BITS], 0, (256/BI_DIGIT_BITS-result.len)*sizeof(digit));
-            memcpy(&s.digits[64/BI_DIGIT_BITS], s.digits, 64/BI_DIGIT_BITS*sizeof(digit));
-            s.len = 128/BI_DIGIT_BITS;
-            s.shrink();
-            add(s);
-
-            if(result.len > 256/BI_DIGIT_BITS)
+            s.copybits(0, result, 192, 64);
+            s.dupbits(64, 0, 64);
+            s.shrinkbits(128);
+            add(s); // S1
+
+            if(result.morebits(256))
             {
-                memset(s.digits, 0, 64/BI_DIGIT_BITS*sizeof(digit));
-                memcpy(&s.digits[64/BI_DIGIT_BITS], &result.digits[256/BI_DIGIT_BITS], min(result.len-256/BI_DIGIT_BITS, 64/BI_DIGIT_BITS)*sizeof(digit));
-                if(result.len < 320/BI_DIGIT_BITS) memset(&s.digits[result.len+(64-256)/BI_DIGIT_BITS], 0, (320/BI_DIGIT_BITS-result.len)*sizeof(digit));
-                memcpy(&s.digits[128/BI_DIGIT_BITS], &s.digits[64/BI_DIGIT_BITS], 64/BI_DIGIT_BITS*sizeof(digit));
-                s.len = GF_DIGITS;
-                s.shrink();
-                add(s);
-
-                if(result.len > 320/BI_DIGIT_BITS)
+                s.zerobits(0, 64);
+                s.copybits(64, result, 256, 64);
+                s.dupbits(128, 64, 64);
+                s.shrinkdigits(GF_DIGITS);
+                add(s); // S2
+
+                if(result.morebits(320))
                 {
-                    memcpy(s.digits, &result.digits[320/BI_DIGIT_BITS], min(result.len-320/BI_DIGIT_BITS, 64/BI_DIGIT_BITS)*sizeof(digit));
-                    if(result.len < 384/BI_DIGIT_BITS) memset(&s.digits[result.len-320/BI_DIGIT_BITS], 0, (384/BI_DIGIT_BITS-result.len)*sizeof(digit));
-                    memcpy(&s.digits[64/BI_DIGIT_BITS], s.digits, 64/BI_DIGIT_BITS*sizeof(digit));
-                    memcpy(&s.digits[128/BI_DIGIT_BITS], s.digits, 64/BI_DIGIT_BITS*sizeof(digit));
-                    s.len = GF_DIGITS;
-                    s.shrink();
-                    add(s);
+                    s.copybits(0, result, 320, 64);
+                    s.dupbits(64, 0, 64);
+                    s.dupbits(128, 0, 64);
+                    s.shrinkdigits(GF_DIGITS);
+                    add(s); // S3
                 }
             }
         }
         else if(*this >= P) gfint::sub(*this, P);
+#elif GF_BITS==256
+        // B = T + 2*S1 + 2*S2 + S3 + S4 - D1 - D2 - D3 - D4 mod p
+        copyshrinkdigits(result, GF_DIGITS); // T
+
+        if(result.morebits(256))
+        {
+            gfield s;
+            if(result.morebits(352))
+            {
+                s.zerobits(0, 96);
+                s.copybits(96, result, 352, 160);
+                s.shrinkdigits(GF_DIGITS);
+                add(s); add(s); // S1
+            
+                if(result.morebits(384))
+                {
+                    //s.zerobits(0, 96);
+                    s.copybits(96, result, 384, 128);
+                    s.shrinkbits(224);
+                    add(s); add(s); // S2
+                }
+            }
+
+            s.copybits(0, result, 256, 96);
+            s.zerobits(96, 96);
+            s.copybits(192, result, 448, 64);
+            s.shrinkdigits(GF_DIGITS);
+            add(s); // S3
+           
+            s.copybits(0, result, 288, 96);
+            s.copybits(96, result, 416, 96);
+            s.dupbits(192, 96, 32);
+            s.copybits(224, result, 256, 32); 
+            s.shrinkdigits(GF_DIGITS);
+            add(s); // S4
+
+            s.copybits(0, result, 352, 96);
+            s.zerobits(96, 96);
+            s.copybits(192, result, 256, 32);
+            s.copybits(224, result, 320, 32);
+            s.shrinkdigits(GF_DIGITS);
+            sub(s); // D1
+
+            s.copybits(0, result, 384, 128);
+            //s.zerobits(128, 64);
+            s.copybits(192, result, 288, 32);
+            s.copybits(224, result, 352, 32);
+            s.shrinkdigits(GF_DIGITS);
+            sub(s); // D2
+
+            s.copybits(0, result, 416, 96);
+            s.copybits(96, result, 256, 96);
+            s.zerobits(192, 32);
+            s.copybits(224, result, 384, 32);
+            s.shrinkdigits(GF_DIGITS);
+            sub(s); // D3
+
+            s.copybits(0, result, 448, 64);
+            s.zerobits(64, 32);
+            s.copybits(96, result, 288, 96);
+            //s.zerobits(192, 32);
+            s.copybits(224, result, 416, 32);
+            s.shrinkdigits(GF_DIGITS);
+            sub(s); // D4
+        }
+        else if(*this >= P) gfint::sub(*this, P);
 #else
 #error Unsupported GF
 #endif
@@ -647,16 +748,16 @@ struct ecjacobian
         y.sub(f, x).sub(x).mul(b).sub(e.mul(a).mul(d)).div2();
     }
 
-    template<int Q_DIGITS> void mul(const ecjacobian &p, const bigint<Q_DIGITS> q)
+    template<int Q_DIGITS> void mul(const ecjacobian &p, const bigint<Q_DIGITS> &q)
     {
         *this = origin;
-        for(int i = q.numbits()-1; i >= 0; i--)
+        loopirev(q.numbits())
         {
             mul2();
             if(q.hasbit(i)) add(p);
         }
     }
-    template<int Q_DIGITS> void mul(const bigint<Q_DIGITS> q) { ecjacobian tmp(*this); mul(tmp, q); }
+    template<int Q_DIGITS> void mul(const bigint<Q_DIGITS> &q) { ecjacobian tmp(*this); mul(tmp, q); }
 
     void normalize()
     {
@@ -708,21 +809,21 @@ const gfield gfield::P("ffffffffffffffffffffffffffffffff000000000000000000000001
 const gfield ecjacobian::B("b4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4");
 const ecjacobian ecjacobian::base(
     gfield("b70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21"),
-    gfield("bd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34"),
+    gfield("bd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34")
 );
 #elif GF_BITS==256
 const gfield gfield::P("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
 const gfield ecjacobian::B("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
 const ecjacobian ecjacobian::base(
     gfield("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"),
-    gfield("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5"),
+    gfield("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5")
 );
 #elif GF_BITS==384
 const gfield gfield::P("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff");
 const gfield ecjacobian::B("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef");
 const ecjacobian ecjacobian::base(
     gfield("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"),
-    gfield("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f"),
+    gfield("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f")
 );
 #elif GF_BITS==521
 const gfield gfield::P("1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
@@ -807,7 +908,7 @@ void freepubkey(void *pubkey)
 void *genchallenge(void *pubkey, const void *seed, int seedlen, vector<char> &challengestr)
 {
     tiger::hashval hash;
-    tiger::hash((const uchar *)seed, sizeof(seed), hash);
+    tiger::hash((const uchar *)seed, seedlen, hash);
     gfint challenge;
     memcpy(challenge.digits, hash.bytes, sizeof(hash.bytes));
     challenge.len = 8*sizeof(hash.bytes)/BI_DIGIT_BITS;
diff --git a/src/shared/cube.h b/src/shared/cube.h
index 35e3874..1691409 100644
--- a/src/shared/cube.h
+++ b/src/shared/cube.h
@@ -22,6 +22,7 @@
 #include <ctype.h>
 #include <stdarg.h>
 #include <limits.h>
+#include <float.h>
 #include <assert.h>
 #include <time.h>
 
@@ -48,22 +49,12 @@
 
 #ifndef STANDALONE
 #include <SDL.h>
-#include <SDL_image.h>
 #include <SDL_syswm.h>
 #include <SDL_opengl.h>
 #endif
 
 #include <enet/enet.h>
 
-#ifdef __sun__
-#undef sun
-#undef MAXNAMELEN
-#ifdef queue
-  #undef queue
-#endif
-#define queue __squeue
-#endif
-
 #include "tools.h"
 #include "command.h"
 #include "geom.h"
diff --git a/src/shared/cube2font.c b/src/shared/cube2font.c
index cb62df2..1e260c7 100644
--- a/src/shared/cube2font.c
+++ b/src/shared/cube2font.c
@@ -425,7 +425,8 @@ int main(int argc, char **argv)
         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);
+        if(inborder > 0 || outborder > 0) FT_Glyph_To_Bitmap(&p2, FT_RENDER_MODE_NORMAL, 0, 1);
+        else p2 = p;
         b = (FT_BitmapGlyph)p;
         b2 = (FT_BitmapGlyph)p2;
         dst->tex = -1;
@@ -543,7 +544,7 @@ int main(int argc, char **argv)
     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);
+        if(chars[i].alpha != chars[i].color) FT_Done_Glyph((FT_Glyph)chars[i].alpha);
         FT_Done_Glyph((FT_Glyph)chars[i].color);
     }
     FT_Stroker_Done(s);
diff --git a/src/shared/ents.h b/src/shared/ents.h
index 7b8108b..fd367f3 100644
--- a/src/shared/ents.h
+++ b/src/shared/ents.h
@@ -41,30 +41,27 @@ struct entity : entbase
 {
     attrvector attrs;
     linkvector links;
+};
 
-    entity() { reset(); }
-
-    void reset()
-    {
-        attrs.setsize(0);
-        links.setsize(0);
-    }
+enum
+{
+    EF_OCTA      = 1<<0,
+    EF_RENDER    = 1<<1,
+    EF_SPAWNED   = 1<<2
 };
 
 struct extentity : entity                       // part of the entity that doesn't get saved to disk
 {
-    uchar spawned, inoctanode, visible;        // the only dynamic state of a map entity
+    int flags;        // the only dynamic state of a map entity
     entitylight light;
     int lastemit, emit[3];
 
-    extentity() { reset(); }
+    extentity() : flags(0), lastemit(0) { emit[0] = emit[1] = emit[2] = 0; }
 
-    void reset()
-    {
-        entity::reset();
-        spawned = inoctanode = visible = false;
-        lastemit = emit[0] = emit[1] = emit[2] = 0;
-    }
+    bool spawned() const { return (flags&EF_SPAWNED) != 0; }
+    void setspawned(bool val) { if(val) flags |= EF_SPAWNED; else flags &= ~EF_SPAWNED; }
+    void setspawned() { flags |= EF_SPAWNED; }
+    void clearspawned() { flags &= ~EF_SPAWNED; }
 };
 
 #define MAXENTS 30000
@@ -72,12 +69,13 @@ struct extentity : entity                       // part of the entity that doesn
 #define MAXENTKIN 100
 
 extern int efocus, enthover, entorient;
-#define entfocus(i, f)  { int n = efocus = (i); if(n>=0) { extentity &e = *entities::getents()[n]; f; } }
+#define entfocusv(i, f, v){ int n = efocus = (i); if(n>=0) { extentity &e = *v[n]; f; } }
+#define entfocus(i, f)    entfocusv(i, f, entities::getents())
 
 enum { CS_ALIVE = 0, CS_DEAD, CS_EDITING, CS_SPECTATOR, CS_WAITING }; // beware, some stuff uses >= CS_SPECTATOR
 enum { PHYS_FLOAT = 0, PHYS_FALL, PHYS_SLIDE, PHYS_SLOPE, PHYS_FLOOR, PHYS_STEP_UP, PHYS_STEP_DOWN };
 enum { ENT_PLAYER = 0, ENT_AI, ENT_INANIMATE, ENT_CAMERA, ENT_PROJ, ENT_RAGDOLL, ENT_DUMMY };
-enum { COLLIDE_AABB = 0, COLLIDE_OBB, COLLIDE_ELLIPSE };
+enum { COLLIDE_NONE = 0, COLLIDE_ELLIPSE, COLLIDE_OBB, COLLIDE_ELLIPSE_PRECISE };
 
 struct physent                                  // base entity type, can be affected by physics
 {
@@ -150,7 +148,7 @@ struct physent                                  // base entity type, can be affe
     vec abovehead(float offset = 1) const { return vec(o).add(vec(0, 0, aboveeye+offset)); }
     vec feetpos(float offset = 0) const { return vec(o).add(vec(0, 0, offset-height)); }
     vec headpos(float offset = 0) const { return vec(o).add(vec(0, 0, offset)); }
-    vec center() const { return vec(o).sub(vec(0, 0, height/2)); }
+    vec center() const { return vec(o).sub(vec(0, 0, height*0.5f)); }
 };
 
 enum
diff --git a/src/shared/geom.cpp b/src/shared/geom.cpp
index d568a56..91bcb71 100644
--- a/src/shared/geom.cpp
+++ b/src/shared/geom.cpp
@@ -22,12 +22,18 @@ void vecfromyawpitch(float yaw, float pitch, int move, int strafe, vec &m)
         m.x += strafe*cosf(RAD*yaw);
         m.y += strafe*sinf(RAD*yaw);
     }
+
+    if(!m.iszero()) m.normalize();
 }
 
 void vectoyawpitch(const vec &v, float &yaw, float &pitch)
 {
-    yaw = -atan2(v.x, v.y)/RAD;
-    pitch = asin(v.z/v.magnitude())/RAD;
+    if(v.iszero()) yaw = pitch = 0;
+    else
+    {
+        yaw = -atan2(v.x, v.y)/RAD;
+        pitch = asin(v.z/v.magnitude())/RAD;
+    }
 }
 
 static inline float det2x2(float a, float b, float c, float d) { return a*d - b*c; }
@@ -85,7 +91,7 @@ bool glmatrixf::invert(const glmatrixf &m, float mindet)
 {
     float a1 = m.v[0], b1 = m.v[4], c1 = m.v[8], d1 = m.v[12];
     adjoint(m);
-    float det = a1*v[0] + b1*v[1] + c1*v[2] + d1*v[3]; // float det = m.determinant(); 
+    float det = a1*v[0] + b1*v[1] + c1*v[2] + d1*v[3]; // float det = m.determinant();
     if(fabs(det) < mindet) return false;
     float invdet = 1/det;
     loopi(16) v[i] *= invdet;
@@ -105,7 +111,7 @@ bool raysphereintersect(const vec &center, float radius, const vec &o, const vec
     return true;
 }
 
-bool rayrectintersect(const vec &b, const vec &s, const vec &o, const vec &ray, float &dist, int &orient)
+bool rayboxintersect(const vec &b, const vec &s, const vec &o, const vec &ray, float &dist, int &orient)
 {
     loop(d, 3) if(ray[d])
     {
@@ -304,6 +310,6 @@ extern const vec2 sincos360[721] =
     vec2(0.95105652, -0.30901699), vec2(0.95630476, -0.29237170), vec2(0.96126170, -0.27563736), vec2(0.96592583, -0.25881905), vec2(0.97029573, -0.24192190), vec2(0.97437006, -0.22495105), // 702
     vec2(0.97814760, -0.20791169), vec2(0.98162718, -0.19080900), vec2(0.98480775, -0.17364818), vec2(0.98768834, -0.15643447), vec2(0.99026807, -0.13917310), vec2(0.99254615, -0.12186934), // 708
     vec2(0.99452190, -0.10452846), vec2(0.99619470, -0.08715574), vec2(0.99756405, -0.06975647), vec2(0.99862953, -0.05233596), vec2(0.99939083, -0.03489950), vec2(0.99984770, -0.01745241), // 714
-    vec2(1.00000000, 0.00000000) // 720        
+    vec2(1.00000000, 0.00000000) // 720
 };
 
diff --git a/src/shared/geom.h b/src/shared/geom.h
index f3d1416..f25ff85 100644
--- a/src/shared/geom.h
+++ b/src/shared/geom.h
@@ -1,5 +1,59 @@
+struct vec;
 struct vec4;
-struct vec2;
+
+struct vec2
+{
+    union
+    {
+        struct { float x, y; };
+        float v[2];
+    };
+
+    vec2() {}
+    vec2(float x, float y) : x(x), y(y) {}
+    explicit vec2(const vec &v);
+    explicit vec2(const vec4 &v);
+
+    float &operator[](int i)       { return v[i]; }
+    float  operator[](int i) const { return v[i]; }
+
+    bool operator==(const vec2 &o) const { return x == o.x && y == o.y; }
+    bool operator!=(const vec2 &o) const { return x != o.x || y != o.y; }
+
+    bool iszero() const { return x==0 && y==0; }
+    float dot(const vec2 &o) const  { return x*o.x + y*o.y; }
+    float squaredlen() const { return dot(*this); }
+    float magnitude() const  { return sqrtf(squaredlen()); }
+    vec2 &normalize() { mul(1/magnitude()); return *this; }
+    float cross(const vec2 &o) const { return x*o.y - y*o.x; }
+
+    vec2 &mul(float f)       { x *= f; y *= f; return *this; }
+    vec2 &mul(const vec2 &o) { x *= o.x; y *= o.y; return *this; }
+    vec2 &div(float f)       { x /= f; y /= f; return *this; }
+    vec2 &div(const vec2 &o) { x /= o.x; y /= o.y; return *this; }
+    vec2 &add(float f)       { x += f; y += f; return *this; }
+    vec2 &add(const vec2 &o) { x += o.x; y += o.y; return *this; }
+    vec2 &sub(float f)       { x -= f; y -= f; return *this; }
+    vec2 &sub(const vec2 &o) { x -= o.x; y -= o.y; return *this; }
+    vec2 &neg()              { x = -x; y = -y; return *this; }
+
+    vec2 &lerp(const vec2 &b, float t) { x += (b.x-x)*t; y += (b.y-y)*t; return *this; }
+    vec2 &lerp(const vec2 &a, const vec2 &b, float t) { x = a.x + (b.x-a.x)*t; y = a.y + (b.y-a.y)*t; return *this; }
+    vec2 &avg(const vec2 &b) { add(b); mul(0.5f); return *this; }
+};
+
+static inline bool htcmp(const vec2 &x, const vec2 &y)
+{
+    return x == y;
+}
+
+static inline uint hthash(const vec2 &k)
+{
+    union { uint i; float f; } x, y;
+    x.f = k.x; y.f = k.y;
+    uint v = x.i^y.i;
+    return v + (v>>12);
+}
 
 struct vec
 {
@@ -16,10 +70,10 @@ struct vec
     vec(float a, float b, float c) : x(a), y(b), z(c) {}
     explicit vec(int v[3]) : x(v[0]), y(v[1]), z(v[2]) {}
     explicit vec(float *v) : x(v[0]), y(v[1]), z(v[2]) {}
+    explicit vec(const vec2 &v, float z = 0) : x(v.x), y(v.y), z(z) {}
     explicit vec(const vec4 &v);
-    explicit vec(const vec2 &v, float z = 0);
 
-    vec(float yaw, float pitch) : x(-sinf(yaw)*cosf(pitch)), y(cosf(yaw)*cosf(pitch)), z(sinf(pitch)) {}
+    vec(float yaw, float pitch) : x(-sinf(yaw)*cosf(pitch)), y(cosf(yaw)*cosf(pitch)), z(sinf(pitch)) { if(!iszero()) normalize(); }
 
     float &operator[](int i)       { return v[i]; }
     float  operator[](int i) const { return v[i]; }
@@ -31,8 +85,10 @@ struct vec
 
     bool iszero() const { return x==0 && y==0 && z==0; }
     float squaredlen() const { return x*x + y*y + z*z; }
+    float dot2(const vec2 &o) const { return x*o.x + y*o.y; }
     float dot2(const vec &o) const { return x*o.x + y*o.y; }
     float dot(const vec &o) const { return x*o.x + y*o.y + z*o.z; }
+    float absdot(const vec &o) const { return fabs(x*o.x) + fabs(y*o.y) + fabs(z*o.z); }
     vec &mul(const vec &o)   { x *= o.x; y *= o.y; z *= o.z; return *this; }
     vec &mul(float f)        { x *= f; y *= f; z *= f; return *this; }
     vec &div(const vec &o)   { x /= o.x; y /= o.y; z /= o.z; return *this; }
@@ -47,7 +103,8 @@ struct vec
     vec &max(const vec &o)   { x = ::max(x, o.x); y = ::max(y, o.y); z = ::max(z, o.z); return *this; }
     vec &min(float f)        { x = ::min(x, f); y = ::min(y, f); z = ::min(z, f); return *this; }
     vec &max(float f)        { x = ::max(x, f); y = ::max(y, f); z = ::max(z, f); return *this; }
-    vec &clamp(float f, float h) { x = ::clamp(x, f, h); y = ::clamp(y, f, h); z = ::clamp(z, f, h); return *this; }
+    vec &abs() { x = fabs(x); y = fabs(y); z = fabs(z); return *this; }
+    vec &clamp(float l, float h) { x = ::clamp(x, l, h); y = ::clamp(y, l, h); z = ::clamp(z, l, h); return *this; }
     float magnitude2() const { return sqrtf(dot2(*this)); }
     float magnitude() const  { return sqrtf(squaredlen()); }
     vec &normalize()         { div(magnitude()); return *this; }
@@ -56,6 +113,7 @@ struct vec
     float dist(const vec &e) const { vec t; return dist(e, t); }
     float dist(const vec &e, vec &t) const { t = *this; t.sub(e); return t.magnitude(); }
     float dist2(const vec &o) const { float dx = x-o.x, dy = y-o.y; return sqrtf(dx*dx + dy*dy); }
+    float distrange(const vec &o, float r = 1e16f, float m = 0.f) const { float dt = dist(o); return r > m && dt > m ? (dt < r ? (dt-m)/(r-m) : 1.f) : 0.f; }
     bool reject(const vec &o, float r) { return x>o.x+r || x<o.x-r || y>o.y+r || y<o.y-r; }
     bool rejectxyz(const vec &o, float rx, float ry, float rz1, float rz2) { return x>o.x+rx || x<o.x-rx || y>o.y+ry || y<o.y-ry || z>o.z+rz2 || z<o.z-rz1; }
     template<class A, class B>
@@ -114,10 +172,7 @@ struct vec
 
     void orthogonal(const vec &d)
     {
-        int i = fabs(d.x) > fabs(d.y) ? (fabs(d.x) > fabs(d.z) ? 0 : 2) : (fabs(d.y) > fabs(d.z) ? 1 : 2);
-        v[i] = d[(i+1)%3];
-        v[(i+1)%3] = -d[i];
-        v[(i+2)%3] = 0;
+        *this = fabs(d.x) > fabs(d.z) ? vec(-d.y, d.x, 0) : vec(0, -d.z, d.y);
     }
 
     void orthonormalize(vec &s, vec &t) const
@@ -127,6 +182,18 @@ struct vec
          .sub(vec(s).mul(s.dot(t)));
     }
 
+    template<class T>
+    bool insidebb(const T &bbmin, const T &bbmax) const
+    {
+        return x >= bbmin.x && x <= bbmax.x && y >= bbmin.y && y <= bbmax.y && z >= bbmin.z && z <= bbmax.z;
+    }
+
+    template<class T, class U>
+    bool insidebb(const T &o, U size) const
+    {
+        return x >= o.x && x <= o.x + size && y >= o.y && y <= o.y + size && z >= o.z && z <= o.z + size;
+    }
+
     template<class T> float dist_to_bb(const T &min, const T &max) const
     {
         float sqrdist = 0;
@@ -148,9 +215,11 @@ struct vec
         return vec(((color>>16)&0xFF)*(1.0f/255.0f), ((color>>8)&0xFF)*(1.0f/255.0f), (color&0xFF)*(1.0f/255.0f));
     }
 
-    int tohexcolor() { return ((int(r*255)>>16)&0xFF)|((int(g*255)>>8)&0xFF)|(int(b*255)&0xFF); }
+    int tohexcolor() { return (int(::clamp(r, 0.0f, 1.0f)*255)<<16)|(int(::clamp(g, 0.0f, 1.0f)*255)<<8)|int(::clamp(b, 0.0f, 1.0f)*255); }
 };
 
+inline vec2::vec2(const vec &v) : x(v.x), y(v.y) {}
+
 static inline bool htcmp(const vec &x, const vec &y)
 {
     return x == y;
@@ -229,60 +298,9 @@ struct vec4
     vec4 &rotate_around_y(float angle) { return rotate_around_y(cosf(angle), sinf(angle)); }
 };
 
+inline vec2::vec2(const vec4 &v) : x(v.x), y(v.y) {}
 inline vec::vec(const vec4 &v) : x(v.x), y(v.y), z(v.z) {}
 
-struct vec2
-{
-    union
-    {
-        struct { float x, y; };
-        float v[2];
-    };
-
-    vec2() {}
-    vec2(float x, float y) : x(x), y(y) {}
-    explicit vec2(const vec &v) : x(v.x), y(v.y) {}
-    explicit vec2(const vec4 &v) : x(v.x), y(v.y) {}
-
-    float &operator[](int i)       { return v[i]; }
-    float  operator[](int i) const { return v[i]; }
-
-    bool operator==(const vec2 &o) const { return x == o.x && y == o.y; }
-    bool operator!=(const vec2 &o) const { return x != o.x || y != o.y; }
-
-    bool iszero() const { return x==0 && y==0; }
-    float dot(const vec2 &o) const  { return x*o.x + y*o.y; }
-    float squaredlen() const { return dot(*this); }
-    float magnitude() const  { return sqrtf(squaredlen()); }
-    vec2 &normalize() { mul(1/magnitude()); return *this; }
-    float cross(const vec2 &o) const { return x*o.y - y*o.x; }
-
-    vec2 &mul(float f)       { x *= f; y *= f; return *this; }
-    vec2 &mul(const vec2 &o) { x *= o.x; y *= o.y; return *this; }
-    vec2 &div(float f)       { x /= f; y /= f; return *this; }
-    vec2 &div(const vec2 &o) { x /= o.x; y /= o.y; return *this; }
-    vec2 &add(float f)       { x += f; y += f; return *this; }
-    vec2 &add(const vec2 &o) { x += o.x; y += o.y; return *this; }
-    vec2 &sub(float f)       { x -= f; y -= f; return *this; }
-    vec2 &sub(const vec2 &o) { x -= o.x; y -= o.y; return *this; }
-    vec2 &neg()              { x = -x; y = -y; return *this; }
-};
-
-inline vec::vec(const vec2 &v, float z) : x(v.x), y(v.y), z(z) {}
-
-static inline bool htcmp(const vec2 &x, const vec2 &y)
-{
-    return x == y;
-}
-
-static inline uint hthash(const vec2 &k)
-{
-    union { uint i; float f; } x, y;
-    x.f = k.x; y.f = k.y;
-    uint v = x.i^y.i;
-    return v + (v>>12);
-}
-
 struct matrix3x3;
 struct matrix3x4;
 
@@ -573,6 +591,18 @@ struct matrix3x3
         c = vec(axis.x*axis.z*(1-ck)-axis.y*sk, axis.y*axis.z*(1-ck)+axis.x*sk, axis.z*axis.z*(1-ck)+ck);
     }
 
+    void setyaw(float ck, float sk)
+    {
+        a = vec(ck, -sk, 0);
+        b = vec(sk, ck, 0);
+        c = vec(0, 0, 1);
+    }
+
+    void setyaw(float angle)
+    {
+        setyaw(cosf(angle), sinf(angle));
+    }
+
     bool calcangleaxis(float &angle, vec &axis, float threshold = 1e-9f)
     {
         angle = acosf(clamp(0.5f*(a.x + b.y + c.z - 1), -1.0f, 1.0f));
@@ -619,6 +649,14 @@ struct matrix3x3
                    a.y*o.x + b.y*o.y + c.y*o.z,
                    a.z*o.x + b.z*o.y + c.z*o.z);
     }
+    vec abstransform(const vec &o) const
+    {
+        return vec(a.absdot(o), b.absdot(o), c.absdot(o));
+    }
+    vec abstransposedtransform(const vec &o) const
+    {
+        return vec(a).mul(o.x).abs().add(vec(b).mul(o.y).abs()).add(vec(c).mul(o.z).abs());
+    }
 
     void identity()
     {
@@ -627,29 +665,38 @@ struct matrix3x3
         c = vec(0, 0, 1);
     }
 
-    void rotate_around_x(float angle)
+    void rotate_around_x(float ck, float sk)
     {
-        float ck = cosf(angle), sk = -sinf(angle);
+        sk = -sk;
         a.rotate_around_x(ck, sk);
         b.rotate_around_x(ck, sk);
         c.rotate_around_x(ck, sk);
     }
+    void rotate_around_x(float angle) { rotate_around_x(cosf(angle), sinf(angle)); }
+    void rotate_around_x(const vec2 &sc) { rotate_around_x(sc.x, sc.y); }
 
-    void rotate_around_y(float angle)
+    void rotate_around_y(float ck, float sk)
     {
-        float ck = cosf(angle), sk = -sinf(angle);
+        sk = -sk;
         a.rotate_around_y(ck, sk);
         b.rotate_around_y(ck, sk);
         c.rotate_around_y(ck, sk);
     }
+    void rotate_around_y(float angle) { rotate_around_y(cosf(angle), sinf(angle)); }
+    void rotate_around_y(const vec2 &sc) { rotate_around_y(sc.x, sc.y); }
 
-    void rotate_around_z(float angle)
+    void rotate_around_z(float ck, float sk)
     {
-        float ck = cosf(angle), sk = -sinf(angle);
+        sk = -sk;
         a.rotate_around_z(ck, sk);
         b.rotate_around_z(ck, sk);
         c.rotate_around_z(ck, sk);
     }
+    void rotate_around_z(float angle) { rotate_around_z(cosf(angle), sinf(angle)); }
+    void rotate_around_z(const vec2 &sc) { rotate_around_z(sc.x, sc.y); }
+
+    vec transform(const vec2 &o) const { return vec(a.dot2(o), b.dot2(o), c.dot2(o)); }
+    vec transposedtransform(const vec2 &o) { return vec(a).mul(o.x).add(vec(b).mul(o.y)); }
 };
 
 struct matrix3x4
@@ -1051,6 +1098,8 @@ static inline uint hthash(const ivec &k)
     return k.x^k.y^k.z;
 }
 
+struct bvec4;
+
 struct bvec
 {
     union
@@ -1062,8 +1111,9 @@ struct bvec
 
     bvec() {}
     bvec(uchar x, uchar y, uchar z) : x(x), y(y), z(z) {}
-    bvec(const vec &v) : x((uchar)((v.x+1)*255/2)), y((uchar)((v.y+1)*255/2)), z((uchar)((v.z+1)*255/2)) {}
+    bvec(const vec &v) : x(uchar((v.x+1)*(255.0f/2.0f))), y(uchar((v.y+1)*(255.0f/2.0f))), z(uchar((v.z+1)*(255.0f/2.0f))) {}
     explicit bvec(int color) : x((color>>16)&0xFF), y((color>>8)&0xFF), z(color&0xFF) {}
+    explicit bvec(const bvec4 &v);
 
     uchar &operator[](int i)       { return v[i]; }
     uchar  operator[](int i) const { return v[i]; }
@@ -1081,6 +1131,12 @@ struct bvec
     bvec &sub(int n) { x -= n; y -= n; z -= n; return *this; }
     bvec &add(const bvec &v) { x += v.x; y += v.y; z += v.z; return *this; }
     bvec &sub(const bvec &v) { x -= v.x; y -= v.y; z -= v.z; return *this; }
+    bvec &min(const bvec &o)   { x = ::min(x, o.x); y = ::min(y, o.y); z = ::min(z, o.z); return *this; }
+    bvec &max(const bvec &o)   { x = ::max(x, o.x); y = ::max(y, o.y); z = ::max(z, o.z); return *this; }
+    bvec &min(int f)        { x = ::min(int(x), f); y = ::min(int(y), f); z = ::min(int(z), f); return *this; }
+    bvec &max(int f)        { x = ::max(int(x), f); y = ::max(int(y), f); z = ::max(int(z), f); return *this; }
+    bvec &abs() { x = fabs(x); y = fabs(y); z = fabs(z); return *this; }
+    bvec &clamp(int l, int h) { x = ::clamp(int(x), l, h); y = ::clamp(int(y), l, h); z = ::clamp(int(z), l, h); return *this; }
 
     vec tovec() const { return vec(x*(2.0f/255.0f)-1.0f, y*(2.0f/255.0f)-1.0f, z*(2.0f/255.0f)-1.0f); }
 
@@ -1096,12 +1152,70 @@ struct bvec
 
     void lerp(const bvec &a, const bvec &b, float t) { x = uchar(a.x + (b.x-a.x)*t); y = uchar(a.y + (b.y-a.y)*t); z = uchar(a.z + (b.z-a.z)*t); }
 
-    void flip() { x -= 128; y -= 128; z -= 128; }
+    void lerp(const bvec &a, const bvec &b, int ka, int kb, int d)
+    {
+        x = uchar((a.x*ka + b.x*kb)/d);
+        y = uchar((a.y*ka + b.y*kb)/d);
+        z = uchar((a.z*ka + b.z*kb)/d);
+    }
+
+    void flip() { x ^= 0x80; y ^= 0x80; z ^= 0x80; }
 
-    static bvec fromcolor(const vec &v) { return bvec(uchar(v.x*255.0f), uchar(v.y*255.0f), uchar(v.z*255.0f)); }
-    vec tocolor() const { return vec(x*(1.0f/255.0f), y*(1.0f/255.0f), z*(1.0f/255.0f)); }
+    void scale(int k, int d) { x = uchar((x*k)/d); y = uchar((y*k)/d); z = uchar((z*k)/d); }
+
+    static bvec fromcolor(const vec &v) { return bvec(uchar(v.r*255.0f), uchar(v.g*255.0f), uchar(v.b*255.0f)); }
+    vec tocolor() const { return vec(r*(1.0f/255.0f), g*(1.0f/255.0f), b*(1.0f/255.0f)); }
+
+    static bvec from565(ushort c) { return bvec((((c>>11)&0x1F)*527 + 15) >> 6, (((c>>5)&0x3F)*259 + 35) >> 6, ((c&0x1F)*527 + 15) >> 6); }
+
+    int tohexcolor() { return ((::clamp(int(r), 0, 255))<<16)|((::clamp(int(g), 0, 255))<<8)|(::clamp(int(b), 0, 255)); }
 };
 
+struct bvec4
+{
+    union
+    {
+        struct { uchar x, y, z, w; };
+        struct { uchar r, g, b, a; };
+        uchar v[4];
+        uint mask;
+    };
+
+    bvec4() {}
+    bvec4(uchar x, uchar y, uchar z, uchar w = 0) : x(x), y(y), z(z), w(w) {}
+    bvec4(const bvec &v, uchar w = 0) : x(v.x), y(v.y), z(v.z), w(w) {}
+
+    uchar &operator[](int i)       { return v[i]; }
+    uchar  operator[](int i) const { return v[i]; }
+
+    bool operator==(const bvec4 &v) const { return mask==v.mask; }
+    bool operator!=(const bvec4 &v) const { return mask!=v.mask; }
+
+    bool iszero() const { return mask==0; }
+
+    vec tovec() const { return vec(x*(2.0f/255.0f)-1.0f, y*(2.0f/255.0f)-1.0f, z*(2.0f/255.0f)-1.0f); }
+
+    void lerp(const bvec4 &a, const bvec4 &b, float t)
+    {
+        x = uchar(a.x + (b.x-a.x)*t);
+        y = uchar(a.y + (b.y-a.y)*t);
+        z = uchar(a.z + (b.z-a.z)*t);
+        w = a.w;
+    }
+
+    void lerp(const bvec4 &a, const bvec4 &b, int ka, int kb, int d)
+    {
+        x = uchar((a.x*ka + b.x*kb)/d);
+        y = uchar((a.y*ka + b.y*kb)/d);
+        z = uchar((a.z*ka + b.z*kb)/d);
+        w = a.w;
+    }
+
+    void flip() { mask ^= 0x80808080; }
+};
+
+inline bvec::bvec(const bvec4 &v) : x(v.x), y(v.y), z(v.z) {}
+
 struct glmatrixf
 {
     float v[16];
@@ -1411,12 +1525,21 @@ struct glmatrixf
 extern void vecfromyawpitch(float yaw, float pitch, int move, int strafe, vec &m);
 extern void vectoyawpitch(const vec &v, float &yaw, float &pitch);
 extern bool raysphereintersect(const vec &center, float radius, const vec &o, const vec &ray, float &dist);
-extern bool rayrectintersect(const vec &b, const vec &s, const vec &o, const vec &ray, float &dist, int &orient);
+extern bool rayboxintersect(const vec &b, const vec &s, const vec &o, const vec &ray, float &dist, int &orient);
 extern bool linecylinderintersect(const vec &from, const vec &to, const vec &start, const vec &end, float radius, float &dist);
 extern vec closestpointcylinder(const vec &center, const vec &start, const vec &end, float radius);
 
 extern const vec2 sincos360[];
 
+static inline int mod360(int angle)
+{
+    if(angle < 0) angle = 360 + (angle <= -360 ? angle%360 : angle);
+    else if(angle >= 360) angle %= 360;
+    return angle;
+}
+
+static inline const vec2 &sincosmod360(int angle) { return sincos360[mod360(angle)]; }
+
 struct cvec
 {
     union
diff --git a/src/shared/iengine.h b/src/shared/iengine.h
index affaa8f..28d6cd5 100644
--- a/src/shared/iengine.h
+++ b/src/shared/iengine.h
@@ -2,8 +2,8 @@
 
 extern int verbose, curtime, lastmillis, totalmillis, timescale, paused;
 extern uint totalsecs;
-extern time_t clocktime;
-extern int servertype, serverport, servermasterport;
+extern time_t clocktime, currenttime, clockoffset;
+extern int servertype, serverport, serverlanport, servermasterport;
 extern char *servermaster, *serverip;
 extern ENetAddress masteraddress;
 extern void fatal(const char *s, ...) PRINTFARGS(1, 2);
@@ -109,7 +109,12 @@ extern char *getcurcommand();
 extern void resetcomplete();
 extern void complete(char *s, const char *cmdprefix);
 extern const char *searchbind(const char *action, int type);
-extern void searchbindlist(const char *action, int type, int limit, const char *sep, const char *pretty, vector<char> &names);
+extern void searchbindlist(const char *action, int type, int limit, const char *s1, const char *s2, const char *sep1, const char *sep2, vector<char> &names, bool force = true);
+extern int textkeybg, textkeyseps;
+
+extern bool capslockon, numlockon;
+extern bool capslocked();
+extern bool numlocked();
 
 struct bindlist
 {
@@ -118,12 +123,12 @@ struct bindlist
 
     bindlist() : lastsearch(-1) {}
 
-    const char *search(const char *action, int type = 0, int limit = 5, const char *sep = "or", const char *pretty = "\fw")
+    const char *search(const char *action, int type = 0, const char *s1 = "\f{", const char *s2 = "}", int limit = 5)
     {
         if(names.empty() || lastsearch != changedkeys)
         {
             names.shrink(0);
-            searchbindlist(action, type, limit, sep, pretty, names);
+            searchbindlist(action, type, limit, s1, s2, textkeyseps ? (textkeybg ? "|" : ", ") : (textkeybg ? "" : " "), textkeyseps ? (textkeybg ? "|" : " or ") : (textkeybg ? "" : " "), names);
             lastsearch = changedkeys;
         }
         return names.getbuf();
@@ -134,7 +139,7 @@ struct bindlist
 
 // menus
 extern void newgui(char *name, char *contents, char *initscript = NULL);
-extern void showgui(const char *name, int tab = 0);
+extern void showgui(const char *name, int tab = 0, bool *keep = NULL);
 
 // main
 struct igame;
@@ -148,6 +153,7 @@ enum
     TEXT_SHADOW         = 1<<0,
     TEXT_NO_INDENT      = 1<<1,
     TEXT_UPWARD         = 1<<2,
+    TEXT_BALLOON        = 1<<3,
 
     TEXT_ALIGN          = 3<<8,
     TEXT_LEFT_JUSTIFY   = 0<<8,
@@ -156,7 +162,11 @@ enum
 
     TEXT_LEFT_UP        = TEXT_UPWARD|TEXT_LEFT_JUSTIFY,
     TEXT_CENTER_UP      = TEXT_UPWARD|TEXT_CENTERED,
-    TEXT_RIGHT_UP       = TEXT_UPWARD|TEXT_RIGHT_JUSTIFY
+    TEXT_RIGHT_UP       = TEXT_UPWARD|TEXT_RIGHT_JUSTIFY,
+
+    TEXT_LEFT_BAL       = TEXT_BALLOON|TEXT_LEFT_JUSTIFY,
+    TEXT_CENTER_BAL     = TEXT_BALLOON|TEXT_CENTERED,
+    TEXT_RIGHT_BAL      = TEXT_BALLOON|TEXT_RIGHT_JUSTIFY
 };
 
 extern bool setfont(const char *name);
@@ -259,13 +269,14 @@ enum
     PART_FIREBALL_SOFT, PART_FIREBALL,
     PART_PLASMA_SOFT, PART_PLASMA,
     PART_ELECTRIC_SOFT, PART_ELECTRIC,
+    PART_ELECZAP_SOFT, PART_ELECZAP,
     PART_FLAME,
-    PART_FLARE, PART_MUZZLE_FLARE, PART_LIGHTNING_FLARE,
+    PART_FLARE, PART_MUZZLE_FLARE, PART_LIGHTNING_FLARE, PART_LIGHTZAP_FLARE,
     PART_MUZZLE_FLASH,
     PART_SNOW,
     PART_TEXT, PART_TEXT_ONTOP,
     PART_EXPLOSION, PART_SHOCKWAVE, PART_SHOCKBALL,
-    PART_LIGHTNING,
+    PART_LIGHTNING, PART_LIGHTZAP,
     PART_LENS_FLARE,
     PART_MAX
 };
@@ -346,7 +357,6 @@ extern void clearmapcrc();
 
 // physics
 extern bool ellipsecollide(physent *d, const vec &dir, const vec &o, const vec &center, float yaw, float xr, float yr, float hi, float lo);
-extern bool rectcollide(physent *d, const vec &dir, const vec &o, float xr, float yr,  float hi, float lo, uchar visible = 0xFF);
 extern bool collide(physent *d, const vec &dir = vec(0, 0, 0), float cutoff = 0, bool playercol = true);
 extern bool plcollide(physent *d, const vec &dir = vec(0, 0, 0));
 extern bool plcollide(physent *d, const vec &dir, physent *o);
@@ -388,15 +398,17 @@ extern bool validragdoll(dynent *d, int millis);
 extern void moveragdoll(dynent *d, bool smooth);
 extern void cleanragdoll(dynent *d);
 extern void warpragdoll(dynent *d, const vec &vel, const vec &offset = vec(0, 0, 0));
+extern void twitchragdoll(dynent *d, float vel);
 
 // server
 #define MAXCLIENTS 256                  // in a multiplayer game, can be arbitrarily changed
 #define MAXTRANS 5000                  // max amount of data to swallow in 1 go
 
-enum { DISC_NONE = 0, DISC_EOP, DISC_CN, DISC_KICK, DISC_MSGERR, DISC_IPBAN, DISC_PRIVATE, DISC_MAXCLIENTS, DISC_TIMEOUT, DISC_OVERFLOW, DISC_SHUTDOWN, DISC_NUM };
+enum { DISC_NONE = 0, DISC_EOP, DISC_CN, DISC_KICK, DISC_MSGERR, DISC_IPBAN, DISC_PRIVATE, DISC_PASSWORD, DISC_PURE, DISC_MAXCLIENTS, DISC_INCOMPATIBLE, DISC_TIMEOUT, DISC_OVERFLOW, DISC_SHUTDOWN, DISC_NUM };
 
 extern void *getinfo(int i);
 extern const char *gethostname(int i);
+extern const char *gethostip(int i);
 extern void sendf(int cn, int chan, const char *format, ...);
 extern void sendfile(int cn, int chan, stream *file, const char *format = "", ...);
 extern void sendpacket(int cn, int chan, ENetPacket *packet, int exclude = -1);
@@ -404,14 +416,17 @@ extern void flushserver(bool force);
 extern int getservermtu();
 extern int getnumclients();
 extern uint getclientip(int n);
-extern bool filtertext(char *dst, const char *src, bool newline = true, bool colour = true, bool whitespace = true, int len = sizeof(string)-1);
+extern bool filterword(char *src, const char *list);
+extern bool filterstring(char *dst, const char *src, bool newline = true, bool colour = true, bool whitespace = true, bool wsstrip = false, size_t len = MAXSTRLEN-1);
+extern bool filterbigstring(char *dst, const char *src, bool newline = true, bool colour = true, bool whitespace = true, bool wsstrip = false, size_t len = BIGSTRLEN-1);
 extern void disconnect_client(int n, int reason);
 extern void kicknonlocalclients(int reason);
 extern bool hasnonlocalclients();
 extern bool haslocalclients();
+extern void limitdupclients();
 extern void sendqueryreply(ucharbuf &p);
 extern bool resolverwait(const char *name, ENetAddress *address);
-extern int connectwithtimeout(ENetSocket sock, const char *hostname, ENetAddress &address);
+extern int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &address);
 extern bool connectedmaster();
 extern ENetSocket connectmaster(bool reuse = true);
 extern void disconnectmaster();
@@ -439,7 +454,7 @@ struct serverinfo
     int numplayers, lastping, lastinfo, nextping, ping, resolved, port, priority;
     int pings[MAXPINGS];
     vector<int> attr;
-    vector<char *> players;
+    vector<char *> players, handles;
     ENetAddress address;
 
     serverinfo(uint ip, int port, int priority = 0)
@@ -465,6 +480,7 @@ struct serverinfo
         clearpings();
         attr.setsize(0);
         players.deletearrays();
+        handles.deletearrays();
         numplayers = 0;
     }
 
@@ -526,35 +542,39 @@ struct guient
 {
     virtual ~guient() {}
 
-    virtual void start(int starttime, float basescale, int *tab = NULL, bool allowinput = true, bool wantstitle = true) = 0;
+    virtual void start(int starttime, float basescale, int *tab = NULL, bool allowinput = true, bool wantstitle = true, bool wantsbgfx = true) = 0;
     virtual void end() = 0;
 
-    virtual int text(const char *text, int color, const char *icon = NULL, int icolor = 0xFFFFFF) = 0;
-    int textf(const char *fmt, int color, const char *icon = NULL, int icolor = 0xFFFFFF, ...) PRINTFARGS(2, 6)
+    virtual int text(const char *text, int color, const char *icon = NULL, int icolor = 0xFFFFFF, int wrap = -1) = 0;
+    int textf(const char *fmt, int color, const char *icon = NULL, int icolor = 0xFFFFFF, int wrap = -1, ...) PRINTFARGS(2, 7)
     {
-        defvformatstring(str, icolor, fmt);
-        return text(str, color, icon, icolor);
+        defvformatstring(str, wrap, fmt);
+        return text(str, color, icon, icolor, wrap);
     }
-    virtual int button(const char *text, int color, const char *icon = NULL, int icolor = 0xFFFFFF, bool faded = true) = 0;
-    int buttonf(const char *fmt, int color, const char *icon = NULL, int icolor = 0xFFFFFF, bool faded = true, ...) PRINTFARGS(2, 7)
+    virtual int button(const char *text, int color, const char *icon = NULL, int icolor = 0xFFFFFF, int wrap = -1, bool faded = true) = 0;
+    int buttonf(const char *fmt, int color, const char *icon = NULL, int icolor = 0xFFFFFF, int wrap = -1, bool faded = true, ...) PRINTFARGS(2, 8)
     {
         defvformatstring(str, faded, fmt);
-        return button(str, color, icon, icolor, faded);
+        return button(str, color, icon, icolor, wrap, faded);
     }
-    virtual void background(int color, int parentw = 0, int parenth = 0) = 0;
+    virtual void fill(int color, int parentw = 0, int parenth = 0) = 0;
+    virtual void outline(int color, int parentw = 0, int parenth = 0, int offsetx = 0, int offsety = 0) = 0;
+    virtual void background(int colour1 = -1, float blend1 = -1, int colour2 = -1, float blend2 = -1, bool skinborder = false, int parentw = 0, int parenth = 0) = 0;
 
     virtual void pushlist(bool merge = false) {}
     virtual int poplist() { return 0; }
 
     virtual void allowhitfx(bool on) = 0;
+    virtual bool visibletab() = 0;
     virtual bool visible() = 0;
     virtual bool shouldtab() { return false; }
     virtual void tab(const char *name = NULL, int color = 0xFFFFFF, bool front = false) = 0;
-    virtual int title(const char *text, int color = 0xFFFFFF, const char *icon = NULL, int icolor = 0xFFFFFF) = 0;
+    virtual void setstatus(const char *fmt, int width, ...) = 0;
+    virtual void settooltip(const char *fmt, int width, ...) = 0;
     virtual int image(Texture *t, float scale, bool overlaid = false, int icolor = 0xFFFFFF) = 0;
     virtual int texture(VSlot &vslot, float scale, bool overlaid = true) = 0;
     virtual int slice(Texture *t, float scale, float start = 0, float end = 1, const char *text = NULL) = 0;
-    virtual void slider(int &val, int vmin, int vmax, int color, const char *label = NULL, bool reverse = false, bool scroll = false) = 0;
+    virtual int slider(int &val, int vmin, int vmax, int colour, const char *label = NULL, bool reverse = false, bool scroll = false, int style = 0, int scolour = -1) = 0;
     virtual void separator() = 0;
     virtual void progress(float percent, float scale) = 0;
     virtual void pushfont(const char *font) = 0;
@@ -562,8 +582,8 @@ struct guient
     virtual void strut(float size = 1) = 0;
     virtual void space(float size = 1) = 0;
     virtual void spring(int weight = 1) = 0;
-    virtual char *field(const char *name, int color, int length, int height = 0, const char *initval = NULL, int initmode = EDITORFOCUSED, bool focus = false, const char *parent = NULL) = 0;
-    virtual char *keyfield(const char *name, int color, int length, int height = 0, const char *initval = NULL, int initmode = EDITORFOCUSED, bool focus = false, const char *parent = NULL) = 0;
+    virtual char *field(const char *name, int color, int length, int height = 0, const char *initval = NULL, int initmode = EDITORFOCUSED, bool focus = false, const char *parent = NULL, const char *prompt = NULL, bool immediate = false) = 0;
+    virtual char *keyfield(const char *name, int color, int length, int height = 0, const char *initval = NULL, int initmode = EDITORFOCUSED, bool focus = false, const char *parent = NULL, const char *prompt = NULL, bool immediate = false) = 0;
     virtual int playerpreview(int model, int color, int team, int weap, const char *vanity, float sizescale, bool overlaid = false, float scale = 1, float blend = 1) { return 0; }
     virtual int modelpreview(const char *name, int anim, float sizescale, bool overlaid = false, float scale = 1, float blend = 1) { return 0; }
 
@@ -576,15 +596,14 @@ struct guicb
     virtual void gui(guient &g, bool firstpass) = 0;
 };
 
-extern char *guioverlaytex, *guislidertex;
+extern char *guiskintex, *guiskinbordertex, *guioverlaytex, *guiexittex, *guihovertex;
 
 struct editor;
 
 namespace UI
 {
-    extern bool isopen, ready;
+    extern bool isopen;
     extern bool keypress(int code, bool isdown, int cooked);
-    extern void setup();
     extern void update();
     extern void render();
     extern bool active(bool pass = true);
@@ -605,7 +624,7 @@ struct clientdata
     int type;
     int num;
     ENetPeer *peer;
-    string hostname;
+    string hostname, hostip;
     void *info;
 };
 
@@ -617,11 +636,10 @@ extern ENetHost *serverhost;
 
 // world
 
-extern bool inside;
+extern bool collideinside;
 extern physent *hitplayer;
 extern int hitflags;
-extern vec wall, hitsurface;
-extern float walldistance;
+extern vec collidewall, hitsurface;
 
 enum { HITFLAG_NONE = 0, HITFLAG_LEGS = 1<<0, HITFLAG_TORSO = 1<<1, HITFLAG_HEAD = 1<<2, HITFLAG_FULL = 1<<4 };
 
diff --git a/src/shared/igame.h b/src/shared/igame.h
index e513017..0f31c1a 100644
--- a/src/shared/igame.h
+++ b/src/shared/igame.h
@@ -8,7 +8,7 @@ namespace entities
     extern void readent(stream *g, int mtype, int mver, char *gid, int gver, int id);
     extern void writeent(stream *g, int id);
     extern void remapents(vector<int> &idxs);
-    extern void initents(stream *g, int mtype, int mver, char *gid, int gver);
+    extern void initents(int mtype, int mver, char *gid, int gver);
     extern float dropheight(extentity &e);
     extern void fixentity(int n, bool recurse = true, bool create = false);
     extern bool cansee(int n);
@@ -27,13 +27,14 @@ namespace entities
 
 namespace client
 {
+    extern int getcn(physent *d);
     extern int maxmsglen();
     extern void gamedisconnect(int clean);
     extern void parsepacketclient(int chan, packetbuf &p);
     extern void gameconnect(bool _remote);
     extern bool allowedittoggle(bool edit);
     extern void edittoggled(bool edit);
-    extern void toserver(int flags, char *text);
+    extern void toserver(int flags, const char *text, const char *target = NULL);
     extern void editvar(ident *id, bool local);
     extern void edittrigger(const selinfo &sel, int op, int arg1 = 0, int arg2 = 0, int arg3 = 0);
     extern void changemap(const char *name);
@@ -41,13 +42,16 @@ namespace client
     extern void connectattempt(const char *name, int port, const char *password, const ENetAddress &address);
     extern void connectfail();
     extern int state();
-    extern int otherclients(bool nospec = false);
+    extern int otherclients(bool self = false, bool nospec = false);
+    extern int numplayers();
     extern int servercompare(serverinfo *a, serverinfo *b);
+    extern const char *getname();
+    extern bool sendcmd(int nargs, const char *cmd, const char *arg);
 }
 
 namespace hud
 {
-    extern char *progresstex;
+    extern char *progresstex, *progringtex;
     extern bool hasinput(bool pass = false, bool focus = true);
     extern bool keypress(int code, bool isdown, int cooked);
     extern void drawhud(bool noview = false);
@@ -79,8 +83,7 @@ namespace physics
     extern void move(physent *d, int moveres = 10, bool local = true);
     extern bool entinmap(physent *d, bool avoidplayers);
     extern void updatephysstate(physent *d);
-    extern bool droptofloor(vec &o, float radius = 1, float height = 1);
-    extern bool iscrouching(physent *d);
+    extern bool droptofloor(vec &o, int type = ENT_CAMERA, float radius = 1, float height = 1);
     extern bool moveplayer(physent *pl, int moveres, bool local, int millis);
     extern void interppos(physent *d);
     extern void updatematerial(physent *pl, const vec &center, const vec &bottom, bool local = false);
@@ -91,6 +94,7 @@ namespace physics
 
 namespace game
 {
+    extern void start();
     extern bool clientoption(char *arg);
     extern void preload();
     extern void updateworld();
@@ -134,9 +138,10 @@ namespace server
     extern bool serveroption(char *arg);
     extern void *newinfo();
     extern void deleteinfo(void *ci);
-    extern int numclients(int exclude = -1, bool nospec = false, int aitype = -1);
+    extern int numclients(int exclude = -1, bool nospec = false, int actortype = -1);
     extern int reserveclients();
     extern int numchannels();
+    extern int dupclients();
     extern void clientdisconnect(int n, bool local = false, int reason = DISC_NONE);
     extern int clientconnect(int n, uint ip, bool local = false);
     extern bool allowbroadcast(int n);
@@ -148,11 +153,12 @@ namespace server
     extern void serverupdate();
     extern const char *gameid();
     extern int getver(int n = 0);
-    extern const char *pickmap(const char *suggest = NULL);
-    extern const char *choosemap(const char *suggest = NULL, int mode = -1, int muts = -1, int force = 0);
+    extern const char *pickmap(const char *suggest = NULL, int mode = -1, int muts = -1, bool notry = false);
+    extern const char *choosemap(const char *suggest = NULL, int mode = -1, int muts = -1, int force = 0, bool notry = false);
     extern void changemap(const char *name = NULL, int mode = -1, int muts = -1);
     extern bool canload(const char *type);
     extern bool rewritecommand(ident *id, tagval *args, int numargs);
-    extern void disconnectedmaster();
     extern void processmasterinput(const char *cmd, int cmdlen, const char *args);
+    extern void masterconnected();
+    extern void masterdisconnected();
 }
diff --git a/src/shared/stream.cpp b/src/shared/stream.cpp
index 34d8094..11568e7 100644
--- a/src/shared/stream.cpp
+++ b/src/shared/stream.cpp
@@ -84,8 +84,46 @@ extern const uchar uni2cubechars[878] =
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
 };
+extern const uchar cubelowerchars[256] =
+{
+    0, 130, 131, 132, 133, 134, 135, 136, 137, 9, 10, 11, 12, 13, 138, 139,
+    140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155,
+    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, 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, 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, 156,
+    157, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143,
+    144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 160,
+    160, 162, 162, 164, 164, 166, 166, 168, 168, 170, 170, 172, 172, 105, 174, 176,
+    176, 178, 178, 180, 180, 182, 182, 184, 184, 186, 186, 188, 188, 190, 190, 192,
+    192, 194, 194, 196, 196, 198, 198, 158, 201, 201, 203, 203, 205, 205, 206, 207,
+    208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223,
+    224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
+    240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255
+};
+extern const uchar cubeupperchars[256] =
+{
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+    16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+    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, 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, 123, 124, 125, 126, 127,
+    128, 129, 1, 2, 3, 4, 5, 6, 7, 8, 14, 15, 16, 17, 18, 19,
+    20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 127, 128, 199, 159,
+    159, 161, 161, 163, 163, 165, 165, 167, 167, 169, 169, 171, 171, 173, 73, 175,
+    175, 177, 177, 179, 179, 181, 181, 183, 183, 185, 185, 187, 187, 189, 189, 191,
+    191, 193, 193, 195, 195, 197, 197, 199, 200, 200, 202, 202, 204, 204, 206, 207,
+    208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223,
+    224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
+    240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255
+};
 
-int decodeutf8(uchar *dstbuf, int dstlen, const uchar *srcbuf, int srclen, int *carry)
+size_t decodeutf8(uchar *dstbuf, size_t dstlen, const uchar *srcbuf, size_t srclen, size_t *carry)
 {
     uchar *dst = dstbuf, *dstend = &dstbuf[dstlen];
     const uchar *src = srcbuf, *srcend = &srcbuf[srclen];
@@ -139,7 +177,7 @@ decode:
     return dst - dstbuf;
 }
 
-int encodeutf8(uchar *dstbuf, int dstlen, const uchar *srcbuf, int srclen, int *carry)
+size_t encodeutf8(uchar *dstbuf, size_t dstlen, const uchar *srcbuf, size_t srclen, size_t *carry)
 {
     uchar *dst = dstbuf, *dstend = &dstbuf[dstlen];
     const uchar *src = srcbuf, *srcend = &srcbuf[srclen];
@@ -303,6 +341,11 @@ char *path(char *s, bool simple)
             {
                 if(prevdir+2==curdir && prevdir[0]=='.' && prevdir[1]=='.') continue;
                 memmove(prevdir, curdir+4, strlen(curdir+4)+1);
+                if(prevdir-2 >= curpart && prevdir[-1]==PATHDIV)
+                {
+                    prevdir -= 2;
+                    while(prevdir-1 >= curpart && prevdir[-1] != PATHDIV) --prevdir;
+                }
                 curdir = prevdir;
             }
         }
@@ -338,6 +381,7 @@ bool fileexists(const char *path, const char *mode)
 {
     bool exists = true;
     if(mode[0]=='w' || mode[0]=='a') path = parentdir(path);
+    else if(findzipfile(path)) return true;
 #ifdef WIN32
     if(GetFileAttributes(path[0] ? path : ".\\") == INVALID_FILE_ATTRIBUTES) exists = false;
 #else
@@ -423,14 +467,15 @@ const char *findfile(const char *filename, const char *mode)
         formatstring(s)("%s%s", packagedirs[i].name, filename); path(s);
         if(fileexists(s, mode)) return s;
     }
+    if(mode[0]=='e') return NULL;
     copystring(s, filename); path(s);
     return s;
 }
 
 bool listdir(const char *dirname, bool rel, const char *ext, vector<char *> &files)
 {
-    int extsize = ext ? (int)strlen(ext)+1 : 0;
-    #ifdef WIN32
+    size_t extsize = ext ? strlen(ext)+1 : 0;
+#ifdef WIN32
     defformatstring(pathname)(rel ? ".\\%s\\*.%s" : "%s\\*.%s", dirname, ext ? ext : "*");
     WIN32_FIND_DATA FindFileData;
     HANDLE Find = FindFirstFile(pathname, &FindFileData);
@@ -440,15 +485,19 @@ bool listdir(const char *dirname, bool rel, const char *ext, vector<char *> &fil
             if(!ext) files.add(newstring(FindFileData.cFileName));
             else
             {
-                int namelength = (int)strlen(FindFileData.cFileName) - extsize;
-                if(namelength > 0 && FindFileData.cFileName[namelength] == '.' && strncmp(FindFileData.cFileName+namelength+1, ext, extsize-1)==0)
-                    files.add(newstring(FindFileData.cFileName, namelength));
+                size_t namelen = strlen(FindFileData.cFileName);
+                if(namelen > extsize)
+                {
+                    namelen -= extsize;
+                    if(FindFileData.cFileName[namelen] == '.' && strncmp(FindFileData.cFileName+namelen+1, ext, extsize-1)==0)
+                        files.add(newstring(FindFileData.cFileName, namelen));
+                }
             }
         } while(FindNextFile(Find, &FindFileData));
         FindClose(Find);
         return true;
     }
-    #else
+#else
     defformatstring(pathname)(rel ? "./%s" : "%s", dirname);
     DIR *d = opendir(pathname);
     if(d)
@@ -459,15 +508,19 @@ bool listdir(const char *dirname, bool rel, const char *ext, vector<char *> &fil
             if(!ext) files.add(newstring(de->d_name));
             else
             {
-                int namelength = (int)strlen(de->d_name) - extsize;
-                if(namelength > 0 && de->d_name[namelength] == '.' && strncmp(de->d_name+namelength+1, ext, extsize-1)==0)
-                    files.add(newstring(de->d_name, namelength));
+                size_t namelen = strlen(de->d_name);
+                if(namelen > extsize)
+                {
+                    namelen -= extsize;
+                    if(de->d_name[namelen] == '.' && strncmp(de->d_name+namelen+1, ext, extsize-1)==0)
+                        files.add(newstring(de->d_name, namelen));
+                }
             }
         }
         closedir(d);
         return true;
     }
-    #endif
+#endif
     else return false;
 }
 
@@ -476,7 +529,7 @@ int listfiles(const char *dir, const char *ext, vector<char *> &files)
     string dirname;
     copystring(dirname, dir);
     path(dirname);
-    int dirlen = (int)strlen(dirname);
+    size_t dirlen = strlen(dirname);
     while(dirlen > 1 && dirname[dirlen-1] == PATHDIV) dirname[--dirlen] = '\0';
     int dirs = 0;
     if(listdir(dirname, true, ext, files)) dirs++;
@@ -491,9 +544,7 @@ int listfiles(const char *dir, const char *ext, vector<char *> &files)
         formatstring(s)("%s%s", packagedirs[i].name, dirname);
         if(listdir(s, false, ext, files)) dirs++;
     }
-#ifndef STANDALONE
     dirs += listzipfiles(dirname, ext, files);
-#endif
     return dirs;
 }
 
@@ -543,7 +594,7 @@ stream::offset stream::size()
     return pos == endpos || seek(pos, SEEK_SET) ? endpos : -1;
 }
 
-bool stream::getline(char *str, int len)
+bool stream::getline(char *str, size_t len)
 {
     loopi(len-1)
     {
@@ -554,22 +605,24 @@ bool stream::getline(char *str, int len)
     return true;
 }
 
-int stream::printf(const char *fmt, ...)
+size_t stream::printf(const char *fmt, ...)
 {
     char buf[512];
     char *str = buf;
     va_list args;
 #if defined(WIN32) && !defined(__GNUC__)
     va_start(args, fmt);
-    size_t len = _vscprintf(fmt, args);
-    if(len >= sizeof(buf)) str = new char[len+1];
+    int len = _vscprintf(fmt, args);
+    if(len <= 0) { va_end(args); return 0; }
+    if(len >= (int)sizeof(buf)) str = new char[len+1];
     _vsnprintf(str, len+1, fmt, args);
     va_end(args);
 #else
     va_start(args, fmt);
-    size_t len = vsnprintf(buf, sizeof(buf), fmt, args);
+    int len = vsnprintf(buf, sizeof(buf), fmt, args);
     va_end(args);
-    if(len >= sizeof(buf))
+    if(len <= 0) return 0;
+    if(len >= (int)sizeof(buf))
     {
         str = new char[len+1];
         va_start(args, fmt);
@@ -577,7 +630,7 @@ int stream::printf(const char *fmt, ...)
         va_end(args);
     }
 #endif
-    int n = write(str, len);
+    size_t n = write(str, len);
     if(str != buf) delete[] str;
     return n;
 }
@@ -638,20 +691,21 @@ struct filestream : stream
 #endif
     }
 
-    int read(void *buf, int len) { return (int)fread(buf, 1, len, file); }
-    int write(const void *buf, int len) { return (int)fwrite(buf, 1, len, file); }
+    size_t read(void *buf, size_t len) { return fread(buf, 1, len, file); }
+    size_t write(const void *buf, size_t len) { return fwrite(buf, 1, len, file); }
+    bool flush() { return !fflush(file); }
     int getchar() { return fgetc(file); }
     bool putchar(int c) { return fputc(c, file)!=EOF; }
-    bool getline(char *str, int len) { return fgets(str, len, file)!=NULL; }
+    bool getline(char *str, size_t len) { return fgets(str, len, file)!=NULL; }
     bool putstring(const char *str) { return fputs(str, file)!=EOF; }
 
-    int printf(const char *fmt, ...)
+    size_t printf(const char *fmt, ...)
     {
         va_list v;
         va_start(v, fmt);
         int result = vfprintf(file, fmt, v);
         va_end(v);
-        return result;
+        return max(result, 0);
     }
 };
 
@@ -684,7 +738,7 @@ struct gzstream : stream
     uchar *buf;
     bool reading, writing, autoclose;
     uint crc;
-    int headersize;
+    size_t headersize;
 
     gzstream() : file(NULL), buf(NULL), reading(false), writing(false), autoclose(false), crc(0), headersize(0)
     {
@@ -706,15 +760,15 @@ struct gzstream : stream
         file->write(header, sizeof(header));
     }
 
-    void readbuf(int size = BUFSIZE)
+    void readbuf(size_t size = BUFSIZE)
     {
         if(!zfile.avail_in) zfile.next_in = (Bytef *)buf;
-        size = min(size, int(&buf[BUFSIZE] - &zfile.next_in[zfile.avail_in]));
-        int n = file->read(zfile.next_in + zfile.avail_in, size);
+        size = min(size, size_t(&buf[BUFSIZE] - &zfile.next_in[zfile.avail_in]));
+        size_t n = file->read(zfile.next_in + zfile.avail_in, size);
         if(n > 0) zfile.avail_in += n;
     }
 
-    int readbyte(int size = BUFSIZE)
+    uchar readbyte(size_t size = BUFSIZE)
     {
         if(!zfile.avail_in) readbuf(size);
         if(!zfile.avail_in) return 0;
@@ -722,11 +776,11 @@ struct gzstream : stream
         return *(uchar *)zfile.next_in++;
     }
 
-    void skipbytes(int n)
+    void skipbytes(size_t n)
     {
         while(n > 0 && zfile.avail_in > 0)
         {
-            int skipped = min(n, (int)zfile.avail_in);
+            size_t skipped = min(n, size_t(zfile.avail_in));
             zfile.avail_in -= skipped;
             zfile.next_in += skipped;
             n -= skipped;
@@ -739,19 +793,19 @@ struct gzstream : stream
     {
         readbuf(10);
         if(readbyte() != MAGIC1 || readbyte() != MAGIC2 || readbyte() != Z_DEFLATED) return false;
-        int flags = readbyte();
+        uchar flags = readbyte();
         if(flags & F_RESERVED) return false;
         skipbytes(6);
         if(flags & F_EXTRA)
         {
-            int len = readbyte(512);
-            len |= readbyte(512)<<8;
+            size_t len = readbyte(512);
+            len |= size_t(readbyte(512))<<8;
             skipbytes(len);
         }
         if(flags & F_NAME) while(readbyte(512));
         if(flags & F_COMMENT) while(readbyte(512));
         if(flags & F_CRC) skipbytes(2);
-        headersize = int(file->tell() - zfile.avail_in);
+        headersize = size_t(file->tell() - zfile.avail_in);
         return zfile.avail_in > 0 || !file->end();
     }
 
@@ -770,7 +824,6 @@ struct gzstream : stream
         else if(writing && deflateInit2(&zfile, level, Z_DEFLATED, -MAX_WBITS, min(MAX_MEM_LEVEL, 8), Z_DEFAULT_STRATEGY) != Z_OK) writing = false;
         if(!reading && !writing) return false;
 
-        autoclose = needclose;
         file = f;
         crc = crc32(0, NULL, 0);
         buf = new uchar[BUFSIZE];
@@ -780,6 +833,8 @@ struct gzstream : stream
             if(!checkheader()) { stopreading(); return false; }
         }
         else if(writing) writeheader();
+
+        autoclose = needclose;
         return true;
     }
 
@@ -816,7 +871,7 @@ struct gzstream : stream
         {
             int err = zfile.avail_out > 0 ? deflate(&zfile, Z_FINISH) : Z_OK;
             if(err != Z_OK && err != Z_STREAM_END) break;
-            flush();
+            flushbuf();
             if(err == Z_STREAM_END) break;
         }
         uchar trailer[8] =
@@ -845,8 +900,8 @@ struct gzstream : stream
     }
 
     bool end() { return !reading && !writing; }
-    offset tell() { return reading ? zfile.total_out : (writing ? zfile.total_in : -1); }
-    offset rawtell() { return file ? file->tell() : -1; }
+    offset tell() { return reading ? zfile.total_out : (writing ? zfile.total_in : offset(-1)); }
+    offset rawtell() { return file ? file->tell() : offset(-1); }
 
     offset size()
     {
@@ -854,10 +909,10 @@ struct gzstream : stream
         offset pos = tell();
         if(!file->seek(-4, SEEK_END)) return -1;
         uint isize = file->getlil<uint>();
-        return file->seek(pos, SEEK_SET) ? isize : -1;
+        return file->seek(pos, SEEK_SET) ? isize : offset(-1);
     }
 
-    offset rawsize() { return file ? file->size() : -1; }
+    offset rawsize() { return file ? file->size() : offset(-1); }
 
     bool seek(offset pos, int whence)
     {
@@ -892,7 +947,7 @@ struct gzstream : stream
         uchar skip[512];
         while(pos > 0)
         {
-            int skipped = (int)min(pos, (offset)sizeof(skip));
+            size_t skipped = (size_t)min(pos, (offset)sizeof(skip));
             if(read(skip, skipped) != skipped) { stopreading(); return false; }
             pos -= skipped;
         }
@@ -900,7 +955,7 @@ struct gzstream : stream
         return true;
     }
 
-    int read(void *buf, int len)
+    size_t read(void *buf, size_t len)
     {
         if(!reading || !buf || !len) return 0;
         zfile.next_out = (Bytef *)buf;
@@ -920,11 +975,12 @@ struct gzstream : stream
         return len - zfile.avail_out;
     }
 
-    bool flush()
+    bool flushbuf(bool full = false)
     {
+        if(full) deflate(&zfile, Z_SYNC_FLUSH);
         if(zfile.next_out && zfile.avail_out < BUFSIZE)
         {
-            if(file->write(buf, BUFSIZE - zfile.avail_out) != int(BUFSIZE - zfile.avail_out))
+            if(file->write(buf, BUFSIZE - zfile.avail_out) != BUFSIZE - zfile.avail_out || (full && !file->flush()))
                 return false;
         }
         zfile.next_out = buf;
@@ -932,14 +988,16 @@ struct gzstream : stream
         return true;
     }
 
-    int write(const void *buf, int len)
+    bool flush() { return flushbuf(true); }
+
+    size_t write(const void *buf, size_t len)
     {
         if(!writing || !buf || !len) return 0;
         zfile.next_in = (Bytef *)buf;
         zfile.avail_in = len;
         while(zfile.avail_in > 0)
         {
-            if(!zfile.avail_out && !flush()) { stopwriting(); break; }
+            if(!zfile.avail_out && !flushbuf()) { stopwriting(); break; }
             int err = deflate(&zfile, Z_NO_FLUSH);
             if(err != Z_OK) { stopwriting(); break; }
         }
@@ -956,7 +1014,7 @@ struct utf8stream : stream
     };
     stream *file;
     offset pos;
-    int bufread, bufcarry, buflen;
+    size_t bufread, bufcarry, buflen;
     bool reading, writing, autoclose;
     uchar buf[BUFSIZE];
 
@@ -969,13 +1027,13 @@ struct utf8stream : stream
         close();
     }
 
-    bool readbuf(int size = BUFSIZE)
+    bool readbuf(size_t size = BUFSIZE)
     {
         if(bufread >= bufcarry) { if(bufcarry > 0 && bufcarry < buflen) memmove(buf, &buf[bufcarry], buflen - bufcarry); buflen -= bufcarry; bufread = bufcarry = 0; }
-        int n = file->read(&buf[buflen], min(size, BUFSIZE - buflen));
+        size_t n = file->read(&buf[buflen], min(size, BUFSIZE - buflen));
         if(n <= 0) return false;
         buflen += n;
-        int carry = bufcarry;
+        size_t carry = bufcarry;
         bufcarry += decodeutf8(&buf[bufcarry], BUFSIZE-bufcarry, &buf[bufcarry], buflen-bufcarry, &carry);
         if(carry > bufcarry && carry < buflen) { memmove(&buf[bufcarry], &buf[carry], buflen - carry); buflen -= carry - bufcarry; }
         return true;
@@ -983,7 +1041,7 @@ struct utf8stream : stream
 
     bool checkheader()
     {
-        int n = file->read(buf, 3);
+        size_t n = file->read(buf, 3);
         if(n == 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF) return true;
         buflen = n;
         return false;
@@ -999,11 +1057,11 @@ struct utf8stream : stream
         }
         if(!reading && !writing) return false;
 
-        autoclose = needclose;
         file = f;
 
         if(reading) checkheader();
 
+        autoclose = needclose;
         return true;
     }
 
@@ -1032,7 +1090,7 @@ struct utf8stream : stream
     }
 
     bool end() { return !reading && !writing; }
-    offset tell() { return reading || writing ? pos : -1; }
+    offset tell() { return reading || writing ? pos : offset(-1); }
 
     bool seek(offset off, int whence)
     {
@@ -1058,7 +1116,7 @@ struct utf8stream : stream
         uchar skip[512];
         while(off > 0)
         {
-            int skipped = (int)min(off, (offset)sizeof(skip));
+            size_t skipped = (size_t)min(off, (offset)sizeof(skip));
             if(read(skip, skipped) != skipped) { stopreading(); return false; }
             off -= skipped;
         }
@@ -1066,14 +1124,14 @@ struct utf8stream : stream
         return true;
     }
 
-    int read(void *dst, int len)
+    size_t read(void *dst, size_t len)
     {
         if(!reading || !dst || !len) return 0;
-        int next = 0;
+        size_t next = 0;
         while(next < len)
         {
             if(bufread >= bufcarry) { if(readbuf(BUFSIZE)) continue; stopreading(); break; }
-            int n = min(len - next, bufcarry - bufread);
+            size_t n = min(len - next, bufcarry - bufread);
             memcpy(&((uchar *)dst)[next], &buf[bufread], n);
             next += n;
             bufread += n;
@@ -1082,15 +1140,15 @@ struct utf8stream : stream
         return next;
     }
 
-    bool getline(char *dst, int len)
+    bool getline(char *dst, size_t len)
     {
         if(!reading || !dst || !len) return false;
         --len;
-        int next = 0;
+        size_t next = 0;
         while(next < len)
         {
             if(bufread >= bufcarry) { if(readbuf(BUFSIZE)) continue; stopreading(); if(!next) return false; break; }
-            int n = min(len - next, bufcarry - bufread);
+            size_t n = min(len - next, bufcarry - bufread);
             uchar *endline = (uchar *)memchr(&buf[bufread], '\n', n);
             if(endline) { n = endline+1 - &buf[bufread]; len = next + n; }
             memcpy(&((uchar *)dst)[next], &buf[bufread], n);
@@ -1102,20 +1160,22 @@ struct utf8stream : stream
         return true;
     }
 
-    int write(const void *src, int len)
+    size_t write(const void *src, size_t len)
     {
         if(!writing || !src || !len) return 0;
         uchar dst[512];
-        int next = 0;
+        size_t next = 0;
         while(next < len)
         {
-            int carry = 0, n = encodeutf8(dst, sizeof(dst), &((uchar *)src)[next], len - next, &carry);
+            size_t carry = 0, n = encodeutf8(dst, sizeof(dst), &((uchar *)src)[next], len - next, &carry);
             if(n > 0 && file->write(dst, n) != n) { stopwriting(); break; }
             next += carry;
         }
         pos += next;
         return next;
     }
+
+    bool flush() { return file->flush(); }
 };
 
 stream *openrawfile(const char *filename, const char *mode)
@@ -1129,10 +1189,8 @@ stream *openrawfile(const char *filename, const char *mode)
 
 stream *openfile(const char *filename, const char *mode)
 {
-#ifndef STANDALONE
     stream *s = openzipfile(filename, mode);
     if(s) return s;
-#endif
     return openrawfile(filename, mode);
 }
 
@@ -1149,7 +1207,7 @@ stream *opengzfile(const char *filename, const char *mode, stream *file, int lev
     stream *source = file ? file : openfile(filename, mode);
     if(!source) return NULL;
     gzstream *gz = new gzstream;
-    if(!gz->open(source, mode, !file, level)) { if(!file) delete source; return NULL; }
+    if(!gz->open(source, mode, !file, level)) { if(!file) delete source; delete gz; return NULL; }
     return gz;
 }
 
@@ -1158,26 +1216,26 @@ stream *openutf8file(const char *filename, const char *mode, stream *file)
     stream *source = file ? file : openfile(filename, mode);
     if(!source) return NULL;
     utf8stream *utf8 = new utf8stream;
-    if(!utf8->open(source, mode, !file)) { if(!file) delete source; return NULL; }
+    if(!utf8->open(source, mode, !file)) { if(!file) delete source; delete utf8; return NULL; }
     return utf8;
 }
 
-char *loadfile(const char *fn, int *size, bool utf8)
+char *loadfile(const char *fn, size_t *size, bool utf8)
 {
     stream *f = openfile(fn, "rb");
     if(!f) return NULL;
-    int len = (int)f->size();
+    size_t len = f->size();
     if(len <= 0) { delete f; return NULL; }
     char *buf = new char[len+1];
     if(!buf) { delete f; return NULL; }
-    int offset = 0;
+    size_t offset = 0;
     if(utf8 && len >= 3)
     {
         if(f->read(buf, 3) != 3) { delete f; delete[] buf; return NULL; }
         if(((uchar *)buf)[0] == 0xEF && ((uchar *)buf)[1] == 0xBB && ((uchar *)buf)[2] == 0xBF) len -= 3;
         else offset += 3;
     }
-    int rlen = f->read(&buf[offset], len-offset);
+    size_t rlen = f->read(&buf[offset], len-offset);
     delete f;
     if(rlen != len-offset) { delete[] buf; return NULL; }
     if(utf8) len = decodeutf8((uchar *)buf, len, (uchar *)buf, len);
diff --git a/src/shared/tools.cpp b/src/shared/tools.cpp
index 2762d21..f5953d8 100644
--- a/src/shared/tools.cpp
+++ b/src/shared/tools.cpp
@@ -2,6 +2,24 @@
 
 #include "cube.h"
 
+void *operator new(size_t size)
+{
+    void *p = malloc(size);
+    if(!p) abort();
+    return p;
+}
+
+void *operator new[](size_t size)
+{
+    void *p = malloc(size);
+    if(!p) abort();
+    return p;
+}
+
+void operator delete(void *p) { if(p) free(p); }
+
+void operator delete[](void *p) { if(p) free(p); }
+
 #ifndef WIN32
 #include <unistd.h>
 #endif
@@ -144,7 +162,7 @@ void sendstring(const char *t, ucharbuf &p) { sendstring_(t, p); }
 void sendstring(const char *t, packetbuf &p) { sendstring_(t, p); }
 void sendstring(const char *t, vector<uchar> &p) { sendstring_(t, p); }
 
-void getstring(char *text, ucharbuf &p, int len)
+void getstring(char *text, ucharbuf &p, size_t len)
 {
     char *t = text;
     do
diff --git a/src/shared/tools.h b/src/shared/tools.h
index 0384d9f..79d1a38 100644
--- a/src/shared/tools.h
+++ b/src/shared/tools.h
@@ -26,20 +26,11 @@ typedef unsigned long long int ullong;
 #define RESTRICT
 #endif
 
-inline void *operator new(size_t size)
-{
-    void *p = malloc(size);
-    if(!p) abort();
-    return p;
-}
-inline void *operator new[](size_t size)
-{
-    void *p = malloc(size);
-    if(!p) abort();
-    return p;
-}
-inline void operator delete(void *p) { if(p) free(p); }
-inline void operator delete[](void *p) { if(p) free(p); }
+#ifdef __GNUC__
+#define UNUSED __attribute__((unused))
+#else
+#define UNUSED
+#endif
 
 inline void *operator new(size_t, void *p) { return p; }
 inline void *operator new[](size_t, void *p) { return p; }
@@ -78,17 +69,21 @@ static inline T clamp(T a, T b, T c)
     return max(b, min(a, c));
 }
 
-#define rnd(x) ((int)(randomMT()&0xFFFFFF)%(x))
-#define rndscale(x) (float((randomMT()&0xFFFFFF)*double(x)/double(0xFFFFFF)))
+#define rnd(x) ((int)(randomMT()&0x7FFFFFFF)%(x))
+#define rndscale(x) (float((randomMT()&0x7FFFFFFF)*double(x)/double(0x7FFFFFFF)))
 #define detrnd(s, x) ((int)(((((uint)(s))*1103515245+12345)>>16)%(x)))
 #define isnumeric(c) (isdigit(c) || c == '+' || c == '-')
 
-#define loop(v,m) for(int v = 0; v<int(m); v++)
+#define loop(v,m) for(int v = 0; v < int(m); ++v)
 #define loopi(m) loop(i,m)
 #define loopj(m) loop(j,m)
 #define loopk(m) loop(k,m)
 #define loopl(m) loop(l,m)
-#define loopirev(v) for(int i = v-1; i>=0; i--)
+#define looprev(v,m) for(int v = int(m); --v >= 0;)
+#define loopirev(m) looprev(i,m)
+#define loopjrev(m) looprev(j,m)
+#define loopkrev(m) looprev(k,m)
+#define looplrev(m) looprev(l,m)
 
 #define DELETEP(p) if(p) { delete   p; p = 0; }
 #define DELETEA(p) if(p) { delete[] p; p = 0; }
@@ -112,8 +107,8 @@ static inline T clamp(T a, T b, T c)
 #pragma warning (disable: 4244) // conversion from 'int' to 'float', possible loss of data
 #pragma warning (disable: 4267) // conversion from 'size_t' to 'int', possible loss of data
 #pragma warning (disable: 4355) // 'this' : used in base member initializer list
-#pragma warning (disable: 4996) // 'strncpy' was declared deprecated
 #pragma warning (disable: 4800) // forcing value to bool 'true' or 'false' (performance warning)
+#pragma warning (disable: 4996) // 'strncpy' was declared deprecated
 #include <direct.h>
 #define chdir _chdir
 #define strcasecmp _stricmp
@@ -137,11 +132,42 @@ static inline T clamp(T a, T b, T c)
 
 #define MAXSTRLEN 512 // must be at least 512 bytes to comply with rfc1459
 typedef char string[MAXSTRLEN];
-#define mkstring(d) string d; d[0] = 0;
 
-inline void vformatstring(char *d, const char *fmt, va_list v, int len = MAXSTRLEN) { _vsnprintf(d, len, fmt, v); d[len-1] = 0; }
-inline char *copystring(char *d, const char *s, size_t len = MAXSTRLEN) { strncpy(d, s, len); d[len-1] = 0; return d; }
-inline char *concatstring(char *d, const char *s, size_t len = MAXSTRLEN) { size_t used = strlen(d); return used < len ? copystring(d+used, s, len-used) : d; }
+#define BIGSTRLEN 4096
+typedef char bigstring[BIGSTRLEN];
+
+inline void vformatstring(char *d, const char *fmt, va_list v, int len = MAXSTRLEN)
+{
+    _vsnprintf(d, len, fmt, v);
+    d[len-1] = 0;
+}
+inline void vformatbigstring(char *d, const char *fmt, va_list v, int len = BIGSTRLEN) { vformatstring(d, fmt, v, len); }
+
+inline char *copystring(char *d, const char *s, size_t len = MAXSTRLEN)
+{
+    size_t slen = min(strlen(s)+1, len);
+    memcpy(d, s, slen);
+    d[slen-1] = 0;
+    return d;
+}
+inline char *copybigstring(char *d, const char *s, size_t len = BIGSTRLEN) { return copystring(d, s, len); }
+
+inline char *concatstring(char *d, const char *s, size_t len = MAXSTRLEN)
+{
+    size_t used = strlen(d);
+    return used < len ? copystring(d+used, s, len-used) : d;
+}
+inline char *concatbigstring(char *d, const char *s, size_t len = BIGSTRLEN) { return concatstring(d, s, len); }
+
+inline char *prependstring(char *d, const char *s, size_t len = MAXSTRLEN)
+{
+    size_t slen = min(strlen(s), len);
+    memmove(&d[slen], d, min(len - slen, strlen(d) + 1));
+    memcpy(d, s, slen);
+    d[len-1] = 0;
+    return d;
+}
+inline char *prependbigstring(char *d, const char *s, size_t len = BIGSTRLEN) { return prependstring(d, s, len); }
 
 struct stringformatter
 {
@@ -167,6 +193,39 @@ struct stringformatter
 #define defformatstring(d) string d; formatstring(d)
 #define defvformatstring(d,last,fmt) string d; { va_list ap; va_start(ap, last); vformatstring(d, fmt, ap); va_end(ap); }
 
+struct bigstringformatter
+{
+    char *buf;
+    bigstringformatter(char *buf): buf((char *)buf) {}
+    void operator()(const char *fmt, ...) PRINTFARGS(2, 3)
+    {
+        va_list v;
+        va_start(v, fmt);
+        vformatbigstring(buf, fmt, v);
+        va_end(v);
+    }
+    void operator()(int len, const char *fmt, ...) PRINTFARGS(3, 4)
+    {
+        va_list v;
+        va_start(v, fmt);
+        vformatbigstring(buf, fmt, v, len+1);
+        va_end(v);
+    }
+};
+
+#define formatbigstring(d) bigstringformatter((char *)d)
+#define defformatbigstring(d) bigstring d; formatbigstring(d)
+#define defvformatbigstring(d,last,fmt) bigstring d; { va_list ap; va_start(ap, last); vformatbigstring(d, fmt, ap); va_end(ap); }
+
+template<size_t N> inline bool matchstring(const char *s, size_t len, const char (&d)[N])
+{
+    return len == N-1 && !memcmp(s, d, N-1);
+}
+
+inline char *newstring(size_t l)                { return new char[l+1]; }
+inline char *newstring(const char *s, size_t l) { return copystring(newstring(l), s, l+1); }
+inline char *newstring(const char *s)           { if(!s) s = ""; size_t l = strlen(s); char *d = newstring(l); memcpy(d, s, l+1); return d; }
+
 #define loopv(v)    for(int i = 0; i<(v).length(); i++)
 #define loopvj(v)   for(int j = 0; j<(v).length(); j++)
 #define loopvk(v)   for(int k = 0; k<(v).length(); k++)
@@ -202,7 +261,7 @@ struct databuf
 
     databuf subbuf(int sz)
     {
-        sz = min(sz, maxlen-len);
+        sz = clamp(sz, 0, maxlen-len);
         len += sz;
         return databuf(&buf[len-sz], sz);
     }
@@ -550,12 +609,12 @@ template <class T> struct vector
     {
         int olen = alen;
         if(!alen) alen = max(MINSIZE, sz);
-        else while(alen < sz) alen *= 2;
+        else while(alen < sz) alen += alen/2;
         if(alen <= olen) return;
         uchar *newbuf = new uchar[alen*sizeof(T)];
         if(olen > 0)
         {
-            memcpy(newbuf, (void *)buf, olen*sizeof(T));
+            if(ulen > 0) memcpy(newbuf, (void *)buf, ulen*sizeof(T));
             delete[] (uchar *)buf;
         }
         buf = (T *)newbuf;
@@ -622,9 +681,20 @@ template <class T> struct vector
         return -1;
     }
 
+    void addunique(const T &o)
+    {
+        if(find(o) < 0) add(o);
+    }
+
     void removeobj(const T &o)
     {
-        loopi(ulen) if(buf[i]==o) remove(i--);
+        loopi(ulen) if(buf[i] == o)
+        {
+            int dst = i;
+            for(int j = i+1; j < ulen; j++) if(!(buf[j] == o)) buf[dst++] = buf[j];
+            setsize(dst);
+            break;
+        }
     }
 
     void replacewithlast(const T &o)
@@ -633,6 +703,7 @@ template <class T> struct vector
         loopi(ulen-1) if(buf[i]==o)
         {
             buf[i] = buf[ulen-1];
+            break;
         }
         ulen--;
     }
@@ -739,15 +810,13 @@ template <class T> struct smallvector
 
     void growbuf(int sz)
     {
-        int olen = len;
         len = max(sz, 0);
-        uchar *newbuf = len > 0 ? new uchar[len*sizeof(T)] : NULL;
-        if(olen > 0)
+        if(len)
         {
-            if(len > 0) memcpy(newbuf, buf, min(len, olen)*sizeof(T));
-            delete[] (uchar *)buf;
+            buf = (T *)realloc(buf, len*sizeof(T));
+            if(!buf) abort();
         }
-        buf = (T *)newbuf;
+        else if(buf) { free(buf); buf = NULL; }
     }
 
     T &add(const T &x)
@@ -1112,46 +1181,6 @@ struct unionfind
     }
 };
 
-template <class T, int SIZE> struct ringbuf
-{
-    int index, len;
-    T data[SIZE];
-
-    ringbuf() { clear(); }
-
-    void clear()
-    {
-        index = len = 0;
-    }
-
-    bool empty() const { return !len; }
-
-    const int length() const { return len; }
-
-    T &add()
-    {
-        T &t = data[index];
-        index++;
-        if(index >= SIZE) index -= SIZE;
-        if(len < SIZE) len++;
-        return t;
-    }
-
-    T &add(const T &e) { return add() = e; }
-
-    T &operator[](int i)
-    {
-        i += index - len;
-        return data[i < 0 ? i + SIZE : i%SIZE];
-    }
-
-    const T &operator[](int i) const
-    {
-        i += index - len;
-        return data[i < 0 ? i + SIZE : i%SIZE];
-    }
-};
-
 template <class T, int SIZE> struct queue
 {
     int head, tail, len;
@@ -1165,35 +1194,69 @@ template <class T, int SIZE> struct queue
     bool empty() const { return !len; }
     bool full() const { return len == SIZE; }
 
+    bool inrange(size_t i) const { return i<size_t(len); }
+    bool inrange(int i) const { return i>=0 && i<len; }
+
     T &added() { return data[tail > 0 ? tail-1 : SIZE-1]; }
     T &added(int offset) { return data[tail-offset > 0 ? tail-offset-1 : tail-offset-1 + SIZE]; }
     T &adding() { return data[tail]; }
     T &adding(int offset) { return data[tail+offset >= SIZE ? tail+offset - SIZE : tail+offset]; }
     T &add()
     {
-        ASSERT(len < SIZE);
         T &t = data[tail];
-        tail = (tail + 1)%SIZE;
-        len++;
+        tail++;
+        if(tail >= SIZE) tail -= SIZE;
+        if(len < SIZE) len++;
         return t;
     }
+    T &add(const T &e) { return add() = e; }
+
+    T &insertback(int offset)
+    {
+        int cur = tail, next = tail;
+        add();
+        loopi(offset)
+        {
+            cur--;
+            if(cur < 0) cur += SIZE;
+            data[next] = data[cur];
+            next = cur;
+        }
+        return data[cur];
+    }
+    T &insertback(int offset, const T &e) { return insertback(offset) = e; }
+
+    T &pop()
+    {
+        tail--;
+        if(tail < 0) tail += SIZE;
+        len--;
+        return data[tail];
+    }
 
     T &removing() { return data[head]; }
     T &removing(int offset) { return data[head+offset >= SIZE ? head+offset - SIZE : head+offset]; }
     T &remove()
     {
-        ASSERT(len > 0);
         T &t = data[head];
-        head = (head + 1)%SIZE;
+        head++;
+        if(head >= SIZE) head -= SIZE;
         len--;
         return t;
     }
+
+    T &operator[](int offset) { return removing(offset); }
+    const T &operator[](int offset) const { return removing(offset); }
 };
 
-inline char *newstring(size_t l) { return new char[l+1]; }
-inline char *newstring(const char *s, size_t l) { return copystring(newstring(l), s, l+1); }
-inline char *newstring(const char *s) { return newstring(s, strlen(s));          }
-inline char *newstringbuf(const char *s) { return newstring(s, MAXSTRLEN-1);       }
+template <class T, int SIZE> struct reversequeue : queue<T, SIZE>
+{
+    T &insert(int offset) { return queue<T, SIZE>::insertback(offset); }
+    T &insert(int offset, const T &e) { return queue<T, SIZE>::insertback(offset, e); }
+
+    T &operator[](int offset) { return queue<T, SIZE>::added(offset); }
+    const T &operator[](int offset) const { return queue<T, SIZE>::added(offset); }
+};
 
 #if defined(WIN32) && !defined(__GNUC__)
 #ifdef _DEBUG
@@ -1223,9 +1286,9 @@ template<> inline int endianswap<int>(int n) { return endianswap32(n); }
 template<> inline ullong endianswap<ullong>(ullong n) { return endianswap64(n); }
 template<> inline llong endianswap<llong>(llong n) { return endianswap64(n); }
 template<> inline double endianswap<double>(double n) { union { double t; uint i; } conv; conv.t = n; conv.i = endianswap64(conv.i); return conv.t; }
-template<class T> inline void endianswap(T *buf, int len) { for(T *end = &buf[len]; buf < end; buf++) *buf = endianswap(*buf); }
+template<class T> inline void endianswap(T *buf, size_t len) { for(T *end = &buf[len]; buf < end; buf++) *buf = endianswap(*buf); }
 template<class T> inline T endiansame(T n) { return n; }
-template<class T> inline void endiansame(T *buf, int len) {}
+template<class T> inline void endiansame(T *buf, size_t len) {}
 #ifdef SDL_BYTEORDER
 #if SDL_BYTEORDER == SDL_LIL_ENDIAN
 #define lilswap endiansame
@@ -1236,9 +1299,9 @@ template<class T> inline void endiansame(T *buf, int len) {}
 #endif
 #else
 template<class T> inline T lilswap(T n) { return *(const uchar *)&islittleendian ? n : endianswap(n); }
-template<class T> inline void lilswap(T *buf, int len) { if(!*(const uchar *)&islittleendian) endianswap(buf, len); }
+template<class T> inline void lilswap(T *buf, size_t len) { if(!*(const uchar *)&islittleendian) endianswap(buf, len); }
 template<class T> inline T bigswap(T n) { return *(const uchar *)&islittleendian ? endianswap(n) : n; }
-template<class T> inline void bigswap(T *buf, int len) { if(*(const uchar *)&islittleendian) endianswap(buf, len); }
+template<class T> inline void bigswap(T *buf, size_t len) { if(*(const uchar *)&islittleendian) endianswap(buf, len); }
 #endif
 
 /* workaround for some C platforms that have these two functions as macros - not used anywhere */
@@ -1269,22 +1332,23 @@ struct stream
     virtual bool seek(offset pos, int whence = SEEK_SET) { return false; }
     virtual offset size();
     virtual offset rawsize() { return size(); }
-    virtual int read(void *buf, int len) { return 0; }
-    virtual int write(const void *buf, int len) { return 0; }
+    virtual size_t read(void *buf, size_t len) { return 0; }
+    virtual size_t write(const void *buf, size_t len) { return 0; }
+    virtual bool flush() { return true; }
     virtual int getchar() { uchar c; return read(&c, 1) == 1 ? c : -1; }
     virtual bool putchar(int n) { uchar c = n; return write(&c, 1) == 1; }
-    virtual bool getline(char *str, int len);
-    virtual bool putstring(const char *str) { int len = (int)strlen(str); return write(str, len) == len; }
+    virtual bool getline(char *str, size_t len);
+    virtual bool putstring(const char *str) { size_t len = strlen(str); return write(str, len) == len; }
     virtual bool putline(const char *str) { return putstring(str) && putchar('\n'); }
-    virtual int printf(const char *fmt, ...) PRINTFARGS(2, 3);
+    virtual size_t printf(const char *fmt, ...) PRINTFARGS(2, 3);
     virtual uint getcrc() { return 0; }
 
-    template<class T> int put(const T *v, int n) { return write(v, n*sizeof(T))/sizeof(T); }
+    template<class T> size_t put(const T *v, size_t n) { return write(v, n*sizeof(T))/sizeof(T); }
     template<class T> bool put(T n) { return write(&n, sizeof(n)) == sizeof(n); }
     template<class T> bool putlil(T n) { return put<T>(lilswap(n)); }
     template<class T> bool putbig(T n) { return put<T>(bigswap(n)); }
 
-    template<class T> int get(T *v, int n) { return read(v, n*sizeof(T))/sizeof(T); }
+    template<class T> size_t get(T *v, size_t n) { return read(v, n*sizeof(T))/sizeof(T); }
     template<class T> T get() { T n; return read(&n, sizeof(n)) == sizeof(n) ? n : 0; }
     template<class T> T getlil() { return lilswap(get<T>()); }
     template<class T> T getbig() { return bigswap(get<T>()); }
@@ -1302,10 +1366,10 @@ struct streambuf
     streambuf(stream *s) : s(s) {}
 
     T get() { return s->get<T>(); }
-    int get(T *vals, int numvals) { return s->get(vals, numvals); }
+    size_t get(T *vals, size_t numvals) { return s->get(vals, numvals); }
     void put(const T &val) { s->put(&val, 1); }
-    void put(const T *vals, int numvals) { s->put(vals, numvals); }
-    int length() { return s->size(); }
+    void put(const T *vals, size_t numvals) { s->put(vals, numvals); }
+    size_t length() { return s->size(); }
 };
 
 enum
@@ -1336,8 +1400,18 @@ static inline uchar uni2cube(int c)
     extern const uchar uni2cubechars[];
     return uint(c) <= 0x7FF ? uni2cubechars[uni2cubeoffsets[c>>8] + (c&0xFF)] : 0;
 }
-extern int decodeutf8(uchar *dst, int dstlen, const uchar *src, int srclen, int *carry = NULL);
-extern int encodeutf8(uchar *dstbuf, int dstlen, const uchar *srcbuf, int srclen, int *carry = NULL);
+static inline uchar cubelower(uchar c)
+{
+    extern const uchar cubelowerchars[256];
+    return cubelowerchars[c];
+}
+static inline uchar cubeupper(uchar c)
+{
+    extern const uchar cubeupperchars[256];
+    return cubeupperchars[c];
+}
+extern size_t decodeutf8(uchar *dst, size_t dstlen, const uchar *src, size_t srclen, size_t *carry = NULL);
+extern size_t encodeutf8(uchar *dstbuf, size_t dstlen, const uchar *srcbuf, size_t srclen, size_t *carry = NULL);
 
 extern char *makerelpath(const char *dir, const char *file, const char *prefix = NULL, const char *cmd = NULL);
 extern char *makefile(const char *s, const char *e = "", int revision = 0, int start = 1, bool store = false, bool skip = false);
@@ -1351,13 +1425,14 @@ extern void appendhomedir(const char *dir);
 extern void addpackagedir(const char *dir, int flags = 0);
 extern int maskpackagedirs(int flags);
 extern const char *findfile(const char *filename, const char *mode);
+extern bool findzipfile(const char *filename);
 extern stream *openrawfile(const char *filename, const char *mode);
 extern stream *openzipfile(const char *filename, const char *mode);
 extern stream *openfile(const char *filename, const char *mode);
 extern stream *opentempfile(const char *filename, const char *mode);
 extern stream *opengzfile(const char *filename, const char *mode, stream *file = NULL, int level = Z_BEST_COMPRESSION);
 extern stream *openutf8file(const char *filename, const char *mode, stream *file = NULL);
-extern char *loadfile(const char *fn, int *size, bool utf8 = true);
+extern char *loadfile(const char *fn, size_t *size, bool utf8 = true);
 extern bool listdir(const char *dir, bool rel, const char *ext, vector<char *> &files);
 extern int listfiles(const char *dir, const char *ext, vector<char *> &files);
 extern int listzipfiles(const char *dir, const char *ext, vector<char *> &files);
@@ -1382,8 +1457,8 @@ extern float getfloat(ucharbuf &p);
 extern void sendstring(const char *t, ucharbuf &p);
 extern void sendstring(const char *t, packetbuf &p);
 extern void sendstring(const char *t, vector<uchar> &p);
-extern void getstring(char *t, ucharbuf &p, int len);
-template<size_t N> static inline void getstring(char (&t)[N], ucharbuf &p) { getstring(t, p, int(N)); }
+extern void getstring(char *t, ucharbuf &p, size_t len);
+template<size_t N> static inline void getstring(char (&t)[N], ucharbuf &p) { getstring(t, p, N); }
 
 #endif
 
diff --git a/src/shared/zip.cpp b/src/shared/zip.cpp
index bb9a997..85b9b36 100644
--- a/src/shared/zip.cpp
+++ b/src/shared/zip.cpp
@@ -74,23 +74,27 @@ static bool findzipdirectory(FILE *f, zipdirectoryheader &hdr)
 {
     if(fseek(f, 0, SEEK_END) < 0) return false;
 
+    long offset = ftell(f);
+    if(offset < 0) return false;
+
     uchar buf[1024], *src = NULL;
-    int len = 0, offset = ftell(f), end = max(offset - 0xFFFF - ZIP_DIRECTORY_SIZE, 0);
+    long end = max(offset - 0xFFFFL - ZIP_DIRECTORY_SIZE, 0L);
+    size_t len = 0;
     const uint signature = lilswap<uint>(ZIP_DIRECTORY_SIGNATURE);
 
     while(offset > end)
     {
-        int carry = min(len, ZIP_DIRECTORY_SIZE-1), next = min((int)sizeof(buf) - carry, offset - end);
+        size_t carry = min(len, size_t(ZIP_DIRECTORY_SIZE-1)), next = min(sizeof(buf) - carry, size_t(offset - end));
         offset -= next;
         memmove(&buf[next], buf, carry);
-        if(next + carry < ZIP_DIRECTORY_SIZE || fseek(f, offset, SEEK_SET) < 0 || (int)fread(buf, 1, next, f) != next) return false;
+        if(next + carry < ZIP_DIRECTORY_SIZE || fseek(f, offset, SEEK_SET) < 0 || fread(buf, 1, next, f) != next) return false;
         len = next + carry;
         uchar *search = &buf[next-1];
         for(; search >= buf; search--) if(*(uint *)search == signature) break;
         if(search >= buf) { src = search; break; }
     }
 
-    if(&buf[len] - src < ZIP_DIRECTORY_SIZE) return false;
+    if(!src || &buf[len] - src < ZIP_DIRECTORY_SIZE) return false;
 
     hdr.signature = lilswap(*(uint *)src); src += 4;
     hdr.disknumber = lilswap(*(ushort *)src); src += 2;
@@ -110,10 +114,10 @@ static bool findzipdirectory(FILE *f, zipdirectoryheader &hdr)
 VAR(0, dbgzip, 0, 0, 1);
 #endif
 
-static bool readzipdirectory(const char *archname, FILE *f, int entries, int offset, int size, vector<zipfile> &files)
+static bool readzipdirectory(const char *archname, FILE *f, int entries, int offset, uint size, vector<zipfile> &files)
 {
     uchar *buf = new uchar[size], *src = buf;
-    if(fseek(f, offset, SEEK_SET) < 0 || (int)fread(buf, 1, size, f) != size) { delete[] buf; return false; }
+    if(fseek(f, offset, SEEK_SET) < 0 || fread(buf, 1, size, f) != size) { delete[] buf; return false; }
     loopi(entries)
     {
         if(src + ZIP_FILE_SIZE > &buf[size]) break;
@@ -169,9 +173,8 @@ static bool readzipdirectory(const char *archname, FILE *f, int entries, int off
 
 static bool readlocalfileheader(FILE *f, ziplocalfileheader &h, uint offset)
 {
-    fseek(f, offset, SEEK_SET);
     uchar buf[ZIP_LOCAL_FILE_SIZE];
-    if(fread(buf, 1, ZIP_LOCAL_FILE_SIZE, f) != ZIP_LOCAL_FILE_SIZE)
+    if(fseek(f, offset, SEEK_SET) < 0 || fread(buf, 1, ZIP_LOCAL_FILE_SIZE, f) != ZIP_LOCAL_FILE_SIZE)
         return false;
     uchar *src = buf;
     h.signature = lilswap(*(uint *)src); src += 4;
@@ -209,9 +212,9 @@ static bool checkprefix(vector<zipfile> &files, const char *prefix, int prefixle
 
 static void mountzip(ziparchive &arch, vector<zipfile> &files, const char *mountdir, const char *stripdir)
 {
-    string packagesdir = "packages/";
+    string packagesdir = "data/";
     path(packagesdir);
-    int striplen = stripdir ? (int)strlen(stripdir) : 0;
+    size_t striplen = stripdir ? strlen(stripdir) : 0;
     if(!mountdir && !stripdir) loopv(files)
     {
         zipfile &f = files[i];
@@ -225,19 +228,19 @@ static void mountzip(ziparchive &arch, vector<zipfile> &files, const char *mount
             }
             break;
         }
-        const char *foundogz = strstr(f.name, ".ogz");
-        if(foundogz)
+        const char *foundmpz = strstr(f.name, ".mpz");
+        if(foundmpz)
         {
-            const char *ogzdir = foundogz;
-            while(--ogzdir >= f.name && *ogzdir != PATHDIV);
-            if(ogzdir < f.name || checkprefix(files, f.name, ogzdir + 1 - f.name))
+            const char *mpzdir = foundmpz;
+            while(--mpzdir >= f.name && *mpzdir != PATHDIV);
+            if(mpzdir < f.name || checkprefix(files, f.name, mpzdir + 1 - f.name))
             {
-                if(ogzdir >= f.name)
+                if(mpzdir >= f.name)
                 {
                     stripdir = f.name;
-                    striplen = ogzdir + 1 - f.name;
+                    striplen = mpzdir + 1 - f.name;
                 }
-                if(!mountdir) mountdir = "packages/base/";
+                if(!mountdir) mountdir = "maps/";
                 break;
             }
         }
@@ -265,7 +268,7 @@ bool addzip(const char *name, const char *mount = NULL, const char *strip = NULL
     string pname;
     copystring(pname, name);
     path(pname);
-    int plen = (int)strlen(pname);
+    size_t plen = strlen(pname);
     if(plen < 4 || !strchr(&pname[plen-4], '.')) concatstring(pname, ".zip");
 
     ziparchive *exists = findzip(pname);
@@ -335,10 +338,10 @@ struct zipstream : stream
     zipfile *info;
     z_stream zfile;
     uchar *buf;
-    int reading;
+    uint reading;
     bool ended;
 
-    zipstream() : arch(NULL), info(NULL), buf(NULL), reading(-1), ended(false)
+    zipstream() : arch(NULL), info(NULL), buf(NULL), reading(~0U), ended(false)
     {
         zfile.zalloc = NULL;
         zfile.zfree = NULL;
@@ -362,8 +365,8 @@ struct zipstream : stream
             if(fseek(arch->data, reading, SEEK_SET) >= 0) arch->owner = this;
             else return;
         }
-        uint remaining = info->offset + info->compressedsize - reading;
-        int n = arch->owner == this ? (int)fread(zfile.next_in + zfile.avail_in, 1, min(size, remaining), arch->data) : 0;
+        uint remaining = info->offset + info->compressedsize - reading,
+             n = arch->owner == this ? fread(zfile.next_in + zfile.avail_in, 1, min(size, remaining), arch->data) : 0U;
         zfile.avail_in += n;
         reading += n;
     }
@@ -391,12 +394,12 @@ struct zipstream : stream
 
     void stopreading()
     {
-        if(reading < 0) return;
+        if(reading == ~0U) return;
 #ifndef STANDALONE
         if(dbgzip) conoutf(info->compressedsize ? "%s: zfile.total_out %u, info->size %u" : "%s: reading %u, info->size %u", info->name, info->compressedsize ? uint(zfile.total_out) : reading - info->offset, info->size);
 #endif
         if(info->compressedsize) inflateEnd(&zfile);
-        reading = -1;
+        reading = ~0U;
     }
 
     void close()
@@ -407,12 +410,12 @@ struct zipstream : stream
     }
 
     offset size() { return info->size; }
-    bool end() { return reading < 0 || ended; }
-    offset tell() { return reading >= 0 ? (info->compressedsize ? zfile.total_out : reading - info->offset) : -1; }
+    bool end() { return reading == ~0U || ended; }
+    offset tell() { return reading != ~0U ? (info->compressedsize ? zfile.total_out : reading - info->offset) : offset(-1); }
 
     bool seek(offset pos, int whence)
     {
-        if(reading < 0) return false;
+        if(reading == ~0U) return false;
         if(!info->compressedsize)
         {
             switch(whence)
@@ -472,7 +475,7 @@ struct zipstream : stream
         uchar skip[512];
         while(pos > 0)
         {
-            int skipped = (int)min(pos, (offset)sizeof(skip));
+            size_t skipped = (size_t)min(pos, (offset)sizeof(skip));
             if(read(skip, skipped) != skipped) return false;
             pos -= skipped;
         }
@@ -481,9 +484,9 @@ struct zipstream : stream
         return true;
     }
 
-    int read(void *buf, int len)
+    size_t read(void *buf, size_t len)
     {
-        if(reading < 0 || !buf || !len) return 0;
+        if(reading == ~0U || !buf || !len) return 0;
         if(!info->compressedsize)
         {
             if(arch->owner != this)
@@ -493,7 +496,7 @@ struct zipstream : stream
                 arch->owner = this;
             }
 
-            int n = (int)fread(buf, 1, min(len, int(info->size + info->offset - reading)), arch->data);
+            size_t n = fread(buf, 1, min(len, size_t(info->size + info->offset - reading)), arch->data);
             reading += n;
             if(n < len) ended = true;
             return n;
@@ -525,6 +528,7 @@ struct zipstream : stream
 stream *openzipfile(const char *name, const char *mode)
 {
     for(; *mode; mode++) if(*mode=='w' || *mode=='a') return NULL;
+    name = copypath(name, true);
     loopvrev(archives)
     {
         ziparchive *arch = archives[i];
@@ -537,9 +541,22 @@ stream *openzipfile(const char *name, const char *mode)
     return NULL;
 }
 
+bool findzipfile(const char *name)
+{
+    name = copypath(name, true);
+    loopvrev(archives)
+    {
+        ziparchive *arch = archives[i];
+        if(arch->files.access(name)) return true;
+    }
+    return false;
+}
+
 int listzipfiles(const char *dir, const char *ext, vector<char *> &files)
 {
-    int extsize = ext ? (int)strlen(ext)+1 : 0, dirsize = (int)strlen(dir), dirs = 0;
+    dir = copypath(dir, true);
+    size_t extsize = ext ? strlen(ext)+1 : 0, dirsize = strlen(dir);
+    int dirs = 0;
     loopvrev(archives)
     {
         ziparchive *arch = archives[i];
@@ -553,9 +570,13 @@ int listzipfiles(const char *dir, const char *ext, vector<char *> &files)
             if(!ext) files.add(newstring(name));
             else
             {
-                int namelength = (int)strlen(name) - extsize;
-                if(namelength > 0 && name[namelength] == '.' && strncmp(name+namelength+1, ext, extsize-1)==0)
-                    files.add(newstring(name, namelength));
+                size_t namelen = strlen(name);
+                if(namelen > extsize)
+                {
+                    namelen -= extsize;
+                    if(name[namelen] == '.' && strncmp(name+namelen+1, ext, extsize-1)==0)
+                        files.add(newstring(name, namelen));
+                }
             }
         });
         if(files.length() > oldsize) dirs++;
@@ -563,8 +584,6 @@ int listzipfiles(const char *dir, const char *ext, vector<char *> &files)
     return dirs;
 }
 
-#ifndef STANDALONE
 ICOMMAND(0, addzip, "sss", (const char *name, const char *mount, const char *strip), addzip(name, mount[0] ? mount : NULL, strip[0] ? strip : NULL));
 ICOMMAND(0, removezip, "s", (const char *name), removezip(name));
-#endif
 
diff --git a/src/system-install.mk b/src/system-install.mk
index 6f375f9..e6eae10 100644
--- a/src/system-install.mk
+++ b/src/system-install.mk
@@ -1,10 +1,9 @@
 appname=$(APPNAME)
-appnamefull:=$(shell sed -n 's/versionname *"\([^"]*\)"/\1/p' ../game/$(APPSHORTNAME)/version.cfg)
+appnamefull=$(shell sed -n 's/.define VERSION_NAME *"\([^"]*\)"/\1/p' version.h)
 appsrcname=$(APPNAME)
-cappname:=$(shell echo $(appname) | tr '[:lower:]' '[:upper:]')# Captial appname
-appclient=$(APPCLIENT)
-appserver=$(APPSERVER)
-appgamedir=game/$(APPSHORTNAME)
+cappname=$(shell echo $(appname) | tr '[:lower:]' '[:upper:]')# Captial appname
+appclient=$(APPCLIENT)$(APPMODIFIER)$(BIN_SUFFIX)
+appserver=$(APPSERVER)$(APPMODIFIER)$(BIN_SUFFIX)
 
 prefix=/usr/local
 games=
@@ -54,46 +53,54 @@ install/nix/$(appsrcname)_x32.xpm: $(ICON)
 icons: $(ICONS)
 
 system-install-client: client
-	install -d $(libexecdir)/$(appname)
-	install -d $(gamesbindir)
+	$(MKDIR) $(libexecdir)/$(appname)
+	$(MKDIR) $(gamesbindir)
 	install -m755 $(appclient) $(libexecdir)/$(appname)/$(appname)
 	install -m755 install/nix/$(appsrcname).am \
 		$(gamesbindir)/$(appname)
-	sed -e 's, at LIBEXECDIR@,$(patsubst $(DESTDIR)%,%,$(libexecdir)),g' \
-		-e 's, at DATADIR@,$(patsubst $(DESTDIR)%,%,$(datadir)),g' \
-		-e 's, at DOCDIR@,$(patsubst $(DESTDIR)%,%,$(docdir)),g' \
-		-e 's, at APPNAME@,$(appname),g' \
-		-i $(gamesbindir)/$(appname)
+	printf "\
+	g, at LIBEXECDIR@,\
+	s, at LIBEXECDIR@,$(patsubst $(DESTDIR)%,%,$(libexecdir)),g\n\
+	g, at DATADIR@,\
+	s, at DATADIR@,$(patsubst $(DESTDIR)%,%,$(datadir)),g\n\
+	g, at DOCDIR@,\
+	s, at DOCDIR@,$(patsubst $(DESTDIR)%,%,$(docdir)),g\n\
+	g, at APPNAME@,\
+	s, at APPNAME@,$(appname),g\n\
+	w\n" | ed -s $(gamesbindir)/$(appname)
 	ln -s $(patsubst $(DESTDIR)%,%,$(datadir))/$(appname)/data \
 		$(libexecdir)/$(appname)/data
-	ln -s $(patsubst $(DESTDIR)%,%,$(datadir))/$(appname)/game \
-		$(libexecdir)/$(appname)/game
 
 system-install-server: server
-	install -d $(libexecdir)/$(appname)
-	install -d $(gamesbindir)
-	install -d $(datadir)/$(appname)
+	$(MKDIR) $(libexecdir)/$(appname)
+	$(MKDIR) $(gamesbindir)
+	$(MKDIR) $(datadir)/$(appname)
 	install -m755 $(appserver) \
 		$(libexecdir)/$(appname)/$(appname)-server
 	install -m755 install/nix/$(appsrcname)-server.am \
 		$(gamesbindir)/$(appname)-server
-	sed -e 's, at LIBEXECDIR@,$(patsubst $(DESTDIR)%,%,$(libexecdir)),g' \
-		-e 's, at DATADIR@,$(patsubst $(DESTDIR)%,%,$(datadir)),g' \
-		-e 's, at DOCDIR@,$(patsubst $(DESTDIR)%,%,$(docdir)),g' \
-		-e 's, at APPNAME@,$(appname),g' \
-		-i $(gamesbindir)/$(appname)-server
-	install -m644 ../$(appgamedir)/version.cfg \
+	printf "\
+	g, at LIBEXECDIR@,\
+	s, at LIBEXECDIR@,$(patsubst $(DESTDIR)%,%,$(libexecdir)),g\n\
+	g, at DATADIR@,\
+	s, at DATADIR@,$(patsubst $(DESTDIR)%,%,$(datadir)),g\n\
+	g, at DOCDIR@,\
+	s, at DOCDIR@,$(patsubst $(DESTDIR)%,%,$(docdir)),g\n\
+	g, at APPNAME@,\
+	s, at APPNAME@,$(appname),g\n\
+	w\n" | ed -s $(gamesbindir)/$(appname)-server
+	install -m644 ../config/version.cfg \
 		$(datadir)/$(appname)/version.cfg
+	ln -s $(patsubst $(DESTDIR)%,%,$(datadir))/$(appname)/version.cfg \
+		$(libexecdir)/$(appname)/version.cfg
 
 system-install-data:
-	install -d $(datadir)/$(appname)
-	install -d $(datadir)/$(appname)/game
 	cp -r ../data $(datadir)/$(appname)/data
-	cp -r ../$(appgamedir) $(datadir)/$(appname)/game
+	rm -f $(datadir)/$(appname)/data/misc/largeandincharge.png
 
 system-install-docs: $(MANPAGES)
-	install	-d $(mandir)/man6
-	install -d $(docdir)/$(appname)
+	$(MKDIR) $(mandir)/man6
+	$(MKDIR) $(docdir)/$(appname)
 	sed -e 's, at LIBEXECDIR@,$(patsubst $(DESTDIR)%,%,$(libexecdir)),g' \
 		-e 's, at DATADIR@,$(patsubst $(DESTDIR)%,%,$(datadir)),g' \
 		-e 's, at DOCDIR@,$(patsubst $(DESTDIR)%,%,$(docdir)),g' \
@@ -112,13 +119,13 @@ system-install-docs: $(MANPAGES)
 	cp ../doc/guidelines.txt $(docdir)/$(appname)/guidelines.txt
 
 system-install-menus: icons
-	install -d $(menudir)
-	install -d $(icondir)/16x16/apps
-	install -d $(icondir)/32x32/apps
-	install -d $(icondir)/48x48/apps
-	install -d $(icondir)/64x64/apps
-	install -d $(icondir)/128x128/apps
-	install -d $(pixmapdir)
+	$(MKDIR) $(menudir)
+	$(MKDIR) $(icondir)/16x16/apps
+	$(MKDIR) $(icondir)/32x32/apps
+	$(MKDIR) $(icondir)/48x48/apps
+	$(MKDIR) $(icondir)/64x64/apps
+	$(MKDIR) $(icondir)/128x128/apps
+	$(MKDIR) $(pixmapdir)
 	sed -e 's, at LIBEXECDIR@,$(patsubst $(DESTDIR)%,%,$(libexecdir)),g' \
 		-e 's, at DATADIR@,$(patsubst $(DESTDIR)%,%,$(datadir)),g' \
 		-e 's, at DOCDIR@,$(patsubst $(DESTDIR)%,%,$(docdir)),g' \
@@ -139,11 +146,11 @@ system-install-menus: icons
 		$(pixmapdir)/$(appname).xpm
 
 system-install-cube2font: cube2font system-install-cube2font-docs
-	install -d $(bindir)
+	$(MKDIR) $(bindir)
 	install -m755 cube2font $(bindir)/cube2font
 
 system-install-cube2font-docs: ../doc/man/cube2font.1
-	install -d $(mandir)/man1
+	$(MKDIR) $(mandir)/man1
 	gzip -9 -n -c < ../doc/man/cube2font.1 \
 		> $(mandir)/man1/cube2font.1.gz
 
@@ -152,7 +159,7 @@ system-install: system-install-client system-install-server system-install-data
 system-uninstall-client:
 	@rm -fv $(libexecdir)/$(appname)/$(appname)
 	@rm -fv $(libexecdir)/$(appname)/data
-	@rm -fv $(libexecdir)/$(appname)/game
+	@rm -fv	$(libexecdir)/$(appname)/version.cfg
 	@rm -fv $(gamesbindir)/$(appname)
 
 system-uninstall-server:
@@ -161,7 +168,7 @@ system-uninstall-server:
 
 system-uninstall-data:
 	rm -rf $(datadir)/$(appname)/data
-	rm -rf $(datadir)/$(appname)/game
+	rm -fv $(datadir)/$(appname)/version.cfg
 
 system-uninstall-docs:
 	@rm -rfv $(docdir)/$(appname)/examples
diff --git a/src/version.h b/src/version.h
new file mode 100644
index 0000000..b58f790
--- /dev/null
+++ b/src/version.h
@@ -0,0 +1,10 @@
+#define VERSION_MAJOR 1
+#define VERSION_MINOR 5
+#define VERSION_PATCH 1
+#define VERSION_STRING "1.5.1"
+#define VERSION_NAME "Red Eclipse"
+#define VERSION_UNAME "redeclipse"
+#define VERSION_RELEASE "Aurora Edition"
+#define VERSION_URL "http://redeclipse.net/"
+#define VERSION_COPY "2010-2015"
+#define VERSION_DESC "A fun-filled new take on the casual first-person arena shooter."
diff --git a/src/wiki.mk b/src/wiki.mk
new file mode 100644
index 0000000..b06ff9b
--- /dev/null
+++ b/src/wiki.mk
@@ -0,0 +1,107 @@
+weapon-names=$(shell sed -n '/WPSVAR(0, name,/,/);/s/ *"\([^"]*\)",*/\1 /g;s/ $$//p' game/weapons.h)
+weapon-wiki-pages=$(shell for w in $(weapon-names); do echo "../doc/wiki-weapon-$${w}.txt"; done)
+
+../doc/wiki-contributors.txt: ../readme.txt
+	scripts/wiki-contributors $< $@
+
+wiki-contributors: ../doc/wiki-contributors.txt
+
+../doc/wiki-guidelines.txt: ../doc/guidelines.txt
+	scripts/wiki-guidelines $< $@
+
+wiki-guidelines: ../doc/wiki-guidelines.txt
+
+../doc/wiki-manpage-%.txt: ../doc/man/%.am
+	# first substitute all placeholders via sed
+	# then convert to mediawiki syntax in two steps
+	# then via sed remove the "Content-type" header
+	# and remove the end links from the "Section" header
+	# and remove some separators and stray single-chars
+	# and remove some unneeded parts of the tail end
+	# then squeeze all multiple empty lines
+	sed -e 's, at LIBEXECDIR@,$(patsubst $(DESTDIR)%,%,$(libexecdir)),g' \
+		-e 's, at DATADIR@,$(patsubst $(DESTDIR)%,%,$(datadir)),g' \
+		-e 's, at DOCDIR@,$(patsubst $(DESTDIR)%,%,$(docdir)),g' \
+		-e 's, at APPNAME@,$(appname),g' \
+		-e 's, at CAPPNAME@,$(cappname),g' $< | \
+		man2html | \
+		pandoc -f html -t mediawiki | \
+		sed -e '1,2d' \
+			-e 's/\(^Section:[^[]*\).*/\1/' \
+			-e '/-----/d' \
+			-e '/^.$$/d' \
+			-e '/== SEE ALSO ==/,$$d' | \
+		cat -s > $@
+
+wiki-manpages: ../doc/wiki-manpage-$(APPNAME).6.txt ../doc/wiki-manpage-$(APPNAME)-server.6.txt
+
+../doc/varsinfo-all.txt: install-client
+	RE_TEMPHOME="$$(mktemp -d)"; \
+		../redeclipse.sh -h"$$RE_TEMPHOME" -df0 -w640 -dh480 -du0 -x"writevarsinfo; quit"; \
+		mv "$$RE_TEMPHOME/varsinfo.txt" $@; \
+		rm -r "$$RE_TEMPHOME"
+
+../doc/varsinfo-weapon-%.txt: ../doc/varsinfo-all.txt
+	# check if beginning matches weapon name
+	awk 'match($$1, /^$*/) {print}' $^ > $@
+
+../doc/varsinfo-non-weapons.txt: ../doc/varsinfo-all.txt
+	# don't match weapons, do match VAR, FVAR and SVAR types
+	awk '!match($$1, /^('"$$(echo $(weapon-names) | tr ' ' '|' )"')/) && \
+		(match($$2, 0) || match($$2, 1) || match($$2, 2)) \
+		{print}' $^ > $@
+
+../doc/varsinfo-client-and-admin.txt: ../doc/varsinfo-all.txt
+	# don't match weapons, commands
+	# do match client or admin flags
+	# overlaps world vars
+	gawk '!match($$1, /^('"$$(echo $(weapon-names) | tr ' ' '|' )"')/) && \
+		!match($$2, "3") && \
+		!and($$3, lshift(1, 3)) && \
+		and($$3, or(lshift(1, 6), lshift(1,9))) \
+		{print}' $^ > $@
+
+../doc/varsinfo-textures.txt: ../doc/varsinfo-all.txt
+	# check if texture flag is set
+	# overlaps weapon tex vars
+	gawk 'and($$3, lshift(1, 5)) {print}' $^ > $@
+
+../doc/varsinfo-world.txt: ../doc/varsinfo-all.txt
+	# check if world flag is set
+	# overlaps client-and-admin vars
+	gawk 'and($$3, lshift(1, 3)) {print}' $^ > $@
+
+../doc/varsinfo-commands.txt: ../doc/varsinfo-all.txt
+	# check if type is == 3
+	awk 'match($$2, "3") {print}' $^ > $@
+
+../doc/varsinfo-aliases.txt: ../doc/varsinfo-all.txt
+	# check if type is == 4
+	awk 'match($$2, "4") {print}' $^ > $@
+
+../doc/wiki-%.txt: ../doc/varsinfo-%.txt
+	scripts/wiki-convert $^ > $@
+
+../doc/wiki-all-vars-commands.txt: ../doc/varsinfo-all.txt
+	scripts/wiki-convert $^ > $@
+
+wiki-all-vars-commands: ../doc/wiki-all-vars-commands.txt
+
+wiki-weapons: $(weapon-wiki-pages)
+
+wiki-non-weapons: ../doc/wiki-non-weapons.txt
+
+wiki-client-and-admin: ../doc/wiki-client-and-admin.txt
+
+wiki-textures: ../doc/wiki-textures.txt
+
+wiki-world: ../doc/wiki-world.txt
+
+wiki-commands: ../doc/wiki-commands.txt
+
+wiki-aliases: ../doc/wiki-aliases.txt
+
+wiki-all: ../doc/wiki-all-vars-commands.txt $(weapon-wiki-pages) ../doc/wiki-non-weapons.txt ../doc/wiki-client-and-admin.txt ../doc/wiki-textures.txt ../doc/wiki-world.txt ../doc/wiki-commands.txt ../doc/wiki-aliases.txt
+
+wiki-clean:
+	rm -f ../doc/varsinfo-*.txt ../doc/wiki-*.txt

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/redeclipse.git



More information about the Pkg-games-commits mailing list